預(yù)加載與智能預(yù)加載(iOS)

前兩次的分享分別介紹了 ASDK 對(duì)于渲染的優(yōu)化以及 ASDK 中使用的另一種布局模型;這兩個(gè)新機(jī)制的引入分別解決了 iOS 在主線程渲染視圖以及 Auto Layout 的性能問(wèn)題,而這一次討論的主要內(nèi)容是 ASDK 如何預(yù)先請(qǐng)求服務(wù)器數(shù)據(jù),達(dá)到看似無(wú)限滾動(dòng)列表的效果的。

這篇文章是 ASDK 系列中的最后一篇凡资,文章會(huì)介紹 iOS 中幾種預(yù)加載的方案,以及 ASDK 中是如何處理預(yù)加載的。

不過(guò)壹堰,在介紹 ASDK 中實(shí)現(xiàn)智能預(yù)加載的方式之前,文章中會(huì)介紹幾種簡(jiǎn)單的預(yù)加載方式骡湖,方便各位開(kāi)發(fā)者進(jìn)行對(duì)比贱纠,選擇合適的機(jī)制實(shí)現(xiàn)預(yù)加載這一功能。

網(wǎng)絡(luò)與性能

ASDK 通過(guò)在渲染視圖和布局方面的優(yōu)化已經(jīng)可以使應(yīng)用在任何用戶(hù)的瘋狂操作下都能保持 60 FPS 的流暢程度响蕴,也就是說(shuō)谆焊,我們已經(jīng)充分的利用了當(dāng)前設(shè)備的性能,調(diào)動(dòng)各種資源加快視圖的渲染浦夷。

但是辖试,僅僅在 CPU 以及 GPU 方面的優(yōu)化往往是遠(yuǎn)遠(yuǎn)不夠的。在目前的軟件開(kāi)發(fā)中劈狐,很難找到一個(gè)沒(méi)有任何網(wǎng)絡(luò)請(qǐng)求的應(yīng)用罐孝,哪怕是一個(gè)記賬軟件也需要服務(wù)器來(lái)同步保存用戶(hù)的信息,防止資料的丟失肥缔;所以莲兢,只在渲染這一層面進(jìn)行優(yōu)化還不能讓用戶(hù)的體驗(yàn)達(dá)到最佳,因?yàn)榫W(wǎng)絡(luò)請(qǐng)求往往是一個(gè)應(yīng)用最為耗時(shí)以及昂貴的操作辫继。

network

每一個(gè)應(yīng)用程序在運(yùn)行時(shí)都可以看做是 CPU 在底層利用各種?資源瘋狂做加減法運(yùn)算怒见,其中最耗時(shí)的操作并不是進(jìn)行加減法的過(guò)程,而是資源轉(zhuǎn)移的過(guò)程姑宽。

舉一個(gè)不是很恰當(dāng)?shù)睦忧菜#鲝N(CPU)在炒一道菜(計(jì)算)時(shí)往往需要的時(shí)間并不多,但是菜的采購(gòu)以及準(zhǔn)備(資源的轉(zhuǎn)移)會(huì)占用大量的時(shí)間炮车,如果在每次炒菜之前舵变,都由幫廚提前準(zhǔn)備好所有的食材(緩存),那么做一道菜的時(shí)間就大大減少了瘦穆。

而提高資源轉(zhuǎn)移的效率的最佳辦法就是使用多級(jí)緩存:

multi-laye

從上到下纪隙,雖然容量越來(lái)越大,直到 Network 層包含了整個(gè)互聯(lián)網(wǎng)的內(nèi)容扛或,但是訪問(wèn)時(shí)間也是直線上升绵咱;在 Core 或者三級(jí)緩存中的資源可能訪問(wèn)只需要幾個(gè)或者幾十個(gè)時(shí)鐘周期,但是網(wǎng)絡(luò)中的資源就遠(yuǎn)遠(yuǎn)大于這個(gè)數(shù)字熙兔,幾分鐘悲伶、幾小時(shí)都是有可能的艾恼。

更糟糕的是,因?yàn)樘斐木W(wǎng)絡(luò)情況及其復(fù)雜麸锉,運(yùn)營(yíng)商劫持 DNS钠绍、404 無(wú)法訪問(wèn)等問(wèn)題導(dǎo)致網(wǎng)絡(luò)問(wèn)題極其嚴(yán)重;而如何加速網(wǎng)絡(luò)請(qǐng)求成為了很多移動(dòng)端以及 Web 應(yīng)用的重要問(wèn)題花沉。

