Android系統(tǒng)從4.1(API 16)開始加入Choreographer這個(gè)類來控制同步處理輸入(Input)琅翻、動(dòng)畫(Animation)法精、繪制(Draw)三個(gè)UI操作痴晦。其實(shí)UI顯示的時(shí)候每一幀要完成的事情只有這三種矩乐。如下圖是官網(wǎng)的相關(guān)說明:
Choreographer接收顯示系統(tǒng)的時(shí)間脈沖(垂直同步信號(hào)-VSync信號(hào))璧函,在下一個(gè)frame渲染時(shí)控制執(zhí)行這些操作印蔬。
Choreographer中文翻譯過來是"舞蹈指揮"勋桶,字面上的意思就是優(yōu)雅地指揮以上三個(gè)UI操作一起跳一支舞。這個(gè)詞可以概括這個(gè)類的工作侥猬,如果android系統(tǒng)是一場芭蕾舞例驹,他就是Android UI顯示這出精彩舞劇的編舞,指揮臺(tái)上的演員們相互合作陵究,精彩演出眠饮。Google的工程師看來挺喜歡舞蹈的!
好了廢話不多說铜邮,下面讓我們來看看劇本是怎么設(shè)計(jì)的仪召,Let's Read the fucking source code!
Choreographer的源碼位于android.view這個(gè)pakage中,是view層框架的一部分松蒜,Android studio里面搜一下就可以看到源碼了扔茅。
首先看看頭部的一些說明,大體了解一下這個(gè)類是干嘛的秸苗,有助于我們理解接下來的源碼召娜。 和官網(wǎng)的文檔是一樣的,應(yīng)該就是用這個(gè)生成的惊楼,和上面一部分相比介紹了Choreographer的使用接口玖瘸。開發(fā)者可以使用Choreographer#postFrameCallback設(shè)置自己的callback與Choreographer交互秸讹,你設(shè)置的callCack會(huì)在下一個(gè)frame被渲染時(shí)觸發(fā)。Callback有4種類型雅倒,Input璃诀、Animation、Draw蔑匣,還有一種是用來解決動(dòng)畫啟動(dòng)問題的劣欢,將在下文介紹。這四種操作都是這么觸發(fā)的裁良。
如下圖:
收到VSync信號(hào)后凿将,順序執(zhí)行3個(gè)操作,然后等待下一個(gè)信號(hào)价脾,再次順序執(zhí)行3個(gè)操作牧抵。假設(shè)在第二個(gè)信號(hào)到來之前,所有的操作都執(zhí)行完成了彼棍,即Draw操作完成了灭忠,那么第二個(gè)信號(hào)來到時(shí)膳算,此時(shí)界面將會(huì)更新為第一frame的內(nèi)容座硕,因?yàn)镈raw操作已經(jīng)完成了。否則界面將不會(huì)更新涕蜂,還是顯示上一個(gè)frame的內(nèi)容华匾,表示你丟幀了。丟幀是造成卡頓的原因机隙。如下圖:
第二個(gè)信號(hào)到來時(shí)蜘拉,Draw操作沒有按時(shí)完成,導(dǎo)致第三個(gè)時(shí)鐘周期內(nèi)顯示的還是第一幀的內(nèi)容有鹿。
注意文檔的最后一段話:
Each Looper thread has its own choreographer. Other threads can post callbacks to run on the choreographer but they will run on the Looper to which the choreographer belongs.*
每個(gè)線程都有自己的choreographer旭旭。
基本上的原理就是上面這樣,那么接下來我們通過源碼詳細(xì)地看一下細(xì)節(jié)是怎么實(shí)現(xiàn)的葱跋。
首先先看看構(gòu)造函數(shù)持寄。
構(gòu)造函數(shù)
private Choreographer(Looper looper) {
mLooper = looper;
mHandler = new FrameHandler(looper);
mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper) : null;
mLastFrameTimeNanos = Long.MIN_VALUE;
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
}
這里做了幾個(gè)初始化操作,根據(jù)Looper對(duì)象生成娱俺,Looper和線程是一對(duì)一的關(guān)系稍味,對(duì)應(yīng)上面說明里的每個(gè)線程對(duì)應(yīng)一個(gè)Choreographer。
1.初始化FrameHandler荠卷。接收處理消息模庐。
2.初始化FrameDisplayEventReceiver。FrameDisplayEventReceiver用來接收垂直同步脈沖油宜,就是VSync信號(hào)掂碱,VSync信號(hào)是一個(gè)時(shí)間脈沖怜姿,一般為60HZ,用來控制系統(tǒng)同步操作疼燥,怎么同ChoreoGrapher一起工作的社牲,將在下文介紹。
3.初始化mLastFrameTimeNanos(標(biāo)記上一個(gè)frame的渲染時(shí)間)以及mFrameIntervalNanos(幀率,fps悴了,一般手機(jī)上為1s/60)搏恤。
4.初始化CallbackQueue,callback隊(duì)列湃交,將在下一幀開始渲染時(shí)回調(diào)熟空。
我們首先看看FrameHandler和FrameDisplayEventReceiver的結(jié)構(gòu)。
FrameHandler
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:
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC:
doScheduleVsync();
break;
case MSG_DO_SCHEDULE_CALLBACK:
doScheduleCallback(msg.arg1);
break;
}
}
}
看上面的代碼搞莺,就是一個(gè)簡單的Handler息罗。處理3個(gè)類型的消息。
MSG_DO_FRAME:開始渲染下一幀的操作
MSG_DO_SCHEDULE_VSYNC:請(qǐng)求Vsync信號(hào)
MSG_DO_SCHEDULE_CALLBACK:請(qǐng)求執(zhí)行callback
額才沧,下面再細(xì)分一下迈喉,分別詳細(xì)看一下這三個(gè)步驟是怎么實(shí)現(xiàn)的。繼續(xù)看源碼吧温圆。挨摸。。
FrameDisplayEventReceiver
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
public FrameDisplayEventReceiver(Looper looper) {
super(looper);
}
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
...
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}
FrameDisplayEventReceiver繼承自DisplayEventReceiver接收底層的VSync信號(hào)開始處理UI過程岁歉。VSync信號(hào)由SurfaceFlinger實(shí)現(xiàn)并定時(shí)發(fā)送得运。FrameDisplayEventReceiver收到信號(hào)后,調(diào)用onVsync方法組織消息發(fā)送到主線程處理锅移。這個(gè)消息主要內(nèi)容就是run方法里面的doFrame了熔掺,這里mTimestampNanos是信號(hào)到來的時(shí)間參數(shù)。
FrameHandler和FrameDisplayEventReceiver是怎么工作的呢非剃?ChoreoGrapher的總體流程圖如下圖:
流程圖
以上是總體的流程圖:
1.PostCallBack,發(fā)起添加回調(diào)置逻,這個(gè)FrameCallBack將在下一幀被渲染時(shí)執(zhí)行。
2.AddToCallBackQueue,將FrameCallBack添加到回調(diào)隊(duì)列里面备绽,等待時(shí)機(jī)執(zhí)行回調(diào)券坞。每種類型的callback按照設(shè)置的執(zhí)行時(shí)間(dueTime)順序排序分別保存在一個(gè)單鏈表中。
3.判斷FrameCallBack設(shè)定的執(zhí)行時(shí)間是否在當(dāng)前時(shí)間之后疯坤,若是报慕,發(fā)送MSG_DO_SCHEDULE_CALLBACK消息到主線程,安排執(zhí)行doScheduleCallback压怠,安排執(zhí)行CallBack眠冈。否則直接跳到第4步。
4.執(zhí)行scheduleFrameLocked,安排執(zhí)行下一幀蜗顽。
5.判斷上一幀是否已經(jīng)執(zhí)行布卡,若未執(zhí)行,當(dāng)前操作直接結(jié)束雇盖。若已經(jīng)執(zhí)行忿等,根據(jù)情況執(zhí)行以下6、7步崔挖。
6.若使用垂直同步信號(hào)進(jìn)行同步贸街,則執(zhí)行7.否則,直接跳到9狸相。
7.若當(dāng)前線程是UI線程薛匪,則通過執(zhí)行scheduleVsyncLocked請(qǐng)求垂直同步信號(hào)。否則脓鹃,送MSG_DO_SCHEDULE_VSYNC消息到主線程逸尖,安排執(zhí)行doScheduleVsync,在主線程調(diào)用scheduleVsyncLocked瘸右。
8.收到垂直同步信號(hào)娇跟,調(diào)用FrameDisplayEventReceiver.onVsync(),發(fā)送消息到主線程太颤,請(qǐng)求執(zhí)行doFrame苞俘。
9.執(zhí)行doFrame,渲染下一幀栋齿。
主要的工作在doFrame中苗胀,接下來我們具體看看doFrame函數(shù)都干了些什么。
從名字看很容易理解doFrame函數(shù)就是開始進(jìn)行下一幀的顯示工作瓦堵。好了以下源代碼又來了,我們一行一行分析一下吧歌亲。
doFrame
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) { //判斷是否有callback需要執(zhí)行菇用,mFrameScheduled會(huì)在postCallBack的時(shí)候置為true,一次frame執(zhí)行時(shí)置為false
return; // no work to do
}
\\\\打印跳frame時(shí)間
if (DEBUG_JANK && mDebugPrintNextFrameTimeDelta) {
mDebugPrintNextFrameTimeDelta = false;
Log.d(TAG, "Frame time delta: "
+ ((frameTimeNanos - mLastFrameTimeNanos) * 0.000001f) + " ms");
}
//設(shè)置當(dāng)前frame的Vsync信號(hào)到來時(shí)間
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();//實(shí)際開始執(zhí)行當(dāng)前frame的時(shí)間
//時(shí)間差
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
//時(shí)間差大于一個(gè)時(shí)鐘周期,認(rèn)為跳frame
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
//跳frame數(shù)大于默認(rèn)值陷揪,打印警告信息惋鸥,默認(rèn)值為30
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
//計(jì)算實(shí)際開始當(dāng)前frame與時(shí)鐘信號(hào)的偏差值
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
//打印偏差及跳幀信息
if (DEBUG_JANK) {
Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
+ "which is more than the frame interval of "
+ (mFrameIntervalNanos * 0.000001f) + " ms! "
+ "Skipping " + skippedFrames + " frames and setting frame "
+ "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
}
//修正偏差值,忽略偏差悍缠,為了后續(xù)更好地同步工作
frameTimeNanos = startNanos - lastFrameOffset;
}
//若時(shí)間回溯卦绣,則不進(jìn)行任何工作,等待下一個(gè)時(shí)鐘信號(hào)的到來
//這里為什么會(huì)發(fā)生時(shí)間回溯我沒搞明白飞蚓,大概是未知時(shí)鐘錯(cuò)誤引起滤港?注釋里說的maybe 好像不太對(duì)
if (frameTimeNanos < mLastFrameTimeNanos) {
if (DEBUG_JANK) {
Log.d(TAG, "Frame time appears to be going backwards. May be due to a "
+ "previously skipped frame. Waiting for next vsync.");
}
//請(qǐng)求下一次時(shí)鐘信號(hào)
scheduleVsyncLocked();
return;
}
//記錄當(dāng)前frame信息
mFrameInfo.setVsync(intendedFrameTimeNanos,frameTimeNanos);
mFrameScheduled = false;
//記錄上一次frame開始時(shí)間,修正后的
mLastFrameTimeNanos = frameTimeNanos;
}
try {
//執(zhí)行相關(guān)callBack
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
if (DEBUG_FRAMES) {
final long endNanos = System.nanoTime();
Log.d(TAG, "Frame " + frame + ": Finished, took "
+ (endNanos - startNanos) * 0.000001f + " ms, latency "
+ (startNanos - frameTimeNanos) * 0.000001f + " ms.");
}
}
大部分內(nèi)容都在上面的注釋中說明了趴拧,大概是以下的流程:
總結(jié)起來其實(shí)主要是兩個(gè)操作:
1.設(shè)置當(dāng)前frame的啟動(dòng)時(shí)間溅漾。
判斷是否跳幀山叮,若跳幀修正當(dāng)前frame的啟動(dòng)時(shí)間到最近的VSync信號(hào)時(shí)間。如果沒跳幀添履,當(dāng)前frame啟動(dòng)時(shí)間直接設(shè)置為當(dāng)前VSync信號(hào)時(shí)間屁倔。修正完時(shí)間后,無論當(dāng)前frame是否跳幀暮胧,使得當(dāng)前frame的啟動(dòng)時(shí)間與VSync信號(hào)還是在一個(gè)節(jié)奏上的锐借,可能可能延后了一到幾個(gè)周期,但是節(jié)奏點(diǎn)還是吻合的往衷。
如下圖所示是時(shí)間修正的一個(gè)例子瞎饲,
由于第二個(gè)frame執(zhí)行超時(shí),第三個(gè)frame實(shí)際啟動(dòng)時(shí)間比第三個(gè)VSync信號(hào)到來時(shí)間要晚炼绘,因?yàn)檫@時(shí)候延時(shí)比較小嗅战,沒有超過一個(gè)時(shí)鐘周期,系統(tǒng)還是將frameTimeNanos3傳給回調(diào)俺亮,回調(diào)拿到的時(shí)間和VSync信號(hào)同步驮捍。
再來看看下圖:
由于第二個(gè)frame執(zhí)行時(shí)間超過2個(gè)時(shí)鐘周期,導(dǎo)致第三個(gè)frame延后執(zhí)行時(shí)間大于一個(gè)時(shí)鐘周期脚曾,系統(tǒng)認(rèn)為這時(shí)候影響較大东且,判定為跳幀了,將第三個(gè)frame的時(shí)間修正為frameTimeNanos4,比VSync真正到來的時(shí)間晚了一個(gè)時(shí)鐘周期本讥。
時(shí)間修正珊泳,既保證了doFrame操作和VSync保持同步節(jié)奏,又保證實(shí)際啟動(dòng)時(shí)間與記錄的時(shí)間點(diǎn)相差不會(huì)太大拷沸,便于同步及分析色查。
2.順序執(zhí)行callBack隊(duì)列里面的callback.
然后接下來看看doCallbacks的執(zhí)行過程:
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
// We use "now" to determine when callbacks become due because it's possible
// for earlier processing phases in a frame to post callbacks that should run
// in a following phase, such as an input event that causes an animation to start.
final long now = System.nanoTime();
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked( now / TimeUtils.NANOS_PER_MS);
if (callbacks == null) {
return;
}
mCallbacksRunning = true;
// Update the frame time if necessary when committing the frame.
// We only update the frame time if we are more than 2 frames late reaching
// the commit phase. This ensures that the frame time which is observed by the
// callbacks will always increase from one frame to the next and never repeat.
// We never want the next frame's starting frame time to end up being less than
// or equal to the previous frame's commit frame time. Keep in mind that the
// next frame has most likely already been scheduled by now so we play it
// safe by ensuring the commit time is always at least one frame behind.
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]);
for (CallbackRecord c = callbacks; c != null; c = c.next) {
if (DEBUG_FRAMES) {
Log.d(TAG, "RunCallback: type=" + callbackType
+ ", action=" + c.action + ", token=" + c.token
+ ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
}
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);
}
}
callback的類型有以下4種,除了文章一開始提到的3中外撞芍,還有一個(gè)CALLBACK_COMMIT秧了。
CALLBACK_INPUT:輸入
CALLBACK_ANIMATION:動(dòng)畫
CALLBACK_TRAVERSAL:遍歷,執(zhí)行measure序无、layout验毡、draw
CALLBACK_COMMIT:遍歷完成的提交操作,用來修正動(dòng)畫啟動(dòng)時(shí)間
然后看上面的源碼帝嗡,分析一下每個(gè)callback的執(zhí)行過程:
1.callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked( now / TimeUtils.NANOS_PER_MS);得到執(zhí)行時(shí)間在當(dāng)前時(shí)間之前的所有CallBack晶通,保存在單鏈表中。每種類型的callback按執(zhí)行時(shí)間先后順序排序分別存在一個(gè)單鏈表里面哟玷。為了保證當(dāng)前callback執(zhí)行時(shí)新post進(jìn)來的callback在下一個(gè)frame時(shí)才被執(zhí)行狮辽,這個(gè)地方extractDueCallbacksLocked會(huì)將需要執(zhí)行的callback和以后執(zhí)行的callback斷開變成兩個(gè)鏈表,新post進(jìn)來的callback會(huì)被放到后面一個(gè)鏈表中。當(dāng)前frame只會(huì)執(zhí)行前一個(gè)鏈表中的callback隘竭,保證了在執(zhí)行callback時(shí)塘秦,如果callback中Post相同類型的callback,這些新加的callback將在下一個(gè)frame啟動(dòng)后才會(huì)被執(zhí)行动看。
2.接下來尊剔,看一大段注釋,如果類型是CALLBACK_COMMIT菱皆,并且當(dāng)前frame渲染時(shí)間超過了兩個(gè)時(shí)鐘周期须误,則將當(dāng)前提交時(shí)間修正為上一個(gè)垂直同步信號(hào)時(shí)間。為了保證下一個(gè)frame的提交時(shí)間和當(dāng)前frame時(shí)間相差為一且不重復(fù)仇轻。
這個(gè)地方注釋挺難看懂京痢,實(shí)際上這個(gè)地方CALLBACK_COMMIT是為了解決ValueAnimator的一個(gè)問題而引入的,主要是解決因?yàn)楸闅v時(shí)間過長導(dǎo)致動(dòng)畫時(shí)間啟動(dòng)過長篷店,時(shí)間縮短祭椰,導(dǎo)致跳幀,這里修正動(dòng)畫第一個(gè)frame開始時(shí)間延后來改善疲陕,這時(shí)候才表示動(dòng)畫真正啟動(dòng)方淤。為什么不直接設(shè)置當(dāng)前時(shí)間而是回溯一個(gè)時(shí)鐘周期之前的時(shí)間呢?看注釋蹄殃,這里如果設(shè)置為當(dāng)前frame時(shí)間携茂,因?yàn)閯?dòng)畫的第一個(gè)frame其實(shí)已經(jīng)繪制完成,第二個(gè)frame這時(shí)候已經(jīng)開始了诅岩,設(shè)置為當(dāng)前時(shí)間會(huì)導(dǎo)致這兩個(gè)frame時(shí)間一樣讳苦,導(dǎo)致沖突。詳細(xì)情況請(qǐng)看官方針對(duì)這個(gè)問題的修改吩谦。Fix animation start jank due to expensive layout operations.
如下圖所示:
比如說在第二個(gè)frame開始執(zhí)行時(shí)鸳谜,開始渲染動(dòng)畫的第一個(gè)畫面,第二個(gè)frame執(zhí)行時(shí)間超過了兩個(gè)時(shí)鐘周期逮京,Draw操作執(zhí)行結(jié)束后卿堂,這時(shí)候完成了動(dòng)畫第一幀的渲染,動(dòng)畫實(shí)際上還沒開始懒棉,但是時(shí)間已經(jīng)過了兩個(gè)時(shí)鐘周期,后面動(dòng)畫實(shí)際執(zhí)行時(shí)間將會(huì)縮短一個(gè)時(shí)鐘周期览绿。這時(shí)候系統(tǒng)通過修正commit時(shí)間到frameTimeNanos的上一個(gè)VSync信號(hào)時(shí)間策严,即完成動(dòng)畫第一幀渲染之前的VSync信號(hào)到來時(shí)間,修正了動(dòng)畫啟動(dòng)時(shí)間饿敲,保證動(dòng)畫執(zhí)行時(shí)間的正確性妻导。
3.接下來就是調(diào)用c.run(frameTimeNanos);執(zhí)行回調(diào)。
例如,你可以寫一個(gè)自定義的FPSFrameCallback繼承自Choreographer.FrameCallback倔韭,實(shí)現(xiàn)里面的doFrame方法术浪。
public class FPSFrameCallback implements Choreographer.FrameCallback{
@Override
public void doFrame(long frameTimeNanos){
//do something
}
}
通過
Choreographer.getInstance().postFrameCallback(new FPSFrameCallback());
把你的回調(diào)添加到Choreographer之中,那么在下一個(gè)frame被渲染的時(shí)候就會(huì)回調(diào)你的callback,執(zhí)行你定義的doFrame操作寿酌,這時(shí)候你就可以獲取到這一幀的開始渲染時(shí)間并做一些自己想做的事情了胰苏。
開源組件Tiny Dancer就是根據(jù)這個(gè)原理獲取每一幀的渲染時(shí)間,繼而分析實(shí)現(xiàn)獲取設(shè)備的當(dāng)前幀率的醇疼。有興趣的人可以查看硕并。
Tiny Dancer
好了,關(guān)于Choreographer的分析到此結(jié)束秧荆。希望對(duì)你有幫助倔毙。