Android效能優化-方法耗時
方法耗時
在我之前的部落格如何優雅的檢測主執行緒中的耗時方法中分析了:利用Android系統的訊息機制原理去檢測主執行緒中的耗時方法,其實對於執行方法引起的效能開銷主要分兩類:
- 執行時間長的方法。
- 執行次數多的方法。
對於這兩類方法,可以使用工具Traceview(在Android Studio 3.0 版本和以下版本中使用)和CPU Profiler(Android Studio 3.1版本以上使用)進行分析。
由於Traceview已經被拋棄,所以這裡不再做介紹。
例子:
模擬在Activity中執行時間長的方法和次數多的方法:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.activity_main_test1).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { method1(); } }); findViewById(R.id.activity_main_test2).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { for (int i = 0; i < 20; i++) { method2(); } } }); } //執行次數多的方法 private void method2() { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } // 執行時間長的方法 private void method1() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }
上面程式碼主要是測試點選按鈕讓主執行緒睡一秒的兩種方法,使用CPU Profiler分析如下:
Call Chart 標籤:
圖1.Call Chart 標籤
Call Chart 標籤提供函式跟蹤的圖形表示形式,其中,水平軸表示函式呼叫(或呼叫方)的時間段和時間,並沿垂直軸顯示其被呼叫者。 對系統 API 的函式呼叫顯示為橙色,對應用自有函式的呼叫顯示為綠色,對第三方 API(包括 Java 語言 API)的函式呼叫顯示為藍色。
通過上面圖1中的Call Chart 標籤,可以看到函式從上到下的棧呼叫軌跡:
com.android.internal.os.ZygoteLnit.main ->......-> android.app.ActivityThread,main -> ...... android.os.Looper.loop -> ...... -> MainActivity.onClick -> ...... -> MainActivity.method1 -> ......
Flame Chart 標籤:
圖2.Flame Chart 標籤
上面圖2中顯示了Flame Chart 標籤,Flame Chart 標籤提供一個倒置的呼叫圖表,其彙總相同的呼叫堆疊。 即,收集共享相同呼叫方順序的完全相同的函式,並在火焰圖中用一個較長的橫條表示它們(而不是將它們顯示為多個較短的橫條,如呼叫圖表中所示)。 這樣更方便您檢視哪些函式消耗最多時間。 不過,這也意味著水平軸不再代表時間線,相反,它表示每個函式相對的執行時間。
呼叫Thread類方法的相同呼叫堆疊:
android.os.Handler.dispatchMessage -> ...... -> android.view.View.performClick -> MainActivity.onClick -> ...... -> MainActivity.method1 -> Thread.sleep -> ......
android.os.Handler.dispatchMessage -> ...... -> android.view.View.performClick -> MainActivity.onClick -> ...... -> MainActivity.method2 -> Thread.sleep -> ......
呼叫MainActivity類方法的相同呼叫堆疊:
android.os.Handler.dispatchMessage -> ...... -> android.view.View.performClick -> MainActivity.onClick -> ...... -> MainActivity.method1
android.os.Handler.dispatchMessage -> ...... -> android.view.View.performClick -> MainActivity.onClick -> ...... -> MainActivity.method2
Top Down 標籤:
圖3.Top Down 標籤
上面圖3顯示Top Down 標籤,Top Down 標籤顯示一個函式呼叫列表,在該列表中展開函式節點會顯示函式的被呼叫方。
從上面可以看到method2()方法執行的時間是:1006692us。
另外:Top Down 標籤還提供了在每個函式呼叫上所花費的 CPU 時間(時間也可以用執行緒總時間佔所選時間範圍的持續時間的百分比表示):
- Self: 表示函式呼叫在執行自己的程式碼(而非被呼叫方的程式碼)上所花的時間,如下圖中的函式 D 所示。
- Children: 表示函式呼叫在執行自己的被呼叫方(而非自己的程式碼)上所花的時間,如下圖中的函式 D 所示。
- 總和: 函式的 Self 和 Children 時間的總和。 這表示應用在執行函式呼叫上所花的總時間,如下圖 中函式 D 所示
Bottom Up標籤:
圖4. Bottom Up標籤
上面圖4顯示了Bottom Up標籤, Bottom Up 標籤顯示一個函式呼叫列表,在該列表中展開函式節點將顯示函式的呼叫方。
從上面可以看到函式method1的呼叫方。另外,Bottom Up 標籤還用於按照消耗最多(最少)CPU 時間排序函式,上面函式列表從上到下,函式執行時間以此遞減。
Top Down 標籤和Bottom Up標籤的區別:
圖5. 一個“Top Down”樹。
圖6. 圖 5 中函式 C 的“Bottom Up”樹。
如圖 5 所示,在“Top Down”標籤中展開函式 A 的節點可顯示它的被呼叫方,即函式 B 和 D。 然後,展開函式 D 的節點可顯示它的被呼叫方,即函式 B 和 C 等等。 與 Flame chart 標籤相似,“Top Down”樹彙總共享相同呼叫堆疊的相同函式的跟蹤資訊。 也就是說,Flame chart 標籤可提供Top down 標籤的圖形化表示形式。
圖 6 為函式 C 提供了一個“Bottom Up”樹。 在“Bottom Up”樹中開啟函式 C 的節點可顯示它獨有的呼叫方,即函式 B 和 D。 請注意,儘管 B 呼叫 C 兩次,但在“Bottom Up”樹中展開函式 C 的節點時,B 僅顯示一次。 然後,展開 B 的節點顯示其呼叫方,即函式 A 和 D。
使用輔助類來幫助執行時間長的方法
執行時間長的方法,可以放在子執行緒中去執行。在Android中提供了IntentService,HandlerThread,AsyncTask類,在Java中也提供了ThreadPoolExecutor類來幫助執行時間長的方法。
HandlerThread
用於啟動具有looper的新執行緒的方便類。 然後可以使用looper來建立處理程式類。 請注意,仍然必須呼叫start()。
HandlerThread繼承自Thread類,在 run方法中建立了一個當前執行緒的Looper並呼叫了訊息迴圈loop()方法:
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}
HandlerThread可以做定時,延時或者無限迴圈的任務。
IntentService
IntentService是服務的基類,可根據需要處理非同步請求(表示為Intents)。 客戶端通過Context.startService(Intent)呼叫傳送請求; 服務根據需要啟動,使用工作執行緒依次處理每個Intent,並在工作失敗時自行停止。
這種“工作佇列處理器”模式通常用於從應用程式的主執行緒解除安裝任務。 存在IntentService類以簡化此模式並處理機制。 要使用它,請擴充套件IntentService並實現onHandleIntent(Intent)。 IntentService將接收Intents,啟動工作執行緒,並根據需要停止服務。
所有請求都在一個工作執行緒上處理 - 它們可能需要多長時間(並且不會阻止應用程式的主迴圈),但一次只能處理一個請求。
由於IntentService是一個Service,所以需要在AndroidManifest.xml配置。
IntentService內部使用了HandlerThread:
private volatile Looper mServiceLooper;
private volatile ServiceHandler mServiceHandler;
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}
@Override
public void onCreate() {
// TODO: It would be nice to have an option to hold a partial wakelock
// during processing, and to have a static startService(Context, Intent)
// method that would launch the service & hand off a wakelock.
super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}
@Override
public void onStart(@Nullable Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}
在HandlerThread類中的子執行緒中執行任務,任務來自Intent(必須是一個Inteng),執行任務後會停止自己。
IntentService適合執行時間長的耗時任務,比如下載,上傳,複雜計算等。
AsyncTask
AsyncTask可以正確,方便地使用UI執行緒。 此類允許您執行後臺操作並在UI執行緒上釋出結果,而無需操作執行緒和/或處理程式。
AsyncTask被設計為圍繞Thread和Handler的助手類,並不構成通用的執行緒框架。 理想情況下,AsyncTasks應該用於短操作(最多幾秒鐘)。如果需要保持執行緒長時間執行,強烈建議您使用java.util.concurrent包提供的各種API,例如 Executor,ThreadPoolExecutor和FutureTask。
非同步任務由在後臺執行緒上執行的計算定義,其結果在UI執行緒上釋出。 非同步任務由3種泛型型別定義,稱為Params,Progress和Result,以及4個步驟,稱為onPreExecute,doInBackground,onProgressUpdate和onPostExecute。
AsyncTask內部實現是THREAD_POOL_EXECUTOR+SERIAL_EXECUTOR+Handler:
- THREAD_POOL_EXECUTOR:可用於並行執行任務的Executor。
- SERIAL_EXECUTOR:用於任務排隊的Executor。
- Handler:將執行緒切換到主執行緒。
執行任務的THREAD_POOL_EXECUTOR:
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));//執行緒池大小:最少2個執行緒,最多4個執行緒。
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;//最大執行緒數
private static final int KEEP_ALIVE_SECONDS = 30;//執行緒閒置時,30秒後被回收
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);//大小為128的有界佇列
public static final Executor THREAD_POOL_EXECUTOR;
static {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
sPoolWorkQueue, sThreadFactory);
threadPoolExecutor.allowCoreThreadTimeOut(true);//核心執行緒閒置時,超時也會被回收
THREAD_POOL_EXECUTOR = threadPoolExecutor;
}
用於任務排隊的SERIAL_EXECUTOR:
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static class SerialExecutor implements Executor {
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();//用於任務排隊的佇列
Runnable mActive;
public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();//執行當前任務
} finally {
scheduleNext();//去執行下一個任務
}
}
});
if (mActive == null) {
scheduleNext();//去執行下一個任務
}
}
protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}
執行緒池
Executors提供的工廠方法:
- newFixedThreadPool:執行緒數量固定的執行緒池,當執行緒處於空閒時,它們並不會回收,除非執行緒池被關閉了。當所有的執行緒處於活動狀態時,新任務都會處於等待狀態,直到執行緒空閒出來。只有核心執行緒,沒有非核心執行緒,最大執行緒量為核心執行緒數量,執行緒閒置時沒有超時時長,使用的佇列是LinkedBlockingQueue。
- newCachedThreadPool:執行緒數量不定的執行緒,最大執行緒數量為Integer.MAX_VALUE。當執行緒池中的執行緒都處於活動狀態時,執行緒池會建立新的執行緒來處理新任務,否則就會利用空閒的執行緒來處理新任務。執行緒池中的執行緒都有超時機制,這個超時時長為60秒,超過執行緒就會被回收。 沒有核心執行緒,只有非核心執行緒,最大執行緒量為Integer.MAX_VALUE,執行緒閒置時超時時長為60秒,使用的佇列是SynchronoseQueue。
- newScheduledThreadPool:核心執行緒數量固定,非核心執行緒數量沒有限制,並且非核心執行緒閒置時就會被回收。有核心執行緒,也有非核心執行緒,最大執行緒量為Integer.MAX_VALUE,執行緒閒置時超時時長為0秒,使用的佇列是DelayWorkQueue。
- newSingleThreadExecutor:只有一個核心執行緒,確保所有的任務都在同一個執行緒中按順序執行。有核心執行緒,沒有非核心執行緒,最大執行緒量為1,執行緒閒置時沒有超時時長,使用的佇列是LinkedBlockingQueue。
建立執行緒池ThreadPoolExecutor:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- corePoolSize:核心執行緒。
- maximumPoolSize:最大執行緒數。
- keepAliveTime:執行緒閒置時的存活時間。
- unit:存活時間的時間單位。
- workQueue:儲存任務的佇列。
- threadFactory:執行緒工廠。
- handler:飽和策略。AbortPolicy (中止策略,預設方式,該策略會丟擲未檢查異常 RejectedExecutionException), CallerRunsPolicy(呼叫著執行策略實現了一種調節機制,該策略既不會拋棄任務,也不會丟擲異常,而是將某些任務回退到呼叫著,從而降低新任務的流量) , DiscardPolicy(當新提價的任務無法儲存到佇列中等待執行時,拋棄策略會悄悄拋棄該任務) , DiscardOldestPolicy(拋棄最舊的策略則會拋棄下一個將被執行的任務,然後嘗試重新提交新的任務)。
在執行次數多的方法中應避免的事項
1.避免建立大量的物件
在自定義View時,通常會重寫onMeasure,onLayout,onDraw方法,這些方法在View的生命週期過程中,通常會被呼叫多次,所以應該避免在方法中建立大量的對像,能夠定義為全域性物件的就定義為全域性物件。
2.及時移除回撥監聽介面
ViewTreeObserver.OnGlobalLayoutListener監聽器:
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//do somthing
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}else{
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
}
});
RecyclerView.OnScrollListener監聽器:
recyclerView.clearOnScrollListeners();
ViewPager.OnPageChangeListener監聽器:
viewPager.clearOnPageChangeListeners();
3.不要做複雜的計算
在頻繁的被回撥的方法中不要做複雜的計算。
RecyclerView滑動的時候,RecyclerView.OnScrollListener監聽器的onScrolled方法會被不斷的回撥:
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
}
});
輸入文字時,TextWatcher監聽器的方法會不斷的被回撥:
editText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
}
});