前兩次的分享分別介紹了 ASDK 對于渲染的優(yōu)化以及 ASDK 中使用的另一種布局模型;這兩個新機制的引入分別解決了 iOS 在主線程渲染視圖以及 Auto Layout 的性能問題,而這一次討論的主要內容是 ASDK 如何預先請求服務器數(shù)據,達到看似無限滾動列表的效果的。
這篇文章是 ASDK 系列中的最后一篇,文章會介紹 iOS 中幾種預加載的方案,以及 ASDK 中是如何處理預加載的。
不過,在介紹 ASDK 中實現(xiàn)智能預加載的方式之前,文章中會介紹幾種簡單的預加載方式,方便各位開發(fā)者進行對比,選擇合適的機制實現(xiàn)預加載這一功能。
網絡與性能
ASDK 通過在渲染視圖和布局方面的優(yōu)化已經可以使應用在任何用戶的瘋狂操作下都能保持 60 FPS 的流暢程度,也就是說,我們已經充分的利用了當前設備的性能,調動各種資源加快視圖的渲染。
但是,僅僅在 CPU 以及 GPU 方面的優(yōu)化往往是遠遠不夠的。在目前的軟件開發(fā)中,很難找到一個沒有任何網絡請求的應用,哪怕是一個記賬軟件也需要服務器來同步保存用戶的信息,防止資料的丟失;所以,只在渲染這一層面進行優(yōu)化還不能讓用戶的體驗達到最佳,因為網絡請求往往是一個應用最為耗時以及昂貴的操作。

每一個應用程序在運行時都可以看做是 CPU 在底層利用各種資源瘋狂做加減法運算,其中最耗時的操作并不是進行加減法的過程,而是資源轉移的過程。
舉一個不是很恰當?shù)睦?,主廚(CPU)在炒一道菜(計算)時往往需要的時間并不多,但是菜的采購以及準備(資源的轉移)會占用大量的時間,如果在每次炒菜之前,都由幫廚提前準備好所有的食材(緩存),那么做一道菜的時間就大大減少了。
而提高資源轉移的效率的最佳辦法就是使用多級緩存:

從上到下,雖然容量越來越大,直到 Network 層包含了整個互聯(lián)網的內容,但是訪問時間也是直線上升;在 Core 或者三級緩存中的資源可能訪問只需要幾個或者幾十個時鐘周期,但是網絡中的資源就遠遠大于這個數(shù)字,幾分鐘、幾小時都是有可能的。
更糟糕的是,因為天朝的網絡情況及其復雜,運營商劫持 DNS、404 無法訪問等問題導致網絡問題極其嚴重;而如何加速網絡請求成為了很多移動端以及 Web 應用的重要問題。
預加載
本文就會提供一種緩解網絡請求緩慢導致用戶體驗較差的解決方案,也就是預加載;在本地真正需要渲染界面之前就通過網絡請求獲取資源存入內存或磁盤。
預加載并不能徹底解決網絡請求緩慢的問題,而是通過提前發(fā)起網絡請求緩解這一問題。
那么,預加載到底要關注哪些方面的問題呢?總結下來,有以下兩個關注點:
需要預加載的資源
預加載發(fā)出的時間
文章會根據上面的兩個關注點,分別分析四種預加載方式的實現(xiàn)原理以及優(yōu)缺點:
無限滾動列表
threshold
惰性加載
智能預加載
無限滾動列表
其實,無限滾動列表并不能算是一種預加載的實現(xiàn)原理,它只是提供一種分頁顯示的方法,在每次滾動到UITableView底部時,才會開始發(fā)起網絡請求向服務器獲取對應的資源。
雖然這種方法并不是預加載方式的一種,放在這里的主要作用是作為對比方案,看看如果不使用預加載的機制,用戶體驗是什么樣的。

