区块链技术博客
www.b2bchain.cn

反思|Android LayoutInflater机制的设计与实现求职学习资料

本文介绍了反思|Android LayoutInflater机制的设计与实现求职学习资料,有助于帮助完成毕业设计以及求职,是一篇很好的资料。

对技术面试,学习经验等有一些体会,在此分享。

概述

Android体系本身非常宏大,源码中值得思考和借鉴之处众多。以LayoutInflater本身为例,其整个流程中除了调用inflate()函数 填充布局 功能之外,还涉及到了 应用启动调用系统服务(进程间通信)、对应组件作用域内单例管理额外功能扩展 等等一系列复杂的逻辑。

本文笔者将针对LayoutInlater的整个设计思路进行描述,其整体结构如下图:

反思|Android LayoutInflater机制的设计与实现

整体思路

1、创建流程

顾名思义,LayoutInflater的作用就是 布局填充器 ,其行为本质是调用了Android本身提供的 系统服务。而在Android系统的设计中,获取系统服务的实现方式就是通过ServiceManager来取得和对应服务交互的IBinder对象,然后创建对应系统服务的代理。

Android应用层将系统服务注册相关的API放在了SystemServiceRegistry类中,而将注册服务行为的代码放在了ContextImpl类中,ContextImpl类实现了Context类下的所有抽象方法。

Android应用层还定义了一个Context的另外一个子类:ContextWrapperActivityService等组件继承了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.Factory2接口在Android SDK中的应用非常普遍,AppCompatActivityFragmentManager就是最有力的体现,LayoutInflater.inflate()方法的理解虽然重要,但笔者窃以为LayoutInflater.Factory2的重要性与其相比不逞多让。

对于LayoutInflater整体不甚熟悉的开发者而言,本小节文字描述似乎晦涩难懂,且难免有是否过度设计的疑惑,但这些文字的本质却是布局填充流程整体的设计思想,读者不应该将本文视为源码分析,而应该将自己代入到设计的过程中

创建流程

反思|Android LayoutInflater机制的设计与实现

1.Context:系统服务的提供者

上文提到,LayoutInflater作为系统服务之一,获取方式是通过ServiceManager来取得和对应服务交互的IBinder对象,然后创建对应系统服务的代理。

Binder机制相关并非本文的重点,读者可以注意到,Android的设计者将获取系统服务的接口交给了Context类,意味着开发者可以通过任意一个Context的实现类获取系统服务,包括不限于ActivityServiceApplication等等:

public abstract class Context {   // 获取系统服务   public abstract Object getSystemService(String name);   // ...... }

读者需要理解,Context类地职责并非只针对 系统服务 进行提供,还包括诸如 启动其它组件获取SharedPerferences 等等,其中大部分功能对于Context的子类而言都是公共的,因此没有必要每个子类都对其进行实现。

Android设计者并没有直接通过继承的方式将公共业务逻辑放入Base类供组件调用或者重写,而是借鉴了 装饰器模式 的思想:分别定义了ContextImplContextWrapper两个子类:

反思|Android LayoutInflater机制的设计与实现

2.ContextImpl:Context的公共API实现

Context的公共API的实现都交给了ContextImpl,以获取系统服务为例,Android应用层将系统服务注册相关的API放在了SystemServiceRegistry类中,而ContextImpl则是SystemServiceRegistry#getSystemService的唯一调用者:

class ContextImpl extends Context {     // 该成员即开发者使用的`Activity`等外部组件     private Context mOuterContext;           public Object getSystemService(String name) {         return SystemServiceRegistry.getSystemService(this, name);     } }

这种设计使得 系统服务的注册SystemServiceRegistry类) 和 系统服务的获取ContextImpl类) 在代码中只有一处声明和调用,大幅降低了模块之间的耦合。

3.ContextWrapper:Context的装饰器

ContextWrapper则是Context的装饰器,当组件需要获取系统服务时交给ContextImpl成员处理,伪代码实现如下:

// class Activity extends ContextWrapper class ContextWrapper extends Context {     // 1.将 ContextImpl 作为成员进行存储     public ContextWrapper(ContextImpl base) {         mBase = base;     }      ContextImpl mBase;      // 2.系统服务的获取统一交给了ContextImpl          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, ...)函数,该函数很重要,在【布局流程】一节中会继续引申。

