Android 性能优化(一):布局与绘制优化

一、布局优化

总是首先想到的也是最直观的优化方向。具体的优化方式有:

  • 尽量减少布局中的控件层级,减少嵌套。布局中需要嵌套时不要使用 LinerLayout,改用 RelativeLayout。简单层级布局建议使用 FrameLayout、LinerLayout、RelativeLayout 次之。

要找到布局或页面中多余的 View,可以使用 Android Studio Monitor 里的 Hierarachy Viewer 工具。参考:Hierarchy Viewer使用详解

  • 使用 <include><merge> 进行布局的套用,ViewStub 按需加载。

<include> 无需多说,<merge> 标签一般配合 <include> 使用。被 Include 的布局使用 <merge> 标签,引入时 <merge> 会被忽略让该布局跟随引用者的布局模式。
ViewStub 是一种轻量级的 View,需要显示时必须在 UI 线程加载。

  • 相关工具:除了上文所说的 Hierarachy Viewer 工具,Android 还提供了 Lint 工具。Lint Tool 不仅会提示布局文件使用不当,还会提示其它的代码需要优化的地方,比如 项目中有无用的 import、某些代码可能产生不同版本适配问题等。

H Viewer 和 Lint Tool 的使用可以参考:Android App优化之Layout怎么摆

在此记录自己项目中有关 Lint Tool 的使用:
(1)打开 Lint Tool:

Open Lint.png

(2)选择要检测整个项目或者某个 Moudule 或具体文件的问题。


Lint 选项.png

默认会选择当前打开的文件,我这里选择第一项查看整个项目存在的问题。

(3)查看问题


Lint Wranning.png

如上图,这个项目不算太大但是问题还真不少...上图中选了一个 XML 的问题点击链接到布局文件中存在无用的引用。

但是这里提一下,Lint 工具检测出来的很大一部分属于警告,只是提示开发者或许不该这样或那样做。具体怎样做还是需要开发者自行查看并根据具体的情况对代码和逻辑进行处理。

二、 绘制优化

  • 一方面指 View 的 onDraw 方法要避免大量的操作,包括不要在 onDraw 中创建局部变量,因为 onDraw 是一个经常被调用的方法。同时也不要在 onDraw 中进行耗时任务。
  • 另一方面指过度绘制,所谓的过度绘制是指 屏幕上某些像素点在一帧中被重复绘制多次,就是过度绘制
    查看是否过度绘制可以通过开启手机 开发者选项 -- 调试 GPU 过度绘制 -- 显示过度绘制区域 选项,下面举例说明:

(1)开启 "显示过度绘制区域" 选项:


开启选项.jpg

可以看到屏幕变成花花绿绿的一片了,不同的颜色表示这块 View 绘制的次数:

  • 原色:没有 OverDraw
  • 蓝色:一次 OverDraw
  • 绿色:两次 OverDraw
  • 粉色:三次 OverDraw
  • 红色:四次及以上 OverDraw
    那么设置选项中存在原色、蓝色、绿色和粉色,一般来说不超过粉色或许不存在过度绘制的情况。因为整个设置选项比较简单,也就是选项的那些滑块进行了多次绘制也情有可原。

(2)查看 APP 绘制情况,那么就拿 简书APP 来举例吧


简书主页绘制情况.jpg

这个页面看到了系统状态栏(原色)、简书底部导航栏图片(蓝色)、标题栏 TitleBar(绿色)、标题栏文字以及背景(粉色)、Item 字体(红色)。
这样看的话个人猜测简书 APP 这个页面重复设置了一些 background,也不能说就是特别严重的过度绘制,因为这样做的原因是为了优化用户体验。

(3)过度绘制优化方法:

  • 去除 Activity 自带的默认背景颜色:
    具体做法是在 Activity 使用的主题中进行设置 Background 为 null。当然也要视情况而定,如果你的 Activity 自定义了背景色,那么就可以替换默认的背景色。如果不需要背景色就可以直接删除。
<style name="AppTheme" parent="android:Theme.Light.NoTitleBar">
    <item name="android:windowBackground">@null</item>
</style>

或者在相关 Activity 中:

