RecyclerView性能優(yōu)化之異步預(yù)加載

前言

首先需要強(qiáng)調(diào)的是,這篇文章是對(duì)我之前寫(xiě)的《淺談RecyclerView的性能優(yōu)化》文章的補(bǔ)充,建議大家先讀完這篇文章后再來(lái)看這篇文章,味道更佳。

當(dāng)時(shí)由于篇幅的原因,并沒(méi)有深入展開(kāi)講解,于是有很多感興趣的朋友紛紛留言表示:能不能結(jié)合相關(guān)的示例代碼講解一下到底如何實(shí)現(xiàn)?那么今天我就結(jié)合之前講的如何優(yōu)化onCreateViewHolder的加載時(shí)間,講一講如何實(shí)現(xiàn)onCreateViewHolder的異步預(yù)加載,文章末尾會(huì)給出示例代碼的鏈接地址,希望能給你帶來(lái)啟發(fā)。

分析

之前我們講過(guò),在優(yōu)化onCreateViewHolder方法的時(shí)候,可以降低item的布局層級(jí),可以減少界面創(chuàng)建的渲染時(shí)間,其本質(zhì)就是降低view的inflate時(shí)間。因?yàn)?code>onCreateViewHolder最大的耗時(shí)部分,就是view的inflate。相信讀過(guò)LayoutInflater.inflate源碼的人都知道,這部分的代碼是同步操作,并且涉及到大量的文件IO的操作以及鎖操作,通常來(lái)說(shuō)這部分的代碼快的也需要幾毫秒,慢的可能需要幾十毫秒乃至上百毫秒也是很有可能的。 如果真到了每個(gè)ItemView的inflate需要花上上百毫秒的話(huà),那么在大數(shù)據(jù)量的RecyclerView進(jìn)行快速上下滑動(dòng)的時(shí)候,就必然會(huì)導(dǎo)致界面的滑動(dòng)卡頓、不流暢。

那么如何你的程序里真的有這樣一個(gè)列表,它的每個(gè)ItemView都需要花上上百毫秒的時(shí)間去inflate的話(huà),你該怎么做?

  • 首先就是對(duì)布局進(jìn)行優(yōu)化,降低item的布局層級(jí)。但這點(diǎn)的優(yōu)化往往是微乎其微的。
  • 其次可能就是想辦法讓設(shè)計(jì)師重新設(shè)計(jì),將布局中的某些內(nèi)容刪除或者折疊了,對(duì)暫不展示的內(nèi)容使用ViewStub進(jìn)行延遲加載。不過(guò)說(shuō)實(shí)在話(huà),你既然有能力讓設(shè)計(jì)師重新設(shè)計(jì)的話(huà),還干個(gè)球的開(kāi)發(fā)啊,直接當(dāng)項(xiàng)目經(jīng)理不香嗎?
  • 最后你可能會(huì)考慮不用xml寫(xiě)布局,改為使用代碼自己一個(gè)一個(gè)new布局。話(huà)說(shuō)回來(lái)了,一個(gè)使用xml加載的布局都要花上上百毫秒的布局,可能xml都快上千行下去了,你確定要自己一個(gè)一個(gè)new下去?

以上的方式,都是建立在列表布局可以修改的情況下,如果我們使用的列表布局是第三方已經(jīng)提供好的呢?(例如廣告SDK等)

那么有沒(méi)有什么辦法既可以不用修改當(dāng)前的xml布局,又可以極大地縮短布局的加載時(shí)間呢?毫無(wú)疑問(wèn),布局異步加載將為你打開(kāi)新的世界。

原理

Google官方很早就發(fā)現(xiàn)了XML布局加載的性能問(wèn)題,于是在androidx中提供了異步加載工具AsyncLayoutInflater。其本質(zhì)就是開(kāi)了一個(gè)長(zhǎng)期等待的異步線(xiàn)程,在子線(xiàn)程中inflate view,然后把加載好的view通過(guò)接口拋出去,完成view的加載。

