一、Android UI渲染简述
1、屏幕刷新机制
在一个典型的显示系统中,一般包括CPU、GPU、display三个部分,其中CPU负责计算数据,把计算好的数据交给GPU,GPU对图形数据进行渲染,渲染好后放到buffer中存起来,然后display负责把buffer的数据呈现到屏幕上。
在Android中也是如此,由CPU/GPU准备好数据,存入buffer,display每隔一段时间去buffer里取数据,然后显示出来
display在Android中,读取的频率是固定的,为16ms。之所以是16ms是因为人眼与大脑之间的协作无法感知超过60fps的画面更新。
我们在使用APP的时候,当界面出现卡顿不流畅的情况,是因为当前界面UI的处理超过了16ms,会占用下一个16ms,这就导致16ms*2都是同一帧,也就是“卡”了。
了解了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 商店的模拟器,那么您会看到所有正在运行的应用。 否则,您只能看到可以调试的运行中应用。
View Tree:视图在布局中的层次结构
Screenshot:带每个视图可视边界的设备屏幕截图。
Properties Table:选定视图的布局属性
其中,我们最需要使用的是View Tree,像本案例中,设置页的每一个栏目,由SettingItem包着RelativeLayout,其中SettingItem这个自定义View本身又继承了RelativeLayout,其实就多余了,可以用merge标签进行优化,降低层级
当然,最好使用约束布局,可以大大降低层级
2、使用调试GPU过度绘制功能检查过度绘制
从开发者模式中找到调试GPU过度绘制功能开关,打开
- 蓝色、绿色、浅红、深红
- 分为四个等级,其中蓝色为可接受的,当出现红色就应该要优化了
这种过度绘制常见于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之间的叠加,从而产生同一个像素点被绘制多次的情况,原理就是这样。举一个案例:
上述的三张图,相互重叠的情况,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开发人员都应该去注意的,本文介绍了绘制相关的一些监控手段和常见的优化手段,希望对大家有帮助~