很多客戶端都使用了分頁的加載方式,并沒有添加額外的預加載的機制來提升用戶體驗,雖然這種方式并不是不能接受,不過每次滑動到視圖底部之后,總要等待網絡請求的完成確實對視圖的流暢性有一定影響。
雖然僅僅使用無限滾動列表而不提供預加載機制會在一定程度上影響用戶體驗,不過,這種需要用戶等待幾秒鐘的方式,在某些時候確實非常好用,比如:投放廣告。
QQ 空間就是這么做的,它們投放的廣告基本都是在整個列表的最底端,這樣,當你滾動到列表最下面的時候,就能看到你急需的租房、租車、同城交友、信用卡辦理、只有 iPhone 能玩的游戲以及各種奇奇怪怪的辣雞廣告了,很好的解決了我們的日常生活中的各種需求。(哈哈哈哈哈哈哈哈哈哈哈哈哈)
Threshold
使用 Threshold 進行預加載是一種最為常見的預加載方式,知乎客戶端就使用了這種方式預加載條目,而其原理也非常簡單,根據當前UITableView的所在位置,除以目前整個UITableView.contentView的高度,來判斷當前是否需要發(fā)起網絡請求:
Swift
letthreshold:CGFloat=0.7varcurrentPage=0overridefuncscrollViewDidScroll(_scrollView:UIScrollView){letcurrent=scrollView.contentOffset.y+scrollView.frame.size.heightlettotal=scrollView.contentSize.heightletratio=current/totalifratio>=threshold{currentPage+=1print("Request page\(currentPage)from server.")}}
上面的代碼在當前頁面已經劃過了 70% 的時候,就請求新的資源,加載數(shù)據;但是,僅僅使用這種方法會有另一個問題,尤其是當列表變得很長時,十分明顯,比如說:用戶從上向下滑動,總共加載了 5 頁數(shù)據:

Page 當前總頁數(shù);
Total 當前UITableView總元素個數(shù);
Threshold 網絡請求觸發(fā)時間;
Diff 表示最新加載的頁面被瀏覽了多少;
當 Threshold 設置為 70% 的時候,其實并不是單頁 70%,這就會導致新加載的頁面都沒有看,應用就會發(fā)出另一次請求,獲取新的資源。
動態(tài)的 Threshold
解決這個問題的辦法,還是比較簡單的,通過修改上面的代碼,將 Threshold 變成一個動態(tài)的值,隨著頁數(shù)的增長而增長:
Swift
letthreshold:CGFloat=0.7letitemPerPage:CGFloat=10varcurrentPage:CGFloat=0overridefuncscrollViewDidScroll(_scrollView:UIScrollView){letcurrent=scrollView.contentOffset.y+scrollView.frame.size.heightlettotal=scrollView.contentSize.heightletratio=current/totalletneedRead=itemPerPage*threshold+currentPage*itemPerPagelettotalItem=itemPerPage*(currentPage+1)letnewThreshold=needRead/totalItemifratio>=newThreshold{currentPage+=1print("Request page\(currentPage)from server.")}}
通過這種方法獲取的newThreshold就會隨著頁數(shù)的增長而動態(tài)的改變,解決了上面出現(xiàn)的問題:

惰性加載
使用 Threshold 進行預加載其實已經適用于大多數(shù)應用場景了;但是,下面介紹的方式,惰性加載能夠有針對性的加載用戶“會看到的” Cell。
惰性加載,就是在用戶滾動的時候會對用戶滾動結束的區(qū)域進行計算,只加載目標區(qū)域中的資源。
用戶在飛速滾動中會看到巨多的空白條目,因為用戶并不想閱讀這些條目,所以,我們并不需要真正去加載這些內容,只需要在ASTableView/ASCollectionView中只根據用戶滾動的目標區(qū)域惰性加載資源。

惰性加載的方式不僅僅減少了網絡請求的冗余資源,同時也減少了渲染視圖、數(shù)據綁定的耗時。
計算用戶滾動的目標區(qū)域可以直接使用下面的代理方法獲?。?/p>
Swift
letmarkedView=UIView()letrowHeight:CGFloat=44.0overridefuncscrollViewWillEndDragging(_scrollView:UIScrollView,withVelocity velocity:CGPoint,targetContentOffset:UnsafeMutablePointer){lettargetOffset=targetContentOffset.pointeelettargetRect=CGRect(origin:targetOffset,size:scrollView.frame.size)markedView.frame=targetRect? ? markedView.backgroundColor=UIColor.black.withAlphaComponent(0.1)tableView.addSubview(markedView)varindexPaths:[IndexPath]=[]letstartIndex=Int(targetRect.origin.y/rowHeight)letendIndex=Int((targetRect.origin.y+tableView.frame.height)/rowHeight)forindexinstartIndex...endIndex{indexPaths.append(IndexPath(row:index,section:0))}print("\(targetRect)\(indexPaths)")}
以上代碼只會大致計算出目標區(qū)域內的IndexPath數(shù)組,并不會展開新的 page,同時會使用淺黑色標記目標區(qū)域。
當然,惰性加載的實現(xiàn)也并不只是這么簡單,不僅需要客戶端的工作,同時因為需要加載特定 offset 資源,也需要服務端提供相應 API 的支持。
雖然惰性加載的方式能夠按照用戶的需要請求對應的資源,但是,在用戶滑動UITableView的過程中會看到大量的空白條目,這樣的用戶體驗是否可以接受又是值得考慮的問題了。
智能預加載
終于到了智能預加載的部分了,當我第一次得知 ASDK 可以通過滾動的方向預加載不同數(shù)量的內容,感覺是非常神奇的。

如上圖所示 ASDK 把正在滾動的ASTableView/ASCollectionView劃分為三種狀態(tài):
Fetch Data
Display
Visible
上面的這三種狀態(tài)都是由 ASDK 來管理的,而每一個ASCellNode的狀態(tài)都是由ASRangeController控制,所有的狀態(tài)都對應一個ASInterfaceState:
ASInterfaceStatePreload當前元素貌似要顯示到屏幕上,需要從磁盤或者網絡請求數(shù)據;
ASInterfaceStateDisplay當前元素非??赡芤兂煽梢姷?,需要進行異步繪制;
ASInterfaceStateVisible當前元素最少在屏幕上顯示了 1px
當用戶滾動當前視圖時,ASRangeController就會修改不同區(qū)域內元素的狀態(tài):

上圖是用戶在向下滑動時,ASCellNode是如何被標記的,假設當前視圖可見的范圍高度為 1,那么在默認情況下,五個區(qū)域會按照上圖的形式進行劃分:

