文章目录
View布局优化
布局优化首先我们应该做的就是尽可能的减少布局文件的层级,当布局的层级减少了,那绘制的性能自然就提高了,同时还可以使用下面的方式来优化我们的布局。
1.优先选择高性能的ViewGroup
android中界面布局使用android中提供的控件就可以,但是在使用的时候可以根据场景择优选择所需的控件,在选择ViewGroup的时候,可以优先选择性能较高的FrameLayout和LinearLayout,因为RelativeLayout较为费时,但是如果布局需要进行嵌套,可以考虑使用RelativeLayout来实现布局,以达到减少布局层级的目的。
2.可以使用TextView的leftDrawabel/rightDrawable代替ImageView+TextView布局,减少View的数量
3.使用include和merge标签重复利用布局,同时减少嵌套
- 使用include标签我们可以将公共的布局抽取出来,这样公共的布局我们不用总是重新再写一遍,同时include标签只支持 android:layout_ 开头的属性和android:id 属性,其他的属性不支持,同时在使用该属性的时候,如果使用了android:layout_*开头的其他属性,那么layout_width和layout_height属性必须同时存在,否则设置的android:layout_*开头的属性不起作用。
- 使用merge标签可以用来减少布局的层级,当使用include标签引用布局的时候,如果被引用布局的根布局和include标签的父布局为同一布局,就可以使用merge标签替换被引用布局的根布局标签。
- 自己在尝试的过程中发现,使用include标签时,如果被引用布局的根元素使用merge标签,include属性设置android:layout_属性将不起作用,而需要在merge标签内设置include属性,这样include标签的android:layout_属性才会起作用。(此处为自己通过demo简单实践后得到的,如果有错误还请指正)
不起作用的写法:
layout布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".LayoutPerformanceActivity">
<!--注意layout前面没android开头的命名空间-->
<include
android:id="@+id/reused_image"
layout="@layout/commonimageview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"/>
</LinearLayout>
被引用布局
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher_round" />
</merge>
运行结果:layout_margin属性不起作用
- 起作用的写法
修改被引用布局结构,在merge标签中使用include标签引用布局
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<include
android:id="@+id/reused_image"
layout="@layout/stub_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp" />
</merge>
stub_layout布局文件仅为一个图片:
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@mipmap/ic_launcher" />
运行后查看在merge里面的include使用layout_margin属性起作用了。。
3.使用ViewStub标签进行按需加载
ViewStub继承了View,自身不参与任何布局和绘制过程,当我们在开发中,一些布局在初始化时没有必要一起进行初始化,这时就可以使用ViewStub来按需加载该布局,因为当view设置为Gone或者Invisiable的时候,还是会占资源,具体参见(Android 使用View Gone 与 ViewStub的区别)
但是需要注意的是ViewStub的引用布局不支持merge标签
ViewStub使用方式
<ViewStub
android:id="@+id/stub_import"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inflatedId="@+id/stub_import_view"
android:layout="@layout/stub_import" />
android:id指定的即为ViewStub的id,当ViewStub被替换后,加载后得到的布局的Id即为android:inflatedId指定的id。
加载方式:
// mViewStub为 mViewStub = findViewById(R.id.stub_import);
mViewStub.setVisibility(View.VISIBLE);
代码:include标签和merge标签使用以及ViewStub标签使用
App绘制耗时分析工具Hierarchy Viewer
分析界面加载耗时情况时,可以使用Hierarchy Viewer工具去分析,
使用HierarchyViewer工具
查看过度绘制Overdraw
在"调试GPU过度绘制"中开启过度绘制调试:
Overdraw:用来描述一个像素在屏幕上重绘了多少次
原色: 没有overdraw
蓝色: 1次overdraw
绿色: 2次overdraw
粉色: 3次overdraw
红色: 4次及4次以上的overdraw
一般来说, 蓝色是可接受的, 是性能优的.
查看我们的代码如果有过度绘制的话,我们尽量优化代码减少过度绘制,以优化我们的布局。
硬件加速绘制和HARDWARE_LAYER
什么是硬件加速绘制?
实际上应该叫 GPU 加速,软硬件加速的区别主要是图形的绘制究竟是 GPU 来处理还是 CPU,如果是 GPU,就认为是硬件加速绘制,反之,则是软件绘制
如果TargetAPI的级别在14及以上,硬件加速是默认开启的。在判断View是否使用硬件加速绘制的时候,可以使用下面的方式去判断:
chat_wv.post(new Runnable() {
@Override
public void run() {
Log.e("isHardwareAccelerated", ""+chat_wv.isHardwareAccelerated());
}
});
如果在创建View之后就直接使用isHardwareAccelerated()方法去判断,这样会得到false,因为view只是刚创建。
使用硬件加速的情况下绘制效率要优于软件加速,但同时也会增加内存消耗。
什么是HARDWARE_LAYER?
首先Layer的概念主要是针对View的:
查看官方解释,Hardware layer表示拥有一个在硬件上渲染的缓冲区,当然只有开启硬件加速才会有该缓冲区,所以Hardware layer依赖硬件加速打开,当硬件加速没有打开的时候,使用Hardware layer效果和LAYER_TYPE_SOFTWARE一样。
使用Hardware layer的作用:
hardware layer 可用于将复杂视图树缓存到纹理中,并降低绘制操作的复杂性。 例如,在使用旋转动画时,可以使用硬件层仅渲染视图树一次,不用反复渲染,提高效率。
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "rotationY", 180);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setLayerType(View.LAYER_TYPE_NONE, null);
}
});
animator.start();
使用HardwareLayer的时候,需要在使用完成后将layer type设置为LAYER_TYPE_NONE,这样才能释放HardwareLayer占用的内存。
在执行属性动画( alpha \ translation \ scale \ rotation )的时候,只是更新View的Property,每一帧不会去销毁和重建缓存,因此设置为hardware layer对性能有很大的提升。但是如果在onAnimationUpdate中做了添加删除view的操作,或者一直invalidate(),使得视图反复刷新,反而会使得动画性能变差。
查看动画帧率效果的时候可以使用下面的命令来获取App的帧率信息
adb shell dumpsys gfxinfo 包名
参考:Android 中的 Hardware Layer 详解
异步加载Bitmap
加载bitmap的时候,如果bitmap加载的时候不是从内存中实时读取的话,那么这个时候就不应该在主线程中进行加载,因为磁盘读写速度,CPU处理能力,网络环境等都没办法确保图片可以实时加载,从而造成卡顿甚至是ANR(activity位于前台5s,广播10s),
下面例子加载的时候使用AsyncTask:
使用AsyncTask的时候,实现doInBackground()方法,该方法会在子线程中执行,我们可以在该方法中执行耗时操作,但是execute的调用必须放到主线程中。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
// WeakReference用来防止内存泄漏
private final WeakReference<ImageView> imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView);
}
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100);
}
// Once complete, see if ImageView is still around and set bitmap.
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
// 如果imageView被GC回收了就不执行
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
-----------------------------------------------------------------------------
// 使用
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
上面的代码使用WeakReference来引用ImageView对象,这样可以防止内存泄漏。Android中的各种引用的回收情况如下:
强引用:当JVM内存空间不足,JVM宁愿抛出OutOfMemoryError运行时错误(OOM),使程序异常终止,也不会靠随意回收具有强引用
软引用: 软引用的生命周期比强引用短一些。只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象
弱引用:弱引用通过WeakReference类实现。 弱引用的生命周期比软引用短。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
上面的代码完成了实现了图片的异步加载。但是由于Android中的控件会有一套View的回收复用机制,所以这里需要处理View复用的情况:
- 自定义图片加载的Task.
- 自定义一个Drawable,包含一个默认的Bitmap和一个Task,默认的Bitmap用于初始时的显示,Task用于加载图片并显示.
- 在每次绑定childView和Task的时候,由于childView可能是由之前的View复用而来,应此先判断该View是否已经绑定了Task,若有则取消重新绑定,若无则直接绑定.
- 将childView和要加载的图片的Task绑定.
1.自定义的图片加载器子线程加载的代码见上面的BitmapWorkerTask,只不过在onPostExecute时我们需要判断线程是否cancel,以及imageView是否被复用和回收。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
@Override
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled()) {
bitmap = null;
}
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
final BitmapWorkerTask bitmapWorkerTask =
getBitmapWorkerTask(imageView);
// 判断非空
if (this == bitmapWorkerTask && imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
2.自定义Drawable,Drawable中存储当前加载图片的BitmapWorkerTask的弱引用:
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
}
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
获取View绑定的WorkerTask,通过自定义过的Drawable中存储的Task,可以获取到WorkerTask。
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
}
判断是否取消ImageView绑定的Task,注意下面的data只是一个标识符,用于判断该Task是否是所需要的Task,如果是自己当前需要加载的资源的Task,就没必要cancel了,因为当前的imageview加载的就是需要的资源
public static boolean cancelPotentialWork(int data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
// If bitmapData is not yet set or it differs from the new data
if (bitmapData == 0 || bitmapData != data) {
// Cancel previous task
bitmapWorkerTask.cancel(true);
} else {
// The same work is already in progress
return false;
}
}
// No task associated with the ImageView, or an existing task was cancelled
return true;
}
入参的时候传入两个参数:ImageView和data,防止图片加载错乱。
public void loadBitmap(int resId, ImageView imageView) {
// cancelPotentialWork判断是否需要再次加载。
if (cancelPotentialWork(resId, imageView)) {
// 如果取消了就重新创建一个新的Task
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(resId);
}
}
参考:
DisplayingBitmaps
https://www.jianshu.com/p/4b6f57646745
AsyncLayoutInflater异步加载布局
官方链接
使用AsyncLayoutInflater可以在子线程中加载我们的布局,这样不会影响主线程ui。
代码中我们使用以下方式进行使用
new AsyncLayoutInflater(this).inflate(R.layout.activity_async_load, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) {
setContentView(R.layout.activity_main);
Log.i(TAG, "onInflateFinished: ");
}
});
https://www.jianshu.com/p/f0c0eda06ae4
使用的时候我们需要注意以下几点(官方文档中有说明)
- 使用异步 inflate,那么需要这个 layout 的 parent 的 generateLayoutParams 函数是线程安全的;
- 所有构建的 View 中必须不能创建 Handler 或者是调用 Looper.myLooper;(因为是在异步线程中加载的,异步线程默认没有调用 Looper.prepare );
- 异步转换出来的 View 并没有被加到 parent view中,AsyncLayoutInflater 是调用了 LayoutInflater.inflate(int, ViewGroup, false),因此如果需要加到 parent view 中,就需要我们自己手动添加;
- AsyncLayoutInflater 不支持设置 LayoutInflater.Factory 或者 LayoutInflater.Factory2;
- 不支持加载包含 Fragment 的 layout;
- 如果 AsyncLayoutInflater 失败,那么会自动回退到UI线程来加载布局;
使用NinePatch