AsyncLayoutInflater
是一个异步初始化布局的 Helper
类。
它的本质就是把对布局文件的 inflate
放入到了子线程里面,等到初始化成功后,在通过接口抛回到主线程。
在 Activity
中的简单代码实现为:
// 异步加载 xml, 在 Activity.onCreate(xxx) 里面
AsyncLayoutInflater(this).inflate(R.layout.activity_try_everything, null, object : AsyncLayoutInflater.OnInflateFinishedListener {
override fun onInflateFinished(view: View, p1: Int, p2: ViewGroup?) {
setContentView(view)
}
})
一般来说,我们都是通过 setContentView(R.layout.xxx)
的形式加载 xml
的.
其实有三个不同方法参数的 setContentView()
函数, 如下
public void setContentView(@LayoutRes int layoutResID) {
this.getDelegate().setContentView(layoutResID);
}
public void setContentView(View view) {
this.getDelegate().setContentView(view);
}
public void setContentView(View view, LayoutParams params) {
this.getDelegate().setContentView(view, params);
}
我们可以通过 inflate
出来 layoutResID
成一个对应的 view
, 然后调用 setContentView(View view)
, 通过这样传递。
那么 AsyncLayoutInflater
是如何工作的呢?
以下内容分为以下部分:
-
AsyncLayoutInflater
的简单介绍和创建 - 添加
request
初始化布局的请求:AsyncLayoutInflater.inflate(xxx)
-
Handler
里面处理Message
发送的消息, 把结果返回给UI
线程 - 小结
1. AsyncLayoutInflater
的简单介绍和创建
1.1 AsyncLayoutInflater
的简单介绍
AsyncLayoutInflater
的实现, 它主要有三个成员变量和一个内部类:
-
LayoutInflater
用来inflate
布局 -
Handler
用来 post 到主线程 -
InflateThread
是一个Thread
,一个子线程,在里面完成inflate
过程 -
InflateRequest
静态内部类,用来承载必要的信息 -
OnInflateFinishedListener
对外部暴漏的接口,用于通知外部,已经初始化View
完成
1.2 AsyncLayoutInflater
的创建
AsyncLayoutInflater
的创建, 从代码中可以看到:
public AsyncLayoutInflater(@NonNull Context context) {
// 在创建一个 AsyncLayoutInflater 对象时,会同时新建 inflater, handler, 和 thread
mInflater = new BasicInflater(context);
mHandler = new Handler(mHandlerCallback);
mInflateThread = InflateThread.getInstance();
}
其中,代码分析如下:
mInflater
是后续用来inflate
布局-
mInflateThread
是一个单例对象,在获取时该thread
已经开始工作start
代码如下:private static final InflateThread sInstance; // 静态代码块,在加载该类时就会被调用 static { sInstance = new InflateThread(); // 此时子线程已经开始跑 sInstance.start(); }
-
mHandler
是用来接收thread
发出的消息,回到UI
线程Message
的发送消息位置:Message.obtain(request.inflater.mHandler, 0, request).sendToTarget()
并且把主要的信息「即
request
」放入在Message
中的object
中,
做好上面的准备后,此时 mInflateThread
里面是没有请求要执行的,一旦它的队列里有了 request
, 则会执行对应的逻辑
2. 添加 request
初始化布局的请求: AsyncLayoutInflater.inflate(xxx)
当调用该方法时,会有一系列步骤:
- 创建
InflateRequest
请求; - 通过
mInflateThread.enqueue(request)
把该请求放入到mInflateThread
子线程里面
源码如下:
@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 request = mInflateThread.obtainRequest();
request.inflater = this;
request.resid = resid;
request.parent = parent;
request.callback = callback;
mInflateThread.enqueue(request);
}
从代码中可以看到,每一次调用 inflate(xxx)
方法都会新创建一个 InflateRequest
, 并且把该 request
加入 mInflateThread
的队列中。
2.1 创建 InflateRequest
请求
而在 InflateThread
中有一个队列 mQueue
用来存放 InflateRequest
请求
private ArrayBlockingQueue<InflateRequest> mQueue = new ArrayBlockingQueue<>(10);
InflateThread
线程在 start()
之后,会去调用 runInner()
尝试获取 inflateRequest
然后执行对应逻辑。
@Override
public void run() {
while (true) {
runInner();
}
}
注:这里并不会是一个死循环, 因为
mQueue.take()
方法。
ArrayBlockingQueue
是会产生阻塞的队列,在take()
方法中,如果count == 0
, 则会一直陷入notEmpty.await()
ArrayBlockingQueue
的 take()
方法源码:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 这里会一直等待,直到有消息为止
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
2.2 通过 mInflateThread.enqueue(request)
添加请求后,mQueue
不为空
当该 mQueue
里面可以获取到 request
. 则会通过 inflater.inflate(xxx)
在子线程中完成 view
的构建,并通过 Message
发送消息给对应的 handler
处理。代码如下:
public void runInner() {
InflateRequest request;
try {
// 获取是否有请求
request = mQueue.take();
} catch (InterruptedException ex) {
// Odd, just continue
Log.w(TAG, ex);
return;
}
//
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();
}
3. Handler
里面处理 Message
发送的消息, 把结果返回给 UI
线程
通过 Message
发送消息后,真正接受消息的地方是在 Handler
的 handleMessage(xxx)
方法里面,
此时已经切回到了 UI
线程中:
@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;
}
代码逻辑:
- 从
msg.obj
中获取到InflateRequest
- 判断
request.view
是否为null
- 如果为空,则重新
inflate
,此时是在UI
线程中进行的,和一般的初始化一样; - 通过接口
OnInflateFinishedListener
通知外部,并把得到的view
传递出去
注:在这里做了兼容,防止在异步中
inflate
失败,做了判断
对外暴漏的接口:AsyncLayoutInflater.OnInflateFinishedListener
没太多值得说的,类似与最简单的 View.OnClickListener
一样,通过接口把事情抛到外部的调用方。
4. 小结
上述部分,简单的介绍了 AsyncLayoutInflater
的内部实现。
可能会在心里产生一点点疑问:这个简单来说不就是 new
了一个新的子线程,然后 inflate
好 view
后,重新放入到 UI
线程中, 会有很大用处吗?
确实,AsyncLayoutInflater
简单来说就是实现了上述表述。
而且它也有很多缺点,官方文档是明确的写着:
This inflater does not support setting a LayoutInflater.Factory nor LayoutInflater.Factory2. Similarly it does not support inflating layouts that contain fragments.
有着局限性,这也是我们一般不会使用这种方式的一个很大原因。
但是呢?当一个 XML
已经可以支持异步 inflate
, 那么能不能 PreInflate
呢?
能不能提前在子线程中去 inflate
布局?然后在真正需要这些 view
的时候,就可以省去加载布局的时间了?
当然可以,具体怎么实现还需要接下来再次梳理。
本次只是一个简单的介绍 AsyncLayoutInflater
。
期待后续能够整理好。
2019.10.10 by chendroid
文章来自公众号:「Droid 二三事
」
PS:
后续会接着整理梳理知识。
参考:
- 官方文档说明:
https://developer.android.com/reference/android/support/v4/view/AsyncLayoutInflater
https://juejin.im/post/5b651ad8e51d4519635008bd
简书:https://www.jianshu.com/p/a3a3bd314c45