前言

首先须要强调的是,这篇文章是对我之前写的《浅谈RecyclerView的性能优化》文章的补充,倡议大家先读完这篇文章后再来看这篇文章,滋味更佳。

过后因为篇幅的起因,并没有深刻开展解说,于是有很多感兴趣的敌人纷纷留言示意:能不能联合相干的示例代码解说一下到底如何实现?那么明天我就联合之前讲的如何优化onCreateViewHolder的加载工夫,讲一讲如何实现onCreateViewHolder的异步预加载,文章开端会给出示例代码的链接地址,心愿能给你带来启发。

剖析

之前咱们讲过,在优化onCreateViewHolder办法的时候,能够升高item的布局层级,能够缩小界面创立的渲染工夫,其本质就是升高view的inflate工夫。因为onCreateViewHolder最大的耗时局部,就是view的inflate。置信读过LayoutInflater.inflate源码的人都晓得,这部分的代码是同步操作,并且波及到大量的文件IO的操作以及锁操作,通常来说这部分的代码快的也须要几毫秒,慢的可能须要几十毫秒乃至上百毫秒也是很有可能的。 如果真到了每个ItemView的inflate须要花上上百毫秒的话,那么在大数据量的RecyclerView进行疾速高低滑动的时候,就必然会导致界面的滑动卡顿、不晦涩。

那么如何你的程序里真的有这样一个列表,它的每个ItemView都须要花上上百毫秒的工夫去inflate的话,你该怎么做?

  • 首先就是对布局进行优化,升高item的布局层级。但这点的优化往往是微不足道的。
  • 其次可能就是想方法让设计师从新设计,将布局中的某些内容删除或者折叠了,对暂不展现的内容应用ViewStub进行提早加载。不过说实在话,你既然有能力让设计师从新设计的话,还干个球的开发啊,间接当项目经理不香吗?
  • 最初你可能会思考不必xml写布局,改为应用代码本人一个一个new布局。话说回来了,一个应用xml加载的布局都要花上上百毫秒的布局,可能xml都快上千行上来了,你确定要本人一个一个new上来?

以上的形式,都是建设在列表布局能够批改的状况下,如果咱们应用的列表布局是第三方曾经提供好的呢?(例如广告SDK等)

那么有没有什么方法既能够不必批改以后的xml布局,又能够极大地缩短布局的加载工夫呢?毫无疑问,布局异步加载将为你关上新的世界。

原理

Google官网很早就发现了XML布局加载的性能问题,于是在androidx中提供了异步加载工具AsyncLayoutInflater。其本质就是开了一个长期期待的异步线程,在子线程中inflate view,而后把加载好的view通过接口抛出去,实现view的加载。

一般来说,对于简单的列表,往往都对应了简单的数据,而这简单的数据往往又是通过服务器获取而来。所以一般来说,一个列表在加载前,往往先须要拜访服务器获取数据,而后再刷新列表显示,而这拜访服务器的工夫大概也在300ms~1000ms之间。很多开发人员对这段时间往往没有加以利用,只是加上一个loading动画了事。

其实对于这一段事务真空的工夫窗口,咱们能够提前进行列表的ItemView的加载,这样等数据申请下来刷新列表的时候,咱们onCreateViewHolder的时候就能够间接到曾经当时预加载好的View缓存池中间接获取View传到ViewHolder中应用,这样onCreateViewHolder的创立工夫简直耗时为0,从而极大地晋升了列表的加载和渲染速度。具体的流程能够参见下图:

实现

下面我简略地解说了一下原理,下一步就是思考如何实现这样的成果了。

预加载缓存池

首先在预加载前,咱们须要先创立一个缓存池来存储预加载的View对象。

这里我抉择应用SparseArray进行存储,key是Int型,寄存布局资源的layoutId,value是Object型,寄存的是这类布局加载View的汇合。

这里的汇合类型我抉择的是LinkedList,因为咱们的缓存须要频繁的增加和删除操作,并且LinkedList实现了Deque接口,具备先入先出的能力。

这里View的援用我抉择的是软援用SoftReference,之所以不采纳WeakReference, 目标就是心愿缓存能多存在一段时间,防止内存的频繁开释和回收造成内存的抖动。

