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

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

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

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

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

前言

从一个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知识。了解绘制原理不止对面试有帮助。对于自己的开发工作也有不小的助力。

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

推荐阅读更多精彩内容