「深入理解Android布局优化 1」-布局的加载流程与绘制原理

前言

本篇文章是《深入理解Android布局优化》系列文章的第一篇。系列的主要目的是希望将Android开发中涉及布局优化的部分做一次系统的归纳、总结和学习。本系列文章包含理论基础常见工具项目实践三个部分。

理论基础:「深入理解Android布局优化 1」-布局的加载流程与绘制原理,主要讲解布局的加载流程与绘制原理,从源码上发现布局的性能瓶颈。

常见工具:「深入理解Android布局优化 2」-常见工具的使用,主要讲解Android布局优化时各种常见工具的使用。

项目实践:以一个实际的APP为例,将学习到的理论和工具,实际运用到Android开发中。

本文中实践时使用的项目地址:https://github.com/linux-link/Fan,可以先阅读这篇文章了解这个项目一次组件化与Android Jetpack的实践

本篇属于三个部分中的理论基础部分。

目录

  • Android系统的绘图机制
  • Activity的组成
  • 布局文件的加载流程
  • View的绘制流程
  • 布局优化的简单建议
  • 总结

正文

一、Android系统的绘图机制

Android系统每隔16ms就重新绘制一次Activity,这就要求UI界面必须在16ms内完成屏幕刷新的全部逻辑操作,这样才能达到每秒60fps,然而这个fps是由手机硬件所决定,现在大多数手机屏幕刷新率是60Hz(赫兹是国际单位制中频率的单位,它是每秒中的周期性变动重复次数的计量),也就是说我们有16ms(1000ms/60fps=16.66ms)的时间去完成每帧的绘制逻辑操作,如果超过了就会出现所谓的丢帧。实际开发中复杂的界面往往在16ms内完成全部绘制,但是尽量降级UI的绘制时间,总是可以有效的降低卡顿感。

对于Android系统的硬件绘图机制,并非布局优化的重点,有兴趣的可以翻看文末的参考资料。

二、Activity的组成

一个Activity层级结构图,如下所示


它有点像洋葱圈一层包裹着一层,下面我们就来逐个介绍一下。

  • PhoneWindow

    PhoneWindow是Window的子类,Window是顶级窗口外观和行为策略的抽象基类。它提供标准的UI策略,例如背景,标题区域,默认密钥处理等。它的唯一实现就是PhoneWindow

  • DecorView

    DecorView是一个ViewGroup类,继承自FrameLayout,是Activity在绘制布局文件时的宿主,也可以把它理解为绘制布局文件时的“画布”。

  • TitleActionBar

    Android提供一个默认的ActionBar,我们在写demo时经常会看到这个ActionBar,一般正式开发时,会在Style.xml中把它去掉.

    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    
  • ContentView

    ContentView就是我们在setContentView时传入的xml布局文件绘制出来的ViewGroup,在Activity(kotlin语言)中我们可以通过如下代码获取到各个ContentView

    //kotlin
    window.decorView.findViewById<ViewGroup>(android.R.id.content).getChildAt(0)
    //java
    getWindow().getDecorView().findViewById(android.R.id.content).getChildAt(0)
    

通过这张层级关系图,我们就大致明白了Activity层级结构,理解Activity的页面层级结构非常的重要,它不仅与性能优化息息相关,而且也可以帮助我们理解Android触摸事件的分发机制。

触摸事件的分发机制,经常涉及到自定义的View,自定义View其实也是我们在布局优化时常用的手段之一。

这里重新画了一张“洋葱圈”一样的层级结构图,来帮助你理解触摸事件的向上传递机制。这张图很形象的解释了触摸事件是如何从Activity中开始传递,又是如何回到Activity中的。关于触摸事件的分发具体的分发机制,请参阅其他文章,这里就不再细说了。

洋葱圈结构图

三、布局文件的加载流程

在Android开发中setContentView是我们最常用的将xml格式的布局文件绘制到activity中的方法。那么布局文件是如何绘制到Activity当中的呢?通过阅读setcontentView的源代码,可以发现布局文件的加载大致分为,读取xml创建View对象两个流程。

image

1.读取xml布局文件

  • setContentView

    setContentView很好理解,就是向Activity的DecorView中装载布局文件。Activity再通过decorView拿到当前设定的布局,交给LayoutInflater解析。

    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
     
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    
  • LayoutInflater.inflate()

    LayoutInflater,在Android系统通过它将布局XML文件实例化为其对应的View 对象。在inflate中通过loadXmlResourceParser方法来读取xml布局文件,并把读取到的文件流封装到XmlResourceParser中。这样就把xml布局文件就从存储器中放到了内存中,注意loadXmlResourceParser是一个IO操作

2.根据xml布局文件,创建对应的View或ViewGroup

  • LayoutInflater.createViewFromTag()

    在loadXmlResourceParser把文件流装载到XmlResourceParser之后,LayoutInflater会调用createViewFromTag方法,根据标签来创建对应的View对象,例如根据读取到的<TextView>创建TextView对象。

    createViewFromTag创建View对象主要是Fractory的onCreateView或是调用createView方法来实现,createView内部具体是通过反射来创建View对象。

简单梳理一遍View的加载流程,你会发现,到这里Android系统就完成了把xml布局文件转换成具体的View对象,在这其中我们可以看到至少两个会影响性能地方,一个是loadXmlResourceParser(),把xml读取到内存中这样的IO操作会影响性能,另一个则是createView(),通过反射创建对象会影响性能。这两个地方将是我们日后进行布局优化的重点。

目前为止Activity还是看不任何东西的,因为创建的View还没有开始绘制。接下来我们就来看看View的绘制流程。

