1. 1. 前言
  2. 2. 目录
  3. 3. 关键类解释
  4. 4. 一. 流程图分析
    1. 4.1. 1.1 创建Activity到setContentView的窗口附加流程图
    2. 4.2. 1.2 view绘制流程图
  5. 5. 二. view绘制流程
    1. 5.1. 2.1 绘制流程分析
    2. 5.2. 2.2 ViewRootImpl 创建时机
  6. 6. 三. 附加contentView到界面
    1. 6.1. 3.1 创建PhoneWindow
    2. 6.2. 3.2 setContentView的本质
    3. 6.3. 3.3 确认window ,decorView 以及 subDecorView
    4. 6.4. 3.4 初始化 installDecor
    5. 6.5. 3.5 小结:
      1. 6.5.0.1. 启动activity
      2. 6.5.0.2. 创建PhoneWindow
      3. 6.5.0.3. 设置布局setContentView
      4. 6.5.0.4. addView的时候调用 requestLayout(); invalidate(true);
      5. 6.5.0.5. 等待绘制时机(handleResumeActivity之后才会触发绘制),通过Choreographer 遍历view树的布局和绘制操作。
  • 7. 最后
  • 天空看不见云,大火球在上面肆意发光,逼着毛孔慢慢渗出汗水。

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

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

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

    前言

    从一个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 绘制流程分析

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

    1. 调用requestLayout invalidate标记绘制和充布局信息
    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. 注册mDisplayListener DisplayManager,接收显示更新回调
      2. 调用 requestLayout 更新一次布局大小和位置信,以确保从系统接收任何其他事件之前进行过一次布局
      3. 通过WindowSession 调用addToDisplayAsUser,添加window
    4. 在接收系统事件的时候,调用scheduleTraversals 绘制view树
      WindowMangerGlobal 最终调用的其实都是ViewRootImpl方法。ViewRootImpl在addView关联号DecorView后,还调用了setView方法进行初始化,接收垂直同步脉冲信息,代码如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @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()的主要实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @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知识。了解绘制原理不止对面试有帮助。对于自己的开发工作也有不小的助力。