diff --git a/.DS_Store b/.DS_Store
index b831c72..c4c15af 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/Chapter1 - iOS/1.101.md b/Chapter1 - iOS/1.101.md
index 754a353..20b6250 100644
--- a/Chapter1 - iOS/1.101.md
+++ b/Chapter1 - iOS/1.101.md
@@ -1,6 +1,9 @@
# 离屏渲染
## 什么是离屏渲染
+什么是在屏渲染?
+在当前屏幕渲染,指的是 GPU 的渲染操作是在当前用于显示的屏幕缓冲区中进行的。
+
如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的 Frame Buffer(帧缓冲区),作为像素数据存储区域,然后由显示控制器把帧缓冲区的数据显示到屏幕上。如果因为面临一些限制,比如阴影、光栅、遮罩等,CPU 无法把渲染结果直接写入 Frame Buffer,而是先暂时把中间的临时状态保存在额外的内存区域,之后再写入 Frame Buffer,那么这个过程被称为离屏渲染。
系统如果没有直接把渲染结果直接写入到 GPU FrameBuffer 中,则认为发生了一次离屏渲染。(离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作)
@@ -60,13 +63,17 @@ self.imageView.layer.cornerRadius = YES;
```
## 离屏渲染的影响?
+触发离屏渲染后,会增加 GPU 的工作量,CPU + GPU 工作时间可能会超过一次渲染周期,会发生 UI 卡顿。
+
离屏渲染,指的是 GPU 在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。由上面的一个结论视图和圆角的大小对帧率并没有什么影响,数量的多少才显著影响性能。可以知道离屏渲染耗时是发生在离屏这个动作上面,而不是渲染。为什么离屏这么耗时?原因主要有创建缓冲区和上下文切换。创建新的缓冲区代价都不算大,付出最大代价的是上下文切换。
上下文切换,不管是在GPU渲染过程中,还是广为人知的进程切换,上下文切换都是一个相当耗时的操作。首先要保存当前屏幕渲染环境,然后切换到一个新的绘制环境,申请绘制资源,初始化环境,然后开始一个绘制,绘制完毕后销毁这个绘制环境,如需要切换到On-Screen Rendering 或者再开始一个新的离屏渲染重复之前的操作。
-一次mask发生了两次离屏渲染和一次主屏渲染。即使忽略昂贵的上下文切换,一次mask需要渲染三次才能在屏幕上显示,这已经是普通视图显示3陪耗时,若再加上下文环境切换,一次mask就是普通渲染的n(n>3)倍以上耗时操作
+一次 mask 发生了两次离屏渲染和一次主屏渲染。即使忽略昂贵的上下文切换,一次mask需要渲染三次才能在屏幕上显示,这已经是普通视图显示3陪耗时,若再加上下文环境切换,一次 mask 就是普通渲染的n(n>3)倍以上耗时操作
正常流程:App Source Code -> CPU -> Frame Buffer -> Dispaly
离屏渲染流程:App Source Code -> CPU -> Off Screen Frame Buffer -> Frame Buffer -> Dispaly
+
+
## 如何优化?
- 针对 shadow 可以增加 shadowPath
- 针对圆角可以增加贝塞尔曲线或者一张图片实现(类似遮罩)。
diff --git a/Chapter1 - iOS/1.102.md b/Chapter1 - iOS/1.102.md
index 33d7431..044759c 100644
--- a/Chapter1 - iOS/1.102.md
+++ b/Chapter1 - iOS/1.102.md
@@ -10,7 +10,7 @@ LLVM 不是 low level virtual machine 的缩写,就是项目名称。
## 结构
-
+
LLVM 由三部分构成:
@@ -22,7 +22,7 @@ LLVM 由三部分构成:
-
+
正是由于这样的设计,使得 LLVM 具备很多有点:
@@ -42,7 +42,7 @@ LLVM 现在被作为实现各种静态和运行时编译语言的通用基础结
广义上来讲,LLVM 说的是一种架构。狭义上来讲,LLVM 强调的是偏后端部分,如下图的除了 clang 编译前端外的部分,包括优化器和编译后端,统称为 LLVM 后端。
-
+
@@ -64,7 +64,7 @@ Clang 相较于 GCC,具备下面优点:
- 设计清晰简单,容易理解,易于扩展增强
-
+
@@ -92,7 +92,7 @@ clang -ccc-print-phases main.m
展示如下:
-
+
@@ -104,7 +104,7 @@ clang -ccc-print-phases main.m
查看 preprocessor (预处理)的结果:`clang -E main.m`。预处理主要做的事情就是头文件导入( include、import)、宏定义替换等。展示如下:
-
+
@@ -112,7 +112,7 @@ clang -ccc-print-phases main.m
词法分析阶段,主要生成 Token。使用指令 `clang -fmodules -E -Xclang -dump-tokens main.m` 查看具体做了什么
-
+
@@ -120,7 +120,7 @@ clang -ccc-print-phases main.m
语法分析阶段,生成语法树(AST,Abstract Syntax Tree)。使用指令 `clang -fmodules -fsyntax-only -Xclang -ast-dump main.m` 查看
-
+
对 main.m 的代码进行改造
@@ -142,7 +142,7 @@ void test(int a, int b) {
再次查看 AST 可以加深理解
-
+
其中:
@@ -154,7 +154,7 @@ void test(int a, int b) {
也就是先运算蓝色框内的值,然后用结果和红色框内的进行相减。所以这是很标准的树形结构。
-
+
@@ -171,7 +171,7 @@ LLVM IR 有3种表示格式:
- text:便于阅读的文本格式,类似于汇编语言,推展名为 `.ll`。使用指令 `clang -S -emit-llvm main.m` 进行转换
-
+
学过 arm64 汇编的话看这段 IR 很眼熟,汇编里 `load` 相关的指令都是从内存中装载数据,比如 `ldr`、`ldur` 、`ldp`。`store` 相关的指令是往内存中写入数据,比如 `str`、 `stur`、 `stp`
@@ -304,9 +304,9 @@ Tips:ninja 如果安装失败,可以直接从 [github]( https://github.com/n
因为要编写 Clang 插件,是 c++ 代码,所以需要借助 IDE 的能力,我们选用 Xcode 进行编译。如下图所示,代表编译成功
-
+
-
+
@@ -334,11 +334,11 @@ Tips:ninja 如果安装失败,可以直接从 [github]( https://github.com/n
- 先创建一个插件文件夹 `code-style-validate-plugin`
-
+
- 编辑 `CMakeLists.txt` 文件,在最后添加 `add_clang_subdirectory(code-style-validate-plugin)`
-
+
@@ -367,17 +367,17 @@ Xcode 打开项目,选择自动创建 Schemes
-
+
选择 Target 为 `CodeStyleValidatePlugin`,源代码所在文件夹为 `Sources/Loadable modules`,然后选中 CodeStyleValidatePlugin.cpp` 文件进行编写逻辑
-
+
初步编写后 Command + B 进行编译,在 Products 下可以看到编译产物:`CodeStyleValidatePlugin.dylib` 动态库。
-
+
@@ -387,7 +387,7 @@ Xcode 打开项目,选择自动创建 Schemes
此步骤的目的是:在 testLLVM 项目中,加载 `CodeStyleValidatePlugin.dylib` 插件可以成功。因为默认的 Xcode 使用的 clang/clang++ 编译器和编译 `CodeStyleValidatePlugin.dylib` 动态库不是一个版本。不做修改的话,Xcode 加载 `CodeStyleValidatePlugin.dylib` 会报错。所以需要先编译出同一个 LLVM 版本的 clang/clang++。
-
+
@@ -406,7 +406,7 @@ Xcode 打开项目,选择自动创建 Schemes
- `-Xclang`
- 插件名称
-
+
@@ -414,7 +414,7 @@ Xcode 打开项目,选择自动创建 Schemes
在新创建的 TestLLVM Xcode 项目中加载创建的 `CodeStyleValidatePlugin.dylib` 会报错。原因是:由于 Clang 插件需要使用对应的版本去加载,如果版本不一致则会导致编译错误。如下所示:
-
+
@@ -426,17 +426,17 @@ Xcode 打开项目,选择自动创建 Schemes
如下所示:
-
+
继续编译还是会报错,报错如下:
-
+
解决方案为:在 `Build Settings` 栏目中搜索 `index`,将 `Enable Index-Wihle-Building Functionality` 的 ` Default` 改为 `NO`。
-
+
@@ -448,7 +448,7 @@ Tips: 由于重新修改了插件的源码,所以每次 Build 构建完 FANP
编译成功,可以看到在日志中输出了我们编写的日志信息。
-
+
@@ -481,7 +481,7 @@ NS_ASSUME_NONNULL_END
利用 Clang 查看 AST 指令为 `clang -fmodules -fsyntax-only -Xclang -ast-dump workaholic_person.m`
-
+
核心思路为:我们要分析类名不符合规范的情况,要精确报错,首先要识别到类名,利用 AST 的能力可以办到(类名在 AST 的 `ObjCInterfaceDecl` 节点上)。然后获取到类名的行号信息,精确报错。
@@ -771,7 +771,7 @@ X("CodeStyleValidatePlugin", "This plugin is designed for scanning code styles,
- 可以对 Category 名做检测,如果带下划线,则报错提示并给出修改意见
- 编写的 `CodeStyleValidatePlugin` Demo 中对不符合规范的做了 `DiagnosticsEngine::Warning` 级别的警告。如果遇到1个警告则不影响,继续编译。如果是 `DiagnosticsEngine::Error` 级别的编译报错,遇到1个则终止编译,请注意该区别,按需编写自己的插件逻辑。
-
+
@@ -819,7 +819,7 @@ X("CodeStyleValidatePlugin", "This plugin is designed for scanning code styles,
- 使用 LLVM 编写 Clang 插件,解析 AST 拿到所有的 `ObjCInterfaceDecl` 信息,然后结合 `ObjCMethodDecl` 信息便可获取 Category 中的 所有方法,再判断方法是否同名
-
+
diff --git a/Chapter1 - iOS/1.104.md b/Chapter1 - iOS/1.104.md
index 60f475b..3cd9f5c 100644
--- a/Chapter1 - iOS/1.104.md
+++ b/Chapter1 - iOS/1.104.md
@@ -2,6 +2,8 @@
> 有人聊起来 NSNotification 可以在不同的线程发和接收吗?对于不知道或者不确定的知识,有必要探究记录下
+
+
## NSNotificationCenter
```objectivec
@@ -24,6 +26,8 @@
直接上 GUNStep 源码探索下
+
+
### Observation
```c
@@ -38,6 +42,8 @@ typedef struct Obs {
结构体存储了 observer、selector 信息。此外可以看出,是一个链表结构(next),指向注册了同一个通知的下一个观察者。
+
+
### NCTbl
```c
@@ -55,11 +61,13 @@ typedef struct NCTbl {
- nemeless:同于保存添加观察者时没有传递 NotificationName 的情况
+
+
### named Table
该表用于存储添加观察者时传了 NotificationName 的情况。也就是 Named Table 中,NotificationName 作为 key。在使用系统 API 注册观察者的时候还可以传入 object 参数,表示只监听该对象发出的通知,所以还需要一张表存储 object 和 observer 的对应关系,object 为 key,observer 为 value。
-
+
- 第一个 MapTable key 为 notificationName,value 为另一个 MapTable(子 Table)
@@ -71,7 +79,7 @@ typedef struct NCTbl {
nameless Table 结构较为简单,因为没有 notificationName,所以就一层 MapTable。key 为 object,value 为链表,存储所有的观察者。
-
+
### wildcard
diff --git a/Chapter1 - iOS/1.105.md b/Chapter1 - iOS/1.105.md
index a6e985d..914189a 100644
--- a/Chapter1 - iOS/1.105.md
+++ b/Chapter1 - iOS/1.105.md
@@ -1,5 +1,60 @@
# iOS 界面渲染流程
+> 下面几个问题你熟悉吗?
+>
+> - 为什么调用 `[UIView serNeedsDisplay]` 并没有立刻发生当前视图的绘制工作?
+
+
+
+## 视图显示原理
+
+为什么调用 `[UIView setNeedsDisplay]` 并没有立刻发生当前视图的绘制工作?
+
+UIView 绘制流程。
+
+
+
+
+
+当调用 UIView `[UIView setNeedsDisplay]` 方法时,系统会立刻调用其 Layer 的同名方法 `[view.layer setNeedsDisplay]` 方法,之后相当于给当前 Layer 打上一个脏标记,之后会在当前 RunLoop 快要结束的时候才会调用 Layer 的 `[CALayer display]` 方法。然后进入当前 UIView 真正的绘制流程中。
+
+其次,会判断 CALayer 的代理,有没有实现 `displayLayer:` 方法,如果没有实现,则进入系统的绘制流程中;如果实现了,则可能是异步绘制或者自定义渲染的实现。
+
+Tips:`[UIView setNeedsDisplay]` 之后并不会立马调用 `[view.layer setNeedsDisplay]` 方法。要么手动触发 `[view.layer setNeedsDisplay]` 要么,调用一下 `-(void)drawRect:(CGRect)rect` 方法(即使是空实现也没关系)。
+
+下面来个 Demo 展示下简单的异步绘制一个 String。
+
+
+
+
+
+
+
+接下来看看系统的绘制实现流程:
+
+
+
+如何实现异步绘制?
+
+`[layer.delegate displayPlayer:]`
+
+- 代理负责生成对应的 bitmap
+- 设置该 bitmap 作为 layer.contents 属性的值
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
## 渲染机制

diff --git a/Chapter1 - iOS/1.108.md b/Chapter1 - iOS/1.108.md
index e74de54..3a5b878 100644
--- a/Chapter1 - iOS/1.108.md
+++ b/Chapter1 - iOS/1.108.md
@@ -346,7 +346,7 @@ Xcode 选择 products,show In Finder。然后上上层的 `Intermediates.noind
5. 在 `CodeCoverageAnalysis2` 目录下利用指令 `lcov -c -d . -o CodeCoverage2.info` 生成新的一份覆盖率信息 `CodeCoverage2.info`
-6. 然后利用 `locv -a` 指令合并2个 `.info` 文件。指令为 `lcov -a CodeCoverage2.info -a ./../CodeCoverageAnalysis/CodeCoverage.info -o CodeCoverageCombined.info`
+6. 然后利用 `locv -a` 指令合并2个 `.info` 文件。指令为 `lcov -a CodeCoverage2.info -a https://github.com/FantasticLBP/knowledge-kit/raw/master/CodeCoverageAnalysis/CodeCoverage.info -o CodeCoverageCombined.info`
7. 然后利用 `genhtml` 生成合并后的覆盖率可视化 html 文件 `genhtml -o html CodeCoverageCombined.info`
@@ -396,7 +396,7 @@ Ruby 脚本利用 [xcodeproj](https://github.com/CocoaPods/Xcodeproj) 对每个
```ruby
require 'xcodeproj'
-CONFIG_DIR = Pathname.new(File.join(File.dirname(__FILE__), "../../../..")).realpath
+CONFIG_DIR = Pathname.new(File.join(File.dirname(__FILE__), ".https://github.com/FantasticLBP/knowledge-kit/raw/master/../..")).realpath
CONFIG_FILE = File.join(CONFIG_DIR, "CodeCoverageConfig.rb")
def update(args)
@@ -427,7 +427,7 @@ update(ARGV)
代码不变的情况下,发现 QA 或者开发自己测试的情况下,发现代码覆盖率不高,测试没有全面,则继续测试。这样生成多分 `.gcda` 文件,
- 生成覆盖率:`lcov -c -d {$SOURCE} -o {$DEST_INFO}`,比如 `lcov -c -d . -o CodeCoverage2.info`
-- 合并覆盖率:`lcov -a {$SOURCE_INFO_1} -a {$SOUCE_INFO_2} -o {$DEST_INFO}`,比如 `lcov -a CodeCoverage2.info -a ./../CodeCoverageAnalysis/CodeCoverage.info -o CodeCoverageCombined.info`
+- 合并覆盖率:`lcov -a {$SOURCE_INFO_1} -a {$SOUCE_INFO_2} -o {$DEST_INFO}`,比如 `lcov -a CodeCoverage2.info -a https://github.com/FantasticLBP/knowledge-kit/raw/master/CodeCoverageAnalysis/CodeCoverage.info -o CodeCoverageCombined.info`
@@ -759,7 +759,7 @@ xcrun llvm-cov show\
我这边具体指令为:
-`xcrun llvm-cov show -use-color -format=html -arch=x86_64 -instr-profile=SwiftCodeCoverage.profdata SwiftCodeCoverage ./../ -output-dir ./SwiftCodeCoverageReport `
+`xcrun llvm-cov show -use-color -format=html -arch=x86_64 -instr-profile=SwiftCodeCoverage.profdata SwiftCodeCoverage https://github.com/FantasticLBP/knowledge-kit/raw/master/ -output-dir ./SwiftCodeCoverageReport `
第十一步,查看整体的覆盖率信息与单个文件的覆盖率,查看代码执行情况
@@ -791,7 +791,7 @@ xcrun llvm-cov show\
3. 利用指令将 `.txt` 改为 `.profdata` 格式。`xcrun llvm-profdata merge SwiftCodeCoverageCombined.txt -o SwiftCodeCoverageCombinedFromText.profdata`
-4. 再根据合并后的 `.profdata` 生成 html 覆盖率报告。`xcrun llvm-cov show -use-color -format=html -arch=x86_64 -instr-profile=SwiftCodeCoverageCombinedFromText.profdata SwiftCodeCoverage ./../ -output-dir ./SwiftCodeCoveragCombinedReportFromText`
+4. 再根据合并后的 `.profdata` 生成 html 覆盖率报告。`xcrun llvm-cov show -use-color -format=html -arch=x86_64 -instr-profile=SwiftCodeCoverageCombinedFromText.profdata SwiftCodeCoverage https://github.com/FantasticLBP/knowledge-kit/raw/master/ -output-dir ./SwiftCodeCoveragCombinedReportFromText`
效果如下:
diff --git a/Chapter1 - iOS/1.110.md b/Chapter1 - iOS/1.110.md
index 7317cb8..fdf318d 100644
--- a/Chapter1 - iOS/1.110.md
+++ b/Chapter1 - iOS/1.110.md
@@ -403,3 +403,29 @@ OrderSumitValidatorFactory {
最后选什么?组合优于继承,个人倾向使用责任链模式去组织代码。关于责任链设计模式的文章也可以看这篇[文章](./../Chapter6%20-%20Design%20Pattern/6.23.md)
+
+
+## 拓展
+
+如果业务真的是高频迭代变化,但校验顺序不变的话,甚至可以做成多端协定后,对应业务校验编号和业务关联,动态下发
+
+```json
+// Version1
+{
+ "validatorRuleOrder": ["1", "4", "3", "2"]
+}
+
+// Version2
+{
+ "validatorRuleOrder": ["1", "3", "4", "2"]
+}
+```
+
+App 动态请求,然后执行业务逻辑。需思考一些问题:
+
+- 网络请求慢怎么处理?
+- 需不需要缓存?
+- 有缓存的花,更新策略是什么?
+- 需不需要内置的产品逻辑?
+
+当然,这不在本篇文章范畴内,不做展开。
diff --git a/Chapter1 - iOS/1.135.md b/Chapter1 - iOS/1.135.md
new file mode 100644
index 0000000..4d0464b
--- /dev/null
+++ b/Chapter1 - iOS/1.135.md
@@ -0,0 +1,234 @@
+# 框架设计
+
+
+
+## 图片框架
+
+### 角色
+
+- Manager
+ - 内存
+ - 磁盘
+ - 网络
+- Code Manager
+ - 图片解码
+ - 图片压缩/解压缩
+
+
+
+### 图片读写过程
+
+- 以图片 url 的 hash 值为 key,存储
+
+
+
+### 读取过程
+
+
+
+
+
+### 内存设计
+
+- 内存存储的空间
+
+ - 10kb 以下,使用场景多,设计50张容量
+ - 100kb 以下,使用场景次之,设计20张容量
+ - 100kb 以上,使用场景最小,设计10张容量
+
+- 淘汰策略
+
+ 队列实现,先进先出。
+
+
+
+### 淘汰策略
+
+LRU,最近最久未使用算法。比如3天内没有使用过的,则认为需要被淘汰。
+
+
+
+### 淘汰时机
+
+- 定时检查,比如 30分钟检查一次
+
+- 提高检查频率:
+
+ - 每次进行图片读写时
+ - 前后台切换时
+
+
+
+### 磁盘设计
+
+- 存储方式
+- 大小限制(如200MB)
+- 淘汰策略:如果某一张图片存储时间距今已经超过7天
+
+
+
+### 网络设计
+
+- 图片请求的最大并发量
+- 请求超时策略。超时重试1次,再次超时则取消
+- 请求优先级
+
+
+
+### 图片解码
+
+对于不同格式的图片,图片解码怎么处理?
+
+应用**策略模式**,对不同图片格式进行解码。一方面可以解码不同格式、另一个方面替换解码算法,对于稳定性有帮助
+
+在哪个阶段进行解码?
+
+磁盘读取后、网络请求返回后
+
+
+
+### 线程处理
+
+
+
+## 阅读时长记录器
+
+
+
+### 记录器种类
+
+- 页面式:普通的 push、pop 页面
+- feed 流式:类似 weibo 这种 feed 流式的记录
+- 自定义式:可拓展性的体现,面向未来
+
+
+
+QA:为什么要有不同类型的记录器?
+
+- 基于不同分类场景提供的关于记录的封装、适配
+-
+
+### 记录数据存储
+
+- 内存缓存
+- 磁盘存储
+
+
+
+### 准确性
+
+数据收集(存储)、上报(移除)2个核心流程。准确性也和这2个方面息息相关。
+
+- 定时写磁盘 从内存中 flush 到本地磁盘。定时器1分钟 flush 一次
+- 限定内存缓存条数。超过该条数,即写磁盘。内存记录每满10条 flush 一次
+
+
+
+### 上传策略
+
+思考:
+
+- 需要立马上传吗?每收集到1次页面阅读时长就需要立马上传1次吗?ROI 衡量。性能、线程数
+- 关于延时上传的场景有哪些?
+
+
+
+上传时机:
+
+- 定时器,比如每5分钟上传1次。
+- 前后台切换,比如从后台切换到前台触发1次上传逻辑
+- 无网切换到有网
+
+
+
+### 网络上传效率
+
+自定义报文,高效传输。
+
+iOS 小端序,网络大端序。
+
+
+
+
+
+## 复杂页面架构设计
+
+- MVVM
+
+- Redux 数据流
+
+
+
+## 客户端架构
+
+
+
+## 业务之间解耦后的通信方式
+
+- openURL
+- 依赖注入:中间层
+
+
+
+## AFNetworking
+
+### 主要类关系图
+
+
+
+
+
+ ### AFURLSessionManager
+
+- 创建和管理 NSURLSession、NSURLSessionTask
+- 实现 NSURLSessionDelegate 协议代理方法,处理网络请求的重定向、认证、网络数据的处理
+- 引入 AFSecurityPolicy,用来保证请求安全
+- 引入 AFNetworkReachabilityManager 监控网络状态
+
+
+
+## SDWebImage
+
+### 架构图
+
+
+
+
+
+## 图片加载流程
+
+- 查找内存缓存
+- 查找磁盘缓存
+- 网络下载图片并磁盘缓存
+
+
+
+## AsyncDisplayKit
+
+### 主要处理问题
+
+主要通过减轻主线程压力,尽量将一些可以放到子线程的任务都放到子线程处理,减轻主线程压力
+
+主要分3方面:
+
+- UI 布局 layout:文本宽高计算、视图布局计算
+- 渲染 Rendering:文本渲染、图片解码、图形绘制
+- UIKit Objects:对象创建、调整、销毁
+
+
+
+### 基本原理
+
+
+
+- UIView 作为 CALayer 的代理实现。CALayer 作为 UIView 的实例变量,负责展示规工作。
+
+- 针对 UIView 的修改,都抽象为针对 ASNode 的修改,这些修改可以在子线程进行。针对 ASNode 的修改和提交,会对其进行封装,提交到一个全局容器中。
+- 对 Runloop 状态进行监听,进入休眠前,ASDK 执行该 loop 内提交的所有任务。
+
+
+
+
+
+
+
diff --git a/Chapter1 - iOS/1.136.md b/Chapter1 - iOS/1.136.md
new file mode 100644
index 0000000..8a39588
--- /dev/null
+++ b/Chapter1 - iOS/1.136.md
@@ -0,0 +1,142 @@
+
+
+## 多环境配置
+多环境配置的3种方式:
+- 多 target 配置
+- Scheme 多 target 进行环境配置
+- xconfig 文件配置
+
+QA:Target、Scheme 的关系是什么?
+- Project:包含了项目所有的代码、资源文件,所有信息
+- Scheme:对于指定 Target 的环境配置
+- Target:对于指定代码和资源文件的具体构建方式
+
+
+
+## 多环境配置的不同方式
+
+### 多 Target 的方式
+
+针对需要多配置的项目,在 Xcode 中,对其进行复制,存在多个 Target。搭配不同的宏定义,来实现控制逻辑的效果。
+
+注意:duplicate 之后,target 虽然多了一份,但是代码和资源不变,所以
+
+
+
+
+
+
+
+
+
+- OC:Build Settings -> Preprocessor Macros 里面的 Debug/Release 模式下添加自定义宏。比如在 debug 模式下 `IsOCDebug = 1`
+- Swift:Build Settings -> Other Swift Flags 里的 Debug/Release 模式下添加自宏定义。注意命名有格式要求:`-D + 宏名称`
+
+
+
+
+
+当对某个 Target “Duplicate” 之后,会产生一份新的 plist 文件
+
+
+
+
+
+
+
+思考:该方式还是存在的问题:多个 info.plist、配置比较乱
+
+
+
+### 多 Scheme 的方式
+
+针对一个 Target 可以添加多个 Scheme,步骤如下
+
+
+
+
+
+这样的创建好之后,该 Target 存在3个 Scheme 了。有了 Scheme 有什么作用呢?设置宏定义的时候可以针对不同的 Scheme 进行设置。
+
+
+
+
+
+针对 OC、Swift 分别设置了很多宏定义,接下去需要跑 Beta 配置的代码,怎么办?
+
+
+
+点击 Edit Scheme,在 Run 里面选择对应的 Scheme。
+
+但这样好像蛮烦的,每次运行不同配置的代码,都需要手动切换 Scheme。有没有什么办法解决切换问题呢。
+
+
+
+创建实体 Scheme
+
+创建 Scheme 步骤:Xcode -> New Scheme,再弹出的方框内,选择对应的 Target,然后输入需要创建的 Scheme 名称。此次我们创建了:Debug、Beta 2个新的 Scheme。
+
+
+
+
+
+创建好之后,可以看到实体 Scheme 和虚拟 Scheme 存在多对多的关系。但我们可以基于此,选择实体的 Scheme,然后在 Run 里面 “Build Configuration” 里面选择对应的 Scheme 与之对应。
+
+
+
+
+
+创建之后就可以根据 Scheme 设置值,在 `Build Settings -> User-Defined` 下添加自定义的字段,同时可以根据 Scheme 设置不同的值。
+
+设置后的值怎么使用?将自定义的变量用 plist 存储。之后读取再使用。
+
+完整如下图:
+
+
+
+
+
+切换不同的 Scheme,可以运行不同的效果,当前 case 下,选择 Debug Scheme,输出不同结果 `HOST_URL: http://www.debug.baidu.com`
+
+
+
+思考:目前的方案已经优雅不少,但是还是存在,自定义宏的时候需要选择不同的 Scheme,过程繁琐。
+
+
+
+### Xcconfig
+
+Xcode 自带的 Configuration Settings File 可以支持自定义一些宏,还可以修改 Build Settings 里面的选项。
+
+创建步骤如下:
+
+
+
+文件命名为:`文件夹名称-项目名称.scheme名称.xcconfig`,比如 `Config-Xcconfig.debug.xcconfig`
+
+几个 Scheme 就创建几个对应的 Xcconfig 文件。
+
+
+
+修改和完善创建的 Xcconfig 配置文件里的内容。之后在 Xcode 的 Project 选项下,找到 Configurations,选择对应的 Scheme,然后选择右边对应的 Xcconfig 文件。如下图
+
+
+
+
+
+我们只在 `Config-Xcconfig.debug.xcconfig` 文件中添加了 `OTHER_LDFLAGS = -framework "AFNetworking"`,Xcode 切换到 debug scheme 下,然后 Command + B 编译。
+
+
+
+验证结果:
+
+- 编译前切换到 Build Settings 后,查看 ”Other Linker Flags“ 的 Debug 项为空
+- 编译后切换到 Build Settings 后,查看 ”Other Linker Flags“ 的 Debug 项为 `-framework ”AFNetworking“`
+
+因为 Xcconfig 文件,具有操作和修改 Build Settings 的能力,所以用好 Xcconfig 文件,不只可以实现替代宏定义和切换繁琐的问题,还可以实现很多其他手动修改 Build Settings 的问题。
+
+
+
+说明:在 Xcode Build Settings 手动配置的信息,和通过 Xcconfig 方式编写的信息,不会冲突。
+
+对于 xcconfig 文件,我们其实并不陌生、因为在使用 Cocoapods 的时候就已经在使用这个文件了,只是很多人不知道其中变量的含义。
\ No newline at end of file
diff --git a/Chapter1 - iOS/1.137.md b/Chapter1 - iOS/1.137.md
new file mode 100644
index 0000000..e69de29
diff --git a/Chapter1 - iOS/1.16.md b/Chapter1 - iOS/1.16.md
index a44c262..8402da9 100644
--- a/Chapter1 - iOS/1.16.md
+++ b/Chapter1 - iOS/1.16.md
@@ -1,10 +1,49 @@
# Swift、OC混编
-```
-1、在oc文件中使用swift文件。
-选中项目TARGETS->Building Settings->搜索“Objective-C Genereated Interface Header Name”对应的名字。
-在oc文件中需要使用swift的地方,头文件导入上一步对应的名字。
+## apinotes 文件
+
+
+
+经常在 Swift、OC 混编的时候,系统会给方法命名等做一些优化,比如 OC 侧的枚举,在 Swift 就是结构体。为了代码规范或者某些因素考量,我们需要做一些约定,不让编译器自动处理,比如一些常见的宏:
+
+- `NS_SWIFT_NAME`
+- `NS_TYPED_EXTENSIABLE_ENUM`
+- `NS_REFINED_FOR_SWIFT`
+
+宏来配置存在弊端,手动去处理一个工程、一个 SDK 的话,假设有10000个方法,工作量太大。
+
+Xcode 推出解决方案:
+
+- 创建 `SDK名称.apinotes` 文件
+- 放到 SDK 根目录下
+- 按照 yaml 格式,编写内容
+
+比如:
+
+```yaml
+---
+Name: PersonFramework
+Classes:
+- Name: WorkHard
+# SwiftName: WorkHardAtSwift
+ Methods:
+ - Selector: "upgradeToLeader:"
+ Parameters:
+ - Position: 0
+ Nullability: O
+ MethodKind: Instance
+ SwiftPrivate: true
+ # Availability: nonswift // WorkHard 类的 upgradeToLeader 方法,在 Swift 侧不允许调用
+ #AvailabilityMsg: "prefer 'deinit'" // 如果调用,则提示对应的信息
+ - Selector: "initWithName:"
+ MethodKind: Instance
+ DesignatedInit: true
```
+更多格式,请参考 [clang::APINOTES](https://clang.llvm.org/docs/APINotes.html)
+
+该方案是 Apple 标准做法,不是骚操作,Objc 源码中也有使用。如下所示
+
+
diff --git a/Chapter1 - iOS/1.38.md b/Chapter1 - iOS/1.38.md
index 9b4a93d..9edec7e 100644
--- a/Chapter1 - iOS/1.38.md
+++ b/Chapter1 - iOS/1.38.md
@@ -1,5 +1,11 @@
# RunLoop 探究
+> 为什么 main 函数可以保持一直运行而不退出?
+>
+> 卡顿如何监控
+
+
+
## RunLoop 是什么
- 运行循环
@@ -11,7 +17,7 @@
- 处理App中的各种事件(比如触摸事件、定时器事件等)
-- 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
+- 节省CPU资源,提高程序性能:该做事时做事,该休息时休息。休息和工作是从用户态切换到内核态,内核态切换到用户态的不断切换
- ......
@@ -29,13 +35,17 @@
先附上一张总结的非常棒的RunLoop图
-
+
-和
-
-## RunLoop API
+一言以蔽之,什么是 RunLoop?为什么 main 函数可以保持一直运行而不退出?
+
+iOS 侧 main 函数中,调用 UIApplicationMain 方法,内部启动主线程的 RunLoop,RunLoop 是一个事件循环的维护机制。有事情做的时候做事情(Source0、Source1),没有事做的时,从用户态到内核态的切换,去实现线程休眠。避免资源浪费。
+
+
+
+## RunLoop 几个重要角色
### 获取 RunLoop
@@ -65,6 +75,8 @@ NSRunLoop 是对 CFRunLoopRef 的一层 OC 包装,所以要了解 RunLoop 的
- RunLoop 在第一次获取时创建,在线程结束时消失
+
+
### RunLoop 相关的5个类
- CFRunLoopRef
@@ -103,6 +115,8 @@ struct __CFRunLoopMode {
};
```
+
+
### CFRunLoopModeRef 代表 RunLoop 的运行模式
- 一个 RunLoop 包含若干个 Mode,每个 Mode 包含若干个 Source/Timer/Observer
@@ -110,9 +124,7 @@ struct __CFRunLoopMode {
- 如果需要切换 Mode,只能退出 RunLoop,则以一个 Mode 进入
- 如果 Mode 里没有任何 Source0/Source1/Timer/Observer,RunLoop 会立马退出
-QA:为什么一个 RunLoop 需要创建这么多 Mode?
-这样做的目的是为了分隔开不同组的 Source/Timer/Observer 互不影响(性能、功能)。
系统默认注册了5个Mode
@@ -122,6 +134,26 @@ QA:为什么一个 RunLoop 需要创建这么多 Mode?
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
- kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
+
+
+Demo:
+
+
+
+
+
+结论:NSRunLoop 是对 CFRunLoop 的一层包装。
+
+
+
+QA:为什么一个 RunLoop 需要创建这么多 Mode?
+
+这样做的目的是为了分隔开不同组的 Source/Timer/Observer 互不影响(性能、功能)。
+
+在UITableView场景下,不同的 RunLoop Mode 主要是与 UITableView 的滑动优化和事件处理相关的。当 UITableView 滑动时,RunLoop的运行模式会从默认的 CFRunLoopDefaultMode 切换到 CFRunLoopTrackingMode,此 Mode 下 RunLoop 主要关注于处理与滑动相关的触摸事件和动画效果,而忽略其他类型的事件,如定时器事件。这是因为如果同时处理所有类型的事件,可能会导致滑动不流畅,影响用户体验。之前添加到 CFRunLoopDefaultMode 上的事件通知(如定时器事件)可能无法被及时处理,这就是为什么在UITableView 滑动时,添加到主线程的 NSTimer 可能会停止执行的原因。
+
+
+
### Source0、Source1、Timer、Observers 是什么
```c
@@ -138,12 +170,18 @@ RunLoop 在各个 Mode 下做事情,其实就是在处理某个 Mode 中的 So
Source0:
-- 屏幕触摸事件处理(非基于 Port 的事件),对应需要手动触发的事件,对应官方文档Input Source 中的 Custom 和 `performSelector:onThread` 事件源。
+- 屏幕触摸事件处理(非基于 Port 的事件),对应需要手动触发的事件,对应官方文档 Input Source 中的 Custom 和 `performSelector:onThread` 事件源。
- `performSelector:onThread:`
- 数组
+Demo:给屏幕点击事件加断点,查看堆栈可以看到是 Source0 触发的。
+
+
+
+
+
Source1:
- 基于 Port 的线程间通信,可以主动唤醒 RunLoop
@@ -156,7 +194,7 @@ Timers:
- NSTimer
-- `performSelector:withObject:afterDelay:`
+- `performSelector:withObject:afterDelay:`,底层也是 Timer
Observers:
@@ -179,6 +217,101 @@ CFRunLoopSourceRef 事件源(输入源)
- Source0:非基于 port 的,用户主动触发的事件
- Source1: 基于 port的,通过内核在线程间相互发送消息
+
+
+### 一对多的关系
+
+
+
+
+
+#### RunLoopTimer 的封装
+
+- `+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;`
+- `+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;`
+- `- (void)performSelector:(SEL)aSelector withObject: (id)argument afterDelay: (NSTimeInterval)seconds inModes: (NSArray*)modes;`
+- `+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;`
+- `- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;`
+
+
+
+#### CFRunLoopSource
+
+Source 是 RunLoop 的数据源抽象类(protocol)
+
+RunLoop 定义了2个 Version 的 Source:
+
+- Source0:处理 App 内部事件、App 自己负责管理(触发),如 UIEvent、CGSocket
+- Source1:由 RunLoop 和内核管理,Mach port 驱动,如 CFMachPort、CFMessagePort。
+
+定义如下:
+
+```c++
+struct __CFRunLoopSource {
+ CFRuntimeBase _base;
+ uint32_t _bits;
+ pthread_mutex_t _lock;
+ CFIndex _order; /* immutable */
+ CFMutableBagRef _runLoops;
+ union {
+ CFRunLoopSourceContext version0; /* immutable, except invalidation */
+ CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
+ } _context;
+};
+
+typedef struct {
+ CFIndex version;
+ void * info;
+ const void *(*retain)(const void *info);
+ void (*release)(const void *info);
+ CFStringRef (*copyDescription)(const void *info);
+ Boolean (*equal)(const void *info1, const void *info2);
+ CFHashCode (*hash)(const void *info);
+ void (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
+ void (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
+ void (*perform)(void *info);
+} CFRunLoopSourceContext;
+
+typedef struct {
+ CFIndex version;
+ void * info;
+ const void *(*retain)(const void *info);
+ void (*release)(const void *info);
+ CFStringRef (*copyDescription)(const void *info);
+ Boolean (*equal)(const void *info1, const void *info2);
+ CFHashCode (*hash)(const void *info);
+#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
+ mach_port_t (*getPort)(void *info);
+ void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
+#else
+ void * (*getPort)(void *info);
+ void (*perform)(void *info);
+#endif
+} CFRunLoopSourceContext1;
+```
+
+
+
+#### CFRunLoopObserver
+
+向外部报告 RunLoop 当前状态的更改。框架中很多机制都是由 RunLoopObserver 触发,比如 CAAnimation、AutoReleasePool。
+
+系统或者开发者很多都是 RunLoop 的业务方。
+
+
+
+#### CFRunLoopMode
+
+Mode 是 iOS App 滑动流畅的关键。
+
+不同任务被添加到不同 Mode 中去。
+
+UITrackingMode 模式下,核心关注滚动时 UI 流畅相关逻辑。
+
+
+
+
+
### CFRunLoopObserverRef 监听 RunLoop 状态变化
```objective-c
@@ -212,7 +345,7 @@ CFRelease(obersver);
```objective-c
//给 RunLoop 添加监听者
-- (void)testRunLoopObserver{
+- (void) {
//创建监听者
// CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopAllActivities, <#Boolean repeats#>, <#CFIndex order#>, <#CFRunLoopObserverCallBack callout#>, <#CFRunLoopObserverContext *context#>)
@@ -301,7 +434,7 @@ CFRelease(obersver);
*/
```
-
+
上个实验是在主线程对 RunLoop 进行的监听,但是由于是主线程是由系统创建的,所以系统也创建了对应的主 RunLoop,所以我们看不到 RunLoop 创建的状态,为了模拟完整的状态,我们开启子线程,在子线程中模拟
@@ -371,20 +504,22 @@ CFRelease(obersver);
*/
```
-## RunLoop 内部运行原理
-
+
+## RunLoop 运行原理
+
+### 运行原概要
+
+
- 图上左上角的 Input source 是早期 RunLoop 的分法,现在分法为:Source0 和 Source1。
- Source0:非基于 port 的,用户主动触发的事件。
- Source1:基于 port,通过内核和其它线程互相发送消息
- RunLoop 我们不能自己手动创建,而是可以通过 [NSRunLoop currentRunLoop] 方法获取,类似于懒加载。系统底层的做法是在全局维护了一个字典,字典的 key 和 value 分别是当前的线程和线程对应的 RunLoop,如果新开辟的线程没有对应的 RunLoop,系统则为其创建 RunLoop,并将其写入字典(线程、为其创建的 RunLoop)
-## RunLoopMode 的概念
-
-## 底层实现
+### 源码探究
内部就是 do-while 的循环,在这个循环内部不断处理各种任务(Timer、Source、Observer)
@@ -392,7 +527,7 @@ CFRelease(obersver);
但是如何直到系统是运行 RunLoop 的哪个函数?给 viewDidLoad 设置断点,在 lldb 模式输入 `bt` 查看堆栈
-
+
查看 CF 中 `CFRunLoop.c`源码。方法比较复杂,做了精简摘要
@@ -409,6 +544,8 @@ SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterva
}
```
+`CFRunLoopRunSpecific` 方法就是系统启动 RunLoop 的入口。内部先通知 Runloop 的观察者进入 Runloop 了,然后 调用 `__CFRunLoopRun` 执行核心逻辑(处理 timers、source 事件、block),最后告诉观察者退出 Runloop。
+
我们继续看看 `__CFRunLoopRun` 。源码很多很乱,对无关代码进行裁剪,便于理解流程逻辑
```c
@@ -472,10 +609,10 @@ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInter
} while (1);
// 通知 Observers:结束休眠
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
-
- handle_msg:;
+
+ handle_msg:; // 判断是怎么唤醒的 Runloop
__CFRunLoopSetIgnoreWakeUps(rl);
-
+ //
if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
@@ -490,7 +627,7 @@ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInter
__CFArmNextTimerInMode(rlm, rl);
}
}
-
+ // 被 Timer 唤醒,执行代码。
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
// On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
@@ -532,8 +669,7 @@ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInter
voucher_mach_msg_revert(voucherState);
os_release(voucherCopy);
-
- } while (0 == retVal);
+ } while (0 == retVal); // 当 retVal == 0 的时候结束 Runloop
return retVal;
}
```
@@ -931,9 +1067,7 @@ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInter
}
```
-__CFRunLoopModeIsEmpty
-
-此函数的作用就是判断这个 Mode 下面有没有 source0、source1、timer,只要存在就说明当前 Mode 不是空的,同时看看这个 Mode 是不是属于当前的 RunLoop
+`__CFRunLoopModeIsEmpty`函数的作用就是判断这个 Mode 下面有没有 source0、source1、timer,只要存在就说明当前 Mode 不是空的,同时看看这个 Mode 是不是属于当前的 RunLoop
```objective-c
// expects rl and rlm locked
@@ -993,9 +1127,51 @@ __CFRunLoopModeIsEmpty
}
```
-## RunLoop 休眠原理
-本质上就是函数 `__CFRunLoopServiceMachPort` 来控制实现休眠。查看 CFRunLoop.c 源码可以发现,是由 `mach_msg` 实现的。不是平时的在应用层 API 上 sleep 休眠的。比如 while 循环。`mach_msg` 休眠是从用户态切换到内核态,内核 api 控制休眠,做到真正节省影响的作用,等到由新消息来到,继续切换到用户态。
+
+RunLoop 内部几个核心的动作:`__CFRunLoopDoObservers`、`__CFRunLoopServiceMachPort` 、`__CFRunLoopDoTimers` 方法内部实现调用的还是 `__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__`、`__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__` 等方法,均以 `__CFRUNLOOP_IS_CALLING_OUT_TO_` 方法名作为开头。可以在堆栈上得以体现。
+
+
+
+### 运行流程
+
+上面结合源码看了 Runloop 是怎么运行的。下面通过图片看看 RunLoop 各个状态运行切换的完整流程。
+
+
+
+Demo:
+
+1. 上面第4步的 blocks 是指可以给 RunLoop 添加 Block 任务。
+
+ ```objective-c
+ CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopDefaultMode, ^{
+ NSLog(@"runloop block task");
+ });
+ ```
+
+2. 上面8>2,Runloop 处理 GCD Async To Main Quque
+
+
+
+
+
+
+
+
+
+### RunLoop 休眠原理
+
+> Runloop 在处理完 timer、source、block 后会检查有没有 source1 事件,没有则休眠。这个休眠是 while 循环死等吗?怎么实现的?
+
+可以在 App 运行过程中,点击 Xcode 左下角的 debug 暂停按钮,可以看到 App 堆栈存在系统调用
+
+
+
+
+
+本质上就是函数 `__CFRunLoopServiceMachPort` 来控制实现休眠。查看 CFRunLoop.c 源码可以发现,是由 `mach_msg` 实现的。不是平时的在应用层 API 上 sleep 休眠的。比如 while 循环。
+
+**`mach_msg` 休眠是从用户态切换到内核态,内核 api 控制休眠,做到真正节省资源的作用,等到由新消息来到,继续切换到用户态**。能力更底层,效果更好,从而更加省电
```c
static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) {
@@ -1050,6 +1226,16 @@ static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header
}
```
+
+
+### RunLoop 是如何响应用户操作的?
+
+用户交互事件首先在 IOHID 层生成 HIDEvent,然后向事件处理线程的 Source1 的 mach port 发送 HIDEvent 消息,Source1 的回调函数将事件转化为 UIEvent 并筛选需要处理的事件推入待处理事件队列,向主线程的事件处理 Source0 发送信号,并唤醒主线程,主线程检查到事件处理 Source0 有待处理信号后,触发 Source0 的回调函数,从待处理事件队列中提取 UIEvent,最后进入 hit-test 等 UIEvent 事件响应流程
+
+等待梳理完善。
+
+
+
## CFRunLoopTimerRef 是基于时间的触发器
- 基本上说就是 NSTimer,它会收到 RunLoopMode 的影响
@@ -1170,6 +1356,8 @@ if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentM
// ...
```
+
+
## Mach Port 跨线程通信
1. Mach IPC 基于 Mach 内核实现进程间通讯。
@@ -1310,6 +1498,8 @@ if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentM
可以看到每次在点击屏幕时调用 `CFRunLoopWakeUp` 尝试唤醒 RunLoop,然后监听 RunLoop 的 _wakeUpPort,都可以在回调中获取到消息。
+
+
## RunLoop 应用场景
- 控制线程生命周期(线程保活)
@@ -1320,6 +1510,8 @@ if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentM
- 性能优化
+
+
### NSTimer 经常会不准确,原因是什么?
NSTimer 在创建的时候经常会指派到特定的 NSRunLoopMode 中去,举个例子,默认创建的NSTimer 是被添加到 NSRunLoopDefaultMode 中去,当你的页面上有 UIScrollView 或者子类的时如果被拖动了,当前 RunLoop 的 NSRunloopMode 会从 NSDefaultRunLoopMode 转变为 UITrackingRunLoopMode 。遇到这种情况你需要精确的 NSTimer 的话,在创建好 NSTimer 之后,设置 RunLoopMod 为 NSRunLoopCommonModes。
@@ -1388,23 +1580,33 @@ NSTimer 会受 NSRunLoopMode 影响,GCD 的 timer 则不会。
@end
```
+
+
### ImageView显示(PerformSelector)
-UITableView 在滚动的时候一个优化点之一就是 UIImageView 的显示,通常需要根据网络去下载图片。所以如果用户快速滚动列表的时候,如果立马下载并显示图片的话,势必会对 UI 的刷新产生影响,直观的表现就是会卡顿,**FPS** 达不到60。
-
-利用 RunLoop 可以实现这个效果,就是给下载并显示图片的方法指定 **NSRunLoopMode**。
+UITableView 在滚动的时候一个优化点之一就是 UIImageView 的显示,通常需要根据网络去下载图片。所以如果用户快速滚动列表的时候,如果立马下载并显示图片的话,势必会对 UI 的刷新产生影响,直观的表现就是会卡顿,FPS 达不到60。
```objectivec
+- (void)downloadAndShowImage{
+ self.imageview.image = [UIImage imageNamed:@"test"];
+}
+
- (IBAction)clickLoadIMage:(id)sender {
//[self.imageview performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"test"] afterDelay:2];
[self performSelector:@selector(downloadAndShowImage) withObject:nil afterDelay:2 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]];
}
-
-- (void)downloadAndShowImage{
- self.imageview.image = [UIImage imageNamed:@"test"];
-}
```
+知道 RunLoop 的工作原理,就清楚 UITableView(任何 UIScrollView 子类)在滚动的时候,RunLoop 会处于 `UITrackingRunLoopMode`,那么可以将图片下载或者解码显示的逻辑放到 `NSDefaultRunLoopMode` 中
+
+```objective-c
+[self performSelector:@selector(downloadAndShowImage) withObject:nil afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
+```
+
+
+
+
+
### 自动释放池
自动释放池什么时候创建和释放?
@@ -1421,6 +1623,8 @@ App 启动后,主线程 RunLoop 里注册了2个 Observer,其回调都是 `_
总结版:在主线程执行的代码,通常是写在事件回调、Timer 回调内的,这些回调都会被 RunLoop 自身状态相关的 AutoreleasePool 所包裹,所以会自动管理内存,开发者不需要手动创建 AutoreleasePool。
+
+
### 事件响应
系统注册了 Source1(基于 Mach port)用来接收系统事件,其回调函数为 `__IOHIDEventSystemClientQueueCallback`
@@ -1429,18 +1633,24 @@ App 启动后,主线程 RunLoop 里注册了2个 Observer,其回调都是 `_
`_UIApplicationHandleEventQueue` 会把 `IOHIDEvent` 处理并包装成 UIEvent 进行处理和分发(其中包括 UIGesture、屏幕旋转等)。
+
+
### 手势识别
`_UIApplicationHandleEventQueue` 识别到一个手势时,首先会调用 cancel 将当前的 touchBegin/End/Move 系统回调打断,然后系统会将对应的 `UIGestureRecognizer ` 标记为待处理。
苹果注册了一个 Observer 监控 RunLoop 的 `kCFRunLoopBeforeWaiting`(将要休眠)状态,回调为 `_UIGestureRecognizerUpdateObserver`,其内部会获取所有刚被标记为待处理的 UIGestureRecognizer,并执行对应的回调。
+
+
### UI 刷新
当界面的 Frame 改变,或者更改 UIView、CALayer 的层次时,或者调用了 UIView、CALayer 的 setNeedsLayout、setNeedsDisplay 方法后,这个 UIView、CALayer 会被标记为待处理(类比前端的 Virtual Dom Diff,标记为 dirty),并被提交到一个全局容器中。
苹果设计 UI 更新也是 RunLoop 的业务方,所以会注册一个 Obserger 监控 `kCFRunLoopBeforeWaiting`(将要休眠)和 `kCFRunLoopExit` (即将退出 RunLoop)状态,然后会执行 `_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()` 回调。内部会遍历所有待处理的 UIView、CALayer 以执行实际的绘制和渲染,更新 UI
+
+
### RunLoop 空闲时做一些任务
```objectivec
@@ -1465,6 +1675,8 @@ App 启动后,主线程 RunLoop 里注册了2个 Observer,其回调都是 `_
}
```
+
+
### Crash 防护
利用监控手段,比如 C/OC crash、Signal、Mach 异常,当监控到异常之后,正常来说会发生闪退等,体验较差。某些场景下希望 App 从异常中恢复,重新启动,这个可以利用 RunLoop 实现。
@@ -1494,37 +1706,162 @@ CFRelease(allModes);
### 线程保活
+为什么线程做完事情就会退出?
+
+NSThread 的一个工作流程如下:
+
+`start() -> 创建 pthread -> main() -> [target performSelector:selector] -> exit`
+
+NSThread 需要保活。为什么会死掉?看看 gnu 源码
+
+```c++
+- (void) start
+{
+ pthread_attr_t attr;
+
+ if (_active == YES)
+ {
+ [NSException raise: NSInternalInconsistencyException
+ format: @"[%@-%@] called on active thread",
+ NSStringFromClass([self class]),
+ NSStringFromSelector(_cmd)];
+ }
+ if (_cancelled == YES)
+ {
+ [NSException raise: NSInternalInconsistencyException
+ format: @"[%@-%@] called on cancelled thread",
+ NSStringFromClass([self class]),
+ NSStringFromSelector(_cmd)];
+ }
+ if (_finished == YES)
+ {
+ [NSException raise: NSInternalInconsistencyException
+ format: @"[%@-%@] called on finished thread",
+ NSStringFromClass([self class]),
+ NSStringFromSelector(_cmd)];
+ }
+
+ /* Make sure the notification is posted BEFORE the new thread starts.
+ */
+ gnustep_base_thread_callback();
+
+ /* The thread must persist until it finishes executing.
+ */
+ RETAIN(self);
+
+ /* Mark the thread as active while it's running.
+ */
+ _active = YES;
+
+ errno = 0;
+ pthread_attr_init(&attr);
+ /* Create this thread detached, because we never use the return state from
+ * threads.
+ */
+ pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
+ /* Set the stack size when the thread is created. Unlike the old setrlimit
+ * code, this actually works.
+ */
+ if (_stackSize > 0)
+ {
+ pthread_attr_setstacksize(&attr, _stackSize);
+ }
+ // 设置回调函数
+ if (pthread_create(&pthreadID, &attr, nsthreadLauncher, self))
+ {
+ DESTROY(self);
+ [NSException raise: NSInternalInconsistencyException
+ format: @"Unable to detach thread (last error %@)",
+ [NSError _last]];
+ }
+}
+```
+
+看看 pthread 创建后的回调函数
+
+```c++
+static void *
+nsthreadLauncher(void *thread)
+{
+ NSThread *t = (NSThread*)thread;
+
+ setThreadForCurrentThread(t);
+
+ /*
+ * Let observers know a new thread is starting.
+ */
+ if (nc == nil)
+ {
+ nc = RETAIN([NSNotificationCenter defaultCenter]);
+ }
+ // 发送通知
+ [nc postNotificationName: NSThreadDidStartNotification
+ object: t
+ userInfo: nil];
+ // 设置线程名
+ [t _setName: [t name]];
+ // 调用 main 方法
+ [t main];
+ // 线程退出
+ [NSThread exit];
+ // Not reached
+ return NULL;
+}
+```
+
+看了源码,会发现 NSThread 调用 start 内部就会调用 `[NSThread exit]` 所以会退出。要想常驻,就需要在 main 方法做 runloop 保活。
+
+```c++
+- (void) main
+{
+ if (_active == NO)
+ {
+ [NSException raise: NSInternalInconsistencyException
+ format: @"[%@-%@] called on inactive thread",
+ NSStringFromClass([self class]),
+ NSStringFromSelector(_cmd)];
+ }
+
+ [_target performSelector: _selector withObject: _arg];
+}
+```
+
+main 方法内其实就是在执行 _selector。也就是在 NSThread 的初始化方法中,传入的 selector 中进行 runloop 保活逻辑。
+
+
+
应用场景:经常在子线程中处理某些逻辑的场景。如果销毁再创建再销毁再创建效率很低,这个情况下就需要线程保活。
-```objectivec
-@interface LBPThread : NSThread
+```objective-c
+@interface LifeThread : NSThread
@end
-@implementation LBPThread
+@implementation LifeThread
- (void)dealloc{
NSLog(@"%s", __func__);
}
@end
@interface ViewController ()
-@property (nonatomic, strong) LBPThread *task;
+@property (nonatomic, strong) LifeThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
- self.task = [[LBPThread alloc] initWithTarget:self selector:@selector(run) object:nil];
- [self.task start];
+ self.thread = [[LifeThread alloc] initWithTarget:self selector:@selector(run) object:nil];
+ [self.thread start];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
[self performSelector:@selector(test) onThread:self.task withObject:nil waitUntilDone:NO];
}
- (void)test{
- NSLog(@"沿用保活的线程,做事情:%@", [NSThread currentThread]);
+ NSLog(@"沿用保活的线程,处理任务:%@", [NSThread currentThread]);
}
+// 该方法仅用于线程保活
- (void)run{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
- // 给 RunLoop 的某个 Mode 里添加 Source/Timer/Observer
+ // 给 RunLoop 的某个 Mode 里添加 Source/Timer/Observer。其中 addPort 就是 Source1
[[NSRunLoop currentRunLoop] addPort:[[NSMachPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"task finished");
@@ -1534,7 +1871,7 @@ CFRelease(allModes);
默认创建的 NSThread 会在 NSDefaultRunLoopMode 模式下运行,当 UI 滑动则进入 UITrackingMode 模式,所以 NSThread 的方法会停止。
-线程保活就是给方法内部添加 RunLoop,但是新创建的 RunLoop 运行肯定是基于某个 Mode,RunLoop 持续运行(保活)的前提是必须有 Timer、Sources、Observer。所以添加了 NSMachPort。
+线程保活就是给方法内部添加 RunLoop,但是新创建的 RunLoop 运行肯定是基于某个 Mode,RunLoop 持续运行(保活)的前提是必须有 Timer、Sources、Observer。所以在 Demo 中我们添加了 `NSMachPort`。
上面的代码存在问题:
@@ -1542,67 +1879,64 @@ CFRelease(allModes);
2. LBPThread 线程不会死亡,假如我们需要在某个时机让保活线程销毁,现在是办不到的
+3. RunLoop 不会停止
+
改进:
1. Thread 换种 api `-(instancetype)initWithBlock:(void (^)(void))block`,线程不持有 self
2. `[[NSRunLoop currentRunLoop] run]` api 换掉。查看系统说明,底层其实就是一个无限循环,循环内部不断调用 `runMode:beforeDate:`。下面也有建议,建议我们想销毁 RunLoop,可以替换 API,比如设置一个变量,标记是否需要结束 RunLoop
- 
+
改进代码如下
-```objectivec
-@interface ViewController ()
-@property (strong, nonatomic) LBPThread *task;
-@property (assign, nonatomic, getter=isStoped) BOOL stopped;
-@end
+```objective-c
+__weak ViewController *weakself = self;
+self.thread = [[LifeThread alloc] initWithBlock:^{
+ NSLog(@"RunLoop Start");
+ [[NSRunLoop currentRunLoop] addPort:[[NSMachPort alloc] init] forMode:NSDefaultRunLoopMode];
+ while (weakself && !weakself.needStopThread) {
+ [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
+ }
+ NSLog(@"RunLoop Stop");
+}];
+[self.thread start];
-@implementation ViewController
-- (void)viewDidLoad {
- [super viewDidLoad];
- __weak typeof(self) weakSelf = self;
- self.stopped = NO;
- self.task = [[LBPThread alloc] initWithBlock:^{
- NSLog(@"%@----begin----", [NSThread currentThread]);
- // 往RunLoop里面添加Source\Timer\Observer
- [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
- while (weakSelf && !weakSelf.isStoped) {
- [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
- }
- NSLog(@"%@----end----", [NSThread currentThread]);
- }];
- [self.task start];
+- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
+ if(!self.thread) return;
+ [self performSelector:@selector(threadTask) onThread:self.thread withObject:nil waitUntilDone:YES];
}
-- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
-{
- if (!self.task) return;
- [self performSelector:@selector(test) onThread:self.task withObject:nil waitUntilDone:NO];
+#pragma mark - 线程相关
+- (void)threadTask {
+ NSLog(@"线程任务 %@", [NSThread currentThread]);
}
-// 子线程需要执行的任务
-- (void)test {
- NSLog(@"沿用保活的线程,做事情:%@", [NSThread currentThread]);
+
+- (void)stopThread {
+ if(!self.thread) return;
+ [self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:YES];
}
-- (IBAction)stop {
- if (!self.task) return;
- // 在子线程调用stop
- [self performSelector:@selector(stopThread) onThread:self.task withObject:nil waitUntilDone:YES];
-}
-// 用于停止子线程的RunLoop
-- (void)stopThread{
- // 设置标记为NO
- self.stopped = YES;
- // 停止RunLoop
+
+- (void)stop {
+ self.needStopThread = YES;
CFRunLoopStop(CFRunLoopGetCurrent());
- NSLog(@"%s %@", __func__, [NSThread currentThread]);
- self.task = nil;
+ self.thread = nil;
}
-- (void)dealloc{
+
+- (void)dealloc {
NSLog(@"%s", __func__);
+ [self stopThread];
}
+
@end
```
+效果如下:
+
+
+
+
+
注意:
- 如果 `stop` 方法内部的 `waitUntilDone` 为 NO,则会出现 Crash。因为该参数代表后续代表会不会等该 selector 执行完毕。因为为 NO,所以 ViewController 执行 dealloc 了,所以 `dealloc` 方法和 Thread 内部的 block 同时进行,不能确保在 block 内部执行的时候 dealloc 有没有执行完,访问 weakSelf.isStoped 可能会 crash
@@ -1629,16 +1963,9 @@ CFRelease(allModes);
}
```
-线程保活后如何暂停?
+线程保活的目的是保证线程处于激活状态,而不是使用强指针让线程不要释放。为让其处于激活状态就需要使用 RunLoop。
-```
-[thread cancel];
-thread = nil;
-// 指针 nil,还是被 RunLoop 持有。
-// 也不行。CFRunLoopStop([NSRunLoop currentRunLoop].getCFRunLoop);
-```
-主线程为什么
### 线程封装
@@ -1751,3 +2078,124 @@ typedef void (^LBPPermenantThreadTask)(void);
}
@end
```
+
+
+
+### 卡顿监控
+
+RunLoop 监控卡顿,可以查看 [带你打造一套 APM 系统](./1.4.md) 文章
+
+
+
+### AsyncDisplayKit
+
+卡顿主要原因是 CPU/GPU 高负荷工作(mask/cornerRadius/drawrect/opaque 带来的 offscreen rendering/blending 等),或者任务在时间分配下不均衡。
+
+Autolayout 布局性能瓶颈。约束的计算会随着 View 数量和层级的增长呈指数级增长,且必须在主线程执行。
+
+并行效率低。大多数情况下,主线程繁忙,其他子线程空余。所以思路是把主线程的任务转移一部分给其他线程进行异步处理,主线程带来性能提升
+
+AsyncDisplayKit 主要针对:
+
+- 渲染:对于大量文字、图片混合在一起时,而文字区域的大小和布局,恰恰依赖着渲染结果。ASDK 尽可能走后台线程进行渲染,完成后再同步回到主线程相应的 UIView
+- 布局。ASDK 抛弃了 Autolayout,实现了自己的布局和缓存
+- 系统对象的创建和销毁。UIKit 封装了 CALayer 以支持出没灯显示以外的操作。耗时也增加了,这些操作也需要在主线程进行。ASDK 基于 Node 的设计,突破了 UIKit 线程的限制。
+
+
+
+ASDK 创建了 ASDisplayNode 对象,内部封装了 UIView/CALayer,具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor,这些属性都可以在子线程更改,这样可以实现将排版和绘制放到后台线程,但最终都需要将 View 的更改同步到主线程的 UIView/CALayer 中去。
+
+这个同步时机,就是利用 RunLoop 实现的。系统的 UIKit/QuartzCore 也是 RunLoop 的业务方,同样,我们可以模仿系统行为,将针对 View 的改动,在主线程 RunLoop 添加一个 Observer,监听 `kCFRunLoopBeforeWaiting` 、`kCFRunLoopExit` 状态,当收到回调时,遍历所有之前加入到队列中待处理的事务,然后一一执行。
+
+```objective-c
++ (void)registerTransactionGroupAsMainRunloopObserver:(_ASAsyncTransactionGroup *)transactionGroup
+{
+ ASDisplayNodeAssertMainThread();
+ static CFRunLoopObserverRef observer;
+ ASDisplayNodeAssert(observer == NULL, @"A _ASAsyncTransactionGroup should not be registered on the main runloop twice");
+ // defer the commit of the transaction so we can add more during the current runloop iteration
+ CFRunLoopRef runLoop = CFRunLoopGetCurrent();
+ CFOptionFlags activities = (kCFRunLoopBeforeWaiting | // before the run loop starts sleeping
+ kCFRunLoopExit); // before exiting a runloop run
+ CFRunLoopObserverContext context = {
+ 0, // version
+ (__bridge void *)transactionGroup, // info
+ &CFRetain, // retain
+ &CFRelease, // release
+ NULL // copyDescription
+ };
+
+ observer = CFRunLoopObserverCreate(NULL, // allocator
+ activities, // activities
+ YES, // repeats
+ INT_MAX, // order after CA transaction commits
+ &_transactionGroupRunLoopObserverCallback, // callback
+ &context); // context
+ CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes);
+ CFRelease(observer);
+}
+
+static void _transactionGroupRunLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
+{
+ ASDisplayNodeCAssertMainThread();
+ _ASAsyncTransactionGroup *group = (__bridge _ASAsyncTransactionGroup *)info;
+ [group commit];
+}
+
+- (void)commit
+{
+ ASDisplayNodeAssertMainThread();
+
+ if ([_containers count]) {
+ NSHashTable *containersToCommit = _containers;
+ _containers = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality];
+
+ for (id container in containersToCommit) {
+ // Note that the act of committing a transaction may open a new transaction,
+ // so we must nil out the transaction we're committing first.
+ _ASAsyncTransaction *transaction = container.asyncdisplaykit_currentAsyncTransaction;
+ container.asyncdisplaykit_currentAsyncTransaction = nil;
+ [transaction commit];
+ }
+ }
+}
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Chapter1 - iOS/1.39.md b/Chapter1 - iOS/1.39.md
index 8e31668..bc19448 100644
--- a/Chapter1 - iOS/1.39.md
+++ b/Chapter1 - iOS/1.39.md
@@ -1,6 +1,16 @@
# 多线程探究
-> 平时我们经常使用 GCD、锁、队列、block,那这些概念和本质到底是什么?线程安全如何实现?自旋锁、互斥锁区别是什么?本文来一探究竟
+> 平时我们经常使用 GCD、锁、队列、block,那这些概念和本质到底是什么?
+>
+> 线程安全如何实现?
+>
+> 自旋锁、互斥锁区别是什么?
+>
+> 什么是死锁?
+>
+> 如果不清楚这些问题,带着问题,跟随本文来一探究竟
+
+
## 多线程方案
@@ -16,10 +26,69 @@
| 同步(sync) | 不开新线程、串行执行任务 | 不开新线程、串行执行任务 | 不开新线程、串行执行任务 |
| 异步(async) | 开新线程、并发执行任务 | 开新线程、串行执行任务 | 不开新线程、串行执行任务 |
+
+
## 多线程死锁
+什么是死锁?
+
+**队列任务引起的循环等待。**
+
+
+
看几个 Demo 观察下死锁情况
+Demo0
+
+```objective-c
+// 死锁
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ NSLog(@"task");
+ });
+}
+
+// 死锁
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ NSLog(@"1");
+ dispatch_queue_t serialQueue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_SERIAL);
+ dispatch_sync(serialQueue, ^{
+ NSLog(@"task");
+ });
+ NSLog(@"2");
+}
+// console
+1
+task
+2
+
+// 死锁
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ NSLog(@"1");
+ dispatch_queue_t serialQueue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_SERIAL);
+ dispatch_sync(serialQueue, ^{
+ NSLog(@"task");
+ dispatch_sync(serialQueue, ^{
+ NSLog(@"3");
+ });
+ });
+ NSLog(@"2");
+}
+```
+
+为什么一个死锁了,一个没有死锁?
+
+第一个写法:是因为 viewDidLoad 默认是主队列上跑的,主队列也只有一个主线程。所以 `viewDidLoad` 和 `NSLog(@"task")` 这2个任务都被放到主队列上等待被调度,然而主线程在执行的时候, `viewDidLoad` 的执行依赖 `NSLog(@"task")` ,`NSLog` 的执行依赖 `viewDidLoad`,所以主队列任务循环等待,引起了死锁
+
+第二个写法:创建了一个新的串行队列,队列里只加了1个任务 `NSLog(@"task")`,主队列中有 `viewDidLoad`,没有与之相互等待的任务,故而不会产生死锁。
+
+第三个写法:创建了一个新的串行队列。主队列里只有 `viewDidLoad` 任务。在主线程执行获取任务的时候,先从主队列获取了 `viewDidLoad` 任务,然后从创建的串行队列中获取任务,先获取了 `NSLog(@"task")` 任务,由于同步执行,后面的代码执行需要等待当前任务的执行结束,但当前任务的执行结束又依赖后面的任务 `NSLog(@"3")`
+
+
+
Demo1
```objectivec
@@ -103,7 +172,7 @@ NSLog(@"执行任务5");
- 队列决定了任务执行完是否需要等待。任务决定是否可以产生新线程
-- 死锁:使用`sync` 函数给当前串行队列派发任务,则会卡住当前串行队列,产生死锁
+- 死锁:使用 `sync` 函数给当前串行队列派发任务,则会卡住当前串行队列,产生死锁
Demo6
@@ -124,8 +193,44 @@ Demo6
分析:为什么打印1、3,没有打印2。因为 `-(void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;` 底层是开启了定时器,定时器运行需要添加到 RunLoop。上述代码是在全局并发队列上开启子线程,子线程中没有 RunLoop,所以定时器没有运行。
+
+
Demo7
+```objective-c
+NSLog(@"1");
+dispatch_sync(dispatch_get_global_queue(0, 0), ^{
+ NSLog(@"2");
+ dispatch_sync(dispatch_get_global_queue(0, 0), ^{
+ NSLog(@"3");
+ dispatch_sync(dispatch_get_global_queue(0, 0), ^{
+ NSLog(@"4");
+ });
+ });
+});
+NSLog(@"5");
+// console
+1 2 3 4 5
+```
+
+只要是同步提交任务 `dispatch_sync()` 不管是提交到串行队列还是并发队列,都是在当前线程执行。所以都是在主线程中执行。
+
+因为是并发队列,所以可以同时执行,不需要互相等待,则先提交 `NSLog(@"2")` 任务到全局并发队列中,然后执行。由于不需要等待,可以执行后面的代码,继续提交 `NSLog(@"3")` 到全局并发队列,继续执行,因为不需要等待,继续执行后面的代码。继续提交 `NSLog(@"4")` 到到全局并发队列,继续执行。之后从主队列取出 `NSLog(@"5")` 任务
+
+
+
+### 总结
+
+只要是同步提交任务 `dispatch_sync()` 不管是提交到串行队列还是并发队列,都是在当前线程执行。
+
+
+
+
+
+## performSelector...withObject 研究
+
+### performSelector...withObject
+
```objectivec
- (void)test{
NSLog(@"2");
@@ -152,9 +257,27 @@ Demo7
}
```
-## iOS 底层研究宝藏
-如何查看 `-(void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;` 源码。
+
+### performSelector...withObject...afterDelay
+
+Demo1
+
+
+
+QA:为什么先打印1、3再打印2?因为 `performSelector...withObject...afterDelay` 相当于给 RunLoop 添加了一个 Timer,Timer 运行需要 RunLoop 配合。RunLoop 在被唤醒的时候会处理定时器。
+
+Demo2:
+
+
+
+
+
+
+
+QA:为什么 showLog 里的2没有打印?
+
+查看源码分析,如何查看 `-(void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;` 源码。
- 未开源,但是可以设置断点查看汇编分析;
@@ -162,7 +285,7 @@ Demo7
查看 GUNStep 源码
-```c
+```c++
- (void) performSelector: (SEL)aSelector
withObject: (id)argument
afterDelay: (NSTimeInterval)seconds
@@ -180,7 +303,9 @@ Demo7
}
```
-可以看到底层实现就是 开启一个 Timer 并添加到 RunLoop。但是没有 Run。所以代码改下就可运行。
+答:通过源码分析可以看到,`performSelector...withObject...afterDelay...` 本质是开启一个定时器,并添加到 RunLoop。但没有启动 RunLoop。打印1、2是由于他们不需要 RunLoop 的配合,而定时器需要 RunLoop 的配合。
+
+所以代码改下就可运行。
```objectivec
- (void)test{
@@ -200,6 +325,28 @@ Demo7
所以要研究 iOS 底层的同学,看看 **GUNStep 代码吧,这是宝藏**
+Demo3:
+
+
+
+同理,GCD 虽然开启了子线程,但是 Block 结束后,线程也就结束了。所以线程任务中的1秒后的任务肯定也结束了。
+
+
+
+Demo4:
+
+
+
+可以看到 NSThread 里的 block 执行结束后,thread 结束了。后面的 performSelector 想在线程里执行任务,就会 crash。
+
+解决办法也是在线程的 block 里面加 RunLoop,让它保活
+
+
+
+
+
+
+
## 队列组
- 实现异步并发执行任务1、任务2
@@ -228,15 +375,13 @@ dispatch_group_notify(group, queue, ^{
});
```
-## 多线程安全问题
-多线程存在资源共享问题。比如1块内存可能会被多个线程共享,同时读或者写,导致不一致,很容易引发数据错乱和数据安全问题。典型的生产者消费者问题
-比如多个线程访问同一个对象、同一个变量、同一个文件
+## 多线程安全问题-锁
-解决方案
+多线程存在资源共享问题。比如1块内存可能会被多个线程共享,同时读或者写,导致不一致,很容易引发数据错乱和数据安全问题。典型的生产者消费者问题。比如多个线程访问同一个对象、同一个变量、同一个文件
-使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)
+解决方案:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)
常见的线程同步技术是:加锁。
@@ -253,9 +398,13 @@ dispatch_group_notify(group, queue, ^{
- NSConditionLock
- @synchronized
+
+
### OSSpinLock
-`OSSpinLock` 叫做”自旋锁”。
+#### 使用
+
+`OSSpinLock` 叫做”自旋锁”。缺点是:自旋类似于一个 while 循环,在死等。
使用的时候需要导入 `#import `
@@ -267,13 +416,7 @@ dispatch_group_notify(group, queue, ^{
`bool res = OSSpinLockTry(&lock)` 尝试加锁(如果需要等待就不加锁直接返回 false,如果不需等待则加锁,返回 true)
-存在问题:
-
-- 等待锁的线程会处于忙等(busy-wait)状态,一直占用着 CPU 资源
-
-- 不安全,可能会出现优先级反转问题
-
-- 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
+Demo:
```objectivec
@interface ViewController ()
@@ -323,26 +466,47 @@ dispatch_group_notify(group, queue, ^{
@end
```
+
+
+#### 存在问题
+
+- 等待锁的线程会处于忙等(busy-wait)状态,一直占用着 CPU 资源
+
+- 不安全,可能会出现优先级反转问题
+
+- 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
+
QA:优先级反转是什么?
-线程本质上就是 CPU 高速切换,看上去是同时在做多个线程内的事情。操作系统会使用基于优先级抢占式调度算法。高优先级的线程始终在低优先级线程前执行。
+线程本质上就是 CPU 高速切换,系统分配很少的时间段分别给不同的线程,导致用户看上去是同时在做多个线程内的事情。操作系统会使用基于优先级抢占式调度算法。高优先级的线程始终在低优先级线程前执行。
+
+
+
+举个例子:优先级:线程 A < 线程 B < 线程 C,线程A、C 都会使用共享资源 R,该资源由信号量控制进行互斥访问。

-线程 A 在 T1 时刻拿到锁,并处理数据。
+线程 A 在 T1 时刻使用资源 R 并拿到锁。开始执行线程内的逻辑。
-线程 C 在 T2 时刻被唤醒,但是此时锁被线程 A 使用,所以线程 C 放弃 CPU 进入阻塞状态,而线程 A 继续占据 CPU,执行任务
+线程 C 在 T2 时刻被唤醒,但是此时锁被线程 A 使用,线程 C 尝试访问资源 R,但由于自然 R 被 线程 A 所占有,所以线程 C 放弃 CPU 进入阻塞(等待)状态,而线程 A 继续占据 CPU,执行任务
目前来看一切正常。
-但是在 T3 时刻,线程 B 被唤醒,由于优先级比较高,所以会立即抢占 CPU,此时线程 A 被迫进入 READY 状态等待。
+但是在 T3 时刻,线程 B 被唤醒,由于优先级比较高,所以会立即抢占 CPU,此时线程 A 被迫进入 READY 状态等待(导致线程 A 无法及时释放资源 R)。
-T4 时刻,线程 B 放弃 CPU,此时线程 A (优先级10)是唯一处于 READY 状态的线程,所以再次占据 CPU 去执行任务,在 T5 时刻释放锁。
+T4 时刻,线程 B 放弃 CPU,此时线程 A (优先级10)是唯一处于 READY 状态的线程,所以再次占据 CPU 去执行任务,在 T5 时刻释放锁和资源 R。
在 T5 时刻,线程 A 解锁瞬间,线程 C 立即获取锁,并在优先级20上等待 CPU,因为优先级比较高,所以系统会立刻调度线程 C 的任务执行。此时线程 A 进入 READY 状态。
线程 B 从 T3 到 T4 这个时间段占据 CPU 资源的行为叫做优先级反转。一个优先级 15 的线程B,通过压制优线级10的线程 A,而事实上导致高优先级线程 C 无法正确得到 CPU。这段时间是不可控的,因为线程 B 可以长时间占据 CPU(即使轮转时间片到时,线程 A 和 B 都处于可执行态,但是因为B的优先级高,它依然可以占据 CPU),其结果就是高优先级线程 C 可能长时间无法得到 CPU。
+当高优先级任务正等待信号量(此信号量被一个低优先级任务拥有着)的时候,一个介于两个任务优先之间的中等优先级任务开始执行——这就会导致一个高优先级任务在等待一个低优先级任务,而低优先级任务却无法执行类似死锁]的情形发生
+
+为了解决优先级反转问题,可以采取以下策略:
+
+1. 优先级天花板策略(Priority Ceiling):当任务使用共享资源时,将其优先级提高到访问该资源的所有任务的最高优先级或某个确定的优先级(即“优先级天花板”)。这样可以确保持有资源的任务不会被其他低优先级的任务抢占,从而避免了优先级反转。
+2. 优先级继承策略(Priority Inheritance):当一个任务被阻塞并等待一个低优先级任务释放资源时,将低优先级任务的优先级提升到等待它的最高优先级任务的优先级。这样可以确保低优先级任务能够尽快释放资源,从而使高优先级任务能够继续执行。
+
上面的代码改进下
```objectivec
@@ -368,8 +532,50 @@ T4 时刻,线程 B 放弃 CPU,此时线程 A (优先级10)是唯一处
}
```
+#### 汇编窥探原理
+
+自旋锁是指在等锁的时候通过类似 while 循环的代码,让线程忙碌等到锁的到来。
+
+自旋锁是一种特殊的锁机制,当线程试图获取锁但失败时,它会在一个循环中持续尝试(即“自旋”),而不是立即阻塞。这可以在某些情况下提高性能,尤其是当锁被持有的时间很短时。
+
+
+
+为了调试方便,开启10个线程去执行 `saveMoney` 方法,为了查看自旋锁的等是什么实现。我们给里面休眠600s。同时 Xcode - Debug - DebugWorkflow - Always Show Disassembly
+
+lldb 模式下调试汇编有几个指令
+
+c: 代表 continue,
+
+si:step instruction,简写为 stepi,si。当你在 Xcode 汇编面板看到某个认识或者可疑符号,断点在这一行的时候,在下方 lldb 面板,属于 si,即可进入内部实现。
+
+第一步:当第二次调用 saveMoney 方法,开启汇编调试
+
+
+
+看到可疑方法 `OSSpinLockLock`,给它加断点,看到第10行高亮了。lldb 模式输入 c,敲回车。次数输入 si 即可进入 `OSSpinLockLock` 方法内部调试
+
+第二步:继续输入 si,敲回车
+
+
+
+第三步:看到可疑方法 `_OSSpinLockLockSlow`,给它加断点,lldb 输入 C。此时断点到这一行了,继续输入 si。
+
+
+
+第四步:在 `OSSpinLockLockSlow` 方法内部调试,不断输入 si。
+
+
+
+发现不断 si 最终一直会在第6行到第19行之间执行。懂汇编的会发现这其实是一个 while 循环。便可以证明自旋锁 OSSpinLock 在等锁的时候,底层实现是执行 while 循环,忙等,“太浪费性能了”(如果使用锁资源的线程任务很简单,那自旋也是高效的,可以快速获取锁。)
+
+结论:OSSpinLock 底层就是一个自旋锁,内部不断循环,盲等。
+
+
+
### os_unfair_lock
+#### 使用
+
`os_unfair_lock` 用于取代不安全的 `OSSpinLock` ,从iOS10开始才支持。使用的时候需要导入头文件 `#import `
从底层调用看,等待 `os_unfair_lock` 锁的线程会处于休眠状态,并非忙等(自旋锁会忙等)
@@ -437,16 +643,37 @@ int cursorr = 1;
假如对存钱过程,忘记解锁怎么办?产生死锁,如下
-
+
添加 cursor 标记死锁是发生在 `saveMoney` 方法执行的第几次。发现是第二次。因为第一次锁没有任何使用方,所以加锁成功,当第二次加锁的时候发现锁没有释放,所以产生死锁。
这时候使用尝试加锁 API `os_unfair_lock_trylock` 即可成功如下
-
+
+
+
+
+#### 汇编窥探原理
+
+同样方式看看 ,按照上述调试汇编代码的步骤,我将关键步骤截图如下
+
+
+
+
+
+
+
+
+结论:可以看到 `os_unfair_lock` 在锁等待的时候,底层调用的是 `sysCall`,当这一步执行后会发现后续代码都不执行了,也就是调用系统底层能力,线程真正休眠了,而不是一个循环忙等,性能也好。
+
+系统对其描述是:`Low-level lock that allows waiters to block efficiently on contention.`,即低级锁,低级锁的特点是等不到锁就休眠。
+
+
### pthread_mutex
+#### 使用
+
`mutex` 叫做”互斥锁”,等待锁的线程会处于休眠状态。使用时需要引入 `#import `
使用:
@@ -484,40 +711,102 @@ pthread_mutex_destroy(&_moneyLock);
使用如下
-```objectivec
-- (void)saveMoney {
- pthread_mutex_lock(&_moneyLock);
- NSInteger previousMoney = self.money;
- sleep(0.2);
- previousMoney += 50;
- self.money = previousMoney;
- NSLog(@"存50,还剩%zd元 - %@", self.money, [NSThread currentThread]);
- pthread_mutex_unlock(&_moneyLock);
-}
-```
+
+
+#### 递归锁
如果在某个方法内部递归调用自身怎么实现,好像挺简单的,直接内部调用即可。
-```objectivec
-int cursor = 0;
-- (void)sayHi {
- if (cursor<10) {
- pthread_mutex_lock(&_moneyLock);
- cursor++;
- NSLog(@"Hi %d", cursor);
- [self sayHi];
- pthread_mutex_unlock(&_moneyLock);
+```objective-c
+- (void)otherTest {
+ pthread_mutex_lock(&_lock);
+ static int count = 0;
+ count++;
+ NSLog(@"%s", __func__);
+ if (count<10) {
+ [self otherTest];
}
+ pthread_mutex_unlock(&_lock);
}
-// Hi 1
+
+- (void)sayHi {
+ NSLog(@"Hello");
+}
+// console
+-[PThreadMutexRecursiveLockTester otherTest]
```
只打印了 1。为什么?因为第一次调用正常加锁,然后递归调用自身,第二次调用的时候尝试加锁,但是这时候第一次调用时候锁还没释放。
-互斥锁提供 API 实现该功能。只需要在互斥锁初始化地方将属性修改为 `PTHREAD_MUTEX_RECURSIVE`。即 `pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);`在**同一个线程中可以多次获取同一把锁。并且不会死锁**。
+先加锁,然后递归调用,再继续加锁,再调用再加锁,最后一次函数执行完则解锁,出栈后继续解锁,再解锁。效果等价于
+
+```shell
++ 代表加锁;- 代表解锁
+线程1: otherTest(+ -)
+ otherTest(+ -)
+ otherTest(+ -)
+```
+
+
+
+互斥锁提供 API 实现该功能。只需要在互斥锁初始化地方将属性修改为 `PTHREAD_MUTEX_RECURSIVE`。即 `pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);`
+
+在**同一个线程中可以多次获取同一把锁。并且不会死锁**。
+
+
+
+改进后的效果如下
+
+
+
+
+
+QA:互斥递归锁,可以在不同线程中加锁吗?
+
+不可以,线程1加锁后,线程2尝试加锁的时候,发现锁已经被其他线程所使用,线程2则等待。
+
+
+
+#### 汇编窥探原理
+
+
+
+输入 si 继续跟进,可以看到还是在执行我们自己的代码,LockExplore image 的 `pthread_mutex_lock` 方法
+
+
+
+继续输入 si 跟进
+
+
+
+可以看到此时调用到系统 `libsystem_pthread.dylib` 库的 `pthread_mutex_lock` 方法了。
+
+第41行看到关键函数,继续输入 si 进去看看
+
+
+
+可以看到内部第62行关键函数调用了 `_pthread_mutex_firstfit_lock_wait` 方法。此时继续输入 si 跟踪看看
+
+
+
+可以看到内部第25行调用了关键函数 `__psynch_mutexwait`,继续输入 si 看看
+
+
+
+可以看到内部继续调用了系统 `libsystem_pthread.dylib` 库的 `__psynch_mutexwait` 方法。继续输入 si
+
+
+
+可以看到内部第4行发生了系统调用 `sysCall`,执行完第四句指令,线程立马就结束了。
+
+结论:汇编逐句研究了 `pthread_mutex_t` 会发现最后也是调用 `syscall` 做到线程休眠,不像自旋锁一样,OSSpinLock 在底层实现是 while 循环一样忙等,浪费资源。
+
+
### 互斥条件锁 pthread_cond_t
+#### 使用
+
初始化互斥锁条件 `pthread_cond_init(&_condition, NULL);`
等待条件进入休眠,放开 mutex 锁,被唤醒后会再次对 mutex 加锁 `pthread_cond_wait(&_condition, &_moneyLock);`
@@ -526,146 +815,28 @@ int cursor = 0;
激活所有等待该条件的线程 `pthread_cond_broadcast(&_condition)`
-```objectivec
-@interface ViewController ()
-@property (nonatomic, assign) pthread_mutex_t moneyLock;
-@property (nonatomic, assign) pthread_cond_t condition;
-@property (nonatomic, strong) NSMutableArray *array;
-@end
+
-@implementation ViewController
-- (void)viewDidLoad{
- [super viewDidLoad];
- pthread_mutexattr_t attr;
- pthread_mutexattr_init(&attr);
- pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
- // 初始化锁
- pthread_mutex_init(&_moneyLock, &attr);
- // 释放属性内存
- pthread_mutexattr_destroy(&attr);
- // 加锁
- pthread_mutex_lock(&_moneyLock);
- // 解锁
- pthread_mutex_unlock(&_moneyLock);
- // 初始化互斥锁条件
- pthread_cond_init(&_condition, NULL);
- [self test];
- self.array = [NSMutableArray array];
-}
-- (void)test{
- [[[NSThread alloc] initWithTarget:self selector:@selector(delete) object:nil] start];
- [[[NSThread alloc] initWithTarget:self selector:@selector(add) object:nil] start];
-}
-- (void)add{
- NSLog(@"add beign");
- pthread_mutex_lock(&_moneyLock);
- [self.array addObject:@"bb"];
- NSLog(@"加元素 %zd", self.array.count);
- pthread_cond_signal(&_condition);
- pthread_mutex_unlock(&_moneyLock);
- NSLog(@"add beign");
-}
-- (void)delete{
- NSLog(@"delete beign");
- pthread_mutex_lock(&_moneyLock);
- if (self.array.count == 0) {
- pthread_cond_wait(&_condition, &_moneyLock);
- }
- [self.array removeLastObject];
- NSLog(@"减元素 %zd", self.array.count);
- pthread_mutex_unlock(&_moneyLock);
- NSLog(@"delete end");
-}
-@end
-// delete beign
-// add beign
-// 加元素 1
-// add beign
-// 减元素 0
-// delete end
-```
+可以看到同时调用 remove、add 方法
-可以看到同时调用 delete、add 方法
+- 执行 remove 方法先加锁,但是由于数组为空,这时候就不需要执行删除元素,然后执行 add 方法
+- add 方法要加锁,发现锁被 remove 方法占用了
+- remove 方法为了等有元素再去执行 remove 引入了互斥锁条件 `pthread_cond_t`,调用 `pthread_cond_wait` 。此时线程进入休眠,同时会释放锁。
+- add 方法内加完元素会调用 `pthread_cond_signal` 来激活等待该条件的线程,此时 remove 方法内的线程获得锁,此时再次加锁
+- remove 方法执行完线程任务后,再解锁。
-- 执行 delete 方法先加锁,但是由于数组为空,这时候就不需要执行删除元素,然后执行 add 方法
-- add 方法要加锁,发现锁被 delete 方法占用了
-
-- delete 方法为了等有元素再去执行 delete 引入了互斥锁条件 `pthread_cond_t`,调用 `pthread_cond_wait` 。此时线程进入休眠,同时会释放锁。
-
-- add 方法内加完元素会调用 `pthread_cond_signal` 来激活等待该条件的线程
-
-### 从汇编角度分析 os_unfair_lock 属于什么锁(教你如何用汇编分析源码)
-
-属于互斥锁。自旋锁是指在等锁的时候通过类似 while 循环的代码,让线程忙碌等到锁的到来。
-
-> Low-level lock that allows waiters to block efficiently on contention.
-
-系统说它是低级锁,等不到锁就休眠。
-
-测试代码
-
-```objectivec
-dispatch_queue_t queue = dispatch_queue_create("com.lbp.money.queue", DISPATCH_QUEUE_CONCURRENT);
-for (int i = 0; i < 10; i++) {
- [[[NSThread alloc] initWithTarget:self selector:@selector(saveMoney) object:nil] start];
-}
-
-- (void)saveMoney {
- OSSpinLockLock(&_bankLock);
- NSInteger previousMoney = self.money;
- sleep(600);
- previousMoney += 50;
- self.money = previousMoney;
- NSLog(@"存50,还剩%zd元 - %@", self.money, [NSThread currentThread]);
- OSSpinLockUnlock(&_bankLock);
-}
-```
-
-为了调试方便,开启10个线程去执行 `saveMoney` 方法,为了查看自旋锁的等是什么实现。我们给里面休眠600s。同时 Xcode - Debug - DebugWorkflow - Always Show Disassembly
-
-lldb 模式下调试汇编有几个指令
-
-c: 代表 continue,
-
-si:step instruction,简写为 stepi,si。当你在 Xcode 汇编面板看到某个认识或者可疑符号,断点在这一行的时候,在下方 lldb 面板,属于 si,即可进入内部实现。
-
-第一步:当第二次调用 saveMoney 方法,开启汇编调试
-
-
-
-看到可疑方法 `OSSpinLockLock`,给它加断点,看到第10行高亮了。lldb 模式输入 c,敲回车。次数输入 si 即可进入 `OSSpinLockLock` 方法内部调试
-
-第二步:继续输入 si,敲回车
-
-
-
-第三步:看到可疑方法 `_OSSpinLockLockSlow`,给它加断点,lldb 输入 C。此时断点到这一行了,继续输入 si。
-
-
-
-第四步:在 `OSSpinLockLockSlow` 方法内部调试,不断输入 si。
-
-
-
-发现不断 si 最终一直会在第6行到第19行之间执行。懂汇编的会发现这其实是一个 while 循环。便可以证明自旋锁 OSSpinLock 在等锁的时候,底层实现是执行 while 循环,忙等,太浪费性能了。
-
-同样方式看看 ,按照上述调试汇编代码的步骤,我将关键步骤截图如下
-
-
-
-
-
-可以看到最后一步调用到了 `syscall`,当这一步执行后会发现后续代码都不执行了,也就是调用系统底层能力,线程真正休眠了
-
-同样的步骤研究 `pthread_mutex_t` 会发现最后也是调用 `syscall` 做到线程休眠,不像自旋锁一样,在底层实现是 while 循环一样忙等,浪费资源。
### NSLock、NSRecursiveLock
+#### 使用
+
NSLock 是对 mutex 普通锁(pthread_mutex_t)的封装
NSRecursiveLock 是对 mutex 递归锁(pthread_mutex_t ,且 attr 为 `PTHREAD_MUTEX_RECURSIVE`)的封装,API 跟 NSLock 基本一致
+区别在于一个是 c 语言版本的 API,一个是 OC 版本的包装。
+
查看 GUN 源码可以看看到底是如何实现的
```objectivec
@@ -742,10 +913,48 @@ pthread_mutexattr_settype(&attr_recursive, PTHREAD_MUTEX_RECURSIVE);
NSRecursiveLock 不能在多线程下递归调用。@synchronized 可以在多线程下递归调用。底层原因是 TLS 有关。
+
+
+Demo
+
+
+
+
+
+NSLock 死锁
+
+
+
+会发生死锁,后续代码无法执行,App 表现就是 ANR。重复对 NSLock 进行加锁可能导致死锁问题,同时也可能引发数据竞争和性能下降等并发相关隐患
+
+针对上述的例子,可以用递归锁解决。可以重复加锁。
+
+
+
### NSCondition
NSCondition 是对 mutex 和 cond 的封装。
+API
+
+```objective-c
+@interface NSCondition : NSObject
+
+- (void)wait NS_SWIFT_UNAVAILABLE_FROM_ASYNC("Use async-safe scoped locking instead");
+
+- (BOOL)waitUntilDate:(NSDate *)limit NS_SWIFT_UNAVAILABLE_FROM_ASYNC("Use async-safe scoped locking instead");
+
+- (void)signal NS_SWIFT_UNAVAILABLE_FROM_ASYNC("Use async-safe scoped locking instead");
+
+- (void)broadcast;
+
+@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
+
+@end
+```
+
+源码
+
```objectivec
- (id) init {
if (nil != (self = [super init])) {
@@ -760,92 +969,76 @@ NSCondition 是对 mutex 和 cond 的封装。
}
```
-因为 NSCondtion 已经封装好锁和条件,所以直接使即可。pthread_mutex_t 需要搭配 pthread_cond_t 一起使用
+因为 NSCondtion 已经封装好锁和条件,所以直接使即可。pthread_mutex_t 需要搭配 pthread_cond_t 一起使用。
+
+Demo:
+
+
+
+
+
+
+
+使用 NSCondition 的时候 unlock 和 signal 的顺序可能会对结果造成影响。举个例子
+
+
+
+可以看到在这种情况下,由于 NSCondition 另一个地方 wait,wait 也需要释放锁,但是另一个发 signal 的地方,还没释放锁。所以会等待2s。
+
+针对这个情况,可以将 unlock 和 signal 的顺序进行调整。
+
+
+
+先解锁,然后发送 singal,后续其他的业务逻辑也不影响。当然这个需要针对实际代码进行设计。
+
-Demo
-```objectivec
- @interface ViewController ()
-@property (nonatomic, assign) NSInteger money;
-@property (nonatomic, strong) NSCondition *condition;
-@property (nonatomic, strong) NSMutableArray *array;
-@end
-@implementation ViewController
-- (void)viewDidLoad{
- [super viewDidLoad];
- self.condition = [[NSCondition alloc] init];
- [self test];
- self.array = [NSMutableArray array];
-}
-- (void)test{
- [[[NSThread alloc] initWithTarget:self selector:@selector(delete) object:nil] start];
- [[[NSThread alloc] initWithTarget:self selector:@selector(add) object:nil] start];
-}
-- (void)add{
- NSLog(@"add beign");
- [self.condition lock];
- [self.array addObject:@"bb"];
- NSLog(@"加元素 %zd", self.array.count);
- [self.condition unlock];
- [self.condition signal];
- NSLog(@"add beign");
-}
-- (void)delete{
- NSLog(@"delete beign");
- [self.condition lock];
- if (self.array.count == 0) {
- [self.condition wait];
- }
- [self.array removeLastObject];
- NSLog(@"减元素 %zd", self.array.count);
- [self.condition unlock];
- NSLog(@"delete end");
-}
-@end
-```
存在 `虚假唤醒` 的问题。则可以将后续的 if 判断换为 while。比如某一时刻发送了一次 signal,然后可能有多个线程收到唤醒的信号,则可能还是会存在问题。所以 if 换为 while。
+
+
### NSCondtionLock
-`NSConditionLock` 是对 NSCondition 的进一步封装,可以设置具体的条件值。
+`NSConditionLock` 是对 NSCondition 的进一步封装,可以设置具体的条件值(感兴趣的可以查看 GUN 源码)。
+
+API 如下:
+
+```objective-c
+@interface NSConditionLock : NSObject
+
+- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
+
+@property (readonly) NSInteger condition;
+- (void)lockWhenCondition:(NSInteger)condition NS_SWIFT_UNAVAILABLE_FROM_ASYNC("Use async-safe scoped locking instead");
+
+- (BOOL)tryLock NS_SWIFT_UNAVAILABLE_FROM_ASYNC("Use async-safe scoped locking instead");
+
+- (BOOL)tryLockWhenCondition:(NSInteger)condition NS_SWIFT_UNAVAILABLE_FROM_ASYNC("Use async-safe scoped locking instead");
+
+- (void)unlockWithCondition:(NSInteger)condition NS_SWIFT_UNAVAILABLE_FROM_ASYNC("Use async-safe scoped locking instead");
+
+- (BOOL)lockBeforeDate:(NSDate *)limit NS_SWIFT_UNAVAILABLE_FROM_ASYNC("Use async-safe scoped locking instead");
+
+- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit NS_SWIFT_UNAVAILABLE_FROM_ASYNC("Use async-safe scoped locking instead");
+
+@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
-```objectivec
-@interface ViewController ()
-@property (nonatomic, assign) NSInteger money;
-@property (nonatomic, strong) NSConditionLock *condition;
-@end
-@implementation ViewController
-- (void)viewDidLoad{
- [super viewDidLoad];
- self.condition = [[NSConditionLock alloc] initWithCondition:1];
- [self test];
-}
-- (void)test{
- [[[NSThread alloc] initWithTarget:self selector:@selector(task1) object:nil] start];
- [[[NSThread alloc] initWithTarget:self selector:@selector(task2) object:nil] start];
- [[[NSThread alloc] initWithTarget:self selector:@selector(task3) object:nil] start];
-}
-- (void)task1{
- [self.condition lockWhenCondition:1];
- NSLog(@"task1");
- [self.condition unlockWithCondition:2];
-}
-- (void)task2{
- [self.condition lockWhenCondition:2];
- NSLog(@"task2");
- [self.condition unlockWithCondition:3];
-}
-- (void)task3{
- [self.condition lockWhenCondition:3];
- NSLog(@"task3");
- [self.condition unlock];
-}
@end
```
-通过 NSCondtionLock 可以控制线程的执行顺序。
+Demo
+
+
+
+分析:虽然通过3个线程,设置了线程的先后顺序,但是多线程任务执行的时候到底谁先执行,是没办法控制的。但是通过 `NSConditionLock lockWhenCondition:*` 的能力,可以控制线程的执行顺序。
+
+
+
+另外如果初始化设置了 `[[NSConditionLock alloc] initWithCondition:1]` 但是使用的地方没有用 `lockWhenCondition` 而是直接用 `lock` 则会忽略 condition 的值,直接加锁成功。
+
+
### dispatch_queue
@@ -853,51 +1046,39 @@ Demo
线程同步的本质就是多线程的任务是顺序执行
+
+
+
+
### dispatch_semaphore
+#### 使用
+
semaphore 叫做”信号量”
信号量的初始值,可以用来控制线程并发访问的最大数量
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
-```objectivec
-@interface ViewController ()
-@property (nonatomic, assign) NSInteger money;
-@property (nonatomic, strong) dispatch_semaphore_t semaphore;
-@end
-@implementation ViewController
-- (void)viewDidLoad{
- [super viewDidLoad];
- // 控制线程最大并发量
- self.semaphore = dispatch_semaphore_create(5);
- [self test];
-}
-- (void)test{
- for (NSInteger index = 0; index<20; index++) {
- [[[NSThread alloc] initWithTarget:self selector:@selector(task) object:nil] start];
- }
-}
-- (void)task{
- dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
- NSLog(@"task %@", [NSThread currentThread]);
- sleep(5);
- dispatch_semaphore_signal(self.semaphore);
-}
-@end
-```
+
+
+可以看到打印了20个线程,但是我们控制线程最大数量怎么办呢?可以用信号量实现。效果如下:
+
+
`dispatch_semaphore_wait` 函数的本质
- 如果信号量的值 > 0,则会让信号量的值 -1,然后继续向下执行代码
-- 如果信号量的值 <= 0,则线程休眠等待。等待多久取决于第二个参数。直到信号量的值 > 0,此时会让信号量的值 -1,然后继续向下执行代码
+- 如果信号量的值 <= 0,则线程休眠等待。等待多久取决于第二个参数。等到信号量的值 > 0(直到其他的线程,任务执行完毕,利用 `dispatch_semaphore_signal`API 让信号量的值+1),此时继续会让信号量的值 -1,然后继续向下执行代码
`dispatch_semaphore_signal` 函数的本质:让信号量的值 + 1
-所以如何让线程同步?设置信号量的值=1即可。保证同一时间只有一个线程任务在执行
+所以如何让线程同步?设置信号量的值=1即可。保证同一时间只有一个线程任务在执行。代码如下
+
+
+
-NSCache 扩容策略。x86 3/4,arm7/8.
有趣的实验:
@@ -920,177 +1101,7 @@ void _dispatch_semaphore_dispose(dispatch_object_t dou,
}
```
-### @synchronized
-
-@synchronized 可递归重入的原理分析/线程缓存空间
-
-```objectivec
-@interface ViewController ()
-@property (nonatomic, assign) NSInteger money;
-@end
-
-@implementation ViewController
-- (void)viewDidLoad{
- [super viewDidLoad];
- self.money = 100;
- [self moneyTest];
-}
-- (void)moneyTest {
- dispatch_queue_t queue = dispatch_queue_create("com.lbp.money.queue", DISPATCH_QUEUE_CONCURRENT);
- dispatch_async(queue, ^{
- for (int i = 0; i < 10; i++) {
- [self saveMoney];
- }
- });
- dispatch_async(queue, ^{
- for (int i = 0; i < 10; i++) {
- [self withdrawMoney];
- }
- });
- // 100 + 10*50 - 10*10 = 500
-}
-- (void)saveMoney {
- @synchronized (self) {
- NSInteger previousMoney = self.money;
- sleep(0.2);
- previousMoney += 50;
- self.money = previousMoney;
- NSLog(@"存50,还剩%zd元 - %@", self.money, [NSThread currentThread]);
- }
-}
-- (void)withdrawMoney {
- @synchronized (self) {
- NSInteger previousMoney = self.money;
- sleep(0.2);
- previousMoney -= 10;
- self.money = previousMoney;
- NSLog(@"取20,还剩%zd元 - %@", self.money, [NSThread currentThread]);
- }
-}
-@end
-```
-
-`@synchronized` 使用很方便,它是对 `pthread_mutex_t` 递归锁的封装
-
-为了探究下实现,开启汇编调试
-
-
-
-可以查看 objc4 的源码,查找 `objc_sync_enter`
-
-```c
-int objc_sync_enter(id obj){
- int result = OBJC_SYNC_SUCCESS;
- if (obj) {
- SyncData* data = id2data(obj, ACQUIRE);
- assert(data);
- data->mutex.lock();
- } else {
- // @synchronized(nil) does nothing
- if (DebugNilSync) {
- _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
- }
- objc_sync_nil();
- }
- return result;
-}
-
-typedef struct SyncData {
- struct SyncData* nextData;
- DisguisedPtr object;
- int32_t threadCount; // number of THREADS using this block
- recursive_mutex_t mutex;
-} SyncData;
-```
-
-可以看到 `@synchronized` 的本质是一个互斥递归锁 `recursive_mutex_t`.
-
-传递一个参数 obj,经过 `id2data` 方法得到一个结构体对象,访问结构体对象的成员变量
-
-`mutex`,然后调用 `lock` 方法。
-
-如何根据 obj 获取对象,继续查看 `id2data` 方法。
-
-```c
-#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
-#define LISTFOR_OBJ(obj) sDataLists[obj].data
-static StripedMap sDataLists;
-
-static SyncData* id2data(id object, enum usage why)
-{
- spinlock_t *lockp = &LOCK_FOR_OBJ(object);
- SyncData **listp = &LIST_FOR_OBJ(object);
- SyncData* result = NULL;
- // ...
-}
-
-class recursive_mutex_tt : nocopy_t {
- pthread_mutex_t mLock;
-
- public:
- recursive_mutex_tt() : mLock(PTHREAD_RECURSIVE_MUTEX_INITIALIZER) {
- lockdebug_remember_recursive_mutex(this);
- }
-
- recursive_mutex_tt(const fork_unsafe_lock_t unsafe)
- : mLock(PTHREAD_RECURSIVE_MUTEX_INITIALIZER)
- { }
-
- void lock()
- {
- lockdebug_recursive_mutex_lock(this);
-
- int err = pthread_mutex_lock(&mLock);
- if (err) _objc_fatal("pthread_mutex_lock failed (%d)", err);
- }
-
- void unlock()
- {
- lockdebug_recursive_mutex_unlock(this);
-
- int err = pthread_mutex_unlock(&mLock);
- if (err) _objc_fatal("pthread_mutex_unlock failed (%d)", err);
- }
-
- void forceReset()
- {
- lockdebug_recursive_mutex_unlock(this);
-
- bzero(&mLock, sizeof(mLock));
- mLock = pthread_mutex_t PTHREAD_RECURSIVE_MUTEX_INITIALIZER;
- }
-
- bool tryUnlock()
- {
- int err = pthread_mutex_unlock(&mLock);
- if (err == 0) {
- lockdebug_recursive_mutex_unlock(this);
- return true;
- } else if (err == EPERM) {
- return false;
- } else {
- _objc_fatal("pthread_mutex_unlock failed (%d)", err);
- }
- }
-
-
- void assertLocked() {
- lockdebug_recursive_mutex_assert_locked(this);
- }
-
- void assertUnlocked() {
- lockdebug_recursive_mutex_assert_unlocked(this);
- }
-};
-```
-
-可以看到是一个哈希表 `StripedMap`,哈希表工作原理就是传递一个 key,经过哈希算法生成索引,然后获取对应的值。
-
-内部维护了一个哈希表,一个对象一个锁。
-
-另外 `recursive_mutex_tt` 在初始化的时候传入 `PTHREAD_RECURSIVE_MUTEX_INITIALIZER`,看起来也支持递归。所以 @synchronized 是一个递归互斥锁的封装。
-
-封装
+#### 封装
有的时候我们需要在方法内部创建 semaphore ,则可以创建宏
@@ -1107,11 +1118,285 @@ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_semaphore_signal(semaphore);
```
+使用
+
+```objective-c
+- (void)withdrawMoney {
+ SemaphoreBegin
+ [super withdrawMoney];
+ SemaphoreEnd
+}
+```
+
+
+
+### @synchronized
+
+`@synchronized` 可递归重入的原理分析/线程缓存空间
+
+`@synchronized` 使用很方便,它是对 `pthread_mutex_t` 递归锁的封装。Demo 如下
+
+
+
+
+
+为了探究下实现,开启汇编调试
+
+
+
+
+
+通过汇编可以看到 `@synchronized` 底层调用了 `objc_sync_enter` 方法,其中又调用了 `id2data` 和 `os_unfair_recursive_lock_lock_with_options` 方法。 可以查看 objc4 的源码(笔者的 objc 版本为 objc4-objc4-912.3),查找 `objc_sync_enter`
+
+```c++
+// Begin synchronizing on 'obj'.
+// Allocates recursive mutex associated with 'obj' if needed.
+// Returns OBJC_SYNC_SUCCESS once lock is acquired.
+int objc_sync_enter(id obj)
+{
+ int result = _objc_sync_enter_kind(obj, SyncKind::atSynchronize);
+ if (result != OBJC_SYNC_SUCCESS)
+ OBJC_DEBUG_OPTION_REPORT_ERROR(DebugSyncErrors,
+ "objc_sync_enter(%p) returned error %d", obj, result);
+ return result;
+}
+
+int _objc_sync_enter_kind(id obj, SyncKind kind)
+{
+ int result = OBJC_SYNC_SUCCESS;
+
+ if (obj) {
+ SyncData* data = id2data(obj, kind, ACQUIRE);
+ ASSERT(data);
+ data->mutex.lock();
+ } else {
+ // @synchronized(nil) does nothing
+ if (DebugNilSync) {
+ _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
+ }
+ objc_sync_nil();
+ if (DebugNilSync == Fatal)
+ _objc_fatal("@synchronized(nil) is fatal");
+ }
+
+ return result;
+}
+
+typedef struct alignas(CacheLineSize) SyncData {
+ struct SyncData* nextData;
+ DisguisedPtr object;
+ SyncKind kind;
+ int32_t threadCount; // number of THREADS using this block
+ recursive_mutex_t mutex;
+
+ bool matches(id matchObject, SyncKind matchKind) {
+ ASSERT(matchKind != SyncKind::invalid);
+ ASSERT(kind != SyncKind::invalid);
+ return object == matchObject && kind == matchKind;
+ }
+} SyncData;
+
+
+using recursive_mutex_t = objc_recursive_lock_t;
+```
+
+可以看到 `@synchronized` 的本质是一个包装了 `objc_recursive_lock_t`(不同版本的 OBJC ,其内部实现会不同) 的 `recursive_mutex_tt` C++ 类。
+
+可以发现,如果 `@synchronized` 参数为`nil`,`@synchronized(nil) `调用 `objc_sync_nil()`,最终什么也不执行。
+
+```
+static SyncData* id2data(id object, SyncKind kind, enum usage why)
+{
+ ASSERT(kind != SyncKind::invalid);
+ spinlock_t *lockp = &LOCK_FOR_OBJ(object);
+ SyncData **listp = &LIST_FOR_OBJ(object);
+ SyncData* result = NULL;
+
+#if ENABLE_FAST_CACHE
+ // Check per-thread single-entry fast cache for matching object
+ bool fastCacheOccupied = NO;
+ SyncData *data = syncData;
+ if (data) {
+ fastCacheOccupied = YES;
+
+ if (data->matches(object, kind)) {
+ // Found a match in fast cache.
+ result = data;
+ if (result->threadCount <= 0 || syncLockCount <= 0) {
+ _objc_fatal("id2data fastcache is buggy");
+ }
+
+ switch(why) {
+ case ACQUIRE: {
+ ++syncLockCount;
+ break;
+ }
+ case RELEASE:
+ if (--syncLockCount == 0) {
+ // remove from fast cache
+ syncData = nullptr;
+ // atomic because may collide with concurrent ACQUIRE
+ AtomicDecrement(&result->threadCount);
+ }
+ break;
+ case CHECK:
+ // do nothing
+ break;
+ }
+
+ return result;
+ }
+ }
+#endif // ENABLE_FAST_CACHE
+
+ // Check per-thread cache of already-owned locks for matching object
+ SyncCache *cache = fetch_cache(NO);
+ if (cache) {
+ unsigned int i;
+ for (i = 0; i < cache->used; i++) {
+ SyncCacheItem *item = &cache->list[i];
+ if (!item->data->matches(object, kind)) continue;
+
+ // Found a match.
+ result = item->data;
+ if (result->threadCount <= 0 || item->lockCount <= 0) {
+ _objc_fatal("id2data cache is buggy");
+ }
+
+ switch(why) {
+ case ACQUIRE:
+ item->lockCount++;
+ break;
+ case RELEASE:
+ item->lockCount--;
+ if (item->lockCount == 0) {
+ // remove from per-thread cache
+ cache->list[i] = cache->list[--cache->used];
+ // atomic because may collide with concurrent ACQUIRE
+ AtomicDecrement(&result->threadCount);
+ }
+ break;
+ case CHECK:
+ // do nothing
+ break;
+ }
+
+ return result;
+ }
+ }
+
+ // Thread cache didn't find anything.
+ // Walk in-use list looking for matching object
+ // Spinlock prevents multiple threads from creating multiple
+ // locks for the same new object.
+ // We could keep the nodes in some hash table if we find that there are
+ // more than 20 or so distinct locks active, but we don't do that now.
+
+ lockp->lock();
+
+ {
+ SyncData* p;
+ SyncData* firstUnused = NULL;
+ for (p = *listp; p != NULL; p = p->nextData) {
+ if ( p->matches(object, kind) ) {
+ result = p;
+ // atomic because may collide with concurrent RELEASE
+ AtomicIncrement(&result->threadCount);
+ goto done;
+ }
+ if ( (firstUnused == NULL) && (p->threadCount == 0) )
+ firstUnused = p;
+ }
+
+ // no SyncData currently associated with object
+ if ( (why == RELEASE) || (why == CHECK) )
+ goto done;
+
+ // an unused one was found, use it
+ if ( firstUnused != NULL ) {
+ result = firstUnused;
+ result->object = (objc_object *)object;
+ result->kind = kind;
+ result->threadCount = 1;
+ goto done;
+ }
+ }
+
+ // Allocate a new SyncData and add to list.
+ // XXX allocating memory with a global lock held is bad practice,
+ // might be worth releasing the lock, allocating, and searching again.
+ // But since we never free these guys we won't be stuck in allocation very often.
+ posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
+ result->object = (objc_object *)object;
+ result->kind = kind;
+ result->threadCount = 1;
+ new (&result->mutex) recursive_mutex_t(fork_unsafe);
+ result->nextData = *listp;
+ *listp = result;
+
+ done:
+ lockp->unlock();
+ if (result) {
+ // Only new ACQUIRE should get here.
+ // All RELEASE and CHECK and recursive ACQUIRE are
+ // handled by the per-thread caches above.
+ if (why == RELEASE) {
+ // Probably some thread is incorrectly exiting
+ // while the object is held by another thread.
+ return nil;
+ }
+ if (why != ACQUIRE) _objc_fatal("id2data is buggy");
+ if (!result->matches(object, kind)) _objc_fatal("id2data is buggy");
+
+#if ENABLE_FAST_CACHE
+ if (!fastCacheOccupied) {
+ // Save in fast thread cache
+ syncData = result;
+ syncLockCount = 1;
+ } else
+#endif // ENABLE_FAST_CACHE
+ {
+ // Save in thread cache
+ if (!cache) cache = fetch_cache(YES);
+ cache->list[cache->used].data = result;
+ cache->list[cache->used].lockCount = 1;
+ cache->used++;
+ }
+ }
+
+ return result;
+}
+```
+
+传递一个参数 obj,经过 `id2data` 方法得到一个结构体对象,访问结构体对象的成员变量 `mutex`,然后调用 `lock` 方法。
+
+可以看到是一个哈希表 `StripedMap`,哈希表工作原理就是传递一个 key,经过哈希算法生成索引,然后获取对应的值。
+
+内部维护了一个哈希表,一个对象对应一个锁(所以为了锁的使用正确,加解锁,需要用同一个对象)
+
+另外 `recursive_mutex_tt` 在初始化的时候传入 `OS_UNFAIR_RECURSIVE_LOCK_INIT`,看起来也支持递归。所以 `@synchronized` 的本质是一个**递归互斥锁**的封装。
+
+
+
+
+
+
+
+
+
+
+
+### 各种锁性能对比
+
+性能从高到低:`os_unfair_lock > OSSpinLock > dispatch_semaphore > pthread_mutex > dispatch_queue(DISPATCH_QUEUE_SERIAL) >NSLock > NSCondition > pthread_mutex(recursive) > NSRecursiveLock > @synchronized`
+
+
+
### 自旋锁、互斥锁对比
-什么情况使用自旋锁比较划算?
+什么情况适合使用自旋锁?
-- 预计线程等待锁的时间很短
+- 预计线程等待锁的时间很短(假设线程1的任务本来就很短,如果使用其他的锁,比如还需要互斥锁的话,底层实现会调用 sysCall,一个休眠一个唤醒,这个时间可能比如循环忙等更耗时。所以如果一个线程任务执行时间很短,则考虑使用自旋锁会更高效一些。)
- 加锁的代码(临界区)经常被调用,但竞争情况很少发生
@@ -1125,19 +1410,52 @@ dispatch_semaphore_signal(semaphore);
- 单核处理器
-- 临界区有IO操作(IO一般占用 CPU 资源较多。互斥锁本身就占用 CPU,所以不适合)
+- 临界区有IO操作(IO 操作一般占用 CPU 资源较多,互斥锁本身就占用 CPU,所以不适合)
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈
+
+
### atomic
+### 源码探究
+
`atomic` 用于保证属性 setter、getter 的原子性操作,相当于在 getter 和 setter 内部加了线程同步的锁。
-可以参考源码 objc4 的 `objc-accessors.mm`
+与之相对的是 `nonatomic`,也就是非原子性的。假设多线程下,针对一个属性的 setter、getter,需要自己加锁来保证读写问题。
-```c
+使用 `atomic` 则属性类似下面的伪代码
+
+```objective-c
+@property (atomic, strong) NSString *name;
+
+- (NSString *)name {
+ // 加锁
+ // logic
+ // 解锁
+ return _name;
+}
+
+- (void)setName:(NSString *)name {
+ // 加锁
+ // logic
+ _name = name;
+ // 解锁
+}
+```
+
+**用 atomic 修饰就是线程安全的吗?不是的**
+
+比如 atomic 修饰了数组,那么对数组指针的读取和赋值(数组的地址修改)是线程安全的,但是数组的操作不是线程安全的,比如增加元素、删除元素、读取元素。
+
+
+具体实现,可以参考源码 objc4 的 `objc-accessors.mm`
+
+属性取值逻辑:
+
+```c++
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
@@ -1146,13 +1464,13 @@ id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
-
+
// Atomic retain release world
- spinlock_t& slotlock = PropertyLocks[slot];
+ spinlock_t& slotlock = PropertyLocks.get()[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
-
+
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
@@ -1164,6 +1482,8 @@ id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
- 如果是 atomic,则调用自旋锁 `slotlock` 加锁,取值,解锁,return
+属性赋值逻辑:
+
```c
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
@@ -1173,7 +1493,7 @@ static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t o
}
id oldValue;
- id *slot = (id*) ((char*)self offset);
+ id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
@@ -1185,10 +1505,12 @@ static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t o
}
if (!atomic) {
+ // nonatomic,直接赋值
oldValue = *slot;
*slot = newValue;
} else {
- spinlock_t& slotlock = PropertyLocks[slot];
+ // atomic,加锁
+ spinlock_t& slotlock = PropertyLocks.get()[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
@@ -1216,16 +1538,19 @@ void lock() {
可以看到设置属性的时候会判断是不是 atomic
- atomic 类型,则直接赋值
-
- 非 atomic 类型,则先自旋锁加锁、赋值、解锁
+
+
它并不能保证使用属性的过程是线程安全的。
QA:为什么在 iOS 上几乎没有使用?
-因为属性 getter、setter 使用太高频,另外 atomic 内部实现是自旋锁,自旋锁是忙等,所以太耗费性能了。
+因为属性 getter、setter 使用太高频了,另外 atomic 内部实现是自旋锁,自旋锁是忙等,针对移动设备上那寸土寸金的 CPU,太奢侈了,太耗费性能了。
-#### atomic 并不能保证使用属性的过程是线程安全的?
+
+
+### atomic 并不能保证使用属性的过程是线程安全的
```objectivec
@property (atomic,copy) NSString *name;
@@ -1254,11 +1579,15 @@ atomic 是原子属性,它内部实现是针对属性的 setter、getter 进
所以多线程并发编程来说,推荐使用锁是一个合理的方案。此外自旋锁不推荐使用,互斥锁中 pthread_mutex 等性能高一些的锁推荐使用。
-## 读写安全
-- 同一时间,只能有1个线程进行写的操作
-- 同一时间,允许有多个线程进行读的操作
-- 同一时间,不允许既有写的操作,又有读的操作
+
+## 多线程读写安全
+
+读写的特点:
+
+- 同一时间,只能有1个线程进行写的操作(只能有1个写)
+- 同一时间,允许有多个线程进行读的操作(可以同时读)
+- 同一时间,不允许既有写的操作,又有读的操作(读写不能同时进行)
“多读单写”问题,经常用于文件、数据的读写操作。iOS 主流方案有:
@@ -1266,83 +1595,39 @@ atomic 是原子属性,它内部实现是针对属性的 setter、getter 进
- dispatch_barrier_async:异步栅栏调用
+
+
### pthread_rwlock
-初始化
+初始化 :
```objectivec
pthread_rwlock_t lock
pthread_rwlock_init(&_lock, NULL)
```
-读操作-加锁 `pthread_rwlock_rdlock(&_lock)`
+读操作-加锁: `pthread_rwlock_rdlock(&_lock)`
-读操作-尝试加锁 `pthread_rwlock_tryrdlock(&_lock);`
+读操作-尝试加锁: `pthread_rwlock_tryrdlock(&_lock);`
-写操作-加锁 `pthread_rwlock_wrlock(&_lock);`
+写操作-加锁: `pthread_rwlock_wrlock(&_lock);`
-写操作-尝试加锁 `pthread_rwlock_trywrlock(&_lock);`
+写操作-尝试加锁: `pthread_rwlock_trywrlock(&_lock);`
-解锁 `pthread_rwlock_unlock(&_lock);`
+解锁: `pthread_rwlock_unlock(&_lock);`
-销毁 `pthread_rwlock_destroy(&_lock);`
+销毁: `pthread_rwlock_destroy(&_lock);`
+
+Demo
+
+
-```objectivec
-@interface ViewController ()
-@property (nonatomic, assign) NSInteger money;
-@property (nonatomic, assign) pthread_rwlock_t lock;
-@end
-@implementation ViewController
-- (void)viewDidLoad{
- [super viewDidLoad];
- pthread_rwlock_init(&_lock, NULL); // 初始化读写锁
- self.money = 100;
- [self moneyTest];
-}
-- (void)moneyTest {
- dispatch_queue_t queue = dispatch_queue_create("com.lbp.money.queue", DISPATCH_QUEUE_CONCURRENT);
- dispatch_async(queue, ^{
- for (int i = 0; i < 10; i++) {
- [self read];
- }
- });
- dispatch_async(queue, ^{
- for (int i = 0; i < 4; i++) {
- [self write];
- }
- });
-}
-- (void)read {
- pthread_rwlock_rdlock(&_lock);
- NSLog(@"read %zd", self.money);
- pthread_rwlock_unlock(&_lock);
-}
-- (void)write {
- pthread_rwlock_wrlock(&_lock);
- self.money += 25;
- NSLog(@"write %zd", self.money);
- pthread_rwlock_unlock(&_lock);
-}
-@end
-2022-04-09 22:25:25.042853+0800 DDD[13652:333135] read 100
-2022-04-09 22:25:25.043058+0800 DDD[13652:333126] write 125
-2022-04-09 22:25:25.043269+0800 DDD[13652:333135] read 125
-2022-04-09 22:25:25.043445+0800 DDD[13652:333126] write 150
-2022-04-09 22:25:25.043586+0800 DDD[13652:333135] read 150
-2022-04-09 22:25:25.043724+0800 DDD[13652:333126] write 175
-2022-04-09 22:25:25.043895+0800 DDD[13652:333135] read 175
-2022-04-09 22:25:25.044037+0800 DDD[13652:333126] write 200
-2022-04-09 22:25:25.044226+0800 DDD[13652:333135] read 200
-2022-04-09 22:25:25.044393+0800 DDD[13652:333135] read 200
-2022-04-09 22:25:25.044557+0800 DDD[13652:333135] read 200
-2022-04-09 22:25:25.044713+0800 DDD[13652:333135] read 200
-2022-04-09 22:25:25.044869+0800 DDD[13652:333135] read 200
-2022-04-09 22:25:25.045020+0800 DDD[13652:333135] read 200
-```
### dispatch_barrier_async
+多读单写,
+
```objectivec
// 初始化队列
self.queue = dispatch_queue_create("rwqueue", DISPATCH_QUEUE_CONCURRENT);
@@ -1363,60 +1648,286 @@ dispatch_barrier_async(self.queue, ^{
上 Demo
-```objectivec
-@interface ViewController ()
-@property (nonatomic, assign) NSInteger money;
-@property (nonatomic, strong) dispatch_queue_t queue;
-@end
+
-@implementation ViewController
-- (void)viewDidLoad{
- [super viewDidLoad];
- self.queue = dispatch_queue_create("rwqueue", DISPATCH_QUEUE_CONCURRENT);
- self.money = 100;
- [self moneyTest];
+
+
+
+
+### dispatch_group_async
+
+如何实现 A、B、C 三个任务并发执行完,再去执行任务 D ?假设需求是根据省市区下载 json,然后根据 json 数据,选中地址 picker view。
+
+```objective-c
+dispatch_group_t group = dispatch_group_create();
+dispatch_queue_t queue = dispatch_queue_create("com.unix.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
+for (NSURL *url in addressArray) {
+ dispatch_group_async(group, queue, ^{
+ // 根据 url 请求 json 数据
+ });
}
-- (void)moneyTest {
- dispatch_queue_t queue = dispatch_queue_create("com.lbp.money.queue", DISPATCH_QUEUE_CONCURRENT);
- dispatch_async(queue, ^{
- for (int i = 0; i < 10; i++) {
- [self read];
- }
- });
- dispatch_async(queue, ^{
- for (int i = 0; i < 4; i++) {
- [self write];
- }
- });
-}
-- (void)read {
- dispatch_async(self.queue, ^{
- NSLog(@"read %zd", self.money);
- });
-}
-- (void)write {
- dispatch_barrier_async(self.queue, ^{
- self.money += 25;
- NSLog(@"write %zd", self.money);
- });
-}
-@end
-2022-04-09 22:37:26.476134+0800 DDD[14019:343934] read 100
-2022-04-09 22:37:26.476134+0800 DDD[14019:343932] read 100
-2022-04-09 22:37:26.476317+0800 DDD[14019:343932] write 125
-2022-04-09 22:37:26.476489+0800 DDD[14019:343932] read 125
-2022-04-09 22:37:26.476629+0800 DDD[14019:343932] write 150
-2022-04-09 22:37:26.476766+0800 DDD[14019:343932] read 150
-2022-04-09 22:37:26.476905+0800 DDD[14019:343932] write 175
-2022-04-09 22:37:26.477033+0800 DDD[14019:343932] write 200
-2022-04-09 22:37:26.477199+0800 DDD[14019:343932] read 200
-2022-04-09 22:37:26.477216+0800 DDD[14019:343934] read 200
-2022-04-09 22:37:26.477233+0800 DDD[14019:343936] read 200
-2022-04-09 22:37:26.477247+0800 DDD[14019:343937] read 200
-2022-04-09 22:37:26.477267+0800 DDD[14019:343938] read 200
-2022-04-09 22:37:26.477269+0800 DDD[14019:343933] read 200
+
+// 会等到上面加入到 group 的3个并发任务全部执行完,再执行下面的 block 任务
+dispatch_group_notify(group, dispatch_get_main_queue(), ^{
+ // 根据下载好的省市区 json 去更新 picker view 。
+});
```
+
+
+## NSOperation
+
+需要和 NSOperationQueue 配合使用。优点:
+
+- 可以添加任务依赖
+
+- 任务执行状态控制
+
+ - isReady
+ - isExecuting
+ - isFinished
+ - isCancelled
+
+ 如果只重写了 main 方法,底层控制变更任务执行完成状态,以及任务退出。
+
+ 如果重写了 start 方法,自行控制任务状态
+
+- 最大并发量
+
+
+
+系统是怎么样移除一个 isFinished = YES 的 NSOperation 的?
+
+看看 GNU 源码。
+
+```c++
+- (void) start
+{
+ ENTER_POOL
+ // 获取线程优先级
+ double prio = [NSThread threadPriority];
+
+ AUTORELEASE(RETAIN(self)); // Make sure we exist while running.
+ [internal->lock lock];
+ NS_DURING
+ {
+ // 做一些状态判断
+ if (YES == [self isExecuting])
+ {
+ [NSException raise: NSInvalidArgumentException
+ format: @"[%@-%@] called on executing operation",
+ NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
+ }
+ if (YES == [self isFinished])
+ {
+ [NSException raise: NSInvalidArgumentException
+ format: @"[%@-%@] called on finished operation",
+ NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
+ }
+ if (NO == [self isReady])
+ {
+ [NSException raise: NSInvalidArgumentException
+ format: @"[%@-%@] called on operation which is not ready",
+ NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
+ }
+ if (NO == internal->executing)
+ {
+ // 如果调用 start 方法,通过 KVO 将 isExecuting 更改为 YES
+ [self willChangeValueForKey: @"isExecuting"];
+ internal->executing = YES;
+ [self didChangeValueForKey: @"isExecuting"];
+ }
+ }
+ NS_HANDLER
+ {
+ [internal->lock unlock];
+ [localException raise];
+ }
+ NS_ENDHANDLER
+ [internal->lock unlock];
+
+ NS_DURING
+ {
+ if (NO == [self isCancelled])
+ {
+ [NSThread setThreadPriority: internal->threadPriority];
+ // 内部调用 main 方法
+ [self main];
+ }
+ }
+ NS_HANDLER
+ {
+ [NSThread setThreadPriority: prio];
+ [localException raise];
+ }
+ NS_ENDHANDLER;
+ // 调用完 start,内部调用 finish 方法
+ [self _finish];
+ LEAVE_POOL
+}
+```
+
+可以看到,内部会调用 `_finish` 方法
+
+```c++
+- (void) _finish
+{
+ [internal->lock lock];
+ if (NO == internal->finished)
+ {
+ if (YES == internal->executing)
+ {
+ [self willChangeValueForKey: @"isExecuting"];
+ [self willChangeValueForKey: @"isFinished"];
+ internal->executing = NO;
+ internal->finished = YES;
+ [self didChangeValueForKey: @"isFinished"];
+ [self didChangeValueForKey: @"isExecuting"];
+ }
+ else
+ {
+ [self willChangeValueForKey: @"isFinished"];
+ internal->finished = YES;
+ [self didChangeValueForKey: @"isFinished"];
+ }
+ if (NULL != internal->completionBlock)
+ {
+ CALL_BLOCK_NO_ARGS(
+ ((GSOperationCompletionBlock)internal->completionBlock));
+ }
+ }
+ [internal->lock unlock];
+}
+```
+
+可以看到在 `_finish` 方法中,系统通过 KVO 移除 NSOperationQueue 中 NSOperation 的。
+
+
+
+## NSThread
+
+NSThread 的一个工作流程如下:
+
+`start() -> 创建 pthread -> main() -> [target performSelector:selector] -> exit`
+
+NSThread 需要保活。为什么会死掉?看看 gnu 源码
+
+```c++
+- (void) start
+{
+ pthread_attr_t attr;
+
+ if (_active == YES)
+ {
+ [NSException raise: NSInternalInconsistencyException
+ format: @"[%@-%@] called on active thread",
+ NSStringFromClass([self class]),
+ NSStringFromSelector(_cmd)];
+ }
+ if (_cancelled == YES)
+ {
+ [NSException raise: NSInternalInconsistencyException
+ format: @"[%@-%@] called on cancelled thread",
+ NSStringFromClass([self class]),
+ NSStringFromSelector(_cmd)];
+ }
+ if (_finished == YES)
+ {
+ [NSException raise: NSInternalInconsistencyException
+ format: @"[%@-%@] called on finished thread",
+ NSStringFromClass([self class]),
+ NSStringFromSelector(_cmd)];
+ }
+
+ /* Make sure the notification is posted BEFORE the new thread starts.
+ */
+ gnustep_base_thread_callback();
+
+ /* The thread must persist until it finishes executing.
+ */
+ RETAIN(self);
+
+ /* Mark the thread as active while it's running.
+ */
+ _active = YES;
+
+ errno = 0;
+ pthread_attr_init(&attr);
+ /* Create this thread detached, because we never use the return state from
+ * threads.
+ */
+ pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
+ /* Set the stack size when the thread is created. Unlike the old setrlimit
+ * code, this actually works.
+ */
+ if (_stackSize > 0)
+ {
+ pthread_attr_setstacksize(&attr, _stackSize);
+ }
+ // 设置回调函数
+ if (pthread_create(&pthreadID, &attr, nsthreadLauncher, self))
+ {
+ DESTROY(self);
+ [NSException raise: NSInternalInconsistencyException
+ format: @"Unable to detach thread (last error %@)",
+ [NSError _last]];
+ }
+}
+```
+
+看看 pthread 创建后的回调函数
+
+```c++
+static void *
+nsthreadLauncher(void *thread)
+{
+ NSThread *t = (NSThread*)thread;
+
+ setThreadForCurrentThread(t);
+
+ /*
+ * Let observers know a new thread is starting.
+ */
+ if (nc == nil)
+ {
+ nc = RETAIN([NSNotificationCenter defaultCenter]);
+ }
+ // 发送通知
+ [nc postNotificationName: NSThreadDidStartNotification
+ object: t
+ userInfo: nil];
+ // 设置线程名
+ [t _setName: [t name]];
+ // 调用 main 方法
+ [t main];
+ // 线程退出
+ [NSThread exit];
+ // Not reached
+ return NULL;
+}
+```
+
+看了源码,会发现 NSThread 调用 start 内部就会调用 `[NSThread exit]` 所以会退出。要想常驻,就需要在 main 方法做 runloop 保活。
+
+```c++
+- (void) main
+{
+ if (_active == NO)
+ {
+ [NSException raise: NSInternalInconsistencyException
+ format: @"[%@-%@] called on inactive thread",
+ NSStringFromClass([self class]),
+ NSStringFromSelector(_cmd)];
+ }
+
+ [_target performSelector: _selector withObject: _arg];
+}
+```
+
+main 方法内其实就是在执行 _selector。也就是在 NSThread 的初始化方法中,传入的 selector 中进行 runloop 保活逻辑。
+
+
+
## 其他常见的多线程编程模式
### Promise
@@ -1461,4 +1972,17 @@ APIClient.fetchData(...).then().onFailure();
通过将多个并发的任务存入队列实现任务的串行化,并为这些串行化任务创建唯一的工作者线程进行处理。
-比如 FMDB 的设计,内部就是一个串行队列。
\ No newline at end of file
+比如 FMDB 的设计,内部就是一个串行队列。
+
+
+
+
+
+## 总结
+
+- 怎么样实现多读单写?GCD dispatch_barrier_async
+- iOS 提供了几种多线程技术
+ - GCD:简单的任务处理,以及多读单写、读写锁、dispatch_group_async
+ - NSOperationQueue、NSOperation:AFNetworking、SDWebImage ,可以方便对 Operation 的状态管理和依赖管理
+ - NSThread,主要用于实现常驻线程
+- NSOperation Finished 之后如何移除?KVO
\ No newline at end of file
diff --git a/Chapter1 - iOS/1.40.md b/Chapter1 - iOS/1.40.md
index d9cebca..ac6f7b5 100644
--- a/Chapter1 - iOS/1.40.md
+++ b/Chapter1 - iOS/1.40.md
@@ -1,18 +1,34 @@
# iOS 内存原理探究
+> 端上内存寸土寸金,对于内存知识你掌握了吗?掌握内存分配、释放的细节有助于我们写出内存使用有好的代码?同时为做 APM 内存监控打下坚实基础。接下来通过下面几个问题,研究下 iOS 侧内存分配、释放的相关知识:
+>
+> - NSTimer 存在什么问题、CADisplayLink 存在什么问题?
+> - weak 指针的实现原理是什么?
+> - ARC 帮我们做了什么处理?
+> - 方法里有局部对象,出了方法会立马结束吗?
+> - autorelease 修饰的对象,其内存在什么时机释放?
+> - 类中的实例变量在哪释放?
+> - 当对象 dealloc 方法中没有显示调用 `[super dealloc]` ,父类的析构如何触发?
+
+
+
## 定时器内存泄漏
-NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES block:nil]` 和当前的 VC 都会互相持有,造成环,会存在内存泄漏问题。
+NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES block:nil]` 对其 target 会产生强引用,如果 target 再对其产生强引用,则互相持有,会造成环,产生内存泄漏
定时器内存泄漏原因,解决方案以及高精度定时器,具体可以看这篇 [NSTimer 中的内存泄露](./1.45.md) 。
+
+
## iOS 内存布局
栈、堆、BSS、数据段、代码段
-
+
-栈(stack):又称作堆栈,用来存储程序的局部变量(但不包括static声明的变量,static修饰的数据存放于数据段中)。除此之外,在函数被调用时,栈用来传递参数和返回值。栈内存地址越来越少
+
+
+栈(stack):又称作堆栈,用来存储程序的局部变量(但不包括static声明的变量,static 修饰的数据存放于数据段中)。除此之外,在函数被调用时,栈用来传递参数和返回值。栈内存地址越来越少(函数内第一行变量、对象的地址最大、后续越来越小,最后一行代码的变量、对象越来越小)
```
func a {
@@ -31,13 +47,13 @@ BSS段(bss segment):通常用来存储程序中未被初始化的全局变
- 字符串常量。比如 `NSString *str = @"杭城小刘";`
-- 已初始化数据:已经初始化的全局变量、静态变量等
+- 已初始化数据:已经初始化的全局变量、静态变量等(内存挨在一起的)
- 未初始化数据:未初始化的全局变量、静态变量等
代码段(code segment):编译之后的代码。通常是指用来存储程序可执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。
-
+
上 Demo 验证
@@ -82,72 +98,681 @@ NSLog(@"%p %p %@", obj, &obj, obj);
分别打印 obj指针指向的堆上的内存地址、obj 指针在栈上的地址、obj 内容
+
+
## Tagged Pointer
-先来一个 Demo 开启本部分内容(画外音:代码很短,但让我产生了一个大大的问号)
+### 为什么有 Tagged Pointer
-```objectivec
-- (bool)isTaggedPointer:(const void *)ptr
+现状:一般,存放 NSNumber、NSDate 这类变量的时候,本身占用的内存大小常常不需要8个字节。4字节带符号的证书可以达到2^31= 2147483648,99% 的情况都能满足了。因此为了更高效、更节省空间,用一个看似是指针的计数,来存储数据,且在 Runtime 侧判断了,节省了消息机制那一套冗长的流程,Tagged Pointer cover 一些小数据的场景,cover 不了则申请堆内存。
+
+
+
+创建对象需要动态分配内存、维护引用计数等,对象指针存储的是堆中对象的地址值。
+
+创建一个对象的流程:先在堆上申请一块内存,然后再在栈上增加一个指针类型,指针指向堆上这块内存。举个例子,假设用 NSNumber 指针,存储一个数值, `NSNumber *value = [NSNumber numberWithInt:2]` ,分析下,需要耗费多少内存?
+
+栈:在栈上,value 是一个指针类型,占用8字节
+
+堆:在堆上,1个 isa 指针占8字节,1个 int 类型,占4字节,但由于存在内存对齐机制,所以堆上共需要16字节大小。
+
+加起来 24 字节,耗费24字节就为了存储一个值为2的 int 数据。
+
+效率上:此外还需要维护引用计数,沿用 OC 中指针,isa、类对象、元类对象的结构和消息发送流程,是不是太大材小用了??
+
+
+
+
+
+### 什么是 Tagged Pointer
+
+iOS 从 64bit 开始引入了Tagged Pointer 技术,用于优化 NSNumber、NSDate、NSString 等小对象的存储。
+
+Tagged Pointer 格式下,指针值不再是有效抵制,而是表示值。对象指针里面存储的数据变成了:`Tag + Data`,将数据直接存储在了指针中。当指针不够存储数据时,才会使用动态分配内存的方式来存储数据。
+
+当对 TaggedPointer 数据调用方法的时候,objc_msgSend 能识别出如果是 Tagged Pointer,比如 NSNumber 的 intValue 方法,直接从指针提取数据,节省了调用开销。所以使用了 TaggedPointer 技术不仅节约内存空间,又能提高方法查找速度。
+
+
+
+
+
+Tagged Pointer 也就是一个伪指针,对象的指针中存储的数据变成了:Tag + Data 的形式。
+
+- Tag 为特殊标记,用于区分是否是 Tagged Pointer 指针以及具体是 NSNumber、NSDate、NSString 等对象类型
+- Data:为对象对应存储的值。
+
+
+
+根据官方的说明,使用tagged pointer进行小数据存储的优势非常明显:
+
+- 可以见上一半的内存占用;
+- 可以将访问速度提升3倍以上;
+- 提升100倍的创建销毁速度
+
+从 objc 源码,objc-runtime-new.mm 中可以看到:
+
+> /***********************************************************************
+>
+> \* Tagged pointer objects.
+>
+> *
+>
+> \* Tagged pointer objects store the class and the object value in the
+>
+> \* object pointer; the "pointer" does not actually point to anything.
+>
+> *
+>
+> \* Tagged pointer objects currently use this representation:
+>
+> \* (LSB)
+>
+> \* 1 bit set if tagged, clear if ordinary object pointer
+>
+> \* 3 bits tag index
+>
+> \* 60 bits payload
+>
+> \* (MSB)
+>
+> \* The tag index defines the object's class.
+>
+> \* The payload format is defined by the object's class.
+>
+> *
+>
+> \* If the tag index is 0b111, the tagged pointer object uses an
+>
+> \* "extended" representation, allowing more classes but with smaller payloads:
+>
+> \* (LSB)
+>
+> \* 1 bit set if tagged, clear if ordinary object pointer
+>
+> \* 3 bits 0b111
+>
+> \* 8 bits extended tag index
+>
+> \* 52 bits payload
+>
+> \* (MSB)
+>
+> *
+>
+> \* Some architectures reverse the MSB and LSB in these representations.
+>
+> *
+>
+> \* This representation is subject to change. Representation-agnostic SPI is:
+>
+> \* objc-internal.h for class implementers.
+>
+> \* objc-gdb.h for debuggers.
+>
+> ***\**\**\**\**\**\**\**\**\**\**\**\**\**\**\**\**\**\**\**\**\**\**\**\**\**\**\**\**\**\**\**\**\**\*******/**
+
+从 apple 给出的声明中,可以得到:
+
+- 标签指针对象存储了类信息和对象实际的值,此时的指针不指向任何东西;
+
+- 使用最低位作为标记位,如果是标签指针对象就标记为1,如果是普通对象类型就标记为0;
+
+- 紧接着三位是标签索引位;
+
+- 剩余的60位位有效的负载位,标签索引位定义了标签对象代表的对象的真实类型,负载的格式由实际的类定义;
+
+- 如果标签位是0b111,表示该对象使用了是被扩展的标签对象,这种扩展的方式可以运训更多的类使用标签对象来表示,同时负载的有效位数变小。这时:
+ - 最低位是标记位
+ - 紧接着三位位0b111
+ - 紧接着八位位扩展的标记位
+ - 剩余的52位才是真正的有效的负载位
+
+- 并不是所有的架构中都使用低位做标记位.在指令集框架中,除了64-bit的Mac操作系统之外,其余全是使用 MSB。比如 iOS 就是使用高位作为标志位。
+
+
+
+Demo
+
+
+
+在 64 位的 cpu 下,如果使用 Tagged Pointer 技术的话,则 NSNumber 对象的值直接存储在了指针中,系统不会为其在堆上分配内存,可以节省很多内存开销。此时,NSNumber 对象的指针中存储的数据变成了 Tag + Data 的形式(Tag 为特殊标记,用于区分NSNumber、NSDate、NSString 等小内存对象的类型;Data 为具体的值)。这样使用一个 NSNumber 对象只需要在栈中开辟 8 个字节的指针内存。当栈中 8 个字节的指针内存不够存储数据时,才会再将 NSNumber 对象存储到堆中
+
+系统会自动判断是分配 TaggedPointer 还是普通指针(也就是真正在堆上开辟内存)
+
+
+
+### 实践探索 Tagged Pointer
+
+为了保证数据安全,对 Tagged Pointer 类型的指针做了数据混淆,无法通过打印指针的内容来判断一个指针是否为 Tagged Pointer 类型,更无法读取存储在 Tagged Pointer 类型的指针中的数据
+
+为了方便我们在分析 Tagged Pointer 的原理时调试程序,需要先解除系统对 Tagged Pointer 的数据混淆,2个办法:
+
+第一种:Xcode 环境变量。
+
+环境变量 `OBJC_DISABLE_TAG_OBFUSCATION` 来控制 Tagged Pointer 数据混淆的禁用和启用。默认情况下,Tagged Pointer 的数据混淆处于启用状态。
+
+路径:Xcode - Edit Scheme - Run - Arguments - Environment Variables - 添加环境变量 `OBJC_DISABLE_TAG_OBFUSCATION` 设置为 YES 即可。
+
+
+
+
+
+第二种:还原 Runtime 对 tagged Pointer 的混淆函数。objc 源码中 objc-runtime-new.mm 文件
+
+```c++
+/***********************************************************************
+* initializeTaggedPointerObfuscator
+* Initialize objc_debug_taggedpointer_obfuscator with randomness.
+*
+* The tagged pointer obfuscator is intended to make it more difficult
+* for an attacker to construct a particular object as a tagged pointer,
+* in the presence of a buffer overflow or other write control over some
+* memory. The obfuscator is XORed with the tagged pointers when setting
+* or retrieving payload values. They are filled with randomness on first
+* use.
+**********************************************************************/
+static void
+initializeTaggedPointerObfuscator(void)
{
- return ((uintptr_t)ptr & (1UL<<63)) == (1UL<<63);
+ if (!DisableTaggedPointerObfuscation
+#if !TARGET_OS_EXCLAVEKIT
+ && dyld_program_sdk_at_least(dyld_fall_2018_os_versions)
+#endif
+ ) {
+ // Pull random data into the variable, then shift away all non-payload bits.
+ arc4random_buf(&objc_debug_taggedpointer_obfuscator,
+ sizeof(objc_debug_taggedpointer_obfuscator));
+ objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
+
+#if OBJC_SPLIT_TAGGED_POINTERS
+ // The obfuscator doesn't apply to any of the extended tag mask or the no-obfuscation bit.
+ objc_debug_taggedpointer_obfuscator &= ~(_OBJC_TAG_EXT_MASK | _OBJC_TAG_NO_OBFUSCATION_MASK);
+
+ // Shuffle the first seven entries of the tag permutator.
+ int max = 7;
+ for (int i = max - 1; i >= 0; i--) {
+ int target = uniformRandom(i + 1);
+ swap(objc_debug_tag60_permutations[i],
+ objc_debug_tag60_permutations[target]);
+ }
+#endif
+ } else {
+ // Set the obfuscator to zero for apps linked against older SDKs,
+ // in case they're relying on the tagged pointer representation.
+ objc_debug_taggedpointer_obfuscator = 0;
+ }
}
-
-NSNumber *number = [NSNumber numberWithInt:10]; // 0xb0000000000000a2 b:12 1100
-NSLog(@"%p %d %@", number, [self isTaggedPointer:(__bridge const void *)number], number.class);
-
-NSString *name1 = [NSString stringWithFormat:@"ss"]; // 0xa000000000073732 a:11 1011
-NSLog(@"%p %d %@", name1, [self isTaggedPointer:(__bridge const void *)name1], name1.class);
```
-前提说明:
+`initializeTaggedPointerObfuscator` 函数用一个随机数,来初始化 Tagged Pointer 的混淆因子 `objc_debug_taggedpointer_obfuscator`。
-创建一个 NSNumer 类型的变量 number,NSString 类型的 name1,代码打印地址、类型。产生一个问题:为什么 NSNumber 是 TaggedPointer,但是 class 却显示 __NSCFNumber ?
+主要作用是使攻击者在发现(缓冲区溢出漏洞)或者(内存写入控制漏洞)时,更难将特定对象构造成 Tagged Pointer 类型的指针,为的是更安全。
-```c
-static inline bool _objc_isTaggedPointer(const void * _Nullable ptr)
+同时,也提供了 Tagged Pointer 的编码、解码方法
+
+```c++
+// objc-internal.h
+static inline void * _Nonnull
+_objc_encodeTaggedPointer(uintptr_t ptr)
+{
+ return _objc_encodeTaggedPointer_withObfuscator(ptr, objc_debug_taggedpointer_obfuscator);
+}
+
+static inline void * _Nonnull
+_objc_encodeTaggedPointer_withObfuscator(uintptr_t ptr, uintptr_t obfuscator)
+{
+ uintptr_t value = (obfuscator ^ ptr);
+#if OBJC_SPLIT_TAGGED_POINTERS
+ if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
+ return (void *)ptr;
+ uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
+ uintptr_t permutedTag = _objc_basicTagToObfuscatedTag(basicTag);
+ value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
+ value |= permutedTag << _OBJC_TAG_INDEX_SHIFT;
+#endif
+ return (void *)value;
+}
+
+static inline uintptr_t
+_objc_decodeTaggedPointer(const void * _Nullable ptr)
+{
+ return _objc_decodeTaggedPointer_withObfuscator(ptr, objc_debug_taggedpointer_obfuscator);
+}
+
+static inline uintptr_t
+_objc_decodeTaggedPointer_withObfuscator(const void * _Nullable ptr,
+ uintptr_t obfuscator)
+{
+ uintptr_t value
+ = _objc_decodeTaggedPointer_noPermute_withObfuscator(ptr, obfuscator);
+#if OBJC_SPLIT_TAGGED_POINTERS
+ uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
+
+ value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
+ value |= _objc_obfuscatedTagToBasicTag(basicTag) << _OBJC_TAG_INDEX_SHIFT;
+#endif
+ return value;
+}
+
+static inline uintptr_t
+_objc_decodeTaggedPointer_noPermute_withObfuscator(const void * _Nullable ptr,
+ uintptr_t obfuscator)
+{
+ uintptr_t value = (uintptr_t)ptr;
+#if OBJC_SPLIT_TAGGED_POINTERS
+ if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
+ return value;
+#endif
+ return value ^ obfuscator;
+}
+```
+
+虽然无法使用 objc 里的函数,但是 `objc_debug_taggedpointer_obfuscator` 是一个导出的全局变量。
+
+```c++
+OBJC_EXPORT uintptr_t objc_debug_taggedpointer_obfuscator
+ OBJC_AVAILABLE(10.14, 12.0, 12.0, 5.0, 3.0);
+```
+
+所以可以参考源码,对其进行改造,打造自己的 Tagged Pointer 编码、解码方法
+```objective-c
+OBJC_EXPORT uintptr_t objc_debug_taggedpointer_obfuscator;
+
+static inline void * _Nonnull lbp_objc_encodeTaggedPointer(uintptr_t ptr) {
+ return lbp_objc_encodeTaggedPointer_withObfuscator(ptr, objc_debug_taggedpointer_obfuscator);
+}
+
+static inline void * _Nonnull lbp_objc_encodeTaggedPointer_withObfuscator(uintptr_t ptr, uintptr_t obfuscator) {
+ uintptr_t value = (obfuscator ^ ptr);
+#if OBJC_SPLIT_TAGGED_POINTERS
+ if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
+ return (void *)ptr;
+ uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
+ uintptr_t permutedTag = _objc_basicTagToObfuscatedTag(basicTag);
+ value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
+ value |= permutedTag << _OBJC_TAG_INDEX_SHIFT;
+#endif
+ return (void *)value;
+}
+
+static inline uintptr_t lbp_objc_decodeTaggedPointer(const void * _Nullable ptr) {
+ return lbp_objc_decodeTaggedPointer_withObfuscator(ptr, objc_debug_taggedpointer_obfuscator);
+}
+
+static inline uintptr_t lbp_objc_decodeTaggedPointer_withObfuscator(const void * _Nullable ptr, uintptr_t obfuscator) {
+ uintptr_t value
+ = _objc_decodeTaggedPointer_noPermute_withObfuscator(ptr, obfuscator);
+#if OBJC_SPLIT_TAGGED_POINTERS
+ uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
+
+ value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
+ value |= _objc_obfuscatedTagToBasicTag(basicTag) << _OBJC_TAG_INDEX_SHIFT;
+#endif
+ return value;
+}
+
+static inline uintptr_t
+_objc_decodeTaggedPointer_noPermute_withObfuscator(const void * _Nullable ptr, uintptr_t obfuscator) {
+ uintptr_t value = (uintptr_t)ptr;
+#if OBJC_SPLIT_TAGGED_POINTERS
+ if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
+ return value;
+#endif
+ return value ^ obfuscator;
+}
+```
+
+
+
+
+
+### Tagged Pointer 结构
+
+#### Tagged Pointer 与 isa
+
+
+
+通过参考 objc 源码,针对对象指针进行解密后发现:
+
+| 原始指针地址 | decode 后指针地址 | 数值 | isa |
+| ------------------ | ------------------ | ----------------- | ------------ |
+| 0xa6560a805d53eb09 | 0xb000000000000013 | 1 | 0x0 |
+| 0xa6560a805d53eb39 | 0xb000000000000023 | 2 | 0x0 |
+| 0xa6560a805d53eb29 | 0xb000000000000033 | 3 | 0x0 |
+| 0x600000ca8020 | 0x16566a805d996b3a | 0xFFFFFFFFFFFFFFF | __NSCFNumber |
+
+num1、num2、num3 对象的值1、2、3分别存储在指针 `0xb000000000000013` 、`0xb000000000000023`、`0xb000000000000033` 的倒数第二位中。而 `0xFFFFFFFFFFFFFFF` 数据太大,无法存储在1个指针长度可以表示的数据范围内,所以申请了堆内存。
+
+num1、num2、num3 都是 Tagged Pointer,是伪指针,所以 isa 都是 nil。num4 1个指针长度存储不下数据,所以分配了堆内存,是真正的对象,有 isa。
+
+
+
+#### Tagged Pointer 数据类型
+
+0xb000000000000013 种的 b 和 3是什么?
+
+b 也就是11,二进制为 `1011`,Tagged Pointer 中,iOS 侧第一位是 Tagged Pointer 标识位,1代表是 Tagged Pointer;
+
+`011` 是类标识位,对应10进制的3,表示 NSNumber 类( 源码中 `OBJC_TAG_NSNumber = 3` )。
+
+
+
+指针中的 3 代表什么?
+
+3 区分数据类型。具体是什么数据类型,继续做个实验看看
+
+
+
+
+
+分析:
+
+- 针对 NSNumber:按照指针顺序依次打印发现,不同的基本数据类型,但都用 NSNumber 类包装,内存地址中,倒数第二位都是字面量的值,指针地址除了最后一位不同之外,都相同。
+
+ | 数据类型 | 内存地址二进制最后1位 |
+ | -------- | --------------------- |
+ | char | 0 |
+ | short | 1 |
+ | int | 2 |
+ | long | 3 |
+ | float | 4 |
+ | double | 5 |
+
+ 所以这个3代表是数据类型(NSNumber 中 char、short、int、long、float、double,NSString 为 string 长度)
+
+ Objc 源码中,NSInteger、NSUInteger 都是别名。初始化 NSNumber 的时候用的是 `NSNumber numberWithInteger:<#(NSInteger)#>`
+
+
+
+
+
+- 通过对 str1、str2、str3 的分析可以看出,指针最后1位代表字符串的长度,长度分别为1、2、3。后面分别是字符串的 ASCII 值
+
+ | 原始指针地址 | decode 后指针地址 | 数值 | ASCII 值 |
+ | ------------------ | ------------------ | ---- | -------- |
+ | 0x82afd51af4bd2853 | 0xa000000000000611 | a | 61 |
+ | 0x82afd51af4bb0850 | 0xa000000000062612 | ab | 61 62 |
+ | 0x82afd51af28b0851 | 0xa000000006362613 | abc | 61 62 63 |
+
+结论:对于 NSNumber 来说,最后一位代表包装前原始数据的类型;对于 NSString 来说,最后一位代表字符串的长度。
+
+
+
+#### 类标识
+
+Tagged Pointer 如何区分是较小的对象,比如 NSString、NSDate、NSNumber?
+
+源码 `objc_internal.h` 如下
+
+```c++
+{
+ // 60-bit payloads
+ OBJC_TAG_NSAtom = 0,
+ OBJC_TAG_1 = 1,
+ OBJC_TAG_NSString = 2,
+ OBJC_TAG_NSNumber = 3,
+ OBJC_TAG_NSIndexPath = 4,
+ OBJC_TAG_NSManagedObjectID = 5,
+ OBJC_TAG_NSDate = 6,
+
+ // 60-bit reserved
+ OBJC_TAG_RESERVED_7 = 7,
+
+ // 52-bit payloads
+ OBJC_TAG_Photos_1 = 8,
+ OBJC_TAG_Photos_2 = 9,
+ OBJC_TAG_Photos_3 = 10,
+ OBJC_TAG_Photos_4 = 11,
+ OBJC_TAG_XPC_1 = 12,
+ OBJC_TAG_XPC_2 = 13,
+ OBJC_TAG_XPC_3 = 14,
+ OBJC_TAG_XPC_4 = 15,
+ OBJC_TAG_NSColor = 16,
+ OBJC_TAG_UIColor = 17,
+ OBJC_TAG_CGColor = 18,
+ OBJC_TAG_NSIndexSet = 19,
+ OBJC_TAG_NSMethodSignature = 20,
+ OBJC_TAG_UTTypeRecord = 21,
+ OBJC_TAG_Foundation_1 = 22,
+ OBJC_TAG_Foundation_2 = 23,
+ OBJC_TAG_Foundation_3 = 24,
+ OBJC_TAG_Foundation_4 = 25,
+ OBJC_TAG_CGRegion = 26,
+
+ // When using the split tagged pointer representation
+ // (OBJC_SPLIT_TAGGED_POINTERS), this is the first tag where
+ // the tag and payload are unobfuscated. All tags from here to
+ // OBJC_TAG_Last52BitPayload are unobfuscated. The shared cache
+ // builder is able to construct these as long as the low bit is
+ // not set (i.e. even-numbered tags).
+ OBJC_TAG_FirstUnobfuscatedSplitTag = 136, // 128 + 8, first ext tag with high bit set
+
+ OBJC_TAG_Constant_CFString = 136,
+
+ OBJC_TAG_First60BitPayload = 0,
+ OBJC_TAG_Last60BitPayload = 6,
+ OBJC_TAG_First52BitPayload = 8,
+ OBJC_TAG_Last52BitPayload = 263,
+
+ OBJC_TAG_RESERVED_264 = 264
+};
+```
+
+验证下
+
+
+
+可以看到:
+
+- num1 地址 `0xb000000000000013`,其中 b 为11,二进制为 `1011`,其中 iOS 侧采用 LSB,则第一位标记判断是不是 Tagged Pointer,当前为1,则说明是 Tagged Pointer。剩下的 `011` 是类标识位,对应十进制为3,表示 NSNumber 类型。
+- str1 地址 `0xa000000000062612`,其中 a 为10,二进制为 `1010`,第一位是 1,表示是 Tagged Pointer。其余 `010`,也就是2,表示 NSString 类
+
+
+
+#### 结构
+
+下面是针对 double 包装成 NSNumber 的 Tagged Pointer 指针结构拆分:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+### 如何判断一个指针是否为Tagged Pointer
+
+查看 objc4 源码(目前新版的 objc4-objc4-912.3)
+
+```c++
+// objc-internal.h
+#if __arm64__
+// ARM64 uses a new tagged pointer scheme where normal tags are in
+// the low bits, extended tags are in the high bits, and half of the
+// extended tag space is reserved for unobfuscated payloads.
+# define OBJC_SPLIT_TAGGED_POINTERS 1
+#else
+# define OBJC_SPLIT_TAGGED_POINTERS 0
+#endif
+
+#if OBJC_SPLIT_TAGGED_POINTERS
+# define _OBJC_TAG_MASK (1UL<<63)
+# define _OBJC_TAG_INDEX_SHIFT 0
+# define _OBJC_TAG_SLOT_SHIFT 0
+# define _OBJC_TAG_PAYLOAD_LSHIFT 1
+# define _OBJC_TAG_PAYLOAD_RSHIFT 4
+# define _OBJC_TAG_EXT_MASK (_OBJC_TAG_MASK | 0x7UL)
+# define _OBJC_TAG_NO_OBFUSCATION_MASK ((1UL<<62) | _OBJC_TAG_EXT_MASK)
+# define _OBJC_TAG_CONSTANT_POINTER_MASK \
+ ~(_OBJC_TAG_EXT_MASK | ((uintptr_t)_OBJC_TAG_EXT_SLOT_MASK << _OBJC_TAG_EXT_SLOT_SHIFT))
+# define _OBJC_TAG_EXT_INDEX_SHIFT 55
+# define _OBJC_TAG_EXT_SLOT_SHIFT 55
+# define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 9
+# define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12
+#elif OBJC_MSB_TAGGED_POINTERS
+# define _OBJC_TAG_MASK (1UL<<63)
+# define _OBJC_TAG_INDEX_SHIFT 60
+# define _OBJC_TAG_SLOT_SHIFT 60
+# define _OBJC_TAG_PAYLOAD_LSHIFT 4
+# define _OBJC_TAG_PAYLOAD_RSHIFT 4
+# define _OBJC_TAG_EXT_MASK (0xfUL<<60)
+# define _OBJC_TAG_EXT_INDEX_SHIFT 52
+# define _OBJC_TAG_EXT_SLOT_SHIFT 52
+# define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 12
+# define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12
+#else
+# define _OBJC_TAG_MASK 1UL
+# define _OBJC_TAG_INDEX_SHIFT 1
+# define _OBJC_TAG_SLOT_SHIFT 0
+# define _OBJC_TAG_PAYLOAD_LSHIFT 0
+# define _OBJC_TAG_PAYLOAD_RSHIFT 4
+# define _OBJC_TAG_EXT_MASK 0xfUL
+# define _OBJC_TAG_EXT_INDEX_SHIFT 4
+# define _OBJC_TAG_EXT_SLOT_SHIFT 4
+# define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 0
+# define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12
+#endif
+
+static inline bool
+_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
```
-- 通过 objc4 源码研究写了个判断对象是否是 Tagged Pointer 类型的方法。通过系统源码参考写了判断方法 `isTaggedPointer`。调用方法得到 number 对象是 Tagged Pointer 类型
+分析:
-- 根据 iOS 平台特性,根据内存地址高位分析确实是 TaggedPointer 类型
+- MacOS 采用 **LSB**(Least Significant Bit,即最低有效位)为 Tagged Pointer 标识位;
+- iOS 采用 **MSB**(Most Significant Bit,即最高有效位)为 Tagged Pointer 标识位
+- 宏定义 `OBJC_SPLIT_TAGGED_POINTERS` 在 arm64 架构下为真,其他情况为假。
-- 同样的 NSString 指针指向的字符串内容比较少,占用内存没必要开创新的内存时,name1 就是 NSTaggedPointerString,打印出 class 也是 NSTaggedPointerString。调用 `isTaggedPointer` 得到也是 Tageed Pointer 类型
+可以看到源码通过 `_objc_isTaggedPointer` 方法判断是否是 Tagged Pointer 类型。传入对象地址,内部通过 `_OBJC_TAG_MASK` 按位与运算。
-带着问题开始吧
+其中 `_OBJC_TAG_MASK` 是一个宏,宏定义外部有个 if 判读,判断是 `OBJC_SPLIT_TAGGED_POINTERS` 或 `OBJC_MSB_TAGGED_POINTERS`,都为 `(1UL<<63)`,其余则为 `1UL`
-### 什么是 Tagged Pointer
+综合来看,iOS 侧不管是不是 arm64,对于 `_OBJC_TAG_MASK` 的值都是 ` (1UL<<63)` ,其他 MacOS 下则为 `1UL`
-iOS 从 64bit 开始引入了Tagged Pointer 技术,用于优化 NSNumber、NSDate、NSString等小对象的存储。
+- iOSOS: 最高有效位是1(第64bit)`1UL<<63`,也就是 `10000000...0`,第一位是1,后面63个0。
-在此之前,创建对象需要动态分配内存、维护引用计数等,对象指针存储的是堆中对象的地址值创建一个对象的流程。先在堆上申请一块内存,然后再在栈上增加一个指针类型,指针指向堆上这块内存。假如是 `NSNumber *value = [NSNumber numberWithInt:2]` value 是指针长度为8字节,堆上内存16字节。加起来24字节就存一个int 2。
+- 非 iOS: 最低有效位是1`1UL`。也就是 `0000...1`,共63个0,最后一位是1。
-此外还需要维护引用技术,沿用一个真正对象那一套,太大材小用了。
-Tagged Pointer 格式,对象指针里面存储的数据变成了:`Tag + Data`,将数据直接存储在了指针中。当指针不够存储数据时,才会使用动态分配内存的方式来存储数据
-objc_msgSend 能识别 Tagged Pointer,比如 NSNumber 的 intValue 方法,直接从指针提取数据,节省了调用开销。
+所以,判断是不是 TaggedPointer 可以用下面代码判断
-### 经典问题
+```c++
+#if OBJC_SPLIT_TAGGED_POINTERS
+# define _OBJC_TAG_MASK (1UL<<63)
+#elif OBJC_MSB_TAGGED_POINTERS
+# define _OBJC_TAG_MASK (1UL<<63)
+#else
+# define _OBJC_TAG_MASK 1UL
+#endif
-Demo1
-
-```objectivec
-- (void)test {
- dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
- for (NSInteger i = 0; i<1000; i++) {
- dispatch_async(queue, ^{
- self.name = [NSString stringWithFormat:@"和好多好多好多好多事看看上课上课上课"];
- });
- }
+static inline bool _objc_isTaggedPointer(const void * _Nullable ptr) {
+ return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
```
+
+
+### NONPOINTER_ISA
+
+在64位架构下,ISA 占64位空间,但实际上用不了那么多,实际上有32位或者40位就够用了,剩余的比较浪费。iOS 为了提高利用率,在剩余的位上存储了一些内存管理相关的信息。所以是不纯粹的指针。叫 NONPOINTER_ISA。
+
+isa 中64位的首位为1,即 NONPOINTER_ISA。
+
+
+
+### Tagged Pointer 与内存管理
+
+因为 Tagged Pointer 是伪指针,如果设计 objc 指针的一些逻辑,比如对象 retain、release,都是优先判断是不是 Tagged Pointer 的。没必要执行一个真正对象指针的后续流程。
+
+```c++
+// objc-object.h
+ALWAYS_INLINE id
+objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
+{
+ if (slowpath(isTaggedPointer())) return (id)this;
+ // ...
+}
+
+ALWAYS_INLINE bool
+objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
+{
+ if (slowpath(isTaggedPointer())) return false;
+ // ...
+}
+
+inline bool
+objc_object::isTaggedPointer() const
+{
+ return _objc_isTaggedPointer(this);
+}
+```
+
+
+
+### Tagged Pointer 与消息发送
+
+消息机制 objc_msgSend 也会优先判断 Tagged Pointer 相关逻辑
+
+```assembly
+ MSG_ENTRY _objc_msgSend // 入口
+ UNWIND _objc_msgSend, NoFrame
+
+ cmp p0, #0 // nil check and tagged pointer check 判断 nil 和 Tagged Pointer 逻辑
+#if SUPPORT_TAGGED_POINTERS // 如果支持 Tagged Pointer,则执行下面逻辑
+ b.le LNilOrTagged // (MSB tagged pointer looks negative) // 跳转到 LNilOrTagged 部分
+#else
+ b.eq LReturnZero
+#endif
+ ldr p14, [x0] // p14 = raw isa
+ GetClassFromIsa_p16 p14, 1, x0 // p16 = class
+LGetIsaDone:
+ // calls imp or objc_msgSend_uncached
+ CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
+
+#if SUPPORT_TAGGED_POINTERS
+LNilOrTagged:
+ b.eq LReturnZero // nil check 先做 nil 检查
+ GetTaggedClass // 获取 Tagged Pointer 真实的 class
+ b LGetIsaDone // 跳转到 LGetIsaDone
+// SUPPORT_TAGGED_POINTERS
+#endif
+
+LReturnZero:
+ // x0 is already zero
+ mov x1, #0
+ movi d0, #0
+ movi d1, #0
+ movi d2, #0
+ movi d3, #0
+ ret
+
+ END_ENTRY _objc_msgSend
+```
+
+可以看到 objc_msgSend 的汇编实现里,也优先判断了 Tagged Pointer。
+
+
+
+### Tagged Pointer 经典问题
+
+Demo1
+
运行该代码会 Crash,报错信息如下
-
+
+
+
说明:一开始的报错信息只说坏内存访问,但是并没有显示具体的方法调用堆。想知道具体 Crash 原因还是需要看看堆栈比较方便。输入 bt 查看最后是由于 `objc_release` 方法造成 crash。
@@ -166,22 +791,25 @@ Demo1
}
```
-Demo
+怎么改?
+
+改法1:将 property 改为 **atomic** 修饰的。
+
+
+
+改法2:对 name 加锁
+
+
+
+
+
+
+
+Demo2
+
+
+
-```objectivec
-- (void)test {
- dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
- for (NSInteger i = 0; i<1000; i++) {
- dispatch_async(queue, ^{
- self.name = [NSString stringWithFormat:@"ss"];
- if (i == 100) {
- NSLog(@"%p %@", self.name, self.name.class);
- }
- });
- }
-}
-// 0xa000000000073732 NSTaggedPointerString
-```
同样的代码字符串变短居然不 crash 了?因为命中 Tagged Pointer 逻辑了,查看类型是 `NSTaggedPointerString`
@@ -191,188 +819,186 @@ Demo
- 多线程情况下访问 setter 需要加锁
-- 字符串在 NSTaggedPointerString 情况下不存在像 OC 对象的 setter 方法内的 release、copy 操作
+- 字符串在 NSTaggedPointerString 情况下,不存在像 OC 对象的 setter 方法内的 release、copy 操作。所以多线程下不会 crash
-### 如何判断一个指针是否为Tagged Pointer
-查看 objc4 源码
-```c
-#if TARGET_OS_OSX && __x86_64__
- // 64-bit Mac - tag bit is LSB
-# define OBJC_MSB_TAGGED_POINTERS 0
-#else
- // Everything else - tag bit is MSB
-# define OBJC_MSB_TAGGED_POINTERS 1
-#endif
+### TaggedPointer 与类簇
+Cocoa 里有很多类簇。比如 NSArray、NSString。
+- 类簇模式可以把实现细节隐藏在一套简单的公共接口后面
+- 系统框架中经常使用类簇
+- 从类簇的公共抽象基类中继承子类时要小心,应该覆写基类中需要覆写的方法。
+- 工厂方法是实现类簇的常见方案
-#if OBJC_MSB_TAGGED_POINTERS
-# define _OBJC_TAG_MASK (1UL<<63)
-# define _OBJC_TAG_INDEX_SHIFT 60
-# define _OBJC_TAG_SLOT_SHIFT 60
-# define _OBJC_TAG_PAYLOAD_LSHIFT 4
-# define _OBJC_TAG_PAYLOAD_RSHIFT 4
-# define _OBJC_TAG_EXT_MASK (0xfUL<<60)
-# define _OBJC_TAG_EXT_INDEX_SHIFT 52
-# define _OBJC_TAG_EXT_SLOT_SHIFT 52
-# define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 12
-# define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12
-#else
-# define _OBJC_TAG_MASK 1UL
-# define _OBJC_TAG_INDEX_SHIFT 1
-# define _OBJC_TAG_SLOT_SHIFT 0
-# define _OBJC_TAG_PAYLOAD_LSHIFT 0
-# define _OBJC_TAG_PAYLOAD_RSHIFT 4
-# define _OBJC_TAG_EXT_MASK 0xfUL
-# define _O BJC_TAG_EXT_INDEX_SHIFT 4
-# define _OBJC_TAG_EXT_SLOT_SHIFT 4
-# define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 0
-# define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12
-#endif
+NSString 是一个抽象工厂模式设计的类簇。NSString、NSMutableString 在外部提供了接口,这些方法的实现由具体的内部类完成。当使用 NSString、NSMutableString 的外部接口生成一个实例对象的时候,初始化方法会判断哪个内部类最适合完成,最后根据此内部类生成具体实例返回给调用者。不同的创建方式、不同的字符串长度,决定生成不同的内部类类型。
-static inline bool _objc_isTaggedPointer(const void * _Nullable ptr)
-{
- return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
-}
-```
+NSString、NSMutableString 的内部类如下:
-可以看到源码通过 `_objc_isTaggedPointer` 方法判断是否是 Tagged Pointer 类型。传入对象地址,内部通过 `_OBJC_TAG_MASK` 按位与运算。
+- NSTaggedPointerString:数据存储在指针中,不需要维护引用计数和方法调用的开销
+- NSCFConstantString:用于表示字符串常量,存储在字符串常量区,不需要维护引用计数。相同内容的 NSCFConstantString 对象的地址相同,也就是说字符串常量是一种单例对象,NSCFConstantString 对象一般通过字面量 `@"**"` 创建
+- NSCFString 存储在堆区,需要维护引用计数。通过 `stringWithFormat:` 等方法创建的 NSString 对象(且字符串长度过长,无法使用 Tagged Pointer 存储)一般都是这种类型
-其中 _OBJC_TAG_MASK 是一个宏,宏定义外部有个 if 判读,判断是 Mac OS 并且是 x86_64 架构则为0,否则为1。也就是 Mac OS 并且是 x86_64 架构情况下则与 `1UL` 按位与,否则与 `1UL<<63` 按位与。
+NSString、NSMutableString 继承关系如下:
-- iOS平台 | Mac 非 x86 平台: 最高有效位是1(第64bit)`1UL<<63`
+- NSTaggedPointerString 继承自 NSString
+- NSCFConstantString 继承自 NSSimpleCString,NSSimpleCString 继承 NSString
+- NSMutableString 继承自 NSString
-- Mac 且 x86平台: 最低有效位是1`1UL`
+通过 `@"**"` 创建的 NSString:
-比如 iOS 平台下
+- 无论字面量长度多长或多短,都是 `__NSCFConstantString` 类型
+ - 无论字面量是中文还是英文,都是 `__NSCFConstantString` 类型
+ - 在创建相同内容的字符串时,得到的内存地址相同
+ - `__NSCFConstantString` 类型的字符串引用计数为-1,对其进行 retain、release 等操作,不会改变其引用计数
+ - `[[NSString alloc] initWithString:@""]` 、`[NSString stringWithString:@"xx"]` 与直接用字面量赋值的结果是一样的,创建的都是 `__NSCFConstantString` 类型的字符串。
-```
-0xb0000000000000a2 b:12 1100
- 1100
-& 1000
--------
- 1000
-```
-tips:某些对象虽然是 TaggedPointer 类型,但是打印 class 发现不是,猜测可能是系统用类簇隐藏了某些实现细节。比如下面
+
-
+通过 `[[NSString alloc] initWithString:@"**"]` 方式创建的 NSString 字符串
-针对 NSNumber 的 TaggedPoniter 的 case,查看 class 打印出 `__NSCFNumber`。但根据源码和内存高地址位分析确实是 TaggedPoniter。
+- 当 `@"**"` 长度为0,是 `__NSCFConstantString` 类型
+- 当 `@"**"` 长度在1~10之间,是 `NSTaggedPointerString` 类型
+- 当 `@"**"` 长度为大于10,是 `__NSCFString` 类型
+- 当字符串长度超过7 Byte 时,NSTaggedPointerString 并没有立即转换为 `__NSCFString`,而是采用了一种压缩算法进行编码,把字符串的长度进行压缩
+- 当压缩算法产生的字符串长度还是大于7 Byte 时,才会将 `NSTaggedPointerString` 转换为 `__NSCFString`
+- 字符的编码格式有很多,其他语言字符不能用标准的 ASCII 码来表示,所以对于中文、日文等非 ASCII 字符,即使只有1个字符,也用 `__NSCFString` 来存储
-疑问是:为什么 NSNumber 的 TaggedPpinter case 下打印 class 是 `__NSCFNumber`。如果是类簇隐藏细节实现,为什么同样 KVO 也改变了 isa,但是命名是一个新的名字,而不是类簇的实现?
+通过 `[[NSMutableString alloc] initWithFormat:@"xx"]` 格式创建的 NSMutableString
-和朋友讨论后有2种观点(观点不是独立的,而是并且同时成立的。对错难以判定,仅供参考):
+- 无论 `@"**"` 为什么值,都是 `__NSCFString` 类型
-- 类簇,为了隐藏细节实现
-- KVO 和当前 case 不一致。类簇是系统类的设计,KVO 是针对开发者写的对象所以没有类簇,只能动态生成类,改变原类的 isa,命名为 `NSKVONotifying_***` 这样的规则。
-
-## 类簇
-
-类簇(Class Cluster )是抽象工厂模式在 OC 数组中的实现,NSArray、NSNumber、NSString、NSDictionary 都有体现。借口简单性和拓展性的权衡体现。系统隐藏了较多实现细节,只暴露出简单接口。
-
-```objectivec
-- (void)classCus
-{
- id obj1 = [NSArray alloc]; // __NSPlaceholderArray
- id obj2 = [NSMutableArray alloc]; // __NSPlaceholderArray
- id obj3 = [obj1 init]; // __NSArray0
- id obj4 = [obj2 init]; // __NSArrayM
- NSLog(@"%@ %@ %@ %@", obj1, obj2, obj3, obj4);
-}
-```
-
-调用 alloc 之后产生的是 `__NSPlaceholderArray` 不符合预期。继续调用 init 发现满足期望了。所以猜测 `__NSPlaceholderArray` 是一个中间对象,后续的 init 方法就是给中间对象发消息,再由它做工厂,生成真的对象,这里的 `__NSArray0`、`__NSArrayM` 对应 NSArray、NSMutableArray
-
-Foundation用了静态实例地址方式来实现,伪代码如下:
-
-```objectivec
-static __NSPlacehodlerArray *GetPlaceholderForNSArray() {
- static __NSPlacehodlerArray *instanceForNSArray;
- if (!instanceForNSArray) {
- instanceForNSArray = [[__NSPlacehodlerArray alloc] init];
- }
- return instanceForNSArray;
-}
-
-static __NSPlacehodlerArray *GetPlaceholderForNSMutableArray() {
- static __NSPlacehodlerArray *instanceForNSMutableArray;
- if (!instanceForNSMutableArray) {
- instanceForNSMutableArray = [[__NSPlacehodlerArray alloc] init];
- }
- return instanceForNSMutableArray;
-}
-// NSArray实现
-+ (id)alloc {
- if (self == [NSArray class]) {
- return GetPlaceholderForNSArray()
- }
-}
-// NSMutableArray实现
-+ (id)alloc {
- if (self == [NSMutableArray class]) {
- return GetPlaceholderForNSMutableArray()
- }
-}
-// __NSPlacehodlerArray实现
-- (id)init {
- if (self == GetPlaceholderForNSArray()) {
- self = [[__NSArrayI alloc] init];
- }
- else if (self == GetPlaceholderForNSMutableArray()) {
- self = [[__NSArrayM alloc] init];
- }
- return self;
-}
-```
-
-另外 iOS Foundation 对静态不可变空对象(当前 case 为数组)做了优化
-
-```objectivec
-NSArray *a1 = [[NSArray alloc] init];
-NSArray *a2 = [[NSArray alloc] init];
-NSArray *a3 = [[NSArray alloc] init];
-(lldb) p a1
-(__NSArray0 *) $0 = 0x0000000109f50a10 @"0 elements"
-(lldb) p a2
-(__NSArray0 *) $1 = 0x0000000109f50a10 @"0 elements"
-(lldb) p a3
-(__NSArray0 *) $2 = 0x0000000109f50a10 @"0 elements"
-(lldb)
-```
-
-若干个不可变的空数组间没有任何特异性,返回一个静态对象。
## OC 对象内存管理
+### 内存管理方法
+
+
+
+#### alloc 实现
+
+经过一系列调用,最终调用了 c 函数的 calloc。此时并没有设置引用计数为1。
+
+#### retain 实现
+
+```c++
+SideTable& table = SideTables()[this];
+size_t& refcntStorage = table.refcnts[this];
+refcntStorage += SIDE_TABLE_RC_ONE;
+```
+
+SIDE_TABLE_RC_ONE 不是1,为什么?
+
+size_t 是64位,其中前2位不是存储引用计数信息的,所以+1,其实就是加偏移量
+
+#### release 实现
+
+```c++
+SideTable& table = SideTables()[this];
+RefcountMap::iterator it = table.refcnts.find[this];
+it->second -= SIDE_TABLE_RC_ONE;
+```
+
+
+
+#### retainCount 实现
+
+```c++
+SideTable& table = SideTables()[this];
+size_t refcnt_result = 1;
+RefcountMap::iterator it = table.refcnts.find(this);
+refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
+```
+
+alloc 后的对象,在引用计数表中是没有对应的 key、value 信息的。由于局部变量 refcnt_result 是1,所以计算完结果就是1。这也是调用 retainCount 返回为1的原因。
+
+
+
+### 引用计数和 getter、setter
+
iOS 中使用引用计数来管理 OC 对象的内存。一个新创建的 OC 对象引用计数默认是1,当引用计数减为 0,OC 对象就会销毁,释放其占用的内存空间
调用 retain/copy 会让 OC 对象的引用计数 +1,调用 release 会让 OC 对象的引用计数 -1。
+
+
+可以看到,如果我们提前将 cat 释放了,那后续赋值给 person 的 _cat 成员变量就没法使用了,因为已经释放了,否则就会造成 `EXC_BAD_ACCESS`。这样子太不灵活了。需要改进下:
+
+调用 setCat 的时候,对传入的 cat 进行 retain,引用计数 +1,谁用谁管理,同样的最后在 Person 对象释放的时候对 cat 进行 release,引用计数 -1.
+
+
+
+但上面的代码不完美,还是存在问题。假设 cat1、cat2 2个对象,当作参数调用2次 setCat 方法,如果 setCat 方法内部不做处理,会导致第2次调用 setCat 后,之前调用时传入的 cat1 会无法释放。
+
+
+
+修改下。调用 setCat 方法时,对之前的 _cat 调用 release,对旧的引用计数-1,再对新传入的对象调用 retain,让引用计数+1,然后赋值
+
+
+
+
+
+上面的代码还是存在问题,会造成僵尸对象问题
+
+
+
+分析下 cat 的引用计数情况:
+
+- 创建后引用计数为1
+- 第一次调用 setCat,由于 _cat 为nil,对 cat 进行 retain 后,引用计数为2
+- 然后调用1次 relase,引用计数为1
+- 再调用 setCat 时,由于 _cat 就是外部的 cat,所以对其调用 `[_cat relase]` 会让引用计数-1,变为0
+- 当引用计数为0的时候,调用 `_cat = [cat retain]` ,Xcode 开启僵尸对象检测,则会 crash
+
+改进
+
+
+
+
+
+
+
内存管理的经验总结
- 当调用 alloc、new、copy、mutableCopy 方法返回了一个对象,在不需要这个对象时,要调用 release 或者 autorelease 来释放它
-- 想拥有某个对象,就让它的引用计数 +1;不想再拥有某个对象,就让它的引用计数 -1
+- 想拥有某个对象,就让它的引用计数 +1;不想再拥有某个对象,就让它的引用计数 -1(谁用谁就 +1,最后要在合适的时机 -1)
-- 可以通过以下私有函数来查看自动释放池的情况`extern void _objc_autoreleasePoolPrint(void);`
+- 可以通过以下私有函数来查看自动释放池的情况 `extern void _objc_autoreleasePoolPrint(void);`
僵尸对象:重复释放内存造成的。一个典型场景是多次 setter。setter 内部实现不合理,比如下面 setter。
```objectivec
+//Person.h
+@interface Person: NSObject {
+ Cat *_cat;
+}
+- (void)setCat:(Cat *)cat;
+@end
+
+// Person.m
+@implementation Person
+- (void)setCat:(Cat *)cat
+{
+ [_cat release];
+ _cat = [cat retain]; // 谁用谁+1,随后在合适的时间 -1
+}
+
+- (void)dealloc {
+ [_cat release]; // -1
+ _cat = nil;
+ [super dealloc];
+}
+@end
+
Person *p = [[Person aloc] init]; // 1
Cat *cat = [[Cat alloc] init]; // 1
[p setCat:cat]; // 2
[cat release]; // 1
[p setCat:cat]; // 0
[p setCat:cat]; // badAccess
-- (void)setCat:(Cat *)cat
-{
- [_cat release];
- _cat = [cat retain];
-}
```
改进
@@ -387,7 +1013,7 @@ Cat *cat = [[Cat alloc] init]; // 1
}
```
-早期在 MRC 时代,在 .h 文件中 @property 只会属性的 getter、setter 声明,`@synthesize` 会自动生成成员变量和属性的 setter、getter 的实现。随着编译器进步,现在 @property 会做完全部的事情。
+早期在 MRC 时代,在 .h 文件中 `@property` 只会属性的 getter、setter 声明,`@synthesize` 会自动生成成员变量和属性的 setter、getter 的实现。随着编译器进步,现在 `@property` 会做完全部的事情。
早期 VC 中使用属性
@@ -412,17 +1038,27 @@ self.dict = dict;
}
```
+
+
QA:ARC 做了什么
ARC 其实是 LLVM + Runtime 共同作用的结果。LLVM 编译器自动插入 retain、release 内存管理代码。Runtime 运行时帮我们处理类似 `__weak` 程序运行过程中弱引用清除掉。
+
+
## copy/mutableCopy
OC 有2个拷贝方法
- copy 不可变拷贝,产生新不可变对象
+ - 针对不可变类型,调用 copy 方法,效果是产生一个新的引用。因为本身不可变,所以一个引用就好,可以实现“产生不可变对象”的目的。
+
+ - 针对可变类型,调用 copy 方法,效果是产生一个新的对象,并且将内容拷贝到新对象里面。产生1个新的不可变对象
- mutableCopy 可变拷贝,产生新可变对象
+ - 针对不可变类型,调用 mutablecopy 方法,需要产生一个可变对象,但是需要互不影响的新的可变对象
+ - 针对可变类型,调用 mutablecopy 方法,需要产生一个新的可变对象。
+
上个 Demo1
@@ -485,7 +1121,13 @@ NSLog(@"array2 --- %zd", array2.retainCount);
| copy | NSString 浅拷贝 | NSString 深拷贝 | NSArray 浅拷贝 | NSArray 深拷贝 | NSDictionary 浅拷贝 | NSDictionary 深拷贝 |
| mutableCopy | NSMutableString 深拷贝 | NSMutableString 深拷贝 | NSMutableArray 深拷贝 | NSMutableArray 深拷贝 | NSMutableDictionary 深拷贝 | NSMutableDictionary 深拷贝 |
-## 引用计数
+深拷贝和浅拷贝的区别?
+- 深拷贝不会影响对的引用计数
+- 深拷贝开辟了新的内存空间
+
+
+
+## 引用计数及weak指针
```objectivec
union isa_t {
@@ -507,11 +1149,15 @@ union isa_t {
iOS 从 64 位开始开始,对 isa 进行了优化,信息存放于 union 结构中
-- `extra_rc` 存储引用计数信息-1,可以看到是 19位。存储引用计数器 -1
+- `extra_rc` 存储着引用计数值 -1 后的值。可以看到是 19位
- `has_sidetable_rc` 引用计数是否过大无法存储在 isa。当过大无法存储与 isa 中时,`has_sidetable_rc` 这位会变为1,引用计数存储在 SideTable 的类的属性中
-也就是说,iOS 从64位开始,引用计数存放于 isa 结构体的一个 union 中,字段为 extra_rc,值为对象引用计数值 -1。当引用计数过大无法存放的时候 union 中 has_sidetable_rc 为 1,则引用计数存放于 SideTable 结构体中。
+也就是说,iOS 从64位开始,引用计数存放于 isa 结构体的一个 union 中,字段为 `extra_rc`,值为对象引用计数值 。当引用计数过大无法存放的时候, union 中 `has_sidetable_rc `为 1,则引用计数存放于 SideTable 结构体中。
+
+
+
+### 散列表
SideTable 结构如下
@@ -523,25 +1169,33 @@ struct SideTable {
};
```
-其中 refcnts 是一个存放着对象引用计数的散列表
+其中 refcnts 是一个存放着对象引用计数的散列表。
-查看 objc4 关于引用计数的实现
-```c
-uintptr_t _objc_rootRetainCount(id obj) {
+
+查看 objc4 源码,看看如何获取引用计数
+
+```c++
+uintptr_t
+_objc_rootRetainCount(id obj)
+{
assert(obj);
+
return obj->rootRetainCount();
}
-inline uintptr_t objc_object::rootRetainCount() {
- if (isTaggedPointer()) return (uintptr_t)this;
+inline uintptr_t
+objc_object::rootRetainCount()
+{
+ if (isTaggedPointer()) return (uintptr_t)this;//如果是采用isTaggedPointer直接返回this本身
+
sidetable_lock();
- isa_t bits = LoadExclusive(&isa.bits);
- ClearExclusive(&isa.bits);
- if (bits.nonpointer) { // 优化过的 isa
- uintptr_t rc = 1 + bits.extra_rc;
- if (bits.has_sidetable_rc) { // 引用计数不是存储在 isa 中,而是 SideTable
- rc += sidetable_getExtraRC_nolock();
+ isa_t bits = LoadExclusive(&isa.bits);//取出isa_t
+ ClearExclusive(&isa.bits);
+ if (bits.nonpointer) { //如果是优化的指针
+ uintptr_t rc = 1 + bits.extra_rc; // 引用计数值
+ if (bits.has_sidetable_rc) {// 如果 has_sidetable_rc 为1,则说明引用计数过大无法存贮在 isa 中,需要去 SideTable 中获取
+ rc += sidetable_getExtraRC_nolock();//去sidetable中去拿取计数
}
sidetable_unlock();
return rc;
@@ -550,16 +1204,40 @@ inline uintptr_t objc_object::rootRetainCount() {
return sidetable_retainCount();
}
-size_t objc_object::sidetable_getExtraRC_nolock() {
+size_t
+objc_object::sidetable_getExtraRC_nolock()
+{
assert(isa.nonpointer);
- SideTable& table = SideTables()[this];
- RefcountMap::iterator it = table.refcnts.find(this); // key 拿值
+ SideTable& table = SideTables()[this]; // SideTables 重载 [] 运算符,本质上就是调用 indexForPointer 方法
+ RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) return 0;
else return it->second >> SIDE_TABLE_RC_SHIFT;
}
+
+//没有优化过的isa去sidetable中拿计数
+uintptr_t
+objc_object::sidetable_retainCount()
+{
+ SideTable& table = SideTables()[this];//根据地址拿到SideTable
+
+ size_t refcnt_result = 1;
+
+ table.lock();
+ RefcountMap::iterator it = table.refcnts.find(this);//从SideTable中根据地址拿取RefcountMap
+ if (it != table.refcnts.end()) {
+ // this is valid for SIDE_TABLE_RC_PINNED too
+ refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
+ }
+ table.unlock();
+ return refcnt_result;//返回RefcountMap中的计数
+}
```
-`__unsafe_unretained` 不安全如何体现?上 Demo
+
+
+`__unsafe_unretained` 不安全
+
+如何体现?上 Demo
```objectivec
__weak Person *p2;
@@ -586,7 +1264,687 @@ NSLog(@"%@", p3);
2022-04-12 21:40:47.559330+0800 Main[5342:99598]
```
-当用 `__unsafe_unretained` 修饰后,虽然释放了,但是内存还没回收,这时候去使用很容易出错。
+当对象用 `__unsafe_unretained` 修饰后,对象虽然被释放了,但是内存还没回收,这时候去使用,很容易出错,报 `EXC_BAD_ACCESS` 。
+
+
+
+### 分离锁
+
+为什么不是一个 SideTable?而是 SideTables
+
+| Ptr(1) | 1 |
+| ------ | ---- |
+| Ptr(2) | 3 |
+| ... | ... |
+| Ptr(N) | 2 |
+
+假设所有的对象和其引用计数信息存在一个 SideTable 中,不同的对象可能在不同的线程中操作,那不同的线程操作一张表需要进行加锁处理,才可以保证数据访问安全。App 运行过程中可能有成千上万的对象,都去访问这个表,下一个对象则需要前一个对象把锁使用完释放后才可以使用,则会存在效率问题。
+
+为了解决这个问题,系统引入了“分离锁”方案。
+
+系统将内存对象对应的引用计数表分拆成多个,在 iOS 真机模式下,SideTable的最大数量是8张(StripeCount=8)。
+
+- 需要对多个这样的表分别加锁。例如,当对象 A 在表 1 中,对象 B 在表 2 中时,A 和 B 的引用计数操作可以并发进行。
+- 这种方式避免了单一锁模型下的顺序操作,提高了多线程环境下的访问效率。
+
+
+
+### 如何实现快速分流?
+
+SideTables 的本质是一张 **Hash 表**。
+
+也就是根据对象地址,如何快速知道,属于哪一张 SideTable?哈希函数。
+
+输入:ptr -> 经过:f(ptr) -> 计算出 index。`f(ptr) = (uintptr_t)ptr % array.count`
+
+使用哈希查找就是为了提高查找效率。
+
+```c++
+template
+class StripedMap {
+ // ...
+#if TARGET_OS_EMBEDDED
+ enum { StripeCount = 8 };
+#else
+ enum { StripeCount = 64 };
+#endif
+ static unsigned int indexForPointer(const void *p) {
+ uintptr_t addr = reinterpret_cast(p);
+ return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
+ }
+}
+```
+
+
+
+SideTable 源码
+
+```c++
+template
+class StripedMap {
+
+ enum { CacheLineSize = 64 };
+
+#if TARGET_OS_EMBEDDED
+ enum { StripeCount = 8 }; // iOS 侧 SideTables 包含8个 SideTable
+#else
+ enum { StripeCount = 64 };
+#endif
+
+ struct PaddedT {
+ T value alignas(CacheLineSize);
+ };
+
+ PaddedT array[StripeCount];
+
+ static unsigned int indexForPointer(const void *p) {
+ uintptr_t addr = reinterpret_cast(p);
+ return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
+ }
+
+ public:
+ T& operator[] (const void *p) { // 重写运算符 [],调用起来更像一个数组。底层调用 indexForPointer 方法。
+ return array[indexForPointer(p)].value;
+ }
+ const T& operator[] (const void *p) const {
+ return const_cast>(this)[p];
+ }
+
+ // Shortcuts for StripedMaps of locks.
+ void lockAll() {
+ for (unsigned int i = 0; i < StripeCount; i++) {
+ array[i].value.lock();
+ }
+ }
+
+ void unlockAll() {
+ for (unsigned int i = 0; i < StripeCount; i++) {
+ array[i].value.unlock();
+ }
+ }
+
+ void forceResetAll() {
+ for (unsigned int i = 0; i < StripeCount; i++) {
+ array[i].value.forceReset();
+ }
+ }
+
+ void defineLockOrder() {
+ for (unsigned int i = 1; i < StripeCount; i++) {
+ lockdebug_lock_precedes_lock(&array[i-1].value, &array[i].value);
+ }
+ }
+
+ void precedeLock(const void *newlock) {
+ // assumes defineLockOrder is also called
+ lockdebug_lock_precedes_lock(&array[StripeCount-1].value, newlock);
+ }
+
+ void succeedLock(const void *oldlock) {
+ // assumes defineLockOrder is also called
+ lockdebug_lock_precedes_lock(oldlock, &array[0].value);
+ }
+
+ const void *getLock(int i) {
+ if (i < StripeCount) return &array[i].value;
+ else return nil;
+ }
+
+#if DEBUG
+ StripedMap() {
+ // Verify alignment expectations.
+ uintptr_t base = (uintptr_t)&array[0].value;
+ uintptr_t delta = (uintptr_t)&array[1].value - base;
+ assert(delta % CacheLineSize == 0);
+ assert(base % CacheLineSize == 0);
+ }
+#endif
+};
+```
+
+- iOS 侧 StripeCount 为8
+- `indexForPointer` 方法根据传入的指针,将指针地址转换为 uintptr_t 类型,然后将地址右移4位和右移9位的结果进行抑或运算,然后将结果取模 StripeCount(iOS 侧为8),用于确定索引的范围(范围在:[0, stripeCount -1] )
+- Operator 重写了运算符 [],底层调用 `indexForPointer` 方法,使之使用起来更像一个数组。
+
+
+
+### 引用计数表
+
+
+
+
+
+### 弱引用表 weak 指针原理
+
+weak_table_t 结构如下:
+
+
+
+```c++
+#define WEAK_INLINE_COUNT 4
+#define REFERRERS_OUT_OF_LINE 2
+
+struct weak_entry_t {
+ DisguisedPtr referent; // 被弱引用的对象
+
+ // 引用该对象的对象列表,联合。 引用个数小于4,用 inline_referrers 数组。 用个数大于4,用动态数组 weak_referrer_t *referrers
+ union {
+ struct {
+ weak_referrer_t *referrers; // 弱引用该对象的对象指针地址的hash数组
+ uintptr_t out_of_line_ness : 2; // 是否使用动态hash数组标记位
+ uintptr_t num_refs : PTR_MINUS_2; // hash数组中的元素个数
+ uintptr_t mask; // hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)素个数)。
+ uintptr_t max_hash_displacement; // 可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)
+ };
+ struct {
+ // out_of_line_ness field is low bits of inline_referrers[1]
+ weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
+ };
+ };
+
+ bool out_of_line() {
+ return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
+ }
+
+ weak_entry_t& operator=(const weak_entry_t& other) {
+ memcpy(this, &other, sizeof(other));
+ return *this;
+ }
+
+ weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
+ : referent(newReferent) // 构造方法,里面初始化了静态数组
+ {
+ inline_referrers[0] = newReferrer;
+ for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
+ inline_referrers[i] = nil;
+ }
+ }
+};
+```
+
+可以看到
+
+- 在 `weak_entry_t ` 的结构中有联合体,在联合体的内部有定长数组 `inline_referrers[WEAK_INLINE_COUNT]` 和动态数组`weak_referrer_t *referrers `两种方式来存储弱引用对象的指针地址。
+- 通过 `out_of_line()` 这样一个函数方法来判断采用哪种存储方式
+ - 当弱引用该对象的指针数目小于等于 `WEAK_INLINE_COUNT`时,使用定长数组
+ - 当超过`WEAK_INLINE_COUNT`时,会将定长数组中的元素转移到动态数组中,并之后都是用动态数组存储
+
+
+
+#### 存 weak 对象
+
+
+
+
+
+可以看到当一个 weak 指针被赋值的时候,底层调用了 `objc_initWeak`,跟踪查看 objc 源码
+
+```c++
+id objc_initWeak(id *location, id newObj) // location: __weak 指针的地址。 newObj:指向对象的地址,即 person
+{
+ if (!newObj) {
+ *location = nil;
+ return nil;
+ }
+
+ return storeWeak
+ (location, (objc_object*)newObj);
+}
+```
+
+继续跟进
+
+```c++
+static id storeWeak(id *location, objc_object *newObj)
+{
+ assert(haveOld || haveNew);
+ if (!haveNew) assert(newObj == nil);
+
+ Class previouslyInitializedClass = nil;
+ id oldObj;
+ SideTable *oldTable;
+ SideTable *newTable;
+
+ // Acquire locks for old and new values.
+ // Order by lock address to prevent lock ordering problems.
+ // Retry if the old value changes underneath us.
+ retry:
+ if (haveOld) { // 如果 weak 指针之前弱引用过一个对象,则将这个对象对应的 SideTable 取出,赋值给 oldTable
+ oldObj = *location;
+ oldTable = &SideTables()[oldObj];
+ } else {
+ oldTable = nil;
+ }
+ if (haveNew) { // 如果 weak 指针,要修饰一个新的对象,则将该对象对应的 SideTable 取出(SideTables 中根据对象地址,进行哈希算法,取出对应的 SideTable),赋值给 newTable
+ newTable = &SideTables()[newObj];
+ } else {
+ newTable = nil;
+ }
+ // 加锁,多线程保护
+ SideTable::lockTwo(oldTable, newTable);
+
+ if (haveOld && *location != oldObj) {
+ SideTable::unlockTwo(oldTable, newTable);
+ goto retry;
+ }
+
+ // Prevent a deadlock between the weak reference machinery
+ // and the +initialize machinery by ensuring that no
+ // weakly-referenced object has an un-+initialized isa.
+ if (haveNew && newObj) {
+ Class cls = newObj->getIsa();
+ if (cls != previouslyInitializedClass &&
+ !((objc_class *)cls)->isInitialized()) // 如果 cla 还没有初始化,则先初始化,再尝试设置 weak
+ {
+ SideTable::unlockTwo(oldTable, newTable);
+ _class_initialize(_class_getNonMetaClass(cls, (id)newObj));
+
+ // If this class is finished with +initialize then we're good.
+ // If this class is still running +initialize on this thread
+ // (i.e. +initialize called storeWeak on an instance of itself)
+ // then we may proceed but it will appear initializing and
+ // not yet initialized to the check above.
+ // Instead set previouslyInitializedClass to recognize it on retry.
+ previouslyInitializedClass = cls; // 记录 previouslyInitializedClass,防止再次进入
+
+ goto retry; // 重新获取一遍 newObj,因为此时已经确保 newObj 初始化过了
+ }
+ }
+
+ // Clean up old value, if any. 如果当前的 weak 指针,修饰过旧的对象,则调用 weak_unregister_no_lock 方法
+ if (haveOld) {
+ weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
+ }
+
+ // Assign new value, if any.
+ if (haveNew) { // 如果 weak 指针,修饰新的对象
+ // 调用 weak_register_no_lock 方法,将 weak 指针地址(location),记录到 newObj 对应的 weak_entry_t 中(weak_referrer_t的结构)
+ newObj = (objc_object *)
+ weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
+ crashIfDeallocating);
+ // weak_register_no_lock returns nil if weak store should be rejected
+
+ // 更新 newObj 的 isa 中的 weakly_referenced bit 标记位
+ // Set is-weakly-referenced bit in refcount table.
+ if (newObj && !newObj->isTaggedPointer()) {
+ newObj->setWeaklyReferenced_nolock();
+ }
+
+ // Do not set *location anywhere else. That would introduce a race.
+ *location = (id)newObj;
+ }
+ else {
+ // No new value. The storage is not changed.
+ }
+ // 多线程解锁
+ SideTable::unlockTwo(oldTable, newTable);
+
+ return (id)newObj; // 返回 newObj,此时 newObj 的 isa union 中,weakly_referenced bit 为1
+}
+```
+
+说明:
+
+- storeWeak 方法实际上接受5个参数,分别是:haveOld、haveNew、crashIfDeallocating、location,、newObj,前3个是以模版的方式传入,是 BOOL 类型。分别表示 weak 指针之前是否修饰过一个弱引用,weak 指针是否需要指向一个新的引用,如果被弱引用的对象正在析构,此时再弱引用是否需要 crash
+- 如果 weak 指针,之前指向过一个弱引用,则调用 `weak_unregister_no_lock` 逻辑,会将旧的 weak 指针地址移除
+- 如果 weak 指针,指向一个新的引用,则调用 `weak_register_no_lock` 将新的 weak 指针地址添加到 weak_table_t 中
+- 最后调用 `setWeaklyReferenced_nolock` 方法,修改对象的 isa union 中的 weak 标记位
+
+其中,看看 `weak_register_no_lock`
+
+```c++
+id
+weak_register_no_lock(weak_table_t *weak_table, id referent_id,
+ id *referrer_id, bool crashIfDeallocating)
+{
+ objc_object *referent = (objc_object *)referent_id;
+ objc_object **referrer = (objc_object **)referrer_id;
+ // 前置判断,如果是 nil 或者是 TaggedPointer 则直接返回(TaggedPointer 仅仅是一个虚假指针,没有在堆上面分配对象,所以也不存在 weak 修饰的问题)
+ if (!referent || referent->isTaggedPointer()) return referent_id;
+ // 确保对象可用(没有在析构,且支持 weak)
+ // ensure that the referenced object is viable
+ bool deallocating;
+ if (!referent->ISA()->hasCustomRR()) {
+ deallocating = referent->rootIsDeallocating();
+ }
+ else {
+ BOOL (*allowsWeakReference)(objc_object *, SEL) =
+ (BOOL(*)(objc_object *, SEL))
+ object_getMethodImplementation((id)referent,
+ SEL_allowsWeakReference);
+ if ((IMP)allowsWeakReference == _objc_msgForward) {
+ return nil;
+ }
+ deallocating =
+ ! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
+ }
+ // 如果在析构函,则报错
+ if (deallocating) {
+ if (crashIfDeallocating) {
+ _objc_fatal("Cannot form weak reference to instance (%p) of "
+ "class %s. It is possible that this object was "
+ "over-released, or is in the process of deallocation.",
+ (void*)referent, object_getClassName((id)referent));
+ } else {
+ return nil;
+ }
+ }
+
+ // now remember it and where it is being stored
+ weak_entry_t *entry;
+ // 根据对象,从 weak_table_t 中找到 weak_entry_t
+ if ((entry = weak_entry_for_referent(weak_table, referent))) {
+ append_referrer(entry, referrer); // 将 referrer 插入到 weak_entry_t 的引用数组中
+ }
+ else {
+ // 如果找不到,则对当前的对象,创建一个 weak_entry_t
+ weak_entry_t new_entry(referent, referrer);
+ // 创建后,判断要不要增长空间
+ weak_grow_maybe(weak_table);
+ // 插入 weak_table_t 中
+ weak_entry_insert(weak_table, &new_entry);
+ }
+
+ // Do not set *referrer. objc_storeWeak() requires that the
+ // value not change.
+
+ return referent_id;
+}
+```
+
+`referent_id ` 是 weak 指针,`*referrer_id` 是 weak 指针地址。
+
+```c++
+static weak_entry_t *
+weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
+{
+ assert(referent);
+
+ weak_entry_t *weak_entries = weak_table->weak_entries;
+
+ if (!weak_entries) return nil;
+
+ size_t begin = hash_pointer(referent) & weak_table->mask; // 这里通过和 mask 按位与的位操作,来确保 index 不会越界
+ size_t index = begin;
+ size_t hash_displacement = 0;
+ while (weak_table->weak_entries[index].referent != referent) {
+ index = (index+1) & weak_table->mask;
+ if (index == begin) bad_weak_table(weak_table->weak_entries); // 触发 bad weak table crash
+ hash_displacement++;
+ if (hash_displacement > weak_table->max_hash_displacement) { // 当 hash 冲突超过了 max hash 冲突时,说明元素不在 hash 表中,返回 nil
+ return nil;
+ }
+ }
+
+ return &weak_table->weak_entries[index];
+}
+```
+
+继续看看 `append_referrer` 方法
+
+```objective-c
+static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
+{
+ if (! entry->out_of_line()) { // weak_entry_t 没有走动态数组,走静态数组
+ // Try to insert inline.
+ for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
+ if (entry->inline_referrers[i] == nil) {
+ entry->inline_referrers[i] = new_referrer;
+ return;
+ }
+ }
+ // 走到这里,说明 inline_referrers 满了,此时创建动态数组 referrers
+ // Couldn't insert inline. Allocate out of line.
+ weak_referrer_t *new_referrers = (weak_referrer_t *)
+ calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
+ // This constructed table is invalid, but grow_refs_and_insert
+ // will fix it and rehash it.
+ // for 循环,填充创建的动态数组
+ for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
+ new_referrers[i] = entry->inline_referrers[i];
+ }
+ entry->referrers = new_referrers;
+ entry->num_refs = WEAK_INLINE_COUNT;
+ entry->out_of_line_ness = REFERRERS_OUT_OF_LINE;
+ entry->mask = WEAK_INLINE_COUNT-1;
+ entry->max_hash_displacement = 0;
+ }
+ // 断言,保护逻辑,走到这里一定是使用了动态数组
+ assert(entry->out_of_line());
+
+ if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {// 拓容。如果数组元素个数大于或等于数组位置空间的3/4,则拓展为当前长度的一倍
+ return grow_refs_and_insert(entry, new_referrer); // 填充,并插入
+ }
+ // 如果没有执行拓容逻辑,则说明空间足够,直接插入到 weak_entry_t 中。weak_entry是一个哈希表,key:w_hash_pointer(new_referrer) value: new_referrer
+
+ size_t begin = w_hash_pointer(new_referrer) & (entry->mask); // 哈希算法,确保 begin 只能小于等于数组的长度
+ size_t index = begin;
+ size_t hash_displacement = 0; // 用于记录 hash 冲突的次数,也就是 hash 再位移的次数
+ while (entry->referrers[index] != nil) {
+ hash_displacement++;
+ index = (index+1) & entry->mask; // // index + 1, 移到下一个位置,再试一次能否插入。(这里要考虑到entry->mask取值,一定是:0x111, 0x1111, 0x11111, ... ,因为数组每次都是*2增长,即8, 16, 32,对应动态数组空间长度-1的mask,也就是前面的取值。
+ if (index == begin) bad_weak_table(entry); // // index == begin 意味着数组绕了一圈都没有找到合适位置,这时候一定是出了什么问题。
+ }
+ // 记录最大的hash冲突次数, max_hash_displacement意味着: 我们尝试至多max_hash_displacement次,肯定能够找到object对应的hash位置
+ if (hash_displacement > entry->max_hash_displacement) {
+ entry->max_hash_displacement = hash_displacement;
+ }
+ // 找到要插入的位置 index,设置引用,然后引用 = 新要加入的 new_referrer ,完成插入
+ weak_referrer_t &ref = entry->referrers[index];
+ ref = new_referrer;
+ // 更新元素个数
+ entry->num_refs++;
+}
+```
+
+逻辑内先判断能否使用定长数组,然后将 weak 指针地址添加到合适的位置。不能则创建动态数组,然后找到要插入的位置进行插入
+
+
+
+如果 weak 指针之前就指向一个弱引用,则会调用 weak_unregister_no_lock 方法,将旧的 weak 指针地址移除。
+
+```c++
+void
+weak_unregister_no_lock(weak_table_t *weak_table, id referent_id,
+ id *referrer_id)
+{
+ // referent_id 对象
+ // referrer_id weak 指针
+ objc_object *referent = (objc_object *)referent_id; // 对象
+ objc_object **referrer = (objc_object **)referrer_id; // weak 指针
+
+ weak_entry_t *entry;
+
+ if (!referent) return;
+
+ // 从 weak_table_t 中找到对象对应的 weak_entry_t
+ if ((entry = weak_entry_for_referent(weak_table, referent))) {
+ // 从 weak_entry_t 中移除 weak 指针
+ remove_referrer(entry, referrer);
+ bool empty = true;
+ if (entry->out_of_line() && entry->num_refs != 0) {
+ empty = false;
+ }
+ else {
+ for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
+ if (entry->inline_referrers[i]) {
+ empty = false;
+ break;
+ }
+ }
+ }
+ // 如果 entry 已经空了,则从 weak_table_t中 移除 weak_entry_t
+ if (empty) {
+ weak_entry_remove(weak_table, entry);
+ }
+ }
+
+ // Do not set *referrer = nil. objc_storeWeak() requires that the
+ // value not change.
+}
+```
+
+总结:
+
+weak 指针修饰步骤:
+
+1. 通过 SideTable 找到 weak_table_t
+2. weak_table_t 根据 referent 找到或创建 weak_entry_t
+3. 然后 append_referrer(entry, referrer) 将新的弱引用的对象加入到 entry 中
+4. 最后调用 weak_entry_insert 把 entry 加入到 weak_table_t 中
+
+
+
+#### 释放 weak 对象
+
+释放就是 dealloc 环节做的事情。
+
+```
+- (void)dealloc {
+ _objc_rootDealloc(self);
+}
+
+void
+_objc_rootDealloc(id obj)
+{
+ assert(obj);
+
+ obj->rootDealloc();
+}
+
+
+inline void
+objc_object::rootDealloc()
+{
+ if (isTaggedPointer()) return; // fixme necessary?
+
+ if (fastpath(isa.nonpointer &&
+ !isa.weakly_referenced &&
+ !isa.has_assoc &&
+ !isa.has_cxx_dtor &&
+ !isa.has_sidetable_rc))
+ {
+ assert(!sidetable_present());
+ free(this);
+ }
+ else {
+ object_dispose((id)this);
+ }
+}
+
+void *objc_destructInstance(id obj)
+{
+ if (obj) {
+ // Read all of the flags at once for performance.
+ bool cxx = obj->hasCxxDtor();
+ bool assoc = obj->hasAssociatedObjects();
+
+ // This order is important.
+ if (cxx) object_cxxDestruct(obj);
+ if (assoc) _object_remove_assocations(obj);
+ obj->clearDeallocating();
+ }
+
+ return obj;
+}
+```
+
+着重看看 `objc_object::clearDeallocating`
+
+```c++
+inline void
+objc_object::clearDeallocating()
+{
+ if (slowpath(!isa.nonpointer)) {
+ // Slow path for raw pointer isa.
+ // 针对 isa 是 union 结构的转型下面逻辑
+ sidetable_clearDeallocating();
+ }
+ else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
+ // Slow path for non-pointer isa with weak refs and/or side table data.
+ clearDeallocating_slow();
+ }
+
+ assert(!sidetable_present());
+}
+
+void
+objc_object::sidetable_clearDeallocating()
+{
+ // StripeMap 重写 [] 运算符,传入对象地址,哈希计算,找到对应的 SideTable
+ SideTable& table = SideTables()[this];
+
+ // clear any weak table items
+ // clear extra retain count and deallocating bit
+ // (fixme warn or abort if extra retain count == 0 ?)
+ table.lock();
+ // 从 SideTable 中找到 refcnts 引用计数信息
+ RefcountMap::iterator it = table.refcnts.find(this);
+ if (it != table.refcnts.end()) {
+ // 找到对象的引用计数信息后,同时清理 weak_table_t
+ if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
+ weak_clear_no_lock(&table.weak_table, (id)this);
+ }
+ table.refcnts.erase(it);
+ }
+ table.unlock();
+}
+
+void
+weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
+{
+ objc_object *referent = (objc_object *)referent_id;
+ // 通过对象找到 weak_table_t 中的 weak_entry_t
+ weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
+ if (entry == nil) {
+ /// XXX shouldn't happen, but does with mismatched CF/objc
+ //printf("XXX no entry for clear deallocating %p\n", referent);
+ return;
+ }
+
+ // zero out references
+ weak_referrer_t *referrers;
+ size_t count;
+ // 判断使用动态数组还是定长数组,来找出 referrers 的数组长度和数组地址
+ if (entry->out_of_line()) {
+ referrers = entry->referrers;
+ count = TABLE_SIZE(entry);
+ }
+ else {
+ referrers = entry->inline_referrers;
+ count = WEAK_INLINE_COUNT;
+ }
+
+ for (size_t i = 0; i < count; ++i) {
+ objc_object **referrer = referrers[i]; // 取出每个 weak 指针地址
+ if (referrer) {
+ if (*referrer == referent) { // 如果 weak 指针,确实引用了 referent 对象,则将 weak 指针设置为 nil
+ *referrer = nil;
+ }
+ else if (*referrer) { // 如果所存储的weak ptr没有weak 引用referent,这可能是由于runtime代码的逻辑错误引起的,报错
+ _objc_inform("__weak variable at %p holds %p instead of %p. "
+ "This is probably incorrect use of "
+ "objc_storeWeak() and objc_loadWeak(). "
+ "Break on objc_weak_error to debug.\n",
+ referrer, (void*)*referrer, (void*)referent);
+ objc_weak_error();
+ }
+ }
+ }
+ // 由于指向该对象的 weak 指针都释放了,所以 weak_table_t 也要移除 weak_entry_t
+ weak_entry_remove(weak_table, entry);
+}
+```
+
+总结:对象释放的时候,调用 `clearDeallocating` 根据对象地址,获取 weak 指针地址的数组,然后遍历,依次设置为 nil,最后从 weak_table_t 中移除 weak_entry_t,完成了对象释放后,所有指向该对象的 weak 指针都被设置为 nil 这个效果
+
+
+
+### 总结
+
+在 OC 中,每个对象对应一个 SideTable,而一个 SideTable 对应多个对象。StrippedMap 是一种数据结构,用于实现高效的并发访问和锁分离。在 StrippedMap 中,有多个 SideTable 实例(iOS 真机,是8个),每个 SideTable 包含一个 `weak_table_t` 和一个 `spinlock_t` ,以实现对弱引用表和引用计数的线程安全访问。这种设计通过锁分离和分区的方式,提高了系统的并发性能,避免了全局锁带来的性能瓶颈,从而实现了高效的对象管理和引用计数处理。(但也有缺点,哈希表越满,哈希冲突会多,性能越差.)
+
+
## dealloc 是如何工作的?
@@ -600,7 +1958,7 @@ NSLog(@"%@", p3);
}
```
-但在 ARC 时代,dealloc 中一般只需要写一些非 OC 对象的内存释放工作,比如 CFRelease()
+但在 ARC 时代,dealloc 中一般只需要写一些非 OC 对象的内存释放工作,比如 `CFRelease()`
带来2个问题:
@@ -608,6 +1966,8 @@ NSLog(@"%@", p3);
- 当前类中没有显示调用 `[super dealloc]` ,父类的析构如何触发?
+
+
### LLVM 文档对 dealloc 的描述
[LLVM ARC 文档对 dealloc 描述](https://clang.llvm.org/docs/AutomaticReferenceCounting.html#dealloc) 如下
@@ -618,10 +1978,12 @@ NSLog(@"%@", p3);
根据描述可以看到 dealloc 方法在最后一次 release 方法调用后触发,但实例变量(ivars) 还未释放,父类的 dealloc 方法将会在子类 dealloc 方法返回后自动调用。
-ARC 模式下,对象的实例变量会在根类 [NSObject dealloc] 中释放,但是释放的顺序是不一定的。
+ARC 模式下,对象的实例变量会在基类 `[NSObject dealloc]` 中释放,但是释放的顺序是不一定的。
也就是说会自动调用 `[super dealloc]`,那到底如何实现的,探究下。
+
+
### 查看 objc4 源码
```c
@@ -629,91 +1991,174 @@ ARC 模式下,对象的实例变量会在根类 [NSObject dealloc] 中释放
_objc_rootDealloc(self);
}
-void _objc_rootDealloc(id obj) {
- assert(obj);
+void
+_objc_rootDealloc(id obj)
+{
+ ASSERT(obj);
+
obj->rootDealloc();
}
-inline void objc_object::rootDealloc() {
+inline void
+objc_object::rootDealloc()
+{
+ // 如果是 Tagged Pointer 指针,也就是一个伪对象,不需要执行堆上内存回收流程,直接 return
if (isTaggedPointer()) return; // fixme necessary?
- // fastpath 判断当前对象是否满足条件。
- if (fastpath(isa.nonpointer && // nonpointer
- !isa.weakly_referenced && // 是否有弱引用
- !isa.has_assoc && // 关联对象
- !isa.has_cxx_dtor && // c++ 析构函数
- !isa.has_sidetable_rc)) // 是否有 SideTable
+
+#if !ISA_HAS_INLINE_RC
+ object_dispose((id)this);
+#else
+ // fastpath 判断当前对象是否满足条件。
+ if (fastpath(isa().nonpointer && // nonpointer:0,普通的 isa 指针,1,代表优化过的 isa 指针,是一个联合体结构
+ !isa().weakly_referenced && // 是否有弱引用
+ !isa().has_assoc && // 是否有关联对象
+#if ISA_HAS_CXX_DTOR_BIT
+ !isa().has_cxx_dtor && // 是否有 c++ 析构函数
+#else
+ !isa().getClass(false)->hasCxxDtor() && // 析构函数
+#endif
+ !isa().has_sidetable_rc)) // 引用计数信息是否存的下,存不下则用 sideTable 存储
{
assert(!sidetable_present());
- free(this);
- } else {
- object_dispose((id)this);
+ free(this); // 一个普通的对象,会执行快速释放逻辑 free
+ }
+ else {
+ object_dispose((id)this); // 执行完整的对象释放流程
}
+#endif // ISA_HAS_INLINE_RC
}
-id object_dispose(id obj){
+id
+object_dispose(id obj)
+{
if (!obj) return nil;
- objc_destructInstance(obj);
+
+ objc_destructInstance(obj);
free(obj);
+
return nil;
}
-void *objc_destructInstance(id obj) {
+void *objc_destructInstance(id obj)
+{
if (obj) {
// Read all of the flags at once for performance.
- bool cxx = obj->hasCxxDtor();
- bool assoc = obj->hasAssociatedObjects();
+ bool cxx = obj->hasCxxDtor(); // 判断有c++析构函数
+ bool assoc = obj->hasAssociatedObjects(); // 判断有关联对象
// This order is important.
- if (cxx) object_cxxDestruct(obj); // 清除成员变量
- if (assoc) _object_remove_assocations(obj);
- obj->clearDeallocating(); // 将指向当前对象的弱指针置为 nil
+ if (cxx) object_cxxDestruct(obj); // 清除成员变量
+ if (assoc) _object_remove_associations(obj, /*deallocating*/true); // 移除关联对象
+ obj->clearDeallocating(); // 将指向当前对象的弱指针置为 nil
}
+
return obj;
}
-inline void objc_object::clearDeallocating() {
- if (slowpath(!isa.nonpointer)) {
+inline void
+objc_object::clearDeallocating()
+{
+ if (slowpath(!isa().nonpointer)) { // nonpointer 为0,则代表是普通的 isa 指针
// Slow path for raw pointer isa.
- sidetable_clearDeallocating();
- }
- else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
+ sidetable_clearDeallocating(); // 普通的 isa 指针执行 sidetable_clearDeallocating 方法
+#if ISA_HAS_INLINE_RC // 编译器定义了 ISA_HAS_INLINE_RC,则会执行慢路径操作,调用 clearDeallocating_slow 方法
+ } else if (slowpath(isa().weakly_referenced || isa().has_sidetable_rc)) {
+#else
+ } else { // 对象具有弱引用或引用计数表数据,也会执行 clearDeallocating_slow 方法
+#endif
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
- assert(!sidetable_present());
+
+ /*
+等价于
+
+ if (slowpath(!isa().nonpointer)) {
+ // Slow path for raw pointer isa.
+ sidetable_clearDeallocating();
+ } else if (slowpath(isa().weakly_referenced || isa().has_sidetable_rc)) {
+ // Slow path for non-pointer isa with weak refs and/or side table data.
+ clearDeallocating_slow();
+ }
+
+ if (slowpath(!isa().nonpointer)) {
+ // Slow path for raw pointer isa.
+ sidetable_clearDeallocating();
+ } else {
+ // Slow path for non-pointer isa with weak refs and/or side table data.
+ clearDeallocating_slow();
+ }
+*/
+
+ assert(!sidetable_present()); // 因为走完了指向对象的弱指针置为 nil 的逻辑,所以断言判断不存在引用计数表
}
-void objc_object::sidetable_clearDeallocating(){
- SideTable& table = SideTables()[this];
+
+
+void
+objc_object::sidetable_clearDeallocating()
+{
+ SideTable& table = SideTables()[this]; // 根据对象的地址获取 SideTab(refcnts、weak_table)。用于管理对象的引用计数和若引用信息
+
// clear any weak table items
// clear extra retain count and deallocating bit
// (fixme warn or abort if extra retain count == 0 ?)
- table.lock();
- RefcountMap::iterator it = table.refcnts.find(this);
- if (it != table.refcnts.end()) {
- if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
- weak_clear_no_lock(&table.weak_table, (id)this);
+ table.lock(); // 多线程环境下加锁
+ RefcountMap::iterator it = table.refcnts.find(this); // 根据对象的地址,查找对象的引用计数信息
+ if (it != table.refcnts.end()) { // 如果找到了对象的引用计数信息
+ if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) { // 如果对象被弱引用指向
+ weak_clear_no_lock(&table.weak_table, (id)this); // 则执行 weak_clear_no_lock 方法清除与该对象相关的弱引用
}
- table.refcnts.erase(it);
+ table.refcnts.erase(it); // 然后从 refcnts 中移除当前对象的引用计数信息,表示该对象即将被释放
}
- table.unlock();
+ table.unlock(); // 解锁
}
+NEVER_INLINE void
+objc_object::clearDeallocating_slow()
+{
+ // 断言,判断进该方法的,符合前面的 if 条件。nonpointer 普通 isa 指针,且存在弱引用计数信息,才执行下面流程
+ ASSERT(isa().nonpointer && (isa().weakly_referenced
+#if ISA_HAS_INLINE_RC
+ || isa().has_sidetable_rc
+#endif
+ ));
+ // 根据对象地址获取 SideTable
+ SideTable& table = SideTables()[this];
+ // 加锁
+ table.lock();
+ // isa 中 weakly_referenced 为真,则执行 weak_clear_no_lock 清除与当前对象有关的引用计数信息。
+ if (isa().weakly_referenced) {
+ weak_clear_no_lock(&table.weak_table, (id)this);
+ }
+#if ISA_HAS_INLINE_RC
+ if (isa().has_sidetable_rc) {
+#endif
+ table.refcnts.erase(this); // 如果对象有引用计数表数据,则从 refcnts 中移除当前对象的引用计数信息
+#if ISA_HAS_INLINE_RC
+ }
+#endif
+ table.unlock(); // 解锁
+}
+
+
void
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
+ // 根据传入对象的指针 referent_id 转换为 objc_object 类型的指针 referent
objc_object *referent = (objc_object *)referent_id;
-
+ // 调用 weak_entry_for_referent 方法查找指向该对象的弱引用条目
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
+ // 如果未找到对应的条目,则可能出现异常,则打印一条警告信息后 return
if (entry == nil) {
/// XXX shouldn't happen, but does with mismatched CF/objc
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}
-
+
// zero out references
weak_referrer_t *referrers;
size_t count;
-
+ // 根据弱引用条目的类型(是否超出内联存储)判断需要处理的弱引用数组和数量
if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
@@ -722,24 +2167,24 @@ weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
-
+ // 遍历数组
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
+ // 如果弱引用指针指向的对象和当前对象 referent 相同,则将该弱引用指针设为 nil,表示对象已经释放
if (*referrer == referent) {
*referrer = nil;
}
+ // 如果不同则可能存在错误,报告错误信息
else if (*referrer) {
- _objc_inform("__weak variable at %p holds %p instead of %p. "
- "This is probably incorrect use of "
- "objc_storeWeak() and objc_loadWeak(). "
- "Break on objc_weak_error to debug.\n",
- referrer, (void*)*referrer, (void*)referent);
- objc_weak_error();
+ REPORT_WEAK_ERROR("__weak variable at %p holds %p instead of %p. "
+ "This is probably incorrect use of "
+ "objc_storeWeak() and objc_loadWeak().",
+ referrer, (void*)*referrer, (void*)referent);
}
}
}
-
+ // 调用该方法从弱引用表中移除该弱引用,完成对象的弱引用清除
weak_entry_remove(weak_table, entry);
}
```
@@ -754,11 +2199,13 @@ weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
继续看看 object_cxxDestruct 方法内部细节。
+
+
### 神秘的 cxx_destruct
`object_cxxDestruct` 方法最终会调用到 `object_cxxDestructFromClass`
-```c
+```c++
void object_cxxDestruct(id obj) {
if (_objc_isTaggedPointerOrNil(obj)) return;
object_cxxDestructFromClass(obj, obj->ISA());
@@ -806,6 +2253,8 @@ void sel_init(size_t selrefCount){
也就是说,当编译器看到 C++ 对象的时候,它将会生成 `.cxx_destruct` 析构方法,但是 ARC 借用这个方法,并在其中插入了代码以实现自动内存释放的功能。
+
+
### 探究啥时候生成 .cxx_destruct 方法
```objectivec
@@ -826,11 +2275,11 @@ void sel_init(size_t selrefCount){
在 gone 处加断点,利用 runtime 查看类中的方法信息
-
+
发现存在 `.cxx_destruct` 方法。
-我们一开要研究的是 ivars 啥时候释放,所以控制变量,将属性改为成员对象
+我们一开始要研究的是 ivars 啥时候释放,所以控制变量,将属性改为成员对象
```objectivec
@interface Person : NSObject
@@ -861,7 +2310,7 @@ void sel_init(size_t selrefCount){
@end
```
-
+
Tips:@property 会自动生成成员变量,另外类后面加 `{}` 在内部也可以加成员变量,假如成员变量是对象类型,比如 NSString,则叫实例变量。
@@ -875,7 +2324,9 @@ Tips:@property 会自动生成成员变量,另外类后面加 `{}` 在内部
在 gone 的地方加断点,输入 `watchpoint set variable p->_name`,则会将 `_name` 实例变量加入 watchpoint,当变量被修改时会触发断点,可以看出从某个值变为 0x0,也就是 nil。此时边上调用堆栈显示在 `objc_storestrong` 方法中,被设置为 nil.
-
+
+
+
### 深入 .cxx_destruct
@@ -979,6 +2430,8 @@ id objc_storeStrong(id *object, id value) {
在 `.cxx_destruct` 方法内部会对所有的实例变量调用 `objc_storeStrong(&ivar, null)` ,实例变量就会 release 。
+
+
### 自动调用 [super dealloc] 的原理
同理,CodeGen 也会做自动调用 `[super dealloc]` 的事情。https://clang.llvm.org/doxygen/CGObjC_8cpp_source.html,第751行 `StartObjCMethod` 方法。
@@ -1037,9 +2490,108 @@ struct FinishARCDealloc : EHScopeStack::Cleanup {
- ARC 模式下 `[super dealloc]` 由 llvm 编译器自动插入(CodeGen)
+
+
+### ARC 帮我们做了什么?
+
+#### LLVM + Runtime 共同协作的结果
+
+LLVM 编译器前端 clang 在编译阶段,自动帮我们给对象加了 release、retain、autorelease 的代码(比如在一个大括号内的代码,生命的对象,在大括号将要结束的时候会自动加 `[person release] 之类的代码`)。
+
+ARC 中禁止手动调用 retain/release/retainCount/dealloc 方法。
+
+ARC 中新增 weak、strong 属性关键字。
+
+弱引用这样的情况,需要借助于 Runtime 实现。在对象将要销毁的时候,执行 dealloc 方法,判断对象存在 c++ 析构函数、关联对象,则执行进一步的处理,清除成员变量、关联对象。内部借助于 Runtime 能力,根据 isa 找到对象的 SideTable(weak_table、refcnts),清除所有指向对象的弱应引用指针。
+
+
+
+#### 编译器对 Method Family 的处理
+
+一个方法生成的对象,没有任何附加标识,ARC 如何知道生成的对象是不是 `autorelease` ?
+
+```objective-c
+@interface Person: NSObject
+- (instancetype)initWithName:(NSString *)name;
++ (instancetype)personWithName:(NSString *)name;
+@end
+
+Person *person1 = [[Person alloc] initWithName:@"FantasticLBP"];
+Person *person2 = [Person personWithName:@"FantasticLBP"];
+```
+
+使用约定。NS 定义了下面3个编译属性:
+
+```objective-c
+#define NS_RETURNS_RETAINED __attribute__((ns_returns_retained))
+#define NS_RETURNS_NOT_RETAINED __attribute__((ns_returns_not_retained))
+#define NS_RETURNS_INNER_POINTER __attribute__((objc_returns_inner_pointer))
+```
+
+这3个属性是 Clang 自己使用的标识,除非特殊情况,不要自己使用。
+
+- `NS_RETURNS_RETAINED`:init 和 initWithName 都属于 init 家族方法。对于以 alloc、init、copy、mutableCopy、new 开头的家族方法,后面默认加 `NS_RETURNS_RETAINED`,ARC 会在调用方法外围加上内存管理代码,retain + release
+- `NS_RETURNS_NOT_RETAINED`: `personWithName` 方法,则是不带 alloc、init、copy、mutableCopy、new 开头的方法,默认添加 `NS_RETURNS_NOT_RETAINED` 标识,表明返回的对象已经在方法内加过 autorelease 了
+- `NS_RETURNS_INNER_POINTER`:这个只用做返回 c 语言指针变量,ARC 外围不做内存管理的操作。如 `- (__strong const char *)UTF8String NS_RETURNS_INNER_POINTER;`
+
+
+
+上面提到了[Method families](https://clang.llvm.org/docs/AutomaticReferenceCounting.html#id37)
+
+> An Objective-C method may fall into a **method family**, which is a conventional set of behaviors ascribed to it by the Cocoa conventions.
+
+在 OC 中,方法可以被归类到方法族(method family)中,这是由 Cocoa 约定赋予方法的一组传统行为。方法族是一种命名约定,用于指示方法的特定行为和语义。举例来说,alloc、init、copy、mutableCopy、new 等方法族在 OC 中具有特殊的内存管理行为和语义,这些方法族在 Cocoa 框架中有着重要的作用,帮助开发者遵循内存管理规则和约定,确保代码的可靠性和性能。
+
+这些方法族的存在使得 OC 代码更易于理解和遵循,同时也有助于保持代码的一致性和可维护性。通过遵循 Cocoa 的方法族约定,开发者可以更好地利用 OC 的动态、面向对象的特性,编写出清晰、高效的代码。
+
+
+
+上面针对 Person 的代码,编译器看起来是这样的
+
+```objective-c
+@interface Person: NSObject
+- (instancetype)initWithName:(NSString *)name NS_RETURNS_RETAINED;
++ (instancetype)personWithName:(NSString *)name NS_RETURNS_NOT_RETAINED;
+@end
+```
+
+这也是为什么不能在 ARC 下,将属性命名以 new 开头的原因,`@property (nonatomic, copy) NSString *newString;` 编译器会报错。`newString` 的 getter 方法会被编译器看成是 new 家族方法,会在外围加入内存管理代码 retain + release,从而导致内存管理错误。
+
+
+
+### ARC 中显式或隐式调用对于引用计数的影响
+
+约定 `[target selector]` 的方式为显示,其他都是隐式。
+
+
+
+隐式调用工厂方法
+
+
+
+
+
+隐式调用的时候没有对 person 进行显式的赋值,而是传入 `getReturnValue: `方法中去获取返回值,这样的赋值后 ARC 没有自动给这个变量插入 retain 语句,但退出作用域时还是自动插入了release 语句,导致这个变量多释放了一次,导致 crash
+
+
+
+如何修改?加一个 bridge 即可。
+
+
+
+由于 ARC 没有加 retain。所以 `person = (__bridge id)result;` 这里完成了对象的 retain。ARC 在退出方法的作用域时给对象加上release。前后对应,内存正确。
+
+
+
+
+
+
+
+
+
## AutoreleasePool 底层原理探索
-### 单 AutoreleasePool 的 case
+### AutoreleasePool 结构探究
```objectivec
int main(int argc, const char * argv[]) {
@@ -1052,7 +2604,7 @@ int main(int argc, const char * argv[]) {
clang 转为 c++ `xcrun -sdk iphonesimulator clang -rewrite-objc main.m`
-```c
+```c++
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
@@ -1065,7 +2617,7 @@ int main(int argc, const char * argv[]) {
继续查找
-```c
+```c++
struct __AtAutoreleasePool {
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
@@ -1085,7 +2637,7 @@ OC 对象本质就是结构体
main 内的代码作用域,离开代表销毁。所以上面代码等价于
-```objectivec
+```objective-c
atautoreleasepoolobj = objc_autoreleasePoolPush();
Person *p = [[[Person alloc] init] autorelease];
objc_autoreleasePoolPop(atautoreleasepoolobj);
@@ -1093,11 +2645,24 @@ objc_autoreleasePoolPop(atautoreleasepoolobj);
利用关键函数继续查看 objc4 源码
-```c
-void *objc_autoreleasePoolPush(void) {
+```c++
+void *_objc_autoreleasePoolPush(void)
+{
+ return objc_autoreleasePoolPush();
+}
+
+void _objc_autoreleasePoolPop(void *ctxt)
+{
+ objc_autoreleasePoolPop(ctxt);
+}
+
+void *objc_autoreleasePoolPush(void)
+{
return AutoreleasePoolPage::push();
}
-void objc_autoreleasePoolPop(void *ctxt) {
+
+NEVER_INLINEvoid objc_autoreleasePoolPop(void *ctxt)
+{
AutoreleasePoolPage::pop(ctxt);
}
```
@@ -1106,9 +2671,9 @@ void objc_autoreleasePoolPop(void *ctxt) {
**objc_autoreleasePoolPush、objc_autoreleasePoolPop 底层都是调用了 AutoreleasePoolPage 对象来管理的。**
-查看源码
+查看源码 `NSObject-internal.h` 后对 `AutoreleasePoolPageData` 剔出无用成员后,关键信息如下
-```c
+```c++
class AutoreleasePoolPage {
magic_t const magic;
id *next;
@@ -1120,10 +2685,10 @@ class AutoreleasePoolPage {
}
```
-- 每个 AutoreleasePoolPage 对象占用 4096 字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放 autorelease 对象的地址
-- 所有的 AutoreleasePoolPage 对象通过**双向链表**的形式连接在一起。child 指向下一个对象,parent 指向上一个对象
+- 每个 AutoreleasePoolPage 对象占用 4096 (16的3次方,0x2000)字节内存,除了用来存放它内部的成员变量(内部成员固定有7个,56个字节,即 `0x18`, `0x1000 + 0x38 = 0x1038` ),剩下的空间用来存放 autorelease 对象的地址
+- 所有的 AutoreleasePoolPage 对象通过**双向链表**的形式连接在一起。child 指向下一个 AutoreleasePoolPage 对象,parent 指向上一个 AutoreleasePoolPage 对象
-
+
```objectivec
id * begin() {
@@ -1133,32 +2698,339 @@ id * begin() {
id * end() {
return (id *) ((uint8_t *)this+SIZE);
}
+
+ static size_t const SIZE =
+#if PROTECT_AUTORELEASEPOOL
+ PAGE_MAX_SIZE; // must be multiple of vm page size
+#else
+ PAGE_MIN_SIZE; // size and alignment, power of 2
+#endif
```
-其中 begin 方法返回 autoreleasePoolPage 对象中开始存储 autorelease 对象的开始地址
+分析:
-end 方法返回 autoreleasePoolPage 对象中结束存储 autorelease 对象的开始地址
+- begin 方法返回 autoreleasePoolPage 对象中开始存储 autorelease 对象的开始地址,也就是4049个字节中,扣除最开始存放 AutoreleasePoolPage 对象固定空间外,可以存 autorelease 对象的位置。该怎么算?`可以存放 autorelease 对象的开始地址 = 自己对象的地址 + 偏移量 = 自己对象的地址 + 自己对象的所占空间 = (uint8_t *)this + sizeof(*this)`
-调用 `AutoreleasePoolPage::push` 方法会将一个 `POOL_BOUNDARY `入栈,并且返回其存放的内存地址
+- end 方法返回 autoreleasePoolPage 对象中结束存储 autorelease 对象的开始地址。该怎么算?`end 地址 = 自己对象的开始地址 + 4096字节`。其中 `SIZE` 是一个宏计算结果,也就是 4096。
-调用 `AutoreleasePoolPage::pop` 方法时传入一个 `POOL_BOUNDARY` 的内存地址,系统会从最后一个入栈的对象开始发送 release消 息,直到遇到这个 `POOL_BOUNDARY`
-`id *next` 指向了下一个能存放 autorelease 对象地址的区域
-```c
+#### 源码分析
+
+1.源码分析 push 方法
+
+```c++
static inline void *push() {
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page.
- dest = autoreleaseNewPage(POOL_BOUNDARY);
+ dest = autoreleaseNewPage(POOL_BOUNDARY); // 没有 autoreleasepool 则执行 autoreleaseNewPage 方法,并且传入一个 POOL_BOUNDARY
} else {
- dest = autoreleaseFast(POOL_BOUNDARY);
+ dest = autoreleaseFast(POOL_BOUNDARY); // 如果有 autoreleasepool,执行 push 也会放入一个 POOL_BOUNDARY
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
+
+static __attribute__((noinline))
+id *autoreleaseNewPage(id obj)
+{
+ AutoreleasePoolPage *page = hotPage(); // 通过 hotPage 方法创建一个 AutoreleasePoolPage 对象
+ if (page) return autoreleaseFullPage(obj, page); // 调用 autoreleaseFullPage 方法,传入新创建的 page 和外部传入的 POOL_BOUNDARY
+ else return autoreleaseNoPage(obj);
+}
+
+static __attribute__((noinline))
+id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
+{
+ // The hot page is full.
+ // Step to the next non-full page, adding a new page if necessary.
+ // Then add the object to that page.
+ ASSERT(page == hotPage());
+ ASSERT(page->full() || DebugPoolAllocation);
+
+ do {
+ if (page->child) page = page->child;
+ else page = new AutoreleasePoolPage(page);
+ } while (page->full());
+
+ setHotPage(page);
+
+ // dtrace probe
+ OBJC_RUNTIME_AUTORELEASE_POOL_GROW(page->depth);
+
+ return page->add(obj); // 将首次创建的 AutoreleasePoolPage 对象中,添加一个外部传入的 POOL_BOUNDARY
+}
+
```
+结论:**调用 `AutoreleasePoolPage::push` 方法会将一个 `POOL_BOUNDARY `入栈,并且返回其存放的内存地址**
+
+
+
+2.源码分析 pop 方法
+
+```c++
+class AutoreleasePoolPage: private AutoreleasePoolPageData {
+ // ...
+ static inline void
+ pop(void *token)
+ {
+ // dtrace probe
+ OBJC_RUNTIME_AUTORELEASE_POOL_POP(token);
+
+ // We may have an object in the ReturnAutorelease TLS when the pool is
+ // otherwise empty. Release that first before checking for an empty pool
+ // so we don't return prematurely. Loop in case the release placed a new
+ // object in the TLS.
+ while (releaseReturnAutoreleaseTLS())
+ ;
+
+ AutoreleasePoolPage *page;
+ id *stop;
+ if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
+ // Popping the top-level placeholder pool.
+ page = hotPage();
+ if (!page) {
+ // Pool was never used. Clear the placeholder.
+ return setHotPage(nil);
+ }
+ // Pool was used. Pop its contents normally.
+ // Pool pages remain allocated for re-use as usual.
+ page = coldPage();
+ token = page->begin();
+ } else {
+ page = pageForPointer(token);
+ }
+
+ stop = (id *)token;
+ if (*stop != POOL_BOUNDARY) {
+ if (stop == page->begin() && !page->parent) {
+ // Start of coldest page may correctly not be POOL_BOUNDARY:
+ // 1. top-level pool is popped, leaving the cold page in place
+ // 2. an object is autoreleased with no pool
+ } else {
+ // Error. For bincompat purposes this is not
+ // fatal in executables built with old SDKs.
+ return badPop(token);
+ }
+ }
+
+ if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
+ return popPageDebug(token, page, stop);
+ }
+
+ return popPage(token, page, stop);
+ }
+}
+
+template
+static void
+popPage(void *token, AutoreleasePoolPage *page, id *stop)
+{
+ if (allowDebug && PrintPoolHiwat) printHiwat();
+
+ page->releaseUntil(stop);
+
+ // memory: delete empty children
+ if (allowDebug && DebugPoolAllocation && page->empty()) {
+ // special case: delete everything during page-per-pool debugging
+ AutoreleasePoolPage *parent = page->parent;
+ page->kill();
+ setHotPage(parent);
+ } else if (allowDebug && DebugMissingPools && page->empty() && !page->parent) {
+ // special case: delete everything for pop(top)
+ // when debugging missing autorelease pools
+ page->kill();
+ setHotPage(nil);
+ } else if (page->child) {
+ // hysteresis: keep one empty child if page is more than half full
+ if (page->lessThanHalfFull()) {
+ page->child->kill();
+ }
+ else if (page->child->child) {
+ page->child->child->kill();
+ }
+ }
+}
+
+void releaseUntil(id *stop)
+ {
+ // Not recursive: we don't want to blow out the stack
+ // if a thread accumulates a stupendous amount of garbage
+
+ do {
+ while (this->next != stop) {
+ // Restart from hotPage() every time, in case -release
+ // autoreleased more objects
+ AutoreleasePoolPage *page = hotPage();
+
+ // fixme I think this `while` can be `if`, but I can't prove it
+ while (page->empty()) {
+ page = page->parent;
+ setHotPage(page);
+ }
+
+ page->unprotect();
+#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
+ AutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next;
+
+ // create an obj with the zeroed out top byte and release that
+ id obj = (id)entry->getPointer();
+ int count = (int)entry->getCount(); // grab these before memset
+#else
+ id obj = *--page->next; // 跨 AutoreleasePoolPage release 对象
+#endif
+ memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
+ page->protect();
+
+ if (obj != POOL_BOUNDARY) {
+#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
+ // release count+1 times since it is count of the additional
+ // autoreleases beyond the first one
+ for (int i = 0; i < count + 1; i++) {
+ objc_release(obj);
+ }
+#else
+ objc_release(obj); // for 循环,对每个对象调用 objc_release 方法
+#endif
+ }
+ }
+
+ // Stale return autorelease info is conceptually autoreleased. If
+ // there is any, release the object in the info. If stale info is
+ // present, we have to loop in case it autoreleased more objects
+ // when it was released.
+ } while (releaseReturnAutoreleaseTLS());
+
+ setHotPage(this);
+
+#if DEBUG
+ // we expect any children to be completely empty
+ for (AutoreleasePoolPage *page = child; page; page = page->child) {
+ ASSERT(page->empty());
+ }
+#endif
+ }
+```
+
+系统在 autoreleasepool 执行析构函数的时候,会调用 pop 方法,pop 方法传入一个创建时候的地址,也就是 `POOL_BOUNDARY` 的地址,... 最后调用 `releaseUntil` 方法,for 循环不断遍历(当前对象到 `POOL_BOUNDARY` 地址之间)对象,调用 `objc_release` 方法释放对象。并向回移动`next`指针到正确位置。
+
+在移除的过程中,从最新加入的对象一直向前清理,期间可以向前跨越若干个page,直到哨兵所在的page
+
+结论:**调用 `AutoreleasePoolPage::pop` 方法时传入一个 `POOL_BOUNDARY` 的内存地址,系统会从最后一个入栈的对象开始发送 `release` 消 息,不断调用,直到遇到这个 `POOL_BOUNDARY` 地址为止**。
+
+
+
+- `next` 方法指向了下一个能存放 autorelease 对象地址的区域(感兴趣的可以查看源码)
+
+
+
+看看系统源码,OC 对象调用 autorelease 方法做了什么?
+
+```c++
+// NSObject.mm
+// Replaced by ObjectAlloc
+- (id)autorelease {
+ return _objc_rootAutorelease(self);
+}
+
+NEVER_INLINE id
+_objc_rootAutorelease(id obj)
+{
+ ASSERT(obj);
+ return obj->rootAutorelease();
+}
+
+// Base autorelease implementation, ignoring overrides.
+ALWAYS_INLINE id
+objc_object::rootAutorelease()
+{
+ if (isTaggedPointer()) return (id)this;
+ bool nonpointerIsa = false;
+#if ISA_HAS_INLINE_RC
+ nonpointerIsa = isa().nonpointer;
+
+ // When we can cheaply determine if the object is deallocating, avoid
+ // putting it in the pool. Refcounting doesn't work on a deallocating object
+ // so it's pointless to put it in the pool, and potentially dangerous.
+ if (nonpointerIsa && isa().isDeallocating()) return (id)this;
+#endif
+
+ // If the class has custom dealloc initiation, we also want to avoid putting
+ // deallocating instances in the pool even if it's expensive to check. (UIView
+ // and UIViewController need this. rdar://97186669)
+ if (!nonpointerIsa && ISA()->hasCustomDeallocInitiation() && rootIsDeallocating())
+ return (id)this;
+
+ if (prepareOptimizedReturn((id)this, true, ReturnAtPlus1)) return (id)this;
+ if (slowpath(isClass())) return (id)this;
+
+ return rootAutorelease2();
+}
+
+
+__attribute__((noinline,used))
+id
+objc_object::rootAutorelease2()
+{
+ ASSERT(!isTaggedPointer());
+ return AutoreleasePoolPage::autorelease((id)this);
+}
+
+class AutoreleasePoolPage : private AutoreleasePoolPageData {
+ //...
+ static inline id *autoreleaseFast(id obj){
+ AutoreleasePoolPage *page = hotPage();
+ if (page && !page->full()) {
+ return page->add(obj); // 有 autoreleasepage 且没有满
+ } else if (page) { // 有 page 但满了
+ return autoreleaseFullPage(obj, page);
+ } else { // 没有 page,则去创建 page
+ return autoreleaseNoPage(obj);
+ }
+ }
+
+ static inline id autorelease(id obj) {
+ ASSERT(!_objc_isTaggedPointerOrNil(obj));
+ id *dest __unused = autoreleaseFast(obj);
+ #if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
+ ASSERT(!dest || dest == (id *)EMPTY_POOL_PLACEHOLDER || (id)((AutoreleasePoolEntry *)dest)->getPointer() == obj);
+ #else
+ ASSERT(!dest || dest == (id *)EMPTY_POOL_PLACEHOLDER || *dest == obj);
+ #endif
+ return obj;
+ }
+}
+```
+
+查看源码发现,对象调用 `autorelease ` 方法,会调用 `_objc_rootAutorelease` 方法。内部会继续调用 `rootAutorelease` 方法,底层实现会调用 `rootAutorelease2` 方法,其会调用 `AutoreleasePoolPage::autorelease` 方法,其会调用 `autoreleaseFast` 方法,其会调用 `page->add` 方法,将对象添加到自动释放池中。
+
+结论:**对象调用 `autorelease()` 方法的本质是将该对象加入到自动释放池中**
+
+
+
+### 单 AutoreleasePool 的 case
+
+举个例子,for 循环创建1000个 Person 对象,用 autorelease 修饰,如何工作?
+
+
+
+分析:
+
+1. 首先创建第一个 AutoreleasePoolPage 对象,假设地址为 `0x1000`,由于一个 Auto releasePoolPage 内有固定成员变量,占用56个字节。AutoreleasePoolPage 对象的 `begin` 方法,获取可以存储被 autorelease 修饰的对象地址。
+2. 一个 person 指针占用8个字节,循环 1000 次,创建了1000个 person 对象,所以需要 8000 字节空间存储 autorelease 对象
+3. 最开始,先插入一个 `POOL_BOUNDARY` 对象
+4. 然后开始插入第一个 person 对象,插入到 `POOL_BOUNDARY` 紧挨着的位置,也就是 `0x1040` 处,占用8个字节
+5. 第三个 person 对象被插入到 `0x1048` 处,依次类推...
+6. 但第一个 AutoreleasePoolPage 存不下1000个 person 指针,所以创建了第二个 AutoreleasePoolPage 对象
+7. 同样按照上面的逻辑,一个 person 对象占8个字节,将剩余的 person 对象插满
+8. 最后 for 循环结束,也就是 `atautoreleasepoolobj` 对象将要释放了,本质上就是执行 ` objc_autoreleasePoolPop(atautoreleasepoolobj)`
+9. 释放的过程和插入的过程刚好相反,从最后一个 person 对象开始,执行 `release` 方法
+10. 直到遇到传入的 `atautoreleasepoolobj` 为止(`atautoreleasepoolobj` 地址,其实就是传入的 `POOL_BOUNDARY` 内存地址)
+
+
+
### 多 AutoreleasePool 的 case
来个骚一些的例子
@@ -1181,17 +3053,42 @@ int main(int argc, const char * argv[]) {
main 方法内部3个 autoreleasepool 底层怎么样工作的?
-
+
-3个@auto releasepool, 系统遇到第一个的时候底层就是初始化一个结构体 `__AtAutoreleasePool`,结构体构造方法内部调用 `AutoreleasePoolPage::push` 方法,系统给 AutoreleasePoolPage 真正保存 autorelease 对象的地方存储进一个 `POOL_BOUNDARY` 对象,然后储存 P1、P2 对象地址,遇到第二个则继续初始化结构体,调用 push 方法,存储一个` POOL_BOUNDARY` 对象,继续保存 P3,遇到第三个则继续初始化结构体,调用 push 方法,存储一个 `POOL_BOUNDARY` 对象,继续保存 P4。
+分析:
-当结束第三个大括号的时候,第三个结构体对象,调用析构函数,内部调用 `AutoreleasePoolPage::pop` 方法,会从最后一个入栈的对象开始发送 release 消息,直到遇到 `POOL_BOUNDARY` 对象。
+上面代码等价于
-紧接着第二个大括号结束,第二个结构体对象析构函数执行,内部调用 `AutoreleasePoolPage::pop` 方法,会从最后一个入栈的对象开始发送 release 消息,直到遇到 `POOL_BOUNDARY` 对象。
+```c++
+atautoreleasepoolobj1 = objc_autoreleasePoolPush();
+Person *p1 = [[[Person alloc] init] autorelease];
+Person *p2 = [[[Person alloc] init] autorelease];
+objc_autoreleasePoolPop(atautoreleasepoolobj1);
+
+atautoreleasepoolobj2 = objc_autoreleasePoolPush();
+Person *p3 = [[[Person alloc] init] autorelease];
+objc_autoreleasePoolPop(atautoreleasepoolobj2);
+
+atautoreleasepoolobj3 = objc_autoreleasePoolPush();
+Person *p4 = [[[Person alloc] init] autorelease];
+objc_autoreleasePoolPop(atautoreleasepoolobj3);
+```
+
+- 共 3 个@auto releasepool,系统遇到一个 `@autoreleasepool{}` 的时候,底层就是初始化一个结构体 `__AtAutoreleasePool`,结构体构造方法内部调用 `AutoreleasePoolPage::push` 方法、析构函数调用 `AutoreleasePoolPage::pop` 方法
+- 插入阶段:
+ - 遇到第一个 `@autoreleasepool{}` ,则创建第一个 AutoreleasePoolPage 对象 atautoreleasepoolobj1,构造方法调用 push 方法的时候,首先插入一个 `POOL_BOUNDARY` 对象,一个对象调用 `autorelease` 方法,则会被加入到 AutoreleasePoolPage 对象的,自动释放区。然后插入 P1、P2 对象地址
+ - 遇到第二个 `@autoreleasepool{}` ,则创建第一个 AutoreleasePoolPage 对象 atautoreleasepoolobj2,构造方法调用 push 方法的时候,首先插入一个 `POOL_BOUNDARY` 对象,一个对象调用 `autorelease` 方法,则会被加入到 AutoreleasePoolPage 对象的,自动释放区。然后插入 P3 对象地址
+ - 遇到第三个 `@autoreleasepool{}` ,则创建第一个 AutoreleasePoolPage 对象 atautoreleasepoolobj3,构造方法调用 push 方法的时候,首先插入一个 `POOL_BOUNDARY` 对象,一个对象调用 `autorelease` 方法,则会被加入到 AutoreleasePoolPage 对象的,自动释放区。然后插入 P4 对象地址
+- 释放阶段:(从里到外释放)
+ - 遇到第三个 `@autoreleasepool{}` 大括号结束,则其内部的 atautoreleasepoolobj3 执行析构函数,调用 `AutoreleasePoolPage::pop` 方法
+ - 首先从 p4(最后一个入栈的对象) 开始,不断执行 `release` 消息,直到遇到调用 `objc_autoreleasePoolPop(atautoreleasepoolobj3)` 传入的 atautoreleasepoolobj3
+ - 继续从 p3 开始,不断执行 `release` 消息,直到遇到调用 `objc_autoreleasePoolPop(atautoreleasepoolobj2)` 传入的 atautoreleasepoolobj2
+ - 继续从 p2 开始,不断执行 `release` 消息,直到遇到调用 `objc_autoreleasePoolPop(atautoreleasepoolobj1)` 传入的 atautoreleasepoolobj1
+- 至此,完成了 autorelease 对象的管理和释放流程。
+
+所以,嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次 push 的位置为止,多层的 pool 就是多个**哨兵对象(POOL_BOUNDARY)**而已,就像剥洋葱一样,每次一层,互不影响
-所以,嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象而已,就像剥洋葱一样,每次一层,互不影响
-第一个同理。
小窍门,对于上述原理的分析可以用源码中看到的 `AutoreleasePoolPage` 对象的 `printAll` 方法。
@@ -1945,7 +3842,9 @@ static inline id *autoreleaseFast(id obj) {
查看 NSObject autorelease 方法调用链路可以看到最后还是调用 AutoreleasePoolPage 的 add 方法(会判断有没有页、有没有满)
-### 容器类会自动添加 AutoreleasePool
+
+
+### ARC 时代会自动加 autorelease
系统容器类,在使用 block 枚举器的时候,内部会自动创建 AutoreleasePool
@@ -1959,29 +3858,69 @@ static inline id *autoreleaseFast(id obj) {
所以,我们老老实实写的 for、while 循环中需要手加局部 AutoreleasePool。推荐使用系统提供的容器类的 block 枚举器。
+
+
+Cocoa 框架中,很多类方法用于返回 autorelease 对象。
+
+```objective-c
+// NSArray.m
++ (id) arrayWithCapacity: (NSUInteger)numItems
+{
+ return AUTORELEASE([[self allocWithZone: NSDefaultMallocZone()]
+ initWithCapacity: numItems]);
+}
+
+// NSDictionary.m
++ (id) dictionary
+{
+ return AUTORELEASE([[self allocWithZone: NSDefaultMallocZone()] init]);
+}
+```
+
+
+
### autorelease 对象什么时候调用 release 方法
-每当进行一次`objc_autoreleasePoolPush`调用时,runtime 向当前的 AutoreleasePoolPage 中 add 进一个`哨兵对象`,值为0(也就是个nil),那么这一个page就变成了下面的样子:
+iOS 项目中,`viewDidLoad` 方法内的创建的对象什么时候释放?
-
+```objective-c
+@implementation ViewController
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ Person *p = [[Person alloc] init];
+ NSLog(@"%s", __func__);
+}
+- (void)viewWillAppear:(BOOL)animated {
+ [super viewWillAppear:animated];
+ NSLog(@"%s", __func__);
+}
+- (void)viewDidAppear:(BOOL)animated {
+ [super viewDidAppear:animated];
+ NSLog(@"%s", __func__);
+}
+@end
+// console
+- [ViewController viewDidLoad]
+- [ViewController viewWillAppear]
+- [Person dealloc]
+- [ViewController viewDidAppear]
+```
-`objc_autoreleasePoolPush`的返回值正是这个哨兵对象的地址,被`objc_autoreleasePoolPop(哨兵对象)`作为入参,于是:
+看上去这个释放时机不确定?
-1. 根据传入的哨兵对象地址找到哨兵对象所处的page
-2. 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次`- release`消息,并向回移动`next`指针到正确位置
-3. 补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page
-
-其次,AutoreleasePool 和 RunLoop 的也有关系
+在 `viewDidLoad` 方法中,打印 runloop。可以发现:
iOS 在主线程的 Runloop 中注册了2个 Observer
- 第1个 Observer 监听了 `kCFRunLoopEntry` 事件,会调用`objc_autoreleasePoolPush()`
+- 第2个 Observer 监听了2个事件:
+ - `kCFRunLoopBeforeWaiting` 事件,会调用`objc_autoreleasePoolPop()` 同时也会调用 `objc_autoreleasePoolPush()`
+ - `kCFRunLoopBeforeExit`事件,会调用 `objc_autoreleasePoolPop()`
-- 第2个 Observer 监听了 `kCFRunLoopBeforeWaiting` 事件,会调用`objc_autoreleasePoolPop()`、`objc_autoreleasePoolPush()`。还监听了`kCFRunLoopBeforeExit`事件,会调用 `objc_autoreleasePoolPop()`
结合 RunLoop 运行图
-
+
- 01 通知 Observer 进入 Loop 会调用 `objc_autoreleasePoolPush`
@@ -1995,9 +3934,86 @@ iOS 在主线程的 Runloop 中注册了2个 Observer
- 11 没任务将要休眠,调用 `objc_autoreleasePoolPop`
-可以看到 objc_autoreleasePoolPush、objc_autoreleasePoolPop 成对调用,贯穿 RunLoop
+可以看到 `objc_autoreleasePoolPush`、`objc_autoreleasePoolPop` 成对调用,贯穿 RunLoop
-## 内存问题典型 case
+AutoreleasePool 也是 RunLoop 的业务方。iOS GUI 系统下,很多动画、视图渲染、对象的销毁都依赖 RunLoop。
+
+可以回答上面的问题了:系统对 Runloop 添加了观察者,监听 RunLoop 的状态,当将要休眠的时候会触发回调,系统会执行 AutoreleasePool 的 pop 方法,来释放当前一轮 RunLoop 中需要释放的对象。
+
+RunLoop 和 AutoreleasePool 存在几种可能:
+
+- RunLoop 进入的时候(`kCFRunLoopEntry`),执行 `objc_autoreleasePoolPush` 方法,然后不断运行,等待没任务的时候,RunLoop 在进入休息的时候(`kCFRunLoopBeforeWaiting`),执行一次 `objc_autoreleasePoolPop` 方法,释放当前需要释放的对象。同时执行一次 `objc_autoreleasePoolPush` 方法,然后去休眠。休眠唤醒后继续执行 RunLoop 各个阶段的事情。等没事(Timer、Source)处理的时候,继续 RunLoop 在进入休息的时候(`kCFRunLoopBeforeWaiting`),执行一次 `objc_autoreleasePoolPop` 方法,释放当前需要释放的对象。不断循环,依次类推,push 和 pop 成对存在
+- RunLoop 进入的时候(`kCFRunLoopEntry`),执行 `objc_autoreleasePoolPush` 方法,然后不断运行,等待需要退出的时候(`kCFRunLoopBeforeExit`),执行一次 `objc_autoreleasePoolPop` 方法,释放当前需要释放的对象。push 和 pop 成对存在
+- RunLoop 进入的时候(`kCFRunLoopEntry`),执行 `objc_autoreleasePoolPush` 方法,然后不断运行,等待没任务的时候,RunLoop 在进入休息的时候(`kCFRunLoopBeforeWaiting`),执行一次 `objc_autoreleasePoolPop` 方法,释放当前需要释放的对象。同时执行一次 `objc_autoreleasePoolPush` 方法,然后去休眠。休眠唤醒后继续执行 RunLoop 各个阶段的事情。等没事(Timer、Source)处理的时候,继续 RunLoop 在进入休息的时候(`kCFRunLoopBeforeWaiting`),执行一次 `objc_autoreleasePoolPop` 方法,释放当前需要释放的对象。不断循环,最后没事情处理了,等待需要退出的时候(`kCFRunLoopBeforeExit`),执行一次 `objc_autoreleasePoolPop` 方法,释放当前需要释放的对象。push 和 pop 成对存在在
+
+
+
+
+
+### autorelease 实现原理
+
+`[obj autoreleaae] ` 底层调用 NSObject 的 autorelease 实例方法。
+
+```objective-c
+// NSObject.m
+static id autorelease_class = nil;
+static SEL autorelease_sel;
+static IMP autorelease_imp;
+
++ (void) initialize
+{
+ // ...
+ autorelease_class = [NSAutoreleasePool class];
+ autorelease_sel = @selector(addObject:);
+ autorelease_imp = [autorelease_class methodForSelector: autorelease_sel];
+ // ...
+}
+
+- (id) autorelease
+{
+ if (double_release_check_enabled)
+ {
+ NSUInteger release_count;
+ NSUInteger retain_count = [self retainCount];
+ release_count = [autorelease_class autoreleaseCountForObject:self];
+ if (release_count > retain_count)
+ [NSException
+ raise: NSGenericException
+ format: @"Autorelease would release object too many times.\n"
+ @"%"PRIuPTR" release(s) versus %"PRIuPTR" retain(s)",
+ release_count, retain_count];
+ }
+
+ (*autorelease_imp)(autorelease_class, autorelease_sel, self);
+ return self;
+}
+```
+
+对象的 autorelease 方法,本质就是调用 NSAutoreleasePool 的 addObject 类方法。
+
+另外,通过 GUN 源码可以看到, autorelease 方法是用一种特殊的方法来实现的。这方法能够高效的运行 OSX、iOS 应用程序中频繁调用的 autorelease 方法,它被称为 “**IMP Caching**”。在进行方法调用时,为了解决类名/方法名以及取得方法运行时的函数指针,在 NSObject 类的 initialize 方法中,对其结果进行缓存。
+
+IMP Caching 比其他方法快2倍。
+
+
+
+### AutoreleasePoolPage::pop
+
+- 根据传入的哨兵对象找到对应位置
+- 给上次 push 之后添加的对象依次发送 release 消息
+- 回退 next 指针到正确的位置
+
+
+
+### 思考
+
+- 在当次 runloop 将要结束的时候调用 AutoreleasePoolPage::pop()
+- AutoreleasePool 多层嵌套的本质就是多次插入哨兵对象,AutoreleasePoolPage 以栈为节点的双向链表结构。
+- 在循环中比如 alloc 图片数据等内存消耗较大的场景,手动插入 autoreleasePool,每次 for 循环都会进行一次内存的释放 @autoreleasepool{} 本质编译器会转换为 AutoreleasePoolPage 的构造函数和析构函数,内部会调用 `AutoreleasePoolPage::push` 和 `AutoreleasePoolPage::pop` 来及时释放内存。否则释放时机就是 RunLoop 将要休眠的时候,对象没有及时释放,会导致内存压力陡增
+
+
+
+## 典型的内存问题
### OC 中有没有不对内存进行强持有的集合类型?
@@ -2070,7 +4086,7 @@ NSHashMap、NSMapTable 都可以描述 key、value 的内存修饰。
这段代码运行会 crash,信息如下
-
+
原因是 NSError 构造方法内部会加 autorelease。源码如下
@@ -2133,7 +4149,7 @@ MRC 下的 `[(id)(object) autorelease]` 等价于 ARC 下的 `id __autoreleasing
我写了个僵尸对象检测工具,效果如下
-
+
可以定位僵尸对象,并且打印出具体堆栈,并模拟系统行为调用 `abort` 。对监控原理和工具实现感兴趣的可以查看这里[带你打造一套 APM 监控系统-内存监控之野指针/内存泄漏监控](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.74.md#zombieSniffer)
diff --git a/Chapter1 - iOS/1.45.md b/Chapter1 - iOS/1.45.md
index b570ee6..7e57e6c 100644
--- a/Chapter1 - iOS/1.45.md
+++ b/Chapter1 - iOS/1.45.md
@@ -1,67 +1,280 @@
-# NSTimer 中的内存泄露
+# NSTimer、CSDisplayLink 中的内存泄露
-NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES block:nil]` 和当前的 VC 都会互相持有,造成环,会存在内存泄漏问题
+
+
+## CADisplayLink 内存泄漏
+
+
+
+可以看到 CADisplayLink 和 VC,VC 和 CADisplayLink 互相持有,造成内存泄漏,没有释放。即使页面离开,定时器还在继续运行,不断打印。
+
+
+
+## NSTimer 内存泄漏
+
+### 对比实验
+
+NSTimer 的基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES block:nil]` 和当前的 VC 都会互相持有,造成环,会存在内存泄漏问题
+
+Demo 如下:
+
+
+
+但是当使用 `[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerTask) userInfo:nil repeats:NO];` repeats 为 NO 的时候,好像不会内存泄漏。这是为什么?
+
+
+
+
+
+### 源码分析
+
+查看 gnu 源码发现
```objective-c
-@interface ViewController()
-@property (nonatomic, strong) NSTimer *timer;
-@end
-
-@implementation ViewController
-- (void)viewDidLoad {
- [super viewDidLoad];
-
- self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1
- target:self
- selector:@selector(p_doSomeThing)
- userInfo:nil
- repeats:YES];
-
+// NSTimer.m
++ (NSTimer*) timerWithTimeInterval: (NSTimeInterval)ti
+ target: (id)object
+ selector: (SEL)selector
+ userInfo: (id)info
+ repeats: (BOOL)f
+{
+ return AUTORELEASE([[self alloc] initWithFireDate: nil
+ interval: ti
+ target: object
+ selector: selector
+ userInfo: info
+ repeats: f]);
}
-
-- (void)p_doSomeThing {
- // doSomeThing
-}
-
-- (void)p_stopDoSomeThing {
- [self.timer invalidate];
- self.timer = nil;
-}
-
-- (void)dealloc {
- [self.timer invalidate];
-}
-
-@end
```
+内部调用下面的函数
+
+```objective-c
+- (id) initWithFireDate: (NSDate*)fd
+ interval: (NSTimeInterval)ti
+ target: (id)object
+ selector: (SEL)selector
+ userInfo: (id)info
+ repeats: (BOOL)f
+{
+ if (ti <= 0.0)
+ {
+ ti = 0.0001;
+ }
+ if (fd == nil)
+ {
+ _date = [[NSDate_class allocWithZone: NSDefaultMallocZone()]
+ initWithTimeIntervalSinceNow: ti];
+ }
+ else
+ {
+ _date = [fd copyWithZone: NSDefaultMallocZone()];
+ }
+ _target = RETAIN(object);
+ _selector = selector;
+ _info = RETAIN(info);
+ if (f == YES)
+ {
+ _repeats = YES;
+ _interval = ti;
+ }
+ else
+ {
+ _repeats = NO;
+ _interval = 0.0;
+ }
+ return self;
+}
+```
+
+外面的 repeat 根据传递的布尔值,内部赋值给 _repeats 参数。
+
+内部会自动调用 fire
+
+```objective-c
+- (void) fire
+{
+ /* We check that we have not been invalidated before we fire.
+ */
+ if (NO == _invalidated) {
+ if ((id)_block != nil) {
+ CALL_BLOCK(_block, self);
+ } else {
+ id target;
+
+ /* We retain the target so it won't be deallocated while we are using
+ * it (if this timer gets invalidated while we are firing).
+ */
+ target = RETAIN(_target);
+
+ if (_selector == 0) {
+ NS_DURING {
+ [(NSInvocation*)target invoke];
+ }
+ NS_HANDLER {
+ NSLog(@"*** NSTimer ignoring exception '%@' (reason '%@') "
+ @"raised during posting of timer with target %s(%s) "
+ @"and selector '%@'",
+ [localException name], [localException reason],
+ GSClassNameFromObject(target),
+ GSObjCIsInstance(target) ? "instance" : "class",
+ NSStringFromSelector([target selector]));
+ }
+ NS_ENDHANDLER
+ } else {
+ NS_DURING {
+ [target performSelector: _selector withObject: self];
+ }
+ NS_HANDLER {
+ NSLog(@"*** NSTimer ignoring exception '%@' (reason '%@') "
+ @"raised during posting of timer with target %p and "
+ @"selector '%@'",
+ [localException name], [localException reason], target,
+ NSStringFromSelector(_selector));
+ }
+ NS_ENDHANDLER
+ }
+ RELEASE(target);
+ }
+
+ if (_repeats == NO) {
+ [self invalidate];
+ }
+ }
+}
+```
+
+可以看到如果 repeat 为 NO ,则会执行 `[target performSelector: _selector withObject: self];` 调用1次方法,然后会执行 `invalidate` 函数,`invalidate` 实现如下
+
+```objective-c
+- (void) invalidate
+{
+ /* OPENSTEP allows this method to be called multiple times. */
+ _invalidated = YES;
+ if (_target != nil)
+ {
+ DESTROY(_target);
+ }
+ if (_info != nil)
+ {
+ DESTROY(_info);
+ }
+}
+```
+
+可以看到当 target 和 info 存在的时候,都会在 `invalidate` 方法中被 destory,也就是释放。
+
+```
+#define DESTROY(object) ({ \
+ void *__o = (void*)object; \
+ object = nil; \
+ [(id)__o release]; \
+})
+#endif
+```
+
+结论:通过 gnu 可以看到,NSTimer 会对传入的 target、info 对象进行持有强引用,当 repeat 参数为 NO 的时候,则会立马通过 performSelector 的方式执行定时器任务,然后执行 invalidate 方法,对其内部引用的 object、info 进行释放。
+
+
+
上面的代码主要是利用定时器重复执行 p_doSomeThing 方法,在合适的时候调用 p_stopDoSomeThing 方法使定时器失效。
能看出问题吗?在开始讨论上面代码问题之前,需要对 NSTimer 做一点说明。NSTimer 的 `scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:` 方法的最后一个参数为 YES 时,NSTimer 会保留目标对象,等到自身失效才释放目标对象。执行完任务后,一次性的定时器会自动失效;重复性的定时器,需要主动调用 invalidate 方法才会失效。
-当前的 VC 和 定时器互相引用,造成循环引用。
+当前的 VC 和 定时器互相引用,造成循环引用。所以思路如下:
如果能在合适的时机打破循环引用就不会有问题了
1. 控制器不再强引用定时器
2. 定时器不再保留当前的控制器
-## 解决方案:
-### 替换 NSTimer API
+
+## 解决方案
+
+### 改用 block 的方式替换 API,不再持有 target
+
+
+
+该种方式,控制器 (self)强引用 timer,timer 强引用 block,block 弱引用 self,3者没有形成环。
+
+
+
+### 采用系统 NSProxy 代替自定义的中间类
+
+
+
+注意:继承自 NSProxy 的类,不能 init。
+
+
+
+QA:自己写的继承自 NSObject 的代理对象和继承自 NSProxy 的代理有何区别?看上去反而是自定义的 NSObject 使用更简单呀?
+
+答:**NSProxy 效率更高**。NSProxy 的主要作用是为消息转发提供一个通用的接口,是一个继承自 NSObject 的对象,虽然看上去 API 更简单,写法简单,但内部运行的时候还是基于 isa 去查找类对象、元类对象的 cache 中查找,找不到再去 class_rw_t 中查找,找不到再从 superclass 找父类的类对象、元类对象...流程,最后还是找不到,则走 runtime 的动态方法解析、消息转发阶段。
+
+
+
+
+
+看一段神奇的代码
+
+
+
+为什么打印出 `0 1`?
+
+分析:
+
+- p1 是 `TimerProxy` 类,继承于 NSObject 所以就不是 UIViewController 类型。
+
+- p2 是 `MethodProxy` 类,继承自 NSProxy,当调用 isKindOfClass 这个方法的时候,也会进行消息转发,即调用 `forwardInvocation` 方法,其内部实现 `[invocation invokeWithTarget:self.target];` 则触发 self.target 的逻辑。此时 self.target 就是 self,所以上面的 `[p2 isKindOfClass:[self class]]` 等价于 `[self isKindOfClass:[self.class]]`,所以为 1。
+
+也就是说继承自 NSProxy 类的对象,调用方法的时候,会自动走消息转发的流程。
+
+这一点可以查看 GUN 查看下源码印证。`NSProxy.m`
```objectivec
-__weak typeof(self) weakSelf = self;
-self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
- [weakSelf timerTest];
-}];
+- (BOOL) isKindOfClass: (Class)aClass
+{
+ NSMethodSignature *sig;
+ NSInvocation *inv;
+ BOOL ret;
+ sig = [self methodSignatureForSelector: _cmd];
+ inv = [NSInvocation invocationWithMethodSignature: sig];
+ [inv setSelector: _cmd];
+ [inv setArgument: &aClass atIndex: 2];
+ [self forwardInvocation: inv];
+ [inv getReturnValue: &ret];
+ return ret;
+}
```
+可以看到内部直接调用了消息转发。
+
+
+
+
+
### GCD Timer
-CADisplayLink、NSTimer 都是依靠 RunLoop 实现的,所以当 RunLoop 任务繁重的时候,定时器可能不准。
+**CADisplayLink、NSTimer 都是依靠 RunLoop 实现的,所以当 RunLoop 任务繁重的时候,定时器可能不准。**
-GCD 的定时器会更加准时,底层依赖系统内核。
+
+
+
+
+假设一个 NSTimer 被加到 RunLoop 开头,NSTimer 执行周期为1s,RunLoop 前面任务繁重,第一次走完一个完整的 RunLoop 需要0.4s,然后从头检测 NSTimer 有没有到时间,发现还没到继续执行 RunLoop 后续逻辑。后面遇到卡顿任务了,第二次 RunLoop 用了0.5s,然后从头检测 NSTimer 有没有到时间,0.4+0.5还不到时间,继续跑,第三次 RunLoop 比较轻松,耗时0.2s,再判断定时器时间有没有到,则此次已经0.4+0.5+0.2=1.1s了,此时 NSTimer 的事件被执行,此时精确度已经不够了(每次 RunLoop 的执行时间不固定)
+
+如果 NSTimer 被添加到了一个特定的模式,当滚动视图时, RunLoop 会切换到 `UITrackingRunLoopMode`,如果 NSTimer 没有被添加到这个模式,它就不会触发。
+
+当 RunLoop 没有事件可处理时,它会进入休眠状态。这意味着即使定时器的时间间隔到了,但 `RunLoop` 可能还在休眠中,因此定时器不会立即触发。
+
+
+
+网上有些针对 FPS 帧率的检测是基于 CADisplayLink 计算的,所以这种方案不准确。具体可以查看文章:[带你打造一套 APM 监控系统](./1.74.md)
+
+
+
+GCD 的 timer 会更加准时,底层依赖系统内核,不依赖 RunLoop。
```objectivec
@property (nonatomic, strong) dispatch_source_t timer;
@@ -86,27 +299,17 @@ self.timer = timerSource;
GCD timer 不依赖 RunLoop,系统底层驱动,所以会更加准确。因为和 RunLoop 无关,所以和 UI 滚动,RunLoop mode 切换到 UITrackingMode 也不影响 GCD timer。
+
+
### 打破循环引用,NSTimer target 自定义
-```objectivec
-@interface LBPProxy : NSObject
-+ (instancetype)proxyWithObject:(id)target;
-@property (nonatomic, weak) id target;
-@end
+
+
+
+
+
-@implementation LBPProxy
-+ (instancetype)proxyWithObject:(id)target{
- LBPProxy *proxy = [[LBPProxy alloc] init];
- proxy.target = target;
- return proxy;
-}
-- (id)forwardingTargetForSelector:(SEL)aSelector{
- return self.target;
-}
-@end
-self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[LBPProxy proxyWithObject:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
-```
### 高精度定时器封装
@@ -239,122 +442,7 @@ dispatch_semaphore_t semaphore_;

-### NSProxy
-```objectivec
-#import "LBPProxy.h"
-@implementation LBPProxy
-+ (instancetype)proxyWithObject:(id)target{
- LBPProxy *proxy = [LBPProxy alloc];
- proxy.target = target;
- return proxy;
-}
-- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
- return [self.target methodSignatureForSelector:sel];
-}
-- (void)forwardInvocation:(NSInvocation *)invocation{
- // 方法1
- invocation.target = self.target;
- [invocation invoke];
- // 方法2
- [invocation invokeWithTarget:self.target];
-}
-@end
-
-self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[LBPProxy proxyWithObject:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
-```
-
-QA: 自己写的继承自 NSObject 的代理对象和继承自 NSProxy 的代理有何区别?
-
-NSProxy 效率更高。继承自 NSObject 的代理,内部运行的时候还是存在方法查找(isa、superclass、cache、methods)流程。
-
-看一段神奇的代码
-
-`LBPProxy`
-
-```objectivec
-@interface LBPProxy : NSObject
-+ (instancetype)proxyWithObject:(id)target;
-@property (nonatomic, weak) id target;
-@end
-@implementation LBPProxy
-+ (instancetype)proxyWithObject:(id)target{
- LBPProxy *proxy = [LBPProxy alloc];
- proxy.target = target;
- return proxy;
-}
-- (id)forwardingTargetForSelector:(SEL)aSelector{
- return self.target;
-}
-@end
-```
-
-`LBPProxy2`
-
-```objectivec
-@interface LBPProxy2 : NSProxy
-+ (instancetype)proxyWithObject:(id)target;
-@property (nonatomic, weak) id target;
-@end
-@implementation LBPProxy2
-+ (instancetype)proxyWithObject:(id)target{
- LBPProxy2 *proxy = [LBPProxy2 alloc];
- proxy.target = target;
- return proxy;
-}
-- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
- return [self.target methodSignatureForSelector:sel];
-}
-- (void)forwardInvocation:(NSInvocation *)invocation{
- // 方法1
- invocation.target = self.target;
- [invocation invoke];
- // 方法2
- [invocation invokeWithTarget:self.target];
-}
-@end
-```
-
-main.m
-
-```objectivec
-ViewController *vc = [[ViewController alloc] init];
-LBPProxy *p1 = [LBPProxy proxyWithObject:vc];
-LBPProxy2 *p2 = [LBPProxy2 proxyWithObject:vc];
-NSLog(@"%d %d",
- [p1 isKindOfClass:[UIViewController class]],
- [p2 isKindOfClass:[UIViewController class]]);
-appDelegateClassName = NSStringFromClass([AppDelegate class]);
-// 0 1
-```
-
-为什么打印出 `0 1`。
-
-分析:
-
-- p1 是 LBPProxy 类,继承于 NSObject 所以就不是 UIViewController 类型。
-
-- p2 是 LBPProxy2 类,继承自 NSProxy,当调用 isKindOfClass 这个方法的时候,也会进行消息转发,即调用 `forwardInvocation` 方法,其内部实现 `[invocation invokeWithTarget:self.target];` 则触发 self.target 的逻辑。此时 self.target 就是 VC,所以为 1。
-
-这一点可以查看 GUN 查看下源码印证。`NSProxy.m`
-
-```objectivec
-- (BOOL) isKindOfClass: (Class)aClass
-{
- NSMethodSignature *sig;
- NSInvocation *inv;
- BOOL ret;
- sig = [self methodSignatureForSelector: _cmd];
- inv = [NSInvocation invocationWithMethodSignature: sig];
- [inv setSelector: _cmd];
- [inv setArgument: &aClass atIndex: 2];
- [self forwardInvocation: inv];
- [inv getReturnValue: &ret];
- return ret;
-}
-```
-
-可以看到内部直接调用了消息转发。
### 采用 Block 的形式为 NSTimer 增加分类
@@ -476,6 +564,8 @@ __strong __typeof(&*weakSelf)self = weakSelf;
iOS 10 中,定时器 api 增加了 block 方法,实现原理与此类似,这里采用分类为 NSTimer 增加 block 参数的方法,最终的行为一致
+
+
## 检测
根据 Instrucments 提供的工具的工作原理,写一个野指针探针工具去发现并定位问题。具体见[野指针监控工具](./1.74.md#zombieSniffer)
\ No newline at end of file
diff --git a/Chapter1 - iOS/1.46.md b/Chapter1 - iOS/1.46.md
index 3caaf82..0ce983b 100644
--- a/Chapter1 - iOS/1.46.md
+++ b/Chapter1 - iOS/1.46.md
@@ -1,6 +1,282 @@
# KVC && KVO
-## 一、基本用法-字典快速赋值
+> KVO 的实现原理是什么?如何手动触发 KVO?本文来探索下 iOS 中 KVO 底层细节
+
+
+
+## 底层实现分析
+
+Demo1:创建 Person 类,点击事件里触发属性值的改变。
+
+
+
+
+
+分析:
+
+- 添加过 KVO 的 person1,isa 为系统利用 Runtime 技术动态创建的类,名字为 `NSKVONotifying_Person`
+- 没有添加过 KVO 的 person2,isa 为 Person 的类对象
+- `.height = 177` 本质是 `[self.person1 setHeight:177]` 也就是调用 set 方法。
+
+在内存中的结构如下图
+
+
+
+整个流程分析下:
+
+- self.person2 调用 setHeight 的时候,首先根据 self.person2 实例对象的 isa 找到 Person 类对象,然后在方法列表中找到 setHeight 方法,然后进行调用
+- self.person1 调用 setHeight 的时候,首先根据 self.person1 实例对象的 isa 找到 `NSKVONotifying_Person` 类对象,然后在方法列表中找到 setHeight 方法,然后进行调用。内部实现中,会调用 Foundation 的 `_NSSetIntValueAndNotify` 方法。
+- 然后调用: willSet、super setHeight、didSet 方法。
+
+当我们按照 KVO 后动态生成的类名去创建一个新的类的时候,Xcode 会报错:`[general] KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class`。因为自己创建的类名和系统将要动态创建的类名冲突了,并且 KVO 监听失效
+
+
+
+
+
+Demo2:
+
+
+
+分析:
+
+- 可以看到在对 self.person1 添加 KVO 之后,self.person1 的类对象改变了,也就是 self.person1的 isa 改变了,变为系统动态生成的新类
+- 在对 self.person1 添加 KVO 之后,self.person1 的 setHeight 方法的实现变了,添加前后 self.person2 的 setHeight 方法,都是 Person 类对象的 setHeight 方法实现。KVO 添加前后都未改变
+- 利用 `(IMP)方法地址` 查看,没有进行 KVO 的 `setHeight` 是在 `KVOExplore -[Person setHeight:] at Person.h:14)` 里。添加过 KVO 的是在 `Foundation _NSSetLongLongValueAndNotify` 里。且 `_NSSetLongLongValueAndNotify`是个 c 语言函数。
+
+
+
+Demo3:
+
+
+
+可以看到我们将 KVO 的数据类型改为 double 后,原本的 setHeight 是 `_NSSetLongLongValueAndNotify`,现在是 `_NSSetDoubleValueAndNotify`
+
+也可以借助于 `nm` 来查看所有 Foundation 关于 KVO 的方法 `nm Foundation | grep ValueAndNotify ` (需要自己提取真机上的 Foundation 符号表)
+
+
+
+### NSSet**ValueAndNotify 的内部实现
+
+
+
+来对 Person 类增加一些打印方法
+
+
+
+
+
+可以看出内部实现是:
+
+- 调用 `willChangeVlueForKey`
+
+- 调用原本的 setter
+
+- 调用 `didChangeValueForKey`。在 `didChangeValueForKey` 内部会调用 KVO 的 `- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context` 方法
+
+
+
+```objective-c
+@interface Person()
+@property (nonatomic, assign) double height;
+@end
+@implementation Person
+- (void)setHeight:(double)height {
+ _height = height;
+}
+
+@end
+
+@interface NSKVONotifying_Person: Person()
+
+@end
+
+@implementation NSKVONotifying_Person
+- (void)setHeight:(double)height {
+ [self setDoubleValueAndNotify:height];
+}
+
+- (void)setDoubleValueAndNotify:(double)height {
+ [self willChangeValueForKey:@"height"];
+ [super setHeight: height];
+ [self didChangeValueForKey:@"height"];
+}
+
+- (void)didChangeValueForKey:(NSString *)key {
+ [observer observeValueForKeyPath:key ofObject:self change:@{} context:nil];
+}
+
+- (Class)class {
+ return [Person class];
+}
+
+@end
+```
+
+
+
+### 重写 class 方法
+
+
+
+可以看到利用 runtime api,在添加 KVO 之后,类对象为 `NSKVONotifying_Person`
+
+但是利用 class 方法,添加 KVO 之后,获取类对象依旧为 Person。
+
+好处是:屏蔽了 KVO 底层内部实现,隐藏了 `NSKVONotifying_Person` 的存在,通过 `-(Class)class` 方法告诉开发者添加 KVO 之后的类,依旧是 Person,本质上是继承自 Person 的类对象,能力没有改变。
+
+
+
+### KVO 类的所有方法
+
+
+
+
+
+利用 runtime api,打印添加 KVO 后,动态创建的 NSKVONotifying_Person 都存在什么方法?
+
+- setHeight:
+- class
+- dealloc
+- _isKVOA
+
+QA:为什么新创建的类没有 getter?
+
+因为新创建的类是子类,父类中存在 getter。子类中增加的方法只是为了触发 KVO,getter 不影响。
+
+
+
+### 修改成员变量的值可以触发 KVO 吗
+
+
+
+我们将 Person 类的成员变量暴露出来,在点击事件里修改,发现不能触发 KVO。
+
+也就是说,触发 KVO 的本质是必须要有 setter,且触发 setter。直接修改成员变量,是不会触发 setter 的。
+
+QA:如何手动触发?
+
+手动调用 `willChangeValueForKey`、 `didChangeValueForKey`
+
+
+
+
+
+
+
+QA:请描述系统如何实现一个对象的 KVO?KVO 的本质是什么
+
+- 系统利用 runtime 的能力,动态创建一个监听对象的类的子类,子类命名格式为: `NSKVONotifying_类名`。 并且让 instance 对象的 isa 指向这个全新的子类
+- 当修改 instance 对象的属性时,会调用 Foundation 框架的 `_NSSet***ValueAndNotify` c 函数
+- 然后调用:
+ - `willChangeValueForKey`
+ - 原来的 setter
+ - `didChangeValueForKey`,且 didChangeValueForKey 内部会触发监听器(Observer)的监听方法(`- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context`)。也就是发消息
+
+QA:如何手动触发 KVO?
+
+上面剖析了 KVO 的系统实现,所以手动触发 KVO 就需要调用 `willChangeValueForKey` 和 `didChangeValueForKey`
+
+
+
+### 当没有observer观察任何一个property时,删除动态创建的子类
+
+`[self.person removeObserver:self forKeyPath:@"height"];` 该代码调用后,会删除动态创建的子类。
+
+
+
+## KVC
+
+`setValueForKey` 用来设置对象的一层属性值修改。
+
+`setValueForKeyPath` 可以设置对象的某个属性(属性本身是对象)的属性值修改
+
+
+
+### KVC 设值原理
+
+KVC 之后会触发 KVO 吗?
+
+
+
+发现 KVC 触发了 KVO。
+
+问题来了:为什么 KVC 会触发 KVO?探究下 `setValueForKey`
+
+整个流程如下
+
+
+
+`[self.person setValue:@10 forKey:@"age"]` 会先调用 `setKey:` 同名的方法,找不到则调用 `_setKey:` 的方法,如果还是找不到则调用 `+(BOOL)accessInstanceVariableDirectlt`,如果该方法返回 YES,则可以直接修改成员变量的值,会按照 `_key`、`_isKey`、`key`、`isKey` 的顺序寻找成员变量,如果找到则直接赋值,没找到则抛出异常 `NSUnknownKeyException`
+
+```
+@implementation Person
+- (void)setAge:(int)age
+{
+ _age = age;
+}
+
+- (void)_setAge:(int)age
+{
+ _age = age;
+}
+
++ (BOOL)accessInstanceVariableDirectlt
+{
+ return YES;
+}
+
+@end
+```
+
+
+
+### 直接修改成员变量会触发 KVO 吗?
+
+不会。KVO 的实现原理就是在 setter 方法内,调用 `willChangeValueForKey`、`didChangeValueForKey` ,所以直接修改成员变量不会触发 KVO。
+
+想要触发,可以手动调用上面2个 API。
+
+```objective-c
+- (void)setName:(NSString *)name {
+ [self willChangeValueForKey:@"name"];
+ _name = name;
+ [self didChangeValueForKey:@"name"];
+}
+```
+
+
+
+### KVC 取值原理
+
+`valueForKey` 原理
+
+- 按照 getKey、key、isKey、_key 的顺序寻找方法实现,找到则直接调用方法,返回值
+- 如果没找到则调用 `+(BOOL)accessInstanceVariableDirectly` 方法,询问是否可以访问成员变量
+ - 为 NO 则调用 `valueForUndefinedKey `并抛出 `NSUnknownKeyException`异常
+ - 为 YES 则按照 ` __key`、`_isKey`、`key`、`isKey` 的顺序访问成员变量。找到哪个则返回值
+
+- 都没找到则调用 `valueForUndefinedKey `并抛出 `NSUnknownKeyException`异常
+
+
+
+
+
+
+
+### KVC 会破坏面向对象的原则吗?
+
+KVC 有违背面向对象编程思想吗?如果一个类的成员变量是私有的,也没有在 `.h` 中公开一些方法去设置、修改成员变量,那么外部直接通过 KVC 去修改值,是有违背面向对象编程思想的。
+
+KVC 提供了对应的能力,去保护或者说支持面向对象的原则。
+
+
+
+
+
+
+
+## 基本用法-字典快速赋值
KVC 可以将字典里面和 model 同名的 property 进行快速赋值 `setValuesForKeysWithDictionary`。
@@ -84,11 +360,11 @@ self.person.dog.weight = 50;
}
```
-## 二、 KVO 的本质
-kVO 是Objective-C 对观察者模式的实现。也是 Cocoa Binding 的基础。
-### 几个基本的知识点
+
+
+## 几个基本的知识点
1. KVO 观察者和属性被观察的对象之间不是强引用的关系
@@ -127,15 +403,11 @@ kVO 是Objective-C 对观察者模式的实现。也是 Cocoa Binding 的基础
@end
```
-8. 手动触发 KVO?调用 willChangeValueForKey、didChangeValueForKey
-
-9.
-
-## 三、 实现机制
+## 实现机制
> Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ...
@@ -145,22 +417,43 @@ Apple 文档告诉我们:被观察对象的 `isa指针` 会指向一个中间
- KVO 是基于 Runtime 机制实现的
-- 当某个类的属性第一次被观察的时候,系统会在运行期动态的创建该类的一个派生类。在派生类中重写任何被观察属性的 setter 方法。派生类在真正实现`通知机制`
+- 当某个类的属性第一次被观察的时候,系统会在运行期动态的创建该类的一个派生类(子类)。在派生类中重写任何被观察属性的 setter 方法。派生类在真正实现`通知机制`
-- 如果当前类为 Person,则生成的派生类名称为 `NSKVONotifying_Person`
+- 如果当前类为 Person,则生成的派生(子类)类名称为 `NSKVONotifying_Person`
-- 每个类对象中都有一个 `isa指针` 指向当前类,当一个类对象第一次被观察的时候,系统会偷偷将 isa 指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是当前派生类的 `setter` 方法
+- 每个类对象中都有一个 `isa指针` 指向当前类,当一个类对象第一次被观察的时候,系统会偷偷将 isa 指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是当前派生类的 `setter` 方法
- 键值观察通知依赖于 NSObject 的两个方法:`willChangeValueForKey:、didChangeValueForKey:` 。在一个被观察属性改变之前,调用 `willChangeValueForKey:` 记录旧的值。在属性值改变之后调用 `didChangeValueForKey:`,从而 `observeValueForKey:ofObject:change:context:` 也会被调用。
-
+
为什么要选择是继承的子类而不是分类呢?
子类在继承父类对象,子类对象调用调方法的时候先看看当前子类中是否有方法实现,如果不存在方法则通过 isa 指针顺着继承链向上找到父类中是否有方法实现,如果父类种也不存在方法实现,则继续向上找...直到找到 NSObject 类为止,系统会抛出几次机会给程序员补救,如果未做处理则奔溃
关于分类与子类的关系可以看看我之前的 [文章](1.50.md).
-## 四、 模拟实现系统的 KVO
+
+
+## QA
+
+iOS 用什么方式实现对一个对象的 KVO?(KVO 的本质是什么)
+
+- 当对一个对象使用了 KVO 监听,系统会修改这个对象的 isa 指针,改为一个指向通过 Runtime 动态创建的子类
+- 子类拥有自己对监听属性的 setter 实现,内部会调用
+ - willChangeValueForKey
+ - 原来的 setter 实现
+ - didChangeValueForKey,这个方法内部又会调用监听器的 监听方法 `[observer observeValueForKey:ofObject:change:context:]`
+
+如何手动触发 KVO?
+
+```objective-c
+[p willChangeValueForKey:@"height"];
+[p didChangeValueForKey:@"height"];
+```
+
+
+
+## 模拟实现系统的 KVO
1. 创建被观察对象的子类
2. 重写观察对象属性的 set 方法,同时调用 `willChangeValueForKey、didChangeValueForKey`
@@ -267,54 +560,3 @@ KVO 的改装:
-## 五、 KVC
-
-`setValueForKey` 用来设置对象的一层属性值修改。
-
-`setValueForKeyPath` 可以设置对象的某个属性(属性本身是对象)的属性值修改
-
-
-
-KVC 之后会触发 KVO。为什么?探究下 `setValueForKey`
-
-`[self.person setValue:@10 forKey:@"age"]` 会先调用 `setKey:` 同名的方法,找不到则调用 `_setKey:` 的方法,如果还是找不到则调用 `+(BOOL)accessInstanceVariableDirectlt`,如果该方法返回 YES,则可以直接修改成员变量的值,会按照 _key、_isKey、key、isKey 的顺序寻找成员变量,如果找到则直接赋值,没找到则抛出异常 NSUnknownKeyException
-
-整个流程如下
-
-
-
-```
-@implementation Person
-- (void)setAge:(int)age
-{
- _age = age;
-}
-
-- (void)_setAge:(int)age
-{
- _age = age;
-}
-
-+ (BOOL)accessInstanceVariableDirectlt
-{
- return YES;
-}
-
-@end
-```
-
-valueForKey 原理
-
-- 按照 getKey、key、isKey、_key 的顺序寻找方法实现
-
-- 找到则直接调用方法,返回值
-
-- 如果没找到则调用 accessInstanceVariableDirectlt 方法,询问是否可以访问成员变量。为 NO 则抛出异常
-
-- 为 YES 则按照 __key、isKey、key、isKey 的顺序访问成员变量。找到哪个则返回值
-
-- 都没找到则抛出异常
-
-
-
-
diff --git a/Chapter1 - iOS/1.48.md b/Chapter1 - iOS/1.48.md
index 86a145c..b185224 100644
--- a/Chapter1 - iOS/1.48.md
+++ b/Chapter1 - iOS/1.48.md
@@ -1,6 +1,12 @@
-# 类别(Category)、拓展(Extension)、load、initialize
+# Category 底层原理
-> 很多人都知道类别、分类的用法,但是对于一些细节就不是很清楚了,本文主要梳理下这3个概念的细节
+> 很多人都知道 Category、Extension 的用法,但是对于一些细节就不是很清楚了:
+>
+> - Category 中添加到属性、对象方法、类方法、协议在内存中是怎么存储的?
+> - Category 中添加的到属性、对象方法、类方法、协议,是什么时候、如何与 Class 本身的属性、对象方法、类方法、协议合并的?
+> - 假设一个 Person 类存在2个 Category,分别是 Person+Work、Person+Study,都有对象方法 play,当实例对象调用 play 的时候,会调用最后编译的 Category (Person+Study)里的 play 实现。但为什么 load 不是调用最后一个 Category 的 +load,而是3个都调用?
+>
+> 本文主要探索这3个知识点
@@ -34,7 +40,9 @@
```
### 类别的作用
+
可以把类的实现分开在几个不同的源文件里,所以好处是:
+
- 减少耽搁文件的代码行数
- 可以把不痛的功能组织到不同的 category 里
- 可以由多个开发者共同完成一个大的类,方便协作
@@ -65,7 +73,7 @@
## Category 底层原理
-### Category 是 category_t 结构体
+### Category 的真面目是 category_t 结构体
来一个简单的 Person 类,为其添加一个 Category,增加一些属性和类方法、对象方法、遵循协议
@@ -247,6 +255,26 @@ static struct _category_t _OBJC_$_CATEGORY_Person_$_Study __attribute__ ((used,
};
```
+其中
+
+```c++
+struct _category_t {
+ const char *name;
+ struct _class_t *cls;
+ const struct _method_list_t *instance_methods;
+ const struct _method_list_t *class_methods;
+ const struct _protocol_list_t *protocols;
+ const struct _prop_list_t *properties;
+};
+```
+
+- `name` 是类的名字,不是 category 小括号里写的名字
+- `cls` 是要拓展的类对象,编译期间这个值不会有,在 runtime 加载时,才会根据 `name` 对应到类对象
+- `instance_methods `这个 category 所有的 `-` 方法(对象方法)
+- `class_methods` 这个 category 所有的 `+` 方法(类方法)
+- `protocols `这个 category 实现的 protocol
+- `properties `这个 category 所有的 property,这也是 category 里面可以定义属性的原因,不过不会 `@synthesize` 实例变量,一般有需求添加实例变量属性时会采用 `objc_setAssociatedObject` 和 `objc_getAssociatedObject` 方法绑定方法绑定。
+
`_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Study` 结构体存放的是对象方法信息,如下
```c++
@@ -305,6 +333,22 @@ static struct /*_prop_list_t*/ {
};
```
+最后,这个类的 category 们生成了一个数组,存在了 `__DATA` 段下的 `__objc_catlist` section 里
+
+```c++
+static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
+ &_OBJC_$_CATEGORY_Person_$_Study,
+};
+```
+
+
+
+
+
+看完上述信息,可以得出一个结论:
+
+在编译阶段,Class 和 Category 里面的数据是分开的。“将来”分类中的对象方法会塞到类对象中去,分类中的类方法会被塞到元类对象中去。这个“将来”是什么时候?后面继续探究
+
查看 [Objc 4 源代码](http://opensource.apple.com/tarballs/objc4/),Category 定义如下
```c++
@@ -337,15 +381,17 @@ struct category_t {
结论:
- 为某个类添加的分类,分类中可能有属性、对象方法、类方法、遵循的协议、协议方法,本质都是存储到 `category_t` 结构体里面。
-- 当有多个分类的时候,是通过分类数组来承载的
-
+- 当有多个分类的时候,是通过 category_t 数组来承载的
+
### category 中定义的方法,存储在哪?
抛个问题:当对象调用方法的时候,不管这个方法是类自身的方法,还是通过分类添加的方法,本质都是通过 isa 指针去寻找方法实现,(如果是对象方法,则通过 instance 的 isa 去找到类对象,最后找到对象方法的实现去调用;如果是类对象方法,则通过 class 的 isa 找到元类对象,最后找到类方法的实现进行调用),那给 Category 添加的方法,是「**如何“塞到”类对象或者元类对象的方法列表中去的**」?
-带着问题查看 [objc4 的源代码](http://opensource.apple.com/tarballs/objc4/) `objc-os.mm` 文件中的 `_objc_init` 方法
+带着问题查看 [objc4 的源代码](http://opensource.apple.com/tarballs/objc4/) `objc-os.mm` 文件中的 `_objc_init` 方法。
+
+在 library 加载前由 libSystem dyld 调用,进行初始化工作。
```c++
/***********************************************************************
@@ -367,7 +413,7 @@ void _objc_init(void) {
}
```
-`_objc_init` 内部会调用 `map_images` 方法,其内部如下
+`_objc_init` 内部会调用 `map_images` 方法,将文件中的 image(镜像)map 到内存,其内部如下
```c++
/***********************************************************************
@@ -527,7 +573,7 @@ void map_images_nolock(unsigned mhCount, const char * const mhPaths[],
}
```
- `map_images_nolock` 会调用 `_read_images` 方法,如下:
+ `map_images_nolock` 会调用 `_read_images` 方法,用于初始化 map 后的 `image`,这里面干了很多的事情,像 load所 有的类、协议和 category,著名的`+ load` 方法就是这一步调用的。如下:
```c++
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
@@ -910,6 +956,44 @@ void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int un
}
```
+仔细看 category 的初始化,循环调用了 `_getObjc2CategoryList` 方法,
+
+```c++
+// Look for a __DATA or __DATA_CONST or __DATA_DIRTY section
+// with the given name that stores an array of T.
+template
+T* getDataSection(const headerType *mhdr, const char *sectname,
+ size_t *outBytes, size_t *outCount)
+{
+ unsigned long byteCount = 0;
+ T* data = (T*)getsectiondata(mhdr, "__DATA", sectname, &byteCount);
+ if (!data) {
+ data = (T*)getsectiondata(mhdr, "__DATA_CONST", sectname, &byteCount);
+ }
+ if (!data) {
+ data = (T*)getsectiondata(mhdr, "__DATA_DIRTY", sectname, &byteCount);
+ }
+ if (outBytes) *outBytes = byteCount;
+ if (outCount) *outCount = byteCount / sizeof(T);
+ return data;
+}
+
+#define GETSECT(name, type, sectname) \
+ type *name(const headerType *mhdr, size_t *outCount) { \
+ return getDataSection(mhdr, sectname, nil, outCount); \
+ } \
+ type *name(const header_info *hi, size_t *outCount) { \
+ return getDataSection(hi->mhdr(), sectname, nil, outCount); \
+ }
+
+// function name content type section name
+GETSECT(_getObjc2CategoryList, category_t *, "__objc_catlist");
+```
+
+眼熟的 `__objc_catlist`,就是上面 category 存放的数据段了,可以串连起来了。
+
+
+
可以看到内部有 `Discover categories` 相关逻辑,里面和 category 方法相关的有 `remethodizeClass`,其实现如下
```c
@@ -930,7 +1014,7 @@ static void remethodizeClass(Class cls){
}
```
-可以看到内部调用 `attachCategories` 方法。 `attachCategories` 方法传入 3个参数,第一个是类对象(Person),第二个参数是 Category 数组。内部实现如下
+可以看到内部调用 `attachCategories` 方法。 `attachCategories` 方法传入 3个参数,第一个参数是类对象,比如 `[Person class]`,第二个参数是 Category 数组,比如 `[category_t(Person+Study), category_t(Person+Work)]`。内部实现如下
```c++
static void attachCategories(Class cls, category_list *cats, bool flush_caches){
@@ -939,10 +1023,13 @@ static void attachCategories(Class cls, category_list *cats, bool flush_caches){
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
+ // 方法数组,是"二维数组"。比如:[[personWork 对象方法1, personWork 对象方法2, personWork 对象方法3], [personStudy 对象方法1, personStudy 对象方法2, personStudy 对象方法3]]
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
+ // 属性数组,"二维数组"。比如:[[personWork 属性1, personWork 属性2, personWork 属性3], [personStudy 属性1, personStudy 属性2, personStudy 属性3]]
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
+ // 协议数组,"二维数组"。比如:[[personWork 协议1, personWork 协议2, personWork 协议3], [personStudy 协议1, personStudy 协议2, personStudy 协议3]]
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
@@ -953,35 +1040,40 @@ static void attachCategories(Class cls, category_list *cats, bool flush_caches){
int i = cats->count;
bool fromBundle = NO;
while (i--) {
+ // 取出某个分类。比如 Person+Work
auto& entry = cats->list[i];
-
+ // isMeta 为 NO,取出分类中的对象方法
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
+ // 将分类中的对象方法数组,放到 mlists 里面去。所以 mlists 是一个"二维数组"。由于 i 是 Category 数组总长度递减的,所以编译越后面的 Category 的对象方法越靠前
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
-
+ // 取出当前分类的属性数组
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
+ // 将当前分类的属性数组,放到 proplists 中去,所以 proplists 是一个"二维数组"
proplists[propcount++] = proplist;
}
-
+ // 取出当前分类的协议数组
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
+ // 将当前分类的协议数组,放到 protolists 中去,所以 protolists 是一个"二维数组"
protolists[protocount++] = protolist;
}
}
-
+ // 取出类对象的 class_rw_t
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
+ // 将所有分类的对象方法,附加到类对象的方法列表后面
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
-
+ // 将所有分类的属性,附加到类对象的属性列表后面
rw->properties.attachLists(proplists, propcount);
free(proplists);
-
+ // 将所有分类的协议,附加到类对象的协议列表后面
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
@@ -1030,6 +1122,7 @@ void attachLists(List* const * addedLists, uint32_t addedCount) {
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
+ // 下面会有解释
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
@@ -1084,10 +1177,14 @@ c 数组指针 `array()->lists + addedCount` 可以代表其中的位置。
过程如下
-
+
结果就是类方法列表中,最前面的就是所有分类的方法列表,最后是类自身的方法列表。
+效果为 `[[PersonWork Category 方法列表], [PersonStudy Category 方法列表], [Person类自身对象方法列表]]`
+
+
+
总结:
Category 编译之后 底层结构为 struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
@@ -1098,6 +1195,22 @@ Category 编译之后 底层结构为 struct category_t,里面存储着分类
### QA
+#### 为什么二维数组打了引号?
+
+`method_array_t` 中存储了 `method_list_t`,`method_list_t` 的元素为 `method_list_t`, 为什么不直接称它为二维数组?
+
+严格来说,不是二维数组,只不过是 array 里添加的对象也是 array,且各个数组不等长,也不会补空。
+
+为什么需要设计为这样的结构?
+
+调用方法,比如调用 load 是 runtime 加载的时候找到方法地址直接调用的。普通方法走的是消息机制,根据对象方法还是类方法,会根据isa 找类对象(对象方法)和元类对象(对象方法)信息中先从 cache中找方法是否有,没有再通过方法二维数组查找(二维是因为类存在分类,分类可能也有方法)没找到则通过 superclass 继续找父类对象或者父元类对象继续找,找到则执行并给当前类的 cache 方法散列表缓存下来。找到NSObject 还是没找到则走消息转发机制,起死回生几个阶段
+
+ 另外 load 调用会根据编译顺序决定,如果遇到某个类存在父类则先调用父类的load、再执行子类的 load。category 会按照编译顺序,runtime 会给方法进行重新组合顺序,源码显示最后 category 的方法会排到最前面。
+
+ 本类的 class method List 和 category methodList 都是 array,category List 会插在本类 method List 前面,匹配到 category method List 同名方法,本类就不调用了,看着有点像被"覆盖"了。
+
+
+
#### 分类中可以写属性吗?
不可以。从源码角度来讲,查看分类的 category_t 结构体可以看到没有 `const ivar_list_t * ivars;` ,所以 category 声明属性底层只会生成 setter、getter 方法声明,没有实现。需要程序员利用 runtime 关联属性自己实现
@@ -1199,7 +1312,7 @@ Demo: 为 Person 类创建2个 Category,分别存在同名方法 study,具
- 只存在一个文件
- 命名方式:“类名_拓展名.h”
-
+
```
#import "类名.h"
@interface 类名 ()
@@ -1217,7 +1330,7 @@ Demo: 为 Person 类创建2个 Category,分别存在同名方法 study,具
4. 和 Category 类似,但是小括号里面没有拓展的名字
-5. 拓展里面的属性和方法,会在编译阶段将相关数据和类本身合并
+5. 拓展里面的属性和方法,会在编译阶段将相关数据和类本身合并(分类 Category 在编译期无法确定,只有在运行时才会合并到类对象、元类对象的属性、方法列表中去)
### 拓展的局限性
@@ -1226,7 +1339,7 @@ Demo: 为 Person 类创建2个 Category,分别存在同名方法 study,具
2. 通常 Extension 都写在 .m 文件中,不会单独建立一个 Extension 文件。而且 Extension 必须写到 @implementation 上方,否则编译报错
3. 类拓展定义的方法和属性必须在类的实现文件中实现。如果单独定义类扩展的文件并且只定义属性的话,也需要将类实现文件中包含进类扩展文件,否则会找不到属性的 setter 和 getter 方法。
-
+
```objectivec
//Web.h
#import "Person.h"
@@ -1253,17 +1366,39 @@ Demo: 为 Person 类创建2个 Category,分别存在同名方法 study,具
@end
```
+4. 不能为系统类添加拓展
+
+
+
+### QA
+
+Category 的实现原理是什么?
+
+Category 编译之后的底层实现是 struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息。
+
+程序运行过程中,runtime 会将 Category 的数据,合并到类信息中(类对象、元类对象)
+
+
+
+Category 和 Class Extension 的区别是什么?
+
+Class Extension 在编译的时候,它的数据就已经包含在类信息中。
+
+Category 是在运行时,才将数据合并到类信息中(类对象、元类对象)
+
+
+
## 总结
-1. Category 只能拓充方法,不能拓展属性和成员变量(包含成员变量会报错。属性虽然不可以直接拓展,利用 Runtime 可以实现)
-
+1. Category 能拓充实例方法、类方法、协议、属性(属性虽然不可以直接拓展,利用 Runtime 关联属性可以实现)、不能拓展成员变量(包含成员变量会报错)
2. 如果 Category 中声明了1个属性,那么 Category 只会生成 setter 和 getter 的声明,不会有实现
+3. 分类的方法本质是追加在当前类方法列表后,所以分类的方法会覆盖当前类的方法。
+4. Category 具有运行时决议的特点。为某个类添加的 Category 被 runtime 加载后,原本来的方法,可能会被优先找到分类中的方法实现,而“覆盖”(假的覆盖,只是优先查找到 category 的方法实现的时候,原始类的方法查找中止而已)
+5. Category 可以为系统类添加分类,而拓展不行。
-3. Extension 也被成为匿名的 Category
-4. 分类的方法本质是追加在当前类方法列表后,所以分类的方法会覆盖当前类的方法。
-关于第4点,我们可以查看源代码印证下。去 opensource 下载 objc4
+关于第3点,我们可以查看源代码印证下。去 opensource 下载 objc4
OC 入口函数`_objc_init`
@@ -1433,8 +1568,1073 @@ void attachLists(List* const * addedLists, uint32_t addedCount) {
+## 底层剖析 load 方法
+
+假设一个 Person 类存在2个 Category,分别是 Person+Work、Person+Study,都有对象方法 play,当实例对象调用 play 的时候,会调用最后编译的 Category (Person+Study)里的 play 实现。但为什么 load 不是调用最后一个 Category 的 +load,而是3个都调用?
+
+Demo 验证
+
+```objectivec
+@interface Person : NSObject
+@end
+
+@interface Student : Person
+@end
+
+@interface Student (Good)
+@end
+
+@interface Student (Bad)
+@end
+// 其中每个类都存在3个方法
++ (void)load{
+ NSLog(@"%s", __func__);
+}
++ (void)initialize{
+ NSLog(@"%s", __func__);
+}
+- (void)test{
+ NSLog(@"%s", __func__);
+}
+// Test
+Student *st = [[Student alloc] init];
+
+2022-04-16 01:35:22.237692+0800 Main[8752:2908124] +[Person load]
+2022-04-16 01:35:22.238305+0800 Main[8752:2908124] +[Student load]
+2022-04-16 01:35:22.238450+0800 Main[8752:2908124] +[Student(Good) load]
+2022-04-16 01:35:22.238562+0800 Main[8752:2908124] +[Student(Bad) load]
+2022-04-16 01:35:22.238664+0800 Main[8752:2908124] +[Person initialize]
+2022-04-16 01:35:22.238733+0800 Main[8752:2908124] +[Student(Bad) initialize]
+2022-04-16 01:35:22.238794+0800 Main[8752:2908124] -[Student(Bad) test]
+```
+
+
+
+### 为什么 load 方法不是按照 Category 编译顺序倒序调用 load 方法?
+
+看源代码 Objc4(我的版本是 objc4-838.1)
+
+```c
+// objc-os.mm
+/***********************************************************************
+* _objc_init
+* Bootstrap initialization. Registers our image notifier with dyld.
+* Called by libSystem BEFORE library initialization time
+**********************************************************************/
+// Objc 启动会调用该方法,内部会调用 _dyld_objc_notify_register。其中 load_images 方法用于加载模块
+void _objc_init(void)
+{
+ static bool initialized = false;
+ if (initialized) return;
+ initialized = true;
+
+ // fixme defer initialization until an objc-using image is found?
+ environ_init();
+ tls_init();
+ static_init();
+ runtime_init();
+ exception_init();
+#if __OBJC2__
+ cache_t::init();
+#endif
+ _imp_implementationWithBlock_init();
+
+ _dyld_objc_notify_register(&map_images, load_images, unmap_image);
+
+#if __OBJC2__
+ didCallDyldNotifyRegister = true;
+#endif
+}
+
+// objc-runtime-new.mm
+// load_images 方法中最后会调用 call_load_methods 方法,用于调用全部的 +(void)load 方法
+void
+load_images(const char *path __unused, const struct mach_header *mh)
+{
+ if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
+ didInitialAttachCategories = true;
+ loadAllCategories();
+ }
+
+ // Return without taking locks if there are no +load methods here.
+ if (!hasLoadMethods((const headerType *)mh)) return;
+
+ recursive_mutex_locker_t lock(loadMethodLock);
+
+ // Discover load methods
+ {
+ mutex_locker_t lock2(runtimeLock);
+ prepare_load_methods((const headerType *)mh);
+ }
+
+ // Call +load methods (without runtimeLock - re-entrant)
+ call_load_methods();
+}
+
+// objc-loadmethod.mm
+/***********************************************************************
+* call_load_methods
+* Call all pending class and category +load methods.
+* Class +load methods are called superclass-first.
+* Category +load methods are not called until after the parent class's +load.
+*
+* This method must be RE-ENTRANT, because a +load could trigger
+* more image mapping. In addition, the superclass-first ordering
+* must be preserved in the face of re-entrant calls. Therefore,
+* only the OUTERMOST call of this function will do anything, and
+* that call will handle all loadable classes, even those generated
+* while it was running.
+*
+* The sequence below preserves +load ordering in the face of
+* image loading during a +load, and make sure that no
+* +load method is forgotten because it was added during
+* a +load call.
+* Sequence:
+* 1. Repeatedly call class +loads until there aren't any more
+* 2. Call category +loads ONCE.
+* 3. Run more +loads if:
+* (a) there are more classes to load, OR
+* (b) there are some potential category +loads that have
+* still never been attempted.
+* Category +loads are only run once to ensure "parent class first"
+* ordering, even if a category +load triggers a new loadable class
+* and a new loadable category attached to that class.
+*
+* Locking: loadMethodLock must be held by the caller
+* All other locks must not be held.
+**********************************************************************/
+// 在调用 call_load_methods 方法内部,会先调用类的 +load 方法,再调用分类的 +load
+void call_load_methods(void)
+{
+ static bool loading = NO;
+ bool more_categories;
+
+ loadMethodLock.assertLocked();
+
+ // Re-entrant calls do nothing; the outermost call will finish the job.
+ if (loading) return;
+ loading = YES;
+
+ void *pool = objc_autoreleasePoolPush();
+
+ do {
+ // 1. Repeatedly call class +loads until there aren't any more
+ // 先调用全部类的 +load
+ while (loadable_classes_used > 0) {
+ call_class_loads();
+ }
+
+ // 2. Call category +loads ONCE
+ // 再调用分类的 +load
+ more_categories = call_category_loads();
+
+ // 3. Run more +loads if there are classes OR more untried categories
+ } while (loadable_classes_used > 0 || more_categories);
+
+ objc_autoreleasePoolPop(pool);
+
+ loading = NO;
+}
+
+/***********************************************************************
+* call_class_loads
+* Call all pending class +load methods.
+* If new classes become loadable, +load is NOT called for them.
+*
+* Called only by call_load_methods().
+**********************************************************************/
+// 该方法用于调用全部类的 +load 方法
+static void call_class_loads(void)
+{
+ int i;
+
+ // Detach current loadable list.
+ struct loadable_class *classes = loadable_classes;
+ int used = loadable_classes_used;
+ loadable_classes = nil;
+ loadable_classes_allocated = 0;
+ loadable_classes_used = 0;
+
+ // Call all +loads for the detached list.
+ for (i = 0; i < used; i++) {
+ Class cls = classes[i].cls;
+ // 从可加载的类数组 classes 中取出当前类的 method.这个类是 loadable_class 结构体,只有2个成员,cls 和 method,所以该 method 就是 +load 方法
+ load_method_t load_method = (load_method_t)classes[i].method;
+ if (!cls) continue;
+
+ if (PrintLoading) {
+ _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
+ }
+ // 直接调用 +load 方法,而不是采用发消息的形式
+ (*load_method)(cls, @selector(load));
+ }
+
+ // Destroy the detached list.
+ if (classes) free(classes);
+}
+
+typedef void(*load_method_t)(id, SEL);
+
+struct loadable_class {
+ Class cls; // may be nil
+ IMP method;
+};
+
+struct loadable_category {
+ Category cat; // may be nil
+ IMP method;
+};
+
+/***********************************************************************
+* call_category_loads
+* Call some pending category +load methods.
+* The parent class of the +load-implementing categories has all of
+* its categories attached, in case some are lazily waiting for +initalize.
+* Don't call +load unless the parent class is connected.
+* If new categories become loadable, +load is NOT called, and they
+* are added to the end of the loadable list, and we return TRUE.
+* Return FALSE if no new categories became loadable.
+*
+* Called only by call_load_methods().
+**********************************************************************/
+// 该方法用于调用分类的 +load 方法
+static bool call_category_loads(void)
+{
+ int i, shift;
+ bool new_categories_added = NO;
+
+ // Detach current loadable list.
+ struct loadable_category *cats = loadable_categories;
+ int used = loadable_categories_used;
+ int allocated = loadable_categories_allocated;
+ loadable_categories = nil;
+ loadable_categories_allocated = 0;
+ loadable_categories_used = 0;
+
+ // Call all +loads for the detached list.
+ for (i = 0; i < used; i++) {
+ Category cat = cats[i].cat;
+ // 从可加载的分类数组 cats 中取出当前分类,这个类是 loadable_category 结构体,只有2个成员,cat 和 method,所以该 method 就是 +load 方法
+ load_method_t load_method = (load_method_t)cats[i].method;
+ Class cls;
+ if (!cat) continue;
+
+ cls = _category_getClass(cat);
+ if (cls && cls->isLoadable()) {
+ if (PrintLoading) {
+ _objc_inform("LOAD: +[%s(%s) load]\n",
+ cls->nameForLogging(),
+ _category_getName(cat));
+ }
+ // 直接调用 Category 的 +load,不是采用发消息的方式
+ (*load_method)(cls, @selector(load));
+ cats[i].cat = nil;
+ }
+ }
+
+ // Compact detached list (order-preserving)
+ shift = 0;
+ for (i = 0; i < used; i++) {
+ if (cats[i].cat) {
+ cats[i-shift] = cats[i];
+ } else {
+ shift++;
+ }
+ }
+ used -= shift;
+
+ // Copy any new +load candidates from the new list to the detached list.
+ new_categories_added = (loadable_categories_used > 0);
+ for (i = 0; i < loadable_categories_used; i++) {
+ if (used == allocated) {
+ allocated = allocated*2 + 16;
+ cats = (struct loadable_category *)
+ realloc(cats, allocated *
+ sizeof(struct loadable_category));
+ }
+ cats[used++] = loadable_categories[i];
+ }
+
+ // Destroy the new list.
+ if (loadable_categories) free(loadable_categories);
+
+ // Reattach the (now augmented) detached list.
+ // But if there's nothing left to load, destroy the list.
+ if (used) {
+ loadable_categories = cats;
+ loadable_categories_used = used;
+ loadable_categories_allocated = allocated;
+ } else {
+ if (cats) free(cats);
+ loadable_categories = nil;
+ loadable_categories_used = 0;
+ loadable_categories_allocated = 0;
+ }
+
+ if (PrintLoading) {
+ if (loadable_categories_used != 0) {
+ _objc_inform("LOAD: %d categories still waiting for +load\n",
+ loadable_categories_used);
+ }
+ }
+
+ return new_categories_added;
+}
+```
+
+阅读源码发现:
+
+1. 在调用 `+load` 方法的时候,系统会先调用(可加载)类的 `+load` 方法,再调用分类的 `+load` 方法
+2. `call_class_loads`、`call_category_loads` 方法内部实现,是通过 `loadable_class` `loadable_category` 结构体的 method 成员值 ,通过 `load_method_t load_method = (load_method_t)classes[i].method` 找到 `+ load` 方法地址。最后直接调用 `(*load_method)(cls, SEL_load)` 方法本身,没有采用消息机制。
+
+
+
+### 为什么总是父类的 +load 比子类先执行?
+
+无论在 Xcode 中怎么调节编译顺序,发现总是父类的 +load 比子类先执行,为什么?还是从源码角度白盒探究下。
+
+梳理下思路:关于 +load 的执行顺序,类的 +load 比分类先执行,这一点认知是拉齐的吧。那我们聚焦下,看看类里面父类、子类这样的情况是如何调用 +load 的 。
+
+```c++
+static void call_class_loads(void)
+{
+ int i;
+
+ // Detach current loadable list.
+ struct loadable_class *classes = loadable_classes;
+ int used = loadable_classes_used;
+ loadable_classes = nil;
+ loadable_classes_allocated = 0;
+ loadable_classes_used = 0;
+
+ // Call all +loads for the detached list.
+ for (i = 0; i < used; i++) {
+ Class cls = classes[i].cls;
+ load_method_t load_method = (load_method_t)classes[i].method;
+ if (!cls) continue;
+
+ if (PrintLoading) {
+ _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
+ }
+ (*load_method)(cls, @selector(load));
+ }
+
+ // Destroy the detached list.
+ if (classes) free(classes);
+}
+```
+
+我们在类的 +load 执行方法 `call_class_loads` 里看到,方法内部说顺序遍历并执行 +load 的。看到 `loadable_classes` 很可疑。它是不是决定了父类、子类的顺序?
+
+```c++
+void
+load_images(const char *path __unused, const struct mach_header *mh)
+{
+ if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
+ didInitialAttachCategories = true;
+ loadAllCategories();
+ }
+
+ // Return without taking locks if there are no +load methods here.
+ if (!hasLoadMethods((const headerType *)mh)) return;
+
+ recursive_mutex_locker_t lock(loadMethodLock);
+
+ // Discover load methods
+ {
+ mutex_locker_t lock2(runtimeLock);
+ prepare_load_methods((const headerType *)mh);
+ }
+
+ // Call +load methods (without runtimeLock - re-entrant)
+ call_load_methods();
+}
+```
+
+发现在调用 `call_load_methods` 方法之前,有个 `prepare_load_methods` 方法。看名字,感觉像是在做调用 +load 方法前的准备工作。
+
+```c++
+void prepare_load_methods(const headerType *mhdr)
+{
+ size_t count, i;
+
+ runtimeLock.assertLocked();
+
+ classref_t const *classlist =
+ _getObjc2NonlazyClassList(mhdr, &count);
+ for (i = 0; i < count; i++) {
+ // 做一些前置定制动作
+ schedule_class_load(remapClass(classlist[i]));
+ }
+
+ category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
+ for (i = 0; i < count; i++) {
+ category_t *cat = categorylist[i];
+ Class cls = remapClass(cat->cls);
+ if (!cls) continue; // category for ignored weak-linked class
+ if (cls->isSwiftStable()) {
+ _objc_fatal("Swift class extensions and categories on Swift "
+ "classes are not allowed to have +load methods");
+ }
+ realizeClassWithoutSwift(cls, nil);
+ ASSERT(cls->ISA()->isRealized());
+
+ add_category_to_loadable_list(cat);
+ }
+}
+
+/***********************************************************************
+* prepare_load_methods
+* Schedule +load for classes in this image, any un-+load-ed
+* superclasses in other images, and any categories in this image.
+**********************************************************************/
+// Recursively schedule +load for cls and any un-+load-ed superclasses.
+// cls must already be connected.
+static void schedule_class_load(Class cls)
+{
+ if (!cls) return;
+ ASSERT(cls->isRealized()); // _read_images should realize
+
+ if (cls->data()->flags & RW_LOADED) return;
+
+ // Ensure superclass-first ordering
+ // 递归调用,传入当前类的父类
+ schedule_class_load(cls->getSuperclass());
+ // 将 cls 添加到 loadable_classes 数组的最后面
+ add_class_to_loadable_list(cls);
+ cls->setInfo(RW_LOADED);
+}
+```
+
+通过源码,我们可以看出:
+
+- 系统会通过 `schedule_class_load` 方法,保证优先调用当前类的全部父类的加入到 `loadable_classes`,然后将当前类加入到 `loadable_classes`
+- 最后执行 +load 的时候会按照 `loadable_classes` 里的顺序,依次调用 +load 方法
+
+顺便看看分类的调用顺序是怎么控制的?
+
+```c++
+void prepare_load_methods(const headerType *mhdr)
+{
+ size_t count, i;
+
+ runtimeLock.assertLocked();
+
+ classref_t const *classlist =
+ _getObjc2NonlazyClassList(mhdr, &count);
+ for (i = 0; i < count; i++) {
+ schedule_class_load(remapClass(classlist[i]));
+ }
+
+ category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
+ for (i = 0; i < count; i++) {
+ category_t *cat = categorylist[i];
+ Class cls = remapClass(cat->cls);
+ if (!cls) continue; // category for ignored weak-linked class
+ if (cls->isSwiftStable()) {
+ _objc_fatal("Swift class extensions and categories on Swift "
+ "classes are not allowed to have +load methods");
+ }
+ realizeClassWithoutSwift(cls, nil);
+ ASSERT(cls->ISA()->isRealized());
+ add_category_to_loadable_list(cat);
+ }
+}
+
+/***********************************************************************
+* add_category_to_loadable_list
+* Category cat's parent class exists and the category has been attached
+* to its class. Schedule this category for +load after its parent class
+* becomes connected and has its own +load method called.
+**********************************************************************/
+void add_category_to_loadable_list(Category cat)
+{
+ IMP method;
+
+ loadMethodLock.assertLocked();
+
+ method = _category_getLoadMethod(cat);
+
+ // Don't bother if cat has no +load method
+ if (!method) return;
+
+ if (PrintLoading) {
+ _objc_inform("LOAD: category '%s(%s)' scheduled for +load",
+ _category_getClassName(cat), _category_getName(cat));
+ }
+
+ if (loadable_categories_used == loadable_categories_allocated) {
+ loadable_categories_allocated = loadable_categories_allocated*2 + 16;
+ loadable_categories = (struct loadable_category *)
+ realloc(loadable_categories,
+ loadable_categories_allocated *
+ sizeof(struct loadable_category));
+ }
+
+ loadable_categories[loadable_categories_used].cat = cat;
+ loadable_categories[loadable_categories_used].method = method;
+ loadable_categories_used++;
+}
+```
+
+可以看到通过 `schedule_class_load` 处理完类之后,分类是直接通过 `_getObjc2NonlazyCategoryList(mhdr, &count)` 获取的,之后也是直接添加到 `loadable_categories` 中去。
+
+
+
+### +load 总结
+
+- `+load` 方法是系统通过 runtime 在加载类、分类的时候调用的
+- 每个类、分类的 `+load` 在程序运行过程中只调用1次
+- 调用顺序方面:
+ - 先调用类的 `+load` 方法
+ - 存在继承关系的话,调用子类的 `+load` 之前会调用父类的 `+load` 方法(runtime 会保证好,先调用父类的 `+load` ,再调用子类的 `+load` )
+ - 不存在继承关系的话,会按照编译顺序调用 `+load`(先编译的先调用)
+ - 再调用分类的 `+load` 方法
+ - 按照编译顺序调用 `+load`(先编译的先调用)
+
+
+
+### QA
+
+1.Category 中有 `+load` 方法吗?`+load` 调用时机是什么时候?`+load` 可以继承吗?
+
+Category 存在 `+load` 方法。`+load` 是系统在启动阶段通过 runtime 来加载、准备(通过递归的手段保证,如果当前类存在父类,则会加入到 `loadable_classes` 中去),然后先调用类的 `+load`,再调用分类的 `+load`。
+
+`+load` 可以继承。但是这个问法背后的想法有点神奇,因为继承的本质是面向对象,类继承了,方法会重写逻辑。但是 iOS 官方文档说 `+load` 适合做和本类相关的逻辑。所以这个继承就显得不那么合理。但是可以继承的,比如下面的代码:
+
+```objective-c
+@interface Person : NSObject
+@end
+
+@implementation Person
++ (load) {
+ NSLog(@"Person +load");
+}
+@end
+
+@interface Student : Person
+@end
+@implementation Person
+@end
+
+[Student load];
+// console
+Person +load
+```
+
+可以看到,我们主动调用 `[Student load]` 由于 Student 自身没有实现 `+load`,由于存在继承,所以调用了 Person 的 `+load` 。手动调用 `+load` 本质上就是给 runtime 消息机制,等价于 `objc_msgSend([Student class], @selector(load))` ,通过 Student 类对象的 isa,找到 Student 元类对象,判断有没有类方法 `+load`,发现没有,则根据 Student 元类对象的 `superclass` 找到父类的元类对象,也就是 Person 的元类对象,发现有 `+load` 实现,即调用了 `+load` ,打印 `Person +load`
+
+
+
+2.为什么 load 方法打印顺序是这样的?
+
+因为调用 student alloc,相当于发送了消息。则肯定先执行 load 方法。类在 Runtime 启动阶段会调用 `schedule_class_load` 方法。方法内部递归调用,如果当前类存在父类则递归调用,否则将当前类加载到 loadable_classes 最后面。load 方法在本质上是执行 `call_load_methods`,方法地址是确定的(查看下面的源代码可以发现 `load` 方法是在编译期就可以确定的)。不走 `objc_msgSend` 这套流程。所以先打印父类 load、再打印子类 load、最后打印分类 load。如果存在多个分类,则按照编译顺序打印 load。
+
+
+
+## 底层剖析 Initialize 方法
+
+上 Demo
+
+```objectivec
+@interface Person : NSObject
+@end
+
+@interface Student : Person
+@end
+
+@interface Student (Good)
+@end
+```
+
+`Person *p1 = [[Person alloc] init];` 这句代码输出什么?
+
+这个比较简单,initialize 方法在类第一次收到消息的时候调用。所以输出 `+[Person initialize]`
+
+`Student *st = [[Student alloc] init];` 输出什么?
+
+```objectivec
++[Person initialize]
++[Student(Good) initialize]
+```
+
+查看分类在 Runtime 加载类信息时候的调用原理可以知道,分类中的类方法、对象方法都会被加载原始类的前面去(initialize 是类方法)如下图:
+
+
+
+ ### 为什么给子类发消息,父类和子类的 +initialize 都会被调用?且父类的先调用
+
+梳理下目前掌握的信息:当类第一次接收消息,也就是第一次调用对象方法的时候,该类的 `+initialize` 方法会被调用。所以需要从 objc_msgSend 或者 获取对象方法、的角度去查看源码。聚焦下,以 `class_getInstanceMethod` 方法为入口,分析源码
+
+```c++
+/***********************************************************************
+* class_getInstanceMethod. Return the instance method for the
+* specified class and selector.
+**********************************************************************/
+Method class_getInstanceMethod(Class cls, SEL sel)
+{
+ if (!cls || !sel) return nil;
+
+ // This deliberately avoids +initialize because it historically did so.
+
+ // This implementation is a bit weird because it's the only place that
+ // wants a Method instead of an IMP.
+
+#warning fixme build and search caches
+
+ // Search method lists, try method resolver, etc.
+ lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
+
+#warning fixme build and search caches
+
+ return _class_getMethod(cls, sel);
+}
+```
+
+内部调用的是 `lookUpImpOrForward`
+
+```c++
+NEVER_INLINE
+IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
+{
+ const IMP forward_imp = (IMP)_objc_msgForward_impcache;
+ IMP imp = nil;
+ Class curClass;
+
+ runtimeLock.assertUnlocked();
+
+ if (slowpath(!cls->isInitialized())) {
+ // The first message sent to a class is often +new or +alloc, or +self
+ // which goes through objc_opt_* or various optimized entry points.
+ //
+ // However, the class isn't realized/initialized yet at this point,
+ // and the optimized entry points fall down through objc_msgSend,
+ // which ends up here.
+ //
+ // We really want to avoid caching these, as it can cause IMP caches
+ // to be made with a single entry forever.
+ //
+ // Note that this check is racy as several threads might try to
+ // message a given class for the first time at the same time,
+ // in which case we might cache anyway.
+ behavior |= LOOKUP_NOCACHE;
+ }
+
+ // runtimeLock is held during isRealized and isInitialized checking
+ // to prevent races against concurrent realization.
+
+ // runtimeLock is held during method search to make
+ // method-lookup + cache-fill atomic with respect to method addition.
+ // Otherwise, a category could be added but ignored indefinitely because
+ // the cache was re-filled with the old value after the cache flush on
+ // behalf of the category.
+
+ runtimeLock.lock();
+
+ // We don't want people to be able to craft a binary blob that looks like
+ // a class but really isn't one and do a CFI attack.
+ //
+ // To make these harder we want to make sure this is a class that was
+ // either built into the binary or legitimately registered through
+ // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
+ checkIsKnownClass(cls);
+ // 关注这里
+ cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
+ // runtimeLock may have been dropped but is now locked again
+ runtimeLock.assertLocked();
+ curClass = cls;
+
+ // The code used to lookup the class's cache again right after
+ // we take the lock but for the vast majority of the cases
+ // evidence shows this is a miss most of the time, hence a time loss.
+ //
+ // The only codepath calling into this without having performed some
+ // kind of cache lookup is class_getInstanceMethod().
+
+ for (unsigned attempts = unreasonableClassCount();;) {
+ if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
+#if CONFIG_USE_PREOPT_CACHES
+ imp = cache_getImp(curClass, sel);
+ if (imp) goto done_unlock;
+ curClass = curClass->cache.preoptFallbackClass();
+#endif
+ } else {
+ // curClass method list.
+ method_t *meth = getMethodNoSuper_nolock(curClass, sel);
+ if (meth) {
+ imp = meth->imp(false);
+ goto done;
+ }
+
+ if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
+ // No implementation found, and method resolver didn't help.
+ // Use forwarding.
+ imp = forward_imp;
+ break;
+ }
+ }
+
+ // Halt if there is a cycle in the superclass chain.
+ if (slowpath(--attempts == 0)) {
+ _objc_fatal("Memory corruption in class list.");
+ }
+
+ // Superclass cache.
+ imp = cache_getImp(curClass, sel);
+ if (slowpath(imp == forward_imp)) {
+ // Found a forward:: entry in a superclass.
+ // Stop searching, but don't cache yet; call method
+ // resolver for this class first.
+ break;
+ }
+ if (fastpath(imp)) {
+ // Found the method in a superclass. Cache it in this class.
+ goto done;
+ }
+ }
+
+ // No implementation found. Try method resolver once.
+
+ if (slowpath(behavior & LOOKUP_RESOLVER)) {
+ behavior ^= LOOKUP_RESOLVER;
+ return resolveMethod_locked(inst, sel, cls, behavior);
+ }
+
+ done:
+ if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
+#if CONFIG_USE_PREOPT_CACHES
+ while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
+ cls = cls->cache.preoptFallbackClass();
+ }
+#endif
+ log_and_fill_cache(cls, imp, sel, inst, curClass);
+ }
+ done_unlock:
+ runtimeLock.unlock();
+ if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
+ return nil;
+ }
+ return imp;
+}
+```
+
+` cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);` 看上去是实现 initialize 相关逻辑的。
+
+```c++
+/***********************************************************************
+* realizeAndInitializeIfNeeded_locked
+* Realize the given class if not already realized, and initialize it if
+* not already initialized.
+* inst is an instance of cls or a subclass, or nil if none is known.
+* cls is the class to initialize and realize.
+* initializer is true to initialize the class, false to skip initialization.
+**********************************************************************/
+static Class
+realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize)
+{
+ runtimeLock.assertLocked();
+ if (slowpath(!cls->isRealized())) {
+ cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
+ // runtimeLock may have been dropped but is now locked again
+ }
+ // 需要初始化并且没有被初始化过,则执行 if 里面的逻辑
+ if (slowpath(initialize && !cls->isInitialized())) {
+ cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
+ // runtimeLock may have been dropped but is now locked again
+
+ // If sel == initialize, class_initialize will send +initialize and
+ // then the messenger will send +initialize again after this
+ // procedure finishes. Of course, if this is not being called
+ // from the messenger then it won't happen. 2778172
+ }
+ return cls;
+}
+```
+
+`realizeAndInitializeIfNeeded_locked` 内部判断,当 class 需要被初始化且没有初始化过的时候则执行 `initializeAndLeaveLocked`
+
+```
+// Locking: caller must hold runtimeLock; this may drop and re-acquire it
+static Class initializeAndLeaveLocked(Class cls, id obj, mutex_t& lock)
+{
+ return initializeAndMaybeRelock(cls, obj, lock, true);
+}
+
+/***********************************************************************
+* class_initialize. Send the '+initialize' message on demand to any
+* uninitialized class. Force initialization of superclasses first.
+* inst is an instance of cls, or nil. Non-nil is better for performance.
+* Returns the class pointer. If the class was unrealized then
+* it may be reallocated.
+* Locking:
+* runtimeLock must be held by the caller
+* This function may drop the lock.
+* On exit the lock is re-acquired or dropped as requested by leaveLocked.
+**********************************************************************/
+static Class initializeAndMaybeRelock(Class cls, id inst,
+ mutex_t& lock, bool leaveLocked)
+{
+ lock.assertLocked();
+ ASSERT(cls->isRealized());
+
+ if (cls->isInitialized()) {
+ if (!leaveLocked) lock.unlock();
+ return cls;
+ }
+
+ // Find the non-meta class for cls, if it is not already one.
+ // The +initialize message is sent to the non-meta class object.
+ Class nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
+
+ // Realize the non-meta class if necessary.
+ if (nonmeta->isRealized()) {
+ // nonmeta is cls, which was already realized
+ // OR nonmeta is distinct, but is already realized
+ // - nothing else to do
+ lock.unlock();
+ } else {
+ nonmeta = realizeClassMaybeSwiftAndUnlock(nonmeta, lock);
+ // runtimeLock is now unlocked
+ // fixme Swift can't relocate the class today,
+ // but someday it will:
+ cls = object_getClass(nonmeta);
+ }
+
+ // runtimeLock is now unlocked, for +initialize dispatch
+ ASSERT(nonmeta->isRealized());
+ initializeNonMetaClass(nonmeta);
+
+ if (leaveLocked) runtimeLock.lock();
+ return cls;
+}
+```
+
+重点在 `initializeNonMetaClass` 方法中
+
+```c++
+/***********************************************************************
+* class_initialize. Send the '+initialize' message on demand to any
+* uninitialized class. Force initialization of superclasses first.
+**********************************************************************/
+void initializeNonMetaClass(Class cls)
+{
+ ASSERT(!cls->isMetaClass());
+
+ Class supercls;
+ bool reallyInitialize = NO;
+
+ // Make sure super is done initializing BEFORE beginning to initialize cls.
+ // See note about deadlock above.
+ supercls = cls->getSuperclass();
+ // 存在父类,并且父类没有被初始化,则递归调用 initializeNonMetaClass
+ if (supercls && !supercls->isInitialized()) {
+ initializeNonMetaClass(supercls);
+ }
+
+ // Try to atomically set CLS_INITIALIZING.
+ SmallVector<_objc_willInitializeClassCallback, 1> localWillInitializeFuncs;
+ {
+ monitor_locker_t lock(classInitLock);
+ if (!cls->isInitialized() && !cls->isInitializing()) {
+ cls->setInitializing();
+ reallyInitialize = YES;
+
+ // Grab a copy of the will-initialize funcs with the lock held.
+ localWillInitializeFuncs.initFrom(willInitializeFuncs);
+ }
+ }
+
+ if (reallyInitialize) {
+ // We successfully set the CLS_INITIALIZING bit. Initialize the class.
+
+ // Record that we're initializing this class so we can message it.
+ _setThisThreadIsInitializingClass(cls);
+
+ if (MultithreadedForkChild) {
+ // LOL JK we don't really call +initialize methods after fork().
+ performForkChildInitialize(cls, supercls);
+ return;
+ }
+
+ for (auto callback : localWillInitializeFuncs)
+ callback.f(callback.context, cls);
+
+ // Send the +initialize message.
+ // Note that +initialize is sent to the superclass (again) if
+ // this class doesn't implement +initialize. 2157218
+ if (PrintInitializing) {
+ _objc_inform("INITIALIZE: thread %p: calling +[%s initialize]",
+ objc_thread_self(), cls->nameForLogging());
+ }
+
+ // Exceptions: A +initialize call that throws an exception
+ // is deemed to be a complete and successful +initialize.
+ //
+ // Only __OBJC2__ adds these handlers. !__OBJC2__ has a
+ // bootstrapping problem of this versus CF's call to
+ // objc_exception_set_functions().
+#if __OBJC2__
+ @try
+#endif
+ {
+ callInitialize(cls);
+
+ if (PrintInitializing) {
+ _objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
+ objc_thread_self(), cls->nameForLogging());
+ }
+ }
+#if __OBJC2__
+ @catch (...) {
+ if (PrintInitializing) {
+ _objc_inform("INITIALIZE: thread %p: +[%s initialize] "
+ "threw an exception",
+ objc_thread_self(), cls->nameForLogging());
+ }
+ @throw;
+ }
+ @finally
+#endif
+ {
+ // Done initializing.
+ lockAndFinishInitializing(cls, supercls);
+ }
+ return;
+ }
+
+ else if (cls->isInitializing()) {
+ // We couldn't set INITIALIZING because INITIALIZING was already set.
+ // If this thread set it earlier, continue normally.
+ // If some other thread set it, block until initialize is done.
+ // It's ok if INITIALIZING changes to INITIALIZED while we're here,
+ // because we safely check for INITIALIZED inside the lock
+ // before blocking.
+ if (_thisThreadIsInitializingClass(cls)) {
+ return;
+ } else if (!MultithreadedForkChild) {
+ waitForInitializeToComplete(cls);
+ return;
+ } else {
+ // We're on the child side of fork(), facing a class that
+ // was initializing by some other thread when fork() was called.
+ _setThisThreadIsInitializingClass(cls);
+ performForkChildInitialize(cls, supercls);
+ }
+ }
+
+ else if (cls->isInitialized()) {
+ // Set CLS_INITIALIZING failed because someone else already
+ // initialized the class. Continue normally.
+ // NOTE this check must come AFTER the ISINITIALIZING case.
+ // Otherwise: Another thread is initializing this class. ISINITIALIZED
+ // is false. Skip this clause. Then the other thread finishes
+ // initialization and sets INITIALIZING=no and INITIALIZED=yes.
+ // Skip the ISINITIALIZING clause. Die horribly.
+ return;
+ }
+
+ else {
+ // We shouldn't be here.
+ _objc_fatal("thread-safe class init in objc runtime is buggy!");
+ }
+}
+```
+
+可以看到,存在父类,并且父类没有被初始化,则递归调用 `initializeNonMetaClass`。如果不存在父类或者父类已经初始化了,则继续走下面的逻辑。会调用 `callInitialize` 方法
+
+```c++
+void callInitialize(Class cls)
+{
+ ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
+ asm("");
+}
+```
+
+可以看到,调用 initialize 方法的本质是通过 runtime 消息机制的能力。
+
+至此,可以解释 Demo 中的现象了。
+
+
+
+### 为什么 `initialize`方法存在“覆盖”的情况?
+
+有个现象:打印了 Student Category 的 `initialize`,却没有打印 Student 自己的 `initialize`,为什么?
+
+查看 objc 源码发现调用 `initialize` 方法本质上就是通过 runtime 的 `objc_msgSend ` 发消息来实现的。也就是通过 isa 指针查看类对象或元类对象的方法列表中(方法列表中包含当前类各个 Category 的各个方法)查找方法实现。所以当找到方法列表中排列较前的 `initialize` 时,就不再继续查找方法实现了,也就出现了 `initialize` 被覆盖的情况了。
+
+```c++
+void callInitialize(Class cls)
+{
+ ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
+ asm("");
+}
+```
+
+
+
+### `+initialize` 总结
+
+`+initialize` 和 `+load` 最大区别是 `+initialize` 是通过 `objc_msgSend` 进行调用的
+
+- 调用方式:`load` 根据函数地址直接调用,`initialize` 是根据 `objc_msgSend` 调用的
+
+- 调用时刻:load 是 runtime 加载类、分类的时候调用的。initialize 是在类第一次接收消息的时候调用的。每个类只会 initialize 一次,但是父类的 initialize 可能会调用多次(比如子类第一次接收消息,但是子类内部没有实现 `+initialize`,由于消息机制,父类如果实现了 `+initialize` 则会被调用。该父类至少调用2次)
+
+- 调用顺序:
+
+ - load:先调用类的 load(先编译的类优先调用 load、调用子类的 load,会先调用父类的 load)、再调用分类的 load(先编译的分类,优先调用 load )
+ - 如果子类没有实现 `+initialize` 则会调用父类的 `+initialize`(所以父类的 `+initialize` 可能会被调用多次)
+ - 如果分类实现了 `+initialize`,就会覆盖类本身的 `+initialize` 调用
+
+查看源码,伪代码如下:
+
+```objective-c
+if (自己没有初始化) {
+ if (父类存在 && 父类没有初始化) {
+ objc_msgSend(父类,@selector(initializ))
+ }
+ objc_msgSend(子类,@selector(initializ))
+}
+```
+
+
+
## 为 Category 实现属性的 Setter 和 Getter
+为一个类写一个 `@property nonatomic, strong) NSString *name` 编译器会做3件事情:
+
+- 生成成员变量 `_name`
+
+- 在 `.h` 中生成 getter、setter 声明,即 `-(void)setName:(NSString *)name`、`-(NSString *)name`
+
+- 在 `.m` 中生成 setter、getter 的实现
+
+ ```objective-c
+ -(void)setName:(NSString *)name {
+ _name = name;
+ }
+
+ -(NSString *)name {
+ return _name;
+ }
+ ```
+
+但是为一个分类写 `@property nonatomic, strong) NSString *name` 编译器会做1件事情:
+
+- 在 `.h` 中生成 getter、setter 声明,即 `-(void)setName:(NSString *)name`、`-(NSString *)name`
+
+所以在 Category 中增加分类,需要我们自己实现,利用关联对象的技术。
+
+
+
+| **objc_AssociationPolicy** | **对应的修饰符** |
+| --------------------------------- | ----------------- |
+| OBJC_ASSOCIATION_ASSIGN | assign |
+| OBJC_ASSOCIATION_RETAIN_NONATOMIC | strong, nonatomic |
+| OBJC_ASSOCIATION_COPY_NONATOMIC | copy, nonatomic |
+| OBJC_ASSOCIATION_RETAIN | strong, atomic |
+| OBJC_ASSOCIATION_COPY | copy, atomic |
+
```objectivec
#import "Person.h"
@@ -1442,9 +2642,7 @@ NS_ASSUME_NONNULL_BEGIN
@interface Person (Student)
-
-/**< 学号*/
-@property (nonatomic, strong) NSString *studyNumber;
+@property (nonatomic, strong) NSString *studyNumber;
@end
@@ -1459,8 +2657,7 @@ NS_ASSUME_NONNULL_END
- (void)sayHi {
NSLog(@"大家好,我叫%@,我今年%zd岁了",self.name,self.age);
}
-
-
+
/*
* 传统的做法是在 setter 里面这样写
_studyNumber = studyNumber;
@@ -1488,7 +2685,9 @@ NS_ASSUME_NONNULL_END
@end
```
-说明: `objc_setAssociatedObject` 的第二个参数是`const void * _Nonnull key` 所以可以用 "studyNumber" 或者利用 `@selector()` 的特性返回的数据类型也满足,所以示例代码选用第二种方式
+说明: `objc_setAssociatedObject` 的第二个参数是`const void * _Nonnull key` 所以可以用 "studyNumber" 或者利用 `@selector()` 的特性返回的数据类型也满足,所以示例代码选用第二种方式。
+
+
给分类添加属性的时候,为了避免多人开发对于属性添加造成的覆盖,我们需要为属性起一个独特的名字。比如我们的工程是组件化、模块化开展的工程,那么我们可以为属性命名的时候在前面添加当前模块的前缀。
@@ -1528,572 +2727,262 @@ NS_ASSUME_NONNULL_END
-## 底层剖析 load 方法
+## 关联对象的底层实现
-Demo 验证。
+技术实现的几个核心类:
-```objectivec
-@interface Person : NSObject
-@end
+- AssociationsManager
+- AssociationsHashMap
+- ObjectAssociationMap
+- ObjcAssociation
-@interface Student : Person
-@end
+查看 objc 源码
-@interface Student (Good)
-@end
-
-@interface Student (Bad)
-@end
-// 其中每个类都存在3个方法
-+ (void)load{
- NSLog(@"%s", __func__);
+```c++
+// objc-runtime.mm
+void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
+ _object_set_associative_reference(object, (void *)key, value, policy);
}
-+ (void)initialize{
- NSLog(@"%s", __func__);
-}
-- (void)test{
- NSLog(@"%s", __func__);
-}
-// Test
-Student *st = [[Student alloc] init];
-2022-04-16 01:35:22.237692+0800 Main[8752:2908124] +[Person load]
-2022-04-16 01:35:22.238305+0800 Main[8752:2908124] +[Student load]
-2022-04-16 01:35:22.238450+0800 Main[8752:2908124] +[Student(Good) load]
-2022-04-16 01:35:22.238562+0800 Main[8752:2908124] +[Student(Bad) load]
-2022-04-16 01:35:22.238664+0800 Main[8752:2908124] +[Person initialize]
-2022-04-16 01:35:22.238733+0800 Main[8752:2908124] +[Student(Bad) initialize]
-2022-04-16 01:35:22.238794+0800 Main[8752:2908124] -[Student(Bad) test]
+
```
-QA:
+简化版
-- 为什么 `initialize`方法存在“覆盖”的情况?
-
- 就是打印了 Student Category 的 `initialize`,却没有打印 Student 自身的 `initialize`。
+```c++
+class AssociationsManager {
+ static AssociationsHashMap *_map;
+}
- 查看 objc 源码发现调用 `initialize` 方法本质上就是通过 runtime 的 `objc_msgSend ` 发消息来实现的。也就是通过 isa 指针查看类对象或元类对象的方法列表中(方法列表中包含当前类各个 Category 的各个方法)查找方法实现。所以当找到方法列表中排列较前的 `initialize` 时,就不再继续查找方法实现了。也就出现了 `initialize` 被覆盖的情况了。
-
- ```c++
- void callInitialize(Class cls)
- {
- ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
- asm("");
- }
- ```
-
-- 为什么 load 方法打印顺序是这样的?
+typedef DenseMap, ObjectAssociationMap> AssociationsHashMap;
+typedef DenseMap ObjectAssociationMap;
- 因为调用 student alloc,相当于发送了消息。则肯定先执行 load 方法。类在 Runtime 启动阶段会调用 `schedule_class_load` 方法。方法内部递归调用,如果当前类存在父类则递归调用,否则将当前类加载到 loadable_classes 最后面。load 方法在本质上是执行 `call_load_methods`,方法地址是确定的(查看下面的源代码可以发现 `load` 方法是在编译期就可以确定的)。不走 `objc_msgSend` 这套流程。所以先打印父类 load、再打印子类 load、最后打印分类 load。如果存在多个分类,则按照编译顺序打印 load。
+class ObjcAssociation {
+ uintptr_t _policy;
+ id _value;
+}
-- 为什么 load 方法不是按照 Category 编译顺序倒序调用 load 方法?
-
- 看源代码 Objc4
-
- ```c
- void _objc_init(void){
- static bool initialized = false;
- if (initialized) return;
- initialized = true;
-
- // fixme defer initialization until an objc-using image is found?
- environ_init();
- tls_init();
- static_init();
- lock_init();
- exception_init();
-
- _dyld_objc_notify_register(&map_images, load_images, unmap_image);
- }
-
- void load_images(const char *path __unused, const struct mach_header *mh){
- // Return without taking locks if there are no +load methods here.
- if (!hasLoadMethods((const headerType *)mh)) return;
- recursive_mutex_locker_t lock(loadMethodLock);
- // Discover load methods
- {
- rwlock_writer_t lock2(runtimeLock);
- prepare_load_methods((const headerType *)mh);
- }
- // Call +load methods (without runtimeLock - re-entrant)
- call_load_methods();
- }
-
- void call_load_methods(void){
- static bool loading = NO;
- bool more_categories;
- loadMethodLock.assertLocked();
- // Re-entrant calls do nothing; the outermost call will finish the job.
- if (loading) return;
- loading = YES;
-
- void *pool = objc_autoreleasePoolPush();
-
- do {
- // 1. Repeatedly call class +loads until there aren't any more
- while (loadable_classes_used > 0) {
- call_class_loads(); // 先调用类的 load 方法
- }
-
- // 2. Call category +loads ONCE
- more_categories = call_category_loads(); // 再调用 category 的 load 方法
-
- // 3. Run more +loads if there are classes OR more untried categories
- } while (loadable_classes_used > 0 || more_categories);
-
- objc_autoreleasePoolPop(pool);
-
- loading = NO;
- }
-
- static void call_class_loads(void) {
- int i;
- // Detach current loadable list.
- struct loadable_class *classes = loadable_classes;
- int used = loadable_classes_used;
- loadable_classes = nil;
- loadable_classes_allocated = 0;
- loadable_classes_used = 0;
-
- // Call all +loads for the detached list.
- for (i = 0; i < used; i++) {
- Class cls = classes[i].cls;
- load_method_t load_method = (load_method_t)classes[i].method;
- if (!cls) continue;
-
- if (PrintLoading) {
- _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
- }
- (*load_method)(cls, SEL_load);
- }
-
- // Destroy the detached list.
- if (classes) free(classes);
- }
-
- static bool call_category_loads(void) {
- int i, shift;
- bool new_categories_added = NO;
- // Detach current loadable list.
- struct loadable_category *cats = loadable_categories;
- int used = loadable_categories_used;
- int allocated = loadable_categories_allocated;
- loadable_categories = nil;
- loadable_categories_allocated = 0;
- loadable_categories_used = 0;
-
- // Call all +loads for the detached list.
- for (i = 0; i < used; i++) {
- Category cat = cats[i].cat;
- load_method_t load_method = (load_method_t)cats[i].method;
- Class cls;
- if (!cat) continue;
-
- cls = _category_getClass(cat);
- if (cls && cls->isLoadable()) {
- if (PrintLoading) {
- _objc_inform("LOAD: +[%s(%s) load]\n",
- cls->nameForLogging(),
- _category_getName(cat));
- }
- (*load_method)(cls, SEL_load);
- cats[i].cat = nil;
- }
- }
-
- // Compact detached list (order-preserving)
- shift = 0;
- for (i = 0; i < used; i++) {
- if (cats[i].cat) {
- cats[i-shift] = cats[i];
- } else {
- shift++;
- }
- }
- used -= shift;
-
- // Copy any new +load candidates from the new list to the detached list.
- new_categories_added = (loadable_categories_used > 0);
- for (i = 0; i < loadable_categories_used; i++) {
- if (used == allocated) {
- allocated = allocated*2 + 16;
- cats = (struct loadable_category *)
- realloc(cats, allocated *
- sizeof(struct loadable_category));
- }
- cats[used++] = loadable_categories[i];
- }
-
- // Destroy the new list.
- if (loadable_categories) free(loadable_categories);
-
- // Reattach the (now augmented) detached list.
- // But if there's nothing left to load, destroy the list.
- if (used) {
- loadable_categories = cats;
- loadable_categories_used = used;
- loadable_categories_allocated = allocated;
- } else {
- if (cats) free(cats);
- loadable_categories = nil;
- loadable_categories_used = 0;
- loadable_categories_allocated = 0;
- }
-
- if (PrintLoading) {
- if (loadable_categories_used != 0) {
- _objc_inform("LOAD: %d categories still waiting for +load\n",
- loadable_categories_used);
- }
- }
-
- return new_categories_added;
- }
- ```
-
- 会发现源码中先调用类的 load 方法,再调用 category 的 load 方法。
-
- 再看看 `call_class_loads`、`call_category_loads` 方法内部实现,是直接找到 `load_method_t load_method = (load_method_t)classes[i].method;` 类对象的 load 方法地址。最后直接调用 `(*load_method)(cls, SEL_load);` 方法本身。
-
- test 方法是走消息发送流程 `objc_msgSend()` 所以会走 isa、superclass 这一套流程,test 是对象方法,所以需要根据 isa 找到类对象,从类对象的对象方法列表找到 test 方法,找不到则根据 superclass 找到当前类对象的父类对象,继续查找方法列表。直到 NSObject、nil 对象为止,然后走消息起死回生的阶段。
-
- 看2个结构体
-
- ```c
- struct loadable_class {
- Class cls; // may be nil
- IMP method; // 指向类的 load 方法
- };
-
- struct loadable_category {
- Category cat; // may be nil
- IMP method; // 指向分类的 load 方法
- };
- ```
-
-类、分类的 load 方法调用顺序?
-
-1. 调用类的 +load 方法顺序
-
- - 调用类的 +load
-
- - 根据编译先后顺序调用 +load(先编译先调用)
-
- - 存在继承关系的类,会先调用父类的 +load
-
-2. 调用分类的 +load 方法顺序
-
- - 按照编译顺序调用分类的 +load(先编译先调用)
-
-源代码印证
-
-```objectivec
-void load_images(const char *path __unused, const struct mach_header *mh) {
- // Return without taking locks if there are no +load methods here.
- if (!hasLoadMethods((const headerType *)mh)) return;
-
- recursive_mutex_locker_t lock(loadMethodLock);
-
- // Discover load methods
+void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
+ // retain the new value (if any) outside the lock.
+ ObjcAssociation old_association(0, nil);
+ id new_value = value ? acquireValue(value, policy) : nil;
{
- rwlock_writer_t lock2(runtimeLock);
- prepare_load_methods((const headerType *)mh);
- }
-
- // Call +load methods (without runtimeLock - re-entrant)
- call_load_methods();
-}
-
-void prepare_load_methods(const headerType *mhdr){
- size_t count, i;
-
- runtimeLock.assertWriting();
-
- classref_t *classlist =
- _getObjc2NonlazyClassList(mhdr, &count);
- for (i = 0; i < count; i++) {
- schedule_class_load(remapClass(classlist[i]));
- }
-
- category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
- for (i = 0; i < count; i++) {
- category_t *cat = categorylist[i];
- Class cls = remapClass(cat->cls);
- if (!cls) continue; // category for ignored weak-linked class
- realizeClass(cls);
- assert(cls->ISA()->isRealized());
- add_category_to_loadable_list(cat);
- }
-}
-```
-
-我们看看 `schedule_class_load` 方法。方法内部递归调用,如果当前类存在父类则递归调用,否则将当前类加载到 loadable_classes 最后面
-
-```objectivec
-static void schedule_class_load(Class cls)
-{
- if (!cls) return;
- assert(cls->isRealized()); // _read_images should realize
-
- if (cls->data()->flags & RW_LOADED) return;
-
- // Ensure superclass-first ordering
- schedule_class_load(cls->superclass);
-
- add_class_to_loadable_list(cls);
- cls->setInfo(RW_LOADED);
-}
-```
-
-```objectivec
-void add_class_to_loadable_list(Class cls)
-{
- IMP method;
-
- loadMethodLock.assertLocked();
-
- method = cls->getLoadMethod();
- if (!method) return; // Don't bother if cls has no +load method
-
- if (PrintLoading) {
- _objc_inform("LOAD: class '%s' scheduled for +load",
- cls->nameForLogging());
- }
-
- if (loadable_classes_used == loadable_classes_allocated) {
- loadable_classes_allocated = loadable_classes_allocated*2 + 16;
- loadable_classes = (struct loadable_class *)
- realloc(loadable_classes,
- loadable_classes_allocated *
- sizeof(struct loadable_class));
- }
-
- loadable_classes[loadable_classes_used].cls = cls;
- loadable_classes[loadable_classes_used].method = method; // 加载到最后
- loadable_classes_used++;
-}
-```
-
-`prepare_load_methods` 处理完再执行 `call_load_methods`
-
-```objectivec
-void call_load_methods(void)
-{
- static bool loading = NO;
- bool more_categories;
-
- loadMethodLock.assertLocked();
-
- // Re-entrant calls do nothing; the outermost call will finish the job.
- if (loading) return;
- loading = YES;
-
- void *pool = objc_autoreleasePoolPush();
-
- do {
- // 1. Repeatedly call class +loads until there aren't any more
- while (loadable_classes_used > 0) {
- call_class_loads();
- }
-
- // 2. Call category +loads ONCE
- more_categories = call_category_loads();
-
- // 3. Run more +loads if there are classes OR more untried categories
- } while (loadable_classes_used > 0 || more_categories);
-
- objc_autoreleasePoolPop(pool);
-
- loading = NO;
-}
-这里
-```
-
-这里的代码已经看过了,也就先加载类的 +load 方法,加载顺序按照 loadable_classes 中的类顺序进行访问 +load。之后再加载 Catetory 的 +load 方法。
-
-在 `prepare_load_methods` 方法内部先给普通类按照编译顺序(谁先编译谁先添加,遇到存在父类的类,递归调用父类对象)添加类信息到 loadable_classes 中,之后给分类按照编译顺序添加到(谁先编译谁先添加) loadable_categories 中。
-
-+load 方法在 Runtime 加载类、分类的时候调用。
-
-Extension 在编译阶段,数据已经包含在类信息中。
-
-Category 是在运行阶段,才会将数据合并到类信息中。
-
-
-
-## 底层剖析 Initialize 方法
-
-上 Demo
-
-```objectivec
-@interface Person : NSObject
-@end
-
-@interface Student : Person
-@end
-
-@interface Student (Good)
-@end
-```
-
-`Person *p1 = [[Person alloc] init];` 这句代码输出什么? 这个比较简单,initialize 方法在类第一次收到消息的时候调用。所以输出 `+[Person initialize]`
-
-`Student *st = [[Student alloc] init];` 输出什么?
-
-```objectivec
-+[Person initialize]
-+[Student(Good) initialize]
-```
-
-查看分类在 Runtime 加载类信息时候的调用原理可以知道,分类中的类方法、对象方法都会被加载原始类的前面去(initialize 是类方法)如下图:
-
-
-
-
-
-`+initialize` 和 `+load` 最大区别是 `+initialize` 是通过 `objc_msgSend` 进行调用的
-
-- 调用方式:`load` 根据函数地址直接调用,`initialize` 是根据 `objc_msgSend` 调用的
-
-- 调用时刻:load 是 runtime 加载类、分类的时候调用的。initialize 是在类第一次接收消息的时候调用的。每个类只会 initialize 一次,但是父类的 initialize 可能会调用多次
-
-- 调用顺序:
-
- - load:先调用类的 load(先编译的类优先调用 load、调用子类的 load,会先调用父类的 load)、再调用分类的 load(先编译的分类,优先调用 load )
- - 如果子类没有实现 `+initialize` 则会调用父类的 `+initialize`(所以父类的 `+initialize` 可能会被调用多次)
- - 如果分类实现了 `+initialize`,就会覆盖类本身的 `+initialize` 调用
-
-
-查看源码,伪代码如下:
-
-```
-if (自己没有初始化) {
- if (父类没有初始化) {
- objc_msgSend(父类,@selector(initializ))
- }
- objc_msgSend(子类,@selector(initializ))
-}
-```
-
-```objectivec
-void _class_initialize(Class cls)
-{
- assert(!cls->isMetaClass());
-
- Class supercls;
- bool reallyInitialize = NO;
-
- // Make sure super is done initializing BEFORE beginning to initialize cls.
- // See note about deadlock above.
- supercls = cls->superclass;
- if (supercls && !supercls->isInitialized()) {
- _class_initialize(supercls);
- }
-
- // Try to atomically set CLS_INITIALIZING.
- {
- monitor_locker_t lock(classInitLock);
- if (!cls->isInitialized() && !cls->isInitializing()) {
- cls->setInitializing();
- reallyInitialize = YES;
- }
- }
-
- if (reallyInitialize) {
- // We successfully set the CLS_INITIALIZING bit. Initialize the class.
-
- // Record that we're initializing this class so we can message it.
- _setThisThreadIsInitializingClass(cls);
-
- if (MultithreadedForkChild) {
- // LOL JK we don't really call +initialize methods after fork().
- performForkChildInitialize(cls, supercls);
- return;
- }
-
- // Send the +initialize message.
- // Note that +initialize is sent to the superclass (again) if
- // this class doesn't implement +initialize. 2157218
- if (PrintInitializing) {
- _objc_inform("INITIALIZE: thread %p: calling +[%s initialize]",
- pthread_self(), cls->nameForLogging());
- }
-
- // Exceptions: A +initialize call that throws an exception
- // is deemed to be a complete and successful +initialize.
- //
- // Only __OBJC2__ adds these handlers. !__OBJC2__ has a
- // bootstrapping problem of this versus CF's call to
- // objc_exception_set_functions().
-#if __OBJC2__
- @try
-#endif
- {
-
- callInitialize(cls);
-
- if (PrintInitializing) {
- _objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
- pthread_self(), cls->nameForLogging());
+ AssociationsManager manager;
+ AssociationsHashMap &associations(manager.associations());
+ disguised_ptr_t disguised_object = DISGUISE(object); // 指针地址,按位取反
+ // 新值有值
+ if (new_value) {
+ // break any existing association.
+ // 从全局的 AssociationsHashMap 中根据对象地址按位取反后的结果为 key,查找对应的 ObjectAssociationMap
+ AssociationsHashMap::iterator i = associations.find(disguised_object);
+ // 如果找到对应的 ObjectAssociationMap
+ if (i != associations.end()) {
+ // secondary table exists
+ ObjectAssociationMap *refs = i->second;
+ // 从 ObjectAssociationMap 中,根据 key 查找有没有值
+ ObjectAssociationMap::iterator j = refs->find(key);
+ if (j != refs->end()) { // 有值,则说明该 key 之前设置过关联对象,本次就做更新
+ old_association = j->second;
+ j->second = ObjcAssociation(policy, new_value);
+ } else { // 没有值,则给 ObjectAssociationMap 中添加一个 ObjcAssociation 类型的值。ObjcAssociation 由 policy 和 newValue 构成
+ (*refs)[key] = ObjcAssociation(policy, new_value);
+ }
+ } else {
+ // create the new association (first time).
+ // 如果没有找到则说明第一次为该对象设置关联对象,本次需要创建一个新的 ObjectAssociationMap
+ ObjectAssociationMap *refs = new ObjectAssociationMap;
+ // AssociationsHashMap 中以对象地址为 key,ObjectAssociationMap 为 value,新增一条数据
+ associations[disguised_object] = refs;
+ // ObjectAssociationMap 中以关联对象传入的 key 为 key,value 为 ObjcAssociation 为 value 插入一条数据。ObjcAssociation 由 policy 和 newValue 组成
+ (*refs)[key] = ObjcAssociation(policy, new_value);
+ object->setHasAssociatedObjects();
}
- }
-#if __OBJC2__
- @catch (...) {
- if (PrintInitializing) {
- _objc_inform("INITIALIZE: thread %p: +[%s initialize] "
- "threw an exception",
- pthread_self(), cls->nameForLogging());
- }
- @throw;
- }
- @finally
-#endif
- {
- // Done initializing.
- lockAndFinishInitializing(cls, supercls);
- }
- return;
- }
-
- else if (cls->isInitializing()) {
- // We couldn't set INITIALIZING because INITIALIZING was already set.
- // If this thread set it earlier, continue normally.
- // If some other thread set it, block until initialize is done.
- // It's ok if INITIALIZING changes to INITIALIZED while we're here,
- // because we safely check for INITIALIZED inside the lock
- // before blocking.
- if (_thisThreadIsInitializingClass(cls)) {
- return;
- } else if (!MultithreadedForkChild) {
- waitForInitializeToComplete(cls);
- return;
} else {
- // We're on the child side of fork(), facing a class that
- // was initializing by some other thread when fork() was called.
- _setThisThreadIsInitializingClass(cls);
- performForkChildInitialize(cls, supercls);
+ // setting the association to nil breaks the association.
+ // 没有 newValue 则说明需要将 AssociationHashMap 中对象地址对应 ObjectAssociationMap 中 key 对应的的 value 移除
+ AssociationsHashMap::iterator i = associations.find(disguised_object);
+ // 以对象地址为 key,从 AssociationsHashMap 中找到了 ObjectAssociationMap
+ if (i != associations.end()) {
+ ObjectAssociationMap *refs = i->second;
+ ObjectAssociationMap::iterator j = refs->find(key);
+ // 从 ObjectAssociationMap 中找到了 key 记录
+ if (j != refs->end()) {
+ // 从 ObjectAssociationMap 中移除 ObjectAssociation
+ old_association = j->second;
+ refs->erase(j);
+ }
+ }
}
}
-
- else if (cls->isInitialized()) {
- // Set CLS_INITIALIZING failed because someone else already
- // initialized the class. Continue normally.
- // NOTE this check must come AFTER the ISINITIALIZING case.
- // Otherwise: Another thread is initializing this class. ISINITIALIZED
- // is false. Skip this clause. Then the other thread finishes
- // initialization and sets INITIALIZING=no and INITIALIZED=yes.
- // Skip the ISINITIALIZING clause. Die horribly.
- return;
- }
-
- else {
- // We shouldn't be here.
- _objc_fatal("thread-safe class init in objc runtime is buggy!");
- }
+ // release the old value (outside of the lock).
+ if (old_association.hasValue()) ReleaseValue()(old_association);
}
-void callInitialize(Class cls) {
- ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
- asm("");
+```
+
+梳理后,如下图所示:
+
+
+
+AssociationsManager 管理的 AssociationHashMap 结构如下:
+
+```objective-c
+{
+ "0x4927298732": {
+ "@selector(studyNumber)" : {
+ "value": "2022122201",
+ "policy": "retain"
+ },
+ "@selector(title)": {
+ "value": "Hello category",
+ "policy": "retain"
+ }
+ },
+ "0x3666444222": {
+ "@selector(backgroundColor)" : {
+ "value": "0xff0021",
+ "policy": "retain"
+ }
+ }
}
```
+
+
+
+## Category 的使用场景
+
+### 给现有类添加方法
+
+拓展功能,最基础的
+
+
+
+### 将类的实现代码分散到多个分类中
+
+类中经常容易填满各种方法,而这些方法的代码则全部堆在一个巨大的实现文件中。可以通过 Category 机制,把类代码按照逻辑划分到几个分区中。
+
+- 可以减少单个文件的体积
+- 可以把不同的功能组织到不同的category里
+- 可以由多个开发者共同完成一个类
+- 可以按需加载想要的category等等
+
+
+
+比如 Person 类
+
+```objective-c
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface Person : NSObject
+
+@property (nonatomic, copy, readonly) NSString *firstName;
+@property (nonatomic, copy, readonly) NSString *lastName;
+@property (nonatomic, copy, readonly) NSArray *friends;
+
+- (instancetype)initWithFirstName:(NSString *)firstName
+ andLastName:(NSString *)lastName;
+#pragma mark - FriendShip methods
+- (void)addFriend:(Person *)person;
+- (void)removeFriends:(Person *)person;
+- (BOOL)isFriendsWith:(Person *)person;
+
+#pragma mark - Work methods
+- (void)performDaysWork;
+- (void)takeVacationFromWork;
+
+#pragma mark - Play methods
+- (void)goToTheCinema;
+- (void)goToSportsGame;
+
+@end
+
+NS_ASSUME_NONNULL_END
+```
+
+拆分后
+
+```objective-c
+// Person.h
+@interface Person : NSObject
+
+@property (nonatomic, copy, readonly) NSString *firstName;
+@property (nonatomic, copy, readonly) NSString *lastName;
+@property (nonatomic, copy, readonly) NSArray *friends;
+
+- (instancetype)initWithFirstName:(NSString *)firstName
+ andLastName:(NSString *)lastName;
+@end
+
+// Person+FriendShip.h
+@interface Person(FriendShip)
+- (void)addFriend:(Person *)person;
+- (void)removeFriends:(Person *)person;
+- (BOOL)isFriendsWith:(Person *)person;
+@end
+// Person+Work.h
+@interface Person(Work)
+- (void)performDaysWork;
+- (void)takeVacationFromWork;
+@end
+// Person+Play.h
+@interface Person(Play)
+- (void)goToTheCinema;
+- (void)goToSportsGame;
+@end
+```
+
+使用分类后,依然可以把整个类都定义在一个接口文件中,但把实现写到 Category 中。但如果分类越来越多或者觉得不合理,可以将相关 API 也放到 Category 的 `.h` 中。
+
+
+
+### 声明私有方法
+
+
+
+
+
+### UI 和点击事件处理的代码聚合
+
+UIAlertView 的点击后的判断是通过 delegate 进行处理的。一个看上去看起来还好,但假如一个类里面存在多个 AlertView 呢。是不是就需要在代理方法里面判断 alertView 属于哪个,buttonIndex 属于哪个? UIAlertView 的视图创建和点击事件的处理放在2个地方,阅读起来不方便。
+
+```objective-c
+- (void)showAlertView {
+ UIAlertView *alertView = [UIAlertView alloc] initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil];
+ [alertView show];
+}
+
+- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
+ if (buttonIndex == 0) {
+ [self handleCancel];
+ } else {
+ [self handleContinue];
+ }
+}
+```
+
+接下去利用关联对象优化下代码
+
+```objective-c
+static void *AlertViewDialogHandlerKey = "AlertViewDialogHandlerKey";
+
+- (void)showAlertView {
+ UIAlertView *alertView = [UIAlertView alloc] initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil];
+ void(^clickHandler)(NSInteger index) = ^(NSInteger buttonIndex) {
+ if (buttonIndex == 0) {
+ [self handleCancel];
+ } else {
+ [self handleContinue];
+ }
+ };
+ objc_setAssociatedObject(alertView, AlertViewDialogHandlerKey, clickHandler, OBJC_ASSOCIATION_COPY)
+ [alertView show];
+}
+
+- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
+ void(^clickHandler)(NSInteger buttonIndex) = objc_getAssociatedObject(self, AlertViewDialogHandlerKey);
+ clickHandler(buttonIndex);
+}
+```
+
diff --git a/Chapter1 - iOS/1.51.md b/Chapter1 - iOS/1.51.md
index 94307ab..e392867 100644
--- a/Chapter1 - iOS/1.51.md
+++ b/Chapter1 - iOS/1.51.md
@@ -267,4 +267,14 @@ Pod::Spec.new do |s|
CMD
end
```
-## 15.
+## 15. `pod lib create` 报错 `Ignoring ffi-1.16.3 because its extensions are not built`
+
+开始可能发现错误
+
+Ignoring ffi-1.16.3 because its extensions are not built. Try: gem pristine ffi --version 1.16.3
+类似这样的错误
+```
+sudo gem install --user-install rexml
+sudo gem install --user-install xcodeproj
+```
+
diff --git a/Chapter1 - iOS/1.7.md b/Chapter1 - iOS/1.7.md
index 77ca84e..1010420 100644
--- a/Chapter1 - iOS/1.7.md
+++ b/Chapter1 - iOS/1.7.md
@@ -12,7 +12,7 @@ BSS段(bss segment):通常用来存储程序中未被初始化的全局变
代码段(code segment):通常是指用来存储程序可执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量。
-
+
@@ -58,7 +58,7 @@ struct NSObject_IMPL {
因此可以知道,OC 的类底层是由 c/c++ 的继承实现的。
-
+
由于 obj 对象没有任何属性和方法,只有一个 isa 指针,且类的本质就是结构体,所以当结构体只有1个成员时,该成员的地址值,就是该结构体的地址。
@@ -118,7 +118,7 @@ struct Student_IMPL {
类的本质是结构体,结构体成员内存紧挨着。内存布局如图所示:
-
+
@@ -126,7 +126,7 @@ struct Student_IMPL {
如果上述结论正确,那是不是可以声明一个 ` Student_IMPL` 类型的结构体指针,指向 st 指针指向的对象。然后通过结构体指针访问成员变量,看看取值是不是正确的
-
+
发现是可以正确访问的。
@@ -193,7 +193,7 @@ struct Student_IMPL {
为什么 `class_getInstanceSize([Person class])` 也是16,不是8+4吗?因为存在内存对齐,结构体的大小必须是最大成员大小的倍数(Person 中也就是8的倍数)
-
+
@@ -330,12 +330,12 @@ int main(int argc, const char * argv[]) {
`Person *p1 = [Person new];` 这句代码在内存分配原理如下图所示
-
+
**结论**
-
-
+
+
**可以 看到Person类的3个对象p1、p2、p3的isa的值相同。**
@@ -707,7 +707,7 @@ iOS 中,系统分配内存,都是16的倍数。pageSize?系统在分配内
GUN 都存在内存对齐这个概念。
`sizeof` 本质是运算符。在 Xcode 编译后就替换为真正的值。通过指令 `xcrun --sdk iphoneos clang -arch arm64 -S -emit-llvm ViewController.m -o ViewController.ll` 查看 IR。
-
+
@@ -718,21 +718,21 @@ GUN 都存在内存对齐这个概念。
`objc_getClass()` 如果传递 instance 实例对象,返回 class 类对象;传递 Class 类对象,返回 meta-class 元类对象;传递 meta-class 元对象,则返回 NSObject(基类)的 meta-class 对象
-
+
instance 的 isa 指向 Class。当调用方法时,通过 instance 的 isa 找到 Class,最后找到对象方法的实现进行调用
class 的 isa 指向 meta-class。当调用类方法的时,通过 class 的 isa 找到 meta-class,最后找到类方法进行调用。
-
+
当 Student 实例对象调用 Person 的对象方法时,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,在类对象的对象方法列表中找到方法实现并调用。
当 Stduent 实例对象调用 init 方法时候,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,找到 Person Claas 的 superClass 到 NSObject 类对象,在 NSObject 类对象的方法列表中找到 `init` 方法并调用。
-
+
当 Stduent 对象调用类方法的时候,先根据 isa 找到 Student 的元类对象,然后在元类对象的 superclass 找到 Person 的元类对象,再根据 Person 元类对象的 superClass 找到 NSObject 的元类对象。最后找到元类对象的方法列表,调用到对象方法。
-
+
```objectivec
@interface Student : NSObject
@@ -937,7 +937,7 @@ class_rw_t *personMetaClassData = personClass->metaClass()->data();
内存对齐是指数据在内存中存储时按照一定规则对齐到特定的地址上。在 iOS 开发中,内存对齐是为了提高内存访问的效率和性能。内存对齐的原因主要包括以下几点:
1. 提高访问速度:内存对齐可以使数据在内存中的存储更加高效,因为大部分计算机体系结构都要求数据按照特定的边界对齐,这样可以减少内存访问的次数,提高访问速度。CPU访问非对齐的内存时需要进行多次拼接。
如下图,比如需要读取从[2, 5]的内存,需要分别读取两次,然后还需要做位移的运算,最后才能得到需要的数据。这中间的损耗就会影响访问速度。
-
+
2. 为了方便移植。CPU是一块块的进行进行内存访问。有一些硬件平台不允许随机访问,只能访问对齐后的内存地址,否则会报异常。
很多 CPU(如基于 Alpha,IA-64,MIPS,和 SuperH 体系的)拒绝读取未对齐数据。当一个程序要求这些 CPU 读取未对齐数据时,这时 CPU 会进入异常处理状态并且通知程序不能继续执行。举个例子,在 ARM,MIPS,和 SH 硬件平台上,当操作系统被要求存取一个未对齐数据时会默认给应用程序抛出硬件异常。
@@ -1017,7 +1017,7 @@ NSLog(@"%zd", malloc_size(temp));
成员变量占用8字节对齐,每个对象的第一个都是 isa 指针,必须要占用8字节。举例一个极端 case,假设 n 个对象,其中 m 个对象没有成员变量,只有 isa 指针占用8字节,其中的 n-m个对象既有 isa 指针,又有成员变量。每个类交错排列,那么 CPU 在访问对象的时候会耗费大量时间去识别具体的对象。很多时候会取舍,这个 case 就是时间换空间。以16字节对齐,会加快访问速度(参考链表和数组的设计)
上述是 Apple 官方的角度出发探究的,其他系统,比如 Linux 也是存在内存对齐的。由于 Linux 也是采用 GNU 的东西,所以探索下 GNU 下 glibc malloc 的实现。从[这里](https://ftp.gnu.org/gnu/glibc/)下载 glibc 源码。然后拖到 Xcode 中查看
-
+
可以看到 GNU 源码里面,内存对齐 `MALLOC_ALIGNMENT`
在 i386 里面是16,在非 i386 里面有个判断
@@ -1031,7 +1031,7 @@ NSLog(@"%zd", malloc_size(temp));
# define INTERNAL_SIZE_T size_t
```
在 Xcode 打印输出, `__alignof__ (long double)` 为16,`sizeof(size_t)` 为8,即 `2 * SIZE_SZ` = 16,所以不管怎么看,在 GUN 里面内存对齐一定都是16.
-
+
Todo: 研究探索 libmalloc 源码
@@ -1154,3 +1154,42 @@ Class objc_getClass(const char *aClassName)
`-(Class)class`、`+(Class)class`:返回的是类对象
+
+
+
+
+## QA
+
+### 对象的 isa 指向什么?
+- Instance 对象的 isa 指向类对象(Class)
+- Class 对象的 isa 指向元类对象(Meta-Class)
+- Meta-Class 对象的 isa 指向基类的元类对象(Meta-Class)
+
+但注意:
+- 实例对象的 isa 需要与 ISA_MASK 按位与之后才可以得到类对象的地址值。
+- 类对象的 isa 需要与 ISA_MASK 按位与之后才可以得元类对象的地址值。
+
+
+### OC 的类信息存放在哪?
+- Instance 对象:成员变量具体的值,存放在实例对象中
+ ```
+ struct NSObject_IMPL {
+ class isa;
+ }
+
+ struct Person_IMPL {
+ struct NSObject_IMPL NSObject_IAVRS;
+ int _age;
+ int _height;
+ }
+
+ //
+ struct Person_IMPL {
+ class isa;
+ int _age;
+ int _height;
+ }
+ ```
+- Class 对象:属性信息、对象方法信息、成员变量信息、协议信息、superclass、isa 存放在类对象中。
+- Meta-Class 对象:类方法信息,存放在元类对象中。
+
diff --git a/Chapter1 - iOS/1.74.md b/Chapter1 - iOS/1.74.md
index 21e0dc4..d6baf5c 100644
--- a/Chapter1 - iOS/1.74.md
+++ b/Chapter1 - iOS/1.74.md
@@ -1154,6 +1154,8 @@ MemoryStatus 机制会开启一个 memorystatus_jetsam_thread 的线程,它负
当监控线程发现某 App 有内存压力时,就发出通知,此时有内存的 App 就去执行 `didReceiveMemoryWarning` 代理方法。在这个时机,我们还有机会做一些内存资源释放的逻辑,也许会避免 App 被系统杀死。
+
+
**源码角度查看问题**
iOS 系统内核有一个数组,专门维护线程的优先级。数组的每一项是一个包含进程链表的结构体。结构体如下:
@@ -1785,7 +1787,7 @@ for (NSInteger index = 0; index < 10000000; index++) {
现象:
1. 在 viewDidLoad 也就是主线程中内存消耗过大,系统并不会发出低内存警告,直接 Crash。因为内存增长过快,主线程很忙。
-2. 多线程的情况下,App 因内存增长过快,会收到低内存警告,AppDelegate 中的`applicationDidReceiveMemoryWarning` 先执行,随后是当前 VC 的 `didReceiveMemoryWarning`。
+2. 多线程的情况下,App 因内存增长过快,会收到低内存警告,AppDelegate 中的`applicationDidReceiveMemoryWarning` 先执行,随后是当前 VC 的 `didReceiveMemoryWarning`。也可以注册 `UIApplicationDidReceiveMemoryWarningNotification` 通知,此时获取一下内存情况一般就是 high Water 线。
结论:
@@ -2052,7 +2054,7 @@ for (NSInteger index = 0; index < 10000000; index++) {
其他的开发习惯就不一一描述了,良好的开发习惯和代码意识是需要平时注意修炼的。
-### 7.
+### 7. Memory Graph
在使用了一波业界优秀的的内存监控工具后发现了一些问题,比如 `MLeaksFinder`、`OOMDetector`、`FBRetainCycleDetector`等都有一些问题。比如 `MLeaksFinder` 因为单纯通过 VC 的 push、pop 等检测内存泄露的情况,会存在误报的情况。`FBRetainCycleDetector` 则因为对象深度优先遍历,会有一些性能问题,影响 App 性能。`OOMDetector` 因为没有合适的触发时机。
@@ -3699,56 +3701,44 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
@implementation NetworkDelegateProxy
#pragma mark - life cycle
- ```
-
-- (instancetype)sharedInstance {
- static NetworkDelegateProxy *_sharedInstance = nil;
-
- static dispatch_once_t onceToken;
-
- dispatch_once(&onceToken, ^{
- _sharedInstance = [NetworkDelegateProxy alloc];
-
+ - (instancetype)sharedInstance {
+ static NetworkDelegateProxy *_sharedInstance = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ _sharedInstance = [NetworkDelegateProxy alloc];
});
-
- return _sharedInstance;
-
+ return _sharedInstance;
}
#pragma mark - public Method
-
-- (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate {
- NetworkDelegateProxy *instance = [NetworkDelegateProxy sharedInstance];
- instance->_originalTarget = originalTarget;
- instance->_NewDelegate = newDelegate;
- return instance;
- }
-
-- (void)forwardInvocation:(NSInvocation *)invocation {
- if ([_originalTarget respondsToSelector:invocation.selector]) {
- [invocation invokeWithTarget:_originalTarget];
- [((NSURLSessionAndConnectionImplementor *)_NewDelegate) invoke:invocation];
-
- }
+ - (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate {
+ NetworkDelegateProxy *instance = [NetworkDelegateProxy sharedInstance];
+ instance->_originalTarget = originalTarget;
+ instance->_NewDelegate = newDelegate;
+ return instance;
}
-
-- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
- return [_originalTarget methodSignatureForSelector:sel];
+
+ - (void)forwardInvocation:(NSInvocation *)invocation {
+ if ([_originalTarget respondsToSelector:invocation.selector]) {
+ [invocation invokeWithTarget:_originalTarget];
+ [((NSURLSessionAndConnectionImplementor *)_NewDelegate) invoke:invocation];
+ }
+ }
+
+ - (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
+ return [_originalTarget methodSignatureForSelector:sel];
}
@end
-
```
- ```
-
- 创建一个对象,实现 NSURLConnection、NSURLSession、NSIuputStream 代理方法
```objective-c
// NetworkImplementor.m
#pragma mark-NSURLConnectionDelegate
- - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
+ - (void)connection:(NSURLConnection *)connection didFailWithErrorbo:(NSError *)error {
NSLog(@"%s", __func__);
}
diff --git a/Chapter1 - iOS/1.75.md b/Chapter1 - iOS/1.75.md
index 9b4ec89..3cbbb9b 100644
--- a/Chapter1 - iOS/1.75.md
+++ b/Chapter1 - iOS/1.75.md
@@ -1075,6 +1075,19 @@ QA:一个被测方法,有诸多 case,为什么不写在一个测试方法
+### 15. Swift 测试用例代码测试 OC 被测代码
+
+编写 Swift 测试代码去测试 OC 被测类的时候,需要做一些处理:
+
+1. 主工程不管是不是混编,但为了让 Swift 测试代码,可以访问到 OC 被测类,需要创建一个 Swift 文件,系统会自动创建 bridge 文件,且需要在 `AppTestingExplore-Bridging-Header.h` 文件中导出需要被测的头文件
+2. 在 Swift 测试文件中,导入主工程 module。
+
+
+
+
+
+`@testable` 是 Swift 语言的一个特性,它允许测试用例访问应用程序或框架中标记为 `internal` 或 `private` 的属性、方法和其他成员。这样做可以在不改变访问级别的情况下编写测试用例,从而保持代码的封装性和安全性。使用 `@testable` 可以增强测试覆盖率,因为它允许测试那些通常因为访问级别限制而无法测试的内部实现细节。同时,它还有助于保持代码的封装性,因为不需要将内部实现细节暴露为 `public` 就可以进行单元测试。此外,`@testable` 提高了测试的灵活性,在不修改代码访问级别的情况下,能够对代码进行全面的测试
+
## 四、网络测试
@@ -1164,6 +1177,8 @@ SPEC_END
## 五、UI 测试
+### 基础使用
+
上面文章大篇幅的讲了单元测试相关的话题,单元测试十分适合代码质量、逻辑、网络等内容的测试,但是针对最终产物 App 来说单元测试就不太适合了,如果测试 UI 界面的正确性、功能是否正确显然就不太适合了。Apple 在 Xcode 7 开始推出的 `UI Testing` 就是苹果自己的 UI 测试框架。
很多 UI 自动化测试框架的底层实现都依赖于 `Accessibility`,也就是 App 可用性。`UI Accessibility` 是 iOS 3.0 引入的一个人性化功能,帮助身体不便的人士方便使用 App。
@@ -1298,6 +1313,44 @@ Accessibility 通过对 UI 元素进行分类和标记。分类成类似按钮
+### 经验心得
+
+UI 测试另一个问题是,某些 UI 方法比如 AppDelegate 里包含太多 SDK 的或者拉接口的场景,启动会比较慢,测试的诉求是:单个测试 case 需要快速运行。而 UI 测试聚焦的不是借口业务逻辑,所以期望 AppDelegate 里的拉接口这样的逻辑不要走,太慢影响测试速度。
+
+理论分析:如果可以从 `NSClassFromString(@"XCTestCase")` 方式获取到值,说明是测试环境,可以简化 AppDelegate 逻辑。
+
+具体做法是在开发阶段预留测试口子。非测试模式,走正常的业务逻辑;测试模式,走简化版 AppDelegate 逻辑。
+
+第一步:改造 `main.m`
+
+```objective-c
+int main(int argc, char * argv[]) {
+ NSString * appDelegateClassName;
+ @autoreleasepool {
+ // Setup code that might create autoreleased objects goes here.
+ id testClass = NSClassFromString(@"XCTestCase");
+ appDelegateClassName = testClass ? @"TestMockAppDelegate" : NSStringFromClass([AppDelegate class]);
+ }
+ return UIApplicationMain(argc, argv, nil, appDelegateClassName);
+}
+```
+
+第二步:创建 mock 的简化版 `TestMockAppDelegate`,可以剔除一些 UI 测试不关心的逻辑。甚至只需要完成这个方法基础实现都可以。
+
+
+
+### 探索
+
+开发的单元测试代码,运行的背后也是一个可执行文件。查看内部,可以发现一堆和测试相关的 framework。
+
+
+
+思考:一个工程中,只要写好了测试代码,是不是加载这几个动态库就可以运行测试用例了?
+
+
+
+
+
## 六、精准测试
精准测试是最近很火的一个概念,但是也不算在概念阶段,很多公司都落地并实施了精准测试。单测是开发者为了方法级别写的测试用例。精准测试是代码级别的测试覆盖。
@@ -1316,10 +1369,12 @@ Accessibility 通过对 UI 元素进行分类和标记。分类成类似按钮
下面是之前在有赞,开发完精准测试系统后,落地到一个业务项目中取得的价值,帮助2位 QA 发现漏测的代码,倒逼 QA 去设计更完善的测试 case、分析覆盖率低是开发者的兜底代码太多还是真的漏掉了业务 case。
-
+
精准测试助力业务,质量更加稳定。
+精准测试怎么实现?核心问题是 iOS 侧开发语言有 OC、Swift,分别对应不同的编译器:clang、swiftc,插桩手段不一样。具体实现原理和细节可以看这篇文章:[精准测试最佳实践](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.108.md)
+
## 七、 测试经验总结
diff --git a/Chapter1 - iOS/1.79.md b/Chapter1 - iOS/1.79.md
index 623947c..f7c4715 100644
--- a/Chapter1 - iOS/1.79.md
+++ b/Chapter1 - iOS/1.79.md
@@ -11,7 +11,7 @@
### 悲观锁
对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定会有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
-这种线程一旦得到锁,其他需要锁的线程就挂起。共享资源每次只给一个线程使用,其他线程阻塞,用完再把资源转让给其他线程。传统的关系型数据库就用到很多悲观锁这种几只,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。
+这种线程一旦得到锁,其他需要锁的线程就挂起。共享资源每次只给一个线程使用,其他线程阻塞,用完再把资源转让给其他线程。传统的关系型数据库就用到很多悲观锁这种机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。
### 乐观锁
@@ -51,7 +51,7 @@
#### 2. CAS 算法
-**compare and swap(比较与交换)** ,是一种有名的**无锁算法**。 无锁编程,即在不实用锁的情况下实现多线程之间的数据同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫做**非阻塞同步(Non-blocking Synchorization)**。CAS 算法涉及到的三个操作数
+**compare and swap(比较与交换)** ,是一种有名的**无锁算法**。 无锁编程,即在不使用锁的情况下实现多线程之间的数据同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫做**非阻塞同步(Non-blocking Synchorization)**。CAS 算法涉及到的三个操作数
- 需要读写的内存值 V
- 进行比较的值 A
diff --git a/Chapter1 - iOS/1.82.md b/Chapter1 - iOS/1.82.md
index 6797cfd..48db64b 100644
--- a/Chapter1 - iOS/1.82.md
+++ b/Chapter1 - iOS/1.82.md
@@ -1,20 +1,23 @@
# Runtime
> 做很多技术项目或者业务项目、再或者是技术细节验证的时候会用到 Runtime 技术,用了挺久的了,本文就写、结合一些场景和源码分析来系统化学习。
+>
+> 2个问题:
+>
+> - OC 的消息机制是什么样的?
+> - OC 中 super 底层是什么?[super superclass]、[self superclass] 打印结果是什么
+>
+> 带着问题学习本文
## 动态语言
-Runtime 是实现 OC 语言动态的 API。
-
静态语言:在编译阶段确定了变量数据类型、函数地址等,无法动态修改。
动态语言:只有在运行的时候才可以决定变量属于什么类型、方法真正的地址。
-对象 `objc_object` 存了:isa、成员变量的值
-
-类 `objc_class` 存了isa、 superclass、成员变量、实例变量、对象方法、协议
+OC 是一门动态性语言,Runtime 是实现 OC 语言动态的 API。
```objectivec
@interface Person : NSObject
@@ -28,184 +31,104 @@ malloc_size((__bridge const void *)(p)) // 24 isa占8字节 + _name 指针占
class_getInstanceSize(p.class) // 32 ,系统内存对齐
```
-
-
-为什么系统是由16字节对齐的?
+QA:为什么系统是由16字节对齐的?
成员变量占用 8 字节对齐,每个对象的第一个都是 isa 指针,必须要占用8字节。举例一个极端 case,假设 n 个对象,其中 m 个对象没有成员变量,只有 isa 指针占用8字节,其中的 n-m个对象既有 isa 指针,又有成员变量。每个类交错排列,那么 CPU 在访问对象的时候会耗费大量时间去识别具体的对象。很多时候会取舍,这个 case 就是时间换空间。以16字节对齐,会加快访问速度(参考链表和数组的设计)
-
-
-## class_rw_t、class_ro_t、class_rw_ext_t 区别?
-
-`class_ro_t` 在编译时期生成的,`class_rw_t` 是在运行时期生成的。
-
-那么什么是 `class_rw_ext_t`首先明确2个概念
-
-- clean memory:加载后不会被修改。当系统内存紧张时,可以从内存中移除,需要时可以再次加载
-
-- dirty memory:加载后会被修改,一直处于内存中
-
-Runtime 初始化的时候,遇到一个类,则会利用类的 `class_ro_t` 中的基础信息(methods、properties、protocols)来创建 `class_rw_t` 对象。`class_rw_t` 设计的目的就是为了 Runtime 所需(Category 增加属性、协议、动态增加方法等),但是实际上那么多类大多数情况只有少部分类才需要 Runtime 能力。所以 Apple 为了内存优化,在 iOS 14 对 `class_rw_t` 拆分出 `class_rw_ext_t`,用来存储 Methods、Protocols、Properties 信息,会在使用的时候才创建,节省更多内存。
-
-比如访问 method 的过程
-
-```objectivec
-// 新版
-const method_array_t methods() const {
- auto v = get_ro_or_rwe();
- if (v.is()) {
- return v.get(&ro_or_rw_ext)->methods;
- } else {
- return method_array_t{v.get(&ro_or_rw_ext)->baseMethods};
- }
-}
-```
+接下去深入探索下 runtime。
-## 有类对象、为什么设计元类对象
+## 使用较少内存存储属性值
-复用消息机制。比如 `[Person new]`。
+### 位运算的设计
-元类对象:isa、元类方法、
+假设给 Person 对象设置高、富、帅3个属性,如果要用最少的内存,该怎么设计呢?
-`objc_msgSend` 设计初衷就是为了消息发送很快。假如没有元类,则类方法也存储在类对象的方法信息中,则可能需要加额外的字段来标记某个方法是类方法还是对象方法。遍历或者寻找会比较慢。所以引入元类(单一职责),设计元类的目的就是为了提高 `objc_msgSend` 的效率。
-
-
-
-## isa 本质
-
-在 arm64 架构之前,isa 就是一个普通的指针,存储着 Class或Meta-Class 对象的内存地址。
-
-在 arm64 之后,对 isa 进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。
-
-```c
-union isa_t
-{
- Class cls;
- uintptr_t bits;
- # if __arm64__
-# define ISA_MASK 0x0000000ffffffff8ULL
-# define ISA_MAGIC_MASK 0x000003f000000001ULL
-# define ISA_MAGIC_VALUE 0x000001a000000001ULL
- struct {
- uintptr_t nonpointer : 1;
- uintptr_t has_assoc : 1;
- uintptr_t has_cxx_dtor : 1;
- uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
- uintptr_t magic : 6;
- uintptr_t weakly_referenced : 1;
- uintptr_t deallocating : 1;
- uintptr_t has_sidetable_rc : 1;
- uintptr_t extra_rc : 19;
-# define RC_ONE (1ULL<<45)
-# define RC_HALF (1ULL<<18)
- };
-};
-```
-
-struct 内部的成员变量可以指定占用内存位数, `uintptr_t nonpointer : 1` 代表占用1个字节
-
-其中,结构体里面的属于”位域“
-
-- nonpointer:0,代表普通的指针,存储着Class、Meta-Class对象的内存地址;1,代表优化过,使用位域存储更多的信息
-
-- has_assoc:是否有设置过关联对象,如果没有,释放时会更快
-
-- has_cxx_dtor:是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
-
-- shiftcls:存储着Class、Meta-Class对象的内存地址信息
-
-- magic:用于在调试时分辨对象是否未完成初始化
-
-- weakly_referenced:是否有被弱引用指向过,如果没有,释放时会更快
-
-- deallocating:对象是否正在释放
-
-- extra_rc:里面存储的值是引用计数器减1(刚创建出的对象,查看这个信息位0,因为存储着-1之后的引用计数)
-
-- has_sidetable_rc:引用计数器是否过大无法存储在isa中;如果为1,那么引用计数会存储在一个叫SideTable的类的属性中
-
-上面说的更快,是如何得出结论的?
-
-查看 objc4 源代码看到对象执行销毁函数的时候会判断对象是否有关联对象、析构函数,有的话分别调用析构函数、移除关联对象等逻辑。
-
-```c
-/***********************************************************************
-* objc_destructInstance
-* Destroys an instance without freeing memory.
-* Calls C++ destructors.
-* Calls ARC ivar cleanup.
-* Removes associative references.
-* Returns `obj`. Does nothing if `obj` is nil.
-**********************************************************************/
-void *objc_destructInstance(id obj)
-{
- if (obj) {
- // Read all of the flags at once for performance.
- bool cxx = obj->hasCxxDtor();
- bool assoc = obj->hasAssociatedObjects();
-
- // This order is important.
- if (cxx) object_cxxDestruct(obj);
- if (assoc) _object_remove_assocations(obj);
- obj->clearDeallocating();
- }
-
- return obj;
-}
-```
-
-isa 在 arm64 之后必须通过 `ISA_MASK` 去查询 class(类对象、元类对象) 真正的地址
-
-`0x0000000ffffffff8ULL` 用程序员模式打开计算器
-
-
-
-其中,结构体中的数据存放大体是下面的结构:
-
-extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcls、has_has_cxx_dtor、assoc、nonpointer
-
-知道结构体可以指定存储大小这个功能后,可以看到 isa_t 联合体与 ISA_MASK 按位与之后的地址,其实就是类真实的地址信息(可能是类对象、也有可能是元类对象)
-
-如果要找出下面中间的 `1010` 如何实现?按位与即可,且要找的位置补充位1,其他位置为0
+先看几组位运算的特点:
```shell
-0b0010 1000
-
-0b0011 1100
+// 如何把最右边的0变为1,其他位不变???与 00000001 按位或即可
+0010 1000
+0000 0001
-----------
-0b0010 1000
+0010 1001
+
+
+// 如何把最右边的1变位0,其他位不变???与 11111110 按位与即可。也就是将(0000 0001)取反,再按位与即可
+0010 0001
+1111 1110
+-----------
+0010 1110
```
-结论:**根据按位与的效果。`ISA_MASK` 的后3位都是0,所以我们找到的类地址二进制表示时后3位一定为0**
+也就是说,我们可以根据位运算的特点,可以更改某个位的数据,可以随意更改为0或者1
-我们可以验证下
-```objectivec
-Person *p = [[Person alloc] init];
-NSLog(@"%p", [p class]); // 0x1000081d8
-NSLog(@"%p", object_getClass([Person class])); // 0x100008200
-NSLog(@"%p", object_getClass([NSObject class])); // 0x7ff84cb29fe0
-NSLog(@"%p", object_getClass([NSString class])); // 0x7ff84c9dcc28
+
+Person 类存在3个 BOOL 属性:
+
+- 用一个 char 来存储 tall、rich、handsome 3个属性的值,用3个位的0、1表示 BOOL
+- 从最右到左的3位代表高富帅3个布尔值。只有高则表示为:`0000 0001`,只有富则表示为 `0000 0010` ,只有帅则表示为 `0000 0100`
+- 对高的 getter 问题转换为对一个字节数据的特定位取值问题。该 case,判断高的 BOOL 值变为,对最后一位的取值。可以与 `0000 0001` 按位与,然后转换为 BOOL 即可
+- 对高的 setter ,则演变为对一个字节数据的特点位存值的问题。需要区分真假,如果为真,则可以与 `0000 0001` 按位或运算,或运算,一真为真。如果是假,则与 `1111 1110` 按位与即可,`1111 1110` 也就是 `0000 0001` 取反,也就是 `~0000 0001`
+
+上 Demo
+
+
+
+
+
+### 结构体位域
+
+位运算方案虽然实现了使用较少内存存储了 Person 的3个 BOOL 属性值。但是后续增加属性不够灵活,需要关心位运算,不具备较好拓展性
+
+新方案采用:**结构体的位域能力**,限定单个成员变量所占用的内存。代码如下:
+
+
+
+
+
+
+
+
+
+### 共用体
+
+虽然上述方式都可以实现存储 Person 类3个属性的目的,但是还有第三种方案,参考 iOS 系统设计,采用 Union 实现。代码如下
+
+
+
+分析:
+
+```c++
+union {
+ char bits;
+ struct {
+ char tall : 1;
+ char rich : 1;
+ char handsome : 1;
+ };
+} _infoUnit;
```
-为什么有的结尾是8?
+Union 里面的内容等价于下面的内容,因为 struct 使用了位域限制了成员变量的大小,所以占用3个空间的结构体还是小于等于 char 所占用的空间。
-16进制的8转为二进制,`0x1000`
+```c++
+union {
+ char bits;
+} _infoUnit;
+```
-关于这部分的调试,需要在真机上运行,真机上 arm64,拷贝对象地址到系统自带的运算器(程序员模式),查看64位地址。按照下面的顺序一一查看
+但是增加下面的结构体,是为了增加代码的可读性,内存无负向影响。
-`extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcls、has_has_cxx_dtor、assoc、nonpointer`
-所以可以根据 isa 信息查看对象是否创建过关联对象、有没有设置弱引用、
-## 模仿系统位运算设计 API
+### 位运算设计 API
-系统很多 API 都有位或运算。比如 KVO 中的 options,可以传递 `NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld` ,那么系统是如何知道我到底传递了哪几个值?
+系统很多 API 都有位或运算。比如 KVO 中的 options,可以传递 `NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld` ,那么系统是如何知道到底传递了哪几个值?搞清楚这个问题,我们就也可以设计位运算这样的 API。
-按位或运算
+先看看:按位或运算
```shell
0b0000 0001 // 1
@@ -241,48 +164,185 @@ NSLog(@"%p", object_getClass([NSString class])); // 0x7ff84c9dcc28
0b0000 0000
```
-我们发现上面3个数按位或之后的数字,分别与每个数按位与,得到的结果就是数据本身。
+我们发现上面3个数**按位或之后的数字,分别与每个数按位与,得到的结果就是数据本身**。
与一个不是3个数之一的数按位与,得到的结果为`0b0000 0000`。利用这个特性我们可以判断传递来的参数是不是包含了某个值
-```objectivec
-typedef enum {
- OptionsEast = 1<<0, // 0b0001
- OptionsSouth = 1<<1, // 0b0010
- OptionsWest = 1<<2, // 0b0100
- OptionsNorth = 1<<3 // 0b1000
-} Options;
+
-- (void)setOptions:(Options)options
-{
- if (options & OptionsEast) {
- NSLog(@"我自东边来");
- }
+有了上面的铺垫,就可以更好的查看 runtime 中 isa 的定义了。
- if (options & OptionsSouth) {
- NSLog(@"我自南边来");
- }
- if (options & OptionsWest) {
- NSLog(@"我自西边来");
- }
- if (options & OptionsNorth) {
- NSLog(@"我自北边来");
- }
-}
-[self setOptions: OptionsWest | OptionsNorth];
-// 我自西边来
-// 我自北边来
+
+
+## Runtime 源码阅读
+
+### isa 本质
+
+- 在 arm64 架构之前,isa 就是一个普通的指针,存储着 Class 或 Meta-Class 对象的内存地址。
+
+- 在 arm64 之后,对 isa 进行了优化,变成了一个共用体(union)结构,还使用了位域技术来存储更多的信息(位运算)
+
+```c++
+union isa_t {
+ // ...
+ uintptr_t bits;
+ struct {
+ ISA_BITFIELD; // defined in isa.h
+ };
+ // ...
+};
```
-## 类对象 Class 的结构
+跳转到 `isa.h` 中查看 isa 里结构体的定义(arm64 系统为例,源码是 objc4-838.1 版本),笔者将无用的代码删除了,isa 的 union 内容如下:
+
+```c++
+union isa_t {
+ uintptr_t bits;
+ struct {
+ uintptr_t nonpointer : 1;
+ uintptr_t has_assoc : 1;
+ uintptr_t has_cxx_dtor : 1;
+ uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/
+ uintptr_t magic : 6;
+ uintptr_t weakly_referenced : 1;
+ uintptr_t unused : 1;
+ uintptr_t has_sidetable_rc : 1;
+ uintptr_t extra_rc : 19
+ };
+};
+```
+
+struct 内部的成员变量可以指定占用内存位数, `uintptr_t nonpointer : 1` 代表占用1个字节,是结构体里面的 **位域**
+
+- nonpointer:0,代表普通的指针,存储着 class、meta-class 对象的内存地址;1,代表优化过,使用位域存储更多的信息
+
+- has_assoc:是否有设置过关联对象,如果没有,释放时会更快
+
+- has_cxx_dtor:是否有 c++ 的析构函数(`.cxx_destruct`),如果没有,释放时会更快
+
+- shiftcls:存储着 class、meta-class 对象的内存地址信息
+
+- magic:用于在调试时分辨对象是否未完成初始化
+
+- weakly_referenced:是否有被弱引用指向过,如果没有,释放时会更快
+
+- unused(deallocating):对象是否正在释放
+
+- extra_rc:里面存储的值是引用计数器减 1(刚创建出的对象,查看这个信息位 0,因为存储着 -1 之后的引用计数)
+
+- has_sidetable_rc:引用计数器是否过大无法存储在 isa 中;如果为1,那么引用计数会存储在一个叫 SideTable 的类的属性中
+
+
+
+上面说的更快,是如何得出结论的?
+
+查看 objc4 源代码看到对象执行销毁函数的时候会判断对象是否有关联对象、析构函数,有的话分别调用析构函数、移除关联对象等逻辑。
+
+```c++
+/***********************************************************************
+* objc_destructInstance
+* Destroys an instance without freeing memory.
+* Calls C++ destructors.
+* Calls ARC ivar cleanup.
+* Removes associative references.
+* Returns `obj`. Does nothing if `obj` is nil.
+**********************************************************************/
+void *objc_destructInstance(id obj)
+{
+ if (obj) {
+ // Read all of the flags at once for performance.
+ bool cxx = obj->hasCxxDtor();
+ bool assoc = obj->hasAssociatedObjects();
+
+ // This order is important.
+ // 存在析构函数则执行析构函数
+ if (cxx) object_cxxDestruct(obj);
+ // 存在关联对象,则移除关联对象
+ if (assoc) _object_remove_assocations(obj);
+ // 执行销毁逻辑
+ obj->clearDeallocating();
+ }
+
+ return obj;
+}
+```
+
+isa 在 arm64 之后必须通过 `ISA_MASK` 去查询 class(类对象、元类对象) 真正的地址
+
+`0x0000000ffffffff8ULL` 用程序员模式打开计算器
+
+
+
+
+
+其中,结构体中的数据存放大体是下面的结构:
+
+extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcls、has_has_cxx_dtor、assoc、nonpointer
+
+知道结构体可以指定存储大小这个功能后,可以看到 `isa_t` 联合体与 `ISA_MASK` 按位与之后的地址,其实就是类真实的地址信息(可能是类对象、也有可能是元类对象)
+
+
+
+
+
+
+
+如果要找出下面中间的 `1010` 如何实现?按位与即可,且要找的位置补充位1,其他位置为0
+
+```shell
+0b0010 1000
+
+0b0011 1100
+-----------
+0b0010 1000
+```
+
+结论:**根据按位与的效果。`ISA_MASK` 的后3位都是0,所以我们找到的类地址二进制表示时后3位一定为0**
+
+我们可以验证下
+
+```objectivec
+Person *p = [[Person alloc] init];
+NSLog(@"%p", [p class]); // 0x1000081d8
+NSLog(@"%p", object_getClass([Person class])); // 0x100008200
+NSLog(@"%p", object_getClass([NSObject class])); // 0x7ff84cb29fe0
+NSLog(@"%p", object_getClass([NSString class])); // 0x7ff84c9dcc28
+```
+
+为什么有的结尾是8?16进制的8转为二进制,`0x1000`
+
+关于这部分的调试,需要在真机上运行,真机上 arm64,拷贝对象地址到系统自带的运算器(程序员模式),查看64位地址。按照下面的顺序一一查看
+
+`extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcls、has_has_cxx_dtor、assoc、nonpointer`
+
+所以可以根据 isa 信息查看对象是否创建过关联对象、有没有设置弱引用、
+
+
+
+QA:如何理解 isa 指针?
+
+isa 指针,在 arm64 之前,isa 指针就是普通的对象,存储着类对象或元类对象的地址值。
+
+从 arm64 开始,采用共用体 union 的形式,共64位,存储了很多信息,其中33位用来存储 class、meta-class 对象的内存地址信息,其他的31位用来存储其他信息,比如 `has_assoc` 占1位,其代表是否有设置过关联对象,如果没有,释放时会更快。
+
+
+
+有类对象、为什么设计元类对象?
+
+复用消息机制。比如 `[Person new]`。元类对象:isa、元类方法。
+
+`objc_msgSend` 设计初衷就是为了消息发送很快。假如没有元类,则类方法也存储在类对象的方法信息中,则可能需要加额外的字段来标记某个方法是类方法还是对象方法。遍历或者寻找会比较慢。所以引入元类(单一职责),设计元类的目的就是为了提高 `objc_msgSend` 的效率。
+
+
+
+### 类对象 Class 的结构
查看 objc4 源代码看看
```c
struct objc_object {
-private:
isa_t isa;
}
@@ -363,110 +423,778 @@ struct class_ro_t {
具体关系整理如下图
-
+
-说明:
-- `class_rw_t`里面的 methods、properties、protocols 是数组(数组元素是也是方法组成的 Array),是可读可写的,包含了类的初始内容、分类的内容。
+
+源码解读:
+
+- 元类对象可以看成是特殊的类对象,数据类型都是 `Class`,所以大部分数据结构都一样,区别在于某些值是否有值
- 为什么不是二维数组?因为Array 中的子 Array长度不一致,且不能补空
+- `class_rw_t`:里面的 methods、properties、protocols 是数组(`method_array_t` -> `method_list_t` -> `method_t`),是可读可写的,包含了类的初始内容、分类的内容。
- 
- ```c
- static void remethodizeClass(Class cls)
- {
- category_list *cats;
- bool isMeta;
- runtimeLock.assertWriting();
+
- isMeta = cls->isMetaClass();
+ 比如访问 method 的过程
- // Re-methodizing: check for more categories
- if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
- if (PrintConnecting) {
- _objc_inform("CLASS: attaching categories to class '%s' %s",
- cls->nameForLogging(), isMeta ? "(meta)" : "");
- }
-
- attachCategories(cls, cats, true /*flush caches*/);
- free(cats);
+ ```objectivec
+ // 新版
+ const method_array_t methods() const {
+ auto v = get_ro_or_rwe();
+ if (v.is()) {
+ return v.get(&ro_or_rw_ext)->methods;
+ } else {
+ return method_array_t{v.get(&ro_or_rw_ext)->baseMethods};
}
}
- static void
- attachCategories(Class cls, category_list *cats, bool flush_caches)
- {
- if (!cats) return;
- if (PrintReplacedMethods) printReplacements(cls, cats);
-
- bool isMeta = cls->isMetaClass();
-
- // fixme rearrange to remove these intermediate allocations
- method_list_t **mlists = (method_list_t **)
- malloc(cats->count * sizeof(*mlists));
- property_list_t **proplists = (property_list_t **)
- malloc(cats->count * sizeof(*proplists));
- protocol_list_t **protolists = (protocol_list_t **)
- malloc(cats->count * sizeof(*protolists));
-
- // Count backwards through cats to get newest categories first
- int mcount = 0;
- int propcount = 0;
- int protocount = 0;
- int i = cats->count;
- bool fromBundle = NO;
- while (i--) {
- auto& entry = cats->list[i];
-
- method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
- if (mlist) {
- mlists[mcount++] = mlist;
- fromBundle |= entry.hi->isBundle();
- }
-
- property_list_t *proplist =
- entry.cat->propertiesForMeta(isMeta, entry.hi);
- if (proplist) {
- proplists[propcount++] = proplist;
- }
-
- protocol_list_t *protolist = entry.cat->protocols;
- if (protolist) {
- protolists[protocount++] = protolist;
- }
- }
-
- auto rw = cls->data();
-
- prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
- rw->methods.attachLists(mlists, mcount);
- free(mlists);
- if (flush_caches && mcount > 0) flushCaches(cls);
-
- rw->properties.attachLists(proplists, propcount);
- free(proplists);
-
- rw->protocols.attachLists(protolists, protocount);
- free(protolists);
- }
```
- 查看 objc4 源码发现针对类自身信息、Category 信息会进行组合。
-
- `class_ro_t` 里面的 baseMethodList、baseProtocols、ivars、baseProperties 是一维数组,是只读的,包含了类的(原始信息)初始内容
- 
+
-## Method_t
-`method_t` 是对方法\函数的封装
-```c
+
+
+QA:
+
+1. `method_array_t` 中存储了 `method_list_t`,`method_list_t` 的元素为 `method_list_t`, 为什么不直接称它为二维数组?
+
+ 严格来说,不是二维数组,只不过是 array 里添加的对象也是 array,且各个数组不等长,也不会补空。
+
+2. 为什么需要设计为这样的结构?
+
+ 调用方法,比如调用 load 是 runtime 加载的时候找到方法地址直接调用的,普通方法走的是消息机制。首先判断是对象方法还是类方法,然后根据 isa 找类对象(对象方法)和元类对象(对象方法)信息中先从方法缓存中查找方法是否有缓存(方法缓存查找的过程是:先根据方法的 SEL,SEL 本质上是一个指向方法名的 C 字符串指针,将其转换为 uintptr_t,然后和方法缓存哈希表的 MASK 进行按位与,MASK 值初始为 `1<isInitialized()) return;
+
+ // Make sure the entry wasn't added to the cache by some other thread
+ // before we grabbed the cacheUpdateLock.
+ if (cache_getImp(cls, sel)) return;
+
+ cache_t *cache = getCache(cls);
+ cache_key_t key = getKey(sel);
+
+ // Use the cache as-is if it is less than 3/4 full
+ mask_t newOccupied = cache->occupied() + 1;
+ mask_t capacity = cache->capacity();
+ if (cache->isConstantEmptyCache()) {
+ // Cache is read-only. Replace it.
+ // 此处传入的第二个参数为 INIT_CACHE_SIZE
+ cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
+ }
+ else if (newOccupied <= capacity / 4 * 3) {
+ // Cache is less than 3/4 full. Use it as-is.
+ }
+ else {
+ // Cache is too full. Expand it.
+ cache->expand();
+ }
+
+ // Scan for the first unused slot and insert there.
+ // There is guaranteed to be an empty slot because the
+ // minimum size is 4 and we resized at 3/4 full.
+ bucket_t *bucket = cache->find(key, receiver);
+ if (bucket->key() == 0) cache->incrementOccupied();
+ bucket->set(key, imp);
+ }
+
+ void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
+ {
+ bool freeOld = canBeFreed();
+
+ bucket_t *oldBuckets = buckets();
+ bucket_t *newBuckets = allocateBuckets(newCapacity);
+
+ // Cache's old contents are not propagated.
+ // This is thought to save cache memory at the cost of extra cache fills.
+ // fixme re-measure this
+
+ assert(newCapacity > 0);
+ assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
+ // 设置默认的 Mask,为 1<filetype == MH_EXECUTE) {
+ // Size some data structures based on main executable's size
+#if __OBJC2__
+ // If dyld3 optimized the main executable, then there shouldn't
+ // be any selrefs needed in the dynamic map so we can just init
+ // to a 0 sized map
+ if ( !hi->hasPreoptimizedSelectors() ) {
+ size_t count;
+ _getObjc2SelectorRefs(hi, &count);
+ selrefCount += count;
+ _getObjc2MessageRefs(hi, &count);
+ selrefCount += count;
+ }
+#else
+ _getObjcSelectorRefs(hi, &selrefCount);
+#endif
+
+#if SUPPORT_GC_COMPAT
+ // Halt if this is a GC app.
+ if (shouldRejectGCApp(hi)) {
+ _objc_fatal_with_reason
+ (OBJC_EXIT_REASON_GC_NOT_SUPPORTED,
+ OS_REASON_FLAG_CONSISTENT_FAILURE,
+ "Objective-C garbage collection "
+ "is no longer supported.");
+ }
+#endif
+ }
+
+ hList[hCount++] = hi;
+
+ if (PrintImages) {
+ _objc_inform("IMAGES: loading image for %s%s%s%s%s\n",
+ hi->fname(),
+ mhdr->filetype == MH_BUNDLE ? " (bundle)" : "",
+ hi->info()->isReplacement() ? " (replacement)" : "",
+ hi->info()->hasCategoryClassProperties() ? " (has class properties)" : "",
+ hi->info()->optimizedByDyld()?" (preoptimized)":"");
+ }
+ }
+ }
+
+ // Perform one-time runtime initialization that must be deferred until
+ // the executable itself is found. This needs to be done before
+ // further initialization.
+ // (The executable may not be present in this infoList if the
+ // executable does not contain Objective-C code but Objective-C
+ // is dynamically loaded later.
+ if (firstTime) {
+ sel_init(selrefCount);
+ arr_init();
+
+#if SUPPORT_GC_COMPAT
+ // Reject any GC images linked to the main executable.
+ // We already rejected the app itself above.
+ // Images loaded after launch will be rejected by dyld.
+
+ for (uint32_t i = 0; i < hCount; i++) {
+ auto hi = hList[i];
+ auto mh = hi->mhdr();
+ if (mh->filetype != MH_EXECUTE && shouldRejectGCImage(mh)) {
+ _objc_fatal_with_reason
+ (OBJC_EXIT_REASON_GC_NOT_SUPPORTED,
+ OS_REASON_FLAG_CONSISTENT_FAILURE,
+ "%s requires Objective-C garbage collection "
+ "which is no longer supported.", hi->fname());
+ }
+ }
+#endif
+
+#if TARGET_OS_OSX
+ // Disable +initialize fork safety if the app is too old (< 10.13).
+ // Disable +initialize fork safety if the app has a
+ // __DATA,__objc_fork_ok section.
+
+// if (!dyld_program_sdk_at_least(dyld_platform_version_macOS_10_13)) {
+// DisableInitializeForkSafety = true;
+// if (PrintInitializing) {
+// _objc_inform("INITIALIZE: disabling +initialize fork "
+// "safety enforcement because the app is "
+// "too old.)");
+// }
+// }
+
+ for (uint32_t i = 0; i < hCount; i++) {
+ auto hi = hList[i];
+ auto mh = hi->mhdr();
+ if (mh->filetype != MH_EXECUTE) continue;
+ unsigned long size;
+ if (getsectiondata(hi->mhdr(), "__DATA", "__objc_fork_ok", &size)) {
+ DisableInitializeForkSafety = true;
+ if (PrintInitializing) {
+ _objc_inform("INITIALIZE: disabling +initialize fork "
+ "safety enforcement because the app has "
+ "a __DATA,__objc_fork_ok section");
+ }
+ }
+ break; // assume only one MH_EXECUTE image
+ }
+#endif
+
+ }
+
+ if (hCount > 0) {
+ _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
+ }
+
+ firstTime = NO;
+
+ // Call image load funcs after everything is set up.
+ for (auto func : loadImageFuncs) {
+ for (uint32_t i = 0; i < mhCount; i++) {
+ func(mhdrs[i]);
+ }
+ }
+}
+```
+
+最后会调用
+
+```c++
+void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
+{
+ header_info *hi;
+ uint32_t hIndex;
+ size_t count;
+ size_t i;
+ Class *resolvedFutureClasses = nil;
+ size_t resolvedFutureClassCount = 0;
+ static bool doneOnce;
+ bool launchTime = NO;
+ TimeLogger ts(PrintImageTimes);
+
+ runtimeLock.assertLocked();
+
+#define EACH_HEADER \
+ hIndex = 0; \
+ hIndex < hCount && (hi = hList[hIndex]); \
+ hIndex++
+
+ if (!doneOnce) {
+ doneOnce = YES;
+ launchTime = YES;
+
+#if SUPPORT_NONPOINTER_ISA
+ // Disable non-pointer isa under some conditions.
+
+# if SUPPORT_INDEXED_ISA
+ // Disable nonpointer isa if any image contains old Swift code
+ for (EACH_HEADER) {
+ if (hi->info()->containsSwift() &&
+ hi->info()->swiftUnstableVersion() < objc_image_info::SwiftVersion3)
+ {
+ DisableNonpointerIsa = true;
+ if (PrintRawIsa) {
+ _objc_inform("RAW ISA: disabling non-pointer isa because "
+ "the app or a framework contains Swift code "
+ "older than Swift 3.0");
+ }
+ break;
+ }
+ }
+# endif
+
+# if TARGET_OS_OSX
+ // Disable non-pointer isa if the app is too old
+ // (linked before OS X 10.11)
+ // Note: we must check for macOS, because Catalyst and Almond apps
+ // return false for a Mac SDK check! rdar://78225780
+// if (dyld_get_active_platform() == PLATFORM_MACOS && !dyld_program_sdk_at_least(dyld_platform_version_macOS_10_11)) {
+// DisableNonpointerIsa = true;
+// if (PrintRawIsa) {
+// _objc_inform("RAW ISA: disabling non-pointer isa because "
+// "the app is too old.");
+// }
+// }
+
+ // Disable non-pointer isa if the app has a __DATA,__objc_rawisa section
+ // New apps that load old extensions may need this.
+ for (EACH_HEADER) {
+ if (hi->mhdr()->filetype != MH_EXECUTE) continue;
+ unsigned long size;
+ if (getsectiondata(hi->mhdr(), "__DATA", "__objc_rawisa", &size)) {
+ DisableNonpointerIsa = true;
+ if (PrintRawIsa) {
+ _objc_inform("RAW ISA: disabling non-pointer isa because "
+ "the app has a __DATA,__objc_rawisa section");
+ }
+ }
+ break; // assume only one MH_EXECUTE image
+ }
+# endif
+
+#endif
+
+ if (DisableTaggedPointers) {
+ disableTaggedPointers();
+ }
+
+ initializeTaggedPointerObfuscator();
+
+ if (PrintConnecting) {
+ _objc_inform("CLASS: found %d classes during launch", totalClasses);
+ }
+
+ // namedClasses
+ // Preoptimized classes don't go in this table.
+ // 4/3 is NXMapTable's load factor
+ int namedClassesSize =
+ (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
+ gdb_objc_realized_classes =
+ NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
+
+ ts.log("IMAGE TIMES: first time tasks");
+ }
+
+ // Fix up @selector references
+ // Note this has to be before anyone uses a method list, as relative method
+ // lists point to selRefs, and assume they are already fixed up (uniqued).
+ static size_t UnfixedSelectors;
+ {
+ mutex_locker_t lock(selLock);
+ for (EACH_HEADER) {
+ if (hi->hasPreoptimizedSelectors()) continue;
+
+ bool isBundle = hi->isBundle();
+ SEL *sels = _getObjc2SelectorRefs(hi, &count);
+ UnfixedSelectors += count;
+ for (i = 0; i < count; i++) {
+ const char *name = sel_cname(sels[i]);
+ SEL sel = sel_registerNameNoLock(name, isBundle);
+ if (sels[i] != sel) {
+ sels[i] = sel;
+ }
+ }
+ }
+ }
+
+ ts.log("IMAGE TIMES: fix up selector references");
+
+ // Discover classes. Fix up unresolved future classes. Mark bundle classes.
+ bool hasDyldRoots = dyld_shared_cache_some_image_overridden();
+
+ for (EACH_HEADER) {
+ if (! mustReadClasses(hi, hasDyldRoots)) {
+ // Image is sufficiently optimized that we need not call readClass()
+ continue;
+ }
+
+ classref_t const *classlist = _getObjc2ClassList(hi, &count);
+
+ bool headerIsBundle = hi->isBundle();
+ bool headerIsPreoptimized = hi->hasPreoptimizedClasses();
+
+ for (i = 0; i < count; i++) {
+ Class cls = (Class)classlist[i];
+ Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
+
+ if (newCls != cls && newCls) {
+ // Class was moved but not deleted. Currently this occurs
+ // only when the new class resolved a future class.
+ // Non-lazily realize the class below.
+ resolvedFutureClasses = (Class *)
+ realloc(resolvedFutureClasses,
+ (resolvedFutureClassCount+1) * sizeof(Class));
+ resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
+ }
+ }
+ }
+
+ ts.log("IMAGE TIMES: discover classes");
+
+ // Fix up remapped classes
+ // Class list and nonlazy class list remain unremapped.
+ // Class refs and super refs are remapped for message dispatching.
+
+ if (!noClassesRemapped()) {
+ for (EACH_HEADER) {
+ Class *classrefs = _getObjc2ClassRefs(hi, &count);
+ for (i = 0; i < count; i++) {
+ remapClassRef(&classrefs[i]);
+ }
+ // fixme why doesn't test future1 catch the absence of this?
+ classrefs = _getObjc2SuperRefs(hi, &count);
+ for (i = 0; i < count; i++) {
+ remapClassRef(&classrefs[i]);
+ }
+ }
+ }
+
+ ts.log("IMAGE TIMES: remap classes");
+
+#if SUPPORT_FIXUP
+ // Fix up old objc_msgSend_fixup call sites
+ for (EACH_HEADER) {
+ message_ref_t *refs = _getObjc2MessageRefs(hi, &count);
+ if (count == 0) continue;
+
+ if (PrintVtables) {
+ _objc_inform("VTABLES: repairing %zu unsupported vtable dispatch "
+ "call sites in %s", count, hi->fname());
+ }
+ for (i = 0; i < count; i++) {
+ fixupMessageRef(refs+i);
+ }
+ }
+
+ ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
+#endif
+
+
+ // Discover protocols. Fix up protocol refs.
+ for (EACH_HEADER) {
+ extern objc_class OBJC_CLASS_$_Protocol;
+ Class cls = (Class)&OBJC_CLASS_$_Protocol;
+ ASSERT(cls);
+ NXMapTable *protocol_map = protocols();
+ bool isPreoptimized = hi->hasPreoptimizedProtocols();
+
+ // Skip reading protocols if this is an image from the shared cache
+ // and we support roots
+ // Note, after launch we do need to walk the protocol as the protocol
+ // in the shared cache is marked with isCanonical() and that may not
+ // be true if some non-shared cache binary was chosen as the canonical
+ // definition
+ if (launchTime && isPreoptimized) {
+ if (PrintProtocols) {
+ _objc_inform("PROTOCOLS: Skipping reading protocols in image: %s",
+ hi->fname());
+ }
+ continue;
+ }
+
+ bool isBundle = hi->isBundle();
+
+ protocol_t * const *protolist = _getObjc2ProtocolList(hi, &count);
+ for (i = 0; i < count; i++) {
+ readProtocol(protolist[i], cls, protocol_map,
+ isPreoptimized, isBundle);
+ }
+ }
+
+ ts.log("IMAGE TIMES: discover protocols");
+
+ // Fix up @protocol references
+ // Preoptimized images may have the right
+ // answer already but we don't know for sure.
+ for (EACH_HEADER) {
+ // At launch time, we know preoptimized image refs are pointing at the
+ // shared cache definition of a protocol. We can skip the check on
+ // launch, but have to visit @protocol refs for shared cache images
+ // loaded later.
+ if (launchTime && hi->isPreoptimized())
+ continue;
+ protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
+ for (i = 0; i < count; i++) {
+ remapProtocolRef(&protolist[i]);
+ }
+ }
+
+ ts.log("IMAGE TIMES: fix up @protocol references");
+
+ // Discover categories. Only do this after the initial category
+ // attachment has been done. For categories present at startup,
+ // discovery is deferred until the first load_images call after
+ // the call to _dyld_objc_notify_register completes. rdar://problem/53119145
+ if (didInitialAttachCategories) {
+ for (EACH_HEADER) {
+ load_categories_nolock(hi);
+ }
+ }
+
+ ts.log("IMAGE TIMES: discover categories");
+
+ // Category discovery MUST BE Late to avoid potential races
+ // when other threads call the new category code before
+ // this thread finishes its fixups.
+
+ // +load handled by prepare_load_methods()
+
+ // Realize non-lazy classes (for +load methods and static instances)
+ for (EACH_HEADER) {
+ classref_t const *classlist = hi->nlclslist(&count);
+ for (i = 0; i < count; i++) {
+ Class cls = remapClass(classlist[i]);
+ if (!cls) continue;
+
+ addClassTableEntry(cls);
+
+ if (cls->isSwiftStable()) {
+ if (cls->swiftMetadataInitializer()) {
+ _objc_fatal("Swift class %s with a metadata initializer "
+ "is not allowed to be non-lazy",
+ cls->nameForLogging());
+ }
+ // fixme also disallow relocatable classes
+ // We can't disallow all Swift classes because of
+ // classes like Swift.__EmptyArrayStorage
+ }
+ realizeClassWithoutSwift(cls, nil);
+ }
+ }
+
+ ts.log("IMAGE TIMES: realize non-lazy classes");
+
+ // Realize newly-resolved future classes, in case CF manipulates them
+ if (resolvedFutureClasses) {
+ for (i = 0; i < resolvedFutureClassCount; i++) {
+ Class cls = resolvedFutureClasses[i];
+ if (cls->isSwiftStable()) {
+ _objc_fatal("Swift class is not allowed to be future");
+ }
+ realizeClassWithoutSwift(cls, nil);
+ cls->setInstancesRequireRawIsaRecursively(false/*inherited*/);
+ }
+ free(resolvedFutureClasses);
+ }
+
+ ts.log("IMAGE TIMES: realize future classes");
+
+ if (DebugNonFragileIvars) {
+ realizeAllClasses();
+ }
+
+
+ // Print preoptimization statistics
+ if (PrintPreopt) {
+ static unsigned int PreoptTotalMethodLists;
+ static unsigned int PreoptOptimizedMethodLists;
+ static unsigned int PreoptTotalClasses;
+ static unsigned int PreoptOptimizedClasses;
+
+ for (EACH_HEADER) {
+ if (hi->hasPreoptimizedSelectors()) {
+ _objc_inform("PREOPTIMIZATION: honoring preoptimized selectors "
+ "in %s", hi->fname());
+ }
+ else if (hi->info()->optimizedByDyld()) {
+ _objc_inform("PREOPTIMIZATION: IGNORING preoptimized selectors "
+ "in %s", hi->fname());
+ }
+
+ classref_t const *classlist = _getObjc2ClassList(hi, &count);
+ for (i = 0; i < count; i++) {
+ Class cls = remapClass(classlist[i]);
+ if (!cls) continue;
+
+ PreoptTotalClasses++;
+ if (hi->hasPreoptimizedClasses()) {
+ PreoptOptimizedClasses++;
+ }
+
+ const method_list_t *mlist;
+ if ((mlist = cls->bits.safe_ro()->baseMethods)) {
+ PreoptTotalMethodLists++;
+ if (mlist->isFixedUp()) {
+ PreoptOptimizedMethodLists++;
+ }
+ }
+ if ((mlist = cls->ISA()->bits.safe_ro()->baseMethods)) {
+ PreoptTotalMethodLists++;
+ if (mlist->isFixedUp()) {
+ PreoptOptimizedMethodLists++;
+ }
+ }
+ }
+ }
+
+ _objc_inform("PREOPTIMIZATION: %zu selector references not "
+ "pre-optimized", UnfixedSelectors);
+ _objc_inform("PREOPTIMIZATION: %u/%u (%.3g%%) method lists pre-sorted",
+ PreoptOptimizedMethodLists, PreoptTotalMethodLists,
+ PreoptTotalMethodLists
+ ? 100.0*PreoptOptimizedMethodLists/PreoptTotalMethodLists
+ : 0.0);
+ _objc_inform("PREOPTIMIZATION: %u/%u (%.3g%%) classes pre-registered",
+ PreoptOptimizedClasses, PreoptTotalClasses,
+ PreoptTotalClasses
+ ? 100.0*PreoptOptimizedClasses/PreoptTotalClasses
+ : 0.0);
+ _objc_inform("PREOPTIMIZATION: %zu protocol references not "
+ "pre-optimized", UnfixedProtocolReferences);
+ }
+
+#undef EACH_HEADER
+}
+```
+
+核心相关代码
+
+```c++
+/***********************************************************************
+* realizeClassWithoutSwift
+* Performs first-time initialization on class cls,
+* including allocating its read-write data.
+* Does not perform any Swift-side initialization.
+* Returns the real class structure for the class.
+* Locking: runtimeLock must be write-locked by the caller
+**********************************************************************/
+static Class realizeClassWithoutSwift(Class cls, Class previously)
+{
+ // ...
+ class_rw_t *rw;
+ Class supercls;
+ Class metacls;
+ auto ro = (const class_ro_t *)cls->data();
+ auto isMeta = ro->flags & RO_META;
+ if (ro->flags & RO_FUTURE) {
+ // This was a future class. rw data is already allocated.
+ rw = cls->data();
+ ro = cls->data()->ro();
+ ASSERT(!isMeta);
+ cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
+ } else {
+ // Normal class. Allocate writeable class data.
+ rw = objc::zalloc();
+ rw->set_ro(ro);
+ rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
+ cls->setData(rw);
+ }
+ cls->cache.initializeToEmptyOrPreoptimizedInDisguise();
+ // ...
+}
+
+struct class_rw_ext_t {
+ DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
+ class_ro_t_authed_ptr ro;
+ method_array_t methods;
+ property_array_t properties;
+ protocol_array_t protocols;
+ char *demangledName;
+ uint32_t version;
+};
+```
+
+查看 objc4 源码,runtime 启动步骤可以知道:
+
+- `struct objc_class` 结构体中的 `bits` 一开始指向的是 `class_ro_t` 结构体,然后经过 runtime 的 `realizeClassWithoutSwift` 方法,将 `bits` 指向一个新创建的 `class_rw_t` 结构体,新创建的结构体中的成员变量 `ro` 指向类原本的 `class_ro_t`。也就是说 runtime 会对类自身信息和 Category 信息进行组合。
+- `class_ro_t` 在编译时期生成的,`class_rw_t` 是在运行时期生成的。
+
+那么什么是 `class_rw_ext_t`首先明确 2。个概念
+
+- clean memory:加载后不会被修改。当系统内存紧张时,可以从内存中移除,需要时可以再次加载
+
+- dirty memory:加载后会被修改,一直处于内存中
+
+
+
+runtime 初始化的时候,遇到一个类,则会利用类的 `class_ro_t` 中的基础信息(methods、properties、protocols)来创建 `class_rw_t` 对象。`class_rw_t` 设计的目的就是为了 runtime 所需(Category 增加属性、协议、动态增加方法等),但是实际上写了很多的类,只有少部分类才需要 runtime 能力。所以 Apple 为了内存优化,在 iOS 14 对 `class_rw_t` 拆分出 `class_rw_ext_t`,用来存储 Methods、Protocols、Properties 信息,只有在使用的时候才创建,节省更多内存。
+
+
+
+## Runtime - 方法
+
+### Method_t
+
+`method_t` 是对方法、函数的封装
+
+```c++
struct method_t {
- SEL name; // 函数名、方法名
- const char *types; // 编码(返回值类型、参数类型)
- IMP imp; // 指向函数的指针(函数地址)
+ // ...
+ struct big {
+ SEL name; // 函数名、方法名
+ const char *types; // 编码(返回值类型、参数类型)
+ MethodListIMP imp; // 指向函数的指针(函数地址,给方法下断点的话,汇编模式的第一条指令的地址就是函数地址)
+ };
+ struct small {
+ RelativePointer name;
+ RelativePointer types;
+ RelativePointer imp;
+ };
+ // ...
}
```
@@ -476,7 +1204,7 @@ struct method_t {
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
```
-`SEL` 代表方法、函数名,一般叫做选择器,底层结构跟 `char *` 类似
+`SEL` 代表方法、函数名,一般叫做选择器,底层结构跟 `char *` 类似。本质上是一个指向方法名 C 字符串的指针。
```c
typedef struct objc_selector *SEL;
@@ -486,76 +1214,163 @@ typedef struct objc_selector *SEL;
- 可以通过 `sel_getName()` 和 `NSStringFromSelector()` 转成字符串
-- 不同类中相同名字的方法,所对应的方法选择器是相同的
+- 不同类中相同名字的方法,所对应的方法选择器是相同的。也就是 SEL 不具备唯一性,方法命名需要规范,否则 runtime 调用起来就会发生不符合预期的行为。
`types` 包含了函数返回值、参数编码的字符串。`返回值|参数1|参数2| ... | 参数n`
-**Type Encoding**
+
+
+### Type Encoding
iOS 中提供了一个叫做 `@encode` 的指令,可以将具体的类型表示成字符串编码
-
-
-```objectivec
-- (int)calcuate:(int)age heigith:(float)height;
+```objective-c
+NSLog(@"%s", @encode(int));
+NSLog(@"%s", @encode(id));
+NSLog(@"%s", @encode(void));
+NSLog(@"%s", @encode(SEL));
+NSLog(@"%s", @encode(Person));
+// console
+i
+@
+v
+:
+{Person=#}
```
-比如这个方法的 type encoding 为 `i24@0:8i16f20`
+可以对照下面的表格进行查看:
-解读下,上面的方法其实携带了2个基础参数。
+
-`(id)self _cmd:(SEL)_cmd`
+```objectivec
+- (int)calcuate:(int)baseHeight heigith:(float)height;
+```
-`i` 代表方法返回值为 int
+比如这个方法的 `type encoding` 为 `i24@0:8i16f20`
-`24` 代表参数共占24个字节大小。4个参数分别为 id 类型的 `self`、`SEL` 类型的 `_cmd`, int 类型的 age、float 类型的 height。8+8+4+4 共24个字节(id、SEL 都为指针,长度为8)
+解读下,上面的方法其实携带了2个基础参数,`(id)self _cmd:(SEL)_cmd` :
-`@` 代表第一个参数为 object 类型,从第0个字节开始
+- `i` 代表方法返回值为 int
-`:`代表第二个参数为 SEL,从第8个字节开始
+- `24` 代表参数共占24个字节大小。4个参数分别为 id 类型的 `self`、`SEL` 类型的 `_cmd`, int 类型的 age、float 类型的 height。8+8+4+4 共24个字节(id、SEL 都为指针,长度为8)
-`i` 代表第三个参数为 int,从第16个字节开始
+- `@` 代表第一个参数为 id 类型,从第0个字节开始,即 self
-`f` 代表第四个参数为 float,从第20个字节开始
+- `:`代表第二个参数为 SEL,从第8个字节开始
-## 方法缓存
+- `i` 代表第三个参数为 int,从第16个字节开始,占4个字节
-调用方法的本质,比如说对象方法,先根据对象的 isa 找到类对象,在类对象的 `method_list_t` 类型的 methods 方法数组(Array 中的元素是方法 Array)中(类的Category1、类的 Category2... 类自身的方法)查找方法,找不到则调用 superclass 查找父类的 methods 方法数组(Array 中的元素是方法 Array),效率较低,所以为了方便,给类设置了方法缓存。比如调用 Student 对象的 eat 方法,eat 在 student 中不存在,通过 isa 不断找,在 Person 类中找到了,则将 Person 类中的 eat 方法缓存在 Student 的 `cache_t` 类型的 cache 中。
+- `f` 代表第四个参数为 float,从第20个字节开始
+
+
+
+### 方法缓存
`Class` 内部结构中有个方法缓存(cache_t),用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度
+- 用于快速查找方法执行函数
+- 是可增量拓展的哈希表结构
+- 是**局部性原理**的最佳应用(一般调用方法的时候,并不会每个方法都调用,一般来说可能会调用某个类的某几个方法,这几个方法每调用过1次,就缓存起来,下次再去调用的时候,就省去了 runtime 中方法查找的流程,提高效率)
+
+```c++
+/// Represents an instance of a class.
+struct objc_object {
+ Class _Nonnull isa OBJC_ISA_AVAILABILITY;
+};
+
+struct objc_class : objc_object {
+ // Class ISA;
+ // ...
+ Class superclass;
+ cache_t cache; // formerly cache pointer and vtable
+ class_data_bits_t bits;
+}
+```
+
+调用方法的本质,比如说对象方法,先根据对象的 isa 找到类对象,在类对象的 `method_list_t` 类型的 methods 方法数组(Array 中的元素是方法 Array)中(类的Category1、类的 Category2... 类自身的方法)查找方法,找不到则调用 superclass 查找父类的 methods 方法数组(Array 中的元素是方法 Array),假设某个类的方法调用 `[Worker live]`,需要通过 superclass 查找8次,每次都是在方法的“二维数组”里遍历。某个逻辑就需要调用 `[Worker live]`6次,每次需要8次二维数组的遍历,可想而知,效率低下。
+
+所以为了方便,给类设置了**方法缓存**。比如调用 Worker 对象的 live 方法,通过 isa 找到元类对象,元类对象中不存在,则通过 superclass 找父类的元类对象, 不断找,假设经历了8次 superclass,最后在 Person 类中找到了,则将 Person 类中的 eat 方法缓存在 Worker 的 `cache_t` 类型的 cache 中。
+
+
+
所以完整结构为:先根据对象的 isa 找到类对象,在类对象的 cache 列表中查找方法实现,如果找不到,则去 `method_list_t` 类型的 methods 方法数组(Array 中的元素是方法 Array)中(类的Category1、类的 Category2... 类自身的方法)查找方法,找不到则调用 superclass 查找父类的 cache 中查找,找到则调用方法,同时将父类 cache 缓存中的方法,在子类的 cache 中缓存一边。父类 cache 没找到,则在 methods 方法数组(Array 中的元素是方法 Array)查找,找到则调用,同时在子类 cache 中缓存一份。父类 methods 方法数组(Array 中的元素是方法 Array)没找到则继续调用 superclass,依次类推
```c
struct cache_t {
- struct bucket_t *_buckets; // 散列表
- mask_t _mask; // 散列表的参数 -1
+ struct bucket_t *_buckets; // 散列表 -> | bucket_t |bucket_t |bucket_t |bucket_t |...
+ mask_t _mask; // 散列表的长度 -1
mask_t _occupied; // 已经缓存的方法数量
}
```
```c
struct bucket_t {
-private:
cache_key_t _key; // SEL 作为 key
IMP _imp; // 函数的内存地址
}
```
-`_buckets` -> | bucket_t |bucket_t |bucket_t |bucket_t |...
+方法缓存查找原理,就是利用散列表(哈希表)查找。涉及:空间换时间,哈希表拓容策略,哈希碰撞算法。
+
-方法缓存查找原理,散列表查找
objc4 源码 `objc-cache.mm`
-```c
+```c++
+typedef uintptr_t cache_key_t;
+
+// 根据 SEL 计算方法缓存 key 就说根据 SEL(方法名 C 字符串的指针)转换为一个用于存储指针的整数值(这个类型是一个无符号整数类型,uintptr_t 提供了一种方式来将指针转换为一个整数,以及将整数转换回指针)
+cache_key_t getKey(SEL sel)
+{
+ assert(sel);
+ return (cache_key_t)sel;
+}
+
+static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
+{
+ cacheUpdateLock.assertLocked();
+
+ // Never cache before +initialize is done
+ if (!cls->isInitialized()) return;
+
+ // Make sure the entry wasn't added to the cache by some other thread
+ // before we grabbed the cacheUpdateLock.
+ if (cache_getImp(cls, sel)) return;
+ // 获取方法缓存 cache_t
+ cache_t *cache = getCache(cls);
+ // 根据 SEL 计算方法缓存 key
+ cache_key_t key = getKey(sel);
+
+ // Use the cache as-is if it is less than 3/4 full
+ mask_t newOccupied = cache->occupied() + 1;
+ mask_t capacity = cache->capacity();
+ if (cache->isConstantEmptyCache()) {
+ // Cache is read-only. Replace it.
+ cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
+ }
+ else if (newOccupied <= capacity / 4 * 3) {
+ // Cache is less than 3/4 full. Use it as-is.
+ }
+ else {
+ // Cache is too full. Expand it.
+ cache->expand();
+ }
+
+ // Scan for the first unused slot and insert there.
+ // There is guaranteed to be an empty slot because the
+ // minimum size is 4 and we resized at 3/4 full.
+ bucket_t *bucket = cache->find(key, receiver);
+ if (bucket->key() == 0) cache->incrementOccupied();
+ bucket->set(key, imp);
+}
+
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
assert(k != 0);
bucket_t *b = buckets();
mask_t m = mask();
- mask_t begin = cache_hash(k, m);
+ mask_t begin = cache_hash(k, m); // 该方法实现就是 k & mask,按位与
mask_t i = begin;
do {
if (b[i].key() == 0 || b[i].key() == k) {
@@ -567,15 +1382,29 @@ bucket_t * cache_t::find(cache_key_t k, id receiver)
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}
+
+static inline mask_t cache_next(mask_t i, mask_t mask) {
+ return i ? i-1 : mask;
+}
```
-散列表不够了,则会哈希拓容,此时缓存会释放 `cache_collect_free`
+可以看到在查找方法缓存的时候:
+
+- 首先根据缓存 key,利用 `cache_hash` 方法计算出一个 begin,赋值给 i
+- 然后利用 i 在方法缓存 `cache_t` 的 `buckets` 中查找,假设 key 相等,则直接返回对应的方法
+- 如果没有找到则执行 `cache_next` 方法,该方法则会判断 i 是不是等于0,不等于0则自减1,等于0则设置为 mask 的值
+- mask 值,在设计上等于 `buckets` 散列表的长度减1
+
+
+
+散列表长度不够了,则会哈希拓容,此时之前存储的方法缓存则会被释放,执行 `cache_collect_free`
```c
void cache_t::expand()
{
cacheUpdateLock.assertLocked();
uint32_t oldCapacity = capacity();
+ // 扩容,容量变为原来的2倍大小
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
if ((uint32_t)(mask_t)newCapacity != newCapacity) {
// mask overflow - can't grow further
@@ -597,6 +1426,7 @@ void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
setBucketsAndMask(newBuckets, newCapacity - 1);
if (freeOld) {
+ // 释放之前的方法缓存
cache_collect_free(oldBuckets, oldCapacity);
cache_collect(false);
}
@@ -612,183 +1442,17 @@ static inline mask_t cache_hash(cache_key_t key, mask_t mask)
}
```
-空间换时间的一个实现。
+空间换时间的一个实现。且按位与的特点,`(key & mask)` 的结果一定比 mask 值小。
查找类的方法缓存 Demo
-```objectivec
-#import
-
-#ifndef MockClassInfo_h
-#define MockClassInfo_h
-
-# if __arm64__
-# define ISA_MASK 0x0000000ffffffff8ULL
-# elif __x86_64__
-# define ISA_MASK 0x00007ffffffffff8ULL
-# endif
-
-#if __LP64__
-typedef uint32_t mask_t;
-#else
-typedef uint16_t mask_t;
-#endif
-typedef uintptr_t cache_key_t;
-
-struct bucket_t {
- cache_key_t _key;
- IMP _imp;
-};
-
-struct cache_t {
- bucket_t *_buckets;
- mask_t _mask;
- mask_t _occupied;
-};
-
-struct eint main () {
- GoodStudent *goodStudent = [[GoodStudent alloc] init];
- mock_objc_class *goodStudentClass = (__bridge mj_objc_class *)[GoodStudent class];
- [goodStudent goodStudentTest];
- [goodStudent studentTest];
- [goodStudent personTest];
- return 0;
-}ntsize_list_tt {
- uint32_t entsizeAndFlags;
- uint32_t count;
-};
-
-struct method_t {
- SEL name;
- const char *types;
- IMP imp;
-};
-
-struct method_list_t : entsize_list_tt {
- method_t first;
-};
-
-struct ivar_t {
- int32_t *offset;
- const char *name;
- const char *type;
- uint32_t alignment_raw;
- uint32_t size;
-};
-
-struct ivar_list_t : entsize_list_tt {
- ivar_t first;
-};
-
-struct property_t {
- const char *name;
- const char *attributes;
-};
-
-struct property_list_t : entsize_list_tt {
- property_t first;
-};
-
-struct chained_property_list {
- chained_property_list *next;
- uint32_t count;
- property_t list[0];
-};
-
-typedef uintptr_t protocol_ref_t;
-struct protocol_list_t {
- uintptr_t count;
- protocol_ref_t list[0];
-};
-
-struct class_ro_t {
- uint32_t flags;
- uint32_t instanceStart;
- uint32_t instanceSize; // instance对象占用的内存空间
-#ifdef __LP64__
- uint32_t reserved;
-#endif
- const uint8_t * ivarLayout;
- const char * name; // 类名
- method_list_t * baseMethodList;
- protocol_list_t * baseProtocols;
- const ivar_list_t * ivars; // 成员变量列表
- const uint8_t * weakIvarLayout;
- property_list_t *baseProperties;
-};
-
-struct class_rw_t {
- uint32_t flags;
- uint32_t version;
- const class_ro_t *ro;
- method_list_t * methods; // 方法列表
- property_list_t *properties; // 属性列表
- const protocol_list_t * protocols; // 协议列表
- Class firstSubclass;
- Class nextSiblingClass;
- char *demangledName;
-};
-
-#define FAST_DATA_MASK 0x00007ffffffffff8UL
-struct class_data_bits_t {
- uintptr_t bits;
-public:
- class_rw_t* data() {
- return (class_rw_t *)(bits & FAST_DATA_MASK);
- }
-};
-
-/* OC对象 */
-struct mock_objc_object {
- void *isa;
-};
-
-/* 类对象 */
-struct mock_objc_class : mock_objc_object {
- Class superclass;
- cache_t cache;
- class_data_bits_t bits;
-public:
- class_rw_t* data() {
- return bits.data();
- }
-
- mock_objc_class* metaClass() {
- return (mock_objc_class *)((long long)isa & ISA_MASK);
- }
-};
-
-#endif /* MockClassInfo_h */
-
-@interface Person : NSObject
-- (void)personSay;
-@end
-
-@interface Student : Person
-- (void)studentSay;
-@end
-
-@interface GoodStudent : Student
-- (void)goodStudentSay;
-@end
-
-int main () {
- GoodStudent *goodStudent = [[GoodStudent alloc] init];
- mock_objc_class *goodStudentClass = (__bridge mj_objc_class *)[GoodStudent class];
- // breakpoints1
- [goodStudent goodStudentSay];
- // breakpoints2
- [goodStudent studentSay];
- // breakpoints3
- [goodStudent personSay];
- // breakpoints4
- [goodStudent goodStudentSay];
- // breakpoints5
- [goodStudent studentSay];
- // breakpoints6
- NSLog(@"well donw");
- return 0;
-}
+```objective-c
+GoodStudent *goodStudent = [[GoodStudent alloc] init];
+mock_objc_class *personClass = (__bridge mock_objc_class *)[GoodStudent class];
+[goodStudent goodStudentSay]; // 断点1
+[goodStudent studentSay]; // 断点2
+[goodStudent personSay]; // 断点3
+NSLog(@"");
```
流程:
@@ -828,27 +1492,34 @@ void cache_t::expand()
在断点6的地方,`_occupied` 为3则代表只有 personSay、goodStudentSay、studentSay 方法被缓存, `_mask` 为7。
-如何根据方法散列表查找某个方法
+
+
+#### 如何根据方法散列表查找某个方法
```objectivec
-GoodStudent *student = [[GoodStudent alloc] init];
-mock_objc_class *studentClass = (__bridge mock_objc_class *)[GoodStudent class];
-[student goodStudentSay];
-[student studentSay];
-[student personSay];
-NSLog(@"Well done");
+GoodStudent *goodStudent = [[GoodStudent alloc] init];
+mock_objc_class *personClass = (__bridge mock_objc_class *)[GoodStudent class];
+[goodStudent goodStudentSay];
+[goodStudent studentSay];
+[goodStudent personSay];
-cache_t cache = studentClass->cache;
+cache_t cache = personClass->cache;
bucket_t *buckets = cache._buckets;
-
-bucket_t bucket = buckets[(long long)@selector(personSay) & cache._mask];
-NSLog(@"%s %p", bucket._key, bucket._imp);
-// personSay 0xbec8
+// for (int i = 0; i <= cache._mask; i++) {
+// bucket_t bucket = buckets[i];
+// NSLog(@"%s -- %p", (SEL)bucket._key, bucket._imp);
+// }
+bucket_t goodStudentSayBucket = buckets[(uintptr_t)@selector(personSay) & cache._mask];
+NSLog(@"%s %p", goodStudentSayBucket._key, goodStudentSayBucket._imp);
```
-
+
-原理就是根据类对象结构体找到 cache 结构体,cache 结构体内部的 `_buckets` 是一个方法散列表,查看源代码,根据散列表的哈希寻找策略 `(key & mask)` 找到哈希索引,然后找到方法对象 bucket,其中寻找方法索引的 key 就是 方法 selector。
+
+
+
+
+原理就是根据类对象结构体找到 cache 结构体,cache 结构体内部的 `_buckets` 是一个方法散列表,查看源代码,根据散列表的哈希寻找策略 `(key & mask)` 找到哈希索引,然后找到方法对象 bucket,其中寻找方法索引的 key 就是方法 selector。
```c
static inline mask_t cache_hash(cache_key_t key, mask_t mask)
@@ -857,17 +1528,35 @@ static inline mask_t cache_hash(cache_key_t key, mask_t mask)
}
```
-## objc_msgSend
+`[student live]`当调用对象方法的时候。会根据对象的 isa 指针,找到类对象,然后在类对象的方法缓存中查找有没有方法实现
+
+- 如果找到了则立马执行
+- 如果缓存中没有方法实现,则会在 class_rw_t 的 methods 里面查找有没有方法实现
+ - 如果找到了则将方法更新到方法缓存中去,然后立马执行(查找的过程则是根据 SEL 的方法名 C 字符串指针地址值与 _mask 按位与运算,然后在方法缓存的哈希表中查找 比如 `cache.buckets[@selector(methodName) & _mask]` )
+ - 如果此时还是找不到。则根据类对象的 superclass 指针查找父类的类对象。到父类的类对象这一步,也先从方法缓存中查找有没有方法缓存:
+ - 如果找到了,则立马执行,同时将方法缓存到子类自己的缓存中去。下次调用,则直接在子类自己的方法缓存中查找即可
+ - 如果没找到,则继续在 class_rw_t 的 methods 中查找方法实现。
+ 1. 如果找到了,则更新到子类类对象的方法缓存中,然后执行方法
+ 2. 如果没找到,则继续沿着父类类对象的 superclass 指针,继续往上找,查找流程和上面的步骤类似
+- 如果一直找到根类,还是找不到则开始走消息机制...
+
+
+
+## Runtime - objc_msgSend
+
+
+
+```c++
+Person *p = [[Person alloc] init];
+[p eat];
+objc_msgSend(p, sel_registerName("eat"));
+
+[Person sayHi];
+objc_msgSend(object_getClass("Person"), sel_registerName("sayHi"));
+```
oc 方法(对象方法、类方法)调用本质就是 `objc_msgSend`
-```objectivec
-[person eat];
-objc_msgSend(person, sel_registerName("eat"));
-[Person initialize];
-objc_msgSend([Person class], sel_registerName("initialize"));
-```
-
`objc_msgSend` 可以分为3个阶段:
- 消息发送
@@ -876,6 +1565,8 @@ objc_msgSend([Person class], sel_registerName("initialize"));
- 消息转发
+这3个阶段,还是没调用成功则抛出错误:`unrecognized selector sent to instance 0x600000ad0260`
+
查看源码 `objc-msg-arm64.s`
```shell
@@ -932,7 +1623,7 @@ LReturnZero:
ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
and w12, w1, w11 // x12 = _cmd & mask
add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4)
-
+ // 可以看到 cache.buckets[_cmd & mask] == SEL 就是在做方法缓存判断逻辑,看看有没有命中方法缓存
ldp x9, x17, [x12] // {x9, x17} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
@@ -955,21 +1646,20 @@ LReturnZero:
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
-
+ // cachehit 就是命中缓存,则调用或者 return imp
2: // not hit: x12 = not-hit bucket
- // 这里是方法查找失败,则走 checkMiss 逻辑,具体看下面
- CheckMiss $0 // miss if bucket->sel == 0
+ // checkmiss,就是缓存没命中,也就是方法查找失败,则走 checkMiss 逻辑,具体看下面
+ CheckMiss $0 // miss if bucket->sel == 0,NORMAL
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
b 1b // loop
-
3: // double wrap
JumpMiss $0
.endmacro
-// CheckMiss 汇编宏,上面走 Normal 逻辑,内部走 __objc_msgSend_uncached 流程
+// CheckMiss 汇编宏,上面传入了参数 Normal,内部走 __objc_msgSend_uncached 流程。不同参数,走不同的方法调用逻辑
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
@@ -1039,8 +1729,8 @@ END_ENTRY __objc_msgSend_uncached
.endmacro
```
-Tips:c 方法在汇编中使用的时候,需要在方法名前加 `_` 。所以在汇编中某个方法为 `_xxx`,则在其他地方查找实现,需要去掉 `_`
-此时 `__class_lookupMethodAndLoadCache3` 在汇编中没有实现,则按照 `_class_lookupMethodAndLoadCache3` 查找
+Tips:c 方法在汇编中使用的时候,需要在方法名前加 `_` 。所以在汇编中某个方法为 `_xxx`,则在其他地方查找实现,需要去掉 `_`。
+此时 `__class_lookupMethodAndLoadCache3` 在汇编中没有实现,则去掉一个 `_` 按照 `_class_lookupMethodAndLoadCache3` 在非汇编代码中查找
```c
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
@@ -1058,6 +1748,7 @@ IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
runtimeLock.assertUnlocked();
// Optimistic cache lookup
+ // 首先从方法缓存中查找,如果找到则 return imp,也就是把方法地址返回给 bl 指令
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
@@ -1100,13 +1791,14 @@ IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
retry:
runtimeLock.assertReading();
-
+ // 这里也先从方法缓存中查找,目的是前面查找后,后续又通过 runtime 的方式增加了方法缓存
// Try this class's cache.
imp = cache_getImp(cls, sel);
- if (imp) goto done;
+ if (imp) goto done; // 找到方法则执行 done 里面的逻辑,done 里面也是将 IMP 的地址返回出去
// Try this class's method lists.
{
+ // 通过 getMethodNoSuper_nolock 方法查找 cls 中有没有 SEL 方法
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
@@ -1217,7 +1909,7 @@ static method_t *search_method_list(const method_list_t *mlist, SEL sel)
if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
return findMethodInSortedMethodList(sel, mlist);
} else {
- // 没排序则线性查找
+ // 没排序则线性查找,顺序依次遍历查找
// Linear search of unsorted method list
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
@@ -1303,14 +1995,14 @@ log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
if (!cacheIt) return;
}
#endif
- cache_fill (cls, sel, imp, receiver);
+ cache_fill (cls, sel, imp, receiver); // 继续走 cache_fill 逻辑
}
void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
mutex_locker_t lock(cacheUpdateLock);
- cache_fill_nolock(cls, sel, imp, receiver);
+ cache_fill_nolock(cls, sel, imp, receiver); // 继续走 cache_fill_nolock 逻辑
#else
_collecting_in_critical();
return;
@@ -1327,8 +2019,9 @@ static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
// Make sure the entry wasn't added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
if (cache_getImp(cls, sel)) return;
-
+ // 根据类对象/类的元类对象获得方法缓存 cache_t
cache_t *cache = getCache(cls);
+ //根据方法 key 调用 getKey 方法,获取方法缓存哈希表的 key
cache_key_t key = getKey(sel);
// Use the cache as-is if it is less than 3/4 full
@@ -1349,8 +2042,10 @@ static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot because the
// minimum size is 4 and we resized at 3/4 full.
+ // 根据方法 key 和 receiver 查找方法缓存哈希表 bucket
bucket_t *bucket = cache->find(key, receiver);
if (bucket->key() == 0) cache->incrementOccupied();
+ // 将方法缓存 key,方法地址 IMP 保存到方法缓存哈希 bucket 中
bucket->set(key, imp);
}
```
@@ -1379,7 +2074,7 @@ if (imp) goto done;
// Try superclass caches and method lists.
{
unsigned attempts = unreasonableClassCount();
- // for 循环不断查找,找当前类的父类,直到当前类为 nil。
+ // for 循环不断查找,找当前类的父类,父类的父类...直到当前类为 nil(一轮查找后当前类 curClass = curClass->superclass)。
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
@@ -1390,11 +2085,12 @@ if (imp) goto done;
}
// Superclass cache.
- // 先在父类的方法缓存中查找(根据 sel & mask)`cache_getImp` ,找到则将方法写入到自身类的方法缓存中去 `log_and_fill_cache(cls, imp, sel, inst, curClass);`
+ // 先在父类的方法缓存中查找(根据 sel & mask,得到方法缓存哈希中的 key)`cache_getImp` ,找到则将方法写入到自身类的方法缓存中去 `log_and_fill_cache(cls, imp, sel, inst, curClass);`
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
+ // 找到则将 IMP 写入到当前类 cls 的方法缓存中去,比如调用 [student personSay] 方法,从父类的方法列表找到 personSay,则将 personSay 的 IMP 写入到 student 类的方法缓存中去
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
@@ -1419,19 +2115,19 @@ if (imp) goto done;
}
```
-for 循环不断查找,找当前类的父类,直到当前类为 nil。
-
-先在父类的方法缓存中查找(根据 sel & mask)`cache_getImp` ,找到则将方法写入到自身类的方法缓存中去 `log_and_fill_cache(cls, imp, sel, inst, curClass);`
-
-比如 Person 类有 eat 方法,Student 类有 stduy 方法,调用 Student 对象的 eat 方法,则会走到这里,从父类找到方法后写入到 Student 类的方法缓存中去。
-
-如果在父类的方法缓存中没找到,则调用 `getMethodNoSuper_nolock` 父类的 方法数组(Array 元素为方法数组),按照排序好和没排序好分别走二分查找和线性查找。
-
-如果找到则继续填充到当前类的方法缓存中去 `log_and_fill_cache(cls, meth->imp, sel, inst, curClass);`,最后 goto done
-
上面的流程是整个 `objc_msgSend` 的消息发送阶段的整个流程。可以用下图表示
-
+
+
+
+
+
+
+注意:
+
+方法缓存,也叫快速映射表(fast map),即使是快速执行路径(fast path),还是不如“静态绑定的函数调用操作”(statically bound function call)那样迅速,但大多数情况下这不是性能瓶颈。如果真的很在意,可以用纯 c 函数去实现。
+
+
### 动态方法解析阶段
@@ -1515,7 +2211,7 @@ static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
}
```
-核心就调用 `bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);` 运行 `resolveInstanceMethod` 方法。
+核心是 `bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);` ,也就是动态方法解析阶段,如果是类对象,则会调用类方法 `+ (BOOL)resolveInstanceMethod:(SEL)sel` 方法。
元类对象走 `_class_resolveClassMethod` 逻辑
@@ -1566,67 +2262,117 @@ SEL_resolveClassMethod, sel);`
完整流程如下
-
+
-上 Demo
+
+
+QA:
+
+方法动态解析后,为什么还要 `goto retry`?
+
+因为 API 设计阶段只是增加了口子,预留了能力,让开发者可以给类增加对象方法、类方法进行方法的动态解析。
+
+如果我们实现了 `+ (BOOL)resolveInstanceMethod:(SEL)sel` 方法或者实现了 `+ (BOOL)resolveClassMethod:(SEL)sel`,
+
+则根据函数返回值,拿到 YES 标记为已解析,则执行 `goto retry`。此时在一开始查找的时候就可以找到 IMP,此时 `goto done`
+
+里面返回函数地址到汇编代码中。
+
+如果没有实现动态解析方法,则执行 `goto retry` 的时候,一开始找不到 IMP,且在动态方法解析的 if 条件判断不成立 `if(resolver & !triedResolver)` 则不会继续动态方法解析,开始执行下面的逻辑,进入消息转发阶段 `_objc_msgForward_impcache`
+
+
+
+
+
+上述动态消息解析后,会缓存起来吗?
+
+第一次消息动态解析后,只是将方法增加到 class 或者 meta-class 的 class_rw_t 中。然后会继续执行 `goto retry`,在 `gto retry` 的流程中,会先在 class 的方法缓存中查找有没有缓存,如果没有缓存,则会在 class 的 class_rw_t 中查找方法,找到并执行,同时还会吧方法保存到方法缓存中去。以便后续再次调用方法的时候更加高效。
+
+所以这个“会”缓存起来,只不过缓存的时机不是同步的,而是再次调用 `goto retry` 的时候,发现没有缓存,则在 class_rw_t 找查找到后,再缓存起来
+
+
+
+#### 动态消息解析 Demo
+
+##### 实例方法
```objectivec
Person *person = [[Person alloc] init];
-[person eat];
+[person makeLiving];
```
-调用不存在方法则报错 `***** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person eat]: unrecognized selector sent to instance 0x101b2d900'**`
+调用不存在方法则报错 `***** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person makeLiving]: unrecognized makeLiving sent to instance 0x101b2d900'**`
因为调用对象不存在的方法,所以会 Crash
知道 `objc_msgSend` 的流程,我们尝试给它修正下
-```objectivec
-- (void)customEat {
- NSLog(@"我的假的 eat 方法,为了解决奔溃问题");
+
+
+方法1,增加一个兜底方法,然后利用 `class_addMethod` 动态增加方法实现
+
+```objective-c
+struct method_t {
+ SEL sel;
+ char *types;
+ IMP imp;
+};
+
+- (void)mockMakeLiving {
+ NSLog(@"方法动态解析拦截第一阶段");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
- if (sel == @selector(eat)) {
- // 对象方法,存在于对象上。
- Method method = class_getInstanceMethod(self, @selector(customEat));
- class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
+ if (sel == @selector(makeLiving)) {
+ struct method_t *method = (struct method_t *)class_getInstanceMethod(self, @selector(mockMakeLiving));
+ class_addMethod(self, sel, method->imp, method->types);
return YES;
}
return [super resolveInstanceMethod:sel];
}
```
-也可以添加 c 语音方法
+方法2,不用自定义的 struct method_t 结构体,直接用 Method 去承接,只不过在传递 IMP、encoding 的时候用 runtime api 获取即可
-```objectivec
-void customEat (id self, SEL _cmd) {
- NSLog(@"%@-%s-%s", self, sel_getName(_cmd), __func__);
+```objective-c
+Method method = class_getInstanceMethod(self, @selector(mockMakeLiving));
+class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
+```
+
+方法3,也可以添加 c 语言方法
+
+
+
+c 函数即函数地址,只不过传递给 class_addMethod 的时候需要转换为函数参数加 `(IMP)cfuntionResolver`,手动添加方法签名。
+
+```objective-c
+void cfuntionResolver(id self, SEL _cmd) {
+ NSLog(@"cfuntionResolver %@ %@", self, NSStringFromSelector(_cmd));
}
-
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(eat)) {
// 对象方法,存在于对象上。
- class_addMethod(self, sel, (IMP)customEat, "v16@0:8");
+ class_addMethod(self, sel, (IMP)cfuntionResolver, "v16@0:8");
return YES;
}
return [super resolveInstanceMethod:sel];
}
```
-因为 c 语言方法名就是函数地址,所以不需要直接传递即可,需要做下类型转换 `(IMP)customEat`
-也可以给类方法做动态方法解析。需要注意的是类方法。
+
+##### 类方法动态解析
+
+也可以给类方法做动态方法解析。需要注意的是类方法
- 调用 `-(BOOL)resolveClassMethod:(SEL)sel`
- `class_addMethod` 方法中的第一个参数,需要加到类的元类对象中,所以是 `object_getClass`
```objectivec
-Person *person = [[Person alloc] init];
[Person drink];
-void customDrink (id self, SEL _cmd) {
+void mockDrink (id self, SEL _cmd) {
NSLog(@"假喝水");
}
@@ -1634,13 +2380,15 @@ void customDrink (id self, SEL _cmd) {
{
if (sel == @selector(drink)) {
// 类方法,存在于元类对象上。
- class_addMethod(object_getClass(self), sel, (IMP)customDrink, "v16@0:8");
+ class_addMethod(object_getClass(self), sel, (IMP)mockDrink, "v16@0:8");
return YES;
}
return [super resolveClassMethod:sel];
}
```
+
+
### 消息转发阶段
能走到消息转发,说明
@@ -1649,7 +2397,7 @@ void customDrink (id self, SEL _cmd) {
2. `objc_msgSend` 动态方法解析失败或者没有做
-说明类自身和父类没有可以处理该消息的能力,此时应该将该消息转发给其他对象。
+说明:**类自身(自身方法缓存、自身没有方法实现、自身也没有动态增加方法)和父类没有可以处理该消息的能力,此时应该将该消息转发给其他对象**。
查看 objc4 的源码
@@ -1706,7 +2454,7 @@ void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
为什么是 `__forwarding__` 方法。我们可以根据 Xcode 崩溃窥探一二
-
+
```c
int __forwarding__(void *frameStackPointer, int isStret) {
@@ -1750,101 +2498,11 @@ int __forwarding__(void *frameStackPointer, int isStret) {
完整流程如下
-
+
-上 Demo
-Person 类不存在 drink 方法,Bird 类存在
-```objectivec
-@implementation Bird
-- (void)drink
-{
- NSLog(@"一只鸟儿在喝水");
-}
-@end
-Person *person = [[Person alloc] init];
-[person drink];
-```
-
-方法1
-
-```objectivec
-@implementation Person
-- (id)forwardingTargetForSelector:(SEL)aSelector
-{
- if (aSelector == @selector(drink)) {
- return [[Bird alloc] init];
- }
- return [super forwardingTargetForSelector:aSelector];
-}
-@end
-```
-
-方法2
-
-```objectivec
-- (id)forwardingTargetForSelector:(SEL)aSelector
-{
- if (aSelector == @selector(drink)) {
- return nil;
- }
- return [super forwardingTargetForSelector:aSelector];
-}
-
-- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
-{
- NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
- return signature;
-}
-
-- (void)forwardInvocation:(NSInvocation *)anInvocation
-{
- [anInvocation invokeWithTarget:[[Bird alloc] init]];
-}
-```
-
-注意:`methodSignatureForSelector` 如果返回 nil,则 `forwardInvocation` 不会执行
-
-给 Person 类方法进行消息转发处理
-
-方法1
-
-```objectivec
-+ (id)forwardingTargetForSelector:(SEL)aSelector
-{
- if (aSelector == @selector(drink)) {
- return [Bird class];
- }
- return [super forwardingTargetForSelector:aSelector];
-}
-```
-
-方法2
-
-```objectivec
-+ (id)forwardingTargetForSelector:(SEL)aSelector
-{
- if (aSelector == @selector(drink)) {
- return nil;
- }
- return [super forwardingTargetForSelector:aSelector];
-}
-
-+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
-{
- if (aSelector == @selector(drink)) {
- return [[Bird class] methodSignatureForSelector:@selector(drink)];
- }
- return [super methodSignatureForSelector:aSelector];
-}
-
-+ (void)forwardInvocation:(NSInvocation *)anInvocation
-{
- [anInvocation invokeWithTarget:[Bird class]];
-}
-```
#### 方法签名的获取
@@ -1852,24 +2510,148 @@ Person *person = [[Person alloc] init];
方法2 :根据某个类的对象,去调用 `methodSignatureForSelector ` 方法获取。
- `[[[Bird alloc] init] methodSignatureForSelector:**@selector**(drink)];`
+ `[[[Person alloc] init] methodSignatureForSelector:@selector(makeLiving)];`
+
+方法1自己拼的缺点是不够灵活,修改原始方法,需要在方法签名处修改方法的参数、返回值信息,还需手动计算。效率低。如果某个类实现了方法,则可以通过 `[class methodSignatureForSelector:@selector(***)]` 的方式获取方法签名。
```objectivec
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(drink)) {
- return [[[Bird alloc] init] methodSignatureForSelector:@selector(drink)];
+ return [[[PersonHelper alloc] init] methodSignatureForSelector:@selector(makeLiving:)];
}
return [super methodSignatureForSelector:aSelector];
}
```
-## Super 原理
+
+
+#### 消息转发 Demo
+
+##### 对象方法消息转发 Demo
+
+Person 类不存在对象方法 makeliving ,PersonHelper 类存在。
+
+
+
+调用对象不存在的方法,则会抛出错误。同时在 Runtime 动态消息解析阶段,`resolveInstanceMethod` 没有处理对象方法,所以会报错
+
+方法1:因为动态消息解析没有处理,则会开始走消息转发阶段。消息转发首先会调用 `- (id)forwardingTargetForSelector:(SEL)aSelector` 方法。(如果是对象方法则调用 `- (id)forwardingTargetForSelector:(SEL)aSelector`,如果是类方法则调用 `+ (id)forwardingTargetForSelector:(SEL)aSelector`)
+
+
+
+方法2:如果消息转发里,`forwardingTargetForSelector` 返回了 nil,则开始调用方法签名 `methodSignatureForSelector` 方法和 `forwardInvocation` 方法
+
+```objective-c
+- (id)forwardingTargetForSelector:(SEL)aSelector {
+ if (aSelector == @selector(makeLiving)) {
+// return [[PersonHelper alloc] init];
+ return nil;
+ }
+ return [super forwardingTargetForSelector:aSelector];
+}
+
+- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
+ return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
+}
+
+- (void)forwardInvocation:(NSInvocation *)anInvocation {
+ // anInvocation 是包含了函数调用的所有信息
+ // anInvocation.target; 调用者
+ // anInvocation.selector; 方法名
+ // anInvocation getArgument: atIndex:; 方法参数
+ // anInvocation.target = [[PersonHelper alloc] init];
+ // [anInvocation invoke];
+ [anInvocation invokeWithTarget:[[PersonHelper alloc] init] ];
+}
+```
+
+注意:`methodSignatureForSelector` 如果返回 nil,则 `forwardInvocation` 不会执行
+
+
+
+
+
+上述方式不够优雅,针对方法签名的获取太被动,方法改变了,方法签名处需要调整,很麻烦。
+
+
+
+
+
+
+
+##### 类方法消息转发 Demo
+
+上述是实例方法找不到的情况,我们也可以给类方法,增加消息转发的处理。
+
+- 基本上的处理上对象方法转发所用到的方法,前面的 `-` 变为 `+` 即可。
+- 某些关于方法签名或者转发的对象换为类对象即可
+
+```objective-c
++ (id)forwardingTargetForSelector:(SEL)aSelector {
+ return [PersonHelper class];
+}
+
++ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
+ return [[PersonHelper class] methodSignatureForSelector:aSelector];
+}
+
++ (void)forwardInvocation:(NSInvocation *)anInvocation {
+ [anInvocation invokeWithTarget:[PersonHelper class]];
+}
+```
+
+
+
+
+
+
+
+
+
+
+
+### OC 消息机制是什么样的?
+
+OC 中方法调用的本质就是利用 runtime 发消息,发消息也给 receiver(方法调用者)发消息(selector 方法名),就是 `objc_msgSend` 的工作流程,具体分为3个阶段:
+
+- 消息发送:主要是从当前类、当前类的父类...中查找
+- 动态方法解析
+- 消息转发
+
+
+
+### 消息传递流程
+
+1. 先判断是否命中缓存,利用 sel 和 _mask,计算出哈希值,然后在 bucket_list 中查找方法缓存,找到则调用。没有找对则继续查找
+
+2. 没有找到缓存,则根据对象的 isa 在类对象的方法列表中,进行方法查找
+
+ - 对于已经排序好的列表,采用二分查找算法,查找方法对应执行函数
+ - 对于没有排序好的列表,采用一般遍历查找方法对应执行函数
+
+3. 如果在当前类的方法列表中没有找对方法实现,则根据类对象的 superclass 在父类的类对象的方法列表中继续查找
+
+
+
+ 先根据 superclass 找到父类,然后在父类中也按照上面3点进行递归查找,即:
+
+ - 先判断能否命中方法缓存,根据 sel 和 _mask 计算哈希,然后判断是否命中
+ - 没有命中,则在父类的类对象方法列表中查找。对于已经排序好的列表,采用二分查找算法,查找方法对应执行函数;对于没有排序好的列表,采用一般遍历查找方法对应执行函数
+ - 还是没有命中,则对父类的父类,继续上述流程
+
+
+
+## Super 底层原理
```objectivec
+@interface Person: NSObject
+
+@end
@implementation Person
@end
+
@implementation Student
- (instancetype)init
{
@@ -1936,18 +2718,22 @@ struct objc_super arg = {self, class_getSuperclass(self)};
objc_msgSendSuper(arg, sel_registerName("class"))
```
-`[super class]` super 调用的 receiver 还是 self
+也就是说 `[super class]` 中, super 调用的 receiver 还是 self。比如 `[super class]` 等价于 `objc_msgSendSuper({self, class_getSuperClass(objc_getClass("Student"))}, @selector(class))`
+
+调用 `class` 方法,其实是在基类对象 NSObject 中的。因为是类方法,所以先从 Student 的父类 Person 类的元类对象中查找有没有 `class` 方法实现。发现没有,则继续根据 Person 类 superclass 指针,找到 Person 的父类的元类对象的类方法列表中中查找,没找到,则继续向上找,最后找到 NSObject 对象的元类对象的类方法列表中找到了,因为方法调用者是 self,所以获取 class 得到的就是当前类,即 Student。
+
+结构体的目的是为了在类对象查找的过程中,**直接从当前类的父类中查找,而不是本类**(比如 Student 类的 [super init] 会直接从 Person 的类对象中查找 init,找不到则通过 `superclass` 向上查找)
+
-结构体的目的是为了在类对象查找的过程中,直接从当前类的父类中查找,而不是本类(比如 Student 类的 [super init] 会直接从 Person 的类对象中查找 init,找不到则通过 superclass 向上查找)
大致推测系统的 class、superclass 方法实现如下
-```c
-@implementation Person
+```objective-c
+@implementation NSObject
- (Class)class{
return object_getClass(self);
}
-- (Class)superclass {
+- (Class)superclass { // 先获取类对象,然后获取类对象的 superclass
return class_getSuperclass(object_getClass(self));
}
@end
@@ -1971,7 +2757,7 @@ objc_msgSendSuper(arg, sel_registerName("class"))
我们对 iOS 项目`[super viewDidLoad]` 下符号断点,发现`objc_msgSendSuper2`
-
+
查看 objc4 源代码发现是一段汇编实现。
@@ -2055,21 +2841,41 @@ label - 代码标签
call - 调用函数
```
+
+
+也可以在 Xcode 上可视化面板操作,路径为:菜单栏 `Product -> Perform Action -> Assemble "ViewController.m"`
+
+
+
+
+
+
+
+## 消息发送的边界情况
+
+- objc_msgSend_stret: 如果待发送的消息要返回结构体,那么可交给此函数完成。只有当 CPU 的寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息。如果是返回值无法容纳于 CPU 寄存器中(比如结构体太大了),那么就由另一个函数执行派发,此时函数会通过栈上的某个变量来处理消息所返回的结构体
+- objc_msgSend_fpret:如果消息返回的是浮点数,那么可交给此函数处理。在某些架构的 CPU 中调用函数时,需要对浮点数寄存器做特殊处理,通常采用的 objc_msgSend 在这种情况下不合适,这个函数是为了处理 x86 等架构 CPU 中某些特殊的情况的
+- objc_msgSendSuper: 要给超类发消息,就交给此函数处理。
+
+
+
+## 编译器优化之尾调用优化
+
+如果函数的最后一行是调用另一个函数,那么就可以采用“尾调用优化”技术。编译器会生成跳转至另一函数所需的指令码,而不会向调用堆栈中推入新的栈帧。
+
+只有当函数的最后一个操作仅仅是调用其他函数而不会将其返回值另作他用时,才能执行尾调用优化。这项优化对 objc_msgSend 很有用。如果不这么做,那么每次调用 OC 方法之前,都需要为调用 objc_msgSend 函数准备栈帧。而且很容易出现 stack overflow 的问题。
+
+
+
## isKindOfClass、isMemberOfClass
-Demo
+Demo1
-```c
-Student *student = [[Student alloc] init];
-NSLog(@"%hhd", [student isMemberOfClass:[Student class]]); // 1
-NSLog(@"%hhd", [student isKindOfClass:[Person class]]); // 1
-NSLog(@"%hhd", [Student isMemberOfClass:[Student class]]); // 0
-NSLog(@"%hhd", [Student isKindOfClass:[Student class]]); // 0
-```
+
-有些人答对了,有些人错了。
-上面2个判断都是调用对象方法的 `isMemberOfClass` 、`isKindOfClass`
+
+上面的打印有没有有疑惑的?2个判断都是调用对象方法的 `isMemberOfClass` 、`isKindOfClass`
由于 objc4 是开源的,查看 `object.mm`
@@ -2087,7 +2893,13 @@ NSLog(@"%hhd", [Student isKindOfClass:[Student class]]); // 0
`isMemberOfClass` 判断当前对象是不是传递进来的对象
-`isKindOfClass` 内部是一个 for 循环,第一次循环先拿当前类的类对象,判断是不是和传递进来的对象一样,一样则 return YES,否则先给 tlcs 赋值当前类的父类,然后走第二次判断,直到 cls 不存在位置(NSObject 的父类为 nil)。所以 `isKindOfClass` 其实判断的是当前类是传递进来的类,或者传递进来类的子类
+`isKindOfClass` 内部是一个 for 循环,第一次循环先拿当前类的类对象,判断是不是和传递进来的对象一样,一样则 return YES,否则先给 tlcs 赋值当前类的父类,然后走第二次判断,直到 cls 不存在(NSObject 的父类为 nil)。所以 `isKindOfClass` 其实判断的是当前类是传递进来的类,或者传递进来类的子类
+
+
+
+Demo2
+
+
下面面2个判断都是调用类方法的 `isMemberOfClass` 、`isKindOfClass`
@@ -2109,19 +2921,25 @@ NSLog(@"%hhd", [Student isKindOfClass:[Student class]]); // 0
`+(BOOL)isKindOfClass:(Class)cls` 同理分析。作用是当前类的元类,是否是右边传入对象的元类或者元类的子类。
-来个特殊 case
-```c
-NSLog(@"%hhd", [[Student class] isKindOfClass:[NSObject class]]); // NO
-```
-输出 1。为什么?
+QA:
-看坐右边的部分,调用 `isKindOfClass` 方法,本质上就是 Student 类的类对象,也就是 Student 元类,和传入的右边 `[NSObject class]`判断是否想通过
+1. `NSLog(@"%d", [Student isKindOfClass:[Student class]]); ` 为什么会输出0?
+
+ 因为 `isKindOfClass` 底层是 for 循环对传入的 self 使用 `object_getClass((id)self)` 获取类对象,因为传入的已经是类对象,所以 `object_getClass((id)self)` 内部得到的是元类对象。右边是传入的类对象。等号左边的 Student 的元类对象,不等于等号右边的类对象。所以为 false,输出 0.
+
+ 如何更改使得输出1?`[Student isKindOfClass:object_getClass([Student class])]`。右边也是元类对象,则为 True 输出1.
+
+2. `NSLog(@"%hhd", [[Student class] isKindOfClass:[NSObject class]]); ` 为什么输出1?
+
+ 看右边的部分,调用 `isKindOfClass` 方法,本质上就是 Student 类的类对象,也就是 Student 元类,和传入的右边 `[NSObject class]` 判断是否想通过
+
+ 第一次 for 循环当然不同,所以不能 return,会将 `tcls ` 走步长改变逻辑 `tcls = tcls->superclass`,也就是找到当前 Student 元类对象的父类。
+
+ 第二次 for 循环也一样不相等,Person 元类不等于 `[NSObject class]` 继续向上,直到 tcls = NSObject。此时还是不等,这时候 tcls 走步长改变逻辑,`tcls = tcls->superclass` NSObject 元类的 superclass 还是 NSObject。所以 for 循环内部的判断编委 ` [NSObject class] == [NSObject class]`,return YES。
-第一次 for 循环当然不同,所以不能 return,会将 `tcls ` 走步长改变逻辑 `tcls = tcls->superclass`,也就是找到当前 Student 元类对象的父类。
-第二次 for 循环也一样不相等,Person 元类不等于 `[NSObject class]` 继续向上,直到 tcls = NSObject。此时还是不等,这时候 tcls 走步长改变逻辑,`tcls = tcls->superclass` NSObject 元类的 superclass 还是 NSObject。所以 for 循环内部的判断编委 ` [NSObject class] == [NSObject class]`,return YES。
**tips:基类的元类对象指向基类的类对象。**
@@ -2136,46 +2954,52 @@ NSLog(@"%hhd", [[Student class] isKindOfClass:[NSObject class]]); // NO

-Quiz
-```objectivec
-NSLog(@"%hhd", [NSObject isKindOfClass:[NSObject class]]); // 1
-NSLog(@"%hhd", [NSObject isMemberOfClass:[NSObject class]]); //0
-NSLog(@"%hhd", [Person isKindOfClass:[Person class]]); // 0
-NSLog(@"%hhd", [Person isMemberOfClass:[Person class]]); //0
-```
+
+综合练习:
+
+
+
+
+
+
## Runtime 刁钻题
-```objectivec
+能否向编译后的类,添加实例变量?class_ro_t 不可以添加。但可以向动态创建的类添加。
+
+
+
+> 这道题目设计:super 调用的本质、函数栈空间向下增长、runtime 消息调用本质(isa)、访问对象的成员变量(找到 isa,约过前面的8字节,按照成员变量的大小,去找成员)
+>
+> 因为实例对象里存的就是:isa + 各个成员变量的值
+
+
+
+```objective-c
@interface Person : NSObject
-@property (nonatomic, strong) NSString *name;
+@property (nonatomic, copy) NSString *name;
- (void)sayHi;
@end
+
@implementation Person
-- (void)sayHi{
- NSLog(@"hi,my name is %@", self->_name); // hi,my name is 杭城小刘
+- (void)sayHi {
+ NSLog(@"hi,my name is %@", self->_name);
}
@end
-int main(int argc, const char * argv[]) {
- @autoreleasepool {
- NSString *temp = @"杭城小刘";
- id obj = [Person class];
- void *p = &obj;
- [(__bridge id)p sayHi];
-
- test();
- }
- return 0;
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ NSString *temp = @"杭城小刘";
+ id obj = [Person class];
+ void *p = &obj;
+ [(__bridge id)p sayHi];
}
```
程序运行什么结果?
-```
-hi,my name is 杭城小刘
-```
+
为什么会方法调用成功?为什么 name 打印出为 @"杭城小刘"
@@ -2183,7 +3007,7 @@ hi,my name is 杭城小刘
1.**方法调用本质就是寻找 isa 进行消息发送**
-```objectivec
+```objective-c
Person *person = [[Person alloc] init];
[person sayHi];
```
@@ -2205,7 +3029,7 @@ void test () {
方法内的变量存储在栈上,堆向上增长,栈向下增长。
-
+
3.**实例对象的本质就是一个结构体,存储所有成员变量(isa 是一个特殊成员变量,其他的成员变量,这里就是 _name),`sayHi` 方法内部的 self 就是 obj,找成员变量的本质就是找内存地址的过程(此时就是偏移8个字节)**
@@ -2218,28 +3042,19 @@ struct Person_IMPL {
}
```
+
+
再看一个变体1
-```c
-NSObject *temp = [[NSObject alloc] init];
-id obj = [Person class];
-void *p = &obj;
-[(__bridge id)p sayHi];
-// hi,my name is
-```
+
+
+
+
+再看一个变体2
+
+
-再看一个变体2(将代码放在 ViewController中)
-```objectivec
-- (void)viewDidLoad {
- [super viewDidLoad];
- id obj = [Person class];
- void *p = &class;
- NSObject *temp = [[NSObject alloc] init];
- [(__bridge id)p sayHi];
-}
-// hi,my name is
-```
搞懂的小伙伴不迷惑了。没搞懂其实就是没搞懂**栈地址由高到低,向下生长** 和 `super` 调用的本质。
@@ -2248,38 +3063,56 @@ void *p = &obj;
`[super viewDidLoad];` 本质就是 `objc_msgSendSuper({self, class_getSuperclass(self)}, sel_registerName("viewDidLoad"))`
```c
-struct objc_super arg = {self, class_getSuperclass(self)};
+struct objc_super arg = {self, class_getSuperclass(objc_getClass("ViewController"))};
objc_msgSendSuper(arg, sel_registerName("viewDidLoad"));
```
所以此时的“前一个局部变量” 也就是结构体 `objc_super` 类型的 arg。arg 是一个结构体,结构体第一个成员变量就是 self,所以“前一个局部变量” 也就是 self(ViewController)
-
+
+
+可能会疑问,知道了 super 调用本质,知道了会产生一个局部变量的结构体,但是结构体里面2个成员变量,找属性的时候,isa 下的8个字节会命中哪个?
+
+结构体 `objc_super` 存在2个成员变量,self 是第一个,`class_getSuperclass(objc_getClass("ViewController"))` 是第二个,self 地址更低。
+
+画图如下,有助于理解
+
+
+
+
+
+
+
+
## 应用场景
-1.统计 App 中未响应的方法。给 NSObject 添加分类
+### 统计 App 中未响应的方法。给 NSObject 添加分类 `NSObject+ExceptionHunter.m`,利用 NSProxy 实现
```objectivec
-- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
-{
- // 本来能调用的方法
+#import "NSObject+ExceptionHunter.h"
+#import
+
+@implementation NSObject (ExceptionHunter)
+
+- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([self respondsToSelector:aSelector]) {
- return [super methodSignatureForSelector:aSelector];
+ return [self methodSignatureForSelector:aSelector];
}
- // 找不到的方法
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
-// 找不到的方法,都会来到这里
-- (void)forwardInvocation:(NSInvocation *)anInvocation
-{
- NSLog(@"找不到%@方法", NSStringFromSelector(anInvocation.selector));
+- (void)forwardInvocation:(NSInvocation *)anInvocation {
+ // 收集上报
+ NSLog(@"%@方法未调用成功", NSStringFromSelector(anInvocation.selector));
}
+
@end
```
-2.修改类的 isa
+### 修改类的 isa
+
+类似 KVO 的实现,就是更改 isa。
`object_setClass` 实现
@@ -2288,9 +3121,11 @@ Person *p = [Person new];
object_setClass(p, [Student class]);
```
-
+
-3.动态创建类
+
+
+### 动态创建类
`objc_allocateClassPair`、`objc_registerClassPair` 成对存在
@@ -2315,11 +3150,13 @@ void createClass (void) {
}
```
-
+
runtime 中 copy、create 等出来的内存,不使用的时候需要手动释放`objc_disposeClassPair(newClass>)`
-4.访问成员变量信息
+
+
+### 访问成员变量信息
```objectivec
void ivarInfo (void) {
@@ -2339,7 +3176,9 @@ KVC 可以根据具体的值,去取出 NSNumber ,然后调用 intValue
`[p setValue:@27 forKey:@"_age"];`
-5.访问对象的所有成员变量信息
+
+
+### 访问对象的所有成员变量信息
```objectivec
@property (nonatomic, strong) NSString *name;
@@ -2368,24 +3207,41 @@ free(properties);
或者设置字典转模型(不够健壮,随便写的。具体可以参考 YYModel)
-```objectivec
-+ (instancetype)lbp_modelWithDic:(NSDictionary *)dict
-{
+```objective-c
+@implementation NSObject (JSON)
+
++ (instancetype)modelWithJSON:(NSDictionary *)dict {
id obj = [[self alloc] init];
unsigned int count;
- Ivar *properties = class_copyIvarList([self class], &count);
- for (int i =0 ; i
+
+不够健壮体现在:
+
+- 没有考虑对象继承的情况,比如 Student 继承自 Person,那么 `[Student modelWithJSON:dict]` 转换就存在问题
+- 假设服务器返回的 json 有9个字段,本地对象有8个字段,怎么处理?
+- 服务器给的有名字为 id 的数据,OC 对象的属性名不能叫 id,如何处理?
+
+具体可以参考 YYModel
+
+
+
+### 替换方法实现
注意
@@ -2426,7 +3282,11 @@ method_exchangeImplementations(sleep, sayHi);
[p sleep]; // Person sayHi
```
-7.无痕埋点
+runtime 方法交换的本质,就是交换类对象、元类对象的 class_rw_t 的 `method_array_t>` 里的 method_t 的 IMP。
+
+
+
+### 无痕埋点
对 App 内所有的按钮点击事件进行监听并上报。发现 UIButton 继承自 UIControl,所以添加分类,在 load 方法内,替换方法实现。UIControl 存在方法 `sendAction:to:forEvent:`
@@ -2484,13 +3344,21 @@ static void flushCaches(Class cls)
}
```
-总结:
+
+
+### 安全气垫
+
+安全气垫就是对代码运行过程中出错的方法进行兜住,比如数组越界等。ROI 和带来的一些业务异常问题就见仁见智了。实现手段就是 runtime hook 然后更改方法实现。做一些安全判断。和网易大白的作者交流过,发现安全气垫的副作用,比如一个商品价格运算后异常,会 crash 这时候起码 crash 不能交易下单了。但是用了安全气垫,好处是不会 crash,缺点是给一个有问题的值,要么价格为0,要么为空。用户可以正常下单,这时候产生资损了。电商业务虾带来的业务异常问题比稳定性异常问题更严重。电商公司一般会给商家有资损保障,赔付率。
+
+
+
+## 总结
OC 是一门动态性很强的编程语言,允许很多操作推迟到程序运行时决定。OC 动态性其实就是由 Runtime 来实现的,Runtime 是一套 c 语言 api,封装了很多动态性相关函数。平时写的 oc 代码,底层大多都是转换为 Runtime api 进行调用的。
- 关联对象
-- 遍历类的所有成员变量(可以访问私有变量,比如修改 UITextFiled 的 placeholder 颜色、字典转模型、自动归档接档)
+- 遍历类的所有成员变量(可以访问私有变量,比如修改 UITextFiled 的 placeholder 颜色、字典转模型、自动归档解档)
- 交换方法实现
diff --git a/Chapter1 - iOS/1.89.md b/Chapter1 - iOS/1.89.md
index 3daa253..2ac855a 100644
--- a/Chapter1 - iOS/1.89.md
+++ b/Chapter1 - iOS/1.89.md
@@ -1,36 +1,80 @@
# block 底层原理
-## block 本质
+> 大家写 OC 肯定用过不少 block。有这样4个问题:
+>
+> - block 原理是什么,系统是如何实现的?
+> - __block 的作用是什么?
+> - block 作为属性时,为什么用 copu 修饰?
+> - block 在修改 NSMutableArray 的时候,需要加 __block 吗?
+>
+> 带着问题探究本文。
-block 本质上就是一个 oc 对象,也有 isa 指针
-block 是封装了函数调用和函数调用环境的 OC 对象
-
+## block 本质探索
-```objectivec
-int age = 27;
-void(^block)(void) = ^(){
- NSLog(@"age:%d", age);
-};
-block();
+### 实验探索
+
+Demo
+
+```objective-c
+NSInteger age = 27;
+ void(^block)(NSInteger, NSInteger) = ^(NSInteger a, NSInteger b) {
+ NSLog(@"age is %zd", age);
+ NSLog(@"a is %zd, b is %zd", a, b);
+ };
+ block(1, 2);
```
-`xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp`
+用指令`xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转为 c++
-转换为 c++ 如下
+
+
+`ViewController.m` 的 `viewDidLoad` 函数为 `_I_ViewController_viewDidLoad`
```c
-int age = 27;
-void(*block)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
-((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
+static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
+ ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
+ NSInteger age = 27;
+ void(*block)(NSInteger, NSInteger) = ((void (*)(NSInteger, NSInteger))&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, age));
+ ((void (*)(__block_impl *, NSInteger, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);
+}
+```
+block 被定义为一个叫做 `__ViewController__viewDidLoad_block_impl_0` 的结构体
-struct __main_block_impl_0 {
+```c++
+struct __ViewController__viewDidLoad_block_impl_0 {
struct __block_impl impl;
- struct __main_block_desc_0* Desc;
- int age;
- __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
+ struct __ViewController__viewDidLoad_block_desc_0* Desc;
+ NSInteger age;
+ __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, NSInteger _age, int flags=0) : age(_age) {
+ impl.isa = &_NSConcreteStackBlock;
+ impl.Flags = flags;
+ impl.FuncPtr = fp;
+ Desc = desc;
+ }
+};
+
+struct __block_impl {
+ void *isa;
+ int Flags;
+ int Reserved;
+ void *FuncPtr;
+};
+```
+
+因为 `__block_impl` 结构体位于 `__ViewController__viewDidLoad_block_impl_0` 结构体的第一个成员,所以上述代码等价于
+
+```c++
+struct __ViewController__viewDidLoad_block_impl_0 {
+ void *isa;
+ int Flags;
+ int Reserved;
+ void *FuncPtr;
+ struct __ViewController__viewDidLoad_block_desc_0* Desc;
+ NSInteger age;
+ __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, NSInteger _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
@@ -39,39 +83,58 @@ struct __main_block_impl_0 {
};
```
-可以看到 `__main_block_impl_0` 是一个结构体,有3个变量,其中 `__main_block_impl_0` 是一个构造方法,接收4个参数,第四个参数 flags 是非必须的。
+block 内部的 NSLog 语句,被封装为 `__ViewController__viewDidLoad_block_func_0` 结构体
-`__main_block_func_0` 参数是一个方法实现。
+```c++
+static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself, NSInteger a, NSInteger b) {
+ NSInteger age = __cself->age; // bound by copy
-```c
-static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
- int age = __cself->age; // bound by copy
+ NSLog((NSString *)&__NSConstantStringImpl__var_folders_7x_g921y52j5yb_w5hsn3fb3m8r0000gn_T_ViewController_95a9e6_mi_0, age);
+ NSLog((NSString *)&__NSConstantStringImpl__var_folders_7x_g921y52j5yb_w5hsn3fb3m8r0000gn_T_ViewController_95a9e6_mi_1, a, b);
+ }
- NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_eb3c55_mi_0, age);
- }
+__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, NSInteger _age, int flags=0) : age(_age) {
+ impl.isa = &_NSConcreteStackBlock;
+ impl.Flags = flags;
+ impl.FuncPtr = fp;
+ Desc = desc;
+ }
```
-```c
-static struct __main_block_desc_0 {
+可以看到构造函数 `__ViewController__viewDidLoad_block_impl_0` 有4个参数。
+
+第一个参数:
+
+在 `viewDidLoad` 中,`__ViewController__viewDidLoad_block_func_0` 当作构造函数 `__ViewController__viewDidLoad_block_impl_0` 的参数,传递给了参数 fp,构造函数内部将 fp 赋值给了 impl 的 FuncPtr。在
+
+```c++
+((void (*)(__block_impl *, NSInteger, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);
+```
+
+最后在 viewDidLoad 函数中通过结构体 impl 的成员 FuncPtr,调用了函数。
+
+第二个参数:
+
+`__ViewController__viewDidLoad_block_desc_0_DATA` 可以看成是一个 block 信息的描述,占用了 `sizeof(struct __ViewController__viewDidLoad_block_impl_0)` 大小的空间。
+
+```c++
+static struct __ViewController__viewDidLoad_block_desc_0 {
size_t reserved;
size_t Block_size;
-} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
+} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)
```
-`__main_block_desc_0` 是一个 block 信息的描述,占用了 `sizeof(struct __main_block_impl_0)` 大小的空间。
-`void(*block)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));` 第一行代码也就是将构造一个 struct 给 block 变量。
-`((**void** (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);` 第二行代码其实就是 `block->FuncPtr(block)` 根据 block 内部 FuncPtr 方法并调用。
+QA:
-```c
-static struct __main_block_desc_0 {
- size_t reserved;
- size_t Block_size;
-} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
+```c++
+((void (*)(__block_impl *, NSInteger, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 1, 2);
```
-为什么 block->FuncPtr 可以直接访问,而不是 block 先访问 impl,再访问 FuncPtr?因为 __block_impl 就是 __main_block_impl_0 这个结构体的第一个变量地址(结构体特性)
+为什么 `block->FuncPtr` 可以直接访问,而不是 block 先访问 impl,再访问 FuncPtr?因为 __block_impl 就是 __main_block_impl_0 这个结构体的第一个变量地址(结构体特性)
+
+上述的代码,等价于 `block->impl.FuncPtr`
```c
struct __block_impl {
@@ -84,7 +147,7 @@ struct __block_impl {
类似于下面代码
-```
+```c++
struct __main_block_impl_0 {
void *isa;
int Flags;
@@ -101,33 +164,214 @@ struct __main_block_impl_0 {
};
```
+通过 clang 转为 c++ 分析后,知道了 block 的本质,然后自定义结构体,mock 对象去承载 block 信息,然后查看
+
+```objective-c
+struct __block_impl {
+ void *isa;
+ int Flags;
+ int Reserved;
+ void *FuncPtr;
+};
+
+struct __ViewController__viewDidLoad_block_impl_0 {
+ struct __block_impl impl;
+ struct __ViewController__viewDidLoad_block_desc_0* Desc;
+ NSInteger age;
+};
+
+struct __ViewController__viewDidLoad_block_desc_0 {
+ size_t reserved;
+ size_t Block_size;
+};
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+
+ NSInteger age = 27;
+ void(^block)(NSInteger, NSInteger) = ^(NSInteger a, NSInteger b) {
+ NSLog(@"age is %zd", age);
+ NSLog(@"a is %zd, b is %zd", a, b);
+ };
+ block(1, 2);
+
+ struct __ViewController__viewDidLoad_block_impl_0 *mockBlock = (__bridge struct __ViewController__viewDidLoad_block_impl_0 *)block;
+ NSLog(@"");
+}
+```
+
+
+
+
+
+### 结论
+
+通过探索发现:
+
+- block 本质上就是一个 oc 对象,也有 isa 指针
+
+- block 是封装了函数调用和函数调用环境的 OC 对象
+
+
+
+
+
+
+
+
+
## block 变量捕获
-```objectivec
-int age = 27;
-void(^block)(void) = ^(){
- NSLog(@"age:%d", age);
+### auto 变量捕获
+
+Demo1:
+
+一个最简单的 block,参数和返回值都是 void,内部仅一条打印语句。
+
+```objective-c
+void(^printBlock)(void) = ^ {
+ NSLog(@"Hello block");
};
-age = 30;
-block();
+printBlock();
```
-输出27。因为 Block 会对变量进行捕获。
+用 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++
-```c
-int age = 27;
-void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
-age = 30;
- ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
+
+
+概括如下:
+
+
+
+
+
+Demo2: 捕获外部变量
+
+```objective-c
+void(^printAgeBlock)(void) = ^ {
+ NSLog(@"age is %zd", age);
+};
+age = 28;
+printAgeBlock();
```
+用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码
+
+
+
+代码分析:
+
+- 可以看到我们编写的 block 被声明为一个 `__ViewController__viewDidLoad_block_impl_0` 类型的结构体
+- 结构体内有个构造函数,见50773行代码。
+- c++ 中,构造方法中 `age(_age) `的写法,表明传入的 `_age` 会被赋值给结构体内的 age
+- 50794行代码,调用结构体的构造方法,传入参数。结构体构造方法内部将 参数 age 的值保存到结构体内部的 age 中。
+- 因为是值传递。所以即使在 50795 行代码对 age 进行了修改,结构体内部的 age 值不变
+- 所以执行 block,输出 age 依旧为27
+
+
+
+block 内部多了一个变量来存储外部变量,这个现象叫做 block 捕获了外部变量。
+
+c++ 中,在函数内部定义的变量,默认用 **auto** 修饰,叫做自动变量,离开作用域后自动销毁。上述 age 等价于 `auto NSInterge age = 27;`
+
+所以上述的情况,叫做 block 的 auto 变量捕获。
+
+
+
+### static 变量捕获
+
+```objective-c
+auto NSInteger age = 27;
+static NSInteger height = 175;
+void(^printInfoBlock)(void) = ^ {
+ NSLog(@"age is %zd, height is %zd", age, height);
+};
+age = 28;
+height = 176;
+printInfoBlock();
+```
+
+用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码
+
+
+
+
+
+对代码进行分析:
+
+- 可以看到我们编写的 block 被声明为一个 `__ViewController__viewDidLoad_block_impl_0` 类型的结构体
+- 结构体内有个构造函数,见50774行代码。
+- c++ 中,构造方法中 `age(_age) `的写法,表明传入的 `_age` 会被赋值给结构体内的 age,age 为值传递;`height(_height)` 写法,表明传入的 `_height` 会被复制给结构体内的 height,height 为引用传递
+- 50797行代码,调用结构体的构造方法,age 以值传递的方式传入参数,结构体构造方法内部将 参数 age 的值保存到结构体内部的 age 中。height 以引用传递的方式传入参数,结构体构造方法内,将参数 height 的引用保存起来
+- 因为 age 是值传递。所以即使在 50798 行代码对 age 进行了修改,结构体内部的 age 值不变
+- 因为 height 是引用传递。所以在 50799 行代码对 height 进行了修改,结构体内部的 height 值跟着改变
+- 所以执行 block,输出 age 依旧为27,输出 height 的时候,根据保存地址,找到 height,也就是最新的 height 会被输出
+
+
+
+### 全局变量捕获
+
+```objective-c
+NSInteger age = 27;
+static NSInteger height = 175;
+
+@implementation ViewController
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+
+ void(^printInfoBlock)(void) = ^ {
+ NSLog(@"age is %zd, height is %zd", age, height);
+ };
+ age = 28;
+ height = 176;
+ printInfoBlock();
+}
+@end
+// console
+age is 28, height is 176
+```
+
+用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码
+
+
+
+
+
+代码分析:
+
+- 可以看到我们编写的 block 被声明为一个 `__ViewController__viewDidLoad_block_impl_0` 类型的结构体
+- 结构体内有个构造函数,见50774行代码。可见针对全局变量,结构体内部不会捕获全局变量
+- block 内部的指令,被封装为一个叫做 `__ViewController__viewDidLoad_block_func_0` 的结构体,打印的时候直接访问全局变量
+- 针对全局变量的修改会实时生效
+- 所以执行 block,输出 age 和 height 的时候,直接输出全局变量的值
+
+
+
+QA:为什么局部变量存在捕获,全局变量不需要捕获?
+
+全局变量到哪都可以访问,所以没必要捕获。局部变量因为作用域的问题,所以需要捕获到哪步,以便后续使用。
+
+
+
+### 变量捕获总结
+
+block 截获变量可以分为:
+
+- 局部变量
+ - 基本数据类型:对于基本数据类型的局部变量,截获其值
+ - 对象类型:对于对象类型的局部变量,连同所有权修饰符一起截获
+- 静态局部变量:以指针形式进行截获的
+- 全局变量:不截获
+- 静态全局变量:不截获
+
变量分为:static、auto、register。
-static:表示作为静态变量存储在数据区。
+- static:表示作为静态变量存储在数据区。
-auto:一般的变量不加修饰词则默认为 auto,auto 表示作为自动变量存储在栈上。意味着离开作用域变量会自动销毁。
+- auto:一般的变量不加修饰词则默认为 auto,auto 表示作为自动变量存储在栈上。意味着离开作用域变量会自动销毁。
-register:这个关键字告诉编译器尽可能的将变量存在CPU内部寄存器中,而不是通过内存寻址访问,以提高效率。是尽可能,不是绝对。如果定义了很多 register 变量,可能会超过CPU 的寄存器个数,超过容量。所以只是可能。
+- register:这个关键字告诉编译器尽可能的将变量存在CPU内部寄存器中,而不是通过内存寻址访问,以提高效率。是尽可能,不是绝对。如果定义了很多 register 变量,可能会超过CPU 的寄存器个数,超过容量。所以只是可能。
| 作用域 | 捕获到 block 内部 | 访问方式 |
| ---------- | ------------ | ---- |
@@ -135,159 +379,133 @@ register:这个关键字告诉编译器尽可能的将变量存在CPU内部寄
| 局部变量static | YES | 指针传递 |
| 全局变量 | NO | 直接访问 |
-Demo2
-
-```objectivec
-auto int age = 27;
-static int height = 176;
-void(^block)(void) = ^(){
- NSLog(@"age:%d, height: %d", age, height);
-};
-age = 30;
-height = 177;
-block();
-// age:27, height: 177
-```
-
-clang 转为 c++
-
-```c
-auto int age = 27;
-static int height = 176;
-void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
-age = 30;
-height = 177;
-((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
-struct __main_block_impl_0 {
- struct __block_impl impl;
- struct __main_block_desc_0* Desc;
- int age;
- int *height;
- __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
- impl.isa = &_NSConcreteStackBlock;
- impl.Flags = flags;
- impl.FuncPtr = fp;
- Desc = desc;
- }
-};
-```
+来一个开发中常见的 case:
-可以看到 static 修饰的 height 在 c++ 代码底层用指针被 block 捕获,所以值修改后,是最终的 177,static 修饰的 age,被 block 捕获是值传递方式,所以还是27
+下面的例子中 的 block,self 会被捕获吗?
-Demo3
+```objective-c
+#import "Person.h"
-```objectivec
-int age = 27;
-static int height = 176;
-int main(int argc, const char * argv[]) {
- @autoreleasepool {
- void(^block)(void) = ^(){
- NSLog(@"age:%d, height: %d", age, height);
- };
- age = 26;
- height = 177;
- block();
+@implementation Person
+
+- (instancetype)initWithName:(NSString *)name {
+ if (self = [super init]) {
+ _name = name;
}
- return 0;
+ return self;
}
+
+- (void)play {
+ void(^playBlock)(void) = ^{
+ NSLog(@"%@ is playing", self);
+ };
+ playBlock();
+}
+
+@end
```
-转为 c++
+用指令 `xcrun --sdk iphoneos clang -arch arm64 Person.m -rewrite-objc -o Person-arm64.cpp` 转换为 c++ 代码
-```c
-void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
-age = 26;
-height = 177;
-((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
+
-struct __main_block_impl_0 {
- struct __block_impl impl;
- struct __main_block_desc_0* Desc;
- __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
- impl.isa = &_NSConcreteStackBlock;
- impl.Flags = flags;
- impl.FuncPtr = fp;
- Desc = desc;
+
+代码分析:
+
+- 可以看到我们编写的 block 被声明为一个 `__Person__play_block_impl_0` 类型的结构体
+
+- 结构体内有个构造函数,见22893行代码。
+
+- 因为 objective-c 的方法,默认会携带2个参数,`self` 和 ` _cmd`,等价于 `void play(Person *self, SEL _cmd)`,所以22917行代码 调用构造函数的时候,self 会被传递进去。查看 c++ 代码,可以看到 OC 的 play 方法被转换为
+
+ ```c++
+ static void _I_Person_play(Person * self, SEL _cmd) {
+ void(*playBlock)(void) = ((void (*)())&__Person__play_block_impl_0((void *)__Person__play_block_func_0, &__Person__play_block_desc_0_DATA, self, 570425344));
+ ((void (*)(__block_impl *))((__block_impl *)playBlock)->FuncPtr)((__block_impl *)playBlock);
}
-};
-static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
- NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_65da50_mi_0, age, height);
-}
-```
+ ```
-可以看到全局变量是直接访问的,不进行拷贝。
+- block 为了局部变量 self 的将来访问,结构体内部也增加了一个 Person 类型的 self,所以存在 self 的变量捕获。
-为什么这么设计?
-因为 auto 修饰的变量一出作用域马上回收,所以 block 为了自身运行信息的完整性,所以会捕获。static 修饰的变量,数据一直在内存中,所以执行 block 代码的时候是用指针获取内存中最新的数据。全局变量不会捕获。
-self 会捕获吗?
+所以,答案是会,因为 self 就是局部变量。 一个 oc 方法转换为 `void test(Person *self, SEL _cmd)` 形式,所以 self 也是局部变量,会被捕获。
+
-会,因为 self 就是局部变量。 一个 oc 方法转换为 `void test(Person *self, SEL _cmd)` 形式,所以 self 也是局部变量,会被捕获
## block 类型
-block 的类型可以通过 isa 或者 class 方法查看,最终都是继承自 NSBlock 类型
+### 类型划分
-`__NSGlobalBlock__` (`_NSConcreteGlobalBlock`):程序的数据区域(.data 区)
+我们知道 block 可以看成是一个 oc 对象,所以它有类型,写个 Demo1 验证下
-`__NSStackBlock__` (`_NSConcreteStackBlock`)
+
+
+也就是说: `__NSGlobalBlock__` -> `NSBlock` -> `NSObject`。
+
+继续验证,Demo2
+
+
+
+同时利用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++
+
+梳理分析下:
+
+- 通过打印发现,block1 为 `__NSGlobalBlock__` 类型,但是 clang 转为 c++ 后为 `_NSConcreteStackBlock` 类型
+- block1 为 `__NSMallocBlock__` 类型,但是 clang 转为 c++ 后为 `_NSConcreteStackBlock` 类型
+- block2 为 `__NSStackBlock__` 类型,但是 clang 转为 c++ 后为 `_NSConcreteStackBlock` 类型
+- 虽然可以用 clang 将 OC 转换为 c++ 来分析问题,但是 OC 最强大的是运行时,所以编译期转换为 c++ 看到的信息不一定是准确的,还是以运行时的信息为准
+
+
+
+简单结论:
+
+block 的类型可以通过 isa 或者 class 方法查看,最终都是继承自 NSBlock 类型,共存在3种类型的 block:
+
+- `__NSGlobalBlock__` (`_NSConcreteGlobalBlock`):程序的数据区域(.data 区)
+
+- `__NSStackBlock__` (`_NSConcreteStackBlock`),会自动销毁
+
+- `__NSMallocBlock__`(`_NSConcreteMallockBlock`),需要程序员自己管理内存
+
+这3种 block 在内存中的排布如下图:
+
+
-`__NSMallocBlock__`(`_NSConcreteMallockBlock`)
-```objectivec
-void(^block)(void) = ^(){
- NSLog(@"Hello block");
-};
-NSLog(@"%@", [block class]); // __NSGlobalBlock__
-NSLog(@"%@", [[block class] superclass]); // NSBlock
-NSLog(@"%@", [[[block class] superclass] superclass]); // NSObjec
-```
-
-代码存放在 text 段,static 修饰的数据存放在 data 区,程序员手动申请的内存存放在堆,局部变量存放在栈区
### 如何判断 block 属于什么类型
-Demo1
+Demo:
-```objectivec
-void(^block)(void) = ^{
- NSLog(@"");
-};
-int main(int argc, const char * argv[]) {
- @autoreleasepool {
- NSLog(@"%@", [block class]);
- }
- return 0;
-}
-```
+由于 ARC 默认会做一些优化,我们将工程的 ARC 关掉(Build Setting 里 Automatic Reference Counting 设置为 No)
-`__NSGlobalBlock__` ,此**类型的 block 用结构体实例的内容不依赖于执行时的状态,所以整个程序只需要1个实例。因此存放于全局变量相同的数据区域即可。**
+
-```objectivec
-void(^block)(void);
-void test()
-{
- int age = 22;
- block = ^ {
- NSLog(@"ag:%d", age);
- };
-}
-int main(int argc, const char * argv[]) {
- @autoreleasepool {
- test();
- block(); // age:-1074793800
- }
- return 0;
-}
-```
+分析:
-为什么会打印出 `age:-1074793800`。 因为 block 访问了auto 变量,所以是 `__NSStackBlock__`。那么 block 内部的数据在栈上维护,当出了 test 方法后,block 内部变量的地址到底指向什么是不确定的,可能会出现异常。
+- block1 是 `__NSGlobalBlock__` ,此**类型的 block 用结构体实例的内容不依赖于执行时的状态,所以整个程序只需要1个实例。因此存放于全局变量相同的数据区域即可。**
+- block2 是 `__NSStackBlock__`.
+- 为什么执行 block2 的时候发生了 crash?猜测由于在 test 方法内给 block2 赋值,也就是在栈上定义和捕获了栈上的变量 age,test 方法结束,可能栈上的数据消失或者乱了,所以这个情况下调用 block2 会 crash。
+
+针对 block2 的问题该怎么处理?
+
+当 `__NSStackBlock__` 调用 copy 方法后会变为 `__NSMallocBlock__`。如下图:
+
+
+
+
+
+Demo 也同时发现,当对 `__NSGlobalBlock__` 调用 copy ,不会变为 `__NSMallocBlock__` 。
+
+
+
+### 总结
| block 类型 | 环境 |
| ------------------- | ------------------------------ |
@@ -295,6 +513,44 @@ int main(int argc, const char * argv[]) {
| `__NSStackBlock__` | 访问了 auto 变量 |
| `__NSMallocBlock__` | `__NSStackBlock__` 调用了 copy 方法 |
+调用 copy 方法
+
+| Block 类 | 原本位置 | 复制效果 |
+| --------------------------- | ------------ | ---------- |
+| `__NSConcreteStackBlock__` | 栈 | 栈复制到堆 |
+| `__NSConcreteGlobalBlock__` | 程序的数据段 | 什么也不做 |
+| `__NSConcreteMallocBlock__` | 堆 | 引用计数+1 |
+
+
+
+## 内存管理
+
+### ARC 针对 block 的优化
+
+#### block 作为函数返回值,并且捕获了 auto 变量
+
+MRC 下 block 作为函数的返回值是比较危险的。在方法内部,也就是栈上定义的 block,函数调用结束后可能一些相关数据就释放了,存在潜在风险。
+
+
+
+
+
+
+
+MRC 下如果函数返回值是 block,且 block 内做了 auto 变量捕获的逻辑,编译器会报错:`Returning block that lives on the local stack`。此时 block 应该为 `__NSStackBlock__`
+
+
+
+
+
+改为 ARC,看看
+
+
+
+也就是说,ARC 模式下,当 block 捕获了 auto 变量,并且作为函数返回值的时候,ARC 会自动调用 copy 方法,将 `__NSStackBlock__` 变为 `__NSMallocBlock__`
+
+
+
Demo1:
```objectivec
@@ -364,19 +620,88 @@ ARC 下:如果 block 调用 copy 方法,则 block 仍旧为 `__NSMallocBloc
-| Block 类 | 原本位置 | 复制效果 | |
-| --------------------------- | ------ | ------ | --- |
-| `__NSConcreteStackBlock__` | 栈 | 栈复制到堆 | |
-| `__NSConcreteGlobalBlock__` | 程序的数据段 | 什么也不做 | |
-| `__NSConcreteMallocBlock__` | 堆 | 引用计数+1 | |
+#### ARC 针对强指针指向的 block 会调用 copy
-### 内存管理
+MRC 下,栈内捕获了 auto 变量的 block 为 `__NSStackBlock__`
-### 思考题
+
-查看 block 编译成 c++ 代码的源码可以发现 `__main_block_desc_0` 结构体内部是变化的。什么意思呢?reserved、Block_size 是一直有的,void (*copy)、void (*dispose) 只有在修饰对象的时候才有。为什么这么设计?
+改为 ARC
-```c
+
+
+
+
+说明:ARC 模式下,如果 block 被强指针指向,则会自动调用 copy 方法。
+
+- 捕获了 auto 变量的 `__NSStackBlock__`,ARC 下调用 copy 会变为 `__NSMallocBlock__`
+- 没有捕获变量的 `__NSGlobalBlock__`,ARC 下调用 copy 依旧为 `__NSGlobalBlock__`
+
+
+
+#### 总结
+
+在 ARC 下,编译器会根据情况,自动将战上的 block 复制到对上,比如:
+
+- block作为函数返回值时
+- 将 block 赋值给 `__strong` 指针时
+- block 传递给 Cocoa API 中名字含有 usingBlock 的方法参数时
+- block 传递给 GCD 的方法参数时
+
+
+
+
+
+
+
+ARC 下:block 对象捕获了 auto 外部变量,是一种 `__NSMallocBlock__`,捕获的对象将会在 block 销毁后销毁
+
+
+
+
+
+MRC 下:block 对象捕获了 auto 外部变量,是一种 `__NSMallocBlock__`,因为是 MRC,所以需要手动管理内存。会发现对象将在离开作用域后立马销毁,不会被 block 所捕获。
+
+
+
+
+
+MRC,对 block 加 copy,变为 `__NSMallocBlock__` 呢?
+
+
+
+ARC 下对 block 引用的对象加 `__weak` 修饰呢?
+
+
+
+用指令 `xcrun --sdk iphoneos clang -arch arm64 main.m -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 -o main-arm64.cpp` 转换为 c++ 进行分析看看。注意,因为 weak 涉及运行时,需要在 clang 后添加 runtime 参数
+
+
+
+如果对 Person 不加 `__weak` 修饰,block 结构体内部将会是`__strong`。
+
+
+
+
+
+思考:发现生成的 c++ 代码中,block_desc 里面多了2个成员变量。
+
+````c++
+static struct __main_block_desc_0 {
+ size_t reserved;
+ size_t Block_size;
+ void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
+ void (*dispose)(struct __main_block_impl_0*);
+} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
+````
+
+仔细想想,查看 block 编译成 c++ 代码的源码可以发现 `__main_block_desc_0` 结构体内部是变化的。什么意思呢?reserved、Block_size 是一直有的,`void (*copy)`、`void (*dispose)` 只有在修饰对象的时候才有。为什么这么设计?
+
+
+
+### block 的 copy、dispose
+
+```c++
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
@@ -389,29 +714,23 @@ static struct __main_block_desc_0 {
如果 block 访问的不是对象,则结构体没有 `void *copy`、`void *dispose`
-Demo1:
+```c++
+static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);}
-```objectivec
-{
- Person *person = [[Person alloc] init];
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- NSLog(@"1---%@", person);
- });
-}
+static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);}
```
-1s 后 person 执行了 dealloc 方法
+其中 `_Block_object_assign` 会根据要不要拥有对象,内部决定要不要给对象调用 retain 方法。
+
+Demo1:
+
+
Demo2
-```objectivec
-{
- __weak Person *person = [[Person alloc] init];
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- NSLog(@"1---%@", person);
- });
-}
-```
+
+
+
马上执行了 Person 的 dealloc 方法。因为 `__weak` 修饰,block 内部的 `_Block_object_assign` 会根据 `__strong` 为对象引用计数 +1,`__weak` 则引用计数不变。所以是 `__weak` 修饰,出离作用域则立马会释放 Person 对象。
@@ -419,106 +738,23 @@ Demo2
Demo3
-```objectivec
-{
- Person *person = [[Person alloc] init];
- __weak Person *weakP = person;
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- NSLog(@"1---%@", person);
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- NSLog(@"2---%@", person);
- });
- });
-}
-```
+
-3s 后执行 Person 的 dealloc
+因为 GCD 是给 MainQueue 添加任务的,所以是串行,2个任务前后按照3s、1s 后打印。由于最晚的一个任务是访问强指针对象,所以不会释放。等到 GCD 全部执行完后,Person 才释放。
-Demo4
+Demo4
-```objectivec
-{
- Person *person = [[Person alloc] init];
- __weak Person *weakP = person;
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- NSLog(@"1---%@", weakP);
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- NSLog(@"2---%@", person);
- });
- });
-}
-```
+
-3s 后执行 Person 的 dealloc 方法
-Demo5
-```objectivec
-{
- Person *person = [[Person alloc] init];
- __weak Person *weakP = person;
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- NSLog(@"1---%@", person);
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- NSLog(@"2---%@", weakP);
- });
- });
-}
-```
+在给 MainQueue 提交同步任务的时候,第一个任务是一个 block,访问了强指针指向的 Person(内部会调用 `_Block_object_assign`,发现是强引用,对 p 的引用计数 +1,当 block 执行完后,调用 `_Block_object_dispose` 对 p 的引用计数 -1),第二个任务是弱指针指向的 Person,引用计数不做操作。当1s 后第一个任务执行后,Person 被释放。第二个任务执行的时候,访问 name 属性就是给 nil 发消息,不会 crash,但是为 null。
-1s 后执行 Person 的 dealloc 方法。
-结论:block 作为 GCD API 的方法参数时,如果 block 内部访问了对象,对象的生命周期结束需要查看强引用结束时刻。
-### ARC 环境下编译器会自动会 block copy 复制到堆上
+### block 如何修改变量
-1. block 作为函数返回值时(如果栈 block,离开方法作用域之后,return 给新的变量区使用,由于栈变化了,所以不安全。比如访问 auto 变量的栈 block,可能某个变量已经不是之前的某个值了)
-
-2. 将 block 赋值给 __strong 指针时
-
-3. block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时
-
-4. block 作为 GCD API 的方法参数时
-
-栈空间的 block 不会对变量进行 copy 操作
-
-堆空间的 block 会堆变量自动进行 copy 操作
-
-`__NSStackBlock__` 内部访问了对象,默认是 `__strong` 修饰。如果对象是 `__weak` 则 block 转换 c++ 内部捕获的对象,也用 weak 修饰
-
-```c
-typedef void(^MyBlock)(void);
-int main(int argc, const char * argv[]) {
- @autoreleasepool {
- MyBlock block;
- {
- __weak Person *person = [[Person alloc] init];
- person.age = 27;
- block = ^{
- NSLog(@"age:%zd", person.age);
- };
- person.age = 28;
- };
- }
- return 0;
-}
-
-struct __main_block_impl_0 {
- struct __block_impl impl;
- struct __main_block_desc_0* Desc;
- Person *__weak person;
- __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _person, int flags=0) : person(_person) {
- impl.isa = &_NSConcreteStackBlock;
- impl.Flags = flags;
- impl.FuncPtr = fp;
- Desc = desc;
- }
-};
-```
-
-## block 修改变量
-
-Demo1
+#### __block 修饰基本数据类型
```objectivec
typedef void(^MyBlock)(void);
@@ -534,31 +770,47 @@ int main(int argc, const char * argv[]) {
}
```
-运行会报错 `// Variable is not assignable (missing __block type specifier)` 为什么不能修改?继续查看 c++ 源代码
+编译会报错 `// Variable is not assignable (missing __block type specifier)` 为什么不能修改?
+
+把 block 内修改的那行代码注释了,转成 c++ 看看
```c
-static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
+struct __ViewController__viewDidLoad_block_impl_0 {
+ struct __block_impl impl;
+ struct __ViewController__viewDidLoad_block_desc_0* Desc;
+ int age;
+ __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
+ impl.isa = &_NSConcreteStackBlock;
+ impl.Flags = flags;
+ impl.FuncPtr = fp;
+ Desc = desc;
+ }
+};
+static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
- NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_f31a48_mi_0, age);
-}
-static struct __main_block_desc_0 {
+ NSLog((NSString *)&__NSConstantStringImpl__var_folders_7x_g921y52j5yb_w5hsn3fb3m8r0000gn_T_ViewController_3ceae4_mi_0, age);
+ }
+
+static struct __ViewController__viewDidLoad_block_desc_0 {
size_t reserved;
size_t Block_size;
-} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
-int main(int argc, const char * argv[]) {
- /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
- int age = 27;
- MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
- NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_f31a48_mi_1, age);
- }
- return 0;
+} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)};
+
+static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
+ ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
+ int age = 27;
+ void(*ChangeValueBlock)(void) = ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, age));
}
```
-可以看到在 block 内部修改外部变量也就是在新创建的函数内部,修改 main 函数内部的变量 😂 这怎么可能?
+可以看到, block 内部的逻辑被包装成一个新的 `__ViewController__viewDidLoad_block_func_0`方法,然而 age 定义在 `_I_ViewController_viewDidLoad` 方法中。没有传递引用,也没任何特殊处理,所以没办法修改。
-全局变量、static 变量在 block 内部可以修改。
+
+
+思考:那如何实现修改一个变量?
+
+全局变量、static 变量、`__block`修饰的变量在 block 内部可以修改。
- `__block` 用于解决 block 内部无法修改 auto 变量的问题。
@@ -577,11 +829,9 @@ MyBlock block = ^{
转为 C++
-```c
-__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 27};
- MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
- NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_main_f0f60a_mi_0, (age.__forwarding->age));
+
+```c++
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
@@ -590,47 +840,118 @@ __Block_byref_age_0 *__forwarding;
int age;
};
-static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
+struct __ViewController__viewDidLoad_block_impl_0 {
+ struct __block_impl impl;
+ struct __ViewController__viewDidLoad_block_desc_0* Desc;
+ __Block_byref_age_0 *age; // by ref
+ __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
+ impl.isa = &_NSConcreteStackBlock;
+ impl.Flags = flags;
+ impl.FuncPtr = fp;
+ Desc = desc;
+ }
+};
+static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // bound by ref
- (age->__forwarding->age) = 28;
+
+ (age->__forwarding->age) = 28;
+ NSLog((NSString *)&__NSConstantStringImpl__var_folders_7x_g921y52j5yb_w5hsn3fb3m8r0000gn_T_ViewController_7fbccf_mi_0, (age->__forwarding->age));
+ }
+
+static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
+ ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
+ __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 27};
+ void(*ChangeValueBlock)(void) = ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
}
```
-可以看到 `__block int age = 27;` 变为了 `__Block_byref_age_0 age` 结构体。block 内部的函数在修改 age 的时候其实就是通过 `__main_block_impl_0` 结构体的 age 找到 `__Block_byref_age_0`,然后访问 `__Block_byref_age_0` 中的成员变量 `__forwarding` 访问成员变量 age,并修改值。
+可以看到 `__block int age = 27;` 变为了 `__Block_byref_age_0` 结构体,给结构体赋值的时候,第二个成员变量的值就是结构体自身地址。
+
+`__ViewController__viewDidLoad_block_impl_0` 结构体构造函数 `age(_age->__forwarding)` 就是把外面传递进来结构体的指针,所指向的结构体的 `__forwarding` 成员变量赋值给 block 结构体内的 age 成员变量。
+
+block 内部的函数在修改 age 的时候其实就是通过 `__main_block_impl_0` 结构体的 age 找到 `__Block_byref_age_0`,然后访问 `__Block_byref_age_0` 中的成员变量 `__forwarding` 访问成员变量 age,并修改值。
+
+
+
+
-
- `__block` 修饰基本数据类型和对象,对于生成的结构体也不一样。
QA:为什么`__block` 变量的 `__Block_byref_age_0` 结构体并不在 block 结构体 `__main_block_impl_0` 中?
因为这样做可以在多个 block 中使用 `__block` 变量。
-Demo
-```objectivec
-struct __Block_byref_age_0 {
- void *__isa;
-__Block_byref_age_0 *__forwarding;
- int __flags;
- int __size;
- int age;
-};
-struct __Block_byref_p_1 {
- void *__isa;
-__Block_byref_p_1 *__forwarding;
- int __flags;
- int __size;
- void (*__Block_byref_id_object_copy)(void*, void*);
- void (*__Block_byref_id_object_dispose)(void*);
- NSObject *p;
+看个有趣的例子,验证下 __block 的效果
+
+
+
+转换成 c++ 可以看到
+
+```c++
+struct __ViewController__viewDidLoad_block_impl_1 {
+ struct __block_impl impl;
+ struct __ViewController__viewDidLoad_block_desc_1* Desc;
+ __Block_byref_num2_0 *num2; // by ref
+ __ViewController__viewDidLoad_block_impl_1(void *fp, struct __ViewController__viewDidLoad_block_desc_1 *desc, __Block_byref_num2_0 *_num2, int flags=0) : num2(_num2->__forwarding) {
+ impl.isa = &_NSConcreteStackBlock;
+ impl.Flags = flags;
+ impl.FuncPtr = fp;
+ Desc = desc;
+ }
};
+static long __ViewController__viewDidLoad_block_func_1(struct __ViewController__viewDidLoad_block_impl_1 *__cself, NSInteger num3) {
+ __Block_byref_num2_0 *num2 = __cself->num2; // bound by ref
+
+ return (num2->__forwarding->num2) + num3;
+ }
+static void __ViewController__viewDidLoad_block_copy_1(struct __ViewController__viewDidLoad_block_impl_1*dst, struct __ViewController__viewDidLoad_block_impl_1*src) {_Block_object_assign((void*)&dst->num2, (void*)src->num2, 8/*BLOCK_FIELD_IS_BYREF*/);}
+
+static void __ViewController__viewDidLoad_block_dispose_1(struct __ViewController__viewDidLoad_block_impl_1*src) {_Block_object_dispose((void*)src->num2, 8/*BLOCK_FIELD_IS_BYREF*/);}
+
+static struct __ViewController__viewDidLoad_block_desc_1 {
+ size_t reserved;
+ size_t Block_size;
+ void (*copy)(struct __ViewController__viewDidLoad_block_impl_1*, struct __ViewController__viewDidLoad_block_impl_1*);
+ void (*dispose)(struct __ViewController__viewDidLoad_block_impl_1*);
+} __ViewController__viewDidLoad_block_desc_1_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_1), __ViewController__viewDidLoad_block_copy_1, __ViewController__viewDidLoad_block_dispose_1};
+
+__attribute__((__blocks__(byref))) __Block_byref_num2_0 num2 = {(void*)0,(__Block_byref_num2_0 *)&num2, 0, sizeof(__Block_byref_num2_0), 22};
+ NSInteger(*testBlock2)(NSInteger num3) = ((long (*)(NSInteger))&__ViewController__viewDidLoad_block_impl_1((void *)__ViewController__viewDidLoad_block_func_1, &__ViewController__viewDidLoad_block_desc_1_DATA, (__Block_byref_num2_0 *)&num2, 570425344));
+ (num2.__forwarding->num2) = 24;
```
-block 只要和对象打交道,结构体里面要管理内存,所以会有 `void *copy` ,`void *dispose`
+外面的 num2 被 `__block` 修饰后变为了对象。通过 num2 对象的 `__forwarding` 指针,再访问 num2 即可修改值。
-block 内部操作数组等类型,不需要加 `__block`
+
+
+#### __block 修饰对象
+
+对` __block` 修饰的对象,clang 转换为 c++ 后如下:
+
+
+
+
+
+分析发现:
+
+- 对于 `__block` 修饰对象数据,对于生成的结构体也不一样。`__Block_byref_obj_0` 中含有2个操作内存的成员变量`__Block_byref_id_object_copy`、`__Block_byref_id_object_dispose`
+- 其他的逻辑和 `__block` 修饰基本数据类型一致
+
+
+
+注意:
+
+
+
+
+
+- block 外定义的 NSMutableArray,block 内只是使用数组则不需要` __block`
+
+- 如果在 block 利操作指针,则需要加 `__block`
+
+注意:`__weak` 只可以用来修饰对象,(终端用 clang 处理)否则 clang 会报错 `warning: 'objc_ownership' only applies to Objective-C object or block pointer types; type here is 'int' [-Wignored-attributes]`
@@ -692,7 +1013,7 @@ int main(int argc, const char * argv[]) {
我们将断点设置到 NSLog 这里,打印出自定义结构体 `__main_block_impl_0` 中的 age 。
-
+
```c
// 0x0000000105231f70
@@ -734,26 +1055,139 @@ block 内部对变量的值修改其实就是对 block 内部自定义结构体
+#### 什么情况下需要 __block
+
+局部变量:基本数据类型、对象数据类型
+
+
+
+#### 什么情况下不需要 __block
+
+- 全局变量(不截获)
+- 静态全局变量(不截获)
+- 静态局部变量(截获指针)
+
+
+
+
+
## `__forwarding` 的设计
-
+看一个例子,`__block` 如何修改外部变量
-当block在栈中时,`__Block_byref_age_0`结构体内的`__forwarding`指针指向结构体自己。
-
-而当block被复制到堆中时,栈中的`__Block_byref_age_0`结构体也会被复制到堆中一份,而此时栈中的`__Block_byref_age_0`结构体中的`__forwarding`指针指向的就是堆中的`__Block_byref_age_0`结构体,堆中`__Block_byref_age_0`结构体内的`__forwarding`指针依然指向自己。
+```objective-c
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ __block int age = 27;
+ NSLog(@"1: age = %d, address is %p", age, &age);
+ void(^block1)(void) = ^{
+ age = 28;
+ NSLog(@"in block: age = %d, address is %p", age, &age);
+ };
+ NSLog(@"2: age = %d, address is %p", age, &age);
+ block1();
+ NSLog(@"3: age = %d, address is %p", age, &age);
+ age = 29;
+ NSLog(@"4: age = %d, address is %p", age, &age);
+}
+// console
+1: age = 27, address is 0x7ff7b0faebf8
+2: age = 27, address is 0x600000464938
+in block: age = 28, address is 0x600000464938
+3: age = 28, address is 0x600000464938
+4: age = 29, address is 0x600000464938
+```
+
+分析:
+
+- `__block` 修饰的外部变量将会被封装为一个结构体对象,该结构体对象内有一个 `__forwarding` 成员变量
+
+ ```c++
+ struct __Block_byref_age_0 {
+ void *__isa;
+ __Block_byref_age_0 *__forwarding;
+ int __flags;
+ int __size;
+ int age;
+ };
+ ```
+
+- 在给 block 赋值的时候,其成员变量 `__forwarding`的值是由当前结构体对象的地址赋值的
+
+ ````c++
+ __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 27};
+ ````
+
+- block 内部代码,将被封装为一个新的函数 `__ViewController__viewDidLoad_block_func_0`,其内部通过结构体指针` _cself` 的 age 成员变量,获取到 `__Block_byref_age_0` 指针,该指针命名为 age。然后通过 age 指针访问到结构体的 `__forwarding` 成员变量,该成员变量指向的是结构体自己,然后再访问 age 拿到真正的 age 进行修改。
+
+ ```c++
+ static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
+ __Block_byref_age_0 *age = __cself->age; // bound by ref
+ (age->__forwarding->age) = 28;
+ NSLog((NSString *)&__NSConstantStringImpl__var_folders_7x_g921y52j5yb_w5hsn3fb3m8r0000gn_T_ViewController_bccea7_mi_1, (age->__forwarding->age), &(age->__forwarding->age));
+ }
+ ```
+
+- 第一行输出 `1: age = 27, address is 0x7ff7b0faebf8` 是因为此时 age 在栈上,高地址 `0x7ff7b0faebf8`
+
+- 第二行输出 `2: age = 27, address is 0x600000464938` 是因为 block 被拷贝到此对上,内部对于使用到的` __block` 变量也会拷贝到堆上,是通过一个结构体对象来实现的。由于在栈上,地址变为 `0x600000464938`,相较于栈上的地址,地址变低了。
+
+- 将 block 从栈拷贝到堆上时,block 所捕获的 `__block` 变量也会从栈拷贝到堆上,但是此时我们在该函数的作用域内(即 block 外)仍然是可以对 age 变量进行修改的
+
+- 第三行输出 `in block: age = 28, address is 0x600000464938` 是因为此时 age 在堆上,低地址 `0x600000464938`。通过结构体 `__ViewController__viewDidLoad_block_impl_0`的 age 成员变量指向的 `__Block_byref_age_0` 指针,再通过指针指向的 `__forwarding` 指向自己,再访问 age 来修改值
+
+- 为了将上述修改进行同步,在将 `__block` 变量从栈拷贝到堆上时,栈上的 `__Block_byref_val_0` 结构体的 `__forwarding` 指针将会指向堆上的 `__Block_byref_val_0` 结构体。所以此时,`age` 变量(即`age.__forwarding->age`变量)的地址改变了
+
+- 第四行输出 `3: age = 28, address is 0x600000464938` 是因为此时 age 在堆上,低地址 `0x600000464938`,且值为28
+
+- 第五行输出 `4: age = 29, address is 0x600000464938` 是因为此时通过栈上的 age 结构体,通过成员变量 `__forwarding` 指向对上的结构体地址,然后再通过指向堆上的结构体的 age 成员变量已经被修改为 29 了
+
+总结下:
+
+那么` __forwarding` 的作用是什么?为什么这么设计
+
+
+
+
+
+- 当 block在栈中时,`__Block_byref_age_0` 结构体内的 `__forwarding` 指针指向栈上的结构体自己
+
+- 而当 block 被复制到堆中时,栈中的`__Block_byref_age_0` 结构体也会被复制到堆中一份,而此时栈中的 `__Block_byref_age_0` 结构体的成员变量 `__forwarding` 指针指向的就是堆中的 `__Block_byref_age_0`结构体,堆中 `__Block_byref_age_0`结构体内的 `__forwarding` 指针依然指向自己,此时再访问成员变量 age 就可以修改堆上的值
+
+**一言以蔽之,`__forwarding` 指针是为了在 `__block` 变量从栈复制到堆上后,在 block 外对 `__block` 变量的修改也可以同步到堆上实际存储的 `__block` 变量的结构体上。也就是抹平栈、堆上对变量操作的差异。**
+
+不论在
## Block 内存引用
-被 `__block ` 修饰符修饰的对象在内存中如下
+对于` __block` 修饰的变量进行研究
-
+Demo1
+
+
+
+
+
+
+
+Demo2
+
+
+
+
+
+
+
+分析:
+
+- block 结构体里面的针对变量生成的结构体新对象,都是 strong 指针
+- block 所捕获的对象是` __weak` 还是` __strong` 决定的是新生成结构体对象里面的对象内存访问修饰符。
```c
-
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_p_0 p = {(void*)0,(__Block_byref_p_0 *)&p, 33554432, sizeof(__Block_byref_p_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"))};
@@ -772,8 +1206,6 @@ static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_bl
}
```
-
-
如果 `__block` 修饰 `__strong` 则表示 block_impl 结构体中的 person 成员变量指向一个新的结构体 `__Block_byref_person_0`。这个线是强引用。
`__Block_byref_person_0` 结构体成员变量 person 真正的 Person 对象的引用关系要看 block 外部 person 的修饰是 `__strong` 还是 `__weak`,因为从栈上拷贝到堆上,会调用 block 的 desc 的 `__main_block_copy_0`,本质上调用的是 `_Block_object_assign`
@@ -815,15 +1247,17 @@ __attribute__((__blocks__(byref))) __Block_byref_p_0 p = {
self 是一个局部变量,block 访问 self,即存在捕获变量的效果。
+为什么会存在循环引用?block 会对截获的变量是对象类型,会把所有权也进行捕获。为什么 strong 类型的对象,会造成对象和 block 的循环引用
+
+
+
### ARC 下
- `__weak`、`__unsafe_unretained`、`__block``
-
-区别在于:`__weak` 不会产生强引用,指向的对象销毁时,会自动给指针置为 nil
-
-`__unsafe_retained` 不会产生强引用,不安全。当指向的对象销毁时,指针地址值不变。
+ `__weak`、`__unsafe_unretained` 修饰 `__block` 所修饰的变量。区别在于:
+- `__weak` 不会产生强引用,指向的对象销毁时,会自动给指针置为 nil
+- `__unsafe_retained` 不会产生强引用,不安全。当指向的对象销毁时,指针地址值不变。
```objectivec
@interface Person : NSObject
@@ -839,7 +1273,6 @@ __attribute__((__blocks__(byref))) __Block_byref_p_0 p = {
}
- (void)test
{
-
__weak typeof(self) weakself = self;
self.block = ^{
weakself.age = 23;
@@ -855,9 +1288,9 @@ Person *p = [[Person alloc] init];
方法1: `__weak` 修饰。`__weak typeof(self) weakself = self;`
-方法2: `__unsafe_retained` 修饰。`**__unsafe_unretained** **typeof**(**self**) weakself = **self**;`
+方法2: `__unsafe_retained` 修饰。`__unsafe_unretained typeof(self) weakself = self;`
-方法3: `__block` 修饰。因为此时会构成3角关系。所以需要调用 block。block 内部需要将对象设置为 nil。
+方法3: `__block` 修饰。因为此时会构成3角关系。所以需要调用 block,block 内部需要将对象设置为 nil。虽然` __block` 方案也可以解决循环引用的问题,但是缺点是该 block 需要执行,方案会有限制。
```objectivec
__block Person *weakself = [[Person alloc] init];
@@ -871,11 +1304,13 @@ p.block();
`__unsafe_retained` 因为不安全所以不推荐,`__block` 因为使用繁琐,且必须等到调用 block 才会释放内存,所以不推荐。ARC 下最佳用 `__weak`
-
+
+
+
### MRC 下
-方法1: `__unsafe_retained` 修饰。`**__unsafe_unretained** **typeof**(**self**) weakself = **self**;`
+方法1: `__unsafe_retained` 修饰。`__unsafe_unretained typeof(self) weakself = self;`
方法2: `__block` 修饰。MRC 下不会对 block 内部的对象引用计数 +1
@@ -885,18 +1320,12 @@ p.block();
## 总结
-block 本质是什么?
-
-封装了函数调用及其调用环境的 OC 对象
-
-
-
-`__block` 的作用是什么?
-
-可以对 block 外部的变量进行捕获,可以修改。但是需要注意内存管理相关问题。比如`__weak`、`__unsafe_unretained`、`__block`
-
-
-
-修改 NSMutableArray 不需要加 `__block`?
-
-是的
\ No newline at end of file
+1. block 本质是什么?封装了函数调用及其调用环境的 OC 对象。本质实现是一个结构体。
+2. `__block` 的作用是什么?可以对 block 外部的变量进行捕获,可以修改。但是需要注意内存管理相关问题。比如`__weak`、`__unsafe_unretained`、`__block`
+3. 修改 NSMutableArray 不需要加 `__block`? 是的,如果修改 NSMutableArray 指针比如`array = nil` 则需要加`__block `
+4. block 属性修饰词为什么是 copy?没有进行 copy 操作的时候,block 就不会在堆上,对于 block 生命周期以及所使用到的内存,没办法灵活控制(由栈控制,出栈就死)。因为 block 的高频使用场景就是作为方法参数传递、作为类的属性值,所以最常见的场景是:赋值的地方不是使用的地方,所以要捕获周围环境参数和管理所捕获的内存、以及自身内存。
+5. 为什么会产生循环引用?
+ 1. 如果当前当前 block 对于某个变量进行捕获,变量也是强引用类型的,block 捕获变量后,block 对变量是强引用关系,当前对象(VC)对 block 是强引用关系,变量也是 VC 强引用的,就产生了循环引用。
+ 2. 用 __block 修饰:
+ - MRC 下不会产生循环引用
+ - ARC 下会产生循环引用,可以采用断环的方式解决。
\ No newline at end of file
diff --git a/Chapter1 - iOS/1.91.md b/Chapter1 - iOS/1.91.md
index a602793..ac751bc 100644
--- a/Chapter1 - iOS/1.91.md
+++ b/Chapter1 - iOS/1.91.md
@@ -1,6 +1,2488 @@
-DYLD 及 Mach-O
+# DYLD 及 Mach-O
-dynamic loader,动态加载器。在 MacOS/iOS 中,是使用 `/usr/lib/dyld` 程序来加载动态库的。
+什么是 DYLD?dynamic loader,动态加载器。在 MacOS/iOS 中,是使用 `/usr/lib/dyld` 程序来加载动态库的。
+
+
+
+## 从源文件到可执行文件做了什么?
+
+问:源文件 -> 可执行文件,是不是只需要经过编译就够了?
+
+不是的。从源文件到可执行文件,需要经过编译、链接2个步骤:
+
+- 编译:将源代码转为了二进制机器指令。保存这些二进制机器指令的文件,叫做目标文件 `.o` 文件。每个源文件对应一个目标文件。
+- 链接:得到目标文件怎么变成可执行程序呢?链接器将这些目标文件打包成最终可执行程序。除了打包目标文件外,链接器还会打包一个非常重要的东西,就是标准库。可执行程序 = 程序员写的代码 + 使用到的标准库(动态库/静态库)。
+
+那看上去链接器做的事情很简单,不就是个打包工具?**链接器最重要的工作就是决定符号(变量名、函数名)的定义**
+
+```c++
+#include
+
+int main() {
+ NSLog(@"Hello world");
+ return 0;
+}
+```
+
+例如上面的代码 `main.m`。编译器在编译 `main.m` 时遇到 NSLog,根本不知道这个 `NSLog` 符号定义在哪里,这不是编译器该关心的事情。因此,编译器只能看到局部,只聚焦关心一个当前的源文件。到底谁来关心这个 NSLog 符号定义在哪呢?这就是链接器。
+
+链接器打包所有的目标文件,因为链接器可以看到全局,具有上帝视角,因此链接器从依赖的库中去查找 NSLog 这个符号,如果找不到则会报经典的错误 `undefined reference to ***`
+
+编译器只能将 NSLog 这个函数的跳转地址暂时设置为0,随后在链接的时候再去修正它。
+
+
+
+一步步验证下:
+
+第一步,将 main.m 编译为 main.o 文件。指令如下
+
+```shell
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -c main.m -o main.o
+```
+
+第二步,已经得到了 main.o 目标文件,也就是二进制文件,利用指令 `objdump -r main.o` 查看目标文件中的内容
+
+
+
+可以看到 main 函数中,callq 就是调用 NSLog 函数。后面的地址写为了 0,这里的0会在后面链接的过程中被修正。
+
+第三步,另外为了能让链接器能够定位到这些需要被修正的地址,在代码块中可以看到一个重定位表。指令为 `objdump -r main.o`
+
+
+
+NSLog 位于偏移量为19的位置,
+
+第四步,链接目标文件到可执行文件,指令为
+
+```shell
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ main.o -o main
+```
+
+因为目前 Foundation 已经存在于系统目录,所以不需要额外指定动态库/静态库路径了。
+
+
+
+## 输出重定向
+
+
+
+终端输入 `tty` 敲回车,然后在 Xcode 脚本中,就可以将 echo 输出的结果重定向到终端 `echo "message" > /dev/ttys000`
+
+因为要编写脚本,研究 MachO 文件,但每次编译后的 MachO 还要 show in Finder,之后再切换到终端,然后执行脚本很繁琐。所以可以直接在 Xcode 的 Build Phases 中,增加脚本解决该问题。
+
+但 Build Phases 中的 Run Script 长度有限,不能写很多脚本,所以还是需要结合 Xcconfig。
+
+Xcconfig 中定义的变量在 Run Script 中是可以访问的到。
+
+Demo 验证如下:
+
+
+
+
+
+## 符号可见性
+
+按照先后顺序
+
+- 生成目标文件阶段:`-O1、O2、O3、Os、Oz`
+- 链接:死代码剥离 dead code strip
+- 编译后的产物 mach-o:strip 剥离符号
+
+
+
+## MachO 可读可写
+
+Apple 的机制,只要文件可以正确签名,Apple 就认,可以修改。
+
+
+
+## 编译阶段做了什么
+
+- 汇编
+- 将符号归类:
+ - 数据,放在数据段
+ - 可以获取到地址的符号,变成地址
+ - 类似 NSLog 这种只有在链接的时候才可以确定一些东西,那这种暂时无法确定地址的符号,都统一暂存起来。叫做“重定位符号表”。fishhook 就是基于此来实现系统符号的 hook
+- 链接。链接器通过链接将重定位符号表和符号表合并到一张表中,目标文件(`.o` 文件)和符号表,合并到一起,
+
+如何找出需要重定位的符号?
+
+```shell
+objdump --macho --reloc MachOAndSymbol.o
+```
+
+
+
+two_levelnamespace & flat_namespace:
+
+⼆级命名空间与⼀级命名空间。链接器默认采⽤⼆级命名空间,也就是除了会记录符号名称,还会记录符号属于哪个Mach-O的,⽐如会记录下来_NSLog来⾃Foundation。
+
+
+
+## 符号
+
+- 全局符号对整个项目可见,对使用的地方可见,整个符号表都可见。
+
+- Static 只对定义所在的文件可见。
+
+符号的种类
+
+| Symbol Type | **说明** |
+| ----------- | ------------------------------------------------------------ |
+| **U** | undefined(未定义) |
+| **A** | absolute(绝对符号) |
+| **T** | text section symbol(__TEXT.__text) |
+| **D** | data section symbol(__DATA.__data) |
+| **B** | bss section symbol(__DATA.__bss) |
+| **C** | common symbol(只能出现在`MH_OBJECT` 类型的`Mach-O`⽂件中) |
+| **-** | debugger symbol table |
+| **S** | 除了上⾯所述的,存放在其他`section`的内容,例如未初始化的全局变量存放在(__DATA,__common)中 |
+| **I** | indirect symbol(符号信息相同,代表同⼀符号) |
+| **U** | 动态共享库中的⼩写u表示⼀个未定义引⽤对同⼀库中另⼀个模块中私有外部符号 |
+
+编译 `main.m` 到 `main.o`
+
+```shell
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -c main.m -o main.o
+```
+
+使用 `nm -pa .o文件路径` 命令来查看符号
+
+
+
+
+
+
+
+### 符号的分类
+
+- Common Symbol:在定义时,未初始化的全局符号。
+
+ 有趣的 feature:
+
+ ```c++
+ int global_int_age = 28;
+ int global_int_age;
+
+ void main() {
+ print("Hello world");
+ }
+ ```
+
+ 上面的代码不会编译报错。当存在2个同名的全局符号,一个初始化了,一个未初始化,不会报错。在编译链接阶段,当找到定义之后,未定义的会被删掉。
+
+ 针对全局符号:
+
+ - 当存在2个同名的全局符号,一个初始化了,一个未初始化,不会报错。在编译链接阶段,当找到定义之后,未定义的会被删掉。
+ - 链接器默认会把未初始化的全局符号,给强制初始化掉。比如 `int global_age;` 初始化为 `int global_age = 0;`
+
+-
+
+
+
+生成目标文件分过程中,只需要头文件信息(头文件路径),只需要重定位符号表,知道哪些符号需要重定位,链接的时候,会自动将符号重定位。
+
+
+
+## vim 快速查找 API 功能
+
+`man nm`
+
+
+
+
+
+进入 vim 模式了,看到左下角有 `:` 光标,如果想查看当前 nm 命令的参数,可以快速查找,输入 `/ + 具体参数`,敲回车即可跳转到要匹配到的位置,如果有多个结果,且当前自动跳转到的不是正确的位置,vim 模式下可以输入 `n` 跳转到下一个匹配到的位置(n 即 next),输入 `N` 则跳转到上一个匹配到的位置。
+
+
+
+比如查找 `-p`,则输入 `/-p`,敲回车的效果如下
+
+
+
+
+
+### 符号的导入导出及 App 瘦身
+
+代码中使用了 Foundation 库的 NSLog,NSLog 对于业务代码来说,就是一个导入的符号,对于 Foundation 库来说,就是一个导出的符号。
+
+什么符号可以是导出的符号?全局符号可以是导出符号。
+
+App 或者一个 MachO 中,所有使用到的动态库的符号,都保存在间接符号表中,这些间接符号表中的数据,来自于动态库中。
+
+动态库,全局符号 -> 导出符号
+
+间接符号表 -> 动态库符号
+
+所以,Strip 符号的时候,可能不能 Strip 全局符号。
+
+
+
+OC 代码,默认都是导出的全局符号,所以容易占空间,想让体积变小,就可以尽量不想暴露的符号,使用链接器的能力,将不需要暴露的符号不暴露出去。
+
+```shell
+OTHER_LDFLAGS=$(inherited) -Xinker -unexported_symbol -Xlinker _OBJC_CLASS_$_Person
+```
+
+
+
+静态库 = `.o` 文件的合集 + 重定位符号表。但重定位符号表中的符号不能 strip,所以只能 strip `.o` 文件的调试符号。
+
+
+
+QA:从符号角度出发,动态库还是静态库对于 App 瘦身较好(更有抓手)?使用动态库还是静态库会提及更小?
+
+- App 在链接静态库的时候,静态库就是 .o 文件的合集,会把 `.o` 中的符号(包括可以重定位的符号),都放到 App 自身的符号表中,也就意味着可能是:本地符号、全局符号、导出符号。根据 Strip 的原理,Strip 可以脱离除了间接符号表之外的所有符号。
+
+ 静态库链接的时候,除了间接符号表,其他区域都有可能放。所以链接静态库占用体积更小。
+
+- App 在链接动态库的时候,正好相反,App 链接的动态库的符号都放到了间接符号表中,即使 Strip 所有符号,也不可能脱掉间接符号表中的符号。
+
+所以大家在写 SDK 的时候,可以从符号角度出发想想,是选择静态库还是动态库。针对动态库,可以 strip 导出符号。默认 OC 的符号都是导出的。
+
+
+
+## Strip
+
+静态库/ .o 文件 Strip 流程: 处理 `_DWARF` 段
+
+
+
+Strip 的过程,就是在修改 Mach-O 文件中的内容。
+
+
+
+
+
+动态库
+
+
+
+All Symbols
+
+
+
+
+
+Non-Global Symbols(非全局符号):
+
+
+
+
+
+
+
+
+
+## 断点
+
+- 终端 LLDB 模式下通过命令添加的断点是通用的,是符号断点。通过 `br write -f 文件路径` 可以将断点导出,共享给其他人
+- Xcode GUI 添加的断点是带有绝对路径的,通过 `br write -f 文件路径` 导出的断点信息在带有文件的绝对路径,不方便共享。要做的就是文件路径的替换。
+
+
+
+## 链接
+
+源代码编译成目标文件。
+
+```shell
+clang -x objective-c \
+-target x86_64-apple-macos11.1 \
+-fobjc-arc \
+-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+-c main.m -o main.o
+```
+
+
+
+从 `.o` 目标文件链接为可执行文件
+
+```shell
+ clang命令参数:
+ -x: 指定编译文件语言类型
+ -g: 生成调试信息
+ -c: 生成目标文件,只运行preprocess,compile,assemble,不链接
+ -o: 输出文件
+ -isysroot: 使用的SDK路径
+ 1. -I 在指定目录寻找头文件 header search path
+ 2. -L 指定库文件路径(.a\.dylib库文件) library search path
+ 3. -l 指定链接的库文件名称(.a\.dylib库文件)other link flags -lAFNetworking
+ -F 在指定目录寻找framework framework search path
+ -framework 指定链接的framework名称 other link flags -framework AFNetworking
+```
+
+
+
+```shell
+clang -target x86_64-apple-macos13.1 \
+-fobjc-arc \
+-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MaxOSX.platform/Developer/SDKs/MacOSX11.sdk \
+-L./AFNetworking \
+-lAFNetworking \
+test.o -o test
+```
+
+
+
+## 探索「静态库就是 .o 文件的合集」
+
+`.a` 静态库,`.dylib` 动态库使用有问题,一般是:
+
+- header search path
+- library search path
+- other link flags
+
+这3个的一个或者多个造成的问题,着手去排查问题即可。
+
+
+
+做个实验,验证下静态库其实就是 `.o` 文件的合集。
+
+第一步,编写 oc 代码,就一个 Person 类,写一个类方法,编译为静态库。`Person.m` 编译为 `Person.o`
+
+第二步,利用 clang 将 `person.o` 转换为静态库。
+
+
+
+其实,这里就已经可以验证「静态库就是 .o 文件的合集」。
+
+利用 `objdump --macho --private-header Person.dylib` 查看静态库依旧是 `Object File`
+
+
+
+第三步,编写代码 `main.m` 代码,导入静态库 ``
+
+```objective-c
+#import
+#import
+
+int main(int argc, char * argv[]) {
+ Person *p = [[Person alloc] init];
+ [p sayHi];
+ NSLog(@"%@", p);
+}
+```
+
+第四步,利用 clang 将 `main.m` 编译为 `main.o` 文件。注意,因为用到了 NSLog 和导入了静态库的头文件,所以需要加参数指定 NSLog 该符号从哪确定,也需要指定静态库所需的信息。
+
+```shell
+clang -x objective-c \
+ -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -I./StaticLibrary \
+ -c main.m -o main.
+```
+
+第五步,将第四步得到的 `main.o` 文件和前面编译好的 `Person` 静态库
+
+```shell
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ > -L./StaticLibrary \
+ > -lPerson \
+ > main.o -o main
+```
+
+注意:上面第二步,得到的 `Person` 静态库需要重命名下,因为 clang 指令 `-l` 参数的 `Person`,其实就是去找 `libPerson` 的动态库或者静态库。
+
+查找规则:先找 `lib+` 的动态库,找不到,再去找 `lib+` 的静态库,还找不到,就报错
+
+第六步,查看第五步得到的可执行文件,然后执行,看看是否正常?
+
+- 成功,则说明 静态库就是`.o` 文件的集合,单个 `main.o` 文件,修改拓展名就可以变为静态库
+- 不成功,则相反
+
+
+
+
+
+这一部分相关的脚本和源码,可以查看
+
+
+
+## Framework
+
+Mac OS/iOS 平台还可以使用 Framework,Framework 实际上是一种打包方式,将库的:二进制文件、头文件、和有关资源打包到一起,方便管理和分发。
+
+
+
+Framework 和系统 UIKit.Framework 还是有很大区别的。
+
+- 系统的 Framework 不需要拷贝到目标程序中
+- 我们自己的 Framework 不管是静态还是动态的,都需要拷贝到 App 中(App 和 Extension 的 Bundle 是共享的)
+
+因此,Apple 把这种 Framework 又叫做 Embedded Framework
+
+
+
+Framework:
+
+- 动态库:Header + `.dylib` + 签名 + 资源文件
+- 静态库:Header + `.a` + 签名 + 资源文件
+
+
+
+继续做个实验,验证下 Framework 的结构(上面做了静态库),所以我们可以沿用上面的成果,将静态库包装成 Framework
+
+第一步,新建一个文件夹 `Framworks`,下面创建一个 `Person.Framework` 文件夹,把之前得到的静态库 `Person` 移动到该目录下。并把 `main.m` 移动过去。
+
+
+
+第二步,因为 Framework 的结构里有 Header 信息,所以创建 `Headers` 文件夹,把 `Person.h` 文件放进去。
+
+第三步,根据 `main.m` 和 framework 信息,编译成 `main.o`
+
+```shell
+clang -x objective-c \
+ -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -I./Frameworks/Person.framework/Headers \
+ -c main.m -o main.o
+```
+
+第四步,再根据 `main.o` 和 framework 去完成链接。链接三要素:库的头文件、库所在目录、库的名称。只不过在处理 Framework 的时候,参数不一样
+
+```shell
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -F./Frameworks \
+ -framework Person \
+ main.o -o main
+```
+
+说明:main.o 链接 Person.framework 生成 main 可执行文件
+
+- `-F./Frameworks` 在当前目录的子目录 Frameworks 查找需要的库文件
+
+- `-framework Person ` 链接的名称为 `Person.framework` 的动态库或者静态库
+
+ 查找规则:先找 `Person.framework` 的动态库,找不到,再去找 `Person.framework` 的静态库,还找不到,就报错
+
+
+
+
+
+## 动态库
+
+### 直接链接动态库
+
+继续通过小实验来研究动态库的创建与使用
+
+第一步:创建 dylib 文件夹,下面创建 `Person.h` `Person.m` 类。在 dylib 同层目录创建 main.m 文件。代码如下
+
+
+
+第二步:对 main.m 编译成 main.o 文件,指令为
+
+```shell
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -I./dylib \
+ -c main.m -o main.o
+```
+
+第三步:到 dylib 文件夹下,对 Person 编译为 Person.o 文件,指令为
+
+```shell
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -c Person.m -o Person.o
+```
+
+第四步:将 Person.o 编译为动态库,指令为
+
+```shell
+clang -dynamiclib \
+ -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ Person.o -o LibPerson.dylib
+```
+
+第五步,将 main.o 和 LibPerson.dylib 链接,成为 main 可执行文件
+
+```shell
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -L./dylib \
+ -lPerson \
+ main.o -o main
+```
+
+每次针对动态库的操作都是这些差不多的指令,就是一些参数的不同,写个 Shell 脚本,命名为 `build.sh`
+
+```shell
+echo "---------------- start --------------"
+
+echo "第一步:先对 main.m 编译为 main.o"
+
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -I./dylib \
+ -c main.m -o main.o
+
+echo "第二步,再对 Person 编译为 Person.o"
+
+pushd ./dylib
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -c Person.m -o Person.o
+
+echo "第三步,将 Person.o 编译为 libPerson.dylib 动态库"
+
+clang -dynamiclib \
+ -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ Person.o -o libPerson.dylib
+
+echo "第四步,将 main.o 和 libPerson.dylib 链接为可执行文件 main"
+
+popd
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -L./dylib \
+ -lPerson \
+ main.o -o main
+
+echo "---------------- Done --------------"
+```
+
+结果如下:
+
+
+
+
+
+第六步:对生成的 main 可执行文件进行调试运行,使用 lldb 指令 `lldb -file 可执行文件`,然后输入 r 进行运行:
+
+
+
+
+
+咦,为什么我用动态库链接后还是无法使用???带着问题研究下
+
+
+
+### 静态库链接成动态库
+
+因为:
+
+- `.o` 文件可以链接成静态库
+- `.o` 文件可以链接成动态库
+
+所以:能不能推导出这样一个结论:静态库也可以链接成动态库。
+
+做个小实验验证看看:
+
+和上面的材料没有差别,区别在于脚本,其中一步是将静态库链接为动态库。
+
+使用链接器 LD 能力,链接静态库为动态库指令如下
+
+```shell
+ld -dylib -arch x86_64 \
+ -macosx_version_min 13.1 \
+ -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -lsystem -framework Foundation \
+ libPerson.a -o libPerson.dylib
+```
+
+`build.sh` 完整脚本如下
+
+```shell
+echo "---------------- start --------------"
+
+echo "第一步:先对 main.m 编译为 main.o"
+
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -I./dylib \
+ -c main.m -o main.o
+
+echo "第二步,再对 Person 编译为 Person.o"
+
+pushd ./dylib
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -c Person.m -o Person.o
+
+echo "第三步,对 Person.o 编译为 LibPerson.a 静态态库"
+
+libtool -static -arch_only x86_64 Person.o -o libPerson.a
+
+echo "第四步,LD 链接器将 libPerson.a 链接为 libPerson.dylib 动态库"
+
+ld -dylib -arch x86_64 \
+ -macosx_version_min 13.1 \
+ -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -lsystem -framework Foundation \
+ libPerson.a -o libPerson.dylib
+
+echo "第五步,将 main.o 和 libPerson.dylib 链接为可执行文件 main"
+
+popd
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -L./dylib \
+ -lPerson \
+ main.o -o main
+
+echo "---------------- Done --------------"
+```
+
+执行完脚本,又出现了奇怪的现象:
+
+
+
+
+
+发现,在利用动态库链接成可执行文件时报错了 `undefined symbols for **`,然后利用 `objdump --macho --exports-trie libPerson.dylib` 查看动态库的导出符号,居然是空。为什么?
+
+链接器在链接阶段,默认使用 `-noall_load` 参数。共4个参数:
+
+- `-noall_load` :默认是 `-noall_load`,顾名思义就是不会所有符号的加载,而是链接器链接一个静态库之前去扫描静态库文件,找到需要的代码再进行链接
+- `-all_load`:链接所有符号,不管代码有没有使用
+- `-force_load`: 可以指定要载入所有方法的库,后面必须跟一个只想静态库的路径
+- `-ObjC`:告诉链接器把库中定义的 Objective-C 类和 Category 都加载进来,这样编译之后的可执行文件会变大(因为加载了其他的 OC 代码进来)。由于 OC语言符号链接的基本单位是类,静态库链接时首先会链接本类,而 Category 是运行时才会被加载的,因此会被静态链接器直接忽略掉,通过 `-ObjC` 命令是告知链接器链接所有的 OC 代码
+
+知道具体原因那就好办了,修改指令
+
+```shell
+ld -dylib -arch x86_64 \
+ -macosx_version_min 13.1 \
+ -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -lsystem -framework Foundation \
+ -all_load \
+ libPerson.a -o libPerson.dylib
+```
+
+再次运行 build 脚本,然后对可执行文件执行,还是报错 😂
+
+
+
+
+
+通过上面的实验可以得出结论:
+
+- 静态库是 `.o` 文件的合集
+- 动态库是 `.o` 文件链接后的产物
+- 静态库可以链接成动态库
+- 动态库是最终链接产物。动态库比静态库多走一次链接的过程
+
+
+
+### 动态库 Library not loaded?
+
+为什么动态库链接后的可执行文件运行,会报 `Library not loaded: 'libPerson.dylib'` 错误?
+
+不得不聊聊动态库加载原理
+
+
+
+也就是说当 dyld 加载一个可执行文件(main) 的时候,在 Mach-O 中有一个名字叫 `LC_LOAD_DYLIB` 的 Load Command,里面保存了所使用到的动态库的路径。可执行文件的动态库就是靠 dyld 根据动态库路径进行加载的。
+
+用 MachOView 打开另一个 App 看看
+
+
+
+
+
+对于我们自己链接的可执行文件 main 进行查看,利用 `otool -l main | grep 'DYLIB' -A 5` 指令
+
+
+
+可以发现:
+
+- name 好像就是动态库的路径
+- 链接的其他几个动态库的路径都没问题,就是 LibPerson.dylib 路径有问题。
+
+
+
+如何解决?
+
+需要在编译链接生成动态库的时候,有个东西保存动态库路径,这个就是 Mach-O 文件中的另一个 Load Command,即 `LC_ID_DYLIB`。
+
+
+
+#### 方式一:通过 `install_name_tool` 指令
+
+
+
+通过改变动态库 name 来修改动态库的路径。具体指令为 `install_name_tool -id /Users/unix_kernel/Desktop/LDAndFramework/StaticLibraryLinkedIntoDynamicLibrary/dylib/libPerson.dylib libPerson.dylib`
+
+修改动态库的 name 之后,再次 `otool -l libPerson.dylib | grep 'DYLIB' -A 5` 查看路径信息
+
+
+
+动态库有了正确的 name 后,再重新链接生成可执行文件。可执行文件可以正确运行,查看所以来的动态库路径,均正确加载。
+
+
+
+
+
+上面的方案有点缺点,因为路径是绝对路径,因此没办法迁移。
+
+
+
+#### 方式二:`rpath`
+
+`rpath`,Runpath search paths,dyld 搜索路径。运行时,`@rpath` 指示 dyld 按顺序搜索路径列表,以找到动态库。
+
+`@rpath` 保存一个或多个路径的变量。
+
+
+
+前提说明:模拟下 App 真实环境。创建一个文件夹 `Frameworks`,内部继续创建 `Person.framework` 文件夹,其内部继续创建 `Headers`
+
+文件夹,将 dylib 文件夹下的文件复制过去。结构如下
+
+```shell
+// tree -L 4
+.
+├── Frameworks
+│ └── Person.framework
+│ ├── Headers
+│ │ └── Person.h
+│ ├── Person.h
+│ ├── Person.m
+├── build.sh
+├── dylib
+│ ├── Person.h
+│ ├── Person.m
+│ ├── Person.o
+│ ├── libPerson.a
+│ └── libPerson.dylib
+├── main.m
+```
+
+第一步,在 ` Frameworks/Person.framework` 下面执行命令,将 `Person.m` 编译为 `Person.o`
+
+```shell
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -c Person.m -o Person.o
+```
+
+第二步,将 `Person.o` 链接为动态库
+
+```shell
+clang -dynamiclib \
+ -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ Person.o -o Person
+```
+
+第三步,给动态库利用 `install_name_tool` 修改 id,id 指定 `@rpath` 信息
+
+```shell
+install_name_tool -id @rpath/Frameworks/Person.framework/Person /Users/unix_kernel/Desktop/LDAndFramework/StaticLibraryLinkedIntoDynamicLibrary/Frameworks/Person.framework/Person
+```
+
+第四步,利用 `otool` 查看动态库的 name 是否修改好了 `@rpath` 信息
+
+```
+otool -l Person | grep 'ID' -A 5
+```
+
+第五步,回到根目录,将 `main.m` 编译为 `main.o`
+
+```shell
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -I./Frameworks/Person.framework/Headers \
+ -c main.m -o main.o
+```
+
+第六步,将 `main.o` 和 `Person.framework` 链接为可执行文件
+
+```shell
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -F./Frameworks \
+ -framework Person \
+ main.o -o main
+```
+
+此时的可执行文件虽然链接成功了,但是可执行文件需要用到动态库的功能,直接运行会报错 `Library not loaded: '@rpath/Frameworks/Person.framework/Person'`。所以需要给可执行文件添加 `rpath` 信息
+
+第七步,给可执行文件 `main` 添加 `rpath` 信息
+
+```shell
+install_name_tool -add_rpath /Users/unix_kernel/Desktop/LDAndFramework/StaticLibraryLinkedIntoDynamicLibrary main
+```
+
+添加后验证是否添加成功,指令 `otool -l main | grep 'RPATH' -A 5`
+
+第八步,链接成功后 `main` 可执行文件即可执行
+
+
+
+下面是上面全部步骤的截图说明。
+
+
+
+
+
+
+
+给动态库添加 `rpath` 信息。
+
+核心:谁链接动态库,`rpath` 谁来提供,比如一个 Person.framework 路径为: `/usr/meiying/desktop/DynamicExplore/Person.framework`
+
+- 可执行文件中 `Load Command ` 中存在 `LC_RPATH` ,值为 `/usr/meiying/desktop/DynamicExplore/Person.framework`
+- 动态库 Mach-O 中也存在 值为 `@rpath/Frameworks/Person.framework/Person`
+
+反思:上面的方案还是有缺点的,应为可执行文件提供的 `rpath` 还是一个绝对路径。
+
+
+
+#### 方式三:@execute_path、@loader_path
+
+`@executable_path`:表示可执⾏程序所在的⽬录,解析为可执⾏⽂件的绝对路径。
+
+`@loader_path`:表示被加载的`Mach-O` 所在的⽬录,每次加载时,都可能被设置为不同的路径,由上层指定
+
+可以将可执行文件的 rpath 修改为灵活的,而不是写死的路径,指令为
+
+```shell
+install_name_tool -rpath /Users/unix_kernel/Desktop/LDAndFramework/StaticLibraryLinkedIntoDynamicLibrary @executable_path main
+```
+
+
+
+这个可不是花里胡哨的烧操作,Cocoapods 也是这么干的
+
+```
+LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
+```
+
+
+
+
+
+
+
+注意:`@loader_path` 还是比较抽象的,举一个实际场景例子来看看
+
+假设背景是:
+
+- 动态库 Cat 具有 `[cat sleep]` 能力
+- 动态库 Person 具有 `[person sayHi]` 能力,动态库 Person 使用了动态库 Cat
+- 可执行文件,导入了动态库 Person
+
+这样一个场景,代码模拟下,文件目录如下
+
+
+
+1. 在 Cat.framework 文件夹下运行 build.sh
+2. 在 Person.framework 文件夹下运行 build.sh
+3. 在 main.m 根目录下运行 build.sh
+
+得到 main 可执行文件,运行报错。
+
+```shell
+lldb -file main
+(lldb) target create "main"
+Current executable set to '/Users/unix_kernel/Desktop/LDAndFramework/DynamicLibraryUseDynamicLibrary/main' (x86_64).
+(lldb) r
+Process 54157 launched: '/Users/unix_kernel/Desktop/LDAndFramework/DynamicLibraryUseDynamicLibrary/main' (x86_64)
+dyld[54157]: Library not loaded: 'Person'
+ Referenced from: '/Users/unix_kernel/Desktop/LDAndFramework/DynamicLibraryUseDynamicLibrary/main'
+ Reason: tried: 'Person' (no such file), '/usr/local/lib/Person' (no such file), '/usr/lib/Person' (no such file), '/Users/unix_kernel/Desktop/LDAndFramework/DynamicLibraryUseDynamicLibrary/Person' (no such file), '/usr/local/lib/Person' (no such file), '/usr/lib/Person' (no such file)
+Process 54157 stopped
+* thread #1, stop reason = signal SIGABRT
+ frame #0: 0x000000010005698e dyld`__abort_with_payload + 10
+dyld`:
+-> 0x10005698e <+10>: jae 0x100056998 ; <+20>
+ 0x100056990 <+12>: movq %rax, %rdi
+ 0x100056993 <+15>: jmp 0x100013150 ; cerror_nocancel
+ 0x100056998 <+20>: retq
+Target 0: (main) stopped.
+```
+
+得到新的命题:**可执行文件中引入动态库 A,动态库的功能实现依赖动态库 B,链接器该如何链接呢?**
+
+
+
+第一种尝试:
+
+对 Cat.framework 下的 build.sh 修改脚本,指定 `@rpath` 信息, `-Xlinker -install_name -Xlinker @rpath/Cat.framework/Cat`
+
+```shell
+echo "1: 编译 Cat.m 为 Cat.o 可执行文件"
+
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -c Cat.m -o Cat.o
+
+echo "2: 链接 Cat.o 为 Cat 动态库"
+
+clang -dynamiclib \
+ -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -Xlinker -install_name -Xlinker @rpath/Cat.framework/Cat \
+ Cat.o -o Cat
+
+echo "3: 输出动态库的 name(路径)信息"
+otool -l Cat | grep 'ID' -A 5
+```
+
+对 Person.framework 下的 build.sh 修改脚本,指定 `@rpath` 信息,` -Xlinker -install_name -Xlinker @rpath/Person.framework/Person`
+
+```shell
+echo "1:编译 Person.m 为 Person.o 可执行文件"
+
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -I./Headers \
+ -I./Frameworks/Cat.framework/Headers \
+ -c Person.m -o Person.o
+
+echo "2: 链接 Person.o 为 Person 动态库"
+
+clang -dynamiclib \
+ -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -Xlinker -install_name -Xlinker @rpath/Person.framework/Person \
+ -F./Frameworks/ \
+ -framework Cat \
+ Person.o -o Person
+
+
+echo "3: 输出动态库的 dylib 信息"
+otool -l Person | grep 'DYLIB' -A 5
+
+echo "-----------"
+echo "4: 输出动态库的 name 信息"
+otool -l Person | grep 'ID' -A 5
+
+```
+
+对可执行文件 main 根目录,指定 `@executable_path` 信息,`-Xlinker -rpath -Xlinker @executable_path/Frameworks`
+
+```shell
+echo "---------------- start --------------"
+
+echo "第一步:先对 main.m 编译为 main.o"
+
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -I./Frameworks/Person.framework/Headers \
+ -c main.m -o main.o
+
+echo "第二步,将 main.o 和 Person.dylib 链接为可执行文件 main"
+
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -Xlinker -rpath -Xlinker @executable_path/Frameworks \
+ -F./Frameworks \
+ -framework Person \
+ main.o -o main
+
+echo "第三步,输出信息"
+otool -l main | grep 'RPATH' -A 3
+otool -l main | grep 'DYLIB' -A 3
+
+echo "---------------- Done --------------"
+```
+
+运行报错如下:
+
+
+
+为什么还错误了?都已经给可执行文件添加了 `@executable_path`,给2个动态库都添加了 `@rpath`,怎么办?
+
+思路:因为报错是说动态库 Person 找不到动态库 Cat,那是不是聚焦下 Person,在 Person 的链接指令中,给 Person 添加 `@rpath` 就可以了?
+
+动手实践下,修改 Person 动态库的 build.sh 脚本
+
+```shell
+echo "1:编译 Person.m 为 Person.o 可执行文件"
+
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -I./Headers \
+ -I./Frameworks/Cat.framework/Headers \
+ -c Person.m -o Person.o
+
+echo "2: 链接 Person.o 为 Person 动态库"
+
+clang -dynamiclib \
+ -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -Xlinker -install_name -Xlinker @rpath/Person.framework/Person \
+ -Xlinker -rpath -Xlinker @executable_path/Frameworks/Person.framework/Frameworks \
+ -F./Frameworks/ \
+ -framework Cat \
+ Person.o -o Person
+
+
+echo "3: 输出动态库的 dylib 信息"
+otool -l Person | grep 'DYLIB' -A 5
+
+echo "-----------"
+echo "4: 输出动态库的 name 信息"
+otool -l Person | grep 'ID' -A 5
+
+```
+
+在 Person.framework运行下 build.sh,然后在根目录下运行 build.sh,得到新的可执行文件,然后可以成功运行
+
+
+
+反思:可执行文件依赖动态库 A,动态库 A 依赖动态库 B,上面的配置很繁琐:
+
+- 可执行文件提供 `rpath`,指令为: `-Xlinker -rpath -Xlinker @executable_path/Frameworks`
+- 动态库 A 指定 name 为 `-Xlinker -install_name -Xlinker @rpath/Person.framework/Person`,同时又因为依赖了动态库 B,所以同时又要提供 `rpath`,指令为 `-Xlinker -rpath -Xlinker @executable_path/Frameworks/Person.framework/Frameworks `
+- 动态库 B 指定 name,指令为 `-Xlinker -install_name -Xlinker @rpath/Cat.framework/Cat`
+
+好繁琐啊,有没有简化的方法。此时 **`@loader_path`** 呼之欲出了。
+
+在当前场景下,Person 动态库的指令可以简化下。`-Xlinker -rpath -Xlinker @executable_path/Frameworks/Person.framework/Frameworks ` 可以替换为 `-Xlinker -rpath -Xlinker @loader_path/Frameworks `
+
+修改脚本后,分别在 Person.framework 和 main 可执行文件根目录下执行 build.sh 脚本,然后验证可执行文件是否可以正确运行,加载所需动态库
+
+注:为了方便看清楚脚本执行情况和可执行文件执行结果,这次运行注释了 otool 的打印脚本。
+
+
+
+
+
+`loader_path` 是标准解决方案。随便打开 AFNetworking 工程看看
+
+
+
+
+
+
+
+链接器可真强大啊,同时 Mach-O 开的口子也多,也就是可以随意修改已经编译好的可执行文件的 `rpath` ,自己也可以创建一个同名的动态库(name)把原有的动态库替换了,这也是为什么 MacOS 那些破解软件的工作原理。(当然,这些也要结合逆向技术)
+
+上述的操作,其实本质就是修改 Mach-O 文件的 Load Command,按照 Apple 的机制,Mach-O 修改后,必须重新签名才可以运行使用破解软件时,总是要求我们重新签名,背后的命令如下:
+
+```shell
+sudo codesign --force --deep --sign - (应用路径)
+```
+
+
+
+### 动态库如何导出所引用的动态库的符号
+
+- 主工程 -> Person 动态库
+- Person 动态库 -> Cat 动态库
+
+那么主工程可以直接调用 Cat 动态库的能力吗?
+
+正常写代码肯定可以,但是从链接器角度分析下,如何实现
+
+调用的本质就是符号的发现。也就是 Cat 的符号有没有导出?可执行文件 mian 使用的能力就是动态库导出后,自己导入的。
+
+因为 main 引入了 `import ` ,查看下 Person 动态库的导出符号,使用指令 `objdump --macho --exports-trie Person`
+
+进入 Cat.framework 也查看下 Cat 动态库的导出符号,使用指令 `objdump --macho --exports-trie Cat`
+
+发现 Person 没有导出 Cat 的符号。那在可执行文件中调用不了 Cat 的能力了。
+
+
+
+
+
+怎么办呢?链接器 LD 已经是很成熟的东西了,对于处理动态库依赖了动态库,且需要将被依赖动态库的符号导出,这样的需求早已满足了。具体是什么参数?终端输入 `man ld` 查看下指令
+
+
+
+其中,我们需要用的是 `-reexport_framework` 这个参数。指令为 ` -Xlinker -reexport_framework -Xlinker Cat`
+
+对此,修改 Person.framework 的 build.sh 脚本
+
+```
+echo "1:编译 Person.m 为 Person.o 可执行文件"
+
+clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -I./Headers \
+ -I./Frameworks/Cat.framework/Headers \
+ -c Person.m -o Person.o
+
+echo "2: 链接 Person.o 为 Person 动态库"
+
+clang -dynamiclib \
+ -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -Xlinker -install_name -Xlinker @rpath/Person.framework/Person \
+ -Xlinker -rpath -Xlinker @executable_path/Frameworks/Person.framework/Frameworks \
+ -Xlinker -reexport_framework -Xlinker Cat \
+ -F./Frameworks/ \
+ -framework Cat \
+ Person.o -o Person
+
+
+echo "3: 输出动态库的 dylib 信息"
+otool -l Person | grep 'DYLIB' -A 5
+
+echo "-----------"
+echo "4: 输出动态库的 name 信息"
+otool -l Person | grep 'ID' -A 5
+
+```
+
+执行脚本输出如下:
+
+
+
+结构如下:
+
+
+
+理论上来讲,Person.framework 把 Cat.framework 导出了,实现方式是通过给 Mach-O 的一个叫做 `LC_REEXPORT_DYLIB` 的 Load Command。也就是可执行文件,通过 Person.framework 的 `LC_REEXPORT_DYLIB` load Command 可以实现访问 Cat.framework 的符号。
+
+完整验证下,还需要做2件事情:
+
+- 修改 main.m 的代码,因为要访问 Cat.framework 的能力,测试能否正常运行
+
+ ```objective-c
+ #import
+ #import "Person.h"
+ #import
+
+ int main(int argc, char * argv[]) {
+ Person *p = [[Person alloc] init];
+ [p sayHi];
+ Cat *cat = [[Cat alloc] init];
+ [cat sleep];
+ return 0;
+ }
+ ```
+
+- 修改 main.m 的 build.sh 脚本,因为 Person.framework 已经暴露了 Cat.framework 的能力。LD 链接指令需要加 Cat.framework 头文件的参数
+
+ ```shell
+ echo "---------------- start --------------"
+
+ echo "第一步:先对 main.m 编译为 main.o"
+
+ clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -I./Frameworks/Person.framework/Headers \
+ -I./Frameworks/Person.framework/Frameworks/Cat.framework/Headers \
+ -c main.m -o main.o
+
+ echo "第二步,将 main.o 和 Person.dylib 链接为可执行文件 main"
+
+ clang -target x86_64-apple-macos13.1 \
+ -fobjc-arc \
+ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
+ -Xlinker -rpath -Xlinker @executable_path/Frameworks \
+ -F./Frameworks \
+ -framework Person \
+ main.o -o main
+
+ # echo "第三步,输出信息"
+ # otool -l main | grep 'RPATH' -A 3
+ # otool -l main | grep 'DYLIB' -A 3
+
+ echo "---------------- Done --------------"
+ ```
+
+修改完从里到外一次性执行 build.sh,得到 main 可执行文件。一切顺利,输出如下:
+
+
+
+
+
+
+
+## xcframework
+
+### 诞生背景
+
+XCFramework:是苹果官⽅推荐的、⽀持的,可以更⽅便的表示⼀个多平台和架构的分发⼆进制库的格式。专⻔在 2019 年提出的framework 的另⼀种先进格式。
+
+需要 Xcode11 以上⽀持。是为了更好的⽀持 Mac Catalyst 机制和 ARM 芯⽚的 macOS。
+
+
+
+### 优势
+
+胖二进制:Fat Binary,通用二进制格式(Universal Binary)。通用二进制文件实际上就是将支持不同架构的二进制文件打包成一个文件,系统在加载运行时,会根据通用二进制文件中提供的架构,选择和当前系统匹配的二进制文件。
+
+动态库是可以合并的。前提是不同的 CPU 架构。合并之后还是多个不同的动态库,只不过 mach-header 是挨在一起的,所有的库文件也是挨着的。可以理解成是“压缩”。
+
+lipo 指令的缺点:不能合并相同架构。对不同的库处理后,还要处理 `dSYM` 文件,如果库开启了 Bitcode,还会生成 `BCSymbolMaps`
+
+文件。所以使用 lipo 处理动态库的合并、拆分,都需要管理 `dSYM`、`BCSymbolMaps`、`库文件`,较为繁琐。
+
+基于此,Apple 在 2019 诞生了 `xcframework` 技术。
+
+和传统的 framework 相⽐:
+
+- 可以⽤单个`.xcframework` ⽂件提供多个平台的分发⼆进制⽂件
+
+- 与 `Fat Header` 相⽐,可以按照平台划分,可以包含相同架构的不同平台的⽂件
+
+- 在使⽤时,不需要再通过脚本去剥离不需要的架构体系(比如默认包含3种架构,armv7、arm64、x86_64 上架前为了包大小,还会用 lipo 指令剔除不需要的架构)
+
+
+
+### 如何制作 xcframework
+
+第一步:先创建一个动态库 `pod lib create Person`。里面就一个 Person 类, 包含2个方法。
+
+第二步:利用 `xcodebuild` 指令打包。`xcodebuild` 指令执行的是打包当前的工程目录,会读取 `-workspace` 参数指定的项目配置,然后根据指定的 `-scheme` 和 `-configuration` 模式去打包工程。
+
+先打出模拟器的包,指令如下:
+
+```shell
+xcodebuild -workspace Person.xcworkspace \
+ -scheme 'Person-Example' \
+ -configuration Release \
+ -destination 'generic/platform=iOS Simulator' \
+ -archivePath '../archives/Person.framework-iphonesimulator.xcarchive' \
+ SKIP_INSTALL=NO \
+ archive
+```
+
+再打出真机的包,指令如下
+
+```shell
+xcodebuild -workspace Person.xcworkspace \
+ -scheme 'Person-Example' \
+ -configuration Release \
+ -destination 'generic/platform=iOS' \
+ -archivePath '../archives/Person.framework-iphoneos.xcarchive' \
+ SKIP_INSTALL=NO \
+ archive
+```
+
+打包成功的输出如下:
+
+
+
+实体目录如下:
+
+
+
+注意:我们打败归档后生成的符号表只有 `.dSYM` 文件,没有 `BCSymbolMaps` 文件,是因为工程没有开启 BitCode,当开启 BitCode 后的,打包会生成 `BCSymbolMaps` 文件,作用也是用于 Crash 后解析堆栈用的。
+
+因为 `.dSYM` 文件是默认生成的,但是 `Bitcode` 需要分别开启(Pod 和 Demo 工程)。开启 Bitcode 后重新打包,看看生成的产物里有没有 `BCSymbolMaps` 和相关的 `.bcsymbolmap` 文件
+
+
+
+
+
+第三步:利用 `xcodebuild` 打包成 xcframework
+
+```shell
+xcodebuild -create-xcframework \
+ -framework 'archives/Person.framework-iphoneos.xcarchive/Products/Library/Frameworks/Person.framework' \
+ -framework 'archives/Person.framework-iphonesimulator.xcarchive/Products/Library/Frameworks/Person.framework' \
+ -output './../xcframework/Person.xcframework'
+```
+
+结果如下:
+
+
+
+可以看到打包后的 xcframework 自动处理了文件夹。但是缺点是我们提供的 framework,别人使用可能会 crash,所以为了一些使用场景需要在 xcframework 中提供 `.dSYM` 文件或者 `.bcsymbolmap` 文件。
+
+第四步:加入 `.dSYM` 和 `.BCSymbolMaps` 文件,重新生成 xcframework,其参数为 `-debug-symbols`,后面路径必须是绝对路径。
+
+```shell
+xcodebuild -create-xcframework \
+ -framework 'archives/Person.framework-iphoneos.xcarchive/Products/Library/Frameworks/Person.framework' \
+ -debug-symbols '/Users/unix_kernel/Desktop/LDAndFramework/MultipleArchMerge/Person/archives/Person.framework-iphoneos.xcarchive/BCSymbolMaps/3C61A3F4-4398-322F-8AC9-F078B196C381.bcsymbolmap' \
+ -debug-symbols '/Users/unix_kernel/Desktop/LDAndFramework/MultipleArchMerge/Person/archives/Person.framework-iphoneos.xcarchive/BCSymbolMaps/B34138DB-2F8F-372C-93D3-7ADDDFC7BDA1.bcsymbolmap' \
+ -debug-symbols '/Users/unix_kernel/Desktop/LDAndFramework/MultipleArchMerge/Person/archives/Person.framework-iphoneos.xcarchive/dSYMs/Person.framework.dSYM' \
+ -framework 'archives/Person.framework-iphonesimulator.xcarchive/Products/Library/Frameworks/Person.framework' \
+ -debug-symbols '/Users/unix_kernel/Desktop/LDAndFramework/MultipleArchMerge/Person/archives/Person.framework-iphonesimulator.xcarchive/dSYMs/Person.framework.dSYM' \
+ -output './../xcfrmaework/Person.xcframework'
+```
+
+结果如下
+
+
+
+可以看到 xcframework 里面不只有不同的动态库,还携带了对应的 `.dSYM` 和 `bcsymbolmap` 文件,用于堆栈、符号还原。
+
+
+
+第五步:制作好的 xcframework 接入使用下。
+
+- 新建一个叫做 `XCFrameworkUsageDemo` 的 Xcode iOS App 工程
+
+- 导入创建的 `Person.xcframework`
+
+- import 头文件并使用
+
+
+
+- 编译运行,查看 products 下面的产物,因为选择的是模拟器运行,所以验证 `Person.framework` 里面的动态库文件大小,是否和 `Person.xcframework` 里面模拟器目录下 Person 动态库的大小一致
+
+
+
+可以看到我们打包的 `Person.xcframework` 可以正常使用,除此之外,`Person.xcframework` 包含了模拟器和真机的动态库文件和对应的 `.dSYM` 和 `.bcsymbolmap` 文件,当导入到项目中的时候,Xcode 会根据当前编译的架构,自动从里面选择合适的架构文件。
+
+好处有3:
+
+- 不需要处理头文件
+- 我们不需要关心上线前处理,重复架构(lipo 剔除)
+- 调试符号也方便的给到
+
+注意:该部分代码,在 `LDAndFramework/XCFramework` 目录下。
+
+
+
+## Weak Import
+
+先做个小实验
+
+第一步:创建一个 iOS App 工程,然后把上面生成的 `Person.framework` 拖到工程根目录下
+
+第二步:不引入动态库,而是通过 xcconfig 文件,告诉链接器,关于动态库的三要素:头文件位置、动态库名称。
+
+第三步:编译运行。
+
+
+
+结论:编译正常,但是运行会报错 `Library not loaded: @rpath/Person.framework/Person`
+
+第一种解决方案是给 xcconfig 添加 rpath 的具体路径。
+
+
+
+第二种解决方案是将库声明为“弱引用”。输入 `man ld` 查看具体的参数和说明:
+
+```shell
+ -weak_framework name[,suffix]
+ This is the same as the -framework name[,suffix] but forces the framework and all references to it to be marked as weak imports. Note: due to a
+ clang optimizations, if functions are not marked weak, the compiler will optimize out any checks if the function address is NULL.
+```
+
+修改 xcconfig 文件为
+
+```shell
+// 引用一个动态库,3要素:头文件、动态库名称、动态库路径
+// 知道了链接的原理之后,就知道可以不用把动态库托到项目中去,指定了3要素就可以链接了。
+// 1. -I:头文件信息
+HEADER_SEARCH_PATHS = $(inherited) ${SRCROOT}/Person.framework/Headers
+LD_RUNPATH_SEARCH_PATHS = $(inherited)
+// 2. -F:framework
+FRAMEWORK_SEARCH_PATHS = $(inherited) ${SRCROOT}
+// 3. -weak_framework: 允许该库在运行时消失
+OTHER_LDFLAGS = $(inherited) -Xlinker -weak_framework -Xlinker "Person"
+```
+
+修改后编译运行,发现没有报 `Library not loaded: ` 这样的错。
+
+我们对添加了 `_weak-framework` 这个链接器参数的可执行文件查看下,指令为 `otool -l WeakImportDemo`
+
+
+
+查看 Mach-O 发现,从 `LC_LOAD_DYLIB` 变为 `LC_LOAD_WEAK_DYLIB`
+
+
+
+通常,当链接一个库时,链接器会尝试解析所有从该框架中引用的符号。如果某个符号在库不存在或者没有导出,链接器会报错,因为这是一个强链接(strong linking)的要求。
+
+使用 `-weak_framework` 选项,可以告诉链接器,即使框架中缺少某些符号,也不要报错,而是允许链接继续进行。
+
+这样,应用程序在运行时可以优雅地处理缺少的符号,例如,某个特性可能因为缺少实现而不可用,但应用程序的其他部分仍然可以正常工作。
+
+当一个动态库,在运行时,不能保证
+
+
+
+## 静态库符号冲突原因及其解法
+
+探索下静态库符号冲突的情况下,怎么解决?
+
+来个简单的小实验。
+
+第一步:准备2个 AFNetworking 的静态库,符号一模一样,但是静态库名称不同。
+
+第二步:创建 Demo 工程,将静态库放到根目录下。
+
+第三步:创建 xcconfig 文件。配置参数用于配制一些编译、链接信息。
+
+```json
+//// -I
+HEADER_SEARCH_PATHS = $(inherited) "${SRCROOT}/AFNetworking" "$(SRCROOT)/AFNetworking2"
+//// -L
+LIBRARY_SEARCH_PATHS = $(inherited) "${SRCROOT}/AFNetworking" "$(SRCROOT)/AFNetworking2"
+//// -l
+OTHER_LDFLAGS = $(inherited) -l"AFNetworking" -l"AFNetworking2"
+```
+
+第四步:尝试编译,编译成功。
+
+
+
+QA:为什么链接2个同名同符号的静态库,编译不报错?
+
+会有二级命名空间吗?不是的。静态库本质就是一堆 `.o` 文件,所有的 `.o` 最后都会和主工程的可执行文件进行合并,所以不存在二级命名空间的问题。
+
+核心原因是因为,链接器针对静态库,在 deac code strip 专门为静态库,设置为 `-noall_load`,意思是:完全不加载、直接去优化
+
+可以验证下,在链接的时候修改链接参数为 `-all_load` 就会报错
+
+
+
+
+
+如何解决??
+
+LD 提供了 ` -load_hidden` 参数。
+
+```shell
+ -load_hidden path_to_archive
+ Uses specified static library as usual, but treats all global symbols from the static library to as if they are visibility hidden.
+ Useful when building a dynamic library that uses a static library but does not want to export anything from that static library.
+```
+
+修改 LD 链接参数
+
+```shell
+OTHER_LDFLAGS = $(inherited) -l"AFNetworking" -l"AFNetworking2" -Xlinker -force_load -Xlinker "${SRCROOT}/AFNetworking/libAFNetworking.a" -Xlinker -load_hidden -Xlinker "${SRCROOT}/AFNetworking2/libAFNetworking2.a"
+```
+
+
+
+具体代码见: `LDAndFramework/StaticLibConflictsSolution/StaticLibConflictsDemo`
+
+
+
+## 动动
+
+第一步,创建一个名字叫 `NetworkManager` 的 Framework,勾选测试。里面就添加一个 `NetworkDetector` 类,有1个类方法 `+sharedManager`
+
+第二步,给动态库添加 AFNetworking 依赖。
+
+- 项目根目录下执行 `pod init` ,初始化工程
+- 添加 `pod 'AFNetworking'`
+
+第三步:目的是为了研究 App -> NetworkManager 动态库 -> AFNetworking 动态库的情况,所以 App 可以用测试工程代替,测试工程就是一个可执行文件,类似一个 App。
+
+发现编译通过,运行报错。
+
+
+
+为什么?原因
+
+因为 `Pods-NetworkManager.debug.xcconfig` 里面的配置信息告诉链接器关于编译的3要素:头文件位置、动态库名称、rpath 信息。所以可以链接成功,
+
+运行报错是因为 dyld 找不到 AFNetworking 动态库所在的位置。
+
+使用的 AFNetworking 动态库,所以`NetworkManager.Framework` 的 Load Command `LC_LOAD_DYLIB` 中的 name 就告诉外部,AFNetworking 将会在 `@rpath/AFNetworking.framework/AFNetworking` 下面查找。
+
+
+
+遵循原则是谁链接库,谁就提供库所需要的 rpath 信息。所以 `NetworkManager.Framework` 提供 rpath 信息。xcconfig 文件的 `LD_RUNPATH_SEARCH_PATHS` 是 Xcode 项目中的一个设置,它在编译时告诉链接器在生成的可执行文件的运行时路径(rpath)中包含特定的目录( `install_name_tool -add_rpath` 是在二进制文件生成后对其进行修改)
+
+```shell
+LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' '@executable_path/../../Frameworks'
+```
+
+dyld 在运行起来后,会根据 `LD_RUNPATH_SEARCH_PATHS` 提供的 rpath 信息,和 AFNetworking 的 `name` 拼接去查找。但我们面前的例子,路径下没有 AFNetworking。
+
+如何解决?
+
+方式一:low 一点,直接给测试工程也 `pod AFNetworking`。这是在探究原理,简单解决问题可以这么做。
+
+
+
+方式二:不是找不到 AFNetworking 吗?因为 xcconfig 提供的路径找不到,那直接给 `LD_RUNPATH_SEARCH_PATHS` 配置一个可以找到的地方。然后运行成功。这只是为了研究定位问题后,简单解决问题的方案。
+
+
+
+方式三:观察标准做法,比如一个 App 使用了 AFNetworking,这个 case Cocoapods 是怎么处理的?
+
+
+
+是通过 shell 脚本来处理的。该脚本肯定是 Cocoapods 生成的。属于工程化范畴。核心代码如下
+
+
+
+具体怎么做呢?编写 shell 脚本,编译 Person 动态库、AFNetworking 动态库,然后将产物复制到 Frameworks 文件夹下。
+
+Tips
+
+> 往一个 Framework 里通过 Cocoapods 导入一个库,并不会真的导入,而是生成链接器链接所需的参数,并不会把依赖的库文件放到自身的 Framework 中。
+
+
+
+来个有趣的操作:NetworkManager.Framework 如何使用主工程的功能?也就是反向依赖
+
+**功能的本质就是符号,dyld 在链接的时候,会把所有的导出符号放到一起,只要运行的时候找到所有的符号,就可以动态链接。**
+
+ 那怎么做呢?
+
+第一步:在 App(我们的实验中就是测试工程)创建 NetworkObject 类
+
+第二步:在 framework 的 HEADER_SEARCH_PATHS 中增加 App 的头文件查找路径
+
+第三步:framework 中增加实现代码,使用 App 中的符号
+
+
+
+
+
+发现编译报错?符号找不到
+
+- 链接的时候会去找符号的地址
+- 但 App 和动态库的符号原则是,链接成功,App 运行起来,动态库自然可以访问到 App 中的符号
+
+所以,如何链接成功?如何处理这个未定义的符号?
+
+LD 链接器支持符号的处理。
+
+方法一:修改动态库 xcconfig 添加未定义的符号为动态查找。但风险较大,所有未定义的都不会报错误伤较大。
+
+```shell
+OTHER_LDFLAGS = $(inherited) -framework "AFNetworking" -Xlinker -undefined -Xlinker dynamic_lookup
+```
+
+方法二:只对特定的未定义的符号采用动态查找
+
+```shell
+OTHER_LDFLAGS = $(inherited) -framework "AFNetworking" -Xlinker -U -Xlinker _OBJC_CLASS_$_NetworkObject
+```
+
+
+
+
+
+## 动静
+
+模拟:App -> NetworkManager 动态库 -> AFNetworking 静态库。将上面的工程中,NetworkManager 中的 podfile `# use_frameworks!` 注释掉,就会以静态库的形式链接 AF。
+
+动态库依赖静态库,则会把静态库中所有代码链接到动态库中。所以工程可以正常编译、链接。
+
+如何在 App 中引入静态库 AFNetworking 中的符号?因为链接后,动态库中已经包含了静态库中的符号,所以只需要让 Xcode 编译通过即可。符号查找无需关心。
+
+打开 NetworkManager 动态库的 Mach-O 查看下符号,可以看到动态库 NetworkManager 中已经包含了静态库 AFNetworking 的符号。
+
+
+
+如何成功编译?告诉链接器 HEADER_SEARCH_PATH 信息即可。
+
+
+
+
+
+思考:动态库链接静态库后,静态库中暴露的导出符号,在动态库中也是导出符号。假设动态库不想把静态库的符号暴露出来,该怎么做?
+
+LD 链接器提供了能力,将静态库的符号不暴露出来。
+
+```shell
+OTHER_LDFLAGS = $(inherited) -ObjC -Xlinker -hidden-l"AFNetworking"
+```
+
+
+
+
+
+## 静静
+
+模拟:App -> NetworkManager 静态库 -> AFNetworking 静态库
+
+- 将上面的工程中,NetworkManager 中的 `Build Settings` -> `Mach-OType` 改为 `Static Library`
+- 将 Podfile 中 `use_frameworks!` 注释掉,就会以静态库的形式链接 AF
+
+之后编译报错
+
+
+
+此时 NetworkManager 静态库中只有自己的符号,没有所依赖的 AFNetworking 中的符号。
+
+App 链接 NetworkManager 静态库,需要告诉链接器:头文件、库名称、库所在位置。
+
+但 NetworkManager 静态库链接 AFNetworking 静态库,没有配置信息告诉 App。所以需要额外配置。App 直接依赖的静态库,静态库所以来的静态库没有对 App 可见。
+
+方法一:Build Settings -> 查找 Other Linker Flags,添加 `-lAFBetworking` 指明链接哪个库;查找 Libarary Search Path,设置库的查找路径 `${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/AFNetworking`
+
+
+
+
+
+方法二:直接给 App install 静态库。
+
+具体代码见:`LDAndFramework/StaticLibUseStaticLib`
+
+
+
+## 静动
+
+模拟:App -> NetworkManager 静态库 -> AFNetworking 动态库。效果等价于, (App + 静态库)+ 动态库。
+
+- Xcode 中将 NetworkManager 项目的 Build Settings 中的 Mach-O Type 设置为 `Static Library`
+- Podfile 中将 `use_frameworks!` 注释打开
+
+编译报错,符号未定义 。
+
+
+
+所以症结所在:就是把动态库的代码也放到 App 里面,App 才可以使用动态库里面的符号。上面代码运行,NetworkManager 静态库调用 AFNetworking 动态库的能力,就相当于 App 直接访问 AFNetworking 动态库的符号一样。
+
+App 没有配置关于 AFNetworking 的链接三要素,所以找不到 AF。
+
+- AutoLink:当在代码中 `import ` 会自动在目标文件的 Mach-O 中 `OTHER_LDFLAGS` 拼接 `-framework` 参数
+- 所以只需要告诉 App framework search path 即可。参考debug.xcconfig 中的 `FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking"`,展开为 `"${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/AFNetworking"`。将其设置到 Build Settings 的 framework search path 中。
+
+修改后编译没问题,运行报错,因为编译后的 AFNetworking 没有拷贝到 App 所需目录下.
+
+
+
+怎么处理?
+
+- 参考其他使用 AFNetworking 的项目,Cocoapods 工程化模版写好了脚本,直接拷贝一份到工程根目录下,重命名为 `handle-frameworks.sh`
+- Xcode Targets 选择 “NetworkManagerTests” -> Build Phases,添加 “Run script”,内容为 `"${SRCROOT}/handle-frameworks.sh"`
+
+运行成功。
+
+
+
+
+
+具体代码见:`LDAndFramework/StaticLibUseDynamicLib`
+
+
+
+## 同时依赖静态库、动态库
+
+```shell
+platform :ios, '14.1'
+
+target 'InstallDyanmicAndStaticFramework' do
+ use_frameworks!
+ $static_framework = ['AFNetworking']
+
+ pre_install do |install|
+ puts install
+ install.pod_targets.each {| pod |
+ if $static_framework.include?(pod.name)
+ def pod.build_type;
+ Pod::BuildType.static_framework # 使用静态库
+ end
+ end
+ }
+ end
+
+ # Pods for InstallDyanmicAndStaticFramework
+ pod 'SDWebImage'
+ pod 'AFNetworking'
+end
+```
+
+代码见:`LDAndFramework/InstallDyanmicAndStaticFramework`
+
+
+
+
+
+## module
+
+### 定义
+
+一个 Module 是机器代码和数据的最小单位,可以独立于其他代码单元进行链接。
+
+通常,Module 是通过编译单个源文件生成的目标文件。例如,当前的 `test.m` 被编译成目标文件 `test.o` 时,当前的目标文件就代表了一个 Module 。
+
+但是,Module 在调用的时候会产生**开销**,比如我们在使用一个静态库的时候。(这个开销是什么接下去会讲)
+
+
+
+### 场景
+
+说人话,平时什么情况下使用的最多?导入库文件的时候,就是 module 的主战场。
+
+当在 App 里导入一个库的时候,发生了什么事情?`.h` 文件也是需要编译的(里面也会承载一些方法信息)。
+
+先谈谈导入方式,导入方式有2种:
+
+- `include`:
+
+ 假设没有开启 module 能力,当使用 `#include <*.h>` 的时候, 头文件 A、B 在 `use.c ` `another-use.c` 中,include 几次就要编译几次。
+
+
+
+ 编译 use.c 就要编译 include 进来的 A、B。编译 another-use.c 同样要编译 A、B。效率低下。
+
+- `import`:与 `include` 相对应,`import ` 语句用于导入模块,而不是简单的文本包含。使用模块可以减少编译时间,因为编译器只需要编译模块的接口而不是整个模块的实现
+
+
+
+`clang -fmodules -fmodule-map-file=mo dule.modulemap -fmodules-cache-path=./moduleCache -c use.c -o use.o` :
+
+- `-fmodules` 用于告诉 clang 启用 module 编译
+- 编译后 module 缓存保存到 `-fmodules-cache-path` 后面的路径中可以看到 A、B2个头文件,编译缓存也存在2个,分别以 A、B 开头
+- `-fmodule-map-file` 指明 modulemap 文件路径
+
+module 是 clang 提供的能力。可以把头文件编译成二进制文件,缓存到系统目录中。这样的好处是,在使用某个 `.h` 的多个 `.m` 中,就不会因为多处引入 `.h` 而编译多次 `.h`
+
+
+
+### modulemap 规范和实例
+
+
+
+#### case 1:上例中的 modulemap
+
+```shell
+/* module.modulemap */
+module A { // 定义了一个名字叫 A 的模块
+ header "A.h" //模块 A 的公共头文件为 A.h。这意味着任何想要使用模块 A 中定义的类或函数的代码,都需要导入 A.h 文件
+}
+
+module B { // 定义了一个名为 B 的模块。。
+ header "B.h" // 模块 B 的公共头文件是 B.h
+ export A // 模块 B 向外暴露模块 A。这意味着任何导入模块 B 的代码,也可以使用模块 A 中定义的类或函数。export A 将模块 A 作为模块 B 的一部分公开,以便在使用模块 B 时,可以隐式使用模块 A 中的内容。
+}
+```
+
+假设存在一个 c.h 的文件,需求是想使用模块 A 和模块 B 中定义的内容。
+
+方法一:直接 import 模块对应的头文件
+
+```objective-c
+#import "A.h"
+#import "B.h"
+```
+
+方法二:因为模块 B 已经暴露了模块 A,所以导入 B 就可以使用模块 A 中定义的类和函数
+
+```objective-c
+#import "B.h"
+```
+
+
+
+#### case2:AFNetworking Demo 中的 modulemap
+
+AFNetworking 的 [Framework/module.modulemap](https://github.com/AFNetworking/AFNetworking/blob/master/Framework/module.modulemap)
+
+```json
+framework module AFNetworking { // 定义一个名为 AFNetworking 的框架模块
+ umbrella header "AFNetworking.h" // 指定了框架的伞头文件,这个文件是包含了所有公共头文件的文件,方便外部嗲欧哦那个
+ export * // 表示框架中的所有公共接口(类、结构体、枚举、协议等)都被导出,
+ module * { export * } // 意味着框架内部的所有子模块(即 AFNetworking.h 中头文件代表的子模块都会被匹配到),子模块的名称就是 .h 文件的名称,都将被导出,可以被外部所访问
+}
+```
+
+
+
+#### case3:AsyncDisplayKit 的 modulemap
+
+```json
+framework module AsyncDisplayKit { // 定义了一个名为 AsyncDisplayKit 的框架模块
+ umbrella header "AsyncDisplayKit.h" // 指定了框架的伞头文件,这个文件是包含所有公共头文件的文件,方便外部调用
+
+ export * // 表示框架中所有公共借口(类、结构体、枚举、协议等)都被导出,可以被外部代码访问
+ module * { // 意味着框架内部的所有子模块(即 AsyncDisplayKit.h 中头文件代表的子模块都会被匹配到),子模块的名称就是 .h 文件的名称,都将被导出,可以被外部所访问
+ export *
+ }
+
+ explicit module ASControlNode_Subclasses { // 显示指名,定义了一个名为 ASControlNode_Subclasses 的子模块,它包含了与 ASControlNode 相关的子类
+ header "ASControlNode+Subclasses.h" // 指定了子模块的头文件,这个头文件包含了子类的定义
+ export *
+ }
+
+ explicit module ASDisplayNode_Subclasses { // 定义了一个名为 ASDisplayNode_Subclasses 的子模块,它包含了所有与 ASDisplayNode 相关的子类
+ header "ASDisplayNode+Subclasses.h" // 指定了子模块的头文件,这个头文件包含了子类的定义
+ export *
+ }
+
+}
+```
+
+看一个例子,AsyncDisplayKit 中 `umbrella header` 伞头文件,即 `AsyncDisplayKit.h` 的内容
+
+```c++
+#import
+#import
+#import
+#import
+
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+
+#import
+#import
+#import
+#import
+#import
+#import
+
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+
+#import
+#import
+#import
+#import
+
+#import
+#import
+
+#if AS_IG_LIST_KIT
+#import
+#import
+#endif
+
+#import
+
+#import
+#import
+
+#import
+#import
+#import
+#import
+#import
+
+#import
+
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+#import
+
+#import
+#import
+
+#import
+```
+
+使用:子模块可以通过大模块访问到,比如 `@AsyncDisplayKit.ASEventLog;`
+
+更多关于 Module 的信息,可以查看 [Clang::Modules](https://clang.llvm.org/docs/Modules.html)
+
+
+
+### 实战:Framework 使用 modulemap
+
+第一步:创建动态库 `PersonFramework`,创建一个 iOS App Demo 工程。
+
+第二步:给动态库添加 `PersonFramework.modulemap` 文件
+
+第三步:给主工程、动态库设置在同一个 Xcode 项目中编译调试。
+
+
+
+
+
+结论:
+
+- 工程中 `PersonFramework.modulemap` 命名的 modulemap,在编译后被 Xcode 重命名为 `module.modulemap`,系统只认这个
+
+- `.modulemap` 文件有多种写法,就拿上面的举例,等价于3种写法:
+
+ - 写法1
+
+ ```json
+ framework module PersonFramework {
+ // umbrella 不加 Header,则说明后面要跟文件夹路径。
+ umbrella "Headers"
+ export *
+ module * {
+ export *
+ }
+ }
+ ```
+
+ `nubrella` 后不加 Header,说明跟得是文件夹,文件夹中里面的内容和 `Build Phases` 中 `Header` public 部分的头文件描述的一致。会将 public 的头文件,最后放到 `Headers` 文件夹中。
+
+ - 写法2
+
+ ```json
+ framework module PersonFramework {
+ umbrella header "PersonFramework.h"
+ export *
+ module * {
+ export *
+ }
+ }
+ ```
+
+ `umbrella header + 伞头文件` 意味着伞头文件里所有的 `.h` 将会被放到 `Headers` 文件夹中。且给外部访问
+
+ - 写法3
+
+ ```json
+ framework module PersonFramework {
+ umbrella header "PersonFramework.h"
+ explicit module Worker {
+ header "Worker.h"
+ export *
+ }
+ explicit module Student {
+ header "Student.h"
+ export *
+ }
+ }
+ ```
+
+ 写法3是将需要暴露的头文件,挨个显示声明子模块,指明头文件,然后导出。
+
+
+
+### Swift Framework modulemap
+
+背景:探索 Swift Framework 中,没有桥接文件,Swift 如何访问 OC?如何处理 modulemap 导出文件?
+
+问题1:Swift Framework 中 Swift 里如何访问 OC?
+
+利用 modulemap 解决。
+
+
+
+但是,上面的做法有副作用。虽然 Framework 内部 Swift 可以访问 OC 了,但是使用 Framework 的地方,也可以访问到 OCStudent 类。如何解决该问题?
+
+
+
+
+
+如何实现只在 Swift Framework 内 Swift 代码可以访问 OC 类,在使用 framework 的地方,访问不到 oc 类?
+
+module 提供 private modle 的能力。
+
+
+
+说明:
+
+- private module 文件必须命名为 `PersonSwiftFramework.private.modulemap` 的 `private.modulemap` 格式
+
+- `private.modulemap` 模块名必须为 `PersonSwiftFramework_Private`
+
+ ```json
+ /* module.modulemap */
+ framework module PersonSwiftFramework_Private {
+ module OCStudent {
+ header "OCStudent.h"
+ export *
+ }
+ }
+ ```
+
+ 文件内容是不希望通过正常预设模块暴露出去的子模块。
+
+ 什么是正常预设模块?比如 `PersonSwiftFramework` 是正常预设模块,通过 `@import PersonSwiftFramework.` 访问均符合预期。
+
+- private module 是规范,但是还是可以在使用 framework 的地方通过 `@import PersonSwiftFramework_Private.OCStudent;`访问到的
+
+具体代码见: `LDAndFramework/module-collections/ModulePractice`
+
+
+
+
+
+### Swift 静态库的合并
+
+#### 概念
+
+Xcode 9 之后,Swift 开始支持静态库。
+
+Swift 没有头文件的概念,Swift 要用 public 修饰的类和函数怎么办?
+
+Swift 库引入了一个全新的文件 `.swiftmodule`,其包含序列化过的 AST,也包含 SIL(Swift Intermediate Language,Swift 中间语言)
+
+
+
+#### 实战:Swift 静态库合并
+
+第一步:准备2个 Swift 静态库。其中 FLSwiftWorker 2个类完全一样,FLSwiftA、FLSwiftB 同名方法,方法实现不一样。
+
+编写脚本: `cp -Rv -- "${BUILT_PRODUCTS_DIR}/" "${SOURCE_ROOT}/../Products"`,把产物拷贝到 products 目录下
+
+
+
+第二步,打算用 libtool 指令 `libtool -static FLSwiftLibA.framework/FLSwiftLibA FLSwiftLibB.framework/FLSwiftLibB -o libFLSwiftC.a` 进行合并,发现报错
+
+
+
+
+
+因为存在同名符号,但是又不存在2级命名空间,所以如何处理符号问题??
+
+需要找 Headers 头文件信息和 Modules 文件夹里面的信息。
+
+```shell
+.
+├── FLSwiftLibA
+├── Headers
+│ ├── FLSwiftLibA-Swift.h
+│ └── FLSwiftLibA.h
+├── Info.plist
+├── Modules
+│ ├── FLSwiftLibA.swiftmodule
+│ │ ├── Project
+│ │ ├── x86_64-apple-ios-simulator.abi.json
+│ │ ├── x86_64-apple-ios-simulator.swiftdoc
+│ │ └── x86_64-apple-ios-simulator.swiftmodule
+│ └── module.modulemap
+└── _CodeSignature
+ ├── CodeDirectory
+ ├── CodeRequirements
+ ├── CodeRequirements-1
+ ├── CodeResources
+ └── CodeSignature
+```
+
+第三步,新建一个 iOS App 工程,引入合并后的静态库 libSwiftC.a 然后一步步解决符号冲突问题
+
+- 把静态库拖入到工程中
+
+- 新建 xcconfig 文件,配置静态库的头文件等信息
+
+- 编辑 xcconfig 配置头文件信息,不然编译会报错(引入了头文件)。
+
+ ```shell
+ // 头文件信息
+ HEADER_SEARCH_PATHS = $(inherited) "${SRCROOT}/FLStaticLibC/Public/FLSwiftLibA.framework/Headers" "${SRCROOT}/FLStaticLibC/Public/FLSwiftLibB.framework/Headers"
+ ```
+
+ 1.为什么 App 工程中的 OC 类中导入头文件编译成功,但还是无法访问里面的类?
+
+
+
+ 同样,需要一个新的参数,告诉 LD modulemap 的信息,然后系统根据 module.modulemap 去关联查找 `FLSwiftLibA.swiftmodule` 里面的 `x86_64-apple-ios-simulator.swiftmodule` swiftmodule 文件信息。
+
+ 这个时候就可以在 App oc 代码中访问静态库里 Swift 类了。
+
+ 2.为什么 App 工程中的 Swift 类中导入头文件报错?
+
+ 因为上面的配置是 Swift 编译后产生的 modulemap 和 swiftmodule 是配置 `OTHER_CFLAGS`,其实就是配置 c、oc 编译器也就是 clang 的关于 swift 的信息。
+
+ 而 Swift 编译器是 swiftc,需要额外配置。`SWIFT_INCLUDE_PATHS = "${SRCROOT}/FLStaticLibC/Public/FLSwiftLibA.framework/Modules/" "${SRCROOT}/FLStaticLibC/Public/FLSwiftLibB.framework/Modules/"`
+
+
+
+ 但是配置后还是报错,因为看上去文件路径是对的,Frmaework 生成的时候,就是这么划分文件夹的。但实际编译的时候 Headers和 `module.modulemap` 、`.swiftmodule` 文件夹在同一层目录。所以我们手动编译静态库之后,需要处理静态库产物的文件目录信息。
+
+
+
+
+
+
+
+## hmap文件与 Header Maps
+
+### 头文件导入技术发展历史
+
+#### 思考
+
+引入头文件的方式:
+
+- 路径:头文件所在目录 + `具体文件.h`
+- module:一堆 `.h` 放到 module,采用预编译的方式,产出二进制,节省编译时间。比如 `#import `
+
+思考:平时 Xcode 写代码的时候 `#import "ViewController.h"` 背后是怎么工作的?是如何找到具体的文件内容的
+
+1. **本地引用**:当你使用双引号 `"ViewController.h"` 进行导入时,编译器会首先在当前文件的本地目录(即当前 `.m` 或 `.mm` 文件所在的目录)查找头文件。
+2. **递归搜索**:如果在当前目录找不到 `ViewController.h`,编译器会递归地搜索所有子目录。
+3. **项目设置**:如果本地目录中没有找到文件,编译器会根据 Xcode 项目的设置中的 "Header Search Paths" 来确定接下来搜索的目录。这些路径通常包括:
+ - 项目的其他部分,如其他目标的目录。
+ - 项目依赖的库或框架的路径。
+ - 用户或系统级别的额外头文件目录。
+4. **相对路径**:在 "Header Search Paths" 中设置的路径可以是相对路径,Xcode 会将其相对于项目文件(`.xcodeproj`)的位置来解析。
+5. **绝对路径**:也可以使用绝对路径指定头文件的位置。
+6. **环境变量**:有时,Header Search Paths 会包含环境变量,这些变量在编译时会被系统的实际路径替换。
+7. **Framework和Library**:如果 `ViewController.h` 是某个框架或库的一部分,编译器还会在 "Framework Search Paths" 中指定的目录下查找。
+8. **Header Maps**:为了提高查找效率,项目可能使用 Header Maps。这些是预先生成的文件,列出了目录中所有头文件的索引,帮助编译器快速定位。
+9. **编译器缓存**:编译器可能会缓存头文件的查找结果,以避免在后续编译中重复搜索。
+10. **错误报告**:如果编译器在所有指定的搜索路径中都没有找到 `ViewController.h`,它会报错指出找不到文件。
+
+
+
+#### 早期的 import
+
+最早期是 include,再到后来的 import,区别是什么?
+
+- `include`: 是基础引入,编译过程中会被直接展开,其内容会插入到 `#include` 指令的位置。将 `目标.h` 文件中的内容一字不落地拷贝到当前文件中,并替换掉这句 `#include`
+- `import`: 只引入一次。用于导入模块化的头文件,它遵循模块化的结构,可以提供更好的封装性。会加入缓存,判断 import 的内容之前已经引入过则不再引入。
+
+这段缓存相关逻辑,可以在 [LLVM:HeaderSearch.cpp](https://github.com/llvm/llvm-project/blob/main/clang/lib/Lex/HeaderSearch.cpp) 中查看
+
+```c++
+// Cache all of the lookups performed by this method. Many headers are
+// multiply included, and the "pragma once" optimization prevents them from
+// being relex/pp'd, but they would still have to search through a
+// (potentially huge) series of SearchDirs to find it.
+LookupFileCacheInfo &CacheLookup = LookupFileCache[Filename];
+```
+
+import 举个例子吧。在 `Person.m`
+
+```objective-c
+#import
+@implementation Person
+- (void)eat {
+ NSLog(@"Person eat");
+}
+@end
+```
+
+`DynamicLibA.h` 内容如下
+
+```objective-c
+#import
+#import
+#import
+```
+
+在找到上面的内容后,编译器将其复制粘贴到 `Person.m` 中
+
+```objective-c
+#import
+#import
+#import
+@implementation Person
+- (void)eat {
+ NSLog(@"Person eat");
+}
+@end
+```
+
+编译器发现存在3个 import,则继续查找其内容
+
+```objective-c
+// Student.h
+@interface Student: NSObject
+- (void)study;
+@end
+```
+
+编译器会把其内容复制到 `Person.m` 中。
+
+```objective-c
+@interface Student: NSObject
+- (void)study;
+@end
+
+#import
+#import
+@implementation Person
+- (void)eat {
+ NSLog(@"Person eat");
+}
+@end
+```
+
+这样的步骤,直到整个文件(Perosn.m)中的所有 import 被替换掉。同时 `.m` 可能会变得非常长。
+
+存在2个问题:健壮性、拓展性。
+
+我们大多数情况下,经常需要引入一些头文件来助力实现某些功能,假设某个类只有一个方法,方法本身10行。但是因为导入了某些头文件,最后这个文件按照上述 import 的查找替换过程,最后该文件可能存在10万行代码(import 了 Foundation、UIKit、MapKit 等库)。太浪费、太不合理了
+
+
+
+#### PCH(PreCompiled Header)
+
+为了优化前面提到的问题,一种折中的技术方案诞生了,它就是 `PreCompiled Header`。早期做 iOS 开发的都看过 pch 文件。
+
+日常开发中,我们经常可以看到某些组件的头文件会频繁的出现,例如 UIKit,而这很容易让人联想到一个优化点,我们是不是可以通过某种手段,避免重复编译相同的内容呢?
+
+而这就是 PCH 为预编译流程带来的改进点。大体原理就是,在我们编译任意 `.m` 文件前, 编译器会先对 `.pch` 里的内容进行预编译,将其变为一种二进制的中间格式缓存起来,便于后续的使用。当开始编译 `.m` 文件时,如果需要 PCH 里已经编译过的内容,直接读取即可,无须再次编译。
+
+虽然这种技术有一定的优势,但实际应用起来,还存在不少的问题。
+
+首先,它的维护是有一定的成本的:
+
+- 对于大部分历史包袱沉重的组件来说,将项目中的引用关系梳理清楚就十分麻烦。因此不知道需要将哪些头文件放到 `.pch` 文件中
+- 随着版本的不断迭代,哪些头文件需要移出 PCH,哪些头文件需要移进 PCH 将会变得越来越麻烦
+
+
+
+#### clang module
+
+为了解决上面的问题,Clang Module 技术诞生了。
+
+一个 Module 是机器代码和数据的最小单位,可以独立于其他代码单元进行链接。
+
+通常,Module 是通过编译单个源文件生成的目标文件。例如,当前的 `test.m` 被编译成目标文件 `test.o` 时,当前的目标文件就代表了一个 Module 。
+
+在实际编译之时,编译器会创建一个全新的目录,用它来存放已经编译过的 Module 产物。如果在编译的文件中引用到某个 Module 的话,系统将优先在这个列表内查找是否存在对应的中间产物,如果能找到,则说明该文件已经被编译过,则直接使用该中间产物,如果没找到,则把引用到的头文件进行编译,并将产物添加到相应的空间中以备重复使用。
+
+相关的一些使用,可以查看上面的 section。
+
+还是存在问题,因此诞生了 Use Header Maps 技术。
+
+
+
+### 诞生背景
+
+但存在一个问题,如果同时存在多个 `header search path` 的时候,假设一个工作项目有10000个文件,一个类使用了10个头文件,要去10000个文件中分别查找10个头文件具体位置,这个查找过程是发生在编译阶段的。无疑会增加编译耗时。
+
+基于此,诞生了 Header Maps 技术,通过 hmap 文件,让 Xcode 在查找头文件的时候更快速。
+
+### hmap 结构
+
+创建一个 iOS App,默认模版的基础上添加一个 Person 类,一个继承自 Person 的 Student 类。然后编译
+
+
+
+
+
+从 Xcode Compile log 可以看到通过 `-I` 参数来指定 hmap 文件。复制目录路径查看所有的 hmap 文件,可以看到,
+
+**iOS hmap 文件会根据文件分类和使用方式主要与项目结构和编译需求有关。可以分为项目级别 Header Maps、组件级别 Header Maps、公共与私有 Header Maps等等**。
+
+
+
+`hmap` 文件不可读,必须用对应的工具解析,按照指定的格式解析。因为属于编译器的 scope,所以查看 LLVM 源码窥探下。
+
+
+
+可以看到 hmap 结构类似 Mach-O ,顶部 HMapHeader 告诉系统,当前有几个 Bucket,下面是 Bucket 信息。然后最下方是 string 区域。
+
+在 HMapBucket 中:
+
+- Key: 一个 `uint32_t` 类型的成员,表示 bucket 中键的哈希值或者是一个特殊值表示 bucket 的状态(例如,是否为空或者是一个冲突的bucket)
+
+- prefix: 一个 `uint32_t` 类型的成员,通常用于存储键的前缀偏移量,它与 `Suffix` 一起定义了键在整个 Header Map 中的完整字符串
+- Suffix: 一个 `uint32_t` 类型的成员,用于存储键的后缀长度,与 `Prefix` 结合使用可以定位到键的具体字符串
+
+
+
+### 编写工具分析 hmap 文件
+
+其结构、工作原理都类似 Mach-O 文件。参考 Mach-O 文件结构和 [LLVM:HeaderMap.cpp](https://github.com/llvm/llvm-project/blob/main/clang/lib/Lex/HeaderMap.cpp) 的实现,编写一个读取 hmap 文件的代码。
+
+具体代码可以在这个 Repo 中查看并运行 [BlogDemos:HMapDump](https://github.com/FantasticLBP/BlogDemos/tree/master/HMapDump)
+
+运行调试的时候,把需要读取的 HMapFile 拷贝到项目根目录。然后 Edit Scheme - Run - Arguments Passed On Launch
+
+
+
+
+
+如何做到该工具做到像系统自带的 `objdump` 一样,在终端命令行使用:
+
+- 在电脑根目录新建 `CustomTools` 文件夹
+- 将上面编译后的产物复制进去
+- 在 `.zshrc` 文件里将路径添加进去。`export PATH=~/CustomTools:$PATH`
+- 编辑 `.zshrc` 文件后,在终端执行 `souce .zshrc`
+- 即可使用
+
+效果如下:
+
+
+
+
+
+
+
+### 编写工具生成 hmap 文件
+
+#### 为什么要编写 hmap 文件
+
+如果2个 `.m` 文件有相同的头文件代码,造成编译浪费。
+
+
+
+clang 可以使用 `-I` 来指定 Header Search Path 信息。`-I` 后面可以跟:`目录文件夹`、`.hmap` 文件
+
+iOS 导入方式:
+
+- `import <> ` :本质上就是 LD 编译参数 `HEADERS_SEARCH_PATHS -I`
+- `import ""` :本质上就是 LD 编译参数 `USER_HEADER_SEARCH_PATHS -iquote`
+
+Tips:Xcode 设置 Search Path 有三处 `Header Search Path`、`System Header Search Path`、`User Header Search Path`,区别是什么?
+
+- System Header Search Path 是针对系统头文件的设置,通常代指 `<>` 方式引入的文件
+- User Header Search Path 则是针对非系统头文件的设置,通常代指 `""` 方式引入的文件
+- Header Search Path 并不会有任何限制,它普适于任何方式的头文件引用
+
+
+
+Xcode 会主动生成 `.hmap` 文件,那为什么还需要研究生成 `hmap` 文件?
+
+有必要。下面来个例子进行说明。虽然平时开发中经常以二进制组件的方式构建 App,但在某些场景下(精准测试、覆盖率统计等),打包构建还是需要以全源码编译的方式进行。而且在实际开发过程中,大多是以源码的方式进行开发,所以我们将实验对象设置为基于全源码编译的流程。
+
+
+
+创建一个静态库 `HMapStaticLib` 里面就包含2个类文件 `Person` 类、继承自 `Person` 类的 `Student` 类。再创建一个使用 iOS App,使用该静态库。编译该 App,查看编译日志。
+
+然后查看编译日志中的 ` HMapStaticLibApp-project-headers.hmap` 文件。利用上面制作的 `HMapDump` 工具。
+
+
+
+分析:在 App 使用 Static Library 的情况下,假设开启了 `Use Header Map`,静态库中所有头文件类型为 `Project`(只有 Project、Private、Public 3种类型,public 就是字面意思的公开,p r)的情况,最终生成的 `.hmap` 文件中只会包含类似 `#import "Student.h"` 的键值引用。也就是说使用的地方,只有 `#import "Student.h"` 的这种方式才会走 hmap 策略,否则还是走 `Header Search Path` 来寻找头文件路径。
+
+组件、库使用 `#import ` 是访问的标准做法。好处有3点:
+1. 明确头文件的由来,避免歧义
+2. 可以让我们在是否开启 clang module 中随意切换
+3. Apple 在 WWDC 里曾经不止一次建议开发者使用这种方式来引入头文件
+
+所以可以回答上面的问题了。虽然一个静态库、动态库项目中,`Build Setting` 中虽然 `Use Header Maps` 为 YES,但是某些情况下 Header Maps 带来的加速福利没有享受到。
+
+
+
+
+
+一个大型项目有300个 Pod,每个 Pod 有100个头文件,也就是共 30000个头文件,在30000个头文件中,如果没有享受 `Header Maps` 带来的福利,老老实实依靠 `Header Search Path` 中提供的头文件所在的文件夹信息查找,或者递归查找。可能存在 n*m 的循环查找过程,效率很低,涉及的 IO 影响大型项目的编译构建时间,影响分发和发生问题时候的及时热修复。
+
+既然知道某些情况下会存在 hmap 文件没有命中的情况,那有必要修改吗?让静态库的情况,也可以使用 hmap 带来的编译红利。
+
+
+
+#### 如何生成 HMap 文件
+LLVM 真是好东西,把 Xcode 编译、链接等一些幕后的事情变成白盒,有迹可循,感兴趣的可以去查看源码并带着一些关键词来搜索源码进行查看,可能会发现一些平时了解不到的细节。
+
+可以查看 [HeaderMapTest.cpp](https://github.com/llvm/llvm-project/blob/main/clang/unittests/Lex/HeaderMapTest.cpp) 和 [HeaderMapTestUtils.h](https://github.com/llvm/llvm-project/blob/main/clang/unittests/Lex/HeaderMapTestUtils.h) 编写 hmap 的生成代码。编写后的具体代码可以查看 [BlogDemos:HMapWritor](https://github.com/FantasticLBP/BlogDemos/tree/master/HMapWritor)
+
+为了方便平时使用,按照上面的方式,也将二进制放到 `/Users/unix_kernel/CustomTools` 目录下,同时设置 `.zshrc` 可访问性。
+
+
+
+### hmap 助力提升 iOS 项目编译速度
+
+已经编写好生成 `.hmap` 文件的能力了,那如何使用生成的 `.hmap` 文件?
+
+- Xcode `Build Setting` 中 `Use Header Maps` 为 NO
+- `Header Search Path` 设置生成的 hmap 文件路径
+
+实践操作下。把上面静态库无法走 Header Maps 带来的问题解决掉。
+
+第一步,编写 hmap 所需要的 json 信息。
+
+第二步,利用 `HMapWriter` 能力,根据 json 生成静态库所需要的 `StaticLibUsage.hmap`
+
+第三步,在静态库 Xcode 项目中,关闭 `Use Header Maps`(设置为 NO),同时修改 `Header Search Paths` 为生成 `StaticLibUsage.hmap` 路径。
+
+第四步,编译使用静态库的 App 工程。最后比较静态库走自定义 `.hmap` 前后的编译耗时。
+
+
+
+可以看到静态库使用了自定义的 Header Maps 文件后,使用静态的 App 前后,编译耗时减少了0.1s。
+
+
+
+Demo 及其演示代码见 [HMapStaticLibApp](https://github.com/FantasticLBP/BlogDemos/tree/master/HMapStaticLibApp) 和 [HMapStaticLib](https://github.com/FantasticLBP/BlogDemos/tree/master/HMapStaticLib)。
+
+
+
+### 工程化问题
+
+上面是理论上分析并思考了2个问题:
+- 为什么需要介入自己生成 hmap 文件
+- 生成后的 hmap 文件如何使用
+如果每个工程项目都这么做,效率有点低,所以需要站在工程化的角度去设计,如何优化?
+
+Cocoapods 提供了很多钩子,可以自定义编写 Ruby 脚本。
+- HooksManager 注册 cocoapods 的 `post_install` 钩子
+- 通过 `header_mappings_by_file_accessor` 遍历所有头文件和 `header_dir`,由header_dir/header.h和header.h为key,以头文件搜索路径为value,组装成一个Hash,生成所有组件pod头文件的json文件,再通过hmap工具将json文件转成hmap文件。
+- 再修改各 pod 中 `.xcconfig` 文件的 `HEADER_SEARCH_PATHS` 值,仅指向生成的新生成的 `.hmap` 文件,删除原来添加的搜索目录;
+- 修改各 pod 的 `USE_HEADERMAP` 值,关闭对默认的 `.hmap` 文件的访问
+
+
+
+## 死代码剥离
+
+死代码剥离的条件:
+
+- 没有被入口点使用则会被干掉
+- 没有被导出符号所使用,则会被干掉
+
+dead code strip 和 xlinker 提供的4个参数:
+
+- `-noall_load`: 完全不加载、直接去优化。`OTHER_LDFLAGS=-Xlinker -noall_load`
+
+- `-all_load`: 完全加载、不要去优化掉。`OTHER_LDFLAGS=-Xlinker -all_load`
+- `-ObjC` : 排除ObjC的代码、其他的都优化掉。`OTHER_LDFLAGS=-Xlinker -ObjC `
+- `-force_load` : 指定哪些静态库不要优化掉;
@@ -22,7 +2504,192 @@ ImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheI
-## dyld 应用
+## dyld 及其工作流程
+
+### 概念
+
+dyld 是一个动态链接程序。是 iOS 和 macOS 系统中的**动态链接器**(Dynamic Loader)。它负责在程序运行时加载和链接动态库。简单来说,dyld 就如同一个「中间人」,将你的程序代码和它所依赖的所有动态库整合在一起,最终让你的程序能够正常运行。
+
+`libdyld.dylib` 给我们的程序提供在 runtime 期间能使用动态链接功能。比如 dlopen 能力,又或者是系统 lazy-binding 符号表,在运行之后,动态修正符号地址。
+
+懒加载的符号表项通常包括一个或多个指向符号的间接符号(Indirect Symbols),这些间接符号在首次访问时会被动态链接器替换为实际的内存地址。这个过程称为符号绑定(Symbol Binding)。由于懒加载的符号在首次使用前不会被绑定,因此可以减少应用程序的启动时间,并按需加载所需的资源
+
+
+
+```shell
+objdump --macho --private-headers DSYMDemo
+```
+
+
+
+- **启动阶段**:应用程序启动时,iOS 系统首先解析 `Info.plist` 文件,加载相关信息,例如启动画面等,并建立沙盒环境以及进行权限检查
+
+- **Mach-O 加载**:系统加载应用程序的可执行文件(Mach-O 格式),这是一个包含程序指令和数据的文件。加载时会包括 dylib(动态库)的加载时间以及偏移修正(rebase)和符号绑定(binding)的时间
+
+- **dyld 的初始化**:在系统内核完成初始化后,从 Mach-O 的 `LC_LOAD_DYLINKER` load command 中,根据 name 路径信息,然后加载,dyld 开始执行。它的主要任务是加载应用程序的主可执行文件以及其他依赖的动态库
+
+ ```shell
+ Load command 14
+ cmd LC_LOAD_DYLIB
+ cmdsize 88
+ name /System/Library/Frameworks/Foundation.framework/Foundation (offset 24)
+ time stamp 2 Thu Jan 1 08:00:02 1970
+ current version 1953.255.0
+ compatibility version 300.0.0
+ ```
+
+- **依赖分析**:dyld 会分析 Mach-O 文件,找出程序所依赖的所有库,并递归地解析所有依赖关系,形成动态库的依赖图
+
+- **内存映射**:dyld 将匹配 Mach-O 文件到内存空间,确保每个依赖库都被映射到正确的地址
+
+ - 解析 Mach Header,判断当前 Mach-O 文件是否可用
+
+ 比如上面的 Mach header 中
+
+ - cputype 是 `X86_64`,dyld 会判断该架构能否在当前系统上运行
+ - filetype 是 `EXECUTE`,是不是一个可执行文件
+ - 有 ncmds 16个 Load commands,占 sizeofcmds 2848大小的空间
+
+ - 根据 Mach Header,解析 load commands,根据解析的结果,将程序各个部分加载程序到指定的地址空间,同时设置保护标志
+
+ ```js
+ Load command 1
+ cmd LC_SEGMENT_64 // 指定加载命令的类型是 LC_SEGMENT_64,这代表一个 64 位的内存段命令。
+ cmdsize 792 // 表示这个加载命令的总大小是 792 字节。
+ segname __TEXT // 段名称,这里是 __TEXT 段,通常包含程序的指令和只读数据。
+ vmaddr 0x0000000100000000 // 指定了该段在内存中的虚拟地址
+ vmsize 0x0000000000004000 // 定义了该段在内存中的大小,这里是 16 KiB(4000 十六进制转换为十进制是 16384)。
+ fileoff 0 // 表示该段在文件中的偏移量,这里是文件的起始位置
+ filesize 16384 // 表示该段在文件中的实际大小,以字节为单位
+ maxprot r-x // 定义了该段(代码段)的最大保护级别,这里是 r-x(读和执行),表示该段可以被读取和执行,但不能写入。如果是数据段,则可读可写 rw-
+ initprot r-x // 定义了该段在加载到内存时的初始保护级别,与最大保护级别相同
+ nsects 9 // 表示该段包含的节(sections)数量,这里是 9 个
+ flags (none) // 该段没有设置任何特殊标志
+ ```
+
+
+
+- **符号查找和绑定**:进行符号查找,处理程序中的符号引用,并进行符号绑定,确保所有的函数调用和全局变量引用都能正确地指向它们的实现
+
+- **rebase 操作**:由于 ASLR(地址空间布局随机化)的需要,dyld 会对 Mach-O 文件进行 rebase 操作,调整代码和数据的地址以适应随机化的内存布局
+
+- **初始化程序执行**:在链接操作完成后,dyld 会执行初始化程序,包括 Objective-C 的 `+load` 方法和 C 的构造函数,以初始化静态变量
+
+- **main 函数调用**:最后,dyld 读取 Mach-O 文件的 `LC_MAIN` 命令,获取程序的入口地址,并调用 `main` 函数,启动应用程序的主执行流程
+
+- **dyld 3 的优化**:从 iOS 11 开始,引入了 dyld 3,它通过进程外的 Mach-O 分析器/编译器以及进程内的执行引擎,将许多耗时的操作提前处理好,并缓存结果,从而极大提升了启动速度
+
+
+
+### dyld 到底做了什么
+
+1. 执行自身初始化配置加载环境。 `LC_DYLD_INFO_ONLY`
+
+ ```shell
+ Load command 4
+ cmd LC_DYLD_INFO_ONLY // 表示这是一个只包含 dyld 信息的加载命令,它通常不包含实际的 rebase、bind 等信息,而是提供给 dyld 用于优化链接过程的信息
+ cmdsize 48 // 这个命令的总大小是 48 字节
+ rebase_off 0 // 指示 rebase 信息在文件中的位置
+ rebase_size 0 // 指示 rebase 信息在文件中的大小
+ bind_off 0 // 指示 bind 信息的位置
+ bind_size 0 // 指示 bind 信息的大小
+ weak_bind_off 0 // 指示弱绑定信息的位置
+ weak_bind_size 0 // 指示弱绑定信息的大小
+ lazy_bind_off 0 // 指示延迟绑定信息的位置
+ lazy_bind_size 0 // 指示延迟绑定信息的大小
+ export_off 32768 // 导出表(export table)的位置
+ export_size 48 // 导出表(export table)的大小
+ ```
+
+2. 加载当前程序链接的所有动态库到指定的内存中。`LC_LOAD_DYLIB`
+
+ ```shell
+ Load command 12
+ cmd LC_LOAD_DYLIB // 指明了 load command 的类型是 LC_LOAD_DYLIB,意味着接下来的信息是关于加载一个动态库的
+ cmdsize 56 // 这个命令的总大小是56字节
+ name /usr/lib/libSystem.B.dylib (offset 24) // 指定了要加载的动态库的路径和文件名。这里是 libSystem.B.dylib,位于 /usr/lib/ 目录下。(offset 24) 表示文件名在命令数据中的偏移量
+ time stamp 2 Thu Jan 1 08:00:02 1970 // 这是动态库的时间戳,用于版本检查。不过,这个时间戳通常是0或一个固定值,因为系统库的加载不依赖于时间戳
+ current version 1319.0.0 // 这是动态库的当前版本号,用于确保应用程序加载的是兼容的版本
+ compatibility version 1.0.0 // 这是动态库的兼容版本号,表明应用程序至少需要这个版本的库才能正常运行
+ ```
+
+3. 搜索所有动态库,绑定需要在调用程序之前用的符号(非懒加载符号)。`LC_DYSYMTAB`
+
+4. 在 indirect symbol table 中,将需要绑定的导入符号真实地址替换。`LC_DYSYMTAB`
+
+ Todo:fishhook 原理
+
+5. 向程序提供 Runtime 运行时使用 dyld 的接口(存在于 libdyld .dylib 中,由 `LC_LOAD_DYLIB` 提供)
+
+6. 配置 Runtime,执行所有动态库/image 中使用的全局构造函数
+
+7. dyld 调用程序入口函数,开始执行程序。`LC_MAIN`
+
+ ```
+ Load command 11
+ cmd LC_MAIN // 里的 cmd 指明了加载命令的类型是 LC_MAIN,这个命令用于指定程序的主入口点
+ cmdsize 24 // 这个命令的大小是24字节,这是命令头加上任何尾部数据的总和
+ entryoff 16288 // 指定了程序入口点的偏移量,它是相对于文件的开始位置的。在这个例子中,入口点位于文件开头的第 16288 字节处。入口点通常是 main 函数的地址
+ stacksize 0 // 这个字段指定了为程序的主线程初始栈分配的大小。在这个例子中,栈大小被设置为0,这可能意味着栈的大小将使用默认值,或者在其他地方指定(例如,通过链接器的其他设置或命令行选项)
+ ```
+
+
+
+
+
+### dyld 加载过程
+
+
+
+
+
+- 调⽤ `fork `函数,创建⼀个进程
+
+- 调⽤ `execve` 或其衍⽣函数,在该进程上加载,执⾏ `Mach-O`⽂件
+
+- 将 `Mach-O` ⽂件加载到内存
+
+- 开始分析 `Mach-O` 中的 `mach_header`,以确认它是有效的 `Mach-O` ⽂件
+
+- 验证通过,根据 `mach_header `解析 `load commands`。根据解析结果,将程序各个部分加载到指定的地址空间,同时设置保护标记
+
+- 从 `LC_LOAD_DYLINKEN`中加载 `dyld`
+
+- `dyld`开始⼯作
+
+- 调用 `__dyld_start()` 函数,通知 `dyld` 开始工作
+
+- 调用 `dyldbootstrap::start` 函数,使 `dyld` ⾃身进⼊可运⾏状态
+
+- 调用 `dyld::_main` 函数,`dyld `的入口函数
+
+- 检查共享缓存中的缓存,如果找到直接返回(红线逻辑),否则继续后面的流程
+
+- 共享缓存中没有找到,则继续下面流程
+
+- 加载所有手动插入的动态库
+
+- 链接程序需要的动态库
+
+- 链接插入的库
+
+- 应用插入函数
+
+- 绑定符号
+
+- 调用 `instantiateMainExecutable` ,为主可执行文件创建镜像
+
+- 调用当前程序与动态库的初始化构造函数(`__attribute__((constructor))`)
+
+- 通过 `LC_MAIN` 查找设置程序⼊⼝函数,将胶⽔地址设置成⼊⼝函数地址,否则胶⽔地址为0
+
+- 提供胶水地址,返回到 `dyld::_main` 函数中继续执行
+
+- 通过 `dyld::_main`→`dyldbootstrap::start`→`__dyld_start()`,dyld 配置完成,把控制权交给可执⾏⽂件的⼊⼝函数`main()`,继续后面的流程
+
+
+
+### dyld 应用
窥探系统库底层实现的时候可能需要从动态库共享缓存中提取出某个 Framework,比如 UIKit。这时候要么用第三方工具,要么用 dyld 的能力。
@@ -71,15 +2738,126 @@ int main(int argc, const char* argv[])
dyld 本身就是一种 MachO 文件,MachO 文件类型为7,是一种包含多种架构的结构,如下图:
-
+
可以加载以下类型的 Mach-O 文件:MH_EXECUTE、MH_DYLIB、MH_BUNDLE
+#### 插入动态库
+
+工作原理:使用 `__attribute__((constructor))` 是 GCC 和 Clang 编译器支持的一个属性,用于标记一个函数为构造函数,该函数会在程序加载动态库时自动执行。
+
+动态库的加载过程通常是在程序启动时进行的,因此 `__attribute__((constructor))` 属性标记的函数会在 `main` 函数执行之前被调用。
+
+
+
+第一步:创建 `Inject` 动态库,新建 `Inject.m` 文件
+
+```c++
+#import
+
+__attribute__((constructor))
+static void customConstructor(int argc, const char **argv) {
+ NSLog(@"Hello,I am an injected dynamic library!");
+}
+```
+
+第二步:创建 iOS App 测试工程,接入 Inject 动态库。将 `Inject.framework` 里的动态库拖到 App 工程根目录。
+
+第三步:Edit scheme -> Run -> Environment Variables。Name 为 `DYLD_INSERT_LIBRARIES`,value 为 InjectFunction 动态库路径,`${SRCROOT}/Inject`。
+
+
+
+第四步:运行输出如下
+
+
+
+具体 [Demo]()
+
+
+
+#### 函数替换
+
+工作原理:
+
+在 iOS 中,`__DATA, __interpose` 是一个特殊的 Mach-O 段,用于实现函数插桩 (Function Interposing)。它允许你在不修改原始库代码的情况下,拦截并替换库中的某个函数
+
+**`__attribute__((used)) static struct { ... } ... __attribute__ ((section("__DATA, __interpose"))) = { ... };`** 定义一个结构体,它包含两个指向函数的指针:`replacement` 和 `replacee`。结构体使用 `__attribute__((used))` 属性标记,以避免编译器将其优化掉。同时,它被放置在 `__DATA, __interpose` 段,这个段是专门为函数插桩而设计的。
+
+
+
+参考 [dyld::dyld-interposing.h](https://opensource.apple.com/source/dyld/dyld-210.2.3/include/mach-o/dyld-interposing.h)
+
+第一步:创建 `InjectFunction` 动态库,新建 `InjectFunction.m` 文件
+
+```c++
+#import
+#define INTERPOSE(_replacement, _replacee) \
+ __attribute__((used)) static struct { \
+ const void* replacement; \
+ const void* replacee; \
+ } _interpose_##_replacee __attribute__ ((section("__DATA, __interpose"))) = { \
+ (const void*) (unsigned long) &_replacement, \
+ (const void*) (unsigned long) &_replacee \
+ };
+
+void my_NSLog(NSString *format, ...) {
+ NSLog(@"Injected Function ---> %@", format);
+}
+INTERPOSE(my_NSLog, NSLog);
+```
+
+第二步:创建 iOS App 测试工程,接入 InjectFunction 动态库。将 `InjectFunction.framework` 里的动态库托到 App 工程根目录。
+
+第三步:Edit scheme -> Run -> Environment Variables。Name 为 `DYLD_INSERT_LIBRARIES`,value 为 InjectFunction 动态库路径,当有多个动态库的时候,中间用 `:` 把路径隔开。`${SRCROOT}/Inject:${SRCROOT}/InjectFunction`。
+
+第四步:运行输出。发现 NSLog 确实被 hook 做了替换。
+
+
+
+
+
+应用场景:不能上架,但可以去做探索、验证源码等场景。
+
+
+
+### 调试 dyld
+
+一种是替换系统的 dyld,风险大,需要感知源码。推荐使用 dyld 提供的环境变量来控制 dyld 在运行过程中输出感兴趣的信息。
+
+- `DYLD_PRINT_APIS`: 打印 dyld 内几乎所有发生的调用
+- `DYLD_PRINT_LIBRARIES` :打印在应用程序启动期间正在加载的所有动态库
+- `DYLD_PRINT_WARNINGS`:打印 dyld 运行过程中的辅助信息
+- `DYLD_PATH`:显示 dyld 搜索动态库的目录顺序
+- `DYLD_PRINT_ENV`:显示 dyld 初始化的环境变量
+- `DLYD_PRINT_SEGMENTS`:打印当前程序的 segment 信息
+- `DYLD_PRINT_STATISTICS`:打印 pre-main 耗时
+- `DYLD_PRINT_INITIALIZERS`:会在执行每个镜像(image)的初始化器(initializer)时打印出一行信息。这些初始化器包括 C++ 的静态构造函数以及使用 `__attribute__((constructor))` 标记的函数。这个环境变量对于调试和分析程序启动时的初始化顺序和行为非常有用。
+
+怎么用?
+
+`环境变量=1 ./可执行文件`
+
+
+
+
+
+另一种方式是利用 Xcode -> Edit Scheme,增加或修改 dyld 环境变量
+
+
+
+
+
+
+
## Mach-O
-Mach Object 的缩写,是 iOS/MacOS 上用于存储程序、库的标准格式
+Mach Object 的缩写,是 iOS/MacOS 上用于存储程序、库的标准格式。
+
+Mach-O 格式⽤来替代 BSD 系统的 `a.out` 格式。Mach-O ⽂件格式保存了在编译过程和链接过程中产⽣的机器代码和数据,从⽽为静态链接和动态链接的代码提供了单⼀⽂件格式。
+
+
在 XNU 源码中可以查看 Mach-O 的定义。`loader.h`
@@ -119,7 +2897,7 @@ Mach Object 的缩写,是 iOS/MacOS 上用于存储程序、库的标准格式
Xcode 中也可以查看 Mach-O 文件类型
-
+
@@ -127,7 +2905,7 @@ Xcode 中也可以查看 Mach-O 文件类型
Tips:`file` 命令可以查看文件类型。
-
+
`find . -name "*.c"` 比如在当前路径查找 .c 文件
@@ -160,7 +2938,7 @@ Xcode 中可以修改指令集。由 Build Settings 的2个配置决定: `Arch
### Mach-O 结构
-
+
一个 Mach-O 文件包含3块
@@ -172,17 +2950,17 @@ Xcode 中可以修改指令集。由 Build Settings 的2个配置决定: `Arch
可以用 [MachOView:](https://github.com/fangshufeng/MachOView) 和系统自带的 atool 查看 Mach-O 信息
-
+
比如我用 otool 查看我编写的一个 SwiftUIDemo 所依赖的共享缓存库
-
+
用 MachOView 查看 DDD Mach-O 文件
-
+
-
+
可以看到在 Mach-O 文件上,`__PAGEZERO` 的 `VM Size` 有值,但是 File Size 为0,也就是说 `__PAGEZERO` 在 Mach-O 中不占据内存,但是程序运行起来之后,会占据虚拟内存。所以代码段在 Mach-O 中 File Offset 为0(如果前面的 `__PAGEZERO` 的 File size 有值,这里的 File Offset 就不为0)。
@@ -190,6 +2968,283 @@ Xcode 中可以修改指令集。由 Build Settings 的2个配置决定: `Arch
+## Mach-O 文件如何查找地址
+
+第一步,编写一个名为 `main.m` 的代码
+
+```objective-c
+void test1 () {}
+void test2() {}
+int globalValue;
+int main () {
+ globalValue = 21;
+ globalValue = 20;
+ test1();
+ test2();
+ return 0;
+}
+```
+
+第二步,将其转换为可执行文件,指令为`clang main.m -o main`
+
+第三步,分析查看可执行文件 `objdump --macho -d main`
+
+第四步,将其转为目标文件,指令为 `clang -c main.m -o main.o`
+
+第五步,查看目标文件指令信息 `objdump --macho -d main.o`
+
+
+
+分析下指令信息:
+
+- 可以看到源码中从上到下声明的函数,在目标文件和可执行文件中顺序是一致的
+
+ - 可执行文件中,顺序也是从上到下,依次执行的。左侧是真实指令地址
+ - 目标文件中,顺序也是从上大下,但没有真实地址,是相对地址,是偏移量。
+
+- 可以看到像函数调用的逻辑,是 ` e8 00 00 00 00 callq _test1`,其中 `e8` 是固定指令,代表 `callq`。后面的 `00 00 00 00 ` 是相对地址,test1 真正的地址是 `下面 4e` + `上面 00 00 00 00` 的结果。这是一种**近地址相对位移技术**
+
+- test1、test2 2个函数都存在,地址不一样,为什么都是 `00 00 00 00 ` ?那系统如何确定函数真实地址?需要找个地方将这些符号存起来,然后再找个时机去把真实的地址写进去。需要**重定位符号表**,告诉链接器在链接阶段需要重定位
+
+
+
+ - 使用 `objdump --macho --reloc main.o` 指令查看 main.o 的重定位符号表。
+ - 符号表中 `test1` 的地址就是 `0000004a` 对应的数据。`49` 后面就是 `4a`
+ - 符号表中 `test2` 的地址就是 `0000004f` 对应的数据。`4e` 后面就是 `4f`
+ - 符号表中 `globalValue` 的地址就是 `0000003f` 对应的数据,`3c 3d 3e 3f`,经过 `48 8b 05` 到 `3f`
+
+
+
+### 如何找到函数的真实地址
+
+以 test1 为例。
+
+```shell
+100003f93: c7 00 14 00 00 00 movl $20, (%rax)
+100003f99: e8 b2 ff ff ff callq _test1
+100003f9e: e8 bd ff ff ff callq _test2
+100003fa3: 31 c0 xorl %eax, %eax
+```
+
+第一步:根据 B2 计算得到原码,然后计算出原码。
+
+iOS 是小端序,从右向左看,`b2 ff ff ff` 可以看到后4位是 ff,所以是一个经过计算后的补码信息。所以需要计算得到原码信息。
+
+补码如何到原码?所有的1取反,然后加1。第一步计算结果为 `0x4E`
+
+第二步:根据近地址相对位移技术,`下一条指令地址+ b2 ff ff ff` 就是函数 test1 的真实地址。但是 FF 是负的,所以 `0x100003f9e - 0x4E = 0x100003F50 `,也就是 Text 段中 test1 的真实地址。
+
+完整的
+
+
+
+
+
+### 如何找到全局变量地址
+
+第一步,先用指令查看全局变量的地址值。 `objdump --macho -s main`,可以看到地址值为 `0x100008000`
+
+
+
+
+
+第二步,手动计算。根据近地址相对位移技术原理,`下一条指令 + 0x407a` = `0x100003f86 +0x407a = 0x100008000`
+
+去掉最开始的 `48 8d 05` ,iOS 小端序,所以是 `0x407a`
+
+```shell
+100003f70: 55 pushq %rbp
+100003f71: 48 89 e5 movq %rsp, %rbp
+100003f74: 48 83 ec 10 subq $16, %rsp
+100003f78: c7 45 fc 00 00 00 00 movl $0, -4(%rbp)
+
+100003f7f: 48 8d 05 7a 40 00 00 leaq _globalValue(%rip), %rax
+100003f86: c7 00 15 00 00 00 movl $21, (%rax)
+
+100003f8c: 48 8d 05 6d 40 00 00 leaq _globalValue(%rip), %rax
+100003f93: c7 00 14 00 00 00 movl $20, (%rax)
+```
+
+可见 `0x100008000` 和数据段的地址是一样的。
+
+至此,可以了解到 Mach-O 文件是按照不同 Section 去加载数据,访问数据的。但不同 Section 是语义上更方便理解的,程序执行最本质的是:不管处于什么 section,只看偏移量。
+
+
+
+## .dSYM 文件
+
+### 定义
+
+`.dSYM` 文件就是保存 DWARF 格式的调试信息的文件。
+
+`.DSYM` (debugging symbol)文件是保存十六进制函数地址映射信息的中转文件,调试信息(symbols)都包含在该文件中。Xcode 工程每次编译运行都会生成新的 `.DSYM` 文件。默认情况下 debug 模式时不生成 `.DSYM` ,可以在 Build Settings -> Build Options -> Debug Information Format 后将值 `DWARF` 修改为 `DWARF with DSYM File`,这样再次编译运行就可以生成 `.DSYM` 文件。
+
+DWARF 是被众多编译器和调试器使用的,用于支持源码级别调试的调试文件格式。
+
+
+
+### 探索
+
+#### 如何生成 `.dSYM` 文件
+
+第一步,新建 `main.m` 文件
+
+第二步:Xcode 每次编译运行都会生成新的 `.DSYM` 文件。使用 clang -g 参数也可以生成,指令为 `clang -g -c main.m -o main.o`
+
+第三步:查看 Mach-O 中,包含 `__DWARF` 的段,使用指令 `objdump --macho --private-headers main.o`
+
+
+
+第四步:编译成可执行文件,指令为 `clang -g main.m -o main`
+
+第五步:查看可执行文件中是否包含 `__DWARF` 段。`objdump --macho --private-headers main`
+
+
+
+第六步:查看可执行文件的符号表。指令为:`nm -pa main`。红色区域代表调试符号。
+
+
+
+
+
+
+
+看到了调试符号和 DWARF 信息,那如何生成 .dSYM 文件?
+
+- 指令 `clang -g1 main.m -o main`,参数 `-g1` 用于生成 `.dSYM` 文件。
+- 指令 `dwarfdump main.dSYM` 用于查看 `.dSYM` 文件
+
+
+
+
+
+
+
+- 读取 `debug map`
+- 从 `.o` 文件中加载 DWARF
+- 重新定位所有地址
+- 最后将全部的 DWARF 打包成 `.dSYM` bundle
+
+
+
+结论:
+
+编译器会在编译阶段,把调试信息放在单独的 `__DWARF` 段中。当去链接的时候,会把 `__DWARF` 段删掉,同时把所有的调试信息放在符号表中。
+
+打包上线的时候会把调试符号等裁剪掉,但是线上统计到的堆栈我们仍然要能够知道对应的源代码,这时候就需要把符号写到另外一个单独的文件里,这个文件就是DSYM。
+
+
+
+#### .dSYM 符号化
+
+第一步:新建 iOS 项目,Buidl Setting 切换为 `DWARF with dSYM File ` 。设置模拟 crash(数组越界)
+
+第二步:Mac 自带 `console` App 查看崩溃报告,因为有 `.dSYM` 文件,所以可以看到方法信息
+
+
+
+
+
+思考:假设线上 crash 了,如何根据 crash 堆栈中没有符号化的地址,找到符号的真实地址?
+
+```shell
+Last Exception Backtrace:
+0 CoreFoundation 0x7ff80042889b __exceptionPreprocess + 226
+1 libobjc.A.dylib 0x7ff80004dba3 objc_exception_throw + 48
+2 CoreFoundation 0x7ff800304c83 -[__NSArray0 objectEnumerator] + 0
+3 DSYMDemo 0x1003f4f16 -[ViewController visitElement] + 54 (ViewController.m:26)
+4 DSYMDemo 0x1003f4e64 __29-[ViewController viewDidLoad]_block_invoke + 36 (ViewController.m:20)
+5 libdispatch.dylib 0x7ff80013ca3a _dispatch_client_callout + 8
+6 libdispatch.dylib 0x7ff80013fe87 _dispatch_continuation_pop + 715
+7 libdispatch.dylib 0x7ff80015534a _dispatch_source_invoke + 2046
+8 libdispatch.dylib 0x7ff80014c1e9 _dispatch_main_queue_drain + 1015
+9 libdispatch.dylib 0x7ff80014bde4 _dispatch_main_queue_callback_4CF + 31
+10 CoreFoundation 0x7ff800387b1f __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
+11 CoreFoundation 0x7ff800382436 __CFRunLoopRun + 2482
+12 CoreFoundation 0x7ff8003816a7 CFRunLoopRunSpecific + 560
+13 GraphicsServices 0x7ff809cb128a GSEventRunModal + 139
+14 UIKitCore 0x107850ad3 -[UIApplication _run] + 994
+15 UIKitCore 0x1078559ef UIApplicationMain + 123
+16 DSYMDemo 0x1003f51be main + 110 (main.m:17)
+17 dyld_sim 0x1006312bf start_sim + 10
+18 dyld 0x10deea52e start + 462
+// ...
+
+Binary Images:
+ 0x7ff836115000 - 0x7ff83614cfff libsystem_kernel.dylib (*) /usr/lib/system/libsystem_kernel.dylib
+ 0x7ff83616e000 - 0x7ff836179ff7 libsystem_pthread.dylib (*) /usr/lib/system/libsystem_pthread.dylib
+ 0x7ff8000b5000 - 0x7ff800139ff7 libsystem_c.dylib (*) <8a60f5c1-ea1f-352b-b778-967be44e3677> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/system/libsystem_c.dylib
+ 0x7ff800248000 - 0x7ff80025dffb libc++abi.dylib (*) /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libc++abi.dylib
+ 0x7ff80002c000 - 0x7ff80005ffe9 libobjc.A.dylib (*) <2a7a213a-fdb2-311c-81d7-efdfd9ddf25a> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libobjc.A.dylib
+ 0x7ff80013a000 - 0x7ff800185ff3 libdispatch.dylib (*) <59be51c1-e9f3-3a60-8108-cd70ae082897> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/system/libdispatch.dylib
+ 0x7ff800303000 - 0x7ff80068bffc com.apple.CoreFoundation (6.9) <2be0f79f-8b25-3614-9e7e-dbac565f72dd> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
+ 0x7ff809cae000 - 0x7ff809cb5ff2 com.apple.GraphicsServices (1.0) <16365e42-1d5c-363d-84d1-3bb290a43253> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/GraphicsServices.framework/GraphicsServices
+ 0x106a0f000 - 0x1084dafff com.apple.UIKitCore (1.0) /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore
+ 0x1003f3000 - 0x1003f6fff com.unix.kernel.DSYMDemo (1.0) /Users/USER/Library/Developer/CoreSimulator/Devices/5E0D0385-812D-4FE6-83F6-FFE74E964106/data/Containers/Bundle/Application/474123CE-45D0-45A4-A4B1-EC7588A849AB/DSYMDemo.app/DSYMDemo
+ 0x10062f000 - 0x10068efff dyld_sim (*) <6fb74554-3370-3677-93d4-7f7a01ea6a80> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/dyld_sim
+ 0x10dee5000 - 0x10df50fff dyld (*) <499010ac-3054-326e-a050-fefffb5ce89a> /usr/lib/dyld
+ 0x7ff8006fe000 - 0x7ff80102eff4 com.apple.Foundation (6.9) <86cd050d-44fc-3045-a1f3-8ad5047b329e> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Foundation.framework/Foundation
+```
+
+分析:
+
+- iOS 存在 ASLR 技术,所以当发生 crash 的时候,获取到的符号是已经经过 dyld 启动加载,赋予过偏移量之后的地址
+- `.dSYM` 文件保存的地址是偏移后的地址。因为虚拟地址的 dyld 启动为了安全才做的随机偏移,且 `.dSYM` 文件是编译阶段就生成的,所以不可能是虚拟地址,一定是偏移地址。
+- 符号表存储了当前文件的符号信息,静态链接器(ld) 和动态链接器(dyld) 在链接的过程中都会读取符号表,另外调试器也会用符号表来把符号映射到源文件。
+- 打包上线的时候会把调试符号等裁剪掉,但是线上统计到的堆栈我们仍然要能够知道对应的源代码,这时候就需要把符号写到另外一个单独的文件里,这个文件就是 DSYM。
+- 符号化,什么是符号化?依靠符号表根据地址找对应的符号名、代码文件名的这个过程就叫符号化
+-
+- 但是符号表中记录的信息是未经 ASLR 的,也就是根据偏移地址来记录的。所以 crash 发生拿到的地址,需要计算出原始地址(原始地址也是相对 Mach-O 的地址)才可以根据符号表拿到原始符号、文件信息
+- 怎么计算原始地址?假设业务代码(也就是上面的 DSYMDemo)发生 crash,crash 地址为 `0x1003f4f16`,crash 报告最下面也列出了所以来的镜像(Mach-O),也提供了 base 地址。Mach-O 的开始地址为: `0x1003f3000 - 0x1003f6fff com.unix.kernel.DSYMDemo` 。则 ASLR 前的地址为:`0x1003f4f16 - 0x1003f3000`。计算后再去符号表查找对应的符号、文件信息
+- 然后利用 `dwarfdump --lookup 地址 --arch 架构 DSYMDemo.app.dSYM` 来找到相关信息,里面有符号名、文件名、代码行数
+
+
+
+#### 计算原始地址
+
+```objective-c
+#import
+#import
+
+uintptr_t get_slide_address(void) { // 获取 ASLR
+ uintptr_t vmAddress_slide = 0;
+ // 遍历所有加载过的 image,包括 ipa中的可执行文件 + 依赖的动态库
+ for (uint32_t i = 0; i < _dyld_image_count(); i++) {
+ const char *imageName = (char *)_dyld_get_image_name(i);
+ const struct mach_header *header = _dyld_get_image_header(i);
+ if (header->filetype == MH_EXECUTE) {
+ // 获取 image 当前的偏移地址。偏移是基于该符号所在 image 的
+ vmAddress_slide = _dyld_get_image_vmaddr_slide(i);
+ }
+ NSString *str = [NSString stringWithUTF8String:imageName];
+ if ([str containsString:@"DSYMDemo"]) {
+ NSLog(@"image name %s at address 0x%llx and ASLR slide 0x%lx.\n", imageName, (mach_vm_address_t)header, vmAddress_slide);
+ }
+ }
+ return vmAddress_slide;
+}
+
+- (void)getMethodVMAddress {
+ // 获取 sel 的 ASLR 后的地址,因为启动后经过 dyld 做了偏移
+ IMP imp = class_getMethodImplementation(self.class, @selector(visitElement));
+ unsigned long imppos = (unsigned long)imp;
+ unsigned long slide = get_slide_address();
+ // 符号的真实地址,在 ASLR 技术作用下,基于 Mach-O 的一个偏移地址。所以真实地址 = 经过 runtime 拿到的 imp 地址 - 当前 image 的起始地址
+ unsigned long addr = imppos - slide;
+ NSLog(@"%lu", addr);
+}
+```
+
+拿到真实地址,然后利用 `dwarfdump --lookup 0x0000000100001ce0 DSYMDemo.app.dSYM` 找到对应的信息,可以看到
+
+- 符号名
+- 符号所在代码文件
+- 符号所在代码文件的多少行
+
+
+
+
+
## ASLR
### 未使用 ASLR 的问题
@@ -208,13 +3263,13 @@ Xcode 中可以修改指令集。由 Build Settings 的2个配置决定: `Arch
也可以使用 `size -l -m -x DDD` 指令来查看 Mach-O 的内存分布
-
+
利用 MachOView 查看如下:
-
+
-
+
@@ -236,7 +3291,7 @@ Xcode 中可以修改指令集。由 Build Settings 的2个配置决定: `Arch
-
+
@@ -248,7 +3303,7 @@ File Size:在 Mach-O 文件中的占据的大小
从下面的图可以看出,`_PAGEZERO` 在真实的 Mach-O 文件中不存在,不占据大小。只在虚拟内存中存在。
-
+
@@ -262,7 +3317,7 @@ File Size:在 Mach-O 文件中的占据的大小
Address Space Layout Randomization,地址空间布局随机化。是一种针对缓冲区溢出的安全保护技术,通过堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码的位置,达到阻止溢出攻击目的的一种技术。在 iOS 4.3 引入。
-
+
diff --git a/Chapter5 - Network/5.1.md b/Chapter5 - Network/5.1.md
index b24fb8e..dc52c95 100644
--- a/Chapter5 - Network/5.1.md
+++ b/Chapter5 - Network/5.1.md
@@ -143,6 +143,87 @@ TCP 会利用另一种机制来解决超时重传带来的时间等待问题,
- 当 RW < CW 时,速度由 RW 决定
+
+
+## HTTP 特点及解决方案
+
+### 无连接
+
+因为 HTTP 无连接,客户端和服务端交互的时候,打开一个 TCP 连接,然后交互,然后关闭 TCP 连接。下次需要交互的时候,继续打开一个 TCP 连接,继续交互,最后又关闭 TCP 连接。
+
+
+
+
+
+为了解决该问题,HTTP 推出持久连接方案。
+
+涉及哪些头部字段?
+
+- `Connection: keep-alive`:客户端期许使用持久连接
+- `time: 20`:20s 内不会进行四次挥手关闭。20s 内发送请求会复用之前的 TCP 连接
+- `max: 10`:最多可以发送多少个请求和响应对。
+
+怎么样判断一个请求是否结束了?2个方案
+
+- 服务端响应的时候会在 header 中携带 `Content-length: 1024` 如果接受到的数据字节数大小,等于 `Content-length` 则说明已经全部接受完毕。
+- `chunked` :使用 post 请求的时候,服务端返回给客户端是通过多个块返回的。每个报文都带有 chunked 这个字段,最后一个报文会带一个空的 chunked。
+
+
+
+### 无状态 - Cookie/Session
+
+Session、Cookie 都是针对 HTTP 协议无状态特点的补偿。
+
+
+
+#### Cookie
+
+Cookie 主要用来记录用户状态,区分用户;状态保存在客户端。
+
+
+
+客户端请求服务端的时候,服务端生成 Cookie,通过 HTTP 响应报文,Header 部分中 `Set-Cookie` 首部字段设置 Cookie。
+
+- 客户端发送的 Cookie 在 HTTP 请求报文的 `Cookie` 首部字段中
+- 服务端设置 HTTP 的响应报文 `Set-Cookie` 首部字段
+
+
+
+怎么修改 Cookie?
+
+新 Cookie 覆盖旧 Cookie。
+
+覆盖规则:name、path、domain 等需要与原 Cookie 一致。
+
+怎么删除 Cookie?
+
+- 新 Cookie 覆盖旧 Cookie。覆盖规则:name、path、domain 等需要与原 Cookie 一致。
+- 设置 Cookie 的过期时间在过去。比如 `expires = 过去的一个时间点`,或者 `maxAge = 0`
+
+怎么保证 Cookie 的安全?
+
+- 对 Cookie 进行加密处理
+- 只在 HTTPS 上携带 Cookie
+- 设置 Cookie 为 httponly,防止跨站脚本攻击
+
+
+
+#### Session
+
+Session 也用来记录用户状态,区分用户。只不过状态保存在服务端。
+
+Session 需要依赖于 Cookie 机制。
+
+
+
+
+
+
+
+
+
+
+
## HTTP 缓存控制
缓存(Cache)是计算机领域里的一个重要概念,是优化系统性能的利器。
@@ -247,7 +328,7 @@ ETag 即 Entity Tag,代表资源的唯一标识。主要用来解决修改时
代理体现在头信息上就是字段 `Via`,是一个通用字段,请求头或响应头里都可以出现。每当报文经过一个代理节点,代理服务器就会把自身的信息追加到字段的末尾,就像是经手人盖了一个章。如果通信链路中有很多中间代理,就会在 Via 里形成一个链表,这样就可以知道报文究竟走过了多少个环节才到达了目的地。
-
+
`X-Forwarded-For` 的字面意思是“为谁而转发”,形式上和“Via”差不多,也是每经过一个代理节点就会在字段里追加一个信息。但“Via”追加的是代理主机名(或者域名),而“X-Forwarded-For”追加的是请求方的 IP 地址。所以,在字段里最左边的 IP 地址就是客户端的地址
@@ -284,7 +365,7 @@ Host: www.xxx.com\r\n
HTTP 传输链路上,不只是客户端有缓存,服务器上的缓存也是非常有价值的,可以让请求不必走完整个后续处理流程,"就近"获得响应结果。特别是对于那些“读多写少”的数据,例如突发热点新闻、爆款商品的详情页,一秒钟内可能有成千上万次的请求。即使仅仅缓存数秒钟,也能够把巨大的访问流量挡在外面,让 RPS(request per second)降低好几个数量级,减轻应用服务器的并发压力,对性能的改善是非常显著的。
-
+
代理服务器没有缓存的时候:代理服务器每次直接转发来自客户端的报文给服务端,转发服务端的报文给客户端,中间不会存储任何数据,只有基础的中转功能。
@@ -316,7 +397,7 @@ HTTP 传输链路上,不只是客户端有缓存,服务器上的缓存也是
下图是完整的服务器端缓存控制策略,可以同时控制客户端和代理
-
+
### 客户端的缓存控制
@@ -374,6 +455,124 @@ Range:bytes=20-39 //:从第20个字节到第39个字节之间的数据
- 先发送一个 HEAD 方法的请求,知道总文件大小(Content-Length 就是总字节大小)
- 多线程下载(线程1:Range:bytes=0-100,线程2:Range:bytes=100-200,...)
+
+
+## Charles 抓包原理
+
+抓包原理其实就是:HTTP 中间人攻击。
+
+
+
+
+
+## DNS 解析
+
+域名到 IP 地址的映射,DNS 解析请求采用 UDP 数据报,且明文传输。
+
+
+
+### DNS 解析方式
+
+DNS 解析查询方式:
+
+- 递归查询,核心就是“我去给你问一下”
+
+
+
+ 客户端根据网址去请求服务器之前,会先获取 IP 地址信息。
+
+ - 先去本地 DNS 服务器,本地 DNS 服务器可以处理结果(根据域名对应到 IP 数据)则直接返回给客户端
+ - 如果不能解析,则会去请求根域 DNS 服务器,根域 DNS 告诉本地 DNS“你先等一下,我去问问顶级 DNS”
+
+- 迭代查询,核心就是“我告诉你谁可能知道”
+
+
+
+ - 客户端发送请求的时候问一下本地 DNS 服务器,该域名对应的 IP 地址是什么,本地 DNS 服务器说“我不知道,你去问问根域名服务器,它可能知道”
+
+ - 然后客户端去问根域 DNS 服务器,根域 DNS 服务器说“我也不知道,你去问问顶级 DNS 服务器,它可能知道”
+
+ - 然后客户端去问顶级 DNS 服务器,顶级 DNS 服务器说“我也不知道,你去问问权限 DNS 服务器,它可能知道”
+
+ - 然后权限 DNS 服务器把域名对应的 IP 告诉客户端,客户端拿到 IP 后去请求
+
+
+
+#### DNS 解析存在哪些常见问题
+
+最容易遇到:DNS 劫持问题、DNS 解析转发问题
+
+#### DNS 劫持问题
+
+##### 什么是 DNS 劫持
+
+
+
+
+
+由于 DNS 解析是采用 UDP 数据包、明文传输,所以很容易遇到中间人攻击,也就是 DNS 劫持。
+
+
+
+QA:DNS 劫持和 HTTP 的关系是什么?
+
+**DNS 劫持和 HTTP 是没有关系的**。
+
+- DNS 解析是发生在 HTTP 连接建立前
+- DNS 解析请求采用 UDP 数据报,端口为53
+
+
+
+
+
+##### 如何解决 DNS 劫持问题
+
+- httpDNS
+
+ 从“使用 DNS 协议向 DNS 服务器的53端口请求” 变成“使用 HTTP 协议向 HTTP 服务器的80端口请求”
+
+
+
+ 客户端通过 IP 直连的方式,向 DNS 服务器,通过 HTTP Get 请求的方式,携带域名参数,然后响应一个具体的 IP 地址值给客户端。剩余流程就是拿着请求后的 IP 地址去完成其他逻辑。
+
+- 长连接
+
+
+
+ 在客户端和业务服务器之间,建立一个长连接 Server,可以理解成代理服务器。
+
+ 客户端和长连接 Server 建立一个长连接通道。客户端可以发送一个 HTTP 请求,通过长连通道将 HTTP 请求发送给长连 Server。
+
+ 长连 Server 可以通过内网专线的方式,进行 HTTP 的请求和响应。也就是说 DNS 解析这步还存在,只不过是发生在内网专线阶段,避免了在外部公网阶段,DNS 劫持的问题。
+
+
+
+
+
+#### DNS 解析转发
+
+
+
+比如某移动 App 发起网络请求,移动 DNS 服务器为了节省资源,将请求转发到某电信 DNS 服务器,用于帮助移动 DNS 服务器,解析域名,获取对应的 IP 地址。
+
+这个电信 DNS 服务器,会向权威 DNS 服务器去请求解析域名对应的 IP 地址。
+
+权限 DNS 会根据不同运营商请求情况(网络请求)的流量调度分发:
+
+移动返回是2.2.2.2,电信返回是3,3,3,3,这种情况下,客户端在通过电信网络请求移动 DNS 服务器,进而转发到电信 DNS 服务器之后,权威 DNS 会返回3.3.3.3,也就是客户端在移动环境,由于 DNS 解析转发,而返回的 DNS 是3.3.3.3的电信环境,造成了跨网访问的效率问题
+
+
+
+
+
+
+
+
+
+
+
+
+
## HTTPS 安全
HTTP 的缺点之一就是:明文 + 不安全。为此诞生了 HTTPS 协议。
@@ -1115,4 +1314,4 @@ HTTP/3 没有指定默认的端口号,也就是说不一定非要在 UDP 的 8
## 参考资料
-- [HTTP/2 协议-HPACK(HTTP2 头部压缩算法)原理介绍_爱因诗贤的博客-CSDN博客_hpack算法](https://blog.csdn.net/qq_38937634/article/details/111410191)
\ No newline at end of file
+- [HTTP/2 协议-HPACK(HTTP2 头部压缩算法)原理介绍_爱因诗贤的博客-CSDN博客_hpack算法](https://blog.csdn.net/qq_38937634/article/details/111410191)
\ No newline at end of file
diff --git a/Chapter5 - Network/5.2.md b/Chapter5 - Network/5.2.md
index b5a2cee..2397702 100644
--- a/Chapter5 - Network/5.2.md
+++ b/Chapter5 - Network/5.2.md
@@ -1,6 +1,12 @@
# TCP、UDP 的比较
> 网络协议是每个工程师都需要了解和掌握的知识。 TCP/IP 中有2个最具代表性的传输层协议:TCP、UDP。
+>
+> 记住这句话:TCP 是面向字节流协议、UDP 是面向报文协议。主要是因为发送方的发送消息的机制不同。
+>
+> 下面的内容会探索这些。
+
+
## 一、TCP/IP 网络模型
@@ -52,7 +58,13 @@ UDP 不止支持一对一的传输方式,同样支持一对多,多对多,
### 3.UDP是面向报文的
-发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文
+发送方的 UDP 对应用程序交下来的报文,在添加首部后就向下交付 IP 层。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文
+
+
+
+
+
+UDP 在发送消息时,在传输层直接就将一个消息打包成一个完整的包,组装好 UDP 头部,不进行切割,就转发给网络层。也就是每一个UDP 报文就是一个消息。服务端在接收到 UDP 报文时,会将它放到一个队列中,一个元素就是一个 UDP 报文。每次读取时,读取一个元素
### 4.不可靠性
@@ -75,6 +87,31 @@ UDP 不止支持一对一的传输方式,同样支持一对多,多对多,
因此 UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的
+
+
+
+### 功能
+
+#### 复用、分用
+
+复用:UDP 多端口复用指的是在 UDP 通信中,可以通过一个 UDP 端口同时处理多个不同的应用程序或服务的通信。这种技术允许多个应用程序共享同一个 UDP 端口进行通信,而不需要为每个应用程序分配独立的端口。通过在接收数据时区分不同的目标端口,可以实现对多个应用程序的数据传输和处理。这种方式可以提高网络资源的利用率和简化网络配置,但需要确保在接收端能够正确解析和处理来自不同端口的数据。
+
+
+
+
+
+分用:UDP 多端口分用是指在 UDP 通信中,可以通过一个应用程序或服务同时监听和处理多个不同的 UDP 端口。这种技术允许一个应用程序在同一时间接收来自多个不同 UDP 端口的数据包,并根据端口信息来区分和处理这些数据。通过 UDP 多端口分用,一个应用程序可以灵活地处理多个 UDP 端口上的数据,实现更高效的网络通信和数据处理
+
+
+
+#### 差错检测
+
+以16位字位一个单元,按二进制反码计算出这些16位字的和。将和的二进制反码写入到检验和位。
+
+IM 参考 UDP 差错检测,判断消息有没有被篡改。
+
+
+
## 三、TCP
当一台计算机想要与另一台计算机通讯时,两台计算机之间的通信需要畅通且可靠,这样才能保证正确收发数据。例如,当你想查看网页或查看电子邮件时,希望完整且按顺序查看网页,而不丢失任何内容。当你下载文件时,希望获得的是完整的文件,而不仅仅是文件的一部分,因为如果数据丢失或乱序,都不是你希望得到的结果,于是就用到了TCP。
@@ -97,30 +134,277 @@ HTTP/1.0 为每次 HTTP 请求/相应都建立一条新的 TCP 链接。因此
- 第三次握手:客户端收到服务端 ACK+SYN 包,需要向服务端确认。确认报文段的 ACK 置为1(建立连接,2端的ACK都为1),确认号 **ack=y+1**,而自己的序号 **seq=x+1**。客户端和服务端进入 **ESTABLISHED** (TCP 连接成功)状态,完成三次握手。
+
+#### 为什么需要3次握手?
+
+
+
+
+
+假设只有2次握手流程。客户端先给服务端发送了1个 SYN 报文,不巧的是,此时网络不稳定,该报文没有到达服务端,由于超过了超时时间,启用重传策略,重新发送一个 SYN 报文。
+
+当服务端收到一个 SYN 报文后,回复给客户端一个 ACK 同步确认报文。如果只有2步握手,此时说明 TCP 连接已经建立了。假设前面超时的 SYN 报文,在服务端收到来自客户端的 SYN 报文后再发送了一个 ACK 确认报文时间之后到达服务端,对于服务端来说,会认为客户端又要建立一个 TCP 连接,也就是存在2个 TCP 连接。但实际上第一个 SYN 超时后,启动重传机制发送了第二个 SYN 报文,客户端实际上只希望建立1个 TCP 连接。
+
+有了3次握手就可以解决该问题。
+
+上面背景没有变。当客户端收到来自服务端的 ACK 之后,客户端再发送给服务端一个 ACK 确认报文。之后由于超时才到达服务端的 ACK 报文到达了服务端,服务端收到 SYN,会立马发送一个 ACK 、SYN 报文,一段时间过后,发现客户端并没有给服务端发送一个 ACK 报文。则服务端可以认为是后来的这个 SYN 是由于超时机制发送的报文,客户端并没真正想建立连接。
+
+所以需要3次握手,用来规避同步请求报文超时而建立重复(无用)连接的场景。
+
+可能导致客户端重复发起连接请求,从而浪费网络资源和服务器端的资源。同时,也可能造成服务器误认为客户端请求建立连接,而客户端并没有真正发起连接请求,从而造成服务器端资源浪费。因此,三次握手能够更好地确保TCP连接的可靠性。
+
+
+
### 四次挥手

- 第一次挥手:客户端应用进程先向其 TCP 发出连接释放报文段,并停止发送数据,主动关闭 TCP 连接(此时关闭的是自己与客户端的连接)。FIN 置为1,序号 seq=u,它等于前面已传送过的数据的最后一个字节的序号加1。这时客户端进入 **FIN_WAIT_1** (终止等待1)状态,等待服务端的确认。TCP 规定,FIN 报文段即使不携带数据段,它也会消耗掉一个序号。
-- 第二次挥手:服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。发出确认,确认号 ack=u+1,序号为 seq=v,等于服务端前面已经传输过的数据的最后一个字节的序号加1。然后服务端就进入了 CLOSE_WAIT(关闭等待)状态。TCP 服务器进程通知高层应用数据,因而从客户端到服务端这个芳香的连接就释放了。此时 TCP 处于半关闭状态。即客户端已经没数据要发送了,服务端还在发送数据,但是客户端仍需接收数据。也就是说从服务端到客户端的连接尚未关闭
+- 第二次挥手:服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。发出确认,确认号 ack=u+1,序号为 seq=v,等于服务端前面已经传输过的数据的最后一个字节的序号加1。然后服务端就进入了 CLOSE_WAIT(关闭等待)状态。TCP 服务器进程通知高层应用数据,因而从客户端到服务端这个方向的连接就释放了。此时 TCP 处于**半关闭状态**。即客户端已经没数据要发送了,服务端还在发送数据,但是客户端仍需接收数据。也就是说从服务端到客户端的连接尚未关闭
- 第三次挥手:客户端收到来自服务端的消息后进入了 FIN_WAIT_2 (终止等待2)状态。等待服务端发出的连接释放报文段。若服务端已经没有要向客户端发送数据,其应用进程就通知 TCP 释放连接。这时候服务端发出的报文段必须 FIN=1。假设服务端序号为 W(在在关闭状态下服务端可能又发送了一些数据,因此与 V 有一定距离)。服务端还必须重复上次发送过的确认号 ack=u+1。这时服务端就进入 LAST_ACK(最后确认)状态,等待客户端的确认
- 第四次挥手:客户端收到服务端发送的连接释放报文段后,必须对此发出确认。在确认报文段中把 ACK 置1,确认号为 ack=w+1,而自己的序列号 seq=u+1(根据 TCP 标准,前面发送过的 FIN 报文段要消耗一个序号)。然后进入到 TIME_WAIT(时间等待)状态。现在 TCP 还没有释放掉。必须经过时间等待计时器设置的时间 2MSL 后,客户端才可以进入到 CLOSED 状态。时间 MSL 叫做最长报文段寿命。
+
+#### 为什么需要4次挥手
+
+
+
+
+
+首先客户端给服务端发送一个终止 FIN 报文,服务端收到后,同时也发送一个 ACK 确认报文。此时处于半关闭状态。客户端已经不能给服务端发送数据了,但服务端可以给客户端发送数据。
+
+过了一段时间(服务端数据发送完了),服务端主动发送一个只终止确认报文来断开服务端到客户端方向的连接。客户端收到后,发送一个 ACK 确认报文给服务端。
+
+有2个方向的连接断开,是因为 TCP 是全双工的。同样一条通道,客户端可以给服务端发送,服务端也可以给客户端发送。
+
+
+
### TCP 协议的特点
-- 面向连接
- 面向连接,是指发送数据之前必须在两端建立连接。建立连接的方法是三次握手,这样能建立可靠的连接。建立连接是为数据的可靠传输打下了基础。
-- 仅支持单向传输
- 每条 TCP 传输连接只能有2个端点,只能进行点对点的数据传输,不支持多播和广播传输方式
-- 面向字节流
- TCP 不像 UDP 那样一个个报文独立传输,而是在不保留报文边界的情况下以字节流方式进行传输
-- 可靠传输
- 对于可靠传输,判断丢包,误码靠的是 TCP 的段编号以及确认号。TCP 为保证报文传输的可靠,给每个包一个序号,同时序号也保证了传输到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认 ACK;如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据将被重传。
-- 提供拥塞控制
- 当网络出现拥塞时,TCP 能够减小向网络注入数据的速率和数量,缓解拥塞
-- TCP 提供全双工通信
- TCP 允许通信双方的应用程序在任何都能发送数据,因为 TCP 连接的两端都设有缓存,用来临时存放双向通信的数据。当然,TCP 可以发送一个数据段,也可以缓存一段时间一边一次发送更多的数据段(最大的数据段大小取决于 MSS)
+#### 面向连接
+
+面向连接,是指发送数据之前必须在两端建立连接。建立连接的方法是三次握手,这样能建立可靠的连接。建立连接是为数据的可靠传输打下了基础。
+
+数据传输结束后,需要释放连接,需经过4次挥手。
+
+#### 可靠传输
+
+对于可靠传输,判断丢包,误码靠的是 TCP 的段编号以及确认号。TCP 为保证报文传输的可靠,给每个包一个序号,同时序号也保证了传输到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认 ACK;如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据将被重传。
+
+- 无差错
+- 不丢失
+- 不重复
+- 按序到达
+
+这些特点在 IM 系统上可以借鉴。
+
+可靠传输在 TCP 层面是依靠“停止等待协议”实现的。
+
+
+
+##### 停止等待协议
+
+> **停止等待协议**:**通信系统中,规定发送方每发送一帧后就要停下来等待接收方的确认返回,仅当接收方确认正确接收后再继续发送下一帧**。
+
+下面分4个不同情况看看 TCP 是如何保证消息的可靠传输的。
+
+- 无差错情况
+
+
+
+ 这张图是正常传输的情况。没有发生任何错误
+
+- 超时重传
+
+
+
+ 这张图是一个超时重传的情况。客户端给服务端发送了 M1 分组报文,由于网络环境比较差,丢失或者滞留,或者被劫持了篡改了,当劫持篡改的情况下服务端接收到 M1 后,会判断篡改后丢弃该报文。在期许的时间内,客户端没有收到分组报文 M1 的确认,认为发生了超时。触发重传机制。然后启动一个分组报文 M1 的重传,此时网络正常,服务端收到 M1,并发送确认给客户端。客户端收到确认报文后,再发送 M2...
+
+ 针对超时的情况,可以在客户端设置一个超时定时器,如果没有在规定时间范围内收到 M1 分组报文的确认报文,则开启重传机制。该机制保证分组报文不丢失,服务端的差错检测,可以保证无差错。
+
+ 这套流程可以保证:**差错校验、不丢失**
+
+- 确认丢失
+
+
+
+ 客户端发送一个分组报文 M1,服务端收到后发送一个确认报文,但这个确认报文丢失了。此时客户端依旧通过超时定时器,判断在期许的时间内没有收到来自服务端的确认报文,触发超时重传策略。重新发送分组报文 M1,服务端收到 M1 后,由于服务端已经接收过分组报文 M1 了,此时服务端做2件事:丢失重传的 M1 报文;重传确认 M1 报文。客户端收到 M1 确认报文,然后继续发送 M2...
+
+- 确认迟到
+
+
+
+ 客户端发送 M1 报文,服务端收到 M1 后发送确认报文,但是由于网络情况不好,传输较慢,客户端在期许时间范围内没有收到确认报文。客户端在超时定时器的作用下,判断超时,触发重传策略。重新发送报文 M1,服务端收到重传的 M1 后,由于服务端之前已经接受过 M1 报文,所以做2件事情:丢弃重传的 M1 报文;重传 M1 确认报文。
+
+ 客户端收到 M1 确认报文后,继续发送 M2...
+
+ 随后的某个时间,来自服务端的 M1 确认报文到达了客户端,客户端收下后不做任何处理。
+
+
+
+#### 面向字节流
+TCP 不像 UDP 那样一个个报文独立传输,而是在不保留报文边界的情况下以字节流方式进行传输。
+
+
+
+发送方在进行数据发送的时候,在 TCP 层面会有一个发送缓存。接收方也有一个接收缓存。
+
+之后发送的每一个数据都会经过 TCP 连接的逻辑通道传输给接收方。假设发送方要发送13、14,在放到发送缓存之后,至于每次要发送多少个字节是由 TCP 自己控制的。
+
+5、6可能是发送方2次要传送的数据,放到发送缓存区后,TCP 将5、6拼接成一个 TCP 报文一次性发送给接收方。
+
+不管发送方一次性提交给 TCP 缓存多大的数据,TCP 都会根据实际情况来进行划分,比如一次性可以传输几个字节。可能会将发送方的一个数据拆分为多个包进行传输。
+
+
+
+当 TCP 在传输层发送消息时,一个消息可能会被分割成多个 TCP 报文进行转发给网络层。我们不能认为一个 TCP 报文就是一个消息,所以说 TCP 是面向字节流协议。
+
+由于一个消息对应的不是一个 TCP 报文,如果接收方不知道一个消息的长度或者分割的边界在哪里,就会无法组装成一个消息,这就引入了**粘包问题**
+
+
+
+#### 流量控制(滑动窗口)
+
+流量控制是通过滑动窗口协议来实现的。
+
+将数据划分为4种状态:
+
+- 发送并被确认
+- 发送未被确认
+- 需要尽快发送的
+- 等待发送的
+
+其中,发送未被确认的、需要尽快发送的,组成了 TCP 的窗口(因为这些部分的数据,需要核心管理和处理)。发送窗口要比发送缓存要小。
+
+从左到右,字节编号,序号逐渐增大。
+
+
+
+- 可用窗口,主要是尽快要发送的部分
+- 重传队列,主要是发送但未被确认的部分
+
+当发送但未被确认的部分,收到确认之后,窗口将进行合拢。假设 7、8、9 被发送后,变成下面的状态
+
+
+
+7、8、9变成了发送未被确认的状态,10、11、12变成了需要尽快发送的状态,窗口右移。
+
+
+
+假设发送方处于 5G 网络,接收方处于 3G 网络,如果发送方发送太快会导致接收方接收缓存产生大量积累数据,所以需要接收方动态调整发送方发送窗口来决定发送速率。
+
+
+
+
+
+
+
+在接收方侧:
+
+
+
+
+
+- 接受侧提交数据,必须是按序到达的部分。而未按序到达的字节,需要等全部按照顺序接收完毕,保证有序后才可以提交
+- 按序到达可以通过字节序号进行控制
+
+
+
+#### 提供拥塞控制
+
+当网络出现拥塞时,TCP 能够减小向网络注入数据的速率和数量,缓解拥塞
+
+- 慢启动:慢开始、拥塞避免。
+- 快恢复、快重传
+
+
+
+##### 慢开始、拥塞避免
+
+
+
+
+
+工作流程:
+
+- 一开始先发送1个报文,发现没有拥塞,就发送2个报文,发现依旧没有拥塞,继续翻倍,1、2、4、8、16。这个过程就是指数规律增长的过程,称为慢开始算法。
+- 当增长到窗口门限值16的时候,会采用拥塞避免策略,来进行发送报文数量的增长。比如发送17后发现没有拥塞、则增长为18,依次线性增长。
+- 当增长到24的时候,可能会发生网络拥塞。界定网络拥塞比较负责,我们可以简单的视为当连续3个报文的 ACK 没有收到的时候则认为发生了拥塞。拥塞后采用“乘法减小”的策略,来恢复到只发送1个报文的情况,来减少网络层压力。然后重新慢开始。同时将网络拥塞窗口值减少为之前的一半(比如之前的24,现在为12)
+- 调整后继续按照慢开始算法,比如:一开始先发送1个报文,发现没有拥塞,就发送2个报文,发现依旧没有拥塞,继续翻倍,1、2、4、8、16。这个过程就是指数规律增长的过程,称为慢开始算法。
+- 当达到门限值16之后,采用拥塞避免,加法增大的机制。比如:16、17、18...
+
+
+
+##### 快重传
+
+TCP 实现可靠传输依赖的是 **超时重传** 机制。TCP 在发送完数据后,会启动一个定时器。如果在定时器超时前没收到接收方发来的 ACK 确认,就重传数据。
+
+这个是默认的情况,但是传输效率还是有点低,所以 TCP 为了更高的效率,采取了快重传机制。什么是快重传?
+
+
+
+- 左边是发送方,右边是接收方
+- 发送了序号为1的报文后,接收方收到,回复了一个 ACK2 的报文,ACK2 的意思就是“我收到报文1了,接下来我希望收到序号为2的报文”
+- 发送方发送序号为2的报文后,可能网络不好,也可能丢包了,接收方还没收到
+- 发送方发送序号为3的报文后,接收方收到了,但此时序号为2的报文还未收到,只能回复一个 ACK2,表示我接下来还是希望收到一个需要为2的报文
+- 发送方发送序号为4的报文后,可能网络不好,也可能丢包了,接收方还没收到
+- 发送方发送序号为5的报文后,接收方收到了,但此时序号为2的报文还未收到,只能回复一个 ACK2,表示我接下来还是希望收到一个需要为2的报文
+- 发送方发送序号为6的报文后,接收方收到了,但此时序号为2的报文还未收到,只能回复一个 ACK2,表示我接下来还是希望收到一个需要为2的报文
+
+目前为止,除了第一个正常接受的报文1收到后,接收方回复了一个 ACK2 外,发送方已经收到3个连续的 ACK2,发送方会立即快速重传报文2。请注意,如果没有收到3个连续的 ACK2,发送会等到超时重传触发后(发送报文2的时候,也会启动一个超时计时器,等到 RTO 到了,会超时重传报文2),也会发送报文2。
+
+快速重传,着重在“**快速**” ,相较于超时重传,它更快,更迅速,更能提高 TCP 传输效率。因为发送方收到3次 ACK2 触发快速重传的时候,可能超时重传策略还没触发。
+
+
+
+- 快重传是为了解决在没有触发超时重传的时候,就触发发送方传递了数据
+- 快重传本质上是为了提高传输效率的
+- 快速重传不是以时间驱动,而是以数据驱动。发送方收到三次同样的确认报文后,会立即重传丢失的报文
+
+
+
+##### 快恢复
+
+
+
+
+
+当发生网络拥塞的时候,如果直接变为1,后续会再次经过慢开始(1、2、4、8、16)的过程,大多数情况下,网络可能没那么糟糕,所以不直接变为1,采用一个快速恢复的策略。
+
+把新的拥塞窗口大小设置为新的门限值,12。从12开始不走慢开始(指数增长),而直接走快恢复(加法增大、线性增大)的流程。跳过了从1走指数增长到新的门限值的过程。
+
+
+
+
+
+QA:上面4种策略是互斥的吗?怎么样工作的
+
+慢开始、拥塞避免、快恢复、快重传是同时工作的。
+
+- 一开始传输的时候,走的是慢开始算法,此时还没有到达拥塞避免阶段。此时的网络状态很好。假设此时某个报文丢失了,此时的重传是依靠超时定时器的,也就是说慢开始阶段走的是超时重传逻辑。
+- 等达到门限值之后,走的是拥塞避免算法,线性增大。
+
+
+
+发送窗口大小 = Min(接收窗口 rwnd, 拥塞窗口 cwnd)
+
+
+
+**收到重复的确认,说明发出去的数据发生丢包,意味着网络可能发生拥塞。TCP 同样会将门限阈值(ssthresh) 设为当前拥塞窗口 cwnd 的一半,并重新执行慢启动算法加以应对。**
+
+
+
+#### 仅支持单向传输
+每条 TCP 传输连接只能有2个端点,只能进行点对点的数据传输,不支持多播和广播传输方式
+
+
+
+#### TCP 提供全双工通信
+
+TCP 允许通信双方的应用程序在任何都能发送数据,因为 TCP 连接的两端都设有缓存,用来临时存放双向通信的数据。当然,TCP 可以发送一个数据段,也可以缓存一段时间一边一次发送更多的数据段(最大的数据段大小取决于 MSS)
+
+
## TCP、UDP 对比
diff --git a/Chapter6 - Design Pattern/6.14.md b/Chapter6 - Design Pattern/6.14.md
index 203729e..6d18065 100644
--- a/Chapter6 - Design Pattern/6.14.md
+++ b/Chapter6 - Design Pattern/6.14.md
@@ -1,5 +1,7 @@
# 桥接模式
+
+
## 概念理解
桥接模式也叫作桥梁模式,英文是 Bridge Design Pattern。这个模式可以说是 23 种设计模式中最难理解的模式之一了。我查阅了比较多的书籍和资料之后发现,对于这个模式有两种不同的理解方式。
@@ -84,5 +86,88 @@ public class DriverManager {
实际上,JDBC 本身就相当于“抽象”。注意,这里所说的“抽象”,指的并非“抽象类”或“接口”,而是跟具体的数据库无关的、被抽象出来的一套“类库”。具体的Driver(比如,com.mysql.jdbc.Driver)就相当于“实现”。注意,这里所说的“实现”,也并非指“接口的实现类”,而是跟具体数据库相关的一套“类库”。JDBC 和 Driver 独立开发,通过对象之间的组合关系,组装在一起。JDBC 的所有逻辑操作,最终都委托给 Driver 来执行。
+
+## 业务解耦
+
+假设有这样一个场景:业务有个 TableView 列表,版本迭代的时候,后端的接口不变变化,有 V1、V2、V3,这3个版本需要共存,该怎么设计?
+
+假设列表相关逻辑是 `BaseObjectA`,后端接口相关是 `BaseObjectB`
+
+`BaseObject1` 是因为需要处理接口 V1、V2、V3 而产生的基类,具体子类 ObjectA1 等处理具体逻辑;
+
+```objective-c
+// BaseObjectA
+@interface BaseObjectA : NSObject
+// 桥接模式的核心实现
+@property (nonatomic, strong) BaseObjectB *objB;
+// 获取数据
+- (void)handle;
+@end
+
+@implementation BaseObjectA
+ /*
+ A1 --> B1、B2、B3 3种
+ A2 --> B1、B2、B3 3种
+ A3 --> B1、B2、B3 3种
+ */
+- (void)handle {
+ // override to subclass
+ [self.objB fetchData];
+}
+@end
+
+// ObjectA1
+@interface ObjectA1 : BaseObjectA
+
+@end
+
+@implementation ObjectA1
+- (void)handle {
+ // before 业务逻辑操作
+ [super handle];
+ // after 业务逻辑操作
+}
+@end
+```
+
+`BaseObject2` 是处理数据接口相关的基类,`ObjectB1` 是具体 V1 的实现
+
+```objective-c
+// BaseObjectB
+@interface BaseObjectB : NSObject
+- (void)fetchData;
+@end
+
+@implementation BaseObjectB
+- (void)fetchData {
+ // override to subclass
+}
+@end
+
+@interface ObjectB1 : BaseObjectB
+
+@end
+
+@implementation ObjectB1
+- (void)fetchData {
+ // 具体的逻辑处理
+}
+@end
+```
+
+使用
+
+```objective-c
+ObjectA1 *objA = [[ObjectA1 alloc] init];
+BaseObjectB *b1 = [[BaseObjectB alloc] init];
+objA.objB = b1;
+[objA handle];
+```
+
+桥接模式可以解决这种业务兼容的适配问题。
+
+
+
## 总结
+
桥接模式有两种理解方式。第一种理解方式是“将抽象和实现解耦,让它们能独立开发”。这种理解方式比较特别,应用场景也不多。另一种理解方式更加简单,类似“组合优于继承”设计原则,这种理解方式更加通用,应用场景比较多。
\ No newline at end of file
diff --git a/Chapter6 - Design Pattern/6.16.md b/Chapter6 - Design Pattern/6.16.md
index fa51545..741c50b 100644
--- a/Chapter6 - Design Pattern/6.16.md
+++ b/Chapter6 - Design Pattern/6.16.md
@@ -1,14 +1,48 @@
# 适配器模式
+主要用来解决:一个现有类需要适应变化的问题。
+
+
+
## 适配器模式的原理
适配器模式的英文翻译是 Adapter Design Pattern。顾名思义,这个模式就是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。举个现实的例子:USB 转接头充当适配器,把两种不兼容的接口,通过转接变得可以一起工作
+
+
+
+
## 适配器模式的实现
适配器模式有两种实现方式:
- 类适配器,使用继承关系来实现
- 对象适配器,对象适配器使用组合关系来实现
+
+
+
+### 对象适配器
+
+
+
+假设一个类存在年代久远,如果需要适配,则需要创建一个适配对象。然后被适配的对象以成员变量的形式集成到适配对象中。
+
+
+
+```
+- (void)handleLogic {
+ // 适配逻辑
+ [被适配对象 对象方法];
+ // 适配逻辑
+}
+```
+
+
+
+
+
+
+
举个例子:
+
- ITarget 表示要转化成的接口定义
- Adaptee 是一组不兼容 ITarget 接口定义的接口
- Adaptor 将 Adaptee 转化成一组符合 ITarget 接口定义的接口
@@ -73,9 +107,12 @@ public class Adaptor implements ITarget {
适配器模式的应用场景是“接口不兼容”。那在实际的开发中,什么情况下才会出现接口不兼容呢?
+
+
### 封装有缺陷的接口设计
+
假设我们依赖的外部系统在接口设计方面有缺陷(比如包含大量静态方法),引入之后会影响到我们自身代码的可测试性。为了隔离设计上的缺陷,我们希望对外部系统提供的接口进行二次封装,抽象出更好的接口设计,这个时候就可以使用适配器模式了。
-```
+```java
public class CD { //这个类来自外部sdk,我们无权修改它的代码
//...
public static void staticFunction1() { //... }
@@ -108,12 +145,15 @@ public class CDAdaptor extends CD implements ITarget {
}
}
```
+
+
### 统一多个类的接口设计
+
某个功能的实现依赖多个外部系统(或者说类)。通过适配器模式,将它们的接口适配为统一的接口定义,然后我们就可以使用多态的特性来复用代码逻辑。
假设我们的系统要对用户输入的文本内容做敏感词过滤,为了提高过滤的召回率,我们引入了多款第三方敏感词过滤系统,依次对用户输入的内容进行过滤,过滤掉尽可能多的敏感词。但是,每个系统提供的过滤接口都是不同的。这就意味着我们没法复用一套逻辑来调用各个系统。这个时候,我们就可以使用适配器模式,将所有系统的接口适配为统一的接口定义,这样我们可以复用调用敏感词过滤的代码。
-```
+```java
public class ASensitiveWordsFilter { // A敏感词过滤系统提供的接口
//text是原始文本,函数输出用***替换敏感词之后的文本
public String filterSexyWords(String text) {
@@ -178,10 +218,13 @@ public class RiskManagement {
}
}
```
+
+
### 替换依赖的外部系统
+
当我们把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动。
-```
+```java
// 外部系统A
public interface IA {
//...
@@ -218,17 +261,26 @@ public class BAdaptor implemnts IA {
Demo d = new Demo(new BAdaptor(new B()));
```
+
+
### 兼容老版本
+
在做版本升级的时候,对于一些要废弃的接口,我们不直接将其删除,而是暂时保留,并且标注为 deprecated,并将内部实现逻辑委托为新的接口实现。这样做的好处是,让使用它的项目有个过渡期,而不是强制进行代码修改。这也可以粗略地看作适配器模式的一个应用场景
+
+
### 适配不同格式的数据
+
前面我们讲到,适配器模式主要用于接口的适配,实际上,它还可以用在不同格式的数据之间的适配。比如,把从不同征信系统拉取的不同格式的征信数据,统一为相同的格式,以方便存储和使用。再比如,Java 中的 Arrays.asList() 也可以看作一种数据适配器,将数组类型的数据转化为集合容器类型。
```
List stooges = Arrays.asList("Larry", "Moe", "Curly");
```
+
+
## 代理、桥接、装饰器、适配器 4 种设计模式的区别
+
代理、桥接、装饰器、适配器,这 4 种模式是比较常用的结构型设计模式。它们的代码结构非常相似。笼统来说,它们都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类
尽管代码结构相似,但这 4 种设计模式的用意完全不同,也就是说要解决的问题、应用场景不同,这也是它们的主要区别
diff --git a/Chapter6 - Design Pattern/6.23.md b/Chapter6 - Design Pattern/6.23.md
index d457044..9089154 100644
--- a/Chapter6 - Design Pattern/6.23.md
+++ b/Chapter6 - Design Pattern/6.23.md
@@ -1,7 +1,11 @@
-# 职责链模式
+# 责任链模式
+
+> 经常变化的产品需求该怎么设计?一个人关于需求变更问题的解决方案
+
+
## 定义
-职责链模式的英文翻译是 Chain Of Responsibility Design Pattern。在 GoF 的《设计模式》中,它是这么定义的:
+责任链模式的英文翻译是 Chain Of Responsibility Design Pattern。在 GoF 的《设计模式》中,它是这么定义的:
> Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.
翻译成中文就是:将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止
@@ -165,7 +169,12 @@ public class Application {
```
在 GoF 给出的定义中,如果处理器链上的某个处理器能够处理这个请求,那就不会继续往下传递请求。实际上,职责链模式还有一种变体,那就是请求会被所有的处理器都处理一遍,不存在中途终止的情况。
-## 使用场景
-在之前的文章利用[责任链模式设计了一套校验器](./../Chapter1%20-%20iOS/1.110.md)
-再举个例子敏感词过滤的例子。
+
+## 使用场景
+
+在之前的文章利用[责任链模式设计了一套校验器](./../Chapter1%20-%20iOS/1.110.md)
+
+- Node 的洋葱模型
+- Redux 中间件思想都是责任链的使用场景
+- iOS 事件传递机制、响应链
diff --git a/Chapter6 - Design Pattern/6.28.md b/Chapter6 - Design Pattern/6.28.md
index f72e374..6163e95 100644
--- a/Chapter6 - Design Pattern/6.28.md
+++ b/Chapter6 - Design Pattern/6.28.md
@@ -8,12 +8,18 @@
命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能
+
+
## 使用场景
+
落实到编码实现,命令模式用的最核心的实现手段,是将函数封装成对象。我们知道,C 语言支持函数指针,我们可以把函数当作变量传递来传递去。但是,在大部分编程语言中,函数没法儿作为参数传递给其他函数,也没法儿赋值给变量。借助命令模式,我们可以将函数封装成对象。具体来说就是,设计一个包含这个函数的类,实例化一个对象传来传去,这样就可以实现把函数像对象一样使用。从实现的角度来说,它类似我们之前讲过的回调
当我们把函数封装成对象之后,对象就可以存储下来,方便控制执行。所以,命令模式的主要作用和应用场景,是用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等等,这才是命令模式能发挥独一无二作用的地方。
+
+
## 实现
+
假设我们正在开发一个类似《天天酷跑》或者《QQ 卡丁车》这样的手游。这种游戏本身的复杂度集中在客户端。后端基本上只负责数据(比如积分、生命值、装备)的更新和查询,所以,后端逻辑相对于客户端来说,要简单很多。
为了提高性能,我们会把游戏中玩家的信息保存在内存中。在游戏进行的过程中,只更新内存中的数据,游戏结束之后,再将内存中的数据存档,也就是持久化到数据库中。为了降低实现的难度,一般来说,同一个游戏场景里的玩家,会被分配到同一台服务上。这样,一个玩家拉取同一个游戏场景中的其他玩家的信息,就不需要跨服务器去查找了,实现起来就简单了很多。
@@ -78,7 +84,17 @@ public class GameApplication {
}
```
+
+
+命令模式一般有2个角色:
+
+- CommandManager
+- Command
+
+
+
## 命令模式 VS 策略模式
+
命令模式跟策略模式、工厂模式非常相似啊,那它们的区别在哪里呢?
diff --git a/Chapter6 - Design Pattern/6.9.md b/Chapter6 - Design Pattern/6.9.md
index bf38f5e..2557c24 100644
--- a/Chapter6 - Design Pattern/6.9.md
+++ b/Chapter6 - Design Pattern/6.9.md
@@ -5,6 +5,8 @@
单例与静态类的区别?
有何替代的解决方案?
+
+
## 为什么要使用单例?
创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。其中单例模式、工厂模式、建造者模式、原型模式都是创建型模式。
@@ -207,3 +209,34 @@ SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的
## 如何理解单例模式的唯一性
“一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。定义中提到,“一个类只允许创建唯一一个对象”。那对象的唯一性的作用范围是什么呢?是指线程内只允许创建一个对象,还是指进程内只允许创建一个对象?答案是后者,也就是说,单例模式创建的对象是进程唯一的
+
+
+## iOS 侧单例的实现
+
+- GCD `dispatch_once` 保证执行1次
+
+- 实现 `+ (id)allocWithZone:(struct _NSZOne *)zone { } ` 方法。避免单例对象,调用 copy 方法,产生一个新的对象,打破单例效果
+
+ 实现 `- (id)copyWithZone:(nullable NSZone *)zone { }` 方法,避免单例对象,调用 copy 方法,产生一个新的对象,打破单例效果
+
+
+
+```objective-c
++ (id)sharedInstance {
+ static Mooc *instance = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ instance = [[super allocWithZone:NULL] init];
+ });
+ return instance;
+}
+
++ (id)allocWithZone:(struct _NSZone *)zone {
+ return [self sharedInstance];
+}
+
+- (id)copyWithZone:(nullable NSZone *)zone {
+ return self;
+}
+```
+
diff --git a/Chapter6 - Design Pattern/chapter6.md b/Chapter6 - Design Pattern/chapter6.md
index b41c5aa..4ece18a 100644
--- a/Chapter6 - Design Pattern/chapter6.md
+++ b/Chapter6 - Design Pattern/chapter6.md
@@ -25,7 +25,7 @@
* [20、观察者模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.20.md)
* [21、模板模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.21.md)
* [22、模板模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.22.md)
- * [23、职责链模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.23.md)
+ * [23、责任链模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.23.md)
diff --git a/SUMMARY.md b/SUMMARY.md
index 04afb0e..59ee173 100644
--- a/SUMMARY.md
+++ b/SUMMARY.md
@@ -76,7 +76,7 @@
* [71、Flutter初体验-安装](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.71.md)
* [72、架构设计心得](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.72.md)
* [73、Ruby学习](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.73.md)
- * [74、APM](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.74.md)
+ * [74、带你打造一套 APM 监控系统](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.74.md)
* [75、写好测试,提升应用质量](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.75.md)
* [76、iOS Crash分析](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.76.md)
* [77、iOS 打包系统构建加速](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.77.md)
@@ -110,7 +110,7 @@
* [105、iOS 界面渲染流程](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.105.md)
* [106、NSUserDefault 底层原理探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.106.md)
* [107、IM技术](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.107.md)
- * [108、精准测试](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.108.md)
+ * [108、精准测试最佳实践](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.108.md)
* [109、汇编学习](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.109.md)
* [110、妙用设计模式来设计一个客户端校验器](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.110.md)
* [111、写给 iOSer 的鸿蒙开发 tips](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.111.md)
@@ -209,7 +209,7 @@
* [20、观察者模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.20.md)
* [21、模板模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.21.md)
* [22、模板模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.22.md)
- * [23、职责链模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.23.md)
+ * [23、责任链模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.23.md)
* [24、状态模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.24.md)
* [25、迭代器模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.25.md)
* [26、访问者模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.26.md)
diff --git a/assets/ HTTPContinuesousLink.png b/assets/ HTTPContinuesousLink.png
new file mode 100644
index 0000000..c86cff6
Binary files /dev/null and b/assets/ HTTPContinuesousLink.png differ
diff --git a/assets/ MockCrashAndDiaplayViaConsoleApp.png b/assets/ MockCrashAndDiaplayViaConsoleApp.png
new file mode 100644
index 0000000..2c60a20
Binary files /dev/null and b/assets/ MockCrashAndDiaplayViaConsoleApp.png differ
diff --git a/assets/.DS_Store b/assets/.DS_Store
index 8dd394d..730db4b 100644
Binary files a/assets/.DS_Store and b/assets/.DS_Store differ
diff --git a/assets/1785352-087fd4b664e0e387.png b/assets/1785352-087fd4b664e0e387.png
deleted file mode 100644
index 9089c71..0000000
Binary files a/assets/1785352-087fd4b664e0e387.png and /dev/null differ
diff --git a/assets/AFNetworkingClassArch.png b/assets/AFNetworkingClassArch.png
new file mode 100644
index 0000000..cd38da8
Binary files /dev/null and b/assets/AFNetworkingClassArch.png differ
diff --git a/assets/APINoteInObjcSourceCode.png b/assets/APINoteInObjcSourceCode.png
new file mode 100644
index 0000000..27f07fe
Binary files /dev/null and b/assets/APINoteInObjcSourceCode.png differ
diff --git a/assets/ARCObjectWillReleasedWhenLeaveScope.png b/assets/ARCObjectWillReleasedWhenLeaveScope.png
new file mode 100644
index 0000000..2725058
Binary files /dev/null and b/assets/ARCObjectWillReleasedWhenLeaveScope.png differ
diff --git a/assets/ARCWeakObjectWillReleaseWhenLeaveScope.png b/assets/ARCWeakObjectWillReleaseWhenLeaveScope.png
new file mode 100644
index 0000000..fca9a3f
Binary files /dev/null and b/assets/ARCWeakObjectWillReleaseWhenLeaveScope.png differ
diff --git a/assets/ARCWillCrashWhenCallNewMethodDirectly.png b/assets/ARCWillCrashWhenCallNewMethodDirectly.png
new file mode 100644
index 0000000..5123950
Binary files /dev/null and b/assets/ARCWillCrashWhenCallNewMethodDirectly.png differ
diff --git a/assets/AddMacroForDifferentScheme.png b/assets/AddMacroForDifferentScheme.png
new file mode 100644
index 0000000..7d062b7
Binary files /dev/null and b/assets/AddMacroForDifferentScheme.png differ
diff --git a/assets/AllSymbolStripProcess.png b/assets/AllSymbolStripProcess.png
new file mode 100644
index 0000000..5525372
Binary files /dev/null and b/assets/AllSymbolStripProcess.png differ
diff --git a/assets/AppRunFailedDynamicLibNotLocatedAppDir.png b/assets/AppRunFailedDynamicLibNotLocatedAppDir.png
new file mode 100644
index 0000000..2449b04
Binary files /dev/null and b/assets/AppRunFailedDynamicLibNotLocatedAppDir.png differ
diff --git a/assets/AppUseDynamicLibWithCocoapods.png b/assets/AppUseDynamicLibWithCocoapods.png
new file mode 100644
index 0000000..2e3eb52
Binary files /dev/null and b/assets/AppUseDynamicLibWithCocoapods.png differ
diff --git a/assets/AppUseIndirectDynamicLibProcess.png b/assets/AppUseIndirectDynamicLibProcess.png
new file mode 100644
index 0000000..7520d08
Binary files /dev/null and b/assets/AppUseIndirectDynamicLibProcess.png differ
diff --git a/assets/AssociatedSaveValueInRuntimeStructure.png b/assets/AssociatedSaveValueInRuntimeStructure.png
new file mode 100644
index 0000000..d99d5db
Binary files /dev/null and b/assets/AssociatedSaveValueInRuntimeStructure.png differ
diff --git a/assets/AsyncDisplayKitArch.png b/assets/AsyncDisplayKitArch.png
new file mode 100644
index 0000000..e55b499
Binary files /dev/null and b/assets/AsyncDisplayKitArch.png differ
diff --git a/assets/AsyncRenderProcessAPI.png b/assets/AsyncRenderProcessAPI.png
new file mode 100644
index 0000000..3e84f12
Binary files /dev/null and b/assets/AsyncRenderProcessAPI.png differ
diff --git a/assets/AsyncUILabelRender.png b/assets/AsyncUILabelRender.png
new file mode 100644
index 0000000..178a2dc
Binary files /dev/null and b/assets/AsyncUILabelRender.png differ
diff --git a/assets/AutoVariableCaptureByBlock.png b/assets/AutoVariableCaptureByBlock.png
new file mode 100644
index 0000000..84b6336
Binary files /dev/null and b/assets/AutoVariableCaptureByBlock.png differ
diff --git a/assets/AutoreleasePoolMoreItem.png b/assets/AutoreleasePoolMoreItem.png
index 77b4c99..401e6db 100644
Binary files a/assets/AutoreleasePoolMoreItem.png and b/assets/AutoreleasePoolMoreItem.png differ
diff --git a/assets/AutoreleasePoolPageWithForIterator.png b/assets/AutoreleasePoolPageWithForIterator.png
new file mode 100644
index 0000000..e867238
Binary files /dev/null and b/assets/AutoreleasePoolPageWithForIterator.png differ
diff --git a/assets/BOOLPropertyImplementedByBitOperator.png b/assets/BOOLPropertyImplementedByBitOperator.png
new file mode 100644
index 0000000..f14923f
Binary files /dev/null and b/assets/BOOLPropertyImplementedByBitOperator.png differ
diff --git a/assets/BOOLPropertyImplementedByUnion.png b/assets/BOOLPropertyImplementedByUnion.png
new file mode 100644
index 0000000..c9cbdd5
Binary files /dev/null and b/assets/BOOLPropertyImplementedByUnion.png differ
diff --git a/assets/BOOLProptertImpledByStructureBitDomain.png b/assets/BOOLProptertImpledByStructureBitDomain.png
new file mode 100644
index 0000000..9cf7b0f
Binary files /dev/null and b/assets/BOOLProptertImpledByStructureBitDomain.png differ
diff --git a/assets/BitOperateorOnEnumDemo.png b/assets/BitOperateorOnEnumDemo.png
new file mode 100644
index 0000000..df2900c
Binary files /dev/null and b/assets/BitOperateorOnEnumDemo.png differ
diff --git a/assets/BlockAsFunctionReturnValueIsDangerous.png b/assets/BlockAsFunctionReturnValueIsDangerous.png
new file mode 100644
index 0000000..74b595d
Binary files /dev/null and b/assets/BlockAsFunctionReturnValueIsDangerous.png differ
diff --git a/assets/BlockChangeOuterValue.png b/assets/BlockChangeOuterValue.png
new file mode 100644
index 0000000..4af4dc3
Binary files /dev/null and b/assets/BlockChangeOuterValue.png differ
diff --git a/assets/BlockChangeVariableUse__Block.png b/assets/BlockChangeVariableUse__Block.png
new file mode 100644
index 0000000..c041265
Binary files /dev/null and b/assets/BlockChangeVariableUse__Block.png differ
diff --git a/assets/BlockChangeVariableUse__BlockOnObject.png b/assets/BlockChangeVariableUse__BlockOnObject.png
new file mode 100644
index 0000000..92d8940
Binary files /dev/null and b/assets/BlockChangeVariableUse__BlockOnObject.png differ
diff --git a/assets/BlockClassType.png b/assets/BlockClassType.png
new file mode 100644
index 0000000..4a253ee
Binary files /dev/null and b/assets/BlockClassType.png differ
diff --git a/assets/BlockVariableDemo1.png b/assets/BlockVariableDemo1.png
new file mode 100644
index 0000000..8e17c8b
Binary files /dev/null and b/assets/BlockVariableDemo1.png differ
diff --git a/assets/BlockVariableDemo2.png b/assets/BlockVariableDemo2.png
new file mode 100644
index 0000000..612808e
Binary files /dev/null and b/assets/BlockVariableDemo2.png differ
diff --git a/assets/BlockViaClangC.png b/assets/BlockViaClangC.png
new file mode 100644
index 0000000..74ec4b5
Binary files /dev/null and b/assets/BlockViaClangC.png differ
diff --git a/assets/BlockWillCaptureMethodSelfVariable.png b/assets/BlockWillCaptureMethodSelfVariable.png
new file mode 100644
index 0000000..b7475ec
Binary files /dev/null and b/assets/BlockWillCaptureMethodSelfVariable.png differ
diff --git a/assets/Block__BLOCKAssignObject.png b/assets/Block__BLOCKAssignObject.png
new file mode 100644
index 0000000..00ed7f4
Binary files /dev/null and b/assets/Block__BLOCKAssignObject.png differ
diff --git a/assets/CSDisplayLinkMemoryLeak.png b/assets/CSDisplayLinkMemoryLeak.png
new file mode 100644
index 0000000..620667b
Binary files /dev/null and b/assets/CSDisplayLinkMemoryLeak.png differ
diff --git a/assets/CalcuateARLRAddressViaDyld.png b/assets/CalcuateARLRAddressViaDyld.png
new file mode 100644
index 0000000..ac6bd26
Binary files /dev/null and b/assets/CalcuateARLRAddressViaDyld.png differ
diff --git a/assets/CategoryUsageDeclPrivateMethod.png b/assets/CategoryUsageDeclPrivateMethod.png
new file mode 100644
index 0000000..cdb5645
Binary files /dev/null and b/assets/CategoryUsageDeclPrivateMethod.png differ
diff --git a/assets/ChangeDynamicLibName.png b/assets/ChangeDynamicLibName.png
new file mode 100644
index 0000000..c512b6b
Binary files /dev/null and b/assets/ChangeDynamicLibName.png differ
diff --git a/assets/ChangeExecutablePathOnExecutableFile.png b/assets/ChangeExecutablePathOnExecutableFile.png
new file mode 100644
index 0000000..561c978
Binary files /dev/null and b/assets/ChangeExecutablePathOnExecutableFile.png differ
diff --git a/assets/ClangModuleSample.png b/assets/ClangModuleSample.png
new file mode 100644
index 0000000..028e92a
Binary files /dev/null and b/assets/ClangModuleSample.png differ
diff --git a/assets/CocoapodsCopyFramework.png b/assets/CocoapodsCopyFramework.png
new file mode 100644
index 0000000..bd52c5e
Binary files /dev/null and b/assets/CocoapodsCopyFramework.png differ
diff --git a/assets/CocoapodsExecutablePath.png b/assets/CocoapodsExecutablePath.png
new file mode 100644
index 0000000..82bf8f7
Binary files /dev/null and b/assets/CocoapodsExecutablePath.png differ
diff --git a/assets/CompileWillCallCopyWhenBlockCaptureAutoVarsiableAndAsReturnValueInARC.png b/assets/CompileWillCallCopyWhenBlockCaptureAutoVarsiableAndAsReturnValueInARC.png
new file mode 100644
index 0000000..f27fb64
Binary files /dev/null and b/assets/CompileWillCallCopyWhenBlockCaptureAutoVarsiableAndAsReturnValueInARC.png differ
diff --git a/assets/ConvertObjectFileToStaticLib.png b/assets/ConvertObjectFileToStaticLib.png
new file mode 100644
index 0000000..ff43ecb
Binary files /dev/null and b/assets/ConvertObjectFileToStaticLib.png differ
diff --git a/assets/ConvertStaticLibToDynamicLibNoSymbolError.png b/assets/ConvertStaticLibToDynamicLibNoSymbolError.png
new file mode 100644
index 0000000..4d8330f
Binary files /dev/null and b/assets/ConvertStaticLibToDynamicLibNoSymbolError.png differ
diff --git a/assets/CookieLifeCycle.png b/assets/CookieLifeCycle.png
new file mode 100644
index 0000000..4e1f060
Binary files /dev/null and b/assets/CookieLifeCycle.png differ
diff --git a/assets/DNSDNSRecursiveQuery.png b/assets/DNSDNSRecursiveQuery.png
new file mode 100644
index 0000000..11bf2cc
Binary files /dev/null and b/assets/DNSDNSRecursiveQuery.png differ
diff --git a/assets/DNSHacker.png b/assets/DNSHacker.png
new file mode 100644
index 0000000..2cae96f
Binary files /dev/null and b/assets/DNSHacker.png differ
diff --git a/assets/DNSIteratorQuery.png b/assets/DNSIteratorQuery.png
new file mode 100644
index 0000000..35c6a56
Binary files /dev/null and b/assets/DNSIteratorQuery.png differ
diff --git a/assets/DNSParseRedirect.png b/assets/DNSParseRedirect.png
new file mode 100644
index 0000000..6b8b07d
Binary files /dev/null and b/assets/DNSParseRedirect.png differ
diff --git a/assets/DNSRecursiveQuery.png b/assets/DNSRecursiveQuery.png
new file mode 100644
index 0000000..f196adc
Binary files /dev/null and b/assets/DNSRecursiveQuery.png differ
diff --git a/assets/DispatchBarrierReadWriteDemo.png b/assets/DispatchBarrierReadWriteDemo.png
new file mode 100644
index 0000000..2bb2ee2
Binary files /dev/null and b/assets/DispatchBarrierReadWriteDemo.png differ
diff --git a/assets/DispatchSemaphoreControlThreadCount1.png b/assets/DispatchSemaphoreControlThreadCount1.png
new file mode 100644
index 0000000..d6c55e7
Binary files /dev/null and b/assets/DispatchSemaphoreControlThreadCount1.png differ
diff --git a/assets/DispatchSemaphoreControlThreadCount2.png b/assets/DispatchSemaphoreControlThreadCount2.png
new file mode 100644
index 0000000..ff9bef8
Binary files /dev/null and b/assets/DispatchSemaphoreControlThreadCount2.png differ
diff --git a/assets/DoubleGCDMainQueueTaskObjectWillReleaseAfterStrongReference.png b/assets/DoubleGCDMainQueueTaskObjectWillReleaseAfterStrongReference.png
new file mode 100644
index 0000000..156426d
Binary files /dev/null and b/assets/DoubleGCDMainQueueTaskObjectWillReleaseAfterStrongReference.png differ
diff --git a/assets/DoubleGCDMainQueueTaskObjectWillReleaseAfterTotalTime.png b/assets/DoubleGCDMainQueueTaskObjectWillReleaseAfterTotalTime.png
new file mode 100644
index 0000000..37dd995
Binary files /dev/null and b/assets/DoubleGCDMainQueueTaskObjectWillReleaseAfterTotalTime.png differ
diff --git a/assets/DyanmicLibraryStripProcess.png b/assets/DyanmicLibraryStripProcess.png
new file mode 100644
index 0000000..4d9a935
Binary files /dev/null and b/assets/DyanmicLibraryStripProcess.png differ
diff --git a/assets/DynamicLibContainStaticLibSymbols.png b/assets/DynamicLibContainStaticLibSymbols.png
new file mode 100644
index 0000000..f1ade48
Binary files /dev/null and b/assets/DynamicLibContainStaticLibSymbols.png differ
diff --git a/assets/DynamicLibExportFrameworkSymbolWhichUsed.png b/assets/DynamicLibExportFrameworkSymbolWhichUsed.png
new file mode 100644
index 0000000..2bad008
Binary files /dev/null and b/assets/DynamicLibExportFrameworkSymbolWhichUsed.png differ
diff --git a/assets/DynamicLibHaveRightPathThenLinkedExecuteFile.png b/assets/DynamicLibHaveRightPathThenLinkedExecuteFile.png
new file mode 100644
index 0000000..5ec7663
Binary files /dev/null and b/assets/DynamicLibHaveRightPathThenLinkedExecuteFile.png differ
diff --git a/assets/DynamicLibLinkedDynamicLibInApp.png b/assets/DynamicLibLinkedDynamicLibInApp.png
new file mode 100644
index 0000000..0f2e2af
Binary files /dev/null and b/assets/DynamicLibLinkedDynamicLibInApp.png differ
diff --git a/assets/DynamicLibReferenceDynamicLibUseRPathAndExecutablePath.png b/assets/DynamicLibReferenceDynamicLibUseRPathAndExecutablePath.png
new file mode 100644
index 0000000..ee5752c
Binary files /dev/null and b/assets/DynamicLibReferenceDynamicLibUseRPathAndExecutablePath.png differ
diff --git a/assets/DynamicLibReferencedDynamicLibImageNotFoundError.png b/assets/DynamicLibReferencedDynamicLibImageNotFoundError.png
new file mode 100644
index 0000000..019647a
Binary files /dev/null and b/assets/DynamicLibReferencedDynamicLibImageNotFoundError.png differ
diff --git a/assets/DynamicLibReferencedDynamicLibSeeExportedSymbol.png b/assets/DynamicLibReferencedDynamicLibSeeExportedSymbol.png
new file mode 100644
index 0000000..e8b384b
Binary files /dev/null and b/assets/DynamicLibReferencedDynamicLibSeeExportedSymbol.png differ
diff --git a/assets/DynamicLibReferencedDynamicLibUseLoaderPath.png b/assets/DynamicLibReferencedDynamicLibUseLoaderPath.png
new file mode 100644
index 0000000..0241bbb
Binary files /dev/null and b/assets/DynamicLibReferencedDynamicLibUseLoaderPath.png differ
diff --git a/assets/DynamicLibaryCheckResult.png b/assets/DynamicLibaryCheckResult.png
new file mode 100644
index 0000000..59ea332
Binary files /dev/null and b/assets/DynamicLibaryCheckResult.png differ
diff --git a/assets/DynamicLibaryCodeTemplate.png b/assets/DynamicLibaryCodeTemplate.png
new file mode 100644
index 0000000..372e956
Binary files /dev/null and b/assets/DynamicLibaryCodeTemplate.png differ
diff --git a/assets/DynamicLibaryLoadProcess.png b/assets/DynamicLibaryLoadProcess.png
new file mode 100644
index 0000000..ab21939
Binary files /dev/null and b/assets/DynamicLibaryLoadProcess.png differ
diff --git a/assets/DynamicLibraryBuildShellResult.png b/assets/DynamicLibraryBuildShellResult.png
new file mode 100644
index 0000000..0a6e2f9
Binary files /dev/null and b/assets/DynamicLibraryBuildShellResult.png differ
diff --git a/assets/DynamicLinkedDynamicErrorSolvedByInstallOnApp.png b/assets/DynamicLinkedDynamicErrorSolvedByInstallOnApp.png
new file mode 100644
index 0000000..ce64468
Binary files /dev/null and b/assets/DynamicLinkedDynamicErrorSolvedByInstallOnApp.png differ
diff --git a/assets/DynamicUseDynamicLib.png b/assets/DynamicUseDynamicLib.png
new file mode 100644
index 0000000..ab6deb3
Binary files /dev/null and b/assets/DynamicUseDynamicLib.png differ
diff --git a/assets/DynamicUseStaticLibThenUseStaticLibInApp.png b/assets/DynamicUseStaticLibThenUseStaticLibInApp.png
new file mode 100644
index 0000000..2365a24
Binary files /dev/null and b/assets/DynamicUseStaticLibThenUseStaticLibInApp.png differ
diff --git a/assets/ExecutableFileDonotContainDWARF.png b/assets/ExecutableFileDonotContainDWARF.png
new file mode 100644
index 0000000..f9e3919
Binary files /dev/null and b/assets/ExecutableFileDonotContainDWARF.png differ
diff --git a/assets/ExecuteFIleSymbolTable.png b/assets/ExecuteFIleSymbolTable.png
new file mode 100644
index 0000000..11ad29f
Binary files /dev/null and b/assets/ExecuteFIleSymbolTable.png differ
diff --git a/assets/ExternalSymbolStripProcess.png b/assets/ExternalSymbolStripProcess.png
new file mode 100644
index 0000000..7ce8ce8
Binary files /dev/null and b/assets/ExternalSymbolStripProcess.png differ
diff --git a/assets/FixARCWillCrashWhenCallNewMethodDirectly.png b/assets/FixARCWillCrashWhenCallNewMethodDirectly.png
new file mode 100644
index 0000000..f94c724
Binary files /dev/null and b/assets/FixARCWillCrashWhenCallNewMethodDirectly.png differ
diff --git a/assets/FixStackBlockIssueWithCopy.png b/assets/FixStackBlockIssueWithCopy.png
new file mode 100644
index 0000000..0887f1e
Binary files /dev/null and b/assets/FixStackBlockIssueWithCopy.png differ
diff --git a/assets/FrameworkCannotFoundSymbolFromApp.png b/assets/FrameworkCannotFoundSymbolFromApp.png
new file mode 100644
index 0000000..8d82bed
Binary files /dev/null and b/assets/FrameworkCannotFoundSymbolFromApp.png differ
diff --git a/assets/FrameworkUseAppSymbolFixWithLDDynamicLookup.png b/assets/FrameworkUseAppSymbolFixWithLDDynamicLookup.png
new file mode 100644
index 0000000..af1e734
Binary files /dev/null and b/assets/FrameworkUseAppSymbolFixWithLDDynamicLookup.png differ
diff --git a/assets/GCDThreadWillTerminateWhenBlockFinished.png b/assets/GCDThreadWillTerminateWhenBlockFinished.png
new file mode 100644
index 0000000..33a817e
Binary files /dev/null and b/assets/GCDThreadWillTerminateWhenBlockFinished.png differ
diff --git a/assets/GenerateDSYMAndDisplay.png b/assets/GenerateDSYMAndDisplay.png
new file mode 100644
index 0000000..b72bef7
Binary files /dev/null and b/assets/GenerateDSYMAndDisplay.png differ
diff --git a/assets/GlobalValueAddress.png b/assets/GlobalValueAddress.png
new file mode 100644
index 0000000..74e6cf5
Binary files /dev/null and b/assets/GlobalValueAddress.png differ
diff --git a/assets/GlobalVariableWillNotCaptureByBlock.png b/assets/GlobalVariableWillNotCaptureByBlock.png
new file mode 100644
index 0000000..7fac10d
Binary files /dev/null and b/assets/GlobalVariableWillNotCaptureByBlock.png differ
diff --git a/assets/HMapDumpRes.png b/assets/HMapDumpRes.png
new file mode 100644
index 0000000..89807d3
Binary files /dev/null and b/assets/HMapDumpRes.png differ
diff --git a/assets/HMapDumpSystemTools.png b/assets/HMapDumpSystemTools.png
new file mode 100644
index 0000000..29e0b3a
Binary files /dev/null and b/assets/HMapDumpSystemTools.png differ
diff --git a/assets/HMapDumpXcodeConfig.png b/assets/HMapDumpXcodeConfig.png
new file mode 100644
index 0000000..8c50a0a
Binary files /dev/null and b/assets/HMapDumpXcodeConfig.png differ
diff --git a/assets/HMapExploreSample.png b/assets/HMapExploreSample.png
new file mode 100644
index 0000000..e79d739
Binary files /dev/null and b/assets/HMapExploreSample.png differ
diff --git a/assets/HMapStructure.png b/assets/HMapStructure.png
new file mode 100644
index 0000000..18ee29d
Binary files /dev/null and b/assets/HMapStructure.png differ
diff --git a/assets/HTTPDNSServerProcess.png b/assets/HTTPDNSServerProcess.png
new file mode 100644
index 0000000..f5d57ab
Binary files /dev/null and b/assets/HTTPDNSServerProcess.png differ
diff --git a/assets/HeaderFileWillCompileMoreTimesWithoutModule.png b/assets/HeaderFileWillCompileMoreTimesWithoutModule.png
new file mode 100644
index 0000000..003b2d6
Binary files /dev/null and b/assets/HeaderFileWillCompileMoreTimesWithoutModule.png differ
diff --git a/assets/ImportHeaderFileButCannotUseClass.png b/assets/ImportHeaderFileButCannotUseClass.png
new file mode 100644
index 0000000..4894a91
Binary files /dev/null and b/assets/ImportHeaderFileButCannotUseClass.png differ
diff --git a/assets/InjectDylibIntoAppUseDyld.png b/assets/InjectDylibIntoAppUseDyld.png
new file mode 100644
index 0000000..d78246e
Binary files /dev/null and b/assets/InjectDylibIntoAppUseDyld.png differ
diff --git a/assets/InstallNameToolChangeDynamicLibName.png b/assets/InstallNameToolChangeDynamicLibName.png
new file mode 100644
index 0000000..c9c7d55
Binary files /dev/null and b/assets/InstallNameToolChangeDynamicLibName.png differ
diff --git a/assets/InstanceIsaValueAndClassIsaValueAndMetaClassIsaValue.png b/assets/InstanceIsaValueAndClassIsaValueAndMetaClassIsaValue.png
new file mode 100644
index 0000000..0e4b52e
Binary files /dev/null and b/assets/InstanceIsaValueAndClassIsaValueAndMetaClassIsaValue.png differ
diff --git a/assets/KVC-get-process.png b/assets/KVC-get-process.png
index 5d75d14..7725494 100644
Binary files a/assets/KVC-get-process.png and b/assets/KVC-get-process.png differ
diff --git a/assets/KVC-process.png b/assets/KVC-process.png
index c066d88..f4096e2 100644
Binary files a/assets/KVC-process.png and b/assets/KVC-process.png differ
diff --git a/assets/KVCWillTriggerKVO.png b/assets/KVCWillTriggerKVO.png
new file mode 100644
index 0000000..0b6d858
Binary files /dev/null and b/assets/KVCWillTriggerKVO.png differ
diff --git a/assets/KVOCannotInvokeWhenChangeIvarDirectly.png b/assets/KVOCannotInvokeWhenChangeIvarDirectly.png
new file mode 100644
index 0000000..242c920
Binary files /dev/null and b/assets/KVOCannotInvokeWhenChangeIvarDirectly.png differ
diff --git a/assets/KVOInvokeWhenChangeIvarDirectlyMustCallWillChangeAndDidChange.png b/assets/KVOInvokeWhenChangeIvarDirectlyMustCallWillChangeAndDidChange.png
new file mode 100644
index 0000000..ac1a0bd
Binary files /dev/null and b/assets/KVOInvokeWhenChangeIvarDirectlyMustCallWillChangeAndDidChange.png differ
diff --git a/assets/KVOKeyMethodPrint.png b/assets/KVOKeyMethodPrint.png
new file mode 100644
index 0000000..031f752
Binary files /dev/null and b/assets/KVOKeyMethodPrint.png differ
diff --git a/assets/KVOMethodImplAddressAndIsaInspect.png b/assets/KVOMethodImplAddressAndIsaInspect.png
new file mode 100644
index 0000000..9d118c4
Binary files /dev/null and b/assets/KVOMethodImplAddressAndIsaInspect.png differ
diff --git a/assets/KVOMethodImplAddressWithDouble.png b/assets/KVOMethodImplAddressWithDouble.png
new file mode 100644
index 0000000..4a16e27
Binary files /dev/null and b/assets/KVOMethodImplAddressWithDouble.png differ
diff --git a/assets/KVOOverrrideClassMethod.png b/assets/KVOOverrrideClassMethod.png
new file mode 100644
index 0000000..5a872b9
Binary files /dev/null and b/assets/KVOOverrrideClassMethod.png differ
diff --git a/assets/LCLoadDYLIBViaMachO.png b/assets/LCLoadDYLIBViaMachO.png
new file mode 100644
index 0000000..bf77221
Binary files /dev/null and b/assets/LCLoadDYLIBViaMachO.png differ
diff --git a/assets/LDHiddenSymbolWhenDynamicUseStaticLib.png b/assets/LDHiddenSymbolWhenDynamicUseStaticLib.png
new file mode 100644
index 0000000..949f35d
Binary files /dev/null and b/assets/LDHiddenSymbolWhenDynamicUseStaticLib.png differ
diff --git a/assets/LDSetWeakFramework.png b/assets/LDSetWeakFramework.png
new file mode 100644
index 0000000..9f9832e
Binary files /dev/null and b/assets/LDSetWeakFramework.png differ
diff --git a/assets/LibtoolCombineStaticFrameworkErrorDueSameSymbol.png b/assets/LibtoolCombineStaticFrameworkErrorDueSameSymbol.png
new file mode 100644
index 0000000..1c19350
Binary files /dev/null and b/assets/LibtoolCombineStaticFrameworkErrorDueSameSymbol.png differ
diff --git a/assets/LinkDynamicFrameworkFullProcess.png b/assets/LinkDynamicFrameworkFullProcess.png
new file mode 100644
index 0000000..aea0acf
Binary files /dev/null and b/assets/LinkDynamicFrameworkFullProcess.png differ
diff --git a/assets/LinkFailedFromStaticLibConvertedDynamicLib.png b/assets/LinkFailedFromStaticLibConvertedDynamicLib.png
new file mode 100644
index 0000000..f08cfd1
Binary files /dev/null and b/assets/LinkFailedFromStaticLibConvertedDynamicLib.png differ
diff --git a/assets/LoaderPathAndRPathInAFNetworking.png b/assets/LoaderPathAndRPathInAFNetworking.png
new file mode 100644
index 0000000..5a9c45a
Binary files /dev/null and b/assets/LoaderPathAndRPathInAFNetworking.png differ
diff --git a/assets/LocalBlockInARCWillCallCopy.png b/assets/LocalBlockInARCWillCallCopy.png
new file mode 100644
index 0000000..a10bde0
Binary files /dev/null and b/assets/LocalBlockInARCWillCallCopy.png differ
diff --git a/assets/LocalBlockInMRC.png b/assets/LocalBlockInMRC.png
new file mode 100644
index 0000000..7960c9c
Binary files /dev/null and b/assets/LocalBlockInMRC.png differ
diff --git a/assets/LongConnectionAvoidDNSHacker.png b/assets/LongConnectionAvoidDNSHacker.png
new file mode 100644
index 0000000..af676fc
Binary files /dev/null and b/assets/LongConnectionAvoidDNSHacker.png differ
diff --git a/assets/MRCCompileFailedWhenBlockCaptureAutoVarsiableAndAsReturnValue.png b/assets/MRCCompileFailedWhenBlockCaptureAutoVarsiableAndAsReturnValue.png
new file mode 100644
index 0000000..be543dd
Binary files /dev/null and b/assets/MRCCompileFailedWhenBlockCaptureAutoVarsiableAndAsReturnValue.png differ
diff --git a/assets/MRCObjectCannotCaptureByMallocBlock.png b/assets/MRCObjectCannotCaptureByMallocBlock.png
new file mode 100644
index 0000000..53716ae
Binary files /dev/null and b/assets/MRCObjectCannotCaptureByMallocBlock.png differ
diff --git a/assets/MRCObjectWillReleaseWhenBlockIsReleased.png b/assets/MRCObjectWillReleaseWhenBlockIsReleased.png
new file mode 100644
index 0000000..bd5386b
Binary files /dev/null and b/assets/MRCObjectWillReleaseWhenBlockIsReleased.png differ
diff --git a/assets/MachOCalcuateFunAddress.png b/assets/MachOCalcuateFunAddress.png
new file mode 100644
index 0000000..6270f98
Binary files /dev/null and b/assets/MachOCalcuateFunAddress.png differ
diff --git a/assets/MachOContainsDWARFSection.png b/assets/MachOContainsDWARFSection.png
new file mode 100644
index 0000000..2d1368b
Binary files /dev/null and b/assets/MachOContainsDWARFSection.png differ
diff --git a/assets/MachOExploreInstruction.png b/assets/MachOExploreInstruction.png
new file mode 100644
index 0000000..5ec1152
Binary files /dev/null and b/assets/MachOExploreInstruction.png differ
diff --git a/assets/MachOLinkedByObjetFileAndStaticLibrary.png b/assets/MachOLinkedByObjetFileAndStaticLibrary.png
new file mode 100644
index 0000000..1a4cd2b
Binary files /dev/null and b/assets/MachOLinkedByObjetFileAndStaticLibrary.png differ
diff --git a/assets/MachOMainEntryPoint.png b/assets/MachOMainEntryPoint.png
new file mode 100644
index 0000000..e00882a
Binary files /dev/null and b/assets/MachOMainEntryPoint.png differ
diff --git a/assets/ManLDListReexportArg.png b/assets/ManLDListReexportArg.png
new file mode 100644
index 0000000..1e14811
Binary files /dev/null and b/assets/ManLDListReexportArg.png differ
diff --git a/assets/MethodCacheLookUpProcess.png b/assets/MethodCacheLookUpProcess.png
new file mode 100644
index 0000000..6f7fe92
Binary files /dev/null and b/assets/MethodCacheLookUpProcess.png differ
diff --git a/assets/MethodCallIsObjcMsgSend.png b/assets/MethodCallIsObjcMsgSend.png
new file mode 100644
index 0000000..1d12e07
Binary files /dev/null and b/assets/MethodCallIsObjcMsgSend.png differ
diff --git a/assets/MockBlockDemo.png b/assets/MockBlockDemo.png
new file mode 100644
index 0000000..3ce9baf
Binary files /dev/null and b/assets/MockBlockDemo.png differ
diff --git a/assets/ModuleDemoProject.png b/assets/ModuleDemoProject.png
new file mode 100644
index 0000000..9dcd8b3
Binary files /dev/null and b/assets/ModuleDemoProject.png differ
diff --git a/assets/MultiplePListAfterDuplicateTarget.png b/assets/MultiplePListAfterDuplicateTarget.png
new file mode 100644
index 0000000..a10a56d
Binary files /dev/null and b/assets/MultiplePListAfterDuplicateTarget.png differ
diff --git a/assets/MultipleTargetProjectConfig.png b/assets/MultipleTargetProjectConfig.png
new file mode 100644
index 0000000..bb1698c
Binary files /dev/null and b/assets/MultipleTargetProjectConfig.png differ
diff --git a/assets/NMListAllKindOfSymbol.png b/assets/NMListAllKindOfSymbol.png
new file mode 100644
index 0000000..c6908ce
Binary files /dev/null and b/assets/NMListAllKindOfSymbol.png differ
diff --git a/assets/NSConditionLockDemo.png b/assets/NSConditionLockDemo.png
new file mode 100644
index 0000000..aa5724e
Binary files /dev/null and b/assets/NSConditionLockDemo.png differ
diff --git a/assets/NSConditionLockDemo1.png b/assets/NSConditionLockDemo1.png
new file mode 100644
index 0000000..aeb9143
Binary files /dev/null and b/assets/NSConditionLockDemo1.png differ
diff --git a/assets/NSConditionOrder1.png b/assets/NSConditionOrder1.png
new file mode 100644
index 0000000..a3b28b2
Binary files /dev/null and b/assets/NSConditionOrder1.png differ
diff --git a/assets/NSConditionOrder2.png b/assets/NSConditionOrder2.png
new file mode 100644
index 0000000..1a74770
Binary files /dev/null and b/assets/NSConditionOrder2.png differ
diff --git a/assets/NSLockDeadLock.png b/assets/NSLockDeadLock.png
new file mode 100644
index 0000000..4c1ba7d
Binary files /dev/null and b/assets/NSLockDeadLock.png differ
diff --git a/assets/NSLockDemo.png b/assets/NSLockDemo.png
new file mode 100644
index 0000000..af6ef55
Binary files /dev/null and b/assets/NSLockDemo.png differ
diff --git a/assets/NSMutableArrayDonotNeedBlockToUseArray.png b/assets/NSMutableArrayDonotNeedBlockToUseArray.png
new file mode 100644
index 0000000..7a09863
Binary files /dev/null and b/assets/NSMutableArrayDonotNeedBlockToUseArray.png differ
diff --git a/assets/NSNumberAddressToBinary.png b/assets/NSNumberAddressToBinary.png
new file mode 100644
index 0000000..64a0538
Binary files /dev/null and b/assets/NSNumberAddressToBinary.png differ
diff --git a/assets/NSNumberTaggedPointerStructure.png b/assets/NSNumberTaggedPointerStructure.png
new file mode 100644
index 0000000..bdbdd4f
Binary files /dev/null and b/assets/NSNumberTaggedPointerStructure.png differ
diff --git a/assets/NSObject-metaClass-isa-superclass.png b/assets/NSObject-metaClass-isa-superclass.png
new file mode 100644
index 0000000..51bfb4a
Binary files /dev/null and b/assets/NSObject-metaClass-isa-superclass.png differ
diff --git a/assets/NSObjectClassMethodNotImplementedWillCallInstanceMethod.png b/assets/NSObjectClassMethodNotImplementedWillCallInstanceMethod.png
new file mode 100644
index 0000000..addf7b1
Binary files /dev/null and b/assets/NSObjectClassMethodNotImplementedWillCallInstanceMethod.png differ
diff --git a/assets/NSProxyAndNSObjectMethodImpl.png b/assets/NSProxyAndNSObjectMethodImpl.png
new file mode 100644
index 0000000..550bfcf
Binary files /dev/null and b/assets/NSProxyAndNSObjectMethodImpl.png differ
diff --git a/assets/NSRunloopRoundWithCFRunloop.png b/assets/NSRunloopRoundWithCFRunloop.png
new file mode 100644
index 0000000..f02579c
Binary files /dev/null and b/assets/NSRunloopRoundWithCFRunloop.png differ
diff --git a/assets/NSStringClassClusterAndTaggedPointer.png b/assets/NSStringClassClusterAndTaggedPointer.png
new file mode 100644
index 0000000..22f897d
Binary files /dev/null and b/assets/NSStringClassClusterAndTaggedPointer.png differ
diff --git a/assets/NSThreadWillTerminateSoCanUseViaRunLoopPort.png b/assets/NSThreadWillTerminateSoCanUseViaRunLoopPort.png
new file mode 100644
index 0000000..6036a41
Binary files /dev/null and b/assets/NSThreadWillTerminateSoCanUseViaRunLoopPort.png differ
diff --git a/assets/NSThreadWillTerminateSoCannotUse.png b/assets/NSThreadWillTerminateSoCannotUse.png
new file mode 100644
index 0000000..b2bc3fe
Binary files /dev/null and b/assets/NSThreadWillTerminateSoCannotUse.png differ
diff --git a/assets/NSTimerFixMemoryLeakIssueByBlockAPI.png b/assets/NSTimerFixMemoryLeakIssueByBlockAPI.png
new file mode 100644
index 0000000..a157629
Binary files /dev/null and b/assets/NSTimerFixMemoryLeakIssueByBlockAPI.png differ
diff --git a/assets/NSTimerMemoeryLeakDemo.png b/assets/NSTimerMemoeryLeakDemo.png
new file mode 100644
index 0000000..fcb0e28
Binary files /dev/null and b/assets/NSTimerMemoeryLeakDemo.png differ
diff --git a/assets/NSTimerMemoeryNotLeakWhenRepeatNO.png b/assets/NSTimerMemoeryNotLeakWhenRepeatNO.png
new file mode 100644
index 0000000..554e0bb
Binary files /dev/null and b/assets/NSTimerMemoeryNotLeakWhenRepeatNO.png differ
diff --git a/assets/NSTimerMemoryLeakFixedByNSProxy.png b/assets/NSTimerMemoryLeakFixedByNSProxy.png
new file mode 100644
index 0000000..ac3b459
Binary files /dev/null and b/assets/NSTimerMemoryLeakFixedByNSProxy.png differ
diff --git a/assets/NSTimerMemoryLeakFixedByProxy.png b/assets/NSTimerMemoryLeakFixedByProxy.png
new file mode 100644
index 0000000..dbc8a26
Binary files /dev/null and b/assets/NSTimerMemoryLeakFixedByProxy.png differ
diff --git a/assets/NetworkManagerFrameworkLoadCommandInfo.png b/assets/NetworkManagerFrameworkLoadCommandInfo.png
new file mode 100644
index 0000000..8a20420
Binary files /dev/null and b/assets/NetworkManagerFrameworkLoadCommandInfo.png differ
diff --git a/assets/OCBlockType.png b/assets/OCBlockType.png
new file mode 100644
index 0000000..99a8e9a
Binary files /dev/null and b/assets/OCBlockType.png differ
diff --git a/assets/OCInstanceIsaWithClassIsaWithMetaClass.png b/assets/OCInstanceIsaWithClassIsaWithMetaClass.png
new file mode 100644
index 0000000..31aea9f
Binary files /dev/null and b/assets/OCInstanceIsaWithClassIsaWithMetaClass.png differ
diff --git a/assets/OCStackBlockCrash.png b/assets/OCStackBlockCrash.png
new file mode 100644
index 0000000..1fb3746
Binary files /dev/null and b/assets/OCStackBlockCrash.png differ
diff --git a/assets/ObjcClassDataMethodExplore.png b/assets/ObjcClassDataMethodExplore.png
new file mode 100644
index 0000000..4e9a9ab
Binary files /dev/null and b/assets/ObjcClassDataMethodExplore.png differ
diff --git a/assets/ObjcNSIntegerIsLong.png b/assets/ObjcNSIntegerIsLong.png
new file mode 100644
index 0000000..4718e1e
Binary files /dev/null and b/assets/ObjcNSIntegerIsLong.png differ
diff --git a/assets/ObjcStoreWeakWhenUseWeak.png b/assets/ObjcStoreWeakWhenUseWeak.png
new file mode 100644
index 0000000..d9fc6f6
Binary files /dev/null and b/assets/ObjcStoreWeakWhenUseWeak.png differ
diff --git a/assets/ObjdumpWatchObjectFile.png b/assets/ObjdumpWatchObjectFile.png
new file mode 100644
index 0000000..13b39e7
Binary files /dev/null and b/assets/ObjdumpWatchObjectFile.png differ
diff --git a/assets/ObjdumpWatchObjectFileWithRelocationTable.png b/assets/ObjdumpWatchObjectFileWithRelocationTable.png
new file mode 100644
index 0000000..edfb012
Binary files /dev/null and b/assets/ObjdumpWatchObjectFileWithRelocationTable.png differ
diff --git a/assets/ObjdumpWatchStatisLibraryIsObjectFile.png b/assets/ObjdumpWatchStatisLibraryIsObjectFile.png
new file mode 100644
index 0000000..42d0670
Binary files /dev/null and b/assets/ObjdumpWatchStatisLibraryIsObjectFile.png differ
diff --git a/assets/ObjectAdapter.png b/assets/ObjectAdapter.png
new file mode 100644
index 0000000..98cf33d
Binary files /dev/null and b/assets/ObjectAdapter.png differ
diff --git a/assets/ObjectWillReleaseWhenGCDTimeout.png b/assets/ObjectWillReleaseWhenGCDTimeout.png
new file mode 100644
index 0000000..feebcd9
Binary files /dev/null and b/assets/ObjectWillReleaseWhenGCDTimeout.png differ
diff --git a/assets/OtoolWatchLCLoadDYLIB.png b/assets/OtoolWatchLCLoadDYLIB.png
new file mode 100644
index 0000000..933c92a
Binary files /dev/null and b/assets/OtoolWatchLCLoadDYLIB.png differ
diff --git a/assets/PThreadConditionLock.png b/assets/PThreadConditionLock.png
new file mode 100644
index 0000000..8203382
Binary files /dev/null and b/assets/PThreadConditionLock.png differ
diff --git a/assets/PThreadMutexLock1.png b/assets/PThreadMutexLock1.png
new file mode 100644
index 0000000..25df203
Binary files /dev/null and b/assets/PThreadMutexLock1.png differ
diff --git a/assets/PThreadMutexLock2.png b/assets/PThreadMutexLock2.png
new file mode 100644
index 0000000..be01779
Binary files /dev/null and b/assets/PThreadMutexLock2.png differ
diff --git a/assets/PThreadMutexLock3.png b/assets/PThreadMutexLock3.png
new file mode 100644
index 0000000..b0cfeae
Binary files /dev/null and b/assets/PThreadMutexLock3.png differ
diff --git a/assets/PThreadMutexLock4.png b/assets/PThreadMutexLock4.png
new file mode 100644
index 0000000..3720dc8
Binary files /dev/null and b/assets/PThreadMutexLock4.png differ
diff --git a/assets/PThreadMutexLock5.png b/assets/PThreadMutexLock5.png
new file mode 100644
index 0000000..d2a587f
Binary files /dev/null and b/assets/PThreadMutexLock5.png differ
diff --git a/assets/PThreadMutexLock6.png b/assets/PThreadMutexLock6.png
new file mode 100644
index 0000000..44ef11d
Binary files /dev/null and b/assets/PThreadMutexLock6.png differ
diff --git a/assets/PThreadMutexLock7.png b/assets/PThreadMutexLock7.png
new file mode 100644
index 0000000..0d410c4
Binary files /dev/null and b/assets/PThreadMutexLock7.png differ
diff --git a/assets/PThreadMutextLock.png b/assets/PThreadMutextLock.png
new file mode 100644
index 0000000..9eddbb1
Binary files /dev/null and b/assets/PThreadMutextLock.png differ
diff --git a/assets/PThreadMutextLock1.png b/assets/PThreadMutextLock1.png
new file mode 100644
index 0000000..40beb80
Binary files /dev/null and b/assets/PThreadMutextLock1.png differ
diff --git a/assets/PThreadMutextRecursiveLock.png b/assets/PThreadMutextRecursiveLock.png
new file mode 100644
index 0000000..d96776d
Binary files /dev/null and b/assets/PThreadMutextRecursiveLock.png differ
diff --git a/assets/PThreadRWLockDemo.png b/assets/PThreadRWLockDemo.png
new file mode 100644
index 0000000..71acd18
Binary files /dev/null and b/assets/PThreadRWLockDemo.png differ
diff --git a/assets/PersonAndCatMRCIssue.png b/assets/PersonAndCatMRCIssue.png
new file mode 100644
index 0000000..02fd7b8
Binary files /dev/null and b/assets/PersonAndCatMRCIssue.png differ
diff --git a/assets/PersonAndCatMRCIssue1.png b/assets/PersonAndCatMRCIssue1.png
new file mode 100644
index 0000000..94bf7c9
Binary files /dev/null and b/assets/PersonAndCatMRCIssue1.png differ
diff --git a/assets/PersonAndCatMRCIssue2.png b/assets/PersonAndCatMRCIssue2.png
new file mode 100644
index 0000000..4ee1d22
Binary files /dev/null and b/assets/PersonAndCatMRCIssue2.png differ
diff --git a/assets/PersonAndCatMRCIssue3.png b/assets/PersonAndCatMRCIssue3.png
new file mode 100644
index 0000000..3f6b4f6
Binary files /dev/null and b/assets/PersonAndCatMRCIssue3.png differ
diff --git a/assets/PersonAndCatMRCIssue4.png b/assets/PersonAndCatMRCIssue4.png
new file mode 100644
index 0000000..4181ac2
Binary files /dev/null and b/assets/PersonAndCatMRCIssue4.png differ
diff --git a/assets/PersonAndCatMRCIssue5.png b/assets/PersonAndCatMRCIssue5.png
new file mode 100644
index 0000000..9f5afe8
Binary files /dev/null and b/assets/PersonAndCatMRCIssue5.png differ
diff --git a/assets/PersonDynamicLibArch.png b/assets/PersonDynamicLibArch.png
new file mode 100644
index 0000000..5ec4854
Binary files /dev/null and b/assets/PersonDynamicLibArch.png differ
diff --git a/assets/PrintKVOClassAllMethodName.png b/assets/PrintKVOClassAllMethodName.png
new file mode 100644
index 0000000..895f7cd
Binary files /dev/null and b/assets/PrintKVOClassAllMethodName.png differ
diff --git a/assets/PrivateModuleUsage.png b/assets/PrivateModuleUsage.png
new file mode 100644
index 0000000..3df305b
Binary files /dev/null and b/assets/PrivateModuleUsage.png differ
diff --git a/assets/ProjectCompileDyanmicLibUseXcconfig.png b/assets/ProjectCompileDyanmicLibUseXcconfig.png
new file mode 100644
index 0000000..4ea93a9
Binary files /dev/null and b/assets/ProjectCompileDyanmicLibUseXcconfig.png differ
diff --git a/assets/ProjectLinkedTwoSameStaticLib.png b/assets/ProjectLinkedTwoSameStaticLib.png
new file mode 100644
index 0000000..e4ac3d8
Binary files /dev/null and b/assets/ProjectLinkedTwoSameStaticLib.png differ
diff --git a/assets/ProjectLinkedTwoSameStaticLibWillFailedWithAllLoad.png b/assets/ProjectLinkedTwoSameStaticLibWillFailedWithAllLoad.png
new file mode 100644
index 0000000..35eed9b
Binary files /dev/null and b/assets/ProjectLinkedTwoSameStaticLibWillFailedWithAllLoad.png differ
diff --git a/assets/ProjectLinkedTwoSameStaticLibWithLoadHidden.png b/assets/ProjectLinkedTwoSameStaticLibWithLoadHidden.png
new file mode 100644
index 0000000..fbedbb1
Binary files /dev/null and b/assets/ProjectLinkedTwoSameStaticLibWithLoadHidden.png differ
diff --git a/assets/ReadTimeCounter.png b/assets/ReadTimeCounter.png
new file mode 100644
index 0000000..e87a853
Binary files /dev/null and b/assets/ReadTimeCounter.png differ
diff --git a/assets/ReferenceCountintStructure.png b/assets/ReferenceCountintStructure.png
new file mode 100644
index 0000000..bf21552
Binary files /dev/null and b/assets/ReferenceCountintStructure.png differ
diff --git a/assets/RelocationSymbolTableAndCodeInstructionMap.png b/assets/RelocationSymbolTableAndCodeInstructionMap.png
new file mode 100644
index 0000000..617de95
Binary files /dev/null and b/assets/RelocationSymbolTableAndCodeInstructionMap.png differ
diff --git a/assets/RunLoopPerformTaskFromGCDMainQueue.png b/assets/RunLoopPerformTaskFromGCDMainQueue.png
new file mode 100644
index 0000000..c926ef2
Binary files /dev/null and b/assets/RunLoopPerformTaskFromGCDMainQueue.png differ
diff --git a/assets/RunLoopSleepSystemCall.png b/assets/RunLoopSleepSystemCall.png
new file mode 100644
index 0000000..4197ea9
Binary files /dev/null and b/assets/RunLoopSleepSystemCall.png differ
diff --git a/assets/RunLoopThreadKeepLive.png b/assets/RunLoopThreadKeepLive.png
new file mode 100644
index 0000000..6016c0d
Binary files /dev/null and b/assets/RunLoopThreadKeepLive.png differ
diff --git a/assets/RunloopPerformSelector.png b/assets/RunloopPerformSelector.png
new file mode 100644
index 0000000..85c6d95
Binary files /dev/null and b/assets/RunloopPerformSelector.png differ
diff --git a/assets/RunloopPerformSelectorAfterDelay.png b/assets/RunloopPerformSelectorAfterDelay.png
new file mode 100644
index 0000000..8e389d2
Binary files /dev/null and b/assets/RunloopPerformSelectorAfterDelay.png differ
diff --git a/assets/RuntimeCallUnKnownMethod.png b/assets/RuntimeCallUnKnownMethod.png
new file mode 100644
index 0000000..5611125
Binary files /dev/null and b/assets/RuntimeCallUnKnownMethod.png differ
diff --git a/assets/RuntimeDicToObject.png b/assets/RuntimeDicToObject.png
new file mode 100644
index 0000000..c988315
Binary files /dev/null and b/assets/RuntimeDicToObject.png differ
diff --git a/assets/RuntimeIsKindOfClassDemo1.png b/assets/RuntimeIsKindOfClassDemo1.png
new file mode 100644
index 0000000..936145d
Binary files /dev/null and b/assets/RuntimeIsKindOfClassDemo1.png differ
diff --git a/assets/RuntimeIsKindOfClassDemo2.png b/assets/RuntimeIsKindOfClassDemo2.png
new file mode 100644
index 0000000..273b86e
Binary files /dev/null and b/assets/RuntimeIsKindOfClassDemo2.png differ
diff --git a/assets/RuntimeIsKindOfClassUnitDemo.png b/assets/RuntimeIsKindOfClassUnitDemo.png
new file mode 100644
index 0000000..e69a01d
Binary files /dev/null and b/assets/RuntimeIsKindOfClassUnitDemo.png differ
diff --git a/assets/RuntimeMethodCache1.png b/assets/RuntimeMethodCache1.png
new file mode 100644
index 0000000..c33abf3
Binary files /dev/null and b/assets/RuntimeMethodCache1.png differ
diff --git a/assets/RuntimeMethodCache2.png b/assets/RuntimeMethodCache2.png
new file mode 100644
index 0000000..b27b01c
Binary files /dev/null and b/assets/RuntimeMethodCache2.png differ
diff --git a/assets/RuntimeMethodCache3.png b/assets/RuntimeMethodCache3.png
new file mode 100644
index 0000000..bdff561
Binary files /dev/null and b/assets/RuntimeMethodCache3.png differ
diff --git a/assets/RuntimeMethodCache4.png b/assets/RuntimeMethodCache4.png
new file mode 100644
index 0000000..a50e5e8
Binary files /dev/null and b/assets/RuntimeMethodCache4.png differ
diff --git a/assets/RuntimeMethodForwardingByClassMethod.png b/assets/RuntimeMethodForwardingByClassMethod.png
new file mode 100644
index 0000000..dfc9b23
Binary files /dev/null and b/assets/RuntimeMethodForwardingByClassMethod.png differ
diff --git a/assets/RuntimeMethodForwardingByClassMethod2.png b/assets/RuntimeMethodForwardingByClassMethod2.png
new file mode 100644
index 0000000..5b98ee2
Binary files /dev/null and b/assets/RuntimeMethodForwardingByClassMethod2.png differ
diff --git a/assets/RuntimeMethodForwardingDemo1.png b/assets/RuntimeMethodForwardingDemo1.png
new file mode 100644
index 0000000..bf7b148
Binary files /dev/null and b/assets/RuntimeMethodForwardingDemo1.png differ
diff --git a/assets/RuntimeMethodForwardingDemo2.png b/assets/RuntimeMethodForwardingDemo2.png
new file mode 100644
index 0000000..3f894f8
Binary files /dev/null and b/assets/RuntimeMethodForwardingDemo2.png differ
diff --git a/assets/RuntimeMethodForwardingWithMethodSignature.png b/assets/RuntimeMethodForwardingWithMethodSignature.png
new file mode 100644
index 0000000..36ddac5
Binary files /dev/null and b/assets/RuntimeMethodForwardingWithMethodSignature.png differ
diff --git a/assets/RuntimeQA.png b/assets/RuntimeQA.png
new file mode 100644
index 0000000..5ca66f6
Binary files /dev/null and b/assets/RuntimeQA.png differ
diff --git a/assets/RuntimeQA1.png b/assets/RuntimeQA1.png
new file mode 100644
index 0000000..35a7ae0
Binary files /dev/null and b/assets/RuntimeQA1.png differ
diff --git a/assets/RuntimeQA2.png b/assets/RuntimeQA2.png
new file mode 100644
index 0000000..b2db3fa
Binary files /dev/null and b/assets/RuntimeQA2.png differ
diff --git a/assets/RuntimeQA3.png b/assets/RuntimeQA3.png
new file mode 100644
index 0000000..595fda5
Binary files /dev/null and b/assets/RuntimeQA3.png differ
diff --git a/assets/SDWebImageArch.png b/assets/SDWebImageArch.png
new file mode 100644
index 0000000..c4a675d
Binary files /dev/null and b/assets/SDWebImageArch.png differ
diff --git a/assets/SDWebImageProcess.png b/assets/SDWebImageProcess.png
new file mode 100644
index 0000000..3fae907
Binary files /dev/null and b/assets/SDWebImageProcess.png differ
diff --git a/assets/SelfClassNameConflictsWithKVOClass.png b/assets/SelfClassNameConflictsWithKVOClass.png
new file mode 100644
index 0000000..bb35493
Binary files /dev/null and b/assets/SelfClassNameConflictsWithKVOClass.png differ
diff --git a/assets/SemaphoreMethodToControlThreadSync.png b/assets/SemaphoreMethodToControlThreadSync.png
new file mode 100644
index 0000000..74488d6
Binary files /dev/null and b/assets/SemaphoreMethodToControlThreadSync.png differ
diff --git a/assets/SerialQueueToSolveThreadSyn.png b/assets/SerialQueueToSolveThreadSyn.png
new file mode 100644
index 0000000..efa1ce6
Binary files /dev/null and b/assets/SerialQueueToSolveThreadSyn.png differ
diff --git a/assets/SessionWorkflow.png b/assets/SessionWorkflow.png
new file mode 100644
index 0000000..52b9635
Binary files /dev/null and b/assets/SessionWorkflow.png differ
diff --git a/assets/SetValueUseDifferentSchemeAndUseViaPlist.png b/assets/SetValueUseDifferentSchemeAndUseViaPlist.png
new file mode 100644
index 0000000..f667ff2
Binary files /dev/null and b/assets/SetValueUseDifferentSchemeAndUseViaPlist.png differ
diff --git a/assets/SimpleBlockExploreDemo.png b/assets/SimpleBlockExploreDemo.png
new file mode 100644
index 0000000..46efad3
Binary files /dev/null and b/assets/SimpleBlockExploreDemo.png differ
diff --git a/assets/SimpleSimpleBlockCaptureStructure.png b/assets/SimpleSimpleBlockCaptureStructure.png
new file mode 100644
index 0000000..659aecf
Binary files /dev/null and b/assets/SimpleSimpleBlockCaptureStructure.png differ
diff --git a/assets/StaticLibUseDynamicLibErrorWhenUseInApp.png b/assets/StaticLibUseDynamicLibErrorWhenUseInApp.png
new file mode 100644
index 0000000..15b0fc4
Binary files /dev/null and b/assets/StaticLibUseDynamicLibErrorWhenUseInApp.png differ
diff --git a/assets/StaticLibUseDynamicLibFrameworkHandleByShell.png b/assets/StaticLibUseDynamicLibFrameworkHandleByShell.png
new file mode 100644
index 0000000..d062879
Binary files /dev/null and b/assets/StaticLibUseDynamicLibFrameworkHandleByShell.png differ
diff --git a/assets/StaticLibUseHeaderMapEffect.png b/assets/StaticLibUseHeaderMapEffect.png
new file mode 100644
index 0000000..48b2349
Binary files /dev/null and b/assets/StaticLibUseHeaderMapEffect.png differ
diff --git a/assets/StaticLibUseHeaderMapOnlyQuoteMethod.png b/assets/StaticLibUseHeaderMapOnlyQuoteMethod.png
new file mode 100644
index 0000000..b2aeb0d
Binary files /dev/null and b/assets/StaticLibUseHeaderMapOnlyQuoteMethod.png differ
diff --git a/assets/StaticLibUseStaticLibError.png b/assets/StaticLibUseStaticLibError.png
new file mode 100644
index 0000000..b1fb284
Binary files /dev/null and b/assets/StaticLibUseStaticLibError.png differ
diff --git a/assets/StaticLibUseStaticLibSetLibSearchPath.png b/assets/StaticLibUseStaticLibSetLibSearchPath.png
new file mode 100644
index 0000000..76cddf4
Binary files /dev/null and b/assets/StaticLibUseStaticLibSetLibSearchPath.png differ
diff --git a/assets/StaticLibararyStripProcess.png b/assets/StaticLibararyStripProcess.png
new file mode 100644
index 0000000..c37c611
Binary files /dev/null and b/assets/StaticLibararyStripProcess.png differ
diff --git a/assets/StaticLibraryMoveToValidateFramework.png b/assets/StaticLibraryMoveToValidateFramework.png
new file mode 100644
index 0000000..784b5c4
Binary files /dev/null and b/assets/StaticLibraryMoveToValidateFramework.png differ
diff --git a/assets/StaticVariableCaptureByBlock.png b/assets/StaticVariableCaptureByBlock.png
new file mode 100644
index 0000000..425351f
Binary files /dev/null and b/assets/StaticVariableCaptureByBlock.png differ
diff --git a/assets/StrongObjectCapturedByBlock.png b/assets/StrongObjectCapturedByBlock.png
new file mode 100644
index 0000000..931ea75
Binary files /dev/null and b/assets/StrongObjectCapturedByBlock.png differ
diff --git a/assets/StructObjcClassStructureInObjc4-838.1.png b/assets/StructObjcClassStructureInObjc4-838.1.png
new file mode 100644
index 0000000..9a16530
Binary files /dev/null and b/assets/StructObjcClassStructureInObjc4-838.1.png differ
diff --git a/assets/SwiftCModuleInfoInXcode.png b/assets/SwiftCModuleInfoInXcode.png
new file mode 100644
index 0000000..3aa2ffc
Binary files /dev/null and b/assets/SwiftCModuleInfoInXcode.png differ
diff --git a/assets/SwiftFrameworkOCClassCanVisitOutside.png b/assets/SwiftFrameworkOCClassCanVisitOutside.png
new file mode 100644
index 0000000..18d9608
Binary files /dev/null and b/assets/SwiftFrameworkOCClassCanVisitOutside.png differ
diff --git a/assets/SwiftFramworkSwiftCallOCViaModuleMap.png b/assets/SwiftFramworkSwiftCallOCViaModuleMap.png
new file mode 100644
index 0000000..9f828b7
Binary files /dev/null and b/assets/SwiftFramworkSwiftCallOCViaModuleMap.png differ
diff --git a/assets/SwiftModuleFileLocation.png b/assets/SwiftModuleFileLocation.png
new file mode 100644
index 0000000..bc15368
Binary files /dev/null and b/assets/SwiftModuleFileLocation.png differ
diff --git a/assets/SwiftStaticComposedProject.png b/assets/SwiftStaticComposedProject.png
new file mode 100644
index 0000000..2575da0
Binary files /dev/null and b/assets/SwiftStaticComposedProject.png differ
diff --git a/assets/SwiftTestingForOCClass.png b/assets/SwiftTestingForOCClass.png
new file mode 100644
index 0000000..a1bd67e
Binary files /dev/null and b/assets/SwiftTestingForOCClass.png differ
diff --git a/assets/SynchronizedControlThreadSync.png b/assets/SynchronizedControlThreadSync.png
new file mode 100644
index 0000000..290100b
Binary files /dev/null and b/assets/SynchronizedControlThreadSync.png differ
diff --git a/assets/TCPCongestionControl.png b/assets/TCPCongestionControl.png
new file mode 100644
index 0000000..fc5dcd1
Binary files /dev/null and b/assets/TCPCongestionControl.png differ
diff --git a/assets/TCPFourTimesUnBuild.png b/assets/TCPFourTimesUnBuild.png
new file mode 100644
index 0000000..d29af10
Binary files /dev/null and b/assets/TCPFourTimesUnBuild.png differ
diff --git a/assets/TCPOrientedByteBuffer.png b/assets/TCPOrientedByteBuffer.png
new file mode 100644
index 0000000..10b0881
Binary files /dev/null and b/assets/TCPOrientedByteBuffer.png differ
diff --git a/assets/TCPQuickResendSample.png b/assets/TCPQuickResendSample.png
new file mode 100644
index 0000000..0b890dd
Binary files /dev/null and b/assets/TCPQuickResendSample.png differ
diff --git a/assets/TCPSlipWindowProcess.png b/assets/TCPSlipWindowProcess.png
new file mode 100644
index 0000000..df8afa6
Binary files /dev/null and b/assets/TCPSlipWindowProcess.png differ
diff --git a/assets/TCPStopAndWaitProtocolAckLater.png b/assets/TCPStopAndWaitProtocolAckLater.png
new file mode 100644
index 0000000..e458eab
Binary files /dev/null and b/assets/TCPStopAndWaitProtocolAckLater.png differ
diff --git a/assets/TCPStopAndWaitProtocolAckMiss.png b/assets/TCPStopAndWaitProtocolAckMiss.png
new file mode 100644
index 0000000..8e7cdcc
Binary files /dev/null and b/assets/TCPStopAndWaitProtocolAckMiss.png differ
diff --git a/assets/TCPStopAndWaitProtocolNormal.png b/assets/TCPStopAndWaitProtocolNormal.png
new file mode 100644
index 0000000..c020522
Binary files /dev/null and b/assets/TCPStopAndWaitProtocolNormal.png differ
diff --git a/assets/TCPStopAndWaitProtocolResendWhenTimeout.png b/assets/TCPStopAndWaitProtocolResendWhenTimeout.png
new file mode 100644
index 0000000..d653d81
Binary files /dev/null and b/assets/TCPStopAndWaitProtocolResendWhenTimeout.png differ
diff --git a/assets/TCPThressTimesBuild.png b/assets/TCPThressTimesBuild.png
new file mode 100644
index 0000000..4044fa9
Binary files /dev/null and b/assets/TCPThressTimesBuild.png differ
diff --git a/assets/TCPWindowElement.png b/assets/TCPWindowElement.png
new file mode 100644
index 0000000..5396d7d
Binary files /dev/null and b/assets/TCPWindowElement.png differ
diff --git a/assets/TCPWindowMove.png b/assets/TCPWindowMove.png
new file mode 100644
index 0000000..bc5ada5
Binary files /dev/null and b/assets/TCPWindowMove.png differ
diff --git a/assets/TCPWindowProcess.png b/assets/TCPWindowProcess.png
new file mode 100644
index 0000000..8f8e353
Binary files /dev/null and b/assets/TCPWindowProcess.png differ
diff --git a/assets/TaggedPointerCrash.png b/assets/TaggedPointerCrash.png
index 93b15a5..6877599 100644
Binary files a/assets/TaggedPointerCrash.png and b/assets/TaggedPointerCrash.png differ
diff --git a/assets/TaggedPointerCrashFix1.png b/assets/TaggedPointerCrashFix1.png
new file mode 100644
index 0000000..87eba8b
Binary files /dev/null and b/assets/TaggedPointerCrashFix1.png differ
diff --git a/assets/TaggedPointerCrashFix2.png b/assets/TaggedPointerCrashFix2.png
new file mode 100644
index 0000000..100cbab
Binary files /dev/null and b/assets/TaggedPointerCrashFix2.png differ
diff --git a/assets/TaggedPointerDataTypeDemo.png b/assets/TaggedPointerDataTypeDemo.png
new file mode 100644
index 0000000..1cf3870
Binary files /dev/null and b/assets/TaggedPointerDataTypeDemo.png differ
diff --git a/assets/TaggedPointerDecode.png b/assets/TaggedPointerDecode.png
new file mode 100644
index 0000000..fa1acd2
Binary files /dev/null and b/assets/TaggedPointerDecode.png differ
diff --git a/assets/TaggedPointerKind.png b/assets/TaggedPointerKind.png
new file mode 100644
index 0000000..75448ad
Binary files /dev/null and b/assets/TaggedPointerKind.png differ
diff --git a/assets/TaggedPointerNSNumberDemo.png b/assets/TaggedPointerNSNumberDemo.png
new file mode 100644
index 0000000..7ba74de
Binary files /dev/null and b/assets/TaggedPointerNSNumberDemo.png differ
diff --git a/assets/TaggedPointerSaveMemoryUsage.png b/assets/TaggedPointerSaveMemoryUsage.png
new file mode 100644
index 0000000..39c1a7c
Binary files /dev/null and b/assets/TaggedPointerSaveMemoryUsage.png differ
diff --git a/assets/TaggedPointerStructure.png b/assets/TaggedPointerStructure.png
new file mode 100644
index 0000000..672248a
Binary files /dev/null and b/assets/TaggedPointerStructure.png differ
diff --git a/assets/TaggedPointerWillNotCrash.png b/assets/TaggedPointerWillNotCrash.png
new file mode 100644
index 0000000..dff62d5
Binary files /dev/null and b/assets/TaggedPointerWillNotCrash.png differ
diff --git a/assets/TestProjectRPathViaInstallDynamicLib.png b/assets/TestProjectRPathViaInstallDynamicLib.png
new file mode 100644
index 0000000..3e13f49
Binary files /dev/null and b/assets/TestProjectRPathViaInstallDynamicLib.png differ
diff --git a/assets/ThreadRunLoopModeStructure.png b/assets/ThreadRunLoopModeStructure.png
new file mode 100644
index 0000000..c53a0dc
Binary files /dev/null and b/assets/ThreadRunLoopModeStructure.png differ
diff --git a/assets/TouchActionByRunLoopSource0.png b/assets/TouchActionByRunLoopSource0.png
new file mode 100644
index 0000000..5441d18
Binary files /dev/null and b/assets/TouchActionByRunLoopSource0.png differ
diff --git a/assets/UDPMultiplePortCommonUse.png b/assets/UDPMultiplePortCommonUse.png
new file mode 100644
index 0000000..f91b131
Binary files /dev/null and b/assets/UDPMultiplePortCommonUse.png differ
diff --git a/assets/UDPMultiplePortDivideUse.png b/assets/UDPMultiplePortDivideUse.png
new file mode 100644
index 0000000..43334e4
Binary files /dev/null and b/assets/UDPMultiplePortDivideUse.png differ
diff --git a/assets/UDPOrientedPackage.png b/assets/UDPOrientedPackage.png
new file mode 100644
index 0000000..e0a804a
Binary files /dev/null and b/assets/UDPOrientedPackage.png differ
diff --git a/assets/UIViewRefreshProcess.png b/assets/UIViewRefreshProcess.png
new file mode 100644
index 0000000..cb1dd58
Binary files /dev/null and b/assets/UIViewRefreshProcess.png differ
diff --git a/assets/UIViewSystemRenderProcess.png b/assets/UIViewSystemRenderProcess.png
new file mode 100644
index 0000000..5e1415f
Binary files /dev/null and b/assets/UIViewSystemRenderProcess.png differ
diff --git a/assets/UnusedKVOIsaStructure.png b/assets/UnusedKVOIsaStructure.png
new file mode 100644
index 0000000..52306f1
Binary files /dev/null and b/assets/UnusedKVOIsaStructure.png differ
diff --git a/assets/UseIndirectDynamicLibSymbolWhenDynamicLibraryReferencedDynamicLib.png b/assets/UseIndirectDynamicLibSymbolWhenDynamicLibraryReferencedDynamicLib.png
new file mode 100644
index 0000000..cbd4d5e
Binary files /dev/null and b/assets/UseIndirectDynamicLibSymbolWhenDynamicLibraryReferencedDynamicLib.png differ
diff --git a/assets/UsedKVOIsaStructure.png b/assets/UsedKVOIsaStructure.png
new file mode 100644
index 0000000..b27abd4
Binary files /dev/null and b/assets/UsedKVOIsaStructure.png differ
diff --git a/assets/VimManNm.png b/assets/VimManNm.png
new file mode 100644
index 0000000..071506e
Binary files /dev/null and b/assets/VimManNm.png differ
diff --git a/assets/VimModeManNMSearchMode.png b/assets/VimModeManNMSearchMode.png
new file mode 100644
index 0000000..ce5416a
Binary files /dev/null and b/assets/VimModeManNMSearchMode.png differ
diff --git a/assets/WeakObjectCapturedByBlock.png b/assets/WeakObjectCapturedByBlock.png
new file mode 100644
index 0000000..fdb9e1f
Binary files /dev/null and b/assets/WeakObjectCapturedByBlock.png differ
diff --git a/assets/WeakObjectWillReleaseWhenLeaveScope.png b/assets/WeakObjectWillReleaseWhenLeaveScope.png
new file mode 100644
index 0000000..b8b660e
Binary files /dev/null and b/assets/WeakObjectWillReleaseWhenLeaveScope.png differ
diff --git a/assets/WeakReferenceCountTable.png b/assets/WeakReferenceCountTable.png
new file mode 100644
index 0000000..7899097
Binary files /dev/null and b/assets/WeakReferenceCountTable.png differ
diff --git a/assets/XCodoKVOIsaInspect.png b/assets/XCodoKVOIsaInspect.png
new file mode 100644
index 0000000..deb540a
Binary files /dev/null and b/assets/XCodoKVOIsaInspect.png differ
diff --git a/assets/XcconfigFileCanVisitByXcodeScript.png b/assets/XcconfigFileCanVisitByXcodeScript.png
new file mode 100644
index 0000000..5129c0c
Binary files /dev/null and b/assets/XcconfigFileCanVisitByXcodeScript.png differ
diff --git a/assets/XcconfigRPathWithAbsolutePath.png b/assets/XcconfigRPathWithAbsolutePath.png
new file mode 100644
index 0000000..722d719
Binary files /dev/null and b/assets/XcconfigRPathWithAbsolutePath.png differ
diff --git a/assets/XcconfigSetRpathInfo.png b/assets/XcconfigSetRpathInfo.png
new file mode 100644
index 0000000..c9151c1
Binary files /dev/null and b/assets/XcconfigSetRpathInfo.png differ
diff --git a/assets/XcframeworkWillImportArchLibAutomatically.png b/assets/XcframeworkWillImportArchLibAutomatically.png
new file mode 100644
index 0000000..bb2e538
Binary files /dev/null and b/assets/XcframeworkWillImportArchLibAutomatically.png differ
diff --git a/assets/XcodeAddScheme.png b/assets/XcodeAddScheme.png
new file mode 100644
index 0000000..f2567ca
Binary files /dev/null and b/assets/XcodeAddScheme.png differ
diff --git a/assets/XcodeAssembleClass.png b/assets/XcodeAssembleClass.png
new file mode 100644
index 0000000..3b9dd59
Binary files /dev/null and b/assets/XcodeAssembleClass.png differ
diff --git a/assets/XcodeBuildScriptOutputRedirect.png b/assets/XcodeBuildScriptOutputRedirect.png
new file mode 100644
index 0000000..6c5ae0c
Binary files /dev/null and b/assets/XcodeBuildScriptOutputRedirect.png differ
diff --git a/assets/XcodeCreateScheme.png b/assets/XcodeCreateScheme.png
new file mode 100644
index 0000000..4a7eae2
Binary files /dev/null and b/assets/XcodeCreateScheme.png differ
diff --git a/assets/XcodeCreateXCConfig.png b/assets/XcodeCreateXCConfig.png
new file mode 100644
index 0000000..9f6d18a
Binary files /dev/null and b/assets/XcodeCreateXCConfig.png differ
diff --git a/assets/XcodeDebugXcconfigSpecifyLDLinkFlags.png b/assets/XcodeDebugXcconfigSpecifyLDLinkFlags.png
new file mode 100644
index 0000000..680eb23
Binary files /dev/null and b/assets/XcodeDebugXcconfigSpecifyLDLinkFlags.png differ
diff --git a/assets/XcodeDisableTaggedPointerConfuse.png b/assets/XcodeDisableTaggedPointerConfuse.png
new file mode 100644
index 0000000..fc16622
Binary files /dev/null and b/assets/XcodeDisableTaggedPointerConfuse.png differ
diff --git a/assets/XcodeEditDyldEnvValue.png b/assets/XcodeEditDyldEnvValue.png
new file mode 100644
index 0000000..04aaecd
Binary files /dev/null and b/assets/XcodeEditDyldEnvValue.png differ
diff --git a/assets/XcodeMacroSupportedWithOCAndSwift.png b/assets/XcodeMacroSupportedWithOCAndSwift.png
new file mode 100644
index 0000000..f58f1a2
Binary files /dev/null and b/assets/XcodeMacroSupportedWithOCAndSwift.png differ
diff --git a/assets/XcodeProjectUseXcframework.png b/assets/XcodeProjectUseXcframework.png
new file mode 100644
index 0000000..cf8c5ef
Binary files /dev/null and b/assets/XcodeProjectUseXcframework.png differ
diff --git a/assets/XcodeSchemeMatchWithConfigScheme.png b/assets/XcodeSchemeMatchWithConfigScheme.png
new file mode 100644
index 0000000..3d7591e
Binary files /dev/null and b/assets/XcodeSchemeMatchWithConfigScheme.png differ
diff --git a/assets/XcodeSpecifySchemeWithConfig.png b/assets/XcodeSpecifySchemeWithConfig.png
new file mode 100644
index 0000000..292e5d7
Binary files /dev/null and b/assets/XcodeSpecifySchemeWithConfig.png differ
diff --git a/assets/XcodeSwitchSchemeManually.png b/assets/XcodeSwitchSchemeManually.png
new file mode 100644
index 0000000..e22fd98
Binary files /dev/null and b/assets/XcodeSwitchSchemeManually.png differ
diff --git a/assets/XcodeUnitTestDyanimcFramework.png b/assets/XcodeUnitTestDyanimcFramework.png
new file mode 100644
index 0000000..f80b8bb
Binary files /dev/null and b/assets/XcodeUnitTestDyanimcFramework.png differ
diff --git a/assets/XcodebuildArchiveSuccessResult.png b/assets/XcodebuildArchiveSuccessResult.png
new file mode 100644
index 0000000..2f28ca3
Binary files /dev/null and b/assets/XcodebuildArchiveSuccessResult.png differ
diff --git a/assets/XcodebuildDynamicLibResult.png b/assets/XcodebuildDynamicLibResult.png
new file mode 100644
index 0000000..c57c0b3
Binary files /dev/null and b/assets/XcodebuildDynamicLibResult.png differ
diff --git a/assets/XcodebuildResultWithBitcodeEnable.png b/assets/XcodebuildResultWithBitcodeEnable.png
new file mode 100644
index 0000000..18bafd3
Binary files /dev/null and b/assets/XcodebuildResultWithBitcodeEnable.png differ
diff --git a/assets/XcodebuildXCFrameworkWithDebugSymbol.png b/assets/XcodebuildXCFrameworkWithDebugSymbol.png
new file mode 100644
index 0000000..38301ca
Binary files /dev/null and b/assets/XcodebuildXCFrameworkWithDebugSymbol.png differ
diff --git a/assets/XcodebuildXcframeworkResult.png b/assets/XcodebuildXcframeworkResult.png
new file mode 100644
index 0000000..f8e46d4
Binary files /dev/null and b/assets/XcodebuildXcframeworkResult.png differ
diff --git a/assets/block-object-memoery.png b/assets/block-object-memoery.png
deleted file mode 100644
index daaa711..0000000
Binary files a/assets/block-object-memoery.png and /dev/null differ
diff --git a/assets/block-strong-object-memoery.png b/assets/block-strong-object-memoery.png
new file mode 100644
index 0000000..c7563ae
Binary files /dev/null and b/assets/block-strong-object-memoery.png differ
diff --git a/assets/block-structure.png b/assets/block-structure.png
index 0749283..0a83e0e 100644
Binary files a/assets/block-structure.png and b/assets/block-structure.png differ
diff --git a/assets/block-weak-object-memoery.png b/assets/block-weak-object-memoery.png
new file mode 100644
index 0000000..70d5fb9
Binary files /dev/null and b/assets/block-weak-object-memoery.png differ
diff --git a/assets/dyldEnvUsage.png b/assets/dyldEnvUsage.png
new file mode 100644
index 0000000..018b8ba
Binary files /dev/null and b/assets/dyldEnvUsage.png differ
diff --git a/assets/dyldFullProcess.png b/assets/dyldFullProcess.png
new file mode 100644
index 0000000..35633c7
Binary files /dev/null and b/assets/dyldFullProcess.png differ
diff --git a/assets/dyldFunctionHookResult.png b/assets/dyldFunctionHookResult.png
new file mode 100644
index 0000000..08b48b2
Binary files /dev/null and b/assets/dyldFunctionHookResult.png differ
diff --git a/assets/dyldInsetDylibResult.png b/assets/dyldInsetDylibResult.png
new file mode 100644
index 0000000..e8f2944
Binary files /dev/null and b/assets/dyldInsetDylibResult.png differ
diff --git a/assets/dyldWorkFlowViaMachOStructure.png b/assets/dyldWorkFlowViaMachOStructure.png
new file mode 100644
index 0000000..b53ee7f
Binary files /dev/null and b/assets/dyldWorkFlowViaMachOStructure.png differ
diff --git a/assets/objc-isa.png b/assets/objc-isa.png
index eb72d11..412685c 100644
Binary files a/assets/objc-isa.png and b/assets/objc-isa.png differ
diff --git a/assets/objc-metaclass-superclass.png b/assets/objc-metaclass-superclass.png
index a46f193..c301af5 100644
Binary files a/assets/objc-metaclass-superclass.png and b/assets/objc-metaclass-superclass.png differ
diff --git a/assets/objc-superclass.png b/assets/objc-superclass.png
index af02308..dfdcfa8 100644
Binary files a/assets/objc-superclass.png and b/assets/objc-superclass.png differ
diff --git a/assets/objc_msgSend_fix1.png b/assets/objc_msgSend_fix1.png
new file mode 100644
index 0000000..0cb8a1b
Binary files /dev/null and b/assets/objc_msgSend_fix1.png differ
diff --git a/assets/objc_msgSend_fix2.png b/assets/objc_msgSend_fix2.png
new file mode 100644
index 0000000..8be722d
Binary files /dev/null and b/assets/objc_msgSend_fix2.png differ
diff --git a/assets/osunfairlock-assemble1.png b/assets/osunfairlock-assemble1.png
index d8b3dea..242fe70 100644
Binary files a/assets/osunfairlock-assemble1.png and b/assets/osunfairlock-assemble1.png differ
diff --git a/assets/osunfairlock-assemble2.png b/assets/osunfairlock-assemble2.png
index 8eb4786..1086e89 100644
Binary files a/assets/osunfairlock-assemble2.png and b/assets/osunfairlock-assemble2.png differ
diff --git a/assets/osunfairlock-assemble3.png b/assets/osunfairlock-assemble3.png
index 854db56..ad5ac31 100644
Binary files a/assets/osunfairlock-assemble3.png and b/assets/osunfairlock-assemble3.png differ
diff --git a/assets/osunfairlock-assemble4.png b/assets/osunfairlock-assemble4.png
index 21114fd..eda0c7e 100644
Binary files a/assets/osunfairlock-assemble4.png and b/assets/osunfairlock-assemble4.png differ
diff --git a/assets/osunfairlock-assemble5.png b/assets/osunfairlock-assemble5.png
index 132e775..84b89ec 100644
Binary files a/assets/osunfairlock-assemble5.png and b/assets/osunfairlock-assemble5.png differ
diff --git a/assets/osunfairlock-assemble6.png b/assets/osunfairlock-assemble6.png
new file mode 100644
index 0000000..f2e63f5
Binary files /dev/null and b/assets/osunfairlock-assemble6.png differ
diff --git a/assets/runtime-class.png b/assets/runtime-class.png
index 3899fda..6d93138 100644
Binary files a/assets/runtime-class.png and b/assets/runtime-class.png differ
diff --git a/assets/runtime-forwarding.png b/assets/runtime-forwarding.png
index bdde97c..206a65c 100644
Binary files a/assets/runtime-forwarding.png and b/assets/runtime-forwarding.png differ
diff --git a/assets/runtime-method-find.png b/assets/runtime-method-find.png
index 1e16c1a..aeeac46 100644
Binary files a/assets/runtime-method-find.png and b/assets/runtime-method-find.png differ
diff --git a/assets/runtime-objc_msgSend-ResolveMethod.png b/assets/runtime-objc_msgSend-ResolveMethod.png
index 824becf..e21ed9a 100644
Binary files a/assets/runtime-objc_msgSend-ResolveMethod.png and b/assets/runtime-objc_msgSend-ResolveMethod.png differ
diff --git a/assets/runtime-objc_msgSend-messageSend.png b/assets/runtime-objc_msgSend-messageSend.png
index 26b2d12..09ea74d 100644
Binary files a/assets/runtime-objc_msgSend-messageSend.png and b/assets/runtime-objc_msgSend-messageSend.png differ
diff --git a/assets/synchronized-asemble.png b/assets/synchronized-asemble.png
deleted file mode 100644
index ac4f92f..0000000
Binary files a/assets/synchronized-asemble.png and /dev/null differ
diff --git a/assets/synchronized-asemble1.png b/assets/synchronized-asemble1.png
new file mode 100644
index 0000000..d84e555
Binary files /dev/null and b/assets/synchronized-asemble1.png differ
diff --git a/assets/synchronized-asemble2.png b/assets/synchronized-asemble2.png
new file mode 100644
index 0000000..03f086d
Binary files /dev/null and b/assets/synchronized-asemble2.png differ