Skip to content
New issue

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机制的设计与实现 #25

Open
qingmei2 opened this issue Aug 19, 2019 · 0 comments
Open

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

qingmei2 opened this issue Aug 19, 2019 · 0 comments
Labels
Thinking in Android Thinking in Android

Comments

@qingmei2
Copy link
Owner

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

反思 系列博客是我的一种新学习方式的尝试,该系列起源和目录请参考 这里

概述

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

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

整体思路

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

创建流程

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两个子类:

2.ContextImpl:Context的公共API实现

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类) 在代码中只有一处声明和调用,大幅降低了模块之间的耦合。

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
    @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, ...)函数,该函数很重要,在【布局流程】一节中会继续引申。

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;

  @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的窗口进行描述。

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()
   @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的整体流程已经有了一个初步的掌握,需要清楚的两点是:

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

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

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

public View inflate(@LayoutRes 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解析器对象;
    1. 对于单个View的解析而言,一个View的实例化依赖Context上下文对象和attr的属性集,而设计者正是通过将上下文对象和属性集作为参数,通过 反射 注入到View的构造器中对单个View进行创建;
    1. 对于整个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类
@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文件中配置的是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
@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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Thinking in Android Thinking in Android
Projects
None yet
Development

No branches or pull requests

1 participant