Android布局优化(一)LayoutInflate — 从布局加载原理说起
系列文章
- Android布局优化(一)LayoutInflate — 从布局加载原理说起
- Android布局优化(二)优雅获取界面布局耗时
- Android布局优化(三)使用AsyncLayoutInflater异步加载布局
- Android布局优化(四)X2C — 提升布局加载速度200%
- Android布局优化(五)绘制优化—避免过度绘制
目录
前言
最近打算写一些Android布局优化相关的文章,既然要进行布局优化,就要先了解布局加载原理,才能知道有哪些地方可以作为优化的切入点。开发同学做任何事情最好都能够知其所以然
布局加载源码分析
这里主要为了分析布局加载相关的原理,所以省略了一些逻辑。想了解View绘制流程,可以看最全的View绘制流程(上)— Window、DecorView、ViewRootImp的关系
我们先从Activity.setContentView
开始分析布局是如何被加载的
Activity.setContentView(@LayoutRes int layoutResID)
public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar();}
PhoneWIndow.setContentView(int layoutResID)
@Overridepublic void setContentView(int layoutResID) { if (mContentParent == null) { //初始化DecorView和mContentParent installDecor(); } ... //加载资源文件,创建view树装载到mContentParent mLayoutInflater.inflate(layoutResID, mContentParent); ...}
LayoutInflate.inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { final Resources res = getContext().getResources(); //1.加载解析xml文件 final XmlResourceParser parser = res.getLayout(resource); try { //2.填充View树 return inflate(parser, root, attachToRoot); } finally { parser.close(); }}
可以看出布局加载流程主要分为加载解析xml文件和填充View树两部分
加载解析xml文件
Resources.getLayout(@LayoutRes int id)
public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException { return loadXmlResourceParser(id, "layout"); }
Resources.loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,@NonNul l String type)
XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,@NonNul l String type) throws NotFoundException { if (id != 0) { try { synchronized (mCachedXmlBlocks) { final int[] cachedXmlBlockCookies = mCachedXmlBlockCookies; final String[] cachedXmlBlockFiles = mCachedXmlBlockFiles; final XmlBlock[] cachedXmlBlocks = mCachedXmlBlocks; // First see if this block is in our cache. final int num = cachedXmlBlockFiles.length; for (int i = 0; i < num; i++) { if (cachedXmlBlockCookies[i] == assetCookie && cachedXmlBlockFiles[i] != null && cachedXmlBlockFiles[i].equals(file)) { return cachedXmlBlocks[i].newParser(); } } // Not in the cache, create a new block and put it at // the next slot in the cache. final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file); if (block != null) { final int pos = (mLastCachedXmlBlockIndex + 1) % num; mLastCachedXmlBlockIndex = pos; final XmlBlock oldBlock = cachedXmlBlocks[pos]; if (oldBlock != null) { oldBlock.close(); } cachedXmlBlockCookies[pos] = assetCookie; cachedXmlBlockFiles[pos] = file; cachedXmlBlocks[pos] = block; return block.newParser(); } } } catch (Exception e) { final NotFoundException rnf = new NotFoundException("File " + file + " from xml type " + type + " resource ID #0x" + Integer.toHexString(id)); rnf.initCause(e); throw rnf; } } throw new NotFoundException("File " + file + " from xml type " + type + " resource ID #0x" + Integer.toHexString(id)); }
我们不用非常深入这个方法的具体实现细节,我们只需要知道,这个方法的作用就是将我们写的xml文件读取到内存中,并进行一些数据解析和封装。所以这个方法本质上就是一个IO操作,我们知道,IO操作往往是比较耗费性能的
填充View树
LayoutInflate.inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { final Context inflaterContext = mContext; final AttributeSet attrs = Xml.asAttributeSet(parser); Context lastContext = (Context) mConstructorArgs[0]; mConstructorArgs[0] = inflaterContext; View result = root; int type; final String name = parser.getName(); if (TAG_MERGE.equals(name)) { if (root == null || !attachToRoot) { throw new InflateException(" can be used only with a valid " + "ViewGroup root and attachToRoot=true"); } rInflate(parser, root, inflaterContext, attrs, false); } else { // Temp is the root view that was found in the xml final View temp = createViewFromTag(root, name, inflaterContext, attrs); ViewGroup.LayoutParams params = null; if (root != null) { params = root.generateLayoutParams(attrs); if (!attachToRoot) { temp.setLayoutParams(params); } } rInflateChildren(parser, temp, attrs, true); if (root != null && attachToRoot) { root.addView(temp, params); } if (root == null || !attachToRoot) { result = temp; } } return result; }}
上面这个方法中我们最主要关注createViewFromTag(View parent, String name, Context context, AttributeSet attrs)
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { //解析view标签 if (name.equals("view")) { name = attrs.getAttributeValue(null, "class"); } //如果需要该标签与主题相关,需要对context进行包装,将主题信息加入context包装类ContextWrapper if (!ignoreThemeAttr) { final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME); final int themeResId = ta.getResourceId(0, 0); if (themeResId != 0) { context = new ContextThemeWrapper(context, themeResId); } ta.recycle(); } if (name.equals(TAG_1995)) { //BlinkLayout是一种闪烁的FrameLayout,它包裹的内容会一直闪烁,类似QQ提示消息那种。 return new BlinkLayout(context, attrs); } //设置Factory,来对View做额外的拓展,这块属于可定制的内容 View view; if (mFactory2 != null) { view = mFactory2.onCreateView(parent, name, context, attrs); } else if (mFactory != null) { view = mFactory.onCreateView(name, context, attrs); } else { view = null; } if (view == null && mPrivateFactory != null) { view = mPrivateFactory.onCreateView(parent, name, context, attrs); } //如果此时不存在Factory,不管Factory还是Factory2,还是mPrivateFactory都不存在, //那么会直接对name直接进行解析 if (view == null) { final Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = context; try { //如果name中包含"."即为自定义View,否则为原生的View控件 if (-1 == name.indexOf('.')) { view = onCreateView(parent, name, attrs); } else { view = createView(name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; } } return view;}
根据源码可以将createViewFromTag
分为三个流程:
对一些特殊标签,做分别处理,例如:
view
,TAG_1995(blink)
进行对
Factory
、Factory2
的设置判断,如果设置那么就会通过设置Factory
、Factory2
进行生成View
如果没有设置
Factory
或Factory2
,那么就会使用LayoutInflater
默认的生成方式,进行View的生成
createViewFromTag
过程分析:
- 处理
view
标签
如果标签的名称是view
,注意是小写的view
,这个标签一般大家不太常用
在使用时,相当于所有控件标签的父类一样,可以设置class
属性,这个属性会决定view
这个节点会变成什么控件
- 如果该节点与主题相关,则需要特殊处理
如果该节点与主题(Theme)相关,需要将context
与theme信息包装至ContextWrapper
类
- 处理TAG_1995标签
这就有意思了,TAG_1995指的是blink
这个标签,这个标签感觉使用的很少,以至于大家根本不知道。
这个标签最后会被解析成BlinkLayout
,BlinkLayout
其实就是一个FrameLayout
,这个控件最后会将包裹内容一直闪烁(就和电脑版QQ消息提示一样)
- 判断其是否存在Factory或者Factory2
在这里先对Factory
进行判空,这里不管Factory
还是Factory2
(mPrivateFactory
就是Factory2
),本质上都是一种扩展操作,提前解析name
,然后直接将解析后的View
返回
Factory
public interface Factory { public View onCreateView(String name, Context context, AttributeSet attrs);}
Factory2
public interface Factory2 extends Factory { public View onCreateView(View parent, String name, Context context, AttributeSet attrs);}
从这里可以看出,Factory2
和Factory
都是一个接口,需要自己实现,而Factory2
和Factory
的区别是Factory2
继承Factory
,从而扩展出一个参数,就是增加了该节点的父View。设置Factory
和Factory2
需要通过setFactory()
或者setFactory2()
来实现
setFactory()
public void setFactory(Factory factory) { //如果已经设置Factory,不可以继续设置Factory if (mFactorySet) { throw new IllegalStateException("A factory has already been set on this LayoutInflater"); } if (factory == null) { throw new NullPointerException("Given factory can not be null"); } //设置Factory会添加一个标记 mFactorySet = true; if (mFactory == null) { mFactory = factory; } else { mFactory = new FactoryMerger(factory, null, mFactory, mFactory2); }}
setFactory2()
public void setFactory2(Factory2 factory) { if (mFactorySet) { throw new IllegalStateException("A factory has already been set on this LayoutInflater"); } if (factory == null) { throw new NullPointerException("Given factory can not be null"); } //注意设置Factory和Factory2的标记是共用的 mFactorySet = true; if (mFactory == null) { mFactory = mFactory2 = factory; } else { mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2); }}
通过上面代码可以看出,Factory
和Factory2
只能够设置一次,并且Factory
和Factory2
二者互斥,只能存在一个。所以一般setFactory()
或者setFactory2()
,一般在cloneInContext()之
后设置,这样生成一个新的LayoutInflater,标记默认是false,才能够设置
- createView(String name, String prefix, AttributeSet attrs)
public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException { //判断构造器是否存在 Constructor<? extends View> constructor = sConstructorMap.get(name); if (constructor != null && !verifyClassLoader(constructor)) { constructor = null; sConstructorMap.remove(name); } Class<? extends View> clazz = null; try { //如果构造器不存在,这个就相当于Class之前是否被加载过,sConstructorMap就是缓存这些Class的Map if (constructor == null) { //通过前缀+name的方式去加载 clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); //通过过滤去设置一些不需要加载的对象 if (mFilter != null && clazz != null) { boolean allowed = mFilter.onLoadClass(clazz); if (!allowed) { failNotAllowed(name, prefix, attrs); } } constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); //缓存Class sConstructorMap.put(name, constructor); } else { //如果Class存在,并且加载Class的ClassLoader合法 //这里先判断该Class是否应该被过滤 if (mFilter != null) { //过滤器也有缓存之前的Class是否被允许加载,判断这个Class的过滤状态 Boolean allowedState = mFilterMap.get(name); if (allowedState == null) { //加载Class对象操作 clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); //判断Class是否可被加载 boolean allowed = clazz != null && mFilter.onLoadClass(clazz); mFilterMap.put(name, allowed); if (!allowed) { failNotAllowed(name, prefix, attrs); } } else if (allowedState.equals(Boolean.FALSE)) { failNotAllowed(name, prefix, attrs); } } } Object[] args = mConstructorArgs; args[1] = attrs; //如果过滤器不存在,直接实例化该View final View view = constructor.newInstance(args); //如果View属于ViewStub那么需要给ViewStub设置一个克隆过的LayoutInflater if (view instanceof ViewStub) { final ViewStub viewStub = (ViewStub) view; viewStub.setLayoutInflater(cloneInContext((Context) args[0])); } return view
从上面的代码可以看出,我们是通过反射的方式去创建View实例的
总结
经过对布局加载原理的分析,我们可以看出布局加载的主要性能瓶颈主要在两个方面
加载xml文件是一个IO过程,如果xml文件过大,就会比较耗时
View实例是通过反射进行创建的,通过反射创建对象相对会更耗费性能
更多相关文章
- 相对布局(RelativeLayout)
- Android 常用布局整理
- RelativeLayout布局用到的主要属性
- android 控件位置常用布局
- Android界面布局——视图/容器易混淆点总结
- Android View布局xml常用 属性详解
- android 相对布局属性说明
- android布局控件的用法
- android studio 加载.proto不能生成xxxDrpc的问题