四、View的绘制流程

View绘制流程主要分为三个部分:measure、layout、draw,分别对应测量、布局和绘制,其中measure确定View的测量宽高,layout确定View最终宽高和四个顶点的位置,draw负责将view最终绘制到屏幕上。

ViewGroup的绘制流程与View大体相同,唯一的区别就是,View只需要绘制它自己,而ViewGroup不仅要绘制它自己还要绘制它的子View。下面我们就以ViewGroup为例,简单从源代码的角度来看一下这三个流程:

1.Measure与MeasureSpec

测量过程通过measure()来实现,是View树自顶向下的遍历,每个View在循环过程中将尺寸细节向下传递,当测量过程完成之后,所有的View也就都存储了自己尺寸。

ViewGroup是一个抽象类,它并没有重写View的measure()方法,它在内部会调用measureChildren(),然后再去循环调用View的measure()方法。

measure()方法需要传入两个参数widthMeasureSpec和heightMeasureSpec。

protected void measure(int widthMeasureSpec, int heightMeasureSpec)

表面上看widthMeasureSpec和heightMeasureSpec是int的数字,它们是父类传过来的给当前View的一个建议值(这个建议值是我们在XML中设定的),实际上是由mode+size组成的。将widthMeasureSpec转换为二进制后,它是一个32位的数字,前两位表示模式(mode),后30位表示数值(size)。

mode共有三种模式,分别是

  • UNSPECIFIED(未指定)

    不做任何限制,View可以获得任意大小。它一般用于系统的内部测量过程。

  • EXACTLY(完全)

    由父View决定子View的确切大小,子View将被限定在给定边界里而忽略它自身的大小。对应match_parent和具体的dp值

  • AT_MOST(至多)

    View最多达到指定大小的值,对应wrap_content

上述3中模式在自定义view时非常有用,当模式是EXACTLY时,我们是直接使用父类的建议值,当模式是AT_MOST时,我们则需要自己设定View的大小,因为用户没有规定这个View有多大。

2.layout

Layout的作用是ViewGroup用来确定子View的位置。在ViewGroup中调用layout方法确定位置确定后,它会在onLayout中遍历所有子View的layout方法,子View的layout又会调用onLayout方法,确定自己的位置。

layout的大致流程如下:

首先通过setFrame设定View的四个顶点位置;

然后调用onLayout方法,在这里面调用每个子View的layout

3.draw

draw的过程是最简单的,它的作用就是把View绘制到屏幕上,

 public void draw(Canvas canvas) {}

在draw方法中主要完成了一下几个任务:

  • 使用drawBackground方法绘制背景
  • 在onDraw中绘制自己
  • 在dispatch中绘制子View
  • 在onDrawScrollBars中绘制装饰

在Android中draw方法会被频繁的调用,例如:按home键app进入后台,当我们在回到APP时,即使APP没有被销毁,当前界面下View组件的draw方法也会被调用。

简单了解了View的绘制流程后,不难看出这里面也存在至少两个性能瓶颈,一个是measure和layout过程中会循环调用子View的方法,其实这就决定了布局文件不能嵌套过深,否则循环的时间复杂度会很高。另一个是View的draw方法会被频繁的调用,对于这类频繁调用的方法,我们不能在其中创建对象或执行耗时操作,否则会产生剧烈的内存抖动和页面卡顿。

五、布局优化的简单建议

通过上面的分析,我们对布局的组成,加载以及绘制有了一定的了解,现在再来看看常见的布局优化建议,相信你一定对这些建议有了进一步的认识。

  • 使用ConstraintLayout减少布局嵌套

    ConstraintLayout是Google推出一种可以有效减少嵌套问题的布局,它可以让你的布局更加的扁平化,如果你没有使用过ConstraintLayout,强烈推荐使用。

  • 使用<include/>和<merge/>标签来减少布局嵌套

    <include/>标签可以将一个指定的布局引入到当前的布局中,通过这种方式可以复用项目中已经存在的布局。有时候被引用的布局顶级节点与外部布局存在重复的情况,这时就可以使用<merge/>将多余的顶级节点去掉。关于<merge/>

  • 使用ViewStub延迟加载布局

    ViewStub继承了View,它的宽高都是0,因此它不参与任何布局与绘制的过程。在开发中有的布局正常情况下并不显示,这时候就可以使用ViewStub,在布局初始化的时候可以避免加载这类并不需要立即显示的布局。

  • 不要在onDraw()创建对象或执行耗时操作

    具体原因在上面已经说过了,这里就不赘述了。

  • 不使用xml布局

    使用xml布局文件,Android需要通过IO操作把xml布局文件加载到内存中,然后通过反射创建view对象,如果不使用xml就可以完全避免这些影响的性能操作。使用这类思想创建布局文件框架有的iReader的X2C和FaceBook的Litho,不过这是一类很极端的做法,并不推荐。

  • 复杂布局使用自定义View

    当App设计图非常复杂,我们需要使用非常多的系统组件组合才能实现相似的功能时,建议使用自定义View,保持界面的扁平化。

六、总结

本篇文章梳理了一下Activity的组成,一个xml的布局是如何加载到界面中,以及是如果绘制出来的,最后总结了一下目前的布局优化建议。但是在实际的开发中往往很难让所有人完全遵守布局优化的建议,下一篇我们来讲讲布局优化时常用的工具「深入理解Android布局优化 2」-常见工具的使用,通过工具来帮助我们发现UI的性能问题。

参考资料

Android进阶——性能优化之布局渲染原理和底层机制详解(四)

《Android开发艺术探索》 任玉刚著

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

推荐阅读更多精彩内容