Android性能优化(三)-绘制优化

2,044 阅读5分钟

一、Android UI渲染简述

1、屏幕刷新机制

在一个典型的显示系统中,一般包括CPU、GPU、display三个部分,其中CPU负责计算数据,把计算好的数据交给GPU,GPU对图形数据进行渲染,渲染好后放到buffer中存起来,然后display负责把buffer的数据呈现到屏幕上。

在Android中也是如此,由CPU/GPU准备好数据,存入buffer,display每隔一段时间去buffer里取数据,然后显示出来

display在Android中,读取的频率是固定的,为16ms。之所以是16ms是因为人眼与大脑之间的协作无法感知超过60fps的画面更新。

vsync.jpg

我们在使用APP的时候,当界面出现卡顿不流畅的情况,是因为当前界面UI的处理超过了16ms,会占用下一个16ms,这就导致16ms*2都是同一帧,也就是“卡”了。

drop_frame.jpg

了解了Android渲染机制后,我们来分析app为什么会超过16ms的重绘时间,通常有以下原因:

1、布局层级不合理

2、布局存在过度绘制

针对上述情况,我们接下来讲述一些常见的监控手段和布局和优化手段

二、监控手段

1、使用Layout Inspater检查布局层级

Layout Inspater是AndroidStudio自带的工具,用于分析布局层级

1、在连接的设备或模拟上运行你的应用

2、点击Tools > Layout Inspector

3、在出现的Choose Process对话框中,选择你想要检查的应用进程,然后点击OK。

4、默认情况下,Choose Process 对话框仅会为 Android Studio 中当前打开的项目列出进程,并且该项目必须在设备上运行。 如果想要检查设备上的其他应用,请点击 Show all processes。 如果正在使用已取得 root 权限的设备或者没有安装 Google Play 商店的模拟器,那么您会看到所有正在运行的应用。 否则,您只能看到可以调试的运行中应用。

layout_inspector.jpg

View Tree:视图在布局中的层次结构

Screenshot:带每个视图可视边界的设备屏幕截图。

Properties Table:选定视图的布局属性

其中,我们最需要使用的是View Tree,像本案例中,设置页的每一个栏目,由SettingItem包着RelativeLayout,其中SettingItem这个自定义View本身又继承了RelativeLayout,其实就多余了,可以用merge标签进行优化,降低层级

当然,最好使用约束布局,可以大大降低层级

2、使用调试GPU过度绘制功能检查过度绘制

从开发者模式中找到调试GPU过度绘制功能开关,打开

过度绘制显示.jpg

  • 蓝色、绿色、浅红、深红
  • 分为四个等级,其中蓝色为可接受的,当出现红色就应该要优化了

这种过度绘制常见于background的设置

3、使用Choreographer监控帧率

public class FPSMonitor implements Choreographer.FrameCallback, Runnable{

    private HandlerThread mHandlerThread;

    private long startTime = -1;

    private long endTime = -1;

    private final int MONITOR_TIME = 1000;

    private Handler mWorkHandler;

    private int mFpsCount;

    @Override
    public void doFrame(long frameTimeNanos) {
        if (startTime == -1) {
            startTime = frameTimeNanos;
        }
        mFpsCount++;
        //超过一秒了,发消息到工作线程,计算帧率
        long duration = (frameTimeNanos - startTime) / 1000000L;
        if (duration >= MONITOR_TIME) {
            endTime = frameTimeNanos;
            mWorkHandler.post(this);
        } else {
            //没到一秒,设定下一帧监听
            Choreographer.getInstance().postFrameCallback(this);
        }
    }

    @Override
    public void run() {
        //计算帧率
        long duration = (endTime - startTime) / 1000000L;
        float frame = 1000.0f * mFpsCount / duration;
        Log.i("Restart", "当前帧率: " + frame);
        //开启下一秒的计算
        start();

    }

