Android性能优化之-UI优化篇

日常开发中,我们经常会碰到比较复杂的布局,在这种情况下,最简单的方案就是采用多层嵌套实现效果,但是最简单的方法就是最优的方案吗?我认为在不影响效果的情况下应尽可能减少布局的层级、减少嵌套,这样做的好处就是可以让整个布局达到结构清晰,渲染速度快的效果。

一些需要我们掌握的小技巧

< include/> 重用:

< include>标签的作用是在当前布局中引入另外一个布局,作为当前布局的子布局。可以节省大量代码,同时便于统一使用及维护。

以app中常见的标题栏为例:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:background="@color/background">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:src="@mipmap/ic_launcher"/>

    <TextView
        tools:text="标题"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:textSize="16sp"
        android:textColor="@color/black" />

    <TextView
        tools:text="确定"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:gravity="center"
        android:layout_alignParentRight="true"
        android:paddingRight="15dp"
        android:textSize="16sp"
        android:textColor="@color/black" />

</RelativeLayout>

运行效果如下:


你可以在其他xml中调用它,比如:

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

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

</RelativeLayout>

效果和上面是一样的,你也可以在< include>标签当中重新设置宽高等layout属性。

用TextView同时显示图片和文字:

这个不用多说吧,使用TextView的drawableLeft(Right,Top,Bottom),举个栗子:


这种效果如果使用两个ImageView加上一TextView是非常不明智的,我们可以这样写:

<?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/background">

    <TextView
        android:drawableLeft="@mipmap/ic_launcher"
        android:drawableRight="@mipmap/enter"
        android:drawablePadding="15dp"
        android:paddingLeft="15dp"
        android:paddingRight="15dp"
        android:textSize="16sp"
        android:text="朋友圈"
        android:background="@color/white"
        android:gravity="center_vertical"
        android:layout_width="match_parent"
        android:layout_height="50dp" />

</LinearLayout>

这样写是不是简洁多了呢

使用TextView的行间距:

效果图:


看到这个效果,我们第一反应是不是四个Textview呢? 看下代码吧:

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

    <ImageView
        android:padding="25dp"
        android:src="@mipmap/aa"
        android:layout_width="100dp"
        android:layout_height="match_parent"/>

    <TextView
        android:textSize="14dp"
        android:lineSpacingExtra="10dp"
        android:gravity="center_vertical"
        android:text="交易状态:已支付\n快递状态:北京通州1号仓库已出库\n配送时间:1月4日 \n快递费用:包邮"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

可以看到我们仅仅利用Android:lineSpacingExtra="10dp",这一行代码就省去了3个TextView,在行数更多的情况下是不是方便多了呢?

ViewStub:

ViewStub是一个非常轻量级的View,与其他的控件一样,有着自己的属性及特定的方法。它是一个看不见的,不占布局位置,占用资源非常小的控件。可以为ViewStub指定一个布局,ViewStub与其他控件相比,主要区别在以下:

  • 当布局文件inflate时,ViewStub控件虽然也占据内存,但是相相比于其他控件,ViewStub所占内存很小。
  • ViewStub只能用来Inflate一个布局文件,而不是某个具体的View,当然也可以把View写在某个布局文件中。

基于以上,我们可以看出 ViewStub的优点在于实现View的延迟加载,避免资源的浪费,减少渲染时间,在需要的时候才加载View。

实例:

<ViewStub 
    android:id="@+id/stub_import"
    android:inflatedId="@+id/panel_import"
    android:layout="@layout/progress_overlay"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom" />

用ViewStub加载layout文件时,可以调用setVisibility(View.VISIBLE)或者inflate()

