android 布局和绘制优化

前言:Android绘制一帧页面的时间是16ms,也就是需要在16ms内把一个页面的UI都渲染好(包括测量、放置、绘制等),超过这个时间,就需要用下一次的16ms来渲染,这样前16ms看起来就什么事也没做,简单的说就是丢帧了(说的比较浅,具体看这篇文章),从而让用户感觉到卡顿。渲染时间超过16ms一个很重要的原因就是布局比较差,过度绘制严重。下面将在这两点在优化上做下总结。

16ms Per Frame

1. 系统自带的层次,DecorView开始到内容栏目(3层)

前3层系统自带.png
  • DecorView为整个Window界面的最顶层View,是FrameLayout的子类啊,是PhoneWindow的内部私有类。
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
……
}
  • DecorView只有一个子元素为LinearLayout,LinearLayout里包含标题栏,内容显示栏
    • 标题栏 (ViewStub ),当设置为无标题主题时,就不加载布局,只显示ViewStub
    • 内容显示栏(frameLayout),通过setContentView()方法载入的布局界面,就是加入其中
    • -- 据说顶部的通知栏也在LinearLayout里 **

2. 布局优化

2.1 多用merge来减少层次,include来复用布局,ViewStub来懒加载

  • <merge/>可以和父布局合并,消除视图层次结构中多余的视图组。
  • <include />标签能够重用布局文件,标签中所有的Android:layout_*都是有效的,前提是必须要写layout_width和layout_height两个属性。<include />标签设置的新属性会覆盖引用的layout里面的属性。
  • <ViewStub />标签最大的优点是当你需要时才会加载,使用他并不会影响UI初始化时的性能。各种不常用的,可能不会出现的布局建议用<ViewStub />

2.2 封装view带来了布局的加深,简单的布局不建议封装成view(比如TitleBar)

android中封装view带来了使用上的方便,也可能带来了布局层次上的加深,简单封装一个view的方法就是先写个布局,然后在inflate进来,要记得布局文件的顶层用<merge/>,这样相比较没用merge就减少了一层,但是比起布局直接写在主布局中,还是会多一层。
如图,先单看这个消息中心的titlebar:

消息中心.PNG
  • 如果我们先封装一个titlebar,再往主布局中添加:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
    <TextView
        android:id="@+id/title_bar_title"
        android:layout_width="match_parent"
        android:layout_height="@dimen/title_bar_height"
        android:background="@drawable/bottom_line_bg"
        android:gravity="center"
        android:text="@string/msg_center"
        android:textColor="@color/text_color_black"
        android:textSize="18sp"
        />

    <ImageView
        android:id="@+id/title_bar_back"
        android:layout_width="wrap_content"
        android:layout_height="@dimen/title_bar_height"
        android:paddingLeft="@dimen/title_bar_margin_left"
        android:paddingRight="@dimen/title_bar_margin_left"
        android:src="@drawable/bg_back_arrow"
        />
</merge>
public class TitleBar extends FrameLayout {

    private ImageView mBack;
    private TextView mTitle;

    public TitleBar(Context context) {
        super(context);
        initView(context);
    }

    public TitleBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView(context);
    }

    public TitleBar(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initView(context);
    }

    private void initView(Context context) {
        inflate(context, R.layout.my_title_bar, this);
        mBack = (ImageView) findViewById(R.id.title_bar_back);
        mTitle = (TextView) findViewById(R.id.title_bar_title);
        ……
    }
}
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">

    <com.kaola.framework.ui.TitleBar
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        />

</FrameLayout>
  • 下面这种直接在主布局中添加返回键和标题,可以少一层FrameLayout
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">

    <TextView
        android:id="@+id/title_bar_title"
        android:layout_width="match_parent"
        android:layout_height="@dimen/title_bar_height"
        android:gravity="center"
        android:text="@string/msg_center"
        android:background="@drawable/bottom_line_bg"
        android:textColor="@color/text_color_black"
        android:textSize="18sp"
        />

    <ImageView
        android:id="@+id/title_bar_back"
        android:layout_width="wrap_content"
        android:layout_height="@dimen/title_bar_height"
        android:paddingLeft="@dimen/title_bar_margin_left"
        android:paddingRight="@dimen/title_bar_margin_left"
        android:src="@drawable/bg_back_arrow"
        />

        ……

</FrameLayout>
  • 当然应该配合<include/>使用
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">

    <include layout="@layout/my_title_bar"/>

</FrameLayout>

2.3 使用特殊背景图