    public void start() {
        if (mHandlerThread == null) {
            mHandlerThread = new HandlerThread("FPS Monitor Thread");
            mHandlerThread.start();
            mWorkHandler = new Handler(mHandlerThread.getLooper());
        }
        //重置计算值
        startTime = -1;
        endTime = -1;
        mFpsCount = 0;
        //设置帧绘制监听器
        Choreographer.getInstance().postFrameCallback(this);
    }
}

4、使用setFactory2统计某个View创建耗时

 @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        //注意:要在super.onCreate(savedInstanceState);之前调用才不会报错
        LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {
            @Override
            public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
                long start = System.currentTimeMillis();
                AppCompatDelegate delegate = getDelegate();
                View view = delegate.createView(parent, name, context, attrs);
                long duration = System.currentTimeMillis() - start;
                Log.i("Restart", name + "的绘制耗时: " + duration);
                return view;
            }

            @Override
            public View onCreateView(String name, Context context, AttributeSet attrs) {
                return null;
            }
        });
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
    }

三、常见的布局优化手段

1、使用merge标签降低层级

下面介绍一个最常用的场景:

//假定自定义View为RelativeLayout
public class LoginButton extends RelativeLayout {
 	。。。
}
//在xml标签中使用merge作为根标签,而不要再次使用RelativeLayout作为根标签,可以省去一个层级

2、使用ViewStub

ViewStub的使用场景为当一个布局可能需要加载,也可能不需要的情况。假如布局delayInflateLayout可能不需要也可能需要。则可以用ViewStub标签,如下代码所示,在布局文件中使用。

<ViewStub
        android:id="@+id/contentPanel"
        android:inflatedId="@+id/inflatedStart"
        android:layout="@layout/delayInflateLayout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        />

再在代码中控制是否真正加载它

//调用inflate则会真正的加载它,但是只能调用一次
viewStub.inflate();

3、使用clipRect

clipRect的功能可以理解为在一个大的画布中,用一些大小可变的矩形一个一个来裁切,在某一个矩形内,绘制想要绘制的图形,超出的不进行绘制,当我们的app这样进行写自定义View的时候,可以避免view与view之间的叠加,从而产生同一个像素点被绘制多次的情况,原理就是这样。举一个案例:

clipRect.jpg

上述的三张图,相互重叠的情况,onDraw方法优化如下:

@Override
protected void onDraw(Canvas canvas)
{

    super.onDraw(canvas);

    canvas.save();
    canvas.translate(20, 120);
    for (int i = 0; i < mCards.length; i++)
    {
        canvas.translate(120, 0);
        canvas.save();
        if (i < mCards.length - 1)
        {
            //裁剪画布,减少不必要的绘制
            canvas.clipRect(0, 0, 120, mCards[i].getHeight());
        }
        canvas.drawBitmap(mCards[i], 0, 0, null);
        canvas.restore();
    }
    canvas.restore();

}

4、使用约束布局降低层级

5、使用AsyncLayoutInflator异步加载布局文件

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new AsyncLayoutInflater(this).inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
            @Override
            public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) {
                setContentView(R.layout.activity_main);
            }
        });
        
    }

使用AsyncLayoutInflater 的局限性:

所有构建的View中必须不能直接使用 Handler 或者是调用 Looper.myLooper(),因为异步线程默认没有调用 Looper.prepare ();

异步转换出来的 View 并没有被加到 parent view中,必须手动添加;

AsyncLayoutInflater 不支持设置 LayoutInflater.Factory 或者 LayoutInflater.Factory2;

同时缓存队列默认 10 的大小限制如果超过了10个则会导致主线程的等待;

四、小结

开发Android应用程序时也不可能无限制的使用CPU和内存,如果对CPU和内存使用不当也会造成应用的卡顿和内存溢出等问题。因此,性能优化是每个Android开发人员都应该去注意的,本文介绍了绘制相关的一些监控手段和常见的优化手段,希望对大家有帮助~