分頁加載是一種應用很廣泛的數據展示控制機制,相信絕大多數開發者對于這一套機制都非常熟悉。這篇文章的主要目的結合實際的使用場景,對以往在開發中遇到一些概念進行梳理,歸納的同時加深理解,也希望能幫助更多剛剛接觸到開發的同學。
本篇文章不聊怎么具體實現分頁加載,因為現在太多成熟的方案,直接借助第三方的開源庫可以減少很多細節的考慮,重復的造輪子是沒有意義的。我們只需要從基本概念上切入,考慮實際場景的需求,針對一些主要問題進行分化,思考基本的解決方案是如何構建的即可,下面我們來一同進行思考。
適合分頁加載的場景
要實現分頁加載這套機制,在不同終端上的實現可以說是:基本原理相同,只是展示時處理起來有所差異而已。
在前端網頁界面中,通常都是點擊跳轉后到下一頁查看內容,一般都是直接提供了可點擊的頁碼進行跳轉,屬于基本的分頁式。而在移動設備 App 上,交互上主要靠手勢的滑動控制,所以基本都是上滑時分頁加載更多內容,可以說屬于段頁式。
對于移動終端上采用了列表形式( ListView 等等)展示內容時,在數據量較大的情況下,分頁加載具有下面幾個特點:
- 減少初始加載的耗時(網絡加載、數據解析、數據填充等)
- 減小緩存數據時的內存消耗
- 提升數據的實時性(一次請求緩存的數據,可能會存在實效性問題)
- 降低單次網絡請求失敗的概率(弱網環境下,數據量越大越容易失敗)
- 減少一些不必要流量消耗(用戶通常不會一次瀏覽特別多的數據)
- 可以提升用戶在交互上的體驗(上滑加載更多)
在實際場景,如果需要對上述情況需求的,可以考慮一下進行分頁加載。下面以在 Android 端實現 List 分頁加載為例來梳理一些概念
分頁加載的數據請求行為
這里需要先明確兩個概念:界面上每頁實際展示的數量和控制請求時每頁加載的數量。一般來說考慮到多設備適配,請求時每頁加載的數量要大于每頁實際展示的數量。
通常對于分頁加載的數據請求行為主要有下面三個:
- 初始化加載數據(首次啟動界面時加載數據)
- 下拉刷新數據 (刷新當前頁面的數據)
- 上拉加載更多 (加載下一頁數據)
初始化的時候數據作為在創建界面時展示的內容,所以需要在保證在基礎數據完備的情況下,考慮如何更快完成 loading 過程。這里有一個理念就是先保證可用性再考慮錦上添花的事情
通常對于實時性要求不高的應用,可以考慮讀取預先緩存的歷史數據作為初始化時的填充內容,界面加載完成后再主動請求進行刷新操作去更新界面內容。
在這種情況下,為了能夠快速的滑動瀏覽內容,同時為了避免反復的網絡請求,簡單的實現可以設置一個 DataSet 作為網絡數據請求成功后的內存緩存倉庫,當然如果對應用有更高要求的,可以再考慮做數據持久化。這樣就可以引出下面兩種方案來設計。
方案一:緩存容器控制
原則就是:每次都是先讀取當前已經緩存在容器中的數據,而從網絡獲取的數據是為了更新容器的數據,在更新到顯示界面。
該方案基于前面提到的使用一個 DataSet 作為數據請求成功后的內存緩存倉庫,在此基礎上,界面獲取的數據可以從這個 DataSet 中讀取,只需要一次請求緩存較多的數據,不需要每次從網絡讀取數據。
只有當數據需要刷新或者 DataSet 數據展示量到達一個設定的閥值時,才開始從網絡請求獲取數據對 DataSet 容器進行更新,而關于數據排重可以根據每條 item 的唯一 ID 完成。
類似圖片緩存控制一樣,所以考慮做三級緩存也是可以的。
方案二:實時分頁加載
這個方案的原則是:每次請求按需加載,加載更多時進行實時數據獲取。
實際處理起來還是會有一些問題,比如刷新時如何控制新增數據的填充,加載更多時如何控制數據變化導致的數據重復添加。為解決這些問題需要考慮下面幾個因素:
- 每次請求的數據量(每頁的數據量);
- 當前數據展示總量(list 中已經加載的量);
- 服務端數據總量;
- 服務端總頁數(按照當前每次請求數計算);
初始化加載數據時 初始化時,每次像服務器請求最新的第一頁數據展示到 list,請求失敗展示 No Content 頁面(可手動刷新),并記錄上面描述的幾個數值。
下拉刷新數據 對比當前請求回來獲取到的服務端數據總量和上次請求成功時保存服務端數據總量,兩者的差值是否大于當前請求一頁的數據量,如果是則直接替換原來的所有數據,不是的話只要把新增的數據 add 到 list 的 header 即可,注意數據排重。
上拉加載更多
獲取上次請求時保存的頁碼數的下一頁的數據添加到 list 的 footer 即可。
解決下拉加載更多時,服務端數據變化導致數據重復的解決方法有三種:
1、使用緩存:
可以定時的把n頁緩存到數據庫中,這樣獲取前面n頁的時候就不會有重復的問題了,但是后面的分頁內容還是無法保證不重復。
2、使用id作為限定進行分頁:
客戶端記錄當前分頁的最后一條記錄的id,然后在請求下一頁的時候,從這個id開始算起進行獲取一頁大小的內容,比如分頁大小為20,按照id倒序獲取列表內容: select * from tablename where id 優點:這種方式可以確保不會獲取到重復的數據; 缺點:需要調整服務器端和客戶端的分頁方法,通過當前記錄id和pageSize去請求服務器端。并且如果按照其他字段而不是id進行的話要確保該字段不會被修改,并且不會有重復,考慮到性能,最好加上索引,推薦使用整型字段: select * from tablename where 排序字段<:排序字段當前記錄值 order="" by="" desc="" limit="" 0="" 20="" span=""> 另外,如果需要加列表緩存,只能按照當前頁的最后一條記錄的ID作為key的標示,這樣緩存需要的存儲空間需要很多,如果列表添加數據很快,用戶訪問第一頁的時候,總是會獲取到新的數據,這樣會不斷的讀數據庫,然后寫緩存,緩存利用率不高。(而類似于Hibernate的列表緩存,都是在數據表有增刪改操作的時候,讓列表緩存失效的,我猜也是出于數據庫數據有改動的情況下緩存命中率不高,所以讓列表緩存失效的,以便節省內存空間。)
3、客戶端排除:
通過在客戶端中保存已加載記錄的id,進行數據去重,如果被去重的數據比較多,則可以考慮在請求下一頁的數據。 優點:客戶端記錄已經加載的數據,再次加載的時候過濾掉已有的數據。這種方法能確保不會出現重復的數據,并且不改動服務器端的原有邏輯; 缺點:當列表數據增加很快的情況下,比如日志記錄表,獲取下一頁的數據會有很多的重復記錄,不適合這種情況,適用于列表數據添加不是很頻繁的情況。 即使是用到了緩存,當緩存時間比較長,或者新增數據比較快時,在緩存失效以后,重新獲取分頁數據的時候也會有大量的重復內容。