We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
反思 系列博客是我的一种新学习方式的尝试,该系列起源和目录请参考 这里 。
Android体系本身非常宏大,源码中值得思考和借鉴之处众多。以LayoutInflater本身为例,其整个流程中除了调用inflate()函数 填充布局 功能之外,还涉及到了 应用启动、调用系统服务(进程间通信)、对应组件作用域内单例管理、额外功能扩展 等等一系列复杂的逻辑。
Android
LayoutInflater
inflate()
本文笔者将针对LayoutInlater的整个设计思路进行描述,其整体结构如下图:
LayoutInlater
顾名思义,LayoutInflater的作用就是 布局填充器 ,其行为本质是调用了Android本身提供的 系统服务。而在Android系统的设计中,获取系统服务的实现方式就是通过ServiceManager来取得和对应服务交互的IBinder对象,然后创建对应系统服务的代理。
ServiceManager
IBinder
Android应用层将系统服务注册相关的API放在了SystemServiceRegistry类中,而将注册服务行为的代码放在了ContextImpl类中,ContextImpl类实现了Context类下的所有抽象方法。
API
SystemServiceRegistry
ContextImpl
Context
Android应用层还定义了一个Context的另外一个子类:ContextWrapper,Activity、Service等组件继承了ContextWrapper, 每个ContextWrapper的实例有且仅对应一个ContextImpl,形成一一对应的关系,该类是 装饰器模式 的体现:保证了Context类公共功能代码和不同功能代码的隔离。
ContextWrapper
Activity
Service
此外,虽然ContextImpl类作为Context类公共API的实现者,LayoutInlater的获取则交给了ContextThemeWrapper类,该类中将LayoutInlater的获取交给了一个成员变量,保证了单个组件 作用域内的单例。
ContextThemeWrapper
开发者希望直接调用LayoutInflater#inflate()函数对布局进行填充,该函数作用是对xml文件中标签的解析,并根据参数决定是否直接将新创建的View配置在指定的ViewGroup中。
LayoutInflater#inflate()
xml
View
ViewGroup
一般来说,一个View的实例化依赖Context上下文对象和attr的属性集,而设计者正是通过将上下文对象和属性集作为参数,通过 反射 注入到View的构造器中对View进行创建。
attr
除此之外,考虑到 性能优化 和 可扩展性,设计者为LayoutInflater设计了一个LayoutInflater.Factory2接口,该接口设计得非常巧妙:在xml解析过程中,开发者可以通过配置该接口对View的创建过程进行拦截:通过new的方式创建控件以避免大量地使用反射,亦或者 额外配置特殊标签的解析逻辑以创建特殊组件(比如Fragment)。
LayoutInflater.Factory2
Fragment
LayoutInflater.Factory2接口在Android SDK中的应用非常普遍,AppCompatActivity和FragmentManager就是最有力的体现,LayoutInflater.inflate()方法的理解虽然重要,但笔者窃以为LayoutInflater.Factory2的重要性与其相比不逞多让。
Android SDK
AppCompatActivity
FragmentManager
LayoutInflater.inflate()
对于LayoutInflater整体不甚熟悉的开发者而言,本小节文字描述似乎晦涩难懂,且难免有是否过度设计的疑惑,但这些文字的本质却是布局填充流程整体的设计思想,读者不应该将本文视为源码分析,而应该将自己代入到设计的过程中 。
上文提到,LayoutInflater作为系统服务之一,获取方式是通过ServiceManager来取得和对应服务交互的IBinder对象,然后创建对应系统服务的代理。
Binder机制相关并非本文的重点,读者可以注意到,Android的设计者将获取系统服务的接口交给了Context类,意味着开发者可以通过任意一个Context的实现类获取系统服务,包括不限于Activity、Service、Application等等:
Binder
Application
public abstract class Context { // 获取系统服务 public abstract Object getSystemService(String name); // ...... }
读者需要理解,Context类地职责并非只针对 系统服务 进行提供,还包括诸如 启动其它组件、获取SharedPerferences 等等,其中大部分功能对于Context的子类而言都是公共的,因此没有必要每个子类都对其进行实现。
Android设计者并没有直接通过继承的方式将公共业务逻辑放入Base类供组件调用或者重写,而是借鉴了 装饰器模式 的思想:分别定义了ContextImpl和ContextWrapper两个子类:
Base
Context的公共API的实现都交给了ContextImpl,以获取系统服务为例,Android应用层将系统服务注册相关的API放在了SystemServiceRegistry类中,而ContextImpl则是SystemServiceRegistry#getSystemService的唯一调用者:
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.attach(appContext, ...)
读者也许注意到,对于单个Activity而言,多次调用activity.getLayoutInflater()或者LayoutInflater.from(activity),获取到的LayoutInflater对象都是单例的——对于涉及到了跨进程通信的系统服务而言,通过作用域内的单例模式保证以节省性能是完全可以理解的。
activity.getLayoutInflater()
LayoutInflater.from(activity)
设计者将对应的代码放在了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自然是同一个对象了:
ContextThemeWrapper#getSystemService
PhoneWindow
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()函数。
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.setContentView()
context.getSystemService()
而无论activity.getLayoutInflater()还是LayoutInflater.from(activity),其内部最终都执行的是ContextThemeWrapper#getSystemService。
PhoneWindow是如何实例化的呢,读者认真思考可知,一个Activity对应一个PhoneWindow的UI窗口,因此当Activity被创建时,PhoneWindow就被需要被创建了,执行时机就在上文的ActivityThread.performLaunchActivity()中:
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的整体流程已经有了一个初步的掌握,需要清楚的两点是:
ContextImpl.getSystemService()
Activity.setContentView()
从思想上来看,LayoutInflater.inflate()函数内部实现比较简单直观:
public View inflate(@LayoutRes int resource, ViewGroup root, boolean attachToRoot) { // ... }
对该函数的参数进行简单归纳如下:第一个参数代表所要加载的布局,第二个参数是ViewGroup,这个参数需要与第3个参数配合使用,attachToRoot如果为true就把布局添加到ViewGroup中;若为false则只采用ViewGroup的LayoutParams作为测量的依据却不直接添加到ViewGroup中。
attachToRoot
true
false
LayoutParams
从设计的角度上思考,该函数的设计过程中,为什么需要定义这样的三个参数?为什么这样三个参数就能涵盖我们日常开发过程中布局填充的需求?
对于第一个资源id参数而言,UI的创建必然依赖了布局文件资源的引用,因此这个参数无可厚非。
我们先略过第二个参数,直接思考第三个参数,为什么需要这样一个boolean类型的值,以决定是否将创建的View直接添加到指定的ViewGroup中呢,不设计这个参数是否可以?
boolean
换个角度思考,这个问题的本质其实是:是否每个View的创建都必须立即添加在ViewGroup中?答案当然是否定的,为了保证性能,设计者不可能让所有的View被创建后都能够立即被立即添加在ViewGroup中,这与目前Android中很多组件的设计都有冲突,比如ViewStub、RecyclerView的条目、Fragment等等。
ViewStub
RecyclerView
因此,更好的方式应该是可以通过一个boolean的开关将整个过程切分成2个小步骤,当View生成并根据ViewGroup的布局参数生成了对应的测量依据后,开发者可以根据需求手动灵活配置是否立即添加到ViewGroup中——这就是第三个参数的由来。
那么ViewGroup类型的第二个参数为什么可以为空呢?实际开发过程中,似乎并没有什么场景在填充布局时需要使ViewGroup为空?
读者仔细思考可以很容易得出结论,事实上该参数可空是有必要的——对于ActivityUI的创建而言,根结点最顶层的ViewGroup必然是没有父控件的,这时在布局的创建时,就必须通过将null作为第二个参数交给LayoutInlater的inflate()方法,当View被创建好后,将View的布局参数配置为对应屏幕的宽高:
null
// 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的实例化实现如下,这里采用伪代码的方式实现:
// 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个明显的缺陷:
Java
fragment
什么叫做 fragment标签会导致模块之间极高的耦合 ?举例来说,开发者在layout文件中声明这样一个Fragment:
layout
<?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的方式创建控件以避免大量地使用反射,亦或者 额外配置特殊标签的解析逻辑以创建特殊组件 :
LayoutInflater.Factory
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的实例化,最终实现的伪代码如下:
Factory
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()方法:
onCreate()
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的方式进行返回:
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版本的向下兼容。
Button
AppCompatButton
TextView
ImageView
为什么Fragment没有定义类似void setContentView(R.layout.xxx)的函数对布局进行填充,而是使用了View onCreateView()这样的函数,让开发者填充并返回一个对应的View呢?
void setContentView(R.layout.xxx)
View onCreateView()
原因就在于在布局填充的过程中,Fragment最终被视为一个子控件并添加到了ViewGroup中,设计者将FragmentManagerImpl作为FragmentManager的实现类,同时实现了LayoutInflater.Factory2接口。
FragmentManagerImpl
而在布局文件中fragment标签解析的过程中,实际上是调用了FragmentManagerImpl.onCreateView()函数,生成了Fragment之后并将View返回,跳过了系统反射生成View相关的逻辑:
FragmentManagerImpl.onCreateView()
# 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.FactoryMerger
LayoutInflater整体的设计非常复杂且巧妙,从应用启动到进程间通信,从组件的启动再到组件UI的渲染,都可以看到LayoutInflater的身影,因此非常值得认真学习一番,建议读者参考本文开篇的思维导图并结合Android源码进行整体小结。
Hello,我是 却把清梅嗅 ,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的 博客 或者 Github。
如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?
The text was updated successfully, but these errors were encountered:
No branches or pull requests
反思|Android LayoutInflater机制的设计与实现
概述
Android
体系本身非常宏大,源码中值得思考和借鉴之处众多。以LayoutInflater
本身为例,其整个流程中除了调用inflate()
函数 填充布局 功能之外,还涉及到了 应用启动、调用系统服务(进程间通信)、对应组件作用域内单例管理、额外功能扩展 等等一系列复杂的逻辑。本文笔者将针对
LayoutInlater
的整个设计思路进行描述,其整体结构如下图:整体思路
1、创建流程
顾名思义,
LayoutInflater
的作用就是 布局填充器 ,其行为本质是调用了Android
本身提供的 系统服务。而在Android
系统的设计中,获取系统服务的实现方式就是通过ServiceManager
来取得和对应服务交互的IBinder
对象,然后创建对应系统服务的代理。Android
应用层将系统服务注册相关的API
放在了SystemServiceRegistry
类中,而将注册服务行为的代码放在了ContextImpl
类中,ContextImpl
类实现了Context
类下的所有抽象方法。Android
应用层还定义了一个Context
的另外一个子类:ContextWrapper
,Activity
、Service
等组件继承了ContextWrapper
, 每个ContextWrapper
的实例有且仅对应一个ContextImpl
,形成一一对应的关系,该类是 装饰器模式 的体现:保证了Context
类公共功能代码和不同功能代码的隔离。此外,虽然
ContextImpl
类作为Context
类公共API
的实现者,LayoutInlater
的获取则交给了ContextThemeWrapper
类,该类中将LayoutInlater
的获取交给了一个成员变量,保证了单个组件 作用域内的单例。2、布局填充流程
开发者希望直接调用
LayoutInflater#inflate()
函数对布局进行填充,该函数作用是对xml
文件中标签的解析,并根据参数决定是否直接将新创建的View
配置在指定的ViewGroup
中。一般来说,一个
View
的实例化依赖Context
上下文对象和attr
的属性集,而设计者正是通过将上下文对象和属性集作为参数,通过 反射 注入到View
的构造器中对View
进行创建。除此之外,考虑到 性能优化 和 可扩展性,设计者为
LayoutInflater
设计了一个LayoutInflater.Factory2
接口,该接口设计得非常巧妙:在xml
解析过程中,开发者可以通过配置该接口对View
的创建过程进行拦截:通过new的方式创建控件以避免大量地使用反射,亦或者 额外配置特殊标签的解析逻辑以创建特殊组件(比如Fragment
)。对于
LayoutInflater
整体不甚熟悉的开发者而言,本小节文字描述似乎晦涩难懂,且难免有是否过度设计的疑惑,但这些文字的本质却是布局填充流程整体的设计思想,读者不应该将本文视为源码分析,而应该将自己代入到设计的过程中 。创建流程
1.Context:系统服务的提供者
上文提到,
LayoutInflater
作为系统服务之一,获取方式是通过ServiceManager
来取得和对应服务交互的IBinder
对象,然后创建对应系统服务的代理。Binder
机制相关并非本文的重点,读者可以注意到,Android
的设计者将获取系统服务的接口交给了Context
类,意味着开发者可以通过任意一个Context
的实现类获取系统服务,包括不限于Activity
、Service
、Application
等等:读者需要理解,
Context
类地职责并非只针对 系统服务 进行提供,还包括诸如 启动其它组件、获取SharedPerferences 等等,其中大部分功能对于Context
的子类而言都是公共的,因此没有必要每个子类都对其进行实现。Android
设计者并没有直接通过继承的方式将公共业务逻辑放入Base
类供组件调用或者重写,而是借鉴了 装饰器模式 的思想:分别定义了ContextImpl
和ContextWrapper
两个子类:2.ContextImpl:Context的公共API实现
Context
的公共API
的实现都交给了ContextImpl
,以获取系统服务为例,Android
应用层将系统服务注册相关的API
放在了SystemServiceRegistry
类中,而ContextImpl
则是SystemServiceRegistry#getSystemService
的唯一调用者:这种设计使得 系统服务的注册(
SystemServiceRegistry
类) 和 系统服务的获取(ContextImpl
类) 在代码中只有一处声明和调用,大幅降低了模块之间的耦合。3.ContextWrapper:Context的装饰器
ContextWrapper
则是Context
的装饰器,当组件需要获取系统服务时交给ContextImpl
成员处理,伪代码实现如下:ContextWrapper
装饰器的初始化如何实现呢?每当一个ContextWrapper
组件(如Activity
)被创建时,都为其创建一个对应的ContextImpl
实例,伪代码实现如下:4.组件的局部单例
读者也许注意到,对于单个
Activity
而言,多次调用activity.getLayoutInflater()
或者LayoutInflater.from(activity)
,获取到的LayoutInflater
对象都是单例的——对于涉及到了跨进程通信的系统服务而言,通过作用域内的单例模式保证以节省性能是完全可以理解的。设计者将对应的代码放在了
ContextWrapper
的子类ContextThemeWrapper
中,该类用于方便开发者为Activity
配置自定义的主题,除此之外还通过一个成员持有了一个LayoutInflater
对象:而无论
activity.getLayoutInflater()
还是LayoutInflater.from(activity)
,其内部最终都执行的是ContextThemeWrapper#getSystemService
(前者和PhoneWindow
还有点关系,这个后文会提), 因此获取到的LayoutInflater
自然是同一个对象了:布局填充流程
上一节我们提到了
Activity
启动的过程,这个过程中不可避免的要创建一个窗口,最终UI的布局都要展示在这个窗口上,Android
中通过定义了PhoneWindow
类对这个UI的窗口进行描述。1.PhoneWindow:setContentView()的真正实现
Activity
将布局填充相关的逻辑委托给了PhoneWindow
,Activity
的setContentView()
函数,其本质是调用了PhoneWindow
的setContentView()
函数。读者需要清楚,
activity.getLayoutInflater()
和activity.setContentView()
等方法都使用到了PhoneWindow
内部的LayoutInflater
对象,而PhoneWindow
内部对LayoutInflater
的实例化,仍然是调用context.getSystemService()
方法,因此和上一小节的结论并不冲突:PhoneWindow
是如何实例化的呢,读者认真思考可知,一个Activity
对应一个PhoneWindow
的UI窗口,因此当Activity
被创建时,PhoneWindow
就被需要被创建了,执行时机就在上文的ActivityThread.performLaunchActivity()
中:设计到这里,读者应该对
LayoutInflater
的整体流程已经有了一个初步的掌握,需要清楚的两点是:LayoutInflater
,都是通过ContextImpl.getSystemService()
获取的,并且在Activity
等组件的生命周期内保持单例;Activity.setContentView()
函数,本质上也还是通过LayoutInflater.inflate()
函数对布局进行解析和创建。2.inflate()流程的设计和实现
从思想上来看,
LayoutInflater.inflate()
函数内部实现比较简单直观:对该函数的参数进行简单归纳如下:第一个参数代表所要加载的布局,第二个参数是
ViewGroup
,这个参数需要与第3个参数配合使用,attachToRoot
如果为true
就把布局添加到ViewGroup
中;若为false
则只采用ViewGroup
的LayoutParams
作为测量的依据却不直接添加到ViewGroup
中。从设计的角度上思考,该函数的设计过程中,为什么需要定义这样的三个参数?为什么这样三个参数就能涵盖我们日常开发过程中布局填充的需求?
2.1 三个火枪手
对于第一个资源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
的布局参数配置为对应屏幕的宽高:现在我们理解了 为什么三个参数就能涵盖开发过程中布局填充的需求,接下来继续思考下一个问题,
LayoutInflater
是如何解析xml
的。2.2 xml解析流程
xml
解析过程的思路很简单;XmlPullParser
解析器对象;View
的解析而言,一个View
的实例化依赖Context
上下文对象和attr
的属性集,而设计者正是通过将上下文对象和属性集作为参数,通过 反射 注入到View
的构造器中对单个View
进行创建;xml
文件的解析而言,整个流程依然通过典型的递归思想,对布局文件中的xml
文件进行遍历解析,自底至顶对View
依次进行创建,最终完成了整个View
树的创建。单个
View
的实例化实现如下,这里采用伪代码的方式实现:对于整体解析流程而言,伪代码实现如下:
至此,一般情况下的布局填充流程到此结束,
inflate()
方法执行完毕,对应的布局文件解析结束,并根据参数配置决定是否直接添加在ViewGroup
根布局中。LayoutInlater
的设计流程到此就结束了吗,当然不是,更精彩更巧妙的设计还尚未登场。拦截机制和解耦策略
抛出问题
读者需要清楚的是,到目前为止,我们的设计还遗留了2个明显的缺陷:
View
的实例化都依赖了Java
的反射机制,这意味着额外性能的损耗;xml
布局中声明了fragment
标签,会导致模块之间极高的耦合。什么叫做 fragment标签会导致模块之间极高的耦合 ?举例来说,开发者在
layout
文件中声明这样一个Fragment
:看起来似乎没有什么问题,但读者认真思考会发现,如果这是一个v4包的
Fragment
,是否意味着LayoutInflater
额外增加了对Fragment
类的依赖,类似这样:这导致了
LayoutInflater
在解析fragment
标签过程中,强制依赖了很多设计者不希望的依赖(比如v4包下Fragment
相关类),继续往下思考的话,还会遇到更多的问题,这里不再引申。那么如何解决这样的两个问题呢?
解决思路
考虑到 性能优化 和 可扩展性,设计者为
LayoutInflater
设计了一个LayoutInflater.Factory
接口,该接口设计得非常巧妙:在xml
解析过程中,开发者可以通过配置该接口对View
的创建过程进行拦截:通过new的方式创建控件以避免大量地使用反射,亦或者 额外配置特殊标签的解析逻辑以创建特殊组件 :正如上文所说的,
Factory
接口的意义是在xml
解析过程中,开发者可以通过配置该接口对View
的创建过程进行拦截,对于View
的实例化,最终实现的伪代码如下:理解了
LayoutInflater.Factory
接口设计的思路,接下来一起来思考如何解决上文中提到的2个问题。减少反射次数
AppCompatActivity
的源码中隐晦地配置LayoutInflater.Factory
减少了大量反射创建控件的情况——设计者的思路是,在AppCompatActivity
的onCreate()
方法中,为LayoutInflater
对象调用了setFactory2()
方法:配置之后,在
inflate()
过程中,系统的基础控件的实例化都通过代码拦截,并通过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
相关的逻辑:通过定义
LayoutInflater.Factory
接口,设计者将Fragment
的功能抽象为一个View
(虽然Fragment
并不是一个View
),并交给FragmentManagerImpl
进行处理,减少了模块之间的耦合,可以说是非常优秀的设计。小结
LayoutInflater
整体的设计非常复杂且巧妙,从应用启动到进程间通信,从组件的启动再到组件UI的渲染,都可以看到LayoutInflater
的身影,因此非常值得认真学习一番,建议读者参考本文开篇的思维导图并结合Android
源码进行整体小结。参考
关于我
Hello,我是 却把清梅嗅 ,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的 博客 或者 Github。
如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?
The text was updated successfully, but these errors were encountered: