谈谈 MVX 中的 Controller
+在前两篇文章中,我们已经对 iOS 中的 Model 层以及 View 层进行了分析,划分出了它们的具体职责,其中 Model 层除了负责数据的持久存储、缓存工作,还要负责所有 HTTP... »
+diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 50dcf7d..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.md linguist-language=Objective-C diff --git a/.github/ISSUE_REPLY_TEMPLATE.md b/.github/ISSUE_REPLY_TEMPLATE.md new file mode 100644 index 0000000..944c73f --- /dev/null +++ b/.github/ISSUE_REPLY_TEMPLATE.md @@ -0,0 +1,3 @@ +# 注意 + +由于评论维护的问题,所有在 GitHub Issue 中提的问题都不会得到作者的回复,请到对应[博客](http://draveness.me)下面的 Disqus 评论系统留言,谢谢。 diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 0000000..944c73f --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,3 @@ +# 注意 + +由于评论维护的问题,所有在 GitHub Issue 中提的问题都不会得到作者的回复,请到对应[博客](http://draveness.me)下面的 Disqus 评论系统留言,谢谢。 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cfc4d70 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate + +fastlane/report.xml + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control +# +Pods/ + +.DS_Store diff --git a/README.md b/README.md index 729a77d..c4ee9dd 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,51 @@ -# iOS-Source-Code-Analyze +# Analyze
-
+
Banner designed by Levine

