反思 系列博客是个人一种新学习方式的尝试,该系列起源和目录请参考 这里 。java
Android
体系自己很是宏大,源码中值得思考和借鉴之处众多。以LayoutInflater
自己为例,其整个流程中除了调用inflate()
函数 填充布局 功能以外,还涉及到了 应用启动、调用系统服务(进程间通讯)、对应组件做用域内单例管理、额外功能扩展 等等一系列复杂的逻辑。android
本文笔者将针对LayoutInlater
的整个设计思路进行描述,其总体结构以下图:git
顾名思义,LayoutInflater
的做用就是 布局填充器 ,其行为本质是调用了Android
自己提供的 系统服务。而在Android
系统的设计中,获取系统服务的实现方式就是经过ServiceManager
来取得和对应服务交互的IBinder
对象,而后建立对应系统服务的代理。github
Android
应用层将系统服务注册相关的API
放在了SystemServiceRegistry
类中,而将注册服务行为的代码放在了ContextImpl
类中,ContextImpl
类实现了Context
类下的全部抽象方法。性能优化
Android
应用层还定义了一个Context
的另一个子类:ContextWrapper
,Activity
、Service
等组件继承了ContextWrapper
, 每一个ContextWrapper
的实例有且仅对应一个ContextImpl
,造成一一对应的关系,该类是 装饰器模式 的体现:保证了Context
类公共功能代码和不一样功能代码的隔离。markdown
此外,虽然ContextImpl
类做为Context
类公共API
的实现者,LayoutInlater
的获取则交给了ContextThemeWrapper
类,该类中将LayoutInlater
的获取交给了一个成员变量,保证了单个组件 做用域内的单例。app
开发者但愿直接调用LayoutInflater#inflate()
函数对布局进行填充,该函数做用是对xml
文件中标签的解析,并根据参数决定是否直接将新建立的View
配置在指定的ViewGroup
中。ide
通常来讲,一个View
的实例化依赖Context
上下文对象和attr
的属性集,而设计者正是经过将上下文对象和属性集做为参数,经过 反射 注入到View
的构造器中对View
进行建立。函数
除此以外,考虑到 性能优化 和 可扩展性,设计者为LayoutInflater
设计了一个LayoutInflater.Factory2
接口,该接口设计得很是巧妙:在xml
解析过程当中,开发者能够经过配置该接口对View
的建立过程进行拦截:经过new的方式建立控件以免大量地使用反射,亦或者 额外配置特殊标签的解析逻辑以建立特殊组件(好比Fragment
)。oop
LayoutInflater.Factory2
接口在Android SDK
中的应用很是广泛,AppCompatActivity
和FragmentManager
就是最有力的体现,LayoutInflater.inflate()
方法的理解虽然重要,但笔者窃觉得LayoutInflater.Factory2
的重要性与其相比不逞多让。
对于LayoutInflater
总体不甚熟悉的开发者而言,本小节文字描述彷佛晦涩难懂,且不免有是否过分设计的疑惑,但这些文字的本质倒是布局填充流程总体的设计思想,读者不该该将本文视为源码分析,而应该将本身代入到设计的过程当中 。
上文提到,LayoutInflater
做为系统服务之一,获取方式是经过ServiceManager
来取得和对应服务交互的IBinder
对象,而后建立对应系统服务的代理。
Binder
机制相关并不是本文的重点,读者能够注意到,Android
的设计者将获取系统服务的接口交给了Context
类,意味着开发者能够经过任意一个Context
的实现类获取系统服务,包括不限于Activity
、Service
、Application
等等:
public abstract class Context { // 获取系统服务 public abstract Object getSystemService(String name); // ...... } 复制代码
读者须要理解,Context
类地职责并不是只针对 系统服务 进行提供,还包括诸如 启动其它组件、获取SharedPerferences 等等,其中大部分功能对于Context
的子类而言都是公共的,所以没有必要每一个子类都对其进行实现。
Android
设计者并无直接经过继承的方式将公共业务逻辑放入Base
类供组件调用或者重写,而是借鉴了 装饰器模式 的思想:分别定义了ContextImpl
和ContextWrapper
两个子类:
Context
的公共API
的实现都交给了ContextImpl
,以获取系统服务为例,Android
应用层将系统服务注册相关的API
放在了SystemServiceRegistry
类中,而ContextImpl
则是SystemServiceRegistry#getSystemService
的惟一调用者:
class ContextImpl extends Context { // 该成员即开发者使用的`Activity`等外部组件 private Context mOuterContext; @Override public Object getSystemService(String name) { return SystemServiceRegistry.getSystemService(this, name); } } 复制代码
这种设计使得 系统服务的注册(SystemServiceRegistry
类) 和 系统服务的获取(ContextImpl
类) 在代码中只有一处声明和调用,大幅下降了模块之间的耦合。
ContextWrapper
则是Context
的装饰器,当组件须要获取系统服务时交给ContextImpl
成员处理,伪代码实现以下:
// class Activity extends ContextWrapper class ContextWrapper extends Context { // 1.将 ContextImpl 做为成员进行存储 public ContextWrapper(ContextImpl base) { mBase = base; } ContextImpl mBase; // 2.系统服务的获取统一交给了ContextImpl @Override public Object getSystemService(String name) { return mBase.getSystemService(name); } } 复制代码
ContextWrapper
装饰器的初始化如何实现呢?每当一个ContextWrapper
组件(如Activity
)被建立时,都为其建立一个对应的ContextImpl
实例,伪代码实现以下:
public final class ActivityThread { // 每当`Activity`被建立 private Activity performLaunchActivity() { // .... // 1.实例化 ContextImpl ContextImpl appContext = new ContextImpl(); // 2.将 activity 注入 ContextImpl appContext.setOuterContext(activity); // 3.将 ContextImpl 也注入到 activity中 activity.attach(appContext, ....); // .... } } 复制代码
读者应该注意到了第3步的
activity.attach(appContext, ...)
函数,该函数很重要,在【布局流程】一节中会继续引伸。
读者也许注意到,对于单个Activity
而言,屡次调用activity.getLayoutInflater()
或者LayoutInflater.from(activity)
,获取到的LayoutInflater
对象都是单例的——对于涉及到了跨进程通讯的系统服务而言,经过做用域内的单例模式保证以节省性能是彻底能够理解的。
设计者将对应的代码放在了ContextWrapper
的子类ContextThemeWrapper
中,该类用于方便开发者为Activity
配置自定义的主题,除此以外还经过一个成员持有了一个LayoutInflater
对象:
// class Activity extends ContextThemeWrapper public class ContextThemeWrapper extends ContextWrapper { private Resources.Theme mTheme; private LayoutInflater mInflater; @Override public Object getSystemService(String name) { // 保证 LayoutInflater 的局部单例 if (LAYOUT_INFLATER_SERVICE.equals(name)) { if (mInflater == null) { mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this); } return mInflater; } return getBaseContext().getSystemService(name); } } 复制代码
而不管activity.getLayoutInflater()
仍是LayoutInflater.from(activity)
,其内部最终都执行的是ContextThemeWrapper#getSystemService
(前者和PhoneWindow
还有点关系,这个后文会提), 所以获取到的LayoutInflater
天然是同一个对象了:
public abstract class LayoutInflater { public static LayoutInflater from(Context context) { return (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } } 复制代码
上一节咱们提到了Activity
启动的过程,这个过程当中不可避免的要建立一个窗口,最终UI的布局都要展现在这个窗口上,Android
中经过定义了PhoneWindow
类对这个UI的窗口进行描述。
Activity
将布局填充相关的逻辑委托给了PhoneWindow
,Activity
的setContentView()
函数,其本质是调用了PhoneWindow
的setContentView()
函数。
public class PhoneWindow extends Window { public PhoneWindow(Context context) { super(context); mLayoutInflater = LayoutInflater.from(context); } // Activity.setContentView 其实是调用了 PhoneWindow.setContentView() @Override public void setContentView(int layoutResID) { // ... mLayoutInflater.inflate(layoutResID, mContentParent); } } 复制代码
读者须要清楚,activity.getLayoutInflater()
和activity.setContentView()
等方法都使用到了PhoneWindow
内部的LayoutInflater
对象,而PhoneWindow
内部对LayoutInflater
的实例化,仍然是调用context.getSystemService()
方法,所以和上一小节的结论并不冲突:
而不管
activity.getLayoutInflater()
仍是LayoutInflater.from(activity)
,其内部最终都执行的是ContextThemeWrapper#getSystemService
。
PhoneWindow
是如何实例化的呢,读者认真思考可知,一个Activity
对应一个PhoneWindow
的UI窗口,所以当Activity
被建立时,PhoneWindow
就被须要被建立了,执行时机就在上文的ActivityThread.performLaunchActivity()
中:
public final class ActivityThread { // 每当`Activity`被建立 private Activity performLaunchActivity() { // .... // 3.将 ContextImpl 也注入到 activity中 activity.attach(appContext, ....); // .... } } public class Activity extends ContextThemeWrapper { final void attach(Context context, ...) { // ... // 初始化 PhoneWindow // window构造方法中又经过 Context 实例化了 LayoutInflater PhoneWindow mWindow = new PhoneWindow(this, ....); } } 复制代码
设计到这里,读者应该对LayoutInflater
的总体流程已经有了一个初步的掌握,须要清楚的两点是:
LayoutInflater
,都是经过ContextImpl.getSystemService()
获取的,而且在Activity
等组件的生命周期内保持单例;Activity.setContentView()
函数,本质上也仍是经过LayoutInflater.inflate()
函数对布局进行解析和建立。从思想上来看,LayoutInflater.inflate()
函数内部实现比较简单直观:
public View inflate(@LayoutRes int resource, ViewGroup root, boolean attachToRoot) { // ... } 复制代码
对该函数的参数进行简单概括以下:第一个参数表明所要加载的布局,第二个参数是ViewGroup
,这个参数须要与第3个参数配合使用,attachToRoot
若是为true
就把布局添加到ViewGroup
中;若为false
则只采用ViewGroup
的LayoutParams
做为测量的依据却不直接添加到ViewGroup
中。
从设计的角度上思考,该函数的设计过程当中,为何须要定义这样的三个参数?为何这样三个参数就能涵盖咱们平常开发过程当中布局填充的需求?
对于第一个资源id参数而言,UI的建立必然依赖了布局文件资源的引用,所以这个参数无可厚非。
咱们先略过第二个参数,直接思考第三个参数,为何须要这样一个boolean
类型的值,以决定是否将建立的View
直接添加到指定的ViewGroup
中呢,不设计这个参数是否能够?
换个角度思考,这个问题的本质实际上是:是否每一个View
的建立都必须当即添加在ViewGroup
中?答案固然是否认的,为了保证性能,设计者不可能让全部的View
被建立后都可以当即被当即添加在ViewGroup
中,这与目前Android
中不少组件的设计都有冲突,好比ViewStub
、RecyclerView
的条目、Fragment
等等。
所以,更好的方式应该是能够经过一个boolean
的开关将整个过程切分红2个小步骤,当View
生成并根据ViewGroup
的布局参数生成了对应的测量依据后,开发者能够根据需求手动灵活配置是否当即添加到ViewGroup
中——这就是第三个参数的由来。
那么ViewGroup
类型的第二个参数为何能够为空呢?实际开发过程当中,彷佛并无什么场景在填充布局时须要使ViewGroup
为空?
读者仔细思考能够很容易得出结论,事实上该参数可空是有必要的——对于Activity
UI的建立而言,根结点最顶层的ViewGroup
必然是没有父控件的,这时在布局的建立时,就必须经过将null
做为第二个参数交给LayoutInlater
的inflate()
方法,当View
被建立好后,将View
的布局参数配置为对应屏幕的宽高:
// DecorView.onResourcesLoaded()函数 void onResourcesLoaded(LayoutInflater inflater, int layoutResource) { // ... // 建立最顶层的布局时,须要指定父布局为null final View root = inflater.inflate(layoutResource, null); // 而后将宽高的布局参数都指定为 MATCH_PARENT(屏幕的宽高) mDecorCaptionView.addView(root, new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT)); } 复制代码
如今咱们理解了 为何三个参数就能涵盖开发过程当中布局填充的需求,接下来继续思考下一个问题,LayoutInflater
是如何解析xml
的。
xml
解析过程的思路很简单;
XmlPullParser
解析器对象;View
的解析而言,一个View
的实例化依赖Context
上下文对象和attr
的属性集,而设计者正是经过将上下文对象和属性集做为参数,经过 反射 注入到View
的构造器中对单个View
进行建立;xml
文件的解析而言,整个流程依然经过典型的递归思想,对布局文件中的xml
文件进行遍历解析,自底至顶对View
依次进行建立,最终完成了整个View
树的建立。单个View
的实例化实现以下,这里采用伪代码的方式实现:
// LayoutInflater类 public final View createView(String name, String prefix, AttributeSet attrs) { // ... // 1.根据View的全名称路径,获取View的Class对象 Class<? extends View> clazz = mContext.getClassLoader().loadClass(name + prefix).asSubclass(View.class); // 2.获取对应View的构造器 Constructor<? extends View> constructor = clazz.getConstructor(mConstructorSignature); // 3.根据构造器,经过反射生成对应 View args[0] = mContext; args[1] = attrs; final View view = constructor.newInstance(args); return view; } 复制代码
对于总体解析流程而言,伪代码实现以下:
void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs) { // 1.解析当前控件 while (parser.next()!= XmlPullParser.END_TAG) { final View view = createViewFromTag(parent, name, context, attrs); final ViewGroup viewGroup = (ViewGroup) parent; final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); // 2.解析子布局 rInflateChildren(parser, view, attrs, true); // 全部子布局解析结束,将当前控件及布局参数添加到父布局中 viewGroup.addView(view, params); } } final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs, boolean finishInflate){ // 3.子布局做为根布局,经过递归的方式,层级向下一层层解析 // 继续执行 1 rInflate(parser, parent, parent.getContext(), attrs, finishInflate); } 复制代码
至此,通常状况下的布局填充流程到此结束,inflate()
方法执行完毕,对应的布局文件解析结束,并根据参数配置决定是否直接添加在ViewGroup
根布局中。
LayoutInlater
的设计流程到此就结束了吗,固然不是,更巧妙的设计还还没有登场。
读者须要清楚的是,到目前为止,咱们的设计还遗留了2个明显的缺陷:
View
的实例化都依赖了Java
的反射机制,这意味着额外性能的损耗;xml
布局中声明了fragment
标签,会致使模块之间极高的耦合。什么叫作 fragment标签会致使模块之间极高的耦合 ?举例来讲,开发者在layout
文件中声明这样一个Fragment
:
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <!-- 声明一个fragment --> <fragment android:id="@+id/fragment" android:name="com.github.qingmei2.myapplication.AFragment" android:layout_width="match_parent" android:layout_height="match_parent"/> </android.support.constraint.ConstraintLayout> 复制代码
看起来彷佛没有什么问题,但读者认真思考会发现,若是这是一个v4包的Fragment
,是否意味着LayoutInflater
额外增长了对Fragment
类的依赖,相似这样:
// LayoutInflater类 void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs) { // 1.解析当前控件 while (parser.next()!= XmlPullParser.END_TAG) { //【注意】2.若是标签是一个Fragment,反射生成Fragment并返回 if (name == "fragment") { Fragment fragment = clazz.newInstance(); // .....还会关联到SupportFragmentManager、FragmentTransaction的依赖! supportFragmentManager.beginTransaction().add(....).commit(); return; } final View view = createViewFromTag(parent, name, context, attrs); final ViewGroup viewGroup = (ViewGroup) parent; final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); // 3.解析子布局 rInflateChildren(parser, view, attrs, true); // 全部子布局解析结束,将当前控件及布局参数添加到父布局中 viewGroup.addView(view, params); } } 复制代码
这致使了LayoutInflater
在解析fragment
标签过程当中,强制依赖了不少设计者不但愿的依赖(好比v4包下Fragment
相关类),继续往下思考的话,还会遇到更多的问题,这里再也不引伸。
那么如何解决这样的两个问题呢?
考虑到 性能优化 和 可扩展性,设计者为LayoutInflater
设计了一个LayoutInflater.Factory
接口,该接口设计得很是巧妙:在xml
解析过程当中,开发者能够经过配置该接口对View
的建立过程进行拦截:经过new的方式建立控件以免大量地使用反射,亦或者 额外配置特殊标签的解析逻辑以建立特殊组件 :
public abstract class LayoutInflater { private Factory mFactory; private Factory2 mFactory2; private Factory2 mPrivateFactory; public void setFactory(Factory factory) { //... } public void setFactory2(Factory2 factory) { // Factory 只能被set一次 if (mFactorySet) { throw new IllegalStateException("A factory has already been set on this LayoutInflater"); } mFactorySet = true; mFactory = mFactory2 = factory; // ... } public interface Factory { public View onCreateView(String name, Context context, AttributeSet attrs); } public interface Factory2 extends Factory { public View onCreateView(View parent, String name, Context context, AttributeSet attrs); } } 复制代码
正如上文所说的,Factory
接口的意义是在xml
解析过程当中,开发者能够经过配置该接口对View
的建立过程进行拦截,对于View
的实例化,最终实现的伪代码以下:
View createViewFromTag() { View view; // 1. 若是mFactory2不为空, 用mFactory2 拦截建立 View if (mFactory2 != null) { view = mFactory2.onCreateView(parent, name, context, attrs); // 2. 若是mFactory不为空, 用mFactory 拦截建立 View } else if (mFactory != null) { view = mFactory.onCreateView(name, context, attrs); } else { view = null; } // 3. 若是通过拦截机制以后,view仍然是null,再经过系统反射的方式,对View进行实例化 if (view == null) { view = createView(name, null, attrs); } } 复制代码
理解了LayoutInflater.Factory
接口设计的思路,接下来一块儿来思考如何解决上文中提到的2个问题。
AppCompatActivity
的源码中隐晦地配置LayoutInflater.Factory
减小了大量反射建立控件的状况——设计者的思路是,在AppCompatActivity
的onCreate()
方法中,为LayoutInflater
对象调用了setFactory2()
方法:
// AppCompatActivity类 @Override protected void onCreate(@Nullable Bundle savedInstanceState) { getDelegate().installViewFactory(); //... } // AppCompatDelegateImpl类 @Override public void installViewFactory() { LayoutInflater layoutInflater = LayoutInflater.from(mContext); if (layoutInflater.getFactory() == null) { LayoutInflaterCompat.setFactory2(layoutInflater, this); } } 复制代码
配置以后,在inflate()
过程当中,系统的基础控件的实例化都经过代码拦截,并经过new
的方式进行返回:
switch (name) { case "TextView": view = new AppCompatTextView(context, attrs); break; case "ImageView": view = new AppCompatImageView(context, attrs); break; case "Button": view = new AppCompatButton(context, attrs); break; case "EditText": view = new AppCompatEditText(context, attrs); break; // ... // Android 基础组件都经过new方式进行建立 } 复制代码
源码也说明了,即便开发者在xml
文件中配置的是Button
,setContentView()
以后,生成的控件实际上是AppCompatButton
, TextView
或者ImageView
亦然,在避免额外的性能损失的同时,也保证了Android
版本的向下兼容。
为何Fragment
没有定义相似void setContentView(R.layout.xxx)
的函数对布局进行填充,而是使用了View onCreateView()
这样的函数,让开发者填充并返回一个对应的View
呢?
缘由就在于在布局填充的过程当中,Fragment
最终被视为一个子控件并添加到了ViewGroup
中,设计者将FragmentManagerImpl
做为FragmentManager
的实现类,同时实现了LayoutInflater.Factory2
接口。
而在布局文件中fragment
标签解析的过程当中,其实是调用了FragmentManagerImpl.onCreateView()
函数,生成了Fragment
以后并将View
返回,跳过了系统反射生成View
相关的逻辑:
# android.support.v4.app.FragmentManager$FragmentManagerImpl
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (!"fragment".equals(name)) {
return null;
}
// 若是标签是`fragment`,生成Fragment,并返回Fragment的Root
return fragment.mView;
}
复制代码
经过定义LayoutInflater.Factory
接口,设计者将Fragment
的功能抽象为一个View
(虽然Fragment
并非一个View
),并交给FragmentManagerImpl
进行处理,减小了模块之间的耦合,能够说是很是优秀的设计。
实际上
LayoutInflater.Factory
接口的设计还有更多细节(好比LayoutInflater.FactoryMerger
类),篇幅缘由,本文不赘述,有兴趣的读者能够研究一下。
LayoutInflater
总体的设计很是复杂且巧妙,从应用启动到进程间通讯,从组件的启动再到组件UI的渲染,均可以看到LayoutInflater
的身影,所以很是值得认真学习一番,建议读者参考本文开篇的思惟导图并结合Android
源码进行总体小结。
Hello,我是 却把清梅嗅 ,若是您以为文章对您有价值,欢迎 ❤️,也欢迎关注个人 博客 或者 Github。
若是您以为文章还差了那么点东西,也请经过关注督促我写出更好的文章——万一哪天我进步了呢?