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

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

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

不過,在介紹 ASDK 中實現(xiàn)智能預加載的方式之前,文章中會介紹幾種簡單的預加載方式,方便各位開發(fā)者進行對比,選擇合適的機制實現(xiàn)預加載這一功能。

網(wǎng)絡與性能

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

但是,僅僅在 CPU 以及 GPU 方面的優(yōu)化往往是遠遠不夠的。在目前的軟件開發(fā)中,很難找到一個沒有任何網(wǎng)絡請求的應用,哪怕是一個記賬軟件也需要服務器來同步保存用戶的信息,防止資料的丟失;所以,只在渲染這一層面進行優(yōu)化還不能讓用戶的體驗達到最佳,因為網(wǎng)絡請求往往是一個應用最為耗時以及昂貴的操作。

network

每一個應用程序在運行時都可以看做是 CPU 在底層利用各種?資源瘋狂做加減法運算,其中最耗時的操作并不是進行加減法的過程,而是資源轉(zhuǎn)移的過程。

舉一個不是很恰當?shù)睦樱鲝N(CPU)在炒一道菜(計算)時往往需要的時間并不多,但是菜的采購以及準備(資源的轉(zhuǎn)移)會占用大量的時間,如果在每次炒菜之前,都由幫廚提前準備好所有的食材(緩存),那么做一道菜的時間就大大減少了。

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

multi-laye

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

更糟糕的是,因為天朝的網(wǎng)絡情況及其復雜,運營商劫持 DNS、404 無法訪問等問題導致網(wǎng)絡問題極其嚴重;而如何加速網(wǎng)絡請求成為了很多移動端以及 Web 應用的重要問題。

預加載

本文就會提供一種緩解網(wǎng)絡請求緩慢導致用戶體驗較差的解決方案,也就是預加載;在本地真正需要渲染界面之前就通過網(wǎng)絡請求獲取資源存入內(nèi)存或磁盤。

預加載并不能徹底解決網(wǎng)絡請求緩慢的問題,而是通過提前發(fā)起網(wǎng)絡請求緩解這一問題。

那么,預加載到底要關(guān)注哪些方面的問題呢?總結(jié)下來,有以下兩個關(guān)注點:

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

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

  1. 無限滾動列表
  2. threshold
  3. 惰性加載
  4. 智能預加載

無限滾動列表

其實,無限滾動列表并不能算是一種預加載的實現(xiàn)原理,它只是提供一種分頁顯示的方法,在每次滾動到 UITableView 底部時,才會開始發(fā)起網(wǎng)絡請求向服務器獲取對應的資源。

雖然這種方法并不是預加載方式的一種,放在這里的主要作用是作為對比方案,看看如果不使用預加載的機制,用戶體驗是什么樣的。

infinite-list

很多客戶端都使用了分頁的加載方式,并沒有添加額外的預加載的機制來提升用戶體驗,雖然這種方式并不是不能接受,不過每次滑動到視圖底部之后,總要等待網(wǎng)絡請求的完成確實對視圖的流暢性有一定影響。

雖然僅僅使用無限滾動列表而不提供預加載機制會在一定程度上影響用戶體驗,不過,這種需要用戶等待幾秒鐘的方式,在某些時候確實非常好用,比如:投放廣告。

advertise

QQ 空間就是這么做的,它們投放的廣告基本都是在整個列表的最底端,這樣,當你滾動到列表最下面的時候,就能看到你急需的租房、租車、同城交友、信用卡辦理、只有 iPhone 能玩的游戲以及各種奇奇怪怪的辣雞廣告了,很好的解決了我們的日常生活中的各種需求。(哈哈哈哈哈哈哈哈哈哈哈哈哈)

Threshold

使用 Threshold 進行預加載是一種最為常見的預加載方式,知乎客戶端就使用了這種方式預加載條目,而其原理也非常簡單,根據(jù)當前 UITableView 的所在位置,除以目前整個 UITableView.contentView 的高度,來判斷當前是否需要發(fā)起網(wǎ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.")
    }
}