一般來(lái)說(shuō),對(duì)于復(fù)雜的列表,往往都對(duì)應(yīng)了復(fù)雜的數(shù)據(jù),而這復(fù)雜的數(shù)據(jù)往往又是通過(guò)服務(wù)器獲取而來(lái)。所以一般來(lái)說(shuō),一個(gè)列表在加載前,往往先需要訪問(wèn)服務(wù)器獲取數(shù)據(jù),然后再刷新列表顯示,而這訪問(wèn)服務(wù)器的時(shí)間大約也在300ms~1000ms之間。很多開(kāi)發(fā)人員對(duì)這段時(shí)間往往沒(méi)有加以利用,只是加上一個(gè)loading動(dòng)畫(huà)了事。

其實(shí)對(duì)于這一段事務(wù)真空的時(shí)間窗口,我們可以提前進(jìn)行列表的ItemView的加載,這樣等數(shù)據(jù)請(qǐng)求下來(lái)刷新列表的時(shí)候,我們onCreateViewHolder的時(shí)候就可以直接到已經(jīng)事先預(yù)加載好的View緩存池中直接獲取View傳到ViewHolder中使用,這樣onCreateViewHolder的創(chuàng)建時(shí)間幾乎耗時(shí)為0,從而極大地提升了列表的加載和渲染速度。詳細(xì)的流程可以參見(jiàn)下圖:

預(yù)加載流程圖.png

實(shí)現(xiàn)

上面我簡(jiǎn)單地講解了一下原理,下一步就是考慮如何實(shí)現(xiàn)這樣的效果了。

預(yù)加載緩存池

首先在預(yù)加載前,我們需要先創(chuàng)建一個(gè)緩存池來(lái)存儲(chǔ)預(yù)加載的View對(duì)象。

這里我選擇使用SparseArray進(jìn)行存儲(chǔ),key是Int型,存放布局資源的layoutId,value是Object型,存放的是這類(lèi)布局加載View的集合。

這里的集合類(lèi)型我選擇的是LinkedList,因?yàn)槲覀兊木彺嫘枰l繁的添加和刪除操作,并且LinkedList實(shí)現(xiàn)了Deque接口,具備先入先出的能力。

這里View的引用我選擇的是軟引用SoftReference,之所以不采用WeakReference, 目的就是希望緩存能多存在一段時(shí)間,避免內(nèi)存的頻繁釋放和回收造成內(nèi)存的抖動(dòng)。

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方法我們可以看出,這里對(duì)于ViewCache來(lái)說(shuō),每次取出一個(gè)緩存View使用的是pop方法,我們都會(huì)將它從Pool中移除。

布局加載者

因?yàn)関iew的加載方法,涉及到三個(gè)參數(shù): 資源Id-resourceId, 父布局-root和是否添加到根布局-attachToRoot。

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

這里在onCreateViewHolder方法中attachToRoot恒為false,因此異步布局加載只需要前面兩個(gè)參數(shù)以及一個(gè)回調(diào)接口即可,即如下的定義:

public interface ILayoutInflater {
    /**
     * 異步加載View
     *
     * @param parent   父布局
     * @param layoutId 布局資源id
     * @param callback 加載回調(diào)
     */
    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);
}

至于接口實(shí)現(xiàn)的話(huà),就直接使用Google官方提供的異步加載工具AsyncLayoutInflater來(lái)實(shí)現(xiàn)。

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);
    }
}

預(yù)加載輔助類(lèi)

有了預(yù)加載緩存池ViewCache和異步加載能力的提供者IAsyncInflater,下面就是來(lái)協(xié)調(diào)這兩者進(jìn)行合作,完成布局的預(yù)加載和View的讀取。

首先需要定義的是根據(jù)ViewGroup和layoutId獲取View的方法,提供給Adapter的onCreateViewHolder方法使用。

  • 首先我們需要去ViewCache中去取是否已有預(yù)加載好的view供我們使用。如果有則取出,并進(jìn)行一次預(yù)加載補(bǔ)充給ViewCache。
  • 如果沒(méi)有,就只能同步加載布局了。
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);
}

