Android View体系

之前零散写了一些,这篇算是总集篇。大概涉及到

  • setContentView()
  • SubDecor、Decorview、ContentView
  • PhoneWindow、WindowManager
  • ViewRootImpl、Choreographer
  • Toast.show()、Dialog.show()
  • View.invalidate()、View.requestLayout()
  • View.post()

新写一个Activity会调用到setContentView()方法设置布局,那就以此作为切入点。
Activity.setContentView()->AppCompatDelegateImpl.setContentView()

    public void setContentView(int resId) {
        //创建SubDecor
        ensureSubDecor();
        //id为content的FrameLayout
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        //移除content上所有view
        contentParent.removeAllViews();
        //加载xml布局到content
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        //回调
        mOriginalWindowCallback.onContentChanged();
    }

创建SubDecor这个ViewGroup(SubDecor包含ActionBar、Toolbar、ContentView等),并将SubDecor添加到window的Decorview,Decorview就是顶层View了;找到SubDecor上id为content的FrameLayout,加载xml布局反射创建View树并添加到content。

ensureSubDecor()内部调用createSubDecor(),createSubDecor()中调用mWindow.setContentView(subDecor)将subDecor添加到Window的Decorview。

Window大家都知道,其实现类是PhoneWindow,在Activity.attach()方法中初始化。而Activity.attach()在ActivityThread的performLaunchActivity()方法中调用,performLaunchActivity()中创建了Activity实例,加载资源,调用activity.attach()然后通过Instrumentation类回调onCreate()生命周期。当然这篇也不是分析Activity启动流程之类的文章,只需关注PhoneWindow。

Activity.attach()

    final void attach(...){
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
    }

PhoneWindow.setContentView()

    @Override
    public void setContentView(View view) {
        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }

    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        if (mContentParent == null) {
            installDecor();
        } 
     
        mContentParent.addView(view, params);
    }

    //mContentParent
    public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT)

    //Window.findViewById(int id)
    public <T extends View> T findViewById(@IdRes int id) {
        return getDecorView().findViewById(id);
    }

创建DecorView,将SubDecor添加到DecorView。installDecor()方法中可以看到mContentParent是DecorView中id为content的ViewGroup,那这玩意是平常我们所说的ContentView吗?其实不是,刚看到这里我也有点懵逼。如果这玩意是ContentView,那SubDecor已经添加进了ContentView,View树再add进来岂不是会覆盖SubDecor中的Toolbar、Actionbar?实际上ContentView是SubDecor的子View,继续看下去。

AppCompatDelegateImpl.createSubDecor()随便挑一个创建SubDecor的分支

subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                        .inflate(R.layout.abc_screen_toolbar, null);
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                R.id.action_bar_activity_content);
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);

while (windowContentView.getChildCount() > 0) {
    final View child = windowContentView.getChildAt(0);
    windowContentView.removeViewAt(0);
    contentView.addView(child);
}

windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);

精简一下很明显了。DecorView中id为content的windowContentView重置为NO_ID,SubDecor中id为action_bar_activity_content的contentView设置为android.R.id.content;并将原DecorView中windowContentView下的子View剪切到SubDecor的ContentView。

abc_screen_toolbar.xml

<androidx.appcompat.widget.ActionBarOverlayLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/decor_content_parent"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true">

    <include layout="@layout/abc_screen_content_include"/>
    ...
</androidx.appcompat.widget.ActionBarOverlayLayout>

abc_screen_content_include.xml

<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <androidx.appcompat.widget.ContentFrameLayout
            android:id="@id/action_bar_activity_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:foregroundGravity="fill_horizontal|top"
            android:foreground="?android:attr/windowContentOverlay" />

</merge>

所以SubDecor是ActionBarOverlayLayout,ContentView是SubDecor中的ContentFrameLayout

setContentView()看完了,View树何时开始绘制真正显示出来呢?老生常谈的是onResume()之后,那就从onResume()生命周期的调用处开始看。

ActivityThread.handleResumeActivity()

    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {

        //onResume回调
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
        View decor = r.window.getDecorView();
        decor.setVisibility(View.INVISIBLE);
        ViewManager wm = a.getWindowManager();
        WindowManager.LayoutParams l = r.window.getAttributes();
        wm.addView(decor, l);
        r.activity.makeVisible();
    }

Activity.makeVisible()

    void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

熟悉的WindowManager.addView()。WindowManager的实现类是WindowManagerImpl,其通过三个接口方法addView()、updateViewLayout()、removeView()来管理View,也就是说Window是View的管理者。

WindowManager.addView()调用到WindowManagerGlobal.addView()

        ViewRootImpl root;
        root = new ViewRootImpl(view.getContext(), display);
        root.setView(view, wparams, panelParentView, userId);