上面的代碼在當前頁面已經(jīng)劃過了 70% 的時候,就請求新的資源,加載數(shù)據(jù);但是,僅僅使用這種方法會有另一個問題,尤其是當列表變得很長時,十分明顯,比如說:用戶從上向下滑動,總共加載了 5 頁數(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 當前總頁數(shù);
  • Total 當前 UITableView 總元素個數(shù);
  • Threshold 網(wǎng)絡請求觸發(fā)時間;
  • Diff 表示最新加載的頁面被瀏覽了多少;

當 Threshold 設置為 70% 的時候,其實并不是單頁 70%,這就會導致新加載的頁面都沒有看,應用就會發(fā)出另一次請求,獲取新的資源

動態(tài)的 Threshold

解決這個問題的辦法,還是比較簡單的,通過修改上面的代碼,將 Threshold 變成一個動態(tài)的值,隨著頁數(shù)的增長而增長:

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.")
    }
}

通過這種方法獲取的 newThreshold 就會隨著頁數(shù)的增長而動態(tài)的改變,解決了上面出現(xiàn)的問題:

dynamic-threshold

惰性加載

使用 Threshold 進行預加載其實已經(jīng)適用于大多數(shù)應用場景了;但是,下面介紹的方式,惰性加載能夠有針對性的加載用戶“會看到的” Cell。

惰性加載,就是在用戶滾動的時候會對用戶滾動結(jié)束的區(qū)域進行計算,只加載目標區(qū)域中的資源。

用戶在飛速滾動中會看到巨多的空白條目,因為用戶并不想閱讀這些條目,所以,我們并不需要真正去加載這些內(nèi)容,只需要在 ASTableView/ASCollectionView 中只根據(jù)用戶滾動的目標區(qū)域惰性加載資源。

lazy-loading

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

計算用戶滾動的目標區(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)")
}

以上代碼只會大致計算出目標區(qū)域內(nèi)的 IndexPath 數(shù)組,并不會展開新的 page,同時會使用淺黑色標記目標區(qū)域。

當然,惰性加載的實現(xiàn)也并不只是這么簡單,不僅需要客戶端的工作,同時因為需要加載特定 offset 資源,也需要服務端提供相應 API 的支持。

雖然惰性加載的方式能夠按照用戶的需要請求對應的資源,但是,在用戶滑動 UITableView 的過程中會看到大量的空白條目,這樣的用戶體驗是否可以接受又是值得考慮的問題了。

智能預加載

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

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

  • Fetch Data
  • Display
  • Visible

上面的這三種狀態(tài)都是由 ASDK 來管理的,而每一個 ASCellNode 的狀態(tài)都是由 ASRangeController 控制,所有的狀態(tài)都對應一個 ASInterfaceState

  • ASInterfaceStatePreload 當前元素貌似要顯示到屏幕上,需要從磁盤或者網(wǎng)絡請求數(shù)據(jù);
  • ASInterfaceStateDisplay 當前元素非常可能要變成可見的,需要進行異步繪制;
  • ASInterfaceStateVisible 當前元素最少在屏幕上顯示了 1px

當用戶滾動當前視圖時,ASRangeController 就會修改不同區(qū)域內(nèi)元素的狀態(tài):

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

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

在滾動方向(Leading)上 Fetch Data 區(qū)域會是非滾動方向(Trailing)的兩倍,ASDK 會根據(jù)滾動方向的變化實時改變緩沖區(qū)的位置;在向下滾動時,下面的 Fetch Data 區(qū)域就是上面的兩倍,向上滾動時,上面的 Fetch Data 區(qū)域就是下面的兩倍。

這里的兩倍并不是一個確定的數(shù)值,ASDK 會根據(jù)當前設備的不同狀態(tài),改變不同區(qū)域的大小,但是滾動方向的區(qū)域總會比非滾動方向大一些

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

原理

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

  • ASRangeController
  • ASDataController
  • ASTableViewASTableNode

對智能預加載實現(xiàn)的分析,也是根據(jù)這三個部分來介紹的。

工作區(qū)域的管理

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

ASTableView 為例,在視圖進行滾動時,會觸發(fā) -[UIScrollView scrollViewDidScroll:] 代理方法:

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

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

ASRangeController 最重要的私有方法 -[ASRangeController _updateVisibleNodeIndexPaths] 一般都是因為上面的方法間接調(diào)用的:

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

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

我們將這個私有方法的實現(xiàn)分開來看:

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