((ViewStub) findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();

注意:如果ViewStub可见或是被inflate了,ViewStub就不存在了,取而代之的是被inflate的Layout。所以它也被称做惰性控件。

用LinearLayout自带的分割线:

效果图:


上图中分割线大家是怎么实现的呢,是不是用一个view设置高度和背景?其实我们可以使用LinearLayout自带的分割线实现,看代码

<?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:orientation="vertical"
    android:divider="@drawable/divider"
    android:showDividers="middle">

    <TextView
        android:drawableLeft="@mipmap/ic_launcher"
        android:drawableRight="@mipmap/enter"
        android:drawablePadding="10dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:textSize="16sp"
        android:text="设置"
        android:gravity="center_vertical"
        android:layout_width="match_parent"
        android:layout_height="50dp" />

    <TextView
        android:drawableLeft="@mipmap/ic_launcher"
        android:drawableRight="@mipmap/enter"
        android:drawablePadding="10dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:textSize="16sp"
        android:text="档案"
        android:gravity="center_vertical"
        android:layout_width="match_parent"
        android:layout_height="50dp" />

    <TextView
        android:drawableLeft="@mipmap/ic_launcher"
        android:drawableRight="@mipmap/enter"
        android:drawablePadding="10dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:textSize="16sp"
        android:text="下载"
        android:gravity="center_vertical"
        android:layout_width="match_parent"
        android:layout_height="50dp" />

</LinearLayout>

其实实现分割线的就是这两行:

  android:divider="@drawable/divider"
  android:showDividers="middle"

其中divider.xml是分隔线样式。就是一个shape,showDividers 是分隔线的显示位置,beginning、middle、end分别代表显示在开始位置,中间,以及末尾位置。
还有一个dividerPadding的属性没有用到,意思是给divider添加padding。

Space控件:

Space控件是Android4.0中新提供的两个控件之一,另一个是GridLayout,Space控件源码非常简单,先来看看

public class Space extends View {

    public Space(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        if (getVisibility() == VISIBLE) {
            setVisibility(INVISIBLE);
        }
    }

    public Space(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public Space(Context context) {
        this(context, null);
    }

    /**
     * Draw nothing.
     *
     * @param canvas an unused parameter.
     */
    @Override
    public void draw(Canvas canvas) {
    }

    /**
     * Compare to: {@link View#getDefaultSize(int, int)}
     * If mode is AT_MOST, return the child size instead of the parent size
     * (unless it is too big).
     */
    private static int getDefaultSize2(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = size;
                break;
            case MeasureSpec.AT_MOST:
                result = Math.min(size, specSize);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(
                getDefaultSize2(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize2(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
}

该组件直接继承View,相比其他的View组件都有onDraw方法用来绘制自身,而Space控件onDraw方法进行了一个空的实现。Space控件在布局中只占位置,而不去绘制渲染。比如组件中的间隙用Space控件填充比用其它控件填充可以提高效率。

实例:

<Space    
    android:layout_width="match_parent"    
    android:layout_height="15dp" />
减少嵌套:

首先在我们日常开发中,我们心中要有一个大原则:尽量保持布局层级的扁平化。在这个大原则下我们要知道:

  • 在不影响层级深度的情况下,使用LinearLayout而不是RelativeLayout。因为RelativeLayout会让子View调用2次onMeasure,LinearLayout 在有weight时,才会让子View调用2次onMeasure。Measure的耗时越长那么绘制效率就低。
  • 如果非要是嵌套,那么尽量避免RelativeLayout嵌套RelativeLayout,恶性循环。

布局优化之-防止过度绘制

以下部分图片内容引用来自开源中国

Android渲染机制

用户在使用我们app过程中感知到的卡顿等性能问题的最主要根源都是因为渲染性能。从设计师的角度,他们希望App能够有更多的动画,图片等时尚元素来实现流畅的用 户体验。但是Android系统很有可能无法及时完成那些复杂的界面渲染操作。Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染, 如果每次渲染都成功,这样就能够达到流畅的画面,这意味着程序的大多数操作都必须在16ms内完成。通俗的说:你只有16ms的时间来处理所有的任务。

为什么是16ms?

16ms是怎么计算出来的呢?就是1000/60hz,LCD手机屏一般都是60HZ,电脑屏幕亦是如此,即1秒钟时间内整个画面刷新60次,对于人眼而言60HZ已经达到很流畅的程度,1秒=1000ms,这样算下来每一帧都要在16ms内完成就很好解释了。


图片来源:开源中国

如果你的某个操作花费时间是24ms,系统在得到VSYNC信号的时候就无法进行正常渲染,这样就发生了丢帧现象。那么用户在32ms内看到的就会是同一帧画面。


图片来源:开源中国
理解VSYNC(垂直同步)

从Android 4.1开始,谷歌致力于解决Android系统中最饱受诟病的一个问题,滑动不如iOS流畅。因此谷歌在4.1版本引入了一个重大的改进—Project Butter,也就是黄油计划。
Project Butter对Android Display系统进行了重构,引入了三个核心元素,即VSYNC、Triple Buffer和Choreographer。
为了理解App是如何进行渲染的,我们必须了解手机硬件是如何工作,首先我们需要了解两个相关的概念:

  • Refresh Rate:代表了屏幕在一秒内刷新屏幕的次数,这取决于硬件的固定参数,例如60Hz。
  • Frame Rate:代表了GPU在一秒内绘制操作的帧数,例如30fps,60fps。
    GPU会获取图形数据进行渲染,然后硬件负责把渲染后的内容呈现到屏幕上,他们两者不停的进行协作。
    不幸的是,刷新频率和帧率并不是总能够保持相同的节奏。如果发生帧率与刷新频率不一致的情况,就会容易出现Tearing的现象(画面上下两部分显示内容发生断裂,来自不同的两帧数据发生重叠)。
    通常来说,帧率超过刷新频率只是一种理想的状况,在超过60fps的情况下,GPU所产生的帧数据会因为等待VSYNC的刷新信息而被Hold住,这样能够保持每次刷新都有实际的新的数据可以显示。但是我们遇到更多的情况是帧率小于刷新频率。
    在这种情况下,某些帧显示的画面内容就会与上一帧的画面相同。糟糕的事情是,帧率从超过60fps突然掉到60fps以下,这样就会发生LAG,JANK,HITCHING等卡顿掉帧的不顺滑的情况。这也是用户感受不好的原因所在。

用户容易在UI执行动画或者滑动ListView的时候感知到不流畅,是因为这里的操作相对复杂,容易发生丢帧的现象,从而感觉卡顿。有很多原 因可以导致丢帧,也许是因为你的layout太过复杂,无法在16ms内完成渲染,有可能是因为你的UI上有层叠太多的绘制单元,还有可能是因为动画执行 的次数过多。这些都会导致CPU或者GPU负载过重。

怎样检测Overdraw(过度绘制)?

我们可以使用手机设置里 面的开发者选项,打开Show GPU Overdraw等选项进行观察。

设置 > 开发者选项 > 调试GPU过度绘制 > 显示GPU过度绘制区域

打开以后可以切换到需要检测的程序,你可以发现屏幕上有很多色块,对于各个色块的解释,我们来看一张Overdraw的参考图:

图片来源:开源中国

淡紫,淡绿,淡红,深红代表了4种不同程度的Overdraw情况,我们的目标就是尽量减少红色Overdraw,看到更多的淡紫色区域。

嗯,那我们来看个实例:


看到这个页面,基本上在深红色区块中,首先看下这个ListView的代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/app_background"
    android:orientation="vertical">

            <com.view.swipelistview.MySwipeMenuListView
              android:id="@+id/lv_courses"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:layout_marginBottom="40dp"
              android:background="@color/app_background"
              android:divider="@drawable/course_list_divider"
              android:dividerHeight="@dimen/divider"
              android:footerDividersEnabled="false"
              android:headerDividersEnabled="false"
              android:listSelector="@android:color/transparent" />

</LinearLayout>

去除了一些不相关代码,方便观看首先我们可以看到,顶层布局的已经设置了背景,ListView也设置了相同的背景,所以这里的背景就可以去掉了。

再来看item的布局:

<?xml version="1.0" encoding="utf-8" ?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:background="@drawable/list_item_bg"
    android:paddingLeft="@dimen/cf_official_layout_paddingleft"
    android:paddingRight="@dimen/cf_official_layout_paddingright">

</LinearLayout>

这里的顶层布局的background是一个selector,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<selector
      xmlns:android="http://schemas.android.com/apk/res/android>
      <item android:drawable="@color/white2" android:state_pressed="true" />
      <item android:drawable="@color/white2"  android:state_pressed="false"/>
</selector>

上面ListView的android:listSelector="@android:color/transparent",所以这里的selector肯定也是多余的,顺便优化了其他的一些小地方,这里就不说了,修改了这几处,我们运行后再来看一下:


是不是好一点了呢?还有可优化的余地,这里只是举例说明一下。

总结:

巧用这些技巧可以让我们写出更高效,更优雅的代码,具体的情景使用还是要看项目的具体情况,不能为了优化而优化。优化也是不断积累的过程,不要指望立竿见影。

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

推荐阅读更多精彩内容