diff --git a/Chapter1 - iOS/1.101.md b/Chapter1 - iOS/1.101.md index 20b6250..0db407b 100644 --- a/Chapter1 - iOS/1.101.md +++ b/Chapter1 - iOS/1.101.md @@ -62,7 +62,10 @@ self.iamgeView.layer.backgroundColor = [UIColor greenColor].CGColor; self.imageView.layer.cornerRadius = YES; ``` + + ## 离屏渲染的影响? + 触发离屏渲染后,会增加 GPU 的工作量,CPU + GPU 工作时间可能会超过一次渲染周期,会发生 UI 卡顿。 离屏渲染,指的是 GPU 在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。由上面的一个结论视图和圆角的大小对帧率并没有什么影响,数量的多少才显著影响性能。可以知道离屏渲染耗时是发生在离屏这个动作上面,而不是渲染。为什么离屏这么耗时?原因主要有创建缓冲区和上下文切换。创建新的缓冲区代价都不算大,付出最大代价的是上下文切换。 @@ -107,7 +110,7 @@ Xcode 就提供了检测功能,打开路径为: Xcode -> Debug -> View Debug - + ## 引申阅读 - [实时渲染管线中的光源渲染问题](https://zhuanlan.zhihu.com/p/392748735) - [蒙特卡罗方法](https://wiki.mbalib.com/wiki/蒙特卡罗方法) diff --git a/Chapter1 - iOS/1.102.md b/Chapter1 - iOS/1.102.md index 044759c..2e6d1a4 100644 --- a/Chapter1 - iOS/1.102.md +++ b/Chapter1 - iOS/1.102.md @@ -1,4 +1,4 @@ -LLVM +# LLVM [LLVM](https://llvm.org/) 项目是模块化、可重用的编译器以及工具链技术的集合 @@ -10,7 +10,7 @@ LLVM 不是 low level virtual machine 的缩写,就是项目名称。 ## 结构 -![](./../assets/LLVM-segment.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVM-segment.png) LLVM 由三部分构成: @@ -22,7 +22,7 @@ LLVM 由三部分构成: -![](./../assets/LLVM-Structure.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVM-Structure.png) 正是由于这样的设计,使得 LLVM 具备很多有点: @@ -42,7 +42,7 @@ LLVM 现在被作为实现各种静态和运行时编译语言的通用基础结 广义上来讲,LLVM 说的是一种架构。狭义上来讲,LLVM 强调的是偏后端部分,如下图的除了 clang 编译前端外的部分,包括优化器和编译后端,统称为 LLVM 后端。 - + @@ -64,7 +64,7 @@ Clang 相较于 GCC,具备下面优点: - 设计清晰简单,容易理解,易于扩展增强 -![](./../assets/LLVM-phase.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVM-phase.png) @@ -92,7 +92,7 @@ clang -ccc-print-phases main.m 展示如下: - + @@ -104,7 +104,7 @@ clang -ccc-print-phases main.m 查看 preprocessor (预处理)的结果:`clang -E main.m`。预处理主要做的事情就是头文件导入( include、import)、宏定义替换等。展示如下: - + @@ -112,7 +112,7 @@ clang -ccc-print-phases main.m 词法分析阶段,主要生成 Token。使用指令 `clang -fmodules -E -Xclang -dump-tokens main.m` 查看具体做了什么 - + @@ -120,7 +120,7 @@ clang -ccc-print-phases main.m 语法分析阶段,生成语法树(AST,Abstract Syntax Tree)。使用指令 `clang -fmodules -fsyntax-only -Xclang -ast-dump main.m` 查看 - + 对 main.m 的代码进行改造 @@ -142,7 +142,7 @@ void test(int a, int b) { 再次查看 AST 可以加深理解 - + 其中: @@ -154,7 +154,7 @@ void test(int a, int b) { 也就是先运算蓝色框内的值,然后用结果和红色框内的进行相减。所以这是很标准的树形结构。 - + @@ -171,7 +171,7 @@ LLVM IR 有3种表示格式: - text:便于阅读的文本格式,类似于汇编语言,推展名为 `.ll`。使用指令 `clang -S -emit-llvm main.m` 进行转换 - + 学过 arm64 汇编的话看这段 IR 很眼熟,汇编里 `load` 相关的指令都是从内存中装载数据,比如 `ldr`、`ldur` 、`ldp`。`store` 相关的指令是往内存中写入数据,比如 `str`、 `stur`、 `stp` @@ -304,9 +304,9 @@ Tips:ninja 如果安装失败,可以直接从 [github]( https://github.com/n 因为要编写 Clang 插件,是 c++ 代码,所以需要借助 IDE 的能力,我们选用 Xcode 进行编译。如下图所示,代表编译成功 - + - + @@ -334,11 +334,11 @@ Tips:ninja 如果安装失败,可以直接从 [github]( https://github.com/n - 先创建一个插件文件夹 `code-style-validate-plugin` - + - 编辑 `CMakeLists.txt` 文件,在最后添加 `add_clang_subdirectory(code-style-validate-plugin)` - + @@ -367,17 +367,17 @@ Xcode 打开项目,选择自动创建 Schemes - + 选择 Target 为 `CodeStyleValidatePlugin`,源代码所在文件夹为 `Sources/Loadable modules`,然后选中 CodeStyleValidatePlugin.cpp` 文件进行编写逻辑 - + 初步编写后 Command + B 进行编译,在 Products 下可以看到编译产物:`CodeStyleValidatePlugin.dylib` 动态库。 - + @@ -387,7 +387,7 @@ Xcode 打开项目,选择自动创建 Schemes 此步骤的目的是:在 testLLVM 项目中,加载 `CodeStyleValidatePlugin.dylib` 插件可以成功。因为默认的 Xcode 使用的 clang/clang++ 编译器和编译 `CodeStyleValidatePlugin.dylib` 动态库不是一个版本。不做修改的话,Xcode 加载 `CodeStyleValidatePlugin.dylib` 会报错。所以需要先编译出同一个 LLVM 版本的 clang/clang++。 - + @@ -406,7 +406,7 @@ Xcode 打开项目,选择自动创建 Schemes - `-Xclang` - 插件名称 - + @@ -414,7 +414,7 @@ Xcode 打开项目,选择自动创建 Schemes 在新创建的 TestLLVM Xcode 项目中加载创建的 `CodeStyleValidatePlugin.dylib` 会报错。原因是:由于 Clang 插件需要使用对应的版本去加载,如果版本不一致则会导致编译错误。如下所示: - + @@ -426,17 +426,17 @@ Xcode 打开项目,选择自动创建 Schemes 如下所示: - + 继续编译还是会报错,报错如下: - + 解决方案为:在 `Build Settings` 栏目中搜索 `index`,将 `Enable Index-Wihle-Building Functionality` 的 ` Default` 改为 `NO`。 - + @@ -448,7 +448,7 @@ Tips: 由于重新修改了插件的源码,所以每次 Build 构建完 FANP 编译成功,可以看到在日志中输出了我们编写的日志信息。 - + @@ -481,7 +481,7 @@ NS_ASSUME_NONNULL_END 利用 Clang 查看 AST 指令为 `clang -fmodules -fsyntax-only -Xclang -ast-dump workaholic_person.m` - + 核心思路为:我们要分析类名不符合规范的情况,要精确报错,首先要识别到类名,利用 AST 的能力可以办到(类名在 AST 的 `ObjCInterfaceDecl` 节点上)。然后获取到类名的行号信息,精确报错。 @@ -771,7 +771,7 @@ X("CodeStyleValidatePlugin", "This plugin is designed for scanning code styles, - 可以对 Category 名做检测,如果带下划线,则报错提示并给出修改意见 - 编写的 `CodeStyleValidatePlugin` Demo 中对不符合规范的做了 `DiagnosticsEngine::Warning` 级别的警告。如果遇到1个警告则不影响,继续编译。如果是 `DiagnosticsEngine::Error` 级别的编译报错,遇到1个则终止编译,请注意该区别,按需编写自己的插件逻辑。 - + @@ -819,13 +819,71 @@ X("CodeStyleValidatePlugin", "This plugin is designed for scanning code styles, - 使用 LLVM 编写 Clang 插件,解析 AST 拿到所有的 `ObjCInterfaceDecl` 信息,然后结合 `ObjCMethodDecl` 信息便可获取 Category 中的 所有方法,再判断方法是否同名 - + ### Pass 插桩,实现精准测试 这部分涉及到 Objective-C 和 Swift 的代码插桩逻辑的不同实现,篇幅较大,可以查看这篇文章 [精准测试最佳实践](./1.108.md) + + + +### 静态检测、静态分析 + +通过语法树进行代码静态分析,找出非语法性错误。模拟代码执行路径,分析出 control-flow graph(CFG)。 + +LLVM 项目中 clang 内置了一堆 checker,用于实现 lint。 + +具体的使用可以查看这篇文章:[质量检测](./1.137.md) + + + +### CodeGen - IR 代码生成与 OC Runtime 桥接 + +- Class/Meta Class/Protocol/Category 内存结构生成,并存放在指定的 section 中(如 Class:`_DATA, _objc_classrefs`) + +- Non-Fragile ABI:为每个 Ivar 合成 `OBJC_IVAR_$_` 偏移值常量 + +- 存取 Ivar 的语句(_ivar = 123; int a = _ivar) 转成 base + `OBJC_IVAR_$_` 的形式 + +- 将语法树中的 `ObjcMessageExpr` 翻译成相应版本的 `objc_msgSend`,super 翻译成 `objc_msgSendSuper` + +- 根据修饰符 strong、weak、copy、atomic 合成 @property 自动实现的 setter/getter。处理 `@synthesize` + +- ARC:分析对象引用关系,将 `objc_storeStrong` `objc_storeWeak` 等 ARC 代码插入 + +- 将 ObjcAutoreleasePoolStmt 翻译成 `objc_autoreleasePoolPush`、`objc_autoreleasePoolPop` + +- 自动调用 `[super dealloc]` + + + +- 为每个拥有 ivar 的 Class 合成` .cxx_destructor` 方法来自动释放类的成员变量,代替 MRC 时代的 `self.xxx = nil` + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Chapter1 - iOS/1.104.md b/Chapter1 - iOS/1.104.md index 3cd9f5c..22d3589 100644 --- a/Chapter1 - iOS/1.104.md +++ b/Chapter1 - iOS/1.104.md @@ -67,7 +67,7 @@ typedef struct NCTbl { 该表用于存储添加观察者时传了 NotificationName 的情况。也就是 Named Table 中,NotificationName 作为 key。在使用系统 API 注册观察者的时候还可以传入 object 参数,表示只监听该对象发出的通知,所以还需要一张表存储 object 和 observer 的对应关系,object 为 key,observer 为 value。 -![](./../assets/notification-namedTable.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/notification-namedTable.png) - 第一个 MapTable key 为 notificationName,value 为另一个 MapTable(子 Table) @@ -79,7 +79,7 @@ typedef struct NCTbl { nameless Table 结构较为简单,因为没有 notificationName,所以就一层 MapTable。key 为 object,value 为链表,存储所有的观察者。 -![](./../assets/Notification-namelessTable.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Notification-namelessTable.png) ### wildcard diff --git a/Chapter1 - iOS/1.105.md b/Chapter1 - iOS/1.105.md index 914189a..265216c 100644 --- a/Chapter1 - iOS/1.105.md +++ b/Chapter1 - iOS/1.105.md @@ -12,7 +12,7 @@ UIView 绘制流程。 - + @@ -24,7 +24,7 @@ Tips:`[UIView setNeedsDisplay]` 之后并不会立马调用 `[view.layer setN 下面来个 Demo 展示下简单的异步绘制一个 String。 - + @@ -32,7 +32,7 @@ Tips:`[UIView setNeedsDisplay]` 之后并不会立马调用 `[view.layer setN 接下来看看系统的绘制实现流程: - + 如何实现异步绘制? @@ -43,7 +43,7 @@ Tips:`[UIView setNeedsDisplay]` 之后并不会立马调用 `[view.layer setN - + diff --git a/Chapter1 - iOS/1.108.md b/Chapter1 - iOS/1.108.md index 3a5b878..41ea0ae 100644 --- a/Chapter1 - iOS/1.108.md +++ b/Chapter1 - iOS/1.108.md @@ -6,7 +6,7 @@ 下面这张图是22年整理的我们移动中台对于质量的一些把控手段,也是一个有效的 checklist。对于一个业务项目或者技术项目来说,QA 给的测试用例全部通过不能说明代码没有问题。单测覆盖率的最佳实践是针对于基础 SDK,对于业务侧的代码来说,由于经常变化,所以还是以人工测试为主,一些核心的不变的核心链路,沉淀出 UI 自动化用例,每周迭代的时候,交付测试后,开始 UI 自动化回归。 - + 但,这些回归还是不能 cover 所有问题,我们需要为我们写的每行代码买单,如何衡量每行代码的效果呢?这就是精准测试要做的事情。 diff --git a/Chapter1 - iOS/1.109.md b/Chapter1 - iOS/1.109.md index 873f639..f14415e 100644 --- a/Chapter1 - iOS/1.109.md +++ b/Chapter1 - iOS/1.109.md @@ -70,7 +70,7 @@ - rsp、rbp 寄存器用于栈操作。栈顶指针,指向栈的顶部 - leaq 和 movq 是有区别的。`leaq 0xd(%rip), %rax` 是从 `%rip + 0xd` 算出来的地址值赋值给 `%rax` ,`movq 0xd(%rip), %rax` 是从 `%rip + 0xd ` 算出来的地址值,取8个字节给 `%rax`。 -- `xorl` 抑或运算。 +- `xorl` 异或运算。 ## 寄存器的高低位兼容设计 diff --git a/Chapter1 - iOS/1.114.md b/Chapter1 - iOS/1.114.md index 06cb0da..1dbdd6e 100644 --- a/Chapter1 - iOS/1.114.md +++ b/Chapter1 - iOS/1.114.md @@ -303,7 +303,7 @@ print(fn(1, 2)) // 3 -可以看到 `rax` 里面存放了 `plus` 函数地址。第7行汇编是抑或运算,2个 `ecx` 抑或,结果为0,写入到 `ecx` 里。然后第8行汇编将 `ecx` 里的0写入到 `edx`,`edx` 也就是 `rdx`。走完第6行的汇编,继续看第7、8行 +可以看到 `rax` 里面存放了 `plus` 函数地址。第7行汇编是异或运算,2个 `ecx` 异或,结果为0,写入到 `ecx` 里。然后第8行汇编将 `ecx` 里的0写入到 `edx`,`edx` 也就是 `rdx`。走完第6行的汇编,继续看第7、8行 diff --git a/Chapter1 - iOS/1.119.md b/Chapter1 - iOS/1.119.md index cb5df89..0d92c83 100644 --- a/Chapter1 - iOS/1.119.md +++ b/Chapter1 - iOS/1.119.md @@ -59,7 +59,7 @@ var str1: String = "01234😄" -可以看到将9赋值给寄存器 `esi` 即 `rsi`,字符串赋值给寄存器 `rdi`,`xorl %edx, %edx` 抑或运算结果 0 赋值给寄存器 `edx` 即 `rdx`,也就是不纯粹为 +可以看到将9赋值给寄存器 `esi` 即 `rsi`,字符串赋值给寄存器 `rdi`,`xorl %edx, %edx` 异或运算结果 0 赋值给寄存器 `edx` 即 `rdx`,也就是不纯粹为 所以猜想正确。具体字符串创建过程可以查看 [Swift String 源码](https://github.com/apple/swift/blob/81812eaf2a6589610054a5db655bf7de3f3c8de6/stdlib/public/core/String.swift#L774) diff --git a/Chapter1 - iOS/1.135.md b/Chapter1 - iOS/1.135.md index 4d0464b..e92530c 100644 --- a/Chapter1 - iOS/1.135.md +++ b/Chapter1 - iOS/1.135.md @@ -24,7 +24,7 @@ ### 读取过程 - + @@ -93,7 +93,7 @@ LRU,最近最久未使用算法。比如3天内没有使用过的,则认为 ## 阅读时长记录器 - + ### 记录器种类 @@ -174,7 +174,7 @@ iOS 小端序,网络大端序。 ### 主要类关系图 - + @@ -191,7 +191,7 @@ iOS 小端序,网络大端序。 ### 架构图 - + @@ -219,7 +219,7 @@ iOS 小端序,网络大端序。 ### 基本原理 - + - UIView 作为 CALayer 的代理实现。CALayer 作为 UIView 的实例变量,负责展示规工作。 diff --git a/Chapter1 - iOS/1.136.md b/Chapter1 - iOS/1.136.md index 8a39588..062dcd8 100644 --- a/Chapter1 - iOS/1.136.md +++ b/Chapter1 - iOS/1.136.md @@ -21,11 +21,11 @@ QA:Target、Scheme 的关系是什么? 注意:duplicate 之后,target 虽然多了一份,但是代码和资源不变,所以 - + - + @@ -38,7 +38,7 @@ QA:Target、Scheme 的关系是什么? 当对某个 Target “Duplicate” 之后,会产生一份新的 plist 文件 - + @@ -52,19 +52,19 @@ QA:Target、Scheme 的关系是什么? 针对一个 Target 可以添加多个 Scheme,步骤如下 - + 这样的创建好之后,该 Target 存在3个 Scheme 了。有了 Scheme 有什么作用呢?设置宏定义的时候可以针对不同的 Scheme 进行设置。 - + 针对 OC、Swift 分别设置了很多宏定义,接下去需要跑 Beta 配置的代码,怎么办? - + 点击 Edit Scheme,在 Run 里面选择对应的 Scheme。 @@ -76,13 +76,13 @@ QA:Target、Scheme 的关系是什么? 创建 Scheme 步骤:Xcode -> New Scheme,再弹出的方框内,选择对应的 Target,然后输入需要创建的 Scheme 名称。此次我们创建了:Debug、Beta 2个新的 Scheme。 - + 创建好之后,可以看到实体 Scheme 和虚拟 Scheme 存在多对多的关系。但我们可以基于此,选择实体的 Scheme,然后在 Run 里面 “Build Configuration” 里面选择对应的 Scheme 与之对应。 - + @@ -92,7 +92,7 @@ QA:Target、Scheme 的关系是什么? 完整如下图: - + @@ -110,7 +110,7 @@ Xcode 自带的 Configuration Settings File 可以支持自定义一些宏, 创建步骤如下: - + 文件命名为:`文件夹名称-项目名称.scheme名称.xcconfig`,比如 `Config-Xcconfig.debug.xcconfig` @@ -120,13 +120,13 @@ Xcode 自带的 Configuration Settings File 可以支持自定义一些宏, 修改和完善创建的 Xcconfig 配置文件里的内容。之后在 Xcode 的 Project 选项下,找到 Configurations,选择对应的 Scheme,然后选择右边对应的 Xcconfig 文件。如下图 - + 我们只在 `Config-Xcconfig.debug.xcconfig` 文件中添加了 `OTHER_LDFLAGS = -framework "AFNetworking"`,Xcode 切换到 debug scheme 下,然后 Command + B 编译。 - + 验证结果: diff --git a/Chapter1 - iOS/1.137.md b/Chapter1 - iOS/1.137.md index e69de29..fd3626d 100644 --- a/Chapter1 - iOS/1.137.md +++ b/Chapter1 - iOS/1.137.md @@ -0,0 +1,143 @@ +# 质量检测 + +## 静态检测 +### 概念 + +静态分析器是一个 Xcode 内置的工具,使用它可以在不运行源代码的状态下进行静态的代码分析,并且找出其中的 Bug,为 app 提供质量保证。 + +静态分析器工作的时候并不会动态地执行你的代码,它只是进行静态分析,因此它甚至会去分析代码中没有被常规的测试用例覆盖到的代码执行路径。 + +静态分析器只支持 C/C++/Objective-C 语言,因为静态分析器隶属于 Clang 体系中。不过即便如此,它对于 Objective-C 和 Swift 混编的工程也可以很好地支持。 + +其中包括了安全、逻辑、以及 API 方面的问题。分析器可以帮你找到以上的这些问题,并且解释原因以及指出代码的执行路径。 + + + + +### 原理 + +- 通过语法树进行代码静态分析,找出非语法性错误 +- 模拟代码执行路径,分析出 control-flow graph(CFG) +- 预置了常用的 checker + +一个大型项目代码行数非常多,所有跑完全部的 CFG 必定很耗时。 + + + +### 如何使用 + +Xcode 静态分析功能是在程序未运行的情况下,对代码的上下文语义、语法、和内存情况进行分析,可以检测出代码潜在的文本本地化问题(Localizability Issue)、逻辑问题(Logic error)、内存问题(Memery error)、数据问题(Dead store)和语法问题(Core Foundation/Objective-C)等。 +功能入口在: `菜单栏 -> Product -> Analyze`。可以使用快捷键:Command+Shift+B + +#### 文本国际化 + +Xcode Target -> Build Settings -> Static Analysis - Issues - Apple APIs -> Miss Localizablity 设置为 YES。可以帮助检测发现,缺少国际化的文本。 + + +提示说明 `User-facing text should use localized string macro` 缺少本地化的 API,正确的采用下面一行的写法。 + +#### 逻辑问题 + +分母不能为0. + + + +#### 内存问题 + +虽然 Xcode 默认使用 ARC 管理内存,但是某些 C API 还需要开发者自己进行内存管理。比如 CF 框架下的 API。 +以及 block nil 判断等。 + + + + + +#### 数据问题 + +在编码过程中,一些数据问题可以通过Analyze很好的提示出来。比如下图: + + + + + +#### Xcode 13 新增的检查项 + +在 Xcode 13 中,静态分析器也得以升级,现在它可以捕获更多的一些逻辑问题: + +1. 断言 Assert 的副作用 +2. 死循环 +3. 无用的冗余代码(例如多余的分支条件) +4. C++ 中 move 和 forward 的滥用导致的潜在问题 + +一部分的改进来自于开源作者们对 Apple Clang 编译器的贡献。 + + + +##### NSAssert 中的副作用 + +使用 NSAssert 规避非预期的代码逻辑是很常见的好习惯,但是不规范地使用也会带来一些副作用,例如在 NSAssert 的判断条件中对变量或内存进行修改操作。 + +本来 `self.count` 默认为0,经过 `mockAssertIssue` 方法中,赋值为1,然后写了 NSAssert 是为了增加健壮性,但这个断言有副作用,虽然判断了赋值后是1,再自增判断等于2,但这不符合预期。经过断言已经修改为2了。 + + + + + +##### 死循环 + +下面是一个很常见的死循环的案例,这种稍微复杂一些的逻辑,乍眼一看,似乎没有什么问题: + + + +这段代码中,是一个二层循环,但是在内层的循环中,没有对 j 做递增,而是做了 result 的递增,这个问题虽然会隐晦,但是新版本的静态检查器会检测出来。 + + + +Analyze 功能强大,其实际能检测出的问题会更多。 + + + +### 自定义分析器参数 + +Xcode 也为静态分析器提供了很多的自定义项,方便开发者根据自身工作流进行定制。在 BuildSetting 中通过搜索 `Static Analysis` 关键字,可以筛选出跟分析器相关的设置项。 + + + +### 每一次编译都执行静态分析 + +通过打开 `Analyze During 'Build'` 可以使得每一次编译操作都执行分析器: + + + +### 设置静态分析器的运行模式 + +`Mode of Analysis for 'Analyze'` 可以配置分析器运行的模式,Xcode 提供了两种运行模式: + +- `Shallow(faster)` :`Shallow`规避了去检查一些耗时复杂的检查操作,所以 Shallow 运行的更快 +- `Deep` 则进行深入的检查,能抛出更全面的错误 + +同一个工程,分别看看 Shallow 和 Deep 的耗时差别: + + + + + +### 专项检查配置 + +静态分析器也提供了一些专项检查的配置,可以根据工程情况定制选择。假设,项目有严格的安全检查,可以打开下图中选中的这些配置项目: + + + + + +再或者,如果静态分析器抛出的一些问题不想关注,可以在 Xcode Build Settings 中关闭掉。从而更聚焦于感兴趣、更关注的问题。 + + + +### 单个文件的分析 + +也可以针对单个文件做静态检查。操作路径为:Product -> Perform Action -> Analyze "FileName"。 + +这样只会对单个文件检测,且不会分析 import 进来的文件(可以看到右边的 Person.m 的问题没有被检测出来) + + + diff --git a/Chapter1 - iOS/1.138.md b/Chapter1 - iOS/1.138.md new file mode 100644 index 0000000..91ecf2d --- /dev/null +++ b/Chapter1 - iOS/1.138.md @@ -0,0 +1,131 @@ +# AFNetworking 源码解读 + + + +## 结构 + +核心包含5个功能模块: + +- 网络通信模块(AFURLSessionManager、AFHTTPSessionManger) +- 网络状态监听模块(Reachability) +- 网络通信安全策略模块(Security) +- 网络通信信息序列化/反序列化模块(Serialization) +- 对于iOS UIKit库的扩展(UIKit) + +AF 是基于 NSURLSession 来封装的,所以核心类 AFURLSessionManager 也是针对 NSURLSession 做的封装,其余的4个模块,是为了网络通信(请求、响应数据的序列化、HTTPS 安全认证、UIKit 推展) + + + +其中 AFHTTPSessionManager 继承自 AFURLSessionManager,一般的网络请求都是用这个类,但是该类本身没有处理实际的网络,而是做了一些封装,把请求逻辑分发给父类 AFURLSessionManager 或者其他类去做。 + + + +## 以 get 请求为例 + +```objective-c +AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc]init]; + +[manager GET:@"https://somehost.com/goods" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { + +} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { + +}]; +``` + +调用初始化方法生成一个 manager,去看看具体做了什么 + +```objective-c +- (instancetype)init { + return [self initWithBaseURL:nil]; +} + +- (instancetype)initWithBaseURL:(NSURL *)url { + return [self initWithBaseURL:url sessionConfiguration:nil]; +} + +- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration { + return [self initWithBaseURL:nil sessionConfiguration:configuration]; +} + +- (instancetype)initWithBaseURL:(NSURL *)url + sessionConfiguration:(NSURLSessionConfiguration *)configuration +{ + self = [super initWithSessionConfiguration:configuration]; + if (!self) { + return nil; + } + // 对传过来的 BaseUrl 进行处理,如果有值且最后不包含/,url加上"/" + if ([[url path] length] > 0 && ![[url absoluteString] hasSuffix:@"/"]) { + url = [url URLByAppendingPathComponent:@""]; + } + + self.baseURL = url; + + self.requestSerializer = [AFHTTPRequestSerializer serializer]; + self.responseSerializer = [AFJSONResponseSerializer serializer]; + + return self; +} +``` + +初始化都调用到 + + + +## 数字证书 +数字签名,它能确认消息的完整性,进行认证。 +和公钥密码一样也要用到一对公钥、私钥。但相反的是,签名是用私钥加密(生成签名),公钥解密(验证签名)。私钥加密只能有吃有私钥的人完成,而由于公钥是对外公开的,因此任何人都可以用公钥解密(验证签名)。 + + +公钥基础设置(PKI)是为了能够更有效地运用公钥而制定的一系列规范和规格的总称,使用最广泛的 X.509 规范也是 PKI 的一种。 + +### 证书链 + +CA 有层级关系,处于最顶层的认证机构一般是根 CA,下面证书是经过上层签名的,而根 CA 则会对自己的证书进行签名。即自签名。 + + +怎么验证证书有没有被篡改? +当客户端走 HTTPS 访问站点时,服务器会返回整个证书链。先从最底层的CA开始,用上层的公钥对下层证书的数字签名进行验证。这样逐层向上验证,直到遇到了锚点证书。 + + +## 以 get 请求为例,展开探索 + +1. 请求入口 +``` +- (NSURLSessionDataTask *)GET:(NSString *)URLString + parameters:(id)parameters + success:(void (^)(NSURLSessionDataTask *task, id responseObject))success + failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure +``` +2. 创建 NSURLSessionDataTask +``` +- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method + URLString:(NSString *)URLString + parameters:(id)parameters + uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgress + downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgress + success:(void (^)(NSURLSessionDataTask *, id))success + failure:(void (^)(NSURLSessionDataTask *, NSError *))failure + +``` + + + +## 较原生相比,AF 做了什么事情 +拿 get、post 为入口,分析源码 +- 初始化很多属性 +- 安全措施,iOS 8 TaskID 不唯一的问题。dispatch_sync +- 自定义了一个 block 进度回调,使用起来更加方便 +- 用到了解耦,降低了主类的复杂度,维护起来更加方便 + +## AFSecurityPolicy 源码分析 HTTPS 认证 + + + + + +https://www.jianshu.com/p/856f0e26279d + +https://www.jianshu.com/u/14431e509ae8 + +https://blog.csdn.net/ZCMUCZX/article/details/79399517 diff --git a/Chapter1 - iOS/1.139.md b/Chapter1 - iOS/1.139.md new file mode 100644 index 0000000..ab3fa73 --- /dev/null +++ b/Chapter1 - iOS/1.139.md @@ -0,0 +1,347 @@ +# 图形渲染技巧 + +## GPU、CPU + +难道不能直接将数据从 CPU 跨到 GPU 处理?为什么需要多此一举,出现 OpenGL 这个框架? + + + +数据饥饿:从一块内存中将数据复制到另一块内存中,传输速度是非常慢的。内存复制数据时,CPU 和 GPU 都不能操作数据,避免引起错误。 + +- 如果 CPU、GPU 同时操作内存,同步和处理非常麻烦,加一些锁或额外手段将造成速度损耗。 +- 如果 GPU 处理完处于等待状态 +所以加了 buffer 缓冲区来处理该问题。有很多缓冲区,比如颜色缓冲区、深度缓冲区... + + + +## 着色器渲染流程 + + +顶点着色器:有多少个顶点,就执行多少次顶点着色器。 +光栅化:将顶点着色器的结果转换为像素。将输入的图元描述,转换为与屏幕对应的位置像素片元。 +片元着色器:将光栅化的结果转换为颜色。(iOS 显示图片的核心原因) + +顶点着色器执行次数多还是片元着色器执行次数多?一般来说片元着色器执行次数多。比如三角形,顶点着色器执行三次,有多少个像素点片元着色器就执行多少次。 + + + +## 着色器的渲染 +- 顶点着色器(必要) +- 细分着色器(可选) +- 几何着色器(可选) +- 片元着色器(必选) + +QA: +- 什么是管线? +### 什么是可编程管线 +可编程管线(Programmable Pipeline)是一种灵活的渲染流程,它允许开发者通过编写特定的程序代码来控制图形渲染的各个阶段。与固定管线相比,可编程管线提供了更高的灵活性和控制能力,使得开发者能够实现更复杂的图形效果和优化性能。 + +编程通过 Shading Language 语言(基于 C++)编写,开发者可以控制图形渲染的各个阶段,包括顶点着色器、细分着色器、几何着色器和片元着色器等。这些着色器可以实现各种复杂的图形效果和优化性能。 + +Apple 的 Metal 中叫 Metal Shading Language(简称 MSL 或 Metal 着色语言)是苹果公司为其图形和计算API Metal 设计的着色语言。它是一种低级别的编程语言,用于编写3D图形渲染逻辑和并行计算核心逻辑。 + +### 什么是固定管线 +固定管线(Fixed-Function Pipeline)是指一种渲染流程,其中图形渲染的各个阶段都是预定义的,开发者不能直接控制这些阶段的内部操作 + +- 顶点处理(Vertex Processing):顶点坐标转换、光照处理等。 +- 图元装配(Primitive Assembly):将顶点组装成图元,如三角形、线段等。 +- 光栅化(Rasterization):将图元转换为像素。 +- 片元处理(Fragment Processing):对每个像素进行颜色、纹理等处理。 +- 输出合并(Output Merging):将处理后的像素输出到帧缓冲区。 +固定管线的优点是简单易用,对于初学者来说,可以快速上手进行图形渲染。但是,它的缺点是不够灵活,不能满足高级渲染技术的需求,如自定义着色器、高级光照模型等 + + +### 什么是管线 +在OpenGL中,管线(Pipeline)是一个处理图形数据的序列化过程,它将顶点数据转换成最终屏幕上的像素。管线分为几个阶段,每个阶段对数据进行特定的处理 + + +## 渲染过程中可能产生的问题 +### 隐藏面消除 +在绘制 3D 的场景时候,我们需要决定哪些部分是对观察者可见的,哪些部分是对观察者不可见的。对于不可见的部分,应该尽早丢弃,例如在一个不透明的墙壁后,就不应该渲染,这个情况叫“隐藏面消除”(Hidden surface elimination) + +#### 解决方案 +##### 油画算法(画家算法) + +先绘制场景中离观察者较远的物体,再绘制离观察者较近的物体 +例如下面的场景中,先绘制红色部分,再绘制黄色部分,最后再绘制灰色部分。即可解决隐藏面消除的问题。 + +缺点: +- 需要遮盖的部分,画了多次,造成了渲染性能问题,浪费了资源。 +- 叠加的情况,油画算法无法处理。 + 使用油画算法,只要将场景按照物理距离观察者的距离远近排序。由远及近的绘制即可。 但某些情况下,这个距离无法排序。比如下面的场景: + + + 如果三个三角形是叠加的情况,油画算法无法处理。 + +##### 正背面剔除(Face Culling) + +想象一个 3D 图形,你从任何一个方向去观察,最多可以看到几个面?最多3个,从一个立方体的任意位置和方向去看,最多可以看到3个面。 + +思考:为什么需要多余的去绘制根本看不到的3个面? +如果能以某种方式丢弃这部分数据,OpenGL 渲染性能可以提高超过50%。 + +正背面剔除方案,不仅可以解决隐藏面消除问题,还可以带来性能提升。 + +如何知道某个面再观察者的视野中会不会出现?任何平面都有2个面,正面、背面。意味着同一个时刻只能看到一个面。 + +OpenGL 可以做到检查所有正面朝向观察者的面,并渲染他们,从而丢弃背面朝向的面,这样可以节约片元着色器的开销,提高性能。 + +核心:OpenGL 如何知道绘制的图形中,哪个是正面,哪个是背面? +通过分析顶点数据的顺序。 + +- 正面:按照逆时针顶点连接顺序的三角形面 +- 背面:按照顺时针顶点连接顺序的三角形面 + + + + +用顺时针、逆时针判断正反面不是绝对的,还需要结合观察者的位置。 + + + + +分析: +- 左侧三角形的顶点顺序为:1->2->3;右侧三角形顶点顺序为:1->2->3 +- 当观察者在右侧时,则右边三角形方向为逆时针方向,则为正面。左侧三角形为顺时针,则为反面 +- 当观察者在左侧时,则左边三角形顶点为逆时针方向,则为正面。右侧三角形为顺时针,则为反面 +总结: +正面和背面是由三角形顶点顺序和观察者方向共同决定的。随着观察者的角度方向改变,正反面也会改变。 + + +API +``` +// 开启表面剔除(默认背面剔除) +void glEnable(GL_CULL_FACE); +// 关闭表面剔除(默认背面剔除) +void glDisable(GL_CULL_FACE); +// 用户选择剔除哪个面(设置面剔除的方式) +void glCullFace(GLenum mode); // mode 为:GL_FRONT,GL_BACK,GL_FRONT_AND_BACK。默认 GL_BACK +// 用户指定绕序那个为正面 +void glFrontFace(GLenum mode); // mode 为:GL_CCW,GL_CW。默认 GL_CCW +// 剔除正面实现 +glCullFace(GL_BACK); +glFrontFace(GL_CW); +``` + +### 深度问题 +- 什么是深度?深度其实就是该像素点在 3D 世界中距离观察者的距离,z 值。 +- 什么是深度缓冲区?一块内存区域,专门存储着每个像素点(绘制在屏幕上的深度值)。深度值 Z 越大,则离摄像机越远。 +- 为什么需要深度缓冲区?在不使用深度测试的时候,如果先绘制了一个距离比较近的物体,再绘制距离比较远的物体,则距离远的位图因为后绘制,则会把距离近的物体覆盖掉。有了深度缓冲区后,绘制物体的顺序就不那么重要了。实际上,只要存在深度缓冲区,OpenGL 都会把像素的深度写入到缓冲区中。除非调用 glDepthMask(GL_FALSE) 来禁止写入。 + +#### Z-buffer 方法(深度缓冲区 Depth-buffer) +深度测试。深度缓冲区(Depth buffer)和颜色缓冲区(Color buffer)是一一对应的,颜色缓冲区存储像素的颜色信息,而深度缓冲区存储像素的深度信息。 +在决定是否绘制一个物体表面时,首先要将表面对应的像素深度值与当前深度缓冲区中的值进行比较。如果大于深度缓冲区的值,则丢弃这部分。否则利用这个像素对应的深度值和颜色值,分别更新深度缓冲区和颜色缓冲区。这个过程称为“深度测试”。 + +#### 使用深度测试 +深度缓冲区,一般由窗口管理系统 GLFW 创建,深度值一般由16位、24位、32位值表示,通常是24位,位数越高,深度精确度越高。 +- 开启深度测试:`glEnable(GL_DEPTH_TEST)` +- 在绘制场景前,清除颜色和深度缓冲区:`glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClearColor(0.0f, 0.0f, 0.0f, 1.0f);` +- 清除深度缓冲区默认值为1.0,表示最大的深度之,深度值范围是(0, 1),值越小表示越靠近观察者。值越大表示越远离观察者 +- 指定深度测试判断模式:`glDepthFunc(GLEnum mode);` + - GL_ALWAYS:总是绘制 + - GL_NEVER:永远不绘制 + - GL_LESS:如果当前深度值小于测试值,则绘制 + - GL_LEQUAL:如果当前深度值小于等于测试值,则绘制 + - GL_GREATER:如果当前深度值大于测试值,则绘制 + - GL_GEQUAL:如果当前深度值大于等于测试值,则绘制 + - GL_NOTEQUAL:如果当前深度值不等于测试值,则绘制 + + + +### ZFighting 闪烁问题 +为什么会出现闪烁问题? +因为开启深度测试后,OpenGL 就不会再去绘制模型被遮盖的部分,而是直接丢弃。这样的实现显示更真实,但是由于深度缓冲区精度的限制,对于深度相差非常小的情况下(例如在同一平面上进行2次绘制)OpenGL 就可能出现不能正确判断两者的深度值,会导致深度测试的结果不可预测,显示出来的现象是交错闪烁。 + + +#### ZFighting 闪烁问题解决 +第一步:启用 Polygon offset 方式解决 +让深度值之间产生间隔,如果2个图形之间有间隔,是不是意味着就不会产生干涉。可以理解为在执行深度测试前将立方体的深度值做一些细微的增加,于是就能将重叠的2个图形深度值之间有所区分。 +``` +// 启用 Polygon offset 方式 +glEnable(GL_POLYGOn_OFFSET_FILL) +``` +参数列表: +- GL_POLYGON_OFFSET_FILL:对应光栅化 GL_FILL +- GL_POLYGON_OFFSET_LINE:对应光栅化 GL_LINE +- GL_POLYGON_OFFSET_POINT:对应光栅化 GL_POINT + +第二步:指定偏移量。 +- 通过 `glPolygonOffset` 来指定 .glPolygonOffset 需要2个参数:factor 和 units。 +- 每个 Fragment 的深度值都会增加如下所示的偏移量。`Offset = (m * factor) + (r * units);` + - m: 多边形的深度斜率的最大值。理解一个多边形越是与近裁剪面平行,m 就越接近于0 + - r:能产生于窗口坐标系的深度值中可分辨的差异最小值。r 是由具体 OpenGL 平台指定的一个敞亮 +- 一个大于 0 的 Offset 会把模型推到离你(摄像机)更远的位置,相应的一个小于 0 的 Offset 会把模型拉近 +- 一般而言,只需要将 -1.0 和 0 这样简单赋值给 glPolygonOffset 基本可以满足需求 + +``` +void glPolygonOffset(Glfloat factor, Glfloat units); +应用到片段上总偏移计算公式: +Depth offset = (DZ * factor) + (r * units); +DZ:深度值(Z 值) +r:使深度缓冲区产生变化的最小值 +``` + + + +### 混合 +我们把 OpenGL 渲染时,会把颜色值存储在颜色缓冲区中,每个片段的深度值也存储在深度缓冲区。当深度缓冲区被关闭时,新的颜色将简单的覆盖原来的颜色缓冲区存在的颜色值,当深度缓冲区再次打开时,新的颜色片段只是当它们比原来的值更接近邻近的裁剪平面才会替换原来的颜色片段。`glEnable(Gl_BLEND)` + +#### 组合颜色 +目标颜色:已经存储在颜色缓冲区的颜色值 +源颜色:作为当前渲染命令结果进入颜色缓冲区的颜色值 +当混合功能被启用时,源颜色和目标颜色的组合方式是混合方程式控制的。在默认的情况下,混合方程式如下所示: +`Cf = (Cs * s) + (Cd *d)` +- Cf:最终计算参数的颜色 +- Cs:源颜色 +- Cd:目标颜色 +- s:源混合因子 +- d:目标混合因子 + +下面通过一个常见的混合函数组合来说明问题:`glBlendFunc(Gl_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)` + +如果颜色缓冲区存在一种红色(1.0f, 0.0f, 0.0f, 0.0f),目标颜色 Cd,如果在这上面用一种 alpha 为0.6的蓝色(0.0f, 0.0f, 1.0f, 0.6f) +``` +Cd(目标颜色) = (1.0f, 0.0f, 0.0f, 0.0f) +Cs(源颜色) = (0.0f, 0.0f, 1.0f, 0.6f) +S(源 Alpha) = 0.6f +D(目标 Alpha) = 1- S = 0.4f +Cf = (Cs * s) + (Cd * D) +``` + +总结:混合函数经常用于实现在其他一些不透明物体前面绘制一个透明物体的效果。 + + + +## GPUImage + +### 说明 + +开源的基于 GPU 处理图片/视频的一个框架,本身内置了几百种常见的滤镜效果,支持自定义滤镜(由开发者基于 OpenGLES GLSL 实现片元着色器) + +GPUImage 基于以下框架: + +- CoreMedia +- CoreVideo +- AVFoundation +- QuartzCore +- OpenGL ES2.0 + +采用 GPU 加速处理图片/视频滤镜效果。对比 GPUImage/CoreImage + +- GPUImage 可以自定义滤镜,缺乏人脸识别功能 +- GPUImage 在 GPU 上处理速度高于 CPU,百倍。 + +目的:隐藏/减弱关于 OpenGL ES 的复杂性。 + +滤镜处理的原理:就是把静态图片/视频上每一帧图片进行图形变换(饱和度/色温...)处理之后,再现实到屏幕上,本质上(像素点、颜色的变化) + + + + + +### 模块划分 + +- 上下文环境:包括运行 GPUImage 的上下文定义、资源定义、缓存管理相关类都包括在其中 +- 输入源:即滤镜处理链路的源头,包括视频、图片在内的各种输入源都定义其中 + +- 输出源:即处理链路的尽头,用于将处理后的数据绘制到屏幕、或者转成二进制数据推流等等 + +- 滤镜:提供多达上百种的滤镜效果使用来进行图像处理 + + + + + +### 核心流程 + +#### OpenGL ES 处理图片的流程 + +- 初始化 OpenGL ES 环境、编译、链接顶点着色器、片元着色器 +- 缓存顶点/纹理/坐标数据,传输相关数据到 GPU +- 图片绘制在帧缓存区 +- 从帧缓存区绘制图像 + +#### GPUImage 处理图片的流程 + +整体环节:Source(图片/视频数据源) -> filters(一堆滤镜)-> final target(处理好的图片/视频) + + + +##### Source(数据源) + +- GPUImageVideoCamera : 摄像头(用于实时拍摄视频) +- GPUImageStillCamera:摄像头(用于拍照片) +- GPUImagePicture:用于处理已经拍摄完成的照片 +- GPUImageVideo:用于处理已经拍摄好的视频 + +##### Filter(滤镜) + +GPUImageFilter:用来接收图形源,通过自定义顶点/片元着色器来渲染新的图像,完成滤镜处理后交给响应链的下一个对象。 + +GPUImage 中的滤镜均继承自 `GPUImageFilter`其定义了一个滤镜处理的基本流程。GPUImageFilter 继承自 GPUImgaeOutput,同时实现了 GPUImageInput 协议,这就使得 GPUImageFilter 即可接收 frameBuffer 输入进行图形处理。 + +`@interface GPUImageFilter : GPUImageOutput ` + + + +源对象将静止图像帧上传到OpenGL ES作为纹理,然后将这些纹理交给处理链中的下一个对象。 + +- GPUImage中的一个非常重要的基类 `GPUImageOutput` 和一个协议 `GPUImageInput`。基本上所有重要的 `GPUImage` 处理类都是`GPUImageOutput` 的子类,它实现了一个输出的基本功能 + +- 所有的 `GPUImage` 处理类也都遵循 `GPUImageInput` 协议。它定义了一个能够接收 frameBuffer 的接收者所必须实现的基本功能。主要包括: + - 接收上一个GPUImageOutput的相关信息 + - 接收并处理上一个GPUImageOutput渲染完成的通知 + + + +##### Final 环节 + +| 输出源 | 类型 | 说明 | +| --------------------- | ----------------- | ----------------------------------------------------- | +| GPUImageView | 继承自 UIView | 处理后的图像直接渲染到指定的原生 View 上 | +| GPUImageMovieWriter | 封装AVAssetWriter | 将处理后的视频数据逐帧写入指定路径文件中 | +| GPUImageRawDataOutput | 二进制数据 | 获取出来后纹理的二进制数据,可用于上行推流 | +| GPUImageRawDataOutput | 纹理数据 | 每一帧渲染结束后,通过 texture 属性返回输入纹理的索引 | + + + + + +### 责任链模式 + +对 GPUImage 源码的解读可以看到,GPUImage 采用了责任链设计模式来实现链式处理。 + +GPUImage 定义了一个 GPUImageOutput 类和一个 GPUImageInput 协议,实现了 GPUImageInput 协议的对象具备接收 frameBuffer 纹理输入并进行处理的能力。而继承自 GPUImageOutput 的对象,则可以将处理后的输出纹理传递到下一个 filter。 + + + +输入源 input 继承自 GPUImageOutput,可以将图片、视频等数据上传到 frameBuffer 后传递到 GPUImageFilter 中处理。最后一个 filter 处理完成后,将数据传递到了实现 GPUImageInput 协议的输出源 Output 中进行上屏绘制或者上行推流。上下游链路的打通。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Chapter1 - iOS/1.140.md b/Chapter1 - iOS/1.140.md new file mode 100644 index 0000000..0c84133 --- /dev/null +++ b/Chapter1 - iOS/1.140.md @@ -0,0 +1,264 @@ +# Aspects + +> Aspects 核心原理涉及3个技术点: +> +> - objc_msgForward(触发消息转发机制) +> - NSInvocation +> - block 的本质 + + +## 函数指针、指针函数的区别 + +定义不同: + +- 函数指针:本质是一个指针,该指针指向一个函数 +- 指针函数:本质是一个函数,函数的返回值是一个指针类型 + +写法不同 + +- 函数指针:`int (*fun)(int x,int y)` +- 指针函数:`int* fun(int x,int y)` + +用法不同: + +- 函数指针 + + ```c++ + typedef int (*FuncPtr)(int, int); + + int add(int a, int b) { + return a + b; + } + + FuncPtr addPtr = add; + int result = addPtr(3, 2); + ``` + +- 指针函数 + + ```c++ + int (*getAddFunction())(int, int) { + return add; + } + + int add(int a, int b) { + return a + b; + } + + int (*addPtr)(int, int) = getAddFunction(); + int result = addPtr(3, 2); + ``` + + + +## block 本质 + +block 详细探索步骤请查看这篇文章 [block 底层原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.89.md)。接下去查看建议版本的分析。 + +第一步:编写一个基础 block + +第二步:用 `xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 ViewController.m -o ViewController-arm64.cpp` 转成 c++ 查看原理 + + + +分析:可以发现 block 本质就是结构体,和 OC 对象一样,也有 isa 指针。block 传递进去的方法,被包装成 block 的成员变量,是一个叫做 FuncPtr 的函数指针了。 + +是不是我们可以按照系统定义,来构造一个 struct,承接一个 block,然后发起调用呢? + +```objective-c +typedef NS_OPTIONS(int, AspectBlockFlags) { + AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25), + AspectBlockFlagsHasSignature = (1 << 30) +}; + +typedef struct AspectBlock { + __unused Class isa; + AspectBlockFlags Flags; + __unused int Reserved; + void (__unused *invoke)(struct AspectBlock *block, ...); + + struct { + size_t reserved; + size_t Block_size; + void (*copy)(void *dst, const void *src); + void (*dispose)(const void *); + } *descriptor; +} *AspectBlockRef; + +void(^printBlock)(NSString *) = ^void(NSString *msg) { + NSLog(@"%@", msg); +}; +printBlock(@"Hello world"); + +struct AspectBlock *fakeBlock = (__bridge struct AspectBlock *)printBlock; +((void (*)(void *, NSString *))fakeBlock->invoke)(fakeBlock, @"Hello world"); +``` + + + +思考:我们目前已经用自定义的 struct 来承接了 block 并成功执行了。能否用 NSInvocation 来触发 block? + + + +## NSInvocation 触发 block + +一个方法需要成功调用并执行需要3要素: + +- 方法名称 `SEL` +- 方法签名(参数个人、参数类型、返回值类型等信息) `Method Type Encoding` +- 方法地址、方法实现 `IMP` + +如何从自定义的 block 结构体中获取这些信息呢? + + + + + + + + + + + + + + + +AspectsIdentifier:每做一次方法交换,都会转换为一次 AspectsIdentifier。是核心逻辑。 + + + +以一个例子作为源码探索切入口,点击跳转到源码中 + + + +可以看到给 NSObject 添加分类,核心 2 个 API。一个对象方法、一个类方法: + +```objective-c ++ (id)aspect_hookSelector:(SEL)selector + withOptions:(AspectOptions)options + usingBlock:(id)block + error:(NSError **)error; + +- (id)aspect_hookSelector:(SEL)selector + withOptions:(AspectOptions)options + usingBlock:(id)block + error:(NSError **)error; +``` + +内部都走到 `aspect_add` 方法中。其中都会生成 `AspectIdentifier` ,看看是如何生成的? + + + +第一步:生成 block 签名。 + + + +第二步:因为我们通过 AOP 给原始方法添加了 block,最后的效果是既可以调用原始方法,又可以调用 block 添加的代码。实现的前提是什么? + +比较 block 和 hook 类的方法的签名信息需要 Match。具体逻辑查看注释。 + + + +```shell +(lldb) po blockSignature + + number of arguments = 2 + frame size = 224 + is special struct return? NO + return value: -------- -------- -------- -------- + type encoding (v) 'v' + flags {} + modifiers {} + frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0} + memory {offset = 0, size = 0} + argument 0: -------- -------- -------- -------- + type encoding (@) '@?' + flags {isObject, isBlock} + modifiers {} + frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0} + memory {offset = 0, size = 8} + argument 1: -------- -------- -------- -------- + type encoding (@) '@""' + flags {isObject} + modifiers {} + frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0} + memory {offset = 0, size = 8} + conforms to protocol 'AspectInfo' +``` + + + +OC 方法签名和 block 方法签名是有区别的 + +- 比如在 Aspects 框架中,block 方法签名的参数个数是比 oc 方法参数个数少的。oc 方法自带 `id self, SEL _cmd` 2个参数。 +- 都使用相同的类型编码系统,但是 block 签名可能包含额外的信息,例如捕获的变量类型。比如上面的 block 方法签名的最后一个参数 `'@""'`,其中的 `AspectInfo` 就代表捕获的变量类型。 + +比如2个不带参数的 OC 方法签名和 block 方法签名: + +- oc 方法签名:`v@:` = `v` + `@` + `:`,返回值 `void`、参数1 `@` 代表对象、参数2 `:` 代表 SEL 类型 + +- block 方法签名:`v@?` = `v` + `@?`,返回值 `void`、参数1 `@?` 代表既是对象,又是 block + + + + + + + + + +## objc_msgForward + +骚操作: + +- 将待 hook 的方法,和 `objc_msgForward` 进行交换。 `objc_msgForward` 不管对象有没有实现,都会触发消息转发流程 +- 此时会走 Runtime 的 NSObject `forwardInvocation` 流程。且 Aspects 将 `forwardInvocation` 方法指向了 `__ASPECTS_ARE_BEING_CALLED__` 方法。 + +经历这么一波处理,hook 最后都守口到了 `__ASPECTS_ARE_BEING_CALLED__` 方法中。 + +前面研究过了 `AspectIdentifier` 的逻辑。接下去继续看看后续步骤。 + + + +可以看到内部执行 `aspect_prepareClassAndHookSelector`,其内部会调用 `aspect_hookClass`,又会调用 `aspect_swizzleClassInPlace`,最后调用 `aspect_swizzleForwardInvocation` 方法。 + +```objective-c +static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:"; +static void aspect_swizzleForwardInvocation(Class klass) { + NSCParameterAssert(klass); + // If there is no method, replace will act like class_addMethod. + IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@"); + if (originalImplementation) { + class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@"); + } + AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass)); +} +``` + +该方法将被 hook 对象的 `forwardInvocation:` 方法替换为 `__aspects_forwardInvocation:`。 + +回归头继续看下面的逻辑。 + + + +`class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding)` 可以实现将被 hook 类的 hook 方法,替换为 `_objc_msgForward` 或者某些版本下需要的 `_objc_msgForward_stret`。 + + + +比如 ViewController 的 viewWillAppear hook 流程就是: + +`[UIViewController viewWillAppear:]` -> `_objc_msgForwar` -> `[UIViewController forwardInvocation:]` -> `__ASPECTS_ARE_BEING_CALLED__` + + + + + +## 总结 + +Aspects 是 Runtime 使用的一个经典库,处理好核心逻辑后,也做了一些黑名单、线程安全等的保护。也有一些类似日志回放功能的处理。 + + + + + diff --git a/Chapter1 - iOS/1.141.md b/Chapter1 - iOS/1.141.md new file mode 100644 index 0000000..2dd3e34 --- /dev/null +++ b/Chapter1 - iOS/1.141.md @@ -0,0 +1,5 @@ +# Crash + +## Crash 类型 +- 容易越界(数组、字典、字符串等) NSRangeException +- 使用未初始化的变量 \ No newline at end of file diff --git a/Chapter1 - iOS/1.16.md b/Chapter1 - iOS/1.16.md index 8402da9..0f51be4 100644 --- a/Chapter1 - iOS/1.16.md +++ b/Chapter1 - iOS/1.16.md @@ -46,4 +46,4 @@ Classes: 该方案是 Apple 标准做法,不是骚操作,Objc 源码中也有使用。如下所示 - + diff --git a/Chapter1 - iOS/1.38.md b/Chapter1 - iOS/1.38.md index 9edec7e..1076a5e 100644 --- a/Chapter1 - iOS/1.38.md +++ b/Chapter1 - iOS/1.38.md @@ -35,7 +35,7 @@ 先附上一张总结的非常棒的RunLoop图 - + @@ -138,7 +138,7 @@ struct __CFRunLoopMode { Demo: - + @@ -178,7 +178,7 @@ Source0: Demo:给屏幕点击事件加断点,查看堆栈可以看到是 Source0 触发的。 - + @@ -221,7 +221,7 @@ CFRunLoopSourceRef 事件源(输入源) ### 一对多的关系 - + @@ -434,7 +434,7 @@ CFRelease(obersver); */ ``` -![触摸屏幕事件在 RunLoop 下的 source0](./../assets/WX20180801-104553@2x.png) +![触摸屏幕事件在 RunLoop 下的 source0](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WX20180801-104553@2x.png) 上个实验是在主线程对 RunLoop 进行的监听,但是由于是主线程是由系统创建的,所以系统也创建了对应的主 RunLoop,所以我们看不到 RunLoop 创建的状态,为了模拟完整的状态,我们开启子线程,在子线程中模拟 @@ -510,7 +510,7 @@ CFRelease(obersver); ### 运行原概要 -![RunLoop 运行原理图1](./../assets/image-20180801113342611.png) +![RunLoop 运行原理图1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/image-20180801113342611.png) - 图上左上角的 Input source 是早期 RunLoop 的分法,现在分法为:Source0 和 Source1。 - Source0:非基于 port 的,用户主动触发的事件。 @@ -527,7 +527,7 @@ CFRelease(obersver); 但是如何直到系统是运行 RunLoop 的哪个函数?给 viewDidLoad 设置断点,在 lldb 模式输入 `bt` 查看堆栈 -![](./../assets/RunLoop-Specific.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoop-Specific.png) 查看 CF 中 `CFRunLoop.c`源码。方法比较复杂,做了精简摘要 @@ -1137,7 +1137,7 @@ RunLoop 内部几个核心的动作:`__CFRunLoopDoObservers`、`__CFRunLoopSer 上面结合源码看了 Runloop 是怎么运行的。下面通过图片看看 RunLoop 各个状态运行切换的完整流程。 - + Demo: @@ -1151,7 +1151,7 @@ Demo: 2. 上面8>2,Runloop 处理 GCD Async To Main Quque - + @@ -1165,7 +1165,7 @@ Demo: 可以在 App 运行过程中,点击 Xcode 左下角的 debug 暂停按钮,可以看到 App 堆栈存在系统调用 - + @@ -1887,7 +1887,7 @@ main 方法内其实就是在执行 _selector。也就是在 NSThread 的初始 2. `[[NSRunLoop currentRunLoop] run]` api 换掉。查看系统说明,底层其实就是一个无限循环,循环内部不断调用 `runMode:beforeDate:`。下面也有建议,建议我们想销毁 RunLoop,可以替换 API,比如设置一个变量,标记是否需要结束 RunLoop - + 改进代码如下 @@ -1933,7 +1933,7 @@ self.thread = [[LifeThread alloc] initWithBlock:^{ 效果如下: - + diff --git a/Chapter1 - iOS/1.39.md b/Chapter1 - iOS/1.39.md index bc19448..3084837 100644 --- a/Chapter1 - iOS/1.39.md +++ b/Chapter1 - iOS/1.39.md @@ -219,12 +219,65 @@ NSLog(@"5"); +Demo8 + +```objective-c +dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL); + +dispatch_sync(queue, ^{ + NSLog(@"1"); +}); +dispatch_async(queue, ^{ + NSLog(@"2"); + dispatch_sync(queue, ^{ + // 这里有没有具体的逻辑都不影响,本质是一个任务 block,区别是空 block 和非空 block。 + }); + NSLog(@"4"); +}); +dispatch_sync(queue, ^{ + NSLog(@"5"); +}); +// console +1 +2 +死锁 +``` + +Demo9 + +```objective-c +dispatch_sync(dispathc_get_main_queue(), ^{ + NSLog(@"主队列同步"); +}); +// 死锁 +dispatch_sync(dispathc_get_main_queue(), ^{}); +// 死锁 +dispatch_sync(dispatch_get_main_queue(), nil); +// 死锁 +``` + + + ### 总结 只要是同步提交任务 `dispatch_sync()` 不管是提交到串行队列还是并发队列,都是在当前线程执行。 +## 一些经典 Demo + + + +会输出什么? + +打印结果,电脑速度快的话,会有很多次打印出5.慢的话,打印出大于5的几次。 + +分析:因为在循环内部,是全局并发队列。多线程的情况下,执行异步任务,任务的先后顺序没办法保证。可能线程1,拿到a=0,然后内部加了1.线程2一开始拿到a=0,但是代码还没执行到a++,在线程1里面,a就已经变为2,因为是 __block 修饰的。所以线程2里面拿到的a变成了a,然后内部a++后,a就是3.其他线程执行情况类似。 + +NSLog 属于 IO 流,比普通运算耗时。所以当能执行 NSLog 的时候,a 一定是大于等于5的。某条线程 a 大于等于5之后,就立马结束 while 循环,开始执行最后的 NSLog。 + +所以电脑越快,打印5的次数更多。电脑慢的情况下,可能会存在几次输出大于5的情况。 + ## performSelector...withObject 研究 @@ -263,7 +316,7 @@ NSLog(@"5"); Demo1 - + QA:为什么先打印1、3再打印2?因为 `performSelector...withObject...afterDelay` 相当于给 RunLoop 添加了一个 Timer,Timer 运行需要 RunLoop 配合。RunLoop 在被唤醒的时候会处理定时器。 @@ -271,7 +324,7 @@ Demo2: - + @@ -327,7 +380,7 @@ QA:为什么 showLog 里的2没有打印? Demo3: - + 同理,GCD 虽然开启了子线程,但是 Block 结束后,线程也就结束了。所以线程任务中的1秒后的任务肯定也结束了。 @@ -335,13 +388,13 @@ Demo3: Demo4: - + 可以看到 NSThread 里的 block 执行结束后,thread 结束了。后面的 performSelector 想在线程里执行任务,就会 crash。 解决办法也是在线程的 block 里面加 RunLoop,让它保活 - + @@ -550,21 +603,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 循环,忙等,“太浪费性能了”(如果使用锁资源的线程任务很简单,那自旋也是高效的,可以快速获取锁。) @@ -643,26 +696,26 @@ int cursorr = 1; 假如对存钱过程,忘记解锁怎么办?产生死锁,如下 - + 添加 cursor 标记死锁是发生在 `saveMoney` 方法执行的第几次。发现是第二次。因为第一次锁没有任何使用方,所以加锁成功,当第二次加锁的时候发现锁没有释放,所以产生死锁。 这时候使用尝试加锁 API `os_unfair_lock_trylock` 即可成功如下 - + #### 汇编窥探原理 同样方式看看 ,按照上述调试汇编代码的步骤,我将关键步骤截图如下 - - - - - + + + + + - + 结论:可以看到 `os_unfair_lock` 在锁等待的时候,底层调用的是 `sysCall`,当这一步执行后会发现后续代码都不执行了,也就是调用系统底层能力,线程真正休眠了,而不是一个循环忙等,性能也好。 @@ -711,7 +764,7 @@ pthread_mutex_destroy(&_moneyLock); 使用如下 - + #### 递归锁 @@ -757,7 +810,7 @@ pthread_mutex_destroy(&_moneyLock); 改进后的效果如下 - + @@ -769,33 +822,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`,执行完第四句指令,线程立马就结束了。 @@ -815,7 +868,7 @@ QA:互斥递归锁,可以在不同线程中加锁吗? 激活所有等待该条件的线程 `pthread_cond_broadcast(&_condition)` - + 可以看到同时调用 remove、add 方法 @@ -917,13 +970,13 @@ NSRecursiveLock 不能在多线程下递归调用。@synchronized 可以在多 Demo - + NSLock 死锁 - + 会发生死锁,后续代码无法执行,App 表现就是 ANR。重复对 NSLock 进行加锁可能导致死锁问题,同时也可能引发数据竞争和性能下降等并发相关隐患 @@ -973,7 +1026,7 @@ API Demo: - + @@ -981,13 +1034,13 @@ Demo: 使用 NSCondition 的时候 unlock 和 signal 的顺序可能会对结果造成影响。举个例子 - + 可以看到在这种情况下,由于 NSCondition 另一个地方 wait,wait 也需要释放锁,但是另一个发 signal 的地方,还没释放锁。所以会等待2s。 针对这个情况,可以将 unlock 和 signal 的顺序进行调整。 - + 先解锁,然后发送 singal,后续其他的业务逻辑也不影响。当然这个需要针对实际代码进行设计。 @@ -1030,7 +1083,7 @@ API 如下: Demo - + 分析:虽然通过3个线程,设置了线程的先后顺序,但是多线程任务执行的时候到底谁先执行,是没办法控制的。但是通过 `NSConditionLock lockWhenCondition:*` 的能力,可以控制线程的执行顺序。 @@ -1046,7 +1099,7 @@ Demo 线程同步的本质就是多线程的任务是顺序执行 - + @@ -1060,11 +1113,11 @@ semaphore 叫做”信号量” 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步 - + 可以看到打印了20个线程,但是我们控制线程最大数量怎么办呢?可以用信号量实现。效果如下: - + `dispatch_semaphore_wait` 函数的本质 @@ -1076,7 +1129,7 @@ semaphore 叫做”信号量” 所以如何让线程同步?设置信号量的值=1即可。保证同一时间只有一个线程任务在执行。代码如下 - + @@ -1136,15 +1189,15 @@ dispatch_semaphore_signal(semaphore); `@synchronized` 使用很方便,它是对 `pthread_mutex_t` 递归锁的封装。Demo 如下 - + 为了探究下实现,开启汇编调试 - + - + 通过汇编可以看到 `@synchronized` 底层调用了 `objc_sync_enter` 方法,其中又调用了 `id2data` 和 `os_unfair_recursive_lock_lock_with_options` 方法。 可以查看 objc4 的源码(笔者的 objc 版本为 objc4-objc4-912.3),查找 `objc_sync_enter` @@ -1620,7 +1673,7 @@ pthread_rwlock_init(&_lock, NULL) Demo - + @@ -1648,7 +1701,7 @@ dispatch_barrier_async(self.queue, ^{ 上 Demo - + diff --git a/Chapter1 - iOS/1.40.md b/Chapter1 - iOS/1.40.md index ac6f7b5..d48262e 100644 --- a/Chapter1 - iOS/1.40.md +++ b/Chapter1 - iOS/1.40.md @@ -24,7 +24,7 @@ NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval 栈、堆、BSS、数据段、代码段 - + @@ -53,7 +53,7 @@ BSS段(bss segment):通常用来存储程序中未被初始化的全局变 代码段(code segment):编译之后的代码。通常是指用来存储程序可执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。 -![内存](./../assets/ram.png) +![内存](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ram.png) 上 Demo 验证 @@ -132,7 +132,7 @@ Tagged Pointer 格式下,指针值不再是有效抵制,而是表示值。 当对 TaggedPointer 数据调用方法的时候,objc_msgSend 能识别出如果是 Tagged Pointer,比如 NSNumber 的 intValue 方法,直接从指针提取数据,节省了调用开销。所以使用了 TaggedPointer 技术不仅节约内存空间,又能提高方法查找速度。 - + @@ -233,7 +233,7 @@ Tagged Pointer 也就是一个伪指针,对象的指针中存储的数据变 Demo - + 在 64 位的 cpu 下,如果使用 Tagged Pointer 技术的话,则 NSNumber 对象的值直接存储在了指针中,系统不会为其在堆上分配内存,可以节省很多内存开销。此时,NSNumber 对象的指针中存储的数据变成了 Tag + Data 的形式(Tag 为特殊标记,用于区分NSNumber、NSDate、NSString 等小内存对象的类型;Data 为具体的值)。这样使用一个 NSNumber 对象只需要在栈中开辟 8 个字节的指针内存。当栈中 8 个字节的指针内存不够存储数据时,才会再将 NSNumber 对象存储到堆中 @@ -253,7 +253,7 @@ Demo 路径:Xcode - Edit Scheme - Run - Arguments - Environment Variables - 添加环境变量 `OBJC_DISABLE_TAG_OBFUSCATION` 设置为 YES 即可。 - + @@ -422,7 +422,7 @@ _objc_decodeTaggedPointer_noPermute_withObfuscator(const void * _Nullable ptr, u } ``` - + @@ -430,7 +430,7 @@ _objc_decodeTaggedPointer_noPermute_withObfuscator(const void * _Nullable ptr, u #### Tagged Pointer 与 isa - + 通过参考 objc 源码,针对对象指针进行解密后发现: @@ -461,7 +461,7 @@ b 也就是11,二进制为 `1011`,Tagged Pointer 中,iOS 侧第一位是 T 3 区分数据类型。具体是什么数据类型,继续做个实验看看 - + @@ -482,7 +482,7 @@ b 也就是11,二进制为 `1011`,Tagged Pointer 中,iOS 侧第一位是 T Objc 源码中,NSInteger、NSUInteger 都是别名。初始化 NSNumber 的时候用的是 `NSNumber numberWithInteger:<#(NSInteger)#>` - + @@ -560,7 +560,7 @@ Tagged Pointer 如何区分是较小的对象,比如 NSString、NSDate、NSNum 验证下 - + 可以看到: @@ -573,11 +573,11 @@ Tagged Pointer 如何区分是较小的对象,比如 NSString、NSDate、NSNum 下面是针对 double 包装成 NSNumber 的 Tagged Pointer 指针结构拆分: - + - + @@ -770,7 +770,7 @@ Demo1 运行该代码会 Crash,报错信息如下 - + @@ -795,11 +795,11 @@ Demo1 改法1:将 property 改为 **atomic** 修饰的。 - + 改法2:对 name 加锁 - + @@ -807,7 +807,7 @@ Demo1 Demo2 - + @@ -854,7 +854,7 @@ NSString、NSMutableString 继承关系如下: - + 通过 `[[NSString alloc] initWithString:@"**"]` 方式创建的 NSString 字符串 @@ -922,27 +922,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 的引用计数情况: @@ -954,7 +954,7 @@ iOS 中使用引用计数来管理 OC 对象的内存。一个新创建的 OC 改进 - + @@ -1403,14 +1403,14 @@ class StripedMap { ``` - iOS 侧 StripeCount 为8 -- `indexForPointer` 方法根据传入的指针,将指针地址转换为 uintptr_t 类型,然后将地址右移4位和右移9位的结果进行抑或运算,然后将结果取模 StripeCount(iOS 侧为8),用于确定索引的范围(范围在:[0, stripeCount -1] ) +- `indexForPointer` 方法根据传入的指针,将指针地址转换为 uintptr_t 类型,然后将地址右移4位和右移9位的结果进行异或运算,然后将结果取模 StripeCount(iOS 侧为8),用于确定索引的范围(范围在:[0, stripeCount -1] ) - Operator 重写了运算符 [],底层调用 `indexForPointer` 方法,使之使用起来更像一个数组。 ### 引用计数表 - + @@ -1418,7 +1418,7 @@ class StripedMap { weak_table_t 结构如下: - + ```c++ #define WEAK_INLINE_COUNT 4 @@ -1473,7 +1473,7 @@ struct weak_entry_t { #### 存 weak 对象 - + @@ -2275,7 +2275,7 @@ void sel_init(size_t selrefCount){ 在 gone 处加断点,利用 runtime 查看类中的方法信息 -![](./../assets/cxx_destructDemo1.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/cxx_destructDemo1.png) 发现存在 `.cxx_destruct` 方法。 @@ -2310,7 +2310,7 @@ void sel_init(size_t selrefCount){ @end ``` -![](./../assets/cxx_destructdemo3.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/cxx_destructdemo3.png) Tips:@property 会自动生成成员变量,另外类后面加 `{}` 在内部也可以加成员变量,假如成员变量是对象类型,比如 NSString,则叫实例变量。 @@ -2324,7 +2324,7 @@ Tips:@property 会自动生成成员变量,另外类后面加 `{}` 在内部 在 gone 的地方加断点,输入 `watchpoint set variable p->_name`,则会将 `_name` 实例变量加入 watchpoint,当变量被修改时会触发断点,可以看出从某个值变为 0x0,也就是 nil。此时边上调用堆栈显示在 `objc_storestrong` 方法中,被设置为 nil. -![](./../assets/cxx_destructDemo2.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/cxx_destructDemo2.png) @@ -2567,7 +2567,7 @@ Person *person2 = [Person personWithName:@"FantasticLBP"]; 隐式调用工厂方法 - + @@ -2577,7 +2577,7 @@ Person *person2 = [Person personWithName:@"FantasticLBP"]; 如何修改?加一个 bridge 即可。 - + 由于 ARC 没有加 retain。所以 `person = (__bridge id)result;` 这里完成了对象的 retain。ARC 在退出方法的作用域时给对象加上release。前后对应,内存正确。 @@ -2688,7 +2688,7 @@ class AutoreleasePoolPage { - 每个 AutoreleasePoolPage 对象占用 4096 (16的3次方,0x2000)字节内存,除了用来存放它内部的成员变量(内部成员固定有7个,56个字节,即 `0x18`, `0x1000 + 0x38 = 0x1038` ),剩下的空间用来存放 autorelease 对象的地址 - 所有的 AutoreleasePoolPage 对象通过**双向链表**的形式连接在一起。child 指向下一个 AutoreleasePoolPage 对象,parent 指向上一个 AutoreleasePoolPage 对象 -![](./../assets/autoreleasepool.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/autoreleasepool.png) ```objectivec id * begin() { @@ -3014,7 +3014,7 @@ class AutoreleasePoolPage : private AutoreleasePoolPageData { 举个例子,for 循环创建1000个 Person 对象,用 autorelease 修饰,如何工作? - + 分析: @@ -3053,7 +3053,7 @@ int main(int argc, const char * argv[]) { main 方法内部3个 autoreleasepool 底层怎么样工作的? - + 分析: @@ -3920,7 +3920,7 @@ iOS 在主线程的 Runloop 中注册了2个 Observer 结合 RunLoop 运行图 -![](./../assets/RunLoop-SourceCode.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoop-SourceCode.png) - 01 通知 Observer 进入 Loop 会调用 `objc_autoreleasePoolPush` @@ -4017,7 +4017,7 @@ IMP Caching 比其他方法快2倍。 ### OC 中有没有不对内存进行强持有的集合类型? -NSHashMap、NSMapTable 都可以描述 key、value 的内存修饰。 +`NSHashMap`、`NSMapTable` 都可以描述 key、value 的内存修饰。 数组有 NSPointerArray 内部持有的是对象的指针,并非直接保存对象。不过 oc 转指针需要加 `(__bridge void*)` 进行修饰。NSPointerArray 的构造方法中可以通过 NSPointerFunctionsOptions 来声明内存的控制。 @@ -4045,6 +4045,22 @@ NSHashMap、NSMapTable 都可以描述 key、value 的内存修饰。 (lldb) ``` +再来2个实验: + + + +分析:可以看到 p1 的地址,在刚初始化后,和当作 key 加入到 NSDictionary 后,地址发生了变化。对 p1 执行了 copy 操作。 + + + +分析: + +- table1 只有1个元素是因为对 p1 执行的是 `NSPointerFunctionsWeakMemory` 所以不会产生2个对象。同一个对象的 hash 值一样。所以仅存在1个元素 +- table2 的 key 是 `NSPointerFunctionsCopyIn`, copy 产生2个不同的 p,且 hash 值不一样,所以存在2个元素 +- 2个 NSMapTable 对 key 的内存操作不一样,其结果也不一样。如果 NSMapTable key 用 `NSPointerFunctionsCopyIn` 修饰,其效果等价于 NSMutableDictionary。 + + + ### NSError 内存泄漏的 case 同事问了一个问题,下面的代码存在什么问题? @@ -4086,7 +4102,7 @@ NSHashMap、NSMapTable 都可以描述 key、value 的内存修饰。 这段代码运行会 crash,信息如下 -![](./../assets/NSErrorZombieCrash.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSErrorZombieCrash.png) 原因是 NSError 构造方法内部会加 autorelease。源码如下 @@ -4149,7 +4165,7 @@ MRC 下的 `[(id)(object) autorelease]` 等价于 ARC 下的 `id __autoreleasing 我写了个僵尸对象检测工具,效果如下 -![](./../assets/ZombieSniffer.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ZombieSniffer.png) 可以定位僵尸对象,并且打印出具体堆栈,并模拟系统行为调用 `abort` 。对监控原理和工具实现感兴趣的可以查看这里[带你打造一套 APM 监控系统-内存监控之野指针/内存泄漏监控](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.74.md#zombieSniffer) diff --git a/Chapter1 - iOS/1.44.md b/Chapter1 - iOS/1.44.md index ca90742..d5a7569 100644 --- a/Chapter1 - iOS/1.44.md +++ b/Chapter1 - iOS/1.44.md @@ -370,7 +370,7 @@ Native 每次改动都比较“慢”,所以类似 Header 就很需要。 PS: Native 打开 H5,如果 300ms 没有响应则需要 loading 组件,避免白屏 因为 H5 App 本身就有 Header 组件,站在前端框架层来说,需要确保业务代码是一致的,所有的差异需要在框架层做到透明化,简单来说 Header 的设计需要遵循: - H5 Header 组件与 Native 提供的 Header 组件使用调用层接口一致 -- 前端框架层根据环境判断选择应该使用 H5 的 Header 组件抑或 Native 的 Header 组件 +- 前端框架层根据环境判断选择应该使用 H5 的 Header 组件异或 Native 的 Header 组件 一般来说 Header 组件需要完成以下功能: diff --git a/Chapter1 - iOS/1.45.md b/Chapter1 - iOS/1.45.md index 7e57e6c..94fce65 100644 --- a/Chapter1 - iOS/1.45.md +++ b/Chapter1 - iOS/1.45.md @@ -4,7 +4,7 @@ ## CADisplayLink 内存泄漏 - + 可以看到 CADisplayLink 和 VC,VC 和 CADisplayLink 互相持有,造成内存泄漏,没有释放。即使页面离开,定时器还在继续运行,不断打印。 @@ -18,11 +18,11 @@ NSTimer 的基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES blo Demo 如下: - + 但是当使用 `[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerTask) userInfo:nil repeats:NO];` repeats 为 NO 的时候,好像不会内存泄漏。这是为什么? - + @@ -194,7 +194,7 @@ Demo 如下: ### 改用 block 的方式替换 API,不再持有 target - + 该种方式,控制器 (self)强引用 timer,timer 强引用 block,block 弱引用 self,3者没有形成环。 @@ -202,7 +202,7 @@ Demo 如下: ### 采用系统 NSProxy 代替自定义的中间类 - + 注意:继承自 NSProxy 的类,不能 init。 @@ -218,7 +218,7 @@ QA:自己写的继承自 NSObject 的代理对象和继承自 NSProxy 的代 看一段神奇的代码 - + 为什么打印出 `0 1`? @@ -260,7 +260,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 的执行时间不固定) @@ -303,7 +303,7 @@ GCD timer 不依赖 RunLoop,系统底层驱动,所以会更加准确。因 ### 打破循环引用,NSTimer target 自定义 - + diff --git a/Chapter1 - iOS/1.46.md b/Chapter1 - iOS/1.46.md index 0ce983b..3ec368f 100644 --- a/Chapter1 - iOS/1.46.md +++ b/Chapter1 - iOS/1.46.md @@ -8,7 +8,7 @@ Demo1:创建 Person 类,点击事件里触发属性值的改变。 - + @@ -20,7 +20,7 @@ Demo1:创建 Person 类,点击事件里触发属性值的改变。 在内存中的结构如下图 - + 整个流程分析下: @@ -30,13 +30,13 @@ Demo1:创建 Person 类,点击事件里触发属性值的改变。 当我们按照 KVO 后动态生成的类名去创建一个新的类的时候,Xcode 会报错:`[general] KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class`。因为自己创建的类名和系统将要动态创建的类名冲突了,并且 KVO 监听失效 - + Demo2: - + 分析: @@ -48,7 +48,7 @@ Demo2: Demo3: - + 可以看到我们将 KVO 的数据类型改为 double 后,原本的 setHeight 是 `_NSSetLongLongValueAndNotify`,现在是 `_NSSetDoubleValueAndNotify` @@ -58,11 +58,11 @@ Demo3: ### NSSet**ValueAndNotify 的内部实现 - + 来对 Person 类增加一些打印方法 - + @@ -117,7 +117,7 @@ Demo3: ### 重写 class 方法 - + 可以看到利用 runtime api,在添加 KVO 之后,类对象为 `NSKVONotifying_Person` @@ -129,7 +129,7 @@ Demo3: ### KVO 类的所有方法 - + @@ -148,7 +148,7 @@ QA:为什么新创建的类没有 getter? ### 修改成员变量的值可以触发 KVO 吗 - + 我们将 Person 类的成员变量暴露出来,在点击事件里修改,发现不能触发 KVO。 @@ -158,7 +158,7 @@ QA:如何手动触发? 手动调用 `willChangeValueForKey`、 `didChangeValueForKey` - + @@ -197,7 +197,7 @@ QA:如何手动触发 KVO? KVC 之后会触发 KVO 吗? - + 发现 KVC 触发了 KVO。 @@ -205,7 +205,7 @@ KVC 之后会触发 KVO 吗? 整个流程如下 - + `[self.person setValue:@10 forKey:@"age"]` 会先调用 `setKey:` 同名的方法,找不到则调用 `_setKey:` 的方法,如果还是找不到则调用 `+(BOOL)accessInstanceVariableDirectlt`,如果该方法返回 YES,则可以直接修改成员变量的值,会按照 `_key`、`_isKey`、`key`、`isKey` 的顺序寻找成员变量,如果找到则直接赋值,没找到则抛出异常 `NSUnknownKeyException` @@ -260,7 +260,7 @@ KVC 之后会触发 KVO 吗? - + @@ -425,7 +425,7 @@ Apple 文档告诉我们:被观察对象的 `isa指针` 会指向一个中间 - 键值观察通知依赖于 NSObject 的两个方法:`willChangeValueForKey:、didChangeValueForKey:` 。在一个被观察属性改变之前,调用 `willChangeValueForKey:` 记录旧的值。在属性值改变之后调用 `didChangeValueForKey:`,从而 `observeValueForKey:ofObject:change:context:` 也会被调用。 -![KVO原理图](./../assets/2018_11_12_KVO.png) +![KVO原理图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018_11_12_KVO.png) 为什么要选择是继承的子类而不是分类呢? 子类在继承父类对象,子类对象调用调方法的时候先看看当前子类中是否有方法实现,如果不存在方法则通过 isa 指针顺着继承链向上找到父类中是否有方法实现,如果父类种也不存在方法实现,则继续向上找...直到找到 NSObject 类为止,系统会抛出几次机会给程序员补救,如果未做处理则奔溃 diff --git a/Chapter1 - iOS/1.48.md b/Chapter1 - iOS/1.48.md index b185224..53bf3c7 100644 --- a/Chapter1 - iOS/1.48.md +++ b/Chapter1 - iOS/1.48.md @@ -1177,7 +1177,7 @@ c 数组指针 `array()->lists + addedCount` 可以代表其中的位置。 过程如下 -![](./../assets/runtime-categoryattachLists.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-categoryattachLists.png) 结果就是类方法列表中,最前面的就是所有分类的方法列表,最后是类自身的方法列表。 @@ -2822,7 +2822,7 @@ void _object_set_associative_reference(id object, void *key, id value, uintptr_t 梳理后,如下图所示: - + AssociationsManager 管理的 AssociationHashMap 结构如下: @@ -2939,7 +2939,7 @@ NS_ASSUME_NONNULL_END ### 声明私有方法 - + diff --git a/Chapter1 - iOS/1.49.md b/Chapter1 - iOS/1.49.md index 87dc8ff..a56066d 100644 --- a/Chapter1 - iOS/1.49.md +++ b/Chapter1 - iOS/1.49.md @@ -13,6 +13,8 @@ MVC 模式下,软件被划分为视图(View:用户界面)、控制器( 所有的通信都是单向的。 + + ## MVP MVP 模式将 Controller 改名为 Presenter,通信改变了通信方向 @@ -23,6 +25,10 @@ MVP 模式将 Controller 改名为 Presenter,通信改变了通信方向 2. Model 与 View 不发生联系,都通过 Presenter 传递 3. View 层非常薄。不部署任何业务逻辑,称为“被动视图(Passive View)”,即没有任何主动性,而 Presenter 非常厚,所有的逻辑都部署在这层 +如果 Presenter 这一层很厚的话,可以继续拆,比如再增加一个 Interactor 的角色,专门处理处理 View 相关响应事件。这样子角色更多,职责也更清晰。维护也方便。 + + + ## MVVM MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。 @@ -52,11 +58,14 @@ MVVM 就是 MVC 的增强版,将展示层逻辑单独拎出来,即 ViewModel - MVVM 兼容当下的 MVC 机构 - MVVM 增加应用的可测试性 -- MVVM 配合一个绑定机制效果最好 +- MVVM 配合一个绑定机制效果最好 + + ## 一个简单的例子 + PersonModel -``` +```objective-c @interface Person : NSObject - (instancetype)initwithSalutation:(NSString *)salutation firstName:(NSString *)firstName lastName:(NSString *)lastName birthdate:(NSDate *)birthdate; @@ -69,7 +78,7 @@ PersonModel @end ``` PersonViewController -``` +```objective-c - (void)viewDidLoad { [super viewDidLoad]; @@ -87,7 +96,7 @@ PersonViewController 上面是标准的 MVC。现在我们考虑用 ViewModel 增加改进下 PersonViewModel -``` +```objective-c @interface PersonViewModel : NSObject - (instancetype)initWithPerson:(Person *)person; @@ -124,17 +133,15 @@ PersonViewModel ``` 此时,我们的 ViewController 会很轻量 -``` +```objective-c - (void)viewDidLoad { [super viewDidLoad]; self.nameLabel.text = self.viewModel.nameText; self.birthdateLabel.text = self.viewModel.birthdateText; } ``` - - 可测试?View Controller 是出了名的难测试。因为传动的 VC 很重,在 MVVM 的世界里,代码各司其职,测试 View Controller 变得较为容易 -``` +```objective-c SpecBegin(Person) NSString *salutation = @"Dr."; NSString *firstName = @"first"; diff --git a/Chapter1 - iOS/1.61.md b/Chapter1 - iOS/1.61.md index 954c358..2dc4145 100644 --- a/Chapter1 - iOS/1.61.md +++ b/Chapter1 - iOS/1.61.md @@ -2,7 +2,7 @@ ## 启动分类 -- 冷启动(Cold Launch):点击 App 图标启动前,进程不在系统中。需要系统新创建一个进程并加载 Mach-O 文件,dyld 从 Mach-O 头信息中读取依赖(undefined的动态库库),从动态库共享缓存中读取并链接,经历一次完整的启动过程。 +- 冷启动(Cold Launch):点击 App 图标启动前,进程不在系统中。需要系统新创建一个进程并加载 Mach-O 文件,dyld 从 Mach-O 头信息中读取依赖(Load Commands),从动态库共享缓存中读取并链接,经历一次完整的启动过程。(dyld 加载 Mach-O 完整的流程可以查看我[另一篇文章](./1.91.md)) - 热启动(Warm Launch):App 在冷启动后,用户将 App 退后台。此阶段,App 的进程还在系统中,用户重新启动进入 App 的过程,开发对该阶段能做的事情非常少。 所以我们聊启动时间优化,通常是指冷启动。此外启动慢,也就是看到主页面的过程很慢,都是发生在主线程上的。 @@ -13,6 +13,8 @@ - 如果需要更详细的信息,那就将 `DYLD_PRINT_STATISTICS_DETAILS` 设置为1 + + ## 启动阶段划分 App 冷启动可以划分为3大阶段: @@ -37,9 +39,11 @@ main () { } ``` + + ## 第一阶段:进程创建到 main 函数执行(dyld、runtime) -![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/AppLaunchingTime.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AppLaunchingTime.png) 这一阶段主要包括 dyld 和 Runtime 、main 函数的工作。 @@ -187,7 +191,44 @@ void _objc_init(void){ } ``` -方法注释说的很明白,被 dyld 所调用 +方法注释说的很明白,被 dyld 所调用。 + +dyld 加载解析 Mach-O,根据 Mach-O 的 Load Commands 读取所需的动态库、静态库去加载。从 Mach-O 的 `LC_LOAD_DYLINKER` load command 中,根据 name 路径信息,然后加载,dyld 开始执行。它的主要任务是加载应用程序的主可执行文件以及其他依赖的动态库。 + +对于动态库,分为系统动态库和开发者写的动态库: + +- 系统共享缓存:系统动态库,Apple 为了启动优化在 iOS 3.1及以后的版本中,Apple 将系统库文件打包合并成一个大的缓存文件,存放在 `/System/Library/Caches/com.apple.dyld/ ` 目录下,以减少冗余并优化内存使用。 + - 检查共享缓存:当应用程序启动时,dyld 首先检查共享缓存中是否已经包含了所需的系统库。共享缓存是一种优化机制,用于存储系统级别的动态库,以便多个应用程序可以重用这些库,减少内存占用并加快加载速度。 + - 映射到地址空:如果动态库在共享缓存中,dyld 将直接从缓存中映射库到应用程序的地址空间,而不是从磁盘加载 + - 验证和解密:对于加密的 Mach-O 文件(例如应用商店发布的应用程序),dyld 将解密并验证代码签名以确保安全性 + - Rebase & Binding:由于存在 ASLR 和系统共享缓存库的存在,dyld 会进行 rebase 和 binding。解析符号真正的地址。具体可以看[FishHook 原理](./1.88.md) 和[DYLD 及 Mach-O ](./1.91.md) 文章 +- 开发者编写的动态库: + - 解析依赖:dyld 从应用程序的主可执行文件开始,解析出所有依赖的动态库,包括开发者添加的自定义动态库。 + - 加载 Mach-O 文件:对于每个依赖的动态库,dyld 会找到对应的 Mach-O 文件,并进行加载。 + - 读取和映射:dyld 打开并读取 Mach-O 文件,然后使用 mmap 系统调用来将文件的内容映射到内存中。 + - 依赖递归加载:如果动态库本身还依赖其他库,dyld 会递归地加载这些依赖库。 + - 符号解析和绑定:与系统库类似,dyld 也会对自定义动态库进行 Rebase 和 Binding,确保所有符号引用都是正确的。 + - 初始化:加载完成后,dyld 会调用动态库中的初始化代码,例如 C++ 的静态构造函数和 Objective-C 的 +load 方法 + + + +到了 dyld3 之后,带来了**启动闭包**技术。 + +dyld 会首先创建启动闭包,闭包是一个缓存,用来提升启动速度的。既然是缓存,那么必然不是每次启动都创建的,只有在重启手机或者更新/下载 App 的第一次启动才会创建。闭包存储在沙盒的 tmp/com.apple.dyld 目录,清理缓存的时候切记不要清理这个目录 + +闭包是怎么提升启动速度的呢?我们先来看一下闭包里都有什么内容: + +- dependends:依赖动态库列表 +- fixup:bind & rebase 的地址 +- initializer-order:初始化调用顺序 +- optimizeObjc: Objective C 的元数据 +- 其他:main entry, uuid… + +为什么闭包能提高启动速度呢? + +这些信息是每次启动都需要的,把信息存储到一个缓存文件就能避免每次都解析,尤其是 Objective C 的运行时数据(Class/Method...)解析非常慢 + + ### Runtime @@ -203,6 +244,8 @@ void _objc_init(void){ 到此为止,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被 Runtime 所管理 + + ## 第二阶段:main 函数到 didFinishLaunchingWithOptions APP的启动由 dyld 主导,将可执行文件加载到内存,顺便加载所有依赖的动态库。并由Runtime 负责加载成 objc 定义的结构。所有初始化工作结束后,dyld 就会调用 main 函数 @@ -211,6 +254,8 @@ APP的启动由 dyld 主导,将可执行文件加载到内存,顺便加载 AppDelegate 的`application:didFinishLaunchingWithOptions:` 方法。此阶段主要是各种 SDK 的注册、初始化、APM 监控系统的启动、热修复 SDK 的设置、App 初始化数据的加载、IO 等等。 + + ## 第三阶段:`didFinishLaunchingWithOptions` 到业务首页加载完成 这部分主要是首页渲染相关逻辑,不同的场景,首页的定义可能不一样。比如未登录的时候首页就是登陆页、否则就是某个主页 @@ -219,13 +264,27 @@ AppDelegate 的`application:didFinishLaunchingWithOptions:` 方法。此阶段 - 渲染数据的计算 + + ## 启动优化 ### 第一阶段 #### dyld -- 减少动态库加载,合并一些动态库。(定期清理不必要的动态库,iOS 规定开发者写的动态库不能超过6个) +- 减少动态库数量:过多的动态库会增加 dyld 的解析和加载时间。优化库的依赖关系,合并功能相似的库。(定期清理不必要的动态库,iOS 规定开发者写的动态库不能超过6个) +- 使用静态库:考虑将动态库转换为静态库,以减少运行时的加载和链接时间。 +- 懒加载:对于非启动必需的动态库,可以推迟到实际需要时再加载。 +- 减少 Objective-C 元数据:Objective-C 的类、分类和选择器数量会影响 dyld 的 Binding 时间。减少这些元数据的数量可以加快启动速度 +- 优化 C++ 虚函数:C++ 中的虚函数需要在运行时进行解析,这会增加 dyld 的工作量。尽量减少虚函数的使用或使用其他设计模式替代 +- 利用 Swift 结构体:Swift 的结构体是值类型,它们在编译时会进行优化,减少运行时的符号解析需求 +- 优化 +load 方法:Objective-C 中的 +load 方法会在类或分类加载时执行,这可能会影响启动速度。尽量避免在 +load 方法中执行耗时操作,或者使用 +initialize 方法替代,后者只有在类被实际使用时才会调用 +- 二进制重排:接下去单独的篇章会讲。 +- 利用 dyld 缓存:iOS 13 引入的 dyld 3 可以生成“启动闭包”(launch closure),预先处理一些加载和链接工作,加快启动速度。 +- 使用 Xcode 的分析工具:利用 Xcode 的分析工具识别启动过程中的性能瓶颈。单点问题单点追踪分析。 +- 关注 dyld 版本变更:iOS 13 引入了 dyld 3,它在性能上有所改进,但也可能带来兼容性问题。了解 dyld 版本变更对 App 启动性能的影响,如果有需要,请根据需要进行适配。 + + #### Runtime @@ -244,14 +303,24 @@ AppDelegate 的`application:didFinishLaunchingWithOptions:` 方法。此阶段 ### 第二阶段 - 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在 `application:didFinishLaunchingWithOptions` 方法中 -- SDK 初始化遵循规范 -- 任务启动器 +- SDK 初始化遵循规范。什么时候注册、什么时候启动 +- 任务启动器:其实就是按照一定的规则进行任务编排。将任务分类: + - 那些任务是需要在 App 启动完成前主线程同步执行的 + - 那些任务是需要在 App 启动完成前主线程异步执行的 + - 那些任务是需要在 App 启动完成后编排的 + - 闲时主线程队列(监听 runloop 状态,`KCFRunLoopBeforeWaiting` 时执行,在 `KCFRunLoopAfterWaiting` 时停止) + - 异步串行队列 + - 异步并行队列 + - 闲时异步串行队列 + - + - 二进制重排 - 方法耗时统计(time profiler、os_signpost) AppDelegate 中 `application:didFinishLaunchingWithOptions` 函数阶段一般有很多业务代码介入,大多数启动时间问题都是在此阶段造成的。 - 比如二方、三方 SDK 的注册、初始化。这其中有些 SDK 比如 APM 是有必要放在很早的阶段。有些则不需要,比如日志 SDK 的初始化 +- 梳理业务,非必要的延迟加载、启动 ### 第三阶段 @@ -277,17 +346,39 @@ QA: Framework 只是打包方式,动态、静态库都可以支持。甚至可以存放资源文件。 + + ## 二进制重排 -### 虚拟内存、物理内存、内存分页 +### 虚拟内存、物理内存、内存分页、ASLR -应用程序的可执行文件是存储在磁盘上的,启动应用,则需要将可执行文件加载进内存,早期计算机是没有虚拟内存的,一旦加载就会全部加载到内存中,并且进程都是按照顺序排列的,这样存在两个问题: +早期:应用程序的可执行文件是存储在磁盘上的,启动应用,则需要将可执行文件加载进内存,早期计算机是没有虚拟内存的,一旦加载就会全部加载到内存中,并且进程都是按照顺序排列的,这样存在两个问题: -- 上一个进程只需要加一些地址就能访问到下一个进程,安全性很低 +- 当前进程只需要在合适的地址基础上,加减一些地址,就能访问到下一个进程所使用的内存,安全性很低 - 当前软件越来越大,开启多个软件时会直接全部加载到内存中,导致内存不够用。而且使用软件的时候大多数情况只使用部分功能。如果软件一打开就全部加载到内存中,会造成内存的严重浪费。 -基于上述2个问题,诞生了虚拟内存技术。 +基于上述2个问题,诞生了虚拟内存技术。App 进程通过内存管理单元(Memory Management Unit, MMU)来管理的。MMU 使用一张特殊的表来跟踪虚拟内存地址和物理内存地址之间的映射关系。这张表通常被称为页表(Page Table) + +内存分页: + +- 页表结构:页表包含了一系列的表项,每个表项对应虚拟内存中的一个页(通常是 4KB)。每个表项包含了该虚拟页对应的物理页的信息,包括物理页的起始地址和一些状态信息。 +- 虚拟地址到物理地址的转换:当程序访问一个虚拟地址时,MMU 查看页表来找到对应的表项,然后根据表项中的信息将虚拟地址转换为物理地址。 +- 页表缓存(TLB - Translation Lookaside Buffer):为了提高地址转换的速度,MMU 通常会有一个 TLB,它缓存了最近访问的页表项。这样,对于频繁访问的地址,转换过程可以更快地完成 +- 页错误处理:如果一个虚拟地址在页表中没有对应的条目,就会发生页错误。操作系统的页错误处理程序会被调用,它负责加载缺失的页到物理内存中,并更新页表。 +- 内存分配:当应用程序请求内存时,操作系统会分配虚拟地址空间,并在页表中创建相应的条目。如果需要,操作系统还会分配物理内存页,并将其与虚拟页关联起来。 + +当物理内存满的时候,会发生覆盖。用户使用的活跃的数据,覆盖内存中最不活跃的数据那一页。对应现实的表现就是:iPhone 上永远可以较好的打开一个 App,比如打开了 App1、App2、App3...,等打开使用了一阵子后,内存紧张,我们尝试切换打开最早的 App1,会发现 App1 重新启动了,之前用的功能 A 的页面1,已经不见了,经历一个新的启动流程。 + + + +但虚拟内存方案带来一个问题。比如黑客不断探索发现,某个重要的功能位于第3页,是不是完全可以通过固定的地址去访问?? + +因为早期物理内存方案下,App 启动后位于什么地址是不确定的。有了虚拟内存后,App 内符号的地址都是从0到4G,都是相对地址。 + +为了解决该问题,Apple 又推出了 ASLR 技术。核心就是为了解决虚拟内存地址固定不变的问题。就不能通过固定的地址访问内存或者数据了。 + + ### 内存缺页异常 @@ -299,9 +390,11 @@ CPU 不直接和物理内存打交道,而是通过 MMU(Memory Manage Unit, iOS 程序在进行加载时,会根据一 page 大小16kb 将程序分割为多页,启动时部分的页加载进真实内存,部分页还在磁盘中,中间的调度记录在一张内存映射表(Page Table),这个表用来调度磁盘和内存两者之间的数据交换。 -![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/iOSPageInPageOut.png) + + +如上图,App 运行时执行某个任务时,会先访问虚拟页表,如果页表的标记为1,则说明该页面数据已经存在于内存中,可以直接访问。如果页表为0,则说明数据未在物理内存中,这时候系统会阻塞进程,叫做**缺页中断(page fault)**,进程会从用户态切换到内核态,并将缺页中断交给内核的 `page Fault Handler` 处理。等将对应的 page 从磁盘加载到内存之后再进行访问,这个过程叫做 page in。1次缺页异常耗时较少,用户感知不到,但是在 App 启动阶段,容易发生缺页异常,如果发生几十次、几百次,这对于 App 启动时间来说,影响较大。 + -如上图,App 运行时执行某个任务时,会先访问虚拟页表,如果页表的标记为1,则说明该页面数据已经存在于内存中,可以直接访问。如果页表为0,则说明数据未在物理内存中,这时候系统会阻塞进程,叫做缺页中断(page fault),进程会从用户态切换到内核态,并将缺页中断交给内核的 page Fault Handler 处理。等将对应的 page 从磁盘加载到内存之后再进行访问,这个过程叫做 page in。 因为磁盘访问速度较慢,所以 page in 比较耗时,而且 iOS 不仅仅是将数据加载到内存中,还要对这页做 Code Sign 签名认证,所以 iOS 耗时更长 @@ -309,7 +402,7 @@ Tips:Code Sign 加密哈希并不少针对于整个文件,而是针对于 等到程序运行时用到了才去内存中寻找虚拟地址对应的页帧,找不到才进行分配,这就是内存的惰性(延时)分配机制。 -![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/PageFault.png) + 为了提高效率和方便管理,对虚拟内存和物理内存进行分页(Page)管。进程在访问虚拟内存的一个 page 而对应的物理内存却不存在(没有被加载到物理内存中),则会触发一次缺页异常(缺页中断),然后分配物理内存,有需要的话会从磁盘 mmap 读入数据。 @@ -317,23 +410,50 @@ Tips:Code Sign 加密哈希并不少针对于整个文件,而是针对于 二进制重排提升 App 启动速度是通过「解决内存缺页异常」(内存缺页会有几毫秒的耗时)来提速的。 +一个 App 发生大量「内存缺页」的时机就是 App 刚启动的时候。所以优化手段就是「将影响 App 启动的方法集中处理,放到某一页或者某几页」(虚拟内存中的页)。 + +Xcode 工程允许开发者指定 「Order File」,可以「按照文件中的方法顺序去加载」。 + + + +核心就是:二进制重排提升 App 启动速度是通过「解决内存缺页异常」(内存缺页会有几毫秒的耗时)来提速的。 + 一个 App 发生大量「内存缺页」的时机就是 App 刚启动的时候。所以优化手段就是「将影响 App 启动的方法集中处理,放到某一页或者某几页」(虚拟内存中的页)。Xcode 工程允许开发者指定 「Order File」,可以「按照文件中的方法顺序去加载」,可以查看 linkMap 文件(需要在 Xcode 中的 「Buiild Settings」中设置 Order File、Write Link Map Files 参数)。 + + ### 如何获取启动阶段的 Page Fault 次数 Instrucments 中的 System Trace 可以查看详细信息。 -### 如何验证重排是否成功 -查看 LinkMap。发现方法展示顺序是按照,写代码的顺序展示的。 -![image-20200814211215976](/Users/lbp/Library/Application Support/typora-user-images/image-20200814211215976.png) +### 获取符号顺序 + +可以查看 linkMap 文件(需要在 Xcode 中的 「Buiild Settings」中设置 `Order File`、`Write Link Map File` 参数)。 + +设置 Xcode: `Build Settings` -> `Write Link Map Files` 为 YES。 + + + +分析: + +- 可以看到符号顺序是根据链接顺序来决定的 +- 符号的链接顺序并非是 App 方法真正的执行顺序。 + +可以调整下 Person 类中2个方法的顺序,也可以看到新生成的 linkMap 符号顺序变了。 + + + +所以是有空间进行操作的,让符号链接顺序按照 App 启动阶段方法执行顺序来进行,这个抓手就是 `Order File`。 + + ### 有没有办法将 App 启动需要的方法集中收拢? 其实二进制重排 Apple 自己本身就在用,查看 `objc4` 源码的时候就发现了身影 -![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/Objc-OrderFile.png) + Xcode 使用的链接器为 `ld`。 ld 有个参数 `-order_file` 。order_file 中的符号会按照顺序排列在对应 section 的开始。 @@ -343,47 +463,72 @@ Xcode 的 Build Setting GUI 面板也支持配置。 2. 如果你给 Xcode 工程根目录下指定一个 order 文件,比如 `refine.order`,则 Xcode 会按照指定的文件顺序进行二进制数据重排。分析 App 启动阶段,将优先需要加载的函数、方法,集中合并,利用 Order File,减小缺页异常,从而减小启动时间。 + + +### 实践 + +第一步:编写 `.order` 文件。编写顺序是结合业务逻辑和代码顺序,然后再原始的 linkmap 文件中,将符号复制,写入到新创建的 `.order` 文件中。 + +第二步:Build Settings -> Order File 中设置 `.order` 文件的位置。 + +第三步:编译,查看新的 linkmap 文件,验证符号编译顺序是否和 order file 一致。 + + + + + + + ### 如何拿到启动时刻所调用的所有方法名称 -clang 插桩,才可以 hook OC、C、block、Swift 全部。LLVM 官方文档。 +二进制的原理很简答。最核心的、最难的就是如何获取到 App 启动阶段的所有方法。可能有 OC、Swift、C/C++ 的方法。 - 二进制重排提升 App 启动速度是通过「解决内存缺页异常」(内存缺页会有几毫秒的耗时)来提速的。 +fishhook `objc_msgSend` 只可以拿到所有的 OC 符号。那 c/c++、Swift、block 怎么拿到? -一个 App 发生大量「内存缺页」的时机就是 App 刚启动的时候。所以优化手段就是「将影响 App 启动的方法集中处理,放到某一页或者某几页」(虚拟内存中的页)。Xcode 工程允许开发者指定 「Order File」,可以「按照文件中的方法顺序去加载」,可以查看 linkMap 文件(需要在 Xcode 中的 「Buiild Settings」中设置 Order File、Write Link Map Files 参数)。 +Clang 插桩,才可以 hook OC、C/C++、block、Swift 全部的方法调用。 -其实难点是如何拿到启动时刻所调用的所用方法?代码可能是 Swift、block、c、OC,所以hook 肯定不行、fishhook 也不行,用 clang 插桩可行 + + +其实难点是如何拿到启动时刻所调用的所用方法?代码可能是 Swift、block、c、OC,所以hook 肯定不行、fishhook 也不行,用 clang 插桩可行。 + +在 [Clang 10 documentation](https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs) 中可以看到 LLVM 官方对 SanitizerCoverage 的详细介绍,包含了示例代码。 + +简单来说 SanitizerCoverage 是 Clang 内置的一个代码覆盖工具。它把一系列以 `__sanitizer_cov_trace_pc_` 为前缀的函数调用插入到用户定义的函数里,借此实现了全局 AOP 的大杀器。其覆盖之广,包含 Swift/Objective-C/C/C++ 等语言,Method/Function/Block 全支持。 + +也可以看[精准测试最佳实践](./1.108.md)这篇文章,查看详细插桩原理。 步骤: -- 在 Xcode Build Setting 下搜索 “Other c Flags”,在后面添加 `-fsanitize-coverage=trace-pc-guard` +- 在 Xcode Build Setting 下搜索 “Other C Flags”,在后面添加 `-fsanitize-coverage=trace-pc-guard`。如果观察包含 Swift 代码,还需要在 “Other Swift Flags” 中加入 `-sanitize-coverage=func` 和 `-sanitize=undefined`。所有链接到 App 中的二进制都需要开启 SanitizerCoverage,这样才能完全覆盖到所有调用 + + 如果是 Cocoapods 管理。可以脚本处理 + + ```ruby + post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['OTHER_CFLAGS'] = '-fsanitize-coverage=func,trace-pc-guard' + config.build_settings['OTHER_SWIFT_FLAGS'] = '-sanitize-coverage=func -sanitize=undefined' + end + end + end + ``` - 在工程入口文件添加2个方法来解决编译报错问题 `__sanitizer_cov_trace_pc_guard_init`、`__sanitizer_cov_trace_pc_guard` - clang 插桩原理就是给每个(oc、c)方法、block 等方法内部第一行添加 hook 代码,来实现 AOP 效果。所以在 `__sanitizer_cov_trace_pc_guard` 内部将函数的名称打印出来,最后可以统一写入 order 文件 - ```objectivec - void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, - uint32_t *stop) { - static uint64_t N; // Counter for the guards. - if (start == stop || *start) return; // Initialize only once. - printf("INIT: %p %p\n", start, stop); - for (uint32_t *x = start; x < stop; x++) - *x = ++N; // Guards should start from 1. - } + - void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { - void *PC = __builtin_return_address(0); - Dl_info info; - dladdr(PC, &info); - printf("name: %s\n", info.dli_sname); - } - ``` +- 收集 App 启动过程中的函数调用,生成 `.order` 文件 -- 可能会存在 while、for 循环 case,所以为了避免死循环,需修改 "Other c Flags" 配置为 `-fsanitize-coverage=func,trace-pc-guard`。func 表示仅 hook 函数时调用 + 做了个封装,假设我们 App 启动完成的重点是 AppDelegate 的 `didFinishLaunchingWithOptions` 方法。在这里一行调用,便可获得 App 启动阶段的 `.order` 文件。 -- 最后修改 Build Setting 中的 "Order File" 配置项 + +- 最后修改 Build Setting 中的 "Order File" 配置项,值为 `.order` 文件的路径信息。 +完整 Demo 可以查看 [BlogDemos:BinarayOrderExplore](https://github.com/FantasticLBP/BlogDemos/tree/master/BinarayOrderExplore) diff --git a/Chapter1 - iOS/1.68.md b/Chapter1 - iOS/1.68.md index f567cfc..a51d021 100644 --- a/Chapter1 - iOS/1.68.md +++ b/Chapter1 - iOS/1.68.md @@ -1,25 +1,441 @@ # 守护你的App安全 -App Crash 会严重影响用户体验,Crash 率和客户端工程师的个人评级和绩效考核挂钩。因此写出的代码必须安全可靠。崩溃率要保持在一个什么样的水平以下。 - -App 在不断业务迭代,经过多人开发维护后可能因为开发者的水平层次不齐,造成代码质量较低,所以要实现的效果就是保证 App 稳健运行,常见的问题捕获处理,将造成奔溃或者 Crash 的因素处理掉,让 App 正常运行。 - - -## 功能设想 - -对业务代码零侵入性地将原本会导致 App 奔溃的 crash 信息处理掉,保证 App 正常稳健运行,再将 Crash 信息提取出来呈现给开发者。开发者可以根据相应的 Crash 信息去处理解决对应的代码。 +> 从 Web 安全一样,所有的攻防离不开一句话“在合理范围内保证 App 安全,让攻击者增加破解成本,让一部分人三思而后行战术性放弃”。 +## ptrace 简易版本 + +在iOS系统中,`ptrace` 被用于防止应用程序被调试。`ptrace` 函数提供了一种机制,允许一个进程监听和控制另一个进程,并且可以检测被控制进程的内存和寄存器中的数据。在iOS开发中,`ptrace` 可以用于实现断点调试和系统调用跟踪,但它也常被用于反调试措施 + +通过传递 `PT_DENY_ATTACH` 标志,它允许应用程序设置一个标志,以防止其他调试器附加。如果其他调试器尝试附加,则进程将终止。 -## KVO +可以使用类似下面的代码来防止别人破解、逆向。 +```objective-c +#import +__BEGIN_DECLS + int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data); +__END_DECLS + +void disable_gdb(void) { + ptrace(PT_DENY_ATTACH, 0, 0, 0); +} + +int main(int argc, char * argv[]) { + NSString * appDelegateClassName; + @autoreleasepool { +#if DEBUG + // 非 DEBUG 模式下禁止调试 + disable_gdb(); +#endif + // Setup code that might create autoreleased objects goes here. + appDelegateClassName = NSStringFromClass([AppDelegate class]); + } + return UIApplicationMain(argc, argv, nil, appDelegateClassName); +} ``` - *** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer for the key path "name" from because it is not registered as an observer.' -*** First throw call stack: -(0x2045ac518 0x2037879f8 0x2044b6c70 0x204f52470 0x204f5222c 0x101d8ed68 0x2309ad230 0x230456af8 0x100f0edb0 0x230456e18 0x230455e84 0x2309e429c 0x2309e54c4 0x2309c5534 0x230a8b7c0 0x230a8deec 0x230a8711c 0x20453e2bc 0x20453e23c 0x20453db24 0x204538a60 0x204538354 0x20673879c 0x2309abb68 0x101c7086c 0x203ffe8e0) -libc++abi.dylib: terminating with uncaught exception of type NSException -``` \ No newline at end of file + + + +## ptrace 安全吗 + +但上述方式安全吗?一个简单的 fishhook 都可以破解掉。 + +第一步:创建一个 AppHook 的动态库,和一个 AppHookProtoctor 的 iOS App + +第二步:AppHook 里面在 `+load` 方法里使用 fishhook 对 ptrace 进行 hook,判断 `PT_DENY_ATTACH` 则绕过 + +```c++ +int hooked_ptrace(int _request, pid_t _pid, caddr_t _addr, int _data) { + if (_request == PT_DENY_ATTACH) { + return 0; + } + return ptrace_pointer(_request, _pid, _addr, _data); +} +``` + +第三步:在 App 的 main.m 中调用 disable_gdb 来禁止调试。 + + + +结果:可以看到对 ptrace 使用 fishhook hook 之后,ptrace 并没有让 App 进程结束掉。也就是 ptrace 失效了,并不安全。 + + + +## ptrace 安全性改进 + +我们知道 fishhook 的原理是根据符号表进行 rebind 的,那是不是可以通过该原理绕开? + +`ptrace `是系统函数,dyld 会在启动阶段进行 rebase、rebind,遍历 Mach-O 文件的 `__DATA` 段中的 `__nl_symbol_ptr` 和 `__la_symbol_ptr` 两个 section。通过 Indirect Symbol Table、Symbol Table 和 String Table 的配合,Fishhook 能够找到需要替换的函数,并修改其地址。想了解 fishhook 详细工作原理可以查看这篇文章:[fishhook 原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.88.md) + + + +我们禁止 fishhook,对 Xcode 添加一个符号断点 `ptrace`,如下所示。 + + + +我们可以看到 ptrace 位于 `libsystem_kernel.dylib` 动态库中。lldb 模式下通过 `image list` 查看所有的 image 信息。 + +可以看到当前电脑模拟器运行情况下,libsystem_kernel.dylib 位于 `/usr/lib/system/libsystem_kernel.dylib` 路径。这个路径是我电脑调试环境下的路径。真机路径不一样。 + +通过 dlopen、dlsym 的方式来找到 ptrace 符号地址,再去执行,这种方式的本质是没有走符号表的流程。 + +Demo 如下: + +```objective-c +#ifndef DEBUG + // 非 DEBUG 模式下禁止调试 + char *ptraceLibPath = "/usr/lib/system/libsystem_kernel.dylib"; + void *handler = dlopen(ptraceLibPath, RTLD_LAZY); + int (*ptrace_pointer)(int _request, pid_t _pid, caddr_t _addr, int _data); + ptrace_pointer = dlsym(handler, "ptrace"); + if (ptrace_pointer) { + ptrace_pointer(PT_DENY_ATTACH, 0, 0, 0); + } +#endif +``` + + + +工程运行后会发现,App 启动后立马 crash 结束进程。说明这种(通过 dlsym 找到符号地址) ptrace 的防护是有效的 + + + +对代码进行修改,整洁一些,如下所示: + +```c++ +typedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data); + +void disable_gdb(void) { + // 简易版:容易被 FishHook 进行符号表的修改,从而破解 ptrace 的拦截 + // ptrace(PT_DENY_ATTACH, 0, 0, 0); + + // 安全版本 + void *handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW); + ptrace_ptr_t ptrace_ptr = dlsym(handle, "ptrace"); + ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0); + dlclose(handle); +} +``` + + + + + +该方式通过 dlopen、dlsym 的方式延迟、动态的找到 ptrace 的符号地址,没有走符号表的逻辑,避开了 fishhook 的工作流程,从而更安全一些。 + + + + + +## sysctl 简易版本 + +`sysctl` 函数是一个系统调用,用于获取或设置系统相关的信息。这个函数提供了一种机制来查询或修改系统的状态信息,比如系统配置参数、统计数据等。 + +在 Linux 和类 Unix 系统中用于查看和修改内核参数。然而,在 iOS 逆向工程中,`sysctl` 也常被用于检测应用程序是否正在被调试 + +```c++ +int sysctl(int *, u_int, void *, size_t *, void *, size_t); +``` + +参数解释: + +- `name`: 一个指向整数数组的指针,数组中的每个元素代表一个级别的OID(对象标识符),用于指定要查询或设置的系统信息。 +- `namelen`: `name` 数组的长度,即OID的级别数。 +- `oldp`: 一个指向缓冲区的指针,用于接收查询到的现有值。如果设置值,这个参数可以是NULL。 +- `oldlenp`: 一个指向 `size_t` 的指针,用于指定 `oldp` 缓冲区的大小,并在调用后返回实际读取的数据大小。 +- `newp`: 一个指向新值的指针,用于设置系统信息。如果只是查询,这个参数可以是NULL。 +- `newlen`: 新值的大小 + +返回值: + +- 如果成功,返回0 +- 如果失败,返回 -1 + +其中传递的结构体引用,`info.kp_proc.p_flag` 字段,用于判断进程是否处于调试状态。是二进制的0、1。第12位,为1代表处于调试状态。反之不是。 + +思考:如何正确二进制判断某一位是0还是1?用特定位置填充1,其他位填充0来处理。按位与之后,特定位置为1,说明之前是1,否则就是0. + + + +```c++ +#define P_TRACED 0x00000800 /* Debugged process being traced */ +``` + +`info.kp_proc.p_flag` 判断系统提供了一个 `P_TRACED`,按位与用来判断是否是调试模式。 + + + + + +使用 + +```objective-c +bool isInDebugMode(void) { + int name[4]; + name[0] = CTL_KERN; // 内核 + name[1] = KERN_PROC; // 查询进程 + name[2] = KERN_PROC_PID; // 通过进程 id 来查找 + name[3] = getpid(); + + struct kinfo_proc info; // 接收查询信息,利用结构体传递引用 + size_t infoSize = sizeof(info); + int resultCode = sysctl(name, sizeof(name)/sizeof(*name), &info, &infoSize, 0, 0); + assert(resultCode == 0); + return info.kp_proc.p_flag & P_TRACED; +} +``` + +结合定时器,每间隔1秒进行检查一下,运行起来发现处于 debug 模式,则调用 `exit(0)` 结束进程。 + + + +为什么调用 `exit`而不是 `abort`? + +- exit(0):函数是C标准库中的一个函数,用于正常退出程序。当调用时,程序会执行清理操作,比如关闭打开的文件、释放分配的资源等。它允许程序在退出前执行一些清理工作,比如调用 `atexit` 注册的函数。`exit(0)` 表示程序正常退出,返回状态码为0。 + +- abort: 函数也是C标准库中的一个函数,但它用于异常或紧急情况下的退出。当调用时,程序会立即终止,不会进行任何清理工作,比如关闭文件或释放资源。会导致程序发送SIGABRT信号给自身,这通常用于调试目的,以便在发生严重错误时立即停止程序。表示程序是非正常退出的。 + +`exit(0)` 更适合在程序正常结束时使用,而 `abort` 更适合在发生不可恢复的错误时使用。使用 `abort` 可以快速停止程序,但可能会导致资源泄漏等问题,因为它不会执行任何清理操作。 + + + +不过我们的逻辑是,在非 debug 模式才进行这样的检测。所以用 `#ifndef DEBUG` 包装 + +```objective-c +#ifndef DEBUG + timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0)); + dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC); + dispatch_source_set_event_handler(timer, ^{ + if (isInDebugMode()) { + NSLog(@"在调试模式"); + exit(0); + } else { + NSLog(@"不在调试模式"); + } + }); + dispatch_resume(timer); +#endif +``` + + + +## sysctl 安全吗 + +`sysctl ` 是系统函数,存在间接符号表,所以可以用 fishhook 进行 hook。 + +继续用动态库 + App 的形式验证能否 hook 成功。 + + + +第一步:注册一个函数指针,用来保存 sysctl 的函数地址 + +```c++ +// sysctl 函数指针,保存原始 sysctl 函数地址 +int (*sysctl_pointer)(int *, u_int, void *, size_t *, void *, size_t); +``` + +第二步:写替换后的 sysctl 函数实现 + +```c++ +int hooked_sysctl(int *name, u_int namelen, void *info, size_t *infosize, void *newInfo, size_t newInfoSize) { + int resultCode = sysctl_pointer(name, namelen, info, infosize, newInfo, newInfoSize); + if (namelen == 4 && name[0] == CTL_KERN && name[1] == KERN_PROC && name[2] == KERN_PROC_PID && info) { + struct kinfo_proc *myInfo = (struct kinfo_proc *)info; + if (myInfo->kp_proc.p_flag & P_TRACED) { + // 异或取反。设置调试判断位为0. + myInfo->kp_proc.p_flag ^= P_TRACED; + } + return resultCode; + } + return resultCode; +} +``` + +第三步:调用 fishhook rebind_symbols 完成系统符号 `sysctl` 的 hook + +第四步:验证 hook 是否成功。如果成功,则 App 运行起来,处于 debug 模式下,还是会输出 `不在调试模式` + +结果如下: + + + + + +可以看到 fishhook 也可以 hook sysctl。所以不安全。 + + + +## sysctl 安全版本 + +修改思路参考上面的 ptrace,知道 fishhook 的原理,绕开懒加载符号表,绕开 dyld 修正符号和填充地址这个过程。 + +不再赘述,核心代码如下图所示: + + + + + +效果就是在 fishhook hook 的情况下,App 检测到处于 debug 模式下,调用 `exit(0)` 自动结束进程。 + + + +## 更安全的版本 + +### 隐藏符号名称 + +更安全的是不让分析者在 MachO 中显示的看到 ptrace、sysctl 符号名称。所以采用异或运算一个固定的 key,再根据指针指向字符串初始值,再次异或,得到原始字符串。 + +隐藏 ptrace 符号名称的方法,如下所示 + +```c++ +void disable_gdb_via_hidden_ptrace(void) { + // 使用一个 char 数组拼接一个 ptrace 字符串 (此拼接方式可以让逆向的人在使用工具查看汇编时无法直接看到此字符串) + unsigned char funcName[] = { + (KEY ^ 'p'), + (KEY ^ 't'), + (KEY ^ 'r'), + (KEY ^ 'a'), + (KEY ^ 'c'), + (KEY ^ 'e'), + (KEY ^ '\0'), + }; + unsigned char * p = funcName; + // 再次异或之后恢复原本的值 + while (((*p) ^= KEY) != '\0') p++; + + void *handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW); + ptrace_ptr_t ptrace_ptr = dlsym(handle, (const char *)funcName); + if (ptrace_ptr) { + ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0); + } + dlclose(handle); +} +``` + +隐藏 sysctl 符号的方法如下 + +```c++ +bool isInDebugModeViaHiddenSysctl(void) { + // 使用一个 char 数组拼接一个 sysctl 字符串 (此拼接方式可以让逆向的人在使用工具查看汇编时无法直接看到此字符串) + unsigned char funcName[] = { + (KEY ^ 's'), + (KEY ^ 'y'), + (KEY ^ 's'), + (KEY ^ 'c'), + (KEY ^ 't'), + (KEY ^ 'l'), + (KEY ^ '\0'), + }; + unsigned char * p = funcName; + //再次异或之后恢复原本的值 + while (((*p) ^= KEY) != '\0') p++; + + int name[4]; + name[0] = CTL_KERN; // 内核 + name[1] = KERN_PROC; // 查询进程 + name[2] = KERN_PROC_PID; // 通过进程 ID 来查找 + name[3] = getpid(); // 当前进程 ID + + struct kinfo_proc info; // 接收查询信息,利用结构体传递引用 + size_t infoSize = sizeof(info); + void *handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW); + sysctl_ptr_t sysctl_ptr = dlsym(handle, (const char *)funcName); + + sysctl_ptr(name, sizeof(name)/sizeof(*name), &info, &infoSize, 0, 0); + dlclose(handle); + return info.kp_proc.p_flag & P_TRACED; +} +``` + +会发现隐藏符号后,可以实现防止 hook 效果的。骚操作来还原符号,保证安全。 + + + + + +### 利用汇编调用系统函数 + +函数调用都可以利用 `syscall`的方式调用。 + +```c++ +syscall(SYS_ptrace,PT_DENY_ATTACH,0,0); +// 等价于 +syscall(26,31,0,0,0); +``` + +`volatile` 代表不优化此汇编代码 + +```assembly +asm volatile( + "mov x0,#26\n" + "mov x1,#31\n" + "mov x2,#0\n" + "mov x3,#0\n" + "mov x16,#0\n" //这里就是syscall的函数编号 + "svc #0x80\n" //这条指令就是触发中断(系统级别的跳转) +); +``` + +`ptrace(PT_DENY_ATTACH, 0, 0, 0);` 等价于 + +```assembly +asm volatile( + "mov x0,#31\n" //参数1 + "mov x1,#0\n" //参数2 + "mov x2,#0\n" //参数3 + "mov x3,#0\n" //参数4 + "mov x16,#26\n"//中断根据x16 里面的值,跳转ptrace + "svc #0x80\n" //这条指令就是触发中断去找x16执行(系统级别的跳转!) +); +``` + +还可以对 `exit(0)` 进行汇编混合,自定义符号 `quit_process` + +```assembly +static __attribute__((always_inline)) void quit_process () { +#ifdef __arm64__ + asm( + "mov x0,#0\n" + "mov x16,#1\n" //这里相当于 Sys_exit,调用exit函数 + "svc #0x80\n" + ); + return; +#endif +#ifdef __arm__ + asm( + "mov r0,#0\n" + "mov r16,#1\n" //这里相当于 Sys_exit + "svc #80\n" + ); + return; +#endif + exit(0); +} +``` + +最后的效果: + + + + + +完整代码可以这里: + +- [AppHook](https://github.com/FantasticLBP/BlogDemos/tree/master/AppHook) +- [AppHookProtector](https://github.com/FantasticLBP/BlogDemos/tree/master/AppHookProtector) + + + + + + + + + + + diff --git a/Chapter1 - iOS/1.7.md b/Chapter1 - iOS/1.7.md index 1010420..4afe5cd 100644 --- a/Chapter1 - iOS/1.7.md +++ b/Chapter1 - iOS/1.7.md @@ -12,7 +12,7 @@ BSS段(bss segment):通常用来存储程序中未被初始化的全局变 代码段(code segment):通常是指用来存储程序可执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量。 -![内存](./../assets/ram.png) +![内存](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ram.png) @@ -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];` 这句代码在内存分配原理如下图所示 -![解析图](./../assets/Untitled%20Diagram-2.png) +![解析图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Untitled%20Diagram-2.png) **结论** -![p1](./../assets/2017-05-15%20下午5.35.17.png) -![p2](./../assets/2017-05-15%20下午5.35.34.png) +![p1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-05-15%20下午5.35.17.png) +![p2](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-05-15%20下午5.35.34.png) **可以 看到Person类的3个对象p1、p2、p3的isa的值相同。** @@ -707,7 +707,7 @@ iOS 中,系统分配内存,都是16的倍数。pageSize?系统在分配内 GUN 都存在内存对齐这个概念。 `sizeof` 本质是运算符。在 Xcode 编译后就替换为真正的值。通过指令 `xcrun --sdk iphoneos clang -arch arm64 -S -emit-llvm ViewController.m -o ViewController.ll` 查看 IR。 - + @@ -718,21 +718,21 @@ GUN 都存在内存对齐这个概念。 `objc_getClass()` 如果传递 instance 实例对象,返回 class 类对象;传递 Class 类对象,返回 meta-class 元类对象;传递 meta-class 元对象,则返回 NSObject(基类)的 meta-class 对象 -![](./../assets/objc-isa.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/objc-isa.png) instance 的 isa 指向 Class。当调用方法时,通过 instance 的 isa 找到 Class,最后找到对象方法的实现进行调用 class 的 isa 指向 meta-class。当调用类方法的时,通过 class 的 isa 找到 meta-class,最后找到类方法进行调用。 -![](./../assets/objc-superclass.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/objc-superclass.png) 当 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` 方法并调用。 -![](./../assets/objc-metaclass-superclass.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/objc-metaclass-superclass.png) 当 Stduent 对象调用类方法的时候,先根据 isa 找到 Student 的元类对象,然后在元类对象的 superclass 找到 Person 的元类对象,再根据 Person 元类对象的 superClass 找到 NSObject 的元类对象。最后找到元类对象的方法列表,调用到对象方法。 -![](./../assets/class-isa-superclass.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/class-isa-superclass.png) ```objectivec @interface Student : NSObject @@ -937,7 +937,7 @@ class_rw_t *personMetaClassData = personClass->metaClass()->data(); 内存对齐是指数据在内存中存储时按照一定规则对齐到特定的地址上。在 iOS 开发中,内存对齐是为了提高内存访问的效率和性能。内存对齐的原因主要包括以下几点: 1. 提高访问速度:内存对齐可以使数据在内存中的存储更加高效,因为大部分计算机体系结构都要求数据按照特定的边界对齐,这样可以减少内存访问的次数,提高访问速度。CPU访问非对齐的内存时需要进行多次拼接。 如下图,比如需要读取从[2, 5]的内存,需要分别读取两次,然后还需要做位移的运算,最后才能得到需要的数据。这中间的损耗就会影响访问速度。 - + 2. 为了方便移植。CPU是一块块的进行进行内存访问。有一些硬件平台不允许随机访问,只能访问对齐后的内存地址,否则会报异常。 很多 CPU(如基于 Alpha,IA-64,MIPS,和 SuperH 体系的)拒绝读取未对齐数据。当一个程序要求这些 CPU 读取未对齐数据时,这时 CPU 会进入异常处理状态并且通知程序不能继续执行。举个例子,在 ARM,MIPS,和 SH 硬件平台上,当操作系统被要求存取一个未对齐数据时会默认给应用程序抛出硬件异常。 @@ -1017,7 +1017,7 @@ NSLog(@"%zd", malloc_size(temp)); 成员变量占用8字节对齐,每个对象的第一个都是 isa 指针,必须要占用8字节。举例一个极端 case,假设 n 个对象,其中 m 个对象没有成员变量,只有 isa 指针占用8字节,其中的 n-m个对象既有 isa 指针,又有成员变量。每个类交错排列,那么 CPU 在访问对象的时候会耗费大量时间去识别具体的对象。很多时候会取舍,这个 case 就是时间换空间。以16字节对齐,会加快访问速度(参考链表和数组的设计) 上述是 Apple 官方的角度出发探究的,其他系统,比如 Linux 也是存在内存对齐的。由于 Linux 也是采用 GNU 的东西,所以探索下 GNU 下 glibc malloc 的实现。从[这里](https://ftp.gnu.org/gnu/glibc/)下载 glibc 源码。然后拖到 Xcode 中查看 - + 可以看到 GNU 源码里面,内存对齐 `MALLOC_ALIGNMENT` 在 i386 里面是16,在非 i386 里面有个判断 @@ -1031,7 +1031,7 @@ NSLog(@"%zd", malloc_size(temp)); # define INTERNAL_SIZE_T size_t ``` 在 Xcode 打印输出, `__alignof__ (long double)` 为16,`sizeof(size_t)` 为8,即 `2 * SIZE_SZ` = 16,所以不管怎么看,在 GUN 里面内存对齐一定都是16. - + Todo: 研究探索 libmalloc 源码 diff --git a/Chapter1 - iOS/1.74.md b/Chapter1 - iOS/1.74.md index d6baf5c..d859a91 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 显示器原理](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-02-04-ios_screen_scan.png) +![老式 CRT 显示器原理](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-04-ios_screen_scan.png) 讲讲老式的 CRT 显示器的原理。 CRT 电子枪按照上面方式,从上到下一行行扫描,扫面完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;当一帧画面绘制完成后,电子枪恢复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(Vertical synchronization),简称 VSync。显示器通常以固定的频率进行刷新,这个固定的刷新频率就是 VSync 信号产生的频率。虽然现在的显示器基本都是液晶显示屏,但是原理保持不变。 -![显示器和 CPU、GPU 关系](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-02-02-screen_display_gpu.png) +![显示器和 CPU、GPU 关系](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-02-screen_display_gpu.png) 通常,屏幕上一张画面的显示是由 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 信号后,才进行新的一帧的渲染和帧缓冲区的更新。这样的几个机制解决了画面撕裂的情况,也增加了画面流畅度。但需要更多的计算资源 -![IPC唤醒 RunLoop](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-02-08-ios_vsync_runloop.png) +![IPC唤醒 RunLoop](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-08-ios_vsync_runloop.png) 答疑 @@ -48,7 +48,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di 揭秘。请看下图 -![多缓冲区显示原理](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-02-04-Comparison_double_triple_buffering.png) +![多缓冲区显示原理](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-04-Comparison_double_triple_buffering.png) 当第一次 V-Sync 信号到来时,先渲染好一帧图像放到帧缓冲区,但是不展示,当收到第二个 V-Sync 信号后读取第一次渲染好的结果(视频控制器的指针指向第一个帧缓冲区),并同时渲染新的一帧图像并将结果存入第二个帧缓冲区,等收到第三个 V-Sync 信号后,读取第二个帧缓冲区的内容(视频控制器的指针指向第二个帧缓冲区),并开始第三帧图像的渲染并送入第一个帧缓冲区,依次不断循环往复。 @@ -56,7 +56,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di ### 2. 卡顿产生的原因 -![卡顿原因](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-02-04-ios_frame_drop.png) +![卡顿原因](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-04-ios_frame_drop.png) VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容(视图创建、布局计算、图片解码、文本绘制等)。然后将计算的内容提交到 GPU,GPU 经过图层的变换、合成、渲染,随后 GPU 把渲染结果提交到帧缓冲区,等待下一次 VSync 信号到来再显示之前渲染好的结果。在垂直同步机制的情况下,如果在一个 VSync 时间周期内,CPU 或者 GPU 没有完成内容的提交,就会造成该帧的丢弃(也叫掉帧),等待下一次机会再显示,这时候屏幕上还是之前渲染的图像,所以这就是 CPU、GPU 层面界面卡顿的原因。 @@ -75,7 +75,7 @@ RunLoop 负责监听输入源进行调度处理。比如网络、输入设备、 RunLoop 状态如下图 -![RunLoop](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/4.png) +![RunLoop](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/4.png) 第一步:通知 Observers,RunLoop 要开始进入 loop,紧接着进入 loop @@ -251,9 +251,9 @@ if (sourceHandledThisLoop && stopAfterHandle) { } ``` -完整且带有注释的 RunLoop 代码见[此处](https://github.com/FantasticLBP/knowledge-kit/raw/master/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 状态](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-02-05-RunLoop.png) +![RunLoop 状态](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-05-RunLoop.png) 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-ANR](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-04-04-APM-RunLoopANR.jpg) +![RunLoop-ANR](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-04-APM-RunLoopANR.jpg) 可能很多人纳闷 RunLoop 状态那么多,为什么选择 KCFRunLoopBeforeSources 和 KCFRunLoopAfterWaiting?因为大部分卡顿都是在 KCFRunLoopBeforeSources 和 KCFRunLoopAfterWaiting 之间。比如 Source0 类型的 App 内部事件等 Runloop 检测卡顿流程图如下: -![RunLoop ANR](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-04-04-ANRRunloop.png) +![RunLoop ANR](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-04-ANRRunloop.png) 关键代码如下: @@ -371,7 +371,7 @@ while (self.isCancelled == NO) { 在计算机科学中,调用堆栈是一种栈类型的数据结构,用于存储有关计算机程序的线程信息。这种栈也叫做执行堆栈、程序堆栈、控制堆栈、运行时堆栈、机器堆栈等。调用堆栈用于跟踪每个活动的子例程在完成执行后应该返回控制的点。 维基百科搜索到 “Call Stack” 的一张图和例子,如下 -![函数调用栈](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-02-08-StackFrame.png) +![函数调用栈](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-08-StackFrame.png) 上图表示为一个栈。分为若干个栈帧(Frame),每个栈帧对应一个函数调用。下面蓝色部分表示 `DrawSquare` 函数,它在执行的过程中调用了 `DrawLine` 函数,用绿色部分表示。 可以看到栈帧由三部分组成:函数参数、返回地址、局部变量。比如在 DrawSquare 内部调用了 DrawLine 函数:第一先把 DrawLine 函数需要的参数入栈;第二把返回地址(控制信息。举例:函数 A 内调用函数 B,调用函数 B 的下一行代码的地址就是返回地址)入栈;第三函数内部的局部变量也在该栈中存储。 @@ -464,11 +464,11 @@ static mach_port_t main_thread_id; 这里有个策略,卡顿是由于主线程某个或某组方法执行过长而造成的卡顿,所以卡顿堆栈只需要分析主线程即可,但是此时是未符号化的方法(完成流程如下) -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/callStackSymbolicate'.png) + 测试过,单次抓取主线程符号耗时大概1ms左右。因此连续抓取堆栈是可取方案。 -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/callstackCostTime.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/callstackCostTime.png) 按照栈顶函数地址和当前堆栈的深度作为判断依据,如果某个堆栈栈顶地址和深度相同,则出现次数最多的一个堆栈,可以认为是一个卡顿堆栈,因为认为当发生卡顿的时候,函数堆栈大概率不会变化。 @@ -505,7 +505,7 @@ static mach_port_t main_thread_id; 上传这些信息到服务端后,APM 后端调用符号化服务的能力,符号化机器找到对应的 DSYM 文件,调用 atos 的指令进行符号化,如下效果。 -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LagStackSymbolicate.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LagStackSymbolicate.png) 系统堆栈符号化的问题,也就是获取系统符号文件的问题。可以通过 ipsw,也就是刷机所需要的固件信息。 @@ -517,7 +517,7 @@ static mach_port_t main_thread_id; 服务端聚合策略 -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CallStackGroupHash.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CallStackGroupHash.png) 找到最接近栈顶的符合占比条件的业务代码方法,作为卡顿堆栈归类 key。 @@ -551,7 +551,7 @@ curNode.costTime > (parentNode.costTime * 1.1/n) && curNode.costTime > parentNod 应用启动时间是影响用户体验的重要因素之一,所以我们需要量化去衡量一个 App 的启动速度到底有多快。启动分为冷启动和热启动。 -![App 启动时间](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-03-30-APMAppLaunch.png) +![App 启动时间](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-03-30-APMAppLaunch.png) 冷启动:App 尚未运行,必须加载并构建整个应用。完成应用的初始化。冷启动存在较大优化空间。冷启动时间从 `application: didFinishLaunchingWithOptions:` 方法开始计算,App 一般在这里进行各种 SDK 和 App 的基础初始化工作。 @@ -586,10 +586,10 @@ App 启动过程: - 程序执行:调用 main();调用 UIApplicationMain();调用 applicationWillFinishLaunching(); Pre-Main 阶段 -![Pre-Main 阶段](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-02-10-AppSpeed-PreMain.png) +![Pre-Main 阶段](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-10-AppSpeed-PreMain.png) Main 阶段 -![Main 阶段](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-02-10-AppSpeed-Main.png) +![Main 阶段](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-10-AppSpeed-Main.png) #### 2.1 加载 Dylib @@ -673,9 +673,9 @@ Main 阶段 ### 4. 精确版启动时间监控 -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/AppStarupPipeline.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AppStarupPipeline.png) -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/APMStartup.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/APMStartup.png) 进程创建:通过 sysctl 可以拿到 @@ -685,7 +685,7 @@ didFinishLaunching:通过 UIApplicationDidFinishLaunchingNotification 拿到 首屏渲染时间怎么拿?能获取到 `CA::Transaction::commit()` 时刻即可。 -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CATransactionCommit.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CATransactionCommit.png) 对 UI 进行修改后会在主线程休眠前触发 `CA::Transaction::commit()`,其实就是打包 Render Tree,通过 IPC 发送给 Render Server。 @@ -697,21 +697,21 @@ didFinishLaunching:通过 UIApplicationDidFinishLaunchingNotification 拿到 - Layout 布局:调用 `layout` 等与布局相关的 API -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CoreAnimationPipeline1.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline1.png) -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CoreAnimationPipeline2.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline2.png) - Display 绘制:调用 `drawRect` 等与绘制相关方法 -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CoreAnimationPipeline3.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline3.png) - Prepare:图片解码 - Commit:递归打包 Render Tree,通过 IPC 计数发送给 Render Server -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CoreAnimationPipeline4.png) -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CoreAnimationPipeline5.png) -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CoreAnimationPipeline6.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline4.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline5.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline6.png) 断点调试完,`[BSXPCServiceConnectionProxy invokeMethod:onTarget:withMessage:forConnection:]` 底层调用的是 send 方法。send 方法调用 `BSXPCServiceConnectionMessageReply send` 方法。 @@ -754,6 +754,26 @@ didFinishLaunching:通过 UIApplicationDidFinishLaunchingNotification 拿到 开始时间点:一般需要 Native 配合,容器页面创建好就是开始时间点。比如 iOS 的 VC `viewDidLoad`、Android 的 `onActivityCreated` 结束时间点:结束时间一般在跨端侧,比如 RN 中的组件挂载完成 componentDidMount 回调的时刻。 + + +### 6. 工单跟进 + +数据采集上报后,产生工单,自动分配到人、通知负责人和对应的群。 + +这些在其他篇章会讲。比如启动时间的工单信息类似: + +| 阶段 | 耗时 | 方法 | 业务 | 负责人 | 操作 | +| ---- | ---- | ----------------------------- | ----- | ------ | -------------- | +| T1 | 30ms | [Appdeledate handleDBUpgrade] | Goods | @张三 | 更改状态、详情 | + + + + + + + + + ## 三、 CPU 使用率监控 ### 1. CPU 架构 @@ -899,14 +919,14 @@ App 内存不足时,系统会按照一定策略来腾出更多的空间供使 Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能一个 page 持有多个对象,也可能一个大的对象跨越多个 page。通常它是 16KB 大小,且有 3 种类型的 page。 -![内存page种类](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-02-28-iOSMemoryType.png) +![内存page种类](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-28-iOSMemoryType.png) - Clean Memory Clean memory 包括 3 类:可以 `page out` 的内存、内存映射文件、App 使用到的 framework(每个 framework 都有 \_DATA_CONST 段,通常都是 clean 状态,但使用 runtime swizling,那么变为 dirty)。 一开始分配的 page 都是干净的(堆里面的对象分配除外),我们 App 数据写入时候变为 dirty。从硬盘读进内存的文件,也是只读的、clean page。 - ![Clean memory](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-02-28-iOSMemoryTypeClean.png) + ![Clean memory](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-28-iOSMemoryTypeClean.png) - Dirty Memory @@ -914,7 +934,7 @@ Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能 在使用 framework 的过程中会产生 Dirty memory,使用单例或者全局初始化方法有助于帮助减少 Dirty memory(因为单例一旦创建就不销毁,一直在内存中,系统不认为是 Dirty memory)。 - ![Dirty memory](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-02-28-iOSMemoryTypeDirty.png) + ![Dirty memory](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-28-iOSMemoryTypeDirty.png) - Compressed Memory @@ -925,7 +945,7 @@ Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能 App 运行内存 = pageNumbers \* pageSize。因为 Compressed Memory 属于 Dirty memory。所以 Memory footprint = dirtySize + CompressedSize 设备不同,内存占用上限不同,App 上限较高,extension 上限较低,超过上限 crash 到 `EXC_RESOURCE_EXCEPTION`。 -![Memory footprint](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-02-28-iOSMemoryFootprint.png) +![Memory footprint](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-02-28-iOSMemoryFootprint.png) 接下来谈一下如何获取内存上限,以及如何监控 App 因为占用内存过大而被强杀。 @@ -1720,7 +1740,7 @@ static int jetsam_do_kill(proc_t p, int jetsam_flags, os_reason_t jetsam_reason) FacekBook 提出排除法监控 OOM。 -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/Facebook-OOM.jpeg) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Facebook-OOM.jpeg) - App 更新了版本 @@ -2070,23 +2090,25 @@ for (NSInteger index = 0; index < 10000000; index++) { 全部的讲解可以看[这里](<(https://mp.weixin.qq.com/s/4-4M9E8NziAgshlwB7Sc6g)>)。对于 Memory Graph 的实现细节感兴趣的可以看这篇[文章](https://juejin.cn/post/6895583288451465230) + + ## 五、野指针/内存泄漏 ### 1. 概念定义 -1.内存泄漏会导致 OOM,那么什么是内存泄漏? +#### 内存泄漏会导致 OOM,那什么是内存泄漏? 定义:程序中已经动态分配的堆内存,由于某些原因,导致程序无法释放或者未释放内存,造成系统内存的浪费,导致程序的运行速度减慢甚至是系统奔溃等严重后果。 -一般来说导致内存泄漏的原因:对象没有释放、循环引用(无法释放) +一般来说导致内存泄漏的原因:对象没有释放(Core Foundation 对象需要手动调用 release 方法)、循环引用 -2.什么是野指针? +#### 什么是野指针? C 语言中:声明一个指针变量,但是没有初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,指向一个随机的内存空间。所以指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存 OC 语言:指针指向的内存对象已经被释放或回收了,但是指针没有置为 nil,还是指向已经回收的内存空间。 -3.什么是空指针? +#### 什么是空指针? 空指针就是没有存储任何内存地址(也就是没有指向任何对象的指针),即 nil、Nil、NULL、NSNULL、0 @@ -2098,17 +2120,20 @@ OC 语言:指针指向的内存对象已经被释放或回收了,但是指 - NSNull:数值类的空对象 -4.内存回收的本质 +#### 内存回收的本质 申请一块内存空间,其本质是向系统申请一块别人不再使用的内存空间,即已经回收的内存空间 释放一块内存空间,其本质是这个内存空间不再使用,可以由系统分配给别的对象使用。此时内存空间虽然回收了,但是原本的数据依赖的是存在的,可以理解为垃圾数据。 -5.什么是僵尸对象? +- OC 对象释放后,内存回收,表示这一块内存可以分配给别的对象了 +- 这块内存在分配给别的对象之前,仍然保留着已经释放对象的数据 + +#### 什么是僵尸对象? 僵尸对象就是指一个 OC 对象释放后所占用的内存还没被复写(重新分配给其他对象)前被称为僵尸对象。此时僵尸对象内存很不稳定,内存随时可能被系统分配给其他对象所使用。所以此时僵尸对象不应该访问和使用(调用对象的方法等) -6.为什么 OC 野指针 Crash 很多? +#### 为什么 OC 野指针 Crash 很多? App 上线前经过自测、UI 自动化、精准测试、高铁回归包测试等,但是野指针具有随机性,所以很多野指针偷偷跑到线上了。 @@ -2116,19 +2141,29 @@ App 上线前经过自测、UI 自动化、精准测试、高铁回归包测试 - 出错分支比较难进,执行不到出错的 case,所以能做的就是提高测试覆盖率 -- 即使跑进有问题的分支,但是野指针指向的地址并不一定会导致 crash。因为野指针本质是一个指向已删除的对象或受限内存区域的指针。这里 OC 野指针指的是 OC 对象释放后但指针未置空导致的野指针。dealloc 执行后只是告诉系统这篇内存我不用,但是系统并没有让这块内存不能访问。 +- 即使跑进有问题的分支,但是野指针指向的地址并不一定会导致 crash。因为野指针本质是一个指向已删除的对象或受限内存区域的指针。这里 OC 野指针指的是 OC 对象释放后但指针未置空导致的野指针。dealloc 执行后只是告诉系统这篇内存我不用,但是系统并没有让这块内存不能访问。访问的话,暂时是安全的,过了一会儿,由于内存紧张,可能被系统分配到其他对象去了。被填充了一个新的对象的信息。比如成员变量、方法等。再去访问就会 crash -### 2. Zombie Objects +#### 野指针可能存在的问题 -Zoom Object 是 Xcode 提供的一种用来检测内存问题的对象(EXC_BAD_ACCESS),它可以捕获任何尝试访问坏内存的调用。 + + +### 2. Zombie Object + +`Zombie Object` 是 Xcode 提供的一种用来检测内存问题的工具(`EXC_BAD_ACCESS`),它可以捕获任何尝试访问坏内存的调用。 + +一个对象解除了它的引用,已经被释放掉,但仍可以接收消息,就叫 zombie object 。 如果给僵尸对象发送消息,那么系统会在运行期间奔溃并在 Xcode 输出错误日志,通过日志定位到野指针对象调用的方法和类名。 -当野指针指向的僵尸对象所占用的内存还未被复写(未分配),此时是可以访问的 +- 当野指针指向的僵尸对象所占用的内存还未被复写(未分配),此时是可以访问的 -当野指针指向的僵尸对象所占用的内存被分配后,此时访问会奔溃,比如 objc_msgSend 失败、SIGBUS、SIGFPE、SIGILL 等异常。 +- 当野指针指向的僵尸对象所占用的内存被分配后,此时访问会奔溃,比如 objc_msgSend 失败、SIGBUS、SIGFPE、SIGILL 等异常。 -### 3. 探索 Xcode 如何实现僵尸对象检测的原理 +开启僵尸对象时,不回收真正内存,而是将其 isa 变为僵尸对象,僵尸对象在内存中无法复用,所以偶现的 crash 变为必现 crash + + + +### 3. 探索 Xcode Zombie Object 实现原理 Xcode 默认设置不检查指针指向的对象是否是僵尸对象。为什么这么设计?因为开启会影响开发效率,所以线上就会报错。 @@ -2145,31 +2180,25 @@ Demo:MRC + Xcode 开启 Zombie Object printClassInfo(person);     [person description]; } -2022-05-14 01:35:54.521783+0800 DDD[79672:3001452] self:Person - superClass:NSObject -2022-05-14 01:35:54.522115+0800 DDD[79672:3001452] self:_NSZombie_Person - superClass:nil -2022-05-14 01:35:54.523292+0800 DDD[79672:3001452] *** -[Person description]: message sent to deallocated instance 0x6000024f1030 +// console +self:Person - superClass:NSObject +self:_NSZombie_Person - superClass:nil +*** -[Person description]: message sent to deallocated instance 0x6000024f1030 ``` -(前提是开启了 Zombie Objects)可以看到系统在回收对象时,不是真正的回收,而是先将其转为僵尸对象,僵尸对象所在内存无法被重用,所以让不稳定复现的内存奔溃变为稳定崩溃(更好的复现问题)。 +(前提是开启了 Zombie Object)可以看到系统在回收对象时,不是真正的回收,而是先将其转为僵尸对象,僵尸对象所在内存无法被重用,所以让不稳定复现的内存奔溃变为稳定崩溃(更好的复现问题)。 -开启僵尸对象检测后,系统会修改对象 isa 指针,指向新生成的僵尸类,僵尸类可以响应所有的消息,但表现打印格式为 `*** -[类 sel]: message sent to deallocated instance 地址` 的日志,然后结束进程。 -神奇,为什么打印出 `_NSZombie_Person` 。 -```objectivec -// Replaced by NSZombies -- (void)dealloc { - _objc_rootDealloc(self); -} -``` +利用 Instruments 下的 Zombies 工具检测,看看野指针的情况,系统是怎么捕获的。 -objc 源码中 dealloc 方法注释说了 dealloc 的实现会被僵尸对象所替换。 + -开启 Instrucments 分析查看得到,调用了 `__dealloc_zombie` 方法。底层到底做了什么?加个符号断点查看下 +切换到 `Call Trees` 模式下可以看到系调用了 `_dealloc_zombie` 方法。底层到底做了什么?加个符号断点查看下 -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeZombieDetect.png) + -通过符号名称大概可以才到系统会调用 dealloc 方法时,类似 KVO,派生出一个新的僵尸类类,将当前类的 isa 指针指向僵尸类。 +通过符号名称大概可以猜系统会在调用 dealloc 方法时,类似 KVO,派生出一个新的僵尸类,将当前类的 isa 指针指向僵尸类。 查看 Runtime 源码 @@ -2208,7 +2237,7 @@ void *objc_destructInstance(id obj) { } ``` -dealloc 方法最终调用到 object_dispose,但是如果开启 Zombie Object 检测则不会执行 free。其中 objc_destructInstance 方法会清理对象的关联对象、c++ 的析构函数、对象的 ivar 清理。 +dealloc 方法最终调用到 `object_dispose`,但是如果开启 Zombie Object 检测则不会执行 free。其中 `objc_destructInstance` 方法会清理对象的关联对象、c++ 的析构函数、对象的 ivar 清理。 另一方面,从 GUN 源码中窥探下 (NSObject.m 文件) @@ -2372,10 +2401,29 @@ static void GSLogZombie(id o, SEL sel){ 可以从 GUN 中看到调用对象 dealloc 方法时,内部实现通过调用 `GSMakeZombie` 方法,将类的 isa 指向为 NSZombie 类,也就是 zombieClass,其中 zombieClass 在 NSObject `initialize` 方法中初始化。后续针对僵尸对象的所有方法调用,都会走 Runtime forwarding 这个机制,内部会调用 `GSLogZombie` 方法,方法会按照 `NSLog(@"*** -[%@ %@]: message sent to deallocated instance %p", c, NSStringFromSelector(sel), o);` 格式打印日志,最后根据环境变量判断,调用系统底层 `abort` 来奔溃 + + +开启僵尸对象检测后,系统会修改对象 isa 指针,指向新生成的僵尸类,僵尸类可以响应所有的消息,但表现打印格式为 `*** -[类 sel]: message sent to deallocated instance 地址` 的日志,然后结束进程。 + +神奇,为什么打印出 `_NSZombie_Person` 。 + +```objectivec +// Replaced by NSZombies +- (void)dealloc { + _objc_rootDealloc(self); +} +``` + +objc 源码中 dealloc 方法注释说了 dealloc 的实现会被僵尸对象所替换。 + + + ### 4. Malloc Scribble 申请内存 alloc 时在内存上填 `0xAA`,释放内存 dealloc 时在内存上填 `0x55`。此种方案也会让内存泄漏偶现的 crash 变为必现。 + + ### 5. 野指针监控工具 ##### 类 Zombie Object 方案 @@ -2384,13 +2432,16 @@ static void GSLogZombie(id o, SEL sel){ 其中的僵尸对象检测做了这么几件事: -- 开启僵尸对象时,不回收真正内存,而是将其 isa 变为僵尸对象,僵尸对象在内存中无法复用,所以偶现的 crash 变为必现 crash - -- 僵尸类可以响应任何消息,是通过 Runtime forawrding 实现的。表现为先打印一条日志,按照 `NSLog(@"*** -[%@ %@]: message sent to deallocated instance %p", c, NSStringFromSelector(sel), o);` 格式打印日志。随后调用系统的 `abort()` 奔溃。 - +- 获取有问题内存对象的类名 +- 字符串拼接,类似 `_NSZombie_${ClassName}` 的形式 +- 按照上面得到的类名,创建一个继承自 NSProxy 的子类。因为需要响应各种原始对象的任何方法,所以需要是 NSProxy 的子类 +- hook NSObject 和 NSProxy 2个基类的 `dealloc` 方法,新的 dealloc 方法实现里修改当前类的 isa 为新创建的类 +- 为了避免内存空间释放后被重写,造成野指针问题。通过字典存储被方式的对象,同时设置在 30s 后调用 dealloc 方法将字典中存储的对象释放,避免 OOM +- 新创建的类除了处理消息转发外,还需要实现 NSObject 的基础方法,比如 copy、zone、description 方法 +- hook 之后,给僵尸对象发消息,最后都会调用 NSProxy 的 `forwardInvocation` 方法,其内部打印方法名称、对象地址、调用堆栈信息。最后调用 abort 方法,crash 掉。 - 奔溃的时候应该将调用堆栈等案发信息保存下来,走 APM 问题跟进流程 -```objectivec +```objective-c // ZombiePoxy #import @@ -2852,7 +2903,14 @@ bool init_safe_free(void) 注意:ZombieProxy、ZombieObjectDetector 2个类要在 Build Phases 设置为非 ARC,加 `-fno-objc-arc` -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ZombieObjectsDetector.png) + + +### 6. 方案对比 + +- 僵尸对象相比 `Malloc Scribble`,不需要考虑会不会崩溃的问题,只需要野指针指向僵尸对象,那么再次访问野指针就一定会奔溃 +- 僵尸对象的方比如 `Malloc Scribble` 覆盖面广,可以通过 fishhook hook free 方法将 c 函数也包含在其中。 + + ## 六、 App 网络监控 @@ -2860,7 +2918,7 @@ bool init_safe_free(void) ### 1. App 网络请求过程 -![网络请求各阶段](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-04-03-NetworkTime.png) +![网络请求各阶段](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-03-NetworkTime.png) App 发送一次网络请求一般会经历下面几个关键步骤: @@ -2898,7 +2956,7 @@ App 发送一次网络请求一般会经历下面几个关键步骤: iOS 网络框架层级关系如下: -![Network Level](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-04-05-NetworkLevel.png) +![Network Level](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-05-NetworkLevel.png) iOS 网络现状是由 4 层组成的:最底层的 BSD Sockets、SecureTransport;次级底层是 CFNetwork、NSURLSession、NSURLConnection、WebView 是用 Objective-C 实现的,且调用 CFNetwork;应用层框架 AFNetworking 基于 NSURLSession、NSURLConnection 实现。 @@ -3486,11 +3544,11 @@ iOS 中 hook 技术有 2 类,一种是 NSProxy,一种是 method swizzling( 但是如果需要监全部的网络请求就不能满足需求了,查阅资料后发现了阿里百川有 APM 的解决方案,于是有了方案 3,对于网络监控需要做如下的处理 -![network hook](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-04-04-network_monitor.png) +![network hook](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-04-network_monitor.png) 可能对于 CFNetwork 比较陌生,可以看一下 CFNetwork 的层级和简单用法 -![CFNetwork Structure](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-04-04-CFNetworkStructure.png) +![CFNetwork Structure](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-04-CFNetworkStructure.png) CFNetwork 的基础是 CFSocket 和 CFStream。 @@ -3587,9 +3645,9 @@ void printResponseData (CFDataRef responseData) { NSURLSession、NSURLConnection hook 如下。 -![NSURLSession Hook](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets//2020-04-13-NSURLSessionHook.jpeg) +![NSURLSession Hook](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//2020-04-13-NSURLSessionHook.jpeg) -![NSURLConnection Hook](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets//2020-04-13-NSURLConnectionHook.jpeg) +![NSURLConnection Hook](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets//2020-04-13-NSURLConnectionHook.jpeg) 业界有 APM 针对 CFNetwork 的方案,整理描述下: @@ -3864,7 +3922,7 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式 }; ``` - ![method swizzling](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-04-09-methodSwizzling.png) + ![method swizzling](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-09-methodSwizzling.png) method swizzling 改进版如下 @@ -3891,7 +3949,7 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式 typedef struct objc_object *id; ``` - ![isa swizzling](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-04-13-isaSwizzling.png) + ![isa swizzling](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-13-isaSwizzling.png) 我们来分析一下为什么修改 `isa` 可以实现目的呢? @@ -4049,11 +4107,11 @@ self.didCompleteObserver = [[NSNotificationCenter defaultCenter] addObserverForN HTTP 请求报文结构 -![请求报文结构](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-16-HTTPRequestStructure.png) +![请求报文结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-16-HTTPRequestStructure.png) 响应报文的结构 -![响应报文结构](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-16-HTTPResponseStructure.png) +![响应报文结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-16-HTTPResponseStructure.png) 1. HTTP 报文是格式化的数据块,每条报文由三部分组成:对报文进行描述的起始行、包含属性的首部块、以及可选的包含数据的主体部分。 2. 起始行和手部就是由行分隔符的 ASCII 文本,每行都以一个由 2 个字符组成的行终止序列作为结束(包括一个回车符、一个换行符) @@ -4080,11 +4138,11 @@ HTTP 请求报文结构 下图是打开 Chrome 查看极课时间网页的请求信息。包括响应行、响应头、响应体等信息。 -![请求数据结构](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-13-HTTPDataStructure.png) +![请求数据结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-13-HTTPDataStructure.png) 下图是在终端使用 `curl` 查看一个完整的请求和响应数据 -![curl查看HTTP响应](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-14-HTTPRequestStructure.png) +![curl查看HTTP响应](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-14-HTTPRequestStructure.png) 我们都知道在 HTTP 通信中,响应数据会使用 gzip 或其他压缩方式压缩,用 NSURLProtocol 等方案监听,用 NSData 类型去计算分析流量等会造成数据的不精确,因为正常一个 HTTP 响应体的内容是使用 gzip 或其他压缩方式压缩的,所以使用 NSData 会偏大。 @@ -4646,7 +4704,7 @@ Mach 已经通过异常机制提供了底层的陷进处理,而 BSD 则在异 Mach 异常都在 host 层被 `ux_exception` 转换为相应的 unix 信号,并通过 `threadsignal` 将信号投递到出错的线程。 -![Mach 异常处理以及转换为 Unix 信号的流程](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-19-BSDCatchSignal.png) +![Mach 异常处理以及转换为 Unix 信号的流程](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-19-BSDCatchSignal.png) ### 2. Crash 收集方式 @@ -4710,7 +4768,7 @@ KSCrash 功能齐全,可以捕获如下类型的 Crash 流程图如下: -![KSCrash流程图](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-20-KSCrashStructure.png) +![KSCrash流程图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-20-KSCrashStructure.png) 对于 Mach 异常捕获,可以注册一个异常端口,该端口负责对当前任务的所有线程进行监听。 @@ -5021,7 +5079,7 @@ static void restoreExceptionPorts(void) KSCrash 在这里的处理逻辑如下图: -![signal 处理步骤](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-20-signalCrash.png) +![signal 处理步骤](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-20-signalCrash.png) 看一下关键代码: @@ -5381,6 +5439,16 @@ static void setEnabled(bool isEnabled) } ``` +阅读下源码,看看为什么 `NSUncaughtExceptionHandler` 可以收集 crash 信息。查看 objc 源码 + + + + + +发现入口函数 `_objc_init` 中调用可设置异常处理的 `exception_init` 方法。内部通过底层 API `std::set_terminate(&_objc_terminate)` 来设置异常处理的 handler。 + + + #### 2.5. 主线程死锁 主线程死锁的检测和 ANR 的检测有些类似 @@ -5477,7 +5545,7 @@ static void setEnabled(bool isEnabled) 其他几个 crash 也是一样,异常信息经过包装交给 `kscm_handleException()` 函数处理。可以看到这个函数被其他几种 crash 捕获后所调用。 -![caller](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-06-03-KSCrashCaller.png) +![caller](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-03-KSCrashCaller.png) ```c /** Start general exception processing. @@ -5978,7 +6046,7 @@ static int64_t getReportIDFromFilename(const char* filename) } ``` -![KSCrash 存储 Crash 数据位置](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-31-KSCrashStoreCrashData.png) +![KSCrash 存储 Crash 数据位置](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-31-KSCrashStoreCrashData.png) #### 2.7 前端 js 相关的 Crash 的监控 @@ -6002,7 +6070,7 @@ window.onerror = function (msg, url, lineNumber, columnNumber, error) { }; ``` -![h5 异常监控](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-06-19-JSErrorCatch.png) +![h5 异常监控](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-19-JSErrorCatch.png) ##### 2.7.3 React Native 异常监控 @@ -6025,7 +6093,7 @@ window.onerror = function (msg, url, lineNumber, columnNumber, error) { 模拟器点击 `command + d` 调出面板,选择 Debug,打开 Chrome 浏览器, Mac 下快捷键 `Command + Option + J` 打开调试面板,就可以像调试 React 一样调试 RN 代码了。 -![React Native Crash Monitor](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-06-19-ReactNativeCrashMonitor.png) +![React Native Crash Monitor](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-19-ReactNativeCrashMonitor.png) 查看到 crash stack 后点击可以跳转到 sourceMap 的地方。 @@ -6049,7 +6117,7 @@ Tips:RN 项目打 Release 包 现象:iOS 项目奔溃。截图以及日志如下 -![RN crash](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-06-22-RNUncaughtCrash.png) +![RN crash](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-22-RNUncaughtCrash.png) ```shell 2020-06-22 22:26:03.318 [info][tid:main][RCTRootView.m:294] Running application todos ({ @@ -6143,7 +6211,7 @@ global.ErrorUtils.setGlobalHandler((e) => { 现象:iOS 项目不奔溃。日志信息如下,对比 bundle 包中的 js。 -![RN release log](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-06-23-RNReleaseLog.png) +![RN release log](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-23-RNReleaseLog.png) 结论: @@ -6524,7 +6592,7 @@ parseJSError(line, column); 5. 拿脚本解析好的行号、列号、文件信息去和源代码文件比较,结果很正确。 -![RN Log analysis](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-06-23-RNCrashLogAnalysis.png) +![RN Log analysis](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-23-RNCrashLogAnalysis.png) ##### 2.7.5 SourceMap 解析系统设计 @@ -6816,7 +6884,7 @@ parseJSError(line, column); `.DSYM` 文件是从 Mach-O 文件中抽取调试信息而得到的文件目录,发布的时候为了安全,会把调试信息存储在单独的文件,`.DSYM` 其实是一个文件目录,结构如下: -![.DSYM文件结构](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-29-DYSMStructure.png) +![.DSYM文件结构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-29-DYSMStructure.png) #### 4.2 DWARF 文件 @@ -7348,7 +7416,7 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev /Users/你自己的用户名/Library/Developer/Xcode/iOS DeviceSupport/ ``` -![系统符号化文件](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-05-28-SymbolicateLib.png) +![系统符号化文件](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-28-SymbolicateLib.png) ### 5. 服务端处理 @@ -7358,7 +7426,7 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev 早期单体应用时代,几乎应用的所有功能都在一台机器上运行,出了问题,运维人员打开终端输入命令直接查看系统日志,进而定位问题、解决问题。随着系统的功能越来越复杂,用户体量越来越大,单体应用几乎很难满足需求,所以技术架构迭代了,通过水平拓展来支持庞大的用户量,将单体应用进行拆分为多个应用,每个应用采用集群方式部署,负载均衡控制调度,假如某个子模块发生问题,去找这台服务器上终端找日志分析吗?显然台落后,所以日志管理平台便应运而生。通过 Logstash 去收集分析每台服务器的日志文件,然后按照定义的正则模版过滤后传输到 Kafka 或 Redis,然后由另一个 Logstash 从 Kafka 或 Redis 上读取日志存储到 ES 中创建索引,最后通过 Kibana 进行可视化分析。此外可以将收集到的数据进行数据分析,做更进一步的维护和决策。 -![ELK架构图](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-06-14-ELK.png) +![ELK架构图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-14-ELK.png) 上图展示了一个 ELK 的日志架构图。简单说明下: @@ -7368,13 +7436,13 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev 下图贴一个 Elasticsearch 社区分享的一个 “Elastic APM 动手实战”[主题](https://elasticsearch.cn/slides/257#page=3)的内容截图。 -![Elasticsearch & APM](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-06-17-ElastiicsearchAPM.png) +![Elasticsearch & APM](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-ElastiicsearchAPM.png) ##### 5.2 服务侧 Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化处理,以方便定位问题、crash 产生报表和后续处理。 -![crash log 处理流程](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-06-14-CrashLogSymbolicate.png) +![crash log 处理流程](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-14-CrashLogSymbolicate.png) 所以整个流程就是:客户端 APM SDK 收集 crash log -> Kafka 存储 -> Mac 机执行定时任务符号化 -> 数据回传 Kafka -> 产品侧(显示端)对数据进行分类、报表、报警等操作。 @@ -7386,7 +7454,7 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化 现在很多架构设计都是微服务,至于为什么选微服务,不在本文范畴。所以 crash 日志的符号化被设计为一个微服务。架构图如下 -![crash 符号化流程图](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-06-17-APMServerArch.png) +![crash 符号化流程图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-APMServerArch.png) 说明: @@ -7398,19 +7466,19 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化 - 脚手架 cli 有个能力就是调用打包系统的打包构建能力,会根据项目的特点,选择合适的打包机(打包平台是维护了多个打包任务,不同任务根据特点被派发到不同的打包机上,任务详情页可以看到依赖的下载、编译、运行过程等,打包好的产物包括二进制包、下载二维码等等) -![符号化流程图](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-06-17-symolication_flow.png) +![符号化流程图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-symolication_flow.png) 其中符号化服务是大前端背景下大前端团队的产物,所以是 NodeJS 实现的(单线程,所以为了提高机器利用率,就要开启多进程能力)。iOS 的符号化机器是 双核的 Mac mini,这就需要做实验测评到底需要开启几个 worker 进程做符号化服务。结果是双进程处理 crash log,比单进程效率高近一倍,而四进程比双进程效率提升不明显,符合双核 mac mini 的特点。所以开启两个 worker 进程做符号化处理。 下图是完整设计图 -![符号化技术设计图](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-06-17-APMServerWorker.png) +![符号化技术设计图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-APMServerWorker.png) 简单说明下,符号化流程是一个主从模式,一台 master 机,多个 slave 机,master 机读取 .DSYM 和 crash 结果的 cache。「数据处理和任务调度框架」调度符号化服务(内部 2 个 symbolocate worker)同时从七牛云上获取 .DSYM 文件。 系统架构图如下 -![符号化服务架构图](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-06-17-SymbolicateServerArch.png) +![符号化服务架构图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-SymbolicateServerArch.png) ## 九、Weex、Flutter 异常监控 @@ -7706,7 +7774,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) { 1. 当前启动时的js 预载入流程里的JS下载失败 和 进入Weex页面后的JS拉取失败 两处的上报失败未做区分,没法实际统计加载页面时有多少JS获取失败的情况。 2. 随着业务迭代, Weex 页面越来越多,需要做 JS 拉取相关优化,避免白屏问题 3. 目前 JS 缓存逻辑为,每个 Weex 模块单独维护一个 LRUCache,并且会做大量的预加载,但可能大多数页面根本不会访问到。浪费了内存资源去做了一些不会被调用到的页面预加载,可能还会造成 OOM 问题 - ![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/WeexResourcePull.png) + ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WeexResourcePull.png) // todo? 参考前端资源模块化,如何实现资源预加载(全局 LRU或者基于用户行为路径的预热功能,比如某个收银员角色固定的情况下,他日常的操作行为是固定的,商品扫码、开单) @@ -7718,15 +7786,15 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) { 可能有些人一直没有遇到过因为在子线程操作 UI,导致在开发阶段 Xcode console 输出了一堆日志,大体如下 -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2022-0204-SubThreadUIXcode1@2x.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2022-0204-SubThreadUIXcode1@2x.png) 其实我们可以给 Xcode 打个 `Runtime Issue Breakpoint` ,type 选择 `Main Thread Checker`, 在发生子线程操作 UI 的时候就会被系统检测到并触发断点,同时可以看到堆栈情况 -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2022-0204-SubThreadUISymbolBreakpoints@2x.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2022-0204-SubThreadUISymbolBreakpoints@2x.png) 效果如下 -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2022-0204-SubThreadUIXcodeBreakingPointsHappened@2x.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2022-0204-SubThreadUIXcodeBreakingPointsHappened@2x.png) ### 2. 问题及解决方案 @@ -7740,7 +7808,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) { 但是某些类有些 API 我们也不希望在子线程被调用,这时候 `libMainThreadChecker.dylib`是无法满足的。 对 `libMainThreadChecker.dylib` 库的汇编代码研究,发现 `libMainThreadChecker.dylib` 是通过内部 `__main_thread_add_check_for_selector` 这个方法来进行类和方法的注册的。所以如果我们同样可以通过 `dlsym` 来调用该方法,以达到对自定义类和方法的主线程调用监测。 -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2022-0204-SubThreadUIMonitor@2x.PNG) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2022-0204-SubThreadUIMonitor@2x.PNG) 另外该功能可以在线下 debug 阶段开启,判断是否是在 Xcode debug 状态,可以通过苹果提供的[官方判断方法](https://developer.apple.com/library/archive/qa/qa1361/_index.html#//apple_ref/doc/uid/DTS10003368)实现。 @@ -7754,7 +7822,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) { 好像用 APM 的卡顿没发现啥问题。仔细看看发现了问题,我们的卡顿只是监控主线程方法耗时。一个页面加载后可能会触发一些网络请求功能,子线程请求完网络,可能会做一些数组裁剪、组装,拼接为 UI 所需要的信息,这个时候很可能会发生卡顿,所以这种 case 下卡顿监控是无法覆盖的。用下图表示 -![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/PageLoadFullTime.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PageLoadFullTime.png) 页面作为承载用户交互的具体战场,我们需要对页面的性能有个直观的指标。业界一般有2个指标:**页面渲染时长、页面可交互时长**。 @@ -7819,7 +7887,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) { 4. 监控数据需要做内存到文件的写入处理,需要注意策略。监控数据需要存储数据库,数据库大小、设计规则等。存入数据库后如何上报,上报机制等会在另一篇文章讲:[打造一个通用、可配置的数据上报 SDK](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md) 5. 尽量在技术评审后,将各端的技术实现写进文档中,同步给相关人员。比如 ANR 的实现 - + ```objective-c /* android 端 @@ -7865,15 +7933,19 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) { ``` 6. 整个 APM 的架构图如下 - - ![APM Structure](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/2020-06-17-APMStructure.jpg) - + + ![APM Structure](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-06-17-APMStructure.jpg) + 说明: - + - 埋点 SDK,通过 sessionId 来关联日志数据 7. APM 技术方案本身是随着技术手段、分析需求不断调整升级的。上图的几个结构示意图是早期几个版本的,目前使用的是在此基础上进行了升级和结构调整,提几个关键词:Hermes、Flink SQL、InfluxDB。 +8. 获取到 APM 问题数据是第一步,获取之后问题需要及时上报,需要有一个数据上报系统,数据及时准确、高效的上传到服务端(这就是另一个命题,可以查看这篇文章:[打造一个通用、可配置、多句柄的数据上报 SDK](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md)),到 APM 后台。后台产生工单,根据业务模块分配到对应的责任人、群组。根据问题的紧急程度,报警策略和提醒频次也不一样,比如波动报警、自定义报警策略等(这也是另一个命题:智能报警策略、波动报警策略、业务线自定义可配置责任人等)。责任人及时解决问题、修改工单状态、热修复或者发布新版本,问题闭环。 + + + ## 十四、未来规划 - 监控能力继续完善 @@ -7888,8 +7960,6 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) { - [iOS 保持界面流畅的技巧](https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/) - [Call Stack](https://en.wikipedia.org/wiki/Call_stack) -- [关于函数调用栈(call stack)的个人理解](https://blog.csdn.net/VarusK/article/details/83031643) -- [获取任意线程调用栈的那些事](https://bestswifter.com/callstack/) - [iOS 启动时间优化](https://www.zoomfeng.com/blog/launch-time.html) - [WWDC2019 之启动时间与 Dyld3](https://www.zoomfeng.com/blog/launch-optimize-from-wwdc2019.html) - [Apple-libmalloc](https://opensource.apple.com/tarballs/libmalloc/) diff --git a/Chapter1 - iOS/1.75.md b/Chapter1 - iOS/1.75.md index 3cbbb9b..ad72ae2 100644 --- a/Chapter1 - iOS/1.75.md +++ b/Chapter1 - iOS/1.75.md @@ -1082,7 +1082,7 @@ QA:一个被测方法,有诸多 case,为什么不写在一个测试方法 1. 主工程不管是不是混编,但为了让 Swift 测试代码,可以访问到 OC 被测类,需要创建一个 Swift 文件,系统会自动创建 bridge 文件,且需要在 `AppTestingExplore-Bridging-Header.h` 文件中导出需要被测的头文件 2. 在 Swift 测试文件中,导入主工程 module。 - + @@ -1343,7 +1343,7 @@ int main(int argc, char * argv[]) { 开发的单元测试代码,运行的背后也是一个可执行文件。查看内部,可以发现一堆和测试相关的 framework。 - + 思考:一个工程中,只要写好了测试代码,是不是加载这几个动态库就可以运行测试用例了? @@ -1369,7 +1369,7 @@ int main(int argc, char * argv[]) { 下面是之前在有赞,开发完精准测试系统后,落地到一个业务项目中取得的价值,帮助2位 QA 发现漏测的代码,倒逼 QA 去设计更完善的测试 case、分析覆盖率低是开发者的兜底代码太多还是真的漏掉了业务 case。 - + 精准测试助力业务,质量更加稳定。 diff --git a/Chapter1 - iOS/1.82.md b/Chapter1 - iOS/1.82.md index 48db64b..9862294 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 实现。代码如下 - + 分析: @@ -168,7 +168,7 @@ union { 与一个不是3个数之一的数按位与,得到的结果为`0b0000 0000`。利用这个特性我们可以判断传递来的参数是不是包含了某个值 - + 有了上面的铺垫,就可以更好的查看 runtime 中 isa 的定义了。 @@ -273,7 +273,7 @@ isa 在 arm64 之后必须通过 `ISA_MASK` 去查询 class(类对象、元类 `0x0000000ffffffff8ULL` 用程序员模式打开计算器 - + @@ -283,7 +283,7 @@ extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcl 知道结构体可以指定存储大小这个功能后,可以看到 `isa_t` 联合体与 `ISA_MASK` 按位与之后的地址,其实就是类真实的地址信息(可能是类对象、也有可能是元类对象) - + @@ -423,7 +423,7 @@ struct class_ro_t { 具体关系整理如下图 - + @@ -435,7 +435,7 @@ struct class_ro_t { - + 比如访问 method 的过程 @@ -453,7 +453,7 @@ struct class_ro_t { - `class_ro_t` 里面的 baseMethodList、baseProtocols、ivars、baseProperties 是一维数组,是只读的,包含了类的(原始信息)初始内容 - + @@ -1240,7 +1240,7 @@ v 可以对照下面的表格进行查看: -![](./../assets/runtime-method-encoding.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-method-encoding.png) ```objectivec - (int)calcuate:(int)baseHeight heigith:(float)height; @@ -1513,7 +1513,7 @@ bucket_t goodStudentSayBucket = buckets[(uintptr_t)@selector(personSay) & cache. NSLog(@"%s %p", goodStudentSayBucket._key, goodStudentSayBucket._imp); ``` - + @@ -1544,7 +1544,7 @@ static inline mask_t cache_hash(cache_key_t key, mask_t mask) ## Runtime - objc_msgSend - + ```c++ Person *p = [[Person alloc] init]; @@ -2117,7 +2117,7 @@ if (imp) goto done; 上面的流程是整个 `objc_msgSend` 的消息发送阶段的整个流程。可以用下图表示 - + @@ -2262,7 +2262,7 @@ SEL_resolveClassMethod, sel);` 完整流程如下 - + @@ -2307,7 +2307,7 @@ Person *person = [[Person alloc] init]; 知道 `objc_msgSend` 的流程,我们尝试给它修正下 - + 方法1,增加一个兜底方法,然后利用 `class_addMethod` 动态增加方法实现 @@ -2341,7 +2341,7 @@ class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncod 方法3,也可以添加 c 语言方法 - + c 函数即函数地址,只不过传递给 class_addMethod 的时候需要转换为函数参数加 `(IMP)cfuntionResolver`,手动添加方法签名。 @@ -2454,7 +2454,7 @@ void *_objc_forward_handler = (void*)objc_defaultForwardHandler; 为什么是 `__forwarding__` 方法。我们可以根据 Xcode 崩溃窥探一二 -![](./../assets/runtime-forwardingFailed.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-forwardingFailed.png) ```c int __forwarding__(void *frameStackPointer, int isStret) { @@ -2498,7 +2498,7 @@ int __forwarding__(void *frameStackPointer, int isStret) { 完整流程如下 - + @@ -2532,13 +2532,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` 方法 @@ -2568,13 +2568,13 @@ Person 类不存在对象方法 makeliving ,PersonHelper 类存在。 注意:`methodSignatureForSelector` 如果返回 nil,则 `forwardInvocation` 不会执行 - + 上述方式不够优雅,针对方法签名的获取太被动,方法改变了,方法签名处需要调整,很麻烦。 - + @@ -2601,11 +2601,11 @@ Person 类不存在对象方法 makeliving ,PersonHelper 类存在。 } ``` - + - + @@ -2632,7 +2632,7 @@ OC 中方法调用的本质就是利用 runtime 发消息,发消息也给 rece 3. 如果在当前类的方法列表中没有找对方法实现,则根据类对象的 superclass 在父类的类对象的方法列表中继续查找 - + 先根据 superclass 找到父类,然后在父类中也按照上面3点进行递归查找,即: @@ -2757,7 +2757,7 @@ objc_msgSendSuper(arg, sel_registerName("class")) 我们对 iOS 项目`[super viewDidLoad]` 下符号断点,发现`objc_msgSendSuper2` -![](./../assets/runtime-super.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-super.png) 查看 objc4 源代码发现是一段汇编实现。 @@ -2845,7 +2845,7 @@ call - 调用函数 也可以在 Xcode 上可视化面板操作,路径为:菜单栏 `Product -> Perform Action -> Assemble "ViewController.m"` - + @@ -2871,7 +2871,7 @@ call - 调用函数 Demo1 - + @@ -2899,7 +2899,7 @@ Demo1 Demo2 - + 下面面2个判断都是调用类方法的 `isMemberOfClass` 、`isKindOfClass` @@ -2917,7 +2917,7 @@ Demo2 可以看到 `+(BOOL)isMemberOfClass:(Class)cls` 方法内部就是对当前类获取类对象,然后与传递进来的 cls 判断是否相等。由于是 `[Student isMemberOfClass:[Student class]])` `Student` 类调用类方法 `+isMemberOfClass` 所以类对象的类对象也就是元类对象,cls 参数也就是 `[Student class]` 是一个类对象,元类对象等于类对象吗?显然不是 -想让判断成立,可以改为 `[Student isMemberOfClass:object_getClass([Student class])]` 或者 `[[Student **class**] isMemberOfClass:object_getClass([Student class])]` +想让判断成立,可以改为 `[Student isMemberOfClass:object_getClass([Student class])]` 或者 `[[Student class] isMemberOfClass:object_getClass([Student class])]` `+(BOOL)isKindOfClass:(Class)cls` 同理分析。作用是当前类的元类,是否是右边传入对象的元类或者元类的子类。 @@ -2958,7 +2958,7 @@ QA: 综合练习: - + @@ -2966,16 +2966,12 @@ QA: ## Runtime 刁钻题 -能否向编译后的类,添加实例变量?class_ro_t 不可以添加。但可以向动态创建的类添加。 - - +### NSObject 的内存布局、isa、对象属性访问原理 > 这道题目设计:super 调用的本质、函数栈空间向下增长、runtime 消息调用本质(isa)、访问对象的成员变量(找到 isa,约过前面的8字节,按照成员变量的大小,去找成员) > > 因为实例对象里存的就是:isa + 各个成员变量的值 - - ```objective-c @interface Person : NSObject @property (nonatomic, copy) NSString *name; @@ -2999,22 +2995,20 @@ QA: 程序运行什么结果? - + -为什么会方法调用成功?为什么 name 打印出为 @"杭城小刘" +为什么会方法调用成功?为什么 name 打印出为 @"杭城小刘"。我们来分析下: -我们来分析下: - -1.**方法调用本质就是寻找 isa 进行消息发送** +第一、**方法调用本质就是寻找 isa 进行消息发送** ```objective-c Person *person = [[Person alloc] init]; [person sayHi]; ``` -`[[Person alloc] init]`在内存中分配一块内存,然后 isa 指向这块内存,然后 person 指针,指向结构体,结构体的第一个成员。 +`[[Person alloc] init]` 在内存中分配一块内存,然后 isa 指向这块内存,然后 person 指针,指向结构体,结构体的第一个成员。 -2.**栈空间数据内存向下生长。第一个变量地址高,其次降低。且每个变量的内存地址是连续的。** +第二、**栈空间数据内存向下生长。第一个变量地址高,其次降低。且每个变量的内存地址是连续的。** 这个流程其实和上面的代码一样的。所以可以正常调用 @@ -3029,9 +3023,9 @@ void test () { 方法内的变量存储在栈上,堆向上增长,栈向下增长。 -![](./../assets/runtime-isa-demo.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-isa-demo.png) -3.**实例对象的本质就是一个结构体,存储所有成员变量(isa 是一个特殊成员变量,其他的成员变量,这里就是 _name),`sayHi` 方法内部的 self 就是 obj,找成员变量的本质就是找内存地址的过程(此时就是偏移8个字节)** +第三、**实例对象的本质就是一个结构体,存储所有成员变量(isa 是一个特殊成员变量,其他的成员变量,这里就是 _name),`sayHi` 方法内部的 self 就是 obj,找成员变量的本质就是找内存地址的过程,本质就是对象地址 + Offset。此时就是 isa 地址 + 8字节偏移量)** 上面代码可以类比类调用方法的流程。 obj 指针指向 Person 这块内存,给类对象发送 `sayHi` 消息也就是通过 obj 指针找到 isa,恰好 obj 指针指向的地址就是类对象的类结构体的地址,结构体成员变量第一个就是 isa 指针,结构体的其他成员变量就是类的其他属性,这里也就是 `_name`,所以我们给自定义的指针 `void *p` 调用 sayHi 方法,系统 runtime 在打印 name 的时候,会在 p 附近(下8个字节,因为 isa 是指针,长度为8)找 `_name` 属性,此时也就找到了 temp 字符串。 @@ -3046,17 +3040,17 @@ struct Person_IMPL { 再看一个变体1 - + + +打印输出是因为 `*p` 类似 isa 指针。本身占用8字节空间,然后访问 `self->_name` 就是 `base + 8 = isa地址 + 8 ` 出的内存就是 name,`*p` 是在栈中,加8,就是向上声明的变量,当前情况下 `Address(*p) + 8` 就是 temp 变量。所以输出 `` 再看一个变体2 - + - - -搞懂的小伙伴不迷惑了。没搞懂其实就是没搞懂**栈地址由高到低,向下生长** 和 `super` 调用的本质。 +分析: 搞懂的小伙伴不迷惑了。没搞懂其实就是没搞懂**栈地址由高到低,向下生长** 和 `super` 调用的本质。 再强调一句,根据指针寻找成员变量 _name 的过程其实就是根据内存偏移找对象的过程。在变体2中,isa 地址就是 class 的地址,所以按照`地址 +8` 的策略,其实前一个局部变量。 @@ -3069,7 +3063,11 @@ objc_msgSendSuper(arg, sel_registerName("viewDidLoad")); 所以此时的“前一个局部变量” 也就是结构体 `objc_super` 类型的 arg。arg 是一个结构体,结构体第一个成员变量就是 self,所以“前一个局部变量” 也就是 self(ViewController) -![](./../assets/runtime-super-isa-demo.png) +结构体有2个成员变量,顺序越高的成员变量地址越高。所以在栈上,struct 中第一个成员变量地址更低。在通过 isa 内存偏移的时候,优先找到 self。所以会输出 ViewController + + + +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-super-isa-demo.png) 可能会疑问,知道了 super 调用本质,知道了会产生一个局部变量的结构体,但是结构体里面2个成员变量,找属性的时候,isa 下的8个字节会命中哪个? @@ -3079,12 +3077,44 @@ objc_msgSendSuper(arg, sel_registerName("viewDidLoad")); - + +### runtime 对象方法、类方法的查找过程熟悉吗? + +下面的代码会 crash 吗? + +```objective-c +id rs = [NSObject valueForKey:@"isa"]; +NSLog(@"%@", rs); +``` + + + +不会 Crash。输出的是 NSObject 的元类对象。因为 NSObject 调用的是类方法,先去元类对象的方法列表中查找,发现没有 `valueForKey` 方法。则继续从 NSObject 元类对象的父类对象,也就是 NSObject 的类对象上查找 `valueForKey` 方法。 + +因为分类 `NSObject(NSKeyValueCoding)` 实现了 `-valueForKey` 方法。所以不会 crash,在类对象方法中,访问 isa,也就是获取类对象的 isa,也就是 NSObject 的元类对象。 + + + +查看了 objc 源码,会发现很多 NSObject 的基础方法:`+ (id)init`、`- (id)init` 等均有 `+` 、`-` 方法。 + + + +为什么这么设计?猜测是为了代码的健壮。 + +- 因为存在继承关系,所有的对象的基类需要有个源头。即使是 NSObject 也是如此。 +- 调用对象方法的本质就是根据对象的 isa 找到类对象,然后从方法缓存中去查找方法实现,有就调用,没有就从类对象的方法列表中查找,找到则写入方法缓存并调用,没有则根据 superclass 的类对象继续查找...,一直找到 NSObject 还是找不到,则走 Runtime 消息转发流程。 +- 调用类方法的本质是根据对象的 isa 找到类对象,再根据类对象的 isa 找到元类对象,从元类对象的方法方法列表中查找方法实现,如果没有则继续向上查找,直到找到基类的元类对象,也就是 NSObject,如果找不到则走消息转发流程 +- 但是 Apple 的设计是为了 NSObject 元类对象的父类也要有个东西去接着,于是就让 NSObject 的类对象来充当 。 + +这也就是为什么 NSObject 子类对象调用 `+` 类方法不 crash 的原因。 + + + ## 应用场景 ### 统计 App 中未响应的方法。给 NSObject 添加分类 `NSObject+ExceptionHunter.m`,利用 NSProxy 实现 @@ -3121,7 +3151,7 @@ Person *p = [Person new]; object_setClass(p, [Student class]); ``` -![](./../assets/runtime-changeisa-demo.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-changeisa-demo.png) @@ -3150,7 +3180,7 @@ void createClass (void) { } ``` -![](./../assets/runtime-dynamicCreateClass-demo.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-dynamicCreateClass-demo.png) runtime 中 copy、create 等出来的内存,不使用的时候需要手动释放`objc_disposeClassPair(newClass>)` @@ -3229,7 +3259,7 @@ free(properties); - + 不够健壮体现在: @@ -3369,3 +3399,7 @@ OC 是一门动态性很强的编程语言,允许很多操作推迟到程序 - 无痕埋点 - 热修复 + +可以认为 oc 中对象上一个指向 CLassObject 地址的变量 `id obj = &ClassObject` + +而对象的实例变量 `void *ivar = &obj + offset(N*size)` 。根据 isa 也就是对象的基地址,然后偏移访问 ivar。 diff --git a/Chapter1 - iOS/1.86.md b/Chapter1 - iOS/1.86.md index cb67d91..2e1d1ba 100644 --- a/Chapter1 - iOS/1.86.md +++ b/Chapter1 - iOS/1.86.md @@ -5,3 +5,14 @@ 可以设计实现一个线程池,涉及几个角色、任务队列、调度者如何调度、线程池的有哪些策略。iOS GCD 的线程池策略可以类比 Java 中的4个线程池策略,明白不同语言设计的共同之处 - https://juejin.im/post/6855807995570618375 + + + +串行队列:DQF_WIDTH(1) + + + +队列和线程的区别? + + + diff --git a/Chapter1 - iOS/1.88.md b/Chapter1 - iOS/1.88.md index a6e1e41..72eac2a 100644 --- a/Chapter1 - iOS/1.88.md +++ b/Chapter1 - iOS/1.88.md @@ -1,33 +1,29 @@ # fishhook 原理 -## 先看看怎么用 +## hook 分类 -经常会遇到 hook oc 方法,但是遇到像 NSLog、objc_msgSend 等方法的时候 OC Runtime 就不满足了,有了 fishhook 神器,hook “c 函数”已不是难题。 +- Method Swizzle:利用 OC runtime,动态改变 SEL(方法编号)、IMP(方法实现)的对应关系,达到 OC 方法调用流程改变的目的,主要用于 OC 方法 +- fishhook:是 Facebook 提供的一个动态修改链接 Mach-o 文件的工具,利用 Mach-O 文件加载原理,通过修改懒加载和非懒加载2个表的指针,达到 c 函数 hook 的目的。 +- Cydia Substrate: 原名为 Mobile Substrate,主要作用是针对 OC 方法、C 函数以及函数地址进行 hook,当然并不是仅针对 iOS 而设计,Android 也可使用。官方地址:http://www.cydiasubstrate.com + +hook只有二种: +- `inline hook`:直接修改函数入口代码或函数内某处代码跳转到自己代码 +- 地址替换:包含入口表地址替换、出口表地址替换、结构体内地址替换等。这类最简单但不一定有效,不通过地址表的调用 hook 不到 + + +## 应用 + +经常会遇到 hook oc 方法,但是遇到像 NSLog、objc_msgSend 等方法的时候 OC Runtime 就不满足了 ,有了 fishhook 神器,hook “c 函数”已不是难题。 为什么对 “c 函数”加了引号,带着问题往下看 -Hook NSLog,上 Demo -```objectivec -static void (*SystemLog)(NSString *format, ...); -- (void)viewDidLoad { - [super viewDidLoad]; - struct rebinding NSLogRebinding = { - "NSLog", - lbpLog, - (void *)&SystemLog - }; - struct rebinding rebs[1] = {NSLogRebinding}; - rebind_symbols(rebs, 1); - NSLog(@"沙沙"); -} -void lbpLog(NSString *format, ...) { - format = [NSString stringWithFormat:@"fishhook 探索 - %@", format]; - SystemLog(format); -} -@end -// fishhook 探索 - 沙沙 -``` + +### Hook 系统 c 函数 + +以 NSLog 为例。 + + 可以看到 hook 成功了。 @@ -39,10 +35,49 @@ struct rebinding { }; ``` + + +### Hook 自定义 c 函数 + +自定义一个 c 函数 `handleTouchAction` ,发现没有 hook 成功。 + + + +不禁令人好奇,同样是 c 函数,为什么系统 c 函数可以 hook,自定义的 c 函数无法 hook?带着问题继续探究 + + + ## 原理窥探 +FishHook 是 FaceBook 提供的一个可以动态修改链接 Mach-O 文件的工具。利用 Mach-O 文件的加载原理,通过修改懒加载和非懒加载2个表的指针,达到 hook 系统 C 函数的目的。 + + + +### Mach-O 文件权限 + +Mach-O 分为代码段、数据段...: + +- 代码段:可读、可执行、不可写 +- 数据段:可读、可写、不可执行 + + + +### 系统共享缓存 + 我们知道 NSLog 的函数实现在 Foundation 库中,而我们开发自己写的其他函数则在自身可执行文件中,也就是 Mach-O。 +iOS 共享缓存:在iOS 3.1及以后的版本中,Apple 将系统库文件打包合并成一个大的缓存文件,存放在 `/System/Library/Caches/com.apple.dyld/ ` 目录下,以减少冗余并优化内存使用 + +- 所有的进程均可访问 +- 缓存文件的架构特定性:共享缓存文件根据不同的处理器架构有不同的版本,例如 `dyld_shared_cache_arm64` 针对的是 ARM 64 位架构的处理器 +- 动态库的加载优化:通过共享缓存,iOS 系统可以在程序运行时更高效地加载动态库,因为不需要每个应用程序重复加载相同的库文件,从而加快了启动速度并提高了性能 + +App 对应的 Mach-O 被 dyld 装载进内存的时候,`NSLog ` 地址还不确定,其位于 `Foundation` 框架中,也就是位于共享缓存中。 + +这带来一个问题:代码在 Mach-O 文件上策马奔腾,在愉快的执行,当遇到 `NSLog` 的时候,该如何确定位于 `Foundation` 框架中 NSLog 真实函数地址呢?编译阶段,clang 可以知道任何设备(iPhone14、iPhone6s)任何架构(arm64、armv7)上 Foundation 真实的内存吗?显然不可能。 + + + 这里稍微展开谈谈静态链接和动态链接。 链接分为静态链接和动态链接。早期计算机都是采用静态链接这种方式的。静态链接存在缺点: @@ -59,48 +94,100 @@ struct rebinding { 但也带来了坏处,因为都是程序每次装载的时候进行重新链接。有解决方案,叫做延迟绑定(Lazy binding),可使得动态链接对性能的影响减的最小。据估算,动态链接相比静态链接,存在大约5%的性能损失,但换来程序在空间上的节省和程序构建和升级的灵活性,是值得的。 -地址无关代码(PIC) -装载时重定位是解决动态模块中有绝对地址引用的方案之一。但其存在一个很大缺点,是指令部分无法在多个进程间共享,这样就是失去动态链接节省内存这一优势。我们还需要一种更优雅的方案,希望程序模块中的共享指令部分在装载时不需要因为装载地址改变而改变,所以实现的基本想法就是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本,这个方案就是 地址无关代码(Position Independent Code)PIC 技术。 + +如何解决? + +编译阶段给 NSLog 一个默认地址,在应用启动时,通过重定向,把真正的地址重新写入到 Mach-O 中,但效率很低。 后来诞生了 `PIC` 技术 + + + +### PIC 技术 + +装载时重定位是解决动态模块中有绝对地址引用的方案之一。但其存在一个很大缺点,是指令部分无法在多个进程间共享,这样就是失去动态链接节省内存这一优势。 + +我们还需要一种更优雅的方案,希望程序模块中的共享指令部分在装载时不需要因为装载地址改变而改变,所以实现的基本想法就是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本,这个方案就是 地址无关代码(Position Independent Code)PIC 技术。 + + + +Position-Independent Code,即位置无关代码。这是一种编译技术,允许生成的代码在内存中的任何位置执行,而不需要任何修改。这在动态链接库(dylib)中非常重要,因为它允许系统动态地将库加载到内存中的不同位置,而不需要重新链接。 + +优点是: + +- 共享代码:多个应用程序可以共享同一个动态库的实例,节省内存并减少启动时间。 +- 动态链接器(dyld)可以优化符号绑定过程,提高应用程序的启动速度 +- 支持代码重定位,使得应用程序更新和修补更加灵活 + + 写的业务代码里面假如某一行调用了 `NSLog`,那么在编译阶段,使用 NSLog 只是 IDE 提供了功能,让你可以看到声明而已。编译后的可执行文件,还是不知道 NSLog 的具体函数地址。这个底层是如何工作的呢? -在工程编译阶段,所产生的 Mach-O 可执行文件中会预留出一段空间,这个空间被叫做符号表,存放在 `_DATA` 数据段中,且数据段是可读可写的。 +有了 PIC 之后,工作流程为: -工程中所有引用了动态库共享缓存区中的系统符号,其指向的地址设置成符号地址。比如工程中 NSLog,那么编译时就会在 Mach-O 中创建一个 NSLog 符号,工程中的 NSLog 就指向这个符号 +- 编译期,在 Mach-O 中数据段生成一块区域,该区域叫符号表。数据段可读可写。 -当 dyld 将 Mach-O 加载到内存中时,读取 header 中 load command 信息,找出需要加载哪些库文件,去做绑定的操作。比如 dyld 会找到 Foundation 中的 NSLog 的真实地址写到 _DATA 段的符号表中 NSLog 对应的符号上。 + 工程中所有引用了动态库共享缓存区中的系统符号,其指向的地址设置成符号地址。比如工程中 NSLog,那么编译时就会在 Mach-O 中创建一个 NSLog 符号,工程中的 NSLog 就指向这个符号 -当 DYLD 加载当前可执行文件的时候,才将这个表每个编号对应的函数地址去填上去,这个动作叫做**符号绑定**。 +- dyld 加载 Mach-O,做符号绑定。 -它指向了一个表(类似一个应用程序的外部函数名,函数真正实现地址),去这个表里面找地址,这个表叫做**符号表**。 + 当 dyld 将 Mach-O 加载到内存中时,读取 header 中 load command 信息,找出需要加载哪些库文件,去做绑定的操作。比如 dyld 会找到 Foundation 中的 NSLog 的真实地址写到 _DATA 段的符号表中 NSLog 对应的符号上。 -当真正去调用 NSLog 函数的时候才去这个符号表中去寻找函数地址,去调用实现。 -调用外部函数(在内部找不到方法实现)的时候,在 Mach-O 的数据段生成一个区域,叫做符号表。符号表的 key 就是方法名,比如 NSLog。 -fishHook 做的事情就是 rebind,将 NSLog 真正实现的地址,指向到其他我们生成的函数地址去(hook)。 +### 实践探索 -PIC 技术。 +做个实验验证下,完整流程 + +第一步:可以看到 NSLog 位于 Lazy Symbol Pointers 里的第一个。lazy 说明只有在用到的时候才去绑定。下断点验证下 + + + + + +第二步:在 `NSLog` 处打断点,触发后 LLDB 模式下 `image list` 查看所有的 image。可以看到第一个 image 就是 App 主程序镜像,且 image 开始地址为 `0x0000000100da5000` + + + +第三步:进入 LLDB 模式,根据 image base 地址 + offset 计算 NSLog 的地址。即 `memory read 0x0000000102eec000+0xC000`,查看下内存信息 + + + +第四步:断点过下一行,即执行过一次 NSLog。然后再通过 LLDB 根据地址查看汇编代码,`dis -s addr` 查看 + + + +第五步:继续过断点,等执行完 `rebind_symbols` 再看看内存信息。可以看到再 rebind 之后,地址变了。然后根据地址查看汇编代码,发现已经是我们自定义的函数了。 + + + + + + + +第一步: 在 `Lazy Symbol Pointers` 懒加载符号表中看到第一个符号 `NSLog`,索引为1。 + + + +第二步:根据索引,在 `Dynamic Symbol Tables` 动态符号表中看到第一条数据,是 NSLog 相关的。其 Data 值 `00000084` 是十六进制的,换算为十进制就是 132。 + + + +第三步:根据第二步得到的角标,在 `Symbol Table` 符号表中查找第132个位置。可以看到其 Data 值 `000000AA` 是偏移值。 + + + +第四步:在 `String Table` 中,第一个位置 `0000CFE4` 加上偏移值 `0xAA` ,等于 `0xD08E`,如下图所示,就是 `NSLog` 符号真实的地址。 + + + + + +`NSLog`、`dispatch_once` 等,stub 代码指向 `lazy Symbol Pointers` 部分,`lazy Symbol Pointers` 中又指向 `stub_helper`,默认又到了 `dyld_stub_binder`,在首次调用时再绑定真实调用地址。 + +fishhook 正是利用上面这点,将 `lazy Symbol Pointer` 中的符号替换成自己的函数,从而实现 hook,这也是为什么 fishhook 不能 hook 二进制文件中自定义的 C 函数 -| 编号(符号) | 地址 | | -| ------ | -------- | --- | -| NSLog | 0xaabbcc | | -| ... | ... | | -![image-20200810201822593](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/mage-20200810201822593.png) fishhook 做的事情就是将系统的符号表,将符号表中的特定符号对应的地址,修改为自定义的函数地址。起到了 hook 作用。也就是说外部的 c 函数,在 iOS 中的调用属于**动态调用**。 -知道了 fishhook 的工作原理,我们就知道 fishhook 是 hook 不了自己写的 c 函数了。因为自定义函数是没有通过符号去寻找函数真正地址的这个过程。而系统库是通过符号去绑定真实地址的。可以通过 `rebind_symbols` 这个名称得以印证。 - -## 纠错 - -之前同事问了个问题:fishhook为什么能hook系统库的c方法,不能hook c++? - -1. FishHook 的原理是 ASRL + Lazy Symbol Table。系统库 NSLog 地址不确定的,会随机偏移,当 DYLD 加载后根据 offset 动态计算(也就是 rebinding、rebase)。 -2. Data 段可读可写,NSLog 位于 Data 段,自定义函数位于 Text 段,只读。所以C/C++ FishHook 可以hook 系统库/动态库共享缓存这些符号 -3. 知道机制后也就可以说:自定义符号是在 Text 段(Read Only),所以不能被 FishHook hook。另外系统库很多都是 c 实现。要是某个库是 C++ 实现,也可以 hook -4. - -总结版:FishHook 基于 ASRL + Lazy Symbol Table 运行,另外能不能 hook 要看代码是落在 Data 段(RW) 还是 Text(RO) +知道了 fishhook 的工作原理,我们就知道 fishhook 是 hook 不了自己写的 c 函数了。因为自定义函数是没有通过符号去寻找函数真正地址的这个过程。而系统库是通过符号去绑定真实地址的。 diff --git a/Chapter1 - iOS/1.89.md b/Chapter1 - iOS/1.89.md index 2ac855a..e73bbad 100644 --- a/Chapter1 - iOS/1.89.md +++ b/Chapter1 - iOS/1.89.md @@ -28,7 +28,7 @@ NSInteger age = 27; 用指令`xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转为 c++ - + `ViewController.m` 的 `viewDidLoad` 函数为 `_I_ViewController_viewDidLoad` @@ -200,7 +200,7 @@ struct __ViewController__viewDidLoad_block_desc_0 { } ``` - + @@ -214,7 +214,7 @@ struct __ViewController__viewDidLoad_block_desc_0 { - + @@ -237,11 +237,11 @@ printBlock(); 用 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ - + 概括如下: - + @@ -257,7 +257,7 @@ printAgeBlock(); 用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码 - + 代码分析: @@ -293,7 +293,7 @@ printInfoBlock(); 用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码 - + @@ -334,7 +334,7 @@ age is 28, height is 176 用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码 - + @@ -409,7 +409,7 @@ block 截获变量可以分为: 用指令 `xcrun --sdk iphoneos clang -arch arm64 Person.m -rewrite-objc -o Person-arm64.cpp` 转换为 c++ 代码 - + @@ -442,13 +442,13 @@ block 截获变量可以分为: 我们知道 block 可以看成是一个 oc 对象,所以它有类型,写个 Demo1 验证下 - + 也就是说: `__NSGlobalBlock__` -> `NSBlock` -> `NSObject`。 继续验证,Demo2 - + 同时利用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ @@ -473,7 +473,7 @@ block 的类型可以通过 isa 或者 class 方法查看,最终都是继承 这3种 block 在内存中的排布如下图: - + @@ -485,7 +485,7 @@ Demo: 由于 ARC 默认会做一些优化,我们将工程的 ARC 关掉(Build Setting 里 Automatic Reference Counting 设置为 No) - + 分析: @@ -497,7 +497,7 @@ Demo: 当 `__NSStackBlock__` 调用 copy 方法后会变为 `__NSMallocBlock__`。如下图: - + @@ -531,7 +531,7 @@ Demo 也同时发现,当对 `__NSGlobalBlock__` 调用 copy ,不会变为 ` MRC 下 block 作为函数的返回值是比较危险的。在方法内部,也就是栈上定义的 block,函数调用结束后可能一些相关数据就释放了,存在潜在风险。 - + @@ -539,13 +539,13 @@ MRC 下 block 作为函数的返回值是比较危险的。在方法内部,也 MRC 下如果函数返回值是 block,且 block 内做了 auto 变量捕获的逻辑,编译器会报错:`Returning block that lives on the local stack`。此时 block 应该为 `__NSStackBlock__` - + 改为 ARC,看看 - + 也就是说,ARC 模式下,当 block 捕获了 auto 变量,并且作为函数返回值的时候,ARC 会自动调用 copy 方法,将 `__NSStackBlock__` 变为 `__NSMallocBlock__` @@ -624,11 +624,11 @@ ARC 下:如果 block 调用 copy 方法,则 block 仍旧为 `__NSMallocBloc MRC 下,栈内捕获了 auto 变量的 block 为 `__NSStackBlock__` - + 改为 ARC - + @@ -656,31 +656,31 @@ MRC 下,栈内捕获了 auto 变量的 block 为 `__NSStackBlock__` 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`。 - + @@ -724,11 +724,11 @@ static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_objec Demo1: - + Demo2 - + @@ -738,13 +738,13 @@ Demo2 Demo3 - + 因为 GCD 是给 MainQueue 添加任务的,所以是串行,2个任务前后按照3s、1s 后打印。由于最晚的一个任务是访问强指针对象,所以不会释放。等到 GCD 全部执行完后,Person 才释放。 Demo4 - + @@ -829,7 +829,7 @@ MyBlock block = ^{ 转为 C++ - + ```c++ struct __Block_byref_age_0 { @@ -873,7 +873,7 @@ block 内部的函数在修改 age 的时候其实就是通过 `__main_block_imp - + @@ -885,7 +885,7 @@ QA:为什么`__block` 变量的 `__Block_byref_age_0` 结构体并不在 block 看个有趣的例子,验证下 __block 的效果 - + 转换成 c++ 可以看到 @@ -930,7 +930,7 @@ __attribute__((__blocks__(byref))) __Block_byref_num2_0 num2 = {(void*)0,(__Bloc 对` __block` 修饰的对象,clang 转换为 c++ 后如下: - + @@ -943,7 +943,7 @@ __attribute__((__blocks__(byref))) __Block_byref_num2_0 num2 = {(void*)0,(__Bloc 注意: - + @@ -1013,7 +1013,7 @@ int main(int argc, const char * argv[]) { 我们将断点设置到 NSLog 这里,打印出自定义结构体 `__main_block_impl_0` 中的 age 。 - + ```c // 0x0000000105231f70 @@ -1100,7 +1100,7 @@ in block: age = 28, address is 0x600000464938 - + 分析: @@ -1150,7 +1150,7 @@ in block: age = 28, address is 0x600000464938 那么` __forwarding` 的作用是什么?为什么这么设计 - + @@ -1168,17 +1168,17 @@ in block: age = 28, address is 0x600000464938 Demo1 - + - + Demo2 - + - + @@ -1304,7 +1304,7 @@ p.block(); `__unsafe_retained` 因为不安全所以不推荐,`__block` 因为使用繁琐,且必须等到调用 block 才会释放内存,所以不推荐。ARC 下最佳用 `__weak` -![](./../assets/block_object_cycle.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/block_object_cycle.png) diff --git a/Chapter1 - iOS/1.91.md b/Chapter1 - iOS/1.91.md index ac751bc..68c6b24 100644 --- a/Chapter1 - iOS/1.91.md +++ b/Chapter1 - iOS/1.91.md @@ -1,4 +1,4 @@ -# DYLD 及 Mach-O +# DYLD 及 Mach-O 什么是 DYLD?dynamic loader,动态加载器。在 MacOS/iOS 中,是使用 `/usr/lib/dyld` 程序来加载动态库的。 @@ -45,13 +45,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的位置, @@ -70,7 +70,7 @@ clang -target x86_64-apple-macos13.1 \ ## 输出重定向 - + 终端输入 `tty` 敲回车,然后在 Xcode 脚本中,就可以将 echo 输出的结果重定向到终端 `echo "message" > /dev/ttys000` @@ -82,7 +82,7 @@ Xcconfig 中定义的变量在 Run Script 中是可以访问的到。 Demo 验证如下: - + @@ -157,7 +157,7 @@ clang -target x86_64-apple-macos13.1 \ 使用 `nm -pa .o文件路径` 命令来查看符号 - + @@ -199,7 +199,7 @@ clang -target x86_64-apple-macos13.1 \ - + 进入 vim 模式了,看到左下角有 `:` 光标,如果想查看当前 nm 命令的参数,可以快速查找,输入 `/ + 具体参数`,敲回车即可跳转到要匹配到的位置,如果有多个结果,且当前自动跳转到的不是正确的位置,vim 模式下可以输入 `n` 跳转到下一个匹配到的位置(n 即 next),输入 `N` 则跳转到上一个匹配到的位置。 @@ -207,7 +207,7 @@ clang -target x86_64-apple-macos13.1 \ 比如查找 `-p`,则输入 `/-p`,敲回车的效果如下 - + @@ -255,7 +255,7 @@ QA:从符号角度出发,动态库还是静态库对于 App 瘦身较好( 静态库/ .o 文件 Strip 流程: 处理 `_DWARF` 段 - + Strip 的过程,就是在修改 Mach-O 文件中的内容。 @@ -265,11 +265,11 @@ Strip 的过程,就是在修改 Mach-O 文件中的内容。 动态库 - + All Symbols - + @@ -277,7 +277,7 @@ Non-Global Symbols(非全局符号): - + @@ -302,7 +302,7 @@ clang -x objective-c \ -c main.m -o main.o ``` - + 从 `.o` 目标文件链接为可执行文件 @@ -351,13 +351,13 @@ test.o -o test 第二步,利用 clang 将 `person.o` 转换为静态库。 - + 其实,这里就已经可以验证「静态库就是 .o 文件的合集」。 利用 `objdump --macho --private-header Person.dylib` 查看静态库依旧是 `Object File` - + 第三步,编写代码 `main.m` 代码,导入静态库 `` @@ -380,7 +380,7 @@ clang -x objective-c \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \ -I./StaticLibrary \ - -c main.m -o main. + -c main.m -o main.o ``` 第五步,将第四步得到的 `main.o` 文件和前面编译好的 `Person` 静态库 @@ -403,7 +403,7 @@ clang -target x86_64-apple-macos13.1 \ - 成功,则说明 静态库就是`.o` 文件的集合,单个 `main.o` 文件,修改拓展名就可以变为静态库 - 不成功,则相反 - + @@ -415,14 +415,24 @@ clang -target x86_64-apple-macos13.1 \ Mac OS/iOS 平台还可以使用 Framework,Framework 实际上是一种打包方式,将库的:二进制文件、头文件、和有关资源打包到一起,方便管理和分发。 - - Framework 和系统 UIKit.Framework 还是有很大区别的。 - 系统的 Framework 不需要拷贝到目标程序中 - 我们自己的 Framework 不管是静态还是动态的,都需要拷贝到 App 中(App 和 Extension 的 Bundle 是共享的) -因此,Apple 把这种 Framework 又叫做 Embedded Framework +因此,Apple 把这种 Framework 又叫做 `Embedded Framework`。开发中使用的动态库会被放到 ipa 的 framework 目录下,基于沙盒运行。 + +不同的 App 使用相拥的动态库,并不会只在系统中存在一份。而是在各个 App 中各自打包、签名、加载一份。 + + + +根据 Apple 审核要求,上传到 App Store 的 ipa 可执行文件有大小限制。这里的大小不是指二进制(Mach-O)文件大小,而是指去其中 `__TEXT` 段的大小。 + +- iOS 7 之前,二进制文件中所有 `__TEXT` 部分总和不得超过 80M +- iOS7.x 至 iOS8.x,二进制文件中,每个架构中的 `__TEXT` 部分不得超过 60M +- iOS9.0 之后,二进制文件中所有 `__TEXT` 部分的总和不超过 500M + +为了实现该效果,很多公司在组件化或者开源库,都采用动态链接的方式。因为动态链接的部分,不算在当前 Mach-O 的 `__TEXT` 段。 @@ -433,11 +443,60 @@ Framework: + + +QA:通常情况下,**同一份代码,一个库制作成动态库体积会比静态库小**。为什么? + +静态库是一堆 `.o` 文件的集合。假设 AFNetworking 有15个 `.m` 文件,编译后产生 15个 `.o` 文件。每个 `.o` 文件的都存在下面3部分: + +- Mach header +- Segment +- Section + +Mach header 包括一些基础信息,所以 Mach header 存在冗余(大小端序、CPU 类型等),这也就是为什么静态库比动态库体积大的原因之一。 + + + +```shell +Unix_Kernel  ~/Desktop/OCExplore/OCExplore  file Person.o +Person.o: Mach-O 64-bit object x86_64 + Unix_Kernel  ~/Desktop/OCExplore/OCExplore  otool -h Person.o +Person.o: +Mach header + magic cputype cpusubtype caps filetype ncmds sizeofcmds flags + 0xfeedfacf 16777223 3 0x00 1 4 1880 0x00002000 +``` + + + +动态库在 Mach header 这里有改进,AFNetworking 动态库格式如下: + + + +将公共的信息放到一起,公用一个 Mach header。 + + + +但「同一份代码,一个库制作成动态库体积会比静态库小」不绝对。 + +对于 iOS9,Load Commands Segment vmsize 默认是一个内存页,也就是16k + +对于 iOS10,Load Commands Segment vmsize 默认是 32k + +- vmsize:此 segment 占用的虚拟内存的字节数 +- filesize:此 segment 在磁盘上占用的内存数 + +假设项目只有1个源文件,可能打包后的静态库要比动态库体积大。 + + + + + 继续做个实验,验证下 Framework 的结构(上面做了静态库),所以我们可以沿用上面的成果,将静态库包装成 Framework 第一步,新建一个文件夹 `Framworks`,下面创建一个 `Person.Framework` 文件夹,把之前得到的静态库 `Person` 移动到该目录下。并把 `main.m` 移动过去。 - + 第二步,因为 Framework 的结构里有 Header 信息,所以创建 `Headers` 文件夹,把 `Person.h` 文件放进去。 @@ -483,7 +542,7 @@ clang -target x86_64-apple-macos13.1 \ 第一步:创建 dylib 文件夹,下面创建 `Person.h` `Person.m` 类。在 dylib 同层目录创建 main.m 文件。代码如下 - + 第二步:对 main.m 编译成 main.o 文件,指令为 @@ -569,13 +628,13 @@ echo "---------------- Done --------------" 结果如下: - + 第六步:对生成的 main 可执行文件进行调试运行,使用 lldb 指令 `lldb -file 可执行文件`,然后输入 r 进行运行: - + @@ -654,7 +713,7 @@ echo "---------------- Done --------------" 执行完脚本,又出现了奇怪的现象: - + @@ -680,7 +739,7 @@ ld -dylib -arch x86_64 \ 再次运行 build 脚本,然后对可执行文件执行,还是报错 😂 - + @@ -699,19 +758,19 @@ 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` 指令 - + 可以发现: @@ -728,17 +787,17 @@ ld -dylib -arch x86_64 \ #### 方式一:通过 `install_name_tool` 指令 - + 通过改变动态库 name 来修改动态库的路径。具体指令为 `install_name_tool -id /Users/unix_kernel/Desktop/LDAndFramework/StaticLibraryLinkedIntoDynamicLibrary/dylib/libPerson.dylib libPerson.dylib` 修改动态库的 name 之后,再次 `otool -l libPerson.dylib | grep 'DYLIB' -A 5` 查看路径信息 - + 动态库有了正确的 name 后,再重新链接生成可执行文件。可执行文件可以正确运行,查看所以来的动态库路径,均正确加载。 - + @@ -844,7 +903,7 @@ install_name_tool -add_rpath /Users/unix_kernel/Desktop/LDAndFramework/StaticLib 下面是上面全部步骤的截图说明。 - + @@ -873,7 +932,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 也是这么干的 @@ -881,7 +940,7 @@ install_name_tool -rpath /Users/unix_kernel/Desktop/LDAndFramework/StaticLibrary LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' ``` - + @@ -897,7 +956,7 @@ LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_pa 这样一个场景,代码模拟下,文件目录如下 - + 1. 在 Cat.framework 文件夹下运行 build.sh 2. 在 Person.framework 文件夹下运行 build.sh @@ -1019,7 +1078,7 @@ echo "---------------- Done --------------" 运行报错如下: - + 为什么还错误了?都已经给可执行文件添加了 `@executable_path`,给2个动态库都添加了 `@rpath`,怎么办? @@ -1061,7 +1120,7 @@ otool -l Person | grep 'ID' -A 5 在 Person.framework运行下 build.sh,然后在根目录下运行 build.sh,得到新的可执行文件,然后可以成功运行 - + 反思:可执行文件依赖动态库 A,动态库 A 依赖动态库 B,上面的配置很繁琐: @@ -1077,13 +1136,13 @@ otool -l Person | grep 'ID' -A 5 注:为了方便看清楚脚本执行情况和可执行文件执行结果,这次运行注释了 otool 的打印脚本。 - + `loader_path` 是标准解决方案。随便打开 AFNetworking 工程看看 - + @@ -1116,13 +1175,13 @@ sudo codesign --force --deep --sign - (应用路径) 发现 Person 没有导出 Cat 的符号。那在可执行文件中调用不了 Cat 的能力了。 - + 怎么办呢?链接器 LD 已经是很成熟的东西了,对于处理动态库依赖了动态库,且需要将被依赖动态库的符号导出,这样的需求早已满足了。具体是什么参数?终端输入 `man ld` 查看下指令 - + 其中,我们需要用的是 `-reexport_framework` 这个参数。指令为 ` -Xlinker -reexport_framework -Xlinker Cat` @@ -1163,11 +1222,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 的符号。 @@ -1222,7 +1281,7 @@ otool -l Person | grep 'ID' -A 5 修改完从里到外一次性执行 build.sh,得到 main 可执行文件。一切顺利,输出如下: - + @@ -1292,17 +1351,17 @@ xcodebuild -workspace Person.xcworkspace \ 打包成功的输出如下: - + 实体目录如下: - + 注意:我们打败归档后生成的符号表只有 `.dSYM` 文件,没有 `BCSymbolMaps` 文件,是因为工程没有开启 BitCode,当开启 BitCode 后的,打包会生成 `BCSymbolMaps` 文件,作用也是用于 Crash 后解析堆栈用的。 因为 `.dSYM` 文件是默认生成的,但是 `Bitcode` 需要分别开启(Pod 和 Demo 工程)。开启 Bitcode 后重新打包,看看生成的产物里有没有 `BCSymbolMaps` 和相关的 `.bcsymbolmap` 文件 - + @@ -1317,7 +1376,7 @@ xcodebuild -create-xcframework \ 结果如下: - + 可以看到打包后的 xcframework 自动处理了文件夹。但是缺点是我们提供的 framework,别人使用可能会 crash,所以为了一些使用场景需要在 xcframework 中提供 `.dSYM` 文件或者 `.bcsymbolmap` 文件。 @@ -1336,7 +1395,7 @@ xcodebuild -create-xcframework \ 结果如下 - + 可以看到 xcframework 里面不只有不同的动态库,还携带了对应的 `.dSYM` 和 `bcsymbolmap` 文件,用于堆栈、符号还原。 @@ -1350,11 +1409,11 @@ xcodebuild -create-xcframework \ - import 头文件并使用 - + - 编译运行,查看 products 下面的产物,因为选择的是模拟器运行,所以验证 `Person.framework` 里面的动态库文件大小,是否和 `Person.xcframework` 里面模拟器目录下 Person 动态库的大小一致 - + 可以看到我们打包的 `Person.xcframework` 可以正常使用,除此之外,`Person.xcframework` 包含了模拟器和真机的动态库文件和对应的 `.dSYM` 和 `.bcsymbolmap` 文件,当导入到项目中的时候,Xcode 会根据当前编译的架构,自动从里面选择合适的架构文件。 @@ -1378,13 +1437,13 @@ xcodebuild -create-xcframework \ 第三步:编译运行。 - + 结论:编译正常,但是运行会报错 `Library not loaded: @rpath/Person.framework/Person` 第一种解决方案是给 xcconfig 添加 rpath 的具体路径。 - + 第二种解决方案是将库声明为“弱引用”。输入 `man ld` 查看具体的参数和说明: @@ -1412,7 +1471,7 @@ OTHER_LDFLAGS = $(inherited) -Xlinker -weak_framework -Xlinker "Person" 我们对添加了 `_weak-framework` 这个链接器参数的可执行文件查看下,指令为 `otool -l WeakImportDemo` - + 查看 Mach-O 发现,从 `LC_LOAD_DYLIB` 变为 `LC_LOAD_WEAK_DYLIB` @@ -1451,7 +1510,7 @@ OTHER_LDFLAGS = $(inherited) -l"AFNetworking" -l"AFNetworking2" 第四步:尝试编译,编译成功。 - + QA:为什么链接2个同名同符号的静态库,编译不报错? @@ -1461,7 +1520,7 @@ QA:为什么链接2个同名同符号的静态库,编译不报错? 可以验证下,在链接的时候修改链接参数为 `-all_load` 就会报错 - + @@ -1481,7 +1540,7 @@ LD 提供了 ` -load_hidden` 参数。 OTHER_LDFLAGS = $(inherited) -l"AFNetworking" -l"AFNetworking2" -Xlinker -force_load -Xlinker "${SRCROOT}/AFNetworking/libAFNetworking.a" -Xlinker -load_hidden -Xlinker "${SRCROOT}/AFNetworking2/libAFNetworking2.a" ``` - + 具体代码见: `LDAndFramework/StaticLibConflictsSolution/StaticLibConflictsDemo` @@ -1500,7 +1559,7 @@ OTHER_LDFLAGS = $(inherited) -l"AFNetworking" -l"AFNetworking2" -Xlinker -force_ 发现编译通过,运行报错。 - + 为什么?原因 @@ -1510,7 +1569,7 @@ OTHER_LDFLAGS = $(inherited) -l"AFNetworking" -l"AFNetworking2" -Xlinker -force_ 使用的 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` 是在二进制文件生成后对其进行修改) @@ -1524,19 +1583,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 文件夹下。 @@ -1558,7 +1617,7 @@ Tips 第三步:framework 中增加实现代码,使用 App 中的符号 - + @@ -1583,7 +1642,7 @@ OTHER_LDFLAGS = $(inherited) -framework "AFNetworking" -Xlinker -undefined -Xlin OTHER_LDFLAGS = $(inherited) -framework "AFNetworking" -Xlinker -U -Xlinker _OBJC_CLASS_$_NetworkObject ``` - + @@ -1597,11 +1656,11 @@ OTHER_LDFLAGS = $(inherited) -framework "AFNetworking" -Xlinker -U -Xlinker _OBJ 打开 NetworkManager 动态库的 Mach-O 查看下符号,可以看到动态库 NetworkManager 中已经包含了静态库 AFNetworking 的符号。 - + 如何成功编译?告诉链接器 HEADER_SEARCH_PATH 信息即可。 - + @@ -1613,7 +1672,7 @@ LD 链接器提供了能力,将静态库的符号不暴露出来。 OTHER_LDFLAGS = $(inherited) -ObjC -Xlinker -hidden-l"AFNetworking" ``` - + @@ -1626,7 +1685,7 @@ OTHER_LDFLAGS = $(inherited) -ObjC -Xlinker -hidden-l"AFNetworking" 之后编译报错 - + 此时 NetworkManager 静态库中只有自己的符号,没有所依赖的 AFNetworking 中的符号。 @@ -1638,7 +1697,7 @@ App 链接 NetworkManager 静态库,需要告诉链接器:头文件、库名 - + 方法二:直接给 App install 静态库。 @@ -1655,7 +1714,7 @@ App 链接 NetworkManager 静态库,需要告诉链接器:头文件、库名 编译报错,符号未定义 。 - + 所以症结所在:就是把动态库的代码也放到 App 里面,App 才可以使用动态库里面的符号。上面代码运行,NetworkManager 静态库调用 AFNetworking 动态库的能力,就相当于 App 直接访问 AFNetworking 动态库的符号一样。 @@ -1666,7 +1725,7 @@ App 没有配置关于 AFNetworking 的链接三要素,所以找不到 AF。 修改后编译没问题,运行报错,因为编译后的 AFNetworking 没有拷贝到 App 所需目录下. - + 怎么处理? @@ -1675,7 +1734,7 @@ App 没有配置关于 AFNetworking 的链接三要素,所以找不到 AF。 运行成功。 - + @@ -1739,13 +1798,13 @@ end 假设没有开启 module 能力,当使用 `#include <*.h>` 的时候, 头文件 A、B 在 `use.c ` `another-use.c` 中,include 几次就要编译几次。 - + 编译 use.c 就要编译 include 进来的 A、B。编译 another-use.c 同样要编译 A、B。效率低下。 - `import`:与 `include` 相对应,`import ` 语句用于导入模块,而不是简单的文本包含。使用模块可以减少编译时间,因为编译器只需要编译模块的接口而不是整个模块的实现 - + `clang -fmodules -fmodule-map-file=mo dule.modulemap -fmodules-cache-path=./moduleCache -c use.c -o use.o` : @@ -1959,7 +2018,7 @@ framework module AsyncDisplayKit { // 定义了一个名为 AsyncDisplayKit 的 第三步:给主工程、动态库设置在同一个 Xcode 项目中编译调试。 - + @@ -2026,11 +2085,11 @@ framework module AsyncDisplayKit { // 定义了一个名为 AsyncDisplayKit 的 利用 modulemap 解决。 - + 但是,上面的做法有副作用。虽然 Framework 内部 Swift 可以访问 OC 了,但是使用 Framework 的地方,也可以访问到 OCStudent 类。如何解决该问题? - + @@ -2038,7 +2097,7 @@ framework module AsyncDisplayKit { // 定义了一个名为 AsyncDisplayKit 的 module 提供 private modle 的能力。 - + 说明: @@ -2086,11 +2145,11 @@ Swift 库引入了一个全新的文件 `.swiftmodule`,其包含序列化过 编写脚本: `cp -Rv -- "${BUILT_PRODUCTS_DIR}/" "${SOURCE_ROOT}/../Products"`,把产物拷贝到 products 目录下 - + 第二步,打算用 libtool 指令 `libtool -static FLSwiftLibA.framework/FLSwiftLibA FLSwiftLibB.framework/FLSwiftLibB -o libFLSwiftC.a` 进行合并,发现报错 - + @@ -2135,7 +2194,7 @@ Swift 库引入了一个全新的文件 `.swiftmodule`,其包含序列化过 1.为什么 App 工程中的 OC 类中导入头文件编译成功,但还是无法访问里面的类? - + 同样,需要一个新的参数,告诉 LD modulemap 的信息,然后系统根据 module.modulemap 去关联查找 `FLSwiftLibA.swiftmodule` 里面的 `x86_64-apple-ios-simulator.swiftmodule` swiftmodule 文件信息。 @@ -2147,13 +2206,13 @@ Swift 库引入了一个全新的文件 `.swiftmodule`,其包含序列化过 而 Swift 编译器是 swiftc,需要额外配置。`SWIFT_INCLUDE_PATHS = "${SRCROOT}/FLStaticLibC/Public/FLSwiftLibA.framework/Modules/" "${SRCROOT}/FLStaticLibC/Public/FLSwiftLibB.framework/Modules/"` - + 但是配置后还是报错,因为看上去文件路径是对的,Frmaework 生成的时候,就是这么划分文件夹的。但实际编译的时候 Headers和 `module.modulemap` 、`.swiftmodule` 文件夹在同一层目录。所以我们手动编译静态库之后,需要处理静态库产物的文件目录信息。 - + @@ -2311,7 +2370,7 @@ import 举个例子吧。在 `Person.m` 创建一个 iOS App,默认模版的基础上添加一个 Person 类,一个继承自 Person 的 Student 类。然后编译 - + @@ -2323,7 +2382,7 @@ import 举个例子吧。在 `Person.m` `hmap` 文件不可读,必须用对应的工具解析,按照指定的格式解析。因为属于编译器的 scope,所以查看 LLVM 源码窥探下。 - + 可以看到 hmap 结构类似 Mach-O ,顶部 HMapHeader 告诉系统,当前有几个 Bucket,下面是 Bucket 信息。然后最下方是 string 区域。 @@ -2344,9 +2403,9 @@ import 举个例子吧。在 `Person.m` 运行调试的时候,把需要读取的 HMapFile 拷贝到项目根目录。然后 Edit Scheme - Run - Arguments Passed On Launch - + - + 如何做到该工具做到像系统自带的 `objdump` 一样,在终端命令行使用: @@ -2358,7 +2417,7 @@ import 举个例子吧。在 `Person.m` 效果如下: - + @@ -2397,7 +2456,7 @@ Xcode 会主动生成 `.hmap` 文件,那为什么还需要研究生成 `hmap` 然后查看编译日志中的 ` HMapStaticLibApp-project-headers.hmap` 文件。利用上面制作的 `HMapDump` 工具。 - + 分析:在 App 使用 Static Library 的情况下,假设开启了 `Use Header Map`,静态库中所有头文件类型为 `Project`(只有 Project、Private、Public 3种类型,public 就是字面意思的公开,p r)的情况,最终生成的 `.hmap` 文件中只会包含类似 `#import "Student.h"` 的键值引用。也就是说使用的地方,只有 `#import "Student.h"` 的这种方式才会走 hmap 策略,否则还是走 `Header Search Path` 来寻找头文件路径。 @@ -2444,7 +2503,7 @@ LLVM 真是好东西,把 Xcode 编译、链接等一些幕后的事情变成 第四步,编译使用静态库的 App 工程。最后比较静态库走自定义 `.hmap` 前后的编译耗时。 - + 可以看到静态库使用了自定义的 Header Maps 文件后,使用静态的 App 前后,编译耗时减少了0.1s。 @@ -2520,7 +2579,7 @@ dyld 是一个动态链接程序。是 iOS 和 macOS 系统中的**动态链接 objdump --macho --private-headers DSYMDemo ``` - + - **启动阶段**:应用程序启动时,iOS 系统首先解析 `Info.plist` 文件,加载相关信息,例如启动画面等,并建立沙盒环境以及进行权限检查 @@ -2633,13 +2692,13 @@ objdump --macho --private-headers DSYMDemo stacksize 0 // 这个字段指定了为程序的主线程初始栈分配的大小。在这个例子中,栈大小被设置为0,这可能意味着栈的大小将使用默认值,或者在其他地方指定(例如,通过链接器的其他设置或命令行选项) ``` - + ### dyld 加载过程 - + @@ -2738,7 +2797,7 @@ int main(int argc, const char* argv[]) dyld 本身就是一种 MachO 文件,MachO 文件类型为7,是一种包含多种架构的结构,如下图: - + 可以加载以下类型的 Mach-O 文件:MH_EXECUTE、MH_DYLIB、MH_BUNDLE @@ -2767,11 +2826,11 @@ static void customConstructor(int argc, const char **argv) { 第三步:Edit scheme -> Run -> Environment Variables。Name 为 `DYLD_INSERT_LIBRARIES`,value 为 InjectFunction 动态库路径,`${SRCROOT}/Inject`。 - + 第四步:运行输出如下 - + 具体 [Demo]() @@ -2814,7 +2873,7 @@ INTERPOSE(my_NSLog, NSLog); 第四步:运行输出。发现 NSLog 确实被 hook 做了替换。 - + @@ -2839,13 +2898,13 @@ INTERPOSE(my_NSLog, NSLog); `环境变量=1 ./可执行文件` - + 另一种方式是利用 Xcode -> Edit Scheme,增加或修改 dyld 环境变量 - + @@ -2897,7 +2956,7 @@ Mach-O 格式⽤来替代 BSD 系统的 `a.out` 格式。Mach-O ⽂件格式保 Xcode 中也可以查看 Mach-O 文件类型 - + @@ -2905,7 +2964,7 @@ Xcode 中也可以查看 Mach-O 文件类型 Tips:`file` 命令可以查看文件类型。 - + `find . -name "*.c"` 比如在当前路径查找 .c 文件 @@ -2938,7 +2997,7 @@ Xcode 中可以修改指令集。由 Build Settings 的2个配置决定: `Arch ### Mach-O 结构 - + 一个 Mach-O 文件包含3块 @@ -2950,17 +3009,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)。 @@ -2993,7 +3052,7 @@ int main () { 第五步,查看目标文件指令信息 `objdump --macho -d main.o` - + 分析下指令信息: @@ -3006,7 +3065,7 @@ int main () { - test1、test2 2个函数都存在,地址不一样,为什么都是 `00 00 00 00 ` ?那系统如何确定函数真实地址?需要找个地方将这些符号存起来,然后再找个时机去把真实的地址写进去。需要**重定位符号表**,告诉链接器在链接阶段需要重定位 - + - 使用 `objdump --macho --reloc main.o` 指令查看 main.o 的重定位符号表。 - 符号表中 `test1` 的地址就是 `0000004a` 对应的数据。`49` 后面就是 `4a` @@ -3036,7 +3095,7 @@ iOS 是小端序,从右向左看,`b2 ff ff ff` 可以看到后4位是 ff, 完整的 - + @@ -3044,7 +3103,7 @@ iOS 是小端序,从右向左看,`b2 ff ff ff` 可以看到后4位是 ff, 第一步,先用指令查看全局变量的地址值。 `objdump --macho -s main`,可以看到地址值为 `0x100008000` - + @@ -3093,17 +3152,17 @@ DWARF 是被众多编译器和调试器使用的,用于支持源码级别调 第三步:查看 Mach-O 中,包含 `__DWARF` 的段,使用指令 `objdump --macho --private-headers main.o` - + 第四步:编译成可执行文件,指令为 `clang -g main.m -o main` 第五步:查看可执行文件中是否包含 `__DWARF` 段。`objdump --macho --private-headers main` - + 第六步:查看可执行文件的符号表。指令为:`nm -pa main`。红色区域代表调试符号。 - + @@ -3114,7 +3173,7 @@ DWARF 是被众多编译器和调试器使用的,用于支持源码级别调 - 指令 `clang -g1 main.m -o main`,参数 `-g1` 用于生成 `.dSYM` 文件。 - 指令 `dwarfdump main.dSYM` 用于查看 `.dSYM` 文件 - + @@ -3141,7 +3200,7 @@ DWARF 是被众多编译器和调试器使用的,用于支持源码级别调 第二步:Mac 自带 `console` App 查看崩溃报告,因为有 `.dSYM` 文件,所以可以看到方法信息 - + @@ -3241,12 +3300,40 @@ uintptr_t get_slide_address(void) { // 获取 ASLR - 符号所在代码文件 - 符号所在代码文件的多少行 - + ## ASLR +### 虚拟内存、物理内存、内存分页、ASLR + +早期:应用程序的可执行文件是存储在磁盘上的,启动应用,则需要将可执行文件加载进内存,早期计算机是没有虚拟内存的,一旦加载就会全部加载到内存中,并且进程都是按照顺序排列的,这样存在两个问题: + +- 当前进程只需要在合适的地址基础上,加减一些地址,就能访问到下一个进程所使用的内存,安全性很低 + +- 当前软件越来越大,开启多个软件时会直接全部加载到内存中,导致内存不够用。而且使用软件的时候大多数情况只使用部分功能。如果软件一打开就全部加载到内存中,会造成内存的严重浪费。 + +基于上述2个问题,诞生了虚拟内存技术。App 进程通过内存管理单元(Memory Management Unit, MMU)来管理的。MMU 使用一张特殊的表来跟踪虚拟内存地址和物理内存地址之间的映射关系。这张表通常被称为页表(Page Table) + +内存分页: + +- 页表结构:页表包含了一系列的表项,每个表项对应虚拟内存中的一个页(通常是 4KB)。每个表项包含了该虚拟页对应的物理页的信息,包括物理页的起始地址和一些状态信息。 +- 虚拟地址到物理地址的转换:当程序访问一个虚拟地址时,MMU 查看页表来找到对应的表项,然后根据表项中的信息将虚拟地址转换为物理地址。 +- 页表缓存(TLB - Translation Lookaside Buffer):为了提高地址转换的速度,MMU 通常会有一个 TLB,它缓存了最近访问的页表项。这样,对于频繁访问的地址,转换过程可以更快地完成 +- 页错误处理:如果一个虚拟地址在页表中没有对应的条目,就会发生页错误。操作系统的页错误处理程序会被调用,它负责加载缺失的页到物理内存中,并更新页表。 +- 内存分配:当应用程序请求内存时,操作系统会分配虚拟地址空间,并在页表中创建相应的条目。如果需要,操作系统还会分配物理内存页,并将其与虚拟页关联起来。 + +当物理内存满的时候,会发生覆盖。用户使用的活跃的数据,覆盖内存中最不活跃的数据那一页。对应现实的表现就是:iPhone 上永远可以较好的打开一个 App,比如打开了 App1、App2、App3...,等打开使用了一阵子后,内存紧张,我们尝试切换打开最早的 App1,会发现 App1 重新启动了,之前用的功能 A 的页面1,已经不见了,经历一个新的启动流程。 + + + +但虚拟内存方案带来一个问题。比如黑客不断探索发现,某个重要的功能位于第3页,是不是完全可以通过固定的地址去访问?? + +因为早期物理内存方案下,App 启动后位于什么地址是不确定的。有了虚拟内存后,App 内符号的地址都是从0到4G,都是相对地址。 + +为了解决该问题,Apple 又推出了 ASLR 技术。核心就是为了解决虚拟内存地址固定不变的问题。就不能通过固定的地址访问内存或者数据了。 + ### 未使用 ASLR 的问题 - 函数代码存放在 `__TEXT` 段 @@ -3263,13 +3350,13 @@ uintptr_t get_slide_address(void) { // 获取 ASLR 也可以使用 `size -l -m -x DDD` 指令来查看 Mach-O 的内存分布 - + 利用 MachOView 查看如下: - + - + @@ -3291,7 +3378,7 @@ uintptr_t get_slide_address(void) { // 获取 ASLR - + @@ -3303,7 +3390,7 @@ File Size:在 Mach-O 文件中的占据的大小 从下面的图可以看出,`_PAGEZERO` 在真实的 Mach-O 文件中不存在,不占据大小。只在虚拟内存中存在。 - + @@ -3317,7 +3404,7 @@ File Size:在 Mach-O 文件中的占据的大小 Address Space Layout Randomization,地址空间布局随机化。是一种针对缓冲区溢出的安全保护技术,通过堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码的位置,达到阻止溢出攻击目的的一种技术。在 iOS 4.3 引入。 - + diff --git a/Chapter5 - Network/5.1.md b/Chapter5 - Network/5.1.md index dc52c95..5d9686e 100644 --- a/Chapter5 - Network/5.1.md +++ b/Chapter5 - Network/5.1.md @@ -151,7 +151,7 @@ TCP 会利用另一种机制来解决超时重传带来的时间等待问题, 因为 HTTP 无连接,客户端和服务端交互的时候,打开一个 TCP 连接,然后交互,然后关闭 TCP 连接。下次需要交互的时候,继续打开一个 TCP 连接,继续交互,最后又关闭 TCP 连接。 - + @@ -180,7 +180,7 @@ Session、Cookie 都是针对 HTTP 协议无状态特点的补偿。 Cookie 主要用来记录用户状态,区分用户;状态保存在客户端。 - + 客户端请求服务端的时候,服务端生成 Cookie,通过 HTTP 响应报文,Header 部分中 `Set-Cookie` 首部字段设置 Cookie。 @@ -214,7 +214,7 @@ Session 也用来记录用户状态,区分用户。只不过状态保存在服 Session 需要依赖于 Cookie 机制。 - + @@ -328,7 +328,7 @@ ETag 即 Entity Tag,代表资源的唯一标识。主要用来解决修改时 代理体现在头信息上就是字段 `Via`,是一个通用字段,请求头或响应头里都可以出现。每当报文经过一个代理节点,代理服务器就会把自身的信息追加到字段的末尾,就像是经手人盖了一个章。如果通信链路中有很多中间代理,就会在 Via 里形成一个链表,这样就可以知道报文究竟走过了多少个环节才到达了目的地。 -![](./../assets/HTTPProxyVia.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/HTTPProxyVia.png) `X-Forwarded-For` 的字面意思是“为谁而转发”,形式上和“Via”差不多,也是每经过一个代理节点就会在字段里追加一个信息。但“Via”追加的是代理主机名(或者域名),而“X-Forwarded-For”追加的是请求方的 IP 地址。所以,在字段里最左边的 IP 地址就是客户端的地址 @@ -365,7 +365,7 @@ Host: www.xxx.com\r\n HTTP 传输链路上,不只是客户端有缓存,服务器上的缓存也是非常有价值的,可以让请求不必走完整个后续处理流程,"就近"获得响应结果。特别是对于那些“读多写少”的数据,例如突发热点新闻、爆款商品的详情页,一秒钟内可能有成千上万次的请求。即使仅仅缓存数秒钟,也能够把巨大的访问流量挡在外面,让 RPS(request per second)降低好几个数量级,减轻应用服务器的并发压力,对性能的改善是非常显著的。 -![](./../assets/CacheProxyServer.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CacheProxyServer.png) 代理服务器没有缓存的时候:代理服务器每次直接转发来自客户端的报文给服务端,转发服务端的报文给客户端,中间不会存储任何数据,只有基础的中转功能。 @@ -397,7 +397,7 @@ HTTP 传输链路上,不只是客户端有缓存,服务器上的缓存也是 下图是完整的服务器端缓存控制策略,可以同时控制客户端和代理 -![](./../assets/CacheControlPipeline.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CacheControlPipeline.png) ### 客户端的缓存控制 @@ -477,7 +477,7 @@ DNS 解析查询方式: - 递归查询,核心就是“我去给你问一下” - + 客户端根据网址去请求服务器之前,会先获取 IP 地址信息。 @@ -486,7 +486,7 @@ DNS 解析查询方式: - 迭代查询,核心就是“我告诉你谁可能知道” - + - 客户端发送请求的时候问一下本地 DNS 服务器,该域名对应的 IP 地址是什么,本地 DNS 服务器说“我不知道,你去问问根域名服务器,它可能知道” @@ -506,7 +506,7 @@ DNS 解析查询方式: ##### 什么是 DNS 劫持 - + @@ -531,13 +531,13 @@ QA:DNS 劫持和 HTTP 的关系是什么? 从“使用 DNS 协议向 DNS 服务器的53端口请求” 变成“使用 HTTP 协议向 HTTP 服务器的80端口请求” - + 客户端通过 IP 直连的方式,向 DNS 服务器,通过 HTTP Get 请求的方式,携带域名参数,然后响应一个具体的 IP 地址值给客户端。剩余流程就是拿着请求后的 IP 地址去完成其他逻辑。 - 长连接 - + 在客户端和业务服务器之间,建立一个长连接 Server,可以理解成代理服务器。 @@ -551,7 +551,7 @@ QA:DNS 劫持和 HTTP 的关系是什么? #### DNS 解析转发 - + 比如某移动 App 发起网络请求,移动 DNS 服务器为了节省资源,将请求转发到某电信 DNS 服务器,用于帮助移动 DNS 服务器,解析域名,获取对应的 IP 地址。 diff --git a/Chapter5 - Network/5.2.md b/Chapter5 - Network/5.2.md index 2397702..b0b26c1 100644 --- a/Chapter5 - Network/5.2.md +++ b/Chapter5 - Network/5.2.md @@ -60,7 +60,7 @@ UDP 不止支持一对一的传输方式,同样支持一对多,多对多, 发送方的 UDP 对应用程序交下来的报文,在添加首部后就向下交付 IP 层。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文 - + @@ -96,13 +96,13 @@ UDP 在发送消息时,在传输层直接就将一个消息打包成一个完 复用:UDP 多端口复用指的是在 UDP 通信中,可以通过一个 UDP 端口同时处理多个不同的应用程序或服务的通信。这种技术允许多个应用程序共享同一个 UDP 端口进行通信,而不需要为每个应用程序分配独立的端口。通过在接收数据时区分不同的目标端口,可以实现对多个应用程序的数据传输和处理。这种方式可以提高网络资源的利用率和简化网络配置,但需要确保在接收端能够正确解析和处理来自不同端口的数据。 - + 分用:UDP 多端口分用是指在 UDP 通信中,可以通过一个应用程序或服务同时监听和处理多个不同的 UDP 端口。这种技术允许一个应用程序在同一时间接收来自多个不同 UDP 端口的数据包,并根据端口信息来区分和处理这些数据。通过 UDP 多端口分用,一个应用程序可以灵活地处理多个 UDP 端口上的数据,实现更高效的网络通信和数据处理 - + #### 差错检测 @@ -137,7 +137,7 @@ HTTP/1.0 为每次 HTTP 请求/相应都建立一条新的 TCP 链接。因此 #### 为什么需要3次握手? - + @@ -168,7 +168,7 @@ HTTP/1.0 为每次 HTTP 请求/相应都建立一条新的 TCP 链接。因此 #### 为什么需要4次挥手 - + @@ -211,13 +211,13 @@ HTTP/1.0 为每次 HTTP 请求/相应都建立一条新的 TCP 链接。因此 - 无差错情况 - + 这张图是正常传输的情况。没有发生任何错误 - 超时重传 - + 这张图是一个超时重传的情况。客户端给服务端发送了 M1 分组报文,由于网络环境比较差,丢失或者滞留,或者被劫持了篡改了,当劫持篡改的情况下服务端接收到 M1 后,会判断篡改后丢弃该报文。在期许的时间内,客户端没有收到分组报文 M1 的确认,认为发生了超时。触发重传机制。然后启动一个分组报文 M1 的重传,此时网络正常,服务端收到 M1,并发送确认给客户端。客户端收到确认报文后,再发送 M2... @@ -227,13 +227,13 @@ HTTP/1.0 为每次 HTTP 请求/相应都建立一条新的 TCP 链接。因此 - 确认丢失 - + 客户端发送一个分组报文 M1,服务端收到后发送一个确认报文,但这个确认报文丢失了。此时客户端依旧通过超时定时器,判断在期许的时间内没有收到来自服务端的确认报文,触发超时重传策略。重新发送分组报文 M1,服务端收到 M1 后,由于服务端已经接收过分组报文 M1 了,此时服务端做2件事:丢失重传的 M1 报文;重传确认 M1 报文。客户端收到 M1 确认报文,然后继续发送 M2... - 确认迟到 - + 客户端发送 M1 报文,服务端收到 M1 后发送确认报文,但是由于网络情况不好,传输较慢,客户端在期许时间范围内没有收到确认报文。客户端在超时定时器的作用下,判断超时,触发重传策略。重新发送报文 M1,服务端收到重传的 M1 后,由于服务端之前已经接受过 M1 报文,所以做2件事情:丢弃重传的 M1 报文;重传 M1 确认报文。 @@ -246,7 +246,7 @@ HTTP/1.0 为每次 HTTP 请求/相应都建立一条新的 TCP 链接。因此 #### 面向字节流 TCP 不像 UDP 那样一个个报文独立传输,而是在不保留报文边界的情况下以字节流方式进行传输。 - + 发送方在进行数据发送的时候,在 TCP 层面会有一个发送缓存。接收方也有一个接收缓存。 @@ -279,14 +279,14 @@ TCP 不像 UDP 那样一个个报文独立传输,而是在不保留报文边 从左到右,字节编号,序号逐渐增大。 - + - 可用窗口,主要是尽快要发送的部分 - 重传队列,主要是发送但未被确认的部分 当发送但未被确认的部分,收到确认之后,窗口将进行合拢。假设 7、8、9 被发送后,变成下面的状态 - + 7、8、9变成了发送未被确认的状态,10、11、12变成了需要尽快发送的状态,窗口右移。 @@ -302,7 +302,7 @@ TCP 不像 UDP 那样一个个报文独立传输,而是在不保留报文边 在接收方侧: - + @@ -322,7 +322,7 @@ TCP 不像 UDP 那样一个个报文独立传输,而是在不保留报文边 ##### 慢开始、拥塞避免 - + @@ -342,7 +342,7 @@ TCP 实现可靠传输依赖的是 **超时重传** 机制。TCP 在发送完数 这个是默认的情况,但是传输效率还是有点低,所以 TCP 为了更高的效率,采取了快重传机制。什么是快重传? - + - 左边是发送方,右边是接收方 - 发送了序号为1的报文后,接收方收到,回复了一个 ACK2 的报文,ACK2 的意思就是“我收到报文1了,接下来我希望收到序号为2的报文” @@ -366,7 +366,7 @@ TCP 实现可靠传输依赖的是 **超时重传** 机制。TCP 在发送完数 ##### 快恢复 - + diff --git a/Chapter6 - Design Pattern/6.16.md b/Chapter6 - Design Pattern/6.16.md index 741c50b..5b7080a 100644 --- a/Chapter6 - Design Pattern/6.16.md +++ b/Chapter6 - Design Pattern/6.16.md @@ -21,7 +21,7 @@ ### 对象适配器 - + 假设一个类存在年代久远,如果需要适配,则需要创建一个适配对象。然后被适配的对象以成员变量的形式集成到适配对象中。 diff --git a/assets/.DS_Store b/assets/.DS_Store index 730db4b..6b9f645 100644 Binary files a/assets/.DS_Store and b/assets/.DS_Store differ diff --git a/assets/AFNetworkingDynamicLibFormat.png b/assets/AFNetworkingDynamicLibFormat.png new file mode 100644 index 0000000..a8d3e8a Binary files /dev/null and b/assets/AFNetworkingDynamicLibFormat.png differ diff --git a/assets/AFNetworkingProcess.png b/assets/AFNetworkingProcess.png new file mode 100644 index 0000000..4e0bed0 Binary files /dev/null and b/assets/AFNetworkingProcess.png differ diff --git a/assets/AFNetworkingStaticLibFormat.png b/assets/AFNetworkingStaticLibFormat.png new file mode 100644 index 0000000..c3d0723 Binary files /dev/null and b/assets/AFNetworkingStaticLibFormat.png differ diff --git a/assets/AntiDebugViaAssemblyAndPtrace.png b/assets/AntiDebugViaAssemblyAndPtrace.png new file mode 100644 index 0000000..1d9866e Binary files /dev/null and b/assets/AntiDebugViaAssemblyAndPtrace.png differ diff --git a/assets/AspectInvokeBlock.png b/assets/AspectInvokeBlock.png new file mode 100644 index 0000000..81b4a5c Binary files /dev/null and b/assets/AspectInvokeBlock.png differ diff --git a/assets/AspectMockBlock.png b/assets/AspectMockBlock.png new file mode 100644 index 0000000..9b37113 Binary files /dev/null and b/assets/AspectMockBlock.png differ diff --git a/assets/AspectsBlockExplore.png b/assets/AspectsBlockExplore.png new file mode 100644 index 0000000..c975e60 Binary files /dev/null and b/assets/AspectsBlockExplore.png differ diff --git a/assets/AspectsBlockSignature.png b/assets/AspectsBlockSignature.png new file mode 100644 index 0000000..9a2a89b Binary files /dev/null and b/assets/AspectsBlockSignature.png differ diff --git a/assets/AspectsHookForwardInvocation.png b/assets/AspectsHookForwardInvocation.png new file mode 100644 index 0000000..fc2d3aa Binary files /dev/null and b/assets/AspectsHookForwardInvocation.png differ diff --git a/assets/AspectsHookMethodWithObjcMsgForward.png b/assets/AspectsHookMethodWithObjcMsgForward.png new file mode 100644 index 0000000..e4aa46d Binary files /dev/null and b/assets/AspectsHookMethodWithObjcMsgForward.png differ diff --git a/assets/AspectsIdentifierModule.png b/assets/AspectsIdentifierModule.png new file mode 100644 index 0000000..ce20a0e Binary files /dev/null and b/assets/AspectsIdentifierModule.png differ diff --git a/assets/AspectsProcess.png b/assets/AspectsProcess.png new file mode 100644 index 0000000..63a401a Binary files /dev/null and b/assets/AspectsProcess.png differ diff --git a/assets/AspectsProcessXmind.png b/assets/AspectsProcessXmind.png new file mode 100644 index 0000000..76e8a37 Binary files /dev/null and b/assets/AspectsProcessXmind.png differ diff --git a/assets/AspectsSignatureCompare.png b/assets/AspectsSignatureCompare.png new file mode 100644 index 0000000..d57e03d Binary files /dev/null and b/assets/AspectsSignatureCompare.png differ diff --git a/assets/BlockAndQueue.png b/assets/BlockAndQueue.png new file mode 100644 index 0000000..fbae0cb Binary files /dev/null and b/assets/BlockAndQueue.png differ diff --git a/assets/CALevel.png b/assets/CALevel.png new file mode 100644 index 0000000..b94bbe7 Binary files /dev/null and b/assets/CALevel.png differ diff --git a/assets/CPUGPUWithOpenGLBuffer.png b/assets/CPUGPUWithOpenGLBuffer.png new file mode 100644 index 0000000..18efdad Binary files /dev/null and b/assets/CPUGPUWithOpenGLBuffer.png differ diff --git a/assets/ClangCodeCoverageGenerateOrderFile.png b/assets/ClangCodeCoverageGenerateOrderFile.png new file mode 100644 index 0000000..4bdd3af Binary files /dev/null and b/assets/ClangCodeCoverageGenerateOrderFile.png differ diff --git a/assets/CollectAppLaunchMethod.png b/assets/CollectAppLaunchMethod.png new file mode 100644 index 0000000..b52e5cb Binary files /dev/null and b/assets/CollectAppLaunchMethod.png differ diff --git a/assets/DLOpenPtrace.png b/assets/DLOpenPtrace.png new file mode 100644 index 0000000..91b0501 Binary files /dev/null and b/assets/DLOpenPtrace.png differ diff --git a/assets/DataSignProcess.png b/assets/DataSignProcess.png new file mode 100644 index 0000000..cc47d02 Binary files /dev/null and b/assets/DataSignProcess.png differ diff --git a/assets/FishHookDemoImageList.png b/assets/FishHookDemoImageList.png new file mode 100644 index 0000000..2397b08 Binary files /dev/null and b/assets/FishHookDemoImageList.png differ diff --git a/assets/FishHookMachO.png b/assets/FishHookMachO.png new file mode 100644 index 0000000..031d66e Binary files /dev/null and b/assets/FishHookMachO.png differ diff --git a/assets/FishHookMachO1.png b/assets/FishHookMachO1.png new file mode 100644 index 0000000..263a2d4 Binary files /dev/null and b/assets/FishHookMachO1.png differ diff --git a/assets/FishHookMachO2.png b/assets/FishHookMachO2.png new file mode 100644 index 0000000..12e0c15 Binary files /dev/null and b/assets/FishHookMachO2.png differ diff --git a/assets/FishHookMachO3.png b/assets/FishHookMachO3.png new file mode 100644 index 0000000..1834970 Binary files /dev/null and b/assets/FishHookMachO3.png differ diff --git a/assets/FishHookMachO4.png b/assets/FishHookMachO4.png new file mode 100644 index 0000000..2336bde Binary files /dev/null and b/assets/FishHookMachO4.png differ diff --git a/assets/FishHookWithCFunction.png b/assets/FishHookWithCFunction.png new file mode 100644 index 0000000..4c932a8 Binary files /dev/null and b/assets/FishHookWithCFunction.png differ diff --git a/assets/FishHookWithUserCFunction.png b/assets/FishHookWithUserCFunction.png new file mode 100644 index 0000000..d6243ca Binary files /dev/null and b/assets/FishHookWithUserCFunction.png differ diff --git a/assets/FishhookCrackedPtrace.png b/assets/FishhookCrackedPtrace.png new file mode 100644 index 0000000..97eaf55 Binary files /dev/null and b/assets/FishhookCrackedPtrace.png differ diff --git a/assets/FishhookResult.png b/assets/FishhookResult.png new file mode 100644 index 0000000..031ad9f Binary files /dev/null and b/assets/FishhookResult.png differ diff --git a/assets/FishhookSysctlSymbol.png b/assets/FishhookSysctlSymbol.png new file mode 100644 index 0000000..35009b6 Binary files /dev/null and b/assets/FishhookSysctlSymbol.png differ diff --git a/assets/HidePtraceAndSysctlSymbol.png b/assets/HidePtraceAndSysctlSymbol.png new file mode 100644 index 0000000..87b988f Binary files /dev/null and b/assets/HidePtraceAndSysctlSymbol.png differ diff --git a/assets/InstrumentZombiesCaptureZombieObject.png b/assets/InstrumentZombiesCaptureZombieObject.png new file mode 100644 index 0000000..50c5675 Binary files /dev/null and b/assets/InstrumentZombiesCaptureZombieObject.png differ diff --git a/assets/LLDBNSLogAddressSymbol.png b/assets/LLDBNSLogAddressSymbol.png new file mode 100644 index 0000000..a6f4bc6 Binary files /dev/null and b/assets/LLDBNSLogAddressSymbol.png differ diff --git a/assets/LLVMGenerateSuperDealloc.png b/assets/LLVMGenerateSuperDealloc.png new file mode 100644 index 0000000..c3c8944 Binary files /dev/null and b/assets/LLVMGenerateSuperDealloc.png differ diff --git a/assets/LinkMapWithCompileOrder.png b/assets/LinkMapWithCompileOrder.png new file mode 100644 index 0000000..3a04f52 Binary files /dev/null and b/assets/LinkMapWithCompileOrder.png differ diff --git a/assets/LinkOrderWithOrderFile.png b/assets/LinkOrderWithOrderFile.png new file mode 100644 index 0000000..616e274 Binary files /dev/null and b/assets/LinkOrderWithOrderFile.png differ diff --git a/assets/NSDictionaryCopyTheKey.png b/assets/NSDictionaryCopyTheKey.png new file mode 100644 index 0000000..29be881 Binary files /dev/null and b/assets/NSDictionaryCopyTheKey.png differ diff --git a/assets/NSLogFakeAddress.png b/assets/NSLogFakeAddress.png new file mode 100644 index 0000000..39280f9 Binary files /dev/null and b/assets/NSLogFakeAddress.png differ diff --git a/assets/NSMapTableNSDictionaryMemoryControl.png b/assets/NSMapTableNSDictionaryMemoryControl.png new file mode 100644 index 0000000..ad4cf9f Binary files /dev/null and b/assets/NSMapTableNSDictionaryMemoryControl.png differ diff --git a/assets/NSObjectImpleByClassAndInstance.png b/assets/NSObjectImpleByClassAndInstance.png new file mode 100644 index 0000000..74d2bb7 Binary files /dev/null and b/assets/NSObjectImpleByClassAndInstance.png differ diff --git a/assets/ObjcExceptionHandlerExplore.png b/assets/ObjcExceptionHandlerExplore.png new file mode 100644 index 0000000..a3d233b Binary files /dev/null and b/assets/ObjcExceptionHandlerExplore.png differ diff --git a/assets/ObjcInitExceptionHandlerSet.png b/assets/ObjcInitExceptionHandlerSet.png new file mode 100644 index 0000000..1a7cdbb Binary files /dev/null and b/assets/ObjcInitExceptionHandlerSet.png differ diff --git a/assets/OpenGLDrawerAlgorithm.png b/assets/OpenGLDrawerAlgorithm.png new file mode 100644 index 0000000..bd791b9 Binary files /dev/null and b/assets/OpenGLDrawerAlgorithm.png differ diff --git a/assets/OpenGLFrontAndBackSurface.png b/assets/OpenGLFrontAndBackSurface.png new file mode 100644 index 0000000..daa5464 Binary files /dev/null and b/assets/OpenGLFrontAndBackSurface.png differ diff --git a/assets/OpenGLFrontAndBackSurfaceWithViewer.png b/assets/OpenGLFrontAndBackSurfaceWithViewer.png new file mode 100644 index 0000000..34e9eb0 Binary files /dev/null and b/assets/OpenGLFrontAndBackSurfaceWithViewer.png differ diff --git a/assets/OpenGLShaderProcess.png b/assets/OpenGLShaderProcess.png new file mode 100644 index 0000000..2ce88e8 Binary files /dev/null and b/assets/OpenGLShaderProcess.png differ diff --git a/assets/OpenGLZFightingIssue.png b/assets/OpenGLZFightingIssue.png new file mode 100644 index 0000000..4db87a6 Binary files /dev/null and b/assets/OpenGLZFightingIssue.png differ diff --git a/assets/OpenGlDrawerAlgorithmIssue.png b/assets/OpenGlDrawerAlgorithmIssue.png new file mode 100644 index 0000000..1d3d2cf Binary files /dev/null and b/assets/OpenGlDrawerAlgorithmIssue.png differ diff --git a/assets/PTRACEDFlag.png b/assets/PTRACEDFlag.png new file mode 100644 index 0000000..4505b55 Binary files /dev/null and b/assets/PTRACEDFlag.png differ diff --git a/assets/PtraceSafeMethod.png b/assets/PtraceSafeMethod.png new file mode 100644 index 0000000..eb965a1 Binary files /dev/null and b/assets/PtraceSafeMethod.png differ diff --git a/assets/RuntimeCallValueForKeyOnObject.png b/assets/RuntimeCallValueForKeyOnObject.png new file mode 100644 index 0000000..908d0ab Binary files /dev/null and b/assets/RuntimeCallValueForKeyOnObject.png differ diff --git a/assets/SYSCTLKillInDebug.png b/assets/SYSCTLKillInDebug.png new file mode 100644 index 0000000..110bc45 Binary files /dev/null and b/assets/SYSCTLKillInDebug.png differ diff --git a/assets/SysctlAntiFishHook.png b/assets/SysctlAntiFishHook.png new file mode 100644 index 0000000..2fa8207 Binary files /dev/null and b/assets/SysctlAntiFishHook.png differ diff --git a/assets/WildPointerCategory.png b/assets/WildPointerCategory.png new file mode 100644 index 0000000..9166c0c Binary files /dev/null and b/assets/WildPointerCategory.png differ diff --git a/assets/XcodeEnableLinkMap.png b/assets/XcodeEnableLinkMap.png new file mode 100644 index 0000000..b51c9fa Binary files /dev/null and b/assets/XcodeEnableLinkMap.png differ diff --git a/assets/XcodeStaticAnalysis.png b/assets/XcodeStaticAnalysis.png new file mode 100644 index 0000000..ff8dc5a Binary files /dev/null and b/assets/XcodeStaticAnalysis.png differ diff --git a/assets/XcodeStaticAnalysisDataIssue.png b/assets/XcodeStaticAnalysisDataIssue.png new file mode 100644 index 0000000..c6e5139 Binary files /dev/null and b/assets/XcodeStaticAnalysisDataIssue.png differ diff --git a/assets/XcodeStaticAnalysisEndelessLoopIssue.png b/assets/XcodeStaticAnalysisEndelessLoopIssue.png new file mode 100644 index 0000000..f2790fe Binary files /dev/null and b/assets/XcodeStaticAnalysisEndelessLoopIssue.png differ diff --git a/assets/XcodeStaticAnalysisForSingleFile.png b/assets/XcodeStaticAnalysisForSingleFile.png new file mode 100644 index 0000000..8fcff37 Binary files /dev/null and b/assets/XcodeStaticAnalysisForSingleFile.png differ diff --git a/assets/XcodeStaticAnalysisLogicIssue.png b/assets/XcodeStaticAnalysisLogicIssue.png new file mode 100644 index 0000000..42e6ad5 Binary files /dev/null and b/assets/XcodeStaticAnalysisLogicIssue.png differ diff --git a/assets/XcodeStaticAnalysisMemoryIssue.png b/assets/XcodeStaticAnalysisMemoryIssue.png new file mode 100644 index 0000000..615a4f0 Binary files /dev/null and b/assets/XcodeStaticAnalysisMemoryIssue.png differ diff --git a/assets/XcodeStaticAnalysisNSAssertSideEffectIssue.png b/assets/XcodeStaticAnalysisNSAssertSideEffectIssue.png new file mode 100644 index 0000000..218a623 Binary files /dev/null and b/assets/XcodeStaticAnalysisNSAssertSideEffectIssue.png differ diff --git a/assets/XcodeStaticAnalyzerCheckPoint.png b/assets/XcodeStaticAnalyzerCheckPoint.png new file mode 100644 index 0000000..fc40b4d Binary files /dev/null and b/assets/XcodeStaticAnalyzerCheckPoint.png differ diff --git a/assets/XcodeStaticAnalyzerDeepAndShallowBuildDuration.png b/assets/XcodeStaticAnalyzerDeepAndShallowBuildDuration.png new file mode 100644 index 0000000..068e863 Binary files /dev/null and b/assets/XcodeStaticAnalyzerDeepAndShallowBuildDuration.png differ diff --git a/assets/XcodeStaticAnalyzerEachBuild.png b/assets/XcodeStaticAnalyzerEachBuild.png new file mode 100644 index 0000000..546e49f Binary files /dev/null and b/assets/XcodeStaticAnalyzerEachBuild.png differ diff --git a/assets/XcodeStaticAnalyzerLocalizationIssue.png b/assets/XcodeStaticAnalyzerLocalizationIssue.png new file mode 100644 index 0000000..39b6051 Binary files /dev/null and b/assets/XcodeStaticAnalyzerLocalizationIssue.png differ diff --git a/assets/XcodeStaticAnalyzerSecuritySetting.png b/assets/XcodeStaticAnalyzerSecuritySetting.png new file mode 100644 index 0000000..de747e0 Binary files /dev/null and b/assets/XcodeStaticAnalyzerSecuritySetting.png differ diff --git a/assets/callStackSymbolicate'.png b/assets/callStackSymbolicate.png similarity index 100% rename from assets/callStackSymbolicate'.png rename to assets/callStackSymbolicate.png diff --git a/assets/ptraceSymbolBreakpoint.png b/assets/ptraceSymbolBreakpoint.png new file mode 100644 index 0000000..d325046 Binary files /dev/null and b/assets/ptraceSymbolBreakpoint.png differ