【Android面试】高级UI面试题——View.inflate
View.inflater过程与异步inflater
这道题想考察什么?
考察同学对xml解析过程,以及异步解析过程,但是异步解析在使用过程中有诸多限制,所以使用的并不太多。
考生应该如何回答
Inflate解析
我们先分析一下View.inflate 的代码:
// View.java
public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
LayoutInflater factory = LayoutInflater.from(context);
return factory.inflate(resource, root);
}
上面的代码不难发现是调用了LayoutInflater.inflate,于是我们将目光放到这个函数里面来
// LayoutInflater.java
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
// 获取资源解析器 XmlResourceParser
XmlResourceParser parser = res.getLayout(resource);
try {
// 解析View
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
final Context inflaterContext = mContext;
// 存储传进来的根布局
View result = root;
try {
final String name = parser.getName();
//解析 merge标签,rInflate方法会将 merge标签下面的所有子 view添加到根布局中
// 这也是为什么 merge 标签可以简化布局的效果
if (TAG_MERGE.equals(name)) {
// 必须要有父布局,否则报错
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
}
// 解析 merge标签下的所有的 View,添加到根布局中
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// 获取 xml 布局的根 View 对象,比如 LinearLayout 对象,FrameLayout 对象等
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
// 第一种情况:root != null &&attachToRoot 为false,通过 root 来获取根节点的布局参数 ViewGroup.LayoutParams 对象,也就是说,把 xml 中的根节点的 layout_ 开头的属性,如layout_width 和 layout_height 对应的值转为布局参数对象中的字段值,如width 和 height 值
if (root != null) {
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 将 root 提供的 LayoutParams 设置给View
temp.setLayoutParams(params);
}
}
// 渲染子View
rInflateChildren(parser, temp, attrs, true);
// 第二种情况:root != null,且attachToRoot = true,新解析出来的 View 会被 add 到 root 中去,然后将 root 作为结果返回
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// 第三种情况:root == null,或者 attachToRoot = false,直接返回View,这意味着此时的temp没有解析xml中的layout属性
if (root == null || !attachToRoot) {
result = temp;
}
}
}
// 返回result,可能是View,也可能是root
return result;
}
}
从上面的代码大家应该不难发现,inflate 主要是解析xml的属性,并生成xml的根布局temp,并且基于inflate的参数情况,来确定是否将temp添加到 解析它的 外层布局上面,也就是root变量上面。
inflate 的三个参数,其中第二和第三参数分下面几种情况:
- 当 root != null 且 attachToRoot == false 时,新解析的 View 会直接作为结果返回,而且 root 会为新解析的 View 生成 LayoutParams 并设置到该 View 中去。
- 当 root != null 且 attachToRoot == true 时,新解析出来的 View 会被 add 到 root 中去,然后将 root 作为结果返回。
- 当 root == null 且 attachToRoot == false 时,新解析的 View 会直接作为结果返回,注意:此时的view是没有解析它xml中的layout params属性的,所以,xml中设置的宽高将可能失效。
接下来,我们分析一下createViewFromTag 源码
// LayoutInflater.java
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
...
if (view == null) {
if (-1 == name.indexOf('.')) {
// 创建android sdk 的View,例如:LinearLayout、Button等,最终还是调用的createView
view = onCreateView(context, parent, name, attrs);
} else {
// 创建 用户自定义View
view = createView(context, name, null, attrs);
}
}
...
return view;
}
从上面的函数我们发现他们主要是执行了onCreateView函数,那么onCreateView又做了什么呢?
public final View createView(@NonNull Context viewContext, @NonNull String name,
@Nullable String prefix, @Nullable AttributeSet attrs)
throws ClassNotFoundException, InflateException {
// 从缓存中获取constructor
Constructor<? extends View> constructor = sConstructorMap.get(name);
Class<? extends View> clazz = null;
try {
// 没有缓存,则创建
if (constructor == null) {
// 通过全类名获取 Class 对象,如果是sdk的View,需要拼接字符串
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
// 获取 Constructor 对象
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
// 用静态 HashMap保存,优化性能,同一个类,下次就可以直接拿这个constructor创建
sConstructorMap.put(name, constructor);
} else {
}
try {
// 创建View对象
final View view = constructor.newInstance(args);
// 返回创建的View对象
return view;
}
}
}
通过上面大家不难发现 onCreateView 是在通过反射的方式去创建XML中定义的view。并将他们构建成
小结
整个Inflater.inflate的过程,其实就是就是先进行xml解析,拿到xml中相关的tag的情况下,然后通过反射创建tag对应的View对象的一个流程。
异步inflate
Google开发者在v4包中增加了一个用来异步inflate layouts的帮助类。
AsyncLayoutInflater
AsyncLayoutInflater 是来帮助作异步加载 layout 的,inflate(int, ViewGroup, OnInflateFinishedListener) 方法运行结束以后 OnInflateFinishedListener 会在主线程回调返回 View;这样作旨在 UI 的懒加载或者对用户操作的高响应。
简单的说咱们知道默认状况下 setContentView 函数是在 UI 线程执行的,其中有一系列的耗时动做:Xml的解析、View的反射建立等过程一样是在UI线程执行的,AsyncLayoutInflater 就是来帮咱们把这些过程以异步的方式执行,保持UI线程的高响应。
AsyncLayoutInflater 只有一个构造函数及普通调用函数:inflate(int resid, ViewGroup parent, AsyncLayoutInflater.OnInflateFinishedListener callback),使用也很是方便。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new AsyncLayoutInflater(AsyncLayoutActivity.this)
.inflate(R.layout.async_layout, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(View view, int resid, ViewGroup parent) {
setContentView(view);
}
});
// 别的操做
}
AsyncLayoutInflater 构造函数
public AsyncLayoutInflater(@NonNull Context context) {
mInflater = new BasicInflater(context);
mHandler = new Handler(mHandlerCallback);
mInflateThread = InflateThread.getInstance();
}
主要作了三件事情:
1.建立 BasicInflater。BasicInflater 继承自 LayoutInflater,只是覆写了 onCreateView:优先加载sClassPrefixList 中做描述的这三个前缀的 view,而后才按照默认的流程去加载,由于大多数状况下咱们 Layout 中使用的View都在这三个 package 下。
private static class BasicInflater extends LayoutInflater {
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
@Override
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {
}
}
return super.onCreateView(name, attrs);
}
}
2.建立 Handler。建立 Handler 和它普通的做用同样,就是为了线程切换,AsyncLayoutInflater 是在异步里 inflate layout,那建立出来的 View 对象须要回调给主线程,就是经过 Handler 来实现的。
3.获取 InflateThread 对象。InflateThread 从名字上就好理解,是来作 Inflate 工做的工做线程,经过 InflateThread.getInstance 能够猜想 InflateThread 里面是一个单例,默认只在一个线程中作全部的加载工做,这个类咱们会在下面重点分析
inflate
AsyncLayoutInflater 中有一个非常重要的函数:inflate,那么这个函数是怎样的呢?我们一起看代码
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);
}
首先,会经过 InflateThread 去获取一个 InflateRequest,其中有一堆的成员变量。为何须要这个类呢?由于后续异步 inflate 须要一堆的参数(对应 InflateRequest 中的变量),会致使方法签名过长,而使用 InflateRequest 就避免了这一点。那么InflateRequest是什么呢?大家参考下面的代码
private static class InflateRequest {
AsyncLayoutInflater inflater;
ViewGroup parent;
int resid;
View view;
OnInflateFinishedListener callback;
InflateRequest() {
}
}
从上面的代码不难发现,InflateRequest 其实就是一个类,这个类代表了一个xml解析的请求所需要的基本信息。
然后,接下来对 InflateRequest 变量赋值以后会将其加到 InflateThread 中的一个队列中等待执行。
public void enqueue(InflateRequest request) {
try {
mQueue.put(request);
} catch (InterruptedException e) {
throw new RuntimeException(
"Failed to enqueue async inflate request", e);
}
}
InflateThread
private static class InflateThread extends Thread {
private static final InflateThread sInstance;
static {
// 静态代码块,确保只会建立一次,而且建立即start。
sInstance = new InflateThread();
sInstance.start();
}
// 单例,避免重复创建线程带来的开销
public static InflateThread getInstance() {
return sInstance;
}
// 阻塞队列(保存封装过得request)
private ArrayBlockingQueue<InflateRequest> mQueue = new ArrayBlockingQueue<>(10);
private SynchronizedPool<InflateRequest> mRequestPool = new SynchronizedPool<>(10);
public void runInner() {
InflateRequest request;
try {
request = mQueue.take(); // 虽然是死循环,但队列中没有数据会阻塞,不占用cpu
} catch (InterruptedException ex) {
// Odd, just continue
Log.w(TAG, ex);
return;
}
try {
//解析xml的位置在这
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();
}
@Override
public void run() {
// 异步加载布局并使用handler进行
while (true) {
runInner();
}
}
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);
}
public void enqueue(InflateRequest request) {
try {
mQueue.put(request);// 添加到缓存队列中
} catch (InterruptedException e) {
throw new RuntimeException(
"Failed to enqueue async inflate request", e);
}
}
}
总结:
SyncLayoutInflater使用的是ArrayBlockingQueue来实现此模型,所以如果连续大量的调用AsyncLayoutInflater创建布局,可能会造成缓冲区阻塞。
enqueue 函数:只是插入元素到 mQueue 队列中,若是元素过多那么是有排队机制的;
runInner 函数:运行于循环中,从 mQueue 队列取出元素,调用 inflate 方法返回主线程;
SyncLayoutInflater 就是用于异步加载layout的,可以用于懒加载中,或者是在用户想提升onCreate函数的执行效率的时候。但是,它有几个缺陷:
1)异步转换出来的 View 并没有被加到 parent view中,AsyncLayoutInflater 是调用了 LayoutInflater.inflate(int, ViewGroup, false),因此如果需要加到 parent view 中,就需要我们自己手动添加;
2)不支持加载包含 Fragment 的 layout。
有需要面试题的朋友可以关注一下哇哇,以上都可以分享!!!