在平时的iOS面试中,我们经常会考察有关离屏渲染(Offscreen rendering)的知识点。一般来说,绝大多数人都能答出“圆角、mask、阴影会触发离屏渲染”,但是也仅止于此。如果再问得深入哪怕一点点,比如:

  • 离屏渲染是在哪一步进行的?为什么?
  • 设置cornerRadius一定会触发离屏渲染吗?

90%的候选人都没法非常确定地说出答案。作为一个客户端工程师,把控渲染性能是最关键、最独到的技术要点之一,如果仅仅了解表面知识,到了实际应用时往往会失之毫厘谬以千里,无法得到预期的效果。

iOS渲染架构

在WWDC的Advanced Graphics and Animations for iOS Apps(WWDC14 419,关于UIKit和Core Animation基础的session在早年的WWDC中 …

如果你是一个有经验的工程师,在初次看到一个全新的概念时一定会想:这个东西会不会又是一个强行造出来的轮子?使用起来是不是门槛比较高,适不适合推广?

关于RxSwift/RxJava/RxJS的具体使用介绍网上非常多,这篇文章并不是要再一次介绍如何使用,也不打算讨论函数式、副作用等话题,而是想写一些关于Reactive这个话题一些更本质的思考。

Reactive Programming这个词理解起来很容易让人一知半解,感觉和声明式、函数式有一些关系,而且也感觉和Rx系列有什么关系。那到底它的定义是什么呢?看看维基百科上的解释(如果有更精确的表达欢迎指出):

In computing, reactive programming is a declarative programming paradigm concerned with data streams and the propagation of change.

从定义中我们可以发现,有几个关键点,我们一个一个来看。

声明式(declarative)

什么是声明式?这又是一个比较抽象的概念,似乎大家都知道一部分,但是也没法全部讲清。而它的“对立者”,命令式,似乎更好描述一些。说的通俗一些,命令式编程就是你用一个语句,显式地去改变变量的状态,如a=b+c。任何一个写过代码的人,都是从这些简单的赋值语句开始学习的,早已习以为常。

声明式则不是这样,维基百科的定义如下:

a style of building the structure and elements of computer programs — that expresses the logic of a computation without describing its control flow.

也就是说,写代码的时候会把注意力集中在“逻辑”上,而不是具体如何实现,是一个更高抽象层次的做法。这样一来,下层具体实现就被分离开,使用者可以只关心自己的那部分代码。比如HTML是声明式的,你只需要关心你的.html代码,而每个元素具体如何解析,各个浏览器都是按照自己的理解来独立实现的。

数据流(data streams)

我们时常会遇到这些情况:

  • 从网络上下载数据,数据是一片一片传输的
  • 用户触摸屏幕,touchesMoved事件是随着用户的手指移动,一次一次触发的
  • 定时器触发事件,每个事件是按照一定的间隔,一个一个被抛出的

面对“一个系列的多个数据”这种情况,虽然每个语言都会有类似“迭代器”、“序列”的数据结构或概念,但是一方面这些数据需要调用者主动去拉取,另一方面对于异步的支持往往不够强大,需要语言本身的特性来补充(如async/await,coroutine)

传播变化(propagation of change)

通常一个系统中会有很多职责分工不同的模块,而在一个事件触发后,各个模块可能会依次响应和处理数据,并存储最后的结果。而这就是为什么会带来著名的callback hell的原因:似乎各个模块缺少一种优雅而统一的接口语法,而不得不通过丑陋的callback来把它们挨个连接起来。

除了语法上的问题,还有一些更重要的问题需要解决:

  • 数据在不同的时间点被抛出,如何让各个模块串行/并行处理?
  • 如何用通用的方法来“监听”一个信号源?
  • 如何表示数据异常,或者数据已经全部被抛出?
  • 如何用通用的方法来建立传播通道?如果通道中有分叉,或者信号之间有相互依赖,怎样优雅地表示?

当然我们自己是可以把代码写得漂亮一些,但终究没有从框架层面,甚至更高的概念认知层面,建立一种让所有人都能遵守的约定。

为什么要用Reactive Programming

讲到这里,我们已经发现,以上这些问题是如此的常见,以至于无论你用什么思想概念,什么编程范式,都需要去面对解决。有时我们会用Promise/Future/回调/协程来解决异步的问题,有时我们会用Iterator来解决数据流的问题,但是不足以解决以上的所有问题。

工程师们有一句经典的名言:没有增加一层抽象解决不了的问题,如果有就再增加一层。

试想一下,如果有一种通用的数据结构,甚至是一种通用的思想,把以上的问题都规范化,封装成通用的形式,这样就能省下相当一部分精力,让我们可以集中更多注意在上层业务逻辑上,同时代码的可读性、可维护性都更强,岂不是一个不错的选择?同时解决这些问题的方案的名字,就叫做Reactive Programming。

那么为什么这个方案被称作是Reactive(响应式)?我个人的理解是:因为我们日常写的绝大多数代码,其实都是在“响应”某种事件或数据变化。而为了达到这个目的,你需要恰到好处地解决以上问题。

我们来看一些事件传播例子:

  • 用户触发点击/滑动->UI控件状态变化->业务逻辑响应->发出网络请求->回调处理数据变化->UI更新
  • 系统网络断开->app监听到事件->UI展示网络错误
  • 服务端发送推送信息->网络层收到数据->业务逻辑响应->UI更新

我们会发现,这些逻辑的链路,都是始于一个外部的“信号源”,经过层层接力传播到app逻辑内,最后再反映到UI/数据层。这样的例子还有很多很多。

我们当然可以用回调,或者其他命令式的语法来达到同样的目的,但是因为缺少了一层抽象,导致我们在编写代码时总是不得不一次又一次去面对同样的场景(如信号依赖、管道建立、错误处理等等),违反了DRY原则。

这么一来,大家可能会跃跃欲试,写一个自己的轮子来解决这些问题。这也就是ReactiveX诞生的原因。

关于ReactiveX

ReactiveX最初被微软应用在.NET上,而后慢慢的在衍生出了各种不同语言的实现,诸如RxSwift/RxJava/RxJS,它们的使用和优缺点我想应该不用赘述,网上有各种各样的tutorial可以参考。但是这些教程似乎很少讲到一些更本质的原因,或者说它们更聚焦在异步语法的便利上——如果只是异步语法便利或者操作符多,Promise和Future一样能解决相当一部分问题,为什么还需要更复杂的Rx?我认为应该更多地从认识上,从概念上做一些改变。

Rx带来的一些变化

即刻在15年底引入Rx。一开始大家都赞叹Rx带来语法上的革命,享受链式调用的方便和优雅,但随着理解的深入,我们渐渐地发现Rx也带来了其他几点更深层次的变化:

  • 我们思考问题的时候更多的像上面提到的那样,把事件当做一个数据流(可能流里有一个或多个事件),把事件的来源看成是一个可观测信号源(Observable),而后面的每一环都是监听者(Observer)。它们静静地等待事件从上游一步一步流到自己这边,在自己内部逻辑处理完后,像接力一样传递给下一个人。
  • 编写代码时我们会更少地定义上帝模块(如“XXManager”),而是帮各个模块分别定义好自己内部的逻辑,最后在业务层统一组装。
  • 集中组装的代码一般只有寥寥数行,言简意赅,而且非常便于维护和调整,就好像把不同功能的水管方便地连接起来,有时也可以把它们拆开来互换位置,而不用担心他们的接口不兼容,协议会帮你把关。
  • 各个模块都会定义自己内部的“监听”逻辑,而这样一来他们彼此之间就减少了依赖关系,变得更加functional。虽然定义observer方法与定义callback有一些类似,但是也有一些微妙的差别 — — 每个模块现在都有了一个正式身份,叫做“监听者”,来“响应”外部的变化。

总结

  • Reactive programming是声明式的,和函数式概念不同,更多的是专注于处理(异步)数据流的变化
  • 不管你是否采用Reactive的思想,或者是否使用Rx库,都需要面对一些关于异步、数据流、传播的问题。可能Rx以及它定义的Observable-Observer-Scheduler模型并不是唯一解,但是仍然能给我们非常多的思考。
  • Rx带来的好处不仅仅是语法上的便利(尽管几十个功能各异的操作符真的帮我们减少了非常多的负担!),更是潜移默化改变思考问题的角度,从主动的命令式,变成了被动的响应式,而这其实更加符合程序的本质,因为app的每一次状态改变,本质上都是响应事件的结果
  • 增加一层抽象或许会增加使用者的学习成本和门槛,也会有一些诸如调用链过长、增加性能开销的问题。但是事实证明,在即刻三年多RxSwift/RxJava的实践中,获得的收益远大于其成本。如果只能保留一个第三方库,我们一定会选择Rx。

推荐阅读

iPhone 作为一个移动设备,其计算和内存资源通常是非常有限的,而许多用户对应用的性能却很敏感,卡顿、应用回到前台丢失状态、甚至 OOM 闪退,这就给了 iOS 工程师一个很大的挑战。

网上的绝大多数关于 iOS 内存管理的文章,大多是围绕 ARC/MRC、循环引用的原理或者是如何找寻内存泄漏来展开的,而这些内容更准确的说应该是 ObjC 或者 Swift 的内存管理,是语言层面带来的特性,而不是操作系统本身的内存管理。

如果我们需要聊聊”管理“内存,那么就需要先了解一些基础知识。

内存基础概念复习

物理内存

一个设备的 RAM 大小。以下是维基百科上的资料:

简单来说,iPhone 8(不包括 plus) 和 iPhone 7(不包括 plus)及之前都是 2G 内存,iPhone 6 和 6 plus 及之前都是 …

对于一个用户每天打开多次的app来说,用户会重复经历这样一个过程:

  1. 找到app图标并点击
  2. 系统启动动画(从icon放大到全屏)
  3. Launch Screen 停留一段时间
  4. (有些app会展示自己的广告页,看3~5秒广告)
  5. 进入主界面,开始加载动画
  6. 网络加载完成,开始渲染界面
  7. 加载返回数据中的图片

以上是用户看到的加载过程,一定程度上决定了用户对app是否responsive的印象。

从技术上来说,app可以掌控并且优化的,可以按顺序分为四个主要部分:

  1. 应用本身被系统加载到内存
  2. 应用执行启动代码
  3. 第一屏内容的网络请求
  4. 布局计算和文字图形渲染

在即刻,我们通过持续的分析、监控和优化,将整个流程所需要的时间压缩到1秒左右,使用户几乎感觉不到app重启的过程。接下来我们会一一讨论,如何把每个环节都压缩到极限。

应用被系统加载

虽然app代码是“被加载”的,开发者能控制的部分很有限,但是我们可以把代码组织成“最适合被系统加载”的形式,以降低系统的负担,达到加快加载速度的目的。

这部分所涉及到的所有原理、方法、测量工具,大多数都在WWDC16–406和WWDC17–413中有比较详细的解释,这里不再重复。网上也有一些中文翻译可以参考。苹果本身也一直在优化Swift和dyld,并在iOS 11中加入了dyld 3,对每次模块加载过程进行cache,大大加快了加载速度。

简而言之,除去系统已经帮我们做的优化,我们可以做的部分主要是:

  • 减少动态库的数量。动态库的解耦特点和分模块的文件组织形式,对,但是在启动时变成了一个负担,因为dyld需要分别从文件系统中加载每一个framework,执行一系列的初始化操作,对于磁盘IO和CPU都是不小的开销。即刻在去年9月份Swift支持静态库之后第一时间进行了研究,具体内容可以参考我们的知乎专栏文章:即刻Swift静态库实践,事实证明这是app被系统加载过程中,我们能做的最大的一块优化。
打开DYLD_PRINT_STATISTICS以后可以看到加载各阶段所花费的时间
  • 类初始化。最著名的全局初始化可能就是Objc的+load(即刻是一个纯Swift项目,所以不存在这方面的问题)。同时适当减少类的数量和全局静态成员也会有一定的帮助。
  • 不要自己使用dlopen方法,让系统来处理模块加载

应用执行启动代码

我们注意到绝大多数的启动逻辑,诸如初始化app各类基础服务,加载第三方库,都是在AppDelegate.didFinishLaunchWithOptions方法中执行的,而这个方法是在主线程上,会阻塞用户操作。可以注意的点有:

  • 除了用户看到的第一屏内容所依赖的初始化方法(UI和基础服务),尽量以异步,甚至是后台线程的方式来做初始化。
  • 即刻的各模块是以Service的方式组织。除了启动必须调用的服务,其他服务的初始化通常都会等到真正需要调用的时候才被执行。
  • Swift的全局变量和静态变量都是以lazy方式加载,不需要担心不必要的工作量,因此尽量使用Swift。
  • 对于第三方库,虽然每个sdk都建议在didFinishLaunchWithOptions中做初始化,但其实大多都没有这么紧急,稍微延后一些也是完全可以接受的,这样就大大减轻了启动代码的工作量。

对于这一块的性能调试,也在上面提到的两个WWDC session中有详细解释。

第一屏内容的网络请求

通常来说,一个app启动时需要做若干网络请求,包括数据上报、获取配置、拉取首屏内容、加载图片等等,而绝大多数app都是采用HTTP请求的方式来执行的(有些超级大厂如微信会开发自己的协议,这里不做讨论),那么HTTP的性能就不得不关注。

先来回顾一下整个连接的建立过程:

  1. 首先是dns查询,需要一个RTT(Round Trip Time)。
  2. 其次是https连接过程:

似乎听起来这是一件很小的事,为什么要写一篇文章呢?一开始我们也觉得简单,但是最后这件事仍然是超出了我们的预期范围,因此值得稍微整理一下解决的整个过程。

清Badge?没那么容易

对于一个app,如果在home screen上的badge一直消不掉,是一个比较烦人的事。因此一般来说,app需要在未读消息都处理完的时候,通过代码把badge清零(通过本地设置badge或者远程推送badge)。

不过这样一来会有一个副作用:一旦app的badge被设置为0,系统通知中心里先前的推送都会被清空,这是一个系统隐藏的逻辑。我们的问题就是:如何在badge清零的同时,保留通知中心的推送?

一般思路

通常来说对于这类业务逻辑并不复杂,同时跟系统API紧密相关的需求,我们会尝试通过以下几个步骤来解决:

  1. 查看文档(我们发现applicationBadge和远程推送都可以设置badge,但是无法解决通知中心被清空的问题)
  2. 上Google和Stackoverflow搜索(遇到这个问题的不多,也许这是一个比较窄的case,大多数app并不关心)
  3. 尝试一些文档上没有提到的办法(这里就需要警觉了,因为RoI正在快速降低,即使猜出一个可用的办法,以后很可能还会变)记得前几年也遇到了,当时有人说把badge设置为-1可以解决,尝试了一下却不行。现在又试了一下采用Local notification的方式,仍然不行。

一般尝试到了这里,再投入更多的时间钻牛角尖往往是不明智的,省下时间来做其他的事会更有效率。我们会跟pm说no,尝试在需求上做出妥协。在与pm争论的同时,我们发现了另一个app做到了。(如果没有这件事,很可能也就没有后来的结论)

没办法,那我们干脆多花点时间,把问题一次性搞清楚,顺带给大家贡献一些成果,作为低RoI的弥补吧。

已知条件

  1. iOS9直接设置applicationBadgeCount为-1没有作用
  2. 通过push设置badge为-1没有作用
  3. 本地通知也可以设置badge
  4. 无论远程还是本地,设置为badge为0一定会清空通知中心
  5. 从iOS10开始,因为新出了UserNotification框架,本地推送的api发生了变化,老的接口deprecated(但可能仍然能用)。因此我们要同时测试iOS9、10、11三个大版本
  6. app对于推送的响应在前后台有可能不同
  7. 需要同时测试使用老的本地通知API、设置badge为-1、新的本地通知API

测试结果

“完美”表示效果与预期相符合,我们很高兴

最令人意外的是,我们之所以觉得设置badge为-1没有作用,是因为stackoverflow上说的和我们去年的测试结果确实没用——没想到到了iOS11又可以用了。那样来说,其实只有中间的iOS9和10是不行的,让人有一种“修好的bug又坏了”的感觉。这成为了iOS11上唯一能帮助我们达到目标的办法。

思考过程比结果更有价值

对于这类充满暗坑的需求,从产品的角度是很难看到的。一旦遇到了,一般工程师存在三种态度:

  1. 勤勤恳恳的工程师全部接受产品的需求,但是可能一个很小的问题需要花上整整两天时间
  2. 强硬的工程师遇到就选择拒绝,那可能会跟产品吵一架,并且需要花时间另辟蹊径
  3. 选择先评估再做取舍。如果pm一再坚持己见(这里指的并不是全部问题都持固执态度的pm,而是理性的pm),那就从一方面说明RoI中的R可能是足够的,可以适当再追加投入。争论也是评估过程的一部分。

因此,除了想把这个测试结果分享给大家,还希望分享整个思考过程(相比结果而言是更重要的):对于工程师来说,应该不断评估产品需求和开发投入之间的平衡,并把信息传递到pm达成一致。不要怕跟pm争论,适度的争论并不是完全没有意义的,相反却可以达到效率的最大化。

即刻作为一个创业公司,不管是团队还是产品都在快速变化。在提升每个人自身能力的同时,团队协作也慢慢成为一个重要的部分,因此我们不仅引入了Scrum,也在其基础上根据自身情况不停的调整。

先看下没有Scrum时的问题

  • 产品发布周期不稳定,迭代缺乏节奏感。
  • 开发缺乏明确任务时间表,时间安排全凭自己。对于工程方面的工作比较有利,想做的可以马上做,但是对于团队协作开发的需求效率较低
  • 产品需求不够明确(只有idea,缺乏具体细节)时就开始动工。不可否认这样的好处是小功能可以快速试错,但是随着项目发展,当多team协同开发大功能时,会极大地增加沟通成本。比如由于文档不够清晰,客户端A去跟产品经理B确认一个细节,完成了以后还需要通知其他产品经理C、客户端D、后端E、测试F等等,有时候还会推翻之前的结论。这样一来虽然看起来大家都在热火朝天地讨论,但是效率其实不高。
  • 产品和开发对于工期的预期不一致,时常会互相pending,客户端等后端接口,后端等具体需求等等。
  • 经常会因为事先未考虑到的突发情况(产品活动、技术上的障碍)而delay。
  • 在开发中途遇到新的需求或者idea,经常会犹豫是加到当前开发中的版本,赶一下工稍微推迟发布时间,还是顺延到下一版本?如果在赶的过程中还有另外的调整怎么办?

一些假设

  • 就像团队开发需要制定代码规范、模块调用需要调用规范一样,团队协作也需要敏捷开发流程规范,确保团队间互相预期一致,减少低效沟通和互相推锅。当出现问题,定责(不是为了blame某个人,而是找到原因)和改进就相对容易。
  • 在高速变化的创业公司,流程本身应该随着公司和产品情况不断迭代的(元迭代?),如果某个环节大家都不满意,应该立即优化并在下一个sprint中执行。
  • 对于team leader来说,提高流程和团队的效率比提升自己的效率更重要,尤其是对于项目环节越来越多的时候,需要尽量避免个人英雄主义。(一个大工程,光靠某一个好模块是不够的。只有当每个模块间的数据流通畅时,工程本身才能运作良好)
  • “改需求”是无法完全消灭的(在创业公司可能也不应该),但是可以通过合理的流程限制在一个可以接受的水平(好比bug无法完全消灭,但是code review流程可以让工程师自我监督,有效减少bug)。过于灵活的调整会降低开发效率,反过来死板的流程也会伤害产品。需要不停地在两者间做出平衡。
  • 产品经理永远是希望加入更多的功能的,而且一定能给出一些合理的理由来支撑,但是并不一定考虑实际的工作量。Scrum master谨慎评估,在必要时果断拒绝一些短期看似合理,但其实伤害长期效率的需求,防止Scope Creep发生从而破坏团队间的信任。
  • 工程师大多倾向于在自己的领域里单打独斗,天生反感开会等低效而耗时的沟通流程。如果能够减少被打断的次数,可以大幅提高工作效率(就像减少线程/上下文切换可以提高代码执行效率)
  • 功能开发不应该占用工程师全部的时间,敏捷流程应与工程师文化相辅相成。产品迭代、流程迭代和技术迭代、工具迭代一样重要。
  • 工程师应该参与到产品设计中去,从技术和自身角度出发提供建议,提升ownership。尽量在功能正式开发开始之前充分讨论,因此这对产品需求计划的前瞻性也提出了很高的要求。
  • 对于一个功能,如果花费1个小时时间可以做到70分,但是75分需要2个小时,那么优先选择1个小时的做法,在下个迭代中再看情况优化。
  • 一个重要的大前提是,公司能够招到足够优秀的人,且维持足够好的工程师文化。这一点也是即刻一直重点努力的方向之一,在这方面不管是产品、测试、开发至少都能做到“搁置争议,共同开发”。

执行

  • 确定一个稳定的Scrum周期,如两周。当产品的需求周期也是两周时,那么可以做到每两周发一个新版本。
  • Sprint planning前,产品需求应该到一个相当清晰的水平,否则每个team估算的时间成本可能与实际相比有大幅偏差,影响本次sprint计划的制定。
  • 在Sprint planning时,产品开发一起进行任务拆解,估算时间后,决定哪些功能放入本sprint,哪些延后。注意planning meeting不是需求讨论或者brainstorming,不应无休止的争论每个需求的细节。整个会议尽量控制在1小时以内。
  • 当planning完成时,每个小团队内部分配task。当分配完成时,整个sprint来自外部的任务就确定了,接下来工程师有权自主分配开发时间。大家只需要在sprint结束那天达到task完成,并开始beta测试的预期目标。
  • 大家共同的预期是,在Scrum结束时,必须要有一个准release版本进行beta测试。
  • 每天Daily standup meeting与大家分享目前进度,并提出可能遇到的问题(如被谁block,或者工期delay等)。一般每个人只需要十几秒时间就能说完,因此整个meeting最多不超过10分钟。
  • 如果遇到改需求,需要team leader一起评估:类似改字体或者图标,那可以随时改。如果大家认为是较大功能改动(如需要花费1天以上)会影响已有的工作安排,顺延到下个sprint。这对于产品team是一个负反馈,能够促使下一次的需求计划更加合理。
  • 当遇到重大突发情况(如突如其来的流量增长、critical bug fix等)同样需要大家一起沟通,可以延后一些相对不重要的task,但尽量不影响sprint的节奏。

快速迭代,快速反馈,快速改进,不管是产品、开发还是Scrum本身。Facebook(曾经)说Move fast and break things. 对于一个年轻的公司,年轻的团队来说,需要冲劲,更需要专注和执行。

数据序列化是每个app开发者必须面对的工作。

在OC年代,为了实现数据序列化(记得Core Data和NSKeyedArchiver吗?),我们必须为一个数据结构实现NSCoding协议,写一大堆重复啰嗦的代码,或者借助一些第三方库(如Mantle/MagicalRecord)实现。后来由于JSON的流行,iOS逐步引入了JSONSerialization和JSONEncoder类来帮助我们操作。

到了Swift年代,第三方库SwiftyJSON和ObjectMapper都曾经作为JSON转换的中流砥柱,只是这两者还是免不了“手动指定字段和JSON字典映射关系”的工作。于是阿里想了个黑科技(HandyJSON),通过分析Swift数据结构在内存中的布局,自动分析出映射关系,进一步降低开发者使用的成本。

HandyJSON中的部分代码,可以看出对Swift运行时有深入的研究

自从Swift4中开始从语言和系统(Foundation)层面支持Codable(Encodable&Decodable)协议,叫好的声音不断,颇有取代ObjectMapper之势;对于HandyJson来说,也有了更强的官方版本——如果原生已经支持了,何必用第三方库呢?

Codable的第一印象

即刻和绝大多数app一样每天都在和JSON数据格式打交道,尤其是网络通信时,JSON早已成为数据序列化的首选方案。WWDC17的session也不意外的用JSON作为例子对Codable的具体使用进行说明。

一切看起来非常美好:当一个数据结构的所有字段都实现了Codable(基础数据类型系统已经帮我们做好),那只需要将它本身也声明为Codable,剩下的一切都由编译器帮你完成,而不需要像ObjectMapper一样实现mapping方法来为每一个字段指定对应key。

说起Monad,这个词并不直白,再看它的中文译名“单子”,同样让人云里雾里。其运用的场景,常常跟函数式编程密切相关,这对于iOS开发来说,又是一个距离日常工作比较远的概念。在一开始学习时我们也碰到了一些障碍,再看了几篇文章,听了几次分享,也没有完全理解。

如果从数学或者函数式编程的意义上来解释,会显得非常枯燥——如果我们不知道它对于日常使用有什么帮助,而它本身又比较难懂,又为什么要去了解呢?

我们不妨试试从应用的角度切入,看看Monad究竟能做些什么。希望这篇文章能带给你一些启发。

对于iOS开发来说,OC的年代并没有那么明显的函数式的概念。有一次看到Stackoverflow上有人问OC的链式调用应该怎么写,有人吐槽答:

如果没办法把多个函数优雅地拼接在一起,那写函数式的痛苦可想而知。幸运的是Swift带来了很大的改变。

一个实际的例子

即刻从一开始就是一个纯Swift项目,并且在15年下半年用上了RxSwift,迄今已有两年多的时间,积累了一定的实践经验。

举例来说,发送一个网络请求,在之前不使用Rx时可能这样写:

WWDC过去了第一天,看到更新乏力的iOS和各硬件产品线,让人感到有些失望,毕竟更新CPU和GPU都是上游芯片厂商的功劳,并没有看到太多苹果创新的努力。

然而在一众更新的API里,Core ML还是让人眼前一亮。在不久前的atSwift会议上已经有人做出了利用MetalPerformanceShader(其中iOS10新增的MPSNeuron部分)构建CNN来实现实时图片上色,在iPhone7上能达到20+的fps。尽管机器性能被推到极限,大量耗电发热,我们却看到机器学习在移动设备上的使用场景——并不是像我们想的那样必须在云端进行。

在Core ML(实际上封装了底层的Accelerate/MPS等大规模并行计算操作)出现之后,使移动设备上使用机器学习变得更加容易了。在iOS10时可能苹果还没完全准备好,把用来做图形渲染的MPS(拥有强大的并行计算性能)的api直接拿来做神经网络,到了iOS11正式封装成一个完整的framework。

制约ML发展的一大因素是计算资源的不足,所以一直以来ML似乎都是只跟运算资源可以水平扩展的云端有关,而在性能较弱的移动设备上,使用场景非常少,即使需要也可以通过请求云端服务来完成。缺点之一就是受网络制约而实时性较差。

然而我们也看到,在有些场景对实时性要求颇高(或者弱网络)下的ML应用,比如:

  1. Google translate实时AR翻译(离线)
  2. 图像流处理(如最近流行的各种基于机器学习的滤镜
  3. 户外自动驾驶(大疆在无人机上也使用了一部分,用来避开障碍物)

从这些实践中可以看出,即使training的过程比较耗时,对于简单的基于确定模型的推断(Inference),在一些场景下移动端是有必要也完全有能力完成的。training数据可以通过云端下发或者另外途径导入。为此苹果也创建了一套Python工具,将常见第三方(Caffe/Keras等等)模型数据转成CoreML Model来使用。苹果还提供了一个使用ML做价格预测的sample

对于一般开发者而言,在Core ML之上封装了更加通用的计算机视觉(Vision)和NLP的一些类,使调用者可以轻松的实现一些常用的图像识别(人脸识别、OCR、物体识别、二维码等等)/语义识别的操作,这些同样也是目前ML中最接近日常应用的部分。

目前这些API都只能在iOS11的设备上使用,因此第三方库仍然会是首要选择。相信随着移动设备计算能力提高和各种官方/第三方ML库的逐渐成熟,应该陆续会有不少意想不到的使用场景出现,非常值得关注。

Mobile+AI First!

说到视图性能,不能不提到UITableView,对于它的滚动性能的讨论和优化从未停止。在我们的探索过程中,尝试过以下一些措施:

  • Cell reuse,Apple原生支持
  • estimated cell height,iOS8开始原生支持
  • 手动将计算完成的height缓存(或使用FDTemplateLayoutCell等框架自动计算)
  • prefetch API,iOS10开始原生支持
  • 异步加载cell内容,文字图片等

还有一些诸如圆角、opaque等普通UIView性能瓶颈已经在第一篇中讨论了一些,这里不再赘述。

然而我们会想,cell布局是否必须要在主线程,图片和文字渲染是否必须要在主线程,cell预加载是否可以更完善更智能?仍然有许多问题等待被解决。

UITableView加载Cell的过程

我们先看一下一般UITableView加载Cell的过程:

  1. cellForRowAtIndexPath,读取model
  2. 从reuse池中dequeue一个cell,调用prepareForReuse重置其状态
  3. 将model装配到UITableViewCell当中去
  4. 布局(耗时且无法缓存的Autolayout),渲染(文字和图片都很耗时),显示

这些操作都在cell将要进入window的一刹那发生,不难理解,在短短的16ms里(60fps)是很难完成这些任务的,尤其是当用户快速滚动的时候,大量任务堆积在主线程runloop,情况变得雪上加霜。如果将滚动中的CPU占用情况用图表显示出来,大概是这样的(WWDC16 session 219):

这其中每当一个cell将要进入屏幕,一个尖峰就会产生。而在其他时候,cpu的负载相当的低。

自然我们会想到,如果把任务平均分配到一个时间窗口,而不是集中在某一个点,是否就可以避免这样的情况发生?如果我们能够预测一个cell很快将要进入屏幕,而此时cpu空闲,是否可以未雨绸缪,提前做一些布局和渲染的工作?那样一来,在cell真正需要显示的时候,由于布局和渲染结果backing store已经是现成的了,只需要将它送给真正负责显示的view就可以,也就可以避免产生剧烈的性能波动。

ASTableNode/ASCollectionNode开辟的新航路

首先的好消息是,作为ASDK的一员,ASTableNode以及其cellNode已经具备了异步布局和异步渲染的能力,即时没有额外优化,在进入屏幕以后进行渲染,将耗时操作延后,相对于一般UITableView已经有了显著的提升。虽然性能锯齿仍然存在,但是将其转移到了后台线程以后,用户感受到的卡顿就不会那么明显了。

然而这些似乎还不够,在显示之后渲染,会有短暂的白屏现象。既然Async表示view的渲染工作可以在显示之后再进行,那么类似的,也可以在显示之前之前的一段时间,把布局和渲染的工作预先完成

要达到这些目的,首先介绍一些相关的类:

  • ASTableNode/ASCollectionNode,可以认为是UITableView/UICollectionView的异步版本,内部包装了原来的UIKit的对应版本,并扩展了一系列功能使他们能够实现异步布局及渲染。
  • ASInterfaceState,表示一个node不同的显示状态。其实每个ASDisplayNode都具备interfaceState属性,它主要的用武之地还是在tableNode/collectionNode之中。对于一个UITableViewCell来说,布局和渲染一般都是在cellForRowAtIndexpath同时完成,然而当需要精细处理任务时就需要把每一个不同的状态分开,降低某一瞬间由于CPU负载高导致卡顿的可能性。ASInterfaceState递进地分为5种状态:
  • None,该node在一段时间内不会进入屏幕
  • MeasureLayout,可能会在一段时间后进入屏幕,进行layout和size计算
  • Preload,加载所需要的数据,如下载图片,缓存读取等等
  • Display,马上将要进入屏幕,进行渲染操作,显示包含的文字或者图片
  • Visible,该node至少有1个像素已经在屏幕内,正在显示

对于每一个cell而言,原本需要在同一时间点进行的初始化/加载/布局/渲染等工作,现在被分配到了不同的状态进行处理。随着用户滚动列表,根据cell离屏幕的距离,设置相应的interfaceState并触发不同阶段的工作,达到均匀分配的目的。同时,由于不需要在主线程上进行,多个cell的工作可以通过共享后台线程来大幅提高并行效率。

  • ASDataController,与ASTableNode一一对应,负责代替ASTableNode管理delegate和dataSource的一系列方法,诸如初始化,插入,删除和一些代理方法等。
  • ASRangeController,同样与ASTableNode一一对应,并且可以根据设备性能自定义布局、加载、渲染的工作indexPath区间,在滚动时动态高效地调整各cell的interfaceState来层层触发不同显示阶段的工作,对于流畅滚动起到了至关重要的作用。

Jason Yu

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store