Android UI性能优化

3 篇文章 0 订阅
1 篇文章 0 订阅

多数手机的屏幕刷新频率是60hz,在1000/60=16.67ms内没有办法把这一帧的绘制任务执行完毕,就会发生丢帧的现象。丢帧越多,用户感受到的卡顿情况就越严重。这里的绘制包含了所有View的meature、layout、draw等,CPU的计算,以及GPU的栅格化渲染等一系列操作,也就是说,一般我们需要在16ms以内完成单次绘制的所有工作,才能保证app的流畅。
抛去CPU在meature、layout、draw过程中的各种不恰当的耗时操作(如处理大图片等), Android UI问题一般就是过度绘制,即屏幕上某一像素点在一帧中被重复绘制多次。其解决方法简单概括就是:

  • 通过Hierarchy Viewer去检测渲染效率,去除不必要的嵌套
  • 通过Show GPU Overdraw去检测Overdraw,最终可以通过移除不必要的背景,以及使用canvas.clipRect解决大多数覆盖绘制问题。
    这里写图片描述

过度绘制调试工具

GPU呈现模式分析

这里写图片描述
此工具可以记录每一帧绘制所使用的总时长,其中绿横线代表16ms,每一帧竖线代表的含义:
这里写图片描述
其他具体可见此处。若发现超过16ms绿线的帧较多或较密集,可以使用下面的工具进一步确定。

ADM中的Dump

这里写图片描述
使用此工具,获取布局层次结构,根据具体情况,手动去除不必要的层级。之后,用以下工具做进一步处理。

GPU过度绘制调试工具

这里写图片描述
以颜色块表示出像素点被绘制过多少次的工具,过度绘制的主要调试工具。
假设页面布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<!--1x OverDraw-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:background="#fff"
              android:gravity="bottom">
    <!--2x OverDraw-->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:background="#fff"
        android:gravity="bottom">
        <!--3x OverDraw-->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:background="#fff"
            android:gravity="bottom">
            <!--4x+ OverDraw-->
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="150dp"
                android:background="#fff"
                android:gravity="bottom">
                <!--4x+ OverDraw-->
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="100dp"
                    android:background="#fff"
                    android:gravity="bottom">
                    <!--4x+ OverDraw-->
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="50dp"
                        android:background="#fff"
                        android:gravity="bottom">
                    </LinearLayout>
                </LinearLayout>
            </LinearLayout>
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

开启过度绘制之后可以看到:
这里写图片描述
如果颜色为原色,则表示像素只被绘制过一次,是最理想的情况。

解决过度绘制的几个方法

引发过度绘制的原因,不外乎以下几点:
1. 页面UI嵌套层级深;
2. 同一区域里,出现一个元素被另一个元素覆盖的情况;
3. 背景颜色覆盖(其实跟1是同样的的问题)。

解决背景颜色覆盖问题

一般情况下,如果不设置Activity中theme的android:windowBackground属性的话,DecorView会默认的使用黑色背景:

/**
 * Returns the color used to fill areas the app has not rendered content to yet when the
 * user is resizing the window of an activity in multi-window mode.
 */
public static Drawable getResizingBackgroundDrawable(Context context, int backgroundRes,
        int backgroundFallbackRes, boolean windowTranslucent) {
    if (backgroundRes != 0) {
        final Drawable drawable = context.getDrawable(backgroundRes);
        if (drawable != null) {
            return enforceNonTranslucentBackground(drawable, windowTranslucent);
        }
    }

    if (backgroundFallbackRes != 0) {
        final Drawable fallbackDrawable = context.getDrawable(backgroundFallbackRes);
        if (fallbackDrawable != null) {
            return enforceNonTranslucentBackground(fallbackDrawable, windowTranslucent);
        }
    }
    return new ColorDrawable(Color.BLACK);
}

所以,不使用android:windowBackground的话,将其设置成透明色,可以全局的减少一层绘制,及其划算。上例中设置windowBackground为透明后:
这里写图片描述

同理,在布局时,应去掉布局文件中不必要的background设置,以此减少过度绘制的次数。

解决页面UI嵌套层级深问题

使用Hierarchy Viewer可以直观地看到布局的层次结构,但我们大多数时候,都可以直接修改布局文件来减少嵌套过深的问题。基本原则是是我们的布局尽量“扁平化”,布局中的嵌套层次以及冗余层次尽量减少,如下例中:
布局1:

<LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#e5e5e5"
        android:orientation="horizontal"
        android:padding="10dp">

        <ImageView
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:src="@mipmap/ic_launcher"/>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:orientation="vertical">

            <FrameLayout
                android:layout_width="match_parent"
                android:layout_height="20dp">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="姓名"/>

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="right"
                    android:text="年龄"/>
            </FrameLayout>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:text="社会主义好青年"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:text="个人简介:富强、民主、文明、和谐、自由、平等、公正、法治、爱国、敬业、诚信、友善"/>
        </LinearLayout>
    </LinearLayout>

布局二:

<RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="15dp"
        android:background="#e5e5e5"
        android:orientation="horizontal"
        android:padding="10dp">

        <ImageView
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:src="@mipmap/ic_launcher"/>

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="70dp"
            android:text="姓名"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:text="年龄"/>

        <TextView
            android:id="@+id/tv_tag"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignLeft="@+id/tv_name"
            android:layout_below="@+id/tv_name"
            android:layout_marginTop="10dp"
            android:text="社会主义好青年"/>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignLeft="@+id/tv_tag"
            android:layout_below="@+id/tv_tag"
            android:layout_marginTop="10dp"
            android:text="个人简介:富强、民主、文明、和谐、自由、平等、公正、法治、爱国、敬业、诚信、友善"/>

    </RelativeLayout>

