mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-17 06:33:48 +08:00
del
This commit is contained in:
185
极客时间专栏/geek/iOS开发高手课/应用开发篇/21 | 除了 Cocoa,iOS还可以用哪些 GUI 框架开发?.md
Normal file
185
极客时间专栏/geek/iOS开发高手课/应用开发篇/21 | 除了 Cocoa,iOS还可以用哪些 GUI 框架开发?.md
Normal file
@@ -0,0 +1,185 @@
|
||||
<audio id="audio" title="21 | 除了 Cocoa,iOS还可以用哪些 GUI 框架开发?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/aa/f5/aa45d9cf7c88ccefcd1af0f0306bd9f5.mp3"></audio>
|
||||
|
||||
你好,我是戴铭。
|
||||
|
||||
在专栏的第2篇文章“[App 启动速度怎么做优化与监控?](https://time.geekbang.org/column/article/85331)”中,我和你分享了如何实现方法级别的启动优化,从而实现整个 App 启动速度的优化。
|
||||
|
||||
通过这篇文章的内容你会发现,把可以优化方法的工作都处理完之后,比如主线程上的大量计算、IO 操作、懒加载(也叫作延时加载,即当对象需要用到的时候再去加载),就只剩下了GUI(Graphical User Interface 图形用户界面) 相关的方法。
|
||||
|
||||
在iOS开发时,默认使用的都是系统自带的 Cocoa Touch 框架,所以如果你还想进一步提高界面响应速度,赶超其他使用 Cocoa Touch框架的 App 用户体验时,就要考虑使用其他的 GUI 框架来优化 App 界面的响应速度了。
|
||||
|
||||
接下来,我们就一起聊聊除了 Cocoa Touch 框架外,还有哪些 GUI 框架可以用来做 iOS 开发。
|
||||
|
||||
## 目前流行的GUI框架
|
||||
|
||||
现在流行的 GUI 框架除了 Cocoa Touch 外,还有 WebKit、Flutter、Texture(原名 AsyncDisplayKit)、Blink、Android GUI 等。其中,WebKit、Flutter、Texture 可以用于 iOS 开发。接下来,我就和你说说这三款GUI框架。
|
||||
|
||||
WebKit 框架包含了 WebCore 和 JavaScriptCore,使用 HTML 和 CSS 进行布局,使用JavaScript 编写程序。WebKit 还提供了 Objective-C 应用程序编程接口,方便基于 Cocoa API 的应用使用。在iOS开发中,我们最常使用的UIWebView和WKWebView控件都是基于WebKit框架。
|
||||
|
||||
关于 WebKit框架,我以前写过一篇博客“[深入剖析 WebKit](https://ming1016.github.io/2017/10/11/deeply-analyse-webkit/)”,详细分析了它的原理。感兴趣的话,你可以去看一下。
|
||||
|
||||
Flutter 是 Google公司于2017年推出的一个移动应用开发的 GUI 框架,使用 Dart 语言编写程序,一套代码可以同时运行在iOS和Android平台。对Flutter 的相关介绍,我会在专栏后面的文章“React Native、Flutter 等跨端方案,应该怎么选?”和“iOS 原生、大前端和 Flutter 分别是怎么渲染的?”里,和你详细说明。
|
||||
|
||||
Texture框架的基本单元,是基于 UIView 抽象的节点 ASDisplayNode。和 UIView 不同的是 ,ASDisplayNode 是线程安全的,可以在后台线程上并行实例化和配置整个层级结构。Texture框架的开发语言,使用的是苹果公司自家的 Objective-C 和 Swift。
|
||||
|
||||
WebKit、Flutter、Texture这三个 GUI 框架,与Cocoa Touch的对比,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7b/04/7b4ce475bb30b7add7648d54d6b18604.png" alt="">
|
||||
|
||||
通过这个对比,我们可以发现,Texture框架和Cocoa Touch框架,在使用的编程语言、渲染这两个方面,是完全一样的。其实,**Texture框架,正是建立在Cocoa Touch框架之上的。**
|
||||
|
||||
我们再从这些框架使用的布局来看一下,**Texture 和其他 GUI 框架一样都是使用的应用更加广泛的FlexBox布局**。使用FlexBox 布局的好处是,可以让iOS开发者用到前端先进的W3C标准响应式布局。目前, FlexBox 已经是布局的趋势,连 iOS 新推出的 UIStackView 布局方式,也是按照 FlexBox 布局思路来设计的。
|
||||
|
||||
另外,**Texture 是这些框架中唯一使用异步节点计算的框架**。使用异步节点计算,可以提高主线程的响应速度。所以,Texture在节点计算上的效率要比其他框架高。
|
||||
|
||||
基于以上三个方面的原因,如果要从Cocoa Touch框架前移到其他的GUI框架,从学习成本、收益等角度考虑的话,转到Texture会是个不错的选择。
|
||||
|
||||
因此,我会和你重点分析一下Texture框架。因为现在的GUI技术已经非常成熟了,各种GUI框架的底层也大同小异,所以接下来我会先和你介绍GUI框架中的通用性内容,然后再与你讲述Texture的独特之处。
|
||||
|
||||
## GUI 框架里都有什么?
|
||||
|
||||
GUI 框架的基本单元是控件,你熟悉的按钮、图片、文本框等等就是控件。
|
||||
|
||||
控件主要负责界面元素数据的存储和更新,这些原始数据都存储在控件的属性上,直接更新控件的属性就能够完成界面元素更新操作,控件的属性设置成不同的值会让界面元素呈现不同的外观。
|
||||
|
||||
控件之间的关系是由渲染树(Render Tree)这种抽象的树结构来记录的。渲染树关注的是界面的布局,控件在界面中的位置和大小都是由渲染树来确定。
|
||||
|
||||
基于渲染树,GUI 框架还会创建一个渲染层树(RenderLayer Tree),渲染层树由渲染层对象组成,根据 GUI 框架的优化条件来确定创建哪些渲染层对象,每次新创建一个渲染层对象就会去设置它的父对象和兄弟对象。渲染层对象创建完毕,接下来就需要将各渲染层对象里的控件按照渲染树布局生成 Bitmap,最后 GPU 就可以渲染 Bitmap 来让你看到界面了。
|
||||
|
||||
控件、渲染树、渲染层树之间的关系,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f4/74/f424e846b4de4f338350a00fbe749074.png" alt="">
|
||||
|
||||
WebKit 和 Flutter 都是开源项目,我们可以通过它们的代码看到 GUI 框架具体是怎么实现控件、渲染树、渲染层树和生成 Bitmap 的。
|
||||
|
||||
WebKit 在 GUI 框架层面的效率并不低,单就渲染来说,它的性能一点也不弱于 Cocoa Touch 和 Flutter 框架。
|
||||
|
||||
- 使用WebKit的网页显示慢,主要是由于 CSS(Cascading Style Sheet) 和 JavaScript 资源加载方式导致的。
|
||||
- 同时,解析时 HTML、CSS、JavaScript 需要兼容老版本,JavaScript 类型推断失败会重来,列表缺少重用机制等原因,导致WebKit框架的整体性能没有其他框架好。
|
||||
|
||||
开始的时候,Flutter 也是基于 Chrome 浏览器引擎的。后来,谷歌公司考虑到Flutter的性能,所以去掉了HTML、CSS、JavaScript 的支持,而改用自家的Dart 语言以甩掉历史包袱。关于这方面的细节,你可以查看[Flutter 创始人 Eric 的采访视频](https://zhuanlan.zhihu.com/p/52666477)来了解 。
|
||||
|
||||
这些年来,虽然 GUI 框架百家争鸣,但其渲染技术却一直很稳定。接下来,我就和你详细说说 GUI 框架中的渲染过程。
|
||||
|
||||
## 渲染流程
|
||||
|
||||
GUI 框架中的渲染,一般都会经过布局、渲染、合成这三个阶段。
|
||||
|
||||
**布局阶段要完成的主要工作,是依据渲染树计算出控件的大小和位置。**WebKit 用 CSS 来布局,CSS 会提供 Frame 布局和 FlexBox 布局;Flutter 也支持 Frame 布局和 FlexBox 布局;Cocoa Touch 框架本身不支持 FlexBox 布局,但是通过 Facebook 的 [Yoga 库](https://yogalayout.com/)也能够使用 FlexBox 布局。
|
||||
|
||||
由于 Frame 布局需要精确描述每个界面元素的具体位置和大小,无论从代码编写,还是从代码可读性上看,成本都会高很多。所以说,FlexBox 对于 iOS 开发来说是另外一种很好的选择。
|
||||
|
||||
**渲染阶段的主要工作,是利用图形函数计算出界面的内容。**一般情况下,对于 2D 平面的渲染都是使用CPU 计算,对3D空间的渲染会使用 GPU 计算。
|
||||
|
||||
Cocoa Touch 和 Texture 框架使用的是 Core Animation,3D 使用的是 Metal 引擎。Flutter 使用的是 Skia,3D 使用的是 OpenGL(ES)。
|
||||
|
||||
在渲染这方面,我觉得 WebKit 做得更出色,考虑到多平台支持,WebKit 将渲染接口抽象了出来,实现层根据平台进行区分,比如在 iOS 上就用 CoreGraphics 来渲染,在 Android 就用 Skia 渲染。
|
||||
|
||||
**合成阶段的主要工作,是合并图层。**这样做的目的,主要是节省显示内存,只显示一屏需要的像素,也只进行这些像素数据的计算。这个过程完成后,再将所得数据进行光栅化处理,最后交给 GPU 渲染成你可以看到的 Bitmap。
|
||||
|
||||
关于 WebKit、Cocoa Touch、Flutter框架渲染相关的内容,我会在后面“iOS 原生、大前端和 Flutter 分别是怎么渲染的?”的文章里和你详细说明。
|
||||
|
||||
通过上面的内容,我们可以看到,主流 GUI 框架的内容和渲染流程,区别并不是很大。
|
||||
|
||||
但 Texture 对于那些希望能够在用户交互体验上进行大幅提升的 iOS 开发者来说,很小的切换成本,同时性能能大幅提升的收益,其实是很有诱惑力的。
|
||||
|
||||
通过对GUI框架都包括什么和渲染流程的分析,再次印证了我们的观点:Texture是个值得推荐的框架,不仅在于它与Cocoa Touch框架的相似使得学习成本较低,还得益于它本身在性能上的先进性。
|
||||
|
||||
那么,接下来我就再跟你说说 Texture 最大的优势是什么?以及它是怎么做到的?
|
||||
|
||||
## Texture 里 Node 的异步绘制
|
||||
|
||||
Texture 最大的优势就是开发了线程安全的 ASDisplayNode,而且还能够很好的和 UIView 共生。这样的话,我们就可以在原有使用 UIView 开发的程序基础之上使用 Texture,而不用完全重构所有界面。
|
||||
|
||||
随着 CPU 多核技术的发展,界面渲染计算都在主线程完成,对于多核 CPU 来说确实是有些浪费。ASDisplayNode 是 UIView 和 CALayer 的抽象,能实现不在主线程执行视图的布局绘制和层级计算,充分发挥多核 CPU 的优势。
|
||||
|
||||
首先,我们来看看 Texture 最核心的线程安全节点 **ASDisplayNode 是做什么的?**
|
||||
|
||||
在Cocoa Touch 框架里,当 CALayer 内容更新时会去找 CALayer 的 delegate,也就是 displayLayer: 方法。UIView 会实现 displayLayer: 这个 delegate 方法。UIView 里实现 drawRect: 这个 delegate 方法能够自定义 CALayer。
|
||||
|
||||
在 Texture 中,ASDisplayNode 替代了这个delegate,解耦了 UIView 和 CALayer,并将 UIView 包装成 ASDisplayView,将 CALayer 包装成 ASDisplayLayer 供外部使用。
|
||||
|
||||
然后,我们再来看看**ASDisplayNode 是如何进行异步绘制的?**
|
||||
|
||||
ASDisplayLayer 是整个绘制的起点,绘制事件先在 displayBlock 设置好,然后 ASDisplayNode 调用 displayBlock 来进行异步绘制。整个过程分为三步。
|
||||
|
||||
**第一步**,得到 displayBlock。这个 Block 里有需要绘制的内容,对应的代码如下:
|
||||
|
||||
```
|
||||
asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:asynchronously isCancelledBlock:isCancelledBlock rasterizing:NO];
|
||||
|
||||
```
|
||||
|
||||
其中,displayBlock 就是需要绘制的内容。
|
||||
|
||||
**第二步**,定义一个回调 completionBlock ,绘制完成后执行。代码如下:
|
||||
|
||||
```
|
||||
asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id<NSObject> value, BOOL canceled){
|
||||
ASDisplayNodeCAssertMainThread();
|
||||
if (!canceled && !isCancelledBlock()) {
|
||||
// displayBlock 执行的是绘图操作,返回的类型是 UIImage
|
||||
UIImage *image = (UIImage *)value;
|
||||
BOOL stretchable = (NO == UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero));
|
||||
if (stretchable) {
|
||||
ASDisplayNodeSetResizableContents(layer, image);
|
||||
} else {
|
||||
layer.contentsScale = self.contentsScale;
|
||||
// 设置为 CALayer 的寄宿图
|
||||
layer.contents = (id)image.CGImage;
|
||||
}
|
||||
[self didDisplayAsyncLayer:self.asyncLayer];
|
||||
|
||||
if (rasterizesSubtree) {
|
||||
ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) {
|
||||
[node didDisplayAsyncLayer:node.asyncLayer];
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
上面代码中,completionBlock 里就是绘制完成后需要去执行的事情,内容是完成 CALayer 寄宿图的设置。 value 是 displayBlock 返回的值,类型是 UIImage。displayBlock 用的是线程安全的 Core Graphics,所以你可以安心地把 displayBlock 放到后台线程去异步执行。
|
||||
|
||||
**第三步**,如果设置为异步展示,就先向上找到属性 asyncdisplaykit_parentTransactionContainer 为 YES 的 CALayer,获取 containerLayer 的 ASAsyncTransaction,然后将 displayBlock 添加到 ASAsyncTransaction 的调度队列里,根据 drawingPriority 优先级执行displayBlock。具体代码如下:
|
||||
|
||||
```
|
||||
if (asynchronously) {
|
||||
// 向上找到属性 asyncdisplaykit_parentTransactionContainer 为 YES 的 CALayer
|
||||
CALayer *containerLayer = layer.asyncdisplaykit_parentTransactionContainer ? : layer;
|
||||
|
||||
// 获取 containerLayer 的 ASAsyncTransaction
|
||||
_ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction;
|
||||
|
||||
// 将 displayBlock 添加到 ASAsyncTransaction 的调度队列里
|
||||
[transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock];
|
||||
} else {
|
||||
// 设置为不是异步就直接调用 displayBlock 进行绘制
|
||||
UIImage *contents = (UIImage *)displayBlock();
|
||||
completionBlock(contents, NO);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
可以看到,displayBlock 被添加到 ASAsyncTransaction 里进行调度。这里,ASAsyncTransactionQueue 是 Texture 的调度队列。
|
||||
|
||||
通过上面这三步,Texture就完成了 ASDisplayNode 的异步绘制。
|
||||
|
||||
## 小结
|
||||
|
||||
在今天这篇文章中,我和你介绍了目前可以用于iOS开发的主流GUI 框架有哪些,这些框架里都有什么,以及它们的渲染流程是怎样的。
|
||||
|
||||
通过今天的介绍,你会发现在选择GUI 框架时,渲染方面的区别并不大,而且渲染技术相对比较成熟。所以,我们需要在框架的易用性,以及与现有工程的兼容上做更多的考虑。
|
||||
|
||||
如果你想提高 App 的使用体验,让界面操作更流畅的话,我推荐你使用Texture。Texture 易用性和兼容性都很好,同时 Texture 的学习成本与收益比也是最高的。而且,Texture 代码本身的质量很高,有很多值得学习的地方。
|
||||
|
||||
## 课后作业
|
||||
|
||||
今天我跟你介绍了 ASDisplayNode 异步绘制的三个步骤,你能说清楚ASAsyncTransaction 是如何调度 displayBlock的吗?请你把答案留言留言给我吧(提示:你可以去翻看一下Texture 的源码)。
|
||||
|
||||
当然了,我还为你准备了一个动手题,来帮助你巩固今天所学的内容。请你使用 Texture 来写一个列表,完成后上传到 GitHub 上吧。同时,记得将GitHub的地址贴到评论区,我们一起学习,共同进步。
|
||||
|
||||
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
231
极客时间专栏/geek/iOS开发高手课/应用开发篇/22 | 细说 iOS 响应式框架变迁,哪些思想可以为我所用?.md
Normal file
231
极客时间专栏/geek/iOS开发高手课/应用开发篇/22 | 细说 iOS 响应式框架变迁,哪些思想可以为我所用?.md
Normal file
@@ -0,0 +1,231 @@
|
||||
<audio id="audio" title="22 | 细说 iOS 响应式框架变迁,哪些思想可以为我所用?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/01/58/01bca264cf4bc987fc734fdf06227a58.mp3"></audio>
|
||||
|
||||
你好,我是戴铭。
|
||||
|
||||
说到iOS 响应式框架,最开始被大家知道的是 ReactiveCocoa(简称RAC),后来比较流行的是 RxSwift。但据我了解,iOS原生开发使用 ReactiveCocoa框架的团队并不多,而前端在推出React.js 后,响应式思路遍地开花。
|
||||
|
||||
那么,**响应式框架到底是什么,为什么在iOS原生开发中没被广泛采用,却能在前端领域得到推广呢?**
|
||||
|
||||
我们先来看看响应式框架,它指的是能够支持响应式编程范式的框架。使用了响应式框架,你在编程时就可以使用数据流传播数据的变化,响应这个数据流的计算模型会自动计算出新的值,将新的值通过数据流传给下一个响应的计算模型,如此反复下去,直到没有响应者为止。
|
||||
|
||||
React.js框架的底层有个 Virtual DOM(虚拟文档对象模型),页面组件状态会和 Virtual DOM 绑定,用来和 DOM(文档对象模型)做映射与转换。当组件状态更新时,Virtual DOM 就会进行 Diff 计算,最终只将需要渲染的节点进行实际 DOM 的渲染。
|
||||
|
||||
JavaScript 每次操作 DOM 都会全部重新渲染,而Virtual DOM 相当于 JavaScript 和 DOM 之间的一个缓存,JavaScript 每次都是操作这个缓存,对其进行 Diff 和变更,最后才将整体变化对应到 DOM 进行最后的渲染,从而减少没必要的渲染。
|
||||
|
||||
React.js 的 Virtual DOM 映射和转换 DOM 的原理,如下图所示。我们一起通过原理,来分析一下它的性能提升。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/67/a2/672e07e4347b132701c37d21ac7a44a2.png" alt=""><br>
|
||||
可以看出,操作 Virtual DOM 时并不会直接进行 DOM 渲染,而是在完成了 Diff 计算得到所有实际变化的节点后才会进行一次 DOM 操作,然后整体渲染。而 DOM 只要有操作就会进行整体渲染。
|
||||
|
||||
直接在 DOM 上进行操作是非常昂贵的,所以视图组件会和 Virtual DOM 绑定,状态的改变直接更改 Virtual DOM。Virtual DOM 会检查两个状态之间的差异,进行最小的修改,所以 React.js 具有很好的性能。也正是因为性能良好,React.js才能够在前端圈流行起来。
|
||||
|
||||
而反观iOS,ReactiveCocoa框架的思路,其实与React.js中页面组件状态和 Virtual DOM 绑定、同步更新的思路是一致的。那**为什么 ReactiveCocoa 在iOS原生开发中就没流行起来呢?**
|
||||
|
||||
我觉得,主要原因是前端DOM 树的结构非常复杂,进行一次完整的 DOM 树变更,会带来严重的性能问题,而有了 Virtual DOM 之后,不直接操作 DOM 可以避免对整个 DOM 树进行变更,使得我们不用再担忧应用的性能问题。
|
||||
|
||||
但是,这种性能问题并不存在于iOS 原生开发。这,主要是得易于 Cocoa Touch 框架的界面节点树结构要比 DOM 树简单得多,没有前端那样的历史包袱。
|
||||
|
||||
与前端 DOM 渲染机制不同,Cocoa Touch 每次更新视图时不会立刻进行整个视图节点树的重新渲染,而是会通过 setNeedsLayout 方法先标记该视图需要重新布局,直到绘图循环到这个视图节点时才开始调用 layoutSubviews 方法进行重新布局,最后再渲染。
|
||||
|
||||
所以说,ReactiveCocoa框架并没有为 iOS 的 App 带来更好的性能。当一个框架可有可无,而且没有明显收益时,一般团队是没有理由去使用的。那么,像 ReactiveCocoa 这种响应式思想的框架在 iOS 里就没有可取之处了吗?
|
||||
|
||||
我觉得并不是。今天,我就来跟你分享下,**ReactiveCocoa 里有哪些思想可以为我所用,帮我们提高开发效率?**
|
||||
|
||||
ReactiveCocoa 是将函数式编程和响应式编程结合起来的库,通过函数式编程思想建立了数据流的通道,数据流动时会经过各种函数的处理最终到达和数据绑定的界面,由此实现了数据变化响应界面变化的效果。
|
||||
|
||||
## Monad
|
||||
|
||||
ReactiveCocoa 是采用号称纯函数式编程语言里的 Monad 设计模式搭建起来的,核心类是 RACStream。我们使用最多的 RACSignal(信号类,建立数据流通道的基本单元) ,就是继承自RACStream。RACStream 的定义如下:
|
||||
|
||||
```
|
||||
typedef RACStream * (^RACStreamBindBlock)(id value, BOOL *stop);
|
||||
|
||||
/// An abstract class representing any stream of values.
|
||||
///
|
||||
/// This class represents a monad, upon which many stream-based operations can
|
||||
/// be built.
|
||||
///
|
||||
/// When subclassing RACStream, only the methods in the main @interface body need
|
||||
/// to be overridden.
|
||||
@interface RACStream : NSObject
|
||||
|
||||
+ (instancetype)empty;
|
||||
+ (instancetype)return:(id)value;
|
||||
- (instancetype)bind:(RACStreamBindBlock (^)(void))block;
|
||||
- (instancetype)concat:(RACStream *)stream;
|
||||
- (instancetype)zipWith:(RACStream *)stream;
|
||||
|
||||
@end
|
||||
|
||||
```
|
||||
|
||||
通过定义的注释可以看出,RACStream的作者也很明确地写出了RACStream 类表示的是一个 Monad,所以我们在 RACStream 上可以构建许多基于数据流的操作;RACStreamBindBlock,就是用来处理 RACStream 接收到数据的函数。那么,**Monad 就一定是好的设计模式吗?**
|
||||
|
||||
**从代码视觉上看**,Monad 为了避免赋值语句做了很多数据传递的管道工作。这样的话,我们在分析问题时,就很容易从代码层面清晰地看出数据流向和变化。而如果是赋值语句,在分析数据时就需要考虑数据状态和生命周期,会增加调试定位的成本,强依赖调试工具去观察变量。
|
||||
|
||||
**从语言发展来看**,Monad 虽然可以让上层接口看起来很简洁,但底层的实现却犹如一团乱麻。为了达到“纯”函数效果,Monad底层将各种函数的参数和返回值封装在了类型里,将本来可以通过简单数据赋值给变量记录的方式复杂化了。
|
||||
|
||||
不过无论是赋值方式还是 Monad 方式,编译后生成的代码都是一样的。王垠在他的博文“[函数式语言的宗教](http://www.yinwang.org/blog-cn/2013/03/31/purely-functional)”里详细分析了 Monad,并且写了两段分别采用赋值和函数式的代码,编译后的机器码实际上是一样的。如果你感兴趣的话,可以看一下这篇文章。
|
||||
|
||||
所以,如果你不想引入 ReactiveCocoa 库,还想使用函数响应式编程思想来开发程序的话,完全不用去重新实现一个采用 Monad 模式的 RACStream,只要在上层按照函数式编程的思想来搭建数据流管道,在下层使用赋值方式来管理数据就可以了。并且,采用这种方式,可能会比 Monad 这种“纯”函数来得更加容易。
|
||||
|
||||
## 函数响应式编程例子
|
||||
|
||||
接下来,我通过一个具体的案例来和你说明下,如何搭建一个不采用 Monad 模式的函数响应式编程框架。
|
||||
|
||||
这个案例要完成的功能是:添加学生基本信息,添加完学生信息后,通过按钮点击累加学生分数,每次点击按钮分数加5;所得分数在30分内,颜色显示为灰色;分数在30到70分之间,颜色显示为紫色;分数在70分内,状态文本显示不合格;超过70分,分数颜色显示为红色,状态文本显示合格。初始态分数为0,状态文本显示未设置。
|
||||
|
||||
这个功能虽然不难完成,但是如果我们将这些逻辑都写在一起,那必然是条件里套条件,当要修改功能时,还需要从头到尾再捋一遍。
|
||||
|
||||
如果把逻辑拆分成小逻辑放到不同的方法里,当要修改功能时,查找起来也会跳来跳去,加上为了描述方法内逻辑,函数名和参数名也需要非常清晰。这,无疑加重了开发和维护成本,特别是函数里面的逻辑被修改了后,我们还要对应着修改方法名。否则,错误的方法名,将会误导后来的维护者。
|
||||
|
||||
那么,**使用函数响应式编程方式会不会好一些呢?**
|
||||
|
||||
这里,我给出了使用函数响应式编程方式的代码,你可以对比看看是不是比条件里套条件和方法里套方法的写法要好。
|
||||
|
||||
**首先,**创建一个学生的记录,在创建记录的链式调用里添加一个处理状态文本显示的逻辑。代码如下:
|
||||
|
||||
```
|
||||
// 添加学生基本信息
|
||||
self.student = [[[[[SMStudent create]
|
||||
name:@"ming"]
|
||||
gender:SMStudentGenderMale]
|
||||
studentNumber:345]
|
||||
filterIsASatisfyCredit:^BOOL(NSUInteger credit){
|
||||
if (credit >= 70) {
|
||||
// 分数大于等于 70 显示合格
|
||||
self.isSatisfyLabel.text = @"合格";
|
||||
self.isSatisfyLabel.textColor = [UIColor redColor];
|
||||
return YES;
|
||||
} else {
|
||||
// 分数小于 70 不合格
|
||||
self.isSatisfyLabel.text = @"不合格";
|
||||
return NO;
|
||||
}
|
||||
}];
|
||||
|
||||
```
|
||||
|
||||
可以看出,当分数小于70时,状态文本会显示为“不合格”,大于等于70时会显示为“合格”。
|
||||
|
||||
**接下来,**针对分数,我再创建一个信号,当分数有变化时,信号会将分数传递给这个分数信号的两个订阅者。代码如下:
|
||||
|
||||
```
|
||||
// 第一个订阅的credit处理
|
||||
[self.student.creditSubject subscribeNext:^(NSUInteger credit) {
|
||||
NSLog(@"第一个订阅的credit处理积分%lu",credit);
|
||||
self.currentCreditLabel.text = [NSString stringWithFormat:@"%lu",credit];
|
||||
if (credit < 30) {
|
||||
self.currentCreditLabel.textColor = [UIColor lightGrayColor];
|
||||
} else if(credit < 70) {
|
||||
self.currentCreditLabel.textColor = [UIColor purpleColor];
|
||||
} else {
|
||||
self.currentCreditLabel.textColor = [UIColor redColor];
|
||||
}
|
||||
}];
|
||||
|
||||
// 第二个订阅的credit处理
|
||||
[self.student.creditSubject subscribeNext:^(NSUInteger credit) {
|
||||
NSLog(@"第二个订阅的credit处理积分%lu",credit);
|
||||
if (!(credit > 0)) {
|
||||
self.currentCreditLabel.text = @"0";
|
||||
self.isSatisfyLabel.text = @"未设置";
|
||||
}
|
||||
}];
|
||||
|
||||
```
|
||||
|
||||
可以看出,这两个分数信号的订阅者分别处理了两个功能逻辑:
|
||||
|
||||
- 第一个处理的是分数颜色;
|
||||
- 第二个处理的是初始状态下状态文本的显示逻辑。
|
||||
|
||||
整体看起来,所有的逻辑都围绕着分数这个数据的更新自动流动起来,也能够很灵活地通过信号订阅的方式进行归类处理。
|
||||
|
||||
采用这种编程方式,上层实现方式看起来类似于 ReactiveCocoa,而底层实现却非常简单,将信号订阅者直接使用赋值的方式赋值给一个集合进行维护,而没有使用 Monad 方式。底层对信号和订阅者的实现代码如下所示:
|
||||
|
||||
```
|
||||
@interface SMCreditSubject : NSObject
|
||||
|
||||
typedef void(^SubscribeNextActionBlock)(NSUInteger credit);
|
||||
|
||||
+ (SMCreditSubject *)create;
|
||||
|
||||
// 发送信号
|
||||
- (SMCreditSubject *)sendNext:(NSUInteger)credit;
|
||||
// 接收信号
|
||||
- (SMCreditSubject *)subscribeNext:(SubscribeNextActionBlock)block;
|
||||
|
||||
@end
|
||||
|
||||
@interface SMCreditSubject()
|
||||
|
||||
@property (nonatomic, assign) NSUInteger credit; // 积分
|
||||
@property (nonatomic, strong) SubscribeNextActionBlock subscribeNextBlock; // 订阅信号事件
|
||||
@property (nonatomic, strong) NSMutableArray *blockArray; // 订阅信号事件队列
|
||||
|
||||
@end
|
||||
|
||||
@implementation SMCreditSubject
|
||||
|
||||
// 创建信号
|
||||
+ (SMCreditSubject *)create {
|
||||
SMCreditSubject *subject = [[self alloc] init];
|
||||
return subject;
|
||||
}
|
||||
|
||||
// 发送信号
|
||||
- (SMCreditSubject *)sendNext:(NSUInteger)credit {
|
||||
self.credit = credit;
|
||||
if (self.blockArray.count > 0) {
|
||||
for (SubscribeNextActionBlock block in self.blockArray) {
|
||||
block(self.credit);
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
// 订阅信号
|
||||
- (SMCreditSubject *)subscribeNext:(SubscribeNextActionBlock)block {
|
||||
if (block) {
|
||||
block(self.credit);
|
||||
}
|
||||
[self.blockArray addObject:block];
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Getter
|
||||
- (NSMutableArray *)blockArray {
|
||||
if (!_blockArray) {
|
||||
_blockArray = [NSMutableArray array];
|
||||
}
|
||||
return _blockArray;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
如上面代码所示,订阅者都会记录到 blockArray 里,block 的类型是 SubscribeNextActionBlock。
|
||||
|
||||
最终,我们使用函数式编程的思想,简单、高效地实现了这个功能。这个例子完整代码,你可以点击[这个链接](https://github.com/ming1016/RACStudy)查看。
|
||||
|
||||
## 小结
|
||||
|
||||
今天这篇文章,我和你分享了ReactiveCocoa 这种响应式编程框架难以在 iOS 原生开发中流行开的原因。
|
||||
|
||||
从本质上看,响应式编程没能提高App的性能,是其没能流行起来的主要原因。
|
||||
|
||||
在调试上,由于 ReactiveCocoa框架采用了 Monad 模式,导致其底层实现过于复杂,从而在方法调用堆栈里很难去定位到问题。这,也是ReactiveCocoa没能流行起来的一个原因。
|
||||
|
||||
但, ReactiveCocoa的上层接口设计思想,可以用来提高代码维护的效率,还是可以引入到 iOS 开发中的。
|
||||
|
||||
ReactiveCocoa里面还有很多值得我们学习的地方,比如说宏的运用。对此感兴趣的话,你可以看看sunnyxx的那篇[《Reactive Cocoa Tutorial [1] = 神奇的Macros》。](http://blog.sunnyxx.com/2014/03/06/rac_1_macros/)
|
||||
|
||||
对于 iOS 开发来说,响应式编程还有一个很重要的技术是 KVO,使用 KVO 来实现响应式开发的范例可以参考[我以前的一个 demo](https://github.com/ming1016/DecoupleDemo)。如果你有关于KVO的问题,也欢迎在评论区给我留言。
|
||||
|
||||
## 课后作业
|
||||
|
||||
在今天这篇文章里面,我和你聊了Monad 的很多缺点,不知道你是如何看待Monad的,在评论区给我留言分享下你的观点吧。
|
||||
|
||||
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
217
极客时间专栏/geek/iOS开发高手课/应用开发篇/23 | 如何构造酷炫的物理效果和过场动画效果?.md
Normal file
217
极客时间专栏/geek/iOS开发高手课/应用开发篇/23 | 如何构造酷炫的物理效果和过场动画效果?.md
Normal file
@@ -0,0 +1,217 @@
|
||||
<audio id="audio" title="23 | 如何构造酷炫的物理效果和过场动画效果?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/75/bd/75c3e3bcb537d8db8fe4a4c958d8dcbd.mp3"></audio>
|
||||
|
||||
你好,我是戴铭。今天,我要和你分享的是如何为你 App 添加酷炫的动画效果。
|
||||
|
||||
不论是iOS开发,还是Android开发,现在的动画库差不多都需要手动去编写动画代码。这样的话,iOS 和 Android 开发者就需要分别去编写适合自己系统的代码。而且,手动编写动画的代码也非常复杂,不容易维护,很多动画细节的调整还需要和动画设计师不断沟通打磨,尤其是千行以上的动画代码编写、维护、沟通的成本巨大。
|
||||
|
||||
手动编写动画代码,除了会影响到开发者外,动画设计师也难以幸免。一款产品适配的平台越多,动画设计师设计走查的周期就越长,相应的动画成本就越高。同时,动画设计师很兴奋地设计出一套炫酷地动画效果后,在要通过开发者实现出来时,却因为工时评估过长而一再被简化,甚至被直接取消。试想一下,以后他还会动力十足地去设计酷炫的动画效果吗?
|
||||
|
||||
所以,你会发现现在有酷炫的动画效果的 App 非常少,而且多是出自个人开发者之手。那么,这就提高了对个人开发者的要求,不但要求他代码写得好,还要能够设计出好的动画效果。但是,这样的人才也是不可多得。
|
||||
|
||||
那,到底有没有什么办法能够把动画制作和App开发隔离开,专人做专事,而且还能使得多个平台的动画效果保持一致呢?
|
||||
|
||||
办法总比困难多。接下来,我们就一起看看如何实现的问题吧。
|
||||
|
||||
## Lottie
|
||||
|
||||
[Lottie 框架](http://airbnb.io/lottie/#/)就很好地解决了动画制作与开发隔离,以及多平台统一的问题。
|
||||
|
||||
Lottie 是 Airbnb 开源的一个动画框架。Lottie 这个名字来自于一名德国导演洛特·赖尼格尔(Lotte Reiniger),她最著名的电影叫作“阿赫迈德王子历险记(The Adventures of Prince Achmed)”。这个框架和其他的动画框架不太一样,动画的编写和维护将由动画设计师完成,完全无需开发者操心。
|
||||
|
||||
动画设计师做好动画以后,可以使用[After Effects](https://www.adobe.com/products/aftereffects.html)将动画导出成JSON文件,然后由Lottie 加载和渲染这个JSON文件,并转换成对应的动画代码。由于是JSON格式,文件也会很小,可以减少 App 包大小。运行时还可以通过代码控制更改动画,比如更改颜色、位置以及任何关键值。另外,Lottie 还支持页面切换的过场动画(UIViewController Transitions)。
|
||||
|
||||
下面的两张动画,就是使用Lottie 做出来的效果。<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/4a/c9/4a6a19fdb4fc53757d27ddb6aa4380c9.gif" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/27/75/270ed9bc73a18fde74611bdbef419975.gif" alt="">
|
||||
|
||||
上面这些动画,就是由动画设计师使用 After Effects 创作,然后使用 [Bodymovin](https://github.com/airbnb/lottie-web)进行导出的,开发者完全不用做什么额外的代码工作,就能够使用原生方式将其渲染出来。
|
||||
|
||||
Bodymovin 是 Hernan Torrisi 做的一个 After Effects 的插件,起初导出的JSON文件只是通过 JavaScript 在网页中进行动画的播放,后来才将JSON文件的解析渲染应用到了其他平台上。
|
||||
|
||||
那么,如何使用 Bodymovin 呢?
|
||||
|
||||
## Bodymovin
|
||||
|
||||
你需要先到[Adobe官网](https://www.adobeexchange.com/creativecloud.details.12557.html)下载Bodymovin插件,并在 After Effects 中安装。使用 After Effects 制作完动画后,选择 Windows 菜单,找到 Extensions 的 Bodymovin 项,在菜单中选择 Render 按钮就可以输出JSON文件了。
|
||||
|
||||
[LottieFiles网站](https://lottiefiles.com/)还是一个动画设计师分享作品的平台,每个动画效果的JSON文件都可下载使用。所以,如果你现在没有动画设计师配合的话,可以到这个网站去查找并下载一个 Bodymovin 生成的JSON文件,然后运用到工程中去试试效果。
|
||||
|
||||
## 在 iOS 中使用 Lottie
|
||||
|
||||
在iOS开发中使用Lottie也很简单,只要集成 Lottie 框架,然后在程序中通过 Lottie 的接口控制 After Effects 生成的动画 JSON 就行了。
|
||||
|
||||
首先,你可以通过 CocoaPods 集成 Lottie 框架到你工程中。Lottie iOS 框架的 GitHub 地址是[https://github.com/airbnb/lottie-ios/](https://github.com/airbnb/lottie-ios/),官方也提供了[可供学习的示例](https://github.com/airbnb/lottie-ios/tree/master/Example)。
|
||||
|
||||
然后,快速读取一个由Bodymovin 生成的JSON文件进行播放。具体代码如下所示:
|
||||
|
||||
```
|
||||
LOTAnimationView *animation = [LOTAnimationView animationNamed:@"Lottie"];
|
||||
[self.view addSubview:animation];
|
||||
[animation playWithCompletion:^(BOOL animationFinished) {
|
||||
// 动画完成后需要处理的事情
|
||||
}];
|
||||
|
||||
```
|
||||
|
||||
利用 Lottie 的动画进度控制能力,还可以完成手势与动效同步的问题。动画进度控制是 LOTAnimationView 的 animationProgress 属性,设置属性的示例代码如下:
|
||||
|
||||
```
|
||||
CGPoint translation = [gesture getTranslationInView:self.view];
|
||||
CGFloat progress = translation.y / self.view.bounds.size.height;
|
||||
animationView.animationProgress = progress;
|
||||
|
||||
```
|
||||
|
||||
Lottie 还带有一个 UIViewController animation-controller,可以自定义页面切换的过场动画,示例代码如下:
|
||||
|
||||
```
|
||||
#pragma mark -- 定制转场动画
|
||||
|
||||
// 代理返回推出控制器的动画
|
||||
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
|
||||
LOTAnimationTransitionController *animationController = [[LOTAnimationTransitionController alloc] initWithAnimationNamed:@"vcTransition1" fromLayerNamed:@"outLayer" toLayerNamed:@"inLayer" applyAnimationTransform:NO];
|
||||
return animationController;
|
||||
}
|
||||
|
||||
// 代理返回退出控制器的动画
|
||||
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
|
||||
LOTAnimationTransitionController *animationController = [[LOTAnimationTransitionController alloc] initWithAnimationNamed:@"vcTransition2" fromLayerNamed:@"outLayer" toLayerNamed:@"inLayer" applyAnimationTransform:NO];
|
||||
return animationController;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Lottie 在运行期间提供接口和协议来更改动画,有动画数据搜索接口 LOTKeyPath,以及设置动画数据的协议 LOTValueDelegate。详细的说明和使用示例代码,你可以参看[官方 iOS 教程](http://airbnb.io/lottie/#/ios)。
|
||||
|
||||
## 多平台支持
|
||||
|
||||
Lottie 支持多平台,除了 支持[iOS](https://github.com/airbnb/lottie-ios),还支持 [Android](https://github.com/airbnb/lottie-android) 、[React Native](https://github.com/react-native-community/lottie-react-native)和[Flutter](https://github.com/simolus3/fluttie)。除了官方维护的这些平台外,Lottie还支持[Windows](https://github.com/windows-toolkit/Lottie-Windows)、[Qt](https://blog.qt.io/blog/2019/03/08/announcing-qtlottie/)、[Skia](https://skia.org/user/modules/skottie) 。陈卿还实现了 [React](https://github.com/chenqingspring/react-lottie)、[Vue](https://github.com/chenqingspring/vue-lottie)和[Angular](https://github.com/chenqingspring/ng-lottie)对 Lottie的支持,并已将代码放到了GitHub上。
|
||||
|
||||
有了这么多平台的支持,对于动画设计师来说,可以安心做动画,只要简单地转换就可以完美展现动画效果,再也不用担心到开发者那里动画效果被大打折扣了。而对于开发者来说,再也不用写那些难以维护的大量动效代码了,而且App安装包的体积还变小了。
|
||||
|
||||
那么,**这么神奇的框架,在 iOS 里到底是怎么实现的呢?**接下来,我们就看下Lottie的实现原理吧。
|
||||
|
||||
通过原理的学习,你会掌握通过 JSON 来控制代码逻辑的能力。比如,你可以把运营活动流程的代码逻辑设计为一种规范,再设计一个拖拽工具用来创建运营活动流程,最后生成一份表示运营活动逻辑的 JSON,下发到 App 内来开启新的运营活动。
|
||||
|
||||
## Lottie 实现原理
|
||||
|
||||
实际上,[Lottie iOS](https://github.com/airbnb/lottie-ios)在 iOS 内做的事情就是将 After Effects 编辑的动画内容,通过JSON文件这个中间媒介,一一映射到 iOS 的 LayerModel、Keyframe、ShapeItem、DashElement、Marker、Mask、Transform 这些类的属性中并保存了下来,接下来再通过 CoreAnimation 进行渲染。这就和你手动写动画代码的实现是一样的,只不过这个过程的精准描述,全部由动画设计师通过 JSON文件输入进来了。
|
||||
|
||||
Lottie iOS 使用系统自带的 Codable协议来解析JSON文件,这样就可以享受系统升级带来性能提升的便利,比如 ShapeItem 这个类设计如下:
|
||||
|
||||
```
|
||||
// Shape Layer
|
||||
class ShapeItem: Codable {
|
||||
|
||||
/// shape 的名字
|
||||
let name: String
|
||||
|
||||
/// shape 的类型
|
||||
let type: ShapeType
|
||||
|
||||
// 和 json 中字符映射
|
||||
private enum CodingKeys : String, CodingKey {
|
||||
case name = "nm"
|
||||
case type = "ty"
|
||||
}
|
||||
// 初始化
|
||||
required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: ShapeItem.CodingKeys.self)
|
||||
self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Layer"
|
||||
self.type = try container.decode(ShapeType.self, forKey: .type)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
通过上面代码可以看出,ShapeItem 有两个属性,映射到JSON的字符键值是 nm 和 ty,分别代表 shape 的名字和类型。下面,我们再一起看一段 Bodymovin 生成的JSON代码:
|
||||
|
||||
```
|
||||
{"ty":"st","fillEnabled":true,"c":{"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":22,"s":[0,0.65,0.6,1],"e":[0.76,0.76,0.76,1]},{"t":36}]},"o":{"k":100},"w":{"k":3},"lc":2,"lj":2,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"}
|
||||
|
||||
|
||||
```
|
||||
|
||||
在这段JSON代码中,nm 键对应的值是 Stroke 1,ty 键对应的值是 st。那我们再来看看,**st 是什么类型。**
|
||||
|
||||
我们知道,ShapeType 是个枚举类型,它的定义如下:
|
||||
|
||||
```
|
||||
enum ShapeType: String, Codable {
|
||||
case ellipse = "el"
|
||||
case fill = "fl"
|
||||
case gradientFill = "gf"
|
||||
case group = "gr"
|
||||
case gradientStroke = "gs"
|
||||
case merge = "mm"
|
||||
case rectangle = "rc"
|
||||
case repeater = "rp"
|
||||
case round = "rd"
|
||||
case shape = "sh"
|
||||
case star = "sr"
|
||||
case stroke = "st"
|
||||
case trim = "tm"
|
||||
case transform = "tr"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
通过上面的枚举定义,可以看到 st 对应的是 stroke 类型。
|
||||
|
||||
Lottie 就是通过这种方式,定义了一系列的类结构,可以将JSON数据全部映射过来。所有映射用的类都放在 Lottie 的 Model 目录下。使用 CoreAnimation 渲染的相关代码都在 NodeRenderSystem 目录下,比如前面举例的 Stoke。
|
||||
|
||||
在渲染前会生成一个节点,实现在 StrokeNode.swift 里,然后对 StokeNode 这个节点渲染的逻辑在 StrokeRenderer.swift 里。核心代码如下:
|
||||
|
||||
```
|
||||
// 设置 Context
|
||||
func setupForStroke(_ inContext: CGContext) {
|
||||
inContext.setLineWidth(width) // 行宽
|
||||
inContext.setMiterLimit(miterLimit)
|
||||
inContext.setLineCap(lineCap.cgLineCap) // 行间隔
|
||||
inContext.setLineJoin(lineJoin.cgLineJoin)
|
||||
// 设置线条样式
|
||||
if let dashPhase = dashPhase, let lengths = dashLengths {
|
||||
inContext.setLineDash(phase: dashPhase, lengths: lengths)
|
||||
} else {
|
||||
inContext.setLineDash(phase: 0, lengths: [])
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染
|
||||
func render(_ inContext: CGContext) {
|
||||
guard inContext.path != nil && inContext.path!.isEmpty == false else {
|
||||
return
|
||||
}
|
||||
guard let color = color else { return }
|
||||
hasUpdate = false
|
||||
setupForStroke(inContext)
|
||||
inContext.setAlpha(opacity) // 设置透明度
|
||||
inContext.setStrokeColor(color) // 设置颜色
|
||||
inContext.strokePath()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这段代码看起来是不是就很熟悉了?
|
||||
|
||||
如果是手写动画,这些代码就需要不断重复地写。使用第三方库去写动画的话,也无非就是多封装了一层,而属性的设置、动画时间的设置等,还是需要手动添加很多代码来完成。
|
||||
|
||||
但是,使用 Lottie 后,你就完全不用去管这些代码了,只需要在 After Effects 那设置属性、控制动画时间就好了。
|
||||
|
||||
## 小结
|
||||
|
||||
今天这篇文章,我分享了一个制作动画的利器 Lottie,并和你说了如何在 iOS 中使用,以及它的实现原理。听到这,你一定感到奇怪, iOS 开发中还有很多优秀的动画框架,比如 Pop,但是为什么我只跟你说了 Lottie 呢?
|
||||
|
||||
因为在我看来, Lottie 这样的工作流程或许就是未来的趋势,就像 iOS 现在的发展趋势一样,越来越多的业务逻辑不再需要全部使用 Objective-C 或 Swift 来实现了,而是使用JavaScript 语言或者 DSL 甚至是工具来描述业务,然后将描述业务的代码转换成一种中间代码,比如 JSON,不同平台再对相同的中间代码进行解析处理,以执行中间代码描述的业务逻辑。
|
||||
|
||||
这样做不仅可以减轻 App 包的大小,实现多端逻辑的统一处理,还可以让团队分工更加明确,一部分人专门开发业务代码,另一部分人负责端内稳定性、质量把控、性能提升工作的建设。
|
||||
|
||||
## 课后作业
|
||||
|
||||
相信你看到这,一定已经忍不住想小试身手了,那么就请你到 [LottieFiles](https://lottiefiles.com/)网站下载一个JSON文件,做一个 Lottie Demo 感受下吧。
|
||||
|
||||
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
167
极客时间专栏/geek/iOS开发高手课/应用开发篇/24 | A|B 测试:验证决策效果的利器.md
Normal file
167
极客时间专栏/geek/iOS开发高手课/应用开发篇/24 | A|B 测试:验证决策效果的利器.md
Normal file
@@ -0,0 +1,167 @@
|
||||
<audio id="audio" title="24 | A/B 测试:验证决策效果的利器" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a2/7f/a2d650678ab47a5fc3b69d98d41dcd7f.mp3"></audio>
|
||||
|
||||
你好,我是戴铭。今天,我来跟你聊聊验证决策的利器 A/B测试。
|
||||
|
||||
现在App Store中的应用,就像商场中的商品一样琳琅满目,可以解决用户各个方面的需求。这时,你要想创新,或者做出比竞品更优秀的功能,是越来越不容易。所以,很多公司都必须去做一些实验,看看有哪些功能可以增强自己App的竞争力,又有哪些功能可以废弃掉。而进行这样的实验的主要方法,就是A/B 测试。
|
||||
|
||||
A/B测试,也叫桶测试或分流测试,指的是针对一个变量的两个版本 A 和 B,来测试用户的不同反应,从而判断出哪个版本更有效,类似统计学领域使用的双样本假设测试。
|
||||
|
||||
简单地说,A/B测试就是检查App 的不同用户在使用不同版本的功能时,哪个版本的用户反馈最好。
|
||||
|
||||
比如,引导用户加入会员的按钮,要设置为什么颜色更能吸引他们加入,这时候我们就需要进行 A/B测试。产品接触的多了,我们自然清楚一个按钮的颜色,会影响到用户点击它,并进入会员介绍页面的概率。
|
||||
|
||||
这里我再和你分享一件有意思的事儿。记得我毕业后去新西兰的那段时间里,认识了一个住在海边的油画家,她在海边还有一间画廊,出售自己的作品还有美院学生的作品。
|
||||
|
||||
有一天她要给画廊门面重涂油漆,叫我过去帮忙。涂漆之前问我用什么颜色好,我环顾下了旁边的店面,大多是黑色、灰色和深蓝色,而我觉得卖橄榄球衣服那家的黑底红字,看起来很帅气,于是就说黑色可能不错。
|
||||
|
||||
她想了想摇头说:我觉得橙色好,因为这附近都是暗色调,如果用了明亮的橙色可能更容易吸引游客。结果呢,后来一段时间进店的人确实多了,而且画也卖得多了。
|
||||
|
||||
当然了,我举这个例子的目的不是说用了橙色就一定能够提高用户进店率。试想一下,如果这个画廊周围都是花花绿绿的店面,你还能够保证橙色会吸引用户吗。
|
||||
|
||||
实际情况往往要比选择门面颜色更复杂,也只有有专业经验的人才可以做出正确的决策,但并不是每个人都是有相关领域经验的专家。所以,就有了A/B测试这一利器,来辅助我们进行决策。
|
||||
|
||||
知乎上有个关于[A/B测试](https://www.zhihu.com/question/20045543)的问答,里面列举了很多关于实际案例,有兴趣的话你可以去看看。接下来,我和你说说iOS中的A/B测试。
|
||||
|
||||
## App 开发中的 A/B测试
|
||||
|
||||
从 App 开发层面看,新版本发布频繁,基本上是每月或者每半月会发布一个版本。那么,新版本发布后,我们还需要观察界面调整后情况如何,性能问题修复后线上情况如何,新加功能使用情况如何等。这时,我们就需要进行A/B测试来帮助我们分析这些情况,通过度量每个版本的测试数据,来确定下一个版本应该如何迭代。
|
||||
|
||||
对于 App 版本迭代的情况简单说就是,新版本总会在旧版本的基础上做修改。这里,我们可以把旧版本理解为 A/B测试里的 A 版本,把新版本理解为B 版本。在 A/B测试中 A 版本和 B 版本会同时存在,B 版本一开始是将小部分用户放到 B 测试桶里,逐步扩大用户范围,通过分析A版本和 B 版本的数据,看哪个版本更接近期望的目标,最终确定用哪个版本。
|
||||
|
||||
总的来说,A/B测试就是以数据驱动的可回退的灰度方案,客观、安全、风险小,是一种成熟的试错机制。
|
||||
|
||||
## A/B测试全景设计
|
||||
|
||||
一个 A/B测试框架主要包括三部分:
|
||||
|
||||
<li>
|
||||
策略服务,为策略制定者提供策略;
|
||||
</li>
|
||||
<li>
|
||||
A/B测试 SDK,集成在客户端内,用来处理上层业务去走不同的策略;
|
||||
</li>
|
||||
<li>
|
||||
日志系统,负责反馈策略结果供分析人员分析不同策略执行的结果。
|
||||
</li>
|
||||
|
||||
其中,策略服务包含了决策流程、策略维度。A/B测试 SDK 将用户放在不同测试桶里,测试桶可以按照系统信息、地址位置、发布渠道等来划分。日志系统和策略服务,主要是用作服务端处理的,这里我就不再展开了。
|
||||
|
||||
下图是 A/B测试方案的结构图:<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/3f/73/3f56a1a1616f8e95fa3ef1be1ee04d73.png" alt="">
|
||||
|
||||
今天我主要跟你说下客户端内的 A/B测试 SDK。从 iOS 开发者的角度看 A/B测试,如何设计或选择一个好用的 A/B测试 SDK 框架才是我们最关心的。
|
||||
|
||||
## A/B测试 SDK
|
||||
|
||||
谈到A/B测试 SDK框架,我们需要首先要考虑的是生效机制。生效机制主要分为冷启动生效和热启动生效,相对于冷启动,热启动落实策略要及时些。但是,考虑到一个策略可能关联到多个页面或者多个功能,冷启动可以保持策略整体一致性。
|
||||
|
||||
所以我的结论是,**如果一个策略只在一个地方生效的话,可以使用热启动生效机制;而如果一个策略在多个地方生效的话,最好使用冷启动生效机制。**
|
||||
|
||||
除了生效机制,A/B测试SDK框架对于业务方调用接口的设计也很重要。你所熟悉的著名 [AFNetworking](https://github.com/AFNetworking/AFNetworking) 网络库和 [Alamofire](https://github.com/Alamofire/Alamofire) 网络库的作者 Mattt ,曾编写过一个叫作[SkyLab](https://github.com/mattt/SkyLab)的A/B测试库。
|
||||
|
||||
SkyLab 使用的是NSUserDefault 保存策略,使得每个用户在使用过程中,不管是在哪个测试桶里,都能够保持相同的策略。 SkyLab 对外的调用接口,和 AFNetworking 一样使用的是 Block ,来接收版本A 和 B的区别处理。这样设计的接口易用性非常高。
|
||||
|
||||
通过 SkeyLab 原理的学习,你能够体会到如何设计一个优秀易用的接口。这,对你开发公用库的帮助会非常大。
|
||||
|
||||
接下来,我们先看看 SkeyLab 接口使用代码,示例如下:
|
||||
|
||||
```
|
||||
// A/B Test
|
||||
[SkyLab abTestWithName:@"Title" A:^{
|
||||
self.titleLabel.text = NSLocalizedString(@"Hello, World!", nil);
|
||||
} B:^{
|
||||
self.titleLabel.text = NSLocalizedString(@"Greetings, Planet!", nil);
|
||||
}];
|
||||
|
||||
```
|
||||
|
||||
可以看出,Mattt这个人的接口设计功底有多强了。你一看这两个 block 参数名称,就知道是用来做A/B测试的,简单明了。接下来,我们再进入接口看看 Mattt 是具体怎么实现的。
|
||||
|
||||
```
|
||||
+ (void)abTestWithName:(NSString *)name
|
||||
A:(void (^)())A
|
||||
B:(void (^)())B
|
||||
{
|
||||
[self splitTestWithName:name conditions:[NSArray arrayWithObjects:@"A", @"B", nil] block:^(NSString *choice) {
|
||||
if ([choice isEqualToString:@"A"] && A) {
|
||||
// 执行版本 A
|
||||
A();
|
||||
} else if ([choice isEqualToString:@"B"] && B) {
|
||||
// 执行版本 B
|
||||
B();
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
你会发现 SkyLab:abTestWithName:A:B: 方法只是一个包装层,里面真正的实现是 SkyLab:splitTestWithName:conditions:block 方法,其定义如下:
|
||||
|
||||
```
|
||||
+ (void)splitTestWithName:(NSString *)name
|
||||
conditions:(id <NSFastEnumeration>)conditions
|
||||
block:(void (^)(id condition))block;
|
||||
|
||||
```
|
||||
|
||||
通过定义你会发现,conditions 参数是个 id 类型,通过类型约束,即使用 NSFastEnumeration 协议进行了类型限制。Mattt 是希望这个参数能够接收字典和数组,而字典和数组都遵循NSFastEnumeration 协议的限制,两者定义如下:
|
||||
|
||||
```
|
||||
@interface NSDictionary<__covariant KeyType, __covariant ObjectType> : NSObject <NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration>
|
||||
|
||||
@interface NSArray<__covariant ObjectType> : NSObject <NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration>
|
||||
|
||||
```
|
||||
|
||||
在这里,我和你介绍这个接口的设计方式,是因为这个设计非常赞,非常值得我们学习。类型约束,是苹果公司首先在 Swift 泛型引入的一个特性,后来引入到了 Objective-C 中。
|
||||
|
||||
而之所以设计 conditions 这个支持数组和字典的参数,本来是为了扩展这个SkyLab 框架,使其不仅能够支持 A/B测试,还能够支持更为复杂的 [Multivariate testing](https://en.wikipedia.org/wiki/Multivariate_statistics)或 [Multinomial testing](https://en.wikipedia.org/wiki/Multinomial_test)。Multivariate testing 和 Multinomial testing 的区别在于,支持更多版本变体来进行测试验证。
|
||||
|
||||
**接下来,我们再看看 SkyLab 是如何做人群测试桶划分的。**
|
||||
|
||||
SkyLab 使用的是随机分配方式,会将分配结果通过 NSUserDefaults 进行持续化存储,以确保测试桶的一致性。其实测试桶分配最好由服务端来控制,这样服务端能够随时根据用户群的维度分布分配测试桶。
|
||||
|
||||
如果你所在项目缺少服务端支持的话,SkyLab 对测试桶的分配方式还是非常值得借鉴的。SkyLab 对 A/B测试的测试桶分配代码如下:
|
||||
|
||||
```
|
||||
static id SLRandomValueFromArray(NSArray *array) {
|
||||
if ([array count] == 0) {
|
||||
return nil;
|
||||
}
|
||||
// 使用 arc4random_uniform 方法随机返回传入数组中某个值
|
||||
return [array objectAtIndex:(NSUInteger)arc4random_uniform([array count])];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
代码中的 array 参数就是包含 A 和 B 两个版本的数组,随机返回 A 版本或 B 版本,然后保存返回版本。实现代码如下:
|
||||
|
||||
```
|
||||
condition = SLRandomValueFromArray(mutableCandidates);
|
||||
// 判断是否需要立刻进行同步保存
|
||||
BOOL needsSynchronization = ![condition isEqual:[[NSUserDefaults standardUserDefaults] objectForKey:SLUserDefaultsKeyForTestName(name)]];
|
||||
// 通过 NSUserDefaults 进行保存
|
||||
[[NSUserDefaults standardUserDefaults] setObject:condition forKey:SLUserDefaultsKeyForTestName(name)];
|
||||
if (needsSynchronization) {
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
持久化存储后,当前用户就命中了 A和B 版本中的一个,后续的使用会一直按照某个版本来,操作的关键数据会通过日志记录,并反馈到统计后台。至此,你就可以通过 A、B 版本的数据比较,来决策哪个版本更优了。
|
||||
|
||||
## 小结
|
||||
|
||||
今天我跟你说了 A/B测试在产品中的重要性,特别是在 App 版本迭代时,A/B测试可以帮助我们判断新版本的功能更新是否能够更好地服务用户。然后,我为你展示了 A/B测试方案的全景设计,并针对其中iOS开发者最关注的A/B测试 SDK 的设计做了详细分享。
|
||||
|
||||
通过 Mattt 设计的 SkyLab 这个 A/B测试 SDK框架,你会发现好的接口设计不是凭空想出来的,而是需要一定的知识积累。比如,将泛型的类型约束引入到 Objective-C 中以提高接口易用性,这需要了解Swift才能够做到的。
|
||||
|
||||
今天我在看评论区的留言时,有同学问我现在应该学习 Objective-C 还是 Swift,为什么?我想,我们今天对 SkyLab 接口的分析应该就是最好的回答了。知识的学习最好结合工作需求来,无论是 Objective-C 还是 Swift,最重要的还是代码设计能力。
|
||||
|
||||
## 课后作业
|
||||
|
||||
今天我留给你一个作业,前面我提到 Swift 是值得学习的,那么今天的作业就是参照 SkyLab,使用 Swift 来写一个 A/B测试 SDK。
|
||||
|
||||
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
111
极客时间专栏/geek/iOS开发高手课/应用开发篇/25 | 怎样构建底层的发布和订阅事件总线?.md
Normal file
111
极客时间专栏/geek/iOS开发高手课/应用开发篇/25 | 怎样构建底层的发布和订阅事件总线?.md
Normal file
@@ -0,0 +1,111 @@
|
||||
<audio id="audio" title="25 | 怎样构建底层的发布和订阅事件总线?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/90/6d/90163f08777fb74828982ad44747bc6d.mp3"></audio>
|
||||
|
||||
你好,我是戴铭。今天,我来跟你聊聊怎么构建事件总线。
|
||||
|
||||
事件总线是对发布和订阅设计模式的一种实现,通过发布、订阅可以将组件间一对一和一对多的耦合关系解开。这种设计模式,特别适合数据层通过异步发布数据的方式告知 UI 层订阅者,使得 UI 层和数据层可以不用耦合在一起,在重构数据层或者 UI 层时不影响业务层。
|
||||
|
||||
现在,我们先一起来捋一下 iOS 系统里有没有现成可用的技术,当数据层异步发布数据后,可以通过 Delegate 回调给 UI 层来进行展示,但是这个只适合一对一的模式。如果异步处理完后,还需要将数据发布给其他 UI 进行处理和展示的话,就需要继续发布给其他 Delegate,从而造成 Delegate 套 Delegate 的情况。
|
||||
|
||||
使用 Block 和使用 Delegate 的情况类似。如果需要不断异步发布给下一个数据订阅者的话,也会出现 Block 回调嵌套其他 Block 回调的情况。
|
||||
|
||||
iOS 系统里也有一对多模式的技术,比如 KVO 和 NSNotificationCenter。
|
||||
|
||||
使用 KVO 是强依赖属性的,只要更新了属性就会发布给所有的观察者,对应关系过于灵活,难以管控和维护。NSNotificationCenter 也有类似的问题,通过字符串来维护发布者和订阅者之间的关系,不仅可读性差,而且和 KVO 一样面临着难以管控和维护的情况。
|
||||
|
||||
总的来说,由于 Delegate 和 Block 只适合做一对一数据传递,KVO 和 NSNotificationCenter 虽然可以支持一对多的数据传递,但存在过于灵活而无法管控和维护的问题,而事件总线需要通过发布和订阅这种可管控方式实现一对一和一对多数据传递。由此可以看出,iOS 现有的 Delegate、Block、KVO、NSNotificationCenter 等技术并不适合来做事件总线。
|
||||
|
||||
既然iOS系统提供的技术没有适合做事件总线的,那么有没有好的第三方库可以处理事件总线呢?
|
||||
|
||||
其实,响应式第三方库 ReactiveCocoa 和 RxSwift 对事件总线的支持是没有问题的,但这两个库更侧重的是响应式编程,事件总线只是其中很小的一部分。所以,使用它们的话,就有种杀鸡焉用牛刀的感觉。
|
||||
|
||||
那么,事件总线有没有小而美的第三方库可用呢?
|
||||
|
||||
## Promise
|
||||
|
||||
现在前端领域有一种模式叫作 Promise,这是一种专门针对异步数据操作编写的一套统一规则的模式。
|
||||
|
||||
本质上,这种模式本质是通过 Promise 对象保存异步数据操作,同时 Promise 对象提供统一的异步数据操作事件处理的接口。这样,事件总线的数据订阅和数据发布事件,就可以通过 Promise 对象提供的接口实现出来,比以前通过Delegate回调处理异步事件来说更加合理。
|
||||
|
||||
接下来,我们再一起看看,Promise 模式中的 Promise 对象是怎么运作的。
|
||||
|
||||
Promise的概念最早是在 [E 语言](http://erights.org/elib/distrib/pipeline.html)中被提出的。C++ 11 以 std :: promise 模板形式加入到标准库中,随后出现了 CommonJS Promises/A 规范,jQuery 将这个规范实现后引入到 jQuery 1.5 版本中。
|
||||
|
||||
Promise 模式大受欢迎后, ECMAScript 6 将其写入了语言标准,统一了用法,并提供了原生 的Promise 对象。 Promise 对象里保存有异步事件,Promise 的统一接口,使得其他异步操作都能够用相同的接口来处理事件。
|
||||
|
||||
**Promise 对象会有三种状态**,分别是 pending、fulfilled、rejected:
|
||||
|
||||
- pending 表示 Promise 对象当前正在等待异步事件处理中;
|
||||
- fulfilled 指的是 Promise 对象当前处理的异步事件已经成功完成;
|
||||
- rejected 表示 Promise 对象当前处理的异步事件没有成功。
|
||||
|
||||
**Promise 对象还有两个重要的方法**,分别是 then 和 catch。Promise 对象每次执行完 then 和 catch 方法后,这两个方法会返回先前的 Promise 对象,同时根据异步操作结果改变 Promise 对象的状态。
|
||||
|
||||
then 和 catch 方法与 Promise 对象状态更改关系,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/99/c9/999e30f1245495434e8d39186d70c5c9.png" alt=""><br>
|
||||
如上图所示,执行 then 方法后返回的Promise 对象是 rejected 状态的话,程序会直接执行 catch 方法。then 方法执行的就是订阅操作,Promise 对象触发 then 方法就是事件总线中的发布操作,then 方法执行完返回 Promise 对象能够继续同步执行多个 then 方法,由此,实现了一个发布操作对应多个订阅事件。
|
||||
|
||||
有了 Promise 对象后,整个异步发布和订阅操作都以同步操作的方式表现出来了。Promise 对象不仅能够避免回调层层嵌套,而且通过 Promise的统一接口,使得事件总线的发布和订阅操作更加规范和易用。
|
||||
|
||||
## PromiseKit
|
||||
|
||||
ECMAScript 6 已经内置了 Promise 对象,使得前端开发者无需引入其他库就能够直接使用 Promise 来进行日常开发。随后,Homebrew的作者 Max Howell 开发了 PromiseKit,将 Promise 标准带到了 iOS 中。所以,现在 iOS 上也有了小而美的事件总线技术。
|
||||
|
||||
接下来,我就跟你介绍下如何使用 PromiseKit 吧,相信你一定会有种相见恨晚的感觉。
|
||||
|
||||
我们先来看看**如何使用 Promise 对象的 then 和 catch 方法**。
|
||||
|
||||
假设有这么一个需求:
|
||||
|
||||
- 首先,通过一个异步请求获取当前用户信息;
|
||||
- 然后,根据获取到的用户信息里的用户编号再去异步请求获取用户的时间轴列表;
|
||||
- 最后,将用户的时间轴列表数据,赋值给当前类的时间轴列表属性。
|
||||
|
||||
这里,我先给出使用 PromiseKit 实现的具体代码,然后我再和你分析其中的关键步骤。
|
||||
|
||||
使用PromiseKit实现的代码如下:
|
||||
|
||||
```
|
||||
firstly {
|
||||
// 异步获取当前用户信息
|
||||
fetchUserInfo()
|
||||
}.then { userInfo in
|
||||
// 使用异步获取到的用户信息中的 uid 再去异步获取用户的 timeline
|
||||
fetchUserTimeline(uid: userInfo.uid)
|
||||
}.then { timeline in
|
||||
// 记录 timeline
|
||||
self.timeline = timeline
|
||||
}.catch {
|
||||
// 整个方法链的错误都会在这处理
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
可以看出,多次异步请求通过 Promise 的方法调用,看起来就像进行同步操作一样,顺序和逻辑也更加清晰了。使用 then 方法可以让异步操作一个接着一个地按顺序进行。如果异步操作 fetchUserInfo 失败,会返回一个状态是 rejected 的 Promise 对象,返回的这个 Promise对象会跳过后面所有的then 方法直接执行 catch 方法。这就和事件总线中发布事件触发后,订阅事件会一个接一个执行是一样的。
|
||||
|
||||
除了 then 和 catch 方法以外,PromiseKit 还有一些好用的方法。
|
||||
|
||||
- 比如 always方法。使用了 always 方法以后, Promise 对象每次在执行方法时,都会执行一次 always 方法。
|
||||
- 再比如when 方法。这个方法的使用场景就是,指定多个异步操作,等这些操作都执行完成后就会执行 when 方法。when 方法类似 GCD 里面的 Dispatch Group,虽然实现的功能一样,但是代码简单了很多,使用起来也更加方便。
|
||||
|
||||
PromiseKit 还为苹果的 API 提供了扩展。这些扩展需要单独集成,你可以在[PromiseKit 组织页面](https://github.com/PromiseKit)获取。目前大部分常用的API都有扩展,比如 UIKit、Foundation、CoreLocation、QuartzCore、CloudKit 等等,甚至还支持了第三方的框架 Alamofire。
|
||||
|
||||
如果你觉得PromiseKit 提供的扩展还不够,还想让你使用的第三方库也支持 Promises的话,可以通过 PromiseKit 提供的扩展文档,或者直接查看已支持的第三方库(比如 Alamofire )的扩展实现,去学习如何让其他库也支持 Promises。
|
||||
|
||||
## 小结
|
||||
|
||||
在今天这篇文章中,我和你分享了事件总线是什么,以及事件总线解决了什么样的问题。
|
||||
|
||||
当工程业务逻辑越来越复杂时,你会发现如果数据层和 UI 层不做解耦,日后想进行重构或者优化就会非常困难。这,也是很多工程前期没有使用事件总线,到了后期会留下大量无法修改的代码的原因所在。
|
||||
|
||||
如果使用类似 Promise 这样的技术规范实现事件总线,通过简单、清晰、规范的 Promise 接口将异步的数据获取、业务逻辑、界面串起来,对于日后的维护或重构都会容易很多。
|
||||
|
||||
## 课后小作业
|
||||
|
||||
PromiseKit不仅支持 Swift语言,还支持 Objective-C。所以,今天的课后作业是,将 PromiseKit 集成到你的Objective-C工程中,并对其中一个模块进行改造。
|
||||
|
||||
很多优秀工具都是用过才知道好,心动不如行动,你也试试吧。
|
||||
|
||||
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
252
极客时间专栏/geek/iOS开发高手课/应用开发篇/26 | 如何提高 JSON 解析的性能?.md
Normal file
252
极客时间专栏/geek/iOS开发高手课/应用开发篇/26 | 如何提高 JSON 解析的性能?.md
Normal file
@@ -0,0 +1,252 @@
|
||||
<audio id="audio" title="26 | 如何提高 JSON 解析的性能?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a7/c4/a74dbde6c3bf91738e6acd7c821a47c4.mp3"></audio>
|
||||
|
||||
你好,我是戴铭。
|
||||
|
||||
在iOS 开发中,我们都会碰到这样的问题:不同团队开发的库需要进行数据通信,而通信数据规范通常很难确定。今天,我们就来聊聊如何高效地解决这个问题吧。
|
||||
|
||||
同一个编程语言之间的数据通信非常简单,因为数据的规范都是相同的,所以输入和输出不需要做任何转换。但是,在不同编程语言之间进行数据通信,就会比较麻烦了。比如,一种语言按照自身的标准规范输出了一份数据,另一门语言接收到时需要依据自身编程语言标准进行数据对齐。
|
||||
|
||||
对齐一门语言的数据或许你还能够接受,但是如果对接的语言多了,你就需要写很多份能够与之对应的数据对齐转换代码。编写和维护的成本可想而知,那么目前有没有一种通用,而且各个编程语言都能支持的数据格式呢?
|
||||
|
||||
答案是有的。这个数据格式,就是我今天要跟你聊的 JSON。
|
||||
|
||||
接下来,在今天这篇文章中,我会先和你聊聊什么是 JSON;然后,再和你说说 JSON 的使用场景,以及 iOS 里是如何解析 JSON 的;最后,再和你分析如何提高 JSON 的解析性能。
|
||||
|
||||
## 什么是 JSON?
|
||||
|
||||
JSON ,是JavaScript Object Notation的缩写。其实,JSON最初是被设计为 JavaScript 语言的一个子集,但最终因为和编程语言无关,所以成为了一种开放标准的常见数据格式。
|
||||
|
||||
虽然JSON源于 JavaScript,但到目前很多编程语言都有了 JSON 解析的库,包括 C、C++、Java、Perl、Python 等等。除此之外,还有很多编程语言内置了 JSON 生成和解析的方法,比如 PHP 在5.2版本开始内置了 json_encode() 方法,可以将 PHP 里的 Array 直接转化成 JSON。转换代码如下:
|
||||
|
||||
```
|
||||
$arr = array(array(7,11,21));
|
||||
echo json_encode($arr)."<br>";
|
||||
|
||||
$dic = array('name1' => 'val1', 'name2' => 'val2');
|
||||
echo json_encode($dic)
|
||||
|
||||
```
|
||||
|
||||
输出结果如下:
|
||||
|
||||
```
|
||||
[[7,11,21]]
|
||||
{"name1":"val1","name2":"val2"}
|
||||
|
||||
```
|
||||
|
||||
如上所示,生成了两个 JSON 对象,第一个解析完后就是一个二维数组,第二个解析完后就是一个字典。**有了编程语言内置方法解析和生成 JSON 的支持,JSON 成为了理想的数据交换格式。**
|
||||
|
||||
通过上面生成的 JSON 可以看出,JSON 这种文本数据交换格式易读,且结构简单。
|
||||
|
||||
JSON基于两种结构:
|
||||
|
||||
- 名字/值对集合:这种结构在其他编程语言里被实现为对象、字典、Hash 表、结构体或者关联数组。
|
||||
- 有序值列表:这种结构在其他编程语言里被实现为数组、向量、列表或序列。
|
||||
|
||||
各种编程语言都以某种形式支持着这两种结构。比如,PHP 的 Array 既支持名字/值对集合又支持有序值列表;在 Swift 里键值集合就是字典,有序值列表就是数组。**名字/值对集合**在 JSON 和 JavaScript 里都被称为对象。JSON语法图以及说明,你可以在 [JSON 官网](https://www.json.org/)查看。在这里,我只列出了几个用的比较多的语法图。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/00/4b/00077e283c07754189106221b6886c4b.gif" alt=""><br>
|
||||
如上面语法图所示,对象是以左大括号开头和右大括号结尾,名字后面跟冒号,名字/值对用逗号分隔。比如:
|
||||
|
||||
```
|
||||
{"name1":"val1","name2":"val2"}
|
||||
|
||||
```
|
||||
|
||||
**有序值列表**在 JSON 和 JavaScript 里都叫数组,其语法图如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/27/df/27be471d9d5c0a9604216fb55d1401df.gif" alt=""><br>
|
||||
可以看出数组是以左中括号开头,以右中括号结尾,值以逗号分隔。数组代码如下所示:
|
||||
|
||||
```
|
||||
[[7,11,21]]
|
||||
|
||||
```
|
||||
|
||||
**语法图中值**的语法图如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4b/ab/4b85ca012e31a1d9eb73b42a4e2220ab.gif" alt=""><br>
|
||||
可以看出,值可以是字符串、数字、对象、数组、布尔值ture、布尔值false、空值。根据这个语法,JSON 可以通过实现对象和数组的嵌套来描述更为复杂的数据结构。
|
||||
|
||||
JSON 是没有注释的,水平制表符、换行符、回车符都会被当做空格。字符串由双引号括起来,里面可以使零到多个 Unicode 字符序列,使用反斜杠来进行转义。
|
||||
|
||||
## JSON的使用场景
|
||||
|
||||
JSON 的数据结构和任何一门编程语言的语法结构比起来都要简单得多,但它能干的事情却一点儿也不少,甚至可以完整地描述出一门编程语言的代码逻辑。比如,下面的这段 JavaScript 代码:
|
||||
|
||||
```
|
||||
if (hour < 18) {
|
||||
greeting = "Good day";
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这段 JavaScript 代码的逻辑是,当 hour 变量小于18时,greeting 设置为 Good day 字符串,根据 JavaScript 的语法规则,完整逻辑的语法树结构可以通过 JSON 描述出来。对应的JSON,如下:
|
||||
|
||||
```
|
||||
{
|
||||
"type": "Program",
|
||||
"body": [
|
||||
{
|
||||
"type": "IfStatement",
|
||||
"test": {
|
||||
"type": "BinaryExpression",
|
||||
"left": {
|
||||
"type": "Identifier",
|
||||
"name": "hour"
|
||||
},
|
||||
"operator": "<",
|
||||
"right": {
|
||||
"type": "Literal",
|
||||
"value": 18,
|
||||
"raw": "18"
|
||||
}
|
||||
},
|
||||
"consequent": {
|
||||
"type": "BlockStatement",
|
||||
"body": [
|
||||
{
|
||||
"type": "ExpressionStatement",
|
||||
"expression": {
|
||||
"type": "AssignmentExpression",
|
||||
"operator": "=",
|
||||
"left": {
|
||||
"type": "Identifier",
|
||||
"name": "greeting"
|
||||
},
|
||||
"right": {
|
||||
"type": "Literal",
|
||||
"value": "Good day",
|
||||
"raw": "\"Good day\""
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"alternate": null
|
||||
}
|
||||
],
|
||||
"sourceType": "module"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
从上面的 JSON 代码可以看出,每个语法树节点都是一个 JSON 对象,同级节点使用的是 JSON 数组。JavaScript 语法规则标准可以在[Ecma 网站](https://www.ecma-international.org/publications/standards/Standard.htm)上找到。
|
||||
|
||||
比如下面这段 JavaScript 代码:
|
||||
|
||||
```
|
||||
button.onclick = function() {
|
||||
var name = realname('Tom');
|
||||
if(name.length >= 5) {
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面这段 JavaScript 代码对应的语法树如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ad/42/adbf1a5955d2d7014691d098bd8be942.jpeg" alt=""><br>
|
||||
JavaScript 编程语言的语法树能够使用 JSON 来描述,其他编程语言同样也可以,比如Objective-C 或 Swift,都能够生成自己的语法树结构,转成 JSON 后能够在运行期被动态地识别。因此,**App 的业务逻辑动态化就不仅限于使用 JavaScript 这一门语言来编写,而是可以选择使用其他你熟悉的语言。**
|
||||
|
||||
JSON 不仅可以描述业务数据使得业务数据能够动态更新,还可以用来描述业务逻辑,以实现业务逻辑的动态化,除此之外还可以用来描述页面布局。比如,我以前就做过这么一件事儿:解析一个H5页面编辑器生成的 JSON,将 JSON 对应生成 iOS 原生界面布局代码。我当时是用 Swift 语言来编写这个项目的,完整代码在[这里](https://github.com/ming1016/HTN/tree/master/Sources/H5Editor)。
|
||||
|
||||
在这个项目中,对JSON 的解析使用的是系统自带的 JSONDecoder 的 decode 方法,具体代码如下:
|
||||
|
||||
```
|
||||
let jsonData = jsonString.data(using: .utf8)!
|
||||
let decoder = JSONDecoder()
|
||||
let jsonModel = try! decoder.decode(H5Editor.self, from: jsonData)
|
||||
|
||||
```
|
||||
|
||||
上面代码中的,H5Editor 是一个结构体,能够记录 JSON 解析后的字典和数组。H5Editor 结构体完整定义,请点击[这里的链接](https://github.com/ming1016/HTN/blob/master/Sources/H5Editor/H5EditorStruct.swift)。
|
||||
|
||||
那么, JSONDecoder 的 decode 方法到底是怎么解析 JSON 的呢?在我看来,了解这一过程的最好方式,就是直接看看它在Swift 源码里是怎么实现的。
|
||||
|
||||
## JSONDecoder 如何解析 JSON?
|
||||
|
||||
JSONDecoder 的代码,你可以[在 Swift 的官方 GitHub 上](https://github.com/apple/swift/blob/master/stdlib/public/Darwin/Foundation/JSONEncoder.swift)查看。
|
||||
|
||||
接下来,我先跟你说下解析 JSON 的入口, JSONDecoder 的 decode 方法。下面是 decode 方法的定义代码:
|
||||
|
||||
```
|
||||
open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {
|
||||
let topLevel: Any
|
||||
do {
|
||||
topLevel = try JSONSerialization.jsonObject(with: data)
|
||||
} catch {
|
||||
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error))
|
||||
}
|
||||
// JSONDecoder 的初始化
|
||||
let decoder = __JSONDecoder(referencing: topLevel, options: self.options)
|
||||
// 从顶层开始解析 JSON
|
||||
guard let value = try decoder.unbox(topLevel, as: type) else {
|
||||
throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
接下来,我们通过上面的代码一起来看看 decode 方法是如何解析 JSON 的。
|
||||
|
||||
上面 decode 方法入参 T.type 的 T 是一个泛型,具体到解析H5页面编辑器生成的 JSON 的例子,就是 H5Editor 结构体;入参 data 就是 JSON 字符串转成的 Data 数据。
|
||||
|
||||
decode 方法在解析完后会将解析到的数据保存到传入的结构体中,然后返回。在 decode 方法里可以看到,对于传入的 Data 数据会首先通过 JSONSerialization 方法转化成 topLevel 原生对象,然后topLevel 原生对象通过 JSONDecoder 初始化成一个 JSONDecoder 对象,最后使用 JSONDecoder 的 unbox 方法将数据和传入的结构体对应上,并保存在结构体里进行返回。
|
||||
|
||||
可以看出,目前 JSONSerialization 已经能够很好地解析 JSON,JSONDecoder将其包装以后,通过 unbox 方法使得 JSON 解析后能很方便地匹配 JSON 数据结构和 Swift 原生结构体。
|
||||
|
||||
试想一下,如果要将 JSON 应用到更大的场景时,比如对编程语言的描述或者界面布局的描述,其生成的 JSON 文件可能会很大,并且对这种大 JSON 文件解析性能的要求也会更高。那么,有比JSONSerialization性能更好的解析JSON的方法吗?
|
||||
|
||||
## 提高 JSON 解析性能
|
||||
|
||||
2019年2月,Geoff Langdale 和 Daniel Lemire发布了 [simdjson](https://github.com/lemire/simdjson)。 simdjson是一款他们研究了很久的快速 JSON 解析器, 号称每秒可解析千兆字节 JSON 文件。simdjson 和其他 JSON 解析器对比如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/40/5e/401c6aba33f2335f242dbd8f8305885e.png" alt=""><br>
|
||||
可以看出,只有 simdjson 能够达到每秒千兆字节级别,并且远远高于其他 JSON 解析器。那么 ,simdjson 是怎么做到的呢?接下来,我通过 simdjson 解析 JSON 的两个阶段来跟你说明下这个问题。
|
||||
|
||||
**第一个阶段,**使用 simdjson 去发现需要 JSON 里重要的字符集,比如大括号、中括号、逗号、冒号等,还有类似 true、false、null、数字这样的原子字符集。第一个阶段是没有分支处理的,这个阶段与词法分析非常类似。
|
||||
|
||||
**第二个阶段,**simdjson 也没有做分支处理,而是采用的堆栈结构,嵌套关系使用 goto 的方式进行导航。simdjson 通过索引可以处理所有输入的 JSON 内容而无需使用分支,这都归功于聪明的条件移动操作,使得遍历过程变得高效了很多。
|
||||
|
||||
通过 simdjson 解析 JSON 的两个阶段可以看出,simdjson的主要思路是尽可能地以最高效的方式将 JSON 这种可读性高的数据格式转换为计算机能更快理解的数据格式。
|
||||
|
||||
为了达到快速解析的目的, simdjson在第一个阶段一次性使用了 64字节输入进行大规模的数据操作,检查字符和字符类时以及当获得掩码应用变换时以64位进行位操作。这种方式,对于大的 JSON 数据解析性能提升是非常明显的。
|
||||
|
||||
如果你想更详细地了解这两个阶段的解析思路,可以查看这篇论文“[Parsing Gigabytes of JSON per Second](https://arxiv.org/abs/1902.08318)”。其实,simdjson 就是对这篇论文的实现,你可以在[GitHub](https://github.com/lemire/simdjson)上查看具体的实现代码。在我看来,一边看论文,一边看对应的代码实现,不失为一种高效的学习方式。
|
||||
|
||||
而如果你想要在工程中使用 simdjson的话,直接使用它提供的一个简单接口即可。具体的使用代码如下:
|
||||
|
||||
```
|
||||
#include "simdjson/jsonparser.h"
|
||||
|
||||
/...
|
||||
|
||||
const char * filename = ... // JSON 文件
|
||||
std::string_view p = get_corpus(filename);
|
||||
ParsedJson pj = build_parsed_json(p); // 解析方法
|
||||
// you no longer need p at this point, can do aligned_free((void*)p.data())
|
||||
if( ! pj.isValid() ) {
|
||||
// 出错处理
|
||||
}
|
||||
aligned_free((void*)p.data());
|
||||
|
||||
```
|
||||
|
||||
## 小结
|
||||
|
||||
在今天这篇文章中,我和你分享了什么是 JSON,JSON 的使用场景,以及simdjson 这个开源 JSON 解析库。simdjson 能够极大地提高 JSON 解析性能,你也可以非常方便地把它用到自己的工程中。
|
||||
|
||||
当需要对现有方案进行优化时,有的人会利用自己扎实的计算机基础知识找出更优秀的解决方案,而有的人只能等待更优秀的解决方案的出现。simdjson的作者明显就属于前者,而我们也要不断充实自己的基础知识,努力成为这其中的一员。
|
||||
|
||||
## 课后小作业
|
||||
|
||||
对于 JSON 的解析库,我今天只和你分析了系统自带的 JSONSerialization 和 simdjson。那么,我想请你说说你目前使用的 JSON 解析库是什么,以及它是如何解析 JSON 的,性能又如何呢?
|
||||
|
||||
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
<audio id="audio" title="27 | 如何用 Flexbox 思路开发?跟自动布局比,Flexbox 好在哪?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ca/66/ca20efda6f0c304c5b6e480cdf33a466.mp3"></audio>
|
||||
|
||||
你好,我是戴铭。今天,我要和你跟你聊聊 Flexbox。
|
||||
|
||||
你很有可能不知道Flexbox 是啥,但一定不会对 React Native、Weex 和 Texture(AsyncDisplayKit) 感到陌生,而Flexbox就是这些知名布局库采用的布局思路。不可小觑的是,苹果公司官方的UIStackView,也是采用Flexbox思路来实现布局的。
|
||||
|
||||
接下来,我们就一起来看看Flexbox布局思路有什么优势,以及如何用它来实现布局。
|
||||
|
||||
## Flexbox 好在哪?
|
||||
|
||||
目前来看,iOS 系统提供的布局方式有两种:
|
||||
|
||||
- 一种是 Frame 这种原始方式,也就是通过设置横纵坐标和宽高来确定布局。
|
||||
- 另一种是自动布局(Auto Layout),相比较于 Frame 需要指出每个视图的精确位置,自动布局对于视图位置的描述更加简洁和易读,只需要确定两个视图之间的关系就能够确定布局。
|
||||
|
||||
通过 [Masonry](https://github.com/SnapKit/Masonry)和 [SnapKit](https://github.com/SnapKit/SnapKit)这些第三方库,自动布局的易用性也有了很大提升。而且iOS 12 以后,苹果公司也已经解决了自动布局在性能方面的问题(这里,你可以再回顾下前面第4篇文章[《Auto Layout 是怎么进行自动布局的,性能如何?》](https://time.geekbang.org/column/article/85332)中的相关内容)。
|
||||
|
||||
那么在这种情况下,**我们为什么还要关注其他布局思路呢?**关于原因,我觉得主要包括以下两个方面。
|
||||
|
||||
其一,自动布局思路本身还可以再提高。Flexbox 比自动布局提供了更多、更规范的布局方法,布局方式考虑得更全面,使用起来也更加方便。同时,苹果公司基于 Flexbox 的布局思路,又在自动布局之上封装了一层 UIStackView。
|
||||
|
||||
其二,针对多个平台的库需要使用更加通用的布局思想。Flexbox 在2009年被 W3C 提出,可以很简单、完整地实现各种页面布局,而且还是响应式的,开始被应用于前端领域,目前所有浏览器都已支持。后来通过 React Native 和 Weex 等框架,它被带入到客户端开发中,同时支持了 iOS 和 Android。
|
||||
|
||||
与自动布局思路类似,Flexbox 使用的也是描述性的语言来布局。使用 Flexbox 布局的视图元素叫 Flex容器(flex container),其子视图元素叫作Flex项目(flex item)。Flexbox 布局的主要思想是,通过Flex容器设定的属性来改变内部Flex项目的宽、高,并调整 flex项目的位置来填充 flex容器的可用空间。
|
||||
|
||||
下图来自 W3C 官网,表示了 flex容器和 flex项目间的关系,其中 Main-Axis 表示横轴方向,Cross-Axis 表示纵轴方向。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c0/cd/c07d46e57f887adaac965c725aaf9ccd.png" alt=""><br>
|
||||
关于Flexbox 的详细入门资料,你可以参看阮一峰老师的“[Flex 布局教程:语法篇](http://www.ruanyifeng.com/blog/2015/07/flex-grammar.html)”一文。而Flexbox 在 W3C 上完整的定义,你可以点击[这个链接](https://www.w3.org/TR/css-flexbox-1/)查看。
|
||||
|
||||
如果你的工程目前还没有迁移到 React Native 或 Weex,那我觉得你可以通过 Texture 来使用 Flexbox 思路开发界面布局。而关于React Native和 Weex 使用 Flexbox 布局的思路,我会在专栏后面的文章“原生布局转到前端布局,开发思路的转变有哪些?”里和你详细说明。
|
||||
|
||||
## Texture 如何使用 Flexbox 思路进行布局?
|
||||
|
||||
基于Flexbox的布局思路,Texture框架的布局方案考虑得十分长远,也已经十分成熟,虽然学习起来需要费些力气,但是性能远好于苹果的自动布局,而且写起来更简单。
|
||||
|
||||
Texture框架的布局中,Texture考虑到布局扩展性,提供了一个基类 ASLayoutSpec。这个基类 提供了布局的基本能力,使 Texture 可以通过它扩展实现多种布局思路,比如 Wrapper、Inset、Overlay、Ratio、Relative、Absolute 等布局思路,也可以继承 ASLayoutSpec 来自定义你的布局算法。
|
||||
|
||||
ASLayoutSpec的子类,及其具体的功能如下:
|
||||
|
||||
```
|
||||
ASAbsoluteLayoutSpec // 绝对布局
|
||||
ASBackgroundLayoutSpec // 背景布局
|
||||
ASInsetLayoutSpec // 边距布局
|
||||
ASOverlayLayoutSpec // 覆盖布局
|
||||
ASRatioLayoutSpec // 比例布局
|
||||
ASRelativeLayoutSpec // 顶点布局
|
||||
ASCenterLayoutSpec // 居中布局
|
||||
ASStackLayoutSpec // 盒子布局
|
||||
ASWrapperLayoutSpec // 填充布局
|
||||
ASCornerLayoutSpec // 角标布局
|
||||
|
||||
```
|
||||
|
||||
ASLayoutSpec 子类实现了各种布局思路,ASLayoutSpec 会制定各种布局相通的协议方法,遵循这些协议后可以保证这些子类能够使用相同的规则去实现更丰富的布局。
|
||||
|
||||
通过 ASLayoutSpec 遵循的 ASLayoutElement 协议,可以知道 ASLayoutSpec 提供的基本能力有哪些。ASLayoutElement 协议定义如下:
|
||||
|
||||
```
|
||||
@protocol ASLayoutElement <ASLayoutElementExtensibility, ASTraitEnvironment, ASLayoutElementAsciiArtProtocol>
|
||||
|
||||
#pragma mark - Getter
|
||||
|
||||
@property (nonatomic, readonly) ASLayoutElementType layoutElementType;
|
||||
@property (nonatomic, readonly) ASLayoutElementStyle *style;
|
||||
- (nullable NSArray<id<ASLayoutElement>> *)sublayoutElements;
|
||||
|
||||
#pragma mark - Calculate layout
|
||||
|
||||
// 要求节点根据给定的大小范围返回布局
|
||||
- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize;
|
||||
// 在子 layoutElements 上调用它来计算它们在 calculateLayoutThatFits: 方法里实现的布局
|
||||
- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize;
|
||||
// 重写此方法以计算 layoutElement 的布局
|
||||
- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize;
|
||||
// 重写此方法允许你接收 layoutElement 的大小。使用这些值可以计算最终的约束大小。但这个方法要尽量少用
|
||||
- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize
|
||||
restrictedToSize:(ASLayoutElementSize)size
|
||||
relativeToParentSize:(CGSize)parentSize;
|
||||
|
||||
- (BOOL)implementsLayoutMethod;
|
||||
|
||||
@end
|
||||
|
||||
```
|
||||
|
||||
通过上面代码可以看出,协议定义了 layoutThatFits 和 calculateLayoutThatFits 等回调方法。其中,layoutThatFits 回调方法用来要求节点根据给定的大小范围返回布局,重写 calculateLayoutThatFits 方法用以计算 layoutElement 的布局。定义了统一的协议方法,能让 ASLayoutSpec 统一透出布局计算能力,统一规范的协议方法,也有利于布局算法的扩展。
|
||||
|
||||
**接下来,我们一起看看ASLayoutSpec的子类中,应用最广泛的ASStackLayoutSpec。**它和 iOS 中自带的 UIStackView 类似,布局思路参照了 Flexbox,比如 horizontalAlignment、alignItems、flexWrap 等属性很容易和 Flexbox 对应上。
|
||||
|
||||
下面示例是一段官方的 ASStackLayoutSpec 示例代码。ASStackLayoutSpec 布局思路和 Flexbox是一样的,所以我们通过示例可以了解,如何通过 Texture 使用 Flexbox 布局思路开发界面:
|
||||
|
||||
```
|
||||
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constraint
|
||||
{
|
||||
// 创建一个纵轴方向的 ASStackLayoutSpec 视图容器 vStack
|
||||
ASStackLayoutSpec *vStack = [[ASStackLayoutSpec alloc] init];
|
||||
// 设置两个子节点,第一个节点是标题,第二个正文内容
|
||||
[vStack setChildren:@[titleNode, bodyNode];
|
||||
|
||||
// 创建一个横轴方向的 ASStackLayoutSpec 视图容器 hstack
|
||||
ASStackLayoutSpec *hstack = [[ASStackLayoutSpec alloc] init];
|
||||
hStack.direction = ASStackLayoutDirectionHorizontal;
|
||||
hStack.spacing = 5.0; // 设置节点间距为5
|
||||
|
||||
// 在 hStack 里添加 imageNode 和 vStack 节点
|
||||
[hStack setChildren:@[imageNode, vStack]];
|
||||
|
||||
// 创建一个 ASInsetLayoutSpec 容器,设置四周边距为5,将 hStack 作为其子节点
|
||||
ASInsetLayoutSpec *insetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(5,5,5,5) child:hStack];
|
||||
|
||||
return insetSpec;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面这段代码,会先创建一个纵轴方向的 ASStackLayoutSpec 视图容器 vStack;然后,为 vStack 设置两个子节点,第一个节点是标题,第二个节点是正文内容;接下来,创建一个横轴方向的 ASStackLayoutSpec 视图容器 hstack,在 hStack 里添加 imageNode 和 vStack 节点;最后,创建一个 ASInsetLayoutSpec 容器,设置四周边距为5,将 hStack 作为其子节点。
|
||||
|
||||
上面示例代码对应的视图效果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3e/fe/3ed4643a658f3a358d35dd9a151c7cfe.png" alt=""><br>
|
||||
除了 Texture 用到了 Flexbox 的布局思路,React Native 和 Weex 也用到了这个布局思路。这两个框架对 Flexbox 算法的实现,是一个叫作[Yoga](https://github.com/facebook/yoga) 的 C++ 库。
|
||||
|
||||
除了 React Native 和 Weex 之外,Yoga 还为很多其他开源框架提供支持,比如 [Litho](https://fblitho.com/)、[ComponentKit](https://componentkit.org/) 等。
|
||||
|
||||
为了能够用于各个平台,Yoga是由 C/C++ 语言编写的,依赖少,编译后的二进制文件也小,可以被方便地集成到 Android 和 iOS 上。
|
||||
|
||||
随着新硬件的不断推出,比如手表和折叠屏手机,你可能还需要掌握更多的布局算法,以不变应万变。比如说,除了 Flexbox 思路的布局 ASStackLayoutSpec以外,Texture中还有 Wrapper、Inset、Overlay、Ratio、Relative、Absolute 等针对不同场景的布局思路,同时还支持自定义布局算法。
|
||||
|
||||
那么,接下来我就跟你聊聊 Flexbox 的算法是怎样的。了解Flexbox的布局算法设计,一方面能够让你更好地理解 Flexbox 布局;另一方面,你也可以借此完整地了解一个布局算法是怎样设计的,使得你以后也能够设计出适合自己业务场景的布局算法。
|
||||
|
||||
## Flexbox 算法
|
||||
|
||||
Flexbox 算法的主要思想是,让 flex容器能够改变其flex项目的宽高和顺序,以填充可用空间,flex容器可以通过扩大项目来填充可用空间,或者缩小项目以防止其超出其可用空间。
|
||||
|
||||
**首先**,创建一组匿名的 flex 项目,按照这组匿名 flex项目设置的排列规则对其进行排列。
|
||||
|
||||
- 第一步,确定 flex项目的 main space 和 cross space,如果 flex容器定义了大小就直接使用定义的大小;否则, 从 flex容器的可用空间里减去 margin、border、padding。
|
||||
- 第二步,确定每个项目 的 flex base 大小和假设的大小,其中假设的大小是项目依据它最小和最大的大小属性来确定的。flex 容器的大小,由它的大小属性来确定。
|
||||
|
||||
这个计算过程中,flex容器的最小内容大小,是由它所有项目的最小内容大小之和算出的;而flex容器的最大内容大小,则是由它所有项目的最大内容大小之和确定出。
|
||||
|
||||
**接着**,将 flex项目收集到 flex lines 中。如果 flex容器是单行,那么就把所有的 flex项目都收集到单个 flex line 里。否则,就从第一个未收集的项目开始尽可能多地收集 flex项目到 flex line 里,根据 flex容器的 inner 大小判断是否当前 flex line 收集满。重复操作,直到将所有 flex项目都被收集到了 flex lines 里。
|
||||
|
||||
处理完 flex lines 后,需要通过使用过的大小和可用大小来确定每个项目的 cross 大小,然后计算每个 flex line 的 cross 大小以及 flex line 里每个 flex项目的 cross 大小。
|
||||
|
||||
**最后,**进行 Main-Axis 对齐和 Cross-Axis 对齐。
|
||||
|
||||
- Main-Axis 对齐就是分配剩余空间。对于每个 flex line,如果有剩余空间, margin 设置为 auto 的话,就平均分配剩余空间。
|
||||
- Cross-Axis 对齐,先要解决自动 margin,然后沿 cross-axis 对齐所有 flex items;随后确定 flex container 使用的 cross 大小;最后对齐所有 flex lines。
|
||||
|
||||
结合视图的概念,简化后 Flexbox 布局算法如下图:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/80/b7/80350e0a3fb5f6ead639754a808b3cb7.jpeg" alt=""><br>
|
||||
如图中所示,其中 View 类似 flex container,View 的 Subviews 类似 flex items,flexbox 的算法简而言之就是:首先依据 View 的 margin、padding、border 确定出横纵大小,接下来确定排列,根据 View 的大小确定 Subviews 的行内容,确定出行中每个 Subview 的大小,最终确定出 Subview 的位置。
|
||||
|
||||
## 小结
|
||||
|
||||
在今天这篇文中,我与你介绍了 Flexbox 比 iOS 系统自带的自动布局好在哪,还举例说明了 Texture 是如何利用 Flexbox 进行 iOS 开发的。
|
||||
|
||||
其实, iOS 系统自带的 UIStackView 也是依据 Flexbox 思路开发的。我们都知道,苹果公司一般不轻易使用第三方技术。这,也就表明了 Flexbox 的布局思路是非常优秀的。
|
||||
|
||||
所以,在最后我还跟你分享了 Flexbox 的布局算法。如果你想知道这个算法的具体实现,可以直接查看 [Yoga 的代码](https://github.com/facebook/yoga)。
|
||||
|
||||
我以前也做过一个将 HTML 代码转换成 Texture 代码的项目 [HTN](https://github.com/ming1016/HTN/),HTML 使用 Flexbox 写的界面布局可以直接转成对应的 Texture 代码,使用示例代码如下:
|
||||
|
||||
```
|
||||
public func htmlToTexture() {
|
||||
// inputLb.stringValue 是 html 代码
|
||||
let treeBuilder = HTMLTreeBuilder(inputLb.stringValue)
|
||||
_ = treeBuilder.parse()
|
||||
// 解析 CSS
|
||||
let cssStyle = CSSParser(treeBuilder.doc.allStyle()).parseSheet()
|
||||
// 生成 DOM 树
|
||||
let document = StyleResolver().resolver(treeBuilder.doc, styleSheet: cssStyle)
|
||||
document.des() //打印包含样式信息的 DOM 树
|
||||
|
||||
//转 Textrue
|
||||
let layoutElement = LayoutElement().createRenderer(doc: document)
|
||||
_ = HTMLToTexture(nodeName:"Flexbox").converter(layoutElement);
|
||||
nativeCodeLb.string = ""
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 课后小作业
|
||||
|
||||
如果你还没有开始使用 Flexbox ,请你立刻集成 Yoga 对你业务中一个页面使用 Flexbox 布局重写一遍吧。如果你不想集成第三方库,使用 UIStackView 也行。
|
||||
|
||||
今天的作业是基于 ASLayoutElement 协议,实现一个 Texture 自定义布局类。这个布局算法可以很简单,主要是想要帮你验证下你学习 Flexbox 算法的成果。
|
||||
|
||||
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
210
极客时间专栏/geek/iOS开发高手课/应用开发篇/28 | 怎么应对各种富文本表现需求?.md
Normal file
210
极客时间专栏/geek/iOS开发高手课/应用开发篇/28 | 怎么应对各种富文本表现需求?.md
Normal file
@@ -0,0 +1,210 @@
|
||||
<audio id="audio" title="28 | 怎么应对各种富文本表现需求?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a0/f8/a0c0c6358e74fef00551e9f80eb917f8.mp3"></audio>
|
||||
|
||||
你好,我是戴铭。今天,我要和你分享的主题是,在iOS开发中,如何展示富文本的内容。
|
||||
|
||||
在iOS开发中,富文本的展示是一个非常常见的需求。为了帮助你更好地了解如何展示富文本,我在今天这篇文章中,会结合一个项目来跟你说说面对富文本展示需求时,要怎么考虑和实现。这样,你在自己的项目中,也可以借鉴今天这样的实现思路和方法。
|
||||
|
||||
简单来说,富文本就是一段有属性的字符串,可以包含不同字体、不同字号、不同背景、不同颜色、不同字间距的文字,还可以设置段落、图文混排等等属性。
|
||||
|
||||
我以前做过一个 [RSS 阅读器](https://github.com/ming1016/GCDFetchFeed),阅读器启动后,需要抓取最新的 RSS 内容进行展示。RSS 里面的文章内容属于富文本,是用HTML标签来描述的,包含了文字样式、链接和图片。
|
||||
|
||||
比如,RSS阅读器中的某篇文章内容如下:
|
||||
|
||||
```
|
||||
<item>
|
||||
<title>涉国资流失嫌疑 东方广益6亿元入股锤子科技被调查</title>
|
||||
<link>https://www.cnbeta.com/articles/tech/841851.htm</link>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<p><strong>据虎嗅得到的独家消息,成都成华区监察委已立案调查“东方广益6亿元入股锤子科技(北京)股份有限公司”事宜,认为这个项目有国有资产流失嫌疑。</strong>成华区监察委员会成立于2018年2月,按照管理权限对全区行使公权力的公职人员依法实行监察,履行监督、调查和处置职责。</p> <a href="https://www.cnbeta.com/articles/tech/841851.htm" target="_blank"><strong>阅读全文</strong></a>
|
||||
]]>
|
||||
</description>
|
||||
<author>ugmbbc</author>
|
||||
<source>cnBeta.COM</source>
|
||||
<pubDate>Sat, 27 Apr 2019 09:46:45 GMT</pubDate>
|
||||
<guid>https://www.cnbeta.com/articles/tech/841851.htm</guid>
|
||||
</item>
|
||||
|
||||
```
|
||||
|
||||
文章的 HTML 代码就在上面 RSS 中的 description 标签里。解析出 RSS 中所有文章的 HTML 代码,并将它们保存到本地数据库中。
|
||||
|
||||
接下来,如何展示 HTML 内容呢?当时,我的第一反应就是使用 WebView 控件来展示。
|
||||
|
||||
## WebView
|
||||
|
||||
使用 WebView 显示文章只需要创建一个 UIWebView 对象,进行一些基本滚动相关的设置,然后读取 HTML 字符串就可以了,具体实现代码如下:
|
||||
|
||||
```
|
||||
self.wbView = [[UIWebView alloc] init];
|
||||
self.wbView.delegate = self;
|
||||
[self.view addSubview:self.wbView];
|
||||
[self.wbView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||
make.top.left.right.bottom.equalTo(self.view);
|
||||
}];
|
||||
self.wbView.scalesPageToFit = YES; // 确保网页的显示尺寸和屏幕大小相同
|
||||
self.wbView.scrollView.directionalLockEnabled = YES; // 只在一个方向滚动
|
||||
self.wbView.scrollView.showsHorizontalScrollIndicator = NO; // 不显示左右滑动
|
||||
[self.wbView setOpaque:NO]; // 默认是透明的
|
||||
|
||||
// 读取文章 html 字符串进行展示
|
||||
[self.wbView loadHTMLString:articleString baseURL:nil];
|
||||
|
||||
```
|
||||
|
||||
和 UIWebView 的 loadRequest 相比,UIWebView 通过 loadHTMLString 直接读取 HTML 代码,省去了网络请求的时间,展示的速度非常快。不过,HTML 里的图片资源还是需要通过网络请求来获取。所以,如果能够在文章展示之前就缓存下图片,那么无需等待,就能够快速完整地展示丰富的文章内容了。
|
||||
|
||||
那么,我应该使用什么方案来缓存文章中的图片呢?
|
||||
|
||||
在 Cocoa 层使用 NSURLProtocol 可以拦截所有 HTTP 的请求,因此我可以利用 NSURLProtocol 来缓存文章中的图片。
|
||||
|
||||
接下来,我再来和你说说,**如何用我写的一个 Web 页面预加载库** [**STMURLCache**](https://github.com/ming1016/GCDFetchFeed/blob/master/GCDFetchFeed/GCDFetchFeed/STMURLCache.m)**来预缓存 HTML 里的图片。**这个库你也可以应用到自己项目中。
|
||||
|
||||
**首先**,我需要从数据库中取出所有未缓存图片的文章内容 HTML。实现代码如下:
|
||||
|
||||
```
|
||||
[[[[[SMDB shareInstance] selectAllUnCachedFeedItems] subscribeOn:[RACScheduler schedulerWithPriority:RACSchedulerPriorityDefault]] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSMutableArray *x) {
|
||||
// 在数据库中获取所有未缓存的文章数据 x
|
||||
NSMutableArray *urls = [NSMutableArray array];
|
||||
if (x.count > 0) {
|
||||
self.needCacheCount = x.count;
|
||||
for (SMFeedItemModel *aModel in x) {
|
||||
// 将文章数据中的正文内容都存在 urls 数组中
|
||||
[urls addObject:aModel.des];
|
||||
}
|
||||
}
|
||||
...
|
||||
|
||||
}];
|
||||
|
||||
```
|
||||
|
||||
如上面代码所示,在数据库中获取到所有未缓存文章的数据后,遍历所有数据,提取文章数据中的正文 HTML 内容保存到一个新的数组 urls 中。
|
||||
|
||||
**然后**,使用 STMURLCache 开始依次预下载文章中的图片进行缓存。实现代码如下:
|
||||
|
||||
```
|
||||
[[STMURLCache create:^(STMURLCacheMk *mk) {
|
||||
mk.whiteUserAgent(@"gcdfetchfeed").diskCapacity(1000 * 1024 * 1024);
|
||||
}] preloadByWebViewWithHtmls:[NSArray arrayWithArray:urls]].delegate = self;
|
||||
|
||||
```
|
||||
|
||||
STMURLCache 使用 preloadByWebViewWithHtmls 方法去预缓存所有图片,在 STMURLCache 初始化时,会设置 UserAgent 白名单,目的是避免额外缓存了其他不相关 UIWebView 的图片。
|
||||
|
||||
缓存图片的核心技术还是 NSURLProtocol,STMURLCache 最终也是使用 NSURLProtocol 来缓存图片的。NSURLProtocol 是一个抽象类,专门用来处理特定协议的 URL 数据加载。你可以使用自定义 URL 处理的方式,来重新定义系统 URL 加载。STMURLCache 缓存图片的具体实现代码,你可以在 [STMURLProtocol](https://github.com/ming1016/GCDFetchFeed/blob/master/GCDFetchFeed/GCDFetchFeed/STMURLProtocol.m)这个类里查看。
|
||||
|
||||
STMURLProtocol 会在所有网络请求的入口 canInitWithRequest 方法中加上过滤条件,比如 STMURLCache 在初始化时设置 UserAgent 白名单,过滤代码如下:
|
||||
|
||||
```
|
||||
// User-Agent来过滤
|
||||
if (sModel.whiteUserAgent.length > 0) {
|
||||
// 在 HTTP header 里取出 User Agent
|
||||
NSString *uAgent = [request.allHTTPHeaderFields objectForKey:@"User-Agent"];
|
||||
if (uAgent) {
|
||||
// 不在白名单中返回 NO,不会进行缓存
|
||||
if (![uAgent hasSuffix:sModel.whiteUserAgent]) {
|
||||
return NO;
|
||||
}
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
UserAgent 白名单过滤会通过 request 的 allHTTPHeaderFields 获取到当前网络请求的 UserAgent,然后和已经设置的 UserAgent 白名单做比较:如果在白名单中就进行缓存;否则,就不会缓存。
|
||||
|
||||
STMURLProtocol 还可以根据域名进行过滤,这样可以灵活、精确地控制缓存范围。如果你设置了域名白名单,那么只有在白名单里的域名下的网络请求才会执行缓存,过滤代码如下:
|
||||
|
||||
```
|
||||
//对于域名白名单的过滤
|
||||
if (sModel.whiteListsHost.count > 0) {
|
||||
id isExist = [sModel.whiteListsHost objectForKey:request.URL.host];
|
||||
// 如果当前请求的域名不在白名单中也会返回 NO
|
||||
if (!isExist) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
如代码所示,当前网络请求的域名可以通过 request.URL.host 属性获取到,获取到网络请求的域名后,再去看域名白名单里是否有,如果有就缓存,没有就返回 NO,不进行缓存操作。
|
||||
|
||||
在 canInitWithRequest 方法中满足缓存条件后,开始缓存的方法是 startLoading。startLoading 方法会判断已缓存和未缓存的情况,如果没有缓存会发起网络请求,将请求到的数据保存在本地。如果有缓存,则会直接从本地读取缓存,实现代码如下:
|
||||
|
||||
```
|
||||
// 从缓存里读取数据
|
||||
NSData *data = [NSData dataWithContentsOfFile:self.filePath];
|
||||
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:self.request.URL MIMEType:[otherInfo objectForKey:@"MIMEType"] expectedContentLength:data.length textEncodingName:[otherInfo objectForKey:@"textEncodingName"]];
|
||||
|
||||
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
|
||||
|
||||
// 使用 NSURLProtocolClient 的 URLProtocol:didLoadData 方法加载本地数据
|
||||
[self.client URLProtocol:self didLoadData:data];
|
||||
[self.client URLProtocolDidFinishLoading:self];
|
||||
|
||||
```
|
||||
|
||||
如代码所示,STMURLProtocol 先通过缓存的路径获取到缓存的数据,再使用 NSURLProtocolClient 的 URLProtocol:didLoadData 方法加载本地缓存数据,以减少网络请求。
|
||||
|
||||
显示文章内容时使用 NSURLProtocol,对于那些已经缓存过图片的文章就不用发起图片的网络请求,显示的速度跟本地加载显示速度一样快。
|
||||
|
||||
虽然通过 URLProtocol 重新定义系统 URL 加载的方式,来直接读取预缓存提升了加载速度,但在长列表的 Cell 上展示富文本,就需要性能更高、内存占用更小的方法。那么接下来,我们再看看除了 UIWebView 还有没有什么方法可以展示富文本呢?
|
||||
|
||||
当然还有了。
|
||||
|
||||
在长列表这种场景下,如果不用 HTML 来描述富文本的话,想要使用原生 iOS 代码来描述富文本的话,你还可以使用苹果官方的[TextKit](https://developer.apple.com/documentation/appkit/textkit)和 [YYText](https://github.com/ibireme/YYText)来展示。
|
||||
|
||||
其中,YYText 不仅兼容 UILabel 和 UITextView,在异步文字布局和渲染上的性能也非常好。所以接下来,我们就一起看看 YYText是如何展示富文本的吧。
|
||||
|
||||
## YYText
|
||||
|
||||
集成 YYText 到你的App非常简单,只需要在 Podfile 中添加 pod ‘YYText’ 就可以了。下面代码展示了如何展示图文混排的富文本:
|
||||
|
||||
```
|
||||
NSMutableAttributedString *text = [NSMutableAttributedString new];
|
||||
UIFont *font = [UIFont systemFontOfSize:16];
|
||||
NSMutableAttributedString *attachment = nil;
|
||||
|
||||
// 嵌入 UIImage
|
||||
UIImage *image = [UIImage imageNamed:@"dribbble64_imageio"];
|
||||
attachment = [NSMutableAttributedString yy_attachmentStringWithContent:image contentMode:UIViewContentModeCenter attachmentSize:image.size alignToFont:font alignment:YYTextVerticalAlignmentCenter];
|
||||
[text appendAttributedString: attachment];
|
||||
|
||||
// 嵌入 UIView
|
||||
UISwitch *switcher = [UISwitch new];
|
||||
[switcher sizeToFit];
|
||||
attachment = [NSMutableAttributedString yy_attachmentStringWithContent:switcher contentMode:UIViewContentModeBottom attachmentSize:switcher.size alignToFont:font alignment:YYTextVerticalAlignmentCenter];
|
||||
[text appendAttributedString: attachment];
|
||||
|
||||
// 嵌入 CALayer
|
||||
CASharpLayer *layer = [CASharpLayer layer];
|
||||
layer.path = ...
|
||||
attachment = [NSMutableAttributedString yy_attachmentStringWithContent:layer contentMode:UIViewContentModeBottom attachmentSize:switcher.size alignToFont:font alignment:YYTextVerticalAlignmentCenter];
|
||||
[text appendAttributedString: attachment];
|
||||
|
||||
|
||||
```
|
||||
|
||||
如代码所示,YYText 对于富文本的图文混排使用的是自定义的 NSMutableAttributedString 分类,自定义分类不光简化了 NSMutableAttributedString,还增加了功能,除了图片外,可以嵌入 UIView 和 CALayer。
|
||||
|
||||
通过上面 YYText 描述富文本的代码,你会发现原生代码描述富文本跟 HTML 比,既复杂又啰嗦。HTML 代码更易读、更容易维护,所以除了长列表外,我建议你都使用 HTML 来描述富文本。
|
||||
|
||||
对于 UIWebView 内存占用高的问题,你可以考虑使用 HTML 代码转原生代码的思路解决。比如,你可以参考我以前做的将 HTML 代码转原生代码的示例项目 [HTN](https://github.com/ming1016/HTN)里的解决思路。
|
||||
|
||||
## 小结
|
||||
|
||||
今天我跟你介绍了如何通过 UIWebView 和 YYText 来展示富文本。
|
||||
|
||||
UIWebView 展示的是使用 HTML 描述的富文本。HTML 是描述富文本最简单和最常用的方式,相对于 YYText 或 TextKit 那样描述富文本的方式来说,更加简洁和标准。不过,UIWebView 的缺点也比较明显,同时创建多个 UIWebView 实例,对于内存的占用会非常大。
|
||||
|
||||
所以,我对于富文本展示的建议是,如果是列表展示富文本建议使用 TextKit 或者 YYText,其他情况可以选择使用 UIWebView 来展示富文本。
|
||||
|
||||
## 课后作业
|
||||
|
||||
使用 [STMURLCache](https://github.com/ming1016/GCDFetchFeed/blob/master/GCDFetchFeed/GCDFetchFeed/STMURLCache.m)预加载你工程中的一个 Web 页面,看看打开速度提升了多少,预加载成功后,在弱网环境和无网络的环境都可以试试。
|
||||
|
||||
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
111
极客时间专栏/geek/iOS开发高手课/应用开发篇/29 | 如何在 iOS 中进行面向测试驱动开发和面向行为驱动开发?.md
Normal file
111
极客时间专栏/geek/iOS开发高手课/应用开发篇/29 | 如何在 iOS 中进行面向测试驱动开发和面向行为驱动开发?.md
Normal file
@@ -0,0 +1,111 @@
|
||||
<audio id="audio" title="29 | 如何在 iOS 中进行面向测试驱动开发和面向行为驱动开发?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/67/81/67697df5a91793c5a098f224837e0c81.mp3"></audio>
|
||||
|
||||
你好,我是戴铭。今天,我要和你分享的话题是,如何在 iOS 中进行面向测试驱动开发和面向行为驱动开发。
|
||||
|
||||
每当你编写完代码后,都会编译看看运行结果是否符合预期。如果这段代码的影响范围小,你很容易就能看出结果是否符合预期,而如果验证的结果是不符合预期,那么你就会检查刚才编写的代码是否有问题。
|
||||
|
||||
但是,如果这段代码的影响范围比较大,这时需要检查的地方就会非常多,相应地,人工检查的时间成本也会非常大。特别是团队成员多、工程代码量大时,判断这段代码的影响面都需要耗费很多时间。那么,每次编写完代码,先判断它的影响面,然后再手动编译进行检查的开发方式,效率就非常低了,会浪费大量时间。
|
||||
|
||||
虽说一般公司都会有专门的测试团队对产品进行大量测试,但是如果不能在开发阶段及时发现问题,当各团队代码集成到一起,把所有问题都堆积到测试阶段去发现、解决,就会浪费大量的沟通时间,不光是开发同学和测试同学之间的沟通时间,还有开发团队之间的沟通时间也会呈指数级增加。
|
||||
|
||||
那么,有没有什么好的开发方式,能够提高在编写代码后及时检验结果的效率呢?
|
||||
|
||||
所谓好的开发方式,就是开发、测试同步进行,尽早发现问题。从测试范围和开发模式的角度,我们还可以把这种开发模式细分出更多类型。
|
||||
|
||||
**从测试范围上来划分的话**,软件测试可以分为单元测试、集成测试、系统测试。测试团队负责的是集成测试以及系统测试,而单元测试则是有开发者负责的。对于开发者来说,通过单元测试就可以有效提高编写代码后快速发现问题的效率。
|
||||
|
||||
概括来说,单元测试,也叫作模块测试,就是对单一的功能代码进行测试。这个功能代码,可能是一个类的方法,也可能是一个模块的某个函数。
|
||||
|
||||
单元测试会使用 Mock 方式模拟外部使用,通过编写的各种测试用例去检验代码的功能是否正常。一个系统都是由各个功能组合而成,功能模块划分得越小,功能职责就越清晰。清晰的功能职责可以确保单个功能的测试不会出现问题,是单元测试的基础。
|
||||
|
||||
**从开发模式划分的话,**开发方式可以分为 TDD(Test-driven development,面向测试驱动开发)和 BDD(Behavior-driven development ,面向行为驱动开发)。
|
||||
|
||||
- TDD 的开发思路是,先编写测试用例,然后在不考虑代码优化的情况下快速编写功能实现代码,等功能开发完成后,在测试用例的保障下,再进行代码重构,以提高代码质量。
|
||||
- BDD 是 TDD 的进化,基于行为进行功能测试,使用 DSL(Domain Specific Language,领域特定语言)来描述测试用例,让测试用例看起来和文档一样,更易读、更好维护。
|
||||
|
||||
TDD 编写的测试用例主要针对的是开发中最小单元进行测试,适合单元测试。而 BDD 的测试用例是对行为的描述,测试范围可以更大一些,在集成测试和系统测试时都可以使用。同时,不仅开发者可以使用BDD的测试用例高效地发现问题,测试团队也能够很容易参与编写。这,都得益于 BDD 可以使用易于编写行为功能测试的 DSL 语言。
|
||||
|
||||
接下来,我就和你详细聊聊 TDD 和 BDD。
|
||||
|
||||
## TDD
|
||||
|
||||
我刚刚也已经提到了,TDD在确定功能需求后,首先就会开始编写测试用例,用来检验每次的代码更新,能够让我们更快地发现问题,并能保正不会漏掉问题。其实,这就是通过测试用例来推动开发。
|
||||
|
||||
在思想上,和拿到功能需求后直接开发功能的区别是,TDD会先考虑如何对功能进行测试,然后再去考虑如何编写代码,这就给优化代码提供了更多的时间和空间,即使几个版本过后再来优化,只要能够通过先前写好的测试用例,就能够保证代码质量。
|
||||
|
||||
所以说,TDD 非常适合快速迭代的节奏,先尽快实现功能,然后再进行重构和优化。如果我们不使用 TDD 来进行快速迭代开发,虽然在最开始的时候开发效率会比 TDD 高,但是过几个版本再进行功能更新时,就需要在功能验证上花费大量的时间,反而得不偿失。
|
||||
|
||||
其实,TDD 这种开发模式和画漫画的工作方式非常类似:草稿就类似 TDD 中的测试用例,漫画家先画草稿,细节由漫画家和助手一起完成,无论助手怎么换,有了草稿的保障,内容都不会有偏差。分镜的草稿没有细节,人物眼睛、鼻子都可能没有,场景也只需要几条透视线就可以。虽然没有细节,但是草稿基本就确定了漫画完成后要表达的所有内容。
|
||||
|
||||
## BDD
|
||||
|
||||
相比 TDD,BDD更关注的是行为方式的设计,通过对行为的描述来验证功能的可用性。行为描述使用的 DSL,规范、标准而且可读性高,可以当作文档来使用。
|
||||
|
||||
BDD 的 Objective-C 框架有 [Kiwi](https://github.com/kiwi-bdd/Kiwi)、[Specta](https://github.com/specta/specta)、[Expecta](https://github.com/specta/expecta)等,Swift 框架有 [Quick](https://github.com/Quick/Quick)。
|
||||
|
||||
Kiwi框架不光有 Specta 的 DSL 模式,Expecta框架的期望语法,还有 Mocks 和 Stubs 这样的模拟存根能力。所以接下来,我就跟你说说这个iOS中非常有名并且好用的BDD框架,以及怎么用它来进行 BDD 开发。
|
||||
|
||||
## Kiwi
|
||||
|
||||
将Kiwi集成到你的App里,只需要在 Podfile 里添加 pod ‘Kiwi’ 即可。下面这段代码,是 Kiwi 的使用示例:
|
||||
|
||||
```
|
||||
// describe 表示要测试的对象
|
||||
describe(@"RSSListViewController", ^{
|
||||
// context 表示的是不同场景下的行为
|
||||
context(@"when get RSS data", ^{
|
||||
// 同一个 context 下每个 it 调用之前会调用一次 beforeEach
|
||||
beforeEach(^{
|
||||
id dataStore = [DataStore new];
|
||||
});
|
||||
|
||||
|
||||
// it 表示测试内容,一个 context 可以有多个 it
|
||||
it(@"load data", ^{
|
||||
// Kiwi 使用链式调用,should 表示一个期待,用来验证对象行为是否满足期望
|
||||
[[theValue(dataStore.count) shouldNot] beNil];
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
上面这代码描述的是在 RSS 列表页面,当获取 RSS 数据时去读取数据这个行为的测试用例。这段测试用例代码,包含了 Kiwi 的基本元素,也就是describe、context、it。这些元素间的关系可以表述为:
|
||||
|
||||
- describe 表示要测试的对象,context 表示的是不同场景下的行为,一个 describe 里可以包含多个 context。
|
||||
- it表示的是需要测试的内容,同一个场景下的行为会有多个需要测试的内容,也就是说一个 context 下可以有多个 it。
|
||||
|
||||
测试内容使用的是 Kiwi 的 DSL 语法,采用的是链式调用。上面示例代码中 shouldNot 是期望语法,期望是用来验证对象行为是否满足期望。
|
||||
|
||||
期望语法可以是期望数值和数字,也可以是期望字符串的匹配,比如:
|
||||
|
||||
```
|
||||
[[string should] containString:@"rss"];
|
||||
|
||||
```
|
||||
|
||||
should containString 语法表示的是,期望 string 包含了 rss 字符串。Kiwi 里的期望语法非常丰富,还有正则表达式匹配、数量变化、对象测试、集合、交互和消息、通知、异步调用、异常等。完整的期望语法描述,你可以查看Wiki的 [Expectations 部分](https://github.com/allending/Kiwi/wiki/Expectations)。
|
||||
|
||||
除了期望语法外,Kiwi 还支持模拟对象和存根语法。
|
||||
|
||||
模拟对象能够降低对象之间的依赖,可以模拟难以出现的情况。模拟对象包含了模拟 Null 对象、模拟类的实例、模拟协议的实例等。存根可以返回指定选择器或消息模式的请求,可以存根对象和模拟对象。
|
||||
|
||||
模拟对象和存根的详细语法定义,你可以查看Wiki 的 [Mocks and Stubs 部分](https://github.com/allending/Kiwi/wiki/Mocks-and-Stubs)。
|
||||
|
||||
## 小结
|
||||
|
||||
按照 TDD 和 BDD 方式开发,有助于更好地进行模块化设计,划清模块边界,让代码更容易维护。TDD 在测试用例的保障下更容易进行代码重构优化,减少 debug 时间。而使用 BDD 编写的测试用例,则更是好的文档,可读性非常强。通过这些测试用例,在修改代码时,我们能够更方便地了解开发 App 的工作状态。同时,修改完代码后还能够快速全面地测试验证问题。
|
||||
|
||||
无论是 TDD 还是 BDD,开发中对于每个实现的方法都要编写测试用例,而且要注意先编写测试用例代码,再编写方法实现代码。测试用例需要考虑到各种异常条件,以及输入输出的边界。编写完测试用例还需要检查如果输入为错时,测试用例是否会显示为错。
|
||||
|
||||
最后需要强调一点,好的模块化架构和 TDD 、BDD 是相辅相成的。TDD 和 BDD 开发方式会让你的代码更加模块化,而模块化的架构更容易使用 TDD 和 BDD 的方式进行开发。
|
||||
|
||||
在团队中推行 TDD 和 BDD 的最大困难,就是业务迭代太快时,没有时间去写测试用例。我的建议是,优先对基础能力的功能开发使用 TDD 和 BDD,保证了基础能力的稳定,业务怎么变,底子还都是稳固的;当有了业务迭代、有了间隙时,再考虑在核心业务上采用 BDD,最大程度的保证 App 核心功能的稳定。
|
||||
|
||||
## 课后作业
|
||||
|
||||
今天我跟你聊了很多 TDD 和 BDD 的优点,但是很多团队并没有使用这样的开发方式,你觉得这其中的原因是什么呢?
|
||||
|
||||
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
191
极客时间专栏/geek/iOS开发高手课/应用开发篇/30 | 如何制定一套适合自己团队的 iOS 编码规范?.md
Normal file
191
极客时间专栏/geek/iOS开发高手课/应用开发篇/30 | 如何制定一套适合自己团队的 iOS 编码规范?.md
Normal file
@@ -0,0 +1,191 @@
|
||||
<audio id="audio" title="30 | 如何制定一套适合自己团队的 iOS 编码规范?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/23/20/2355b2f94e38bc3487fca4e702671820.mp3"></audio>
|
||||
|
||||
你好,我是戴铭。
|
||||
|
||||
如果团队成员的编码规范各不相同,那么你在接收其他人的代码时是不是总会因为无法认同他的代码风格,而想着去重写呢。但是,重写这个事儿不只会增加梳理逻辑和开发成本,而且重写后出现问题的风险也会相应增加。那么,这个问题应该如何解决呢?
|
||||
|
||||
在我看来,如果出现这种情况,你的团队急需制定出一套适合自己团队的编码规范。有了统一的编码规范,就能有效避免团队成员由于代码风格不一致而导致的相互认同感缺失问题。
|
||||
|
||||
那么,如何制定编码规范呢?在接下来的内容里,我会先跟你说说,我认为的好的编码规范。你在制定编码规范时,也可以按照这个思路去细化出更多、更适合自己的规范,从而制定出团队的编码规范。然后,我会再和你聊聊如何通过 Code Review 的方式将你制定的编码规范进行落地。
|
||||
|
||||
## 好的代码规范
|
||||
|
||||
关于好的代码规范,接下来我会从常量、变量、属性、条件语句、循环语句、函数、类,以及分类这8个方面和你一一说明。
|
||||
|
||||
### 常量
|
||||
|
||||
在常量的使用上,我建议你要尽量使用类型常量,而不是使用宏定义。比如,你要定义一个字符串常量,可以写成:
|
||||
|
||||
```
|
||||
static NSString * const STMProjectName = @"GCDFetchFeed"
|
||||
|
||||
```
|
||||
|
||||
### 变量
|
||||
|
||||
对于变量来说,我认为好的编码习惯是:
|
||||
|
||||
<li>
|
||||
变量名应该可以明确体现出功能,最好再加上类型做后缀。这样也就明确了每个变量都是做什么的,而不是把一个变量当作不同的值用在不同的地方。
|
||||
</li>
|
||||
<li>
|
||||
在使用之前,需要先对变量做初始化,并且初始化的地方离使用它的地方越近越好。
|
||||
</li>
|
||||
<li>
|
||||
不要滥用全局变量,尽量少用它来传递值,通过参数传值可以减少功能模块间的耦合。
|
||||
</li>
|
||||
|
||||
比如,下面这段代码中,当名字为字符串时,就可以把字符串类型作为后缀加到变量名后面。
|
||||
|
||||
```
|
||||
let nameString = "Tom"
|
||||
print("\(nameString)")
|
||||
|
||||
nameLabel.text = nameString
|
||||
|
||||
```
|
||||
|
||||
### 属性
|
||||
|
||||
在iOS开发中,关于属性的编码规范,需要针对开发语言做区分:
|
||||
|
||||
<li>
|
||||
Objective-C 里的属性,要尽量通过 get 方法来进行懒加载,以避免无用的内存占用和多余的计算。
|
||||
</li>
|
||||
<li>
|
||||
Swift 的计算属性如果是只读,可以省掉 get 子句。示例代码如下:
|
||||
</li>
|
||||
|
||||
```
|
||||
var rectangleArea: Double {
|
||||
return long * wide
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 条件语句
|
||||
|
||||
在条件语句中,需要考虑到条件语句中可能涉及的所有分支条件,对于每个分支条件都需要考虑到,并进行处理,减少或不使用默认处理。特别是使用 Switch 处理枚举时,不要有 default 分支。
|
||||
|
||||
在iOS开发中,你使用 Swift 语言编写 Switch 语句时,如果不加default分支的话,当枚举有新增值时,编译器会提醒你增加分支处理。这样,就可以有效避免分支漏处理的情况。
|
||||
|
||||
另外,条件语句的嵌套分支不宜过多,可以充分利用 Swift 中的 guard 语法。比如,这一段处理登录的示例代码:
|
||||
|
||||
```
|
||||
if let userName = login.userNameOK {
|
||||
if let password = login.passwordOK {
|
||||
// 登录处理
|
||||
...
|
||||
} else {
|
||||
fatalError("login wrong")
|
||||
}
|
||||
} else {
|
||||
fatalError("login wrong")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面这段代码表示的是,当用户名和密码都没有问题时再进行登录处理。那么,我们使用 guard 语法时,可以改写如下:
|
||||
|
||||
```
|
||||
guard
|
||||
let userName = login.userNameOK,
|
||||
let password = login.passwordOK,
|
||||
else {
|
||||
fatalError("login wrong")
|
||||
}
|
||||
// 登录处理
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
可以看到,改写后的代码更易读了,异常处理都在一个区域,guard 语句真正起到了守卫的职责。而且你一旦声明了 guard,编译器就会强制你去处理异常,否则就会报错。异常处理越完善,代码就会越健壮。所以,条件语句的嵌套处理,你可以考虑使用guard语法。
|
||||
|
||||
### 循环语句
|
||||
|
||||
在循环语句中,我们应该尽量少地使用 continue 和 break,同样可以使用 guard 语法来解决这个问题。解决方法是:所有需要 continue 和 break 的地方统一使用 guard 去处理,将所有异常都放到一处。这样做的好处是,在维护的时候方便逻辑阅读,使得代码更加易读和易于理解。
|
||||
|
||||
### 函数
|
||||
|
||||
对于函数来说,体积不宜过大,最好控制在百行代码以内。如果函数内部逻辑多,我们可以将复杂逻辑分解成多个小逻辑,并将每个小逻辑提取出来作为一个单独的函数。每个函数处理最小单位的逻辑,然后一层一层往上组合。
|
||||
|
||||
这样,我们就可以通过函数名明确那段逻辑处理的目的,提高代码的可读性。
|
||||
|
||||
拆分成多个逻辑简单的函数后,我们需要注意的是,要对函数的入参进行验证,guard 语法同样适用于检查入参。比如下面的这个函数:
|
||||
|
||||
```
|
||||
func saveRSS(rss: RSS?, store: Store?) {
|
||||
guard let rss = rss else {
|
||||
return
|
||||
}
|
||||
guard let store = store else {
|
||||
return
|
||||
}
|
||||
|
||||
// 保存 RSS
|
||||
return
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
如上面代码所示,通过 guard语法检查入参 rss 和 store 是否异常,提高函数的健壮性会来得更容易些。
|
||||
|
||||
另外,函数内尽量避免使用全局变量来传递数据,使用参数或者局部变量传递数据能够减少函数对外部的依赖,减少耦合,提高函数的独立性,提高单元测试的准确性。
|
||||
|
||||
### 类
|
||||
|
||||
在Objective-C 中,类的头文件应该尽可能少地引入其他类的头文件。你可以通过 class 关键字来声明,然后在实现文件里引入需要的其他类的头文件。
|
||||
|
||||
对于继承和遵循协议的情况,无法避免引入其他类的头文件,所以你在代码设计时还是要尽量减少继承,特别是继承关系太多时不利于代码的维护和修改,比如说修改父类时还需要考虑对所有子类的影响,如果评估不全,影响就难以控制。
|
||||
|
||||
### 分类
|
||||
|
||||
在写分类时,分类里增加的方法名要尽量加上前缀,而如果是系统自带类的分类的话,方法名就一定要加上前缀,来避免方法名重复的问题。
|
||||
|
||||
分类的作用如其名,就是对类做分类用的,所以我建议你,能够把一个类里的公共方法放到不同的分类里,便于管理维护。分类特别适合多人负责同一个类时,根据不同分类来进行各自不同功能的代码维护。
|
||||
|
||||
## Code Review
|
||||
|
||||
上面的内容,就是在我看来比较好的iOS编码规范了。除此之外,你还可以参考其他公司对 iOS 开发制定的编码规范来完善自己团队的编码规范,比如 [Spotify](https://github.com/spotify/ios-style) 的 Objective-C 编码规范、[纽约时报](https://github.com/NYTimes/objective-c-style-guide)的 Objective-C 的编码规范、[Raywenderlich 的 Objective-C](https://github.com/raywenderlich/objective-c-style-guide) 编码规范、[Raywenderlich 的 Swift](https://github.com/raywenderlich/swift-style-guide) 编码规范。
|
||||
|
||||
在我看来,好的代码规范首先要保证代码逻辑清晰,然后再考虑简洁、扩展、重用等问题。逻辑清晰的代码几乎不需要注释来说明,通过命名和清晰地编写逻辑就能够让其他人快速读懂。
|
||||
|
||||
**不需要注释就能轻松读懂的代码,使用的语言特性也必然是通用和经典的,过新的语言特性和黑魔法不利于代码逻辑的阅读,应该减少使用,即使使用也需要多加注释,避免他人无法理解。**
|
||||
|
||||
当你制定出好的代码规范后,就需要考虑如何将代码规范落地执行了。代码规范落地最好的方式就是 Code Review 。通过 Code Review ,你可以去检查代码规范是否被团队成员执行,同时还可以在 Code Review 时,及时指导代码编写不规范的同学。
|
||||
|
||||
那么,**怎么做Code Review 会比较好呢?**
|
||||
|
||||
**首先,**我觉得要利用好 Code Review 这个卡点,先使用静态检查工具对提交的代码进行一次全面检查。
|
||||
|
||||
如果是 Swift 语言的话,你可以使用 [SwiftLint](https://github.com/realm/SwiftLint)工具来检查代码规范。Swift 通过 Hook Clang 和 SourceKit 中 AST 的回调来检查源代码,如何使用SourceKit 开发工具可以参看这篇文章“[Uncovering SourceKit](https://www.jpsim.com/uncovering-sourcekit/)”。
|
||||
|
||||
SwiftLint 检查的默认规则,你可以参考[它的规则说明](https://github.com/realm/SwiftLint/blob/master/Rules.md)。SwiftLint 也支持自定义检查规则,支持你添加自己制定的代码规范。你可以在 SwiftLint 目录下添加一个 .swiftlint.yml 配置文件来自定义基于正则表达式的自定义规则。具体方法,你可以参看官方定义[自定义规则的说明](https://github.com/realm/SwiftLint/blob/master/README_CN.md)。
|
||||
|
||||
如果你是使用 Objective-C 语言开发的话,可以使用 OCLint 来做代码规范检查。关于 OCLint 如何定制自己的代码规范检查,你可以参看杨萧玉的这篇博文“[使用 OCLint 自定义 MVVM 规则](http://yulingtianxia.com/blog/2019/01/27/MVVM-Rules-for-OCLint/)”。
|
||||
|
||||
**然后,进行人工检查。**
|
||||
|
||||
人工检查,就是使用类似 Phabricator 这样的Code Review工具平台,来分配人员审核提交代码,审核完代码后,审核人可以进行通过、打回、评论等操作。这里需要注意的是,人工检查最容易沦为形式主义,因此为了避免团队成员人工检查成为形式,在开始阶段最好能让团队中编码习惯好、喜欢交流的人来做审核人,以起到良好的示范作用,并以此作为后续的执行标准。
|
||||
|
||||
你可能会有疑问,既然工具可以检查代码规范,为什么还需要人工再检查一遍?我想说的是,工具确实可以通过不断完善,甚至引入 AI 分析来提高检查结果的准确性,但是,**我认为 Code Review 之所以最终还是需要人工检查的原因是,通过团队成员之间互相检查代码的方式,希望能够达到相互沟通交流,甚至相互学习的效果。**
|
||||
|
||||
试想一下,如果你经过了大量的思考,花费了很多心思写出来一段自认为完美的代码,这时候可以再得到团队其他成员的鼓励,是不是会干劲儿十足呢。相反地,如果你马虎大意,或者经验不足而写出了不好的代码,通过 Code Review 而得到了团队其他成员的建议和指导,是不是能够让你的编码水平快速提高,同时还能够吸纳更多人的经验呢。
|
||||
|
||||
Code Review 的过程也能够对代码规范进行迭代改进,最后形成一份能体现出团队整体智慧的代码规范。以后再有新成员加入时,他们也能够快速达到团队整体的编码水平,这就好比一锅老汤,新食材放进来涮涮,很快就有了相同的味道。
|
||||
|
||||
## 小结
|
||||
|
||||
在今天这篇文章中,我和你分享了什么是好的代码规范,以及如何通过 Code Review 将编码规范落实到团队中。
|
||||
|
||||
对于编码规范来说,我认为不用过于复杂,只要坚持能够让代码逻辑清晰这个原则就可以了,剩下的所有规则都围绕着这个原则来。代码逻辑清晰是高质量的代码工程最基本、最必要的条件。如果代码不清晰的话,那么其他的扩展、重用、简洁优雅都免谈。
|
||||
|
||||
写代码的首要任务是能让其他人看得懂,千万不要优先过度工程化。难懂的代码无论工程化做得多好,到最后都会被其他人弃用、重构掉。这是一种资源浪费,损己又损人。
|
||||
|
||||
## 课后作业
|
||||
|
||||
你的团队是如何做Code Review 的?如果你的团队还没有 Code Review,那原因是什么呢?
|
||||
|
||||
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
69
极客时间专栏/geek/iOS开发高手课/应用开发篇/31 | iOS 开发学习资料和书单推荐.md
Normal file
69
极客时间专栏/geek/iOS开发高手课/应用开发篇/31 | iOS 开发学习资料和书单推荐.md
Normal file
@@ -0,0 +1,69 @@
|
||||
<audio id="audio" title="31 | iOS 开发学习资料和书单推荐" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/66/db/6641c7c9cea716a2f9241e1430a541db.mp3"></audio>
|
||||
|
||||
你好,我是戴铭。
|
||||
|
||||
在更新专栏的这段时间里,我收到很多同学的反馈,希望我能推荐些iOS开发的优秀学习资料和图书,来帮助自己高效学习。确实,现在各种学习资料非常丰富,但这些资料一方面质量参差不齐,另一方面搜索出适合自己的内容也需要花费不少时间。
|
||||
|
||||
快速找到经过检验的、适合自己的学习资料,不仅可以提升我们的学习效率,还能帮助我们快速解决现阶段遇到的问题。所以,今天我就来跟你分享一些我觉得不错的学习资料和图书。
|
||||
|
||||
## 学习资料
|
||||
|
||||
iOS 开发往往会涉及界面交互,[iOS Examples](https://iosexample.com/)和[Cocoa Controls](https://www.cocoacontrols.com/)这两个网站收集了大量的开源组件和库,并且进行了非常细致的分类,你能够非常方便地找到适合自己的“轮子”。
|
||||
|
||||
如果你希望通过完整的例子来系统学习 App 开发,我推荐你查看一下GitHub上的[Open-Source iOS Apps](https://github.com/dkhamsing/open-source-ios-apps)项目。作者在这个项目中收录了很多优秀的、完整的开源 iOS App,并做了详细分类,还专门标出了上架了 App Store 的开源 iOS App。
|
||||
|
||||
AFNetworking 和 Alamofire 的作者 Mattt 维护着一个 [NSHipster](https://nshipster.com/)的网站,主要关注的是一些不常用的 iOS 知识点。如果你想扩展自己的iOS 知识面,这个网站里的内容会非常适合你。
|
||||
|
||||
[Awesome iOS](https://github.com/vsouza/awesome-ios)也是一个值得推荐的网站,里面包含了 iOS 开发的方方面面,而且内容都是经过人工筛选、分类的。我觉得,**你遇到任何 iOS 的问题,都应该先到这个网站看看**。
|
||||
|
||||
Awesome iOS 最大的特点就是大而全,囊括了从开发、调试到发布 App Store的各种学习资料,也包含了博客、书籍、教程、邮件订阅、播客的推荐。同时,这个网站还包括了 iOS 开发更细分的 Awesome 推荐,比如关于 ARKit 的 [Awesome ARKit](https://github.com/olucurious/Awesome-ARKit),关于面试问题收集的 [Awesome iOS Interview](https://github.com/dashvlas/awesome-ios-interview) question list 等等。
|
||||
|
||||
你还可以通过关注一些知名开发者动态的方式,来为自己学习方向的判断做输入。
|
||||
|
||||
这里有份列表,列出了 iOS 领域那些[知名开发者](https://github.com/ipader/SwiftGuide/blob/master/2019/SwiftDevelopers.md),你可以通过关注他们的博客、Twitter、GitHub ,来了解走在 iOS 领域前沿开发者的视野和 iOS 最新的动向。除了关注知名开发者外,你还可以关注下[开源项目团队](https://github.com/ipader/SwiftGuide/blob/master/2019/SwiftDevelopmentTeam.md)的列表,如果你正在使用他们的开源项目,通过关注他们的动向,随时了解这些开源项目的最新进展。
|
||||
|
||||
## 图书推荐
|
||||
|
||||
通过上面我和你推荐的学习资料,你可以去分析并解决在开发中遇到的问题,也可以通过知名开发者和优秀开源项目的团队动态,来了解iOS开发的技术动向。但是,如果你想要透彻地掌握某领域的专业知识,还是需要静下心去慢慢学习。而,阅读相关书籍,就是一种很好的学习方式。
|
||||
|
||||
那么,接下来我再跟你推荐一些我觉得还不错的书籍,希望能够对你的学习有所帮助。
|
||||
|
||||
[Raywenderlich](https://store.raywenderlich.com/)出版的图书质量都非常不错,可以一步一步教你掌握一些开发知识,内容非常实用,而且这些图书的涉及面广。比如,这些图书包括有 [ARKit](https://store.raywenderlich.com/products/arkit-by-tutorials)、Swift 服务端的 [Vapor](https://store.raywenderlich.com/products/server-side-swift-with-vapor) 和 [Kitura](https://store.raywenderlich.com/products/server-side-swift-with-kitura)、[Metal](https://store.raywenderlich.com/products/metal-by-tutorials)、[数据结构和算法的 Swift 版](https://store.raywenderlich.com/products/data-structures-and-algorithms-in-swift)、[设计模式](https://store.raywenderlich.com/products/design-patterns-by-tutorials)、[Core Data](https://store.raywenderlich.com/products/core-data-by-tutorials)、[iOS 动画](https://store.raywenderlich.com/products/ios-animations-by-tutorials)、[Apple 调试和逆向工程](https://store.raywenderlich.com/products/advanced-apple-debugging-and-reverse-engineering)、[RxSwift](https://store.raywenderlich.com/products/rxswift)、[Realm](https://store.raywenderlich.com/products/realm-building-modern-swift-apps-with-realm-database)、[2D](https://store.raywenderlich.com/products/2d-apple-games-by-tutorials) 和 [3D](https://store.raywenderlich.com/products/3d-apple-games-by-tutorials) 游戏开发等各个方面。
|
||||
|
||||
另外,[objc.io](https://www.objc.io/books/)家的图书会从原理和源代码实现的角度来讲解知识点,也非常不错,内容比 Raywenderlich 出版的图书更深入,适合有一定 iOS 开发经验的人阅读。
|
||||
|
||||
Raywenderlich 和 objc.io 的书基本都是 Swift 来写的。如果你想更深入地理解 Objective-C 的话,我推荐[《Objective-C高级编程》](time://mall?url=https%3A%2F%2Fh5.youzan.com%2Fv2%2Fgoods%2F2ok8btknjr3e0%3Fstep%3D1)这本书。这本书里的知识点并不多,主要讲的是内存管理、Block、GCD(Grand Central Dispatch)。
|
||||
|
||||
这三个知识点对Objective-C来说非常重要,如果使用不当将会置你的工程于风险之中。正是因为涉及的知识点不多,所以全书能基于苹果公司公开的源码,集中讲清楚这三个知识点。这,非常难得。因此,如果你对内存管理、Block、GCD 了解地不是很透彻,我建议你仔细阅读这本书。
|
||||
|
||||
如果你想要了解系统工作原理的话,我推荐阅读[《程序员的自我修养 - 链接、装载与库》](time://mall?url=https%3A%2F%2Fh5.youzan.com%2Fv2%2Fgoods%2F3f42f8o0371ug%3Fstep%3D1)。这本书详细且深入地讲解了硬件、操作系统、线程的知识。
|
||||
|
||||
阅读这本书之前,你需要先掌握 CPU、计算机原理、汇编、编译原理、C 语言、C++语言等计算机学科的基本知识。掌握了这些知识后再阅读这本书,它能帮你把知识串起来,帮你从代码编译、链接到运行时内存虚拟空间和物理空间映射的角度,了解一个程序从编写到运行时的底层知识的方方面面。
|
||||
|
||||
现在编程技术不断推陈出新,不断通过添加中间层将底层埋住,新一代开发人员也越来越不重视底层知识,所以当他们学到的上层知识被更新替代以后就会感叹赶不上技术更新的脚步,知识焦虑感越来越严重。
|
||||
|
||||
而读完这本书,你就会发现,有些知识是不会变的,不管上层如何变化,只要抓住这些知识就能够抓住核心,掌握技术的走向。
|
||||
|
||||
《程序员的自我修养 - 链接、装载与库》耗时30年才被出版,期间作者不断优化其中的内容,最终成为一本经典图书。正如其名,程序员的自我修养,就是来自对程序运行背后机制的学习,而不是一味地追新。除了内容地道以外,这本书的作者们精益求精的精神也非常值得我们学习。
|
||||
|
||||
当你有了大量的编程经验,需要考虑工程架构合理性的时候,我推荐你看看[《架构整洁之道》](time://mall?url=https%3A%2F%2Fh5.youzan.com%2Fv2%2Fgoods%2F276fjn6r89uuw%3Fstep%3D1)这本书。架构设计的思想也不会过时,并适用于所有的知识领域。
|
||||
|
||||
这本书详细分析了架构的目标价值、编程范式、架构设计原则、组件拆分和边界处理。其中,编程范式介绍了结构化编程、面向对象编程、函数式编程等。设计原则包含了开闭原则 OCP、单一职责原则 SRP、里氏替换原则 LSP、接口隔离原则 ISP、依赖反转原则 DIP 等等,内容十分丰富。熟练掌握这些架构设计原则,会让你对架构的理解更深,也可以从更多方面去思考。
|
||||
|
||||
值得一提的是,这本书还通过实践案例把所讲知识都串了起来,让你更容易理解架构设计的知识。
|
||||
|
||||
## 小结
|
||||
|
||||
在今天这篇文章中,我和你分享了很多关于iOS开发的学习资料,这其中有优秀的开源项目,也有一些经典的图书。尤其是《程序员的自我修养 - 链接、装载与库》和《架构整洁之道》这两本书,值得你反复阅读。在不同阶段去阅读这两本书,你会有不同的体会,也会有不同的收获。
|
||||
|
||||
如果你还想阅读更多计算机编程相关的经典书,可以再看看《编码》、[《代码整洁之道》](time://mall?url=https%3A%2F%2Fh5.youzan.com%2Fv2%2Fgoods%2F3691yho9a34oo%3Fstep%3D1)、《代码大全》、[《算法》](time://mall?url=https%3A%2F%2Fh5.youzan.com%2Fv2%2Fgoods%2F3f1l7krpotnjc%3Fstep%3D1)。
|
||||
|
||||
每个人的学习时间都是有限的,上班时要争分夺秒的完成任务,下班放假还要放松休息。与其无止境的寻找资料,还不如静下心来阅读经典和多一些思考。所以,希望我今天的这篇文章可以帮到你。
|
||||
|
||||
## 课后作业
|
||||
|
||||
你在读过的书里,哪一本对你帮助最大呢?
|
||||
|
||||
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
244
极客时间专栏/geek/iOS开发高手课/应用开发篇/32 | 热点问题答疑(三).md
Normal file
244
极客时间专栏/geek/iOS开发高手课/应用开发篇/32 | 热点问题答疑(三).md
Normal file
@@ -0,0 +1,244 @@
|
||||
<audio id="audio" title="32 | 热点问题答疑(三)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b0/14/b0fc063549f9c056091686db13364914.mp3"></audio>
|
||||
|
||||
你好,我是戴铭。
|
||||
|
||||
这是我们《iOS开发高手课》专栏的第三期答疑文章,我将继续和你分享大家在学习前面文章时遇到的最普遍的问题。
|
||||
|
||||
今天,我在这段时间的留言问题中,挑选了几个iOS开发者普遍关注的问题,在这篇答疑文章里来做一个统一回复。
|
||||
|
||||
## A/B测试SDK
|
||||
|
||||
@鼠辈同学在第24篇文章[《A/B测试:验证决策效果的利器》](https://time.geekbang.org/column/article/93097)留言中问道:
|
||||
|
||||
>
|
||||
最近一直在找一个好的 A/B 测试的 SDK,不知道作者之前用过什么好的 A/B 测试的 SDK(三方的,可以后台控制的)
|
||||
|
||||
|
||||
我认为带后台功能的 A/B 测试 SDK没什么必要,原因有二:
|
||||
|
||||
<li>
|
||||
A/B 测试本身就是为业务服务的,需要对会影响产品决策的业务场景做大量定制化开发;
|
||||
</li>
|
||||
<li>
|
||||
A/B 测试功能本身并不复杂,第三方后台定制化开发,成本也不会节省多少。
|
||||
</li>
|
||||
|
||||
因此,我推荐后台功能自己来做,端上使用我在第24篇文章中提到的 SkyLab 就完全没有问题了。另外,SkyLab 也可以很方便地集成到你自己的后台中。
|
||||
|
||||
## 如何衡量性能监控的优劣?
|
||||
|
||||
@ RiverLi 同学在第16篇文章[《](https://time.geekbang.org/column/article/90546)[性能监控:衡量 App 质量的那把尺](https://time.geekbang.org/column/article/90546)[》](https://time.geekbang.org/column/article/90546)的评论区留言问到:
|
||||
|
||||
>
|
||||
对于性能的监控有没有衡量标准,如何衡量优劣?
|
||||
|
||||
|
||||
我觉得,如果给所有 App 制定相同的衡量标准是不现实的,这样的标准,也是无法落地的。为什么这么说呢,很有可能由于历史原因或者 App的特性决定了有些App的性能无法达到另一个 App 的标准。又或者说,有些App需要进行大量的重构,才能要达到另一个 App 的性能标准,而这些重构明显不是一朝一夕就能落地执行的。特别是业务还在快跑的情况下,你只能够有针对性地去做优化,而不是大量的重构。
|
||||
|
||||
回到性能监控的初衷,它主要是希望通过监控手段去发现突发的性能问题,这也是我们再做线上性能监控时需要重点关注的。
|
||||
|
||||
对于 App 运行普遍存在的性能问题,我们应该在上线前就设法优化完成。因为,线下的性能问题是可控的,而线上的性能问题往往是“摸不着”的,也正是这个原因,我们需要监控线上性能问题。
|
||||
|
||||
因此,**性能监控的标准一定是针对 App线下的性能表现来制定的**。比如,你的App在线下连续3秒 CPU 占比都是在70%以下,那么 CPU 占比的监控值就可以设置为3秒内占比在70%以下。如果超过这个阈值就属于突发情况,就做报警处理,进行问题跟踪排查,然后有针对性地修复问题。
|
||||
|
||||
## 关于WatchDog
|
||||
|
||||
我在[第13篇文章](https://time.geekbang.org/column/article/89494)中讲解如何用RunLoop原理去监控卡顿的时候,用到了WatchDog机制。Xqqq0 同学在文后留言中,希望我解释一下这个机制,并推荐一些相关的学习资料。
|
||||
|
||||
WatchDog 是苹果公司设计的一种机制,主要是为了避免 App 界面无响应造成用户无法操作,而强杀掉 App 进程。造成 App 界面无响应的原因种类太多,于是苹果公司采用了一刀切的做法:凡是主线程卡死一定的时间就会被 WatchDog 机制强杀掉。这个卡死时间,WatchDog 在启动时设置的是 20秒,前台时设置的是10秒,后台时设置的是10分钟。
|
||||
|
||||
由于 WatchDog 强杀日志属于系统日志,所以你的 App 上线后需要自己来监控卡顿,这样才能够在 WatchDog 强杀之前捕获到 App 卡死的情况。关于这部分内容的详细讲解,你可以参看苹果公司关于[崩溃分析](https://developer.apple.com/library/archive/technotes/tn2151/_index.html)的文档。
|
||||
|
||||
## 关于iOS崩溃
|
||||
|
||||
在专栏的第12篇文章[《iOS 崩溃千奇百怪,如何全面监控?》](https://time.geekbang.org/column/article/88600)后,(Jet)黄仲平同学提了这么几个问题。考虑到这几个问题涉及知识点比较有代表性,所以我特意在今天这篇答疑文章中和你详细展开下。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/48/6e/48115cbd4fa6a59c96551858aa68876e.jpg" alt="">
|
||||
|
||||
关于实现崩溃问题自动定位到人,我认为通过堆栈信息来匹配到人是没有问题的。关于实现方法的问题,也就是第一个问题,你可以先做个映射表,每个类都能够对应到一个负责人,当获取到崩溃堆栈信息时,根据映射表就能够快速定位到人了。
|
||||
|
||||
对于第二个问题关于日志的收集方法,我想说的是 PLCrashReporter 就是用handleSignalException 方法来收集的。
|
||||
|
||||
第三个关于 dSYM 解析堆栈信息工作原理的问题,也不是很复杂。dSYM 会根据线程中方法调用栈的指针,去符号表里找到这些指针所对应的符号信息进行解析,解析完之后就能够展示出可读的方法调用栈。
|
||||
|
||||
接下来,**我来和你说说通过堆栈匹配到人的具体实现的问题。**
|
||||
|
||||
**第一步**,通过 task_threads 获取当前所有的线程,遍历所有线程,通过 thread_info 获取各个线程的详细信息。
|
||||
|
||||
**第二步**,遍历线程,每个线程都通过 thread_get_state 得到 machine context 里面函数调用栈的指针。
|
||||
|
||||
thread_get_state 获取函数调用栈指针的具体实现代码如下:
|
||||
|
||||
```
|
||||
_STRUCT_MCONTEXT machineContext; //线程栈里所有的栈指针
|
||||
// 通过 thread_get_state 获取完整的 machineContext 信息,包含 thread 状态信息
|
||||
mach_msg_type_number_t state_count = smThreadStateCountByCPU();
|
||||
kern_return_t kr = thread_get_state(thread, smThreadStateByCPU(), (thread_state_t)&machineContext.__ss, &state_count);
|
||||
|
||||
```
|
||||
|
||||
获取到的这些函数调用栈,需要一个栈结构体来保存。
|
||||
|
||||
**第三步**,创建栈结构体。创建后通过栈基地址指针获取到当前栈帧地址,然后往前查找函数调用帧地址,并将它们保存到创建的栈结构体中。具体代码如下:
|
||||
|
||||
```
|
||||
// 为通用回溯设计结构支持栈地址由小到大,地址里存储上个栈指针的地址
|
||||
typedef struct SMStackFrame {
|
||||
const struct SMStackFrame *const previous;
|
||||
const uintptr_t return_address;
|
||||
} SMStackFrame;
|
||||
|
||||
SMStackFrame stackFrame = {0};
|
||||
// 通过栈基址指针获取当前栈帧地址
|
||||
const uintptr_t framePointer = smMachStackBasePointerByCPU(&machineContext);
|
||||
if (framePointer == 0 || smMemCopySafely((void *)framePointer, &stackFrame, sizeof(stackFrame)) != KERN_SUCCESS) {
|
||||
return @"Fail frame pointer";
|
||||
}
|
||||
for (; i < 32; i++) {
|
||||
buffer[i] = stackFrame.return_address;
|
||||
if (buffer[i] == 0 || stackFrame.previous == 0 || smMemCopySafely(stackFrame.previous, &stackFrame, sizeof(stackFrame)) != KERN_SUCCESS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**第四步**,根据获取到的栈帧地址,找到对应的 image 的游标,从而能够获取 image 的更多信息。代码如下:
|
||||
|
||||
```
|
||||
// 初始化保存符号结果的结构体 Dl_info
|
||||
info->dli_fname = NULL;
|
||||
info->dli_fbase = NULL;
|
||||
info->dli_sname = NULL;
|
||||
info->dli_saddr = NULL;
|
||||
|
||||
// 根据地址获取是哪个 image
|
||||
const uint32_t idx = smDyldImageIndexFromAddress(address);
|
||||
if (idx == UINT_MAX) {
|
||||
return false;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**第五步**,在知道了是哪个 image 后,根据 Mach-O 文件的结构,要想获取符号表所在的 segment,需要先找到 Mach-O 里对应的 Header。通过 _dyld_get_image_header 方法,我们可以找到 mach_header 结构体。然后,使用 _dyld_get_image_vmaddr_slide 方法,我们就能够获取虚拟内存地址 slide 的数量。而动态链接器就是通过添加 slide 数量到 image 基地址,以实现将 image 映射到未占用地址的进程虚拟地址空间来加载 image 的。具体实现代码如下:
|
||||
|
||||
```
|
||||
/*
|
||||
Header
|
||||
------------------
|
||||
Load commands
|
||||
Segment command 1 -------------|
|
||||
Segment command 2 |
|
||||
------------------ |
|
||||
Data |
|
||||
Section 1 data |segment 1 <----|
|
||||
Section 2 data | <----|
|
||||
Section 3 data | <----|
|
||||
Section 4 data |segment 2
|
||||
Section 5 data |
|
||||
... |
|
||||
Section n data |
|
||||
*/
|
||||
/*----------Mach Header---------*/
|
||||
// 根据 image 的序号获取 mach_header
|
||||
const struct mach_header* machHeader = _dyld_get_image_header(idx);
|
||||
|
||||
// 将 header 的名字和 machHeader 记录到 Dl_info 结构体里
|
||||
info->dli_fname = _dyld_get_image_name(idx);
|
||||
info->dli_fbase = (void*)machHeader;
|
||||
|
||||
// 返回 image_index 索引的 image 的虚拟内存地址 slide 的数量
|
||||
// 动态链接器就是通过添加 slide 数量到 image 基地址,以实现将 image 映射到未占用地址的进程虚拟地址空间来加载 image 的。
|
||||
const uintptr_t imageVMAddressSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);
|
||||
|
||||
```
|
||||
|
||||
**第六步**,计算 ASLR(地址空间布局随机化) 偏移量。
|
||||
|
||||
ASLR 是一种防范内存损坏漏洞被利用的计算机安全技术,想详细了解 ASLR的话,你可以参看[它的 Wiki页面](https://en.wikipedia.org/wiki/Address_space_layout_randomization)。
|
||||
|
||||
通过 ASLR 偏移量可以获取 segment 的基地址,segment 定义 Mach-O 文件中的字节范围以及动态链接器加载应用程序时这些字节映射到虚拟内存中的地址和内存保护属性。 所以,segment 总是虚拟内存页对齐。
|
||||
|
||||
```
|
||||
/*-----------ASLR 偏移量---------*/
|
||||
// https://en.wikipedia.org/wiki/Address_space_layout_randomization
|
||||
const uintptr_t addressWithSlide = address - imageVMAddressSlide;
|
||||
// 通过 ASLR 偏移量可以获取 segment 的基地址
|
||||
// segment 定义 Mach-O 文件中的字节范围以及动态链接器加载应用程序时这些字节映射到虚拟内存中的地址和内存保护属性。 所以,segment 总是虚拟内存页对齐。
|
||||
const uintptr_t segmentBase = smSegmentBaseOfImageIndex(idx) + imageVMAddressSlide;
|
||||
if (segmentBase == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
**第七步,**遍历所有 segment,查找目标地址在哪个 segment 里。
|
||||
|
||||
除了 __TEXT segment 和 __DATA segment 外,还有 __LINKEDIT segment。__LINKEDIT segment 里包含了动态链接器使用的原始数据,比如符号、字符串、重定位表项。LC_SYMTAB 描述的是,__LINKEDIT segment 里查找的字符串在符号表的位置。有了符号表里字符串的位置,就能找到目标地址对应的字符串,从而完成函数调用栈地址的符号化。
|
||||
|
||||
这个过程的详细实现代码如下:
|
||||
|
||||
```
|
||||
/*--------------Mach Segment-------------*/
|
||||
// 地址最匹配的symbol
|
||||
const nlistByCPU* bestMatch = NULL;
|
||||
uintptr_t bestDistance = ULONG_MAX;
|
||||
uintptr_t cmdPointer = smCmdFirstPointerFromMachHeader(machHeader);
|
||||
if (cmdPointer == 0) {
|
||||
return false;
|
||||
}
|
||||
// 遍历每个 segment 判断目标地址是否落在该 segment 包含的范围里
|
||||
for (uint32_t iCmd = 0; iCmd < machHeader->ncmds; iCmd++) {
|
||||
const struct load_command* loadCmd = (struct load_command*)cmdPointer;
|
||||
/*----------目标 Image 的符号表----------*/
|
||||
// Segment 除了 __TEXT 和 __DATA 外还有 __LINKEDIT segment,它里面包含动态链接器使用的原始数据,比如符号,字符串和重定位表项。
|
||||
// LC_SYMTAB 描述了 __LINKEDIT segment 内查找字符串和符号表的位置
|
||||
if (loadCmd->cmd == LC_SYMTAB) {
|
||||
// 获取字符串和符号表的虚拟内存偏移量。
|
||||
const struct symtab_command* symtabCmd = (struct symtab_command*)cmdPointer;
|
||||
const nlistByCPU* symbolTable = (nlistByCPU*)(segmentBase + symtabCmd->symoff);
|
||||
const uintptr_t stringTable = segmentBase + symtabCmd->stroff;
|
||||
|
||||
for (uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++) {
|
||||
// 如果 n_value 是0,symbol 指向外部对象
|
||||
if (symbolTable[iSym].n_value != 0) {
|
||||
// 给定的偏移量是文件偏移量,减去 __LINKEDIT segment 的文件偏移量获得字符串和符号表的虚拟内存偏移量
|
||||
uintptr_t symbolBase = symbolTable[iSym].n_value;
|
||||
uintptr_t currentDistance = addressWithSlide - symbolBase;
|
||||
// 寻找最小的距离 bestDistance,因为 addressWithSlide 是某个方法的指令地址,要大于这个方法的入口。
|
||||
// 离 addressWithSlide 越近的函数入口越匹配
|
||||
if ((addressWithSlide >= symbolBase) && (currentDistance <= bestDistance)) {
|
||||
bestMatch = symbolTable + iSym;
|
||||
bestDistance = currentDistance;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bestMatch != NULL) {
|
||||
// 将虚拟内存偏移量添加到 __LINKEDIT segment 的虚拟内存地址可以提供字符串和符号表的内存 address。
|
||||
info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddressSlide);
|
||||
info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);
|
||||
if (*info->dli_sname == '_') {
|
||||
info->dli_sname++;
|
||||
}
|
||||
// 所有的 symbols 的已经被处理好了
|
||||
if (info->dli_saddr == info->dli_fbase && bestMatch->n_type == 3) {
|
||||
info->dli_sname = NULL;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
cmdPointer += loadCmd->cmdsize;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
在今天这篇文章中,我针对一些比较有代表性、你大概率会遇到的留言问题做了解答。这其中,包括第三方库的选择、性能衡量标准,以及崩溃分析方面的问题。
|
||||
|
||||
最后,对于第三方库的使用,我的建议是:如果和业务强相关,比如埋点或者 A/B 测试这样的库,最好是自建,你可以借鉴开源库的思路;一些基础的、通用性强的库,比如网络库和持续化存储的库,直接使用成熟的第三方库,既可以节省开发和维护时间,还能够提高产品质量;还有种情况就是,如果你所在团队较小,只有几个 iOS 开发人员,那么还是要尽可能地使用开源项目,你可以在 [Awesome iOS](https://github.com/vsouza/awesome-ios)上去找到适合团队的项目。
|
||||
|
||||
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎把它分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user