getWindow().setBackgroundDrawable(null);
  • 使用 Canvas 的 clipRect() 和 clipPath() 方法限制 View 的绘制区域
    一个 Activity 对应一个 Canvas 对象,用来绘制该 Activity 的所有内容。clipRect() 是 Canvas 提供的一个方法,用来裁剪画布上的一个矩形区域,该矩形区域用 Rect 对象来描述。调用该方法后 Canvas 的绘制范围会被限制在裁剪的范围内。

举个栗子:


Android 8.0 文件管理.png

上图是 Pixel XL 手机的文件管理器,左侧是打开的 DrawerLayout,背景布局用来显示具体文件。按照一般的绘制逻辑,背景布局绘制后打开 DrawerLayout,打开的布局绘制次数应该更多才是。但是打开的 DrawerLayout 明显比背景布局绘制次数少,那么原因就要到 DrawerLayout 的源码去找了。

View 绘制流程中,draw 绘制的过程中会执行 dispatchDraw() 方法。这个方法的主要作用是用来绘制绘制子 View,所以是由 ViewGroup 进行了重写,在 ViewGroup 中该方法遍历所有的子 View 并执行 drawChild() 方法让子 View 执行 draw。ViewGroup 默认的 drawChild() 方法直接调用了子 View 的 draw 方法,但是 DrawerLayout 重写了 drawChild() 方法按照自己的逻辑来绘制自己的子 View。

DrawerLayout#drawChild()

@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    final int height = getHeight();
    final boolean drawingContent = isContentView(child);
    // 记录 DrawerLayout 的左右边界
    int clipLeft = 0, clipRight = getWidth();
    // 保存画布
    final int restoreCount = canvas.save();
    // 判断是否绘制内容
    if (drawingContent) {
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View v = getChildAt(i);
            if (v == child || v.getVisibility() != VISIBLE
                    || !hasOpaqueBackground(v) || !isDrawerView(v)
                    || v.getHeight() < height) {
                // 如果child是内容视图/不可见/视图背景透明/不是抽屉视图/child高度小于父布局高度
                // 跳过循环不做裁切
                continue;
            }

            if (checkDrawerViewAbsoluteGravity(v, Gravity.LEFT)) {
                // 抽屉在左侧时记录右侧边界
                final int vright = v.getRight();
                if (vright > clipLeft) clipLeft = vright;
            } else {
                // 反之抽屉在右侧,记录左侧边界
                final int vleft = v.getLeft();
                if (vleft < clipRight) clipRight = vleft;
            }
        }
        // 进行裁切
        canvas.clipRect(clipLeft, 0, clipRight, getHeight());
    }
    // 绘制其它子 View
    final boolean result = super.drawChild(canvas, child, drawingTime);
    // 画布返回之前状态
    canvas.restoreToCount(restoreCount);
    ...
    return result;
}

Android 屏幕绘制是一帧一帧来进行的,当 DrawerLayout 被打开时不断执行它的 drawChild() 方法来记录 DrawerLayout 的边界信息:left、top、right、bottom。当 DrawerLayout 打开时将未被挡住的背景内容区域裁切出来,裁切完毕后把储存着绘制信息的 canvas 交给自己的父 ViewGroup 进行绘制,所以会先绘制未被挡住的背景内容区域。最后再返回之前画布状态绘制其它内容。

  • ImageView 的 src、background、ImageDrawable
    通过 src 设置的 ImageView 图片不会拉伸,可以设置 scaleType 进行图片的比例设置;而通过 background 设置的背景图片会拉伸至整个 ImageView;ImageDrawable 一般用来设置需要加载的图片,一般在代码中使用,当 background 和 ImageDrawable 同时使用时会产生过度绘制的问题,解决方法为都使用 ImageDrawable。

三、UI 卡顿

Android 著名的 "16ms" 法则:

Android系统每隔16ms会发出VSYNC信号重绘我们的界面(Activity).
为什么是16ms, 因为Android设定的刷新率是60FPS(Frame Per Second), 也就是每秒60帧的刷新率, 约合16ms刷新一次.

如果某页面本该在下一个 16ms 完成绘制而没有完成,而是在 30ms 的时候才绘制完成,那么在下一个 16ms 不会更新视图而是在第 32ms 更新,这就造成了丢帧的现象。

其实 UI 卡顿利用上面记录的一些方法来处理已经基本能解决了,如果想了解更多可以搜索其它工具来找出问题如:StrictMode、Systrace 等。

参考文章:

Android性能优化-过度绘制解决方案

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

推荐阅读更多精彩内容