Activity 启动回调及View.post分析

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自身。之所以先发同步障碍,是为了在消息队列里跳过同步消息,先处理异步消息,绘制消息是异步消息,所以绘制流程能更快的执行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容

  • 【Android Handler 消息机制】 前言 在Android开发中,我们都知道不能在主线程中执行耗时的任务...
    Rtia阅读 4,820评论 1 28
  • 转载于:请叫我大苏的 Android屏幕刷新机制 我主要的目的是跟着文章的思路从新走一遍,让自己更好的理解相关的知...
    ghroost阅读 2,056评论 2 11
  • 本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 这次就来梳理一下 Android 的屏幕刷新机...
    请叫我大苏阅读 25,549评论 48 206
  • 家住19层。蚊子依然肆虐。想了很久,也不知道这丫们是从哪里挤进来的。 门是开了就关的。 窗户也有很细的纱窗。 而且...
    踏歌徐行阅读 237评论 1 0
  • 泰安第十六中学 李树伟 校园里有许多树,迎春、樱花、紫薇、侧柏、雪松,而我独爱校园一角的那棵老槐。 老槐有...
    冬日雨花阅读 596评论 0 1