乐趣区

关于android:RecyclerView性能优化之异步预加载

前言

首先须要强调的是,这篇文章是对我之前写的《浅谈 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 开源之旅】

退出移动版