Paging Library原始碼淺析

順其自然55發表於2019-02-26

首先還是放上官方文件地址:developer.android.google.cn/topic/libra…

如果你還不知道這個庫是可以做什麼事,可以看官方文件事例或者參考我之前寫的這篇文章:

juejin.im/post/5a066b….

目前該庫還處於測試階段,所以我就按上篇文章的內容進行部分簡單的原始碼分析,從中你應該可以瞭解到該庫的設計思路.

直接進入正題,先寫簡單的demo,首先建立我們的model.

MainData:

public class MainData extends AndroidViewModel {

    /**
     * 每次需要10個資料.
     */
    private static final int NEED_NUMBER = 10;

    /**
     * 福利第一頁.
     */
    private static final int PAGE_FIRST = 1;

    /**
     * 分頁.
     */
    private int mPage = PAGE_FIRST;


    /**
     * 列表資料.
     */
    private LiveData<PagedList<GankData>> mDataLiveData;

    public MainData(@NonNull Application application) {
        super(application);
    }

    public LiveData<PagedList<GankData>> getDataLiveData() {
        initPageList();
        return mDataLiveData;
    }

    /**
     * 初始化pageList.
     */
    private void initPageList() {
        //獲取dataSource,列表資料都從這裡獲取,
        final DataSource<Integer, GankData> tiledDataSource = new TiledDataSource<GankData>() {
            /**
             * 需要的總個數,如果數量不定,就傳COUNT_UNDEFINED.
             */
            @Override
            public int countItems() {
                return DataSource.COUNT_UNDEFINED;
            }

            /**
             * 返回需要載入的資料.
             * 這裡是線上程非同步中執行的,所以我們可以同步請求資料並且返回
             * @param startPosition 現在第幾個資料
             * @param count 載入的資料數量
             */
            @Override
            public List<GankData> loadRange(int startPosition, int count) {
                List<GankData> gankDataList = new ArrayList<>();

                //這裡我們用retrofit獲取資料,每次獲取十條資料,數量不為空,則讓mPage+1
                try {
                    Response<BaseResponse<List<GankData>>> execute = RetrofitApi.getInstance().mRetrofit.create(AppService.class)
                            .getWelfare1(mPage, NEED_NUMBER).execute();
                    gankDataList.addAll(execute.body().getResults());

                    if (!gankDataList.isEmpty()) {
                        mPage++;
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }

                return gankDataList;
            }
        };

        //這裡我們建立LiveData<PagedList<GankData>>資料,
        mDataLiveData = new LivePagedListProvider<Integer, GankData>() {

            @Override
            protected DataSource<Integer, GankData> createDataSource() {
                return tiledDataSource;
            }
        }.create(0, new PagedList.Config.Builder()
                .setPageSize(NEED_NUMBER) //每次載入的資料數量
                //距離本頁資料幾個時候開始載入下一頁資料(例如現在載入10個資料,設定prefetchDistance為2,則滑到第八個資料時候開始載入下一頁資料).
                .setPrefetchDistance(NEED_NUMBER)
                //這裡設定是否設定PagedList中的佔位符,如果設定為true,我們的資料數量必須固定,由於網路資料數量不固定,所以設定false.
                .setEnablePlaceholders(false)
                .build());
    }
}複製程式碼

這裡我們需要建立LiveData<PagedList<GankData>>這個物件,對於LiveData之前已經講過了,如果你還不知道這個原理,可以去看下我之前文章:juejin.im/post/5a03ed…

首先我們來看下PageList到底是什麼:首先PageList是個抽象類,並且提供Build模式填充資料,首先看下它的build類:

public static class Builder<Key, Value> {
    private DataSource<Key, Value> mDataSource;
    private Executor mMainThreadExecutor;
    private Executor mBackgroundThreadExecutor;
    private Config mConfig;
    private Key mInitialKey;

