RecyclerView性能优化之异步预加载

news2024/11/24 22:50:41

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开源之旅】

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/685100.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【TCP/IP】广播 - 定义、原理及编程实现

目录 广播 广播的原理及形式 广播的编程与实现 套接字选项设置 发送者 接收者 拓展资料 广播 广播(Broadcast)是指封包在计算机网络中传输时&#xff0c;目的地址为网络中所有设备的一种传输方式。这里所说的“所有设备”也被限定在一个范围之中&#xff0c;这个范围被称…

Android 13(T) - binder阅读(4)- 使用ServiceManager注册服务2

上一篇笔记我们看到了binder_transaction&#xff0c;这个方法很长&#xff0c;这一篇我们将把这个方法拆分开来看binder_transaction做了什么&#xff0c;从而学习binder是如何跨进程通信的。 1 binder_transaction static void binder_transaction(struct binder_proc *proc…

Java 面试知识点合集

一、基础篇 1.1 java基础 1.面向对象的特征&#xff1a;封装、继承、多态 (1).封装&#xff1a;属性能够描述事物的特征&#xff0c;方法能够描述事物的动作。封装就是把同一类事物的共性&#xff08;包括属性和方法&#xff09;归到同一类中&#xff0c;方便使用。 封装的…

音视频处理工具FFmpeg与Java结合的简单使用

一、什么是FFmpeg FFmpeg是一套可以用来记录、转换数字音频、视频&#xff0c;并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec&#xff0c;为了保证高可移植性和编解…

chatgpt赋能python:Python编写n!——让阶乘计算变得更简单

Python编写n!——让阶乘计算变得更简单 阶乘是高中数学中常见的运算&#xff0c;它的含义是从1到n的所有正整数相乘&#xff0c;用叹号表示为n!。例如&#xff0c;5! 1 2 3 4 5 120。在计算机编程中&#xff0c;我们常常需要计算阶乘。而Python作为一门便捷易用的编程语…

chatgpt赋能python:Python编程自动化办公–提升工作效率的利器

Python编程自动化办公 – 提升工作效率的利器 越来越多企业对协作和业务流程的优化提高了要求&#xff0c;自动化办公就是其中之一&#xff0c;而Python编程能够帮助我们实现高效自动化办公。Python是一种多用途&#xff0c;高效的编程语言&#xff0c;被广泛应用于应用程序开…

UE4/5动画系列(3.通过后期处理动画蓝图的头部朝向Actor,两种方法:1.通过动画层接口的look at方法。2.通过control rig的方法)

目录 蓝图 点积dot Yaw判断 后期处理动画蓝图 动画层接口 ControlRig: 蓝图 首先我们创建一个actor类&#xff0c;这个actor类是我们要看的东西&#xff0c;actor在哪&#xff0c;我们的动物就要看到哪里&#xff08;同样&#xff0c;这个我们也是做一个父类&#xff0…

chatgpt赋能python:Python程序员的秘密武器:给不及格成绩加分

Python程序员的秘密武器&#xff1a;给不及格成绩加分 Python是一个语法简洁、易学易用的编程语言&#xff0c;已经成为了很多程序员的首选语言。在学校中&#xff0c;很多学生选择学习Python作为他们的编程课程。然而&#xff0c;有时候即便是刻苦学习&#xff0c;踏实地完成…

3D定制化开发工具HOOPS如何满足EDA设计需求?(上)

HOOPS SDK 是由 Tech Soft 3D 公司开发和提供的一款软件开发工具包。HOOPS SDK 为开发者提供了强大的3D图形渲染和交互功能&#xff0c;用于构建高性能的工程、设计和制造应用程序。其主要功能包括&#xff1a;3D 图形渲染、交互性、数据管理、算法和分析、可定制性等。 HOOPS…

chatgpt赋能python:Python编写一个可以颠倒数组元素的函数

Python编写一个可以颠倒数组元素的函数 在Python编程中&#xff0c;我们经常需要对列表&#xff08;即数组&#xff09;进行操作。其中一个常见的操作就是颠倒列表中各元素的排列顺序。这个操作在很多场景下都有用&#xff0c;例如逆序输出字符串、逆序遍历二叉树等等。在本篇…