在滾動方向(Leading)上 Fetch Data 區(qū)域會是非滾動方向(Trailing)的兩倍,ASDK 會根據滾動方向的變化實時改變緩沖區(qū)的位置;在向下滾動時,下面的 Fetch Data 區(qū)域就是上面的兩倍,向上滾動時,上面的 Fetch Data 區(qū)域就是下面的兩倍。
這里的兩倍并不是一個確定的數(shù)值,ASDK 會根據當前設備的不同狀態(tài),改變不同區(qū)域的大小,但是滾動方向的區(qū)域總會比非滾動方向大一些。
智能預加載能夠根據當前的滾動方向,自動改變當前的工作區(qū)域,選擇合適的區(qū)域提前觸發(fā)請求資源、渲染視圖以及異步布局等操作,讓視圖的滾動達到真正的流暢。
原理
在 ASDK 中整個智能預加載的概念是由三個部分來統(tǒng)一協(xié)調管理的:
ASRangeController
ASDataController
ASTableView與ASTableNode
對智能預加載實現(xiàn)的分析,也是根據這三個部分來介紹的。
工作區(qū)域的管理
ASRangeController是ASTableView以及ASCollectionView內部使用的控制器,主要用于監(jiān)控視圖的可見區(qū)域、維護工作區(qū)域、觸發(fā)網絡請求以及繪制、單元格的異步布局。
以ASTableView為例,在視圖進行滾動時,會觸發(fā)-[UIScrollView scrollViewDidScroll:]代理方法:
Objective-C
-(void)scrollViewDidScroll:(UIScrollView*)scrollView{ASInterfaceState interfaceState=[selfinterfaceStateForRangeController:_rangeController];if(ASInterfaceStateIncludesVisible(interfaceState)){[_rangeController updateCurrentRangeWithMode:ASLayoutRangeModeFull];}...}
每一個ASTableView的實例都持有一個ASRangeController以及ASDataController用于管理工作區(qū)域以及數(shù)據更新。
ASRangeController 最重要的私有方法-[ASRangeController _updateVisibleNodeIndexPaths]一般都是因為上面的方法間接調用的:
Objective-C
-[ASRangeController updateCurrentRangeWithMode:]-[ASRangeController setNeedsUpdate]-[ASRangeController updateIfNeeded]-[ASRangeController _updateVisibleNodeIndexPaths]
調用棧中間的過程其實并不重要,最后的私有方法的主要工作就是計算不同區(qū)域內 Cell 的NSIndexPath數(shù)組,然后更新對應 Cell 的狀態(tài)ASInterfaceState觸發(fā)對應的操作。
我們將這個私有方法的實現(xiàn)分開來看:
Objective-C
-(void)_updateVisibleNodeIndexPaths{NSArray*allNodes=[_dataSource completedNodes];NSUInteger numberOfSections=[allNodes count];NSArray*visibleNodePaths=[_dataSource visibleNodeIndexPathsForRangeController:self];ASScrollDirection scrollDirection=[_dataSource scrollDirectionForRangeController:self];if(_layoutControllerImplementsSetViewportSize){[_layoutController setViewportSize:[_dataSource viewportSizeForRangeController:self]];}if(_layoutControllerImplementsSetVisibleIndexPaths){[_layoutController setVisibleNodeIndexPaths:visibleNodePaths];}...}
當前ASRangeController的數(shù)據源以及代理就是ASTableView,這段代碼首先就獲取了完成計算和布局的ASCellNode以及可見的ASCellNode的NSIndexPath:
Objective-C
-(void)_updateVisibleNodeIndexPaths{NSArray*currentSectionNodes=nil;NSInteger currentSectionIndex=-1;NSUInteger numberOfNodesInSection=0;NSSet*visibleIndexPaths=[NSSet setWithArray:visibleNodePaths];NSSet*displayIndexPaths=nil;NSSet*preloadIndexPaths=nil;NSMutableOrderedSet*allIndexPaths=[[NSMutableOrderedSet alloc]initWithSet:visibleIndexPaths];ASLayoutRangeMode rangeMode=_currentRangeMode;ASRangeTuningParameters parametersPreload=[_layoutController tuningParametersForRangeMode:rangeMode? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? rangeType:ASLayoutRangeTypePreload];if(ASRangeTuningParametersEqualToRangeTuningParameters(parametersPreload,ASRangeTuningParametersZero)){preloadIndexPaths=visibleIndexPaths;}else{preloadIndexPaths=[_layoutController indexPathsForScrolling:scrollDirection? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? rangeMode:rangeMode? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? rangeType:ASLayoutRangeTypePreload];}#:displayIndexPaths 的計算和 preloadIndexPaths 非常類似[allIndexPaths unionSet:displayIndexPaths];[allIndexPaths unionSet:preloadIndexPaths];...}
預加載以及展示部分的ASRangeTuningParameters都是以二維數(shù)組的形式保存在ASAbstractLayoutController中的:

在獲取了ASRangeTuningParameters之后,ASDK 也會通過ASFlowLayoutController的方法-[ASFlowLayoutController indexPathsForScrolling:rangeMode:rangeType:]獲取NSIndexPath對象的集合:
Objective-C
-(NSSet*)indexPathsForScrolling:(ASScrollDirection)scrollDirection rangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType{#:獲取 directionalBuffer 以及 viewportDirectionalSize? ASIndexPath startPath=[selffindIndexPathAtDistance:(-directionalBuffer.negativeDirection*viewportDirectionalSize)fromIndexPath:_visibleRange.start];ASIndexPath endPath=[selffindIndexPathAtDistance:(directionalBuffer.positiveDirection*viewportDirectionalSize)fromIndexPath:_visibleRange.end];NSMutableSet*indexPathSet=[[NSMutableSet alloc]init];NSArray*completedNodes=[_dataSource completedNodes];ASIndexPath currPath=startPath;while(!ASIndexPathEqualToIndexPath(currPath,endPath)){[indexPathSet addObject:[NSIndexPath indexPathWithASIndexPath:currPath]];currPath.row++;while(currPath.row>=[(NSArray*)completedNodes[currPath.section]count]&&currPath.section
方法的執(zhí)行過程非常簡單,根據ASRangeTuningParameters獲取該滾動方向上的緩沖區(qū)大小,在區(qū)域內遍歷所有的ASCellNode查看其是否在當前區(qū)域內,然后加入數(shù)組中。
到這里,所有工作區(qū)域visibleIndexPathsdisplayIndexPaths以及preloadIndexPaths都已經獲取到了;接下來,就到了遍歷NSIndexPath,修改結點狀態(tài)的過程了;
Objective-C
-(void)_updateVisibleNodeIndexPaths{...for(NSIndexPath*indexPathinallIndexPaths){ASInterfaceState interfaceState=ASInterfaceStateMeasureLayout;if(ASInterfaceStateIncludesVisible(selfInterfaceState)){if([visibleIndexPaths containsObject:indexPath]){interfaceState|=(ASInterfaceStateVisible|ASInterfaceStateDisplay|ASInterfaceStatePreload);}else{if([preloadIndexPaths containsObject:indexPath]){interfaceState|=ASInterfaceStatePreload;}if([displayIndexPaths containsObject:indexPath]){interfaceState|=ASInterfaceStateDisplay;}}}
根據當前ASTableView的狀態(tài)以及NSIndexPath所在的區(qū)域,打開ASInterfaceState對應的位。
Objective-C
NSInteger section=indexPath.section;NSInteger row=indexPath.row;if(section>=0&&row>=0&§ion
后面的一部分代碼就會遞歸的設置結點的interfaceState,并且在當前ASRangeController的ASLayoutRangeMode發(fā)生改變時,發(fā)出通知,調用-[ASRangeController _updateVisibleNodeIndexPaths]私有方法,更新結點的狀態(tài)。
Objective-C
-(void)scheduledNodesDidDisplay:(NSNotification*)notification{CFAbsoluteTime notificationTimestamp=((NSNumber*)notification.userInfo[ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp]).doubleValue;if(_pendingDisplayNodesTimestamp
數(shù)據的加載和更新
ASTableNode既然是對ASTableView的封裝,那么表視圖中顯示的數(shù)據仍然需要數(shù)據源來提供,而在 ASDK 中這一機制就比較復雜:

整個過程是由四部分協(xié)作完成的,Controller、ASTableNode、ASTableView以及ASDataController,網絡請求發(fā)起并返回數(shù)據之后,會調用ASTableNode的 API 執(zhí)行插入行的方法,最后再通過ASTableView的同名方法,執(zhí)行管理和更新節(jié)點數(shù)據的ASDataController的方法:
Objective-C
-(void)insertRowsAtIndexPaths:(NSArray*)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions{dispatch_group_wait(_editingTransactionGroup,DISPATCH_TIME_FOREVER);NSArray*sortedIndexPaths=[indexPaths sortedArrayUsingSelector:@selector(compare:)];NSMutableArray*contexts=[[NSMutableArray alloc]initWithCapacity:indexPaths.count];__weak idenvironment=[self.environmentDelegate dataControllerEnvironment];for(NSIndexPath*indexPathinsortedIndexPaths){ASCellNodeBlock nodeBlock=[_dataSource dataController:selfnodeBlockAtIndexPath:indexPath];ASSizeRange constrainedSize=[selfconstrainedSizeForNodeOfKind:ASDataControllerRowNodeKind atIndexPath:indexPath];[contexts addObject:[[ASIndexedNodeContext alloc]initWithNodeBlock:nodeBlock? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? indexPath:indexPath? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? supplementaryElementKind:nil? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? constrainedSize:constrainedSize? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? environment:environment]];}ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(_nodeContexts[ASDataControllerRowNodeKind],sortedIndexPaths,contexts);dispatch_group_async(_editingTransactionGroup,_editingTransactionQueue,^{[self_batchLayoutAndInsertNodesFromContexts:contexts withAnimationOptions:animationOptions];});}
上面的方法總共做了幾件事情:
遍歷所有要插入的NSIndexPath數(shù)組,然后從數(shù)據源中獲取對應的ASCellNodeBlock;
獲取每一個NSIndexPath對應的單元的大小constrainedSize(在圖中沒有表現(xiàn)出來);
初始化一堆ASIndexedNodeContext實例,然后加入到控制器維護的_nodeContexts數(shù)組中;
將節(jié)點插入到_completedNodes中,用于之后的緩存,以及提供給ASTableView的數(shù)據源代理方法使用;
ASTableView會將數(shù)據源協(xié)議的代理設置為自己,而最常見的數(shù)據源協(xié)議在ASTableView中的實現(xiàn)是這樣的:
Objective-C
-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath{_ASTableViewCell*cell=[selfdequeueReusableCellWithIdentifier:kCellReuseIdentifier forIndexPath:indexPath];cell.delegate=self;ASCellNode*node=[_dataController nodeAtCompletedIndexPath:indexPath];if(node){[_rangeController configureContentView:cell.contentView forCellNode:node];cell.node=node;cell.backgroundColor=node.backgroundColor;cell.selectionStyle=node.selectionStyle;cell.clipsToBounds=node.clipsToBounds;}returncell;}
上面的方法會從ASDataController中的_completedNodes中獲取元素的數(shù)量信息:

在內部_externalCompletedNodes與_completedNodes作用基本相同,在這里我們不對它們的區(qū)別進行分析以及解釋。
當ASTableView向數(shù)據源請求數(shù)據時,ASDK 就會從對應的ASDataController中取回最新的node,添加在_ASTableViewCell的實例上顯示出來。
ASTableView 和 ASTableNode
ASTableView和ASTableNode的關系,其實就相當于CALayer和UIView的關系一樣,后者都是前者的一個包裝:

ASTableNode為開發(fā)者提供了非常多的接口,其內部實現(xiàn)往往都是直接調用ASTableView的對應方法,在這里簡單舉幾個例子:
Objective-C
-(void)insertSections:(NSIndexSet*)sections withRowAnimation:(UITableViewRowAnimation)animation{[self.view insertSections:sections withRowAnimation:animation];}-(void)deleteSections:(NSIndexSet*)sections withRowAnimation:(UITableViewRowAnimation)animation{[self.view deleteSections:sections withRowAnimation:animation];}
如果你再去看ASTableView中方法的實現(xiàn)的話,會發(fā)現(xiàn)很多方法都是由ASDataController和ASRangeController驅動的,上面的兩個方法的實現(xiàn)就是這樣的:
Objective-C
-(void)insertSections:(NSIndexSet*)sections withRowAnimation:(UITableViewRowAnimation)animation{if(sections.count==0){return;}[_dataController insertSections:sections withAnimationOptions:animation];}-(void)deleteSections:(NSIndexSet*)sections withRowAnimation:(UITableViewRowAnimation)animation{if(sections.count==0){return;}[_dataController deleteSections:sections withAnimationOptions:animation];}
到這里,整個智能預加載的部分就結束了,從需要預加載的資源以及預加載發(fā)出的時間兩個方面來考慮,ASDK 在不同工作區(qū)域中合理標記了需要預加載的資源,并在節(jié)點狀態(tài)改變時就發(fā)出請求,在用戶體驗上是非常優(yōu)秀的。
總結
ASDK 中的表視圖以及智能預加載其實都是通過下面這四者共同實現(xiàn)的,上層只會暴露出ASTableNode的接口,所有的數(shù)據的批量更新、工作區(qū)域的管理都是在幕后由ASDataController以及ASRangeController這兩個控制器協(xié)作完成。

智能預加載的使用相比其它實現(xiàn)可能相對復雜,但是在筆者看來,ASDK 對于這一套機制的實現(xiàn)還是非常完善的,同時也提供了極其優(yōu)秀的用戶體驗,不過同時帶來的也是相對較高的學習成本。
如果真正要選擇預加載的機制,筆者覺得最好從 Threshold 以及智能預加載兩種方式中選擇:

這兩種方式的選擇,其實也就是實現(xiàn)復雜度和用戶體驗之間的權衡了。
Github Repo:iOS-Source-Code-Analyze
Follow:Draveness · GitHub
Source:http://draveness.me/preload