    /**
     * Creates a {@link PagedList} with the given parameters.
     * <p>
     * This call will initial data and perform any counting needed to initialize the PagedList,
     * therefore it should only be called on a worker thread.
     * <p>
     * While build() will always return a PagedList, it's important to note that the PagedList
     * initial load may fail to acquire data from the DataSource. This can happen for example if
     * the DataSource is invalidated during its initial load. If this happens, the PagedList
     * will be immediately {@link PagedList#isDetached() detached}, and you can retry
     * construction (including setting a new DataSource).
     *
     * @return The newly constructed PagedList
     */
    @WorkerThread
    @NonNull
    public PagedList<Value> build() {
        if (mDataSource == null) {
            throw new IllegalArgumentException("DataSource required");
        }
        if (mMainThreadExecutor == null) {
            throw new IllegalArgumentException("MainThreadExecutor required");
        }
        if (mBackgroundThreadExecutor == null) {
            throw new IllegalArgumentException("BackgroundThreadExecutor required");
        }
        if (mConfig == null) {
            throw new IllegalArgumentException("Config required");
        }

        return PagedList.create(
                mDataSource,
                mMainThreadExecutor,
                mBackgroundThreadExecutor,
                mConfig,
                mInitialKey);
    }複製程式碼

這裡我們看到必須填的資料有至少有四個,第一個DataSource,不用說,自己手動要建立的,兩個執行緒池,一個Config和一個key,我們再來看下Config:

public static class Config {
    final int mPageSize;
    final int mPrefetchDistance;
    final boolean mEnablePlaceholders;
    final int mInitialLoadSizeHint;

    private Config(int pageSize, int prefetchDistance,
            boolean enablePlaceholders, int initialLoadSizeHint) {
        mPageSize = pageSize;
        mPrefetchDistance = prefetchDistance;
        mEnablePlaceholders = enablePlaceholders;
        mInitialLoadSizeHint = initialLoadSizeHint;
    }

    /**
     * Builder class for {@link Config}.
     * <p>
     * You must at minimum specify page size with {@link #setPageSize(int)}.
     */
    public static class Builder {
        private int mPageSize = -1;
        private int mPrefetchDistance = -1;
        private int mInitialLoadSizeHint = -1;
        private boolean mEnablePlaceholders = true;

        /**
         * Creates a {@link Config} with the given parameters.
         *
         * @return A new Config.
         */
        public Config build() {
            if (mPageSize < 1) {
                throw new IllegalArgumentException("Page size must be a positive number");
            }
            if (mPrefetchDistance < 0) {
                mPrefetchDistance = mPageSize;
            }
            if (mInitialLoadSizeHint < 0) {
                mInitialLoadSizeHint = mPageSize * 3;
            }
            if (!mEnablePlaceholders && mPrefetchDistance == 0) {
                throw new IllegalArgumentException("Placeholders and prefetch are the only ways"
                        + " to trigger loading of more data in the PagedList, so either"
                        + " placeholders must be enabled, or prefetch distance must be > 0.");
            }

            return new Config(mPageSize, mPrefetchDistance,
                    mEnablePlaceholders, mInitialLoadSizeHint);
        }
    }複製程式碼

這裡同樣採用Build模式,看下引數:mPageSize代表每次載入數量,mPrefetchDistance代表距離最後多少item數量開始載入下一頁,mInittialLoadSizeHint代表首次載入數量(但是需要配合KeyedDataSource,我們現在用TiledDataSource,所以忽略這個屬性),mEnablePlaceholders代表是否設定null佔位符,需要載入固定數量時候可以設定為true,如果數量不固定則設為false.

因為後面涉及到DataSource,所以我們接下來先分析TiledDataSource.

TiledDataSource的父類DataSource.

// Since we currently rely on implementation details of two implementations,
// prevent external subclassing, except through exposed subclasses
DataSource() {
}

/**
 * 數量不定的標誌.
 */
@SuppressWarnings("WeakerAccess")
public static int COUNT_UNDEFINED = -1;

/**
 * 總數量(數量不定返回COUNT_UNDEFINED).
 */
@WorkerThread
public abstract int countItems();

/**
 * Returns true if the data source guaranteed to produce a contiguous set of items,
 * never producing gaps.
 */
abstract boolean isContiguous();

/**
 * Invalidation callback for DataSource.
 * <p>
 * Used to signal when a DataSource a data source has become invalid, and that a new data source
 * is needed to continue loading data.
 */
public interface InvalidatedCallback {
    /**
     * Called when the data backing the list has become invalid. This callback is typically used
     * to signal that a new data source is needed.
     * <p>
     * This callback will be invoked on the thread that calls {@link #invalidate()}. It is valid
     * for the data source to invalidate itself during its load methods, or for an outside
     * source to invalidate it.
     */
    @AnyThread
    void onInvalidated();
}

/**
 * 資料可用的標誌.
 */
private AtomicBoolean mInvalid = new AtomicBoolean(false);

/**
 * 回撥的執行緒安全集合.
 */
private CopyOnWriteArrayList<InvalidatedCallback> mOnInvalidatedCallbacks =
        new CopyOnWriteArrayList<>();

/**
 * Add a callback to invoke when the DataSource is first invalidated.
 * <p>
 * Once invalidated, a data source will not become valid again.
 * <p>
 * A data source will only invoke its callbacks once - the first time {@link #invalidate()}
 * is called, on that thread.
 *
 * @param onInvalidatedCallback The callback, will be invoked on thread that
 *                              {@link #invalidate()} is called on.
 */
@AnyThread
@SuppressWarnings("WeakerAccess")
public void addInvalidatedCallback(InvalidatedCallback onInvalidatedCallback) {
    mOnInvalidatedCallbacks.add(onInvalidatedCallback);
}

/**
 * Remove a previously added invalidate callback.
 *
 * @param onInvalidatedCallback The previously added callback.
 */
@AnyThread
@SuppressWarnings("WeakerAccess")
public void removeInvalidatedCallback(InvalidatedCallback onInvalidatedCallback) {
    mOnInvalidatedCallbacks.remove(onInvalidatedCallback);
}

/**
 * 對外方法,執行回撥(但是未發現任何地方呼叫了這個方法,估計是為了以後擴充套件用).
 */
@AnyThread
public void invalidate() {
    if (mInvalid.compareAndSet(false, true)) {
        for (InvalidatedCallback callback : mOnInvalidatedCallbacks) {
            callback.onInvalidated();
        }
    }
}

/**
 * Returns true if the data source is invalid, and can no longer be queried for data.
 *
 * @return True if the data source is invalid, and can no longer return data.
 */
@WorkerThread
public boolean isInvalid() {
    return mInvalid.get();
}複製程式碼

這裡需要先說下AtomicBoolean和CopyOrWriteArrayList,

AtomicBoolean:按照文件說明,大概意思就是以原子的方式更新bollean值,其中

