Android布局优化-AsyncLayoutInflater简析

AsyncLayoutInflater

布局加载过程中存在两个耗时点:

  1. 布局文件读取慢,涉及IO操作
  2. 根据<tag>创建View慢(createViewFromTag()),使用反射的方式创建View。布局嵌套层数越多,控件个数越多,反射的次数就会越多

当XML文件过大、嵌套过深时,就会导致页面发生卡顿甚至ANR。

解决方案有两种:

  1. 直接解决:不使用IO操作以及反射
  2. 侧面缓解:把耗时操作放到子线程,等待加载完毕返回主线程展示即可。下面提到的AsyncLayoutInflater就是使用这个方案。

AsyncLayoutInflater

采用异步加载的方式去加载布局,可以节省主线程时间,并且在异步加载完毕后回到主线程。

使用方法

1
2
3
4
5
6
new AsyncLayoutInflater(this).inflate(R.layout.XX, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NonNull View view, int i, @Nullable ViewGroup viewGroup) {
setContentView(view);
}
});

基本原理

构造方法

1
2
3
4
5
6
7
8
9
10
11
public final class AsyncLayoutInflater {
LayoutInflater mInflater;//布局加载器
Handler mHandler;//处理加载完成消息
InflateThread mInflateThread;//执行加载任务

public AsyncLayoutInflater(@NonNull Context context) {
mInflater = new BasicInflater(context);
mHandler = new Handler(mHandlerCallback);
mInflateThread = InflateThread.getInstance();
}
}

BasicInflater

自定义加载器。实现类似PhoneLayoutInflater(默认布局加载器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

private static class BasicInflater extends LayoutInflater {
//优先在这三个包下加载
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};

BasicInflater(Context context) {
super(context);
}

@Override
public LayoutInflater cloneInContext(Context newContext) {
return new BasicInflater(newContext);
}

@Override
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
//加载View
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {
// In this case we want to let the base class take a crack
// at it.
}
}

return super.onCreateView(name, attrs);
}
}

InflateThread

创建子线程,将布局加载请求加入阻塞队列中,按照插入顺序执行LayoutInflater.inflate()加载过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
private static class InflateThread extends Thread {
private static final InflateThread sInstance;
static {
sInstance = new InflateThread();
sInstance.start();
}
//阻塞队列 最多支持10个加载请求
private ArrayBlockingQueue<InflateRequest> mQueue = new ArrayBlockingQueue<>(10);
//对象池,缓存InflateThread对象
private SynchronizedPool<InflateRequest> mRequestPool = new SynchronizedPool<>(10);
//对象池获取缓存对象
public InflateRequest obtainRequest() {
InflateRequest obj = mRequestPool.acquire();
if (obj == null) {
obj = new InflateRequest();
}
return obj;
}
//对象池回收对象,便于下次复用
public void releaseRequest(InflateRequest obj) {
obj.callback = null;
obj.inflater = null;
obj.parent = null;
obj.resid = 0;
obj.view = null;
mRequestPool.release(obj);
}
//将inflate请求添加到 阻塞队列中
public void enqueue(InflateRequest request) {
try {
mQueue.put(request);
} catch (InterruptedException e) {
throw new RuntimeException(
"Failed to enqueue async inflate request", e);
}
}
//需要执行的任务
public void runInner() {
InflateRequest request;
try {
//阻塞队列获取 任务,没任务则阻塞
request = mQueue.take();
} catch (InterruptedException ex) {
// Odd, just continue
Log.w(TAG, ex);
return;
}

try {
//调用BasicInflater去加载布局
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);
}
//构建消息发送到Handler
Message.obtain(request.inflater.mHandler, 0, request)
.sendToTarget();
}

@Override
public void run() {
while (true) {
runInner();
}
}
}

InflateThread不管最后inflate()执行成功或失败,都会把结果发回到Handler进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private Handler.Callback mHandlerCallback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
//获取加载结果
InflateRequest request = (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);
//回收加载请求
mInflateThread.releaseRequest(request);
return true;
}
};

Handler收到消息后,根据InflateRequest.view是否为空,判断接下执行步骤:

如果为空,回到主线程进行布局加载任务,加载完成后回调onInflateFinished()

不为空,直接回调onInflateFinished()

inflate()

发起异步加载布局请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@UiThread
public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent,
@NonNull OnInflateFinishedListener callback)
{
if (callback == null) {
throw new NullPointerException("callback argument may not be null!");
}
//构建InflateRequest对象
InflateRequest request = mInflateThread.obtainRequest();
request.inflater = this;
request.resid = resid;
request.parent = parent;
request.callback = callback;
//插入加载请求到阻塞队列
mInflateThread.enqueue(request);
}

InflateRequest

主线程和子线程之间传递的数据模型,主要封装了异步加载需要的参数

1
2
3
4
5
6
7
8
9
10
private static class InflateRequest {
AsyncLayoutInflater inflater;//加载器
ViewGroup parent;//父布局
int resid;//布局id
View view;//加载完成的View
OnInflateFinishedListener callback;//加载完成回调

InflateRequest() {
}
}

OnInflateFinishedListener

布局加载完成后回调

1
2
3
4
5
public interface OnInflateFinishedListener {
void onInflateFinished(@NonNull View view, //加载完成的View
@LayoutRes int resid,
@Nullable ViewGroup parent)
;
}

使用AsyncLayoutInflater加载布局后,将需要加载的layoutId以及OnInflateFinishedListener构造成InflateRequest,插入到InflateThread的阻塞队列中,等待执行。任务执行完毕后,返回执行结果(成功返回加载后的View,失败返回null)。

通过Handler发送结果回到主线程,返回结果为null,则在主线程再次执行布局加载,得到结果后直接回调onInflateFinished()

局限及改进

局限

  1. AsyncLayoutInflater构造的View,无法直接使用handler或者调用looper.myLooper,因为没有进行初始化
  2. AsyncLayoutInflater构造的View,不会自动加到parent中,需要手动加入
  3. AsyncLayoutInflater不支持设置Factory/Factory2,未设置mPrivateFactory所以不支持包含<fragment>的布局
  4. 最多支持10个布局加载,超出的布局需要等待。

改进

AsyncLayoutInflaterfinal的,无法被继承。需要copy一份代码进行修改。

针对4可以内部替换成线程池,将加载布局请求放入线程池管理

针对3可以修改BasicInflater实现,内部支持factory设置

1
2
3
4
5
6
7
8
9
10
11
BasicInflater(Context context) {
super(context);
if (context instanceof AppCompatActivity) {
// 加上这些可以保证AppCompatActivity的情况下,super.onCreate之前
// 使用AsyncLayoutInflater加载的布局也拥有默认的效果
AppCompatDelegate appCompatDelegate = ((AppCompatActivity) context).getDelegate();
if (appCompatDelegate instanceof LayoutInflater.Factory2) {
LayoutInflaterCompat.setFactory2(this, (LayoutInflater.Factory2) appCompatDelegate);
}
}
}

参考链接

AsyncLayoutInfalter


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!