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

2108次阅读  |  发布于4年以前

前言

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给我们做了一层封装。

布局加载过程分析

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。

比较容易想到的解决方法:

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回调到主线程。

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,从名字就看得出来它是一个子线程,用来加载布局的线程。

@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任务对象后,设置必要参数后,加入任务队列,等待子线程处理。

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<InflateRequest> mQueue = new ArrayBlockingQueue<>(10);
  private SynchronizedPool<InflateRequest> 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发送消息到主线程。

总的来说,AsyncLayoutInflater已经能满足大部分需求,为布局优化提供了很好的支持,当然也可以根据自己的业务规模或者使用环境做一些定制化,针对AsyncLayoutInflater的缺点做一些相应的改进,比如可以自定义一个AsyncLayoutInflater,改造BasicInflater使其支持Factory2,进而得到系统的兼容;也可以引入线程池处理布局加载任务,减少单线程的等待等等,以满足自己的业务需求。相信后续google也会进一步优化AsyncLayoutInflater,使其功能更加完善,并且拥有更好的兼容性,能更好的满足用户需求。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8