2018-07-04

object-c基于collectionView和SDWebImage做的預(yù)加載優(yōu)化

當(dāng)App中使用了 UICollectionView 以瀑布流的形式來呈現(xiàn)數(shù)據(jù)時,站在用戶的角度描馅,用戶在自上至下一頁一頁瀏覽這些內(nèi)容的過程中嚣伐,當(dāng)用戶感到滑動很流暢自然顷级,每頁內(nèi)容從無到有需要用戶等待的時間很短甚至幾乎感覺不到盛嘿,那么 UICollectionView 才會帶給用戶一個很好的體驗(yàn)削锰。本文介紹了為了達(dá)到這兩個目的所作出的一些客戶端的優(yōu)化妹卿。

數(shù)據(jù)的預(yù)加載

數(shù)據(jù)預(yù)加載的目的是不必等到用戶某一時刻瀏覽到CollectionView的末尾了熄求,也即本地已經(jīng)沒有更多數(shù)據(jù)展示了才去發(fā)請求拿下一頁數(shù)據(jù)渣玲,而是有一個預(yù)判,用戶就快要看完本地的數(shù)據(jù)了弟晚,可以向Server要下一頁數(shù)據(jù)了忘衍!

為了實(shí)現(xiàn)預(yù)加載,最開始的方案是在UI層面的預(yù)判卿城。根據(jù) UICollectionView 的基類是 UIScrollView 枚钓,大致思路是對于沿豎直方向滾動的CollectionView,考察它的 contentOffset.y 和 conetntSize.height 瑟押,結(jié)合CollectionView的 frame.size.height 搀捷,可以計算CollectionView全部內(nèi)容底下還有多高沒展示出來,如果高度小于我們預(yù)先設(shè)定的閾值(用戶快滑到底了)多望,那么就觸發(fā)加載下一頁的請求嫩舟。

這樣做似乎沒什么問題,但是仔細(xì)想想怀偷,其實(shí)并不優(yōu)雅家厌。一方面,一旦有UI調(diào)整的需求椎工,CollectionView每行的高度有調(diào)整時饭于,我們也要去調(diào)整閾值,來決定是否去請求下一頁數(shù)據(jù)维蒙;另一方面掰吕,App中不同場景下的CollectionView每行高度不同,需要根據(jù)不同場景去Tuning木西,找出合適的閾值畴栖。

后來很自然想到在邏輯上進(jìn)行預(yù)判,也就是我們現(xiàn)在使用的方案八千。

UICollectionView 每個Cell都需要一個數(shù)據(jù)模型對象(Data Transfer Object吗讶,下稱DTO)來支持它的顯示,通沉道Γ客戶端拿到的服務(wù)端返回的數(shù)據(jù)后照皆,做一系列的解析,得到一個一個DTO沸停,用以支持CollectionView的展示膜毁。到代碼層面DTO們被保存在一個數(shù)組里,任意時刻在正確的狀態(tài)下 UICollectionView 的總Cell數(shù)量應(yīng)該跟當(dāng)前本地DTO的個數(shù)相等愤钾,Cell跟DTO是一一對應(yīng)的關(guān)系瘟滨, 數(shù)據(jù)的預(yù)加載本質(zhì)上就是DTO的預(yù)加載 。

用戶在滾動 UICollectionView 時能颁,當(dāng) UICollectionView 根據(jù)預(yù)定的配置覺得它該展示某行某列的Cell時杂瘸,會向它的DataSource[2]發(fā)送 collectionView:cellForItemAtIndexPath: 消息[3],詢問那行那列該展示什么伙菊,這個方法返回一個Cell對象败玉, UICollectionView 拿到這個Cell后就把它展示在相應(yīng)位置。通常這個方法中要做的重要事情就是去上文提到的保存DTO的數(shù)組中根據(jù)Cell的行列索引找到這個Cell對應(yīng)的DTO镜硕,根據(jù)DTO對Cell配置一番运翼,返回給 UICollectionView 。