  • compareAndSet(boolean expect, boolean update)這個方法,
  • 這個方法主要兩個作用 1. 比較AtomicBoolean和expect的值,如果一致,執行方法內的語句。其實就是一個if語句 2. 把AtomicBoolean的值設成update 比較最要的是這兩件事是一氣呵成的,這連個動作之間不會被打斷,任何內部或者外部的語句都不可能在兩個動作之間執行。為多執行緒的控制提供瞭解決的方案。

CopyOrWriteArrayList是一個執行緒安全的list,它的 add和remove等一些方法都是加鎖了.

再來看下TiledDataSource:

public abstract class TiledDataSource<Type> extends DataSource<Integer, Type> {

    @WorkerThread
    @Override
    public abstract int countItems();

    @Override
    boolean isContiguous() {
        return false;
    }

    @WorkerThread
    public abstract List<Type> loadRange(int startPosition, int count);

    final List<Type> loadRangeWrapper(int startPosition, int count) {
        if (isInvalid()) {
            return null;
        }
        List<Type> list = loadRange(startPosition, count);
        if (isInvalid()) {
            return null;
        }
        return list;
    }

    ContiguousDataSource<Integer, Type> getAsContiguous() {
        return new TiledAsBoundedDataSource<>(this);
    }

    /**
     * BoundedDataSource是最終繼承ContiguousDataSource的類,這個負責包裝TiledDataSource.
     */
    static class TiledAsBoundedDataSource<Value> extends BoundedDataSource<Value> {
        final TiledDataSource<Value> mTiledDataSource;

        TiledAsBoundedDataSource(TiledDataSource<Value> tiledDataSource) {
            mTiledDataSource = tiledDataSource;
        }

