我們都知道Android使用消息機制進行UI更新,UI線程也就是主線程里有個Looper,在其loop()方法中會不斷取出message,調用其綁定的Handler在主線程執行。如果在handler的dispatchMesaage方法里有耗時操作,就會發生卡頓。
我們來看下Looper.loop()的源碼
/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
final long traceTag = me.mTraceTag;
if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
try {
msg.target.dispatchMessage(msg);
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}
msg.recycleUnchecked();
}
}
只要檢測 msg.target.dispatchMessage(msg) 的執行時間,就能檢測到主線程是否有耗時操作。注意到這行執行代碼的前后,有兩個logging.println函數,如果設置了mLogging,會分別打印出”>>>>> Dispatching to “和”<<<<< Finished to “這樣的日志,這樣我們就可以通過兩次log的時間差值,來計算dispatchMessage的執行時間,從而設置閾值判斷是否發生了卡頓。
如何給Looper設置mLogging?
在源碼中定義了私有的mLogging,但是提供了賦值方法。
private Printer mLogging;
public void setMessageLogging(@Nullable Printer printer) {
mLogging = printer;
}
我們可以自己實現一個Printer并賦值。
Looper.getMainLooper().setMessageLogging(new Printer() {
private static final String START = ">>>>> Dispatching";
private static final String END = "<<<<< Finished";
@Override
public void println(String x) {
if (x.startsWith(START)) {
//從這里開啟一個定時任務來打印方法的堆棧信息
}
if (x.startsWith(END)) {
//從這里取消定時任務
}
}
});
我們設定一個閾值為1000ms,當匹配到>>>>> Dispatching時,開啟定時任務,會在1000ms 后執行任務,這個任務負責打印UI線程的堆棧信息。如果消息低于1000ms內執行完成,就可以匹配到<<<<< Finished日志,那么在打印堆棧任務啟動前執行取消了這個任務,則認為沒有卡頓的發生;如果消息超過1000ms才執行完畢,此時認為發生了卡頓,并打印UI線程的堆棧信息。
看下定時任務的代碼實現
public class LooperLog {
private static LooperLog sInstance = new LooperLog();
private HandlerThread mLogThread = new HandlerThread("log");
private Handler mIoHandler;
private static final long TIME_BLOCK = 1000L;
private LooperLog() {
mLogThread.start();
mIoHandler = new Handler(mLogThread.getLooper());
}
private static Runnable mLogRunnable = new Runnable() {
@Override
public void run() {
StringBuilder sb = new StringBuilder();
StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
for (StackTraceElement s : stackTrace) {
sb.append(s.toString() + "\n");
}
Log.i("LogPrinter--", sb.toString());
}
};
public static LooperLog getInstance() {
return sInstance;
}
public void startPrintLog() {
mIoHandler.postDelayed(mLogRunnable, TIME_BLOCK);
}
public void canclePrintLog() {
mIoHandler.removeCallbacks(mLogRunnable);
}
}
這里我們使用HandlerThread來構造一個Handler,HandlerThread繼承自Thread,實際上就一個Thread,只不過比普通的Thread多了一個Looper,對外提供自己這個Looper對象的getLooper方法,然后創建Handler時將HandlerThread中的looper對象傳入。這樣我們的mIoHandler對象就是與HandlerThread這個非UI線程綁定的了,它處理耗時操作將不會阻塞UI。如果UI線程阻塞超過1000ms,就會在子線程中執行mLogRunnable,打印出UI線程當前的堆棧信息,如果處理消息沒有超過1000ms,則會實時的remove掉這個mLogRunnable任務。
發生卡頓時打印出堆棧信息的大致內容如下,開發可以通過log定位耗時的地方。
LogPrinter--: java.lang.Thread.sleep(Native Method)
java.lang.Thread.sleep(Thread.java:1031)
java.lang.Thread.sleep(Thread.java:985)
com.koolearn.android.CommonPperationImpl.startActivityAfterLogin(CommonPperationImpl.java:124)
com.koolearn.android.home.MainActivity.onClick(MainActivity.java:490)
android.view.View.performClick(View.java:4811)
android.view.View$PerformClick.run(View.java:20136)
android.os.Handler.handleCallback(Handler.java:815)
android.os.Handler.dispatchMessage(Handler.java:104)
android.os.Looper.loop(Looper.java:194)
android.app.ActivityThread.main(ActivityThread.java:5546)
java.lang.reflect.Method.invoke(Native Method)
java.lang.reflect.Method.invoke(Method.java:372)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:964)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:759)