當前 ASRangeController 的數(shù)據(jù)源以及代理就是 ASTableView,這段代碼首先就獲取了完成計算和布局的 ASCellNode 以及可見的 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 的計算和 preloadIndexPaths 非常類似
  
  [allIndexPaths unionSet:displayIndexPaths];
  [allIndexPaths unionSet:preloadIndexPaths];
  ...
}

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

aslayout-range-mode-display-preload

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

- (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í)行過程非常簡單,根據(jù) ASRangeTuningParameters 獲取該滾動方向上的緩沖區(qū)大小,在區(qū)域內(nèi)遍歷所有的 ASCellNode 查看其是否在當前區(qū)域內(nèi),然后加入數(shù)組中。

到這里,所有工作區(qū)域 visibleIndexPaths displayIndexPaths 以及 preloadIndexPaths 都已經(jīng)獲取到了;接下來,就到了遍歷 NSIndexPath,修改結(jié)點狀態(tài)的過程了;

- (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ù)當前 ASTableView 的狀態(tài)以及 NSIndexPath 所在的區(qū)域,打開 ASInterfaceState 對應的位。

    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();
            }
          }
        }
      }
    }
  }
  ...
}

后面的一部分代碼就會遞歸的設置結(jié)點的 interfaceState,并且在當前 ASRangeControllerASLayoutRangeMode 發(fā)生改變時,發(fā)出通知,調(diào)用 -[ASRangeController _updateVisibleNodeIndexPaths] 私有方法,更新結(jié)點的狀態(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 既然是對 ASTableView 的封裝,那么表視圖中顯示的數(shù)據(jù)仍然需要數(shù)據(jù)源來提供,而在 ASDK 中這一機制就比較復雜:

astableview-data

整個過程是由四部分協(xié)作完成的,ControllerASTableNodeASTableView 以及 ASDataController,網(wǎng)絡請求發(fā)起并返回數(shù)據(jù)之后,會調(diào)用 ASTableNode 的 API 執(zhí)行插入行的方法,最后再通過 ASTableView 的同名方法,執(zhí)行管理和更新節(jié)點數(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ù)源中獲取對應的 ASCellNodeBlock
  2. 獲取每一個 NSIndexPath 對應的單元的大小 constrainedSize(在圖中沒有表現(xiàn)出來);
  3. 初始化一堆 ASIndexedNodeContext 實例,然后加入到控制器維護的 _nodeContexts 數(shù)組中;
  4. 將節(jié)點插入到 _completedNodes 中,用于之后的緩存,以及提供給 ASTableView 的數(shù)據(jù)源代理方法使用;

ASTableView 會將數(shù)據(jù)源協(xié)議的代理設置為自己,而最常見的數(shù)據(jù)源協(xié)議在 ASTableView 中的實現(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;
}

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

cellforrowatindexpath

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

ASTableView 向數(shù)據(jù)源請求數(shù)據(jù)時,ASDK 就會從對應的 ASDataController 中取回最新的 node,添加在 _ASTableViewCell 的實例上顯示出來。

ASTableView 和 ASTableNode

ASTableViewASTableNode 的關(guān)系,其實就相當于 CALayerUIView 的關(guān)系一樣,后者都是前者的一個包裝:

astableview-astablenode

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

- (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)很多方法都是由 ASDataControllerASRangeController 驅(qū)動的,上面的兩個方法的實現(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];
}

到這里,整個智能預加載的部分就結(jié)束了,從需要預加載的資源以及預加載發(fā)出的時間兩個方面來考慮,ASDK 在不同工作區(qū)域中合理標記了需要預加載的資源,并在節(jié)點狀態(tài)改變時就發(fā)出請求,在用戶體驗上是非常優(yōu)秀的。

總結(jié)

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

multi-layer-asdk

智能預加載的使用相比其它實現(xiàn)可能相對復雜,但是在筆者看來,ASDK 對于這一套機制的實現(xiàn)還是非常完善的,同時也提供了極其優(yōu)秀的用戶體驗,不過同時帶來的也是相對較高的學習成本。

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

pros-cons

這兩種方式的選擇,其實也就是實現(xiàn)復雜度和用戶體驗之間的權(quán)衡了。

Github Repo:iOS-Source-Code-Analyze

Follow: Draveness · GitHub

Source: http://draveness.me/preload

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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

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