private static class ViewCache {    private final SparseArray<LinkedList<SoftReference<View>>> mViewPools = new SparseArray<>();    @NonNull    public LinkedList<SoftReference<View>> getViewPool(int layoutId) {        LinkedList<SoftReference<View>> views = mViewPools.get(layoutId);        if (views == null) {            views = new LinkedList<>();            mViewPools.put(layoutId, views);        }        return views;    }    public int getViewPoolAvailableCount(int layoutId) {        LinkedList<SoftReference<View>> views = getViewPool(layoutId);        Iterator<SoftReference<View>> it = views.iterator();        int count = 0;        while (it.hasNext()) {            if (it.next().get() != null) {                count++;            } else {                it.remove();            }        }        return count;    }    public void putView(int layoutId, View view) {        if (view == null) {            return;        }        getViewPool(layoutId).offer(new SoftReference<>(view));    }    @Nullable    public View getView(int layoutId) {        return getViewFromPool(getViewPool(layoutId));    }    private View getViewFromPool(@NonNull LinkedList<SoftReference<View>> views) {        if (views.isEmpty()) {            return null;        }        View target = views.pop().get();        if (target == null) {            return getViewFromPool(views);        }        return target;    }}

getViewFromPool办法咱们能够看出,这里对于ViewCache来说,每次取出一个缓存View应用的是pop办法,咱们都会将它从Pool中移除。

布局加载者

因为view的加载办法,波及到三个参数: 资源Id-resourceId, 父布局-root和是否增加到根布局-attachToRoot。

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

这里在onCreateViewHolder办法中attachToRoot恒为false,因而异步布局加载只须要后面两个参数以及一个回调接口即可,即如下的定义:

public interface ILayoutInflater {    /**     * 异步加载View     *     * @param parent   父布局     * @param layoutId 布局资源id     * @param callback 加载回调     */    void asyncInflateView(@NonNull ViewGroup parent, int layoutId, InflateCallback callback);    /**     * 同步加载View     *     * @param parent   父布局     * @param layoutId 布局资源id     * @return 加载的View     */    View inflateView(@NonNull ViewGroup parent, int layoutId);}public interface InflateCallback {    void onInflateFinished(int layoutId, View view);}

至于接口实现的话,就间接应用Google官网提供的异步加载工具AsyncLayoutInflater来实现。

public class DefaultLayoutInflater implements PreInflateHelper.ILayoutInflater {    private AsyncLayoutInflater mInflater;    private DefaultLayoutInflater() {}    private static final class InstanceHolder {        static final DefaultLayoutInflater sInstance = new DefaultLayoutInflater();    }    public static DefaultLayoutInflater get() {        return InstanceHolder.sInstance;    }    @Override    public void asyncInflateView(@NonNull ViewGroup parent, int layoutId, PreInflateHelper.InflateCallback callback) {        if (mInflater == null) {            Context context = parent.getContext();            mInflater = new AsyncLayoutInflater(new ContextThemeWrapper(context.getApplicationContext(), context.getTheme()));        }        mInflater.inflate(layoutId, parent, (view, resId, parent1) -> {            if (callback != null) {                callback.onInflateFinished(resId, view);            }        });    }    @Override    public View inflateView(@NonNull ViewGroup parent, int layoutId) {        return InflateUtils.getInflateView(parent, layoutId);    }}

预加载辅助类

有了预加载缓存池ViewCache和异步加载能力的提供者IAsyncInflater,上面就是来协调这两者进行单干,实现布局的预加载和View的读取。

首先须要定义的是依据ViewGroup和layoutId获取View的办法,提供给Adapter的onCreateViewHolder办法应用。

  • 首先咱们须要去ViewCache中去取是否已有预加载好的view供咱们应用。如果有则取出,并进行一次预加载补充给ViewCache。
  • 如果没有,就只能同步加载布局了。
public View getView(@NonNull ViewGroup parent, int layoutId, int maxCount) {    View view = mViewCache.getView(layoutId);    if (view != null) {        UILog.dTag(TAG, "get view from cache!");        preloadOnce(parent, layoutId, maxCount);        return view;    }    return mLayoutInflater.inflateView(parent, layoutId);}

对于预加载布局,并退出缓存的办法实现。

  • 首先咱们须要去ViewCache查问以后可用缓存的数量,如果可用缓存的数量大于等于最大数量,即不须要进行预加载。
  • 对于须要预加载的,须要计算预加载的数量,如果以后没有强制执行的次数,就间接按残余最大数量进行加载,否则取强制执行次数和残余最大数量的最小值进行加载。
  • 对于预加载结束获取的View,间接退出到ViewCache中。