+
+LCD 的成像原理虽然与 CRT 截然不同,每一个像素的颜色可以**在需要改变时**才去改变电压,也就是不需要刷新频率,但是由于一些历史原因,LCD 仍然需要按照一定的刷新频率向 GPU 获取新的图像用于显示。
+
+### 屏幕撕裂
+
+但是显示器只是用于将图像显示在屏幕上,谁又是图像的提供者呢?图像都是我们经常说的 GPU 提供的。
+
+而这导致了另一个问题,由于 GPU 生成图像的频率与显示器刷新的频率是不相关的,那么在显示器刷新时,GPU 没有准备好需要显示的图像怎么办;或者 GPU 的渲染速度过快,显示器来不及刷新,GPU 就已经开始渲染下一帧图像又该如何处理?
+
+
+
+如果解决不了这两个问题,就会出现上图中的*屏幕撕裂*(Screen Tearing)现象,屏幕中一部分显示的是上一帧的内容,另一部分显示的是下一帧的内容。
+
+我们用两个例子来说明可能出现屏幕撕裂的两种情况:
+
++ 如果显示器的刷新频率为 75 Hz,GPU 的渲染速度为 100 Hz,那么在两次屏幕刷新的间隔中,GPU 会渲染 4/3 个帧,后面的 1/3 帧会覆盖已经渲染好的帧栈,最终会导致屏幕在 1/3 或者 2/3 的位置出现屏幕撕裂效果;
++ 那么 GPU 的渲染速度小于显示器呢,比如说 50 Hz,那么在两次屏幕刷新的间隔中,GPU 只会渲染 2/3 帧,剩下的 1/3 会来自上一帧,与上面的结果完全相同,在同样的位置出现撕裂效果。
+
+到这里,有人会说,如果显示器的刷新频率与 GPU 的渲染速度完全相同,应该就会解决屏幕撕裂的问题了吧?其实并不是。显示器从 GPU 拷贝帧的过程依然需要消耗一定的时间,如果屏幕在拷贝图像时刷新,仍然会导致屏幕撕裂问题。
+
+
+
+引入多个缓冲区可以有效地**缓解**屏幕撕裂,也就是同时使用一个*帧缓冲区*(frame buffer)和多个*后备缓冲区*(back buffer);在每次显示器请求内容时,都会从**帧缓冲区**中取出图像然后渲染。
+
+虽然缓冲区可以减缓这些问题,但是却不能解决;如果后备缓冲区绘制完成,而帧缓冲区的图像没有被渲染,后备缓冲区中的图像就会覆盖帧缓冲区,仍然会导致屏幕撕裂。
+
+解决这个问题需要另一个机制的帮助,也就是垂直同步(Vertical synchronization),简称 V-Sync 来解决。
+
+### V-Sync
+
+V-Sync 的主要作用就是保证**只有在帧缓冲区中的图像被渲染之后,后备缓冲区中的内容才可以被拷贝到帧缓冲区中**,理想情况下的 V-Sync 会按这种方式工作:
+
+
+
+每次 V-Sync 发生时,CPU 以及 GPU 都已经完成了对图像的处理以及绘制,显示器可以直接拿到缓冲区中的帧。但是,如果 CPU 或者 GPU 的处理需要的时间较长,就会发生掉帧的问题:
+
+
+
+
+在 V-Sync 信号发出时,CPU 和 GPU 并没有准备好需要渲染的帧,显示器就会继续使用当前帧,这就**加剧**了屏幕的显示问题,而每秒显示的帧数会少于 60。
+
+由于会发生很多次掉帧,在开启了 V-Sync 后,40 ~ 50 FPS 的渲染频率意味着显示器输出的画面帧率会从 60 FPS 急剧下降到 30 FPS,原因在这里不会解释,读者可以自行思考。
+
+其实到这里关于屏幕渲染的内容就已经差不多结束了,根据 V-Sync 的原理,优化应用性能、提高 App 的 FPS 就可以从两个方面来入手,优化 CPU 以及 GPU 的处理时间。
+
+> 读者也可以从 [iOS 保持界面流畅的技巧](http://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/)这篇文章中了解更多的相关内容。
+
+## 性能调优的策略
+
+CPU 和 GPU 在每次 V-Sync 时间点到达之前都在干什么?如果,我们知道了它们各自负责的工作,通过优化代码就可以提升性能。
+
+
+
+很多 CPU 的操作都会延迟 GPU 开始渲染的时间:
+
++ 布局的计算 - 如果你的视图层级太过于复杂,或者视图需要重复多次进行布局,尤其是在使用 Auto Layout 进行自动布局时,对性能影响尤为严重;
++ 视图的惰性加载 - 在 iOS 中只有当视图控制器的视图显示到屏幕时才会加载;
++ 解压图片 - iOS 通常会在真正绘制时才会解码图片,对于一个较大的图片,无论是直接或间接使用 `UIImageView` 或者绘制到 Core Graphics 中,都需要对图片进行解压;
++ ...
+
+宽泛的说,大多数的 `CALayer` 的属性都是由 GPU 来绘制的,比如图片的圆角、变换、应用纹理;但是过多的几何结构、重绘、离屏绘制(Offscrren)以及过大的图片都会导致 GPU 的性能明显降低。
+
+> 上面的内容出自 [CPU vs GPU · iOS 核心动画高级技巧](https://zsisme.gitbooks.io/ios-/content/chapter12/cpu-versus-gpu.html),你可以在上述文章中对 CPU 和 GPU 到底各自做了什么有一个更深的了解。
+
+也就是说,如果我们解决了上述问题,就能加快应用的渲染速度,大大提升用户体验。
+
+## AsyncDisplayKit
+
+文章的前半部分已经从屏幕的渲染原理讲到了性能调优的几个策略;而 [AsyncDisplayKit](http://asyncdisplaykit.org) 就根据上述的策略帮助我们对应用性能进行优化。
+
+
+
+AsyncDisplayKit(以下简称 ASDK)是由 Facebook 开源的一个 iOS 框架,能够帮助最复杂的 UI 界面保持流畅和快速响应。
+
+ASDK 从开发到开源大约经历了一年多的时间,它其实并不是一个简单的框架~~它是一个复杂的框架~~,更像是对 UIKit 的重新实现,把整个 UIKit 以及 CALayer 层封装成一个一个 `Node`,**将昂贵的渲染、图片解码、布局以及其它 UI 操作移出主线程**,这样主线程就可以对用户的操作及时做出反应。
+
+很多分析 ASDK 的文章都会有这么一张图介绍框架中的最基本概念:
+
+
+
+在 ASDK 中最基本的单位就是 `ASDisplayNode`,每一个 node 都是对 `UIView` 以及 `CALayer` 的抽象。但是与 `UIView` 不同的是,`ASDisplayNode` 是线程安全的,它可以在后台线程中完成初始化以及配置工作。
+
+如果按照 60 FPS 的刷新频率来计算,每一帧的渲染时间只有 16ms,在 16ms 的时间内要完成对 `UIView` 的创建、布局、绘制以及渲染,CPU 和 GPU 面临着巨大的压力。
+
+
+
+但是从 A5 处理器之后,多核的设备成为了主流,原有的将所有操作放入主线程的实践已经不能适应复杂的 UI 界面,所以 **ASDK 将耗时的 CPU 操作以及 GPU 渲染纹理(Texture)的过程全部放入后台进程,使主线程能够快速响应用户操作**。
+
+ASDK 通过独特的渲染技巧、代替 AutoLayout 的布局系统、智能的预加载方式等模块来实现对 App 性能的优化。
+
+## ASDK 的渲染过程
+
+ASDK 中到底使用了哪些方法来对视图进行渲染呢?本文主要会从渲染的过程开始分析,了解 ASDK 底层如何提升界面的渲染性能。
+
+在 ASDK 中的渲染围绕 `ASDisplayNode` 进行,其过程总共有四条主线:
+
++ 初始化 `ASDisplayNode` 对应的 `UIView` 或者 `CALayer`;
++ 在当前视图进入视图层级时执行 `setNeedsDisplay`;
++ `display` 方法执行时,向后台线程派发绘制事务;
++ 注册成为 `RunLoop` 观察者,在每个 `RunLoop` 结束时回调。
+
+### UIView 和 CALayer 的加载
+
+当我们运行某一个使用 ASDK 的工程时,`-[ASDisplayNode _loadViewOrLayerIsLayerBacked:]` 总是 ASDK 中最先被调用的方法,而这个方法执行的原因往往就是 `ASDisplayNode` 对应的 `UIView` 和 `CALayer` 被引用了:
+
+```objectivec
+- (CALayer *)layer {
+ if (!_layer) {
+ ASDisplayNodeAssertMainThread();
+
+ if (!_flags.layerBacked) return self.view.layer;
+ [self _loadViewOrLayerIsLayerBacked:YES];
+ }
+ return _layer;
+}
+
+- (UIView *)view {
+ if (_flags.layerBacked) return nil;
+ if (!_view) {
+ ASDisplayNodeAssertMainThread();
+ [self _loadViewOrLayerIsLayerBacked:NO];
+ }
+ return _view;
+}
+```
+
+这里涉及到一个 ASDK 中比较重要的概念,如果 `ASDisplayNode` 是 `layerBacked` 的,它不会渲染对应的 `UIView` 以此来提升性能:
+
+```objectivec
+- (void)_loadViewOrLayerIsLayerBacked:(BOOL)isLayerBacked {
+ if (isLayerBacked) {
+ _layer = [self _layerToLoad];
+ _layer.delegate = (id
+
+LCD 的成像原理虽然与 CRT 截然不同,每一个像素的颜色可以**在需要改变时**才去改变电压,也就是不需要刷新频率,但是由于一些历史原因,LCD 仍然需要按照一定的刷新频率向 GPU 获取新的图像用于显示。
+
+### 屏幕撕裂
+
+但是显示器只是用于将图像显示在屏幕上,谁又是图像的提供者呢?图像都是我们经常说的 GPU 提供的。
+
+而这导致了另一个问题,由于 GPU 生成图像的频率与显示器刷新的频率是不相关的,那么在显示器刷新时,GPU 没有准备好需要显示的图像怎么办;或者 GPU 的渲染速度过快,显示器来不及刷新,GPU 就已经开始渲染下一帧图像又该如何处理?
+
+
+
+如果解决不了这两个问题,就会出现上图中的*屏幕撕裂*(Screen Tearing)现象,屏幕中一部分显示的是上一帧的内容,另一部分显示的是下一帧的内容。
+
+我们用两个例子来说明可能出现屏幕撕裂的两种情况:
+
++ 如果显示器的刷新频率为 75 Hz,GPU 的渲染速度为 100 Hz,那么在两次屏幕刷新的间隔中,GPU 会渲染 4/3 个帧,后面的 1/3 帧会覆盖已经渲染好的帧栈,最终会导致屏幕在 1/3 或者 2/3 的位置出现屏幕撕裂效果;
++ 那么 GPU 的渲染速度小于显示器呢,比如说 50 Hz,那么在两次屏幕刷新的间隔中,GPU 只会渲染 2/3 帧,剩下的 1/3 会来自上一帧,与上面的结果完全相同,在同样的位置出现撕裂效果。
+
+到这里,有人会说,如果显示器的刷新频率与 GPU 的渲染速度完全相同,应该就会解决屏幕撕裂的问题了吧?其实并不是。显示器从 GPU 拷贝帧的过程依然需要消耗一定的时间,如果屏幕在拷贝图像时刷新,仍然会导致屏幕撕裂问题。
+
+
+
+引入多个缓冲区可以有效地**缓解**屏幕撕裂,也就是同时使用一个*帧缓冲区*(frame buffer)和多个*后备缓冲区*(back buffer);在每次显示器请求内容时,都会从**帧缓冲区**中取出图像然后渲染。
+
+虽然缓冲区可以减缓这些问题,但是却不能解决;如果后备缓冲区绘制完成,而帧缓冲区的图像没有被渲染,后备缓冲区中的图像就会覆盖帧缓冲区,仍然会导致屏幕撕裂。
+
+解决这个问题需要另一个机制的帮助,也就是垂直同步(Vertical synchronization),简称 V-Sync 来解决。
+
+### V-Sync
+
+V-Sync 的主要作用就是保证**只有在帧缓冲区中的图像被渲染之后,后备缓冲区中的内容才可以被拷贝到帧缓冲区中**,理想情况下的 V-Sync 会按这种方式工作:
+
+
+
+每次 V-Sync 发生时,CPU 以及 GPU 都已经完成了对图像的处理以及绘制,显示器可以直接拿到缓冲区中的帧。但是,如果 CPU 或者 GPU 的处理需要的时间较长,就会发生掉帧的问题:
+
+
+
+
+在 V-Sync 信号发出时,CPU 和 GPU 并没有准备好需要渲染的帧,显示器就会继续使用当前帧,这就**加剧**了屏幕的显示问题,而每秒显示的帧数会少于 60。
+
+由于会发生很多次掉帧,在开启了 V-Sync 后,40 ~ 50 FPS 的渲染频率意味着显示器输出的画面帧率会从 60 FPS 急剧下降到 30 FPS,原因在这里不会解释,读者可以自行思考。
+
+其实到这里关于屏幕渲染的内容就已经差不多结束了,根据 V-Sync 的原理,优化应用性能、提高 App 的 FPS 就可以从两个方面来入手,优化 CPU 以及 GPU 的处理时间。
+
+> 读者也可以从 [iOS 保持界面流畅的技巧](http://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/)这篇文章中了解更多的相关内容。
+
+## 性能调优的策略
+
+CPU 和 GPU 在每次 V-Sync 时间点到达之前都在干什么?如果,我们知道了它们各自负责的工作,通过优化代码就可以提升性能。
+
+
+
+很多 CPU 的操作都会延迟 GPU 开始渲染的时间:
+
++ 布局的计算 - 如果你的视图层级太过于复杂,或者视图需要重复多次进行布局,尤其是在使用 Auto Layout 进行自动布局时,对性能影响尤为严重;
++ 视图的惰性加载 - 在 iOS 中只有当视图控制器的视图显示到屏幕时才会加载;
++ 解压图片 - iOS 通常会在真正绘制时才会解码图片,对于一个较大的图片,无论是直接或间接使用 `UIImageView` 或者绘制到 Core Graphics 中,都需要对图片进行解压;
++ ...
+
+宽泛的说,大多数的 `CALayer` 的属性都是由 GPU 来绘制的,比如图片的圆角、变换、应用纹理;但是过多的几何结构、重绘、离屏绘制(Offscrren)以及过大的图片都会导致 GPU 的性能明显降低。
+
+> 上面的内容出自 [CPU vs GPU · iOS 核心动画高级技巧](https://zsisme.gitbooks.io/ios-/content/chapter12/cpu-versus-gpu.html),你可以在上述文章中对 CPU 和 GPU 到底各自做了什么有一个更深的了解。
+
+也就是说,如果我们解决了上述问题,就能加快应用的渲染速度,大大提升用户体验。
+
+## AsyncDisplayKit
+
+文章的前半部分已经从屏幕的渲染原理讲到了性能调优的几个策略;而 [AsyncDisplayKit](http://asyncdisplaykit.org) 就根据上述的策略帮助我们对应用性能进行优化。
+
+
+
+AsyncDisplayKit(以下简称 ASDK)是由 Facebook 开源的一个 iOS 框架,能够帮助最复杂的 UI 界面保持流畅和快速响应。
+
+ASDK 从开发到开源大约经历了一年多的时间,它其实并不是一个简单的框架~~它是一个复杂的框架~~,更像是对 UIKit 的重新实现,把整个 UIKit 以及 CALayer 层封装成一个一个 `Node`,**将昂贵的渲染、图片解码、布局以及其它 UI 操作移出主线程**,这样主线程就可以对用户的操作及时做出反应。
+
+很多分析 ASDK 的文章都会有这么一张图介绍框架中的最基本概念:
+
+
+
+在 ASDK 中最基本的单位就是 `ASDisplayNode`,每一个 node 都是对 `UIView` 以及 `CALayer` 的抽象。但是与 `UIView` 不同的是,`ASDisplayNode` 是线程安全的,它可以在后台线程中完成初始化以及配置工作。
+
+如果按照 60 FPS 的刷新频率来计算,每一帧的渲染时间只有 16ms,在 16ms 的时间内要完成对 `UIView` 的创建、布局、绘制以及渲染,CPU 和 GPU 面临着巨大的压力。
+
+
+
+但是从 A5 处理器之后,多核的设备成为了主流,原有的将所有操作放入主线程的实践已经不能适应复杂的 UI 界面,所以 **ASDK 将耗时的 CPU 操作以及 GPU 渲染纹理(Texture)的过程全部放入后台进程,使主线程能够快速响应用户操作**。
+
+ASDK 通过独特的渲染技巧、代替 AutoLayout 的布局系统、智能的预加载方式等模块来实现对 App 性能的优化。
+
+## ASDK 的渲染过程
+
+ASDK 中到底使用了哪些方法来对视图进行渲染呢?本文主要会从渲染的过程开始分析,了解 ASDK 底层如何提升界面的渲染性能。
+
+在 ASDK 中的渲染围绕 `ASDisplayNode` 进行,其过程总共有四条主线:
+
++ 初始化 `ASDisplayNode` 对应的 `UIView` 或者 `CALayer`;
++ 在当前视图进入视图层级时执行 `setNeedsDisplay`;
++ `display` 方法执行时,向后台线程派发绘制事务;
++ 注册成为 `RunLoop` 观察者,在每个 `RunLoop` 结束时回调。
+
+### UIView 和 CALayer 的加载
+
+当我们运行某一个使用 ASDK 的工程时,`-[ASDisplayNode _loadViewOrLayerIsLayerBacked:]` 总是 ASDK 中最先被调用的方法,而这个方法执行的原因往往就是 `ASDisplayNode` 对应的 `UIView` 和 `CALayer` 被引用了:
+
+```objectivec
+- (CALayer *)layer {
+ if (!_layer) {
+ ASDisplayNodeAssertMainThread();
+
+ if (!_flags.layerBacked) return self.view.layer;
+ [self _loadViewOrLayerIsLayerBacked:YES];
+ }
+ return _layer;
+}
+
+- (UIView *)view {
+ if (_flags.layerBacked) return nil;
+ if (!_view) {
+ ASDisplayNodeAssertMainThread();
+ [self _loadViewOrLayerIsLayerBacked:NO];
+ }
+ return _view;
+}
+```
+
+这里涉及到一个 ASDK 中比较重要的概念,如果 `ASDisplayNode` 是 `layerBacked` 的,它不会渲染对应的 `UIView` 以此来提升性能:
+
+```objectivec
+- (void)_loadViewOrLayerIsLayerBacked:(BOOL)isLayerBacked {
+ if (isLayerBacked) {
+ _layer = [self _layerToLoad];
+ _layer.delegate = (id
+
+如上图所示 ASDK 把正在滚动的 ` ASTableView/ASCollectionView` 划分为三种状态:
+
++ Fetch Data
++ Display
++ Visible
+
+上面的这三种状态都是由 ASDK 来管理的,而每一个 `ASCellNode` 的状态都是由 `ASRangeController` 控制,所有的状态都对应一个 `ASInterfaceState`:
+
++ `ASInterfaceStatePreload` 当前元素貌似要显示到屏幕上,需要从磁盘或者网络请求数据;
++ `ASInterfaceStateDisplay` 当前元素非常可能要变成可见的,需要进行异步绘制;
++ `ASInterfaceStateVisible` 当前元素最少在屏幕上显示了 1px
+
+当用户滚动当前视图时,`ASRangeController` 就会修改不同区域内元素的状态:
+
+
+
+上图是用户在向下滑动时,`ASCellNode` 是如何被标记的,假设**当前视图可见的范围高度为 1**,那么在默认情况下,五个区域会按照上图的形式进行划分:
+
+| Buffer | Size |
+| :-: | :-: |
+| Fetch Data Leading Buffer | 2 |
+| Display Leading Buffer | 1 |
+| Visible | 1 |
+| Display Trailing Buffer | 1 |
+| Fetch Data Trailing Buffer | 1 |
+
+在滚动方向(Leading)上 Fetch Data 区域会是非滚动方向(Trailing)的两倍,ASDK 会根据滚动方向的变化实时改变缓冲区的位置;在向下滚动时,下面的 Fetch Data 区域就是上面的两倍,向上滚动时,上面的 Fetch Data 区域就是下面的两倍。
+
+> 这里的两倍并不是一个确定的数值,ASDK 会根据当前设备的不同状态,改变不同区域的大小,但是**滚动方向的区域总会比非滚动方向大一些**。
+
+智能预加载能够根据当前的滚动方向,自动改变当前的工作区域,选择合适的区域提前触发请求资源、渲染视图以及异步布局等操作,让视图的滚动达到真正的流畅。
+
+#### 原理
+
+在 ASDK 中整个智能预加载的概念是由三个部分来统一协调管理的:
+
++ `ASRangeController`
++ `ASDataController`
++ `ASTableView` 与 `ASTableNode`
+
+对智能预加载实现的分析,也是根据这三个部分来介绍的。
+
+#### 工作区域的管理
+
+`ASRangeController` 是 `ASTableView` 以及 `ASCollectionView` 内部使用的控制器,主要用于监控视图的可见区域、维护工作区域、触发网络请求以及绘制、单元格的异步布局。
+
+以 `ASTableView` 为例,在视图进行滚动时,会触发 `-[UIScrollView scrollViewDidScroll:]` 代理方法:
+
+```objectivec
+- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
+ ASInterfaceState interfaceState = [self interfaceStateForRangeController:_rangeController];
+ if (ASInterfaceStateIncludesVisible(interfaceState)) {
+ [_rangeController updateCurrentRangeWithMode:ASLayoutRangeModeFull];
+ }
+ ...
+}
+```
+
+> 每一个 `ASTableView` 的实例都持有一个 `ASRangeController` 以及 `ASDataController` 用于管理工作区域以及数据更新。
+
+ASRangeController 最重要的私有方法 `-[ASRangeController _updateVisibleNodeIndexPaths]` 一般都是因为上面的方法间接调用的:
+
+```objectivec
+-[ASRangeController updateCurrentRangeWithMode:]
+ -[ASRangeController setNeedsUpdate]
+ -[ASRangeController updateIfNeeded]
+ -[ASRangeController _updateVisibleNodeIndexPaths]
+```
+
+调用栈中间的过程其实并不重要,最后的私有方法的主要工作就是计算不同区域内 Cell 的 `NSIndexPath` 数组,然后更新对应 Cell 的状态 `ASInterfaceState` 触发对应的操作。
+
+我们将这个私有方法的实现分开来看:
+
+```objectivec
+- (void)_updateVisibleNodeIndexPaths {
+ NSArray
+
+
在前两篇文章中,我们已经对 iOS 中的 Model 层以及 View 层进行了分析,划分出了它们的具体职责,其中 Model 层除了负责数据的持久存储、缓存工作,还要负责所有 HTTP... »
+ > 所有继承自 `NSObject` 的类实例化后的对象都会包含一个类型为 `isa_t` 的结构体。 @@ -40,14 +39,12 @@ struct objc_class : objc_object { 当**实例方法**被调用时,它要通过自己持有的 `isa` 来查找对应的类,然后在这里的 `class_data_bits_t` 结构体中查找对应方法的实现。同时,每一个 `objc_class` 也有一个**指向自己的父类的指针** `super_class` 用来查找继承的方法。 -> 关于如何在 `class_data_bits_t` 中查找对应方法会在之后的文章中讲到。这里只需要知道,它会在这个结构体中查找到对应方法的实现就可以了。[深入解析 ObjC 中方法的结构](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/深入解析%20ObjC%20中方法的结构.md) +> 关于如何在 `class_data_bits_t` 中查找对应方法会在之后的文章中讲到。这里只需要知道,它会在这个结构体中查找到对应方法的实现就可以了。[深入解析 ObjC 中方法的结构](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/深入解析%20ObjC%20中方法的结构.md) -
 但是,这样就有一个问题,类方法的实现又是如何查找并且调用的呢?这时,就需要引入*元类*来保证无论是类还是对象都能**通过相同的机制查找方法的实现**。 -
 @@ -58,7 +55,6 @@ struct objc_class : objc_object { 下面这张图介绍了对象,类与元类之间的关系,笔者认为已经觉得足够清晰了,所以不在赘述。 -
 > 图片来自 [objc_explain_Classes_and_metaclasses](http://www.sealiesoftware.com/blog/archive/2009/04/14/objc_explain_Classes_and_metaclasses.html) @@ -111,7 +107,6 @@ union isa_t { `isa_t` 是一个 `union` 类型的结构体,对 `union` 不熟悉的读者可以看这个 stackoverflow 上的[回答](http://stackoverflow.com/questions/252552/why-do-we-need-c-unions). 也就是说其中的 `isa_t`、`cls`、 `bits` 还有结构体共用同一块地址空间。而 `isa` 总共会占据 64 位的内存空间(决定于其中的结构体) -
 ```objectivec @@ -173,7 +168,6 @@ inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor) 我们可以把它转换成二进制的数据,然后看一下哪些属性对应的位被这行代码初始化了(标记为红色): -
 从图中了解到,在使用 `ISA_MAGIC_VALUE` 设置 `isa_t` 结构体之后,实际上只是设置了 `indexed` 以及 `magic` 这两部分的值。 @@ -224,7 +218,6 @@ inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor) isa.has_cxx_dtor = hasCxxDtor; ``` -
 ### `shiftcls` @@ -235,14 +228,13 @@ isa.has_cxx_dtor = hasCxxDtor; isa.shiftcls = (uintptr_t)cls >> 3; ``` -> **将当前地址右移三位的主要原因是用于将 Class 指针中无用的后三位清楚减小内存的消耗,因为类的指针要按照字节(8 bits)对齐内存,其指针后三位都是没有意义的 0**。 +> **将当前地址右移三位的主要原因是用于将 Class 指针中无用的后三位清除减小内存的消耗,因为类的指针要按照字节(8 bits)对齐内存,其指针后三位都是没有意义的 0**。 > > 绝大多数机器的架构都是 [byte-addressable](https://en.wikipedia.org/wiki/Byte_addressing) 的,但是对象的内存地址必须对齐到字节的倍数,这样可以提高代码运行的性能,在 iPhone5s 中虚拟地址为 33 位,所以用于对齐的最后三位比特为 `000`,我们只会用其中的 30 位来表示对象的地址。 而 ObjC 中的类指针的地址后三位也为 0,在 `_class_createInstanceFromZone` 方法中打印了调用这个方法传入的类指针: -
 可以看到,这里打印出来的**所有类指针十六进制地址的最后一位都为 8 或者 0**。也就是说,类指针的后三位都为 0,所以,我们在上面存储 `Class` 指针时右移三位是没有问题的。 @@ -253,7 +245,6 @@ isa.shiftcls = (uintptr_t)cls >> 3; 如果再尝试打印对象指针的话,会发现所有对象内存地址的**后四位**都是 0,说明 ObjC 在初始化内存时是以 16 个字节对齐的, 分配的内存地址后四位都是 0。 -
 > 使用整个指针大小的内存来存储 `isa` 指针有些浪费,尤其在 64 位的 CPU 上。在 `ARM64` 运行的 iOS 只使用了 33 位作为指针(与结构体中的 33 位无关,Mac OS 上为 47 位),而剩下的 31 位用于其它目的。类的指针也同样根据字节对齐了,每一个类指针的地址都能够被 8 整除,也就是使最后 3 bits 为 0,为 `isa` 留下 34 位用于性能的优化。 @@ -263,7 +254,6 @@ isa.shiftcls = (uintptr_t)cls >> 3; 我尝试运行了下面的代码将 `NSObject` 的类指针和对象的 `isa` 打印出来,具体分析一下 -
 ``` diff --git "a/objc/\344\273\216\346\272\220\344\273\243\347\240\201\347\234\213 ObjC \344\270\255\346\266\210\346\201\257\347\232\204\345\217\221\351\200\201.md" "b/contents/objc/\344\273\216\346\272\220\344\273\243\347\240\201\347\234\213 ObjC \344\270\255\346\266\210\346\201\257\347\232\204\345\217\221\351\200\201.md" similarity index 96% rename from "objc/\344\273\216\346\272\220\344\273\243\347\240\201\347\234\213 ObjC \344\270\255\346\266\210\346\201\257\347\232\204\345\217\221\351\200\201.md" rename to "contents/objc/\344\273\216\346\272\220\344\273\243\347\240\201\347\234\213 ObjC \344\270\255\346\266\210\346\201\257\347\232\204\345\217\221\351\200\201.md" index 7af5783..24f8734 100644 --- "a/objc/\344\273\216\346\272\220\344\273\243\347\240\201\347\234\213 ObjC \344\270\255\346\266\210\346\201\257\347\232\204\345\217\221\351\200\201.md" +++ "b/contents/objc/\344\273\216\346\272\220\344\273\243\347\240\201\347\234\213 ObjC \344\270\255\346\266\210\346\201\257\347\232\204\345\217\221\351\200\201.md" @@ -10,10 +10,10 @@ 2. `[receiver message]` 会被翻译为 `objc_msgSend(receiver, @selector(message))` 3. 在消息的响应链中**可能**会调用 `- resolveInstanceMethod:` `- forwardInvocation:` 等方法 4. 关于选择子 SEL 的知识 - + > 如果对于上述的知识不够了解,可以看一下这篇文章 [Objective-C Runtime](http://tech.glowing.com/cn/objective-c-runtime/),但是其中关于 `objc_class` 的结构体的代码已经过时了,不过不影响阅读以及理解。 -5. 方法在内存中存储的位置,[深入解析 ObjC 中方法的结构](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/深入解析%20ObjC%20中方法的结构.md)(可选) +5. 方法在内存中存储的位置,[深入解析 ObjC 中方法的结构](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/深入解析%20ObjC%20中方法的结构.md)(可选) > 文章中不会刻意区别方法和函数、消息传递和方法调用之间的区别。 @@ -26,7 +26,6 @@ 由于这个系列的文章都是对 Objective-C 源代码的分析,所以会**从 Objective-C 源代码中分析并合理地推测一些关于消息传递的问题**。 -
 ## 关于 @selector() 你需要知道的 @@ -87,7 +86,6 @@ int main(int argc, const char * argv[]) { 在主函数任意位置打一个断点, 比如 `-> [object hello];` 这里,然后在 lldb 中输入: -
 这里面我们打印了两个选择子的地址` @selector(hello)` 以及 `@selector(undefined_hello_method)`,需要注意的是: @@ -96,7 +94,6 @@ int main(int argc, const char * argv[]) { 如果我们修改程序的代码: -
 在这里,由于我们在代码中显示地写出了 `@selector(undefined_hello_method)`,所以在 lldb 中再次打印这个 `sel` 内存地址跟之前相比有了很大的改变。 @@ -111,7 +108,6 @@ int main(int argc, const char * argv[]) { 在运行时初始化之前,打印 `hello` 选择子的的内存地址: -
 ## message.h 文件 @@ -119,10 +115,10 @@ int main(int argc, const char * argv[]) { Objective-C 中 `objc_msgSend` 的实现并没有开源,它只存在于 `message.h` 这个头文件中。 ```objectivec -/** +/** * @note When it encounters a method call, the compiler generates a call to one of the * functions \c objc_msgSend, \c objc_msgSend_stret, \c objc_msgSendSuper, or \c objc_msgSendSuper_stret. - * Messages sent to an object’s superclass (using the \c super keyword) are sent using \c objc_msgSendSuper; + * Messages sent to an object’s superclass (using the \c super keyword) are sent using \c objc_msgSendSuper; * other messages are sent using \c objc_msgSend. Methods that have data structures as return values * are sent using \c objc_msgSendSuper_stret and \c objc_msgSend_stret. */ @@ -177,7 +173,6 @@ int main(int argc, const char * argv[]) { 在调用 `hello` 方法的这一行打一个断点,当我们尝试进入(Step in)这个方法只会直接跳入这个方法的实现,而不会进入 `objc_msgSend`: -
 因为 `objc_msgSend` 是一个私有方法,我们没有办法进入它的实现,但是,我们却可以在 `objc_msgSend` 的调用栈中“截下”这个函数调用的过程。 @@ -188,7 +183,6 @@ int main(int argc, const char * argv[]) { 在 `objc-runtime-new.mm` 文件中有一个函数 `lookUpImpOrForward`,这个函数的作用就是查找方法的实现,于是运行程序,在运行到 `hello` 这一行时,激活 `lookUpImpOrForward` 函数中的断点。 -
> 由于转成 gif 实在是太大了,笔者试着用各种方法生成动图,然而效果也不是很理想,只能贴一个 Youtube 的视频链接,不过对于能够翻墙的开发者们,应该也不是什么问题吧(手动微笑)
@@ -203,7 +197,6 @@ int main(int argc, const char * argv[]) {
在 `-> [object hello]` 这里增加一个断点,**当程序运行到这一行时**,再向 `lookUpImpOrForward` 函数的第一行添加断点,确保是捕获 `@selector(hello)` 的调用栈,而不是调用其它选择子的调用栈。
-
 由图中的变量区域可以了解,传入的选择子为 `"hello"`,对应的类是 `XXObject`。所以我们可以确信这就是当调用 `hello` 方法时执行的函数。在 Xcode 左侧能看到方法的调用栈: @@ -221,7 +214,7 @@ int main(int argc, const char * argv[]) { ```objectivec IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) { - return lookUpImpOrForward(cls, sel, obj, + return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/); } ``` @@ -291,12 +284,10 @@ imp = cache_getImp(cls, sel); if (imp) goto done; ``` -
 不过 `cache_getImp` 的实现目测是不开源的,同时也是汇编写的,在我们尝试 step in 的时候进入了如下的汇编代码。 -
 它会进入一个 `CacheLookup` 的标签,获取实现,使用汇编的原因还是因为要加速整个实现查找的过程,其原理推测是在类的 `cache` 中寻找对应的实现,只是做了一些性能上的优化。 @@ -316,8 +307,8 @@ if (meth) { ```objectivec static method_t *getMethodNoSuper_nolock(Class cls, SEL sel) { - for (auto mlists = cls->data()->methods.beginLists(), - end = cls->data()->methods.endLists(); + for (auto mlists = cls->data()->methods.beginLists(), + end = cls->data()->methods.endLists(); mlists != end; ++mlists) { @@ -336,7 +327,7 @@ static method_t *search_method_list(const method_list_t *mlist, SEL sel) { int methodListIsFixedUp = mlist->isFixedUp(); int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t); - + if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) { return findMethodInSortedMethodList(sel, mlist); } else { @@ -434,11 +425,11 @@ void _class_resolveMethod(Class cls, SEL sel, id inst) { if (! cls->isMetaClass()) { _class_resolveInstanceMethod(cls, sel, inst); - } + } else { _class_resolveClassMethod(cls, sel, inst); - if (!lookUpImpOrNil(cls, sel, inst, - NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) + if (!lookUpImpOrNil(cls, sel, inst, + NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { _class_resolveInstanceMethod(cls, sel, inst); } @@ -450,7 +441,7 @@ void _class_resolveMethod(Class cls, SEL sel, id inst) ```objectivec static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst) { - if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, + if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { // 没有找到 resolveInstanceMethod: 方法,直接返回。 return; @@ -460,7 +451,7 @@ static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst) { bool resolved = msg(cls, SEL_resolveInstanceMethod, sel); // 缓存结果,以防止下次在调用 resolveInstanceMethod: 方法影响性能。 - IMP imp = lookUpImpOrNil(cls, sel, inst, + IMP imp = lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/); } ``` @@ -486,7 +477,6 @@ cache_fill(cls, sel, imp, inst); 这样就结束了整个方法第一次的调用过程,缓存没有命中,但是在当前类的方法列表中找到了 `hello` 方法的实现,调用了该方法。 -
 @@ -507,7 +497,6 @@ int main(int argc, const char * argv[]) { 然后在第二次调用 `hello` 方法时,加一个断点: -
 `objc_msgSend` 并没有走 `lookupImpOrForward` 这个方法,而是直接结束,打印了另一个 `hello` 字符串。 @@ -518,7 +507,6 @@ int main(int argc, const char * argv[]) { 好,现在重新运行程序至第二个 `hello` 方法调用之前: -
 打印缓存中 bucket 的内容: @@ -568,12 +556,10 @@ int main(int argc, const char * argv[]) { } ``` -
 这样 `XXObject` 中就不存在 `hello` 方法对应实现的缓存了。然后继续运行程序: -
 虽然第二次调用 `hello` 方法,但是因为我们清除了 `hello` 的缓存,所以,会再次进入 `lookupImpOrForward` 方法。 @@ -604,12 +590,10 @@ int main(int argc, const char * argv[]) { 在第一个 `hello` 方法调用之前将实现加入缓存: -
 然后继续运行代码: -
 可以看到,我们虽然没有改变 `hello` 方法的实现,但是在 **objc_msgSend** 的消息发送链路中,使用错误的缓存实现 `cached_imp` 拦截了实现的查找,打印出了 `Cached Hello`。 @@ -625,7 +609,7 @@ int main(int argc, const char * argv[]) { 这篇文章与其说是讲 ObjC 中的消息发送的过程,不如说是讲方法的实现是如何查找的。 Objective-C 中实现查找的路径还是比较符合直觉的: - + 1. 缓存命中 2. 查找当前类的缓存及方法 3. 查找父类的缓存及方法 @@ -636,10 +620,8 @@ Objective-C 中实现查找的路径还是比较符合直觉的: ## 参考资料 -+ [深入解析 ObjC 中方法的结构](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/深入解析%20ObjC%20中方法的结构.md) ++ [深入解析 ObjC 中方法的结构](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/深入解析%20ObjC%20中方法的结构.md) + [Objective-C Runtime](http://tech.glowing.com/cn/objective-c-runtime/) + [Let's Build objc_msgSend](https://www.mikeash.com/pyblog/friday-qa-2012-11-16-lets-build-objc_msgsend.html) Follow: [@Draveness](https://github.com/Draveness) - - diff --git "a/objc/\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243 load \346\226\271\346\263\225\344\271\210\357\274\237.md" "b/contents/objc/\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243 load \346\226\271\346\263\225\344\271\210\357\274\237.md" similarity index 96% rename from "objc/\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243 load \346\226\271\346\263\225\344\271\210\357\274\237.md" rename to "contents/objc/\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243 load \346\226\271\346\263\225\344\271\210\357\274\237.md" index a8135eb..60dd61e 100644 --- "a/objc/\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243 load \346\226\271\346\263\225\344\271\210\357\274\237.md" +++ "b/contents/objc/\344\275\240\347\234\237\347\232\204\344\272\206\350\247\243 load \346\226\271\346\263\225\344\271\210\357\274\237.md" @@ -52,7 +52,7 @@ int main(int argc, const char * argv[]) { 代码总共只实现了一个 `XXObject` 的 `+ load` 方法,主函数中也没有任何的东西: -
 + 虽然在主函数中什么方法都没有调用,但是运行之后,依然打印了 `XXObject load` 字符串,也就是说调用了 `+ load` 方法。 @@ -62,13 +62,13 @@ int main(int argc, const char * argv[]) { > 注意这里 `+` 和 `[` 之间没有空格 -
 + > 为什么要加一个符号断点呢?因为这样看起来比较高级。 重新运行程序。这时,代码会停在 `NSLog(@"XXObject load");` 这一行的实现上: -
 + 左侧的调用栈很清楚的告诉我们,哪些方法被调用了: @@ -128,7 +128,7 @@ load_images(enum dyld_image_states state, uint32_t infoCount, 这里就会遇到一个问题:镜像到底是什么,我们用一个断点打印出所有加载的镜像: -
 + 从控制台输出的结果大概就是这样的,我们可以看到镜像并不是一个 Objective-C 的代码文件,它应该是一个 target 的编译产物。 @@ -160,7 +160,7 @@ load_images(enum dyld_image_states state, uint32_t infoCount, 但是如果进入最下面的这个目录,会发现它是一个**可执行文件**,它的运行结果与 Xcode 中的运行结果相同: -
 + ### 准备 + load 方法 @@ -267,7 +267,7 @@ void call_load_methods(void) 方法的调用流程大概是这样的: -
 + 其中 `call_class_loads` 会从一个待加载的类列表 `loadable_classes` 中寻找对应的类,然后找到 `@selector(load)` 的实现并执行。 @@ -310,7 +310,7 @@ ObjC 对于加载的管理,主要使用了两个列表,分别是 `loadable_c 方法的调用过程也分为两个部分,准备 `load` 方法和调用 `load` 方法,我更觉得这两个部分比较像生产者与消费者: -

+
`add_class_to_loadable_list` 方法负责将类加入 `loadable_classes` 集合,而 `call_class_loads` 负责消费集合中的元素。
diff --git "a/contents/objc/\345\205\263\350\201\224\345\257\271\350\261\241 AssociatedObject \345\256\214\345\205\250\350\247\243\346\236\220.md" "b/contents/objc/\345\205\263\350\201\224\345\257\271\350\261\241 AssociatedObject \345\256\214\345\205\250\350\247\243\346\236\220.md"
new file mode 100644
index 0000000..059cd09
--- /dev/null
+++ "b/contents/objc/\345\205\263\350\201\224\345\257\271\350\261\241 AssociatedObject \345\256\214\345\205\250\350\247\243\346\236\220.md"
@@ -0,0 +1,588 @@
+# 关联对象 AssociatedObject 完全解析
+
+我们在 iOS 开发中经常需要使用分类(Category),为已经存在的类添加属性的需求,但是使用 `@property` 并不能在分类中**正确**创建实例变量和存取方法。
+
+不过,通过 Objective-C 运行时中的关联对象,也就是 Associated Object,我们可以实现上述需求。
+
+## 写在前面
+
+这篇文章包含了两方面的内容:
+
++ [使用关联对象为已经存在的类中添加属性](#关联对象的应用)
++ [关联对象在底层 Objective-C 中的实现](#关联对象的实现)
+
+> 注:如果你刚刚入门 iOS 开发,笔者相信了解第一部分的内容会对你的日常开发中有所帮助,不过第二部分的内容可能有些难以理解。
+>
+> 如果你对关联对象的使用非常熟悉,可以直接跳过第一部分的内容,从[这里](#关联对象的实现)开始深入了解其底层实现。
+
+## 关联对象的应用
+
+关于关联对象的使用相信已经成为了一个老生常谈的问题了,不过为了保证这篇文章的完整性,笔者还是会在这里为各位介绍这部分的内容的。
+
+### 分类中的 @property
+
+`@property` 可以说是一个 Objective-C 编程中的“宏”,它有[元编程](https://zh.wikipedia.org/zh/元编程)的思想。
+
+```objectivec
+@interface DKObject : NSObject
+
+@property (nonatomic, strong) NSString *property;
+
+@end
+```
+
+在使用上述代码时会做三件事:
+
++ 生成实例变量 `_property`
++ 生成 `getter` 方法 `- property`
++ 生成 `setter` 方法 `- setProperty:`
+
+```objectivec
+@implementation DKObject {
+ NSString *_property;
+}
+
+- (NSString *)property {
+ return _property;
+}
+
+- (void)setProperty:(NSString *)property {
+ _property = property;
+}
+
+@end
+```
+
+这些代码都是编译器为我们生成的,虽然你看不到它,但是它确实在这里,我们既然可以在类中使用 `@property` 生成一个属性,那么为什么在分类中不可以呢?
+
+我们来做一个小实验:创建一个 `DKObject` 的分类 `Category`,并添加一个属性 `categoryProperty`:
+
+```objectivec
+@interface DKObject (Category)
+
+@property (nonatomic, strong) NSString *categoryProperty;
+
+@end
+```
+
+看起来还是很不错的,不过 Build 一下这个 Demo,会发现有这么一个警告:
+
+
+
+在这里的警告告诉我们 `categoryProperty` 属性的存取方法需要自己手动去实现,或者使用 `@dynamic` 在运行时实现这些方法。
+
+换句话说,分类中的 `@property` 并没有为我们生成实例变量以及存取方法,而需要我们手动实现。
+
+### 使用关联对象
+
+Q:我们为什么要使用关联对象?
+
+A:因为在分类中 `@property` 并不会自动生成实例变量以及存取方法,所以**一般使用关联对象为已经存在的类添加『属性』**。
+
+上一小节的内容已经给了我们需要使用关联对象的理由。在这里,我们会介绍 ObjC 运行时为我们提供的与关联对象有关的 API,并在分类中实现一个**伪属性**:
+
+```objectivec
+#import "DKObject+Category.h"
+#import