        @WorkerThread
        @Nullable
        @Override
        public List<Value> loadRange(int startPosition, int loadCount) {
            return mTiledDataSource.loadRange(startPosition, loadCount);
        }
    }
}複製程式碼

loadRange方法我們用不到暫時(沒發現呼叫這個方法的地方),我們只需要實現countItemsloadRange方法,我們例子中是無限制數量,所以傳了-1,而loadRange方法是在非同步執行緒中執行,所以這裡可以用網路同步請求返回資料.

然後接下來看PagedList的create方法.

@NonNull
private static <K, T> PagedList<T> create(@NonNull DataSource<K, T> dataSource,
        @NonNull Executor mainThreadExecutor,
        @NonNull Executor backgroundThreadExecutor,
        @NonNull Config config,
        @Nullable K key) {
    if (dataSource.isContiguous() || !config.mEnablePlaceholders) {
        if (!dataSource.isContiguous()) {
            //noinspection unchecked
            dataSource = (DataSource<K, T>) ((TiledDataSource<T>) dataSource).getAsContiguous();
        }
        ContiguousDataSource<K, T> contigDataSource = (ContiguousDataSource<K, T>) dataSource;
        return new ContiguousPagedList<>(contigDataSource,
                mainThreadExecutor,
                backgroundThreadExecutor,
                config,
                key);
    } else {
        return new TiledPagedList<>((TiledDataSource<T>) dataSource,
                mainThreadExecutor,
                backgroundThreadExecutor,
                config,
                (key != null) ? (Integer) key : 0);
    }
}
複製程式碼

根據一系列條件,我們獲取的PagedList是ContiguousPagedList.

然後我們可以來看LiveData<PagedList<GankData>>是怎麼建立的了:

來看下LivePagedListProvider:

public abstract class LivePagedListProvider<Key, Value> {

    /**
     * Construct a new data source to be wrapped in a new PagedList, which will be returned
     * through the LiveData.
     *
     * @return The data source.
     */
    @WorkerThread
    protected abstract DataSource<Key, Value> createDataSource();

    /**
     * Creates a LiveData of PagedLists, given the page size.
     * <p>
     * This LiveData can be passed to a {@link PagedListAdapter} to be displayed with a
     * {@link android.support.v7.widget.RecyclerView}.
     *
     * @param initialLoadKey Initial key used to load initial data from the data source.
     * @param pageSize Page size defining how many items are loaded from a data source at a time.
     *                 Recommended to be multiple times the size of item displayed at once.
     *
     * @return The LiveData of PagedLists.
     */
    @AnyThread
    @NonNull
    public LiveData<PagedList<Value>> create(@Nullable Key initialLoadKey, int pageSize) {
        return create(initialLoadKey,
                new PagedList.Config.Builder()
                        .setPageSize(pageSize)
                        .build());
    }

    /**
     * Creates a LiveData of PagedLists, given the PagedList.Config.
     * <p>
     * This LiveData can be passed to a {@link PagedListAdapter} to be displayed with a
     * {@link android.support.v7.widget.RecyclerView}.
     *
     * @param initialLoadKey Initial key to pass to the data source to initialize data with.
     * @param config PagedList.Config to use with created PagedLists. This specifies how the
     *               lists will load data.
     *
     * @return The LiveData of PagedLists.
     */
    @AnyThread
    @NonNull
    public LiveData<PagedList<Value>> create(@Nullable final Key initialLoadKey,
            final PagedList.Config config) {
        return new ComputableLiveData<PagedList<Value>>() {
            @Nullable
            private PagedList<Value> mList;
            @Nullable
            private DataSource<Key, Value> mDataSource;

            private final DataSource.InvalidatedCallback mCallback =
                    new DataSource.InvalidatedCallback() {
                @Override
                public void onInvalidated() {
                    invalidate();
                }
            };

            @Override
            protected PagedList<Value> compute() {
                @Nullable Key initializeKey = initialLoadKey;
                if (mList != null) {
                    //noinspection unchecked
                    initializeKey = (Key) mList.getLastKey();
                }

                do {
                    if (mDataSource != null) {
                        mDataSource.removeInvalidatedCallback(mCallback);
                    }

                    mDataSource = createDataSource();
                    mDataSource.addInvalidatedCallback(mCallback);

                    mList = new PagedList.Builder<Key, Value>()
                            .setDataSource(mDataSource)
                            .setMainThreadExecutor(ArchTaskExecutor.getMainThreadExecutor())
                            .setBackgroundThreadExecutor(
                                    ArchTaskExecutor.getIOThreadExecutor())
                            .setConfig(config)
                            .setInitialKey(initializeKey)
                            .build();
                } while (mList.isDetached());
                return mList;
            }
        }.getLiveData();
    }
複製程式碼

程式碼很簡單(忽略那些mCallback,現階段完全用不到),只要我們重寫傳入DataSource,建立的主要類是ComputableLiveData幹得,看一下:

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public abstract class ComputableLiveData<T> {

    private final LiveData<T> mLiveData;

    private AtomicBoolean mInvalid = new AtomicBoolean(true);
    private AtomicBoolean mComputing = new AtomicBoolean(false);

    /**
     * Creates a computable live data which is computed when there are active observers.
     * <p>
     * It can also be invalidated via {@link #invalidate()} which will result in a call to
     * {@link #compute()} if there are active observers (or when they start observing)
     */
    @SuppressWarnings("WeakerAccess")
    public ComputableLiveData() {
        mLiveData = new LiveData<T>() {
            @Override
            protected void onActive() {
                // TODO if we make this class public, we should accept an executor
                ArchTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable);
            }
        };
    }

