美文网首页
Activity 启动回调及View.post分析

Activity 启动回调及View.post分析

作者: andev009 | 来源:发表于2018-12-14 15:56 被阅读59次

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

    相关文章

      网友评论

          本文标题:Activity 启动回调及View.post分析

          本文链接:https://www.haomeiwen.com/subject/qlughqtx.html