【企业网站建设】预加载与智能预加载(iOS) - 企业巅云建站官方网站
author:一佰互联 2019-05-25   click:790


前两次的分享分别介绍了 ASDK 对于渲染的优化以及 ASDK 中使用的另一种布局模型;这两个新机制的引入分别解决了 iOS 在主线程渲染视图以及 Auto Layout 的性能问题,而这一次讨论的主要内容是 ASDK 如何预先请求服务器数据,达到看似无限滚动列表的效果的。

这篇文章是 ASDK 系列中的最后一篇,文章会介绍 iOS 中几种 预加载 的方案,以及 ASDK 中是如何处理预加载的。

不过,在介绍 ASDK 中实现 智能预加载 的方式之前,文章中会介绍几种简单的预加载方式,方便各位开发者进行对比,选择合适的机制实现预加载这一功能。

网络与性能

ASDK 通过在渲染视图和布局方面的优化已经可以使应用在任何用户的疯狂操作下都能保持 60 FPS 的流畅程度,也就是说,我们已经充分的利用了当前设备的性能,调动各种资源加快视图的渲染。

但是,仅仅在 CPU 以及 GPU 方面的优化往往是远远不够的。在目前的软件开发中,很难找到一个 没有任何网络请求 的应用,哪怕是一个记账软件也需要服务器来同步保存用户的信息,防止资料的丢失;所以,只在渲染这一层面进行优化还不能让用户的体验达到最佳,因为网络请求往往是一个应用 最为耗时以及昂贵 的操作。

每一个应用程序在运行时都可以看做是 CPU 在底层利用各种资源疯狂做加减法运算,其中最耗时的操作并不是进行加减法的过程,而是 资源转移 的过程。

举一个不是很恰当的例子,主厨(CPU)在炒一道菜(计算)时往往需要的时间并不多,但是菜的采购以及准备(资源的转移)会占用大量的时间,如果在每次炒菜之前,都由帮厨提前准备好所有的食材(缓存),那么做一道菜的时间就大大减少了。

而提高资源转移的效率的最佳办法就是使用多级缓存:


从上到下,虽然容量越来越大,直到 Network 层包含了整个互联网的内容,但是访问时间也是直线上升;在 Core 或者三级缓存中的资源可能访问只需要几个或者几十个时钟周期,但是网络中的资源就 远远 大于这个数字,几分钟、几小时都是有可能的。

更糟糕的是,因为天朝的网络情况及其复杂,运营商劫持 DNS、404 无法访问等问题导致网络问题极其严重;而如何加速网络请求成为了很多移动端以及 Web 应用的重要问题。

预加载

本文就会提供一种 缓解网络请求缓慢导致用户体验较差 的解决方案,也就是预加载;在本地真正需要渲染界面之前就通过网络请求获取资源存入内存或磁盘。

预加载并不能彻底解决网络请求缓慢的问题,而是通过提前发起网络请求 缓解 这一问题。

那么,预加载到底要关注哪些方面的问题呢?总结下来,有以下两个关注点:

  • 需要预加载的资源
  • 预加载发出的时间

文章会根据上面的两个关注点,分别分析四种预加载方式的实现原理以及优缺点:

  1. 无限滚动列表
  2. threshold
  3. 惰性加载
  4. 智能预加载

无限滚动列表

其实,无限滚动列表并不能算是一种预加载的实现原理,它只是提供一种分页显示的方法,在每次滚动到 UITableView 底部时,才会开始发起网络请求向服务器获取对应的资源。

虽然这种方法并不是预加载方式的一种,放在这里的主要作用是作为对比方案,看看如果不使用预加载的机制,用户体验是什么样的。


很多客户端都使用了分页的加载方式,并没有添加额外的预加载的机制来提升用户体验,虽然这种方式并不是不能接受,不过每次滑动到视图底部之后,总要等待网络请求的完成确实对视图的流畅性有一定影响。

虽然仅仅使用无限滚动列表而不提供预加载机制会在一定程度上影响用户体验,不过,这种 需要用户等待几秒钟 的方式,在某些时候确实非常好用,比如:投放广告。

QQ 空间就是这么做的,它们 投放的广告基本都是在整个列表的最底端 ,这样,当你滚动到列表最下面的时候,就能看到你急需的租房、租车、同城交友、信用卡办理、只有 iPhone 能玩的游戏以及各种奇奇怪怪的辣鸡广告了,很好的解决了我们的日常生活中的各种需求。

Threshold

使用 Threshold 进行预加载是一种最为常见的预加载方式,知乎客户端就使用了这种方式预加载条目,而其原理也非常简单,根据当前 UITableView 的所在位置,除以目前整个 UITableView.contentView 的高度,来判断当前是否需要发起网络请求:

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

上面的代码在当前页面已经划过了 70% 的时候,就请求新的资源,加载数据;但是,仅仅使用这种方法会有另一个问题,尤其是当列表变得很长时,十分明显,比如说:用户从上向下滑动,总共加载了 5 页数据:

| 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 当前总页数;
  • Total 当前 UITableView 总元素个数;
  • Threshold 网络请求触发时间;
  • Diff 表示最新加载的页面被浏览了多少;

当 Threshold 设置为 70% 的时候,其实并不是单页 70%,这就会导致 新加载的页面都没有看,应用就会发出另一次请求,获取新的资源

动态的 Threshold

解决这个问题的办法,还是比较简单的,通过修改上面的代码,将 Threshold 变成一个动态的值,随着页数的增长而增长:

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 就会随着页数的增长而动态的改变,解决了上面出现的问题:


惰性加载

使用 Threshold 进行预加载其实已经适用于大多数应用场景了;但是,下面介绍的方式, 惰性加载 能够有针对性的加载用户“会看到的” Cell。

惰性加载 ,就是在用户滚动的时候会对用户滚动结束的区域进行计算,只加载目标区域中的资源。

用户在飞速滚动中会看到巨多的空白条目,因为用户并不想阅读这些条目,所以,我们并不需要真正去加载这些内容,只需要在 ASTableView/ASCollectionView 中只根据用户滚动的目标区域惰性加载资源。


惰性加载的方式不仅仅减少了网络请求的冗余资源,同时也减少了渲染视图、数据绑定的耗时。

计算用户滚动的目标区域可以直接使用下面的代理方法获取:

let markedView = UIView() let rowHeight: CGFloat = 44.0 override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { 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)")
}

以上代码只会大致计算出目标区域内的 IndexPath 数组,并不会展开新的 page,同时会使用浅黑色标记目标区域。

当然,惰性加载的实现也并不只是这么简单,不仅需要客户端的工作,同时因为需要 加载特定 offset 资源 ,也需要服务端提供相应 API 的支持。

虽然惰性加载的方式能够按照用户的需要请求对应的资源,但是,在用户滑动 UITableView 的过程中会看到大量的空白条目,这样的用户体验是否可以接受又是值得考虑的问题了。

智能预加载

终于到了智能预加载的部分了,当我第一次得知 ASDK 可以通过滚动的方向预加载不同数量的内容,感觉是非常神奇的。


如上图所示 ASDK 把正在滚动的 ASTableView/ASCollectionView 划分为三种状态:

  • Fetch Data
  • Display
  • Visible

上面的这三种状态都是由 ASDK 来管理的,而每一个 ASCellNode 的状态都是由 ASRangeController 控制,所有的状态都对应一个 ASInterfaceState

  • ASInterfaceStatePreload 当前元素貌似要显示到屏幕上,需要从磁盘或者网络请求数据;
  • ASInterfaceStateDisplay 当前元素非常可能要变成可见的,需要进行异步绘制;
  • ASInterfaceStateVisible 当前元素最少在屏幕上显示了 1px

当用户滚动当前视图时, ASRangeController 就会修改不同区域内元素的状态:


上图是用户在向下滑动时, ASCellNode 是如何被标记的,假设 当前视图可见的范围高度为 1 ,那么在默认情况下,五个区域会按照上图的形式进行划分:

| 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 区域会是非滚动方向(Trailing)的两倍,ASDK 会根据滚动方向的变化实时改变缓冲区的位置;在向下滚动时,下面的 Fetch Data 区域就是上面的两倍,向上滚动时,上面的 Fetch Data 区域就是下面的两倍。

这里的两倍并不是一个确定的数值,ASDK 会根据当前设备的不同状态,改变不同区域的大小,但是 滚动方向的区域总会比非滚动方向大一些

智能预加载能够根据当前的滚动方向,自动改变当前的工作区域,选择合适的区域提前触发请求资源、渲染视图以及异步布局等操作,让视图的滚动达到真正的流畅。

原理

在 ASDK 中整个智能预加载的概念是由三个部分来统一协调管理的:

  • ASRangeController
  • ASDataController
  • ASTableView ASTableNode

对智能预加载实现的分析,也是根据这三个部分来介绍的。

工作区域的管理

ASRangeController ASTableView 以及 ASCollectionView 内部使用的控制器,主要用于监控视图的可见区域、维护工作区域、触发网络请求以及绘制、单元格的异步布局。

ASTableView 为例,在视图进行滚动时,会触发 -[UIScrollView scrollViewDidScroll:] 代理方法:

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

每一个 ASTableView 的实例都持有一个 ASRangeController 以及 ASDataController 用于管理工作区域以及数据更新。

ASRangeController 最重要的私有方法 -[ASRangeController _updateVisibleNodeIndexPaths] 一般都是因为上面的方法间接调用的:

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

调用栈中间的过程其实并不重要,最后的私有方法的主要工作就是计算不同区域内 Cell 的 NSIndexPath 数组,然后更新对应 Cell 的状态 ASInterfaceState 触发对应的操作。

我们将这个私有方法的实现分开来看:

- (void)_updateVisibleNodeIndexPaths { NSArray<NSArray *> *allNodes = [_dataSource completedNodes]; PREV: 
 
                     
              域名全国最低价68元,优惠延期,抢!