public void preload(@NonNull ViewGroup parent, int layoutId, int maxCount, int forcePreCount) {    int viewsAvailableCount = mViewCache.getViewPoolAvailableCount(layoutId);    if (viewsAvailableCount >= maxCount) {        return;    }    int needPreloadCount = maxCount - viewsAvailableCount;    if (forcePreCount > 0) {        needPreloadCount = Math.min(forcePreCount, needPreloadCount);    }    for (int i = 0; i < needPreloadCount; i++) {        // 异步加载View        mLayoutInflater.asyncInflateView(parent, layoutId, new InflateCallback() {            @Override            public void onInflateFinished(int layoutId, View view) {                mViewCache.putView(layoutId, view);            }        });    }}

Adapter中执行预加载

有了预加载辅助类PreInflateHelper,上面咱们只须要间接调用它的preload办法和getView办法即可。这里须要留神的是,ViewHolder中ItemView的ViewGroup就是RecyclerView它自身,所以Adapter的构造方法须要传入RecyclerView供预加载辅助类进行预加载。

public class OptimizeListAdapter extends MockLongTimeLoadListAdapter {    private static final class InstanceHolder {        static final PreInflateHelper sInstance = new PreInflateHelper();    }        public static PreInflateHelper getInflateHelper() {        return OptimizeListAdapter.InstanceHolder.sInstance;    }    public OptimizeListAdapter(RecyclerView recyclerView) {        getInflateHelper().preload(recyclerView, getItemLayoutId(0));    }    @Override    protected View inflateView(@NonNull ViewGroup parent, int layoutId) {        return getInflateHelper().getView(parent, layoutId);    }}

比照试验

模仿耗时场景

为了可能模仿inflateView的极其状况,这里我简略给inflateView减少300ms的线程sleep来模仿耗时操作。

/** * 模仿耗时加载 */public static View mockLongTimeLoad(@NonNull ViewGroup parent, int layoutId) {    try {        // 模仿耗时        Thread.sleep(300);    } catch (InterruptedException e) {        e.printStackTrace();    }    return LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false);}

对于模仿耗时加载的Adapter,咱们调用下面的办法创立ViewHolder。

public class MockLongTimeLoadListAdapter extends BaseRecyclerAdapter<NewInfo> {    /**     * 这里是加载view的中央, 应用mockLongTimeLoad进行mock     */    @Override    protected View inflateView(@NonNull ViewGroup parent, int layoutId) {        return InflateUtils.mockLongTimeLoad(parent, layoutId);    }}

而对于异步加载的耗时模仿,我则是copy了AsyncLayoutInflater的源码,而后批改了它在InflateThread中的加载办法:

private static class InflateThread extends Thread {    public void runInner() {        // 局部代码省略....        // 模仿耗时加载        request.view = InflateUtils.mockLongTimeLoad(request.inflater.mInflater,                request.parent, request.resid);    }}

比照数据

优化前

优化后

从下面的动图和日志,咱们不难看出在优化前,每个onCreateViewHolder的耗时都在之前设定的300ms以上,这就导致了列表滑动和刷新都会产生比拟显著的卡顿。

而再看优化后的成果,不仅列表滑动和刷新成果十分丝滑,而且每个onCreateViewHolder的耗时都在0ms,极大地晋升了列表的刷新和渲染性能。

总结

置信看完以上内容后,你会发现写了这么多,无非就是把onCreateViewHolder中加载布局的操作提前,并放到了子线程中去解决,其本质仍然是空间换工夫,并将列表数据网络申请到列表刷新这段事务真空的工夫窗口无效利用起来。

本文的全副源码我都放在了github上, 感兴趣的小伙伴能够下下来钻研和学习。

我的项目地址: https://github.com/xuexiangjys/XUI/tree/master/app/src/main/java/com/xuexiang/xuidemo/fragment/components/refresh/sample/preload

我是xuexiangjys,一枚酷爱学习,喜好编程,勤于思考,致力于Android架构钻研以及开源我的项目教训分享的技术up主。获取更多资讯,欢送微信搜寻公众号:【我的Android开源之旅】