ViewRootImpl.setView()

    public void setView(...){
        ......
        requestLayout();
    }

    public void requestLayout() {
        ......
        scheduleTraversals();
    }

    void scheduleTraversals() {
        ......
        mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    }

mChoreographer.postCallback()也就是执行mTraversalRunnable这个Runnable

ViewRootImpl.mTraversalRunnable

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

ViewRootImpl.doTraversal()

    void doTraversal() {
        ......
        performTraversals();
    }

    private void performTraversals() {
        //绘制流程
        performMeasure();
        performLayout();
        performDraw();
    }

WindowManager.addView()最终走到ViewRootImpl.performTraversals()方法,内部依次调用performMeasure()、performLayout()、performDraw(),从DecorView开始遍历View树对应调用View.measure()、View.layout()、View.draw()直到View树完成绘制流程。可以下结论,ViewRootImpl接管了绘制流程。

这里还有个问题,mTraversalRunnable何时执行run()方法呢,下面回看Choreographer.postCallback(),经过一些列调用,调用到内部类FrameDisplayEventReceiver.scheduleVsync()

    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 {
            nativeScheduleVsync(mReceiverPtr);
        }
    }

nativeScheduleVsync(mReceiverPtr)调用native方法发出VSync信号,回调到FrameDisplayEventReceiver.onVsync()

        @Override
        public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
            ......
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run() {
            ......
            doFrame(mTimestampNanos, mFrame);
        }

        void doFrame(long frameTimeNanos, int frame) {
            ......
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
        }

doFrame()就是执行mTraversalRunnable走绘制流程了,也就是说VSync垂直同步信号到来时会执行绘制流程刷新UI,按60帧算每16ms系统都会发出VSYNC信号。

简单总结下View树如何显示:在Activity的onResume()生命周期之后通过WindowManager.addView(),初始化ViewRootImpl,等待VSync垂直同步信号到来,调用到ViewRootImpl.performTraversals()开启View树绘制流程。

Activity是这个流程,那其它的View像Toast、Dialog呢?当然也是一样,Toast.show()、Dialog.show()都会调用到WindowManager.addView(),至于之后的流程就完全和上述一致了。

Toast稍微麻烦点,Toast.show()通过binder机制获取NMS代理对象,调用到NMS.enqueueToast()将Toast内部类TN做为回调对象传入。NMS经过一系列调用最终回调到TN.show()显示Toast,并在显示Duration时长后回调TN.hide()取消Toast。

TN.show()

        final Handler mHandler;

        public void show(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
        }

            mHandler = new Handler(looper, null) {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case SHOW: {
                            IBinder token = (IBinder) msg.obj;
                            handleShow(token);
                            break;
                        }
                        ......
                    }
                }
            };
        }

        public void handleShow(IBinder windowToken) {
                ......
                mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY,
                        mHorizontalMargin, mVerticalMargin,
                        new CallbackBinder(getCallbacks(), mHandler));
            }
        }

ToastPresenter.show()

    public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity,
        ......
        try {
            //重点
            mWindowManager.addView(mView, mParams);
        } catch (WindowManager.BadTokenException e) {
            Log.w(TAG, "Error while attempting to show toast from " + mPackageName, e);
            return;
        }
    }

Dialog.show()

    public void show() {
        ......
        //DecorView
        mDecor = mWindow.getDecorView();
        //重点
        mWindowManager.addView(mDecor, l);
    }

依然是WindowManager.addView()。下面看改变View的属性时调用的View.invalidate()、改变View的大小时调用的View.requestLayout()

View.invalidate()

public void invalidate() {
        invalidate(true);
    }

public void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
        final ViewParent p = mParent;
        p.invalidateChild(this, damage);
    }

    /**
     * The parent this view is attached to.
     * {@hide}
     *
     * @see #getParent()
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    protected ViewParent mParent;

ViewParent也就是父View,往上调用直到ViewRootImpl.invalidateChildInParent()

public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        ......
        invalidateRectOnScreen(dirty);
    }

private void invalidateRectOnScreen(Rect dirty) {
        ......
        scheduleTraversals();
    }

void scheduleTraversals() {
        ......
        mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    }

流程又走到了熟悉的地方,Choreographer.postCallback(),传入mTraversalRunnable,等待Vsync信号回调mTraversalRunnable.run()走绘制流程。

View.requestLayout()

    public void requestLayout() {
        ......
        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
    }

ViewRootImpl.requestLayout()

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

仍然是熟悉的流程,这里看个大概,实际调用链内部代码还是有些复杂的;需知刷新UI最终都要走ViewRootImpl.performTraversals()

View.post()

由前面流程分析可知View树在onResume()之后才开始走绘制流程,那我们经常使用的View.post()在onCreate()中如何获取到View的宽高属性呢?其实思路很简单,把post传入的Runnable保存下来,等待View树绘制完毕再回调run()方法,具体调用链就不再跟了。

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

推荐阅读更多精彩内容