主要内容
补充上一篇文章关于LayoutInflater的一点内容,以及AsyncLayoutInflater的原理讲解,还有一点优化经验的分享。
LayoutInflater 的创建过程
提出疑问
上一篇文章中我讲到了PhoneWindow的构造方法中会获取一个LayoutInflater的实例:
public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}
我们注意到,获取LayoutInflater实例的时候都需要传入Context,那么这个Context和LayoutInflater有很么关系?,接下来我们就带着这个问题来看看源码。
带着问题看源码
先看LayoutInflater#from方法:
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
我们可以看到其中调用了Context#getSystemService,这里传入的是Activity的Context,而从ActivityThread#performLaunchActivity中我们知道,这里的Context引用的实际上就是ContextImpl对象,所以我们继续看ContextImpl中的getSystemService方法:
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
这里调用了SystemServiceRegistry#getSystemService:
private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS = new HashMap<>();
public static Object getSystemService(ContextImpl ctx, String name) {
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}
敲黑板了哈🧐,记住上面方法中使用的fetcher.getService(ctx),还有SYSTEM_SERVICE_FETCHERS,下面要考。
下面简述下SystemServiceRegistry是如何是如何获取各个“Service”的:
1. 通过静态代码块注册各“Service”的ServiceFetcher
比如:
static {
......
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
......
}
private static <T> void registerService(String serviceName, Class<T> serviceClass,
ServiceFetcher<T> serviceFetcher) {
......
SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
}
可以看到Fecther被加到了SYSTEM_SERVICE_FETCHERS中。这里的“Service”还有很多,包括ActivityManager、BluetoothManager、BatteryManager等等。
2. 创建CachedServiceFetcher并记录数目
每创建一个CachedServiceFetcher,SystemServiceRegistry中的一个静态变量都会+1:
private static int sServiceCacheSize;
CachedServiceFetcher继承了Fetcher,实现了Fetcher#getService,对“Service”进行了懒创建,即首次调用才创建,又声明了createService抽象方法,该方法用于在在静态代码块中实现特定的创建功能,参考第1点中的代码。因为是静态代码块中创建的,所以一种“Service”对应一个具体的CachedServiceFetcher对象,并且CachedServiceFetcher会记录当前值作为下标,这个下标有什么用呢,看下一点。
3. 调用fetcher.getService(ctx)获取“Service”
在调用fetcher.getService(ctx)的时候,CachedServiceFetcher会通过ContextImp中的一个非静态成员来获取“Service”的对象:
// The system service cache for the system services that are cached per-ContextImpl.
final Object[] mServiceCache = SystemServiceRegistry.createServiceCache();
而SystemServiceRegistry#createServiceCache是利用了SystemServiceRegistry的静态成员sServiceCacheSize进行初始化(就是上面那个会+1的变量):
/**
* Creates an array which is used to cache per-Context service instances.
*/
public static Object[] createServiceCache() {
return new Object[sServiceCacheSize];
}
这下明白了:静态代码块创建了多个“Service”,与此同时静态的sServiceCacheSize记录了“Service”的数量,CachedServiceFetcher记录了“Service”的下标,在每个ContextImpl被创建的时候,非静态的mServiceCache就根据sServiceCacheSize进行初始化,在我们调用Context#getSystemService的时候,如果当前Context的mServiceCache没有对应的“Service”,就创建一个,放到CachedServiceFetcher事先记录好的下标下。
所以,一个ContextImpl对象会持有一套“Service”,保存在mServiceCache中。当然,如果没有“get”过这个“Service”,它就不会被创建(所以我从来都是只用Application的Context来“getService”,这样各种“Service”就只有一个实例,减少对象数目😂)。总结一下:
结论
Context和LayoutInflater有很么关系?他们的关系就是:一个Context对象会持有最多1个LayoutInflater实例,不同的Context会持有各自的LayoutInflater实例。
AsyncLayoutInflater
当一个Activity需要加载的页面过于复杂的时候,而我们又希望主线程可以“减负”,以免造成卡顿,此时我们就可以考虑使用AsyncLayoutInflater来异步完成。源码部分注释如下:
This is intended for parts of the UI that are created lazily or in response to user interactions. This allows the UI thread to continue to be responsive & animate while the relatively heavy inflate is being performed.
这个类的作用就是异步创建View,使界面能在大量加载View的时候及时响应用户并且能播放动画。
AsyncLayoutInflater 的重要组成元素
OnInflateFinishedListener
回调接口,在inflate方法中传入对象,异步执行完成后,通过Handler回调主线程执行该对象中的回调方法。
BasicInflater
从上面的文章我们知道ContextImpl中的LayoutInflater其实是PhoneLayoutInflater,这里的BasicInflater与PhoneLayoutInflater一模一样,它也是AsyncLayoutInflater中的内部类。
InflateRequest
InflateRequest代表一次inflate请求,它包含了一次inflate的所有关键信息,它是AsyncLayoutInflater中的内部类。
private static class InflateRequest {
AsyncLayoutInflater inflater; // AsyncLayoutInflater本身
ViewGroup parent; // inflate方法传入参数,父布局
int resid; // inflate方法传入参数,注入布局ID
View view; // 根据布局创建的View
OnInflateFinishedListener callback; // 回调对象
InflateRequest() {
}
}
InflateThread
用来执行异步任务的线程,他其中有一个阻塞队列+一个对象池。对象池是用来复用InflateRequest对象的;线程的run方法中是一个无限循环,他会从阻塞队列中take元素。
这里面有个很有意思的地方:
private static class InflateThread extends Thread {
private static final InflateThread sInstance;
static {
sInstance = new InflateThread();
sInstance.start();
}
public static InflateThread getInstance() {
return sInstance;
}
......
}
类似于单例模式的实现方式,这里的静态代码块其实是一种懒加载,在第一次调用getInstance的时候才会被执行,再看看AsyncLayoutInflater的构造方法就知道,InflateThread启动的时候,也就是我们创建AsyncLayoutInflater的时候:
public AsyncLayoutInflater(@NonNull Context context) {
mInflater = new BasicInflater(context);
mHandler = new Handler(mHandlerCallback);
mInflateThread = InflateThread.getInstance();
}
并且InflateThread也是单例模式的。
AsyncLayoutInflater 运行原理
其实AsyncLayoutInflater代码不多,原理也很简单:
- 创建
AsyncLayoutInflater时异步线程启动,然后被阻塞队列take方法阻塞。 - 通过
inflate方法传入构造View的信息与回调对象,然后从对象池中取出InflateRequest对象进行信息更新,然后放入阻塞队列中。 - 阻塞队列
take获取到元素,异步线程开始执行View的创建,并在执行完成后发送消息给Handler。 Handler回调主线程,执行OnInflateFinishedListener回调对象,然后回收InflateRequest对象。
过程大致如下:
关于首帧优化
之前我们的App做了一次首页的首帧优化,优化的内容是什么呢,就是刚跳转首页的时候会有一小段的白屏时间,这个是因为整个测量、布局、绘制时间太长的原因,从systrace中看到的就是performTraversals时间太长,我们的View层级很复杂,不易重构,所以我们就想到了AsyncLayoutInflater,但是这还是不能解决我们的问题,因为这只是异步,界面还是要等到异步执行完成才显示出来,后来我们使用了一种方法——ViewStub,让标题、导航栏等元素首先加载,其余元素使用ViewStub延迟300-500毫秒之后注入,整个界面看上去就是瞬间显示,只是有些元素会有淡入的效果。
以上是一点经验的记录。