LayoutInflater原理分析与复杂布局优化实践


前言

Android布局的加载默认是在主线程的,如果布局太过复杂或者冗余,则会影响页面加载速度,降低UI线程的响应速度,进而让用户感觉卡顿,影响用户体验。当然,目前也有很多布局优化方法。比如:尽量使布局扁平化、merge标签使用、ViewStub延迟化加载标签、避免过度绘制等等。但是当所有技术都用上后,限于业务庞大,布局确实复杂无法再优化,加载布局的过程仍然很耗时,该怎么办呢,我们通过分析加载布局的LayoutInflater类寻求解决方案。


LayoutInflater定义

在Android中,LayoutInflater大家一定不陌生,它就是Android的布局加载器,它可以将xml布局文件实例化为相应的View对象。

获取LayoutInflater:

LayoutInflater layoutInflater = (LayoutInflater) context

      .getSystemService(Context.LAYOUT_INFLATER_SERVICE);

LayoutInflater layoutInflater = LayoutInflater.from(context);

第二种方式其实最终也是调用第一种方法,只是Android给我们做了一层封装。


布局加载过程分析

  • 在获取了LayoutInflater实例化对象之后,就可以调用inflate()来进行加载xml布局了,可以看到有四个重载方法。

  • 重载方法一:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {

  return inflate(resource, root, root != null);

}

  • 重载方法二:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {

  return inflate(parser, root, root != null);

}

  • 重载方法三:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {

  final Resources res = getContext().getResources();

  final XmlResourceParser parser = res.getLayout(resource);

  try {

      return inflate(parser, root, attachToRoot);

  } finally {

      parser.close();

  }

}

  • 重载方法四:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot){

//方法体内容过多,不再贴出

}

可以看见,方法一调用了方法三,方法二和方法三最终都调用了方法四;而方法三中有个final XmlResourceParser parser = res.getLayout(resource);这个方法跟进去发现是从磁盘中读取xml布局文件并进行解析得到XmlResourceParser,可知此操作涉及IO,了解到它是个耗时操作。
由于方法四源码过多,就不再一一贴出,后面我们都把源码分析过程转换成流程图来呈现。
先来看下inflate方法调用流程:


inflate调用流程总结:如果是merge标签直接调用rInflate方法,否则通过createViewFromTag方法先去创建这个布局的根节点view,再将其作为parent入参调用rInflateChildren方法,由于rInflateChildren内部调用了rInflate,而rInflate最终还是遍历view树循环调用createViewFromTag方法进行创建每一层级的view并将其作为parent入参调用rInflateChildren。整个过程其实就是遍历DOM树进行递归创建view。

  • 接下来再看下createViewFromTag方法调用流程图:

  • createViewFromTag调用流程总结:如果mFactory2不为null则通过调用mFactory2的onCreateView,否则如果mFactory不为null则调用mFactory的onCreateView…如果工厂类都为null,则调用LayoutInflater中的onCreateView方法,通过查看onCreateView方法源码我们可以知道最终都是通过反射创建view的。
  • 经过源码分析可以发现整个布局加载过程中主要耗时操作有两点:
  • 涉及IO读取操作的xml布局文件解析
  • 通过反射的方式来递归创建 View 对象

比较容易想到的解决方法:
1. 直接动态的创建布局,new出每个view对象,绕过xml解析和反射,但是这样显然很难维护且可读性差,浪费了Android的xml可视化便捷布局方式。
2. 将布局的加载过程放到子线程处理,google其实提供了比较成熟方案,那就是v4包下的AsyncLayoutInflater,它是将LayoutInflater.inflater过程放到子线程来做。


AsyncLayoutInflater实践

  • 监控页面加载耗时情况

首先针对我们页面加载进行监控,结合业务代码找到有些view加载比较耗时的地方,通过Android Studio自带的Profiler监控可以很方便的查看各方法调用耗时与所占比例,例如:


可以看到initStyleInfoView耗时61ms,经过查看代码在页面初次进入时,在此方法里对一些必备的view进行了inflate加载。

  • 尝试使用异步加载布局,例如:
/**

* 初始化style info的view

*/

private void initStyleInfoView() {

  if (xxxViewA == null) {

      new AsyncLayoutInflater(this).inflate(R.layout.xxx, null, new AsyncLayoutInflater.OnInflateFinishedListener() {

          @Override

          public void onInflateFinished(@NonNull View view, int i, @Nullable ViewGroup viewGroup) {

              xxxViewA = view;

              xxxViewA.init();

          }

      });

  }

?

}

  • 对一些布局进行异步加载后再对应用耗时进行监控,该方法耗时截图如下:

可以看到initStyleInfoView方法耗时降到1.5ms,其真正的耗时操作inflate已经放到了子线程处理。

  • 优化前后目标帧时间监控对比

我们在手机开发者选项中打开GPU呈现模型分析,然后 通过adb shell dumpsys gfxinfo 在终端打印帧时间日志,如下图:


将其复制到表格进行图形化,方便我们分析数据,并对进行优化前后的效果做个对比。
优化之前每帧时间柱状图:


图1
优化之后每帧时间柱状图


图2
对比分析:我们知道Draw + Prepare + Process + Execute = 完整显示一帧的时间,这个时间小于16ms才能保证理想的每秒60帧,所以从第一张优化之前的图可以看出首次进入页面请求数据回来后加载页面时第41、42、44、46、47帧,共5帧时间超过了16ms,其他每帧均保持在16ms之下;我们优化之后,部分布局加载放入了子线程中,减少了主线程的布局加载耗时,图2是优化之后的首次进入页面每帧耗时统计,仅剩3帧时间超过16ms,优化了两帧,通过优化前后的数据对比,可见其效果显著。当然在我们的业务代码中还有其他很多复杂的view,我们都可以不断尝试通过异步加载布局来持续的优化,以不断提升用户体验。

  • 由此可见,对于业务量庞大,布局复杂的页面来说,异步加载布局的确是个不错的选择。


