前语

首要需求着重的是,这篇文章是对我之前写的《浅谈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,从而极大地提升了列表的加载和渲染速度。详细的流程能够参见下图:

RecyclerView性能优化之异步预加载

完结

上面我简略地解说了一下原理,下一步便是考虑怎么完结这样的作用了。

预加载缓存池

首要在预加载前,咱们需求先创立一个缓存池来存储预加载的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);
    }
}

比照数据

优化前

RecyclerView性能优化之异步预加载

RecyclerView性能优化之异步预加载

优化后

RecyclerView性能优化之异步预加载

RecyclerView性能优化之异步预加载

从上面的动图和日志,咱们不难看出在优化前,每个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开源之旅】