下面,笔者将与大家一起进入进行布局优化的实操环节。
六、布局优化常规方案
布局优化的方法有很多,大部分主流的方案笔者已经在Android性能优化之绘制优化里讲解过了。下面,我将介绍一些其它的优化方案。
1、布局Inflate优化方案演进
1、代码动态创建View
使用Java代码动态添加控件的简单示例如下:
Button button=new Button(this);
button.setBackgroundColor(Color.RED);
button.setText("Hello World");
ViewGroup viewGroup = (ViewGroup) LayoutInflater.from(this).inflate(R.layout.activity_main, null);
viewGroup.addView(button);
2、替换MessageQueue来实现异步创建View
在使用子线程创建视图控件的时候,我们可以把子线程Looper的MessageQueue替换成主线程的MessageQueue,在创建完需要的视图控件后记得将子线程Looper中的MessageQueue恢复为原来的。在Awesome-WanAndroid项目下的UiUtils的Ui优化工具类中,提供了相应的实现,代码如下所示:
/**
* 实现将子线程Looper中的MessageQueue替换为主线程中Looper的
* MessageQueue,这样就能够在子线程中异步创建UI。
*
* 注意:需要在子线程中调用。
*
* @param reset 是否将子线程中的MessageQueue重置为原来的,false则表示需要进行替换
* @return 替换是否成功
*/
public static boolean replaceLooperWithMainThreadQueue(boolean reset) {
if (CommonUtils.isMainThread()) {
return true;
} else {
// 1、获取子线程的ThreadLocal实例
ThreadLocal<Looper> threadLocal = ReflectUtils.reflect(Looper.class).field("sThreadLocal").get();
if (threadLocal == null) {
return false;
} else {
Looper looper = null;
if (!reset) {
Looper.prepare();
looper = Looper.myLooper();
// 2、通过调用MainLooper的getQueue方法区获取主线程Looper中的MessageQueue实例
Object queue = ReflectUtils.reflect(Looper.getMainLooper()).method("getQueue").get();
if (!(queue instanceof MessageQueue)) {
return false;
}
// 3、将子线程中的MessageQueue字段的值设置为主线的MessageQueue实例
ReflectUtils.reflect(looper).field("mQueue", queue);
}
// 4、reset为false,表示需要将子线程Looper中的MessageQueue重置为原来的。
ReflectUtils.reflect(threadLocal).method("set", looper);
return true;
}
}
}
3、AsynclayoutInflater异步创建View
在第三小节中,我们对Android的布局加载原理进行了深入地分析,从中我们得出了布局加载过程中的两个耗时点:
- 1、布局文件读取慢:IO过程。
- 2、创建View慢:使用反射,比直接new的方式要慢3倍。布局嵌套层级越多,控件个数越多,反射的次数就会越频繁。
很明显,我们无法从根本上去解决这两个问题,但是Google提供了一个从侧面解决的方案:使用AsyncLayoutInflater去异步加载对应的布局,它的特点如下:
- 1、工作线程加载布局。
- 2、回调主线程。
- 3、节省主线程时间。
接下来,我将详细地介绍AsynclayoutInflater的使用。
首先,在项目的build.gradle中进行配置:
implementation 'com.android.support:asynclayoutinflater:28.0.0'
然后,在Activity中的onCreate方法中将setContentView注释:
super.onCreate(savedInstanceState);
// 内部分别使用了IO和反射的方式去加载布局解析器和创建对应的View
// setContentView(R.layout.activity_main);
接着,在super.onCreate方法前继续布局的异步加载:
// 使用AsyncLayoutInflater进行布局的加载
new AsyncLayoutInflater(MainActivity.this).inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NonNull View view, int i, @Nullable ViewGroup viewGroup) {
setContentView(view);
// findViewById、视图操作等
}
});
super.onCreate(savedInstanceState);
接下来,我们来分析下AsyncLayoutInflater的实现原理与工作流程。
由于我们是使用new的方式创建的AsyncLayoutInflater,所以我们先来看看它的构造函数:
public AsyncLayoutInflater(@NonNull Context context) {
// 1
this.mInflater = new AsyncLayoutInflater.BasicInflater(context);
// 2
this.mHandler = new Handler(this.mHandlerCallback);
// 3
this.mInflateThread = AsyncLayoutInflater.InflateThread.getInstance();
}
在注释1处,创建了一个BasicInflater,它内部的onCreateView并没有使用Factory做AppCompat控件兼容的处理:
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
String[] var3 = sClassPrefixList;
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
String prefix = var3[var5];
try {
View view = this.createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException var8) {
}
}
return super.onCreateView(name, attrs);
}
由前面的分析可知,在createView方法中仅仅是做了反射创建出对应View的处理。
接着,在注释2处,创建了一个全局的Handler对象,主要是用于将异步线程创建好的View实例及其相关信息回调到主线程。
最后,在注释3处,获取了一个用于异步加载View的线程实例。
接着,我们继续跟踪AsyncLayoutInflater实例的inflate方法:
@UiThread
public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent, @NonNull AsyncLayoutInflater.OnInflateFinishedListener callback) {
if (callback == null) {
throw new NullPointerException("callback argument may not be null!");
} else {
// 1
AsyncLayoutInflater.InflateRequest request = this.mInflateThread.obtainRequest();
request.inflater = this;
request.resid = resid;
request.parent = parent;
request.callback = callback;
this.mInflateThread.enqueue(request);
}
}
在注释1处,这里使用InflateRequest对象将我们传进来的三个参数进行了包装,并最终将这个InflateRequest对象加入了mInflateThread线程中的一个ArrayBlockingQueue中:
public void enqueue(AsyncLayoutInflater.InflateRequest request) {
try {
this.mQueue.put(request);
} catch (InterruptedException var3) {
throw new RuntimeException("Failed to enqueue async inflate request", var3);
}
}
并且,在InflateThread这个静态内部类的静态代码块中调用了其自身实例的start方法以启动线程:
static {
sInstance.start();
}
public void run() {
while(true) {
this.runInner();
}
}
public void runInner() {
AsyncLayoutInflater.InflateRequest request;
try {
// 1
request = (AsyncLayoutInflater.InflateRequest)this.mQueue.take();
} catch (InterruptedException var4) {
Log.w("AsyncLayoutInflater", var4);
return;
}
try {
// 2
request.view = request.inflater.mInflater.inflate(request.resid, request.parent, false);
} catch (RuntimeException var3) {
Log.w("AsyncLayoutInflater", "Failed to inflate resource in the background! Retrying on the UI thread", var3);
}
// 3
Message.obtain(request.inflater.mHandler, 0, request).sendToTarget();
}
在run方法中,使用了死循环的方式去不断地调用runInner方法,在runInner方法中,首先在注释1处从ArrayBlockingQueue队列中获取一个InflateRequest对象,然后在注释2处将异步加载好的view对象赋值给了InflateRequest对象,最后,在注释3处,将请求作为消息发送给了Handler的handleMessage:
private Callback mHandlerCallback = new Callback() {
public boolean handleMessage(Message msg) {
AsyncLayoutInflater.InflateRequest request = (AsyncLayoutInflater.InflateRequest)msg.obj;
// 1
if (request.view == null) {
request.view = AsyncLayoutInflater.this.mInflater.inflate(request.resid, request.parent, false);
}
request.callback.onInflateFinished(request.view, request.resid, request.parent);
AsyncLayoutInflater.this.mInflateThread.releaseRequest(request);
return true;
}
};
在handleMessage方法中,当异步加载得到的view为null时,此时在注释1处还做了一个fallback处理,直接在主线程进行view的加载,以此兼容某些异常情况,最后,就调用了回调接口的onInflateFinished方法将view的相关信息返回给Activity对象。
小结
由以上分析可知,AsyncLayoutInflater是通过侧面缓解的方式去缓解布局加载过程中的卡顿,但是它依然存在一些问题:
- 1、不能设置LayoutInflater.Factory,需要通过自定义AsyncLayoutInflater的方式解决,由于它是一个final,所以需要将代码直接拷处进行修改。
- 2、因为是异步加载,所以需要注意在布局加载过程中不能有依赖于主线程的操作。
由于AsyncLayoutInflater仅仅只能通过侧面缓解的方式去缓解布局加载的卡顿,因此,我们下面将介绍一种从根本上解决问题的方案。对于AsynclayoutInflater的改进措施,可以查看祁同伟同学封装之后的代码,具体的改进分析可以查看Android AsyncLayoutInflater 限制及改进,这里附上改进之后的代码:
/**
* 实现异步加载布局的功能,修改点:
* 1. 单一线程;
* 2. super.onCreate之前调用没有了默认的Factory;
* 3. 排队过多的优化;
*/
public class AsyncLayoutInflaterPlus {
private static final String TAG = "AsyncLayoutInflaterPlus";
private Handler mHandler;
private LayoutInflater mInflater;
private InflateRunnable mInflateRunnable;
// 真正执行加载任务的线程池
private static ExecutorService sExecutor = Executors.newFixedThreadPool(Math.max(2,
Runtime.getRuntime().availableProcessors() - 2));
// InflateRequest pool
private static Pools.SynchronizedPool<AsyncLayoutInflaterPlus.InflateRequest> sRequestPool = new Pools.SynchronizedPool<>(10);
private Future<?> future;
public AsyncLayoutInflaterPlus(@NonNull Context context) {
mInflater = new AsyncLayoutInflaterPlus.BasicInflater(context);
mHandler = new Handler(mHandlerCallback);
}
@UiThread
public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent, @NonNull CountDownLatch countDownLatch,
@NonNull AsyncLayoutInflaterPlus.OnInflateFinishedListener callback) {
if (callback == null) {
throw new NullPointerException("callback argument may not be null!");
}
AsyncLayoutInflaterPlus.InflateRequest request = obtainRequest();
request.inflater = this;
request.resid = resid;
request.parent = parent;
request.callback = callback;
request.countDownLatch = countDownLatch;
mInflateRunnable = new InflateRunnable(request);
future = sExecutor.submit(mInflateRunnable);
}
public void cancel() {
future.cancel(true);
}
/**
* 判断这个任务是否已经开始执行
*
* @return
*/
public boolean isRunning() {
return mInflateRunnable.isRunning();
}
private Handler.Callback mHandlerCallback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
AsyncLayoutInflaterPlus.InflateRequest request = (AsyncLayoutInflaterPlus.InflateRequest) msg.obj;
if (request.view == null) {
request.view = mInflater.inflate(
request.resid, request.parent, false);
}
request.callback.onInflateFinished(
request.view, request.resid, request.parent);
request.countDownLatch.countDown();
releaseRequest(request);
return true;
}
};
public interface OnInflateFinishedListener {
void onInflateFinished(View view, int resid, ViewGroup parent);
}
private class InflateRunnable implements Runnable {
private InflateRequest request;
private boolean isRunning;
public InflateRunnable(InflateRequest request) {
this.request = request;
}
@Override
public void run() {
isRunning = true;
try {
request.view = request.inflater.mInflater.inflate(
request.resid, request.parent, false);
} catch (RuntimeException ex) {
// Probably a Looper failure, retry on the UI thread
Log.w(TAG, "Failed to inflate resource in the background! Retrying on the UI"
+ " thread", ex);
}
Message.obtain(request.inflater.mHandler, 0, request)
.sendToTarget();
}
public boolean isRunning() {
return isRunning;
}
}
private static class InflateRequest {
AsyncLayoutInflaterPlus inflater;
ViewGroup parent;
int resid;
View view;
AsyncLayoutInflaterPlus.OnInflateFinishedListener callback;
CountDownLatch countDownLatch;
InflateRequest() {
}
}
private static class BasicInflater extends LayoutInflater {
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
&