Atlas 200I DK A2开发者套件通过路由器联网(360安全路由-V2)

一、参考资料 Windows系统 通过直连路由器连接外部网络 二、准备工作 准备micro SD存储卡&#xff0c;即TF卡&#xff0c;建议128GB以上&#xff1b;准备micro SD读卡器&#xff1b;准备普通网线一根&#xff1b;准备一个路由器&#xff0c; 360安全路由-V2路由器。 三、关键…

机器人开发--SLAM详细介绍

机器人开发--SLAM介绍 1 介绍1.1 概述1.2 发展历程三个时代重要时间节点视觉SLAM分类及里程碑技术发展 1.3 SLAM与各模块关系1.5 SLAM分类1.4 应用领域 2 SLAM框架视觉/惯性SLAM系统框架结构经典框架 3 常见方案3.1 常见激光雷达方案3.2 常见视觉方案3.3 多传感器融合方案 4 地…

chatgpt赋能python:Python中同一变量多次赋值的探讨

Python中同一变量多次赋值的探讨 介绍 Python是一种非常流行的编程语言&#xff0c;具有易于学习和使用、强大的功能和可扩展性、广泛的应用领域等众多优点。在Python中&#xff0c;我们可以对同一变量多次进行赋值&#xff0c;这在某些情况下非常有用。本文将探讨在Python中…

2.3、Bean的管理

一、Bean的装配&#xff08;IOC应用实现&#xff09; 创建应用组件之间的协作的行为通常称为装配&#xff08;wiring&#xff09;。Spring IOC通过应用上下文&#xff08;ApplicationContext&#xff09;装载Bean的定义并把他们组装起来。 Spring应用上下文&#xff08;Applica…

yum安装LNMP

目录 前言 一、yum安装要用在线yum源 二、安装Nginx 1、搭建Nginx环境 2、安装yum 3、查看Nginx是否安装成功 4、设置开机自启 三、安装MySQL 1、除系统中所有以"mariadb"开头的软件包 2、安装MySQL 3、设置开机自启 4、查看MySQL初始密码 5、修改MySQL密码…

第 107 场LeetCode双周赛

A 最大字符串配对数目 显然各字符串对 间匹配的先后顺序不影响最大匹配数目, 可以从后往前遍历数组, 判断前面是否有和当前末尾构成匹配的. class Solution { public:int maximumNumberOfStringPairs(vector<string> &words) {int res 0; while (words.size…

Python基础五

目录 一、Ptyhon数据类型--元组 1.元组的注意事项 2.元组的下标 3.访问元组元素 4.拼接元组 5.删除元组 6.元组运算符 二、Python内置函数--元组相关 一、Ptyhon数据类型--元组 Python 的元组与列表类似&#xff0c;不同之处在于元组的元素不能修改&#xff0c;也不能…

chatgpt赋能python:Python编写预警系统——保障企业安全的得力工具

Python编写预警系统——保障企业安全的得力工具 随着互联网应用的发展&#xff0c;企业所要面对的风险和威胁也与日俱增&#xff0c;预警系统的作用在保障企业安全中越来越显著。Python编写预警系统&#xff0c;成为了许多企业和团队的首选&#xff0c;具有方便快捷、灵活多样…

【软考网络管理员】2023年软考网管初级常见知识考点(13)-ARP、ICMP、IPv6协议详解

#涉及知识点 ARP协议详解、ICMP协议详解、IPv6协议等软考内容详解 软考网络管理员常考知识点&#xff0c;软考网络管理员网络安全&#xff0c;网络管理员考点汇总。 原创于&#xff1a;CSDN博主-《拄杖盲学轻声码》&#xff0c;更多考点汇总可以去他主页查看 文章目录 前言一、…

浅析AI深度学习计算机视觉技术在智能监控领域的场景应用

计算机视觉技术是一种模拟人类视觉功能的技术&#xff0c;通过数字图像处理、模式识别、机器学习等方法&#xff0c;自动分析和理解图像和视频中的信息&#xff0c;从而实现图像和视频的自动理解、识别、分类、检测和跟踪等任务。 计算机视觉技术的使用场景非常广泛&#xff0…