另外值得注意的是,上图中的标题下那根分割线使用.9图设置背景实现的,该做法可以减少一个view。


bottom_line_bg.9.png

再来看看这个类似于京东分类的布局,点击左侧的tab切换右侧的内容,如果产品要求左侧红框区域都是可以点击的,是不是需要先写一个红框大小的容器view,然后再其里面放置一个有这种黑色背景的TextView呢,其实不需要的,只要背景为下图这种带空白边距就可以用一个TextView做到了。

classify_checked.png
某app分类.png

2.4 利用内容显示栏的FrameLayout直接开始布局

目前自动生成的布局都是默认用 <RelativeLayout/>,这个布局会在加载到内容栏中,而内容栏本事是一个 <FrameLayout/>,所以我们可不可以直接利用这个<FrameLayout/>开始布局呢,答案是肯定的。做法就是布局文件中最外层的布局使用<merge/>,如上面的布局中可以改写成

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">

    <include layout="@layout/my_title_bar"/>

</merge>

2.5 利用ListView和LinearLayout的divider来设置分割线。

<ListView
    android:id="@+id/message_center_lv_message_list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginTop="@dimen/title_bar_height"
    android:divider="@color/light_gray_occupy_line"
    android:dividerHeight="2px"
    android:footerDividersEnabled="true"
    android:headerDividersEnabled="false"
    android:paddingLeft="10dp"
    android:paddingRight="10dp"
    />
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:divider="@color/light_gray_occupy_line"
    android:dividerPadding="10dp"
    >
  • ListView分割线相关属性的有四个,分割线图片,高度,是否显示头部和底部的分割线,其中如果高度不是match_parent的话是不会显示底部分割线的
  • LinearLayout分割线相关的有分割线图片、分割线边距。

2.6 如果不是必要的,不要把 “某些模块” 放在一个特地的父布局里

比如上图消息中心页面,TitleBar下面有ListView、LoadingView,还有用于登陆的ViewStub,如果不注意层次的话,会把这些放在放在一个ViewGroup里,然后在设置marginTop使得上边距为TitleBar的高度,其实是没有必要的,只要分别设置好marginTop就可以。代码如下

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <include layout="@layout/my_title_bar"/>

    <ListView
        android:id="@+id/message_center_lv_message_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="@dimen/title_bar_height"
        android:divider="@color/light_gray_occupy_line"
        android:dividerHeight="2px"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        />

    <com.kaola.framework.ui.LoadingView
        android:id="@+id/message_center_lv_loading_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="@dimen/title_bar_height"
        />

    <ViewStub
        android:id="@+id/message_center_vs_login_stub"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="@dimen/title_bar_height"
        android:layout="@layout/login_register_layout"
        />

</merge>

此时,我们来看从DecorView到ListView的viewTree是怎样的,可以看到目前就只有四层,上面两天线分别是返回图标和Title TextView,下面两条线是LoadingView和登陆ViewStub

123.PNG

2.7 利用textview的drawable**来设置图片,利用lineSpacingExtra、lineSpacingMultiplier显示多行间距,利用SpannableString来设置不用的样式

来看下这个消息item的布局,先简单看下面这个样式:


消息item.PNG

图中左侧是一个icon,右侧是有两行,上行字体偏大偏黑,这个布局一般人的写法是,左侧一个ImageView,右侧两个TextView,其实这样的布局一个TextView就可以完成。
布局和代码片段:

<TextView
        android:id="@+id/message_center_tv_last_content"
        android:layout_width="match_parent"
        android:layout_height="71dp"
        android:layout_marginLeft="10dp"
        android:drawablePadding="10dp"
        android:ellipsize="end"
        android:gravity="center_vertical"
        android:lineSpacingExtra="8dp"
        android:maxLines="2"
        android:textColor="@color/text_color_gray_2"
        android:textSize="12sp"
        />
SpannableString spanStr = new SpannableString(
        messageModel.getBoxName() + "\n" + messageModel.getLastestContent());