預(yù)加載

本文就會(huì)提供一種緩解網(wǎng)絡(luò)請(qǐng)求緩慢導(dǎo)致用戶(hù)體驗(yàn)較差的解決方案柳爽,也就是預(yù)加載;在本地真正需要渲染界面之前就通過(guò)網(wǎng)絡(luò)請(qǐng)求獲取資源存入內(nèi)存或磁盤(pán)碱屁。

預(yù)加載并不能徹底解決網(wǎng)絡(luò)請(qǐng)求緩慢的問(wèn)題磷脯,而是通過(guò)提前發(fā)起網(wǎng)絡(luò)請(qǐng)求緩解這一問(wèn)題。

那么娩脾,預(yù)加載到底要關(guān)注哪些方面的問(wèn)題呢争拐?總結(jié)下來(lái),有以下兩個(gè)關(guān)注點(diǎn):

  • 需要預(yù)加載的資源
  • 預(yù)加載發(fā)出的時(shí)間

文章會(huì)根據(jù)上面的兩個(gè)關(guān)注點(diǎn)晦雨,分別分析四種預(yù)加載方式的實(shí)現(xiàn)原理以及優(yōu)缺點(diǎn):

  1. 無(wú)限滾動(dòng)列表
  2. threshold
  3. 惰性加載
  4. 智能預(yù)加載

無(wú)限滾動(dòng)列表

其實(shí),無(wú)限滾動(dòng)列表并不能算是一種預(yù)加載的實(shí)現(xiàn)原理隘冲,它只是提供一種分頁(yè)顯示的方法闹瞧,在每次滾動(dòng)到 UITableView 底部時(shí),才會(huì)開(kāi)始發(fā)起網(wǎng)絡(luò)請(qǐng)求向服務(wù)器獲取對(duì)應(yīng)的資源展辞。

雖然這種方法并不是預(yù)加載方式的一種奥邮,放在這里的主要作用是作為對(duì)比方案,看看如果不使用預(yù)加載的機(jī)制罗珍,用戶(hù)體驗(yàn)是什么樣的洽腺。

infinite-list

很多客戶(hù)端都使用了分頁(yè)的加載方式,并沒(méi)有添加額外的預(yù)加載的機(jī)制來(lái)提升用戶(hù)體驗(yàn)覆旱,雖然這種方式并不是不能接受蘸朋,不過(guò)每次滑動(dòng)到視圖底部之后,總要等待網(wǎng)絡(luò)請(qǐng)求的完成確實(shí)對(duì)視圖的流暢性有一定影響扣唱。

雖然僅僅使用無(wú)限滾動(dòng)列表而不提供預(yù)加載機(jī)制會(huì)在一定程度上影響用戶(hù)體驗(yàn)藕坯,不過(guò),這種需要用戶(hù)等待幾秒鐘的方式噪沙,在某些時(shí)候確實(shí)非常好用炼彪,比如:投放廣告。

advertise

QQ 空間就是這么做的正歼,它們投放的廣告基本都是在整個(gè)列表的最底端辐马,這樣,當(dāng)你滾動(dòng)到列表最下面的時(shí)候局义,就能看到你急需的租房喜爷、租車(chē)冗疮、同城交友、信用卡辦理贞奋、只有 iPhone 能玩的游戲以及各種奇奇怪怪的辣雞廣告了赌厅,很好的解決了我們的日常生活中的各種需求。(哈哈哈哈哈哈哈哈哈哈哈哈哈)

Threshold

使用 Threshold 進(jìn)行預(yù)加載是一種最為常見(jiàn)的預(yù)加載方式轿塔,知乎客戶(hù)端就使用了這種方式預(yù)加載條目特愿,而其原理也非常簡(jiǎn)單,根據(jù)當(dāng)前 UITableView 的所在位置勾缭,除以目前整個(gè) UITableView.contentView 的高度揍障,來(lái)判斷當(dāng)前是否需要發(fā)起網(wǎng)絡(luò)請(qǐng)求:

let threshold: CGFloat = 0.7
var currentPage = 0

override func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let current = scrollView.contentOffset.y + scrollView.frame.size.height
    let total = scrollView.contentSize.height
    let ratio = current / total

    if ratio >= threshold {
        currentPage += 1
        print("Request page \(currentPage) from server.")
    }
}

上面的代碼在當(dāng)前頁(yè)面已經(jīng)劃過(guò)了 70% 的時(shí)候,就請(qǐng)求新的資源俩由,加載數(shù)據(jù)毒嫡;但是,僅僅使用這種方法會(huì)有另一個(gè)問(wèn)題幻梯,尤其是當(dāng)列表變得很長(zhǎng)時(shí)兜畸,十分明顯,比如說(shuō):用戶(hù)從上向下滑動(dòng)碘梢,總共加載了 5 頁(yè)數(shù)據(jù):

Page Total Threshold Diff
1 10 7 7
2 20 14 4
3 30 21 1
4 40 28 -2
5 50 35 -5
  • Page 當(dāng)前總頁(yè)數(shù)咬摇;
  • Total 當(dāng)前 UITableView 總元素個(gè)數(shù);
  • Threshold 網(wǎng)絡(luò)請(qǐng)求觸發(fā)時(shí)間煞躬;
  • Diff 表示最新加載的頁(yè)面被瀏覽了多少肛鹏;

當(dāng) Threshold 設(shè)置為 70% 的時(shí)候,其實(shí)并不是單頁(yè) 70%恩沛,這就會(huì)導(dǎo)致新加載的頁(yè)面都沒(méi)有看在扰,應(yīng)用就會(huì)發(fā)出另一次請(qǐng)求,獲取新的資源雷客。

動(dòng)態(tài)的 Threshold

解決這個(gè)問(wèn)題的辦法芒珠,還是比較簡(jiǎn)單的,通過(guò)修改上面的代碼佛纫,將 Threshold 變成一個(gè)動(dòng)態(tài)的值妓局,隨著頁(yè)數(shù)的增長(zhǎng)而增長(zhǎng):

let threshold:   CGFloat = 0.7
let itemPerPage: CGFloat = 10
var currentPage: CGFloat = 0

override func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let current = scrollView.contentOffset.y + scrollView.frame.size.height
    let total = scrollView.contentSize.height
    let ratio = current / total

    let needRead = itemPerPage * threshold + currentPage * itemPerPage
    let totalItem = itemPerPage * (currentPage + 1)
    let newThreshold = needRead / totalItem

    if ratio >= newThreshold {
        currentPage += 1
        print("Request page \(currentPage) from server.")
    }
}

通過(guò)這種方法獲取的 newThreshold 就會(huì)隨著頁(yè)數(shù)的增長(zhǎng)而動(dòng)態(tài)的改變,解決了上面出現(xiàn)的問(wèn)題:

dynamic-threshold

惰性加載

使用 Threshold 進(jìn)行預(yù)加載其實(shí)已經(jīng)適用于大多數(shù)應(yīng)用場(chǎng)景了呈宇;但是好爬,下面介紹的方式,惰性加載能夠有針對(duì)性的加載用戶(hù)“會(huì)看到的” Cell甥啄。

惰性加載存炮,就是在用戶(hù)滾動(dòng)的時(shí)候會(huì)對(duì)用戶(hù)滾動(dòng)結(jié)束的區(qū)域進(jìn)行計(jì)算,只加載目標(biāo)區(qū)域中的資源。

用戶(hù)在飛速滾動(dòng)中會(huì)看到巨多的空白條目穆桂,因?yàn)橛脩?hù)并不想閱讀這些條目宫盔,所以,我們并不需要真正去加載這些內(nèi)容享完,只需要在 ASTableView/ASCollectionView 中只根據(jù)用戶(hù)滾動(dòng)的目標(biāo)區(qū)域惰性加載資源灼芭。

lazy-loading

惰性加載的方式不僅僅減少了網(wǎng)絡(luò)請(qǐng)求的冗余資源,同時(shí)也減少了渲染視圖般又、數(shù)據(jù)綁定的耗時(shí)彼绷。

計(jì)算用戶(hù)滾動(dòng)的目標(biāo)區(qū)域可以直接使用下面的代理方法獲取:

let markedView = UIView()
let rowHeight: CGFloat = 44.0

override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    let targetOffset = targetContentOffset.pointee
    let targetRect = CGRect(origin: targetOffset, size: scrollView.frame.size)

    markedView.frame = targetRect
    markedView.backgroundColor = UIColor.black.withAlphaComponent(0.1)
    tableView.addSubview(markedView)

    var indexPaths: [IndexPath] = []

    let startIndex = Int(targetRect.origin.y / rowHeight)
    let endIndex = Int((targetRect.origin.y + tableView.frame.height) / rowHeight)

    for index in startIndex...endIndex {
        indexPaths.append(IndexPath(row: index, section: 0))
    }

    print("\(targetRect) \(indexPaths)")
}

以上代碼只會(huì)大致計(jì)算出目標(biāo)區(qū)域內(nèi)的 IndexPath 數(shù)組茴迁,并不會(huì)展開(kāi)新的 page寄悯,同時(shí)會(huì)使用淺黑色標(biāo)記目標(biāo)區(qū)域。

當(dāng)然堕义,惰性加載的實(shí)現(xiàn)也并不只是這么簡(jiǎn)單猜旬,不僅需要客戶(hù)端的工作,同時(shí)因?yàn)樾枰?strong>加載特定 offset 資源倦卖,也需要服務(wù)端提供相應(yīng) API 的支持洒擦。

雖然惰性加載的方式能夠按照用戶(hù)的需要請(qǐng)求對(duì)應(yīng)的資源,但是怕膛,在用戶(hù)滑動(dòng) UITableView 的過(guò)程中會(huì)看到大量的空白條目秘遏,這樣的用戶(hù)體驗(yàn)是否可以接受又是值得考慮的問(wèn)題了。

智能預(yù)加載

終于到了智能預(yù)加載的部分了嘉竟,當(dāng)我第一次得知 ASDK 可以通過(guò)滾動(dòng)的方向預(yù)加載不同數(shù)量的內(nèi)容,感覺(jué)是非常神奇的撞芍。

如上圖所示 ASDK 把正在滾動(dòng)的 ASTableView/ASCollectionView 劃分為三種狀態(tài):

  • Fetch Data
  • Display
  • Visible

上面的這三種狀態(tài)都是由 ASDK 來(lái)管理的米死,而每一個(gè) ASCellNode 的狀態(tài)都是由 ASRangeController 控制酵镜,所有的狀態(tài)都對(duì)應(yīng)一個(gè) ASInterfaceState

  • ASInterfaceStatePreload 當(dāng)前元素貌似要顯示到屏幕上,需要從磁盤(pán)或者網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)边苹;
  • ASInterfaceStateDisplay 當(dāng)前元素非常可能要變成可見(jiàn)的裁僧,需要進(jìn)行異步繪制个束;
  • ASInterfaceStateVisible 當(dāng)前元素最少在屏幕上顯示了 1px

當(dāng)用戶(hù)滾動(dòng)當(dāng)前視圖時(shí),ASRangeController 就會(huì)修改不同區(qū)域內(nèi)元素的狀態(tài):

上圖是用戶(hù)在向下滑動(dòng)時(shí)聊疲,ASCellNode 是如何被標(biāo)記的茬底,假設(shè)當(dāng)前視圖可見(jiàn)的范圍高度為 1,那么在默認(rèn)情況下获洲,五個(gè)區(qū)域會(huì)按照上圖的形式進(jìn)行劃分:

Buffer Size
Fetch Data Leading Buffer 2
Display Leading Buffer 1
Visible 1
Display Trailing Buffer 1
Fetch Data Trailing Buffer 1

在滾動(dòng)方向(Leading)上 Fetch Data 區(qū)域會(huì)是非滾動(dòng)方向(Trailing)的兩倍阱表,ASDK 會(huì)根據(jù)滾動(dòng)方向的變化實(shí)時(shí)改變緩沖區(qū)的位置;在向下滾動(dòng)時(shí),下面的 Fetch Data 區(qū)域就是上面的兩倍最爬,向上滾動(dòng)時(shí)涉馁,上面的 Fetch Data 區(qū)域就是下面的兩倍。

這里的兩倍并不是一個(gè)確定的數(shù)值爱致,ASDK 會(huì)根據(jù)當(dāng)前設(shè)備的不同狀態(tài)烤送,改變不同區(qū)域的大小,但是滾動(dòng)方向的區(qū)域總會(huì)比非滾動(dòng)方向大一些糠悯。

智能預(yù)加載能夠根據(jù)當(dāng)前的滾動(dòng)方向帮坚,自動(dòng)改變當(dāng)前的工作區(qū)域,選擇合適的區(qū)域提前觸發(fā)請(qǐng)求資源逢防、渲染視圖以及異步布局等操作叶沛,讓視圖的滾動(dòng)達(dá)到真正的流暢。

原理

在 ASDK 中整個(gè)智能預(yù)加載的概念是由三個(gè)部分來(lái)統(tǒng)一協(xié)調(diào)管理的:

  • ASRangeController
  • ASDataController
  • ASTableViewASTableNode

對(duì)智能預(yù)加載實(shí)現(xiàn)的分析忘朝,也是根據(jù)這三個(gè)部分來(lái)介紹的灰署。

工作區(qū)域的管理

ASRangeControllerASTableView 以及 ASCollectionView 內(nèi)部使用的控制器,主要用于監(jiān)控視圖的可見(jiàn)區(qū)域局嘁、維護(hù)工作區(qū)域溉箕、觸發(fā)網(wǎng)絡(luò)請(qǐng)求以及繪制、單元格的異步布局悦昵。

ASTableView 為例肴茄,在視圖進(jìn)行滾動(dòng)時(shí),會(huì)觸發(fā) -[UIScrollView scrollViewDidScroll:] 代理方法:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  ASInterfaceState interfaceState = [self interfaceStateForRangeController:_rangeController];
  if (ASInterfaceStateIncludesVisible(interfaceState)) {
    [_rangeController updateCurrentRangeWithMode:ASLayoutRangeModeFull];
  }
  ...
}

每一個(gè) ASTableView 的實(shí)例都持有一個(gè) ASRangeController 以及 ASDataController 用于管理工作區(qū)域以及數(shù)據(jù)更新但指。

ASRangeController 最重要的私有方法 -[ASRangeController _updateVisibleNodeIndexPaths] 一般都是因?yàn)樯厦娴姆椒ㄩg接調(diào)用的:

-[ASRangeController updateCurrentRangeWithMode:]
    -[ASRangeController setNeedsUpdate]
        -[ASRangeController updateIfNeeded]
            -[ASRangeController _updateVisibleNodeIndexPaths]

調(diào)用棧中間的過(guò)程其實(shí)并不重要寡痰,最后的私有方法的主要工作就是計(jì)算不同區(qū)域內(nèi) Cell 的 NSIndexPath 數(shù)組,然后更新對(duì)應(yīng) Cell 的狀態(tài) ASInterfaceState 觸發(fā)對(duì)應(yīng)的操作棋凳。

我們將這個(gè)私有方法的實(shí)現(xiàn)分開(kāi)來(lái)看:

- (void)_updateVisibleNodeIndexPaths {
  NSArray<NSArray *> *allNodes = [_dataSource completedNodes];
  NSUInteger numberOfSections = [allNodes count];

  NSArray<NSIndexPath *> *visibleNodePaths = [_dataSource visibleNodeIndexPathsForRangeController:self];

  ASScrollDirection scrollDirection = [_dataSource scrollDirectionForRangeController:self];
  if (_layoutControllerImplementsSetViewportSize) {
    [_layoutController setViewportSize:[_dataSource viewportSizeForRangeController:self]];
  }
  
  if (_layoutControllerImplementsSetVisibleIndexPaths) {
    [_layoutController setVisibleNodeIndexPaths:visibleNodePaths];
  }
  ...
}

當(dāng)前 ASRangeController 的數(shù)據(jù)源以及代理就是 ASTableView拦坠,這段代碼首先就獲取了完成計(jì)算和布局的 ASCellNode 以及可見(jiàn)的 ASCellNodeNSIndexPath

- (void)_updateVisibleNodeIndexPaths {  
  NSArray<ASDisplayNode *> *currentSectionNodes = nil;
  NSInteger currentSectionIndex = -1;
  NSUInteger numberOfNodesInSection = 0;
  
  NSSet<NSIndexPath *> *visibleIndexPaths = [NSSet setWithArray:visibleNodePaths];
  NSSet<NSIndexPath *> *displayIndexPaths = nil;
  NSSet<NSIndexPath *> *preloadIndexPaths = nil;
  
  NSMutableOrderedSet<NSIndexPath *> *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 的計(jì)算和 preloadIndexPaths 非常類(lèi)似
  
  [allIndexPaths unionSet:displayIndexPaths];
  [allIndexPaths unionSet:preloadIndexPaths];
  ...
}

預(yù)加載以及展示部分的 ASRangeTuningParameters 都是以二維數(shù)組的形式保存在 ASAbstractLayoutController 中的:

aslayout-range-mode-display-preload

在獲取了 ASRangeTuningParameters 之后,ASDK 也會(huì)通過(guò) ASFlowLayoutController 的方法 -[ASFlowLayoutController indexPathsForScrolling:rangeMode:rangeType:] 獲取 NSIndexPath 對(duì)象的集合:

- (NSSet *)indexPathsForScrolling:(ASScrollDirection)scrollDirection rangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType {
  #: 獲取 directionalBuffer 以及 viewportDirectionalSize
  ASIndexPath startPath = [self findIndexPathAtDistance:(-directionalBuffer.negativeDirection * viewportDirectionalSize)
                                          fromIndexPath:_visibleRange.start];
  ASIndexPath endPath   = [self findIndexPathAtDistance:(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 < endPath.section) {
      currPath.row = 0;
      currPath.section++;
    }
  }
  [indexPathSet addObject:[NSIndexPath indexPathWithASIndexPath:endPath]];
  return indexPathSet;
}

方法的執(zhí)行過(guò)程非常簡(jiǎn)單剩岳,根據(jù) ASRangeTuningParameters 獲取該滾動(dòng)方向上的緩沖區(qū)大小贞滨,在區(qū)域內(nèi)遍歷所有的 ASCellNode 查看其是否在當(dāng)前區(qū)域內(nèi),然后加入數(shù)組中拍棕。

到這里晓铆,所有工作區(qū)域 visibleIndexPaths displayIndexPaths 以及 preloadIndexPaths 都已經(jīng)獲取到了;接下來(lái)绰播,就到了遍歷 NSIndexPath骄噪,修改結(jié)點(diǎn)狀態(tài)的過(guò)程了;

- (void)_updateVisibleNodeIndexPaths {
  ...
  for (NSIndexPath *indexPath in allIndexPaths) {
    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;
        }
      }
    }

根據(jù)當(dāng)前 ASTableView 的狀態(tài)以及 NSIndexPath 所在的區(qū)域蠢箩,打開(kāi) ASInterfaceState 對(duì)應(yīng)的位腰池。

    NSInteger section = indexPath.section;
    NSInteger row     = indexPath.row;
    
    if (section >= 0 && row >= 0 && section < numberOfSections) {
      if (section != currentSectionIndex) {
        currentSectionNodes = allNodes[section];
        numberOfNodesInSection = [currentSectionNodes count];
        currentSectionIndex = section;
      }
      
      if (row < numberOfNodesInSection) {
        ASDisplayNode *node = currentSectionNodes[row];
        
        if (node.interfaceState != interfaceState) {
          BOOL nodeShouldScheduleDisplay = [node shouldScheduleDisplayWithNewInterfaceState:interfaceState];
          [node recursivelySetInterfaceState:interfaceState];
          
          if (nodeShouldScheduleDisplay) {
            [self registerForNodeDisplayNotificationsForInterfaceStateIfNeeded:selfInterfaceState];
            if (_didRegisterForNodeDisplayNotifications) {
              _pendingDisplayNodesTimestamp = CFAbsoluteTimeGetCurrent();
            }
          }
        }
      }
    }
  }
  ...
}

后面的一部分代碼就會(huì)遞歸的設(shè)置結(jié)點(diǎn)的 interfaceState尾组,并且在當(dāng)前 ASRangeControllerASLayoutRangeMode 發(fā)生改變時(shí),發(fā)出通知示弓,調(diào)用 -[ASRangeController _updateVisibleNodeIndexPaths] 私有方法讳侨,更新結(jié)點(diǎn)的狀態(tài)。

- (void)scheduledNodesDidDisplay:(NSNotification *)notification {
  CFAbsoluteTime notificationTimestamp = ((NSNumber *) notification.userInfo[ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp]).doubleValue;
  if (_pendingDisplayNodesTimestamp < notificationTimestamp) {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:ASRenderingEngineDidDisplayScheduledNodesNotification object:nil];
    _didRegisterForNodeDisplayNotifications = NO;
    
    [self setNeedsUpdate];
  }
}

數(shù)據(jù)的加載和更新

ASTableNode 既然是對(duì) ASTableView 的封裝奏属,那么表視圖中顯示的數(shù)據(jù)仍然需要數(shù)據(jù)源來(lái)提供跨跨,而在 ASDK 中這一機(jī)制就比較復(fù)雜:

astableview-data

整個(gè)過(guò)程是由四部分協(xié)作完成的,Controller囱皿、ASTableNode勇婴、ASTableView 以及 ASDataController,網(wǎng)絡(luò)請(qǐng)求發(fā)起并返回?cái)?shù)據(jù)之后嘱腥,會(huì)調(diào)用 ASTableNode 的 API 執(zhí)行插入行的方法耕渴,最后再通過(guò) ASTableView 的同名方法,執(zhí)行管理和更新節(jié)點(diǎn)數(shù)據(jù)的 ASDataController 的方法:

- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions {
  dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER);

  NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)];
  NSMutableArray<ASIndexedNodeContext *> *contexts = [[NSMutableArray alloc] initWithCapacity:indexPaths.count];

  __weak id<ASEnvironment> environment = [self.environmentDelegate dataControllerEnvironment];
  
  for (NSIndexPath *indexPath in sortedIndexPaths) {
    ASCellNodeBlock nodeBlock = [_dataSource dataController:self nodeBlockAtIndexPath:indexPath];
    ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind: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];
  });
}

上面的方法總共做了幾件事情:

  1. 遍歷所有要插入的 NSIndexPath 數(shù)組齿兔,然后從數(shù)據(jù)源中獲取對(duì)應(yīng)的 ASCellNodeBlock橱脸;
  2. 獲取每一個(gè) NSIndexPath 對(duì)應(yīng)的單元的大小 constrainedSize(在圖中沒(méi)有表現(xiàn)出來(lái));
  3. 初始化一堆 ASIndexedNodeContext 實(shí)例分苇,然后加入到控制器維護(hù)的 _nodeContexts 數(shù)組中添诉;
  4. 將節(jié)點(diǎn)插入到 _completedNodes 中,用于之后的緩存医寿,以及提供給 ASTableView 的數(shù)據(jù)源代理方法使用栏赴;

ASTableView 會(huì)將數(shù)據(jù)源協(xié)議的代理設(shè)置為自己,而最常見(jiàn)的數(shù)據(jù)源協(xié)議在 ASTableView 中的實(shí)現(xiàn)是這樣的:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  _ASTableViewCell *cell = [self dequeueReusableCellWithIdentifier: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;
  }

  return cell;
}

上面的方法會(huì)從 ASDataController 中的 _completedNodes 中獲取元素的數(shù)量信息:

cellforrowatindexpath

在內(nèi)部 _externalCompletedNodes_completedNodes 作用基本相同靖秩,在這里我們不對(duì)它們的區(qū)別進(jìn)行分析以及解釋须眷。

當(dāng) ASTableView 向數(shù)據(jù)源請(qǐng)求數(shù)據(jù)時(shí),ASDK 就會(huì)從對(duì)應(yīng)的 ASDataController 中取回最新的 node沟突,添加在 _ASTableViewCell 的實(shí)例上顯示出來(lái)柒爸。

ASTableView 和 ASTableNode

ASTableViewASTableNode 的關(guān)系,其實(shí)就相當(dāng)于 CALayerUIView 的關(guān)系一樣事扭,后者都是前者的一個(gè)包裝:

astableview-astablenode

ASTableNode 為開(kāi)發(fā)者提供了非常多的接口,其內(nèi)部實(shí)現(xiàn)往往都是直接調(diào)用 ASTableView 的對(duì)應(yīng)方法乐横,在這里簡(jiǎn)單舉幾個(gè)例子:

- (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 中方法的實(shí)現(xiàn)的話求橄,會(huì)發(fā)現(xiàn)很多方法都是由 ASDataControllerASRangeController 驅(qū)動(dòng)的,上面的兩個(gè)方法的實(shí)現(xiàn)就是這樣的:

- (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];
}

到這里葡公,整個(gè)智能預(yù)加載的部分就結(jié)束了罐农,從需要預(yù)加載的資源以及預(yù)加載發(fā)出的時(shí)間兩個(gè)方面來(lái)考慮,ASDK 在不同工作區(qū)域中合理標(biāo)記了需要預(yù)加載的資源催什,并在節(jié)點(diǎn)狀態(tài)改變時(shí)就發(fā)出請(qǐng)求涵亏,在用戶(hù)體驗(yàn)上是非常優(yōu)秀的。