對(duì)于預(yù)加載布局,并加入緩存的方法實(shí)現(xiàn)。

  • 首先我們需要去ViewCache查詢(xún)當(dāng)前可用緩存的數(shù)量,如果可用緩存的數(shù)量大于等于最大數(shù)量,即不需要進(jìn)行預(yù)加載。
  • 對(duì)于需要預(yù)加載的,需要計(jì)算預(yù)加載的數(shù)量,如果當(dāng)前沒(méi)有強(qiáng)制執(zhí)行的次數(shù),就直接按剩余最大數(shù)量進(jìn)行加載,否則取強(qiáng)制執(zhí)行次數(shù)和剩余最大數(shù)量的最小值進(jìn)行加載。
  • 對(duì)于預(yù)加載完畢獲取的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中執(zhí)行預(yù)加載

有了預(yù)加載輔助類(lèi)PreInflateHelper,下面我們只需要直接調(diào)用它的preload方法和getView方法即可。這里需要注意的是,ViewHolder中ItemView的ViewGroup就是RecyclerView它本身,所以Adapter的構(gòu)造方法需要傳入RecyclerView供預(yù)加載輔助類(lèi)進(jìn)行預(yù)加載。

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);
    }
}

對(duì)比實(shí)驗(yàn)

模擬耗時(shí)場(chǎng)景

為了能夠模擬inflateView的極端情況,這里我簡(jiǎn)單給inflateView增加300ms的線(xiàn)程sleep來(lái)模擬耗時(shí)操作。

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

對(duì)于模擬耗時(shí)加載的Adapter,我們調(diào)用上面的方法創(chuàng)建ViewHolder。

public class MockLongTimeLoadListAdapter extends BaseRecyclerAdapter<NewInfo> {
    /**
     * 這里是加載view的地方, 使用mockLongTimeLoad進(jìn)行mock
     */
    @Override
    protected View inflateView(@NonNull ViewGroup parent, int layoutId) {
        return InflateUtils.mockLongTimeLoad(parent, layoutId);
    }
}

而對(duì)于異步加載的耗時(shí)模擬,我則是copy了AsyncLayoutInflater的源碼,然后修改了它在InflateThread中的加載方法:

private static class InflateThread extends Thread {
    public void runInner() {
        // 部分代碼省略....
        // 模擬耗時(shí)加載
        request.view = InflateUtils.mockLongTimeLoad(request.inflater.mInflater,
                request.parent, request.resid);
    }
}

對(duì)比數(shù)據(jù)

優(yōu)化前

優(yōu)化前.gif
優(yōu)化前日志.png

優(yōu)化后

優(yōu)化后.gif
優(yōu)化后日志.png

從上面的動(dòng)圖和日志,我們不難看出在優(yōu)化前,每個(gè)onCreateViewHolder的耗時(shí)都在之前設(shè)定的300ms以上,這就導(dǎo)致了列表滑動(dòng)和刷新都會(huì)產(chǎn)生比較明顯的卡頓。

而再看優(yōu)化后的效果,不僅列表滑動(dòng)和刷新效果非常絲滑,而且每個(gè)onCreateViewHolder的耗時(shí)都在0ms,極大地提升了列表的刷新和渲染性能。

總結(jié)

相信看完以上內(nèi)容后,你會(huì)發(fā)現(xiàn)寫(xiě)了這么多,無(wú)非就是把onCreateViewHolder中加載布局的操作提前,并放到了子線(xiàn)程中去處理,其本質(zhì)依然是空間換時(shí)間,并將列表數(shù)據(jù)網(wǎng)絡(luò)請(qǐng)求到列表刷新這段事務(wù)真空的時(shí)間窗口有效利用起來(lái)。

本文的全部源碼我都放在了github上, 感興趣的小伙伴可以下下來(lái)研究和學(xué)習(xí)。

項(xiàng)目地址: https://github.com/xuexiangjys/XUI/tree/master/app/src/main/java/com/xuexiang/xuidemo/fragment/components/refresh/sample/preload

我是xuexiangjys,一枚熱愛(ài)學(xué)習(xí),愛(ài)好編程,勤于思考,致力于Android架構(gòu)研究以及開(kāi)源項(xiàng)目經(jīng)驗(yàn)分享的技術(shù)up主。獲取更多資訊,歡迎微信搜索公眾號(hào):【我的Android開(kāi)源之旅】

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容