4.组件的局部单例

读者也许注意到,对于单个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;       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的窗口进行描述。

1.PhoneWindow:setContentView()的真正实现

Activity将布局填充相关的逻辑委托给了PhoneWindowActivitysetContentView()函数,其本质是调用了PhoneWindowsetContentView()函数。

public class PhoneWindow extends Window {     public PhoneWindow(Context context) {        super(context);        mLayoutInflater = LayoutInflater.from(context);    }     // Activity.setContentView 实际上是调用了 PhoneWindow.setContentView()        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的整体流程已经有了一个初步的掌握,需要清楚的两点是:

  • 1.无论是哪种方式获取到的LayoutInflater,都是通过ContextImpl.getSystemService()获取的,并且在Activity等组件的生命周期内保持单例;
  • 2.即使是Activity.setContentView()函数,本质上也还是通过LayoutInflater.inflate()函数对布局进行解析和创建。

2.inflate()流程的设计和实现

从思想上来看,LayoutInflater.inflate()函数内部实现比较简单直观:

public View inflate( int resource, ViewGroup root, boolean attachToRoot) {       // ... }

对该函数的参数进行简单归纳如下:第一个参数代表所要加载的布局,第二个参数是ViewGroup,这个参数需要与第3个参数配合使用,attachToRoot如果为true就把布局添加到ViewGroup中;若为false则只采用ViewGroupLayoutParams作为测量的依据却不直接添加到ViewGroup中。

从设计的角度上思考,该函数的设计过程中,为什么需要定义这样的三个参数?为什么这样三个参数就能涵盖我们日常开发过程中布局填充的需求?

2.1 三个火枪手

对于第一个资源id参数而言,UI的创建必然依赖了布局文件资源的引用,因此这个参数无可厚非。

我们先略过第二个参数,直接思考第三个参数,为什么需要这样一个boolean类型的值,以决定是否将创建的View直接添加到指定的ViewGroup中呢,不设计这个参数是否可以?

换个角度思考,这个问题的本质其实是:是否每个View的创建都必须立即添加在ViewGroup中?答案当然是否定的,为了保证性能,设计者不可能让所有的View被创建后都能够立即被立即添加在ViewGroup中,这与目前Android中很多组件的设计都有冲突,比如ViewStubRecyclerView的条目、Fragment等等。

因此,更好的方式应该是可以通过一个boolean的开关将整个过程切分成2个小步骤,当View生成并根据ViewGroup的布局参数生成了对应的测量依据后,开发者可以根据需求手动灵活配置是否立即添加到ViewGroup中——这就是第三个参数的由来。

那么ViewGroup类型的第二个参数为什么可以为空呢?实际开发过程中,似乎并没有什么场景在填充布局时需要使ViewGroup为空?

读者仔细思考可以很容易得出结论,事实上该参数可空是有必要的——对于ActivityUI的创建而言,根结点最顶层的ViewGroup必然是没有父控件的,这时在布局的创建时,就必须通过将null作为第二个参数交给LayoutInlaterinflate()方法,当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的。

2.2 xml解析流程

xml解析过程的思路很简单;

  • 1. 首先根据布局文件,生成对应布局的XmlPullParser解析器对象;
  • 2. 对于单个View的解析而言,一个View的实例化依赖Context上下文对象和attr的属性集,而设计者正是通过将上下文对象和属性集作为参数,通过 反射 注入到View的构造器中对单个View进行创建;
  • 3. 对于整个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个明显的缺陷:

  • 1.布局的加载流程中,每一个View的实例化都依赖了Java的反射机制,这意味着额外性能的损耗;
  • 2.如果在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减少了大量反射创建控件的情况——设计者的思路是,在AppCompatActivityonCreate()方法中,为LayoutInflater对象调用了setFactory2()方法:

// AppCompatActivity类  protected void onCreate( Bundle savedInstanceState) {     getDelegate().installViewFactory();     //... }  // AppCompatDelegateImpl类  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文件中配置的是ButtonsetContentView()之后,生成的控件其实是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  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源码进行整体小结。

参考

  • Android源码
  • Android探究LayoutInflater setFactory
  • LayoutInflater——你应该知道的一点知识

关于我

Hello,我是 却把清梅嗅 ,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的 博客 或者 Github。

如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?

  • 我的Android学习体系
  • 关于文章纠错
  • 关于知识付费
  • 关于《反思》系列

概述

Android体系本身非常宏大,源码中值得思考和借鉴之处众多。以LayoutInflater本身为例,其整个流程中除了调用inflate()函数 填充布局 功能之外,还涉及到了 应用启动调用系统服务(进程间通信)、对应组件作用域内单例管理额外功能扩展 等等一系列复杂的逻辑。

本文笔者将针对LayoutInlater的整个设计思路进行描述,其整体结构如下图:

反思|Android LayoutInflater机制的设计与实现

整体思路

1、创建流程

顾名思义,LayoutInflater的作用就是 布局填充器 ,其行为本质是调用了Android本身提供的 系统服务。而在Android系统的设计中,获取系统服务的实现方式就是通过ServiceManager来取得和对应服务交互的IBinder对象,然后创建对应系统服务的代理。

Android应用层将系统服务注册相关的API放在了SystemServiceRegistry类中,而将注册服务行为的代码放在了ContextImpl类中,ContextImpl类实现了Context类下的所有抽象方法。

Android应用层还定义了一个Context的另外一个子类:ContextWrapperActivityService等组件继承了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.Factory2接口在Android SDK中的应用非常普遍,AppCompatActivityFragmentManager就是最有力的体现,LayoutInflater.inflate()方法的理解虽然重要,但笔者窃以为LayoutInflater.Factory2的重要性与其相比不逞多让。

对于LayoutInflater整体不甚熟悉的开发者而言,本小节文字描述似乎晦涩难懂,且难免有是否过度设计的疑惑,但这些文字的本质却是布局填充流程整体的设计思想,读者不应该将本文视为源码分析,而应该将自己代入到设计的过程中

创建流程

反思|Android LayoutInflater机制的设计与实现

1.Context:系统服务的提供者

上文提到,LayoutInflater作为系统服务之一,获取方式是通过ServiceManager来取得和对应服务交互的IBinder对象,然后创建对应系统服务的代理。

Binder机制相关并非本文的重点,读者可以注意到,Android的设计者将获取系统服务的接口交给了Context类,意味着开发者可以通过任意一个Context的实现类获取系统服务,包括不限于ActivityServiceApplication等等:

public abstract class Context {   // 获取系统服务   public abstract Object getSystemService(String name);   // ...... }

读者需要理解,Context类地职责并非只针对 系统服务 进行提供,还包括诸如 启动其它组件获取SharedPerferences 等等,其中大部分功能对于Context的子类而言都是公共的,因此没有必要每个子类都对其进行实现。

Android设计者并没有直接通过继承的方式将公共业务逻辑放入Base类供组件调用或者重写,而是借鉴了 装饰器模式 的思想:分别定义了ContextImplContextWrapper两个子类:

反思|Android LayoutInflater机制的设计与实现

2.ContextImpl:Context的公共API实现

Context的公共API的实现都交给了ContextImpl,以获取系统服务为例,Android应用层将系统服务注册相关的API放在了SystemServiceRegistry类中,而ContextImpl则是SystemServiceRegistry#getSystemService的唯一调用者:

class ContextImpl extends Context {     // 该成员即开发者使用的`Activity`等外部组件     private Context mOuterContext;           public Object getSystemService(String name) {         return SystemServiceRegistry.getSystemService(this, name);     } }

这种设计使得 系统服务的注册SystemServiceRegistry类) 和 系统服务的获取ContextImpl类) 在代码中只有一处声明和调用,大幅降低了模块之间的耦合。

3.ContextWrapper:Context的装饰器

ContextWrapper则是Context的装饰器,当组件需要获取系统服务时交给ContextImpl成员处理,伪代码实现如下:

// class Activity extends ContextWrapper class ContextWrapper extends Context {     // 1.将 ContextImpl 作为成员进行存储     public ContextWrapper(ContextImpl base) {         mBase = base;     }      ContextImpl mBase;      // 2.系统服务的获取统一交给了ContextImpl          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, ...)函数,该函数很重要,在【布局流程】一节中会继续引申。

4.组件的局部单例

读者也许注意到,对于单个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;       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的窗口进行描述。

1.PhoneWindow:setContentView()的真正实现

Activity将布局填充相关的逻辑委托给了PhoneWindowActivitysetContentView()函数,其本质是调用了PhoneWindowsetContentView()函数。

public class PhoneWindow extends Window {     public PhoneWindow(Context context) {        super(context);        mLayoutInflater = LayoutInflater.from(context);    }     // Activity.setContentView 实际上是调用了 PhoneWindow.setContentView()        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的整体流程已经有了一个初步的掌握,需要清楚的两点是:

  • 1.无论是哪种方式获取到的LayoutInflater,都是通过ContextImpl.getSystemService()获取的,并且在Activity等组件的生命周期内保持单例;
  • 2.即使是Activity.setContentView()函数,本质上也还是通过LayoutInflater.inflate()函数对布局进行解析和创建。

2.inflate()流程的设计和实现

从思想上来看,LayoutInflater.inflate()函数内部实现比较简单直观:

public View inflate( int resource, ViewGroup root, boolean attachToRoot) {       // ... }

对该函数的参数进行简单归纳如下:第一个参数代表所要加载的布局,第二个参数是ViewGroup,这个参数需要与第3个参数配合使用,attachToRoot如果为true就把布局添加到ViewGroup中;若为false则只采用ViewGroupLayoutParams作为测量的依据却不直接添加到ViewGroup中。

从设计的角度上思考,该函数的设计过程中,为什么需要定义这样的三个参数?为什么这样三个参数就能涵盖我们日常开发过程中布局填充的需求?

2.1 三个火枪手

对于第一个资源id参数而言,UI的创建必然依赖了布局文件资源的引用,因此这个参数无可厚非。

我们先略过第二个参数,直接思考第三个参数,为什么需要这样一个boolean类型的值,以决定是否将创建的View直接添加到指定的ViewGroup中呢,不设计这个参数是否可以?

换个角度思考,这个问题的本质其实是:是否每个View的创建都必须立即添加在ViewGroup中?答案当然是否定的,为了保证性能,设计者不可能让所有的View被创建后都能够立即被立即添加在ViewGroup中,这与目前Android中很多组件的设计都有冲突,比如ViewStubRecyclerView的条目、Fragment等等。

因此,更好的方式应该是可以通过一个boolean的开关将整个过程切分成2个小步骤,当View生成并根据ViewGroup的布局参数生成了对应的测量依据后,开发者可以根据需求手动灵活配置是否立即添加到ViewGroup中——这就是第三个参数的由来。

那么ViewGroup类型的第二个参数为什么可以为空呢?实际开发过程中,似乎并没有什么场景在填充布局时需要使ViewGroup为空?

读者仔细思考可以很容易得出结论,事实上该参数可空是有必要的——对于ActivityUI的创建而言,根结点最顶层的ViewGroup必然是没有父控件的,这时在布局的创建时,就必须通过将null作为第二个参数交给LayoutInlaterinflate()方法,当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的。

2.2 xml解析流程

xml解析过程的思路很简单;

  • 1. 首先根据布局文件,生成对应布局的XmlPullParser解析器对象;
  • 2. 对于单个View的解析而言,一个View的实例化依赖Context上下文对象和attr的属性集,而设计者正是通过将上下文对象和属性集作为参数,通过 反射 注入到View的构造器中对单个View进行创建;
  • 3. 对于整个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个明显的缺陷:

  • 1.布局的加载流程中,每一个View的实例化都依赖了Java的反射机制,这意味着额外性能的损耗;
  • 2.如果在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减少了大量反射创建控件的情况——设计者的思路是,在AppCompatActivityonCreate()方法中,为LayoutInflater对象调用了setFactory2()方法:

// AppCompatActivity类  protected void onCreate( Bundle savedInstanceState) {     getDelegate().installViewFactory();     //... }  // AppCompatDelegateImpl类  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文件中配置的是ButtonsetContentView()之后,生成的控件其实是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  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源码进行整体小结。

参考

  • Android源码
  • Android探究LayoutInflater setFactory
  • LayoutInflater——你应该知道的一点知识

关于我

Hello,我是 却把清梅嗅 ,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的 博客 或者 Github。

如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?

  • 我的Android学习体系
  • 关于文章纠错
  • 关于知识付费
  • 关于《反思》系列

概述

Android体系本身非常宏大,源码中值得思考和借鉴之处众多。以LayoutInflater本身为例,其整个流程中除了调用inflate()函数 填充布局 功能之外,还涉及到了 应用启动调用系统服务(进程间通信)、对应组件作用域内单例管理额外功能扩展 等等一系列复杂的逻辑。

本文笔者将针对LayoutInlater的整个设计思路进行描述,其整体结构如下图:

反思|Android LayoutInflater机制的设计与实现

整体思路

1、创建流程

顾名思义,LayoutInflater的作用就是 布局填充器 ,其行为本质是调用了Android本身提供的 系统服务。而在Android系统的设计中,获取系统服务的实现方式就是通过ServiceManager来取得和对应服务交互的IBinder对象,然后创建对应系统服务的代理。

Android应用层将系统服务注册相关的API放在了SystemServiceRegistry类中,而将注册服务行为的代码放在了ContextImpl类中,ContextImpl类实现了Context类下的所有抽象方法。

Android应用层还定义了一个Context的另外一个子类:ContextWrapperActivityService等组件继承了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.Factory2接口在Android SDK中的应用非常普遍,AppCompatActivityFragmentManager就是最有力的体现,LayoutInflater.inflate()方法的理解虽然重要,但笔者窃以为LayoutInflater.Factory2的重要性与其相比不逞多让。

对于LayoutInflater整体不甚熟悉的开发者而言,本小节文字描述似乎晦涩难懂,且难免有是否过度设计的疑惑,但这些文字的本质却是布局填充流程整体的设计思想,读者不应该将本文视为源码分析,而应该将自己代入到设计的过程中

创建流程

反思|Android LayoutInflater机制的设计与实现

1.Context:系统服务的提供者

上文提到,LayoutInflater作为系统服务之一,获取方式是通过ServiceManager来取得和对应服务交互的IBinder对象,然后创建对应系统服务的代理。

Binder机制相关并非本文的重点,读者可以注意到,Android的设计者将获取系统服务的接口交给了Context类,意味着开发者可以通过任意一个Context的实现类获取系统服务,包括不限于ActivityServiceApplication等等:

public abstract class Context {   // 获取系统服务   public abstract Object getSystemService(String name);   // ...... }

读者需要理解,Context类地职责并非只针对 系统服务 进行提供,还包括诸如 启动其它组件获取SharedPerferences 等等,其中大部分功能对于Context的子类而言都是公共的,因此没有必要每个子类都对其进行实现。

Android设计者并没有直接通过继承的方式将公共业务逻辑放入Base类供组件调用或者重写,而是借鉴了 装饰器模式 的思想:分别定义了ContextImplContextWrapper两个子类:

反思|Android LayoutInflater机制的设计与实现

2.ContextImpl:Context的公共API实现

Context的公共API的实现都交给了ContextImpl,以获取系统服务为例,Android应用层将系统服务注册相关的API放在了SystemServiceRegistry类中,而ContextImpl则是SystemServiceRegistry#getSystemService的唯一调用者:

class ContextImpl extends Context {     // 该成员即开发者使用的`Activity`等外部组件     private Context mOuterContext;           public Object getSystemService(String name) {         return SystemServiceRegistry.getSystemService(this, name);     } }

这种设计使得 系统服务的注册SystemServiceRegistry类) 和 系统服务的获取ContextImpl类) 在代码中只有一处声明和调用,大幅降低了模块之间的耦合。

3.ContextWrapper:Context的装饰器

ContextWrapper则是Context的装饰器,当组件需要获取系统服务时交给ContextImpl成员处理,伪代码实现如下:

// class Activity extends ContextWrapper class ContextWrapper extends Context {     // 1.将 ContextImpl 作为成员进行存储     public ContextWrapper(ContextImpl base) {         mBase = base;     }      ContextImpl mBase;      // 2.系统服务的获取统一交给了ContextImpl          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, ...)函数,该函数很重要,在【布局流程】一节中会继续引申。

4.组件的局部单例

读者也许注意到,对于单个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;       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的窗口进行描述。

1.PhoneWindow:setContentView()的真正实现

Activity将布局填充相关的逻辑委托给了PhoneWindowActivitysetContentView()函数,其本质是调用了PhoneWindowsetContentView()函数。

public class PhoneWindow extends Window {     public PhoneWindow(Context context) {        super(context);        mLayoutInflater = LayoutInflater.from(context);    }     // Activity.setContentView 实际上是调用了 PhoneWindow.setContentView()        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的整体流程已经有了一个初步的掌握,需要清楚的两点是:

  • 1.无论是哪种方式获取到的LayoutInflater,都是通过ContextImpl.getSystemService()获取的,并且在Activity等组件的生命周期内保持单例;
  • 2.即使是Activity.setContentView()函数,本质上也还是通过LayoutInflater.inflate()函数对布局进行解析和创建。

2.inflate()流程的设计和实现

从思想上来看,LayoutInflater.inflate()函数内部实现比较简单直观:

public View inflate( int resource, ViewGroup root, boolean attachToRoot) {       // ... }

对该函数的参数进行简单归纳如下:第一个参数代表所要加载的布局,第二个参数是ViewGroup,这个参数需要与第3个参数配合使用,attachToRoot如果为true就把布局添加到ViewGroup中;若为false则只采用ViewGroupLayoutParams作为测量的依据却不直接添加到ViewGroup中。

从设计的角度上思考,该函数的设计过程中,为什么需要定义这样的三个参数?为什么这样三个参数就能涵盖我们日常开发过程中布局填充的需求?

2.1 三个火枪手

对于第一个资源id参数而言,UI的创建必然依赖了布局文件资源的引用,因此这个参数无可厚非。

我们先略过第二个参数,直接思考第三个参数,为什么需要这样一个boolean类型的值,以决定是否将创建的View直接添加到指定的ViewGroup中呢,不设计这个参数是否可以?

换个角度思考,这个问题的本质其实是:是否每个View的创建都必须立即添加在ViewGroup中?答案当然是否定的,为了保证性能,设计者不可能让所有的View被创建后都能够立即被立即添加在ViewGroup中,这与目前Android中很多组件的设计都有冲突,比如ViewStubRecyclerView的条目、Fragment等等。

因此,更好的方式应该是可以通过一个boolean的开关将整个过程切分成2个小步骤,当View生成并根据ViewGroup的布局参数生成了对应的测量依据后,开发者可以根据需求手动灵活配置是否立即添加到ViewGroup中——这就是第三个参数的由来。

那么ViewGroup类型的第二个参数为什么可以为空呢?实际开发过程中,似乎并没有什么场景在填充布局时需要使ViewGroup为空?

读者仔细思考可以很容易得出结论,事实上该参数可空是有必要的——对于ActivityUI的创建而言,根结点最顶层的ViewGroup必然是没有父控件的,这时在布局的创建时,就必须通过将null作为第二个参数交给LayoutInlaterinflate()方法,当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的。

2.2 xml解析流程

xml解析过程的思路很简单;

  • 1. 首先根据布局文件,生成对应布局的XmlPullParser解析器对象;
  • 2. 对于单个View的解析而言,一个View的实例化依赖Context上下文对象和attr的属性集,而设计者正是通过将上下文对象和属性集作为参数,通过 反射 注入到View的构造器中对单个View进行创建;
  • 3. 对于整个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个明显的缺陷:

  • 1.布局的加载流程中,每一个View的实例化都依赖了Java的反射机制,这意味着额外性能的损耗;
  • 2.如果在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减少了大量反射创建控件的情况——设计者的思路是,在AppCompatActivityonCreate()方法中,为LayoutInflater对象调用了setFactory2()方法:

// AppCompatActivity类  protected void onCreate( Bundle savedInstanceState) {     getDelegate().installViewFactory();     //... }  // AppCompatDelegateImpl类  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文件中配置的是ButtonsetContentView()之后,生成的控件其实是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  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源码进行整体小结。

参考

  • Android源码
  • Android探究LayoutInflater setFactory
  • LayoutInflater——你应该知道的一点知识

关于我

Hello,我是 却把清梅嗅 ,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的 博客 或者 Github。

如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?

  • 我的Android学习体系
  • 关于文章纠错
  • 关于知识付费
  • 关于《反思》系列

部分转自互联网,侵权删除联系

赞(0) 打赏
部分文章转自网络,侵权联系删除b2bchain区块链学习技术社区 » 反思|Android LayoutInflater机制的设计与实现求职学习资料
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

b2b链

联系我们联系我们