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 d107b5f..c4ee9dd 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,51 @@ -# iOS-Source-Code-Analyze +# Analyze

- + Banner designed by Levine

## 为什么要建这个仓库 +欢迎使用 RSS 订阅我的博客 [点击订阅](http://draveness.me/feed.xml) + 世人都说阅读开源框架的源代码对于功力有显著的提升,所以我也尝试阅读开源框架的源代码,并对其内容进行详细地分析和理解。在这里将自己阅读开源框架源代码的心得记录下来,希望能对各位开发者有所帮助。我会不断更新这个仓库中的文章,如果想要关注可以点 `star`。 ## 目录 -> Latest:[如何在 Objective-C 的环境下实现 defer](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/libextobjc/如何在%20Objective-C%20的环境下实现%20defer.md) +Latest: + ++ [谈谈 MVX 中的 Model](contents/architecture/mvx-model.md) ++ [谈谈 MVX 中的 View](contents/architecture/mvx-view.md) ++ [谈谈 MVX 中的 Controller](contents/architecture/mvx-controller.md) ++ [浅谈 MVC、MVP 和 MVVM 架构模式](contents/architecture/mvx.md) | Project | Version | Article | |:-------:|:-------:|:------| -| libextobjc | |[如何在 Objective-C 的环境下实现 defer](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/libextobjc/如何在%20Objective-C%20的环境下实现%20defer.md) | -| IQKeyboardManager | 4.0.3 |[『零行代码』解决键盘遮挡问题(iOS)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/IQKeyboardManager/『零行代码』解决键盘遮挡问题(iOS).md) | -| ObjC | | [从 NSObject 的初始化了解 isa](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/从%20NSObject%20的初始化了解%20isa.md)
[深入解析 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/objc/从源代码看%20ObjC%20中消息的发送.md)
[你真的了解 load 方法么?](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/你真的了解%20load%20方法么?.md)
[上古时代 Objective-C 中哈希表的实现](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/上古时代%20Objective-C%20中哈希表的实现.md)
[自动释放池的前世今生](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/自动释放池的前世今生.md)
[黑箱中的 retain 和 release](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/黑箱中的%20retain%20和%20release.md)
[关联对象 AssociatedObject 完全解析](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/关联对象%20AssociatedObject%20完全解析.md)| -| DKNightVersion | 2.3.0 | [成熟的夜间模式解决方案](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/DKNightVersion/成熟的夜间模式解决方案.md) | -| AFNetworking | 3.0.4 | [AFNetworking 概述(一)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/AFNetworking%20概述(一).md)
[AFNetworking 的核心 AFURLSessionManager(二)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/AFNetworking%20的核心%20AFURLSessionManager(二).md)
[处理请求和响应 AFURLSerialization(三)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/处理请求和响应%20AFURLSerialization(三).md)
[AFNetworkReachabilityManager 监控网络状态(四)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/AFNetworkReachabilityManager%20监控网络状态(四).md)
[验证 HTTPS 请求的证书(五)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/验证%20HTTPS%20请求的证书(五).md) | -| BlocksKit | 2.2.5 | [神奇的 BlocksKit(一)遍历、KVO 和分类](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/BlocksKit/神奇的%20BlocksKit%20(一).md)
[神奇的 BlocksKit(二)动态代理的实现 ](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/BlocksKit/神奇的%20BlocksKit%20(二).md) | -| Alamofire | | [iOS 源代码分析 --- Alamofire](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/Alamofire/iOS%20源代码分析%20----%20Alamofire.md) | -| SDWebImage | | [iOS 源代码分析 --- SDWebImage](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/SDWebImage/iOS%20源代码分析%20---%20SDWebImage.md) | -| MBProgressHUD | | [iOS 源代码分析 --- MBProgressHUD](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/MBProgressHUD/iOS%20源代码分析%20---%20MBProgressHUD.md) | -| Masonry | | [iOS 源代码分析 --- Masonry](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/Masonry/iOS%20源代码分析%20---%20Masonry.md) | - +| Architecture | | [谈谈 MVX 中的 Model](contents/architecture/mvx-model.md)
[谈谈 MVX 中的 View](contents/architecture/mvx-view.md)
[谈谈 MVX 中的 Controller](contents/architecture/mvx-controller.md)
[浅谈 MVC、MVP 和 MVVM 架构模式](contents/architecture/mvx.md) | +| ReactiveObjC | 2.1.2 | [『状态』驱动的世界:ReactiveCocoa](contents/ReactiveObjC/RACSignal.md)
[Pull-Driven 的数据流 RACSequence](contents/ReactiveObjC/RACSequence.md)
[『可变』的热信号 RACSubject](contents/ReactiveObjC/RACSubject.md)
[优雅的 RACCommand](contents/ReactiveObjC/RACCommand.md)
[用于多播的 RACMulticastConnection](contents/ReactiveObjC/RACMulticastConnection.md)
[RAC 中的双向数据绑定 RACChannel](contents/ReactiveObjC/RACChannel.md)
[理解 RACScheduler 的实现](contents/ReactiveObjC/RACScheduler.md)
[从代理到 RACSignal](contents/ReactiveObjC/RACDelegateProxy.md)| +| ObjC | | [从 NSObject 的初始化了解 isa](contents/objc/从%20NSObject%20的初始化了解%20isa.md)
[深入解析 ObjC 中方法的结构](contents/objc/深入解析%20ObjC%20中方法的结构.md)
[从源代码看 ObjC 中消息的发送](contents/objc/从源代码看%20ObjC%20中消息的发送.md)
[你真的了解 load 方法么?](contents/objc/你真的了解%20load%20方法么?.md)
[上古时代 Objective-C 中哈希表的实现](contents/objc/上古时代%20Objective-C%20中哈希表的实现.md)
[自动释放池的前世今生](contents/objc/自动释放池的前世今生.md)
[黑箱中的 retain 和 release](contents/objc/黑箱中的%20retain%20和%20release.md)
[关联对象 AssociatedObject 完全解析](contents/objc/关联对象%20AssociatedObject%20完全解析.md)
[懒惰的 initialize 方法](contents/objc/懒惰的%20initialize%20方法.md)
[对象是如何初始化的(iOS)](contents/objc/对象是如何初始化的(iOS).md)| +| KVOController | 1.2.0 | [如何优雅地使用 KVO](contents/KVOController/KVOController.md) | +| AsyncDisplayKit | 1.9.81 | [提升 iOS 界面的渲染性能](contents/AsyncDisplayKit/提升%20iOS%20界面的渲染性能%20.md)
[从 Auto Layout 的布局算法谈性能](contents/AsyncDisplayKit/从%20Auto%20Layout%20的布局算法谈性能.md)
[预加载与智能预加载(iOS)](contents/AsyncDisplayKit/预加载与智能预加载(iOS).md)| +| CocoaPods | 1.1.0 | [CocoaPods 都做了什么?](contents/CocoaPods/CocoaPods%20都做了什么?.md)
[谈谈 DSL 以及 DSL 的应用(以 CocoaPods 为例)](contents/CocoaPods/谈谈%20DSL%20以及%20DSL%20的应用(以%20CocoaPods%20为例).md)| +| OHHTTPStubs | 5.1.0 | [iOS 开发中使用 NSURLProtocol 拦截 HTTP 请求](contents/OHHTTPStubs/iOS%20开发中使用%20NSURLProtocol%20拦截%20HTTP%20请求.md)
[如何进行 HTTP Mock(iOS)](contents/OHHTTPStubs/如何进行%20HTTP%20Mock(iOS).md) | +| ProtocolKit | | [如何在 Objective-C 中实现协议扩展](contents/ProtocolKit/如何在%20Objective-C%20中实现协议扩展.md) | +| FBRetainCycleDetector | 0.1.2 | [如何在 iOS 中解决循环引用的问题](contents/FBRetainCycleDetector/如何在%20iOS%20中解决循环引用的问题.md)
[检测 NSObject 对象持有的强指针](contents/FBRetainCycleDetector/检测%20NSObject%20对象持有的强指针.md)
[如何实现 iOS 中的 Associated Object](contents/FBRetainCycleDetector/如何实现%20iOS%20中的%20Associated%20Object.md)
[iOS 中的 block 是如何持有对象的](contents/FBRetainCycleDetector/iOS%20中的%20block%20是如何持有对象的.md)| +| fishhook | 0.2 |[动态修改 C 语言函数的实现](contents/fishhook/动态修改%20C%20语言函数的实现.md) | +| libextobjc | |[如何在 Objective-C 的环境下实现 defer](contents/libextobjc/如何在%20Objective-C%20的环境下实现%20defer.md) | +| IQKeyboardManager | 4.0.3 |[『零行代码』解决键盘遮挡问题(iOS)](contents/IQKeyboardManager/『零行代码』解决键盘遮挡问题(iOS).md) | +| DKNightVersion | 2.3.0 | [成熟的夜间模式解决方案](contents/DKNightVersion/成熟的夜间模式解决方案.md) | +| AFNetworking | 3.0.4 | [AFNetworking 概述(一)](contents/AFNetworking/AFNetworking%20概述(一).md)
[AFNetworking 的核心 AFURLSessionManager(二)](contents/AFNetworking/AFNetworking%20的核心%20AFURLSessionManager(二).md)
[处理请求和响应 AFURLSerialization(三)](contents/AFNetworking/处理请求和响应%20AFURLSerialization(三).md)
[AFNetworkReachabilityManager 监控网络状态(四)](contents/AFNetworking/AFNetworkReachabilityManager%20监控网络状态(四).md)
[验证 HTTPS 请求的证书(五)](contents/AFNetworking/验证%20HTTPS%20请求的证书(五).md) | +| BlocksKit | 2.2.5 | [神奇的 BlocksKit(一)遍历、KVO 和分类](contents/BlocksKit/神奇的%20BlocksKit%20(一).md)
[神奇的 BlocksKit(二)动态代理的实现 ](contents/BlocksKit/神奇的%20BlocksKit%20(二).md) | +| Alamofire | | [iOS 源代码分析 --- Alamofire](contents/Alamofire/iOS%20源代码分析%20----%20Alamofire.md) | +| SDWebImage | | [iOS 源代码分析 --- SDWebImage](contents/SDWebImage/iOS%20源代码分析%20---%20SDWebImage.md) | +| MBProgressHUD | | [iOS 源代码分析 --- MBProgressHUD](contents/MBProgressHUD/iOS%20源代码分析%20---%20MBProgressHUD.md) | +| Masonry | | [iOS 源代码分析 --- Masonry](contents/Masonry/iOS%20源代码分析%20---%20Masonry.md) | +| Redis | 3.2.5 | [Redis 和 I/O 多路复用](contents/Redis/redis-io-multiplexing.md)
[Redis 中的事件循环](contents/Redis/redis-eventloop.md)
[Redis 是如何处理命令的(客户端)](contents/Redis/redis-cli)| + +## 微信公众号 + +![](https://img.draveness.me/2020-03-11-15839264230785-wechat-qr-code.png) ## 勘误 @@ -35,4 +55,3 @@ 知识共享许可协议
作品Draveness 创作,采用知识共享署名 4.0 国际许可协议进行许可。 - diff --git "a/AFNetworking/AFNetworkReachabilityManager \347\233\221\346\216\247\347\275\221\347\273\234\347\212\266\346\200\201\357\274\210\345\233\233\357\274\211.md" "b/contents/AFNetworking/AFNetworkReachabilityManager \347\233\221\346\216\247\347\275\221\347\273\234\347\212\266\346\200\201\357\274\210\345\233\233\357\274\211.md" similarity index 95% rename from "AFNetworking/AFNetworkReachabilityManager \347\233\221\346\216\247\347\275\221\347\273\234\347\212\266\346\200\201\357\274\210\345\233\233\357\274\211.md" rename to "contents/AFNetworking/AFNetworkReachabilityManager \347\233\221\346\216\247\347\275\221\347\273\234\347\212\266\346\200\201\357\274\210\345\233\233\357\274\211.md" index b10cd00..9667575 100644 --- "a/AFNetworking/AFNetworkReachabilityManager \347\233\221\346\216\247\347\275\221\347\273\234\347\212\266\346\200\201\357\274\210\345\233\233\357\274\211.md" +++ "b/contents/AFNetworking/AFNetworkReachabilityManager \347\233\221\346\216\247\347\275\221\347\273\234\347\212\266\346\200\201\357\274\210\345\233\233\357\274\211.md" @@ -299,11 +299,11 @@ self.reachabilityManager = [AFNetworkReachabilityManager sharedManager]; 关于其他 AFNetworking 源代码分析的其他文章: -+ [AFNetworking 概述(一)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/AFNetworking%20概述(一).md) -+ [AFNetworking 的核心 AFURLSessionManager(二)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/AFNetworking%20的核心%20AFURLSessionManager(二).md) -+ [处理请求和响应 AFURLSerialization(三)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/处理请求和响应%20AFURLSerialization(三).md) -+ [AFNetworkReachabilityManager 监控网络状态(四)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/AFNetworkReachabilityManager%20监控网络状态(四).md) -+ [验证 HTTPS 请求的证书(五)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/验证%20HTTPS%20请求的证书(五).md) ++ [AFNetworking 概述(一)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/AFNetworking%20概述(一).md) ++ [AFNetworking 的核心 AFURLSessionManager(二)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/AFNetworking%20的核心%20AFURLSessionManager(二).md) ++ [处理请求和响应 AFURLSerialization(三)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/处理请求和响应%20AFURLSerialization(三).md) ++ [AFNetworkReachabilityManager 监控网络状态(四)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/AFNetworkReachabilityManager%20监控网络状态(四).md) ++ [验证 HTTPS 请求的证书(五)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/验证%20HTTPS%20请求的证书(五).md) Follow: [@Draveness](https://github.com/Draveness) diff --git "a/AFNetworking/AFNetworking \346\246\202\350\277\260\357\274\210\344\270\200\357\274\211.md" "b/contents/AFNetworking/AFNetworking \346\246\202\350\277\260\357\274\210\344\270\200\357\274\211.md" similarity index 92% rename from "AFNetworking/AFNetworking \346\246\202\350\277\260\357\274\210\344\270\200\357\274\211.md" rename to "contents/AFNetworking/AFNetworking \346\246\202\350\277\260\357\274\210\344\270\200\357\274\211.md" index 7d95f8e..3bab002 100644 --- "a/AFNetworking/AFNetworking \346\246\202\350\277\260\357\274\210\344\270\200\357\274\211.md" +++ "b/contents/AFNetworking/AFNetworking \346\246\202\350\277\260\357\274\210\344\270\200\357\274\211.md" @@ -135,11 +135,11 @@ AFNetworking 实际上只是对 `NSURLSession` 高度地封装, 提供一些简 关于其他 AFNetworking 源代码分析的其他文章: -+ [AFNetworking 概述(一)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/AFNetworking%20概述(一).md) -+ [AFNetworking 的核心 AFURLSessionManager(二)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/AFNetworking%20的核心%20AFURLSessionManager(二).md) -+ [处理请求和响应 AFURLSerialization(三)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/处理请求和响应%20AFURLSerialization(三).md) -+ [AFNetworkReachabilityManager 监控网络状态(四)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/AFNetworkReachabilityManager%20监控网络状态(四).md) -+ [验证 HTTPS 请求的证书(五)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/验证%20HTTPS%20请求的证书(五).md) ++ [AFNetworking 概述(一)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/AFNetworking%20概述(一).md) ++ [AFNetworking 的核心 AFURLSessionManager(二)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/AFNetworking%20的核心%20AFURLSessionManager(二).md) ++ [处理请求和响应 AFURLSerialization(三)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/处理请求和响应%20AFURLSerialization(三).md) ++ [AFNetworkReachabilityManager 监控网络状态(四)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/AFNetworkReachabilityManager%20监控网络状态(四).md) ++ [验证 HTTPS 请求的证书(五)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/验证%20HTTPS%20请求的证书(五).md) Follow: [@Draveness](https://github.com/Draveness) diff --git "a/AFNetworking/AFNetworking \347\232\204\346\240\270\345\277\203 AFURLSessionManager\357\274\210\344\272\214\357\274\211.md" "b/contents/AFNetworking/AFNetworking \347\232\204\346\240\270\345\277\203 AFURLSessionManager\357\274\210\344\272\214\357\274\211.md" similarity index 97% rename from "AFNetworking/AFNetworking \347\232\204\346\240\270\345\277\203 AFURLSessionManager\357\274\210\344\272\214\357\274\211.md" rename to "contents/AFNetworking/AFNetworking \347\232\204\346\240\270\345\277\203 AFURLSessionManager\357\274\210\344\272\214\357\274\211.md" index 7dc7477..bb4e823 100644 --- "a/AFNetworking/AFNetworking \347\232\204\346\240\270\345\277\203 AFURLSessionManager\357\274\210\344\272\214\357\274\211.md" +++ "b/contents/AFNetworking/AFNetworking \347\232\204\346\240\270\345\277\203 AFURLSessionManager\357\274\210\344\272\214\357\274\211.md" @@ -113,7 +113,7 @@ Blog: [Draveness](http://draveness.me) > `url_session_manager_create_task_safely` 的调用是因为苹果框架中的一个 bug [#2093](https://github.com/AFNetworking/AFNetworking/issues/2093),如果有兴趣可以看一下,在这里就不说明了 1. 调用 `- [NSURLSession dataTaskWithRequest:]` 方法传入 `NSURLRequest` -2. 调用 `- [AFURLSessionManager addDelegateForDataTask:uploadProgress:downloadProgress:completionHandler:]` 方法返回一个 `AFURLSessionManagerTaskDelegate` 对象 +2. 调用 `- [AFURLSessionManager addDelegateForDataTask:uploadProgress:downloadProgress:completionHandler:]` 方法创建一个 `AFURLSessionManagerTaskDelegate` 对象 3. 将 `completionHandler` `uploadProgressBlock` 和 `downloadProgressBlock` 传入该对象并在相应事件发生时进行回调 ```objectivec @@ -563,10 +563,10 @@ didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge 关于其他 AFNetworking 源代码分析的其他文章: -+ [AFNetworking 概述(一)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/AFNetworking%20概述(一).md) -+ [AFNetworking 的核心 AFURLSessionManager(二)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/AFNetworking%20的核心%20AFURLSessionManager(二).md) -+ [处理请求和响应 AFURLSerialization(三)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/处理请求和响应%20AFURLSerialization(三).md) -+ [AFNetworkReachabilityManager 监控网络状态(四)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/AFNetworkReachabilityManager%20监控网络状态(四).md) ++ [AFNetworking 概述(一)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/AFNetworking%20概述(一).md) ++ [AFNetworking 的核心 AFURLSessionManager(二)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/AFNetworking%20的核心%20AFURLSessionManager(二).md) ++ [处理请求和响应 AFURLSerialization(三)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/处理请求和响应%20AFURLSerialization(三).md) ++ [AFNetworkReachabilityManager 监控网络状态(四)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/AFNetworkReachabilityManager%20监控网络状态(四).md) diff --git "a/AFNetworking/\345\244\204\347\220\206\350\257\267\346\261\202\345\222\214\345\223\215\345\272\224 AFURLSerialization\357\274\210\344\270\211\357\274\211.md" "b/contents/AFNetworking/\345\244\204\347\220\206\350\257\267\346\261\202\345\222\214\345\223\215\345\272\224 AFURLSerialization\357\274\210\344\270\211\357\274\211.md" similarity index 97% rename from "AFNetworking/\345\244\204\347\220\206\350\257\267\346\261\202\345\222\214\345\223\215\345\272\224 AFURLSerialization\357\274\210\344\270\211\357\274\211.md" rename to "contents/AFNetworking/\345\244\204\347\220\206\350\257\267\346\261\202\345\222\214\345\223\215\345\272\224 AFURLSerialization\357\274\210\344\270\211\357\274\211.md" index edfc710..540d9f3 100644 --- "a/AFNetworking/\345\244\204\347\220\206\350\257\267\346\261\202\345\222\214\345\223\215\345\272\224 AFURLSerialization\357\274\210\344\270\211\357\274\211.md" +++ "b/contents/AFNetworking/\345\244\204\347\220\206\350\257\267\346\261\202\345\222\214\345\223\215\345\272\224 AFURLSerialization\357\274\210\344\270\211\357\274\211.md" @@ -680,11 +680,11 @@ for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) { 关于其他 AFNetworking 源代码分析的其他文章: -+ [AFNetworking 概述(一)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/AFNetworking%20概述(一).md) -+ [AFNetworking 的核心 AFURLSessionManager(二)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/AFNetworking%20的核心%20AFURLSessionManager(二).md) -+ [处理请求和响应 AFURLSerialization(三)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/处理请求和响应%20AFURLSerialization(三).md) -+ [AFNetworkReachabilityManager 监控网络状态(四)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/AFNetworkReachabilityManager%20监控网络状态(四).md) -+ [验证 HTTPS 请求的证书(五)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/验证%20HTTPS%20请求的证书(五).md) ++ [AFNetworking 概述(一)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/AFNetworking%20概述(一).md) ++ [AFNetworking 的核心 AFURLSessionManager(二)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/AFNetworking%20的核心%20AFURLSessionManager(二).md) ++ [处理请求和响应 AFURLSerialization(三)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/处理请求和响应%20AFURLSerialization(三).md) ++ [AFNetworkReachabilityManager 监控网络状态(四)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/AFNetworkReachabilityManager%20监控网络状态(四).md) ++ [验证 HTTPS 请求的证书(五)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/验证%20HTTPS%20请求的证书(五).md) diff --git "a/AFNetworking/\351\252\214\350\257\201 HTTPS \350\257\267\346\261\202\347\232\204\350\257\201\344\271\246\357\274\210\344\272\224\357\274\211.md" "b/contents/AFNetworking/\351\252\214\350\257\201 HTTPS \350\257\267\346\261\202\347\232\204\350\257\201\344\271\246\357\274\210\344\272\224\357\274\211.md" similarity index 96% rename from "AFNetworking/\351\252\214\350\257\201 HTTPS \350\257\267\346\261\202\347\232\204\350\257\201\344\271\246\357\274\210\344\272\224\357\274\211.md" rename to "contents/AFNetworking/\351\252\214\350\257\201 HTTPS \350\257\267\346\261\202\347\232\204\350\257\201\344\271\246\357\274\210\344\272\224\357\274\211.md" index f02580d..6f77c4b 100644 --- "a/AFNetworking/\351\252\214\350\257\201 HTTPS \350\257\267\346\261\202\347\232\204\350\257\201\344\271\246\357\274\210\344\272\224\357\274\211.md" +++ "b/contents/AFNetworking/\351\252\214\350\257\201 HTTPS \350\257\267\346\261\202\347\232\204\350\257\201\344\271\246\357\274\210\344\272\224\357\274\211.md" @@ -371,11 +371,11 @@ if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthent 关于其他 AFNetworking 源代码分析的其他文章: -+ [AFNetworking 概述(一)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/AFNetworking%20概述(一).md) -+ [AFNetworking 的核心 AFURLSessionManager(二)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/AFNetworking%20的核心%20AFURLSessionManager(二).md) -+ [处理请求和响应 AFURLSerialization(三)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/处理请求和响应%20AFURLSerialization(三).md) -+ [AFNetworkReachabilityManager 监控网络状态(四)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/AFNetworkReachabilityManager%20监控网络状态(四).md) -+ [验证 HTTPS 请求的证书(五)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/AFNetworking/验证%20HTTPS%20请求的证书(五).md) ++ [AFNetworking 概述(一)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/AFNetworking%20概述(一).md) ++ [AFNetworking 的核心 AFURLSessionManager(二)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/AFNetworking%20的核心%20AFURLSessionManager(二).md) ++ [处理请求和响应 AFURLSerialization(三)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/处理请求和响应%20AFURLSerialization(三).md) ++ [AFNetworkReachabilityManager 监控网络状态(四)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/AFNetworkReachabilityManager%20监控网络状态(四).md) ++ [验证 HTTPS 请求的证书(五)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AFNetworking/验证%20HTTPS%20请求的证书(五).md) diff --git "a/Alamofire/iOS \346\272\220\344\273\243\347\240\201\345\210\206\346\236\220 ---- Alamofire.md" "b/contents/Alamofire/iOS \346\272\220\344\273\243\347\240\201\345\210\206\346\236\220 ---- Alamofire.md" similarity index 100% rename from "Alamofire/iOS \346\272\220\344\273\243\347\240\201\345\210\206\346\236\220 ---- Alamofire.md" rename to "contents/Alamofire/iOS \346\272\220\344\273\243\347\240\201\345\210\206\346\236\220 ---- Alamofire.md" diff --git a/contents/AsyncDisplayKit/Layout/.gitignore b/contents/AsyncDisplayKit/Layout/.gitignore new file mode 100644 index 0000000..200a88b --- /dev/null +++ b/contents/AsyncDisplayKit/Layout/.gitignore @@ -0,0 +1,28 @@ +# 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/ diff --git a/contents/AsyncDisplayKit/Layout/Layout.xcodeproj/project.pbxproj b/contents/AsyncDisplayKit/Layout/Layout.xcodeproj/project.pbxproj new file mode 100644 index 0000000..39d8928 --- /dev/null +++ b/contents/AsyncDisplayKit/Layout/Layout.xcodeproj/project.pbxproj @@ -0,0 +1,385 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 518A37E5D0220B807E99ECA0 /* libPods-Layout.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4318ABCD72DA5B6C8E3E7A23 /* libPods-Layout.a */; }; + 724CC0131D729ACE00969D19 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 724CC0121D729ACE00969D19 /* main.m */; }; + 724CC0161D729ACE00969D19 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 724CC0151D729ACE00969D19 /* AppDelegate.m */; }; + 724CC0191D729ACE00969D19 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 724CC0181D729ACE00969D19 /* ViewController.m */; }; + 724CC01C1D729ACE00969D19 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 724CC01A1D729ACE00969D19 /* Main.storyboard */; }; + 724CC01E1D729ACE00969D19 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 724CC01D1D729ACE00969D19 /* Assets.xcassets */; }; + 724CC0211D729ACE00969D19 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 724CC01F1D729ACE00969D19 /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 1C89D50C35EC2591F7CB69A9 /* Pods-Layout.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Layout.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Layout/Pods-Layout.debug.xcconfig"; sourceTree = ""; }; + 2CC6845D33268FBA5F260319 /* Pods-Layout.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Layout.release.xcconfig"; path = "Pods/Target Support Files/Pods-Layout/Pods-Layout.release.xcconfig"; sourceTree = ""; }; + 4318ABCD72DA5B6C8E3E7A23 /* libPods-Layout.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Layout.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 724CC00E1D729ACE00969D19 /* Layout.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Layout.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 724CC0121D729ACE00969D19 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 724CC0141D729ACE00969D19 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 724CC0151D729ACE00969D19 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 724CC0171D729ACE00969D19 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 724CC0181D729ACE00969D19 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 724CC01B1D729ACE00969D19 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 724CC01D1D729ACE00969D19 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 724CC0201D729ACE00969D19 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 724CC0221D729ACE00969D19 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 724CC00B1D729ACE00969D19 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 518A37E5D0220B807E99ECA0 /* libPods-Layout.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 17FDB5A80DF243428EBD53E9 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4318ABCD72DA5B6C8E3E7A23 /* libPods-Layout.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 724CC0051D729ACE00969D19 = { + isa = PBXGroup; + children = ( + 724CC0101D729ACE00969D19 /* Layout */, + 724CC00F1D729ACE00969D19 /* Products */, + DFFAE951264B58C7C0914F93 /* Pods */, + 17FDB5A80DF243428EBD53E9 /* Frameworks */, + ); + sourceTree = ""; + }; + 724CC00F1D729ACE00969D19 /* Products */ = { + isa = PBXGroup; + children = ( + 724CC00E1D729ACE00969D19 /* Layout.app */, + ); + name = Products; + sourceTree = ""; + }; + 724CC0101D729ACE00969D19 /* Layout */ = { + isa = PBXGroup; + children = ( + 724CC0141D729ACE00969D19 /* AppDelegate.h */, + 724CC0151D729ACE00969D19 /* AppDelegate.m */, + 724CC0171D729ACE00969D19 /* ViewController.h */, + 724CC0181D729ACE00969D19 /* ViewController.m */, + 724CC01A1D729ACE00969D19 /* Main.storyboard */, + 724CC01D1D729ACE00969D19 /* Assets.xcassets */, + 724CC01F1D729ACE00969D19 /* LaunchScreen.storyboard */, + 724CC0221D729ACE00969D19 /* Info.plist */, + 724CC0111D729ACE00969D19 /* Supporting Files */, + ); + path = Layout; + sourceTree = ""; + }; + 724CC0111D729ACE00969D19 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 724CC0121D729ACE00969D19 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + DFFAE951264B58C7C0914F93 /* Pods */ = { + isa = PBXGroup; + children = ( + 1C89D50C35EC2591F7CB69A9 /* Pods-Layout.debug.xcconfig */, + 2CC6845D33268FBA5F260319 /* Pods-Layout.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 724CC00D1D729ACE00969D19 /* Layout */ = { + isa = PBXNativeTarget; + buildConfigurationList = 724CC0251D729ACE00969D19 /* Build configuration list for PBXNativeTarget "Layout" */; + buildPhases = ( + 817069C0FFA3399E7AC74743 /* [CP] Check Pods Manifest.lock */, + 724CC00A1D729ACE00969D19 /* Sources */, + 724CC00B1D729ACE00969D19 /* Frameworks */, + 724CC00C1D729ACE00969D19 /* Resources */, + 2F6F816CEDFB06833A4A6211 /* [CP] Embed Pods Frameworks */, + 13F926A8EF9EB7E48D00948B /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Layout; + productName = Layout; + productReference = 724CC00E1D729ACE00969D19 /* Layout.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 724CC0061D729ACE00969D19 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0730; + ORGANIZATIONNAME = Draveness; + TargetAttributes = { + 724CC00D1D729ACE00969D19 = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 724CC0091D729ACE00969D19 /* Build configuration list for PBXProject "Layout" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 724CC0051D729ACE00969D19; + productRefGroup = 724CC00F1D729ACE00969D19 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 724CC00D1D729ACE00969D19 /* Layout */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 724CC00C1D729ACE00969D19 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 724CC0211D729ACE00969D19 /* LaunchScreen.storyboard in Resources */, + 724CC01E1D729ACE00969D19 /* Assets.xcassets in Resources */, + 724CC01C1D729ACE00969D19 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 13F926A8EF9EB7E48D00948B /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Layout/Pods-Layout-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 2F6F816CEDFB06833A4A6211 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Layout/Pods-Layout-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 817069C0FFA3399E7AC74743 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 724CC00A1D729ACE00969D19 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 724CC0191D729ACE00969D19 /* ViewController.m in Sources */, + 724CC0161D729ACE00969D19 /* AppDelegate.m in Sources */, + 724CC0131D729ACE00969D19 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 724CC01A1D729ACE00969D19 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 724CC01B1D729ACE00969D19 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 724CC01F1D729ACE00969D19 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 724CC0201D729ACE00969D19 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 724CC0231D729ACE00969D19 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.3; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 724CC0241D729ACE00969D19 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.3; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 724CC0261D729ACE00969D19 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1C89D50C35EC2591F7CB69A9 /* Pods-Layout.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Layout/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.draveness.Layout; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 724CC0271D729ACE00969D19 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2CC6845D33268FBA5F260319 /* Pods-Layout.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Layout/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.draveness.Layout; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 724CC0091D729ACE00969D19 /* Build configuration list for PBXProject "Layout" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 724CC0231D729ACE00969D19 /* Debug */, + 724CC0241D729ACE00969D19 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 724CC0251D729ACE00969D19 /* Build configuration list for PBXNativeTarget "Layout" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 724CC0261D729ACE00969D19 /* Debug */, + 724CC0271D729ACE00969D19 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 724CC0061D729ACE00969D19 /* Project object */; +} diff --git a/contents/AsyncDisplayKit/Layout/Layout.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/contents/AsyncDisplayKit/Layout/Layout.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..329be3e --- /dev/null +++ b/contents/AsyncDisplayKit/Layout/Layout.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/contents/AsyncDisplayKit/Layout/Layout.xcworkspace/contents.xcworkspacedata b/contents/AsyncDisplayKit/Layout/Layout.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..7a392f3 --- /dev/null +++ b/contents/AsyncDisplayKit/Layout/Layout.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/contents/AsyncDisplayKit/Layout/Layout/AppDelegate.h b/contents/AsyncDisplayKit/Layout/Layout/AppDelegate.h new file mode 100644 index 0000000..3272743 --- /dev/null +++ b/contents/AsyncDisplayKit/Layout/Layout/AppDelegate.h @@ -0,0 +1,17 @@ +// +// AppDelegate.h +// Layout +// +// Created by Draveness on 8/28/16. +// Copyright © 2016 Draveness. All rights reserved. +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + + +@end + diff --git a/contents/AsyncDisplayKit/Layout/Layout/AppDelegate.m b/contents/AsyncDisplayKit/Layout/Layout/AppDelegate.m new file mode 100644 index 0000000..2c1ac88 --- /dev/null +++ b/contents/AsyncDisplayKit/Layout/Layout/AppDelegate.m @@ -0,0 +1,45 @@ +// +// AppDelegate.m +// Layout +// +// Created by Draveness on 8/28/16. +// Copyright © 2016 Draveness. All rights reserved. +// + +#import "AppDelegate.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. +} + +- (void)applicationDidEnterBackground:(UIApplication *)application { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { + // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. +} + +- (void)applicationDidBecomeActive:(UIApplication *)application { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. +} + +- (void)applicationWillTerminate:(UIApplication *)application { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. +} + +@end diff --git a/contents/AsyncDisplayKit/Layout/Layout/Assets.xcassets/AppIcon.appiconset/Contents.json b/contents/AsyncDisplayKit/Layout/Layout/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..118c98f --- /dev/null +++ b/contents/AsyncDisplayKit/Layout/Layout/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/contents/AsyncDisplayKit/Layout/Layout/Base.lproj/LaunchScreen.storyboard b/contents/AsyncDisplayKit/Layout/Layout/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..2e721e1 --- /dev/null +++ b/contents/AsyncDisplayKit/Layout/Layout/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contents/AsyncDisplayKit/Layout/Layout/Base.lproj/Main.storyboard b/contents/AsyncDisplayKit/Layout/Layout/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f56d2f3 --- /dev/null +++ b/contents/AsyncDisplayKit/Layout/Layout/Base.lproj/Main.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contents/AsyncDisplayKit/Layout/Layout/Info.plist b/contents/AsyncDisplayKit/Layout/Layout/Info.plist new file mode 100644 index 0000000..6905cc6 --- /dev/null +++ b/contents/AsyncDisplayKit/Layout/Layout/Info.plist @@ -0,0 +1,40 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/contents/AsyncDisplayKit/Layout/Layout/ViewController.h b/contents/AsyncDisplayKit/Layout/Layout/ViewController.h new file mode 100644 index 0000000..ad103b0 --- /dev/null +++ b/contents/AsyncDisplayKit/Layout/Layout/ViewController.h @@ -0,0 +1,15 @@ +// +// ViewController.h +// Layout +// +// Created by Draveness on 8/28/16. +// Copyright © 2016 Draveness. All rights reserved. +// + +#import + +@interface ViewController : UIViewController + + +@end + diff --git a/contents/AsyncDisplayKit/Layout/Layout/ViewController.m b/contents/AsyncDisplayKit/Layout/Layout/ViewController.m new file mode 100644 index 0000000..d650d9f --- /dev/null +++ b/contents/AsyncDisplayKit/Layout/Layout/ViewController.m @@ -0,0 +1,273 @@ +// +// ViewController.m +// Layout +// +// Created by Draveness on 8/28/16. +// Copyright © 2016 Draveness. All rights reserved. +// + +#import "ViewController.h" + +#import + +@interface ViewController () + +@end + +@implementation ViewController { + UITextField *_textField; + UILabel *_indicateLabel; + NSMutableArray *_views; + + NSMutableDictionary *_resultDictionary; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + _views = [[NSMutableArray alloc] init]; + + UIButton *autoLayoutButton = [[UIButton alloc] init]; + [autoLayoutButton setTitle:@"AutoLayout" forState:UIControlStateNormal]; + [autoLayoutButton setTitleColor:[UIColor blueColor] forState:UIControlStateNormal]; + [autoLayoutButton addTarget:self action:@selector(generateViews) + forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:autoLayoutButton]; + [autoLayoutButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.height.mas_equalTo(20); + make.bottom.mas_equalTo(0); + }]; + + UIButton *nestedButton = [[UIButton alloc] init]; + [nestedButton setTitle:@"Nested" forState:UIControlStateNormal]; + [nestedButton setTitleColor:[UIColor blueColor] forState:UIControlStateNormal]; + [nestedButton addTarget:self action:@selector(generateNestedViews) + forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:nestedButton]; + [nestedButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.height.mas_equalTo(20); + make.bottom.mas_equalTo(0); + }]; + + UIButton *frameButton = [[UIButton alloc] init]; + [frameButton setTitle:@"Frame" forState:UIControlStateNormal]; + [frameButton setTitleColor:[UIColor blueColor] forState:UIControlStateNormal]; + [frameButton addTarget:self action:@selector(generateFrameViews) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:frameButton]; + [frameButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.height.mas_equalTo(20); + make.bottom.mas_equalTo(0); + }]; + + [@[autoLayoutButton, nestedButton, frameButton] mas_distributeViewsAlongAxis:MASAxisTypeHorizontal + withFixedItemLength:140 leadSpacing:0 tailSpacing:0]; + + _textField = [[UITextField alloc] init]; + _textField.textAlignment = NSTextAlignmentCenter; + [self.view addSubview:_textField]; + [_textField mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.mas_equalTo(20); + make.right.mas_equalTo(-20); + make.bottom.mas_equalTo(autoLayoutButton.mas_top); + make.height.mas_equalTo(20); + }]; + + _indicateLabel = [[UILabel alloc] init]; + _indicateLabel.textColor = [UIColor blackColor]; + _indicateLabel.textAlignment = NSTextAlignmentRight; + [self.view addSubview:_indicateLabel]; + [_indicateLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.right.top.mas_equalTo(0); + make.height.mas_equalTo(20); + }]; + + UIButton *printResult = [[UIButton alloc] init]; + [printResult setTitle:@"PrintResult" forState:UIControlStateNormal]; + [printResult setTitleColor:[UIColor blueColor] forState:UIControlStateNormal]; + [printResult addTarget:self action:@selector(printerResult) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:printResult]; + [printResult mas_makeConstraints:^(MASConstraintMaker *make) { + make.height.mas_equalTo(20); + make.top.left.mas_equalTo(0); + make.width.mas_equalTo(100); + }]; + + _resultDictionary = [[NSMutableDictionary alloc] init]; + [_resultDictionary setObject:[[NSMutableDictionary alloc] init] forKey:@"AutoLayout"]; + [_resultDictionary setObject:[[NSMutableDictionary alloc] init] forKey:@"NestedAutoLayout"]; + [_resultDictionary setObject:[[NSMutableDictionary alloc] init] forKey:@"Frame"]; + [_resultDictionary setObject:[[NSMutableDictionary alloc] init] forKey:@"ASDK"]; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + [_textField becomeFirstResponder]; +} + +- (void)generateViews { + NSInteger number = _textField.text.integerValue; + for (UIView *view in _views) { + [view removeFromSuperview]; + } + _views = [[NSMutableArray alloc] init]; + + NSTimeInterval startTime = [NSDate timeIntervalSinceReferenceDate]; + for (NSInteger i = 0; i < number; i++) { + UIView *leftView = self.view; + UIView *topView = self.view; + if (_views.count != 0) { + NSInteger left = arc4random() % _views.count; + NSInteger top = arc4random() % _views.count; + leftView = _views[left]; + topView = _views[top]; + } + + CGFloat hue = ( arc4random() % 256 / 256.0 ); // 0.0 to 1.0 + CGFloat saturation = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from white + CGFloat brightness = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from black + UIColor *color = [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:1]; + + NSInteger leftSpace = (arc4random() % 414) - (int)leftView.frame.origin.x; + NSInteger topSpace = (arc4random() % 568) - (int)topView.frame.origin.y; + + UIView *newView = [[UIView alloc] init]; + newView.backgroundColor = color; + [self.view addSubview:newView]; + [newView mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.mas_greaterThanOrEqualTo(0); + make.right.mas_lessThanOrEqualTo(0); + make.top.mas_greaterThanOrEqualTo(20); + make.bottom.mas_lessThanOrEqualTo(-40); + make.left.mas_equalTo(leftView).offset(leftSpace).priorityMedium(); + make.top.mas_equalTo(topView).offset(topSpace).priorityMedium(); + make.size.mas_equalTo(10); + }]; + + [_views addObject:newView]; + } + NSTimeInterval endTime = [NSDate timeIntervalSinceReferenceDate]; + + NSTimeInterval timeInterval = endTime - startTime; + + NSMutableDictionary *autoLayoutDictionary = _resultDictionary[@"AutoLayout"]; + NSMutableDictionary *currentTimesDictionary = autoLayoutDictionary[@(number)] ?: [[NSMutableDictionary alloc] init]; + NSNumber *times = currentTimesDictionary[@"times"] ? : @0; + NSNumber *avgTime = currentTimesDictionary[@"avgTime"] ? : @0; + currentTimesDictionary[@"avgTime"] = @((times.integerValue * avgTime.doubleValue + timeInterval) / (double)(times.integerValue + 1)); + currentTimesDictionary[@"times"] = @(times.integerValue + 1); + [autoLayoutDictionary setObject:currentTimesDictionary forKey:@(number)]; + + _indicateLabel.text = [NSString stringWithFormat:@"%ld: %f", (long)number, endTime-startTime]; +} + +- (void)generateFrameViews { + NSInteger number = _textField.text.integerValue; + for (UIView *view in _views) { + [view removeFromSuperview]; + } + _views = [[NSMutableArray alloc] init]; + + NSTimeInterval startTime = [NSDate timeIntervalSinceReferenceDate]; + for (NSInteger i = 0; i < number; i++) { + CGFloat hue = ( arc4random() % 256 / 256.0 ); // 0.0 to 1.0 + CGFloat saturation = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from white + CGFloat brightness = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from black + UIColor *color = [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:1]; + + NSInteger leftSpace = (arc4random() % 404) % (int)self.view.frame.size.width; + NSInteger topSpace = (arc4random() % 676) % (int)self.view.frame.size.height + 20; + + UIView *newView = [[UIView alloc] init]; + newView.backgroundColor = color; + newView.frame = CGRectMake(leftSpace, topSpace, 10, 10); + [self.view addSubview:newView]; + + [_views addObject:newView]; + } + NSTimeInterval endTime = [NSDate timeIntervalSinceReferenceDate]; + + NSTimeInterval timeInterval = endTime - startTime; + + NSMutableDictionary *frameDictionary = _resultDictionary[@"Frame"]; + NSMutableDictionary *currentTimesDictionary = frameDictionary[@(number)] ?: [[NSMutableDictionary alloc] init]; + NSNumber *times = currentTimesDictionary[@"times"] ? : @0; + NSNumber *avgTime = currentTimesDictionary[@"avgTime"] ? : @0; + currentTimesDictionary[@"avgTime"] = @((times.integerValue * avgTime.doubleValue + timeInterval) / (double)(times.integerValue + 1)); + currentTimesDictionary[@"times"] = @(times.integerValue + 1); + [frameDictionary setObject:currentTimesDictionary forKey:@(number)]; + + _indicateLabel.text = [NSString stringWithFormat:@"%ld: %f", (long)number, endTime-startTime]; +} + +- (void)generateNestedViews { + NSInteger number = _textField.text.integerValue; + for (UIView *view in _views) { + [view removeFromSuperview]; + } + _views = [[NSMutableArray alloc] init]; + + NSTimeInterval startTime = [NSDate timeIntervalSinceReferenceDate]; + for (NSInteger i = 0; i < number; i++) { + UIView *leftView = self.view; + UIView *topView = self.view; + if (_views.count != 0) { + NSInteger left = arc4random() % _views.count; + NSInteger top = arc4random() % _views.count; + leftView = _views[left]; + topView = _views[top]; + } + + CGFloat hue = ( arc4random() % 256 / 256.0 ); // 0.0 to 1.0 + CGFloat saturation = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from white + CGFloat brightness = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from black + UIColor *color = [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:1]; + + UIView *newView = [[UIView alloc] init]; + newView.backgroundColor = color; + [self.view addSubview:newView]; + if (_views.count == 0) { + [self.view addSubview:newView]; + + [newView mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.mas_equalTo(0.5); + make.top.mas_equalTo(20.5); + make.bottom.mas_equalTo(-40.5); + make.right.mas_equalTo(-0.5); + }]; + } else { + UIView *aView = _views[i - 1]; + [aView addSubview:newView]; + + [newView mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.left.mas_equalTo(1); + make.bottom.right.mas_equalTo(-1); + }]; + } + + [_views addObject:newView]; + } + NSTimeInterval endTime = [NSDate timeIntervalSinceReferenceDate]; + + NSTimeInterval timeInterval = endTime - startTime; + + NSMutableDictionary *autoLayoutDictionary = _resultDictionary[@"NestedAutoLayout"]; + NSMutableDictionary *currentTimesDictionary = autoLayoutDictionary[@(number)] ?: [[NSMutableDictionary alloc] init]; + NSNumber *times = currentTimesDictionary[@"times"] ? : @0; + NSNumber *avgTime = currentTimesDictionary[@"avgTime"] ? : @0; + currentTimesDictionary[@"avgTime"] = @((times.integerValue * avgTime.doubleValue + timeInterval) / (double)(times.integerValue + 1)); + currentTimesDictionary[@"times"] = @(times.integerValue + 1); + [autoLayoutDictionary setObject:currentTimesDictionary forKey:@(number)]; + + _indicateLabel.text = [NSString stringWithFormat:@"%ld: %f", (long)number, endTime-startTime]; + +} + +- (void)printerResult { + NSLog(@"%@", _resultDictionary); +} + +- (BOOL)prefersStatusBarHidden { + return YES; +} + +@end \ No newline at end of file diff --git a/contents/AsyncDisplayKit/Layout/Layout/main.m b/contents/AsyncDisplayKit/Layout/Layout/main.m new file mode 100644 index 0000000..954480b --- /dev/null +++ b/contents/AsyncDisplayKit/Layout/Layout/main.m @@ -0,0 +1,16 @@ +// +// main.m +// Layout +// +// Created by Draveness on 8/28/16. +// Copyright © 2016 Draveness. All rights reserved. +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/contents/AsyncDisplayKit/Layout/Podfile b/contents/AsyncDisplayKit/Layout/Podfile new file mode 100644 index 0000000..c853f7c --- /dev/null +++ b/contents/AsyncDisplayKit/Layout/Podfile @@ -0,0 +1,7 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +target 'Layout' do + # use_frameworks! + pod 'Masonry' +end diff --git a/contents/AsyncDisplayKit/images/CRT.png b/contents/AsyncDisplayKit/images/CRT.png new file mode 100644 index 0000000..291af72 Binary files /dev/null and b/contents/AsyncDisplayKit/images/CRT.png differ diff --git a/contents/AsyncDisplayKit/images/advertise.jpg b/contents/AsyncDisplayKit/images/advertise.jpg new file mode 100644 index 0000000..cb44637 Binary files /dev/null and b/contents/AsyncDisplayKit/images/advertise.jpg differ diff --git a/contents/AsyncDisplayKit/images/apple-a9.jpg b/contents/AsyncDisplayKit/images/apple-a9.jpg new file mode 100644 index 0000000..d50cff3 Binary files /dev/null and b/contents/AsyncDisplayKit/images/apple-a9.jpg differ diff --git a/contents/AsyncDisplayKit/images/asdk-hierarchy.png b/contents/AsyncDisplayKit/images/asdk-hierarchy.png new file mode 100644 index 0000000..544294a Binary files /dev/null and b/contents/AsyncDisplayKit/images/asdk-hierarchy.png differ diff --git a/contents/AsyncDisplayKit/images/asdk-logo.png b/contents/AsyncDisplayKit/images/asdk-logo.png new file mode 100644 index 0000000..dce1ebc Binary files /dev/null and b/contents/AsyncDisplayKit/images/asdk-logo.png differ diff --git a/contents/AsyncDisplayKit/images/aslayout-range-mode-display-preload.jpeg b/contents/AsyncDisplayKit/images/aslayout-range-mode-display-preload.jpeg new file mode 100644 index 0000000..f0cd284 Binary files /dev/null and b/contents/AsyncDisplayKit/images/aslayout-range-mode-display-preload.jpeg differ diff --git a/contents/AsyncDisplayKit/images/astableview-astablenode.jpg b/contents/AsyncDisplayKit/images/astableview-astablenode.jpg new file mode 100644 index 0000000..0c4a039 Binary files /dev/null and b/contents/AsyncDisplayKit/images/astableview-astablenode.jpg differ diff --git a/contents/AsyncDisplayKit/images/astableview-data.png b/contents/AsyncDisplayKit/images/astableview-data.png new file mode 100644 index 0000000..745edb1 Binary files /dev/null and b/contents/AsyncDisplayKit/images/astableview-data.png differ diff --git a/contents/AsyncDisplayKit/images/async-node-calculate.jpeg b/contents/AsyncDisplayKit/images/async-node-calculate.jpeg new file mode 100644 index 0000000..90f4f20 Binary files /dev/null and b/contents/AsyncDisplayKit/images/async-node-calculate.jpeg differ diff --git a/contents/AsyncDisplayKit/images/box-layout.jpg b/contents/AsyncDisplayKit/images/box-layout.jpg new file mode 100644 index 0000000..feff3a7 Binary files /dev/null and b/contents/AsyncDisplayKit/images/box-layout.jpg differ diff --git a/contents/AsyncDisplayKit/images/cache-layer.png b/contents/AsyncDisplayKit/images/cache-layer.png new file mode 100644 index 0000000..dec7937 Binary files /dev/null and b/contents/AsyncDisplayKit/images/cache-layer.png differ diff --git a/contents/AsyncDisplayKit/images/cellforrowatindexpath.jpg b/contents/AsyncDisplayKit/images/cellforrowatindexpath.jpg new file mode 100644 index 0000000..6bdb48b Binary files /dev/null and b/contents/AsyncDisplayKit/images/cellforrowatindexpath.jpg differ diff --git a/contents/AsyncDisplayKit/images/cpu-gpu.jpg b/contents/AsyncDisplayKit/images/cpu-gpu.jpg new file mode 100644 index 0000000..cc341e2 Binary files /dev/null and b/contents/AsyncDisplayKit/images/cpu-gpu.jpg differ diff --git a/contents/AsyncDisplayKit/images/dynamic-threshold.jpeg b/contents/AsyncDisplayKit/images/dynamic-threshold.jpeg new file mode 100644 index 0000000..b94d8c0 Binary files /dev/null and b/contents/AsyncDisplayKit/images/dynamic-threshold.jpeg differ diff --git a/contents/AsyncDisplayKit/images/dynamic-threshold.jpg b/contents/AsyncDisplayKit/images/dynamic-threshold.jpg new file mode 100644 index 0000000..f3f0c4d Binary files /dev/null and b/contents/AsyncDisplayKit/images/dynamic-threshold.jpg differ diff --git a/contents/AsyncDisplayKit/images/how-to-solve-tearing-problem.jpg b/contents/AsyncDisplayKit/images/how-to-solve-tearing-problem.jpg new file mode 100644 index 0000000..8a361e6 Binary files /dev/null and b/contents/AsyncDisplayKit/images/how-to-solve-tearing-problem.jpg differ diff --git a/contents/AsyncDisplayKit/images/infinite-list.jpg b/contents/AsyncDisplayKit/images/infinite-list.jpg new file mode 100644 index 0000000..ecef2a8 Binary files /dev/null and b/contents/AsyncDisplayKit/images/infinite-list.jpg differ diff --git a/contents/AsyncDisplayKit/images/intelligent-preloading-ranges-screenfuls.png b/contents/AsyncDisplayKit/images/intelligent-preloading-ranges-screenfuls.png new file mode 100644 index 0000000..019762c Binary files /dev/null and b/contents/AsyncDisplayKit/images/intelligent-preloading-ranges-screenfuls.png differ diff --git a/contents/AsyncDisplayKit/images/intelligent-preloading-ranges-with-names.png b/contents/AsyncDisplayKit/images/intelligent-preloading-ranges-with-names.png new file mode 100644 index 0000000..472163b Binary files /dev/null and b/contents/AsyncDisplayKit/images/intelligent-preloading-ranges-with-names.png differ diff --git a/contents/AsyncDisplayKit/images/lag-vsync.png b/contents/AsyncDisplayKit/images/lag-vsync.png new file mode 100644 index 0000000..9204986 Binary files /dev/null and b/contents/AsyncDisplayKit/images/lag-vsync.png differ diff --git a/contents/AsyncDisplayKit/images/layout-header.jpg b/contents/AsyncDisplayKit/images/layout-header.jpg new file mode 100644 index 0000000..f218a6b Binary files /dev/null and b/contents/AsyncDisplayKit/images/layout-header.jpg differ diff --git a/contents/AsyncDisplayKit/images/layout-hierarchy.png b/contents/AsyncDisplayKit/images/layout-hierarchy.png new file mode 100644 index 0000000..658829d Binary files /dev/null and b/contents/AsyncDisplayKit/images/layout-hierarchy.png differ diff --git a/contents/AsyncDisplayKit/images/layout-phase.png b/contents/AsyncDisplayKit/images/layout-phase.png new file mode 100644 index 0000000..b6fae26 Binary files /dev/null and b/contents/AsyncDisplayKit/images/layout-phase.png differ diff --git a/contents/AsyncDisplayKit/images/lazy-loading.png b/contents/AsyncDisplayKit/images/lazy-loading.png new file mode 100644 index 0000000..131dad3 Binary files /dev/null and b/contents/AsyncDisplayKit/images/lazy-loading.png differ diff --git a/contents/AsyncDisplayKit/images/lcd.png b/contents/AsyncDisplayKit/images/lcd.png new file mode 100644 index 0000000..71320cc Binary files /dev/null and b/contents/AsyncDisplayKit/images/lcd.png differ diff --git a/contents/AsyncDisplayKit/images/masonry.jpg b/contents/AsyncDisplayKit/images/masonry.jpg new file mode 100644 index 0000000..087cd70 Binary files /dev/null and b/contents/AsyncDisplayKit/images/masonry.jpg differ diff --git a/contents/AsyncDisplayKit/images/multi-layer-asdk.jpg b/contents/AsyncDisplayKit/images/multi-layer-asdk.jpg new file mode 100644 index 0000000..6eb1d06 Binary files /dev/null and b/contents/AsyncDisplayKit/images/multi-layer-asdk.jpg differ diff --git a/contents/AsyncDisplayKit/images/multi-layer.jpg b/contents/AsyncDisplayKit/images/multi-layer.jpg new file mode 100644 index 0000000..afd96c0 Binary files /dev/null and b/contents/AsyncDisplayKit/images/multi-layer.jpg differ diff --git a/contents/AsyncDisplayKit/images/network.jpg b/contents/AsyncDisplayKit/images/network.jpg new file mode 100644 index 0000000..f2cdd57 Binary files /dev/null and b/contents/AsyncDisplayKit/images/network.jpg differ diff --git a/contents/AsyncDisplayKit/images/normal-vsync.png b/contents/AsyncDisplayKit/images/normal-vsync.png new file mode 100644 index 0000000..9cb93a5 Binary files /dev/null and b/contents/AsyncDisplayKit/images/normal-vsync.png differ diff --git a/contents/AsyncDisplayKit/images/performance-chart-100-1000.jpeg b/contents/AsyncDisplayKit/images/performance-chart-100-1000.jpeg new file mode 100644 index 0000000..ca7aeac Binary files /dev/null and b/contents/AsyncDisplayKit/images/performance-chart-100-1000.jpeg differ diff --git a/contents/AsyncDisplayKit/images/performance-layout-10-90.jpeg b/contents/AsyncDisplayKit/images/performance-layout-10-90.jpeg new file mode 100644 index 0000000..3fcf4ff Binary files /dev/null and b/contents/AsyncDisplayKit/images/performance-layout-10-90.jpeg differ diff --git a/contents/AsyncDisplayKit/images/performance-loss.jpeg b/contents/AsyncDisplayKit/images/performance-loss.jpeg new file mode 100644 index 0000000..07d116a Binary files /dev/null and b/contents/AsyncDisplayKit/images/performance-loss.jpeg differ diff --git a/contents/AsyncDisplayKit/images/performance-nested-autolayout-frame.jpeg b/contents/AsyncDisplayKit/images/performance-nested-autolayout-frame.jpeg new file mode 100644 index 0000000..1f07b26 Binary files /dev/null and b/contents/AsyncDisplayKit/images/performance-nested-autolayout-frame.jpeg differ diff --git a/contents/AsyncDisplayKit/images/phone-in-hand.jpg b/contents/AsyncDisplayKit/images/phone-in-hand.jpg new file mode 100644 index 0000000..21a8d04 Binary files /dev/null and b/contents/AsyncDisplayKit/images/phone-in-hand.jpg differ diff --git a/contents/AsyncDisplayKit/images/placeholder-layer.png b/contents/AsyncDisplayKit/images/placeholder-layer.png new file mode 100644 index 0000000..306e71a Binary files /dev/null and b/contents/AsyncDisplayKit/images/placeholder-layer.png differ diff --git a/contents/AsyncDisplayKit/images/pros-cons.jpg b/contents/AsyncDisplayKit/images/pros-cons.jpg new file mode 100644 index 0000000..a742a0d Binary files /dev/null and b/contents/AsyncDisplayKit/images/pros-cons.jpg differ diff --git a/contents/AsyncDisplayKit/images/screen-tearing.jpg b/contents/AsyncDisplayKit/images/screen-tearing.jpg new file mode 100644 index 0000000..bd83f0c Binary files /dev/null and b/contents/AsyncDisplayKit/images/screen-tearing.jpg differ diff --git a/contents/AsyncDisplayKit/images/scrollview-demo.png b/contents/AsyncDisplayKit/images/scrollview-demo.png new file mode 100644 index 0000000..3f92e75 Binary files /dev/null and b/contents/AsyncDisplayKit/images/scrollview-demo.png differ diff --git a/contents/AsyncDisplayKit/images/stack.jpg b/contents/AsyncDisplayKit/images/stack.jpg new file mode 100644 index 0000000..7e9823e Binary files /dev/null and b/contents/AsyncDisplayKit/images/stack.jpg differ diff --git a/contents/AsyncDisplayKit/images/threshold.jpeg b/contents/AsyncDisplayKit/images/threshold.jpeg new file mode 100644 index 0000000..60bdf61 Binary files /dev/null and b/contents/AsyncDisplayKit/images/threshold.jpeg differ diff --git a/contents/AsyncDisplayKit/images/view-demonstrate.png b/contents/AsyncDisplayKit/images/view-demonstrate.png new file mode 100644 index 0000000..68e6a0a Binary files /dev/null and b/contents/AsyncDisplayKit/images/view-demonstrate.png differ diff --git a/contents/AsyncDisplayKit/images/view-layer-cg-compare.png b/contents/AsyncDisplayKit/images/view-layer-cg-compare.png new file mode 100644 index 0000000..a4f9e63 Binary files /dev/null and b/contents/AsyncDisplayKit/images/view-layer-cg-compare.png differ diff --git "a/contents/AsyncDisplayKit/\344\273\216 Auto Layout \347\232\204\345\270\203\345\261\200\347\256\227\346\263\225\350\260\210\346\200\247\350\203\275.md" "b/contents/AsyncDisplayKit/\344\273\216 Auto Layout \347\232\204\345\270\203\345\261\200\347\256\227\346\263\225\350\260\210\346\200\247\350\203\275.md" new file mode 100644 index 0000000..aff99d4 --- /dev/null +++ "b/contents/AsyncDisplayKit/\344\273\216 Auto Layout \347\232\204\345\270\203\345\261\200\347\256\227\346\263\225\350\260\210\346\200\247\350\203\275.md" @@ -0,0 +1,319 @@ +# 从 Auto Layout 的布局算法谈性能 + +> 这是使用 ASDK 性能调优系列的第二篇文章,前一篇文章中讲到了如何提升 iOS 应用的渲染性能,你可以点击 [这里](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/AsyncDisplayKit/提升%20iOS%20界面的渲染性能.md) 了解这部分的内容。 + +在上一篇文章中,我们提到了 iOS 界面的渲染过程以及如何对渲染过程进行优化。ASDK 的做法是将渲染绘制的工作抛到后台线程进行,并在每次 Runloop 结束时,将绘制结果交给 `CALayer` 进行展示。 + +而这篇文章就要从 iOS 中影响性能的另一大杀手,也就是万恶之源 Auto Layout(自动布局)来分析如何对 iOS 应用的性能进行优化以及 Auto Layout 到底为什么会影响性能? + +![box-layout](images/box-layout.jpg) + +## 把 Auto Layout 批判一番 + +由于在 2012 年苹果发布了 4.0 寸的 iPhone5,在 iOS 平台上出现了不同尺寸的移动设备,使得原有的 `frame` 布局方式无法很好地适配不同尺寸的屏幕,所以,为了解决这一问题 Auto Layout 就诞生了。 + +Auto Layout 的诞生并没有如同苹果的其它框架一样收到开发者的好评,它自诞生的第一天起就饱受 iOS 开发者的批评,其蹩脚、冗长的语法使得它在刚刚面世就被无数开发者吐槽,写了几个屏幕的代码都不能完成一个简单的布局,哪怕是 VFL(Visual Format Language)也拯救不了它。 + +真正使 Auto Layout 大规模投入使用的应该还是 [Masonry](https://github.com/SnapKit/Masonry),它使用了链式的语法对 Auto Layout 进行了很好的封装,使得 Auto Layout 更加简单易用;时至今日,开发者也在日常使用中发现了 Masonry 的各种问题,于是出现了各种各样的布局框架,不过这都是后话了。 + +![masonry](images/masonry.jpg) + +## Auto Layout 的原理和 Cassowary + +Auto Layout 的原理其实非常简单,在这里通过一个例子先简单的解释一下: + +![view-demonstrate](images/view-demonstrate.png) + +iOS 中视图所需要的布局信息只有两个,分别是 `origin/center` 和 `size`,在这里我们以 `origin & size` 为例,也就是 `frame` 时代下布局的需要的两个信息;这两个信息由四部分组成: + ++ `x` & `y` ++ `width` & `height` + +以左上角的 `(0, 0)` 为坐标的原点,找到坐标 `(x, y)`,然后绘制一个大小为 `(width, height)` 的矩形,这样就完成了一个最简单的布局。而 Auto Layout 的布局方式与上面所说的 `frame` 有些不同,`frame` 的原理是与父视图之间的绝对距离,但是 Auto Layout 中大部分的约束都是**描述性的**,表示视图间相对距离,以上图为例: + +```objectivec +A.left = Superview.left + 50 +A.top = Superview.top + 30 +A.width = 100 +A.height = 100 + +B.left = (A.left + A.width)/(A.right) + 30 +B.top = A.top +B.width = A.width +B.height = A.height +``` + +虽然上面的约束很好的表示了各个视图之间的关系,但是 Auto Layout 实际上并没有改变原有的 Hard-Coded 形式的布局方式,只是将原有没有太多意义的 `(x, y)` 值,变成了描述性的代码。 + +我们仍然需要知道布局信息所需要的四部分 `x`、`y`、`width` 以及 `height`。换句话说,我们要求解上述的**八元一次**方程组,将每个视图所需要的信息解出来;Cocoa 会在运行时求解上述的方程组,最终使用 `frame` 来绘制视图。 + +![layout-phase](images/layout-phase.png) + +### Cassowary 算法 + +在上世纪 90 年代,一个名叫 [Cassowary](https://en.wikipedia.org/wiki/Cassowary_(software)) 的布局算法解决了用户界面的布局问题,它通过将布局问题抽象成线性等式和不等式约束来进行求解。 + +Auto Layout 其实就是对 Cassowary 算法的一种实现,但是这里并不会对它展开介绍,有兴趣的读者可以在文章最后的 Reference 中了解一下 Cassowary 算法相关的文章。 + +> Auto Layout 的原理就是对**线性方程组或者不等式**的求解。 + +## Auto Layout 的性能 + +在使用 Auto Layout 进行布局时,可以指定一系列的约束,比如视图的高度、宽度等等。而每一个约束其实都是一个简单的线性等式或不等式,整个界面上的所有约束在一起就**明确地(没有冲突)**定义了整个系统的布局。 + +> 在涉及冲突发生时,Auto Layout 会尝试 break 一些优先级低的约束,尽量满足最多并且优先级最高的约束。 + +因为布局系统在最后仍然需要通过 `frame` 来进行,所以 Auto Layout 虽然为开发者在描述布局时带来了一些好处,不过它相比原有的布局系统加入了从约束计算 `frame` 的过程,而在这里,我们需要了解 Auto Layout 的布局性能如何。 + +![performance-loss](images/performance-loss.jpeg) + +因为使用 Cassowary 算法解决约束问题就是对线性等式或不等式求解,所以其时间复杂度就是**多项式时间**的,不难推测出,在处理极其复杂的 UI 界面时,会造成性能上的巨大损失。 + +在这里我们会对 Auto Layout 的性能进行测试,为了更明显的展示 Auto Layout 的性能,我们通过 `frame` 的性能建立一条基准线**以消除对象的创建和销毁、视图的渲染、视图层级的改变带来的影响**。 + +> 你可以在 [这里](https://github.com/Draveness/iOS-Source-Code-Analyze/tree/master/contents/AsyncDisplayKit/Layout) 找到这次对 Layout 性能测量使用的代码。 + +代码分别使用 Auto Layout 和 `frame` 对 N 个视图进行布局,测算其运行时间。 + +使用 AutoLayout 时,每个视图会随机选择两个视图对它的 `top` 和 `left` 进行约束,随机生成一个数字作为 `offset`;同时,还会用几个优先级高的约束保证视图的布局不会超出整个 `keyWindow`。 + +而下图就是对 100~1000 个视图布局所需要的时间的折线图。 + +> 这里的数据是在 OS X EL Captain,Macbook Air (13-inch Mid 2013)上的 iPhone 6s Plus 模拟器上采集的, Xcode 版本为 7.3.1。在其他设备上可能不会获得一致的信息,由于笔者的 iPhone 升级到了 iOS 10,所以没有办法真机测试,最后的结果可能会有一定的偏差。 + +![performance-chart-100-1000](images/performance-chart-100-1000.jpeg) + +从图中可以看到,使用 Auto Layout 进行布局的时间会是只使用 `frame` 的 **16 倍**左右,虽然这里的测试结果可能**受外界条件影响差异**比较大,不过 Auto Layout 的性能相比 `frame` 确实差很多,如果去掉设置 `frame` 的过程消耗的时间,Auto Layout 过程进行的计算量也是非常巨大的。 + +在上一篇文章中,我们曾经提到,想要让 iOS 应用的视图保持 60 FPS 的刷新频率,我们必须在 **1/60 = 16.67 ms** 之内完成包括布局、绘制以及渲染等操作。 + +也就是说如果当前界面上的视图大于 100 的话,使用 Auto Layout 是很难达到绝对流畅的要求的;而在使用 `frame` 时,同一个界面下哪怕有 500 个视图,也是可以在 16.67 ms 之内完成布局的。不过在一般情况下,在 iOS 的整个 `UIWindow` 中也不会一次性出现如此多的视图。 + +我们更关心的是,在日常开发中难免会使用 Auto Layout 进行布局,既然有 16.67 ms 这个限制,那么在界面上出现了多少个视图时,我才需要考虑其它的布局方式呢?在这里,我们将需要布局的视图数量减少一个量级,重新绘制一个图表: + +![performance-layout-10-90](images/performance-layout-10-90.jpeg) + +从图中可以看出,当对 **30 个左右视图**使用 Auto Layout 进行布局时,所需要的时间就会在 16.67 ms 左右,当然这里不排除一些其它因素的影响;到目前为止,会得出一个大致的结论,使用 Auto Layout 对复杂的 UI 界面进行布局时(大于 30 个视图)就会对性能有严重的影响(同时与设备有关,文章中不会考虑设备性能的差异性)。 + +上述对 Auto Layout 的使用还是比较简单的,而在日常使用中,使用嵌套的视图层级又非常正常。 + +> 在笔者对嵌套视图层级中使用 Auto Layout 进行布局时,当视图的数量超过了 500 时,模拟器直接就 crash 了,所以这里没有超过 500 个视图的数据。 + +我们对嵌套视图数量在 100~500 之间布局时间进行测量,并与 Auto Layout 进行比较: + +![performance-nested-autolayout-frame](images/performance-nested-autolayout-frame.jpeg) + +在视图数量大于 200 之后,随着视图数量的增加,使用 Auto Layout 对嵌套视图进行布局的时间相比非嵌套的布局成倍增长。 + +虽然说 Auto Layout 为开发者在多尺寸布局上提供了遍历,而且**支持跨越视图层级**的约束,但是由于其实现原理导致其时间复杂度为**多项式时间**,其性能损耗是仅使用 `frame` 的十几倍,所以在处理庞大的 UI 界面时表现差强人意。 + +> 在三年以前,有一篇关于 Auto Layout 性能分析的文章,可以点击这里了解这篇文章的内容 [Auto Layout Performance on iOS](http://floriankugler.com/2013/04/22/auto-layout-performance-on-ios/)。 + +## ASDK 的布局引擎 + +Auto Layout 不止在复杂 UI 界面布局的表现不佳,它还会强制视图在主线程上布局;所以在 ASDK 中提供了另一种可以在后台线程中运行的布局引擎,它的结构大致是这样的: + +![layout-hierarchy](images/layout-hierarchy.png) + +`ASLayoutSpec` 与下面的所有的 Spec 类都是继承关系,在视图需要布局时,会调用 `ASLayoutSpec` 或者它的子类的 `- measureWithSizeRange:` 方法返回一个用于布局的对象 [ASLayout](#aslayout)。 + +> `ASLayoutable` 是 ASDK 中一个协议,遵循该协议的类实现了一系列的布局方法。 + +当我们使用 ASDK 布局时,需要做下面四件事情中的一件: + ++ 提供 `layoutSpecBlock` ++ 覆写 `- layoutSpecThatFits:` 方法 ++ 覆写 `- calculateSizeThatFits:` 方法 ++ 覆写 `- calculateLayoutThatFits:` 方法 + +只有做上面四件事情中的其中一件才能对 ASDK 中的视图或者说结点进行布局。 + +方法 `- calculateSizeThatFits:` 提供了手动布局的方式,通过在该方法内对 `frame` 进行计算,返回一个当前视图的 `CGSize`。 + +而 `- layoutSpecThatFits:` 与 `layoutSpecBlock` 其实没什么不同,只是前者通过覆写方法返回 `ASLayoutSpec`;后者通过 block 的形式提供一种不需要子类化就可以完成布局的方法,两者可以看做是完全等价的。 + +`- calculateLayoutThatFits:` 方法有一些不同,它把上面的两种布局方式:手动布局和 Spec 布局封装成了一个接口,这样,无论是 `CGSize` 还是 `ASLayoutSpec` 最后都会以 `ASLayout` 的形式返回给方法调用者。 + +### 手动布局 + +这里简单介绍一下手动布局使用的 `-[ASDisplayNode calculatedSizeThatFits:]` 方法,这个方法与 `UIView` 中的 `-[UIView sizeThatFits:]` 非常相似,其区别只是在 ASDK 中,所有的计算出的大小都会通过缓存来提升性能。 + +```objectivec +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { + return _preferredFrameSize; +} +``` + +子类可以在这个方法中进行计算,通过覆写这个方法返回一个合适的大小,不过一般情况下都不会使用手动布局的方式。 + +### 使用 ASLayoutSpec 布局 + +在 ASDK 中,更加常用的是使用 `ASLayoutSpec` 布局,在上面提到的 `ASLayout` 是一个保存布局信息的媒介,而真正计算视图布局的代码都在 `ASLayoutSpec` 中;所有 ASDK 中的布局(手动 / Spec)都是由 `-[ASLayoutable measureWithSizeRange:]` 方法触发的,在这里我们以 `ASDisplayNode` 的调用栈为例看一下方法的执行过程: + +```objectivec +-[ASDisplayNode measureWithSizeRange:] + -[ASDisplayNode shouldMeasureWithSizeRange:] + -[ASDisplayNode calculateLayoutThatFits:] + -[ASDisplayNode layoutSpecThatFits:] + -[ASLayoutSpec measureWithSizeRange:] + +[ASLayout layoutWithLayoutableObject:constrainedSizeRange:size:sublayouts:] + -[ASLayout filteredNodeLayoutTree] +``` + +ASDK 的文档中推荐在子类中覆写 `- layoutSpecThatFits:` 方法,返回一个用于布局的 `ASLayoutSpec` 对象,然后使用 `ASLayoutSpec` 中的 `- measureWithSizeRange:` 方法对它指定的视图进行布局,不过通过覆写 [ASDK 的布局引擎](#asdk-的布局引擎) 一节中的其它方法也都是可以的。 + +如果我们使用 `ASStackLayoutSpec` 对视图进行布局的话,方法调用栈大概是这样的: + +```objectivec +-[ASDisplayNode measureWithSizeRange:] + -[ASDisplayNode shouldMeasureWithSizeRange:] + -[ASDisplayNode calculateLayoutThatFits:] + -[ASDisplayNode layoutSpecThatFits:] + -[ASStackLayoutSpec measureWithSizeRange:] + ASStackUnpositionedLayout::compute + ASStackPositionedLayout::compute ASStackBaselinePositionedLayout::compute +[ASLayout layoutWithLayoutableObject:constrainedSizeRange:size:sublayouts:] + -[ASLayout filteredNodeLayoutTree] +``` + +这里只是执行了 `ASStackLayoutSpec` 对应的 `- measureWithSizeRange:` 方法,对其中的视图进行布局。在 `- measureWithSizeRange:` 中调用了一些 C++ 方法 `ASStackUnpositionedLayout`、`ASStackPositionedLayout` 以及 `ASStackBaselinePositionedLayout` 的 `compute` 方法,这些方法完成了对 `ASStackLayoutSpec` 中视图的布局。 + +相比于 Auto Layout,ASDK 实现了一种完全不同的布局方式;比较类似与前端开发中的 `Flexbox` 模型,而 ASDK 其实就实现了 `Flexbox` 的一个子集。 + +在 ASDK 1.0 时代,很多开发者都表示希望 ASDK 中加入 ComponentKit 的布局引擎;而现在,ASDK 布局引擎的大部分代码都是从 [ComponentKit](http://componentkit.org) 中移植过来的(ComponentKit 是另一个 Facebook 团队开发的用于布局的框架)。 + +#### ASLayout + +`ASLayout` 表示当前的结点在布局树中的大小和位置;当然,它还有一些其它的奇怪的属性: + +```objectivec +@interface ASLayout : NSObject + +@property (nonatomic, weak, readonly) id layoutableObject; +@property (nonatomic, readonly) CGSize size; +@property (nonatomic, readwrite) CGPoint position; +@property (nonatomic, readonly) NSArray *sublayouts; +@property (nonatomic, readonly) CGRect frame; + +... + +@end +``` + +代码中的 `layoutableObject` 表示当前的对象,`sublayouts` 表示当前视图的子布局 `ASLayout` 数组。 + +整个类的实现都没有什么值得多说的,除了大量的构造方法,唯一一个做了一些事情的就是 `-[ASLayout filteredNodeLayoutTree]` 方法了: + +```objectivec +- (ASLayout *)filteredNodeLayoutTree { + NSMutableArray *flattenedSublayouts = [NSMutableArray array]; + struct Context { + ASLayout *layout; + CGPoint absolutePosition; + }; + std::queue queue; + queue.push({self, CGPointMake(0, 0)}); + while (!queue.empty()) { + Context context = queue.front(); + queue.pop(); + + if (self != context.layout && context.layout.type == ASLayoutableTypeDisplayNode) { + ASLayout *layout = [ASLayout layoutWithLayout:context.layout position:context.absolutePosition]; + layout.flattened = YES; + [flattenedSublayouts addObject:layout]; + } + + for (ASLayout *sublayout in context.layout.sublayouts) { + if (sublayout.isFlattened == NO) queue.push({sublayout, context.absolutePosition + sublayout.position}); + } + + return [ASLayout layoutWithLayoutableObject:_layoutableObject + constrainedSizeRange:_constrainedSizeRange + size:_size + sublayouts:flattenedSublayouts]; +} +``` + +而这个方法也只是将 `sublayouts` 中的内容展平,然后实例化一个新的 `ASLayout` 对象。 + +#### ASLayoutSpec + +`ASLayoutSpec` 的作用更像是一个抽象类,在真正使用 ASDK 的布局引擎时,都不会直接使用这个类,而是会用类似 `ASStackLayoutSpec`、`ASRelativeLayoutSpec`、`ASOverlayLayoutSpec` 以及 `ASRatioLayoutSpec` 等子类。 + +笔者不打算一行一行代码深入讲解其内容,简单介绍一下最重要的 `ASStackLayoutSpec`。 + +![stack](images/stack.jpg) + +`ASStackLayoutSpec` 从 `Flexbox` 中获得了非常多的灵感,比如说 `justifyContent`、`alignItems` 等属性,它和苹果的 `UIStackView` 比较类似,不过底层并没有使用 Auto Layout 进行计算。如果没有接触过 `ASStackLayoutSpec` 的开发者,可以通过这个小游戏 [Foggy-ASDK-Layout](http://nguyenhuy.github.io/froggy-asdk-layout/) 快速学习 `ASStackLayoutSpec` 的使用。 + +### 关于缓存以及异步并发 + +因为计算视图的 `CGRect` 进行布局是一种非常昂贵的操作,所以 ASDK 在这里面加入了缓存机制,在每次执行 `- measureWithSizeRange:` 方法时,都会通过 `-shouldMeasureWithSizeRange:` 判断是否需要重新计算布局: + +```objectivec +- (BOOL)shouldMeasureWithSizeRange:(ASSizeRange)constrainedSize { + return [self _hasDirtyLayout] || !ASSizeRangeEqualToSizeRange(constrainedSize, _calculatedLayout.constrainedSizeRange); +} + +- (BOOL)_hasDirtyLayout { + return _calculatedLayout == nil || _calculatedLayout.isDirty; +} +``` + +在一般情况下,只有当前结点被标记为 `dirty` 或者这一次布局传入的 `constrainedSize` 不同时,才需要进行重新计算。在不需要重新计算布局的情况下,只需要直接返回 `_calculatedLayout` 布局对象就可以了。 + +因为 ASDK 实现的布局引擎其实只是对 `frame` 的计算,所以无论是在主线程还是后台的异步并发进程中都是可以执行的,也就是说,你可以在任意线程中调用 `- measureWithSizeRange:` 方法,ASDK 中的一些 `ViewController` 比如:`ASDataViewController` 就会在后台并发进程中执行该方法: + +```objectivec +- (NSArray *)_layoutNodesFromContexts:(NSArray *)contexts { + ... + + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_apply(nodeCount, queue, ^(size_t i) { + ASIndexedNodeContext *context = contexts[i]; + ASCellNode *node = [context allocateNode]; + if (node == nil) node = [[ASCellNode alloc] init]; + + CGRect frame = CGRectZero; + frame.size = [node measureWithSizeRange:context.constrainedSize].size; + node.frame = frame; + + [ASDataController _didLayoutNode]; + }); + + ... + + return nodes; +} +``` + +> 上述代码做了比较大的修改,将原有一些方法调用放到了当前方法中,并省略了大量的代码。 + +### 关于性能的对比 + +由于 ASDK 的布局引擎的问题,其性能比较难以测试,在这里只对 ASDK 使用 `ASStackLayoutSpec` 的**布局计算时间**进行了测试,不包括视图的渲染以及其它时间: + +![async-node-calculate](images/async-node-calculate.jpeg) + +测试结果表明 `ASStackLayoutSpec` 花费的布局时间与结点的数量成正比,哪怕计算 100 个视图的布局也只需要 **8.89 ms**,虽然这里没有包括视图的渲染时间,不过与 Auto Layout 相比性能还是有比较大的提升。 + +## 总结 + +其实 ASDK 的布局引擎大部分都是对 ComponentKit 的封装,不过由于摆脱了 Auto Layout 这一套低效但是通用的布局方式,ASDK 的布局计算不仅在后台并发线程中进行、而且通过引入 `Flexbox` 提升了布局的性能,但是 ASDK 的使用相对比较复杂,如果只想对布局性能进行优化,更推荐单独使用 ComponentKit 框架。 + +## References + ++ [Cassowary, Cocoa Auto Layout, and enaml constraints](http://stacks.11craft.com/cassowary-cocoa-autolayout-and-enaml-constraints.html) ++ [Solving constraint systems](http://cassowary.readthedocs.io/en/latest/topics/theory.html) ++ [Auto Layout Performance on iOS](http://floriankugler.com/2013/04/22/auto-layout-performance-on-ios/) ++ [The Cassowary Linear Arithmetic Constraint Solving Algorithm: Interface and Implementation](https://constraints.cs.washington.edu/cassowary/cassowary-tr.pdf) ++ [The Cassowary Linear Arithmetic Constraint Solving Algorithm](http://constraints.cs.washington.edu/solvers/cassowary-tochi.pdf) ++ [Solving Linear Arithmetic Constraints for User Interface Applications](http://constraints.cs.washington.edu/solvers/uist97.pdf) ++ [AsyncDisplayKit 介绍(二)布局系统](https://medium.com/@jasonyuh/asyncdisplaykit介绍-二-布局系统-1f1a674cf644#.8jskykm15) + +> Github Repo:[iOS-Source-Code-Analyze](https://github.com/draveness/iOS-Source-Code-Analyze) +> +> Follow: [Draveness · GitHub](https://github.com/Draveness) +> +> Source: http://draveness.me/layout-performance + + diff --git "a/contents/AsyncDisplayKit/\344\275\277\347\224\250 ASDK \346\200\247\350\203\275\350\260\203\344\274\230 - \346\217\220\345\215\207 iOS \347\225\214\351\235\242\347\232\204\346\270\262\346\237\223\346\200\247\350\203\275.md" "b/contents/AsyncDisplayKit/\344\275\277\347\224\250 ASDK \346\200\247\350\203\275\350\260\203\344\274\230 - \346\217\220\345\215\207 iOS \347\225\214\351\235\242\347\232\204\346\270\262\346\237\223\346\200\247\350\203\275.md" new file mode 100644 index 0000000..07bf5cf --- /dev/null +++ "b/contents/AsyncDisplayKit/\344\275\277\347\224\250 ASDK \346\200\247\350\203\275\350\260\203\344\274\230 - \346\217\220\345\215\207 iOS \347\225\214\351\235\242\347\232\204\346\270\262\346\237\223\346\200\247\350\203\275.md" @@ -0,0 +1,808 @@ +# 使用 ASDK 性能调优 - 提升 iOS 界面的渲染性能 + +> 这一系列的文章会从几个方面对 [ASDK](http://asyncdisplaykit.org) 在性能调优方面策略的实现进行分析,帮助读者理解 ASDK 如何做到使复杂的 UI 界面达到 60 FPS 的刷新频率的;本篇文章会从视图的渲染层面讲解 ASDK 对于渲染过程的优化并对 ASDK 进行概述。 + +在客户端或者前端开发中,对于性能的优化,尤其是 UI,往往都不是最先考虑的问题。 + +因为在大多数场景下,使用更加复杂的高性能代码替代可用的代码经常会导致代码的可维护性下降,所以更需要我们开发者对优化的时间点以及原因有一个比较清楚的认识,避免过度优化带来的问题。 + +对 iOS 开发比较熟悉的开发者都知道,iOS 中的性能问题大多是阻塞主线程导致用户的交互反馈出现可以感知的延迟。 + +![scrollview-demo](images/scrollview-demo.png) + +详细说起来,大体有三种原因: + +1. UI 渲染需要时间较长,无法按时提交结果; +2. 一些需要**密集计算**的处理放在了主线程中执行,导致主线程被阻塞,无法渲染 UI 界面; +3. 网络请求由于网络状态的问题响应较慢,UI 层由于没有模型返回无法渲染。 + +上面的这些问题都会影响应用的性能,最常见的表现就是 `UITableView` 在滑动时没有达到 **60 FPS**,用户能感受到明显的卡顿。 + +## 屏幕的渲染 + +相信点开这篇文章的大多数开发者都知道 FPS 是什么,那么如何才能优化我们的 App 使其达到 60 FPS 呢?在具体了解方法之前,我们先退一步,提出另一个问题,屏幕是如何渲染的? + +> 对于第一个问题,可能需要几篇文章来回答,希望整个系列的文章能给你一个满意的答案。3 + +### CRT 和 LCD + +屏幕的渲染可能要从 [CRT(Cathode ray tube) 显示器](https://en.wikipedia.org/wiki/Cathode_ray_tube)和 [LCD(Liquid-crystal display) 显示器](https://en.wikipedia.org/wiki/Liquid-crystal_display)讲起。 + +![CRT](images/CRT.png) + +CRT 显示器是比较古老的技术,它使用阴极电子枪发射电子,在阴极高压的作用下,电子由电子枪射向荧光屏,使荧光粉发光,将图像显示在屏幕上,这也就是用磁铁靠近一些老式电视机的屏幕会让它们变色的原因。 + +而 FPS 就是 CRT 显示器的刷新频率,电子枪每秒会对显示器上内容进行 60 - 100 次的刷新,哪怕在我们看来没有任何改变。 + +![lcd](images/lcd.png) + + +但是 LCD 的原理与 CRT 非常不同,LCD 的成像原理跟光学有关: + ++ 在不加电压下,光线会沿着液晶分子的间隙前进旋转 90°,所以光可以通过; ++ 在加入电压之后,光沿着液晶分子的间隙直线前进,被滤光板挡住。 + +如果你可以翻墙,相信下面的视频会更好得帮助你理解 LCD 的工作原理: + + + +LCD 的成像原理虽然与 CRT 截然不同,每一个像素的颜色可以**在需要改变时**才去改变电压,也就是不需要刷新频率,但是由于一些历史原因,LCD 仍然需要按照一定的刷新频率向 GPU 获取新的图像用于显示。 + +### 屏幕撕裂 + +但是显示器只是用于将图像显示在屏幕上,谁又是图像的提供者呢?图像都是我们经常说的 GPU 提供的。 + +而这导致了另一个问题,由于 GPU 生成图像的频率与显示器刷新的频率是不相关的,那么在显示器刷新时,GPU 没有准备好需要显示的图像怎么办;或者 GPU 的渲染速度过快,显示器来不及刷新,GPU 就已经开始渲染下一帧图像又该如何处理? + +![screen-tearing](images/screen-tearing.jpg) + +如果解决不了这两个问题,就会出现上图中的*屏幕撕裂*(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 拷贝帧的过程依然需要消耗一定的时间,如果屏幕在拷贝图像时刷新,仍然会导致屏幕撕裂问题。 + +![how-to-solve-tearing-proble](images/how-to-solve-tearing-problem.jpg) + +引入多个缓冲区可以有效地**缓解**屏幕撕裂,也就是同时使用一个*帧缓冲区*(frame buffer)和多个*后备缓冲区*(back buffer);在每次显示器请求内容时,都会从**帧缓冲区**中取出图像然后渲染。 + +虽然缓冲区可以减缓这些问题,但是却不能解决;如果后备缓冲区绘制完成,而帧缓冲区的图像没有被渲染,后备缓冲区中的图像就会覆盖帧缓冲区,仍然会导致屏幕撕裂。 + +解决这个问题需要另一个机制的帮助,也就是垂直同步(Vertical synchronization),简称 V-Sync 来解决。 + +### V-Sync + +V-Sync 的主要作用就是保证**只有在帧缓冲区中的图像被渲染之后,后备缓冲区中的内容才可以被拷贝到帧缓冲区中**,理想情况下的 V-Sync 会按这种方式工作: + +![normal-vsyn](images/normal-vsync.png) + +每次 V-Sync 发生时,CPU 以及 GPU 都已经完成了对图像的处理以及绘制,显示器可以直接拿到缓冲区中的帧。但是,如果 CPU 或者 GPU 的处理需要的时间较长,就会发生掉帧的问题: + +![lag-vsyn](images/lag-vsync.png) + + +在 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](images/cpu-gpu.jpg) + +很多 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) 就根据上述的策略帮助我们对应用性能进行优化。 + +![asdk-logo](images/asdk-logo.png) + +AsyncDisplayKit(以下简称 ASDK)是由 Facebook 开源的一个 iOS 框架,能够帮助最复杂的 UI 界面保持流畅和快速响应。 + +ASDK 从开发到开源大约经历了一年多的时间,它其实并不是一个简单的框架~~它是一个复杂的框架~~,更像是对 UIKit 的重新实现,把整个 UIKit 以及 CALayer 层封装成一个一个 `Node`,**将昂贵的渲染、图片解码、布局以及其它 UI 操作移出主线程**,这样主线程就可以对用户的操作及时做出反应。 + +很多分析 ASDK 的文章都会有这么一张图介绍框架中的最基本概念: + +![asdk-hierarchy](images/asdk-hierarchy.png) + +在 ASDK 中最基本的单位就是 `ASDisplayNode`,每一个 node 都是对 `UIView` 以及 `CALayer` 的抽象。但是与 `UIView` 不同的是,`ASDisplayNode` 是线程安全的,它可以在后台线程中完成初始化以及配置工作。 + +如果按照 60 FPS 的刷新频率来计算,每一帧的渲染时间只有 16ms,在 16ms 的时间内要完成对 `UIView` 的创建、布局、绘制以及渲染,CPU 和 GPU 面临着巨大的压力。 + +![apple-a9](images/apple-a9.jpg) + +但是从 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)self; + } else { + _view = [self _viewToLoad]; + _view.asyncdisplaykit_node = self; + _layer = _view.layer; + } + _layer.asyncdisplaykit_node = self; + + self.asyncLayer.asyncDelegate = self; +} +``` + +因为 `UIView` 和 `CALayer` 虽然都可以用于展示内容,不过由于 `UIView` 可以用于处理用户的交互,所以如果不需要使用 `UIView` 的特性,直接使用 `CALayer` 进行渲染,能够节省大量的渲染时间。 + +> 如果你使用 Xcode 查看过视图的层级,那么你应该知道,`UIView` 在 Debug View Hierarchy 中是有层级的;而 `CALayer` 并没有,它门的显示都在一个平面上。 + +上述方法中的 `-[ASDisplayNode _layerToLoad]` 以及 `[ASDisplayNode _viewToLoad]` 都只会根据当前节点的 `layerClass` 或者 `viewClass` 初始化一个对象。 + +> [Layer Trees vs. Flat Drawing – Graphics Performance Across iOS Device Generations](http://floriankugler.com/2013/05/24/layer-trees-vs-flat-drawing-graphics-performance-across-ios-device-generations/) 这篇文章比较了 `UIView` 和 `CALayer` 的渲染时间。 + +![view-layer-cg-compare](images/view-layer-cg-compare.png) + +`-[ASDisplayNode asyncLayer]` 只是对当前 `node` 持有的 `layer` 进行封装,确保会返回一个 `_ASDisplayLayer` 的实例: + +```objectivec +- (_ASDisplayLayer *)asyncLayer { + ASDN::MutexLocker l(_propertyLock); + return [_layer isKindOfClass:[_ASDisplayLayer class]] ? (_ASDisplayLayer *)_layer : nil; +} +``` + +最重要的是 `-[ASDisplayNode _loadViewOrLayerIsLayerBacked:]` 方法会将当前节点设置为 `asyncLayer` 的代理,在后面会使用 `ASDisplayNode` 为 `CALayer` 渲染内容。 + +### 视图层级 + +在初始化工作完成之后,当 `ASDisplayNode` 第一次被加入到视图的层级时,`-[_ASDisplayView willMoveToWindow:]` 就会被调用。 + +#### _ASDisplayView 和 _ASDisplayLayer + +`_ASDisplayView` 和 `_ASDisplayLayer` 都是私有类,它们之间的对应关系其实和 `UIView` 与 `CALayer` 完全相同。 + +```objectivec ++ (Class)layerClass { + return [_ASDisplayLayer class]; +} +``` + +`_ASDisplayView` 覆写了很多跟视图层级改变有关的方法: + ++ `-[_ASDisplayView willMoveToWindow:]` ++ `-[_ASDisplayView didMoveToWindow]` ++ `-[_ASDisplayView willMoveToSuperview:]` ++ `-[_ASDisplayView didMoveToSuperview]` + +它们用于在视图的层级改变时,通知对应 `ASDisplayNode` 作出相应的反应,比如 `-[_ASDisplayView willMoveToWindow:]` 方法会在视图被加入层级时调用: + +```objectivec +- (void)willMoveToWindow:(UIWindow *)newWindow { + BOOL visible = (newWindow != nil); + if (visible && !_node.inHierarchy) { + [_node __enterHierarchy]; + } +} +``` + +#### setNeedsDisplay + +当前视图如果不在视图层级中,就会通过 `_node` 的实例方法 `-[ASDisplayNode __enterHierarchy]` 加入视图层级: + +```objectivec +- (void)__enterHierarchy { + if (!_flags.isInHierarchy && !_flags.visibilityNotificationsDisabled && ![self __selfOrParentHasVisibilityNotificationsDisabled]) { + _flags.isEnteringHierarchy = YES; + _flags.isInHierarchy = YES; + + if (_flags.shouldRasterizeDescendants) { + [self _recursiveWillEnterHierarchy]; + } else { + [self willEnterHierarchy]; + } + _flags.isEnteringHierarchy = NO; + + # 更新 layer 显示的内容 + } +} + +``` + +> `_flags` 是 `ASDisplayNodeFlags` 结构体,用于标记当前 `ASDisplayNode` 的一些 BOOL 值,比如,异步显示、栅格化子视图等等,你不需要知道都有什么,根据这些值的字面意思理解就已经足够了。 + +上述方法的前半部分只是对 `_flags` 的标记,如果需要将当前视图的子视图栅格化,也就是**将它的全部子视图与当前视图压缩成一个图层**,就会向这些视图递归地调用 `-[ASDisplayNode willEnterHierarchy]` 方法通知目前的状态: + +```objectivec +- (void)_recursiveWillEnterHierarchy { + _flags.isEnteringHierarchy = YES; + [self willEnterHierarchy]; + _flags.isEnteringHierarchy = NO; + + for (ASDisplayNode *subnode in self.subnodes) { + [subnode _recursiveWillEnterHierarchy]; + } +} +``` + +而 `-[ASDisplayNode willEnterHierarchy]` 会修改当前节点的 `interfaceState` 到 `ASInterfaceStateInHierarchy`,表示当前节点不包含在 `cell` 或者其它,但是在 `window` 中。 + +```objectivec +- (void)willEnterHierarchy { + if (![self supportsRangeManagedInterfaceState]) { + self.interfaceState = ASInterfaceStateInHierarchy; + } +} +``` + +当前结点需要被显示在屏幕上时,如果其内容 `contents` 为空,就会调用 `-[CALayer setNeedsDisplay]` 方法将 `CALayer` 标记为脏的,通知系统需要在下一个绘制循环中重绘视图: + +```objectivec +- (void)__enterHierarchy { + if (!_flags.isInHierarchy && !_flags.visibilityNotificationsDisabled && ![self __selfOrParentHasVisibilityNotificationsDisabled]) { + + # 标记节点的 flag + + if (self.contents == nil) { + CALayer *layer = self.layer; + [layer setNeedsDisplay]; + + if ([self _shouldHavePlaceholderLayer]) { + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + [self _setupPlaceholderLayerIfNeeded]; + _placeholderLayer.opacity = 1.0; + [CATransaction commit]; + [layer addSublayer:_placeholderLayer]; + } + } + } +} +``` + +在将 `CALayer` 标记为 dirty 之后,在绘制循环中就会执行 `-[CALayer display]` 方法,对它要展示的内容进行绘制;如果当前视图需要一些占位图,那么就会在这里的代码中,为当前 `node` 对应的 `layer` 添加合适颜色的占位层。 + +![placeholder-laye](images/placeholder-layer.png) + +### 派发异步绘制事务 + +在上一节中调用 `-[CALayer setNeedsDisplay]` 方法将当前节点标记为 dirty 之后,在下一个绘制循环时就会对所有需要重绘的 `CALayer` 执行 `-[CALayer display]`,这也是这一小节需要分析的方法的入口: + +```objectivec +- (void)display { + [self _hackResetNeedsDisplay]; + + ASDisplayNodeAssertMainThread(); + if (self.isDisplaySuspended) return; + + [self display:self.displaysAsynchronously]; +} +``` + +这一方法的调用栈比较复杂,在具体分析之前,笔者会先给出这个方法的调用栈,给读者一个关于该方法实现的简要印象: + +```objectivec +-[_ASDisplayLayer display] + -[_ASDisplayLayer display:] // 将绘制工作交给 ASDisplayNode 处理 + -[ASDisplayNode(AsyncDisplay) displayAsyncLayer:asynchronously:] + -[ASDisplayNode(AsyncDisplay) _displayBlockWithAsynchronous:isCancelledBlock:rasterizing:] + -[ASDisplayNode(AsyncDisplay) _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:displayBlocks:] + -[CALayer(ASDisplayNodeAsyncTransactionContainer) asyncdisplaykit_parentTransactionContainer] + -[CALayer(ASDisplayNodeAsyncTransactionContainer) asyncdisplaykit_asyncTransaction] + -[_ASAsyncTransaction initWithCallbackQueue:completionBlock:] + -[_ASAsyncTransactionGroup addTransactionContainer:] + -[_ASAsyncTransaction addOperationWithBlock:priority:queue:completion:] + ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block) + void dispatch_async(dispatch_queue_t queue, dispatch_block_t block); +``` + +`-[_ASDisplayLayer display]` 在调用栈中其实会创建一个 `displayBlock`,它其实是一个使用 Core Graphics 进行图像绘制的过程,整个绘制过程是通过事务的形式进行管理的;而 `displayBlock` 会被 GCD 分发到后台的并发进程来处理。 + +调用栈中的第二个方法 `-[_ASDisplayLayer display]` 会将异步绘制的工作交给自己的 `asyncDelegate`,也就是[第一部分](#uiview-和-calayer-的加载)中设置的 `ASDisplayNode`: + +```objectivec +- (void)display:(BOOL)asynchronously { + [_asyncDelegate displayAsyncLayer:self asynchronously:asynchronously]; +} +``` + +#### ASDisplayNode(AsyncDisplay) + +这里省略了一部分 `-[ASDisplayNode(AsyncDisplay) displayAsyncLayer:asynchronously:]` 方法的实现: + +```objectivec +- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously { + ASDisplayNodeAssertMainThread(); + + ... + + asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:asynchronously isCancelledBlock:isCancelledBlock rasterizing:NO]; + + if (!displayBlock) return; + + asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id value, BOOL canceled){ + ASDisplayNodeCAssertMainThread(); + if (!canceled && !isCancelledBlock()) { + UIImage *image = (UIImage *)value; + _layer.contentsScale = self.contentsScale; + _layer.contents = (id)image.CGImage; + } + }; + + if (asynchronously) { + CALayer *containerLayer = _layer.asyncdisplaykit_parentTransactionContainer ? : _layer; + _ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction; + [transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock]; + } else { + UIImage *contents = (UIImage *)displayBlock(); + completionBlock(contents, NO); + } +} +``` + +省略后的代码脉络非常清晰,`-[ASDisplayNode(AsyncDisplay) _displayBlockWithAsynchronous:isCancelledBlock:rasterizing:]` 返回一个用于 `displayBlock`,然后构造一个 `completionBlock`,在绘制结束时执行,在主线程中设置当前 `layer` 的内容。 + +如果当前的渲染是异步的,就会将 `displayBlock` 包装成一个事务,添加到队列中执行,否则就会同步执行当前的 block,并执行 `completionBlock` 回调,通知 `layer` 更新显示的内容。 + +同步显示的部分到这里已经很清楚了,我们更关心的其实还是异步绘制的部分,因为这部分才是 ASDK 提升效率的关键;而这就要从获取 `displayBlock` 的方法开始了解了。 + +#### displayBlock 的构建 + +`displayBlock` 的创建一般分为三种不同的方式: + +1. 将当前视图的子视图压缩成一层绘制在当前页面上 +2. 使用 `- displayWithParameters:isCancelled:` 返回一个 `UIImage`,对图像节点 `ASImageNode` 进行绘制 +3. 使用 `- drawRect:withParameters:isCancelled:isRasterizing:` 在 CG 上下文中绘制文字节点 `ASTextNode` + +这三种方式都通过 ASDK 来优化视图的渲染速度,这些操作最后都会扔到后台的并发线程中进行处理。 + +> 下面三个部分的代码经过了删减,省略了包括取消绘制、通知代理、控制并发数量以及用于调试的代码。 + +##### 栅格化子视图 + +如果当前的视图需要栅格化子视图,就会进入启用下面的构造方式创建一个 block,它会递归地将子视图绘制在父视图上: + +```objectivec +- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing { + asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil; + ASDisplayNodeFlags flags = _flags; + + if (!rasterizing && self.shouldRasterizeDescendants) { + NSMutableArray *displayBlocks = [NSMutableArray array]; + [self _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:isCancelledBlock displayBlocks:displayBlocks]; + + CGFloat contentsScaleForDisplay = self.contentsScaleForDisplay; + BOOL opaque = self.opaque && CGColorGetAlpha(self.backgroundColor.CGColor) == 1.0f; + + displayBlock = ^id{ + + UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay); + + for (dispatch_block_t block in displayBlocks) { + block(); + } + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return image; + }; + } else if (flags.implementsInstanceImageDisplay || flags.implementsImageDisplay) { + #:绘制 UIImage + } else if (flags.implementsInstanceDrawRect || flags.implementsDrawRect) { + #:提供 context,使用 CG 绘图 + } + + return [displayBlock copy]; +} +``` + +在压缩视图层级的过程中就会调用 `-[ASDisplayNode(AsyncDisplay) _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:displayBlocks:]` 方法,获取子视图的所有 `displayBlock`,在得到 `UIGraphicsBeginImageContextWithOptions` 需要的参数之后,创建一个新的 context,执行了所有的 `displayBlock` 将子视图的绘制到当前图层之后,使用 `UIGraphicsGetImageFromCurrentImageContext` 取出图层的内容并返回。 + +`-[ASDisplayNode(AsyncDisplay) _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:displayBlocks:]` 的实现还是有些繁琐的,它主要的功能就是使用 Core Graphics 进行绘图,将背景颜色、仿射变换、位置大小以及圆角等参数绘制到当前的上下文中,而且这个过程是递归的,直到不存在或者不需要绘制子节点为止。 + +##### 绘制图片 + +`displayBlock` 的第二种绘制策略更多地适用于图片节点 `ASImageNode` 的绘制: + +```objectivec +- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing { + asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil; + ASDisplayNodeFlags flags = _flags; + + if (!rasterizing && self.shouldRasterizeDescendants) { + #:栅格化 + } else if (flags.implementsInstanceImageDisplay || flags.implementsImageDisplay) { + id drawParameters = [self drawParameters]; + + displayBlock = ^id{ + UIImage *result = nil; + if (flags.implementsInstanceImageDisplay) { + result = [self displayWithParameters:drawParameters isCancelled:isCancelledBlock]; + } else { + result = [[self class] displayWithParameters:drawParameters isCancelled:isCancelledBlock]; + } + return result; + }; + } else if (flags.implementsInstanceDrawRect || flags.implementsDrawRect) { + #:提供 context,使用 CG 绘图 + } + + return [displayBlock copy]; +} +``` + +通过 `- displayWithParameters:isCancelled:` 的执行返回一个图片,不过这里的绘制也离不开 Core Graphics 的一些 C 函数,你会在 `-[ASImageNode displayWithParameters:isCancelled:]` 中看到对于 CG 的运用,它会使用 `drawParameters` 来修改并绘制自己持有的 `image` 对象。 + +##### 使用 CG 绘图 + +文字的绘制一般都会在 `- drawRect:withParameters:isCancelled:isRasterizing:` 进行,这个方法只是提供了一个合适的用于绘制的上下文,该方法不止可以绘制文字,只是在这里绘制文字比较常见: + +```objectivec +- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing { + asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil; + ASDisplayNodeFlags flags = _flags; + + if (!rasterizing && self.shouldRasterizeDescendants) { + #:栅格化 + } else if (flags.implementsInstanceImageDisplay || flags.implementsImageDisplay) { + #:绘制 UIImage + } else if (flags.implementsInstanceDrawRect || flags.implementsDrawRect) { + if (!rasterizing) { + UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay); + } + + if (flags.implementsInstanceDrawRect) { + [self drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing]; + } else { + [[self class] drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing]; + } + + UIImage *image = nil; + if (!rasterizing) { + image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + + return image; + }; + } + + return [displayBlock copy]; +} +``` + +上述代码跟第一部分比较像,区别是这里不会栅格化子视图;代码根据情况会决定是否重新开一个新的上下文,然后通过 `- drawRect:withParameters:isCancelled:isRasterizing:` 方法实现绘制。 + +#### 管理绘制事务 + +ASDK 提供了一个私有的管理事务的机制,由三部分组成 `_ASAsyncTransactionGroup`、`_ASAsyncTransactionContainer` 以及 `_ASAsyncTransaction`,这三者各自都有不同的功能: + ++ `_ASAsyncTransactionGroup` 会在初始化时,向 Runloop 中注册一个回调,在每次 Runloop 结束时,执行回调来提交 `displayBlock` 执行的结果 ++ `_ASAsyncTransactionContainer` 为当前 `CALayer` 提供了用于保存事务的容器,并提供了获取新的 `_ASAsyncTransaction` 实例的便利方法 ++ `_ASAsyncTransaction` 将异步操作封装成了轻量级的事务对象,使用 C++ 代码对 GCD 进行了封装 + +从上面的小节中,我们已经获取到了用于绘制的 `displayBlock`,然后就需要将 block 添加到绘制事务中: + +```objectivec +- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously { + ... + + if (asynchronously) { + CALayer *containerLayer = _layer.asyncdisplaykit_parentTransactionContainer ? : _layer; + _ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction; + [transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock]; + } else { + ... + } +} +``` + +前两行代码是获取 `_ASAsyncTransaction` 实例的过程,这个实例会包含在一个 `layer` 的哈希表中,最后调用的实例方法 `-[_ASAsyncTransaction addOperationWithBlock:priority:queue:completion:]` 会把用于绘制的 `displayBlock` 添加到后台并行队列中: + +```objectivec ++ (dispatch_queue_t)displayQueue { + static dispatch_queue_t displayQueue = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + displayQueue = dispatch_queue_create("org.AsyncDisplayKit.ASDisplayLayer.displayQueue", DISPATCH_QUEUE_CONCURRENT); + dispatch_set_target_queue(displayQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)); + }); + + return displayQueue; +} +``` + +这个队列是一个并行队列,并且优先级是 `DISPATCH_QUEUE_PRIORITY_HIGH`,**确保 UI 的渲染会在其它异步操作执行之前进行**,而 `-[_ASAsyncTransaction addOperationWithBlock:priority:queue:completion:]` 中会初始化 `ASDisplayNodeAsyncTransactionOperation` 的实例,然后传入 `completionBlock`,在绘制结束时回调: + +```objectivec +- (void)addOperationWithBlock:(asyncdisplaykit_async_transaction_operation_block_t)block priority:(NSInteger)priority queue:(dispatch_queue_t)queue completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion { + ASDisplayNodeAssertMainThread(); + + [self _ensureTransactionData]; + + ASDisplayNodeAsyncTransactionOperation *operation = [[ASDisplayNodeAsyncTransactionOperation alloc] initWithOperationCompletionBlock:completion]; + [_operations addObject:operation]; + _group->schedule(priority, queue, ^{ + @autoreleasepool { + operation.value = block(); + } + }); +} +``` + +`schedule` 方法是一个 C++ 方法,它会向 `ASAsyncTransactionQueue::Group` 中派发一个 block,这个 block 中会执行 `displayBlock`,然后将结果传给 `operation.value`: + +```objectivec +void ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block) { + ASAsyncTransactionQueue &q = _queue; + ASDN::MutexLocker locker(q._mutex); + + DispatchEntry &entry = q._entries[queue]; + + Operation operation; + operation._block = block; + operation._group = this; + operation._priority = priority; + entry.pushOperation(operation); + + ++_pendingOperations; + + NSUInteger maxThreads = [NSProcessInfo processInfo].activeProcessorCount * 2; + + if ([[NSRunLoop mainRunLoop].currentMode isEqualToString:UITrackingRunLoopMode]) + --maxThreads; + + if (entry._threadCount < maxThreads) { + bool respectPriority = entry._threadCount > 0; + ++entry._threadCount; + + dispatch_async(queue, ^{ + while (!entry._operationQueue.empty()) { + Operation operation = entry.popNextOperation(respectPriority); + { + if (operation._block) { + operation._block(); + } + operation._group->leave(); + operation._block = nil; + } + } + --entry._threadCount; + + if (entry._threadCount == 0) { + q._entries.erase(queue); + } + }); + } +} +``` + +`ASAsyncTransactionQueue::GroupImpl` 其实现其实就是对 GCD 的封装,同时添加一些最大并发数、线程锁的功能。通过 `dispatch_async` 将 block 分发到 `queue` 中,立刻执行 block,将数据传回 `ASDisplayNodeAsyncTransactionOperation` 实例。 + +### 回调 + +在 `_ASAsyncTransactionGroup` 调用 `mainTransactionGroup` 类方法获取单例时,会通过 `+[_ASAsyncTransactionGroup registerTransactionGroupAsMainRunloopObserver]` 向 Runloop 中注册回调: + +```objectivec ++ (void)registerTransactionGroupAsMainRunloopObserver:(_ASAsyncTransactionGroup *)transactionGroup { + static CFRunLoopObserverRef observer; + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + CFOptionFlags activities = (kCFRunLoopBeforeWaiting | kCFRunLoopExit); + CFRunLoopObserverContext context = {0, (__bridge void *)transactionGroup, &CFRetain, &CFRelease, NULL}; + + observer = CFRunLoopObserverCreate(NULL, activities, YES, INT_MAX, &_transactionGroupRunLoopObserverCallback, &context); + CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes); + CFRelease(observer); +} +``` + +上述代码会在即将退出 Runloop 或者 Runloop 开始休眠时执行回调 `_transactionGroupRunLoopObserverCallback`,而这个回调方法就是这一条主线的入口: + +```objectivec +static void _transactionGroupRunLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { + ASDisplayNodeCAssertMainThread(); + _ASAsyncTransactionGroup *group = (__bridge _ASAsyncTransactionGroup *)info; + [group commit]; +} +``` + +上一节中只是会将绘制代码提交到后台的并发进程中,而这里才会将结果提交,也就是在每次 Runloop 循环结束时开始绘制内容,而 `-[_operationCompletionBlock commit]` 方法的调用栈能够帮助我们理解内容是如何提交的,又是如何传回 `node` 持有的 `layer` 的: + +```objectivec +-[_ASAsyncTransactionGroup commit] + -[_ASAsyncTransaction commit] + ASAsyncTransactionQueue::GroupImpl::notify(dispatch_queue_t, dispatch_block_t) + _notifyList.push_back(GroupNotify) +``` + +`-[_ASAsyncTransactionGroup commit]` 方法的调用完成了对绘制事务的提交,而在 `-[_ASAsyncTransaction commit]` 中会调用 `notify` 方法,在上一节中的 `displayBlock` 执行结束后调用这里传入的 block 执行 `-[_ASAsyncTransaction completeTransaction]` 方法: + +```objectivec +- (void)commit { + ASDisplayNodeAssertMainThread(); + __atomic_store_n(&_state, ASAsyncTransactionStateCommitted, __ATOMIC_SEQ_CST); + + _group->notify(_callbackQueue, ^{ + ASDisplayNodeAssertMainThread(); + [self completeTransaction]; + }); +} +``` + +我们按照时间顺序来分析在上面的 block 执行之前,方法是如何调用的,以及 block 是如何被执行的;这就不得不回到派发绘制事务的部分了,在 `ASAsyncTransactionQueue::GroupImpl::schedule` 方法中,使用了 `dispatch_async` 将派发 block: + +```objectivec +void ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block) { + ... + if (entry._threadCount < maxThreads) { + ... + dispatch_async(queue, ^{ + ... + while (!entry._operationQueue.empty()) { + Operation operation = entry.popNextOperation(respectPriority); + { + ASDN::MutexUnlocker unlock(q._mutex); + if (operation._block) { + operation._block(); + } + operation._group->leave(); + operation._block = nil; + } + } + ... + }); + } +} +``` + +在 `displayBlock` 执行之后,会调用的 `group` 的 `leave` 方法: + +```objectivec +void ASAsyncTransactionQueue::GroupImpl::leave() { + if (_pendingOperations == 0) { + std::list notifyList; + _notifyList.swap(notifyList); + + for (GroupNotify & notify : notifyList) { + dispatch_async(notify._queue, notify._block); + } + } +} +``` + +这里终于执行了在 `- commit` 中加入的 block,也就是 `-[_ASAsyncTransaction completeTransaction]` 方法: + +```objectivec +- (void)completeTransaction { + if (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) != ASAsyncTransactionStateComplete) { + BOOL isCanceled = (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) == ASAsyncTransactionStateCanceled); + for (ASDisplayNodeAsyncTransactionOperation *operation in _operations) { + [operation callAndReleaseCompletionBlock:isCanceled]; + } + + __atomic_store_n(&_state, ASAsyncTransactionStateComplete, __ATOMIC_SEQ_CST); + } +} +``` + +最后的最后,`-[ASDisplayNodeAsyncTransactionOperation callAndReleaseCompletionBlock:]` 方法执行了回调将 `displayBlock` 执行的结果传回了 CALayer: + +```objectivec +- (void)callAndReleaseCompletionBlock:(BOOL)canceled; { + if (_operationCompletionBlock) { + _operationCompletionBlock(self.value, canceled); + self.operationCompletionBlock = nil; + } +} +``` + +也就是在 `-[ASDisplayNode(AsyncDisplay) displayAsyncLayer:asynchronously:]` 方法中构建的 `completionBlock`: + +```objectivec +asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id value, BOOL canceled){ + ASDisplayNodeCAssertMainThread(); + if (!canceled && !isCancelledBlock()) { + UIImage *image = (UIImage *)value; + BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero); + if (stretchable) { + ASDisplayNodeSetupLayerContentsWithResizableImage(_layer, image); + } else { + _layer.contentsScale = self.contentsScale; + _layer.contents = (id)image.CGImage; + } + [self didDisplayAsyncLayer:self.asyncLayer]; + } +}; +``` + +这一部分进行的大量的数据传递都是通过 block 进行的,从 Runloop 中对事务的提交,以及通过 `notify` 方法加入的 block,都是为了最后将绘制的结果传回 `CALayer` 对象,而到这里可以说整个 ASDK 对于视图内容的绘制过程就结束了。 + +## 总结 + +ASDK 对于绘制过程的优化有三部分:分别是栅格化子视图、绘制图像以及绘制文字。 + +它拦截了视图加入层级时发出的通知 `- willMoveToWindow:` 方法,然后手动调用 `- setNeedsDisplay`,强制所有的 `CALayer` 执行 `- display` 更新内容; + +然后将上面的操作全部抛入了后台的并发线程中,并在 Runloop 中注册回调,在每次 Runloop 结束时,对已经完成的事务进行 `- commit`,以图片的形式直接传回对应的 `layer.content` 中,完成对内容的更新。 + +从它的实现来看,确实从主线程移除了很多昂贵的 CPU 以及 GPU 操作,有效地加快了视图的绘制和渲染,保证了主线程的流畅执行。 + +## References + ++ [How VSync works, and why people loathe it](https://hardforum.com/threads/how-vsync-works-and-why-people-loathe-it.928593/) ++ [脑洞大开:为啥帧率达到 60 fps 就流畅?](http://www.jianshu.com/p/71cba1711de0) ++ [iOS 保持界面流畅的技巧](http://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/) ++ [CADiplayLink Class Reference - Developer- Apple](https://en.wikipedia.org/wiki/Analog_television#Vertical_synchronization) ++ [CPU vs GPU · iOS 核心动画高级技巧](https://zsisme.gitbooks.io/ios-/content/chapter12/cpu-versus-gpu.html) ++ [理解 UIView 的绘制](http://vizlabxt.github.io/blog/2012/10/22/UIView-Rendering/) ++ [Introduce to AsyncDisplayKit](http://vizlabxt.github.io/blog/2015/01/09/Behind-AsyncDisplayKit/) ++ [AsyncDisplayKit Tutorial: Achieving 60 FPS scrolling](https://www.raywenderlich.com/86365/asyncdisplaykit-tutorial-achieving-60-fps-scrolling) ++ [Layer Trees vs. Flat Drawing – Graphics Performance Across iOS Device Generations](http://floriankugler.com/2013/05/24/layer-trees-vs-flat-drawing-graphics-performance-across-ios-device-generations/) ++ [深入理解 RunLoop](http://blog.ibireme.com/2015/05/18/runloop/) + +## 其它 + +> Github Repo:[iOS-Source-Code-Analyze](https://github.com/draveness/iOS-Source-Code-Analyze) +> +> Follow: [Draveness · Github](https://github.com/Draveness) +> +> Source: http://draveness.me/asdk-rendering + + diff --git "a/contents/AsyncDisplayKit/\346\217\220\345\215\207 iOS \347\225\214\351\235\242\347\232\204\346\270\262\346\237\223\346\200\247\350\203\275 .md" "b/contents/AsyncDisplayKit/\346\217\220\345\215\207 iOS \347\225\214\351\235\242\347\232\204\346\270\262\346\237\223\346\200\247\350\203\275 .md" new file mode 100644 index 0000000..2ff39e3 --- /dev/null +++ "b/contents/AsyncDisplayKit/\346\217\220\345\215\207 iOS \347\225\214\351\235\242\347\232\204\346\270\262\346\237\223\346\200\247\350\203\275 .md" @@ -0,0 +1,808 @@ +# 使用 ASDK 性能调优 - 提升 iOS 界面的渲染性能 + +> 这一系列的文章会从几个方面对 [ASDK](http://asyncdisplaykit.org) 在性能调优方面策略的实现进行分析,帮助读者理解 ASDK 如何做到使复杂的 UI 界面达到 60 FPS 的刷新频率的;本篇文章会从视图的渲染层面讲解 ASDK 对于渲染过程的优化并对 ASDK 进行概述。 + +在客户端或者前端开发中,对于性能的优化,尤其是 UI,往往都不是最先考虑的问题。 + +因为在大多数场景下,使用更加复杂的高性能代码替代可用的代码经常会导致代码的可维护性下降,所以更需要我们开发者对优化的时间点以及原因有一个比较清楚的认识,避免过度优化带来的问题。 + +对 iOS 开发比较熟悉的开发者都知道,iOS 中的性能问题大多是阻塞主线程导致用户的交互反馈出现可以感知的延迟。 + +![scrollview-demo](images/scrollview-demo.png) + +详细说起来,大体有三种原因: + +1. UI 渲染需要时间较长,无法按时提交结果; +2. 一些需要**密集计算**的处理放在了主线程中执行,导致主线程被阻塞,无法渲染 UI 界面; +3. 网络请求由于网络状态的问题响应较慢,UI 层由于没有模型返回无法渲染。 + +上面的这些问题都会影响应用的性能,最常见的表现就是 `UITableView` 在滑动时没有达到 **60 FPS**,用户能感受到明显的卡顿。 + +## 屏幕的渲染 + +相信点开这篇文章的大多数开发者都知道 FPS 是什么,那么如果才能优化我们的 App 使其达到 60 FPS 呢?在具体了解方法之前,我们先退一步,提出另一个问题,屏幕是如何渲染的? + +> 对于第一个问题,可能需要几篇文章来回答,希望整个系列的文章能给你一个满意的答案。3 + +### CRT 和 LCD + +屏幕的渲染可能要从 [CRT(Cathode ray tube) 显示器](https://en.wikipedia.org/wiki/Cathode_ray_tube)和 [LCD(Liquid-crystal display) 显示器](https://en.wikipedia.org/wiki/Liquid-crystal_display)讲起。 + +![CRT](images/CRT.png) + +CRT 显示器是比较古老的技术,它使用阴极电子枪发射电子,在阴极高压的作用下,电子由电子枪射向荧光屏,使荧光粉发光,将图像显示在屏幕上,这也就是用磁铁靠近一些老式电视机的屏幕会让它们变色的原因。 + +而 FPS 就是 CRT 显示器的刷新频率,电子枪每秒会对显示器上内容进行 60 - 100 次的刷新,哪怕在我们看来没有任何改变。 + +![lcd](images/lcd.png) + + +但是 LCD 的原理与 CRT 非常不同,LCD 的成像原理跟光学有关: + ++ 在不加电压下,光线会沿着液晶分子的间隙前进旋转 90°,所以光可以通过; ++ 在加入电压之后,光沿着液晶分子的间隙直线前进,被滤光板挡住。 + +如果你可以翻墙,相信下面的视频会更好得帮助你理解 LCD 的工作原理: + + + +LCD 的成像原理虽然与 CRT 截然不同,每一个像素的颜色可以**在需要改变时**才去改变电压,也就是不需要刷新频率,但是由于一些历史原因,LCD 仍然需要按照一定的刷新频率向 GPU 获取新的图像用于显示。 + +### 屏幕撕裂 + +但是显示器只是用于将图像显示在屏幕上,谁又是图像的提供者呢?图像都是我们经常说的 GPU 提供的。 + +而这导致了另一个问题,由于 GPU 生成图像的频率与显示器刷新的频率是不相关的,那么在显示器刷新时,GPU 没有准备好需要显示的图像怎么办;或者 GPU 的渲染速度过快,显示器来不及刷新,GPU 就已经开始渲染下一帧图像又该如何处理? + +![screen-tearing](images/screen-tearing.jpg) + +如果解决不了这两个问题,就会出现上图中的*屏幕撕裂*(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 拷贝帧的过程依然需要消耗一定的时间,如果屏幕在拷贝图像时刷新,仍然会导致屏幕撕裂问题。 + +![how-to-solve-tearing-proble](images/how-to-solve-tearing-problem.jpg) + +引入多个缓冲区可以有效地**缓解**屏幕撕裂,也就是同时使用一个*帧缓冲区*(frame buffer)和多个*后备缓冲区*(back buffer);在每次显示器请求内容时,都会从**帧缓冲区**中取出图像然后渲染。 + +虽然缓冲区可以减缓这些问题,但是却不能解决;如果后备缓冲区绘制完成,而帧缓冲区的图像没有被渲染,后备缓冲区中的图像就会覆盖帧缓冲区,仍然会导致屏幕撕裂。 + +解决这个问题需要另一个机制的帮助,也就是垂直同步(Vertical synchronization),简称 V-Sync 来解决。 + +### V-Sync + +V-Sync 的主要作用就是保证**只有在帧缓冲区中的图像被渲染之后,后备缓冲区中的内容才可以被拷贝到帧缓冲区中**,理想情况下的 V-Sync 会按这种方式工作: + +![normal-vsyn](images/normal-vsync.png) + +每次 V-Sync 发生时,CPU 以及 GPU 都已经完成了对图像的处理以及绘制,显示器可以直接拿到缓冲区中的帧。但是,如果 CPU 或者 GPU 的处理需要的时间较长,就会发生掉帧的问题: + +![lag-vsyn](images/lag-vsync.png) + + +在 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](images/cpu-gpu.jpg) + +很多 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) 就根据上述的策略帮助我们对应用性能进行优化。 + +![asdk-logo](images/asdk-logo.png) + +AsyncDisplayKit(以下简称 ASDK)是由 Facebook 开源的一个 iOS 框架,能够帮助最复杂的 UI 界面保持流畅和快速响应。 + +ASDK 从开发到开源大约经历了一年多的时间,它其实并不是一个简单的框架~~它是一个复杂的框架~~,更像是对 UIKit 的重新实现,把整个 UIKit 以及 CALayer 层封装成一个一个 `Node`,**将昂贵的渲染、图片解码、布局以及其它 UI 操作移出主线程**,这样主线程就可以对用户的操作及时做出反应。 + +很多分析 ASDK 的文章都会有这么一张图介绍框架中的最基本概念: + +![asdk-hierarchy](images/asdk-hierarchy.png) + +在 ASDK 中最基本的单位就是 `ASDisplayNode`,每一个 node 都是对 `UIView` 以及 `CALayer` 的抽象。但是与 `UIView` 不同的是,`ASDisplayNode` 是线程安全的,它可以在后台线程中完成初始化以及配置工作。 + +如果按照 60 FPS 的刷新频率来计算,每一帧的渲染时间只有 16ms,在 16ms 的时间内要完成对 `UIView` 的创建、布局、绘制以及渲染,CPU 和 GPU 面临着巨大的压力。 + +![apple-a9](images/apple-a9.jpg) + +但是从 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)self; + } else { + _view = [self _viewToLoad]; + _view.asyncdisplaykit_node = self; + _layer = _view.layer; + } + _layer.asyncdisplaykit_node = self; + + self.asyncLayer.asyncDelegate = self; +} +``` + +因为 `UIView` 和 `CALayer` 虽然都可以用于展示内容,不过由于 `UIView` 可以用于处理用户的交互,所以如果不需要使用 `UIView` 的特性,直接使用 `CALayer` 进行渲染,能够节省大量的渲染时间。 + +> 如果你使用 Xcode 查看过视图的层级,那么你应该知道,`UIView` 在 Debug View Hierarchy 中是有层级的;而 `CALayer` 并没有,它门的显示都在一个平面上。 + +上述方法中的 `-[ASDisplayNode _layerToLoad]` 以及 `[ASDisplayNode _viewToLoad]` 都只会根据当前节点的 `layerClass` 或者 `viewClass` 初始化一个对象。 + +> [Layer Trees vs. Flat Drawing – Graphics Performance Across iOS Device Generations](http://floriankugler.com/2013/05/24/layer-trees-vs-flat-drawing-graphics-performance-across-ios-device-generations/) 这篇文章比较了 `UIView` 和 `CALayer` 的渲染时间。 + +![view-layer-cg-compare](images/view-layer-cg-compare.png) + +`-[ASDisplayNode asyncLayer]` 只是对当前 `node` 持有的 `layer` 进行封装,确保会返回一个 `_ASDisplayLayer` 的实例: + +```objectivec +- (_ASDisplayLayer *)asyncLayer { + ASDN::MutexLocker l(_propertyLock); + return [_layer isKindOfClass:[_ASDisplayLayer class]] ? (_ASDisplayLayer *)_layer : nil; +} +``` + +最重要的是 `-[ASDisplayNode _loadViewOrLayerIsLayerBacked:]` 方法会将当前节点设置为 `asyncLayer` 的代理,在后面会使用 `ASDisplayNode` 为 `CALayer` 渲染内容。 + +### 视图层级 + +在初始化工作完成之后,当 `ASDisplayNode` 第一次被加入到视图的层级时,`-[_ASDisplayView willMoveToWindow:]` 就会被调用。 + +#### _ASDisplayView 和 _ASDisplayLayer + +`_ASDisplayView` 和 `_ASDisplayLayer` 都是私有类,它们之间的对应关系其实和 `UIView` 与 `CALayer` 完全相同。 + +```objectivec ++ (Class)layerClass { + return [_ASDisplayLayer class]; +} +``` + +`_ASDisplayView` 覆写了很多跟视图层级改变有关的方法: + ++ `-[_ASDisplayView willMoveToWindow:]` ++ `-[_ASDisplayView didMoveToWindow]` ++ `-[_ASDisplayView willMoveToSuperview:]` ++ `-[_ASDisplayView didMoveToSuperview]` + +它们用于在视图的层级改变时,通知对应 `ASDisplayNode` 作出相应的反应,比如 `-[_ASDisplayView willMoveToWindow:]` 方法会在视图被加入层级时调用: + +```objectivec +- (void)willMoveToWindow:(UIWindow *)newWindow { + BOOL visible = (newWindow != nil); + if (visible && !_node.inHierarchy) { + [_node __enterHierarchy]; + } +} +``` + +#### setNeedsDisplay + +当前视图如果不在视图层级中,就会通过 `_node` 的实例方法 `-[ASDisplayNode __enterHierarchy]` 加入视图层级: + +```objectivec +- (void)__enterHierarchy { + if (!_flags.isInHierarchy && !_flags.visibilityNotificationsDisabled && ![self __selfOrParentHasVisibilityNotificationsDisabled]) { + _flags.isEnteringHierarchy = YES; + _flags.isInHierarchy = YES; + + if (_flags.shouldRasterizeDescendants) { + [self _recursiveWillEnterHierarchy]; + } else { + [self willEnterHierarchy]; + } + _flags.isEnteringHierarchy = NO; + + # 更新 layer 显示的内容 + } +} + +``` + +> `_flags` 是 `ASDisplayNodeFlags` 结构体,用于标记当前 `ASDisplayNode` 的一些 BOOL 值,比如,异步显示、栅格化子视图等等,你不需要知道都有什么,根据这些值的字面意思理解就已经足够了。 + +上述方法的前半部分只是对 `_flags` 的标记,如果需要将当前视图的子视图栅格化,也就是**将它的全部子视图与当前视图压缩成一个图层**,就会向这些视图递归地调用 `-[ASDisplayNode willEnterHierarchy]` 方法通知目前的状态: + +```objectivec +- (void)_recursiveWillEnterHierarchy { + _flags.isEnteringHierarchy = YES; + [self willEnterHierarchy]; + _flags.isEnteringHierarchy = NO; + + for (ASDisplayNode *subnode in self.subnodes) { + [subnode _recursiveWillEnterHierarchy]; + } +} +``` + +而 `-[ASDisplayNode willEnterHierarchy]` 会修改当前节点的 `interfaceState` 到 `ASInterfaceStateInHierarchy`,表示当前节点不包含在 `cell` 或者其它,但是在 `window` 中。 + +```objectivec +- (void)willEnterHierarchy { + if (![self supportsRangeManagedInterfaceState]) { + self.interfaceState = ASInterfaceStateInHierarchy; + } +} +``` + +当前结点需要被显示在屏幕上时,如果其内容 `contents` 为空,就会调用 `-[CALayer setNeedsDisplay]` 方法将 `CALayer` 标记为脏的,通知系统需要在下一个绘制循环中重绘视图: + +```objectivec +- (void)__enterHierarchy { + if (!_flags.isInHierarchy && !_flags.visibilityNotificationsDisabled && ![self __selfOrParentHasVisibilityNotificationsDisabled]) { + + # 标记节点的 flag + + if (self.contents == nil) { + CALayer *layer = self.layer; + [layer setNeedsDisplay]; + + if ([self _shouldHavePlaceholderLayer]) { + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + [self _setupPlaceholderLayerIfNeeded]; + _placeholderLayer.opacity = 1.0; + [CATransaction commit]; + [layer addSublayer:_placeholderLayer]; + } + } + } +} +``` + +在将 `CALayer` 标记为 dirty 之后,在绘制循环中就会执行 `-[CALayer display]` 方法,对它要展示的内容进行绘制;如果当前视图需要一些占位图,那么就会在这里的代码中,为当前 `node` 对应的 `layer` 添加合适颜色的占位层。 + +![placeholder-laye](images/placeholder-layer.png) + +### 派发异步绘制事务 + +在上一节中调用 `-[CALayer setNeedsDisplay]` 方法将当前节点标记为 dirty 之后,在下一个绘制循环时就会对所有需要重绘的 `CALayer` 执行 `-[CALayer display]`,这也是这一小节需要分析的方法的入口: + +```objectivec +- (void)display { + [self _hackResetNeedsDisplay]; + + ASDisplayNodeAssertMainThread(); + if (self.isDisplaySuspended) return; + + [self display:self.displaysAsynchronously]; +} +``` + +这一方法的调用栈比较复杂,在具体分析之前,笔者会先给出这个方法的调用栈,给读者一个关于该方法实现的简要印象: + +```objectivec +-[_ASDisplayLayer display] + -[_ASDisplayLayer display:] // 将绘制工作交给 ASDisplayNode 处理 + -[ASDisplayNode(AsyncDisplay) displayAsyncLayer:asynchronously:] + -[ASDisplayNode(AsyncDisplay) _displayBlockWithAsynchronous:isCancelledBlock:rasterizing:] + -[ASDisplayNode(AsyncDisplay) _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:displayBlocks:] + -[CALayer(ASDisplayNodeAsyncTransactionContainer) asyncdisplaykit_parentTransactionContainer] + -[CALayer(ASDisplayNodeAsyncTransactionContainer) asyncdisplaykit_asyncTransaction] + -[_ASAsyncTransaction initWithCallbackQueue:completionBlock:] + -[_ASAsyncTransactionGroup addTransactionContainer:] + -[_ASAsyncTransaction addOperationWithBlock:priority:queue:completion:] + ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block) + void dispatch_async(dispatch_queue_t queue, dispatch_block_t block); +``` + +`-[_ASDisplayLayer display]` 在调用栈中其实会创建一个 `displayBlock`,它其实是一个使用 Core Graphics 进行图像绘制的过程,整个绘制过程是通过事务的形式进行管理的;而 `displayBlock` 会被 GCD 分发到后台的并发进程来处理。 + +调用栈中的第二个方法 `-[_ASDisplayLayer display]` 会将异步绘制的工作交给自己的 `asyncDelegate`,也就是[第一部分](#uiview-和-calayer-的加载)中设置的 `ASDisplayNode`: + +```objectivec +- (void)display:(BOOL)asynchronously { + [_asyncDelegate displayAsyncLayer:self asynchronously:asynchronously]; +} +``` + +#### ASDisplayNode(AsyncDisplay) + +这里省略了一部分 `-[ASDisplayNode(AsyncDisplay) displayAsyncLayer:asynchronously:]` 方法的实现: + +```objectivec +- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously { + ASDisplayNodeAssertMainThread(); + + ... + + asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:asynchronously isCancelledBlock:isCancelledBlock rasterizing:NO]; + + if (!displayBlock) return; + + asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id value, BOOL canceled){ + ASDisplayNodeCAssertMainThread(); + if (!canceled && !isCancelledBlock()) { + UIImage *image = (UIImage *)value; + _layer.contentsScale = self.contentsScale; + _layer.contents = (id)image.CGImage; + } + }; + + if (asynchronously) { + CALayer *containerLayer = _layer.asyncdisplaykit_parentTransactionContainer ? : _layer; + _ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction; + [transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock]; + } else { + UIImage *contents = (UIImage *)displayBlock(); + completionBlock(contents, NO); + } +} +``` + +省略后的代码脉络非常清晰,`-[ASDisplayNode(AsyncDisplay) _displayBlockWithAsynchronous:isCancelledBlock:rasterizing:]` 返回一个用于 `displayBlock`,然后构造一个 `completionBlock`,在绘制结束时执行,在主线程中设置当前 `layer` 的内容。 + +如果当前的渲染是异步的,就会将 `displayBlock` 包装成一个事务,添加到队列中执行,否则就会同步执行当前的 block,并执行 `completionBlock` 回调,通知 `layer` 更新显示的内容。 + +同步显示的部分到这里已经很清楚了,我们更关心的其实还是异步绘制的部分,因为这部分才是 ASDK 提升效率的关键;而这就要从获取 `displayBlock` 的方法开始了解了。 + +#### displayBlock 的构建 + +`displayBlock` 的创建一般分为三种不同的方式: + +1. 将当前视图的子视图压缩成一层绘制在当前页面上 +2. 使用 `- displayWithParameters:isCancelled:` 返回一个 `UIImage`,对图像节点 `ASImageNode` 进行绘制 +3. 使用 `- drawRect:withParameters:isCancelled:isRasterizing:` 在 CG 上下文中绘制文字节点 `ASTextNode` + +这三种方式都通过 ASDK 来优化视图的渲染速度,这些操作最后都会扔到后台的并发线程中进行处理。 + +> 下面三个部分的代码经过了删减,省略了包括取消绘制、通知代理、控制并发数量以及用于调试的代码。 + +##### 栅格化子视图 + +如果当前的视图需要栅格化子视图,就会进入启用下面的构造方式创建一个 block,它会递归地将子视图绘制在父视图上: + +```objectivec +- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing { + asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil; + ASDisplayNodeFlags flags = _flags; + + if (!rasterizing && self.shouldRasterizeDescendants) { + NSMutableArray *displayBlocks = [NSMutableArray array]; + [self _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:isCancelledBlock displayBlocks:displayBlocks]; + + CGFloat contentsScaleForDisplay = self.contentsScaleForDisplay; + BOOL opaque = self.opaque && CGColorGetAlpha(self.backgroundColor.CGColor) == 1.0f; + + displayBlock = ^id{ + + UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay); + + for (dispatch_block_t block in displayBlocks) { + block(); + } + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return image; + }; + } else if (flags.implementsInstanceImageDisplay || flags.implementsImageDisplay) { + #:绘制 UIImage + } else if (flags.implementsInstanceDrawRect || flags.implementsDrawRect) { + #:提供 context,使用 CG 绘图 + } + + return [displayBlock copy]; +} +``` + +在压缩视图层级的过程中就会调用 `-[ASDisplayNode(AsyncDisplay) _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:displayBlocks:]` 方法,获取子视图的所有 `displayBlock`,在得到 `UIGraphicsBeginImageContextWithOptions` 需要的参数之后,创建一个新的 context,执行了所有的 `displayBlock` 将子视图的绘制到当前图层之后,使用 `UIGraphicsGetImageFromCurrentImageContext` 取出图层的内容并返回。 + +`-[ASDisplayNode(AsyncDisplay) _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:displayBlocks:]` 的实现还是有些繁琐的,它主要的功能就是使用 Core Graphics 进行绘图,将背景颜色、仿射变换、位置大小以及圆角等参数绘制到当前的上下文中,而且这个过程是递归的,直到不存在或者不需要绘制子节点为止。 + +##### 绘制图片 + +`displayBlock` 的第二种绘制策略更多地适用于图片节点 `ASImageNode` 的绘制: + +```objectivec +- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing { + asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil; + ASDisplayNodeFlags flags = _flags; + + if (!rasterizing && self.shouldRasterizeDescendants) { + #:栅格化 + } else if (flags.implementsInstanceImageDisplay || flags.implementsImageDisplay) { + id drawParameters = [self drawParameters]; + + displayBlock = ^id{ + UIImage *result = nil; + if (flags.implementsInstanceImageDisplay) { + result = [self displayWithParameters:drawParameters isCancelled:isCancelledBlock]; + } else { + result = [[self class] displayWithParameters:drawParameters isCancelled:isCancelledBlock]; + } + return result; + }; + } else if (flags.implementsInstanceDrawRect || flags.implementsDrawRect) { + #:提供 context,使用 CG 绘图 + } + + return [displayBlock copy]; +} +``` + +通过 `- displayWithParameters:isCancelled:` 的执行返回一个图片,不过这里的绘制也离不开 Core Graphics 的一些 C 函数,你会在 `-[ASImageNode displayWithParameters:isCancelled:]` 中看到对于 CG 的运用,它会使用 `drawParameters` 来修改并绘制自己持有的 `image` 对象。 + +##### 使用 CG 绘图 + +文字的绘制一般都会在 `- drawRect:withParameters:isCancelled:isRasterizing:` 进行,这个方法只是提供了一个合适的用于绘制的上下文,该方法不止可以绘制文字,只是在这里绘制文字比较常见: + +```objectivec +- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing { + asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil; + ASDisplayNodeFlags flags = _flags; + + if (!rasterizing && self.shouldRasterizeDescendants) { + #:栅格化 + } else if (flags.implementsInstanceImageDisplay || flags.implementsImageDisplay) { + #:绘制 UIImage + } else if (flags.implementsInstanceDrawRect || flags.implementsDrawRect) { + if (!rasterizing) { + UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay); + } + + if (flags.implementsInstanceDrawRect) { + [self drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing]; + } else { + [[self class] drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing]; + } + + UIImage *image = nil; + if (!rasterizing) { + image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + + return image; + }; + } + + return [displayBlock copy]; +} +``` + +上述代码跟第一部分比较像,区别是这里不会栅格化子视图;代码根据情况会决定是否重新开一个新的上下文,然后通过 `- drawRect:withParameters:isCancelled:isRasterizing:` 方法实现绘制。 + +#### 管理绘制事务 + +ASDK 提供了一个私有的管理事务的机制,由三部分组成 `_ASAsyncTransactionGroup`、`_ASAsyncTransactionContainer` 以及 `_ASAsyncTransaction`,这三者各自都有不同的功能: + ++ `_ASAsyncTransactionGroup` 会在初始化时,向 Runloop 中注册一个回调,在每次 Runloop 结束时,执行回调来提交 `displayBlock` 执行的结果 ++ `_ASAsyncTransactionContainer` 为当前 `CALayer` 提供了用于保存事务的容器,并提供了获取新的 `_ASAsyncTransaction` 实例的便利方法 ++ `_ASAsyncTransaction` 将异步操作封装成了轻量级的事务对象,使用 C++ 代码对 GCD 进行了封装 + +从上面的小节中,我们已经获取到了用于绘制的 `displayBlock`,然后就需要将 block 添加到绘制事务中: + +```objectivec +- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously { + ... + + if (asynchronously) { + CALayer *containerLayer = _layer.asyncdisplaykit_parentTransactionContainer ? : _layer; + _ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction; + [transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock]; + } else { + ... + } +} +``` + +前两行代码是获取 `_ASAsyncTransaction` 实例的过程,这个实例会包含在一个 `layer` 的哈希表中,最后调用的实例方法 `-[_ASAsyncTransaction addOperationWithBlock:priority:queue:completion:]` 会把用于绘制的 `displayBlock` 添加到后台并行队列中: + +```objectivec ++ (dispatch_queue_t)displayQueue { + static dispatch_queue_t displayQueue = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + displayQueue = dispatch_queue_create("org.AsyncDisplayKit.ASDisplayLayer.displayQueue", DISPATCH_QUEUE_CONCURRENT); + dispatch_set_target_queue(displayQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)); + }); + + return displayQueue; +} +``` + +这个队列是一个并行队列,并且优先级是 `DISPATCH_QUEUE_PRIORITY_HIGH`,**确保 UI 的渲染会在其它异步操作执行之前进行**,而 `-[_ASAsyncTransaction addOperationWithBlock:priority:queue:completion:]` 中会初始化 `ASDisplayNodeAsyncTransactionOperation` 的实例,然后传入 `completionBlock`,在绘制结束时回调: + +```objectivec +- (void)addOperationWithBlock:(asyncdisplaykit_async_transaction_operation_block_t)block priority:(NSInteger)priority queue:(dispatch_queue_t)queue completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion { + ASDisplayNodeAssertMainThread(); + + [self _ensureTransactionData]; + + ASDisplayNodeAsyncTransactionOperation *operation = [[ASDisplayNodeAsyncTransactionOperation alloc] initWithOperationCompletionBlock:completion]; + [_operations addObject:operation]; + _group->schedule(priority, queue, ^{ + @autoreleasepool { + operation.value = block(); + } + }); +} +``` + +`schedule` 方法是一个 C++ 方法,它会向 `ASAsyncTransactionQueue::Group` 中派发一个 block,这个 block 中会执行 `displayBlock`,然后将结果传给 `operation.value`: + +```objectivec +void ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block) { + ASAsyncTransactionQueue &q = _queue; + ASDN::MutexLocker locker(q._mutex); + + DispatchEntry &entry = q._entries[queue]; + + Operation operation; + operation._block = block; + operation._group = this; + operation._priority = priority; + entry.pushOperation(operation); + + ++_pendingOperations; + + NSUInteger maxThreads = [NSProcessInfo processInfo].activeProcessorCount * 2; + + if ([[NSRunLoop mainRunLoop].currentMode isEqualToString:UITrackingRunLoopMode]) + --maxThreads; + + if (entry._threadCount < maxThreads) { + bool respectPriority = entry._threadCount > 0; + ++entry._threadCount; + + dispatch_async(queue, ^{ + while (!entry._operationQueue.empty()) { + Operation operation = entry.popNextOperation(respectPriority); + { + if (operation._block) { + operation._block(); + } + operation._group->leave(); + operation._block = nil; + } + } + --entry._threadCount; + + if (entry._threadCount == 0) { + q._entries.erase(queue); + } + }); + } +} +``` + +`ASAsyncTransactionQueue::GroupImpl` 其实现其实就是对 GCD 的封装,同时添加一些最大并发数、线程锁的功能。通过 `dispatch_async` 将 block 分发到 `queue` 中,立刻执行 block,将数据传回 `ASDisplayNodeAsyncTransactionOperation` 实例。 + +### 回调 + +在 `_ASAsyncTransactionGroup` 调用 `mainTransactionGroup` 类方法获取单例时,会通过 `+[_ASAsyncTransactionGroup registerTransactionGroupAsMainRunloopObserver]` 向 Runloop 中注册回调: + +```objectivec ++ (void)registerTransactionGroupAsMainRunloopObserver:(_ASAsyncTransactionGroup *)transactionGroup { + static CFRunLoopObserverRef observer; + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + CFOptionFlags activities = (kCFRunLoopBeforeWaiting | kCFRunLoopExit); + CFRunLoopObserverContext context = {0, (__bridge void *)transactionGroup, &CFRetain, &CFRelease, NULL}; + + observer = CFRunLoopObserverCreate(NULL, activities, YES, INT_MAX, &_transactionGroupRunLoopObserverCallback, &context); + CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes); + CFRelease(observer); +} +``` + +上述代码会在即将退出 Runloop 或者 Runloop 开始休眠时执行回调 `_transactionGroupRunLoopObserverCallback`,而这个回调方法就是这一条主线的入口: + +```objectivec +static void _transactionGroupRunLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { + ASDisplayNodeCAssertMainThread(); + _ASAsyncTransactionGroup *group = (__bridge _ASAsyncTransactionGroup *)info; + [group commit]; +} +``` + +上一节中只是会将绘制代码提交到后台的并发进程中,而这里才会将结果提交,也就是在每次 Runloop 循环结束时开始绘制内容,而 `-[_operationCompletionBlock commit]` 方法的调用栈能够帮助我们理解内容是如何提交的,又是如何传回 `node` 持有的 `layer` 的: + +```objectivec +-[_ASAsyncTransactionGroup commit] + -[_ASAsyncTransaction commit] + ASAsyncTransactionQueue::GroupImpl::notify(dispatch_queue_t, dispatch_block_t) + _notifyList.push_back(GroupNotify) +``` + +`-[_ASAsyncTransactionGroup commit]` 方法的调用完成了对绘制事务的提交,而在 `-[_ASAsyncTransaction commit]` 中会调用 `notify` 方法,在上一节中的 `displayBlock` 执行结束后调用这里传入的 block 执行 `-[_ASAsyncTransaction completeTransaction]` 方法: + +```objectivec +- (void)commit { + ASDisplayNodeAssertMainThread(); + __atomic_store_n(&_state, ASAsyncTransactionStateCommitted, __ATOMIC_SEQ_CST); + + _group->notify(_callbackQueue, ^{ + ASDisplayNodeAssertMainThread(); + [self completeTransaction]; + }); +} +``` + +我们按照时间顺序来分析在上面的 block 执行之前,方法是如何调用的,以及 block 是如何被执行的;这就不得不回到派发绘制事务的部分了,在 `ASAsyncTransactionQueue::GroupImpl::schedule` 方法中,使用了 `dispatch_async` 将派发 block: + +```objectivec +void ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block) { + ... + if (entry._threadCount < maxThreads) { + ... + dispatch_async(queue, ^{ + ... + while (!entry._operationQueue.empty()) { + Operation operation = entry.popNextOperation(respectPriority); + { + ASDN::MutexUnlocker unlock(q._mutex); + if (operation._block) { + operation._block(); + } + operation._group->leave(); + operation._block = nil; + } + } + ... + }); + } +} +``` + +在 `displayBlock` 执行之后,会调用的 `group` 的 `leave` 方法: + +```objectivec +void ASAsyncTransactionQueue::GroupImpl::leave() { + if (_pendingOperations == 0) { + std::list notifyList; + _notifyList.swap(notifyList); + + for (GroupNotify & notify : notifyList) { + dispatch_async(notify._queue, notify._block); + } + } +} +``` + +这里终于执行了在 `- commit` 中加入的 block,也就是 `-[_ASAsyncTransaction completeTransaction]` 方法: + +```objectivec +- (void)completeTransaction { + if (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) != ASAsyncTransactionStateComplete) { + BOOL isCanceled = (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) == ASAsyncTransactionStateCanceled); + for (ASDisplayNodeAsyncTransactionOperation *operation in _operations) { + [operation callAndReleaseCompletionBlock:isCanceled]; + } + + __atomic_store_n(&_state, ASAsyncTransactionStateComplete, __ATOMIC_SEQ_CST); + } +} +``` + +最后的最后,`-[ASDisplayNodeAsyncTransactionOperation callAndReleaseCompletionBlock:]` 方法执行了回调将 `displayBlock` 执行的结果传回了 CALayer: + +```objectivec +- (void)callAndReleaseCompletionBlock:(BOOL)canceled; { + if (_operationCompletionBlock) { + _operationCompletionBlock(self.value, canceled); + self.operationCompletionBlock = nil; + } +} +``` + +也就是在 `-[ASDisplayNode(AsyncDisplay) displayAsyncLayer:asynchronously:]` 方法中构建的 `completionBlock`: + +```objectivec +asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id value, BOOL canceled){ + ASDisplayNodeCAssertMainThread(); + if (!canceled && !isCancelledBlock()) { + UIImage *image = (UIImage *)value; + BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero); + if (stretchable) { + ASDisplayNodeSetupLayerContentsWithResizableImage(_layer, image); + } else { + _layer.contentsScale = self.contentsScale; + _layer.contents = (id)image.CGImage; + } + [self didDisplayAsyncLayer:self.asyncLayer]; + } +}; +``` + +这一部分进行的大量的数据传递都是通过 block 进行的,从 Runloop 中对事务的提交,以及通过 `notify` 方法加入的 block,都是为了最后将绘制的结果传回 `CALayer` 对象,而到这里可以说整个 ASDK 对于视图内容的绘制过程就结束了。 + +## 总结 + +ASDK 对于绘制过程的优化有三部分:分别是栅格化子视图、绘制图像以及绘制文字。 + +它拦截了视图加入层级时发出的通知 `- willMoveToWindow:` 方法,然后手动调用 `- setNeedsDisplay`,强制所有的 `CALayer` 执行 `- display` 更新内容; + +然后将上面的操作全部抛入了后台的并发线程中,并在 Runloop 中注册回调,在每次 Runloop 结束时,对已经完成的事务进行 `- commit`,以图片的形式直接传回对应的 `layer.content` 中,完成对内容的更新。 + +从它的实现来看,确实从主线程移除了很多昂贵的 CPU 以及 GPU 操作,有效地加快了视图的绘制和渲染,保证了主线程的流畅执行。 + +## References + ++ [How VSync works, and why people loathe it](https://hardforum.com/threads/how-vsync-works-and-why-people-loathe-it.928593/) ++ [脑洞大开:为啥帧率达到 60 fps 就流畅?](http://www.jianshu.com/p/71cba1711de0) ++ [iOS 保持界面流畅的技巧](http://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/) ++ [CADiplayLink Class Reference - Developer- Apple](https://en.wikipedia.org/wiki/Analog_television#Vertical_synchronization) ++ [CPU vs GPU · iOS 核心动画高级技巧](https://zsisme.gitbooks.io/ios-/content/chapter12/cpu-versus-gpu.html) ++ [理解 UIView 的绘制](http://vizlabxt.github.io/blog/2012/10/22/UIView-Rendering/) ++ [Introduce to AsyncDisplayKit](http://vizlabxt.github.io/blog/2015/01/09/Behind-AsyncDisplayKit/) ++ [AsyncDisplayKit Tutorial: Achieving 60 FPS scrolling](https://www.raywenderlich.com/86365/asyncdisplaykit-tutorial-achieving-60-fps-scrolling) ++ [Layer Trees vs. Flat Drawing – Graphics Performance Across iOS Device Generations](http://floriankugler.com/2013/05/24/layer-trees-vs-flat-drawing-graphics-performance-across-ios-device-generations/) ++ [深入理解 RunLoop](http://blog.ibireme.com/2015/05/18/runloop/) + +## 其它 + +> Github Repo:[iOS-Source-Code-Analyze](https://github.com/draveness/iOS-Source-Code-Analyze) +> +> Follow: [Draveness · Github](https://github.com/Draveness) +> +> Source: http://draveness.me/asdk-rendering + + diff --git "a/contents/AsyncDisplayKit/\351\242\204\345\212\240\350\275\275\344\270\216\346\231\272\350\203\275\351\242\204\345\212\240\350\275\275\357\274\210iOS\357\274\211.md" "b/contents/AsyncDisplayKit/\351\242\204\345\212\240\350\275\275\344\270\216\346\231\272\350\203\275\351\242\204\345\212\240\350\275\275\357\274\210iOS\357\274\211.md" new file mode 100644 index 0000000..af45a4b --- /dev/null +++ "b/contents/AsyncDisplayKit/\351\242\204\345\212\240\350\275\275\344\270\216\346\231\272\350\203\275\351\242\204\345\212\240\350\275\275\357\274\210iOS\357\274\211.md" @@ -0,0 +1,531 @@ +# 预加载与智能预加载(iOS) + +> 前两次的分享分别介绍了 ASDK 对于渲染的优化以及 ASDK 中使用的另一种布局模型;这两个新机制的引入分别解决了 iOS 在主线程渲染视图以及 Auto Layout 的性能问题,而这一次讨论的主要内容是 ASDK 如何预先请求服务器数据,达到看似无限滚动列表的效果的。 + +这篇文章是 ASDK 系列中的最后一篇,文章会介绍 iOS 中几种*预加载*的方案,以及 ASDK 中是如何处理预加载的。 + +不过,在介绍 ASDK 中实现**智能预加载**的方式之前,文章中会介绍几种简单的预加载方式,方便各位开发者进行对比,选择合适的机制实现预加载这一功能。 + +## 网络与性能 + +ASDK 通过在渲染视图和布局方面的优化已经可以使应用在任何用户的疯狂操作下都能保持 60 FPS 的流畅程度,也就是说,我们已经充分的利用了当前设备的性能,调动各种资源加快视图的渲染。 + +但是,仅仅在 CPU 以及 GPU 方面的优化往往是远远不够的。在目前的软件开发中,很难找到一个**没有任何网络请求**的应用,哪怕是一个记账软件也需要服务器来同步保存用户的信息,防止资料的丢失;所以,只在渲染这一层面进行优化还不能让用户的体验达到最佳,因为网络请求往往是一个应用**最为耗时以及昂贵**的操作。 + +![network](images/network.jpg) + +每一个应用程序在运行时都可以看做是 CPU 在底层利用各种资源疯狂做加减法运算,其中最耗时的操作并不是进行加减法的过程,而是**资源转移**的过程。 + +> 举一个不是很恰当的例子,主厨(CPU)在炒一道菜(计算)时往往需要的时间并不多,但是菜的采购以及准备(资源的转移)会占用大量的时间,如果在每次炒菜之前,都由帮厨提前准备好所有的食材(缓存),那么做一道菜的时间就大大减少了。 + +而提高资源转移的效率的最佳办法就是使用多级缓存: + +![multi-laye](images/multi-layer.jpg) + +从上到下,虽然容量越来越大,直到 Network 层包含了整个互联网的内容,但是访问时间也是直线上升;在 Core 或者三级缓存中的资源可能访问只需要几个或者几十个时钟周期,但是网络中的资源就**远远**大于这个数字,几分钟、几小时都是有可能的。 + +更糟糕的是,因为天朝的网络情况及其复杂,运营商劫持 DNS、404 无法访问等问题导致网络问题极其严重;而如何加速网络请求成为了很多移动端以及 Web 应用的重要问题。 + +## 预加载 + +本文就会提供一种**缓解网络请求缓慢导致用户体验较差**的解决方案,也就是预加载;在本地真正需要渲染界面之前就通过网络请求获取资源存入内存或磁盘。 + +> 预加载并不能彻底解决网络请求缓慢的问题,而是通过提前发起网络请求**缓解**这一问题。 + +那么,预加载到底要关注哪些方面的问题呢?总结下来,有以下两个关注点: + ++ 需要预加载的资源 ++ 预加载发出的时间 + +文章会根据上面的两个关注点,分别分析四种预加载方式的实现原理以及优缺点: + +1. 无限滚动列表 +2. threshold +3. 惰性加载 +4. 智能预加载 + +### 无限滚动列表 + +其实,无限滚动列表并不能算是一种预加载的实现原理,它只是提供一种分页显示的方法,在每次滚动到 `UITableView` 底部时,才会开始发起网络请求向服务器获取对应的资源。 + +虽然这种方法并不是预加载方式的一种,放在这里的主要作用是作为对比方案,看看如果不使用预加载的机制,用户体验是什么样的。 + +![infinite-list](images/infinite-list.jpg) + +很多客户端都使用了分页的加载方式,并没有添加额外的预加载的机制来提升用户体验,虽然这种方式并不是不能接受,不过每次滑动到视图底部之后,总要等待网络请求的完成确实对视图的流畅性有一定影响。 + +虽然仅仅使用无限滚动列表而不提供预加载机制会在一定程度上影响用户体验,不过,这种**需要用户等待几秒钟**的方式,在某些时候确实非常好用,比如:投放广告。 + +![advertise](images/advertise.jpg) + +> QQ 空间就是这么做的,它们**投放的广告基本都是在整个列表的最底端**,这样,当你滚动到列表最下面的时候,就能看到你急需的租房、租车、同城交友、信用卡办理、只有 iPhone 能玩的游戏以及各种奇奇怪怪的辣鸡广告了,很好的解决了我们的日常生活中的各种需求。(哈哈哈哈哈哈哈哈哈哈哈哈哈) + +### Threshold + +使用 Threshold 进行预加载是一种最为常见的预加载方式,知乎客户端就使用了这种方式预加载条目,而其原理也非常简单,根据当前 `UITableView` 的所在位置,除以目前整个 `UITableView.contentView` 的高度,来判断当前是否需要发起网络请求: + +```swift +let threshold: CGFloat = 0.7 +var currentPage = 0 + +override func scrollViewDidScroll(_ scrollView: UIScrollView) { + let current = scrollView.contentOffset.y + scrollView.frame.size.height + let total = scrollView.contentSize.height + let ratio = current / total + + if ratio >= threshold { + currentPage += 1 + print("Request page \(currentPage) from server.") + } +} +``` + +上面的代码在当前页面已经划过了 70% 的时候,就请求新的资源,加载数据;但是,仅仅使用这种方法会有另一个问题,尤其是当列表变得很长时,十分明显,比如说:用户从上向下滑动,总共加载了 5 页数据: + +| Page | Total | Threshold | Diff | +| :-: | :-: | :-: | :-: | +| 1 | 10 | 7 | 7 | +| 2 | 20 | 14 | 4 | +| 3 | 30 | 21 | 1 | +| 4 | 40 | 28 | -2 | +| 5 | 50 | 35 | -5 | + ++ Page 当前总页数; ++ Total 当前 `UITableView` 总元素个数; ++ Threshold 网络请求触发时间; ++ Diff 表示最新加载的页面被浏览了多少; + +当 Threshold 设置为 70% 的时候,其实并不是单页 70%,这就会导致**新加载的页面都没有看,应用就会发出另一次请求,获取新的资源**。 + +#### 动态的 Threshold + +解决这个问题的办法,还是比较简单的,通过修改上面的代码,将 Threshold 变成一个动态的值,随着页数的增长而增长: + +```swift +let threshold: CGFloat = 0.7 +let itemPerPage: CGFloat = 10 +var currentPage: CGFloat = 0 + +override func scrollViewDidScroll(_ scrollView: UIScrollView) { + let current = scrollView.contentOffset.y + scrollView.frame.size.height + let total = scrollView.contentSize.height + let ratio = current / total + + let needRead = itemPerPage * threshold + currentPage * itemPerPage + let totalItem = itemPerPage * (currentPage + 1) + let newThreshold = needRead / totalItem + + if ratio >= newThreshold { + currentPage += 1 + print("Request page \(currentPage) from server.") + } +} +``` + +通过这种方法获取的 `newThreshold` 就会随着页数的增长而动态的改变,解决了上面出现的问题: + +![dynamic-threshold](images/dynamic-threshold.jpeg) + +### 惰性加载 + +使用 Threshold 进行预加载其实已经适用于大多数应用场景了;但是,下面介绍的方式,*惰性加载*能够有针对性的加载用户“会看到的” Cell。 + +> *惰性加载*,就是在用户滚动的时候会对用户滚动结束的区域进行计算,只加载目标区域中的资源。 + +用户在飞速滚动中会看到巨多的空白条目,因为用户并不想阅读这些条目,所以,我们并不需要真正去加载这些内容,只需要在 `ASTableView/ASCollectionView` 中只根据用户滚动的目标区域惰性加载资源。 + +![lazy-loading](images/lazy-loading.png) + +惰性加载的方式不仅仅减少了网络请求的冗余资源,同时也减少了渲染视图、数据绑定的耗时。 + +计算用户滚动的目标区域可以直接使用下面的代理方法获取: + +```swift +let markedView = UIView() +let rowHeight: CGFloat = 44.0 + +override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + let targetOffset = targetContentOffset.pointee + let targetRect = CGRect(origin: targetOffset, size: scrollView.frame.size) + + markedView.frame = targetRect + markedView.backgroundColor = UIColor.black.withAlphaComponent(0.1) + tableView.addSubview(markedView) + + var indexPaths: [IndexPath] = [] + + let startIndex = Int(targetRect.origin.y / rowHeight) + let endIndex = Int((targetRect.origin.y + tableView.frame.height) / rowHeight) + + for index in startIndex...endIndex { + indexPaths.append(IndexPath(row: index, section: 0)) + } + + print("\(targetRect) \(indexPaths)") +} +``` + +> 以上代码只会大致计算出目标区域内的 `IndexPath` 数组,并不会展开新的 page,同时会使用浅黑色标记目标区域。 + +当然,惰性加载的实现也并不只是这么简单,不仅需要客户端的工作,同时因为需要**加载特定 offset 资源**,也需要服务端提供相应 API 的支持。 + +虽然惰性加载的方式能够按照用户的需要请求对应的资源,但是,在用户滑动 `UITableView` 的过程中会看到大量的空白条目,这样的用户体验是否可以接受又是值得考虑的问题了。 + +### 智能预加载 + +终于到了智能预加载的部分了,当我第一次得知 ASDK 可以通过滚动的方向预加载不同数量的内容,感觉是非常神奇的。 + + + +如上图所示 ASDK 把正在滚动的 ` ASTableView/ASCollectionView` 划分为三种状态: + ++ Fetch Data ++ Display ++ Visible + +上面的这三种状态都是由 ASDK 来管理的,而每一个 `ASCellNode` 的状态都是由 `ASRangeController` 控制,所有的状态都对应一个 `ASInterfaceState`: + ++ `ASInterfaceStatePreload` 当前元素貌似要显示到屏幕上,需要从磁盘或者网络请求数据; ++ `ASInterfaceStateDisplay` 当前元素非常可能要变成可见的,需要进行异步绘制; ++ `ASInterfaceStateVisible` 当前元素最少在屏幕上显示了 1px + +当用户滚动当前视图时,`ASRangeController` 就会修改不同区域内元素的状态: + + + +上图是用户在向下滑动时,`ASCellNode` 是如何被标记的,假设**当前视图可见的范围高度为 1**,那么在默认情况下,五个区域会按照上图的形式进行划分: + +| Buffer | Size | +| :-: | :-: | +| Fetch Data Leading Buffer | 2 | +| Display Leading Buffer | 1 | +| Visible | 1 | +| Display Trailing Buffer | 1 | +| Fetch Data Trailing Buffer | 1 | + +在滚动方向(Leading)上 Fetch Data 区域会是非滚动方向(Trailing)的两倍,ASDK 会根据滚动方向的变化实时改变缓冲区的位置;在向下滚动时,下面的 Fetch Data 区域就是上面的两倍,向上滚动时,上面的 Fetch Data 区域就是下面的两倍。 + +> 这里的两倍并不是一个确定的数值,ASDK 会根据当前设备的不同状态,改变不同区域的大小,但是**滚动方向的区域总会比非滚动方向大一些**。 + +智能预加载能够根据当前的滚动方向,自动改变当前的工作区域,选择合适的区域提前触发请求资源、渲染视图以及异步布局等操作,让视图的滚动达到真正的流畅。 + +#### 原理 + +在 ASDK 中整个智能预加载的概念是由三个部分来统一协调管理的: + ++ `ASRangeController` ++ `ASDataController` ++ `ASTableView` 与 `ASTableNode` + +对智能预加载实现的分析,也是根据这三个部分来介绍的。 + +#### 工作区域的管理 + +`ASRangeController` 是 `ASTableView` 以及 `ASCollectionView` 内部使用的控制器,主要用于监控视图的可见区域、维护工作区域、触发网络请求以及绘制、单元格的异步布局。 + +以 `ASTableView` 为例,在视图进行滚动时,会触发 `-[UIScrollView scrollViewDidScroll:]` 代理方法: + +```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 *allNodes = [_dataSource completedNodes]; + NSUInteger numberOfSections = [allNodes count]; + + NSArray *visibleNodePaths = [_dataSource visibleNodeIndexPathsForRangeController:self]; + + ASScrollDirection scrollDirection = [_dataSource scrollDirectionForRangeController:self]; + if (_layoutControllerImplementsSetViewportSize) { + [_layoutController setViewportSize:[_dataSource viewportSizeForRangeController:self]]; + } + + if (_layoutControllerImplementsSetVisibleIndexPaths) { + [_layoutController setVisibleNodeIndexPaths:visibleNodePaths]; + } + ... +} +``` + +当前 `ASRangeController` 的数据源以及代理就是 `ASTableView`,这段代码首先就获取了完成计算和布局的 `ASCellNode` 以及可见的 `ASCellNode` 的 `NSIndexPath`: + +```objectivec +- (void)_updateVisibleNodeIndexPaths { + NSArray *currentSectionNodes = nil; + NSInteger currentSectionIndex = -1; + NSUInteger numberOfNodesInSection = 0; + + NSSet *visibleIndexPaths = [NSSet setWithArray:visibleNodePaths]; + NSSet *displayIndexPaths = nil; + NSSet *preloadIndexPaths = nil; + + NSMutableOrderedSet *allIndexPaths = [[NSMutableOrderedSet alloc] initWithSet:visibleIndexPaths]; + + ASLayoutRangeMode rangeMode = _currentRangeMode; + + ASRangeTuningParameters parametersPreload = [_layoutController tuningParametersForRangeMode:rangeMode + rangeType:ASLayoutRangeTypePreload]; + if (ASRangeTuningParametersEqualToRangeTuningParameters(parametersPreload, ASRangeTuningParametersZero)) { + preloadIndexPaths = visibleIndexPaths; + } else { + preloadIndexPaths = [_layoutController indexPathsForScrolling:scrollDirection + rangeMode:rangeMode + rangeType:ASLayoutRangeTypePreload]; + } + + #: displayIndexPaths 的计算和 preloadIndexPaths 非常类似 + + [allIndexPaths unionSet:displayIndexPaths]; + [allIndexPaths unionSet:preloadIndexPaths]; + ... +} +``` + +预加载以及展示部分的 `ASRangeTuningParameters` 都是以二维数组的形式保存在 `ASAbstractLayoutController` 中的: + +![aslayout-range-mode-display-preload](images/aslayout-range-mode-display-preload.jpeg) + +在获取了 `ASRangeTuningParameters` 之后,ASDK 也会通过 `ASFlowLayoutController` 的方法 `-[ASFlowLayoutController indexPathsForScrolling:rangeMode:rangeType:]` 获取 `NSIndexPath` 对象的集合: + +```objectivec +- (NSSet *)indexPathsForScrolling:(ASScrollDirection)scrollDirection rangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType { + #: 获取 directionalBuffer 以及 viewportDirectionalSize + ASIndexPath startPath = [self findIndexPathAtDistance:(-directionalBuffer.negativeDirection * viewportDirectionalSize) + fromIndexPath:_visibleRange.start]; + ASIndexPath endPath = [self findIndexPathAtDistance:(directionalBuffer.positiveDirection * viewportDirectionalSize) + fromIndexPath:_visibleRange.end]; + + NSMutableSet *indexPathSet = [[NSMutableSet alloc] init]; + NSArray *completedNodes = [_dataSource completedNodes]; + ASIndexPath currPath = startPath; + while (!ASIndexPathEqualToIndexPath(currPath, endPath)) { + [indexPathSet addObject:[NSIndexPath indexPathWithASIndexPath:currPath]]; + currPath.row++; + + while (currPath.row >= [(NSArray *)completedNodes[currPath.section] count] && currPath.section < endPath.section) { + currPath.row = 0; + currPath.section++; + } + } + [indexPathSet addObject:[NSIndexPath indexPathWithASIndexPath:endPath]]; + return indexPathSet; +} +``` + +方法的执行过程非常简单,根据 `ASRangeTuningParameters` 获取该滚动方向上的缓冲区大小,在区域内遍历所有的 `ASCellNode` 查看其是否在当前区域内,然后加入数组中。 + +到这里,所有工作区域 `visibleIndexPaths` `displayIndexPaths` 以及 `preloadIndexPaths` 都已经获取到了;接下来,就到了遍历 `NSIndexPath`,修改结点状态的过程了; + +```objectivec +- (void)_updateVisibleNodeIndexPaths { + ... + for (NSIndexPath *indexPath in allIndexPaths) { + ASInterfaceState interfaceState = ASInterfaceStateMeasureLayout; + + if (ASInterfaceStateIncludesVisible(selfInterfaceState)) { + if ([visibleIndexPaths containsObject:indexPath]) { + interfaceState |= (ASInterfaceStateVisible | ASInterfaceStateDisplay | ASInterfaceStatePreload); + } else { + if ([preloadIndexPaths containsObject:indexPath]) { + interfaceState |= ASInterfaceStatePreload; + } + if ([displayIndexPaths containsObject:indexPath]) { + interfaceState |= ASInterfaceStateDisplay; + } + } + } +``` + +根据当前 `ASTableView` 的状态以及 `NSIndexPath` 所在的区域,打开 `ASInterfaceState` 对应的位。 + +```objectivec + NSInteger section = indexPath.section; + NSInteger row = indexPath.row; + + if (section >= 0 && row >= 0 && section < numberOfSections) { + if (section != currentSectionIndex) { + currentSectionNodes = allNodes[section]; + numberOfNodesInSection = [currentSectionNodes count]; + currentSectionIndex = section; + } + + if (row < numberOfNodesInSection) { + ASDisplayNode *node = currentSectionNodes[row]; + + if (node.interfaceState != interfaceState) { + BOOL nodeShouldScheduleDisplay = [node shouldScheduleDisplayWithNewInterfaceState:interfaceState]; + [node recursivelySetInterfaceState:interfaceState]; + + if (nodeShouldScheduleDisplay) { + [self registerForNodeDisplayNotificationsForInterfaceStateIfNeeded:selfInterfaceState]; + if (_didRegisterForNodeDisplayNotifications) { + _pendingDisplayNodesTimestamp = CFAbsoluteTimeGetCurrent(); + } + } + } + } + } + } + ... +} +``` + +后面的一部分代码就会递归的设置结点的 `interfaceState`,并且在当前 `ASRangeController` 的 `ASLayoutRangeMode` 发生改变时,发出通知,调用 `-[ASRangeController _updateVisibleNodeIndexPaths]` 私有方法,更新结点的状态。 + +```objectivec +- (void)scheduledNodesDidDisplay:(NSNotification *)notification { + CFAbsoluteTime notificationTimestamp = ((NSNumber *) notification.userInfo[ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp]).doubleValue; + if (_pendingDisplayNodesTimestamp < notificationTimestamp) { + [[NSNotificationCenter defaultCenter] removeObserver:self name:ASRenderingEngineDidDisplayScheduledNodesNotification object:nil]; + _didRegisterForNodeDisplayNotifications = NO; + + [self setNeedsUpdate]; + } +} +``` + +#### 数据的加载和更新 + +`ASTableNode` 既然是对 `ASTableView` 的封装,那么表视图中显示的数据仍然需要数据源来提供,而在 ASDK 中这一机制就比较复杂: + +![astableview-data](images/astableview-data.png) + +整个过程是由四部分协作完成的,`Controller`、`ASTableNode`、`ASTableView` 以及 `ASDataController`,网络请求发起并返回数据之后,会调用 `ASTableNode` 的 API 执行插入行的方法,最后再通过 `ASTableView` 的同名方法,执行管理和更新节点数据的 `ASDataController` 的方法: + +```objectivec +- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + + NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; + NSMutableArray *contexts = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; + + __weak id environment = [self.environmentDelegate dataControllerEnvironment]; + + for (NSIndexPath *indexPath in sortedIndexPaths) { + ASCellNodeBlock nodeBlock = [_dataSource dataController:self nodeBlockAtIndexPath:indexPath]; + ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:ASDataControllerRowNodeKind atIndexPath:indexPath]; + [contexts addObject:[[ASIndexedNodeContext alloc] initWithNodeBlock:nodeBlock + indexPath:indexPath + supplementaryElementKind:nil + constrainedSize:constrainedSize + environment:environment]]; + } + ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(_nodeContexts[ASDataControllerRowNodeKind], sortedIndexPaths, contexts); + dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ + [self _batchLayoutAndInsertNodesFromContexts:contexts withAnimationOptions:animationOptions]; + }); +} +``` + +上面的方法总共做了几件事情: + +1. 遍历所有要插入的 `NSIndexPath` 数组,然后从数据源中获取对应的 `ASCellNodeBlock`; +2. 获取每一个 `NSIndexPath` 对应的单元的大小 `constrainedSize`(在图中没有表现出来); +3. 初始化一堆 `ASIndexedNodeContext` 实例,然后加入到控制器维护的 `_nodeContexts` 数组中; +4. 将节点插入到 `_completedNodes` 中,用于之后的缓存,以及提供给 `ASTableView` 的数据源代理方法使用; + +`ASTableView` 会将数据源协议的代理设置为自己,而最常见的数据源协议在 `ASTableView` 中的实现是这样的: + +```objectivec +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + _ASTableViewCell *cell = [self dequeueReusableCellWithIdentifier:kCellReuseIdentifier forIndexPath:indexPath]; + cell.delegate = self; + + ASCellNode *node = [_dataController nodeAtCompletedIndexPath:indexPath]; + if (node) { + [_rangeController configureContentView:cell.contentView forCellNode:node]; + cell.node = node; + cell.backgroundColor = node.backgroundColor; + cell.selectionStyle = node.selectionStyle; + cell.clipsToBounds = node.clipsToBounds; + } + + return cell; +} +``` + +上面的方法会从 `ASDataController` 中的 `_completedNodes` 中获取元素的数量信息: + +![cellforrowatindexpath](images/cellforrowatindexpath.jpg) + +> 在内部 `_externalCompletedNodes` 与 `_completedNodes` 作用基本相同,在这里我们不对它们的区别进行分析以及解释。 + +当 `ASTableView` 向数据源请求数据时,ASDK 就会从对应的 `ASDataController` 中取回最新的 `node`,添加在 `_ASTableViewCell` 的实例上显示出来。 + +#### ASTableView 和 ASTableNode + +`ASTableView` 和 `ASTableNode` 的关系,其实就相当于 `CALayer` 和 `UIView` 的关系一样,后者都是前者的一个包装: + +![astableview-astablenode](images/astableview-astablenode.jpg) + +`ASTableNode` 为开发者提供了非常多的接口,其内部实现往往都是直接调用 `ASTableView` 的对应方法,在这里简单举几个例子: + +```objectivec +- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { + [self.view insertSections:sections withRowAnimation:animation]; +} + +- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { + [self.view deleteSections:sections withRowAnimation:animation]; +} +``` + +如果你再去看 `ASTableView` 中方法的实现的话,会发现很多方法都是由 `ASDataController` 和 `ASRangeController` 驱动的,上面的两个方法的实现就是这样的: + +```objectivec +- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { + if (sections.count == 0) { return; } + [_dataController insertSections:sections withAnimationOptions:animation]; +} + +- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { + if (sections.count == 0) { return; } + [_dataController deleteSections:sections withAnimationOptions:animation]; +} +``` + +到这里,整个智能预加载的部分就结束了,从*需要预加载的资源*以及*预加载发出的时间*两个方面来考虑,ASDK 在不同工作区域中合理标记了需要预加载的资源,并在节点状态改变时就发出请求,在用户体验上是非常优秀的。 + +## 总结 + +ASDK 中的表视图以及智能预加载其实都是通过下面这四者共同实现的,上层只会暴露出 `ASTableNode` 的接口,所有的数据的批量更新、工作区域的管理都是在幕后由 `ASDataController` 以及 `ASRangeController` 这两个控制器协作完成。 + +![multi-layer-asdk](images/multi-layer-asdk.jpg) + +智能预加载的使用相比其它实现可能相对复杂,但是在笔者看来,ASDK 对于这一套机制的实现还是非常完善的,同时也提供了极其优秀的用户体验,不过同时带来的也是相对较高的学习成本。 + +如果真正要选择预加载的机制,笔者觉得最好从 Threshold 以及智能预加载两种方式中选择: + +![pros-cons](images/pros-cons.jpg) + +这两种方式的选择,其实也就是实现复杂度和用户体验之间的权衡了。 + +> Github Repo:[iOS-Source-Code-Analyze](https://github.com/draveness/iOS-Source-Code-Analyze) +> +> Follow: [Draveness · GitHub](https://github.com/Draveness) +> +> Source: http://draveness.me/preload + + diff --git "a/BlocksKit/\347\245\236\345\245\207\347\232\204 BlocksKit \357\274\210\344\270\200\357\274\211.md" "b/contents/BlocksKit/\347\245\236\345\245\207\347\232\204 BlocksKit \357\274\210\344\270\200\357\274\211.md" similarity index 99% rename from "BlocksKit/\347\245\236\345\245\207\347\232\204 BlocksKit \357\274\210\344\270\200\357\274\211.md" rename to "contents/BlocksKit/\347\245\236\345\245\207\347\232\204 BlocksKit \357\274\210\344\270\200\357\274\211.md" index 6233bb0..b718ad1 100644 --- "a/BlocksKit/\347\245\236\345\245\207\347\232\204 BlocksKit \357\274\210\344\270\200\357\274\211.md" +++ "b/contents/BlocksKit/\347\245\236\345\245\207\347\232\204 BlocksKit \357\274\210\344\270\200\357\274\211.md" @@ -94,7 +94,7 @@ BlocksKit 在这些集合类中所添加的一些方法在 Ruby、Haskell 等语 ```objectivec NSObject *test = [[NSObject alloc] init]; -[test bk_associateValue:@"Draveness" withKey:@" name"]; +[test bk_associateValue:@"Draveness" withKey:@"name"]; NSLog(@"%@",[test bk_associatedValueForKey:@"name"]); 2016-03-05 16:02:25.761 Draveness[10699:452125] Draveness @@ -630,8 +630,8 @@ void (^block)(void) = ^{ 由于这篇文章中的内容较多,所以内容分成了两个部分,下一部分介绍的是 BlocksKit 中的最重要的部分动态代理: -+ [神奇的 BlocksKit(一)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/BlocksKit/神奇的%20BlocksKit%20(一).md) -+ [神奇的 BlocksKit(二)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/BlocksKit/神奇的%20BlocksKit%20(二).md) ++ [神奇的 BlocksKit(一)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/BlocksKit/神奇的%20BlocksKit%20(一).md) ++ [神奇的 BlocksKit(二)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/BlocksKit/神奇的%20BlocksKit%20(二).md) diff --git "a/BlocksKit/\347\245\236\345\245\207\347\232\204 BlocksKit \357\274\210\344\272\214\357\274\211.md" "b/contents/BlocksKit/\347\245\236\345\245\207\347\232\204 BlocksKit \357\274\210\344\272\214\357\274\211.md" similarity index 98% rename from "BlocksKit/\347\245\236\345\245\207\347\232\204 BlocksKit \357\274\210\344\272\214\357\274\211.md" rename to "contents/BlocksKit/\347\245\236\345\245\207\347\232\204 BlocksKit \357\274\210\344\272\214\357\274\211.md" index b2f1037..43033ce 100644 --- "a/BlocksKit/\347\245\236\345\245\207\347\232\204 BlocksKit \357\274\210\344\272\214\357\274\211.md" +++ "b/contents/BlocksKit/\347\245\236\345\245\207\347\232\204 BlocksKit \357\274\210\344\272\214\357\274\211.md" @@ -4,8 +4,8 @@ Blog: [Draveness](http://draveness.me) 这篇文章『神奇的 BlocksKit』的第二部分,关于第一部分的内容在这里: -+ [神奇的 BlocksKit(一)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/BlocksKit/神奇的%20BlocksKit%20(一).md) -+ [神奇的 BlocksKit(二)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/BlocksKit/神奇的%20BlocksKit%20(二).md) ++ [神奇的 BlocksKit(一)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/BlocksKit/神奇的%20BlocksKit%20(一).md) ++ [神奇的 BlocksKit(二)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/BlocksKit/神奇的%20BlocksKit%20(二).md) ## 动态代理 @@ -680,8 +680,8 @@ if (retSize) { 我写这篇文章大约用了七天的时间,如果你对其中的内容有些疑问,可以发邮件或者在下面留言。 -+ [神奇的 BlocksKit(一)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/BlocksKit/神奇的%20BlocksKit%20(一).md) -+ [神奇的 BlocksKit(二)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/BlocksKit/神奇的%20BlocksKit%20(二).md) ++ [神奇的 BlocksKit(一)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/BlocksKit/神奇的%20BlocksKit%20(一).md) ++ [神奇的 BlocksKit(二)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/BlocksKit/神奇的%20BlocksKit%20(二).md) diff --git a/contents/Blog/images/initialize-comments/new-token.png b/contents/Blog/images/initialize-comments/new-token.png new file mode 100644 index 0000000..9404350 Binary files /dev/null and b/contents/Blog/images/initialize-comments/new-token.png differ diff --git a/contents/Blog/images/initialize-comments/personal-access-token.png b/contents/Blog/images/initialize-comments/personal-access-token.png new file mode 100644 index 0000000..bd43b0a Binary files /dev/null and b/contents/Blog/images/initialize-comments/personal-access-token.png differ diff --git a/contents/Blog/initialize-comments.md b/contents/Blog/initialize-comments.md new file mode 100644 index 0000000..ded378d --- /dev/null +++ b/contents/Blog/initialize-comments.md @@ -0,0 +1,136 @@ +# 如何自动初始化 Gitalk/Gitment 评论 + +之前的博客一直都使用 Disqus 作为评论系统,然后因为 Disqus 在国内无法访问,很多读者都只能通过邮件的方式咨询一些问题,昨天觉得长痛不如短痛,直接将博客的评论迁移到了 [Gitalk](https://github.com/gitalk/gitalk),最开始选择了使用 Gitment 作为评论系统,但是由于其开发者很久没有维护、代码七个月也没有更新,所以就选择了有更多人维护的 Gitalk 作为目前博客的评论系统。 + +无论是 Gitalk 还是 Gitment 都只能手动初始化所有文章的评论或者一个一个点开界面,作者觉得这件事情非常麻烦,所以手动抓了一下 Gitalk 和 Gitment 在初始化评论时发出的网络请求后写了一个用于自动化初始评论的脚本。 + +## 获得权限 + +在使用该脚本之前首先要在 GitHub 创建一个新的 [Personal access tokens](https://github.com/settings/tokens),选择 `Generate new token` 后,在当前的页面中为 Token 添加所有 Repo 的权限: + +![personal-access-token](images/initialize-comments/personal-access-token.png) + +在这里创建之后,点击界面最下的面 `Generate token` 按钮获得一个新的 token: + +![new-token](images/initialize-comments/new-token.png) + +> 作者已经把这个 token 删掉了,不要想着用这个 token 就能获得到作者 GitHub 的权限。 + +## 脚本 + +作者在抓取了 Gitalk 和 Gitment 的 API 请求发现,这两个评论服务是**通过 GitHub 提供的 API 创建含有相应标签的 issue**,所以我们应该也可以直接使用 GitHub 的 API 创建所有博客文章对应的 issue,这与通过评论插件创建 issue 是完全一样的,在创建之后无论是 Gitalk 还是 Gitment 都可以通过对应的标签直接在仓库中找到对应的 issue 了。 + +本文中提供的自动化脚本使用的是 Ruby 代码,请确定自己的机器上已经安装了 Ruby,并且使用如下的命令安装脚本所需要的所有依赖: + +```ruby +$ sudo gem install faraday activesupport sitemap-parser +``` + +### 使用 sitemap 文件 + +如果我们使用的博客服务是 Jekyll,那么就可以通过 [jekyll-sitemap](https://github.com/jekyll/jekyll-sitemap) 插件为博客创建对应的 sitemap 文件,例如:https://draveness.me/sitemap.xml。 + +有 sitemap 文件之后就非常好办了,在任意目录下创建 `comment.rb` 文件后,将下面的代码粘贴到文件中: + +```ruby +username = "draveness" # GitHub 用户名 +new_token = "xxxxxxx" # GitHub Token +repo_name = "github-comments-repo" # 存放 issues +sitemap_url = "https://draveness.me/sitemap.xml" # sitemap +kind = "Gitalk" # "Gitalk" or "gitment" + +require 'open-uri' +require 'faraday' +require 'active_support' +require 'active_support/core_ext' +require 'sitemap-parser' + +sitemap = SitemapParser.new sitemap_url +urls = sitemap.to_a + +conn = Faraday.new(:url => "https://api.github.com/repos/#{username}/#{repo_name}/issues") do |conn| + conn.basic_auth(username, token) + conn.adapter Faraday.default_adapter +end + +urls.each_with_index do |url, index| + title = open(url).read.scan(/(.*?)<\/title>/).first.first.force_encoding('UTF-8') + response = conn.post do |req| + req.body = { body: url, labels: [kind, url], title: title }.to_json + end + puts response.body + sleep 15 if index % 20 == 0 +end +``` + +在这里有 5 个配置项,分别是 GitHub 用户名、在上一步获得的 Token、存放 issues 的仓库、sitemap 的地址以及最后你在博客中使用了哪个评论插件,不同的插件拥有标签,可以选择 `"Gitalk"` 或者 `"gitment"`,对于其他评论的插件应该如何设置,作者就并不清楚了。 + +> 需要注意的是,在使用上述的代码为博客创建 issue 时,会为博客中 sitemap 包含的**全部界面创建对应的 issue**,其中包括例如首页、标签界面等,这对于作者来说不是太大的问题,但是对这个问题敏感的读者可以使用下一小节中的代码。 + +在配置完成后就可以在命令行中的当前目录下输入以下命令: + +```ruby +$ ruby comment.rb +``` + +然后当前脚本就会运行并初始化所有的评论了。 + +### 无 sitemap 或自定义 + +如果博客中不包含任何的 sitemap 文件,或者想要手动选择想要初始化的一些文章其实也是可以的,可以使用简化的代码**批量初始化指定博客**的评论: + +```ruby +username = "draveness" # GitHub 用户名 +new_token = "xxxxxxx" # GitHub Token +repo_name = "github-comments-repo" # 存放 issues +kind = "Gitalk" # "Gitalk" or "gitment" +urls = ["xxxxx"] + +require 'open-uri' +require 'faraday' +require 'active_support' +require 'active_support/core_ext' + +conn = Faraday.new(:url => "https://api.github.com/repos/#{username}/#{repo_name}/issues") do |conn| + conn.basic_auth(username, token) + conn.adapter Faraday.default_adapter +end + +urls.each_with_index do |url, index| + title = open(url).read.scan(/<title>(.*?)<\/title>/).first.first.force_encoding('UTF-8') + response = conn.post do |req| + req.body = { body: url, labels: [kind, url], title: title }.to_json + end + puts response.body + sleep 15 if index % 20 == 0 +end +``` + +在这里就需要**手动填入需要初始化文章的数组**了,当然如果你有 sitemap 文件,其实可以在 irb 中运行以下的代码得到全部的文章数组,再手动从其中剔除不想要创建评论的页面: + +```ruby +$ irb +2.3.3 :001 > require 'sitemap-parser' + => true +2.3.3 :002 > sitemap_url = "https://draveness.me/sitemap.xml" + => "https://draveness.me/sitemap.xml" +2.3.3 :003 > SitemapParser.new(sitemap_url).to_a + => ["https://draveness.me/prolog-ji-chu-1", "https://draveness.me/prolog-pi-pei-2", "https://draveness.me/prolog-di-gui-3", ..., "https://draveness.me/dynamo"] + ``` + +当我们将上述结果中不想要创建评论的文章删除之后,将结果填到 `urls` 这个临时变量中,运行下面的命令就可以了。 + +```ruby +$ ruby comment.rb +``` + +## 其他 + +由于 GitHub 会脚本的请求会做一定的限制,所以在连续请求接口,批量创建 issues 的过程中可能会出现创建失败的情况,你可以通过命令中打印的结果看到,不过在脚本中已经在每 20 次创建时休眠 15 秒,所以应该也不会遇到这个问题。 + +另外,GitHub 中 issue 的可以创建但是并不能删除,所以在配置时请一定检查好所有的配置项是否正确,否则会批量创建一些无用的 issue 虽然没有什么影响,但是看起来非常头疼。 + +## 总结 + +手动初始化每一篇文章的评论确实是非常痛苦的,放弃 Disqus 确实也考虑了比较久的事件,Disqus 中也确实有一些有价值的评论,但是本着长痛不如短痛的原则,还是选择迁移到 Gitalk,当然作者也希望 Gitalk 官方能够提供更好地使用体验。 + diff --git "a/contents/CocoaPods/CocoaPods \351\203\275\345\201\232\344\272\206\344\273\200\344\271\210\357\274\237.md" "b/contents/CocoaPods/CocoaPods \351\203\275\345\201\232\344\272\206\344\273\200\344\271\210\357\274\237.md" new file mode 100644 index 0000000..714ae26 --- /dev/null +++ "b/contents/CocoaPods/CocoaPods \351\203\275\345\201\232\344\272\206\344\273\200\344\271\210\357\274\237.md" @@ -0,0 +1,667 @@ +# CocoaPods 都做了什么? + +稍有 iOS 开发经验的人应该都是用过 CocoaPods,而对于 CI、CD 有了解的同学也都知道 Fastlane。而这两个在 iOS 开发中非常便捷的第三方库都是使用 Ruby 来编写的,这是为什么? + +![](images/cocoapods-image.jpg) + +先抛开这个话题不谈,我们来看一下 CocoaPods 和 Fastlane 是如何使用的,首先是 CocoaPods,在每一个使用 CocoaPods 的工程中都有一个 Podfile: + +```ruby +source 'https://github.com/CocoaPods/Specs.git' + +target 'Demo' do + pod 'Mantle', '~> 1.5.1' + pod 'SDWebImage', '~> 3.7.1' + pod 'BlocksKit', '~> 2.2.5' + pod 'SSKeychain', '~> 1.2.3' + pod 'UMengAnalytics', '~> 3.1.8' + pod 'UMengFeedback', '~> 1.4.2' + pod 'Masonry', '~> 0.5.3' + pod 'AFNetworking', '~> 2.4.1' + pod 'Aspects', '~> 1.4.1' +end +``` + +这是一个使用 Podfile 定义依赖的一个例子,不过 Podfile 对约束的描述其实是这样的: + +```ruby +source('https://github.com/CocoaPods/Specs.git') + +target('Demo') do + pod('Mantle', '~> 1.5.1') + ... +end +``` + +> Ruby 代码在调用方法时可以省略括号。 + +Podfile 中对于约束的描述,其实都可以看作是对代码简写,上面的代码在解析时可以当做 Ruby 代码来执行。 + +Fastlane 中的代码 Fastfile 也是类似的: + +```ruby +lane :beta do + increment_build_number + cocoapods + match + testflight + sh "./customScript.sh" + slack +end +``` + +使用描述性的”代码“编写脚本,如果没有接触或者使用过 Ruby 的人很难相信上面的这些文本是代码的。 + +## Ruby 概述 + +在介绍 CocoaPods 的实现之前,我们需要对 Ruby 的一些特性有一个简单的了解,在向身边的朋友“传教”的时候,我往往都会用优雅这个词来形容这门语言~~(手动微笑)~~。 + +除了优雅之外,Ruby 的语法具有强大的表现力,并且其使用非常灵活,能快速实现我们的需求,这里简单介绍一下 Ruby 中的一些特性。 + +### 一切皆对象 + +在许多语言,比如 Java 中,数字与其他的基本类型都不是对象,而在 Ruby 中所有的元素,包括基本类型都是对象,同时也不存在运算符的概念,所谓的 `1 + 1`,其实只是 `1.+(1)` 的语法糖而已。 + +得益于一切皆对象的概念,在 Ruby 中,你可以向任意的对象发送 `methods` 消息,在运行时自省,所以笔者在每次忘记方法时,都会直接用 `methods` 来“查文档”: + +```ruby +2.3.1 :003 > 1.methods + => [:%, :&, :*, :+, :-, :/, :<, :>, :^, :|, :~, :-@, :**, :<=>, :<<, :>>, :<=, :>=, :==, :===, :[], :inspect, :size, :succ, :to_s, :to_f, :div, :divmod, :fdiv, :modulo, :abs, :magnitude, :zero?, :odd?, :even?, :bit_length, :to_int, :to_i, :next, :upto, :chr, :ord, :integer?, :floor, :ceil, :round, :truncate, :downto, :times, :pred, :to_r, :numerator, :denominator, :rationalize, :gcd, :lcm, :gcdlcm, :+@, :eql?, :singleton_method_added, :coerce, :i, :remainder, :real?, :nonzero?, :step, :positive?, :negative?, :quo, :arg, :rectangular, :rect, :polar, :real, :imaginary, :imag, :abs2, :angle, :phase, :conjugate, :conj, :to_c, :between?, :instance_of?, :public_send, :instance_variable_get, :instance_variable_set, :instance_variable_defined?, :remove_instance_variable, :private_methods, :kind_of?, :instance_variables, :tap, :is_a?, :extend, :define_singleton_method, :to_enum, :enum_for, :=~, :!~, :respond_to?, :freeze, :display, :send, :object_id, :method, :public_method, :singleton_method, :nil?, :hash, :class, :singleton_class, :clone, :dup, :itself, :taint, :tainted?, :untaint, :untrust, :trust, :untrusted?, :methods, :protected_methods, :frozen?, :public_methods, :singleton_methods, :!, :!=, :__send__, :equal?, :instance_eval, :instance_exec, :__id__] +``` + +比如在这里向对象 `1` 调用 `methods` 就会返回它能响应的所有方法。 + +一切皆对象不仅减少了语言中类型的不一致,消灭了基本数据类型与对象之间的边界;这一概念同时也简化了语言中的组成元素,这样 Ruby 中只有对象和方法,这两个概念,这也降低了我们理解这门语言的复杂度: + ++ 使用对象存储状态 ++ 对象之间通过方法通信 + +### block + +Ruby 对函数式编程范式的支持是通过 block,这里的 block 和 Objective-C 中的 block 有些不同。 + +首先 Ruby 中的 block 也是一种对象,所有的 Block 都是 Proc 类的实例,也就是所有的 block 都是 first-class 的,可以作为参数传递,返回。 + +```ruby +def twice(&proc) + 2.times { proc.call() } if proc +end + +def twice + 2.times { yield } if block_given? +end +``` + +> `yield` 会调用外部传入的 block,`block_given?` 用于判断当前方法是否传入了 `block`。 + +在这个方法调用时,是这样的: + +```ruby +twice do + puts "Hello" +end +``` + +### eval + +最后一个需要介绍的特性就是 `eval` 了,早在几十年前的 Lisp 语言就有了 `eval` 这个方法,这个方法会将字符串当做代码来执行,也就是说 `eval` 模糊了代码与数据之间的边界。 + +```ruby +> eval "1 + 2 * 3" + => 7 +``` + +有了 `eval` 方法,我们就获得了更加强大的动态能力,在运行时,使用字符串来改变控制流程,执行代码;而不需要去手动解析输入、生成语法树。 + +### 手动解析 Podfile + +在我们对 Ruby 这门语言有了一个简单的了解之后,就可以开始写一个简易的解析 Podfile 的脚本了。 + +在这里,我们以一个非常简单的 Podfile 为例,使用 Ruby 脚本解析 Podfile 中指定的依赖: + +```ruby +source 'http://source.git/' +platform :ios, '8.0' + +target 'Demo' do + pod 'AFNetworking' + pod 'SDWebImage' + pod 'Masonry' + pod "Typeset" + pod 'BlocksKit' + pod 'Mantle' + pod 'IQKeyboardManager' + pod 'IQDropDownTextField' +end +``` + +因为这里的 `source`、`platform`、`target` 以及 `pod` 都是方法,所以在这里我们需要构建一个包含上述方法的上下文: + +```ruby +# eval_pod.rb +$hash_value = {} + +def source(url) +end + +def target(target) +end + +def platform(platform, version) +end + +def pod(pod) +end +``` + +使用一个全局变量 `hash_value` 存储 Podfile 中指定的依赖,并且构建了一个 Podfile 解析脚本的骨架;我们先不去完善这些方法的实现细节,先尝试一下读取 Podfile 中的内容并执行会不会有什么问题。 + +在 `eval_pod.rb` 文件的最下面加入这几行代码: + +```ruby +content = File.read './Podfile' +eval content +p $hash_value +``` + +这里读取了 Podfile 文件中的内容,并把其中的内容当做字符串执行,最后打印 `hash_value` 的值。 + +```shell +$ ruby eval_pod.rb +``` + +运行这段 Ruby 代码虽然并没有什么输出,但是并没有报出任何的错误,接下来我们就可以完善这些方法了: + +```ruby +def source(url) + $hash_value['source'] = url +end + +def target(target) + targets = $hash_value['targets'] + targets = [] if targets == nil + targets << target + $hash_value['targets'] = targets + yield if block_given? +end + +def platform(platform, version) +end + +def pod(pod) + pods = $hash_value['pods'] + pods = [] if pods == nil + pods << pod + $hash_value['pods'] = pods +end +``` + +在添加了这些方法的实现之后,再次运行脚本就会得到 Podfile 中的依赖信息了,不过这里的实现非常简单的,很多情况都没有处理: + +```shell +$ ruby eval_pod.rb +{"source"=>"http://source.git/", "targets"=>["Demo"], "pods"=>["AFNetworking", "SDWebImage", "Masonry", "Typeset", "BlocksKit", "Mantle", "IQKeyboardManager", "IQDropDownTextField"]} +``` + +CocoaPods 中对于 Podfile 的解析与这里的实现其实差不多,接下来就进入了 CocoaPods 的实现部分了。 + +## CocoaPods 的实现 + +在上面简单介绍了 Ruby 的一些语法以及如何解析 Podfile 之后,我们开始深入了解一下 CocoaPods 是如何管理 iOS 项目的依赖,也就是 `pod install` 到底做了些什么。 + +### Pod install 的过程 + +`pod install` 这个命令到底做了什么?首先,在 CocoaPods 中,所有的命令都会由 `Command` 类派发到将对应的类,而真正执行 `pod install` 的类就是 `Install`: + +```ruby +module Pod + class Command + class Install < Command + def run + verify_podfile_exists! + installer = installer_for_config + installer.repo_update = repo_update?(:default => false) + installer.update = false + installer.install! + end + end + end +end +``` + +这里面会从配置类的实例 `config` 中获取一个 `Installer` 的实例,然后执行 `install!` 方法,这里的 `installer` 有一个 `update` 属性,而这也就是 `pod install` 和 `update` 之间最大的区别,**其中后者会无视已有的 Podfile.lock 文件,重新对依赖进行分析**: + +```ruby +module Pod + class Command + class Update < Command + def run + ... + + installer = installer_for_config + installer.repo_update = repo_update?(:default => true) + installer.update = true + installer.install! + end + end + end +end +``` + +### Podfile 的解析 + +Podfile 中依赖的解析其实是与我们在手动解析 Podfile 章节所介绍的差不多,整个过程主要都是由 **CocoaPods-Core** 这个模块来完成的,而这个过程早在 `installer_for_config` 中就已经开始了: + +```ruby +def installer_for_config + Installer.new(config.sandbox, config.podfile, config.lockfile) +end +``` + +这个方法会从 `config.podfile` 中取出一个 `Podfile` 类的实例: + +```ruby +def podfile + @podfile ||= Podfile.from_file(podfile_path) if podfile_path +end +``` + +类方法 `Podfile.from_file` 就定义在 CocoaPods-Core 这个库中,用于分析 Podfile 中定义的依赖,这个方法会根据 Podfile 不同的类型选择不同的调用路径: + +```ruby +Podfile.from_file +`-- Podfile.from_ruby + |-- File.open + `-- eval +``` + +`from_ruby` 类方法就会像我们在前面做的解析 Podfile 的方法一样,从文件中读取数据,然后使用 `eval` 直接将文件中的内容当做 Ruby 代码来执行。 + +```ruby +def self.from_ruby(path, contents = nil) + contents ||= File.open(path, 'r:utf-8', &:read) + + podfile = Podfile.new(path) do + begin + eval(contents, nil, path.to_s) + rescue Exception => e + message = "Invalid `#{path.basename}` file: #{e.message}" + raise DSLError.new(message, path, e, contents) + end + end + podfile +end +``` + +在 Podfile 这个类的顶部,我们使用 Ruby 的 `Mixin` 的语法来混入 Podfile 中代码执行所需要的上下文: + +```ruby +include Pod::Podfile::DSL +``` + +Podfile 中的所有你见到的方法都是定义在 `DSL` 这个模块下面的: + +```ruby +module Pod + class Podfile + module DSL + def pod(name = nil, *requirements) end + def target(name, options = nil) end + def platform(name, target = nil) end + def inhibit_all_warnings! end + def use_frameworks!(flag = true) end + def source(source) end + ... + end + end +end +``` + +这里定义了很多 Podfile 中使用的方法,当使用 `eval` 执行文件中的代码时,就会执行这个模块里的方法,在这里简单看一下其中几个方法的实现,比如说 `source` 方法: + +```ruby +def source(source) + hash_sources = get_hash_value('sources') || [] + hash_sources << source + set_hash_value('sources', hash_sources.uniq) +end +``` + +该方法会将新的 `source` 加入已有的源数组中,然后更新原有的 `sources` 对应的值。 + +稍微复杂一些的是 `target` 方法: + +```ruby +def target(name, options = nil) + if options + raise Informative, "Unsupported options `#{options}` for " \ + "target `#{name}`." + end + + parent = current_target_definition + definition = TargetDefinition.new(name, parent) + self.current_target_definition = definition + yield if block_given? +ensure + self.current_target_definition = parent +end +``` + +这个方法会创建一个 `TargetDefinition` 类的实例,然后将当前环境系的 `target_definition` 设置成这个刚刚创建的实例。这样,之后使用 `pod` 定义的依赖都会填充到当前的 `TargetDefinition` 中: + +```ruby +def pod(name = nil, *requirements) + unless name + raise StandardError, 'A dependency requires a name.' + end + + current_target_definition.store_pod(name, *requirements) +end +``` + +当 `pod` 方法被调用时,会执行 `store_pod` 将依赖存储到当前 `target` 中的 `dependencies` 数组中: + +```ruby +def store_pod(name, *requirements) + return if parse_subspecs(name, requirements) + parse_inhibit_warnings(name, requirements) + parse_configuration_whitelist(name, requirements) + + if requirements && !requirements.empty? + pod = { name => requirements } + else + pod = name + end + + get_hash_value('dependencies', []) << pod + nil +end +``` + +总结一下,CocoaPods 对 Podfile 的解析与我们在前面做的手动解析 Podfile 的原理差不多,构建一个包含一些方法的上下文,然后直接执行 `eval` 方法将文件的内容当做代码来执行,这样只要 Podfile 中的数据是符合规范的,那么解析 Podfile 就是非常简单容易的。 + +### 安装依赖的过程 + +Podfile 被解析后的内容会被转化成一个 `Podfile` 类的实例,而 `Installer` 的实例方法 `install!` 就会使用这些信息安装当前工程的依赖,而整个安装依赖的过程大约有四个部分: + ++ 解析 Podfile 中的依赖 ++ 下载依赖 ++ 创建 `Pods.xcodeproj` 工程 ++ 集成 workspace + +```ruby +def install! + resolve_dependencies + download_dependencies + generate_pods_project + integrate_user_project +end +``` + +在上面的 `install` 方法调用的 `resolve_dependencies` 会创建一个 `Analyzer` 类的实例,在这个方法中,你会看到一些非常熟悉的字符串: + +```ruby +def resolve_dependencies + analyzer = create_analyzer + + plugin_sources = run_source_provider_hooks + analyzer.sources.insert(0, *plugin_sources) + + UI.section 'Updating local specs repositories' do + analyzer.update_repositories + end if repo_update? + + UI.section 'Analyzing dependencies' do + analyze(analyzer) + validate_build_configurations + clean_sandbox + end +end +``` + +在使用 CocoaPods 中经常出现的 `Updating local specs repositories` 以及 `Analyzing dependencies` 就是从这里输出到终端的,该方法不仅负责对本地所有 PodSpec 文件的更新,还会对当前 `Podfile` 中的依赖进行分析: + +```ruby +def analyze(analyzer = create_analyzer) + analyzer.update = update + @analysis_result = analyzer.analyze + @aggregate_targets = analyzer.result.targets +end +``` + +`analyzer.analyze` 方法最终会调用 `Resolver` 的实例方法 `resolve`: + +```ruby +def resolve + dependencies = podfile.target_definition_list.flat_map do |target| + target.dependencies.each do |dep| + @platforms_by_dependency[dep].push(target.platform).uniq! if target.platform + end + end + @activated = Molinillo::Resolver.new(self, self).resolve(dependencies, locked_dependencies) + specs_by_target +rescue Molinillo::ResolverError => e + handle_resolver_error(e) +end +``` + +这里的 `Molinillo::Resolver` 就是用于解决依赖关系的类。 + +#### 解决依赖关系(Resolve Dependencies) + +CocoaPods 为了解决 Podfile 中声明的依赖关系,使用了一个叫做 [Milinillo](https://github.com/CocoaPods/Molinillo/blob/master/ARCHITECTURE.md) 的依赖关系解决算法;但是,笔者在 Google 上并没有找到与这个算法相关的其他信息,推测是 CocoaPods 为了解决 iOS 中的依赖关系创造的算法。 + +Milinillo 算法的核心是 [回溯(Backtracking)](https://en.wikipedia.org/wiki/Backtracking) 以及 [向前检查(forward check)](https://en.wikipedia.org/wiki/Look-ahead_(backtracking)),整个过程会追踪栈中的两个状态(依赖和可能性)。 + +在这里并不想陷入对这个算法执行过程的分析之中,如果有兴趣可以看一下仓库中的 [ARCHITECTURE.md](https://github.com/CocoaPods/Molinillo/blob/master/ARCHITECTURE.md) 文件,其中比较详细的解释了 Milinillo 算法的工作原理,并对其功能执行过程有一个比较详细的介绍。 + +`Molinillo::Resolver` 方法会返回一个依赖图,其内容大概是这样的: + +```ruby +Molinillo::DependencyGraph:[ + Molinillo::DependencyGraph::Vertex:AFNetworking(#<Pod::Specification name="AFNetworking">), + Molinillo::DependencyGraph::Vertex:SDWebImage(#<Pod::Specification name="SDWebImage">), + Molinillo::DependencyGraph::Vertex:Masonry(#<Pod::Specification name="Masonry">), + Molinillo::DependencyGraph::Vertex:Typeset(#<Pod::Specification name="Typeset">), + Molinillo::DependencyGraph::Vertex:CCTabBarController(#<Pod::Specification name="CCTabBarController">), + Molinillo::DependencyGraph::Vertex:BlocksKit(#<Pod::Specification name="BlocksKit">), + Molinillo::DependencyGraph::Vertex:Mantle(#<Pod::Specification name="Mantle">), + ... +] +``` + +这个依赖图是由一个结点数组组成的,在 CocoaPods 拿到了这个依赖图之后,会在 `specs_by_target` 中按照 `Target` 将所有的 `Specification` 分组: + +```ruby +{ + #<Pod::Podfile::TargetDefinition label=Pods>=>[], + #<Pod::Podfile::TargetDefinition label=Pods-Demo>=>[ + #<Pod::Specification name="AFNetworking">, + #<Pod::Specification name="AFNetworking/NSURLSession">, + #<Pod::Specification name="AFNetworking/Reachability">, + #<Pod::Specification name="AFNetworking/Security">, + #<Pod::Specification name="AFNetworking/Serialization">, + #<Pod::Specification name="AFNetworking/UIKit">, + #<Pod::Specification name="BlocksKit/Core">, + #<Pod::Specification name="BlocksKit/DynamicDelegate">, + #<Pod::Specification name="BlocksKit/MessageUI">, + #<Pod::Specification name="BlocksKit/UIKit">, + #<Pod::Specification name="CCTabBarController">, + #<Pod::Specification name="CategoryCluster">, + ... + ] +} +``` + +而这些 `Specification` 就包含了当前工程依赖的所有第三方框架,其中包含了名字、版本、源等信息,用于依赖的下载。 + +#### 下载依赖 + +在依赖关系解决返回了一系列 `Specification` 对象之后,就到了 Pod install 的第二部分,下载依赖: + +```ruby +def install_pod_sources + @installed_specs = [] + pods_to_install = sandbox_state.added | sandbox_state.changed + title_options = { :verbose_prefix => '-> '.green } + root_specs.sort_by(&:name).each do |spec| + if pods_to_install.include?(spec.name) + if sandbox_state.changed.include?(spec.name) && sandbox.manifest + previous = sandbox.manifest.version(spec.name) + title = "Installing #{spec.name} #{spec.version} (was #{previous})" + else + title = "Installing #{spec}" + end + UI.titled_section(title.green, title_options) do + install_source_of_pod(spec.name) + end + else + UI.titled_section("Using #{spec}", title_options) do + create_pod_installer(spec.name) + end + end + end +end +``` + +在这个方法中你会看到更多熟悉的提示,CocoaPods 会使用沙盒(sandbox)存储已有依赖的数据,在更新现有的依赖时,会根据依赖的不同状态显示出不同的提示信息: + +```ruby +-> Using AFNetworking (3.1.0) + +-> Using AKPickerView (0.2.7) + +-> Using BlocksKit (2.2.5) was (2.2.4) + +-> Installing MBProgressHUD (1.0.0) +... +``` + +虽然这里的提示会有三种,但是 CocoaPods 只会根据不同的状态分别调用两种方法: + ++ `install_source_of_pod` ++ `create_pod_installer` + +`create_pod_installer` 方法只会创建一个 `PodSourceInstaller` 的实例,然后加入 `pod_installers` 数组中,因为依赖的版本没有改变,所以不需要重新下载,而另一个方法的 `install_source_of_pod` 的调用栈非常庞大: + +```ruby +installer.install_source_of_pod +|-- create_pod_installer +| `-- PodSourceInstaller.new +`-- podSourceInstaller.install! + `-- download_source + `-- Downloader.download + `-- Downloader.download_request + `-- Downloader.download_source + |-- Downloader.for_target + | |-- Downloader.class_for_options + | `-- Git/HTTP/Mercurial/Subversion.new + |-- Git/HTTP/Mercurial/Subversion.download + `-- Git/HTTP/Mercurial/Subversion.download! + `-- Git.clone +``` + +在调用栈的末端 `Downloader.download_source` 中执行了另一个 CocoaPods 组件 **CocoaPods-Download** 中的方法: + +```ruby +def self.download_source(target, params) + FileUtils.rm_rf(target) + downloader = Downloader.for_target(target, params) + downloader.download + target.mkpath + + if downloader.options_specific? + params + else + downloader.checkout_options + end +end +``` + +方法中调用的 `for_target` 根据不同的源会创建一个下载器,因为依赖可能通过不同的协议或者方式进行下载,比如说 Git/HTTP/SVN 等等,组件 CocoaPods-Downloader 就会根据 Podfile 中依赖的参数选项使用不同的方法下载依赖。 + +大部分的依赖都会被下载到 `~/Library/Caches/CocoaPods/Pods/Release/` 这个文件夹中,然后从这个这里复制到项目工程目录下的 `./Pods` 中,这也就完成了整个 CocoaPods 的下载流程。 + +#### 生成 Pods.xcodeproj + +CocoaPods 通过组件 CocoaPods-Downloader 已经成功将所有的依赖下载到了当前工程中,这里会将所有的依赖打包到 `Pods.xcodeproj` 中: + +```ruby +def generate_pods_project(generator = create_generator) + UI.section 'Generating Pods project' do + generator.generate! + @pods_project = generator.project + run_podfile_post_install_hooks + generator.write + generator.share_development_pod_schemes + write_lockfiles + end +end +``` + +`generate_pods_project` 中会执行 `PodsProjectGenerator` 的实例方法 `generate!`: + +```ruby +def generate! + prepare + install_file_references + install_libraries + set_target_dependencies +end +``` + +这个方法做了几件小事: + ++ 生成 `Pods.xcodeproj` 工程 ++ 将依赖中的文件加入工程 ++ 将依赖中的 Library 加入工程 ++ 设置目标依赖(Target Dependencies) + +这几件事情都离不开 CocoaPods 的另外一个组件 Xcodeproj,这是一个可以操作一个 Xcode 工程中的 Group 以及文件的组件,我们都知道对 Xcode 工程的修改大多数情况下都是对一个名叫 `project.pbxproj` 的文件进行修改,而 Xcodeproj 这个组件就是 CocoaPods 团队开发的用于操作这个文件的第三方库。 + +#### 生成 workspace + +最后的这一部分与生成 `Pods.xcodeproj` 的过程有一些相似,这里使用的类是 `UserProjectIntegrator`,调用方法 `integrate!` 时,就会开始集成工程所需要的 Target: + +```ruby +def integrate! + create_workspace + integrate_user_targets + warn_about_xcconfig_overrides + save_projects +end +``` + +对于这一部分的代码,也不是很想展开来细谈,简单介绍一下这里的代码都做了什么,首先会通过 `Xcodeproj::Workspace` 创建一个 workspace,之后会获取所有要集成的 Target 实例,调用它们的 `integrate!` 方法: + +```ruby +def integrate! + UI.section(integration_message) do + XCConfigIntegrator.integrate(target, native_targets) + + add_pods_library + add_embed_frameworks_script_phase + remove_embed_frameworks_script_phase_from_embedded_targets + add_copy_resources_script_phase + add_check_manifest_lock_script_phase + end +end +``` + +方法将每一个 Target 加入到了工程,使用 Xcodeproj 修改 `Copy Resource Script Phrase` 等设置,保存 `project.pbxproj`,整个 Pod install 的过程就结束了。 + +## 总结 + +最后想说的是 pod install 和 pod update 区别还是比较大的,每次在执行 pod install 或者 update 时最后都会生成或者修改 `Podfile.lock` 文件,其中前者并不会修改 `Podfile.lock` 中**显示指定**的版本,而后者会会无视该文件的内容,尝试将所有的 pod 更新到最新版。 + +CocoaPods 工程的代码虽然非常多,不过代码的逻辑非常清晰,整个管理并下载依赖的过程非常符合直觉以及逻辑。 + +## 其它 + +> Github Repo:[iOS-Source-Code-Analyze](https://github.com/draveness/iOS-Source-Code-Analyze) +> +> Follow: [Draveness · GitHub](https://github.com/Draveness) +> +> Source: http://draveness.me/cocoapods + + diff --git a/contents/CocoaPods/images/cocoapods-image.jpg b/contents/CocoaPods/images/cocoapods-image.jpg new file mode 100644 index 0000000..df4a1af Binary files /dev/null and b/contents/CocoaPods/images/cocoapods-image.jpg differ diff --git a/contents/CocoaPods/images/cocoapods.png b/contents/CocoaPods/images/cocoapods.png new file mode 100644 index 0000000..0b69f9b Binary files /dev/null and b/contents/CocoaPods/images/cocoapods.png differ diff --git a/contents/CocoaPods/images/compiler.png b/contents/CocoaPods/images/compiler.png new file mode 100644 index 0000000..e5c9d00 Binary files /dev/null and b/contents/CocoaPods/images/compiler.png differ diff --git a/contents/CocoaPods/images/css-sass.jpg b/contents/CocoaPods/images/css-sass.jpg new file mode 100644 index 0000000..f88d21e Binary files /dev/null and b/contents/CocoaPods/images/css-sass.jpg differ diff --git a/contents/CocoaPods/images/dom-tree.png b/contents/CocoaPods/images/dom-tree.png new file mode 100644 index 0000000..c8d219a Binary files /dev/null and b/contents/CocoaPods/images/dom-tree.png differ diff --git a/contents/CocoaPods/images/magic.jpg b/contents/CocoaPods/images/magic.jpg new file mode 100644 index 0000000..8401db4 Binary files /dev/null and b/contents/CocoaPods/images/magic.jpg differ diff --git a/contents/CocoaPods/images/rails.jpeg b/contents/CocoaPods/images/rails.jpeg new file mode 100644 index 0000000..45387fa Binary files /dev/null and b/contents/CocoaPods/images/rails.jpeg differ diff --git a/contents/CocoaPods/images/regex.jpg b/contents/CocoaPods/images/regex.jpg new file mode 100644 index 0000000..f35d410 Binary files /dev/null and b/contents/CocoaPods/images/regex.jpg differ diff --git a/contents/CocoaPods/images/silver-bullet.jpg b/contents/CocoaPods/images/silver-bullet.jpg new file mode 100644 index 0000000..3632da2 Binary files /dev/null and b/contents/CocoaPods/images/silver-bullet.jpg differ diff --git "a/contents/CocoaPods/\350\260\210\350\260\210 DSL \344\273\245\345\217\212 DSL \347\232\204\345\272\224\347\224\250\357\274\210\344\273\245 CocoaPods \344\270\272\344\276\213\357\274\211.md" "b/contents/CocoaPods/\350\260\210\350\260\210 DSL \344\273\245\345\217\212 DSL \347\232\204\345\272\224\347\224\250\357\274\210\344\273\245 CocoaPods \344\270\272\344\276\213\357\274\211.md" new file mode 100644 index 0000000..158dca5 --- /dev/null +++ "b/contents/CocoaPods/\350\260\210\350\260\210 DSL \344\273\245\345\217\212 DSL \347\232\204\345\272\224\347\224\250\357\274\210\344\273\245 CocoaPods \344\270\272\344\276\213\357\274\211.md" @@ -0,0 +1,435 @@ +![](images/magic.jpg) + +# 谈谈 DSL 以及 DSL 的应用(以 CocoaPods 为例) + +> 因为 DSL 以及 DSL 的界定本身就是一个比较模糊的概念,所以难免有与他人观点意见相左的地方,如果有不同的意见,我们可以具体讨论。 + +最近在公司做了一次有关 DSL 在 iOS 开发中的应用的分享,这篇文章会简单介绍这次分享的内容。 + +这次文章的题目虽然是谈谈 DSL 以及 DSL 的应用,不过文章中主要侧重点仍然是 DSL,会简单介绍 DSL 在 iOS 开发中(CocoaPods)是如何应用的。 + +## 没有银弹? + +1987 年,IBM 大型电脑之父 Fred Brooks 发表了一篇关于软件工程中的论文 [No Silver Bullet—Essence and Accidents of Software Engineering](No Silver Bullet—Essence and Accidents of Software Engineering) 文中主要围绕这么一个观点:没有任何一种技术或者方法能使软件工程的生产力在十年之内提高十倍。 + +> There is no single development, in either technology or management technique, which by itself promises even one order-of-magnitude improvement within a decade in productivity, in reliability, in simplicity. + +时至今日,我们暂且不谈银弹在软件工程中是否存在(~~这句话在老板或者项目经理要求加快项目进度时,还是十分好用的~~),作为一个开发者也不是很关心这种抽象的理论,我们更关心的是开发效率能否有实质的提升。 + +![silver-bullet](images/silver-bullet.jpg) + +而今天要介绍的 DSL 就可以真正的提升生产力,减少不必要的工作,在一些领域帮助我们更快的实现需求。 + +## DSL 是什么? + +笔者是在两年以前,在大一的一次分享上听到 DSL 这个词的,但是当时并没有对这个名词有多深的理解与认识,听过也就忘记了,但是最近做的一些开源项目让我重新想起了 DSL,也是这次分享题目的由来。 + +DSL 其实是 Domain Specific Language 的缩写,中文翻译为*领域特定语言*(下简称 DSL);而与 DSL 相对的就是 GPL,这里的 GPL 并不是我们知道的开源许可证,而是 General Purpose Language 的简称,即*通用编程语言*,也就是我们非常熟悉的 Objective-C、Java、Python 以及 C 语言等等。 + +[Wikipedia](https://en.wikipedia.org/wiki/Domain-specific_language) 对于 DSL 的定义还是比较简单的: + +> A specialized computer language designed for a specific task. +> +> 为了解决某一类任务而专门设计的计算机语言。 + +与 GPL 相对,DSL 与传统意义上的通用编程语言 C、Python 以及 Haskell 完全不同。通用的计算机编程语言是可以用来编写任意计算机程序的,并且能表达任何的**可被计算**的逻辑,同时也是 [图灵完备](https://en.wikipedia.org/wiki/Turing_completeness) 的。 + +> 这一小节中的 DSL 指外部 DSL,下一节中会介绍 [内部 DSL/嵌入式 DSL](#embedded-dsl嵌入式-dsl) + +但是在里所说的 DSL 并不是图灵完备的,它们的**表达能力有限**,只是在特定领域解决特定任务的。 + +> A computer programming language of limited expressiveness focused on a particular domain. + +另一个世界级软件开发大师 Martin Fowler 对于领域特定语言的定义在笔者看来就更加具体了,**DSL 通过在表达能力上做的妥协换取在某一领域内的高效**。 + +而有限的表达能力就成为了 GPL 和 DSL 之间的一条界限。 + +### 几个栗子 + +最常见的 DSL 包括 Regex 以及 HTML & CSS,在这里会对这几个例子进行简单介绍 + ++ Regex + + 正则表达式仅仅指定了字符串的 pattern,其引擎就会根据 pattern 判断当前字符串跟正则表达式是否匹配。 + ![regex](images/regex.jpg) ++ SQL + + SQL 语句在使用时也并没有真正的执行,我们输入的 SQL 语句最终还要交给数据库来进行处理,数据库会从 SQL 语句中**读取**有用的信息,然后从数据库中返回使用者期望的结果。 ++ HTML & CSS + + HTML 和 CSS 只是对 Web 界面的结构语义和样式进行描述,虽然它们在构建网站时非常重要,但是它们并非是一种编程语言,正相反,我们可以认为 HTML 和 CSS 是在 Web 中的领域特定语言。 + +### Features + +上面的几个🌰明显的缩小了通用编程语言的概念,但是它们确实在自己领域表现地非常出色,因为这些 DSL 就是根据某一个特定领域的特点塑造的;而通用编程语言相比领域特定语言,在设计时是为了解决更加抽象的问题,而关注点并不只是在某一个领域。 + +上面的几个例子有着一些共同的特点: + ++ 没有计算和执行的概念; ++ 其本身并不需要直接表示计算; ++ 使用时只需要声明规则、事实以及某些元素之间的层级和关系; + +虽然了解了 DSL 以及 DSL 的一些特性,但是,到目前为止,我们对于如何构建一个 DSL 仍然不是很清楚。 + +### 构建 DSL + +DSL 的构建与编程语言其实比较类似,想想我们在重新实现编程语言时,需要做那些事情;实现编程语言的过程可以简化为定义语法与语义,然后实现编译器或者解释器的过程,而 DSL 的实现与它也非常类似,我们也需要对 DSL 进行语法与语义上的设计。 + +![compile](images/compiler.png) + +总结下来,实现 DSL 总共有这么两个需要完成的工作: + +1. 设计语法和语义,定义 DSL 中的元素是什么样的,元素代表什么意思 +2. 实现 parser,对 DSL 解析,最终通过解释器来执行 + +以 HTML 为例,HTML 中所有的元素都是包含在尖括号 `<>` 中的,尖括号中不同的元素代表了不同的标签,而这些标签会被浏览器**解析**成 DOM 树,再经过一系列的过程调用 Native 的图形 API 进行绘制。 + +![dom-tree](images/dom-tree.png) + +再比如,我们使用下面这种方式对一个模型进行定义,实现一个 ORM 领域的 DSL: + +```ruby +define :article do + attr :name + attr :content + attr :upvotes, :int + + has_many :comments +end +``` + +在上面的 DSL 中,使用 `define` 来定义一个新的模型,使用 `attr` 来为模型添加属性,使用 `has_many` 建立数据模型中的一对多关系;我们可以使用 DSL 对这段“字符串”进行解析,然后交给代码生成器来生成代码。 + +```swift +public struct Article { + public var title: String + public var content: String + public var createdAt: Date + + public init(title: String, content: String, createdAt: Date) + + static public func new(title: String, content: String, createdAt: Date) -> Article + static public func create(title: String, content: String, createdAt: Date) -> Article? + ... +} +``` + +这里创建的 DSL 中的元素数量非常少,只有 `define` `attr` 以及 `has_many` 等几个关键字,但是通过这几个关键字就可以完成在模型层需要表达的绝大部分语义。 + +### 设计原则和妥协 + +DSL 最大的设计原则就是**简单**,通过简化语言中的元素,降低使用者的负担;无论是 Regex、SQL 还是 HTML 以及 CSS,其说明文档往往只有几页,非常易于学习和掌握。但是,由此带来的问题就是,DSL 中缺乏抽象的概念,比如:模块化、变量以及方法等。 + +> 抽象的概念并不是某个领域所关注的问题,就像 Regex 并不需要有模块、变量以及方法等概念。 + +由于抽象能力的缺乏,在我们的项目规模变得越来越大时,DSL 往往满足不了开发者的需求;我们仍然需要编程语言中的模块化等概念对 DSL 进行补充,以此解决 DSL 并不是真正编程语言的问题。 + +![css-sass](images/css-sass.jpg) + +在当今的 Web 前端项目中,我们在开发大规模项目时往往不会直接手写 CSS 文件,而是会使用 Sass 或者 Less 为 CSS 带来更强大的抽象能力,比如嵌套规则,变量,混合以及继承等特性。 + +```css +nav { + ul { + margin: 0; + padding: 0; + list-style: none; + } + + li { display: inline-block; } + + a { + display: block; + padding: 6px 12px; + text-decoration: none; + } +} +``` + +也就是说,在使用 DSL 的项目规模逐渐变大时,开发者会通过增加抽象能力的方式,对已有的 DSL 进行拓展;但是这种扩展往往需要重新实现通用编程语言中的特性,所以一般情况下都是比较复杂的。 + +## Embedded DSL(嵌入式 DSL) + +那么,是否有一种其它的方法为 DSL 快速添加抽象能力呢?而这也就是这一小节的主题,嵌入式 DSL。 + +在上一节讲到的 DSL 其实可以被称为外部 DSL;而这里即将谈到的嵌入式 DSL 也有一个别名,内部 DSL。 + +这两者最大的区别就是,内部 DSL 的实现往往是嵌入一些编程语言的,比如 iOS 的依赖管理组件 CocoaPods 和 Android 的主流编译工具 Gradle,前者的实现是基于 Ruby 语言的一些特性,而后者基于 Groovy。 + +![cocoapods](images/cocoapods.png) + +CocoaPods 以及其它的嵌入式 DSL 使用了宿主语言(host language)的抽象能力,并且省去了实现复杂语法分析器(Parser)的过程,并不需要重新实现模块、变量等特性。 + +嵌入式 DSL 的产生其实模糊了框架和 DSL 的边界,不过这两者看起来也没有什么比较明显的区别;不过,DSL 一般会使用宿主语言的特性进行创造,在设计 DSL 时,也不会考虑宿主语言中有哪些 API 以及方法,而框架一般都是对语言中的 API 进行组合和再包装。 + +> 我们没有必要争论哪些是框架,哪些是 DSL,因为这些争论并没有什么意义。 + +### Rails 和 Embedded DSL + +最出名也最成功的嵌入式 DSL 应该就是 Ruby on Rails 了,虽然对于 Rails 是否是 DSL 有争议,不过 Rails 为 Web 应用的创建提供大量的内置的支撑,使我们在开发 Web 应用时变得非常容易。 + +![rails](images/rails.jpeg) + +## Ruby、 DSL 和 iOS + +> 为了保证这篇文章的完整性,这一小节中有的一些内容都出自上一篇文章 [CocoaPods 都做了什么?](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/CocoaPods/CocoaPods%20都做了什么?.md)。 + +笔者同时作为 iOS 和 Rails 开发者接触了非常多的 DSL,而在 iOS 开发中最常见的 DSL 就是 CocoaPods 了,而这里我们以 CocoaPods 为例,介绍如何使用 Ruby 创造一个嵌入式 DSL。 + +### Why Ruby? + +看到这里有人可能会问了,为什么使用 Ruby 创造嵌入式 DSL,而不是使用 C、Java、Python 等等语言呢,这里大概有四个原因: + ++ 一切皆对象的特性减少了语言中的元素,不存在基本类型、操作符; ++ 向 Ruby 方法中传入代码块非常方便; ++ 作为解释执行的语言,eval 模糊了数据和代码的边界; ++ 不对代码的格式进行约束,同时一些约定减少了代码中的噪音。 + +#### 一切皆对象 + +在许多语言,比如 Java 中,数字与其他的基本类型都不是对象,而在 Ruby 中所有的元素,包括基本类型都是对象,同时也不存在运算符的概念,所谓的 `1 + 1`,其实只是 `1.+(1)` 的语法糖而已。 + +得益于一切皆对象的概念,在 Ruby 中,你可以向任意的对象发送 `methods` 消息,在运行时自省,所以笔者在每次忘记方法时,都会直接用 `methods` 来“查阅文档”: + +```ruby +2.3.1 :003 > 1.methods + => [:%, :&, :*, :+, :-, :/, :<, :>, :^, :|, :~, :-@, :**, :<=>, :<<, :>>, :<=, :>=, :==, :===, :[], :inspect, :size, :succ, :to_s, :to_f, :div, :divmod, :fdiv, :modulo, ...] +``` + +比如在这里向对象 `1` 调用 `methods` 就会返回它能响应的所有方法。 + +一切皆对象不仅减少了语言中类型的数量,消灭了基本数据类型与对象之间的边界;这一概念同时也简化了组成语言的元素,这样 Ruby 中只有对象和方法,这两个概念,极大降低了这门语言的复杂度: + ++ 使用对象存储状态 ++ 对象之间通过方法通信 + +#### block + +Ruby 对函数式编程范式的支持是通过 block,这里的 block 和 Objective-C 中的 block 有些不同。 + +首先 Ruby 中的 block 也是一种对象,即 `Proc` 类的实例,也就是所有的 block 都是 first-class 的,可以作为参数传递,返回。 + +下面的代码演示了两种向 Ruby 方法中传入代码块的方式: + +```ruby +def twice(&proc) + 2.times { proc.call() } if proc +end + +def twice + 2.times { yield } if block_given? +end +``` + +`yield` 会调用外部传入的 block,`block_given?` 用于判断当前方法是否传入了 `block`。 + +```ruby +twice do + puts "Hello" +end + +twice { puts "hello" } +``` + +向 `twice` 方法传入 block 也非常简单,使用 `do`、`end` 或者 `{`、`}` 就可以向任何的 Ruby 方法中传入代码块。 + +#### eval + +早在几十年前的 Lisp 语言就有了 `eval` 这个方法,这个方法会将字符串当做代码来执行,也就是说 `eval` 模糊了代码与数据之间的边界。 + +```ruby +> eval "1 + 2 * 3" + => 7 +``` + +有了 `eval` 方法,我们就获得了更加强大的动态能力,在运行时,使用字符串来改变控制流程,执行代码并可以直接利用当前语言的解释器;而不需要去手动解析字符串然后执行代码。 + +#### 格式和约定 + +编写 Ruby 脚本时并不需要像 Python 一样对代码的格式有着严格的规定,没有对空行、Tab 的要求,完全可以想怎么写就怎么写,这样极大的增加了 DSL 设计的可能性。 + +同时,在一般情况下,Ruby 在方法调用时并不需要添加括号: + +```ruby +puts "Wello World!" +puts("Hello World!") +``` + +这样减少了 DSL 中的噪音,能够帮助我们更加关心语法以及语义上的设计,降低了使用者出错的可能性。 + +最后,Ruby 中存在一种特殊的数据格式 `Symbol`: + +```ruby +> :symbol.to_s + => "symbol" +> "symbol".to_sym + => :symbol +``` + +Symbol 可以通过 Ruby 中内置的方法与字符串之间无缝转换。那么作为一种字符串的替代品,它的使用也能够降低使用者出错的成本并提升使用体验,我们并不需要去写两边加上引号的字符串,只需要以 `:` 开头就能创建一个 Symbol 对象。 + +### Podfile 是什么 + +对 Ruby 有了一些了解之后,我们就可以再看一下使用 CocoaPods 的工程中的 Podfile 到底是什么了: + +```ruby +source 'https://github.com/CocoaPods/Specs.git' + +target 'Demo' do + pod 'Mantle', '~> 1.5.1' + ... +end +``` + +> 如果不了解 iOS 开发后者没有使用过 CocoaPods,笔者在这里简单介绍一下这个文件中的一些信息。 +> +> `source` 可以看作是存储依赖元信息(包括依赖的对应的 GitHub 地址)的源地址; +> +> `target` 表示需要添加依赖的工程的名字; +> +> `pod` 表示依赖,`Mantle` 为依赖的框架,后面是版本号。 + +上面是一个使用 Podfile 定义依赖的一个例子,不过 Podfile 对约束的描述其实是这样的: + +```ruby +source('https://github.com/CocoaPods/Specs.git') + +target('Demo') do + pod('Mantle', '~> 1.5.1') + ... +end +``` + +Podfile 中对于约束的描述,其实都可以看作是代码的简写,在解析时会当做 Ruby 代码来执行。 + +### 简单搞个 Embedded DSL + +使用 Ruby 实现嵌入式 DSL 一般需要三个步骤,这里以 CocoaPods 为例进行简单介绍: + ++ 创建一个 Podfile 中“代码”执行的上下文,也就是一些方法; ++ 读取 Podfile 中的内容到脚本中; ++ 使用 `eval` 在上下文中执行 Podfile 中的“代码”; + +#### 原理 + +CocoaPods 对于 DSL 的实现基本上就是我们创建一个 DSL 的过程,定义一系列必要的方法,比如 `source`、`pod` 等等,创造一个执行的上下文;然后去读存储 DSL 的文件,并且使用 `eval` 执行。 + +**信息的传递一般都是通过参数**来进行的,比如: + +```ruby +source 'https://github.com/CocoaPods/Specs.git' +``` + +`source` 方法的参数就是依赖元信息 `Specs` 的 Git 地址,在 `eval` 执行时就会被读取到 CocoaPods 中,然后进行分析。 + +#### 实现 + +下面是一个非常常见的 Podfile 内容: + +```ruby +source 'http://source.git/' +platform :ios, '8.0' + +target 'Demo' do + pod 'AFNetworking' + pod 'SDWebImage' + pod 'Masonry' + pod "Typeset" + pod 'BlocksKit' + pod 'Mantle' + pod 'IQKeyboardManager' + pod 'IQDropDownTextField' +end +``` + +因为这里的 `source`、`platform`、`target` 以及 `pod` 都是方法,所以在这里我们需要构建一个包含上述方法的上下文: + +```ruby +# eval_pod.rb +$hash_value = {} + +def source(url) +end + +def target(target) +end + +def platform(platform, version) +end + +def pod(pod) +end +``` + +使用一个全局变量 `hash_value` 存储 Podfile 中指定的依赖,并且构建了一个 Podfile 解析脚本的骨架;我们先不去完善这些方法的实现细节,先尝试一下读取 Podfile 中的内容并执行 `eval` 看看会不会有问题。 + +在 `eval_pod.rb` 文件的最下面加入这几行代码: + +```ruby +content = File.read './Podfile' +eval content +p $hash_value +``` + +这里读取了 Podfile 文件中的内容,并把其中的内容当做字符串执行,最后打印 `hash_value` 的值。 + +```shell +$ ruby eval_pod.rb +``` + +运行这段 Ruby 代码虽然并没有什么输出,但是并没有报出任何的错误,接下来我们就可以完善这些方法了: + +```ruby +def source(url) + $hash_value['source'] = url +end + +def target(target) + targets = $hash_value['targets'] + targets = [] if targets == nil + targets << target + $hash_value['targets'] = targets + yield if block_given? +end + +def platform(platform, version) +end + +def pod(pod) + pods = $hash_value['pods'] + pods = [] if pods == nil + pods << pod + $hash_value['pods'] = pods +end +``` + +在添加了这些方法的实现之后,再次运行脚本就会得到 Podfile 中的依赖信息了,不过这里的实现非常简单的,很多情况都没有处理: + +```shell +$ ruby eval_pod.rb +{"source"=>"http://source.git/", "targets"=>["Demo"], "pods"=>["AFNetworking", "SDWebImage", "Masonry", "Typeset", "BlocksKit", "Mantle", "IQKeyboardManager", "IQDropDownTextField"]} +``` + +不过使用 Ruby 构建一个嵌入式 DSL 的过程大概就是这样,使用语言内建的特性来进行创作,创造出一个在使用时看起来并不像代码的 DSL。 + +## 写在后面 + +在最后,笔者想说的是,当我们在某一个领域经常需要解决重复性问题时,可以考虑实现一个 DSL 专门用来解决这些类似的问题。 + +而使用嵌入式 DSL 来解决这些问题是一个非常好的办法,我们并不需要重新实现解释器,也可以利用宿主语言的抽象能力。 + +同时,在嵌入式 DSL 扩展了 DSL 的范畴之后,不要纠结于某些东西到底是框架还是领域特定语言,这些都不重要,重要的是,在遇到了某些问题时,我们能否跳出来,使用文中介绍的方法减轻我们的工作量。 + +## Reference + ++ [No Silver Bullet—Essence and Accidents of Software Engineering](No Silver Bullet—Essence and Accidents of Software Engineering) ++ [Domain-specific language](https://en.wikipedia.org/wiki/Domain-specific_language) ++ [DomainSpecificLanguage](http://martinfowler.com/bliki/DomainSpecificLanguage.html) ++ [How browsers work](http://taligarsiel.com/Projects/howbrowserswork1.htm) + +## 其它 + +> GitHub Repo:[iOS-Source-Code-Analyze](https://github.com/draveness/iOS-Source-Code-Analyze) +> +> Follow: [Draveness · GitHub](https://github.com/Draveness) +> +> Source: http://draveness.me/dsl + + diff --git "a/DKNightVersion/\346\210\220\347\206\237\347\232\204\345\244\234\351\227\264\346\250\241\345\274\217\350\247\243\345\206\263\346\226\271\346\241\210.md" "b/contents/DKNightVersion/\346\210\220\347\206\237\347\232\204\345\244\234\351\227\264\346\250\241\345\274\217\350\247\243\345\206\263\346\226\271\346\241\210.md" similarity index 100% rename from "DKNightVersion/\346\210\220\347\206\237\347\232\204\345\244\234\351\227\264\346\250\241\345\274\217\350\247\243\345\206\263\346\226\271\346\241\210.md" rename to "contents/DKNightVersion/\346\210\220\347\206\237\347\232\204\345\244\234\351\227\264\346\250\241\345\274\217\350\247\243\345\206\263\346\226\271\346\241\210.md" diff --git a/contents/Database/concurrency-control.md b/contents/Database/concurrency-control.md new file mode 100644 index 0000000..9ab9a6f --- /dev/null +++ b/contents/Database/concurrency-control.md @@ -0,0 +1,201 @@ +# 浅谈数据库并发控制 - 锁和 MVCC + +在学习几年编程之后,你会发现所有的问题都没有简单、快捷的解决方案,很多问题都需要权衡和妥协,而本文介绍的就是数据库在并发性能和可串行化之间做的权衡和妥协 - 并发控制机制。 + +![tradeoff-between-performance-and-serializability](images/concurrency-control/tradeoff-between-performance-and-serializability.png) + +如果数据库中的所有事务都是串行执行的,那么它非常容易成为整个应用的性能瓶颈,虽然说没法水平扩展的节点在最后都会成为瓶颈,但是串行执行事务的数据库会加速这一过程;而并发(Concurrency)使一切事情的发生都有了可能,它能够解决一定的性能问题,但是它会带来更多诡异的错误。 + +引入了并发事务之后,如果不对事务的执行进行控制就会出现各种各样的问题,你可能没有享受到并发带来的性能提升就已经被各种奇怪的问题折磨的欲仙欲死了。 + +## 概述 + +如何控制并发是数据库领域中非常重要的问题之一,不过到今天为止事务并发的控制已经有了很多成熟的解决方案,而这些方案的原理就是这篇文章想要介绍的内容,文章中会介绍最为常见的三种并发控制机制: + +![pessimistic-optimistic-multiversion-conccurency-control](images/concurrency-control/pessimistic-optimistic-multiversion-conccurency-control.png) + +分别是悲观并发控制、乐观并发控制和多版本并发控制,其中悲观并发控制其实是最常见的并发控制机制,也就是锁;而乐观并发控制其实也有另一个名字:乐观锁,乐观锁其实并不是一种真实存在的锁,我们会在文章后面的部分中具体介绍;最后就是多版本并发控制(MVCC)了,与前两者对立的命名不同,MVCC 可以与前两者中的任意一种机制结合使用,以提高数据库的读性能。 + +既然这篇文章介绍了不同的并发控制机制,那么一定会涉及到不同事务的并发,我们会通过示意图的方式分析各种机制是如何工作的。 + +## 悲观并发控制 + +控制不同的事务对同一份数据的获取是保证数据库的一致性的最根本方法,如果我们能够让事务在同一时间对同一资源有着独占的能力,那么就可以保证操作同一资源的不同事务不会相互影响。 + +![pessimistic-conccurency-control](images/concurrency-control/pessimistic-conccurency-control.png) + +最简单的、应用最广的方法就是使用锁来解决,当事务需要对资源进行操作时需要先获得资源对应的锁,保证其他事务不会访问该资源后,在对资源进行各种操作;在悲观并发控制中,数据库程序对于数据被修改持悲观的态度,在数据处理的过程中都会被锁定,以此来解决竞争的问题。 + +### 读写锁 + +为了最大化数据库事务的并发能力,数据库中的锁被设计为两种模式,分别是共享锁和互斥锁。当一个事务获得共享锁之后,它只可以进行读操作,所以共享锁也叫读锁;而当一个事务获得一行数据的互斥锁时,就可以对该行数据进行读和写操作,所以互斥锁也叫写锁。 + +![Shared-Exclusive-Lock](images/concurrency-control/Shared-Exclusive-Lock.png) + +共享锁和互斥锁除了限制事务能够执行的读写操作之外,它们之间还有『共享』和『互斥』的关系,也就是多个事务可以同时获得某一行数据的共享锁,但是互斥锁与共享锁和其他的互斥锁并不兼容,我们可以很自然地理解这么设计的原因:多个事务同时写入同一数据难免会发生各种诡异的问题。 + +![lock-and-wait](images/concurrency-control/lock-and-wait.png) + +如果当前事务没有办法获取该行数据对应的锁时就会陷入等待的状态,直到其他事务将当前数据对应的锁释放才可以获得锁并执行相应的操作。 + +### 两阶段锁协议 + +两阶段锁协议(2PL)是一种能够保证事务可串行化的协议,它将事务的获取锁和释放锁划分成了增长(Growing)和缩减(Shrinking)两个不同的阶段。 + +![growing-to-shrinking](images/concurrency-control/growing-to-shrinking.png) + +在增长阶段,一个事务可以获得锁但是不能释放锁;而在缩减阶段事务只可以释放锁,并不能获得新的锁,如果只看 2PL 的定义,那么到这里就已经介绍完了,但是它还有两个变种: + +1. **Strict 2PL**:事务持有的**互斥**锁必须在提交后再释放; +2. **Rigorous 2PL**:事务持有的**所有**锁必须在提交后释放; + +![two-phase-locking](images/concurrency-control/two-phase-locking.png) + +虽然锁的使用能够为我们解决不同事务之间由于并发执行造成的问题,但是两阶段锁的使用却引入了另一个严重的问题,死锁;不同的事务等待对方已经锁定的资源就会造成死锁,我们在这里举一个简单的例子: + +![deadlock](images/concurrency-control/deadlock.png) + +两个事务在刚开始时分别获取了 draven 和 beacon 资源上面的锁,然后再请求对方已经获得的锁时就会发生死锁,双方都没有办法等到锁的释放,如果没有死锁的处理机制就会无限等待下去,两个事务都没有办法完成。 + +### 死锁的处理 + +死锁在多线程编程中是经常遇到的事情,一旦涉及多个线程对资源进行争夺就需要考虑当前的几个线程或者事务是否会造成死锁;解决死锁大体来看有两种办法,一种是从源头杜绝死锁的产生和出现,另一种是允许系统进入死锁的状态,但是在系统出现死锁时能够及时发现并且进行恢复。 + +![deadlock-handling](images/concurrency-control/deadlock-handling.png) + +#### 预防死锁 + +有两种方式可以帮助我们预防死锁的出现,一种是保证事务之间的等待不会出现环,也就是事务之间的等待图应该是一张**有向无环图**,没有循环等待的情况或者保证一个事务中想要获得的所有资源都在事务开始时以原子的方式被锁定,所有的资源要么被锁定要么都不被锁定。 + +但是这种方式有两个问题,在事务一开始时很难判断哪些资源是需要锁定的,同时因为一些很晚才会用到的数据被提前锁定,数据的利用率与事务的并发率也非常的低。一种解决的办法就是按照一定的顺序为所有的数据行加锁,同时与 2PL 协议结合,在加锁阶段保证所有的数据行都是从小到大依次进行加锁的,不过这种方式依然需要事务提前知道将要加锁的数据集。 + +另一种预防死锁的方法就是使用抢占加事务回滚的方式预防死锁,当事务开始执行时会先获得一个时间戳,数据库程序会根据事务的时间戳决定事务应该等待还是回滚,在这时也有两种机制供我们选择,一种是 wait-die 机制: + +![deadlock-prevention-wait-die](images/concurrency-control/deadlock-prevention-wait-die.png) + +当执行事务的时间戳小于另一事务时,即事务 A 先于 B 开始,那么它就会等待另一个事务释放对应资源的锁,否则就会保持当前的时间戳并回滚。 + +另一种机制叫做 wound-wait,这是一种抢占的解决方案,它和 wait-die 机制的结果完全相反,当前事务如果先于另一事务执行并请求了另一事务的资源,那么另一事务会立刻回滚,将资源让给先执行的事务,否则就会等待其他事务释放资源: + +![deadlock-prevention-wound-wait](images/concurrency-control/deadlock-prevention-wound-wait.png) + +两种方法都会造成不必要的事务回滚,由此会带来一定的性能损失,更简单的解决死锁的方式就是使用超时时间,但是超时时间的设定是需要仔细考虑的,否则会造成耗时较长的事务无法正常执行,或者无法及时发现需要解决的死锁,所以它的使用还是有一定的局限性。 + +### 死锁检测和恢复 + +如果数据库程序无法通过协议从原理上保证死锁不会发生,那么就需要在死锁发生时及时检测到并从死锁状态恢复到正常状态保证数据库程序可以正常工作。在使用检测和恢复的方式解决死锁时,数据库程序需要维护数据和事务之间的引用信息,同时也需要提供一个用于判断当前数据库是否进入死锁状态的算法,最后需要在死锁发生时提供合适的策略及时恢复。 + +在上一节中我们其实提到死锁的检测可以通过一个有向的等待图来进行判断,如果一个事务依赖于另一个事务正在处理的数据,那么当前事务就会等待另一个事务的结束,这也就是整个等待图中的一条边: + +![deadlock-wait-for-graph](images/concurrency-control/deadlock-wait-for-graph.png) + +如上图所示,如果在这个有向图中出现了环,就说明当前数据库进入了死锁的状态 `TransB -> TransE -> TransF -> TransD -> TransB`,在这时就需要死锁恢复机制接入了。 + +如何从死锁中恢复其实非常简单,最常见的解决办法就是选择整个环中一个事务进行回滚,以打破整个等待图中的环,在整个恢复的过程中有三个事情需要考虑: + +![deadlock-recovery](images/concurrency-control/deadlock-recovery.png) + +每次出现死锁时其实都会有多个事务被波及,而选择其中哪一个任务进行回滚是必须要做的事情,在选择牺牲品(Victim)时的黄金原则就是**最小化代价**,所以我们需要综合考虑事务已经计算的时间、使用的数据行以及涉及的事务等因素;当我们选择了牺牲品之后就可以开始回滚了,回滚其实有两种选择一种是全部回滚,另一种是部分回滚,部分回滚会回滚到事务之前的一个检查点上,如果没有检查点那自然没有办法进行部分回滚。 + +> 在死锁恢复的过程中,其实还可能出现某些任务在多次死锁时都被选择成为牺牲品,一直都不会成功执行,造成饥饿(Starvation),我们需要保证事务会在有穷的时间内执行,所以要在选择牺牲品时将时间戳加入考虑的范围。 + +### 锁的粒度 + +到目前为止我们都没有对不同粒度的锁进行讨论,一直以来我们都讨论的都是数据行锁,但是在有些时候我们希望将多个节点看做一个数据单元,使用锁直接将这个数据单元、表甚至数据库锁定起来。这个目标的实现需要我们在数据库中定义不同粒度的锁: + +![granularity-hierarchy](images/concurrency-control/granularity-hierarchy.png) + +当我们拥有了不同粒度的锁之后,如果某个事务想要锁定整个数据库或者整张表时只需要简单的锁住对应的节点就会在当前节点加上显示(explicit)锁,在所有的子节点上加隐式(implicit)锁;虽然这种不同粒度的锁能够解决父节点被加锁时,子节点不能被加锁的问题,但是我们没有办法在子节点被加锁时,立刻确定父节点不能被加锁。 + +在这时我们就需要引入*意向锁*来解决这个问题了,当需要给子节点加锁时,先给所有的父节点加对应的意向锁,意向锁之间是完全不会互斥的,只是用来帮助父节点快速判断是否可以对该节点进行加锁: + +![lock-type-compatibility-matrix](images/concurrency-control/lock-type-compatibility-matrix.png) + +这里是一张引入了两种意向锁,*意向共享锁*和*意向互斥锁*之后所有的锁之间的兼容关系;到这里,我们通过不同粒度的锁和意向锁加快了数据库的吞吐量。 + +## 乐观并发控制 + +除了悲观并发控制机制 - 锁之外,我们其实还有其他的并发控制机制,*乐观并发控制*(Optimistic Concurrency Control)。乐观并发控制也叫乐观锁,但是它并不是真正的锁,很多人都会误以为乐观锁是一种真正的锁,然而它只是一种并发控制的思想。 + +![pessimistic-and-optimisti](images/concurrency-control/pessimistic-and-optimistic.png) + +在这一节中,我们将会先介绍*基于时间戳的并发控制机制*,然后在这个协议的基础上进行扩展,实现乐观的并发控制机制。 + +### 基于时间戳的协议 + +锁协议按照不同事务对同一数据项请求的时间依次执行,因为后面执行的事务想要获取的数据已将被前面的事务加锁,只能等待锁的释放,所以基于锁的协议执行事务的顺序与获得锁的顺序有关。在这里想要介绍的基于时间戳的协议能够在事务执行之前先决定事务的执行顺序。 + +每一个事务都会具有一个全局唯一的时间戳,它即可以使用系统的时钟时间,也可以使用计数器,只要能够保证所有的时间戳都是唯一并且是随时间递增的就可以。 + +![timestamp-ordering-protocol](images/concurrency-control/timestamp-ordering-protocol.png) + +基于时间戳的协议能够保证事务并行执行的顺序与事务按照时间戳串行执行的效果完全相同;每一个数据项都有两个时间戳,读时间戳和写时间戳,分别代表了当前成功执行对应操作的事务的时间戳。 + +该协议能够保证所有冲突的读写操作都能按照时间戳的大小串行执行,在执行对应的操作时不需要关注其他的事务只需要关心数据项对应时间戳的值就可以了: + +![timestamp-ordering-protocol-process](images/concurrency-control/timestamp-ordering-protocol-process.png) + +无论是读操作还是写操作都会从左到右依次比较读写时间戳的值,如果小于当前值就会直接被拒绝然后回滚,数据库系统会给回滚的事务添加一个新的时间戳并重新执行这个事务。 + +### 基于验证的协议 + +*乐观并发控制*其实本质上就是基于验证的协议,因为在多数的应用中只读的事务占了绝大多数,事务之间因为写操作造成冲突的可能非常小,也就是说大多数的事务在不需要并发控制机制也能运行的非常好,也可以保证数据库的一致性;而并发控制机制其实向整个数据库系统添加了很多的开销,我们其实可以通过别的策略降低这部分开销。 + +而验证协议就是我们找到的解决办法,它根据事务的只读或者更新将所有事务的执行分为两到三个阶段: + +![validation-based-protoco](images/concurrency-control/validation-based-protocol.png) + +在读阶段,数据库会执行事务中的**全部读操作和写操作**,并将所有写后的值存入临时变量中,并不会真正更新数据库中的内容;在这时候会进入下一个阶段,数据库程序会检查当前的改动是否合法,也就是是否有其他事务在 RAED PHASE 期间更新了数据,如果通过测试那么直接就进入 WRITE PHASE 将所有存在临时变量中的改动全部写入数据库,没有通过测试的事务会直接被终止。 + +为了保证乐观并发控制能够正常运行,我们需要知道一个事务不同阶段的发生时间,包括事务开始时间、验证阶段的开始时间以及写阶段的结束时间;通过这三个时间戳,我们可以保证任意冲突的事务不会同时写入数据库,一旦由一个事务完成了验证阶段就会立即写入,其他读取了相同数据的事务就会回滚重新执行。 + +作为乐观的并发控制机制,它会假定所有的事务在最终都会通过验证阶段并且执行成功,而锁机制和基于时间戳排序的协议是悲观的,因为它们会在发生冲突时强制事务进行等待或者回滚,哪怕有不需要锁也能够保证事务之间不会冲突的可能。 + +## 多版本并发控制 + +到目前为止我们介绍的并发控制机制其实都是通过延迟或者终止相应的事务来解决事务之间的竞争条件(Race condition)来保证事务的可串行化;虽然前面的两种并发控制机制确实能够从根本上解决并发事务的可串行化的问题,但是在实际环境中数据库的事务大都是只读的,读请求是写请求的很多倍,如果写请求和读请求之前没有并发控制机制,那么最坏的情况也是读请求读到了已经写入的数据,这对很多应用完全是可以接受的。 + +![multiversion-scheme](images/concurrency-control/multiversion-scheme.png) + +在这种大前提下,数据库系统引入了另一种并发控制机制 - *多版本并发控制*(Multiversion Concurrency Control),每一个写操作都会创建一个新版本的数据,读操作会从有限多个版本的数据中挑选一个最合适的结果直接返回;在这时,读写操作之间的冲突就不再需要被关注,而管理和快速挑选数据的版本就成了 MVCC 需要解决的主要问题。 + +MVCC 并不是一个与乐观和悲观并发控制对立的东西,它能够与两者很好的结合以增加事务的并发量,在目前最流行的 SQL 数据库 MySQL 和 PostgreSQL 中都对 MVCC 进行了实现;但是由于它们分别实现了悲观锁和乐观锁,所以 MVCC 实现的方式也不同。 + +### MySQL 与 MVCC + +MySQL 中实现的多版本两阶段锁协议(Multiversion 2PL)将 MVCC 和 2PL 的优点结合了起来,每一个版本的数据行都具有一个唯一的时间戳,当有读事务请求时,数据库程序会直接从多个版本的数据项中具有最大时间戳的返回。 + +![multiversion-2pl-read](images/concurrency-control/multiversion-2pl-read.png) + +更新操作就稍微有些复杂了,事务会先读取最新版本的数据计算出数据更新后的结果,然后创建一个新版本的数据,新数据的时间戳是目前数据行的最大版本 `+1`: + +![multiversion-2pl-write](images/concurrency-control/multiversion-2pl-write.png) + +数据版本的删除也是根据时间戳来选择的,MySQL 会将版本最低的数据定时从数据库中清除以保证不会出现大量的遗留内容。 + +### PostgreSQL 与 MVCC + +与 MySQL 中使用悲观并发控制不同,PostgreSQL 中都是使用乐观并发控制的,这也就导致了 MVCC 在于乐观锁结合时的实现上有一些不同,最终实现的叫做多版本时间戳排序协议(Multiversion Timestamp Ordering),在这个协议中,所有的的事务在执行之前都会被分配一个唯一的时间戳,每一个数据项都有读写两个时间戳: + +![dataitem-with-timestamps](images/concurrency-control/dataitem-with-timestamps.png) + +当 PostgreSQL 的事务发出了一个读请求,数据库直接将最新版本的数据返回,不会被任何操作阻塞,而写操作在执行时,事务的时间戳一定要大或者等于数据行的读时间戳,否则就会被回滚。 + +这种 MVCC 的实现保证了读事务永远都不会失败并且不需要等待锁的释放,对于读请求远远多于写请求的应用程序,乐观锁加 MVCC 对数据库的性能有着非常大的提升;虽然这种协议能够针对一些实际情况做出一些明显的性能提升,但是也会导致两个问题,一个是每一次读操作都会更新读时间戳造成两次的磁盘写入,第二是事务之间的冲突是通过回滚解决的,所以如果冲突的可能性非常高或者回滚代价巨大,数据库的读写性能还不如使用传统的锁等待方式。 + +## 总结 + +数据库的并发控制机制到今天已经有了非常成熟、完善的解决方案,我们并不需要自己去设计一套新的协议来处理不同事务之间的冲突问题,从数据库的并发控制机制中学习到的相关知识,无论是锁还是乐观并发控制在其他的领域或者应用中都被广泛使用,所以了解、熟悉不同的并发控制机制的原理是很有必要的。 + +> 原文链接:[浅谈数据库并发控制 - 锁和 MVCC · 面向信仰编程](https://draveness.me/database-concurrency-control.html) +> Follow: [Draveness · GitHub](https://github.com/Draveness) + +## Reference + ++ [浅谈数据库并发控制 - 锁和 MVCC · 面向信仰编程](https://draveness.me/database-concurrency-control.html) ++ [PESSIMISTIC vs. OPTIMISTIC concurrency control](https://www.ibm.com/support/knowledgecenter/en/SSPK3V_7.0.0/com.ibm.swg.im.soliddb.sql.doc/doc/pessimistic.vs.optimistic.concurrency.control.html) ++ [PostgreSQL Concurrency with MVCC](https://devcenter.heroku.com/articles/postgresql-concurrency) ++ [Well-known Databases Use Different Approaches for MVCC](https://www.enterprisedb.com/well-known-databases-use-different-approaches-mvcc) ++ [Serializability](http://www.cs.unc.edu/~dewan/242/s01/notes/trans/node3.html) ++ [Race condition](https://en.wikipedia.org/wiki/Race_condition) + diff --git a/contents/Database/dynamo.md b/contents/Database/dynamo.md new file mode 100644 index 0000000..89cc929 --- /dev/null +++ b/contents/Database/dynamo.md @@ -0,0 +1,156 @@ +# 分布式键值存储 Dynamo 的实现原理 + +在最近的一周时间里,一直都在研究和阅读 Amazon 的一篇论文 [Dynamo: Amazon’s Highly Available Key-value Store](http://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf),论文中描述了 Amazon 的高可用分布式键值存储服务 Dynamo 的实现原理。 + +![dynamodb](images/dynamo/dynamodb.png) + +之前在阅读 Google 的 [Bigtable: A Distributed Storage System for Structured Data](https://static.googleusercontent.com/media/research.google.com/en//archive/bigtable-osdi06.pdf) 时写了一篇 [浅析 Bigtable 和 LevelDB 的实现](https://draveness.me/bigtable-leveldb) 文章分析了 Bigtable 的单机版 LevelDB 的实现原理;在研究 Dynamo 时,作者发现 Dynamo 虽然和 Bigtable 同为 NoSQL,但是它们的实现却有着很大的不同,最主要的原因来自不同的应用场景和不同的目的。 + +## Bigtable 和 Dynamo + +Bigtable 和 Dynamo 两者分别是 Google 和 Amazon 两大巨头给出的存储海量数据的解决方法,作为 NoSQL 两者都具有分布式、容错以及可扩展的几大特性。 + +![nosql-main-characteristics](images/dynamo/nosql-main-characteristics.png) + +虽然两者都是 NoSQL,并且有着相似的特性,但是它们在侧重的方向上有非常明显的不同,从两个数据库论文的标题中,我们就能看到 Amazon 的 Dynamo 追求的是高可用性并且提供的是类似 MongoDB 的 Key-value 文档存储,而 Bigtable 中描述的数据库却可以用于结构化的数据存储。 + +由于 Bigtable 和 Dynamo 都属于同一个类别 - NoSQL,所以它们经常会被放在一起进行对比,这篇文章不仅会介绍 Dynamo 的设计理念以及架构等问题,还会就其中的部分问题与 Bigtable 中相对应的概念进行对比,这样能够让我们更加清楚地了解不同的数据库对不同问题,因设计理念的差异做出的权衡。 + +## 架构 + +在数据库领域中尤其是分布式数据库,最重要的就是服务的架构,多数的分布式系统在设计时都会假设服务运行在廉价的节点上,并没有出众的性能和也不能提供稳定的服务,所以水平扩展和容错的能力是分布式数据库的标配;但是不同的分布式数据库选用了不同的架构来组织大量的节点。 + +很多的分布式服务例如 GFS 和 Bigtable 都使用了带有主节点的架构来维护整个系统中的元数据,包括节点的位置等信息,而 Dynamo 的实现不同于这些中心化的分布式服务,在 Dynamo 中所有的节点都有着完全相同的职责,会对外界提供同样的服务,所以在整个系统中并不会出现单点故障的问题。 + +![dynamo-architecture](images/dynamo/dynamo-architecture.png) + +去中心化的架构使得系统的水平扩展非常容易,节点可以在任何时候直接加入到整个 Dynamo 的集群中,并且只会造成集群中少量数据的迁移。 + +Bigtable 使用了中心化的架构,通过主节点来维护整个系统中全部的元数据信息,但是 Bigtable 本身其实并不会处理来自客户端的读写请求,所有请求都会由客户端直接和从节点通信,不过由于有了中心化的主节点,所以主节点一旦发生故障宕机就会造成服务的不可用,虽然 Bigtable 以及类似的服务通过其他方式解决这个问题,但是这个问题仍然是中心化的设计所造成的。 + +![centralized-architecture](images/dynamo/centralized-architecture.png) + +中心化或者去中心化并不是一个绝对好或者绝对坏的选择,选择中心化的解决方案能够降低系统实现的复杂度,而去中心化的方式能够避免单点故障,让系统能够更好更快地增加新的节点,提供优秀的水平扩展能力。 + +## 分片和复制 + +Dynamo 在设计之初就定下了**增量扩展**(Incremental Scalability)的核心需求,这也就需要一种能够在一组节点中动态分片的机制,Dynamo 的分片策略依赖于*一致性哈希*,通过这种策略 Dynamo 能够将负载合理的分配到不同的存储节点上。 + +所有的键在存储之前都会通过哈希函数得到一个唯一的值,哈希函数的输出被看做是一个固定长度的环,也就是其输出的最大值和最小值是『连接』到一起的: + +![partition-in-dynamo](images/dynamo/partition-in-dynamo.png) + +每一个节点都会被 Dynamo 在这个环中分配一个随机的位置,而这个节点会处理从哈希的输出在当前节点前的所有键;假设我们有一个键值对 `(draven, developer)`,`Hash(draven)` 的结果位于上图中的绿色区域,从环中的位置开始按照**顺时针**的顺序寻找,找到的以第一个节点 B 就会成为协调者(coordinator)负责处理当前的键值对,上图中的每一个节点都会负责与其颜色相同的部分。 + +由于 Dynamo 系统中的每一个节点在刚刚加入当前的集群时,会被分配一个随机的位置,所以由于算法的随机性可能会导致不同节点处理的范围有所不同,最终每一个节点的负载也并不相同;为了解决这个问题,Dynamo 使用了一致性哈希算法的变种,将同一个物理节点分配到环中的多个位置(标记),成为多个虚拟节点,但是在这种策略下,如果当前的 Dynamo 节点一天处理上百万的请求,那么新增节点为了不影响已有节点的性能,会在后台进行启动,整个过程大约会**消耗一整天**的时间,这其实是很难接受的,除此之外这种策略还会造成系统进行日常归档极其缓慢。 + +![equal-size-partition-in-dynamo](images/dynamo/equal-size-partition-in-dynamo.png) + +为了解决负载的不均衡的问题,除了上面使用虚拟节点的策略之外,Dynamo 论文中还提供了另外两种策略,其中性能相对较好的是将数据的哈希分成 Q 个大小相等的区域,S 个节点每一个处理 Q/S 个分区,当某一个节点因为故障或者其他原因需要退出集群时,会将它处理的数据分片随机分配给其它的节点,当有节点加入系统时,会从其它的节点中『接管』对应的数据分片。上图只是对这种策略下的分片情况简单展示,在真实环境中分片数 Q 的值远远大于节点数 S。 + +Dynamo 为了达到高可用性和持久性,防止由于节点宕机故障或者数据丢失,将同一份数据在协调者和随后的 `N-1` 个节点上备份了多次,N 是一个可以配置的值,在一般情况下都为 3。 + +![replication-in-dynamo](images/dynamo/replication-in-dynamo.png) + +也就是说,上图中黄色区域的值会存储在三个节点 A、B 和 C 中,绿色的区域会被 B、C、D 三个节点处理,从另一个角度来看,A 节点会处理范围在 `(C, A]` 之间的值,而 B 节点会处理从 `(D, B]` 区域内的值。 + +![replication-range-in-dynamo](images/dynamo/replication-range-in-dynamo.png) + +负责存储某一个特定键值对的节点列表叫做偏好列表(preference list),因为虚拟节点在环中会随机存在,为了保证出现节点故障时不会影响可用性和持久性,偏好列表中的全部节点必须都为**不同的物理节点**。 + +Bigtable 中对分片和复制的实现其实就与 Dynamo 中完全不同,这不仅是因为 Bigtable 的节点有主从之分,还因为 Bigtable 的设计理念与 Dynamo 完全不同。在 Bigtable 中,数据是按照键的顺序存储的,数据存储的单位都是 tablet,每一张表都由多个 tablet 组成,而每一个的 tablet 都有一个 tablet 服务器来处理,而 tablet 的位置都存储在 METADATA 表中。 + +![partition-in-bigtable](images/dynamo/partition-in-bigtable.png) + +在 Bigtable 中,所有的 tablet 都在 GFS 中以 SSTable 的格式存储起来,这些 SSTable 都被分成了固定大小的块在 chunkserver 上存储,而每一个块也都会在存储在多个 chunkserver 中。 + +## 读写请求的执行 + +Dynamo 集群中的任意节点都能够接受来自客户端的对于任意键的读写请求,所有的请求都通过 RPC 调用执行,客户端在选择节点时有两种不同的策略:一种是通过一个负载均衡器根据负载选择不同的节点,另一种是通过一个清楚当前集群分片的库直接请求相应的节点。 + +![node-selecting-strategies](images/dynamo/node-selecting-strategies.png) + +从上面我们就已经知道了处理读写请求的节点就叫做协调者(coordinator),前 N 个『健康』的节点会参与读写请求的处理;Dynamo 使用了 Quorum 一致性协议来保证系统中的一致性,协议中有两个可以配置的值:R 和 W,其中 R 是成功参与一个读请求的最小节点数,而 W 是成功参与写请求的最小节点数。 + +![dynamo-read-write-operation](images/dynamo/dynamo-read-write-operation.png) + +当 R = 2 时,所有的读请求必须等待两个节点成功返回对应键的结果,才认为当前的请求结束了,也就是说读请求的时间取决于返回最慢的节点,对于写请求来说也是完全相同的;当协调者接收到了来自客户端的写请求 `put()` 时,它会创建一个新的向量时钟(vector clock),然后将新版本的信息存储在本地,之后向偏好列表(preference list)中的前 `N-1` 个节点发送消息,直到其中的 `W-1` 个返回这次请求才成功结束,读请求 `get()` 与上述请求的唯一区别就是,如果协调者发现节点中的数据出现了冲突,就会对冲突尝试进行解决并将结果重新写回对应的节点。 + +## 冲突和向量时钟 + +Dynamo 与目前的绝大多数分布式系统一样都提供了**最终一致性**,最终一致性能够允许我们异步的更新集群中的节点,`put()` 请求可能会在所有的节点后更新前就返回对应的结果了,在这时随后的 `get()` 就可能获取到过期的数据。 + +![inconsistent-in-dynamo](images/dynamo/inconsistent-in-dynamo.png) + +如果在系统中出现了节点故障宕机,那么数据的更新可能在一段时间内都不会到达失效的节点,这也是在使用 Dynamo 或者使用相似原理的系统时会遇到的问题,Amazon 中的很多应用虽然都能够忍受这种数据层面可能发生的不一致性,但是有些对业务数据一致性非常高的应用在选择 Dynamo 时就需要好好考虑了。 + +因为 Dynamo 在工作的过程中不同的节点可能会发生数据不一致的问题,这种问题肯定是需要解决的,Dynamo 能够确保**一旦数据之间发生了冲突不会丢失**,但是可能会有**已被删除的数据重新出现**的问题。 + +在多数情况下,Dynamo 中的最新版本的数据都会取代之前的版本,系统在这时可以通过语法调解(syntactic reconcile)数据库中的正确版本。但是版本也可能会出现分支,在这时,Dynamo 就会返回所有它无法处理的数据版本,由客户端在多个版本的数据中选择或者创建(collapse)合适的版本返回给 Dynamo,其实这个过程比较像出现冲突的 `git merge` 操作,git 没有办法判断当前的哪个版本是合适的,所以只能由开发者对分支之间的冲突进行处理。 + +![version-evolution-in-dynamo](images/dynamo/version-evolution-in-dynamo.png) + +上图中的每一个对象的版本 Dx 中存储着一个或多个向量时钟 `[Sn, N]`,每次 Dynamo 对数据进行写入时都会更新向量时钟的版本,节点 Sx 第一次写入时向量时钟为 `[Sx, 1]`,第二次为 `[Sx, 2]`,在这时假设节点 Sy 和 Sz 都不知道 Sx 已经对节点进行写入了,它们接收到了来自其他客户端的请求,在本地也对同样键做出了写入并分别生成了不同的时钟 `[Sy, 1]` 和 `[Sz, 1]`,当客户端再次使用 `get()` 请求时就会发现数据出现了冲突,由于 Dynamo 无法根据向量时钟自动解决,所以它需要手动合并三个不同的数据版本。 + +论文中对 24 小时内的请求进行了统计,其中 99.94% 的请求仅会返回一个版本,0.00057% 的请求会返回两个版本,0.00047 的请求会返回三个版本,0.000009% 的请求会返回四个版本,虽然论文中说: + +> This shows that divergent versions are created rarely. + +但是作者仍然认为在海量的数据面前 99.94% 并不是一个特别高的百分比,处理分歧的数据版本仍然会带来额外的工作量和负担。虽然在这种情况下,数据库本身确实没有足够的信息来解决数据的不一致问题,也确实只能由客户端去解决冲突,但是这种将问题抛给上层去解决的方式并不友好,论文中也提到了 Amazon 中使用 Dynamo 的应用程序也都是能够适应并解决这些数据不一致的问题的,不过对于作者来说,仅仅这一个问题就成为不选择 Dynamo 的理由了。 + +## 节点的增删 + +因为在分布式系统中节点的失效是非常常见的事情,而节点也很少会因为某些原因永久失效,往往大部分节点会临时宕机然后快速重新加入系统;由于这些原因,Dynamo 选择使用了显式的机制向系统中添加和移除节点。 + +![ring-membership](images/dynamo/ring-membership.png) + +添加节点时可以使用命令行工具或者浏览器连接 Dynamo 中的任意节点后触发一个成员变动的事件,这个事件会从当前的环中移除或者向环中添加一个新的节点,当节点的信息发生改变时,该节点会通过 Gossip 协议通知它所能通知的最多的节点。 + +![gossip-protoco](images/dynamo/gossip-protocol.png) + +在 Gossip 协议中,每次通讯的两个节点会对当前系统中的节点信息达成一致;通过节点之间互相传递成员信息,最终整个 Dyanmo 的集群中所有的节点都会就成员信息达成一致,如上图所示,"gossip" 首先会被 C 节点接收,然后它会传递给它能接触到的最多的节点 A、D、F、G 四个节点,然后 "gossip" 会进行二次传播传递给系统中的灰色节点,到此为止系统中的所有节点都得到了最新的 "gossip" 消息。 + +当我们向 Dynamo 中加入了新的节点时,会发生节点之间的分片转移,假设我们连接上了 Dynamo 数据库,然后添加了一个 X 节点,该节点被分配到了如下图所示的 A 和 B 节点之间。 + +![adding-storage-node](images/dynamo/adding-storage-node.png) + +新引入的节点 X 会从三个节点 C、D、E 中接受它们管理的分片的一部分,也就是上图中彩色的 `(E, A]`、`(A, B]` 和 `(B, X]` 三个部分,在 X 节点加入集群之前分别属于与其颜色相同的节点管理。 + +Dynamo 由于其去中心化的架构,节点增删的事件都需要通过 Gossip 协议进行传递,然而拥有主从节点之分的 Bigtable 就不需要上述的方式对集群中的节点进行增删了,它可以直接通过用于管理其他从节点的服务直接注册新的节点或者撤下已有的节点。 + +## 副本同步 + +在 Dynamo 运行的过程中,由于一些情况会造成不同节点中的数据不一致的问题,Dynamo 使用了反信息熵(anti-entropy)的策略保证所有的副本存储的信息都是同步的。 + +为了快速确认多个副本之间的数据的一致性并避免大量的数据传输,Dynamo 使用了 [Merkle tree](https://en.wikipedia.org/wiki/Merkle_tree) 对不同节点中的数据进行快速验证。 + +![merkle-hash-tree](images/dynamo/merkle-hash-tree.png) + +在 Merkle 树中,所有父节点中的内容都是叶子节点的哈希,通过这种方式构建的树形结构能够保证整棵树不会被篡改,任何的改动都能被立刻发现。 + +Dynamo 中的每一个节点都为其持有的键的范围维护了一颗 Merkle 树,在验证两份节点中的数据是否相同时,只需要发送根节点中的哈希值,如果相同那么说明两棵树的内容全部相同,否则就会依次对比不同层级节点中的内容,直到找出不同的副本,这种做法虽然能够减少数据的传输并能够快速找到副本之间的不同,但是当有新的节点加入或者旧的节点退出时会导致大量的 Merkle 树重新计算。 + +## 总结 + +在 Dynamo 的论文公开之后,有一篇文章将 Dynamo 的设计称作 ["A flawed architecture"](http://jsensarma.com/blog/?p=55),这篇文章的作者在文中对 Dynamo 的实现进行了分析,主要对其最终一致性和 Quorom 机制进行了批评,它在 [HackerNews](https://news.ycombinator.com/item?id=915212) 上也引起了广泛的讨论,帖子中的很多内容都值得一看,能够帮助我们了解 Dynamo 的设计原理,而 Amazon 的 CTO 对于这篇文章也发了一条 Twitter: + +![amazon-cto-twitter-about-dynamo](images/dynamo/amazon-cto-twitter-about-dynamo.png) + +不管如何,Dynamo 作为支撑亚马逊业务的底层服务,其实现原理和思想对于整个社区都是非常有价值的,然而它使用的去中心化的策略也带了很多问题,虽然作者可能会因为这个原因在选择数据库时不会 Dynamo,不过相信它也是有合适的应用场景的。 + +> 原文链接:[理解 ActiveRecord](https://draveness.me/dynamo) +> +> Follow: [Draveness · GitHub](https://github.com/Draveness) + +## Reference + ++ [Dynamo: Amazon’s Highly Available Key-value Store](http://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf) ++ [Dynamo: A flawed architecture – Part I](http://jsensarma.com/blog/?p=55) ++ [Dynamo – Part I: a followup and re-rebuttals](http://jsensarma.com/blog/?p=64) ++ [Dynamo and BigTable - Review and Comparison](https://www.slideshare.net/GrishaWeintraub/presentation-46722530) ++ [DynamoDB vs. BigTable · vsChart](http://vschart.com/compare/dynamo-db/vs/bigtable) ++ [Merkle tree](https://en.wikipedia.org/wiki/Merkle_tree) ++ [A Digital Signature Based on a Conventional Encryption Function](https://link.springer.com/content/pdf/10.1007/3-540-48184-2_32.pdf) ++ [Dynamo 的实现技术和去中心化](http://www.raychase.net/2396) ++ [浅析 Bigtable 和 LevelDB 的实现](https://draveness.me/bigtable-leveldb) + diff --git a/contents/Database/images/concurrency-control/Shared-Exclusive-Lock.jpg b/contents/Database/images/concurrency-control/Shared-Exclusive-Lock.jpg new file mode 100644 index 0000000..8061f69 Binary files /dev/null and b/contents/Database/images/concurrency-control/Shared-Exclusive-Lock.jpg differ diff --git a/contents/Database/images/concurrency-control/Shared-Exclusive-Lock.png b/contents/Database/images/concurrency-control/Shared-Exclusive-Lock.png new file mode 100644 index 0000000..16a48a1 Binary files /dev/null and b/contents/Database/images/concurrency-control/Shared-Exclusive-Lock.png differ diff --git a/contents/Database/images/concurrency-control/dataitem-with-timestamps.jpg b/contents/Database/images/concurrency-control/dataitem-with-timestamps.jpg new file mode 100644 index 0000000..4cfa7b9 Binary files /dev/null and b/contents/Database/images/concurrency-control/dataitem-with-timestamps.jpg differ diff --git a/contents/Database/images/concurrency-control/dataitem-with-timestamps.png b/contents/Database/images/concurrency-control/dataitem-with-timestamps.png new file mode 100644 index 0000000..5037440 Binary files /dev/null and b/contents/Database/images/concurrency-control/dataitem-with-timestamps.png differ diff --git a/contents/Database/images/concurrency-control/deadlock-handling.jpg b/contents/Database/images/concurrency-control/deadlock-handling.jpg new file mode 100644 index 0000000..48c22b1 Binary files /dev/null and b/contents/Database/images/concurrency-control/deadlock-handling.jpg differ diff --git a/contents/Database/images/concurrency-control/deadlock-handling.png b/contents/Database/images/concurrency-control/deadlock-handling.png new file mode 100644 index 0000000..d4ec5c4 Binary files /dev/null and b/contents/Database/images/concurrency-control/deadlock-handling.png differ diff --git a/contents/Database/images/concurrency-control/deadlock-prevention-die.jpg b/contents/Database/images/concurrency-control/deadlock-prevention-die.jpg new file mode 100644 index 0000000..1c92221 Binary files /dev/null and b/contents/Database/images/concurrency-control/deadlock-prevention-die.jpg differ diff --git a/contents/Database/images/concurrency-control/deadlock-prevention-wait-die.jpg b/contents/Database/images/concurrency-control/deadlock-prevention-wait-die.jpg new file mode 100644 index 0000000..658862e Binary files /dev/null and b/contents/Database/images/concurrency-control/deadlock-prevention-wait-die.jpg differ diff --git a/contents/Database/images/concurrency-control/deadlock-prevention-wait-die.png b/contents/Database/images/concurrency-control/deadlock-prevention-wait-die.png new file mode 100644 index 0000000..e5078b7 Binary files /dev/null and b/contents/Database/images/concurrency-control/deadlock-prevention-wait-die.png differ diff --git a/contents/Database/images/concurrency-control/deadlock-prevention-wait.jpg b/contents/Database/images/concurrency-control/deadlock-prevention-wait.jpg new file mode 100644 index 0000000..04166b4 Binary files /dev/null and b/contents/Database/images/concurrency-control/deadlock-prevention-wait.jpg differ diff --git a/contents/Database/images/concurrency-control/deadlock-prevention-wound-wait.jpg b/contents/Database/images/concurrency-control/deadlock-prevention-wound-wait.jpg new file mode 100644 index 0000000..08d908e Binary files /dev/null and b/contents/Database/images/concurrency-control/deadlock-prevention-wound-wait.jpg differ diff --git a/contents/Database/images/concurrency-control/deadlock-prevention-wound-wait.png b/contents/Database/images/concurrency-control/deadlock-prevention-wound-wait.png new file mode 100644 index 0000000..1b8f0cf Binary files /dev/null and b/contents/Database/images/concurrency-control/deadlock-prevention-wound-wait.png differ diff --git a/contents/Database/images/concurrency-control/deadlock-recovery.jpg b/contents/Database/images/concurrency-control/deadlock-recovery.jpg new file mode 100644 index 0000000..b3f0437 Binary files /dev/null and b/contents/Database/images/concurrency-control/deadlock-recovery.jpg differ diff --git a/contents/Database/images/concurrency-control/deadlock-recovery.png b/contents/Database/images/concurrency-control/deadlock-recovery.png new file mode 100644 index 0000000..f0d2076 Binary files /dev/null and b/contents/Database/images/concurrency-control/deadlock-recovery.png differ diff --git a/contents/Database/images/concurrency-control/deadlock-wait-for-graph.jpg b/contents/Database/images/concurrency-control/deadlock-wait-for-graph.jpg new file mode 100644 index 0000000..3603d6e Binary files /dev/null and b/contents/Database/images/concurrency-control/deadlock-wait-for-graph.jpg differ diff --git a/contents/Database/images/concurrency-control/deadlock-wait-for-graph.png b/contents/Database/images/concurrency-control/deadlock-wait-for-graph.png new file mode 100644 index 0000000..ff5734e Binary files /dev/null and b/contents/Database/images/concurrency-control/deadlock-wait-for-graph.png differ diff --git a/contents/Database/images/concurrency-control/deadlock.jpg b/contents/Database/images/concurrency-control/deadlock.jpg new file mode 100644 index 0000000..e953e58 Binary files /dev/null and b/contents/Database/images/concurrency-control/deadlock.jpg differ diff --git a/contents/Database/images/concurrency-control/deadlock.png b/contents/Database/images/concurrency-control/deadlock.png new file mode 100644 index 0000000..096ebac Binary files /dev/null and b/contents/Database/images/concurrency-control/deadlock.png differ diff --git a/contents/Database/images/concurrency-control/granularity-hierarchy.jpg b/contents/Database/images/concurrency-control/granularity-hierarchy.jpg new file mode 100644 index 0000000..0b612a2 Binary files /dev/null and b/contents/Database/images/concurrency-control/granularity-hierarchy.jpg differ diff --git a/contents/Database/images/concurrency-control/granularity-hierarchy.png b/contents/Database/images/concurrency-control/granularity-hierarchy.png new file mode 100644 index 0000000..2166a61 Binary files /dev/null and b/contents/Database/images/concurrency-control/granularity-hierarchy.png differ diff --git a/contents/Database/images/concurrency-control/growing-to-shrinking.jpg b/contents/Database/images/concurrency-control/growing-to-shrinking.jpg new file mode 100644 index 0000000..74c770b Binary files /dev/null and b/contents/Database/images/concurrency-control/growing-to-shrinking.jpg differ diff --git a/contents/Database/images/concurrency-control/growing-to-shrinking.png b/contents/Database/images/concurrency-control/growing-to-shrinking.png new file mode 100644 index 0000000..e38f6d4 Binary files /dev/null and b/contents/Database/images/concurrency-control/growing-to-shrinking.png differ diff --git a/contents/Database/images/concurrency-control/lock-and-wait.jpg b/contents/Database/images/concurrency-control/lock-and-wait.jpg new file mode 100644 index 0000000..8782c8b Binary files /dev/null and b/contents/Database/images/concurrency-control/lock-and-wait.jpg differ diff --git a/contents/Database/images/concurrency-control/lock-and-wait.png b/contents/Database/images/concurrency-control/lock-and-wait.png new file mode 100644 index 0000000..8615eab Binary files /dev/null and b/contents/Database/images/concurrency-control/lock-and-wait.png differ diff --git a/contents/Database/images/concurrency-control/lock-type-compatibility-matrix.jpg b/contents/Database/images/concurrency-control/lock-type-compatibility-matrix.jpg new file mode 100644 index 0000000..73fb3ef Binary files /dev/null and b/contents/Database/images/concurrency-control/lock-type-compatibility-matrix.jpg differ diff --git a/contents/Database/images/concurrency-control/lock-type-compatibility-matrix.png b/contents/Database/images/concurrency-control/lock-type-compatibility-matrix.png new file mode 100644 index 0000000..b066343 Binary files /dev/null and b/contents/Database/images/concurrency-control/lock-type-compatibility-matrix.png differ diff --git a/contents/Database/images/concurrency-control/multiversion-2pl-read.jpg b/contents/Database/images/concurrency-control/multiversion-2pl-read.jpg new file mode 100644 index 0000000..7d476ae Binary files /dev/null and b/contents/Database/images/concurrency-control/multiversion-2pl-read.jpg differ diff --git a/contents/Database/images/concurrency-control/multiversion-2pl-read.png b/contents/Database/images/concurrency-control/multiversion-2pl-read.png new file mode 100644 index 0000000..108b931 Binary files /dev/null and b/contents/Database/images/concurrency-control/multiversion-2pl-read.png differ diff --git a/contents/Database/images/concurrency-control/multiversion-2pl-write.jpg b/contents/Database/images/concurrency-control/multiversion-2pl-write.jpg new file mode 100644 index 0000000..2e6bf89 Binary files /dev/null and b/contents/Database/images/concurrency-control/multiversion-2pl-write.jpg differ diff --git a/contents/Database/images/concurrency-control/multiversion-2pl-write.png b/contents/Database/images/concurrency-control/multiversion-2pl-write.png new file mode 100644 index 0000000..68d54a7 Binary files /dev/null and b/contents/Database/images/concurrency-control/multiversion-2pl-write.png differ diff --git a/contents/Database/images/concurrency-control/multiversion-scheme.jpg b/contents/Database/images/concurrency-control/multiversion-scheme.jpg new file mode 100644 index 0000000..1f0cdca Binary files /dev/null and b/contents/Database/images/concurrency-control/multiversion-scheme.jpg differ diff --git a/contents/Database/images/concurrency-control/multiversion-scheme.png b/contents/Database/images/concurrency-control/multiversion-scheme.png new file mode 100644 index 0000000..3e6df5f Binary files /dev/null and b/contents/Database/images/concurrency-control/multiversion-scheme.png differ diff --git a/contents/Database/images/concurrency-control/pessimistic-and-optimistic.jpg b/contents/Database/images/concurrency-control/pessimistic-and-optimistic.jpg new file mode 100644 index 0000000..c17c9e2 Binary files /dev/null and b/contents/Database/images/concurrency-control/pessimistic-and-optimistic.jpg differ diff --git a/contents/Database/images/concurrency-control/pessimistic-and-optimistic.png b/contents/Database/images/concurrency-control/pessimistic-and-optimistic.png new file mode 100644 index 0000000..a363032 Binary files /dev/null and b/contents/Database/images/concurrency-control/pessimistic-and-optimistic.png differ diff --git a/contents/Database/images/concurrency-control/pessimistic-conccurency-control.jpg b/contents/Database/images/concurrency-control/pessimistic-conccurency-control.jpg new file mode 100644 index 0000000..36599dd Binary files /dev/null and b/contents/Database/images/concurrency-control/pessimistic-conccurency-control.jpg differ diff --git a/contents/Database/images/concurrency-control/pessimistic-conccurency-control.png b/contents/Database/images/concurrency-control/pessimistic-conccurency-control.png new file mode 100644 index 0000000..00debda Binary files /dev/null and b/contents/Database/images/concurrency-control/pessimistic-conccurency-control.png differ diff --git a/contents/Database/images/concurrency-control/pessimistic-optimistic-multiversion-conccurency-control.jpg b/contents/Database/images/concurrency-control/pessimistic-optimistic-multiversion-conccurency-control.jpg new file mode 100644 index 0000000..8c2a3a9 Binary files /dev/null and b/contents/Database/images/concurrency-control/pessimistic-optimistic-multiversion-conccurency-control.jpg differ diff --git a/contents/Database/images/concurrency-control/pessimistic-optimistic-multiversion-conccurency-control.png b/contents/Database/images/concurrency-control/pessimistic-optimistic-multiversion-conccurency-control.png new file mode 100644 index 0000000..9cbef29 Binary files /dev/null and b/contents/Database/images/concurrency-control/pessimistic-optimistic-multiversion-conccurency-control.png differ diff --git a/contents/Database/images/concurrency-control/rigorous-two-phase-locking.jpg b/contents/Database/images/concurrency-control/rigorous-two-phase-locking.jpg new file mode 100644 index 0000000..4d57e37 Binary files /dev/null and b/contents/Database/images/concurrency-control/rigorous-two-phase-locking.jpg differ diff --git a/contents/Database/images/concurrency-control/timestamp-ordering-protocol-process.jpg b/contents/Database/images/concurrency-control/timestamp-ordering-protocol-process.jpg new file mode 100644 index 0000000..0608b5e Binary files /dev/null and b/contents/Database/images/concurrency-control/timestamp-ordering-protocol-process.jpg differ diff --git a/contents/Database/images/concurrency-control/timestamp-ordering-protocol-process.png b/contents/Database/images/concurrency-control/timestamp-ordering-protocol-process.png new file mode 100644 index 0000000..38beea5 Binary files /dev/null and b/contents/Database/images/concurrency-control/timestamp-ordering-protocol-process.png differ diff --git a/contents/Database/images/concurrency-control/timestamp-ordering-protocol.jpg b/contents/Database/images/concurrency-control/timestamp-ordering-protocol.jpg new file mode 100644 index 0000000..a5b2d2e Binary files /dev/null and b/contents/Database/images/concurrency-control/timestamp-ordering-protocol.jpg differ diff --git a/contents/Database/images/concurrency-control/timestamp-ordering-protocol.png b/contents/Database/images/concurrency-control/timestamp-ordering-protocol.png new file mode 100644 index 0000000..e881e66 Binary files /dev/null and b/contents/Database/images/concurrency-control/timestamp-ordering-protocol.png differ diff --git a/contents/Database/images/concurrency-control/tradeoff-between-performance-and-serializability.jpg b/contents/Database/images/concurrency-control/tradeoff-between-performance-and-serializability.jpg new file mode 100644 index 0000000..2417555 Binary files /dev/null and b/contents/Database/images/concurrency-control/tradeoff-between-performance-and-serializability.jpg differ diff --git a/contents/Database/images/concurrency-control/tradeoff-between-performance-and-serializability.png b/contents/Database/images/concurrency-control/tradeoff-between-performance-and-serializability.png new file mode 100644 index 0000000..6b645c3 Binary files /dev/null and b/contents/Database/images/concurrency-control/tradeoff-between-performance-and-serializability.png differ diff --git a/contents/Database/images/concurrency-control/two-phase-locking.jpg b/contents/Database/images/concurrency-control/two-phase-locking.jpg new file mode 100644 index 0000000..637fa73 Binary files /dev/null and b/contents/Database/images/concurrency-control/two-phase-locking.jpg differ diff --git a/contents/Database/images/concurrency-control/two-phase-locking.png b/contents/Database/images/concurrency-control/two-phase-locking.png new file mode 100644 index 0000000..ffe7b08 Binary files /dev/null and b/contents/Database/images/concurrency-control/two-phase-locking.png differ diff --git a/contents/Database/images/concurrency-control/validation-based-protocol.jpg b/contents/Database/images/concurrency-control/validation-based-protocol.jpg new file mode 100644 index 0000000..a4d3a8c Binary files /dev/null and b/contents/Database/images/concurrency-control/validation-based-protocol.jpg differ diff --git a/contents/Database/images/concurrency-control/validation-based-protocol.png b/contents/Database/images/concurrency-control/validation-based-protocol.png new file mode 100644 index 0000000..e635152 Binary files /dev/null and b/contents/Database/images/concurrency-control/validation-based-protocol.png differ diff --git a/contents/Database/images/dynamo/adding-storage-node.png b/contents/Database/images/dynamo/adding-storage-node.png new file mode 100644 index 0000000..5ffcc57 Binary files /dev/null and b/contents/Database/images/dynamo/adding-storage-node.png differ diff --git a/contents/Database/images/dynamo/amazon-cto-twitter-about-dynamo.png b/contents/Database/images/dynamo/amazon-cto-twitter-about-dynamo.png new file mode 100644 index 0000000..69417c0 Binary files /dev/null and b/contents/Database/images/dynamo/amazon-cto-twitter-about-dynamo.png differ diff --git a/contents/Database/images/dynamo/centralized-architecture.png b/contents/Database/images/dynamo/centralized-architecture.png new file mode 100644 index 0000000..e3da755 Binary files /dev/null and b/contents/Database/images/dynamo/centralized-architecture.png differ diff --git a/contents/Database/images/dynamo/dynamo-architecture.png b/contents/Database/images/dynamo/dynamo-architecture.png new file mode 100644 index 0000000..b936260 Binary files /dev/null and b/contents/Database/images/dynamo/dynamo-architecture.png differ diff --git a/contents/Database/images/dynamo/dynamo-read-write-operation.png b/contents/Database/images/dynamo/dynamo-read-write-operation.png new file mode 100644 index 0000000..14d5583 Binary files /dev/null and b/contents/Database/images/dynamo/dynamo-read-write-operation.png differ diff --git a/contents/Database/images/dynamo/dynamodb.png b/contents/Database/images/dynamo/dynamodb.png new file mode 100644 index 0000000..0cce042 Binary files /dev/null and b/contents/Database/images/dynamo/dynamodb.png differ diff --git a/contents/Database/images/dynamo/equal-size-partition-in-dynamo.png b/contents/Database/images/dynamo/equal-size-partition-in-dynamo.png new file mode 100644 index 0000000..f5250e8 Binary files /dev/null and b/contents/Database/images/dynamo/equal-size-partition-in-dynamo.png differ diff --git a/contents/Database/images/dynamo/gossip-protocol.png b/contents/Database/images/dynamo/gossip-protocol.png new file mode 100644 index 0000000..68d9598 Binary files /dev/null and b/contents/Database/images/dynamo/gossip-protocol.png differ diff --git a/contents/Database/images/dynamo/inconsistent-in-dynamo.png b/contents/Database/images/dynamo/inconsistent-in-dynamo.png new file mode 100644 index 0000000..f2977f9 Binary files /dev/null and b/contents/Database/images/dynamo/inconsistent-in-dynamo.png differ diff --git a/contents/Database/images/dynamo/merkle-hash-tree.png b/contents/Database/images/dynamo/merkle-hash-tree.png new file mode 100644 index 0000000..68fa263 Binary files /dev/null and b/contents/Database/images/dynamo/merkle-hash-tree.png differ diff --git a/contents/Database/images/dynamo/node-selecting-strategies.png b/contents/Database/images/dynamo/node-selecting-strategies.png new file mode 100644 index 0000000..47b6245 Binary files /dev/null and b/contents/Database/images/dynamo/node-selecting-strategies.png differ diff --git a/contents/Database/images/dynamo/nosql-main-characteristics.png b/contents/Database/images/dynamo/nosql-main-characteristics.png new file mode 100644 index 0000000..5669c9d Binary files /dev/null and b/contents/Database/images/dynamo/nosql-main-characteristics.png differ diff --git a/contents/Database/images/dynamo/partition-in-bigtable.png b/contents/Database/images/dynamo/partition-in-bigtable.png new file mode 100644 index 0000000..d0441ef Binary files /dev/null and b/contents/Database/images/dynamo/partition-in-bigtable.png differ diff --git a/contents/Database/images/dynamo/partition-in-dynamo.png b/contents/Database/images/dynamo/partition-in-dynamo.png new file mode 100644 index 0000000..e7c9709 Binary files /dev/null and b/contents/Database/images/dynamo/partition-in-dynamo.png differ diff --git a/contents/Database/images/dynamo/replication-in-dynamo.png b/contents/Database/images/dynamo/replication-in-dynamo.png new file mode 100644 index 0000000..a61adc3 Binary files /dev/null and b/contents/Database/images/dynamo/replication-in-dynamo.png differ diff --git a/contents/Database/images/dynamo/replication-range-in-dynamo.png b/contents/Database/images/dynamo/replication-range-in-dynamo.png new file mode 100644 index 0000000..04bfdc5 Binary files /dev/null and b/contents/Database/images/dynamo/replication-range-in-dynamo.png differ diff --git a/contents/Database/images/dynamo/ring-membership.png b/contents/Database/images/dynamo/ring-membership.png new file mode 100644 index 0000000..e72933a Binary files /dev/null and b/contents/Database/images/dynamo/ring-membership.png differ diff --git a/contents/Database/images/dynamo/version-evolution-in-dynamo.png b/contents/Database/images/dynamo/version-evolution-in-dynamo.png new file mode 100644 index 0000000..af4b604 Binary files /dev/null and b/contents/Database/images/dynamo/version-evolution-in-dynamo.png differ diff --git a/contents/Database/images/leveldb-bigtable/Bigtable-DataModel-Row-Column-Timestamp-Value.jpg b/contents/Database/images/leveldb-bigtable/Bigtable-DataModel-Row-Column-Timestamp-Value.jpg new file mode 100644 index 0000000..c9899bb Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/Bigtable-DataModel-Row-Column-Timestamp-Value.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/Bigtable-LevelDB-Cover.jpg b/contents/Database/images/leveldb-bigtable/Bigtable-LevelDB-Cover.jpg new file mode 100644 index 0000000..ad58e03 Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/Bigtable-LevelDB-Cover.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/FileMetaData.jpg b/contents/Database/images/leveldb-bigtable/FileMetaData.jpg new file mode 100644 index 0000000..65d6f84 Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/FileMetaData.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/Goals-of-Bigtable.jpg b/contents/Database/images/leveldb-bigtable/Goals-of-Bigtable.jpg new file mode 100644 index 0000000..7a9cbba Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/Goals-of-Bigtable.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/Immutable-MemTable.jpg b/contents/Database/images/leveldb-bigtable/Immutable-MemTable.jpg new file mode 100644 index 0000000..c39b695 Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/Immutable-MemTable.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/LevelDB-After-Compactions.jpg b/contents/Database/images/leveldb-bigtable/LevelDB-After-Compactions.jpg new file mode 100644 index 0000000..6c81e8d Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/LevelDB-After-Compactions.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/LevelDB-BackgroundCompaction-Processes.jpg b/contents/Database/images/leveldb-bigtable/LevelDB-BackgroundCompaction-Processes.jpg new file mode 100644 index 0000000..621f2fb Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/LevelDB-BackgroundCompaction-Processes.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/LevelDB-Level0-Layer.jpg b/contents/Database/images/leveldb-bigtable/LevelDB-Level0-Layer.jpg new file mode 100644 index 0000000..1c35d9b Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/LevelDB-Level0-Layer.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/LevelDB-LevelN-Layers.jpg b/contents/Database/images/leveldb-bigtable/LevelDB-LevelN-Layers.jpg new file mode 100644 index 0000000..e164f58 Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/LevelDB-LevelN-Layers.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/LevelDB-MemTable-SkipList.jpg b/contents/Database/images/leveldb-bigtable/LevelDB-MemTable-SkipList.jpg new file mode 100644 index 0000000..64b4351 Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/LevelDB-MemTable-SkipList.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/LevelDB-Memtable-Key-Value-Format.jpg b/contents/Database/images/leveldb-bigtable/LevelDB-Memtable-Key-Value-Format.jpg new file mode 100644 index 0000000..582c0b8 Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/LevelDB-Memtable-Key-Value-Format.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/LevelDB-Pick-Compactions.jpg b/contents/Database/images/leveldb-bigtable/LevelDB-Pick-Compactions.jpg new file mode 100644 index 0000000..e77d104 Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/LevelDB-Pick-Compactions.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/LevelDB-Put.jpg b/contents/Database/images/leveldb-bigtable/LevelDB-Put.jpg new file mode 100644 index 0000000..142b7a0 Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/LevelDB-Put.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/LevelDB-Read-Processes.jpg b/contents/Database/images/leveldb-bigtable/LevelDB-Read-Processes.jpg new file mode 100644 index 0000000..b55359e Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/LevelDB-Read-Processes.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/LevelDB-Serving.jpg b/contents/Database/images/leveldb-bigtable/LevelDB-Serving.jpg new file mode 100644 index 0000000..d365bb1 Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/LevelDB-Serving.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/LevelDB-log-format-and-recordtype.jpg b/contents/Database/images/leveldb-bigtable/LevelDB-log-format-and-recordtype.jpg new file mode 100644 index 0000000..a5c214a Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/LevelDB-log-format-and-recordtype.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/Major-Compaction.jpg b/contents/Database/images/leveldb-bigtable/Major-Compaction.jpg new file mode 100644 index 0000000..c9ac5ee Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/Major-Compaction.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/Master-Manage-Tablet-Servers-And-Tablets.jpg b/contents/Database/images/leveldb-bigtable/Master-Manage-Tablet-Servers-And-Tablets.jpg new file mode 100644 index 0000000..cee9f9e Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/Master-Manage-Tablet-Servers-And-Tablets.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/Minor-Compaction.jpg b/contents/Database/images/leveldb-bigtable/Minor-Compaction.jpg new file mode 100644 index 0000000..1ee6468 Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/Minor-Compaction.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/SSTable-Footer.jpg b/contents/Database/images/leveldb-bigtable/SSTable-Footer.jpg new file mode 100644 index 0000000..5c4c2f5 Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/SSTable-Footer.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/SSTable-Format.jpg b/contents/Database/images/leveldb-bigtable/SSTable-Format.jpg new file mode 100644 index 0000000..91f507a Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/SSTable-Format.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/Tablet-Location-Hierarchy.jpg b/contents/Database/images/leveldb-bigtable/Tablet-Location-Hierarchy.jpg new file mode 100644 index 0000000..66d67f0 Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/Tablet-Location-Hierarchy.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/Tablet-Serving.jpg b/contents/Database/images/leveldb-bigtable/Tablet-Serving.jpg new file mode 100644 index 0000000..2b24d6c Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/Tablet-Serving.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/VersionSet-Version-And-VersionEdit.jpg b/contents/Database/images/leveldb-bigtable/VersionSet-Version-And-VersionEdit.jpg new file mode 100644 index 0000000..1599a71 Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/VersionSet-Version-And-VersionEdit.jpg differ diff --git a/contents/Database/images/leveldb-bigtable/leveldb-logo.png b/contents/Database/images/leveldb-bigtable/leveldb-logo.png new file mode 100644 index 0000000..5ce6e2d Binary files /dev/null and b/contents/Database/images/leveldb-bigtable/leveldb-logo.png differ diff --git a/contents/Database/images/mongodb-to-mysql/array-to-string-or-relation.png b/contents/Database/images/mongodb-to-mysql/array-to-string-or-relation.png new file mode 100644 index 0000000..fb797b3 Binary files /dev/null and b/contents/Database/images/mongodb-to-mysql/array-to-string-or-relation.png differ diff --git a/contents/Database/images/mongodb-to-mysql/embedded-reference-documents.png b/contents/Database/images/mongodb-to-mysql/embedded-reference-documents.png new file mode 100644 index 0000000..3fc2b66 Binary files /dev/null and b/contents/Database/images/mongodb-to-mysql/embedded-reference-documents.png differ diff --git a/contents/Database/images/mongodb-to-mysql/embedded-to-reference.png b/contents/Database/images/mongodb-to-mysql/embedded-to-reference.png new file mode 100644 index 0000000..4dfe55a Binary files /dev/null and b/contents/Database/images/mongodb-to-mysql/embedded-to-reference.png differ diff --git a/contents/Database/images/mongodb-to-mysql/embedded_reference_documents.png b/contents/Database/images/mongodb-to-mysql/embedded_reference_documents.png new file mode 100644 index 0000000..d9c9ca6 Binary files /dev/null and b/contents/Database/images/mongodb-to-mysql/embedded_reference_documents.png differ diff --git a/contents/Database/images/mongodb-to-mysql/mongodb-mysql-datatype-relation.png b/contents/Database/images/mongodb-to-mysql/mongodb-mysql-datatype-relation.png new file mode 100644 index 0000000..49c5cc7 Binary files /dev/null and b/contents/Database/images/mongodb-to-mysql/mongodb-mysql-datatype-relation.png differ diff --git a/contents/Database/images/mongodb-to-mysql/mongodb-mysql-enum.png b/contents/Database/images/mongodb-to-mysql/mongodb-mysql-enum.png new file mode 100644 index 0000000..f7f9763 Binary files /dev/null and b/contents/Database/images/mongodb-to-mysql/mongodb-mysql-enum.png differ diff --git a/contents/Database/images/mongodb-to-mysql/mongodb-mysql-id.png b/contents/Database/images/mongodb-to-mysql/mongodb-mysql-id.png new file mode 100644 index 0000000..814fcee Binary files /dev/null and b/contents/Database/images/mongodb-to-mysql/mongodb-mysql-id.png differ diff --git a/contents/Database/images/mongodb-to-mysql/mongodb-mysql-problems-to-be-solved.png b/contents/Database/images/mongodb-to-mysql/mongodb-mysql-problems-to-be-solved.png new file mode 100644 index 0000000..85caa78 Binary files /dev/null and b/contents/Database/images/mongodb-to-mysql/mongodb-mysql-problems-to-be-solved.png differ diff --git a/contents/Database/images/mongodb-to-mysql/mongodb-pre-migration.png b/contents/Database/images/mongodb-to-mysql/mongodb-pre-migration.png new file mode 100644 index 0000000..7d1776f Binary files /dev/null and b/contents/Database/images/mongodb-to-mysql/mongodb-pre-migration.png differ diff --git a/contents/Database/images/mongodb-to-mysql/mongoid-activerecord-enum.png b/contents/Database/images/mongodb-to-mysql/mongoid-activerecord-enum.png new file mode 100644 index 0000000..54f5611 Binary files /dev/null and b/contents/Database/images/mongodb-to-mysql/mongoid-activerecord-enum.png differ diff --git a/contents/Database/images/mongodb-to-mysql/mongoid-to-activerecord-model-and-query.png b/contents/Database/images/mongodb-to-mysql/mongoid-to-activerecord-model-and-query.png new file mode 100644 index 0000000..1de57a1 Binary files /dev/null and b/contents/Database/images/mongodb-to-mysql/mongoid-to-activerecord-model-and-query.png differ diff --git a/contents/Database/images/mongodb-to-mysql/mysql-after-migrations.png b/contents/Database/images/mongodb-to-mysql/mysql-after-migrations.png new file mode 100644 index 0000000..81ef19c Binary files /dev/null and b/contents/Database/images/mongodb-to-mysql/mysql-after-migrations.png differ diff --git a/contents/Database/images/mongodb-to-mysql/mysql-and-mongodb-cover.png b/contents/Database/images/mongodb-to-mysql/mysql-and-mongodb-cover.png new file mode 100644 index 0000000..7efd294 Binary files /dev/null and b/contents/Database/images/mongodb-to-mysql/mysql-and-mongodb-cover.png differ diff --git a/contents/Database/images/mongodb-to-mysql/mysql-and-mongodb-work-together.png b/contents/Database/images/mongodb-to-mysql/mysql-and-mongodb-work-together.png new file mode 100644 index 0000000..4a99859 Binary files /dev/null and b/contents/Database/images/mongodb-to-mysql/mysql-and-mongodb-work-together.png differ diff --git a/contents/Database/images/mongodb-to-mysql/mysql-and-mongodb.png b/contents/Database/images/mongodb-to-mysql/mysql-and-mongodb.png new file mode 100644 index 0000000..77030fd Binary files /dev/null and b/contents/Database/images/mongodb-to-mysql/mysql-and-mongodb.png differ diff --git a/contents/Database/images/mongodb-to-mysql/mysql-before-migrations.png b/contents/Database/images/mongodb-to-mysql/mysql-before-migrations.png new file mode 100644 index 0000000..d9b0c2a Binary files /dev/null and b/contents/Database/images/mongodb-to-mysql/mysql-before-migrations.png differ diff --git a/contents/Database/images/mongodb-to-mysql/mysql-migrations.png b/contents/Database/images/mongodb-to-mysql/mysql-migrations.png new file mode 100644 index 0000000..65b1996 Binary files /dev/null and b/contents/Database/images/mongodb-to-mysql/mysql-migrations.png differ diff --git a/contents/Database/images/mongodb-to-mysql/mysql-to-mongodb.png b/contents/Database/images/mongodb-to-mysql/mysql-to-mongodb.png new file mode 100644 index 0000000..a385137 Binary files /dev/null and b/contents/Database/images/mongodb-to-mysql/mysql-to-mongodb.png differ diff --git a/contents/Database/images/mongodb-to-mysql/mysqldump-csv.png b/contents/Database/images/mongodb-to-mysql/mysqldump-csv.png new file mode 100644 index 0000000..295c44e Binary files /dev/null and b/contents/Database/images/mongodb-to-mysql/mysqldump-csv.png differ diff --git a/contents/Database/images/mongodb-wiredtiger/Checkpoints-Conditions.jpg b/contents/Database/images/mongodb-wiredtiger/Checkpoints-Conditions.jpg new file mode 100644 index 0000000..d9bddf7 Binary files /dev/null and b/contents/Database/images/mongodb-wiredtiger/Checkpoints-Conditions.jpg differ diff --git a/contents/Database/images/mongodb-wiredtiger/Compound-Index.jpg b/contents/Database/images/mongodb-wiredtiger/Compound-Index.jpg new file mode 100644 index 0000000..bfddce0 Binary files /dev/null and b/contents/Database/images/mongodb-wiredtiger/Compound-Index.jpg differ diff --git a/contents/Database/images/mongodb-wiredtiger/Different-Data-Structure.jpg b/contents/Database/images/mongodb-wiredtiger/Different-Data-Structure.jpg new file mode 100644 index 0000000..284ed3c Binary files /dev/null and b/contents/Database/images/mongodb-wiredtiger/Different-Data-Structure.jpg differ diff --git a/contents/Database/images/mongodb-wiredtiger/Embedded-Data-Models-MongoDB.jpg b/contents/Database/images/mongodb-wiredtiger/Embedded-Data-Models-MongoDB.jpg new file mode 100644 index 0000000..82d22a3 Binary files /dev/null and b/contents/Database/images/mongodb-wiredtiger/Embedded-Data-Models-MongoDB.jpg differ diff --git a/contents/Database/images/mongodb-wiredtiger/MongoDB-Architecture.jpg b/contents/Database/images/mongodb-wiredtiger/MongoDB-Architecture.jpg new file mode 100644 index 0000000..84577b7 Binary files /dev/null and b/contents/Database/images/mongodb-wiredtiger/MongoDB-Architecture.jpg differ diff --git a/contents/Database/images/mongodb-wiredtiger/MongoDB-Cover.jpg b/contents/Database/images/mongodb-wiredtiger/MongoDB-Cover.jpg new file mode 100644 index 0000000..c7c8965 Binary files /dev/null and b/contents/Database/images/mongodb-wiredtiger/MongoDB-Cover.jpg differ diff --git a/contents/Database/images/mongodb-wiredtiger/MongoDB-Covers.jpg b/contents/Database/images/mongodb-wiredtiger/MongoDB-Covers.jpg new file mode 100644 index 0000000..c7c8965 Binary files /dev/null and b/contents/Database/images/mongodb-wiredtiger/MongoDB-Covers.jpg differ diff --git a/contents/Database/images/mongodb-wiredtiger/MongoDB-Indexes.jpg b/contents/Database/images/mongodb-wiredtiger/MongoDB-Indexes.jpg new file mode 100644 index 0000000..309eb3d Binary files /dev/null and b/contents/Database/images/mongodb-wiredtiger/MongoDB-Indexes.jpg differ diff --git a/contents/Database/images/mongodb-wiredtiger/MongoDB-ObjectId.jpg b/contents/Database/images/mongodb-wiredtiger/MongoDB-ObjectId.jpg new file mode 100644 index 0000000..4242d98 Binary files /dev/null and b/contents/Database/images/mongodb-wiredtiger/MongoDB-ObjectId.jpg differ diff --git a/contents/Database/images/mongodb-wiredtiger/Multiple-Storage-Engines.jpg b/contents/Database/images/mongodb-wiredtiger/Multiple-Storage-Engines.jpg new file mode 100644 index 0000000..3219800 Binary files /dev/null and b/contents/Database/images/mongodb-wiredtiger/Multiple-Storage-Engines.jpg differ diff --git a/contents/Database/images/mongodb-wiredtiger/Not-Found-Document.jpg b/contents/Database/images/mongodb-wiredtiger/Not-Found-Document.jpg new file mode 100644 index 0000000..588f428 Binary files /dev/null and b/contents/Database/images/mongodb-wiredtiger/Not-Found-Document.jpg differ diff --git a/contents/Database/images/mongodb-wiredtiger/Reference-MongoDB.jpg b/contents/Database/images/mongodb-wiredtiger/Reference-MongoDB.jpg new file mode 100644 index 0000000..36522a9 Binary files /dev/null and b/contents/Database/images/mongodb-wiredtiger/Reference-MongoDB.jpg differ diff --git a/contents/Database/images/mongodb-wiredtiger/Single-Field-Index.jpg b/contents/Database/images/mongodb-wiredtiger/Single-Field-Index.jpg new file mode 100644 index 0000000..abd36b3 Binary files /dev/null and b/contents/Database/images/mongodb-wiredtiger/Single-Field-Index.jpg differ diff --git a/contents/Database/images/mongodb-wiredtiger/Translating-Between-RDBMS-and-MongoDB.jpg b/contents/Database/images/mongodb-wiredtiger/Translating-Between-RDBMS-and-MongoDB.jpg new file mode 100644 index 0000000..71c19a8 Binary files /dev/null and b/contents/Database/images/mongodb-wiredtiger/Translating-Between-RDBMS-and-MongoDB.jpg differ diff --git a/contents/Database/images/mongodb-wiredtiger/WiredTiger-Cache.jpg b/contents/Database/images/mongodb-wiredtiger/WiredTiger-Cache.jpg new file mode 100644 index 0000000..894174f Binary files /dev/null and b/contents/Database/images/mongodb-wiredtiger/WiredTiger-Cache.jpg differ diff --git a/contents/Database/images/mongodb-wiredtiger/its-not-always-simple-banner.jpg b/contents/Database/images/mongodb-wiredtiger/its-not-always-simple-banner.jpg new file mode 100644 index 0000000..c307a1a Binary files /dev/null and b/contents/Database/images/mongodb-wiredtiger/its-not-always-simple-banner.jpg differ diff --git a/contents/Database/images/mongodb-wiredtiger/logo.png b/contents/Database/images/mongodb-wiredtiger/logo.png new file mode 100644 index 0000000..1537089 Binary files /dev/null and b/contents/Database/images/mongodb-wiredtiger/logo.png differ diff --git a/contents/Database/images/mysql/Antelope-Barracuda-Row-Format.jpg b/contents/Database/images/mysql/Antelope-Barracuda-Row-Format.jpg new file mode 100644 index 0000000..a4cca7f Binary files /dev/null and b/contents/Database/images/mysql/Antelope-Barracuda-Row-Format.jpg differ diff --git a/contents/Database/images/mysql/B+Tree.jpg b/contents/Database/images/mysql/B+Tree.jpg new file mode 100644 index 0000000..5346601 Binary files /dev/null and b/contents/Database/images/mysql/B+Tree.jpg differ diff --git a/contents/Database/images/mysql/COMPACT-And-REDUNDANT-Row-Format.jpg b/contents/Database/images/mysql/COMPACT-And-REDUNDANT-Row-Format.jpg new file mode 100644 index 0000000..ac01b41 Binary files /dev/null and b/contents/Database/images/mysql/COMPACT-And-REDUNDANT-Row-Format.jpg differ diff --git a/contents/Database/images/mysql/Clustered-Index.jpg b/contents/Database/images/mysql/Clustered-Index.jpg new file mode 100644 index 0000000..f313192 Binary files /dev/null and b/contents/Database/images/mysql/Clustered-Index.jpg differ diff --git a/contents/Database/images/mysql/Clustered-Secondary-Index.jpg b/contents/Database/images/mysql/Clustered-Secondary-Index.jpg new file mode 100644 index 0000000..c34c625 Binary files /dev/null and b/contents/Database/images/mysql/Clustered-Secondary-Index.jpg differ diff --git a/contents/Database/images/mysql/Database-Instance.jpg b/contents/Database/images/mysql/Database-Instance.jpg new file mode 100644 index 0000000..8f38d51 Binary files /dev/null and b/contents/Database/images/mysql/Database-Instance.jpg differ diff --git a/contents/Database/images/mysql/Deadlocks.jpg b/contents/Database/images/mysql/Deadlocks.jpg new file mode 100644 index 0000000..e2e4845 Binary files /dev/null and b/contents/Database/images/mysql/Deadlocks.jpg differ diff --git a/contents/Database/images/mysql/Infimum-Rows-Supremum.jpg b/contents/Database/images/mysql/Infimum-Rows-Supremum.jpg new file mode 100644 index 0000000..2894d4b Binary files /dev/null and b/contents/Database/images/mysql/Infimum-Rows-Supremum.jpg differ diff --git a/contents/Database/images/mysql/InnoDB-B-Tree-Node.jpg b/contents/Database/images/mysql/InnoDB-B-Tree-Node.jpg new file mode 100644 index 0000000..22409df Binary files /dev/null and b/contents/Database/images/mysql/InnoDB-B-Tree-Node.jpg differ diff --git a/contents/Database/images/mysql/Lock-Type-Compatibility-Matrix.jpg b/contents/Database/images/mysql/Lock-Type-Compatibility-Matrix.jpg new file mode 100644 index 0000000..c5098ee Binary files /dev/null and b/contents/Database/images/mysql/Lock-Type-Compatibility-Matrix.jpg differ diff --git a/contents/Database/images/mysql/Logical-View-of-MySQL-Architecture.jpg b/contents/Database/images/mysql/Logical-View-of-MySQL-Architecture.jpg new file mode 100644 index 0000000..be2c1ad Binary files /dev/null and b/contents/Database/images/mysql/Logical-View-of-MySQL-Architecture.jpg differ diff --git a/contents/Database/images/mysql/Optimistic-Pessimistic-Locks.jpg b/contents/Database/images/mysql/Optimistic-Pessimistic-Locks.jpg new file mode 100644 index 0000000..6cfff4a Binary files /dev/null and b/contents/Database/images/mysql/Optimistic-Pessimistic-Locks.jpg differ diff --git a/contents/Database/images/mysql/Read-Commited-Non-Repeatable-Read.jpg b/contents/Database/images/mysql/Read-Commited-Non-Repeatable-Read.jpg new file mode 100644 index 0000000..7d4b259 Binary files /dev/null and b/contents/Database/images/mysql/Read-Commited-Non-Repeatable-Read.jpg differ diff --git a/contents/Database/images/mysql/Read-Uncommited-Dirty-Read.jpg b/contents/Database/images/mysql/Read-Uncommited-Dirty-Read.jpg new file mode 100644 index 0000000..54d913d Binary files /dev/null and b/contents/Database/images/mysql/Read-Uncommited-Dirty-Read.jpg differ diff --git a/contents/Database/images/mysql/Relation-Between-Page-Size-Extent-Size.png b/contents/Database/images/mysql/Relation-Between-Page-Size-Extent-Size.png new file mode 100644 index 0000000..6f68e46 Binary files /dev/null and b/contents/Database/images/mysql/Relation-Between-Page-Size-Extent-Size.png differ diff --git a/contents/Database/images/mysql/Repeatable-Read-Phantom-Read.jpg b/contents/Database/images/mysql/Repeatable-Read-Phantom-Read.jpg new file mode 100644 index 0000000..a7633be Binary files /dev/null and b/contents/Database/images/mysql/Repeatable-Read-Phantom-Read.jpg differ diff --git a/contents/Database/images/mysql/Repeatable-with-Next-Key-Lock.jpg b/contents/Database/images/mysql/Repeatable-with-Next-Key-Lock.jpg new file mode 100644 index 0000000..bab3dc4 Binary files /dev/null and b/contents/Database/images/mysql/Repeatable-with-Next-Key-Lock.jpg differ diff --git a/contents/Database/images/mysql/Row-Overflow-in-Barracuda.jpg b/contents/Database/images/mysql/Row-Overflow-in-Barracuda.jpg new file mode 100644 index 0000000..f702e4e Binary files /dev/null and b/contents/Database/images/mysql/Row-Overflow-in-Barracuda.jpg differ diff --git a/contents/Database/images/mysql/Row-Overflow.jpg b/contents/Database/images/mysql/Row-Overflow.jpg new file mode 100644 index 0000000..c743e7a Binary files /dev/null and b/contents/Database/images/mysql/Row-Overflow.jpg differ diff --git a/contents/Database/images/mysql/Secondary-Index.jpg b/contents/Database/images/mysql/Secondary-Index.jpg new file mode 100644 index 0000000..1924223 Binary files /dev/null and b/contents/Database/images/mysql/Secondary-Index.jpg differ diff --git a/contents/Database/images/mysql/Shared-Exclusive-Lock.jpg b/contents/Database/images/mysql/Shared-Exclusive-Lock.jpg new file mode 100644 index 0000000..9f07746 Binary files /dev/null and b/contents/Database/images/mysql/Shared-Exclusive-Lock.jpg differ diff --git a/contents/Database/images/mysql/Tablespace-segment-extent-page-row.jpg b/contents/Database/images/mysql/Tablespace-segment-extent-page-row.jpg new file mode 100644 index 0000000..66db246 Binary files /dev/null and b/contents/Database/images/mysql/Tablespace-segment-extent-page-row.jpg differ diff --git a/contents/Database/images/mysql/Transaction-Isolation-Matrix.jpg b/contents/Database/images/mysql/Transaction-Isolation-Matrix.jpg new file mode 100644 index 0000000..c203fca Binary files /dev/null and b/contents/Database/images/mysql/Transaction-Isolation-Matrix.jpg differ diff --git a/contents/Database/images/mysql/frm-and-ibd-file.jpg b/contents/Database/images/mysql/frm-and-ibd-file.jpg new file mode 100644 index 0000000..dacb667 Binary files /dev/null and b/contents/Database/images/mysql/frm-and-ibd-file.jpg differ diff --git a/contents/Database/images/mysql/frm-file-hex.png b/contents/Database/images/mysql/frm-file-hex.png new file mode 100644 index 0000000..7e4a446 Binary files /dev/null and b/contents/Database/images/mysql/frm-file-hex.png differ diff --git a/contents/Database/images/mysql/mysql.png b/contents/Database/images/mysql/mysql.png new file mode 100644 index 0000000..5e57082 Binary files /dev/null and b/contents/Database/images/mysql/mysql.png differ diff --git a/contents/Database/images/mysql/page-size-and-extent-size.png b/contents/Database/images/mysql/page-size-and-extent-size.png new file mode 100644 index 0000000..c7a862d Binary files /dev/null and b/contents/Database/images/mysql/page-size-and-extent-size.png differ diff --git a/contents/Database/images/sql-index-intro/Behind-Three-Star-Index.jpg b/contents/Database/images/sql-index-intro/Behind-Three-Star-Index.jpg new file mode 100644 index 0000000..39fb485 Binary files /dev/null and b/contents/Database/images/sql-index-intro/Behind-Three-Star-Index.jpg differ diff --git a/contents/Database/images/sql-index-intro/Combined-Filter-Factor-Related.jpg b/contents/Database/images/sql-index-intro/Combined-Filter-Factor-Related.jpg new file mode 100644 index 0000000..0371a61 Binary files /dev/null and b/contents/Database/images/sql-index-intro/Combined-Filter-Factor-Related.jpg differ diff --git a/contents/Database/images/sql-index-intro/Combined-Filter-Factor.jpg b/contents/Database/images/sql-index-intro/Combined-Filter-Factor.jpg new file mode 100644 index 0000000..fe5237c Binary files /dev/null and b/contents/Database/images/sql-index-intro/Combined-Filter-Factor.jpg differ diff --git a/contents/Database/images/sql-index-intro/Different-Stars-Index.jpg b/contents/Database/images/sql-index-intro/Different-Stars-Index.jpg new file mode 100644 index 0000000..88b92d1 Binary files /dev/null and b/contents/Database/images/sql-index-intro/Different-Stars-Index.jpg differ diff --git a/contents/Database/images/sql-index-intro/Disk-IO-Total-Time.jpg b/contents/Database/images/sql-index-intro/Disk-IO-Total-Time.jpg new file mode 100644 index 0000000..ee65dde Binary files /dev/null and b/contents/Database/images/sql-index-intro/Disk-IO-Total-Time.jpg differ diff --git a/contents/Database/images/sql-index-intro/Disk-IO.jpg b/contents/Database/images/sql-index-intro/Disk-IO.jpg new file mode 100644 index 0000000..5041d88 Binary files /dev/null and b/contents/Database/images/sql-index-intro/Disk-IO.jpg differ diff --git a/contents/Database/images/sql-index-intro/Disk-Random-IO.jpg b/contents/Database/images/sql-index-intro/Disk-Random-IO.jpg new file mode 100644 index 0000000..9e7c535 Binary files /dev/null and b/contents/Database/images/sql-index-intro/Disk-Random-IO.jpg differ diff --git a/contents/Database/images/sql-index-intro/Filter-Factor.jpg b/contents/Database/images/sql-index-intro/Filter-Factor.jpg new file mode 100644 index 0000000..e9792b8 Binary files /dev/null and b/contents/Database/images/sql-index-intro/Filter-Factor.jpg differ diff --git a/contents/Database/images/sql-index-intro/Index-and-Performance.jpg b/contents/Database/images/sql-index-intro/Index-and-Performance.jpg new file mode 100644 index 0000000..0a73457 Binary files /dev/null and b/contents/Database/images/sql-index-intro/Index-and-Performance.jpg differ diff --git a/contents/Database/images/sql-index-intro/Match-Columns-Filter-Columns.jpg b/contents/Database/images/sql-index-intro/Match-Columns-Filter-Columns.jpg new file mode 100644 index 0000000..23f8081 Binary files /dev/null and b/contents/Database/images/sql-index-intro/Match-Columns-Filter-Columns.jpg differ diff --git a/contents/Database/images/sql-index-intro/Page-DatabaseBufferPool-Disk.jpg b/contents/Database/images/sql-index-intro/Page-DatabaseBufferPool-Disk.jpg new file mode 100644 index 0000000..cfe8b8c Binary files /dev/null and b/contents/Database/images/sql-index-intro/Page-DatabaseBufferPool-Disk.jpg differ diff --git a/contents/Database/images/sql-index-intro/Page-DatabaseBufferPool.jpg b/contents/Database/images/sql-index-intro/Page-DatabaseBufferPool.jpg new file mode 100644 index 0000000..4baa52d Binary files /dev/null and b/contents/Database/images/sql-index-intro/Page-DatabaseBufferPool.jpg differ diff --git a/contents/Database/images/sql-index-intro/Random-IO.jpg b/contents/Database/images/sql-index-intro/Random-IO.jpg new file mode 100644 index 0000000..41a5ce6 Binary files /dev/null and b/contents/Database/images/sql-index-intro/Random-IO.jpg differ diff --git a/contents/Database/images/sql-index-intro/Random-to-Sequential.jpg b/contents/Database/images/sql-index-intro/Random-to-Sequential.jpg new file mode 100644 index 0000000..d1accfa Binary files /dev/null and b/contents/Database/images/sql-index-intro/Random-to-Sequential.jpg differ diff --git a/contents/Database/images/sql-index-intro/Read-from-Memory.jpg b/contents/Database/images/sql-index-intro/Read-from-Memory.jpg new file mode 100644 index 0000000..67ec820 Binary files /dev/null and b/contents/Database/images/sql-index-intro/Read-from-Memory.jpg differ diff --git a/contents/Database/images/sql-index-intro/Same-Columns-Filter-Factor.jpg b/contents/Database/images/sql-index-intro/Same-Columns-Filter-Factor.jpg new file mode 100644 index 0000000..9a997f8 Binary files /dev/null and b/contents/Database/images/sql-index-intro/Same-Columns-Filter-Factor.jpg differ diff --git a/contents/Database/images/sql-index-intro/Sequential-Reads-from-Disk.jpg b/contents/Database/images/sql-index-intro/Sequential-Reads-from-Disk.jpg new file mode 100644 index 0000000..355b671 Binary files /dev/null and b/contents/Database/images/sql-index-intro/Sequential-Reads-from-Disk.jpg differ diff --git a/contents/Database/images/sql-index-intro/Thin-Index-and-Clustered-Index.jpg b/contents/Database/images/sql-index-intro/Thin-Index-and-Clustered-Index.jpg new file mode 100644 index 0000000..fa3e541 Binary files /dev/null and b/contents/Database/images/sql-index-intro/Thin-Index-and-Clustered-Index.jpg differ diff --git a/contents/Database/images/sql-index-intro/Thin-Index-and-Fat-Index.jpg b/contents/Database/images/sql-index-intro/Thin-Index-and-Fat-Index.jpg new file mode 100644 index 0000000..4511eb1 Binary files /dev/null and b/contents/Database/images/sql-index-intro/Thin-Index-and-Fat-Index.jpg differ diff --git a/contents/Database/images/sql-index-intro/Three-Star-Index.jpg b/contents/Database/images/sql-index-intro/Three-Star-Index.jpg new file mode 100644 index 0000000..3e14711 Binary files /dev/null and b/contents/Database/images/sql-index-intro/Three-Star-Index.jpg differ diff --git a/contents/Database/images/sql-index-performance/Complicated-Query-with-Order-By.jpg b/contents/Database/images/sql-index-performance/Complicated-Query-with-Order-By.jpg new file mode 100644 index 0000000..1fec1df Binary files /dev/null and b/contents/Database/images/sql-index-performance/Complicated-Query-with-Order-By.jpg differ diff --git a/contents/Database/images/sql-index-performance/Disk-Service-Time.jpg b/contents/Database/images/sql-index-performance/Disk-Service-Time.jpg new file mode 100644 index 0000000..0bb3742 Binary files /dev/null and b/contents/Database/images/sql-index-performance/Disk-Service-Time.jpg differ diff --git a/contents/Database/images/sql-index-performance/Filter-Factor.jpg b/contents/Database/images/sql-index-performance/Filter-Factor.jpg new file mode 100644 index 0000000..eb9f91b Binary files /dev/null and b/contents/Database/images/sql-index-performance/Filter-Factor.jpg differ diff --git a/contents/Database/images/sql-index-performance/Index-Search.jpg b/contents/Database/images/sql-index-performance/Index-Search.jpg new file mode 100644 index 0000000..07afdb7 Binary files /dev/null and b/contents/Database/images/sql-index-performance/Index-Search.jpg differ diff --git a/contents/Database/images/sql-index-performance/Index-Slice-Scan.jpg b/contents/Database/images/sql-index-performance/Index-Slice-Scan.jpg new file mode 100644 index 0000000..4e6112c Binary files /dev/null and b/contents/Database/images/sql-index-performance/Index-Slice-Scan.jpg differ diff --git a/contents/Database/images/sql-index-performance/Index-Table-Touch.jpg b/contents/Database/images/sql-index-performance/Index-Table-Touch.jpg new file mode 100644 index 0000000..7b20b76 Binary files /dev/null and b/contents/Database/images/sql-index-performance/Index-Table-Touch.jpg differ diff --git a/contents/Database/images/sql-index-performance/Local-Response-Time-Calculation.jpg b/contents/Database/images/sql-index-performance/Local-Response-Time-Calculation.jpg new file mode 100644 index 0000000..6ee31eb Binary files /dev/null and b/contents/Database/images/sql-index-performance/Local-Response-Time-Calculation.jpg differ diff --git a/contents/Database/images/sql-index-performance/Local-Response-Time.jpg b/contents/Database/images/sql-index-performance/Local-Response-Time.jpg new file mode 100644 index 0000000..9cbeebd Binary files /dev/null and b/contents/Database/images/sql-index-performance/Local-Response-Time.jpg differ diff --git a/contents/Database/images/sql-index-performance/Proactive-Index-Design.jpg b/contents/Database/images/sql-index-performance/Proactive-Index-Design.jpg new file mode 100644 index 0000000..6b2bec3 Binary files /dev/null and b/contents/Database/images/sql-index-performance/Proactive-Index-Design.jpg differ diff --git a/contents/Database/images/sql-index-performance/QUBE-LRT.jpg b/contents/Database/images/sql-index-performance/QUBE-LRT.jpg new file mode 100644 index 0000000..7bd55cc Binary files /dev/null and b/contents/Database/images/sql-index-performance/QUBE-LRT.jpg differ diff --git a/contents/Database/images/sql-index-performance/SQL-Query-Time-After-Optimization.jpg b/contents/Database/images/sql-index-performance/SQL-Query-Time-After-Optimization.jpg new file mode 100644 index 0000000..bafe708 Binary files /dev/null and b/contents/Database/images/sql-index-performance/SQL-Query-Time-After-Optimization.jpg differ diff --git a/contents/Database/images/sql-index-performance/SQL-Query-Time.jpg b/contents/Database/images/sql-index-performance/SQL-Query-Time.jpg new file mode 100644 index 0000000..5b2b327 Binary files /dev/null and b/contents/Database/images/sql-index-performance/SQL-Query-Time.jpg differ diff --git a/contents/Database/images/sql-index-performance/Semifat-Index-and-Fat-Index.jpg b/contents/Database/images/sql-index-performance/Semifat-Index-and-Fat-Index.jpg new file mode 100644 index 0000000..0fb89ef Binary files /dev/null and b/contents/Database/images/sql-index-performance/Semifat-Index-and-Fat-Index.jpg differ diff --git a/contents/Database/images/sql-index-performance/User-Table.jpg b/contents/Database/images/sql-index-performance/User-Table.jpg new file mode 100644 index 0000000..79d4f0a Binary files /dev/null and b/contents/Database/images/sql-index-performance/User-Table.jpg differ diff --git a/contents/Database/images/transaction/ACID-And-CAP.jpg b/contents/Database/images/transaction/ACID-And-CAP.jpg new file mode 100644 index 0000000..78a48e4 Binary files /dev/null and b/contents/Database/images/transaction/ACID-And-CAP.jpg differ diff --git a/contents/Database/images/transaction/Atomic-Operation.jpg b/contents/Database/images/transaction/Atomic-Operation.jpg new file mode 100644 index 0000000..45ac6e7 Binary files /dev/null and b/contents/Database/images/transaction/Atomic-Operation.jpg differ diff --git a/contents/Database/images/transaction/Atomitc-Transaction-State.jpg b/contents/Database/images/transaction/Atomitc-Transaction-State.jpg new file mode 100644 index 0000000..eef883f Binary files /dev/null and b/contents/Database/images/transaction/Atomitc-Transaction-State.jpg differ diff --git a/contents/Database/images/transaction/Cascading-Rollback.jpg b/contents/Database/images/transaction/Cascading-Rollback.jpg new file mode 100644 index 0000000..e480da6 Binary files /dev/null and b/contents/Database/images/transaction/Cascading-Rollback.jpg differ diff --git a/contents/Database/images/transaction/Compensating-Transaction.jpg b/contents/Database/images/transaction/Compensating-Transaction.jpg new file mode 100644 index 0000000..8f22559 Binary files /dev/null and b/contents/Database/images/transaction/Compensating-Transaction.jpg differ diff --git a/contents/Database/images/transaction/Isolation-Performance.jpg b/contents/Database/images/transaction/Isolation-Performance.jpg new file mode 100644 index 0000000..c7c6f9d Binary files /dev/null and b/contents/Database/images/transaction/Isolation-Performance.jpg differ diff --git a/contents/Database/images/transaction/Logical-Undo-Log.jpg b/contents/Database/images/transaction/Logical-Undo-Log.jpg new file mode 100644 index 0000000..7ffb139 Binary files /dev/null and b/contents/Database/images/transaction/Logical-Undo-Log.jpg differ diff --git a/contents/Database/images/transaction/Nonatomitc-Transaction-State.jpg b/contents/Database/images/transaction/Nonatomitc-Transaction-State.jpg new file mode 100644 index 0000000..d863dab Binary files /dev/null and b/contents/Database/images/transaction/Nonatomitc-Transaction-State.jpg differ diff --git a/contents/Database/images/transaction/Nonrecoverable-Schedule.jpg b/contents/Database/images/transaction/Nonrecoverable-Schedule.jpg new file mode 100644 index 0000000..0459826 Binary files /dev/null and b/contents/Database/images/transaction/Nonrecoverable-Schedule.jpg differ diff --git a/contents/Database/images/transaction/Reasons-for-Allowing-Concurrency.jpg b/contents/Database/images/transaction/Reasons-for-Allowing-Concurrency.jpg new file mode 100644 index 0000000..68eca3e Binary files /dev/null and b/contents/Database/images/transaction/Reasons-for-Allowing-Concurrency.jpg differ diff --git a/contents/Database/images/transaction/Recoverable-Schedule.jpg b/contents/Database/images/transaction/Recoverable-Schedule.jpg new file mode 100644 index 0000000..8e5dec0 Binary files /dev/null and b/contents/Database/images/transaction/Recoverable-Schedule.jpg differ diff --git a/contents/Database/images/transaction/Redo-Logging.jpg b/contents/Database/images/transaction/Redo-Logging.jpg new file mode 100644 index 0000000..9165d12 Binary files /dev/null and b/contents/Database/images/transaction/Redo-Logging.jpg differ diff --git a/contents/Database/images/transaction/Shared-Exclusive-Lock.jpg b/contents/Database/images/transaction/Shared-Exclusive-Lock.jpg new file mode 100644 index 0000000..e85567b Binary files /dev/null and b/contents/Database/images/transaction/Shared-Exclusive-Lock.jpg differ diff --git a/contents/Database/images/transaction/Shared-Lock-and-Atomicity.jpg b/contents/Database/images/transaction/Shared-Lock-and-Atomicity.jpg new file mode 100644 index 0000000..19e2e93 Binary files /dev/null and b/contents/Database/images/transaction/Shared-Lock-and-Atomicity.jpg differ diff --git a/contents/Database/images/transaction/Shutdown-After-Commited.jpg b/contents/Database/images/transaction/Shutdown-After-Commited.jpg new file mode 100644 index 0000000..a939582 Binary files /dev/null and b/contents/Database/images/transaction/Shutdown-After-Commited.jpg differ diff --git a/contents/Database/images/transaction/Timestamps-Record.jpg b/contents/Database/images/transaction/Timestamps-Record.jpg new file mode 100644 index 0000000..a715b28 Binary files /dev/null and b/contents/Database/images/transaction/Timestamps-Record.jpg differ diff --git a/contents/Database/images/transaction/Transaction-Basics.jpg b/contents/Database/images/transaction/Transaction-Basics.jpg new file mode 100644 index 0000000..94b288f Binary files /dev/null and b/contents/Database/images/transaction/Transaction-Basics.jpg differ diff --git a/contents/Database/images/transaction/Transaction-Consistency.jpg b/contents/Database/images/transaction/Transaction-Consistency.jpg new file mode 100644 index 0000000..615a833 Binary files /dev/null and b/contents/Database/images/transaction/Transaction-Consistency.jpg differ diff --git a/contents/Database/images/transaction/Transaction-Cover.jpg b/contents/Database/images/transaction/Transaction-Cover.jpg new file mode 100644 index 0000000..851e7e2 Binary files /dev/null and b/contents/Database/images/transaction/Transaction-Cover.jpg differ diff --git a/contents/Database/images/transaction/Transaction-Isolation-Matrix.jpg b/contents/Database/images/transaction/Transaction-Isolation-Matrix.jpg new file mode 100644 index 0000000..3d5ccb2 Binary files /dev/null and b/contents/Database/images/transaction/Transaction-Isolation-Matrix.jpg differ diff --git a/contents/Database/images/transaction/Transaction-Log.jpg b/contents/Database/images/transaction/Transaction-Log.jpg new file mode 100644 index 0000000..45526f1 Binary files /dev/null and b/contents/Database/images/transaction/Transaction-Log.jpg differ diff --git a/contents/Database/images/transaction/Transaction-Undo-Log.jpg b/contents/Database/images/transaction/Transaction-Undo-Log.jpg new file mode 100644 index 0000000..cf5034f Binary files /dev/null and b/contents/Database/images/transaction/Transaction-Undo-Log.jpg differ diff --git a/contents/Database/leveldb-bigtable.md b/contents/Database/leveldb-bigtable.md new file mode 100644 index 0000000..c942086 --- /dev/null +++ b/contents/Database/leveldb-bigtable.md @@ -0,0 +1,469 @@ +![Bigtable-LevelDB-Cover](images/leveldb-bigtable/Bigtable-LevelDB-Cover.jpg) + +# 浅析 Bigtable 和 LevelDB 的实现 + +在 2006 年的 OSDI 上,Google 发布了名为 [Bigtable: A Distributed Storage System for Structured Data](https://static.googleusercontent.com/media/research.google.com/en//archive/bigtable-osdi06.pdf) 的论文,其中描述了一个用于管理结构化数据的分布式存储系统 - Bigtable 的数据模型、接口以及实现等内容。 + +![leveldb-logo](images/leveldb-bigtable/leveldb-logo.png) + +本文会先对 Bigtable 一文中描述的分布式存储系统进行简单的描述,然后对 Google 开源的 KV 存储数据库 [LevelDB](https://github.com/google/leveldb) 进行分析;LevelDB 可以理解为单点的 Bigtable 的系统,虽然其中没有 Bigtable 中与 tablet 管理以及一些分布式相关的逻辑,不过我们可以通过对 LevelDB 源代码的阅读增加对 Bigtable 的理解。 + +## Bigtable + +Bigtable 是一个用于管理**结构化数据**的分布式存储系统,它有非常优秀的扩展性,可以同时处理上千台机器中的 PB 级别的数据;Google 中的很多项目,包括 Web 索引都使用 Bigtable 来存储海量的数据;Bigtable 的论文中声称它实现了四个目标: + +![Goals-of-Bigtable](images/leveldb-bigtable/Goals-of-Bigtable.jpg) + +在作者看来这些目标看看就好,其实并没有什么太大的意义,所有的项目都会对外宣称它们达到了高性能、高可用性等等特性,我们需要关注的是 Bigtable 到底是如何实现的。 + +### 数据模型 + +Bigtable 与数据库在很多方面都非常相似,但是它提供了与数据库不同的接口,它并没有支持全部的关系型数据模型,反而使用了简单的数据模型,使数据可以被更灵活的控制和管理。 + +在实现中,Bigtable 其实就是一个稀疏的、分布式的、多维持久有序哈希。 + +> A Bigtable is a sparse, distributed, persistent multi-dimensional sorted map. + +它的定义其实也就决定了其数据模型非常简单并且易于实现,我们使用 `row`、`column` 和 `timestamp` 三个字段作为这个哈希的键,值就是一个字节数组,也可以理解为字符串。 + +![Bigtable-DataModel-Row-Column-Timestamp-Value](images/leveldb-bigtable/Bigtable-DataModel-Row-Column-Timestamp-Value.jpg) + +这里最重要的就是 `row` 的值,它的长度最大可以为 64KB,对于同一 `row` 下数据的读写都可以看做是原子的;因为 Bigtable 是按照 `row` 的值使用字典顺序进行排序的,每一段 `row` 的范围都会被 Bigtable 进行分区,并交给一个 tablet 进行处理。 + +### 实现 + +在这一节中,我们将介绍 Bigtable 论文对于其本身实现的描述,其中包含很多内容:tablet 的组织形式、tablet 的管理、读写请求的处理以及数据的压缩等几个部分。 + +#### tablet 的组织形式 + +我们使用类似 B+ 树的三层结构来存储 tablet 的位置信息,第一层是一个单独的 [Chubby](https://static.googleusercontent.com/media/research.google.com/en//archive/chubby-osdi06.pdf) 文件,其中保存了根 tablet 的位置。 + +> Chubby 是一个分布式锁服务,我们可能会在后面的文章中介绍它。 + +![Tablet-Location-Hierarchy](images/leveldb-bigtable/Tablet-Location-Hierarchy.jpg) + +每一个 METADATA tablet 包括根节点上的 tablet 都存储了 tablet 的位置和该 tablet 中 key 的最小值和最大值;每一个 METADATA 行大约在内存中存储了 1KB 的数据,如果每一个 METADATA tablet 的大小都为 128MB,那么整个三层结构可以存储 2^61 字节的数据。 + +#### tablet 的管理 + +既然在整个 Bigtable 中有着海量的 tablet 服务器以及数据的分片 tablet,那么 Bigtable 是如何管理海量的数据呢?Bigtable 与很多的分布式系统一样,使用一个主服务器将 tablet 分派给不同的服务器节点。 + +![Master-Manage-Tablet-Servers-And-Tablets](images/leveldb-bigtable/Master-Manage-Tablet-Servers-And-Tablets.jpg) + +为了减轻主服务器的负载,所有的客户端仅仅通过 Master 获取 tablet 服务器的位置信息,它并不会在每次读写时都请求 Master 节点,而是直接与 tablet 服务器相连,同时客户端本身也会保存一份 tablet 服务器位置的缓存以减少与 Master 通信的次数和频率。 + +#### 读写请求的处理 + +从读写请求的处理,我们其实可以看出整个 Bigtable 中的各个部分是如何协作的,包括日志、memtable 以及 SSTable 文件。 + +![Tablet-Serving](images/leveldb-bigtable/Tablet-Serving.jpg) + +当有客户端向 tablet 服务器发送写操作时,它会先向 tablet 服务器中的日志追加一条记录,在日志成功追加之后再向 memtable 中插入该条记录;这与现在大多的数据库的实现完全相同,通过顺序写向日志追加记录,然后再向数据库随机写,因为随机写的耗时远远大于追加内容,如果直接进行随机写,可能由于发生设备故障造成数据丢失。 + +当 tablet 服务器接收到读操作时,它会在 memtable 和 SSTable 上进行合并查找,因为 memtable 和 SSTable 中对于键值的存储都是字典顺序的,所以整个读操作的执行会非常快。 + +#### 表的压缩 + +随着写操作的进行,memtable 会随着事件的推移逐渐增大,当 memtable 的大小超过一定的阈值时,就会将当前的 memtable 冻结,并且创建一个新的 memtable,被冻结的 memtable 会被转换为一个 SSTable 并且写入到 GFS 系统中,这种压缩方式也被称作 *Minor Compaction*。 + +![Minor-Compaction](images/leveldb-bigtable/Minor-Compaction.jpg) + +每一个 Minor Compaction 都能够创建一个新的 SSTable,它能够有效地降低内存的占用并且降低服务进程异常退出后,过大的日志导致的过长的恢复时间。既然有用于压缩 memtable 中数据的 Minor Compaction,那么就一定有一个对应的 Major Compaction 操作。 + +![Major-Compaction](images/leveldb-bigtable/Major-Compaction.jpg) + +Bigtable 会在**后台周期性**地进行 *Major Compaction*,将 memtable 中的数据和一部分的 SSTable 作为输入,将其中的键值进行归并排序,生成新的 SSTable 并移除原有的 memtable 和 SSTable,新生成的 SSTable 中包含前两者的全部数据和信息,并且将其中一部分标记未删除的信息彻底清除。 + +#### 小结 + +到这里为止,对于 Google 的 Bigtable 论文的介绍就差不多完成了,当然本文只介绍了其中的一部分内容,关于压缩算法的实现细节、缓存以及提交日志的实现等问题我们都没有涉及,想要了解更多相关信息的读者,这里强烈推荐去看一遍 Bigtable 这篇论文的原文 [Bigtable: A Distributed Storage System for Structured Data](https://static.googleusercontent.com/media/research.google.com/en//archive/bigtable-osdi06.pdf) 以增强对其实现的理解。 + +## LevelDB + +文章前面对于 Bigtable 的介绍其实都是对 [LevelDB](https://github.com/google/leveldb) 这部分内容所做的铺垫,当然这并不是说前面的内容就不重要,LevelDB 是对 Bigtable 论文中描述的键值存储系统的单机版的实现,它提供了一个极其高速的键值存储系统,并且由 Bigtable 的作者 [Jeff Dean](https://research.google.com/pubs/jeff.html) 和 [Sanjay Ghemawat](https://research.google.com/pubs/SanjayGhemawat.html) 共同完成,可以说高度复刻了 Bigtable 论文中对于其实现的描述。 + +因为 Bigtable 只是一篇论文,同时又因为其实现依赖于 Google 的一些不开源的基础服务:GFS、Chubby 等等,我们很难接触到它的源代码,不过我们可以通过 LevelDB 更好地了解这篇论文中提到的诸多内容和思量。 + +### 概述 + +LevelDB 作为一个键值存储的『仓库』,它提供了一组非常简单的增删改查接口: + +```cpp +class DB { + public: + virtual Status Put(const WriteOptions& options, const Slice& key, const Slice& value) = 0; + virtual Status Delete(const WriteOptions& options, const Slice& key) = 0; + virtual Status Write(const WriteOptions& options, WriteBatch* updates) = 0; + virtual Status Get(const ReadOptions& options, const Slice& key, std::string* value) = 0; +} +``` + +> `Put` 方法在内部最终会调用 `Write` 方法,只是在上层为调用者提供了两个不同的选择。 + +`Get` 和 `Put` 是 LevelDB 为上层提供的用于读写的接口,如果我们能够对读写的过程有一个非常清晰的认知,那么理解 LevelDB 的实现就不是那么困难了。 + +在这一节中,我们将先通过对读写操作的分析了解整个工程中的一些实现,并在遇到问题和新的概念时进行解释,我们会在这个过程中一步一步介绍 LevelDB 中一些重要模块的实现以达到掌握它的原理的目标。 + +### 从写操作开始 + +首先来看 `Get` 和 `Put` 两者中的写方法: + +```cpp +Status DB::Put(const WriteOptions& opt, const Slice& key, const Slice& value) { + WriteBatch batch; + batch.Put(key, value); + return Write(opt, &batch); +} + +Status DBImpl::Write(const WriteOptions& options, WriteBatch* my_batch) { + ... +} +``` + +正如上面所介绍的,`DB::Put` 方法将传入的参数封装成了一个 `WritaBatch`,然后仍然会执行 `DBImpl::Write` 方法向数据库中写入数据;写入方法 `DBImpl::Write` 其实是一个是非常复杂的过程,包含了很多对上下文状态的判断,我们先来看一个写操作的整体逻辑: + +![LevelDB-Put](images/leveldb-bigtable/LevelDB-Put.jpg) + +从总体上看,LevelDB 在对数据库执行写操作时,会有三个步骤: + +1. 调用 `MakeRoomForWrite` 方法为即将进行的写入提供足够的空间; + + 在这个过程中,由于 memtable 中空间的不足可能会冻结当前的 memtable,发生 Minor Compaction 并创建一个新的 `MemTable` 对象; + + 在某些条件满足时,也可能发生 Major Compaction,对数据库中的 SSTable 进行压缩; +2. 通过 `AddRecord` 方法向日志中追加一条写操作的记录; +3. 再向日志成功写入记录后,我们使用 `InsertInto` 直接插入 memtable 中,完成整个写操作的流程; + +在这里,我们并不会提供 LevelDB 对于 `Put` 方法实现的全部代码,只会展示一份精简后的代码,帮助我们大致了解一下整个写操作的流程: + +```cpp +Status DBImpl::Write(const WriteOptions& options, WriteBatch* my_batch) { + Writer w(&mutex_); + w.batch = my_batch; + + MakeRoomForWrite(my_batch == NULL); + + uint64_t last_sequence = versions_->LastSequence(); + Writer* last_writer = &w; + WriteBatch* updates = BuildBatchGroup(&last_writer); + WriteBatchInternal::SetSequence(updates, last_sequence + 1); + last_sequence += WriteBatchInternal::Count(updates); + + log_->AddRecord(WriteBatchInternal::Contents(updates)); + WriteBatchInternal::InsertInto(updates, mem_); + + versions_->SetLastSequence(last_sequence); + return Status::OK(); +} +``` + +#### 不可变的 memtable + +在写操作的实现代码 `DBImpl::Put` 中,写操作的准备过程 `MakeRoomForWrite` 是我们需要注意的一个方法: + +```cpp +Status DBImpl::MakeRoomForWrite(bool force) { + uint64_t new_log_number = versions_->NewFileNumber(); + WritableFile* lfile = NULL; + env_->NewWritableFile(LogFileName(dbname_, new_log_number), &lfile); + + delete log_; + delete logfile_; + logfile_ = lfile; + logfile_number_ = new_log_number; + log_ = new log::Writer(lfile); + imm_ = mem_; + has_imm_.Release_Store(imm_); + mem_ = new MemTable(internal_comparator_); + mem_->Ref(); + MaybeScheduleCompaction(); + return Status::OK(); +} +``` + +当 LevelDB 中的 memtable 已经被数据填满导致内存已经快不够用的时候,我们会开始对 memtable 中的数据进行冻结并创建一个新的 `MemTable` 对象。 + +![Immutable-MemTable](images/leveldb-bigtable/Immutable-MemTable.jpg) + +你可以看到,与 Bigtable 中论文不同的是,LevelDB 中引入了一个不可变的 memtable 结构 imm,它的结构与 memtable 完全相同,只是其中的所有数据都是不可变的。 + +![LevelDB-Serving](images/leveldb-bigtable/LevelDB-Serving.jpg) + +在切换到新的 memtable 之后,还可能会执行 `MaybeScheduleCompaction` 来触发一次 Minor Compaction 将 imm 中数据固化成数据库中的 SSTable;imm 的引入能够解决由于 memtable 中数据过大导致压缩时不可写入数据的问题。 + +引入 imm 后,如果 memtable 中的数据过多,我们可以直接将 memtable 指针赋值给 imm,然后创建一个新的 MemTable 实例,这样就可以继续接受外界的写操作,不再需要等待 Minor Compaction 的结束了。 + +#### 日志记录的格式 + +作为一个持久存储的 KV 数据库,LevelDB 一定要有日志模块以支持错误发生时恢复数据,我们想要深入了解 LevelDB 的实现,那么日志的格式是一定绕不开的问题;这里并不打算展示用于追加日志的方法 `AddRecord` 的实现,因为方法中只是实现了对表头和字符串的拼接。 + +日志在 LevelDB 是以块的形式存储的,每一个块的长度都是 32KB,**固定的块长度**也就决定了日志可能存放在块中的任意位置,LevelDB 中通过引入一位 `RecordType` 来表示当前记录在块中的位置: + +```cpp +enum RecordType { + // Zero is reserved for preallocated files + kZeroType = 0, + kFullType = 1, + // For fragments + kFirstType = 2, + kMiddleType = 3, + kLastType = 4 +}; +``` + +日志记录的类型存储在该条记录的头部,其中还存储了 4 字节日志的 CRC 校验、记录的长度等信息: + +![LevelDB-log-format-and-recordtype](images/leveldb-bigtable/LevelDB-log-format-and-recordtype.jpg) + +上图中一共包含 4 个块,其中存储着 6 条日志记录,我们可以通过 `RecordType` 对每一条日志记录或者日志记录的一部分进行标记,并在日志需要使用时通过该信息重新构造出这条日志记录。 + +```cpp +virtual Status Sync() { + Status s = SyncDirIfManifest(); + if (fflush_unlocked(file_) != 0 || + fdatasync(fileno(file_)) != 0) { + s = Status::IOError(filename_, strerror(errno)); + } + return s; +} +``` + +因为向日志中写新记录都是顺序写的,所以它写入的速度非常快,当在内存中写入完成时,也会直接将缓冲区的这部分的内容 `fflush` 到磁盘上,实现对记录的持久化,用于之后的错误恢复等操作。 + +#### 记录的插入 + +当一条数据的记录写入日志时,这条记录仍然无法被查询,只有当该数据写入 memtable 后才可以被查询,而这也是这一节将要介绍的内容,无论是数据的插入还是数据的删除都会向 memtable 中添加一条记录。 + +![LevelDB-Memtable-Key-Value-Format](images/leveldb-bigtable/LevelDB-Memtable-Key-Value-Format.jpg) + +添加和删除的记录的区别就是它们使用了不用的 `ValueType` 标记,插入的数据会将其设置为 `kTypeValue`,删除的操作会标记为 `kTypeDeletion`;但是它们实际上都向 memtable 中插入了一条数据。 + +```cpp +virtual void Put(const Slice& key, const Slice& value) { + mem_->Add(sequence_, kTypeValue, key, value); + sequence_++; +} +virtual void Delete(const Slice& key) { + mem_->Add(sequence_, kTypeDeletion, key, Slice()); + sequence_++; +} +``` + +我们可以看到它们都调用了 memtable 的 `Add` 方法,向其内部的数据结构 skiplist 以上图展示的格式插入数据,这条数据中既包含了该记录的键值、序列号以及这条记录的种类,这些字段会在拼接后存入 skiplist;既然我们并没有在 memtable 中对数据进行删除,那么我们是如何保证每次取到的数据都是最新的呢?首先,在 skiplist 中,我们使用了自己定义的一个 `comparator`: + +```cpp +int InternalKeyComparator::Compare(const Slice& akey, const Slice& bkey) const { + int r = user_comparator_->Compare(ExtractUserKey(akey), ExtractUserKey(bkey)); + if (r == 0) { + const uint64_t anum = DecodeFixed64(akey.data() + akey.size() - 8); + const uint64_t bnum = DecodeFixed64(bkey.data() + bkey.size() - 8); + if (anum > bnum) { + r = -1; + } else if (anum < bnum) { + r = +1; + } + } + return r; +} +``` + +> 比较的两个 key 中的数据可能包含的内容都不完全相同,有的会包含键值、序列号等全部信息,但是例如从 `Get` 方法调用过来的 key 中可能就只包含键的长度、键值和序列号了,但是这并不影响这里对数据的提取,因为我们只从每个 key 的头部提取信息,所以无论是完整的 key/value 还是单独的 key,我们都不会取到 key 之外的任何数据。 + +该方法分别从两个不同的 key 中取出键和序列号,然后对它们进行比较;比较的过程就是使用 `InternalKeyComparator` 比较器,它通过 `user_key` 和 `sequence_number` 进行排序,其中 `user_key` 按照递增的顺序排序、`sequence_number` 按照递减的顺序排序,因为随着数据的插入序列号是不断递增的,所以我们可以保证先取到的都是最新的数据或者删除信息。 + +![LevelDB-MemTable-SkipList](images/leveldb-bigtable/LevelDB-MemTable-SkipList.jpg) + +在序列号的帮助下,我们并不需要对历史数据进行删除,同时也能加快写操作的速度,提升 LevelDB 的写性能。 + +### 数据的读取 + +从 LevelDB 中读取数据其实并不复杂,memtable 和 imm 更像是两级缓存,它们在内存中提供了更快的访问速度,如果能直接从内存中的这两处直接获取到响应的值,那么它们一定是最新的数据。 + +> LevelDB 总会将新的键值对写在最前面,并在数据压缩时删除历史数据。 + +![LevelDB-Read-Processes](images/leveldb-bigtable/LevelDB-Read-Processes.jpg) + +数据的读取是按照 MemTable、Immutable MemTable 以及不同层级的 SSTable 的顺序进行的,前两者都是在内存中,后面不同层级的 SSTable 都是以 `*.ldb` 文件的形式持久存储在磁盘上,而正是因为有着不同层级的 SSTable,所以我们的数据库的名字叫做 LevelDB。 + +精简后的读操作方法的实现代码是这样的,方法的脉络非常清晰,作者相信这里也不需要过多的解释: + +```cpp +Status DBImpl::Get(const ReadOptions& options, const Slice& key, std::string* value) { + LookupKey lkey(key, versions_->LastSequence()); + if (mem_->Get(lkey, value, NULL)) { + // Done + } else if (imm_ != NULL && imm_->Get(lkey, value, NULL)) { + // Done + } else { + versions_->current()->Get(options, lkey, value, NULL); + } + + MaybeScheduleCompaction(); + return Status::OK(); +} +``` + +当 LevelDB 在 memtable 和 imm 中查询到结果时,如果查询到了数据并不一定表示当前的值一定存在,它仍然需要判断 `ValueType` 来确定当前记录是否被删除。 + +#### 多层级的 SSTable + +当 LevelDB 在内存中没有找到对应的数据时,它才会到磁盘中多个层级的 SSTable 中进行查找,这个过程就稍微有一点复杂了,LevelDB 会在多个层级中逐级进行查找,并且不会跳过其中的任何层级;在查找的过程就涉及到一个非常重要的数据结构 `FileMetaData`: + +![FileMetaData](images/leveldb-bigtable/FileMetaData.jpg) + +`FileMetaData` 中包含了整个文件的全部信息,其中包括键的最大值和最小值、允许查找的次数、文件被引用的次数、文件的大小以及文件号,因为所有的 `SSTable` 都是以固定的形式存储在同一目录下的,所以我们可以通过文件号轻松查找到对应的文件。 + +![LevelDB-Level0-Laye](images/leveldb-bigtable/LevelDB-Level0-Layer.jpg) + +查找的顺序就是从低到高了,LevelDB 首先会在 Level0 中查找对应的键。但是,与其他层级不同,Level0 中多个 SSTable 的键的范围有重合部分的,在查找对应值的过程中,会依次查找 Level0 中固定的 4 个 SSTable。 + +![LevelDB-LevelN-Layers](images/leveldb-bigtable/LevelDB-LevelN-Layers.jpg) + +但是当涉及到更高层级的 SSTable 时,因为同一层级的 SSTable 都是没有重叠部分的,所以我们在查找时可以利用已知的 SSTable 中的极值信息 `smallest/largest` 快速查找到对应的 SSTable,再判断当前的 SSTable 是否包含查询的 key,如果不存在,就继续查找下一个层级直到最后的一个层级 `kNumLevels`(默认为 7 级)或者查询到了对应的值。 + +#### SSTable 的『合并』 + +既然 LevelDB 中的数据是通过多个层级的 SSTable 组织的,那么它是如何对不同层级中的 SSTable 进行合并和压缩的呢;与 Bigtable 论文中描述的两种 Compaction 几乎完全相同,LevelDB 对这两种压缩的方式都进行了实现。 + +无论是读操作还是写操作,在执行的过程中都可能调用 `MaybeScheduleCompaction` 来尝试对数据库中的 SSTable 进行合并,当合并的条件满足时,最终都会执行 `BackgroundCompaction` 方法在后台完成这个步骤。 + +![LevelDB-BackgroundCompaction-Processes](images/leveldb-bigtable/LevelDB-BackgroundCompaction-Processes.jpg) + +这种合并分为两种情况,一种是 Minor Compaction,即内存中的数据超过了 memtable 大小的最大限制,改 memtable 被冻结为不可变的 imm,然后执行方法 `CompactMemTable()` 对内存表进行压缩。 + +```cpp +void DBImpl::CompactMemTable() { + VersionEdit edit; + Version* base = versions_->current(); + WriteLevel0Table(imm_, &edit, base); + versions_->LogAndApply(&edit, &mutex_); + DeleteObsoleteFiles(); +} +``` + +`CompactMemTable` 会执行 `WriteLevel0Table` 将当前的 imm 转换成一个 Level0 的 SSTable 文件,同时由于 Level0 层级的文件变多,可能会继续触发一个新的 Major Compaction,在这里我们就需要在这里选择需要压缩的合适的层级: + +```cpp +Status DBImpl::WriteLevel0Table(MemTable* mem, VersionEdit* edit, Version* base) { + FileMetaData meta; + meta.number = versions_->NewFileNumber(); + Iterator* iter = mem->NewIterator(); + BuildTable(dbname_, env_, options_, table_cache_, iter, &meta); + + const Slice min_user_key = meta.smallest.user_key(); + const Slice max_user_key = meta.largest.user_key(); + int level = base->PickLevelForMemTableOutput(min_user_key, max_user_key); + edit->AddFile(level, meta.number, meta.file_size, meta.smallest, meta.largest); + return Status::OK(); +} +``` + +所有对当前 SSTable 数据的修改由一个统一的 `VersionEdit` 对象记录和管理,我们会在后面介绍这个对象的作用和实现,如果成功写入了就会返回这个文件的元数据 `FileMetaData`,最后调用 `VersionSet` 的方法 `LogAndApply` 将文件中的全部变化如实记录下来,最后做一些数据的清理工作。 + +当然如果是 Major Compaction 就稍微有一些复杂了,不过整理后的 `BackgroundCompaction` 方法的逻辑非常清晰: + +```cpp +void DBImpl::BackgroundCompaction() { + if (imm_ != NULL) { + CompactMemTable(); + return; + } + + Compaction* c = versions_->PickCompaction(); + CompactionState* compact = new CompactionState(c); + DoCompactionWork(compact); + CleanupCompaction(compact); + DeleteObsoleteFiles(); +} +``` + +我们从当前的 `VersionSet` 中找到需要压缩的文件信息,将它们打包存入一个 `Compaction` 对象,该对象需要选择两个层级的 SSTable,低层级的表很好选择,只需要选择大小超过限制的或者查询次数太多的 SSTable;当我们选择了低层级的一个 SSTable 后,就在更高的层级选择与该 SSTable 有重叠键的 SSTable 就可以了,通过 `FileMetaData` 中数据的帮助我们可以很快找到待压缩的全部数据。 + +> 查询次数太多的意思就是,当客户端调用多次 `Get` 方法时,如果这次 `Get` 方法在某个层级的 SSTable 中找到了对应的键,那么就算做上一层级中包含该键的 SSTable 的一次查找,也就是这次查找由于不同层级键的覆盖范围造成了更多的耗时,每个 SSTable 在创建之后的 `allowed_seeks` 都为 100 次,当 `allowed_seeks < 0` 时就会触发该文件的与更高层级和合并,以减少以后查询的查找次数。 + +![LevelDB-Pick-Compactions](images/leveldb-bigtable/LevelDB-Pick-Compactions.jpg) + +LevelDB 中的 `DoCompactionWork` 方法会对所有传入的 SSTable 中的键值使用归并排序进行合并,最后会在高高层级(图中为 Level2)中生成一个新的 SSTable。 + +![LevelDB-After-Compactions](images/leveldb-bigtable/LevelDB-After-Compactions.jpg) + +这样下一次查询 17~40 之间的值时就可以减少一次对 SSTable 中数据的二分查找以及读取文件的时间,提升读写的性能。 + +#### 存储 db 状态的 VersionSet + +LevelDB 中的所有状态其实都是被一个 `VersionSet` 结构所存储的,一个 `VersionSet` 包含一组 `Version` 结构体,所有的 `Version` 包括历史版本都是通过双向链表连接起来的,但是只有一个版本是当前版本。 + +![VersionSet-Version-And-VersionEdit](images/leveldb-bigtable/VersionSet-Version-And-VersionEdit.jpg) + +当 LevelDB 中的 SSTable 发生变动时,它会生成一个 `VersionEdit` 结构,最终执行 `LogAndApply` 方法: + +```cpp +Status VersionSet::LogAndApply(VersionEdit* edit, port::Mutex* mu) { + Version* v = new Version(this); + Builder builder(this, current_); + builder.Apply(edit); + builder.SaveTo(v); + + std::string new_manifest_file; + new_manifest_file = DescriptorFileName(dbname_, manifest_file_number_); + env_->NewWritableFile(new_manifest_file, &descriptor_file_); + + std::string record; + edit->EncodeTo(&record); + descriptor_log_->AddRecord(record); + descriptor_file_->Sync(); + + SetCurrentFile(env_, dbname_, manifest_file_number_); + AppendVersion(v); + + return Status::OK(); +} +``` + +该方法的主要工作是使用当前版本和 `VersionEdit` 创建一个新的版本对象,然后将 `Version` 的变更追加到 MANIFEST 日志中,并且改变数据库中全局当前版本信息。 + +> MANIFEST 文件中记录了 LevelDB 中所有层级中的表、每一个 SSTable 的 Key 范围和其他重要的元数据,它以日志的格式存储,所有对文件的增删操作都会追加到这个日志中。 + +#### SSTable 的格式 + +SSTable 中其实存储的不只是数据,其中还保存了一些元数据、索引等信息,用于加速读写操作的速度,虽然在 Bigtable 的论文中并没有给出 SSTable 的数据格式,不过在 LevelDB 的实现中,我们可以发现 SSTable 是以这种格式存储数据的: + +![SSTable-Format](images/leveldb-bigtable/SSTable-Format.jpg) + +当 LevelDB 读取 SSTable 存在的 `ldb` 文件时,会先读取文件中的 `Footer` 信息。 + +![SSTable-Foote](images/leveldb-bigtable/SSTable-Footer.jpg) + +整个 `Footer` 在文件中占用 48 个字节,我们能在其中拿到 MetaIndex 块和 Index 块的位置,再通过其中的索引继而找到对应值存在的位置。 + +`TableBuilder::Rep` 结构体中就包含了一个文件需要创建的全部信息,包括数据块、索引块等等: + +```cpp +struct TableBuilder::Rep { + WritableFile* file; + uint64_t offset; + BlockBuilder data_block; + BlockBuilder index_block; + std::string last_key; + int64_t num_entries; + bool closed; + FilterBlockBuilder* filter_block; + ... +} +``` + +到这里,我们就完成了对整个数据读取过程的解析了;对于读操作,我们可以理解为 LevelDB 在它内部的『多级缓存』中依次查找是否存在对应的键,如果存在就会直接返回,唯一与缓存不同可能就是,在数据『命中』后,它并不会把数据移动到更近的地方,而是会把数据移到更远的地方来减少下一次的访问时间,虽然这么听起来却是不可思议,不过仔细想一下确实是这样。 + +## 小结 + +在这篇文章中,我们通过对 LevelDB 源代码中读写操作的分析,了解了整个框架的绝大部分实现细节,包括 LevelDB 中存储数据的格式、多级 SSTable、如何进行合并以及管理版本等信息,不过由于篇幅所限,对于其中的一些问题并没有展开详细地进行介绍和分析,例如错误恢复以及缓存等问题;但是对 LevelDB 源代码的阅读,加深了我们对 Bigtable 论文中描述的分布式 KV 存储数据库的理解。 + +LevelDB 的源代码非常易于阅读,也是学习 C++ 语言非常优秀的资源,如果对文章的内容有疑问,可以翻墙后在博客下面的 Disqus 中留言。 + +## Reference + ++ [Bigtable: A Distributed Storage System for Structured Data](https://static.googleusercontent.com/media/research.google.com/en//archive/bigtable-osdi06.pdf) ++ [LevelDB](https://github.com/google/leveldb) ++ [The Chubby lock service for loosely-coupled distributed systems](https://static.googleusercontent.com/media/research.google.com/en//archive/chubby-osdi06.pdf) ++ [LevelDB · Impl](https://github.com/google/leveldb/blob/master/doc/impl.md) ++ [leveldb 中的 SSTable](http://bean-li.github.io/leveldb-sstable/) + + diff --git a/contents/Database/mongodb-to-mysql.md b/contents/Database/mongodb-to-mysql.md new file mode 100644 index 0000000..ab97b5e --- /dev/null +++ b/contents/Database/mongodb-to-mysql.md @@ -0,0 +1,441 @@ +# 如何从 MongoDB 迁移到 MySQL + +最近的一个多月时间其实都在做数据库的迁移工作,我目前在开发的项目其实在上古时代是使用 MySQL 作为主要数据库的,后来由于一些业务上的原因从 MySQL 迁移到了 MongoDB,使用了几个月的时间后,由于数据库服务非常不稳定,再加上无人看管,同时 MongoDB 本身就是无 Schema 的数据库,最后导致数据库的脏数据问题非常严重。目前团队的成员没有较为丰富的 Rails 开发经验,所以还是希望使用 ActiveRecord 加上 Migration 的方式对数据进行一些强限制,保证数据库中数据的合法。 + +![mysql-and-mongodb](images/mongodb-to-mysql/mysql-and-mongodb.png) + +文中会介绍作者在迁移数据库的过程中遇到的一些问题,并为各位读者提供需要**停机**迁移数据库的可行方案,如果需要不停机迁移数据库还是需要别的方案来解决,在这里提供的方案用于百万数据量的 MongoDB,预计的停机时间在两小时左右,如果数据量在千万级别以上,过长的停机时间可能是无法接受的,应该设计不停机的迁移方案;无论如何,作者希望这篇文章能够给想要做数据库迁移的开发者带来一些思路,少走一些坑。 + +## 从关系到文档 + +虽然这篇文章的重点是从 MongoDB 迁移到 MySQL,但是作者还是想简单提一下从 MySQL 到 MongoDB 的迁移,如果我们仅仅是将 MySQL 中的全部数据导入到 MongoDB 中其实是一间比较简单的事情,其中最重要的原因就是 **MySQL 支持的数据类型是 MongoDB 的子集**: + +![mongodb-mysql-datatype-relation](images/mongodb-to-mysql/mongodb-mysql-datatype-relation.png) + +在迁移的过程中可以将 MySQL 中的全部数据以 csv 的格式导出,然后再将所有 csv 格式的数据使用 `mongoimport` 全部导入到 MongoDB 中: + +``` +$ mysqldump -u<username> -p<password> \ + -T <output_directory> \ + --fields-terminated-by ',' \ + --fields-enclosed-by '\"' \ + --fields-escaped-by '\' \ + --no-create-info <database_name> + +$ mongoimport --db <database_name> --collection <collection_name> \ + --type csv \ + --file <data.csv> \ + --headerline +``` + +虽然整个过程看起来只需要两个命令非常简单,但是等到你真要去做的时候你会遇到非常多的问题,作者没有过从 MySQL 或者其他关系型数据库迁移到 MongoDB 的经验,但是 Google 上相关的资料特别多,所以这总是一个有无数前人踩过坑的问题,而前人的经验也能够帮助我们节省很多时间。 + +![mysql-to-mongodb](images/mongodb-to-mysql/mysql-to-mongodb.png) + +> 使用 csv 的方式导出数据在绝大多数的情况都不会出现问题,但是如果数据库中的某些文档中存储的是富文本,那么虽然在导出数据时不会出现问题,最终导入时可能出现一些比较奇怪的错误。 + +## 从文档到关系 + +相比于从 MySQL 到 MongoDB 的迁移,反向的迁移就麻烦了不止一倍,这主要是因为 MongoDB 中的很多数据类型和集合之间的关系在 MySQL 中都并不存在,比如嵌入式的数据结构、数组和哈希等集合类型、多对多关系的实现,很多的问题都不是仅仅能通过数据上的迁移解决的,我们需要在对数据进行迁移之前先对部分数据结构进行重构,本文中的后半部分会介绍需要处理的数据结构和逻辑。 + +![mongodb-mysql-problems-to-be-solved](images/mongodb-to-mysql/mongodb-mysql-problems-to-be-solved.png) + +当我们准备将数据库彻底迁移到 MySQL 之前,需要做一些准备工作,将最后迁移所需要的工作尽可能地减少,保证停机的时间不会太长,准备工作的目标就是尽量消灭工程中复杂的数据结构。 + +### 数据的预处理 + +在进行迁移之前要做很多准备工作,第一件事情是要把所有嵌入的数据结构改成非嵌入式的数据结构: + +![embedded-reference-documents](images/mongodb-to-mysql/embedded-reference-documents.png) + +也就是把所有 `embeds_many` 和 `embeds_one` 的关系都改成 `has_many` 和 `has_one`,同时将 `embedded_in` 都替换成 `belongs_to`,同时我们需要将工程中对应的测试都改成这种引用的关系,然而只改变代码中的关系并没有真正改变 MongoDB 中的数据。 + +```ruby +def embeds_many_to_has_many(parent, child) + child_key_name = child.to_s.underscore.pluralize + parent.collection.find({}).each do |parent_document| + next unless parent_document[child_key_name] + parent_document[child_key_name].each do |child_document| + new_child = child_document.merge "#{parent.to_s.underscore}_id": parent_document['_id'] + child.collection.insert_one new_child + end + end + parent.all.unset(child_key_name.to_sym) +end + +embeds_many_to_has_many(Person, Address) +``` + +我们可以使用上述的代码将关系为嵌入的模型都转换成引用,拍平所有复杂的数据关系,这段代码的运行时间与嵌入关系中的两个模型的数量有关,需要注意的是,MongoDB 中嵌入模型的数据可能因为某些原因出现相同的 `_id` 在插入时会发生冲突导致崩溃,你可以对 `insert_one` 使用 `resuce` 来保证这段代码的运行不会因为上述原因而停止。 + +![embedded-to-reference](images/mongodb-to-mysql/embedded-to-reference.png) + +通过这段代码我们就可以轻松将原有的嵌入关系全部展开变成引用的关系,将嵌入的关系变成引用除了做这两个改变之外,不需要做其他的事情,无论是数据的查询还是模型的创建都不需要改变代码的实现,不过记得为子模型中父模型的外键**添加索引**,否则会导致父模型在获取自己持有的全部子模型时造成**全表扫描**: + +```ruby +class Comment + include Mongoid::Document + index post_id: 1 + belongs_to :post +end +``` + +在处理了 MongoDB 中独有的嵌入式关系之后,我们就需要解决一些复杂的集合类型了,比如数组和哈希,如果我们使用 MySQL5.7 或者 PostgreSQL 的话,其实并不需要对他们进行处理,因为最新版本的 MySQL 和 PostgreSQL 已经提供了对 JSON 的支持,不过作者还是将项目中的数组和哈希都变成了常见的数据结构。 + +在这个可选的过程中,其实并没有什么标准答案,我们可以根据需要将不同的数据转换成不同的数据结构: + +![array-to-string-or-relation](images/mongodb-to-mysql/array-to-string-or-relation.png) + +比如,将数组变成字符串或者一对多关系,将哈希变成当前文档的键值对等等,如何处理这些集合数据其实都要看我们的业务逻辑,在改变这些字段的同时尽量为上层提供一个与原来直接 `.tags` 或者 `.categories` 结果相同的 API: + +```ruby +class Post + ... + def tag_titles + tags.map(&:title) + end + + def split_categories + categories.split(',') + end +end +``` + +这一步其实也是可选的,上述代码只是为了减少其他地方的修改负担,当然如果你想使用 MySQL5.7 或者 PostgreSQL 数据库对 JSON 的支持也没有什么太大的问题,只是在查询集合字段时有一些不方便。 + +### Mongoid 的『小兄弟』们 + +在使用 Mongoid 进行开发期间难免会用到一些相关插件,比如 [mongoid-enum](https://github.com/thetron/mongoid-enum)、[mongoid-slug](https://github.com/mongoid/mongoid-slug) 和 [mongoid-history](https://github.com/mongoid/mongoid-history) 等,这些插件的实现与 ActiveRecord 中具有相同功能的插件在实现上有很大的不同。 + +对于有些插件,比如 mongoid-slug 只是在引入插件的模型的文档中插入了 `_slugs` 字段,我们只需要在进行数据迁移忽略这些添加的字段并将所有的 `#slug` 方法改成 `#id`,不需要在预处理的过程中做其它的改变。而枚举的实现在 Mongoid 的插件和 ActiveRecord 中就截然不同了: + +![mongodb-mysql-enu](images/mongodb-to-mysql/mongodb-mysql-enum.png) + +mongoid-enum 使用字符串和 `_status` 来保存枚举类型的字段,而 ActiveRecord 使用整数和 `status` 表示枚举类型,两者在底层数据结构的存储上有一些不同,我们会在之后的迁移脚本中解决这个问题。 + +![mongoid-activerecord-enum](images/mongodb-to-mysql/mongoid-activerecord-enum.png) + +如果在项目中使用了很多 Mongoid 的插件,由于其实现不同,我们也只能根据不同的插件的具体实现来决定如何对其进行迁移,如果使用了一些支持特殊功能的插件可能很难在 ActiveRecord 中找到对应的支持,在迁移时可以考虑暂时将部分不重要的功能移除。 + +### 主键与 UUID + +我们希望从 MongoDB 迁移到 MySQL 的另一个重要原因就是 MongoDB 每一个文档的主键实在是太过冗长,一个 32 字节的 `_id` 无法给我们提供特别多的信息,只能增加我们的阅读障碍,再加上项目中并没有部署 MongoDB 集群,所以没能享受到用默认的 UUID 生成机制带来的好处。 + +![mongodb-mysql-id](images/mongodb-to-mysql/mongodb-mysql-id.png) + +我们不仅没有享受到 UUID 带来的有点,它还在迁移 MySQL 的过程中为我们带来了很大的麻烦,一方面是因为 ActiveRecord 的默认主键是整数,不支持 32 字节长度的 UUID,如果我们想要不改变 MongoDB 的 UUID,直接迁移到 MySQL 中使用其实也没有什么问题,只是我们要将默认的整数类型的主键变成字符串类型,同时要使用一个 UUID 生成器来保证所有的主键都是根据时间递增的并且不会冲突。 + +如果准备使用 UUID 加生成器的方式,其实会省去很多迁移的时间,不过看起来确实不是特别的优雅,如何选择还是要权衡和评估,但是如果我们选择了使用 `integer` 类型的自增主键时,就需要做很多额外的工作了,首先是为所有的表添加 `uuid` 字段,同时为所有的外键例如 `post_id` 创建对应的 `post_uuid` 字段,通过 `uuid` 将两者关联起来: + +![mysql-before-migrations](images/mongodb-to-mysql/mysql-before-migrations.png) + +在数据的迁移过程中,我们会将原有的 `_id` 映射到 `uuid` 中,`post_id` 映射到 `post_uuid` 上,我们通过保持 `uuid` 和 `post_uuid` 之间的关系保证模型之间的关系没有丢失,在迁移数据的过程中 `id` 和 `post_id` 是完全不存在任何联系的。 + +当我们按照 `_id` 的顺序遍历整个文档,将文档中的数据被插入到表中时,MySQL 会为所有的数据行自动生成的递增的主键 `id`,而 `post_id` 在这时都为空。 + +![mysql-after-migrations](images/mongodb-to-mysql/mysql-after-migrations.png) + +在全部的数据都被插入到 MySQL 之后,我们通过 `#find_by_uuid` 查询的方式将 `uuid` 和 `post_uuid` 中的关系迁移到 `id` 和 `post_id` 中,并将与 `uuid` 相关的字段全部删除,这样我们能够保证模型之间的关系不会消失,并且数据行的相对位置与迁移前完全一致。 + +### 代码的迁移 + +Mongoid 在使用时都是通过 `include` 将相关方法加载到当前模型中的,而 ActiveRecord 是通过继承 `ActiveRecord::Base` 的方式使用的,完成了对数据的预处理,我们就可以对现有模型层的代码进行修改了。 + +首先当然是更改模型的『父类』,把所有的 `Mongoid::Document` 都改成 `ActiveRecord::Base`,然后创建类对应的 Migration 迁移文件: + +```ruby +# app/models/post.rb +class Post < ActiveRecord::Base + validate_presence_of :title, :content +end + +# db/migrate/20170908075625_create_posts.rb +class CreatePosts < ActiveRecord::Migration[5.1] + def change + create_table :posts do |t| + t.string :title, null: false + t.text :content, null: false + t.string :uuid, null: false + + t.timestamps null: false + end + + add_index :posts, :uuid, unique: true + end +end +``` + +> 注意:要为每一张表添加类型为字符串的 `uuid` 字段,同时为 `uuid` 建立唯一索引,以加快通过 `uuid` 建立不同数据模型之间关系的速度。 + +除了建立数据库的迁移文件并修改基类,我们还需要修改一些 `include` 的模块和 Mongoid 中独有的查询,比如使用 `gte` 或者 `lte` 的日期查询和使用正则进行模式匹配的查询,这些查询在 ActiveRecord 中的使用方式与 Mongoid 中完全不同,我们需要通过手写 SQL 来解决这些问题。 + +![mongoid-to-activerecord-model-and-query](images/mongodb-to-mysql/mongoid-to-activerecord-model-and-query.png) + +除此之外,我们也需要处理一些复杂的模型关系,比如 Mongoid 中的 `inverse_of` 在 ActiveRecord 中叫做 `foreign_key` 等等,这些修改其实都并不复杂,只是如果想要将这部分的代码全部处理掉,就需要对业务逻辑进行详细地测试以保证不会有遗留的问题,这也就对我们项目的测试覆盖率有着比较高的要求了,不过我相信绝大多数的 Rails 工程都有着非常好的测试覆盖率,能够保证这一部分代码和逻辑能够顺利迁移,但是如果项目中完全没有测试或者测试覆盖率很低,就只能人肉进行测试或者自求多福了,或者**就别做迁移了,多写点测试再考虑这些重构的事情吧**。 + +### 数据的迁移 + +为每一个模型创建对应的迁移文件并建表其实一个不得不做的体力活,虽然有一些工作我们没法省略,但是我们可以考虑使用自动化的方式为所有的模型添加 `uuid` 字段和索引,同时也为类似 `post_id` 的字段添加相应的 `post_uuid` 列: + +```ruby +class AddUuidColumns < ActiveRecord::Migration[5.1] + def change + Rails.application.eager_load! + ActiveRecord::Base.descendants.map do |klass| + # add `uuid` column and create unique index on `uuid`. + add_column klass.table_name, :uuid, :string, unique: true + add_index klass.table_name, unique: true + + # add `xxx_uuid` columns, ex: `post_uuid`, `comment_uuid` and etc. + uuids = klass.attribute_names + .select { |attr| attr.include? '_id' } + .map { |attr| attr.gsub '_id', '_uuid' } + next unless uuids.present? + uuids.each do |uuid| + add_column klass.table_name, uuid, :string + end + end + end +end +``` + +在添加 `uuid` 列并建立好索引之后,我们就可以开始对数据库进行迁移了,如果我们决定在迁移的过程中改变原有数据的主键,那么我们会将迁移分成两个步骤,数据的迁移和关系的重建,前者仅指将 MongoDB 中的所有数据全部迁移到 MySQL 中对应的表中,并将所有的 `_id` 转换成 `uuid`、`xx_id` 转换成 `xx_uuid`,而后者就是前面提到的:通过 `uuid` 和 `xx_uuid` 的关联重新建立模型之间的关系并在最后删除所有的 `uuid` 字段。 + +我们可以使用如下的代码对数据进行迁移,这段代码从 MongoDB 中遍历某个集合 Collection 中的全部数据,然后将文档作为参数传入 block,然后再分别通过 `DatabaseTransformer#delete_obsolete_columns` 和 `DatabaseTransformer#update_rename_columns` 方法删除部分已有的列、更新一些数据列最后将所有的 `id` 列都变成 `uuid`: + +```ruby +module DatabaseTransformer + def import(collection_name, *obsolete_columns, **rename_columns) + collection = Mongoid::Clients.default.collections.select do |c| + c.namespace == "#{database}.#{collection_name.to_s.pluralize}" + end.first + + unless collection.present? + STDOUT.puts "#{collection_name.to_s.yellow}: skipped" + STDOUT.puts + return + end + + constant = collection_name.to_s.singularize.camelcase.constantize + reset_callbacks constant + + DatabaseTransformer.profiling do + collection_count = collection.find.count + collection.find.each_with_index do |document, index| + document = yield document if block_given? + delete_obsolete_columns document, obsolete_columns + update_rename_columns document, rename_columns + update_id_columns document + + insert_record constant, document + STDOUT.puts "#{index}/#{collection_count}\n" if (index % 1000).zero? + end + end + end +end +``` + +当完成了对文档的各种操作之后,该方法会直接调用 `DatabaseTransformer#insert_record` 将数据插入 MySQL 对应的表中;我们可以直接使用如下的代码将某个 Collection 中的全部文档迁移到 MySQL 中: + +```ruby +transformer = DatabaseTransformer.new 'draven_production' +transformer.import :post, :_slugs, name: :title, _status: :status +``` + +上述代码会在迁移时将集合每一个文档的 `_slugs` 字段全部忽略,同时将 `name` 重命名成 `title`、`_status` 重命名成 `status`,虽然作为枚举类型的字段 mongoid-enum 和 ActiveRecord 的枚举类型完全不同,但是在这里可以直接插入也没有什么问题,ActiveRecord 的模型在创建时会自己处理字符串和整数之间的转换: + +```ruby +def insert_record(constant, params) + model = constant.new params + model.save! validate: false +rescue Exception => exception + STDERR.puts "Import Error: #{exception}" + raise exception +end +``` + +为了加快数据的插入速度,同时避免所有由于插入操作带来的副作用,我们会在数据迁移期间重置所有的回调: + +```ruby +def reset_callbacks(constant) + %i(create save update).each do |callback| + constant.reset_callbacks callback + end +end +``` + +这段代码的作用仅在这个脚本运行的过程中才会生效,不会对工程中的其他地方造成任何的影响;同时,该脚本会在每 1000 个模型插入成功后向标准输出打印当前进度,帮助我们快速发现问题和预估迁移的时间。 + +> 你可以在 [database_transformer.rb](https://gist.github.com/Draveness/10476fe67a10128a37ba27a4c6967d07) 找到完整的数据迁移代码。 + +将所有的数据全部插入到 MySQL 的表之后,模型之间还没有任何显式的关系,我们还需要将通过 `uuid` 连接的模型转换成使用 `id` 的方式,对象之间的关系才能通过点语法直接访问,关系的建立其实非常简单,我们获得当前类所有结尾为 `_uuid` 的属性,然后遍历所有的数据行,根据 `uuid` 的值和 `post_uuid` 属性中的 "post" 部分获取到表名,最终得到对应的关联模型,在这里我们也处理了类似多态的特殊情况: + +```ruby +module RelationBuilder + def build_relations(class_name, polymorphic_associations = [], rename_associations = {}) + uuids = class_name.attribute_names.select { |name| name.end_with? '_uuid' } + + unless uuids.present? + STDOUT.puts "#{class_name.to_s.yellow}: skipped" + STDOUT.puts + return + end + + reset_callbacks class_name + + RelationBuilder.profiling do + models_count = class_name.count + class_name.unscoped.all.each_with_index do |model, index| + update_params = uuids.map do |uuid| + original_association_name = uuid[0...-5] + + association_model = association_model( + original_association_name, + model[uuid], + polymorphic_associations, + rename_associations + ) + + [original_association_name.to_s, association_model] + end.compact + + begin + Hash[update_params].each do |key, value| + model.send "#{key}=", value + end + model.save! validate: false + rescue Exception => e + STDERR.puts e + raise e + end + + STDOUT.puts "#{index}/#{models_count}\n" if (counter % 1000).zero? + end + end + end +end +``` + +在查找到对应的数据行之后就非常简单了,我们调用对应的 `post=` 等方法更新外键最后直接将外键的值保存到数据库中,与数据的迁移过程一样,我们在这段代码的执行过程中也会打印出当前的进度。 + +在初始化 `RelationBuilder` 时,如果我们传入了 `constants`,那么在调用 `RelationBuilder#build!` 时就会重建其中的全部关系,但是如果没有传入就会默认加载 ActiveRecord 中所有的子类,并去掉其中包含 `::` 的模型,也就是 ActiveRecord 中使用 `has_and_belongs_to_many` 创建的中间类,我们会在下一节中介绍如何单独处理多对多关系: + +```ruby +def initialize(constants = []) + if constants.present? + @constants = constants + else + Rails.application.eager_load! + @constants = ActiveRecord::Base.descendants + .reject { |constant| constant.to_s.include?('::') } + end +end +``` + +> 跟关系重建相关的代码可以在 [relation_builder.rb](https://gist.github.com/Draveness/c0798fb1272f483a176fa67741a3f1ee) 找到完整的用于关系迁移的代码。 + +```ruby +builder = RelationBuilder.new([Post, Comment]) +builder.build! +``` + +通过这数据迁移和关系重建两个步骤就已经可以解决绝大部分的数据迁移问题了,但是由于 MongoDB 和 ActiveRecord 中对于多对多关系的处理比较特殊,所以我们需要单独进行解决,如果所有的迁移问题到这里都已经解决了,那么我们就可以使用下面的迁移文件将数据库中与 `uuid` 有关的全部列都删除了: + +```ruby +class RemoveAllUuidColumns < ActiveRecord::Migration[5.1] + def change + Rails.application.eager_load! + ActiveRecord::Base.descendants.map do |klass| + attrs = klass.attribute_names.select { |n| n.include? 'uuid' } + next unless attrs.present? + remove_columns klass.table_name, *attrs + end + end +end +``` + +到这里位置整个迁移的过程就基本完成了,接下来就是跟整个迁移过程中有关的其他事项,例如:对多对关系、测试的重要性等话题。 + +### 多对多关系的处理 + +多对多关系在数据的迁移过程中其实稍微有一些复杂,在 Mongoid 中使用 `has_and_belongs_to_many` 会在相关的文档下添加一个 `tag_ids` 或者 `post_ids` 数组: + +```ruby +# The post document. +{ + "_id" : ObjectId("4d3ed089fb60ab534684b7e9"), + "tag_ids" : [ + ObjectId("4d3ed089fb60ab534684b7f2"), + ObjectId("4d3ed089fb60ab53468831f1") + ], + "title": "xxx", + "content": "xxx" +} +``` + +而 ActiveRecord 中会建立一张单独的表,表的名称是两张表名按照字母表顺序的拼接,如果是 `Post` 和 `Tag`,对应的多对多表就是 `posts_tags`,除了创建多对多表,`has_and_belongs_to_many` 还会创建两个 `ActiveRecord::Base` 的子类 `Tag::HABTM_Posts` 和 `Post::HABTM_Tags`,我们可以使用下面的代码简单实验一下: + +```ruby +require 'active_record' + +class Tag < ActiveRecord::Base; end +class Post < ActiveRecord::Base + has_and_belongs_to_many :tags +end +class Tag < ActiveRecord::Base + has_and_belongs_to_many :posts +end +puts ActiveRecord::Base.descendants +# => [Tag, Post, Post::HABTM_Tags, Tag::HABTM_Posts] +``` + +上述代码打印出了两个 `has_and_belongs_to_many` 生成的类 `Tag::HABTM_Posts` 和 `Post::HABTM_Tags`,它们有着完全相同的表 `posts_tags`,处理多对多关系时,我们只需要在使用 `DatabaseTransformer` 导入表中的所有的数据之后,再通过遍历 `posts_tags` 表中的数据更新多对多的关系表就可以了: + +```ruby +class PostsTag < ActiveRecord::Base; end + +# migrate data from mongodb to mysql. +transformer = DatabaseTransformer.new 'draven_production' +transformer.import :posts_tags + +# establish association between posts and tags. +PostsTag.unscoped.all.each do |model| + post = Post.find_by_uuid model.post_uuid + tag = Tag.find_by_uuid model.tag_uuid + next unless post.present? && tag.present? + model.update_columns post_id: post.id, tag_id: tag.id +end +``` + +所有使用 `has_and_belongs_to_many` 的多对多关系都需要通过上述代码进行迁移,这一步需要在删除数据库中的所有 `uuid` 字段之前完成。 + +### 测试的重要性 + +在真正对线上的服务进行停机迁移之前,我们其实需要对数据库已有的数据进行部分和全量测试,在部分测试阶段,我们可以在本地准备一个数据量为生产环境数据量 1/10 或者 1/100 的 MongoDB 数据库,通过在本地模拟 MongoDB 和 MySQL 的环境进行预迁移,确保我们能够尽快地发现迁移脚本中的错误。 + +![mongodb-pre-migration](images/mongodb-to-mysql/mongodb-pre-migration.png) + +准备测试数据库的办法是通过关系删除一些主要模型的数据行,在删除时可以通过 MongoDB 中的 `dependent: :destroy` 删除相关的模型,这样可以尽可能的保证数据的一致性和完整性,但是在对线上数据库进行迁移之前,我们依然需要对 MongoDB 中的全部数据进行全量的迁移测试,这样可以发现一些更加隐蔽的问题,保证真正上线时可以出现更少的状况。 + +数据库的迁移其实也属于重构,在进行 MongoDB 的数据库迁移之前一定要保证项目有着完善的测试体系和测试用例,这样才能让我们在项目重构之后,确定不会出现我们难以预料的问题,整个项目才是可控的,如果工程中没有足够的测试甚至没有测试,那么就不要再说重构这件事情了 -- **单元测试是重构的基础**。 + +## 总结 + +如何从 MongoDB 迁移到 MySQL 其实是一个工程问题,我们需要在整个过程中不断寻找可能出错的问题,将一个比较复杂的任务进行拆分,在真正做迁移之前尽可能地减少迁移对服务可用性以及稳定性带来的影响。 + +![mysql-and-mongodb-work-together](images/mongodb-to-mysql/mysql-and-mongodb-work-together.png) + +除此之外,MongoDB 和 MySQL 之间的选择也不一定是非此即彼,我们将项目中的大部分数据都迁移到了 MySQL 中,但是将一部分用于计算和分析的数据留在了 MongoDB,这样就可以保证 MongoDB 宕机之后仍然不会影响项目的主要任务,同时,MySQL 的备份和恢复速度也会因为数据库变小而非常迅速。 + +最后一点,测试真的很重要,如果没有测试,没有人能够做到在**修改大量的业务代码的过程中不丢失任何的业务逻辑**,甚至如果没有测试,很多业务逻辑可能在开发的那一天就已经丢失了。 + +如果对文章的内容有疑问或者有 MongoDB 迁移相关的问题,可以在评论中留言,评论系统使用 Disqus 需要梯子。 + +> 原文链接:[如何从 MongoDB 迁移到 MySQL · 面向信仰编程](https://draveness.me/mongodb-to-mysql.html) +> +> Follow: [Draveness · GitHub](https://github.com/Draveness) + +## Reference + ++ [How do I migrate data from a MongoDB to MySQL database? · Quora](https://www.quora.com/How-do-I-migrate-data-from-a-MongoDB-to-MySQL-database-Can-it-be-done-in-a-real-time-scenario-What-are-the-pros-and-cons-for-each-migration-Which-one-do-you-advice-What-is-your-experience-Any-reference-DB-expert-who-can-do-it) + diff --git a/contents/Database/mongodb-wiredtiger.md b/contents/Database/mongodb-wiredtiger.md new file mode 100644 index 0000000..5b099f6 --- /dev/null +++ b/contents/Database/mongodb-wiredtiger.md @@ -0,0 +1,220 @@ +# 『浅入浅出』MongoDB 和 WiredTiger + +![MongoDB-Covers](images/mongodb-wiredtiger/MongoDB-Covers.jpg) + +MongoDB 是目前主流的 NoSQL 数据库之一,与关系型数据库和其它的 NoSQL 不同,MongoDB 使用了面向文档的数据存储方式,将数据以类似 JSON 的方式存储在磁盘上,因为项目上的一些历史遗留问题,作者在最近的工作中也不得不经常与 MongoDB 打交道,这也是这篇文章出现的原因。 + +![logo](images/mongodb-wiredtiger/logo.png) + +虽然在之前也对 MongoDB 有所了解,但是真正在项目中大规模使用还是第一次,使用过程中也暴露了大量的问题,不过在这里,我们主要对 MongoDB 中的一些重要概念的原理进行介绍,也会与 MySQL 这种传统的关系型数据库做一个对比,让读者自行判断它们之间的优势和劣势。 + +## 概述 + +MongoDB 虽然也是数据库,但是它与传统的 RDBMS 相比有着巨大的不同,很多开发者都认为或者被灌输了一种思想,MongoDB 这种无 Scheme 的数据库相比 RDBMS 有着巨大的性能提升,这个判断其实是一种误解;因为数据库的性能不止与数据库本身的设计有关系,还与开发者对表结构和索引的设计、存储引擎的选择和业务有着巨大的关系,如果认为**仅进行了数据库的替换就能得到数量级的性能提升**,那还是太年轻了。 + +![its-not-always-simple-banner](images/mongodb-wiredtiger/its-not-always-simple-banner.jpg) + +### 架构 + +现有流行的数据库其实都有着非常相似的架构,MongoDB 其实就与 MySQL 中的架构相差不多,底层都使用了『可插拔』的存储引擎以满足用户的不同需要。 + +![MongoDB-Architecture](images/mongodb-wiredtiger/MongoDB-Architecture.jpg) + +用户可以根据表中的数据特征选择不同的存储引擎,它们可以在同一个 MongoDB 的实例中使用;在最新版本的 MongoDB 中使用了 WiredTiger 作为默认的存储引擎,WiredTiger 提供了不同粒度的并发控制和压缩机制,能够为不同种类的应用提供了最好的性能和存储效率。 + +在不同的存储引擎上层的就是 MongoDB 的数据模型和查询语言了,与关系型数据库不同,由于 MongoDB 对数据的存储与 RDBMS 有较大的差异,所以它创建了一套不同的查询语言;虽然 MongoDB 查询语言非常强大,支持的功能也很多,同时也是可编程的,不过其中包含的内容非常繁杂、API 设计也不是非常优雅,所以还是需要一些学习成本的,对于长时间使用 MySQL 的开发者肯定会有些不习惯。 + +```js +db.collection.updateMany( + <filter>, + <update>, + { + upsert: <boolean>, + writeConcern: <document>, + collation: <document> + } +) +``` + +查询语言的复杂是因为 MongoDB 支持了很多的数据类型,同时每一条数据记录也就是文档有着非常复杂的结构,这点是从设计上就没有办法避免的,所以还需要使用 MongoDB 的开发者花一些时间去学习各种各样的 API。 + +### RDBMS 与 MongoDB + +MongoDB 使用面向文档的的数据模型,导致很多概念都与 RDBMS 有一些差别,虽然从总体上来看两者都有相对应的概念,不过概念之间细微的差别其实也会影响我们对 MongoDB 的理解: + +![Translating-Between-RDBMS-and-MongoDB](images/mongodb-wiredtiger/Translating-Between-RDBMS-and-MongoDB.jpg) + +传统的 RDBMS 其实使用 `Table` 的格式将数据逻辑地存储在一张二维的表中,其中不包括任何复杂的数据结构,但是由于 MongoDB 支持嵌入文档、数组和哈希等多种复杂数据结构的使用,所以它最终将所有的数据以 [BSON](http://bsonspec.org) 的数据格式存储起来。 + +RDBMS 和 MongoDB 中的概念都有着相互对应的关系,数据库、表、行和索引的概念在两中数据库中都非常相似,唯独最后的 `JOIN` 和 `Embedded Document` 或者 `Reference` 有着巨大的差别。这一点差别其实也影响了在使用 MongoDB 时对集合(Collection)Schema 的设计,如果我们在 MongoDB 中遵循了与 RDBMS 中相同的思想对 Collection 进行设计,那么就不可避免的使用很多的 "JOIN" 语句,而 MongoDB 是不支持 "JOIN" 的,在应用内做这种查询的性能非常非常差,在这时使用嵌入式的文档其实就可以解决这种问题了,嵌入式的文档虽然可能会造成很多的数据冗余导致我们在更新时会很痛苦,但是查询时确实非常迅速。 + +```js +{ + _id: <ObjectId1>, + name: "draveness", + books: [ + { + _id: <ObjectId2>, + name: "MongoDB: The Definitive Guide" + }, + { + _id: <ObjectId3>, + name: "High Performance MySQL" + } + ] +} +``` + +在 MongoDB 的使用时,我们一定要忘记很多 RDBMS 中对于表设计的规则,同时想清楚 MongoDB 的优势,仔细思考如何对表进行设计才能利用 MongoDB 提供的诸多特性提升查询的效率。 + +## 数据模型 + +MongoDB 与 RDBMS 之间最大的不同,就是数据模型的设计有着非常明显的差异,数据模型的不同决定了它有着非常不同的特性,存储在 MongoDB 中的数据有着非常灵活的 Schema,我们不需要像 RDBMS 一样,在插入数据之前就决定并且定义表中的数据结构,MongoDB 的结合不对 Collection 的数据结构进行任何限制,但是在实际使用中,同一个 Collection 中的大多数文档都具有类似的结构。 + +![Different-Data-Structure](images/mongodb-wiredtiger/Different-Data-Structure.jpg) + +在为 MongoDB 应用设计数据模型时,如何表示数据模型之间的关系其实是需要开发者需要仔细考虑的,MongoDB 为表示文档之间的关系提供了两种不同的方法:引用和嵌入。 + +### 标准化数据模型 + +引用(Reference)在 MongoDB 中被称为标准化的数据模型,它与 MySQL 的外键非常相似,每一个文档都可以通过一个 `xx_id` 的字段『链接』到其他的文档: + +![Reference-MongoDB](images/mongodb-wiredtiger/Reference-MongoDB.jpg) + +但是 MongoDB 中的这种引用不像 MySQL 中可以直接通过 JOIN 进行查找,我们需要使用额外的查询找到该引用对应的模型,这虽然提供了更多的灵活性,不过由于增加了客户端和 MongoDB 之间的交互次数(Round-Trip)也会导致查询变慢,甚至非常严重的性能问题。 + +MongoDB 中的引用并不会对引用对应的数据模型是否真正存在做出任何的约束,所以如果在应用层级没有对文档之间的关系有所约束,那么就可能会出现引用了指向不存在的文档的问题: + +![Not-Found-Document](images/mongodb-wiredtiger/Not-Found-Document.jpg) + +虽然引用有着比较严重的性能问题并且在数据库层面没有对模型是否被删除加上限制,不过它提供的一些特点是嵌入式的文档无法给予了,当我们需要表示多对多关系或者更加庞大的数据集时,就可以考虑使用标准化的数据模型 — 引用了。 + +### 嵌入式数据模型 + +除了与 MySQL 中非常相似的引用,MongoDB 由于其独特的数据存储方式,还提供了嵌入式的数据模型,嵌入式的数据模型也被认为是不标准的数据模型: + +![Embedded-Data-Models-MongoDB](images/mongodb-wiredtiger/Embedded-Data-Models-MongoDB.jpg) + +因为 MongoDB 使用 BSON 的数据格式对数据进行存储,而嵌入式数据模型中的子文档其实就是父文档中的另一个值,只是其中存储的是一个对象: + +```javascript +{ + _id: <ObjectId1>, + username: "draveness", + age: 20, + contact: [ + { + _id: <ObjectId2>, + email: "i@draveness.me" + } + ] +} +``` + +嵌入式的数据模型允许我们将有相同的关系的信息存储在同一个数据记录中,这样应用就可以更快地对相关的数据进行查询和更新了;当我们的数据模型中有『包含』这样的关系或者模型经常需要与其他模型一起出现(查询)时,比如文章和评论,那么就可以考虑使用嵌入式的关系对数据模型进行设计。 + +总而言之,嵌入的使用让我们在更少的请求中获得更多的相关数据,能够为读操作提供更高的性能,也为在同一个写请求中同时更新相关数据提供了支持。 + +> MongoDB 底层的 WiredTiger 存储引擎能够保证对于同一个文档的操作都是原子的,任意一个写操作都不能原子性地影响多个文档或者多个集合。 + +## 主键和索引 + +在这一节中,我们将主要介绍 MongoDB 中不同类型的索引,当然也包括每个文档中非常重要的字段 `_id`,可以**理解**为 MongoDB 的『主键』,除此之外还会介绍单字段索引、复合索引以及多键索引等类型的索引。 + +MongoDB 中索引的概念其实与 MySQL 中的索引相差不多,无论是底层的数据结构还是基本的索引类型都几乎完全相同,两者之间的区别就在于因为 MongoDB 支持了不同类型的数据结构,所以也理所应当地提供了更多的索引种类。 + +![MongoDB-Indexes](images/mongodb-wiredtiger/MongoDB-Indexes.jpg) + +### 默认索引 + +MySQL 中的每一个数据行都具有一个主键,数据库中的数据都是按照以主键作为键物理地存储在文件中的;除了用于数据的存储,主键由于其特性也能够加速数据库的查询语句。 + +而 MongoDB 中所有的文档也都有一个唯一的 `_id` 字段,在默认情况下所有的文档都使用一个长 12 字节的 `ObjectId` 作为默认索引: + +![MongoDB-ObjectId](images/mongodb-wiredtiger/MongoDB-ObjectId.jpg) + +前四位代表当前 `_id` 生成时的 Unix 时间戳,在这之后是三位的机器标识符和两位的处理器标识符,最后是一个三位的计数器,初始值就是一个随机数;通过这种方式代替递增的 `id` 能够解决分布式的 MongoDB 生成唯一标识符的问题,同时可以在一定程度上保证 `id` 的的增长是递增的。 + +### 单字段索引(Single Field) + +除了 MongoDB 提供的默认 `_id` 字段之外,我们还可以建立其它的单键索引,而且其中不止支持顺序的索引,还支持对索引倒排: + +```javasciprt +db.users.createIndex( { age: -1 } ) +``` + +MySQL8.0 之前的索引都只能是正序排列的,在 8.0 之后才引入了逆序的索引,单一字段索引可以说是 MySQL 中的辅助(Secondary)索引的一个子集,它只是对除了 `_id` 外的任意单一字段建立起正序或者逆序的索引树。 + +![Single-Field-Index](images/mongodb-wiredtiger/Single-Field-Index.jpg) + +### 复合索引(Compound) + +除了单一字段索引这种非常简单的索引类型之外,MongoDB 还支持多个不同字段组成的复合索引(Compound Index),由于 MongoDB 中支持对同一字段的正逆序排列,所以相比于 MySQL 中的辅助索引就会出现更多的情况: + +```javascript +db.users.createIndex( { username: 1, age: -1 } ) +db.users.createIndex( { username: 1, age: 1 } ) +``` + +上面的两个索引是完全不同的,在磁盘上的 B+ 树其实也按照了完全不同的顺序进行存储,虽然 `username` 字段都是升序排列的,但是对于 `age` 来说,两个索引的处理是完全相反的: + +![Compound-Index](images/mongodb-wiredtiger/Compound-Index.jpg) + +这也就造成了在使用查询语句对集合中数据进行查找时,如果约定了正逆序,那么其实是会使用不同的索引的,所以在索引创建时一定要考虑好使用的场景,避免创建无用的索引。 + +### 多键索引(Multikey) + +由于 MongoDB 支持了类似数组的数据结构,所以也提供了名为多键索引的功能,可以将数组中的每一个元素进行索引,索引的创建其实与单字段索引没有太多的区别: + +```javascript +db.collection.createIndex( { address: 1 } ) +``` + +如果一个字段是值是数组,那么在使用上述代码时会自动为这个字段创建一个多键索引,能够加速对数组中元素的查找。 + +### 文本索引(Text) + +文本索引是 MongoDB 为我们提供的另一个比较实用的功能,不过在这里也只是对这种类型的索引提一下,也不打算深入去谈谈这东西的性能如何,如果真的要做全文索引的话,还是推荐使用 Elasticsearch 这种更专业的东西来做,而不是使用 MongoDB 提供的这项功能。 + +## 存储 + +如何存储数据就是一个比较重要的问题,在前面我们已经提到了 MongoDB 与 MySQL 一样都提供了插件化的存储引擎支持,作为 MongoDB 的主要组件之一,存储引擎全权负责了 MongoDB 对数据的管理。 + +![Multiple-Storage-Engines](images/mongodb-wiredtiger/Multiple-Storage-Engines.jpg) + +### WiredTiger + +MongoDB3.2 之后 WiredTiger 就是默认的存储引擎了,如果对各个存储引擎并不了解,那么还是不要改变 MongoDB 的默认存储引擎;它有着非常多的优点,比如拥有效率非常高的缓存机制: + +![WiredTiger-Cache](images/mongodb-wiredtiger/WiredTiger-Cache.jpg) + +WiredTiger 还支持在内存中和磁盘上对索引进行压缩,在压缩时也使用了前缀压缩的方式以减少 RAM 的使用,在后面的文章中我们会详细介绍和分析 WiredTiger 存储引擎是如何对各种数据进行存储的。 + +### Journaling + +为了在数据库宕机保证 MongoDB 中数据的持久性,MongoDB 使用了 Write Ahead Logging 向磁盘上的 journal 文件预先进行写入;除了 journal 日志,MongoDB 还使用检查点(Checkpoint)来保证数据的一致性,当数据库发生宕机时,我们就需要 Checkpoint 和 journal 文件协作完成数据的恢复工作: + +1. 在数据文件中查找上一个检查点的标识符; +2. 在 journal 文件中查找标识符对应的记录; +3. 重做对应记录之后的全部操作; + +MongoDB 会每隔 60s 或者在 journal 数据的写入达到 2GB 时设置一次检查点,当然我们也可以通过在写入时传入 `j: true` 的参数强制 journal 文件的同步。 + +![Checkpoints-Conditions](images/mongodb-wiredtiger/Checkpoints-Conditions.jpg) + +这篇文章并不会介绍 Journal 文件的格式以及相关的内容,作者可能会在之后介绍分析 WiredTiger 的文章中简单分析其存储格式以及一些其它特性。 + +## 总结 + +这篇文章中只是对 MongoDB 的一些基本特性以及数据模型做了简单的介绍,虽然『无限』扩展是 MongoDB 非常重要的特性,但是由于篇幅所限,我们并没有介绍任何跟 MongoDB 集群相关的信息,不过会在之后的文章中专门介绍多实例的 MongoDB 是如何协同工作的。 + +在这里,我想说的是,如果各位读者接收到了类似 MongoDB 比 MySQL 性能好很多的断言,但是在使用 MongoDB 的过程中仍然遵循以往 RDBMS 对数据库的设计方式,那么我相信性能在最终也不会有太大的提升,反而可能会不升反降;只有真正理解 MongoDB 的数据模型,并且根据业务的需要进行设计才能很好地利用类似嵌入式文档等特性并提升 MongoDB 的性能。 + +## References + ++ [MongoDB Architecture](https://www.mongodb.com/mongodb-architecture) ++ [Thinking in Documents: Part 1](https://www.mongodb.com/blog/post/thinking-documents-part-1?jmp=docs) ++ [DB-Engines Ranking](https://db-engines.com/en/ranking) ++ [Data Modeling Introduction](https://docs.mongodb.com/manual/core/data-modeling-introduction/) ++ [Building Applications with MongoDB's Pluggable Storage Engines: Part 1](https://www.mongodb.com/blog/post/building-applications-with-mongodbs-pluggable-storage-engines-part-1?jmp=docs) + diff --git a/contents/Database/mysql.md b/contents/Database/mysql.md new file mode 100644 index 0000000..2577d9c --- /dev/null +++ b/contents/Database/mysql.md @@ -0,0 +1,382 @@ +# 『浅入浅出』MySQL 和 InnoDB + +作为一名开发人员,在日常的工作中会难以避免地接触到数据库,无论是基于文件的 sqlite 还是工程上使用非常广泛的 MySQL、PostgreSQL,但是一直以来也没有对数据库有一个非常清晰并且成体系的认知,所以最近两个月的时间看了几本数据库相关的书籍并且阅读了 MySQL 的官方文档,希望对各位了解数据库的、不了解数据库的有所帮助。 + +![mysql](images/mysql/mysql.png) + + +本文中对于数据库的介绍以及研究都是在 MySQL 上进行的,如果涉及到了其他数据库的内容或者实现会在文中单独指出。 + +## 数据库的定义 + +很多开发者在最开始时其实都对数据库有一个比较模糊的认识,觉得数据库就是一堆数据的集合,但是实际却比这复杂的多,数据库领域中有两个词非常容易混淆,也就是*数据库*和*实例*: + ++ 数据库:物理操作文件系统或其他形式文件类型的集合; ++ 实例:MySQL 数据库由后台线程以及一个共享内存区组成; + +> 对于数据库和实例的定义都来自于 [MySQL 技术内幕:InnoDB 存储引擎](https://book.douban.com/subject/24708143/) 一书,想要了解 InnoDB 存储引擎的读者可以阅读这本书籍。 + +### 数据库和实例 + +在 MySQL 中,实例和数据库往往都是一一对应的,而我们也无法直接操作数据库,而是要通过数据库实例来操作数据库文件,可以理解为数据库实例是数据库为上层提供的一个专门用于操作的接口。 + +![Database - Instance](images/mysql/Database%20-%20Instance.jpg) + +在 Unix 上,启动一个 MySQL 实例往往会产生两个进程,`mysqld` 就是真正的数据库服务守护进程,而 `mysqld_safe` 是一个用于检查和设置 `mysqld` 启动的控制程序,它负责监控 MySQL 进程的执行,当 `mysqld` 发生错误时,`mysqld_safe` 会对其状态进行检查并在合适的条件下重启。 + +### MySQL 的架构 + +MySQL 从第一个版本发布到现在已经有了 20 多年的历史,在这么多年的发展和演变中,整个应用的体系结构变得越来越复杂: + +![Logical-View-of-MySQL-Architecture](images/mysql/Logical-View-of-MySQL-Architecture.jpg) + +最上层用于连接、线程处理的部分并不是 MySQL 『发明』的,很多服务都有类似的组成部分;第二层中包含了大多数 MySQL 的核心服务,包括了对 SQL 的解析、分析、优化和缓存等功能,存储过程、触发器和视图都是在这里实现的;而第三层就是 MySQL 中真正负责数据的存储和提取的存储引擎,例如:[InnoDB](https://en.wikipedia.org/wiki/InnoDB)、[MyISAM](https://en.wikipedia.org/wiki/MyISAM) 等,文中对存储引擎的介绍都是对 InnoDB 实现的分析。 + +## 数据的存储 + +在整个数据库体系结构中,我们可以使用不同的存储引擎来存储数据,而绝大多数存储引擎都以二进制的形式存储数据;这一节会介绍 InnoDB 中对数据是如何存储的。 + +在 InnoDB 存储引擎中,所有的数据都被**逻辑地**存放在表空间中,表空间(tablespace)是存储引擎中最高的存储逻辑单位,在表空间的下面又包括段(segment)、区(extent)、页(page): + +![Tablespace-segment-extent-page-row](images/mysql/Tablespace-segment-extent-page-row.jpg) + +同一个数据库实例的所有表空间都有相同的页大小;默认情况下,表空间中的页大小都为 16KB,当然也可以通过改变 `innodb_page_size` 选项对默认大小进行修改,需要注意的是不同的页大小最终也会导致区大小的不同: + +![Relation Between Page Size - Extent Size](images/mysql/Relation%20Between%20Page%20Size%20-%20Extent%20Size.png) + +从图中可以看出,在 InnoDB 存储引擎中,一个区的大小最小为 1MB,页的数量最少为 64 个。 + +### 如何存储表 + +MySQL 使用 InnoDB 存储表时,会将**表的定义**和**数据索引**等信息分开存储,其中前者存储在 `.frm` 文件中,后者存储在 `.ibd` 文件中,这一节就会对这两种不同的文件分别进行介绍。 + +![frm-and-ibd-file](images/mysql/frm-and-ibd-file.jpg) + +#### .frm 文件 + +无论在 MySQL 中选择了哪个存储引擎,所有的 MySQL 表都会在硬盘上创建一个 `.frm` 文件用来描述表的格式或者说定义;`.frm` 文件的格式在不同的平台上都是相同的。 + +```sql +CREATE TABLE test_frm( + column1 CHAR(5), + column2 INTEGER +); +``` + +当我们使用上面的代码创建表时,会在磁盘上的 `datadir` 文件夹中生成一个 `test_frm.frm` 的文件,这个文件中就包含了表结构相关的信息: + +![frm-file-hex](images/mysql/frm-file-hex.png) + +> MySQL 官方文档中的 [11.1 MySQL .frm File Format](https://dev.mysql.com/doc/internals/en/frm-file-format.html) 一文对于 `.frm` 文件格式中的二进制的内容有着非常详细的表述,在这里就不展开介绍了。 + +#### .ibd 文件 + +InnoDB 中用于存储数据的文件总共有两个部分,一是系统表空间文件,包括 `ibdata1`、`ibdata2` 等文件,其中存储了 InnoDB 系统信息和用户数据库表数据和索引,是所有表公用的。 + +当打开 `innodb_file_per_table` 选项时,`.ibd` 文件就是每一个表独有的表空间,文件存储了当前表的数据和相关的索引数据。 + +### 如何存储记录 + +与现有的大多数存储引擎一样,InnoDB 使用页作为磁盘管理的最小单位;数据在 InnoDB 存储引擎中都是按行存储的,每个 16KB 大小的页中可以存放 2-200 行的记录。 + +当 InnoDB 存储数据时,它可以使用不同的行格式进行存储;MySQL 5.7 版本支持以下格式的行存储方式: + +![Antelope-Barracuda-Row-Format](images/mysql/Antelope-Barracuda-Row-Format.jpg) + +> Antelope 是 InnoDB 最开始支持的文件格式,它包含两种行格式 Compact 和 Redundant,它最开始并没有名字;Antelope 的名字是在新的文件格式 Barracuda 出现后才起的,Barracuda 的出现引入了两种新的行格式 Compressed 和 Dynamic;InnoDB 对于文件格式都会向前兼容,而官方文档中也对之后会出现的新文件格式预先定义好了名字:Cheetah、Dragon、Elk 等等。 + +两种行记录格式 Compact 和 Redundant 在磁盘上按照以下方式存储: + +![COMPACT-And-REDUNDANT-Row-Format](images/mysql/COMPACT-And-REDUNDANT-Row-Format.jpg) + +Compact 和 Redundant 格式最大的不同就是记录格式的第一个部分;在 Compact 中,行记录的第一部分倒序存放了一行数据中列的长度(Length),而 Redundant 中存的是每一列的偏移量(Offset),从总体上上看,Compact 行记录格式相比 Redundant 格式能够减少 20% 的存储空间。 + +#### 行溢出数据 + +当 InnoDB 使用 Compact 或者 Redundant 格式存储极长的 VARCHAR 或者 BLOB 这类大对象时,我们并不会直接将所有的内容都存放在数据页节点中,而是将行数据中的前 768 个字节存储在数据页中,后面会通过偏移量指向溢出页。 + +![Row-Overflo](images/mysql/Row-Overflow.jpg) + +但是当我们使用新的行记录格式 Compressed 或者 Dynamic 时都只会在行记录中保存 20 个字节的指针,实际的数据都会存放在溢出页面中。 + +![Row-Overflow-in-Barracuda](images/mysql/Row-Overflow-in-Barracuda.jpg) + +当然在实际存储中,可能会对不同长度的 TEXT 和 BLOB 列进行优化,不过这就不是本文关注的重点了。 + +> 想要了解更多与 InnoDB 存储引擎中记录的数据格式的相关信息,可以阅读 [InnoDB Record Structure](https://dev.mysql.com/doc/internals/en/innodb-record-structure.html) + +### 数据页结构 + +页是 InnoDB 存储引擎管理数据的最小磁盘单位,而 B-Tree 节点就是实际存放表中数据的页面,我们在这里将要介绍页是如何组织和存储记录的;首先,一个 InnoDB 页有以下七个部分: + +![InnoDB-B-Tree-Node](images/mysql/InnoDB-B-Tree-Node.jpg) + +每一个页中包含了两对 header/trailer:内部的 Page Header/Page Directory 关心的是页的状态信息,而 Fil Header/Fil Trailer 关心的是记录页的头信息。 + +在页的头部和尾部之间就是用户记录和空闲空间了,每一个数据页中都包含 Infimum 和 Supremum 这两个**虚拟**的记录(可以理解为占位符),Infimum 记录是比该页中任何主键值都要小的值,Supremum 是该页中的最大值: + +![Infimum-Rows-Supremum](images/mysql/Infimum-Rows-Supremum.jpg) + +User Records 就是整个页面中真正用于存放行记录的部分,而 Free Space 就是空余空间了,它是一个链表的数据结构,为了保证插入和删除的效率,整个页面并不会按照主键顺序对所有记录进行排序,它会自动从左侧向右寻找空白节点进行插入,行记录在物理存储上并不是按照顺序的,它们之间的顺序是由 `next_record` 这一指针控制的。 + +B+ 树在查找对应的记录时,并不会直接从树中找出对应的行记录,它只能获取记录所在的页,将整个页加载到内存中,再通过 Page Directory 中存储的稀疏索引和 `n_owned`、`next_record` 属性取出对应的记录,不过因为这一操作是在内存中进行的,所以通常会忽略这部分查找的耗时。 + +InnoDB 存储引擎中对数据的存储是一个非常复杂的话题,这一节中也只是对表、行记录以及页面的存储进行一定的分析和介绍,虽然作者相信这部分知识对于大部分开发者已经足够了,但是想要真正消化这部分内容还需要很多的努力和实践。 + +## 索引 + +索引是数据库中非常非常重要的概念,它是存储引擎能够快速定位记录的秘密武器,对于提升数据库的性能、减轻数据库服务器的负担有着非常重要的作用;**索引优化是对查询性能优化的最有效手段**,它能够轻松地将查询的性能提高几个数量级。 + +### 索引的数据结构 + +在上一节中,我们谈了行记录的存储和页的存储,在这里我们就要从更高的层面看 InnoDB 中对于数据是如何存储的;InnoDB 存储引擎在绝大多数情况下使用 B+ 树建立索引,这是关系型数据库中查找最为常用和有效的索引,但是 B+ 树索引并不能找到一个给定键对应的具体值,它只能找到数据行对应的页,然后正如上一节所提到的,数据库把整个页读入到内存中,并在内存中查找具体的数据行。 + +![B+Tree](images/mysql/B+Tree.jpg) + +B+ 树是平衡树,它查找任意节点所耗费的时间都是完全相同的,比较的次数就是 B+ 树的高度;在这里,我们并不会深入分析或者动手实现一个 B+ 树,只是对它的特性进行简单的介绍。 + +### 聚集索引和辅助索引 + +数据库中的 B+ 树索引可以分为聚集索引(clustered index)和辅助索引(secondary index),它们之间的最大区别就是,聚集索引中存放着一条行记录的全部信息,而辅助索引中只包含索引列和一个用于查找对应行记录的『书签』。 + +#### 聚集索引 + +InnoDB 存储引擎中的表都是使用索引组织的,也就是按照键的顺序存放;聚集索引就是按照表中主键的顺序构建一颗 B+ 树,并在叶节点中存放表中的行记录数据。 + +```sql +CREATE TABLE users( + id INT NOT NULL, + first_name VARCHAR(20) NOT NULL, + last_name VARCHAR(20) NOT NULL, + age INT NOT NULL, + PRIMARY KEY(id), + KEY(last_name, first_name, age) + KEY(first_name) +); +``` + +如果使用上面的 SQL 在数据库中创建一张表,B+ 树就会使用 `id` 作为索引的键,并在叶子节点中存储一条记录中的**所有**信息。 + +![Clustered-Index](images/mysql/Clustered-Index.jpg) + +> 图中对 B+ 树的描述与真实情况下 B+ 树中的数据结构有一些差别,不过这里想要表达的主要意思是:聚集索引叶节点中保存的是整条行记录,而不是其中的一部分。 + +聚集索引与表的物理存储方式有着非常密切的关系,所有正常的表应该**有且仅有一个**聚集索引(绝大多数情况下都是主键),表中的所有行记录数据都是按照**聚集索引**的顺序存放的。 + +当我们使用聚集索引对表中的数据进行检索时,可以直接获得聚集索引所对应的整条行记录数据所在的页,不需要进行第二次操作。 + +#### 辅助索引 + +数据库将所有的非聚集索引都划分为辅助索引,但是这个概念对我们理解辅助索引并没有什么帮助;辅助索引也是通过 B+ 树实现的,但是它的叶节点并不包含行记录的全部数据,仅包含索引中的所有键和一个用于查找对应行记录的『书签』,在 InnoDB 中这个书签就是当前记录的主键。 + +辅助索引的存在并不会影响聚集索引,因为聚集索引构成的 B+ 树是数据实际存储的形式,而辅助索引只用于加速数据的查找,所以一张表上往往有多个辅助索引以此来提升数据库的性能。 + +> 一张表一定包含一个聚集索引构成的 B+ 树以及若干辅助索引的构成的 B+ 树。 + +![Secondary-Index](images/mysql/Secondary-Index.jpg) + +如果在表 `users` 中存在一个辅助索引 `(first_name, age)`,那么它构成的 B+ 树大致就是上图这样,按照 `(first_name, age)` 的字母顺序对表中的数据进行排序,当查找到主键时,再通过聚集索引获取到整条行记录。 + +![Clustered-Secondary-Index](images/mysql/Clustered-Secondary-Index.jpg) + +上图展示了一个使用辅助索引查找一条表记录的过程:通过辅助索引查找到对应的主键,最后在聚集索引中使用主键获取对应的行记录,这也是通常情况下行记录的查找方式。 + +### 索引的设计 + +索引的设计其实是一个非常重要的内容,同时也是一个非常复杂的内容;索引的设计与创建对于提升数据库的查询性能至关重要,不过这不是本文想要介绍的内容,有关索引的设计与优化可以阅读 [数据库索引设计与优化](数据库索引设计与优化) 一书,书中提供了一种非常科学合理的方法能够帮助我们在数据库中建立最适合的索引,当然作者也可能会在之后的文章中对索引的设计进行简单的介绍和分析。 + +## 锁 + +我们都知道锁的种类一般分为乐观锁和悲观锁两种,InnoDB 存储引擎中使用的就是悲观锁,而按照锁的粒度划分,也可以分成行锁和表锁。 + +### 并发控制机制 + +乐观锁和悲观锁其实都是并发控制的机制,同时它们在原理上就有着本质的差别; + ++ 乐观锁是一种思想,它其实并不是一种真正的『锁』,它会先尝试对资源进行修改,在写回时判断资源是否进行了改变,如果没有发生改变就会写回,否则就会进行重试,在整个的执行过程中其实都**没有对数据库进行加锁**; ++ 悲观锁就是一种真正的锁了,它会在获取资源前对资源进行加锁,确保同一时刻只有有限的线程能够访问该资源,其他想要尝试获取资源的操作都会进入等待状态,直到该线程完成了对资源的操作并且释放了锁后,其他线程才能重新操作资源; + +虽然乐观锁和悲观锁在本质上并不是同一种东西,一个是一种思想,另一个是一种真正的锁,但是它们都是一种并发控制机制。 + +![Optimistic-Pessimistic-Locks](images/mysql/Optimistic-Pessimistic-Locks.jpg) + +乐观锁不会存在死锁的问题,但是由于更新后验证,所以当**冲突频率**和**重试成本**较高时更推荐使用悲观锁,而需要非常高的**响应速度**并且**并发量**非常大的时候使用乐观锁就能较好的解决问题,在这时使用悲观锁就可能出现严重的性能问题;在选择并发控制机制时,需要综合考虑上面的四个方面(冲突频率、重试成本、响应速度和并发量)进行选择。 + +### 锁的种类 + +对数据的操作其实只有两种,也就是读和写,而数据库在实现锁时,也会对这两种操作使用不同的锁;InnoDB 实现了标准的行级锁,也就是共享锁(Shared Lock)和互斥锁(Exclusive Lock);共享锁和互斥锁的作用其实非常好理解: + ++ **共享锁(读锁)**:允许事务对一条行数据进行读取; ++ **互斥锁(写锁)**:允许事务对一条行数据进行删除或更新; + +而它们的名字也暗示着各自的另外一个特性,共享锁之间是兼容的,而互斥锁与其他任意锁都不兼容: + +![Shared-Exclusive-Lock](images/mysql/Shared-Exclusive-Lock.jpg) + +稍微对它们的使用进行思考就能想明白它们为什么要这么设计,因为共享锁代表了读操作、互斥锁代表了写操作,所以我们可以在数据库中**并行读**,但是只能**串行写**,只有这样才能保证不会发生线程竞争,实现线程安全。 + +### 锁的粒度 + +无论是共享锁还是互斥锁其实都只是对某一个数据行进行加锁,InnoDB 支持多种粒度的锁,也就是行锁和表锁;为了支持多粒度锁定,InnoDB 存储引擎引入了意向锁(Intention Lock),意向锁就是一种表级锁。 + +与上一节中提到的两种锁的种类相似的是,意向锁也分为两种: + ++ **意向共享锁**:事务想要在获得表中某些记录的共享锁,需要在表上先加意向共享锁; ++ **意向互斥锁**:事务想要在获得表中某些记录的互斥锁,需要在表上先加意向互斥锁; + +随着意向锁的加入,锁类型之间的兼容矩阵也变得愈加复杂: + +![Lock-Type-Compatibility-Matrix](images/mysql/Lock-Type-Compatibility-Matrix.jpg) + +意向锁其实不会阻塞全表扫描之外的任何请求,它们的主要目的是为了表示**是否有人请求锁定表中的某一行数据**。 + +> 有的人可能会对意向锁的目的并不是完全的理解,我们在这里可以举一个例子:如果没有意向锁,当已经有人使用行锁对表中的某一行进行修改时,如果另外一个请求要对全表进行修改,那么就需要对所有的行是否被锁定进行扫描,在这种情况下,效率是非常低的;不过,在引入意向锁之后,当有人使用行锁对表中的某一行进行修改之前,会先为表添加意向互斥锁(IX),再为行记录添加互斥锁(X),在这时如果有人尝试对全表进行修改就不需要判断表中的每一行数据是否被加锁了,只需要通过等待意向互斥锁被释放就可以了。 + +### 锁的算法 + +到目前为止已经对 InnoDB 中锁的粒度有一定的了解,也清楚了在对数据库进行读写时会获取不同的锁,在这一小节将介绍锁是如何添加到对应的数据行上的,我们会分别介绍三种锁的算法:Record Lock、Gap Lock 和 Next-Key Lock。 + +#### Record Lock + +记录锁(Record Lock)是加到**索引记录**上的锁,假设我们存在下面的一张表 `users`: + +```sql +CREATE TABLE users( + id INT NOT NULL AUTO_INCREMENT, + last_name VARCHAR(255) NOT NULL, + first_name VARCHAR(255), + age INT, + PRIMARY KEY(id), + KEY(last_name), + KEY(age) +); +``` + +如果我们使用 `id` 或者 `last_name` 作为 SQL 中 `WHERE` 语句的过滤条件,那么 InnoDB 就可以通过索引建立的 B+ 树找到行记录并添加索引,但是如果使用 `first_name` 作为过滤条件时,由于 InnoDB 不知道待修改的记录具体存放的位置,也无法对将要修改哪条记录提前做出判断就会锁定整个表。 + +#### Gap Lock + +记录锁是在存储引擎中最为常见的锁,除了记录锁之外,InnoDB 中还存在间隙锁(Gap Lock),间隙锁是对索引记录中的一段连续区域的锁;当使用类似 `SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE;` 的 SQL 语句时,就会阻止其他事务向表中插入 `id = 15` 的记录,因为整个范围都被间隙锁锁定了。 + +> 间隙锁是存储引擎对于性能和并发做出的权衡,并且只用于某些事务隔离级别。 + +虽然间隙锁中也分为共享锁和互斥锁,不过它们之间并不是互斥的,也就是不同的事务可以同时持有一段相同范围的共享锁和互斥锁,它唯一阻止的就是**其他事务向这个范围中添加新的记录**。 + +#### Next-Key Lock + +Next-Key 锁相比前两者就稍微有一些复杂,它是记录锁和记录前的间隙锁的结合,在 `users` 表中有以下记录: + +```sql ++------+-------------+--------------+-------+ +| id | last_name | first_name | age | +|------+-------------+--------------+-------| +| 4 | stark | tony | 21 | +| 1 | tom | hiddleston | 30 | +| 3 | morgan | freeman | 40 | +| 5 | jeff | dean | 50 | +| 2 | donald | trump | 80 | ++------+-------------+--------------+-------+ +``` + +如果使用 Next-Key 锁,那么 Next-Key 锁就可以在需要的时候锁定以下的范围: + +```sql +(-∞, 21] +(21, 30] +(30, 40] +(40, 50] +(50, 80] +(80, ∞) +``` + +> 既然叫 Next-Key 锁,锁定的应该是当前值和后面的范围,但是实际上却不是,Next-Key 锁锁定的是当前值和前面的范围。 + +当我们更新一条记录,比如 `SELECT * FROM users WHERE age = 30 FOR UPDATE;`,InnoDB 不仅会在范围 `(21, 30]` 上加 Next-Key 锁,还会在这条记录后面的范围 `(30, 40]` 加间隙锁,所以插入 `(21, 40]` 范围内的记录都会被锁定。 + +Next-Key 锁的作用其实是为了解决幻读的问题,我们会在下一节谈事务的时候具体介绍。 + +### 死锁的发生 + +既然 InnoDB 中实现的锁是悲观的,那么不同事务之间就可能会互相等待对方释放锁造成死锁,最终导致事务发生错误;想要在 MySQL 中制造死锁的问题其实非常容易: + +![Deadlocks](images/mysql/Deadlocks.jpg) + +两个会话都持有一个锁,并且尝试获取对方的锁时就会发生死锁,不过 MySQL 也能在发生死锁时及时发现问题,并保证其中的一个事务能够正常工作,这对我们来说也是一个好消息。 + +## 事务与隔离级别 + +在介绍了锁之后,我们再来谈谈数据库中一个非常重要的概念 —— 事务;相信只要是一个合格的软件工程师就对事务的特性有所了解,其中被人经常提起的就是事务的原子性,在数据提交工作时,要么保证所有的修改都能够提交,要么就所有的修改全部回滚。 + +但是事务还遵循包括原子性在内的 ACID 四大特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability);文章不会对这四大特性全部展开进行介绍,相信你能够通过 Google 和数据库相关的书籍轻松获得有关它们的概念,本文最后要介绍的就是事务的四种隔离级别。 + +### 几种隔离级别 + +事务的隔离性是数据库处理数据的几大基础之一,而隔离级别其实就是提供给用户用于在性能和可靠性做出选择和权衡的配置项。 + +ISO 和 ANIS SQL 标准制定了四种事务隔离级别,而 InnoDB 遵循了 SQL:1992 标准中的四种隔离级别:`READ UNCOMMITED`、`READ COMMITED`、`REPEATABLE READ` 和 `SERIALIZABLE`;每个事务的隔离级别其实都比上一级多解决了一个问题: + ++ `RAED UNCOMMITED`:使用查询语句不会加锁,可能会读到未提交的行(Dirty Read); ++ `READ COMMITED`:只对记录加记录锁,而不会在记录之间加间隙锁,所以允许新的记录插入到被锁定记录的附近,所以再多次使用查询语句时,可能得到不同的结果(Non-Repeatable Read); ++ `REPEATABLE READ`:多次读取同一范围的数据会返回第一次查询的快照,不会返回不同的数据行,但是可能发生幻读(Phantom Read); ++ `SERIALIZABLE`:InnoDB 隐式地将全部的查询语句加上共享锁,解决了幻读的问题; + +MySQL 中默认的事务隔离级别就是 `REPEATABLE READ`,但是它通过 Next-Key 锁也能够在某种程度上解决幻读的问题。 + +![Transaction-Isolation-Matrix](images/mysql/Transaction-Isolation-Matrix.jpg) + +接下来,我们将数据库中创建如下的表并通过个例子来展示在不同的事务隔离级别之下,会发生什么样的问题: + +```sql +CREATE TABLE test( + id INT NOT NULL, + UNIQUE(id) +); +``` + +### 脏读 + +当事务的隔离级别为 `READ UNCOMMITED` 时,我们在 `SESSION 2` 中插入的**未提交**数据在 `SESSION 1` 中是可以访问的。 + +![Read-Uncommited-Dirty-Read](images/mysql/Read-Uncommited-Dirty-Read.jpg) + +### 不可重复读 + +当事务的隔离级别为 `READ COMMITED` 时,虽然解决了脏读的问题,但是如果在 `SESSION 1` 先查询了一个范围的数据,在这之后 `SESSION 2` 中插入一条数据并且提交了修改,在这时,如果 `SESSION 1` 中再次使用相同的查询语句,就会发现两次查询的结果不一样。 + +![Read-Commited-Non-Repeatable-Read](images/mysql/Read-Commited-Non-Repeatable-Read.jpg) + +不可重复读的原因就是,在 `READ COMMITED` 的隔离级别下,存储引擎不会在查询记录时添加间隙锁,锁定 `id < 5` 这个范围。 + +### 幻读 + +重新开启了两个会话 `SESSION 1` 和 `SESSION 2`,在 `SESSION 1` 中我们查询全表的信息,没有得到任何记录;在 `SESSION 2` 中向表中插入一条数据并提交;由于 `REPEATABLE READ` 的原因,再次查询全表的数据时,我们获得到的仍然是空集,但是在向表中插入同样的数据却出现了错误。 + +![Repeatable-Read-Phantom-Read](images/mysql/Repeatable-Read-Phantom-Read.jpg) + +这种现象在数据库中就被称作幻读,虽然我们使用查询语句得到了一个空的集合,但是插入数据时却得到了错误,好像之前的查询是幻觉一样。 + +在标准的事务隔离级别中,幻读是由更高的隔离级别 `SERIALIZABLE` 解决的,但是它也可以通过 MySQL 提供的 Next-Key 锁解决: + +![Repeatable-with-Next-Key-Lock](images/mysql/Repeatable-with-Next-Key-Lock.jpg) + +`REPERATABLE READ` 和 `READ UNCOMMITED` 其实是矛盾的,如果保证了前者就看不到已经提交的事务,如果保证了后者,就会导致两次查询的结果不同,MySQL 为我们提供了一种折中的方式,能够在 `REPERATABLE READ` 模式下加锁访问已经提交的数据,其本身并不能解决幻读的问题,而是通过文章前面提到的 Next-Key 锁来解决。 + +## 总结 + +> 文章中的内容大都来自于 [高性能 MySQL](https://book.douban.com/subject/23008813/)、[MySQL 技术内幕:InnoDB 存储引擎](https://book.douban.com/subject/24708143/)、[数据库索引设计与优化](https://book.douban.com/subject/26419771/) 以及 MySQL 的 [官方文档](https://dev.mysql.com/doc/)。 + +由于篇幅所限仅能对数据库中一些重要内容进行简单的介绍和总结,文中内容难免有所疏漏,如果对文章内容的有疑问,可以在博客下面评论留言(评论系统使用 Disqus,需要翻墙)。 + +## Reference + ++ [mysqld_safe version different than mysqld?](https://dba.stackexchange.com/questions/35962/mysqld-safe-version-different-than-mysqld) ++ [File Space Management](https://dev.mysql.com/doc/refman/5.7/en/innodb-file-space.html) ++ [Externally Stored Fields in InnoDB](http://mysqlserverteam.com/externally-stored-fields-in-innodb/) ++ [InnoDB Record Structure](https://dev.mysql.com/doc/internals/en/innodb-record-structure.html) ++ [InnoDB Page Structure](https://dev.mysql.com/doc/internals/en/innodb-page-structure.html) ++ [Difference between clustered and nonclustered index](https://stackoverflow.com/questions/5070529/difference-between-clustered-and-nonclustered-index) ++ [InnoDB Locking](https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html) ++ [乐观锁与悲观锁的区别](http://www.cnblogs.com/Bob-FD/p/3352216.html) ++ [Optimistic concurrency control](https://en.wikipedia.org/wiki/Optimistic_concurrency_control) ++ [MySQL 四种事务隔离级的说明](http://www.cnblogs.com/zhoujinyi/p/3437475.html) + diff --git a/contents/Database/sql-index-intro.md b/contents/Database/sql-index-intro.md new file mode 100644 index 0000000..856fa8a --- /dev/null +++ b/contents/Database/sql-index-intro.md @@ -0,0 +1,176 @@ +# MySQL 索引设计概要 + +在关系型数据库中设计索引其实并不是复杂的事情,很多开发者都觉得设计索引能够提升数据库的性能,相关的知识一定非常复杂。 + +![Index-and-Performance](images/sql-index-intro/Index-and-Performance.jpg) + +然而这种想法是不正确的,索引其实并不是一个多么高深莫测的东西,只要我们掌握一定的方法,理解索引的实现就能在不需要 DBA 的情况下设计出高效的索引。 + +本文会介绍 [数据库索引设计与优化](https://www.amazon.cn/图书/dp/B00ZH27RH0) 中设计索引的一些方法,让各位读者能够快速的在现有的工程中设计出合适的索引。 + +## 磁盘 IO + +一个数据库必须保证其中存储的所有数据都是可以随时读写的,同时因为 MySQL 中所有的数据其实都是以文件的形式存储在磁盘上的,而从磁盘上**随机访问**对应的数据非常耗时,所以数据库程序和操作系统提供了缓冲池和内存以提高数据的访问速度。 + +![Disk-IO](images/sql-index-intro/Disk-IO.jpg) + +除此之外,我们还需要知道数据库对数据的读取并不是以行为单位进行的,无论是读取一行还是多行,都会将该行或者多行所在的页全部加载进来,然后再读取对应的数据记录;也就是说,读取所耗费的时间与行数无关,只与页数有关。 + +![Page-DatabaseBufferPool](images/sql-index-intro/Page-DatabaseBufferPool.jpg) + +在 MySQL 中,页的大小一般为 16KB,不过也可能是 8KB、32KB 或者其他值,这跟 MySQL 的存储引擎对数据的存储方式有很大的关系,文中不会展开介绍,不过**索引或行记录是否在缓存池中极大的影响了访问索引或者数据的成本**。 + +### 随机读取 + +数据库等待一个页从磁盘读取到缓存池的所需要的成本巨大的,无论我们是想要读取一个页面上的多条数据还是一条数据,都需要消耗**约** 10ms 左右的时间: + +![Disk-Random-IO](images/sql-index-intro/Disk-Random-IO.jpg) + +10ms 的时间在计算领域其实是一个非常巨大的成本,假设我们使用脚本向装了 SSD 的磁盘上顺序写入字节,那么在 10ms 内可以写入大概 3MB 左右的内容,但是数据库程序在 10ms 之内只能将一页的数据加载到数据库缓冲池中,从这里可以看出随机读取的代价是巨大的。 + +![Disk-IO-Total-Time](images/sql-index-intro/Disk-IO-Total-Time.jpg) + +这 10ms 的一次随机读取是按照每秒 50 次的读取计算得到的,其中等待时间为 3ms、磁盘的实际繁忙时间约为 6ms,最终数据页从磁盘传输到缓冲池的时间为 1ms 左右,在对查询进行估算时并不需要准确的知道随机读取的时间,只需要知道估算出的 10ms 就可以了。 + +### 内存读取 + +如果在数据库的**缓存池**中没有找到对应的数据页,那么会去内存中寻找对应的页面: + +![Read-from-Memory](images/sql-index-intro/Read-from-Memory.jpg) + +当对应的页面存在于内存时,数据库程序就会使用内存中的页,这能够将数据的读取时间降低一个数量级,将 10ms 降低到 1ms;MySQL 在执行读操作时,会先从数据库的缓冲区中读取,如果不存在与缓冲区中就会尝试从内存中加载页面,如果前面的两个步骤都失败了,最后就只能执行随机 IO 从磁盘中获取对应的数据页。 + +### 顺序读取 + +从磁盘读取数据并不是都要付出很大的代价,当数据库管理程序一次性从磁盘中**顺序**读取大量的数据时,读取的速度会异常的快,大概在 40MB/s 左右。 + +![Sequential-Reads-from-Disk](images/sql-index-intro/Sequential-Reads-from-Disk.jpg) + +如果一个页面的大小为 4KB,那么 1s 的时间就可以读取 10000 个页,读取一个页面所花费的平均时间就是 0.1ms,相比随机读取的 10ms 已经降低了两个数量级,甚至比内存中读取数据还要快。 + +![Random-to-Sequentia](images/sql-index-intro/Random-to-Sequential.jpg) + +数据页面的顺序读取有两个非常重要的优势: + +1. 同时读取多个界面意味着总时间的消耗会大幅度减少,磁盘的吞吐量可以达到 40MB/s; +2. 数据库管理程序会对一些即将使用的界面进行预读,以减少查询请求的等待和响应时间; + +### 小结 + +数据库查询操作的时间大都消耗在从磁盘或者内存中读取数据的过程,由于随机 IO 的代价巨大,如何在一次数据库查询中减少随机 IO 的次数往往能够大幅度的降低查询所耗费的时间提高磁盘的吞吐量。 + +## 查询过程 + +在上一节中,文章从数据页加载的角度介绍了磁盘 IO 对 MySQL 查询的影响,而在这一节中将介绍 MySQL 查询的执行过程中以及数据库中的数据的特征对最终查询性能的影响。 + +### 索引片(Index Slices) + +索引片其实就是 SQL 查询在执行过程中扫描的一个索引片段,在这个范围中的索引将被顺序扫描,根据索引片包含的列数不同,[数据库索引设计与优化](https://www.amazon.cn/图书/dp/B00ZH27RH0) 书中对将索引分为宽索引和窄索引: + +![Thin-Index-and-Fat-Index](images/sql-index-intro/Thin-Index-and-Fat-Index.jpg) + +> 主键列 `id` 在所有的 MySQL 索引中都是一定会存在的。 + +对于查询 `SELECT id, username, age FROM users WHERE username="draven"` 来说,(id, username) 就是一个窄索引,因为该索引没有包含存在于 SQL 查询中的 age 列,而 (id, username, age) 就是该查询的一个宽索引了,它**包含这个查询中所需要的全部数据列**。 + +宽索引能够避免二次的随机 IO,而窄索引就需要在对索引进行顺序读取之后再根据主键 id 从主键索引中查找对应的数据: + +![Thin-Index-and-Clustered-Index](images/sql-index-intro/Thin-Index-and-Clustered-Index.jpg) + +对于窄索引,每一个在索引中匹配到的记录行最终都需要执行另外的随机读取从聚集索引中获得剩余的数据,如果结果集非常大,那么就会导致随机读取的次数过多进而影响性能。 + +### 过滤因子 + +从上一小节对索引片的介绍,我们可以看到影响 SQL 查询的除了查询本身还与数据库表中的数据特征有关,如果使用的是窄索引那么对表的随机访问就不可避免,在这时如何让索引片变『薄』就是我们需要做的了。 + +一个 SQL 查询扫描的索引片大小其实是由过滤因子决定的,也就是满足查询条件的记录行数所占的比例: + +![Filter-Facto](images/sql-index-intro/Filter-Factor.jpg) + +对于 users 表来说,sex="male" 就不是一个好的过滤因子,它会选择整张表中一半的数据,所以**在一般情况下**我们最好不要使用 sex 列作为整个索引的第一列;而 name="draven" 的使用就可以得到一个比较好的过滤因子了,它的使用能过滤整个数据表中 99.9% 的数据;当然我们也可以将这三个过滤进行组合,创建一个新的索引 (name, age, sex) 并同时使用这三列作为过滤条件: + +![Combined-Filter-Facto](images/sql-index-intro/Combined-Filter-Factor.jpg) + +> 当三个过滤条件都是等值谓词时,几个索引列的顺序其实是无所谓的,索引列的顺序不会影响同一个 SQL 语句对索引的选择,也就是索引 (name, age, sex) 和 (age, sex, name) 对于上图中的条件来说是完全一样的,这两个索引在执行查询时都有着完全相同的效果。 + +组合条件的过滤因子就可以达到十万分之 6 了,如果整张表中有 10w 行数据,也只需要在扫描薄索引片后进行 6 次随机读取,这种直接使用乘积来计算组合条件的过滤因子其实有一个比较重要的问题:列与列之间不应该有太强的相关性,如果不同的列之间有相关性,那么得到的结果就会比直接乘积得出的结果大一些,比如:所在的城市和邮政编码就有非常强的相关性,两者的过滤因子直接相乘其实与实际的过滤因子会有很大的偏差,不过这在多数情况下都不是太大的问题。 + +对于一张表中的同一个列,不同的值也会有不同的过滤因子,这也就造成了同一列的不同值最终的查询性能也会有很大差别: + +![Same-Columns-Filter-Facto](images/sql-index-intro/Same-Columns-Filter-Factor.jpg) + +当我们评估一个索引是否合适时,需要考虑极端情况下查询语句的性能,比如 0% 或者 50% 等;最差的输入往往意味着最差的性能,在平均情况下表现良好的 SQL 语句在极端的输入下可能就完全无法正常工作,这也是在设计索引时需要注意的问题。 + +总而言之,需要扫描的索引片的大小对查询性能的影响至关重要,而扫描的索引记录的数量,就是总行数与组合条件的过滤因子的乘积,索引片的大小最终也决定了从表中读取数据所需要的时间。 + +### 匹配列与过滤列 + +假设在 users 表中有 name、age 和 (name, sex, age) 三个辅助索引;当 WHERE 条件中存在类似 age = 21 或者 name = "draven" 这种**等值谓词**时,它们都会成为匹配列(Matching Column)用于选择索引树中的数据行,但是当我们使用以下查询时: + +```sql +SELECT * FROM users +WHERE name = "draven" AND sex = "male" AND age > 20; +``` + +虽然我们有 (name, sex, age) 索引包含了上述查询条件中的全部列,但是在这里只有 name 和 sex 两列才是匹配列,MySQL 在执行上述查询时,会选择 name 和 sex 作为匹配列,扫描所有满足条件的数据行,然后将 age 当做过滤列(Filtering Column): + +![Match-Columns-Filter-Columns](images/sql-index-intro/Match-Columns-Filter-Columns.jpg) + +过滤列虽然不能够减少索引片的大小,但是能够减少从表中随机读取数据的次数,所以在索引中也扮演着非常重要的角色。 + +## 索引的设计 + +作者相信文章前面的内容已经为索引的设计提供了充足的理论基础和知识,从总体来看如何减少随机读取的次数是设计索引时需要重视的最重要的问题,在这一节中,我们将介绍 [数据库索引设计与优化](https://www.amazon.cn/图书/dp/B00ZH27RH0) 一书中归纳出的设计最佳索引的方法。 + +### 三星索引 + +三星索引是对于一个查询语句可能的最好索引,如果一个查询语句的索引是三星索引,那么它只需要进行**一次磁盘的随机读及一个窄索引片的顺序扫描**就可以得到全部的结果集;因此其查询的响应时间比普通的索引会少几个数量级;根据书中对三星索引的定义,我们可以理解为主键索引对于 `WHERE id = 1` 就是一个特殊的三星索引,我们只需要对主键索引树进行一次索引访问并且顺序读取一条数据记录查询就结束了。 + +![Three-Star-Index](images/sql-index-intro/Three-Star-Index.jpg) + +为了满足三星索引中的三颗星,我们分别需要做以下几件事情: + +1. 第一颗星需要取出所有等值谓词中的列,作为索引开头的最开始的列(任意顺序); +2. 第二颗星需要将 ORDER BY 列加入索引中; +3. 第三颗星需要将查询语句剩余的列全部加入到索引中; + +> 三星索引的概念和星级的给定来源于 [数据库索引设计与优化](https://www.amazon.cn/图书/dp/B00ZH27RH0) 书中第四章三星索引一节。 + +如果对于一个查询语句我们依照上述的三个条件进行设计,那么就可以得到该查询的三星索引,这三颗星中的最后一颗星往往都是最容易获得的,满足第三颗星的索引也就是上面提到的宽索引,能够避免大量的随机 IO,如果我们遵循这个顺序为一个 SQL 查询设计索引那么我们就可以得到一个完美的索引了;这三颗星的获得其实也没有表面上这么简单,每一颗星都有自己的意义: + +![Behind-Three-Star-Index](images/sql-index-intro/Behind-Three-Star-Index.jpg) + +1. 第一颗星不只是将等值谓词的列加入索引,它的作用是减少索引片的大小以减少需要扫描的数据行; +2. 第二颗星用于避免排序,减少磁盘 IO 和内存的使用; +3. 第三颗星用于避免每一个索引对应的数据行都需要进行一次随机 IO 从聚集索引中读取剩余的数据; + +在实际场景中,问题往往没有这么简单,我们虽然可以总能够通过宽索引避免大量的随机访问,但是在一些复杂的查询中我们无法同时获得第一颗星和第二颗星。 + +```sql +SELECT id, name, age FROM users +WHERE age BETWEEN 18 AND 21 + AND city = "Beijing" +ORDER BY name; +``` + +在上述查询中,我们总可以通过增加索引中的列以获得第三颗星,但是如果我们想要获得第一颗星就需要最小化索引片的大小,这时索引的前缀必须为 (city, age),在这时再想获得第三颗星就不可能了,哪怕在 age 的后面添加索引列 name,也会因为 name 在范围索引列 age 后面必须进行一次排序操作,最终得到的索引就是 (city, age, name, id): + +![Different-Stars-Index](images/sql-index-intro/Different-Stars-Index.jpg) + +如果我们需要在内存中避免排序的话,就需要交换 age 和 name 的位置了,在这时就可以得到索引 (city, name, age, id),当一个 SQL 查询中**同时拥有范围谓词和 ORDER BY 时**,无论如何我们都是没有办法获得一个三星索引的,我们能够做的就是在这两者之间做出选择,是牺牲第一颗星还是第二颗星。 + +总而言之,在设计单表的索引时,首先把查询中所有的**等值谓词全部取出**以任意顺序放在索引最前面,在这时,如果索引中同时存在范围索引和 ORDER BY 就需要权衡利弊了,希望最小化扫描的索引片厚度时,应该将**过滤因子最小的范围索引列**加入索引,如果希望避免排序就选择 **ORDER BY 中的全部列**,在这之后就只需要将查询中**剩余的全部列**加入索引了,通过这种固定的方法和逻辑就可以最快地获得一个查询语句的二星或者三星索引了。 + +## 总结 + +在单表上对索引进行设计其实还是非常容易的,只需要遵循固定的套路就能设计出一个理想的三星索引,在这里强烈推荐 [数据库索引设计与优化](https://www.amazon.cn/图书/dp/B00ZH27RH0) 这本书籍,其中包含了大量与索引设计与优化的相关内容;在之后的文章中读者也会分析介绍书中提供的几种估算方法,来帮助我们通过预估问题设计出更高效的索引。 + +> Follow: [Draveness · GitHub](https://github.com/Draveness) + +## Reference + ++ [数据库索引设计与优化](https://www.amazon.cn/图书/dp/B00ZH27RH0) ++ [File Space Management](https://dev.mysql.com/doc/refman/5.7/en/innodb-file-space.html) ++ [Inside of Hard Drive - YouTube](https://www.youtube.com/watch?v=9eMWG3fwiEU) ++ [Hard Disk Working | How does a hard disk work | Hard Drive - YouTube](https://www.youtube.com/watch?v=4iaxOUYalJU) + diff --git a/contents/Database/sql-index-performance.md b/contents/Database/sql-index-performance.md new file mode 100644 index 0000000..7ae02e2 --- /dev/null +++ b/contents/Database/sql-index-performance.md @@ -0,0 +1,94 @@ +# MySQL 索引性能分析概要 + +上一篇文章 [MySQL 索引设计概要](http://draveness.me/sql-index-intro.html) 介绍了影响索引设计的几大因素,包括过滤因子、索引片的宽窄与大小以及匹配列和过滤列。在文章的后半部分介绍了 [数据库索引设计与优化](https://www.amazon.cn/图书/dp/B00ZH27RH0) 一书中,理想的三星索引的设计流程和套路,到目前为止虽然我们掌握了单表索引的设计方法,但是却没有分析预估索引耗时的能力。 + +![Proactive-Index-Design](images/sql-index-performance/Proactive-Index-Design.jpg) + +在本文中,我们将介绍书中提到的两种分析索引性能的方法:基本问题法(BQ)和快速估算上限法(QUBE),这两种方法能够帮助我们快速分析、估算索引的性能,及时发现问题。 + +## 基本问题法 + +当我们需要考虑对现有的 SELECT 查询进行分析时,哪怕没有足够的时间,也应该使用基本问题法对查询进行评估,评估的内容非常简单:现有的索引或者即将添加的索引是否包含了 WHERE 中使用的全部列,也就是对于当前查询来说,是否有一个索引是半宽索引。 + +![Semifat-Index-and-Fat-Index](images/sql-index-performance/Semifat-Index-and-Fat-Index.jpg) + +在上一篇文章中,我们介绍过宽索引和窄索引,窄索引 (username) 其实就叫做半宽索引,其中包含了 WHERE 中的全部的列 username,当前索引的对于该查询只有一颗星,它虽然避免了无效的回表查询造成的随机 IO,但是如果当前的索引的性能仍然无法满足需要,就可以添加 age 将该索引变成宽索引 (username, age) 以此来避免回表访问造成的性能影响;对于上图中的简单查询,索引 (username, age) 其实已经是一个三星索引了,但是对于包含 ORDER BY 或者更加复杂的查询,(username, age) 可能就只是二星索引: + +![Complicated-Query-with-Order-By](images/sql-index-performance/Complicated-Query-with-Order-By.jpg) + +在这时如果该索引仍然不能满足性能的需要,就可以考虑按照上一篇文章 [MySQL 索引设计概要](http://draveness.me/sql-index-intro.html) 中提供的索引设计方法重新设计了。 + +> 虽然基本问题法能够快速解决一些由于索引造成的问题,但是它并不能保证足够的性能,当表中有 (city, username, age) 索引,谓词为 `WHERE username="draveness" AND age="21"` 时,使用基本问题法并不能得出正确的结果。 + +## 快速估算上限法 + +基本问题法非常简单,它能够最短的时间内帮助我们评估一个查询的性能,但是它并不能准确地反映一个索引相关的性能问题,而快速估算上限法就是一种更加准确、复杂的方法了;其目的在于在程序开发期间就能将访问路径缓慢的问题暴露出来,这个估算方法的输出就是本地响应时间(Local Response Time): + +![QUBE-LRT](images/sql-index-performance/QUBE-LRT.jpg) + +本地响应时间就是查询在数据库服务器中的耗时,不包括任何的网络延迟和多层环境的通信时间,仅包括执行查询任务的耗时。 + +### 响应时间 + +本地响应时间等于服务时间和排队时间的总和,一次查询请求需要在数据库中等待 CPU 以及磁盘的响应,也可能会因为其他事务正在对同样的数据进行读写,导致当前查询需要等待锁的获取,不过组成响应时间中的主要部分还是磁盘的服务时间: + +![Local-Response-Time](images/sql-index-performance/Local-Response-Time.jpg) + +QUBE 在计算的过程中会忽略除了磁盘排队时间的其他排队时间,这样能够简化整个评估流程,而磁盘的服务时间主要还是包括同步读写以及异步读几个部分: + +![Disk-Service-Time](images/sql-index-performance/Disk-Service-Time.jpg) + +在排除了上述多个部分的内容,我们得到了一个非常简单的估算过程,整个估算时间的输入仅为随机读和顺序读以及数据获取的三个输入,而它们也是影响查询的主要因素: + +![Local-Response-Time-Calculation](images/sql-index-performance/Local-Response-Time-Calculation.jpg) + +其中数据获取的过程在比较不同的索引对同一查询的影响是不需要考虑的,因为同一查询使用不同的索引也会得到相同的结果集,获取的数据也是完全相同的。 + +### 访问 + +当 MySQL 读取一个索引行或者一个表行时,就会发生一次访问,当使用全表扫描或者扫描索引片时,读取的第一个行就是随机访问,随机访问需要磁盘进行寻道和旋转,所以其代价巨大,而接下来顺序读取的所有行都是通过顺序访问读取的,代价只有随机访问的千分之一。 + +如果大量的顺序读取索引行和表行,在原理上可能会造成一些额外的零星的随机访问,不过这对于整个查询的估算来说其实并不重要;在计算本地响应时间时,仍然会把它们当做顺序访问进行估算。 + +### 示例 + +在这里,我们简单地举一个例子来展示如何计算查询在使用某个索引时所需要的本地响应时间,假设我们有一张 `users` 表,其中有一千万条数据: + +![User-Table](images/sql-index-performance/User-Table.jpg) + +在该 `users` 表中除了主键索引之外,还具有以下 (username, city)、(username, age) 和 (username) 几个辅助索引,当我们使用如下所示的查询时: + +![Filter-Facto](images/sql-index-performance/Filter-Factor.jpg) + +两个查询条件分别有着 0.05% 和 12% 的过滤因子,该查询可以直接使用已有的辅助索引 (username, city),接下来我们根据表中的总行数和过滤因子开始估算这一步骤 SQL 的执行时间: + +![Index-Slice-Scan](images/sql-index-performance/Index-Slice-Scan.jpg) + +该查询在开始时会命中 (username, city) 索引,扫描符合条件的索引片,该索引总共会访问 10,000,000 * 0.05% * 12% = 600 条数据,其中包括 1 次的随机访问和 599 次的顺序访问,因为该索引中的列并不能满足查询的需要,所以对于每一个索引行都会产生一次表的随机访问,以获取剩余列 age 的信息: + +![Index-Table-Touch](images/sql-index-performance/Index-Table-Touch.jpg) + +在这个过程中总共产生了 600 次随机访问,最后取回结果集的过程中也会有 600 次 FETCH 操作,从总体上来看这一次 SQL 查询共进行了 **601 次随机访问**、599 次顺序访问和 600 次 FETCH,根据上一节中的公式我们可以得到这个查询的用时约为 6075.99ms 也就是 6s 左右,这个时间对于绝大多数应用都是无法接受的。 + +![SQL-Query-Time](images/sql-index-performance/SQL-Query-Time.jpg) + +在整个查询的过程中,回表查询的 600 次随机访问成为了这个超级慢的查询的主要贡献,为了解决这个问题,我们只需要添加一个 (username, city, age) 索引或者在已有的 (username, city) 后添加新的 age 列就可以避免 600 次的随机访问: + +![SQL-Query-Time-After-Optimization](images/sql-index-performance/SQL-Query-Time-After-Optimization.jpg) + +(username, city, age) 索引对于该查询其实就是一个三星索引了,有关索引设计的内容可以阅读上一篇文章 [MySQL 索引设计概要](http://draveness.me/sql-index-intro.html) 如果读者有充足的时间依然强烈推荐 [数据库索引设计与优化](https://www.amazon.cn/图书/dp/B00ZH27RH0) 这本书。 + +## 总结 + +这篇文章是这一年来写的最短的一篇文章了,本来想详细介绍一下 [数据库索引设计与优化](https://www.amazon.cn/图书/dp/B00ZH27RH0) 书中对于索引性能分析的预估方法,仔细想了一下这部分的内容实在太多,例子也非常丰富,只通过一篇文章很难完整地介绍其中的全部内容,所以只选择了其中的一部分知识点简单介绍,这也是这篇文章叫概要的原因。 + +如果对文章的内容有疑问,可以在评论中留言,评论系统使用 Disqus 需要梯子。 + +> Follow: [Draveness · GitHub](https://github.com/Draveness) +> 原文链接:http://draveness.me/sql-index-performance.html + +## Reference + ++ [数据库索引设计与优化](https://www.amazon.cn/图书/dp/B00ZH27RH0) + + diff --git a/contents/Database/transaction.md b/contents/Database/transaction.md new file mode 100644 index 0000000..01dfa48 --- /dev/null +++ b/contents/Database/transaction.md @@ -0,0 +1,216 @@ +# 『浅入深出』MySQL 中事务的实现 + +在关系型数据库中,事务的重要性不言而喻,只要对数据库稍有了解的人都知道事务具有 ACID 四个基本属性,而我们不知道的可能就是数据库是如何实现这四个属性的;在这篇文章中,我们将对事务的实现进行分析,尝试理解数据库是如何实现事务的,当然我们也会在文章中简单对 MySQL 中对 ACID 的实现进行简单的介绍。 + +![Transaction-Basics](images/transaction/Transaction-Basics.jpg) + +事务其实就是**并发控制的基本单位**;相信我们都知道,事务是一个序列操作,其中的操作要么都执行,要么都不执行,它是一个不可分割的工作单位;数据库事务的 ACID 四大特性是事务的基础,了解了 ACID 是如何实现的,我们也就清除了事务的实现,接下来我们将依次介绍数据库是如何实现这四个特性的。 + +## 原子性 + +在学习事务时,经常有人会告诉你,事务就是一系列的操作,要么全部都执行,要都不执行,这其实就是对事务原子性的刻画;虽然事务具有原子性,但是原子性并不是只与事务有关系,它的身影在很多地方都会出现。 + +![Atomic-Operation](images/transaction/Atomic-Operation.jpg) + +由于操作并不具有原子性,并且可以再分为多个操作,当这些操作出现错误或抛出异常时,整个操作就可能不会继续执行下去,而已经进行的操作造成的副作用就可能造成数据更新的丢失或者错误。 + +事务其实和一个操作没有什么太大的区别,它是一系列的数据库操作(可以理解为 SQL)的集合,如果事务不具备原子性,那么就没办法保证同一个事务中的所有操作都被执行或者未被执行了,整个数据库系统就既不可用也不可信。 + +### 回滚日志 + +想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行**回滚**,而在 MySQL 中,恢复机制是通过*回滚日志*(undo log)实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后在对数据库中的对应行进行写入。 + +![Transaction-Undo-Log](images/transaction/Transaction-Undo-Log.jpg) + +这个过程其实非常好理解,为了能够在发生错误时撤销之前的全部操作,肯定是需要将之前的操作都记录下来的,这样在发生错误时才可以回滚。 + +回滚日志除了能够在发生错误或者用户执行 `ROLLBACK` 时提供回滚相关的信息,它还能够在整个系统发生崩溃、数据库进程直接被杀死后,当用户再次启动数据库进程时,还能够立刻通过查询回滚日志将之前未完成的事务进行回滚,这也就需要回滚日志必须先于数据持久化到磁盘上,是我们需要先写日志后写数据库的主要原因。 + +回滚日志并不能将数据库物理地恢复到执行语句或者事务之前的样子;它是逻辑日志,当回滚日志被使用时,它只会按照日志**逻辑地**将数据库中的修改撤销掉看,可以**理解**为,我们在事务中使用的每一条 `INSERT` 都对应了一条 `DELETE`,每一条 `UPDATE` 也都对应一条相反的 `UPDATE` 语句。 + +![Logical-Undo-Log](images/transaction/Logical-Undo-Log.jpg) + +在这里,我们并不会介绍回滚日志的格式以及它是如何被管理的,本文重点关注在它到底是一个什么样的东西,究竟解决了、如何解决了什么样的问题,如果想要了解具体实现细节的读者,相信网络上关于回滚日志的文章一定不少。 + +### 事务的状态 + +因为事务具有原子性,所以从远处看的话,事务就是密不可分的一个整体,事务的状态也只有三种:Active、Commited 和 Failed,事务要不就在执行中,要不然就是成功或者失败的状态: + +![Atomitc-Transaction-State](images/transaction/Atomitc-Transaction-State.jpg) + +但是如果放大来看,我们会发现事务不再是原子的,其中包括了很多中间状态,比如部分提交,事务的状态图也变得越来越复杂。 + +![Nonatomitc-Transaction-State](images/transaction/Nonatomitc-Transaction-State.jpg) + +> 事务的状态图以及状态的描述取自 [Database System Concepts](https://www.amazon.com/Database-System-Concepts-Computer-Science/dp/0073523321) 一书中第 14 章的内容。 + ++ Active:事务的初始状态,表示事务正在执行; ++ Partially Commited:在最后一条语句执行之后; ++ Failed:发现事务无法正常执行之后; ++ Aborted:事务被回滚并且数据库恢复到了事务进行之前的状态之后; ++ Commited:成功执行整个事务; + +虽然在发生错误时,整个数据库的状态可以恢复,但是如果我们在事务中执行了诸如:向标准输出打印日志、向外界发出邮件、没有通过数据库修改了磁盘上的内容甚至在事务执行期间发生了转账汇款,那么这些操作作为可见的外部输出都是没有办法回滚的;这些问题都是由应用开发者解决和负责的,在绝大多数情况下,我们都需要在整个事务提交后,再触发类似的无法回滚的操作。 + +![Shutdown-After-Commited](images/transaction/Shutdown-After-Commited.jpg) + +以订票为例,哪怕我们在整个事务结束之后,才向第三方发起请求,由于向第三方请求并获取结果是一个需要较长事件的操作,如果在事务刚刚提交时,数据库或者服务器发生了崩溃,那么我们就非常有可能丢失发起请求这一过程,这就造成了非常严重的问题;而这一点就不是数据库所能保证的,开发者需要在适当的时候查看请求是否被发起、结果是成功还是失败。 + +### 并行事务的原子性 + +到目前为止,所有的事务都只是串行执行的,一直都没有考虑过并行执行的问题;然而在实际工作中,并行执行的事务才是常态,然而并行任务下,却可能出现非常复杂的问题: + +![Nonrecoverable-Schedule](images/transaction/Nonrecoverable-Schedule.jpg) + +当 Transaction1 在执行的过程中对 `id = 1` 的用户进行了读写,但是没有将修改的内容进行提交或者回滚,在这时 Transaction2 对同样的数据进行了读操作并提交了事务;也就是说 Transaction2 是依赖于 Transaction1 的,当 Transaction1 由于一些错误需要回滚时,因为要保证事务的原子性,需要对 Transaction2 进行回滚,但是由于我们已经提交了 Transaction2,所以我们已经没有办法进行回滚操作,在这种问题下我们就发生了问题,[Database System Concepts](https://www.amazon.com/Database-System-Concepts-Computer-Science/dp/0073523321) 一书中将这种现象称为*不可恢复安排*(Nonrecoverable Schedule),那什么情况下是可以恢复的呢? + +> A recoverable schedule is one where, for each pair of transactions Ti and Tj such that Tj reads a data item previously written by Ti , the commit operation of Ti appears before the commit operation of Tj . + +简单理解一下,如果 Transaction2 依赖于事务 Transaction1,那么事务 Transaction1 必须在 Transaction2 提交之前完成提交的操作: + +![Recoverable-Schedule](images/transaction/Recoverable-Schedule.jpg) + +然而这样还不算完,当事务的数量逐渐增多时,整个恢复流程也会变得越来越复杂,如果我们想要从事务发生的错误中恢复,也不是一件那么容易的事情。 + +![Cascading-Rollback](images/transaction/Cascading-Rollback.jpg) + +在上图所示的一次事件中,Transaction2 依赖于 Transaction1,而 Transaction3 又依赖于 Transaction1,当 Transaction1 由于执行出现问题发生回滚时,为了保证事务的原子性,就会将 Transaction2 和 Transaction3 中的工作全部回滚,这种情况也叫做*级联回滚*(Cascading Rollback),级联回滚的发生会导致大量的工作需要撤回,是我们难以接受的,不过如果想要达到**绝对的**原子性,这件事情又是不得不去处理的,我们会在文章的后面具体介绍如何处理并行事务的原子性。 + +## 持久性 + +既然是数据库,那么一定对数据的持久存储有着非常强烈的需求,如果数据被写入到数据库中,那么数据一定能够被安全存储在磁盘上;而事务的持久性就体现在,一旦事务被提交,那么数据一定会被写入到数据库中并持久存储起来。 + +![Compensating-Transaction](images/transaction/Compensating-Transaction.jpg) + +当事务已经被提交之后,就无法再次回滚了,唯一能够撤回已经提交的事务的方式就是创建一个相反的事务对原操作进行『补偿』,这也是事务持久性的体现之一。 + +### 重做日志 + +与原子性一样,事务的持久性也是通过日志来实现的,MySQL 使用重做日志(redo log)实现事务的持久性,重做日志由两部分组成,一是内存中的重做日志缓冲区,因为重做日志缓冲区在内存中,所以它是易失的,另一个就是在磁盘上的重做日志文件,它是持久的。 + +![Redo-Logging](images/transaction/Redo-Logging.jpg) + +当我们在一个事务中尝试对数据进行修改时,它会先将数据从磁盘读入内存,并更新内存中缓存的数据,然后生成一条重做日志并写入重做日志缓存,当事务真正提交时,MySQL 会将重做日志缓存中的内容刷新到重做日志文件,再将内存中的数据更新到磁盘上,图中的第 4、5 步就是在事务提交时执行的。 + +在 InnoDB 中,重做日志都是以 512 字节的块的形式进行存储的,同时因为块的大小与磁盘扇区大小相同,所以重做日志的写入可以保证原子性,不会由于机器断电导致重做日志仅写入一半并留下脏数据。 + +除了所有对数据库的修改会产生重做日志,因为回滚日志也是需要持久存储的,它们也会创建对应的重做日志,在发生错误后,数据库重启时会从重做日志中找出未被更新到数据库磁盘中的日志重新执行以满足事务的持久性。 + +### 回滚日志和重做日志 + +到现在为止我们了解了 MySQL 中的两种日志,回滚日志(undo log)和重做日志(redo log);在数据库系统中,事务的原子性和持久性是由事务日志(transaction log)保证的,在实现时也就是上面提到的两种日志,前者用于对事务的影响进行撤销,后者在错误处理时对已经提交的事务进行重做,它们能保证两点: + +1. 发生错误或者需要回滚的事务能够成功回滚(原子性); +2. 在事务提交后,数据没来得及写会磁盘就宕机时,在下次重新启动后能够成功恢复数据(持久性); + +在数据库中,这两种日志经常都是一起工作的,我们**可以**将它们整体看做一条事务日志,其中包含了事务的 ID、修改的行元素以及修改前后的值。 + +![Transaction-Log](images/transaction/Transaction-Log.jpg) + +一条事务日志同时包含了修改前后的值,能够非常简单的进行回滚和重做两种操作,在这里我们也不会对重做和回滚日志展开进行介绍,可能会在之后的文章谈一谈数据库系统的恢复机制时提到两种日志的使用。 + +## 隔离性 + +其实作者在之前的文章 [『浅入浅出』MySQL 和 InnoDB](http://draveness.me/mysql-innodb.html) 就已经介绍过数据库事务的隔离性,不过问了保证文章的独立性和完整性,我们还会对事务的隔离性进行介绍,介绍的内容可能稍微有所不同。 + +事务的隔离性是数据库处理数据的几大基础之一,如果没有数据库的事务之间没有隔离性,就会发生在 [并行事务的原子性](#并行事务的原子性) 一节中提到的级联回滚等问题,造成性能上的巨大损失。如果所有的事务的执行顺序都是线性的,那么对于事务的管理容易得多,但是允许事务的并行执行却能能够提升吞吐量和资源利用率,并且可以减少每个事务的等待时间。 + +![Reasons-for-Allowing-Concurrency](images/transaction/Reasons-for-Allowing-Concurrency.jpg) + +当多个事务同时并发执行时,事务的隔离性可能就会被违反,虽然单个事务的执行可能没有任何错误,但是从总体来看就会造成数据库的一致性出现问题,而串行虽然能够允许开发者忽略并行造成的影响,能够很好地维护数据库的一致性,但是却会影响事务执行的性能。 + +### 事务的隔离级别 + +所以说数据库的隔离性和一致性其实是一个需要开发者去权衡的问题,为数据库提供什么样的隔离性层级也就决定了数据库的性能以及可以达到什么样的一致性;在 SQL 标准中定义了四种数据库的事务的隔离级别:`READ UNCOMMITED`、`READ COMMITED`、`REPEATABLE READ` 和 `SERIALIZABLE`;每个事务的隔离级别其实都比上一级多解决了一个问题: + ++ `RAED UNCOMMITED`:使用查询语句不会加锁,可能会读到未提交的行(Dirty Read); ++ `READ COMMITED`:只对记录加记录锁,而不会在记录之间加间隙锁,所以允许新的记录插入到被锁定记录的附近,所以再多次使用查询语句时,可能得到不同的结果(Non-Repeatable Read); ++ `REPEATABLE READ`:多次读取同一范围的数据会返回第一次查询的快照,不会返回不同的数据行,但是可能发生幻读(Phantom Read); ++ `SERIALIZABLE`:InnoDB 隐式地将全部的查询语句加上共享锁,解决了幻读的问题; + +以上的所有的事务隔离级别都不允许脏写入(Dirty Write),也就是当前事务更新了另一个事务已经更新但是还未提交的数据,大部分的数据库中都使用了 READ COMMITED 作为默认的事务隔离级别,但是 MySQL 使用了 REPEATABLE READ 作为默认配置;从 RAED UNCOMMITED 到 SERIALIZABLE,随着事务隔离级别变得越来越严格,数据库对于并发执行事务的性能也逐渐下降。 + +![Isolation-Performance](images/transaction/Isolation-Performance.jpg) + +对于数据库的使用者,从理论上说,并不需要知道事务的隔离级别是如何实现的,我们只需要知道这个隔离级别解决了什么样的问题,但是不同数据库对于不同隔离级别的是实现细节在很多时候都会让我们遇到意料之外的坑。 + +如果读者不了解脏读、不可重复读和幻读究竟是什么,可以阅读之前的文章 [『浅入浅出』MySQL 和 InnoDB](http://draveness.me/mysql-innodb.html),在这里我们仅放一张图来展示各个隔离层级对这几个问题的解决情况。 + +![Transaction-Isolation-Matrix](images/transaction/Transaction-Isolation-Matrix.jpg) + +### 隔离级别的实现 + +数据库对于隔离级别的实现就是使用**并发控制机制**对在同一时间执行的事务进行控制,限制不同的事务对于同一资源的访问和更新,而最重要也最常见的并发控制机制,在这里我们将简单介绍三种最重要的并发控制器机制的工作原理。 + +#### 锁 + +锁是一种最为常见的并发控制机制,在一个事务中,我们并不会将整个数据库都加锁,而是只会锁住那些需要访问的数据项, MySQL 和常见数据库中的锁都分为两种,共享锁(Shared)和互斥锁(Exclusive),前者也叫读锁,后者叫写锁。 + +![Shared-Exclusive-Lock](images/transaction/Shared-Exclusive-Lock.jpg) + +读锁保证了读操作可以并发执行,相互不会影响,而写锁保证了在更新数据库数据时不会有其他的事务访问或者更改同一条记录造成不可预知的问题。 + +#### 时间戳 + +除了锁,另一种实现事务的隔离性的方式就是通过时间戳,使用这种方式实现事务的数据库,例如 PostgreSQL 会为每一条记录保留两个字段;*读时间戳*中报错了所有访问该记录的事务中的最大时间戳,而记录行的*写时间戳*中保存了将记录改到当前值的事务的时间戳。 + +![Timestamps-Record](images/transaction/Timestamps-Record.jpg) + +使用时间戳实现事务的隔离性时,往往都会使用乐观锁,先对数据进行修改,在写回时再去判断当前值,也就是时间戳是否改变过,如果没有改变过,就写入,否则,生成一个新的时间戳并再次更新数据,乐观锁其实并不是真正的锁机制,它只是一种思想,在这里并不会对它进行展开介绍。 + +#### 多版本和快照隔离 + +通过维护多个版本的数据,数据库可以允许事务在数据被其他事务更新时对旧版本的数据进行读取,很多数据库都对这一机制进行了实现;因为所有的读操作不再需要等待写锁的释放,所以能够显著地提升读的性能,MySQL 和 PostgreSQL 都对这一机制进行自己的实现,也就是 MVCC,虽然各自实现的方式有所不同,MySQL 就通过文章中提到的回滚日志实现了 MVCC,保证事务并行执行时能够不等待互斥锁的释放直接获取数据。 + +### 隔离性与原子性 + +在这里就需要简单提一下在在原子性一节中遇到的级联回滚等问题了,如果一个事务对数据进行了写入,这时就会获取一个互斥锁,其他的事务就想要获得改行数据的读锁就必须等待写锁的释放,自然就不会发生级联回滚等问题了。 + +![Shared-Lock-and-Atomicity](images/transaction/Shared-Lock-and-Atomicity.jpg) + +不过在大多数的数据库,比如 MySQL 中都使用了 MVCC 等特性,也就是正常的读方法是不需要获取锁的,在想要对读取的数据进行更新时需要使用 `SELECT ... FOR UPDATE` 尝试获取对应行的互斥锁,以保证不同事务可以正常工作。 + +## 一致性 + +作者认为数据库的一致性是一个非常让人迷惑的概念,原因是数据库领域其实包含两个一致性,一个是 ACID 中的一致性、另一个是 CAP 定义中的一致性。 + +![ACID-And-CAP](images/transaction/ACID-And-CAP.jpg) + +这两个数据库的一致性说的**完全不是**一个事情,很多很多人都对这两者的概念有非常深的误解,当我们在讨论数据库的一致性时,一定要清楚上下文的语义是什么,尽量明确的问出我们要讨论的到底是 ACID 中的一致性还是 CAP 中的一致性。 + +### ACID + +数据库对于 ACID 中的一致性的定义是这样的:如果一个事务原子地在一个一致地数据库中独立运行,那么在它执行之后,数据库的状态一定是一致的。对于这个概念,它的第一层意思就是对于数据完整性的约束,包括主键约束、引用约束以及一些约束检查等等,在事务的执行的前后以及过程中不会违背对数据完整性的约束,所有对数据库写入的操作都应该是合法的,并不能产生不合法的数据状态。 + +> A transaction must preserve database consistency - if a transaction is run atomically in isolation starting from a consistent database, the database must again be consistent at the end of the transaction. + +我们可以将事务理解成一个函数,它接受一个外界的 SQL 输入和一个一致的数据库,它一定会返回一个一致的数据库。 + +![Transaction-Consistency](images/transaction/Transaction-Consistency.jpg) + +而第二层意思其实是指逻辑上的对于开发者的要求,我们要在代码中写出正确的事务逻辑,比如银行转账,事务中的逻辑不可能只扣钱或者只加钱,这是应用层面上对于数据库一致性的要求。 + +> Ensuring consistency for an individual transaction is the responsibility of the application programmer who codes the transaction. - [Database System Concepts](https://www.amazon.com/Database-System-Concepts-Computer-Science/dp/0073523321) + +数据库 ACID 中的一致性对事务的要求不止包含对数据完整性以及合法性的检查,还包含应用层面逻辑的正确。 + +CAP 定理中的数据一致性,其实是说分布式系统中的各个节点中对于同一数据的拷贝有着相同的值;而 ACID 中的一致性是指数据库的规则,如果 schema 中规定了一个值必须是唯一的,那么一致的系统必须确保在所有的操作中,该值都是唯一的,由此来看 CAP 和 ACID 对于一致性的定义有着根本性的区别。 + +## 总结 + +事务的 ACID 四大基本特性是保证数据库能够运行的基石,但是完全保证数据库的 ACID,尤其是隔离性会对性能有比较大影响,在实际的使用中我们也会根据业务的需求对隔离性进行调整,除了隔离性,数据库的原子性和持久性相信都是比较好理解的特性,前者保证数据库的事务要么全部执行、要么全部不执行,后者保证了对数据库的写入都是持久存储的、非易失的,而一致性不仅是数据库对本身数据的完整性的要求,同时也对开发者提出了要求 - 写出逻辑正确并且合理的事务。 + +最后,也是最重要的,当别人在将一致性的时候,一定要搞清楚他的上下文,如果对文章的内容有疑问,可以在评论中留言,评论系统使用 Disqus 需要梯子。 + +## References + ++ [Database System Concepts](https://www.amazon.com/Database-System-Concepts-Computer-Science/dp/0073523321) ++ [数据库事务](https://zh.wikipedia.org/wiki/数据库事务) ++ [How does MVCC (Multi-Version Concurrency Control) work](https://vladmihalcea.com/2017/03/01/how-does-mvcc-multi-version-concurrency-control-work/) ++ [How does a relational database work](https://vladmihalcea.com/2017/02/14/how-does-a-relational-database-work/) ++ [Implementing Transaction Processing using Redo Logs](http://www.mathcs.emory.edu/~cheung/Courses/377/Syllabus/10-Transactions/redo-log.html) ++ [Implementing Transaction Processing using Undo Logs](http://www.mathcs.emory.edu/~cheung/Courses/377/Syllabus/10-Transactions/undo-log.html) ++ [Undo/Redo Logging Rules](http://cs.ulb.ac.be/public/_media/teaching/infoh417/05_-_logging-sol-slides.pdf) ++ [MySQL 解密:InnoDB 存储引擎重做日志漫游](https://www.qiancheng.me/post/coding/mysql-001) ++ [ACID 中 C 与 CAP 定理中 C 的区别](http://www.jdon.com/46956) ++ [Disambiguating ACID and CAP](https://www.voltdb.com/blog/2015/10/22/disambiguating-acid-cap/) diff --git "a/contents/FBRetainCycleDetector/iOS \344\270\255\347\232\204 block \346\230\257\345\246\202\344\275\225\346\214\201\346\234\211\345\257\271\350\261\241\347\232\204.md" "b/contents/FBRetainCycleDetector/iOS \344\270\255\347\232\204 block \346\230\257\345\246\202\344\275\225\346\214\201\346\234\211\345\257\271\350\261\241\347\232\204.md" new file mode 100644 index 0000000..8149569 --- /dev/null +++ "b/contents/FBRetainCycleDetector/iOS \344\270\255\347\232\204 block \346\230\257\345\246\202\344\275\225\346\214\201\346\234\211\345\257\271\350\261\241\347\232\204.md" @@ -0,0 +1,390 @@ +# iOS 中的 block 是如何持有对象的 + +> Follow: [Draveness · Github](https://github.com/Draveness) + +Block 是 Objective-C 中笔者最喜欢的特性,它为 Objective-C 这门语言提供了强大的函数式编程能力,而最近苹果推出的很多新的 API 都已经开始原生的支持 block 语法,可见它在 Objective-C 中变得越来越重要。 + +![](images/block.jpg) + +这篇文章并不会详细介绍 block 在内存中到底是以什么形式存在的,主要会介绍 block 是如何持有并且释放对象的。文章中的代码都出自 Facebook 开源的**用于检测循环引用**的框架 [FBRetainCycleDetector](https://github.com/facebook/FBRetainCycleDetector),这是分析该框架文章中的最后一篇,也是笔者觉得最有意思的一部分。 + +> 如果你希望了解 FBRetainCycleDetector 的原理可以阅读[如何在 iOS 中解决循环引用的问题](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/FBRetainCycleDetector/如何在%20iOS%20中解决循环引用的问题.md)以及后续文章。 + +## 为什么会谈到 block + +可能很多读者会有这样的疑问,本文既然是对 `FBRetainCycleDetector` 解析的文章,为什么会提到 block?原因其实很简单,因为在 iOS 开发中大多数的循环引用都是因为 block 使用不当导致的,由于 block 会 retain 它持有的对象,这样就很容易造成循环引用,最终导致内存泄露。 + +在 `FBRetainCycleDetector` 中存在这样一个类 `FBObjectiveCBlock`,这个类的 `- allRetainedObjects` 方法就会返回所有 block 持有的强引用,这也是文章需要关注的重点。 + +```objectivec +- (NSSet *)allRetainedObjects { + NSMutableArray *results = [[[super allRetainedObjects] allObjects] mutableCopy]; + + __attribute__((objc_precise_lifetime)) id anObject = self.object; + + void *blockObjectReference = (__bridge void *)anObject; + NSArray *allRetainedReferences = FBGetBlockStrongReferences(blockObjectReference); + + for (id object in allRetainedReferences) { + FBObjectiveCGraphElement *element = FBWrapObjectGraphElement(self, object, self.configuration); + if (element) { + [results addObject:element]; + } + } + + return [NSSet setWithArray:results]; +} +``` + +这部分代码中的大部分都不重要,只是在开头调用父类方法,在最后将获取的对象包装成一个系列 `FBObjectiveCGraphElement`,最后返回一个数组,也就是当前对象 block 持有的全部强引用了。 + +## Block 是什么? + +对 block 稍微有了解的人都知道,block 其实是一个结构体,其结构大概是这样的: + +```objectivec +struct BlockLiteral { + void *isa; + int flags; + int reserved; + void (*invoke)(void *, ...); + struct BlockDescriptor *descriptor; +}; + +struct BlockDescriptor { + unsigned long int reserved; + unsigned long int size; + void (*copy_helper)(void *dst, void *src); + void (*dispose_helper)(void *src); + const char *signature; +}; +``` + +在 `BlockLiteral` 结构体中有一个 `isa` 指针,而对 `isa`了解的人也都知道,这里的 `isa` 其实指向了一个类,每一个 block 指向的类可能是 `__NSGlobalBlock__`、`__NSMallocBlock__` 或者 `__NSStackBlock__`,但是这些 block,它们继承自一个共同的父类,也就是 `NSBlock`,我们可以使用下面的代码来获取这个类: + +```objectivec +static Class _BlockClass() { + static dispatch_once_t onceToken; + static Class blockClass; + dispatch_once(&onceToken, ^{ + void (^testBlock)() = [^{} copy]; + blockClass = [testBlock class]; + while(class_getSuperclass(blockClass) && class_getSuperclass(blockClass) != [NSObject class]) { + blockClass = class_getSuperclass(blockClass); + } + [testBlock release]; + }); + return blockClass; +} +``` + +Objective-C 中的三种 block `__NSMallocBlock__`、`__NSStackBlock__` 和 `__NSGlobalBlock__` 会在下面的情况下出现: + +| | ARC | 非 ARC | +|------------|:----------------------------:|-----------------------------| +| 捕获外部变量 | `__NSMallocBlock__` <br> `__NSStackBlock__` | `__NSStackBlock__`| +| 未捕获外部变量 | `__NSGlobalBlock__`| `__NSGlobalBlock__` | + + ++ 在 ARC 中,捕获外部了变量的 block 的类会是 `__NSMallocBlock__` 或者 `__NSStackBlock__`,如果 block 被赋值给了某个变量在这个过程中会执行 `_Block_copy` 将原有的 `__NSStackBlock__` 变成 `__NSMallocBlock__`;但是如果 block 没有被赋值给某个变量,那它的类型就是 `__NSStackBlock__`;没有捕获外部变量的 block 的类会是 `__NSGlobalBlock__` 即不在堆上,也不在栈上,它类似 C 语言函数一样会在代码段中。 ++ 在非 ARC 中,捕获了外部变量的 block 的类会是 `__NSStackBlock__`,放置在栈上,没有捕获外部变量的 block 时与 ARC 环境下情况相同。 + +如果我们不断打印一个 block 的 `superclass` 的话最后就会在继承链中找到 `NSBlock` 的身影: + +![block-superclass](images/block-superclass.png) + +然后可以通过这种办法来判断当前对象是不是 block: + +```objectivec +BOOL FBObjectIsBlock(void *object) { + Class blockClass = _BlockClass(); + + Class candidate = object_getClass((__bridge id)object); + return [candidate isSubclassOfClass:blockClass]; +} +``` + +## Block 如何持有对象 + +在这一小节,我们将讨论 block 是**如何持有对象**的,我们会通过对 FBRetainCycleDetector 的源代码进行分析最后尽量详尽地回答这一问题。 + +重新回到文章开头提到的 `- allRetainedObjects` 方法: + +```objectivec +- (NSSet *)allRetainedObjects { + NSMutableArray *results = [[[super allRetainedObjects] allObjects] mutableCopy]; + + __attribute__((objc_precise_lifetime)) id anObject = self.object; + + void *blockObjectReference = (__bridge void *)anObject; + NSArray *allRetainedReferences = FBGetBlockStrongReferences(blockObjectReference); + + for (id object in allRetainedReferences) { + FBObjectiveCGraphElement *element = FBWrapObjectGraphElement(self, object, self.configuration); + if (element) { + [results addObject:element]; + } + } + + return [NSSet setWithArray:results]; +} +``` + +通过函数的符号我们也能够猜测出,上述方法中通过 `FBGetBlockStrongReferences` 获取 block 持有的所有强引用: + +```objectivec +NSArray *FBGetBlockStrongReferences(void *block) { + if (!FBObjectIsBlock(block)) { + return nil; + } + + NSMutableArray *results = [NSMutableArray new]; + + void **blockReference = block; + NSIndexSet *strongLayout = _GetBlockStrongLayout(block); + [strongLayout enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + void **reference = &blockReference[idx]; + + if (reference && (*reference)) { + id object = (id)(*reference); + + if (object) { + [results addObject:object]; + } + } + }]; + + return [results autorelease]; +} +``` + +而 `FBGetBlockStrongReferences` 是对另一个私有函数 `_GetBlockStrongLayout` 的封装,也是实现最有意思的部分。 + +### 几个必要的概念 + +在具体介绍 `_GetBlockStrongLayout` 函数的源代码之前,我希望先对其原理有一个简单的介绍,便于各位读者的理解;在这里有三个概念需要介绍,首先是 block 持有的对象都存在的位置。 + +#### 如何持有对象 + +在文章的上面曾经出现过 block 的结构体,不知道各位读者是否还有印象: + +```objectivec +struct BlockLiteral { + void *isa; + int flags; + int reserved; + void (*invoke)(void *, ...); + struct BlockDescriptor *descriptor; + // imported variables +}; +``` + +在每个 block 结构体的下面就会存放当前 block 持有的所有对象,无论强弱。我们可以做一个小实验来验证这个观点,我们在程序中声明这样一个 block: + +```objectivec +NSObject *firstObject = [NSObject new]; +__attribute__((objc_precise_lifetime)) NSObject *object = [NSObject new]; +__weak NSObject *secondObject = object; +NSObject *thirdObject = [NSObject new]; + +__unused void (^block)() = ^{ + __unused NSObject *first = firstObject; + __unused NSObject *second = secondObject; + __unused NSObject *third = thirdObject; +}; +``` + +然后在代码中打一个断点: + +![block-capture-var-layout](images/block-capture-var-layout.png) + + +> 上面代码中 block 由于被变量引用,执行了 `_Block_copy`,所以其类型为 `__NSMallocBlock__`,没有被变量引用的 block 都是 `__NSStackBlock__`。 + +1. 首先打印 block 变量的大小,因为 block 变量其实只是一个指向结构体的指针,所以大小为 8,而结构体的大小为 32; +2. 以 block 的地址为基址,偏移 32,得到一个指针 +3. 使用 `$3[0]` `$3[1]` `$3[2]` 依次打印地址为 `0x1001023b0` `0x1001023b8` `0x1001023c0` 的内容,可以发现它们就是 block 捕获的全部引用,前两个是强引用,最后的是弱引用 + +这可以得出一个结论:block 将其捕获的引用存放在结构体的下面,但是为什么这里的顺序并不是按照引用的顺序呢?接下来增加几个变量,再做另一次实验: + +![block-capture-strong-weak-orde](images/block-capture-strong-weak-order.png) + +在代码中多加入了几个对象之后,block 对持有的对象的布局的顺序依然是**强引用在前、弱引用在后**,我们不妨做一个假设:**block 会将强引用的对象排放在弱引用对象的前面**。但是这个假设能够帮助我们在**只有 block 但是没有上下文信息的情况下**区分哪些是强引用么?我觉得并不能,因为我们没有办法知道它们之间的分界线到底在哪里。 + +#### dispose_helper + +第二个需要介绍的是 `dispose_helper`,这是 `BlockDescriptor` 结构体中的一个指针: + +```objectivec +struct BlockDescriptor { + unsigned long int reserved; // NULL + unsigned long int size; + // optional helper functions + void (*copy_helper)(void *dst, void *src); // IFF (1<<25) + void (*dispose_helper)(void *src); // IFF (1<<25) + const char *signature; // IFF (1<<30) +}; +``` + +上面的结构体中有两个函数指针,`copy_helper` 用于 block 的拷贝,`dispose_helper` 用于 block 的 `dispose` 也就是 block 在析构的时候会调用这个函数指针,销毁自己持有的对象,而这个原理也是区别强弱引用的关键,因为在 `dispose_helper` 会对强引用发送 `release` 消息,对弱引用不会做任何的处理。 + +#### FBBlockStrongRelationDetector + +最后就是用于从 `dispose_helper` 接收消息的类 `FBBlockStrongRelationDetector` 了;它的实例在接受 `release` 消息时,并不会真正的释放,只会将标记 `_strong` 为 YES: + +```objectivec +- (oneway void)release { + _strong = YES; +} + +- (oneway void)trueRelease { + [super release]; +} +``` + +只有真正执行 `trueRelease` 的时候才会向对象发送 `release` 消息。 + +因为这个文件覆写了 `release` 方法,所以要在非 ARC 下编译: + +```objectivec +#if __has_feature(objc_arc) +#error This file must be compiled with MRR. Use -fno-objc-arc flag. +#endif +``` + +如果 block 持有了另一个 block 对象,`FBBlockStrongRelationDetector` 也可以将自身 fake 成为一个假的 block 防止在接收到关于 block 释放的消息时发生 crash: + +```objectivec +struct _block_byref_block; +@interface FBBlockStrongRelationDetector : NSObject { + // __block fakery + void *forwarding; + int flags; //refcount; + int size; + void (*byref_keep)(struct _block_byref_block *dst, struct _block_byref_block *src); + void (*byref_dispose)(struct _block_byref_block *); + void *captured[16]; +} +``` + +该类的实例在初始化时,会设置 `forwarding`、`byref_keep` 和 `byref_dispose`,后两个方法的实现都是空的,只是为了防止 crash: + +```objectivec ++ (id)alloc { + FBBlockStrongRelationDetector *obj = [super alloc]; + + // Setting up block fakery + obj->forwarding = obj; + obj->byref_keep = byref_keep_nop; + obj->byref_dispose = byref_dispose_nop; + + return obj; +} + +static void byref_keep_nop(struct _block_byref_block *dst, struct _block_byref_block *src) {} +static void byref_dispose_nop(struct _block_byref_block *param) {} +``` + +### 获取 block 强引用的对象 + +到现在为止,获取 block 强引用对象所需要的知识都介绍完了,接下来可以对私有方法 `_GetBlockStrongLayout` 进行分析了: + +```objectivec +static NSIndexSet *_GetBlockStrongLayout(void *block) { + struct BlockLiteral *blockLiteral = block; + + if ((blockLiteral->flags & BLOCK_HAS_CTOR) + || !(blockLiteral->flags & BLOCK_HAS_COPY_DISPOSE)) { + return nil; + } + + ... +} +``` + ++ 如果 block 有 Cpp 的构造器/析构器,说明它**持有的对象很有可能没有按照指针大小对齐**,我们很难检测到所有的对象 ++ 如果 block 没有 `dispose` 函数,说明它无法 `retain` 对象,也就是说我们也没有办法测试其强引用了哪些对象 + +```objectivec +static NSIndexSet *_GetBlockStrongLayout(void *block) { + ... + void (*dispose_helper)(void *src) = blockLiteral->descriptor->dispose_helper; + const size_t ptrSize = sizeof(void *); + const size_t elements = (blockLiteral->descriptor->size + ptrSize - 1) / ptrSize; + + void *obj[elements]; + void *detectors[elements]; + + for (size_t i = 0; i < elements; ++i) { + FBBlockStrongRelationDetector *detector = [FBBlockStrongRelationDetector new]; + obj[i] = detectors[i] = detector; + } + + @autoreleasepool { + dispose_helper(obj); + } + ... +} +``` + +1. 从 `BlockDescriptor` 取出 `dispose_helper` 以及 `size`(block 持有的所有对象的大小) +2. 通过 `(blockLiteral->descriptor->size + ptrSize - 1) / ptrSize` 向上取整,获取 block 持有的指针的数量 +3. 初始化两个包含 `elements` 个 `FBBlockStrongRelationDetector` 实例的数组,其中第一个数组用于传入 `dispose_helper`,第二个数组用于检测 `_strong` 是否被标记为 `YES` +4. 在自动释放池中执行 `dispose_helper(obj)`,释放 block 持有的对象 + +```objectivec +static NSIndexSet *_GetBlockStrongLayout(void *block) { + ... + NSMutableIndexSet *layout = [NSMutableIndexSet indexSet]; + + for (size_t i = 0; i < elements; ++i) { + FBBlockStrongRelationDetector *detector = (FBBlockStrongRelationDetector *)(detectors[i]); + if (detector.isStrong) { + [layout addIndex:i]; + } + + [detector trueRelease]; + } + + return layout; +} +``` + +因为 `dispose_helper` 只会调用 `release` 方法,但是这并不会导致我们的 `FBBlockStrongRelationDetector` 实例被释放掉,反而会标记 `_string` 属性,在这里我们只需要判断这个属性的真假,将对应索引加入数组,最后再调用 `trueRelease` 真正的释放对象。 + +我们可以执行下面的代码,分析其工作过程: + +```objectivec +NSObject *firstObject = [NSObject new]; +__attribute__((objc_precise_lifetime)) NSObject *object = [NSObject new]; +__weak NSObject *secondObject = object; +NSObject *thirdObject = [NSObject new]; + +__unused void (^block)() = ^{ + __unused NSObject *first = firstObject; + __unused NSObject *second = secondObject; + __unused NSObject *third = thirdObject; +}; + +FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; +[detector addCandidate:block]; +[detector findRetainCycles]; +``` + +在 `dispose_helper` 调用之前: + +![before-dispose-helpe](images/before-dispose-helper.jpeg) + +`obj` 数组中的每一个位置都存储了 `FBBlockStrongRelationDetector` 的实例,但是在 `dispose_helper` 调用之后: + +![after-dispose-helpe](images/after-dispose-helper.png) + +索引为 4 和 5 处的实例已经被清空了,这里对应的 `FBBlockStrongRelationDetector` 实例的 `strong` 已经被标记为 `YES`、加入到数组中并返回;最后也就获取了所有强引用的索引,同时得到了 block 强引用的对象。 + +## 总结 + +其实最开始笔者对这个 `dispose_helper` 实现的机制并不是特别的肯定,只是有一个猜测,但是在询问了 `FBBlockStrongRelationDetector` 的作者之后,才确定 `dispose_helper` 确实会负责向所有捕获的变量发送 `release` 消息,如果有兴趣可以看这个 [issue](https://github.com/facebook/FBRetainCycleDetector/issues/15)。这部分的代码其实最开始源于 mikeash 大神的 [Circle](https://github.com/mikeash/Circle),不过对于他是如何发现这一点的,笔者并不清楚,如果各位有相关的资料或者合理的解释,可以随时联系我。 + +> Follow: [Draveness · Github](https://github.com/Draveness) + + diff --git a/contents/FBRetainCycleDetector/images/after-dispose-helper.png b/contents/FBRetainCycleDetector/images/after-dispose-helper.png new file mode 100644 index 0000000..f91e481 Binary files /dev/null and b/contents/FBRetainCycleDetector/images/after-dispose-helper.png differ diff --git a/contents/FBRetainCycleDetector/images/before-dispose-helper.jpeg b/contents/FBRetainCycleDetector/images/before-dispose-helper.jpeg new file mode 100644 index 0000000..550c0f0 Binary files /dev/null and b/contents/FBRetainCycleDetector/images/before-dispose-helper.jpeg differ diff --git a/contents/FBRetainCycleDetector/images/block-capture-strong-weak-order.png b/contents/FBRetainCycleDetector/images/block-capture-strong-weak-order.png new file mode 100644 index 0000000..af3420d Binary files /dev/null and b/contents/FBRetainCycleDetector/images/block-capture-strong-weak-order.png differ diff --git a/contents/FBRetainCycleDetector/images/block-capture-var-layout.png b/contents/FBRetainCycleDetector/images/block-capture-var-layout.png new file mode 100644 index 0000000..863498c Binary files /dev/null and b/contents/FBRetainCycleDetector/images/block-capture-var-layout.png differ diff --git a/contents/FBRetainCycleDetector/images/block-superclass.png b/contents/FBRetainCycleDetector/images/block-superclass.png new file mode 100644 index 0000000..583a5e8 Binary files /dev/null and b/contents/FBRetainCycleDetector/images/block-superclass.png differ diff --git a/contents/FBRetainCycleDetector/images/block.jpg b/contents/FBRetainCycleDetector/images/block.jpg new file mode 100644 index 0000000..c2d9d9e Binary files /dev/null and b/contents/FBRetainCycleDetector/images/block.jpg differ diff --git a/contents/FBRetainCycleDetector/images/filtered-ivars.png b/contents/FBRetainCycleDetector/images/filtered-ivars.png new file mode 100644 index 0000000..841ea45 Binary files /dev/null and b/contents/FBRetainCycleDetector/images/filtered-ivars.png differ diff --git a/contents/FBRetainCycleDetector/images/get-ivar-layout.png b/contents/FBRetainCycleDetector/images/get-ivar-layout.png new file mode 100644 index 0000000..bd7fd00 Binary files /dev/null and b/contents/FBRetainCycleDetector/images/get-ivar-layout.png differ diff --git a/contents/FBRetainCycleDetector/images/get-ivars.png b/contents/FBRetainCycleDetector/images/get-ivars.png new file mode 100644 index 0000000..04e69de Binary files /dev/null and b/contents/FBRetainCycleDetector/images/get-ivars.png differ diff --git a/contents/FBRetainCycleDetector/images/retain-objects.png b/contents/FBRetainCycleDetector/images/retain-objects.png new file mode 100644 index 0000000..8bbfa87 Binary files /dev/null and b/contents/FBRetainCycleDetector/images/retain-objects.png differ diff --git "a/contents/FBRetainCycleDetector/\345\246\202\344\275\225\345\234\250 iOS \344\270\255\350\247\243\345\206\263\345\276\252\347\216\257\345\274\225\347\224\250\347\232\204\351\227\256\351\242\230.md" "b/contents/FBRetainCycleDetector/\345\246\202\344\275\225\345\234\250 iOS \344\270\255\350\247\243\345\206\263\345\276\252\347\216\257\345\274\225\347\224\250\347\232\204\351\227\256\351\242\230.md" new file mode 100644 index 0000000..0c9cbad --- /dev/null +++ "b/contents/FBRetainCycleDetector/\345\246\202\344\275\225\345\234\250 iOS \344\270\255\350\247\243\345\206\263\345\276\252\347\216\257\345\274\225\347\224\250\347\232\204\351\227\256\351\242\230.md" @@ -0,0 +1,233 @@ +# 如何在 iOS 中解决循环引用的问题 + +稍有常识的人都知道在 iOS 开发时,我们经常会遇到循环引用的问题,比如两个强指针相互引用,但是这种简单的情况作为稍有经验的开发者都会轻松地查找出来。 + +但是遇到下面这样的情况,如果只看其实现代码,也很难仅仅凭借肉眼上的观察以及简单的推理就能分析出其中存在的循环引用问题,更何况真实情况往往比这复杂的多: + +```objectivec +testObject1.object = testObject2; +testObject1.secondObject = testObject3; +testObject2.object = testObject4; +testObject2.secondObject = testObject5; +testObject3.object = testObject1; +testObject5.object = testObject6; +testObject4.object = testObject1; +testObject5.secondObject = testObject7; +testObject7.object = testObject2; +``` + +上述代码确实是存在循环引用的问题: + +![detector-retain-objects](images/retain-objects.png) + +这一次分享的内容就是用于检测循环引用的框架 [FBRetainCycleDetector]([https://github.com/facebook/FBRetainCycleDetector]) 我们会分几个部分来分析 FBRetainCycleDetector 是如何工作的: + +1. 检测循环引用的基本原理以及过程 +2. [检测涉及 NSObject 对象的循环引用问题](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/FBRetainCycleDetector/检测%20NSObject%20对象持有的强指针.md) +2. [检测涉及 Associated Object 关联对象的循环引用问题](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/FBRetainCycleDetector/如何实现%20iOS%20中的%20Associated%20Object.md) +3. 检测涉及 Block 的循环引用问题 + +这是四篇文章中的第一篇,我们会以类 `FBRetainCycleDetector` 的 `- findRetainCycles` 方法为入口,分析其实现原理以及运行过程。 + +简单介绍一下 `FBRetainCycleDetector` 的使用方法: + +```objectivec +_RCDTestClass *testObject = [_RCDTestClass new]; +testObject.object = testObject; + +FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; +[detector addCandidate:testObject]; +NSSet *retainCycles = [detector findRetainCycles]; + +NSLog(@"%@", retainCycles); +``` + +1. 初始化一个 `FBRetainCycleDetector` 的实例 +2. 调用 `- addCandidate:` 方法添加潜在的泄露对象 +3. 执行 `- findRetainCycles` 返回 `retainCycles` + +在控制台中的输出是这样的: + +```c +2016-07-29 15:26:42.043 xctest[30610:1003493] {( + ( + "-> _object -> _RCDTestClass " + ) +)} +``` + +说明 `FBRetainCycleDetector` 在代码中发现了循环引用。 + +## findRetainCycles 的实现 + +在具体开始分析 `FBRetainCycleDetector` 代码之前,我们可以先观察一下方法 `findRetainCycles` 的调用栈: + +```objectivec +- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)findRetainCycles +└── - (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)findRetainCyclesWithMaxCycleLength:(NSUInteger)length + └── - (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)_findRetainCyclesInObject:(FBObjectiveCGraphElement *)graphElement stackDepth:(NSUInteger)stackDepth + └── - (instancetype)initWithObject:(FBObjectiveCGraphElement *)object + └── - (FBNodeEnumerator *)nextObject + ├── - (NSArray<FBObjectiveCGraphElement *> *)_unwrapCycle:(NSArray<FBNodeEnumerator *> *)cycle + ├── - (NSArray<FBObjectiveCGraphElement *> *)_shiftToUnifiedCycle:(NSArray<FBObjectiveCGraphElement *> *)array + └── - (void)addObject:(ObjectType)anObject; +``` + +调用栈中最上面的两个简单方法的实现都是比较容易理解的: + +```objectivec +- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)findRetainCycles { + return [self findRetainCyclesWithMaxCycleLength:kFBRetainCycleDetectorDefaultStackDepth]; +} + +- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)findRetainCyclesWithMaxCycleLength:(NSUInteger)length { + NSMutableSet<NSArray<FBObjectiveCGraphElement *> *> *allRetainCycles = [NSMutableSet new]; + for (FBObjectiveCGraphElement *graphElement in _candidates) { + NSSet<NSArray<FBObjectiveCGraphElement *> *> *retainCycles = [self _findRetainCyclesInObject:graphElement + stackDepth:length]; + [allRetainCycles unionSet:retainCycles]; + } + [_candidates removeAllObjects]; + + return allRetainCycles; +} +``` + +`- findRetainCycles` 调用了 `- findRetainCyclesWithMaxCycleLength:` 传入了 `kFBRetainCycleDetectorDefaultStackDepth` 参数来限制查找的深度,如果超过该深度(默认为 10)就不会继续处理下去了(查找的深度的增加会对性能有非常严重的影响)。 + +在 `- findRetainCyclesWithMaxCycleLength:` 中,我们会遍历所有潜在的内存泄露对象 `candidate`,执行整个框架中最核心的方法 `- _findRetainCyclesInObject:stackDepth:`,由于这个方法的实现太长,这里会分几块对其进行介绍,并会省略其中的注释: + +```objectivec +- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)_findRetainCyclesInObject:(FBObjectiveCGraphElement *)graphElement + stackDepth:(NSUInteger)stackDepth { + NSMutableSet<NSArray<FBObjectiveCGraphElement *> *> *retainCycles = [NSMutableSet new]; + FBNodeEnumerator *wrappedObject = [[FBNodeEnumerator alloc] initWithObject:graphElement]; + + NSMutableArray<FBNodeEnumerator *> *stack = [NSMutableArray new]; + + NSMutableSet<FBNodeEnumerator *> *objectsOnPath = [NSMutableSet new]; + + ... +} +``` + +其实整个对象的相互引用情况可以看做一个**有向图**,对象之间的引用就是图的 `Edge`,每一个对象就是 `Vertex`,**查找循环引用的过程就是在整个有向图中查找环的过程**,所以在这里我们使用 DFS 来扫面图中的环,这些环就是对象之间的循环引用。 + +> 文章中并不会介绍 DFS 的原理,如果对 DFS 不了解的读者可以看一下这个[视频]([https://www.youtube.com/watch?v=tlPuVe5Otio]),或者找以下相关资料了解一下 DFS 的实现。 + +接下来就是 DFS 的实现: + +```objectivec +- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)_findRetainCyclesInObject:(FBObjectiveCGraphElement *)graphElement + stackDepth:(NSUInteger)stackDepth { + ... + [stack addObject:wrappedObject]; + + while ([stack count] > 0) { + @autoreleasepool { + FBNodeEnumerator *top = [stack lastObject]; + [objectsOnPath addObject:top]; + + FBNodeEnumerator *firstAdjacent = [top nextObject]; + if (firstAdjacent) { + + BOOL shouldPushToStack = NO; + + if ([objectsOnPath containsObject:firstAdjacent]) { + NSUInteger index = [stack indexOfObject:firstAdjacent]; + NSInteger length = [stack count] - index; + + if (index == NSNotFound) { + shouldPushToStack = YES; + } else { + NSRange cycleRange = NSMakeRange(index, length); + NSMutableArray<FBNodeEnumerator *> *cycle = [[stack subarrayWithRange:cycleRange] mutableCopy]; + [cycle replaceObjectAtIndex:0 withObject:firstAdjacent]; + + [retainCycles addObject:[self _shiftToUnifiedCycle:[self _unwrapCycle:cycle]]]; + } + } else { + shouldPushToStack = YES; + } + + if (shouldPushToStack) { + if ([stack count] < stackDepth) { + [stack addObject:firstAdjacent]; + } + } + } else { + [stack removeLastObject]; + [objectsOnPath removeObject:top]; + } + } + } + return retainCycles; +} +``` + +这里其实就是对 DFS 的具体实现,其中比较重要的有两点,一是使用 `nextObject` 获取下一个需要遍历的对象,二是对查找到的环进行处理和筛选;在这两点之中,第一点相对重要,因为 `nextObject` 的实现是调用 `allRetainedObjects` 方法获取被当前对象持有的对象,如果没有这个方法,我们就无法获取当前对象的邻接结点,更无从谈起遍历了: + +```objectivec +- (FBNodeEnumerator *)nextObject { + if (!_object) { + return nil; + } else if (!_retainedObjectsSnapshot) { + _retainedObjectsSnapshot = [_object allRetainedObjects]; + _enumerator = [_retainedObjectsSnapshot objectEnumerator]; + } + + FBObjectiveCGraphElement *next = [_enumerator nextObject]; + + if (next) { + return [[FBNodeEnumerator alloc] initWithObject:next]; + } + + return nil; +} +``` + +基本上所有图中的对象 `FBObjectiveCGraphElement` 以及它的子类 `FBObjectiveCBlock` `FBObjectiveCObject` 和 `FBObjectiveCNSCFTimer` 都实现了这个方法返回其持有的对象数组。获取数组之后,就再把其中的对象包装成新的 `FBNodeEnumerator` 实例,也就是下一个 `Vertex`。 + +因为使用 `- subarrayWithRange:` 方法获取的数组中的对象都是 `FBNodeEnumerator` 的实例,还需要一定的处理才能返回: + +1. - (NSArray<FBObjectiveCGraphElement *> *)_unwrapCycle:(NSArray<FBNodeEnumerator *> *)cycle +2. - (NSArray<FBObjectiveCGraphElement *> *)_shiftToUnifiedCycle:(NSArray<FBObjectiveCGraphElement *> *)array + + +`- _unwrapCycle:` 的作用是将数组中的每一个 `FBNodeEnumerator` 实例转换成 `FBObjectiveCGraphElement`: + +```objectivec +- (NSArray<FBObjectiveCGraphElement *> *)_unwrapCycle:(NSArray<FBNodeEnumerator *> *)cycle { + NSMutableArray *unwrappedArray = [NSMutableArray new]; + for (FBNodeEnumerator *wrapped in cycle) { + [unwrappedArray addObject:wrapped.object]; + } + + return unwrappedArray; +} +``` + +`- _shiftToUnifiedCycle:` 方法将每一个环中的元素按照**地址递增以及字母顺序**来排序,方法签名很好的说明了它们的功能,两个方法的代码就不展示了,它们的实现没有什么值得注意的地方: + +```objectivec +- (NSArray<FBObjectiveCGraphElement *> *)_shiftToUnifiedCycle:(NSArray<FBObjectiveCGraphElement *> *)array { + return [self _shiftToLowestLexicographically:[self _shiftBufferToLowestAddress:array]]; +} +``` + +方法的作用是防止出现**相同环的不同表示方式**,比如说下面的两个环其实是完全相同的: + +``` +-> object1 -> object2 +-> object2 -> object1 +``` + +在获取图中的环并排序好之后,就可以讲这些环 union 一下,去除其中重复的元素,最后返回所有查找到的循环引用了。 + +## 总结 + +到目前为止整个 `FBRetainCycleDetector` 的原理介绍大概就结束了,其原理完全是基于 DFS 算法:把整个对象的之间的引用情况当做图进行处理,查找其中的环,就找到了循环引用。不过原理真的很简单,如果这个 lib 的实现仅仅是这样的话,我也不会写几篇文章来专门分析这个框架,真正让我感兴趣的还是 `- allRetainedObjects` 方法**在各种对象以及 block 中获得它们强引用的对象的过程**,这也是之后的文章要分析的主要内容。 + +> Follow: [Draveness · Github](https://github.com/Draveness) + + diff --git "a/contents/FBRetainCycleDetector/\345\246\202\344\275\225\345\256\236\347\216\260 iOS \344\270\255\347\232\204 Associated Object.md" "b/contents/FBRetainCycleDetector/\345\246\202\344\275\225\345\256\236\347\216\260 iOS \344\270\255\347\232\204 Associated Object.md" new file mode 100644 index 0000000..c426d5f --- /dev/null +++ "b/contents/FBRetainCycleDetector/\345\246\202\344\275\225\345\256\236\347\216\260 iOS \344\270\255\347\232\204 Associated Object.md" @@ -0,0 +1,221 @@ +# 如何实现 iOS 中的 Associated Object + +这一篇文章是对 [FBRetainCycleDetector]([https://github.com/facebook/FBRetainCycleDetector]) 中实现的关联对象机制的分析;因为追踪的需要, FBRetainCycleDetector 重新实现了关联对象,本文主要就是对其实现关联对象的方法进行分析。 + +文章中涉及的类主要就是 `FBAssociationManager`: + +> FBAssociationManager is a tracker of object associations. For given object it can return all objects that are being retained by this object with objc_setAssociatedObject & retain policy. + +FBRetainCycleDetector 在对关联对象进行追踪时,修改了底层处理关联对象的两个 C 函数,`objc_setAssociatedObject` 和 `objc_removeAssociatedObjects`,在这里不会分析它是如何修改底层 C 语言函数实现的,如果想要了解相关的内容,可以阅读下面的文章。 + +> 关于如何动态修改 C 语言函数实现可以看[动态修改 C 语言函数的实现]([https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/fishhook/动态修改%20C%20语言函数的实现.md])这篇文章,使用的第三方框架是 [fishhook]([https://github.com/facebook/fishhook])。 + +## FBAssociationManager + +在 `FBAssociationManager` 的类方法 `+ hook` 调用时,fishhook 会修改 `objc_setAssociatedObject` 和 `objc_removeAssociatedObjects` 方法: + +```objectivec ++ (void)hook { +#if _INTERNAL_RCD_ENABLED + std::lock_guard<std::mutex> l(*FB::AssociationManager::hookMutex); + rcd_rebind_symbols((struct rcd_rebinding[2]){ + { + "objc_setAssociatedObject", + (void *)FB::AssociationManager::fb_objc_setAssociatedObject, + (void **)&FB::AssociationManager::fb_orig_objc_setAssociatedObject + }, + { + "objc_removeAssociatedObjects", + (void *)FB::AssociationManager::fb_objc_removeAssociatedObjects, + (void **)&FB::AssociationManager::fb_orig_objc_removeAssociatedObjects + }}, 2); + FB::AssociationManager::hookTaken = true; +#endif //_INTERNAL_RCD_ENABLED +} +``` + +将它们的实现替换为 `FB::AssociationManager:: fb_objc_setAssociatedObject` 以及 `FB::AssociationManager::fb_objc_removeAssociatedObjects` 这两个 Cpp 静态方法。 + +上面的两个方法实现都位于 `FB::AssociationManager` 的命名空间中: + +```objectivec +namespace FB { namespace AssociationManager { + using ObjectAssociationSet = std::unordered_set<void *>; + using AssociationMap = std::unordered_map<id, ObjectAssociationSet *>; + + static auto _associationMap = new AssociationMap(); + static auto _associationMutex = new std::mutex; + + static std::mutex *hookMutex(new std::mutex); + static bool hookTaken = false; + + ... +} +``` + +命名空间中有两个用于存储关联对象的数据结构: + ++ `AssociationMap` 用于存储从对象到 `ObjectAssociationSet *` 指针的映射 ++ `ObjectAssociationSet` 用于存储某对象所有关联对象的集合 + +其中还有几个比较重要的成员变量: + ++ `_associationMap` 就是 `AssociationMap` 的实例,是一个用于存储所有关联对象的数据结构 ++ `_associationMutex` 用于在修改关联对象时加锁,防止出现线程竞争等问题,导致不可预知的情况发生 ++ `hookMutex` 以及 `hookTaken` 都是在类方法 `+ hook` 调用时使用的,用于保证 hook 只会执行一次并保证线程安全 + +用于追踪关联对象的静态方法 `fb_objc_setAssociatedObject` 只会追踪强引用: + +```objectivec +static void fb_objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy) { + { + std::lock_guard<std::mutex> l(*_associationMutex); + if (policy == OBJC_ASSOCIATION_RETAIN || + policy == OBJC_ASSOCIATION_RETAIN_NONATOMIC) { + _threadUnsafeSetStrongAssociation(object, key, value); + } else { + // We can change the policy, we need to clear out the key + _threadUnsafeResetAssociationAtKey(object, key); + } + } + + fb_orig_objc_setAssociatedObject(object, key, value, policy); +} +``` + +`std::lock_guard<std::mutex> l(*_associationMutex)` 对 `fb_objc_setAssociatedObject` 过程加锁,防止死锁问题,不过 `_associationMutex` 会在作用域之外被释放。 + +通过输入的 `policy` 我们可以判断哪些是强引用对象,然后调用 `_threadUnsafeSetStrongAssociation` 追踪它们,如果不是强引用对象,通过 `_threadUnsafeResetAssociationAtKey` 将 `key` 对应的 `value` 删除,保证追踪的正确性: + +```objectivec +void _threadUnsafeSetStrongAssociation(id object, void *key, id value) { + if (value) { + auto i = _associationMap->find(object); + ObjectAssociationSet *refs; + if (i != _associationMap->end()) { + refs = i->second; + } else { + refs = new ObjectAssociationSet; + (*_associationMap)[object] = refs; + } + refs->insert(key); + } else { + _threadUnsafeResetAssociationAtKey(object, key); + } +} +``` + +`_threadUnsafeSetStrongAssociation` 会以 object 作为键,查找或者创建一个 `ObjectAssociationSet *` 集合,将新的 `key` 插入到集合中,当然,如果 `value == nil` 或者上面 `fb_objc_setAssociatedObject` 方法中传入的 `policy` 是非 `retain` 的就会调用 `_threadUnsafeResetAssociationAtKey ` 重置 `ObjectAssociationSet` 中的关联对象: + +```objectivec +void _threadUnsafeResetAssociationAtKey(id object, void *key) { + auto i = _associationMap->find(object); + + if (i == _associationMap->end()) { + return; + } + + auto *refs = i->second; + auto j = refs->find(key); + if (j != refs->end()) { + refs->erase(j); + } +} +``` + +同样在查找到对应的 `ObjectAssociationSet` 之后会擦除 `key` 对应的值,`_threadUnsafeRemoveAssociations` 的实现与这个方法也差不多,相较于 reset 方法移除某一个对象的**所有**关联对象,该方法仅仅移除了某一个 `key` 对应的值。 + +```objectivec +void _threadUnsafeRemoveAssociations(id object) { + if (_associationMap->size() == 0 ){ + return; + } + + auto i = _associationMap->find(object); + if (i == _associationMap->end()) { + return; + } + + auto *refs = i->second; + delete refs; + _associationMap->erase(i); +} +``` + + +调用 `_threadUnsafeRemoveAssociations` 的方法 `fb_objc_removeAssociatedObjects` 的实现也很简单,利用了上面的方法,并在执行结束后,使用原 `obj_removeAssociatedObjects` 方法对应的函数指针 `fb_orig_objc_removeAssociatedObjects` 移除关联对象: + +```objectivec +static void fb_objc_removeAssociatedObjects(id object) { + { + std::lock_guard<std::mutex> l(*_associationMutex); + _threadUnsafeRemoveAssociations(object); + } + + fb_orig_objc_removeAssociatedObjects(object); +} +``` + +## FBObjectiveCGraphElement 获取关联对象 + +因为在获取某一个对象持有的所有强引用时,不可避免地需要获取其强引用的关联对象;因此我们也就需要使用 `FBAssociationManager` 提供的 `+ associationsForObject:` 接口获取所有**强引用**关联对象: + +```objectivec +- (NSSet *)allRetainedObjects { + NSArray *retainedObjectsNotWrapped = [FBAssociationManager associationsForObject:_object]; + NSMutableSet *retainedObjects = [NSMutableSet new]; + + for (id obj in retainedObjectsNotWrapped) { + FBObjectiveCGraphElement *element = FBWrapObjectGraphElementWithContext(self, obj, _configuration, @[@"__associated_object"]); + if (element) { + [retainedObjects addObject:element]; + } + } + + return retainedObjects; +} +``` + +这个接口调用我们在上一节中介绍的 `_associationMap`,最后得到某一个对象的所有关联对象的强引用: + +```objectivec ++ (NSArray *)associationsForObject:(id)object { + return FB::AssociationManager::associations(object); +} + +NSArray *associations(id object) { + std::lock_guard<std::mutex> l(*_associationMutex); + if (_associationMap->size() == 0 ){ + return nil; + } + + auto i = _associationMap->find(object); + if (i == _associationMap->end()) { + return nil; + } + + auto *refs = i->second; + + NSMutableArray *array = [NSMutableArray array]; + for (auto &key: *refs) { + id value = objc_getAssociatedObject(object, key); + if (value) { + [array addObject:value]; + } + } + + return array; +} +``` + +这部分的代码没什么好解释的,遍历所有的 `key`,检测是否真的存在关联对象,然后加入可变数组,最后返回。 + +## 总结 + +FBRetainCycleDetector 为了追踪某一 `NSObject` 对关联对象的引用,重新实现了关联对象模块,不过其实现与 ObjC 运行时中对关联对象的实现其实所差无几,如果对运行时中的关联对象实现原理有兴趣的话,可以看[关联对象 AssociatedObject 完全解析](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/关联对象%20AssociatedObject%20完全解析.md)这篇文章,它介绍了底层运行时中的关联对象的实现。 + +这是 FBRetainCycleDetector 系列文章中的第三篇,第四篇也是最后一篇文章会介绍 FBRetainCycleDetector 是如何获取 block 持有的强引用的,这也是我觉得整个框架中实现最精彩的一部分。 + +> Follow: [Draveness · Github](https://github.com/Draveness) + + diff --git "a/contents/FBRetainCycleDetector/\346\243\200\346\265\213 NSObject \345\257\271\350\261\241\346\214\201\346\234\211\347\232\204\345\274\272\346\214\207\351\222\210.md" "b/contents/FBRetainCycleDetector/\346\243\200\346\265\213 NSObject \345\257\271\350\261\241\346\214\201\346\234\211\347\232\204\345\274\272\346\214\207\351\222\210.md" new file mode 100644 index 0000000..7a142be --- /dev/null +++ "b/contents/FBRetainCycleDetector/\346\243\200\346\265\213 NSObject \345\257\271\350\261\241\346\214\201\346\234\211\347\232\204\345\274\272\346\214\207\351\222\210.md" @@ -0,0 +1,449 @@ +# 检测 NSObject 对象持有的强指针 + +在上一篇文章中介绍了 `FBRetainCycleDetector` 的基本工作原理,这一篇文章中我们开始分析它是如何从每一个对象中获得它持有的强指针的。 + +> 如果没有看第一篇文章这里还是最好看一下,了解一下 `FBRetainCycleDetector` 的工作原理,[如何在 iOS 中解决循环引用的问题](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/FBRetainCycleDetector/如何在%20iOS%20中解决循环引用的问题.md)。 + +`FBRetainCycleDetector` 获取对象的强指针是通过 `FBObjectiveCObject` 类的 `- allRetainedObjects` 方法,这一方法是通过其父类 `FBObjectiveCGraphElement` 继承过来的,只是内部有着不同的实现。 + +## allRetainedObjects 方法 + +我们会以 `XXObject` 为例演示 `- allRetainedObjects` 方法的调用过程: + +```objectivec +#import <Foundation/Foundation.h> + +@interface XXObject : NSObject + +@property (nonatomic, strong) id first; +@property (nonatomic, weak) id second; +@property (nonatomic, strong) id third; +@property (nonatomic, strong) id forth; +@property (nonatomic, weak) id fifth; +@property (nonatomic, strong) id sixth; + +@end +``` + +使用 `FBRetainCycleDetector` 的代码如下: + +```objectivec +XXObject *object = [[XXObject alloc] init]; + +FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; +[detector addCandidate:object]; +__unused NSSet *cycles = [detector findRetainCycles]; +``` + +在 `FBObjectiveCObject` 中,`- allRetainedObjects` 方法只是调用了 `- _unfilteredRetainedObjects`,然后进行了过滤,文章主要会对 `- _unfilteredRetainedObjects` 的实现进行分析: + +```objectivec +- (NSSet *)allRetainedObjects { + NSArray *unfiltered = [self _unfilteredRetainedObjects]; + return [self filterObjects:unfiltered]; +} +``` + +方法 `- _unfilteredRetainedObjects` 的实现代码还是比较多的,这里会将代码分成几个部分,首先是最重要的部分:如何得到对象持有的强引用: + +```objectivec +- (NSArray *)_unfilteredRetainedObjects + NSArray *strongIvars = FBGetObjectStrongReferences(self.object, self.configuration.layoutCache); + + NSMutableArray *retainedObjects = [[[super allRetainedObjects] allObjects] mutableCopy]; + + for (id<FBObjectReference> ref in strongIvars) { + id referencedObject = [ref objectReferenceFromObject:self.object]; + + if (referencedObject) { + NSArray<NSString *> *namePath = [ref namePath]; + FBObjectiveCGraphElement *element = FBWrapObjectGraphElementWithContext(self, + referencedObject, + self.configuration, + namePath); + if (element) { + [retainedObjects addObject:element]; + } + } + } + + ... +} +``` + +获取强引用是通过 `FBGetObjectStrongReferences` 这一函数: + +```objectivec +NSArray<id<FBObjectReference>> *FBGetObjectStrongReferences(id obj, + NSMutableDictionary<Class, NSArray<id<FBObjectReference>> *> *layoutCache) { + NSMutableArray<id<FBObjectReference>> *array = [NSMutableArray new]; + + __unsafe_unretained Class previousClass = nil; + __unsafe_unretained Class currentClass = object_getClass(obj); + + while (previousClass != currentClass) { + NSArray<id<FBObjectReference>> *ivars; + + if (layoutCache && currentClass) { + ivars = layoutCache[currentClass]; + } + + if (!ivars) { + ivars = FBGetStrongReferencesForClass(currentClass); + if (layoutCache && currentClass) { + layoutCache[(id<NSCopying>)currentClass] = ivars; + } + } + [array addObjectsFromArray:ivars]; + + previousClass = currentClass; + currentClass = class_getSuperclass(currentClass); + } + + return [array copy]; +} +``` + +上面代码的核心部分是执行 `FBGetStrongReferencesForClass` 返回 `currentClass` 中的强引用,只是在这里我们递归地查找了所有父类的指针,并且加入了缓存以加速查找强引用的过程,接下来就是从对象的结构中获取强引用的过程了: + +```objectivec +static NSArray<id<FBObjectReference>> *FBGetStrongReferencesForClass(Class aCls) { + NSArray<id<FBObjectReference>> *ivars = [FBGetClassReferences(aCls) filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { + if ([evaluatedObject isKindOfClass:[FBIvarReference class]]) { + FBIvarReference *wrapper = evaluatedObject; + return wrapper.type != FBUnknownType; + } + return YES; + }]]; + + const uint8_t *fullLayout = class_getIvarLayout(aCls); + + if (!fullLayout) { + return nil; + } + + NSUInteger minimumIndex = FBGetMinimumIvarIndex(aCls); + NSIndexSet *parsedLayout = FBGetLayoutAsIndexesForDescription(minimumIndex, fullLayout); + + NSArray<id<FBObjectReference>> *filteredIvars = + [ivars filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id<FBObjectReference> evaluatedObject, + NSDictionary *bindings) { + return [parsedLayout containsIndex:[evaluatedObject indexInIvarLayout]]; + }]]; + + return filteredIvars; +} +``` + +该方法的实现大约有三个部分: + +1. 调用 `FBGetClassReferences` 从类中获取它指向的所有引用,无论是强引用或者是弱引用 +2. 调用 `FBGetLayoutAsIndexesForDescription` 从类的变量布局中获取强引用的位置信息 +3. 使用 `NSPredicate` 过滤数组中的弱引用 + +### 获取类的 Ivar 数组 + +`FBGetClassReferences` 方法主要调用 runtime 中的 `class_copyIvarList` 得到类的所有 `ivar`: + +> 这里省略对结构体属性的处理,因为太过复杂,并且涉及大量的C++ 代码,有兴趣的读者可以查看 `FBGetReferencesForObjectsInStructEncoding` 方法的实现。 + +```objectivec +NSArray<id<FBObjectReference>> *FBGetClassReferences(Class aCls) { + NSMutableArray<id<FBObjectReference>> *result = [NSMutableArray new]; + + unsigned int count; + Ivar *ivars = class_copyIvarList(aCls, &count); + + for (unsigned int i = 0; i < count; ++i) { + Ivar ivar = ivars[i]; + FBIvarReference *wrapper = [[FBIvarReference alloc] initWithIvar:ivar]; + [result addObject:wrapper]; + } + free(ivars); + + return [result copy]; +} +``` + +上述实现还是非常直接的,遍历 `ivars` 数组,使用 `FBIvarReference` 将其包装起来然后加入 `result` 中,其中的类 `FBIvarReference` 仅仅起到了一个包装的作用,将 Ivar 中保存的各种属性全部保存起来: + +```objectivec +typedef NS_ENUM(NSUInteger, FBType) { + FBObjectType, + FBBlockType, + FBStructType, + FBUnknownType, +}; + +@interface FBIvarReference : NSObject <FBObjectReference> + +@property (nonatomic, copy, readonly, nullable) NSString *name; +@property (nonatomic, readonly) FBType type; +@property (nonatomic, readonly) ptrdiff_t offset; +@property (nonatomic, readonly) NSUInteger index; +@property (nonatomic, readonly, nonnull) Ivar ivar; + +- (nonnull instancetype)initWithIvar:(nonnull Ivar)ivar; + +@end +``` + +包括属性的名称、类型、偏移量以及索引,类型是通过[类型编码](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html)来获取的,在 `FBIvarReference` 的实例初始化时,会通过私有方法 `- _convertEncodingToType:` 将类型编码转换为枚举类型: + +```objectivec +- (FBType)_convertEncodingToType:(const char *)typeEncoding { + if (typeEncoding[0] == '{') return FBStructType; + + if (typeEncoding[0] == '@') { + if (strncmp(typeEncoding, "@?", 2) == 0) return FBBlockType; + return FBObjectType; + } + + return FBUnknownType; +} +``` + +当代码即将从 `FBGetClassReferences` 方法中返回时,使用 lldb 打印 `result` 中的所有元素: + +![get-ivars](./images/get-ivars.png) + +上述方法成功地从 `XXObject` 类中获得了正确的属性数组,不过这些数组中不止包含了强引用,还有被 `weak` 标记的弱引用: + +```objectivec +<__NSArrayM 0x7fdac0f31860>( + [_first, index: 1], + [_second, index: 2], + [_third, index: 3], + [_forth, index: 4], + [_fifth, index: 5], + [_sixth, index: 6] +) +``` + +### 获取 Ivar Layout + +当我们取出了 `XXObject` 中所有的属性之后,还需要对其中的属性进行过滤;那么我们如何判断一个属性是强引用还是弱引用呢?Objective-C 中引入了 Ivar Layout 的概念,对类中的各种属性的强弱进行描述。 + +它是如何工作的呢,我们先继续执行 `FBGetStrongReferencesForClass` 方法: + +![get-ivar-layout](./images/get-ivar-layout.png) + +在 ObjC 运行时中的 `class_getIvarLayout` 可以获取某一个类的 Ivar Layout,而 `XXObject` 的 Ivar Layout 是什么样的呢? + +```c +(lldb) po fullLayout +"\x01\x12\x11" +``` + +Ivar Layout 就是一系列的字符,每两个一组,比如 `\xmn`,每一组 Ivar Layout 中第一位表示有 `m` 个非强属性,第二位表示接下来有 `n` 个强属性;如果没有明白,我们以 `XXObject` 为例演示一下: + +```objectivec +@interface XXObject : NSObject + +@property (nonatomic, strong) id first; +@property (nonatomic, weak) id second; +@property (nonatomic, strong) id third; +@property (nonatomic, strong) id forth; +@property (nonatomic, weak) id fifth; +@property (nonatomic, strong) id sixth; + +@end +``` + ++ 第一组的 `\x01` 表示有 0 个非强属性,然后有 1 个强属性 `first` ++ 第二组的 `\x12` 表示有 1 个非强属性 `second`,然后有 2 个强属性 `third` `forth` ++ 第三组的 `\x11` 表示有 1 个非强属性 `fifth`, 然后有 1 个强属性 `sixth` + +在对 Ivar Layout 有一定了解之后,我们可以继续对 `FBGetStrongReferencesForClass` 分析了,下面要做的就是使用 Ivar Layout 提供的信息过滤其中的所有非强引用,而这就需要两个方法的帮助,首先需要 `FBGetMinimumIvarIndex` 方法获取变量索引的最小值: + +```objectivec +static NSUInteger FBGetMinimumIvarIndex(__unsafe_unretained Class aCls) { + NSUInteger minimumIndex = 1; + unsigned int count; + Ivar *ivars = class_copyIvarList(aCls, &count); + + if (count > 0) { + Ivar ivar = ivars[0]; + ptrdiff_t offset = ivar_getOffset(ivar); + minimumIndex = offset / (sizeof(void *)); + } + + free(ivars); + + return minimumIndex; +} +``` + +然后执行 `FBGetLayoutAsIndexesForDescription(minimumIndex, fullLayout)` 获取所有强引用的 `NSRange`: + +```objectivec +static NSIndexSet *FBGetLayoutAsIndexesForDescription(NSUInteger minimumIndex, const uint8_t *layoutDescription) { + NSMutableIndexSet *interestingIndexes = [NSMutableIndexSet new]; + NSUInteger currentIndex = minimumIndex; + + while (*layoutDescription != '\x00') { + int upperNibble = (*layoutDescription & 0xf0) >> 4; + int lowerNibble = *layoutDescription & 0xf; + + currentIndex += upperNibble; + [interestingIndexes addIndexesInRange:NSMakeRange(currentIndex, lowerNibble)]; + currentIndex += lowerNibble; + + ++layoutDescription; + } + + return interestingIndexes; +} +``` + +因为高位表示非强引用的数量,所以我们要加上 `upperNibble`,然后 `NSMakeRange(currentIndex, lowerNibble)` 就是强引用的范围;略过 `lowerNibble` 长度的索引,移动 `layoutDescription` 指针,直到所有的 `NSRange` 都加入到了 `interestingIndexes` 这一集合中,就可以返回了。 + +### 过滤数组中的弱引用 + +在上一阶段由于已经获取了强引用的范围,在这里我们直接使用 `NSPredicate` 谓词来进行过滤就可以了: + +```objectivec +NSArray<id<FBObjectReference>> *filteredIvars = +[ivars filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id<FBObjectReference> evaluatedObject, + NSDictionary *bindings) { + return [parsedLayout containsIndex:[evaluatedObject indexInIvarLayout]]; +}]]; +``` + +![filtered-ivars](./images/filtered-ivars.png) + +==== + +接下来,我们回到文章开始的 `- _unfilteredRetainedObjects` 方法: + +```objectivec +- (NSSet *)allRetainedObjects { + NSArray *strongIvars = FBGetObjectStrongReferences(self.object, self.configuration.layoutCache); + + NSMutableArray *retainedObjects = [[[super allRetainedObjects] allObjects] mutableCopy]; + + for (id<FBObjectReference> ref in strongIvars) { + id referencedObject = [ref objectReferenceFromObject:self.object]; + + if (referencedObject) { + NSArray<NSString *> *namePath = [ref namePath]; + FBObjectiveCGraphElement *element = FBWrapObjectGraphElementWithContext(self, + referencedObject, + self.configuration, + namePath); + if (element) { + [retainedObjects addObject:element]; + } + } + } + + ... +} +``` + +`FBGetObjectStrongReferences` 只是返回 `id<FBObjectReference>` 对象,还需要 `FBWrapObjectGraphElementWithContext` 把它进行包装成 `FBObjectiveCGraphElement`: + +```objectivec +FBObjectiveCGraphElement *FBWrapObjectGraphElementWithContext(id object, + FBObjectGraphConfiguration *configuration, + NSArray<NSString *> *namePath) { + if (FBObjectIsBlock((__bridge void *)object)) { + return [[FBObjectiveCBlock alloc] initWithObject:object + configuration:configuration + namePath:namePath]; + } else { + if ([object_getClass(object) isSubclassOfClass:[NSTimer class]] && + configuration.shouldInspectTimers) { + return [[FBObjectiveCNSCFTimer alloc] initWithObject:object + configuration:configuration + namePath:namePath]; + } else { + return [[FBObjectiveCObject alloc] initWithObject:object + configuration:configuration + namePath:namePath]; + } + } +} +``` + +最后会把封装好的实例添加到 `retainedObjects` 数组中。 + +`- _unfilteredRetainedObjects` 同时也要处理集合类,比如数组或者字典,但是如果是无缝桥接的 CF 集合,或者是元类,虽然它们可能遵循 `NSFastEnumeration` 协议,但是在这里并不会对它们进行处理: + +```objectivec +- (NSArray *)_unfilteredRetainedObjects { + ... + + if ([NSStringFromClass(aCls) hasPrefix:@"__NSCF"]) { + return retainedObjects; + } + + if (class_isMetaClass(aCls)) { + return nil; + } + + ... +} +``` + +在遍历内容时,Mutable 的集合类中的元素可能会改变,所以会重试多次以确保集合类中的所有元素都被获取到了: + +```objectivec +- (NSArray *)_unfilteredRetainedObjects { + ... + + if ([aCls conformsToProtocol:@protocol(NSFastEnumeration)]) { + + NSInteger tries = 10; + for (NSInteger i = 0; i < tries; ++i) { + NSMutableSet *temporaryRetainedObjects = [NSMutableSet new]; + @try { + for (id subobject in self.object) { + [temporaryRetainedObjects addObject:FBWrapObjectGraphElement(subobject, self.configuration)]; + [temporaryRetainedObjects addObject:FBWrapObjectGraphElement([self.object objectForKey:subobject], self.configuration)]; + } + } + @catch (NSException *exception) { + continue; + } + + [retainedObjects addObjectsFromArray:[temporaryRetainedObjects allObjects]]; + break; + } + } + + return retainedObjects; +} +``` + +这里将遍历集合中的元素的代码放入了 `@try` 中,如果在遍历时插入了其它元素,就会抛出异常,然后 `continue` 重新遍历集合,最后返回所有持有的对象。 + +最后的过滤部分会使用 `FBObjectGraphConfiguration` 中的 `filterBlocks` 将不需要加入集合中的元素过滤掉: + +```objectivec +- (NSSet *)filterObjects:(NSArray *)objects { + NSMutableSet *filtered = [NSMutableSet new]; + + for (FBObjectiveCGraphElement *reference in objects) { + if (![self _shouldBreakGraphEdgeFromObject:self toObject:reference]) { + [filtered addObject:reference]; + } + } + + return filtered; +} + +- (BOOL)_shouldBreakGraphEdgeFromObject:(FBObjectiveCGraphElement *)fromObject + toObject:(FBObjectiveCGraphElement *)toObject { + for (FBGraphEdgeFilterBlock filterBlock in _configuration.filterBlocks) { + if (filterBlock(fromObject, toObject) == FBGraphEdgeInvalid) return YES; + } + + return NO; +} +``` + +## 总结 + +`FBRetainCycleDetector` 在对象中查找强引用取决于类的 Ivar Layout,它为我们提供了与属性引用强弱有关的信息,帮助筛选强引用。 + diff --git a/IQKeyboardManager/images/IQKeyboardManager-Hierarchy.png b/contents/IQKeyboardManager/images/IQKeyboardManager-Hierarchy.png similarity index 100% rename from IQKeyboardManager/images/IQKeyboardManager-Hierarchy.png rename to contents/IQKeyboardManager/images/IQKeyboardManager-Hierarchy.png diff --git a/IQKeyboardManager/images/IQKeyboardManager-hide-keyboard.png b/contents/IQKeyboardManager/images/IQKeyboardManager-hide-keyboard.png similarity index 100% rename from IQKeyboardManager/images/IQKeyboardManager-hide-keyboard.png rename to contents/IQKeyboardManager/images/IQKeyboardManager-hide-keyboard.png diff --git a/IQKeyboardManager/images/IQToolBar.png b/contents/IQKeyboardManager/images/IQToolBar.png similarity index 100% rename from IQKeyboardManager/images/IQToolBar.png rename to contents/IQKeyboardManager/images/IQToolBar.png diff --git a/IQKeyboardManager/images/IQToolBarItem.png b/contents/IQKeyboardManager/images/IQToolBarItem.png similarity index 100% rename from IQKeyboardManager/images/IQToolBarItem.png rename to contents/IQKeyboardManager/images/IQToolBarItem.png diff --git a/IQKeyboardManager/images/UITextView-Notification-IQKeyboardManager.png b/contents/IQKeyboardManager/images/UITextView-Notification-IQKeyboardManager.png similarity index 100% rename from IQKeyboardManager/images/UITextView-Notification-IQKeyboardManager.png rename to contents/IQKeyboardManager/images/UITextView-Notification-IQKeyboardManager.png diff --git a/IQKeyboardManager/images/easiest-integration-demo.png b/contents/IQKeyboardManager/images/easiest-integration-demo.png similarity index 100% rename from IQKeyboardManager/images/easiest-integration-demo.png rename to contents/IQKeyboardManager/images/easiest-integration-demo.png diff --git a/IQKeyboardManager/images/notification-IQKeyboardManager.png b/contents/IQKeyboardManager/images/notification-IQKeyboardManager.png similarity index 100% rename from IQKeyboardManager/images/notification-IQKeyboardManager.png rename to contents/IQKeyboardManager/images/notification-IQKeyboardManager.png diff --git "a/IQKeyboardManager/\343\200\216\351\233\266\350\241\214\344\273\243\347\240\201\343\200\217\350\247\243\345\206\263\351\224\256\347\233\230\351\201\256\346\214\241\351\227\256\351\242\230\357\274\210iOS\357\274\211.md" "b/contents/IQKeyboardManager/\343\200\216\351\233\266\350\241\214\344\273\243\347\240\201\343\200\217\350\247\243\345\206\263\351\224\256\347\233\230\351\201\256\346\214\241\351\227\256\351\242\230\357\274\210iOS\357\274\211.md" similarity index 100% rename from "IQKeyboardManager/\343\200\216\351\233\266\350\241\214\344\273\243\347\240\201\343\200\217\350\247\243\345\206\263\351\224\256\347\233\230\351\201\256\346\214\241\351\227\256\351\242\230\357\274\210iOS\357\274\211.md" rename to "contents/IQKeyboardManager/\343\200\216\351\233\266\350\241\214\344\273\243\347\240\201\343\200\217\350\247\243\345\206\263\351\224\256\347\233\230\351\201\256\346\214\241\351\227\256\351\242\230\357\274\210iOS\357\274\211.md" diff --git a/contents/KVOController/KVOController.md b/contents/KVOController/KVOController.md new file mode 100644 index 0000000..7b8f187 --- /dev/null +++ b/contents/KVOController/KVOController.md @@ -0,0 +1,447 @@ +# 如何优雅地使用 KVO + +KVO 作为 iOS 中一种强大并且有效的机制,为 iOS 开发者们提供了很多的便利;我们可以使用 KVO 来检测对象属性的变化、快速做出响应,这能够为我们在开发强交互、响应式应用以及实现视图和模型的双向绑定时提供大量的帮助。 + +但是在大多数情况下,除非遇到不用 KVO 无法解决的问题,笔者都会尽量避免它的使用,这并不是因为 KVO 有性能问题或者使用场景不多,总重要的原因是 KVO 的使用是在是太 ** **麻烦**了。 + +![trouble](images/trouble.jpg) + +使用 KVO 时,既需要进行**注册成为某个对象属性的观察者**,还要在合适的时间点将自己**移除**,再加上需要**覆写一个又臭又长的方法**,并在方法里**判断这次是不是自己要观测的属性发生了变化**,每次想用 KVO 解决一些问题的时候,作者的第一反应就是头疼,这篇文章会给为 KVO 所苦的开发者提供一种更优雅的解决方案。 + +## 使用 KVO + +不过在介绍如何优雅地使用 KVO 之前,我们先来回忆一下,在通常情况下,我们是如何使用 KVO 进行键值观测的。 + +首先,我们有一个 `Fizz` 类,其中包含一个 `number` 属性,它在初始化时会自动被赋值为 `@0`: + +```objectivec +// Fizz.h +@interface Fizz : NSObject + +@property (nonatomic, strong) NSNumber *number; + +@end + +// Fizz.m +@implementation Fizz + +- (instancetype)init { + if (self = [super init]) { + _number = @0; + } + return self; +} + +@end +``` + +我们想在 `Fizz` 对象中的 `number` 对象发生改变时获得通知得到**新**的和**旧**的值,这时我们就要祭出 `-addObserver:forKeyPath:options:context` 方法来监控 `number` 属性的变化: + +```objectivec +Fizz *fizz = [[Fizz alloc] init]; +[fizz addObserver:self + forKeyPath:@"number" + options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld + context:nil]; +fizz.number = @2; +``` + +在将当前对象 `self `注册成为 `fizz` 的观察者之后,我们需要在当前对象中覆写 `-observeValueForKeyPath:ofObject:change:context:` 方法: + +```objectivec +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { + if ([keyPath isEqualToString:@"number"]) { + NSLog(@"%@", change); + } +} +``` + +在大多数情况下我们只需要对比 `keyPath` 的值,就可以知道我们到底监控的是哪个对象,但是在更复杂的业务场景下,使用 `context` 上下文以及其它辅助手段才能够帮助我们更加精准地确定被观测的对象。 + +但是当上述代码运行时,虽然可以成功打印出 `change` 字典,但是却会发生崩溃,你会在控制台中看到下面的内容: + +```objectivec +2017-02-26 23:44:19.666 KVOTest[15888:513229] { + kind = 1; + new = 2; + old = 0; +} +2017-02-26 23:44:19.720 KVOTest[15888:513229] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x60800001dd20 of class Fizz was deallocated while key value observers were still registered with it. Current observation info: <NSKeyValueObservationInfo 0x60800003d320> ( +<NSKeyValueObservance 0x608000057310: Observer: 0x7fa098f07590, Key path: number, Options: <New: YES, Old: YES, Prior: NO> Context: 0x0, Property: 0x608000057400> +)' +``` + +这是因为 `fizz` 对象没有被其它对象引用,在脱离 `viewDidLoad` 作用于之后就被回收了,然而在 `-dealloc` 时,并没有移除观察者,所以会造成崩溃。 + +我们可以使用下面的代码来验证上面的结论是否正确: + +```objectivec +// Fizz.h +@interface Fizz : NSObject + +@property (nonatomic, strong) NSNumber *number; +@property (nonatomic, weak) NSObject *observer; + +@end + +// Fizz.m +@implementation Fizz + +- (instancetype)init { + if (self = [super init]) { + _number = @0; + } + return self; +} + +- (void)dealloc { + [self removeObserver:self.observer forKeyPath:@"number"]; +} + +@end +``` + +在 `Fizz` 类的接口中添加一个 `observer` 弱引用来持有对象的观察者,并在对象 `-dealloc` 时将它移除,重新运行这段代码,就不会发生崩溃了。 + +![not-crash-with-remove-observer-when-deallo](images/not-crash-with-remove-observer-when-dealloc.png) + +由于没有移除观察者导致崩溃是使用 KVO 时经常会遇到的问题之一,解决办法其实有很多,我们在这里简单介绍一个,使用当前对象持有被观测的对象,并在当前对象 `-dealloc` 时,移除观察者: + +```objectivec +- (void)viewDidLoad { + [super viewDidLoad]; + self.fizz = [[Fizz alloc] init]; + [self.fizz addObserver:self + forKeyPath:@"number" + options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld + context:nil]; + self.fizz.number = @2; +} + +- (void)dealloc { + [self.fizz removeObserver:self forKeyPath:@"number"]; +} +``` + +这也是我们经常使用来避免崩溃的办法,但是在笔者看来也是非常的不优雅,除了上述的崩溃问题,使用 KVO 的过程也非常的别扭和痛苦: + +1. 需要手动**移除观察者**,且移除观察者的**时机必须合适**; +2. 注册观察者的代码和事件发生处的代码上下文不同,**传递上下文**是通过 `void *` 指针; +3. 需要覆写 `-observeValueForKeyPath:ofObject:change:context:` 方法,比较麻烦; +4. 在复杂的业务逻辑中,准确判断被观察者相对比较麻烦,有多个被观测的对象和属性时,需要在方法中写大量的 `if` 进行判断; + +虽然上述几个问题并不影响 KVO 的使用,不过这也足够成为笔者尽量不使用 KVO 的理由了。 + +## 优雅地使用 KVO + +如何优雅地解决上一节提出的几个问题呢?我们在这里只需要使用 Facebook 开源的 [KVOController](https://github.com/facebook/KVOController) 框架就可以优雅地解决这些问题了。 + +如果想要实现同样的业务需求,当使用 KVOController 解决上述问题时,只需要以下代码就可以达到与上一节中**完全相同**的效果: + +```objectivec +[self.KVOController observe:self.fizz + keyPath:@"number" + options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld + block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) { + NSLog(@"%@", change); + }]; +``` + +我们可以在任意对象上**获得** `KVOController` 对象,然后调用它的实例方法 `-observer:keyPath:options:block:` 就可以检测某个对象对应的属性了,该方法传入的参数还是非常容易理解的,在 block 中也可以获得所有与 KVO 有关的参数。 + +使用 KVOController 进行键值观测可以说完美地解决了在使用原生 KVO 时遇到的各种问题。 + +1. 不需要手动移除观察者; +2. 实现 KVO 与事件发生处的代码上下文相同,不需要跨方法传参数; +3. 使用 block 来替代方法能够减少使用的复杂度,提升使用 KVO 的体验; +4. 每一个 `keyPath` 会对应一个属性,不需要在 block 中使用 `if` 判断 `keyPath`; + +## KVOController 的实现 + +KVOController 其实是对 Cocoa 中 KVO 的封装,它的实现其实也很简单,整个框架中只有两个实现文件,先来简要看一下 KVOController 如何为所有的 `NSObject` 对象都提供 `-KVOController` 属性的吧。 + +### 分类和 KVOController 的初始化 + +KVOController 不止为 Cocoa Touch 中所有的对象提供了 `-KVOController` 属性还提供了另一个 `KVOControllerNonRetaining` 属性,实现方法就是分类和 ObjC Runtime。 + +```objectivec +@interface NSObject (FBKVOController) + +@property (nonatomic, strong) FBKVOController *KVOController; +@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining; + +@end +``` + +从名字可以看出 `KVOControllerNonRetaining` 在使用时并不会**持有**被观察的对象,与它相比 `KVOController` 就会持有该对象了。 + +对于 `KVOController` 和 `KVOControllerNonRetaining` 属性来说,其实现都非常简单,对运行时非常熟悉的读者都应该知道使用关联对象就可以轻松实现这一需求。 + +```objectivec +- (FBKVOController *)KVOController { + id controller = objc_getAssociatedObject(self, NSObjectKVOControllerKey); + if (nil == controller) { + controller = [FBKVOController controllerWithObserver:self]; + self.KVOController = controller; + } + return controller; +} + +- (void)setKVOController:(FBKVOController *)KVOController { + objc_setAssociatedObject(self, NSObjectKVOControllerKey, KVOController, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (FBKVOController *)KVOControllerNonRetaining { + id controller = objc_getAssociatedObject(self, NSObjectKVOControllerNonRetainingKey); + if (nil == controller) { + controller = [[FBKVOController alloc] initWithObserver:self retainObserved:NO]; + self.KVOControllerNonRetaining = controller; + } + return controller; +} + +- (void)setKVOControllerNonRetaining:(FBKVOController *)KVOControllerNonRetaining { + objc_setAssociatedObject(self, NSObjectKVOControllerNonRetainingKey, KVOControllerNonRetaining, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} +``` + +两者的 `setter` 方法都只是使用 `objc_setAssociatedObject` 按照键值简单地存一下,而 `getter` 中不同的其实也就是对于 `FBKVOController` 的初始化了。 + +![easy](images/easy.jpg) + +到这里这个整个 FBKVOController 框架中的两个实现文件中的一个就介绍完了,接下来要看一下其中的另一个文件中的类 `KVOController`。 + +#### KVOController 的初始化 + +`KVOController` 是整个框架中提供 KVO 接口的类,作为 KVO 的管理者,其必须持有当前对象所有与 KVO 有关的信息,而在 `KVOController` 中,用于存储这个信息的数据结构就是 `NSMapTable`。 + +![KVOControlle](images/KVOController.png) + +为了使 `KVOController` 达到线程安全,它还必须持有一把 `pthread_mutex_t` 锁,用于在操作 `_objectInfosMap` 时使用。 + +再回到上一节提到的初始化问题,`NSObject` 的属性 `FBKVOController` 和 `KVOControllerNonRetaining` 的区别在于前者会持有观察者,使其引用计数加一。 + +```objectivec +- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved { + self = [super init]; + if (nil != self) { + _observer = observer; + NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality; + _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0]; + pthread_mutex_init(&_lock, NULL); + } + return self; +} +``` + +在初始化方法中使用各自的方法对 `KVOController` 对象持有的所有实例变量进行初始化,`KVOController` 和 `KVOControllerNonRetaining` 的区别就体现在生成的 `NSMapTable` 实例时传入的是 `NSPointerFunctionsStrongMemory` 还是 `NSPointerFunctionsWeakMemory` 选项。 + +### KVO 的过程 + +使用 `KVOController` 实现键值观测时,大都会调用实例方法 `-observe:keyPath:options:block` 来注册成为某个对象的观察者,监控属性的变化: + +```objectivec +- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block { + _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block]; + + [self _observe:object info:info]; +} +``` + +#### 数据结构 _FBKVOInfo + +这个方法中就涉及到另外一个私有的数据结构 `_FBKVOInfo`,这个类中包含着所有与 KVO 有关的信息: + +![_FBKVOInfo](images/_FBKVOInfo.png) + +`_FBKVOInfo` 在 `KVOController` 中充当的作用仅仅是一个数据结构,我们主要用它来存储整个 KVO 过程中所需要的全部信息,其内部没有任何值得一看的代码,需要注意的是,`_FBKVOInfo` 覆写了 `-isEqual:` 方法用于对象之间的判等以及方便 `NSMapTable` 的存储。 + +如果再有点别的什么特别作用的就是,其中的 `state` 表示当前的 KVO 状态,不过在本文中不会具体介绍。 + +```objectivec +typedef NS_ENUM(uint8_t, _FBKVOInfoState) { + _FBKVOInfoStateInitial = 0, + _FBKVOInfoStateObserving, + _FBKVOInfoStateNotObserving, +}; +``` + +#### observe 的过程 + +在使用 `-observer:keyPath:options:block:` 监听某一个对象属性的变化时,该过程的核心调用栈其实还是比较简单: + +![KVOController-Observe-Stack](images/KVOController-Observe-Stack.png) + +我们从栈底开始简单分析一下整个封装 KVO 的过程,其中栈底的方法,也就是我们上面提到的 `-observer:keyPath:options:block:` 初始化了一个名为 `_FBKVOInfo` 的对象: + +```objectivec +- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block { + _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block]; + [self _observe:object info:info]; +} +``` + +在创建了 `_FBKVOInfo` 之后执行了另一个私有方法 `-_observe:info:`: + +```objectivec +- (void)_observe:(id)object info:(_FBKVOInfo *)info { + pthread_mutex_lock(&_lock); + NSMutableSet *infos = [_objectInfosMap objectForKey:object]; + + _FBKVOInfo *existingInfo = [infos member:info]; + if (nil != existingInfo) { + pthread_mutex_unlock(&_lock); + return; + } + + if (nil == infos) { + infos = [NSMutableSet set]; + [_objectInfosMap setObject:infos forKey:object]; + } + [infos addObject:info]; + pthread_mutex_unlock(&_lock); + + [[_FBKVOSharedController sharedController] observe:object info:info]; +} +``` + +这个私有方法通过自身持有的 `_objectInfosMap` 来判断当前对象、属性以及各种上下文是否已经注册在表中存在了,在这个 `_objectInfosMap` 中保存着对象以及与对象有关的 `_FBKVOInfo` 集合: + +![objectInfosMap](images/objectInfosMap.png) + +在操作了当前 `KVOController` 持有的 `_objectInfosMap` 之后,才会执行私有的 `_FBKVOSharedController` 类的实例方法 `-observe:info:`: + +```objectivec +- (void)observe:(id)object info:(nullable _FBKVOInfo *)info { + pthread_mutex_lock(&_mutex); + [_infos addObject:info]; + pthread_mutex_unlock(&_mutex); + + [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info]; + + if (info->_state == _FBKVOInfoStateInitial) { + info->_state = _FBKVOInfoStateObserving; + } else if (info->_state == _FBKVOInfoStateNotObserving) { + [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; + } +} +``` + +`_FBKVOSharedController` 才是最终调用 Cocoa 中的 `-observe:forKeyPath:options:context:` 方法开始对属性的监听的地方;同时,在整个应用运行时,只会存在一个 `_FBKVOSharedController` 实例: + +```objectivec ++ (instancetype)sharedController { + static _FBKVOSharedController *_controller = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _controller = [[_FBKVOSharedController alloc] init]; + }); + return _controller; +} +``` + +这个唯一的 `_FBKVOSharedController` 实例会在 KVO 的回调方法中将事件分发给 KVO 的观察者。 + +```objectivec +- (void)observeValueForKeyPath:(nullable NSString *)keyPath + ofObject:(nullable id)object + change:(nullable NSDictionary<NSString *, id> *)change + context:(nullable void *)context { + _FBKVOInfo *info; + pthread_mutex_lock(&_mutex); + info = [_infos member:(__bridge id)context]; + pthread_mutex_unlock(&_mutex); + + FBKVOController *controller = info->_controller; + id observer = controller.observer; + + if (info->_block) { + NSDictionary<NSString *, id> *changeWithKeyPath = change; + if (keyPath) { + NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey]; + [mChange addEntriesFromDictionary:change]; + changeWithKeyPath = [mChange copy]; + } + info->_block(observer, object, changeWithKeyPath); + } else if (info->_action) { + [observer performSelector:info->_action withObject:change withObject:object]; + } else { + [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context]; + } +} +``` + +在这个 `-observeValueForKeyPath:ofObject:change:context:` 回调方法中,`_FBKVOSharedController` 会根据 KVO 的信息 `_KVOInfo` 选择不同的方式分发事件,如果观察者没有传入 block 或者选择子,就会调用观察者 KVO 回调方法。 + +![KVOSharedControlle](images/KVOSharedController.png) + +上图就是在使用 KVOController 时,如果一个 KVO 事件触发之后,整个框架是如何对这个事件进行处理以及回调的。 + +### 如何 removeObserver + +在使用 KVOController 时,我们并不需要手动去处理 KVO 观察者的移除,因为所有的 KVO 事件都由私有的 `_KVOSharedController` 来处理; + +![KVOController-Unobserve-Stack](images/KVOController-Unobserve-Stack.png) + +当每一个 `KVOController` 对象被释放时,都会将它自己持有的所有 KVO 的观察者交由 `_KVOSharedController` 的 `-unobserve:infos:` 方法处理: + +```objectivec +- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos { + pthread_mutex_lock(&_mutex); + for (_FBKVOInfo *info in infos) { + [_infos removeObject:info]; + } + pthread_mutex_unlock(&_mutex); + + for (_FBKVOInfo *info in infos) { + if (info->_state == _FBKVOInfoStateObserving) { + [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; + } + info->_state = _FBKVOInfoStateNotObserving; + } +} +``` + +该方法会遍历所有传入的 `_FBKVOInfo`,从其中取出 `keyPath` 并将 `_KVOSharedController` 移除观察者。 + +除了在 `KVOController` 析构时会自动移除观察者,我们也可以通过它的实例方法 `-unobserve:keyPath:` 操作达到相同的效果;不过在调用这个方法时,我们能够得到一个不同的调用栈: + +![KVOController-Unobserve-Object-Stack](images/KVOController-Unobserve-Object-Stack.png) + +功能的实现过程其实都是类似的,都是通过 `-removeObserver:forKeyPath:context:` 方法移除观察者: + +```objectivec +- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info { + pthread_mutex_lock(&_mutex); + [_infos removeObject:info]; + pthread_mutex_unlock(&_mutex); + + if (info->_state == _FBKVOInfoStateObserving) { + [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; + } + info->_state = _FBKVOInfoStateNotObserving; +} +``` + +不过由于这个方法的参数并不是一个数组,所以并不需要使用 `for` 循环,而是只需要将该 `_FBKVOInfo` 对应的 KVO 事件移除就可以了。 + +## 总结 + +KVOController 对于 Cocoa 中 KVO 的封装非常的简洁和优秀,我们只需要调用一个方法就可以完成一个对象的键值观测,同时不需要处理移除观察者等问题,能够降低我们出错的可能性。 + +在笔者看来 KVOController 中唯一不是很优雅的地方就是,需要写出 `object.KVOController` 才可以执行 KVO,如果能将 `KVOController` 换成更短的形式可能看起来更舒服一些: + +```objectivec +[self.kvo observer:keyPath:options:block:]; +``` + +不过这并不是一个比较大的问题,同时也只是笔者自己的看法,况且不影响 KVOController 的使用,所以各位读者也无须太过介意。 + +![](images/you-know.jpg) + +> Github Repo:[iOS-Source-Code-Analyze](https://github.com/draveness/iOS-Source-Code-Analyze) +> +> Follow: [Draveness · GitHub](https://github.com/Draveness) +> +> Source: http://draveness.me/kvocontroller + diff --git a/contents/KVOController/images/KVOController-Observe-Stack.png b/contents/KVOController/images/KVOController-Observe-Stack.png new file mode 100644 index 0000000..f7fab87 Binary files /dev/null and b/contents/KVOController/images/KVOController-Observe-Stack.png differ diff --git a/contents/KVOController/images/KVOController-Unobserve-Object-Stack.png b/contents/KVOController/images/KVOController-Unobserve-Object-Stack.png new file mode 100644 index 0000000..d29c21b Binary files /dev/null and b/contents/KVOController/images/KVOController-Unobserve-Object-Stack.png differ diff --git a/contents/KVOController/images/KVOController-Unobserve-Stack.png b/contents/KVOController/images/KVOController-Unobserve-Stack.png new file mode 100644 index 0000000..2db80ad Binary files /dev/null and b/contents/KVOController/images/KVOController-Unobserve-Stack.png differ diff --git a/contents/KVOController/images/KVOController.png b/contents/KVOController/images/KVOController.png new file mode 100644 index 0000000..dbf33d6 Binary files /dev/null and b/contents/KVOController/images/KVOController.png differ diff --git a/contents/KVOController/images/KVOSharedController.png b/contents/KVOController/images/KVOSharedController.png new file mode 100644 index 0000000..b3e5741 Binary files /dev/null and b/contents/KVOController/images/KVOSharedController.png differ diff --git a/contents/KVOController/images/_FBKVOInfo.png b/contents/KVOController/images/_FBKVOInfo.png new file mode 100644 index 0000000..ea390b1 Binary files /dev/null and b/contents/KVOController/images/_FBKVOInfo.png differ diff --git a/contents/KVOController/images/easy.jpg b/contents/KVOController/images/easy.jpg new file mode 100644 index 0000000..0372bb4 Binary files /dev/null and b/contents/KVOController/images/easy.jpg differ diff --git a/contents/KVOController/images/kvocontroller-banner.jpg b/contents/KVOController/images/kvocontroller-banner.jpg new file mode 100644 index 0000000..6d40a61 Binary files /dev/null and b/contents/KVOController/images/kvocontroller-banner.jpg differ diff --git a/contents/KVOController/images/not-crash-with-remove-observer-when-dealloc.png b/contents/KVOController/images/not-crash-with-remove-observer-when-dealloc.png new file mode 100644 index 0000000..d36a8b9 Binary files /dev/null and b/contents/KVOController/images/not-crash-with-remove-observer-when-dealloc.png differ diff --git a/contents/KVOController/images/objectInfosMap.png b/contents/KVOController/images/objectInfosMap.png new file mode 100644 index 0000000..a7f65ba Binary files /dev/null and b/contents/KVOController/images/objectInfosMap.png differ diff --git a/contents/KVOController/images/trouble.jpg b/contents/KVOController/images/trouble.jpg new file mode 100644 index 0000000..b93c293 Binary files /dev/null and b/contents/KVOController/images/trouble.jpg differ diff --git a/contents/KVOController/images/you-know.jpg b/contents/KVOController/images/you-know.jpg new file mode 100644 index 0000000..8eb215c Binary files /dev/null and b/contents/KVOController/images/you-know.jpg differ diff --git "a/MBProgressHUD/iOS \346\272\220\344\273\243\347\240\201\345\210\206\346\236\220 --- MBProgressHUD.md" "b/contents/MBProgressHUD/iOS \346\272\220\344\273\243\347\240\201\345\210\206\346\236\220 --- MBProgressHUD.md" similarity index 100% rename from "MBProgressHUD/iOS \346\272\220\344\273\243\347\240\201\345\210\206\346\236\220 --- MBProgressHUD.md" rename to "contents/MBProgressHUD/iOS \346\272\220\344\273\243\347\240\201\345\210\206\346\236\220 --- MBProgressHUD.md" diff --git "a/Masonry/iOS \346\272\220\344\273\243\347\240\201\345\210\206\346\236\220 --- Masonry.md" "b/contents/Masonry/iOS \346\272\220\344\273\243\347\240\201\345\210\206\346\236\220 --- Masonry.md" similarity index 100% rename from "Masonry/iOS \346\272\220\344\273\243\347\240\201\345\210\206\346\236\220 --- Masonry.md" rename to "contents/Masonry/iOS \346\272\220\344\273\243\347\240\201\345\210\206\346\236\220 --- Masonry.md" diff --git "a/contents/OHHTTPStubs/iOS \345\274\200\345\217\221\344\270\255\344\275\277\347\224\250 NSURLProtocol \346\213\246\346\210\252 HTTP \350\257\267\346\261\202.md" "b/contents/OHHTTPStubs/iOS \345\274\200\345\217\221\344\270\255\344\275\277\347\224\250 NSURLProtocol \346\213\246\346\210\252 HTTP \350\257\267\346\261\202.md" new file mode 100644 index 0000000..cd42abd --- /dev/null +++ "b/contents/OHHTTPStubs/iOS \345\274\200\345\217\221\344\270\255\344\275\277\347\224\250 NSURLProtocol \346\213\246\346\210\252 HTTP \350\257\267\346\261\202.md" @@ -0,0 +1,141 @@ +![](images/intercept.png) + +# iOS 开发中使用 NSURLProtocol 拦截 HTTP 请求 + +这篇文章会提供一种在 Cocoa 层拦截所有 HTTP 请求的方法,其实标题已经说明了拦截 HTTP 请求需要的了解的就是 `NSURLProtocol`。 + +由于文章的内容较长,会分成两部分,这篇文章介绍 `NSURLProtocol` 拦截 HTTP 请求的原理,另一篇文章[如何进行 HTTP Mock(iOS)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/OHHTTPStubs/如何进行%20HTTP%20Mock(iOS).md) + 介绍这个原理在 `OHHTTPStubs` 中的应用,它是如何 Mock(伪造)某个 HTTP 请求对应的响应的。 + +## NSURLProtocol + +`NSURLProtocol` 是苹果为我们提供的 [URL Loading System](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html) 的一部分,这是一张从官方文档贴过来的图片: + +![URL-loading-syste](images/URL-loading-system.png) + +官方文档对 `NSURLProtocol` 的描述是这样的: + +> An NSURLProtocol object handles the loading of protocol-specific URL data. The NSURLProtocol class itself is an abstract class that provides the infrastructure for processing URLs with a specific URL scheme. You create subclasses for any custom protocols or URL schemes that your app supports. + +在每一个 HTTP 请求开始时,URL 加载系统创建一个合适的 `NSURLProtocol` 对象处理对应的 URL 请求,而我们需要做的就是写一个继承自 `NSURLProtocol` 的类,并通过 `- registerClass:` 方法注册我们的协议类,然后 URL 加载系统就会在请求发出时使用我们创建的协议对象对该请求进行处理。 + +这样,我们需要解决的核心问题就变成了如何使用 `NSURLProtocol` 来处理所有的网络请求,这里使用苹果官方文档中的 [CustomHTTPProtocol](https://developer.apple.com/library/ios/samplecode/CustomHTTPProtocol/CustomHTTPProtocol.zip) 进行介绍,你可以点击[这里](https://developer.apple.com/library/ios/samplecode/CustomHTTPProtocol/CustomHTTPProtocol.zip)下载源代码。 + +在这个工程中 `CustomHTTPProtocol.m` 是需要重点关注的文件,`CustomHTTPProtocol` 就是 `NSURLProtocol` 的子类: + +```objectivec +@interface CustomHTTPProtocol : NSURLProtocol + +... + +@end +``` + +现在重新回到需要解决的问题,也就是 **如何使用 NSURLProtocol 拦截 HTTP 请求?**,有这个么几个问题需要去解决: + ++ 如何决定哪些请求需要当前协议对象处理? ++ 对当前的请求对象需要进行哪些处理? ++ `NSURLProtocol` 如何实例化? ++ 如何发出 HTTP 请求并且将响应传递给调用者? + +上面的这几个问题其实都可以通过 `NSURLProtocol` 为我们提供的 API 来解决,决定请求是否需要当前协议对象处理的方法是:`+ canInitWithRequest`: + +```objectivec ++ (BOOL)canInitWithRequest:(NSURLRequest *)request { + BOOL shouldAccept; + NSURL *url; + NSString *scheme; + + shouldAccept = (request != nil); + if (shouldAccept) { + url = [request URL]; + shouldAccept = (url != nil); + } + return shouldAccept; +} +``` + +因为项目中的这个方法是大约有 60 多行,在这里只粘贴了其中的一部分,只为了说明该方法的作用:每一次请求都会有一个 `NSURLRequest` 实例,上述方法会拿到所有的请求对象,我们就可以根据对应的请求选择是否处理该对象;而上面的代码只会处理所有 `URL` 不为空的请求。 + +请求经过 `+ canInitWithRequest:` 方法过滤之后,我们得到了所有要处理的请求,接下来需要对请求进行一定的操作,而这都会在 `+ canonicalRequestForRequest:` 中进行,虽然它与 `+ canInitWithRequest:` 方法传入的 request 对象都是一个,但是最好不要在 `+ canInitWithRequest:` 中操作对象,可能会有语义上的问题;所以,我们需要覆写 `+ canonicalRequestForRequest:` 方法提供一个标准的请求对象: + +```objectivec ++ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { + return request; +} +``` + +这里对请求不做任何修改,直接返回,当然你也可以给这个请求加个 header,只要最后返回一个 `NSURLRequest` 对象就可以。 + +在得到了需要的请求对象之后,就可以初始化一个 `NSURLProtocol` 对象了: + +```objectivec +- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id <NSURLProtocolClient>)client { + return [super initWithRequest:request cachedResponse:cachedResponse client:client]; +} +``` + +在这里直接调用 `super` 的指定构造器方法,实例化一个对象,然后就进入了发送网络请求,获取数据并返回的阶段了: + +```objectivec +- (void)startLoading { + NSURLSession *session = [NSURLSession sessionWithConfiguration:[[NSURLSessionConfiguration alloc] init] delegate:self delegateQueue:nil]; + NSURLSessionDataTask *task = [session dataTaskWithRequest:self.request]; + [task resume]; +} +``` + +> 这里使用简化了 CustomHTTPClient 中的项目代码,可以达到几乎相同的效果。 + +你可以在 `- startLoading` 中使用任何方法来对协议对象持有的 `request` 进行转发,包括 `NSURLSession`、 `NSURLConnection` 甚至使用 AFNetworking 等网络库,只要你能在回调方法中把数据传回 `client`,帮助其正确渲染就可以,比如这样: + +```objectivec +- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler { + [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed]; + + completionHandler(NSURLSessionResponseAllow); +} + +- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { + [[self client] URLProtocol:self didLoadData:data]; +} +``` + +> 当然这里省略后的代码只会保证大多数情况下的正确执行,只是给你一个对获取响应数据粗略的认知,如果你需要更加详细的代码,我觉得最好还是查看一下 `CustomHTTPProtocol` 中对 HTTP 响应处理的代码,也就是 `NSURLSessionDelegate` 协议实现的部分。 + +`client` 你可以理解为当前网络请求的发起者,所有的 `client` 都实现了 `NSURLProtocolClient` 协议,协议的作用就是在 HTTP 请求发出以及接受响应时向其它对象传输数据: + +```objectivec +@protocol NSURLProtocolClient <NSObject> +... +- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy; + +- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data; + +- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol; +... +@end +``` + +当然这个协议中还有很多其他的方法,比如 HTTPS 验证、重定向以及响应缓存相关的方法,你需要在合适的时候调用这些代理方法,对信息进行传递。 + +如果你只是继承了 `NSURLProtocol` 并且实现了上述方法,依然不能达到预期的效果,完成对 HTTP 请求的拦截,你还需要在 URL 加载系统中注册当前类: + +```objectivec +[NSURLProtocol registerClass:self]; +``` + +> 需要注意的是 `NSURLProtocol` 只能拦截 `UIURLConnection`、`NSURLSession` 和 `UIWebView` 中的请求,对于 `WKWebView` 中发出的网络请求也无能为力,如果真的要拦截来自 `WKWebView` 中的请求,还是需要实现 `WKWebView` 对应的 `WKNavigationDelegate`,并在代理方法中获取请求。 +> 无论是 `NSURLProtocol`、`NSURLConnection` 还是 `NSURLSession` 都会走底层的 socket,但是 `WKWebView` 可能由于基于 WebKit,并不会执行 C socket 相关的函数对 HTTP 请求进行处理,具体会执行什么代码暂时不是很清楚,如果对此有兴趣的读者,可以联系笔者一起讨论。 + +## 总结 + +如果你只想了解如何对 HTTP 请求进行拦截,其实看到这里就可以了,不过如果你想应用文章中的内容或者希望了解如何伪造 HTTP 响应,可以看下一篇文章[如何进行 HTTP Mock(iOS)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/OHHTTPStubs/如何进行%20HTTP%20Mock(iOS).md) +。 + +> Follow: [Draveness · Github](https://github.com/Draveness) + +## References +
+ [NSURLProtocol]([http://nshipster.com/nsurlprotocol/]) +[如何进行 HTTP Mock(iOS)](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/OHHTTPStubs/如何进行%20HTTP%20Mock(iOS).md) + diff --git a/contents/OHHTTPStubs/images/OHHTTPStubs-test.png b/contents/OHHTTPStubs/images/OHHTTPStubs-test.png new file mode 100644 index 0000000..95bd2e3 Binary files /dev/null and b/contents/OHHTTPStubs/images/OHHTTPStubs-test.png differ diff --git a/contents/OHHTTPStubs/images/URL-loading-system.png b/contents/OHHTTPStubs/images/URL-loading-system.png new file mode 100644 index 0000000..74bc1ef Binary files /dev/null and b/contents/OHHTTPStubs/images/URL-loading-system.png differ diff --git a/contents/OHHTTPStubs/images/http-mock-test.png b/contents/OHHTTPStubs/images/http-mock-test.png new file mode 100644 index 0000000..c7286fb Binary files /dev/null and b/contents/OHHTTPStubs/images/http-mock-test.png differ diff --git a/contents/OHHTTPStubs/images/intercept.png b/contents/OHHTTPStubs/images/intercept.png new file mode 100644 index 0000000..43291c2 Binary files /dev/null and b/contents/OHHTTPStubs/images/intercept.png differ diff --git "a/contents/OHHTTPStubs/\345\246\202\344\275\225\350\277\233\350\241\214 HTTP Mock\357\274\210iOS\357\274\211.md" "b/contents/OHHTTPStubs/\345\246\202\344\275\225\350\277\233\350\241\214 HTTP Mock\357\274\210iOS\357\274\211.md" new file mode 100644 index 0000000..88f5691 --- /dev/null +++ "b/contents/OHHTTPStubs/\345\246\202\344\275\225\350\277\233\350\241\214 HTTP Mock\357\274\210iOS\357\274\211.md" @@ -0,0 +1,408 @@ +![](images/http-mock-test.png) + +# 如何进行 HTTP Mock(iOS) + +这篇文章会对 [OHHTTPStubs]([https://github.com/AliSoftware/OHHTTPStubs]) 源代码的分析,其实现原理是建立在 `NSURLProtocol` 的基础上的,对这部分内容不了解的读者,可以阅读这篇文章 [iOS 开发中使用 NSURLProtocol 拦截 HTTP 请求](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/OHHTTPStubs/iOS%20开发中使用%20NSURLProtocol%20拦截%20HTTP%20请求.md)了解相关知识,本文中不会介绍拦截 HTTP 请求的原理。 + +### 如何使用 OHHTTPStubs Mock 网络请求 + +HTTP Mock 在测试中非常好用,我们可以在不需要后端 API 的情况下,在本地对 HTTP 请求进行拦截,返回想要的 `json` 数据,而 OHHTTPStubs 就为我们提供了这样一种解决方案。 + +在了解其实现之前,先对 OHHTTPStubs 进行简单的介绍,引入头文件这种事情在这里会直接省略,先来看一下程序的源代码: + +```objectivec +[OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest * _Nonnull request) { + return [request.URL.absoluteString isEqualToString:@"https://idont.know/"]; +} withStubResponse:^OHHTTPStubsResponse * _Nonnull(NSURLRequest * _Nonnull request) { + NSString *fixture = OHPathForFile(@"example.json", self.class); + return [OHHTTPStubsResponse responseWithFileAtPath:fixture statusCode:200 headers:@{@"Content-Type":@"application/json"}]; +}]; + +AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; +[manager GET:@"https://idont.know/" + parameters:nil + progress:nil + success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { + NSLog(@"%@", responseObject); + } failure:nil]; +``` + +我们向 `https://idont.know` 这个 URL 发送一个 GET 请求,虽然这个 URL 并不存在,但是这里的代码通过 HTTP stub 成功地模拟了 HTTP 响应: + +![OHHTTPStubs-test](images/OHHTTPStubs-test.png) + +## OHHTTPStubs 的实现 + +在了解了 OHHTTPStubs 的使用之后,我们会对其实现进行分析,它分成四部分进行: + ++ `OHHTTPStubsProtocol` 拦截 HTTP 请求 ++ `OHHTTPStubs` 单例管理 `OHHTTPStubsDescriptor` 实例 ++ `OHHTTPStubsResponse` 伪造 HTTP 响应 ++ 一些辅助功能 + +### OHHTTPStubsProtocol 拦截 HTTP 请求 + +在 OHHTTPStubs 中继承 `NSURLProtocol` 的类就是 `OHHTTPStubsProtocol`,它在 HTTP 请求发出之前对 request 对象进行过滤以及处理: + +```objectivec ++ (BOOL)canInitWithRequest:(NSURLRequest *)request { + return ([OHHTTPStubs.sharedInstance firstStubPassingTestForRequest:request] != nil); +} + +- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)response client:(id<NSURLProtocolClient>)client { + OHHTTPStubsProtocol* proto = [super initWithRequest:request cachedResponse:nil client:client]; + proto.stub = [OHHTTPStubs.sharedInstance firstStubPassingTestForRequest:request]; + return proto; +} + ++ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { + return request; +} +``` + +判断请求是否会被当前协议对象进行处理是需要 `OHHTTPStubs` 的实例方法 `- firstStubPassingTestForRequest:` 的执行的,在这里暂时先不对这个方法进行讨论。 + +接下来就是请求发送的过程 `- startLoading` 方法了,该方法的实现实在是太过于复杂,所以这里分块来分析代码: + +```objectivec +- (void)startLoading { + NSURLRequest* request = self.request; + id<NSURLProtocolClient> client = self.client; + + OHHTTPStubsResponse* responseStub = self.stub.responseBlock(request); + + if (OHHTTPStubs.sharedInstance.onStubActivationBlock) { + OHHTTPStubs.sharedInstance.onStubActivationBlock(request, self.stub, responseStub); + } + + ... +} +``` + +从当前对象中取出 `request` 以及 `client` 对象,如果 `OHHTTPStubs` 的单例中包含 `onStubActivationBlock`,就会执行这里的 block,然后调用 `responseBlock` 获取一个 `OHHTTPStubsResponse` HTTP 响应对象。 + +`OHHTTPStubs` 不只提供了 `onStubActivationBlock` 这一个钩子,还有以下 block: + ++ `+ onStubActivationBlock`:stub 被激活时调用 ++ `+ onStubRedirectBlock`:发生重定向时 ++ `+ afterStubFinishBlock`:在 stub 结束时调用 + +如果响应对象的生成没有遇到任何问题,就会进入处理 Cookie、重定向、发送响应和模拟数据流的过程了。 + +1. 首先是对 Cookie 的处理 + +```objectivec +NSHTTPURLResponse* urlResponse = [[NSHTTPURLResponse alloc] initWithURL:request.URL + statusCode:responseStub.statusCode + HTTPVersion:@"HTTP/1.1" + headerFields:responseStub.httpHeaders]; + +if (request.HTTPShouldHandleCookies && request.URL) { + NSArray* cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:responseStub.httpHeaders forURL:request.URL]; + if (cookies) { + [NSHTTPCookieStorage.sharedHTTPCookieStorage setCookies:cookies forURL:request.URL mainDocumentURL:request.mainDocumentURL]; + } +} +``` + +2. 如果 HTTP 状态码在 300-400 之间,就会处理重定向的问题,调用 `onStubRedirectBlock` 进行需要的回调 + +```objectivec +NSString* redirectLocation = (responseStub.httpHeaders)[@"Location"]; +NSURL* redirectLocationURL = redirectLocation ? [NSURL URLWithString:redirectLocation] : nil; + +if (((responseStub.statusCode > 300) && (responseStub.statusCode < 400)) && redirectLocationURL) { + NSURLRequest* redirectRequest = [NSURLRequest requestWithURL:redirectLocationURL]; + [self executeOnClientRunLoopAfterDelay:responseStub.requestTime block:^{ + if (!self.stopped) { + [client URLProtocol:self wasRedirectedToRequest:redirectRequest redirectResponse:urlResponse]; + if (OHHTTPStubs.sharedInstance.onStubRedirectBlock) { + OHHTTPStubs.sharedInstance.onStubRedirectBlock(request, redirectRequest, self.stub, responseStub); + } + } + }]; +} +``` + +3. 最后这里有一些复杂,我们根据 `stub` 中存储的 `responseTime` 来模拟响应的一个延迟时间,然后使用 `- streamDataForClient:withStubResponse:completion:` 来模拟数据以 `NSData` 的形式分块发送回 `client` 的过程,最后调用 `afterStubFinishBlock`。 + +```objectivec +[self executeOnClientRunLoopAfterDelay:responseStub.requestTime block:^{ + if (!self.stopped) { + [client URLProtocol:self didReceiveResponse:urlResponse cacheStoragePolicy:NSURLCacheStorageNotAllowed]; + if(responseStub.inputStream.streamStatus == NSStreamStatusNotOpen) { + [responseStub.inputStream open]; + } + [self streamDataForClient:client + withStubResponse:responseStub + completion:^(NSError * error) { + [responseStub.inputStream close]; + NSError *blockError = nil; + if (error==nil) { + [client URLProtocolDidFinishLoading:self]; + } else { + [client URLProtocol:self didFailWithError:responseStub.error]; + blockError = responseStub.error; + } + if (OHHTTPStubs.sharedInstance.afterStubFinishBlock) { + OHHTTPStubs.sharedInstance.afterStubFinishBlock(request, self.stub, responseStub, blockError); + } + }]; + } +}]; +``` + +当然如果在生成 `responseStub` 的时候发生了错误,也会进行类似的操作,在延迟一定时间(模拟网络延迟)后执行 block 并传入各种参数: + +```objectivec +[self executeOnClientRunLoopAfterDelay:responseStub.responseTime block:^{ + if (!self.stopped) { + [client URLProtocol:self didFailWithError:responseStub.error]; + if (OHHTTPStubs.sharedInstance.afterStubFinishBlock) { + OHHTTPStubs.sharedInstance.afterStubFinishBlock(request, self.stub, responseStub, responseStub.error); + } + } +}]; +``` + +#### 模拟数据流 + +因为在客户端接收数据时,所有的 `NSData` 并不是一次就涌入客户端的,而是分块加载打包解码的,尤其是在我们执行下载操作时,有时几 MB 的文件不可能同时到达服务端,而 `- startLoading` 中调用的 `- streamDataForClient:withStubResponse:completion:` 方法就是为了模拟数据流,分块向服务端发送数据,不过这部分的处理涉及到一个私有的结构体 `OHHTTPStubsStreamTimingInfo`: + +```objectivec +typedef struct { + NSTimeInterval slotTime; + double chunkSizePerSlot; + double cumulativeChunkSize; +} OHHTTPStubsStreamTimingInfo; +``` + +这个结构体包含了关于发送数据流的信息: + ++ `slotTime`:两次发送 `NSData` 的间隔时间 ++ `chunkSizePerSlot`:每块数据流大小 ++ `cumulativeChunkSize`:已发送的数据流大小 + +模拟数据流的过程需要两个方法的支持,其中一个方法做一些预加载工作: + +```objectivec +- (void)streamDataForClient:(id<NSURLProtocolClient>)client + withStubResponse:(OHHTTPStubsResponse*)stubResponse + completion:(void(^)(NSError * error))completion { + if ((stubResponse.dataSize>0) && stubResponse.inputStream.hasBytesAvailable && (!self.stopped)) { + OHHTTPStubsStreamTimingInfo timingInfo = { + .slotTime = kSlotTime, + .cumulativeChunkSize = 0 + }; + + if(stubResponse.responseTime < 0) { + timingInfo.chunkSizePerSlot = (fabs(stubResponse.responseTime) * 1000) * timingInfo.slotTime; + } else if (stubResponse.responseTime < kSlotTime) { + timingInfo.chunkSizePerSlot = stubResponse.dataSize; + timingInfo.slotTime = stubResponse.responseTime; + } else { + timingInfo.chunkSizePerSlot = ((stubResponse.dataSize/stubResponse.responseTime) * timingInfo.slotTime); + } + + [self streamDataForClient:client + fromStream:stubResponse.inputStream + timingInfo:timingInfo + completion:completion]; + } else { + if (completion) completion(nil); + } +} +``` + +该方法将生成的 `OHHTTPStubsStreamTimingInfo` 信息传入下一个实例方法 `- streamDataForClient:fromStream:timingInfo:completion:`: + +```objectivec +- (void)streamDataForClient:(id<NSURLProtocolClient>)client fromStream:(NSInputStream*)inputStream timingInfo:(OHHTTPStubsStreamTimingInfo)timingInfo completion:(void(^)(NSError * error))completion { + if (inputStream.hasBytesAvailable && (!self.stopped)) { + double cumulativeChunkSizeAfterRead = timingInfo.cumulativeChunkSize + timingInfo.chunkSizePerSlot; + NSUInteger chunkSizeToRead = floor(cumulativeChunkSizeAfterRead) - floor(timingInfo.cumulativeChunkSize); + timingInfo.cumulativeChunkSize = cumulativeChunkSizeAfterRead; + + if (chunkSizeToRead == 0) { + [self executeOnClientRunLoopAfterDelay:timingInfo.slotTime block:^{ + [self streamDataForClient:client fromStream:inputStream + timingInfo:timingInfo completion:completion]; + }]; + } else { + uint8_t* buffer = (uint8_t*)malloc(sizeof(uint8_t)*chunkSizeToRead); + NSInteger bytesRead = [inputStream read:buffer maxLength:chunkSizeToRead]; + if (bytesRead > 0) { + NSData * data = [NSData dataWithBytes:buffer length:bytesRead]; + [self executeOnClientRunLoopAfterDelay:((double)bytesRead / (double)chunkSizeToRead) * timingInfo.slotTime block:^{ + [client URLProtocol:self didLoadData:data]; + [self streamDataForClient:client fromStream:inputStream + timingInfo:timingInfo completion:completion]; + }]; + } else { + if (completion) completion(inputStream.streamError); + } + free(buffer); + } + } else { + if (completion) completion(nil); + } +} +``` + ++ 上述方法会先计算 `chunkSizeToRead`,也就是接下来要传递给 `client` 的数据长度 ++ 从 `NSInputStream` 中读取对应长度的数据 ++ 通过 `- executeOnClientRunLoopAfterDelay:block:` 模拟数据传输的延时 ++ 使用 `- URLProtocol:didLoadData:` 代理方法将数据传回 `client` + +OHHTTPStubs 通过上面的两个方法很好的模拟了 HTTP 响应由于网络造成的延迟以及数据分块到达客户端的特点。 + +### OHHTTPStubs 以及 OHHTTPStubsDescriptor 对 stub 的管理 + +`OHHTTPStubs` 遵循单例模式,其主要作用就是提供便利的 API 并持有一个 `OHHTTPStubsDescriptor` 数组,对 stub 进行管理。 + +`OHHTTPStubs` 提供的类方法 `+ stubRequestsPassingTest:withStubResponse:` 会添加一个 `OHHTTPStubsDescriptor ` 的实例到 `OHHTTPStubsDescriptor` 数组中: + +```objectivec ++ (id<OHHTTPStubsDescriptor>)stubRequestsPassingTest:(OHHTTPStubsTestBlock)testBlock + withStubResponse:(OHHTTPStubsResponseBlock)responseBlock { + OHHTTPStubsDescriptor* stub = [OHHTTPStubsDescriptor stubDescriptorWithTestBlock:testBlock + responseBlock:responseBlock]; + [OHHTTPStubs.sharedInstance addStub:stub]; + return stub; +} +``` + +该类主要有两种方法,一种方法用于管理持有的 HTTP stub,比如说: + ++ `+ (BOOL)removeStub:(id<OHHTTPStubsDescriptor>)stubDesc` ++ `+ (void)removeAllStubs` ++ `- (void)addStub:(OHHTTPStubsDescriptor*)stubDesc` ++ `- (BOOL)removeStub:(id<OHHTTPStubsDescriptor>)stubDesc` ++ `- (void)removeAllStubs` + +这些方法都是用来操作单例持有的数组的,而另一种方法用来设置相应事件发生时的回调: + ++ `+ (void)onStubActivation:( nullable void(^)(NSURLRequest* request, id<OHHTTPStubsDescriptor> stub, OHHTTPStubsResponse* responseStub) )block` ++ `+ (void)onStubRedirectResponse:( nullable void(^)(NSURLRequest* request, NSURLRequest* redirectRequest, id<OHHTTPStubsDescriptor> stub, OHHTTPStubsResponse* responseStub) )block` ++ `+ (void)afterStubFinish:( nullable void(^)(NSURLRequest* request, id<OHHTTPStubsDescriptor> stub, OHHTTPStubsResponse* responseStub, NSError* error) )block` + +类中最重要的实例方法就是 `- firstStubPassingTestForRequest:`,它遍历自己持有的全部 stub,通过 `testBlock` 的调用返回第一个符合条件的 stub: + +```objectivec +- (OHHTTPStubsDescriptor*)firstStubPassingTestForRequest:(NSURLRequest*)request { + OHHTTPStubsDescriptor* foundStub = nil; + @synchronized(_stubDescriptors) { + for(OHHTTPStubsDescriptor* stub in _stubDescriptors.reverseObjectEnumerator) { + if (stub.testBlock(request)) { + foundStub = stub; + break; + } + } + } + return foundStub; +} +``` + +相比之下 `OHHTTPStubsDescriptor` 仅仅作为一个保存信息的类,其职能相对单一、实现相对简单: + +```objectivec +@interface OHHTTPStubsDescriptor : NSObject <OHHTTPStubsDescriptor> +@property(atomic, copy) OHHTTPStubsTestBlock testBlock; +@property(atomic, copy) OHHTTPStubsResponseBlock responseBlock; +@end + +@implementation OHHTTPStubsDescriptor + ++ (instancetype)stubDescriptorWithTestBlock:(OHHTTPStubsTestBlock)testBlock + responseBlock:(OHHTTPStubsResponseBlock)responseBlock { + OHHTTPStubsDescriptor* stub = [OHHTTPStubsDescriptor new]; + stub.testBlock = testBlock; + stub.responseBlock = responseBlock; + return stub; +} + +@end +``` + +两个属性以及一个方法构成了 `OHHTTPStubsDescriptor` 类的全部实现。 + +### OHHTTPStubsResponse 伪造 HTTP 响应 + +`OHHTTPStubsResponse` 类为请求提供了相应所需要的各种参数,HTTP 状态码、请求时间以及数据的输入流也就是用于模拟网络请求的 `inputStream`。 + +指定构造器 `- initWithFileURL:statusCode:headers:` 完成了对这些参数的配置: + +```objectivec +- (instancetype)initWithInputStream:(NSInputStream*)inputStream dataSize:(unsigned long long)dataSize statusCode:(int)statusCode headers:(nullable NSDictionary*)httpHeaders { + if (self = [super init]) { + _inputStream = inputStream; + _dataSize = dataSize; + _statusCode = statusCode; + NSMutableDictionary * headers = [NSMutableDictionary dictionaryWithDictionary:httpHeaders]; + static NSString *const ContentLengthHeader = @"Content-Length"; + if (!headers[ContentLengthHeader]) { + headers[ContentLengthHeader] = [NSString stringWithFormat:@"%llu",_dataSize]; + } + _httpHeaders = [NSDictionary dictionaryWithDictionary:headers]; + } + return self; +} +``` + +同时,该类也提供了非常多的便利构造器以及类方法帮助我们实例化 `OHHTTPStubsResponse`,整个类中的所有构造方法大都会调用上述构造器;只是会传入不同的参数: + +```objectivec +- (instancetype)initWithFileURL:(NSURL *)fileURL statusCode:(int)statusCode headers:(nullable NSDictionary *)httpHeaders { + NSNumber *fileSize; + NSError *error; + const BOOL success __unused = [fileURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:&error]; + + return [self initWithInputStream:[NSInputStream inputStreamWithURL:fileURL] dataSize:[fileSize unsignedLongLongValue] statusCode:statusCode headers:httpHeaders]; +} +``` + +比如 `- initWithFileURL:statusCode:headers:` 方法就会从文件中读取数据,然后构造一个数据输入流。 + +### 其他内容 + +使用 `NSURLProtocol` 拦截 HTTP 请求时会有一个非常严重的问题,如果发出的是 POST 请求,请求的 body 会在到达 OHHTTPStubs 时被重置为空,也就是我们无法直接在 `testBlock` 中获取其 `HTTPBody`;所以,我们只能通过通过方法调剂在设置 `HTTPBody` 时,进行备份: + +```objectivec +typedef void(*OHHHTTPStubsSetterIMP)(id, SEL, id); +static OHHHTTPStubsSetterIMP orig_setHTTPBody; + +static void OHHTTPStubs_setHTTPBody(id self, SEL _cmd, NSData* HTTPBody) { + if (HTTPBody) { + [NSURLProtocol setProperty:HTTPBody forKey:OHHTTPStubs_HTTPBodyKey inRequest:self]; + } + orig_setHTTPBody(self, _cmd, HTTPBody); +} +@interface NSMutableURLRequest (HTTPBodyTesting) @end + +@implementation NSMutableURLRequest (HTTPBodyTesting) + ++ (void)load { + orig_setHTTPBody = (OHHHTTPStubsSetterIMP)OHHTTPStubsReplaceMethod(@selector(setHTTPBody:), (IMP)OHHTTPStubs_setHTTPBody, [NSMutableURLRequest class], NO); +} + +@end +``` + +除了对于 `HTTPBody` 的备份之外,OHHTTPStubs 还提供了一些用于从文件中获取数据的 C 函数: + +```objectivec +NSString* __nullable OHPathForFile(NSString* fileName, Class inBundleForClass); +NSString* __nullable OHPathForFileInBundle(NSString* fileName, NSBundle* bundle); +NSString* __nullable OHPathForFileInDocumentsDir(NSString* fileName); +NSBundle* __nullable OHResourceBundle(NSString* bundleBasename, Class inBundleForClass); +``` + +这些 C 语言函数能够帮助我们构造 HTTP 响应。 + +## 总结 + +如果阅读过上一篇文章中的内容,理解这里的实现原理也不是什么太大的问题。在需要使用到 HTTP mock 进行测试时,使用 OHHTTPStubs 还是很方便的,当然现在也有很多其他的 HTTP stub 框架,不过实现基本上都是基于 `NSURLProtocol` 的。 + +> Follow: [Draveness · Github](https://github.com/Draveness) + + diff --git a/contents/ProtocolKit/images/protocol-demo.jpeg b/contents/ProtocolKit/images/protocol-demo.jpeg new file mode 100644 index 0000000..c54254c Binary files /dev/null and b/contents/ProtocolKit/images/protocol-demo.jpeg differ diff --git a/contents/ProtocolKit/images/protocol-recordings.jpeg b/contents/ProtocolKit/images/protocol-recordings.jpeg new file mode 100644 index 0000000..abe924c Binary files /dev/null and b/contents/ProtocolKit/images/protocol-recordings.jpeg differ diff --git "a/contents/ProtocolKit/\345\246\202\344\275\225\345\234\250 Objective-C \344\270\255\345\256\236\347\216\260\345\215\217\350\256\256\346\211\251\345\261\225.md" "b/contents/ProtocolKit/\345\246\202\344\275\225\345\234\250 Objective-C \344\270\255\345\256\236\347\216\260\345\215\217\350\256\256\346\211\251\345\261\225.md" new file mode 100644 index 0000000..2512ff2 --- /dev/null +++ "b/contents/ProtocolKit/\345\246\202\344\275\225\345\234\250 Objective-C \344\270\255\345\256\236\347\216\260\345\215\217\350\256\256\346\211\251\345\261\225.md" @@ -0,0 +1,339 @@ +# 如何在 Objective-C 中实现协议扩展 + +![protocol-recordings](images/protocol-recordings.jpeg) + +Swift 中的协议扩展为 iOS 开发带来了非常多的可能性,它为我们提供了一种类似多重继承的功能,帮助我们减少一切可能导致重复代码的地方。 + +## 关于 Protocol Extension + +在 Swift 中比较出名的 Then 就是使用了协议扩展为所有的 `AnyObject` 添加方法,而且不需要调用 runtime 相关的 API,其实现简直是我见过最简单的开源框架之一: + +```swift +public protocol Then {} + +extension Then where Self: AnyObject { + public func then(@noescape block: Self -> Void) -> Self { + block(self) + return self + } +} + +extension NSObject: Then {} +``` + +只有这么几行代码,就能为所有的 `NSObject` 添加下面的功能: + +```swift +let titleLabel = UILabel().then { + $0.textColor = .blackColor() + $0.textAlignment = .Center +} +``` + +这里没有调用任何的 runtime 相关 API,也没有在 `NSObject` 中进行任何的方法声明,甚至 `protocol Then {}` 协议本身都只有一个大括号,整个 Then 框架就是基于协议扩展来实现的。 + +在 Objective-C 中同样有协议,但是这些协议只是相当于接口,遵循某个协议的类只表明实现了这些接口,每个类都需要**对这些接口有单独的实现**,这就很可能会导致重复代码的产生。 + +而协议扩展可以调用协议中声明的方法,以及 `where Self: AnyObject` 中的 `AnyObject` 的类/实例方法,这就大大提高了可操作性,便于开发者写出一些意想不到的扩展。 + +> 如果读者对 Protocol Extension 兴趣或者不了解协议扩展,可以阅读最后的 [Reference](#reference) 了解相关内容。 + +## ProtocolKit + +其实协议扩展的强大之处就在于它能为遵循协议的类添加一些方法的实现,而不只是一些接口,而今天为各位读者介绍的 [ProtocolKit]([https://github.com/forkingdog/ProtocolKit]) 就实现了这一功能,为遵循协议的类添加方法。 + +### ProtocolKit 的使用 + +我们先来看一下如何使用 ProtocolKit,首先定义一个协议: + +```objectivec +@protocol TestProtocol + +@required + +- (void)fizz; + +@optional + +- (void)buzz; + +@end +``` + +在协议中定义了两个方法,必须实现的方法 `fizz` 以及可选实现 `buzz`,然后使用 ProtocolKit 提供的接口 `defs` 来定义协议中方法的实现了: + +```objectivec +@defs(TestProtocol) + +- (void)buzz { + NSLog(@"Buzz"); +} + +@end +``` + +这样所有遵循 `TestProtocol` 协议的对象都可以调用 `buzz` 方法,哪怕它们没有实现: + +![protocol-demo](images/protocol-demo.jpeg) + +上面的 `XXObject` 虽然没有实现 `buzz` 方法,但是该方法仍然成功执行了。 + +### ProtocolKit 的实现 + +ProtocolKit 的主要原理仍然是 runtime 以及宏的;通过宏的使用来**隐藏类的声明以及实现的代码**,然后在 main 函数运行之前,**将类中的方法实现加载到内存**,使用 runtime 将实现**注入到目标类**中。 + +> 如果你对上面的原理有所疑惑也不是太大的问题,这里只是给你一个 ProtocolKit 原理的简单描述,让你了解它是如何工作的。 + +ProtocolKit 中有两条重要的执行路线: + ++ `_pk_extension_load` 将协议扩展中的方法实现加载到了内存 ++ `_pk_extension_inject_entry` 负责将扩展协议注入到实现协议的类 + +#### 加载实现 + +首先要解决的问题是如何将方法实现加载到内存中,这里可以先了解一下上面使用到的 `defs` 接口,它其实只是一个调用了其它宏的**超级宏**~~这名字是我编的~~: + +```objectivec +#define defs _pk_extension + +#define _pk_extension($protocol) _pk_extension_imp($protocol, _pk_get_container_class($protocol)) + +#define _pk_extension_imp($protocol, $container_class) \ + protocol $protocol; \ + @interface $container_class : NSObject <$protocol> @end \ + @implementation $container_class \ + + (void)load { \ + _pk_extension_load(@protocol($protocol), $container_class.class); \ + } \ + +#define _pk_get_container_class($protocol) _pk_get_container_class_imp($protocol, __COUNTER__) +#define _pk_get_container_class_imp($protocol, $counter) _pk_get_container_class_imp_concat(__PKContainer_, $protocol, $counter) +#define _pk_get_container_class_imp_concat($a, $b, $c) $a ## $b ## _ ## $c +``` + +> 使用 `defs` 作为接口的是因为它是一个保留的 keyword,Xcode 会将它渲染成与 `@property` 等其他关键字相同的颜色。 + +上面的这一坨宏并不需要一个一个来分析,只需要看一下最后展开会变成什么: + +```objectivec +@protocol TestProtocol; + +@interface __PKContainer_TestProtocol_0 : NSObject <TestProtocol> + +@end + +@implementation __PKContainer_TestProtocol_0 + ++ (void)load { + _pk_extension_load(@protocol(TestProtocol), __PKContainer_TestProtocol_0.class); +} +``` + +根据上面宏的展开结果,这里可以介绍上面的一坨宏的作用: + ++ `defs` 这货没什么好说的,只是 `_pk_extension` 的别名,为了提供一个更加合适的名字作为接口 ++ `_pk_extension` 向 `_pk_extension_imp ` 中传入 `$protocol` 和 `_pk_get_container_class($protocol)` 参数 + + `_pk_get_container_class` 的执行生成一个类名,上面生成的类名就是 `__PKContainer_TestProtocol_0`,这个类名是 `__PKContainer_`、 `$protocol` 和 `__COUNTER__` 拼接而成的(`__COUNTER__` 只是一个计数器,可以理解为每次调用时加一) ++ `_pk_extension_imp` 会以传入的类名生成一个遵循当前 `$protocol` 协议的类,然后在 `+ load` 方法中执行 `_pk_extension_load` 加载扩展协议 + +通过宏的运用成功隐藏了 `__PKContainer_TestProtocol_0` 类的声明以及实现,还有 `_pk_extension_load` 函数的调用: + +```objectivec +void _pk_extension_load(Protocol *protocol, Class containerClass) { + + pthread_mutex_lock(&protocolsLoadingLock); + + if (extendedProtcolCount >= extendedProtcolCapacity) { + size_t newCapacity = 0; + if (extendedProtcolCapacity == 0) { + newCapacity = 1; + } else { + newCapacity = extendedProtcolCapacity << 1; + } + allExtendedProtocols = realloc(allExtendedProtocols, sizeof(*allExtendedProtocols) * newCapacity); + extendedProtcolCapacity = newCapacity; + } + + ... + + pthread_mutex_unlock(&protocolsLoadingLock); +} +``` + +ProtocolKit 使用了 `protocolsLoadingLock` 来保证静态变量 `allExtendedProtocols` 以及 `extendedProtcolCount` `extendedProtcolCapacity` 不会因为线程竞争导致问题: + ++ `allExtendedProtocols` 用于保存所有的 `PKExtendedProtocol` 结构体 ++ 后面的两个变量确保数组不会越界,并在数组满的时候,将内存占用地址翻倍 + +方法的后半部分会在静态变量中寻找或创建传入的 `protocol` 对应的 `PKExtendedProtocol` 结构体: + +```objectivec +size_t resultIndex = SIZE_T_MAX; +for (size_t index = 0; index < extendedProtcolCount; ++index) { + if (allExtendedProtocols[index].protocol == protocol) { + resultIndex = index; + break; + } +} + +if (resultIndex == SIZE_T_MAX) { + allExtendedProtocols[extendedProtcolCount] = (PKExtendedProtocol){ + .protocol = protocol, + .instanceMethods = NULL, + .instanceMethodCount = 0, + .classMethods = NULL, + .classMethodCount = 0, + }; + resultIndex = extendedProtcolCount; + extendedProtcolCount++; +} + +_pk_extension_merge(&(allExtendedProtocols[resultIndex]), containerClass); +``` + +这里调用的 `_pk_extension_merge` 方法非常重要,不过在介绍 `_pk_extension_merge` 之前,首先要了解一个用于保存协议扩展信息的私有结构体 `PKExtendedProtocol`: + +```objectivec +typedef struct { + Protocol *__unsafe_unretained protocol; + Method *instanceMethods; + unsigned instanceMethodCount; + Method *classMethods; + unsigned classMethodCount; +} PKExtendedProtocol; +``` + +`PKExtendedProtocol` 结构体中保存了协议的指针、实例方法、类方法、实例方法数以及类方法数用于框架记录协议扩展的状态。 + +回到 `_pk_extension_merge` 方法,它会将新的扩展方法追加到 `PKExtendedProtocol` 结构体的数组 `instanceMethods` 以及 `classMethods` 中: + +```objectivec +void _pk_extension_merge(PKExtendedProtocol *extendedProtocol, Class containerClass) { + // Instance methods + unsigned appendingInstanceMethodCount = 0; + Method *appendingInstanceMethods = class_copyMethodList(containerClass, &appendingInstanceMethodCount); + Method *mergedInstanceMethods = _pk_extension_create_merged(extendedProtocol->instanceMethods, + extendedProtocol->instanceMethodCount, + appendingInstanceMethods, + appendingInstanceMethodCount); + free(extendedProtocol->instanceMethods); + extendedProtocol->instanceMethods = mergedInstanceMethods; + extendedProtocol->instanceMethodCount += appendingInstanceMethodCount; + + // Class methods + ... +} +``` + +> 因为类方法的追加与实例方法几乎完全相同,所以上述代码省略了向结构体中的类方法追加方法的实现代码。 + +实现中使用 `class_copyMethodList` 从 `containerClass` 拉出方法列表以及方法数量;通过 `_pk_extension_create_merged` 返回一个合并之后的方法列表,最后在更新结构体中的 `instanceMethods` 以及 `instanceMethodCount` 成员变量。 + +`_pk_extension_create_merged` 只是重新 `malloc` 一块内存地址,然后使用 `memcpy` 将所有的方法都复制到了这块内存地址中,最后返回首地址: + +```objectivec +Method *_pk_extension_create_merged(Method *existMethods, unsigned existMethodCount, Method *appendingMethods, unsigned appendingMethodCount) { + + if (existMethodCount == 0) { + return appendingMethods; + } + unsigned mergedMethodCount = existMethodCount + appendingMethodCount; + Method *mergedMethods = malloc(mergedMethodCount * sizeof(Method)); + memcpy(mergedMethods, existMethods, existMethodCount * sizeof(Method)); + memcpy(mergedMethods + existMethodCount, appendingMethods, appendingMethodCount * sizeof(Method)); + return mergedMethods; +} +``` + +这一节的代码从使用宏生成的类中抽取方法实现,然后以结构体的形式加载到内存中,等待之后的方法注入。 + +#### 注入方法实现 + +注入方法的时间点在 main 函数执行之前议实现的注入并不是在 `+ load` 方法 `+ initialize` 方法调用时进行的,而是使用的编译器指令(compiler directive) `__attribute__((constructor))` 实现的: + +```objectivec +__attribute__((constructor)) static void _pk_extension_inject_entry(void); +``` + +使用上述编译器指令的函数会在 shared library 加载的时候执行,也就是 main 函数之前,可以看 StackOverflow 上的这个问题 [How exactly does __attribute__((constructor)) work?](http://stackoverflow.com/questions/2053029/how-exactly-does-attribute-constructor-work)。 + +```objectivec +__attribute__((constructor)) static void _pk_extension_inject_entry(void) { + #1:加锁 + unsigned classCount = 0; + Class *allClasses = objc_copyClassList(&classCount); + + @autoreleasepool { + for (unsigned protocolIndex = 0; protocolIndex < extendedProtcolCount; ++protocolIndex) { + PKExtendedProtocol extendedProtcol = allExtendedProtocols[protocolIndex]; + for (unsigned classIndex = 0; classIndex < classCount; ++classIndex) { + Class class = allClasses[classIndex]; + if (!class_conformsToProtocol(class, extendedProtcol.protocol)) { + continue; + } + _pk_extension_inject_class(class, extendedProtcol); + } + } + } + #2:解锁并释放 allClasses、allExtendedProtocols +} +``` + +`_pk_extension_inject_entry` 会在 main 执行之前遍历内存中的**所有** `Class`(整个遍历过程都是在一个自动释放池中进行的),如果某个类遵循了`allExtendedProtocols` 中的协议,调用 `_pk_extension_inject_class` 向类中注射(inject)方法实现: + +```objectivec +static void _pk_extension_inject_class(Class targetClass, PKExtendedProtocol extendedProtocol) { + + for (unsigned methodIndex = 0; methodIndex < extendedProtocol.instanceMethodCount; ++methodIndex) { + Method method = extendedProtocol.instanceMethods[methodIndex]; + SEL selector = method_getName(method); + + if (class_getInstanceMethod(targetClass, selector)) { + continue; + } + + IMP imp = method_getImplementation(method); + const char *types = method_getTypeEncoding(method); + class_addMethod(targetClass, selector, imp, types); + } + + #1: 注射类方法 +} +``` + +如果类中没有实现该实例方法就会通过 runtime 中的 `class_addMethod` 注射该实例方法;而类方法的注射有些不同,因为类方法都是保存在元类中的,而一些类方法由于其特殊地位最好不要改变其原有实现,比如 `+ load` 和 `+ initialize` 这两个类方法就比较特殊,如果想要了解这两个方法的相关信息,可以在 [Reference](#reference) 中查看相关的信息。 + +```objectivec +Class targetMetaClass = object_getClass(targetClass); +for (unsigned methodIndex = 0; methodIndex < extendedProtocol.classMethodCount; ++methodIndex) { + Method method = extendedProtocol.classMethods[methodIndex]; + SEL selector = method_getName(method); + + if (selector == @selector(load) || selector == @selector(initialize)) { + continue; + } + if (class_getInstanceMethod(targetMetaClass, selector)) { + continue; + } + + IMP imp = method_getImplementation(method); + const char *types = method_getTypeEncoding(method); + class_addMethod(targetMetaClass, selector, imp, types); +} +``` + +实现上的不同仅仅在获取元类、以及跳过 `+ load` 和 `+ initialize` 方法上。 + +## 总结 + +ProtocolKit 通过宏和 runtime 实现了类似协议扩展的功能,其实现代码总共也只有 200 多行,还是非常简洁的;在另一个叫做 [libextobjc](https://github.com/jspahrsummers/libextobjc) 的框架中也实现了类似的功能,有兴趣的读者可以查看 [EXTConcreteProtocol.h · libextobjc]([https://github.com/jspahrsummers/libextobjc/blob/master/contents/extobjc/EXTConcreteProtocol.h]) 这个文件。 + +## Reference + ++ [Protocols · Apple Doc](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift\_Programming\_Language/Extensions.html#//apple\_ref/doc/uid/TP40014097-CH24-ID151) ++ [EXTConcreteProtocol.h · libextobjc](https://github.com/jspahrsummers/libextobjc/blob/master/contents/extobjc/EXTConcreteProtocol.h) ++ [\_\_attribute__ · NSHipster](http://nshipster.com/__attribute__/) ++ [你真的了解 load 方法么?](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/你真的了解%20load%20方法么?.md) ++ [懒惰的 initialize 方法](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/懒惰的%20initialize%20方法.md) + + diff --git a/contents/Rails/activerecord.md b/contents/Rails/activerecord.md new file mode 100644 index 0000000..ac3acca --- /dev/null +++ b/contents/Rails/activerecord.md @@ -0,0 +1,1684 @@ +# 全面理解 ActiveRecord + +最近事情并不是特别多,看了一些数据库相关的书籍,最后想到自己并不了解每天都在用的 ActiveRecord,对于它是如何创建模型、建立关系、执行 SQL 查询以及完成数据库迁移的,作者一直都有着自己的猜测,但是真正到源代码中去寻找答案一直都是没有做过的。 + +![activerecord-architecture](images/activerecord/activerecord-architecture.png) + +我们可以将 ActiveRecord 理解为一个不同 SQL 数据库的 Wrapper,同时为上层提供一种简洁、优雅的 API 或者说 DSL,能够极大得减轻开发者的负担并提升工作效率。 + +文章分四个部分介绍了 ActiveRecord 中的重要内容,模型的创建过程、Scope 和查询的实现、模型关系的实现以及最后的 Migrations 任务的实现和执行过程,各个模块之间没有太多的关联,由于文章内容比较多,如果读者只对某一部分的内容感兴趣,可以只挑选一部分进行阅读。 + +## 模型的创建过程 + +在这篇文章中,我们会先分析阅读 ActiveRecord 是如何创建模型并将数据插入到数据库中的,由于 ActiveRecord 的源码变更非常迅速,这里使用的 ActiveRecord 版本是 v5.1.4,如果希望重现文中对方法的追踪过程可以 checkout 到 v5.1.4 的标签上并使用如下所示的命令安装指定版本的 ActiveRecord: + +```shell +$ gem install activerecord -v '5.1.4' +``` + +### 引入 ActiveRecord + +在正式开始使用 [pry](https://github.com/pry/pry) 对方法进行追踪之前,我们需要现在 pry 中 `require` 对应的 gem,并且创建一个用于追踪的模型类: + +```ruby +pry(main)> require 'active_record' +=> true +pry(main)> class Post < ActiveRecord::Base; end +=> nil +``` + +这个步骤非常的简单,这里也不多说什么了,只是创建了一个继承自 `ActiveRecord::Base` 的类 `Post`,虽然我们并没有在数据库中创建对应的表结构,不过目前来说已经够用了。 + +### 从 Post.create 开始 + +使用过 ActiveRecord 的人都知道,当我们使用 `Post.create` 方法的时候就会在数据库中创建一条数据记录,所以在这里我们就将该方法作为入口一探究竟: + +```ruby +pry(main)> $ Post.create + +From: lib/active_record/persistence.rb @ line 29: +Owner: ActiveRecord::Persistence::ClassMethods + +def create(attributes = nil, &block) + if attributes.is_a?(Array) + attributes.collect { |attr| create(attr, &block) } + else + object = new(attributes, &block) + object.save + object + end +end +``` + +> `$` 是 pry 为我们提供的用于查看方法源代码的工具,这篇文章中会省略 `$` 方法的一部分输出,还可能会对方法做一些简化减少理解方法实现时的干扰。 + +通过 pry 的输出,我们可以在 ActiveRecord 的 `lib/active_record/persistence.rb` 文件中找到 `ActiveRecord::Base.create` 方法的实现,如果传入的参数是一个 `Hash`,该方法会先后执行 `ActiveRecord::Base.new` 和 `ActiveRecord::Base#save!` 创建一个新的对象并保存。 + +#### 使用 pry 追踪 #save! + +`ActiveRecord::Base.new` 在大多数情况下都会调用父类的 `#initialize` 方法初始化实例,所以没有什么好说的,而 `ActiveRecord::Base#save!` 方法就做了很多事情: + +```ruby +pry(main)> $ ActiveRecord::Base#save! + +From: lib/active_record/suppressor.rb @ line 45: +Owner: ActiveRecord::Suppressor + +def save!(*) # :nodoc: + SuppressorRegistry.suppressed[self.class.name] ? true : super +end +``` + +首先是使用 `SuppressorRegistry` 来判断是否需要对当前的存取请求进行抑制,然后执行 `super` 方法,由于从上述代码中没有办法知道这里的 `super` 到底是什么,所以我们就需要通过 `.ancestors` 方法看看 `ActiveRecord::Base` 到底有哪些父类了: + +```ruby +pry(main)> ActiveRecord::Base.ancestors +=> [ActiveRecord::Base, + ActiveRecord::Suppressor, + ... + ActiveRecord::Persistence, + ActiveRecord::Core, + ActiveSupport::ToJsonWithActiveSupportEncoder, + Object, + ... + Kernel, + BasicObject] + +pry(main)> ActiveRecord::Base.ancestors.count +=> 65 +``` + +使用 `.ancestors` 方法,你就可以看到整个方法调用链上包含 64 个父类,在这时简单的使用 pry 就已经不能帮助我们理解方法的调用过程了,因为 pry 没法查看当前的方法在父类中是否存在,我们需要从工程中分析哪些类的 `#save!` 方法在整个过程中被执行了并根据上述列表排出它们执行的顺序;经过分析,我们得到如下的结果: + +![activerecord-base-save](images/activerecord/activerecord-base-save.png) + +从 `ActiveRecord::Suppressor` 到 `ActiveRecord::Persistence` 一共有五个 module 实现了 `#save!` 方法,上面我们已经知道了 `ActiveRecord::Suppressor#save!` 模块提供了对保存的抑制功能,接下来将依次看后四个方法都在保存模型的过程中做了什么。 + +#### 事务的执行 + +从名字就可以看出 `ActiveRecord::Transactions` 主要是为数据库事务提供支持,并在数据库事务的不同阶段执行不同的回调,这个 module 中的 `#save!` 方法仅在 `#with_transaction_returning_status` 的 block 中执行了 `super`: + +```ruby +module ActiveRecord + module Transactions + def save!(*) #:nodoc: + with_transaction_returning_status { super } + end + end +end +``` + +`#with_transaction_returning_status` 方法会运行外部传入的 block 通过 `super` 执行父类的 `#save!` 方法: + +```ruby +def with_transaction_returning_status + status = nil + self.class.transaction do + add_to_transaction + begin + status = yield + rescue ActiveRecord::Rollback + clear_transaction_record_state + status = nil + end + + raise ActiveRecord::Rollback unless status + end + status +ensure + if @transaction_state && @transaction_state.committed? + clear_transaction_record_state + end +end +``` + +通过上述方法,我们将所有的 SQL 请求都包装在了一个 `.transaction` 中,开启一个新的数据库事务并在其中执行请求,在这里统一处理一些跟事务回滚以及异常相关的逻辑,同时 `ActiveRecord::Transactions` 又能为当前的模型添加一些回调的支持: + +```ruby +module ActiveRecord + module Transactions + included do + define_callbacks :commit, :rollback, + :before_commit, + :before_commit_without_transaction_enrollment, + :commit_without_transaction_enrollment, + :rollback_without_transaction_enrollment, + scope: [:kind, :name] + end + end +end +``` + +开发者就能够在模型中根据需要注册回调用来监听各种数据库事务相关的事件,绝大多数的事务最终都会在 `ActiveRecord::ConnectionAdapters::Transaction#within_new_transaction` 方法中执行: + +```ruby +def within_new_transaction(options = {}) + @connection.lock.synchronize do + begin + transaction = begin_transaction options + yield + rescue Exception => error + if transaction + rollback_transaction + after_failure_actions(transaction, error) + end + raise + ensure + unless error + if Thread.current.status == "aborting" + rollback_transaction if transaction + else + begin + commit_transaction + rescue Exception + rollback_transaction(transaction) unless transaction.state.completed? + raise + end + end + end + end + end +end +``` + +上述方法虽然看起来非常复杂,但是方法的逻辑还是还是非常清晰的,如果事务没有抛出任何的异常,就可以将上述代码简化成以下的几行代码: + +```ruby +def within_new_transaction(options = {}) + @connection.lock.synchronize do + begin_transaction options + yield + commit_transaction + end + end +end +``` + +我们可以看到,经过一系列的方法调用最后会在数据库中执行 `BEGIN`、SQL 语句和 `COMMIT` 来完成数据的持久化。 + +#### 追踪属性的重置 + +当 `ActiveRecord::Transactions#save!` 通过 `super` 将方法抛给上层之后,就由 `ActiveRecord::AttributesMethod::Dirty` 来处理了: + +```ruby +def save!(*) + super.tap do + changes_applied + end +end +``` + +如果 `#save!` 最终执行成功,在这个阶段会将所有模型改变的标记全部清除,对包括 `@changed_attributes`、`@mutation_tracker` 在内的实例变量全部重置,为追踪下一次模型的修改做准备。 + +#### 字段的验证 + +沿着整个继承链往下走,下一个被执行的模块就是 `ActiveRecord::Validations` 了,正如这么模块名字所暗示的,我们在这里会对模型中的字段进行验证: + +```ruby +def save!(options = {}) + perform_validations(options) ? super : raise_validation_error +end +``` + +上述代码使用 `#perform_validations` 方法验证模型中的全部字段,以此来保证所有的字段都符合我们的预期: + +```ruby +def perform_validations(options = {}) + options[:validate] == false || valid?(options[:context]) +end +``` + +在这个方法中我们可以看到如果在调用 `save!` 方法时,传入了 `validate: false` 所有的验证就都会被跳过,我们通过 `#valid?` 来判断当前的模型是否合法,而这个方法的执行过程其实也包含两个过程: + +```ruby +module ActiveRecord + module Validations + def valid?(context = nil) + context ||= default_validation_context + output = super(context) + errors.empty? && output + end + end +end + +module ActiveModel + module Validations + def valid?(context = nil) + current_context, self.validation_context = validation_context, context + errors.clear + run_validations! + ensure + self.validation_context = current_context + end + end +end +``` + +由于 `ActiveModel::Validations` 是 `ActiveRecord::Validations` 的『父类』,所以在 `ActiveRecord::Validations` 执行 `#valid?` 方法时,最终会执行父类 `#run_validations` 运行全部的验证回调。 + +```ruby +module ActiveModel + module Validations + def run_validations! + _run_validate_callbacks + errors.empty? + end + end +end +``` + +通过上述方法的实现,我们可以发现验证是否成功其实并不是通过我们在 `validate` 中传入一个返回 `true/false` 的方法决定的,而是要向当前模型的 `errors` 中添加更多的错误: + +```ruby +class Invoice < ApplicationRecord + validate :active_customer + + def active_customer + errors.add(:customer_id, "is not active") unless customer.active? + end +end +``` + +在这个过程中执行的另一个方法 `#_run_validate_callbacks` 其实是通过 `ActiveSupport::Callbacks` 提供的 `#define_callbacks` 方法动态生成的,所以我们没有办法在工程中搜索到: + +```ruby +def define_callbacks(*names) + options = names.extract_options! + + names.each do |name| + name = name.to_sym + set_callbacks name, CallbackChain.new(name, options) + module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def _run_#{name}_callbacks(&block) + run_callbacks #{name.inspect}, &block + end + + def self._#{name}_callbacks + get_callbacks(#{name.inspect}) + end + + def self._#{name}_callbacks=(value) + set_callbacks(#{name.inspect}, value) + end + + def _#{name}_callbacks + __callbacks[#{name.inspect}] + end + RUBY + end +end +``` + +在这篇文章中,我们只需要知道该 `#save!` 在合适的时机运行了正确的回调就可以了,在后面的文章(可能)中会详细介绍整个 callbacks 的具体执行流程。 + +#### 数据的持久化 + +`#save!` 的调用栈最顶端就是 `ActiveRecord::Persistence#save!` 方法: + +```ruby +def save!(*args, &block) + create_or_update(*args, &block) || raise(RecordNotSaved.new("Failed to save the record", self)) +end + +def create_or_update(*args, &block) + _raise_readonly_record_error if readonly? + result = new_record? ? _create_record(&block) : _update_record(*args, &block) + result != false +end +``` + +在这个方法中,我们执行了 `#create_or_update` 以及 `#_create_record` 两个方法来创建模型: + +```ruby +def _create_record(attribute_names = self.attribute_names) + attributes_values = arel_attributes_with_values_for_create(attribute_names) + new_id = self.class.unscoped.insert attributes_values + self.id ||= new_id if self.class.primary_key + @new_record = false + yield(self) if block_given? + id +end +``` + +在这个私有方法中开始执行数据的插入操作了,首先是通过 `ActiveRecord::AttributeMethods#arel_attributes_with_values_for_create` 方法获取一个用于插入数据的字典,其中包括了数据库中的表字段和对应的待插入值。 + +![database-statement-insert](images/activerecord/database-statement-insert.png) + +而下面的 `.insert` 方法就会将这个字典转换成 SQL 语句,经过上图所示的调用栈最终到不同的数据库中执行语句并返回最新的主键。 + +### 小结 + +从整个模型的创建过程中,我们可以看到 ActiveRecord 对于不同功能的组织非常优雅,每一个方法都非常简短并且易于阅读,通过对应的方法名和模块名我们就能够明确的知道这个东西是干什么的,对于同一个方法的不同执行逻辑也分散了不同的模块中,最终使用 module 加上 include 的方式组织起来,如果要对某个方法添加一些新的逻辑也可以通过增加更多的 module 达到目的。 + +通过对源代码的阅读,我们可以看到对于 ActiveRecord 来说,`#create` 和 `#save!` 方法的执行路径其实是差不多的,只是在细节上有一些不同之处。 + +![actual-callstack-for-activerecord-base-save](images/activerecord/actual-callstack-for-activerecord-base-save.png) + +虽然模型或者说数据行的创建过程最终会从子类一路执行到父类的 `#save!` 方法,但是逻辑的**处理顺序**并不是按照从子类到父类执行的,我们可以通过上图了解不同模块的真正执行过程。 + +## Scope 和查询的实现 + +除了模型的插入、创建和迁移模块,ActiveRecord 中还有另一个非常重要的模块,也就是 Scope 和查询;为什么同时介绍这两个看起来毫不相干的内容呢?这是因为 Scope 和查询是完全分不开的一个整体,在 ActiveRecord 的实现中,两者有着非常紧密的联系。 + +### ActiveRecord::Relation + +对 ActiveRecord 稍有了解的人都知道,在使用 ActiveRecord 进行查询时,所有的查询方法其实都会返回一个 `#{Model}::ActiveRecord_Relation` 类的对象,比如 `User.all`: + +```ruby +pry(main)> User.all.class +=> User::ActiveRecord_Relation +``` + +在这里使用 pry 来可以帮助我们快速理解整个过程到底都发生了什么事情: + +```ruby +pry(main)> $ User.all + +From: lib/active_record/scoping/named.rb @ line 24: +Owner: ActiveRecord::Scoping::Named::ClassMethods + +def all + if current_scope + current_scope.clone + else + default_scoped + end +end +``` + +`#all` 方法中的注释中也写着它会返回一个 `ActiveRecord::Relation` 对象,它其实可以理解为 ActiveRecord 查询体系中的单位元,它的调用并不改变当前查询;而如果我们使用 pry 去看其他的方法例如 `User.where` 的时候: + +```ruby +pry(main)> $ User.where + +From: lib/active_record/querying.rb @ line 10: +Owner: ActiveRecord::Querying + +delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or, + :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, + :having, :create_with, :distinct, :references, :none, :unscope, :merge, to: :all +``` + +从这里我们可以看出,真正实现为 `User` 类方法的只有 `.all`,其他的方法都会代理给 `all` 方法,在 `.all` 方法返回的对象上执行: + +![active-record-relation-delegation](images/activerecord/active-record-relation-delegation.png) + +所有直接在类上调用的方法都会先执行 `#all`,也就是说下面的几种写法是完全相同的: + +```ruby +User .where(name: 'draven') +User.all.where(name: 'draven') +User.all.where(name: 'draven').all +``` + +当我们了解了 `.where == .all + #where` 就可以再一次使用 pry 来查找真正被 ActiveRecord 实现的 `#where` 方法: + +```ruby +pry(main)> $ User.all.where + +From: lib/active_record/relation/query_methods.rb @ line 599: +Owner: ActiveRecord::QueryMethods + +def where(opts = :chain, *rest) + if :chain == opts + WhereChain.new(spawn) + elsif opts.blank? + self + else + spawn.where!(opts, *rest) + end +end +``` + +在分析查询的过程中,我们会选择几个常见的方法作为入口,尽可能得覆盖较多的查询相关的代码,增加我们对 ActiveRecord 的理解和认识。 + +### 从 User.all 开始 + +再来看一下上面看到的 `ActiveRecord::Relation.all` 方法,无论是 `#current_scope` 还是 `#default_scoped` 其实返回的都是当前的 `ActiveRecord` 对象: + +```ruby +def all + if current_scope + current_scope.clone + else + default_scoped + end +end +``` + +#### current_scope 和 default_scope + +如果当前没有 `#current_scope` 那么,就会调用 `#default_scoped` 返回响应的结果,否则就会 clone 当前对象并返回,可以简单举一个例子证明这里的猜测: + +```ruby +pry(main)> User.current_scope +=> nil +pry(main)> User.all.current_scope + User Load (0.1ms) SELECT "users".* FROM "users" +=> [] +pry(main)> User.all.current_scope.class +=> User::ActiveRecord_Relation +``` + +`.current_scope` 是存储在位于线程变量的 `ScopeRegistry` 中,它其实就是当前的查询语句的上下文,存储着这一次链式调用造成的全部副作用: + +```ruby +def current_scope(skip_inherited_scope = false) + ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope) +end +``` + +而 `.default_scoped` 就是在当前查询链刚开始时执行的第一个方法,因为在执行第一个查询方法之前 `.current_scope` 一定为空: + +```ruby +def default_scoped(scope = relation) + build_default_scope(scope) || scope +end + +def build_default_scope(base_rel = nil) + return if abstract_class? + + if default_scopes.any? + base_rel ||= relation + evaluate_default_scope do + default_scopes.inject(base_rel) do |default_scope, scope| + scope = scope.respond_to?(:to_proc) ? scope : scope.method(:call) + default_scope.merge(base_rel.instance_exec(&scope)) + end + end + end +end +``` + +当我们在 Rails 的模型层中使用 `.default_scope` 定义一些默认的上下文时,所有的 block 都换被转换成 `Proc` 对象最终添加到 `default_scopes` 数组中: + +```ruby +def default_scope(scope = nil) # :doc: + scope = Proc.new if block_given? + # ... + self.default_scopes += [scope] +end +``` + +上面提到的 `.build_default_scope` 方法其实只是在 `default_scopes` 数组不为空时,将当前的 `Relation` 对象和数组中的全部 scope 一一 `#merge` 并返回一个新的 `Relation` 对象。 + +#### ActiveRecord::Relation 对象 + +`.default_scoped` 方法的参数 `scope` 其实就有一个默认值 `#relation`,这个默认值其实就是一个 `Relation` 类的实例: + +```ruby +def relation + relation = Relation.create(self, arel_table, predicate_builder) + + if finder_needs_type_condition? && !ignore_default_scope? + relation.where(type_condition).create_with(inheritance_column.to_s => sti_name) + else + relation + end +end +``` + +`Relation.create` 对象的创建过程其实比较复杂,我们只需要知道经过 ActiveRecord 一系列的疯狂操作,最终会将几个参数传入 `.new` 方法初始化一个 `ActiveRecord::Relation` 实例: + +```ruby +class Relation + def initialize(klass, table, predicate_builder, values = {}) + @klass = klass + @table = table + @values = values + @offsets = {} + @loaded = false + @predicate_builder = predicate_builder + end +end +``` + +当执行的是 `#all`、`.all` 或者绝大多数查询方法时,都会直接将这个初始化的对象返回来接受随后的链式调用。 + +### where 方法 + +相比于 `#all`、`#where` 查询的实现就复杂多了,不像 `#all` 会返回一个默认的 `Relation` 对象,`#where` 由 `WhereClause` 以及 `WhereClauseFactory` 等类共同处理;在 `#where` 的最正常的执行路径中,它会执行 `#where!` 方法: + +```ruby +def where(opts = :chain, *rest) + if :chain == opts + WhereChain.new(spawn) + elsif opts.blank? + self + else + spawn.where!(opts, *rest) + end +end + +def where!(opts, *rest) + opts = sanitize_forbidden_attributes(opts) + references!(PredicateBuilder.references(opts)) if Hash === opts + self.where_clause += where_clause_factory.build(opts, rest) + self +end +``` + +> `#spawn` 其实就是对当前的 `Relation` 对象进行 `#clone`。 + +查询方法 `#where!` 中的四行代码只有一行代码是我们需要关注的,该方法调用 `WhereClauseFactory#build` 生成一条 where 查询并存储到当前对象的 `where_clause` 中,在这个过程中并不会生成 SQL,而是会生成一个 `WhereClause` 对象,其中存储着 SQL 节点树: + +```ruby +pry(main)> User.where(name: 'draven').where_clause +=> #<ActiveRecord::Relation::WhereClause:0x007fe5a10bf2c8 + @binds= + [#<ActiveRecord::Relation::QueryAttribute:0x007fe5a10bf4f8 + @name="name", + @original_attribute=nil, + @type=#<ActiveModel::Type::String:0x007fe59d33f2e0 @limit=nil, @precision=nil, @scale=nil>, + @value_before_type_cast="draven">], + @predicates= + [#<Arel::Nodes::Equality:0x007fe5a10bf368 + @left= + #<struct Arel::Attributes::Attribute + relation= + #<Arel::Table:0x007fe59cc87830 + @name="users", + @table_alias=nil, + @type_caster= + #<ActiveRecord::TypeCaster::Map:0x007fe59cc87bf0 + @types= + User(id: integer, avatar: string, nickname: string, wechat: string, name: string, gender: integer, school: string, grade: string, major: string, completed: boolean, created_at: datetime, updated_at: datetime, mobile: string, admin: boolean)>>, + name="name">, + @right=#<Arel::Nodes::BindParam:0x007fe5a10bf520>>]> +``` + +> [Arel](https://github.com/rails/arel) 是一个 Ruby 的 SQL 抽象语法树的管理器,ActiveRecord 查询的过程都是惰性的,在真正进入数据库查询之前,查询条件都是以语法树的形式存储的。 + +在这里不像展开介绍 SQL 语法树的生成过程,因为过程比较复杂,详细分析也没有太大的意义。 + +### order 方法 + +除了 `#where` 方法之外,在这里还想简单介绍一下另外一个常用的方法 `#order`: + +```ruby +def order(*args) + check_if_method_has_arguments!(:order, args) + spawn.order!(*args) +end + +def order!(*args) + preprocess_order_args(args) + self.order_values += args + self +end +``` + +该方法的调用栈与 `#where` 非常相似,在调用栈中都会执行另一个带有 `!` 的方法,也都会向自己持有的某个『属性』追加一些参数,参数的处理也有点复杂,在这里简单看一看就好: + +```ruby +def preprocess_order_args(order_args) + order_args.map! do |arg| + klass.send(:sanitize_sql_for_order, arg) + end + order_args.flatten! + validate_order_args(order_args) + + references = order_args.grep(String) + references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! + references!(references) if references.any? + + # if a symbol is given we prepend the quoted table name + order_args.map! do |arg| + case arg + when Symbol + arel_attribute(arg).asc + when Hash + arg.map { |field, dir| + case field + when Arel::Nodes::SqlLiteral + field.send(dir.downcase) + else + arel_attribute(field).send(dir.downcase) + end + } + else + arg + end + end.flatten! +end +``` + +同样的,`#order` 方法的使用也会向 `order_values` 数组中添加对应的语法元素: + +```ruby +pry(main)> User.order(name: :desc).order_values +=> [#<Arel::Nodes::Descending:0x007fe59ce4f190 + @expr= + #<struct Arel::Attributes::Attribute + relation= + #<Arel::Table:0x007fe59cc87830 + @name="users", + @table_alias=nil, + @type_caster= + #<ActiveRecord::TypeCaster::Map:0x007fe59cc87bf0 + @types= + User(id: integer, avatar: string, nickname: string, wechat: string, name: string, gender: integer, school: string, grade: string, major: string, completed: boolean, created_at: datetime, updated_at: datetime, mobile: string, admin: boolean)>>, + name=:name>>] +``` + +在这个方法的返回值中,我们也能看到与 Arel 相关的各种节点,可以大致理解上述语法树的作用。 + +### 语法树的存储 + +无论是 `#where` 还是 `#order` 方法,它们其实都会向当前的 `Relation` 对象中追加相应的语法树节点,而除了上述的两个方法之外 `#from`、`#distinct`、`#lock`、`#limit` 等等,几乎所有的查询方法都会改变 `Relation` 中的某个值,然而所有的值其实都是通过 `@values` 这个实例变量存储的: + +![activerecord-relation-value-methods](images/activerecord/activerecord-relation-value-methods.png) + +`@values` 中存储的值分为三类,`SINGLE_VALUE`、`MULTI_VALUE` 和 `CLAUSE`,这三类属性会按照下面的规则存储在 `@values` 中: + +```ruby +Relation::VALUE_METHODS.each do |name| + method_name = \ + case name + when *Relation::MULTI_VALUE_METHODS then "#{name}_values" + when *Relation::SINGLE_VALUE_METHODS then "#{name}_value" + when *Relation::CLAUSE_METHODS then "#{name}_clause" + end + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{method_name} # def includes_values + get_value(#{name.inspect}) # get_value(:includes) + end # end + + def #{method_name}=(value) # def includes_values=(value) + set_value(#{name.inspect}, value) # set_value(:includes, value) + end # end + CODE +end +``` + +各种不同的值在最后都会按照一定的命名规则,存储在这个 `@values` 字典中: + +```ruby +def get_value(name) + @values[name] || default_value_for(name) +end + +def set_value(name, value) + assert_mutability! + @values[name] = value +end +``` + +如果我们直接在一个查询链中访问 `#values` 方法可以获得其中存储的所有查询条件: + +```ruby +pry(main)> User.where(name: 'draven').order(name: :desc).values +=> {:references=>[], + :where=> + #<ActiveRecord::Relation::WhereClause:0x007fe59d14d860>, + :order=> + [#<Arel::Nodes::Descending:0x007fe59d14cd98>]} +``` + +很多 ActiveRecord 的使用者其实在使用的过程中都感觉在各种链式方法调用时没有改变任何事情,所有的方法都可以任意组合进行链式调用,其实每一个方法的调用都会对 `@values` 中存储的信息进行了修改,只是 ActiveRecord 很好地将它隐藏了幕后,让我们没有感知到它的存在。 + +### scope 方法 + +相比于 `.default_scope` 这个类方法只是改变了当前模型中的 `default_scopes` 数组,另一个方法 `.scope` 会为当前类定义一个新的类方法: + +```ruby +From: lib/active_record/scoping/named.rb @ line 155: +Owner: ActiveRecord::Scoping::Named::ClassMethods + +def scope(name, body, &block) + extension = Module.new(&block) if block + + if body.respond_to?(:to_proc) + singleton_class.send(:define_method, name) do |*args| + scope = all.scoping { instance_exec(*args, &body) } + scope = scope.extending(extension) if extension + scope || all + end + else + singleton_class.send(:define_method, name) do |*args| + scope = all.scoping { body.call(*args) } + scope = scope.extending(extension) if extension + scope || all + end + end +end +``` + +上述方法会直接在当前类的单类上通过 `define_methods` 为当前类定义类方法,定义的方法会在上面提到的 `.all` 的返回结果上执行 `#scoping`,存储当前执行的上下文,执行传入的 block,再恢复 `current_scope`: + +```ruby +def scoping + previous, klass.current_scope = klass.current_scope(true), self + yield +ensure + klass.current_scope = previous +end +``` + +在这里其实有一个可能很多人从来没用过的特性,就是在 `.scope` 方法中传入一个 block: + +```ruby +class User + scope :male, -> { where gender: :male } do + def twenty + where age: 20 + end + end +end + +pry(main)> User.male.twenty +#=> <#User:0x007f98f3d61c38> +pry(main)> User.twenty +#=> NoMethodError: undefined method `twenty' for #<Class:0x007f98f5c7b2b8> +pry(main)> User.female.twenty +#=> NoMethodError: undefined method `twenty' for #<User::ActiveRecord_Relation:0x007f98f5d950e0> +``` + +这个传入的 block 只会在当前 `Relation` 对象的单类上添加方法,如果我们想定义一些不想在其他作用域使用的方法就可以使用这种方式: + +```ruby +def extending(*modules, &block) + if modules.any? || block + spawn.extending!(*modules, &block) + else + self + end +end + +def extending!(*modules, &block) + modules << Module.new(&block) if block + modules.flatten! + self.extending_values += modules + extend(*extending_values) if extending_values.any? + self +end +``` + +而 `extending` 方法的实现确实与我们预期的一样,创建了新的 `Module` 对象之后,直接使用 `#extend` 将其中的方法挂载当前对象的单类上。 + +### 小结 + +到这里为止,我们对 ActiveRecord 中查询的分析就已经比较全面了,从最终要的 `Relation` 对象,到常见的 `#all`、`#where` 和 `#order` 方法,到 ActiveRecord 对语法树的存储,如何与 Arel 进行协作,在最后我们也介绍了 `.scope` 方法的工作原理,对于其它方法或者功能的实现其实也都大同小异,在这里就不展开细谈了。 + +## 模型的关系 + +作为一个关系型数据库的 ORM,ActiveRecord 一定要提供对模型之间关系的支持,它为模型之间的关系建立提供了四个类方法 `has_many`、`has_one`、`belongs_to` 和 `has_and_belongs_to_many`,在文章的这一部分,我们会从上面几个方法中选择一部分介绍 ActiveRecord 是如何建立模型之间的关系的。 + +![activerecord-associations](images/activerecord/activerecord-associations.png) + +### Association 和继承链 + +首先来看 `.has_many` 方法是如何实现的,我们可以通过 pry 直接找到该方法的源代码: + +```ruby +pry(main)> $ User.has_many + +From: lib/active_record/associations.rb @ line 1401: +Owner: ActiveRecord::Associations::ClassMethods + +def has_many(name, scope = nil, options = {}, &extension) + reflection = Builder::HasMany.build(self, name, scope, options, &extension) + Reflection.add_reflection self, name, reflection +end +``` + +整个 `.has_many` 方法的实现也只有两行代码,总共涉及两个类 `Builder::HasMany` 和 `Reflection`,其中前者用于创建新的 `HasMany` 关系,后者负责将关系添加到当前类中。 + +`HasMany` 类的实现其实非常简单,但是它从父类和整个继承链中继承了很多方法: + +![activerecord-hasmany-ancestors](images/activerecord/activerecord-hasmany-ancestors.png) + +我们暂时先忘记 `.has_many` 方法的实现,先来看一下这里涉及的两个非常重要的类都是如何工作的,首先是 `Association` 以及它的子类;在 ActiveRecord 的实现中,我们其实能够找到四种关系的 Builder,它们有着非常清晰简单的继承关系: + +![activerecord-ancestor-builders](images/activerecord/activerecord-ancestor-builders.png) + +在这里定义的 `.build` 方法其实实现也很清晰,它通过调用当前抽象类 `Association` 或者子类的响应方法完成一些建立关系必要的工作: + +```ruby +def self.build(model, name, scope, options, &block) + extension = define_extensions model, name, &block + reflection = create_reflection model, name, scope, options, extension + define_accessors model, reflection + define_callbacks model, reflection + define_validations model, reflection + reflection +end +``` + +其中包括创建用于操作、查询和管理当前关系扩展 Module 的 `.define_extensions` 方法,同时也会使用 `.create_reflection` 创建一个用于检查 ActiveRecord 类的关系的 `Reflection` 对象,我们会在下一节中展开介绍,在创建了 `Reflection` 后,我们会根据传入的模型和 `Reflection` 对象为当前的类,例如 `User` 定义属性存取方法、回调以及验证: + +```ruby +def self.define_accessors(model, reflection) + mixin = model.generated_association_methods + name = reflection.name + define_readers(mixin, name) + define_writers(mixin, name) +end + +def self.define_readers(mixin, name) + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}(*args) + association(:#{name}).reader(*args) + end + CODE +end + +def self.define_writers(mixin, name) + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}=(value) + association(:#{name}).writer(value) + end + CODE +end +``` + +存取方法还是通过 Ruby 的元编程能力定义的,在这里通过 `.class_eval` 方法非常轻松地就能在当前的模型中定义方法,关于回调和验证的定义在这里就不在展开介绍了。 + +### Reflection 和继承链 + +`Reflection` 启用了检查 ActiveRecord 类和对象的关系和聚合的功能,它能够在 Builder 中使用为 ActiveRecord 中的类创建对应属性和方法。 + +与 `Association` 一样,ActiveRecord 中的不同关系也有不同的 `Reflection`,根据不同的关系和不同的配置,ActiveRecord 中建立了一套 Reflection 的继承体系与数据库中的不同关系一一对应: + +![activerecord-reflections](images/activerecord/activerecord-reflections.png) + +当我们在上面使用 `.has_many` 方法时,会通过 `.create_reflection` 创建一个 `HasManyReflection` 对象: + +```ruby +def self.create_reflection(model, name, scope, options, extension = nil) + if scope.is_a?(Hash) + options = scope + scope = nil + end + + validate_options(options) + scope = build_scope(scope, extension) + ActiveRecord::Reflection.create(macro, name, scope, options, model) +end +``` + +`Reflection#create` 方法是一个工厂方法,它会根据传入的 `macro` 和 `options` 中的值选择合适的类实例化: + +```ruby +def self.create(macro, name, scope, options, ar) + klass = \ + case macro + when :composed_of + AggregateReflection + when :has_many + HasManyReflection + when :has_one + HasOneReflection + when :belongs_to + BelongsToReflection + else + raise "Unsupported Macro: #{macro}" + end + + reflection = klass.new(name, scope, options, ar) + options[:through] ? ThroughReflection.new(reflection) : reflection +end +``` + +这个创建的 `Reflection` 在很多时候都有非常重要的作用,在创建存储方法、回调和验证时,都需要将这个对象作为参数传入提供一定的支持,起到了数据源和提供 Helper 方法的作用。 + +在整个定义方法、属性以及回调的工作完成之后,会将当前的对象以 `name` 作为键存储到自己持有的一个 `_reflections` 字典中: + +```ruby +# class_attribute :_reflections, instance_writer: false + +def self.add_reflection(ar, name, reflection) + ar.clear_reflections_cache + ar._reflections = ar._reflections.merge(name.to_s => reflection) +end +``` + +这个字典中存储着所有在当前类中使用 `has_many`、`has_one`、`belongs_to` 等方法定义的关系对应的映射。 + +### 一对多关系 + +一对多关系的这一节会分别介绍两个极其重要的方法 `.has_many` 和 `.belongs_to` 的实现;在这里,会先通过 `.has_many` 关系了解它是如何通过覆写父类方法定制自己的特性的,之后会通过 `.belongs_to` 研究 getter/setter 方法的调用栈。 + +![one-to-many-association](images/activerecord/one-to-many-association.png) + +一对多关系在数据库的模型之间非常常见,而这两个方法在 ActiveRecord 也经常成对出现。 + +#### has_many + +当我们对构建关系模块的两大支柱都已经有所了解之后,再来看这几个常用的方法就没有太多的难度了,首先来看一下一对多关系中的『多』是怎么实现的: + +```ruby +def has_many(name, scope = nil, options = {}, &extension) + reflection = Builder::HasMany.build(self, name, scope, options, &extension) + Reflection.add_reflection self, name, reflection +end +``` + +由于已经对 `Reflection.add_reflection` 方法的实现有所了解,所以这里直接看 `.has_many` 调用的 `Builder::HasMany.build` 方法的实现就可以知道这个类方法究竟做了什么,: + +```ruby +def self.build(model, name, scope, options, &block) + extension = define_extensions model, name, &block + reflection = create_reflection model, name, scope, options, extension + define_accessors model, reflection + define_callbacks model, reflection + define_validations model, reflection + reflection +end +``` + +在这里执行的 `.build` 方法与抽象类中的方法实现完全相同,子类并没有覆盖父类实现的方法,我们来找一下 `.define_accessors`、`.define_callbacks` 和 `.define_validations` 三个方法在 has_many 关系中都做了什么。 + +`HasMany` 作为 has_many 关系的 Builder 类,其本身并没有实现太多的方法,只是对一些关系选项有一些自己独有的声明: + +```ruby +module ActiveRecord::Associations::Builder + class HasMany < CollectionAssociation + def self.macro + :has_many + end + + def self.valid_options(options) + super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type, :index_errors] + end + + def self.valid_dependent_options + [:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception] + end + end +end +``` + +由于本身 has_many 关系中的读写方法都是对集合的操作,所以首先覆写了 `.define_writers` 和 `.define_readers` 两个方法生成了另外一组操作 id 的 getter/setter 方法: + +```ruby +def self.define_readers(mixin, name) + super + + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name.to_s.singularize}_ids + association(:#{name}).ids_reader + end + CODE +end + +def self.define_writers(mixin, name) + super + + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name.to_s.singularize}_ids=(ids) + association(:#{name}).ids_writer(ids) + end + CODE +end +``` + +has_many 关系在 `CollectionAssociation` 和 `HasManyAssociation` 中实现的几个方法 `#reader`、`#writer`、`#ids_reader` 和 `#ids_writer` 其实还是比较复杂的,在这里就跳过不谈了。 + +而 `.define_callbacks` 和 `.define_extensions` 其实都大同小异,在作者看来没有什么值得讲的,has_many 中最重要的部分还是读写方法的实现过程,不过由于篇幅所限这里就不多说了。 + +#### belongs_to + +在一对多关系中,经常与 has_many 对应的关系 belongs_to 其实实现和调用栈也几乎完全相同: + +```ruby +def belongs_to(name, scope = nil, options = {}) + reflection = Builder::BelongsTo.build(self, name, scope, options) + Reflection.add_reflection self, name, reflection +end +``` + +但是与 has_many 比较大的不同是 `Builder::BelongsTo` 通过继承的父类定义了很多用于创建新关系的方法: + +```ruby +def self.define_accessors(model, reflection) + super + mixin = model.generated_association_methods + name = reflection.name + define_constructors(mixin, name) if reflection.constructable? + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def reload_#{name} + association(:#{name}).force_reload_reader + end + CODE +end + +def self.define_constructors(mixin, name) + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def build_#{name}(*args, &block) + association(:#{name}).build(*args, &block) + end + def create_#{name}(*args, &block) + association(:#{name}).create(*args, &block) + end + def create_#{name}!(*args, &block) + association(:#{name}).create!(*args, &block) + end + CODE +end +``` + +其他的部分虽然实现上也与 has_many 有着非常大的不同,但是原理基本上完全一致,不过在这里我们可以来看一下 belongs_to 关系创建的两个方法 `association` 和 `association=` 究竟是如何对数据库进行操作的。 + +```ruby +class Topic < ActiveRecord::Base + has_many :subtopics +end + +class Subtopic < ActiveRecord::Base + belongs_to :topic +end +``` + +假设我们有着如上所示的两个模型,它们之间是一对多关系,我们以这对模型为例先来看一下 `association` 这个读方法的调用栈。 + +![callstack-for-belongs-to-association-gette](images/activerecord/callstack-for-belongs-to-association-getter.png) + +通过我们对源代码和调用栈的阅读,我们可以发现其实如下的所有方法调用在大多数情况下是完全等价的,假设我们已经持有了一个 `Subtopic` 对象: + +```ruby +subtopic = Subtopic.first #=> #<Subtopic:0x007ff513f67768> + +subtopic.topic +subtopic.association(:topic).reader +subtopic.association(:topic).target +subtopic.association(:topic).load_target +subtopic.association(:topic).send(:find_target) +``` + +上述的五种方式都可以获得当前 `Subtopic` 对象的 belongs_to 关系对应的 `Topic` 数据行,而最后一个方法 `#find_target` 其实也就是真正创建、绑定到最后执行查询 SQL 的方法: + +```ruby +pry(main)> $ subtopic.association(:topic).find_target + +From: lib/active_record/associations/singular_association.rb @ line 38: +Owner: ActiveRecord::Associations::SingularAssociation + +def find_target + return scope.take if skip_statement_cache? + + conn = klass.connection + sc = reflection.association_scope_cache(conn, owner) do + StatementCache.create(conn) { |params| + as = AssociationScope.create { params.bind } + target_scope.merge(as.scope(self, conn)).limit(1) + } + end + + binds = AssociationScope.get_bind_values(owner, reflection.chain) + sc.execute(binds, klass, conn) do |record| + set_inverse_instance record + end.first +rescue ::RangeError + nil +end +``` + +我们已经对 `association` 方法的实现有了非常清楚的认知了,下面再来过一下 `association=` 方法的实现,首先还是来看一下 setter 方法的调用栈: + +![callstack-for-belongs-to-association-sette](images/activerecord/callstack-for-belongs-to-association-setter.png) + +相比于 getter 的调用栈,setter 方法的调用栈都复杂了很多,在研究 setter 方法实现的过程中我们一定要记住这个方法并不会改变数据库中对应的数据行,只会改变当前对应的某个属性,经过对调用栈和源代码的分析,我们可以有以下的结论:假设现在有一个 `Subtopic` 对象和一个新的 `Topic` 实例,那么下面的一系列操作其实是完全相同的: + +```ruby +subtopic = Subtopic.first #=> #<Subtopic:0x007ff513f67768> +new_topic = Topic.first #=> #<Topic:0x007ff514b24cb8> + +subtopic.topic = new_topic +subtopic.topic_id = new_topic.id +subtopic.association(:topic).writer(new_topic) +subtopic.association(:topic).replace(new_topic) +subtopic.association(:topic).replace_keys(new_topic) +subtopic.association(:topic).owner[:topic_id] = new_topic.id +subtopic[:topic_id] = new_topic.id +subtopic.write_attribute(:topic_id, new_topic.id) +``` + +虽然这些方法最后返回的结果可能有所不同,但是它们最终都会将 `subtopic` 对象的 `topic_id` 属性更新成 `topic.id`,上面的方法中有简单的,也有复杂的,不过都能达到相同的目的;我相信如果读者亲手创建上述的关系并使用 pry 查看源代码一定会对 getter 和 setter 的执行过程有着非常清楚的认识。 + +### 多对多关系 habtm + +无论是 has_many 还是 belongs_to 其实都是一个 ORM 原生需要支持的关系,但是 habtm(has_and_belongs_to_many) 却是 ActiveRecord 为我们提供的一个非常方便的语法糖,哪怕是并没有 `.has_and_belongs_to_many` 这个方法,我们也能通过 `.has_many` 实现多对多关系,得到与前者完全等价的效果,只是实现的过程稍微麻烦一些。 + +在这一小节中,我们想要了解 habtm 这个语法糖是如何工作的,它是如何将现有的关系组成更复杂的 habtm 的多对多关系的;想要了解它的工作原理,我们自然要分析它的源代码: + +```ruby +def has_and_belongs_to_many(name, scope = nil, **options, &extension) + builder = Builder::HasAndBelongsToMany.new name, self, options + join_model = ActiveSupport::Deprecation.silence { builder.through_model } + const_set join_model.name, join_model + private_constant join_model.name + + habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self) + middle_reflection = builder.middle_reflection join_model + Builder::HasMany.define_callbacks self, middle_reflection + Reflection.add_reflection self, middle_reflection.name, middle_reflection + middle_reflection.parent_reflection = habtm_reflection + + # ... + + hm_options = {} + hm_options[:through] = middle_reflection.name + hm_options[:source] = join_model.right_reflection.name + + # ... + + ActiveSupport::Deprecation.silence { has_many name, scope, hm_options, &extension } + _reflections[name.to_s].parent_reflection = habtm_reflection +end +``` + +> 在这里,我们对该方法的源代码重新进行组织和排序,方法的作用与 v5.1.4 中的完全相同。 + +上述方法在最开始先创建了一个 `HasAndBelongsToMany` 的 Builder 实例,然后在 block 中执行了这个 Builder 的 `#through_model` 方法: + +```ruby +def through_model + habtm = JoinTableResolver.build lhs_model, association_name, options + + join_model = Class.new(ActiveRecord::Base) { + class << self; + attr_accessor :left_model + attr_accessor :name + attr_accessor :table_name_resolver + attr_accessor :left_reflection + attr_accessor :right_reflection + end + + # ... + } + + join_model.name = "HABTM_#{association_name.to_s.camelize}" + join_model.table_name_resolver = habtm + join_model.left_model = lhs_model + join_model.add_left_association :left_side, anonymous_class: lhs_model + join_model.add_right_association association_name, belongs_to_options(options) + join_model +end +``` + +`#through_model` 方法会返回一个新的继承自 `ActiveRecord::Base` 的类,我们通过一下的例子来说明一下这里究竟做了什么,假设在我们的工程中定义了如下的两个类: + +```ruby +class Post < ActiveRecord::Base + has_and_belongs_to_many :tags +end + +class Tag < ActiveRecord::Base + has_and_belongs_to_many :posts +end +``` + +它们每个类都通过 `.has_and_belongs_to_many` 创建了一个 `join_model` 类,这两个类都是在当前类的命名空间下的: + +```ruby +class Post::HABTM_Posts < ActiveRecord::Base; end +class Tags::HABTM_Posts < ActiveRecord::Base; end +``` + +除了在当前类的命名空间下定义两个新的类之外,`#through_model` 方法还通过 `#add_left_association` 和 `#add_right_association` 为创建的私有类添加了两个 `.belongs_to` 方法的调用: + +```ruby +join_model = Class.new(ActiveRecord::Base) { + # ... + + def self.add_left_association(name, options) + belongs_to name, required: false, **options + self.left_reflection = _reflect_on_association(name) + end + + def self.add_right_association(name, options) + rhs_name = name.to_s.singularize.to_sym + belongs_to rhs_name, required: false, **options + self.right_reflection = _reflect_on_association(rhs_name) + end +} +``` + +所以在这里,每一个 HABTM 类中都通过 `.belongs_to` 增加了两个对数据库表中对应列的映射: + +```ruby +class Post::HABTM_Posts < ActiveRecord::Base + belongs_to :post_id, required: false + belongs_to :tag_id, required: false +end + +class Tags::HABTM_Posts < ActiveRecord::Base + belongs_to :tag_id, required: false + belongs_to :post_id, required: false +end +``` + +看到这里,你可能会认为既然有两个模型,那么应该会有两张表分别对应这两个模型,但是实际情况却不是这样。 + +![habtm-association-table-name](images/activerecord/habtm-association-table-name.png) + + +ActiveRecord 通过覆写这两个类的 `.table_name` 方法,使用一个 `JoinTableResolver` 来解决不同的模型拥有相同的数据库表的问题: + +```ruby +class Migration + module JoinTable + def join_table_name(table_1, table_2) + ModelSchema.derive_join_table_name(table_1, table_2).to_sym + end + end +end + +module ModelSchema + def self.derive_join_table_name(first_table, second_table) + [first_table.to_s, second_table.to_s].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_") + end +end +``` + +在默认的 `join_table` 规则中,两张表会按照字母顺序排序,最后通过 `_` 连接到一起,但是如果两张表有着完全相同的前缀,比如 music_artists 和 music_records 两张表,它们连接的结果就是 music_artists_records,公共的前缀会被删除,这种情况经常发生在包含命名空间的模型中,例如:`Music::Artist`。 + +当我们已经通过多对多关系的 Builder 创建了一个中间模型之后,就会建立两个 `Reflection` 对象: + +```ruby +habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self) +middle_reflection = builder.middle_reflection join_model +Builder::HasMany.define_callbacks self, middle_reflection +Reflection.add_reflection self, middle_reflection.name, middle_reflection +middle_reflection.parent_reflection = habtm_reflection +``` + +其中一个对象是 `HasAndBelongsToManyReflection` 实例,表示当前的多对多关系,另一个对象是 `#middle_reflection` 方法返回的 `HasMany`,表示当前的类与 `join_model` 之间有一个一对多关系,这个关系是隐式的,不过我们可以通过下面的代码来『理解』它: + +```ruby +class Post < ActiveRecord::Base + # has_and_belongs_to_many :posts + # = + has_many :posts_tag + # + + # ... +end +``` + +上述的代码构成了整个多对多关系的一部分,而另一部分由下面的代码来处理,当模型持有了一个跟中间模型相关的一对多关系之后,就会创建另一个以中间模型为桥梁 has_many 关系: + +```ruby +hm_options = {} +hm_options[:through] = middle_reflection.name +hm_options[:source] = join_model.right_reflection.name + +ActiveSupport::Deprecation.silence { has_many name, scope, hm_options, &extension } +``` + +这里还是使用 `Post` 和 `Tag` 这两个模型之间的关系举例子,通过上述代码,我们会在两个类中分别建立如下的关系: + +```ruby +class Post < ActiveRecord::Base + # has_many :posts_tag + has_many :tags, through: :posts_tag, source: :tag +end + +class Tag < ActiveRecord::Base + # has_many :tags_post + has_many :post, through: :tags_post, source: :post +end +``` + +通过两个隐式的 has_many 关系,两个显示的 has_many 就能够通过 `through` 和 `source` 间接找到自己对应的多个数据行,而从开发者的角度来看,整个工程中只使用了一行代码 `has_and_belongs_to_many :models`,其他的工作完全都是隐式的。 + +![many-to-many-associations](images/activerecord/many-to-many-associations.png) + +由于关系型数据库其实并没有物理上的多对多关系,只有在逻辑上才能实现多对多,所以对于每一个模型来说,它实现的都是一对多关系;只有从整体来看,通过 `PostsTags` 第三张表的引入,我们实现的才是从 `Post` 到 `Tag` 之间的多对多关系。 + +### 小结 + +ActiveRecord 对关系的支持其实非常全面,从最常见的一对一、一对多关系,再到多对多关系,都有着非常优雅、简洁的实现,虽然这一小节中没能全面的介绍所有关系的实现,但是对整个模块中重要类和整体架构的介绍已经非常具体了;不得不感叹 ActiveRecord 对多对多关系方法 `has_and_belongs_to_many` 的实现非常整洁,我们在分析其实现时也非常顺畅。 + +## Migrations 任务和执行过程 + +Migrations(迁移)是 ActiveRecord 提供的一种用于更改数据库 Schema 的方式,它提供了可以直接操作数据库的 DSL,这样我们就不需要自己去手写所有的 SQL 来更新数据库中的表结构了。 + +![activerecord-migrations](images/activerecord/activerecord-migrations.png) + +每一个 Migration 都具有一个唯一的时间戳,每次进行迁移时都会在现有的数据库中执行当前 Migration 文件的 DSL 更新数据库 Schema 得到新的数据库版本。而想要理解 Migrations 是如何工作的,就需要知道 `#create_table`、`#add_column` 等 DSL 是怎么实现的。 + +### Migration[5.1] + +我在使用 ActiveRecord 提供的数据库迁移的时候一直都特别好奇 `Migration[5.1]` 后面跟着的这个 `[5.1]` 是个什么工作原理,看了源代码之后我才知道: + +```ruby +class Migration + def self.[](version) + Compatibility.find(version) + end +end +``` + +`.[]` 是 `ActiveRecord::Migration` 的类方法,它通过执行 `Compatibility.find` 来判断当前的代码中使用的数据库迁移版本是否与 gem 中的版本兼容: + +```ruby +class Current < Migration +end +``` + +`compatibility.rb` 在兼容性方面做了很多事情,保证 ActiveRecord 中的迁移都是可以向前兼容的,在这里也就不准备介绍太多了。 + +### 从 rake db:migrate 开始 + +作者在阅读迁移部分的源代码时最开始以 `Migration` 类作为入口,结果发现这并不是一个好的选择,最终也没能找到定义 DSL 的位置,所以重新选择了 `rake db:migrate` 作为入口分析迁移的实现;通过对工程目录的分析,很快就能发现 ActiveRecord 中所有的 rake 命令都位于 `lib/railties/database.rake` 文件中,在文件中也能找到 `db:migrate` 对应的 rake 任务: + +```ruby +db_namespace = namespace :db do + desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." + task migrate: [:environment, :load_config] do + ActiveRecord::Tasks::DatabaseTasks.migrate + db_namespace["_dump"].invoke + end +end +``` + +上述代码中的 `DatabaseTasks` 类就包含在 `lib/active_record/tasks` 目录中的 `database_tasks.rb` 文件里: + +```ruby +lib/active_record/tasks/ +├── database_tasks.rb +├── mysql_database_tasks.rb +├── postgresql_database_tasks.rb +└── sqlite_database_tasks.rb +``` + +`#migrate` 方法就是 `DatabaseTasks` 的一个实例方法,同时 ActiveRecord 通过 `extend self` 将 `#migrate` 方法添加到了当前类的单类上,成为了当前类的类方法: + +```ruby +module Tasks + module DatabaseTasks + extend self + + def migrate + raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty? + + version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil + scope = ENV["SCOPE"] + Migrator.migrate(migrations_paths, version) do |migration| + scope.blank? || scope == migration.scope + end + ActiveRecord::Base.clear_cache! + end + end +end +``` + +#### 『迁移器』Migrator + +迁移任务中主要使用了 `Migrator.migrate` 方法,通过传入迁移文件的路径和期望的迁移版本对数据库进行迁移: + +```ruby +class Migrator#:nodoc: + class << self + def migrate(migrations_paths, target_version = nil, &block) + case + when target_version.nil? + up(migrations_paths, target_version, &block) + when current_version == 0 && target_version == 0 + [] + when current_version > target_version + down(migrations_paths, target_version, &block) + else + up(migrations_paths, target_version, &block) + end + end + end +end +``` + +在默认情况下,显然我们是不会传入目标的数据库版本的,也就是 `target_version.nil? == true`,这时会执行 `.up` 方法,对数据库向『上』迁移: + +```ruby +def up(migrations_paths, target_version = nil) + migrations = migrations(migrations_paths) + migrations.select! { |m| yield m } if block_given? + + new(:up, migrations, target_version).migrate +end +``` + +#### 方法调用栈 + +通过 `.new` 方法 ActiveRecord 初始化了一个新的 `Migrator` 实例,然后执行了 `Migrator#migrate`,在整个迁移执行的过程中,我们有以下的方法调用栈: + +![rake-db-migrate](images/activerecord/rake-db-migrate.png) + +在整个迁移过程的调用栈中,我们会关注以下的四个部分,首先是 `Migrator#migrate_without_lock` 方法: + +```ruby +def migrate_without_lock + if invalid_target? + raise UnknownMigrationVersionError.new(@target_version) + end + + result = runnable.each do |migration| + execute_migration_in_transaction(migration, @direction) + end + + record_environment + result +end +``` + +这个方法其实并没有那么重要,但是这里调用了 `Migrator#runnable` 方法,这个无参的方法返回了所有需要运行的 `Migration` 文件,`Migrator#runnable` 是如何选择需要迁移的文件是作者比较想要了解的,也是作者认为比较重要的地方: + +```ruby +def runnable + runnable = migrations[start..finish] + if up? + runnable.reject { |m| ran?(m) } + else + runnable.pop if target + runnable.find_all { |m| ran?(m) } + end +end + +def ran?(migration) + migrated.include?(migration.version.to_i) +end +``` + +通过对这个方法的阅读的分析,我们可以看到,如果迁移模式是 `:up`,那么就会选择所有未迁移的文件,也就是说在这时**迁移文件的选择与创建的顺序是无关的**。 + +#### 迁移的执行 + +当我们通过 `#runnable` 获得了整个待运行的迁移文件数组之后,就可以遍历所有的文件一次执行 `Migrator#execute_migrate_in_transaction` 方法了,在调用栈的最后会执行 `Migration#exec_migration`: + +```ruby +def exec_migration(conn, direction) + @connection = conn + if respond_to?(:change) + if direction == :down + revert { change } + else + change + end + else + send(direction) + end +ensure + @connection = nil +end +``` + +到这里就能与我们平时在 `Migration` 中实现的 `#change`、`#up` 和 `#down` 连到一起,逻辑也走通了;上述代码的逻辑还是很清晰的,如果当前的 `Migratoin` 实现了 `#change` 方法就会根据 `direction` 选择执行 `#change` 还是 `#revert + #change`,否则就会按照迁移的方向执行对应的方法。 + +### Migrations 的 DSL + +在数据迁移的模块执行的 Migration 文件中包含的都是 ActiveRecord 提供的 DSL 语法,这部分语法包含两部分,一部分是 Schema 相关的 DSL `schema_statements.rb`,其中包括表格的创建和删除以及一些用于辅助 Schema 创建的 `#column_exists?` 等方法,另一部分是表定义相关的 DSL `schema_definitions.rb`,其中包括处理表结构的 `TableDefinition` 类和抽象代表一张数据库中表的 `Table` 类。 + +#### 抽象适配器 + +在整个 `connection_adapters` 的子模块中,绝大多数模块在三大 SQL 数据库,MySQL、PostgreSQL 和 sqlite3 中都有着各自的实现: + +```ruby +lib/active_record/connection_adapters +├── abstract +│   ├── connection_pool.rb +│   ├── database_limits.rb +│   ├── database_statements.rb +│   ├── query_cache.rb +│   ├── quoting.rb +│   ├── savepoints.rb +│   ├── schema_creation.rb +│   ├── schema_definitions.rb +│   ├── schema_dumper.rb +│   ├── schema_statements.rb +│   └── transaction.rb +├── mysql +│   ├── column.rb +│   ├── database_statements.rb +│   ├── explain_pretty_printer.rb +│   ├── quoting.rb +│   ├── schema_creation.rb +│   ├── schema_definitions.rb +│   ├── schema_dumper.rb +│   ├── schema_statements.rb +│   └── type_metadata.rb +├── postgresql +│   └── ... +├── sqlite3 +│   └── ... +├── abstract_adapter.rb +├── ... +└── sqlite3_adapter.rb +``` + +不过这三个数据库的所有子模块都继承自 `AbstractAdapter` 下面对应的子模块,以获得一些三者共用的能力,包括数据库、Schema 的声明与管理等功能。 + +![abstract-adapter-and-much-more](images/activerecord/abstract-adapter-and-much-more.png) + +通过 `AbstractAdapter` 抽离出的公用功能,我们可以通过新的适配器随时适配其他的 SQL 数据库。 + +#### Schema DSL + +数据库的 Schema DSL 部分就包含我们经常使用的 `#create_table`、`#rename_table` 以及 `#add_column` 这些需要表名才能执行的方法,在这里以最常见的 `#create_table` 为例,简单分析一下这部分代码的实现: + +```ruby +def create_table(table_name, comment: nil, **options) + td = create_table_definition table_name, options[:temporary], options[:options], options[:as], comment: comment + + yield td if block_given? + + execute schema_creation.accept td +end +``` + +首先,在创建表时先通过 `#create_table_definition` 方法创建一个新的 `TableDefinition` 实例,然后将这个实例作为参数传入 block: + +```ruby +create_table :users do |t| +end +``` + +在 block 对这个 `TableDefinition` 对象一顿操作后,会通过 `SchemaCreation#accept` 方法获得一个用于在数据库中,能够创建表的 SQL 语句: + +```ruby +def accept(o) + m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}" + send m, o +end + +def visit_TableDefinition(o) + create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} " + + statements = o.columns.map { |c| accept c } + statements << accept(o.primary_keys) if o.primary_keys + + create_sql << "(#{statements.join(', ')})" if statements.present? + add_table_options!(create_sql, table_options(o)) + create_sql << " AS #{@conn.to_sql(o.as)}" if o.as + create_sql +end +``` + +`SchemaCreation` 类就是一个接受各种各样的 `TableDefinition`、`PrimaryKeyDefinition` 对象返回 SQL 的一个工具,可以将 `SchemaCreation` 理解为一个表结构的解释器;最后的 `#execute` 会在数据库中执行 SQL 改变数据库中的表结构。 + +在 `SchemaStatements` 中定义的其它方法的实现也都是大同小异,比如 `#drop_table` 其实都是删除数据库中的某张表: + +```ruby +def drop_table(table_name, options = {}) + execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}" +end +``` + +#### 表定义 DSL + +`SchemaStatements` 中定义的方法,参数大都包含 `table_name`,而另一个类 `TableDefinitions` 就包含了直接对表操作的 DSL: + +```ruby +create_table :foo do |t| + puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition" +end +``` + +当我们在 `#create_table` 中使用例如 `#string`、`#integer` 等方法时,所有的方法都会通过元编程的魔法最终执行 `TableDefinition#column` 改变表的定义: + +```ruby +module ColumnMethods + [ + :bigint, + # ... + :integer, + :string, + :text, + :time, + :timestamp, + :virtual, + ].each do |column_type| + module_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{column_type}(*args, **options) + args.each { |name| column(name, :#{column_type}, options) } + end + CODE + end + alias_method :numeric, :decimal +end +``` + +`#column` 方法非常神奇,它从各处收集有关当前表的定义,最终为表中的每一个字段创建一个 `ColumnDefinition` 实例,并存储到自己持有的 `@columns_hash` 中: + +```ruby +def column(name, type, options = {}) + name = name.to_s + type = type.to_sym if type + options = options.dup + + index_options = options.delete(:index) + index(name, index_options.is_a?(Hash) ? index_options : {}) if index_options + @columns_hash[name] = new_column_definition(name, type, options) + self +end + +def new_column_definition(name, type, **options) + type = aliased_types(type.to_s, type) + options[:primary_key] ||= type == :primary_key + options[:null] = false if options[:primary_key] + create_column_definition(name, type, options) +end + +def create_column_definition(name, type, options) + ColumnDefinition.new(name, type, options) +end +``` + +除了 `ColumnDefinition` 之外,在 ActiveRecord 中还存在 `PrimaryKeyDefinition`、`IndexDefinition` 等等类和结构体用于表示数据库中的某一种元素。 + +表结构在最后会被 `SchemaCreation` 类的 `#accept` 方法展开,最后在数据库中执行。 + +### 小结 + +到这里整个 Migrations 部分的实现就已经阅读分析完了,整个『模块』包含两个部分,一部分是 rake 任务执行 DSL 代码的过程,另一部分是 DSL 的实现,两部分的结合最终构成了整个 Migrations 模块的全部内容。 + +ActiveRecord 对于 Migration 迁移机制的设计确实很好的解决数据库中的表结构不断变更的问题,同时因为所有的 Migration 文件都在版本控制中管理,我们也能够随时还原数据库中的表结构。 + +## 总结 + +文章对 ActiveRecord 中涉及的很多问题都进行了分析和介绍,包括模型的创建、查询以及关系,还包括数据库表迁移的实现,本来想将文中的几个部分分开进行介绍,但是写着写着就懒得分开了,如果对文章的内容有疑问,请在博客下面的 Disqus 评论系统中留言,需要翻墙。 + +> 原文链接:[理解 ActiveRecord](https://draveness.me/activerecord.html) +> +> Follow: [Draveness · GitHub](https://github.com/Draveness) + + diff --git a/contents/Rails/images/activerecord/15079789320881.jpg b/contents/Rails/images/activerecord/15079789320881.jpg new file mode 100644 index 0000000..7257a5e Binary files /dev/null and b/contents/Rails/images/activerecord/15079789320881.jpg differ diff --git a/contents/Rails/images/activerecord/15079789363342.jpg b/contents/Rails/images/activerecord/15079789363342.jpg new file mode 100644 index 0000000..7257a5e Binary files /dev/null and b/contents/Rails/images/activerecord/15079789363342.jpg differ diff --git a/contents/Rails/images/activerecord/abstract-adapter-and-much-more.png b/contents/Rails/images/activerecord/abstract-adapter-and-much-more.png new file mode 100644 index 0000000..f8535c8 Binary files /dev/null and b/contents/Rails/images/activerecord/abstract-adapter-and-much-more.png differ diff --git a/contents/Rails/images/activerecord/active-record-relation-delegation.png b/contents/Rails/images/activerecord/active-record-relation-delegation.png new file mode 100644 index 0000000..724fac3 Binary files /dev/null and b/contents/Rails/images/activerecord/active-record-relation-delegation.png differ diff --git a/contents/Rails/images/activerecord/activemodel-validators.png b/contents/Rails/images/activerecord/activemodel-validators.png new file mode 100644 index 0000000..d69b1af Binary files /dev/null and b/contents/Rails/images/activerecord/activemodel-validators.png differ diff --git a/contents/Rails/images/activerecord/activerecord-ancestor-builders.png b/contents/Rails/images/activerecord/activerecord-ancestor-builders.png new file mode 100644 index 0000000..bd2f166 Binary files /dev/null and b/contents/Rails/images/activerecord/activerecord-ancestor-builders.png differ diff --git a/contents/Rails/images/activerecord/activerecord-architecture.png b/contents/Rails/images/activerecord/activerecord-architecture.png new file mode 100644 index 0000000..ea1a0d3 Binary files /dev/null and b/contents/Rails/images/activerecord/activerecord-architecture.png differ diff --git a/contents/Rails/images/activerecord/activerecord-associations.png b/contents/Rails/images/activerecord/activerecord-associations.png new file mode 100644 index 0000000..b203cb3 Binary files /dev/null and b/contents/Rails/images/activerecord/activerecord-associations.png differ diff --git a/contents/Rails/images/activerecord/activerecord-base-save.png b/contents/Rails/images/activerecord/activerecord-base-save.png new file mode 100644 index 0000000..a887e8f Binary files /dev/null and b/contents/Rails/images/activerecord/activerecord-base-save.png differ diff --git a/contents/Rails/images/activerecord/activerecord-hasmany-ancestors.png b/contents/Rails/images/activerecord/activerecord-hasmany-ancestors.png new file mode 100644 index 0000000..63438c7 Binary files /dev/null and b/contents/Rails/images/activerecord/activerecord-hasmany-ancestors.png differ diff --git a/contents/Rails/images/activerecord/activerecord-migrations.png b/contents/Rails/images/activerecord/activerecord-migrations.png new file mode 100644 index 0000000..a72dd3c Binary files /dev/null and b/contents/Rails/images/activerecord/activerecord-migrations.png differ diff --git a/contents/Rails/images/activerecord/activerecord-reflections.png b/contents/Rails/images/activerecord/activerecord-reflections.png new file mode 100644 index 0000000..5bfdfbd Binary files /dev/null and b/contents/Rails/images/activerecord/activerecord-reflections.png differ diff --git a/contents/Rails/images/activerecord/activerecord-relation-value-methods.png b/contents/Rails/images/activerecord/activerecord-relation-value-methods.png new file mode 100644 index 0000000..a254b85 Binary files /dev/null and b/contents/Rails/images/activerecord/activerecord-relation-value-methods.png differ diff --git a/contents/Rails/images/activerecord/actual-callstack-for-activerecord-base-save.png b/contents/Rails/images/activerecord/actual-callstack-for-activerecord-base-save.png new file mode 100644 index 0000000..140e177 Binary files /dev/null and b/contents/Rails/images/activerecord/actual-callstack-for-activerecord-base-save.png differ diff --git a/contents/Rails/images/activerecord/callstack-for-belongs-to-association-getter.png b/contents/Rails/images/activerecord/callstack-for-belongs-to-association-getter.png new file mode 100644 index 0000000..e4d06fe Binary files /dev/null and b/contents/Rails/images/activerecord/callstack-for-belongs-to-association-getter.png differ diff --git a/contents/Rails/images/activerecord/callstack-for-belongs-to-association-setter.png b/contents/Rails/images/activerecord/callstack-for-belongs-to-association-setter.png new file mode 100644 index 0000000..88010b3 Binary files /dev/null and b/contents/Rails/images/activerecord/callstack-for-belongs-to-association-setter.png differ diff --git a/contents/Rails/images/activerecord/database-statement-insert.png b/contents/Rails/images/activerecord/database-statement-insert.png new file mode 100644 index 0000000..1da4a7f Binary files /dev/null and b/contents/Rails/images/activerecord/database-statement-insert.png differ diff --git a/contents/Rails/images/activerecord/habtm-association-table-name.png b/contents/Rails/images/activerecord/habtm-association-table-name.png new file mode 100644 index 0000000..e87b4c3 Binary files /dev/null and b/contents/Rails/images/activerecord/habtm-association-table-name.png differ diff --git a/contents/Rails/images/activerecord/many-to-many-associations.png b/contents/Rails/images/activerecord/many-to-many-associations.png new file mode 100644 index 0000000..8f32cb6 Binary files /dev/null and b/contents/Rails/images/activerecord/many-to-many-associations.png differ diff --git a/contents/Rails/images/activerecord/one-to-many-association.png b/contents/Rails/images/activerecord/one-to-many-association.png new file mode 100644 index 0000000..125a88c Binary files /dev/null and b/contents/Rails/images/activerecord/one-to-many-association.png differ diff --git a/contents/Rails/images/activerecord/rake-db-migrate.png b/contents/Rails/images/activerecord/rake-db-migrate.png new file mode 100644 index 0000000..550857b Binary files /dev/null and b/contents/Rails/images/activerecord/rake-db-migrate.png differ diff --git a/contents/ReactiveObjC/RACChannel.md b/contents/ReactiveObjC/RACChannel.md new file mode 100644 index 0000000..69834f9 --- /dev/null +++ b/contents/ReactiveObjC/RACChannel.md @@ -0,0 +1,280 @@ +# RAC 中的双向数据绑定 RACChannel + +之前讲过了 ReactiveCocoa 中的一对一的单向数据流 `RACSignal` 和一对多的单向数据流 `RACMulticastConnection`,这一篇文章分析的是一对一的双向数据流 `RACChannel`。 + +![What-is-RACChanne](images/RACChannel/What-is-RACChannel.png) + +`RACChannel` 其实是一个相对比较复杂的类,但是,对其有一定了解之后合理运用的话,会在合适的业务中提供非常强大的支持能够极大的简化业务代码。 + +## RACChannel 简介 + +`RACChannel` 可以被理解为一个双向的连接,这个连接的两端都是 `RACSignal` 实例,它们可以向彼此发送消息,如果我们在视图和模型之间通过 `RACChannel` 建立这样的连接: + +![Connection-Between-View-Mode](images/RACChannel/Connection-Between-View-Model.png) + +那么从模型发出的消息,最后会发送到视图上;反之,用户对视图进行的操作最后也会体现在模型上。这种通信方式的实现是基于信号的,`RACChannel` 内部封装了两个 `RACChannelTerminal` 对象,它们都是 `RACSignal` 的子类: + +![RACChannel-Interface](images/RACChannel/RACChannel-Interface.png) + +对模型进行的操作最后都会发送给 `leadingTerminal` 再通过内部的实现发送给 `followingTerminal`,由于视图是 `followingTerminal` 的订阅者,所以消息最终会发送到视图上。 + +![Messages-Send-From-Mode](images/RACChannel/Messages-Send-From-Model.png) + +在上述情况下,`leadingTerminal` 的订阅者(模型)并不会收到消息,它的订阅者(视图)只会在 `followingTerminal` 收到消息时才会接受到新的值。 + +同时,`RACChannel` 的绑定都是双向的,视图收到用户的动作,例如点击等事件时,会将消息发送给 `followingTerminal`,而 `followingTerminal` 并**不会**将消息发送给自己的订阅者(视图),而是会发送给 `leadingTerminal`,并通过 `leadingTerminal` 发送给其订阅者,即模型。 + +![Terminals-Between-View-Mode](images/RACChannel/Terminals-Between-View-Model.png) + +上图描述了信息在 `RACChannel` 之间的传递过程,无论是模型属性的改变还是用户对视图进行的操作都会通过这两个 `RACChannelTerminal` 传递到另一端;同时,由于消息不会发送给自己的订阅者,所以不会造成信息的循环发送。 + +## RACChannel 和 RACChannelTerminal + +`RACChannel` 和 `RACChannelTerminal` 的关系非常密切,前者可以理解为一个网络连接,后者可以理解为 `socket`,表示网络连接的一端,下图描述了 `RACChannel` 与网络连接中概念的一一对应关系。 + +![Channel-And-Network-Connection](images/RACChannel/Channel-And-Network-Connection.png) + ++ 在客户端使用 `write` 向 `socket` 中发送消息时,`socket` 的持有者客户端不会收到消息,只有在 `socket` 上调用 `read` 的服务端才会收到消息;反之亦然。 ++ 在模型使用 `sendNext` 向`leadingTerminal` 中发送消息时,`leadingTerminal` 的订阅者模型不会收到消息,只有在 `followingTerminal` 上调用 `subscribe` 的视图才会收到消息;反之亦然。 + +### RACChannelTerminal 的实现 + +为什么向 `RACChannelTerminal` 发送消息,它的订阅者获取不到?先来看一下它在头文件中的定义: + +```objectivec +@interface RACChannelTerminal : RACSignal <RACSubscriber> +@end +``` + +`RACChannelTerminal` 是一个信号的子类,同时它还遵循了 `RACSubscriber` 协议,也就是可以向它调用 `-sendNext:` 等方法;`RAChannelTerminal` 中持有了两个对象: + +![RACChannelTerminal-Interface](images/RACChannel/RACChannelTerminal-Interface.png) + +在初始化时,需要传入 `values` 和 `otherTerminal` 这两个属性,其中 `values` 表示当前断点,`otherTerminal` 表示远程端点: + +```objectivec +- (instancetype)initWithValues:(RACSignal *)values otherTerminal:(id<RACSubscriber>)otherTerminal { + self = [super init]; + _values = values; + _otherTerminal = otherTerminal; + return self; +} +``` + +当然,作为 `RACSignal` 的子类,`RACChannelTerminal` 必须覆写 `-subscribe:` 方法: + +```objectivec +- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber { + return [self.values subscribe:subscriber]; +} +``` + +在订阅者调用 `-subscribeNext:` 等方法发起订阅时,实际上订阅的是当前端点;如果向当前端点发送消息,会被转发到远程端点上,而这也就是当前端点的订阅者不会接收到向当前端点发送消息的原因: + +```objectivec +- (void)sendNext:(id)value { + [self.otherTerminal sendNext:value]; +} +- (void)sendError:(NSError *)error { + [self.otherTerminal sendError:error]; +} +- (void)sendCompleted { + [self.otherTerminal sendCompleted]; +} +``` + +### RACChannel 的初始化 + +我们在任何情况下都不应该直接使用 `-init` 方法初始化 `RACChannelTerminal` 的实例,而是应该以创建 `RACChannel` 的方式生成它: + +```objectivec +- (instancetype)init { + self = [super init]; + + RACReplaySubject *leadingSubject = [RACReplaySubject replaySubjectWithCapacity:0]; + RACReplaySubject *followingSubject = [RACReplaySubject replaySubjectWithCapacity:1]; + + [[leadingSubject ignoreValues] subscribe:followingSubject]; + [[followingSubject ignoreValues] subscribe:leadingSubject]; + + _leadingTerminal = [[RACChannelTerminal alloc] initWithValues:leadingSubject otherTerminal:followingSubject]; + _followingTerminal = [[RACChannelTerminal alloc] initWithValues:followingSubject otherTerminal:leadingSubject]; + + return self; +} +``` + +两个 `RACChannelTerminal` 中包装的其实是两个 `RACSubject` 热信号,它们既可以作为订阅者,也可以接收其他对象发送的消息;我们并不希望 `leadingSubject` 有任何的初始值,但是我们需要 `error` 和 `completed` 信息可以被重播。 + +![Sending-Errors-And-Completed-Messages](images/RACChannel/Sending-Errors-And-Completed-Messages.png) + +通过 `-ignoreValues` 和 `-subscribe:` 方法,`leadingSubject` 和 `followingSubject` 两个热信号中产生的错误会互相发送,这是为了防止连接的两端一边发生了错误,另一边还继续工作的情况的出现。 + +在初始化方法的最后,生成两个 `RACChannelTerminal` 实例的过程就不多说了。 + +## RACChannel 与 UIKit 组件 + +如果在整个 ReactiveCocoa 工程中搜索 `RACChannel`,你会发现以下的 UIKit 组件都与 `RACChannel` 有着非常密切的关系: + +![RACChannel-Hierachy](images/RACChannel/RACChannel-Hierachy.png) + +UIKit 中的这些组件都提供了使用 `RACChannel` 的接口,用以降低数据双向绑定的复杂度,我们以 `UITextField` 为例,它在分类的接口中提供了 `rac_newTextChannel` 方法: + +```objectivec +- (RACChannelTerminal *)rac_newTextChannel { + return [self rac_channelForControlEvents:UIControlEventAllEditingEvents key:@keypath(self.text) nilValue:@""]; +} +``` + +上述方法用于返回一个一端绑定 `UIControlEventAllEditingEvents` 事件的 `RACChannelTerminal` 对象。 + +![UITextField-RACChannel-Interface](images/RACChannel/UITextField-RACChannel-Interface.png) + +`UIControlEventAllEditingEvents` 事件发生时,它会将自己的 `text` 属性作为信号发送到 `followingTerminal -> leadingTerminal` 管道中,最后发送给 `leadingTerminal` 的订阅者。 + +在 `rac_newTextChannel` 中调用的方法 `-rac_channelForControlEvents:key:nilValue:` 是一个 `UIControl` 的私有方法: + +```objectivec +- (RACChannelTerminal *)rac_channelForControlEvents:(UIControlEvents)controlEvents key:(NSString *)key nilValue:(id)nilValue { + key = [key copy]; + RACChannel *channel = [[RACChannel alloc] init]; + + RACSignal *eventSignal = [[[self + rac_signalForControlEvents:controlEvents] + mapReplace:key] + takeUntil:[[channel.followingTerminal + ignoreValues] + catchTo:RACSignal.empty]]; + [[self + rac_liftSelector:@selector(valueForKey:) withSignals:eventSignal, nil] + subscribe:channel.followingTerminal]; + + RACSignal *valuesSignal = [channel.followingTerminal + map:^(id value) { + return value ?: nilValue; + }]; + [self rac_liftSelector:@selector(setValue:forKey:) withSignals:valuesSignal, [RACSignal return:key], nil]; + + return channel.leadingTerminal; +} +``` + +这个方法为所有的 `UIControl` 子类,包括 `UITextField`、`UISegmentedControl` 等等,它的主要作用就是当传入的 `controlEvents` 事件发生时,将 UIKit 组件的属性 `key` 发送到返回的 `RACChannelTerminal` 实例中;同时,在向返回的 `RACChannelTerminal` 实例中发送消息时,也会自动更新 UIKit 组件的属性。 + +上面的代码在初始化 `RACChannel` 之后做了两件事情,首先是在 `UIControlEventAllEditingEvents` 事件发生时,将 `text` 属性发送到 `followingTerminal` 中: + +```objectivec +RACSignal *eventSignal = [[[self + rac_signalForControlEvents:controlEvents] + mapReplace:key] + takeUntil:[[channel.followingTerminal + ignoreValues] + catchTo:RACSignal.empty]]; +[[self + rac_liftSelector:@selector(valueForKey:) withSignals:eventSignal, nil] + subscribe:channel.followingTerminal]; +``` + +第二个是在 `followingTerminal` 接收到来自 `leadingTerminal` 的消息时,更新 `UITextField` 的 `text` 属性。 + +```objectivec +RACSignal *valuesSignal = [channel.followingTerminal + map:^(id value) { + return value ?: nilValue; + }]; +[self rac_liftSelector:@selector(setValue:forKey:) withSignals:valuesSignal, [RACSignal return:key], nil]; +``` + +这两件事情都是通过 `-rac_liftSelector:withSignals:` 方法来完成的,不过,我们不会在这篇文章中介绍这个方法。 + +## RACChannel 与 KVO + +`RACChannel` 不仅为 UIKit 组件提供了接口,还为键值观测提供了 `RACKVOChannel` 来高效地完成双向绑定;`RACKVOChannel` 是 `RACChannel` 的子类: + +![RACKVOChanne](images/RACChannel/RACKVOChannel.png) + +在 `RACKVOChannel` 提供的接口中,我们一般都会使用 `RACChannelTo` 来观测某一个对象的对应属性,三个参数依次为对象、属性和默认值: + +```objectivec +RACChannelTerminal *integerChannel = RACChannelTo(self, integerProperty, @42); +``` + +而 `RACChannelTo` 是 `RACKVOChannel` 头文件中的一个宏,上面的表达式可以展开成为: + +```objectivec +RACChannelTerminal *integerChannel = [[RACKVOChannel alloc] initWithTarget:self keyPath:@"integerProperty" nilValue:@42][@"followingTerminal"]; +``` + +该宏初始化了一个 `RACKVOChannel` 对象,并通过方括号的方式获取其中的 `followingTerminal`,这种获取类属性的方式是通过覆写以下的两个方法实现的: + +```objectivec +- (RACChannelTerminal *)objectForKeyedSubscript:(NSString *)key { + RACChannelTerminal *terminal = [self valueForKey:key]; + return terminal; +} + +- (void)setObject:(RACChannelTerminal *)otherTerminal forKeyedSubscript:(NSString *)key { + RACChannelTerminal *selfTerminal = [self objectForKeyedSubscript:key]; + [otherTerminal subscribe:selfTerminal]; + [[selfTerminal skip:1] subscribe:otherTerminal]; +} +``` + +又由于覆写了这两个方法,在 `-setObject:forKeyedSubscript:` 时会自动调用 `-subscribe:` 方法完成双向绑定,所以我们可以使用 `=` 来对两个 `RACKVOChannel` 进行双向绑定: + +```objectivec +RACChannelTo(view, property) = RACChannelTo(model, property); + +[[RACKVOChannel alloc] initWithTarget:view keyPath:@"property" nilValue:nil][@"followingTerminal"] = [[RACKVOChannel alloc] initWithTarget:model keyPath:@"property" nilValue:nil][@"followingTerminal"]; +``` + +以上的两种方式是完全等价的,它们都会在对方的属性更新时更新自己的属性。 + +![RACChannelTo-Model-Vie](images/RACChannel/RACChannelTo-Model-View.png) + +实现的方式其实与 `RACChannel` 差不多,这里不会深入到代码中进行介绍,与 `RACChannel` 的区别是,`RACKVOChannel` 并没有暴露出 `leadingTerminal` 而是 `followingTerminal`: + +![RACChannelTo-And-Property](images/RACChannel/RACChannelTo-And-Property.png) + +## RACChannel 实战 + +这一小节通过一个简单的例子来解释如何使用 `RACChannel` 进行双向数据绑定。 + +![TextField-With-Channe](images/RACChannel/TextField-With-Channel.gif) + +在整个视图上有两个 `UITextField`,我们想让这两个 `UITextField` `text` 的值相互绑定,在一个 `UITextField` 编辑时也改变另一个 `UITextField` 中的内容: + +```objectivec +@property (weak, nonatomic) IBOutlet UITextField *textField; +@property (weak, nonatomic) IBOutlet UITextField *anotherTextField; +``` + +实现的过程非常简单,分别获取两个 `UITextField` 的 `rac_newTextChannel` 属性,并让它们订阅彼此的内容: + +```objectivec +[self.textField.rac_newTextChannel subscribe:self.anotherTextField.rac_newTextChannel]; +[self.anotherTextField.rac_newTextChannel subscribe:self.textField.rac_newTextChannel]; +``` + +这样在使用两个文本输入框时就能达到预期的效果了,这是一个非常简单的例子,可以得到如下的结构图。 + +![Two-UITextField-With-RACChanne](images/RACChannel/Two-UITextField-With-RACChannel.png) + +两个 `UITextField` 通过 `RACChannel` 互相影响,在对方属性更新时同时更新自己的属性。 + +## 总结 + +`RACChannel` 非常适合于视图和模型之间的双向绑定,在对方的属性或者状态更新时及时通知自己,达到预期的效果;我们可以使用 ReactiveCocoa 中内置的很多与 `RACChannel` 有关的方法,来获取开箱即用的 `RACChannelTerminal`,当然也可以使用 `RACChannelTo` 通过 `RACKVOChannel` 来快速绑定类与类的属性。 + +## References + ++ [Bi-directional Data Bindings in ReactiveCocoa with RACChannel](https://spin.atomicobject.com/2015/05/04/bi-directional-data-bindings-reactivecocoa/) ++ [ReactiveCocoa 核心元素与信号流](http://tech.meituan.com/ReactiveCocoaSignalFlow.html) + +> Github Repo:[iOS-Source-Code-Analyze](https://github.com/draveness/iOS-Source-Code-Analyze) +> +> Follow: [Draveness · GitHub](https://github.com/Draveness) +> +> Source: http://draveness.me/racchannel + + diff --git a/contents/ReactiveObjC/RACCommand.md b/contents/ReactiveObjC/RACCommand.md new file mode 100644 index 0000000..72e3673 --- /dev/null +++ b/contents/ReactiveObjC/RACCommand.md @@ -0,0 +1,427 @@ +# 优雅的 RACCommand + +![raccommad-cove](images/RACCommand/raccommad-cover.jpg) + +`RACCommand` 是一个在 ReactiveCocoa 中比较复杂的类,大多数使用 ReactiveCocoa 的人,尤其是初学者并不会经常使用它。 + +在很多情况下,虽然使用 `RACSignal` 和 `RACSubject` 就能解决绝大部分问题,但是 `RACCommand` 的使用会为我们带来巨大的便利,尤其是在与副作用相关的操作中。 + +![What-is-RACCommand](images/RACCommand/What-is-RACCommand.png) + +> 文章中不会讨论 `RACCommand` 中的并行执行问题,也就是忽略了 `allowsConcurrentExecution` 以及 `allowsConcurrentExecutionSubject` 的存在,不过它们确实在 `RACCommand` 中非常重要,这里只是为了减少不必要的干扰因素。 + +## RACCommand 简介 + +与前面几篇文章中介绍的 `RACSignal` 等元素不同,`RACCommand` 并不表示数据流,它只是一个继承自 `NSObject` 的类,但是它却可以用来创建和订阅用于响应某些事件的信号。 + +```objectivec +@interface RACCommand<__contravariant InputType, __covariant ValueType> : NSObject + +@end +``` + +它本身并不是一个 `RACStream` 或者 `RACSignal` 的子类,而是一个用于管理 `RACSignal` 的创建与订阅的类。 + +在 ReactiveCocoa 中的 FrameworkOverview 部分对 `RACCommand` 有这样的解释: + +> A command, represented by the RACCommand class, creates and subscribes to a signal in response to some action. This makes it easy to perform side-effecting work as the user interacts with the app. + +在用于与 UIKit 组件进行交互或者执行包含副作用的操作时,`RACCommand` 能够帮助我们更快的处理并且响应任务,减少编码以及工程的复杂度。 + +## RACCommand 的初始化与执行 + +在 `-initWithSignalBlock:` 方法的方法签名上,你可以看到在每次 `RACCommand` 初始化时都会传入一个类型为 `RACSignal<ValueType> * (^)(InputType _Nullable input)` 的 `signalBlock`: + +```objectivec +- (instancetype)initWithSignalBlock:(RACSignal<ValueType> * (^)(InputType _Nullable input))signalBlock; +``` + +输入为 `InputType` 返回值为 `RACSignal<ValueType> *`,而 `InputType` 也就是在调用 `-execute:` 方法时传入的对象: + +```objectivec +- (RACSignal<ValueType> *)execute:(nullable InputType)input; +``` + +这也就是 `RACCommand` 将外部变量(或『副作用』)传入 ReactiveCocoa 内部的方法,你可以理解为 `RACCommand` 将外部的变量 `InputType` 转换成了使用 `RACSignal` 包裹的 `ValueType` 对象。 + +![Execute-For-RACCommand](images/RACCommand/Execute-For-RACCommand.png) + +我们以下面的代码为例,先来看一下 `RACCommand` 是如何工作的: + +```objectivec +RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(NSNumber * _Nullable input) { + return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { + NSInteger integer = [input integerValue]; + for (NSInteger i = 0; i < integer; i++) { + [subscriber sendNext:@(i)]; + } + [subscriber sendCompleted]; + return nil; + }]; +}]; +[[command.executionSignals switchToLatest] subscribeNext:^(id _Nullable x) { + NSLog(@"%@", x); +}]; + +[command execute:@1]; +[RACScheduler.mainThreadScheduler afterDelay:0.1 + schedule:^{ + [command execute:@2]; + }]; +[RACScheduler.mainThreadScheduler afterDelay:0.2 + schedule:^{ + [command execute:@3]; + }]; +``` + +首先使用 `-initWithSignalBlock:` 方法创建一个 `RACCommand` 的对象,传入一个类型为 `InputType -> RACSignal<ValueType>` 的 block,这个信号根据输入会发送对应次数的消息,如果运行上面的代码,会打印出: + +```objectivec +0 +0 +1 +0 +1 +2 +``` + +> `-switchToLatest` 方法只能操作**信号的信号**。 + +每次 `executionSignals` 中发送了新的信号时,`switchToLatest` 方法返回的信号都会订阅这个最新的信号,这里也就保证了每次都会打印出最新的信号中的值。 + +![Multiple-Executes](images/RACCommand/Multiple-Executes.png) + +在上面代码中还有最后一个问题需要回答,为什么要使用 `RACScheduler.mainThreadScheduler` 延迟调用之后的 `-execute:` 方法?由于在默认情况下 `RACCommand` 都是不支持并发操作的,需要在上一次命令执行之后才可以发送下一次操作,否则就会返回错误信号 `RACErrorSignal`,这些错误可以通过订阅 `command.errors` 获得。 + +如果使用如下的方式执行几次 `-execute:` 方法: + +```objectivec +[command execute:@1]; +[command execute:@2]; +[command execute:@3]; +``` + +笔者相信,不出意外的话,你只能在控制台中看到输出 `0`。 + +### 最重要的内部『信号』 + +`RACCommand` 中最重要的内部『信号』就是 `addedExecutionSignalsSubject`: + +```objectivec +@property (nonatomic, strong, readonly) RACSubject *addedExecutionSignalsSubject; +``` + +这个 `RACSubject` 对象通过各种操作衍生了几乎所有 `RACCommand` 中的其他信号,我们会在下一节中具体介绍; + +既然 `addedExecutionSignalsSubject` 是一个 `RACSubject`,它不能在创建时预设好对订阅者发送的消息,它会在哪里接受数据并推送给订阅者呢?答案就在 `-execute:` 方法中: + +```objectivec +- (RACSignal *)execute:(id)input { + BOOL enabled = [[self.immediateEnabled first] boolValue]; + if (!enabled) { + NSError *error = [NSError errorWithDomain:RACCommandErrorDomain code:RACCommandErrorNotEnabled userInfo:@{ + NSLocalizedDescriptionKey: NSLocalizedString(@"The command is disabled and cannot be executed", nil), + RACUnderlyingCommandErrorKey: self + }]; + + return [RACSignal error:error]; + } + + RACSignal *signal = self.signalBlock(input); + RACMulticastConnection *connection = [[signal + subscribeOn:RACScheduler.mainThreadScheduler] + multicast:[RACReplaySubject subject]]; + + [self.addedExecutionSignalsSubject sendNext:connection.signal]; + + [connection connect]; + return [connection.signal setNameWithFormat:@"%@ -execute: %@", self, RACDescription(input)]; +} +``` + +在方法中这里你也能看到连续几次执行 `-execute:` 方法不能成功的原因:每次执行这个方法时,都会从另一个信号 `immediateEnabled` 中读取是否能执行当前命令的 `BOOL` 值,如果不可以执行的话,就直接返回 `RACErrorSignal`。 + +![Execute-on-RACCommand](images/RACCommand/Execute-on-RACCommand.png) + +> `-execute:` 方法是唯一一个为 `addedExecutionSignalsSubject` 生产信息的方法。 + +在执行 `signalBlock` 返回一个 `RACSignal` 之后,会将当前信号包装成一个 `RACMulticastConnection`,然后调用 `-sendNext:` 方法发送到 `addedExecutionSignalsSubject` 上,执行 `-connect` 方法订阅原有的信号,最后返回。 + +### 复杂的初始化 + +与简单的 `-execute:` 方法相比,`RACCommand` 的初始化方法就复杂多了,虽然我们在方法中传入了 `signalBlock`,但是 `-initWithEnabled:signalBlock:` 方法只是对这个 block 进行了简单的 `copy`,真正使用这个 block 的还是上一节中的 `-execute:` 方法中。 + +由于 `RACCommand` 在初始化方法中初始化了七个高阶信号,它的实现非常复杂: + +```objectivec +- (instancetype)initWithEnabled:(RACSignal *)enabledSignal signalBlock:(RACSignal<id> * (^)(id input))signalBlock { + self = [super init]; + + _addedExecutionSignalsSubject = [RACSubject new]; + _signalBlock = [signalBlock copy]; + + _executionSignals = ...; + _errors = ...; + RACSignal *immediateExecuting = ...; + _executing = ...; + RACSignal *moreExecutionsAllowed = ...; + _immediateEnabled =...; + _enabled = ...; + + return self; +} +``` + +这一小节并不能完全介绍全部的七个信号的实现,只会介绍其中的 `immediateExecuting` 和 `moreExecutionsAllowed` 两个临时信号,剩下的信号都会在下一节中分析。 + +#### 表示当前有操作执行的信号 + +首先是 `immediateExecuting` 信号: + +```objectivec +RACSignal *immediateExecuting = [[[[self.addedExecutionSignalsSubject + flattenMap:^(RACSignal *signal) { + return [[[signal + catchTo:[RACSignal empty]] + then:^{ + return [RACSignal return:@-1]; + }] + startWith:@1]; + }] + scanWithStart:@0 reduce:^(NSNumber *running, NSNumber *next) { + return @(running.integerValue + next.integerValue); + }] + map:^(NSNumber *count) { + return @(count.integerValue > 0); + }] + startWith:@NO]; +``` + +`immediateExecuting` 是一个用于表示当前是否有任务执行的信号,如果输入的 `addedExecutionSignalsSubject` 等价于以下的信号: + +```objectivec +[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { + [subscriber sendNext:[RACSignal error:[NSError errorWithDomain:@"Error" code:1 userInfo:nil]]]; + [subscriber sendNext:[RACSignal return:@1]]; + [subscriber sendNext:[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { + [RACScheduler.mainThreadScheduler afterDelay:1 + schedule:^ + { + [subscriber sendCompleted]; + }]; + return nil; + }]]; + [subscriber sendNext:[RACSignal return:@3]]; + [subscriber sendCompleted]; + return nil; +}]; +``` + +> 在本文的所有章节中都会假设输入的 `addedExecutionSignalsSubject` 信号跟上面的代码返回的完全相同。 + +那么,最后生成的高阶信号 `immediateExecuting` 如下: + +![immediateExecuting-Signal-in-RACCommand](images/RACCommand/immediateExecuting-Signal-in-RACCommand.png) + +1. `-catchTo:` 将所有的错误转换成 `RACEmptySignal` 信号; +2. `-flattenMap:` 将每一个信号的开始和结束的时间点转换成 `1` 和 `-1` 两个信号; +3. `-scanWithStart:reduce:` 从 `0` 开始累加原有的信号; +4. `-map:` 将大于 `1` 的信号转换为 `@YES`; +5. `-startWith:` 在信号序列最前面加入 `@NO`,表示在最开始时,没有任何动作在执行。 + +`immediateExecuting` 使用几个 `RACSignal` 的操作成功将原有的信号流转换成了表示是否有操作执行的信号流。 + +#### 表示是否允许更多操作执行的信号 + +相比于 `immediateExecuting` 信号的复杂,`moreExecutionsAllowed` 就简单多了: + +```objectivec +RACSignal *moreExecutionsAllowed = [RACSignal + if:[self.allowsConcurrentExecutionSubject startWith:@NO] + then:[RACSignal return:@YES] + else:[immediateExecuting not]]; +``` + +因为文章中不准备介绍与并发执行有关的内容,所以这里的 `then` 语句永远不会执行,既然 `RACCommand` 不支持并行操作,那么这段代码就非常好理解了,当前 `RACCommand` 能否执行操作就是 `immediateExecuting` 取反: + +![MoreExecutionAllowed-Signa](images/RACCommand/MoreExecutionAllowed-Signal.png) + +到这里所有初始化方法中的临时信号就介绍完了,在下一节中会继续介绍初始化方法中的其它高阶信号。 + +## RACCommand 接口中的高阶信号 + +每一个 `RACCommand` 对象中都管理着多个信号,它在接口中暴露出的四个信号是这一节关注的重点: + +![RACCommand-Interface](images/RACCommand/RACCommand-Interface.png) + +这一小节会按照顺序图中从上到下的顺序介绍 `RACCommand` 接口中暴露出来的信号,同时会涉及一些为了生成这些信号的中间产物。 + +### executionSignals + +`executionSignals` 是 `RACCommand` 中最重要的信号;从类型来看,它是一个**包含信号的信号**,在每次执行 `-execute:` 方法时,最终都会向 `executionSignals` 中传入一个最新的信号。 + +虽然它最重要,但是`executionSignals` 是这个几个高阶信号中实现最简单的: + +```objectivec +_executionSignals = [[[self.addedExecutionSignalsSubject + map:^(RACSignal *signal) { + return [signal catchTo:[RACSignal empty]]; + }] + deliverOn:RACScheduler.mainThreadScheduler] + setNameWithFormat:@"%@ -executionSignals", self]; +``` + +它只是将信号中的所有的错误 `NSError` 转换成了 `RACEmptySignal` 对象,并派发到主线程上。 + +![Execution-Signals](images/RACCommand/Execution-Signals.png) + +如果你只订阅了 `executionSignals`,那么其实你不会收到任何的错误,所有的错误都会以 `-sendNext:` 的形式被发送到 `errors` 信号中,这会在后面详细介绍。 + +### executing + +`executing` 是一个表示当前是否有任务执行的信号,这个信号使用了在上一节中介绍的临时变量作为数据源: + +```objectivec +_executing = [[[[[immediateExecuting + deliverOn:RACScheduler.mainThreadScheduler] + startWith:@NO] + distinctUntilChanged] + replayLast] + setNameWithFormat:@"%@ -executing", self]; +``` + +这里对 `immediateExecuting` 的变换还是非常容易理解的: + +![Executing-Signa](images/RACCommand/Executing-Signal.png) + +最后的 `replayLast` 方法将原有的信号变成了容量为 `1` 的 `RACReplaySubject` 对象,这样在每次有订阅者订阅 `executing` 信号时,都只会发送最新的状态,因为订阅者并不关心过去的 `executing` 的值。 + +### enabled + +`enabled` 信号流表示当前的命令是否可以再次被执行,也就是 `-execute:` 方法能否可以成功执行新的任务;该信号流依赖于另一个私有信号 `immediateEnabled`: + +```objectivec +RACSignal *enabledSignal = [RACSignal return:@YES]; + +_immediateEnabled = [[[[RACSignal + combineLatest:@[ enabledSignal, moreExecutionsAllowed ]] + and] + takeUntil:self.rac_willDeallocSignal] + replayLast]; +``` + +虽然这个信号的实现比较简单,不过它同时与三个信号有关,`enabledSignal`、`moreExecutionsAllowed` 以及 `rac_willDeallocSignal`: + +![Immediate-Enabled-Signa](images/RACCommand/Immediate-Enabled-Signal.png) + +虽然图中没有体现出方法 `-takeUntil:self.rac_willDeallocSignal` 的执行,不过你需要知道,这个信号在当前 `RACCommand` 执行 `dealloc` 之后就不会再发出任何消息了。 + +而 `enabled` 信号其实与 `immediateEnabled` 相差无几: + +```objectivec +_enabled = [[[[[self.immediateEnabled + take:1] + concat:[[self.immediateEnabled skip:1] deliverOn:RACScheduler.mainThreadScheduler]] + distinctUntilChanged] + replayLast] + setNameWithFormat:@"%@ -enabled", self]; +``` + +从名字你可以看出来,`immediateEnabled` 在每次原信号发送消息时都会重新计算,而 `enabled` 调用了 `-distinctUntilChanged` 方法,所以如果连续几次值相同就不会再次发送任何消息。 + +除了调用 `-distinctUntilChanged` 的区别之外,你可以看到 `enabled` 信号在最开始调用了 `-take:`和 `-concat:` 方法: + +```objectivec +[[self.immediateEnabled + take:1] + concat:[[self.immediateEnabled skip:1] deliverOn:RACScheduler.mainThreadScheduler]] +``` + +虽然序列并没有任何的变化,但是在这种情况下,`enabled` 信号流中的第一个值会在订阅线程上到达,剩下的所有的值都会在主线程上派发;如果你知道,在一般情况下,我们都会使用 `enabled` 信号来控制 UI 的改变(例如 `UIButton`),相信你就会明白这么做的理由了。 + +### errors + +错误信号是 `RACCommand` 中比较简单的信号;为了保证 `RACCommand` 对此执行 `-execute:` 方法也可以继续运行,我们只能将所有的错误以其它的形式发送到 `errors` 信号中,防止向 `executionSignals` 发送错误信号后,`executionSignals` 信号就会中止的问题。 + +我们使用如下的方式创建 `errors` 信号: + +```objectivec +RACMulticastConnection *errorsConnection = [[[self.addedExecutionSignalsSubject + flattenMap:^(RACSignal *signal) { + return [[signal + ignoreValues] + catch:^(NSError *error) { + return [RACSignal return:error]; + }]; + }] + deliverOn:RACScheduler.mainThreadScheduler] + publish]; + +_errors = [errorsConnection.signal setNameWithFormat:@"%@ -errors", self]; +[errorsConnection connect]; +``` + +信号的创建过程是把所有的错误消息重新打包成 `RACErrorSignal` 并在主线程上进行派发: + +![Errors-Signals](images/RACCommand/Errors-Signals.png) + +使用者只需要调用 `-subscribeNext:` 就可以从这个信号中获取所有执行过程中发生的错误。 + +## RACCommand 的使用 + +`RACCommand` 非常适合封装网络请求,我们可以使用下面的代码封装一个网络请求: + +```objectivec +RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id _Nullable input) { + return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { + NSURL *url = [NSURL URLWithString:@"http://localhost:3000/"]; + AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:url]; + NSString *URLString = [NSString stringWithFormat:@"/api/products/%@", input ?: @1]; + NSURLSessionDataTask *task = [manager GET:URLString parameters:nil progress:nil + success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { + [subscriber sendNext:responseObject]; + [subscriber sendCompleted]; + } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { + [subscriber sendError:error]; + }]; + return [RACDisposable disposableWithBlock:^{ + [task cancel]; + }]; + }]; +}]; +``` + +上面的 `RACCommand` 对象可以通过 `-execute:` 方法执行,同时,订阅 `executionSignals` 以及 `errors` 来获取网络请求的结果。 + +```objectivec +[[command.executionSignals switchToLatest] subscribeNext:^(id _Nullable x) { + NSLog(@"%@", x); +}]; +[command.errors subscribeNext:^(NSError * _Nullable x) { + NSLog(@"%@", x); +}]; +[command execute:@1]; +``` + +向方法 `-execute:` 中传入了 `@1` 对象,从服务器中获取了 `id = 1` 的商品对象;当然,我们也可以传入不同的 `id` 来获取不同的模型,所有的网络请求以及 JSON 转换模型的逻辑都可以封装到这个 `RACCommand` 的 block 中,外界只是传入一个 `id`,最后就从 `executionSignals` 信号中获取了开箱即用的对象。 + +## 总结 + +使用 `RACCommand` 能够优雅地将包含副作用的操作和与副作用无关的操作分隔起来;整个 `RACCommand` 相当于一个黑箱,从 `-execute:` 方法中获得输入,最后以向信号发送消息的方式,向订阅者推送结果。 + +![RACCommand-Side-Effect](images/RACCommand/RACCommand-Side-Effect.png) + +这种执行任务的方式就像是一个函数,根据输入的不同,有着不同的输出,非常适合与 UI、网络操作的相关的任务,这也是 `RACCommand` 的设计的优雅之处。 + +## References + ++ [ReactiveCocoa Essentials: Understanding and Using RACCommand](http://codeblog.shape.dk/blog/2013/12/05/reactivecocoa-essentials-understanding-and-using-raccommand/) ++ [ReactiveCocoa 中 RACCommand 底层实现分析](https://halfrost.com/reactivecocoa_raccommand/) + +> Github Repo:[iOS-Source-Code-Analyze](https://github.com/draveness/iOS-Source-Code-Analyze) +> +> Follow: [Draveness · GitHub](https://github.com/Draveness) +> +> Source: http://draveness.me/raccommand + + diff --git a/contents/ReactiveObjC/RACDelegateProxy.md b/contents/ReactiveObjC/RACDelegateProxy.md new file mode 100644 index 0000000..df90970 --- /dev/null +++ b/contents/ReactiveObjC/RACDelegateProxy.md @@ -0,0 +1,432 @@ +# 从代理到 RACSignal + +ReactiveCocoa 将 Cocoa 中的 Target-Action、KVO、通知中心以及代理等设计模式都桥接到了 RAC 的世界中,我们在随后的几篇文章中会介绍 RAC 如何做到了上面的这些事情,而本篇文章会介绍 ReactiveCocoa 是如何把**代理**转换为信号的。 + +![Delegate-To-RACSigna](images/RACDelegateProxy/Delegate-To-RACSignal.png) + +## RACDelegateProxy + +从代理转换成信号所需要的核心类就是 `RACDelegateProxy`,这是一个设计的非常巧妙的类;虽然在类的头文件中,它被标记为私有类,但是我们仍然可以使用 `-initWithProtocol:` 方法直接初始化该类的实例。 + +```objectivec +- (instancetype)initWithProtocol:(Protocol *)protocol { + self = [super init]; + class_addProtocol(self.class, protocol); + _protocol = protocol; + return self; +} +``` + +从初始化方法中,我们可以看出 `RACDelegateProxy` 是一个包含实例变量 `_protocol` 的类: + +![RACDelegateProxy](images/RACDelegateProxy/RACDelegateProxy.png) + +在整个 `RACDelegateProxy` 类的实现中,你都不太能看出与这个实例变量 `_protocol` 的关系;稍微对 iOS 有了解的人可能都知道,在 Cocoa 中有一个非常特别的根类 `NSProxy`,而从它的名字我们也可以推断出来,`NSProxy` 一般用于实现代理(主要是对消息进行转发),但是 ReactiveCocoa 中这个 `delegate` 的代理 `RACDelegateProxy` 并没有继承这个 `NSProxy` 根类: + +```objectivec +@interface RACDelegateProxy : NSObject + +@end +``` + +那么 `RACDelegateProxy` 是如何作为 Cocoa 中组件的代理,并为原生组件添加 `RACSignal` 的支持呢?我们以 `UITableView` 为例来展示 `RACDelegateProxy` 是如何与 UIKit 组件互动的,我们需要实现的是以下功能: + +![RACDelegateProxy-UITableVie](images/RACDelegateProxy/RACDelegateProxy-UITableView.gif) + +在点击所有的 `UITableViewCell` 时都会自动取消点击状态,通常情况下,我们可以直接在代理方法 `-tableView:didSelectRowAtIndexPath:` 中执行 `-deselectRowAtIndexPath:animated:` 方法: + +```objectivec +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + [tableView deselectRowAtIndexPath:indexPath animated:YES]; +} +``` + +使用信号的话相比而言就比较麻烦了: + +```objectivec +RACDelegateProxy *proxy = [[RACDelegateProxy alloc] initWithProtocol:@protocol(UITableViewDelegate)]; +objc_setAssociatedObject(self, _cmd, proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +proxy.rac_proxiedDelegate = self; +[[proxy rac_signalForSelector:@selector(tableView:didSelectRowAtIndexPath:)] + subscribeNext:^(RACTuple *value) { + [value.first deselectRowAtIndexPath:value.second animated:YES]; + }]; +self.tableView.delegate = (id<UITableViewDelegate>)proxy; +``` + +1. 初始化 `RACDelegateProxy` 实例,传入 `UITableViewDelegate` 协议,并将实例存入视图控制器以**确保实例不会被意外释放**造成崩溃; +2. 设置代理的 `rac_proxiedDelegate` 属性为视图控制器; +3. 使用 `-rac_signalForSelector:` 方法生成一个 `RACSignal`,在 `-tableView:didSelectRowAtIndexPath:` 方法调用时将方法的参数打包成 `RACTuple` 向信号中发送新的 `next` 消息; +4. 重新设置 `UITableView` 的代理; + +在 `UITableViewDelgate` 中的代理方法执行时,实际上会被 `RACDelegateProxy` 拦截,并根据情况决定是处理还是转发: + +![UITableViewDelegate-With-RACDelegateProxy](images/RACDelegateProxy/UITableViewDelegate-With-RACDelegateProxy.png) + +如果 `RACDelegateProxy` 实现了该代理方法就会交给它处理,如:`-tableView:didSelectRowAtIndexPath:`;否则,当前方法就会被转发到原 `delegate` 上,在这里就是 `UIViewController` 对象。 + +`RACDelegateProxy` 中有两个值得特别注意的问题,一是 `RACDelegateProxy` 是如何进行消息转发的,有事如何将自己无法实现的消息交由原代理处理,第二是 `RACDelegateProxy` 如何通过方法 `-rac_signalForSelector:` 在原方法调用时以 `RACTuple` 的方式发送到 `RACSignal` 上。 + +## 消息转发的实现 + +首先,我们来看 `RACDelegateProxy` 是如何在无法响应方法时,将方法转发给原有的代理的;`RACDelegateProxy` 通过覆写几个方法来实现,最关键的就是 `-forwardInvocation:` 方法: + +```objectivec +- (void)forwardInvocation:(NSInvocation *)invocation { + [invocation invokeWithTarget:self.rac_proxiedDelegate]; +} +``` + +当然,作为消息转发流程的一部分 `-methodSignatureForSelector:` 方法也需要在 `RACDelegateProxy` 对象中实现: + +```objectivec +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { + struct objc_method_description methodDescription = protocol_getMethodDescription(_protocol, selector, NO, YES); + if (methodDescription.name == NULL) { + methodDescription = protocol_getMethodDescription(_protocol, selector, YES, YES); + if (methodDescription.name == NULL) return [super methodSignatureForSelector:selector]; + } + return [NSMethodSignature signatureWithObjCTypes:methodDescription.types]; +} +``` + +我们会从协议的方法中尝试获取其中的可选方法和必须实现的方法,最终获取方法的签名 `NSMethodSignature` 对象。 + +整个方法决议和消息转发的过程如下图所示,在整个方法决议和消息转发的过程中 Objective-C 运行时会再次提供执行该方法的机会。 + +![Message-Forwarding](images/RACDelegateProxy/Message-Forwarding.png) + +例子中的代理方法最后也被 `-forwardInvocation:` 方法成功的转发到了 `UITableView` 的原代理上。 + +## 从代理到信号 + +在 `RACDelegateProxy` 中的另一个非常神奇的方法就是将某一个代理方法转换成信号的 `-signalForSelector:`: + +```objectivec +- (RACSignal *)signalForSelector:(SEL)selector { + return [self rac_signalForSelector:selector fromProtocol:_protocol]; +} + +- (RACSignal *)rac_signalForSelector:(SEL)selector fromProtocol:(Protocol *)protocol { + return NSObjectRACSignalForSelector(self, selector, protocol); +} +``` + +该方法会在传入的协议方法被调用时,将协议方法中的所有参数以 `RACTuple` 的形式发送到返回的信号上,使用者可以通过订阅这个信号来获取所有的参数;而方法 `NSObjectRACSignalForSelector` 的实现还是比较复杂的。 + +```objectivec +static RACSignal *NSObjectRACSignalForSelector(NSObject *self, SEL selector, Protocol *protocol) { + SEL aliasSelector = RACAliasForSelector(selector); + + RACSubject *subject = objc_getAssociatedObject(self, aliasSelector); + if (subject != nil) return subject; + + Class class = RACSwizzleClass(self); + subject = [RACSubject subject]; + objc_setAssociatedObject(self, aliasSelector, subject, OBJC_ASSOCIATION_RETAIN); + + Method targetMethod = class_getInstanceMethod(class, selector); + if (targetMethod == NULL) { + const char *typeEncoding; + if (protocol == NULL) { + typeEncoding = RACSignatureForUndefinedSelector(selector); + } else { + struct objc_method_description methodDescription = protocol_getMethodDescription(protocol, selector, NO, YES); + if (methodDescription.name == NULL) { + methodDescription = protocol_getMethodDescription(protocol, selector, YES, YES); + } + typeEncoding = methodDescription.types; + } + class_addMethod(class, selector, _objc_msgForward, typeEncoding); + } else if (method_getImplementation(targetMethod) != _objc_msgForward) { + const char *typeEncoding = method_getTypeEncoding(targetMethod); + + class_addMethod(class, aliasSelector, method_getImplementation(targetMethod), typeEncoding); + class_replaceMethod(class, selector, _objc_msgForward, method_getTypeEncoding(targetMethod)); + } + return subject; +} +``` + +这个 C 函数总共做了两件非常重要的事情,第一个是将传入的选择子对应的实现变为 `_objc_msgForward`,也就是在调用该方法时,会直接进入消息转发流程,第二是用 `RACSwizzleClass` 调剂当前类的一些方法。 + +![NSObjectRACSignalForSelecto](images/RACDelegateProxy/NSObjectRACSignalForSelector.png) + +### 从 selector 到 _objc_msgForward + +我们具体看一下这部分代码是如何实现的,在修改选择子对应的实现之前,我们会先做一些准备工作: + +```objectivec +SEL aliasSelector = RACAliasForSelector(selector); + +RACSubject *subject = objc_getAssociatedObject(self, aliasSelector); +if (subject != nil) return subject; + +Class class = RACSwizzleClass(self); + +subject = [RACSubject subject]; +objc_setAssociatedObject(self, aliasSelector, subject, OBJC_ASSOCIATION_RETAIN); + +Method targetMethod = class_getInstanceMethod(class, selector); +``` + +1. 获取选择子的别名,在这里我们通过为选择子加前缀 `rac_alias_` 来实现; +2. 尝试以 `rac_alias_selector` 为键获取一个热信号 `RACSubject`; +3. 使用 `RACSwizzleClass` 调剂当前类的一些方法(我们会在下一节中介绍); +4. 从当前类中获取目标方法的结构体 `targetMethod`; + +在进行了以上的准备工作之后,我们就开始修改选择子对应的实现了,整个的修改过程会分为三种情况: + +![Swizzle-objc_msgForward](images/RACDelegateProxy/Swizzle-objc_msgForward.png) + +下面会按照这三种情况依次介绍在不同情况下,如何将对应选择子的实现改为 `_objc_msgForward` 完成消息转发的。 + +#### targetMethod == NULL && protocol == NULL + +在找不到选择子对应的方法并且没有传入协议时,这时执行的代码最为简单: + +```objectivec +typeEncoding = RACSignatureForUndefinedSelector(selector); +class_addMethod(class, selector, _objc_msgForward, typeEncoding); +``` + +我们会通过 `RACSignatureForUndefinedSelector` 生成一个当前方法默认的类型编码。 + +> 对类型编码不了解的可以阅读苹果的官方文档 [Type Encodings · Apple Developer](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html),其中详细解释了类型编码是什么,它在整个 Objective-C 运行时有什么作用。 + +```objectivec +static const char *RACSignatureForUndefinedSelector(SEL selector) { + const char *name = sel_getName(selector); + NSMutableString *signature = [NSMutableString stringWithString:@"v@:"]; + + while ((name = strchr(name, ':')) != NULL) { + [signature appendString:@"@"]; + name++; + } + + return signature.UTF8String; +} +``` + +该方法在生成类型编码时,会按照 `:` 的个数来为 `v@:` 这个类型编码添加 `@` 字符;简单说明一下它的意思,ReactiveCocoa 默认所有的方法的返回值类型都为空 `void`,都会传入 `self` 以及当前方法的选择子 `SEL`,它们的类型编码可以在下图中找到,分别是 `v@:`;而 `@` 代表 `id` 类型,也就是我们默认代理方法中的所有参数都是 `NSObject` 类型的。 + +![TypeEncoding](images/RACDelegateProxy/TypeEncoding.png) + +生成了类型编码之后,由于我们并没有在当前类中找到该选择子对应的方法,所以会使用 `class_addMethod` 为当前类提供一个方法的实现,直接将当前选择子的实现改为 `_objc_msgForward`。 + +![Selector-To-ObjC-Message-Forward](images/RACDelegateProxy/Selector-To-ObjC-Message-Forward.png) + +#### targetMethod == NULL && protocol != NULL + +当类中不存在当前选择子对应的方法 `targetMethod`,但是向当前函数中传入了协议时,我们会尝试从协议中获取方法描述: + +```objectivec +struct objc_method_description methodDescription = protocol_getMethodDescription(protocol, selector, NO, YES); + +if (methodDescription.name == NULL) { + methodDescription = protocol_getMethodDescription(protocol, selector, YES, YES); +} +typeEncoding = methodDescription.types; +class_addMethod(class, selector, _objc_msgForward, typeEncoding); +``` + +这里会使用 `protocol_getMethodDescription` 两次从协议中获取可选和必须实现的方法的描述,并从结构体中拿出类型编码,最后为类添加这个之前不存在的方法: + +![Selector-To-ObjC-Message-Forward](images/RACDelegateProxy/Selector-To-ObjC-Message-Forward.png) + +在这种情况下,其最后的结果与上一种的完全相同,因为它们都是对不存在该方法,只需要获得方法的类型编码并将实现添加为 `_objc_msgForward`,交给消息转发流程进行处理即可。 + +#### targetMethod != NULL + +在目标方法的实现不为空并且它的实现并不是 `_objc_msgForward` 时,我们就会进入以下流程修改原有方法的实现: + +```objectivec +const char *typeEncoding = method_getTypeEncoding(targetMethod); + +class_addMethod(class, aliasSelector, method_getImplementation(targetMethod), typeEncoding); +class_replaceMethod(class, selector, _objc_msgForward, method_getTypeEncoding(targetMethod)); +``` + +同样,我们需要获得目标方法的方法签名、添加 `aliasSelector` 这个新方法,最后在修改原方法的实现到 `_objc_msgForward`。 + +![Selector-To-ObjC-Message-Forward-With-RACSelecto](images/RACDelegateProxy/Selector-To-ObjC-Message-Forward-With-RACSelector.png) + +上图展示了在目标方法不为空并且其实现不为 `_objc_msgForward` 时,`NSObjectRACSignalForSelector` 是如何修改原方法实现的。 + +### 调剂类的方法 + +`NSObjectRACSignalForSelector` 在修改原选择子方法实现的之前就已经修改了当前类很多方法的实现: + ++ `-methodSignatureForSelector:` ++ `-class` ++ `-respondsToSelector` ++ `-forwardInvocation:` + +整个调剂方法的过程 `RACSwizzleClass` 还是比较复杂的,我们可以分三部分看下面的代码: + +```objectivec +static Class RACSwizzleClass(NSObject *self) { + Class statedClass = self.class; + Class baseClass = object_getClass(self); + + NSString *className = NSStringFromClass(baseClass); + const char *subclassName = [className stringByAppendingString:RACSubclassSuffix].UTF8String; + Class subclass = objc_getClass(subclassName); + + if (subclass == nil) { + subclass = objc_allocateClassPair(baseClass, subclassName, 0); + if (subclass == nil) return nil; + + RACSwizzleForwardInvocation(subclass); + RACSwizzleRespondsToSelector(subclass); + RACSwizzleGetClass(subclass, statedClass); + RACSwizzleGetClass(object_getClass(subclass), statedClass); + RACSwizzleMethodSignatureForSelector(subclass); + + objc_registerClassPair(subclass); + } + object_setClass(self, subclass); + return subclass; +} +``` + +1. 从当前类 `RACDelegateProxy` 衍生出一个子类 `RACDelegateProxy_RACSelectorSignal`; +2. 调用各种 `RACSwizzleXXX` 方法修改当前子类的一些表现; +3. 将 `RACDelegateProxy` 对象的类设置成自己,这样就会在查找方法时,找到 `RACDelegateProxy_RACSelectorSignal` 中的实现; + +在修改的几个方法中最重要的就是 `-forwardInvocation:`: + +```objectivec +static void RACSwizzleForwardInvocation(Class class) { + SEL forwardInvocationSEL = @selector(forwardInvocation:); + Method forwardInvocationMethod = class_getInstanceMethod(class, forwardInvocationSEL); + + void (*originalForwardInvocation)(id, SEL, NSInvocation *) = NULL; + if (forwardInvocationMethod != NULL) { + originalForwardInvocation = (__typeof__(originalForwardInvocation))method_getImplementation(forwardInvocationMethod); + } + + id newForwardInvocation = ^(id self, NSInvocation *invocation) { + BOOL matched = RACForwardInvocation(self, invocation); + if (matched) return; + + if (originalForwardInvocation == NULL) { + [self doesNotRecognizeSelector:invocation.selector]; + } else { + originalForwardInvocation(self, forwardInvocationSEL, invocation); + } + }; + + class_replaceMethod(class, forwardInvocationSEL, imp_implementationWithBlock(newForwardInvocation), "v@:@"); +} +``` + +这个方法中大部分的内容都是平淡无奇的,在新的 `-forwardInvocation:` 方法中,执行的 `RACForwardInvocation` 是实现整个消息转发的关键内容: + +```objectivec +static BOOL RACForwardInvocation(id self, NSInvocation *invocation) { + SEL aliasSelector = RACAliasForSelector(invocation.selector); + RACSubject *subject = objc_getAssociatedObject(self, aliasSelector); + + Class class = object_getClass(invocation.target); + BOOL respondsToAlias = [class instancesRespondToSelector:aliasSelector]; + if (respondsToAlias) { + invocation.selector = aliasSelector; + [invocation invoke]; + } + + if (subject == nil) return respondsToAlias; + + [subject sendNext:invocation.rac_argumentsTuple]; + return YES; +} +``` + +在 `-rac_signalForSelector:` 方法返回的 `RACSignal` 上接收到的参数信号,就是从这个方法发送过去的,新的实现 `RACForwardInvocation` 改变了原有的 `selector` 到 `aliasSelector`,然后使用 `-invoke` 完成该调用,而所有的参数会以 `RACTuple` 的方式发送到信号上。 + +像其他的方法 `-respondToSelector:` 等等,它们的实现就没有这么复杂并且重要了: + +```objectivec +id newRespondsToSelector = ^ BOOL (id self, SEL selector) { + Method method = rac_getImmediateInstanceMethod(class, selector); + + if (method != NULL && method_getImplementation(method) == _objc_msgForward) { + SEL aliasSelector = RACAliasForSelector(selector); + if (objc_getAssociatedObject(self, aliasSelector) != nil) return YES; + } + + return originalRespondsToSelector(self, respondsToSelectorSEL, selector); +}; +``` + +`rac_getImmediateInstanceMethod` 从当前类获得方法的列表,并从中找到与当前 `selector` 同名的方法 `aliasSelector`,然后根据不同情况判断方法是否存在。 + +对 `class` 的修改,是为了让对象对自己的身份『说谎』,因为我们子类化了 `RACDelegateProxy`,并且重新设置了对象的类,将所有的方法都转发到了这个子类上,如果不修改 `class` 方法,那么当开发者使用它自省时就会得到错误的类,而这是我们不希望看到的。 + +```objectivec +static void RACSwizzleGetClass(Class class, Class statedClass) { + SEL selector = @selector(class); + Method method = class_getInstanceMethod(class, selector); + IMP newIMP = imp_implementationWithBlock(^(id self) { + return statedClass; + }); + class_replaceMethod(class, selector, newIMP, method_getTypeEncoding(method)); +} +``` + +在最后我们会对获得方法签名的 `-methodSignatureForSelector:` 方法进行修改: + +```objectivec +IMP newIMP = imp_implementationWithBlock(^(id self, SEL selector) { + Class actualClass = object_getClass(self); + Method method = class_getInstanceMethod(actualClass, selector); + if (method == NULL) { + struct objc_super target = { + .super_class = class_getSuperclass(class), + .receiver = self, + }; + NSMethodSignature * (*messageSend)(struct objc_super *, SEL, SEL) = (__typeof__(messageSend))objc_msgSendSuper; + return messageSend(&target, @selector(methodSignatureForSelector:), selector); + } + + char const *encoding = method_getTypeEncoding(method); + return [NSMethodSignature signatureWithObjCTypes:encoding]; +}); +``` + +在方法不存在时,通过 `objc_msgSendSuper` 调用父类的 `-methodSignatureForSelector:` 方法获取方法签名。 + +## 方法调用的过程 + +在一般情况下,Objective-C 中某一消息被发送到一个对象时,它会先获取当前对象对应的类,然后从类的选择子表查找该方法对应的实现并执行。 + +![Selector-To-IMP](images/RACDelegateProxy/Selector-To-IMP.png) + +与正常的方法实现查找以及执行过程的简单不同,如果我们对某一个方法调用了 `-rac_signalForSelector:` 方法,那么对于同一个对象对应的类的所有方法,它们的执行过程会变得非常复杂: + +![After-Call-RACSignalForSelecto](images/RACDelegateProxy/After-Call-RACSignalForSelector.png) + +1. 由于当前对象对应的类已经被改成了 `Subclass`,即 `Class_RACSelectorSignal`,所以会在子类中查找方法的实现; +2. 方法对应的实现已经被改成了 `-forwardInvocation:`,会直接进入消息转发流程中处理; +3. 根据传入的选择子获取同名选择子 `rac_alias_selector`; +4. 拿到当前 `NSInvocation` 对象中 `target` 的类,判断是否可以响应该选择子; +5. 将 `NSInvocation` 对象中的选择子改为 `rac_alias_selector` 并执行其实现; +6. 从 `NSInvocation` 对象中获取参数并打包成 `RACTuple`,以 `next` 消息的形式发送到持有的 `RACSubject` 热信号上; + +这时所有的订阅者才会在该方法被调用时收到消息,完成相应的任务。 + +## 总结 + +ReactiveCocoa 使用了一种非常神奇的办法把原有的代理模式成功的桥接到 `RACSignal` 的世界中,并为我们提供了 `RACDelegateProxy` 这一接口,能够帮助我们以信号的形式监听所有的代理方法,可以用 block 的形式去代替原有的方法,为我们减少一些工作量。 + +## References + ++ [Type Encodings · Apple Developer](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html) + +> Github Repo:[iOS-Source-Code-Analyze](https://github.com/draveness/iOS-Source-Code-Analyze) +> +> Follow: [Draveness · GitHub](https://github.com/Draveness) +> +> Source: http://draveness.me/racdelegateproxy + + diff --git a/contents/ReactiveObjC/RACMulticastConnection.md b/contents/ReactiveObjC/RACMulticastConnection.md new file mode 100644 index 0000000..c86a6af --- /dev/null +++ b/contents/ReactiveObjC/RACMulticastConnection.md @@ -0,0 +1,289 @@ +# 用于多播的 RACMulticastConnection + +ReactiveCocoa 中的信号信号在默认情况下都是冷的,每次有新的订阅者订阅信号时都会执行信号创建时传入的 block;这意味着对于任意一个订阅者,所需要的数据都会**重新计算**,这在大多数情况下都是开发者想看到的情况,但是这在信号中的 block 有副作用或者较为昂贵时就会有很多问题。 + +![RACMulticastConnection](images/RACMulticastConnection/RACMulticastConnection.png) + +我们希望有一种模型能够将冷信号转变成热信号,并在合适的时间触发,向所有的订阅者发送消息;而今天要介绍的 `RACMulticastConnection` 就是用于解决上述问题的。 + +## RACMulticastConnection 简介 + +`RACMulticastConnection` 封装了将一个信号的订阅分享给多个订阅者的思想,它的每一个对象都持有两个 `RACSignal`: + +![RACMulticastConnection-Interface](images/RACMulticastConnection/RACMulticastConnection-Interface.png) + +一个是私有的源信号 `sourceSignal`,另一个是用于广播的信号 `signal`,其实是一个 `RACSubject` 对象,不过对外只提供 `RACSignal` 接口,用于使用者通过 `-subscribeNext:` 等方法进行订阅。 + +## RACMulticastConnection 的初始化 + +`RACMulticastConnection` 有一个非常简单的初始化方法 `-initWithSourceSignal:subject:`,不过这个初始化方法是私有的: + +```objectivec +- (instancetype)initWithSourceSignal:(RACSignal *)source subject:(RACSubject *)subject { + self = [super init]; + + _sourceSignal = source; + _serialDisposable = [[RACSerialDisposable alloc] init]; + _signal = subject; + + return self; +} +``` + +在 `RACMulticastConnection` 的头文件的注释中,对它的初始化有这样的说明: + +> Note that you shouldn't create RACMulticastConnection manually. Instead use -[RACSignal publish] or -[RACSignal multicast:]. + +我们不应该直接使用 `-initWithSourceSignal:subject:` 来初始化一个对象,我们应该通过 `RACSignal` 的实例方法初始化 `RACMulticastConnection` 实例。 + +```objectivec +- (RACMulticastConnection *)publish { + RACSubject *subject = [RACSubject subject]; + RACMulticastConnection *connection = [self multicast:subject]; + return connection; +} + +- (RACMulticastConnection *)multicast:(RACSubject *)subject { + RACMulticastConnection *connection = [[RACMulticastConnection alloc] initWithSourceSignal:self subject:subject]; + return connection; +} +``` + +这两个方法 `-publish` 和 `-multicast:` 都是对初始化方法的封装,并且都会返回一个 `RACMulticastConnection` 对象,传入的 `sourceSignal` 就是当前信号,`subject` 就是用于对外广播的 `RACSubject` 对象。 + +## RACSignal 和 RACMulticastConnection + +网络请求在客户端其实是一个非常昂贵的操作,也算是多级缓存中最慢的一级,在使用 ReactiveCocoa 处理业务需求中经常会遇到下面的情况: + +```objectivec +RACSignal *requestSignal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { + NSLog(@"Send Request"); + NSURL *url = [NSURL URLWithString:@"http://localhost:3000/"]; + AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:url]; + NSString *URLString = [NSString stringWithFormat:@"/api/products/1"]; + NSURLSessionDataTask *task = [manager GET:URLString parameters:nil progress:nil + success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { + [subscriber sendNext:responseObject]; + [subscriber sendCompleted]; + } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { + [subscriber sendError:error]; + }]; + return [RACDisposable disposableWithBlock:^{ + [task cancel]; + }]; +}]; + +[requestSignal subscribeNext:^(id _Nullable x) { + NSLog(@"product: %@", x); +}]; + +[requestSignal subscribeNext:^(id _Nullable x) { + NSNumber *productId = [x objectForKey:@"id"]; + NSLog(@"productId: %@", productId); +}]; +``` + +通过订阅发出网络请求的信号经常会被多次订阅,以满足不同 UI 组件更新的需求,但是以上代码却有非常严重的问题。 + +![RACSignal-And-Subscribe](images/RACMulticastConnection/RACSignal-And-Subscribe.png) + +每一次在 `RACSignal` 上执行 `-subscribeNext:` 以及类似方法时,都会发起一次新的网络请求,我们希望避免这种情况的发生。 + +为了解决上述问题,我们使用了 `-publish` 方法获得一个多播对象 `RACMulticastConnection`,更改后的代码如下: + +```objectivec +RACMulticastConnection *connection = [[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { + NSLog(@"Send Request"); + ... +}] publish]; + +[connection.signal subscribeNext:^(id _Nullable x) { + NSLog(@"product: %@", x); +}]; +[connection.signal subscribeNext:^(id _Nullable x) { + NSNumber *productId = [x objectForKey:@"id"]; + NSLog(@"productId: %@", productId); +}]; + +[connection connect]; +``` + +在这个例子中,我们使用 `-publish` 方法生成实例,订阅者不再订阅源信号,而是订阅 `RACMulticastConnection` 中的 `RACSubject` 热信号,最后通过 `-connect` 方法触发源信号中的任务。 + +![RACSignal-RACMulticastConnection-Connect](images/RACMulticastConnection/RACSignal-RACMulticastConnection-Connect.png) + +> 对于热信号不了解的读者,可以阅读这篇文章 [『可变』的热信号 RACSubject](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/ReactiveObjC/RACSubject.md)。 + +### publish 和 multicast 方法 + +我们再来看一下 `-publish` 和 `-multicast:` 这两个方法的实现: + +```objectivec +- (RACMulticastConnection *)publish { + RACSubject *subject = [RACSubject subject]; + RACMulticastConnection *connection = [self multicast:subject]; + return connection; +} + +- (RACMulticastConnection *)multicast:(RACSubject *)subject { + RACMulticastConnection *connection = [[RACMulticastConnection alloc] initWithSourceSignal:self subject:subject]; + return connection; +} +``` + +当 `-publish` 方法调用时相当于向 `-multicast:` 传入了 `RACSubject`。 + +![publish-and-multicast](images/RACMulticastConnection/publish-and-multicast.png) + +`-publish` 只是对 `-multicast:` 方法的简单封装,它们都是通过 `RACMulticastConnection` 私有的初始化方法 `-initWithSourceSignal:subject:` 创建一个新的实例。 + +在使用 `-multicast:` 方法时,传入的信号其实就是用于广播的信号;这个信号必须是一个 `RACSubject` 本身或者它的子类: + +![RACSubject - Subclasses](images/RACMulticastConnection/RACSubject%20-%20Subclasses.png) + +传入 `-multicast:` 方法的一般都是 `RACSubject` 或者 `RACReplaySubject` 对象。 + +### 订阅源信号的时间点 + +订阅 `connection.signal` 中的数据流时,其实只是向多播对象中的热信号 `RACSubject` 持有的数组中加入订阅者,而这时刚刚创建的 `RACSubject` 中并没有任何的消息。 + +![SubscribeNext-To-RACSubject-Before-Connect](images/RACMulticastConnection/SubscribeNext-To-RACSubject-Before-Connect.png) + +只有在调用 `-connect` 方法之后,`RACSubject` 才会**订阅**源信号 `sourceSignal`。 + +```objectivec +- (RACDisposable *)connect { + self.serialDisposable.disposable = [self.sourceSignal subscribe:_signal]; + return self.serialDisposable; +} +``` + +这时源信号的 `didSubscribe` 代码块才会执行,向 `RACSubject` 推送消息,消息向下继续传递到 `RACSubject` 所有的订阅者中。 + +![Values-From-RACSignal-To-Subscribers](images/RACMulticastConnection/Values-From-RACSignal-To-Subscribers.png) + +`-connect` 方法通过 `-subscribe:` 实际上建立了 `RACSignal` 和 `RACSubject` 之间的连接,这种方式保证了 `RACSignal` 中的 `didSubscribe` 代码块只执行了一次。 + +所有的订阅者不再订阅原信号,而是订阅 `RACMulticastConnection` 持有的热信号 `RACSubject`,实现对冷信号的一对多传播。 + +在 `RACMulticastConnection` 中还有另一个用于连接 `RACSignal` 和 `RACSubject` 信号的 `-autoconnect` 方法: + +```objectivec +- (RACSignal *)autoconnect { + __block volatile int32_t subscriberCount = 0; + return [RACSignal + createSignal:^(id<RACSubscriber> subscriber) { + OSAtomicIncrement32Barrier(&subscriberCount); + RACDisposable *subscriptionDisposable = [self.signal subscribe:subscriber]; + RACDisposable *connectionDisposable = [self connect]; + + return [RACDisposable disposableWithBlock:^{ + [subscriptionDisposable dispose]; + if (OSAtomicDecrement32Barrier(&subscriberCount) == 0) { + [connectionDisposable dispose]; + } + }]; + }]; +} +``` + +它保证了在 `-autoconnect` 方法返回的对象被第一次订阅时,就会建立源信号与热信号之间的连接。 + +### 使用 RACReplaySubject 订阅源信号 + +虽然使用 `-publish` 方法已经能够解决大部分问题了,但是在 `-connect` 方法调用之后才订阅的订阅者并不能收到消息。 + +如何才能保存 `didSubscribe` 执行过程中发送的消息,并在 `-connect` 调用之后也可以收到消息?这时,我们就要使用 `-multicast:` 方法和 `RACReplaySubject` 来完成这个需求了。 + +```objectivec +RACSignal *sourceSignal = [RACSignal createSignal:...]; +RACMulticastConnection *connection = [sourceSignal multicast:[RACReplaySubject subject]]; +[connection.signal subscribeNext:^(id _Nullable x) { + NSLog(@"product: %@", x); +}]; +[connection connect]; +[connection.signal subscribeNext:^(id _Nullable x) { + NSNumber *productId = [x objectForKey:@"id"]; + NSLog(@"productId: %@", productId); +}]; +``` + +除了使用上述的代码,也有一个更简单的方式创建包含 `RACReplaySubject` 对象的 `RACMulticastConnection`: + +```objectivec +RACSignal *signal = [[RACSignal createSignal:...] replay]; +[signal subscribeNext:^(id _Nullable x) { + NSLog(@"product: %@", x); +}]; +[signal subscribeNext:^(id _Nullable x) { + NSNumber *productId = [x objectForKey:@"id"]; + NSLog(@"productId: %@", productId); +}]; +``` + +`-replay` 方法和 `-publish` 差不多,只是内部封装的热信号不同,并在方法调用时就连接原信号: + +```objectivec +- (RACSignal *)replay { + RACReplaySubject *subject = [RACReplaySubject subject]; + RACMulticastConnection *connection = [self multicast:subject]; + [connection connect]; + return connection.signal; +} +``` + +除了 `-replay` 方法,`RACSignal` 中还定义了与 `RACMulticastConnection` 中相关的其它 `-replay` 方法: + +```objectivec +- (RACSignal<ValueType> *)replay; +- (RACSignal<ValueType> *)replayLast; +- (RACSignal<ValueType> *)replayLazily; +``` + +三个方法都会在 `RACMulticastConnection` 初始化时传入一个 `RACReplaySubject` 对象,不过却有一点细微的差别: + +![Difference-Between-Replay-Methods](images/RACMulticastConnection/Difference-Between-Replay-Methods.png) + +相比于 `-replay` 方法,`-replayLast` 方法生成的 `RACMulticastConnection` 中热信号的容量为 `1`: + +```objectivec +- (RACSignal *)replayLast { + RACReplaySubject *subject = [RACReplaySubject replaySubjectWithCapacity:1]; + RACMulticastConnection *connection = [self multicast:subject]; + [connection connect]; + return connection.signal; +} +``` + +而 `replayLazily` 会在返回的信号被**第一次订阅**时,才会执行 `-connect` 方法: + +```objectivec +- (RACSignal *)replayLazily { + RACMulticastConnection *connection = [self multicast:[RACReplaySubject subject]]; + return [RACSignal + defer:^{ + [connection connect]; + return connection.signal; + }]; +} +``` + +## 总结 + +`RACMulticastConnection` 在处理冷热信号相互转换时非常好用,在 `RACSignal` 中也提供了很多将原有的冷信号通过 `RACMulticastConnection` 转换成热信号的方法。 + +![RACMulticastConnection](images/RACMulticastConnection/RACMulticastConnection.png) + +在遇到冷信号中的行为有副作用后者非常昂贵时,我们就可以使用这些方法将单播变成多播,提高执行效率,减少副作用。 + +## References + ++ [『可变』的热信号 RACSubject](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/ReactiveObjC/RACSubject.md) ++ [细说 ReactiveCocoa 的冷信号与热信号](http://williamzang.com/blog/2015/08/18/talk-about-reactivecocoas-cold-signal-and-hot-signal/) + +> Github Repo:[iOS-Source-Code-Analyze](https://github.com/draveness/iOS-Source-Code-Analyze) +> +> Follow: [Draveness · GitHub](https://github.com/Draveness) +> +> Source: http://draveness.me/racconnection + diff --git a/contents/ReactiveObjC/RACScheduler.md b/contents/ReactiveObjC/RACScheduler.md new file mode 100644 index 0000000..d72a26b --- /dev/null +++ b/contents/ReactiveObjC/RACScheduler.md @@ -0,0 +1,287 @@ +# 理解 RACScheduler 的实现 + +`RACScheduler` 是一个线性执行队列,ReactiveCocoa 中的信号可以在 `RACScheduler` 上执行任务、发送结果;它的实现并不复杂,由多个简单的方法和类组成整个 `RACScheduler` 模块,是整个 ReactiveCocoa 中非常易于理解的部分。 + +## RACScheduler 简介 + +`RACScheduler` 作为 ReactiveCocoa 中唯一的用于调度的模块,它包含很多个性化的子类: + +![RACScheduler-Subclasses](images/RACScheduler/RACScheduler-Subclasses.png) + +`RACScheduler` 类的内部只有一个用于追踪标记和 debug 的属性 `name`,头文件和实现文件中的其它内容都是各种各样的方法;我们可以把其中的方法分为两类,一类是用于初始化 `RACScheduler` 实例的初始化方法: + +![RACScheduler-Initializers](images/RACScheduler/RACScheduler-Initializers.png) + +另一类就是用于调度、执行任务的 `+schedule:` 等方法: + +![RACScheduler-Schedule](images/RACScheduler/RACScheduler-Schedule.png) + +在图中都省略了一些参数缺省的方法,以及一些调用其他方法的调度方法或者初始化方法,用以减少我们分析和理解整个 `RACScheduler` 类的难度。 + +在 `RACScheduler` 中,大部分的调度方法都是需要子类覆写,它本身只提供少数的功能,比如递归 block 的执行: + +```objectivec +- (RACDisposable *)scheduleRecursiveBlock:(RACSchedulerRecursiveBlock)recursiveBlock { + RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable]; + [self scheduleRecursiveBlock:[recursiveBlock copy] addingToDisposable:disposable]; + return disposable; +} +``` + +该方法会递归的执行传入的 `recursiveBlock`,使用的方式非常简单: + +```objectivec +[scheduler scheduleRecursiveBlock:^(void (^reschedule)(void)) { + if (needTerminated) return; + + // do something + + reschedule(); +}]; +``` + +如果需要递归就执行方法中的 `reschedule()`,就会再次执行当前的 block;`-scheduleRecursiveBlock:` 中调用的 `-scheduleRecursiveBlock:addingToDisposable:` 实现比较复杂: + +```objectivec +- (void)scheduleRecursiveBlock:(RACSchedulerRecursiveBlock)recursiveBlock addingToDisposable:(RACCompoundDisposable *)disposable { + ... + RACDisposable *schedulingDisposable = [self schedule:^{ + void (^reallyReschedule)(void) = ^{ + [self scheduleRecursiveBlock:recursiveBlock addingToDisposable:disposable]; + }; + + recursiveBlock(^{ + reallyReschedule(); + }); + }]; + ... +} +``` + +> 方法使用了 `NSLock` 保证在并发情况下并不会出现任何问题,不过在这里展示的代码中,我们将它省略了,一并省略的还有 `RACDisposable` 相关的代码,以保证整个方法逻辑的清晰,方法的原实现可以查看这里 [RACScheduler.m#L130-L187](https://github.com/ReactiveCocoa/ReactiveObjC/blob/9164a24abfbb7d6b2280d78f9c9308a9842bfcfe/ReactiveObjC/RACScheduler.m#L130-L187)。 + +在每次执行 `recursiveBlock` 时,都会传入一个 `reallyReschedule` 用于递归执行传入的 block。 + +其他的方法包括 `+schedule:`、`+after:schedule:` 以及 `after:repeatingEvery:withLeeway:schedule:` 方法都需要子类覆写: + +```objectivec +- (RACDisposable *)schedule:(void (^)(void))block; +- (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block; +- (RACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block { + NSCAssert(NO, @"%@ must be implemented by subclasses.", NSStringFromSelector(_cmd)); + return nil; +} +``` + +而接下来我们就按照初始化方法的顺序依次介绍 `RACScheduler` 的子类了。 + +### RACImmediateScheduler + +`RACImmediateScheduler` 是一个会立即执行传入的代码块的调度器,我们可以使用 `RACScheduler` 的类方法 `+immediateScheduler` 返回一个它的实例: + +```objectivec ++ (RACScheduler *)immediateScheduler { + static dispatch_once_t onceToken; + static RACScheduler *immediateScheduler; + dispatch_once(&onceToken, ^{ + immediateScheduler = [[RACImmediateScheduler alloc] init]; + }); + return immediateScheduler; +} +``` + +由于 `RACImmediateScheduler` 是一个私有类,全局只能通过该方法返回它的实例,所以整个程序的运行周期内,我们通过『合法』手段只能获得唯一一个单例。 + +作为 `RACScheduler` 的子类,它必须对父类的调度方法进行覆写,不过因为本身的职能原因,`RACImmediateScheduler` 对于父类的覆写还是非常简单的: + +```objectivec +- (RACDisposable *)schedule:(void (^)(void))block { + block(); + return nil; +} + +- (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block { + [NSThread sleepUntilDate:date]; + block(); + return nil; +} + +- (RACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block { + NSCAssert(NO, @"+[RACScheduler immediateScheduler] does not support %@.", NSStringFromSelector(_cmd)); + return nil; +} +``` + ++ `+schedule` 方法会立刻执行传入的 block; ++ `+after:schedule:` 方法会将当前线程休眠到指定时间后执行 block; ++ 而对于 `+after:repeatingEvery:withLeeway:schedule:` 方法就干脆不支持。 + +这确实非常符合 `RACImmediateScheduler` 类的名字以及功能,虽然没有要求对递归执行 block 的方法进行覆写,不过它依然做了这件事情: + +```objectivec +- (RACDisposable *)scheduleRecursiveBlock:(RACSchedulerRecursiveBlock)recursiveBlock { + for (__block NSUInteger remaining = 1; remaining > 0; remaining--) { + recursiveBlock(^{ + remaining++; + }); + } + return nil; +} +``` + +实现的过程非常简洁,甚至没有什么值得解释的地方了。 + +### RACTargetQueueScheduler + +`RACTargetQueueScheduler` 继承自 `RACQueueScheduler`,但是由于后者是抽象类,我们并不会直接使用它,它只是为前者提供必要的方法支持,将一部分逻辑抽离出来: + +![RACTargetQueueSchedule](images/RACScheduler/RACTargetQueueScheduler.png) + +这里我们先简单看一下 `RACTargetQueueScheduler` 的实现,整个 `RACTargetQueueScheduler` 类中只有一个初始化方法: + +```objectivec +- (instancetype)initWithName:(NSString *)name targetQueue:(dispatch_queue_t)targetQueue { + dispatch_queue_t queue = dispatch_queue_create(name.UTF8String, DISPATCH_QUEUE_SERIAL); + dispatch_set_target_queue(queue, targetQueue); + return [super initWithName:name queue:queue]; +} +``` + +初始化方法 `-initWithName:targetQueue:` 使用 `dispatch_queue_create` 创建了一个串行队列,然后通过 `dispatch_set_target_queue` 根据传入的 `targetQueue` 设置队列的优先级,最后调用父类的指定构造器完成整个初始化过程。 + +`RACTargetQueueScheduler` 在使用时,将待执行的任务加入一个私有的串行队列中,其优先级与传入的 `targetQueue` 完全相同;不过提到 `RACTargetQueueScheduler` 中队列的优先级,对 GCD 稍有了解的人应该都知道在 GCD 中有着四种不同优先级的全局并行队列,而在 `RACScheduler` 中也有一一对应的枚举类型: + +![RACScheduler-Priority](images/RACScheduler/RACScheduler-Priority.png) + +在使用 `+schedulerWithPriority:` 方法创建 `RACTargetQueueScheduler` 时,就需要传入上面的优先级,方法会通过 GCD 的内置方法 `dispatch_get_global_queue` 获取全局的并行队列,最终返回一个新的实例。 + +```objectivec ++ (RACScheduler *)schedulerWithPriority:(RACSchedulerPriority)priority name:(NSString *)name { + return [[RACTargetQueueScheduler alloc] initWithName:name targetQueue:dispatch_get_global_queue(priority, 0)]; +} +``` + +在 `RACScheduler` 接口中另一个获得主线程调度器的方法 `+mainThreadScheduler`,其实现也是返回一个 `RACTargetQueueScheduler` 对象: + +```objectivec ++ (RACScheduler *)mainThreadScheduler { + static dispatch_once_t onceToken; + static RACScheduler *mainThreadScheduler; + dispatch_once(&onceToken, ^{ + mainThreadScheduler = [[RACTargetQueueScheduler alloc] initWithName:@"org.reactivecocoa.ReactiveObjC.RACScheduler.mainThreadScheduler" targetQueue:dispatch_get_main_queue()]; + }); + + return mainThreadScheduler; +} +``` + +与前者不同的是,后者通过单例模式每次调用时返回一个相同的主线程队列。 + +#### 抽象类 RACQueueScheduler + +在我们对 `RACTargetQueueScheduler` 有一定了解之后,再看它的抽象类就非常简单了;`RACImmediateScheduler` 会立即执行传入的任务,而 `RACQueueScheduler` 其实就是对 GCD 的封装,相信各位读者从它的子类的实现就可以看出来。 + +`RACQueueScheduler` 对三个需要覆写的方法都进行了重写,其实现完全基于 GCD,以 `-schedule:` 方法为例: + +```objectivec +- (RACDisposable *)schedule:(void (^)(void))block { + RACDisposable *disposable = [[RACDisposable alloc] init]; + + dispatch_async(self.queue, ^{ + if (disposable.disposed) return; + [self performAsCurrentScheduler:block]; + }); + + return disposable; +} +``` + +使用 `dispatch_async` 方法直接将需要执行的任务**异步派发**到它所持有的队列上;而 `-after:schedule:` 方法的实现相信各位读者也能猜到: + +```objectivec +- (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block { + RACDisposable *disposable = [[RACDisposable alloc] init]; + + dispatch_after([self.class wallTimeWithDate:date], self.queue, ^{ + if (disposable.disposed) return; + [self performAsCurrentScheduler:block]; + }); + + return disposable; +} +``` + +哪怕不使用 `RACScheduler`,我们也能够想到利用 `dispatch_after` 完成一些需要延迟执行的任务,最后的 `+after:repeatingEvery:withLeeway:schedule:` 方法的实现就稍微复杂一些了: + +```objectivec +- (RACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block { + uint64_t intervalInNanoSecs = (uint64_t)(interval * NSEC_PER_SEC); + uint64_t leewayInNanoSecs = (uint64_t)(leeway * NSEC_PER_SEC); + + dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.queue); + dispatch_source_set_timer(timer, [self.class wallTimeWithDate:date], intervalInNanoSecs, leewayInNanoSecs); + dispatch_source_set_event_handler(timer, block); + dispatch_resume(timer); + + return [RACDisposable disposableWithBlock:^{ + dispatch_source_cancel(timer); + }]; +} +``` + +方法使用 `dispatch_source_t` 以及定时器,完成了每隔一段时间需要执行任务的需求。 + +### RACSubscriptionScheduler + +最后的 `RACSubscriptionScheduler` 是 ReactiveCocoa 中一个比较特殊的调度器,所有 ReactiveCocoa 中的订阅事件都会在 `RACSubscriptionScheduler` 调度器上进行;而它是通过封装两个调度器实现的: + +![RACSubscriptionSchedule](images/RACScheduler/RACSubscriptionScheduler.png) + +> `backgroundScheduler` 是一个优先级为 `RACSchedulerPriorityDefault` 的串行队列。 + +`RACSubscriptionScheduler` 本身不提供任何的调度功能,它会根据当前状态选择持有的两个调度器中的一个执行任务;首先判断当前线程是否存在 `currentScheduler`,如果不存在的话才会在 `backgroundScheduler` 执行任务。 + +```objectivec +- (RACDisposable *)schedule:(void (^)(void))block { + if (RACScheduler.currentScheduler == nil) return [self.backgroundScheduler schedule:block]; + block(); + return nil; +} + +- (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block { + RACScheduler *scheduler = RACScheduler.currentScheduler ?: self.backgroundScheduler; + return [scheduler after:date schedule:block]; +} + +- (RACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block { + RACScheduler *scheduler = RACScheduler.currentScheduler ?: self.backgroundScheduler; + return [scheduler after:date repeatingEvery:interval withLeeway:leeway schedule:block]; +} +``` + +`RACSubscriptionScheduler` 作为一个私有类,我们并不能直接在 ReactiveCocoa 外部使用它,需要通过私有方法 `+subscriptionScheduler` 获取这个调度器: + +```objectivec ++ (RACScheduler *)subscriptionScheduler { + static dispatch_once_t onceToken; + static RACScheduler *subscriptionScheduler; + dispatch_once(&onceToken, ^{ + subscriptionScheduler = [[RACSubscriptionScheduler alloc] init]; + }); + + return subscriptionScheduler; +} +``` + +## 总结 + +`RACScheduler` 在某些方面与 GCD 中的队列十分相似,与 GCD 中的队列不同的有两点,第一,它可以通过 `RACDisposable` 对执行中的任务进行取消,第二是 `RACScheduler` 中任务的执行都是线性的;与此同时 `RACScheduler` 也与 `NSOperationQueue` 非常类似,但是它并不支持对调度的任务进行**重排序**以及实现任务与任务之间的**依赖**关系。 + +## References + +> Github Repo:[iOS-Source-Code-Analyze](https://github.com/draveness/iOS-Source-Code-Analyze) +> +> Follow: [Draveness · GitHub](https://github.com/Draveness) +> +> Source: http://draveness.me/racscheduler + diff --git a/contents/ReactiveObjC/RACSequence.md b/contents/ReactiveObjC/RACSequence.md new file mode 100644 index 0000000..4e35871 --- /dev/null +++ b/contents/ReactiveObjC/RACSequence.md @@ -0,0 +1,462 @@ +# Pull-Driven 的数据流 RACSequence + +ReactiveCocoa 在设计上很大程度借鉴了 Reactive Extension 中的概念,可以说 ReactiveCocoa 是 Rx 在 Objective-C 语言中的实现。 + +在 Rx 中主要的两个概念*信号*和*序列*都在 ReactiveCocoa 中有着相对应的组件 `RACSignal` 和 `RACSequence`,上一篇文章已经对前者有一个简单的介绍,而这篇文章主要会介绍后者,也就是 `RACSequence`。 + +## Push-Driven & Pull-Driven + +虽然这篇文章主要介绍 `RACSequence`,但是在介绍它之前,笔者想先就推驱动(push-driven)和拉驱动(pull-driven)这两个概念做一点简单的说明。 + +`RACSignal` 和 `RACSequence` 都是 `RACStream` 的子类,它们不仅共享了来自父类的很多方法,也都表示数据流。 + +![RACSignal - RACSequence](./images/RACSequence/RACSignal%20-%20RACSequence.png) + +`RACSignal` 和 `RACSequence` 最大区别就是: + ++ `RACSignal` 是推驱动的,也就是在每次信号中的出现新的数据时,所有的订阅者都会自动接受到最新的值; ++ `RACSequence` 作为推驱动的数据流,在改变时并不会通知使用当前序列的对象,只有使用者再次从这个 `RACSequence` 对象中获取数据才能更新,它的更新是需要使用者自己拉取的。 + +由于拉驱动在数据改变时,并不会主动推送给『订阅者』,所以往往适用于简化集合类对象等操作,相比于推驱动,它的适应场合较少。 + +![Usage for RACSignal - RACSequence Copy](./images/RACSequence/Usage%20for%20RACSignal%20-%20RACSequence%20Copy.png) + +> 图片中的内容来自 [Reactive​Cocoa · NSHipster](http://nshipster.com/reactivecocoa/) 中。 + +## 预加载与延迟加载 + +在 `RACSequence` 中还涉及到另外一对概念,也就是预加载和延迟加载(也叫懒加载);如果你之前学习过 Lisp 这门编程语言,那么你一定知道 Lisp 中有两种列表,一种是正常的列表 List,另一种叫做流 Stream,这两者的主要区别就是**流的加载是延迟加载的**,只有在真正使用数据时才会计算数据的内容。 + +![List-and-Strea](./images/RACSequence/List-and-Stream.png) + +> 由于流是懒加载的,这也就是说它可以**表示无穷长度的列表**。 + +Stream 由两部分组成,分别是 `head` 和 `tail`,两者都是在访问时才会计算,在上图前者是一个数字,而后者会是另一个 Stream 或者 `nil`。 + +```objectivec +@interface RACSequence<__covariant ValueType> : RACStream <NSCoding, NSCopying, NSFastEnumeration> + +@property (nonatomic, strong, readonly, nullable) ValueType head; +@property (nonatomic, strong, readonly, nullable) RACSequence<ValueType> *tail; + +@end +``` + +`RACSequence` 头文件的中定义能够帮助我们更好理解递归的序列以及 `head` 和 `tail` 的概念,`head` 是一个值,`tail` 是一个 `RACSequence` 对象。 + +## RACSequence 简介 + +了解了几个比较重要的概念之后,就可以进入正题了,先看一下在 ReactiveCocoa 中,`RACSequence` 都有哪些子类: + +![RACSequence - Subclasses](./images/RACSequence/RACSequence%20-%20Subclasses.png) + +`RACSequence` 总共有九个子类,这篇文章不会覆盖其中所有的内容,只会简单介绍其中的几个;不过,我们先从父类 `RACSequence` 开始。 + +### return 和 bind 方法 + +与介绍 `RACSignal` 时一样,这里我们先介绍两个 `RACSequence` 必须覆写的方法,第一个就是 `+return:` + +```objectivec ++ (RACSequence *)return:(id)value { + return [RACUnarySequence return:value]; +} +``` + +`+return:` 方法用到了 `RACSequence` 的子类 `RACUnarySequence` 私有类,这个类在外界是不可见的,其实现非常简单,只是将原来的 `value` 包装成了一个简单的 `RACUnarySequence` 对象: + +```objectivec ++ (RACUnarySequence *)return:(id)value { + RACUnarySequence *sequence = [[self alloc] init]; + sequence.head = value; + return [sequence setNameWithFormat:@"+return: %@", RACDescription(value)]; +} +``` + +这样在访问 `head` 时可以获取到传入的 `value`;在访问 `tail` 时只需要返回 `nil`: + +```objectivec +- (RACSequence *)tail { + return nil; +} +``` + +整个 `RACUnarySequence` 也只是对 `value` 简单封装成一个 `RACSequence` 对象而已: + +![RACUnarySequence](./images/RACSequence/RACUnarySequence.png) + +相比于 `+return:` 方法的简单实现,`-bind:` 的实现就复杂多了: + +```objectivec +- (RACSequence *)bind:(RACSequenceBindBlock (^)(void))block { + RACSequenceBindBlock bindBlock = block(); + return [[self bind:bindBlock passingThroughValuesFromSequence:nil] setNameWithFormat:@"[%@] -bind:", self.name]; +} +``` + +首先是对 `-bind:` 方法进行一次转发,将控制权交给 `-bind:passingThroughValuesFromSequence:` 方法中: + +```objectivec +- (RACSequence *)bind:(RACSequenceBindBlock)bindBlock passingThroughValuesFromSequence:(RACSequence *)passthroughSequence { + __block RACSequence *valuesSeq = self; + __block RACSequence *current = passthroughSequence; + __block BOOL stop = NO; + + RACSequence *sequence = [RACDynamicSequence sequenceWithLazyDependency:^ id { + while (current.head == nil) { + if (stop) return nil; + id value = valuesSeq.head; + if (value == nil) { + stop = YES; + return nil; + } + current = (id)bindBlock(value, &stop); + if (current == nil) { + stop = YES; + return nil; + } + + valuesSeq = valuesSeq.tail; + } + return nil; + } headBlock:^(id _) { + return current.head; + } tailBlock:^ id (id _) { + if (stop) return nil; + + return [valuesSeq bind:bindBlock passingThroughValuesFromSequence:current.tail]; + }]; + + sequence.name = self.name; + return sequence; +} +``` + +这个非常复杂的方法实际作用就是创建了一个私有类 `RACDynamicSequence` 对象,使用的初始化方法也都是私有的 `+sequenceWithLazyDependency:headBlock:tailBlock:`: + +```objectivec ++ (RACSequence *)sequenceWithLazyDependency:(id (^)(void))dependencyBlock headBlock:(id (^)(id dependency))headBlock tailBlock:(RACSequence *(^)(id dependency))tailBlock { + RACDynamicSequence *seq = [[RACDynamicSequence alloc] init]; + seq.headBlock = [headBlock copy]; + seq.tailBlock = [tailBlock copy]; + seq.dependencyBlock = [dependencyBlock copy]; + seq.hasDependency = YES; + return seq; +} +``` + +在使用 `RACDynamicSequence` 中的元素时,无论是 `head` 还是 `tail` 都会用到在初始化方法中传入的三个 block: + +```objectivec +- (id)head { + @synchronized (self) { + id untypedHeadBlock = self.headBlock; + if (untypedHeadBlock == nil) return _head; + + if (self.hasDependency) { + if (self.dependencyBlock != nil) { + _dependency = self.dependencyBlock(); + self.dependencyBlock = nil; + } + + id (^headBlock)(id) = untypedHeadBlock; + _head = headBlock(_dependency); + } else { + id (^headBlock)(void) = untypedHeadBlock; + _head = headBlock(); + } + + self.headBlock = nil; + return _head; + } +} +``` + +`head` 的计算依赖于 `self.headBlock` 和 `self.dependencyBlock`; + +而 `tail` 的计算也依赖于 `self.headBlock` 和 `self.dependencyBlock`,只是 `tail` 会执行 `tailBlock` 返回另一个 `RACDynamicSequence` 的实例: + +```objectivec +^ id (id _) { + return [valuesSeq bind:bindBlock passingThroughValuesFromSequence:current.tail]; +} +``` + +这里通过一段代码更好的了解 `-bind:` 方法是如何使用的: + +```objectivec +RACSequence *sequence = [RACSequence sequenceWithHeadBlock:^id _Nullable{ + return @1; +} tailBlock:^RACSequence * _Nonnull{ + return [RACSequence sequenceWithHeadBlock:^id _Nullable{ + return @2; + } tailBlock:^RACSequence * _Nonnull{ + return [RACSequence return:@3]; + }]; +}]; +RACSequence *bindSequence = [sequence bind:^RACSequenceBindBlock _Nonnull{ + return ^(NSNumber *value, BOOL *stop) { + NSLog(@"RACSequenceBindBlock: %@", value); + value = @(value.integerValue * 2); + return [RACSequence return:value]; + }; +}]; +NSLog(@"sequence: head = (%@), tail=(%@)", sequence.head, sequence.tail); +NSLog(@"BindSequence: head = (%@), tail=(%@)", bindSequence.head, bindSequence.tail); +``` + +在上面的代码中,我们使用 `+sequenceWithHeadBlock:tailBlock:` 这个唯一暴露出来的初始化方法创建了一个如下图所示的 `RACSequence`: + +![RACSequence-Instance](./images/RACSequence/RACSequence-Instance.png) + +> 图中展示了完整的 `RACSequence` 对象的值,其中的内容暂时都是 `unresolved` 的。 + +上述代码在运行之后,会打印出如下内容: + +```objectivec +sequence: head = (1), tail=(<RACDynamicSequence: 0x60800009eb40>{ name = , head = (unresolved), tail = (unresolved) }) +RACSequenceBindBlock: 1 +BindSequence: head = (2), tail=(<RACDynamicSequence: 0x608000282940>{ name = , head = (unresolved), tail = (unresolved) }) +``` + +无论是 `sequence` 还是 `bindSequence`,其中的 `tail` 部分都是一个 `RACDynamicSequence` 对象,并且其中的 `head` 和 `tail` 部分都是 `unresolved`。 + +![Unsolved-RACSequence-Instance](./images/RACSequence/Unsolved-RACSequence-Instance.png) + +在上面的代码中 `RACSequenceBindBlock` 的执行也是惰性的,只有在获取 `bindSequence.head` 时,才会执行将数字转换成 `RACUnarySequence` 对象,最后通过 `head` 属性取出来。 + +### lazySequence 和 eagerSequence + +上一节的代码中展示的所有序列都是惰性的,而在整个 ReactiveCocoa 中,所有的 `RACSequence` 对象在**默认情况**下都是惰性的,序列中的值只有在真正需要使用时才会被展开,在其他时间都是 **unresolved**。 + +`RACSequence` 中定义了两个分别获取 `lazySequence` 和 `eagerSequence` 的属性: + +```objectivec +@property (nonatomic, copy, readonly) RACSequence<ValueType> *eagerSequence; +@property (nonatomic, copy, readonly) RACSequence<ValueType> *lazySequence; +``` + +> 笔者一直认为在大多数情况下,在客户端上的惰性求值都是没有太多意义的,如果一个序列的**长度没有达到比较庞大的数量级或者说计算量比较小**,我们完全都可以使用贪婪求值(Eager Evaluation)的方式尽早获得结果; +> +> 同样,在数量级和计算量不需要考虑时,我们也不需要考虑是否应该设计成哪种求值方式,只需要使用默认行为。 + +与上一节相同,在这里使用相同的代码创建一个 `RACSequence` 对象: + +```objectivec +RACSequence *sequence = [RACSequence sequenceWithHeadBlock:^id _Nullable{ + return @1; +} tailBlock:^RACSequence * _Nonnull{ + return [RACSequence sequenceWithHeadBlock:^id _Nullable{ + return @2; + } tailBlock:^RACSequence * _Nonnull{ + return [RACSequence return:@3]; + }]; +}]; + +NSLog(@"Lazy: %@", sequence.lazySequence); +NSLog(@"Eager: %@", sequence.eagerSequence); +NSLog(@"Lazy: %@", sequence.lazySequence); +``` + +然后分别三次打印出当前对象的 `lazySequence` 和 `eagerSequence` 中的值: + +```objectivec +Lazy: <RACDynamicSequence: 0x608000097160> +{ name = , head = (unresolved), tail = (unresolved) } +Eager: <RACEagerSequence: 0x600000035de0> +{ name = , array = ( + 1, + 2, + 3 +) } +Lazy: <RACDynamicSequence: 0x608000097160> +{ name = , head = 1, tail = <RACDynamicSequence: 0x600000097070> + { name = , head = 2, tail = <RACUnarySequence: 0x600000035f00> + { name = , head = 3 } } } +``` + +在第一调用 `sequence.lazySequence` 时,因为元素没有被使用,惰性序列的 `head` 和 `tail` 都为 unresolved;而在 `sequence.eagerSequence` 调用后,访问了序列中的所有元素,在这之后再打印 `sequence.lazySequence` 中的值就都不是 unresolved 的了。 + +![RACSequence-Status-Before-And-After-Executed](./images/RACSequence/RACSequence-Status-Before-And-After-Executed.png) + +这种情况的出现不难理解,不过因为 `lazySequence` 和 `eagerSequence` 是 `RACSequence` 的方法,所以我们可以在任意子类的实例包括 `RACEagerSequence` 和非惰性序列上调用它们,这就会出现以下的多种情况: + +![EagerSequence - LazySequence](./images/RACSequence/EagerSequence%20-%20LazySequence.png) + +总而言之,调用过 `eagerSequence` 的序列的元素已经不再是 `unresolved` 了,哪怕再调用 `lazySequence` 方法,读者可以自行实验验证这里的结论。 + +### 操作 RACSequence + +`RACStream` 为 `RACSequence` 提供了很多基本的操作,`-map:`、`-filter:`、`-ignore:` 等等,因为这些方法的实现都基于 `-bind:`,而 `-bind:` 方法的执行是惰性的,所以在调用上述方法之后返回的 `RACSequence` 中所有的元素都是 unresolved 的,需要在访问之后才会计算并展开: + +```objectivec +RACSequence *sequence = [@[@1, @2, @3].rac_sequence map:^id _Nullable(NSNumber * _Nullable value) { + return @(value.integerValue * value.integerValue); +}]; +NSLog(@"%@", sequence); -> <RACDynamicSequence: 0x60800009ad10>{ name = , head = (unresolved), tail = (unresolved) } +NSLog(@"%@", sequence.eagerSequence); -> <RACEagerSequence: 0x60800002bfc0>{ name = , array = (1, 4, 9) } +``` + +除了从 `RACStream` 中继承的一些方法,在 `RACSequence` 类中也有一些自己实现的方法,比如说 `-foldLeftWithStart:reduce:` 方法: + +```objectivec +- (id)foldLeftWithStart:(id)start reduce:(id (^)(id, id))reduce { + if (self.head == nil) return start; + + for (id value in self) { + start = reduce(start, value); + } + + return start; +} +``` + +使用简单的 `for` 循环,将序列中的数据进行『折叠』,最后返回一个结果: + +```objectivec +RACSequence *sequence = @[@1, @2, @3].rac_sequence; +NSNumber *sum = [sequence foldLeftWithStart:0 reduce:^id _Nullable(NSNumber * _Nullable accumulator, NSNumber * _Nullable value) { + return @(accumulator.integerValue + value.integerValue); +}]; +NSLog(@"%@", sum); +``` + +与上面方法相似的是 `-foldRightWithStart:reduce:` 方法,从右侧开始向左折叠整个序列,虽然过程有一些不同,但是结果还是一样的。 + +![FoldLeft - FoldRight](./images/RACSequence/FoldLeft%20-%20FoldRight.png) + +从两次方法的调用栈上来看,就能看出两者实现过程的明显区别: + +![Call-Stacks-of-FoldLeft-FoldRight](./images/RACSequence/Call-Stacks-of-FoldLeft-FoldRight.png) + ++ `foldLeft` 由于其实现是通过 `for` 循环遍历序列,所以调用栈不会展开,在循环结束之后就返回了,调用栈中只有当前方法; ++ `foldRight` 的调用栈**递归**的调用自己,直到出现了边界条件 `self.tail == nil` 后停止,左侧的调用栈也是其调用栈最深的时候,在这时调用栈的规模开始按照箭头方向缩小,直到方法返回。 + +在源代码中,你也可以看到方法在创建 `RACSequence` 的 block 中递归调用了当前的方法: + +```objectivec +- (id)foldRightWithStart:(id)start reduce:(id (^)(id, RACSequence *))reduce { + if (self.head == nil) return start; + + RACSequence *rest = [RACSequence sequenceWithHeadBlock:^{ + if (self.tail) { + return [self.tail foldRightWithStart:start reduce:reduce]; + } else { + return start; + } + } tailBlock:nil]; + + return reduce(self.head, rest); +} +``` + +### RACSequence 与 RACSignal + +虽然 `RACSequence` 与 `RACSignal` 有很多不同,但是在 ReactiveCocoa 中 `RACSequence` 与 `RACSignal` 却可以双向转换。 + +![Transform Between RACSequence - RACSigna](./images/RACSequence/Transform%20Between%20RACSequence%20-%20RACSignal.png) + +#### 将 RACSequence 转换成 RACSignal + +将 `RACSequence` 转换成 `RACSignal` 对象只需要调用一个方法。 + +![Transform-RACSequence-To-RACSigna](./images/RACSequence/Transform-RACSequence-To-RACSignal.png) + +分析其实现之前先看一下如何使用 `-signal` 方法将 `RACSequence` 转换成 `RACSignal` 对象的: + +```objectivec +RACSequence *sequence = @[@1, @2, @3].rac_sequence; +RACSignal *signal = sequence.signal; +[signal subscribeNext:^(id _Nullable x) { + NSLog(@"%@", x); +}]; +``` + +其实过程非常简单,原序列 `@[@1, @2, @3]` 中的元素会按照次序发送,可以理解为依次调用 `-sendNext:`,它可以等价于下面的代码: + +```objectivec +RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { + [subscriber sendNext:@1]; + [subscriber sendNext:@2]; + [subscriber sendNext:@3]; + [subscriber sendCompleted]; + return nil; +}]; +[signal subscribeNext:^(id _Nullable x) { + NSLog(@"%@", x); +}]; +``` + +`-signal` 方法的实现依赖于另一个实例方法 `-signalWithScheduler:`,它会在一个 `RACScheduler` 对象上发送序列中的所有元素: + +```objectivec +- (RACSignal *)signal { + return [[self signalWithScheduler:[RACScheduler scheduler]] setNameWithFormat:@"[%@] -signal", self.name]; +} + +- (RACSignal *)signalWithScheduler:(RACScheduler *)scheduler { + return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) { + __block RACSequence *sequence = self; + + return [scheduler scheduleRecursiveBlock:^(void (^reschedule)(void)) { + if (sequence.head == nil) { + [subscriber sendCompleted]; + return; + } + [subscriber sendNext:sequence.head]; + sequence = sequence.tail; + reschedule(); + }]; + }] setNameWithFormat:@"[%@] -signalWithScheduler: %@", self.name, scheduler]; +} +``` + +`RACScheduler` 并不是这篇文章准备介绍的内容,这里的代码其实相当于递归调用了 `reschedule` block,不断向 `subscriber` 发送 `-sendNext:`,直到 `RACSequence` 为空为止。 + +#### 将 RACSignal 转换成 RACSequence + +反向转换 `RACSignal` 的过程相比之下就稍微复杂一点了,我们需要连续调用两个方法,才能将它转换成 `RACSequence`。 + +![Transform RACSignal to RACSequence](./images/RACSequence/Transform%20RACSignal%20to%20RACSequence.png) + +通过一段代码来看转换过程是如何进行的: + +```objectivec +RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { + [subscriber sendNext:@1]; + [subscriber sendNext:@2]; + [subscriber sendNext:@3]; + [subscriber sendCompleted]; + return nil; +}]; +NSLog(@"%@", signal.toArray.rac_sequence); +``` + +运行上面的代码,会得到一个如下的 `RACArraySequence` 对象: + +```objectivec +<RACArraySequence: 0x608000024e80>{ name = , array = ( + 1, + 2, + 3 +) } +``` + +在这里不想过多介绍其实现原理,我们只需要知道这里使用了 `RACStream` 提供的操作『收集』了信号发送过程中的发送的所有对象 `@1`、`@2`、`@3` 就可以了。 + +## 总结 + +相比于 `RACSignal` 来说,虽然 `RACSequence` 有很多的子类,但是它的用途和实现复杂度都少很多,这主要是因为它是 Pull-Driven 的,只有在使用时才会更新,所以我们一般只会使用 `RACSequence` 操作数据流,使用 `map`、`filter`、`flattenMap` 等方法快速操作数据。 + +## References + ++ [Reactive​Cocoa · NSHipster](http://nshipster.com/reactivecocoa/) ++ [What is the difference between RACSequence and RACSignal](http://stackoverflow.com/questions/28952900/what-is-the-difference-between-racsequence-and-racsignal) ++ [ReactiveCocoa Design Patterns](http://rcdp.io/Signal.html) + +> Github Repo:[iOS-Source-Code-Analyze](https://github.com/draveness/iOS-Source-Code-Analyze) +> +> Follow: [Draveness · GitHub](https://github.com/Draveness) +> +> Source: http://draveness.me/racsignal + + diff --git a/contents/ReactiveObjC/RACSignal.md b/contents/ReactiveObjC/RACSignal.md new file mode 100644 index 0000000..bc1c3de --- /dev/null +++ b/contents/ReactiveObjC/RACSignal.md @@ -0,0 +1,879 @@ +# 『状态』驱动的世界:ReactiveCocoa + +![RACSignal-Banner](./images/RACSignal/RACSignal-Banner.png) + +这篇以及之后的文章主要会对 ReactiveObjc v2.1.2 的实现进行分析,从最简单的例子中了解 ReactiveCocoa 的工作原理以及概念,也是笔者个人对于 RAC 学习的总结与理解。本文主要会围绕 RAC 中核心概念 `RACSignal` 展开,详细了解其底层实现。 + +## 状态驱动 + +2015 年的夏天的时候,做了几个简单的开源框架,想做点其它更有意思的框架却没什么思路,就开始看一些跟编程没有太大关系的书籍。 + +![out-of-contro](./images/RACSignal/out-of-control.jpg) + +其中一本叫做《失控》给了我很大的启发,其中有一则故事是这样的: + +> 布鲁克斯开展了一个雄心勃勃的研究生课题项目,研发更接近昆虫而非恐龙的机器人。 +> +> 布鲁克斯的设想在一个叫「成吉思」的机巧装置上成形。成吉思有橄榄球大小,像只蟑螂似的。布鲁克斯把他的精简理念发挥到了极致。小成吉思有 6 条腿却没有一丁点儿可以称为「脑」的东西。所有 12 个电机和 21 个传感器分布在没有中央处理器的可解耦网络上。然而这 12 个充当肌肉的电机和 21 个传感器之间的交互作用居然产生了令人惊叹的复杂性和类似生命体的行为。 > +> 成吉思的每条小细腿都在自顾自地工作,和其余的腿毫无关系。每条腿都通过自己的一组神经元——一个微型处理器——来控制其动作。每条腿只需管好自己!对成吉思来说,走路是一个团队合作项目,至少有六个小头脑在工作。它体内其余更微小的脑力则负责腿与腿之间的通讯。昆虫学家说这正是蚂蚁和蟑螂的解决之道——这些爬行昆虫的足肢上的神经元负责为该足肢进行思考。 +> +> ------ 《失控》第三章·第二节 快速、廉价、失控 + + +书中对于机器人的介绍比较冗长,在这里就简单总结一下:机器人的每一条腿都单独进行工作,通过传感器感应的**状态**做出响应: + ++ 如果腿抬起来了,那么它要落下去; ++ 如果腿在向前动,要让另外五条腿距离它远一点; + +这种去中心化的方式,简化了整个系统的构造,使得各个组件只需要关心状态,以及状态对应的动作;不再需要一个中枢系统来组织、管理其它的组件,并负责大多数的业务逻辑。这种自底向下的、状态驱动的构建方式能够使用多个较小的组件,减少臃肿的中枢出现的可能性,从而降低系统的复杂度。 + +## ReactiveCocoa 与信号 + +ReactiveCocoa 对于状态的理解与《失控》一书中十分类似,将原有的各种设计模式,包括代理、Target/Action、通知中心以及观察者模式各种『输入』,都抽象成了信号(也可以理解为状态流)让单一的组件能够对自己的响应动作进行控制,简化了视图控制器的负担。 + +在 ReactiveCocoa 中最重要的信号,也就是 `RACSignal` 对象是这一篇文章介绍的核心;文章中主要会介绍下面的代码片段出现的内容: + +```objectivec +RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { + [subscriber sendNext:@1]; + [subscriber sendNext:@2]; + [subscriber sendCompleted]; + return [RACDisposable disposableWithBlock:^{ + NSLog(@"dispose"); + }]; +}]; +[signal subscribeNext:^(id _Nullable x) { + NSLog(@"%@", x); +}]; +``` + +在上述代码执行时,会在控制台中打印出以下内容: + +```c +1 +2 +dispose +``` + +代码片段基本都是围绕 `RACSignal` 类进行的,文章会分四部分对上面的代码片段的工作流程进行简单的介绍: + ++ 简单了解 `RACSignal` ++ 信号的创建 ++ 信号的订阅与发送 ++ 订阅的回收过程 + +## RACSignal 简介 + +`RACSignal` 其实是抽象类 `RACStream` 的子类,在整个 ReactiveObjc 工程中有另一个类 `RACSequence` 也继承自抽象类 `RACStream`: + +![RACSignal-Hierachy](./images/RACSignal/RACSignal-Hierachy.png) + +`RACSignal` 可以说是 ReactiveCocoa 中的核心类,也是最重要的概念,整个框架围绕着 `RACSignal` 的概念进行组织,对 `RACSignal` 最简单的理解就是它表示一连串的状态: + +![What-is-RACSigna](./images/RACSignal/What-is-RACSignal.png) + +在状态改变时,对应的订阅者 `RACSubscriber` 就会收到通知执行相应的指令,在 ReactiveCocoa 的世界中所有的消息都是通过信号的方式来传递的,原有的设计模式都会简化为一种模型,这篇文章作为 ReactiveCocoa 系列的第一篇文章并不会对这些问题进行详细的展开和介绍,只会对 `RACSignal` 使用过程的原理进行简单的分析。 + +这一小节会对 `RACStream` 以及 `RACSignal` 中与 `RACStream` 相关的部分进行简单的介绍。 + +### RACStream + +`RACStream` 作为抽象类本身不提供方法的实现,其实现内部原生提供的而方法都是抽象方法,会在调用时直接抛出异常: + +```objectivec ++ (__kindof RACStream *)empty { + NSString *reason = [NSString stringWithFormat:@"%@ must be overridden by subclasses", NSStringFromSelector(_cmd)]; + @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:reason userInfo:nil]; +} + +- (__kindof RACStream *)bind:(RACStreamBindBlock (^)(void))block; ++ (__kindof RACStream *)return:(id)value; +- (__kindof RACStream *)concat:(RACStream *)stream; +- (__kindof RACStream *)zipWith:(RACStream *)stream; +``` + +![RACStream-AbstractMethod](./images/RACSignal/RACStream-AbstractMethod.png) + +上面的这些抽象方法都需要子类覆写,不过 `RACStream` 在 `Operations` 分类中使用上面的抽象方法提供了丰富的内容,比如说 `-flattenMap:` 方法: + +```objectivec +- (__kindof RACStream *)flattenMap:(__kindof RACStream * (^)(id value))block { + Class class = self.class; + + return [[self bind:^{ + return ^(id value, BOOL *stop) { + id stream = block(value) ?: [class empty]; + NSCAssert([stream isKindOfClass:RACStream.class], @"Value returned from -flattenMap: is not a stream: %@", stream); + + return stream; + }; + }] setNameWithFormat:@"[%@] -flattenMap:", self.name]; +} +``` + +其他方法比如 `-skip:`、`-take:`、`-ignore:` 等等实例方法都构建在这些抽象方法之上,只要子类覆写了所有抽象方法就能自动获得所有的 `Operation` 分类中的方法。 + +![RACStream-Operation](./images/RACSignal/RACStream-Operation.png) + +### RACSignal 与 Monad + +> 如果你对 Monad 有所了解,那么你应该知道 `bind` 和 `return` 其实是 Monad 中的概念,但 Monad 并不是本篇文章所覆盖的内容,并不会具体解释它到底是什么。 + +ReactiveCocoa 框架中借鉴了很多其他平台甚至语言中的概念,包括微软中的 Reactive Extension 以及 Haskell 中的 Monad,`RACStream` 提供的抽象方法中的 `+return:` 和 `-bind:` 就与 Haskell 中 Monad 完全一样。 + +> 很多人都说 Monad 只是一个自函子范畴上的一个幺半群而已;在笔者看来这种说法虽然是正确的,不过也很扯淡,这句话解释了还是跟没解释一样,如果有人再跟你用这句话解释 Monad,我觉得你最好的回应就是买一本范畴论糊他一脸。如果真的想了解 Haskell 中的 Monad 到底是什么?可以从代码的角度入手,多写一些代码就明白了,这个概念理解起来其实根本没什么困难的,当然也可以看一下 [A Fistful of Monads](http://learnyouahaskell.com/a-fistful-of-monads),写写其中的代码,会对 Monad 有自己的认知,当然,请不要再写一篇解释 Monad 的教程了(手动微笑)。 + +首先来看一下 `+return` 方法的 [实现](https://github.com/ReactiveCocoa/ReactiveObjC/blob/1180ab256000573ef82141e5d40e9b9c35dfd69c/ReactiveObjC/RACSignal.m#L89-L91): + +```objectivec ++ (RACSignal *)return:(id)value { + return [RACReturnSignal return:value]; +} +``` + +该方法接受一个 `NSObject` 对象,并返回一个 `RACSignal` 的实例,它会将一个 UIKit 世界的对象 `NSObject` 转换成 ReactiveCocoa 中的 `RACSignal`: + +![RACSignal-Return](./images/RACSignal/RACSignal-Return.png) + +而 `RACReturnSignal` 也仅仅是把 `NSObject` 对象包装一下,并没有做什么复杂的事情: + +```objectivec ++ (RACSignal *)return:(id)value { + RACReturnSignal *signal = [[self alloc] init]; + signal->_value = value; + return signal; +} +``` + +但是 `-bind:` 方法的 [实现](https://github.com/ReactiveCocoa/ReactiveObjC/blob/1180ab256000573ef82141e5d40e9b9c35dfd69c/ReactiveObjC/RACSignal.m#L93-L176) 相比之下就十分复杂了: + +```objectivec +- (RACSignal *)bind:(RACSignalBindBlock (^)(void))block { + return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) { + RACSignalBindBlock bindingBlock = block(); + return [self subscribeNext:^(id x) { + BOOL stop = NO; + id signal = bindingBlock(x, &stop); + + if (signal != nil) { + [signal subscribeNext:^(id x) { + [subscriber sendNext:x]; + } error:^(NSError *error) { + [subscriber sendError:error]; + } completed:^{ + [subscriber sendCompleted]; + }]; + } + if (signal == nil || stop) { + [subscriber sendCompleted]; + } + } error:^(NSError *error) { + [subscriber sendError:error]; + } completed:^{ + [subscriber sendCompleted]; + }]; + }] setNameWithFormat:@"[%@] -bind:", self.name]; +} +``` + +> 笔者在这里对 `-bind:` 方法进行了大量的省略,省去了其中对各种 `RACDisposable` 的处理过程。 + +`-bind:` 方法会在原信号每次发出消息时,都执行 `RACSignalBindBlock` 对原有的信号中的消息进行**变换**生成一个新的信号: + +![RACSignal-Bind](./images/RACSignal/RACSignal-Bind.png) + +> 在原有的 `RACSignal` 对象上调用 `-bind:` 方法传入 `RACSignalBindBlock`,图示中的右侧就是具体的执行过程,原信号在变换之后变成了新的蓝色的 `RACSignal` 对象。 + +`RACSignalBindBlock` 可以简单理解为一个接受 `NSObject` 对象返回 `RACSignal` 对象的函数: + +```objectivec +typedef RACSignal * _Nullable (^RACSignalBindBlock)(id _Nullable value, BOOL *stop); +``` + +其函数签名可以理解为 `id -> RACSignal`,然而这种函数是无法直接对 `RACSignal` 对象进行变换的;不过通过 `-bind:` 方法就可以使用这种函数操作 `RACSignal`,其实现如下: + +1. 将 `RACSignal` 对象『解包』出 `NSObject` 对象; +2. 将 `NSObject` 传入 `RACSignalBindBlock` 返回 `RACSignal`。 + +如果在不考虑 `RACSignal` 会发出错误或者完成信号时,`-bind:` 可以简化为更简单的形式: + +```objectivec +- (RACSignal *)bind:(RACSignalBindBlock (^)(void))block { + return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) { + RACSignalBindBlock bindingBlock = block(); + return [self subscribeNext:^(id x) { + BOOL stop = NO; + [bindingBlock(x, &stop) subscribeNext:^(id x) { + [subscriber sendNext:x]; + }]; + }]; + }] setNameWithFormat:@"[%@] -bind:", self.name]; +} +``` + +调用 `-subscribeNext:` 方法订阅当前信号,将信号中的状态解包,然后将原信号中的状态传入 `bindingBlock` 中并订阅返回的新的信号,将生成的新状态 `x` 传回原信号的订阅者。 + +这里通过两个简单的例子来了解 `-bind:` 方法的作用: + +```objectivec +RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { + [subscriber sendNext:@1]; + [subscriber sendNext:@2]; + [subscriber sendNext:@3]; + [subscriber sendNext:@4]; + [subscriber sendCompleted]; + return nil; +}]; +RACSignal *bindSignal = [signal bind:^RACSignalBindBlock _Nonnull{ + return ^(NSNumber *value, BOOL *stop) { + value = @(value.integerValue * value.integerValue); + return [RACSignal return:value]; + }; +}]; +[signal subscribeNext:^(id _Nullable x) { + NSLog(@"signal: %@", x); +}]; +[bindSignal subscribeNext:^(id _Nullable x) { + NSLog(@"bindSignal: %@", x); +}]; +``` + +上面的代码中直接使用了 `+return:` 方法将 `value` 打包成了 `RACSignal *` 对象: + +![Before-After-Bind-RACSigna](./images/RACSignal/Before-After-Bind-RACSignal.png) + +> 在 BindSignal 中的每一个数字其实都是由一个 `RACSignal` 包裹的,这里没有画出,在下一个例子中,读者可以清晰地看到其中的区别。 + +上图简要展示了变化前后的信号中包含的状态,在运行上述代码时,会在终端中打印出: + +```objectivec +signal: 1 +signal: 2 +signal: 3 +signal: 4 +bindSignal: 1 +bindSignal: 4 +bindSignal: 9 +bindSignal: 16 +``` + +这是一个最简单的例子,直接使用 `-return:` 打包 `NSObject` 返回一个 `RACSignal`,接下来用一个更复杂的例子来帮助我们更好的了解 `-bind:` 方法: + +```objectivec +RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { + [subscriber sendNext:@1]; + [subscriber sendNext:@2]; + [subscriber sendCompleted]; + return nil; +}]; +RACSignal *bindSignal = [signal bind:^RACSignalBindBlock _Nonnull{ + return ^(NSNumber *value, BOOL *stop) { + NSNumber *returnValue = @(value.integerValue * value.integerValue); + return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { + for (NSInteger i = 0; i < value.integerValue; i++) [subscriber sendNext:returnValue]; + [subscriber sendCompleted]; + return nil; + }]; + }; +}]; +[bindSignal subscribeNext:^(id _Nullable x) { + NSLog(@"%@", x); +}]; +``` + +下图相比上面例子中的图片更能精确的表现出 `-bind:` 方法都做了什么: + +![Before-After-Bind-RACSignal-Complicated](./images/RACSignal/Before-After-Bind-RACSignal-Complicated.png) + +信号中原有的状态经过 `-bind:` 方法中传入 `RACSignalBindBlock` 的处理实际上返回了**多个** `RACSignal`。 + +在源代码的注释中清楚地写出了方法的实现过程: + +1. 订阅原信号中的值; +2. 将原信号发出的值传入 `RACSignalBindBlock` 进行转换; +3. 如果 `RACSignalBindBlock` 返回一个信号,就会订阅该信号并将信号中的所有值传给订阅者 `subscriber`; +4. 如果 `RACSignalBindBlock` 请求终止信号就会向**原**信号发出 `-sendCompleted` 消息; +5. 当**所有**信号都完成时,会向订阅者发送 `-sendCompleted`; +6. 无论何时,如果信号发出错误,都会向订阅者发送 `-sendError:` 消息。 + +如果想要了解 `-bind:` 方法在执行的过程中是如何处理订阅的清理和销毁的,可以阅读文章最后的 [-bind: 中对订阅的销毁]() 部分。 + +## 信号的创建 + +信号的创建过程十分简单,`-createSignal:` 是推荐的创建信号的方法,方法其实只做了一次转发: + +```objectivec ++ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe { + return [RACDynamicSignal createSignal:didSubscribe]; +} + ++ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe { + RACDynamicSignal *signal = [[self alloc] init]; + signal->_didSubscribe = [didSubscribe copy]; + return [signal setNameWithFormat:@"+createSignal:"]; +} +``` + +该方法其实只是创建了一个 `RACDynamicSignal` 实例并保存了传入的 `didSubscribe` 代码块,在每次有订阅者订阅当前信号时,都会执行一遍,向订阅者发送消息。 + +### RACSignal 类簇 + +虽然 `-createSignal:` 的方法签名上返回的是 `RACSignal` 对象的实例,但是实际上这里返回的是 `RACDynamicSignal`,也就是 `RACSignal` 的子类;同样,在 ReactiveCocoa 中也有很多其他的 `RACSignal` 子类。 + +使用类簇的方式设计的 `RACSignal` 在创建实例时可能会返回 `RACDynamicSignal`、`RACEmptySignal`、`RACErrorSignal` 和 `RACReturnSignal` 对象: + +![RACSignal-Subclasses](./images/RACSignal/RACSignal-Subclasses.png) + +其实这几种子类并没有对原有的 `RACSignal` 做出太大的改变,它们的创建过程也不是特别的复杂,只需要调用 `RACSignal` 不同的类方法: + +![RACSignal-Instantiate-Object](./images/RACSignal/RACSignal-Instantiate-Object.png) + +`RACSignal` 只是起到了一个代理的作用,最后的实现过程还是会指向对应的子类: + +```objectivec ++ (RACSignal *)error:(NSError *)error { + return [RACErrorSignal error:error]; +} + ++ (RACSignal *)empty { + return [RACEmptySignal empty]; +} + ++ (RACSignal *)return:(id)value { + return [RACReturnSignal return:value]; +} +``` + +以 `RACReturnSignal` 的创建过程为例: + +```objectivec ++ (RACSignal *)return:(id)value { + RACReturnSignal *signal = [[self alloc] init]; + signal->_value = value; + return signal; +} +``` + +这个信号的创建过程和 `RACDynamicSignal` 的初始化过程一样,都非常简单;只是将传入的 `value` 简单保存一下,在有其他订阅者 `-subscribe:` 时,向订阅者发送 `value`: + +```objectivec +- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber { + return [RACScheduler.subscriptionScheduler schedule:^{ + [subscriber sendNext:self.value]; + [subscriber sendCompleted]; + }]; +} +``` + +`RACEmptySignal` 和 `RACErrorSignal` 的创建过程也异常的简单,只是对传入的数据进行简单的存储,然后在订阅时发送出来: + +```objectivec +// RACEmptySignal ++ (RACSignal *)empty { + return [[[self alloc] init] setNameWithFormat:@"+empty"]; +} + +- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber { + return [RACScheduler.subscriptionScheduler schedule:^{ + [subscriber sendCompleted]; + }]; +} + +// RACErrorSignal ++ (RACSignal *)error:(NSError *)error { + RACErrorSignal *signal = [[self alloc] init]; + signal->_error = error; + return signal; +} + +- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber { + return [RACScheduler.subscriptionScheduler schedule:^{ + [subscriber sendError:self.error]; + }]; +} +``` + +这两个创建过程的唯一区别就是一个发送的是『空值』,另一个是 `NSError` 对象。 + +## 信号的订阅与信息的发送 + +ReactiveCocoa 中信号的订阅与信息的发送过程主要是由 `RACSubscriber` 类来处理的,而这也是信号的处理过程中最重要的一部分,这一小节会先分析整个工作流程,之后会深入代码的实现。 + +![RACSignal-Subcribe-Process](./images/RACSignal/RACSignal-Subcribe-Process.png) + +在信号创建之后调用 `-subscribeNext:` 方法返回一个 `RACDisposable`,然而这不是这一流程关心的重点,在订阅过程中生成了一个 `RACSubscriber` 对象,向这个对象发送消息 `-sendNext:` 时,就会向所有的订阅者发送消息。 + +### 信号的订阅 + +信号的订阅与 `-subscribe:` 开头的一系列方法有关: + +![RACSignal-Subscribe-Methods](./images/RACSignal/RACSignal-Subscribe-Methods.png) + +订阅者可以选择自己想要感兴趣的信息类型 `next/error/completed` 进行关注,并在对应的信息发生时调用 block 进行处理回调。 + +所有的方法其实只是对 `nextBlock`、`completedBlock` 以及 `errorBlock` 的组合,这里以其中最长的 `-subscribeNext:error:completed:` 方法的实现为例(也只需要介绍这一个方法): + +```objectivec +- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock error:(void (^)(NSError *error))errorBlock completed:(void (^)(void))completedBlock { + RACSubscriber *o = [RACSubscriber subscriberWithNext:nextBlock error:errorBlock completed:completedBlock]; + return [self subscribe:o]; +} +``` + +> 方法中传入的所有 block 参数都应该是非空的。 + +拿到了传入的 block 之后,使用 `+subscriberWithNext:error:completed:` 初始化一个 `RACSubscriber` 对象的实例: + +```objectivec ++ (instancetype)subscriberWithNext:(void (^)(id x))next error:(void (^)(NSError *error))error completed:(void (^)(void))completed { + RACSubscriber *subscriber = [[self alloc] init]; + + subscriber->_next = [next copy]; + subscriber->_error = [error copy]; + subscriber->_completed = [completed copy]; + + return subscriber; +} +``` + +在拿到这个对象之后,调用 `RACSignal` 的 `-subscribe:` 方法传入订阅者对象: + +```objectivec +- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber { + NSCAssert(NO, @"This method must be overridden by subclasses"); + return nil; +} +``` + +`RACSignal` 类中其实并没有实现这个实例方法,需要在上文提到的四个子类对这个方法进行覆写,这里仅分析 `RACDynamicSignal` 中的方法: + +```objectivec +- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber { + RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable]; + subscriber = [[RACPassthroughSubscriber alloc] initWithSubscriber:subscriber signal:self disposable:disposable]; + + RACDisposable *schedulingDisposable = [RACScheduler.subscriptionScheduler schedule:^{ + RACDisposable *innerDisposable = self.didSubscribe(subscriber); + [disposable addDisposable:innerDisposable]; + }]; + + [disposable addDisposable:schedulingDisposable]; + + return disposable; +} +``` + +> 这里暂时不需要关注与 `RACDisposable` 有关的任何内容,我们会在下一节中详细介绍。 + +`RACPassthroughSubscriber` 就像它的名字一样,只是对上面创建的订阅者对象进行简单的包装,将所有的消息转发给内部的 `innerSubscriber`,也就是传入的 `RACSubscriber` 对象: + +```objectivec +- (instancetype)initWithSubscriber:(id<RACSubscriber>)subscriber signal:(RACSignal *)signal disposable:(RACCompoundDisposable *)disposable { + self = [super init]; + + _innerSubscriber = subscriber; + _signal = signal; + _disposable = disposable; + + [self.innerSubscriber didSubscribeWithDisposable:self.disposable]; + return self; +} +``` + +如果直接简化 `-subscribe:` 方法的实现,你可以看到一个看起来极为敷衍的代码: + +```objectivec +- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber { + return self.didSubscribe(subscriber); +} +``` + +方法只是执行了在创建信号时传入的 `RACSignalBindBlock`: + +```objectivec +[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { + [subscriber sendNext:@1]; + [subscriber sendNext:@2]; + [subscriber sendCompleted]; + return [RACDisposable disposableWithBlock:^{ + NSLog(@"dispose"); + }]; +}]; +``` + +总而言之,信号的订阅过程就是初始化 `RACSubscriber` 对象,然后执行 `didSubscribe` 代码块的过程。 + +![Principle-of-Subscribing-Signals](./images/RACSignal/Principle-of-Subscribing-Signals.png) + +### 信息的发送 + +在 `RACSignalBindBlock` 中,订阅者可以根据自己的兴趣选择自己想要订阅哪种消息;我们也可以按需发送三种消息: + +![RACSignal-Subcription-Messages-Sending](./images/RACSignal/RACSignal-Subcription-Messages-Sending.png) + +而现在只需要简单看一下这三个方法的实现,就能够明白信息的发送过程了(真是没啥好说的,不过为了~~凑字数~~完整性): + +```objectivec +- (void)sendNext:(id)value { + @synchronized (self) { + void (^nextBlock)(id) = [self.next copy]; + if (nextBlock == nil) return; + + nextBlock(value); + } +} +``` + +`-sendNext:` 只是将方法传入的值传入 `nextBlock` 再调用一次,并没有什么值得去分析的地方,而剩下的两个方法实现也差不多,会调用对应的 block,在这里就省略了。 + +## 订阅的回收过程 + +在创建信号时,我们向 `-createSignal:` 方法中传入了 `didSubscribe` 信号,这个 block 在执行结束时会返回一个 `RACDisposable` 对象,用于在订阅结束时进行必要的清理,同样也可以用于取消因为订阅创建的**正在执行**的任务。 + +而处理这些事情的核心类就是 `RACDisposable` 以及它的子类: + +![RACDisposable-And-Subclasses](./images/RACSignal/RACDisposable-And-Subclasses.png) + +> 这篇文章中主要关注的是左侧的三个子类,当然 `RACDisposable` 的子类不止这三个,还有用于处理 KVO 的 `RACKVOTrampoline`,不过在这里我们不会讨论这个类的实现。 + +### RACDisposable + +在继续分析讨论订阅的回收过程之前,笔者想先对 `RACDisposable` 进行简要的剖析和介绍: + +![RACDisposable](./images/RACSignal/RACDisposable.png) + +类 `RACDisposable` 是以 `_disposeBlock` 为核心进行组织的,几乎所有的方法以及属性其实都是对 `_disposeBlock` 进行的操作。 + +#### 关于 _disposeBlock 中的 self + +> 这一小节的内容是可选的,跳过不影响整篇文章阅读的连贯性。 + +`_disposeBlock` 是一个私有的指针变量,当 `void (^)(void)` 类型的 block 被传入之后都会转换成 CoreFoundation 中的类型并以 `void *` 的形式存入 `_disposeBlock` 中: + +```objectivec + ++ (instancetype)disposableWithBlock:(void (^)(void))block { + return [[self alloc] initWithBlock:block]; +} + +- (instancetype)initWithBlock:(void (^)(void))block { + self = [super init]; + + _disposeBlock = (void *)CFBridgingRetain([block copy]); + OSMemoryBarrier(); + + return self; +} +``` + +奇怪的是,`_disposeBlock` 中不止会存储代码块 block,还有可能存储桥接之后的 `self`: + +```objectivec +- (instancetype)init { + self = [super init]; + + _disposeBlock = (__bridge void *)self; + OSMemoryBarrier(); + + return self; +} +``` + +这里,刚开始看到可能会觉得比较奇怪,有两个疑问需要解决: + +1. 为什么要提供一个 `-init` 方法来初始化 `RACDisposable` 对象? +2. 为什么要向 `_disposeBlock` 中传入当前对象? + +对于 `RACDisposable` 来说,虽然一个不包含 `_disposeBlock` 的对象没什么太多的意义,但是对于 `RACSerialDisposable` 等子类来说,却不完全是这样,因为 `RACSerialDisposable` 在 `-dispose` 时,并不需要执行 `disposeBlock`,这样就浪费了内存和 CPU 时间;但是同时我们需要一个合理的方法准确地判断当前对象的 `isDisposed`: + +```objectivec +- (BOOL)isDisposed { + return _disposeBlock == NULL; +} +``` + +所以,使用向 `_disposeBlock` 中传入 `NULL` 的方式来判断 `isDisposed`;在 `-init` 调用时传入 `self` 而不是 `NULL` 防止状态被误判,这样就在不引入其他实例变量、增加对象的设计复杂度的同时,解决了这两个问题。 + +如果仍然不理解上述的两个问题,在这里举一个错误的例子,如果 `_disposeBlock` 在使用时只传入 `NULL` 或者 `block`,那么在 `RACCompoundDisposable` 初始化时,是应该向 `_disposeBlock` 中传入什么呢? + ++ 传入 `NULL` 会导致在初始化之后 `isDisposed == YES`,然而当前对象根本没有被回收; ++ 传入 `block` 会导致无用的 block 的执行,浪费内存以及 CPU 时间; + +这也就是为什么要引入 `self` 来作为 `_disposeBlock` 内容的原因。 + +#### -dispose: 方法的实现 + +这个只有不到 20 行的 `-dispose:` 方法已经是整个 `RACDisposable` 类中最复杂的方法了: + +```objectivec +- (void)dispose { + void (^disposeBlock)(void) = NULL; + + while (YES) { + void *blockPtr = _disposeBlock; + if (OSAtomicCompareAndSwapPtrBarrier(blockPtr, NULL, &_disposeBlock)) { + if (blockPtr != (__bridge void *)self) { + disposeBlock = CFBridgingRelease(blockPtr); + } + + break; + } + } + + if (disposeBlock != nil) disposeBlock(); +} +``` + +但是其实它的实现也没有复杂到哪里去,从 `_disposeBlock` 实例变量中调用 `CFBridgingRelease` 取出一个 `disposeBlock`,然后执行这个 block,整个方法就结束了。 + +### RACSerialDisposable + +`RACSerialDisposable` 是一个用于持有 `RACDisposable` 的容器,它一次只能持有一个 `RACDisposable` 的实例,并可以原子地换出容器中保存的对象: + +```objectivec +- (RACDisposable *)swapInDisposable:(RACDisposable *)newDisposable { + RACDisposable *existingDisposable; + BOOL alreadyDisposed; + + pthread_mutex_lock(&_mutex); + alreadyDisposed = _disposed; + if (!alreadyDisposed) { + existingDisposable = _disposable; + _disposable = newDisposable; + } + pthread_mutex_unlock(&_mutex); + + if (alreadyDisposed) { + [newDisposable dispose]; + return nil; + } + + return existingDisposable; +} +``` + +线程安全的 `RACSerialDisposable` 使用 `pthred_mutex_t` 互斥锁来保证在访问关键变量时不会出现线程竞争问题。 + +`-dispose` 方法的处理也十分简单: + +```objectivec +- (void)dispose { + RACDisposable *existingDisposable; + + pthread_mutex_lock(&_mutex); + if (!_disposed) { + existingDisposable = _disposable; + _disposed = YES; + _disposable = nil; + } + pthread_mutex_unlock(&_mutex); + + [existingDisposable dispose]; +} +``` + +使用锁保证线程安全,并在内部的 `_disposable` 换出之后在执行 `-dispose` 方法对订阅进行处理。 + +### RACCompoundDisposable + +与 `RACSerialDisposable` 只负责一个 `RACDisposable` 对象的释放不同;`RACCompoundDisposable` 同时负责多个 `RACDisposable` 对象的释放。 + +相比于只管理一个 `RACDisposable` 对象的 `RACSerialDisposable`,`RACCompoundDisposable` 由于管理多个对象,其实现更加复杂,而且为了**性能和内存占用之间的权衡**,其实现方式是通过持有两个实例变量: + +```objectivec +@interface RACCompoundDisposable () { + ... + RACDisposable *_inlineDisposables[RACCompoundDisposableInlineCount]; + + CFMutableArrayRef _disposables; + ... +} +``` + +在对象持有的 `RACDisposable` 不超过 `RACCompoundDisposableInlineCount` 时,都会存储在 `_inlineDisposables` 数组中,而更多的实例都会存储在 `_disposables` 中: + +![RACCompoundDisposable](./images/RACSignal/RACCompoundDisposable.png) + +`RACCompoundDisposable` 在使用 `-initWithDisposables:`初始化时,会初始化两个 `RACDisposable` 的位置用于加速销毁订阅的过程,同时为了不浪费内存空间,在默认情况下只占用两个位置: + +```objectivec +- (instancetype)initWithDisposables:(NSArray *)otherDisposables { + self = [self init]; + + [otherDisposables enumerateObjectsUsingBlock:^(RACDisposable *disposable, NSUInteger index, BOOL *stop) { + self->_inlineDisposables[index] = disposable; + if (index == RACCompoundDisposableInlineCount - 1) *stop = YES; + }]; + + if (otherDisposables.count > RACCompoundDisposableInlineCount) { + _disposables = RACCreateDisposablesArray(); + + CFRange range = CFRangeMake(RACCompoundDisposableInlineCount, (CFIndex)otherDisposables.count - RACCompoundDisposableInlineCount); + CFArrayAppendArray(_disposables, (__bridge CFArrayRef)otherDisposables, range); + } + + return self; +} +``` + +如果传入的 `otherDisposables` 多于 `RACCompoundDisposableInlineCount`,就会创建一个新的 `CFMutableArrayRef` 引用,并将剩余的 `RACDisposable` 全部传入这个数组中。 + +在 `RACCompoundDisposable` 中另一个值得注意的方法就是 `-addDisposable:` + +```objectivec +- (void)addDisposable:(RACDisposable *)disposable { + if (disposable == nil || disposable.disposed) return; + + BOOL shouldDispose = NO; + + pthread_mutex_lock(&_mutex); + { + if (_disposed) { + shouldDispose = YES; + } else { + for (unsigned i = 0; i < RACCompoundDisposableInlineCount; i++) { + if (_inlineDisposables[i] == nil) { + _inlineDisposables[i] = disposable; + goto foundSlot; + } + } + + if (_disposables == NULL) _disposables = RACCreateDisposablesArray(); + CFArrayAppendValue(_disposables, (__bridge void *)disposable); + foundSlot:; + } + } + pthread_mutex_unlock(&_mutex); + if (shouldDispose) [disposable dispose]; +} +``` + +在向 `RACCompoundDisposable` 中添加新的 `RACDisposable` 对象时,会先尝试在 `_inlineDisposables` 数组中寻找空闲的位置,如果没有找到,就会加入到 `_disposables` 中;但是,在添加 `RACDisposable` 的过程中也难免遇到当前 `RACCompoundDisposable` 已经 `dispose` 的情况,而这时就会直接 `-dispose` 刚刚加入的对象。 + +### 订阅的销毁过程 + +在了解了 ReactiveCocoa 中与订阅销毁相关的类,我们就可以继续对 `-bind:` 方法的分析了,之前在分析该方法时省略了 `-bind:` 在执行过程中是如何处理订阅的清理和销毁的,所以会省略对于正常值和错误的处理过程,首先来看一下简化后的代码: + +```objectivec +- (RACSignal *)bind:(RACSignalBindBlock (^)(void))block { + return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) { + RACSignalBindBlock bindingBlock = block(); + __block volatile int32_t signalCount = 1; + RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable]; + + void (^completeSignal)(RACDisposable *) = ... + void (^addSignal)(RACSignal *) = ... + + RACSerialDisposable *selfDisposable = [[RACSerialDisposable alloc] init]; + [compoundDisposable addDisposable:selfDisposable]; + RACDisposable *bindingDisposable = [self subscribeNext:^(id x) { + BOOL stop = NO; + id signal = bindingBlock(x, &stop); + + if (signal != nil) addSignal(signal); + if (signal == nil || stop) { + [selfDisposable dispose]; + completeSignal(selfDisposable); + } + } completed:^{ + completeSignal(selfDisposable); + }]; + selfDisposable.disposable = bindingDisposable; + return compoundDisposable; + }] setNameWithFormat:@"[%@] -bind:", self.name]; +} +``` + +在简化的代码中,订阅的清理是由一个 `RACCompoundDisposable` 的实例负责的,向这个实例中添加 `RACSerialDisposable` 以及 `RACDisposable` 对象,并在 `RACCompoundDisposable` 销毁时销毁。 + +`completeSignal` 和 `addSignal` 两个 block 主要负责处理新创建信号的清理工作: + +```objectivec +void (^completeSignal)(RACDisposable *) = ^(RACDisposable *finishedDisposable) { + if (OSAtomicDecrement32Barrier(&signalCount) == 0) { + [subscriber sendCompleted]; + [compoundDisposable dispose]; + } else { + [compoundDisposable removeDisposable:finishedDisposable]; + } +}; + +void (^addSignal)(RACSignal *) = ^(RACSignal *signal) { + OSAtomicIncrement32Barrier(&signalCount); + RACSerialDisposable *selfDisposable = [[RACSerialDisposable alloc] init]; + [compoundDisposable addDisposable:selfDisposable]; + RACDisposable *disposable = [signal completed:^{ + completeSignal(selfDisposable); + }]; + selfDisposable.disposable = disposable; +}; +``` + +先通过一个例子来看一下 `-bind:` 方法调用之后,订阅是如何被清理的: + +```objectivec +RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { + [subscriber sendNext:@1]; + [subscriber sendNext:@2]; + [subscriber sendCompleted]; + return [RACDisposable disposableWithBlock:^{ + NSLog(@"Original Signal Dispose."); + }]; +}]; +RACSignal *bindSignal = [signal bind:^RACSignalBindBlock _Nonnull{ + return ^(NSNumber *value, BOOL *stop) { + NSNumber *returnValue = @(value.integerValue); + return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) { + for (NSInteger i = 0; i < value.integerValue; i++) [subscriber sendNext:returnValue]; + [subscriber sendCompleted]; + return [RACDisposable disposableWithBlock:^{ + NSLog(@"Binding Signal Dispose."); + }]; + }]; + }; +}]; +[bindSignal subscribeNext:^(id _Nullable x) { + NSLog(@"%@", x); +}]; +``` + +在每个订阅创建以及所有的值发送之后,订阅就会被就地销毁,调用 `disposeBlock`,并从 `RACCompoundDisposable` 实例中移除: + +```objectivec +1 +Binding Signal Dispose. +2 +2 +Binding Signal Dispose. +Original Signal Dispose. +``` + +原订阅的销毁时间以及绑定信号的控制是由 `SignalCount` 控制的,其表示 `RACCompoundDisposable` 中的 `RACSerialDisposable` 实例的个数,在每次有新的订阅被创建时都会向 `RACCompoundDisposable` 加入一个新的 `RACSerialDisposable`,并在订阅发送结束时从数组中移除,整个过程用图示来表示比较清晰: + +![RACSignal-Bind-Disposable](./images/RACSignal/RACSignal-Bind-Disposable.png) + +> 紫色的 `RACSerialDisposable` 为原订阅创建的对象,灰色的为新信号订阅的对象。 + +## 总结 + +这是整个 ReactiveCocoa 源代码分析系列文章的第一篇,想写一个跟这个系列有关的代码已经很久了,文章中对于 `RACSignal` 进行了一些简单的介绍,项目中绝大多数的方法都是很简洁的,行数并不多,代码的组织方式也很易于理解。虽然没有太多让人意外的东西,不过整个工程还是很值得阅读的。 + +## References + ++ [A Fistful of Monads](http://learnyouahaskell.com/a-fistful-of-monads) ++ [What is (functional) reactive programming?](http://stackoverflow.com/questions/1028250/what-is-functional-reactive-programming/1030631#1030631) + +## 方法实现对照表 + +| 方法 | 实现 | +| :-: | :-: | +| `+return:` | [RACSignal.m#L89-L91](https://github.com/ReactiveCocoa/ReactiveObjC/blob/1180ab256000573ef82141e5d40e9b9c35dfd69c/ReactiveObjC/RACSignal.m#L89-L91)| +|  `-bind:` | [RACSignal.m#L93-176](https://github.com/ReactiveCocoa/ReactiveObjC/blob/1180ab256000573ef82141e5d40e9b9c35dfd69c/ReactiveObjC/RACSignal.m#L93-L176) | + +> Github Repo:[iOS-Source-Code-Analyze](https://github.com/draveness/iOS-Source-Code-Analyze) +> +> Follow: [Draveness · GitHub](https://github.com/Draveness) +> +> Source: http://draveness.me/racsignal + + diff --git a/contents/ReactiveObjC/RACSubject.md b/contents/ReactiveObjC/RACSubject.md new file mode 100644 index 0000000..ae49c42 --- /dev/null +++ b/contents/ReactiveObjC/RACSubject.md @@ -0,0 +1,430 @@ +# 『可变』的热信号 RACSubject + +在 ReactiveCocoa 中除了不可变的信号 `RACSignal`,也有用于桥接非 RAC 代码到 ReactiveCocoa 世界的『可变』信号 `RACSubject`。 + +![“Mutable” RACSignal — RACSubject](images/RACSubject/%E2%80%9CMutable%E2%80%9D%20RACSignal%20%E2%80%94%20RACSubject.png) + +`RACSubject` 到底是什么?根据其字面意思,可以将它理解为一个可以订阅的主题,我们在订阅主题之后,向主题发送新的消息时,**所有**的订阅者都会接收到最新的消息。 + +但是这么解释确实有点晦涩,也不易于理解,ReactiveCocoa 团队对 `RACSubject` 的解释是,`RACSubject` 其实就是一个可以**手动**控制的信号(感觉这么解释更难理解了)。 + +> A subject, represented by the RACSubject class, is a signal that can be manually controlled. + +## RACSubject 简介 + +`RACSubject` 是 `RACSignal` 的子类,与 `RACSignal` 以及 `RACSequence` 有着众多的类簇不同,`RACSubject` 在整个工程中并没有多少子类;不过,在大多数情况下,我们也只会使用 `RACSubject` 自己或者 `RACReplaySubject`。 + +![RACSubject - Subclasses](images/RACSubject/RACSubject%20-%20Subclasses.png) + +相比于 `RACSignal` 丰富的头文件 ,`RACSubject` 对外的接口并没有提供太多的方法: + +```objectivec +@interface RACSubject : RACSignal <RACSubscriber> + ++ (instancetype)subject; + +@end +``` + +唯一提供的接口就是用于返回一个新实例的 `+subject` 方法;除此之外,在笔者看来它与 `RACSignal` 最大的不同就是:`RACSubject` 实现了 `RACSubscriber` 协议,也就是下面的这些方法: + +```objectivec +@protocol RACSubscriber <NSObject> +@required + +- (void)sendNext:(nullable id)value; +- (void)sendError:(nullable NSError *)error; +- (void)sendCompleted; +- (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable; + +@end +``` + +我们并不能在一个 `RACSignal` 对象上执行这些方法,只能在创建信号的 block 里面向遵循 `RACSubscriber` 协议的对象发送新的值或者错误,这也是 `RACSubject` 和父类最大的不同:在 `RACSubject` 实例初始化之后,也可以通过这个实例向所有的订阅者发送消息。 + +## 冷信号与热信号 + +提到 `RACSubject` 就不得不提 ReactiveCocoa 中的另一对概念,冷信号和热信号。 + +> 其实解释这两者之间区别的文章已经很多了,我相信各位读者能找到很多的资料,在这里就简单介绍一下冷热信号的概念,如果想要了解更多的内容可以在 [References](#references) 中找到更多的文章。 + +对于冷热信号概念,我们借用 Rx 中的描述: + +![Hot-Signal-And-Cold-Signa](images/RACSubject/Hot-Signal-And-Cold-Signal.png) + +> Cold signal is sequences that are passive and start producing notifications on request (when subscribed to), and hot signal is sequences that are active and produce notifications regardless of subscriptions. ---- [Hot and Cold observables](http://www.introtorx.com/content/v1.0.10621.0/14_HotAndColdObservables.html) + +冷信号是被动的,只会在被订阅时向订阅者发送通知;热信号是主动的,它会在任意时间发出通知,与订阅者的订阅时间无关。 + +也就是说冷信号所有的订阅者会在订阅时收到完全相同的序列;而订阅热信号之后,只会收到在订阅之后发出的序列。 + +> 热信号的订阅者能否收到消息取决于订阅的时间。 + +热信号在我们生活中有很多的例子,比如订阅杂志时并不会把之前所有的期刊都送到我们手中,只会接收到订阅之后的期刊;而对于冷信号的话,举一个不恰当的例子,每一年的高考考生在『订阅』高考之后,收到往年所有的试卷,并在高考之后会取消订阅。 + +## 热信号 RACSubject + +在 ReactiveCocoa 中,我们使用 `RACSignal` 来表示冷信号,也就是每一个订阅者在订阅信号时都会收到完整的序列;`RACSubject` 用于表示热信号,订阅者接收到多少值取决于它订阅的时间。 + +前面的文章中已经对 `RACSignal` 冷信号有了很多的介绍,这里也就不会多说了;这一小节主要的内容是想通过一个例子,简单展示 `RACSubject` 的订阅者收到的内容与订阅时间的关系: + +```objectivec +RACSubject *subject = [RACSubject subject]; + +// Subscriber 1 +[subject subscribeNext:^(id _Nullable x) { + NSLog(@"1st Sub: %@", x); +}]; +[subject sendNext:@1]; + +// Subscriber 2 +[subject subscribeNext:^(id _Nullable x) { + NSLog(@"2nd Sub: %@", x); +}]; +[subject sendNext:@2]; + +// Subscriber 3 +[subject subscribeNext:^(id _Nullable x) { + NSLog(@"3rd Sub: %@", x); +}]; +[subject sendNext:@3]; +[subject sendCompleted]; +``` + +这里以图的方式来展示整个订阅与订阅者接收消息的过程: + +![Track-RACSubject-Subscription-Process](images/RACSubject/Track-RACSubject-Subscription-Process.png) + +从图中我们可以清楚的看到,几个订阅者根据**订阅时间**的不同收到了不同的数字序列,`RACSubject` 是**时间相关**的,它在发送消息时只会向已订阅的订阅者推送消息。 + +## RACSubject 的实现 + +`RACSubject` 的实现并不复杂,它『可变』的特性都来源于持有的订阅者数组 `subscribers`,在每次执行 `subscribeNext:error:completed:` 一类便利方法时,都会将传入的 `id<RACSubscriber>` 对象加入数组: + +```objectivec +- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber { + RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable]; + subscriber = [[RACPassthroughSubscriber alloc] initWithSubscriber:subscriber signal:self disposable:disposable]; + + NSMutableArray *subscribers = self.subscribers; + @synchronized (subscribers) { + [subscribers addObject:subscriber]; + } + + [disposable addDisposable:[RACDisposable disposableWithBlock:^{ + @synchronized (subscribers) { + NSUInteger index = [subscribers indexOfObjectWithOptions:NSEnumerationReverse passingTest:^ BOOL (id<RACSubscriber> obj, NSUInteger index, BOOL *stop) { + return obj == subscriber; + }]; + + if (index != NSNotFound) [subscribers removeObjectAtIndex:index]; + } + }]]; + + return disposable; +} +``` + +订阅的过程分为三个部分: + +1. 初始化一个 `RACPassthroughSubscriber` 实例; +2. 将 `subscriber` 加入 `RACSubject` 持有的数组中; +3. 创建一个 `RACDisposable` 对象,在当前 `subscriber` 销毁时,将自身从数组中移除。 + +![Send-Subscibe-to-RACSubject](images/RACSubject/Send-Subscibe-to-RACSubject.png) + +`-subscribe:` 将所有遵循 `RACSubscriber` 协议的对象全部加入当前 `RACSubject` 持有的数组 `subscribers` 中。 + +在上一节的例子中,我们能对 `RACSubject` 发送 `-sendNext:` 等消息也都取决于它实现了 `RACSubscriber` 协议: + +```objectivec +- (void)sendNext:(id)value { + [self enumerateSubscribersUsingBlock:^(id<RACSubscriber> subscriber) { + [subscriber sendNext:value]; + }]; +} + +- (void)sendError:(NSError *)error { + [self.disposable dispose]; + + [self enumerateSubscribersUsingBlock:^(id<RACSubscriber> subscriber) { + [subscriber sendError:error]; + }]; +} + +- (void)sendCompleted { + [self.disposable dispose]; + + [self enumerateSubscribersUsingBlock:^(id<RACSubscriber> subscriber) { + [subscriber sendCompleted]; + }]; +} +``` + +`RACSubject` 会在自身接受到这些方法时,下发给持有的全部的 `subscribers`。 + +![Send-Messages-to-RACSubject](images/RACSubject/Send-Messages-to-RACSubject.png) + +代码中的 `-enumerateSubscribersUsingBlock:` 只是一个使用 `for` 循环遍历 `subscribers` 的安全方法: + +```objectivec +- (void)enumerateSubscribersUsingBlock:(void (^)(id<RACSubscriber> subscriber))block { + NSArray *subscribers; + @synchronized (self.subscribers) { + subscribers = [self.subscribers copy]; + } + + for (id<RACSubscriber> subscriber in subscribers) { + block(subscriber); + } +} +``` + +`RACSubject` 就是围绕一个 `NSMutableArray` 数组实现的,实现还是非常简单的,只是在需要访问 `subscribers` 的方法中使用 `@synchronized` 避免线程竞争。 + +```objectivec +@interface RACSubject () + +@property (nonatomic, strong, readonly) NSMutableArray *subscribers; + +@end +``` + +`RACSubject` 提供的初始化类方法 `+subject` 也只是初始化了几个成员变量: + +```objectivec ++ (instancetype)subject { + return [[self alloc] init]; +} + +- (instancetype)init { + self = [super init]; + if (self == nil) return nil; + + _disposable = [RACCompoundDisposable compoundDisposable]; + _subscribers = [[NSMutableArray alloc] initWithCapacity:1]; + + return self; +} +``` + +至此,对于 `RACSubject` 的分析就结束了,接下来会分析更多的子类。 + +## RACBehaviorSubject 与 RACReplaySubject + +这一节会介绍 `RACSubject` 的两个子类 `RACBehaviorSubject` 和 `RACReplaySubject`,前者在订阅时会向订阅者发送最新的消息,后者在订阅之后**可以**重新发送之前的**所有**消息序列。 + +### RACBehaviorSubject + +先来介绍两者中实现较简单的 `RACBehaviorSubject`,它在内部会保存一个 `currentValue` 对象,也就是最后一次发送的消息: + +```objectivec +@interface RACBehaviorSubject () + +@property (nonatomic, strong) id currentValue; + +@end +``` + +在每次执行 `-sendNext:` 时,都会对 `RACBehaviorSubject` 中保存的 `currentValue` 进行更新,并使用父类的 `-sendNext:` 方法,向所有的订阅者发送最新的消息: + +```objectivec +- (void)sendNext:(id)value { + @synchronized (self) { + self.currentValue = value; + [super sendNext:value]; + } +} +``` + +`RACBehaviorSubject` 最重要的特性就是在订阅时,向最新的订阅者发送之前的消息,这是通过覆写 `-subscribe:` 方法实现的。 + +在调用子类的 `-subscribe:` 方法之后,会在 `subscriber` 对象上执行 `-sendNext:` 方法: + +```objectivec +- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber { + RACDisposable *subscriptionDisposable = [super subscribe:subscriber]; + + RACDisposable *schedulingDisposable = [RACScheduler.subscriptionScheduler schedule:^{ + @synchronized (self) { + [subscriber sendNext:self.currentValue]; + } + }]; + + return [RACDisposable disposableWithBlock:^{ + [subscriptionDisposable dispose]; + [schedulingDisposable dispose]; + }]; +} +``` + +接下来,通过一个简单的例子来演示 `RACBehaviorSubject` 到底是如何工作的: + +```objectivec +RACBehaviorSubject *subject = [RACBehaviorSubject subject]; + +[subject subscribeNext:^(id _Nullable x) { + NSLog(@"1st Sub: %@", x); +}]; +[subject sendNext:@1]; + +[subject subscribeNext:^(id _Nullable x) { + NSLog(@"2nd Sub: %@", x); +}]; +[subject sendNext:@2]; + +[subject subscribeNext:^(id _Nullable x) { + NSLog(@"3rd Sub: %@", x); +}]; +[subject sendNext:@3]; +[subject sendCompleted]; +``` + +上面的代码其实与 `RACSubject` 一节中的代码差不多,只将 `RACSubject` 转换成了 `RACBehaviorSubject` 对象。 + +![Track-RACBehaviorSubject-Subscription-Process](images/RACSubject/Track-RACBehaviorSubject-Subscription-Process.png) + +在每次订阅者订阅 `RACBehaviorSubject` 之后,都会向该订阅者发送**最新**的消息,这也就是 `RACBehaviorSubject` 最重要的行为。 + +`RACBehaviorSubject` 有一个用于创建包含默认值的类方法 `+behaviorSubjectWithDefaultValue:`,如果将上面的第一行代码改成: + +```objectivec +RACBehaviorSubject *subject = [RACBehaviorSubject behaviorSubjectWithDefaultValue:@0]; +``` + +那么在第一个订阅者刚订阅 `RACBehaviorSubject` 时就会收到 `@0` 对象。 + +![Track-RACBehaviorSubject-Subscription-Process-With-Default-Value](images/RACSubject/Track-RACBehaviorSubject-Subscription-Process-With-Default-Value.png) + +### RACReplaySubject + +`RACReplaySubject` 相当于一个自带 `buffer` 的 `RACBehaviorSubject`,它可以在每次有新的订阅者订阅之后发送之前的全部消息。 + +```objectivec +@interface RACReplaySubject () + +@property (nonatomic, assign, readonly) NSUInteger capacity; +@property (nonatomic, strong, readonly) NSMutableArray *valuesReceived; + +@end +``` + +实现的方式是通过持有一个 `valuesReceived` 的数组和能够存储的对象的上限 `capacity`,默认值为: + +```objectivec +const NSUInteger RACReplaySubjectUnlimitedCapacity = NSUIntegerMax; +``` + +当然你可以用 `+replaySubjectWithCapacity:` 初始化一个其它大小的 `RACReplaySubject` 对象: + +```objectivec ++ (instancetype)replaySubjectWithCapacity:(NSUInteger)capacity { + return [(RACReplaySubject *)[self alloc] initWithCapacity:capacity]; +} + +- (instancetype)initWithCapacity:(NSUInteger)capacity { + self = [super init]; + + _capacity = capacity; + _valuesReceived = (capacity == RACReplaySubjectUnlimitedCapacity ? [NSMutableArray array] : [NSMutableArray arrayWithCapacity:capacity]); + + return self; +} +``` + +在每次调用 `-sendNext:` 方法发送消息时,都会将其加入 `valuesReceived` 数组中,并踢出之前的元素: + +```objectivec +- (void)sendNext:(id)value { + @synchronized (self) { + [self.valuesReceived addObject:value ?: RACTupleNil.tupleNil]; + [super sendNext:value]; + + if (self.capacity != RACReplaySubjectUnlimitedCapacity && self.valuesReceived.count > self.capacity) { + [self.valuesReceived removeObjectsInRange:NSMakeRange(0, self.valuesReceived.count - self.capacity)]; + } + } +} +``` + +需要注意的有两点,一是对 `valuesReceived` 的数组的操作必须使用 `@synchronized` 加锁;第二,如果 `value` 为空的话,也需要将其转换成 `RACTupleNil.tupleNil` 对象进行保存。 + +![Send-Messages-to-RACReplaySubject](images/RACSubject/Send-Messages-to-RACReplaySubject.png) + +`-sendError:` 和 `-sendCompleted` 方法都会标记对应 `flag`,即 `hasCompleted` 和 `hasError`,这里就不介绍了;同样的,`RACReplaySubject` 也覆写了 `-subscribe:` 方法,在每次有订阅者订阅时重新发送所有的序列: + +```objectivec +- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber { + RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable]; + + RACDisposable *schedulingDisposable = [RACScheduler.subscriptionScheduler schedule:^{ + @synchronized (self) { + for (id value in self.valuesReceived) { + if (compoundDisposable.disposed) return; + + [subscriber sendNext:(value == RACTupleNil.tupleNil ? nil : value)]; + } + + if (compoundDisposable.disposed) return; + + if (self.hasCompleted) { + [subscriber sendCompleted]; + } else if (self.hasError) { + [subscriber sendError:self.error]; + } else { + RACDisposable *subscriptionDisposable = [super subscribe:subscriber]; + [compoundDisposable addDisposable:subscriptionDisposable]; + } + } + }]; + + [compoundDisposable addDisposable:schedulingDisposable]; + + return compoundDisposable; +} +``` + +我们仍然使用上一节中的例子来展示 `RACReplaySubject` 是如何工作的,只修改第一行代码: + +```objectivec +RACReplaySubject *subject = [RACReplaySubject subject]; + +[subject subscribeNext:^(id _Nullable x) { + NSLog(@"1st Subscriber: %@", x); +}]; +[subject sendNext:@1]; + +[subject subscribeNext:^(id _Nullable x) { + NSLog(@"2nd Subscriber: %@", x); +}]; +[subject sendNext:@2]; + +[subject subscribeNext:^(id _Nullable x) { + NSLog(@"3rd Subscriber: %@", x); +}]; +[subject sendNext:@3]; +[subject sendCompleted]; +``` + +运行这段代码之后,会得到如下图的结果: + +![Track-RACReplaySubject-Subscription-Process](images/RACSubject/Track-RACReplaySubject-Subscription-Process.png) + +所有订阅 `RACReplaySubject` 的对象(默认行为)都能获得完整的序列,而这个特性在与 `RACMulticastConnection` 一起使用也有着巨大威力,我们会在之后的文章中介绍。 + +## 总结 + +`RACSubject` 在 `RACSignal` 对象之上进行了简单的修改,将原有的冷信号改造成了热信号,将不可变变成了可变。 + +虽然 `RACSubject` 的实现并不复杂,只是存储了一个遵循 `RACSubscriber` 协议的对象列表以及所有的消息,但是在解决实际问题时却能够很好地解决很多与网络操作相关的问题。 + +## References + ++ [细说 ReactiveCocoa 的冷信号与热信号](http://williamzang.com/blog/2015/08/18/talk-about-reactivecocoas-cold-signal-and-hot-signal/) ++ [Hot and Cold observables](http://www.introtorx.com/content/v1.0.10621.0/14_HotAndColdObservables.html) + +> Github Repo:[iOS-Source-Code-Analyze](https://github.com/draveness/iOS-Source-Code-Analyze) +> +> Follow: [Draveness · GitHub](https://github.com/Draveness) +> +> Source: http://draveness.me/racsubject diff --git a/contents/ReactiveObjC/images/RACChannel/Channel-And-Network-Connection.png b/contents/ReactiveObjC/images/RACChannel/Channel-And-Network-Connection.png new file mode 100644 index 0000000..0188009 Binary files /dev/null and b/contents/ReactiveObjC/images/RACChannel/Channel-And-Network-Connection.png differ diff --git a/contents/ReactiveObjC/images/RACChannel/Connection-Between-View-Model.png b/contents/ReactiveObjC/images/RACChannel/Connection-Between-View-Model.png new file mode 100644 index 0000000..f1e612c Binary files /dev/null and b/contents/ReactiveObjC/images/RACChannel/Connection-Between-View-Model.png differ diff --git a/contents/ReactiveObjC/images/RACChannel/English-Channel-banner.jpg b/contents/ReactiveObjC/images/RACChannel/English-Channel-banner.jpg new file mode 100644 index 0000000..406e692 Binary files /dev/null and b/contents/ReactiveObjC/images/RACChannel/English-Channel-banner.jpg differ diff --git a/contents/ReactiveObjC/images/RACChannel/Messages-Send-From-Model.png b/contents/ReactiveObjC/images/RACChannel/Messages-Send-From-Model.png new file mode 100644 index 0000000..3ac1159 Binary files /dev/null and b/contents/ReactiveObjC/images/RACChannel/Messages-Send-From-Model.png differ diff --git a/contents/ReactiveObjC/images/RACChannel/RACChannel-Hierachy.png b/contents/ReactiveObjC/images/RACChannel/RACChannel-Hierachy.png new file mode 100644 index 0000000..0df96d8 Binary files /dev/null and b/contents/ReactiveObjC/images/RACChannel/RACChannel-Hierachy.png differ diff --git a/contents/ReactiveObjC/images/RACChannel/RACChannel-Interface.png b/contents/ReactiveObjC/images/RACChannel/RACChannel-Interface.png new file mode 100644 index 0000000..a4bb7a3 Binary files /dev/null and b/contents/ReactiveObjC/images/RACChannel/RACChannel-Interface.png differ diff --git a/contents/ReactiveObjC/images/RACChannel/RACChannelTerminal-Interface.png b/contents/ReactiveObjC/images/RACChannel/RACChannelTerminal-Interface.png new file mode 100644 index 0000000..c1392e9 Binary files /dev/null and b/contents/ReactiveObjC/images/RACChannel/RACChannelTerminal-Interface.png differ diff --git a/contents/ReactiveObjC/images/RACChannel/RACChannelTo-And-Property.png b/contents/ReactiveObjC/images/RACChannel/RACChannelTo-And-Property.png new file mode 100644 index 0000000..f4a765a Binary files /dev/null and b/contents/ReactiveObjC/images/RACChannel/RACChannelTo-And-Property.png differ diff --git a/contents/ReactiveObjC/images/RACChannel/RACChannelTo-Model-View.png b/contents/ReactiveObjC/images/RACChannel/RACChannelTo-Model-View.png new file mode 100644 index 0000000..da3055c Binary files /dev/null and b/contents/ReactiveObjC/images/RACChannel/RACChannelTo-Model-View.png differ diff --git a/contents/ReactiveObjC/images/RACChannel/RACKVOChannel.png b/contents/ReactiveObjC/images/RACChannel/RACKVOChannel.png new file mode 100644 index 0000000..28022fa Binary files /dev/null and b/contents/ReactiveObjC/images/RACChannel/RACKVOChannel.png differ diff --git a/contents/ReactiveObjC/images/RACChannel/Sending-Errors-And-Completed-Messages.png b/contents/ReactiveObjC/images/RACChannel/Sending-Errors-And-Completed-Messages.png new file mode 100644 index 0000000..411b2ba Binary files /dev/null and b/contents/ReactiveObjC/images/RACChannel/Sending-Errors-And-Completed-Messages.png differ diff --git a/contents/ReactiveObjC/images/RACChannel/Terminals-Between-View-Model.png b/contents/ReactiveObjC/images/RACChannel/Terminals-Between-View-Model.png new file mode 100644 index 0000000..6e31cd9 Binary files /dev/null and b/contents/ReactiveObjC/images/RACChannel/Terminals-Between-View-Model.png differ diff --git a/contents/ReactiveObjC/images/RACChannel/TextField-With-Channel.gif b/contents/ReactiveObjC/images/RACChannel/TextField-With-Channel.gif new file mode 100644 index 0000000..0d93980 Binary files /dev/null and b/contents/ReactiveObjC/images/RACChannel/TextField-With-Channel.gif differ diff --git a/contents/ReactiveObjC/images/RACChannel/Two-UITextField-With-RACChannel.png b/contents/ReactiveObjC/images/RACChannel/Two-UITextField-With-RACChannel.png new file mode 100644 index 0000000..d2d8ec4 Binary files /dev/null and b/contents/ReactiveObjC/images/RACChannel/Two-UITextField-With-RACChannel.png differ diff --git a/contents/ReactiveObjC/images/RACChannel/UITextField-RACChannel-Interface.png b/contents/ReactiveObjC/images/RACChannel/UITextField-RACChannel-Interface.png new file mode 100644 index 0000000..267cd65 Binary files /dev/null and b/contents/ReactiveObjC/images/RACChannel/UITextField-RACChannel-Interface.png differ diff --git a/contents/ReactiveObjC/images/RACChannel/What-is-RACChannel.png b/contents/ReactiveObjC/images/RACChannel/What-is-RACChannel.png new file mode 100644 index 0000000..2740c14 Binary files /dev/null and b/contents/ReactiveObjC/images/RACChannel/What-is-RACChannel.png differ diff --git a/contents/ReactiveObjC/images/RACCommand/Errors-Signals.png b/contents/ReactiveObjC/images/RACCommand/Errors-Signals.png new file mode 100644 index 0000000..94b106a Binary files /dev/null and b/contents/ReactiveObjC/images/RACCommand/Errors-Signals.png differ diff --git a/contents/ReactiveObjC/images/RACCommand/Execute-For-RACCommand.png b/contents/ReactiveObjC/images/RACCommand/Execute-For-RACCommand.png new file mode 100644 index 0000000..a1cd88f Binary files /dev/null and b/contents/ReactiveObjC/images/RACCommand/Execute-For-RACCommand.png differ diff --git a/contents/ReactiveObjC/images/RACCommand/Execute-on-RACCommand.png b/contents/ReactiveObjC/images/RACCommand/Execute-on-RACCommand.png new file mode 100644 index 0000000..deddaf2 Binary files /dev/null and b/contents/ReactiveObjC/images/RACCommand/Execute-on-RACCommand.png differ diff --git a/contents/ReactiveObjC/images/RACCommand/Executing-Signal.png b/contents/ReactiveObjC/images/RACCommand/Executing-Signal.png new file mode 100644 index 0000000..a7ee1d1 Binary files /dev/null and b/contents/ReactiveObjC/images/RACCommand/Executing-Signal.png differ diff --git a/contents/ReactiveObjC/images/RACCommand/Execution-Signals.png b/contents/ReactiveObjC/images/RACCommand/Execution-Signals.png new file mode 100644 index 0000000..550eccb Binary files /dev/null and b/contents/ReactiveObjC/images/RACCommand/Execution-Signals.png differ diff --git a/contents/ReactiveObjC/images/RACCommand/Immediate-Enabled-Signal.png b/contents/ReactiveObjC/images/RACCommand/Immediate-Enabled-Signal.png new file mode 100644 index 0000000..bd6e057 Binary files /dev/null and b/contents/ReactiveObjC/images/RACCommand/Immediate-Enabled-Signal.png differ diff --git a/contents/ReactiveObjC/images/RACCommand/Interact-Between-UI-And-RACCommand.png b/contents/ReactiveObjC/images/RACCommand/Interact-Between-UI-And-RACCommand.png new file mode 100644 index 0000000..2c9c289 Binary files /dev/null and b/contents/ReactiveObjC/images/RACCommand/Interact-Between-UI-And-RACCommand.png differ diff --git a/contents/ReactiveObjC/images/RACCommand/MoreExecutionAllowed-Signal.png b/contents/ReactiveObjC/images/RACCommand/MoreExecutionAllowed-Signal.png new file mode 100644 index 0000000..ebb0a7d Binary files /dev/null and b/contents/ReactiveObjC/images/RACCommand/MoreExecutionAllowed-Signal.png differ diff --git a/contents/ReactiveObjC/images/RACCommand/Multiple-Executes.png b/contents/ReactiveObjC/images/RACCommand/Multiple-Executes.png new file mode 100644 index 0000000..6902084 Binary files /dev/null and b/contents/ReactiveObjC/images/RACCommand/Multiple-Executes.png differ diff --git a/contents/ReactiveObjC/images/RACCommand/RACCommand-Interface.png b/contents/ReactiveObjC/images/RACCommand/RACCommand-Interface.png new file mode 100644 index 0000000..7d9f94d Binary files /dev/null and b/contents/ReactiveObjC/images/RACCommand/RACCommand-Interface.png differ diff --git a/contents/ReactiveObjC/images/RACCommand/RACCommand-Side-Effect.png b/contents/ReactiveObjC/images/RACCommand/RACCommand-Side-Effect.png new file mode 100644 index 0000000..cf91aa0 Binary files /dev/null and b/contents/ReactiveObjC/images/RACCommand/RACCommand-Side-Effect.png differ diff --git a/contents/ReactiveObjC/images/RACCommand/What-is-RACCommand.png b/contents/ReactiveObjC/images/RACCommand/What-is-RACCommand.png new file mode 100644 index 0000000..2c9c289 Binary files /dev/null and b/contents/ReactiveObjC/images/RACCommand/What-is-RACCommand.png differ diff --git a/contents/ReactiveObjC/images/RACCommand/immediateExecuting-Signal-in-RACCommand.png b/contents/ReactiveObjC/images/RACCommand/immediateExecuting-Signal-in-RACCommand.png new file mode 100644 index 0000000..9a34a90 Binary files /dev/null and b/contents/ReactiveObjC/images/RACCommand/immediateExecuting-Signal-in-RACCommand.png differ diff --git a/contents/ReactiveObjC/images/RACCommand/raccommad-cover.jpg b/contents/ReactiveObjC/images/RACCommand/raccommad-cover.jpg new file mode 100644 index 0000000..dc011fe Binary files /dev/null and b/contents/ReactiveObjC/images/RACCommand/raccommad-cover.jpg differ diff --git a/contents/ReactiveObjC/images/RACDelegateProxy/After-Call-RACSignalForSelector.png b/contents/ReactiveObjC/images/RACDelegateProxy/After-Call-RACSignalForSelector.png new file mode 100644 index 0000000..96de284 Binary files /dev/null and b/contents/ReactiveObjC/images/RACDelegateProxy/After-Call-RACSignalForSelector.png differ diff --git a/contents/ReactiveObjC/images/RACDelegateProxy/Delegate-To-RACSignal.png b/contents/ReactiveObjC/images/RACDelegateProxy/Delegate-To-RACSignal.png new file mode 100644 index 0000000..928c83e Binary files /dev/null and b/contents/ReactiveObjC/images/RACDelegateProxy/Delegate-To-RACSignal.png differ diff --git a/contents/ReactiveObjC/images/RACDelegateProxy/Message-Forwarding.png b/contents/ReactiveObjC/images/RACDelegateProxy/Message-Forwarding.png new file mode 100644 index 0000000..bf823af Binary files /dev/null and b/contents/ReactiveObjC/images/RACDelegateProxy/Message-Forwarding.png differ diff --git a/contents/ReactiveObjC/images/RACDelegateProxy/NSObjectRACSignalForSelector.png b/contents/ReactiveObjC/images/RACDelegateProxy/NSObjectRACSignalForSelector.png new file mode 100644 index 0000000..ebab3c7 Binary files /dev/null and b/contents/ReactiveObjC/images/RACDelegateProxy/NSObjectRACSignalForSelector.png differ diff --git a/contents/ReactiveObjC/images/RACDelegateProxy/RACDelegateProxy-UITableView.gif b/contents/ReactiveObjC/images/RACDelegateProxy/RACDelegateProxy-UITableView.gif new file mode 100644 index 0000000..e28a316 Binary files /dev/null and b/contents/ReactiveObjC/images/RACDelegateProxy/RACDelegateProxy-UITableView.gif differ diff --git a/contents/ReactiveObjC/images/RACDelegateProxy/RACDelegateProxy.png b/contents/ReactiveObjC/images/RACDelegateProxy/RACDelegateProxy.png new file mode 100644 index 0000000..b45d90e Binary files /dev/null and b/contents/ReactiveObjC/images/RACDelegateProxy/RACDelegateProxy.png differ diff --git a/contents/ReactiveObjC/images/RACDelegateProxy/Selector-To-IMP.png b/contents/ReactiveObjC/images/RACDelegateProxy/Selector-To-IMP.png new file mode 100644 index 0000000..4609dc1 Binary files /dev/null and b/contents/ReactiveObjC/images/RACDelegateProxy/Selector-To-IMP.png differ diff --git a/contents/ReactiveObjC/images/RACDelegateProxy/Selector-To-ObjC-Message-Forward-With-RACSelector.png b/contents/ReactiveObjC/images/RACDelegateProxy/Selector-To-ObjC-Message-Forward-With-RACSelector.png new file mode 100644 index 0000000..69520a3 Binary files /dev/null and b/contents/ReactiveObjC/images/RACDelegateProxy/Selector-To-ObjC-Message-Forward-With-RACSelector.png differ diff --git a/contents/ReactiveObjC/images/RACDelegateProxy/Selector-To-ObjC-Message-Forward.png b/contents/ReactiveObjC/images/RACDelegateProxy/Selector-To-ObjC-Message-Forward.png new file mode 100644 index 0000000..5368ac9 Binary files /dev/null and b/contents/ReactiveObjC/images/RACDelegateProxy/Selector-To-ObjC-Message-Forward.png differ diff --git a/contents/ReactiveObjC/images/RACDelegateProxy/Swizzle-objc_msgForward.png b/contents/ReactiveObjC/images/RACDelegateProxy/Swizzle-objc_msgForward.png new file mode 100644 index 0000000..f3146fc Binary files /dev/null and b/contents/ReactiveObjC/images/RACDelegateProxy/Swizzle-objc_msgForward.png differ diff --git a/contents/ReactiveObjC/images/RACDelegateProxy/TypeEncoding.png b/contents/ReactiveObjC/images/RACDelegateProxy/TypeEncoding.png new file mode 100644 index 0000000..76571e2 Binary files /dev/null and b/contents/ReactiveObjC/images/RACDelegateProxy/TypeEncoding.png differ diff --git a/contents/ReactiveObjC/images/RACDelegateProxy/UITableViewDelegate-With-RACDelegateProxy.png b/contents/ReactiveObjC/images/RACDelegateProxy/UITableViewDelegate-With-RACDelegateProxy.png new file mode 100644 index 0000000..d401eb2 Binary files /dev/null and b/contents/ReactiveObjC/images/RACDelegateProxy/UITableViewDelegate-With-RACDelegateProxy.png differ diff --git a/contents/ReactiveObjC/images/RACDelegateProxy/delegate-banner.jpg b/contents/ReactiveObjC/images/RACDelegateProxy/delegate-banner.jpg new file mode 100644 index 0000000..7c68144 Binary files /dev/null and b/contents/ReactiveObjC/images/RACDelegateProxy/delegate-banner.jpg differ diff --git a/contents/ReactiveObjC/images/RACMulticastConnection/Difference-Between-Replay-Methods.png b/contents/ReactiveObjC/images/RACMulticastConnection/Difference-Between-Replay-Methods.png new file mode 100644 index 0000000..8fb44f8 Binary files /dev/null and b/contents/ReactiveObjC/images/RACMulticastConnection/Difference-Between-Replay-Methods.png differ diff --git a/contents/ReactiveObjC/images/RACMulticastConnection/RACMulticastConnection-Interface.png b/contents/ReactiveObjC/images/RACMulticastConnection/RACMulticastConnection-Interface.png new file mode 100644 index 0000000..8b5ea3c Binary files /dev/null and b/contents/ReactiveObjC/images/RACMulticastConnection/RACMulticastConnection-Interface.png differ diff --git a/contents/ReactiveObjC/images/RACMulticastConnection/RACMulticastConnection.png b/contents/ReactiveObjC/images/RACMulticastConnection/RACMulticastConnection.png new file mode 100644 index 0000000..78fc92f Binary files /dev/null and b/contents/ReactiveObjC/images/RACMulticastConnection/RACMulticastConnection.png differ diff --git a/contents/ReactiveObjC/images/RACMulticastConnection/RACSignal-And-Subscribe.png b/contents/ReactiveObjC/images/RACMulticastConnection/RACSignal-And-Subscribe.png new file mode 100644 index 0000000..b46d82a Binary files /dev/null and b/contents/ReactiveObjC/images/RACMulticastConnection/RACSignal-And-Subscribe.png differ diff --git a/contents/ReactiveObjC/images/RACMulticastConnection/RACSignal-RACMulticastConnection-Connect.png b/contents/ReactiveObjC/images/RACMulticastConnection/RACSignal-RACMulticastConnection-Connect.png new file mode 100644 index 0000000..034ac13 Binary files /dev/null and b/contents/ReactiveObjC/images/RACMulticastConnection/RACSignal-RACMulticastConnection-Connect.png differ diff --git a/contents/ReactiveObjC/images/RACMulticastConnection/RACSubject - Subclasses.png b/contents/ReactiveObjC/images/RACMulticastConnection/RACSubject - Subclasses.png new file mode 100644 index 0000000..4dfe482 Binary files /dev/null and b/contents/ReactiveObjC/images/RACMulticastConnection/RACSubject - Subclasses.png differ diff --git a/contents/ReactiveObjC/images/RACMulticastConnection/SubscribeNext-To-RACSubject-Before-Connect.png b/contents/ReactiveObjC/images/RACMulticastConnection/SubscribeNext-To-RACSubject-Before-Connect.png new file mode 100644 index 0000000..dc1a540 Binary files /dev/null and b/contents/ReactiveObjC/images/RACMulticastConnection/SubscribeNext-To-RACSubject-Before-Connect.png differ diff --git a/contents/ReactiveObjC/images/RACMulticastConnection/Values-From-RACSignal-To-Subscribers.png b/contents/ReactiveObjC/images/RACMulticastConnection/Values-From-RACSignal-To-Subscribers.png new file mode 100644 index 0000000..5eea888 Binary files /dev/null and b/contents/ReactiveObjC/images/RACMulticastConnection/Values-From-RACSignal-To-Subscribers.png differ diff --git a/contents/ReactiveObjC/images/RACMulticastConnection/connection-banner.jpg b/contents/ReactiveObjC/images/RACMulticastConnection/connection-banner.jpg new file mode 100644 index 0000000..c995135 Binary files /dev/null and b/contents/ReactiveObjC/images/RACMulticastConnection/connection-banner.jpg differ diff --git a/contents/ReactiveObjC/images/RACMulticastConnection/publish-and-multicast.png b/contents/ReactiveObjC/images/RACMulticastConnection/publish-and-multicast.png new file mode 100644 index 0000000..ab78439 Binary files /dev/null and b/contents/ReactiveObjC/images/RACMulticastConnection/publish-and-multicast.png differ diff --git a/contents/ReactiveObjC/images/RACScheduler/RACScheduler-Initializers.png b/contents/ReactiveObjC/images/RACScheduler/RACScheduler-Initializers.png new file mode 100644 index 0000000..bc824d4 Binary files /dev/null and b/contents/ReactiveObjC/images/RACScheduler/RACScheduler-Initializers.png differ diff --git a/contents/ReactiveObjC/images/RACScheduler/RACScheduler-Priority.png b/contents/ReactiveObjC/images/RACScheduler/RACScheduler-Priority.png new file mode 100644 index 0000000..e6f1a63 Binary files /dev/null and b/contents/ReactiveObjC/images/RACScheduler/RACScheduler-Priority.png differ diff --git a/contents/ReactiveObjC/images/RACScheduler/RACScheduler-Schedule.png b/contents/ReactiveObjC/images/RACScheduler/RACScheduler-Schedule.png new file mode 100644 index 0000000..e7bb551 Binary files /dev/null and b/contents/ReactiveObjC/images/RACScheduler/RACScheduler-Schedule.png differ diff --git a/contents/ReactiveObjC/images/RACScheduler/RACScheduler-Subclasses.png b/contents/ReactiveObjC/images/RACScheduler/RACScheduler-Subclasses.png new file mode 100644 index 0000000..07a37dd Binary files /dev/null and b/contents/ReactiveObjC/images/RACScheduler/RACScheduler-Subclasses.png differ diff --git a/contents/ReactiveObjC/images/RACScheduler/RACSubscriptionScheduler.png b/contents/ReactiveObjC/images/RACScheduler/RACSubscriptionScheduler.png new file mode 100644 index 0000000..3a4ee7b Binary files /dev/null and b/contents/ReactiveObjC/images/RACScheduler/RACSubscriptionScheduler.png differ diff --git a/contents/ReactiveObjC/images/RACScheduler/RACTargetQueueScheduler.png b/contents/ReactiveObjC/images/RACScheduler/RACTargetQueueScheduler.png new file mode 100644 index 0000000..510a24a Binary files /dev/null and b/contents/ReactiveObjC/images/RACScheduler/RACTargetQueueScheduler.png differ diff --git a/contents/ReactiveObjC/images/RACScheduler/schedule-header.jpg b/contents/ReactiveObjC/images/RACScheduler/schedule-header.jpg new file mode 100644 index 0000000..832d9f3 Binary files /dev/null and b/contents/ReactiveObjC/images/RACScheduler/schedule-header.jpg differ diff --git a/contents/ReactiveObjC/images/RACSequence/Call-Stacks-of-FoldLeft-FoldRight.png b/contents/ReactiveObjC/images/RACSequence/Call-Stacks-of-FoldLeft-FoldRight.png new file mode 100644 index 0000000..7407147 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSequence/Call-Stacks-of-FoldLeft-FoldRight.png differ diff --git a/contents/ReactiveObjC/images/RACSequence/EagerSequence - LazySequence.png b/contents/ReactiveObjC/images/RACSequence/EagerSequence - LazySequence.png new file mode 100644 index 0000000..62dd798 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSequence/EagerSequence - LazySequence.png differ diff --git a/contents/ReactiveObjC/images/RACSequence/FoldLeft - FoldRight.png b/contents/ReactiveObjC/images/RACSequence/FoldLeft - FoldRight.png new file mode 100644 index 0000000..d42eaae Binary files /dev/null and b/contents/ReactiveObjC/images/RACSequence/FoldLeft - FoldRight.png differ diff --git a/contents/ReactiveObjC/images/RACSequence/List-and-Stream.png b/contents/ReactiveObjC/images/RACSequence/List-and-Stream.png new file mode 100644 index 0000000..a257588 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSequence/List-and-Stream.png differ diff --git a/contents/ReactiveObjC/images/RACSequence/RACSequence - Subclasses.png b/contents/ReactiveObjC/images/RACSequence/RACSequence - Subclasses.png new file mode 100644 index 0000000..7654797 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSequence/RACSequence - Subclasses.png differ diff --git a/contents/ReactiveObjC/images/RACSequence/RACSequence-Instance.png b/contents/ReactiveObjC/images/RACSequence/RACSequence-Instance.png new file mode 100644 index 0000000..7db1a98 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSequence/RACSequence-Instance.png differ diff --git a/contents/ReactiveObjC/images/RACSequence/RACSequence-Status-Before-And-After-Executed.png b/contents/ReactiveObjC/images/RACSequence/RACSequence-Status-Before-And-After-Executed.png new file mode 100644 index 0000000..fbd0f76 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSequence/RACSequence-Status-Before-And-After-Executed.png differ diff --git a/contents/ReactiveObjC/images/RACSequence/RACSignal - RACSequence.png b/contents/ReactiveObjC/images/RACSequence/RACSignal - RACSequence.png new file mode 100644 index 0000000..a71c218 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSequence/RACSignal - RACSequence.png differ diff --git a/contents/ReactiveObjC/images/RACSequence/RACUnarySequence.png b/contents/ReactiveObjC/images/RACSequence/RACUnarySequence.png new file mode 100644 index 0000000..8003a47 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSequence/RACUnarySequence.png differ diff --git a/contents/ReactiveObjC/images/RACSequence/ReactiveCocoa - RACSequence.png b/contents/ReactiveObjC/images/RACSequence/ReactiveCocoa - RACSequence.png new file mode 100644 index 0000000..eb6d095 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSequence/ReactiveCocoa - RACSequence.png differ diff --git a/contents/ReactiveObjC/images/RACSequence/Transform Between RACSequence - RACSignal.png b/contents/ReactiveObjC/images/RACSequence/Transform Between RACSequence - RACSignal.png new file mode 100644 index 0000000..31e1e5b Binary files /dev/null and b/contents/ReactiveObjC/images/RACSequence/Transform Between RACSequence - RACSignal.png differ diff --git a/contents/ReactiveObjC/images/RACSequence/Transform RACSignal to RACSequence.png b/contents/ReactiveObjC/images/RACSequence/Transform RACSignal to RACSequence.png new file mode 100644 index 0000000..9860e66 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSequence/Transform RACSignal to RACSequence.png differ diff --git a/contents/ReactiveObjC/images/RACSequence/Transform-RACSequence-To-RACSignal.png b/contents/ReactiveObjC/images/RACSequence/Transform-RACSequence-To-RACSignal.png new file mode 100644 index 0000000..51c1f22 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSequence/Transform-RACSequence-To-RACSignal.png differ diff --git a/contents/ReactiveObjC/images/RACSequence/Unsolved-RACSequence-Instance.png b/contents/ReactiveObjC/images/RACSequence/Unsolved-RACSequence-Instance.png new file mode 100644 index 0000000..f1816c7 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSequence/Unsolved-RACSequence-Instance.png differ diff --git a/contents/ReactiveObjC/images/RACSequence/Usage for RACSignal - RACSequence Copy.png b/contents/ReactiveObjC/images/RACSequence/Usage for RACSignal - RACSequence Copy.png new file mode 100644 index 0000000..0f30630 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSequence/Usage for RACSignal - RACSequence Copy.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/Before-After-Bind-RACSignal-Complicated.png b/contents/ReactiveObjC/images/RACSignal/Before-After-Bind-RACSignal-Complicated.png new file mode 100644 index 0000000..5b7ce28 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/Before-After-Bind-RACSignal-Complicated.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/Before-After-Bind-RACSignal.png b/contents/ReactiveObjC/images/RACSignal/Before-After-Bind-RACSignal.png new file mode 100644 index 0000000..7af5a9a Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/Before-After-Bind-RACSignal.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/Principle-of-Subscribing-Signals.png b/contents/ReactiveObjC/images/RACSignal/Principle-of-Subscribing-Signals.png new file mode 100644 index 0000000..5bb266b Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/Principle-of-Subscribing-Signals.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/RACCompoundDisposable.png b/contents/ReactiveObjC/images/RACSignal/RACCompoundDisposable.png new file mode 100644 index 0000000..996e6ea Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/RACCompoundDisposable.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/RACDisposable-And-Subclasses.png b/contents/ReactiveObjC/images/RACSignal/RACDisposable-And-Subclasses.png new file mode 100644 index 0000000..125cb74 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/RACDisposable-And-Subclasses.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/RACDisposable.png b/contents/ReactiveObjC/images/RACSignal/RACDisposable.png new file mode 100644 index 0000000..3b8ab63 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/RACDisposable.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/RACSignal-Banner.png b/contents/ReactiveObjC/images/RACSignal/RACSignal-Banner.png new file mode 100644 index 0000000..bf127a9 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/RACSignal-Banner.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/RACSignal-Bind-Disposable.png b/contents/ReactiveObjC/images/RACSignal/RACSignal-Bind-Disposable.png new file mode 100644 index 0000000..f7e6ebe Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/RACSignal-Bind-Disposable.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/RACSignal-Bind.png b/contents/ReactiveObjC/images/RACSignal/RACSignal-Bind.png new file mode 100644 index 0000000..bee3511 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/RACSignal-Bind.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/RACSignal-Hierachy.png b/contents/ReactiveObjC/images/RACSignal/RACSignal-Hierachy.png new file mode 100644 index 0000000..00e8848 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/RACSignal-Hierachy.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/RACSignal-Instantiate-Object.png b/contents/ReactiveObjC/images/RACSignal/RACSignal-Instantiate-Object.png new file mode 100644 index 0000000..9087f60 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/RACSignal-Instantiate-Object.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/RACSignal-Return.png b/contents/ReactiveObjC/images/RACSignal/RACSignal-Return.png new file mode 100644 index 0000000..2d5e90c Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/RACSignal-Return.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/RACSignal-Subclasses.png b/contents/ReactiveObjC/images/RACSignal/RACSignal-Subclasses.png new file mode 100644 index 0000000..d016624 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/RACSignal-Subclasses.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/RACSignal-Subcribe-Process.png b/contents/ReactiveObjC/images/RACSignal/RACSignal-Subcribe-Process.png new file mode 100644 index 0000000..4cc0255 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/RACSignal-Subcribe-Process.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/RACSignal-Subcription-Messages-Sending.png b/contents/ReactiveObjC/images/RACSignal/RACSignal-Subcription-Messages-Sending.png new file mode 100644 index 0000000..677a9e1 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/RACSignal-Subcription-Messages-Sending.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/RACSignal-Subscribe-Methods.png b/contents/ReactiveObjC/images/RACSignal/RACSignal-Subscribe-Methods.png new file mode 100644 index 0000000..e50cf9c Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/RACSignal-Subscribe-Methods.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/RACStream-AbstractMethod.png b/contents/ReactiveObjC/images/RACSignal/RACStream-AbstractMethod.png new file mode 100644 index 0000000..f4b332f Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/RACStream-AbstractMethod.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/RACStream-Operation.png b/contents/ReactiveObjC/images/RACSignal/RACStream-Operation.png new file mode 100644 index 0000000..bd683e8 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/RACStream-Operation.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/What-is-RACSignal.png b/contents/ReactiveObjC/images/RACSignal/What-is-RACSignal.png new file mode 100644 index 0000000..ca16b54 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/What-is-RACSignal.png differ diff --git a/contents/ReactiveObjC/images/RACSignal/out-of-control.jpg b/contents/ReactiveObjC/images/RACSignal/out-of-control.jpg new file mode 100644 index 0000000..870d5fe Binary files /dev/null and b/contents/ReactiveObjC/images/RACSignal/out-of-control.jpg differ diff --git a/contents/ReactiveObjC/images/RACSubject/Hot-Signal-And-Cold-Signal.png b/contents/ReactiveObjC/images/RACSubject/Hot-Signal-And-Cold-Signal.png new file mode 100644 index 0000000..b0c936c Binary files /dev/null and b/contents/ReactiveObjC/images/RACSubject/Hot-Signal-And-Cold-Signal.png differ diff --git a/contents/ReactiveObjC/images/RACSubject/RACSubject - Subclasses.png b/contents/ReactiveObjC/images/RACSubject/RACSubject - Subclasses.png new file mode 100644 index 0000000..4dfe482 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSubject/RACSubject - Subclasses.png differ diff --git a/contents/ReactiveObjC/images/RACSubject/Send-Messages-to-RACReplaySubject.png b/contents/ReactiveObjC/images/RACSubject/Send-Messages-to-RACReplaySubject.png new file mode 100644 index 0000000..5ef54f3 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSubject/Send-Messages-to-RACReplaySubject.png differ diff --git a/contents/ReactiveObjC/images/RACSubject/Send-Messages-to-RACSubject.png b/contents/ReactiveObjC/images/RACSubject/Send-Messages-to-RACSubject.png new file mode 100644 index 0000000..d532538 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSubject/Send-Messages-to-RACSubject.png differ diff --git a/contents/ReactiveObjC/images/RACSubject/Send-Subscibe-to-RACSubject.png b/contents/ReactiveObjC/images/RACSubject/Send-Subscibe-to-RACSubject.png new file mode 100644 index 0000000..cdaf768 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSubject/Send-Subscibe-to-RACSubject.png differ diff --git a/contents/ReactiveObjC/images/RACSubject/Track-RACBehaviorSubject-Subscription-Process-With-Default-Value.png b/contents/ReactiveObjC/images/RACSubject/Track-RACBehaviorSubject-Subscription-Process-With-Default-Value.png new file mode 100644 index 0000000..46c04a7 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSubject/Track-RACBehaviorSubject-Subscription-Process-With-Default-Value.png differ diff --git a/contents/ReactiveObjC/images/RACSubject/Track-RACBehaviorSubject-Subscription-Process.png b/contents/ReactiveObjC/images/RACSubject/Track-RACBehaviorSubject-Subscription-Process.png new file mode 100644 index 0000000..2452ef2 Binary files /dev/null and b/contents/ReactiveObjC/images/RACSubject/Track-RACBehaviorSubject-Subscription-Process.png differ diff --git a/contents/ReactiveObjC/images/RACSubject/Track-RACReplaySubject-Subscription-Process.png b/contents/ReactiveObjC/images/RACSubject/Track-RACReplaySubject-Subscription-Process.png new file mode 100644 index 0000000..b64962e Binary files /dev/null and b/contents/ReactiveObjC/images/RACSubject/Track-RACReplaySubject-Subscription-Process.png differ diff --git a/contents/ReactiveObjC/images/RACSubject/Track-RACSubject-Subscription-Process.png b/contents/ReactiveObjC/images/RACSubject/Track-RACSubject-Subscription-Process.png new file mode 100644 index 0000000..cdf80ea Binary files /dev/null and b/contents/ReactiveObjC/images/RACSubject/Track-RACSubject-Subscription-Process.png differ diff --git "a/contents/ReactiveObjC/images/RACSubject/\342\200\234Mutable\342\200\235 RACSignal \342\200\224 RACSubject.png" "b/contents/ReactiveObjC/images/RACSubject/\342\200\234Mutable\342\200\235 RACSignal \342\200\224 RACSubject.png" new file mode 100644 index 0000000..3a1f4b1 Binary files /dev/null and "b/contents/ReactiveObjC/images/RACSubject/\342\200\234Mutable\342\200\235 RACSignal \342\200\224 RACSubject.png" differ diff --git a/contents/Redis/images/I:O-Multiplexing-Model.png b/contents/Redis/images/I:O-Multiplexing-Model.png new file mode 100644 index 0000000..a1a6d34 Binary files /dev/null and b/contents/Redis/images/I:O-Multiplexing-Model.png differ diff --git a/contents/Redis/images/ae-module.jpg b/contents/Redis/images/ae-module.jpg new file mode 100644 index 0000000..4dad920 Binary files /dev/null and b/contents/Redis/images/ae-module.jpg differ diff --git a/contents/Redis/images/blocking-io.png b/contents/Redis/images/blocking-io.png new file mode 100644 index 0000000..9c84b83 Binary files /dev/null and b/contents/Redis/images/blocking-io.png differ diff --git a/contents/Redis/images/eventloop-file-event-in-redis.png b/contents/Redis/images/eventloop-file-event-in-redis.png new file mode 100644 index 0000000..12b6f35 Binary files /dev/null and b/contents/Redis/images/eventloop-file-event-in-redis.png differ diff --git a/contents/Redis/images/process-end.png b/contents/Redis/images/process-end.png new file mode 100644 index 0000000..b60b559 Binary files /dev/null and b/contents/Redis/images/process-end.png differ diff --git a/contents/Redis/images/process-time-event.png b/contents/Redis/images/process-time-event.png new file mode 100644 index 0000000..da3fbba Binary files /dev/null and b/contents/Redis/images/process-time-event.png differ diff --git a/contents/Redis/images/process-time-events-in-redis.png b/contents/Redis/images/process-time-events-in-redis.png new file mode 100644 index 0000000..de2e7d3 Binary files /dev/null and b/contents/Redis/images/process-time-events-in-redis.png differ diff --git a/contents/Redis/images/redis-choose-io-function.jpg b/contents/Redis/images/redis-choose-io-function.jpg new file mode 100644 index 0000000..8ed15bd Binary files /dev/null and b/contents/Redis/images/redis-choose-io-function.jpg differ diff --git a/contents/Redis/images/redis-cli-banner.jpg b/contents/Redis/images/redis-cli-banner.jpg new file mode 100644 index 0000000..5bc7816 Binary files /dev/null and b/contents/Redis/images/redis-cli-banner.jpg differ diff --git a/contents/Redis/images/redis-client-process-commands.jpg b/contents/Redis/images/redis-client-process-commands.jpg new file mode 100644 index 0000000..16e9ff3 Binary files /dev/null and b/contents/Redis/images/redis-client-process-commands.jpg differ diff --git a/contents/Redis/images/redis-client-server.jpg b/contents/Redis/images/redis-client-server.jpg new file mode 100644 index 0000000..a2f0b6a Binary files /dev/null and b/contents/Redis/images/redis-client-server.jpg differ diff --git a/contents/Redis/images/redis-eventloop-logo.jpg b/contents/Redis/images/redis-eventloop-logo.jpg new file mode 100644 index 0000000..3be5879 Binary files /dev/null and b/contents/Redis/images/redis-eventloop-logo.jpg differ diff --git a/contents/Redis/images/redis-eventloop-proces-event.png b/contents/Redis/images/redis-eventloop-proces-event.png new file mode 100644 index 0000000..36973ba Binary files /dev/null and b/contents/Redis/images/redis-eventloop-proces-event.png differ diff --git a/contents/Redis/images/redis-lldb-cmd.png b/contents/Redis/images/redis-lldb-cmd.png new file mode 100644 index 0000000..0093a1a Binary files /dev/null and b/contents/Redis/images/redis-lldb-cmd.png differ diff --git a/contents/Redis/images/redis-lldb-nwritten.png b/contents/Redis/images/redis-lldb-nwritten.png new file mode 100644 index 0000000..853b2d9 Binary files /dev/null and b/contents/Redis/images/redis-lldb-nwritten.png differ diff --git a/contents/Redis/images/redis-lldb-read.png b/contents/Redis/images/redis-lldb-read.png new file mode 100644 index 0000000..2b21a2f Binary files /dev/null and b/contents/Redis/images/redis-lldb-read.png differ diff --git a/contents/Redis/images/redis-reactor-pattern.png b/contents/Redis/images/redis-reactor-pattern.png new file mode 100644 index 0000000..c4801e8 Binary files /dev/null and b/contents/Redis/images/redis-reactor-pattern.png differ diff --git a/contents/Redis/images/redis-resp-data-byte.jpg b/contents/Redis/images/redis-resp-data-byte.jpg new file mode 100644 index 0000000..1cf432c Binary files /dev/null and b/contents/Redis/images/redis-resp-data-byte.jpg differ diff --git a/contents/Redis/images/redis-resp-type-and-examples.jpg b/contents/Redis/images/redis-resp-type-and-examples.jpg new file mode 100644 index 0000000..da2a89d Binary files /dev/null and b/contents/Redis/images/redis-resp-type-and-examples.jpg differ diff --git a/contents/Redis/images/reids-eventloop.png b/contents/Redis/images/reids-eventloop.png new file mode 100644 index 0000000..e862312 Binary files /dev/null and b/contents/Redis/images/reids-eventloop.png differ diff --git a/contents/Redis/redis-cli.md b/contents/Redis/redis-cli.md new file mode 100644 index 0000000..8015089 --- /dev/null +++ b/contents/Redis/redis-cli.md @@ -0,0 +1,496 @@ +# Redis 是如何处理命令的(客户端) + +在使用 Redis 的过程中经常会好奇,在 Redis-Cli 中键入 `SET KEY MSG` 并回车之后,Redis 客户端和服务是如何对命令进行解析处理的,而在内部的实现过程是什么样的。 + +这两篇文章会分别介绍 Redis 客户端和服务端分别对命令是如何处理的,本篇文章介绍的是 Redis 客户端如何处理输入的命令、向服务发送命令以及取得服务端回复并输出到终端等过程。 + +![redis-client-serve](images/redis-client-server.jpg) + +文章中会将 Redis 服务看做一个输入为 Redis 命令,输出为命令执行结果的黑箱,对从命令到结果的过程不做任何解释,只会着眼于客户端的逻辑,也就是上图中的 1 和 4 两个过程。 + +## 从 main 函数开始 + +与其它的 C 语言框架/服务类似,Redis 的客户端 `redis-cli` 也是从 `main` 函数开始执行的,位于 `redis-cli.c` 文件的最后: + +```c +int main(int argc, char **argv) { + ... + if (argc == 0 && !config.eval) { + repl(); + } + ... +} +``` + +在一般情况下,Redis 客户端都会进入 `repl` 模式,对输入进行解析; + +> Redis 中有好多模式,包括:Latency、Slave、Pipe、Stat、Scan、LRU test 等等模式,不过这些模式都不是这篇文章关注的重点,我们只会关注最常见的 repl 模式。 + +```c +static void repl(void) { + char *line; + int argc; + sds *argv; + + ... + + while((line = linenoise(context ? config.prompt : "not connected> ")) != NULL) { + if (line[0] != '\0') { + argv = cliSplitArgs(line,&argc); + + if (argv == NULL) { + printf("Invalid argument(s)\n"); + continue; + } + if (strcasecmp(argv[0],"???") == 0) { + ... + } else { + issueCommandRepeat(argc, argv, 1); + } + } + } + exit(0); +} +``` + +在上述代码中,我们省略了大量的实现细节,只保留整个 `repl` 中循环的主体部分,方便进行理解和分析,在 `while` 循环中的条件你可以看到 `linenoise` 方法的调用,通过其中的 `prompt` 和 `not connected> ` 可以判断出,这里向终端中输出了提示符,同时会调用 `fgets` 从标准输入中读取字符串: + +```c +127.0.0.1:6379> +``` + +全局搜一下 `config.prompt` 不难发现这一行代码,也就是控制命令行提示的 `prompt`: + +```c +anetFormatAddr(config.prompt, sizeof(config.prompt),config.hostip, config.hostport); +``` + +接下来执行的 `cliSplitArgs` 函数会将 `line` 中的字符串分割成几个不同的参数,然后根据字符串 `argv[0]` 的不同执行的命令,在这里省略了很多原有的代码: + +```c +if (strcasecmp(argv[0],"quit") == 0 || + strcasecmp(argv[0],"exit") == 0) +{ + exit(0); +} else if (argv[0][0] == ':') { + cliSetPreferences(argv,argc,1); + continue; +} else if (strcasecmp(argv[0],"restart") == 0) { + ... +} else if (argc == 3 && !strcasecmp(argv[0],"connect")) { + ... +} else if (argc == 1 && !strcasecmp(argv[0],"clear")) { +} else { + issueCommandRepeat(argc, argv, 1); +} +``` + +在遇到 `quit`、`exit` 等跟**客户端状态有关的命令**时,就会直接执行相应的代码;否则就会将命令和参数 `issueCommandRepeat` 函数。 + +### 追踪一次命令的执行 + +> Redis Commit: `790310d89460655305bd615bc442eeaf7f0f1b38` +> +> lldb: lldb-360.1.65 +> +> macOS 10.11.6 + +在继续分析 `issueCommandRepeat` 之前,我们先对 Redis 中的这部分代码进行调试追踪,在使用 `make` 编译了 Redis 源代码,启动 `redis-server` 之后;启动 lldb 对 Redis 客户端进行调试: + +```shell +$ lldb src/redis-cli +(lldb) target create "src/redis-cli" +Current executable set to 'src/redis-cli' (x86_64). +(lldb) b redis-cli.c:1290 +Breakpoint 1: where = redis-cli`repl + 228 at redis-cli.c:1290, address = 0x0000000100008cd4 +(lldb) process launch +Process 8063 launched: '~/redis/src/redis-cli' (x86_64) +127.0.0.1:6379> +``` + +在 `redis-cli.c:1290` 也就是下面这行代码的地方打断点之后: + +```c +-> 1290 if (line[0] != '\0') { +``` + +执行 `process launch` 启动 `redis-cli`,然后输入 `SET KEY MSG` 回车以及 Ctrl-C: + +> 在 lldb 中调试时,回车的输入经常会有问题,在这里输入 Ctrl-C 进入信号处理器,在通过 continue 命令进入断点: + +```c +127.0.0.1:6379> SET KEY MSG +^C +8063 stopped +* thread #1: tid = 0xa95147, 0x00007fff90923362 libsystem_kernel.dylib`read + 10, stop reason = signal SIGSTOP + frame #0: 0x00007fff90923362 libsystem_kernel.dylib`read + 10 +libsystem_kernel.dylib`read: +-> 0x7fff90923362 <+10>: jae 0x7fff9092336c ; <+20> + 0x7fff90923364 <+12>: movq %rax, %rdi + 0x7fff90923367 <+15>: jmp 0x7fff9091c7f2 ; cerror + 0x7fff9092336c <+20>: retq +(lldb) c +Process 8063 resuming + +Process 8063 stopped +* thread #1: tid = 0xa95147, 0x0000000100008cd4 redis-cli`repl + 228 at redis-cli.c:1290, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 + frame #0: 0x0000000100008cd4 redis-cli`repl + 228 at redis-cli.c:1290 + 1287 + 1288 cliRefreshPrompt(); + 1289 while((line = linenoise(context ? config.prompt : "not connected> ")) != NULL) { +-> 1290 if (line[0] != '\0') { + 1291 argv = cliSplitArgs(line,&argc); + 1292 if (history) linenoiseHistoryAdd(line); + 1293 if (historyfile) linenoiseHistorySave(historyfile); +(lldb) +``` + +输入两次 `n` 之后,打印 `argv` 和 `argc` 的值: + +```c +(lldb) p argc +(int) $1 = 3 +(lldb) p *argv +(sds) $2 = 0x0000000100106cc3 "SET" +(lldb) p *(argv+1) +(sds) $3 = 0x0000000100106ce3 "KEY" +(lldb) p *(argv+2) +(sds) $4 = 0x0000000100106cf3 "MSG" +(lldb) p line +(char *) $5 = 0x0000000100303430 "SET KEY MSG\n" +``` + +`cliSplitArgs` 方法成功将 `line` 中的字符串分隔成字符串参数,在多次执行 `n` 之后,进入 `issueCommandRepeat` 方法: + +```c +-> 1334 issueCommandRepeat(argc-skipargs, argv+skipargs, repeat); +``` + +## 对输入命令的处理 + +上一阶段执行 `issueCommandRepeat` 的函数调用栈中,会发现 Redis 并不会直接把所有的命令发送到服务端: + +```c +issueCommandRepeat + cliSendCommand + redisAppendCommandArgv + redisFormatCommandArgv + __redisAppendCommand +``` + +而是会在 `redisFormatCommandArgv` 中对所有的命令进行格式化处理,将字符串转换为符合 RESP 协议的数据。 + +### RESP 协议 + +Redis 客户端与 Redis 服务进行通讯时,会使用名为 **RESP**(REdis Serialization Protocol) 的协议,它的使用非常简单,并且可以序列化多种数据类型包括整数、字符串以及数组等。 + +对于 RESP 协议的详细介绍可以看官方文档中的 [Redis Protocol specification](https://redis.io/topics/protocol),在这里对这个协议进行简单的介绍。 + +在将不同的数据类型序列化时,会使用第一个 byte 来表示当前数据的数据类型,以便在客户端或服务器在处理时能恢复原来的数据格式。 + +![redis-resp-data-byte](images/redis-resp-data-byte.jpg) + +举一个简单的例子,字符串 `OK` 以及错误`Error Message` 等不同种类的信息的 RESP 表示如下: + +![redis-resp-type-and-examples](images/redis-resp-type-and-examples.jpg) + +在这篇文章中我们需要简单了解的就是 RESP “数据格式”的**第一个字节用来表示数据类型**,然后**逻辑上属于不同部分的内容通过 CRLF(\r\n)分隔**。 + +### 数据格式的转换 + +在 `redisFormatCommandArgv` 方法中几乎没有需要删减的代码,所有的命令都会以字符串数组的形式发送到客户端: + +```c +int redisFormatCommandArgv(char **target, int argc, const char **argv, const size_t *argvlen) { + char *cmd = NULL; + int pos; + size_t len; + int totlen, j; + + totlen = 1+intlen(argc)+2; + for (j = 0; j < argc; j++) { + len = argvlen ? argvlen[j] : strlen(argv[j]); + totlen += bulklen(len); + } + + cmd = malloc(totlen+1); + if (cmd == NULL) + return -1; + + pos = sprintf(cmd,"*%d\r\n",argc); + for (j = 0; j < argc; j++) { + len = argvlen ? argvlen[j] : strlen(argv[j]); + pos += sprintf(cmd+pos,"$%zu\r\n",len); + memcpy(cmd+pos,argv[j],len); + pos += len; + cmd[pos++] = '\r'; + cmd[pos++] = '\n'; + } + assert(pos == totlen); + cmd[pos] = '\0'; + + *target = cmd; + return totlen; +} +``` + +`SET KEY MSG` 这一命令,经过这个方法的处理会变成: + +```c +*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$3\r\nMSG\r\n +``` + +你可以这么理解上面的结果: + +```c +*3\r\n + $3\r\nSET\r\n + $3\r\nKEY\r\n + $3\r\nMSG\r\n +``` + +这是一个由三个字符串组成的数组,数组中的元素是 `SET`、`KEY` 以及 `MSG` 三个字符串。 + +如果在这里打一个断点并输出 `target` 中的内容: + +![redis-lldb-cmd](images/redis-lldb-cmd.png) + +到这里就完成了对输入命令的格式化,在格式化之后还会将当前命令写入全局的 `redisContext` 的 `write` 缓冲区 `obuf` 中,也就是在上面的缓冲区看到的第二个方法: + +```c +int __redisAppendCommand(redisContext *c, const char *cmd, size_t len) { + sds newbuf; + + newbuf = sdscatlen(c->obuf,cmd,len); + if (newbuf == NULL) { + __redisSetError(c,REDIS_ERR_OOM,"Out of memory"); + return REDIS_ERR; + } + + c->obuf = newbuf; + return REDIS_OK; +} +``` + +### redisContext + +再继续介绍下一部分之前需要简单介绍一下 `redisContext` 结构体: + +```c +typedef struct redisContext { + int err; + char errstr[128]; + int fd; + int flags; + char *obuf; + redisReader *reader; +} redisContext; +``` + +每一个 `redisContext` 的结构体都表示一个 Redis 客户端对服务的连接,而这个上下文会在每一个 redis-cli 中作为静态变量仅保存一个: + +```c +static redisContext *context; +``` + +`obuf` 中包含了客户端未写到服务端的数据;而 `reader` 是用来处理 RESP 协议的结构体;`fd` 就是 Redis 服务对应的文件描述符;其他的内容就不多做解释了。 + +到这里,对命令的格式化处理就结束了,接下来就到了向服务端发送命令的过程了。 + +## 向服务器发送命令 + +与对输入命令的处理差不多,向服务器发送命令的方法也在 `issueCommandRepeat` 的调用栈中,而且藏得更深,如果不仔细阅读源代码其实很难发现: + +```c +issueCommandRepeat + cliSendCommand + cliReadReply + redisGetReply + redisBufferWrite +``` + +Redis 在 `redisGetReply` 中完成对命令的发送: + +```c +int redisGetReply(redisContext *c, void **reply) { + int wdone = 0; + void *aux = NULL; + + if (aux == NULL && c->flags & REDIS_BLOCK) { + do { + if (redisBufferWrite(c,&wdone) == REDIS_ERR) + return REDIS_ERR; + } while (!wdone); + + ... + } while (aux == NULL); + } + + if (reply != NULL) *reply = aux; + return REDIS_OK; +} +``` + +上面的代码向 `redisBufferWrite` 函数中传递了全局的静态变量 `redisContext`,其中的 `obuf` 中存储了没有向 Redis 服务发送的命令: + +```c +int redisBufferWrite(redisContext *c, int *done) { + int nwritten; + + if (sdslen(c->obuf) > 0) { + nwritten = write(c->fd,c->obuf,sdslen(c->obuf)); + if (nwritten == -1) { + if ((errno == EAGAIN && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) { + } else { + __redisSetError(c,REDIS_ERR_IO,NULL); + return REDIS_ERR; + } + } else if (nwritten > 0) { + if (nwritten == (signed)sdslen(c->obuf)) { + sdsfree(c->obuf); + c->obuf = sdsempty(); + } else { + sdsrange(c->obuf,nwritten,-1); + } + } + } + if (done != NULL) *done = (sdslen(c->obuf) == 0); + return REDIS_OK; +} +``` + +代码的逻辑其实十分清晰,调用 `write` 向 Redis 服务代表的文件描述符发送写缓冲区 `obuf` 中的数据,然后根据返回值做出相应的处理,如果命令发送成功就会清空 `obuf` 并将 `done` 指针标记为真,然后返回,这样就完成了向服务器发送命令这一过程。 + +![redis-lldb-nwritten](images/redis-lldb-nwritten.png) + +## 获取服务器回复 + +其实获取服务器回复和上文中的发送命令过程基本上差不多,调用栈也几乎完全一样: + +```c +issueCommandRepeat + cliSendCommand + cliReadReply + redisGetReply + redisBufferRead + redisGetReplyFromReader + cliFormatReplyRaw + fwrite +``` + +同样地,在 `redisGetReply` 中获取服务器的响应: + +```c +int redisGetReply(redisContext *c, void **reply) { + int wdone = 0; + void *aux = NULL; + + if (aux == NULL && c->flags & REDIS_BLOCK) { + do { + if (redisBufferWrite(c,&wdone) == REDIS_ERR) + return REDIS_ERR; + } while (!wdone); + + do { + if (redisBufferRead(c) == REDIS_ERR) + return REDIS_ERR; + if (redisGetReplyFromReader(c,&aux) == REDIS_ERR) + return REDIS_ERR; + } while (aux == NULL); + } + + if (reply != NULL) *reply = aux; + return REDIS_OK; +} +``` + +在 `redisBufferWrite` 成功发送命令并返回之后,就会开始等待服务端的回复,总共分为两个部分,一是使用 `redisBufferRead` 从服务端读取原始格式的回复(符合 RESP 协议): + +```c +int redisBufferRead(redisContext *c) { + char buf[1024*16]; + int nread; + + nread = read(c->fd,buf,sizeof(buf)); + if (nread == -1) { + if ((errno == EAGAIN && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) { + } else { + __redisSetError(c,REDIS_ERR_IO,NULL); + return REDIS_ERR; + } + } else if (nread == 0) { + __redisSetError(c,REDIS_ERR_EOF,"Server closed the connection"); + return REDIS_ERR; + } else { + if (redisReaderFeed(c->reader,buf,nread) != REDIS_OK) { + __redisSetError(c,c->reader->err,c->reader->errstr); + return REDIS_ERR; + } + } + return REDIS_OK; +} +``` + +在 `read` 从文件描述符中成功读取数据并返回之后,我们可以打印 `buf` 中的内容: + +![redis-lldb-read](images/redis-lldb-read.png) + +刚刚向 `buf` 中写入的数据还需要经过 `redisReaderFeed` 方法的处理,截取正确的长度;然后存入 `redisReader` 中: + +```c +int redisReaderFeed(redisReader *r, const char *buf, size_t len) { + sds newbuf; + + if (buf != NULL && len >= 1) { + if (r->len == 0 && r->maxbuf != 0 && sdsavail(r->buf) > r->maxbuf) { + sdsfree(r->buf); + r->buf = sdsempty(); + r->pos = 0; + assert(r->buf != NULL); + } + + newbuf = sdscatlen(r->buf,buf,len); + if (newbuf == NULL) { + __redisReaderSetErrorOOM(r); + return REDIS_ERR; + } + + r->buf = newbuf; + r->len = sdslen(r->buf); + } + + return REDIS_OK; +} +``` + +最后的 `redisGetReplyFromReader` 方法会从 `redisContext` 中取出 `reader`,然后反序列化 RESP 对象,最后打印出来。 + +![process-end](images/process-end.png) + +当我们从终端的输出中看到了 OK 以及这个命令的执行的时间时,`SET KEY MSG` 这一命令就已经处理完成了。 + +## 总结 + +处理命令的过程在客户端还是比较简单的: + +1. 在一个 `while` 循环中,输出提示符; +2. 接收到输入命令时,对输入命令进行格式化处理; +3. 通过 `write` 发送到 Redis 服务,并调用 `read` 阻塞当前进程直到服务端返回为止; +4. 对服务端返回的数据反序列化; +5. 将结果打印到终端。 + +用一个简单的图表示,大概是这样的: + +![redis-client-process-commands](images/redis-client-process-commands.jpg) + +## References + ++ [Redis Protocol specification](https://redis.io/topics/protocol) ++ [Redis 和 I/O 多路复用](http://draveness.me/redis-io-multiplexing/) ++ [Redis 中的事件循环](http://draveness.me/redis-eventloop) + + +> Follow: [Draveness · GitHub](https://github.com/Draveness) +> +> Source: http://draveness.me/redis-cli + diff --git a/contents/Redis/redis-eventloop.md b/contents/Redis/redis-eventloop.md new file mode 100644 index 0000000..cf72789 --- /dev/null +++ b/contents/Redis/redis-eventloop.md @@ -0,0 +1,330 @@ +# Redis 中的事件循环 + +在目前的很多服务中,由于需要持续接受客户端或者用户的输入,所以需要一个事件循环来等待并处理外部事件,这篇文章主要会介绍 Redis 中的事件循环是如何处理事件的。 + +在文章中,我们会先从 Redis 的实现中分析事件是如何被处理的,然后用更具象化的方式了解服务中的不同模块是如何交流的。 + +## aeEventLoop + +在分析具体代码之前,先了解一下在事件处理中处于核心部分的 `aeEventLoop` 到底是什么: + +![reids-eventloop](./images/reids-eventloop.png) + +`aeEventLoop` 在 Redis 就是负责保存待处理文件事件和时间事件的结构体,其中保存大量事件执行的上下文信息,同时持有三个事件数组: + ++ `aeFileEvent` ++ `aeTimeEvent` ++ `aeFiredEvent` + +`aeFileEvent` 和 `aeTimeEvent` 中会存储监听的文件事件和时间事件,而最后的 `aeFiredEvent` 用于存储待处理的文件事件,我们会在后面的章节中介绍它们是如何工作的。 + +### Redis 服务中的 EventLoop + +在 `redis-server` 启动时,首先会初始化一些 redis 服务的配置,最后会调用 `aeMain` 函数陷入 `aeEventLoop` 循环中,等待外部事件的发生: + +```c +int main(int argc, char **argv) { + ... + + aeMain(server.el); +} +``` + +`aeMain` 函数其实就是一个封装的 `while` 循环,循环中的代码会一直运行直到 `eventLoop` 的 `stop` 被设置为 `true`: + +```c +void aeMain(aeEventLoop *eventLoop) { + eventLoop->stop = 0; + while (!eventLoop->stop) { + if (eventLoop->beforesleep != NULL) + eventLoop->beforesleep(eventLoop); + aeProcessEvents(eventLoop, AE_ALL_EVENTS); + } +} +``` + +它会不停尝试调用 `aeProcessEvents` 对可能存在的多种事件进行处理,而 `aeProcessEvents` 就是实际用于处理事件的函数: + +```c +int aeProcessEvents(aeEventLoop *eventLoop, int flags) { + int processed = 0, numevents; + + if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0; + + if (eventLoop->maxfd != -1 || + ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) { + struct timeval *tvp; + + #1:计算 I/O 多路复用的等待时间 tvp + + numevents = aeApiPoll(eventLoop, tvp); + for (int j = 0; j < numevents; j++) { + aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd]; + int mask = eventLoop->fired[j].mask; + int fd = eventLoop->fired[j].fd; + int rfired = 0; + + if (fe->mask & mask & AE_READABLE) { + rfired = 1; + fe->rfileProc(eventLoop,fd,fe->clientData,mask); + } + if (fe->mask & mask & AE_WRITABLE) { + if (!rfired || fe->wfileProc != fe->rfileProc) + fe->wfileProc(eventLoop,fd,fe->clientData,mask); + } + processed++; + } + } + if (flags & AE_TIME_EVENTS) processed += processTimeEvents(eventLoop); + return processed; +} +``` + +上面的代码省略了 I/O 多路复用函数的等待时间,不过不会影响我们对代码的理解,整个方法大体由两部分代码组成,一部分处理文件事件,另一部分处理时间事件。 + +> Redis 中会处理两种事件:时间事件和文件事件。 + +### 文件事件 + +在一般情况下,`aeProcessEvents` 都会先**计算最近的时间事件发生所需要等待的时间**,然后调用 `aeApiPoll` 方法在这段时间中等待事件的发生,在这段时间中如果发生了文件事件,就会优先处理文件事件,否则就会一直等待,直到最近的时间事件需要触发: + +```c +numevents = aeApiPoll(eventLoop, tvp); +for (j = 0; j < numevents; j++) { + aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd]; + int mask = eventLoop->fired[j].mask; + int fd = eventLoop->fired[j].fd; + int rfired = 0; + + if (fe->mask & mask & AE_READABLE) { + rfired = 1; + fe->rfileProc(eventLoop,fd,fe->clientData,mask); + } + if (fe->mask & mask & AE_WRITABLE) { + if (!rfired || fe->wfileProc != fe->rfileProc) + fe->wfileProc(eventLoop,fd,fe->clientData,mask); + } + processed++; +} +``` + +文件事件如果绑定了对应的读/写事件,就会执行对应的对应的代码,并传入事件循环、文件描述符、数据以及掩码: + +```c +fe->rfileProc(eventLoop,fd,fe->clientData,mask); +fe->wfileProc(eventLoop,fd,fe->clientData,mask); +``` + +其中 `rfileProc` 和 `wfileProc` 就是在文件事件被创建时传入的函数指针: + +```c +int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask, aeFileProc *proc, void *clientData) { + aeFileEvent *fe = &eventLoop->events[fd]; + + if (aeApiAddEvent(eventLoop, fd, mask) == -1) + return AE_ERR; + fe->mask |= mask; + if (mask & AE_READABLE) fe->rfileProc = proc; + if (mask & AE_WRITABLE) fe->wfileProc = proc; + fe->clientData = clientData; + if (fd > eventLoop->maxfd) + eventLoop->maxfd = fd; + return AE_OK; +} +``` + +需要注意的是,传入的 `proc` 函数会在对应的 `mask` 位事件发生时执行。 + +### 时间事件 + +在 Redis 中会发生两种时间事件: + ++ 一种是定时事件,每隔一段时间会执行一次; ++ 另一种是非定时事件,只会在某个时间点执行一次; + +时间事件的处理在 `processTimeEvents` 中进行,我们会分三部分分析这个方法的实现: + +```c +static int processTimeEvents(aeEventLoop *eventLoop) { + int processed = 0; + aeTimeEvent *te, *prev; + long long maxId; + time_t now = time(NULL); + + if (now < eventLoop->lastTime) { + te = eventLoop->timeEventHead; + while(te) { + te->when_sec = 0; + te = te->next; + } + } + eventLoop->lastTime = now; +``` + +由于对系统时间的调整会影响当前时间的获取,进而影响时间事件的执行;如果系统时间先被设置到了未来的时间,又设置成正确的值,这就会导致**时间事件会随机延迟一段时间执行**,也就是说,时间事件不会按照预期的安排尽早执行,而 `eventLoop` 中的 `lastTime` 就是用于检测上述情况的变量: + +```c +typedef struct aeEventLoop { + ... + time_t lastTime; /* Used to detect system clock skew */ + ... +} aeEventLoop; +``` + +如果发现了系统时间被改变(小于上次 `processTimeEvents` 函数执行的开始时间),就会强制所有时间事件尽早执行。 + +```c + prev = NULL; + te = eventLoop->timeEventHead; + maxId = eventLoop->timeEventNextId-1; + while(te) { + long now_sec, now_ms; + long long id; + + if (te->id == AE_DELETED_EVENT_ID) { + aeTimeEvent *next = te->next; + if (prev == NULL) + eventLoop->timeEventHead = te->next; + else + prev->next = te->next; + if (te->finalizerProc) + te->finalizerProc(eventLoop, te->clientData); + zfree(te); + te = next; + continue; + } +``` + +Redis 处理时间事件时,不会在当前循环中直接移除不再需要执行的事件,而是会在当前循环中将时间事件的 `id` 设置为 `AE_DELETED_EVENT_ID`,然后再下一个循环中删除,并执行绑定的 `finalizerProc`。 + +```c + aeGetTime(&now_sec, &now_ms); + if (now_sec > te->when_sec || + (now_sec == te->when_sec && now_ms >= te->when_ms)) + { + int retval; + + id = te->id; + retval = te->timeProc(eventLoop, id, te->clientData); + processed++; + if (retval != AE_NOMORE) { + aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms); + } else { + te->id = AE_DELETED_EVENT_ID; + } + } + prev = te; + te = te->next; + } + return processed; +} +``` + +在移除不需要执行的时间事件之后,我们就开始通过比较时间来判断是否需要调用 `timeProc` 函数,`timeProc` 函数的返回值 `retval` 为时间事件执行的时间间隔: + ++ `retval == AE_NOMORE`:将时间事件的 `id` 设置为 `AE_DELETED_EVENT_ID`,等待下次 `aeProcessEvents` 执行时将事件清除; ++ `retval != AE_NOMORE`:修改当前时间事件的执行时间并重复利用当前的时间事件; + +以使用 `aeCreateTimeEvent` 一个创建的简单时间事件为例: + +```c +aeCreateTimeEvent(config.el,1,showThroughput,NULL,NULL) +``` + +时间事件对应的函数 `showThroughput` 在每次执行时会返回一个数字,也就是该事件发生的时间间隔: + +```c +int showThroughput(struct aeEventLoop *eventLoop, long long id, void *clientData) { + ... + float dt = (float)(mstime()-config.start)/1000.0; + float rps = (float)config.requests_finished/dt; + printf("%s: %.2f\r", config.title, rps); + fflush(stdout); + return 250; /* every 250ms */ +} +``` + +这样就不需要重新 `malloc` 一块相同大小的内存,提高了时间事件处理的性能,并减少了内存的使用量。 + +我们对 Redis 中对时间事件的处理以流程图的形式简单总结一下: + +![process-time-event](./images/process-time-event.png) + +创建时间事件的方法实现其实非常简单,在这里不想过多分析这个方法,唯一需要注意的就是时间事件的 `id` 跟数据库中的大多数主键都是递增的: + +```c +long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds, + aeTimeProc *proc, void *clientData, + aeEventFinalizerProc *finalizerProc) { + long long id = eventLoop->timeEventNextId++; + aeTimeEvent *te; + + te = zmalloc(sizeof(*te)); + if (te == NULL) return AE_ERR; + te->id = id; + aeAddMillisecondsToNow(milliseconds,&te->when_sec,&te->when_ms); + te->timeProc = proc; + te->finalizerProc = finalizerProc; + te->clientData = clientData; + te->next = eventLoop->timeEventHead; + eventLoop->timeEventHead = te; + return id; +} +``` + +## 事件的处理 + +> 上一章节我们已经从代码的角度对 Redis 中事件的处理有一定的了解,在这里,我想从更高的角度来观察 Redis 对于事件的处理是怎么进行的。 + +整个 Redis 服务在启动之后会陷入一个巨大的 while 循环,不停地执行 `processEvents` 方法处理文件事件 fe 和时间事件 te 。 + +> 有关 Redis 中的 I/O 多路复用模块可以看这篇文章 [Redis 和 I/O 多路复用](http://draveness.me/redis-io-multiplexing/)。 + +当文件事件触发时会被标记为 “红色” 交由 `processEvents` 方法处理,而时间事件的处理都会交给 `processTimeEvents` 这一子方法: + +![redis-eventloop-proces-event](./images/redis-eventloop-proces-event.png) + +在每个事件循环中 Redis 都会先处理文件事件,然后再处理时间事件直到整个循环停止,`processEvents` 和 `processTimeEvents` 作为 Redis 中发生事件的消费者,每次都会从“事件池”中拉去待处理的事件进行消费。 + +### 文件事件的处理 + +由于文件事件触发条件较多,并且 OS 底层实现差异性较大,底层的 I/O 多路复用模块使用了 `eventLoop->aeFiredEvent` 保存对应的文件描述符以及事件,将信息传递给上层进行处理,并抹平了底层实现的差异。 + +整个 I/O 多路复用模块在事件循环看来就是一个输入事件、输出 `aeFiredEvent` 数组的一个黑箱: + +![eventloop-file-event-in-redis](./images/eventloop-file-event-in-redis.png) + +在这个黑箱中,我们使用 `aeCreateFileEvent`、 `aeDeleteFileEvent` 来添加删除需要监听的文件描述符以及事件。 + +在对应事件发生时,当前单元格会“变色”表示发生了可读(黄色)或可写(绿色)事件,调用 `aeApiPoll` 时会把对应的文件描述符和事件放入 `aeFiredEvent` 数组,并在 `processEvents` 方法中执行事件对应的回调。 + +### 时间事件的处理 + +时间事件的处理相比文件事件就容易多了,每次 `processTimeEvents` 方法调用时都会对整个 `timeEventHead` 数组进行遍历: + +![process-time-events-in-redis](./images/process-time-events-in-redis.png) + +遍历的过程中会将时间的触发时间与当前时间比较,然后执行时间对应的 `timeProc`,并根据 `timeProc` 的返回值修改当前事件的参数,并在下一个循环的遍历中移除不再执行的时间事件。 + +## 总结 + +> 笔者对于文章中两个模块的展示顺序考虑了比较久的时间,最后还是觉得,目前这样的顺序更易于理解。 + +Redis 对于事件的处理方式十分精巧,通过传入函数指针以及返回值的方式,将时间事件移除的控制权交给了需要执行的处理器 `timeProc`,在 `processTimeEvents` 设置 `aeApiPoll` 超时时间也十分巧妙,充分地利用了每一次事件循环,防止过多的无用的空转,并且保证了该方法不会阻塞太长时间。 + +事件循环的机制并不能时间事件准确地在某一个时间点一定执行,往往会比实际约定处理的时间稍微晚一些。 + +## Reference + ++ [Redis Event Library](https://redis.io/topics/internals-rediseventlib) ++ [Redis Core Implementation](http://key-value-stories.blogspot.com/2015/01/redis-core-implementation.html) ++ [Redis 和 I/O 多路复用](http://draveness.me/redis-io-multiplexing/) ++ [Redis 设计与实现](http://redisbook.com) + +## 其它 + +> Follow: [Draveness · GitHub](https://github.com/Draveness) +> +> Source: http://draveness.me/redis-eventloop + + diff --git a/contents/Redis/redis-io-multiplexing.md b/contents/Redis/redis-io-multiplexing.md new file mode 100644 index 0000000..bc869d3 --- /dev/null +++ b/contents/Redis/redis-io-multiplexing.md @@ -0,0 +1,298 @@ +# Redis 和 I/O 多路复用 + +最近在看 UNIX 网络编程并研究了一下 Redis 的实现,感觉 Redis 的源代码十分适合阅读和分析,其中 I/O 多路复用(mutiplexing)部分的实现非常干净和优雅,在这里想对这部分的内容进行简单的整理。 + +## 几种 I/O 模型 + +为什么 Redis 中要使用 I/O 多路复用这种技术呢? + +首先,Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 **I/O 多路复用**就是为了解决这个问题而出现的。 + +### Blocking I/O + +先来看一下传统的阻塞 I/O 模型到底是如何工作的:当使用 `read` 或者 `write` 对某一个**文件描述符(File Descriptor 以下简称 FD)**进行读写时,如果当前 FD 不可读或不可写,整个 Redis 服务就不会对其它的操作作出响应,导致整个服务不可用。 + +这也就是传统意义上的,也就是我们在编程中使用最多的阻塞模型: + +![blocking-io](images/blocking-io.png) + +阻塞模型虽然开发中非常常见也非常易于理解,但是由于它会影响其他 FD 对应的服务,所以在需要处理多个客户端任务的时候,往往都不会使用阻塞模型。 + +### I/O 多路复用 + +> 虽然还有很多其它的 I/O 模型,但是在这里都不会具体介绍。 + +阻塞式的 I/O 模型并不能满足这里的需求,我们需要一种效率更高的 I/O 模型来支撑 Redis 的多个客户(redis-cli),这里涉及的就是 I/O 多路复用模型了: + +![I:O-Multiplexing-Mode](images/I:O-Multiplexing-Model.png) + +在 I/O 多路复用模型中,最重要的函数调用就是 `select`,该方法的能够同时监控多个文件描述符的可读可写情况,当其中的某些文件描述符可读或者可写时,`select` 方法就会返回可读以及可写的文件描述符个数。 + +> 关于 `select` 的具体使用方法,在网络上资料很多,这里就不过多展开介绍了; +> +> 与此同时也有其它的 I/O 多路复用函数 `epoll/kqueue/evport`,它们相比 `select` 性能更优秀,同时也能支撑更多的服务。 + +## Reactor 设计模式 + +Redis 服务采用 Reactor 的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符) + +![redis-reactor-pattern](images/redis-reactor-pattern.png) + +文件事件处理器使用 I/O 多路复用模块同时监听多个 FD,当 `accept`、`read`、`write` 和 `close` 文件事件产生时,文件事件处理器就会回调 FD 绑定的事件处理器。 + +虽然整个文件事件处理器是在单线程上运行的,但是通过 I/O 多路复用模块的引入,实现了同时对多个 FD 读写的监控,提高了网络通信模型的性能,同时也可以保证整个 Redis 服务实现的简单。 + +## I/O 多路复用模块 + +I/O 多路复用模块封装了底层的 `select`、`epoll`、`avport` 以及 `kqueue` 这些 I/O 多路复用函数,为上层提供了相同的接口。 + +![ae-module](images/ae-module.jpg) + +在这里我们简单介绍 Redis 是如何包装 `select` 和 `epoll` 的,简要了解该模块的功能,整个 I/O 多路复用模块抹平了不同平台上 I/O 多路复用函数的差异性,提供了相同的接口: + ++ `static int aeApiCreate(aeEventLoop *eventLoop)` ++ `static int aeApiResize(aeEventLoop *eventLoop, int setsize)` ++ `static void aeApiFree(aeEventLoop *eventLoop)` ++ `static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask)` ++ `static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask) ` ++ `static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)` + +同时,因为各个函数所需要的参数不同,我们在每一个子模块内部通过一个 `aeApiState` 来存储需要的上下文信息: + +```c +// select +typedef struct aeApiState { + fd_set rfds, wfds; + fd_set _rfds, _wfds; +} aeApiState; + +// epoll +typedef struct aeApiState { + int epfd; + struct epoll_event *events; +} aeApiState; +``` + +这些上下文信息会存储在 `eventLoop` 的 `void *state` 中,不会暴露到上层,只在当前子模块中使用。 + +### 封装 select 函数 + +> `select` 可以监控 FD 的可读、可写以及出现错误的情况。 + +在介绍 I/O 多路复用模块如何对 `select` 函数封装之前,先来看一下 `select` 函数使用的大致流程: + +```c +int fd = /* file descriptor */ + +fd_set rfds; +FD_ZERO(&rfds); +FD_SET(fd, &rfds) + +for ( ; ; ) { + select(fd+1, &rfds, NULL, NULL, NULL); + if (FD_ISSET(fd, &rfds)) { + /* file descriptor `fd` becomes readable */ + } +} +``` + +1. 初始化一个可读的 `fd_set` 集合,保存需要监控可读性的 FD; +2. 使用 `FD_SET` 将 `fd` 加入 `rfds`; +3. 调用 `select` 方法监控 `rfds` 中的 FD 是否可读; +4. 当 `select` 返回时,检查 FD 的状态并完成对应的操作。 + +而在 Redis 的 `ae_select` 文件中代码的组织顺序也是差不多的,首先在 `aeApiCreate` 函数中初始化 `rfds` 和 `wfds`: + +```c +static int aeApiCreate(aeEventLoop *eventLoop) { + aeApiState *state = zmalloc(sizeof(aeApiState)); + if (!state) return -1; + FD_ZERO(&state->rfds); + FD_ZERO(&state->wfds); + eventLoop->apidata = state; + return 0; +} +``` + +而 `aeApiAddEvent` 和 `aeApiDelEvent` 会通过 `FD_SET` 和 `FD_CLR` 修改 `fd_set` 中对应 FD 的标志位: + +```c +static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) { + aeApiState *state = eventLoop->apidata; + if (mask & AE_READABLE) FD_SET(fd,&state->rfds); + if (mask & AE_WRITABLE) FD_SET(fd,&state->wfds); + return 0; +} +``` + +整个 `ae_select` 子模块中最重要的函数就是 `aeApiPoll`,它是实际调用 `select` 函数的部分,其作用就是在 I/O 多路复用函数返回时,将对应的 FD 加入 `aeEventLoop` 的 `fired` 数组中,并返回事件的个数: + +```c +static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) { + aeApiState *state = eventLoop->apidata; + int retval, j, numevents = 0; + + memcpy(&state->_rfds,&state->rfds,sizeof(fd_set)); + memcpy(&state->_wfds,&state->wfds,sizeof(fd_set)); + + retval = select(eventLoop->maxfd+1, + &state->_rfds,&state->_wfds,NULL,tvp); + if (retval > 0) { + for (j = 0; j <= eventLoop->maxfd; j++) { + int mask = 0; + aeFileEvent *fe = &eventLoop->events[j]; + + if (fe->mask == AE_NONE) continue; + if (fe->mask & AE_READABLE && FD_ISSET(j,&state->_rfds)) + mask |= AE_READABLE; + if (fe->mask & AE_WRITABLE && FD_ISSET(j,&state->_wfds)) + mask |= AE_WRITABLE; + eventLoop->fired[numevents].fd = j; + eventLoop->fired[numevents].mask = mask; + numevents++; + } + } + return numevents; +} +``` + +### 封装 epoll 函数 + +Redis 对 `epoll` 的封装其实也是类似的,使用 `epoll_create` 创建 `epoll` 中使用的 `epfd`: + +```c +static int aeApiCreate(aeEventLoop *eventLoop) { + aeApiState *state = zmalloc(sizeof(aeApiState)); + + if (!state) return -1; + state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize); + if (!state->events) { + zfree(state); + return -1; + } + state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */ + if (state->epfd == -1) { + zfree(state->events); + zfree(state); + return -1; + } + eventLoop->apidata = state; + return 0; +} +``` + +在 `aeApiAddEvent` 中使用 `epoll_ctl` 向 `epfd` 中添加需要监控的 FD 以及监听的事件: + +```c +static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) { + aeApiState *state = eventLoop->apidata; + struct epoll_event ee = {0}; /* avoid valgrind warning */ + /* If the fd was already monitored for some event, we need a MOD + * operation. Otherwise we need an ADD operation. */ + int op = eventLoop->events[fd].mask == AE_NONE ? + EPOLL_CTL_ADD : EPOLL_CTL_MOD; + + ee.events = 0; + mask |= eventLoop->events[fd].mask; /* Merge old events */ + if (mask & AE_READABLE) ee.events |= EPOLLIN; + if (mask & AE_WRITABLE) ee.events |= EPOLLOUT; + ee.data.fd = fd; + if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1; + return 0; +} +``` + +由于 `epoll` 相比 `select` 机制略有不同,在 `epoll_wait` 函数返回时并不需要遍历所有的 FD 查看读写情况;在 `epoll_wait` 函数返回时会提供一个 `epoll_event` 数组: + +```c +typedef union epoll_data { + void *ptr; + int fd; /* 文件描述符 */ + uint32_t u32; + uint64_t u64; +} epoll_data_t; + +struct epoll_event { + uint32_t events; /* Epoll 事件 */ + epoll_data_t data; +}; +``` + +> 其中保存了发生的 `epoll` 事件(`EPOLLIN`、`EPOLLOUT`、`EPOLLERR` 和 `EPOLLHUP`)以及发生该事件的 FD。 + +`aeApiPoll` 函数只需要将 `epoll_event` 数组中存储的信息加入 `eventLoop` 的 `fired` 数组中,将信息传递给上层模块: + +```c +static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) { + aeApiState *state = eventLoop->apidata; + int retval, numevents = 0; + + retval = epoll_wait(state->epfd,state->events,eventLoop->setsize, + tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1); + if (retval > 0) { + int j; + + numevents = retval; + for (j = 0; j < numevents; j++) { + int mask = 0; + struct epoll_event *e = state->events+j; + + if (e->events & EPOLLIN) mask |= AE_READABLE; + if (e->events & EPOLLOUT) mask |= AE_WRITABLE; + if (e->events & EPOLLERR) mask |= AE_WRITABLE; + if (e->events & EPOLLHUP) mask |= AE_WRITABLE; + eventLoop->fired[j].fd = e->data.fd; + eventLoop->fired[j].mask = mask; + } + } + return numevents; +} +``` + +### 子模块的选择 + +因为 Redis 需要在多个平台上运行,同时为了最大化执行的效率与性能,所以会根据编译平台的不同选择不同的 I/O 多路复用函数作为子模块,提供给上层统一的接口;在 Redis 中,我们通过宏定义的使用,合理的选择不同的子模块: + +```c +#ifdef HAVE_EVPORT +#include "ae_evport.c" +#else + #ifdef HAVE_EPOLL + #include "ae_epoll.c" + #else + #ifdef HAVE_KQUEUE + #include "ae_kqueue.c" + #else + #include "ae_select.c" + #endif + #endif +#endif +``` + +因为 `select` 函数是作为 POSIX 标准中的系统调用,在不同版本的操作系统上都会实现,所以将其作为保底方案: + +![redis-choose-io-function](images/redis-choose-io-function.jpg) + +Redis 会优先选择时间复杂度为 $O(1)$ 的 I/O 多路复用函数作为底层实现,包括 Solaries 10 中的 `evport`、Linux 中的 `epoll` 和 macOS/FreeBSD 中的 `kqueue`,上述的这些函数都使用了内核内部的结构,并且能够服务几十万的文件描述符。 + +但是如果当前编译环境没有上述函数,就会选择 `select` 作为备选方案,由于其在使用时会扫描全部监听的描述符,所以其时间复杂度较差 $O(n)$,并且只能同时服务 1024 个文件描述符,所以一般并不会以 `select` 作为第一方案使用。 + +## 总结 + +Redis 对于 I/O 多路复用模块的设计非常简洁,通过宏保证了 I/O 多路复用模块在不同平台上都有着优异的性能,将不同的 I/O 多路复用函数封装成相同的 API 提供给上层使用。 + +整个模块使 Redis 能以单进程运行的同时服务成千上万个文件描述符,避免了由于多进程应用的引入导致代码实现复杂度的提升,减少了出错的可能性。 + +## Reference + ++ [Select-Man-Pages](http://man7.org/linux/man-pages/man2/select.2.html) ++ [Reactor-Pattern](https://en.wikipedia.org/wiki/Reactor_pattern) ++ [epoll vs kqueue](https://people.eecs.berkeley.edu/~sangjin/2012/12/21/epoll-vs-kqueue.html) + +## 其它 + +> Follow: [Draveness · GitHub](https://github.com/Draveness) +> +> Source: http://draveness.me/redis-io-multiplexing + + diff --git a/contents/Ruby/images/sidekiq/Async-Schedule.jpg b/contents/Ruby/images/sidekiq/Async-Schedule.jpg new file mode 100644 index 0000000..cbac454 Binary files /dev/null and b/contents/Ruby/images/sidekiq/Async-Schedule.jpg differ diff --git a/contents/Ruby/images/sidekiq/Client-Push-Item.jpg b/contents/Ruby/images/sidekiq/Client-Push-Item.jpg new file mode 100644 index 0000000..1062962 Binary files /dev/null and b/contents/Ruby/images/sidekiq/Client-Push-Item.jpg differ diff --git a/contents/Ruby/images/sidekiq/Client-Redis-Sidekiq-Worker.jpg b/contents/Ruby/images/sidekiq/Client-Redis-Sidekiq-Worker.jpg new file mode 100644 index 0000000..d08de7d Binary files /dev/null and b/contents/Ruby/images/sidekiq/Client-Redis-Sidekiq-Worker.jpg differ diff --git a/contents/Ruby/images/sidekiq/Job-in-Redis.jpg b/contents/Ruby/images/sidekiq/Job-in-Redis.jpg new file mode 100644 index 0000000..c79a7a2 Binary files /dev/null and b/contents/Ruby/images/sidekiq/Job-in-Redis.jpg differ diff --git a/contents/Ruby/images/sidekiq/Launcher-Poller-Manager-Processors.jpg b/contents/Ruby/images/sidekiq/Launcher-Poller-Manager-Processors.jpg new file mode 100644 index 0000000..e696d16 Binary files /dev/null and b/contents/Ruby/images/sidekiq/Launcher-Poller-Manager-Processors.jpg differ diff --git a/contents/Ruby/images/sidekiq/Middlewares-Client-Redis-Sidekiq-Worker.jpg b/contents/Ruby/images/sidekiq/Middlewares-Client-Redis-Sidekiq-Worker.jpg new file mode 100644 index 0000000..aad305e Binary files /dev/null and b/contents/Ruby/images/sidekiq/Middlewares-Client-Redis-Sidekiq-Worker.jpg differ diff --git a/contents/Ruby/images/sidekiq/Perform-async-in-Redis.jpg b/contents/Ruby/images/sidekiq/Perform-async-in-Redis.jpg new file mode 100644 index 0000000..f3b98cb Binary files /dev/null and b/contents/Ruby/images/sidekiq/Perform-async-in-Redis.jpg differ diff --git a/contents/Ruby/images/sidekiq/Redis-Sidekiq-Poller.jpg b/contents/Ruby/images/sidekiq/Redis-Sidekiq-Poller.jpg new file mode 100644 index 0000000..52f8c30 Binary files /dev/null and b/contents/Ruby/images/sidekiq/Redis-Sidekiq-Poller.jpg differ diff --git a/contents/Ruby/images/sidekiq/Redis-Sorted-Set.jpg b/contents/Ruby/images/sidekiq/Redis-Sorted-Set.jpg new file mode 100644 index 0000000..597a0ed Binary files /dev/null and b/contents/Ruby/images/sidekiq/Redis-Sorted-Set.jpg differ diff --git a/contents/Ruby/images/sidekiq/Sidekiq-Arch.jpg b/contents/Ruby/images/sidekiq/Sidekiq-Arch.jpg new file mode 100644 index 0000000..1c2bda2 Binary files /dev/null and b/contents/Ruby/images/sidekiq/Sidekiq-Arch.jpg differ diff --git a/contents/Ruby/images/sidekiq/Sidekiq-Cover.jpg b/contents/Ruby/images/sidekiq/Sidekiq-Cover.jpg new file mode 100644 index 0000000..89db8d3 Binary files /dev/null and b/contents/Ruby/images/sidekiq/Sidekiq-Cover.jpg differ diff --git a/contents/Ruby/images/sidekiq/Sidekiq-Middlewares.jpg b/contents/Ruby/images/sidekiq/Sidekiq-Middlewares.jpg new file mode 100644 index 0000000..7630a76 Binary files /dev/null and b/contents/Ruby/images/sidekiq/Sidekiq-Middlewares.jpg differ diff --git a/contents/Ruby/images/sidekiq/Sidekiq-Multi-Processes.jpg b/contents/Ruby/images/sidekiq/Sidekiq-Multi-Processes.jpg new file mode 100644 index 0000000..8af8b1c Binary files /dev/null and b/contents/Ruby/images/sidekiq/Sidekiq-Multi-Processes.jpg differ diff --git a/contents/Ruby/images/sidekiq/sidekiq-logo.png b/contents/Ruby/images/sidekiq/sidekiq-logo.png new file mode 100644 index 0000000..703e98e Binary files /dev/null and b/contents/Ruby/images/sidekiq/sidekiq-logo.png differ diff --git a/contents/Ruby/sidekiq.md b/contents/Ruby/sidekiq.md new file mode 100644 index 0000000..4601dfd --- /dev/null +++ b/contents/Ruby/sidekiq.md @@ -0,0 +1,656 @@ +# Sidekiq 如何处理异步任务 + +[Sidekiq](https://github.com/mperham/sidekiq) 是 Ruby 和 Rails 项目中常用的后台任务处理系统,其本身提供的 API 十分简洁,源代码也非常易于阅读,是一个轻量级的异步处理组件;虽然其本身没有提供太多复杂的功能,但是它的使用和部署非常简单。在这篇文章中,我们将对 Sidekiq 的实现原理进行介绍和分析。 + +![Sidekiq-Cove](images/sidekiq/Sidekiq-Cover.jpg) + +文章中并不会详细介绍 Sidekiq 的使用,也并不是一篇 Sidekiq 的教程,在这里我们会介绍任务的入队过程、Sidekiq 任务在 Redis 中的存储方式和消费者对任务的处理过程,除此之外,文章将介绍 Sidekiq 中间件的实现以及任务重试的原理。 + +## 概述 + +在具体分析介绍 Sidekiq 的实现原理之前,我们需要对整个组件的使用过程进行概述,保证我们对 Sidekiq 的结构有一个总体上的了解。 + +```ruby +class HardWorker + include Sidekiq::Worker + def perform(name, count) + # do something + end +end + +HardWorker.perform_async('bob', 5) +``` + +在这里,我们直接照搬 Sidekiq Wiki 中 [Getting Started](https://github.com/mperham/sidekiq/wiki/Getting-Started) 部分的代码简单展示下它是如何使用的,当我们执行 `HardWorker.perform_async` 方法时,Sidekiq 的 Worker 会将一个异步任务以 JSON 的形式将相关的信息加入 Redis 中并等待消费者对任务的拉取和处理。 + +![Sidekiq-Arch](images/sidekiq/Sidekiq-Arch.jpg) + +Sidekiq 的消费者有三个部分组成,分别是 `Manager`、`Processor` 和 `Poller`;他们三者会相互协作共同完成对 Redis 中任务消费的过程。 + +> 需要注意的是,Sidekiq 中的 `Sidekiq::Worker` 并不是真正用于处理任务的 Worker,负责执行执行任务的类型其实是 `Sidekiq::Processor`;在文章中,当我们提到 Sidekiq Worker 时,其实说的是 `Sidekiq::Processor`,当我们使用了形如 `Sidekiq::Worker` 或者 `Worker` 的形式时,我们说的就是对应的类。 + +## 异步任务的入队 + +当我们对需要异步执行的任务调用类似 `Worker.perform_async` 的方法时,Sidekiq 其实并不会真正去创建一个 `HardWorker` 等 `Worker` 的对象,它实际上会调用 `Worker.client_push` 方法并将当前的 `class` 和 `args` 参数传进去,也就是需要异步执行的类和参数: + +```ruby +def perform_async(*args) + client_push('class'.freeze => self, 'args'.freeze => args) +end +``` + +除了 `Worker.perform_async` 之外,`Worker` 还提供了另外一对用于**在一段时间之后或者某个时间点**执行相应任务的方法 `Worker.perform_at` 和 `Worker.perform_in`: + +```ruby +def perform_in(interval, *args) + int = interval.to_f + now = Time.now.to_f + ts = (int < 1_000_000_000 ? now + int : int) + item = { 'class'.freeze => self, 'args'.freeze => args, 'at'.freeze => ts } + item.delete('at'.freeze) if ts <= now + client_push(item) +end +alias_method :perform_at, :perform_in +``` + +为了使用同一个接口支持两种不同的安排方式(时间点和多久之后),方法内部对传入的 `internal` 进行了判断,当 `interval.to_f < 1_000_000_000` 时就会在一段时间之后执行任务,否则就会以时间点的方式执行任务,虽然 `Worker.perform_at` 和 `Worker.perform_in` 是完全相同的方法,不过我们在使用时还是尽量遵循方法的语义选择两者中更符合逻辑的方法。 + +![Client-Push-Item](images/sidekiq/Client-Push-Item.jpg) + +两种创建异步任务的方式,最终都执行了 `Worker.client_push` 方法并传入了一个哈希,其中可能包含以上三个部分的内容;在方法的实现中,它获取了上下文中的 Redis 池并将传入的 `item` 对象传入 Redis 中: + +```ruby +def client_push(item) + pool = Thread.current[:sidekiq_via_pool] || get_sidekiq_options['pool'.freeze] || Sidekiq.redis_pool + item.keys.each do |key| + item[key.to_s] = item.delete(key) + end + Sidekiq::Client.new(pool).push(item) +end +``` + +简单整理一下,从 `Worker.perform_async` 方法到 `Client#push` 方法整个过程都在对即将加入到 Redis 中队列的哈希进行操作,从添加 `at` 字段到字符串化、再到 `Client#normalize_item` 方法中添加 `jid` 和 `created_at` 字段。 + +```ruby +def push(item) + normed = normalize_item(item) + payload = process_single(item['class'.freeze], normed) + + if payload + raw_push([payload]) + payload['jid'.freeze] + end +end +``` + +所有添加异步任务的方法最终都调用了私有方法 `Client#raw_push` 以及 `Client#atomic_push` 向 Redis 中添加数据,在这时会有两种不同的情况发生,当异步任务需要在未来的某一时间点进行安排时,它会加入 Redis 的一个有序集合: + +```ruby +def atomc_push(conn, payloads) + if payloads.first['at'.freeze] + conn.zadd('schedule'.freeze, payloads.map do |hash| + at = hash.delete('at'.freeze).to_s + [at, Sidekiq.dump_json(hash)] + end) + else + # ... + end +end +``` + +在这个有序集合中,Sidekiq 理所应当地将 `schedule` 作为权重,而其他的全部字段都以 JSON 的格式作为负载传入;但是当 Sidekiq 遇到需要立即执行的异步任务时,实现就有一些不同了: + +```ruby +def atomc_push(conn, payloads) + if payloads.first['at'.freeze] + # ... + else + q = payloads.first['queue'.freeze] + now = Time.now.to_f + to_push = payloads.map do |entry| + entry['enqueued_at'.freeze] = now + Sidekiq.dump_json(entry) + end + conn.sadd('queues'.freeze, q) + conn.lpush("queue:#{q}", to_push) + end +end +``` + +除了设置当前任务的入队时间 `enqueued_at` 之外,Sidekiq 将队列加入到一个大队列 `queues` 的集合中,并且将负载直接推到 `"queue:#{q}"` 数组中等待消费者的拉取,我们稍微梳理一下两种安排异步队列方法的调用过程: + +![Async-Schedule](images/sidekiq/Async-Schedule.jpg) + +### Redis 中的存储 + +无论是立即执行还是需要安排的异步任务都会进入 Redis 的队列中,但是它们之间还是有一些区别的,`Worker.perform_in/at` 会将任务以 `[at, args]` 的形式加入到 `schedules` 有序集中,而 +`Worker.perform_async` 将负载加入到指定的队列,并向整个 Sidekiq 的队列集合 `queues` 中添加该队列。 + +![Perform-async-in-Redis](images/sidekiq/Perform-async-in-Redis.jpg) + +所有的 `payload` 中都包含了一个异步任务需要执行的全部信息,包括该任务的执行的队列 `queue`、异步队列的类 `class`、参数 `args` 以及 `sidekiq_options` 中的全部参数。 + +![Job-in-Redis](images/sidekiq/Job-in-Redis.jpg) + +除了上述参数,一个异步任务还包含诸如 `created_at`、`enqueued_at` 等信息,也有一个通过 `SecureRandom.hex(12)` 生成的任务唯一标识符 `jid`。 + +## Sidekiq 的启动过程 + +作者对于 Sidekiq 印象最深刻的就是它在命令行启动的时候输出的一个字符画,我们能在 `cli.rb` 的 `Cli.banner` 方法中找到这个字符画: + +``` + m, + `$b + .ss, $$: .,d$ + `$$P,d$P' .,md$P"' + ,$$$$$bmmd$$$P^' + .d$$$$$$$$$$P' + $$^' `"^$$$' ____ _ _ _ _ + $: ,$$: / ___|(_) __| | ___| | _(_) __ _ + `b :$$ \___ \| |/ _` |/ _ \ |/ / |/ _` | + $$: ___) | | (_| | __/ <| | (_| | + $$ |____/|_|\__,_|\___|_|\_\_|\__, | + .d$$ |_| +``` + +这一节也将介绍 Sidekiq 的启动过程,在 `bin` 文件夹中的 sidekiq 文件包含的内容就是在命令行执行 `sidekiq` 时执行的代码: + +```ruby +begin + cli = Sidekiq::CLI.instance + cli.parse + cli.run +rescue => e + # ... +end +``` + +这里的代码就是创建了一个 `CLI` 对象,执行 `CLI#parse` 方法对参数进行解析,最后调用 `CLI#run` 方法: + +```ruby +def run + print_banner + + self_read, self_write = IO.pipe + # ... + + launcher = Sidekiq::Launcher.new(options) + begin + launcher.run + while readable_io = IO.select([self_read]) + signal = readable_io.first[0].gets.strip + handle_signal(signal) + end + rescue Interrupt + launcher.stop + end +end +``` + +### 从 Launcher 到 Manager + +`CLI#run` 在执行最开始就会打印 banner,也就是我们在每次启动 Sidekiq 时看到的字符画,而在之后会执行 `Launcher#run` 运行用于处理异步任务的 `Processor` 等对象。 + +![Launcher-Poller-Manager-Processors](images/sidekiq/Launcher-Poller-Manager-Processors.jpg) + +每一个 `Launcher` 都会启动一个 `Manager` 对象和一个 `Poller`,其中 `Manager` 同时管理了多个 `Processor` 对象,这些不同的类之间有着如上图所示的关系。 + +```ruby +def run + @thread = safe_thread("heartbeat", &method(:start_heartbeat)) + @poller.start + @manager.start +end +``` + +`Manager` 会在初始化时根据传入的 `concurrency` 的值创建对应数量的 `Processor`,默认的并行数量为 25;当执行 `Manager#start` 时,就会启动对应数量的**线程**和处理器开始对任务进行处理: + +```ruby +class Manager + def start + @workers.each do |x| + x.start + end + end +end + +class Processor + def start + @thread ||= safe_thread("processor", &method(:run)) + end +end +``` + +从 `Launcher` 的启动到现在只是一个调用 `initialize` 和 `start` 方法的过程,再加上 Sidekiq 源代码非常简单,所以阅读起没有丝毫的难度,也就不做太多的解释了。 + +### 并行模型 + +当处理器开始执行 `Processor#run` 方法时,就开始对所有的任务进行处理了;从总体来看,Sidekiq 使用了多线程的模型对任务进行处理,每一个 `Processor` 都是使用了 `safe_thread` 方法在一个新的线程里面运行的: + +```ruby +def safe_thread(name, &block) + Thread.new do + Thread.current['sidekiq_label'.freeze] = name + watchdog(name, &block) + end +end +``` + +在使用 Sidekiq 时,我们也会在不同的机器上开启多个 Sidekiq Worker,也就是说 Sidekiq 可以以多进程、多线程的方式运行,同时处理大量的异步任务。 + +![Sidekiq-Multi-Processes](images/sidekiq/Sidekiq-Multi-Processes.jpg) + +到目前为止,我们已经分析了异步任务的入队以及 Sidekiq Worker 的启动过程了,接下来即将分析 Sidekiq 对异步任务的处理过程。 + +### 『主题』的订阅 + +作为一个 Sidekiq Worker 进程,它在启动时就会决定选择订阅哪些『主题』去执行,比如当我们使用下面的命令时: + +```sh +> sidekiq -q critical,2 -q default +``` + +`CLI#parse` 方法会对传入的 `-q` 参数进行解析,但是当执行 `sidekiq` 命令却没有传入队列参数时,Sidekiq 只会订阅 `default` 队列中的任务: + +```ruby +def parse(args=ARGV) + # ... + validate! + # ... +end + +def validate! + options[:queues] << 'default' if options[:queues].empty? +end +``` + +同时,默认情况下的队列的优先级都为 `1`,高优先级的队列在当前的任务中可以得到更多的执行机会,实现的方法是通过增加同一个 `queues` 集合中高优先级队列的数量,我们可以在 `CLI#parse_queue` 中找到实现这一功能的代码: + +```ruby +def parse_queue(opts, q, weight=nil) + [weight.to_i, 1].max.times do + (opts[:queues] ||= []) << q + end + opts[:strict] = false if weight.to_i > 0 +end +``` + +到这里,其实我们就完成了设置过程中 Sidekiq Worker 『主题』订阅的功能了,我们将在后面 [执行任务](#执行任务) 的部分具体介绍 Sidekiq 是如何使用这些参数的。 + +## 异步任务的处理 + +从异步任务的入队一节中,我们可以清楚地看到使用 `#perform_async` 和 `#perform_in` 两种方法创建的数据结构 `payload` 最终以不同的方式进入了 Redis 中,所以在这里我们将异步任务的处理分为定时任务和『立即』任务两个部分,分别对它们不同的处理方式进行分析。 + +### 定时任务 + +Sidekiq 使用 `Scheduled::Poller` 对 Redis 中 `schedules` 有序集合中的负载进行处理,其中包括 `retry` 和 `schedule` 两个有序集合中的内容。 + +![Redis-Sorted-Set](images/sidekiq/Redis-Sorted-Set.jpg) + +在 `Poller` 被 `Scheduled::Poller` 启动时会调用 `#start` 方法开始对上述两个有序集合轮训,`retry` 中包含了所有重试的任务,而 `schedule` 就是被安排到指定时间执行的定时任务了: + +```ruby +def start + @thread ||= safe_thread("scheduler") do + initial_wait + while !@done + enqueue + wait + end + end +end +``` + +`Scheduled::Poller#start` 方法内部执行了一个 `while` 循环,在循环内部也只包含入队和等待两个操作,用于入队的方法最终调用了 `Scheduled::Poll::Enq#enqueue_jobs` 方法: + +```ruby +def enqueue_jobs(now=Time.now.to_f.to_s, sorted_sets=SETS) + Sidekiq.redis do |conn| + sorted_sets.each do |sorted_set| + while job = conn.zrangebyscore(sorted_set, '-inf'.freeze, now, :limit => [0, 1]).first do + if conn.zrem(sorted_set, job) + Sidekiq::Client.push(Sidekiq.load_json(job)) + end + end + end + end +end +``` + +传入的 `SETS` 其实就是 `retry` 和 `schedule` 构成的数组,在上述方法中,Sidekiq 通过一个 `Redis#zrangebyscore` 和 `Redis#zrem` 将集合中小于当前时间的任务全部加到立即任务中,最终调用是在前面已经提到过的 `Client#push` 方法将任务推到指定的队列中。 + +![Redis-Sidekiq-Poller](images/sidekiq/Redis-Sidekiq-Poller.jpg) + +由于 `Scheduled::Poller` 并不是不停地对 Redis 中的数据进行处理的,因为当前进程一直都在执行 `Poller#enqueue` 其实是一个非常低效的方式,所以 Sidekiq 会在每次执行 `Poller#enqueue` 之后,执行 `Poller#wait` 方法,随机等待一段时间: + +```ruby +def wait + @sleeper.pop(random_poll_interval) + # ... +end + +def random_poll_interval + poll_interval_average * rand + poll_interval_average.to_f / 2 +end +``` + +随机等待时间的范围在 `[0.5 * poll_interval_average, 1.5 * poll_interval_average]` 之间;通过随机的方式,Sidekiq 可以避免在多个线程处理任务时,短时间内 Redis 接受大量的请求发生延迟等问题,能够保证从长期来看 Redis 接受的请求数是平均的;同时因为 `Scheduled::Poller` 使用了 `#enqueue` 加 `#wait` 对 Redis 中的数据进行消费,所以没有办法保证任务会在指定的时间点执行,**执行的时间一定比安排的时间要晚**,这也是我们在使用 Sidekiq 时需要注意的。 + +> 随机等待的时间其实不止与 `poll_interval_average` 有关,在默认情况下,它是当前进程数的 15 倍,在有 30 个 Sidekiq 线程时,每个线程会每隔 225 ~ 675s 的时间请求一次。 + +### 执行任务 + +定时任务是由 `Scheduled::Poller` 进行处理的,将其中需要执行的异步任务加入到指定的队列中,而这些任务最终都会在 `Processor#run` 真正被执行: + +```ruby +def run + begin + while !@done + process_one + end + @mgr.processor_stopped(self) + rescue Exception => ex + # ... + end +end +``` + +当处理结束或者发生异常时会调用 `Manager#processor_stopped` 或者 `Manager#processor_died` 方法对 `Processor` 进行处理;在处理任务时其实也分为两个部分,也就是 `#fetch` 和 `#process` 两个方法: + +```ruby +def process_one + @job = fetch + process(@job) if @job + @job = nil +end +``` + +我们先来看一下整个方法的调用栈,任务的获取从 `Processor#process_one` 一路调用下来,直到 `BasicFetch#retrive_work` 返回了 `UnitOfWork` 对象,返回的对象会经过分发最后执行对应类的 `#perform` 传入参数真正运行该任务: + +``` +Processor#process_one +├── Processor#fetch +│   └── Processor#get_one +│      └── BasicFetch#retrive_work +│      ├── Redis#brpop +│      └── UnitOfWork#new +└── Processor#process +    ├── Processor#dispatch +    ├── Processor#execute_job +    └── Worker#perform +``` + +对于任务的获取,我们需要关注的就是 `BasicFetch#retrive_work` 方法,他会从 Redis 中相应队列的有序数组中 `Redis#brpop` 出一个任务,然后封装成 `UnitOfWork` 对象后返回。 + +```ruby +def retrieve_work + work = Sidekiq.redis { |conn| conn.brpop(*queues_cmd) } + UnitOfWork.new(*work) if work +end +``` + +`#queues_cmd` 这个实例方法其实就用到了在主题的订阅一节中的 `queues` 参数,该参数会在 `Processor` 初始化是创建一个 `BasicFetch` 策略对象,最终在 `BasicFetch#queues_cmd` 方法调用时返回一个类似下面的数组: + +```ruby +queue:high +queue:high +queue:high +queue:low +queue:low +queue:default +``` + +这样就可以实现了队列的优先级这一个功能了,返回的 `UnitOfWork` 其实是一个通过 `Struct.new` 创建的结构体,它会在 `Processor#process` 方法中作为资源被处理: + +```ruby +def process(work) + jobstr = work.job + queue = work.queue_name + + begin + # ... + + job_hash = Sidekiq.load_json(jobstr) + dispatch(job_hash, queue) do |worker| + Sidekiq.server_middleware.invoke(worker, job_hash, queue) do + execute_job(worker, cloned(job_hash['args'.freeze])) + end + end + rescue Exception => ex + # ... + end +end +``` + +该方法对任务的执行其实总共有四个步骤: + +1. 将 Redis 中存储的字符串加载为 JSON; +2. 执行 `Processor#dispatch` 方法并在内部提供方法重试等功能,同时也实例化一个 `Sidekiq::Worker` 对象; +3. 依次执行服务端的中间件,可能会对参数进行更新; +4. 调用 `Processor#execute_job` 方法执行任务; + +而最后调用的时用于执行任务的方法 `Processor#execute_job`,它的实现也是到目前为止最为简单的方法之一了: + +```ruby +def execute_job(worker, cloned_args) + worker.perform(*cloned_args) +end +``` + +该方法在**线程**中执行了客户端创建的 `Worker` 类的实例方法 `#perform` 并传入了经过两侧中间件处理后的参数。 + +### 小结 + +到目前为止,Sidekiq Worker 对任务的消费过程就是圆满的了,从客户端创建一个拥有 `#perform` 方法的 `Worker` 到消费者去执行该方法形成了一个闭环,完成了对任务的调度。 + +![Client-Redis-Sidekiq-Worker](images/sidekiq/Client-Redis-Sidekiq-Worker.jpg) + +Sidekiq 是一个非常轻量级的任务调度系统,它使用 Redis 作为整个系统的消息队列,在两侧分别建立了生产者和消费者的模块,不过除了这几个比较重要的模块,Sidekiq 中还有一些功能是我们无法忽略的,比如中间件、兼容 ActiveJob 甚至是测试的实现,都是我们需要去了解的;接下来,我们将介绍和分析主干之外的『分叉』功能。 + +## 中间件 + +中间件模块是 Sidekiq 为我们在整个任务的处理流程提供的两个钩子,一个是在客户端的钩子,另一个在 Sidekiq Worker 中。 + +![Middlewares-Client-Redis-Sidekiq-Worker](images/sidekiq/Middlewares-Client-Redis-Sidekiq-Worker.jpg) + +中间件的使用其实非常简单,我们默认所有的中间件都会拥有一个实例方法 `#call` 并接受 `worker`、`job` 和 `queue` 三个参数,在使用时也只需要直接调用 `Chain#add` 方法将其加入数组就可以了: + +```ruby +class AcmeCo::MyMiddleware + def call(worker, job, queue) + # ... + end +end + +# config/initializers/sidekiq.rb +Sidekiq.configure_server do |config| + config.server_middleware do |chain| + chain.add AcmeCo::MyMiddleware + end +end +``` + +Sidekiq 将中间件分为了客户端和服务端两个部分,这两个部分的中间件其实并不是严格意义上的在执行之前,由于执行时间点的不同,导致它们有不同的功能: + ++ 服务端中间件是『包围』了任务执行过程的,我们可以在中间件中使用 `begin`、`rescue` 语句,这样当任务出现问题时,我们就可以拿到异常了; ++ 客户端中间件在任务即将被推入 Redis 之前运行,它能够阻止任务进入 Redis 并且允许我们在任务入队前对其进行修改和停止; + +当我们对 Sidekiq 中间的使用都有一定的了解时,就可以开始分析中间件的实现了。 + +### 实现 + +无论是异步任务真正进入队列之前,还是在客户端处理,跟任务有关的信息都会先通过一个预处理流程,客户端和服务端两个中间件的链式调用都使用 `Middleware::Chain` 中的类进行处理的: + +```ruby +class Chain + include Enumerable + attr_reader :entries + + def initialize + @entries = [] + yield self if block_given? + end + + def remove(klass); end + def add(klass, *args); end + def prepend(klass, *args); end + def insert_before(oldklass, newklass, *args); end + def insert_after(oldklass, newklass, *args); end +end +``` + +每一个 `Middleware::Chain` 中都包含一系列的 `Entry`,其中存储了中间件的相关信息,无论是客户端还是服务端都会在执行之前对每一个异步任务的参数执行 `invoke` 方法调用 `Middleware::Chain` 对象中的所有中间件: + +```ruby +def invoke(*args) + chain = retrieve.dup + traverse_chain = lambda do + if chain.empty? + yield + else + chain.shift.call(*args, &traverse_chain) + end + end + traverse_chain.call +end + +``` + +`Chain#invoke` 会对其持有的每一个中间件都执行 `#call` 方法,中间件都可以对异步任务的参数进行改变或者进行一些记录日志等操作,最后执行传入的 block 并返回结果。 + +![Sidekiq-Middlewares](images/sidekiq/Sidekiq-Middlewares.jpg) + +当异步队列入队时,就会执行 `Client#process_single` 方法调用 Sidekiq 载入中的全部中间件最后返回新的 `item` 对象: + +```ruby +def process_single(worker_class, item) + queue = item['queue'.freeze] + middleware.invoke(worker_class, item, queue, @redis_pool) do + item + end +end +``` + +每一个 Sidekiq Worker 在处理中间件时也基本遵循相同的逻辑,如 `#process` 方法先先执行各种中间件,最后再运行 block 中的内容。 + +```ruby +def process(work) + jobstr = work.job + queue = work.queue_name + + begin + # ... + + job_hash = Sidekiq.load_json(jobstr) + Sidekiq.server_middleware.invoke(worker, job_hash, queue) do + execute_job(worker, cloned(job_hash['args'.freeze])) + end + rescue Exception => ex + # ... + end +end +``` + +在 `#execute_job` 方法执行期间,由于异步任务可能抛出异常,在这时,我们注册的中间件就可以根据情况对异常进行捕获并选择是否对异常进行处理或者抛给上层了。 + +## 任务的重试 + +Sidekiq 中任务的重试是由 `JobRetry` 负责的,`Prcessor` 中的 `#dispatch` 方法中调用了 `JobRetry#global` 方法捕获在异步任务执行过程中发生的错误: + +```ruby +def dispatch(job_hash, queue) + pristine = cloned(job_hash) + + # ... + @retrier.global(pristine, queue) do + klass = constantize(job_hash['class'.freeze]) + worker = klass.new + worker.jid = job_hash['jid'.freeze] + @retrier.local(worker, pristine, queue) do + yield worker + end + end +end +``` + +任务的执行过程分别调用了两个 `JobRetry` 的方法 `#global` 和 `#local`,这两个方法在实现上差不多,都将执行异步任务的 block 包在了一个 `begin`、`rescue` 中,选择在合适的时间重试: + +```ruby +def local(worker, msg, queue) + yield +# ... +rescue Exception => e + raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e) + + if msg['retry'] == nil + msg['retry'] = worker.class.get_sidekiq_options['retry'] + end + + raise e unless msg['retry'] + attempt_retry(worker, msg, queue, e) + raise Skip +end +``` + +如果我们在定义 `Worker` 时就禁用了重试,那么在这里就会直接抛出上层的异常,否则就会进入 `#attempt_retry` 方法安排任务进行重试: + +```ruby +def attempt_retry(worker, msg, queue, exception) + max_retry_attempts = retry_attempts_from(msg['retry'], @max_retries) + + msg['queue'] = if msg['retry_queue'] + msg['retry_queue'] + else + queue + end + + count = if msg['retry_count'] + msg['retried_at'] = Time.now.to_f + msg['retry_count'] += 1 + else + msg['failed_at'] = Time.now.to_f + msg['retry_count'] = 0 + end + + if count < max_retry_attempts + delay = delay_for(worker, count, exception) + retry_at = Time.now.to_f + delay + payload = Sidekiq.dump_json(msg) + Sidekiq.redis do |conn| + conn.zadd('retry', retry_at.to_s, payload) + end + else + retries_exhausted(worker, msg, exception) + end +end +``` + +在上面其实我们提到过,`Poller` 每次会从两个有序集合 `retry` 和 `schedule` 中查找到时的任务加入到对应的队列中,在 `#attempt_retry` 方法中,就可以找到看到 `retry` 队列中的元素是如何加入的了。 + +当任务的重试次数超过了限定的重试次数之后,就会执行 `#retries_exhausted` 以及 `# send_to_morgue` 这一方法,将任务的负载加入 `DeadSet` 对象中: + +```ruby +def send_to_morgue(msg) + payload = Sidekiq.dump_json(msg) + DeadSet.new.kill(payload) +end +``` + +这样整个任务的重试过程就结束了,Sidekiq 使用 `begin`、`rescue` 捕获整个流程中出现的异常,并根据传入的 `retry_count` 参数进行重试,调度过程还是非常简洁也非常容易理解的。 + +## 总结 + +作为一个 Ruby 社区中广泛被使用的异步任务处理的依赖,它的实现是很简单的并且其源代码非常易于阅读,整体的架构也非常清晰。 + +![Middlewares-Client-Redis-Sidekiq-Worker](images/sidekiq/Middlewares-Client-Redis-Sidekiq-Worker.jpg) + +使用键值的内存数据库 Redis 作为客户端和 Worker 之间的桥梁,Redis 的使用简化了 Sidekiq 的很多逻辑,同时对中间件的支持也使其有着良好的扩展性,不过正其实现简单,所以例如任务取消以及定时任务这种比较常见的功能其本身都没有实现,有的是 Sidekiq 本身设计问题导致的,有的需要另外的插件,不过在绝大多数情况下,Sidekiq 都能完全满足我们的需要,解决绝大多数的问题。 + +> Follow: [Draveness · GitHub](https://github.com/Draveness) + +## References + ++ [Sidekiq](https://github.com/mperham/sidekiq) ++ [Sidekiq 任务调度流程分析](https://ruby-china.org/topics/31470) + + diff --git "a/SDWebImage/iOS \346\272\220\344\273\243\347\240\201\345\210\206\346\236\220 --- SDWebImage.md" "b/contents/SDWebImage/iOS \346\272\220\344\273\243\347\240\201\345\210\206\346\236\220 --- SDWebImage.md" similarity index 98% rename from "SDWebImage/iOS \346\272\220\344\273\243\347\240\201\345\210\206\346\236\220 --- SDWebImage.md" rename to "contents/SDWebImage/iOS \346\272\220\344\273\243\347\240\201\345\210\206\346\236\220 --- SDWebImage.md" index 5dd27a7..394654c 100644 --- "a/SDWebImage/iOS \346\272\220\344\273\243\347\240\201\345\210\206\346\236\220 --- SDWebImage.md" +++ "b/contents/SDWebImage/iOS \346\272\220\344\273\243\347\240\201\345\210\206\346\236\220 --- SDWebImage.md" @@ -48,7 +48,7 @@ placeholderImage:(UIImage *)placeholder; ``` -这一方法为入口研究一下 `SDWebImage` 是怎样工作的. 我们打开上面这段方法的实现代码 [UIImageView+WebCache.m](https://github.com/rs/SDWebImage/blob/master/SDWebImage/UIImageView%2BWebCache.m) +这一方法为入口研究一下 `SDWebImage` 是怎样工作的. 我们打开上面这段方法的实现代码 [UIImageView+WebCache.m](https://github.com/rs/SDWebImage/blob/master/contents/SDWebImage/UIImageView%2BWebCache.m) 当然你也可以 `git clone git@github.com:rs/SDWebImage.git` 到本地来查看. @@ -192,7 +192,7 @@ dispatch_main_sync_safe(^{ ###SDWebImageManager -在 [SDWebImageManager.h](https://github.com/rs/SDWebImage/blob/master/SDWebImage/SDWebImageManager.h) 中你可以看到关于 `SDWebImageManager` 的描述: +在 [SDWebImageManager.h](https://github.com/rs/SDWebImage/blob/master/contents/SDWebImage/SDWebImageManager.h) 中你可以看到关于 `SDWebImageManager` 的描述: > The SDWebImageManager is the class behind the UIImageView+WebCache category and likes. It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache). You can use this class directly to benefit from web image downloading with caching in another context than a UIView. @@ -313,7 +313,7 @@ operation.cancelBlock = ^{ ###SDWebImageCache -[SDWebImageCache.h](https://github.com/rs/SDWebImage/blob/master/SDWebImage/SDImageCache.h) 这个类在源代码中有这样的注释: +[SDWebImageCache.h](https://github.com/rs/SDWebImage/blob/master/contents/SDWebImage/SDImageCache.h) 这个类在源代码中有这样的注释: > SDImageCache maintains a memory cache and an optional disk cache. @@ -365,7 +365,7 @@ if (diskImage) { ###SDWebImageDownloader -按照之前的惯例, 我们先来看一下 [SDWebImageDownloader.h](https://github.com/rs/SDWebImage/blob/master/SDWebImage/SDWebImageDownloader.h) 中对这个类的描述. +按照之前的惯例, 我们先来看一下 [SDWebImageDownloader.h](https://github.com/rs/SDWebImage/blob/master/contents/SDWebImage/SDWebImageDownloader.h) 中对这个类的描述. > Asynchronous downloader dedicated and optimized for image loading. diff --git a/contents/architecture/images/controller/Binder-View-ViewModel.jpg b/contents/architecture/images/controller/Binder-View-ViewModel.jpg new file mode 100644 index 0000000..90fb060 Binary files /dev/null and b/contents/architecture/images/controller/Binder-View-ViewModel.jpg differ diff --git a/contents/architecture/images/controller/Controller-Only.jpg b/contents/architecture/images/controller/Controller-Only.jpg new file mode 100644 index 0000000..ef53f45 Binary files /dev/null and b/contents/architecture/images/controller/Controller-Only.jpg differ diff --git a/contents/architecture/images/controller/Controller-RootView.jpg b/contents/architecture/images/controller/Controller-RootView.jpg new file mode 100644 index 0000000..b7dc015 Binary files /dev/null and b/contents/architecture/images/controller/Controller-RootView.jpg differ diff --git a/contents/architecture/images/controller/Coupling-View-And-Model.jpg b/contents/architecture/images/controller/Coupling-View-And-Model.jpg new file mode 100644 index 0000000..9141e47 Binary files /dev/null and b/contents/architecture/images/controller/Coupling-View-And-Model.jpg differ diff --git a/contents/architecture/images/controller/Eager-Lazy-Initialization.jpg b/contents/architecture/images/controller/Eager-Lazy-Initialization.jpg new file mode 100644 index 0000000..150b9a0 Binary files /dev/null and b/contents/architecture/images/controller/Eager-Lazy-Initialization.jpg differ diff --git a/contents/architecture/images/controller/MVC-MVVM-MVP.jpg b/contents/architecture/images/controller/MVC-MVVM-MVP.jpg new file mode 100644 index 0000000..914cd9f Binary files /dev/null and b/contents/architecture/images/controller/MVC-MVVM-MVP.jpg differ diff --git a/contents/architecture/images/controller/MVC-in-iOS.jpg b/contents/architecture/images/controller/MVC-in-iOS.jpg new file mode 100644 index 0000000..4f6f7fe Binary files /dev/null and b/contents/architecture/images/controller/MVC-in-iOS.jpg differ diff --git a/contents/architecture/images/controller/Model-View-Controller.jpg b/contents/architecture/images/controller/Model-View-Controller.jpg new file mode 100644 index 0000000..4b130b9 Binary files /dev/null and b/contents/architecture/images/controller/Model-View-Controller.jpg differ diff --git a/contents/architecture/images/controller/Model-View-VIewModel.jpg b/contents/architecture/images/controller/Model-View-VIewModel.jpg new file mode 100644 index 0000000..04b657b Binary files /dev/null and b/contents/architecture/images/controller/Model-View-VIewModel.jpg differ diff --git a/contents/architecture/images/controller/UINavigationController-UITabBarController.jpg b/contents/architecture/images/controller/UINavigationController-UITabBarController.jpg new file mode 100644 index 0000000..6b7e651 Binary files /dev/null and b/contents/architecture/images/controller/UINavigationController-UITabBarController.jpg differ diff --git a/contents/architecture/images/controller/UITableView-DataSource.jpg b/contents/architecture/images/controller/UITableView-DataSource.jpg new file mode 100644 index 0000000..9de850e Binary files /dev/null and b/contents/architecture/images/controller/UITableView-DataSource.jpg differ diff --git a/contents/architecture/images/model/404.gif b/contents/architecture/images/model/404.gif new file mode 100644 index 0000000..fb5e4b8 Binary files /dev/null and b/contents/architecture/images/model/404.gif differ diff --git a/contents/architecture/images/model/Abstract-Manager.png b/contents/architecture/images/model/Abstract-Manager.png new file mode 100644 index 0000000..54d0a09 Binary files /dev/null and b/contents/architecture/images/model/Abstract-Manager.png differ diff --git a/contents/architecture/images/model/Abstract-Request.jpg b/contents/architecture/images/model/Abstract-Request.jpg new file mode 100644 index 0000000..c961a52 Binary files /dev/null and b/contents/architecture/images/model/Abstract-Request.jpg differ diff --git a/contents/architecture/images/model/Abstract-Request.png b/contents/architecture/images/model/Abstract-Request.png new file mode 100644 index 0000000..a0f28bd Binary files /dev/null and b/contents/architecture/images/model/Abstract-Request.png differ diff --git a/contents/architecture/images/model/Dynamic-Static.png b/contents/architecture/images/model/Dynamic-Static.png new file mode 100644 index 0000000..20ab21e Binary files /dev/null and b/contents/architecture/images/model/Dynamic-Static.png differ diff --git a/contents/architecture/images/model/JSON-Model.jpg b/contents/architecture/images/model/JSON-Model.jpg new file mode 100644 index 0000000..1bec2ef Binary files /dev/null and b/contents/architecture/images/model/JSON-Model.jpg differ diff --git a/contents/architecture/images/model/JSON-to-Model.jpg b/contents/architecture/images/model/JSON-to-Model.jpg new file mode 100644 index 0000000..ef44ed1 Binary files /dev/null and b/contents/architecture/images/model/JSON-to-Model.jpg differ diff --git a/contents/architecture/images/model/MVCS-Architecture.png b/contents/architecture/images/model/MVCS-Architecture.png new file mode 100644 index 0000000..1a86ce4 Binary files /dev/null and b/contents/architecture/images/model/MVCS-Architecture.png differ diff --git a/contents/architecture/images/model/Manager-And-Request.jpg b/contents/architecture/images/model/Manager-And-Request.jpg new file mode 100644 index 0000000..b00f6ed Binary files /dev/null and b/contents/architecture/images/model/Manager-And-Request.jpg differ diff --git a/contents/architecture/images/model/Model-And-Dictioanry.jpg b/contents/architecture/images/model/Model-And-Dictioanry.jpg new file mode 100644 index 0000000..a7de03f Binary files /dev/null and b/contents/architecture/images/model/Model-And-Dictioanry.jpg differ diff --git a/contents/architecture/images/model/Model-in-Client.jpg b/contents/architecture/images/model/Model-in-Client.jpg new file mode 100644 index 0000000..dc40f70 Binary files /dev/null and b/contents/architecture/images/model/Model-in-Client.jpg differ diff --git a/contents/architecture/images/model/Relation-Between-Database-And-Model.jpg b/contents/architecture/images/model/Relation-Between-Database-And-Model.jpg new file mode 100644 index 0000000..1033a67 Binary files /dev/null and b/contents/architecture/images/model/Relation-Between-Database-And-Model.jpg differ diff --git a/contents/architecture/images/model/Server-MVC.jpg b/contents/architecture/images/model/Server-MVC.jpg new file mode 100644 index 0000000..86dffee Binary files /dev/null and b/contents/architecture/images/model/Server-MVC.jpg differ diff --git a/contents/architecture/images/model/Service-And-API.jpg b/contents/architecture/images/model/Service-And-API.jpg new file mode 100644 index 0000000..4605caa Binary files /dev/null and b/contents/architecture/images/model/Service-And-API.jpg differ diff --git a/contents/architecture/images/model/client-black-box.jpg b/contents/architecture/images/model/client-black-box.jpg new file mode 100644 index 0000000..231465e Binary files /dev/null and b/contents/architecture/images/model/client-black-box.jpg differ diff --git a/contents/architecture/images/model/web-black-box.jpg b/contents/architecture/images/model/web-black-box.jpg new file mode 100644 index 0000000..85bcc24 Binary files /dev/null and b/contents/architecture/images/model/web-black-box.jpg differ diff --git a/contents/architecture/images/mvx/Binder-View-ViewModel.jpg b/contents/architecture/images/mvx/Binder-View-ViewModel.jpg new file mode 100644 index 0000000..90fb060 Binary files /dev/null and b/contents/architecture/images/mvx/Binder-View-ViewModel.jpg differ diff --git a/contents/architecture/images/mvx/Essential-Dependencies-in-MVC.jpg b/contents/architecture/images/mvx/Essential-Dependencies-in-MVC.jpg new file mode 100644 index 0000000..4de88f6 Binary files /dev/null and b/contents/architecture/images/mvx/Essential-Dependencies-in-MVC.jpg differ diff --git a/contents/architecture/images/mvx/Essential-Dependencies-in-Passive-View.jpg b/contents/architecture/images/mvx/Essential-Dependencies-in-Passive-View.jpg new file mode 100644 index 0000000..8415514 Binary files /dev/null and b/contents/architecture/images/mvx/Essential-Dependencies-in-Passive-View.jpg differ diff --git "a/contents/architecture/images/mvx/MVC-\005in-Rails-with-different-view.jpg" "b/contents/architecture/images/mvx/MVC-\005in-Rails-with-different-view.jpg" new file mode 100644 index 0000000..d58714a Binary files /dev/null and "b/contents/architecture/images/mvx/MVC-\005in-Rails-with-different-view.jpg" differ diff --git a/contents/architecture/images/mvx/MVC-1979.jpg b/contents/architecture/images/mvx/MVC-1979.jpg new file mode 100644 index 0000000..fba8359 Binary files /dev/null and b/contents/architecture/images/mvx/MVC-1979.jpg differ diff --git a/contents/architecture/images/mvx/MVC-App-Arch.jpg b/contents/architecture/images/mvx/MVC-App-Arch.jpg new file mode 100644 index 0000000..7387d04 Binary files /dev/null and b/contents/architecture/images/mvx/MVC-App-Arch.jpg differ diff --git a/contents/architecture/images/mvx/MVC-MVC.jpg b/contents/architecture/images/mvx/MVC-MVC.jpg new file mode 100644 index 0000000..566a8ea Binary files /dev/null and b/contents/architecture/images/mvx/MVC-MVC.jpg differ diff --git a/contents/architecture/images/mvx/MVC-MVVM-MVP.jpg b/contents/architecture/images/mvx/MVC-MVVM-MVP.jpg new file mode 100644 index 0000000..914cd9f Binary files /dev/null and b/contents/architecture/images/mvx/MVC-MVVM-MVP.jpg differ diff --git a/contents/architecture/images/mvx/MVC-Web-App.jpg b/contents/architecture/images/mvx/MVC-Web-App.jpg new file mode 100644 index 0000000..879b7d6 Binary files /dev/null and b/contents/architecture/images/mvx/MVC-Web-App.jpg differ diff --git a/contents/architecture/images/mvx/MVC-in-ASP.NET.png b/contents/architecture/images/mvx/MVC-in-ASP.NET.png new file mode 100644 index 0000000..9e1c4e2 Binary files /dev/null and b/contents/architecture/images/mvx/MVC-in-ASP.NET.png differ diff --git a/contents/architecture/images/mvx/MVC-in-Wikipedia.jpg b/contents/architecture/images/mvx/MVC-in-Wikipedia.jpg new file mode 100644 index 0000000..5d54d8e Binary files /dev/null and b/contents/architecture/images/mvx/MVC-in-Wikipedia.jpg differ diff --git a/contents/architecture/images/mvx/MVC-with-ASP.NET.jpg b/contents/architecture/images/mvx/MVC-with-ASP.NET.jpg new file mode 100644 index 0000000..8e357fa Binary files /dev/null and b/contents/architecture/images/mvx/MVC-with-ASP.NET.jpg differ diff --git a/contents/architecture/images/mvx/MVC-with-Rails.jpg b/contents/architecture/images/mvx/MVC-with-Rails.jpg new file mode 100644 index 0000000..c489b2c Binary files /dev/null and b/contents/architecture/images/mvx/MVC-with-Rails.jpg differ diff --git a/contents/architecture/images/mvx/MVC-with-Spring.jpg b/contents/architecture/images/mvx/MVC-with-Spring.jpg new file mode 100644 index 0000000..f9ce7be Binary files /dev/null and b/contents/architecture/images/mvx/MVC-with-Spring.jpg differ diff --git a/contents/architecture/images/mvx/MVC-with-iOS.jpg b/contents/architecture/images/mvx/MVC-with-iOS.jpg new file mode 100644 index 0000000..be9dc83 Binary files /dev/null and b/contents/architecture/images/mvx/MVC-with-iOS.jpg differ diff --git a/contents/architecture/images/mvx/Main-Controller.jpg b/contents/architecture/images/mvx/Main-Controller.jpg new file mode 100644 index 0000000..d5162c3 Binary files /dev/null and b/contents/architecture/images/mvx/Main-Controller.jpg differ diff --git a/contents/architecture/images/mvx/Main-View-in-MVP.jpg b/contents/architecture/images/mvx/Main-View-in-MVP.jpg new file mode 100644 index 0000000..25f7e22 Binary files /dev/null and b/contents/architecture/images/mvx/Main-View-in-MVP.jpg differ diff --git a/contents/architecture/images/mvx/Model-View-ViewModel.jpg b/contents/architecture/images/mvx/Model-View-ViewModel.jpg new file mode 100644 index 0000000..04b657b Binary files /dev/null and b/contents/architecture/images/mvx/Model-View-ViewModel.jpg differ diff --git a/contents/architecture/images/mvx/Observer-Synchronization.jpg b/contents/architecture/images/mvx/Observer-Synchronization.jpg new file mode 100644 index 0000000..9064ca9 Binary files /dev/null and b/contents/architecture/images/mvx/Observer-Synchronization.jpg differ diff --git a/contents/architecture/images/mvx/PM-View-Domain-Object.jpg b/contents/architecture/images/mvx/PM-View-Domain-Object.jpg new file mode 100644 index 0000000..2d84685 Binary files /dev/null and b/contents/architecture/images/mvx/PM-View-Domain-Object.jpg differ diff --git a/contents/architecture/images/mvx/PM-and-MVVM.jpg b/contents/architecture/images/mvx/PM-and-MVVM.jpg new file mode 100644 index 0000000..3d6934a Binary files /dev/null and b/contents/architecture/images/mvx/PM-and-MVVM.jpg differ diff --git a/contents/architecture/images/mvx/PassIve-View.jpg b/contents/architecture/images/mvx/PassIve-View.jpg new file mode 100644 index 0000000..0aeea60 Binary files /dev/null and b/contents/architecture/images/mvx/PassIve-View.jpg differ diff --git a/contents/architecture/images/mvx/Passive-Model.jpg b/contents/architecture/images/mvx/Passive-Model.jpg new file mode 100644 index 0000000..7d2a7b8 Binary files /dev/null and b/contents/architecture/images/mvx/Passive-Model.jpg differ diff --git a/contents/architecture/images/mvx/Passive-View-with-Tags.jpg b/contents/architecture/images/mvx/Passive-View-with-Tags.jpg new file mode 100644 index 0000000..3882e95 Binary files /dev/null and b/contents/architecture/images/mvx/Passive-View-with-Tags.jpg differ diff --git a/contents/architecture/images/mvx/Presentation-Domain.jpg b/contents/architecture/images/mvx/Presentation-Domain.jpg new file mode 100644 index 0000000..78b58d8 Binary files /dev/null and b/contents/architecture/images/mvx/Presentation-Domain.jpg differ diff --git a/contents/architecture/images/mvx/Presentation-Model.jpg b/contents/architecture/images/mvx/Presentation-Model.jpg new file mode 100644 index 0000000..09543dd Binary files /dev/null and b/contents/architecture/images/mvx/Presentation-Model.jpg differ diff --git a/contents/architecture/images/mvx/Standard-MVC.jpg b/contents/architecture/images/mvx/Standard-MVC.jpg new file mode 100644 index 0000000..55fa495 Binary files /dev/null and b/contents/architecture/images/mvx/Standard-MVC.jpg differ diff --git a/contents/architecture/images/mvx/Standard-MVP.jpg b/contents/architecture/images/mvx/Standard-MVP.jpg new file mode 100644 index 0000000..ac880e8 Binary files /dev/null and b/contents/architecture/images/mvx/Standard-MVP.jpg differ diff --git a/contents/architecture/images/mvx/Supervising-Controller-With-Tag.jpg b/contents/architecture/images/mvx/Supervising-Controller-With-Tag.jpg new file mode 100644 index 0000000..b375df8 Binary files /dev/null and b/contents/architecture/images/mvx/Supervising-Controller-With-Tag.jpg differ diff --git a/contents/architecture/images/mvx/Supervising-Controller.jpg b/contents/architecture/images/mvx/Supervising-Controller.jpg new file mode 100644 index 0000000..c5ebeaf Binary files /dev/null and b/contents/architecture/images/mvx/Supervising-Controller.jpg differ diff --git a/contents/architecture/images/view/Android-View-Tree.jpg b/contents/architecture/images/view/Android-View-Tree.jpg new file mode 100644 index 0000000..496aa81 Binary files /dev/null and b/contents/architecture/images/view/Android-View-Tree.jpg differ diff --git a/contents/architecture/images/view/AutoLayout.jpg b/contents/architecture/images/view/AutoLayout.jpg new file mode 100644 index 0000000..3638fa0 Binary files /dev/null and b/contents/architecture/images/view/AutoLayout.jpg differ diff --git a/contents/architecture/images/view/Frame-And-Components.jpg b/contents/architecture/images/view/Frame-And-Components.jpg new file mode 100644 index 0000000..d058a17 Binary files /dev/null and b/contents/architecture/images/view/Frame-And-Components.jpg differ diff --git a/contents/architecture/images/view/Node-Delegate-Filter.jpg b/contents/architecture/images/view/Node-Delegate-Filter.jpg new file mode 100644 index 0000000..fb4e1b2 Binary files /dev/null and b/contents/architecture/images/view/Node-Delegate-Filter.jpg differ diff --git a/contents/architecture/images/view/Node-Delegate-UIView.jpg b/contents/architecture/images/view/Node-Delegate-UIView.jpg new file mode 100644 index 0000000..c87cc7e Binary files /dev/null and b/contents/architecture/images/view/Node-Delegate-UIView.jpg differ diff --git a/contents/architecture/images/view/UIStackView.jpg b/contents/architecture/images/view/UIStackView.jpg new file mode 100644 index 0000000..b595c65 Binary files /dev/null and b/contents/architecture/images/view/UIStackView.jpg differ diff --git a/contents/architecture/images/view/UIView-And-Subclasses.jpg b/contents/architecture/images/view/UIView-And-Subclasses.jpg new file mode 100644 index 0000000..2082c8f Binary files /dev/null and b/contents/architecture/images/view/UIView-And-Subclasses.jpg differ diff --git a/contents/architecture/images/view/animation.gif b/contents/architecture/images/view/animation.gif new file mode 100644 index 0000000..e5c9f08 Binary files /dev/null and b/contents/architecture/images/view/animation.gif differ diff --git a/contents/architecture/images/view/html-css.jpg b/contents/architecture/images/view/html-css.jpg new file mode 100644 index 0000000..664c4fd Binary files /dev/null and b/contents/architecture/images/view/html-css.jpg differ diff --git a/contents/architecture/images/view/lottie.jpg b/contents/architecture/images/view/lottie.jpg new file mode 100644 index 0000000..9d5c15e Binary files /dev/null and b/contents/architecture/images/view/lottie.jpg differ diff --git a/contents/architecture/images/view/texture.png b/contents/architecture/images/view/texture.png new file mode 100644 index 0000000..d1e84d6 Binary files /dev/null and b/contents/architecture/images/view/texture.png differ diff --git a/contents/architecture/images/view/vue.jpg b/contents/architecture/images/view/vue.jpg new file mode 100644 index 0000000..3f1dc7c Binary files /dev/null and b/contents/architecture/images/view/vue.jpg differ diff --git a/contents/architecture/images/view/vue.png b/contents/architecture/images/view/vue.png new file mode 100644 index 0000000..74389d8 Binary files /dev/null and b/contents/architecture/images/view/vue.png differ diff --git a/contents/architecture/mvx-controller.md b/contents/architecture/mvx-controller.md new file mode 100644 index 0000000..15c509c --- /dev/null +++ b/contents/architecture/mvx-controller.md @@ -0,0 +1,591 @@ +# 谈谈 MVX 中的 Controller + ++ [谈谈 MVX 中的 Model](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/architecture/mvx-model.md) ++ [谈谈 MVX 中的 View](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/architecture/mvx-view.md) ++ [谈谈 MVX 中的 Controller](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/architecture/mvx-controller.md) ++ [浅谈 MVC、MVP 和 MVVM 架构模式](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/architecture/mvx.md) + +> Follow GitHub: [Draveness](https://github.com/Draveness) + +在前两篇文章中,我们已经对 iOS 中的 Model 层以及 View 层进行了分析,划分出了它们的具体职责,其中 Model 层除了负责数据的持久存储、缓存工作,还要负责所有 HTTP 请求的发出等工作;而对于 View 层的职责,我们并没有做出太多的改变,有的只是细分其内部的视图种类,以及分离 `UIView` 不应该具有的属性和功能。 + +> 如果想要具体了解笔者对 Model 层以及 View 层的理解和设计,这是前面两篇文章的链接:[谈谈 MVX 中的 Model 层](http://draveness.me/mvx-model.html)、[谈谈 MVX 中的 View 层](http://draveness.me/mvx-view.html) + +这是 MVX 系列的第三篇文章,而这篇文章准备介绍整个 MVX 中无法避免的话题,也就是 X 这一部分。 + +## X 是什么 + +在进入正题之前,我们首先要知道这里的 X 到底是什么?无论是在 iOS 开发领域还是其它的领域,造出了一堆又一堆的名词,除了我们最常见的 MVC 和 MVVM 以及 Android 中的 MVP 还有一些其他的奇奇怪怪的名词。 + +![MVC-MVVM-MVP](images/controller/MVC-MVVM-MVP.jpg) + +模型层和视图层是整个客户端应用不可分割的一部分,它们的职责非常清楚,一个用于处理本地数据的获取以及存储,另一个用于展示内容、接受用户的操作与事件;在这种情况下,整个应用中的其它功能和逻辑就会被自然而然的扔到 X 层中。 + +这个 X 在 MVC 中就是 Controller 层、在 MVVM 中就是 ViewModel 层,而在 MVP 中就是 Presenter 层,这篇文章介绍的就是 MVC 中的控制器层 Controller。 + +## 臃肿的 Controller + +从 Cocoa Touch 框架使用十年以来,iOS 开发者就一直遵循框架中的设计,使用 Model-View-Controller 的架构模式开发 iOS 应用程序,下面也是对 iOS 中 MVC 的各层交互的最简单的说明。 + +![Model-View-Controlle](images/controller/Model-View-Controller.jpg) + +iOS 中的 Model 层大多为 `NSObject` 的子类,也就是一个简单的对象;所有的 View 层对象都是 `UIView` 的子类;而 Controller 层的对象都是 `UIViewController` 的实例。 + +我们在这一节中主要是介绍 `UIViewController` 作为 Controller 层中的最重要的对象,它具有哪些职责,它与 Model 以及 View 层是如何进行交互的。 + +总体来说,Controller 层要负责以下的问题(包括但不仅限于): + +1. 管理根视图的生命周期和应用生命周期 +1. 负责将视图层的 `UIView` 对象添加到持有的根视图上; +2. 负责处理用户行为,比如 `UIButton` 的点击以及手势的触发; +3. 储存当前界面的状态; +4. 处理界面之间的跳转; +3. 作为 `UITableView` 以及其它容器视图的代理以及数据源; +4. 负责 HTTP 请求的发起; + +除了上述职责外,`UIViewController` 对象还可能需要处理**业务逻辑**以及各种复杂的动画,这也就是为什么在 iOS 应用中的 Controller 层都非常庞大、臃肿的原因了,而 MVVM、MVP 等架构模式的目的之一就是减少单一 Controller 中的代码。 + +### 管理生命周期 + +Controller 层作为整个 MVC 架构模式的中枢,承担着非常重要的职责,不仅要与 Model 以及 View 层进行交互,还有通过 AppDelegate 与诸多的应用生命周期打交道。 + +```objectivec +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions; +- (void)applicationWillResignActive:(UIApplication *)application; +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler; +``` + +虽然与应用生命周期沟通的工作并不在单独的 Controller 中,但是 `self.window.rootController` 作为整个应用程序界面的入口,还是需要在 AppDelegate 中进行设置。 + +除此之外,由于每一个 `UIViewController` 都持有一个视图对象,所以每一个 `UIViewController` 都需要负责这个根视图的加载、布局以及生命周期的管理,包括: + +```objectivec +- (void)loadView; + +- (void)viewWillLayoutSubviews; +- (void)viewDidLayoutSubviews; + +- (void)viewDidLoad; +- (void)viewWillAppear:(BOOL)animated; +- (void)viewDidAppear:(BOOL)animated; +``` + +除了负责应用生命周期和视图生命周期,控制器还要负责展示内容和布局。 + +### 负责展示内容和布局 + +由于每一个 `UIViewController` 都持有一个 `UIView` 的对象,所以视图层的对象想要出现在屏幕上,必须成为这个根视图的子视图,也就是说视图层完全没有办法脱离 `UIViewController` 而单独存在,其一方面是因为 `UIViewController` 隐式的承担了应用中路由的工作,处理界面之间的跳转,另一方面就是 `UIViewController` 的设计导致了所有的视图必须加在其根视图上才能工作。 + +![Controller-RootVie](images/controller/Controller-RootView.jpg) + +我们来看一段 `UIViewController` 中关于视图层的简单代码: + +```objectivec +- (void)viewDidLoad { + [super viewDidLoad]; + [self setupUI]; +} + +- (void)setupUI { + _backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"backgroundView"]]; + + _registerButton = [[UIButton alloc] init]; + [_registerButton setTitle:@"注册" forState:UIControlStateNormal]; + [_registerButton setTitleColor:UIColorFromRGB(0x00C3F3) forState:UIControlStateNormal]; + [_registerButton addTarget:self action:@selector(registerButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; + + [self.view addSubview:_backgroundView]; + [self.view addSubview:_registerButton]; + + [_backgroundView mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.mas_equalTo(self.view); + }]; + [_registerButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.size.mas_equalTo(CGSizeMake(140, 45)); + make.bottom.mas_equalTo(self.view).offset(-25); + make.left.mas_equalTo(self.view).offset(32); + }]; +} +``` + +在这个欢迎界面以及大多数界面中,由于视图层的代码非常简单,我们很多情况下并不会去写一个单独的 `UIView` 类,而是将全部的视图层代码丢到了 `UIViewController` 中,这种情况下甚至也没有 Model 层,Controller 承担了全部的工作。 + +![Controller-Only](images/controller/Controller-Only.jpg) + +上述的代码对视图进行了初始化,将需要展示的视图加到了自己持有的根视图中,然后对这些视图进行简单的布局。 + +当然我们也可以将视图的初始化单独放到一个类中,不过仍然需要处理 `DRKBackgroundView` 视图的布局等问题。 + +```objectivec +- (void)setupUI { + DRKBackgroundView *backgroundView = [[DRKBackgroundView alloc] init]; + [backgroundView.registerButton addTarget:self action:@selector(registerButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; + + [self.view addSubview:backgroundView]; + + [backgroundView mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.mas_equalTo(self.view); + }]; +} +``` + +`UIViewController` 的这种中心化的设计虽然简单,不过也导致了很多代码没有办法真正解耦,视图层必须依赖于 `UIViewController` 才能展示。 + +#### 惰性初始化 + +当然,很多人在 Controller 中也会使用惰性初始化的方式生成 Controller 中使用的视图,比如: + +```objectivec +@interface ViewController () + +@property (nonatomic, strong) UIImageView *backgroundView; + +@end + +@implementation ViewController + +- (UIImageView *)backgroundView { + if (!_backgroundView) { + _backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"backgroundView"]]; + } + return _backgroundView; +} + +@end +``` + +这样在 `-viewDidLoad` 方法中就可以直接处理视图的视图层级以及布局工作: + +```objectivec +- (void)viewDidLoad { + [super viewDidLoad]; + + [self.view addSubview:self.backgroundView]; + + [self.backgroundView mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.mas_equalTo(self.view); + }]; +} +``` + +惰性初始化的方法与其他方法其实并没有什么绝对的优劣,两者的选择只是对于代码规范的一种选择,我们所需要做的,只是在同一个项目中将其中一种做法坚持到底。 + +### 处理用户行为 + +在 `UIViewController` 中处理用户的行为是经常需要做的事情,这部分代码不能放到视图层或者其他地方的原因是,用户的行为经常需要与 Controller 的上下文有联系,比如,界面的跳转需要依赖于 `UINavigationController` 对象: + +```objectivec +- (void)registerButtonTapped:(UIButton *)button { + RegisterViewController *registerViewController = [[RegisterViewController alloc] init]; + [self.navigationController pushViewController:registerViewController animated:YES]; +} +``` + +而有的用户行为需要改变模型层的对象、持久存储数据库中的数据或者发出网络请求,主要因为我们要秉承着 MVC 的设计理念,避免 Model 层和 View 层的直接耦合。 + +### 存储当前界面的状态 + +在 iOS 中,我们经常需要处理表视图,而在现有的大部分表视图在加载内容时都会进行分页,使用下拉刷新和上拉加载的方式获取新的条目,而这就需要在 Controller 层保存当前显示的页数: + +```objectivec +@interface TableViewController () + +@property (nonatomic, assign) NSUInteger currentPage; + +@end +``` + +只有保存在了当前页数的状态,才能在下次请求网络数据时传入合适的页数,最后获得正确的资源,当然哪怕当前页数是可以计算出来的,比如通过当前的 Model 对象的数和每页个 Model 数,在这种情况下,我们也需要在当前 Controller 中 Model 数组的值。 + +```objectivec +@interface TableViewController () + +@property (nonatomic, strong) NSArray<Model *> *models; + +@end +``` + +在 MVC 的设计中,这种保存当前页面状态的需求是存在的,在很多复杂的页面中,我们也需要维护大量的状态,这也是 Controller 需要承担的重要职责之一。 + +### 处理界面之间的跳转 + +由于 Cocoa Touch 提供了 `UINavigationController` 和 `UITabBarController` 这两种容器 Controller,所以 iOS 中界面跳转的这一职责大部分都落到了 Controller 上。 + +![UINavigationController-UITabBarControlle](images/controller/UINavigationController-UITabBarController.jpg) + +iOS 中总共有三种界面跳转的方式: + ++ `UINavigationController` 中使用 push 和 pop 改变栈顶的 `UIViewController` 对象; ++ `UITabBarController` 中点击各个 `UITabBarItem` 实现跳转; ++ 使用所有的 `UIViewController` 实例都具有的 `-presentViewController:animated:completion` 方法; + +因为所有的 `UIViewController` 的实例都可以通过 `navigationController` 这一属性获取到最近的 `UINavigationController` 对象,所以我们不可避免的要在 Controller 层对界面之间的跳转进行操作。 + +> 当然,我们也可以引入 Router 路由对 `UIViewController` 进行注册,在访问合适的 URL 时,通过根 `UINavigationController` 进行跳转,不过这不是本篇文章想要说明的内容。 + +`UINavigationController` 提供的 API 还是非常简单的,我们可以直接使用 `-pushViewController:animated:` 就可以进行跳转。 + +```objectivec +RegisterViewController *registerViewController = [[RegisterViewController alloc] init]; +[self.navigationController pushViewController:registerViewController animated:YES]; +``` + +### 作为数据源以及代理 + +很多 Cocoa Touch 中视图层都是以代理的形式为外界提供接口的,其中最为典型的例子就是 `UITableView` 和它的数据源协议 `UITableViewDataSource` 和代理 `UITableViewDelegate`。 + +这是因为 `UITableView` 作为视图层的对象,需要根据 Model 才能知道自己应该展示什么内容,所以在早期的很多视图层组件都是用了代理的形式,从 Controller 或者其他地方获取需要展示的数据。 + +```objectivec +#pragma mark - UITableViewDataSource + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return self.models.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath]; + Model *model = self.models[indexPath.row]; + [cell setupWithModel:model]; + return cell; +} +``` + +上面就是使用 `UITableView` 时经常需要的方法。 + +很多文章中都提供了一种用于减少 Controller 层中代理方法数量的技巧,就是使用一个单独的类作为 `UITableView` 或者其他视图的代理: + +```objectivec +self.tableView.delegate = anotherObject; +self.tableView.dataSource = anotherObject; +``` + +然而在笔者看来这种办法并没有什么太大的用处,只是将代理方法挪到了一个其他的地方,如果这个代理方法还依赖于当前 `UIViewController` 实例的上下文,还要向这个对象中传入更多的对象,反而让原有的 MVC 变得更加复杂了。 + +### 负责 HTTP 请求的发起 + +当用户的行为触发一些事件时,比如下拉刷新、更新 Model 的属性等等,Controller 就需要通过 Model 层提供的接口向服务端发出 HTTP 请求,这一过程其实非常简单,但仍然是 Controller 层的职责,也就是响应用户事件,并且更新 Model 层的数据。 + +```objectivec +- (void)registerButtonTapped:(UIButton *)button { + LoginManager *manager = [LoginManager manager]; + manager.countryCode = _registerPanelView.countryCode; + ... + [manager startWithSuccessHandler:^(CCStudent *user) { + self.currentUser = user; + ... + } failureHandler:^(NSError *error) { + ... + }]; +} +``` + +当按钮被点击时 `LoginManager` 就会执行 `-startWithSuccessHandler:failureHandler:` 方法发起请求,并在请求结束后执行回调,更新 Model 的数据。 + +### 小结 + +iOS 中 Controller 层的职责一直都逃不开与 View 层和 Model 层的交互,因为其作用就是视图层的用户行为进行处理并更新视图的内容,同时也会改变模型层中的数据、使用 HTTP 请求向服务端请求新的数据等作用,其功能就是处理整个应用中的业务逻辑和规则。 + +但是由于 iOS 中 Controller 的众多职责,单一的 `UIViewController` 类可能会有上千行的代码,使得非常难以管理和维护,我们也希望在 iOS 中引入新的架构模式来改变 Controller 过于臃肿这一现状。 + +## 几点建议 + +Controller 层作为 iOS 应用中重要的组成部分,在 MVC 以及类似的架构下,笔者对于 Controller 的设计其实没有太多立竿见影的想法。作为应用中处理绝大多数逻辑的 Controller 其实很难简化其中代码的数量;我们能够做的,也是只对其中的代码进行一定的规范以提高它的可维护性,在这里,笔者有几点对于 Controller 层如何设计的建议,供各位读者参考。 + +### 不要把 DataSource 提取出来 + +iOS 中的 `UITableView` 和 `UICollectionView` 等需要 `dataSource` 的视图对象十分常见,在一些文章中会提议将数据源的实现单独放到一个对象中。 + +```objectivec +void (^configureCell)(PhotoCell*, Photo*) = ^(PhotoCell* cell, Photo* photo) { + cell.label.text = photo.name; +}; +photosArrayDataSource = [[ArrayDataSource alloc] initWithItems:photos + cellIdentifier:PhotoCellIdentifier + configureCellBlock:configureCell]; +self.tableView.dataSource = photosArrayDataSource; +``` + +在 [Lighter View Controllers](https://www.objc.io/issues/1-view-controllers/lighter-view-controllers/) 一文中就建议可以将数据源协议的实现方法放到 `ArrayDataSource` 对象中: + +```objectivec +@implementation ArrayDataSource + +- (id)itemAtIndexPath:(NSIndexPath*)indexPath { + return items[(NSUInteger)indexPath.row]; +} + +- (NSInteger)tableView:(UITableView*)tableView + numberOfRowsInSection:(NSInteger)section { + return items.count; +} + +- (UITableViewCell*)tableView:(UITableView*)tableView + cellForRowAtIndexPath:(NSIndexPath*)indexPath { + id cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier + forIndexPath:indexPath]; + id item = [self itemAtIndexPath:indexPath]; + configureCellBlock(cell,item); + return cell; +} + +@end +``` + +做出这种建议的理由是:单独的 `ArrayDataSource` 类可以更方便的进行测试,同时,展示一个数组的对象是表视图中非常常见的需求,而 `ArrayDataSource` 能够将这种需求抽象出来并进行重用,也可以达到减轻视图控制器负担的最终目的,但是在笔者看来,上述做法并没有起到**实质性**效果,只是简单的将视图控制器中的一部分代码*移到了*别的位置而已,还会因为增加了额外的类使 Controller 的维护变得更加的复杂。 + +![UITableView-DataSource](images/controller/UITableView-DataSource.jpg) + +让每一个 Controller 作为 `UITableView` 对象的代理和数据源其实是没有任何问题的,将这些方法移出 Controller 并不能解决实际的问题。 + +### 属性和实例变量的选择 + +文章的前面曾经提到过在很多的 iOS 应用中,Controller 由于持有一个根视图 `UIView` 对象,所以需要负责展示内容以及布局,很多 iOS 开发者都把一些模块的视图层代码放到了控制器中,但是无论是将视图层代码放到控制器中,还是新建一个单独的视图类都需要对视图以及子视图进行初始化和布局。 + +在对视图进行初始化和布局时,我们有两种选择,一种是使用实例变量的方式主动对视图对象进行初始化,另一种是使用属性 `@property` 对视图对象进行惰性初始化。 + +![Eager-Lazy-Initialization](images/controller/Eager-Lazy-Initialization.jpg) + +虽然上述两种代码在结果上几乎是等价的,但是笔者更加偏好两者之中的后者,它将各个视图属性的初始化放到了各个属性的 getter 方法中,能够将代码在逻辑上分块还是比较清晰的。这两种方法其实只是不同的 taste,有些人会坚持将不需要暴露的变量都写成 `_xxx` 的形式,有些人更喜欢后者这种分散的写法,这些都不是什么太大的问题,而且很多人担心的性能问题其实也根本不是问题,重要的是我们要在同一个项目中坚持同一种写法,并且保证只有同一个风格的代码合入主分支。 + +### 把业务逻辑移到 Model 层 + +控制器中有很多代码和逻辑其实与控制器本身并没有太多的关系,比如: + +```objectivec +@implementation ViewController + +- (NSString *)formattedPostCreatedAt { + NSDateFormatter *format = [[NSDateFormatter alloc] init]; + [format setDateFormat:@"MMM dd, yyyy HH:mm"]; + return [format stringFromDate:self.post.createdAt]; +} + +@end +``` + +在 [谈谈 MVX 中的 Model 层](http://draveness.me/mvx-model.html) 一文中,我们曾经分析过,上述逻辑其实应该属于 Model 层,作为 `Post` 的一个实例方法: + +```objectivec +@implementation Post + +- (NSString *)formattedCreatedAt { + NSDateFormatter *format = [[NSDateFormatter alloc] init]; + [format setDateFormat:@"MMM dd, yyyy HH:mm"]; + return [format stringFromDate:self.createdAt]; +} + +@end +``` + +这一条建议是从一些经典的后端 MVC 框架中学习的,Rails 提倡 *Fat Model, Skinny Controller* 就是希望开发者将 Model 相关的业务逻辑都放到 Model 层中,以减轻 Controller 层的负担。 + +### 把视图层代码移到 View 层 + +因为 UIKit 框架设计的原因,Controller 和 View 层是强耦合的,每一个 `UIViewController` 都会持有一个 `UIView` 视图对象,这也是导致我们将很多的视图层代码直接放在 Controller 层的原因。 + +![MVC-in-iOS](images/controller/MVC-in-iOS.jpg) + +这种做法在当前模块的视图层比较简单时,笔者觉得没有任何的问题,虽然破坏了经典的 MVC 的架构图,但是也不是什么问题;不过,当视图层的视图对象非常多的时候,大量的配置和布局代码就会在控制器中占据大量的位置,我们可以将整个视图层的代码都移到一个单独的 `UIView` 子类中。 + +```objectivec +// RegisterView.h +@interface RegisterView : UIView + +@property (nonatomic, strong) UITextField *phoneNumberTextField; +@property (nonatomic, strong) UITextField *passwordTextField; + +@end + +// RegisterView.m +@implementation RegisterView + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + [self addSubview:self.phoneNumberTextField]; + [self addSubview:self.passwordTextField]; + + [self.phoneNumberTextField mas_makeConstraints:^(MASConstraintMaker *make) { + ... + }]; + [self.passwordTextField mas_makeConstraints:^(MASConstraintMaker *make) { + ... + }]; + } + return self; +} + +- (UITextField *)phoneNumberTextField { + if (!_phoneNumberTextField) { + _phoneNumberTextField = [[UITextField alloc] init]; + _phoneNumberTextField.font = [UIFont systemFontOfSize:16]; + } + return _phoneNumberTextField; +} + +- (UITextField *)passwordTextField { + if (!_passwordTextField) { + _passwordTextField = [[UITextField alloc] init]; + ... + } + return _passwordTextField; +} + +@end +``` + +而 Controller 需要持有该视图对象,并将自己持有的根视图替换成该视图对象: + +```objectivec +@interface ViewController () + +@property (nonatomic, strong) RegisterView *view; + +@end + +@implementation ViewController + +@dynamic view; + +- (void)loadView { + self.view = [[RegisterView alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; +} + +@end +``` + +在 `UIViewController` 对象中,我们可以通过覆写 `-loadView` 方法改变其本身持有的视图对象,并使用新的 `@property` 声明以及 `@dynamic` 改变 Controller 持有的根视图,这样我们就把视图层的配置和布局代码从控制器中完全分离了。 + +### 使用 pragma 或 extension 分割代码块 + +在很多时候,我们对于 Controller 中上千行的代码是非常绝望的,不熟悉这个模块的开发者想要在里面快速找到自己想要的信息真的是非常的麻烦,尤其是如果一个 `UIViewController` 中的代码没有被组织好的话,那分析起来更是异常头疼。 + +我们既然没有把上千行的代码瞬间变没的方法,那就只能想想办法在现有的代码上进行美化了,办法其实很简单,就是将具有相同功能的代码分块并使用 `pragma` 预编译指定或者 `MARK` 加上 `extension` 对代码块进行分割。 + +这里给一个简单的例子, + +```objectivec +@implementation ViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [self setupUI]; +} + +- (void)layoutSubviews { } + +#pragma mark - UI + +- (void)setupUI {} + +#pragma mark - UITableViewDataSource + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return 1; +} +... + +#pragma mark - UITableViewDelegate + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + return 100.0; +} +... + +#pragma mark - Callback + +- (void)buttonTapped:(UIButton *)button {} +- (void)gestureTriggered:(UIGestureRecognizer *)gesture {} +- (void)keyboardWillShow:(NSNotification *)noti {} + +#pragma mark - Getter/Setter + +- (NSString *)string { return _string; } +- (void)setString:(NSString*)string { _string = string; } + +#pragma mark - Helper + +- (void)helperMethod {} + +@end +``` + +一个 `UIViewController` 大体由上面这些部分组成: + ++ 生命周期以及一些需要 `override` 的方法 ++ 视图层代码的初始化 ++ 各种数据源和代理协议的实现 ++ 事件、手势和通知的回调 ++ 实例变量的存取方法 ++ 一些其他的 Helper 方法 + +在 Objective-C 的工程中,我们使用 `pragma` 预编译指令来对 `UIViewController` 中的;在 Swift 中,我们可以使用 `extension` 加上 `MARK` 来对代码进行分块: + +```swift +class ViewController: UIViewController {} + +// MARK: - UI +extension ViewController {} + +// MARK: - UITableViewDataSource +extension ViewController: UITableViewDataSource {} + +// MARK: - UITableViewDelegate +extension ViewController: UITableViewDelegate {} + +// MARK: - Callback +extension ViewController {} + +// MARK: - Getter/Setter +extension ViewController {} + +// MARK: - Helper +extension ViewController {} +``` + +上述方法是一种在控制器层分割代码块的方法,它们的顺序并不是特别的重要,最重要的还是要在不同的控制器中保持上述行为的一致性,将合理的方法放到合适的代码块中。 + +### 耦合的 View 和 Model 层 + +很多的 iOS 项目中都会为 `UIView` 添加一个绑定 Model 对象的方法,比如说: + +```objectivec +@implementation UIView (Model) + +- (void)setupWithModel:(id)model {} + +@end +``` + +这个方法也可能叫做 `-bindWithModel:` 或者其他名字,其作用就是根据传入的 Model 对象更新当前是视图中的各种状态,比如 `UILabel` 中的文本、`UIImageView` 中的图片等等。 + +有了上述分类,我们可以再任意的 `UIView` 的子类中覆写该方法: + +```objectivec +- (void)setupWithModel:(Model *)model { + self.imageView.image = model.image; + self.label.text = model.name; +} +``` + +这种做法其实是将原本 Controller 做的事情放到了 View 中,由视图层来负责如何展示模型对象;虽然它能够减少 Controller 中的代码,但是也导致了 View 和 Model 的耦合。 + +![Coupling-View-And-Mode](images/controller/Coupling-View-And-Model.jpg) + +对于 MVC 架构模式中,Model、View 和 Controller 之间的交互没有明确的规则,但是视图和模型之间的耦合会导致视图层代码很难复用;因为这样设计的视图层都依赖于外部的模型对象,所以**如果同一个视图需要显示多种类型的模型时就会遇到问题**。 + +视图和模型之间解耦是通过控制器来处理的,控制器获取模型对象并取出其中的属性一一装填到视图中,也就是将 `-setupWithModel:` 方法中的代码从视图层移到控制器层中,并在视图类中暴露合适的接口。 + +## 总结 + +本文虽然对 Controller 层的职责进行了分析,但是由于 Controller 在 MVC 中所处的位置,如果不脱离 MVC 架构模式,那么 Controller 的职责很难简化,只能在代码规范和职责划分上进行限制,而在下一篇文章中我们会详细讨论 MVC 以及衍化出来的 MVP 以及 MVVM 到底是什么、以及它们有什么样的差异。 + +## Reference + ++ [Lighter View Controllers](https://www.objc.io/issues/1-view-controllers/lighter-view-controllers/) + + diff --git a/contents/architecture/mvx-model.md b/contents/architecture/mvx-model.md new file mode 100644 index 0000000..d89f792 --- /dev/null +++ b/contents/architecture/mvx-model.md @@ -0,0 +1,571 @@ +# 谈谈 MVX 中的 Model + ++ [谈谈 MVX 中的 Model](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/architecture/mvx-model.md) ++ [谈谈 MVX 中的 View](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/architecture/mvx-view.md) ++ [谈谈 MVX 中的 Controller](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/architecture/mvx-controller.md) ++ [浅谈 MVC、MVP 和 MVVM 架构模式](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/architecture/mvx.md) + +> Follow GitHub: [Draveness](https://github.com/Draveness) + + +## 常见的 Model 层 + +在大多数 iOS 的项目中,Model 层只是一个单纯的数据结构,你可以看到的绝大部分模型都是这样的: + +```swift +struct User { + enum Gender: String { + case male = "male" + case female = "female" + } + let name: String + let email: String + let age: Int + let gender: Gender +} +``` + +模型起到了定义一堆『坑』的作用,只是一个简单的模板,并没有参与到实际的业务逻辑,只是在模型层进行了一层**抽象**,将服务端发回的 JSON 或者说 `Dictionary` 对象中的字段一一取出并装填到预先定义好的模型中。 + +![JSON-to-Mode](images/model/JSON-to-Model.jpg) + +我们可以将这种模型层中提供的对象理解为『即开即用』的 `Dictionary` 实例;在使用时,可以直接从模型中取出属性,省去了从 `Dictionary` 中抽出属性以及验证是否合法的过程。 + +```swift +let user = User... + +nameLabel.text = user.name +emailLabel.text = user.email +ageLabel.text = "\(user.age)" +genderLabel.text = user.gender.rawValue +``` + +### JSON -> Model + +使用 Swift 将 `Dictionary` 转换成模型,在笔者看来其实是一件比较麻烦的事情,主要原因是 Swift 作为一个号称类型安全的语言,有着使用体验非常差的 Optional 特性,从 `Dictionary` 中取出的值都是不一定存在的,所以如果需要纯手写这个过程其实还是比较麻烦的。 + +```swift +extension User { + init(json: [String: Any]) { + let name = json["name"] as! String + let email = json["email"] as! String + let age = json["age"] as! Int + let gender = Gender(rawValue: json["gender"] as! String)! + self.init(name: name, email: email, age: age, gender: gender) + } +} +``` + +这里为 `User` 模型创建了一个 extension 并写了一个简单的模型转换的初始化方法,当我们从 JSON 对象中取值时,得到的都是 Optional 对象;而在大多数情况下,我们都没有办法直接对 Optional 对象进行操作,这就非常麻烦了。 + +#### 麻烦的 Optional + +在 Swift 中遇到无法立即使用的 Optional 对象时,我们可以会使用 `!` 默认将字典中取出的值当作非 Optional 处理,但是如果服务端发回的数据为空,这里就会直接崩溃;当然,也可使用更加安全的 `if let` 对 Optional 对象进行解包(unwrap)。 + +```swift +extension User { + init?(json: [String: Any]) { + if let name = json["name"] as? String, + let email = json["email"] as? String, + let age = json["age"] as? Int, + let genderString = json["gender"] as? String, + let gender = Gender(rawValue: genderString) { + self.init(name: name, email: email, age: age, gender: gender) + } + return nil + } +} +``` + +上面的代码看起来非常的丑陋,而正是因为上面的情况在 Swift 中非常常见,所以社区在 Swift 2.0 中引入了 `guard` 关键字来优化代码的结构。 + +```swift +extension User { + init?(json: [String: Any]) { + guard let name = json["name"] as? String, + let email = json["email"] as? String, + let age = json["age"] as? Int, + let genderString = json["gender"] as? String, + let gender = Gender(rawValue: genderString) else { + return nil + } + self.init(name: name, email: email, age: age, gender: gender) + } +} +``` + +不过,上面的代码在笔者看来,并没有什么本质的区别,不过使用 `guard` 对错误的情况进行提前返回确实是一个非常好的编程习惯。 + +#### 不关心空值的 OC + +为什么 Objective-C 中没有这种问题呢?主要原因是在 OC 中所有的对象其实都是 Optional 的,我们也并不在乎对象是否为空,因为在 OC 中**向 nil 对象发送消息并不会造成崩溃,Objective-C 运行时仍然会返回 nil 对象**。 + +> 这虽然在一些情况下会造成一些问题,比如,当 `nil` 导致程序发生崩溃时,比较难找到程序中 `nil` 出现的原始位置,但是却保证了程序的灵活性,笔者更倾向于 Objective-C 中的做法,不过这也就见仁见智了。 + +OC 作为动态语言,这种设计思路其实还是非常优秀的,它避免了大量由于对象不存在导致无法完成方法调用造成的崩溃;同时,作为开发者,我们往往都不需要考虑 `nil` 的存在,所以使用 OC 时写出的模型转换的代码都相对好看很多。 + +```objectivec +// User.h +typedef NS_ENUM(NSUInteger, Gender) { + Male = 0, + Female = 1, +}; + +@interface User: NSObject + +@property (nonatomic, strong) NSString *email; +@property (nonatomic, strong) NSString *name; +@property (nonatomic, assign) NSUInteger age; +@property (nonatomic, assign) Gender gender; + +@end + +// User.m +@implementation User + +- (instancetype)initWithJSON:(NSDictionary *)json { + if (self = [super init]) { + self.email = json[@"email"]; + self.name = json[@"name"]; + self.age = [json[@"age"] integerValue]; + self.gender = [json[@"gender"] integerValue]; + } + return self; +} + +@end +``` + +当然,在 OC 中也有很多优秀的 JSON 转模型的框架,如果我们使用 YYModel 这种开源框架,其实只需要写一个 `User` 类的定义就可以获得 `-yy_modelWithJSON:` 等方法来初始化 `User` 对象: + +```objectivec +User *user = [User yy_modelWithJSON:json]; +``` + +而这也是通过 Objective-C 强大的运行时特性做到的。 + +> 除了 YYModel,我们也可以使用 Mantle 等框架在 OC 中解决 JSON 到模型的转换的问题。 + +#### 元编程能力 + +从上面的代码,我们可以看出:Objective-C 和 Swift 对于相同功能的处理,却有较大差别的实现。这种情况的出现主要原因是语言的设计思路导致的;Swift 一直*鼓吹*自己有着较强的安全性,能够写出更加稳定可靠的应用程序,而安全性来自于 Swift 语言的设计哲学;由此看来静态类型、安全和动态类型、元编程能力(?)看起来是比较难以共存的。 + +> 其实很多静态编程语言,比如 C、C++ 和 Rust 都通过宏实现了比较强大的元编程能力,虽然 Swift 也通过模板在元编程支持上做了一些微小的努力,不过到目前来看( 3.0 )还是远远不够的。 + +![Dynamic-Stati](images/model/Dynamic-Static.png) + +OC 中对于 `nil` 的处理能够减少我们在编码时的工作量,不过也对工程师的代码质量提出了考验。我们需要思考 `nil` 的出现会不会带来崩溃,是否会导致行为的异常、增加应用崩溃的风险以及不确定性,而这也是 Swift 引入 Optional 这一概念来避免上述问题的初衷。 + +相比而言,笔者还是更喜欢强大的元编程能力,这样可以减少大量的重复工作并且提供更多的可能性,与提升工作效率相比,牺牲一些安全性还是可以接受的。 + +### 网络服务 Service 层 + +现有的大多数应用都会将网路服务组织成单独的一层,所以有时候你会看到所谓的 MVCS 架构模式,它其实只是在 MVC 的基础上加上了一个服务层(Service),而在 iOS 中常见的 MVC 架构模式也都可以理解为 MVCS 的形式,当引入了 Service 层之后,整个数据的获取以及处理的流程是这样的: + +![MVCS-Architecture](images/model/MVCS-Architecture.png) + +1. 大多数情况下服务的发起都是在 Controller 中进行的; +2. 然后会在 HTTP 请求的回调中交给模型层处理 JSON 数据; +3. 返回开箱即用的对象交还给 Controller 控制器; +4. 最后由 View 层展示服务端返回的数据; + +不过按理来说服务层并不属于模型层,为什么要在这里进行介绍呢?这是因为 **Service 层其实与 Model 层之间的联系非常紧密**;网络请求返回的结果决定了 Model 层该如何设计以及该有哪些功能模块,而 Service 层的设计是与后端的 API 接口的设计强关联的,这也是我们谈模型层的设计无法绕过的坑。 + +iOS 中的 Service 层大体上有两种常见的组织方式,其中一种是命令式的,另一种是声明式的。 + +#### 命令式 + +命令式的 Service 层一般都会为每一个或者一组 API 写一个专门用于 HTTP 请求的 Manager 类,在这个类中,我们会在每一个静态方法中使用 AFNetworking 或者 Alamofire 等网络框架发出 HTTP 请求。 + +```objectivec +import Foundation +import Alamofire + +final class UserManager { + static let baseURL = "http://localhost:3000/" + static let usersBaseURL = "\(baseURL)/users" + + static func allUsers(completion: @escaping ([User]) -> ()) { + let url = "\(usersBaseURL)" + Alamofire.request(url).responseJSON { response in + if let jsons = response.result.value as? [[String: Any]] { + let users = User.users(jsons: jsons) + completion(users) + } + } + } + + static func user(id: Int, completion: @escaping (User) -> ()) { + let url = "\(usersBaseURL)/\(id)" + Alamofire.request(url).responseJSON { response in + if let json = response.result.value as? [String: Any], + let user = User(json: json) { + completion(user) + } + } + } +} +``` + +在这个方法中,我们完成了网络请求、数据转换 JSON、JSON 转换到模型以及最终使用 `completion` 回调的过程,调用 Service 服务的 Controller 可以直接从回调中使用构建好的 Model 对象。 + +```objectivec +UserManager.user(id: 1) { user in + self.nameLabel.text = user.name + self.emailLabel.text = user.email + self.ageLabel.text = "\(user.age)" + self.genderLabel.text = user.gender.rawValue +} +``` + +#### 声明式 + +使用声明式的网络服务层与命令式的方法并没有本质的不同,它们最终都调用了底层的一些网络库的 API,这种网络服务层中的请求都是以配置的形式实现的,需要对原有的命令式的请求进行一层封装,也就是说所有的参数 `requestURL`、`method` 和 `parameters` 都应该以配置的形式声明在每一个 `Request` 类中。 + +![Abstract-Request](images/model/Abstract-Request.jpg) + +如果是在 Objective-C 中,一般会定义一个抽象的基类,并让所有的 Request 都继承它;但是在 Swift 中,我们可以使用协议以及协议扩展的方式实现这一功能。 + +```swift +protocol AbstractRequest { + var requestURL: String { get } + var method: HTTPMethod { get } + var parameters: Parameters? { get } +} + +extension AbstractRequest { + func start(completion: @escaping (Any) -> Void) { + Alamofire.request(requestURL, method: self.method).responseJSON { response in + if let json = response.result.value { + completion(json) + } + } + } +} +``` + +在 `AbstractRequest` 协议中,我们定义了发出一个请求所需要的全部参数,并在协议扩展中实现了 `start(completion:)` 方法,这样实现该协议的类都可以直接调用 `start(completion:)` 发出网络请求。 + +```swift +final class AllUsersRequest: AbstractRequest { + let requestURL = "http://localhost:3000/users" + let method = HTTPMethod.get + let parameters: Parameters? = nil +} + +final class FindUserRequest: AbstractRequest { + let requestURL: String + let method = HTTPMethod.get + let parameters: Parameters? = nil + + init(id: Int) { + self.requestURL = "http://localhost:3000/users//(id)" + } +} +``` + +我们在这里写了两个简单的 `Request` 类 `AllUsersRequest` 和 `FindUserRequest`,它们两个一个负责获取所有的 `User` 对象,一个负责从服务端获取指定的 `User`;在使用上面的声明式 Service 层时也与命令式有一些不同: + +```swift +FindUserRequest(id: 1).start { json in + if let json = json as? [String: Any], + let user = User(json: json) { + print(user) + } +} +``` + +因为在 Swift 中,我们没法将 JSON 在 Service 层转换成模型对象,所以我们不得不在 `FindUserRequest` 的回调中进行类型以及 JSON 转模型等过程;又因为 HTTP 请求可能依赖其他的参数,所以在使用这种形式请求资源时,我们需要在初始化方法传入参数。 + +#### 命令式 vs 声明式 + +现有的 iOS 开发中的网络服务层一般都是使用这两种组织方式,我们一般会按照**资源**或者**功能**来划分命令式中的 `Manager` 类,而声明式的 `Request` 类与实际请求是一对一的关系。 + +![Manager-And-Request](images/model/Manager-And-Request.jpg) + +这两种网络层的组织方法在笔者看来没有高下之分,无论是 `Manager` 还是 `Request` 的方式,尤其是后者由于一个类只对应一个 API 请求,在整个 iOS 项目变得异常复杂时,就会导致**网络层类的数量剧增**。 + +这个问题并不是不可以接受的,在大多数项目中的网络请求就是这么做的,虽然在查找实际的请求类时有一些麻烦,不过只要遵循一定的**命名规范**还是可以解决的。 + +### 小结 + +现有的 MVC 下的 Model 层,其实只起到了对数据结构定义的作用,它将服务端返回的 JSON 数据,以更方便使用的方式包装了一下,这样呈现给上层的就是一些即拆即用的『字典』。 + +![Model-And-Dictioanry](images/model/Model-And-Dictioanry.jpg) + +单独的 Model 层并不能返回什么关键的作用,它只有与网络服务层 Service 结合在一起的时候才能发挥更重要的能力。 + +![Service-And-API](images/model/Service-And-API.jpg) + +而网络服务 Service 层是对 HTTP 请求的封装,其实现形式有两种,一种是命令式的,另一种是声明式的,这两种实现的方法并没有绝对的优劣,遵循合适的形式设计或者重构现有的架构,随着应用的开发与迭代,为上层提供相同的接口,保持一致性才是设计 Service 层最重要的事情。 + +## 服务端的 Model 层 + +虽然文章是对客户端中 Model 层进行分析和介绍,但是在客户端大规模使用 MVC 架构模式之前,服务端对于 MVC 的使用早已有多年的历史,而移动端以及 Web 前端对于架构的设计是近年来才逐渐被重视。 + +因为客户端的应用变得越来越复杂,动辄上百万行代码的巨型应用不断出现,以前流水线式的开发已经没有办法解决现在的开发、维护工作,所以合理的架构设计成为客户端应用必须要重视的事情。 + +这一节会以 Ruby on Rails 中 Model 层的设计为例,分析在经典的 MVC 框架中的 Model 层是如何与其他模块进行交互的,同时它又担任了什么样的职责。 + +### Model 层的职责 + +Rails 中的 Model 层主要承担着以下两大职责: + +1. 使用数据库存储并管理 Web 应用的数据; +2. 包含 Web 应用**所有**的业务逻辑; + +除了上述两大职责之外,Model 层还会存储应用的状态,同时,由于它对用户界面一无所知,所以它不依赖于任何视图的状态,这也使得 Model 层的代码可以复用。 + +Model 层的两大职责决定了它在整个 MVC 框架的位置: + +![Server-MV](images/model/Server-MVC.jpg) + +因为 Model 是对数据库中表的映射,所以当 Controller 向 Model 层请求数据时,它会从数据库中获取相应的数据,然后对数据进行加工最后返回给 Controller 层。 + +#### 数据库 + +Model 层作为数据库中表的映射,它就需要实现两部分功能: + +1. 使用合理的方式对数据库进行迁移和更新; +2. 具有数据库的绝大部分功能,包括最基础的增删改查; + +在这里我们以 Rails 的 ActiveRecord 为例,简单介绍这两大功能是如何工作的。 + +ActiveRecord 为数据库的迁移和更新提供了一种名为 Migration 的机制,它可以被理解为一种 DSL,对数据库中的表的字段、类型以及约束进行描述: + +```ruby +class CreateProducts < ActiveRecord::Migration[5.0] + def change + create_table :products do |t| + t.string :name + t.text :description + end + end +end +``` + +上面的 Ruby 代码创建了一个名为 `Products` 表,其中包含三个字段 `name`、`description` 以及一个默认的主键 `id`,然而在上述文件生成时,数据库中对应的表还不存在,当我们在命令行中执行 `rake db:migrate` 时,才会执行下面的 SQL 语句生成一张表: + +```sql +CREATE TABLE products ( + id int(11) DEFAULT NULL auto_increment PRIMARY KEY + name VARCHAR(255), + description text, +); +``` + +同样地,如果我们想要更新数据库中的表的字段,也需要创建一个 Migration 文件,ActiveRecord 会为我们直接生成一个 SQL 语句并在数据库中执行。 + +ActiveRecord 对数据库的增删改查功能都做了相应的实现,在使用它进行数据库查询时,会生成一条 SQL 语句,在数据库中执行,并将执行的结果初始化成一个 Model 的实例并返回: + +```ruby +user = User.find(10) +# => SELECT * FROM users WHERE (users.id = 10) LIMIT 1 +``` + +这就是 ActiveRecord 作为 Model 层的 ORM 框架解决两个关键问题的方式,其最终结果都是生成一条 SQL 语句并扔到数据库中执行。 + +![Relation-Between-Database-And-Mode](images/model/Relation-Between-Database-And-Model.jpg) + +总而言之,Model 层为调用方屏蔽了所有与数据库相关的底层细节,使开发者不需要考虑如何手写 SQL 语句,只需要关心原生的代码,能够极大的降低出错的概率;但是,由于 SQL 语句都由 Model 层负责处理生成,它并不会根据业务帮助我们优化 SQL 查询语句,所以在遇到数据量较大时,其性能难免遇到各种问题,我们仍然需要手动优化查询的 SQL 语句。 + +#### Controller + +Model 与数据库之间的关系其实大多数都与数据的存储查询有关,而与 Controller 的关系就不是这样了,在 Rails 这个 MVC 框架中,提倡将业务逻辑放到 Model 层进行处理,也就是所谓的: + +> Fat Models, skinny controllers. + +这种说法形成的原因是,在绝大部分的 MVC 框架中,Controller 的作用都是将请求代理给 Model 去完成,它本身并不包含任何的业务逻辑,任何实际的查询、更新和删除操作都不应该在 Controller 层直接进行,而是要讲这些操作交给 Model 去完成。 + +```ruby +class UsersController + def show + @user = User.find params[:id] + end +end +``` + +这也就是为什么在后端应用中设计合理的 Controller 实际上并没有多少行代码,因为大多数业务逻辑相关的代码都会放到 Model 层。 + +Controller 的作用更像是胶水,将 Model 层中获取的模型传入 View 层中,渲染 HTML 或者返回 JSON 数据。 + +### 小结 + +虽然服务端对于应用架构的设计已经有了很长时间的沉淀,但是由于客户端和服务端的职责截然不同,我们可以从服务端借鉴一些设计,但是并不应该照搬后端应用架构设计的思路。 + +服务端重数据,如果把整个 Web 应用看做一个黑箱,那么它的输入就是用户发送的数据,发送的形式无论是遵循 HTTP 协议也好还是其它协议也好,它们都是数据。 + +![web-black-box](images/model/web-black-box.jpg) + +在服务端拿到数据后对其进行处理、加工以及存储,最后仍然以数据的形式返回给用户。 + +而客户端重展示,其输入就是用户的行为触发的事件,而输出是用户界面: + +![client-black-box](images/model/client-black-box.jpg) + +也就是说,用户的行为在客户端应用中得到响应,并更新了用户界面 GUI。总而言之: + +> 客户端重展示,服务端重数据。 + +这也是在设计客户端 Model 层时需要考虑的重要因素。 + +## 理想中的 Model 层 + +在上面的两个小节中,分别介绍了 iOS 中现有的 Model 层以及服务端的 Model 层是如何使用的,并且介绍了它们的职责,在这一章节中,我们准备介绍笔者对于 Model 层的看法以及设计。 + +### 明确职责 + +在具体讨论 Model 层设计之前,肯定要明确它的职责,它应该做什么、不应该做什么以及需要为外界提供什么样的接口和功能。 + +客户端重展示,无论是 Web、iOS 还是 Android,普通用户应该**无法直接接触到服务端**,如果一个软件系统的使用非常复杂,并且让**普通**用户**直接**接触到服务端的各种报错、提示,比如 404 等等,那么这个软件的设计可能就是不合理的。 + +> 这里加粗了普通和直接两个词,如果对这句话有疑问,请多读几遍 :) +> 专业的错误信息在软件工程师介入排错时非常有帮助,这种信息应当放置在不明显的角落。 + +![404](images/model/404.gif) + +作为软件工程师或者设计师,应该为用户提供更加合理的界面以及展示效果,比如,使用*您所浏览的网页不存在*来描述或者代替只有从事软件开发行业的人才了解的 404 或者 500 等错误是更为**合适**的方式。 + +上面的例子主要是为了说明客户端的最重要的职责,将**数据合理地展示给用户**,从这里我们可以领会到,Model 层虽然重要,但是却不是客户端最为复杂的地方,它只是起到了一个将服务端数据『映射』到客户端的作用,这个映射的过程就是获取数据的过程,也决定了 Model 层在 iOS 应用中的位置。 + +![Model-in-Client](images/model/Model-in-Client.jpg) + +那么这样就产生了几个非常重要的问题和子问题: + ++ 数据如何获取? + + 在何时获取数据? + + 如何存储服务端的数据? ++ 数据如何展示? + + 应该为上层提供什么样的接口? + +### Model 层 += Service 层? + +首先,我们来解决数据获取的问题,在 iOS 客户端常见的 Model 层中,数据的获取都不是由 Model 层负责的,而是由一个单独的 Service 层进行处理,然而经常这么组织网络请求并不是一个非常优雅的办法: + +1. 如果按照 API 组织 Service 层,那么网络请求越多,整个项目的 Service 层的类的数量就会越庞大; +2. 如果按照资源组织 Service 层,那么为什么不把 Service 层中的代码直接扔到 Model 层呢? + +既然 HTTP 请求都以获取相应的资源为目标,那么以 Model 层为中心来组织 Service 层并没有任何语义和理解上的问题。 + +如果服务端的 API 严格地按照 RESTful 的形式进行设计,那么就可以在客户端的 Model 层建立起一一对应的关系,拿最基本的几个 API 请求为例: + +```swift +extension RESTful { + static func index(completion: @escaping ([Self]) -> ()) + + static func show(id: Int, completion: @escaping (Self?) -> ()) + + static func create(params: [String: Any], completion: @escaping (Self?) -> ()) + + static func update(id: Int, params: [String: Any], completion: @escaping (Self?) -> ()) + + static func delete(id: Int, completion: @escaping () -> ()) +} +``` + +我们在 Swift 中通过 Protocol Extension 的方式为所有遵循 `RESTful` 协议的模型添加基本的 CRUD 方法,那么 `RESTful` 协议本身又应该包含什么呢? + +```swift +protocol RESTful { + init?(json: [String: Any]) + static var url: String { get } +} +``` + +RESTful 协议本身也十分简单,一是 JSON 转换方法,也就是如何将服务器返回的 JSON 数据转换成对应的模型,另一个是资源的 `url` + +> 对于这里的 `url`,我们可以遵循约定优于配置的原则,通过反射获取一个**默认**的资源链接,从而简化原有的 `RESTful` 协议,但是这里为了简化代码并没有使用这种方法。 + +```swift +extension User: RESTful { + static var url: String { + return "http://localhost:3000/users" + } + + init?(json: [String: Any]) { + guard let id = json["id"] as? Int, + let name = json["name"] as? String, + let email = json["email"] as? String, + let age = json["age"] as? Int, + let genderValue = json["gender"] as? Int, + let gender = Gender(rawInt: genderValue) else { + return nil + } + self.init(id: id, name: name, email: email, age: age, gender: gender) + } +} +``` + +在 `User` 模型遵循上述协议之后,我们就可以简单的通过它的静态方法来对服务器上的资源进行一系列的操作。 + +```swift +User.index { users in + // users +} + +User.create(params: ["name": "Stark", "email": "example@email.com", "gender": 0, "age": 100]) { user in + // user +} +``` + +当然 RESTful 的 API 接口仍然需要服务端提供支持,不过以 Model 取代 Service 作为 HTTP 请求的发出者确实是可行的。 + +#### 问题 + +虽然上述的方法简化了 Service 层,但是在真正使用时确实会遇到较多的限制,比如,用户需要对另一用户进行关注或者取消关注操作,这样的 API 如果要遵循 RESTful 就需要使用以下的方式进行设计: + +```swift +POST /api/users/1/follows +DELETE /api/users/1/follows +``` + +这种情况就会导致在当前的客户端的 Model 层没法建立合适的抽象,因为 `follows` 并不是一个真实存在的模型,它只代表两个用户之间的关系,所以在当前所设计的模型层中没有办法实现上述的功能,还需要引入 Service 层,来对服务端中的每一个 Controller 的 action 进行抽象,在这里就不展开讨论了。 + +对 Model 层网络服务的设计,与服务端的设计有着非常大的关联,如果能够对客户端和服务端之间的 API 进行严格规范,那么对于设计出简洁、优雅的网络层还是有巨大帮助的。 + +### 缓存与持久存储 + +客户端的持久存储其实与服务端的存储天差地别,客户端中保存的各种数据更准确的说其实是**缓存**,既然是缓存,那么它在客户端应用中的地位并不是极其重要、非他不可的;正相反,很多客户端应用没有缓存也运行的非常好,它并不是一个必要的功能,只是能够提升用户体验而已。 + +虽然客户端的存储只是缓存,但是在目前的大型应用中,也确实需要这种缓存,有以下几个原因: + ++ 能够快速为用户提供可供浏览的内容; ++ 在网络情况较差或者无网络时,也能够为用户提供兜底数据; + +以上的好处其实都是从用户体验的角度说的,不过缓存确实能够提高应用的质量。 + +在 iOS 中,持久存储虽然不是一个必要的功能,但是苹果依然为我们提供了不是那么好用的 Core Data 框架,但这并不是这篇文章需要介绍和讨论的内容。 + +目前的绝大多数 Model 框架,其实提供的都只是**硬编码**的数据库操作能力,或者提供的 API 不够优雅,原因是虽然 Swift 语法比 Objective-C 更加简洁,但是缺少元编程能力是它的硬伤。 + +熟悉 ActiveRecord 的开发者应该都熟悉下面的使用方式: + +```ruby +User.find_by_name "draven" +``` + +在 Swift 中通过现有的特性很难提供这种 API,所以很多情况下只能退而求其次,继承 `NSObject` 并且使用 `dynamic` 关键字记住 Objective-C 的特性实现一些功能: + +```objectivec +class User: Object { + dynamic var name = "" + dynamic var age = 0 +} +``` + +这确实是一种解决办法,但是并不是特别的优雅,如果我们在编译器间获得模型信息,然后使用这些信息生成代码就可以解决这些问题了,这种方法同时也能够在 Xcode 编译器中添加代码提示。 + +### 上层接口 + +Model 层为上层提供提供的接口其实就是自身的一系列属性,只是将服务器返回的 JSON 经过处理和类型转换,变成了即拆即用的数据。 + +![JSON-Mode](images/model/JSON-Model.jpg) + +上层与 Model 层交互有两种方式,一是通过 Model 层调用 HTTP 请求,异步获取模型数据,另一种就是通过 Model 暴露出来的属性进行存取,而底层数据库会在 Model 属性更改时发出网络请求并且修改对应的字段。 + +## 总结 + +虽然客户端的 Model 层与服务端的 Model 层有着相同的名字,但是客户端的 Model 层由于处理的是缓存,对本地的数据库中的表进行迁移、更改并不是一个必要的功能,在本地表字段进行大规模修改时,只需要删除全部表中的内容,并重新创建即可,只要不影响服务端的数据就不是太大的问题。 + +iOS 中的 Model 层不应该是一个单纯的数据结构,它应该起到发出 HTTP 请求、进行字段验证以及持久存储的职责,同时为上层提供网络请求的方法以及字段作为接口,为视图的展示提供数据源的作用。我们应该将更多的与 Model 层有关的业务逻辑移到 Model 中以控制 Controller 的复杂性。 + diff --git a/contents/architecture/mvx-view.md b/contents/architecture/mvx-view.md new file mode 100644 index 0000000..a655288 --- /dev/null +++ b/contents/architecture/mvx-view.md @@ -0,0 +1,447 @@ +# 谈谈 MVX 中的 View + ++ [谈谈 MVX 中的 Model](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/architecture/mvx-model.md) ++ [谈谈 MVX 中的 View](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/architecture/mvx-view.md) ++ [谈谈 MVX 中的 Controller](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/architecture/mvx-controller.md) ++ [浅谈 MVC、MVP 和 MVVM 架构模式](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/architecture/mvx.md) + +> Follow GitHub: [Draveness](https://github.com/Draveness) + +这是谈谈 MVX 系列的第二篇文章,上一篇文章中对 iOS 中 Model 层的设计进行了简要的分析;而在这里,我们会对 MVC 中的视图层进行讨论,谈一谈现有的视图层有着什么样的问题,如何在框架的层面上去改进,同时与服务端的视图层进行对比,分析它们的差异。 + +## UIKit + +UIKit 是 Cocoa Touch 中用于构建和管理应用的用户界面的框架,其中几乎包含着与 UI 相关的全部功能,而我们今天想要介绍的其实是 UIKit 中与视图相关的一部分,也就是 `UIView` 以及相关类。 + +`UIView` 可以说是 iOS 中用于渲染和展示内容的最小单元,作为开发者能够接触到的大多数属性和方法也都由 `UIView` 所提供,比如最基本的布局方式 frame 就是通过 `UIView` 的属性所控制,在 Cocoa Touch 中的所有布局系统最终都会转化为 CFRect 并通过 frame 的方式完成最终的布局。 + +![Frame-And-Components](images/view/Frame-And-Components.jpg) + +`UIView` 作为 UIKit 中极为重要的类,它的 API 以及设计理念决定了整个 iOS 的视图层该如何工作,这也是理解视图层之前必须要先理解 `UIView` 的原因。 + +### UIView + +在 UIKit 中,除了极少数用于展示的类不继承自 `UIView` 之外,几乎所有类的父类或者或者祖先链中一定会存在 `UIView`。 + +![UIView-And-Subclasses](images/view/UIView-And-Subclasses.jpg) + +我们暂且抛开不继承自 `UIView` 的 `UIBarItem` 类簇不提,先通过一段代码分析一下 `UIView` 具有哪些特性。 + +```objectivec +UIImageView *backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"backgoundImage"]]; +UIImageView *logoView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"logo"]]; + +UIButton *loginButton = [[UIButton alloc] init]; +[loginButton setTitle:@"登录" forState:UIControlStateNormal]; +[loginButton setTitleColor:UIColorFromRGB(0xFFFFFF) forState:UIControlStateNormal]; +[loginButton.titleLabel setFont:[UIFont boldSystemFontOfSize:18]]; +[loginButton setBackgroundColor:UIColorFromRGB(0x00C3F3)]; + +[self.view addSubview:backgroundView]; +[backgroundView addSubview:logoView]; +[backgroundView addSubview:loginButton]; +``` + +`UIView` 作为视图层大部分元素的根类,提供了两个非常重要的特性: + ++ 由于 `UIView` 具有 `frame` 属性,所以为所有继承自 `UIView` 的类提供了绝对布局相关的功能,也就是在 Cocoa Touch 中,所有的视图元素都可以通过 `frame` 设置自己在父视图中的绝对布局; ++ `UIView` 在接口中提供了操作和管理视图层级的属性和方法,比如 `superview`、`subviews` 以及 `-addSubview:` 等方法; + + ```objectivec + @interface UIView (UIViewHierarchy) + + @property (nullable, nonatomic, readonly) UIView *superview; + @property (nonatomic, readonly, copy) NSArray<__kindof UIView *> *subviews; + + - (void)addSubview:(UIView *)view; + + ... + + @end + ``` + + 也就是说 **UIView 和它所有的子类都可以拥有子视图,成为容器并包含其他 UIView 的实例**。 + + ```objectivec + [self.view addSubview:backgroundView]; + [backgroundView addSubview:logoView]; + [backgroundView addSubview:loginButton]; + ``` + +这种使用 `UIView` 同时为子类提供默认的 `frame` 布局以及子视图支持的方式在一定程度上能够降低视图模型的复杂度:因为所有的视图都是一个容器,所以在开发时不需要区分视图和容器,但是这种方式虽然带来了一些方便,但是也不可避免地带来了一些问题。 + +### UIView 与布局 + +在早期的 Cocoa Touch 中,整个视图层的布局都只是通过 `frame` 属性来完成的(绝对布局),一方面是因为在 iPhone5 之前,iOS 应用需要适配的屏幕尺寸非常单一,完全没有适配的兼容问题,所以使用单一的 `frame` 布局方式完全是可行的。 + +但是在目前各种屏幕尺寸的种类暴增的情况下,就很难使用 `frame` 对所有的屏幕进行适配,在这时苹果就引入了 Auto Layout 采用相对距离为视图层的元素进行布局。 + +![AutoLayout](images/view/AutoLayout.jpg) + +不过,这算是苹果比较失败的一次性尝试,主要是因为使用 Auto Layout 对视图进行布局实在太过复杂,所以刚出来的时候也不温不火,很少有人使用,直到 Masonry 的出现使得编写 Auto Layout 代码没有那么麻烦和痛苦才普及起来。 + +但是由于 Auto Layout 的工作原理实际上是解 N 元一次方程组,所以在遇到复杂视图时,会遇到非常严重的性能问题,如果想要了解相关的问题的话,可以阅读 [从 Auto Layout 的布局算法谈性能](http://draveness.me/layout-performance.html) 这篇文章,在这里就不再赘述了。 + +然而 Auto Layout 的相对布局虽然能够在*一定程度上*解决适配**屏幕大小和尺寸接近的**适配问题,比如 iPhone4s、iPhone5、iPhone6 Plus 等移动设备,或者iPad 等平板设备。但是,Auto Layout 不能通过一套代码打通 iPhone 和 iPad 之间布局方式的差异,只能通过代码中的 if 和 else 进行判断。 + +在这种背景下,苹果做了很多的尝试,比如说 [Size-Class-Specific Layout](https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/Size-ClassSpecificLayout.html),Size Class 将屏幕的长宽分为三种: + ++ Compact ++ Regular ++ Any + +这样就出现了最多 3 x 3 的组合,比如屏幕宽度为 Compact 高度为 Regular 等等,它与 Auto Layout 一起工作省去了一些 if 和 else 的条件判断,但是从实际效果上来说,它的用处并不是特别大,而且使用代码来做 Size Class 的相关工作依然非常困难。 + +除了 Auto Layout 和 Size Class 之外,苹果在 iOS9 还推出了 `UIStackView` 来增加 iOS 中的布局方式和手段,这是一种类似 flexbox 的布局方式。 + +虽然 `UIStackView` 可以起到一定的作用,但是由于大多数 iOS 应用都要求对设计稿进行严格还原并且其 API 设计相对啰嗦,开发者同时也习惯了使用 Auto Layout 的开发方式,在惯性的驱动下,`UIStackView` 应用的也不是非常广泛。 + +![UIStackVie](images/view/UIStackView.jpg) + +不过现在很多跨平台的框架都是用类似 `UIStackView` 的方式进行布局,比如 React Native、Weex 等,其内部都使用 Facebook 开源的 Yoga。 + +> 由于 flexbox 以及类似的布局方式在其他平台上都有类似的实现,并且其应用确实非常广泛,笔者认为随着工具的完善,这种布局方式会逐渐进入 iOS 开发者的工具箱中。 + +三种布局方式 `frame`、Auto Layout 以及 `UIStackView` 其实最终布局都会使用 `frame`,其他两种方式 Auto Layout 和 `UIStackView` 都会将代码*描述*的布局转换成 `frame` 进行。 + +#### 布局机制的混用 + +Auto Layout 和 `UIStackView` 的出现虽然为布局提供了一些方便,但是也增加了布局系统的复杂性。 + +因为在 iOS 中几乎所有的视图都继承自 `UIView`,这样也同时继承了 `frame` 属性,在使用 Auto Layout 和 `UIStackView` 时,并没有禁用 `frame` 布局,所以在混用却没有掌握技巧时可能会有一些比较奇怪的问题。 + +其实,在混用 Auto Layout 和 `frame` 时遇到的大部分奇怪的问题都是因为 [translatesAutoresizingMaskIntoConstraints](https://developer.apple.com/reference/uikit/uiview/1622572-translatesautoresizingmaskintoco) 属性没有被正确设置的原因。 + +> If this property’s value is true, the system creates a set of constraints that duplicate the behavior specified by the view’s autoresizing mask. This also lets you modify the view’s size and location using the view’s frame, bounds, or center properties, allowing you to create a static, frame-based layout within Auto Layout. + +在这里就不详细解释该属性的作用和使用方法了。 + +#### 对动画的影响 + +在 Auto Layout 出现之前,由于一切布局都是使用 `frame` 工作的,所以在 iOS 中完成对动画的编写十分容易。 + +```objectivec +UIView.animate(withDuration: 1.0) { + view.frame = CGRect(x: 10, y: 10, width: 200, height: 200) +} +``` + +而当大部分的 iOS 应用都转而使用 Auto Layout 之后,对于视图大小、位置有关的动画就比较麻烦了: + +```objectivec +topConstraint.constant = 10 +leftConstraint.constant = 10 +heightConstraint.constant = 200 +widthConstraint.constant = 200 +UIView.animate(withDuration: 1.0) { + view.layoutIfNeeded() +} +``` + +我们需要对视图上的约束对象一一修改并在最后调用 `layoutIfNeeded` 方法才可以完成相同的动画。由于 Auto Layout 对动画的支持并不是特别的优秀,所以在很多时候笔者在使用 Auto Layout 的视图上,都会使用 `transform` 属性来改变视图的位置,这样虽然也没有那么的优雅,不过也是一个比较方便的解决方案。 + +![lottie](images/view/lottie.jpg) + +### frame 的问题 + +每一个 `UIView` 的 `frame` 属性其实都是一个 `CGRect` 结构体,这个结构体展开之后有四个组成部分: + ++ origin + + x + + y ++ size + + width + + height + +当我们设置一个 `UIView` 对象的 `frame` 属性时,其实是同时设置了它在父视图中的位置和它的大小,从这里可以获得一条比较重要的信息: + +> iOS 中所有的 `UIView` 对象都是使用 `frame` 布局的,否则 `frame` 中的 `origin` 部分就失去了意义。 + +但是如果为 `UIStackView` 中的视图设置 `frame` 的话,这个属性就完全没什么作用了,比如下面的代码: + +```objectivec +UIStackView *stackView = [[UIStackView alloc] init]; +stackView.frame = self.view.frame; +[self.view addSubview:stackView]; + +UIView *greenView = [[UIView alloc] init]; +greenView.backgroundColor = [UIColor greenColor]; +greenView.frame = CGRectMake(0, 0, 100, 100); +[stackView addArrangedSubview:greenView]; + +UIView *redView = [[UIView alloc] init]; +redView.backgroundColor = [UIColor redColor]; +redView.frame = CGRectMake(0, 0, 100, 100); +[stackView addArrangedSubview:redView]; +``` + +`frame` 属性在 `UIStackView` 上基本上就完全失效了,我们还需要使用约束来控制 `UIStackView` 中视图的大小,不过如果你要使用 `frame` 属性来查看视图在父视图的位置和大小,在恰当的时机下是可行的。 + +#### 谈谈 origin + +但是 `frame` 的不正确使用会导致视图之间的耦合,如果内部视图设置了自己在父视图中的 `origin`,但是父视图其实并不会使用直接 `frame` 布局该怎么办?比如,父视图是一个 `UIStackView`,它就会重写子视图的 `origin` 甚至是没有正确设置的 `size` 属性。 + +最重要的是 `UIView` 上 `frame` 的设计导致了视图之间可能会有较强的耦合,因为**子视图不应该知道自己在父视图中的位置**,它应该只关心自己的大小。 + +也就是作为一个简单的 `UIView` 它应该只能设置自己的 `size` 而不是 `origin`,因为父视图可能是一个 `UIStackView` 也可能是一个 `UITableView` 甚至是一个扇形的视图也不是不可能,所以**位置这一信息并不是子视图应该关心的**。 + +如果视图设置了自己的 `origin` 其实也就默认了自己的父视图一定是使用 `frame` 进行布局的,而一旦依赖于外部的信息,它就很难进行复用了。 + +#### 再谈 size + +关于视图大小的确认,其实也是有一些问题的,因为视图在布局时确实可能依赖于父视图的大小,或者更确切的说是需要父视图提供一个可供布局的大小,然后让子视图通过这个 `CGSize` 返回一个自己需要的大小给父视图。 + +![texture](images/view/texture.png) + +这种计算视图大小的方式,其实比较像 [Texture](https://github.com/TextureGroup/Texture) 也就是原来的 AsyncDisplayKit 中对于布局系统的实现。 + +父视图通过调用子视图的 `-layoutSpecThatFits:` 方法获取子视图布局所需要的大小,而子视图通过父视图传入的 `CGSizeRange` 来设置自己的大小。 + +```objectivec +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize + ... +} +``` + +通过这种方式,子视图对父视图一无所知,它不知道父视图的任何属性,只通过 `-layoutSpecThatFits:` 方法传入的参数进行布局,实现了解耦以及代码复用。 + +### 小结 + +由于确实需要对多尺寸的屏幕进行适配,苹果推出 Auto Layout 和 `UIStackView` 的初衷也没有错,但是在笔者看来,因为绝大部分视图都继承自 `UIView`,所以在很多情况下并没有对开发者进行强限制,比如在使用 `UIStackView` 时只能使用 flexbox 式的布局,在使用 Auto Layout 时也只能使用约束对视图进行布局等等,所以在很多时候会带来一些不必要的问题。 + +同时 `UIView` 中的 `frame` 属性虽然在一开始能够很好的解决的布局的问题,但是随着布局系统变得越来越复杂,使得很多 UI 组件在与非 `frame` 布局的容器同时使用时产生了冲突,最终破坏了良好的封装性。 + +到目前为止 iOS 中的视图层的问题主要就是 `UIView` 作为视图层中的上帝类,提供的 `frame` 布局系统不能良好的和其他布局系统工作,在一些时候 `frame` 属性完全成为了摆设。 + +## 其他平台对视图层的设计 + +在接下来的文章中,我们会介绍和分析其他平台 Android、Web 前端以及后端是如何对视图层进行设计的。 + +### Android 与 View + +与 iOS 上使用命令式的风格生成界面不同,Android 使用声明式的 XML 对界面进行描述,在这里举一个最简单的例子: + +```xml +<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context="com.example.draveness.myapplication.DisplayMessageActivity"> + + <TextView + android:id="@+id/textView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="TextView" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + +</android.support.constraint.ConstraintLayout> +``` + +> 整个 XML 文件同时描述了视图的结构和样式,而这也是 Android 对于视图层的设计方式,将结构和样式混合在一个文件中。 + +我们首先来分析一下上述代码的结构,整个 XML 文件中只有两个元素,如果我们去掉其中所有的属性,整个界面的元素就是这样的: + +```xml +<ConstraintLayout> + <TextView/> +</ConstraintLayout> +``` + +由一个 `ConstraintLayout` 节点包含一个 `TextView` 节点。 + +#### View 和 ViewGroup + +我们再来看一个 Android 中稍微复杂的视图结构: + +```xml +<LinearLayout> + <RelativeLayout> + <ImageView/> + <LinearLayout> + <TextView/> + <TextView/> + </LinearLayout> + </RelativeLayout> + <View/> +</LinearLayout> +``` + +上面的 XML 代码描述了一个更加复杂的视图树,这里通过一张图更清晰地展示该视图表示的结构: + +![Android-View-Tree](images/view/Android-View-Tree.jpg) + +我们可以发现,Android 的视图其实分为两类: + ++ 一类是不能有子节点的视图,比如 `View`、`ImageView` 和 `TextView` 等; ++ 另一类是可以有子节点的视图,比如 `LinearLayout` 和 `RelativeLayout` 等; + +在 Android 中,这两类的前者都是 `View` 的子类,也就是视图;后者是 `ViewGroup` 的子类,它主要充当视图的容器,与它的子节点以树形的结构形成了一个层次结构。 + +这种分离视图和容器的方式很好的分离了职责,将管理和控制子视图的功能划分给了 `ViewGroup`,将显示内容的职责抛给了 `View` 对各个功能进行了合理的拆分。 + +子视图的布局属性只有在父视图为特定 `ViewGroup` 时才会激活,否则就会忽略在 XML 中声明的属性。 + +#### 混合的结构与样式 + +在使用 XML 或者类 XML 的这种文本来描述视图层的内容时,总会遇到一种无法避免的争论:样式到底应该放在哪里?上面的例子显然说明了 Android 对于这一问题的选择,也就是将样式放在 XML 结构中。 + +这一章节中并不会讨论样式到底应该放在哪里这一问题,我们会在后面的章节中具体讨论,将样式放在 XML 结构中和单独使用各自的优缺点。 + +### Web 前端 + +随着 Web 前端应用变得越来越复杂,在目前的大多数 Web 前端项目的实践中,我们已经会使用前后端分离方式开发 Web 应用,而 Web 前端也同时包含 Model、View 以及 Controller 三部分,不再通过服务端直接生成前端的 HTML 代码了。 + +![html-css](images/view/html-css.jpg) + +现在最流行的 Web 前端框架有三个,分别是 React、Vue 和 Angular。不过,这篇文章会以最根本的 HTML 和 CSS 为例,简单介绍 Web 前端中的视图层是如何工作的。 + +```html +<div> + <h1 class="text-center">Header</h1> +</div> + +.text-center { + text-align: center; +} +``` + +在 HTML 中其实并没有视图和容器这种概念的划分,绝大多数的元素节点都可以包含子节点,只有少数的无内容标签,比如说 `br`、`hr`、`img`、`input`、`link` 以及 `meta` 才不会**解析**自己的子节点。 + +#### 分离的结构与样式 + +与 Android 在定义视图时,使用混合的结构与样式不同,Web 前端在视图层中,采用 HTML 与 CSS 分离,即结构与样式分离的方式进行设计;虽然在 HTML 中,我们也可以使用 `style` 将 CSS 代码写在视图层的结构中,不过在一般情况下,我们并不会这么做。 + +```html +<body style="background-color:powderblue;"> +</body> +``` + +### 结构与样式 + +在这一章节中,我们会对结构与样式组织方式之间的优劣进行简单的讨论。 + +Android 和 Web 前端使用不同的方式对视图层的结构和样式进行组织,前者使用混合的方式,后者使用分离的结构和样式。 + +相比于分离的组织方式,混合的组织方式有以下的几个优点: + ++ 不需要实现元素选择器,降低视图层解析器实现的复杂性; ++ 元素的样式是内联的,对于元素的样式的定义一目了然,不需要考虑样式的继承等复杂特性; + +分离的组织方式却正相反: + ++ 元素选择器的实现,增加了 CSS 样式代码的复用性,不需要多次定义相同的样式; ++ 将 CSS 代码从结构中抽离能够增强 HTML 的可读性,可以非常清晰、直观的了解 HTML 的层级结构; + +对于结构与样式,不同的组织方式能够带来不同的收益,这也是在设计视图层时需要考虑的事情,我们没有办法在使用一种组织方式时获得两种方式的优点,只能尽可能权衡利弊,选择最合适的方法。 + +### 后端的视图层 + +这一章节将会研究一下后端视图层的设计,不过在真正开始分析其视图层设计之前,我们需要考虑一个问题,后端的视图层到底是什么?它有客户端或者 Web 前端中的**用于展示内容**视图层么? + +这其实是一个比较难以回答的问题,不过严格意义上的后端是没有用于展示内容的视图层的,也就是为客户端提供 API 接口的后端,它们的视图层,其实就是用于返回 JSON 的模板。 + +```ruby +json.extract! user, :id, :mobile, :nickname, :gender, :created_at, :updated_at +json.url user_url user, format: :json +``` + +在 Ruby on Rails 中一般都是类似于上面的 jbuilder 代码。拥有视图层的后端应用大多都是使用了模板引擎技术,直接为 HTTP 请求返回渲染之后的 HTML 和 CSS 等前端代码。 + +总而言是,使用了模板引擎的后端应用其实是混合了 Web 前端和后端,整个服务的视图层其实就是 Web 前端的代码;而现在的大多数 Web 应用,由于遵循了前后端分离的设计,两者之间的通信都使用约定好的 API 接口,所以后端的视图层其实就是单纯的用于渲染 JSON 的代码,比如 Rails 中的 jbuilder。 + +## 理想中的视图层 + +iOS 中理想的视图层需要解决两个最关键的问题: + +1. 细分 `UIView` 的职责,将其分为视图和容器两类,前者负责展示内容,后者负责对子视图进行布局; +2. 去除整个视图层对于 `frame` 属性的依赖,不对外提供 `frame` 接口,每个视图只能知道自己的大小; + +解决上述两个问题的办法就是封装原有的 `UIView` 类,使用组合模式为外界提供合适的接口。 + +![Node-Delegate-UIVie](images/view/Node-Delegate-UIView.jpg) + +### 细分 UIView 的职责 + +`Node` 会作为 `UIView` 的代理,同时也作为整个视图层新的根类,它将屏蔽掉外界与 `UIView` 层级操作的有关方法,比如说:`-addSubview:` 等,同时,它也会屏蔽掉 `frame` 属性,这样每一个 `Node` 类的实例就只能设置自己的大小了。 + +```swift +public class Node: Buildable { + public typealias Element = Node + public let view: UIView = UIView() + + @discardableResult + public func size(_ size: CGSize) -> Element { + view.size = size + return self + } +} +``` + +上面的代码简单说明了这一设计的实现原理,我们可以理解为 `Node` 作为 `UIView` 的透明代理,它不提供任何与视图层级相关的方法以及 `frame` 属性。 + +![Node-Delegate-Filte](images/view/Node-Delegate-Filter.jpg) + +### 容器的实现 + +除了添加一个用于展示内容的 `Node` 类,我们还需要一个 `Container` 的概念,提供为管理子视图的 API 和方法,在这里,我们添加了一个空的 `Container` 协议: + +```swift +public protocol Container { } +``` + +利用这个协议,我们构建一个 iOS 中最简单的容器 `AbsoluteContainer`,内部使用 `frame` 对子视图进行布局,它应该为外界提供添加子视图的接口,在这里就是 `build(closure:)` 方法: + +```swift +public class AbsoluteContainer: Node, Container { + typealias Element = AbsoluteContainer + @discardableResult + public func build(closure: () -> Node) -> Relation<AbsoluteContainer> { + let node = closure() + view.addSubview(node.view) + return Relation<AbsoluteContainer>(container: self, node: node) + } +} +``` + +该方法会在调用后返回一个 `Relation` 对象,这主要是因为在这种设计下的 `origin` 或者 `center` 等属性不再是 `Node` 的一个接口,它应该是 `Node` 节点出现在 `AbsoluteContainer` 时的产物,也就是说,只有在这两者同时出现时,才可以使用这些属性更新 `Node` 节点的位置: + +```swift +public class Relation<Container> { + public let container: Container + public let node: Node + + public init(container: Container, node: Node) { + self.container = container + self.node = node + } +} + +public extension Relation where Container == AbsoluteContainer { + @discardableResult + public func origin(_ origin: CGPoint) -> Relation { + node.view.origin = origin + return self + } +} +``` + +这样就完成了对于 `UIView` 中视图层级和位置功能的剥离,同时使用透明代理以及 `Relation` 为 `Node` 提供其他用于设置视图位置的接口。 + +> 这一章节中的代码都来自于 [Mineral](https://github.com/Draveness/Mineral),如果对代码有兴趣的读者,可以下载自行查看。 + +## 总结 + +Cocoa Touch 中的 UIKit 对视图层的设计在一开始确实是没有问题的,主要原因是在 iOS 早期的布局方式并不复杂,只有单一的 `frame` 布局,而这种方式也恰好能够满足整个平台对于 iOS 应用开发的需要,但是随着屏幕尺寸的增多,苹果逐渐引入的其它布局方式与原有的体系发生了一些冲突,导致在开发时可能遇到奇怪的问题,而这也是本文想要解决的,将原有属于 `UIView` 的职责抽离出来,提供更合理的抽象。 + +## References + ++ [从 Auto Layout 的布局算法谈性能](http://draveness.me/layout-performance.html) ++ [Understanding Auto Layout](https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/index.html#//apple_ref/doc/uid/TP40010853-CH7-SW1) ++ [Size-Class-Specific Layout](https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/Size-ClassSpecificLayout.html) ++ [translatesAutoresizingMaskIntoConstraints](https://developer.apple.com/reference/uikit/uiview/1622572-translatesautoresizingmaskintoco) + diff --git a/contents/architecture/mvx.md b/contents/architecture/mvx.md new file mode 100644 index 0000000..3f370ac --- /dev/null +++ b/contents/architecture/mvx.md @@ -0,0 +1,393 @@ +# 浅谈 MVC、MVP 和 MVVM 架构模式 + ++ [谈谈 MVX 中的 Model](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/architecture/mvx-model.md) ++ [谈谈 MVX 中的 View](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/architecture/mvx-view.md) ++ [谈谈 MVX 中的 Controller](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/architecture/mvx-controller.md) ++ [浅谈 MVC、MVP 和 MVVM 架构模式](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/architecture/mvx.md) + +> Follow GitHub: [Draveness](https://github.com/Draveness) + +这是 MVX 系列的第四篇文章,在前面的文章中,我们先后介绍了 MVC 架构模式中的 Model、View 和 Controller 的现状,对比了其他平台中的设计,最后给出了作者理想中的结构。 + +而在这一篇文章中,作者会依次介绍 MVC、MVP 以及 MVVM 架构模式以及不同平台对它们的使用;虽然参考了诸多资料,不过文中观点难免掺入作者的主观意见,作者也希望文中的错误与不足之处能被各位读者指出。 + +![MVC-MVVM-MVP](images/mvx/MVC-MVVM-MVP.jpg) + +前面的几篇文章中重点都是介绍 iOS 平台上的 Model、View 和 Controller 如何设计,而这篇文章会对目前 GUI 应用中的 MVC、MVP 和 MVVM 架构模式进行详细地介绍。 + +## MVC + +在整个 GUI 编程领域,MVC 已经拥有将近 50 年的历史了。早在几十年前,Smalltalk-76 就对 MVC 架构模式进行了实现,在随后的几十年历史中,MVC 产生了很多的变种,例如:HMVC、MVA、MVP、MVVM 和其它将 MVC 运用于其它不同领域的模式。 + +### 早期的 MVC + +而本文的内容就是从 MVC 开始的,作为最出名并且应用最广泛的架构模式,MVC 并没有一个**明确的**定义,网上流传的 MVC 架构图也是形态各异,作者查阅了很多资料也没有办法确定到底什么样的架构图才是**标准的** MVC 实现。 + +![MVC-1979](images/mvx/MVC-1979.jpg) + +设计 MVC 的重要目的就是在人的心智模型与计算机的模型之间建立一个桥梁,而 MVC 能够解决这一问题并**为用户提供直接看到信息和操作信息的功能**。 + +> 更早的概念类似 Model-View-Editor(MVE)这里就不再提及了,感兴趣的读者可以阅读这篇论文 [Thing-Model-View-Editor](http://heim.ifi.uio.no/~trygver/1979/mvc-1/1979-05-MVC.pdf) 了解更多的信息。 + +### 混乱的 MVC 架构 + +作者相信,稍有编程经验的开发者就对 MVC 有所了解,至少也是听过 MVC 的名字。作者也一直都认为绝大多数人对于 MVC 理解的概念都一样,很多人对于 MVVM 的实现有很大争论,说遵循什么什么架构的是 MVVM,MVVM 有什么组件、没有什么组件,而对于 MVC 仿佛没有那么大的疑问,这其实却不然。 + +#### ASP.NET MVC + +在最近的几个月,作者发现不同人对于 MVC 的理解有巨大的差异,这是 [ASP.NET MVC Overview](https://msdn.microsoft.com/en-us/library/dd381412(v=vs.108).aspx) 一文中对于 MVC 模式描述的示意图。 + +![MVC-with-ASP.NET](images/mvx/MVC-with-ASP.NET.jpg) + +图片中并没有对 Model、View 和 Controller 三者之间如何交互进行说明,有的也只是几个箭头。我们应该可以这么简单地理解: + +1. 控制器负责管理视图和模型; +2. 视图负责展示模型中的内容; + +> 由于文章没有明确对这三个箭头的含义进行解释,所以在这里也仅作推断,无法确认原作者的意思。 + +#### Spring MVC + +与 ASP.NET 不同,Spring MVC 对于 MVC 架构模式的实现就更加复杂了,增加了一个用于分发请求、管理视图的 DispatchServlet: + +![MVC-with-Spring](images/mvx/MVC-with-Spring.jpg) + +在这里不再介绍 Spring MVC 对于 HTTP 请求的处理流程,我们对其中 Model、View 和 Controller 之间的关系进行简单的分析: + +1. 通过 DispatchServlet 将控制器层和视图层完全解耦; +2. 视图层和模型层之间没有直接关系,只有间接关系,通过控制器对模型进行查询、返回给 DispatchServlet 后再传递至视图层; + +虽然 Spring MVC 也声称自己遵循 MVC 架构模式,但是这里的 MVC 架构模式和 ASP.NET 中却有很大的不同。 + +#### iOS MVC + +iOS 客户端中的 Cocoa Touch 自古以来就遵循 MVC 架构模式,不过 Cocoa Touch 中的 MVC 与 ASP.NET 和 Spring 中的 MVC 截然不同。 + +![MVC-with-iOS](images/mvx/MVC-with-iOS.jpg) + +在 iOS 中,由于 `UIViewController` 类持有一个根视图 `UIView`,所以视图层与控制器层是紧密耦合在一起的,这也是 iOS 项目经常遇到视图控制器非常臃肿的重要原因之一。 + +#### Rails MVC + +Rails 作为著名的 MVC 框架,视图层和模型层没有直接的耦合,而是通过控制器作为中间人对信息进行传递: + +![MVC-with-Rails](images/mvx/MVC-with-Rails.jpg) + +这种 MVC 的设计分离了视图层和模型层之间的耦合,作为承担数据存储功能的模型层,可以通过控制器同时为多种不同的视图提供数据: + +![MVC-in-Rails-with-different-view](images/mvx/MVC-%05in-Rails-with-different-view.jpg) + +控制器根据用户发出的 HTTP 请求,从模型中取出相同的数据,然后传给不同的视图以渲染出不同的结果。Rails 中的 MVC 架构模式能够很好地将用于展示的视图和用于存储数据的数据库进行分离,两者之间通过控制器解耦,能够实现同一数据库对应多种视图的架构。 + +#### 维基百科中的 MVC + +除了上述框架中的 MVC 架构模式,还有一些其它的书籍或者资料对于 MVC 也有着不同的解释,比如维基百科的 [Model-view-controller](https://en.wikipedia.org/wiki/Model–view–controller) 条目,该条目是我们在 Google 搜索 [MVC](https://www.google.com/search?q=MVC) 时能够出现的前几个条目,这也是维基百科中的架构图能够出现在这篇文章中的原因 —— 有着广泛的受众。 + +![MVC-in-Wikipedia](images/mvx/MVC-in-Wikipedia.jpg) + +维基百科中对于 MVC 架构模式交互图的描述其实相比上面的图片还都是比较清晰的,这主要是因为它对架构图中的箭头进行了详细的说明,指出了这个关系到底表示什么。 + +1. 视图被用户看到; +2. 用户使用控制器; +3. 控制器操作模型; +4. 模型更新视图; + +虽然说整个架构图的逻辑是可以说的通的,不过相比于前面的架构图总是感觉有一些奇怪,而在这幅图片中,视图和控制器之间是毫无关系的,这与前面见到的所有 MVC 架构模式都完全不同,作者也不清楚这幅图来源是什么、为什么这么画,放在这里也仅作参考。 + +### 『标准』的 MVC + +到底什么才是标准的 MVC 这个问题,到现在作者也没有一个**确切的**答案;不过多个框架以及书籍对 MVC 的理解有一点是完全相同的,也就是它们都将整个应用分成 Model、View 和 Controller 三个部分,而这些组成部分其实也有着几乎相同的职责。 + ++ 视图:管理作为位图展示到屏幕上的图形和文字输出; ++ 控制器:翻译用户的输入并依照用户的输入操作模型和视图; ++ 模型:管理应用的行为和数据,响应数据请求(经常来自视图)和更新状态的指令(经常来自控制器); + +> 上述内容出自 [Applications Programming in Smalltalk-80: How to use Model-View-Controller (MVC)](http://www.dgp.toronto.edu/~dwigdor/teaching/csc2524/2012_F/papers/mvc.pdf) 一文。 + +作者所理解的真正 MVC 架构模式其实与 ASP.NET 中对于 MVC 的设计完全相同: + +![Standard-MV](images/mvx/Standard-MVC.jpg) + +控制器负责对模型中的数据进行更新,而视图向模型中请求数据;当有用户的行为触发操作时,会有控制器更新模型,并通知视图进行更新,在这时视图向模型请求新的数据,而这就是**作者所理解的**标准 MVC 模式下,Model、View 和 Controller 之间的协作方式。 + +#### 依赖关系 + +虽然我们对 MVC 中的各个模块的交互不是特别了解,但是三者之间的依赖关系却是非常明确的;在 MVC 中,模型层可以单独工作,而视图层和控制器层都依赖与模型层中的数据。 + +![Essential-Dependencies-in-MVC](images/mvx/Essential-Dependencies-in-MVC.jpg) + +> 虽然如上图所示,视图和控制器之间没有相互依赖,不过因为视图和控制器之间的依赖并不常用,所以图中将视图和控制器之间的依赖省略了。 + +#### 分离展示层 + +在 Martin Fowler 对于 Model-View-Controller 的描述中,MVC 最重要的概念就是分离展示层 [Separated Presentation](https://www.martinfowler.com/eaaDev/SeparatedPresentation.html),如何在领域对象(Domain Object)和我们在屏幕上看到的 GUI 元素进行划分是 MVC 架构模式中最核心的问题。 + +GUI 应用程序由于其需要展示内容的特点,分为两个部分:一部分是用于展示内容的展示层(Presentation Layer),另一部分包含领域和数据逻辑的领域层(Domain Layer)。 + +![Presentation-Domain](images/mvx/Presentation-Domain.jpg) + +展示层依赖于领域层中存储的数据,而领域层对于展示层一无所知,领域层其实也是 MVC 模式中的模型层,而展示层可以理解为 VC 部分。 + +MVC 最重要的目的并不是规定各个模块应该如何交互和联系,而是将原有的混乱的应用程序划分出合理的层级,把一团混乱的代码,按照展示层和领域层分成两个部分;在这时,领域层中的领域对象由于其自身特点不需要对展示层有任何了解,可以同时为不同的展示层工作。 + +#### 观察者同步 + +除了分离展示层,MVC 还与观察者同步 [Observer Synchronization](https://www.martinfowler.com/eaaDev/MediatedSynchronization.html) 关系紧密。因为在 MVC 模式中,模型可以单独工作,同时它对使用模型中数据的视图和控制器一无所知,为了保持模型的独立性,我们需要一种机制,当模型发生改变时,能够同时更新多个视图和控制器的内容;在这时,就需要以观察者同步的方式解决这个问题。 + +![Observer-Synchronization](images/mvx/Observer-Synchronization.jpg) + +我们将所有需要实时更新的组件注册成为模型的观察者,在模型的属性发生变化时,通过观察者模式推送给所有注册的观察者(视图和控制器)。 + +当多个视图共享相同的数据时,观察者同步是一个非常关键的模式,它能够在对这些视图不知情的前提下,同时通知多个视图;通过观察者模式,我们可以非常容易地创建一个依赖于同一模型的视图。 + +观察者同步或者说观察者模式的主要缺点就是:由于事件触发的隐式行为可能导致很难查找问题的来源并影响其解决,不过虽然它有着这样的缺点,但是观察者同步这一机制仍然成为 MVC 以及其衍生架构模式中非常重要的一部分。 + +#### 占主导地位的控制器 + +MVC 架构模式的三个组成部分:Model、View 和 Controller 中最重要的就是控制器,它承担了整个架构中的大部分业务逻辑,同时在用户请求到达或者事件发生时都会首先通知控制器并由它来决定如何响应这次请求或者事件。 + +![Main-Controlle](images/mvx/Main-Controller.jpg) + +在 MVC 中,所有的用户请求都会首先交给控制器,再由控制器来决定如何响应用户的输入,无论是更新模型中的信息还是渲染相应的视图,都是通过控制器来决定的;也就是说,在 MVC 中,控制器占据主导地位,它决定用户的输入是如何被处理的。 + +#### 被动的模型 + +在绝大多数的 MVC 架构模式中,模型都不会主动向视图或者控制器推送消息;模型都是被动的,它只存储整个应用中的数据,而信息的获取和更新都是由控制器来驱动的。 + +![Passive-Mode](images/mvx/Passive-Model.jpg) + +但是当模型中的数据发生变化时,却需要通过一些方式通知对应的视图进行更新,在这种情况下其实也不需要模型**主动**将数据变化的消息推送给视图;因为所有对于模型层的改变都是**由用户的操作导致的**,而用户的操作都是通过控制器来处理的,所以只需要在控制器改变模型时,将更新的信息发送给视图就可以了;当然,我们也可以通过**观察者模式**向未知的观察者发送通知,以保证状态在不同模块之间能够保持同步。 + +作为被动的模型层,它对于视图和控制器的存在并不知情,只是向外部提供接口并响应视图和控制器对于数据的请求和更新操作。 + +#### MVC + MVC + +目前的大多数应用程序都非常复杂并且同时包含客户端和服务端,两者分开部署但同时又都遵循 MVC 或者衍生的架构模式;过去的 Web 应用由于并不复杂,前端和服务端往往都部署在同一台服务器上,比如说使用 erb 模板引擎的 Rails 或者使用 jsp 的 Java 等等;这时的 Web 应用都遵循 MVC 架构模式: + +![MVC-Web-App](images/mvx/MVC-Web-App.jpg) + +> 上图的 MVC 架构模式的通信方式与标准的 MVC 中不同,上图以 Rails 为例展示其中的 MVC 是如何工作的,其中的 HTML、CSS 和 Javascript 代码就是视图层,控制器负责视图的渲染并且操作模型,模型中包含部分业务逻辑并负责管理数据库。 + +过去的 Web 应用的非常简单,而现在的应用程序都非常复杂,而整个应用程序无论是 Web 还是客户端其实都包含两个部分,也就是前端/客户端和后端;先抛开后端不谈,无论是 Web 前端、iOS 还是 Android 都遵循 MVC 架构模式或者它的变种。 + +![MVC-App-Arch](images/mvx/MVC-App-Arch.jpg) + +在实际情况下,单独的 iOS、Android 和 Web 应用往往不能单独工作,这些客户端应用需要与服务端一起工作;当前端/客户端与后端一同工作时,其实分别『部署』了两个不同的应用,这两个应用都遵循 MVC 架构模式: + +![MVC-MV](images/mvx/MVC-MVC.jpg) + +客户端和服务器通过网络进行连接,并组成了一个更大的 MVC 架构;从这个角度来看,服务端的模型层才存储了真正的数据,而客户端的模型层只不过是一个存储在客户端设备中的本地缓存和临时数据的集合;同理,服务端的视图层也不是整个应用的视图层,用于为用户展示数据的视图层位于客户端,也就是整个架构的最顶部;中间的五个部分,也就是从低端的模型层到最上面的视图共同组成了整个应用的控制器,将模型中的数据以合理的方式传递给最上层的视图层用于展示。 + +## MVP + +MVP 架构模式是 MVC 的一个变种,很多框架都自称遵循 MVC 架构模式,但是它们实际上却实现了 MVP 模式;MVC 与 MVP 之间的区别其实并不明显,作者认为两者之间最大的区别就是 MVP 中使用 Presenter 对视图和模型进行了解耦,它们彼此都对对方一无所知,沟通都通过 Presenter 进行。 + +MVP 作为一个比较有争议的架构模式,在维基百科的 [Model-view-presenter](https://en.wikipedia.org/wiki/Model–view–presenter) 词条中被描述为 MVC 设计模式的变种(derivation),自上个世纪 90 年代出现在 IBM 之后,随着不断的演化,虽然有着很多分支,不过 Martin Fowler 对 MVP 架构模式的定义最终被广泛接受和讨论。 + +![Standard-MVP](images/mvx/Standard-MVP.jpg) + +在 MVP 中,Presenter 可以理解为松散的控制器,其中包含了视图的 UI 业务逻辑,所有从视图发出的事件,都会通过代理给 Presenter 进行处理;同时,Presenter 也通过视图暴露的接口与其进行通信。 + +目前常见的 MVP 架构模式其实都是它的变种:[Passive View](https://www.martinfowler.com/eaaDev/PassiveScreen.html) 和 [Supervising Controller](https://www.martinfowler.com/eaaDev/SupervisingPresenter.html),接下来的内容也是围绕这两种变种进行展开的。 + +### 被动视图 + +MVP 的第一个主要变种就是被动视图(Passive View);顾名思义,在该变种的架构模式中,视图层是被动的,它本身不会改变自己的任何的状态,所有的状态都是通过 Presenter 来间接改变的。 + +![PassIve-Vie](images/mvx/PassIve-View.jpg) + +被动的视图层就像前端中的 HTML 和 CSS 代码,只负责展示视图的结构和内容,本身不具有任何的逻辑: + +```swift +<article class="post"> + <header class="post-header"> + <h2 class="post-title"><a href="https://github.com/mvx-controller.html">谈谈 MVX 中的 Controller</a></h2> + </header> + <section class="post-excerpt"> + <p>在前两篇文章中,我们已经对 iOS 中的 Model 层以及 View 层进行了分析,划分出了它们的具体职责,其中 Model 层除了负责数据的持久存储、缓存工作,还要负责所有 HTTP... <a class="read-more" href="https://github.com/mvx-controller.html">»</a></p> + </section> + <footer class="post-meta"> + <img class="author-thumb" src="https://github.com/assets/images/draven.png" alt="Author image" nopin="nopin" /> + <a href='https://github.com/author/draveness'>Draveness</a> + <time class="post-date" datetime="2017-06-23">23 Jun 2017</time> + </footer> +</article> +``` + +#### 依赖关系 + +视图成为了完全被动的并且不再根据模型来更新视图本身的内容,也就是说,不同于 MVC 中的依赖关系;在被动视图中,视图层对于模型层没有任何的依赖: + +![Essential-Dependencies-in-Passive-Vie](images/mvx/Essential-Dependencies-in-Passive-View.jpg) + +因为视图层不依赖与其他任何层级也就最大化了视图层的可测试性,同时也将视图层和模型层进行了合理的分离,两者不再相互依赖。 + +#### 通信方式 + +被动视图的示意图中一共有四条线,用于表示 Model、View 和 Presenter 之间的通信: + +![Passive-View-with-Tags](images/mvx/Passive-View-with-Tags.jpg) + +1. 当视图接收到来自用户的事件时,会将事件转交给 Presenter 进行处理; +2. 被动的视图向外界暴露接口,当需要更新视图时 Presenter 通过视图暴露的接口更新视图的内容; +3. Presenter 负责对模型进行操作和更新,在需要时取出其中存储的信息; +4. 当模型层改变时,可以将改变的信息发送给**观察者** Presenter; + +在 MVP 的变种被动视图中,模型的操作以及视图的更新都仅通过 Presenter 作为中间人进行。 + +### 监督控制器 + +与被动视图中状态同步都需要**显式**的操作不同,监督控制器(Supervising Controller)就将部分需要显式同步的操作变成了隐式的: + +![Supervising-Controller](images/mvx/Supervising-Controller.jpg) + +在监督控制器中,视图层接管了一部分视图逻辑,主要内容就是同步**简单的**视图和模型的状态;而监督控制器就需要负责响应用户的输入以及一部分更加复杂的视图、模型状态同步工作。 + +对于用户输入的处理,监督控制器的做法与标准 MVP 中的 Presenter 完全相同;但是对于视图、模型的同步工作,监督控制器会尽可能地将所有简单的属性**以数据绑定的形式声明在视图层中**,类似于 Vue 中双向绑定的简化版本: + +```html +<a v-bind:href="https://github.com/url"></a> +``` + +剩下的无法通过上述方式直接绑定的属性就需要通过监督控制器来操作和更新了。 + +#### 通信方式 + +监督控制器中的视图和模型层之间增加了两者之间的耦合,也就增加了整个架构的复杂性: + +![Supervising-Controller-With-Tag](images/mvx/Supervising-Controller-With-Tag.jpg) + +视图和监督控制器、模型与监督控制器的关系与被动视图中两者与 Presenter 的关系几乎相同,视图和模型之间新增的依赖就是数据绑定的产物;视图通过声明式的语法与模型中的简单属性进行绑定,当模型发生改变时,会通知其观察者视图作出相应的更新。 + +通过这种方式能够减轻监督控制器的负担,减少其中简单的代码,将一部分逻辑交由视图进行处理;这样也就导致了视图同时可以被 Presenter 和数据绑定两种方式更新,相比于被动视图,监督控制器的方式也降低了视图的可测试性和封装性。 + +### 占主导地位的视图 + +无论是在被动视图还是监督控制器版本的 MVP 架构模式中,视图层在整个架构中都是占主导地位的: + +![Main-View-in-MVP](images/mvx/Main-View-in-MVP.jpg) + +在 MVC 中,控制器负责**以不同的视图响应客户端请求的不同动作**;然而,不同于 MVC 模式,MVP 中视图将所有的动作交给 Presenter 进行处理;MVC 中的所有的动作都对应着一个控制器的方法调用,Web 应用中的每一个动作都是对某一个 URL 进行的操作,控制器根据访问的路由和方法(GET 等)对数据进行操作,最终选择正确的视图进行返回。 + +MVC 中控制器返回的视图没有直接绑定到模型上,它仅仅被控制器渲染并且是完全无状态的,其中不包含任何的逻辑,但是 MVP 中的视图**必须要将对应的事件代理给 Presenter 执行**,否则事件就无法被响应。 + +另一个 MVP 与 MVC 之间的重大区别就是,MVP(Passive View)中的视图和模型是完全解耦的,它们对于对方的存在完全不知情,这也是区分 MVP 和 MVC 的一个比较容易的方法。 + +> 上述内容取自 [What are MVP and MVC and what is the difference? · Stack Overflow](https://stackoverflow.com/questions/2056/what-are-mvp-and-mvc-and-what-is-the-difference) 中的 Model-View-Controller 部分。 + +## MVVM + +相较于 MVC 和 MVP 模式,MVVM 在定义上就明确得多,同时,维基百科上对于 [Model-View-ViewModel](https://en.wikipedia.org/wiki/Model–view–viewmodel) 的词条也没有歧义;不过,在谈 MVVM 架构模式之前,我们需要先了解它是如何发展和演变的。 + +### MVVM 的演变 + +早在 2004 年,Martin Fowler 发表了一篇名为 [Presentation Model](https://www.martinfowler.com/eaaDev/PresentationModel.html) (以下简称为 PM 模式)的文章,PM 模式与 MVP 比较相似,它从视图层中分离了行为和状态;PM 模式中创建了一个视图的抽象,叫做 Presentation Model,而视图也成为了这个模型的『渲染』结果。 + +![PM-and-MVV](images/mvx/PM-and-MVVM.jpg) + +2005 年,John Gossman 在他的博客上公布了 [Introduction to Model/View/ViewModel pattern for building WPF apps](https://blogs.msdn.microsoft.com/johngossman/2005/10/08/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps/) 一文。MVVM 与 Martin Fowler 所说的 PM 模式其实是完全相同的,Fowler 提出的 PM 模式是一种与平台无关的创建视图抽象的方法,而 Gossman 的 MVVM 是专门用于 WPF 框架来简化用户界面的创建的模式;我们可以认为 **MVVM 是在 WPF 平台上对于 PM 模式的实现**。 + +> 有兴趣的读者可以阅读 [Introduction to Model/View/ViewModel pattern for building WPF apps](https://blogs.msdn.microsoft.com/johngossman/2005/10/08/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps/ · MSDN) 获得更多与 MVVM 演化的相关信息。 + +### 展示模型 + +> 本节大部分内容都节选自 Martin Fowler 的 [Presentation Model](https://www.martinfowler.com/eaaDev/PresentationModel.html) 一文。 + +既然 MVVM 是展示模型 [Presentation Model](https://www.martinfowler.com/eaaDev/PresentationModel.html) 的一个实现,那么在介绍 Model-View-ViewModel 之前,我们就需要了解 PM 模式到底是什么。 + +在 MVC 一节中曾经有过对展示层和领域层进行分离的讨论,而 PM 模式就与分离展示层 [Separated Presentation](https://www.martinfowler.com/eaaDev/SeparatedPresentation.html) 有一定的关系。 + +作为 Martin Fowler 在 2004 年提出的概念,Presentation Model 到今天其实也是非常先进的,PM 模式将视图中的全部状态和行为放到一个单独的展示模型中,协调领域对象(模型)并且为视图层提供一个接口。 + +在监督控制器中,视图层与模型层中的一些简单属性进行绑定,在模型属性变化时直接更新视图,而 PM 通过引入展示模型将**模型层中的数据与复杂的业务逻辑封装成属性与简单的数据同时暴露给视图,让视图和展示模型中的属性进行同步**。 + +![Presentation-Mode](images/mvx/Presentation-Model.jpg) + +展示模型中包含所有的视图渲染需要的动态信息,包括视图的内容(text、color)、组件是否启用(enable),除此之外还会将一些方法暴露给视图用于某些事件的响应。 + +#### 状态的同步 + +展示模型对于模型层的操作以及为视图层暴露接口都是非常容易的,在整个 PM 模式中,最为麻烦的就是视图和展示模型状态的同步。 + +因为展示模型是视图的抽象,其中包含了视图的状态(属性)和行为(动作),视图的行为可能很少发生改变,但是视图状态的改变就是非常常见的了,那么同步视图和展示模型的代码应该放哪里就是一个需要考虑的问题了。 + +到目前为止,我们能够防止状态同步代码的地方其实只有两个,也就是视图和展示模型;如果将同步的代码放在视图中,那么可能会影响视图的测试,不过由于现在的大部分客户端程序完全没有测试,这一点其实也影响不大;如果将代码放在展示模型中,实际上就为展示模型增加了视图的依赖,导致不同层级之间的耦合。 + +> 在作者看来这两种选择其实都影响并不大,反正我们的应用中并没有测试嘛。 + +#### 展示模型与其他模块的关系 + +在 PM 模式中,同一个展示模型可以与多个领域对象交互,多个视图可以使用相同的展示模型,但是每一个视图只能持有一个展示模型。 + +![PM-View-Domain-Object](images/mvx/PM-View-Domain-Object.jpg) + +PM 模式中不同层级之间的关系还是非常容易理解的,在这里就不做具体解释了。 + +### MVVM 与 WPF + +MVVM 架构模式是微软在 2005 年诞生的,从诞生一开始就与 WPF 框架的联系非常紧密,在这一节中,我们将介绍 MVVM 模式是如何遵循 PM 模式实现的,WPF 作为微软用于处理 GUI 软件的框架,提供了一套非常优雅的解决方案。 + +![Model-View-ViewModel](images/mvx/Model-View-ViewModel.jpg) + +从 Model-View-ViewModel 这个名字来看,它由三个部分组成,也就是 Model、View 和 ViewModel;其中视图模型(ViewModel)其实就是 PM 模式中的展示模型,在 MVVM 中叫做视图模型。 + +除了我们非常熟悉的 Model、View 和 ViewModel 这三个部分,在 MVVM 的实现中,还引入了**隐式的**一个 Binder 层,而声明式的数据和命令的绑定在 MVVM 模式中就是通过它完成的。 + +![Binder-View-ViewModel](images/mvx/Binder-View-ViewModel.jpg) + +在实现 PM 模式时,我们需要处理视图和展示模型之间状态的同步,也就是 MVVM 中的视图和视图模型,我们使用隐式的 Binder 和 XAML 文件来完成视图和视图模型两者之间的双向绑定: + +```xml +<Window x:Class ="WPFDataBinding.MainWindow" Title="MainWindow" Height="350" Width="604"> + <Grid> + <Label Name="nameLabel" Margin="2">_Name:</Label> + <TextBox Name="nameText" Grid.Column="1" Margin="2" + Text="{Binding Name}"/> + <Label Name="ageLabel" Margin="2" Grid.Row ="1">_Age:</Label> + <TextBox Name="ageText" Grid.Column="1" Grid.Row ="1" Margin ="2" + Text="{Binding Age}"/> + </Grid> +</Window> +``` + +在 WPF 中我们可以使用 Binding 关键字在 XAML 中完成双向绑定,当 `TextBox` 中的文字更新时,Binder 也会更新 ViewModel 中对应属性 `Name` 或者 `Age` 的值。 + +我们可以说 MVVM 将视图和展示模型之间的同步代码放到了视图层(XAML)中,也可以说通过隐式的方法实现了状态的同步。 + +无论是 MVVM 还是 Presentation Model,其中最重要的不是如何同步视图和展示模型/视图模型之间的状态,是使用观察者模式、双向绑定还是其它的机制都不是整个模式中最重要的部分,最为关键的是**展示模型/视图模型创建了一个视图的抽象,将视图中的状态和行为抽离出一个新的抽象**,这才是 MVVM 和 PM 中需要注意的。 + +## 总结 + +从 MVC 架构模式到 MVVM,从分离展示层到展示模型层,经过几十年的发展和演变,MVC 架构模式出现了各种各样的变种,并在不同的平台上有着自己的实现。 + +在架构模式的选用时,我们往往没有太多的发言权,主要因为平台本身往往对应用层有着自己的设计,我们在开发客户端或者前端应用时,只需要遵循平台固有的设计就可以完成应用的开发;不过,在有些时候,由于工程变得庞大、业务逻辑变得异常复杂,我们也可以考虑在原有的架构之上实现一个新的架构以满足工程上的需要。 + +各种架构模式的作用就是分离关注,将属于不同模块的功能分散到合适的位置中,同时尽量降低各个模块的相互依赖并且减少需要联系的胶水代码。文中对于 MVC、MVP 和 MVVM 架构模式的描述很难不掺杂作者的主观意见,如果对文章中的内容有疑问,欢迎提出不同的意见进行讨论。 + +## Reference + ++ [MVC Index](http://heim.ifi.uio.no/~trygver/themes/mvc/mvc-index.html) ++ [The Model-View-Controller (MVC) Its Past and Present](http://heim.ifi.uio.no/~trygver/2003/javazone-jaoo/MVC_pattern.pdf) ++ [The evolution of the Dolphin Smalltalk MVP application framework](http://www.object-arts.com/downloads/papers/TwistingTheTriad.PDF) ++ [MVP: Model-View-Presenter · The Taligent Programming Model for C++ and Java](http://www.wildcrest.com/Potel/Portfolio/mvp.pdf) ++ [Implementing the Model-View-ViewModel Pattern · MSDN](https://msdn.microsoft.com/en-us/library/ff798384.aspx) ++ [GUI Architectures · Martin Fowler](https://martinfowler.com/eaaDev/uiArchs.html) ++ [GUI 应用程序架构的十年变迁](https://zhuanlan.zhihu.com/p/26799645) ++ [Elm Architecture Tutorial · GitHub](https://github.com/evancz/elm-architecture-tutorial/) ++ [Presentation Model · Martin Fowler](https://martinfowler.com/eaaDev/PresentationModel.html) ++ [Model-view-controller · Wikipedia](https://en.wikipedia.org/wiki/Model–view–controller) ++ [Model-view-presenter · Wikipedia](https://en.wikipedia.org/wiki/Model–view–presenter) ++ [Model-view-viewmodel · Wikipedia](https://en.wikipedia.org/wiki/Model–view–viewmodel) ++ [Thing-Model-View-Editor](http://heim.ifi.uio.no/~trygver/1979/mvc-1/1979-05-MVC.pdf) ++ [ASP.NET MVC Overview · MSDN](https://msdn.microsoft.com/en-us/library/dd381412(v=vs.108).aspx) ++ [Intermediate Rails: Understanding Models, Views and Controllers](https://betterexplained.com/articles/intermediate-rails-understanding-models-views-and-controllers/) ++ [Passive View · Martin Fowler](https://www.martinfowler.com/eaaDev/PassiveScreen.html) ++ [Supervising Controller · Martin Fowler](https://www.martinfowler.com/eaaDev/SupervisingPresenter.html) ++ [Applications Programming in Smalltalk-80: How to use Model-View-Controller (MVC)](http://www.dgp.toronto.edu/~dwigdor/teaching/csc2524/2012_F/papers/mvc.pdf) ++ [What are MVP and MVC and what is the difference? · Stack Overflow](https://stackoverflow.com/questions/2056/what-are-mvp-and-mvc-and-what-is-the-difference) ++ [Model-View-Presenter Pattern](http://webclientguidance.codeplex.com/wikipage?title=ModelViewPresenterPatternDescription&referringTitle=MVPDocumentation) ++ [Patterns - WPF Apps With The Model-View-ViewModel Design Pattern · MSDN](https://msdn.microsoft.com/en-us/magazine/dd419663.aspx) ++ [Introduction to Model/View/ViewModel pattern for building WPF apps](https://blogs.msdn.microsoft.com/johngossman/2005/10/08/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps/ · MSDN) ++ [设计模式](https://en.wikipedia.org/wiki/Design_Patterns) + + diff --git a/contents/fishhook/images/fishbook-printf.png b/contents/fishhook/images/fishbook-printf.png new file mode 100644 index 0000000..62dd5ac Binary files /dev/null and b/contents/fishhook/images/fishbook-printf.png differ diff --git a/contents/fishhook/images/fishhook-before-after.png b/contents/fishhook/images/fishhook-before-after.png new file mode 100644 index 0000000..6197f2c Binary files /dev/null and b/contents/fishhook/images/fishhook-before-after.png differ diff --git a/contents/fishhook/images/fishhook-hello-breakpoint.png b/contents/fishhook/images/fishhook-hello-breakpoint.png new file mode 100644 index 0000000..f10fcc2 Binary files /dev/null and b/contents/fishhook/images/fishhook-hello-breakpoint.png differ diff --git a/contents/fishhook/images/fishhook-hello.png b/contents/fishhook/images/fishhook-hello.png new file mode 100644 index 0000000..5aeb225 Binary files /dev/null and b/contents/fishhook/images/fishhook-hello.png differ diff --git a/contents/fishhook/images/fishhook-imp.png b/contents/fishhook/images/fishhook-imp.png new file mode 100644 index 0000000..82be834 Binary files /dev/null and b/contents/fishhook/images/fishhook-imp.png differ diff --git a/contents/fishhook/images/fishhook-mach-o.png b/contents/fishhook/images/fishhook-mach-o.png new file mode 100644 index 0000000..ca48d72 Binary files /dev/null and b/contents/fishhook/images/fishhook-mach-o.png differ diff --git a/contents/fishhook/images/fishhook-result.png b/contents/fishhook/images/fishhook-result.png new file mode 100644 index 0000000..aec5200 Binary files /dev/null and b/contents/fishhook/images/fishhook-result.png differ diff --git a/contents/fishhook/images/fishhook-symbol.png b/contents/fishhook/images/fishhook-symbol.png new file mode 100644 index 0000000..3f3c303 Binary files /dev/null and b/contents/fishhook/images/fishhook-symbol.png differ diff --git "a/contents/fishhook/\345\212\250\346\200\201\344\277\256\346\224\271 C \350\257\255\350\250\200\345\207\275\346\225\260\347\232\204\345\256\236\347\216\260.md" "b/contents/fishhook/\345\212\250\346\200\201\344\277\256\346\224\271 C \350\257\255\350\250\200\345\207\275\346\225\260\347\232\204\345\256\236\347\216\260.md" new file mode 100644 index 0000000..7414b35 --- /dev/null +++ "b/contents/fishhook/\345\212\250\346\200\201\344\277\256\346\224\271 C \350\257\255\350\250\200\345\207\275\346\225\260\347\232\204\345\256\236\347\216\260.md" @@ -0,0 +1,571 @@ +# 动态修改 C 语言函数的实现 + +Objective-C 作为基于 Runtime 的语言,它有非常强大的动态特性,可以在运行期间自省、进行方法调剂、为类增加属性、修改消息转发链路,在代码运行期间通过 Runtime 几乎可以修改 Objecitve-C 层的一切类、方法以及属性。 + +> 真正绝对意义上的动态语言或者静态语言是不存在的。 + +C 语言往往会给我们留下**不可修改**的这一印象;在之前的几年时间里,笔者确实也是这么认为的,然而最近接触到的 [fishhook](https://github.com/facebook/fishhook) 使我对 **C 语言的不可修改**有了更加深刻的理解。 + +> 在文章中涉及到一个比较重要的概念,就是镜像(image);在 Mach-O 文件系统中,所有的可执行文件、dylib 以及 Bundle 都是镜像。 + +## fishhook 简介 + +到这里,我们该简单介绍一下今天分享的 fishhook;fishhook 是一个由 facebook 开源的第三方框架,其主要作用就是**动态修改 C 语言函数实现**。 + +这个框架的代码其实非常的简单,只包含两个文件:`fishhook.c` 以及 `fishhook.h`;两个文件所有的代码加起来也不超过 300 行。 + +不过它的实现原理是非常有意思并且精妙的,我们可以从 `fishhook` 提供的接口中入手。 + +## 从接口开始 + +fishhook 提供非常简单的两个接口以及一个结构体: + +```c +struct rebinding { + const char *name; + void *replacement; + void **replaced; +}; + +int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel); + +int rebind_symbols_image(void *header, + intptr_t slide, + struct rebinding rebindings[], + size_t rebindings_nel); +``` + +其中 `rebind_symbols` 接收一个 `rebindings` 数组,也就是重新绑定信息,还有就是 `rebindings_nel`,也就是 `rebindings` 的个数。 + +## 使用 fishhook 修改 C 函数 + +使用 fishhook 修改 C 函数很容易,我们使用它提供的几个范例来介绍它的使用方法。 + +这里要修改的是底层的 `open` 函数的实现,首先在工程中引入 `fishhook.h` 头文件,然后声明一个与原函数签名相同的函数指针: + +```c +#import "fishhook.h" + +static int (*origianl_open)(const char *, int, ...); +``` + +然后重新实现 `new_open` 函数: + +```c +int new_open(const char *path, int oflag, ...) { + va_list ap = {0}; + mode_t mode = 0; + + if ((oflag & O_CREAT) != 0) { + // mode only applies to O_CREAT + va_start(ap, oflag); + mode = va_arg(ap, int); + va_end(ap); + printf("Calling real open('%s', %d, %d)\n", path, oflag, mode); + return orig_open(path, oflag, mode); + } else { + printf("Calling real open('%s', %d)\n", path, oflag); + return orig_open(path, oflag, mode); + } +} +``` + +这里调用的 `original_open` 其实相当于执行原 `open`;最后,在 main 函数中使用 `rebind_symbols` 对符号进行重绑定: + +```c +// 初始化一个 rebinding 结构体 +struct rebinding open_rebinding = { "open", new_open, (void *)&original_open }; + +// 将结构体包装成数组,并传入数组的大小,对原符号 open 进行重绑定 +rebind_symbols((struct rebinding[1]){open_rebinding}, 1); + +// 调用 open 函数 +__unused int fd = open(argv[0], O_RDONLY); +``` + +在对符号进行重绑定之后,所有调用 `open` 函数的地方实际上都会执行 `new_open` 的实现,也就完成了对 `open` 的修改。 + +![fishhook-result](images/fishhook-result.png) + +程序运行之后打印了 `Calling real open('/Users/apple/Library/Developer/Xcode/DerivedData/Demo-cdnoozusghmqtubdnbzedzdwaagp/Build/Products/Debug/Demo', 0)` 说明我们的对 `open` 函数的修改达到了预期的效果。 + +> 整个 main.m 文件中的代码在文章的最后面 [main.m](#main.m) + +## fishhook 的原理以及实现 + +在介绍 fishhook 具体实现原理之前,有几个非常重要的知识需要我们了解,那就是 **dyld**、动态链接以及 Mach-O 文件系统。 + +### dyld 与动态链接 + +dyld 是 the dynamic link editor 的缩写~~(笔者并不知道为什么要这么缩写)~~。至于它的作用,简单一点说,就是负责将各种各样程序需要的**镜像**加载到程序运行的内存空间中,**这个过程发生的时间非常早 --- 在 objc 运行时初始化之前**。 + +在 dyld 加载镜像时,会执行注册过的回调函数;当然,我们也可以使用下面的方法注册自定义的回调函数,**同时也会为所有已经加载的镜像执行回调**: + +```c +extern void _dyld_register_func_for_add_image( + void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide) +); +``` + +对于每一个已经存在的镜像,当它被**动态链接**时,都会执行回调 `void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide)`,传入文件的 `mach_header` 以及一个虚拟内存地址 `intptr_t`。 + +以一个最简单的 Hello World 程序为例: + +```c +#include <stdio.h> + +int main(int argc, const char * argv[]) { + printf("Hello, World!\n"); + return 0; +} +``` + +代码中只引用了一个 `stdio` 库中的函数 `printf`;我们如果 Build 这段代码,生成可执行文件之后,使用下面的命令 `nm`: + +```shell +$ nm -nm HelloWorld +``` + +`nm` 命令可以查看可执行文件中的符号(对 `nm` 不熟悉的读者可以在终端中使用 `man nm` 查看手册): + +```c + (undefined) external _printf (from libSystem) + (undefined) external dyld_stub_binder (from libSystem) +0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header +0000000100000f50 (__TEXT,__text) external _main +``` + +在可执行文件中的符号列表中,`_printf` 这个符号是未定义(undefined)的,换句话说,编译器还不知道这个符号对应什么东西。 + +但是,如果在文件中加入一个 C 函数 `hello_world`: + +```c +#include <stdio.h> + +void hello_world() { + printf("Hello, World!\n"); +} + +int main(int argc, const char * argv[]) { + printf("Hello, World!\n"); + return 0; +} +``` + +在构建之后,同样使用 `nm` 查看其中的符号: + +```c + (undefined) external _printf (from libSystem) + (undefined) external dyld_stub_binder (from libSystem) +0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header +0000000100000f30 (__TEXT,__text) external _hello_world +0000000100000f50 (__TEXT,__text) external _main +``` + +我们的符号 `_hello_world` 并不是未定义的(undefined),它包含一个内存地址以及 `__TEXT` 段。也就是说**手写的一些函数,在编译之后,其地址并不是未定义的**,这一点对于之后分析 fishhook 有所帮助。 + +使用 `nm` 打印出的另一个符号 `dyld_stub_binder` 对应另一个同名函数。`dyld_stub_binder` 会在目标符号(例如 `printf`)被调用时,将其链接到指定的动态链接库 `libSystem`,再执行 `printf` 的实现(`printf` 符号位于 `__DATA` 端中的 lazy 符号表中): + +![fishhook-symbo](images/fishhook-symbol.png) + +每一个镜像中的 `__DATA` 端都包含两个与动态链接有关的表,其中一个是 `__nl_symbol_ptr`,另一个是 `__la_symbol_ptr`: + ++ `__nl_symbol_ptr` 中的 non-lazy 符号是在动态链接库绑定的时候进行加载的 ++ `__la_symbol_ptr` 中的符号会在该符号被第一次调用时,通过 dyld 中的 `dyld_stub_binder` 过程来进行加载 + +```c +0000000100001010 dq 0x0000000100000f9c ; XREF=0x1000002f8, imp___stubs__printf +``` + +地址 `0x0000000100000f9c` 就是 `printf` 函数打印字符串实现的位置: + +![fishbook-printf](images/fishbook-printf.png) + +在上述代码调用 `printf` 时,由于符号是没有被加载的,就会通过 `dyld_stub_binder` 动态绑定符号。 + +### Mach-O + +由于文章中会涉及一些关于 Mach-O 文件格式的知识,所以在这里会简单介绍一下 Mach-O 文件格式的结构。 + +每一个 Mach-O 文件都会被分为不同的 Segments,比如 `__TEXT`, `__DATA`, `__LINKEDIT`: + +![fishhook-mach-o](images/fishhook-mach-o.png) + +这也就是 Mach-O 中的 `segment_command`(32 位与 64 位不同): + +```c +struct segment_command_64 { /* for 64-bit architectures */ + uint32_t cmd; /* LC_SEGMENT_64 */ + uint32_t cmdsize; /* includes sizeof section_64 structs */ + char segname[16]; /* segment name */ + uint64_t vmaddr; /* memory address of this segment */ + uint64_t vmsize; /* memory size of this segment */ + uint64_t fileoff; /* file offset of this segment */ + uint64_t filesize; /* amount to map from the file */ + vm_prot_t maxprot; /* maximum VM protection */ + vm_prot_t initprot; /* initial VM protection */ + uint32_t nsects; /* number of sections in segment */ + uint32_t flags; /* flags */ +}; +``` + +而每一个 `segment_command` 中又包含了不同的 `section`: + +```c +struct section_64 { /* for 64-bit architectures */ + char sectname[16]; /* name of this section */ + char segname[16]; /* segment this section goes in */ + uint64_t addr; /* memory address of this section */ + uint64_t size; /* size in bytes of this section */ + uint32_t offset; /* file offset of this section */ + uint32_t align; /* section alignment (power of 2) */ + uint32_t reloff; /* file offset of relocation entries */ + uint32_t nreloc; /* number of relocation entries */ + uint32_t flags; /* flags (section type and attributes)*/ + uint32_t reserved1; /* reserved (for offset or index) */ + uint32_t reserved2; /* reserved (for count or sizeof) */ + uint32_t reserved3; /* reserved */ +}; +``` + +你只需要对这几个概念有一个简单的了解,知道它们有怎样的包含关系,当文章中跳出这个名字时,对它不是一无所知就足够了,这里并不会涉及太多相关的知识。 + +### fishhook 的原理 + +到目前为止,我们对 dyld 以及 Mach-O 有了一个初步的了解,而 fishhook 使用了前面章节提到的 `_dyld_register_func_for_add_image` 注册了一个回调,在每次加载镜像到程序中执行回调,动态修改 C 函数实现。 + +在具体分析其源代码之前,先为各位读者详细地介绍它的实现原理: + +dyld 通过更新 Mach-O 二进制文件 `__DATA` 段中的一些指针来绑定 lazy 和 non-lazy 的符号;而 fishhook 先确定某一个符号在 `__DATA` 段中的位置,然后**保存原符号对应的函数指针,并使用新的函数指针覆盖原有符号的函数指针**,实现重绑定。 + +整个过程可以用这么一张图来表示: + +![fishhook-before-afte](images/fishhook-before-after.png) + + +原理看起来还是很简单的,其中最复杂的部分就是从二进制文件中寻找某个符号的位置,在 fishhook 的 README 中,有这样一张图: + +![fishhook-imp](images/fishhook-imp.png) + +这张图初看很复杂,不过它演示的是寻找符号的过程,我们根据这张图来分析一下这个过程: + +1. 从 `__DATA` 段中的 lazy 符号指针表中查找某个符号,获得这个符号的偏移量 `1061`,然后在每一个 `section_64` 中查找 `reserved1`,通过这两个值找到 Indirect Symbol Table 中符号对应的条目 +2. 在 Indirect Symbol Table 找到符号表指针以及对应的索引 `16343` 之后,就需要访问符号表 +3. 然后通过符号表中的偏移量,获取字符串表中的符号 `_close` + +### fishhook 的实现 + +上面梳理了寻找符号的过程,现在,我们终于要开始分析 fishhook 的源代码,看它是如何一步一步替换原有函数实现的。 + +对实现的分析会 `rebind_symbols` 函数为入口,首先看一下函数的调用栈: + +```c +int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel); +└── extern void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide)); + +static void _rebind_symbols_for_image(const struct mach_header *header, intptr_t slide) +└── static void rebind_symbols_for_image(struct rebindings_entry *rebindings, const struct mach_header *header, intptr_t slide) + └── static void perform_rebinding_with_section(struct rebindings_entry *rebindings, section_t *section, intptr_t slide, nlist_t *symtab, char *strtab, uint32_t *indirect_symtab) +``` + +其实函数调用栈非常简单,因为整个库中也没有几个函数,`rebind_symbols` 作为接口,其主要作用就是注册一个函数并在镜像加载时回调: + +```c +int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) { + int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel); + if (retval < 0) return retval; + + if (!_rebindings_head->next) { + _dyld_register_func_for_add_image(_rebind_symbols_for_image); + } else { + uint32_t c = _dyld_image_count(); + for (uint32_t i = 0; i < c; i++) { + _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i)); + } + } + return retval; +} +``` + +在 `rebind_symbols` 最开始执行时,会先调用一个 `prepend_rebindings` 的函数,将整个 `rebindings` 数组添加到 `_rebindings_head` 这个私有数据结构的头部: + +```c +static int prepend_rebindings(struct rebindings_entry **rebindings_head, + struct rebinding rebindings[], + size_t nel) { + struct rebindings_entry *new_entry = malloc(sizeof(struct rebindings_entry)); + if (!new_entry) { + return -1; + } + new_entry->rebindings = malloc(sizeof(struct rebinding) * nel); + if (!new_entry->rebindings) { + free(new_entry); + return -1; + } + memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel); + new_entry->rebindings_nel = nel; + new_entry->next = *rebindings_head; + *rebindings_head = new_entry; + return 0; +} +``` + +也就是说每次调用的 `rebind_symbols` 方法传入的 `rebindings` 数组以及数组的长度都会以 `rebindings_entry` 的形式添加到 `_rebindings_head` 这个私有链表的首部: + +```c +struct rebindings_entry { + struct rebinding *rebindings; + size_t rebindings_nel; + struct rebindings_entry *next; +}; + +static struct rebindings_entry *_rebindings_head; +``` + +这样可以通过判断 `_rebindings_head->next` 的值来判断是否为第一次调用,然后使用 `_dyld_register_func_for_add_image` 将 `_rebind_symbols_for_image` 注册为回调或者为所有存在的镜像单独调用 `_rebind_symbols_for_image`: + +```c +static void _rebind_symbols_for_image(const struct mach_header *header, intptr_t slide) { + rebind_symbols_for_image(_rebindings_head, header, slide); +} +``` + +`_rebind_symbols_for_image` 只是对另一个名字非常相似的函数 `rebind_symbols_for_image` 的封装,从这个函数开始,就到了重绑定符号的过程;不过由于这个方法的实现比较长,具体分析会分成三个部分并省略一些不影响理解的代码: + +```c +static void rebind_symbols_for_image(struct rebindings_entry *rebindings, + const struct mach_header *header, + intptr_t slide) { + segment_command_t *cur_seg_cmd; + segment_command_t *linkedit_segment = NULL; + struct symtab_command* symtab_cmd = NULL; + struct dysymtab_command* dysymtab_cmd = NULL; + + uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t); + for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) { + cur_seg_cmd = (segment_command_t *)cur; + if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) { + if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) { + linkedit_segment = cur_seg_cmd; + } + } else if (cur_seg_cmd->cmd == LC_SYMTAB) { + symtab_cmd = (struct symtab_command*)cur_seg_cmd; + } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) { + dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd; + } + } + + ... +} +``` + +这部分的代码主要功能是从镜像中查找 `linkedit_segment` `symtab_command` 和 `dysymtab_command`;在开始查找之前,要先跳过 `mach_header_t` 长度的位置,然后将当前指针强转成 `segment_command_t`,通过对比 `cmd` 的值,来找到需要的 `segment_command_t`。 + +在查找了几个关键的 segment 之后,我们可以根据几个 segment 获取对应表的内存地址: + +```c +static void rebind_symbols_for_image(struct rebindings_entry *rebindings, const struct mach_header *header, intptr_t slide) { + ... + + uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff; + nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff); + char *strtab = (char *)(linkedit_base + symtab_cmd->stroff); + + uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff); + + ... +} +``` + +在 `linkedit_segment` 结构体中获得其虚拟地址以及文件偏移量,然后通过一下公式来计算当前 `__LINKEDIT` 段的位置: + +``` +slide + vmaffr - fileoff +``` + +类似地,在 `symtab_command` 中获取符号表偏移量和字符串表偏移量,从 `dysymtab_command` 中获取间接符号表(indirect symbol table)偏移量,就能够获得_符号表_、_字符串表_以及_间接符号表_的引用了。 + ++ 间接符号表中的元素都是 `uint32_t *`,指针的值是对应条目 `n_list` 在符号表中的位置 ++ 符号表中的元素都是 `nlist_t` 结构体,其中包含了当前符号在字符串表中的下标 + + ```c + struct nlist_64 { + union { + uint32_t n_strx; /* index into the string table */ + } n_un; + uint8_t n_type; /* type flag, see below */ + uint8_t n_sect; /* section number or NO_SECT */ + uint16_t n_desc; /* see <mach-o/stab.h> */ + uint64_t n_value; /* value of this symbol (or stab offset) */ + }; + ``` + ++ 字符串表中的元素是 `char` 字符 + +该函数的最后一部分就开启了遍历模式,查找整个镜像中的 `SECTION_TYPE` 为 `S_LAZY_SYMBOL_POINTERS` 或者 `S_NON_LAZY_SYMBOL_POINTERS` 的 section,然后调用下一个函数 `perform_rebinding_with_section` 来对 section 中的符号进行处理: + +```c +static void perform_rebinding_with_section(struct rebindings_entry *rebindings, section_t *section, intptr_t slide, nlist_t *symtab, char *strtab, uint32_t *indirect_symtab) { + uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1; + void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr); + for (uint i = 0; i < section->size / sizeof(void *); i++) { + uint32_t symtab_index = indirect_symbol_indices[i]; + uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx; + char *symbol_name = strtab + strtab_offset; + + struct rebindings_entry *cur = rebindings; + while (cur) { + for (uint j = 0; j < cur->rebindings_nel; j++) { + if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) { + if (cur->rebindings[j].replaced != NULL && + indirect_symbol_bindings[i] != cur->rebindings[j].replacement) { + *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i]; + } + indirect_symbol_bindings[i] = cur->rebindings[j].replacement; + goto symbol_loop; + } + } + cur = cur->next; + } + symbol_loop:; + } +} +``` + +该函数的实现的核心内容就是将符号表中的 `symbol_name` 与 `rebinding` 中的名字 `name` 进行比较,如果出现了匹配,就会将原函数的实现传入 `origian_open` 函数指针的地址,并使用新的函数实现 `new_open` 代替原实现: + +```c +if (cur->rebindings[j].replaced != NULL && + indirect_symbol_bindings[i] != cur->rebindings[j].replacement) { + *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i]; // 将原函数的实现传入 original_open 函数指针的地址 +} +indirect_symbol_bindings[i] = cur->rebindings[j].replacement; // 使用新的函数实现 new_open 替换原实现 +``` + +如果你理解了上面的实现代码,该函数的其它代码就很好理解了: + +1. 通过 `indirect_symtab + section->reserved1` 获取 `indirect_symbol_indices *`,也就是符号表的数组 +2. 通过 `(void **)((uintptr_t)slide + section->addr)` 获取函数指针列表 `indirect_symbol_bindings` +3. 遍历符号表数组 `indirect_symbol_indices *` 中的所有符号表中,获取其中的符号表索引 `symtab_index` +4. 通过符号表索引 `symtab_index` 获取符号表中某一个 `n_list` 结构体,得到字符串表中的索引 `symtab[symtab_index].n_un.n_strx` +5. 最后在字符串表中获得符号的名字 `char *symbol_name` + +到这里比较前的准备工作就完成了,剩下的代码会遍历整个 `rebindings_entry` 数组,在其中查找匹配的符号,完成函数实现的替换: + +```c +while (cur) { + for (uint j = 0; j < cur->rebindings_nel; j++) { + if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) { + if (cur->rebindings[j].replaced != NULL && + indirect_symbol_bindings[i] != cur->rebindings[j].replacement) { + *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i]; + } + indirect_symbol_bindings[i] = cur->rebindings[j].replacement; + goto symbol_loop; + } + } + cur = cur->next; +} +``` + +在之后对某一函数的调用(例如 `open`),当查找其函数实现时,都会查找到 `new_open` 的函数指针;在 `new_open` 调用 `origianl_open` 时,同样也会执行原有的函数实现,因为我们通过 `*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i]` 将原函数实现绑定到了新的函数指针上。 + +## 实验 + +fishhook 在 `dyld` 加载镜像时,插入了一个回调函数,交换了原有函数的实现;但是 fishhook 能否修改非动态链接库,比如开发人员自己手写的函数呢?我们可以做一个非常简单的小实验,下面是我们的 `main.m` 文件: + +```c +#import <Foundation/Foundation.h> +#import "fishhook.h" + +void hello() { + printf("hello\n"); +} + +static void (*original_hello)(); + +void new_hello() { + printf("New_hello\n"); + original_hello(); +} + +int main(int argc, const char * argv[]) { + @autoreleasepool { + struct rebinding open_rebinding = { "hello", new_hello, (void *)&original_hello }; + rebind_symbols((struct rebinding[1]){open_rebinding}, 1); + hello(); + } + return 0; +} +``` + +这里的函数实现非常的简单,相信也不需要笔者过多解释了,我们直接运行这份代码: + +![fishhook-hello](images/fishhook-hello.png) + +代码中只打印了 `hello`,说明 fishhook 对这种手写的函数是没有作用的,如果在下面这里打一个断点: + +![fishhook-hello-breakpoint](images/fishhook-hello-breakpoint.png) + +代码并不会进这里,因为 `hello` 这个函数是包含在当前镜像的,它只是从当前镜像的其它代码地址跳转到了当前函数实现,只是一个普通的跳转;这与我们调用外部库时有很大的不同,当调用外部库时,我们需要 dyld 解决函数地址的问题,但是函数在当前镜像中却并不需要 [issue #25](https://github.com/facebook/fishhook/issues/25)。 + +> fishhook can only hook functions that exist in other libraries. It cannot +hook functions that exist in the same image (library or executable) as your +currently running code. + +> The reason for this is that there's no indirection that happens when you +call a function in your own executable. It's just a plain jump to another +code address in your executable. + +> That's very different from calling a function in an external library, where +your executable uses dyld to figure out the address of the function being +called before jumping to it. + + +## 小结 + +fishhook 的实现非常的巧妙,但是它的使用也有一定的局限性,在接触到 fishhook 之前,从没有想到过可以通过一种方式修改 C 函数的实现。 + +## Reference + ++ [Mach-O 可执行文件](https://objccn.io/issue-6-3/) ++ [Static linking vs dynamic linking](http://stackoverflow.com/questions/1993390/static-linking-vs-dynamic-linking) + +## 其它 + +### main.m + +```c +#import <Foundation/Foundation.h> + +#import "fishhook.h" + +static int (*original_open)(const char *, int, ...); + +int new_open(const char *path, int oflag, ...) { + va_list ap = {0}; + mode_t mode = 0; + + if ((oflag & O_CREAT) != 0) { + // mode only applies to O_CREAT + va_start(ap, oflag); + mode = va_arg(ap, int); + va_end(ap); + printf("Calling real open('%s', %d, %d)\n", path, oflag, mode); + return original_open(path, oflag, mode); + } else { + printf("Calling real open('%s', %d)\n", path, oflag); + return original_open(path, oflag, mode); + } +} + +int main(int argc, const char * argv[]) { + @autoreleasepool { + struct rebinding open_rebinding = { "open", new_open, (void *)&original_open }; + rebind_symbols((struct rebinding[1]){open_rebinding}, 1); + __unused int fd = open(argv[0], O_RDONLY); + } + return 0; +} +``` + diff --git a/images/AFURLResponseSerialization.png b/contents/images/AFURLResponseSerialization.png similarity index 100% rename from images/AFURLResponseSerialization.png rename to contents/images/AFURLResponseSerialization.png diff --git a/images/PendingInitializeMap.png b/contents/images/PendingInitializeMap.png similarity index 100% rename from images/PendingInitializeMap.png rename to contents/images/PendingInitializeMap.png diff --git a/images/afnetworking-arch.png b/contents/images/afnetworking-arch.png similarity index 100% rename from images/afnetworking-arch.png rename to contents/images/afnetworking-arch.png diff --git a/images/afnetworking-logo.png b/contents/images/afnetworking-logo.png similarity index 100% rename from images/afnetworking-logo.png rename to contents/images/afnetworking-logo.png diff --git a/images/afnetworking-plist.png b/contents/images/afnetworking-plist.png similarity index 100% rename from images/afnetworking-plist.png rename to contents/images/afnetworking-plist.png diff --git a/images/banner.png b/contents/images/banner.png similarity index 100% rename from images/banner.png rename to contents/images/banner.png diff --git a/images/blockskit.png b/contents/images/blockskit.png similarity index 100% rename from images/blockskit.png rename to contents/images/blockskit.png diff --git a/images/logo.png b/contents/images/logo.png similarity index 100% rename from images/logo.png rename to contents/images/logo.png diff --git a/images/obj-method-struct.png b/contents/images/obj-method-struct.png similarity index 100% rename from images/obj-method-struct.png rename to contents/images/obj-method-struct.png diff --git a/images/objc-ao-associateobjcect.png b/contents/images/objc-ao-associateobjcect.png similarity index 100% rename from images/objc-ao-associateobjcect.png rename to contents/images/objc-ao-associateobjcect.png diff --git a/images/objc-ao-isa-struct.png b/contents/images/objc-ao-isa-struct.png similarity index 100% rename from images/objc-ao-isa-struct.png rename to contents/images/objc-ao-isa-struct.png diff --git a/images/objc-ao-warning-category-property.png b/contents/images/objc-ao-warning-category-property.png similarity index 100% rename from images/objc-ao-warning-category-property.png rename to contents/images/objc-ao-warning-category-property.png diff --git a/images/objc-autorelease-AutoreleasePoolPage-linked-list.png b/contents/images/objc-autorelease-AutoreleasePoolPage-linked-list.png similarity index 100% rename from images/objc-autorelease-AutoreleasePoolPage-linked-list.png rename to contents/images/objc-autorelease-AutoreleasePoolPage-linked-list.png diff --git a/images/objc-autorelease-AutoreleasePoolPage.png b/contents/images/objc-autorelease-AutoreleasePoolPage.png similarity index 100% rename from images/objc-autorelease-AutoreleasePoolPage.png rename to contents/images/objc-autorelease-AutoreleasePoolPage.png diff --git a/images/objc-autorelease-after-insert-to-page.png b/contents/images/objc-autorelease-after-insert-to-page.png similarity index 100% rename from images/objc-autorelease-after-insert-to-page.png rename to contents/images/objc-autorelease-after-insert-to-page.png diff --git a/images/objc-autorelease-breakpoint-main.png b/contents/images/objc-autorelease-breakpoint-main.png similarity index 100% rename from images/objc-autorelease-breakpoint-main.png rename to contents/images/objc-autorelease-breakpoint-main.png diff --git a/images/objc-autorelease-main-cpp-struct.png b/contents/images/objc-autorelease-main-cpp-struct.png similarity index 100% rename from images/objc-autorelease-main-cpp-struct.png rename to contents/images/objc-autorelease-main-cpp-struct.png diff --git a/images/objc-autorelease-main-cpp.png b/contents/images/objc-autorelease-main-cpp.png similarity index 100% rename from images/objc-autorelease-main-cpp.png rename to contents/images/objc-autorelease-main-cpp.png diff --git a/images/objc-autorelease-main.png b/contents/images/objc-autorelease-main.png similarity index 100% rename from images/objc-autorelease-main.png rename to contents/images/objc-autorelease-main.png diff --git a/images/objc-autorelease-page-in-memory.png b/contents/images/objc-autorelease-page-in-memory.png similarity index 100% rename from images/objc-autorelease-page-in-memory.png rename to contents/images/objc-autorelease-page-in-memory.png diff --git a/images/objc-autorelease-pop-stack.png b/contents/images/objc-autorelease-pop-stack.png similarity index 100% rename from images/objc-autorelease-pop-stack.png rename to contents/images/objc-autorelease-pop-stack.png diff --git a/images/objc-autorelease-pop-string.png b/contents/images/objc-autorelease-pop-string.png similarity index 100% rename from images/objc-autorelease-pop-string.png rename to contents/images/objc-autorelease-pop-string.png diff --git a/images/objc-autorelease-print-pool-content.png b/contents/images/objc-autorelease-print-pool-content.png similarity index 100% rename from images/objc-autorelease-print-pool-content.png rename to contents/images/objc-autorelease-print-pool-content.png diff --git a/images/objc-hashtable-copy-class-list.png b/contents/images/objc-hashtable-copy-class-list.png similarity index 100% rename from images/objc-hashtable-copy-class-list.png rename to contents/images/objc-hashtable-copy-class-list.png diff --git a/images/objc-hashtable-hash-state-init.png b/contents/images/objc-hashtable-hash-state-init.png similarity index 100% rename from images/objc-hashtable-hash-state-init.png rename to contents/images/objc-hashtable-hash-state-init.png diff --git a/images/objc-hashtable-hashstate-next.gif b/contents/images/objc-hashtable-hashstate-next.gif similarity index 100% rename from images/objc-hashtable-hashstate-next.gif rename to contents/images/objc-hashtable-hashstate-next.gif diff --git a/images/objc-hashtable-insert-empty.gif b/contents/images/objc-hashtable-insert-empty.gif similarity index 100% rename from images/objc-hashtable-insert-empty.gif rename to contents/images/objc-hashtable-insert-empty.gif diff --git a/images/objc-hashtable-insert-many.gif.gif b/contents/images/objc-hashtable-insert-many.gif.gif similarity index 100% rename from images/objc-hashtable-insert-many.gif.gif rename to contents/images/objc-hashtable-insert-many.gif.gif diff --git a/images/objc-hashtable-insert-one.gif.gif b/contents/images/objc-hashtable-insert-one.gif.gif similarity index 100% rename from images/objc-hashtable-insert-one.gif.gif rename to contents/images/objc-hashtable-insert-one.gif.gif diff --git a/images/objc-hashtable-instrument.png b/contents/images/objc-hashtable-instrument.png similarity index 100% rename from images/objc-hashtable-instrument.png rename to contents/images/objc-hashtable-instrument.png diff --git a/images/objc-hashtable-nsarray-instrument.png b/contents/images/objc-hashtable-nsarray-instrument.png similarity index 100% rename from images/objc-hashtable-nsarray-instrument.png rename to contents/images/objc-hashtable-nsarray-instrument.png diff --git a/images/objc-initialize-breakpoint-lookup-imp-or-forward.png b/contents/images/objc-initialize-breakpoint-lookup-imp-or-forward.png similarity index 100% rename from images/objc-initialize-breakpoint-lookup-imp-or-forward.png rename to contents/images/objc-initialize-breakpoint-lookup-imp-or-forward.png diff --git a/images/objc-initialize-breakpoint.png b/contents/images/objc-initialize-breakpoint.png similarity index 100% rename from images/objc-initialize-breakpoint.png rename to contents/images/objc-initialize-breakpoint.png diff --git a/images/objc-initialize-class_rw_t_-bits-flag.png b/contents/images/objc-initialize-class_rw_t_-bits-flag.png similarity index 100% rename from images/objc-initialize-class_rw_t_-bits-flag.png rename to contents/images/objc-initialize-class_rw_t_-bits-flag.png diff --git a/images/objc-initialize-print-initialize.png b/contents/images/objc-initialize-print-initialize.png similarity index 100% rename from images/objc-initialize-print-initialize.png rename to contents/images/objc-initialize-print-initialize.png diff --git a/images/objc-initialize-print-nothing.png b/contents/images/objc-initialize-print-nothing.png similarity index 100% rename from images/objc-initialize-print-nothing.png rename to contents/images/objc-initialize-print-nothing.png diff --git a/images/objc-initialize-print-selector.png b/contents/images/objc-initialize-print-selector.png similarity index 100% rename from images/objc-initialize-print-selector.png rename to contents/images/objc-initialize-print-selector.png diff --git a/images/objc-isa-class-diagram.png b/contents/images/objc-isa-class-diagram.png similarity index 100% rename from images/objc-isa-class-diagram.png rename to contents/images/objc-isa-class-diagram.png diff --git a/images/objc-isa-class-object.png b/contents/images/objc-isa-class-object.png similarity index 100% rename from images/objc-isa-class-object.png rename to contents/images/objc-isa-class-object.png diff --git a/images/objc-isa-class-pointer.png b/contents/images/objc-isa-class-pointer.png similarity index 100% rename from images/objc-isa-class-pointer.png rename to contents/images/objc-isa-class-pointer.png diff --git a/images/objc-isa-isat-bits-has-css-dtor.png b/contents/images/objc-isa-isat-bits-has-css-dtor.png similarity index 100% rename from images/objc-isa-isat-bits-has-css-dtor.png rename to contents/images/objc-isa-isat-bits-has-css-dtor.png diff --git a/images/objc-isa-isat-bits.png b/contents/images/objc-isa-isat-bits.png similarity index 100% rename from images/objc-isa-isat-bits.png rename to contents/images/objc-isa-isat-bits.png diff --git a/images/objc-isa-isat-class-highlight-bits.png b/contents/images/objc-isa-isat-class-highlight-bits.png similarity index 100% rename from images/objc-isa-isat-class-highlight-bits.png rename to contents/images/objc-isa-isat-class-highlight-bits.png diff --git a/images/objc-isa-isat.png b/contents/images/objc-isa-isat.png similarity index 100% rename from images/objc-isa-isat.png rename to contents/images/objc-isa-isat.png diff --git a/images/objc-isa-meta-class.png b/contents/images/objc-isa-meta-class.png similarity index 100% rename from images/objc-isa-meta-class.png rename to contents/images/objc-isa-meta-class.png diff --git a/images/objc-isa-print-class-object.png b/contents/images/objc-isa-print-class-object.png similarity index 100% rename from images/objc-isa-print-class-object.png rename to contents/images/objc-isa-print-class-object.png diff --git a/images/objc-isa-print-cls.png b/contents/images/objc-isa-print-cls.png similarity index 100% rename from images/objc-isa-print-cls.png rename to contents/images/objc-isa-print-cls.png diff --git a/images/objc-isa-print-object.png b/contents/images/objc-isa-print-object.png similarity index 100% rename from images/objc-isa-print-object.png rename to contents/images/objc-isa-print-object.png diff --git a/images/objc-load-break-after-add-breakpoint.png b/contents/images/objc-load-break-after-add-breakpoint.png similarity index 100% rename from images/objc-load-break-after-add-breakpoint.png rename to contents/images/objc-load-break-after-add-breakpoint.png diff --git a/images/objc-load-diagram.png b/contents/images/objc-load-diagram.png similarity index 100% rename from images/objc-load-diagram.png rename to contents/images/objc-load-diagram.png diff --git a/images/objc-load-image-binary.png b/contents/images/objc-load-image-binary.png similarity index 100% rename from images/objc-load-image-binary.png rename to contents/images/objc-load-image-binary.png diff --git a/images/objc-load-print-image-info.png b/contents/images/objc-load-print-image-info.png similarity index 100% rename from images/objc-load-print-image-info.png rename to contents/images/objc-load-print-image-info.png diff --git a/images/objc-load-print-load.png b/contents/images/objc-load-print-load.png similarity index 100% rename from images/objc-load-print-load.png rename to contents/images/objc-load-print-load.png diff --git a/images/objc-load-producer-consumer-diagram.png b/contents/images/objc-load-producer-consumer-diagram.png similarity index 100% rename from images/objc-load-producer-consumer-diagram.png rename to contents/images/objc-load-producer-consumer-diagram.png diff --git a/images/objc-load-symbolic-breakpoint.png b/contents/images/objc-load-symbolic-breakpoint.png similarity index 100% rename from images/objc-load-symbolic-breakpoint.png rename to contents/images/objc-load-symbolic-breakpoint.png diff --git a/images/objc-message-add-imp-to-cache.png b/contents/images/objc-message-add-imp-to-cache.png similarity index 100% rename from images/objc-message-add-imp-to-cache.png rename to contents/images/objc-message-add-imp-to-cache.png diff --git a/images/objc-message-after-flush-cache-trap-in-lookup-again.png b/contents/images/objc-message-after-flush-cache-trap-in-lookup-again.png similarity index 100% rename from images/objc-message-after-flush-cache-trap-in-lookup-again.png rename to contents/images/objc-message-after-flush-cache-trap-in-lookup-again.png diff --git a/images/objc-message-after-flush-cache.png b/contents/images/objc-message-after-flush-cache.png similarity index 100% rename from images/objc-message-after-flush-cache.png rename to contents/images/objc-message-after-flush-cache.png diff --git a/images/objc-message-before-flush-cache.png b/contents/images/objc-message-before-flush-cache.png similarity index 100% rename from images/objc-message-before-flush-cache.png rename to contents/images/objc-message-before-flush-cache.png diff --git a/images/objc-message-cache-struct.png b/contents/images/objc-message-cache-struct.png similarity index 100% rename from images/objc-message-cache-struct.png rename to contents/images/objc-message-cache-struct.png diff --git a/images/objc-message-core.png b/contents/images/objc-message-core.png similarity index 100% rename from images/objc-message-core.png rename to contents/images/objc-message-core.png diff --git a/images/objc-message-find-selector-before-init.png b/contents/images/objc-message-find-selector-before-init.png similarity index 100% rename from images/objc-message-find-selector-before-init.png rename to contents/images/objc-message-find-selector-before-init.png diff --git a/images/objc-message-first-call-hello.png b/contents/images/objc-message-first-call-hello.png similarity index 100% rename from images/objc-message-first-call-hello.png rename to contents/images/objc-message-first-call-hello.png diff --git a/images/objc-message-objc-msgSend-with-cache.gif b/contents/images/objc-message-objc-msgSend-with-cache.gif similarity index 100% rename from images/objc-message-objc-msgSend-with-cache.gif rename to contents/images/objc-message-objc-msgSend-with-cache.gif diff --git a/images/objc-message-run-after-add-cache.png b/contents/images/objc-message-run-after-add-cache.png similarity index 100% rename from images/objc-message-run-after-add-cache.png rename to contents/images/objc-message-run-after-add-cache.png diff --git a/images/objc-message-selector-undefined.png b/contents/images/objc-message-selector-undefined.png similarity index 100% rename from images/objc-message-selector-undefined.png rename to contents/images/objc-message-selector-undefined.png diff --git a/images/objc-message-selector.png b/contents/images/objc-message-selector.png similarity index 100% rename from images/objc-message-selector.png rename to contents/images/objc-message-selector.png diff --git a/images/objc-message-step-in-cache-getimp.png b/contents/images/objc-message-step-in-cache-getimp.png similarity index 100% rename from images/objc-message-step-in-cache-getimp.png rename to contents/images/objc-message-step-in-cache-getimp.png diff --git a/images/objc-message-wrong-step-in.gif b/contents/images/objc-message-wrong-step-in.gif similarity index 100% rename from images/objc-message-wrong-step-in.gif rename to contents/images/objc-message-wrong-step-in.gif diff --git a/images/objc-message-youtube-preview.jpg b/contents/images/objc-message-youtube-preview.jpg similarity index 100% rename from images/objc-message-youtube-preview.jpg rename to contents/images/objc-message-youtube-preview.jpg diff --git a/images/objc-method-after-compile.png b/contents/images/objc-method-after-compile.png similarity index 100% rename from images/objc-method-after-compile.png rename to contents/images/objc-method-after-compile.png diff --git a/images/objc-method-after-methodizeClass.png b/contents/images/objc-method-after-methodizeClass.png similarity index 100% rename from images/objc-method-after-methodizeClass.png rename to contents/images/objc-method-after-methodizeClass.png diff --git a/images/objc-method-after-realize-breakpoint.png b/contents/images/objc-method-after-realize-breakpoint.png similarity index 100% rename from images/objc-method-after-realize-breakpoint.png rename to contents/images/objc-method-after-realize-breakpoint.png diff --git a/images/objc-method-after-realize-class.png b/contents/images/objc-method-after-realize-class.png similarity index 100% rename from images/objc-method-after-realize-class.png rename to contents/images/objc-method-after-realize-class.png diff --git a/images/objc-method-before-realize.png b/contents/images/objc-method-before-realize.png similarity index 100% rename from images/objc-method-before-realize.png rename to contents/images/objc-method-before-realize.png diff --git a/images/objc-method-breakpoint-before-set-rw.png b/contents/images/objc-method-breakpoint-before-set-rw.png similarity index 100% rename from images/objc-method-breakpoint-before-set-rw.png rename to contents/images/objc-method-breakpoint-before-set-rw.png diff --git a/images/objc-method-class-data-bits-t.png b/contents/images/objc-method-class-data-bits-t.png similarity index 100% rename from images/objc-method-class-data-bits-t.png rename to contents/images/objc-method-class-data-bits-t.png diff --git a/images/objc-method-class.png b/contents/images/objc-method-class.png similarity index 100% rename from images/objc-method-class.png rename to contents/images/objc-method-class.png diff --git a/images/objc-method-class_data_bits_t.png b/contents/images/objc-method-class_data_bits_t.png similarity index 100% rename from images/objc-method-class_data_bits_t.png rename to contents/images/objc-method-class_data_bits_t.png diff --git a/images/objc-method-compile-class.png b/contents/images/objc-method-compile-class.png similarity index 100% rename from images/objc-method-compile-class.png rename to contents/images/objc-method-compile-class.png diff --git a/images/objc-method-lldb-breakpoint.png b/contents/images/objc-method-lldb-breakpoint.png similarity index 100% rename from images/objc-method-lldb-breakpoint.png rename to contents/images/objc-method-lldb-breakpoint.png diff --git a/images/objc-method-lldb-print-before-realize.png b/contents/images/objc-method-lldb-print-before-realize.png similarity index 100% rename from images/objc-method-lldb-print-before-realize.png rename to contents/images/objc-method-lldb-print-before-realize.png diff --git a/images/objc-method-lldb-print-method-list.png b/contents/images/objc-method-lldb-print-method-list.png similarity index 100% rename from images/objc-method-lldb-print-method-list.png rename to contents/images/objc-method-lldb-print-method-list.png diff --git a/images/objc-method-print-class-struct-after-realize.png b/contents/images/objc-method-print-class-struct-after-realize.png similarity index 100% rename from images/objc-method-print-class-struct-after-realize.png rename to contents/images/objc-method-print-class-struct-after-realize.png diff --git a/images/objc-method-target.png b/contents/images/objc-method-target.png similarity index 100% rename from images/objc-method-target.png rename to contents/images/objc-method-target.png diff --git a/images/objc-rr-isa-struct.png b/contents/images/objc-rr-isa-struct.png similarity index 100% rename from images/objc-rr-isa-struct.png rename to contents/images/objc-rr-isa-struct.png diff --git "a/libextobjc/\345\246\202\344\275\225\345\234\250 Objective-C \347\232\204\347\216\257\345\242\203\344\270\213\345\256\236\347\216\260 defer.md" "b/contents/libextobjc/\345\246\202\344\275\225\345\234\250 Objective-C \347\232\204\347\216\257\345\242\203\344\270\213\345\256\236\347\216\260 defer.md" similarity index 100% rename from "libextobjc/\345\246\202\344\275\225\345\234\250 Objective-C \347\232\204\347\216\257\345\242\203\344\270\213\345\256\236\347\216\260 defer.md" rename to "contents/libextobjc/\345\246\202\344\275\225\345\234\250 Objective-C \347\232\204\347\216\257\345\242\203\344\270\213\345\256\236\347\216\260 defer.md" diff --git a/objc/README.md b/contents/objc/README.md similarity index 51% rename from objc/README.md rename to contents/objc/README.md index f974393..cbc0df4 100644 --- a/objc/README.md +++ b/contents/objc/README.md @@ -4,12 +4,12 @@ + ObjC 源代码 - + [从 NSObject 的初始化了解 isa](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/从%20NSObject%20的初始化了解%20isa.md) - + [深入解析 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/objc/从源代码看%20ObjC%20中消息的发送.md) - + [你真的了解 load 方法么?](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/你真的了解%20load%20方法么?.md) - + [懒惰的 initialize](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/懒惰的%20initialize%20方法.md) - + [自动释放池的前世今生](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/自动释放池的前世今生.md) - + [黑箱中的 retain 和 release](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/黑箱中的%20retain%20和%20release.md) + + [从 NSObject 的初始化了解 isa](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/从%20NSObject%20的初始化了解%20isa.md) + + [深入解析 ObjC 中方法的结构](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/深入解析%20ObjC%20中方法的结构.md) + + [从源代码看 ObjC 中消息的发送](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/从源代码看%20ObjC%20中消息的发送.md) + + [你真的了解 load 方法么?](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/你真的了解%20load%20方法么?.md) + + [懒惰的 initialize](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/懒惰的%20initialize%20方法.md) + + [自动释放池的前世今生](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/自动释放池的前世今生.md) + + [黑箱中的 retain 和 release](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/黑箱中的%20retain%20和%20release.md) diff --git "a/objc/\344\270\212\345\217\244\346\227\266\344\273\243 Objective-C \344\270\255\345\223\210\345\270\214\350\241\250\347\232\204\345\256\236\347\216\260.md" "b/contents/objc/\344\270\212\345\217\244\346\227\266\344\273\243 Objective-C \344\270\255\345\223\210\345\270\214\350\241\250\347\232\204\345\256\236\347\216\260.md" similarity index 99% rename from "objc/\344\270\212\345\217\244\346\227\266\344\273\243 Objective-C \344\270\255\345\223\210\345\270\214\350\241\250\347\232\204\345\256\236\347\216\260.md" rename to "contents/objc/\344\270\212\345\217\244\346\227\266\344\273\243 Objective-C \344\270\255\345\223\210\345\270\214\350\241\250\347\232\204\345\256\236\347\216\260.md" index aa30d9d..9389b9d 100644 --- "a/objc/\344\270\212\345\217\244\346\227\266\344\273\243 Objective-C \344\270\255\345\223\210\345\270\214\350\241\250\347\232\204\345\256\236\347\216\260.md" +++ "b/contents/objc/\344\270\212\345\217\244\346\227\266\344\273\243 Objective-C \344\270\255\345\223\210\345\270\214\350\241\250\347\232\204\345\256\236\347\216\260.md" @@ -690,7 +690,7 @@ NXHashTable *NXCreateHashTable (NXHashTablePrototype prototype, unsigned capacit ![objc-hashtable-nsarray-instrument](../images/objc-hashtable-nsarray-instrument.png) -导致这一现象的原始可能是:在将原数组中的内容移入新数组时,**临时变量申请了大量的内存控件**。 +导致这一现象的原始可能是:在将原数组中的内容移入新数组时,**临时变量申请了大量的内存空间**。 > 在之后关于 CoreFoundation 源代码分析的文中会介绍它们是怎么实现的。 @@ -702,7 +702,7 @@ NXHashTable *NXCreateHashTable (NXHashTablePrototype prototype, unsigned capacit static NXHashTable *realized_class_hash = nil; ``` -我么可以使用 `objc_copyClassList` 获取类的数组: +我们可以使用 `objc_copyClassList` 获取类的数组: ```objectivec Class * diff --git "a/objc/\344\273\216 NSObject \347\232\204\345\210\235\345\247\213\345\214\226\344\272\206\350\247\243 isa.md" "b/contents/objc/\344\273\216 NSObject \347\232\204\345\210\235\345\247\213\345\214\226\344\272\206\350\247\243 isa.md" similarity index 98% rename from "objc/\344\273\216 NSObject \347\232\204\345\210\235\345\247\213\345\214\226\344\272\206\350\247\243 isa.md" rename to "contents/objc/\344\273\216 NSObject \347\232\204\345\210\235\345\247\213\345\214\226\344\272\206\350\247\243 isa.md" index d06f047..06b3e2e 100644 --- "a/objc/\344\273\216 NSObject \347\232\204\345\210\235\345\247\213\345\214\226\344\272\206\350\247\243 isa.md" +++ "b/contents/objc/\344\273\216 NSObject \347\232\204\345\210\235\345\247\213\345\214\226\344\272\206\350\247\243 isa.md" @@ -12,7 +12,6 @@ struct objc_object { 当 ObjC 为一个对象分配内存,初始化实例变量后,在这些对象的实例变量的结构体中的第一个就是 `isa`。 -<p align='center'> ![objc-isa-class-object](../images/objc-isa-class-object.png) > 所有继承自 `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) -<p align='center'> ![objc-isa-class-pointer](../images/objc-isa-class-pointer.png) 但是,这样就有一个问题,类方法的实现又是如何查找并且调用的呢?这时,就需要引入*元类*来保证无论是类还是对象都能**通过相同的机制查找方法的实现**。 -<p align='center'> ![objc-isa-meta-class](../images/objc-isa-meta-class.png) @@ -58,7 +55,6 @@ struct objc_class : objc_object { 下面这张图介绍了对象,类与元类之间的关系,笔者认为已经觉得足够清晰了,所以不在赘述。 -<p align='center'> ![](../images/objc-isa-class-diagram.png) > 图片来自 [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 位的内存空间(决定于其中的结构体) -<p align='center'> ![objc-isa-isat](../images/objc-isa-isat.png) ```objectivec @@ -173,7 +168,6 @@ inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor) 我们可以把它转换成二进制的数据,然后看一下哪些属性对应的位被这行代码初始化了(标记为红色): -<p align='center'> ![objc-isa-isat-bits](../images/objc-isa-isat-bits.png) 从图中了解到,在使用 `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; ``` -<p align='center'> ![objc-isa-isat-bits-has-css-dto](../images/objc-isa-isat-bits-has-css-dtor.png) ### `shiftcls` @@ -242,7 +235,6 @@ isa.shiftcls = (uintptr_t)cls >> 3; 而 ObjC 中的类指针的地址后三位也为 0,在 `_class_createInstanceFromZone` 方法中打印了调用这个方法传入的类指针: -<p align='center'> ![objc-isa-print-cls](../images/objc-isa-print-cls.png) 可以看到,这里打印出来的**所有类指针十六进制地址的最后一位都为 8 或者 0**。也就是说,类指针的后三位都为 0,所以,我们在上面存储 `Class` 指针时右移三位是没有问题的。 @@ -253,7 +245,6 @@ isa.shiftcls = (uintptr_t)cls >> 3; 如果再尝试打印对象指针的话,会发现所有对象内存地址的**后四位**都是 0,说明 ObjC 在初始化内存时是以 16 个字节对齐的, 分配的内存地址后四位都是 0。 -<p align='center'> ![objc-isa-print-object](../images/objc-isa-print-object.png) > 使用整个指针大小的内存来存储 `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` 打印出来,具体分析一下 -<p align='center'> ![objc-isa-print-class-object](../images/objc-isa-print-class-object.png) ``` 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 源代码中分析并合理地推测一些关于消息传递的问题**。 -<p align='center'> ![objc-message-core](../images/objc-message-core.png) ## 关于 @selector() 你需要知道的 @@ -87,7 +86,6 @@ int main(int argc, const char * argv[]) { 在主函数任意位置打一个断点, 比如 `-> [object hello];` 这里,然后在 lldb 中输入: -<p align='center'> ![objc-message-selecto](../images/objc-message-selector.png) 这里面我们打印了两个选择子的地址` @selector(hello)` 以及 `@selector(undefined_hello_method)`,需要注意的是: @@ -96,7 +94,6 @@ int main(int argc, const char * argv[]) { 如果我们修改程序的代码: -<p align='center'> ![objc-message-selector-undefined](../images/objc-message-selector-undefined.png) 在这里,由于我们在代码中显示地写出了 `@selector(undefined_hello_method)`,所以在 lldb 中再次打印这个 `sel` 内存地址跟之前相比有了很大的改变。 @@ -111,7 +108,6 @@ int main(int argc, const char * argv[]) { 在运行时初始化之前,打印 `hello` 选择子的的内存地址: -<p align='center'> ![objc-message-find-selector-before-init](../images/objc-message-find-selector-before-init.png) ## 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`: -<p align='center'> ![objc-message-wrong-step-in](../images/objc-message-wrong-step-in.gif) 因为 `objc_msgSend` 是一个私有方法,我们没有办法进入它的实现,但是,我们却可以在 `objc_msgSend` 的调用栈中“截下”这个函数调用的过程。 @@ -188,7 +183,6 @@ int main(int argc, const char * argv[]) { 在 `objc-runtime-new.mm` 文件中有一个函数 `lookUpImpOrForward`,这个函数的作用就是查找方法的实现,于是运行程序,在运行到 `hello` 这一行时,激活 `lookUpImpOrForward` 函数中的断点。 -<p align='center'> <a href="https://youtu.be/bCdjdI4VhwQ" target="_blank"><img src='https://github.com/images/objc-message-youtube-preview.jpg'></a> > 由于转成 gif 实在是太大了,笔者试着用各种方法生成动图,然而效果也不是很理想,只能贴一个 Youtube 的视频链接,不过对于能够翻墙的开发者们,应该也不是什么问题吧(手动微笑) @@ -203,7 +197,6 @@ int main(int argc, const char * argv[]) { 在 `-> [object hello]` 这里增加一个断点,**当程序运行到这一行时**,再向 `lookUpImpOrForward` 函数的第一行添加断点,确保是捕获 `@selector(hello)` 的调用栈,而不是调用其它选择子的调用栈。 -<p align='center'> ![objc-message-first-call-hello](../images/objc-message-first-call-hello.png) 由图中的变量区域可以了解,传入的选择子为 `"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; ``` -<p align='center'> ![objc-message-cache-struct](../images/objc-message-cache-struct.png) 不过 `cache_getImp` 的实现目测是不开源的,同时也是汇编写的,在我们尝试 step in 的时候进入了如下的汇编代码。 -<p align='center'> ![objc-message-step-in-cache-getimp](../images/objc-message-step-in-cache-getimp.png) 它会进入一个 `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` 方法的实现,调用了该方法。 -<p align='center'> ![objc-message-first-call-hello](../images/objc-message-first-call-hello.png) @@ -507,7 +497,6 @@ int main(int argc, const char * argv[]) { 然后在第二次调用 `hello` 方法时,加一个断点: -<p align='center'> ![objc-message-objc-msgSend-with-cache](../images/objc-message-objc-msgSend-with-cache.gif) `objc_msgSend` 并没有走 `lookupImpOrForward` 这个方法,而是直接结束,打印了另一个 `hello` 字符串。 @@ -518,7 +507,6 @@ int main(int argc, const char * argv[]) { 好,现在重新运行程序至第二个 `hello` 方法调用之前: -<p align='center'> ![objc-message-before-flush-cache](../images/objc-message-before-flush-cache.png) 打印缓存中 bucket 的内容: @@ -568,12 +556,10 @@ int main(int argc, const char * argv[]) { } ``` -<p align='center'> ![objc-message-after-flush-cache](../images/objc-message-after-flush-cache.png) 这样 `XXObject` 中就不存在 `hello` 方法对应实现的缓存了。然后继续运行程序: -<p align='center'> ![objc-message-after-flush-cache-trap-in-lookup-again](../images/objc-message-after-flush-cache-trap-in-lookup-again.png) 虽然第二次调用 `hello` 方法,但是因为我们清除了 `hello` 的缓存,所以,会再次进入 `lookupImpOrForward` 方法。 @@ -604,12 +590,10 @@ int main(int argc, const char * argv[]) { 在第一个 `hello` 方法调用之前将实现加入缓存: -<p align='center'> ![objc-message-add-imp-to-cache](../images/objc-message-add-imp-to-cache.png) 然后继续运行代码: -<p align='center'> ![objc-message-run-after-add-cache](../images/objc-message-run-after-add-cache.png) 可以看到,我们虽然没有改变 `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` 方法,主函数中也没有任何的东西: -<p align='center'>![objc-load-print-load](../images/objc-load-print-load.png) +![objc-load-print-load](../images/objc-load-print-load.png) 虽然在主函数中什么方法都没有调用,但是运行之后,依然打印了 `XXObject load` 字符串,也就是说调用了 `+ load` 方法。 @@ -62,13 +62,13 @@ int main(int argc, const char * argv[]) { > 注意这里 `+` 和 `[` 之间没有空格 -<p align='center'>![objc-load-symbolic-breakpoint](../images/objc-load-symbolic-breakpoint.png) +![objc-load-symbolic-breakpoint](../images/objc-load-symbolic-breakpoint.png) > 为什么要加一个符号断点呢?因为这样看起来比较高级。 重新运行程序。这时,代码会停在 `NSLog(@"XXObject load");` 这一行的实现上: -<p align='center'>![objc-load-break-after-add-breakpoint](../images/objc-load-break-after-add-breakpoint.png) +![objc-load-break-after-add-breakpoint](../images/objc-load-break-after-add-breakpoint.png) 左侧的调用栈很清楚的告诉我们,哪些方法被调用了: @@ -128,7 +128,7 @@ load_images(enum dyld_image_states state, uint32_t infoCount, 这里就会遇到一个问题:镜像到底是什么,我们用一个断点打印出所有加载的镜像: -<p align='center'>![objc-load-print-image-info](../images/objc-load-print-image-info.png) +![objc-load-print-image-info](../images/objc-load-print-image-info.png) 从控制台输出的结果大概就是这样的,我们可以看到镜像并不是一个 Objective-C 的代码文件,它应该是一个 target 的编译产物。 @@ -160,7 +160,7 @@ load_images(enum dyld_image_states state, uint32_t infoCount, 但是如果进入最下面的这个目录,会发现它是一个**可执行文件**,它的运行结果与 Xcode 中的运行结果相同: -<p align='center'>![objc-load-image-binary](../images/objc-load-image-binary.png) +![objc-load-image-binary](../images/objc-load-image-binary.png) ### 准备 + load 方法 @@ -267,7 +267,7 @@ void call_load_methods(void) 方法的调用流程大概是这样的: -<p align='center'>![objc-load-diagra](../images/objc-load-diagram.png) +![objc-load-diagra](../images/objc-load-diagram.png) 其中 `call_class_loads` 会从一个待加载的类列表 `loadable_classes` 中寻找对应的类,然后找到 `@selector(load)` 的实现并执行。 @@ -310,7 +310,7 @@ ObjC 对于加载的管理,主要使用了两个列表,分别是 `loadable_c 方法的调用过程也分为两个部分,准备 `load` 方法和调用 `load` 方法,我更觉得这两个部分比较像生产者与消费者: -<p align='center'>![objc-load-producer-consumer-diagra](../images/objc-load-producer-consumer-diagram.png) +![objc-load-producer-consumer-diagra](../images/objc-load-producer-consumer-diagram.png) `add_class_to_loadable_list` 方法负责将类加入 `loadable_classes` 集合,而 `call_class_loads` 负责消费集合中的元素。 diff --git "a/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" similarity index 99% rename from "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" rename to "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" index cfc3acb..059cd09 100644 --- "a/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" @@ -457,7 +457,7 @@ inline void objc_object::setHasAssociatedObjects() { ![objc-ao-isa-struct](../images/objc-ao-isa-struct.png) -> 如果想要了解关于 isa 的知识,可以阅读[从 NSObject 的初始化了解 isa](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/从%20NSObject%20的初始化了解%20isa.md) +> 如果想要了解关于 isa 的知识,可以阅读[从 NSObject 的初始化了解 isa](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/从%20NSObject%20的初始化了解%20isa.md) ### objc_getAssociatedObject diff --git "a/contents/objc/\345\257\271\350\261\241\346\230\257\345\246\202\344\275\225\345\210\235\345\247\213\345\214\226\347\232\204\357\274\210iOS\357\274\211.md" "b/contents/objc/\345\257\271\350\261\241\346\230\257\345\246\202\344\275\225\345\210\235\345\247\213\345\214\226\347\232\204\357\274\210iOS\357\274\211.md" new file mode 100644 index 0000000..0bb16a1 --- /dev/null +++ "b/contents/objc/\345\257\271\350\261\241\346\230\257\345\246\202\344\275\225\345\210\235\345\247\213\345\214\226\347\232\204\357\274\210iOS\357\274\211.md" @@ -0,0 +1,157 @@ +# 对象是如何初始化的(iOS) + +在之前,我们已经讨论了非常多的问题了,关于 objc 源代码系列的文章也快结束了,其实关于对象是如何初始化的这篇文章本来是我要写的第一篇文章,但是由于有很多前置内容不得不说,所以留到了这里。 + +`+ alloc` 和 `- init` 这一对我们在 iOS 开发中每天都要用到的初始化方法一直困扰着我, 于是笔者仔细研究了一下 objc 源码中 `NSObject` 如何进行初始化。 + +在具体分析对象的初始化过程之前,我想先放出结论,以免文章中的细枝末节对读者的理解有所影响;整个对象的初始化过程其实只是**为一个分配内存空间,并且初始化 isa_t 结构体的过程**。 + +## alloc 方法分析 + +先来看一下 `+ alloc` 方法的调用栈(在调用栈中省略了很多不必要的方法的调用): + +```objectivec +id _objc_rootAlloc(Class cls) +└── static id callAlloc(Class cls, bool checkNil, bool allocWithZone=false) + └── id class_createInstance(Class cls, size_t extraBytes) + └── id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, bool cxxConstruct, size_t *outAllocatedSize) + ├── size_t instanceSize(size_t extraBytes) + ├── void *calloc(size_t, size_t) + └── inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor) +``` + +这个调用栈中的方法涉及了多个文件中的代码,在下面的章节中会对调用的方法逐步进行分析,如果这个调用栈让你觉得很头疼,也不是什么问题。 + +### alloc 的实现 + +```objectivec ++ (id)alloc { + return _objc_rootAlloc(self); +} +``` + +`alloc` 方法的实现真的是非常的简单, 它直接调用了另一个私有方法 `id _objc_rootAlloc(Class cls)` + +```objectivec +id _objc_rootAlloc(Class cls) { + return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/); +} +``` + +这就是上帝类 `NSObject` 对 `callAlloc` 的实现,我们省略了非常多的代码,展示了最常见的执行路径: + +```objectivec +static id callAlloc(Class cls, bool checkNil, bool allocWithZone=false) { + id obj = class_createInstance(cls, 0); + return obj; +} + +id class_createInstance(Class cls, size_t extraBytes) { + return _class_createInstanceFromZone(cls, extraBytes, nil); +} +``` + +对象初始化中最重要的操作都在 `_class_createInstanceFromZone` 方法中执行: + +```objectivec +static id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, bool cxxConstruct = true, size_t *outAllocatedSize = nil) { + size_t size = cls->instanceSize(extraBytes); + + id obj = (id)calloc(1, size); + if (!obj) return nil; + obj->initInstanceIsa(cls, hasCxxDtor); + + return obj; +} +``` + +### 对象的大小 + +在使用 `calloc` 为对象分配一块内存空间之前,我们要先获取对象在内存的大小: + +```objectivec +size_t instanceSize(size_t extraBytes) { + size_t size = alignedInstanceSize() + extraBytes; + if (size < 16) size = 16; + return size; +} + +uint32_t alignedInstanceSize() { + return word_align(unalignedInstanceSize()); +} + +uint32_t unalignedInstanceSize() { + assert(isRealized()); + return data()->ro->instanceSize; +} +``` + +实例大小 `instanceSize` 会存储在类的 `isa_t` 结构体中,然后经过对齐最后返回。 + +> Core Foundation 需要所有的对象的大小都必须大于或等于 16 字节。 + +在获取对象大小之后,直接调用 `calloc` 函数就可以为对象分配内存空间了。 + +### isa 的初始化 + +在对象的初始化过程中除了使用 `calloc` 来分配内存之外,还需要根据类初始化 `isa_t` 结构体: + +```objectivec +inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor) { + if (!indexed) { + isa.cls = cls; + } else { + isa.bits = ISA_MAGIC_VALUE; + isa.has_cxx_dtor = hasCxxDtor; + isa.shiftcls = (uintptr_t)cls >> 3; + } +} +``` + +上面的代码只是对 `isa_t` 结构体进行初始化而已: + +```objectivec +union isa_t { + isa_t() { } + isa_t(uintptr_t value) : bits(value) { } + + Class cls; + uintptr_t bits; + + struct { + uintptr_t indexed : 1; + uintptr_t has_assoc : 1; + uintptr_t has_cxx_dtor : 1; + uintptr_t shiftcls : 44; + uintptr_t magic : 6; + uintptr_t weakly_referenced : 1; + uintptr_t deallocating : 1; + uintptr_t has_sidetable_rc : 1; + uintptr_t extra_rc : 8; + }; +}; +``` + +> 在这里并不想过多介绍关于 `isa_t` 结构体的内容,你可以看[从 NSObject 的初始化了解 isa](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/从%20NSObject%20的初始化了解%20isa.md) 来了解你想知道的关于 `isa_t` 的全部内容。 + +## init 方法 + +`NSObject` 的 `- init` 方法只是调用了 `_objc_rootInit` 并返回了当前对象: + +```objectivec +- (id)init { + return _objc_rootInit(self); +} + +id _objc_rootInit(id obj) { + return obj; +} +``` + +## 总结 + +在 iOS 中一个对象的初始化过程很符合直觉,只是分配内存空间、然后初始化 `isa_t` 结构体,其实现也并不复杂,这篇文章也是这个系列文章中较为简单并且简短的一篇。 + +> Follow: [Draveness · Github](https://github.com/Draveness) + + diff --git "a/objc/\346\207\222\346\203\260\347\232\204 initialize \346\226\271\346\263\225.md" "b/contents/objc/\346\207\222\346\203\260\347\232\204 initialize \346\226\271\346\263\225.md" similarity index 97% rename from "objc/\346\207\222\346\203\260\347\232\204 initialize \346\226\271\346\263\225.md" rename to "contents/objc/\346\207\222\346\203\260\347\232\204 initialize \346\226\271\346\263\225.md" index baa20b8..4ed7ab6 100644 --- "a/objc/\346\207\222\346\203\260\347\232\204 initialize \346\226\271\346\263\225.md" +++ "b/contents/objc/\346\207\222\346\203\260\347\232\204 initialize \346\226\271\346\263\225.md" @@ -6,7 +6,7 @@ 这篇文章可能是对 Objective-C 源代码解析系列文章中最短的一篇了,在 Objective-C 中,我们总是会同时想到 `load`、`initialize` 这两个类方法。而这两个方法也经常在一起比较: -在上一篇介绍 `load` 方法的[文章](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/你真的了解%20load%20方法么?.md)中,已经对 `load` 方法的调用时机、调用顺序进行了详细地分析,所以对于 `load` 方法,这里就不在赘述了。 +在上一篇介绍 `load` 方法的[文章](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/你真的了解%20load%20方法么?.md)中,已经对 `load` 方法的调用时机、调用顺序进行了详细地分析,所以对于 `load` 方法,这里就不在赘述了。 这篇文章会~~假设你知道:~~假设你是 iOS 开发者。 @@ -80,7 +80,7 @@ int main(int argc, const char * argv[]) { 6 start ``` -直接来看调用栈中的 `lookUpImpOrForward` 方法,`lookUpImpOrForward` 方法**只会在向对象发送消息,并且在类的缓存中没有找到消息的选择子时**才会调用,具体可以看这篇文章,[从源代码看 ObjC 中消息的发送](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/从源代码看%20ObjC%20中消息的发送.md)。 +直接来看调用栈中的 `lookUpImpOrForward` 方法,`lookUpImpOrForward` 方法**只会在向对象发送消息,并且在类的缓存中没有找到消息的选择子时**才会调用,具体可以看这篇文章,[从源代码看 ObjC 中消息的发送](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/从源代码看%20ObjC%20中消息的发送.md)。 在这里,我们知道 `lookUpImpOrForward` 方法是 `objc_msgSend` 触发的就够了。 diff --git "a/objc/\346\267\261\345\205\245\350\247\243\346\236\220 ObjC \344\270\255\346\226\271\346\263\225\347\232\204\347\273\223\346\236\204.md" "b/contents/objc/\346\267\261\345\205\245\350\247\243\346\236\220 ObjC \344\270\255\346\226\271\346\263\225\347\232\204\347\273\223\346\236\204.md" similarity index 96% rename from "objc/\346\267\261\345\205\245\350\247\243\346\236\220 ObjC \344\270\255\346\226\271\346\263\225\347\232\204\347\273\223\346\236\204.md" rename to "contents/objc/\346\267\261\345\205\245\350\247\243\346\236\220 ObjC \344\270\255\346\226\271\346\263\225\347\232\204\347\273\223\346\236\204.md" index ecf91b6..17d666a 100644 --- "a/objc/\346\267\261\345\205\245\350\247\243\346\236\220 ObjC \344\270\255\346\226\271\346\263\225\347\232\204\347\273\223\346\236\204.md" +++ "b/contents/objc/\346\267\261\345\205\245\350\247\243\346\236\220 ObjC \344\270\255\346\226\271\346\263\225\347\232\204\347\273\223\346\236\204.md" @@ -2,7 +2,7 @@ > 因为 ObjC 的 runtime 只能在 Mac OS 下才能编译,所以文章中的代码都是在 Mac OS,也就是 `x86_64` 架构下运行的,对于在 arm64 中运行的代码会特别说明。 -在上一篇分析 `isa` 的文章[从 NSObject 的初始化了解 isa](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/从%20NSObject%20的初始化了解%20isa.md) 中曾经说到过实例方法被调用时,会通过其持有 `isa` 指针寻找对应的类,然后在其中的 `class_data_bits_t` 中查找对应的方法,在这一篇文章中会介绍方法在 ObjC 中是如何存储方法的。 +在上一篇分析 `isa` 的文章[从 NSObject 的初始化了解 isa](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/从%20NSObject%20的初始化了解%20isa.md) 中曾经说到过实例方法被调用时,会通过其持有 `isa` 指针寻找对应的类,然后在其中的 `class_data_bits_t` 中查找对应的方法,在这一篇文章中会介绍方法在 ObjC 中是如何存储方法的。 这篇文章的首先会根据 ObjC 源代码来分析方法在内存中的存储结构,然后在 lldb 调试器中一步一步验证分析的正确性。 @@ -10,7 +10,6 @@ 先来了解一下 ObjC 中类的结构图: -<p align="center"> ![objc-method-class](../images/objc-method-class.png) + `isa` 是指向元类的指针,不了解元类的可以看 [Classes and Metaclasses](http://www.sealiesoftware.com/blog/archive/2009/04/14/objc_explain_Classes_and_metaclasses.html) @@ -24,7 +23,6 @@ 下面就是 ObjC 中 `class_data_bits_t` 的结构体,其中只含有一个 64 位的 `bits` 用于存储与类有关的信息: -<p align="center"> ![objc-method-class-data-bits-t](../images/objc-method-class-data-bits-t.png) 在 `objc_class` 结构体中的注释写到 `class_data_bits_t` 相当于 `class_rw_t` 指针加上 rr/alloc 的标志。 @@ -47,7 +45,6 @@ class_rw_t* data() { 因为 `class_rw_t *` 指针只存于第 `[3, 47]` 位,所以可以使用最后三位来存储关于当前类的其他信息: -<p align="center"> ![objc-method-class_data_bits_t](../images/objc-method-class_data_bits_t.png) @@ -126,7 +123,6 @@ struct class_ro_t { **在编译期间**类的结构中的 `class_data_bits_t *data` 指向的是一个 `class_ro_t *` 指针: -<p align='center'> ![objc-method-before-realize](../images/objc-method-before-realize.png) @@ -147,7 +143,6 @@ cls->setData(rw); 下图是 `realizeClass` 方法执行过后的类所占用内存的布局,你可以与上面调用方法前的内存布局对比以下,看有哪些更改: -<p align='center'> ![objc-method-after-realize-class](../images/objc-method-after-realize-class.png) 但是,在这段代码运行之后 `class_rw_t` 中的方法,属性以及协议列表均为空。这时需要 `realizeClass` 调用 `methodizeClass` 方法来**将类自己实现的方法(包括分类)、属性和遵循的协议加载到 `methods`、 `properties` 和 `protocols` 列表中**。 @@ -181,7 +176,6 @@ cls->setData(rw); > 这段代码是运行在 Mac OS X 10.11.3 (x86_64)版本中,而不是运行在 iPhone 模拟器或者真机上的,如果你在 iPhone 或者真机上运行,可能有一定差别。 -<p align='center'> ![objc-method-target](../images/objc-method-target.png) 这是主程序的代码: @@ -209,7 +203,6 @@ int main(int argc, const char * argv[]) { 接下来,在整个 ObjC 运行时初始化之前,也就是 `_objc_init` 方法中加入一个断点: -<p align="center"> ![objc-method-after-compile](../images/objc-method-after-compile.png) 然后在 lldb 中输入以下命令: @@ -240,7 +233,6 @@ warning: could not load any Objective-C class information. This will significant } ``` -<p align="center"> ![objc-method-lldb-print-before-realize](../images/objc-method-lldb-print-before-realize.png) 现在我们获取了类经过编译器处理后的只读属性 `class_ro_t`: @@ -281,7 +273,6 @@ The process has been returned to the state before expression evaluation. (lldb) ``` -<p align="center"> ![objc-method-lldb-print-method-list](../images/objc-method-lldb-print-method-list.png) 使用 `$5->get(0)` 时,成功获取到了 `-[XXObject hello]` 方法的结构体 `method_t`。而尝试获取下一个方法时,断言提示我们当前类只有一个方法。 @@ -299,14 +290,12 @@ static Class realizeClass(Class cls) 上面就是这个方法的签名,我们需要在这个方法中打一个条件断点,来判断当前类是否为 `XXObject`: -<p align="center"> ![objc-method-lldb-breakpoint](../images/objc-method-lldb-breakpoint.png) 这里直接判断两个指针是否相等,而不使用 `[NSStringFromClass(cls) isEqualToString:@"XXObject"]` 是因为在这个时间点,这些方法都不能调用,在 ObjC 中没有这些方法,所以只能通过判断类指针是否相等的方式来确认当前类是 `XXObject`。 > 直接与指针比较是因为类在内存中的位置是编译期确定的,只要代码不改变,类在内存中的位置就会不变(已经说过很多遍了)。 -<p align="center"> ![objc-method-breakpoint-before-set-r](../images/objc-method-breakpoint-before-set-rw.png) @@ -316,13 +305,11 @@ static Class realizeClass(Class cls) 在这时打印类结构体中的 `data` 的值,发现其中的布局依旧是这样的: -<p align="center"> ![objc-method-before-realize](../images/objc-method-before-realize.png) 在运行完这段代码之后: -<p align="center"> ![objc-method-after-realize-breakpoint](../images/objc-method-after-realize-breakpoint.png) 我们再来打印类的结构: @@ -400,7 +387,6 @@ Assertion failed: (i < count), function get, file /Users/apple/Desktop/objc-runt (lldb) ``` -<p align="center"> ![objc-method-print-class-struct-after-realize](../images/objc-method-print-class-struct-after-realize.png) > 最后一个操作实在是截取不到了 @@ -415,7 +401,6 @@ cls->setData(rw); 在上述的代码运行之后,类的只读指针 `class_ro_t` 以及可读写指针 `class_rw_t` 都被正确的设置了。但是到这里,其 `class_rw_t` 部分的方法等成员的指针 `methods`、 `protocols` 和 `properties` 均为空,这些会在 `methodizeClass` 中进行设置: -<p align="center"> ![objc-method-after-methodizeClass](../images/objc-method-after-methodizeClass.png) 在这里调用了 `method_array_t` 的 `attachLists` 方法,将 `baseMethods` 中的方法添加到 `methods` 数组之后。我们访问 `methods` 才会获取当前类的实例方法。 @@ -434,7 +419,6 @@ struct method_t { 其中包含方法名,类型还有方法的实现指针 `IMP`: -<p align="center"> ![obj-method-struct](../images/obj-method-struct.png) 上面的 `-[XXObject hello]` 方法的结构体是这样的: diff --git "a/objc/\350\207\252\345\212\250\351\207\212\346\224\276\346\261\240\347\232\204\345\211\215\344\270\226\344\273\212\347\224\237.md" "b/contents/objc/\350\207\252\345\212\250\351\207\212\346\224\276\346\261\240\347\232\204\345\211\215\344\270\226\344\273\212\347\224\237.md" similarity index 99% rename from "objc/\350\207\252\345\212\250\351\207\212\346\224\276\346\261\240\347\232\204\345\211\215\344\270\226\344\273\212\347\224\237.md" rename to "contents/objc/\350\207\252\345\212\250\351\207\212\346\224\276\346\261\240\347\232\204\345\211\215\344\270\226\344\273\212\347\224\237.md" index 1717739..9f222be 100644 --- "a/objc/\350\207\252\345\212\250\351\207\212\346\224\276\346\261\240\347\232\204\345\211\215\344\270\226\344\273\212\347\224\237.md" +++ "b/contents/objc/\350\207\252\345\212\250\351\207\212\346\224\276\346\261\240\347\232\204\345\211\215\344\270\226\344\273\212\347\224\237.md" @@ -2,8 +2,8 @@ > 由于 Objective-C 中的内存管理是一个比较大的话题,所以会分为两篇文章来对内存管理中的一些机制进行剖析,一部分分析自动释放池以及 `autorelease` 方法,另一部分分析 `retain`、`release` 方法的实现以及自动引用计数。 -+ [自动释放池的前世今生](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/自动释放池的前世今生.md) -+ [黑箱中的 retain 和 release](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/黑箱中的%20retain%20和%20release.md) ++ [自动释放池的前世今生](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/自动释放池的前世今生.md) ++ [黑箱中的 retain 和 release](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/黑箱中的%20retain%20和%20release.md) ## 写在前面 diff --git "a/objc/\351\273\221\347\256\261\344\270\255\347\232\204 retain \345\222\214 release.md" "b/contents/objc/\351\273\221\347\256\261\344\270\255\347\232\204 retain \345\222\214 release.md" similarity index 98% rename from "objc/\351\273\221\347\256\261\344\270\255\347\232\204 retain \345\222\214 release.md" rename to "contents/objc/\351\273\221\347\256\261\344\270\255\347\232\204 retain \345\222\214 release.md" index 6996cc3..f8471a8 100644 --- "a/objc/\351\273\221\347\256\261\344\270\255\347\232\204 retain \345\222\214 release.md" +++ "b/contents/objc/\351\273\221\347\256\261\344\270\255\347\232\204 retain \345\222\214 release.md" @@ -2,8 +2,8 @@ > 由于 Objective-C 中的内存管理是一个比较大的话题,所以会分为两篇文章来对内存管理中的一些机制进行剖析,一部分分析自动释放池以及 `autorelease` 方法,另一部分分析 `retain`、`release` 方法的实现以及自动引用计数。 -+ [自动释放池的前世今生](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/自动释放池的前世今生.md) -+ [黑箱中的 retain 和 release](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/黑箱中的%20retain%20和%20release.md) ++ [自动释放池的前世今生](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/自动释放池的前世今生.md) ++ [黑箱中的 retain 和 release](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/黑箱中的%20retain%20和%20release.md) ## 写在前面 @@ -359,7 +359,7 @@ inline uintptr_t objc_object::rootRetainCount() { + Objective-C 使用 `isa` 中的 `extra_rc` 和 `SideTable` 来存储对象的引用计数 + 在对象的引用计数归零时,会调用 `dealloc` 方法回收对象 -有关于自动释放池实现的介绍,可以看[自动释放池的前世今生](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/objc/自动释放池的前世今生.md)。 +有关于自动释放池实现的介绍,可以看[自动释放池的前世今生](https://github.com/Draveness/iOS-Source-Code-Analyze/blob/master/contents/objc/自动释放池的前世今生.md)。 > Follow: [Draveness · Github](https://github.com/Draveness) diff --git a/contents/rack/images/rack-thin/event-callback.png b/contents/rack/images/rack-thin/event-callback.png new file mode 100644 index 0000000..97e6423 Binary files /dev/null and b/contents/rack/images/rack-thin/event-callback.png differ diff --git a/contents/rack/images/rack-thin/eventmachine-select.png b/contents/rack/images/rack-thin/eventmachine-select.png new file mode 100644 index 0000000..8673de9 Binary files /dev/null and b/contents/rack/images/rack-thin/eventmachine-select.png differ diff --git a/contents/rack/images/rack-thin/reactor-eventloop.png b/contents/rack/images/rack-thin/reactor-eventloop.png new file mode 100644 index 0000000..eed3171 Binary files /dev/null and b/contents/rack/images/rack-thin/reactor-eventloop.png differ diff --git a/contents/rack/images/rack-thin/selectable-and-subclasses.png b/contents/rack/images/rack-thin/selectable-and-subclasses.png new file mode 100644 index 0000000..af1891c Binary files /dev/null and b/contents/rack/images/rack-thin/selectable-and-subclasses.png differ diff --git a/contents/rack/images/rack-thin/thin-handle-request.png b/contents/rack/images/rack-thin/thin-handle-request.png new file mode 100644 index 0000000..aaec961 Binary files /dev/null and b/contents/rack/images/rack-thin/thin-handle-request.png differ diff --git a/contents/rack/images/rack-thin/thin-initialize-server.png b/contents/rack/images/rack-thin/thin-initialize-server.png new file mode 100644 index 0000000..515c9d0 Binary files /dev/null and b/contents/rack/images/rack-thin/thin-initialize-server.png differ diff --git a/contents/rack/images/rack-thin/thin-io-model.png b/contents/rack/images/rack-thin/thin-io-model.png new file mode 100644 index 0000000..a5846b3 Binary files /dev/null and b/contents/rack/images/rack-thin/thin-io-model.png differ diff --git a/contents/rack/images/rack-thin/thin-send-response.png b/contents/rack/images/rack-thin/thin-send-response.png new file mode 100644 index 0000000..0685d5b Binary files /dev/null and b/contents/rack/images/rack-thin/thin-send-response.png differ diff --git a/contents/rack/images/rack-thin/thin-start-server.png b/contents/rack/images/rack-thin/thin-start-server.png new file mode 100644 index 0000000..ebdc972 Binary files /dev/null and b/contents/rack/images/rack-thin/thin-start-server.png differ diff --git a/contents/rack/images/rack-unicorn/unicorn-daemonize.png b/contents/rack/images/rack-unicorn/unicorn-daemonize.png new file mode 100644 index 0000000..2d4bb0b Binary files /dev/null and b/contents/rack/images/rack-unicorn/unicorn-daemonize.png differ diff --git a/contents/rack/images/rack-unicorn/unicorn-io-model.png b/contents/rack/images/rack-unicorn/unicorn-io-model.png new file mode 100644 index 0000000..b25d078 Binary files /dev/null and b/contents/rack/images/rack-unicorn/unicorn-io-model.png differ diff --git a/contents/rack/images/rack-unicorn/unicorn-multi-processes.png b/contents/rack/images/rack-unicorn/unicorn-multi-processes.png new file mode 100644 index 0000000..423398a Binary files /dev/null and b/contents/rack/images/rack-unicorn/unicorn-multi-processes.png differ diff --git a/contents/rack/images/rack-unicorn/unicorn.jpeg b/contents/rack/images/rack-unicorn/unicorn.jpeg new file mode 100644 index 0000000..6df4100 Binary files /dev/null and b/contents/rack/images/rack-unicorn/unicorn.jpeg differ diff --git a/contents/rack/images/rack-webrick/mounttable-and-applications.png b/contents/rack/images/rack-webrick/mounttable-and-applications.png new file mode 100644 index 0000000..0a3b6b3 Binary files /dev/null and b/contents/rack/images/rack-webrick/mounttable-and-applications.png differ diff --git a/contents/rack/images/rack-webrick/webrick-io-model.png b/contents/rack/images/rack-webrick/webrick-io-model.png new file mode 100644 index 0000000..4092d1b Binary files /dev/null and b/contents/rack/images/rack-webrick/webrick-io-model.png differ diff --git a/contents/rack/images/rack/rack-and-web-servers-frameworks.png b/contents/rack/images/rack/rack-and-web-servers-frameworks.png new file mode 100644 index 0000000..51163df Binary files /dev/null and b/contents/rack/images/rack/rack-and-web-servers-frameworks.png differ diff --git a/contents/rack/images/rack/rack-app.png b/contents/rack/images/rack/rack-app.png new file mode 100644 index 0000000..c2c7966 Binary files /dev/null and b/contents/rack/images/rack/rack-app.png differ diff --git a/contents/rack/images/rack/rack-logo.png b/contents/rack/images/rack/rack-logo.png new file mode 100644 index 0000000..f5bba1a Binary files /dev/null and b/contents/rack/images/rack/rack-logo.png differ diff --git a/contents/rack/images/rack/rack-protocol.png b/contents/rack/images/rack/rack-protocol.png new file mode 100644 index 0000000..b5ae951 Binary files /dev/null and b/contents/rack/images/rack/rack-protocol.png differ diff --git a/contents/rack/images/rack/rails-application.png b/contents/rack/images/rack/rails-application.png new file mode 100644 index 0000000..0fe07d9 Binary files /dev/null and b/contents/rack/images/rack/rails-application.png differ diff --git a/contents/rack/images/rack/server-app-call-stack.png b/contents/rack/images/rack/server-app-call-stack.png new file mode 100644 index 0000000..8dde02d Binary files /dev/null and b/contents/rack/images/rack/server-app-call-stack.png differ diff --git a/contents/rack/images/rack/wrapped-app.png b/contents/rack/images/rack/wrapped-app.png new file mode 100644 index 0000000..2b0e12a Binary files /dev/null and b/contents/rack/images/rack/wrapped-app.png differ diff --git a/contents/rack/rack-thin.md b/contents/rack/rack-thin.md new file mode 100644 index 0000000..99ceaf9 --- /dev/null +++ b/contents/rack/rack-thin.md @@ -0,0 +1,786 @@ +# 浅谈 Thin 的事件驱动模型 + ++ [谈谈 Rack 协议与实现](https://draveness.me/rack) ++ [浅谈 WEBrick 的实现](https://draveness.me/rack-webrick) ++ [浅谈 Thin 的事件驱动模型](https://draveness.me/rack-thin) ++ [浅谈 Unicorn 的多进程模型](https://draveness.me/rack-unicorn) ++ [浅谈 Puma 的实现](https://draveness.me/rack-puma) + +在上一篇文章中我们已经介绍了 WEBrick 的实现,它的 handler 是写在 Rack 工程中的,而在这篇文章介绍的 webserver [thin](https://github.com/macournoyer/thin) 的 Rack 处理器也是写在 Rack 中的;与 WEBrick 相同,Thin 的实现也非常简单,官方对它的介绍是: + +> A very fast & simple Ruby web server. + +它将 [Mongrel](https://zedshaw.com/archive/ragel-state-charts/)、[Event Machine](https://github.com/eventmachine/eventmachine) 和 [Rack](http://rack.github.io) 三者进行组合,在其中起到胶水的作用,所以在理解 Thin 的实现的过程中我们也需要分析 EventMachine 到底是如何工作的。 + +## Thin 的实现 + +在这一节中我们将从源代码来分析介绍 Thin 的实现原理,因为部分代码仍然是在 Rack 工程中实现的,所以我们要从 Rack 工程的代码开始理解 Thin 的实现。 + +### 从 Rack 开始 + +Thin 的处理器 `Rack::Handler::Thin` 与其他遵循 Rack 协议的 webserver 一样都实现了 `.run` 方法,接受 Rack 应用和 `options` 作为输入: + +```ruby +module Rack + module Handler + class Thin + def self.run(app, options={}) + environment = ENV['RACK_ENV'] || 'development' + default_host = environment == 'development' ? 'localhost' : '0.0.0.0' + + host = options.delete(:Host) || default_host + port = options.delete(:Port) || 8080 + args = [host, port, app, options] + args.pop if ::Thin::VERSION::MAJOR < 1 && ::Thin::VERSION::MINOR < 8 + server = ::Thin::Server.new(*args) + yield server if block_given? + server.start + end + end + end +end +``` + +上述方法仍然会从 `options` 中取出 ip 地址和端口号,然后初始化一个 `Thin::Server` 的实例后,执行 `#start` 方法在 8080 端口上监听来自用户的请求。 + +### 初始化服务 + +Thin 服务的初始化由以下的代码来处理,首先会处理在 `Rack::Handler::Thin.run` 中传入的几个参数 `host`、`port`、`app` 和 `options`,将 Rack 应用存储在临时变量中: + +```ruby +From: lib/thin/server.rb @ line 100: +Owner: Thin::Server + +def initialize(*args, &block) + host, port, options = DEFAULT_HOST, DEFAULT_PORT, {} + + args.each do |arg| + case arg + when 0.class, /^\d+$/ then port = arg.to_i + when String then host = arg + when Hash then options = arg + else + @app = arg if arg.respond_to?(:call) + end + end + + @backend = select_backend(host, port, options) + @backend.server = self + @backend.maximum_connections = DEFAULT_MAXIMUM_CONNECTIONS + @backend.maximum_persistent_connections = DEFAULT_MAXIMUM_PERSISTENT_CONNECTIONS + @backend.timeout = options[:timeout] || DEFAULT_TIMEOUT + + @app = Rack::Builder.new(&block).to_app if block +end +``` + +在初始化服务的过程中,总共只做了三件事情,处理参数、选择并配置 `backend`,创建新的应用: + +![thin-initialize-serve](images/rack-thin/thin-initialize-server.png) + +处理参数的过程自然不用多说,只是这里判断的方式并不是按照顺序处理的,而是按照参数的类型;在初始化器的最后,如果向初始化器传入了 block,那么就会使用 `Rack::Builder` 和 block 中的代码初始化一个新的 Rack 应用。 + +### 选择后端 + +在选择后端时 Thin 使用了 `#select_backend` 方法,这里使用 `case` 语句替代多个 `if`、`else`,也是一个我们可以使用的小技巧: + +```ruby +From: lib/thin/server.rb @ line 261: +Owner: Thin::Server + +def select_backend(host, port, options) + case + when options.has_key?(:backend) + raise ArgumentError, ":backend must be a class" unless options[:backend].is_a?(Class) + options[:backend].new(host, port, options) + when options.has_key?(:swiftiply) + Backends::SwiftiplyClient.new(host, port, options) + when host.include?('/') + Backends::UnixServer.new(host) + else + Backends::TcpServer.new(host, port) + end +end +``` + +在大多数时候,我们只会选择 `UnixServer` 和 `TcpServer` 两种后端中的一个,而后者又是两者中使用更为频繁的后端: + +```ruby +From: lib/thin/backends/tcp_server.rb @ line 8: +Owner: Thin::Backends::TcpServer + +def initialize(host, port) + @host = host + @port = port + super() +end + +From: lib/thin/backends/base.rb @ line 47: +Owner: Thin::Backends::Base + +def initialize + @connections = {} + @timeout = Server::DEFAULT_TIMEOUT + @persistent_connection_count = 0 + @maximum_connections = Server::DEFAULT_MAXIMUM_CONNECTIONS + @maximum_persistent_connections = Server::DEFAULT_MAXIMUM_PERSISTENT_CONNECTIONS + @no_epoll = false + @ssl = nil + @threaded = nil + @started_reactor = false +end +``` + +初始化的过程中只是对属性设置默认值,比如 `host`、`port` 以及超时时间等等,并没有太多值得注意的代码。 + +### 启动服务 + +在启动服务时会直接调用 `TcpServer#start` 方法并在其中传入一个用于处理信号的 block: + +```ruby +From: lib/thin/server.rb @ line 152: +Owner: Thin::Server + +def start + raise ArgumentError, 'app required' unless @app + + log_info "Thin web server (v#{VERSION::STRING} codename #{VERSION::CODENAME})" + log_debug "Debugging ON" + trace "Tracing ON" + + log_info "Maximum connections set to #{@backend.maximum_connections}" + log_info "Listening on #{@backend}, CTRL+C to stop" + + @backend.start { setup_signals if @setup_signals } +end +``` + +虽然这里的 `backend` 其实已经被选择成了 `TcpServer`,但是该子类并没有覆写 `#start` 方法,这里执行的方法其实是从父类继承的: + +```ruby +From: lib/thin/backends/base.rb @ line 60: +Owner: Thin::Backends::Base + +def start + @stopping = false + starter = proc do + connect + yield if block_given? + @running = true + end + + # Allow for early run up of eventmachine. + if EventMachine.reactor_running? + starter.call + else + @started_reactor = true + EventMachine.run(&starter) + end +end +``` + +上述方法在构建一个 `starter` block 之后,将该 block 传入 `EventMachine.run` 方法,随后执行的 `#connect` 会启动一个 `EventMachine` 的服务器用于处理用户的网络请求: + +```ruby +From: lib/thin/backends/tcp_server.rb @ line 15: +Owner: Thin::Backends::TcpServer + +def connect + @signature = EventMachine.start_server(@host, @port, Connection, &method(:initialize_connection)) + binary_name = EventMachine.get_sockname( @signature ) + port_name = Socket.unpack_sockaddr_in( binary_name ) + @port = port_name[0] + @host = port_name[1] + @signature +end +``` + +在 EventMachine 的文档中,`.start_server` 方法被描述成一个在指定的地址和端口上初始化 TCP 服务的方法,正如这里所展示的,它经常在 `.run` 方法的 block 中执行;该方法的参数 `Connection` 作为处理 TCP 请求的类,会实现不同的方法接受各种各样的回调,传入的 `initialize_connection` block 会在有请求需要处理时对 `Connection` 对象进行初始化: + +> `Connection` 对象继承自 `EventMachine::Connection`,是 EventMachine 与外界的接口,在 EventMachine 中的大部分事件都会调用 `Connection` 的一个实例方法来传递数据和参数。 + +```ruby +From: lib/thin/backends/base.rb @ line 145: +Owner: Thin::Backends::Base + +def initialize_connection(connection) + connection.backend = self + connection.app = @server.app + connection.comm_inactivity_timeout = @timeout + connection.threaded = @threaded + connection.start_tls(@ssl_options) if @ssl + + if @persistent_connection_count < @maximum_persistent_connections + connection.can_persist! + @persistent_connection_count += 1 + end + @connections[connection.__id__] = connection +end +``` + +### 处理请求的连接 + +`Connection` 类中有很多的方法 `#post_init`、`#receive_data` 方法等等都是由 EventMachine 在接收到请求时调用的,当 Thin 的服务接收到来自客户端的数据时就会调用 `#receive_data` 方法: + +```ruby +From: lib/thin/connection.rb @ line 36: +Owner: Thin::Connection + +def receive_data(data) + @idle = false + trace data + process if @request.parse(data) +rescue InvalidRequest => e + log_error("Invalid request", e) + post_process Response::BAD_REQUEST +end +``` + +在这里我们看到了与 WEBrick 在处理来自客户端的原始数据时使用的方法 `#parse`,它会解析客户端请求的原始数据并执行 `#process` 来处理 HTTP 请求: + +```ruby +From: lib/thin/connection.rb @ line 47: +Owner: Thin::Connection + +def process + if threaded? + @request.threaded = true + EventMachine.defer { post_process(pre_process) } + else + @request.threaded = false + post_process(pre_process) + end +end +``` + +如果当前的连接允许并行处理多个用户的请求,那么就会在 `EventMachine.defer` 的 block 中执行两个方法 `#pre_process` 和 `#post_process`: + +```ruby +From: lib/thin/connection.rb @ line 63: +Owner: Thin::Connection + +def pre_process + @request.remote_address = remote_address + @request.async_callback = method(:post_process) + + response = AsyncResponse + catch(:async) do + response = @app.call(@request.env) + end + response +rescue Exception => e + unexpected_error(e) + can_persist? && @request.persistent? ? Response::PERSISTENT_ERROR : Response::ERROR +end +``` + +在 `#pre_process` 中没有做太多的事情,只是调用了 Rack 应用的 `#call` 方法,得到一个三元组 `response`,在这之后将这个数组传入 `#post_process` 方法: + +```ruby +From: lib/thin/connection.rb @ line 95: +Owner: Thin::Connection + +def post_process(result) + return unless result + result = result.to_a + return if result.first == AsyncResponse.first + + @response.status, @response.headers, @response.body = *result + @response.each do |chunk| + send_data chunk + end +rescue Exception => e + unexpected_error(e) + close_connection +ensure + if @response.body.respond_to?(:callback) && @response.body.respond_to?(:errback) + @response.body.callback { terminate_request } + @response.body.errback { terminate_request } + else + terminate_request unless result && result.first == AsyncResponse.first + end +end +``` + +`#post_response` 方法将传入的数组赋值给 `response` 的 `status`、`headers` 和 `body` 这三部分,在这之后通过 `#send_data` 方法将 HTTP 响应以块的形式写回 Socket;写回结束后可能会调用对应的 `callback` 并关闭持有的 `request` 和 `response` 两个实例变量。 + +> 上述方法中调用的 `#send_data` 继承自 `EventMachine::Connection` 类。 + +### 小结 + +到此为止,我们对于 Thin 是如何处理来自用户的 HTTP 请求的就比较清楚了,我们可以看到 Thin 本身并没有做一些类似解析 HTTP 数据包以及发送数据的问题,它使用了来自 Rack 和 EventMachine 两个开源框架中很多已有的代码逻辑,确实只做了一些胶水的事情。 + +对于 Rack 是如何工作的我们在前面的文章 [谈谈 Rack 协议与实现](https://draveness.me/rack) 中已经介绍过了;虽然我们看到了很多与 EventMachine 相关的代码,但是到这里我们仍然对 EventMachine 不是太了解。 + +## EventMachine 和 Reactor 模式 + +为了更好地理解 Thin 的工作原理,在这里我们会介绍一个 EventMachine 和 Reactor 模式。 + +EventMachine 其实是一个使用 Ruby 实现的事件驱动的并行框架,它使用 Reactor 模式提供了事件驱动的 IO 模型,如果你对 Node.js 有所了解的话,那么你一定对事件驱动这个词并不陌生,EventMachine 的出现主要是为了解决两个核心问题: + ++ 为生产环境提供更高的可伸缩性、更好的性能和稳定性; ++ 为上层提供了一些能够减少高性能的网络编程复杂性的 API; + +其实 EventMachine 的主要作用就是将所有同步的 IO 都变成异步的,调度都通过事件来进行,这样用于监听用户请求的进程不会被其他代码阻塞,能够同时为更多的客户端提供服务;在这一节中,我们需要了解一下在 Thin 中使用的 EventMachine 中几个常用方法的实现。 + +### 启动事件循环 + +EventMachine 其实就是一个事件循环(Event Loop),当我们想使用 EventMachine 来处理某些任务时就一定需要调用 `.run` 方法启动这个事件循环来接受外界触发的各种事件: + +```ruby +From: lib/eventmachine.rb @ line 149: +Owner: #<Class:EventMachine> + +def self.run blk=nil, tail=nil, &block + # ... + begin + @reactor_pid = Process.pid + @reactor_running = true + initialize_event_machine + (b = blk || block) and add_timer(0, b) + if @next_tick_queue && !@next_tick_queue.empty? + add_timer(0) { signal_loopbreak } + end + @reactor_thread = Thread.current + + run_machine + ensure + until @tails.empty? + @tails.pop.call + end + + release_machine + cleanup_machine + @reactor_running = false + @reactor_thread = nil + end +end +``` + +在这里我们会使用 `.initialize_event_machine` 初始化当前的事件循环,其实也就是一个全局的 `Reactor` 的单例,最终会执行 `Reactor#initialize_for_run` 方法: + +```ruby +From: lib/em/pure_ruby.rb @ line 522: +Owner: EventMachine::Reactor + +def initialize_for_run + @running = false + @stop_scheduled = false + @selectables ||= {}; @selectables.clear + @timers = SortedSet.new # [] + set_timer_quantum(0.1) + @current_loop_time = Time.now + @next_heartbeat = @current_loop_time + HeartbeatInterval +end +``` + +在启动事件循环的过程中,它还会将传入的 block 与一个 `interval` 为 0 的键组成键值对存到 `@timers` 字典中,所有加入的键值对都会在大约 `interval` 的时间过后执行一次 block。 + +随后执行的 `#run_machine` 在最后也会执行 `Reactor` 的 `#run` 方法,该方法中包含一个 loop 语句,也就是我们一直说的事件循环: + +```ruby +From: lib/em/pure_ruby.rb @ line 540: +Owner: EventMachine::Reactor + +def run + raise Error.new( "already running" ) if @running + @running = true + + begin + open_loopbreaker + + loop { + @current_loop_time = Time.now + + break if @stop_scheduled + run_timers + break if @stop_scheduled + crank_selectables + break if @stop_scheduled + run_heartbeats + } + ensure + close_loopbreaker + @selectables.each {|k, io| io.close} + @selectables.clear + + @running = false + end +end +``` + +在启动事件循环之间会在 `#open_loopbreaker` 中创建一个 `LoopbreakReader` 的实例绑定在 `127.0.0.1` 和随机的端口号组成的地址上,然后开始运行事件循环。 + +![reactor-eventloop](images/rack-thin/reactor-eventloop.png) + +在事件循环中,Reactor 总共需要执行三部分的任务,分别是执行定时器、处理 Socket 上的事件以及运行心跳方法。 + +无论是运行定时器还是执行心跳方法其实都非常简单,只要与当前时间进行比较,如果到了触发的时间就调用正确的方法或者回调,最后的 `#crank_selectables` 方法就是用于处理 Socket 上读写事件的方法了: + +```ruby +From: lib/em/pure_ruby.rb @ line 540: +Owner: EventMachine::Reactor + +def crank_selectables + readers = @selectables.values.select { |io| io.select_for_reading? } + writers = @selectables.values.select { |io| io.select_for_writing? } + + s = select(readers, writers, nil, @timer_quantum) + + s and s[1] and s[1].each { |w| w.eventable_write } + s and s[0] and s[0].each { |r| r.eventable_read } + + @selectables.delete_if {|k,io| + if io.close_scheduled? + io.close + begin + EventMachine::event_callback io.uuid, ConnectionUnbound, nil + rescue ConnectionNotBound; end + true + end + } +end +``` + +上述方法会在 Socket 变成可读或者可写时执行 `#eventable_write` 或 `#eventable_read` 执行事件的回调,我们暂时放下这两个方法,先来了解一下 EventMachine 是如何启动服务的。 + +### 启动服务 + +在启动服务的过程中,最重要的目的就是创建一个 Socket 并绑定在指定的 ip 和端口上,在实现这个目的的过程中,我们使用了以下的几个方法,首先是 `EventMachine.start_server`: + +```ruby +From: lib/eventmachine.rb @ line 516: +Owner: #<Class:EventMachine> + +def self.start_server server, port=nil, handler=nil, *args, &block + port = Integer(port) + klass = klass_from_handler(Connection, handler, *args) + + s = if port + start_tcp_server server, port + else + start_unix_server server + end + @acceptors[s] = [klass, args, block] + s +end +``` + +该方法其实使我们在使用 EventMachine 时常见的接口,只要我们想要启动一个新的 TCP 或者 UNIX 服务器,就可以上述方法,在这里会根据端口号是否存在,选择执行 `.start_tcp_server` 或者 `.start_unix_server` 创建一个新的 Socket 并存储在 `@acceptors` 中: + +```ruby +From: lib/em/pure_ruby.rb @ line 184: +Owner: #<Class:EventMachine> + +def self.start_tcp_server host, port + (s = EvmaTCPServer.start_server host, port) or raise "no acceptor" + s.uuid +end +``` + +`EventMachine.start_tcp_server` 在这里也只做了个『转发』方法的作用的,直接调用 `EvmaTCPServer.start_server` 创建一个新的 Socket 对象并绑定到传入的 `<host, port>` 上: + +```ruby +From: lib/em/pure_ruby.rb @ line 1108: +Owner: #<Class:EventMachine::EvmaTCPServer> + +def self.start_server host, port + sd = Socket.new( Socket::AF_LOCAL, Socket::SOCK_STREAM, 0 ) + sd.setsockopt( Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true ) + sd.bind( Socket.pack_sockaddr_in( port, host )) + sd.listen( 50 ) # 5 is what you see in all the books. Ain't enough. + EvmaTCPServer.new sd +end +``` + +方法的最后会创建一个新的 `EvmaTCPServer` 实例的过程中,我们需要通过 `#fcntl` 将 Socket 变成非阻塞式的: + +```ruby +From: lib/em/pure_ruby.rb @ line 687: +Owner: EventMachine::Selectable + +def initialize io + @io = io + @uuid = UuidGenerator.generate + @is_server = false + @last_activity = Reactor.instance.current_loop_time + + m = @io.fcntl(Fcntl::F_GETFL, 0) + @io.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK | m) + + @close_scheduled = false + @close_requested = false + + se = self; @io.instance_eval { @my_selectable = se } + Reactor.instance.add_selectable @io +end +``` + +不只是 `EvmaTCPServer`,所有的 `Selectable` 子类在初始化的最后都会将新的 Socket 以 `uuid` 为键存储到 `Reactor` 单例对象的 `@selectables` 字典中: + +```ruby +From: lib/em/pure_ruby.rb @ line 532: +Owner: EventMachine::Reactor + +def add_selectable io + @selectables[io.uuid] = io +end +``` + +在整个事件循环的大循环中,这里存入的所有 Socket 都会被 `#select` 方法监听,在响应的事件发生时交给合适的回调处理,作者在 [Redis 中的事件循环](https://draveness.me/redis-eventloop) 一文中也介绍过非常相似的处理过程。 + +![eventmachine-select](images/rack-thin/eventmachine-select.png) + +所有的 Socket 都会存储在一个 `@selectables` 的哈希中并由 `#select` 方法监听所有的读写事件,一旦相应的事件触发就会通过 `eventable_read` 或者 `eventable_write` 方法来响应该事件。 + +### 处理读写事件 + +所有的读写事件都是通过 `Selectable` 和它的子类来处理的,在 EventMachine 中,总共有以下的几种子类: + +![selectable-and-subclasses](images/rack-thin/selectable-and-subclasses.png) + +所有处理服务端读写事件的都是 `Selectable` 的子类,也就是 `EvmaTCPServer` 和 `EvmaUNIXServer`,而所有处理客户端读写事件的都是 `StreamObject` 的子类 `EvmaTCPServer` 和 `EvmaUNIXClient`。 + +当我们初始化的绑定在 `<host, port>` 上的 Socket 对象监听到了来自用户的 TCP 请求时,当前的 Socket 就会变得可读,事件循环中的 `#select` 方法就会调用 `EvmaTCPClient#eventable_read` 通知由一个请求需要处理: + +```ruby +From: lib/em/pure_ruby.rb @ line 1130: +Owner: EventMachine::EvmaTCPServer + +def eventable_read + begin + 10.times { + descriptor, peername = io.accept_nonblock + sd = EvmaTCPClient.new descriptor + sd.is_server = true + EventMachine::event_callback uuid, ConnectionAccepted, sd.uuid + } + rescue Errno::EWOULDBLOCK, Errno::EAGAIN + end +end +``` + +在这里会尝试多次 `#accept_non_block` 当前的 Socket 并会创建一个 TCP 的客户端对象 `EvmaTCPClient`,同时通过 `.event_callback` 方法发送 `ConnectionAccepted` 消息。 + +`EventMachine::event_callback` 就像是一个用于处理所有事件的中心方法,所有的回调都要通过这个中继器进行调度,在实现上就是一个庞大的 `if`、`else` 语句,里面处理了 EventMachine 中可能出现的 10 种状态和操作: + +![event-callback](images/rack-thin/event-callback.png) + +大多数事件在触发时,都会从 `@conns` 中取出相应的 `Connection` 对象,最后执行合适的方法来处理,而这里触发的 `ConnectionAccepted` 事件是通过以下的代码来处理的: + +```ruby +From: lib/eventmachine.rb @ line 1462: +Owner: #<Class:EventMachine> + +def self.event_callback conn_binding, opcode, data + if opcode == # ... + # ... + elsif opcode == ConnectionAccepted + accep, args, blk = @acceptors[conn_binding] + raise NoHandlerForAcceptedConnection unless accep + c = accep.new data, *args + @conns[data] = c + blk and blk.call(c) + c + else + # ... + end +end +``` + +上述的 `accep` 变量就是我们在 Thin 调用 `.start_server` 时传入的 `Connection` 类,在这里我们初始化了一个新的实例,同时以 Socket 的 `uuid` 作为键存到 `@conns` 中。 + +在这之后 `#select` 方法就会监听更多 Socket 上的事件了,当这个 "accept" 后创建的 Socket 接收到数据时,就会触发下面的 `#eventable_read` 方法: + +```ruby +From: lib/em/pure_ruby.rb @ line 1130: +Owner: EventMachine::StreamObject + +def eventable_read + @last_activity = Reactor.instance.current_loop_time + begin + if io.respond_to?(:read_nonblock) + 10.times { + data = io.read_nonblock(4096) + EventMachine::event_callback uuid, ConnectionData, data + } + else + data = io.sysread(4096) + EventMachine::event_callback uuid, ConnectionData, data + end + rescue Errno::EAGAIN, Errno::EWOULDBLOCK, SSLConnectionWaitReadable + rescue Errno::ECONNRESET, Errno::ECONNREFUSED, EOFError, Errno::EPIPE, OpenSSL::SSL::SSLError + @close_scheduled = true + EventMachine::event_callback uuid, ConnectionUnbound, nil + end +end +``` + +方法会从 Socket 中读取数据并通过 `.event_callback` 发送 `ConnectionData` 事件: + +```ruby +From: lib/eventmachine.rb @ line 1462: +Owner: #<Class:EventMachine> + +def self.event_callback conn_binding, opcode, data + if opcode == # ... + # ... + elsif opcode == ConnectionData + c = @conns[conn_binding] or raise ConnectionNotBound, "received data #{data} for unknown signature: #{conn_binding}" + c.receive_data data + else + # ... + end +end +``` + +从上述方法对 `ConnectionData` 事件的处理就可以看到通过传入 Socket 的 `uuid` 和数据,就可以找到上面初始化的 `Connection` 对象,`#receive_data` 方法就能够将数据传递到上层,让用户在自定义的 `Connection` 中实现自己的处理逻辑,这也就是 Thin 需要覆写 `#receive_data` 方法来接受数据的原因了。 + +当 Thin 以及 Rack 应用已经接收到了来自用户的请求、完成处理并返回之后经过一系列复杂的调用栈就会执行 `Connection#send_data` 方法: + +```ruby +From: lib/em/connection.rb @ line 324: +Owner: EventMachine::Connection + +def send_data data + data = data.to_s + size = data.bytesize if data.respond_to?(:bytesize) + size ||= data.size + EventMachine::send_data @signature, data, size +end + +From: lib/em/pure_ruby.rb @ line 172: +Owner: #<Class:EventMachine> + +def self.send_data target, data, datalength + selectable = Reactor.instance.get_selectable( target ) or raise "unknown send_data target" + selectable.send_data data +end + +From: lib/em/pure_ruby.rb @ line 851: +Owner: EventMachine::StreamObject + +def send_data data + unless @close_scheduled or @close_requested or !data or data.length <= 0 + @outbound_q << data.to_s + end +end +``` + +经过一系列同名方法的调用,在调用栈末尾的 `StreamObject#send_data` 中,将所有需要写入的数据全部加入 `@outbound_q` 中,这其实就是一个待写入数据的队列。 + +当 Socket 变得可写之后,就会由 `#select` 方法触发 `#eventable_write` 将 `@outbound_q` 队列中的数据通过 `#write_nonblock` 或者 `syswrite` 写入 Socket,也就是将请求返回给客户端。 + +```ruby +From: lib/em/pure_ruby.rb @ line 823: +Owner: EventMachine::StreamObject + +def eventable_write + @last_activity = Reactor.instance.current_loop_time + while data = @outbound_q.shift do + begin + data = data.to_s + w = if io.respond_to?(:write_nonblock) + io.write_nonblock data + else + io.syswrite data + end + + if w < data.length + @outbound_q.unshift data[w..-1] + break + end + rescue Errno::EAGAIN, SSLConnectionWaitReadable, SSLConnectionWaitWritable + @outbound_q.unshift data + break + rescue EOFError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EPIPE, OpenSSL::SSL::SSLError + @close_scheduled = true + @outbound_q.clear + end + end +end +``` + +### 关闭 Socket + +当数据写入时发生了 `EOFError` 或者其他错误时就会将 `close_scheduled` 标记为 `true`,在随后的事件循环中会关闭 Socket 并发送 `ConnectionUnbound` 事件: + +```ruby +From: lib/em/pure_ruby.rb @ line 540: +Owner: EventMachine::Reactor + +def crank_selectables + # ... + + @selectables.delete_if {|k,io| + if io.close_scheduled? + io.close + begin + EventMachine::event_callback io.uuid, ConnectionUnbound, nil + rescue ConnectionNotBound; end + true + end + } +end +``` + +`.event_callback` 在处理 `ConnectionUnbound` 事件时会在 `@conns` 中将结束的 `Connection` 剔除: + +```ruby +def self.event_callback conn_binding, opcode, data + if opcode == ConnectionUnbound + if c = @conns.delete( conn_binding ) + c.unbind + io = c.instance_variable_get(:@io) + begin + io.close + rescue Errno::EBADF, IOError + end + elsif c = @acceptors.delete( conn_binding ) + else + raise ConnectionNotBound, "received ConnectionUnbound for an unknown signature: #{conn_binding}" + end + elsif opcode = 1 + #... + end +end +``` + +在这之后会调用 `Connection` 的 `#unbind` 方法,再次执行 `#close` 确保 Socket 连接已经断掉了。 + +### 小结 + +EventMachine 在处理用户的请求时,会通过一个事件循环和一个中心化的事件处理中心 `.event_callback` 来响应所有的事件,你可以看到在使用 EventMachine 时所有的响应都是异步的,尤其是对 Socket 的读写,所有外部的输入在 EventMachine 看来都是一个事件,它们会被 EventMachine 选择合适的处理器进行转发。 + +## I/O 模型 + +Thin 本身其实没有实现任何的 I/O 模型,它通过对 EventMachine 进行封装,使用了其事件驱动的特点,为上层提供了处理并发 I/O 的 Reactor 模型,在不同的阶段有着不同的工作流程,在启动 Thin 的服务时,Thin 会直接通过 `.start_server` 创建一个 Socket 监听一个 `<host, port>` 组成的元组: + +![thin-start-server](images/rack-thin/thin-start-server.png) + +当服务启动之后,就可以接受来自客户端的 HTTP 请求了,处理 HTTP 请求总共需要三个模块的合作,分别是 EventMachine、Thin 以及 Rack 应用: + +![thin-handle-request](images/rack-thin/thin-handle-request.png) + +在上图中省略了 Rack 的处理部分,不过对于其他部分的展示还是比较详细的,EventMachine 负责对 TCP Socket 进行监听,在发生事件时通过 `.event_callback` 进行处理,将消息转发给位于 Thin 中的 `Connection`,该类以及模块负责处理 HTTP 协议相关的内容,将整个请求包装成一个 `env` 对象,调用 `#call` 方法。 + +在这时就开始了返回响应的逻辑了,`#call` 方法会返回一个三元组,经过 Thin 中的 `#send_data` 最终将数据写入 `outbound_q` 队列中等待处理: + +![thin-send-response](images/rack-thin/thin-send-response.png) + +EventMachine 会通过一个事件循环,使用 `#select` 监听当前 Socket 的可读写状态,并在合适的时候触发 `#eventable_write` 从 `outbound_q` 队列中读取数据写入 Socket,在写入结束后 Socket 就会被关闭,整个请求的响应也就结束了。 + +![thin-io-model](images/rack-thin/thin-io-model.png) + +Thin 使用了 EventMachine 作为底层处理 TCP 协议的框架,提供了事件驱动的 I/O 模型,也就是我们理解的 Reactor 模型,对于每一个 HTTP 请求都会创建一个对应的 `Connection` 对象,所有的事件都由 EventMachine 来派发,最大程度做到了 I/O 的读写都是异步的,不会阻塞当前的线程,这也是 Thin 以及 Node.js 能够并发处理大量请求的原因。 + +## 总结 + +Thin 作为一个 Ruby 社区中简单的 webserver,其实本身没有做太多的事情,只是使用了 EventMachine 提供的事件驱动的 I/O 模型,为上层提供了更加易用的 API,相比于其他同步处理请求的 webserver,Reactor 模式的优点就是 Thin 的优点,主程序只负责监听事件和分发事件,一旦涉及到 I/O 的工作都尽量使用回调的方式处理,当回调完成后再发送通知,这种方式能够减少进程的等待时间,时刻都在处理用户的请求和事件。 + +## Reference + ++ [Ragel State Charts](https://zedshaw.com/archive/ragel-state-charts/) ++ [Ragel State Machine Compiler](http://www.colm.net/open-source/ragel/) ++ [Ruby EventMachine - The Speed Demon](https://www.igvita.com/2008/05/27/ruby-eventmachine-the-speed-demon/) + diff --git a/contents/rack/rack-unicorn.md b/contents/rack/rack-unicorn.md new file mode 100644 index 0000000..576ab74 --- /dev/null +++ b/contents/rack/rack-unicorn.md @@ -0,0 +1,640 @@ +# 浅谈 Unicorn 的多进程模型 + ++ [谈谈 Rack 协议与实现](https://draveness.me/rack) ++ [浅谈 WEBrick 的实现](https://draveness.me/rack-webrick) ++ [浅谈 Thin 的事件驱动模型](https://draveness.me/rack-thin) ++ [浅谈 Unicorn 的多进程模型](https://draveness.me/rack-unicorn) ++ [浅谈 Puma 的实现](https://draveness.me/rack-puma) + +作为 Ruby 社区中老牌的 webserver,在今天也有很多开发者在生产环境使用 Unicorn 处理客户端的发出去的 HTTP 请求,与 WEBrick 和 Thin 不同,Unicorn 使用了完全不同的模型,提供了多进程模型批量处理来自客户端的请求。 + +![unicorn](images/rack-unicorn/unicorn.jpeg) + +Unicorn 为 Rails 应用提供并发的方式是使用 `fork` 创建多个 worker 线程,监听同一个 Socket 上的输入。 + +> 本文中使用的是 5.3.1 的 Unicorn,如果你使用了不同版本的 Unicorn,原理上的区别不会太大,只是在一些方法的实现上会有一些细微的不同。 + +## 实现原理 + +Unicorn 虽然也是一个遵循 Rack 协议的 Ruby webserver,但是因为它本身并没有提供 Rack 处理器,随意没有办法直接通过 `rackup -s Unicorn` 来启动 Unicorn 的进程。 + +```ruby +$ unicorn -c unicorn.rb +I, [2017-11-06T08:05:03.082116 #33222] INFO -- : listening on addr=0.0.0.0:8080 fd=10 +I, [2017-11-06T08:05:03.082290 #33222] INFO -- : worker=0 spawning... +I, [2017-11-06T08:05:03.083505 #33222] INFO -- : worker=1 spawning... +I, [2017-11-06T08:05:03.083989 #33222] INFO -- : master process ready +I, [2017-11-06T08:05:03.084610 #33223] INFO -- : worker=0 spawned pid=33223 +I, [2017-11-06T08:05:03.085100 #33223] INFO -- : Refreshing Gem list +I, [2017-11-06T08:05:03.084902 #33224] INFO -- : worker=1 spawned pid=33224 +I, [2017-11-06T08:05:03.085457 #33224] INFO -- : Refreshing Gem list +I, [2017-11-06T08:05:03.123611 #33224] INFO -- : worker=1 ready +I, [2017-11-06T08:05:03.123670 #33223] INFO -- : worker=0 ready +``` + +在使用 Unicorn 时,我们需要直接使用 `unicorn` 命令来启动一个 Unicorn 服务,在使用时可以通过 `-c` 传入一个配置文件,文件中的内容其实都是 Ruby 代码,每一个方法调用都是 Unicorn 的一条配置项: + +```ruby +$ cat unicorn.rb +worker_processes 2 +``` + +### 可执行文件 + +`unicorn` 这个命令位于 `bin/unicorn` 中,在这个可执行文件中,大部分的代码都是对命令行参数的配置和说明,整个文件可以简化为以下的几行代码: + +```ruby +rackup_opts = # ... +app = Unicorn.builder(ARGV[0] || 'config.ru', op) +Unicorn::Launcher.daemonize!(options) if rackup_opts[:daemonize] +Unicorn::HttpServer.new(app, options).start.join +``` + +`unicorn` 命令会从 Rack 应用的标配 config.ru 文件或者传入的文件中加载代码构建一个新的 Rack 应用;初始化 Rack 应用后会使用 `.daemonize!` 方法将 unicorn 进程启动在后台运行;最后会创建并启动一个新的 `HttpServer` 的实例。 + +### 构建应用 + +读取 config.ru 文件并解析的过程其实就是直接使用了 Rack 的 `Builder` 模块,通过 `eval` 运行一段代码得到一个 Rack 应用: + +```ruby +From: lib/unicorn.rb @ line 39: +Owner: #<Class:Unicorn> + +def self.builder(ru, op) + raw = File.read(ru) + inner_app = eval("Rack::Builder.new {(\n#{raw}\n)}.to_app", TOPLEVEL_BINDING, ru) + + middleware = { + ContentLength: nil, + Chunked: nil, + CommonLogger: [ $stderr ], + ShowExceptions: nil, + Lint: nil, + TempfileReaper: nil, + } + + Rack::Builder.new do + middleware.each do |m, args| + use(Rack.const_get(m), *args) if Rack.const_defined?(m) + end + run inner_app + end.to_app +end +``` + +在该方法中会执行两次 `Rack::Builder.new` 方法,第一次会运行 config.ru 中的代码,第二次会添加一些默认的中间件,最终会返回一个接受 `#call` 方法返回三元组的 Rack 应用。 + +### 守护进程 + +在默认情况下,Unicorn 的进程都是以前台进程的形式运行的,但是在生产环境我们往往需要在后台运行 Unicorn 进程,这也就是 `Unicorn::Launcher` 所做的工作。 + +```ruby +From: lib/unicorn.rb @ line 23: +Owner: #<Class:Unicorn::Launcher> + +def self.daemonize!(options) + cfg = Unicorn::Configurator + $stdin.reopen("/dev/null") + + unless ENV['UNICORN_FD'] + rd, wr = IO.pipe + grandparent = $$ + if fork + wr.close + else + rd.close + Process.setsid + exit if fork + end + + if grandparent == $$ + master_pid = (rd.readpartial(16) rescue nil).to_i + unless master_pid > 1 + warn "master failed to start, check stderr log for details" + exit!(1) + end + exit 0 + else + options[:ready_pipe] = wr + end + end + cfg::DEFAULTS[:stderr_path] ||= "/dev/null" + cfg::DEFAULTS[:stdout_path] ||= "/dev/null" + cfg::RACKUP[:daemonized] = true +end +``` + +想要真正理解上述代码的工作,我们需要理解广义上的 daemonize 过程,在 Unix-like 的系统中,一个 [daemon](https://en.wikipedia.org/wiki/Daemon_(computing))(守护进程)是运行在后台不直接被用户操作的进程;一个进程想要变成守护进程通常需要做以下的事情: + +1. 执行 `fork` 和 `exit` 来创建一个后台任务; +2. 从 tty 的控制中分离、创建一个新的 session 并成为新的 session 和进程组的管理者; +3. 将根目录 `/` 设置为当前进程的工作目录; +4. 将 umask 更新成 `0` 以提供自己的权限管理掩码; +5. 使用日志文件、控制台或者 `/dev/null` 设备作为标准输入、输出和错误; + +在 `.daemonize!` 方法中我们总共使用 fork 创建了两个进程,整个过程涉及三个进程的协作,其中 grandparent 是启动 Unicorn 的进程一般指终端,parent 是用来启动 Unicorn 服务的进程,master 就是 Unicorn 服务中的主进程,三个进程有以下的关系: + +![unicorn-daemonize](images/rack-unicorn/unicorn-daemonize.png) + +上述的三个进程中,grandparent 表示用于启动 Unicorn 进程的终端,parent 只是一个用于设置进程状态和掩码的中间进程,它在启动 Unicorn 的 master 进程后就会立刻退出。 + +在这里,我们会分三个部分分别介绍 grandparent、parent 和 master 究竟做了哪些事情;首先,对于 grandparent 进程来说,我们实际上运行了以下的代码: + +```ruby +def self.daemonize!(options) + $stdin.reopen("/dev/null") + rd, wr = IO.pipe + wr.close + + # fork + + master_pid = (rd.readpartial(16) rescue nil).to_i + unless master_pid > 1 + warn "master failed to start, check stderr log for details" + exit!(1) + end +end +``` + +通过 `IO.pipe` 方法创建了一对 Socket 节点,其中一个用于读,另一个用于写,在这里由于当前进程 grantparent 不需要写,所以直接将写的一端 `#close`,保留读的一端等待 Unicorn master 进程发送它的 `pid`,如果 master 没有成功启动就会报错,这也是 grandparent 进程的主要作用。 + +对于 parent 进程来说做的事情其实就更简单了,在 `fork` 之后会直接将读的一端执行 `#close`,这样无论是当前进程 parent 还是 parent fork 出来的进程都无法通过 `rd` 读取数据: + +```ruby +def self.daemonize!(options) + $stdin.reopen("/dev/null") + rd, wr = IO.pipe + + # fork + + rd.close + Process.setsid + exit if fork +end +``` + +在 parent 进程中,我们通过 `Process.setsid` 将当前的进程设置为新的 session 和进程组的管理者,从 tty 中分离;最后直接执行 `fork` 创建一个新的进程 master 并退出 parent 进程,parent 进程的作用其实就是为了启动新 Unicorn master 进程。 + +```ruby +def self.daemonize!(options) + cfg = Unicorn::Configurator + $stdin.reopen("/dev/null") + + rd, wr = IO.pipe + rd.close + + Process.setsid + + # fork + + options[:ready_pipe] = wr + cfg::DEFAULTS[:stderr_path] ||= "/dev/null" + cfg::DEFAULTS[:stdout_path] ||= "/dev/null" + cfg::RACKUP[:daemonized] = true +end +``` + +新的进程 Unicorn master 就是一个不关联在任何 tty 的一个后台进程,不过到这里为止也仅仅创建另一个进程,Unicorn 还无法对外提供服务,我们将可读的 Socket `wr` 写入 `options` 中,在 webserver 成功启动后将通过 `IO.pipe` 创建的一对 Socket 将信息回传给 grandparent 进程通知服务启动的结果。 + +### 初始化服务 + +HTTP 服务在初始化时其实也没有做太多的事情,只是对 Rack 应用进行存储并初始化了一些实例变量: + +```ruby +From: lib/unicorn/http_server.rb @ line 69: +Owner: Unicorn::HttpServer + +def initialize(app, options = {}) + @app = app + @request = Unicorn::HttpRequest.new + @reexec_pid = 0 + @ready_pipe = options.delete(:ready_pipe) + @init_listeners = options[:listeners] ? options[:listeners].dup : [] + self.config = Unicorn::Configurator.new(options) + self.listener_opts = {} + + @self_pipe = [] + @workers = {} + @sig_queue = [] + @pid = nil + + config.commit!(self, :skip => [:listeners, :pid]) + @orig_app = app + @queue_sigs = [:WINCH, :QUIT, :INT, :TERM, :USR1, :USR2, :HUP, :TTIN, :TTOU] +end +``` + +在 `.daemonize!` 方法中存储的 `ready_pipe` 在这时被当前的 `HttpServer` 对象持有,之后会通过这个管道上传数据。 + +### 启动服务 + +`HttpServer` 服务的启动一看就是这个 `#start` 实例方法控制的,在这个方法中总过做了两件比较重要的事情: + +```ruby +From: lib/unicorn/http_server.rb @ line 120: +Owner: Unicorn::HttpServer + +def start + @queue_sigs.each { |sig| trap(sig) { @sig_queue << sig; awaken_master } } + trap(:CHLD) { awaken_master } + + self.pid = config[:pid] + + spawn_missing_workers + self +end +``` + +第一件事情是将构造器中初始化的 `queue_sigs` 实例变量中的全部信号,通过 `trap` 为信号提供用于响应事件的代码。 + +第二件事情就是通过 `#spawn_missing_workers` 方法 `fork` 当前 master 进程创建一系列的 worker 进程: + +```ruby +From: lib/unicorn/http_server.rb @ line 531: +Owner: Unicorn::HttpServer + +def spawn_missing_workers + worker_nr = -1 + until (worker_nr += 1) == @worker_processes + worker = Unicorn::Worker.new(worker_nr) + before_fork.call(self, worker) + + pid = fork + + unless pid + after_fork_internal + worker_loop(worker) + exit + end + + @workers[pid] = worker + worker.atfork_parent + end +rescue => e + @logger.error(e) rescue nil + exit! +end +``` + +在这种调用了 `fork` 的方法中,我们还是将其一分为二来看,在这里就是 master 和 worker 进程,对于 master 进程来说: + +```ruby +From: lib/unicorn/http_server.rb @ line 531: +Owner: Unicorn::HttpServer + +def spawn_missing_workers + worker_nr = -1 + until (worker_nr += 1) == @worker_processes + worker = Unicorn::Worker.new(worker_nr) + before_fork.call(self, worker) + + pid = fork + + # ... + + @workers[pid] = worker + end +rescue => e + @logger.error(e) rescue nil + exit! +end +``` + +通过一个 until 循环,master 进程能够创建 `worker_processes` 个 worker 进程,在每个循环中,上述方法都会创建一个 `Unicorn::Worker` 对象并在 `fork` 之后,将子进程的 `pid` 和 `worker` 以键值对的形式存到 `workers` 这个实例变量中。 + +`before_fork` 中存储的 block 是我们非常熟悉的,其实就是向服务器的日志中追加内容: + +```ruby +DEFAULTS = { + # ... + :after_fork => lambda { |server, worker| + server.logger.info("worker=#{worker.nr} spawned pid=#{$$}") + }, + :before_fork => lambda { |server, worker| + server.logger.info("worker=#{worker.nr} spawning...") + }, + :before_exec => lambda { |server| + server.logger.info("forked child re-executing...") + }, + :after_worker_exit => lambda { |server, worker, status| + m = "reaped #{status.inspect} worker=#{worker.nr rescue 'unknown'}" + if status.success? + server.logger.info(m) + else + server.logger.error(m) + end + }, + :after_worker_ready => lambda { |server, worker| + server.logger.info("worker=#{worker.nr} ready") + }, + # ... +} +``` + +所有日志相关的输出大都在 `Unicorn::Configurator` 类中作为常量定义起来,并在初始化时作为缺省值赋值到 `HttpServer` 相应的实例变量上。而对于真正处理 HTTP 请求的 worker 进程来说,就会进入更加复杂的逻辑了: + +```ruby +def spawn_missing_workers + worker_nr = -1 + until (worker_nr += 1) == @worker_processes + worker = Unicorn::Worker.new(worker_nr) + before_fork.call(self, worker) + + # fork + + after_fork_internal + worker_loop(worker) + exit + end +rescue => e + @logger.error(e) rescue nil + exit! +end +``` + +在这里调用了两个实例方法,其中一个是 `#after_fork_internal`,另一个是 `#worker_loop` 方法,前者用于处理一些 `fork` 之后收尾的逻辑,比如关闭仅在 master 进程中使用的 `self_pipe`: + +```ruby +def after_fork_internal + @self_pipe.each(&:close).clear + @ready_pipe.close if @ready_pipe + Unicorn::Configurator::RACKUP.clear + @ready_pipe = @init_listeners = @before_exec = @before_fork = nil +end +``` + +而后者就是 worker 持续监听 Socket 输入并处理请求的循环了。 + +### 循环 + +当我们开始运行 worker 中的循环时,就开始监听 Socket 上的事件,整个过程还是比较直观的: + +```ruby +From: lib/unicorn/http_server.rb @ line 681: +Owner: Unicorn::HttpServer + +def worker_loop(worker) + ppid = @master_pid + readers = init_worker_process(worker) + ready = readers.dup + @after_worker_ready.call(self, worker) + + begin + tmp = ready.dup + while sock = tmp.shift + if client = sock.kgio_tryaccept + process_client(client) + end + end + + unless nr == 0 + tmp = ready.dup + redo + end + + ppid == Process.ppid or return + + ret = IO.select(readers, nil, nil, @timeout) and ready = ret[0] + rescue => e + # ... + end while readers[0] +end +``` + +如果当前 Socket 上有等待处理的 HTTP 请求,就会执行 `#process_client` 方法队请求进行处理,在这里调用了 Rack 应用的 `#call` 方法得到了三元组: + +```ruby +From: lib/unicorn/http_server.rb @ line 605: +Owner: Unicorn::HttpServer + +def process_client(client) + status, headers, body = @app.call(env = @request.read(client)) + + begin + return if @request.hijacked? + + @request.headers? or headers = nil + http_response_write(client, status, headers, body, + @request.response_start_sent) + ensure + body.respond_to?(:close) and body.close + end + + unless client.closed? + client.shutdown + client.close + end +rescue => e + handle_error(client, e) +end +``` + +请求的解析是通过 `Request#read` 处理的,而向 Socket 写 HTTP 响应是通过 `#http_response_write` 方法来完成的,在这里有关 HTTP 请求的解析和响应的处理都属于一些不重要的实现细节,在这里也就不展开介绍了;当我们已经响应了用户的请求就可以将当前 Socket 直接关掉,断掉这个 TCP 连接了。 + +## 调度 + +我们在上面已经通过多次 `fork` 启动了用于管理 Unicorn worker 进程的 master 以及多个 worker 进程,由于 Unicorn webserver 涉及了多个进程,所以需要进程之间进行调度。 + +在 Unix 中,进程的调度往往都是通过信号来进行的,`HttpServer#join` 就在 Unicorn 的 master 进程上监听外界发送的各种信号,不过在监听信号之前,要通过 `ready_pipe` 通知 grandparent 进程当前 master 进程已经启动完毕: + +```ruby +From: lib/unicorn/http_server.rb @ line 267: +Owner: Unicorn::HttpServer + +def join + respawn = true + last_check = time_now + + proc_name 'master' + logger.info "master process ready" # test_exec.rb relies on this message + if @ready_pipe + begin + @ready_pipe.syswrite($$.to_s) + rescue => e + logger.warn("grandparent died too soon?: #{e.message} (#{e.class})") + end + @ready_pipe = @ready_pipe.close rescue nil + end + + # ... +end +``` + +当 grandparent 进程,也就是执行 Unicorn 命令的进程接收到命令退出之后,就可以继续做其他的操作了,而 master 进程会进入一个 while 循环持续监听外界发送的信号: + +```ruby +def join + # ... + + begin + reap_all_workers + case @sig_queue.shift + # ... + when :WINCH + respawn = false + soft_kill_each_worker(:QUIT) + self.worker_processes = 0 + when :TTIN + respawn = true + self.worker_processes += 1 + when :TTOU + self.worker_processes -= 1 if self.worker_processes > 0 + when # ... + end + rescue => e + Unicorn.log_error(@logger, "master loop error", e) + end while true + + # ... +end +``` + +这一部分的几个信号都会改变当前 Unicorn worker 的进程数,无论是 `TTIN`、`TTOU` 还是 `WINCH` 信号最终都修改了 `worker_processes` 变量,其中 `#soft_kill_each_worker` 方法向所有的 Unicorn worker 进程发送了 `QUIT` 信号。 + +除了一些用于改变当前 worker 数量的信号,Unicorn 的 master 进程还监听了一些用于终止 master 进程或者更新配置文件的信号。 + +```ruby +def join + # ... + + begin + reap_all_workers + case @sig_queue.shift + # ... + when :QUIT + break + when :TERM, :INT + stop(false) + break + when :HUP + respawn = true + if config.config_file + load_config! + else # exec binary and exit if there's no config file + reexec + end + when # ... + end + rescue => e + Unicorn.log_error(@logger, "master loop error", e) + end while true + + # ... +end +``` + +无论是 `QUIT` 信号还是 `TERM`、`INT` 最终都执行了 `#stop` 方法,选择使用不同的信号干掉当前 master 管理的 worker 进程: + +```ruby +From: lib/unicorn/http_server.rb @ line 339: +Owner: Unicorn::HttpServer + +def stop(graceful = true) + self.listeners = [] + limit = time_now + timeout + until @workers.empty? || time_now > limit + if graceful + soft_kill_each_worker(:QUIT) + else + kill_each_worker(:TERM) + end + sleep(0.1) + reap_all_workers + end + kill_each_worker(:KILL) +end +``` + +上述方法其实非常容易理解,它会根据传入的参数选择强制终止或者正常停止所有的 worker 进程,这样整个 Unicorn 服务才真正停止并不再为外界提供服务了。 + +当我们向 master 发送 `TTIN` 或者 `TTOU` 信号时只是改变了实例变量 `worker_process` 的值,还并没有 `fork` 出新的进程,这些操作都是在 `nil` 条件中完成的: + +```ruby +def join + # ... + + begin + reap_all_workers + case @sig_queue.shift + when nil + if (last_check + @timeout) >= (last_check = time_now) + sleep_time = murder_lazy_workers + else + sleep_time = @timeout/2.0 + 1 + end + maintain_worker_count if respawn + master_sleep(sleep_time) + when # ... + end + rescue => e + Unicorn.log_error(@logger, "master loop error", e) + end while true + + # ... +end +``` + +当 `@sig_queue.shift` 返回 `nil` 时也就代表当前没有需要处理的信号,如果需要创建新的进程或者停掉进程就会通过 `#maintain_worker_count` 方法,之后 master 进程会陷入睡眠直到被再次唤醒。 + +```ruby +From: lib/unicorn/http_server.rb @ line 561: +Owner: Unicorn::HttpServer + +def maintain_worker_count + (off = @workers.size - worker_processes) == 0 and return + off < 0 and return spawn_missing_workers + @workers.each_value { |w| w.nr >= worker_processes and w.soft_kill(:QUIT) } +end +``` + +通过创建缺失的进程并关闭多余的进程,我们能够实时的保证整个 Unicorn 服务中的进程数与期望的配置完全相同。 + +在 Unicorn 的服务中,不仅 master 进程能够接收到来自用户或者其他进程的各种信号,worker 进程也能通过以下的方式将接受到的信号交给 master 处理:
 +```ruby +From: lib/unicorn/http_server.rb @ line 120: +Owner: Unicorn::HttpServer + +def start + # ... + @queue_sigs.each { |sig| trap(sig) { @sig_queue << sig; awaken_master } } + trap(:CHLD) { awaken_master } +end + +From: lib/unicorn/http_server.rb @ line 391: +Owner: Unicorn::HttpServer + +def awaken_master + return if $$ != @master_pid + @self_pipe[1].kgio_trywrite('.') +end +``` + +所以即使向 worker 进程发送 `TTIN` 或者 `TTOU` 等信号也能够改变整个 Unicorn 服务中 worker 进程的个数。 + +## 多进程模型 + +总的来说,Unicorn 作为 Web 服务使用了多进程的模型,通过一个 master 进程来管理多个 worker 进程,其中 master 进程不负责处理客户端的 HTTP 请求,多个 worker 进程监听同一组 Socket: + +![unicorn-io-mode](images/rack-unicorn/unicorn-io-model.png) + +一组 worker 进程在监听 Socket 时,如果发现当前的 Socket 有等待处理的请求时就会在当前的进程中直接通过 `#process_client` 方法处理,整个过程会阻塞当前的进程,而多进程阻塞 I/O 的方式没有办法接受慢客户端造成的性能损失,只能通过反向代理 nginx 才可以解决这个问题。 + +![unicorn-multi-processes](images/rack-unicorn/unicorn-multi-processes.png) + +在 Unicorn 中,worker 之间的负载均衡是由操作系统解决的,所有的 worker 是通过 `.select` 方法等待共享 Socket 上的请求,一旦出现可用的 worker,就可以立即进行处理,避开了其他负载均衡算法没有考虑到请求处理时间的问题。 + +## 总结 + +Unicorn 的源代码其实是作者读过的可读性最差的 Ruby 代码了,很多 Ruby 代码的风格写得跟 C 差不多,看起来也比较头疼,可能是需要处理很多边界条件以及信号,涉及较多底层的进程问题;虽然代码风格上看起来确实让人头疼,不过实现还是值得一看的,重要的代码大都包含在 unicorn.rb 和 http_server.rb 两个文件中,阅读时也不需要改变太多的上下文。 + +相比于 WEBrick 的单进程多线程的 I/O 模型,Unicorn 的多进程模型有很多优势,一是能够充分利用多核 CPU 的性能,其次能够通过 master 来管理并监控 Unicorn 中包含的一组 worker 并提供了零宕机部署的功能,除此之外,多进程的 I/O 模型还不在乎当前的应用是否是线程安全的,所以不会出现线程竞争等问题,不过 Unicorn 由于 `fork` 了大量的 worker 进程,如果长时间的在 Unicorn 上运行内存泄露的应用会非常耗费内存资源,可以考虑使用 [unicorn-worker-killer](https://github.com/kzk/unicorn-worker-killer) 来自动重启。 + +## Reference + ++ [How To Optimize Unicorn Workers in a Ruby on Rails App](https://www.digitalocean.com/community/tutorials/how-to-optimize-unicorn-workers-in-a-ruby-on-rails-app) ++ [Ruby Web 服务器:这十五年](https://read01.com/zh-hk/zm5B.html#.Wf0oLduB0sk) ++ [unicorn-worker-killer](https://github.com/kzk/unicorn-worker-killer) ++ [Daemon (computing)](https://en.wikipedia.org/wiki/Daemon_(computing)) ++ [Nginx 与 Unicorn](http://jiangpeng.info/blogs/2014/03/10/nginx-unicorn.html) ++ [Unicorn!](https://github.com/blog/517-unicorn) + diff --git a/contents/rack/rack-webrik.md b/contents/rack/rack-webrik.md new file mode 100644 index 0000000..4bf75e7 --- /dev/null +++ b/contents/rack/rack-webrik.md @@ -0,0 +1,459 @@ +# 浅谈 WEBrick 的实现 + ++ [谈谈 Rack 协议与实现](https://draveness.me/rack) ++ [浅谈 WEBrick 的实现](https://draveness.me/rack-webrick) ++ [浅谈 Thin 的事件驱动模型](https://draveness.me/rack-thin) ++ [浅谈 Unicorn 的多进程模型](https://draveness.me/rack-unicorn) ++ [浅谈 Puma 的实现](https://draveness.me/rack-puma) + +这篇文章会介绍在开发环境中最常用的应用容器 WEBrick 的实现原理,除了通过源代码分析之外,我们也会介绍它的 IO 模型以及一些特点。 + +在 GitHub 上,WEBrick 从 2003 年的六月份就开始开发了,有着十几年历史的 WEBrick 的实现非常简单,总共只有 4000 多行的代码: + +```ruby +$ loc_counter . +40 files processed +Total 6918 lines +Empty 990 lines +Comments 1927 lines +Code 4001 lines +``` + +## WEBrick 的实现 + +由于 WEBrick 是 Rack 中内置的处理器,所以与 Unicorn 和 Puma 这种第三方开发的 webserver 不同,WEBrick 的处理器是在 Rack 中实现的,而 WEBrick 的运行也都是从这个处理器的开始的。 + +```ruby +module Rack + module Handler + class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet + def self.run(app, options={}) + environment = ENV['RACK_ENV'] || 'development' + default_host = environment == 'development' ? 'localhost' : nil + + options[:BindAddress] = options.delete(:Host) || default_host + options[:Port] ||= 8080 + @server = ::WEBrick::HTTPServer.new(options) + @server.mount "/", Rack::Handler::WEBrick, app + yield @server if block_given? + @server.start + end + end + end +end +``` + +我们在上一篇文章 [谈谈 Rack 协议与实现](https://draveness.me/rack) 中介绍 Rack 的实现原理时,最终调用了上述方法,从这里开始大部分的实现都与 WEBrick 有关了。 + +在这里,你可以看到方法会先处理传入的参数比如:地址、端口号等等,在这之后会使用 WEBrick 提供的 `HTTPServer` 来处理 HTTP 请求,调用 `mount` 在根路由上挂载应用和处理器 `Rack::Handler::WEBrick` 接受请求,最后执行 `#start` 方法启动服务器。 + +### 初始化服务器 + +`HTTPServer` 的初始化分为两个阶段,一部分是 `HTTPServer` 的初始化,另一部分调用父类的 `initialize` 方法,在自己构造器中,会配置当前服务器能够处理的 HTTP 版本并初始化新的 `MountTable` 实例: + +```ruby +From: lib/webrick/httpserver.rb @ line 46: +Owner: #<Class:WEBrick::HTTPServer> + +def initialize(config={}, default=Config::HTTP) + super(config, default) + @http_version = HTTPVersion::convert(@config[:HTTPVersion]) + + @mount_tab = MountTable.new + if @config[:DocumentRoot] + mount("/", HTTPServlet::FileHandler, @config[:DocumentRoot], + @config[:DocumentRootOptions]) + end + + unless @config[:AccessLog] + @config[:AccessLog] = [ + [ $stderr, AccessLog::COMMON_LOG_FORMAT ], + [ $stderr, AccessLog::REFERER_LOG_FORMAT ] + ] + end + + @virtual_hosts = Array.new +end +``` + +在父类 `GenericServer` 中初始化了用于监听端口号的 Socket 连接: + +```ruby +From: lib/webrick/server.rb @ line 185: +Owner: #<Class:WEBrick::GenericServer> + +def initialize(config={}, default=Config::General) + @config = default.dup.update(config) + @status = :Stop + + @listeners = [] + listen(@config[:BindAddress], @config[:Port]) + if @config[:Port] == 0 + @config[:Port] = @listeners[0].addr[1] + end +end +``` + +每一个服务器都会在初始化的时候创建一系列的 `listener` 用于监听地址和端口号组成的元组,其内部调用了 `Utils` 模块中定义的方法: + +```ruby +From: lib/webrick/server.rb @ line 127: +Owner: #<Class:WEBrick::GenericServer> + +def listen(address, port) + @listeners += Utils::create_listeners(address, port) +end + +From: lib/webrick/utils.rb @ line 61: +Owner: #<Class:WEBrick::Utils> + +def create_listeners(address, port) + sockets = Socket.tcp_server_sockets(address, port) + sockets = sockets.map {|s| + s.autoclose = false + ts = TCPServer.for_fd(s.fileno) + s.close + ts + } + return sockets +end +module_function :create_listeners +``` + +在 `.create_listeners` 方法中调用了 `.tcp_server_sockets` 方法由于初始化一组 `Socket` 对象,最后得到一个数组的 `TCPServer` 实例。 + +### 挂载应用 + +在使用 `WEBrick` 启动服务的时候,第二步就是将处理器和 Rack 应用挂载到根路由下: + +```ruby +@server.mount "/", Rack::Handler::WEBrick, app +``` + +`#mount` 方法其实是一个比较简单的方法,因为我们在构造器中已经初始化了 `MountTable` 对象,所以这一步只是将传入的多个参数放到这个表中: + +```ruby +From: lib/webrick/httpserver.rb @ line 155: +Owner: WEBrick::HTTPServer + +def mount(dir, servlet, *options) + @mount_tab[dir] = [ servlet, options ] +end +``` + +`MountTable` 是一个包含从路由到 Rack 处理器一个 App 的映射表: + +![mounttable-and-applications](images/rack-webrick/mounttable-and-applications.png) + +当执行了 `MountTable` 的 `#compile` 方法时,上述的对象就会将表中的所有键按照加入的顺序逆序拼接成一个如下的正则表达式用来匹配传入的路由: + +```ruby +^(/|/admin|/user)(?=/|$) +``` + +上述正则表达式在使用时如果匹配到了指定的路由就会返回 `$&` 和 `$'` 两个部分,前者表示整个匹配的文本,后者表示匹配文本后面的字符串。 + +### 启动服务器 + +在 `Rack::Handler::WEBrick` 中的 `.run` 方法先初始化了服务器,将处理器和应用挂载到了根路由上,在最后执行 `#start` 方法启动服务器: + +```ruby +From: lib/webrick/server.rb @ line 152: +Owner: WEBrick::GenericServer + +def start(&block) + raise ServerError, "already started." if @status != :Stop + + @status = :Running + begin + while @status == :Running + begin + if svrs = IO.select([*@listeners], nil, nil, 2.0) + svrs[0].each{ |svr| + sock = accept_client(svr) + start_thread(sock, &block) + } + end + rescue Errno::EBADF, Errno::ENOTSOCK, IOError, StandardError => ex + rescue Exception => ex + raise + end + end + ensure + cleanup_listener + @status = :Stop + end +end +``` + +由于原方法的实现比较复杂不容易阅读,在这里对方法进行了简化,省略了向 logger 中输出内容、处理服务的关闭以及执行回调等功能。 + +我们可以理解为上述方法通过 `.select` 方法对一组 Socket 进行监听,当有消息需要处理时就依次执行 `#accept_client` 和 `#start_thread` 两个方法处理来自客户端的请求: + +```ruby +From: lib/webrick/server.rb @ line 254: +Owner: WEBrick::GenericServer + +def accept_client(svr) + sock = nil + begin + sock = svr.accept + sock.sync = true + Utils::set_non_blocking(sock) + rescue Errno::ECONNRESET, Errno::ECONNABORTED, + Errno::EPROTO, Errno::EINVAL + rescue StandardError => ex + msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}" + @logger.error msg + end + return sock +end +``` + +WEBrick 在 `#accept_client` 方法中执行了 `#accept` 方法来得到一个 TCP 客户端 Socket,同时会通过 `set_non_blocking` 将该 Socket 变成非阻塞的,最后在方法末尾返回创建的 Socket。 + +在 `#start_thread` 方法中会**开启一个新的线程**,并在新的线程中执行 `#run` 方法来处理请求: + +```ruby +From: lib/webrick/server.rb @ line 278: +Owner: WEBrick::GenericServer + +def start_thread(sock, &block) + Thread.start { + begin + Thread.current[:WEBrickSocket] = sock + run(sock) + rescue Errno::ENOTCONN, ServerError, Exception + ensure + Thread.current[:WEBrickSocket] = nil + sock.close + end + } +end +``` + +### 处理请求 + +所有的请求都不会由 `GenericServer` 这个通用的服务器来处理,它只处理通用的逻辑,对于 HTTP 请求的处理都是在 `HTTPServer#run` 中完成的: + +```ruby +From: lib/webrick/httpserver.rb @ line 69: +Owner: WEBrick::HTTPServer + +def run(sock) + while true + res = HTTPResponse.new(@config) + req = HTTPRequest.new(@config) + server = self + begin + timeout = @config[:RequestTimeout] + while timeout > 0 + break if sock.to_io.wait_readable(0.5) + break if @status != :Running + timeout -= 0.5 + end + raise HTTPStatus::EOFError if timeout <= 0 || @status != :Running + raise HTTPStatus::EOFError if sock.eof? + req.parse(sock) + res.request_method = req.request_method + res.request_uri = req.request_uri + res.request_http_version = req.http_version + self.service(req, res) + rescue HTTPStatus::EOFError, HTTPStatus::RequestTimeout, HTTPStatus::Error => ex + res.set_error(ex) + rescue HTTPStatus::Status => ex + res.status = ex.code + rescue StandardError => ex + res.set_error(ex, true) + ensure + res.send_response(sock) if req.request_line + end + break if @http_version < "1.1" + end +end +``` + +对 HTTP 协议了解的读者应该能从上面的代码中看到很多与 HTTP 协议相关的东西,比如 HTTP 的版本号、方法、URL 等等,上述方法总共做了三件事情,等待监听的 Socket 变得可读,执行 `#parse` 方法解析 Socket 上的数据,通过 `#service` 方法完成处理请求的响应,首先是对 Socket 上的数据进行解析: + +```ruby +From: lib/webrick/httprequest.rb @ line 192: +Owner: WEBrick::HTTPRequest + +def parse(socket=nil) + @socket = socket + begin + @peeraddr = socket.respond_to?(:peeraddr) ? socket.peeraddr : [] + @addr = socket.respond_to?(:addr) ? socket.addr : [] + rescue Errno::ENOTCONN + raise HTTPStatus::EOFError + end + + read_request_line(socket) + if @http_version.major > 0 + # ... + end + return if @request_method == "CONNECT" + return if @unparsed_uri == "*" + + begin + setup_forwarded_info + @request_uri = parse_uri(@unparsed_uri) + @path = HTTPUtils::unescape(@request_uri.path) + @path = HTTPUtils::normalize_path(@path) + @host = @request_uri.host + @port = @request_uri.port + @query_string = @request_uri.query + @script_name = "" + @path_info = @path.dup + rescue + raise HTTPStatus::BadRequest, "bad URI `#{@unparsed_uri}'." + end + + if /close/io =~ self["connection"] + # deal with keep alive + end +end +``` + +由于 HTTP 协议本身就比较复杂,请求中包含的信息也非常多,所以在这里用于**解析** HTTP 请求的代码也很多,想要了解 WEBrick 是如何解析 HTTP 请求的可以看 httprequest.rb 文件中的代码,在处理了 HTTP 请求之后,就开始执行 `#service` 响应该 HTTP 请求了: + +```ruby +From: lib/webrick/httpserver.rb @ line 125: +Owner: WEBrick::HTTPServer + +def service(req, res) + servlet, options, script_name, path_info = search_servlet(req.path) + raise HTTPStatus::NotFound, "`#{req.path}' not found." unless servlet + req.script_name = script_name + req.path_info = path_info + si = servlet.get_instance(self, *options) + si.service(req, res) +end +``` + +在这里我们会从上面提到的 `MountTable` 中找出在之前注册的处理器 handler 和 Rack 应用: + +```ruby +From: lib/webrick/httpserver.rb @ line 182: +Owner: WEBrick::HTTPServer + +def search_servlet(path) + script_name, path_info = @mount_tab.scan(path) + servlet, options = @mount_tab[script_name] + if servlet + [ servlet, options, script_name, path_info ] + end +end +``` + +得到了处理器 handler 之后,通过 `.get_instance` 方法创建一个新的实例,这个方法在大多数情况下等同于初始化方法 `.new`,随后调用了该处理器 `Rack::WEBrick::Handler` 的 `#service` 方法,该方法是在 rack 工程中定义的: + +```ruby +From: rack/lib/handler/webrick.rb @ line 57: +Owner: Rack::Handler::WEBrick + +def service(req, res) + res.rack = true + env = req.meta_vars + env.delete_if { |k, v| v.nil? } + + env.update( + # ... + RACK_URL_SCHEME => ["yes", "on", "1"].include?(env[HTTPS]) ? "https" : "http", + # ... + ) + + status, headers, body = @app.call(env) + begin + res.status = status.to_i + headers.each { |k, vs| + # ... + } + + body.each { |part| + res.body << part + } + ensure + body.close if body.respond_to? :close + end +end +``` + +由于上述方法也涉及了非常多 HTTP 协议的实现细节所以很多过程都被省略了,在上述方法中,我们先构建了应用的输入 `env` 哈希变量,然后通过执行 `#call` 方法将控制权交给 Rack 应用,最后获得一个由 `status`、`headers` 和 `body` 组成的三元组;在接下来的代码中,分别对这三者进行处理,为这次请求『填充』一个完成的 HTTP 请求。 + +到这里,最后由 `WEBrick::HTTPServer#run` 方法中的 `ensure` block 来结束整个 HTTP 请求的处理: + +```ruby +From: lib/webrick/httpserver.rb @ line 69: +Owner: WEBrick::HTTPServer + +def run(sock) + while true + res = HTTPResponse.new(@config) + req = HTTPRequest.new(@config) + server = self + begin + # ... + ensure + res.send_response(sock) if req.request_line + end + break if @http_version < "1.1" + end +end +``` + +在 `#send_reponse` 方法中,分别执行了 `#send_header` 和 `#send_body` 方法向当前的 Socket 中发送 HTTP 响应中的数据: + +```ruby +From: lib/webrick/httpresponse @ line 205: +Owner: WEBrick::HTTPResponse + +def send_response(socket) + begin + setup_header() + send_header(socket) + send_body(socket) + rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ENOTCONN => ex + @logger.debug(ex) + @keep_alive = false + rescue Exception => ex + @logger.error(ex) + @keep_alive = false + end +end +``` + +所有向 Socket 中写入数据的工作最终都会由 `#_write_data` 这个方法来处理,将数据全部写入 Socket 中: + +```ruby +From: lib/webrick/httpresponse @ line 464: +Owner: WEBrick::HTTPResponse + +def _write_data(socket, data) + socket << data +end +``` + +从解析 HTTP 请求、调用 Rack 应用、创建 Response 到最后向 Socket 中写回数据,WEBrick 处理 HTTP 请求的部分就结束了。 + +## I/O 模型 + +通过对 WEBrick 源代码的阅读,我们其实已经能够了解整个 webserver 的工作原理,当我们启动一个 WEBrick 服务时只会启动一个进程,该进程会在指定的 ip 和端口上使用 `.select` 监听来自用户的所有 HTTP 请求: + +![webrick-io-mode](images/rack-webrick/webrick-io-model.png) + +当 `.select` 接收到来自用户的请求时,会为每一个请求创建一个新的 `Thread` 并在新的线程中对 HTTP 请求进行处理。 + +由于 WEBrick 在运行时只会启动一个进程,并没有其他的守护进程,所以它不够健壮,不能在发生问题时重启持续对外界提供服务,再加上 WEBrick 确实历史比较久远,代码的风格也不是特别的优雅,还有普遍知道的内存泄漏以及 HTTP 解析的问题,所以在生产环境中很少被使用。 + +虽然 WEBrick 有一些性能问题,但是作为 Ruby 自带的默认 webserver,在开发阶段使用 WEBrick 提供服务还是没有什么问题的。 + +## 总结 + +WEBrick 是 Ruby 社区中老牌的 webserver,虽然至今也仍然被广泛了解和使用,但是在生产环境中开发者往往会使用更加稳定的 Unicorn 和 Puma 代替它,我们选择在这个系列的文章中介绍它很大原因就是 WEBrick 的源代码与实现足够简单,我们很快就能了解一个 webserver 到底具备那些功能,在接下来的文章中我们就可以分析更加复杂的 webserver、了解更复杂的 IO 模型与实现了。 + +## Reference + ++ [Ruby on Rails Server options](https://stackoverflow.com/questions/4113299/ruby-on-rails-server-options) + + diff --git a/contents/rack/rack.md b/contents/rack/rack.md new file mode 100644 index 0000000..13fd418 --- /dev/null +++ b/contents/rack/rack.md @@ -0,0 +1,669 @@ +# 谈谈 Rack 的协议与实现 + ++ [谈谈 Rack 协议与实现](https://draveness.me/rack) ++ [浅谈 WEBrick 的实现](https://draveness.me/rack-webrick) ++ [浅谈 Thin 的事件驱动模型](https://draveness.me/rack-thin) ++ [浅谈 Unicorn 的多进程模型](https://draveness.me/rack-unicorn) ++ [浅谈 Puma 的实现](https://draveness.me/rack-puma) + +作为 Rails 开发者,基本上每天都与 Rails 的各种 API 以及数据库打交道,Rails 的世界虽然非常简洁,不过其内部的实现还是很复杂的,很多刚刚接触 Rails 的开发者可能都不知道 Rails 其实就是一个 [Rack](https://github.com/rack/rack) 应用,在这一系列的文章中,我们会分别介绍 Rack 以及一些常见的遵循 Rack 协议的 webserver 的实现原理。 + +![rack-logo](images/rack/rack-logo.png) + +不只是 Rails,几乎所有的 Ruby 的 Web 框架都是一个 Rack 的应用,除了 Web 框架之外,Rack 也支持相当多的 Web 服务器,可以说 Ruby 世界几乎一切与 Web 相关的服务都与 Rack 有关。 + +![rack-and-web-servers-frameworks](images/rack/rack-and-web-servers-frameworks.png) + +所以如果想要了解 Rails 或者其他 Web 服务底层的实现,那么一定需要了解 Rack 是如何成为应用容器(webserver)和应用框架之间的桥梁的,本文中介绍的是 2.0.3 版本的 rack。 + +## Rack 协议 + +在 Rack 的协议中,将 Rack 应用描述成一个可以响应 `call` 方法的 Ruby 对象,它仅接受来自外界的一个参数,也就是环境,然后返回一个只包含三个值的数组,按照顺序分别是状态码、HTTP Headers 以及响应请求的正文。 + +> A Rack application is a Ruby object (not a class) that responds to call. It takes exactly one argument, the environment and returns an Array of exactly three values: The status, the headers, and the body. + +![rack-protoco](images/rack/rack-protocol.png) + +Rack 在 webserver 和应用框架之间提供了一套最小的 API 接口,如果 webserver 都遵循 Rack 提供的这套规则,那么所有的框架都能通过协议任意地改变底层使用 webserver;所有的 webserver 只需要在 `Rack::Handler` 的模块中创建一个实现了 `.run` 方法的类就可以了: + +```ruby +module Rack + module Handler + class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet + def self.run(app, options={}) + # .. + end + end + end +end +``` + +这个类方法接受两个参数,分别是一个 Rack 应用对象和一个包含各种参数的 `options` 字典,其中可能包括自定义的 ip 地址和端口号以及各种配置,根据 Rack 协议,所有应用对象在接受到一个 `#call` 方法并且传入 `env` 时,都会返回一个三元组: + +![rack-app](images/rack/rack-app.png) + +最后的 `body` 响应体其实是一个由多个响应内容组成的数组,Rack 使用的 webserver 会将 `body` 中几个部分的连接到一起最后拼接成一个 HTTP 响应后返回。 + +## Rack 的使用 + +我们在大致了解 Rack 协议之后,其实可以从一段非常简单的代码入手来了解 Rack 是如何启动 webserver 来处理来自用户的请求的,我们可以在任意目录下创建如下所示的 config.ru 文件: + +```ruby +# config.ru + +run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] } +``` + +> 因为 `Proc` 对象也能够响应 `#call` 方法,所以上述的 Proc 对象也可以看做是一个 Rack 应用。 + +接下来,我们在同一目录使用 `rackup` 命令在命令行中启动一个 webserver 进程: + +```bash +$ rackup config.ru +[2017-10-26 22:59:26] INFO WEBrick 1.3.1 +[2017-10-26 22:59:26] INFO ruby 2.3.3 (2016-11-21) [x86_64-darwin16] +[2017-10-26 22:59:26] INFO WEBrick::HTTPServer#start: pid=83546 port=9292 +``` + +从命令的输出我们可以看到,使用 rackup 运行了一个 WEBrick 的进程,监听了 9292 端口,如果我们使用 curl 来访问对应的请求,就可以得到在 config.ru 文件中出现的 `'get rack\'d'` 文本: + +> 在这篇文章中,作者都会使用开源的工具 [httpie](https://github.com/jakubroztocil/httpie) 代替 curl 在命令行中发出 HTTP 请求,相比 curl 而言 httpie 能够提供与 HTTP 响应有关的更多信息。 + +```ruby +$ http http://localhost:9292 +HTTP/1.1 200 OK +Connection: Keep-Alive +Content-Type: text/html +Date: Thu, 26 Oct 2017 15:07:47 GMT +Server: WEBrick/1.3.1 (Ruby/2.3.3/2016-11-21) +Transfer-Encoding: chunked + +get rack'd +``` + +从上述请求返回的 HTTP 响应头中的信息,我们可以看到 WEBrick 确实按照 config.ru 文件中的代码对当前的 HTTP 请求进行了处理。 + +### 中间件 + +Rack 协议和中间件是 Rack 能达到今天地位不可或缺的两个功能或者说特性,Rack 协议规定了 webserver 和 Rack 应用之间应该如何通信,而 Rack 中间件能够在上层改变 HTTP 的响应或者请求,在不改变应用的基础上为 Rack 应用增加新的功能。 + +Rack 的中间件是一个实现了两个方法 `.initialize` 和 `#call` 的类,初始化方法会接受两个参数,分别是 `app` 和 `options` 字典,而 `#call` 方法接受一个参数也就是 HTTP 请求的环境参数 `env`,在这里我们创建了一个新的 Rack 中间件 `StatusLogger`: + +```ruby +class StatusLogger + def initialize(app, options={}) + @app = app + end + + def call(env) + status, headers, body = @app.call(env) + puts status + [status, headers, body] + end +end +``` + +在所有的 `#call` 方法中都**应该**调用 `app.call` 让应用对 HTTP 请求进行处理并在方法结束时将所有的参数按照顺序返回。 + +```ruby +use StatusLogger +run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] } +``` + +如果需要使用某一个 Rack 中间件只需要在当前文件中使用 `use` 方法,在每次接收到来自用户的 HTTP 请求时都会打印出当前响应的状态码。 + +```ruby +$ rackup +[2017-10-27 19:46:40] INFO WEBrick 1.3.1 +[2017-10-27 19:46:40] INFO ruby 2.3.3 (2016-11-21) [x86_64-darwin16] +[2017-10-27 19:46:40] INFO WEBrick::HTTPServer#start: pid=5274 port=9292 +200 +127.0.0.1 - - [27/Oct/2017:19:46:53 +0800] "GET / HTTP/1.1" 200 - 0.0004 +``` + +除了直接通过 `use` 方法直接传入 `StatusLogger` 中间件之外,我们也可以在 `use` 中传入配置参数,所有的配置都会通过 `options` 最终初始化一个中间件的实例,比如,我们有以下的中间件 `BodyTransformer`: + +```ruby +class BodyTransformer + def initialize(app, options={}) + @app = app + @count = options[:count] + end + + def call(env) + status, headers, body = @app.call(env) + body = body.map { |str| str[0...@count].upcase + str[@count..-1] } + [status, headers, body] + end +end +``` + +上述中间件会在每次调用时都将 Rack 应用返回的 `body` 中前 `count` 个字符变成大写的,我们可以在 config.ru 中添加一个新的中间件: + +```ruby +use StatusLogger +use BodyTransformer, count: 3 +run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] } +``` + +当我们再次使用 http 命令请求相同的 URL 时,就会获得不同的结果,同时由于我们保留了 `StatusLogger`,所以在 console 中也会打印出当前响应的状态码: + +```ruby +# session 1 +$ rackup +[2017-10-27 21:04:05] INFO WEBrick 1.3.1 +[2017-10-27 21:04:05] INFO ruby 2.3.3 (2016-11-21) [x86_64-darwin16] +[2017-10-27 21:04:05] INFO WEBrick::HTTPServer#start: pid=7524 port=9292 +200 +127.0.0.1 - - [27/Oct/2017:21:04:19 +0800] "GET / HTTP/1.1" 200 - 0.0005 + +# session 2 +$ http http://localhost:9292 +HTTP/1.1 200 OK +Connection: Keep-Alive +Content-Type: text/html +Date: Fri, 27 Oct 2017 13:04:19 GMT +Server: WEBrick/1.3.1 (Ruby/2.3.3/2016-11-21) +Transfer-Encoding: chunked + +GET rack'd +``` + +Rack 的中间件的使用其实非常简单,我们只需要定义符合要求的类,然后在合适的方法中返回合适的结果就可以了,在接下来的部分我们将介绍 Rack 以及中间件的实现原理。 + +## Rack 的实现原理 + +到这里,我们已经对 Rack 的使用有一些基本的了解了,包括如何使用 `rackup` 命令启动一个 webserver,也包括 Rack 的中间件如何使用,接下来我们就准备开始对 Rack 是如何实现上述功能进行分析了。 + +### rackup 命令 + +那么 `rackup` 到底是如何工作的呢,首先我们通过 `which` 命令来查找当前 `rackup` 的执行路径并打印出该文件的全部内容: + +```ruby +$ which rackup +/Users/draveness/.rvm/gems/ruby-2.3.3/bin/rackup + +$ cat /Users/draveness/.rvm/gems/ruby-2.3.3/bin/rackup +#!/usr/bin/env ruby_executable_hooks +# +# This file was generated by RubyGems. +# +# The application 'rack' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'rubygems' + +version = ">= 0.a" + +if ARGV.first + str = ARGV.first + str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding + if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then + version = $1 + ARGV.shift + end +end + +load Gem.activate_bin_path('rack', 'rackup', version) +``` + +从上述文件中的注释中可以看到当前文件是由 RubyGems 自动生成的,在文件的最后由一个 `load` 方法加载了某一个文件中的代码,我们可以在 pry 中尝试运行一下这个命令。 + +首先,通过 `gem list` 命令得到当前机器中所有 rack 的版本,然后进入 pry 执行 `.activate_bin_path` 命令: + +```ruby +$ gem list "^rack$" + +*** LOCAL GEMS *** + +rack (2.0.3, 2.0.1, 1.6.8, 1.2.3) + +$ pry +[1] pry(main)> Gem.activate_bin_path('rack', 'rackup', '2.0.3') +=> "/Users/draveness/.rvm/gems/ruby-2.3.3/gems/rack-2.0.3/bin/rackup" + +$ cat /Users/draveness/.rvm/gems/ruby-2.3.3/gems/rack-2.0.3/bin/rackup +#!/usr/bin/env ruby + +require "rack" +Rack::Server.start +``` + +> `rackup` 命令定义在 rack 工程的 bin/rackup 文件中,在通过 rubygems 安装后会生成另一个加载该文件的可执行文建。 + +在最后打印了该文件的内容,到这里我们就应该知道 `.activate_bin_path` 方法会查找对应 gem 当前生效的版本,并返回文件的路径;在这个可执行文件中,上述代码只是简单的 `require` 了一下 rack 方法,之后运行 `.start` 启动了一个 `Rack::Server`。 + +### Server 的启动 + +从这里开始,我们就已经从 rackup 命令的执行进入了 rack 的源代码,可以直接使用 pry 找到 `.start` 方法所在的文件,从方法中可以看到当前类方法初始化了一个新的实例后,在新的对象上执行了 `#start` 方法: + +```ruby +$ pry +[1] pry(main)> require 'rack' +=> true +[2] pry(main)> $ Rack::Server.start + +From: lib/rack/server.rb @ line 147: +Owner: #<Class:Rack::Server> + +def self.start(options = nil) + new(options).start +end +``` + +### 初始化和配置 + +在 `Rack::Server` 启动的过程中初始化了一个新的对象,初始化的过程中其实也包含了整个服务器的配置过程: + +```ruby +From: lib/rack/server.rb @ line 185: +Owner: #<Class:Rack::Server> + +def initialize(options = nil) + @ignore_options = [] + + if options + @use_default_options = false + @options = options + @app = options[:app] if options[:app] + else + argv = defined?(SPEC_ARGV) ? SPEC_ARGV : ARGV + @use_default_options = true + @options = parse_options(argv) + end +end +``` + +在这个 `Server` 对象的初始化器中,虽然可以通过 `options` 从外界传入参数,但是当前类中仍然存在这个 `#options` 和 `#default_options` 两个实例方法: + +```ruby +From: lib/rack/server.rb @ line 199: +Owner: Rack::Server + +def options + merged_options = @use_default_options ? default_options.merge(@options) : @options + merged_options.reject { |k, v| @ignore_options.include?(k) } +end + +From: lib/rack/server.rb @ line 204: +Owner: Rack::Server + +def default_options + environment = ENV['RACK_ENV'] || 'development' + default_host = environment == 'development' ? 'localhost' : '0.0.0.0' + { + :environment => environment, + :pid => nil, + :Port => 9292, + :Host => default_host, + :AccessLog => [], + :config => "config.ru" + } +end +``` + +上述两个方法中处理了一些对象本身定义的一些参数,比如默认的端口号 9292 以及默认的 config 文件,config 文件也就是 `rackup` 命令接受的一个文件参数,文件中的内容就是用来配置一个 Rack 服务器的代码,在默认情况下为 config.ru,也就是如果文件名是 config.ru,我们不需要向 `rackup` 命令传任何参数,它会自动找当前目录的该文件: + +```ruby +$ rackup +[2017-10-27 09:00:34] INFO WEBrick 1.3.1 +[2017-10-27 09:00:34] INFO ruby 2.3.3 (2016-11-21) [x86_64-darwin16] +[2017-10-27 09:00:34] INFO WEBrick::HTTPServer#start: pid=96302 port=9292 +``` + +访问相同的 URL 能得到完全一致的结果,在这里就不再次展示了,有兴趣的读者可以亲自尝试一下。 + +### 『包装』应用 + +当我们执行了 `.initialize` 方法初始化了一个新的实例之后,接下来就会进入 `#start` 实例方法尝试启动一个 webserver 处理 config.ru 中定义的应用了: + +```ruby +From: lib/rack/server.rb @ line 258: +Owner: Rack::Server + +def start &blk + # ... + + wrapped_app + # .. + + server.run wrapped_app, options, &blk +end +``` + +我们已经从上述方法中删除了很多对于本文来说不重要的代码实现,所以上述方法中最重要的部分就是 `#wrapped_app` 方法,以及另一个 `#server` 方法,首先来看 `#wrapped_app` 方法的实现。 + + +```ruby +From: lib/rack/server.rb @ line 353: +Owner: Rack::Server + +def wrapped_app + @wrapped_app ||= build_app app +end +``` + +上述方法有两部分组成,分别是 `#app` 和 `#build_app` 两个实例方法,其中 `#app` 方法的调用栈比较复杂: + +![server-app-call-stack](images/rack/server-app-call-stack.png) + +整个方法在最终会执行 `Builder.new_from_string` 通过 Ruby 中元编程中经常使用的 `eval` 方法,将输入文件中的全部内容与两端字符串拼接起来,并直接执行这段代码: + +```ruby +From: lib/rack/builder.rb @ line 48: +Owner: Rack::Builder + +def self.new_from_string(builder_script, file="(rackup)") + eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app", + TOPLEVEL_BINDING, file, 0 +end +``` + +在 `eval` 方法中执行代码的作用其实就是如下所示的: + +```ruby +Rack::Builder.new { + use StatusLogger + use BodyTransformer, count: 3 + run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] } +}.to_app +``` + +我们先暂时不管这段代码是如何执行的,我们只需要知道上述代码存储了所有的中间件以及 Proc 对象,最后通过 `#to_app` 方法返回一个 Rack 应用。 + +在这之后会使用 `#build_app` 方法将所有的中间件都包括在 Rack 应用周围,因为所有的中间件也都是一个响应 `#call` 方法,返回三元组的对象,其实也就是一个遵循协议的 App,唯一的区别就是中间件中会调用初始化时传入的 Rack App: + +```ruby +From: lib/rack/server.rb @ line 343: +Owner: Rack::Server + +def build_app(app) + middleware[options[:environment]].reverse_each do |middleware| + middleware = middleware.call(self) if middleware.respond_to?(:call) + next unless middleware + klass, *args = middleware + app = klass.new(app, *args) + end + app +end +``` + +经过上述方法,我们在一个 Rack 应用周围一层一层包装上了所有的中间件,最后调用的中间件在整个调用栈中的最外层,当包装后的应用接受来自外界的请求时,会按照如下的方式进行调用: + +![wrapped-app](images/rack/wrapped-app.png) + +所有的请求都会先经过中间件,每一个中间件都会在 `#call` 方法内部调用另一个中间件或者应用,在接收到应用的返回之后会分别对响应进行处理最后由最先定义的中间件返回。 + +### 中间件的实现 + +在 Rack 中,中间件是由两部分的代码共同处理的,分别是 `Rack::Builder` 和 `Rack::Server` 两个类,前者包含所有的能够在 config.ru 文件中使用的 DSL 方法,当我们使用 `eval` 执行 config.ru 文件中的代码时,会先初始化一个 `Builder` 的实例,然后执行 `instance_eval` 运行代码块中的所有内容: + +```ruby +From: lib/rack/builder.rb @ line 53: +Owner: Rack::Builder + +def initialize(default_app = nil, &block) + @use, @map, @run, @warmup = [], nil, default_app, nil + instance_eval(&block) if block_given? +end +``` + +在这时,config.ru 文件中的代码就会在当前实例的环境下执行,文件中的 `#use` 和 `#run` 方法在调用时就会执行 `Builder` 的实例方法,我们可以先看一下 `#use` 方法是如何实现的: + +```ruby +From: lib/rack/builder.rb @ line 81: +Owner: Rack::Builder + +def use(middleware, *args, &block) + @use << proc { |app| middleware.new(app, *args, &block) } +end +``` + +上述方法会将传入的参数组合成一个接受 `app` 作为入参的 `Proc` 对象,然后加入到 `@use` 数组中存储起来,在这里并没有发生任何其他的事情,另一个 `#run` 方法的实现其实就更简单了: + +```ruby +From: lib/rack/builder.rb @ line 103: +Owner: Rack::Builder + +def run(app) + @run = app +end +``` + +它只是将传入的 `app` 对象存储到持有的 `@run` 实例变量中,如果我们想要获取当前的 `Builder` 生成的应用,只需要通过 `#to_app` 方法: + +```ruby +From: lib/rack/builder.rb @ line 144: +Owner: Rack::Builder + +def to_app + fail "missing run or map statement" unless @run + @use.reverse.inject(@run) { |a,e| e[a] } +end +``` + +上述方法将所有传入 `#use` 和 `#run` 命令的应用和中间件进行了组合,通过 `#inject` 方法达到了如下所示的效果: + +```ruby +# config.ru +use MiddleWare1 +use MiddleWare2 +run RackApp + +# equals to +MiddleWare1.new(MiddleWare2.new(RackApp))) +``` + +`Builder` 类其实简单来看就做了这件事情,将一种非常难以阅读的代码,变成比较清晰可读的 DSL,最终返回了一个中间件(也可以说是应用)对象,虽然在 `Builder` 中也包含其他的 DSL 语法元素,但是在这里都没有介绍。 + +上一小节提到的 `#build_app` 方法其实也只是根据当前的环境选择合适的中间件继续包裹到这个链式的调用中: + +```ruby +From: lib/rack/server.rb @ line 343: +Owner: Rack::Server + +def build_app(app) + middleware[options[:environment]].reverse_each do |middleware| + middleware = middleware.call(self) if middleware.respond_to?(:call) + next unless middleware + klass, *args = middleware + app = klass.new(app, *args) + end + app +end +``` + +在这里的 `#middleware` 方法可以被子类覆写,如果不覆写该方法会根据环境的不同选择不同的中间件数组包裹当前的应用: + +```ruby +From: lib/rack/server.rb @ line 229: +Owner: #<Class:Rack::Server> + +def default_middleware_by_environment + m = Hash.new {|h,k| h[k] = []} + m["deployment"] = [ + [Rack::ContentLength], + [Rack::Chunked], + logging_middleware, + [Rack::TempfileReaper] + ] + m["development"] = [ + [Rack::ContentLength], + [Rack::Chunked], + logging_middleware, + [Rack::ShowExceptions], + [Rack::Lint], + [Rack::TempfileReaper] + ] + m +end +``` + +`.default_middleware_by_environment` 中就包含了不同环境下应该使用的中间件,`#build_app` 会视情况选择中间件加载。 + +### webserver 的选择 + +在 `Server#start` 方法中,我们已经通过 `#wrapped_app` 方法将应用和中间件打包到了一起,然后分别执行 `#server` 和 `Server#run` 方法选择并运行 webserver,先来看 webserver 是如何选择的: + +```ruby +From: lib/rack/server.rb @ line 300: +Owner: Rack::Server + +def server + @_server ||= Rack::Handler.get(options[:server]) + unless @_server + @_server = Rack::Handler.default + end + @_server +end +``` + +如果我们在运行 `rackup` 命令时传入了 `server` 选项,例如 `rackup -s WEBrick`,就会直接使用传入的 webserver,否则就会使用默认的 Rack 处理器: + +```ruby +From: lib/rack/handler.rb @ line 46: +Owner: #<Class:Rack::Handler> + +def self.default + # Guess. + if ENV.include?("PHP_FCGI_CHILDREN") + Rack::Handler::FastCGI + elsif ENV.include?(REQUEST_METHOD) + Rack::Handler::CGI + elsif ENV.include?("RACK_HANDLER") + self.get(ENV["RACK_HANDLER"]) + else + pick ['puma', 'thin', 'webrick'] + end +end +``` + +在这个方法中,调用 `.pick` 其实最终也会落到 `.get` 方法上,在 `.pick` 中我们通过遍历传入的数组**尝试**对其进行加载: + +```ruby +From: lib/rack/handler.rb @ line 34: +Owner: #<Class:Rack::Handler> + +def self.pick(server_names) + server_names = Array(server_names) + server_names.each do |server_name| + begin + return get(server_name.to_s) + rescue LoadError, NameError + end + end + + raise LoadError, "Couldn't find handler for: #{server_names.join(', ')}." +end +``` + +`.get` 方法是用于加载 webserver 对应处理器的方法,方法中会通过一定的命名规范从对应的文件目录下加载相应的常量: + +```ruby +From: lib/rack/handler.rb @ line 11: +Owner: #<Class:Rack::Handler> + +def self.get(server) + return unless server + server = server.to_s + + unless @handlers.include? server + load_error = try_require('rack/handler', server) + end + + if klass = @handlers[server] + klass.split("::").inject(Object) { |o, x| o.const_get(x) } + else + const_get(server, false) + end + +rescue NameError => name_error + raise load_error || name_error +end +``` + +一部分常量是预先定义在 handler.rb 文件中的,另一部分是由各个 webserver 的开发者自己定义或者遵循一定的命名规范加载的: + +```ruby +register 'cgi', 'Rack::Handler::CGI' +register 'fastcgi', 'Rack::Handler::FastCGI' +register 'webrick', 'Rack::Handler::WEBrick' +register 'lsws', 'Rack::Handler::LSWS' +register 'scgi', 'Rack::Handler::SCGI' +register 'thin', 'Rack::Handler::Thin' +``` + +在默认的情况下,如果不在启动服务时指定服务器就会按照 puma、thin 和 webrick 的顺序依次尝试加载响应的处理器。 + +### webserver 的启动 + +当 Rack 已经使用中间件对应用进行包装并且选择了对应的 webserver 之后,我们就可以将处理好的应用作为参数传入 `WEBrick.run` 方法了: + +```ruby +module Rack + module Handler + class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet + def self.run(app, options={}) + environment = ENV['RACK_ENV'] || 'development' + default_host = environment == 'development' ? 'localhost' : nil + + options[:BindAddress] = options.delete(:Host) || default_host + options[:Port] ||= 8080 + @server = ::WEBrick::HTTPServer.new(options) + @server.mount "/", Rack::Handler::WEBrick, app + yield @server if block_given? + @server.start + end + end + end +end +``` + +所有遵循 Rack 协议的 webserver 都会实现上述 `.run` 方法接受 `app`、`options` 和一个 block 作为参数运行一个进程来处理所有的来自用户的 HTTP 请求,在这里就是每个 webserver 自己需要解决的了,它其实并不属于 Rack 负责的部门,但是 Rack 实现了一些常见 webserver 的 handler,比如 CGI、Thin 和 WEBrick 等等,这些 handler 的实现原理都不会包含在这篇文章中。 + +## Rails 和 Rack + +在了解了 Rack 的实现之后,其实我们可以发现 Rails 应用就是一堆 Rake 中间件和一个 Rack 应用的集合,在任意的工程中我们执行 `rake middleware` 的命令都可以得到以下的输出: + +```ruby +$ rake middleware +use Rack::Sendfile +use ActionDispatch::Static +use ActionDispatch::Executor +use ActiveSupport::Cache::Strategy::LocalCache::Middleware +use Rack::Runtime +use ActionDispatch::RequestId +use ActionDispatch::RemoteIp +use Rails::Rack::Logger +use ActionDispatch::ShowExceptions +use ActionDispatch::DebugExceptions +use ActionDispatch::Reloader +use ActionDispatch::Callbacks +use ActiveRecord::Migration::CheckPending +use Rack::Head +use Rack::ConditionalGet +use Rack::ETag +run ApplicationName::Application.routes +``` + +在这里包含了很多使用 `use` 加载的 Rack 中间件,当然在最后也包含一个 Rack 应用,也就是 `ApplicationName::Application.routes`,这个对象其实是一个 `RouteSet` 实例,也就是说在 Rails 中所有的请求在经过中间件之后都会先有一个路由表来处理,路由会根据一定的规则将请求交给其他控制器处理: + +![rails-application](images/rack/rails-application.png) + +除此之外,`rake middleware` 命令的输出也告诉我们 Rack 其实为我们提供了很多非常方便的中间件比如 `Rack::Sendfile` 等可以减少我们在开发一个 webserver 时需要处理的事情。 + +## 总结 + +Rack 协议可以说占领了整个 Ruby 服务端的市场,无论是常见的服务器还是框架都遵循 Rack 协议进行了设计,而正因为 Rack 以及 Rack 协议的存在我们在使用 Rails 或者 Sinatra 开发 Web 应用时才可以对底层使用的 webserver 进行无缝的替换,在接下来的文章中会逐一介绍不同的 webserver 是如何对 HTTP 请求进行处理以及它们拥有怎样的 I/O 模型。 + +## Reference + ++ [Rack · A modular Ruby webserver interface](https://github.com/rack/rack) ++ [Rack: a Ruby Webserver Interface](http://rack.github.io) ++ [Rack interface specification](http://rubydoc.info/github/rack/rack/master/file/SPEC) ++ [Rails on Rack](http://guides.rubyonrails.org/rails_on_rack.html) ++ [Rack Middleware](http://railscasts.com/episodes/151-rack-middleware) ++ [Introducing Rack](http://chneukirchen.org/blog/archive/2007/02/introducing-rack.html) ++ [Ruby on Rails Server options](https://stackoverflow.com/questions/4113299/ruby-on-rails-server-options) + diff --git a/objc/objc-runtime/objc.xcodeproj/project.xcworkspace/xcuserdata/Draveness.xcuserdatad/UserInterfaceState.xcuserstate b/objc/objc-runtime/objc.xcodeproj/project.xcworkspace/xcuserdata/Draveness.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index 263c1b0..0000000 Binary files a/objc/objc-runtime/objc.xcodeproj/project.xcworkspace/xcuserdata/Draveness.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