    /**
     * Returns the LiveData managed by this class.
     *
     * @return A LiveData that is controlled by ComputableLiveData.
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public LiveData<T> getLiveData() {
        return mLiveData;
    }

    /**
     * mInvalid預設為true,這裡可以一條線執行下去,PagedList就是上面那個compute裡新建的物件,
     * 並且執行了postValue方法,觀察者那邊可以就收到通知了.
     */
    @VisibleForTesting
    final Runnable mRefreshRunnable = new Runnable() {
        @WorkerThread
        @Override
        public void run() {
            boolean computed;
            do {
                computed = false;
                // compute can happen only in 1 thread but no reason to lock others.
                if (mComputing.compareAndSet(false, true)) {
                    // as long as it is invalid, keep computing.
                    try {
                        T value = null;
                        while (mInvalid.compareAndSet(true, false)) {
                            computed = true;
                            value = compute();
                        }
                        if (computed) {
                            mLiveData.postValue(value);
                        }
                    } finally {
                        // release compute lock
                        mComputing.set(false);
                    }
                }
                // check invalid after releasing compute lock to avoid the following scenario.
                // Thread A runs compute()
                // Thread A checks invalid, it is false
                // Main thread sets invalid to true
                // Thread B runs, fails to acquire compute lock and skips
                // Thread A releases compute lock
                // We've left invalid in set state. The check below recovers.
            } while (computed && mInvalid.get());
        }
    };