順著這個思路兴枯,在這個方法中可以知道當(dāng)前 UICollectionView 需要展示的Cell的索引血淌,由于Cell跟DTO是一一對應(yīng)的關(guān)系,那我們也知道了當(dāng)前需要的DTO在總數(shù)據(jù)模型對象中的索引财剖,當(dāng)剩下的數(shù)據(jù)模型對象不夠支持一頁的顯示時悠夯,就去請求下一頁。

表達(dá)的可能有點(diǎn)抽象峰伙,假設(shè)請求一次Server返回20個DTO疗疟,過程可以更形象化一點(diǎn):

- CollectionView: 數(shù)據(jù)源數(shù)據(jù)源,用戶滑到第181個Cell要露出來了瞳氓,快給我策彤!

- DataSource: 好的,我首先要去拿第181個Cell對應(yīng)的DTO匣摘,根據(jù)這個配置好一個Cell給你去展示店诗!

等等,你都已經(jīng)展示到第181個Cell了耙舭瘛庞瘸!我發(fā)現(xiàn)DTO目前本地總共只有200個,200 - 181 = 19 < 20不夠支持你展示下一頁所需要的20個Cell了赠叼,我先發(fā)起一個異步請求擦囊,去拿新一頁的DTO违霞!

關(guān)鍵代碼,很簡單:

NSUInteger countOfDataModel = dataModel.count; // 目前本地有的DTO數(shù)量

NSUInteger currentRequestIndex = indexPath.row; // 當(dāng)前需要的Cell索引瞬场,也即當(dāng)前需要的數(shù)據(jù)模型索引

if (countOfDataModel - currentRequestIndex < 19) {

[self fetchNextPageAsync];

}

要注意的問題是要做好防止重復(fù)發(fā)送請求的保護(hù)工作买鸽。

圖片加載邏輯優(yōu)化

當(dāng) UICollectionView 的每個Cell都需要展示一個(或多個)圖片時,在上文提到的根據(jù)DTO配置Cell過程中贯被,會根據(jù)DTO中指定的圖片的URL眼五,發(fā)送一個異步的圖片請求,等到圖片請求完畢了彤灶,再把圖片展示到對應(yīng)的Cell上(當(dāng)然看幼,可以把這一切交給 SDWebImage : )。

或許你會問幌陕,加載圖片已經(jīng)是異步了啊诵姜,我還要優(yōu)化什么?不苞轿,這遠(yuǎn)遠(yuǎn)不夠茅诱。在實(shí)際的測試中,這種樸素的做法依然會帶來明顯的滑動過程的卡頓搬卒。使用Instruments進(jìn)行profile發(fā)現(xiàn)瑟俭,在滑動過程中始終會丟那么15幀左右,不能忍契邀!

再回到 UICollectionView 繼承自 UIScrollView 上來摆寄。通過 UIScrollView 的Delegate,我們能感知到滑動過程中CollectionView的各種關(guān)鍵狀態(tài)坯门,包括用戶的手是否正在拖拽微饥,以及CollectionView是否正在滑動、減速等等古戴,這就是我們優(yōu)化的秘密武器欠橘!

那么,本著不該做的事情不要做现恼,或者等到不得不做的時候再做的原則肃续,讓我們分析用戶在滑動CollectionView的過程中有哪些地方可以細(xì)摳。

用戶在滑動(拖拽)CollectionView時(手與屏幕正在接觸)叉袍,很有可能是用戶在認(rèn)真逐個瀏覽每個Cell始锚,要去加載當(dāng)前可見Cell的圖片

用戶滑動CollectionView結(jié)束后,手離開了屏幕喳逛,并引發(fā)了CollectionView減速時瞧捌, 預(yù)判 CollectionView減速結(jié)束后靜止時的狀態(tài),對于那些將來靜止時用戶可見的Cell,提前去加載它們的圖片姐呐;對于那些只是“曇花一現(xiàn)”的Cell殿怜,即它們只是在減速的過程中出現(xiàn)那么一剎那,就被“頂”上去了皮钠,只加載這些Cell中圖片在本地有緩存的圖片(從內(nèi)存中加載稳捆,不值得去發(fā)網(wǎng)絡(luò)請求赠法,即使是異步的也不值得)

