標(biāo)簽: Choreographer UI卡頓 UI丟幀
作者公眾號(hào):
本文將介紹3個(gè)知識(shí)點(diǎn):
- 獲取系統(tǒng)UI刷新頻率
- 檢測(cè)UI丟幀和卡頓
-
輸出UI丟幀和卡頓堆棧信息
Choreographer.jpg
系統(tǒng)UI刷新頻率
Android系統(tǒng)每隔16ms重繪UI界面,16ms是因?yàn)锳ndroid系統(tǒng)規(guī)定UI繪圖的刷新頻率60FPS归露。Android系統(tǒng)每隔16ms靶擦,發(fā)送一個(gè)系統(tǒng)級(jí)別信號(hào)VSYNC喚起重繪操作雇毫。1秒內(nèi)繪制UI界面60次棚放。每16ms為一個(gè)UI界面繪制周期。
現(xiàn)在有些手機(jī)廠商的手機(jī)屏幕刷新頻率已經(jīng)是120FPS馍迄,每隔8.3毫秒重繪UI界面攀圈;
獲取系統(tǒng)UI刷新頻率
private float getRefreshRate() { //獲取屏幕主頻頻率
Display display = getWindowManager().getDefaultDisplay();
float refreshRate = display.getRefreshRate();
Log.d(TAG, "屏幕主頻頻率 =" + refreshRate);
return refreshRate;
}
log打印如下:
D/MainActivity: 屏幕主頻頻率 =60.0
UI丟幀和卡頓檢查-Choreographer
平常所說(shuō)的“丟幀”情況峦甩,并不是真的把繪圖的幀給“丟失”了,也而是UI繪圖的操作沒有和系統(tǒng)16ms的繪圖更新頻率步調(diào)一致犬辰,開發(fā)者代碼在繪圖中繪制操作太多,導(dǎo)致操作的時(shí)間超過(guò)16ms灸促,在Android系統(tǒng)需要在16ms時(shí)需要重繪的時(shí)刻由于UI線程被阻塞而繪制失敗浴栽。如果丟的幀數(shù)量是一兩幀,用戶在視覺上沒有明顯感覺吃度,但是如果超過(guò)3幀椿每,用戶就有視覺上的感知英遭。丟幀數(shù)如果再持續(xù)增多挖诸,在視覺上就是所謂的“卡頓”多律。
丟幀是引起卡頓的重要原因。在Android中可以通過(guò)Choreographer檢測(cè)Android系統(tǒng)的丟幀情況辽装。
public class MainActivity extends Activity {
...
private MyFrameCallback mFrameCallback = new MyFrameCallback();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Choreographer.getInstance().postFrameCallback(mFrameCallback);
MYTest();
button = findViewById(R.id.bottom);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
uiLongTimeWork();
Log.d(MainActivity.class.getSimpleName(), "button click");
}
});
}
private void MYTest() {
setContentView(R.layout.activity_main);
Log.d(MainActivity.class.getSimpleName(), "MYTest");
}
private float getRefreshRate() { //獲取屏幕主頻頻率
Display display = getWindowManager().getDefaultDisplay();
float refreshRate = display.getRefreshRate();
// Log.d(TAG, "屏幕主頻頻率 =" + refreshRate);
return refreshRate;
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
public class MyFrameCallback implements Choreographer.FrameCallback {
private String TAG = "性能檢測(cè)";
private long lastTime = 0;
@Override
public void doFrame(long frameTimeNanos) {
if (lastTime == 0) {
//代碼第一次初始化。不做檢測(cè)統(tǒng)計(jì)丰涉。
lastTime = frameTimeNanos;
} else {
long times = (frameTimeNanos - lastTime) / 1000000;
int frames = (int) (times / (1000/getRefreshRate()));
if (times > 16) {
Log.w(TAG, "UI線程超時(shí)(超過(guò)16ms):" + times + "ms" + " , 丟幀:" + frames);
}
lastTime = frameTimeNanos;
}
Choreographer.getInstance().postFrameCallback(mFrameCallback);
}
}
private void uiLongTimeWork() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Choreographer周期性的在UI重繪時(shí)候觸發(fā)一死,在代碼中記錄上一次和下一次繪制的時(shí)間間隔投慈,如果超過(guò)16ms,就意味著一次UI線程重繪的“丟幀”瘩绒。丟幀的數(shù)量為間隔時(shí)間除以16,如果超過(guò)3锁荔,就開始有卡頓的感知蟀给。
Log如下
W/性能檢測(cè): UI線程超時(shí)(超過(guò)16ms):33ms , 丟幀:1
W/性能檢測(cè): UI線程超時(shí)(超過(guò)16ms):19ms , 丟幀:1
W/性能檢測(cè): UI線程超時(shí)(超過(guò)16ms):1016ms , 丟幀:60
W/性能檢測(cè): UI線程超時(shí)(超過(guò)16ms):24ms , 丟幀:1
W/性能檢測(cè): UI線程超時(shí)(超過(guò)16ms):21ms , 丟幀:1
W/性能檢測(cè): UI線程超時(shí)(超過(guò)16ms):1016ms , 丟幀:60
W/性能檢測(cè): UI線程超時(shí)(超過(guò)16ms):23ms , 丟幀:1
W/性能檢測(cè): UI線程超時(shí)(超過(guò)16ms):33ms , 丟幀:1
如果手動(dòng)點(diǎn)擊按鈕故意阻塞1秒,丟棄的幀數(shù)更多阳堕。丟幀:60跋理,就是點(diǎn)擊button按鈕,執(zhí)行uiLongTimeWork產(chǎn)生的恬总;
UI丟幀和卡頓堆棧信息輸出
以上是“UI丟幀和卡頓檢查-Choreographer”使用Android的Choreographer監(jiān)測(cè)App發(fā)生的UI卡頓丟幀問題前普。Choreographer本身依賴于Android主線程的Looper消息機(jī)制。
發(fā)生在Android主線程的每(1000/UI刷新頻率)ms重繪操作依賴于Main Looper中消息的發(fā)送和獲取壹堰。如果App一切運(yùn)行正常拭卿,無(wú)卡頓無(wú)丟幀現(xiàn)象發(fā)生,那么開發(fā)者的代碼在主線程Looper消息隊(duì)列中發(fā)送和接收消息的時(shí)間會(huì)很短峻厚,理想情況是(1000/UI刷新頻率)ms惠桃,這是也是Android系統(tǒng)規(guī)定的時(shí)間。但是呐馆,如果一些發(fā)生在主線程的代碼寫的太重摹恰,執(zhí)行任務(wù)花費(fèi)時(shí)間太久,就會(huì)在主線程延遲Main Looper的消息在(1000/UI刷新頻率)ms尺度范圍內(nèi)的讀和寫遣耍。
先看下Android官方實(shí)現(xiàn)的Looper中l(wèi)oop()函數(shù)代碼官方實(shí)現(xiàn):
/**
* 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 slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
final long traceTag = me.mTraceTag;
if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
final long end;
try {
msg.target.dispatchMessage(msg);
end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (slowDispatchThresholdMs > 0) {
final long time = end - start;
if (time > slowDispatchThresholdMs) {
Slog.w(TAG, "Dispatch took " + time + "ms on "
+ Thread.currentThread().getName() + ", h=" +
msg.target + " cb=" + msg.callback + " msg=" + msg.what);
}
}
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();
}
}
在loop()函數(shù)中赊豌,Android完成了Looper消息隊(duì)列的分發(fā)碘饼,在分發(fā)消息開始,會(huì)打印一串log日志:
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
同時(shí)在消息處理結(jié)束后也會(huì)打印一串消息日志:
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
正常的情況下钠绍,分發(fā)消息開始到消息結(jié)束柳爽,理想的情況下應(yīng)該在(1000/UI刷新頻率)ms以內(nèi)。但是分發(fā)處理的消息到上層争拐,由開發(fā)者代碼接管并處理架曹,如果耗時(shí)太久,就很可能超出(1000/UI刷新頻率)ms万牺,也即發(fā)生了丟幀脚粟,超時(shí)太多核无,由于Android系統(tǒng)依賴主線程Looper重繪UI的消息遲遲得不到處理噪沙,那么就導(dǎo)致繪圖動(dòng)作停滯正歼,用戶視覺上就會(huì)感受到卡頓朋腋。
利用這一特性和情景,可以使用主線程的Looper監(jiān)測(cè)系統(tǒng)發(fā)生的卡頓和丟幀穷绵。具體是這樣的:
首先給App的主線程Looper注冊(cè)一個(gè)自己的消息日志輸出打印器仲墨,正常情況下,該日志打印器將輸出全部的Android Looper上的日志癌蚁,但是在這里,技巧性的過(guò)濾兩個(gè)特殊日志:
>>>>> Dispatching to
表示Looper開始分發(fā)主線程上的消息咬摇。
<<<<< Finished to
表示Looper分發(fā)主線程上的消失結(jié)束逸邦。
從>>>>> Dispatching to 到 <<<<< Finished to 之間這段操作,就是留給開發(fā)者所寫的代碼發(fā)生在上層主線程操作的動(dòng)作烛卧,通常所說(shuō)的卡頓也就發(fā)生這一段。
正確情況下局雄,從消息分發(fā)(>>>>> Dispatching to)開始炬搭,到消息處理結(jié)束(<<<<< Finished to),這段操作理想情況應(yīng)在(1000/UI刷新頻率)ms以內(nèi)完成灼芭,如果超過(guò)這一時(shí)間彼绷,也即意味著卡頓和丟幀。
現(xiàn)在設(shè)計(jì)一種技巧性的編程方案:在(>>>>> Dispatching to)開始時(shí)候猜旬,延時(shí)一定時(shí)間(THREAD_HOLD)執(zhí)行一個(gè)線程,延時(shí)時(shí)間為THREAD_HOLD秘遏,該線程只完成打印當(dāng)前Android堆棧的信息。THREAD_HOLD即為開發(fā)者意圖捕捉到的超時(shí)時(shí)間倦蚪。如果沒什么意外,該線程在THREAD_HOLD后慕购,就打印出當(dāng)前Android的堆棧信息获洲。巧就巧妙在利用這一點(diǎn)兒,因?yàn)檠訒r(shí)THREAD_HOLD執(zhí)行的線程和主線程Looper中的線程是并行執(zhí)行的门岔,當(dāng)在>>>>> Dispatching to時(shí)刻把延時(shí)線程任務(wù)構(gòu)建完拋出去等待THREAD_HOLD后執(zhí)行,而當(dāng)前的Looper線程中的消息分發(fā)也在執(zhí)行牢裳,這兩個(gè)是并發(fā)執(zhí)行的不同線程蒲讯。
設(shè)想如果Looper線程中的操作代碼很快就執(zhí)行完畢,不到16ms就到了<<<<< Finished to晦墙,那么毫無(wú)疑問當(dāng)前的主線程無(wú)卡頓和丟幀發(fā)生。如果特意把THREAD_HOLD設(shè)置成大于16ms的延時(shí)時(shí)間抗楔,比如1000ms,如果線程運(yùn)行順暢不卡頓無(wú)丟幀,那么從>>>>> Dispatching to到達(dá)<<<<< Finished to后勺良,把延時(shí)THREAD_HOLD執(zhí)行的線程刪除掉腰池,那么線程就不會(huì)輸出任何堆棧信息讳侨。若不行主線程發(fā)生阻塞,當(dāng)從>>>>> Dispatching to到達(dá)<<<<< Finished to花費(fèi)1000ms甚至更長(zhǎng)時(shí)間后勇婴,而由于到達(dá)<<<<< Finished to的時(shí)候沒來(lái)得及把堆棧打印線程刪除掉,因此就輸出了當(dāng)前堆棧信息,此堆棧信息剛好即為發(fā)生卡頓和丟幀的代碼堆棧添诉,正好就是所需的卡頓和丟幀檢測(cè)代碼。
public class MainActivity extends Activity {
...
private CheckTask mCheckTask = new CheckTask();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
check();
...
button = findViewById(R.id.bottom);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
uiLongTimeWork();
Log.d(MainActivity.class.getSimpleName(), "button click");
}
});
}
private void check() {
Looper.getMainLooper().setMessageLogging(new Printer() {
private final String START = ">>>>> Dispatching to";
private final String END = "<<<<< Finished to";
@Override
public void println(String s) {
if (s.startsWith(START)) {
mCheckTask.start();
} else if (s.startsWith(END)) {
mCheckTask.end();
}
}
});
}
private void uiLongTimeWork() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private class CheckTask {
private HandlerThread mHandlerThread = new HandlerThread("卡頓檢測(cè)");
private Handler mHandler;
private final int THREAD_HOLD = 1000;
public CheckTask() {
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
}
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
log();
}
};
public void start() {
mHandler.postDelayed(mRunnable, THREAD_HOLD);
}
public void end() {
mHandler.removeCallbacks(mRunnable);
}
}
/**
* 輸出當(dāng)前異常或及錯(cuò)誤堆棧信息捎稚。
*/
private void log() {
StringBuilder sb = new StringBuilder();
StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
for (StackTraceElement s : stackTrace) {
sb.append(s + "\n");
}
Log.w(TAG, sb.toString());
}
運(yùn)行輸出:
1970-02-14 17:35:06.367 11590-11590/com.yanbing.aop_project D/MainActivity: button click
1970-02-14 17:35:06.367 11590-11611/com.yanbing.aop_project W/MainActivity: java.lang.String.indexOf(String.java:1658)
java.lang.String.indexOf(String.java:1638)
java.lang.String.contains(String.java:2126)
java.lang.Class.classNameImpliesTopLevel(Class.java:1169)
java.lang.Class.getEnclosingConstructor(Class.java:1159)
java.lang.Class.isLocalClass(Class.java:1312)
java.lang.Class.getSimpleName(Class.java:1219)
com.yanbing.aop_project.MainActivity$2.onClick(MainActivity.java:71)
android.view.View.performClick(View.java:6294)
android.view.View$PerformClick.run(View.java:24770)
android.os.Handler.handleCallback(Handler.java:790)
android.os.Handler.dispatchMessage(Handler.java:99)
android.os.Looper.loop(Looper.java:164)
android.app.ActivityThread.main(ActivityThread.java:6494)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
可以看到當(dāng)點(diǎn)擊按鈕故意制造一個(gè)卡頓后,卡頓被檢測(cè)到蒲凶,并且輸出和定位到了卡頓的具體代碼位置。
總結(jié):利用主線程的Looper檢測(cè)卡頓和丟幀灵巧,從成對(duì)的消息分發(fā)(>>>>> Dispatching to)融欧,到消息處理結(jié)束(<<<<< Finished to)权她,正常的理想時(shí)間應(yīng)該在16ms以內(nèi),若當(dāng)前代碼耗時(shí)太多,這一段時(shí)間就會(huì)超過(guò)16ms廓啊。假設(shè)現(xiàn)在要檢測(cè)耗時(shí)超過(guò)1秒(1000ms)的耗時(shí)操作,那就在>>>>> Dispatching to時(shí)刻疮装,拋出一個(gè)延時(shí)執(zhí)行的線程,該線程打印當(dāng)前堆棧的信息,延時(shí)的時(shí)間特意設(shè)置成閾值1000专缠。此種情況下藤肢,正常順暢執(zhí)行無(wú)卡頓無(wú)丟幀的代碼從>>>>> Dispatching to到<<<<< Finished to之間不會(huì)超過(guò)設(shè)置的閾值1000ms蟀淮,因此當(dāng)Looper中的代碼到達(dá)<<<<< Finished to就把之前拋出來(lái)延時(shí)執(zhí)行的線程刪除掉,也就不會(huì)輸出任何堆棧信息策治。但是只有當(dāng)耗時(shí)代碼從>>>>> Dispatching to到<<<<< Finished to超過(guò)了1000ms混蔼,由于Looper中由于耗時(shí)操作很晚(超過(guò)我們?cè)O(shè)定的閾值)才到達(dá)<<<<< Finished to遵湖,沒趕上刪掉堆棧打印線程,于是堆棧線程得以有機(jī)會(huì)打印當(dāng)前堆棧信息烹卒,這就是卡頓和丟幀的發(fā)生場(chǎng)景檢測(cè)機(jī)制。
事實(shí)上可以靈活設(shè)置延時(shí)閾值THREAD_HOLD,從而檢測(cè)到任何大于或等于該時(shí)間的耗時(shí)操作谣辞。