    // invalidation check always happens on the main thread
    @VisibleForTesting
    final Runnable mInvalidationRunnable = new Runnable() {
        @MainThread
        @Override
        public void run() {
            boolean isActive = mLiveData.hasActiveObservers();
            if (mInvalid.compareAndSet(false, true)) {
                if (isActive) {
                    // TODO if we make this class public, we should accept an executor.
                    ArchTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable);
                }
            }
        }
    };
}
複製程式碼

程式碼也是很簡單的,先看構造方法,構造方法中直接新建了LiveData,並且在生命週期onStrart後執行mRefreshRunable執行緒,這個執行緒直接給LiveData賦值,然後activity註冊的地方就可以收到PagedList的回撥了.

我們再來看看構造PagedList的預設執行緒池,追蹤程式碼:

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class DefaultTaskExecutor extends TaskExecutor {
    private final Object mLock = new Object();
    private ExecutorService mDiskIO = Executors.newFixedThreadPool(2);

    @Nullable
    private volatile Handler mMainHandler;

    @Override
    public void executeOnDiskIO(Runnable runnable) {
        mDiskIO.execute(runnable);
    }

    @Override
    public void postToMainThread(Runnable runnable) {
        if (mMainHandler == null) {
            synchronized (mLock) {
                if (mMainHandler == null) {
                    mMainHandler = new Handler(Looper.getMainLooper());
                }
            }
        }
        //noinspection ConstantConditions
        mMainHandler.post(runnable);
    }

    @Override
    public boolean isMainThread() {
        return Looper.getMainLooper().getThread() == Thread.currentThread();
    }
}
複製程式碼

就是這個Executors.newFixedThreadPool(2)執行緒池了,

接下來我們看PagedList:

public abstract class PagedListAdapter<T, VH extends RecyclerView.ViewHolder>
        extends RecyclerView.Adapter<VH> {
    private final PagedListAdapterHelper<T> mHelper;

    /**
     * Creates a PagedListAdapter with default threading and
     * {@link android.support.v7.util.ListUpdateCallback}.
     *
     * Convenience for {@link #PagedListAdapter(ListAdapterConfig)}, which uses default threading
     * behavior.
     *
     * @param diffCallback The {@link DiffCallback} instance to compare items in the list.
     */
    protected PagedListAdapter(@NonNull DiffCallback<T> diffCallback) {
        mHelper = new PagedListAdapterHelper<>(this, diffCallback);
    }

    @SuppressWarnings("unused, WeakerAccess")
    protected PagedListAdapter(@NonNull ListAdapterConfig<T> config) {
        mHelper = new PagedListAdapterHelper<>(new ListAdapterHelper.AdapterCallback(this), config);
    }

    /**
     * Set the new list to be displayed.
     * <p>
     * If a list is already being displayed, a diff will be computed on a background thread, which
     * will dispatch Adapter.notifyItem events on the main thread.
     *
     * @param pagedList The new list to be displayed.
     */
    public void setList(PagedList<T> pagedList) {
        mHelper.setList(pagedList);
    }

    @Nullable
    protected T getItem(int position) {
        return mHelper.getItem(position);
    }

    @Override
    public int getItemCount() {
        return mHelper.getItemCount();
    }

    /**
     * Returns the list currently being displayed by the Adapter.
     * <p>
     * This is not necessarily the most recent list passed to {@link #setList(PagedList)}, because a
     * diff is computed asynchronously between the new list and the current list before updating the
     * currentList value.
     *
     * @return The list currently being displayed.
     */
    @Nullable
    public PagedList<T> getCurrentList() {
        return mHelper.getCurrentList();
    }
}複製程式碼

可以看到,PagedListAdapter只是個殼,做事的是PagedListAdapterHelper.

我們主要看PagedListAdapterHelper的這個幾個方法:

public void setList(final PagedList<T> pagedList) {
    if (pagedList != null) {
        if (mList == null) {
            mIsContiguous = pagedList.isContiguous();
        } else {
            if (pagedList.isContiguous() != mIsContiguous) {
                throw new IllegalArgumentException("AdapterHelper cannot handle both contiguous"
                        + " and non-contiguous lists.");
            }
        }
    }

    if (pagedList == mList) {
        // nothing to do
        return;
    }

    // incrementing generation means any currently-running diffs are discarded when they finish
    final int runGeneration = ++mMaxScheduledGeneration;

    if (pagedList == null) {
        mUpdateCallback.onRemoved(0, mList.size());
        mList.removeWeakCallback(mPagedListCallback);
        mList = null;
        return;
    }

    if (mList == null) {
        // fast simple first insert
        mUpdateCallback.onInserted(0, pagedList.size());
        mList = pagedList;
        pagedList.addWeakCallback(null, mPagedListCallback);
        return;
    }

    if (!mList.isImmutable()) {
        // first update scheduled on this list, so capture mPages as a snapshot, removing
        // callbacks so we don't have resolve updates against a moving target
        mList.removeWeakCallback(mPagedListCallback);
        mList = (PagedList<T>) mList.snapshot();
    }

    final PagedList<T> oldSnapshot = mList;
    final List<T> newSnapshot = pagedList.snapshot();
    mUpdateScheduled = true;
    mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
        @Override
        public void run() {
            final DiffUtil.DiffResult result;
            if (mIsContiguous) {
                result = ContiguousDiffHelper.computeDiff(
                        (NullPaddedList<T>) oldSnapshot, (NullPaddedList<T>) newSnapshot,
                        mConfig.getDiffCallback(), true);
            } else {
                result = SparseDiffHelper.computeDiff(
                        (PageArrayList<T>) oldSnapshot, (PageArrayList<T>) newSnapshot,
                        mConfig.getDiffCallback(), true);
            }

            mConfig.getMainThreadExecutor().execute(new Runnable() {
                @Override
                public void run() {
                    if (mMaxScheduledGeneration == runGeneration) {
                        mUpdateScheduled = false;
                        latchPagedList(pagedList, newSnapshot, result);
                    }
                }
            });
        }
    });
}

private void latchPagedList(
        PagedList<T> newList, List<T> diffSnapshot,
        DiffUtil.DiffResult diffResult) {
    if (mIsContiguous) {
        ContiguousDiffHelper.dispatchDiff(mUpdateCallback,
                (NullPaddedList<T>) mList, (ContiguousPagedList<T>) newList, diffResult);
    } else {
        SparseDiffHelper.dispatchDiff(mUpdateCallback, diffResult);
    }
    mList = newList;
    newList.addWeakCallback((PagedList<T>) diffSnapshot, mPagedListCallback);
}複製程式碼