減速結(jié)束后麦轰,CollectionView處于靜止?fàn)顟B(tài),加載當(dāng)前全部可見Cell的圖片

OK砖织,那么來看我們怎么實(shí)現(xiàn)它款侵。

對于CollectionView的每個Cell,我們給它添加一個異步加載圖片的方法 loadImage 侧纯。直接上關(guān)鍵代碼新锈,看了便知。

// CollectionView將來靜止時可見的區(qū)域眶熬,同時也是標(biāo)識CollectionView當(dāng)前是正在被用戶拖拽還是已經(jīng)被拖拽完畢并正在減速

@property (nonatomic, strong) CGRect *targetRect;

#pragma mark - UICollectionView DataSource

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {

// ....

[self loadImageForCell:cell atIndexPath:indexPath];

// ....

}

#pragma mark - UIScrollView Delegate

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {

self.targetRect = nil;

[self loadImageForVisibleCells];

}

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {

self.targetRect = CGRectMake(targetContentOffset->x, targetContentOffset->y, scrollView.frame.size.width, scrollView.frame.size.height);

}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {

self.targetRect = nil;

[self loadImageForVisibleCells];

}

#pragma mark - Decide to Load Image For Cells

- (void)loadImageForCell:(AESmartCollectionFlowViewCell *)cell

atIndexPath:(NSIndexPath *)indexPath {

// Cell的targetURLString是指派給Cell的新的圖片URL妹笆,在根據(jù)Cell的DTO配置Cell時為其賦值

if (!cell.targetURLString) {

return;

}

// Cell的imageURLString是Cell的當(dāng)前正在顯示的圖片URL

if (![cell.targetURLString isEqualToString:cell.imageURLString] || cell.isDisplayingPlaceholderNow) {

SDWebImageManager *manager = [SDWebImageManager sharedManager];

UICollectionViewLayoutAttributes *attr = [self.collectionView layoutAttributesForItemAtIndexPath:indexPath];

CGRect cellFrame = attr.frame;

BOOL shouldLoadImageForCurrentCell = YES;

// 如果正在減速而且當(dāng)前Cell的frame不在將來滑動停止后的可見區(qū)域

if (self.targetRect && !CGRectIntersectsRect(self.targetRect.CGRectValue, cellFrame)) {

// 那么只有Cell的targetURL在內(nèi)存的緩存中,才去加載它

SDImageCache *imageCache = [SDImageCache sharedImageCache];

NSString *key = [manager cacheKeyForURL:[NSURL URLWithString:cell.targetURLString]];

if (![imageCache imageFromMemoryCacheForKey:key]) {

shouldLoadImageForCurrentCell = NO;

}

}

if (shouldLoadImageForCurrentCell) {

[cell loadImage];

}

}

}

- (void)loadImageForVisibleCells {

NSArray *visibleCells = [self.collectionView visibleCells];

for (UICollectionViewCell *cell in visibleCells) {

NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell];

[self loadImageForCell:cell atIndexPath:indexPath];

}

}

做了這些努力后拳缠,再去profile一下,發(fā)現(xiàn)網(wǎng)速良好情況下滑動時幀率只丟了那么1窟坐、2幀哲鸳,而且滑動起來無明顯卡頓盔憨!

要么不做,要么做絕

哈哈婿奔,這個有點(diǎn)狠啊脸秽,頗有朱元璋的風(fēng)格。

做了這么多后记餐,我們發(fā)現(xiàn)片酝,數(shù)據(jù)預(yù)加載完畢后,向CollectionView發(fā)送 reloadData 消息通知它數(shù)據(jù)模型變化時雕沿,就在這一瞬間审轮,還是會導(dǎo)致CollectionView卡頓那么一下下。

