美文网首页AndroidAndroid Framework安卓
【Android进阶】这一次把View绘制流程刻在脑子里!!

【Android进阶】这一次把View绘制流程刻在脑子里!!

作者: 吃人的锅 | 来源:发表于2021-07-27 10:50 被阅读0次

    天空看不见云,大火球在上面肆意发光,逼着毛孔慢慢渗出汗水。

    我离开舒适区,跑出去面试了几次。

    得到的最多的反馈是不够深入。

    作为一个五年经验的安卓开发者,欠缺的还有很多。

    前言

    从一个view实例被创建,到展示到屏幕上,都经历了怎么样的一个流程?在安卓开发中,这似乎是一个基本的知识,应该被开发者清楚地认识明白,面试中也作为问题频频出现,然而我还是认识得不深刻。
    Android View的绘制流程 是View相关的核心知识点。我希望通过这篇文章学习并分享Android View绘制流程的始末。
    并将其刻在脑子里。

    目录

    本文分为以下流程学习,阅读完本文将会学习到PhoneWindow,WindowManger,ViewRootImpl,View 等关键类的联系和作用。对window窗体机制以及绘制流程有所了解。

    1. 流程图分析
    2. 了解view绘制流程
    3. 了解setContentView如何附加到内容到页面

    关键类解释

    • Choreographer:协调动画、输入和绘图的时间。Choreographer从显示子系统接收定时脉冲(例如垂直同步),然后安排工作发生,作为渲染下一个显示帧的一部分。

    一. 流程图分析

    1.1 创建Activity到setContentView的窗口附加流程图

    下图展示了window的创建到setContentView之后的窗体view树变化情况

    activity 设置布局流程

    1.2 view绘制流程图

    绘制流程图

    二. view绘制流程

    2.1 绘制流程分析

    在我们调用requestLayoutinvalidate的时候,我们会让view刷新布局和绘制。所以从这两个方法入手,可以完整地走一遍绘制流程。
    绘制动画等行为主要通过Choreographer 类协调。

    1. 调用requestLayoutinvalidate标记绘制和充布局信息
    2. Choreographer接受系统垂直同步等脉冲消息,在scheduleTraversals方法中回调执行doTraversal 开始遍历view树。
    3. 触发ViewRootImpl#performTraversals完成view树遍历
      1. 如果layoutRequested 为true,measureHierarchy 中测量 mView 及其子view
      2. 需要的话,触发ViewRootImpl#performLayout 完成布局
      3. 如果view没有隐藏且TreeObserver中没有拦截绘制,就调用performDraw,完成绘制
        1. 计算dirty脏区域
        2. 从mSurface中 获取脏区域的canvas,交给view绘制

    2.2 ViewRootImpl 创建时机

    从上面可以看到,所有的绘制和布局都是由ViewRootImpl#doTraversal触发,然后对其持有的view树进行遍历绘制。所以一定要了解ViewRootImpl和其持有的DecorView的创建和关联时机。关键流程如下:

    1. Activity#handleResume 的时候,调用WIndowManager#addView添加decorView
    2. 调用到WindowManagerGlobal#addView 的时候创建ViewRootImpl实例。
    3. 调用ViewRootImpl#setView完成一系列初始化方法
      1. 注册mDisplayListenerDisplayManager,接收显示更新回调
      2. 调用 requestLayout更新一次布局大小和位置信,以确保从系统接收任何其他事件之前进行过一次布局
      3. 通过WindowSession调用addToDisplayAsUser,添加window
    4. 在接收系统事件的时候,调用scheduleTraversals 绘制view树
      WindowMangerGlobal 最终调用的其实都是ViewRootImpl方法。ViewRootImpl在addView关联号DecorView后,还调用了setView方法进行初始化,接收垂直同步脉冲信息,代码如下:
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
            int userId) {
                ...
                mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
                ...
                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                requestLayout();
               ...
               try{
                    res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mDisplayCutout, inputChannel,
             
                } 
    }
    

    在初始化的最后,通过WindowSession 调用addToDisplayAsUser添加了window到屏幕显示中。

    三. 附加contentView到界面

    当我们启动activity,将我们写的xml布局文件显示在屏幕上,其中经历了那些过程呢?我们要在界面上展示内容,有如下几个步骤:

    1. 启动activity,在performLaunchActivity的时候创建Activity并且attach和调用onCreate方法
    2. 在attach的时候,创建PhoneWindow实例并持有mWindow引用
    3. 调用setContentView 以附加内容到windows中
    4. 通过确认decorView以及 subDecorView存在,创建DecorViewsubDecorView
    5. 添加ContentViewdecorView树中的 R.id.content节点
    6. handleResumeActivity的时候,调用WindowManager.addView。关联ViewViewRootImpl,后续便可以绘制。

    3.1 创建PhoneWindow

    我们先看启动activity的方法,ActivityThread#performLaunchAcivity。 从该方法源码中可知,启动activity的方法流程如下:

    1. 创建Activity实例 ,在Instrumentation#newActivity完成
    2. 创建PhoneWindows附加到Activity。在Activity#attachAcitivity完成
    3. 调用Activity的onCreate生命周期,代码是Instrumentation#callActivityOnCreate
    4. onCreate中执行用户自定义的代码,比如setContentView
      所以可知,在activity准备启动的时候,就已经完成了PhoneWindows实例的创建。而接下来就执行到了我们在Activity#onCreate中调用setContentView方法设置的自定义布局。

    3.2 setContentView的本质

    activity在启动之后,我们通常在onCreate调用setContentView中设置自己的布局文件。我们来具体看看setContentView做了什么。
    setContentView方法本质其实是向android.R.id.content添加自己。
    我们看AppCompatDelegateImpl#setContentView

    @Override
    public void setContentView(View v, ViewGroup.LayoutParams lp) {
        ///确认好 window decorView 以及 subDecorView
        ensureSubDecor();
        //向 android.R.id.content 添contentView
        ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        contentParent.addView(v, lp);
        mAppCompatWindowCallback.getWrapped().onContentChanged();
    }
    

    这一块代码关键在于向id为android.R.id.content的子view中添加contentView
    addView的过程自然会触发布局的重新渲染。
    关键之处还是在于ensureSubDecor()方法中对于decoView以及subDecorView的实例化创建工作。

    3.3 确认window ,decorView 以及 subDecorView

    先看看AppCompatDelegateImpl#ensureSubDecor()的主要实现:

    private void ensureSubDecor() {
        if (!mSubDecorInstalled) {
            mSubDecor = createSubDecor();
        }
    }
    private ViewGroup createSubDecor() {
        // Now let's make sure that the Window has installed its decor by retrieving it
        ensureWindow();
        mWindow.getDecorView();
    
        final LayoutInflater inflater = LayoutInflater.from(mContext);
        ViewGroup subDecor = null;
    
        //省略其他样式subDecor布局的实例化
        //包含 actionBar floatTitle ActionMode等样式
       subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
      
    
        //省略状态栏适配代码
        //省略actionBar布局替换代码
        mWindow.setContentView(subDecor);
        return subDecor;
    }
    

    代码很长,上面是经过省略之后的主要代码。可以看到代码逻辑很清晰:

    • 步骤一:确认window并attach(设置背景等操作)
    • 步骤二:获取DecorView,因为是第一次调用所以会installDecor(创建DecorView和Window#ContentLyout)
    • 步骤三:从xml中实例化出subDecor布局
    • 步骤四:设置内容布局: mWindow.setContentView(subDecor);

    3.4 初始化 installDecor

    关键两处代码是Window#installDecorWindow#setContentView
    先看一下Window#installDecor的代码:

    private void installDecor() {
        mForceDecorInstall = false;
        mDecor = generateDecor(-1);
        if (mContentParent == null) {
            //R.id.content
            mContentParent = generateLayout(mDecor);
            final decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);
    
            if (decorContentParent != null) {
                //...省略一些decorContentParent的处理
            } else {
                mTitleView = findViewById(R.id.title);
                final View titleContainer = findViewById(R.id.title_container);
                ///省略设置mTitle 设置标题容器显示隐藏
            }
    
            //设置decor背景
            //省略activity各种动画的实例化
        }
    }
    

    这一块除了一些标题。动画的初始化之外,最为关键的就是

    • 通过generateDecor()生成了DecorView
    • 以及通过generateLayout()获取了ContentLayout
      • 获取windowStyle的各种属性,并设置Features和WindowManager.LayoutParams.flags等
      • 如果window是顶层容器,获取背景资源等信息
      • 获取各种默认布局实例化( R.layout.screen_simple等),加到DecorView中。和AppComptDelegateImpl#createSubDecor创建的subDecor类似。
      • 获取com.android.internal.R.id.content 布局,并返回为ContentLayout

    接下来再看Window#setContentView了:

    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        // 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) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }
    
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            view.setLayoutParams(params);
            final Scene newScene = new Scene(mContentParent, view);
            transitionTo(newScene);
        } else {
            mContentParent.addView(view, params);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }
    

    关键代码很简单,就是往mContentParent中添加view。而从上文可知,mContentParent就是andorid.R.id.content的布局。

    3.5 小结:

    分析得知,xml 编写layout布局到展示布局在界面上,经历了这么个流程:

    1. 启动activity

    2. 创建PhoneWindow

    3. 设置布局setContentView

      1. 确认subDecorView的初始化
        1. 初始化生成DecorView
          1. Window中 创建DecorView
          2. Window中 创建样例到代码布局作为DecorView的子布局(比如R.layout.smple)
          3. 返回 com.android.internal.R.id.content 作为ContentPrent
          4. Window中 处理DecorContentParent布局,或者处理标题等内容
        2. 实例化subDecorView,如R.layout.abc_screen_simple
        3. 设置 subDecorView到Window的ContentPrent
      2. 添加实例化的Layout 到android.R.id.content
    4. addView的时候调用 requestLayout(); invalidate(true);

      1. requestLayout遍历View树到DecorView,调用ViewRootImpl#requestLayoutDuringLayout
      2. invalidate 判断区域内的view,将需要刷新的view设置为dirty。
    5. 等待绘制时机(handleResumeActivity之后才会触发绘制),通过Choreographer 遍历view树的布局和绘制操作。

    据此算是完全搞清楚了setContentView的时候经历了什么。也明白了activity如何根据float, title等属性生成不同的布局了。

    最后

    这一篇详细介绍了view的绘制系统,同时也是window窗口机制以及 android显示机制的前置知识。view系统是我们ui开发过程中接触最深的android知识。了解绘制原理不止对面试有帮助。对于自己的开发工作也有不小的助力。

    相关文章

      网友评论

        本文标题:【Android进阶】这一次把View绘制流程刻在脑子里!!

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