UI 優(yōu)化系列專題,來聊一聊 Android 渲染相關知識,主要涉及 UI 渲染背景知識、如何優(yōu)化 UI 渲染兩部分內(nèi)容。
UI 優(yōu)化系列專題
- UI 渲染背景知識
《View 繪制流程之 setContentView() 到底做了什么?》
《View 繪制流程之 DecorView 添加至窗口的過程》
《深入 Activity 三部曲(3)View 繪制流程》
《Android 之 LayoutInflater 全面解析》
《關于渲染,你需要了解什么?》
《Android 之 Choreographer 詳細分析》
- 如何優(yōu)化 UI 渲染
《Android 之如何優(yōu)化 UI 渲染(上)》
《Android 之如何優(yōu)化 UI 渲染(下)》
Android 的 UI 渲染性能是 Google 長期以來非常重視的,基本每次 Google I/O 都會花很多篇幅講這一塊。不過隨著 Android 系統(tǒng)的不斷演進和完善,時至今日,關于 Android UI 卡頓的話題也越來越少。
Google 在 2012 年的 I/O 大會上宣布了 Project Butter 計劃,那個曾經(jīng)嚴重影響 Android 口碑的 UI 流程性問題,首先在這得到有效的控制。并且在 Android 4.1 中正式開啟了這個機制。
Project Butter 主要包含三個組成部分:VSYNC、Triple Buffering 和今天要重點分析的 Choreographer。關于 Project Butter 的詳細介紹,你可以參考這里。
Choreographer
Choreographer 是 Android 4.1 新增的機制,用于配合系統(tǒng)的 VSYNC 中斷信號。它本質(zhì)是一個 Java 類,如果直譯的話為舞蹈指導,看到這個詞不得不贊嘆設計者除了 Coding 之外的廣泛視野。舞蹈是有節(jié)奏的,節(jié)奏使舞蹈的每個動作更加協(xié)調(diào)和連貫;視圖刷新也是如此,Choreographer 可以接收系統(tǒng)的 VSYNC 信號,統(tǒng)一管理應用的輸入、動畫和繪制等任務的執(zhí)行時機。業(yè)界一般通過它來監(jiān)控應用的幀率。
我們先從 Choreographer 的構造方法入手,看看 Choreographer 是如何協(xié)調(diào)任務的執(zhí)行。
private Choreographer(Looper looper, int vsyncSource) {
// 當前線程的Looper
mLooper = looper;
// 使用該Looper創(chuàng)建FrameHandler
mHandler = new FrameHandler(looper);
// 是否開啟VSYNC,開啟VSYNC后將通過FrameDisplayEventReceiver接收VSYNC脈沖信號
mDisplayEventReceiver = USE_VSYNC
? new FrameDisplayEventReceiver(looper, vsyncSource)
: null;
mLastFrameTimeNanos = Long.MIN_VALUE;
// 計算一幀的時間
// Android手機屏幕采用60Hz的刷新頻率
// 這里是納秒 ≈16000000ns 還是16ms
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
// 創(chuàng)建一個CallbackQueu的數(shù)組,默認為4
// CallbackQueue中存放要執(zhí)行的輸入、動畫、遍歷繪制等任務
// 也就是 CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
// b/68769804: For low FPS experiments.
setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));
}
- Choreographer 的構造方法被設計成私有,并且是線程單例的。只能通過其內(nèi)部的 getInstance 方法獲取當前線程的 Choreographer 實例:
public static Choreographer getInstance() {
// Choreographer線程單例的實現(xiàn)方式
return sThreadInstance.get();
}
通過 ThreadLocal 實現(xiàn) Choreographer 的線程單例。
private static final ThreadLocal<Choreographer> sThreadInstance =
new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
// 獲取當前線程的Looper對象
Looper looper = Looper.myLooper();
if (looper == null) {
// 如果當前線程未創(chuàng)建Looper對象則拋出異常
// 主線程(UI線程)的Looper默認在ActivityThread的main方法被創(chuàng)建
throw new IllegalStateException("The current thread must have a looper!");
}
// 為當前線程創(chuàng)建一個Choreographer對象
Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
if (looper == Looper.getMainLooper()) {
// 如果是UI線程賦值給成員mMainInstance
mMainInstance = choreographer;
}
return choreographer;
}
};
- Choreographer 的構造必須傳遞一個 Looper 對象,其內(nèi)部會根據(jù)該 Looper 創(chuàng)建一個 FrameHandler。Choreographer 的所有任務最終都會發(fā)送到該 Looper 所在的線程。
private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DO_FRAME:
// 執(zhí)行doFrame
// 如果啟用VSYNC機制,當VSYNC信號到來時觸發(fā)
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC:
// 申請VSYNC信號,例如當前需要繪制任務時
doScheduleVsync();
break;
case MSG_DO_SCHEDULE_CALLBACK:
// 需要延遲的任務,最終還是執(zhí)行上述兩個事件
doScheduleCallback(msg.arg1);
break;
}
}
}
- 注意 USE_VSYNC,用于判斷當前是否啟用 VSYNC 機制,Android 在 4.1 之后默認開啟該機制。
private static final boolean USE_VSYNC = SystemProperties.getBoolean(
"debug.choreographer.vsync", true);
FrameDisplayEventReceiver 是 DisplayEventReceiver 的子類,DisplayEventReceiver 是一個 abstract class。在 DisplayEventReceiver 的構造方法會通過 JNI 創(chuàng)建一個 IDisplayEventConnection 的 VSYNC 的監(jiān)聽者。
public DisplayEventReceiver(Looper looper, int vsyncSource) {
if (looper == null) {
throw new IllegalArgumentException("looper must not be null");
}
mMessageQueue = looper.getQueue();
// 注冊VSYNC信號監(jiān)聽者
mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,
vsyncSource);
mCloseGuard.open("dispose");
}
另外 DisplayEventReceiver 內(nèi)還包括用于申請 VSYNC 信號的 scheduledVsync 方法,
public void scheduleVsync() {
if (mReceiverPtr == 0) {
Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
+ "receiver has already been disposed.");
} else {
// 申請VSYNC中斷信號
// 會回調(diào)onVsync方法
nativeScheduleVsync(mReceiverPtr);
}
}
和用于接收 VSYNC 信號的 onVsync 方法。這樣,當應用需要繪制時,通過 scheduledVsync 方法申請 VSYNC 中斷,來自 EventThread 的 VSYNC 信號就可以傳遞到 Choreographer:
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
// 該方法在其子類FrameDisplayEventReceiver中被重寫
// 目的是通知Choreographer
}
- CallbackQueue,用于保存通過 postCallback 添加的任務。目前一共定義了四種任務類型,它們分別是:
- CALLBACK_INPUT:優(yōu)先級最高,和輸入事件處理有關。
- CALLBACK_ANIMATION:優(yōu)先級其次,和 Animation 的處理有關
- CALLBACK_TRAVERSAL:優(yōu)先級最低,和 UI 繪制任務有關
- CALLBACK_COMMIT:最后執(zhí)行,和提交任務有關(在 API Level 23 添加)
優(yōu)先級的高低和處理順序有關,每當收到 VSYNC 信號時,Choreographer 將首先處理 INPUT 類型的任務,然后是 ANIMATION 類型,最后才是 TRAVERSAL 類型。
通過 Choreographer 添加的任務最后都被封裝成 CallbackRecord,同種任務之間按照時間順序以鏈表的形式保存在 CallbackQueue 內(nèi)。
private static final class CallbackRecord {
// 鏈表,指向下一個
public CallbackRecord next;
// 到期時間
public long dueTime;
// Runnable or FrameCallback
public Object action;
public Object token;
public void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) {
// 通過postFrameCallback 或 postFrameCallbackDelayed
// 會執(zhí)行這里
((FrameCallback)action).doFrame(frameTimeNanos);
} else {
((Runnable)action).run();
}
}
}
CallbackQueue 是一個容量為 4 的數(shù)組,分別對應不同的任務類型。
接下來,以 View 的繪制流程為例,從 ViewRootImpl 的 scheduleTraversals 方法開始,其內(nèi)部通過 Choreographer 的 postCallback 將繪制任務添加到 Chorographer。關于 View 繪制流程的詳細分析,可以參考《View 繪制流程之 DecorView 添加至窗口的過程》和《深入 Activity 三部曲(3)之 View 繪制流程》。
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 同步屏障,阻塞所有的同步消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 注意mTraversaRunnable是一個Runnable對象
// 通過 Choreographer 發(fā)送繪制任務
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
// ...
}
}
Choreographer 是線程單例的,大家是否還記得 Android 系統(tǒng)的 Looper 對象也是 線程單例。主線程 Looper 是在 ActivityThread 的 main 方法被創(chuàng)建。如果要在子線程使用 Handler,必須先為其創(chuàng)建一個 Looper 實例。
Choreographer 提供了兩種添加任務的方式,postCallback() 和 postFrameCallback(),當然還有對應的 delay 方法。
- postCallback 對應調(diào)用 postCallbackDelayed
public void postCallbackDelayed(int callbackType,
Runnable action, Object token, long delayMillis) {
if (action == null) {
throw new IllegalArgumentException("action must not be null");
}
if (callbackType < 0 || callbackType > CALLBACK_LAST) {
throw new IllegalArgumentException("callbackType is invalid");
}
// 最終都會調(diào)用到postCallbackDelayedInternal
postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}
- postFrameCallback 對應調(diào)用 postFrameCallbackDelayed
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
//最終調(diào)用postCallbackDelayedInternal
postCallbackDelayedInternal(CALLBACK_ANIMATION,
callback, FRAME_CALLBACK_TOKEN, delayMillis);
}
postCallback 相比 postFrameCallback 更加靈活一些。
它們最終都會調(diào)用到 postCallbackDelayedInternal 方法:
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
// 當前時間
final long now = SystemClock.uptimeMillis();
// 加上延遲時間
final long dueTime = now + delayMillis;
// 根據(jù)任務類型添加到mCallbackQueues中
// VSYNC信號處理任務具有優(yōu)先級
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
//表示立即執(zhí)行,立即申請VSYNC信號
scheduleFrameLocked(now);
} else {
// 在指定時間運行,最終仍然會調(diào)用scheduleFrameLocked
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
// 到時根據(jù)callbackType在mCallbackQueues中查找執(zhí)行
msg.arg1 = callbackType;
// 消息設置為異步
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
根據(jù)任務類型 callbackType 添加到對應的 CallbackQueue 內(nèi),然后判斷任務是否有延遲,無延遲則立即執(zhí)行 scheduleFrameLocked 方法,否則發(fā)送定時消息到 FrameHandler,不過其最終還是調(diào)用到 scheduleFrameLocked 方法:
private void scheduleFrameLocked(long now) {
//mFrameScheduled默認為false
if (!mFrameScheduled) {
mFrameScheduled = true;
// 判斷是否開啟VSYNC
if (USE_VSYNC) {
// 判斷是否在原線程
if (isRunningOnLooperThreadLocked()) {
//默認會走這里
scheduleVsyncLocked();
} else {
// 否則不在原線程,發(fā)送消息到原線程
// 最后還是調(diào)用scheduleVsyncLocked方法
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
// 如果未開啟VSYNC則直接doFrame方法
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}
注意 USE_VSYNC,如果系統(tǒng)未開啟 VSYNC 機制,此時直接發(fā)送 MSG_DO_FRAME 消息到 FrameHandler。注意查看上面貼出的 FrameHandler 代碼,此時直接執(zhí)行 doFrame 方法。
不過 Android 4.1 之后系統(tǒng)默認開啟 VSYNC,還記得在 Choreographer 的構造方法會創(chuàng)建一個 FrameDisplayEventReceiver,scheduleVsyncLocked 方法將會通過它申請 VSYNC 信號。
- 這里注意 isRunningOnLooperThreadLocked 方法,其內(nèi)部根據(jù) Looper 判斷是否在原線程,否則發(fā)送消息到 FrameHandler。最終還是會調(diào)用 scheduleVsyncLocked 方法申請 VSYNC 信號。
通過 FrameDisplayEventReceiver 申請 VSYNC 信號的過程如下:
private void scheduleVsyncLocked() {
// 調(diào)用 FrameDisplayEventReceiver 的scheduleVsync
// 實際調(diào)用到其父類DisplayEventReceiver
mDisplayEventReceiver.scheduleVsync();
}
前面我們也有說過,申請 VSYNC 信號實際是在其父類 DisplayEventReceiver。
public void scheduleVsync() {
if (mReceiverPtr == 0) {
Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
+ "receiver has already been disposed.");
} else {
// 申請VSYNC信號
nativeScheduleVsync(mReceiverPtr);
}
}
接著看下 VSYNC 信號的接收方法 onVsync,該方法在其子類 FrameDisplayEventReceiver 中重寫:
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;
public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource);
}
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
// 忽略來自非主屏的VSYNC信號
scheduleVsync();
return;
}
// ... 省略
if (mHavePendingVsync) {
Log.w(TAG, "Already have a pending vsync event. There should only be "
+ "one at a time.");
} else {
mHavePendingVsync = true;
}
mTimestampNanos = timestampNanos;
mFrame = frame;
// 發(fā)送消息執(zhí)行doFrame
// 注意this,表示當前Runnable
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
// 回調(diào)這里,執(zhí)行doFrame方法
doFrame(mTimestampNanos, mFrame);
}
}
FrameDisplayEventReceiver 實現(xiàn)了 Runnable,將其作為 callback 發(fā)送到 FrameHandler,此時 run 方法便得到執(zhí)行并且執(zhí)行 doFrame 方法:
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
// 不是在執(zhí)行Frame任務直接return
return;
}
// ... 省略
// 預期執(zhí)行時間
long intendedFrameTimeNanos = frameTimeNanos;
// 當前時間
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos;
// 超時時間是否超過一幀的時間
if (jitterNanos >= mFrameIntervalNanos) {
// 計算掉幀數(shù)
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
// 掉幀超過30幀打印Log提示
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
// 著名的掉幀Log
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
frameTimeNanos = startNanos - lastFrameOffset;
}
if (frameTimeNanos < mLastFrameTimeNanos) {
// 未知原因,居然小于最后一幀的時間
// 重新申請VSYNC信號
scheduleVsyncLocked();
return;
}
if (mFPSDivisor > 1) {
long timeSinceVsync = frameTimeNanos - mLastFrameTimeNanos;
if (timeSinceVsync < (mFrameIntervalNanos * mFPSDivisor) && timeSinceVsync > 0) {
scheduleVsyncLocked();
return;
}
}
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
// Frame標志位恢復
mFrameScheduled = false;
// 記錄最后一幀時間
mLastFrameTimeNanos = frameTimeNanos;
}
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
mFrameInfo.markInputHandlingStart();
// 先執(zhí)行CALLBACK_INPUT任務
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
mFrameInfo.markAnimationsStart();
// 再執(zhí)行CALLBACK_ANIMATION
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
mFrameInfo.markPerformTraversalsStart();
// 其次執(zhí)行CALLBACK_TRAVERSAL
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
// API Level 23 之后加入,
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
- 注意第一個 if 語句,不知道大家是否在自己項目的 Logcat 臺遇到過這樣一條日志:
Skipped (該值>=30) frames! The application may be doing too much work on its main thread
該 Log 用于提示開發(fā)人員當前存在耗時的任務導致 UI 繪制掉幀超過 30 幀(≈ 16ms * 30 >= 480ms)。
- 注意看方法的最后,按照類型順序觸發(fā) doCallbacks 回調(diào)相關任務。
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
doCallbacks 方法將根據(jù)不同的任務類型依次執(zhí)行其 run 方法:
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
final long now = System.nanoTime();
// 根據(jù)指定的類型CallbackkQueue中查找到達執(zhí)行時間的CallbackRecord
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
now / TimeUtils.NANOS_PER_MS);
if (callbacks == null) {
return;
}
mCallbacksRunning = true;
if (callbackType == Choreographer.CALLBACK_COMMIT) {
final long jitterNanos = now - frameTimeNanos;
Trace.traceCounter(Trace.TRACE_TAG_VIEW, "jitterNanos", (int) jitterNanos);
if (jitterNanos >= 2 * mFrameIntervalNanos) {
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos
+ mFrameIntervalNanos;
if (DEBUG_JANK) {
Log.d(TAG, "Commit callback delayed by " + (jitterNanos * 0.000001f)
+ " ms which is more than twice the frame interval of "
+ (mFrameIntervalNanos * 0.000001f) + " ms! "
+ "Setting frame time to " + (lastFrameOffset * 0.000001f)
+ " ms in the past.");
mDebugPrintNextFrameTimeDelta = true;
}
frameTimeNanos = now - lastFrameOffset;
mLastFrameTimeNanos = frameTimeNanos;
}
}
}
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
// 迭代執(zhí)行所有任務
for (CallbackRecord c = callbacks; c != null; c = c.next) {
// 回調(diào)CallbackRecord的run
// 其內(nèi)部回調(diào)Callback的run
c.run(frameTimeNanos);
}
} finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
final CallbackRecord next = callbacks.next;
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
注意遍歷 CallbackRecord 鏈表調(diào)用其 run 方法:
public void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) {
// 通過postFrameCallback 或 postFrameCallbackDelayed
// 會執(zhí)行這里
((FrameCallback)action).doFrame(frameTimeNanos);
} else {
((Runnable)action).run();
}
}
注意 token == FRAME_CALLBACK_TOKEN 表示通過 postFrameCallback 添加的任務。這里就是按照 Callback 類型回調(diào)其 run 方法。
回到 ViewRootImpl 發(fā)起的繪制任務,此時 View 的繪制流程便開始了。
final class TraversalRunnable implements Runnable{
@Override
public void run(){
// View 的繪制任務開始
doTraversal();
}
}
至此 Choreographer 的工作流程就已經(jīng)分析清楚了,Choreographer 支持四種類型任務:輸入、動畫、繪制和提交,并配合系統(tǒng)的 VSYNC 進行刷新、繪制等流程。確實做到了統(tǒng)一協(xié)調(diào)管理。
下面,再通過一張圖來加深對 Choreographer 的工作流程的理解。
正如文章開頭介紹 Choreographer 可以配合系統(tǒng)的 VSYNC 信號完成 UI 的繪制任務。那我們便可以通過它來監(jiān)控應用的幀率,雖然 Choreographer 內(nèi)部也實現(xiàn)了對掉幀的監(jiān)控,但是默認只能監(jiān)控超過 30 幀及以上。
不過通過今天的分析,你是否也可以實現(xiàn)一個任意掉幀數(shù)的監(jiān)控呢?并且可以將其用于線上統(tǒng)計,更好的幫助我們優(yōu)化應用的渲染性能。
關于 UI 渲染所涉及的內(nèi)容非常多,文章最后也會附上一些擴展資料,便于更好的學習理解。
文中如有不妥或有更好的分析結果,歡迎您的分享留言或指正。
文章如果對你有幫助,請留個贊吧。
擴展閱讀
- 關于 UI 渲染,你需要了解什么?
- Android 之如何優(yōu)化 UI 渲染(上)
- Android 之理解 VSYNC
- Android 之 LayoutInflater 全面解析
- Android 之你真的了解 View.post() 原理嗎?
- Android 之 Choreographer 詳細分析
其他系列專題