Android 源碼 (http://androidxref.com/)
先看下面這段代碼,目的是獲取TextView的尺寸。
public class MainActivity extends AppCompatActivity {
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView)findViewById(R.id.text) ;
Log.d("MainActivity", "onCreate textView:" + textView.getWidth());
}
@Override
public void onResume(){
super.onResume();
Log.d("MainActivity", "onResume textView:" + textView.getWidth());
new Handler().post(new Runnable() {
@Override
public void run() {
Log.d("MainActivity", "onResume Handler.post textView:" + textView.getWidth());
}
});
textView.post(new Runnable() {
@Override
public void run() {
Log.d("MainActivity", "onResume textView.post textView:" + textView.getWidth());
}
});
}
}
打印結果如下:
D/MainActivity: onCreate textView:0
D/MainActivity: onResume textView:0
D/MainActivity: onResume Handler.post textView:0
D/MainActivity: onResume textView.post textView:210
可見只有最后的View.post可以獲取尺寸,其他方式不能。大家都知道在onCreate里是獲取不到控件的尺寸的,但是為什么onResume里也不能獲取,Handler().post方式也不能獲取呢。
先看下onResume里為什么不能,這里涉及到Activity啟動流程,當調用startActivity后,經過binder機制,會通過ActivityThread的Handler對象mH發送message,最終走到handleLaunchActivity方法, Activity就在此方法里被創建:
@Override
public Activity handleLaunchActivity(ActivityClientRecord r,
PendingTransactionActions pendingActions, Intent customIntent) {
...
final Activity a = performLaunchActivity(r, customIntent);
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);
return a;
...
}
performLaunchActivity創建完了Activity后,通過handleResumeActivity走onResume生命周期回調,這里我們沒有看到onCreate回調,猜測onCreate回調在performLaunchActivity里。
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
Activity activity = null;
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback);
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
...
return activity;
}
可見上面方法完成了生成了Activity對象,生成了Application,同時回調了Activity的OnCreate方法,再看attach方法:
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window) {
...
mWindow = new PhoneWindow(this, window);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
...
}
上面創建了個Window,并且Window的回調設為Actvity,這就是為什么Activity能接收按鍵反饋的原因。從上面分析看出,performLaunchActivity完成了Application, Activity的創建,還有Activity的OnCreate回調。performLaunchActivity走完了,就開始走handleResumeActivity,
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
ActivityClientRecord r = mActivities.get(token);
r = performResumeActivity(token, clearHide, reason);//回調onResume
if (r.window == null && !a.mFinished && willBeVisible) {//下面就是將UI顯示出來,并經過measure,layout,draw流程
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient && !a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);//UI繪制流程開始的地方
}
}
}
上面關于Window的那段很長,主要就是通過ViewRootImpl將UI顯示出來,注意到流程沒有,是使用performResumeActivity來回調onResume,然后再顯示UI,所以在onResume里去取得控件尺寸,當然是失敗的,這時候控件都沒有走measure,layout ,draw流程,這個流程的發起者就是這里的ViewRootImpl。
通過以上分析我們明白了為什么onCreate,onResume里不能取得控件尺寸了。
這里分析下UI繪制流程,為分析那兩個post結果不同做準備,從上面的wm.addView(decor, l);開始,這個方法的功能是把最頂層的decorView加入Window。進去addView看看就知道,Window把這個任務交給了ViewRootImpl,所以上面說發起者是ViewRootImpl,ViewRootImpl調用setView方法:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
....
requestLayout();
...
}
public void requestLayout() {
scheduleTraversals();
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().
getQueue().postSyncBarrier();//發送同步障礙消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);//發起繪制消息
...
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
...
performTraversals();
...
}
}
總結上面的繪制流程,就是ViewRootImpl通過Handler先發個同步障礙消息,再發個真正繪制消息(TraversalRunnable封裝的),最后由performTraversals執行真正的繪制任務。
這里明白為什么通過Handler.post不能取得控件尺寸了吧?因為Handler.post在onResume里先發,繪制消息后發,所以Handler.post的消息先回調,這時控件還沒繪制,所以得不到尺寸。
那么為什么View.post方式可以取得控件尺寸呢?看看View.post發生了什么:
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
上面代碼意思是attachInfo如果不空,就用attachInfo的mHandler發消息,否則就把消息先保存著。這時候在onResume回調里,attachInfo還是空的,在dispatchAttachedToWindow時,attachInfo才被賦值。
可見Runnable并沒有像Handler那樣被post到消息隊列里,getRunQueue().post(action);注釋上也有說推遲(Postpone the runnable until we know on which thread it needs to run.),簡單說就是View自帶個HandlerActionQueue來保存post的Runnable,初始時可以放4個Runnable,這個查源碼就知道了。
現在的問題就是放在HandlerActionQueue的Runnable什么時候執行了。
答案在上面說的dispatchAttachedToWindow方法里,查看View的dispatchAttachedToWindow方法:
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
...
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);//發送之前保存的Runnable消息
mRunQueue = null;
}
onAttachedToWindow();
...
}
通過info.mHandler把之前保存的runnable發送出去。之前分析的繪制流程最后一步是performTraversals,dispatchAttachedToWindow就是在這個方法里被調用:
private void performTraversals() {
host.dispatchAttachedToWindow(mAttachInfo, 0);//通過ViewGroup一層層傳給child,看源碼就知道了
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
performLayout(lp, mWidth, mHeight);
performDraw();
}
可見View.post先把之前保存的Runnable消息發送給消息隊列,然后執行performMeasure,performLayout,performDraw三個真正的繪制方法,當Runnable消息回調時,當然就可以取得控件尺寸了。
額外的東西:
同步障礙
Handler 消息分為同步消息和異步消息,在之前的分析中,Handler在發繪制消息前,先發了個同步障礙消息,同步障礙消息和普通Message不同是它的target為空,而普通Message的target就是Handler自身。之所以先發同步障礙,是為了在消息隊列里跳過同步消息,先處理異步消息,繪制消息是異步消息,所以繪制流程能更快的執行。