AsyncLayoutInflater异步加载布局
提到应用性能优化,首先就是布局优化:布局臃肿、层级太深等等会导致绘制卡顿,严重的话影响用户体验。在布局优化一文中,提到布局优化相关的知识。
如果你的应用布局实在是太复杂加载很耗时,甚至造成应用的卡顿,该如何优化呢?思路就是将耗时的布局加载放到子线程中,加载完毕再回到主线程显示,这就是AsyncLayoutInflater的做法。
1、AsyncLayoutInflater
AsyncLayoutInflater用的很少,因为涉及到异步多线程,还是和View加载相关的,玩不好会出问题。应当尽量把应用布局、性能优化好,避免使用AsyncLayoutInflater。
1.1、介绍
AsyncLayoutInflater是用于异步加载布局的帮助类。内部定义了布局加载完成时的回调接口OnInflateFinishedListener。关于AsyncLayoutInflater的源码,详见AsyncLayoutInflater源码颇析。
public interface OnInflateFinishedListener {
void onInflateFinished(@NonNull View view, @LayoutRes int resid,@Nullable ViewGroup parent);
}
1.2、依赖
AsyncLayoutInflater属于Jetpack组件库,多数情况下在使用其他AndroidX库的时候会被其它依赖添加到项目中。也可以手动添加下面的依赖到build.gradle中。
dependencies {
...
//Asynclayoutinflater
implementation "androidx.asynclayoutinflater:asynclayoutinflater:1.0.0"
}
2、AsyncLayoutInflater简单使用
以前Activity在onCreate()中加载布局直接调用AppCompatActivity的setContentView(int )添加布局,如果布局复杂就会因为布局加载而卡顿,通常我们这样在Activity中设置layout:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity);
}
2.1、AsyncLayoutInflater异步加载布局
如果各种布局优化的手段都使用后,效果仍然不佳,这时候AsyncLayoutInflater就派上用场了:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new AsyncLayoutInflater(this).inflate(R.layout.activity, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) {
setContentView(view);
}
});
}
看到没,使用起来很简单:
①传入Context构造AsyncLayoutInflater实例(必须在UI线程创建实例)
②再调用它的inflate()方法,传入要加载的xml布局资源id、父布局(可空)、以及OnInflateFinishedListener回调接口(非空)
③在OnInflateFinishedListener回调接口中将布局设置给界面显示,UI线程中回调该方法
2.2、setContentView(...)
setContentView(...)方法有几个重载,它们在AppCompatActivity中的定义如下:
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
@Override
public void setContentView(View view) {
getDelegate().setContentView(view);
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().setContentView(view, params);
}
都是通过AppCompatDelegate抽象代理类完成布局的添加,AppCompatDelegate的实现类是AppCompatDelegateImpl。当我们传入的参数是int类型的布局xml资源id时,需要先解析xml布局,这是耗时操作。
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
//先解析xml布局,这是耗时的操作
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
但是当我们借助AsyncLayoutInflater在子线程中先加载完布局,直接将View传入setContentView(...)方法就可以替主线程省去一大笔性能开销。
@Override
public void setContentView(View v) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
contentParent.addView(v);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
3、注意坑点
似乎AsyncLayoutInflater的用途不广,是的,别指望它能解决应用布局加载的卡顿问题。看了源码就会更加清楚它内部的实现机制,无非就是:阻塞队列 -> 线程 -> Handler模型。Android中很多优秀的开源库都有这样的设计。
3.1、最大10个布局加载任务
查看源码可以看到,内部设置了异步加载阻塞队列的大小为10,超过这个数量无法将任务加入到队列。虽然不会引起主线程阻塞,但是超出数量的任务会被丢弃,因为内部做了异常捕获。
private static class InflateThread extends Thread {
...
private ArrayBlockingQueue<InflateRequest> mQueue = new ArrayBlockingQueue<>(10);
private SynchronizedPool<InflateRequest> mRequestPool = new SynchronizedPool<>(10);
...
public void enqueue(InflateRequest request) {
try {
mQueue.put(request);
} catch (InterruptedException e) {
throw new RuntimeException(
"Failed to enqueue async inflate request", e);
}
}
}
能不能将AsyncLayoutInflater用到列表适配器Adapter的getView()方法中去加载复杂列表?看来是不可取的,初始滑动列表一下子来十几个任务入队,超出十个的加载任务被丢弃。所以还是直接复用convertView优化列表最方便。
3.2、避免使用Looper或Handler
因为布局是在子线程中解析加载的,所以构建的View中必须不能直接使用 Handler 或者是调用 Looper.myLooper(),因为异步线程默认没有调用 Looper.prepare()。不过却可以使用下面的方式获取主线程Handler,关于Handler的构造,详见博客创建Handler的几种方法。
Handler handler = new Handler(Looper.getMainLooper())
3.3、attachToRoot默认false
使用AsyncLayoutInflater加载布局,内部默认异步inflate转换出来的 View 并没有被加到parent中,必须在回调中手动添加;
request.view = request.inflater.mInflater.inflate(request.resid, request.parent, false);
3.4、不支持Fragment
继续往下看是不是不想用AsyncLayoutInflater了?没想到缺憾这么多。包换Fragment的layout布局无法使用。注释里
/* <p>This inflater does not support setting a {@link LayoutInflater.Factory}
* nor {@link LayoutInflater.Factory2}. Similarly it does not support inflating
* layouts that contain fragments.
*/
3.5、不支持Factory和Factory2
上面的注释也提到了。关于Factory和Factory2以后在深入了解,很多开发者应该都没有使用过。其实这是Android提供的一种hook的方法,开发者通过Factory可以拦截LayoutInflater创建View的过程。
应用场景:XML布局中自定义标签名称;全局替换系统控件为自定义View;替换应用字体;全局换肤。
public interface Factory {
/**
* Hook you can supply that is called when inflating from a LayoutInflater.
* You can use this to customize the tag names available in your XML
* layout files.
*/
View onCreateView(String name, Context context,AttributeSet attrs);
}
public interface Factory2 extends Factory {
/**
* Version of {@link #onCreateView(String, Context, AttributeSet)}
* that also supplies the parent that the view created view will be
* placed in.
*/
View onCreateView(View parent,String name,Context context,AttributeSet attrs);
}
参考资料:
Android Developers > Jetpack > Libraries > Asynclayoutinflater
Android Developers > Docs > Reference > AsyncLayoutInflater
Android Developers > Docs > Reference > AsyncLayoutInflater.OnInflateFinishedListener