两者的显示效果是一样的:
这里写图片描述
但是布局二中,少绘制了很多布局一中的冗余控件和层次,使用Hierachy Viewe的话,可以明显的看出两者的差距(Hierachy Viewer一般无法在真机上使用,此处就不做比较结果了…)。

其次,在需要有条件的动态显示View的情况下,使用ViewStub先占位,可以避免直接先将所有的View实例化(即使Gone的情况也会被实例化),而在ViewStub使用inflate方法后,具体的View才会被实例化,这样,就可以使用ViewStub,来方便的在运行时决定,要不要显示某个布局或者具体要显示哪个布局。

另外,使用merge作为xml的根标签,也可以在子视图不需要指定任何针对父视图的布局属性(layout_gravity等),或者父视图与当前根节点是同一种ViewGroup时,干掉一个view层级。

解决元素覆盖问题

先来看一个例子,显示一个叠加的横向图片列表:

public class Card extends View {
    private int picWidth;
    private int space;
    private Bitmap bm;
    private Paint paint;

    public Card(Context context) {
        super(context);
        init();
    }

    public Card(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public Card(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        picWidth = getResources().getDimensionPixelOffset(R.dimen.dp150);
        space = getResources().getDimensionPixelOffset(R.dimen.dp50);
        bm = BitmapFactory.decodeResource(getResources(), R.mipmap.ysl);
        paint = new Paint();
        paint.setAntiAlias(true);
    }

    @Override
    protected void onDraw(Canvas canvas) {
       super.onDraw(canvas);
        int n = (getWidth()  - picWidth) / space;
        for (int i = 0; i < n; i++) {
            drawPic(canvas, space * i, picWidth);
        }
    }

    private void drawPic(Canvas canvas, int marginLeft, int size) {
        Rect recDes = new Rect(marginLeft, 0, marginLeft + size, size);
        canvas.drawBitmap(bm, new Rect(0, 0, getWidth(), getHeight()),
                recDes, paint);
    }
}

此时的表现为:
这里写图片描述
可以看到,中间红、绿色部分本应是被盖住的部分,依然多绘制了一次以上。我们的期望是,只绘制每张图片没有“相交”的部分,此时就用到了canvas.clipRece:

/**
* Intersect the current clip with the specified rectangle, which is
* expressed in local coordinates.
*
* @param left   The left side of the rectangle to intersect with the
*               current clip
* @param top    The top of the rectangle to intersect with the current clip
* @param right  The right side of the rectangle to intersect with the
*               current clip
* @param bottom The bottom of the rectangle to intersect with the current
*               clip
* @return       true if the resulting clip is non-empty
*/
public boolean clipRect(int left, int top, int right, int bottom) {
   return nClipRect(mNativeCanvasWrapper, left, top, right, bottom,
           Region.Op.INTERSECT.nativeInt);
}

简单来说,这个clipRect就是对canvas画布进行裁剪,即只在定义的矩形范围内绘制,运用后修改原代码为:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int n = (getWidth() - picWidth) / space;
    for (int i = 0; i < n; i++) {
        int left = space * i;
        canvas.save();
        int right = left + (i < n - 1 ? space : picWidth);
        // 只裁剪出需要绘制的区域
        canvas.clipRect(left, 0, right, picWidth);
        drawPic(canvas, left, picWidth);
        canvas.restore();
    }
}

private void drawPic(Canvas canvas, int marginLeft, int size) {
    Rect recDes = new Rect(marginLeft, 0, marginLeft + size, size);
    canvas.drawBitmap(bm, new Rect(0, 0, getWidth(), getHeight()),
            recDes, paint);
}

现在效果如下:
这里写图片描述
实际上,熟悉一下canvas的具体用法,以及clip的具体用法等,能发现更多值得探索和优化的东西,此处就不一一列举了。

至于View之间的叠加引发的过度绘制(很多时候都是这种情况),目前鄙人还没找到很好的解决方法,只能在开发中尽量避免了。

避免过度优化

减少过度绘制确实可以提升app性能,但一味的追求所谓的优化,有时候也是很浪费时间的。比如明明简简单单的就能用叠加控件的方式,实现一些交叉比较小的动态画廊控件,为了追求完美,花了大量时间去计算运动过程中的叠加部分,结果反而会让cpu计算时间增长,与初衷相背驰了。因此,在实际设计和开发时,需要在复杂的逻辑,与简单易用的界面中做一个平衡。

Hierarchy Viewer

上面的工具是可以比较直观的去发现优化方案的,对于要精细把握app整体页面的渲染情况是,可以使用Hierarchy Viewer。此工具提供了可视化的页面布局层次结构,并可得到每个view的具体meature、layout、draw的耗时,非常直观的帮助我们优化布局。
Hierarchy Viewer只能在开发版手机(几乎没有)以及模拟器上使用,使用ViewServer库也可以在普通android机上使用,但是在部分国产机上无效,且在需要调试的Activity中需添加部分代码,麻烦,建议还是以模拟器的方式来使用。
食用方式:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值