diff --git a/Chapter1 - iOS/1.102.md b/Chapter1 - iOS/1.102.md
index af97e1b..de878d7 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`
@@ -199,15 +199,15 @@ LLVM IR 有3种表示格式:
## 调试 LLVM
选择 Edit Scheme.
-
-
-
-
-
-
+
+
+
+
+
+
最后就可以加断点进行 Debug 了。但为了让调试更有意义,类似 `nm -a /Users/unix_kernel/Library/Developer/Xcode/DerivedData/LDExploreDemo-ehvvtxafpkdkubgrswvvsudzhqbb/Build/Products/Debug-iphonesimulator/LDExploreDemo.app/LDExploreDemo` 一样可以查看到更有意义的信息,可以在 Edit Scheme 面板中 `Run -> Arguments -> Arguments Passed On Launch` section 中的 **+** 点击,添加一些参数,如下图:
-
+
最后允许测试。注意:LLVM 项目较大,可以选择顶部 "Product -> Perform Action -> Run Without Building".
@@ -319,9 +319,9 @@ Tips:ninja 如果安装失败,可以直接从 [github]( https://github.com/n
因为要编写 Clang 插件,是 c++ 代码,所以需要借助 IDE 的能力,我们选用 Xcode 进行编译。如下图所示,代表编译成功
-
+
-
+
@@ -349,11 +349,11 @@ Tips:ninja 如果安装失败,可以直接从 [github]( https://github.com/n
- 先创建一个插件文件夹 `code-style-validate-plugin`
-
+
- 编辑 `CMakeLists.txt` 文件,在最后添加 `add_clang_subdirectory(code-style-validate-plugin)`
-
+
@@ -382,17 +382,17 @@ Xcode 打开项目,选择自动创建 Schemes
-
+
选择 Target 为 `CodeStyleValidatePlugin`,源代码所在文件夹为 `Sources/Loadable modules`,然后选中 CodeStyleValidatePlugin.cpp` 文件进行编写逻辑
-
+
初步编写后 Command + B 进行编译,在 Products 下可以看到编译产物:`CodeStyleValidatePlugin.dylib` 动态库。
-
+
@@ -402,7 +402,7 @@ Xcode 打开项目,选择自动创建 Schemes
此步骤的目的是:在 testLLVM 项目中,加载 `CodeStyleValidatePlugin.dylib` 插件可以成功。因为默认的 Xcode 使用的 clang/clang++ 编译器和编译 `CodeStyleValidatePlugin.dylib` 动态库不是一个版本。不做修改的话,Xcode 加载 `CodeStyleValidatePlugin.dylib` 会报错。所以需要先编译出同一个 LLVM 版本的 clang/clang++。
-
+
@@ -421,7 +421,7 @@ Xcode 打开项目,选择自动创建 Schemes
- `-Xclang`
- 插件名称
-
+
@@ -429,7 +429,7 @@ Xcode 打开项目,选择自动创建 Schemes
在新创建的 TestLLVM Xcode 项目中加载创建的 `CodeStyleValidatePlugin.dylib` 会报错。原因是:由于 Clang 插件需要使用对应的版本去加载,如果版本不一致则会导致编译错误。如下所示:
-
+
@@ -441,17 +441,17 @@ Xcode 打开项目,选择自动创建 Schemes
如下所示:
-
+
继续编译还是会报错,报错如下:
-
+
解决方案为:在 `Build Settings` 栏目中搜索 `index`,将 `Enable Index-Wihle-Building Functionality` 的 ` Default` 改为 `NO`。
-
+
@@ -463,7 +463,7 @@ Tips: 由于重新修改了插件的源码,所以每次 Build 构建完 FANP
编译成功,可以看到在日志中输出了我们编写的日志信息。
-
+
@@ -496,7 +496,7 @@ NS_ASSUME_NONNULL_END
利用 Clang 查看 AST 指令为 `clang -fmodules -fsyntax-only -Xclang -ast-dump workaholic_person.m`
-
+
核心思路为:我们要分析类名不符合规范的情况,要精确报错,首先要识别到类名,利用 AST 的能力可以办到(类名在 AST 的 `ObjCInterfaceDecl` 节点上)。然后获取到类名的行号信息,精确报错。
@@ -786,7 +786,7 @@ X("CodeStyleValidatePlugin", "This plugin is designed for scanning code styles,
- 可以对 Category 名做检测,如果带下划线,则报错提示并给出修改意见
- 编写的 `CodeStyleValidatePlugin` Demo 中对不符合规范的做了 `DiagnosticsEngine::Warning` 级别的警告。如果遇到1个警告则不影响,继续编译。如果是 `DiagnosticsEngine::Error` 级别的编译报错,遇到1个则终止编译,请注意该区别,按需编写自己的插件逻辑。
-
+
@@ -834,7 +834,7 @@ X("CodeStyleValidatePlugin", "This plugin is designed for scanning code styles,
- 使用 LLVM 编写 Clang 插件,解析 AST 拿到所有的 `ObjCInterfaceDecl` 信息,然后结合 `ObjCMethodDecl` 信息便可获取 Category 中的 所有方法,再判断方法是否同名
-
+
@@ -872,7 +872,7 @@ LLVM 项目中 clang 内置了一堆 checker,用于实现 lint。
- 自动调用 `[super dealloc]`
-
+
- 为每个拥有 ivar 的 Class 合成` .cxx_destructor` 方法来自动释放类的成员变量,代替 MRC 时代的 `self.xxx = nil`
diff --git a/Chapter1 - iOS/1.111.md b/Chapter1 - iOS/1.111.md
index 5479def..13494ab 100644
--- a/Chapter1 - iOS/1.111.md
+++ b/Chapter1 - iOS/1.111.md
@@ -1,7 +1,7 @@
## 写给 iOSer 的鸿蒙开发 tips
## 下载问题
-
+
The other possible cause is that the system language of the PC is English and the region code is US. You could try to perform the following operations to change the region code to CN. Before changing the region code, close DevEco Studio.
@@ -104,7 +104,7 @@ For Mac OS: ~/Library/Application Support/Huawei/DevEcoStudio3.0/options/country
参考鸿蒙 RN 团队在1月份的方案,使用 stack 组件内部,forEach 循环渲染子组件。适配初期,在嵌套不深的页面没有发现问题,整体上打通了从 JS 代码到引擎渲染的核心流程。
-
+
左侧的 UI 树和右侧的 Model 树,通过 `@ObjectLink` 、`@Observed` 来进行数据渲染和刷新。
@@ -224,23 +224,23 @@ Apsect.hookMethod({
无统一修改点场景三:`router.pushUrl`,编译时 + 运行时组合实现偷梁换柱。
-
+
如何实现编译时替换?
鸿蒙提供了 Hvigor Plugin 编译时自定义插件,用于实现定制化构建。思路:直接利用 Hvigor Plugin hook 某个编译 task
-
+
那到底 hook 哪个编译 task?
-
+
问题:
- output 修改无效
-
+
@@ -252,7 +252,7 @@ Apsect.hookMethod({
- input 无法 hook,Hvigor plugin 暂未开放相关能力
-
+
Hvigor 目前开放的能力有限,仅支持在默认的 task 前后加一些钩子函数。但无法修改 input。此路不通
@@ -260,7 +260,7 @@ Apsect.hookMethod({
思路:copy -> modify -> revert
-
+
@@ -302,13 +302,13 @@ Apsect.hookMethod({
于是,整个流程就变成了
-
+
可以利用 [Typescript AST Viewer](https://github.com/dsherret/ts-ast-viewer) 来查看 AST 抽象语法树信息。可以在线查看 AST,官网地址为:[Typescript AST Viewer](https://ts-ast-viewer.com)
-
+
diff --git a/Chapter1 - iOS/1.112.md b/Chapter1 - iOS/1.112.md
index 847f123..7fd81a1 100644
--- a/Chapter1 - iOS/1.112.md
+++ b/Chapter1 - iOS/1.112.md
@@ -25,15 +25,15 @@ print("over")
- `var season: Season = Season.spring` 基础枚举,默认值是0。
-
+
- `season = Season.summer`,此时可以看到第一个字节的位置是1.
-
+
- `season = Season.antumn` ,此时可以看到第一个字节的位置是2
-
+
结论:查看内存信息,可以看到**不带关联值、不带原始值的基础枚举,只占1个字节大小空间,且值为默认值**
@@ -69,11 +69,11 @@ print("over")
- `var season: Season = Season.spring` 基础枚举,变量默认值,可以看到第一个字节的位置是0
-
+
- `season = .winter` 基础枚举,当赋值为 winter 的时候,可以看到第一个字节的位置是3
-
+
结论:**只带有原始值的枚举,同样只占用1个字节,该字节的值为枚举的位置索引(比如1、2),而非原始值。原始值不占用枚举的内存**
@@ -107,7 +107,7 @@ print("over")
`.spring` 有3个 Int,单个 Int 占8个字节空间,所以红色框代表 spring 的1,蓝色框代表 spring 的2,绿色框代表 spring 的3,黄色框代表枚举的第1个 case,剩余7个字节,为空。后续的7个字节是为了内存对齐而补齐的内存。
-
+
其内存信息如下(8字节为1组,对应上图)
@@ -133,7 +133,7 @@ print("over")
- `season = Season.summer(4, 5)` 带有关联值的枚举,`.summer` 这个枚举关联值有2个 Int,单个 Int 占8个字节空间,所以红色框代表 summer 第一个关联值 4,蓝色框代表 summer 第二个关联值 5,绿色框为空,黄色框代表枚举的第2个 case,剩余7个字节,为空。
-
+
其内存信息如下(8字节为1组,对应上图)
@@ -147,7 +147,7 @@ print("over")
- `season = Season.antumn(6) 带有关联值的枚举,`. `antumn` 存在关联值 1个 Int,单个 Int 占8个字节空间,所以红色框代表 antumn 的6,蓝色框为空,绿色框为空,黄色框代表枚举的第3个 case,剩余7个字节,为空。
-
+
其内存信息如下(8字节为1组,对应上图)
@@ -161,7 +161,7 @@ print("over")
- `season = Season.winter(true)` 带有关联值的枚举,`. `winter` 关联值是 1个 Bool,单个 Bool 占1个字节空间,所以红色框代表 winter 的 true,蓝色框为空,绿色框为空,黄色框代表枚举的第4个 case,剩余7个字节,为空。
-
+
其内存信息如下(8字节为1组,对应上图)
@@ -175,7 +175,7 @@ print("over")
- `season = Season.unknown` 带有关联值的枚举,`unknown` 没有关联值,所以红色框为空,蓝色框为空,绿色框为空,黄色框代表枚举的第5个 case,剩余7个字节,为空。
-
+
其内存信息如下(8字节为1组,对应上图)
@@ -299,7 +299,7 @@ case1 占用8个字节,case2 占用1个字节,能用 case1 的8个字节的
更改验证标签对于枚举占用内存大小的影响
-
+
可以看到 enum 只有1个 case 的时候,内存大小只和最大关联值大小有关,1个 case 的情况下不需要额外的空间来判断所属哪个 case。
@@ -329,7 +329,7 @@ print("over")
断点停到 `var season: Season = Season.spring(1, 2, 3)` 位置
-
+
将断点处的汇编单独摘出来研究
diff --git a/Chapter1 - iOS/1.113.md b/Chapter1 - iOS/1.113.md
index 4039155..c9f26bd 100644
--- a/Chapter1 - iOS/1.113.md
+++ b/Chapter1 - iOS/1.113.md
@@ -18,7 +18,7 @@ var point = Point()
在`init` 方法内第一行处加 断点,如下所示
-
+
实验2:struct 内不自己加 init
@@ -32,7 +32,7 @@ var point = Point()
在`var point = Point()`处加 断点,如下所示
-
+
现象:可以看到加不加自定义初始化器的汇编代码基本相同。
@@ -129,13 +129,13 @@ test()
断点打在 `var point1 = Point(x: 10, y: 20)` 处,查看汇编
-
+
乍一看如果不认识的话,先找字面量(立即数),比如红色框内的 `0xa`,就是 10,`0x14` 就是20。[之前](./109.md)学过寄存器的设计,64位寄存器是兼容32位寄存器的。红色框内将 `0xa`,也就是 10 保存到 `%edi ` 寄存器内部,也就是保存到 `%rdi` 中,将 `0x14` 也就是20,保存到 `%esi` 也就是保存到 `%rsi` 寄存器中。
LLDB 模式下输入 `si` 进入 init 方法内部。
-
+
可以查看到将 `%rsi` 里的10 保存到 `%rdx` 中了;将 `%rdi` 里的20 保存到 `%rax` 中了。这也就是 struct `init` 方法做的事情。
@@ -251,11 +251,11 @@ testReferenceType()
下断点,可以看到下面的汇编:
-
+
在调用(汇编的 call)完 `allocating_init` 方法后,方法返回值用 `%rax` 保存的。然后打印出 `%rax` 寄存器的值,查看内存信息如下
-
+
红色框代表类信息的地址,蓝色框代表引用计数,绿色框代表10,黄色框代表20.
diff --git a/Chapter1 - iOS/1.114.md b/Chapter1 - iOS/1.114.md
index 19a10e6..5fc2a74 100644
--- a/Chapter1 - iOS/1.114.md
+++ b/Chapter1 - iOS/1.114.md
@@ -43,9 +43,9 @@ print("全局变量", Mems.ptr(ofVal: &p))
print("堆空间", Mems.ptr(ofRef: p))
```
-
+
-
+
代码段:Person.sayHi 0x1000034d0
@@ -183,7 +183,7 @@ print(fn(2)) // 2
print(fn(3)) // 3
```
-
+
@@ -207,7 +207,7 @@ print(fn(3)) // 6
-
+
可以看到上面有 `allocObject` 方法调用,说明产生了堆空间对象,用于存放 `num` 。是由 `var fn = getFn()` 造成的,调用1次 `getFn` 则产生1次堆空间分配,用于保存 num。
@@ -235,7 +235,7 @@ print(fn3(3)) // 4
我们在汇编 `swift_allocObject` 下面下个断点
-
+
@@ -243,7 +243,7 @@ print(fn3(3)) // 4
敏感点,查看到字面量1,给汇编代码15行加断点,执行完15行,继续查看内存信息
-
+
可以看到内存数据发生了改变。绿色框内有了值1。
@@ -253,13 +253,13 @@ print(fn3(3)) // 4
敏感点,查看到字面量1,给汇编代码15行加断点,执行完15行,继续查看内存信息
-
+
第三次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 `Debug -> Debug Workflow -> View Memory` 查看内存信息`0x000060000020d460`。此时还没值。
敏感点,查看到字面量1,给汇编代码15行加断点,执行完15行,继续查看内存信息
-
+
打印结果也说明了问题,因为调用3次 `getFn()` 所以会在堆上 alloc 3块内存,用于保存捕获的变量。所以调用 fn1 得到 2,调用 fn2 得到 3,调用 fn3 得到 4。
@@ -279,7 +279,7 @@ print(fn(1, 2)) // 3
在 `var fn = sum` 处下断点,可以看到下面汇编
-
+
我们知道 `leaq` 是从 `%rip + 0x10f` 算出来的地址,赋值给 `%rax`。所以大概可以推测 `%rip + 0x10f` 就是 `sum` 函数地址。LLDM p 打印 `sum` 地址为 `0x0000000100003a30`。
@@ -297,15 +297,15 @@ print(fn(1, 2)) // 3
直奔主题,研究闭包内存
-
+
可以看到在调用完第六行的函数后将寄存器 `rax`、`rdx` 里的值取出来使用了。进入函数内部看看发生了什么,LLDB 输入 `si`
-
+
可以看到 `rax` 里面存放了 `plus` 函数地址。第7行汇编是异或运算,2个 `ecx` 异或,结果为0,写入到 `ecx` 里。然后第8行汇编将 `ecx` 里的0写入到 `edx`,`edx` 也就是 `rdx`。走完第6行的汇编,继续看第7、8行
-
+
将第6行函数的返回结果 `rax` 里的函数地址赋值给 `rip +0x89e7 = 0x100003819 + 0x89e7 = 0x10000C200 `, 第7行的`rdx` 的值赋值个给 `rip +0x89e8 = 0x100003820 + 0x89e8 = 0x10000C208`。
@@ -315,7 +315,7 @@ print(fn(1, 2)) // 3
继续对比实验,查看闭包放了什么东西(注意和上面的实验不同,下面存在闭包)
-
+
基本可以断定:函数会返回一个长度为16 Byte 的内存。分别保存在 `rax` 和 `rdx`上。所以针对性的研究 `rdx` 和 `rax`
@@ -323,7 +323,7 @@ print(fn(1, 2)) // 3
20行的 `rax` 保存了一个看似是 `plus` 的函数地址。具体是:`rip + 0x167 = 0x100003969 + 0x167= 0x100003C37 `
-
+
继续走,走到真正的 `plus` 方法内,可以看到函数地址是 `0x100003970` 。所以返回的 `rax` 里面可能是间接调用真正的 `plus` 函数的。
@@ -340,13 +340,13 @@ Tips:由于地址是动态生成的,所以真正去调用 plus 的时候一
顺着思路,分析下汇编:
-
+
我们看到25行 `callq *%rax` 存在动态调用,所以需要找到 `%rax` 里的值是哪里来的。然后顺着向上找,找到23行 `movq -0x30(%rbp), %rax`。然后继续向上看到16行 `movq %rax, -0x30(%rbp)`。继续向上看到15行 `movq 0x88db(%rip), %rax`。相当于就是在 `rip + 0x88db ` 处取出8个字节出来,当作函数地址调用(汇编代码的右边写了,`fn1` ),地址为:`0x88db(%rip) = rip + 0x88db = 0x10000392d + 0x88db = 0x10000C208` 。
断点继续放开,在汇编25行处加断点 `callq *%rax`
-
+
可以看到在方法内部,第6行汇编处,直接调用一个代码段的函数地址 `jmp 0x1000039f0 `
@@ -357,13 +357,13 @@ Tips:由于地址是动态生成的,所以真正去调用 plus 的时候一
LLDB 输入 `si`,可以看到是 `plus` 函数真正的地址
-
+
`fn1` 函数调用的时候,参数如何传递?
-
+
汇编17行 `movq 0x88d8(%rip), %r13 ; SwiftDemo.fn1 : (Swift.Int) -> Swift.Int + 8`可以看到将返回的 fn1 本身16字节的后8个字节,也就是堆地址空间值,保存到寄存器 `edi` 也就是寄存器 `rdi` 上了。
@@ -371,7 +371,7 @@ LLDB 输入 `si`,可以看到是 `plus` 函数真正的地址
然后 LLDB 输入 `si` 去分析 callq 内部
-
+
@@ -379,7 +379,7 @@ LLDB 输入 `si`,可以看到是 `plus` 函数真正的地址
继续输入 `si`
-
+
可以看到汇编的第5行 `movq %rsi, -0x50(%rbp)` 将 `rsi` 里的1,保存到 `rbp - 0x50` 处。第6行汇编 `movq %rdi, -0x58(%rbp)` 将 `rdi` 堆地址值保存到 `rbp - 0x58` 处。
@@ -387,7 +387,7 @@ LLDB 输入 `si`,可以看到是 `plus` 函数真正的地址
然后真正做 `plus` 加法运算的就是28行的 `addq 0x10(%rsi), %rdi`, 从 `rsi` 堆地址值的第16个字节的地方取出8个字节的值,也就是捕获的外部变量 `num` 再和参数1相加。
-
+
可以看到第6行堆地址空间的值写入到 `rbp -0x58` ,第26行又将 `rbp -0x58` 写入到 `rdi`,29行将 `rdi` 的值,写入到 `rbp - 0x48`,34行将 `rbp - 0x48` 写入到 `rcx`,35行将 `rcx` 的地址值,写入到 `rax` 对应的存储空间。也就是堆上的 `num` 值已经修改,被覆盖了。
@@ -502,7 +502,7 @@ serve(customer: group.remove(at: 0))
但如果函数参数没有加 `@autoclosure`,在调用函数的时候,传参没有加 `{}` 编译器是会报错的
-
+
正确的做法是:要么加 `@autoclosure` 要么在函数调用的时候加 `{}`
diff --git a/Chapter1 - iOS/1.115.md b/Chapter1 - iOS/1.115.md
index 9d41edd..e59869c 100644
--- a/Chapter1 - iOS/1.115.md
+++ b/Chapter1 - iOS/1.115.md
@@ -122,11 +122,11 @@ getDiameter () {
然后通过汇编来窥探下,在 `circle.diameter = 22` 处加断点,可以看到本质上调用的就是 `setter` 方法,`setter` 内部的实现就不一一窥探了
-
+
然后断点继续,在 `let diameter = circle.diameter` 处加断点,可以看到调用了 getter 方法
-
+
计算属性的本质就是方法,看上去是属性,但是不占用结构体的内存。而是独立在代码段中,所以只占用1个 Int 即8个字节的大小。
@@ -300,7 +300,7 @@ let season = Season.summer
print(season.rawValue)
```
-
+
通过汇编 `SwiftDemo.Season.rawValue.getter` 可以看到,在调用 **`enum` 的 `rawvalue` 的时候本质是通过计算属性调用 `getter` 来实现的**。
@@ -431,11 +431,11 @@ width is 20, side is 4, girth is 80
-
+
然后看到17行的关键代码,LLDB 输入 `si`,可以看到在第6行 `movq $0x14, (%rdi)`,将16进制的 `0x14` 也就是20,移动到指定的内存地址 `rdi` 上
-
+
因为 `struct` 结构体内存布局中,成员变量在内存中是连续存储的。所以 `struct `的地址也就是 `struct` 中第一个成员变量 `width` 的地址。
@@ -462,7 +462,7 @@ width is 5, side is 4, girth is 20
在 `changeValue(&shape.girth)` 处下断点,查看汇编
-
+
核心思路:方法参数用 `inout`修饰,则传递的是引用(内存地址)。
@@ -476,7 +476,7 @@ width is 5, side is 4, girth is 20
- LLDB 输入 `si` 查看 changeValue 内部。可以看到第6行 `movq $0x14, (%rdi)` 直接将20赋值给 `rdi`
-
+
@@ -511,7 +511,7 @@ width is 10, side is 20, girth is 200
在 `changeValue(&shape.side)` 处添加断点,查看汇编
-
+
分析:
@@ -529,7 +529,7 @@ width is 10, side is 20, girth is 200
- LLDB 输入 `si`,可以看到 `setter` 方法内部,调用了 `willSet`、`didSet`
-
+
@@ -605,7 +605,7 @@ width is 10, side is 20, girth is 200
Demo1:
-
+
`movq $0xa, 0x86d1(%rip) ` num1 的地址为: `rip + 0x86d1 = 0x100003b0f + 0x86d1 = 0x10000C1E0 `
@@ -619,7 +619,7 @@ Demo1:
Demo2
-
+
`movq $0xa, 0x8b65(%rip)` num1 的内存为 `rip + 0x8b65 = 0x1000037c3 + 0x8b65 = 0x10000C328 `
@@ -650,17 +650,17 @@ Manager.count = 11
- 抽象内存访问隐藏实际存储位置(可能位于不同段或延迟分配)
- 支持属性观察(didSet)通过封装访问点插入回调逻辑
-
+
lldb 输入 si 查看具体实现
-
+
可以看到底层调用了 `swift_once` 函数,函数传递了2个参数, rsi 存储 dispatch_once 的 block 参数,rdi 存储了 onceToken
继续敲 si 可以看到底层调用的就是 GCD 的 `dispatch_once` 函数。
-
+
diff --git a/Chapter1 - iOS/1.116.md b/Chapter1 - iOS/1.116.md
index 1ecb47f..67d00bd 100644
--- a/Chapter1 - iOS/1.116.md
+++ b/Chapter1 - iOS/1.116.md
@@ -159,10 +159,10 @@ Dog.speak() // Animal speak dog is bark
但如果将 `Animal` 方法的 `class` 改为 `static`,就无法 `override` 了
-
+
-
+
- 如果父类的方法是被 class 修饰的,子类继承后重写时,可以将 class 改为 static。
- 虽然子类可以将父类方法的 class 改为 static。但影响的是当前子类的子类,无法再重写方法了。
@@ -294,9 +294,9 @@ Animal sleep
在 `animal.speak()` 处加断点,可以看到
-
+
-
+
解释:
@@ -310,7 +310,7 @@ Animal sleep
画了张图,也就是说 `rax` 中存放了 Dog 对象内存中的前8个字节,也就是下图的最右侧
-
+
@@ -415,7 +415,7 @@ print(num) // Optional(12)
1. 不允许同时定义参数标签、参数个数、参数类型相同的可失败初始化器和非可失败初始化器。因为在外部调用的时候,不知道到底是使用哪个初始化方法。编译器会报错 `Invalid redeclaration of 'init(_:)'`
-
+
2. 可以用 `init!` 来定义隐式解包的可失败初始化器
@@ -453,7 +453,7 @@ print(num) // Optional(12)
}
```
-
+
且前面的写法比较危险,假设第一个 `init?` 返回 `nil`,第二个 `convenience init()` 去对 nil 强制解包,则会 crash
@@ -732,7 +732,7 @@ var personType: Person.Type = Person.self
-
+
在第二行代码下断点,可以看到关键的汇编是第8行和第12行:
@@ -750,7 +750,7 @@ var personType: Person.Type = Person.self
- `metadata` 结构类似下图右侧
-
+
@@ -951,7 +951,7 @@ class Person: Runable {
就像上面[多态实现的原理](#target-anchor)这里讲到的一样
-
+
查表是一种简单、易实现、性能可预知的方式。然而,这种派发方式比起直接派发来说,还是慢了一点(从字节码的角度来看,多了两次读和一次跳转。由此带来了性能损耗)。另一个慢的原因在于编译器可能会由于函数内执行的任务,导致无法优化(如果函数带有副作用的话)
diff --git a/Chapter1 - iOS/1.119.md b/Chapter1 - iOS/1.119.md
index 5efabe8..dbd4890 100644
--- a/Chapter1 - iOS/1.119.md
+++ b/Chapter1 - iOS/1.119.md
@@ -7,7 +7,7 @@
-
+
@@ -19,7 +19,7 @@ var str1: String = "0123456789"
实验很简单,就一行代码。来窥探下 String 的初始化和内存结构。出发断点看到下面汇编:
-
+
简单分析下:
@@ -45,7 +45,7 @@ QA:这个10是什么东西?1是什么东西?
var str1: String = "01234"
```
-
+
可以看到其他和上面的没变化,唯一不同的是 `movl $0x5, %esi` 将5赋值给寄存器 `esi`,即寄存器 `rsi`。实验的字符串 "01234" UTF8 字符个数为5
@@ -55,7 +55,7 @@ var str1: String = "01234"
var str1: String = "01234😄"
```
-
+
可以看到将9赋值给寄存器 `esi` 即 `rsi`,字符串赋值给寄存器 `rdi`,`xorl %edx, %edx` 异或运算结果 0 赋值给寄存器 `edx` 即 `rdx`,也就是不纯粹为
@@ -93,7 +93,7 @@ extension String: _ExpressibleByBuiltinStringLiteral {
继续探索:
-
+
可以看到 `str1` 地址为 `rip + 0x8853 = 0x1000039a5 + 0x8853 = 0x10000C1F8 `,然后读取对应堆上的内容为:
@@ -128,7 +128,7 @@ var str1: String = "0123456789ABCDEF"
print(Mems.memStr(ofVal: &str1))
```
-
+
分析下:
@@ -154,7 +154,7 @@ print(Mems.memStr(ofVal: &str1))
- LLDB 输入 finish 结束函数细节,外部可以看到第10行 `movq %rdx, 0x8864(%rip) ` 将 `rdx` 寄存器里的值(也就是:`字符串真实地址` + `0x7fffffffffffffe0` )赋值给 `str1` 指针的后8个字节
-
+
@@ -172,13 +172,13 @@ var str1: String = "0123456789ABCDEF"
字符串地址为 ` 0x10000397f + 0x3ea1 = 0x100007820`,看着像全局变量、也有可能是常量区?字符串内容写死的情况下,编译器编译后内存地址应该可以确定,那到底在什么地方呢?利用 `MachOView` 窥探下 `MachO` 文件吧
-
+
利用 MachOView 打开如下
-
+
@@ -217,7 +217,7 @@ print(Mems.memStr(ofVal: &str1)) // 0xd000000000000010 0x8000000100007800
print(Mems.memStr(ofVal: &str2)) // 0x0000353433323130 0xe600000000000000
```
-
+
可以看到:
@@ -323,7 +323,7 @@ print("explore")
上面可知, `字符串真实地址 = 指针内存8个字节地址 - 0x7fffffffffffffe0`,又等价于 `字符串真实地址 = 指针内存8个字节地址 + 0x20`
-
+
字符串真实地址:`0x0000600001700440 + 0x20 = 0x0000600001700460`,LLDB `x 0x0000600001700460 ` 可以看到字符串拼接后的结果。看上去是存储在堆上。如何验证?
@@ -331,11 +331,11 @@ print("explore")
我们知道堆上的内存初始化在 Swift 侧,关键方法为 `swift_allocObject ` -> `swift_slowAlloc` -> `malloc `。给 malloc 下断点,然后在断点出查看调用堆栈,可以看到在 `string.append()` 后有堆分配内存
-
+
结束当前函数调用,在外层可以看到 str2 地址值的后8个字节为 `0x000060000170410`,再 + `0x20` 就是字符串真实地址,打印如下。
-
+
0x20 是什么?这32个字节存放了什么信息?存储字符串的描述信息,比如:引用计数、字符串长度等信息。
@@ -374,7 +374,7 @@ Swift 中 `String` 类型的初始化方法(`init`)的地址是否采用延
-
+
`__stub_helper` 节是 `__TEXT` 段中的一个特定节,它包含了用于处理符号懒加载的辅助函数和代码。当动态链接的符号首次被调用时,这些辅助函数会被触发,以解析符号并将其地址绑定到调用点。
diff --git a/Chapter1 - iOS/1.121.md b/Chapter1 - iOS/1.121.md
index 0337eae..eea22e1 100644
--- a/Chapter1 - iOS/1.121.md
+++ b/Chapter1 - iOS/1.121.md
@@ -142,7 +142,7 @@ weak、unowned 都能解决循环引用问题。但是 weak 由于当对象释
- 初始化赋值后再也不会变为 nil 的对象,推荐使用 unowned
## 闭包的循环引用
-
+
上面的代码会发生循环引用,会导致局部变量的 p 无法释放(看不到 Person 的 deinit 方法调用)
解法:
diff --git a/Chapter1 - iOS/1.124.md b/Chapter1 - iOS/1.124.md
index 0b6da33..3537a4e 100644
--- a/Chapter1 - iOS/1.124.md
+++ b/Chapter1 - iOS/1.124.md
@@ -284,7 +284,7 @@ NSObject 是 Objective-C 的根类,定义了对象的基本行为:
### `p.run()` 底层是怎么调用的?
Demo1: Swift 调用 Swift 对象方法
-
+
纯 Swift 环境中,调用对象的方法,走的是虚表的逻辑。最终底层会调用 `callq *0x78(%rax)`
@@ -296,7 +296,7 @@ Demo2: OC 调用 Swift 对象方法
3. OC 方法中调用 Swift 对象和方法
4. 给 OC 环境中,调用 Swift 对象方法的地方下个断点,查看走的是 OC 的 Runtime 还是 Swift 的虚函数表的逻辑
断点截图如下:
-
+
可以看到在 OC 环境中,调用 Swift 对象的方法,本质上走的是 Runtime 的流程,汇编可以看到走的是 `objc_msgSend` 流程,效果类似 `objc_msgSend(p, @selector(run))`
@@ -310,7 +310,7 @@ Demo3: 暴露给 OC 的 Swift 对象,被 Swift 环境调用
3. 在 Swift 环境中调用暴露给 OC 的 Swift 对象方法
4. 断点查看方法调用的本质
-
+
可以看到,在 Swift 环境中,即使某个 Swift 类暴露给了 OC,调用其对象方法的本质,依旧是走虚函数表。因为此时用不到 Runtime 的能力
Demo4: 在 Demo3 的基础上,swift 方法内部调用 OC 对象方法
@@ -321,8 +321,8 @@ Demo4: 在 Demo3 的基础上,swift 方法内部调用 OC 对象方法
3. 在 sayHi 方法内部,调用 OC Person 对象的 run 方法
下断点可以看到:
-
-
+
+
分为2个阶段:
1. 第一阶段:在 Swift 环境调用虽然暴露给 OC 的 Swift 对象方法,但因为没有和 OC 直接交互,所以走的是 Swift 虚函数表逻辑
@@ -330,7 +330,7 @@ Demo4: 在 Demo3 的基础上,swift 方法内部调用 OC 对象方法
思考:想让 Swift 方法也走 OC 的 Runtime,可以利用 **`dymanic`** 关键词修饰方法。如下:
-
+
## dynamic 的作用
diff --git a/Chapter1 - iOS/1.136.md b/Chapter1 - iOS/1.136.md
index 14f7d64..9ccb5c9 100644
--- a/Chapter1 - iOS/1.136.md
+++ b/Chapter1 - iOS/1.136.md
@@ -29,11 +29,11 @@ Project、Target、Scheme 主要管理什么?
#### 关键步骤
-
+
-
+
##### 管理配置文件
@@ -43,7 +43,7 @@ Project、Target、Scheme 主要管理什么?
当对某个 Target “Duplicate” 之后,会产生一份新的 plist 文件
-
+
- 在新 Target 的 `Build Settings` → `Packaging` → `Info.plist File` 指定新路径
@@ -93,19 +93,19 @@ Project、Target、Scheme 主要管理什么?
先选中 Project,然后在右侧选择 Info 选项卡,在 Configurations Section ,点击 "+" ,即可创建新的 Configuration。
-
+
创建好之后,该 Target 存在3份 Configuration 了。不同的 Configuration 有什么作用呢?设置宏定义的时候可以针对不同的 Configuration 进行设置。
-
+
针对 OC、Swift 分别设置了很多宏定义,接下去需要跑 Beta 配置的代码,怎么办?
-
+
点击 Edit Scheme,在 Run 里面选择对应的 Configuration。
@@ -117,13 +117,13 @@ Project、Target、Scheme 主要管理什么?
创建 Scheme 步骤:Xcode -> New Scheme,再弹出的方框内,选择对应的 Target,然后输入需要创建的 Scheme 名称。此次我们创建了:Debug、Beta 2个新的 Scheme。
-
+
创建好之后,可以看到实体 Configuration 和虚拟 Scheme 存在多对多的关系。但我们可以基于此,选择实体的 Scheme,然后在 Run 里面 “Build Configuration” 里面选择对应的 Configuration 与之对应。
-
+
@@ -134,7 +134,7 @@ Project、Target、Scheme 主要管理什么?
完整如下图:
-
+
切换不同的 Scheme,可以运行不同的效果,当前 case 下,选择 Debug Scheme,输出不同结果 `HOST_URL: http://www.debug.baidu.com`
@@ -160,7 +160,7 @@ Xcode 自带的 Configuration Settings File 可以支持自定义一些宏,
第一:创建步骤如下:
-
+
文件命名为:`文件夹名称-项目名称.scheme名称.xcconfig`,比如 `Config-Xcconfig.debug.xcconfig`
@@ -170,13 +170,13 @@ Xcode 自带的 Configuration Settings File 可以支持自定义一些宏,
第二:修改和完善创建的 Xcconfig 配置文件里的内容。之后在 Xcode 的 Project 选项下,找到 Configurations,选择对应的 Target,然后选择右边对应的 Xcconfig 文件。如下图
-
+
我们只在 `Config-Xcconfig.debug.xcconfig` 文件中添加了 `OTHER_LDFLAGS = -framework "AFNetworking"`,Xcode 切换到 debug scheme 下,然后 Command + B 编译。
-
+
验证结果:
@@ -204,7 +204,7 @@ Xcode 自带的 Configuration Settings File 可以支持自定义一些宏,
- 在 `.xcconfig` 文件里添加了:`HOST_URL=127.0.0.1`
- 在 plist 中需要加一栏:key 为 `HOST_URL`,value为 `${HOST_URL}`
-
+
- 代码中使用
@@ -309,7 +309,7 @@ TEMP_LDFLAGS = $(BASE_LDFLAGS) -framework "AFNetworking" -framework "SDWebImage"
OTHER_LDFLAGS = $(TEMP_LDFLAGS)
```
-
+
第三步:
@@ -320,7 +320,7 @@ OTHER_LDFLAGS = $(TEMP_LDFLAGS)
结果:编译工程,可以看到报错了。符合预期
-
+
原因:本 Demo 的目的就是通过 `xcconfig` 文件和继承关系来验证对 Xcode Build Settings 中的 `Other Linker Flags` GUI 面板来验证 xcconfig 及其层级关系会正确影响到最终的编译参数上。
@@ -358,7 +358,7 @@ end
为了测试 xcconfig 配置信息的继承,故意把生成的原始信息注释掉。去掉了 `-framework "ImageIO"`
-
+
第三步:创建 `Base.xcconfig` 文件。引入 Cocoapods 自动生成的 `Pods-LDExploreDemo.debug.xcconfig` 然后声明 `OTHER_LDFLAGS = $(inherited) -framework "ImageIO"`由2部分组成,一部分是 `$(inherited)` 一部分是新加的 `-framework "ImageIO"`
@@ -371,7 +371,7 @@ end
NSLog(@"%@", policy);
````
-
+
说明:
diff --git a/Chapter1 - iOS/1.141.md b/Chapter1 - iOS/1.141.md
index 1500c8f..dd99c20 100644
--- a/Chapter1 - iOS/1.141.md
+++ b/Chapter1 - iOS/1.141.md
@@ -4,7 +4,7 @@
## LLDB 架构
-
+
说明:类似一个 CS 架构。
@@ -20,7 +20,7 @@
## LLDB Workflow
-
+
说明:
diff --git a/Chapter1 - iOS/1.142.md b/Chapter1 - iOS/1.142.md
index 704e087..16c8027 100644
--- a/Chapter1 - iOS/1.142.md
+++ b/Chapter1 - iOS/1.142.md
@@ -97,10 +97,10 @@ QA:存在一个情况,点击一个网页,下载 iOS App 到本地,点击
Xcode 中,对于项目是可以看到配置文件的。我们可以鼠标按住,拖动到桌面文件夹下。
-
+
此外,`mobileprovision` 文件是不可读的。可以通过 **security cms -D -i 195103db-6d6f-4da1-bd0e-66d5db88176f.mobileprovision -o profil
e.plist** 指令,dump 成为一个 plist 格式的文件。如下图所示:
-
+
指令解读:
- security:是 macOS 自带的安全相关的命令行工具,用于处理证书、配置文件、密钥等的处理
@@ -158,7 +158,7 @@ e.plist** 指令,dump 成为一个 plist 格式的文件。如下图所示:
1个 iOS App 工程,使用动态库的方式,依赖了 AFNetworking。选择模拟器进行编译,查看日志:
-
+
日志分析:
1. 日志中的 `--sign` 后一般接的是证书的名称,但是当前日志中是 `-`,代表使用自动签名模式(Automatic Signing)。
@@ -170,30 +170,30 @@ e.plist** 指令,dump 成为一个 plist 格式的文件。如下图所示:
2. 日志中的 `--entitlements /Users/unix_kernel/Library/Developer/Xcode/DerivedData/InstallDyanmicAndStaticFramework-bmcbqvmynpdalkdhzqirbithtfwp/Build/Intermediates.noindex/InstallDyanmicAndStaticFramework.build/Debug-iphonesimulator/InstallDyanmicAndStaticFramework.build/InstallDyanmicAndStaticFramework.app.xcent` 代表 Xcode 开启的 App 能力信息。`--entitlements` 参数后面跟随的是一个 `.xcent` 文件的路径,这个文件包含了应用程序的权限(Entitlements)配置信息,也就是 “App 能力信息”
对其查看,内容如下:
-
+
`security find-identity -v -p codesigning` 指令用于 macOS 系统中用于查看可用代码签名证书的命令,主要用于开发者在进行代码签名操作前确认可用的证书信息
##### 2. Demo2
1个 iOS App 工程,使用动态库的方式,依赖了 AFNetworking。选择真机进行编译,查看日志:
-
+
日志分析:
1. 日志中的 `Signing Identity: "Apple Development: FantasticLBP@github.com (953PZFXZFR)"` 变成了开发者证书。多了一个配置文件。
2. 使用的动态库 AFNetworking 是如何签名的?
选择 Pods 的 Product 里面的 AFNetworking 动态库,右击 “show in finder”。看到并没有一个 **`_CodeSignature`** 的文件夹,也就是没有签名信息。然后用指令 ` objdump --macho --private-headers /Users/unix_kernel/Library/Developer/Xcode/DerivedData/InstallDyanmicAndStaticFramework-bmcbqvmynpdalkdhzqirbithtfwp/Build/Products/Debug-iphoneos/AFNetworking/AFNetworking.framework/AFNetworking` 进行查看,发现也不存在 **`LC_CODE_SIGNATURE`** 存储签名信息的 load command。
-
+
可能会问,如何确定是动态库?使用 `file AFNetworking` 指令即可验证
-
+
问题:动态库 AFNetworking 没有经过签名,为什么拷贝到 App 里面后,可以上架?
其实 Cocoapods 自动生成了脚本,在主工程的 `Build Phases -> Embed Pods Frameworks` 下。且 `Input File Lists` 配置的文件内容,就是所依赖库的文件路径。会被当作参数传递给 `Embed Pods Frameworks` 脚本。
-
+
观察编译日志,会发现 「Run custom shell script '[CP] Embed Pods Frameworks'」这里,先对 Frameworks 目录进行了创建和拷贝。然后对 AFNetworking 进行签名。
在主工程的 Products 目录下,选择 App,show in finder,然后显示包内容。查看 `Frameworks` 文件夹下的 `AFNetworking.framework` 已经存在了 `_CodeSignature` 文件夹,也就是已经签名完成。继续查看 Load Command,发现也存在了 **LC_CODE_SIGNATURE** Load Command。
-
+
同时,也可以看到是先对动态库签名,再对 App 签名。
@@ -204,13 +204,13 @@ e.plist** 指令,dump 成为一个 plist 格式的文件。如下图所示:
安装方式为:`brew install --no-quarantine excitedplus1s/repo/jtool2`
使用方式为:**`jtool2 --sig -vv ${MachOFile}`**
-
+
### 5. fastlane 相关概念
-
+
- fastlane 本质就是一套命令行工具,专为用来简化并实现我们与 Apple 交互时的自动化
- fastlane 的每一个单独工具都是为了解决常见的 App Store 或其他问题而设计的
- fastlane 通过脚本方式集合了一系列常见的行为,叫做 lane。也就意味着可以通过 lane 来对自己的 App 做一些量身定制的需求
@@ -276,7 +276,7 @@ e.plist** 指令,dump 成为一个 plist 格式的文件。如下图所示:
end
```
- 第三种:使用特定的 Fastlane 文件。比如使用脚本 `Fastlane scan init` 会生成关于 scan 相关逻辑的脚本文件 `Scanfile`
-
+
建议使用方式二、三,不建议使用方式一。关于 fastlane 脚本编写文档查看 [fastlane docs](http://docs.fastlane.tools)
@@ -405,7 +405,7 @@ end
终端使用 `fastlane match init` 指令创建 Matchfile,同时根据提示选择一些模版和输入信息。
-
+
diff --git a/Chapter1 - iOS/1.143.md b/Chapter1 - iOS/1.143.md
index 31c01c8..87a92a7 100644
--- a/Chapter1 - iOS/1.143.md
+++ b/Chapter1 - iOS/1.143.md
@@ -3,14 +3,14 @@
## 一、实时特征回流
传统智能的问题、弊端是什么?
-
+
- 推荐系统需要收集用户的行为,将这些行为作为用户的意图表征,表征他的偏好,回流到服务端。
- 服务端拿到这个数据,在发起一次实时请求的时候,会根据用户的行为特征,去商品池里面召回一批用户喜欢的商品,再返回给端上,给用户做展示
- 同时,这个用户的行为数据,还会作为训练模型的一个样本
-
+
可以发现传统的推荐系统存在一些瓶颈:
- 实时性不足
行为数据回流的时效性,会影响算法对于用户意图变化的感知,会影响推荐的准确性。
@@ -31,7 +31,7 @@
前面说过,用户的数据回流时效性会影响推荐的准确度。那么是不是可以把用户的特征、用户的数据,做到最实时的回流?
这里,我们做了一些这样的尝试:
-
+
- 首先可以把用户的最原始的行为数据回流。比如用户在逛手淘的过程中产生的一些浏览、点击等行为回流到服务端
- 同时,也可以把用户的这些原始行为数据做一定的聚合,生成一个信息量含量更高,但是数据量更小的用户特征数据(数据聚合)
@@ -74,7 +74,7 @@
此时,从商品详情页到回退到外面这个商品列表页的时候,会根据用户刚刚的浏览、加购行为,推荐一个相似的商品。
-
+
会根据用户刚刚浏览的这些行为,去给他推荐一个相似的商品。这个过程会发生很多行为,通过滚动、曝光等行为可以推测用户在信息流的浏览意图是逐渐从逛切换到了买。
@@ -87,7 +87,7 @@
商品卡片的回退推荐策略落地上线后,效果是非常好的。比普通商品卡片的转换率高五六倍左右。
-
+
存在什么问题?
类似程序员和产品设计沟通出的一种机制,可以理解为命令式编程,是人为先验地找到了一个能够代表用户意图的时机,也就是在回退时机。这种时机靠人去发现梳理,往往是很难覆盖全面的。依赖于对于用户强意图的梳理、选择。那么有没有一种方式可以自动的去预测用户意图的变化?
@@ -100,7 +100,7 @@
在信息流上面做了另外一个尝试:在本地进行了一次端上的重排。
-
+
一个用户在逛信息流的过程中,会产生滚动、曝光、点击、加购、收藏、停留、回退、滚动这些行为。从用户的实时的行为序列中,其实是表征了用户背后的一个隐式的意图表达。
@@ -114,7 +114,7 @@
做完决策后,就可以将这个决策结果通知到前台,去做相应的响应。
还存在一些问题:
-
+
决策选择还是受限于产品策略。程序员还需要和产品去约定设计产品策略。开发和产品共同约定,在用户的某一时机之下,接下来对应的一个处理是什么。它的整个呈现形式和所处业务域还是存在紧密关系的。
@@ -134,7 +134,7 @@
完整流程:
-
+
用户进入手淘后,会不断收集端上的行为(滚动、曝光、点击、加购、收藏、停留)数据,然后会把行为数据输入到另一个意图模型中,去判断他当前的一些实时意图。不断的根据行为做分析。也会输入到另一个决策模型中去,但这个(智能 Push)场景下的决策模型和前面的端上重排场景的决策模型是不一样的。这个决策模型会**判断用户当前的状态适不适合接受一次干预**,或者适不适合接受某一次营销的推荐。当我们发现用户当前处于一个相对空闲的状态,这个时机更适合接受一条干预的时候,我们就会把这个信号通知到服务端。这时候服务端在海量的内容中去筛选出一条对应着用户当前的意图,有效的一条信息,再推送给客户端。
@@ -181,7 +181,7 @@
## 三、端智能整体架构
要素:算法、数据、调度框架、运行环境
架构如下:
-
+
1. 围绕着端上的算法分为2种模型:
- 用户意图计算模型:不断分析用户当前所处状态
@@ -199,7 +199,7 @@
### 1. 端上算法方案
-
+
- Algorhitms Solution On Edge:
- 提供了模型、特征和样本这三大机器学习算法基础组件的端上通用方案
@@ -212,7 +212,7 @@
### 2. 端上特征中心
为端智能应用而设计,提供端侧算法所使用的标准化的全域用户行为数据和特征服务
-
+
- 定义端侧用户行为标准
该特征中心会定义端上用户的行为标准,产生什么样的用户行为,比如用户的浏览行为。这个浏览行为会有一些我的浏览区域、浏览停留时长等等标准化属性。其次,也会有一些像用户手势行为。比如滚动、点击等等。
- 建立行为数据图化索引
@@ -224,7 +224,7 @@
数据分层架构:
-
+
- 存储层:将采集到的用户行为数据按照约定的标准,在客户端本地做持久化存储。同时对用户数据进行一次加工处理,生成一份信息密度比较高的基础特征表。比如对详情页的浏览行为、App 页面间操作路径的数据、页面浏览的时序特征,这些数据都会存储在客户端本地。
- 接口层:实时接口层,提供了 Python 层面的接口服务,给算法侧使用。可以做到数据的实时查询,将下层的通用数据、用户行为数据、环境信息等打包好给算法侧使用。
@@ -234,7 +234,7 @@
### 3. 端上的决策中心
-
+
比如:用户在:我的淘宝 -> 我的订单 -> 订单详情页查看了某个订单详情,然后回退到“我的淘宝”页,这时候会对用户的意图进行分析,判断当前是不是处于一个空闲的状态。如果发现是空闲状态,则给他发送一条 Push 消息,引导进入双11主会场。这个就是一个智能 Push 的案例。
还有其他的需求:
@@ -251,15 +251,15 @@
这种面向用户行为的切面的编程方式,既可以用在运营规则上,也可以用在算法模型上。后续的响应,可以是弹出弹层、发送 Push、发送1次请求等。
### 4. 端计算容器
-
+
端上的算法模型需要跑在容器里,手淘用的是一个轻量级的推理引擎 MNN。MNN 提供了算法在端上跑模型所需要的算子。
## 四、云端一体协同
-
+
上图是端计算的优势和云计算的劣势。
未来的端计算并不是完全割裂的。端和云协同才可以迸发最大的效果。
-
+
可以看到端和云拥有各自擅长的领域。
在做云端协同的过程中,会遇到不少问题。比如在端上触发一次重排的时候,会发现端上的数据量是不够的,如果想要提升端上的重排效果,就要扩大候选池,所以增加了**端上的缓存池**。在端上的模型在本地运行过程中,由于模型本身是在服务端训练的,它的模型和特征向量的同步一致性是需要细节方面处理好的。
@@ -310,17 +310,17 @@ QA: 决策框架与 MNN 推理引擎的区别是什么?
#### 1. 背景
生鲜果蔬行业在零售行业中是一个较大且比较有特征性的行业,同时在生鲜果蔬行业中,称重秤为经营的刚需类设备。目前商家主要使用条码秤,通过 PLU(Price Lookup Code) 码进行商品的管理,每个 PLU 码对应一个商品,我们可以想象下在超市购买水果的时候会碰到下面这个流程:
-
+
所以在门店商品种类比较多时(一个典型生鲜果蔬类商家商品种类大多超过 200 个,随机调研了 5 家有赞果蔬商家,平均 SKU 数量 500+),PLU 码较难记忆清楚,在打秤时需临时查询,称重耗时比较长,为了避免高峰时期排队现象,需在门店增加秤台和打称员,导致商家人力成本较高。
因此就前面所提到的场景,我们需要通过更加智能的方式帮助商家加购,那么基于机器学习的图像识别能力就被提上了议程。我们通过条码秤关联的摄像头进行实时拍摄,基于机器学习技术和图像识别技术,将店员放置在秤盘上的商品进行识别,并给出相关商品的列表,减少收银员收银场景中的操作次数,减少商家对新收银员的PLU码的培训并降低熟悉相关商品的培训成本,从而在整体上降低收银员的门槛以及商家的人力成本。所以我们可以得到我们期望的购买流程:
-
+
#### 2. 架构设计
我们针对于商家的痛点和可行的解决方案绘制了下面的流程图:
-
+
整个流程中的基础能力:
- 实现摄像头对于商品的拍摄
@@ -378,7 +378,7 @@ PLU 码的核心作用是 “快速调取商品单价”,重量和总价需结
#### 6. 反馈闭环
在确定了核心能力的解决方案后,接下来需要解决的是如何将商家本地的数据进行上传,并对于已有模型进行强化。为了更加及时的获得用户本地的选择情况,我们选择了有赞埋点平台作为技术支撑,通过离线缓存,并结合闲时上报的能力,将用户选择图片的整体筛选情况,基于店铺/角色等维护进行拆分,并将最终的选择数据导入 ODS 库中。并在算法前结合用户选择时机的拍摄图片上传 + 用户选择商品情况进行结合,进一步针对于对应店铺的模型进行加强。从而在不断的强化商家模型,从而提高用户准确性。
-
+
#### 7. 流程优化
要达到好用的程度。所以我们需要对于数据统计流程/用户交互流程进行更加深入的优化
diff --git a/Chapter1 - iOS/1.144.md b/Chapter1 - iOS/1.144.md
index b775938..f39667f 100644
--- a/Chapter1 - iOS/1.144.md
+++ b/Chapter1 - iOS/1.144.md
@@ -291,7 +291,7 @@ Weex Android 侧用 C++、iOS 侧用 OC 的差异,是:
## 增强并发
-
+
上图左侧是未经优化前JS&Native通信流程,可以看出,每当JS发送一个callNative时,Native都会有一个callJS回调,这种方式更类似于JS同Native握手的方式,这种设计方式保证了页面渲染时所需的时序性。
diff --git a/Chapter1 - iOS/1.148.md b/Chapter1 - iOS/1.148.md
index 59f15f6..3ad29a3 100644
--- a/Chapter1 - iOS/1.148.md
+++ b/Chapter1 - iOS/1.148.md
@@ -502,7 +502,7 @@ NSString *const WXExceptionContextExtraKey = @"extra";
落地方式:
- 集成到 CI/CD Pipeline,代码提交时触发静态分析,有风险则阻断合入
- 通过编写 LLVM 插件,就可以在 Xcode 有问题的代码上实时提示 “数组越界风险”“字典 key 可能为 nil”。比如
-
+
效果:线上低概率异常的根源被提前消灭,问题无法逃逸到现线上,业务层几乎无需处理这类异常(因为代码本身就没有漏洞)
diff --git a/Chapter1 - iOS/1.39.md b/Chapter1 - iOS/1.39.md
index c9cb0fb..17e3413 100644
--- a/Chapter1 - iOS/1.39.md
+++ b/Chapter1 - iOS/1.39.md
@@ -315,7 +315,7 @@ dispatch_sync(dispatch_get_main_queue(), nil);
Demo1
-
+
QA:为什么先打印1、3再打印2?
@@ -332,7 +332,7 @@ Demo2:
-
+
@@ -372,13 +372,13 @@ QA:为什么 test 里的2没有打印?
所以代码改下就可运行。
-
+
注意:可能有一部分人会这么在子线程中添加 RunLoop,会存在3无法打印的问题。为什么?
-
+
`[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];` 本质上让 RunLoop 在指定模式下运行,直到发生以下情况之一:
@@ -391,15 +391,15 @@ QA:为什么 test 里的2没有打印?
第一种:在子线程方法中,手动关闭子线程中的 RunLoop
-
+
第二种:不要用 `[NSDate distantFuture]`,设置个0秒也可以,改为 `[NSDate dateWithTimeIntervalSinceNow:0]]`
-
+
第三种:不要用 `[NSDate distantFuture]`,设置个0秒也可以,改为 `[NSDate dateWithTimeIntervalSinceNow:0]]`。另外不要加 Port,直接在子线程中先获取一次 RunLoop 就好,因为 ``performSelector...withObject...afterDelay...` ` 已经给当前的 RunLoop 添加了 NSTimer,只是没有开启。 分析 RunLoop 源码分析后会发现,在子线程中获取一次 RunLoop,会默认创建一个 RunLoop。
-
+
所以要研究 iOS 底层的同学,看看 **GUNStep 代码吧,这是宝藏**
@@ -409,13 +409,13 @@ QA:为什么 test 里的2没有打印?
Demo1:
-
+
同理,GCD 虽然开启了子线程,但是 Block 结束后,线程也就结束了。所以线程任务中的1秒后的任务肯定也结束了。
Demo2:
-
+
可以看到 NSThread 里的 block 执行结束后,thread 结束了。后面的 performSelector 想在线程里执行任务,就会 crash。
@@ -528,7 +528,7 @@ static void *nsthreadLauncher(void *thread) {
所以解决办法也是在线程的 block 里面加 RunLoop,让它保活
-
+
@@ -569,7 +569,7 @@ dispatch_group_notify(group, dispatch_get_main_queue(), ^{
});
```
-
+
@@ -577,7 +577,7 @@ dispatch_group_notify(group, dispatch_get_main_queue(), ^{
### 经典 Demo
-
+
会输出什么?
@@ -802,21 +802,21 @@ si:step instruction,简写为 stepi,si。当你在 Xcode 汇编面板看
第一步:当第二次调用 saveMoney 方法,开启汇编调试
-
+
看到可疑方法 `OSSpinLockLock`,给它加断点,看到第10行高亮了。lldb 模式输入 c,敲回车。次数输入 si 即可进入 `OSSpinLockLock` 方法内部调试
第二步:继续输入 si,敲回车
-
+
第三步:看到可疑方法 `_OSSpinLockLockSlow`,给它加断点,lldb 输入 C。此时断点到这一行了,继续输入 si。
-
+
第四步:在 `OSSpinLockLockSlow` 方法内部调试,不断输入 si。
-
+
发现不断 si 最终一直会在第6行到第19行之间执行。懂汇编的会发现这其实是一个 while 循环。便可以证明自旋锁 OSSpinLock 在等锁的时候,底层实现是执行 while 循环,忙等,“太浪费性能了”(如果使用锁资源的线程任务很简单,那自旋也是高效的,可以快速获取锁。)
@@ -915,26 +915,26 @@ int cursorr = 1;
假如对存钱过程,忘记解锁怎么办?产生死锁,如下
-
+
添加 cursor 标记死锁是发生在 `saveMoney` 方法执行的第几次。发现是第二次。因为第一次锁没有任何使用方,所以加锁成功,当第二次加锁的时候发现锁没有释放,所以产生死锁。
这时候使用尝试加锁 API `os_unfair_lock_trylock` 即可成功如下
-
+
#### 汇编剖析实现原理
同样方式看看 ,按照上述调试汇编代码的步骤,我将关键步骤截图如下
-
-
-
-
-
+
+
+
+
+
-
+
结论:可以看到 `os_unfair_lock` 在锁等待的时候,底层调用的是 `sysCall`,当这一步执行后会发现后续代码都不执行了,也就是调用系统底层能力,线程真正休眠了,而不是一个循环忙等的实现,所以性能好。
@@ -985,7 +985,7 @@ pthread_mutex_destroy(&_moneyLock);
使用如下
-
+
#### 化身递归锁
@@ -1033,7 +1033,7 @@ pthread_mutex_destroy(&_moneyLock);
改进后的效果如下
-
+
@@ -1045,33 +1045,33 @@ QA:互斥递归锁,可以在不同线程中加锁吗?
#### 汇编剖析实现原理
-
+
输入 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`,执行完第四句指令,线程立马就结束了。
@@ -1256,7 +1256,7 @@ CocoaLumberjack 的 DDLog.m 中,锁返回值检查的严谨写法:
激活所有等待该条件的线程 `pthread_cond_broadcast(&_condition)`
-
+
可以看到同时调用 remove、add 方法
@@ -1358,13 +1358,13 @@ NSRecursiveLock 不能在多线程下递归调用。@synchronized 可以在多
Demo
-
+
NSLock 死锁
-
+
会发生死锁,后续代码无法执行,App 表现就是 ANR。重复对 NSLock 进行加锁可能导致死锁问题,同时也可能引发数据竞争和性能下降等并发相关隐患
@@ -1414,7 +1414,7 @@ API
Demo:
-
+
@@ -1426,7 +1426,7 @@ Demo:
疑问:调用 signal 方法后,另一个等待锁的地方会立马得到锁资源吗?可以做个实验,给 signal 后 sleep 2秒,再调用 unlock
-
+
观察打印信息可以看到:
@@ -1438,7 +1438,7 @@ Demo:
-
+
@@ -1479,7 +1479,7 @@ API 如下:
Demo
-
+
分析:虽然通过3个线程,设置了线程的先后顺序,但是多线程任务执行的时候到底谁先执行,是没办法控制的。但是通过 `NSConditionLock lockWhenCondition:*` 的能力,可以控制线程的执行顺序。
@@ -1495,7 +1495,7 @@ Demo
线程同步的本质就是多线程的任务是顺序执行
-
+
@@ -1509,11 +1509,11 @@ semaphore 叫做”信号量”
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
-
+
可以看到打印了20个线程,但是我们控制线程最大数量怎么办呢?可以用信号量实现。效果如下:
-
+
#### dispatch_semaphore_wait 原理
@@ -1527,7 +1527,7 @@ semaphore 叫做”信号量”
所以如何让线程同步?设置信号量的值=1即可。保证同一时间只有一个线程任务在执行。代码如下
-
+
@@ -1587,7 +1587,7 @@ dispatch_semaphore_signal(semaphore);
`@synchronized` 使用很方便,它是对 `pthread_mutex_t` 递归锁的封装。Demo 如下
-
+
@@ -1595,9 +1595,9 @@ dispatch_semaphore_signal(semaphore);
为了探究下实现,开启汇编调试
-
+
-
+
通过汇编可以看到 `@synchronized` 底层调用了 `objc_sync_enter` 方法,其中又调用了 `id2data` 和 `os_unfair_recursive_lock_lock_with_options` 方法。 可以查看 objc4 的源码(笔者的 objc 版本为 objc4-objc4-912.3),查找 `objc_sync_enter`
@@ -2106,7 +2106,7 @@ pthread_rwlock_init(&_lock, NULL)
Demo
-
+
@@ -2134,7 +2134,7 @@ dispatch_barrier_async(self.queue, ^{
上 Demo
-
+
@@ -2163,7 +2163,7 @@ Demo
}
```
-
+
@@ -2186,7 +2186,7 @@ Demo
}
```
-
+
结论:可以发现 GCD `dispatch_barrier_async` 栅栏函数,拦不住全局队列,却可以拦住自己创建的普通队列。这是为什么?
diff --git a/Chapter1 - iOS/1.40.md b/Chapter1 - iOS/1.40.md
index aa945a1..54b33d9 100644
--- a/Chapter1 - iOS/1.40.md
+++ b/Chapter1 - iOS/1.40.md
@@ -21,7 +21,7 @@
假设我们计算有`128MB`内存,程序A需要`10MB`,程序B需要`100MB`,程序C需要`20MB`。如果我们需要同时运行程序A和B,那么比较直接的做法是将内存的`前10MB`分配给程序A,`10MB~110MB`分配给B。
-
+
但存在以下问题:
@@ -51,7 +51,7 @@
比如A需要`10M`,就假设有`0x00000000` 到`0x00A00000`大小的虚拟空间,然后从物理内存分配一个相同大小的空间,比如是`0x00100000`到`0x00B00000`。操作系统来设置这个映射函数,实际的地址转换由硬件完成。如果越界,硬件就会判断这是一个非法访问,拒绝这个地址请求,并上报操作系统或监控程序。
-
+
这样一来利用**分段**的方式可以解决之前的**地址空间不隔离**和**程序运行地址不确定**
@@ -78,13 +78,13 @@
-
+
保护页也是页映射的目的之一,简单地说就是每个页可以设置权限属性,谁可以修改,谁可以访问,而且只有操作系统有权修改这些属性,那么操作系统就可以做到保护自己和保护进程。
虚拟存储的实现需要硬件支持,几乎所有CPU都采用称为**MMU的部件来进行页的映射**
-
+
在页映射模式下,`CPU`发出的是`Virtual Address`,即我们程序看到的是`虚拟地址`。经过`MMU`转换以后就变成了`Physical Address`。一般`MMU`集成在`CPU`内部,不会以独立的部件存在。
@@ -102,7 +102,7 @@ NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval
栈、堆、BSS、数据段、代码段
-
+
@@ -131,7 +131,7 @@ BSS段(bss segment):通常用来存储程序中未被初始化的全局变
代码段(code segment):编译之后的代码。通常是指用来存储程序可执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。
-
+
上 Demo 验证
@@ -210,7 +210,7 @@ Tagged Pointer 格式下,指针值不再是有效抵制,而是表示值。
当对 TaggedPointer 数据调用方法的时候,objc_msgSend 能识别出如果是 Tagged Pointer,比如 NSNumber 的 intValue 方法,直接从指针提取数据,节省了调用开销。所以使用了 TaggedPointer 技术不仅节约内存空间,又能提高方法查找速度。
-
+
@@ -311,7 +311,7 @@ Tagged Pointer 也就是一个伪指针,对象的指针中存储的数据变
Demo
-
+
在 64 位的 cpu 下,如果使用 Tagged Pointer 技术的话,则 NSNumber 对象的值直接存储在了指针中,系统不会为其在堆上分配内存,可以节省很多内存开销。此时,NSNumber 对象的指针中存储的数据变成了 Tag + Data 的形式(Tag 为特殊标记,用于区分NSNumber、NSDate、NSString 等小内存对象的类型;Data 为具体的值)。这样使用一个 NSNumber 对象只需要在栈中开辟 8 个字节的指针内存。当栈中 8 个字节的指针内存不够存储数据时,才会再将 NSNumber 对象存储到堆中
@@ -331,7 +331,7 @@ Demo
路径:Xcode - Edit Scheme - Run - Arguments - Environment Variables - 添加环境变量 `OBJC_DISABLE_TAG_OBFUSCATION` 设置为 YES 即可。
-
+
@@ -500,7 +500,7 @@ _objc_decodeTaggedPointer_noPermute_withObfuscator(const void * _Nullable ptr, u
}
```
-
+
@@ -508,7 +508,7 @@ _objc_decodeTaggedPointer_noPermute_withObfuscator(const void * _Nullable ptr, u
#### Tagged Pointer 与 isa
-
+
通过参考 objc 源码,针对对象指针进行解密后发现:
@@ -539,7 +539,7 @@ b 也就是11,二进制为 `1011`,Tagged Pointer 中,iOS 侧第一位是 T
3 区分数据类型。具体是什么数据类型,继续做个实验看看
-
+
@@ -560,7 +560,7 @@ b 也就是11,二进制为 `1011`,Tagged Pointer 中,iOS 侧第一位是 T
Objc 源码中,NSInteger、NSUInteger 都是别名。初始化 NSNumber 的时候用的是 `NSNumber numberWithInteger:<#(NSInteger)#>`
-
+
@@ -638,7 +638,7 @@ Tagged Pointer 如何区分是较小的对象,比如 NSString、NSDate、NSNum
验证下
-
+
可以看到:
@@ -651,11 +651,11 @@ Tagged Pointer 如何区分是较小的对象,比如 NSString、NSDate、NSNum
下面是针对 double 包装成 NSNumber 的 Tagged Pointer 指针结构拆分:
-
+
-
+
@@ -861,7 +861,7 @@ Demo1
运行该代码会 Crash,报错信息如下
-
+
@@ -884,11 +884,11 @@ Demo1
改法1:将 property 改为 **atomic** 修饰的。
-
+
改法2:对 name 加锁
-
+
@@ -896,7 +896,7 @@ Demo1
Demo2
-
+
@@ -952,7 +952,7 @@ NSString、NSMutableString 继承关系如下:
-
+
通过 `[[NSString alloc] initWithString:@"**"]` 方式创建的 NSString 字符串
@@ -1022,27 +1022,27 @@ iOS 中使用引用计数来管理 OC 对象的内存。一个新创建的 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 的引用计数情况:
@@ -1054,7 +1054,7 @@ iOS 中使用引用计数来管理 OC 对象的内存。一个新创建的 OC
改进
-
+
@@ -1217,7 +1217,7 @@ NSLog(@"array2 --- %zd", array2.retainCount);
Demo3
-
+
会发现发生了 crash。问题是因为
@@ -1605,7 +1605,7 @@ class StripedMap {
### 引用计数表
-
+
@@ -1613,7 +1613,7 @@ class StripedMap {
weak_table_t 结构如下:
-
+
```c++
#define WEAK_INLINE_COUNT 4
@@ -1708,7 +1708,7 @@ objc_destoryWeak(&obj);
上 Demo
-
+
可以看到当一个 weak 指针被赋值的时候,底层调用了 `objc_initWeak`,跟踪查看 objc 源码
@@ -2549,7 +2549,7 @@ void sel_init(size_t selrefCount){
在 gone 处加断点,利用 runtime 查看类中的方法信息
-
+
发现存在 `.cxx_destruct` 方法。
@@ -2584,7 +2584,7 @@ void sel_init(size_t selrefCount){
@end
```
-
+
Tips:@property 会自动生成成员变量,另外类后面加 `{}` 在内部也可以加成员变量,假如成员变量是对象类型,比如 NSString,则叫实例变量。
@@ -2598,7 +2598,7 @@ Tips:@property 会自动生成成员变量,另外类后面加 `{}` 在内部
在 gone 的地方加断点,输入 `watchpoint set variable p->_name`,则会将 `_name` 实例变量加入 watchpoint,当变量被修改时会触发断点,可以看出从某个值变为 0x0,也就是 nil。此时边上调用堆栈显示在 `objc_storestrong` 方法中,被设置为 nil.
-
+
@@ -2843,7 +2843,7 @@ Person *person2 = [Person personWithName:@"FantasticLBP"];
隐式调用工厂方法
-
+
@@ -2853,7 +2853,7 @@ Person *person2 = [Person personWithName:@"FantasticLBP"];
如何修改?加一个 bridge 即可。
-
+
由于 ARC 没有加 retain。所以 `person = (__bridge id)result;` 这里完成了对象的 retain。ARC 在退出方法的作用域时给对象加上release。前后对应,内存正确。
@@ -2964,7 +2964,7 @@ class AutoreleasePoolPage {
- 每个 AutoreleasePoolPage 对象占用 4096 (16的3次方,0x2000)字节内存,除了用来存放它内部的成员变量(内部成员固定有7个,56个字节,即 `0x18`, `0x1000 + 0x38 = 0x1038` ),剩下的空间用来存放 autorelease 对象的地址
- 所有的 AutoreleasePoolPage 对象通过**双向链表**的形式连接在一起。child 指向下一个 AutoreleasePoolPage 对象,parent 指向上一个 AutoreleasePoolPage 对象
-
+
```objectivec
id * begin() {
@@ -3288,7 +3288,7 @@ class AutoreleasePoolPage : private AutoreleasePoolPageData {
举个例子,for 循环创建1000个 Person 对象,用 autorelease 修饰,如何工作?
-
+
分析:
@@ -3327,7 +3327,7 @@ int main(int argc, const char * argv[]) {
main 方法内部3个 autoreleasepool 底层怎么样工作的?
-
+
分析:
@@ -4226,7 +4226,7 @@ iOS 在主线程的 Runloop **通用模式(Common Modes)** 中注册 **1 个
结合 RunLoop 运行图
-
+
- 01 通知 Observer 进入 Loop 会调用 `objc_autoreleasePoolPush`
@@ -4373,7 +4373,7 @@ Cocoa 框架中,很多类方法用于返回 autorelease 对象。
### NSTimer、CSDisplayLink 中的内存泄露
#### CADisplayLink 内存泄漏
-
+
可以看到 CADisplayLink 和 VC,VC 和 CADisplayLink 互相持有,造成内存泄漏,没有释放。即使页面离开,定时器还在继续运行,不断打印。
@@ -4387,11 +4387,11 @@ NSTimer 的基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES blo
Demo 如下:
-
+
但是当使用 `[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerTask) userInfo:nil repeats:NO];` repeats 为 NO 的时候,好像不会内存泄漏。这是为什么?
-
+
@@ -4563,7 +4563,7 @@ Demo 如下:
##### 改用 block 的方式替换 API,不再持有 target
-
+
该种方式,控制器 (self)强引用 timer,timer 强引用 block,block 弱引用 self,3者没有形成环。
@@ -4623,7 +4623,7 @@ TimerTarget.target(weak) -> VC
}
```
-
+
解决方案2:使用专门处理消息转发的 NSProxy 类
@@ -4631,7 +4631,7 @@ TimerTarget.target(weak) -> VC
##### NSProxy 闪亮登场
-
+
可以看到使用 NSProxy 也可以解决 NSTimer 和 VC 循环引用的问题。但注意:继承自 NSProxy 的类,不能 init。
@@ -4645,7 +4645,7 @@ QA:自己写的继承自 NSObject 的代理对象和继承自 NSProxy 的代
看一段神奇的代码
-
+
为什么打印出 `0 1`?
@@ -4684,7 +4684,7 @@ QA:自己写的继承自 NSObject 的代理对象和继承自 NSProxy 的代
-
+
假设一个 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 的执行时间不固定)
@@ -4854,7 +4854,7 @@ dispatch_semaphore_t semaphore_;
说明:直接 `performSelector` 存在警告,可以告诉编译器忽略警告。可以在 Xcode 点开警告,查看详情,复制 `[]` 里面的字符串去忽略警告
-
+
@@ -5035,11 +5035,11 @@ NSHashTable *hashTable = [NSHashTable weakObjectsHashTable];
再来2个实验:
-
+
分析:可以看到 p1 的地址,在刚初始化后,和当作 key 加入到 NSDictionary 后,地址发生了变化。对 p1 执行了 copy 操作。
-
+
分析:
@@ -5090,7 +5090,7 @@ NSHashTable *hashTable = [NSHashTable weakObjectsHashTable];
这段代码运行会 crash,信息如下
-
+
原因是 NSError 构造方法内部会加 autorelease。源码如下
@@ -5153,7 +5153,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.46.md b/Chapter1 - iOS/1.46.md
index 68b19b0..fab93fb 100644
--- a/Chapter1 - iOS/1.46.md
+++ b/Chapter1 - iOS/1.46.md
@@ -24,7 +24,7 @@
在需要触发 KVO 的地方,先调用 `willChangeValueForKey`,然后更改被观察对象的属性值,最后调用 `didChangeValueForKey` 方法。
-
+
@@ -214,7 +214,7 @@ Demo: 实时数据流处理
代码如下
-
+
看上去很麻烦,有没有优雅点的方案?
@@ -224,7 +224,7 @@ Demo: 实时数据流处理
实现如下:
-
+
注意:在对 Person 对象的 `dog` 属性进行监听后,Person 内部需要实现 `+`keyPathsForValuesAffectingValueForKey 方法,判断 key 为 `@"dog"` 后,想监听 dog 的哪个属性变化就通知 Person 对象的观察者收到响应的话就写上去。但注意绿色框里面,set 添加的内容,必须换个名字,比如 `@"_dog.name"`,不能是 `@"dog.name"`。这会导致循环依赖或逻辑矛盾
@@ -326,7 +326,7 @@ self.p1.dog.name = @"Lucy";
可以在下面的 Demo1 中可以看到 KVO 无法直接对数组进行 KVO 监听。但系统为了方便,也提供了容器类的接口,比如针对 `NSMutableArray` 系统就提供了 `mutableArrayValueForKey` 接口。
-
+
那么虽然功能实现了,我们可以想想,这个接口背后做了哪些事?
@@ -434,7 +434,7 @@ FBKVO 工作原理:
### Demo1
-
+
可以发现对成员变量添加观察者的时候,成员变量的值变化了,KVO 也是监听不到的
@@ -442,13 +442,13 @@ FBKVO 工作原理:
### Demo2
-
+
可以看到对 NSMutableArray 类型的属性添加了 KVO。然后点击屏幕,NSMutableArray 里添加了元素,但是观察方法没有触发。
对实验进行改进下
-
+
结论: **KVO 只可以对属性的 setter 方法起作用**。
@@ -458,7 +458,7 @@ FBKVO 工作原理:
创建 Person 类,点击事件里触发属性值的改变
-
+
分析:
@@ -468,7 +468,7 @@ FBKVO 工作原理:
在内存中的结构如下图
-
+
整个流程分析下:
@@ -478,11 +478,11 @@ FBKVO 工作原理:
当我们按照 KVO 后动态生成的类名去创建一个新的类的时候,Xcode 会报错:`[general] KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class`。因为自己创建的类名和系统将要动态创建的类名冲突了,并且 KVO 监听失效
-
+
### Demo4
-
+
分析:
@@ -494,7 +494,7 @@ FBKVO 工作原理:
### Demo5
-
+
可以看到我们将 KVO 的数据类型改为 double 后,原本的 setHeight 是 `_NSSetLongLongValueAndNotify`,现在是 `_NSSetDoubleValueAndNotify`
@@ -504,11 +504,11 @@ FBKVO 工作原理:
### NSSet**ValueAndNotify 的内部实现
-
+
来对 Person 类增加一些打印方法
-
+
@@ -577,7 +577,7 @@ FBKVO 工作原理:
### 重写 class 方法
-
+
可以看到利用 runtime api,在添加 KVO 之后,类对象为 `NSKVONotifying_Person`
@@ -589,7 +589,7 @@ FBKVO 工作原理:
### KVO 类的所有方法
-
+
@@ -663,7 +663,7 @@ Apple 文档告诉我们:被观察对象的 `isa指针` 会指向一个中间
- 键值观察通知依赖于 NSObject 的两个方法:`willChangeValueForKey:、didChangeValueForKey:` 。在一个被观察属性改变之前,调用 `willChangeValueForKey:` 记录旧的值。在属性值改变之后调用 `didChangeValueForKey:`,从而 `observeValueForKey:ofObject:change:context:` 也会被调用。
-
+
@@ -809,7 +809,7 @@ KVO 的改装:
KVC 之后会触发 KVO 吗?
-
+
发现 KVC 触发了 KVO。
@@ -817,7 +817,7 @@ KVC 之后会触发 KVO 吗?
整个流程如下
-
+
`[self.person setValue:@10 forKey:@"age"]` 会先调用 `setKey:` 同名的方法,找不到则调用 `_setKey:` 的方法,如果还是找不到则调用 `+(BOOL)accessInstanceVariableDirectlt`,如果该方法返回 YES,则可以直接修改成员变量的值,会按照 `_key`、`_isKey`、`key`、`isKey` 的顺序寻找成员变量,如果找到则直接赋值,没找到则抛出异常 `NSUnknownKeyException`
@@ -874,7 +874,7 @@ KVC 之后会触发 KVO 吗?
-
+
diff --git a/Chapter1 - iOS/1.48.md b/Chapter1 - iOS/1.48.md
index 7c305d8..f8fee87 100644
--- a/Chapter1 - iOS/1.48.md
+++ b/Chapter1 - iOS/1.48.md
@@ -1177,7 +1177,7 @@ c 数组指针 `array()->lists + addedCount` 可以代表其中的位置。
过程如下
-
+
结果就是类方法列表中,最前面的就是所有分类的方法列表,最后是类自身的方法列表。
@@ -1334,7 +1334,7 @@ Demo: 为 Person 类创建2个 Category,分别存在同名方法 study,具
}
```
-
+
可以看到 sayHi 方法存在多个,但是由于 Category 同名的方法在方法列表的前面,所以类自身的方法实现”被覆盖了“(根据 isa 查找方法实现的时候,优先查找到 Category 的方法实现,则停止查找了)
@@ -1347,17 +1347,17 @@ Demo: 为 Person 类创建2个 Category,分别存在同名方法 study,具
Demo: 为 Person 类创建2个 Category,分别存在同名方法 study,具有不同实现。探索编译顺序决定方法实现
-
+
2个对比实验:
让 `Person+Study` 参与后编译
-
+
让 `Person+Learn` 参与后编译
-
+
@@ -2304,7 +2304,7 @@ Person +load
查看分类在 Runtime 加载类信息时候的调用原理可以知道,分类中的类方法、对象方法都会被加载原始类的前面去(initialize 是类方法)如下图:
-
+
### 为什么给子类发消息,父类和子类的 +initialize 都会被调用?且父类的先调用
@@ -3059,7 +3059,7 @@ void _object_set_associative_reference(id object, void *key, id value, uintptr_t
梳理后,如下图所示:
-
+
AssociationsManager 管理的 AssociationsHashMap 结构如下:
@@ -3178,7 +3178,7 @@ NS_ASSUME_NONNULL_END
### 声明私有方法
-
+
diff --git a/Chapter1 - iOS/1.49.md b/Chapter1 - iOS/1.49.md
index 6d85086..c80fdca 100644
--- a/Chapter1 - iOS/1.49.md
+++ b/Chapter1 - iOS/1.49.md
@@ -6,7 +6,7 @@ MVC 模式下,软件被划分为视图(View:用户界面)、控制器(
-
+
1. 用户操作 View,在 View 上面的事件都将被传递到 Controller 处理
2. Controller 处理事件、请求网络,操作 Model 更新状态
@@ -20,7 +20,7 @@ MVC 模式下,软件被划分为视图(View:用户界面)、控制器(
效果等价于:
-
+
最典型的就是 iOS 侧的 UITableView 的设计:
@@ -61,7 +61,7 @@ MVC 模式下,软件被划分为视图(View:用户界面)、控制器(
### MVC 架构变种
-
+
改变:
@@ -331,7 +331,7 @@ MVC 模式下,软件被划分为视图(View:用户界面)、控制器(
MVP 模式将 Controller 改名为 Presenter,通信改变了通信方向
-
+
1. 各部分之间的通信都是双向的
2. Model 与 View 不发生联系,都通过 Presenter 传递
@@ -658,25 +658,25 @@ Controller 里组装和创建 Presenter,在 `viewDidLoad` 里面调用 present
MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。
-
+
区别在于:采用双向绑定的模式。View 的改变,自动反应在 ViewModel 上。 ViewModel 的改变会发应在 View 上。
MVC 等到业务逻辑很复杂的时候被称为 Massive View Controller (重量级视图控制器)。这样的 Controller 后期维护看着阅读成本很高、不易于测试、维护。当时写这个代码的人离职后,几千行规模的代码在后期添加新功能或者修改 Bug 都是一件折磨人的事情。
-
+
看到 View 和 ViewController 是不同的技术组件,但是日常开发中它们总是成对的存在,为什么不考虑将他们的连接正规化呢?
-
+
典型的 MVC 存在弊端就是 Controller 层非常复杂,很多逻辑都在里面,包括一些不是逻辑的“表示逻辑”(presentation logic)。用 MVVM 术语来说就是将那些 Model 数据转换为 View 可以呈现的东西。例如将一个 NSDate 格式化为一个“2018-11-17” 这样的 NSString。
上图中缺少一个环节,一个专门用来处理所有的表示逻辑。称为 “ViewModel”。位于 ViewController 与 Model 之间。
-
+
MVVM 就是 MVC 的增强版,将展示层逻辑单独拎出来,即 ViewModel。iOS 使用 MVVM 可以降低 ViewController 的复杂性并使得表示逻辑易于测试。
diff --git a/Chapter1 - iOS/1.60.md b/Chapter1 - iOS/1.60.md
index 6c5f090..9b04c54 100644
--- a/Chapter1 - iOS/1.60.md
+++ b/Chapter1 - iOS/1.60.md
@@ -32,7 +32,7 @@ App Thinning 是苹果公司推出的一项改善 App 下载进程的新技术
### 1.1 Slicing
-
+
当向 App Store Connect 上传 .ipa 后,App Store Connect 构建过程中,会自动分割该 App,创建特定的变体(variant)以适配不同设备。然后用户从 App Store 中下载到的安装包,即这个特定的变体,这一过程叫做 Slicing。
@@ -42,7 +42,7 @@ App Thinning 是苹果公司推出的一项改善 App 下载进程的新技术
其中,2x 和 3x 的细分,要求图片在 **Assets** 中管理。Bundle 内的则会同时包含。
-
+
### 1.2 Bitcode
@@ -66,9 +66,9 @@ Bitcode 是一种程序`中间码`。包含 Bitcode 配置的程序将会在 App
在上传到 App Store 时需勾选“Includ app symbols for your application...”。勾选之后 Apple 会自动生成对应的 dYSM,然后可以在 Xcode -> Window -> Organizer 中,或者在 Apple Store Connect 中下载对应的 dYSM 来进行符号化
-
+
-
+
那么 Bitcode 会对 App Thining 有什么作用?
@@ -83,7 +83,7 @@ Bitcode 是一种程序`中间码`。包含 Bitcode 配置的程序将会在 App
on-Demand Resource 即一部分图片可以被放置在苹果的服务器上,不随着 App 的下载而下载,直到用户真正进入到某个页面时才下载这些资源文件。
-
+
应用场景:相机应用的贴纸或者滤镜、关卡游戏等
@@ -101,7 +101,7 @@ on-Demand Resource 即一部分图片可以被放置在苹果的服务器上,
包体积,评判标准是以 App Store 上看到的为准。但是上传到 App Store Connect 处理完后,会自动帮我们生成具体设备上看到的大小。如下:
-
+
这其中:又可以分为2类: Universal 和具体设备
Universal 指通用设备,即未应用 App slicing 优化,同时包含了所有架构、资源。所以包体积会比较大
@@ -245,13 +245,13 @@ self.imageView.image = images.lastObject;
Timeprofile-imageNamedFromAssets
-
+
TimeProfile-imageWithContentsOfFile
-
+
Timeprofile-UIImageNamedFromFolder
-
+
Images.xcassets :
@@ -276,7 +276,7 @@ CocoPods 中两种资源引用方式介绍下:
说说项目中的情况吧:在工程中之前是通过 resource_bundles 引用资源的。资源是放在 Resources 目录下的图片引用。查询资料后说「如果图片资源放到 .xcasset 里面 Xcode 会帮我们自动优化、可以使用 Slicing 等(这里不仅仅指的是 resource_bundle 下的 xcassets」。所以动手将各个 Pod 库里面的图片全都通过 Assets Catalog 的方式进行处理。
-
+
步骤:
@@ -520,7 +520,7 @@ Link-Time Optimization 是 LLVM 编译器的一个特性,用于在 link 中间
LinkMap 文件分为3部分:Object File、Section、Symbols。
-
+
- Object File:包含了代码工程的所有文件
- Section:描述了代码段在生成的 Mach-O 里的偏移位置和大小
@@ -529,7 +529,7 @@ LinkMap 文件分为3部分:Object File、Section、Symbols。
先说说如何快速找到方法和类的全集?
我们可以通过 **LinkMap** 来获得所有的代码类和方法的信息。获取 LinkMap 可以通过将 Build Setting 里面的 **Write Link Map File** 设置为 YES,然后指定 **Path to Link Map File** 的路径就可以得到每次编译后的 LinkMap 文件了。
-
+
产出的 LinkMap 阅读起来比较累,github 有个[可视化项目](https://github.com/jayden320/LinkMap) 用来查看 LinkMap 文件。
@@ -541,11 +541,11 @@ LinkMap 文件分为3部分:Object File、Section、Symbols。
上面我们得知可以通过 LinkMap 统计出所有的类和方法,还可以清晰地看到代码所占包大小的具体分布,进而有针对性地进行代码优化。
-
+
-
+
-
+
得到了代码的全集信息后,我们还需要找到已经使用过的方法和类,这样才可以获取差集,找到无用代码。所以接下来就谈谈如何通过 Mach-O 取到使用过的类和方法。
@@ -562,7 +562,7 @@ Objective-C 中的方法都会通过 **objc_msgSend** 来调用,而 objc_msgSe
前置条件:先运行项目,在生成的 Products 目录下的 BridgeLabiPhone.app 解压,取出对应的和工程同名的 BridgeLabiPhone。然后运行上面的 Github 项目。可以看到运行了一个 Mac App。点击顶部的菜单栏里面的 File->Open。选择电脑上的 BridgeLabiPhone.app 选择里面的 BridgeLabiPhone。见下图
-
+
由于 Objective-C 是一门动态语言,所以检测出的结果仍旧需要我们2次确认。
@@ -576,7 +576,7 @@ Objective-C 中的方法都会通过 **objc_msgSend** 来调用,而 objc_msgSe
AppCode 提供了 Inspect Code 来诊断代码,其中含有查找无用代码的功能。它可以帮助我们查找出 AppCode 中无用的类、无用的方法甚至是无用的 import ,但是无法扫描通过字符串拼接方式来创建的类和调用的方法,所以说还是上面所说的 基于源码扫描 更加准确和安全。
-
+
说明:AppCode检测出了实际上需要的大部分场景的问题,但是由于 Objective-C 是一门动态性语言,所以 AppCode 检测出无用的方法等都需要工程师自己再次确认后删除。(在我们的工程中有一些和 H5 交互的桥接方法,因此 AppCode 视为 Unused Method,但是你删除的话,那就自己哭去吧 😭)。实际经验告诉我,使用 AppCode 的时候如果工程比较大,则整个 code inspect 会非常耗时(给你打个预防针哦,笔芯)
@@ -684,7 +684,7 @@ lipo create libWeiboSDK-armv7.a libWeiboSDK-arm64.a -output libWeiboSDK.device.a
DebuggingWith Arbitrary Record Formats 是 ELF 和 Mach-O 等文件格式中用来存储和处理调试信息的标准格式,.dSYM 文件中真正保存符号表数据的是 DWARF 文件。DWARF 文件中不同的数据都保存在相应的 section 中。
最后的一个对比效果图:
-
+
总结:瘦身技术常见操作就这些,但是维持应用包体积的瘦身却是一个观念,从日常开发到线上发布都需要有这个意识。这样当你在写代码的时候就会考虑同样一个效果,你的具体实现手段是怎么样的。比如为了一个稍微炫酷的效果就要引入一个很大的三方库,有了“瘦身”的意识,你很大可能就是自己动手撸一个代码。比如一些无用资源的管理方式、有用的图片资源的高效管理方式等等。有了意识,行动自然会往这个方面去靠。(😂大道理一套一套的。我也不想的,毕竟是playboy)
diff --git a/Chapter1 - iOS/1.7.md b/Chapter1 - iOS/1.7.md
index f652e80..3713c3f 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的值相同。**
@@ -768,7 +768,7 @@ iOS 中,系统分配内存,都是16的倍数。pageSize?系统在分配内
GUN 都存在内存对齐这个概念。
`sizeof` 本质是运算符。在 Xcode 编译后就替换为真正的值。通过指令 `xcrun --sdk iphoneos clang -arch arm64 -S -emit-llvm ViewController.m -o ViewController.ll` 查看 IR。
-
+
@@ -779,21 +779,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
@@ -997,7 +997,7 @@ class_rw_t *personMetaClassData = personClass->metaClass()->data();
可以看到 Xcode 报错了,因为是 `main.m` 引入了 `MockClassInfo.h` ,会当作 OC 去编译。为了解决编译报错,将 `main.m` 改为 `main.mm` ,变成为 objective-c++ 文件,支持 OC 和 C++ 混编。
-
+
@@ -1006,7 +1006,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 硬件平台上,当操作系统被要求存取一个未对齐数据时会默认给应用程序抛出硬件异常。
@@ -1103,7 +1103,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 里面有个判断
@@ -1118,7 +1118,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 源码
@@ -1302,7 +1302,7 @@ Class objc_getClass(const char *aClassName)
## 总结
-
+
1. 实例对象的 isa 指针指向类对象
2. 类对象的 isa 指针指向元类对象
diff --git a/Chapter1 - iOS/1.73.md b/Chapter1 - iOS/1.73.md
index 651de85..4df013b 100644
--- a/Chapter1 - iOS/1.73.md
+++ b/Chapter1 - iOS/1.73.md
@@ -95,7 +95,7 @@ TrueClass
### 3. Bundler
Bundler 能够跟踪并安装所需的特定版本的 gem,以此来为 Ruby 项目提供一致的运行环境
-
+
```
source 'https://rubygems.org' gem 'rails', '4.1.0.rc2'
@@ -285,15 +285,15 @@ Podfile 是一个文件,以 DSL 来描述依赖关系,用于描述项目所
```
10. VSCode 插件市场安装:VSCode rdbg Ruby Debugger、Ruby LSP
11. VSCode 面板中,点击左侧的调试按钮。便可调试。要是看到下面的图,说明可以正常 Debug 了
-
+
接下来就可以愉快的调试了。
说明:
- pod 的每个指令,分别对应 Cocoapods 工程中一个代码文件
-
+
- 同时根据观察,发现 `target do` 的代码比 pre_install、post_install 执行更早。所以我们可以做一些脚本化的操作。
比如下面,增加了一段自定义的脚本
-
+
@@ -312,11 +312,11 @@ lc_rpath = MachO::LoadCommands::LoadCommand.create(:LC_RPATH, 'test_rpath')
file_exec.add_command lc_rpath
```
-
+
对 Mach-O 文件中删除了类型为 `LC_LINKER_OPTION` 的 Load Command
-
+
#### 2. 操作动态库
@@ -341,7 +341,7 @@ file_exec.add_command lc_rpath
修改后在终端用指令 **objdump --macho --private-headers ./macho/libAFNetworking_copy.dylib | grep 'LC_ID_DYLIB' -A 5** 验证效果,如下图所示:
-
+
也可以直接查看动态库的 id
@@ -360,7 +360,7 @@ file_exec.add_command lc_rpath
MachO::Tools.change_rpath(macho_copy_filepath, '@loader_path/Frameworks', '@loader_path/Frameworks/FantasicLBP')
```
-
+
#### 3. 合并动态库到胖二进制
@@ -374,7 +374,7 @@ MachO::Tools.merge_machos(dylib_merged_filepath, *filenames)
合并后用 `otool -f ./macho/libAFNetworking_merged.dylib` 指令查看指令集
-
+
@@ -392,7 +392,7 @@ app_workspace.schemes.each do | scheme |
end
```
-
+
也可以针对特定的 target 修改 xcconfig
@@ -407,7 +407,7 @@ app_project.targets.first.build_configurations.first.base_configuration_referenc
效果如下:
-
+
也可以对特定的 target 修改 buildSetting 中的信息,比如 bundle id
@@ -420,7 +420,7 @@ app_project.targets.each do | target |
end
```
-
+
@@ -560,7 +560,7 @@ end
VSCode 中运行效果如下:
-
+
#### 4. 如何将自定义的指令加入到 cocoapods 中
@@ -574,7 +574,7 @@ VSCode 中运行效果如下:
调试运行后的效果如下:
-
+
类名小写,和类文件关联起来
@@ -588,11 +588,11 @@ VSCode 中运行效果如下:
一开始有报错,如下图所示。按照提示修改 `cocoapods-hmap.gemspec` 中的配置,然后就可以成功安装了。然后输入 **gem list** 查看:
-
+
输入: **gem info cocoapods-hmap** 查看安装信息
-
+
@@ -603,7 +603,7 @@ VSCode 中运行效果如下:
| 随便一个工程目录,没有 Podfile 文件 | 执行 `pod hmap` 会报错 | 符合预期 |
| 存在 Podfile 文件的目录 | 正常执行 run 方法里面的逻辑(打印逻辑) | 符合预期 |
-
+
@@ -715,7 +715,7 @@ VSCode 中运行效果如下:
- 第一种:在 cocospods-hmap 工程中测试,如下图
-
+
- 第二种:
@@ -724,7 +724,7 @@ VSCode 中运行效果如下:
效果如下:
-
+
diff --git a/Chapter1 - iOS/1.74.md b/Chapter1 - iOS/1.74.md
index 98d39c4..29b7450 100644
--- a/Chapter1 - iOS/1.74.md
+++ b/Chapter1 - iOS/1.74.md
@@ -22,11 +22,11 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
### 1. 屏幕绘制原理
-
+
讲讲老式的 CRT 显示器的原理。 CRT 电子枪按照上面方式,从上到下一行行扫描,扫面完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;当一帧画面绘制完成后,电子枪恢复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(Vertical synchronization),简称 VSync。显示器通常以固定的频率进行刷新,这个固定的刷新频率就是 VSync 信号产生的频率。虽然现在的显示器基本都是液晶显示屏,但是原理保持不变。
-
+
通常,屏幕上一张画面的显示是由 CPU、GPU 和显示器是按照上图的方式协同工作的。CPU 根据工程师写的代码计算好需要显实的内容(比如视图创建、布局计算、图片解码、文本绘制等),然后把计算结果提交到 GPU,GPU 负责图层合成、纹理渲染,随后 GPU 将渲染结果提交到帧缓冲区。随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过数模转换传递给显示器显示。
@@ -36,7 +36,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
为了解决这个问题,GPU 通常有一个机制叫垂直同步信号(V-Sync),当开启垂直同步信号后,GPU 会等到视频控制器发送 V-Sync 信号后,才进行新的一帧的渲染和帧缓冲区的更新。这样的几个机制解决了画面撕裂的情况,也增加了画面流畅度。但需要更多的计算资源
-
+
答疑
@@ -48,7 +48,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
揭秘。请看下图
-
+
当第一次 V-Sync 信号到来时,先渲染好一帧图像放到帧缓冲区,但是不展示,当收到第二个 V-Sync 信号后读取第一次渲染好的结果(视频控制器的指针指向第一个帧缓冲区),并同时渲染新的一帧图像并将结果存入第二个帧缓冲区,等收到第三个 V-Sync 信号后,读取第二个帧缓冲区的内容(视频控制器的指针指向第二个帧缓冲区),并开始第三帧图像的渲染并送入第一个帧缓冲区,依次不断循环往复。
@@ -56,7 +56,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
### 2. 卡顿产生的原因
-
+
VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容(视图创建、布局计算、图片解码、文本绘制等)。然后将计算的内容提交到 GPU,GPU 经过图层的变换、合成、渲染,随后 GPU 把渲染结果提交到帧缓冲区,等待下一次 VSync 信号到来再显示之前渲染好的结果。在垂直同步机制的情况下,如果在一个 VSync 时间周期内,CPU 或者 GPU 没有完成内容的提交,就会造成该帧的丢弃(也叫掉帧),等待下一次机会再显示,这时候屏幕上还是之前渲染的图像,所以这就是 CPU、GPU 层面界面卡顿的原因。
@@ -75,7 +75,7 @@ RunLoop 负责监听输入源进行调度处理。比如网络、输入设备、
RunLoop 状态如下图
-
+
第一步:通知 Observers,RunLoop 要开始进入 loop,紧接着进入 loop
@@ -251,9 +251,9 @@ if (sourceHandledThisLoop && stopAfterHandle) {
}
```
-完整且带有注释的 RunLoop 代码见[此处](./../assets/CFRunLoop.c)。 Source1 是 RunLoop 用来处理 Mach port 传来的系统事件的,Source0 是用来处理用户事件的。收到 Source1 的系统事件后本质还是调用 Source0 事件的处理函数。
+完整且带有注释的 RunLoop 代码见[此处](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CFRunLoop.c)。 Source1 是 RunLoop 用来处理 Mach port 传来的系统事件的,Source0 是用来处理用户事件的。收到 Source1 的系统事件后本质还是调用 Source0 事件的处理函数。
-
+
RunLoop 6 个状态
```Objective-C
@@ -286,13 +286,13 @@ WatchDog 在不同状态下具有不同的值。
通过 `long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)` 方法判断是否阻塞主线程,`Returns zero on success, or non-zero if the timeout occurred.` 返回非 0 则代表超时阻塞了主线程。
-
+
可能很多人纳闷 RunLoop 状态那么多,为什么选择 KCFRunLoopBeforeSources 和 KCFRunLoopAfterWaiting?因为大部分卡顿都是在 KCFRunLoopBeforeSources 和 KCFRunLoopAfterWaiting 之间。比如 Source0 类型的 App 内部事件等
Runloop 检测卡顿流程图如下:
-
+
关键代码如下:
@@ -371,7 +371,7 @@ while (self.isCancelled == NO) {
在计算机科学中,调用堆栈是一种栈类型的数据结构,用于存储有关计算机程序的线程信息。这种栈也叫做执行堆栈、程序堆栈、控制堆栈、运行时堆栈、机器堆栈等。调用堆栈用于跟踪每个活动的子例程在完成执行后应该返回控制的点。
维基百科搜索到 “Call Stack” 的一张图和例子,如下
-
+
上图表示为一个栈。分为若干个栈帧(Frame),每个栈帧对应一个函数调用。下面蓝色部分表示 `DrawSquare` 函数,它在执行的过程中调用了 `DrawLine` 函数,用绿色部分表示。
可以看到栈帧由三部分组成:函数参数、返回地址、局部变量。比如在 DrawSquare 内部调用了 DrawLine 函数:第一先把 DrawLine 函数需要的参数入栈;第二把返回地址(控制信息。举例:函数 A 内调用函数 B,调用函数 B 的下一行代码的地址就是返回地址)入栈;第三函数内部的局部变量也在该栈中存储。
@@ -464,11 +464,11 @@ static mach_port_t main_thread_id;
这里有个策略,卡顿是由于主线程某个或某组方法执行过长而造成的卡顿,所以卡顿堆栈只需要分析主线程即可,但是此时是未符号化的方法(完成流程如下)
-
+
测试过,单次抓取主线程符号耗时大概1ms左右。因此连续抓取堆栈是可取方案。
-
+
按照栈顶函数地址和当前堆栈的深度作为判断依据,如果某个堆栈栈顶地址和深度相同,则出现次数最多的一个堆栈,可以认为是一个卡顿堆栈,因为认为当发生卡顿的时候,函数堆栈大概率不会变化。
@@ -505,7 +505,7 @@ static mach_port_t main_thread_id;
上传这些信息到服务端后,APM 后端调用符号化服务的能力,符号化机器找到对应的 DSYM 文件,调用 atos 的指令进行符号化,如下效果。
-
+
系统堆栈符号化的问题,也就是获取系统符号文件的问题。可以通过 ipsw,也就是刷机所需要的固件信息。
@@ -517,7 +517,7 @@ static mach_port_t main_thread_id;
服务端聚合策略
-
+
找到最接近栈顶的符合占比条件的业务代码方法,作为卡顿堆栈归类 key。
@@ -632,7 +632,7 @@ Xcode 增加环境变量,添加 `DYLD_PRINT_STATISTICS` 设为1,来查看 pr
应用启动时间是影响用户体验的重要因素之一,所以我们需要量化去衡量一个 App 的启动速度到底有多快。启动分为冷启动和热启动。
-
+
冷启动:App 尚未运行,必须加载并构建整个应用。完成应用的初始化。冷启动存在较大优化空间。冷启动时间从 `application: didFinishLaunchingWithOptions:` 方法开始计算,App 一般在这里进行各种 SDK 和 App 的基础初始化工作。
@@ -669,10 +669,10 @@ App 启动过程:
- 程序执行:调用 main();调用 UIApplicationMain();调用 applicationWillFinishLaunching();
Pre-Main 阶段
-
+
Main 阶段
-
+
#### 2.1 加载 Dylib
@@ -760,9 +760,9 @@ QA:为什么 `+ initialize` 需要搭配 `dispatch_once`?因为 `+initialize`
### 4. 精确版启动时间监控
-
+
-
+
进程创建:通过 sysctl 可以拿到
@@ -772,7 +772,7 @@ didFinishLaunching:通过 UIApplicationDidFinishLaunchingNotification 拿到
首屏渲染时间怎么拿?能获取到 `CA::Transaction::commit()` 时刻即可。
-
+
对 UI 进行修改后会在主线程休眠前触发 `CA::Transaction::commit()`,其实就是打包 Render Tree,通过 IPC 发送给 Render Server。
@@ -784,21 +784,21 @@ didFinishLaunching:通过 UIApplicationDidFinishLaunchingNotification 拿到
- Layout 布局:调用 `layout` 等与布局相关的 API
-
+
-
+
- Display 绘制:调用 `drawRect` 等与绘制相关方法
-
+
- Prepare:图片解码
- Commit:递归打包 Render Tree,通过 IPC 计数发送给 Render Server
-
-
-
+
+
+
断点调试完,`[BSXPCServiceConnectionProxy invokeMethod:onTarget:withMessage:forConnection:]` 底层调用的是 send 方法。send 方法调用 `BSXPCServiceConnectionMessageReply send` 方法。
@@ -1006,14 +1006,14 @@ App 内存不足时,系统会按照一定策略来腾出更多的空间供使
Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能一个 page 持有多个对象,也可能一个大的对象跨越多个 page。通常它是 16KB 大小,且有 3 种类型的 page。
-
+
- Clean Memory
Clean memory 包括 3 类:可以 `page out` 的内存、内存映射文件、App 使用到的 framework(每个 framework 都有 \_DATA_CONST 段,通常都是 clean 状态,但使用 runtime swizling,那么变为 dirty)。
一开始分配的 page 都是干净的(堆里面的对象分配除外),我们 App 数据写入时候变为 dirty。从硬盘读进内存的文件,也是只读的、clean page。
- 
+ 
- Dirty Memory
@@ -1021,7 +1021,7 @@ Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能
在使用 framework 的过程中会产生 Dirty memory,使用单例或者全局初始化方法有助于帮助减少 Dirty memory(因为单例一旦创建就不销毁,一直在内存中,系统不认为是 Dirty memory)。
- 
+ 
- Compressed Memory
@@ -1032,7 +1032,7 @@ Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能
App 运行内存 = pageNumbers \* pageSize。因为 Compressed Memory 属于 Dirty memory。所以 Memory footprint = dirtySize + CompressedSize
设备不同,内存占用上限不同,App 上限较高,extension 上限较低,超过上限 crash 到 `EXC_RESOURCE_EXCEPTION`。
-
+
接下来谈一下如何获取内存上限,以及如何监控 App 因为占用内存过大而被强杀。
@@ -1827,7 +1827,7 @@ static int jetsam_do_kill(proc_t p, int jetsam_flags, os_reason_t jetsam_reason)
FacekBook 提出排除法监控 OOM。
-
+
- App 更新了版本
@@ -2232,7 +2232,7 @@ App 上线前经过自测、UI 自动化、精准测试、高铁回归包测试
#### 野指针可能存在的问题
-
+
### 2. Zombie Object
@@ -2279,11 +2279,11 @@ self:_NSZombie_Person - superClass:nil
利用 Instruments 下的 Zombies 工具检测,看看野指针的情况,系统是怎么捕获的。
-
+
切换到 `Call Trees` 模式下可以看到系调用了 `_dealloc_zombie` 方法。底层到底做了什么?加个符号断点查看下
-
+
通过符号名称大概可以猜系统会在调用 dealloc 方法时,类似 KVO,派生出一个新的僵尸类,将当前类的 isa 指针指向僵尸类。
@@ -2990,7 +2990,7 @@ bool init_safe_free(void)
注意:ZombieProxy、ZombieObjectDetector 2个类要在 Build Phases 设置为非 ARC,加 `-fno-objc-arc`
-
+
### 6. 方案对比
@@ -3005,7 +3005,7 @@ bool init_safe_free(void)
### 1. App 网络请求过程
-
+
App 发送一次网络请求一般会经历下面几个关键步骤:
@@ -3043,7 +3043,7 @@ App 发送一次网络请求一般会经历下面几个关键步骤:
iOS 网络框架层级关系如下:
-
+
iOS 网络现状是由 4 层组成的:最底层的 BSD Sockets、SecureTransport;次级底层是 CFNetwork、NSURLSession、NSURLConnection、WebView 是用 Objective-C 实现的,且调用 CFNetwork;应用层框架 AFNetworking 基于 NSURLSession、NSURLConnection 实现。
@@ -3631,11 +3631,11 @@ iOS 中 hook 技术有 2 类,一种是 NSProxy,一种是 method swizzling(
但是如果需要监全部的网络请求就不能满足需求了,查阅资料后发现了阿里百川有 APM 的解决方案,于是有了方案 3,对于网络监控需要做如下的处理
-
+
可能对于 CFNetwork 比较陌生,可以看一下 CFNetwork 的层级和简单用法
-
+
CFNetwork 的基础是 CFSocket 和 CFStream。
@@ -3732,9 +3732,9 @@ void printResponseData (CFDataRef responseData) {
NSURLSession、NSURLConnection hook 如下。
-
+
-
+
业界有 APM 针对 CFNetwork 的方案,整理描述下:
@@ -4009,7 +4009,7 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
};
```
- 
+ 
method swizzling 改进版如下
@@ -4036,7 +4036,7 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
typedef struct objc_object *id;
```
- 
+ 
我们来分析一下为什么修改 `isa` 可以实现目的呢?
@@ -4194,11 +4194,11 @@ self.didCompleteObserver = [[NSNotificationCenter defaultCenter] addObserverForN
HTTP 请求报文结构
-
+
响应报文的结构
-
+
1. HTTP 报文是格式化的数据块,每条报文由三部分组成:对报文进行描述的起始行、包含属性的首部块、以及可选的包含数据的主体部分。
2. 起始行和手部就是由行分隔符的 ASCII 文本,每行都以一个由 2 个字符组成的行终止序列作为结束(包括一个回车符、一个换行符)
@@ -4225,11 +4225,11 @@ HTTP 请求报文结构
下图是打开 Chrome 查看极课时间网页的请求信息。包括响应行、响应头、响应体等信息。
-
+
下图是在终端使用 `curl` 查看一个完整的请求和响应数据
-
+
我们都知道在 HTTP 通信中,响应数据会使用 gzip 或其他压缩方式压缩,用 NSURLProtocol 等方案监听,用 NSData 类型去计算分析流量等会造成数据的不精确,因为正常一个 HTTP 响应体的内容是使用 gzip 或其他压缩方式压缩的,所以使用 NSData 会偏大。
@@ -4875,7 +4875,7 @@ Mach 已经通过异常机制提供了底层的陷进处理,而 BSD 则在异
Mach 异常都在 host 层被 `ux_exception` 转换为相应的 unix 信号,并通过 `threadsignal` 将信号投递到出错的线程。
-
+
### 2. Crash 收集方式
@@ -4939,7 +4939,7 @@ KSCrash 功能齐全,可以捕获如下类型的 Crash
流程图如下:
-
+
对于 Mach 异常捕获,可以注册一个异常端口,该端口负责对当前任务的所有线程进行监听。
@@ -5250,7 +5250,7 @@ static void restoreExceptionPorts(void)
KSCrash 在这里的处理逻辑如下图:
-
+
看一下关键代码:
@@ -5612,9 +5612,9 @@ static void setEnabled(bool isEnabled)
阅读下源码,看看为什么 `NSUncaughtExceptionHandler` 可以收集 crash 信息。查看 objc 源码
-
+
-
+
发现入口函数 `_objc_init` 中调用可设置异常处理的 `exception_init` 方法。内部通过底层 API `std::set_terminate(&_objc_terminate)` 来设置异常处理的 handler。
@@ -5716,7 +5716,7 @@ static void setEnabled(bool isEnabled)
其他几个 crash 也是一样,异常信息经过包装交给 `kscm_handleException()` 函数处理。可以看到这个函数被其他几种 crash 捕获后所调用。
-
+
```c
/** Start general exception processing.
@@ -6217,7 +6217,7 @@ static int64_t getReportIDFromFilename(const char* filename)
}
```
-
+
#### 2.7 前端 js 相关的 Crash 的监控
@@ -6241,7 +6241,7 @@ window.onerror = function (msg, url, lineNumber, columnNumber, error) {
};
```
-
+
##### 2.7.3 React Native 异常监控
@@ -6264,7 +6264,7 @@ window.onerror = function (msg, url, lineNumber, columnNumber, error) {
模拟器点击 `command + d` 调出面板,选择 Debug,打开 Chrome 浏览器, Mac 下快捷键 `Command + Option + J` 打开调试面板,就可以像调试 React 一样调试 RN 代码了。
-
+
查看到 crash stack 后点击可以跳转到 sourceMap 的地方。
@@ -6288,7 +6288,7 @@ Tips:RN 项目打 Release 包
现象:iOS 项目奔溃。截图以及日志如下
-
+
```shell
2020-06-22 22:26:03.318 [info][tid:main][RCTRootView.m:294] Running application todos ({
@@ -6382,7 +6382,7 @@ global.ErrorUtils.setGlobalHandler((e) => {
现象:iOS 项目不奔溃。日志信息如下,对比 bundle 包中的 js。
-
+
结论:
@@ -6763,7 +6763,7 @@ parseJSError(line, column);
5. 拿脚本解析好的行号、列号、文件信息去和源代码文件比较,结果很正确。
-
+
##### 2.7.5 SourceMap 解析系统设计
@@ -7055,7 +7055,7 @@ parseJSError(line, column);
`.DSYM` 文件是从 Mach-O 文件中抽取调试信息而得到的文件目录,发布的时候为了安全,会把调试信息存储在单独的文件,`.DSYM` 其实是一个文件目录,结构如下:
-
+
#### 4.2 DWARF 文件
@@ -7587,7 +7587,7 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
/Users/你自己的用户名/Library/Developer/Xcode/iOS DeviceSupport/
```
-
+
### 5. 服务端处理
@@ -7597,7 +7597,7 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
早期单体应用时代,几乎应用的所有功能都在一台机器上运行,出了问题,运维人员打开终端输入命令直接查看系统日志,进而定位问题、解决问题。随着系统的功能越来越复杂,用户体量越来越大,单体应用几乎很难满足需求,所以技术架构迭代了,通过水平拓展来支持庞大的用户量,将单体应用进行拆分为多个应用,每个应用采用集群方式部署,负载均衡控制调度,假如某个子模块发生问题,去找这台服务器上终端找日志分析吗?显然台落后,所以日志管理平台便应运而生。通过 Logstash 去收集分析每台服务器的日志文件,然后按照定义的正则模版过滤后传输到 Kafka 或 Redis,然后由另一个 Logstash 从 Kafka 或 Redis 上读取日志存储到 ES 中创建索引,最后通过 Kibana 进行可视化分析。此外可以将收集到的数据进行数据分析,做更进一步的维护和决策。
-
+
上图展示了一个 ELK 的日志架构图。简单说明下:
@@ -7607,13 +7607,13 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
下图贴一个 Elasticsearch 社区分享的一个 “Elastic APM 动手实战”[主题](https://elasticsearch.cn/slides/257#page=3)的内容截图。
-
+
##### 5.2 服务侧
Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化处理,以方便定位问题、crash 产生报表和后续处理。
-
+
所以整个流程就是:客户端 APM SDK 收集 crash log -> Kafka 存储 -> Mac 机执行定时任务符号化 -> 数据回传 Kafka -> 产品侧(显示端)对数据进行分类、报表、报警等操作。
@@ -7625,7 +7625,7 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
现在很多架构设计都是微服务,至于为什么选微服务,不在本文范畴。所以 crash 日志的符号化被设计为一个微服务。架构图如下
-
+
说明:
@@ -7637,19 +7637,19 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
- 脚手架 cli 有个能力就是调用打包系统的打包构建能力,会根据项目的特点,选择合适的打包机(打包平台是维护了多个打包任务,不同任务根据特点被派发到不同的打包机上,任务详情页可以看到依赖的下载、编译、运行过程等,打包好的产物包括二进制包、下载二维码等等)
-
+
其中符号化服务是大前端背景下大前端团队的产物,所以是 NodeJS 实现的(单线程,所以为了提高机器利用率,就要开启多进程能力)。iOS 的符号化机器是 双核的 Mac mini,这就需要做实验测评到底需要开启几个 worker 进程做符号化服务。结果是双进程处理 crash log,比单进程效率高近一倍,而四进程比双进程效率提升不明显,符合双核 mac mini 的特点。所以开启两个 worker 进程做符号化处理。
下图是完整设计图
-
+
简单说明下,符号化流程是一个主从模式,一台 master 机,多个 slave 机,master 机读取 .DSYM 和 crash 结果的 cache。「数据处理和任务调度框架」调度符号化服务(内部 2 个 symbolocate worker)同时从七牛云上获取 .DSYM 文件。
系统架构图如下
-
+
## 九、Weex、Flutter 异常监控
@@ -7945,7 +7945,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
1. 当前启动时的js 预载入流程里的JS下载失败 和 进入Weex页面后的JS拉取失败 两处的上报失败未做区分,没法实际统计加载页面时有多少JS获取失败的情况。
2. 随着业务迭代, Weex 页面越来越多,需要做 JS 拉取相关优化,避免白屏问题
3. 目前 JS 缓存逻辑为,每个 Weex 模块单独维护一个 LRUCache,并且会做大量的预加载,但可能大多数页面根本不会访问到。浪费了内存资源去做了一些不会被调用到的页面预加载,可能还会造成 OOM 问题
- 
+ 
// todo? 参考前端资源模块化,如何实现资源预加载(全局 LRU或者基于用户行为路径的预热功能,比如某个收银员角色固定的情况下,他日常的操作行为是固定的,商品扫码、开单)
@@ -8145,17 +8145,17 @@ UI 控件存在依赖关系(比如父子组件、UI 组件树),多线程
可能有些人一直没有遇到过因为在子线程操作 UI,导致在开发阶段 Xcode console 输出了一堆日志,大体如下
-
+
本来常见的开发都会规避这些写法,没机会看到子线程操作 UI 的问题,但是 Weex 的业务代码,检测出存在子线程操作 UI 的问题,所以还是有必要增加这个能力的。
其实我们可以给 Xcode 打个 `Runtime Issue Breakpoint` ,type 选择 `Main Thread Checker`, 在发生子线程操作 UI 的时候就会被系统检测到并触发断点,同时可以看到堆栈情况
-
+
效果如下
-
+
### 2. 问题及解决方案
@@ -8169,7 +8169,7 @@ UI 控件存在依赖关系(比如父子组件、UI 组件树),多线程
但是某些类有些 API 我们也不希望在子线程被调用,这时候 `libMainThreadChecker.dylib`是无法满足的。
对 `libMainThreadChecker.dylib` 库的汇编代码研究,发现 `libMainThreadChecker.dylib` 是通过内部 `__main_thread_add_check_for_selector` 这个方法来进行类和方法的注册的。所以如果我们同样可以通过 `dlsym` 来调用该方法,以达到对自定义类和方法的主线程调用监测。
-
+
另外该功能可以在线下 debug 阶段开启,判断是否是在 Xcode debug 状态,可以通过苹果提供的[官方判断方法](https://developer.apple.com/library/archive/qa/qa1361/_index.html#//apple_ref/doc/uid/DTS10003368)实现。
@@ -8183,7 +8183,7 @@ UI 控件存在依赖关系(比如父子组件、UI 组件树),多线程
好像用 APM 的卡顿没发现啥问题。仔细看看发现了问题,我们的卡顿只是监控主线程方法耗时。一个页面加载后可能会触发一些网络请求功能,子线程请求完网络,可能会做一些数组裁剪、组装,拼接为 UI 所需要的信息,这个时候很可能会发生卡顿,所以这种 case 下卡顿监控是无法覆盖的。用下图表示
-
+
页面作为承载用户交互的具体战场,我们需要对页面的性能有个直观的指标。业界一般有2个指标:**页面渲染时长、页面可交互时长**。
@@ -8295,7 +8295,7 @@ UI 控件存在依赖关系(比如父子组件、UI 组件树),多线程
6. 整个 APM 的架构图如下
- 
+ 
说明:
diff --git a/Chapter1 - iOS/1.75.md b/Chapter1 - iOS/1.75.md
index 171c46f..f06313a 100644
--- a/Chapter1 - iOS/1.75.md
+++ b/Chapter1 - iOS/1.75.md
@@ -1586,7 +1586,7 @@ sequenceDiagram
下面是之前在有赞,开发完精准测试系统后,落地到一个业务项目中取得的价值,帮助2位 QA 发现漏测的代码,倒逼 QA 去设计更完善的测试 case、分析覆盖率低是开发者的兜底代码太多还是真的漏掉了业务 case。
-
+
精准测试助力业务,质量更加稳定。
diff --git a/Chapter1 - iOS/1.82.md b/Chapter1 - iOS/1.82.md
index 4e94a3a..c0710e4 100644
--- a/Chapter1 - iOS/1.82.md
+++ b/Chapter1 - iOS/1.82.md
@@ -75,7 +75,7 @@ Person 类存在3个 BOOL 属性:
上 Demo
-
+
@@ -85,7 +85,7 @@ Person 类存在3个 BOOL 属性:
新方案采用:**结构体的位域能力**,限定单个成员变量所占用的内存。代码如下:
-
+
@@ -97,7 +97,7 @@ Person 类存在3个 BOOL 属性:
虽然上述方式都可以实现存储 Person 类3个属性的目的,但是还有第三种方案,参考 iOS 系统设计,采用 Union 实现。代码如下
-
+
分析:
@@ -186,7 +186,7 @@ union {
与一个不是3个数之一的数按位与,得到的结果为`0b0000 0000`。利用这个特性我们可以判断传递来的参数是不是包含了某个值
-
+
有了上面的铺垫,就可以更好的查看 runtime 中 isa 的定义了。
@@ -325,7 +325,7 @@ isa 在 arm64 之后必须通过 `ISA_MASK` 去查询 class(类对象、元类
`0x0000000ffffffff8ULL` 用程序员模式打开计算器
-
+
@@ -335,7 +335,7 @@ extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcl
知道结构体可以指定存储大小这个功能后,可以看到 `isa_t` 联合体与 `ISA_MASK` 按位与之后的地址,其实就是类真实的地址信息(可能是类对象、也有可能是元类对象)
-
+
@@ -483,7 +483,7 @@ struct class_ro_t {
具体关系整理如下图
-
+
@@ -495,7 +495,7 @@ struct class_ro_t {
-
+
比如访问 method 的过程
@@ -513,7 +513,7 @@ struct class_ro_t {
- `class_ro_t` 里面的 baseMethodList、baseProtocols、ivars、baseProperties 是一维数组,是只读的,包含了类的(原始信息)初始内容
-
+
- 当系统运行 Runtime 会把类自身的信息(class_ro_t) 中的信息和 Category 中的信息合并起来,放到 class_rw_t 中的信息去
@@ -1431,7 +1431,7 @@ v
可以对照下面的表格进行查看:
-
+
```objectivec
- (int)calcuate:(int)baseHeight heigith:(float)height;
@@ -2013,7 +2013,7 @@ bucket_t goodStudentSayBucket = buckets[(uintptr_t)@selector(personSay) & cache.
NSLog(@"%s %p", goodStudentSayBucket._key, goodStudentSayBucket._imp);
```
-
+
@@ -2087,7 +2087,7 @@ do {
-
+
```c++
Person *p = [[Person alloc] init];
@@ -2658,7 +2658,7 @@ if (imp) goto done;
上面的流程是整个 `objc_msgSend` 的消息发送阶段的整个流程。可以用下图表示
-
+
@@ -2807,7 +2807,7 @@ SEL_resolveClassMethod, sel);`
完整流程如下
-
+
@@ -2852,7 +2852,7 @@ Person *person = [[Person alloc] init];
知道 `objc_msgSend` 的流程,我们尝试给它修正下
-
+
方法1,增加一个兜底方法,然后利用 `class_addMethod` 动态增加方法实现
@@ -2886,7 +2886,7 @@ class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncod
方法3,也可以添加 c 语言方法
-
+
c 函数即函数地址,只不过传递给 class_addMethod 的时候需要转换为函数参数加 `(IMP)cfuntionResolver`,手动添加方法签名。
@@ -2999,7 +2999,7 @@ void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
为什么是 `__forwarding__` 方法。我们可以根据 Xcode 崩溃窥探一二
-
+
```c
int __forwarding__(void *frameStackPointer, int isStret) {
@@ -3043,7 +3043,7 @@ int __forwarding__(void *frameStackPointer, int isStret) {
完整流程如下
-
+
@@ -3077,13 +3077,13 @@ int __forwarding__(void *frameStackPointer, int isStret) {
Person 类不存在对象方法 makeliving ,PersonHelper 类存在。
-
+
调用对象不存在的方法,则会抛出错误。同时在 Runtime 动态消息解析阶段,`resolveInstanceMethod` 没有处理对象方法,所以会报错
方法1:因为动态消息解析没有处理,则会开始走消息转发阶段。消息转发首先会调用 `- (id)forwardingTargetForSelector:(SEL)aSelector` 方法。(如果是对象方法则调用 `- (id)forwardingTargetForSelector:(SEL)aSelector`,如果是类方法则调用 `+ (id)forwardingTargetForSelector:(SEL)aSelector`)
-
+
方法2:如果消息转发里,`forwardingTargetForSelector` 返回了 nil,则开始调用方法签名 `methodSignatureForSelector` 方法和 `forwardInvocation` 方法
@@ -3116,7 +3116,7 @@ Person 类不存在对象方法 makeliving ,PersonHelper 类存在。
1. `methodSignatureForSelector` 如果返回 nil,则 `forwardInvocation` 不会执行
2. 方法签名 `methodSignatureForSelector` 必须正确,否则会获取参数 crash,报错 `Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSInvocation getArgument:atIndex:]: index (2) out of bounds [-1, 1]'` 的错误
-
+
@@ -3124,7 +3124,7 @@ Person 类不存在对象方法 makeliving ,PersonHelper 类存在。
通过 `NSInvocation` 获取参数一般是从2开始,因为第一个是 self,第二个是 _cmd,第三个是 cost
-
+
@@ -3151,11 +3151,11 @@ Person 类不存在对象方法 makeliving ,PersonHelper 类存在。
}
```
-
+
-
+
注意:**实例对象有消息转发,类方法也有消息转发机制**。但是在 Xcode 中只可以提示 `-(id)forwardingTargetForSelector:(SEL)aSelector`
@@ -3184,7 +3184,7 @@ OC 中方法调用的本质就是利用 runtime 发消息,发消息也给 rece
3. 如果在当前类的方法列表中没有找对方法实现,则根据类对象的 superclass 在父类的类对象的方法列表中继续查找
-
+
先根据 superclass 找到父类,然后在父类中也按照上面3点进行递归查找,即:
@@ -3311,7 +3311,7 @@ objc_msgSendSuper(arg, sel_registerName("class"))
我们对 iOS 项目`[super viewDidLoad]` 下符号断点,发现`objc_msgSendSuper2`
-
+
查看 objc4 源代码发现是一段汇编实现。
@@ -3399,7 +3399,7 @@ call - 调用函数
也可以在 Xcode 上可视化面板操作,路径为:菜单栏 `Product -> Perform Action -> Assemble "ViewController.m"`
-
+
@@ -3425,7 +3425,7 @@ call - 调用函数
Demo1
-
+
@@ -3453,7 +3453,7 @@ Demo1
Demo2
-
+
下面面2个判断都是调用类方法的 `isMemberOfClass` 、`isKindOfClass`
@@ -3515,7 +3515,7 @@ QA:`NSLog(@"%d", [Person isKindOfClass:[NSObject class]]);` 为什么输出1
- 开启 for 循环,判断 tcls 是否等于方法参数(也就是 NSObject 的类对象)
- for 循环一开始不满足,则不断进行,令 tcls = tcls->superClass。for 循环结束的条件是 tcls 为 nil,所以最后找到 NSObject 的元类的时候,继续往上找,NSObject 元类对象的父类是 NSObject 的类对象,此时 tcls 就是 NSObject 的类对象,cls 也是 NSObject 类的类对象,相等,输出1.
-
+
同理 `[NSObject isKindOfClass:[NSObject class]]` 也为 YES,工作流程和上面的类似。也就是 `[继承自 NSObject 及其继承自任何子类 isKindOfClass:[NSObject class]]` 都为 YES
@@ -3523,7 +3523,7 @@ QA:`NSLog(@"%d", [Person isKindOfClass:[NSObject class]]);` 为什么输出1
-
+
@@ -3560,7 +3560,7 @@ QA:`NSLog(@"%d", [Person isKindOfClass:[NSObject class]]);` 为什么输出1
程序运行什么结果?
-
+
为什么会方法调用成功?为什么 name 打印出为 @"杭城小刘"。我们来分析下:
@@ -3594,7 +3594,7 @@ void test () {
方法内的变量存储在栈上,堆向上增长,栈向下增长。
-
+
第三,id 的本质是
@@ -3645,7 +3645,7 @@ struct Person_IMPL {
再看一个变体1
-
+
打印输出是因为 `*p` 类似 isa 指针。本身占用8字节空间,然后访问 `self->_name` 就是 `base + 8 = isa地址 + 8 ` 出的内存就是 name,`*p` 是在栈中,加8,就是向上声明的变量,当前情况下 `Address(*p) + 8` 就是 temp 变量。所以输出 ``
@@ -3653,7 +3653,7 @@ struct Person_IMPL {
再看一个变体2
-
+
分析: 搞懂的小伙伴不迷惑了。没搞懂其实就是没搞懂**栈地址由高到低,向下生长** 和 `super` 调用的本质。
@@ -3672,7 +3672,7 @@ objc_msgSendSuper(arg, sel_registerName("viewDidLoad"));
-
+
可能会疑问,知道了 super 调用本质,知道了会产生一个局部变量的结构体,但是结构体里面2个成员变量,找属性的时候,isa 下的8个字节会命中哪个?
@@ -3691,7 +3691,7 @@ struct objc_super {
-
+
@@ -3706,7 +3706,7 @@ id rs = [NSObject valueForKey:@"isa"];
NSLog(@"%@", rs);
```
-
+
不会 Crash。输出的是 NSObject 的元类对象。因为 NSObject 调用的是类方法,先去元类对象的方法列表中查找,发现没有 `valueForKey` 方法。则继续从 NSObject 元类对象的父类对象,也就是 NSObject 的类对象上查找 `valueForKey` 方法。
@@ -3716,7 +3716,7 @@ NSLog(@"%@", rs);
查看了 objc 源码,会发现很多 NSObject 的基础方法:`+ (id)init`、`- (id)init` 等均有 `+` 、`-` 方法。
-
+
为什么这么设计?猜测是为了代码的健壮。
@@ -3767,7 +3767,7 @@ Person *p = [Person new];
object_setClass(p, [Student class]);
```
-
+
@@ -3796,7 +3796,7 @@ void createClass (void) {
}
```
-
+
注意:
@@ -3906,7 +3906,7 @@ free(properties);
-
+
不够健壮体现在:
diff --git a/Chapter1 - iOS/1.89.md b/Chapter1 - iOS/1.89.md
index dbd4224..d29b761 100644
--- a/Chapter1 - iOS/1.89.md
+++ b/Chapter1 - iOS/1.89.md
@@ -28,7 +28,7 @@ block(1, 2);
用指令`xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转为 c++
-
+
`ViewController.m` 的 `viewDidLoad` 函数为 `_I_ViewController_viewDidLoad`
@@ -240,7 +240,7 @@ struct __ViewController__viewDidLoad_block_desc_0 {
}
```
-
+
@@ -254,7 +254,7 @@ struct __ViewController__viewDidLoad_block_desc_0 {
-
+
@@ -279,11 +279,11 @@ printBlock();
用 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++
-
+
概括如下:
-
+
@@ -301,7 +301,7 @@ printAgeBlock();
用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码
-
+
代码分析:
@@ -338,7 +338,7 @@ printInfoBlock();
用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码
-
+
@@ -379,7 +379,7 @@ age is 28, height is 176
用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码
-
+
@@ -460,7 +460,7 @@ block 截获变量可以分为:
用指令 `xcrun --sdk iphoneos clang -arch arm64 Person.m -rewrite-objc -o Person-arm64.cpp` 转换为 c++ 代码
-
+
@@ -582,13 +582,13 @@ static void __Person__testBlockCapture_block_func_0(struct __Person__testBlockCa
我们知道 block 可以看成是一个 oc 对象,所以它有类型,写个 Demo1 验证下
-
+
也就是说: `__NSGlobalBlock__` -> `NSBlock` -> `NSObject`。
继续验证,Demo2
-
+
同时利用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++
@@ -613,7 +613,7 @@ block 的类型可以通过 isa 或者 class 方法查看,最终都是继承
这3种 block 在内存中的排布如下图:
-
+
@@ -625,7 +625,7 @@ Demo:
由于 ARC 默认会做一些优化,为了彻底的研究 block,我们将工程的 ARC 关掉(Build Setting 里 Automatic Reference Counting 设置为 No)
-
+
分析:
@@ -643,7 +643,7 @@ Demo:
当 `__NSStackBlock__` 调用 `copy` 方法后会变为 `__NSMallocBlock__`。如下图:
-
+
@@ -699,7 +699,7 @@ int main(int argc, const char * argv[]) {
MRC 下 block 作为函数的返回值是比较危险的。在方法内部,也就是栈上定义的 block,函数调用结束后可能一些相关数据就释放了,存在潜在风险。
-
+
@@ -707,7 +707,7 @@ MRC 下 block 作为函数的返回值是比较危险的。在方法内部,也
MRC 下如果函数返回值是 block,且 block 内做了 auto 变量捕获的逻辑,编译器会报错:`Returning block that lives on the local stack`。此时 block 应该为 `__NSStackBlock__`
-
+
即使编译器不报错,也存在很大风险,因为 block 创建在栈上,函数返回后栈内存可能被回收,导致后续访问野指针。
@@ -738,7 +738,7 @@ HelloBlock generateBlock(void) {
另外的改法是,在 Build Setting 中改为 ARC,看看
-
+
也就是说,ARC 模式下,当 block 捕获了 auto 变量,并且作为函数返回值的时候,ARC 会自动调用 copy 方法,将 `__NSStackBlock__` 变为 `__NSMallocBlock__`
@@ -819,11 +819,11 @@ ARC 下:如果 block 调用 copy 方法,则 block 仍旧为 `__NSMallocBloc
MRC 下,栈内捕获了 auto 变量的 block 为 `__NSStackBlock__`
-
+
改为 ARC
-
+
@@ -862,31 +862,31 @@ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), di
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`。
-
+
@@ -930,13 +930,13 @@ static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_objec
Demo1:
-
+
说明:ARC 环境下,GCD 的 block 会自动被拷贝到堆上 `__NSMallocBlock__`,堆上的 block 会对使用的对象进行 copy,所以 person 引用计数+1,则在 GCD block 执行完毕后才 release
Demo2
-
+
- 栈空间上的 block 是不会对对象进行保命的(不管是 ARC 还是 MRC,都不调用 retain、copy 方法)。
- ARC 下, block 如果访问了 auto, static 变量,则属于 `__NSStackBlock__`,ARC 下用强指针指向,则会变为 `__NSMallocBlock__ 堆上的 block, 是会对对象进行保命的。GCD 的 block 会自动拷贝到堆上,属于 `\__NSMallocBlock__`,也会对对象进 行 copy 保命。
@@ -950,13 +950,13 @@ Demo2
Demo3
-
+
因为 GCD 是给 MainQueue 添加任务的,所以是串行,2个任务前后按照3s、1s 后打印。由于最晚的一个任务是访问强指针对象,所以不会释放。等到 GCD 全部执行完后,Person 才释放。
Demo4
-
+
@@ -1130,7 +1130,7 @@ test 和 test2 的本质区别
Demo3
-
+
为什么上面的代码会 crash?
@@ -1195,7 +1195,7 @@ Demo3
Demo4
-
+
分析:
@@ -1391,7 +1391,7 @@ MyBlock block = ^{
转为 C++
-
+
```c++
struct __Block_byref_age_0 {
@@ -1435,7 +1435,7 @@ block 内部的函数在修改 age 的时候其实就是通过 `__main_block_imp
-
+
@@ -1447,7 +1447,7 @@ QA:为什么`__block` 变量的 `__Block_byref_age_0` 结构体并不在 block
看个有趣的例子,验证下 __block 的效果
-
+
转换成 c++ 可以看到
@@ -1492,7 +1492,7 @@ __attribute__((__blocks__(byref))) __Block_byref_num2_0 num2 = {(void*)0,(__Bloc
对` __block` 修饰的对象,clang 转换为 c++ 后如下:
-
+
@@ -1505,7 +1505,7 @@ __attribute__((__blocks__(byref))) __Block_byref_num2_0 num2 = {(void*)0,(__Bloc
注意:
-
+
@@ -1575,7 +1575,7 @@ int main(int argc, const char * argv[]) {
我们将断点设置到 NSLog 这里,打印出自定义结构体 `__main_block_impl_0` 中的 age 。
-
+
```c
// 0x0000000105231f70
@@ -1750,7 +1750,7 @@ in block: age = 28, address is 0x600000464938
-
+
分析:
@@ -1804,7 +1804,7 @@ in block: age = 28, address is 0x600000464938
通过 Demo1 和 Demo2 总结下:` __forwarding` 的作用是什么?为什么这么设计
-
+
@@ -1903,7 +1903,7 @@ Tips:实现 block 拷贝及其捕获对象的函数是 `_Block_copy`,工作
Demo0
-
+
可以看到:
@@ -1912,17 +1912,17 @@ Demo0
Demo1
-
+
-
+
Demo2
-
+
-
+
@@ -2121,7 +2121,7 @@ __attribute__((__blocks__(byref))) __Block_byref_p_0 p = {
看个 Demo
-
+
可以看到 block 放在堆上的时候(被抢指针指向、作为返回值)的时候,如果 block 内部访问了强指针指向的对象,则会发生循环引用。
@@ -2164,7 +2164,7 @@ Person *p = [[Person alloc] init];
-
+
@@ -2186,7 +2186,7 @@ p.block();
`__unsafe_retained` 因为不安全所以不推荐,`__block` 因为使用繁琐,且必须等到调用 block 才会释放内存,所以不推荐。ARC 下最佳用 `__weak`
-
+
diff --git a/Chapter1 - iOS/1.91.md b/Chapter1 - iOS/1.91.md
index 2d9a590..5683ce7 100644
--- a/Chapter1 - iOS/1.91.md
+++ b/Chapter1 - iOS/1.91.md
@@ -44,11 +44,11 @@
要增加新的参数,选中对应的 Configuration,双击后在弹出的面板后,按照 **DEBUG=1** 的格式添加
-
+
- 对于 Swift 则有不同的编译器。在 Build Settings 中的 **Other Swift Flags** 里选择对应的 Configuration,按照 **-DDEV** 的格式添加。**-D** 是必须要存在的,后面要加的 Flag,就紧跟在后面
-
+
@@ -67,7 +67,7 @@
1. 多 Scheme 可以选中 PROJECT,然后点击左下方的 **+**,点击「Duplicate "Debug" Configuration」,增加一列后,重命名为 Beta
-
+
2. 选择 TARGETS,点击左上角的 **+** 号,在弹出面板中选择 "Add User-Defined Setting"。然后区分不同的 Configuration,设置不同的值
@@ -79,11 +79,11 @@
6. 分别选择不同的 Scheme,点击 Edit Scheme。**让不同的 Scheme 对应不同的 Configuration**
-
+
7. 选择 Debug Scheme,Debug Scheme 的名称就是 `LDExploreDemo`,点击 Run 验证
-
+
@@ -114,7 +114,7 @@
3. Edit Scheme。让不同的 Scheme 选择对应的 Build Configuration
-
+
4. 创建不同的 `.xcconfig` 文件,命名格式为:`Pods-{ProjectName}.${ConfigurationName}.xcconfig`。比如:Config-LDExploreDemo.Debug.xcconfig、Config-LDExploreDemo.Beta.xcconfig、Config-LDExploreDemo.Release.xcconfig。并编辑里面的内容
@@ -126,11 +126,11 @@
效果如下:
-
+
同样 Cocoapods 管理的工程,也会根据 Configuration 生成不同的 xcconfig 文件,我们可以修改或者创新新的 xcconfig 文件,但引入使用。
-
+
#### 2. xcconfig 编写规范
@@ -214,7 +214,7 @@ xcconfig (Xcode Configuration Settings File) 文件是用于管理 Xcode 项目
设置条件后,如果在不符合的条件下,编译会报错
-
+
5. 值引用和继承
@@ -310,7 +310,7 @@ xcconfig (Xcode Configuration Settings File) 文件是用于管理 Xcode 项目
#### 3. 编译链接日志输出重定向
-
+
终端输入 `tty` 敲回车,然后在 Xcode 脚本中,就可以将 echo 输出的结果重定向到终端 `echo "message" > /dev/ttys000`
@@ -322,9 +322,9 @@ Xcconfig 中定义的变量在 Run Script 中是可以访问的到。
Demo 验证如下:
-
+
-所以编写了一个脚本,进行输出重定向 [xcode_run_cmd](./../assets/xcode_run_cmd.sh)
+所以编写了一个脚本,进行输出重定向 [xcode_run_cmd](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/xcode_run_cmd.sh)
使用的时候,需要搭配 `.xcconfig` 文件,需要设置3个参数:
@@ -332,7 +332,7 @@ Demo 验证如下:
- CMD_FLAG :运行到命令参数
- TTY:需要打开终端,查看终端编号配置进去。
-
+
@@ -344,7 +344,7 @@ Demo 验证如下:
步骤:Edit Scheme - Arguments - 添加2个参数:-pa 和 需要脱符号的路径
-
+
关于 LLVM 工程如何编译、运行的具体步骤,可以查看 [LLVM](./1.102.md)
@@ -404,19 +404,19 @@ D --> E[最终生效值]
- `Base.xcconfig`:只链接一个 UI 基础库 WantUIKit
- `Dev.xcconfig`:引入 `Base.xcconfig` 文件,同时在此基础上,引入:SDWebImage、AFNetworking、PrismClient 3个库
-
+
3. 配置工程的 PROJECT 对应的 Configuration。让 Debug 模式,选择 `Dev.xcconfig`
编译后,查看如下:
-
+
所以可以看到:Xcode 的配置是遵循层级关系和继承的。
当配置完 Xcconfig 和 Xcode GUI 面板上操作完后,可以用 **xcodebuild -showBuildSettings -configuration Debug** 查看最终的结果是否符合预期
-
+
@@ -737,13 +737,13 @@ clang -target x86_64-apple-macos13.1 \
第二步,已经得到了 main.o 目标文件,也就是二进制文件,利用指令 `objdump -r main.o` 查看目标文件中的内容
-
+
可以看到 main 函数中,callq 就是调用 NSLog 函数。后面的地址写为了 0,这里的0会在后面链接的过程中被修正。
第三步,另外为了能让链接器能够定位到这些需要被修正的地址,在代码块中可以看到一个重定位表。指令为 `objdump -r main.o`
-
+
NSLog 位于偏移量为19的位置,
@@ -790,7 +790,7 @@ clang -target x86_64-apple-macos13.1 \
使用 `nm -pa .o文件路径` 命令来查看符号
-
+
#### 2. 符号的大分类
@@ -889,11 +889,11 @@ void undefinedFunc(); // 声明未实现的函数
**Weak definition Symbol**:表示此符号为弱定义符号。如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则该弱定义将被忽略。只能将合并部分中的符号标记为弱定义
-
+
当把其中一个符号通过 **\__attribute__((weak))** 改为 weak symbol 的时候,再次编译,发现没有问题。
-
+
- 当用 **\__attribute__((weak, visibility("hidden")))** 把弱定义的全局符号设置为隐藏的时候,本来是全局符号的弱定义符号,就会变成局部弱定义符号
@@ -924,11 +924,11 @@ void undefinedFunc(); // 声明未实现的函数
实验不方便模拟动态库和 App 的情况。就看同一个 Mach-O 中,只有函数声明,没有实现的情况
-
+
但告诉编译器该符号是弱引用符号,就可以编译链接成功
-
+
@@ -946,14 +946,14 @@ void undefinedFunc(); // 声明未实现的函数
看个 Demo:OC 对象默认就是全局符号。
-
+
链接器也提供了能力,将导出符号变为不导出的符号:**-Xlinker -unexported_symbol -Xlinker _OBJC_CLASS_$_Person**
-
+
如果有一批符号需要隐藏,链接器提供了更方便的参数: **-Xlinker -unexported_symbols_list -Xlinker ${PROJECT_DIR}/LDExploreDemo/hidden_symbols.list**
-
+
@@ -1067,13 +1067,13 @@ Tips: 后续有很多终端使用指令的场景,为了查找方便高效,
**man 指令**,比如 `man nm`:
-
+
进入 vim 模式了,看到左下角有 `:` 光标,如果想查看当前 nm 命令的参数,可以快速查找,输入 `/ + 具体参数`,敲回车即可跳转到要匹配到的位置,如果有多个结果,且当前自动跳转到的不是正确的位置,vim 模式下可以输入 `n` 跳转到下一个匹配到的位置(n 即 next),输入 `N` 则跳转到上一个匹配到的位置。
比如查找 `-p`,则输入 `/-p`,敲回车的效果如下
-
+
@@ -1195,7 +1195,7 @@ clang 编译、链接的参数解释如下:
第一步,编写 oc 代码,就一个 Person 类,写一个类方法,编译为静态库。`Person.m` 编译为 `Person.o`
-
+
第二步,将 `Person.o` 重命名为 `Person.dylib`
@@ -1203,7 +1203,7 @@ clang 编译、链接的参数解释如下:
利用 `objdump --macho --private-header Person.dylib` 查看静态库依旧是 `Object File`
-
+
第三步,编写代码 `main.m` 代码,导入静态库 ``
@@ -1249,7 +1249,7 @@ clang -target x86_64-apple-macos13.1 \
- 成功,则说明 静态库就是`.o` 文件的集合,单个 `main.o` 文件,修改拓展名就可以变为静态库
- 不成功,则相反
-
+
@@ -1272,7 +1272,7 @@ clang -target x86_64-apple-macos13.1 \
3. 再利用 clang 指令将 .o 目标文件,链接为静态库
-
+
4. 利用 libtool 合并2个静态库
@@ -1284,7 +1284,7 @@ clang -target x86_64-apple-macos13.1 \
5. 用 **ar -t libPersonCat** 指令,验证合并后的静态库所包含的 .o 目标文件
-
+
### 4. Auto-Link
@@ -1314,7 +1314,7 @@ clang -target x86_64-apple-macos13.1 \
##### 1. Demo1
一个 cocoapods 组织的工程,以 pod 的形式引入了 AFNetworking 库。再将另一个 `libAFNetworking.a` 以静态库的形式引入了 AFNetworking。
-
+
现象:工程编译成功。编译链接后,运行输出打印信息。
@@ -1333,8 +1333,8 @@ clang -target x86_64-apple-macos13.1 \
一个 cocoapods 组织的工程,以 pod 的形式引入了 AFNetworking 库,再将另一个 `libAFNetworking.a` 静态库改名为 `libAFNetworkingCopy.a`,然后引入 Xcode 工程。
-
-
+
+
现象:工程链接失败。报错:`Issues223 duplicate symbols for architecture x86_64`
问题:为什么上次的链接成功,这次却链接失败了,报符号重复的错误?
@@ -1350,7 +1350,7 @@ clang -target x86_64-apple-macos13.1 \
- 新创建的静态库就1个 `AFNetworkingMock` 文件。不过类里面还存在一个类 `AFURLSessionManager` 和一个全局函数 `global_function`
现象:工程链接失败。报错:`error: duplicate interface definition for class 'AFURLSessionManager'`
-
+
问题:为什么上次的链接成功,这次却链接失败了,报符号重复的错误?
分析:
@@ -1365,7 +1365,7 @@ clang -target x86_64-apple-macos13.1 \
##### 1. 收集需要重命名的符号
通过 **objdump --macho -t ${MACH_PATH}** 就可以输出符号信息。Demo 中需要重命名的符号如下:
-
+
@@ -1376,24 +1376,24 @@ clang -target x86_64-apple-macos13.1 \
关于 LLVM 如何编译运行,或者开发 pass 可以查看 [LLVM 编译运行](./1.102.md#编写-xcode-插件)
打开 LLVM 工程,Scheme 切换为 `llvm-objcopy` 然后编译运行,从 Products 目录下将产物拷贝到个人电脑的 CustomTools 文件夹,后续就可以在终端访问 llvm-objcopy 指令。(因为在 `.zshrc` 文件里配置过 `export PATH=~/CustomTools:$PATH`)
-
+
这样就说明已经成功了
-
+
然后改造 Demo 工程静态库的脚本为 **llvm-objcopy --prefix-symbols=FantasticLBP_ ${MACH_PATH}** 去给冲突的符号加前缀 `FantasticLBP_`(当然这个前缀名字可以是 SDK 名称简写、业务线简写,我这里是 Github ID ,验证问题而已)。
结果:但发现运行报错,提示 `llvm-objcopy: command not found`
-
+
改进:将 `llvm-objcopy` 移动到源码根目录下,现在 `Command not found` 的问题解决了,但是报错: **error: option is not supported for MachO**
-
+
说明 `--prefix-symbols` 并不支持 MachO 格式。而 `--redefine-sym` 符合要求。
打开 llvm 源码进行调试,配置启动参数。
-
+
发现修改成功
-
+
上面演示了单个修改符号名称的过程。llvm-objcopy 可以批量修改。
- 指令改为 `--redefine-syms`。
@@ -1404,14 +1404,14 @@ clang -target x86_64-apple-macos13.1 \
_OBJC_IVAR_$_AFURLSessionManager._session FantasticLBP__OBJC_IVAR_$_AFURLSessionManager._session
```
修改后运行,发现批量符号修改成功。
-
+
##### 3. 使用工具修改符号名称
利用 llvm-objcopy 的能力,修改重名的符号后测试下。
-
+
可以看到:
@@ -1501,7 +1501,7 @@ target 'StaticLibConflictsDemo' do
end
```
效果:
-
+
可以看到不光是解决了符号重复的问题,也解决了同一个类,存在2处实现的问题。
@@ -1538,11 +1538,11 @@ end
静态库的 xcconfig 为
-
+
动态库的 xcconfig 为
-
+
###### 2. 源码级解决方案:添加类前缀
@@ -1571,7 +1571,7 @@ objc_library(
静态库/ .o 文件 Strip 流程: 处理 `_DWARF` 段
-
+
Strip 的过程,就是在修改 Mach-O 文件中的内容。
@@ -1579,11 +1579,11 @@ Strip 的过程,就是在修改 Mach-O 文件中的内容。
动态库
-
+
All Symbols
-
+
@@ -1591,7 +1591,7 @@ Non-Global Symbols(非全局符号):
-
+
@@ -1682,7 +1682,7 @@ test.o -o test
第五步:成功得到了 test 可执行文件,说明模仿 Framwork 搭建的静态库 Framwork 成功了。然后测试下可执行文件运行结果。进行 double check
-
+
@@ -1696,7 +1696,7 @@ test.o -o test
Mach header 包括一些基础信息,所以 Mach header 存在冗余(大小端序、CPU 类型等),这也就是为什么静态库比动态库体积大的原因之一。
-
+
```shell
Unix_Kernel ~/Desktop/OCExplore/OCExplore file Person.o
@@ -1712,7 +1712,7 @@ Mach header
动态库在 Mach header 这里有改进,AFNetworking 动态库格式如下:
-
+
将公共的信息放到一起,公用一个 Mach header。
@@ -1814,7 +1814,7 @@ exports:
第一步:创建 dylib 文件夹,下面创建 `Person.h` `Person.m` 类。在 dylib 同层目录创建 main.m 文件。代码如下
-
+
第二步:对 main.m 编译成 main.o 文件,指令为
@@ -1900,13 +1900,13 @@ echo "---------------- Done --------------"
结果如下:
-
+
第六步:对生成的 main 可执行文件进行调试运行,使用 lldb 指令 `lldb -file 可执行文件`,然后输入 r 进行运行:
-
+
咦,为什么我用动态库链接后还是无法使用???带着问题研究下
@@ -1983,7 +1983,7 @@ echo "---------------- Done --------------"
执行完脚本,又出现了奇怪的现象:
-
+
@@ -2009,7 +2009,7 @@ ld -dylib -arch x86_64 \
再次运行 build 脚本,然后对可执行文件执行,还是报错 😂
-
+
@@ -2028,17 +2028,17 @@ ld -dylib -arch x86_64 \
不得不聊聊动态库加载原理
-
+
也就是说:**当 dyld 加载一个可执行文件(main) 的时候,在 Mach-O 中有一些名字叫 `LC_LOAD_DYLIB` 的 Load Command,里面保存了所使用到的动态库的路径。可执行文件所依赖的动态库,就是靠 dyld 根据动态库路径进行加载的**。
用 MachOView 打开另一个 App **看看**
-
+
对于我们自己链接的可执行文件 main 进行查看,利用 `otool -l main | grep 'DYLIB' -A 5` 指令
-
+
可以发现:
@@ -2055,17 +2055,17 @@ ld -dylib -arch x86_64 \
#### 方式一:通过 `install_name_tool` 指令
-
+
通过改变动态库 name 来修改动态库的路径。具体指令为: `install_name_tool -id 动态库路径 动态库名称`。即:`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 后,再重新链接生成可执行文件。可执行文件可以正确运行,查看所以来的动态库路径,均正确加载。
-
+
方式一有明显**缺点,因为路径是绝对路径,因此没办法迁移**。
@@ -2176,7 +2176,7 @@ install_name_tool -add_rpath /Users/unix_kernel/Desktop/LDAndFramework/StaticLib
下面是上面全部步骤的截图说明。
-
+
@@ -2205,7 +2205,7 @@ install_name_tool -add_rpath /Users/unix_kernel/Desktop/LDAndFramework/StaticLib
install_name_tool -rpath /Users/unix_kernel/Desktop/LDAndFramework/StaticLibraryLinkedIntoDynamicLibrary @executable_path main
```
-
+
这个可不是花里胡哨的烧操作,Cocoapods 也是这么干的
@@ -2213,7 +2213,7 @@ install_name_tool -rpath /Users/unix_kernel/Desktop/LDAndFramework/StaticLibrary
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
```
-
+
@@ -2229,7 +2229,7 @@ LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_pa
这样一个场景,代码模拟下,文件目录如下
-
+
1. 在 Cat.framework 文件夹下运行 build.sh
2. 在 Person.framework 文件夹下运行 build.sh
@@ -2351,7 +2351,7 @@ echo "---------------- Done --------------"
运行报错如下:
-
+
为什么还错误了?都已经给可执行文件添加了 `@executable_path`,给2个动态库都添加了 `@rpath`,怎么办?
@@ -2393,7 +2393,7 @@ otool -l Person | grep 'ID' -A 5
在 Person.framework运行下 build.sh,然后在根目录下运行 build.sh,得到新的可执行文件,然后可以成功运行
-
+
反思:可执行文件依赖动态库 A,动态库 A 依赖动态库 B,上面的配置很繁琐:
@@ -2409,13 +2409,13 @@ otool -l Person | grep 'ID' -A 5
注:为了方便看清楚脚本执行情况和可执行文件执行结果,这次运行注释了 otool 的打印脚本。
-
+
`loader_path` 是标准解决方案。随便打开 AFNetworking 工程看看
-
+
@@ -2448,11 +2448,11 @@ sudo codesign --force --deep --sign - (应用路径)
发现 Person 没有导出 Cat 的符号。那在可执行文件中调用不了 Cat 的能力了。
-
+
怎么办呢?链接器 LD 已经是很成熟的东西了,对于处理动态库依赖了动态库,且需要将被依赖动态库的符号导出,这样的需求早已满足了。具体是什么参数?终端输入 `man ld` 查看下指令
-
+
其中,我们需要用的是 **-reexport_framework** 这个参数。指令为 ` -Xlinker -reexport_framework -Xlinker Cat`
@@ -2493,11 +2493,11 @@ 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 的符号。
@@ -2552,7 +2552,7 @@ otool -l Person | grep 'ID' -A 5
修改完从里到外一次性执行 build.sh,得到 main 可执行文件。一切顺利,输出如下:
-
+
### 6. 二级命名空间
@@ -2758,17 +2758,17 @@ xcodebuild -workspace Person.xcworkspace \
打包成功的输出如下:
-
+
实体目录如下:
-
+
注意:我们打包归档后生成的符号表只有 `.dSYM` 文件,没有 `BCSymbolMaps` 文件,是因为工程没有开启 bitcode,当开启 bitcode 后的,打包会生成 `BCSymbolMaps` 文件,作用也是用于 Crash 后解析堆栈用的。
因为 `.dSYM` 文件是默认生成的,但是 `bitcode` 需要分别开启(Pod 和 Demo 工程)。开启 bitcode 后重新打包,看看生成的产物里有没有 `BCSymbolMaps` 和相关的 `.bcsymbolmap` 文件
-
+
@@ -2783,7 +2783,7 @@ xcodebuild -create-xcframework \
结果如下:
-
+
可以看到打包后的 xcframework 自动处理了文件夹。但是缺点是我们提供的 framework,别人使用可能会 crash,所以为了一些使用场景需要在 xcframework 中提供 `.dSYM` 文件或者 `.bcsymbolmap` 文件。
@@ -2802,7 +2802,7 @@ xcodebuild -create-xcframework \
结果如下
-
+
可以看到 xcframework 里面不只有不同的动态库,还携带了对应的 `.dSYM` 和 `bcsymbolmap` 文件,用于堆栈、符号还原。
@@ -2816,11 +2816,11 @@ xcodebuild -create-xcframework \
- import 头文件并使用
-
+
- 编译运行,查看 products 下面的产物,因为选择的是模拟器运行,所以验证 `Person.framework` 里面的动态库文件大小,是否和 `Person.xcframework` 里面模拟器目录下 Person 动态库的大小一致
-
+
可以看到我们打包的 `Person.xcframework` 可以正常使用,除此之外,`Person.xcframework` 包含了模拟器和真机的动态库文件和对应的 `.dSYM` 和 `.bcsymbolmap` 文件,当导入到项目中的时候,Xcode 会根据当前编译的架构,自动从里面选择合适的架构文件。
@@ -2842,13 +2842,13 @@ xcodebuild -create-xcframework \
第三步:编译运行。
-
+
结论:编译正常,但是运行会报错 `Library not loaded: @rpath/Person.framework/Person`
第一种解决方案是给 xcconfig 添加 rpath 的具体路径。
-
+
**第二种解决方案是将库声明为“弱链接”**。输入 `man ld` 查看具体的参数和说明:
@@ -2895,7 +2895,7 @@ OTHER_LDFLAGS = $(inherited) -Xlinker -weak_framework -Xlinker "Person"
我们对添加了 `_weak-framework` 这个链接器参数的可执行文件查看下,指令为 `otool -l WeakImportDemo`
-
+
查看 Mach-O 发现,**被 `-weak-framework` 声明后,cmd 从 `LC_LOAD_DYLIB` 变为 `LC_LOAD_WEAK_DYLIB`**
@@ -2934,7 +2934,7 @@ OTHER_LDFLAGS = $(inherited) -l"AFNetworking" -l"AFNetworking2"
第四步:尝试编译,编译成功。
-
+
QA:**为什么链接2个同名同符号的静态库,编译不报错?**
@@ -2944,7 +2944,7 @@ QA:**为什么链接2个同名同符号的静态库,编译不报错?**
**可以验证下,在链接的时候修改链接参数为 `-all_load` 就会报错**
-
+
@@ -2970,7 +2970,7 @@ OTHER_LDFLAGS = $(inherited)
-Xlinker "${SRCROOT}/AFNetworking2/libAFNetworking2.a"
```
-
+
具体代码见: `LDAndFramework/StaticLibConflictsSolution/StaticLibConflictsDemo`
@@ -2991,7 +2991,7 @@ OTHER_LDFLAGS = $(inherited)
发现编译通过,运行报错。
-
+
为什么?原因
@@ -3001,7 +3001,7 @@ OTHER_LDFLAGS = $(inherited)
使用的 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` 是在二进制文件生成后对其进行修改)
@@ -3015,19 +3015,19 @@ dyld 在运行起来后,会根据 `LD_RUNPATH_SEARCH_PATHS` 提供的 rpath
方式一:low 一点,直接给测试工程也 `pod AFNetworking`。这是在探究原理,简单解决问题可以这么做。
-
+
方式二:不是找不到 AFNetworking 吗?因为 xcconfig 提供的路径找不到,那直接给 `LD_RUNPATH_SEARCH_PATHS` 配置一个可以找到的地方。然后运行成功。这只是为了研究定位问题后,简单解决问题的方案。
-
+
方式三:观察标准做法,比如一个 App 使用了 AFNetworking,这个 case Cocoapods 是怎么处理的?
-
+
是通过 shell 脚本来处理的。该脚本肯定是 Cocoapods 生成的。属于工程化范畴。核心代码如下
-
+
具体怎么做呢?编写 shell 脚本,编译 Person 动态库、AFNetworking 动态库,然后将产物复制到 Frameworks 文件夹下。
@@ -3049,7 +3049,7 @@ Tips
第三步:framework 中增加实现代码,使用 App 中的符号
-
+
@@ -3086,7 +3086,7 @@ graph LR
OTHER_LDFLAGS = $(inherited) -framework "AFNetworking" -Xlinker -U -Xlinker _OBJC_CLASS_$_NetworkObject
```
-
+
### 2. 动静
@@ -3098,11 +3098,11 @@ OTHER_LDFLAGS = $(inherited) -framework "AFNetworking" -Xlinker -U -Xlinker _OBJ
打开 NetworkManager 动态库的 Mach-O 查看下符号,可以看到动态库 NetworkManager 中已经包含了静态库 AFNetworking 的符号。
-
+
如何成功编译?告诉链接器 HEADER_SEARCH_PATH 信息即可。
-
+
@@ -3114,7 +3114,7 @@ LD 链接器提供了能力,将静态库的符号不暴露出来。指令为
OTHER_LDFLAGS = $(inherited) -ObjC -Xlinker -hidden-l"AFNetworking"
```
-
+
### 3. 静静
@@ -3125,7 +3125,7 @@ OTHER_LDFLAGS = $(inherited) -ObjC -Xlinker -hidden-l"AFNetworking"
之后编译报错
-
+
静态库链接的本质是:链接器只提取被直接引用的目标文件。此时 NetworkManager 静态库中只有自己的符号,没有所依赖的 AFNetworking 中的符号。
@@ -3137,7 +3137,7 @@ App 链接 NetworkManager 静态库,需要告诉链接器:头文件、库名
-
+
方法二:直接给 App install 静态库。Cocoapods 处理这些依赖关系。
@@ -3154,7 +3154,7 @@ App 链接 NetworkManager 静态库,需要告诉链接器:头文件、库名
编译报错,符号未定义 。
-
+
App 调用静态库,静态库中被 App 引用的符号最终会被链接到 App 里。所以问题演变为:App 如何使用动态库里面的符号?
@@ -3167,7 +3167,7 @@ App 没有配置关于 AFNetworking 的链接三要素,所以找不到 AFNetwo
修改后编译没问题,运行报错 `image not found`,因为编译后的 AFNetworking 没有拷贝到 App 所需目录下。(通过配置,链接器会在 `build/Debug-iphoneos/AFNetworking` 的位置查找 AFNetworking,目前找不到)
-
+
怎么处理?
@@ -3179,7 +3179,7 @@ App 没有配置关于 AFNetworking 的链接三要素,所以找不到 AFNetwo
运行成功。同时查看测试过程的 Product 里 framework 目录下确实存在了 AFNetworking
-
+
具体代码见:`LDAndFramework/StaticLibUseDynamicLib`
@@ -3326,7 +3326,7 @@ Module 生成的 `.pcm`(Precompiled Module)文件是 Clang 对 C++ Module
假设没有开启 module 能力,当使用 `#include "*.h"` 的时候, 头文件 A、B 在 `use.c ` `another-use.c` 中,include 几次就要编译几次。
-
+
当编译 `use.c` 的时候,就要编译 include 进来的 `A.h`、`B.h`。编译 `another-use.c` 同样要再编译一次 `A.h`、`B.h`。被 include 了几次,就要编译几次,重复编译,效率低下。
@@ -3336,7 +3336,7 @@ Module 生成的 `.pcm`(Precompiled Module)文件是 Clang 对 C++ Module
- `import`:与 `include` 相对应,`import ` 语句用于导入模块,而不是简单的文本包含。使用模块可以减少编译时间,因为编译器只需要编译模块的接口而不是整个模块的实现
-
+
**`clang -fmodules -fmodule-map-file=mo dule.modulemap -fmodules-cache-path=./moduleCache -c use.c -o use.o` **:
@@ -3571,7 +3571,7 @@ framework module AsyncDisplayKit { // 定义了一个名为 AsyncDisplayKit 的
第三步:给主工程、动态库设置在同一个 Xcode 项目中编译调试。
-
+
@@ -3638,11 +3638,11 @@ framework module AsyncDisplayKit { // 定义了一个名为 AsyncDisplayKit 的
利用 modulemap 解决。
-
+
但是,上面的做法有副作用。虽然 Framework 内部 Swift 可以访问 OC 了,但是使用 Framework 的地方,也可以访问到 OCStudent 类。如何解决该问题?
-
+
@@ -3650,7 +3650,7 @@ framework module AsyncDisplayKit { // 定义了一个名为 AsyncDisplayKit 的
module 提供 private modle 的能力。
-
+
说明:
@@ -3694,11 +3694,11 @@ Swift 库引入了一个全新的文件 `.swiftmodule`,其包含序列化过
编写脚本: `cp -Rv -- "${BUILT_PRODUCTS_DIR}/" "${SOURCE_ROOT}/../Products"`,把产物拷贝到 products 目录下
-
+
第二步,打算用 libtool 指令 `libtool -static FLSwiftLibA.framework/FLSwiftLibA FLSwiftLibB.framework/FLSwiftLibB -o libFLSwiftC.a` 进行合并,发现报错
-
+
因为存在同名符号,但是又不存在2级命名空间,所以如何处理符号问题??
@@ -3741,7 +3741,7 @@ Swift 库引入了一个全新的文件 `.swiftmodule`,其包含序列化过
**为什么 App 工程中的 OC 类中导入头文件编译成功,但还是无法访问里面的类?**
-
+
同样,需要一个新的参数,告诉 LD modulemap 的信息,然后系统根据 module.modulemap 去关联查找 `FLSwiftLibA.swiftmodule` 里面的 `x86_64-apple-ios-simulator.swiftmodule` swiftmodule 文件信息。
@@ -3755,13 +3755,13 @@ Swift 库引入了一个全新的文件 `.swiftmodule`,其包含序列化过
而 Swift 编译器是 swiftc,需要额外配置。`SWIFT_INCLUDE_PATHS = "${SRCROOT}/FLStaticLibC/Public/FLSwiftLibA.framework/Modules/" "${SRCROOT}/FLStaticLibC/Public/FLSwiftLibB.framework/Modules/"`
-
+
但是配置后还是报错,因为看上去文件路径是对的,Framework 生成的时候,就是这么划分文件夹的。但实际编译的时候 Headers和 `module.modulemap` 、`.swiftmodule` 文件夹在同一层目录。所以我们手动编译静态库之后,需要处理静态库产物的文件目录信息。
-
+
#### 3. 为什么 Swift 静态库在链接的时候需要配置2个信息?
@@ -3999,7 +3999,7 @@ QA:Xcode 自己会生成 hmap 文件,为什么我们还需要自己生成?
创建一个 iOS App,默认模版的基础上添加一个 Person 类,一个继承自 Person 的 Student 类。然后编译
-
+
@@ -4011,7 +4011,7 @@ QA:Xcode 自己会生成 hmap 文件,为什么我们还需要自己生成?
`hmap` 文件不可读,必须用对应的工具解析,按照指定的格式解析。因为属于编译器的 scope,所以查看 LLVM 源码窥探下。
-
+
可以看到 hmap 结构类似 Mach-O ,顶部 HMapHeader 告诉系统,当前有几个 Bucket,下面是 Bucket 信息。然后最下方是 string 区域。
@@ -4110,9 +4110,9 @@ sizeOf(HMapHeader)
这里我把 [github Textture](https://github.com/texturegroup/texture/) 编译日志中的 hmap 拖到项目的根目录下了
-
+
-
+
#### 2. 变成终端可使用的能力
@@ -4126,7 +4126,7 @@ QA:如何做到该工具做到像系统自带的 `objdump` 一样,在终端
效果如下:
-
+
@@ -4165,7 +4165,7 @@ Xcode 会主动生成 `.hmap` 文件,那为什么还需要研究生成 `hmap`
然后查看编译日志中的 ` HMapStaticLibApp-project-headers.hmap` 文件。利用上面制作的 `HMapDump` 工具。
-
+
分析:在 App 使用 Static Library 的情况下,假设开启了 `Use Header Map`,静态库中所有头文件类型为 `Project`(只有 Project、Private、Public 3种类型,public 就是字面意思的公开,private 则代表 In Progress, project 才是通常意义上的 Private 含义)的情况,最终生成的 `.hmap` 文件中只会包含类似 `#import "Student.h"` 的键值引用。也就是说使用的地方,只有 `#import "Student.h"` 的这种方式才会走 hmap 策略,否则还是走 `Header Search Path` 来寻找头文件路径。
@@ -4213,7 +4213,7 @@ LLVM 真是好东西,把 Xcode 编译、链接等一些幕后的事情变成
整体目录结构和设置如下:
-
+
@@ -4227,15 +4227,15 @@ LLVM 真是好东西,把 Xcode 编译、链接等一些幕后的事情变成
注意:因为我的 Demo 都是在一个 Xcode 工程,不同的 **TARGETS**,所以即使打包好的静态库产物,复制到 App 工程 **StaticLib** 文件夹下,系统在编译的时候还是会以源码的形式编译。
会看到以下的编译日志。
-
+
解决办法:
- 方法1: 创建不同的 Xcode 工程
- 方法2: 还是同一个 Project,不同的 Targets,选中当前的 Target,**Edit Scheme**,在 **Build** 下取消勾选 **Find Implicit Depedencies**
-
+
问题修复完,最终编译链接效果如下:
-
+
编译日志里时间先放着。等以 HeaderMap 编译链接实现结束一起对比看看时间节省了多少。
@@ -4250,14 +4250,14 @@ LLVM 真是好东西,把 Xcode 编译、链接等一些幕后的事情变成
- 第三步:把这几个类的头文件导入到静态库头文件中
- 第四步:**Build Phases - Headers** 中设置静态库头文件为 **Public**,允许外部直接访问。其他几个头文件设为 **Private**
- 第五步:根据源码中头文件目录,创建一个 **HMapFile.json** 文件。以数组的形式写入信息,3个元素,分别为:Key、Prefix、Suffix。利用 LLVM 开发的 hmapmaker 能力,将 json 转换为 hmap 文件
-
+
- 第六步:**Build Settings - Use Header Maps** 中设为 **NO**,**Header Search Paths** 设为:`${SRCROOT}/HMapStaticLib-HeaderMap/hmap`(hmap 文件所在目录)
- 第七步:**Build Phases - Headers** 中设置静态库头文件为 **Public**,允许外部直接访问。其他几个头文件设为 **Private**
- 第八步:为了方便操作,不用每次创建文件夹,手动拖 **.a** 文件和 **.h** 文件很低效。编写 shell 脚本处理。指定到 **Build Phases - Run Script** 中
整体目录结构和设置如下:
-
+
@@ -4271,18 +4271,18 @@ LLVM 真是好东西,把 Xcode 编译、链接等一些幕后的事情变成
注意:因为我的 Demo 都是在一个 Xcode 工程,不同的 **TARGETS**,所以即使打包好的静态库产物,复制到 App 工程 **StaticLib** 文件夹下,系统在编译的时候还是会以源码的形式编译。
会看到以下的编译日志。
-
+
解决办法:同一个 Project,不同的 Targets,选中当前的 Target,**Edit Scheme**,在 **Build** 下取消勾选 **Find Implicit Depedencies**
最终编译链接效果如下:
-
+
#### 3. 数据分析及结论
-
+
**可以看到静态库先用 Header Search Paths 的方式,App 编译耗时6.5s。使用了自定义的 Header Map 文件后,App 编译耗时6.1s。编译耗时减少了0.4s,节省了6.6%。这是只有1个静态库且只有7个头文件的情况下测试得到的数据。真实项目中,如果静态库数量越多、项目文件目录长且嵌套复杂、头文件数量越多,以自定义 hmap 的方式将会在编译阶段节省更多的时间**
@@ -4394,13 +4394,13 @@ ImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheI
也可以使用 `size -l -m -x DDD` 指令来查看 Mach-O 的内存分布
-
+
利用 MachOView 查看如下:
-
+
-
+
@@ -4422,7 +4422,7 @@ ImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheI
-
+
@@ -4434,7 +4434,7 @@ File Size:在 Mach-O 文件中的占据的大小
从下面的图可以看出,`_PAGEZERO` 在真实的 Mach-O 文件中不存在,不占据大小。只在虚拟内存中存在。
-
+
@@ -4446,7 +4446,7 @@ File Size:在 Mach-O 文件中的占据的大小
Address Space Layout Randomization,地址空间布局随机化。是一种针对缓冲区溢出的安全保护技术,通过堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码的位置,达到阻止溢出攻击目的的一种技术。在 iOS 4.3 引入。
-
+
@@ -4478,7 +4478,7 @@ dyld 是一个动态链接程序。是 iOS 和 macOS 系统中的**动态链接
objdump --macho --private-headers DSYMDemo
```
-
+
- **启动阶段**:应用程序启动时,iOS 系统首先解析 `Info.plist` 文件,加载相关信息,例如启动画面等,并建立沙盒环境以及进行权限检查
@@ -4682,7 +4682,7 @@ objdump --macho --private-headers DSYMDemo
stacksize 0 // 这个字段指定了为程序的主线程初始栈分配的大小。在这个例子中,栈大小被设置为0,这可能意味着栈的大小将使用默认值,或者在其他地方指定(例如,通过链接器的其他设置或命令行选项)
```
-
+
@@ -4690,13 +4690,13 @@ objdump --macho --private-headers DSYMDemo
实验一:
-3.
+3.
编写2个函数,main 函数和 test 函数,除了方法名不同,在汇编侧是一样的。
实验二:
-
+
可以看到创建的 `test.c` 文件中,只有一个 `test` 方法。没有 `main` 方法,然后用 gcc 发现链接报错。
@@ -4719,7 +4719,7 @@ iOS 侧,dyld 默认以 main 函数作为函数起点。
### 6. dyld 加载过程
-
+
@@ -4876,7 +4876,7 @@ int main(int argc, const char* argv[])
dyld 本身就是一种 MachO 文件,MachO 文件类型为7,是一种包含多种架构的结构,如下图:
-
+
可以加载以下类型的 Mach-O 文件:MH_EXECUTE、MH_DYLIB、MH_BUNDLE
@@ -4905,11 +4905,11 @@ static void customConstructor(int argc, const char **argv) {
第三步:Edit scheme -> Run -> Environment Variables。Name 为 `DYLD_INSERT_LIBRARIES`,value 为 InjectFunction 动态库路径,`${SRCROOT}/Inject`。
-
+
第四步:运行输出如下
-
+
具体 [Demo]()
@@ -4978,7 +4978,7 @@ INTERPOSE(my_NSLog, NSLog);
第四步:运行输出。发现 NSLog 确实被 hook 做了替换。
-
+
@@ -5031,11 +5031,11 @@ lldb 保留了一个库列表,避免在按名称设置断点时出现问题,
怎么用?
-
+
另一种方式是利用 Xcode -> Edit Scheme,增加或修改 dyld 环境变量
-
+
@@ -5087,7 +5087,7 @@ Mach-O 格式⽤来替代 BSD 系统的 `a.out` 格式。Mach-O ⽂件格式保
Xcode 中也可以查看 Mach-O 文件类型
-
+
@@ -5095,7 +5095,7 @@ Xcode 中也可以查看 Mach-O 文件类型
Tips:`file` 命令可以查看文件类型。
-
+
`find . -name "*.c"` 比如在当前路径查找 .c 文件
@@ -5128,7 +5128,7 @@ Xcode 中可以修改指令集。由 Build Settings 的2个配置决定: `Arch
### 3. Mach-O 结构
-
+
一个 Mach-O 文件包含3块
@@ -5140,17 +5140,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)。
@@ -5183,7 +5183,7 @@ int main () {
第五步,查看目标文件指令信息 `objdump --macho -d main.o`
-
+
分析下指令信息:
@@ -5196,7 +5196,7 @@ int main () {
- test1、test2 2个函数都存在,地址不一样,为什么都是 `00 00 00 00 ` ?那系统如何确定函数真实地址?需要找个地方将这些符号存起来,然后再找个时机去把真实的地址写进去。需要**重定位符号表**,告诉链接器在链接阶段需要重定位
-
+
- 使用 `objdump --macho --reloc main.o` 指令查看 main.o 的重定位符号表。
- 符号表中 `test1` 的地址就是 `0000004a` 对应的数据。`49` 后面就是 `4a`
@@ -5224,13 +5224,13 @@ iOS 是小端序,从右向左看,`b2 ff ff ff` 可以看到后4位是 ff,
完整的
-
+
#### 2. 如何找到全局变量地址
第一步,先用指令查看全局变量的地址值。 `objdump --macho -s main`,可以看到地址值为 `0x100008000`
-
+
@@ -5279,7 +5279,7 @@ DWARF 是被众多编译器和调试器使用的,用于支持源码级别调
第三步:查看 Mach-O 中,包含 `__DWARF` 的段,使用指令查看:**`objdump --macho --private-headers main.o`**
-
+
可以看到:**编译阶段,编译器会把调试信息放到一个单独的段中,该段名为 `__DWARF`**
@@ -5291,11 +5291,11 @@ DWARF 是被众多编译器和调试器使用的,用于支持源码级别调
第五步:查看可执行文件中是否包含 `__DWARF` 段。`objdump --macho --private-headers main`
-
+
第六步:查看可执行文件的符号表。指令为:`nm -pa main`。红色区域代表调试符号。
-
+
可以看到:链接完成后,所有的调试符号、使用的符号,都放在符号表里了。
@@ -5304,7 +5304,7 @@ DWARF 是被众多编译器和调试器使用的,用于支持源码级别调
- 指令 **`clang -g1 main.m -o main`**,**参数 `-g1` 用于生成 `.dSYM` 文件**。
- 指令 **`dwarfdump main.dSYM` 用于查看 `.dSYM` 文件**
-
+
@@ -5432,7 +5432,7 @@ graph LR
第二步:Mac 自带 `console` App 查看崩溃报告,因为有 `.dSYM` 文件,所以可以看到方法信息
-
+
思考:假设线上 crash 了,如何根据 crash 堆栈中没有符号化的地址,找到符号的真实地址?
@@ -5824,11 +5824,11 @@ Full Report
- 在终端切目录到 App 内用:**nm -a ExploreDSYM | grep 1ee0** 验证,发现找到该符号的信息
-
+
- 在终端切目录到 App 内用: `dwarfdump --lookup 地址 --arch 架构 {AppName}.app.dSYM` 来找到相关信息,里面有符号名、文件名、代码行数,具体为:**dwarfdump --lookup 0x100001EE0 ExploreDSYM.app.dSYM** 验证,发现找到该符号的信息
-
+
- 在终端切目录到 App 内用:**objdump -d --start-address=0x100001ee0 --stop-address=0x100001f17 ExploreDSYM.app/ExploreDSYM** 验证,发现找到该符号的信息
@@ -5844,7 +5844,7 @@ Full Report
ExploreDSYM
```
-
+
- 符号表存储了当前文件的符号信息,静态链接器(ld) 和动态链接器(dyld) 在链接的过程中都会读取符号表,另外调试器也会用符号表来把符号映射到源文件。
@@ -5905,7 +5905,7 @@ uintptr_t get_slide_address(void) {
- 符号所在代码文件
- 符号所在代码文件的多少行
-
+
可以确认:由于 iOS ASLR 的存在,符号真正的地址,是基于 image 的开始地址进行偏移得到的。