好吧不能忍篡诽,封裝一個我們自己的 reloadData 方法榴捡,在這里簡單的hold住reload吊圾,根據(jù)上文中的 targetRect 屬性的標(biāo)記作用,當(dāng)且僅當(dāng)在CollectionView減速停止后项乒,再去真正向它發(fā)送 reloadData 消息板丽。在這里僅提供思路,不做贅述了猖辫。

此外砚殿,在開發(fā)中似炎,我們把這一系列的方法以 NSObject 類的Category形式做一個封裝,這樣不管誰是CollectionView的Delegate或者DataSource都可以從容應(yīng)對贩毕。

作者:指尖的跳動

鏈接:http://www.reibang.com/p/32fe33caca3f

來源:簡書

簡書著作權(quán)歸作者所有仆嗦,任何形式的轉(zhuǎn)載都請聯(lián)系作者獲得授權(quán)并注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末垃僚,一起剝皮案震驚了整個濱河市谆棺,隨后出現(xiàn)的幾起案子改淑,更是在濱河造成了極大的恐慌炫贤,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,590評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件询吴,死亡現(xiàn)場離奇詭異,居然都是意外死亡猛计,警方通過查閱死者的電腦和手機(jī)奉瘤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評論 3 399
  • 文/潘曉璐 我一進(jìn)店門盗温,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人卖局,你說我怎么就攤上這事砚偶。” “怎么了均芽?”我有些...
    開封第一講書人閱讀 169,301評論 0 362
  • 文/不壞的土叔 我叫張陵掀宋,是天一觀的道長。 經(jīng)常有香客問我囤萤,道長涛舍,這世上最難降的妖魔是什么富雅? 我笑而不...
    開封第一講書人閱讀 60,078評論 1 300
  • 正文 為了忘掉前任肛搬,我火速辦了婚禮温赔,結(jié)果婚禮上陶贼,老公的妹妹穿的比我還像新娘。我一直安慰自己痹屹,他們只是感情好志衍,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,082評論 6 398
  • 文/花漫 我一把揭開白布楼肪。 她就那樣靜靜地躺著佃牛,像睡著了一般俘侠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上央星,一...
    開封第一講書人閱讀 52,682評論 1 312
  • 那天莉给,我揣著相機(jī)與錄音,去河邊找鬼颓遏。 笑死叁幢,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的曼玩。 我是一名探鬼主播黍判,決...
    沈念sama閱讀 41,155評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼美旧,長吁一口氣:“原來是場噩夢啊……” “哼陈症!你這毒婦竟也來了震糖?” 一聲冷哼從身側(cè)響起吊说,我...
    開封第一講書人閱讀 40,098評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎雅宾,沒想到半個月后眉抬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蜀变,經(jīng)...
    沈念sama閱讀 46,638評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡库北,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,701評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了情屹。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片垃你。...
    茶點(diǎn)故事閱讀 40,852評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖恤筛,靈堂內(nèi)的尸體忽然破棺而出官还,到底是詐尸還是另有隱情,我是刑警寧澤毒坛,帶...
    沈念sama閱讀 36,520評論 5 351
  • 正文 年R本政府宣布望伦,位于F島的核電站,受9級特大地震影響煎殷,放射性物質(zhì)發(fā)生泄漏屯伞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,181評論 3 335
  • 文/蒙蒙 一豪直、第九天 我趴在偏房一處隱蔽的房頂上張望劣摇。 院中可真熱鬧,春花似錦弓乙、人聲如沸末融。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽勾习。三九已至,卻和暖如春艺栈,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背漾唉。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評論 1 274
  • 我被黑心中介騙來泰國打工场刑, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留铐懊,地道東北人贼急。 一個月前我還...
    沈念sama閱讀 49,279評論 3 379
  • 正文 我出身青樓走敌,卻偏偏與公主長得像影斑,于是被迫代替她去往敵國和親片迅。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,851評論 2 361

推薦閱讀更多精彩內(nèi)容