Android View绘制流程

作者: 唠嗑008 | 来源:发表于2020-06-27 15:59 被阅读0次

    前言

    不知道大家有没有想过一个问题,当启动一个Activity的时候,相应的XML布局文件中的View是如何显示到屏幕上的?有些同学会说是通过onMeasure()onLayout()onDraw()这3个方法来完成的,实际上这只是系统暴露给我们使用的最基本的方法,背后的流程要比这个更加复杂,今天就和大家一起扒一下背后还做了什么事情。

    我们知道Activity执行了onCreate()onStart()onResume()3个方法后,用户就能看见视图了。这背后实际上经历了2个过程,其一,通过ActivityThread调度生命周期相关的方法;其二,通过setContentView()把XML解析成View对象。这里兵分2路,先来看一下setContentView()

    setContentView()方法

    这里借用这篇文章的图来表示setContentView整个流程
    https://blog.csdn.net/Rayht/article/details/80782697

    setContentView.png
    override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    }
    

    从这里进去会进去到Activity.setContentView(),最后会调用PhoneWindow.setContentView()

    ##PhoneWindow
    @Override
        public void setContentView(int layoutResID) {
            // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
            // decor, when theme attributes and the like are crystalized. Do not check the feature
            // before this happens.
            if (mContentParent == null) {
                //1.初始化DecorView
                installDecor();
            } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
                mContentParent.removeAllViews();
            }
    
            //判断有没有转场动画
            if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
                final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                        getContext());
                transitionTo(newScene);
            } else {
                //2.解析传进来的xml布局,生成一个ContentView
                mLayoutInflater.inflate(layoutResID, mContentParent);
            }
            mContentParent.requestApplyInsets();
            final Callback cb = getCallback();
            if (cb != null && !isDestroyed()) {
                cb.onContentChanged();
            }
            mContentParentExplicitlySet = true;
        }
    

    这里分为2条线,我们先来看一下初始化mDecor。

    private void installDecor() {
            mForceDecorInstall = false;
            if (mDecor == null) {
                //1.生成DecorView
                mDecor = generateDecor(-1);
                mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
                mDecor.setIsRootNamespace(true);
                if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                    mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
                }
            } else {
                mDecor.setWindow(this);
            }
            if (mContentParent == null) {
                //2.生成mContentParent
                mContentParent = generateLayout(mDecor);
        }
        .......
    }
    
    protected DecorView generateDecor(int featureId) {
            Context context;
            ......
            //DecorView extends FrameLayout
            return new DecorView(context, featureId, this, getAttributes());
        }
    

    mDecor就是DecorView对象,它是PhoneWindow的顶层View,查看DecorView源码可以看出DecorView 是一个FrameLayout,但是源码中并没有展示出它是一个怎样的布局,因为它是在注释2mContentParent = generateLayout(mDecor)中添加的,下面就来一起看一下。

    ##PhoneWindow. generateLayout()
    protected ViewGroup generateLayout(DecorView decor) {
              // Inflate the window decor.
    
            int layoutResource; //是一个布局文件id
            int features = getLocalFeatures();
            // 根据不同的主题将对应的布局文件id赋值给layoutResource
            if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
                layoutResource = R.layout.screen_swipe_dismiss;
                setCloseOnSwipeEnabled(true);
            } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
                ....
                }
                ....          
             else {
                layoutResource = R.layout.screen_simple;
            }
    
            mDecor.startChanging();
            //通过LayoutInflater加载解析layoutResource布局文件
            mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    
            .......
    }
    

    这里展示的是generateLayout的上半部分,先根据不同的主题生成不同的布局文件,然后解析layoutResource布局文件。

    onResourcesLoaded()

    ##DecorView. onResourcesLoaded()
    void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
            .....
    
            mDecorCaptionView = createDecorCaptionView(inflater);
            //1.通过XmlResourceParser解析XML布局文件,得到一个View对象
            //这里的root就是DecorView中添加的layoutResource 
            final View root = inflater.inflate(layoutResource, null);
            //2.把layoutResource布局文件添加到DecorView中
            if (mDecorCaptionView != null) {
                if (mDecorCaptionView.getParent() == null) {
                    addView(mDecorCaptionView,
                            new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
                }
                mDecorCaptionView.addView(root,
                        new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
            } else {
    
                // Put it below the color views.
                addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
            }
            mContentRoot = (ViewGroup) root;
            initializeElevation();
        }
    

    现在我们知道了,generateLayout其实是给DecorView添加一个布局文件,下面就来看一下DecorView到底是怎样的布局?

    上面说过会根据不同的主题选择不同的layoutResource,这里我们看一下最常用的layoutResource = R.layout.screen_simple

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        android:orientation="vertical">
        <!-- ActionBar    -->
        <ViewStub android:id="@+id/action_mode_bar_stub"
                  android:inflatedId="@+id/action_mode_bar"
                  android:layout="@layout/action_mode_bar"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  android:theme="?attr/actionBarTheme" />
        <FrameLayout
             android:id="@android:id/content"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:foregroundInsidePadding="false"
             android:foregroundGravity="fill_horizontal|top"
             android:foreground="?android:attr/windowContentOverlay" />
    </LinearLayout>
    

    可以看出这是一个垂直的LinearLayout,上面是一个ActionBar,下面是一个id为content的FrameLayout,先给大家透漏一下,这个FrameLayout就是用于装载平时我们写的Activity中的xml布局。

    generateLayout下半部分

    ##PhoneWindow. generateLayout()
    protected ViewGroup generateLayout(DecorView decor) {
            int layoutResource; //是一个布局文件id
            ......
            //往DecorView中添加layoutResource布局文件
            mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
            //查找DecorView中id为ID_ANDROID_CONTENT的View
            ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
            if (contentParent == null) {
                throw new RuntimeException("Window couldn't find content container view");
            }
            .......
          return contentParent;
    }
    

    这里就更简单了,通过findViewById找到之前布局中id为content的view,最后返回到这个contentParent就是刚才的FrameLayout。

    我们再回到最开始的setContentView()方法

    ##PhoneWindow
    @Override
        public void setContentView(int layoutResID) {
            // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
            // decor, when theme attributes and the like are crystalized. Do not check the feature
            // before this happens.
            if (mContentParent == null) {
                //1.初始化DecorView
                installDecor();
            } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
                mContentParent.removeAllViews();
            }
    
            //判断有没有转场动画
            if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
                final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                        getContext());
                transitionTo(newScene);
            } else {
                //2.解析传进来的xml布局,生成一个ContentView
                mLayoutInflater.inflate(layoutResID, mContentParent);
            }
            mContentParent.requestApplyInsets();
            final Callback cb = getCallback();
            if (cb != null && !isDestroyed()) {
                cb.onContentChanged();
            }
            mContentParentExplicitlySet = true;
        }
    

    上面我们讲了installDecor()实际上就是创建并初始化DecorView对象,也就是完成了mContentParent的初始化,接下来看一下注释2,这里是解析Activity中传进来的布局layoutResID,其中parent是mContentParent,也就是之前说过的那个FrameLayout,这样就把Activity中的布局添加到DecorView中了。

    小结一下:

    setContentView()流程如下:
    1、在PhoneWindow中创建顶层的DecorView;
    2、在DecorView中会根据主题的不同加载一个不同的布局;
    3、把Activity中的布局解析并添加到DecorView的FrameLayout中

    这是我画的一幅图,按照这个去看源码会比较清晰。

    setContentView().png

    到这里算是把setContentView流程分析完了,但把我们仅仅是把自己的Layout添加到DecorView中,但如何显示到屏幕上还没看呢。

    布局文件中的UI是如何显示的呢?

    其实在文章开头说过了,在ActivityThread的生命周期调度中完成的。刚才已经看过了onCreate(),后面还会调用onStart()、onResume(),而最终UI的显示就是在onResume()中完成的,也就是说我们平常接触最多的measurelayoutdraw3个方法都在onResume中完成的。而onResume()是在ActivityThread的handleResumeActivity()中调用的。

     public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
                String reason) {
        unscheduleGcIdler();
            mSomeActivitiesChanged = true;
    
            // 1.会调用Activity的onResume
            final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
            
            final Activity a = r.activity;
    
          
            final int forwardBit = isForward
                    ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
    
            // If the window hasn't yet been added to the window manager,
            // and this guy didn't finish itself or start another activity,
            // then go ahead and add the window.
            boolean willBeVisible = !a.mStartedActivity;
            if (!willBeVisible) {
                try {
                    willBeVisible = ActivityTaskManager.getService().willActivityBeVisible(
                            a.getActivityToken());
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                //获取WindowManager对象
                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) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true;
                       //2.WindowManager添加DecorView
                        wm.addView(decor, l);
                    } else {
                        // The activity will get a callback for this {@link LayoutParams} change
                        // earlier. However, at that time the decor will not be set (this is set
                        // in this method), so no action will be taken. This call ensures the
                        // callback occurs with the decor set.
                        a.onWindowAttributesChanged(l);
                    }
                }
                ......
    }
    

    这里最关键的地方是注释1和注释2,这里先看一下注释2,wm是一个接口ViewManager对象,而wm是通过Activity的getWindowManager()获取的,最后你会发现wm是在WindowManagerImpl中初始化的,

    ##WindowManagerImpl
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
            applyDefaultToken(params);
            mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
        }
    
    ##WindowManagerGlobal
    public void addView(View view, ViewGroup.LayoutParams params,
                Display display, Window parentWindow) {
        ....
         ViewRootImpl root;
         View panelParentView = null;
          //创建ViewRootImpl对象,挺重要的一个类,后面会解释
         root = new ViewRootImpl(view.getContext(), display);
    
                view.setLayoutParams(wparams);
    
                mViews.add(view);
                mRoots.add(root);
                mParams.add(wparams);
    
          
                try {
                    //1.关键调用
                    root.setView(view, wparams, panelParentView);
                } catch (RuntimeException e) {
                    // BadTokenException or InvalidDisplayException, clean up.
                    if (index >= 0) {
                        removeViewLocked(index, true);
                    }
                    throw e;
                }
            }
    }
    

    这里最关键的是调用了setView()方法,这个方法内又调用了requestLayout()

    ##ViewRootImpl
    public void requestLayout() {
            if (!mHandlingLayoutInLayoutRequest) {
                //检查是否在UI线程
                checkThread();
                mLayoutRequested = true;
                
                scheduleTraversals();
            }
        }
    
    void scheduleTraversals() {
            if (!mTraversalScheduled) {
                mTraversalScheduled = true;
                mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
                //callback与Choreographer交互,会在下一帧被渲染时触发
                mChoreographer.postCallback(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                if (!mUnbufferedInputDispatch) {
                    scheduleConsumeBatchedInput();
                }
                notifyRendererOfFramePending();
                pokeDrawLockIfNeeded();
            }
        }
    

    这里的 mChoreographer实际上是Choreographer对象,Choreographer是在屏幕刷新机制中接收显示系统的VSync信号,postFrameCallback设置自己的callback与Choreographer交互,你设置的callCack会在下一个frame被渲染时触发。这里简单了解下即可。这里重点关注下mTraversalRunnable对象。

    ##ViewRootImpl
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
    
    final class TraversalRunnable implements Runnable {
            @Override
            public void run() {
                doTraversal();
            }
        }
    

    traversal是遍历的意思,也就是说后面会做遍历操作,至于为什么,接着往下看。

    ##ViewRootImpl
     void doTraversal() {
            if (mTraversalScheduled) {
                mTraversalScheduled = false;
                mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
    
                if (mProfile) {
                    Debug.startMethodTracing("ViewAncestor");
                }
                //关键代码
                performTraversals();
    
                if (mProfile) {
                    Debug.stopMethodTracing();
                    mProfile = false;
                }
            }
        }
    

    performTraversals()
    这段代码非常长,但是核心的地方就是下面注释的3处。

    ##ViewRootImpl
    private void performTraversals() {
         int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
         int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);      
         ......
         // Ask host how big it wants to be
         //1.执行测量 
         performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
         ......
         //2.执行布局
         performLayout(lp, mWidth, mHeight);
        ......
         //3.执行绘制
         performDraw();
    }
    
    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
            if (mView == null) {
                return;
            }
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
            try {
                  //调用View的measure()
                mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
        }
    
    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
                int desiredWindowHeight) {
            mLayoutRequested = false;
            mScrollMayChange = true;
            mInLayout = true;
    
            final View host = mView;
    
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
           //调用View的layout()
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    }
    
    private void performDraw() {
       ......
       draw(fullRedrawNeeded);
       ......
    }
    
     public void draw(Canvas canvas) {
          ......
          //调用View的onDraw()
          onDraw(canvas);
          ......
    }
    
    • performMeasure():从根节点向下遍历View树,完成所有ViewGroup和View的测量工作,计算出所有ViewGroup和View显示出来需要的高度和宽度;

    • performLayout():从根节点向下遍历View树,完成所有ViewGroup和View的布局计算工作,根据测量出来的宽高及自身属性,计算出所有ViewGroup和View显示在屏幕上的区域;

    • performDraw():从根节点向下遍历View树,完成所有ViewGroup和View的绘制工作,根据布局过程计算出的显示区域,将所有View的当前需显示的内容画到屏幕上。

    现在明白了为什么前面那个方法的名字是遍历了吧,因为最后是要完成以DecorView为根节点的view树的遍历。

    大家对照我画的这幅图去看源码,会比较好理解。


    View绘制流程.png

    关于View绘制流程真的很长,代码量也大,但是我们只需要关注核心流程就可以了。最后做一个总结:

    • 1、在onCreate 方法中通过setContentView将XML布局解析成java对象,并添加到PhoneWindow的DecorView中;
    • 2、在onResume中将DecorView添加到WindowManagerImpl中,然后通过ViewRootImpl来执行View的绘制流程;
    • 3、在ViewRootImplperformTraversals()方法中分别调用performMeasureperformLayoutperformDraw来完成测量、布局、绘制;
    • 4、performMeasureperformLayoutperformDraw这几个方法最终会调用measure()layout()draw()来完成最终的绘制。

    参考

    Android 自定义View之View的绘制流程(一)
    【朝花夕拾】Android自定义View篇之(一)View绘制流程

    相关文章

      网友评论

        本文标题:Android View绘制流程

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