spanStr.setSpan(new AbsoluteSizeSpan(14, true), 0,
        messageModel.getBoxName().length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spanStr.setSpan(new ForegroundColorSpan(0xff333333), 0,
        messageModel.getBoxName().length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
holder.tvMsgContent.setText(spanStr);
holder.tvMsgContent.setCompoundDrawablesWithIntrinsicBounds(
        getMsgIconResId(messageModel.getBoxType()), 0, 0, 0);

以上仅仅提供一个思路,在实际中发现对于android5.0以下是可行的,而对于android5.0及以上不行,因为在android5.0以下 android:lineSpacingExtra="8dp"这个属性不会对TextView最后添加间距,而android5.0及以上添加了,因此设置垂直居中在android5.0及其以上会算入最后一行的间隙导致不正确。所以不带左侧图片,那么可以通过paddingTop或者marginTop来让两行文字居中,而不android:gravity="center_vertical"。
再比如图中这个优惠券,不管最后一行有没有间距,设置好优惠券高度再设置好顶部距离就可以居中


优惠券.png

2.8 利用RelativeLayout的 layout_alignParent** 、layout_align** 等属性进行边界对齐,配合marrgin使用更灵活

关于几个容器布局的选择,差别主要在与测量上,在不增加布局层次的情况下,优先使用FrameLayout,其次如果没有weight属性,优先使用LinearLayout,因为这些都只测量一遍,而加了weight的LinearLayout和RelativeLayout需要测量两遍。另外ListView、GridView的item中不应该出现带有weight的LinearLayout,这会导致测量次数增加。

RelativeLayout虽然要测量两遍,但是其布局灵活在一些情况往往比别的布局少用一层布局。上面的布局中,icon右上角的红点(消息弱提示)或者数字(消息强提示)是简单的做法是通过设置marginLeft和marginTop来设置,但是这样写不够精准,弱提示的消息红点和icon的位置如下图,刚好右侧和icon的右侧相连,顶部和icon的顶部相连,这样的一般人可能会先写个父布局,然后在里面写ImageView和设置红点View,红点View的设置成右上角,这样自然就多了一层。正确的做法其实是利用RelativeLayout里的android:layout_alignBottom和android:layout_alignTop,让红色的View与ImageView的顶部和右侧相连即可。


layout_align**的使用.PNG
<ImageView
    android:id="@+id/message_center_iv_icon"
    android:layout_width="35dp"
    android:layout_height="35dp"
    android:layout_gravity="center"
    android:layout_marginBottom="18dp"
    android:layout_marginTop="18dp"
    />

<TextView
    android:id="@+id/message_center_tv_weak_hint"
    android:layout_width="@dimen/red_point_width"
    android:layout_height="@dimen/red_point_height"
    android:layout_alignTop="@id/message_center_iv_icon"
    android:layout_alignRight="@id/message_center_iv_icon"
    android:background="@drawable/bg_message_dot_circle_red"
    />

再来看看这种带消息个数的强提示,并不是刚好和图片的顶部和右侧相连,这种情况其实只要加上一个负数的margin就可以


123.PNG
<ImageView
        android:id="@+id/message_center_iv_icon"
        android:layout_width="35dp"
        android:layout_height="35dp"
        android:layout_gravity="center"
        android:layout_marginBottom="18dp"
        android:layout_marginTop="18dp"
        />
    
    <TextView
        android:id="@+id/message_center_tv_strong_hint"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignRight="@id/message_center_iv_icon"
        android:layout_alignTop="@id/message_center_iv_icon"
        android:layout_marginRight="-3dp"
        android:layout_marginTop="-3dp"
        android:background="@drawable/bg_message_num_circle_red"
        android:gravity="center"
        android:minHeight="@dimen/message_center_unread_num_min_size"
        android:minWidth="@dimen/message_center_unread_num_min_size"
        android:text="1"
        android:textColor="@color/text_color_white"
        android:textSize="@dimen/text_size_11sp"
        />

当然也可以先设置图片的paddingRight和paddingTop,让强提示刚好连着图标的右侧和顶部,然后弱提示设置正数的marginTop和marginRight就可以。
layout_alignParent***这个属性是与父布局某一侧相连,用的比较多,就不强调了。

到目前可以发现消息的item就RelativeLayout里放了几个TextView和ImageView,共两层。因此这个消息中心布局不考虑上面提到LoadingView的只需要六层就可以。

最后引用官网的一个标准:默认最大深度为10层。

Deep layouts - Layouts with too much nesting are bad for performance. Consider using flatter layouts such as RelativeLayout orGridLayout to improve performance. The default maximum depth is 10.

3. 过度绘制(overdraw)优化

一个app可能会在一帧里的相同像素点上绘制多次,我们叫它过度绘制。过度绘制往往是不必要的,浪费了GPU的时间来呈现不同的像素点,而对用户在屏幕上看没有一点用处,所以最好能消除。

在多层次的UI结构里面,如果不可见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这就浪费大量的CPU以及GPU资源,这可能导致渲染这些UI的时间大于16ms,从而使你的app出现了卡顿。

在android手机上,可以通过 设置 → 开发者选项 → 调试GPU过度绘制 → 显示过度绘制区域 开启过度绘制的开关。

官网上给出指标,如果是真实的颜色,则没有过度绘制,其余盖上不同颜色蒙层可以从下面的图片上查过过度绘制程度,我个人的标准是:要有“没过度绘制”的区域和尽量少的红色区域。

过度绘制.png

先简单翻译下官网给出的3条建议:

  • 移除没有必要的背景
    默认情况下,布局都是没有背景的,也就是说这些背景都是人为添加上去的,当添加了背景,就可能导致过度绘制。移除没有必要的背景是快速提高渲染速度的一个方式,没有必要的背景也许是永远都不会被用户看到的,因为它被上面的view所覆盖。通过 Hierarchy Viewer 可以发现过度绘制,哪些容器有相同的背景色可以去消除。实际上消除没有必要的背景,可以通过设置window的背景,然后让它上面的容器view全都不要设置背景。

  • 让view 的层次图扁平化
    时尚前沿的布局可以轻易让一款产品堆放和布置更多的组件而达到优美的设计,然后这样做可能会过度绘制而降低渲染性能,尤其是在不透明的view中,要求不看到的和看不到的都绘制出来。如果遇到这样的问题,可以考虑优化view 的层次图,让view的重叠数降低来提高渲染性能。

  • 减少透明ui
    透明的ui渲染,如alpha 渲染是导致过度绘制一个关键的因素,不像标准的过度绘制,系统可以完全隐藏了下层的像素。透明渲染需要让现存的像素先绘制,这样才能产生正确的混合公式。像透明、阴影、渐变等视觉都会导致过度绘制,你可以通过减少透明ui来提高你的渲染,(比如黑色加透明可以产生灰色,如果当做背景,你可以直接使用灰色比黑色加透明效果要好)。

还是看消息中心,图中明显过度绘制了:

消息中心_过度绘制.png
<activity
    android:name=".spring.ui.message.MessageActivity"
    android:screenOrientation="portrait"
    android:theme="@style/NoTitleBarThemeWhite"/>
 <style name="NoTitleBarThemeWhite" parent="AppThemeNoActionBar">
        <item name="android:windowBackground">@color/white</item>
    </style>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:background="@color/white"
              android:orientation="vertical">
              ……

代码中可以看出设置了两次白色背景,只要把主布局的背景去掉就可以了,如果按照上面说的利用content直接开始布局,使用merge,那就不存在这个问题了,就算在merge中设置了background也是无效的。

以前因为思维定势,一开始都会在windows主题上加背景,这个导致有个情况就过度绘制了,在一个activity中一个ViewPager,ViewPager里放fragment,一个fragment背景是白色的,其余两个fragment背景是灰色的,不管在Windows设置白色还是灰色,都会让另一种颜色的fragment多绘制了一层背景色。这个时候就应该要把Windows设置为无背景,然后不同fragment布局文件中设置不同的背景色。

设置windows为背景

<activity
    android:name=".spring.ui.message.MessageActivity"
    android:screenOrientation="portrait"
    android:theme="@style/NoTitleBarThemeTransparent"/>
<style name="NoTitleBarThemeTransparent" parent="AppThemeNoActionBar">
        <item name="android:windowBackground">@null</item>
    </style>

除了官网那几点,在实际项目中还总结了这几点建议:

  • ImageView的背景大部分都是多余的,如果一开始显示灰色,加载好后显示白色,那那个灰色可以写在src中,而不是background。

  • 如果采用的是selector的背景,将normal状态的color设置为”@android:color/transparent”,也同样可以解决问题。

  • 公共的视图组件不要设置背景,让使用者需要背景色后再自己设置背景。

  • 自定义view使用clipRect & quickReject来设置绘制区域。
    quickReject可以判断是够和一个矩形重合。
    clipRect可以设置一个需要绘制的矩形区域,可以设置多次,每次出现的矩形和之前的矩形就有一些关系运算了,并,或等,取决于第二个参数
    canvas.clipRect(aRect);
    canvas.clipRect(bRect, Op.xxxx);
    比如说一个自定义view中有两个重叠的image,中间重叠了

Paste_Image.png

我们可以先裁剪左侧矩形进行绘制,然后保存画布,之后再裁剪右侧的矩形进行绘制,之后还原画布,这样就消除了过度绘制。

Paste_Image.png

最后推荐一篇文章:Google《Android性能优化》学习笔记

.
.
.
.
.
.
.

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

推荐阅读更多精彩内容