AsyncLayoutInflater实现原理

概述:首先它会创建一个阻塞队列,开启一个子线程,当调用AsyncLayoutInflater的inflate布局时会往阻塞队列里添加inflate任务,子线程再从队列中取出inflate任务进行加载,加载完成后再通过handler转换线程,将view回调到主线程。

  • 先看下AsyncLayoutInflater的构造函数
public AsyncLayoutInflater(@NonNull Context context) {

  mInflater = new BasicInflater(context);

  mHandler = new Handler(mHandlerCallback);

  mInflateThread = InflateThread.getInstance();

}

先创建一个BasicInflater对象,它继承自LayoutInflater,只是重写了onCreateView。

private static class BasicInflater extends LayoutInflater {

    private static final String[] sClassPrefixList = {

        "android.widget.",

        "android.webkit.",

        "android.app."

    };



BasicInflater(Context context) { super(context); }

@Override public LayoutInflater cloneInContext(Context newContext) { return new BasicInflater(newContext); }

@Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { for (String prefix : sClassPrefixList) { try { View view = createView(name, prefix, attrs); if (view != null) { return view; } } catch (ClassNotFoundException e) { } } return super.onCreateView(name, attrs); } }

创建一个Handler,作用只是为了切换线程。
创建一个InflateThread,从名字就看得出来它是一个子线程,用来加载布局的线程。

  • AsyncLayoutInflater解析布局的inflate方法
@UiThread

public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent,

        @NonNull OnInflateFinishedListener callback) {

    if (callback == null) {

        throw new NullPointerException("callback argument may not be null!");

    }

    InflateRequest request = mInflateThread.obtainRequest();

    request.inflater = this;

    request.resid = resid;

    request.parent = parent;

    request.callback = callback;

    mInflateThread.enqueue(request);

}

它通过mInflateThread获取到InflateRequest任务对象后,设置必要参数后,加入任务队列,等待子线程处理。

  • InflateThread代码
private static class InflateThread extends Thread {

  private static final InflateThread sInstance;

  static {

      sInstance = new InflateThread();

      sInstance.start();

  }

  public static InflateThread getInstance() {

      return sInstance;

  }

?

  private ArrayBlockingQueue mQueue = new ArrayBlockingQueue(10);

  private SynchronizedPool mRequestPool = new SynchronizedPool(10);

  public void runInner() {

      InflateRequest request;

      try {

          request = mQueue.take();

      } catch (InterruptedException ex) {

          return;

      }

      try {

          request.view = request.inflater.mInflater.inflate(

                  request.resid, request.parent, false);

      } catch (RuntimeException ex) {

      }

      Message.obtain(request.inflater.mHandler, 0, request)

              .sendToTarget();

  }

  @Override

  public void run() {

      while (true) {

          runInner();

      }

  }

  public InflateRequest obtainRequest() {

      InflateRequest obj = mRequestPool.acquire();

      if (obj == null) {

          obj = new InflateRequest();

      }

      return obj;

  }

  public void releaseRequest(InflateRequest obj) {

      obj.callback = null;

      obj.inflater = null;

      obj.parent = null;

      obj.resid = 0;

      obj.view = null;

      mRequestPool.release(obj);

  }

  public void enqueue(InflateRequest request) {

      try {

          mQueue.put(request);

      } catch (InterruptedException e) {

          throw new RuntimeException(

                  "Failed to enqueue async inflate request", e);

      }

  }

}

从这个线程的run方法可以看出这个线程开启一个while循环执行runInner方法,而此方法通过mQueue.take()从阻塞队列ArrayBlockingQueue中取任务,进行布局解析,解析完成后再通过handler发送消息到主线程。

  • AsyncLayoutInflate虽然看起来很不错,但使用起来也遇到不少“坑”,如:

* 使用异步inflate,需要这个布局的父布局的generateLayoutParams函数是线程安全的。
* 异步加载的view中不能有创建Handler或者调用myLooper()的操作,原因通过上面源码也知道,我们加载view是在子线程的默认没有Looper.prepare。
* AsyncLayoutInflate是不支持设置Factory和Factory2的,这会导致有些布局无法得到系统的兼容。
* 不支持加载包含Fragment的布局。
* 从AsyncLayoutInflater源码也可以看出其内部使用单线程来做所有的布局加载工作,假如有很多任务,单线程可能不够用;内部ArrayBlockingQueue阻塞队列的默认大小只有10,假如超过了10个任务,也会导致主线程的等待。
总的来说,AsyncLayoutInflater已经能满足大部分需求,为布局优化提供了很好的支持,当然也可以根据自己的业务规模或者使用环境做一些定制化,针对AsyncLayoutInflater的缺点做一些相应的改进,比如可以自定义一个AsyncLayoutInflater,改造BasicInflater使其支持Factory2,进而得到系统的兼容;也可以引入线程池处理布局加载任务,减少单线程的等待等等,以满足自己的业务需求。相信后续google也会进一步优化AsyncLayoutInflater,使其功能更加完善,并且拥有更好的兼容性,能更好的满足用户需求。

参考文档:

https://developer.android.google.cn/reference/androidx/asynclayoutinflater/view/AsyncLayoutInflater?hl=en
https://developer.android.google.cn/reference/kotlin/android/view/LayoutInflater?hl=en