總結(jié)

ASDK 中的表視圖以及智能預(yù)加載其實(shí)都是通過(guò)下面這四者共同實(shí)現(xiàn)的,上層只會(huì)暴露出 ASTableNode 的接口气筋,所有的數(shù)據(jù)的批量更新拆内、工作區(qū)域的管理都是在幕后由 ASDataController 以及 ASRangeController 這兩個(gè)控制器協(xié)作完成。

multi-layer-asdk

智能預(yù)加載的使用相比其它實(shí)現(xiàn)可能相對(duì)復(fù)雜宠默,但是在筆者看來(lái)麸恍,ASDK 對(duì)于這一套機(jī)制的實(shí)現(xiàn)還是非常完善的,同時(shí)也提供了極其優(yōu)秀的用戶(hù)體驗(yàn)搀矫,不過(guò)同時(shí)帶來(lái)的也是相對(duì)較高的學(xué)習(xí)成本抹沪。

如果真正要選擇預(yù)加載的機(jī)制,筆者覺(jué)得最好從 Threshold 以及智能預(yù)加載兩種方式中選擇:

pros-cons

這兩種方式的選擇瓤球,其實(shí)也就是實(shí)現(xiàn)復(fù)雜度和用戶(hù)體驗(yàn)之間的權(quán)衡了融欧。

Github Repo:iOS-Source-Code-Analyze

Follow: Draveness · GitHub

Source: http://draveness.me/preload

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市卦羡,隨后出現(xiàn)的幾起案子噪馏,更是在濱河造成了極大的恐慌,老刑警劉巖虹茶,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逝薪,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡蝴罪,警方通過(guò)查閱死者的電腦和手機(jī)董济,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)要门,“玉大人虏肾,你說(shuō)我怎么就攤上這事』端眩” “怎么了封豪?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)炒瘟。 經(jīng)常有香客問(wèn)我吹埠,道長(zhǎng),這世上最難降的妖魔是什么疮装? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任缘琅,我火速辦了婚禮,結(jié)果婚禮上廓推,老公的妹妹穿的比我還像新娘刷袍。我一直安慰自己,他們只是感情好樊展,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布呻纹。 她就那樣靜靜地躺著堆生,像睡著了一般。 火紅的嫁衣襯著肌膚如雪雷酪。 梳的紋絲不亂的頭發(fā)上淑仆,一...
    開(kāi)封第一講書(shū)人閱讀 51,624評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音太闺,去河邊找鬼糯景。 笑死,一個(gè)胖子當(dāng)著我的面吹牛省骂,可吹牛的內(nèi)容都是我干的蟀淮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼钞澳,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼怠惶!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起轧粟,我...
    開(kāi)封第一講書(shū)人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤策治,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后兰吟,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體通惫,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年混蔼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了履腋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡惭嚣,死狀恐怖遵湖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情晚吞,我是刑警寧澤延旧,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站槽地,受9級(jí)特大地震影響迁沫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜捌蚊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一集畅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧逢勾,春花似錦、人聲如沸藐吮。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至迫摔,卻和暖如春沐扳,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背句占。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工沪摄, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人纱烘。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓杨拐,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親擂啥。 傳聞我的和親對(duì)象是個(gè)殘疾皇子哄陶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

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

  • 前兩次的分享分別介紹了 ASDK 對(duì)于渲染的優(yōu)化以及 ASDK 中使用的另一種布局模型;這兩個(gè)新機(jī)制的引入分別解決...
    Kevin追夢(mèng)先生閱讀 4,909評(píng)論 2 12
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,161評(píng)論 25 707
  • 翻譯自“Collection View Programming Guide for iOS” 0 關(guān)于iOS集合視...
    lakerszhy閱讀 3,866評(píng)論 1 22
  • 落花隨雨碾入水 花影眷眷兩相映 雨襲是寒寒是我 心似清明人無(wú)意
    應(yīng)如是閱讀 241評(píng)論 0 1
  • 下午實(shí)在是無(wú)聊點(diǎn)進(jìn)騰訊視頻首頁(yè)就是惡棍天使哺壶,就點(diǎn)進(jìn)去看完了屋吨,給我影響最深的就是中間鄧超說(shuō)的一段話 當(dāng)時(shí)看完這段心里...
    默辰閱讀 323評(píng)論 0 0