這個理解成給adapter設定集合,理論上設定一次,這裡對於多次設定集合做了重新整理的比較處理。這裡還是用到了RecylerView的 DifffUtil工具.

當然這個庫的主要功能是在未滑倒底部時候就進行資料載入,讓使用者無感知載入,這個處理方法就在getItem(int index)裡:

@SuppressWarnings("WeakerAccess")
@Nullable
public T getItem(int index) {
    if (mList == null) {
        throw new IndexOutOfBoundsException("Item count is zero, getItem() call is invalid");
    }

    mList.loadAround(index);
    return mList.get(index);
}複製程式碼

recyclerView滑動載入到第幾個資料時候就會呼叫adapter的getItem方法,通過判斷當前載入的index位置,還有使用者設定的預載入位置,從而進行提前載入資料。這裡呼叫的是PagedList的loadAround方法,現在我們去找這個方法的實現在哪:

ContiguousPageList:

@Override
public void loadAround(int index) {
    mLastLoad = index + mPositionOffset;

    int prependItems = mConfig.mPrefetchDistance - (index - mLeadingNullCount);
    int appendItems = index + mConfig.mPrefetchDistance - (mLeadingNullCount + mList.size());

    mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
    if (mPrependItemsRequested > 0) {
        schedulePrepend();
    }

    mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
    if (mAppendItemsRequested > 0) {
        scheduleAppend();
    }
}

@MainThread
private void schedulePrepend() {
    if (mPrependWorkerRunning) {
        return;
    }
    mPrependWorkerRunning = true;

    final int position = mLeadingNullCount + mPositionOffset;
    final T item = mList.get(0);
    mBackgroundThreadExecutor.execute(new Runnable() {
        @Override
        public void run() {
            if (mDetached.get()) {
                return;
            }

            final List<T> data = mDataSource.loadBefore(position, item, mConfig.mPageSize);
            if (data != null) {
                mMainThreadExecutor.execute(new Runnable() {
                    @Override
                    public void run() {
                        if (mDetached.get()) {
                            return;
                        }
                        prependImpl(data);
                    }
                });
            } else {
                detach();
            }
        }
    });
}

@MainThread
private void prependImpl(List<T> before) {
    final int count = before.size();
    if (count == 0) {
        // Nothing returned from source, stop loading in this direction
        return;
    }

    Collections.reverse(before);
    mList.addAll(0, before);

    final int changedCount = Math.min(mLeadingNullCount, count);
    final int addedCount = count - changedCount;

    if (changedCount != 0) {
        mLeadingNullCount -= changedCount;
    }
    mPositionOffset -= addedCount;
    mNumberPrepended += count;


    // only try to post more work after fully prepended (with offsets / null counts updated)
    mPrependItemsRequested -= count;
    mPrependWorkerRunning = false;
    if (mPrependItemsRequested > 0) {
        // not done prepending, keep going
        schedulePrepend();
    }

    // finally dispatch callbacks, after prepend may have already been scheduled
    for (WeakReference<Callback> weakRef : mCallbacks) {
        Callback callback = weakRef.get();
        if (callback != null) {
            if (changedCount != 0) {
                callback.onChanged(mLeadingNullCount, changedCount);
            }
            if (addedCount != 0) {
                callback.onInserted(0, addedCount);
            }
        }
    }
}

複製程式碼

由於我們傳入的是TiledDataSource,所以這些mDataSource的loadBefore和loadAfter等最終都呼叫了TiledDataSource的loadRange方法。

到這裡為止,好像整體流程已經分析完了,整體流程就看這張圖吧:

Paging Library原始碼淺析

首先Recyclerview設定PagedListAdater,PagedListAdapter設定對應的PagedList,每次adapter getItme就讓PagedList知道使用者已經滑到第幾個item,PagedList計算這些數量以及設定的各種條件,條件達成就通知DataSource,讓其返回資料,資料返回成功,通知PagedListAdapter,讓其使用進行高效重新整理.

目前該庫還是測試階段,裡面還有方法回撥都未被用到,本文只是簡單分析下原始碼流程,不過也貌似開啟了一些新的思路.

由於本人水平也不高,文中也有不少理解錯誤之處,也希望能各位指教,一起學習,一起進步.


相關文章