feature: App 逆向防护

This commit is contained in:
杭城小刘
2024-07-15 20:03:01 +08:00
parent 13f7457be9
commit 83fefff66b
109 changed files with 2549 additions and 672 deletions

View File

@@ -62,7 +62,10 @@ self.iamgeView.layer.backgroundColor = [UIColor greenColor].CGColor;
self.imageView.layer.cornerRadius = YES; self.imageView.layer.cornerRadius = YES;
``` ```
## 离屏渲染的影响? ## 离屏渲染的影响?
触发离屏渲染后,会增加 GPU 的工作量CPU + GPU 工作时间可能会超过一次渲染周期,会发生 UI 卡顿。 触发离屏渲染后,会增加 GPU 的工作量CPU + GPU 工作时间可能会超过一次渲染周期,会发生 UI 卡顿。
离屏渲染,指的是 GPU 在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。由上面的一个结论视图和圆角的大小对帧率并没有什么影响,数量的多少才显著影响性能。可以知道离屏渲染耗时是发生在离屏这个动作上面,而不是渲染。为什么离屏这么耗时?原因主要有创建缓冲区和上下文切换。创建新的缓冲区代价都不算大,付出最大代价的是上下文切换。 离屏渲染,指的是 GPU 在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。由上面的一个结论视图和圆角的大小对帧率并没有什么影响,数量的多少才显著影响性能。可以知道离屏渲染耗时是发生在离屏这个动作上面,而不是渲染。为什么离屏这么耗时?原因主要有创建缓冲区和上下文切换。创建新的缓冲区代价都不算大,付出最大代价的是上下文切换。
@@ -107,7 +110,7 @@ Xcode 就提供了检测功能,打开路径为: Xcode -> Debug -> View Debug
## 引申阅读 ## 引申阅读
- [实时渲染管线中的光源渲染问题](https://zhuanlan.zhihu.com/p/392748735) - [实时渲染管线中的光源渲染问题](https://zhuanlan.zhihu.com/p/392748735)
- [蒙特卡罗方法](https://wiki.mbalib.com/wiki/蒙特卡罗方法) - [蒙特卡罗方法](https://wiki.mbalib.com/wiki/蒙特卡罗方法)

View File

@@ -1,4 +1,4 @@
LLVM # LLVM
[LLVM](https://llvm.org/) 项目是模块化、可重用的编译器以及工具链技术的集合 [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 由三部分构成: LLVM 由三部分构成:
@@ -22,7 +22,7 @@ LLVM 由三部分构成:
![](./../assets/LLVM-Structure.png) ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVM-Structure.png)
正是由于这样的设计,使得 LLVM 具备很多有点: 正是由于这样的设计,使得 LLVM 具备很多有点:
@@ -42,7 +42,7 @@ LLVM 现在被作为实现各种静态和运行时编译语言的通用基础结
广义上来讲LLVM 说的是一种架构。狭义上来讲LLVM 强调的是偏后端部分,如下图的除了 clang 编译前端外的部分,包括优化器和编译后端,统称为 LLVM 后端。 广义上来讲LLVM 说的是一种架构。狭义上来讲LLVM 强调的是偏后端部分,如下图的除了 clang 编译前端外的部分,包括优化器和编译后端,统称为 LLVM 后端。
<img src="./../assets/LLVMFullStructure.png" style="zoom:45%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMFullStructure.png" style="zoom:45%">
@@ -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
展示如下: 展示如下:
<img src="./../assets/LLVMDisplayPhases.png" style="zoom:45%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMDisplayPhases.png" style="zoom:45%">
@@ -104,7 +104,7 @@ clang -ccc-print-phases main.m
查看 preprocessor (预处理)的结果:`clang -E main.m`。预处理主要做的事情就是头文件导入( include、import、宏定义替换等。展示如下 查看 preprocessor (预处理)的结果:`clang -E main.m`。预处理主要做的事情就是头文件导入( include、import、宏定义替换等。展示如下
<img src="./../assets/LLVMPreProcessorPhase.png" style="zoom:35%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMPreProcessorPhase.png" style="zoom:35%">
@@ -112,7 +112,7 @@ clang -ccc-print-phases main.m
词法分析阶段,主要生成 Token。使用指令 `clang -fmodules -E -Xclang -dump-tokens main.m` 查看具体做了什么 词法分析阶段,主要生成 Token。使用指令 `clang -fmodules -E -Xclang -dump-tokens main.m` 查看具体做了什么
<img src="./../assets/LLVMAnalysize.png" style="zoom:35%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMAnalysize.png" style="zoom:35%">
@@ -120,7 +120,7 @@ clang -ccc-print-phases main.m
语法分析阶段生成语法树ASTAbstract Syntax Tree。使用指令 `clang -fmodules -fsyntax-only -Xclang -ast-dump main.m` 查看 语法分析阶段生成语法树ASTAbstract Syntax Tree。使用指令 `clang -fmodules -fsyntax-only -Xclang -ast-dump main.m` 查看
<img src="./../assets/LLVMASTAnalysis.png" style="zoom:35%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMASTAnalysis.png" style="zoom:35%">
对 main.m 的代码进行改造 对 main.m 的代码进行改造
@@ -142,7 +142,7 @@ void test(int a, int b) {
再次查看 AST 可以加深理解 再次查看 AST 可以加深理解
<img src="./../assets/LLVMASTAnalysis2.png" style="zoom:35%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMASTAnalysis2.png" style="zoom:35%">
其中: 其中:
@@ -154,7 +154,7 @@ void test(int a, int b) {
也就是先运算蓝色框内的值,然后用结果和红色框内的进行相减。所以这是很标准的树形结构。 也就是先运算蓝色框内的值,然后用结果和红色框内的进行相减。所以这是很标准的树形结构。
<img src="./../assets/LLVMASTTreeDemo.png" style="zoom:10%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMASTTreeDemo.png" style="zoom:10%">
@@ -171,7 +171,7 @@ LLVM IR 有3种表示格式
- text便于阅读的文本格式类似于汇编语言推展名为 `.ll`。使用指令 `clang -S -emit-llvm main.m` 进行转换 - text便于阅读的文本格式类似于汇编语言推展名为 `.ll`。使用指令 `clang -S -emit-llvm main.m` 进行转换
<img src="./../assets/LLVMIRType1.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMIRType1.png" style="zoom:30%">
学过 arm64 汇编的话看这段 IR 很眼熟,汇编里 `load` 相关的指令都是从内存中装载数据,比如 `ldr`、`ldur` 、`ldp`。`store` 相关的指令是往内存中写入数据,比如 `str`、 `stur`、 `stp` 学过 arm64 汇编的话看这段 IR 很眼熟,汇编里 `load` 相关的指令都是从内存中装载数据,比如 `ldr`、`ldur` 、`ldp`。`store` 相关的指令是往内存中写入数据,比如 `str`、 `stur`、 `stp`
@@ -304,9 +304,9 @@ Tipsninja 如果安装失败,可以直接从 [github]( https://github.com/n
因为要编写 Clang 插件,是 c++ 代码,所以需要借助 IDE 的能力,我们选用 Xcode 进行编译。如下图所示,代表编译成功 因为要编写 Clang 插件,是 c++ 代码,所以需要借助 IDE 的能力,我们选用 Xcode 进行编译。如下图所示,代表编译成功
<img src="./../assets/LLVMComplieXcode1.png" style="zoom:20%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMComplieXcode1.png" style="zoom:20%">
<img src="./../assets/LLVMComplieXcode2.png" style="zoom:20%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMComplieXcode2.png" style="zoom:20%">
@@ -334,11 +334,11 @@ Tipsninja 如果安装失败,可以直接从 [github]( https://github.com/n
- 先创建一个插件文件夹 `code-style-validate-plugin` - 先创建一个插件文件夹 `code-style-validate-plugin`
<img src="./../assets/LLVMAddConfiguration1.png" style="zoom:20%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMAddConfiguration1.png" style="zoom:20%">
- 编辑 `CMakeLists.txt` 文件,在最后添加 `add_clang_subdirectory(code-style-validate-plugin)` - 编辑 `CMakeLists.txt` 文件,在最后添加 `add_clang_subdirectory(code-style-validate-plugin)`
<img src="./../assets/LLVMAddConfiguration2.png" style="zoom:20%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMAddConfiguration2.png" style="zoom:20%">
@@ -367,17 +367,17 @@ Xcode 打开项目,选择自动创建 Schemes
<img src="./../assets/XcodeOpenLLVMProject.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeOpenLLVMProject.png" style="zoom:30%">
选择 Target 为 `CodeStyleValidatePlugin`,源代码所在文件夹为 `Sources/Loadable modules`,然后选中 CodeStyleValidatePlugin.cpp` 文件进行编写逻辑 选择 Target 为 `CodeStyleValidatePlugin`,源代码所在文件夹为 `Sources/Loadable modules`,然后选中 CodeStyleValidatePlugin.cpp` 文件进行编写逻辑
<img src="./../assets/ClangPluginSourceCode.png" style="zoom:20%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClangPluginSourceCode.png" style="zoom:20%">
初步编写后 Command + B 进行编译,在 Products 下可以看到编译产物:`CodeStyleValidatePlugin.dylib` 动态库。 初步编写后 Command + B 进行编译,在 Products 下可以看到编译产物:`CodeStyleValidatePlugin.dylib` 动态库。
<img src="./../assets/ClangCompileProducts.png" style="zoom:20%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClangCompileProducts.png" style="zoom:20%">
@@ -387,7 +387,7 @@ Xcode 打开项目,选择自动创建 Schemes
此步骤的目的是:在 testLLVM 项目中,加载 `CodeStyleValidatePlugin.dylib` 插件可以成功。因为默认的 Xcode 使用的 clang/clang++ 编译器和编译 `CodeStyleValidatePlugin.dylib` 动态库不是一个版本。不做修改的话Xcode 加载 `CodeStyleValidatePlugin.dylib` 会报错。所以需要先编译出同一个 LLVM 版本的 clang/clang++。 此步骤的目的是:在 testLLVM 项目中,加载 `CodeStyleValidatePlugin.dylib` 插件可以成功。因为默认的 Xcode 使用的 clang/clang++ 编译器和编译 `CodeStyleValidatePlugin.dylib` 动态库不是一个版本。不做修改的话Xcode 加载 `CodeStyleValidatePlugin.dylib` 会报错。所以需要先编译出同一个 LLVM 版本的 clang/clang++。
<img src="./../assets/XcodeCompileClang.png" style="zoom:20%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeCompileClang.png" style="zoom:20%">
@@ -406,7 +406,7 @@ Xcode 打开项目,选择自动创建 Schemes
- `-Xclang` - `-Xclang`
- 插件名称 - 插件名称
<img src="./../assets/XcodeLoadPlugin.png" style="zoom:20%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeLoadPlugin.png" style="zoom:20%">
@@ -414,7 +414,7 @@ Xcode 打开项目,选择自动创建 Schemes
在新创建的 TestLLVM Xcode 项目中加载创建的 `CodeStyleValidatePlugin.dylib` 会报错。原因是:由于 Clang 插件需要使用对应的版本去加载,如果版本不一致则会导致编译错误。如下所示: 在新创建的 TestLLVM Xcode 项目中加载创建的 `CodeStyleValidatePlugin.dylib` 会报错。原因是:由于 Clang 插件需要使用对应的版本去加载,如果版本不一致则会导致编译错误。如下所示:
<img src="./../assets/XcodeLoadClangPluginError.png" style="zoom:20%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeLoadClangPluginError.png" style="zoom:20%">
@@ -426,17 +426,17 @@ Xcode 打开项目,选择自动创建 Schemes
如下所示: 如下所示:
<img src="./../assets/XcodeSpecifyClangPath.png" style="zoom:20%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeSpecifyClangPath.png" style="zoom:20%">
继续编译还是会报错,报错如下: 继续编译还是会报错,报错如下:
<img src="./../assets/XcodeLoadClangPluginError2.png" style="zoom:20%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeLoadClangPluginError2.png" style="zoom:20%">
解决方案为:在 `Build Settings` 栏目中搜索 `index`,将 `Enable Index-Wihle-Building Functionality`` Default` 改为 `NO` 解决方案为:在 `Build Settings` 栏目中搜索 `index`,将 `Enable Index-Wihle-Building Functionality`` Default` 改为 `NO`
<img src="./../assets/XcodeLoadClangThenBuildErrorFix.png" style="zoom:20%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeLoadClangThenBuildErrorFix.png" style="zoom:20%">
@@ -448,7 +448,7 @@ Tips 由于重新修改了插件的源码,所以每次 Build 构建完 FANP
编译成功,可以看到在日志中输出了我们编写的日志信息。 编译成功,可以看到在日志中输出了我们编写的日志信息。
<img src="./../assets/XcodeLoadClangPluginTest.png" style="zoom:20%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeLoadClangPluginTest.png" style="zoom:20%">
@@ -481,7 +481,7 @@ NS_ASSUME_NONNULL_END
利用 Clang 查看 AST 指令为 `clang -fmodules -fsyntax-only -Xclang -ast-dump workaholic_person.m` 利用 Clang 查看 AST 指令为 `clang -fmodules -fsyntax-only -Xclang -ast-dump workaholic_person.m`
<img src="./../assets/ClassNameViaClangAST.png" style="zoom:20%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClassNameViaClangAST.png" style="zoom:20%">
核心思路为:我们要分析类名不符合规范的情况,要精确报错,首先要识别到类名,利用 AST 的能力可以办到(类名在 AST 的 `ObjCInterfaceDecl` 节点上)。然后获取到类名的行号信息,精确报错。 核心思路为:我们要分析类名不符合规范的情况,要精确报错,首先要识别到类名,利用 AST 的能力可以办到(类名在 AST 的 `ObjCInterfaceDecl` 节点上)。然后获取到类名的行号信息,精确报错。
@@ -771,7 +771,7 @@ X("CodeStyleValidatePlugin", "This plugin is designed for scanning code styles,
- 可以对 Category 名做检测,如果带下划线,则报错提示并给出修改意见 - 可以对 Category 名做检测,如果带下划线,则报错提示并给出修改意见
- 编写的 `CodeStyleValidatePlugin` Demo 中对不符合规范的做了 `DiagnosticsEngine::Warning` 级别的警告。如果遇到1个警告则不影响继续编译。如果是 `DiagnosticsEngine::Error` 级别的编译报错遇到1个则终止编译请注意该区别按需编写自己的插件逻辑。 - 编写的 `CodeStyleValidatePlugin` Demo 中对不符合规范的做了 `DiagnosticsEngine::Warning` 级别的警告。如果遇到1个警告则不影响继续编译。如果是 `DiagnosticsEngine::Error` 级别的编译报错遇到1个则终止编译请注意该区别按需编写自己的插件逻辑。
<img src="./../assets/LLVMClangPluginUseInXcode.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMClangPluginUseInXcode.png" style="zoom:25%">
@@ -819,13 +819,71 @@ X("CodeStyleValidatePlugin", "This plugin is designed for scanning code styles,
- 使用 LLVM 编写 Clang 插件,解析 AST 拿到所有的 `ObjCInterfaceDecl` 信息,然后结合 `ObjCMethodDecl` 信息便可获取 Category 中的 所有方法,再判断方法是否同名 - 使用 LLVM 编写 Clang 插件,解析 AST 拿到所有的 `ObjCInterfaceDecl` 信息,然后结合 `ObjCMethodDecl` 信息便可获取 Category 中的 所有方法,再判断方法是否同名
<img src="./../assets/ClangASTCategoryMethod.png" style="zoom:20%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClangASTCategoryMethod.png" style="zoom:20%">
### Pass 插桩,实现精准测试 ### Pass 插桩,实现精准测试
这部分涉及到 Objective-C 和 Swift 的代码插桩逻辑的不同实现,篇幅较大,可以查看这篇文章 [精准测试最佳实践](./1.108.md) 这部分涉及到 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]`
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMGenerateSuperDealloc.png" style="zoom:30%" />
- 为每个拥有 ivar 的 Class 合成` .cxx_destructor` 方法来自动释放类的成员变量,代替 MRC 时代的 `self.xxx = nil`

View File

@@ -67,7 +67,7 @@ typedef struct NCTbl {
该表用于存储添加观察者时传了 NotificationName 的情况。也就是 Named Table 中NotificationName 作为 key。在使用系统 API 注册观察者的时候还可以传入 object 参数,表示只监听该对象发出的通知,所以还需要一张表存储 object 和 observer 的对应关系object 为 keyobserver 为 value。 该表用于存储添加观察者时传了 NotificationName 的情况。也就是 Named Table 中NotificationName 作为 key。在使用系统 API 注册观察者的时候还可以传入 object 参数,表示只监听该对象发出的通知,所以还需要一张表存储 object 和 observer 的对应关系object 为 keyobserver 为 value。
![](./../assets/notification-namedTable.png) ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/notification-namedTable.png)
- 第一个 MapTable key 为 notificationNamevalue 为另一个 MapTable子 Table - 第一个 MapTable key 为 notificationNamevalue 为另一个 MapTable子 Table
@@ -79,7 +79,7 @@ typedef struct NCTbl {
nameless Table 结构较为简单,因为没有 notificationName所以就一层 MapTable。key 为 objectvalue 为链表,存储所有的观察者。 nameless Table 结构较为简单,因为没有 notificationName所以就一层 MapTable。key 为 objectvalue 为链表,存储所有的观察者。
![](./../assets/Notification-namelessTable.png) ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Notification-namelessTable.png)
### wildcard ### wildcard

View File

@@ -12,7 +12,7 @@
UIView 绘制流程。 UIView 绘制流程。
<img src="./../assets/UIViewRefreshProcess.png" style="zoom:60%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/UIViewRefreshProcess.png" style="zoom:60%" />
@@ -24,7 +24,7 @@ Tips`[UIView setNeedsDisplay]` 之后并不会立马调用 `[view.layer setN
下面来个 Demo 展示下简单的异步绘制一个 String。 下面来个 Demo 展示下简单的异步绘制一个 String。
<img src="./../assets/AsyncUILabelRender.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AsyncUILabelRender.png" style="zoom:30%" />
@@ -32,7 +32,7 @@ Tips`[UIView setNeedsDisplay]` 之后并不会立马调用 `[view.layer setN
接下来看看系统的绘制实现流程: 接下来看看系统的绘制实现流程:
<img src="./../assets/UIViewSystemRenderProcess.png" style="zoom:60%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/UIViewSystemRenderProcess.png" style="zoom:60%" />
如何实现异步绘制? 如何实现异步绘制?
@@ -43,7 +43,7 @@ Tips`[UIView setNeedsDisplay]` 之后并不会立马调用 `[view.layer setN
<img src='./../assets/AsyncRenderProcessAPI.png' style="zoom:30%" /> <img src='https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AsyncRenderProcessAPI.png' style="zoom:30%" />

View File

@@ -6,7 +6,7 @@
下面这张图是22年整理的我们移动中台对于质量的一些把控手段也是一个有效的 checklist。对于一个业务项目或者技术项目来说QA 给的测试用例全部通过不能说明代码没有问题。单测覆盖率的最佳实践是针对于基础 SDK对于业务侧的代码来说由于经常变化所以还是以人工测试为主一些核心的不变的核心链路沉淀出 UI 自动化用例,每周迭代的时候,交付测试后,开始 UI 自动化回归。 下面这张图是22年整理的我们移动中台对于质量的一些把控手段也是一个有效的 checklist。对于一个业务项目或者技术项目来说QA 给的测试用例全部通过不能说明代码没有问题。单测覆盖率的最佳实践是针对于基础 SDK对于业务侧的代码来说由于经常变化所以还是以人工测试为主一些核心的不变的核心链路沉淀出 UI 自动化用例,每周迭代的时候,交付测试后,开始 UI 自动化回归。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CodeQualityChecklist.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CodeQualityChecklist.png" style="zoom:25%">
但,这些回归还是不能 cover 所有问题,我们需要为我们写的每行代码买单,如何衡量每行代码的效果呢?这就是精准测试要做的事情。 但,这些回归还是不能 cover 所有问题,我们需要为我们写的每行代码买单,如何衡量每行代码的效果呢?这就是精准测试要做的事情。

View File

@@ -70,7 +70,7 @@
- rsp、rbp 寄存器用于栈操作。栈顶指针,指向栈的顶部 - rsp、rbp 寄存器用于栈操作。栈顶指针,指向栈的顶部
- leaq 和 movq 是有区别的。`leaq 0xd(%rip), %rax` 是从 `%rip + 0xd` 算出来的地址值赋值给 `%rax` `movq 0xd(%rip), %rax` 是从 `%rip + 0xd ` 算出来的地址值取8个字节给 `%rax`。 - leaq 和 movq 是有区别的。`leaq 0xd(%rip), %rax` 是从 `%rip + 0xd` 算出来的地址值赋值给 `%rax` `movq 0xd(%rip), %rax` 是从 `%rip + 0xd ` 算出来的地址值取8个字节给 `%rax`。
- `xorl` 或运算。 - `xorl` 或运算。
## 寄存器的高低位兼容设计 ## 寄存器的高低位兼容设计

View File

@@ -303,7 +303,7 @@ print(fn(1, 2)) // 3
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClouseExploreNotCaptureVariableDemo2.png" style="zoom:25%"> <img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClouseExploreNotCaptureVariableDemo2.png" style="zoom:25%">
可以看到 `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行
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClouseExploreNotCaptureVariableDemo1.png" style="zoom:25%"> <img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClouseExploreNotCaptureVariableDemo1.png" style="zoom:25%">

View File

@@ -59,7 +59,7 @@ var str1: String = "01234😄"
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStringExploreDemo3.png" style="zoom:25%"> <img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftStringExploreDemo3.png" style="zoom:25%">
可以看到将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) 所以猜想正确。具体字符串创建过程可以查看 [Swift String 源码](https://github.com/apple/swift/blob/81812eaf2a6589610054a5db655bf7de3f3c8de6/stdlib/public/core/String.swift#L774)

View File

@@ -24,7 +24,7 @@
### 读取过程 ### 读取过程
<img src="./../assets/SDWebImageProcess.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SDWebImageProcess.png" style="zoom:30%" />
@@ -93,7 +93,7 @@ LRU最近最久未使用算法。比如3天内没有使用过的则认为
## 阅读时长记录器 ## 阅读时长记录器
<img src="./../assets/ReadTimeCounter.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ReadTimeCounter.png" style="zoom:30%" />
### 记录器种类 ### 记录器种类
@@ -174,7 +174,7 @@ iOS 小端序,网络大端序。
### 主要类关系图 ### 主要类关系图
<img src="./../assets/AFNetworkingClassArch.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AFNetworkingClassArch.png" style="zoom:30%" />
@@ -191,7 +191,7 @@ iOS 小端序,网络大端序。
### 架构图 ### 架构图
<img src="./../assets/SDWebImageArch.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SDWebImageArch.png" style="zoom:30%" />
@@ -219,7 +219,7 @@ iOS 小端序,网络大端序。
### 基本原理 ### 基本原理
<img src="./../assets/AsyncDisplayKitArch.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AsyncDisplayKitArch.png" style="zoom:30%" />
- UIView 作为 CALayer 的代理实现。CALayer 作为 UIView 的实例变量,负责展示规工作。 - UIView 作为 CALayer 的代理实现。CALayer 作为 UIView 的实例变量,负责展示规工作。

View File

@@ -21,11 +21,11 @@ QATarget、Scheme 的关系是什么?
注意duplicate 之后target 虽然多了一份,但是代码和资源不变,所以 注意duplicate 之后target 虽然多了一份,但是代码和资源不变,所以
<img src="./../assets/MultipleTargetProjectConfig.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MultipleTargetProjectConfig.png" style="zoom:30%" />
<img src="./../assets/XcodeMacroSupportedWithOCAndSwift.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeMacroSupportedWithOCAndSwift.png" style="zoom:30%" />
@@ -38,7 +38,7 @@ QATarget、Scheme 的关系是什么?
当对某个 Target “Duplicate” 之后,会产生一份新的 plist 文件 当对某个 Target “Duplicate” 之后,会产生一份新的 plist 文件
<img src="./../assets/MultiplePListAfterDuplicateTarget.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MultiplePListAfterDuplicateTarget.png" style="zoom:30%" />
@@ -52,19 +52,19 @@ QATarget、Scheme 的关系是什么?
针对一个 Target 可以添加多个 Scheme步骤如下 针对一个 Target 可以添加多个 Scheme步骤如下
<img src="./../assets/XcodeAddScheme.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeAddScheme.png" style="zoom:30%" />
这样的创建好之后,该 Target 存在3个 Scheme 了。有了 Scheme 有什么作用呢?设置宏定义的时候可以针对不同的 Scheme 进行设置。 这样的创建好之后,该 Target 存在3个 Scheme 了。有了 Scheme 有什么作用呢?设置宏定义的时候可以针对不同的 Scheme 进行设置。
<img src="./../assets/AddMacroForDifferentScheme.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AddMacroForDifferentScheme.png" style="zoom:30%" />
针对 OC、Swift 分别设置了很多宏定义,接下去需要跑 Beta 配置的代码,怎么办? 针对 OC、Swift 分别设置了很多宏定义,接下去需要跑 Beta 配置的代码,怎么办?
<img src="./../assets/XcodeSwitchSchemeManually.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeSwitchSchemeManually.png" style="zoom:30%" />
点击 Edit Scheme在 Run 里面选择对应的 Scheme。 点击 Edit Scheme在 Run 里面选择对应的 Scheme。
@@ -76,13 +76,13 @@ QATarget、Scheme 的关系是什么?
创建 Scheme 步骤Xcode -> New Scheme再弹出的方框内选择对应的 Target然后输入需要创建的 Scheme 名称。此次我们创建了Debug、Beta 2个新的 Scheme。 创建 Scheme 步骤Xcode -> New Scheme再弹出的方框内选择对应的 Target然后输入需要创建的 Scheme 名称。此次我们创建了Debug、Beta 2个新的 Scheme。
<img src="./../assets/XcodeCreateScheme.png" style="zoom:40%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeCreateScheme.png" style="zoom:40%" />
创建好之后,可以看到实体 Scheme 和虚拟 Scheme 存在多对多的关系。但我们可以基于此,选择实体的 Scheme然后在 Run 里面 “Build Configuration” 里面选择对应的 Scheme 与之对应。 创建好之后,可以看到实体 Scheme 和虚拟 Scheme 存在多对多的关系。但我们可以基于此,选择实体的 Scheme然后在 Run 里面 “Build Configuration” 里面选择对应的 Scheme 与之对应。
<img src="./../assets/XcodeSchemeMatchWithConfigScheme.png" style="zoom:40%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeSchemeMatchWithConfigScheme.png" style="zoom:40%" />
@@ -92,7 +92,7 @@ QATarget、Scheme 的关系是什么?
完整如下图: 完整如下图:
<img src="./../assets/SetValueUseDifferentSchemeAndUseViaPlist.png" style="zoom:40%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SetValueUseDifferentSchemeAndUseViaPlist.png" style="zoom:40%" />
@@ -110,7 +110,7 @@ Xcode 自带的 Configuration Settings File 可以支持自定义一些宏,
创建步骤如下: 创建步骤如下:
<img src="./../assets/XcodeCreateXCConfig.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeCreateXCConfig.png" style="zoom:30%" />
文件命名为:`文件夹名称-项目名称.scheme名称.xcconfig`,比如 `Config-Xcconfig.debug.xcconfig` 文件命名为:`文件夹名称-项目名称.scheme名称.xcconfig`,比如 `Config-Xcconfig.debug.xcconfig`
@@ -120,13 +120,13 @@ Xcode 自带的 Configuration Settings File 可以支持自定义一些宏,
修改和完善创建的 Xcconfig 配置文件里的内容。之后在 Xcode 的 Project 选项下,找到 Configurations选择对应的 Scheme然后选择右边对应的 Xcconfig 文件。如下图 修改和完善创建的 Xcconfig 配置文件里的内容。之后在 Xcode 的 Project 选项下,找到 Configurations选择对应的 Scheme然后选择右边对应的 Xcconfig 文件。如下图
<img src="./../assets/XcodeSpecifySchemeWithConfig.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeSpecifySchemeWithConfig.png" style="zoom:30%" />
我们只在 `Config-Xcconfig.debug.xcconfig` 文件中添加了 `OTHER_LDFLAGS = -framework "AFNetworking"`Xcode 切换到 debug scheme 下,然后 Command + B 编译。 我们只在 `Config-Xcconfig.debug.xcconfig` 文件中添加了 `OTHER_LDFLAGS = -framework "AFNetworking"`Xcode 切换到 debug scheme 下,然后 Command + B 编译。
<img src="./../assets/XcodeDebugXcconfigSpecifyLDLinkFlags.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeDebugXcconfigSpecifyLDLinkFlags.png" style="zoom:30%" />
验证结果: 验证结果:

View File

@@ -0,0 +1,143 @@
# 质量检测
## 静态检测
### 概念
静态分析器是一个 Xcode 内置的工具,使用它可以在不运行源代码的状态下进行静态的代码分析,并且找出其中的 Bug为 app 提供质量保证。
静态分析器工作的时候并不会动态地执行你的代码,它只是进行静态分析,因此它甚至会去分析代码中没有被常规的测试用例覆盖到的代码执行路径。
静态分析器只支持 C/C++/Objective-C 语言,因为静态分析器隶属于 Clang 体系中。不过即便如此,它对于 Objective-C 和 Swift 混编的工程也可以很好地支持。
其中包括了安全、逻辑、以及 API 方面的问题。分析器可以帮你找到以上的这些问题,并且解释原因以及指出代码的执行路径。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalyzerCheckPoint.png" style="zoom:30%" />
### 原理
- 通过语法树进行代码静态分析,找出非语法性错误
- 模拟代码执行路径,分析出 control-flow graphCFG
- 预置了常用的 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。可以帮助检测发现缺少国际化的文本。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalyzerLocalizationIssue.png" style="zoom:30%" />
提示说明 `User-facing text should use localized string macro` 缺少本地化的 API正确的采用下面一行的写法。
#### 逻辑问题
分母不能为0.
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalysisLogicIssue.png" style="zoom:30%" />
#### 内存问题
虽然 Xcode 默认使用 ARC 管理内存,但是某些 C API 还需要开发者自己进行内存管理。比如 CF 框架下的 API。
以及 block nil 判断等。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalysisMemoryIssue.png" style="zoom:30%" />
#### 数据问题
在编码过程中一些数据问题可以通过Analyze很好的提示出来。比如下图
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalysisDataIssue.png" style="zoom:30%" />
#### 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了。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalysisNSAssertSideEffectIssue.png" style="zoom:30%" />
##### 死循环
下面是一个很常见的死循环的案例,这种稍微复杂一些的逻辑,乍眼一看,似乎没有什么问题:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalysisEndelessLoopIssue.png" style="zoom:30%" />
这段代码中,是一个二层循环,但是在内层的循环中,没有对 j 做递增,而是做了 result 的递增,这个问题虽然会隐晦,但是新版本的静态检查器会检测出来。
Analyze 功能强大,其实际能检测出的问题会更多。
### 自定义分析器参数
Xcode 也为静态分析器提供了很多的自定义项,方便开发者根据自身工作流进行定制。在 BuildSetting 中通过搜索 `Static Analysis` 关键字,可以筛选出跟分析器相关的设置项。
### 每一次编译都执行静态分析
通过打开 `Analyze During 'Build'` 可以使得每一次编译操作都执行分析器:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalyzerEachBuild.png" style="zoom:30%" />
### 设置静态分析器的运行模式
`Mode of Analysis for 'Analyze'` 可以配置分析器运行的模式Xcode 提供了两种运行模式:
- `Shallow(faster)` `Shallow`规避了去检查一些耗时复杂的检查操作,所以 Shallow 运行的更快
- `Deep` 则进行深入的检查,能抛出更全面的错误
同一个工程,分别看看 Shallow 和 Deep 的耗时差别:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalyzerDeepAndShallowBuildDuration.png" style="zoom:30%" />
### 专项检查配置
静态分析器也提供了一些专项检查的配置,可以根据工程情况定制选择。假设,项目有严格的安全检查,可以打开下图中选中的这些配置项目:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalyzerSecuritySetting.png" style="zoom:30%" />
再或者,如果静态分析器抛出的一些问题不想关注,可以在 Xcode Build Settings 中关闭掉。从而更聚焦于感兴趣、更关注的问题。
### 单个文件的分析
也可以针对单个文件做静态检查。操作路径为Product -> Perform Action -> Analyze "FileName"。
这样只会对单个文件检测,且不会分析 import 进来的文件(可以看到右边的 Person.m 的问题没有被检测出来)
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeStaticAnalysisForSingleFile.png" style="zoom:30%" />

131
Chapter1 - iOS/1.138.md Normal file
View File

@@ -0,0 +1,131 @@
# AFNetworking 源码解读
## 结构
核心包含5个功能模块
- 网络通信模块(AFURLSessionManager、AFHTTPSessionManger)
- 网络状态监听模块(Reachability)
- 网络通信安全策略模块(Security)
- 网络通信信息序列化/反序列化模块(Serialization)
- 对于iOS UIKit库的扩展(UIKit)
AF 是基于 NSURLSession 来封装的,所以核心类 AFURLSessionManager 也是针对 NSURLSession 做的封装其余的4个模块是为了网络通信请求、响应数据的序列化、HTTPS 安全认证、UIKit 推展)
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AFNetworkingProcess.png" style="zoom:60%" />
其中 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;
}
```
初始化都调用到
## 数字证书
数字签名,它能确认消息的完整性,进行认证。
和公钥密码一样也要用到一对公钥、私钥。但相反的是,签名是用私钥加密(生成签名),公钥解密(验证签名)。私钥加密只能有吃有私钥的人完成,而由于公钥是对外公开的,因此任何人都可以用公钥解密(验证签名)。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/DataSignProcess.png" style="zoom:30%" />
公钥基础设置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

347
Chapter1 - iOS/1.139.md Normal file
View File

@@ -0,0 +1,347 @@
# 图形渲染技巧
## GPU、CPU
难道不能直接将数据从 CPU 跨到 GPU 处理?为什么需要多此一举,出现 OpenGL 这个框架?
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CPUGPUWithOpenGLBuffer.png" style="zoom:50%" />
数据饥饿从一块内存中将数据复制到另一块内存中传输速度是非常慢的。内存复制数据时CPU 和 GPU 都不能操作数据,避免引起错误。
- 如果 CPU、GPU 同时操作内存,同步和处理非常麻烦,加一些锁或额外手段将造成速度损耗。
- 如果 GPU 处理完处于等待状态
所以加了 buffer 缓冲区来处理该问题。有很多缓冲区,比如颜色缓冲区、深度缓冲区...
## 着色器渲染流程
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OpenGLShaderProcess.pngg" style="zoom:50%" />
顶点着色器:有多少个顶点,就执行多少次顶点着色器。
光栅化:将顶点着色器的结果转换为像素。将输入的图元描述,转换为与屏幕对应的位置像素片元。
片元着色器将光栅化的结果转换为颜色。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
#### 解决方案
##### 油画算法(画家算法)
先绘制场景中离观察者较远的物体,再绘制离观察者较近的物体
例如下面的场景中,先绘制红色部分,再绘制黄色部分,最后再绘制灰色部分。即可解决隐藏面消除的问题。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OpenGLDrawerAlgorithm.png" style="zoom:30%" />
缺点:
- 需要遮盖的部分,画了多次,造成了渲染性能问题,浪费了资源。
- 叠加的情况,油画算法无法处理。
使用油画算法,只要将场景按照物理距离观察者的距离远近排序。由远及近的绘制即可。 但某些情况下,这个距离无法排序。比如下面的场景:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OpenGlDrawerAlgorithmIssue.png" style="zoom:30%" />
如果三个三角形是叠加的情况,油画算法无法处理。
##### 正背面剔除Face Culling
想象一个 3D 图形你从任何一个方向去观察最多可以看到几个面最多3个从一个立方体的任意位置和方向去看最多可以看到3个面。
思考为什么需要多余的去绘制根本看不到的3个面
如果能以某种方式丢弃这部分数据OpenGL 渲染性能可以提高超过50%。
正背面剔除方案,不仅可以解决隐藏面消除问题,还可以带来性能提升。
如何知道某个面再观察者的视野中会不会出现任何平面都有2个面正面、背面。意味着同一个时刻只能看到一个面。
OpenGL 可以做到检查所有正面朝向观察者的面,并渲染他们,从而丢弃背面朝向的面,这样可以节约片元着色器的开销,提高性能。
核心OpenGL 如何知道绘制的图形中,哪个是正面,哪个是背面?
通过分析顶点数据的顺序。
- 正面:按照逆时针顶点连接顺序的三角形面
- 背面:按照顺时针顶点连接顺序的三角形面
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OpenGLFrontAndBackSurface.png" style="zoom:30%" />
用顺时针、逆时针判断正反面不是绝对的,还需要结合观察者的位置。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OpenGLFrontAndBackSurfaceWithViewer.png" style="zoom:30%" />
分析:
- 左侧三角形的顶点顺序为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 就可能出现不能正确判断两者的深度值,会导致深度测试的结果不可预测,显示出来的现象是交错闪烁。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OpenGLZFightingIssue.png" style="zoom:30%" />
#### 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 <GPUImageInput>`
源对象将静止图像帧上传到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 中进行上屏绘制或者上行推流。上下游链路的打通。

264
Chapter1 - iOS/1.140.md Normal file
View File

@@ -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++ 查看原理
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsBlockExplore.png" style="zoom:30%" />
分析:可以发现 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");
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectMockBlock.png" style="zoom:30%" />
思考:我们目前已经用自定义的 struct 来承接了 block 并成功执行了。能否用 NSInvocation 来触发 block
## NSInvocation 触发 block
一个方法需要成功调用并执行需要3要素
- 方法名称 `SEL`
- 方法签名(参数个人、参数类型、返回值类型等信息) `Method Type Encoding`
- 方法地址、方法实现 `IMP`
如何从自定义的 block 结构体中获取这些信息呢?
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsProcess.png" style="zoom:60%" />
AspectsIdentifier每做一次方法交换都会转换为一次 AspectsIdentifier。是核心逻辑。
以一个例子作为源码探索切入口,点击跳转到源码中
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsIdentifierModule.png" style="zoom:30%" />
可以看到给 NSObject 添加分类,核心 2 个 API。一个对象方法、一个类方法
```objective-c
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
```
内部都走到 `aspect_add` 方法中。其中都会生成 `AspectIdentifier` ,看看是如何生成的?
第一步:生成 block 签名。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsBlockSignature.png" style="zoom:30%" />
第二步:因为我们通过 AOP 给原始方法添加了 block最后的效果是既可以调用原始方法又可以调用 block 添加的代码。实现的前提是什么?
比较 block 和 hook 类的方法的签名信息需要 Match。具体逻辑查看注释。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsSignatureCompare.png" style="zoom:30%" />
```shell
(lldb) po blockSignature
<NSMethodSignature: 0x600003829c20>
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 (@) '@"<AspectInfo>"'
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>"'`,其中的 `AspectInfo` 就代表捕获的变量类型。
比如2个不带参数的 OC 方法签名和 block 方法签名:
- oc 方法签名:`v@:` = `v` + `@` + `:`,返回值 `void`、参数1 `@` 代表对象、参数2 `` 代表 SEL 类型
- block 方法签名:`v@?` = `v` + `@?`,返回值 `void`、参数1 `@?` 代表既是对象,又是 block
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectInvokeBlock.png" style="zoom:30%" />
## objc_msgForward
骚操作:
- 将待 hook 的方法,和 `objc_msgForward` 进行交换。 `objc_msgForward` 不管对象有没有实现,都会触发消息转发流程
- 此时会走 Runtime 的 NSObject `forwardInvocation` 流程。且 Aspects 将 `forwardInvocation` 方法指向了 `__ASPECTS_ARE_BEING_CALLED__` 方法。
经历这么一波处理hook 最后都守口到了 `__ASPECTS_ARE_BEING_CALLED__` 方法中。
前面研究过了 `AspectIdentifier` 的逻辑。接下去继续看看后续步骤。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsHookForwardInvocation.png" style="zoom:30%" />
可以看到内部执行 `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:`。
回归头继续看下面的逻辑。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsHookMethodWithObjcMsgForward.png" style="zoom:30%" />
`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 使用的一个经典库,处理好核心逻辑后,也做了一些黑名单、线程安全等的保护。也有一些类似日志回放功能的处理。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AspectsProcessXmind.png" style="zoom:30%" />

5
Chapter1 - iOS/1.141.md Normal file
View File

@@ -0,0 +1,5 @@
# Crash
## Crash 类型
- 容易越界(数组、字典、字符串等) NSRangeException
- 使用未初始化的变量

View File

@@ -46,4 +46,4 @@ Classes:
该方案是 Apple 标准做法不是骚操作Objc 源码中也有使用。如下所示 该方案是 Apple 标准做法不是骚操作Objc 源码中也有使用。如下所示
<img src="./../assets/APINoteInObjcSourceCode.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/APINoteInObjcSourceCode.png" style="zoom:30%" />

View File

@@ -35,7 +35,7 @@
先附上一张总结的非常棒的RunLoop图 先附上一张总结的非常棒的RunLoop图
<img src="./../assets/2019-05-09-RunLoop-review.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-09-RunLoop-review.png" style="zoom:30%" />
@@ -138,7 +138,7 @@ struct __CFRunLoopMode {
Demo: Demo:
<img src="./../assets/NSRunloopRoundWithCFRunloop.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSRunloopRoundWithCFRunloop.png" style="zoom:30%" />
@@ -178,7 +178,7 @@ Source0:
Demo给屏幕点击事件加断点查看堆栈可以看到是 Source0 触发的。 Demo给屏幕点击事件加断点查看堆栈可以看到是 Source0 触发的。
<img src="./../assets/TouchActionByRunLoopSource0.png" style="zoom:25%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TouchActionByRunLoopSource0.png" style="zoom:25%" />
@@ -221,7 +221,7 @@ CFRunLoopSourceRef 事件源(输入源)
### 一对多的关系 ### 一对多的关系
<img src="./../assets/ThreadRunLoopModeStructure.png" style="zoom:70%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ThreadRunLoopModeStructure.png" style="zoom:70%" />
@@ -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 创建的状态,为了模拟完整的状态,我们开启子线程,在子线程中模拟 上个实验是在主线程对 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。 - 图上左上角的 Input source 是早期 RunLoop 的分法现在分法为Source0 和 Source1。
- Source0:非基于 port 的,用户主动触发的事件。 - Source0:非基于 port 的,用户主动触发的事件。
@@ -527,7 +527,7 @@ CFRelease(obersver);
但是如何直到系统是运行 RunLoop 的哪个函数?给 viewDidLoad 设置断点,在 lldb 模式输入 `bt` 查看堆栈 但是如何直到系统是运行 RunLoop 的哪个函数?给 viewDidLoad 设置断点,在 lldb 模式输入 `bt` 查看堆栈
![](./../assets/RunLoop-Specific.png) ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoop-Specific.png)
查看 CF 中 `CFRunLoop.c`源码。方法比较复杂,做了精简摘要 查看 CF 中 `CFRunLoop.c`源码。方法比较复杂,做了精简摘要
@@ -1137,7 +1137,7 @@ RunLoop 内部几个核心的动作:`__CFRunLoopDoObservers`、`__CFRunLoopSer
上面结合源码看了 Runloop 是怎么运行的。下面通过图片看看 RunLoop 各个状态运行切换的完整流程。 上面结合源码看了 Runloop 是怎么运行的。下面通过图片看看 RunLoop 各个状态运行切换的完整流程。
<img src="./../assets/RunLoop-SourceCode.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoop-SourceCode.png" style="zoom:30%" />
Demo Demo
@@ -1151,7 +1151,7 @@ Demo
2. 上面8>2Runloop 处理 GCD Async To Main Quque 2. 上面8>2Runloop 处理 GCD Async To Main Quque
<img src="./../assets/RunLoopPerformTaskFromGCDMainQueue.png" style="zoom:25%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoopPerformTaskFromGCDMainQueue.png" style="zoom:25%" />
@@ -1165,7 +1165,7 @@ Demo
可以在 App 运行过程中,点击 Xcode 左下角的 debug 暂停按钮,可以看到 App 堆栈存在系统调用 可以在 App 运行过程中,点击 Xcode 左下角的 debug 暂停按钮,可以看到 App 堆栈存在系统调用
<img src="./../assets/RunLoopSleepSystemCall.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoopSleepSystemCall.png" style="zoom:30%" />
@@ -1887,7 +1887,7 @@ main 方法内其实就是在执行 _selector。也就是在 NSThread 的初始
2. `[[NSRunLoop currentRunLoop] run]` api 换掉。查看系统说明,底层其实就是一个无限循环,循环内部不断调用 `runMode:beforeDate:`。下面也有建议,建议我们想销毁 RunLoop可以替换 API比如设置一个变量标记是否需要结束 RunLoop 2. `[[NSRunLoop currentRunLoop] run]` api 换掉。查看系统说明,底层其实就是一个无限循环,循环内部不断调用 `runMode:beforeDate:`。下面也有建议,建议我们想销毁 RunLoop可以替换 API比如设置一个变量标记是否需要结束 RunLoop
<img src="./../assets/RunLoop-RunIssue.png" style="zoom:45%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoop-RunIssue.png" style="zoom:45%" />
改进代码如下 改进代码如下
@@ -1933,7 +1933,7 @@ self.thread = [[LifeThread alloc] initWithBlock:^{
效果如下: 效果如下:
<img src="./../assets/RunLoopThreadKeepLive.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoopThreadKeepLive.png" style="zoom:30%" />

View File

@@ -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()` 不管是提交到串行队列还是并发队列,都是在当前线程执行。 只要是同步提交任务 `dispatch_sync()` 不管是提交到串行队列还是并发队列,都是在当前线程执行。
## 一些经典 Demo
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/BlockAndQueue.png" style="zoom:30%" />
会输出什么?
打印结果电脑速度快的话会有很多次打印出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 研究 ## performSelector...withObject 研究
@@ -263,7 +316,7 @@ NSLog(@"5");
Demo1 Demo1
<img src="./../assets/RunloopPerformSelector.png" style="zoom:25%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunloopPerformSelector.png" style="zoom:25%" />
QA为什么先打印1、3再打印2因为 `performSelector...withObject...afterDelay` 相当于给 RunLoop 添加了一个 TimerTimer 运行需要 RunLoop 配合。RunLoop 在被唤醒的时候会处理定时器。 QA为什么先打印1、3再打印2因为 `performSelector...withObject...afterDelay` 相当于给 RunLoop 添加了一个 TimerTimer 运行需要 RunLoop 配合。RunLoop 在被唤醒的时候会处理定时器。
@@ -271,7 +324,7 @@ Demo2:
<img src="./../assets/RunloopPerformSelectorAfterDelay.png" style="zoom:25%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunloopPerformSelectorAfterDelay.png" style="zoom:25%" />
@@ -327,7 +380,7 @@ QA为什么 showLog 里的2没有打印
Demo3: Demo3:
<img src="./../assets/GCDThreadWillTerminateWhenBlockFinished.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/GCDThreadWillTerminateWhenBlockFinished.png" style="zoom:30%" />
同理GCD 虽然开启了子线程,但是 Block 结束后线程也就结束了。所以线程任务中的1秒后的任务肯定也结束了。 同理GCD 虽然开启了子线程,但是 Block 结束后线程也就结束了。所以线程任务中的1秒后的任务肯定也结束了。
@@ -335,13 +388,13 @@ Demo3:
Demo4: Demo4:
<img src="./../assets/NSThreadWillTerminateSoCannotUse.png" style="zoom:25%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSThreadWillTerminateSoCannotUse.png" style="zoom:25%" />
可以看到 NSThread 里的 block 执行结束后thread 结束了。后面的 performSelector 想在线程里执行任务,就会 crash。 可以看到 NSThread 里的 block 执行结束后thread 结束了。后面的 performSelector 想在线程里执行任务,就会 crash。
解决办法也是在线程的 block 里面加 RunLoop让它保活 解决办法也是在线程的 block 里面加 RunLoop让它保活
<img src="./../assets/NSThreadWillTerminateSoCanUseViaRunLoopPort.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSThreadWillTerminateSoCanUseViaRunLoopPort.png" style="zoom:30%" />
@@ -550,21 +603,21 @@ sistep instruction简写为 stepisi。当你在 Xcode 汇编面板看
第一步:当第二次调用 saveMoney 方法,开启汇编调试 第一步:当第二次调用 saveMoney 方法,开启汇编调试
<img src="./../assets/OSSpinLock-Assemble2.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OSSpinLock-Assemble2.png" style="zoom:30%" />
看到可疑方法 `OSSpinLockLock`给它加断点看到第10行高亮了。lldb 模式输入 c敲回车。次数输入 si 即可进入 `OSSpinLockLock`  方法内部调试 看到可疑方法 `OSSpinLockLock`给它加断点看到第10行高亮了。lldb 模式输入 c敲回车。次数输入 si 即可进入 `OSSpinLockLock`  方法内部调试
第二步:继续输入 si敲回车 第二步:继续输入 si敲回车
<img src="./../assets/OSSpinLock-Assemble3.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OSSpinLock-Assemble3.png" style="zoom:30%" />
第三步:看到可疑方法 `_OSSpinLockLockSlow`给它加断点lldb 输入 C。此时断点到这一行了继续输入 si。 第三步:看到可疑方法 `_OSSpinLockLockSlow`给它加断点lldb 输入 C。此时断点到这一行了继续输入 si。
<img src="./../assets/OSSpinLock-Assemble4.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OSSpinLock-Assemble4.png" style="zoom:30%" />
第四步:在 `OSSpinLockLockSlow` 方法内部调试,不断输入 si。 第四步:在 `OSSpinLockLockSlow` 方法内部调试,不断输入 si。
<img src="./../assets/OSSpinLockAssemble1.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OSSpinLockAssemble1.png" style="zoom:30%" />
发现不断 si 最终一直会在第6行到第19行之间执行。懂汇编的会发现这其实是一个 while 循环。便可以证明自旋锁 OSSpinLock 在等锁的时候,底层实现是执行 while 循环,忙等,“太浪费性能了”(如果使用锁资源的线程任务很简单,那自旋也是高效的,可以快速获取锁。) 发现不断 si 最终一直会在第6行到第19行之间执行。懂汇编的会发现这其实是一个 while 循环。便可以证明自旋锁 OSSpinLock 在等锁的时候,底层实现是执行 while 循环,忙等,“太浪费性能了”(如果使用锁资源的线程任务很简单,那自旋也是高效的,可以快速获取锁。)
@@ -643,26 +696,26 @@ int cursorr = 1;
假如对存钱过程,忘记解锁怎么办?产生死锁,如下 假如对存钱过程,忘记解锁怎么办?产生死锁,如下
<img src="./../assets/Thread-deadlock-unfaillock.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Thread-deadlock-unfaillock.png" style="zoom:30%" />
添加 cursor 标记死锁是发生在 `saveMoney` 方法执行的第几次。发现是第二次。因为第一次锁没有任何使用方,所以加锁成功,当第二次加锁的时候发现锁没有释放,所以产生死锁。 添加 cursor 标记死锁是发生在 `saveMoney` 方法执行的第几次。发现是第二次。因为第一次锁没有任何使用方,所以加锁成功,当第二次加锁的时候发现锁没有释放,所以产生死锁。
这时候使用尝试加锁 API `os_unfair_lock_trylock` 即可成功如下 这时候使用尝试加锁 API `os_unfair_lock_trylock` 即可成功如下
<img src="./../assets/Thread-deadlock-unfairTrylock.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Thread-deadlock-unfairTrylock.png" style="zoom:30%" />
#### 汇编窥探原理 #### 汇编窥探原理
同样方式看看 ,按照上述调试汇编代码的步骤,我将关键步骤截图如下 同样方式看看 ,按照上述调试汇编代码的步骤,我将关键步骤截图如下
<img src="./../assets/osunfairlock-assemble1.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/osunfairlock-assemble1.png" style="zoom:30%" />
<img src="./../assets/osunfairlock-assemble2.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/osunfairlock-assemble2.png" style="zoom:30%" />
<img src="./../assets/osunfairlock-assemble3.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/osunfairlock-assemble3.png" style="zoom:30%" />
<img src="./../assets/osunfairlock-assemble4.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/osunfairlock-assemble4.png" style="zoom:30%" />
<img src="./../assets/osunfairlock-assemble5.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/osunfairlock-assemble5.png" style="zoom:30%" />
<img src="./../assets/osunfairlock-assemble6.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/osunfairlock-assemble6.png" style="zoom:30%" />
结论:可以看到 `os_unfair_lock` 在锁等待的时候,底层调用的是 `sysCall`,当这一步执行后会发现后续代码都不执行了,也就是调用系统底层能力,线程真正休眠了,而不是一个循环忙等,性能也好。 结论:可以看到 `os_unfair_lock` 在锁等待的时候,底层调用的是 `sysCall`,当这一步执行后会发现后续代码都不执行了,也就是调用系统底层能力,线程真正休眠了,而不是一个循环忙等,性能也好。
@@ -711,7 +764,7 @@ pthread_mutex_destroy(&_moneyLock);
使用如下 使用如下
<img src="./../assets/PThreadMutextLock.png" style="zoom:25%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PThreadMutextLock.png" style="zoom:25%" />
#### 递归锁 #### 递归锁
@@ -757,7 +810,7 @@ pthread_mutex_destroy(&_moneyLock);
改进后的效果如下 改进后的效果如下
<img src="./../assets/PThreadMutextRecursiveLock.png" style="zoom:25%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PThreadMutextRecursiveLock.png" style="zoom:25%" />
@@ -769,33 +822,33 @@ QA互斥递归锁可以在不同线程中加锁吗
#### 汇编窥探原理 #### 汇编窥探原理
<img src="./../assets/PThreadMutexLock1.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PThreadMutexLock1.png" style="zoom:30%" />
输入 si 继续跟进可以看到还是在执行我们自己的代码LockExplore image 的 `pthread_mutex_lock` 方法 输入 si 继续跟进可以看到还是在执行我们自己的代码LockExplore image 的 `pthread_mutex_lock` 方法
<img src="./../assets/PThreadMutexLock2.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PThreadMutexLock2.png" style="zoom:30%" />
继续输入 si 跟进 继续输入 si 跟进
<img src="./../assets/PThreadMutexLock3.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PThreadMutexLock3.png" style="zoom:30%" />
可以看到此时调用到系统 `libsystem_pthread.dylib` 库的 `pthread_mutex_lock` 方法了。 可以看到此时调用到系统 `libsystem_pthread.dylib` 库的 `pthread_mutex_lock` 方法了。
第41行看到关键函数继续输入 si 进去看看 第41行看到关键函数继续输入 si 进去看看
<img src="./../assets/PThreadMutexLock4.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PThreadMutexLock4.png" style="zoom:30%" />
可以看到内部第62行关键函数调用了 `_pthread_mutex_firstfit_lock_wait` 方法。此时继续输入 si 跟踪看看 可以看到内部第62行关键函数调用了 `_pthread_mutex_firstfit_lock_wait` 方法。此时继续输入 si 跟踪看看
<img src="./../assets/PThreadMutexLock5.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PThreadMutexLock5.png" style="zoom:30%" />
可以看到内部第25行调用了关键函数 `__psynch_mutexwait`,继续输入 si 看看 可以看到内部第25行调用了关键函数 `__psynch_mutexwait`,继续输入 si 看看
<img src="./../assets/PThreadMutexLock6.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PThreadMutexLock6.png" style="zoom:30%" />
可以看到内部继续调用了系统 `libsystem_pthread.dylib` 库的 `__psynch_mutexwait` 方法。继续输入 si 可以看到内部继续调用了系统 `libsystem_pthread.dylib` 库的 `__psynch_mutexwait` 方法。继续输入 si
<img src="./../assets/PThreadMutexLock7.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PThreadMutexLock7.png" style="zoom:30%" />
可以看到内部第4行发生了系统调用 `sysCall`,执行完第四句指令,线程立马就结束了。 可以看到内部第4行发生了系统调用 `sysCall`,执行完第四句指令,线程立马就结束了。
@@ -815,7 +868,7 @@ QA互斥递归锁可以在不同线程中加锁吗
激活所有等待该条件的线程 `pthread_cond_broadcast(&_condition)` 激活所有等待该条件的线程 `pthread_cond_broadcast(&_condition)`
<img src="./../assets/PThreadConditionLock.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PThreadConditionLock.png" style="zoom:30%" />
可以看到同时调用 remove、add 方法 可以看到同时调用 remove、add 方法
@@ -917,13 +970,13 @@ NSRecursiveLock 不能在多线程下递归调用。@synchronized 可以在多
Demo Demo
<img src="./../assets/NSLockDemo.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSLockDemo.png" style="zoom:30%" />
NSLock 死锁 NSLock 死锁
<img src="./../assets/NSLockDeadLock.png" style="zoom:40%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSLockDeadLock.png" style="zoom:40%" />
会发生死锁后续代码无法执行App 表现就是 ANR。重复对 NSLock 进行加锁可能导致死锁问题,同时也可能引发数据竞争和性能下降等并发相关隐患 会发生死锁后续代码无法执行App 表现就是 ANR。重复对 NSLock 进行加锁可能导致死锁问题,同时也可能引发数据竞争和性能下降等并发相关隐患
@@ -973,7 +1026,7 @@ API
Demo Demo
<img src="./../assets/NSConditionLockDemo.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSConditionLockDemo.png" style="zoom:30%" />
@@ -981,13 +1034,13 @@ Demo
使用 NSCondition 的时候 unlock 和 signal 的顺序可能会对结果造成影响。举个例子 使用 NSCondition 的时候 unlock 和 signal 的顺序可能会对结果造成影响。举个例子
<img src="./../assets/NSConditionOrder1.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSConditionOrder1.png" style="zoom:30%" />
可以看到在这种情况下,由于 NSCondition 另一个地方 waitwait 也需要释放锁,但是另一个发 signal 的地方还没释放锁。所以会等待2s。 可以看到在这种情况下,由于 NSCondition 另一个地方 waitwait 也需要释放锁,但是另一个发 signal 的地方还没释放锁。所以会等待2s。
针对这个情况,可以将 unlock 和 signal 的顺序进行调整。 针对这个情况,可以将 unlock 和 signal 的顺序进行调整。
<img src="./../assets/NSConditionOrder2.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSConditionOrder2.png" style="zoom:30%" />
先解锁,然后发送 singal后续其他的业务逻辑也不影响。当然这个需要针对实际代码进行设计。 先解锁,然后发送 singal后续其他的业务逻辑也不影响。当然这个需要针对实际代码进行设计。
@@ -1030,7 +1083,7 @@ API 如下:
Demo Demo
<img src="./../assets/NSConditionLockDemo1.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSConditionLockDemo1.png" style="zoom:30%" />
分析虽然通过3个线程设置了线程的先后顺序但是多线程任务执行的时候到底谁先执行是没办法控制的。但是通过 `NSConditionLock lockWhenCondition:*` 的能力,可以控制线程的执行顺序。 分析虽然通过3个线程设置了线程的先后顺序但是多线程任务执行的时候到底谁先执行是没办法控制的。但是通过 `NSConditionLock lockWhenCondition:*` 的能力,可以控制线程的执行顺序。
@@ -1046,7 +1099,7 @@ Demo
线程同步的本质就是多线程的任务是顺序执行 线程同步的本质就是多线程的任务是顺序执行
<img src="./../assets/SerialQueueToSolveThreadSyn.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SerialQueueToSolveThreadSyn.png" style="zoom:30%" />
@@ -1060,11 +1113,11 @@ semaphore 叫做”信号量”
信号量的初始值为1代表同时只允许1条线程访问资源保证线程同步 信号量的初始值为1代表同时只允许1条线程访问资源保证线程同步
<img src="./../assets/DispatchSemaphoreControlThreadCount1.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/DispatchSemaphoreControlThreadCount1.png" style="zoom:30%" />
可以看到打印了20个线程但是我们控制线程最大数量怎么办呢可以用信号量实现。效果如下 可以看到打印了20个线程但是我们控制线程最大数量怎么办呢可以用信号量实现。效果如下
<img src="./../assets/DispatchSemaphoreControlThreadCount2.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/DispatchSemaphoreControlThreadCount2.png" style="zoom:30%" />
`dispatch_semaphore_wait` 函数的本质 `dispatch_semaphore_wait` 函数的本质
@@ -1076,7 +1129,7 @@ semaphore 叫做”信号量”
所以如何让线程同步?设置信号量的值=1即可。保证同一时间只有一个线程任务在执行。代码如下 所以如何让线程同步?设置信号量的值=1即可。保证同一时间只有一个线程任务在执行。代码如下
<img src="./../assets/SemaphoreMethodToControlThreadSync.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SemaphoreMethodToControlThreadSync.png" style="zoom:30%" />
@@ -1136,15 +1189,15 @@ dispatch_semaphore_signal(semaphore);
`@synchronized` 使用很方便,它是对 `pthread_mutex_t` 递归锁的封装。Demo 如下 `@synchronized` 使用很方便,它是对 `pthread_mutex_t` 递归锁的封装。Demo 如下
<img src="./../assets/SynchronizedControlThreadSync.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SynchronizedControlThreadSync.png" style="zoom:30%" />
为了探究下实现,开启汇编调试 为了探究下实现,开启汇编调试
<img src="./../assets/synchronized-asemble1.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/synchronized-asemble1.png" style="zoom:30%" />
<img src="./../assets/synchronized-asemble2.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/synchronized-asemble2.png" style="zoom:30%" />
通过汇编可以看到 `@synchronized` 底层调用了 `objc_sync_enter` 方法,其中又调用了 `id2data` 和 `os_unfair_recursive_lock_lock_with_options` 方法。 可以查看 objc4 的源码(笔者的 objc 版本为 objc4-objc4-912.3),查找 `objc_sync_enter` 通过汇编可以看到 `@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 Demo
<img src="./../assets/PThreadRWLockDemo.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PThreadRWLockDemo.png" style="zoom:30%" />
@@ -1648,7 +1701,7 @@ dispatch_barrier_async(self.queue, ^{
上 Demo 上 Demo
<img src="./../assets/DispatchBarrierReadWriteDemo.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/DispatchBarrierReadWriteDemo.png" style="zoom:30%" />

View File

@@ -24,7 +24,7 @@ NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval
栈、堆、BSS、数据段、代码段 栈、堆、BSS、数据段、代码段
<img src="./../assets/iOS-MemoryLayout.png" style="zoom:50%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/iOS-MemoryLayout.png" style="zoom:50%" />
@@ -53,7 +53,7 @@ BSS段bss segment通常用来存储程序中未被初始化的全局变
代码段code segment编译之后的代码。通常是指用来存储程序可执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定并且内存区域通常属于只读某些架构也允许代码段为可写即允许修改程序。 代码段code segment编译之后的代码。通常是指用来存储程序可执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定并且内存区域通常属于只读某些架构也允许代码段为可写即允许修改程序。
![内存](./../assets/ram.png) ![内存](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ram.png)
上 Demo 验证 上 Demo 验证
@@ -132,7 +132,7 @@ Tagged Pointer 格式下,指针值不再是有效抵制,而是表示值。
当对 TaggedPointer 数据调用方法的时候objc_msgSend 能识别出如果是 Tagged Pointer比如 NSNumber 的 intValue 方法,直接从指针提取数据,节省了调用开销。所以使用了 TaggedPointer 技术不仅节约内存空间,又能提高方法查找速度。 当对 TaggedPointer 数据调用方法的时候objc_msgSend 能识别出如果是 Tagged Pointer比如 NSNumber 的 intValue 方法,直接从指针提取数据,节省了调用开销。所以使用了 TaggedPointer 技术不仅节约内存空间,又能提高方法查找速度。
<img src="./../assets/TaggedPointerStructure.png" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TaggedPointerStructure.png" />
@@ -233,7 +233,7 @@ Tagged Pointer 也就是一个伪指针,对象的指针中存储的数据变
Demo Demo
<img src="./../assets/TaggedPointerSaveMemoryUsage.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TaggedPointerSaveMemoryUsage.png" style="zoom:30%" />
在 64 位的 cpu 下,如果使用 Tagged Pointer 技术的话,则 NSNumber 对象的值直接存储在了指针中系统不会为其在堆上分配内存可以节省很多内存开销。此时NSNumber 对象的指针中存储的数据变成了 Tag + Data 的形式Tag 为特殊标记用于区分NSNumber、NSDate、NSString 等小内存对象的类型Data 为具体的值)。这样使用一个 NSNumber 对象只需要在栈中开辟 8 个字节的指针内存。当栈中 8 个字节的指针内存不够存储数据时,才会再将 NSNumber 对象存储到堆中 在 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 即可。 路径Xcode - Edit Scheme - Run - Arguments - Environment Variables - 添加环境变量 `OBJC_DISABLE_TAG_OBFUSCATION` 设置为 YES 即可。
<img src="./../assets/XcodeDisableTaggedPointerConfuse.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeDisableTaggedPointerConfuse.png" style="zoom:30%" />
@@ -422,7 +422,7 @@ _objc_decodeTaggedPointer_noPermute_withObfuscator(const void * _Nullable ptr, u
} }
``` ```
<img src="./../assets/TaggedPointerDecode.png" style="zoom:40%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TaggedPointerDecode.png" style="zoom:40%" />
@@ -430,7 +430,7 @@ _objc_decodeTaggedPointer_noPermute_withObfuscator(const void * _Nullable ptr, u
#### Tagged Pointer 与 isa #### Tagged Pointer 与 isa
<img src="./../assets/TaggedPointerNSNumberDemo.png" style="zoom:40%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TaggedPointerNSNumberDemo.png" style="zoom:40%" />
通过参考 objc 源码,针对对象指针进行解密后发现: 通过参考 objc 源码,针对对象指针进行解密后发现:
@@ -461,7 +461,7 @@ b 也就是11二进制为 `1011`Tagged Pointer 中iOS 侧第一位是 T
3 区分数据类型。具体是什么数据类型,继续做个实验看看 3 区分数据类型。具体是什么数据类型,继续做个实验看看
<img src="./../assets/TaggedPointerDataTypeDemo.png" style="zoom:40%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TaggedPointerDataTypeDemo.png" style="zoom:40%" />
@@ -482,7 +482,7 @@ b 也就是11二进制为 `1011`Tagged Pointer 中iOS 侧第一位是 T
Objc 源码中NSInteger、NSUInteger 都是别名。初始化 NSNumber 的时候用的是 `NSNumber numberWithInteger:<#(NSInteger)#>` Objc 源码中NSInteger、NSUInteger 都是别名。初始化 NSNumber 的时候用的是 `NSNumber numberWithInteger:<#(NSInteger)#>`
<img src="./../assets/ObjcNSIntegerIsLong.png" style="zoom:30%" > <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ObjcNSIntegerIsLong.png" style="zoom:30%" >
@@ -560,7 +560,7 @@ Tagged Pointer 如何区分是较小的对象,比如 NSString、NSDate、NSNum
验证下 验证下
<img src="./../assets/TaggedPointerKind.png" style="zoom:30%" > <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TaggedPointerKind.png" style="zoom:30%" >
可以看到: 可以看到:
@@ -573,11 +573,11 @@ Tagged Pointer 如何区分是较小的对象,比如 NSString、NSDate、NSNum
下面是针对 double 包装成 NSNumber 的 Tagged Pointer 指针结构拆分: 下面是针对 double 包装成 NSNumber 的 Tagged Pointer 指针结构拆分:
<img src="./../assets/NSNumberTaggedPointerStructure.png" style="zoom:90%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSNumberTaggedPointerStructure.png" style="zoom:90%" />
<img src="./../assets/NSNumberAddressToBinary.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSNumberAddressToBinary.png" style="zoom:30%" />
@@ -770,7 +770,7 @@ Demo1
运行该代码会 Crash报错信息如下 运行该代码会 Crash报错信息如下
<img src="./../assets/TaggedPointerCrash.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TaggedPointerCrash.png" style="zoom:30%" />
@@ -795,11 +795,11 @@ Demo1
改法1将 property 改为 **atomic** 修饰的。 改法1将 property 改为 **atomic** 修饰的。
<img src="./../assets/TaggedPointerCrashFix1.png" style="zoom:50%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TaggedPointerCrashFix1.png" style="zoom:50%" />
改法2对 name 加锁 改法2对 name 加锁
<img src="./../assets/TaggedPointerCrashFix2.png" style="zoom:50%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TaggedPointerCrashFix2.png" style="zoom:50%" />
@@ -807,7 +807,7 @@ Demo1
Demo2 Demo2
<img src="./../assets/TaggedPointerWillNotCrash.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TaggedPointerWillNotCrash.png" style="zoom:30%" />
@@ -854,7 +854,7 @@ NSString、NSMutableString 继承关系如下:
<img src="./../assets/NSStringClassClusterAndTaggedPointer.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSStringClassClusterAndTaggedPointer.png" style="zoom:30%" />
通过 `[[NSString alloc] initWithString:@"**"]` 方式创建的 NSString 字符串 通过 `[[NSString alloc] initWithString:@"**"]` 方式创建的 NSString 字符串
@@ -922,27 +922,27 @@ iOS 中使用引用计数来管理 OC 对象的内存。一个新创建的 OC
调用 retain/copy 会让 OC 对象的引用计数 +1调用 release 会让 OC 对象的引用计数 -1。 调用 retain/copy 会让 OC 对象的引用计数 +1调用 release 会让 OC 对象的引用计数 -1。
<img src="./../assets/PersonAndCatMRCIssue.png" style="30%" > <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PersonAndCatMRCIssue.png" style="30%" >
可以看到,如果我们提前将 cat 释放了,那后续赋值给 person 的 _cat 成员变量就没法使用了,因为已经释放了,否则就会造成 `EXC_BAD_ACCESS`。这样子太不灵活了。需要改进下: 可以看到,如果我们提前将 cat 释放了,那后续赋值给 person 的 _cat 成员变量就没法使用了,因为已经释放了,否则就会造成 `EXC_BAD_ACCESS`。这样子太不灵活了。需要改进下:
调用 setCat 的时候,对传入的 cat 进行 retain引用计数 +1谁用谁管理同样的最后在 Person 对象释放的时候对 cat 进行 release引用计数 -1. 调用 setCat 的时候,对传入的 cat 进行 retain引用计数 +1谁用谁管理同样的最后在 Person 对象释放的时候对 cat 进行 release引用计数 -1.
<img src="./../assets/PersonAndCatMRCIssue1.png" style="30%" > <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PersonAndCatMRCIssue1.png" style="30%" >
但上面的代码不完美,还是存在问题。假设 cat1、cat2 2个对象当作参数调用2次 setCat 方法,如果 setCat 方法内部不做处理会导致第2次调用 setCat 后,之前调用时传入的 cat1 会无法释放。 但上面的代码不完美,还是存在问题。假设 cat1、cat2 2个对象当作参数调用2次 setCat 方法,如果 setCat 方法内部不做处理会导致第2次调用 setCat 后,之前调用时传入的 cat1 会无法释放。
<img src="./../assets/PersonAndCatMRCIssue2.png" style="30%" > <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PersonAndCatMRCIssue2.png" style="30%" >
修改下。调用 setCat 方法时,对之前的 _cat 调用 release对旧的引用计数-1再对新传入的对象调用 retain让引用计数+1然后赋值 修改下。调用 setCat 方法时,对之前的 _cat 调用 release对旧的引用计数-1再对新传入的对象调用 retain让引用计数+1然后赋值
<img src="./../assets/PersonAndCatMRCIssue3.png" style="30%" > <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PersonAndCatMRCIssue3.png" style="30%" >
上面的代码还是存在问题,会造成僵尸对象问题 上面的代码还是存在问题,会造成僵尸对象问题
<img src="./../assets/PersonAndCatMRCIssue4.png" style="30%" > <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PersonAndCatMRCIssue4.png" style="30%" >
分析下 cat 的引用计数情况: 分析下 cat 的引用计数情况:
@@ -954,7 +954,7 @@ iOS 中使用引用计数来管理 OC 对象的内存。一个新创建的 OC
改进 改进
<img src="./../assets/PersonAndCatMRCIssue5.png" style="30%" > <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PersonAndCatMRCIssue5.png" style="30%" >
@@ -1403,14 +1403,14 @@ class StripedMap {
``` ```
- iOS 侧 StripeCount 为8 - iOS 侧 StripeCount 为8
- `indexForPointer` 方法根据传入的指针,将指针地址转换为 uintptr_t 类型然后将地址右移4位和右移9位的结果进行或运算,然后将结果取模 StripeCountiOS 侧为8用于确定索引的范围范围在[0, stripeCount -1] - `indexForPointer` 方法根据传入的指针,将指针地址转换为 uintptr_t 类型然后将地址右移4位和右移9位的结果进行或运算,然后将结果取模 StripeCountiOS 侧为8用于确定索引的范围范围在[0, stripeCount -1]
- Operator 重写了运算符 [],底层调用 `indexForPointer` 方法,使之使用起来更像一个数组。 - Operator 重写了运算符 [],底层调用 `indexForPointer` 方法,使之使用起来更像一个数组。
### 引用计数表 ### 引用计数表
<img src="./../assets/ReferenceCountintStructure.png" style="zoom:40%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ReferenceCountintStructure.png" style="zoom:40%" />
@@ -1418,7 +1418,7 @@ class StripedMap {
weak_table_t 结构如下: weak_table_t 结构如下:
<img src="./../assets/WeakReferenceCountTable.png" style="zoom:70%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WeakReferenceCountTable.png" style="zoom:70%" />
```c++ ```c++
#define WEAK_INLINE_COUNT 4 #define WEAK_INLINE_COUNT 4
@@ -1473,7 +1473,7 @@ struct weak_entry_t {
#### 存 weak 对象 #### 存 weak 对象
<img src="./../assets/ObjcStoreWeakWhenUseWeak.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ObjcStoreWeakWhenUseWeak.png" style="zoom:30%" />
@@ -2275,7 +2275,7 @@ void sel_init(size_t selrefCount){
在 gone 处加断点,利用 runtime 查看类中的方法信息 在 gone 处加断点,利用 runtime 查看类中的方法信息
![](./../assets/cxx_destructDemo1.png) ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/cxx_destructDemo1.png)
发现存在 `.cxx_destruct` 方法。 发现存在 `.cxx_destruct` 方法。
@@ -2310,7 +2310,7 @@ void sel_init(size_t selrefCount){
@end @end
``` ```
![](./../assets/cxx_destructdemo3.png) ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/cxx_destructdemo3.png)
Tips@property 会自动生成成员变量,另外类后面加 `{}` 在内部也可以加成员变量,假如成员变量是对象类型,比如 NSString则叫实例变量。 Tips@property 会自动生成成员变量,另外类后面加 `{}` 在内部也可以加成员变量,假如成员变量是对象类型,比如 NSString则叫实例变量。
@@ -2324,7 +2324,7 @@ Tips@property 会自动生成成员变量,另外类后面加 `{}` 在内部
在 gone 的地方加断点,输入 `watchpoint set variable p->_name`,则会将 `_name` 实例变量加入 watchpoint当变量被修改时会触发断点可以看出从某个值变为 0x0也就是 nil。此时边上调用堆栈显示在 `objc_storestrong` 方法中,被设置为 nil. 在 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"];
隐式调用工厂方法 隐式调用工厂方法
<img src="./../assets/ARCWillCrashWhenCallNewMethodDirectly.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ARCWillCrashWhenCallNewMethodDirectly.png" style="zoom:30%" />
@@ -2577,7 +2577,7 @@ Person *person2 = [Person personWithName:@"FantasticLBP"];
如何修改?加一个 bridge 即可。 如何修改?加一个 bridge 即可。
<img src="./../assets/FixARCWillCrashWhenCallNewMethodDirectly.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FixARCWillCrashWhenCallNewMethodDirectly.png" style="zoom:30%" />
由于 ARC 没有加 retain。所以 `person = (__bridge id)result;` 这里完成了对象的 retain。ARC 在退出方法的作用域时给对象加上release。前后对应内存正确。 由于 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 对象占用 4096 16的3次方0x2000字节内存除了用来存放它内部的成员变量内部成员固定有7个56个字节即 `0x18`, `0x1000 + 0x38 = 0x1038` ),剩下的空间用来存放 autorelease 对象的地址
- 所有的 AutoreleasePoolPage 对象通过**双向链表**的形式连接在一起。child 指向下一个 AutoreleasePoolPage 对象parent 指向上一个 AutoreleasePoolPage 对象 - 所有的 AutoreleasePoolPage 对象通过**双向链表**的形式连接在一起。child 指向下一个 AutoreleasePoolPage 对象parent 指向上一个 AutoreleasePoolPage 对象
![](./../assets/autoreleasepool.png) ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/autoreleasepool.png)
```objectivec ```objectivec
id * begin() { id * begin() {
@@ -3014,7 +3014,7 @@ class AutoreleasePoolPage : private AutoreleasePoolPageData {
举个例子for 循环创建1000个 Person 对象,用 autorelease 修饰,如何工作? 举个例子for 循环创建1000个 Person 对象,用 autorelease 修饰,如何工作?
<img src="./../assets/AutoreleasePoolPageWithForIterator.png" style="zoom:80%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AutoreleasePoolPageWithForIterator.png" style="zoom:80%" />
分析: 分析:
@@ -3053,7 +3053,7 @@ int main(int argc, const char * argv[]) {
main 方法内部3个 autoreleasepool 底层怎么样工作的? main 方法内部3个 autoreleasepool 底层怎么样工作的?
<img src="./../assets/AutoreleasePoolMoreItem.png" style="zoom:60%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AutoreleasePoolMoreItem.png" style="zoom:60%" />
分析: 分析:
@@ -3920,7 +3920,7 @@ iOS 在主线程的 Runloop 中注册了2个 Observer
结合 RunLoop 运行图 结合 RunLoop 运行图
![](./../assets/RunLoop-SourceCode.png) ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoop-SourceCode.png)
- 01 通知 Observer 进入 Loop 会调用 `objc_autoreleasePoolPush` - 01 通知 Observer 进入 Loop 会调用 `objc_autoreleasePoolPush`
@@ -4017,7 +4017,7 @@ IMP Caching 比其他方法快2倍。
### OC 中有没有不对内存进行强持有的集合类型? ### OC 中有没有不对内存进行强持有的集合类型?
NSHashMapNSMapTable 都可以描述 key、value 的内存修饰。 `NSHashMap`、`NSMapTable` 都可以描述 key、value 的内存修饰。
数组有 NSPointerArray 内部持有的是对象的指针,并非直接保存对象。不过 oc 转指针需要加 `(__bridge void*)` 进行修饰。NSPointerArray 的构造方法中可以通过 NSPointerFunctionsOptions 来声明内存的控制。 数组有 NSPointerArray 内部持有的是对象的指针,并非直接保存对象。不过 oc 转指针需要加 `(__bridge void*)` 进行修饰。NSPointerArray 的构造方法中可以通过 NSPointerFunctionsOptions 来声明内存的控制。
@@ -4045,6 +4045,22 @@ NSHashMap、NSMapTable 都可以描述 key、value 的内存修饰。
(lldb) (lldb)
``` ```
再来2个实验
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSDictionaryCopyTheKey.png" style="zoom:30%" />
分析:可以看到 p1 的地址,在刚初始化后,和当作 key 加入到 NSDictionary 后,地址发生了变化。对 p1 执行了 copy 操作。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSMapTableNSDictionaryMemoryControl.png" style="zoom:30%" />
分析:
- table1 只有1个元素是因为对 p1 执行的是 `NSPointerFunctionsWeakMemory` 所以不会产生2个对象。同一个对象的 hash 值一样。所以仅存在1个元素
- table2 的 key 是 `NSPointerFunctionsCopyIn` copy 产生2个不同的 p且 hash 值不一样所以存在2个元素
- 2个 NSMapTable 对 key 的内存操作不一样,其结果也不一样。如果 NSMapTable key 用 `NSPointerFunctionsCopyIn` 修饰,其效果等价于 NSMutableDictionary。
### NSError 内存泄漏的 case ### NSError 内存泄漏的 case
同事问了一个问题,下面的代码存在什么问题? 同事问了一个问题,下面的代码存在什么问题?
@@ -4086,7 +4102,7 @@ NSHashMap、NSMapTable 都可以描述 key、value 的内存修饰。
这段代码运行会 crash信息如下 这段代码运行会 crash信息如下
![](./../assets/NSErrorZombieCrash.png) ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSErrorZombieCrash.png)
原因是 NSError 构造方法内部会加 autorelease。源码如下 原因是 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) 可以定位僵尸对象,并且打印出具体堆栈,并模拟系统行为调用 `abort` 。对监控原理和工具实现感兴趣的可以查看这里[带你打造一套 APM 监控系统-内存监控之野指针/内存泄漏监控](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.74.md#zombieSniffer)

View File

@@ -370,7 +370,7 @@ Native 每次改动都比较“慢”,所以类似 Header 就很需要。
PS Native 打开 H5如果 300ms 没有响应则需要 loading 组件,避免白屏 PS Native 打开 H5如果 300ms 没有响应则需要 loading 组件,避免白屏
因为 H5 App 本身就有 Header 组件,站在前端框架层来说,需要确保业务代码是一致的,所有的差异需要在框架层做到透明化,简单来说 Header 的设计需要遵循: 因为 H5 App 本身就有 Header 组件,站在前端框架层来说,需要确保业务代码是一致的,所有的差异需要在框架层做到透明化,简单来说 Header 的设计需要遵循:
- H5 Header 组件与 Native 提供的 Header 组件使用调用层接口一致 - H5 Header 组件与 Native 提供的 Header 组件使用调用层接口一致
- 前端框架层根据环境判断选择应该使用 H5 的 Header 组件或 Native 的 Header 组件 - 前端框架层根据环境判断选择应该使用 H5 的 Header 组件或 Native 的 Header 组件
一般来说 Header 组件需要完成以下功能: 一般来说 Header 组件需要完成以下功能:

View File

@@ -4,7 +4,7 @@
## CADisplayLink 内存泄漏 ## CADisplayLink 内存泄漏
<img src="./../assets/CSDisplayLinkMemoryLeak.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CSDisplayLinkMemoryLeak.png" style="zoom:30%" />
可以看到 CADisplayLink 和 VCVC 和 CADisplayLink 互相持有,造成内存泄漏,没有释放。即使页面离开,定时器还在继续运行,不断打印。 可以看到 CADisplayLink 和 VCVC 和 CADisplayLink 互相持有,造成内存泄漏,没有释放。即使页面离开,定时器还在继续运行,不断打印。
@@ -18,11 +18,11 @@ NSTimer 的基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES blo
Demo 如下: Demo 如下:
<img src="./../assets/NSTimerMemoeryLeakDemo.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSTimerMemoeryLeakDemo.png" style="zoom:30%" />
但是当使用 `[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerTask) userInfo:nil repeats:NO];` repeats 为 NO 的时候,好像不会内存泄漏。这是为什么? 但是当使用 `[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerTask) userInfo:nil repeats:NO];` repeats 为 NO 的时候,好像不会内存泄漏。这是为什么?
<img src="./../assets/NSTimerMemoeryNotLeakWhenRepeatNO.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSTimerMemoeryNotLeakWhenRepeatNO.png" style="zoom:30%" />
@@ -194,7 +194,7 @@ Demo 如下:
### 改用 block 的方式替换 API不再持有 target ### 改用 block 的方式替换 API不再持有 target
<img src="./../assets/NSTimerFixMemoryLeakIssueByBlockAPI.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSTimerFixMemoryLeakIssueByBlockAPI.png" style="zoom:30%" />
该种方式,控制器 self强引用 timertimer 强引用 blockblock 弱引用 self3者没有形成环。 该种方式,控制器 self强引用 timertimer 强引用 blockblock 弱引用 self3者没有形成环。
@@ -202,7 +202,7 @@ Demo 如下:
### 采用系统 NSProxy 代替自定义的中间类 ### 采用系统 NSProxy 代替自定义的中间类
<img src="./../assets/NSTimerMemoryLeakFixedByNSProxy.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSTimerMemoryLeakFixedByNSProxy.png" style="zoom:30%" />
注意:继承自 NSProxy 的类,不能 init。 注意:继承自 NSProxy 的类,不能 init。
@@ -218,7 +218,7 @@ QA自己写的继承自 NSObject 的代理对象和继承自 NSProxy 的代
看一段神奇的代码 看一段神奇的代码
<img src="./../assets/NSProxyAndNSObjectMethodImpl.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSProxyAndNSObjectMethodImpl.png" style="zoom:30%" />
为什么打印出 `0 1` 为什么打印出 `0 1`
@@ -260,7 +260,7 @@ QA自己写的继承自 NSObject 的代理对象和继承自 NSProxy 的代
<img src="./../assets/RunLoop-SourceCode.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoop-SourceCode.png" style="zoom:30%" />
假设一个 NSTimer 被加到 RunLoop 开头NSTimer 执行周期为1sRunLoop 前面任务繁重,第一次走完一个完整的 RunLoop 需要0.4s,然后从头检测 NSTimer 有没有到时间,发现还没到继续执行 RunLoop 后续逻辑。后面遇到卡顿任务了,第二次 RunLoop 用了0.5s,然后从头检测 NSTimer 有没有到时间0.4+0.5还不到时间,继续跑,第三次 RunLoop 比较轻松耗时0.2s再判断定时器时间有没有到则此次已经0.4+0.5+0.2=1.1s了,此时 NSTimer 的事件被执行,此时精确度已经不够了(每次 RunLoop 的执行时间不固定) 假设一个 NSTimer 被加到 RunLoop 开头NSTimer 执行周期为1sRunLoop 前面任务繁重,第一次走完一个完整的 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 自定义 ### 打破循环引用NSTimer target 自定义
<img src="./../assets/NSTimerMemoryLeakFixedByProxy.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSTimerMemoryLeakFixedByProxy.png" style="zoom:30%" />

View File

@@ -8,7 +8,7 @@
Demo1创建 Person 类,点击事件里触发属性值的改变。 Demo1创建 Person 类,点击事件里触发属性值的改变。
<img src="./../assets/XCodoKVOIsaInspect.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XCodoKVOIsaInspect.png" style="zoom:25%">
@@ -20,7 +20,7 @@ Demo1创建 Person 类,点击事件里触发属性值的改变。
在内存中的结构如下图 在内存中的结构如下图
<img src="./../assets/UnusedKVOIsaStructure.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/UnusedKVOIsaStructure.png" style="zoom:25%">
整个流程分析下: 整个流程分析下:
@@ -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 监听失效 当我们按照 KVO 后动态生成的类名去创建一个新的类的时候Xcode 会报错:`[general] KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class`。因为自己创建的类名和系统将要动态创建的类名冲突了,并且 KVO 监听失效
<img src="./../assets/SelfClassNameConflictsWithKVOClass.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SelfClassNameConflictsWithKVOClass.png" style="zoom:25%">
Demo2: Demo2:
<img src="./../assets/KVOMethodImplAddressAndIsaInspect.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/KVOMethodImplAddressAndIsaInspect.png" style="zoom:25%">
分析: 分析:
@@ -48,7 +48,7 @@ Demo2:
Demo3: Demo3:
<img src="./../assets/KVOMethodImplAddressWithDouble.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/KVOMethodImplAddressWithDouble.png" style="zoom:25%">
可以看到我们将 KVO 的数据类型改为 double 后,原本的 setHeight 是 `_NSSetLongLongValueAndNotify`,现在是 `_NSSetDoubleValueAndNotify` 可以看到我们将 KVO 的数据类型改为 double 后,原本的 setHeight 是 `_NSSetLongLongValueAndNotify`,现在是 `_NSSetDoubleValueAndNotify`
@@ -58,11 +58,11 @@ Demo3:
### NSSet**ValueAndNotify 的内部实现 ### NSSet**ValueAndNotify 的内部实现
<img src="./../assets/UsedKVOIsaStructure.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/UsedKVOIsaStructure.png" style="zoom:25%">
来对 Person 类增加一些打印方法 来对 Person 类增加一些打印方法
<img src="./../assets/KVOKeyMethodPrint.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/KVOKeyMethodPrint.png" style="zoom:25%">
@@ -117,7 +117,7 @@ Demo3:
### 重写 class 方法 ### 重写 class 方法
<img src="./../assets/KVOOverrrideClassMethod.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/KVOOverrrideClassMethod.png" style="zoom:25%">
可以看到利用 runtime api在添加 KVO 之后,类对象为 `NSKVONotifying_Person` 可以看到利用 runtime api在添加 KVO 之后,类对象为 `NSKVONotifying_Person`
@@ -129,7 +129,7 @@ Demo3:
### KVO 类的所有方法 ### KVO 类的所有方法
<img src="./../assets/PrintKVOClassAllMethodName.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PrintKVOClassAllMethodName.png" style="zoom:25%">
@@ -148,7 +148,7 @@ QA为什么新创建的类没有 getter
### 修改成员变量的值可以触发 KVO 吗 ### 修改成员变量的值可以触发 KVO 吗
<img src="./../assets/KVOCannotInvokeWhenChangeIvarDirectly.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/KVOCannotInvokeWhenChangeIvarDirectly.png" style="zoom:25%">
我们将 Person 类的成员变量暴露出来,在点击事件里修改,发现不能触发 KVO。 我们将 Person 类的成员变量暴露出来,在点击事件里修改,发现不能触发 KVO。
@@ -158,7 +158,7 @@ QA如何手动触发
手动调用 `willChangeValueForKey`、 `didChangeValueForKey` 手动调用 `willChangeValueForKey`、 `didChangeValueForKey`
<img src="./../assets/KVOInvokeWhenChangeIvarDirectlyMustCallWillChangeAndDidChange.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/KVOInvokeWhenChangeIvarDirectlyMustCallWillChangeAndDidChange.png" style="zoom:25%">
@@ -197,7 +197,7 @@ QA如何手动触发 KVO
KVC 之后会触发 KVO 吗? KVC 之后会触发 KVO 吗?
<img src="./../assets/KVCWillTriggerKVO.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/KVCWillTriggerKVO.png" style="zoom:25%">
发现 KVC 触发了 KVO。 发现 KVC 触发了 KVO。
@@ -205,7 +205,7 @@ KVC 之后会触发 KVO 吗?
整个流程如下 整个流程如下
<img src="./../assets/KVC-process.png" style="zoom:45%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/KVC-process.png" style="zoom:45%">
`[self.person setValue:@10 forKey:@"age"]` 会先调用 `setKey:` 同名的方法,找不到则调用 `_setKey:` 的方法,如果还是找不到则调用 `+(BOOL)accessInstanceVariableDirectlt`,如果该方法返回 YES则可以直接修改成员变量的值会按照 `_key`、`_isKey`、`key`、`isKey` 的顺序寻找成员变量,如果找到则直接赋值,没找到则抛出异常 `NSUnknownKeyException` `[self.person setValue:@10 forKey:@"age"]` 会先调用 `setKey:` 同名的方法,找不到则调用 `_setKey:` 的方法,如果还是找不到则调用 `+(BOOL)accessInstanceVariableDirectlt`,如果该方法返回 YES则可以直接修改成员变量的值会按照 `_key`、`_isKey`、`key`、`isKey` 的顺序寻找成员变量,如果找到则直接赋值,没找到则抛出异常 `NSUnknownKeyException`
@@ -260,7 +260,7 @@ KVC 之后会触发 KVO 吗?
<img src="./../assets/KVC-get-process.png" style="zoom:45%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/KVC-get-process.png" style="zoom:45%">
@@ -425,7 +425,7 @@ Apple 文档告诉我们:被观察对象的 `isa指针` 会指向一个中间
- 键值观察通知依赖于 NSObject 的两个方法:`willChangeValueForKey:、didChangeValueForKey:` 。在一个被观察属性改变之前,调用 `willChangeValueForKey:` 记录旧的值。在属性值改变之后调用 `didChangeValueForKey:`,从而 `observeValueForKey:ofObject:change:context:` 也会被调用。 - 键值观察通知依赖于 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 类为止,系统会抛出几次机会给程序员补救,如果未做处理则奔溃 子类在继承父类对象,子类对象调用调方法的时候先看看当前子类中是否有方法实现,如果不存在方法则通过 isa 指针顺着继承链向上找到父类中是否有方法实现,如果父类种也不存在方法实现,则继续向上找...直到找到 NSObject 类为止,系统会抛出几次机会给程序员补救,如果未做处理则奔溃

View File

@@ -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
梳理后,如下图所示: 梳理后,如下图所示:
<img src="./../assets/AssociatedSaveValueInRuntimeStructure.png" style="zoom:45%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AssociatedSaveValueInRuntimeStructure.png" style="zoom:45%">
AssociationsManager 管理的 AssociationHashMap 结构如下: AssociationsManager 管理的 AssociationHashMap 结构如下:
@@ -2939,7 +2939,7 @@ NS_ASSUME_NONNULL_END
### 声明私有方法 ### 声明私有方法
<img src="./../assets/CategoryUsageDeclPrivateMethod.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CategoryUsageDeclPrivateMethod.png" style="zoom:30%" />

View File

@@ -13,6 +13,8 @@ MVC 模式下软件被划分为视图View用户界面、控制器
所有的通信都是单向的。 所有的通信都是单向的。
## MVP ## MVP
MVP 模式将 Controller 改名为 Presenter通信改变了通信方向 MVP 模式将 Controller 改名为 Presenter通信改变了通信方向
@@ -23,6 +25,10 @@ MVP 模式将 Controller 改名为 Presenter通信改变了通信方向
2. Model 与 View 不发生联系,都通过 Presenter 传递 2. Model 与 View 不发生联系,都通过 Presenter 传递
3. View 层非常薄。不部署任何业务逻辑称为“被动视图Passive View即没有任何主动性而 Presenter 非常厚,所有的逻辑都部署在这层 3. View 层非常薄。不部署任何业务逻辑称为“被动视图Passive View即没有任何主动性而 Presenter 非常厚,所有的逻辑都部署在这层
如果 Presenter 这一层很厚的话,可以继续拆,比如再增加一个 Interactor 的角色,专门处理处理 View 相关响应事件。这样子角色更多,职责也更清晰。维护也方便。
## MVVM ## MVVM
MVVM 模式将 Presenter 改名为 ViewModel基本上与 MVP 模式完全一致。 MVVM 模式将 Presenter 改名为 ViewModel基本上与 MVP 模式完全一致。
@@ -52,11 +58,14 @@ MVVM 就是 MVC 的增强版,将展示层逻辑单独拎出来,即 ViewModel
- MVVM 兼容当下的 MVC 机构 - MVVM 兼容当下的 MVC 机构
- MVVM 增加应用的可测试性 - MVVM 增加应用的可测试性
- MVVM 配合一个绑定机制效果最好 - MVVM 配合一个绑定机制效果最好
## 一个简单的例子 ## 一个简单的例子
PersonModel PersonModel
``` ```objective-c
@interface Person : NSObject @interface Person : NSObject
- (instancetype)initwithSalutation:(NSString *)salutation firstName:(NSString *)firstName lastName:(NSString *)lastName birthdate:(NSDate *)birthdate; - (instancetype)initwithSalutation:(NSString *)salutation firstName:(NSString *)firstName lastName:(NSString *)lastName birthdate:(NSDate *)birthdate;
@@ -69,7 +78,7 @@ PersonModel
@end @end
``` ```
PersonViewController PersonViewController
``` ```objective-c
- (void)viewDidLoad { - (void)viewDidLoad {
[super viewDidLoad]; [super viewDidLoad];
@@ -87,7 +96,7 @@ PersonViewController
上面是标准的 MVC。现在我们考虑用 ViewModel 增加改进下 上面是标准的 MVC。现在我们考虑用 ViewModel 增加改进下
PersonViewModel PersonViewModel
``` ```objective-c
@interface PersonViewModel : NSObject @interface PersonViewModel : NSObject
- (instancetype)initWithPerson:(Person *)person; - (instancetype)initWithPerson:(Person *)person;
@@ -124,17 +133,15 @@ PersonViewModel
``` ```
此时,我们的 ViewController 会很轻量 此时,我们的 ViewController 会很轻量
``` ```objective-c
- (void)viewDidLoad { - (void)viewDidLoad {
[super viewDidLoad]; [super viewDidLoad];
self.nameLabel.text = self.viewModel.nameText; self.nameLabel.text = self.viewModel.nameText;
self.birthdateLabel.text = self.viewModel.birthdateText; self.birthdateLabel.text = self.viewModel.birthdateText;
} }
``` ```

可测试View Controller 是出了名的难测试。因为传动的 VC 很重,在 MVVM 的世界里,代码各司其职,测试 View Controller 变得较为容易 可测试View Controller 是出了名的难测试。因为传动的 VC 很重,在 MVVM 的世界里,代码各司其职,测试 View Controller 变得较为容易
``` ```objective-c
SpecBegin(Person) SpecBegin(Person)
NSString *salutation = @"Dr."; NSString *salutation = @"Dr.";
NSString *firstName = @"first"; NSString *firstName = @"first";

View File

@@ -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 LaunchApp 在冷启动后,用户将 App 退后台。此阶段App 的进程还在系统中,用户重新启动进入 App 的过程,开发对该阶段能做的事情非常少。 - 热启动Warm LaunchApp 在冷启动后,用户将 App 退后台。此阶段App 的进程还在系统中,用户重新启动进入 App 的过程,开发对该阶段能做的事情非常少。
所以我们聊启动时间优化,通常是指冷启动。此外启动慢,也就是看到主页面的过程很慢,都是发生在主线程上的。 所以我们聊启动时间优化,通常是指冷启动。此外启动慢,也就是看到主页面的过程很慢,都是发生在主线程上的。
@@ -13,6 +13,8 @@
- 如果需要更详细的信息,那就将 `DYLD_PRINT_STATISTICS_DETAILS` 设置为1 - 如果需要更详细的信息,那就将 `DYLD_PRINT_STATISTICS_DETAILS` 设置为1
## 启动阶段划分 ## 启动阶段划分
App 冷启动可以划分为3大阶段 App 冷启动可以划分为3大阶段
@@ -37,9 +39,11 @@ main () {
} }
``` ```
## 第一阶段:进程创建到 main 函数执行dyld、runtime ## 第一阶段:进程创建到 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 函数的工作。 这一阶段主要包括 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依赖动态库列表
- fixupbind & rebase 的地址
- initializer-order初始化调用顺序
- optimizeObjc: Objective C 的元数据
- 其他main entry, uuid…
为什么闭包能提高启动速度呢?
这些信息是每次启动都需要的,把信息存储到一个缓存文件就能避免每次都解析,尤其是 Objective C 的运行时数据Class/Method...)解析非常慢
### Runtime ### Runtime
@@ -203,6 +244,8 @@ void _objc_init(void){
到此为止,可执行文件和动态库中所有的符号(ClassProtocolSelectorIMP…)都已经按格式成功加载到内存中,被 Runtime 所管理 到此为止,可执行文件和动态库中所有的符号(ClassProtocolSelectorIMP…)都已经按格式成功加载到内存中,被 Runtime 所管理
## 第二阶段main 函数到 didFinishLaunchingWithOptions ## 第二阶段main 函数到 didFinishLaunchingWithOptions
APP的启动由 dyld 主导将可执行文件加载到内存顺便加载所有依赖的动态库。并由Runtime 负责加载成 objc 定义的结构。所有初始化工作结束后dyld 就会调用 main 函数 APP的启动由 dyld 主导将可执行文件加载到内存顺便加载所有依赖的动态库。并由Runtime 负责加载成 objc 定义的结构。所有初始化工作结束后dyld 就会调用 main 函数
@@ -211,6 +254,8 @@ APP的启动由 dyld 主导,将可执行文件加载到内存,顺便加载
AppDelegate 的`application:didFinishLaunchingWithOptions:` 方法。此阶段主要是各种 SDK 的注册、初始化、APM 监控系统的启动、热修复 SDK 的设置、App 初始化数据的加载、IO 等等。 AppDelegate 的`application:didFinishLaunchingWithOptions:` 方法。此阶段主要是各种 SDK 的注册、初始化、APM 监控系统的启动、热修复 SDK 的设置、App 初始化数据的加载、IO 等等。
## 第三阶段:`didFinishLaunchingWithOptions` 到业务首页加载完成 ## 第三阶段:`didFinishLaunchingWithOptions` 到业务首页加载完成
这部分主要是首页渲染相关逻辑,不同的场景,首页的定义可能不一样。比如未登录的时候首页就是登陆页、否则就是某个主页 这部分主要是首页渲染相关逻辑,不同的场景,首页的定义可能不一样。比如未登录的时候首页就是登陆页、否则就是某个主页
@@ -219,13 +264,27 @@ AppDelegate 的`application:didFinishLaunchingWithOptions:` 方法。此阶段
- 渲染数据的计算 - 渲染数据的计算
## 启动优化 ## 启动优化
### 第一阶段 ### 第一阶段
#### dyld #### 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 #### Runtime
@@ -244,14 +303,24 @@ AppDelegate 的`application:didFinishLaunchingWithOptions:` 方法。此阶段
### 第二阶段 ### 第二阶段
- 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在 `application:didFinishLaunchingWithOptions` 方法中 - 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在 `application:didFinishLaunchingWithOptions` 方法中
- SDK 初始化遵循规范 - SDK 初始化遵循规范。什么时候注册、什么时候启动
- 任务启动器 - 任务启动器:其实就是按照一定的规则进行任务编排。将任务分类:
- 那些任务是需要在 App 启动完成前主线程同步执行的
- 那些任务是需要在 App 启动完成前主线程异步执行的
- 那些任务是需要在 App 启动完成后编排的
- 闲时主线程队列(监听 runloop 状态,`KCFRunLoopBeforeWaiting` 时执行,在 `KCFRunLoopAfterWaiting` 时停止)
- 异步串行队列
- 异步并行队列
- 闲时异步串行队列
-
- 二进制重排 - 二进制重排
- 方法耗时统计time profiler、os_signpost - 方法耗时统计time profiler、os_signpost
AppDelegate 中 `application:didFinishLaunchingWithOptions` 函数阶段一般有很多业务代码介入,大多数启动时间问题都是在此阶段造成的。 AppDelegate 中 `application:didFinishLaunchingWithOptions` 函数阶段一般有很多业务代码介入,大多数启动时间问题都是在此阶段造成的。
- 比如二方、三方 SDK 的注册、初始化。这其中有些 SDK 比如 APM 是有必要放在很早的阶段。有些则不需要,比如日志 SDK 的初始化 - 比如二方、三方 SDK 的注册、初始化。这其中有些 SDK 比如 APM 是有必要放在很早的阶段。有些则不需要,比如日志 SDK 的初始化
- 梳理业务,非必要的延迟加载、启动
### 第三阶段 ### 第三阶段
@@ -277,17 +346,39 @@ QA
Framework 只是打包方式,动态、静态库都可以支持。甚至可以存放资源文件。 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 不直接和物理内存打交道,而是通过 MMUMemory Manage Unit
iOS 程序在进行加载时,会根据一 page 大小16kb 将程序分割为多页启动时部分的页加载进真实内存部分页还在磁盘中中间的调度记录在一张内存映射表Page Table这个表用来调度磁盘和内存两者之间的数据交换。 iOS 程序在进行加载时,会根据一 page 大小16kb 将程序分割为多页启动时部分的页加载进真实内存部分页还在磁盘中中间的调度记录在一张内存映射表Page Table这个表用来调度磁盘和内存两者之间的数据交换。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/iOSPageInPageOut.png) <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/iOSPageInPageOut.png" style="zoom:40%" />
如上图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 耗时更长 因为磁盘访问速度较慢,所以 page in 比较耗时,而且 iOS 不仅仅是将数据加载到内存中,还要对这页做 Code Sign 签名认证,所以 iOS 耗时更长
@@ -309,7 +402,7 @@ TipsCode Sign 加密哈希并不少针对于整个文件,而是针对于
等到程序运行时用到了才去内存中寻找虚拟地址对应的页帧,找不到才进行分配,这就是内存的惰性(延时)分配机制。 等到程序运行时用到了才去内存中寻找虚拟地址对应的页帧,找不到才进行分配,这就是内存的惰性(延时)分配机制。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/PageFault.png) <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PageFault.png" style="zoom:50%" />
为了提高效率和方便管理对虚拟内存和物理内存进行分页Page管。进程在访问虚拟内存的一个 page 而对应的物理内存却不存在(没有被加载到物理内存中),则会触发一次缺页异常(缺页中断),然后分配物理内存,有需要的话会从磁盘 mmap 读入数据。 为了提高效率和方便管理对虚拟内存和物理内存进行分页Page管。进程在访问虚拟内存的一个 page 而对应的物理内存却不存在(没有被加载到物理内存中),则会触发一次缺页异常(缺页中断),然后分配物理内存,有需要的话会从磁盘 mmap 读入数据。
@@ -317,23 +410,50 @@ TipsCode Sign 加密哈希并不少针对于整个文件,而是针对于
二进制重排提升 App 启动速度是通过「解决内存缺页异常」(内存缺页会有几毫秒的耗时)来提速的。 二进制重排提升 App 启动速度是通过「解决内存缺页异常」(内存缺页会有几毫秒的耗时)来提速的。
一个 App 发生大量「内存缺页」的时机就是 App 刚启动的时候。所以优化手段就是「将影响 App 启动的方法集中处理,放到某一页或者某几页」(虚拟内存中的页)。
Xcode 工程允许开发者指定 「Order File」可以「按照文件中的方法顺序去加载」。
核心就是:二进制重排提升 App 启动速度是通过「解决内存缺页异常」(内存缺页会有几毫秒的耗时)来提速的。
一个 App 发生大量「内存缺页」的时机就是 App 刚启动的时候。所以优化手段就是「将影响 App 启动的方法集中处理放到某一页或者某几页」虚拟内存中的页。Xcode 工程允许开发者指定 「Order File」可以「按照文件中的方法顺序去加载」可以查看 linkMap 文件(需要在 Xcode 中的 「Buiild Settings」中设置 Order File、Write Link Map Files 参数)。 一个 App 发生大量「内存缺页」的时机就是 App 刚启动的时候。所以优化手段就是「将影响 App 启动的方法集中处理放到某一页或者某几页」虚拟内存中的页。Xcode 工程允许开发者指定 「Order File」可以「按照文件中的方法顺序去加载」可以查看 linkMap 文件(需要在 Xcode 中的 「Buiild Settings」中设置 Order File、Write Link Map Files 参数)。
### 如何获取启动阶段的 Page Fault 次数 ### 如何获取启动阶段的 Page Fault 次数
Instrucments 中的 System Trace 可以查看详细信息。 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。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeEnableLinkMap.png" style="zoom:30%" />
分析:
- 可以看到符号顺序是根据链接顺序来决定的
- 符号的链接顺序并非是 App 方法真正的执行顺序。
可以调整下 Person 类中2个方法的顺序也可以看到新生成的 linkMap 符号顺序变了。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LinkMapWithCompileOrder.png" style="zoom:45%" />
所以是有空间进行操作的,让符号链接顺序按照 App 启动阶段方法执行顺序来进行,这个抓手就是 `Order File`
### 有没有办法将 App 启动需要的方法集中收拢? ### 有没有办法将 App 启动需要的方法集中收拢?
其实二进制重排 Apple 自己本身就在用,查看 `objc4` 源码的时候就发现了身影 其实二进制重排 Apple 自己本身就在用,查看 `objc4` 源码的时候就发现了身影
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/Objc-OrderFile.png) <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Objc-OrderFile.png" style="zoom:40%" >
Xcode 使用的链接器为 `ld`。 ld 有个参数 `-order_file` 。order_file 中的符号会按照顺序排列在对应 section 的开始。 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减小缺页异常从而减小启动时间。 2. 如果你给 Xcode 工程根目录下指定一个 order 文件,比如 `refine.order`,则 Xcode 会按照指定的文件顺序进行二进制数据重排。分析 App 启动阶段,将优先需要加载的函数、方法,集中合并,利用 Order File减小缺页异常从而减小启动时间。
### 实践
第一步:编写 `.order` 文件。编写顺序是结合业务逻辑和代码顺序,然后再原始的 linkmap 文件中,将符号复制,写入到新创建的 `.order` 文件中。
第二步Build Settings -> Order File 中设置 `.order` 文件的位置。
第三步:编译,查看新的 linkmap 文件,验证符号编译顺序是否和 order file 一致。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LinkOrderWithOrderFile.png" style="zoom:30%" >
### 如何拿到启动时刻所调用的所有方法名称 ### 如何拿到启动时刻所调用的所有方法名称
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` - 在工程入口文件添加2个方法来解决编译报错问题 `__sanitizer_cov_trace_pc_guard_init`、`__sanitizer_cov_trace_pc_guard`
- clang 插桩原理就是给每个oc、c方法、block 等方法内部第一行添加 hook 代码,来实现 AOP 效果。所以在 `__sanitizer_cov_trace_pc_guard` 内部将函数的名称打印出来,最后可以统一写入 order 文件 - clang 插桩原理就是给每个oc、c方法、block 等方法内部第一行添加 hook 代码,来实现 AOP 效果。所以在 `__sanitizer_cov_trace_pc_guard` 内部将函数的名称打印出来,最后可以统一写入 order 文件
```objectivec <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClangCodeCoverageGenerateOrderFile.png" style="zoom:30%" />
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) { - 收集 App 启动过程中的函数调用,生成 `.order` 文件
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
printf("name: %s\n", info.dli_sname);
}
```
- 可能会存在 while、for 循环 case所以为了避免死循环需修改 "Other c Flags" 配置为 `-fsanitize-coverage=func,trace-pc-guard`。func 表示仅 hook 函数时调用 做了个封装,假设我们 App 启动完成的重点是 AppDelegate 的 `didFinishLaunchingWithOptions` 方法。在这里一行调用,便可获得 App 启动阶段的 `.order` 文件。
- 最后修改 Build Setting 中的 "Order File" 配置项 <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CollectAppLaunchMethod.png" style="zoom:30%" />
- 最后修改 Build Setting 中的 "Order File" 配置项,值为 `.order` 文件的路径信息。
完整 Demo 可以查看 [BlogDemos:BinarayOrderExplore](https://github.com/FantasticLBP/BlogDemos/tree/master/BinarayOrderExplore)

View File

@@ -1,25 +1,441 @@
# 守护你的App安全 # 守护你的App安全
App Crash 会严重影响用户体验Crash 率和客户端工程师的个人评级和绩效考核挂钩。因此写出的代码必须安全可靠。崩溃率要保持在一个什么样的水平以下 > 从 Web 安全一样,所有的攻防离不开一句话“在合理范围内保证 App 安全,让攻击者增加破解成本,让一部分人三思而后行战术性放弃”
App 在不断业务迭代,经过多人开发维护后可能因为开发者的水平层次不齐,造成代码质量较低,所以要实现的效果就是保证 App 稳健运行,常见的问题捕获处理,将造成奔溃或者 Crash 的因素处理掉,让 App 正常运行。
## 功能设想
对业务代码零侵入性地将原本会导致 App 奔溃的 crash 信息处理掉,保证 App 正常稳健运行,再将 Crash 信息提取出来呈现给开发者。开发者可以根据相应的 Crash 信息去处理解决对应的代码。
## ptrace 简易版本
在iOS系统中`ptrace` 被用于防止应用程序被调试。`ptrace` 函数提供了一种机制允许一个进程监听和控制另一个进程并且可以检测被控制进程的内存和寄存器中的数据。在iOS开发中`ptrace` 可以用于实现断点调试和系统调用跟踪,但它也常被用于反调试措施
通过传递 `PT_DENY_ATTACH` 标志,它允许应用程序设置一个标志,以防止其他调试器附加。如果其他调试器尝试附加,则进程将终止。
## KVO 可以使用类似下面的代码来防止别人破解、逆向。
```objective-c
#import <dlfcn.h>
__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 <LHMainHomePageNavigationBar 0x1035a05a0> for the key path "name" from <LHMainHomePageNavigationBar 0x1035a05a0> 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 ## 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 来禁止调试。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishhookCrackedPtrace.png" style="zoom:30%" />
结果:可以看到对 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`,如下所示。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ptraceSymbolBreakpoint.png" style="zoom:30%" />
我们可以看到 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
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/DLOpenPtrace.png" style="zoom:30%" />
工程运行后会发现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);
}
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PtraceSafeMethod.png" style="zoom:30%" />
该方式通过 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`,按位与用来判断是否是调试模式。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PTRACEDFlag.png" style="zoom:30%" >
使用
```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)` 结束进程。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SYSCTLKillInDebug.png" style="zoom:30%" >
为什么调用 `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 模式下,还是会输出 `不在调试模式`
结果如下:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishhookSysctlSymbol.png" style="zoom:30%" >
可以看到 fishhook 也可以 hook sysctl。所以不安全。
## sysctl 安全版本
修改思路参考上面的 ptrace知道 fishhook 的原理,绕开懒加载符号表,绕开 dyld 修正符号和填充地址这个过程。
不再赘述,核心代码如下图所示:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SysctlAntiFishHook.png" style="zoom:30%" >
效果就是在 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 效果的。骚操作来还原符号,保证安全。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/HidePtraceAndSysctlSymbol.png" style="zoom:30%" >
### 利用汇编调用系统函数
函数调用都可以利用 `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);
}
```
最后的效果:
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AntiDebugViaAssemblyAndPtrace.png" style="zoom:30%" >
完整代码可以这里:
- [AppHook](https://github.com/FantasticLBP/BlogDemos/tree/master/AppHook)
- [AppHookProtector](https://github.com/FantasticLBP/BlogDemos/tree/master/AppHookProtector)

View File

@@ -12,7 +12,7 @@ BSS段bss segment通常用来存储程序中未被初始化的全局变
代码段code 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++ 的继承实现的。 因此可以知道OC 的类底层是由 c/c++ 的继承实现的。
<img src="./../assets/OCObjectLayoutWhenISA.png" style="zoom:45%"> <img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/OCObjectLayoutWhenISA.png" style="zoom:45%">
由于 obj 对象没有任何属性和方法,只有一个 isa 指针且类的本质就是结构体所以当结构体只有1个成员时该成员的地址值就是该结构体的地址。 由于 obj 对象没有任何属性和方法,只有一个 isa 指针且类的本质就是结构体所以当结构体只有1个成员时该成员的地址值就是该结构体的地址。
@@ -118,7 +118,7 @@ struct Student_IMPL {
类的本质是结构体,结构体成员内存紧挨着。内存布局如图所示: 类的本质是结构体,结构体成员内存紧挨着。内存布局如图所示:
<img src="./../assets/StudentClassLayout.png" style="zoom:45%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StudentClassLayout.png" style="zoom:45%">
@@ -126,7 +126,7 @@ struct Student_IMPL {
如果上述结论正确,那是不是可以声明一个 ` Student_IMPL` 类型的结构体指针,指向 st 指针指向的对象。然后通过结构体指针访问成员变量,看看取值是不是正确的 如果上述结论正确,那是不是可以声明一个 ` Student_IMPL` 类型的结构体指针,指向 st 指针指向的对象。然后通过结构体指针访问成员变量,看看取值是不是正确的
<img src="./../assets/StructPointerVistorClassIvars.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StructPointerVistorClassIvars.png" style="zoom:25%">
发现是可以正确访问的。 发现是可以正确访问的。
@@ -193,7 +193,7 @@ struct Student_IMPL {
为什么 `class_getInstanceSize([Person class])` 也是16不是8+4吗因为存在内存对齐结构体的大小必须是最大成员大小的倍数Person 中也就是8的倍数 为什么 `class_getInstanceSize([Person class])` 也是16不是8+4吗因为存在内存对齐结构体的大小必须是最大成员大小的倍数Person 中也就是8的倍数
<img src="./../assets/StudentClassExtendsFromPersonClass.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StudentClassExtendsFromPersonClass.png" style="zoom:25%">
@@ -330,12 +330,12 @@ int main(int argc, const char * argv[]) {
`Person *p1 = [Person new];` 这句代码在内存分配原理如下图所示 `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) ![p1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2017-05-15%20下午5.35.17.png)
![p2](./../assets/2017-05-15%20下午5.35.34.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的值相同。** **可以 看到Person类的3个对象p1、p2、p3的isa的值相同。**
@@ -707,7 +707,7 @@ iOS 中系统分配内存都是16的倍数。pageSize系统在分配内
GUN 都存在内存对齐这个概念。 GUN 都存在内存对齐这个概念。
`sizeof` 本质是运算符。在 Xcode 编译后就替换为真正的值。通过指令 `xcrun --sdk iphoneos clang -arch arm64 -S -emit-llvm ViewController.m -o ViewController.ll` 查看 IR。 `sizeof` 本质是运算符。在 Xcode 编译后就替换为真正的值。通过指令 `xcrun --sdk iphoneos clang -arch arm64 -S -emit-llvm ViewController.m -o ViewController.ll` 查看 IR。
<img src="./../assets/XcoedeViewSizeOfViaAssembly.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcoedeViewSizeOfViaAssembly.png" style="zoom:25%">
@@ -718,21 +718,21 @@ GUN 都存在内存对齐这个概念。
`objc_getClass()` 如果传递 instance 实例对象,返回 class 类对象;传递 Class 类对象,返回 meta-class 元类对象;传递 meta-class 元对象,则返回 NSObject(基类)的 meta-class 对象 `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最后找到对象方法的实现进行调用 instance 的 isa 指向 Class。当调用方法时通过 instance 的 isa 找到 Class最后找到对象方法的实现进行调用
class 的 isa 指向 meta-class。当调用类方法的时通过 class 的 isa 找到 meta-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 类对象,在类对象的对象方法列表中找到方法实现并调用。 当 Student 实例对象调用 Person 的对象方法时,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,在类对象的对象方法列表中找到方法实现并调用。
当 Stduent 实例对象调用 init 方法时候,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,找到 Person Claas 的 superClass 到 NSObject 类对象,在 NSObject 类对象的方法列表中找到 `init` 方法并调用。 当 Stduent 实例对象调用 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 的元类对象。最后找到元类对象的方法列表,调用到对象方法。 当 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 ```objectivec
@interface Student : NSObject @interface Student : NSObject
@@ -937,7 +937,7 @@ class_rw_t *personMetaClassData = personClass->metaClass()->data();
内存对齐是指数据在内存中存储时按照一定规则对齐到特定的地址上。在 iOS 开发中,内存对齐是为了提高内存访问的效率和性能。内存对齐的原因主要包括以下几点: 内存对齐是指数据在内存中存储时按照一定规则对齐到特定的地址上。在 iOS 开发中,内存对齐是为了提高内存访问的效率和性能。内存对齐的原因主要包括以下几点:
1. 提高访问速度内存对齐可以使数据在内存中的存储更加高效因为大部分计算机体系结构都要求数据按照特定的边界对齐这样可以减少内存访问的次数提高访问速度。CPU访问非对齐的内存时需要进行多次拼接。 1. 提高访问速度内存对齐可以使数据在内存中的存储更加高效因为大部分计算机体系结构都要求数据按照特定的边界对齐这样可以减少内存访问的次数提高访问速度。CPU访问非对齐的内存时需要进行多次拼接。
如下图,比如需要读取从[2, 5]的内存,需要分别读取两次,然后还需要做位移的运算,最后才能得到需要的数据。这中间的损耗就会影响访问速度。 如下图,比如需要读取从[2, 5]的内存,需要分别读取两次,然后还需要做位移的运算,最后才能得到需要的数据。这中间的损耗就会影响访问速度。
<img src="./../assets/MemoryAlignReason.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MemoryAlignReason.png" style="zoom:30%">
2. 为了方便移植。CPU是一块块的进行进行内存访问。有一些硬件平台不允许随机访问只能访问对齐后的内存地址否则会报异常。 2. 为了方便移植。CPU是一块块的进行进行内存访问。有一些硬件平台不允许随机访问只能访问对齐后的内存地址否则会报异常。
很多 CPU如基于 AlphaIA-64MIPS和 SuperH 体系的)拒绝读取未对齐数据。当一个程序要求这些 CPU 读取未对齐数据时,这时 CPU 会进入异常处理状态并且通知程序不能继续执行。举个例子,在 ARMMIPS和 SH 硬件平台上,当操作系统被要求存取一个未对齐数据时会默认给应用程序抛出硬件异常。 很多 CPU如基于 AlphaIA-64MIPS和 SuperH 体系的)拒绝读取未对齐数据。当一个程序要求这些 CPU 读取未对齐数据时,这时 CPU 会进入异常处理状态并且通知程序不能继续执行。举个例子,在 ARMMIPS和 SH 硬件平台上,当操作系统被要求存取一个未对齐数据时会默认给应用程序抛出硬件异常。
@@ -1017,7 +1017,7 @@ NSLog(@"%zd", malloc_size(temp));
成员变量占用8字节对齐每个对象的第一个都是 isa 指针必须要占用8字节。举例一个极端 case假设 n 个对象,其中 m 个对象没有成员变量,只有 isa 指针占用8字节其中的 n-m个对象既有 isa 指针,又有成员变量。每个类交错排列,那么 CPU 在访问对象的时候会耗费大量时间去识别具体的对象。很多时候会取舍,这个 case 就是时间换空间。以16字节对齐会加快访问速度参考链表和数组的设计 成员变量占用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 中查看 上述是 Apple 官方的角度出发探究的,其他系统,比如 Linux 也是存在内存对齐的。由于 Linux 也是采用 GNU 的东西,所以探索下 GNU 下 glibc malloc 的实现。从[这里](https://ftp.gnu.org/gnu/glibc/)下载 glibc 源码。然后拖到 Xcode 中查看
<img src="./../assets/GlibcInXcodeProject.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/GlibcInXcodeProject.png" style="zoom:25%">
可以看到 GNU 源码里面,内存对齐 `MALLOC_ALIGNMENT` 可以看到 GNU 源码里面,内存对齐 `MALLOC_ALIGNMENT`
在 i386 里面是16在非 i386 里面有个判断 在 i386 里面是16在非 i386 里面有个判断
@@ -1031,7 +1031,7 @@ NSLog(@"%zd", malloc_size(temp));
# define INTERNAL_SIZE_T size_t # define INTERNAL_SIZE_T size_t
``` ```
在 Xcode 打印输出, `__alignof__ (long double)` 为16`sizeof(size_t)` 为8即 `2 * SIZE_SZ` = 16所以不管怎么看在 GUN 里面内存对齐一定都是16. 在 Xcode 打印输出, `__alignof__ (long double)` 为16`sizeof(size_t)` 为8即 `2 * SIZE_SZ` = 16所以不管怎么看在 GUN 里面内存对齐一定都是16.
<img src="./../assets/GLibcMallocAlignment.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/GLibcMallocAlignment.png" style="zoom:25%">
Todo: 研究探索 libmalloc 源码 Todo: 研究探索 libmalloc 源码

View File

@@ -22,11 +22,11 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
### 1. 屏幕绘制原理 ### 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 信号产生的频率。虽然现在的显示器基本都是液晶显示屏,但是原理保持不变。 讲讲老式的 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 根据工程师写的代码计算好需要现实的内容(比如视图创建、布局计算、图片解码、文本绘制等),然后把计算结果提交到 GPUGPU 负责图层合成、纹理渲染,随后 GPU 将渲染结果提交到帧缓冲区。随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过数模转换传递给显示器显示。 通常,屏幕上一张画面的显示是由 CPU、GPU 和显示器是按照上图的方式协同工作的。CPU 根据工程师写的代码计算好需要现实的内容(比如视图创建、布局计算、图片解码、文本绘制等),然后把计算结果提交到 GPUGPU 负责图层合成、纹理渲染,随后 GPU 将渲染结果提交到帧缓冲区。随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过数模转换传递给显示器显示。
@@ -36,7 +36,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
为了解决这个问题GPU 通常有一个机制叫垂直同步信号V-Sync当开启垂直同步信号后GPU 会等到视频控制器发送 V-Sync 信号后,才进行新的一帧的渲染和帧缓冲区的更新。这样的几个机制解决了画面撕裂的情况,也增加了画面流畅度。但需要更多的计算资源 为了解决这个问题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 信号后,读取第二个帧缓冲区的内容(视频控制器的指针指向第二个帧缓冲区),并开始第三帧图像的渲染并送入第一个帧缓冲区,依次不断循环往复。 当第一次 V-Sync 信号到来时,先渲染好一帧图像放到帧缓冲区,但是不展示,当收到第二个 V-Sync 信号后读取第一次渲染好的结果(视频控制器的指针指向第一个帧缓冲区),并同时渲染新的一帧图像并将结果存入第二个帧缓冲区,等收到第三个 V-Sync 信号后,读取第二个帧缓冲区的内容(视频控制器的指针指向第二个帧缓冲区),并开始第三帧图像的渲染并送入第一个帧缓冲区,依次不断循环往复。
@@ -56,7 +56,7 @@ _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_di
### 2. 卡顿产生的原因 ### 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 等机制通知 AppApp 主线程开始在 CPU 中计算显示内容(视图创建、布局计算、图片解码、文本绘制等)。然后将计算的内容提交到 GPUGPU 经过图层的变换、合成、渲染,随后 GPU 把渲染结果提交到帧缓冲区,等待下一次 VSync 信号到来再显示之前渲染好的结果。在垂直同步机制的情况下,如果在一个 VSync 时间周期内CPU 或者 GPU 没有完成内容的提交,就会造成该帧的丢弃(也叫掉帧),等待下一次机会再显示,这时候屏幕上还是之前渲染的图像,所以这就是 CPU、GPU 层面界面卡顿的原因。 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 AppApp 主线程开始在 CPU 中计算显示内容(视图创建、布局计算、图片解码、文本绘制等)。然后将计算的内容提交到 GPUGPU 经过图层的变换、合成、渲染,随后 GPU 把渲染结果提交到帧缓冲区,等待下一次 VSync 信号到来再显示之前渲染好的结果。在垂直同步机制的情况下,如果在一个 VSync 时间周期内CPU 或者 GPU 没有完成内容的提交,就会造成该帧的丢弃(也叫掉帧),等待下一次机会再显示,这时候屏幕上还是之前渲染的图像,所以这就是 CPU、GPU 层面界面卡顿的原因。
@@ -75,7 +75,7 @@ RunLoop 负责监听输入源进行调度处理。比如网络、输入设备、
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)
第一步:通知 ObserversRunLoop 要开始进入 loop紧接着进入 loop 第一步:通知 ObserversRunLoop 要开始进入 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 个状态 RunLoop 6 个状态
```Objective-C ```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 则代表超时阻塞了主线程。 通过 `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 状态那么多,为什么选择 KCFRunLoopBeforeSources 和 KCFRunLoopAfterWaiting因为大部分卡顿都是在 KCFRunLoopBeforeSources 和 KCFRunLoopAfterWaiting 之间。比如 Source0 类型的 App 内部事件等
Runloop 检测卡顿流程图如下: 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” 的一张图和例子,如下 维基百科搜索到 “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` 函数,用绿色部分表示。 上图表示为一个栈。分为若干个栈帧Frame每个栈帧对应一个函数调用。下面蓝色部分表示 `DrawSquare` 函数,它在执行的过程中调用了 `DrawLine` 函数,用绿色部分表示。
可以看到栈帧由三部分组成:函数参数、返回地址、局部变量。比如在 DrawSquare 内部调用了 DrawLine 函数:第一先把 DrawLine 函数需要的参数入栈;第二把返回地址(控制信息。举例:函数 A 内调用函数 B调用函数 B 的下一行代码的地址就是返回地址)入栈;第三函数内部的局部变量也在该栈中存储。 可以看到栈帧由三部分组成:函数参数、返回地址、局部变量。比如在 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) <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/callStackSymbolicate.png" style="zoom:30%" />
测试过单次抓取主线程符号耗时大概1ms左右。因此连续抓取堆栈是可取方案。 测试过单次抓取主线程符号耗时大概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 的指令进行符号化,如下效果。 上传这些信息到服务端后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也就是刷机所需要的固件信息。 系统堆栈符号化的问题,也就是获取系统符号文件的问题。可以通过 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。 找到最接近栈顶的符合占比条件的业务代码方法,作为卡顿堆栈归类 key。
@@ -551,7 +551,7 @@ curNode.costTime > (parentNode.costTime * 1.1/n) && curNode.costTime > parentNod
应用启动时间是影响用户体验的重要因素之一,所以我们需要量化去衡量一个 App 的启动速度到底有多快。启动分为冷启动和热启动。 应用启动时间是影响用户体验的重要因素之一,所以我们需要量化去衡量一个 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 的基础初始化工作。 冷启动App 尚未运行,必须加载并构建整个应用。完成应用的初始化。冷启动存在较大优化空间。冷启动时间从 `application: didFinishLaunchingWithOptions:` 方法开始计算App 一般在这里进行各种 SDK 和 App 的基础初始化工作。
@@ -586,10 +586,10 @@ App 启动过程:
- 程序执行:调用 main();调用 UIApplicationMain();调用 applicationWillFinishLaunching() - 程序执行:调用 main();调用 UIApplicationMain();调用 applicationWillFinishLaunching()
Pre-Main 阶段 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 阶段
![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 #### 2.1 加载 Dylib
@@ -673,9 +673,9 @@ Main 阶段
### 4. 精确版启动时间监控 ### 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 可以拿到 进程创建:通过 sysctl 可以拿到
@@ -685,7 +685,7 @@ didFinishLaunching通过 UIApplicationDidFinishLaunchingNotification 拿到
首屏渲染时间怎么拿?能获取到 `CA::Transaction::commit()` 时刻即可。 首屏渲染时间怎么拿?能获取到 `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。 对 UI 进行修改后会在主线程休眠前触发 `CA::Transaction::commit()`,其实就是打包 Render Tree通过 IPC 发送给 Render Server。
@@ -697,21 +697,21 @@ didFinishLaunching通过 UIApplicationDidFinishLaunchingNotification 拿到
- Layout 布局:调用 `layout` 等与布局相关的 API - 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` 等与绘制相关方法 - 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图片解码 - Prepare图片解码
- Commit递归打包 Render Tree通过 IPC 计数发送给 Render Server - Commit递归打包 Render Tree通过 IPC 计数发送给 Render Server
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CoreAnimationPipeline4.png) ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline4.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CoreAnimationPipeline5.png) ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline5.png)
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CoreAnimationPipeline6.png) ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationPipeline6.png)
断点调试完,`[BSXPCServiceConnectionProxy invokeMethod:onTarget:withMessage:forConnection:]` 底层调用的是 send 方法。send 方法调用 `BSXPCServiceConnectionMessageReply send` 方法。 断点调试完,`[BSXPCServiceConnectionProxy invokeMethod:onTarget:withMessage:forConnection:]` 底层调用的是 send 方法。send 方法调用 `BSXPCServiceConnectionMessageReply send` 方法。
@@ -754,6 +754,26 @@ didFinishLaunching通过 UIApplicationDidFinishLaunchingNotification 拿到
开始时间点:一般需要 Native 配合,容器页面创建好就是开始时间点。比如 iOS 的 VC `viewDidLoad`、Android 的 `onActivityCreated` 开始时间点:一般需要 Native 配合,容器页面创建好就是开始时间点。比如 iOS 的 VC `viewDidLoad`、Android 的 `onActivityCreated`
结束时间点:结束时间一般在跨端侧,比如 RN 中的组件挂载完成 componentDidMount 回调的时刻。 结束时间点:结束时间一般在跨端侧,比如 RN 中的组件挂载完成 componentDidMount 回调的时刻。
### 6. 工单跟进
数据采集上报后,产生工单,自动分配到人、通知负责人和对应的群。
这些在其他篇章会讲。比如启动时间的工单信息类似:
| 阶段 | 耗时 | 方法 | 业务 | 负责人 | 操作 |
| ---- | ---- | ----------------------------- | ----- | ------ | -------------- |
| T1 | 30ms | [Appdeledate handleDBUpgrade] | Goods | @张三 | 更改状态、详情 |
## 三、 CPU 使用率监控 ## 三、 CPU 使用率监控
### 1. CPU 架构 ### 1. CPU 架构
@@ -899,14 +919,14 @@ App 内存不足时,系统会按照一定策略来腾出更多的空间供使
Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能一个 page 持有多个对象,也可能一个大的对象跨越多个 page。通常它是 16KB 大小,且有 3 种类型的 page。 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
Clean memory 包括 3 类:可以 `page out` 的内存、内存映射文件、App 使用到的 framework每个 framework 都有 \_DATA_CONST 段,通常都是 clean 状态,但使用 runtime swizling那么变为 dirty Clean memory 包括 3 类:可以 `page out` 的内存、内存映射文件、App 使用到的 framework每个 framework 都有 \_DATA_CONST 段,通常都是 clean 状态,但使用 runtime swizling那么变为 dirty
一开始分配的 page 都是干净的(堆里面的对象分配除外),我们 App 数据写入时候变为 dirty。从硬盘读进内存的文件也是只读的、clean page。 一开始分配的 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 - Dirty Memory
@@ -914,7 +934,7 @@ Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能
在使用 framework 的过程中会产生 Dirty memory使用单例或者全局初始化方法有助于帮助减少 Dirty memory因为单例一旦创建就不销毁一直在内存中系统不认为是 Dirty memory 在使用 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 - Compressed Memory
@@ -925,7 +945,7 @@ Memory page\*\* 是内存管理中的最小单位,是系统分配的,可能
App 运行内存 = pageNumbers \* pageSize。因为 Compressed Memory 属于 Dirty memory。所以 Memory footprint = dirtySize + CompressedSize App 运行内存 = pageNumbers \* pageSize。因为 Compressed Memory 属于 Dirty memory。所以 Memory footprint = dirtySize + CompressedSize
设备不同内存占用上限不同App 上限较高extension 上限较低,超过上限 crash 到 `EXC_RESOURCE_EXCEPTION`。 设备不同内存占用上限不同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 因为占用内存过大而被强杀。 接下来谈一下如何获取内存上限,以及如何监控 App 因为占用内存过大而被强杀。
@@ -1720,7 +1740,7 @@ static int jetsam_do_kill(proc_t p, int jetsam_flags, os_reason_t jetsam_reason)
FacekBook 提出排除法监控 OOM。 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 更新了版本 - 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) 全部的讲解可以看[这里](<(https://mp.weixin.qq.com/s/4-4M9E8NziAgshlwB7Sc6g)>)。对于 Memory Graph 的实现细节感兴趣的可以看这篇[文章](https://juejin.cn/post/6895583288451465230)
## 五、野指针/内存泄漏 ## 五、野指针/内存泄漏
### 1. 概念定义 ### 1. 概念定义
1.内存泄漏会导致 OOM什么是内存泄漏? #### 内存泄漏会导致 OOM那什么是内存泄漏
定义:程序中已经动态分配的堆内存,由于某些原因,导致程序无法释放或者未释放内存,造成系统内存的浪费,导致程序的运行速度减慢甚至是系统奔溃等严重后果。 定义:程序中已经动态分配的堆内存,由于某些原因,导致程序无法释放或者未释放内存,造成系统内存的浪费,导致程序的运行速度减慢甚至是系统奔溃等严重后果。
一般来说导致内存泄漏的原因:对象没有释放、循环引用(无法释放) 一般来说导致内存泄漏的原因:对象没有释放Core Foundation 对象需要手动调用 release 方法)、循环引用
2.什么是野指针? #### 什么是野指针?
C 语言中声明一个指针变量但是没有初始化。任何指针变量刚被创建时不会自动成为NULL指针它的缺省值是随机的指向一个随机的内存空间。所以指针变量在创建的同时应当被初始化要么将指针设置为NULL要么让它指向合法的内存 C 语言中声明一个指针变量但是没有初始化。任何指针变量刚被创建时不会自动成为NULL指针它的缺省值是随机的指向一个随机的内存空间。所以指针变量在创建的同时应当被初始化要么将指针设置为NULL要么让它指向合法的内存
OC 语言:指针指向的内存对象已经被释放或回收了,但是指针没有置为 nil还是指向已经回收的内存空间。 OC 语言:指针指向的内存对象已经被释放或回收了,但是指针没有置为 nil还是指向已经回收的内存空间。
3.什么是空指针? #### 什么是空指针?
空指针就是没有存储任何内存地址(也就是没有指向任何对象的指针),即 nil、Nil、NULL、NSNULL、0 空指针就是没有存储任何内存地址(也就是没有指向任何对象的指针),即 nil、Nil、NULL、NSNULL、0
@@ -2098,17 +2120,20 @@ OC 语言:指针指向的内存对象已经被释放或回收了,但是指
- NSNull数值类的空对象 - NSNull数值类的空对象
4.内存回收的本质 #### 内存回收的本质
申请一块内存空间,其本质是向系统申请一块别人不再使用的内存空间,即已经回收的内存空间 申请一块内存空间,其本质是向系统申请一块别人不再使用的内存空间,即已经回收的内存空间
释放一块内存空间,其本质是这个内存空间不再使用,可以由系统分配给别的对象使用。此时内存空间虽然回收了,但是原本的数据依赖的是存在的,可以理解为垃圾数据。 释放一块内存空间,其本质是这个内存空间不再使用,可以由系统分配给别的对象使用。此时内存空间虽然回收了,但是原本的数据依赖的是存在的,可以理解为垃圾数据。
5.什么是僵尸对象 - OC 对象释放后,内存回收,表示这一块内存可以分配给别的对象
- 这块内存在分配给别的对象之前,仍然保留着已经释放对象的数据
#### 什么是僵尸对象?
僵尸对象就是指一个 OC 对象释放后所占用的内存还没被复写(重新分配给其他对象)前被称为僵尸对象。此时僵尸对象内存很不稳定,内存随时可能被系统分配给其他对象所使用。所以此时僵尸对象不应该访问和使用(调用对象的方法等) 僵尸对象就是指一个 OC 对象释放后所占用的内存还没被复写(重新分配给其他对象)前被称为僵尸对象。此时僵尸对象内存很不稳定,内存随时可能被系统分配给其他对象所使用。所以此时僵尸对象不应该访问和使用(调用对象的方法等)
6.为什么 OC 野指针 Crash 很多? #### 为什么 OC 野指针 Crash 很多?
App 上线前经过自测、UI 自动化、精准测试、高铁回归包测试等,但是野指针具有随机性,所以很多野指针偷偷跑到线上了。 App 上线前经过自测、UI 自动化、精准测试、高铁回归包测试等,但是野指针具有随机性,所以很多野指针偷偷跑到线上了。
@@ -2116,19 +2141,29 @@ App 上线前经过自测、UI 自动化、精准测试、高铁回归包测试
- 出错分支比较难进,执行不到出错的 case所以能做的就是提高测试覆盖率 - 出错分支比较难进,执行不到出错的 case所以能做的就是提高测试覆盖率
- 即使跑进有问题的分支,但是野指针指向的地址并不一定会导致 crash。因为野指针本质是一个指向已删除的对象或受限内存区域的指针。这里 OC 野指针指的是 OC 对象释放后但指针未置空导致的野指针。dealloc 执行后只是告诉系统这篇内存我不用,但是系统并没有让这块内存不能访问。 - 即使跑进有问题的分支,但是野指针指向的地址并不一定会导致 crash。因为野指针本质是一个指向已删除的对象或受限内存区域的指针。这里 OC 野指针指的是 OC 对象释放后但指针未置空导致的野指针。dealloc 执行后只是告诉系统这篇内存我不用,但是系统并没有让这块内存不能访问。访问的话,暂时是安全的,过了一会儿,由于内存紧张,可能被系统分配到其他对象去了。被填充了一个新的对象的信息。比如成员变量、方法等。再去访问就会 crash
### 2. Zombie Objects #### 野指针可能存在的问题
Zoom Object 是 Xcode 提供的一种用来检测内存问题的对象EXC_BAD_ACCESS它可以捕获任何尝试访问坏内存的调用。 <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WildPointerCategory.png" style="zoom:30%" />
### 2. Zombie Object
`Zombie Object` 是 Xcode 提供的一种用来检测内存问题的工具(`EXC_BAD_ACCESS`),它可以捕获任何尝试访问坏内存的调用。
一个对象解除了它的引用,已经被释放掉,但仍可以接收消息,就叫 zombie object 。
如果给僵尸对象发送消息,那么系统会在运行期间奔溃并在 Xcode 输出错误日志,通过日志定位到野指针对象调用的方法和类名。 如果给僵尸对象发送消息,那么系统会在运行期间奔溃并在 Xcode 输出错误日志,通过日志定位到野指针对象调用的方法和类名。
当野指针指向的僵尸对象所占用的内存还未被复写(未分配),此时是可以访问的 - 当野指针指向的僵尸对象所占用的内存还未被复写(未分配),此时是可以访问的
当野指针指向的僵尸对象所占用的内存被分配后,此时访问会奔溃,比如 objc_msgSend 失败、SIGBUS、SIGFPE、SIGILL 等异常。 - 当野指针指向的僵尸对象所占用的内存被分配后,此时访问会奔溃,比如 objc_msgSend 失败、SIGBUS、SIGFPE、SIGILL 等异常。
### 3. 探索 Xcode 如何实现僵尸对象检测的原理 开启僵尸对象时,不回收真正内存,而是将其 isa 变为僵尸对象,僵尸对象在内存中无法复用,所以偶现的 crash 变为必现 crash
### 3. 探索 Xcode Zombie Object 实现原理
Xcode 默认设置不检查指针指向的对象是否是僵尸对象。为什么这么设计?因为开启会影响开发效率,所以线上就会报错。 Xcode 默认设置不检查指针指向的对象是否是僵尸对象。为什么这么设计?因为开启会影响开发效率,所以线上就会报错。
@@ -2145,31 +2180,25 @@ DemoMRC + Xcode 开启 Zombie Object
printClassInfo(person); printClassInfo(person);
    [person description];     [person description];
} }
2022-05-14 01:35:54.521783+0800 DDD[79672:3001452] self:Person - superClass:NSObject // console
2022-05-14 01:35:54.522115+0800 DDD[79672:3001452] self:_NSZombie_Person - superClass:nil self:Person - superClass:NSObject
2022-05-14 01:35:54.523292+0800 DDD[79672:3001452] *** -[Person description]: message sent to deallocated instance 0x6000024f1030 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 利用 Instruments 下的 Zombies 工具检测,看看野指针的情况,系统是怎么捕获的。
// Replaced by NSZombies
- (void)dealloc {
_objc_rootDealloc(self);
}
```
objc 源码中 dealloc 方法注释说了 dealloc 的实现会被僵尸对象所替换。 <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/InstrumentZombiesCaptureZombieObject.png" style="zoom:30%" />
开启 Instrucments 分析查看得到,调用了 `__dealloc_zombie` 方法。底层到底做了什么?加个符号断点查看下 切换到 `Call Trees` 模式下可以看到系调用了 `_dealloc_zombie` 方法。底层到底做了什么?加个符号断点查看下
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeZombieDetect.png) <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeZombieDetect.png" style="zoom:30%" />
通过符号名称大概可以才到系统会调用 dealloc 方法时,类似 KVO派生出一个新的僵尸类,将当前类的 isa 指针指向僵尸类。 通过符号名称大概可以系统会调用 dealloc 方法时,类似 KVO派生出一个新的僵尸类将当前类的 isa 指针指向僵尸类。
查看 Runtime 源码 查看 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 文件) 另一方面,从 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` 来奔溃 可以从 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 ### 4. Malloc Scribble
申请内存 alloc 时在内存上填 `0xAA`,释放内存 dealloc 时在内存上填 `0x55`。此种方案也会让内存泄漏偶现的 crash 变为必现。 申请内存 alloc 时在内存上填 `0xAA`,释放内存 dealloc 时在内存上填 `0x55`。此种方案也会让内存泄漏偶现的 crash 变为必现。
### 5. 野指针监控工具<a name="zombieSniffer"></a> ### 5. 野指针监控工具<a name="zombieSniffer"></a>
##### 类 Zombie Object 方案 ##### 类 Zombie Object 方案
@@ -2384,13 +2432,16 @@ static void GSLogZombie(id o, SEL sel){
其中的僵尸对象检测做了这么几件事: 其中的僵尸对象检测做了这么几件事:
- 开启僵尸对象时,不回收真正内存,而是将其 isa 变为僵尸对象,僵尸对象在内存中无法复用,所以偶现的 crash 变为必现 crash - 获取有问题内存对象的类名
- 字符串拼接,类似 `_NSZombie_${ClassName}` 的形式
- 僵尸类可以响应任何消息,是通过 Runtime forawrding 实现的。表现为先打印一条日志,按照 `NSLog(@"*** -[%@ %@]: message sent to deallocated instance %p", c, NSStringFromSelector(sel), o);` 格式打印日志。随后调用系统的 `abort()` 奔溃。 - 按照上面得到的类名,创建一个继承自 NSProxy 的子类。因为需要响应各种原始对象的任何方法,所以需要是 NSProxy 的子类
- hook NSObject 和 NSProxy 2个基类的 `dealloc` 方法,新的 dealloc 方法实现里修改当前类的 isa 为新创建的类
- 为了避免内存空间释放后被重写,造成野指针问题。通过字典存储被方式的对象,同时设置在 30s 后调用 dealloc 方法将字典中存储的对象释放,避免 OOM
- 新创建的类除了处理消息转发外,还需要实现 NSObject 的基础方法,比如 copy、zone、description 方法
- hook 之后,给僵尸对象发消息,最后都会调用 NSProxy 的 `forwardInvocation` 方法,其内部打印方法名称、对象地址、调用堆栈信息。最后调用 abort 方法crash 掉。
- 奔溃的时候应该将调用堆栈等案发信息保存下来,走 APM 问题跟进流程 - 奔溃的时候应该将调用堆栈等案发信息保存下来,走 APM 问题跟进流程
```objectivec ```objective-c
// ZombiePoxy // ZombiePoxy
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
@@ -2852,7 +2903,14 @@ bool init_safe_free(void)
注意ZombieProxy、ZombieObjectDetector 2个类要在 Build Phases 设置为非 ARC加 `-fno-objc-arc` 注意ZombieProxy、ZombieObjectDetector 2个类要在 Build Phases 设置为非 ARC加 `-fno-objc-arc`
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ZombieObjectsDetector.png) <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ZombieObjectsDetector.png" style="zoom:30%" />
### 6. 方案对比
- 僵尸对象相比 `Malloc Scribble`,不需要考虑会不会崩溃的问题,只需要野指针指向僵尸对象,那么再次访问野指针就一定会奔溃
- 僵尸对象的方比如 `Malloc Scribble` 覆盖面广,可以通过 fishhook hook free 方法将 c 函数也包含在其中。
## 六、 App 网络监控 ## 六、 App 网络监控
@@ -2860,7 +2918,7 @@ bool init_safe_free(void)
### 1. App 网络请求过程 ### 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 发送一次网络请求一般会经历下面几个关键步骤: App 发送一次网络请求一般会经历下面几个关键步骤:
@@ -2898,7 +2956,7 @@ App 发送一次网络请求一般会经历下面几个关键步骤:
iOS 网络框架层级关系如下: 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 实现。 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对于网络监控需要做如下的处理 但是如果需要监全部的网络请求就不能满足需求了,查阅资料后发现了阿里百川有 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 比较陌生,可以看一下 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。 CFNetwork 的基础是 CFSocket 和 CFStream。
@@ -3587,9 +3645,9 @@ void printResponseData (CFDataRef responseData) {
NSURLSession、NSURLConnection hook 如下。 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 的方案,整理描述下: 业界有 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 改进版如下 method swizzling 改进版如下
@@ -3891,7 +3949,7 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
typedef struct objc_object *id; 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` 可以实现目的呢? 我们来分析一下为什么修改 `isa` 可以实现目的呢?
@@ -4049,11 +4107,11 @@ self.didCompleteObserver = [[NSNotificationCenter defaultCenter] addObserverForN
HTTP 请求报文结构 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 报文是格式化的数据块,每条报文由三部分组成:对报文进行描述的起始行、包含属性的首部块、以及可选的包含数据的主体部分。 1. HTTP 报文是格式化的数据块,每条报文由三部分组成:对报文进行描述的起始行、包含属性的首部块、以及可选的包含数据的主体部分。
2. 起始行和手部就是由行分隔符的 ASCII 文本,每行都以一个由 2 个字符组成的行终止序列作为结束(包括一个回车符、一个换行符) 2. 起始行和手部就是由行分隔符的 ASCII 文本,每行都以一个由 2 个字符组成的行终止序列作为结束(包括一个回车符、一个换行符)
@@ -4080,11 +4138,11 @@ HTTP 请求报文结构
下图是打开 Chrome 查看极课时间网页的请求信息。包括响应行、响应头、响应体等信息。 下图是打开 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` 查看一个完整的请求和响应数据
![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 会偏大。 我们都知道在 HTTP 通信中,响应数据会使用 gzip 或其他压缩方式压缩,用 NSURLProtocol 等方案监听,用 NSData 类型去计算分析流量等会造成数据的不精确,因为正常一个 HTTP 响应体的内容是使用 gzip 或其他压缩方式压缩的,所以使用 NSData 会偏大。
@@ -4646,7 +4704,7 @@ Mach 已经通过异常机制提供了底层的陷进处理,而 BSD 则在异
Mach 异常都在 host 层被 `ux_exception` 转换为相应的 unix 信号,并通过 `threadsignal` 将信号投递到出错的线程。 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 收集方式 ### 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 异常捕获,可以注册一个异常端口,该端口负责对当前任务的所有线程进行监听。 对于 Mach 异常捕获,可以注册一个异常端口,该端口负责对当前任务的所有线程进行监听。
@@ -5021,7 +5079,7 @@ static void restoreExceptionPorts(void)
KSCrash 在这里的处理逻辑如下图: 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 源码
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ObjcInitExceptionHandlerSet.png" style="zoom:30%" />
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ObjcExceptionHandlerExplore.png" style="zoom:30%" />
发现入口函数 `_objc_init` 中调用可设置异常处理的 `exception_init` 方法。内部通过底层 API `std::set_terminate(&_objc_terminate)` 来设置异常处理的 handler。
#### 2.5. 主线程死锁 #### 2.5. 主线程死锁
主线程死锁的检测和 ANR 的检测有些类似 主线程死锁的检测和 ANR 的检测有些类似
@@ -5477,7 +5545,7 @@ static void setEnabled(bool isEnabled)
其他几个 crash 也是一样,异常信息经过包装交给 `kscm_handleException()` 函数处理。可以看到这个函数被其他几种 crash 捕获后所调用。 其他几个 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 ```c
/** Start general exception processing. /** 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 的监控 #### 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 异常监控 ##### 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 代码了。 模拟器点击 `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 的地方。 查看到 crash stack 后点击可以跳转到 sourceMap 的地方。
@@ -6049,7 +6117,7 @@ TipsRN 项目打 Release 包
现象iOS 项目奔溃。截图以及日志如下 现象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 ```shell
2020-06-22 22:26:03.318 [info][tid:main][RCTRootView.m:294] Running application todos ({ 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。 现象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. 拿脚本解析好的行号、列号、文件信息去和源代码文件比较,结果很正确。 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 解析系统设计 ##### 2.7.5 SourceMap 解析系统设计
@@ -6816,7 +6884,7 @@ parseJSError(line, column);
`.DSYM` 文件是从 Mach-O 文件中抽取调试信息而得到的文件目录,发布的时候为了安全,会把调试信息存储在单独的文件,`.DSYM` 其实是一个文件目录,结构如下: `.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 文件 #### 4.2 DWARF 文件
@@ -7348,7 +7416,7 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
/Users/你自己的用户名/Library/Developer/Xcode/iOS DeviceSupport/ /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. 服务端处理 ### 5. 服务端处理
@@ -7358,7 +7426,7 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
早期单体应用时代,几乎应用的所有功能都在一台机器上运行,出了问题,运维人员打开终端输入命令直接查看系统日志,进而定位问题、解决问题。随着系统的功能越来越复杂,用户体量越来越大,单体应用几乎很难满足需求,所以技术架构迭代了,通过水平拓展来支持庞大的用户量,将单体应用进行拆分为多个应用,每个应用采用集群方式部署,负载均衡控制调度,假如某个子模块发生问题,去找这台服务器上终端找日志分析吗?显然台落后,所以日志管理平台便应运而生。通过 Logstash 去收集分析每台服务器的日志文件,然后按照定义的正则模版过滤后传输到 Kafka 或 Redis然后由另一个 Logstash 从 Kafka 或 Redis 上读取日志存储到 ES 中创建索引,最后通过 Kibana 进行可视化分析。此外可以将收集到的数据进行数据分析,做更进一步的维护和决策。 早期单体应用时代,几乎应用的所有功能都在一台机器上运行,出了问题,运维人员打开终端输入命令直接查看系统日志,进而定位问题、解决问题。随着系统的功能越来越复杂,用户体量越来越大,单体应用几乎很难满足需求,所以技术架构迭代了,通过水平拓展来支持庞大的用户量,将单体应用进行拆分为多个应用,每个应用采用集群方式部署,负载均衡控制调度,假如某个子模块发生问题,去找这台服务器上终端找日志分析吗?显然台落后,所以日志管理平台便应运而生。通过 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 的日志架构图。简单说明下: 上图展示了一个 ELK 的日志架构图。简单说明下:
@@ -7368,13 +7436,13 @@ app 和 .DSYM 文件可以通过打包的产物得到,路径为 `~/Library/Dev
下图贴一个 Elasticsearch 社区分享的一个 “Elastic APM 动手实战”[主题](https://elasticsearch.cn/slides/257#page=3)的内容截图。 下图贴一个 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 服务侧 ##### 5.2 服务侧
Crash log 统一入库 Kibana 时是没有符号化的所以需要符号化处理以方便定位问题、crash 产生报表和后续处理。 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 -> 产品侧(显示端)对数据进行分类、报表、报警等操作。 所以整个流程就是:客户端 APM SDK 收集 crash log -> Kafka 存储 -> Mac 机执行定时任务符号化 -> 数据回传 Kafka -> 产品侧(显示端)对数据进行分类、报表、报警等操作。
@@ -7386,7 +7454,7 @@ Crash log 统一入库 Kibana 时是没有符号化的,所以需要符号化
现在很多架构设计都是微服务,至于为什么选微服务,不在本文范畴。所以 crash 日志的符号化被设计为一个微服务。架构图如下 现在很多架构设计都是微服务,至于为什么选微服务,不在本文范畴。所以 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 有个能力就是调用打包系统的打包构建能力,会根据项目的特点,选择合适的打包机(打包平台是维护了多个打包任务,不同任务根据特点被派发到不同的打包机上,任务详情页可以看到依赖的下载、编译、运行过程等,打包好的产物包括二进制包、下载二维码等等) - 脚手架 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 进程做符号化处理。 其中符号化服务是大前端背景下大前端团队的产物,所以是 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 文件。 简单说明下,符号化流程是一个主从模式,一台 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 异常监控 ## 九、Weex、Flutter 异常监控
@@ -7706,7 +7774,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
1. 当前启动时的js 预载入流程里的JS下载失败 和 进入Weex页面后的JS拉取失败 两处的上报失败未做区分没法实际统计加载页面时有多少JS获取失败的情况。 1. 当前启动时的js 预载入流程里的JS下载失败 和 进入Weex页面后的JS拉取失败 两处的上报失败未做区分没法实际统计加载页面时有多少JS获取失败的情况。
2. 随着业务迭代, Weex 页面越来越多,需要做 JS 拉取相关优化,避免白屏问题 2. 随着业务迭代, Weex 页面越来越多,需要做 JS 拉取相关优化,避免白屏问题
3. 目前 JS 缓存逻辑为,每个 Weex 模块单独维护一个 LRUCache并且会做大量的预加载但可能大多数页面根本不会访问到。浪费了内存资源去做了一些不会被调用到的页面预加载可能还会造成 OOM 问题 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或者基于用户行为路径的预热功能比如某个收银员角色固定的情况下他日常的操作行为是固定的商品扫码、开单 // todo? 参考前端资源模块化,如何实现资源预加载(全局 LRU或者基于用户行为路径的预热功能比如某个收银员角色固定的情况下他日常的操作行为是固定的商品扫码、开单
@@ -7718,15 +7786,15 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
可能有些人一直没有遇到过因为在子线程操作 UI导致在开发阶段 Xcode console 输出了一堆日志,大体如下 可能有些人一直没有遇到过因为在子线程操作 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 的时候就会被系统检测到并触发断点,同时可以看到堆栈情况 其实我们可以给 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. 问题及解决方案 ### 2. 问题及解决方案
@@ -7740,7 +7808,7 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
但是某些类有些 API 我们也不希望在子线程被调用,这时候 `libMainThreadChecker.dylib`是无法满足的。 但是某些类有些 API 我们也不希望在子线程被调用,这时候 `libMainThreadChecker.dylib`是无法满足的。
对 `libMainThreadChecker.dylib` 库的汇编代码研究,发现 `libMainThreadChecker.dylib` 是通过内部 `__main_thread_add_check_for_selector` 这个方法来进行类和方法的注册的。所以如果我们同样可以通过 `dlsym` 来调用该方法,以达到对自定义类和方法的主线程调用监测。 对 `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)实现。 另外该功能可以在线下 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 下卡顿监控是无法覆盖的。用下图表示 好像用 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个指标**页面渲染时长、页面可交互时长**。 页面作为承载用户交互的具体战场我们需要对页面的性能有个直观的指标。业界一般有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) 4. 监控数据需要做内存到文件的写入处理,需要注意策略。监控数据需要存储数据库,数据库大小、设计规则等。存入数据库后如何上报,上报机制等会在另一篇文章讲:[打造一个通用、可配置的数据上报 SDK](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md)
5. 尽量在技术评审后,将各端的技术实现写进文档中,同步给相关人员。比如 ANR 的实现 5. 尽量在技术评审后,将各端的技术实现写进文档中,同步给相关人员。比如 ANR 的实现
```objective-c ```objective-c
/* /*
android 端 android 端
@@ -7865,15 +7933,19 @@ typedef NS_ENUM(NSUInteger, WeexAPMType) {
``` ```
6. 整个 APM 的架构图如下 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 来关联日志数据 - 埋点 SDK通过 sessionId 来关联日志数据
7. APM 技术方案本身是随着技术手段、分析需求不断调整升级的。上图的几个结构示意图是早期几个版本的目前使用的是在此基础上进行了升级和结构调整提几个关键词Hermes、Flink SQL、InfluxDB。 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/) - [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://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) - [iOS 启动时间优化](https://www.zoomfeng.com/blog/launch-time.html)
- [WWDC2019 之启动时间与 Dyld3](https://www.zoomfeng.com/blog/launch-optimize-from-wwdc2019.html) - [WWDC2019 之启动时间与 Dyld3](https://www.zoomfeng.com/blog/launch-optimize-from-wwdc2019.html)
- [Apple-libmalloc](https://opensource.apple.com/tarballs/libmalloc/) - [Apple-libmalloc](https://opensource.apple.com/tarballs/libmalloc/)

View File

@@ -1082,7 +1082,7 @@ QA一个被测方法有诸多 case为什么不写在一个测试方法
1. 主工程不管是不是混编,但为了让 Swift 测试代码,可以访问到 OC 被测类,需要创建一个 Swift 文件,系统会自动创建 bridge 文件,且需要在 `AppTestingExplore-Bridging-Header.h` 文件中导出需要被测的头文件 1. 主工程不管是不是混编,但为了让 Swift 测试代码,可以访问到 OC 被测类,需要创建一个 Swift 文件,系统会自动创建 bridge 文件,且需要在 `AppTestingExplore-Bridging-Header.h` 文件中导出需要被测的头文件
2. 在 Swift 测试文件中,导入主工程 module。 2. 在 Swift 测试文件中,导入主工程 module。
<img src="./../assets/SwiftTestingForOCClass.png" style="zoom:30%; align:left;"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SwiftTestingForOCClass.png" style="zoom:30%; align:left;">
@@ -1343,7 +1343,7 @@ int main(int argc, char * argv[]) {
开发的单元测试代码,运行的背后也是一个可执行文件。查看内部,可以发现一堆和测试相关的 framework。 开发的单元测试代码,运行的背后也是一个可执行文件。查看内部,可以发现一堆和测试相关的 framework。
<img src="./../assets/XcodeUnitTestDyanimcFramework.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeUnitTestDyanimcFramework.png" style="zoom:30%" />
思考:一个工程中,只要写好了测试代码,是不是加载这几个动态库就可以运行测试用例了? 思考:一个工程中,只要写好了测试代码,是不是加载这几个动态库就可以运行测试用例了?
@@ -1369,7 +1369,7 @@ int main(int argc, char * argv[]) {
下面是之前在有赞开发完精准测试系统后落地到一个业务项目中取得的价值帮助2位 QA 发现漏测的代码,倒逼 QA 去设计更完善的测试 case、分析覆盖率低是开发者的兜底代码太多还是真的漏掉了业务 case。 下面是之前在有赞开发完精准测试系统后落地到一个业务项目中取得的价值帮助2位 QA 发现漏测的代码,倒逼 QA 去设计更完善的测试 case、分析覆盖率低是开发者的兜底代码太多还是真的漏掉了业务 case。
<img src="./../assets/iOS_PreciousUnitTest1.png" style="zoom:20%;display:inline"> <img src="./../assets/iOS_PreciousUnitTest2.png" style="zoom:20%;display:inline"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/iOS_PreciousUnitTest1.png" style="zoom:20%;display:inline"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/iOS_PreciousUnitTest2.png" style="zoom:20%;display:inline">
精准测试助力业务,质量更加稳定。 精准测试助力业务,质量更加稳定。

View File

@@ -75,7 +75,7 @@ Person 类存在3个 BOOL 属性:
上 Demo 上 Demo
<img src="./../assets/BOOLPropertyImplementedByBitOperator.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/BOOLPropertyImplementedByBitOperator.png" style="zoom:25%">
@@ -85,7 +85,7 @@ Person 类存在3个 BOOL 属性:
新方案采用:**结构体的位域能力**,限定单个成员变量所占用的内存。代码如下: 新方案采用:**结构体的位域能力**,限定单个成员变量所占用的内存。代码如下:
<img src="./../assets/BOOLProptertImpledByStructureBitDomain.png" style="zoom:25%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/BOOLProptertImpledByStructureBitDomain.png" style="zoom:25%" />
@@ -97,7 +97,7 @@ Person 类存在3个 BOOL 属性:
虽然上述方式都可以实现存储 Person 类3个属性的目的但是还有第三种方案参考 iOS 系统设计,采用 Union 实现。代码如下 虽然上述方式都可以实现存储 Person 类3个属性的目的但是还有第三种方案参考 iOS 系统设计,采用 Union 实现。代码如下
<img src="./../assets/BOOLPropertyImplementedByUnion.png" style="zoom:25%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/BOOLPropertyImplementedByUnion.png" style="zoom:25%" />
分析: 分析:
@@ -168,7 +168,7 @@ union {
与一个不是3个数之一的数按位与得到的结果为`0b0000 0000`。利用这个特性我们可以判断传递来的参数是不是包含了某个值 与一个不是3个数之一的数按位与得到的结果为`0b0000 0000`。利用这个特性我们可以判断传递来的参数是不是包含了某个值
<img src="./../assets/BitOperateorOnEnumDemo.png" style="zoom:25%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/BitOperateorOnEnumDemo.png" style="zoom:25%" />
有了上面的铺垫,就可以更好的查看 runtime 中 isa 的定义了。 有了上面的铺垫,就可以更好的查看 runtime 中 isa 的定义了。
@@ -273,7 +273,7 @@ isa 在 arm64 之后必须通过 `ISA_MASK` 去查询 class类对象、元类
`0x0000000ffffffff8ULL` 用程序员模式打开计算器 `0x0000000ffffffff8ULL` 用程序员模式打开计算器
<img src="./../assets/objc-isa-mask.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/objc-isa-mask.png" style="zoom:30%">
@@ -283,7 +283,7 @@ extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcl
知道结构体可以指定存储大小这个功能后,可以看到 `isa_t` 联合体与 `ISA_MASK` 按位与之后的地址,其实就是类真实的地址信息(可能是类对象、也有可能是元类对象) 知道结构体可以指定存储大小这个功能后,可以看到 `isa_t` 联合体与 `ISA_MASK` 按位与之后的地址,其实就是类真实的地址信息(可能是类对象、也有可能是元类对象)
<img src="./../assets/objc-isa.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/objc-isa.png" style="zoom:30%" />
@@ -423,7 +423,7 @@ struct class_ro_t {
具体关系整理如下图 具体关系整理如下图
<img src="./../assets/runtime-class.png" style="zoom:50%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-class.png" style="zoom:50%" />
@@ -435,7 +435,7 @@ struct class_ro_t {
<img src="./../assets/runtime-class-rw-t.png" style="zoom:40%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-class-rw-t.png" style="zoom:40%" />
比如访问 method 的过程 比如访问 method 的过程
@@ -453,7 +453,7 @@ struct class_ro_t {
- `class_ro_t` 里面的 baseMethodList、baseProtocols、ivars、baseProperties 是一维数组,是只读的,包含了类的(原始信息)初始内容 - `class_ro_t` 里面的 baseMethodList、baseProtocols、ivars、baseProperties 是一维数组,是只读的,包含了类的(原始信息)初始内容
<img src="./../assets/runtime-class-ro-t.png" style="zoom:50%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-class-ro-t.png" style="zoom:50%" />
@@ -1240,7 +1240,7 @@ v
可以对照下面的表格进行查看: 可以对照下面的表格进行查看:
![](./../assets/runtime-method-encoding.png) ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-method-encoding.png)
```objectivec ```objectivec
- (int)calcuate:(int)baseHeight heigith:(float)height; - (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); NSLog(@"%s %p", goodStudentSayBucket._key, goodStudentSayBucket._imp);
``` ```
<img src="./../assets/runtime-method-find.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-method-find.png" style="zoom:25%">
@@ -1544,7 +1544,7 @@ static inline mask_t cache_hash(cache_key_t key, mask_t mask)
## Runtime - objc_msgSend ## Runtime - objc_msgSend
<img src="./../assets/MethodCallIsObjcMsgSend.png" style="zoom:20%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MethodCallIsObjcMsgSend.png" style="zoom:20%" />
```c++ ```c++
Person *p = [[Person alloc] init]; Person *p = [[Person alloc] init];
@@ -2117,7 +2117,7 @@ if (imp) goto done;
上面的流程是整个 `objc_msgSend` 的消息发送阶段的整个流程。可以用下图表示 上面的流程是整个 `objc_msgSend` 的消息发送阶段的整个流程。可以用下图表示
<img src="./../assets/runtime-objc_msgSend-messageSend.png" style="zoom:60%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-objc_msgSend-messageSend.png" style="zoom:60%" />
@@ -2262,7 +2262,7 @@ SEL_resolveClassMethod, sel);`
完整流程如下 完整流程如下
<img src="./../assets/runtime-objc_msgSend-ResolveMethod.png" style="zoom:40%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-objc_msgSend-ResolveMethod.png" style="zoom:40%" />
@@ -2307,7 +2307,7 @@ Person *person = [[Person alloc] init];
知道 `objc_msgSend` 的流程,我们尝试给它修正下 知道 `objc_msgSend` 的流程,我们尝试给它修正下
<img src="./../assets/objc_msgSend_fix1.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/objc_msgSend_fix1.png" style="zoom:25%">
方法1增加一个兜底方法然后利用 `class_addMethod` 动态增加方法实现 方法1增加一个兜底方法然后利用 `class_addMethod` 动态增加方法实现
@@ -2341,7 +2341,7 @@ class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncod
方法3也可以添加 c 语言方法 方法3也可以添加 c 语言方法
<img src="./../assets/objc_msgSend_fix2.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/objc_msgSend_fix2.png" style="zoom:25%">
c 函数即函数地址,只不过传递给 class_addMethod 的时候需要转换为函数参数加 `(IMP)cfuntionResolver`,手动添加方法签名。 c 函数即函数地址,只不过传递给 class_addMethod 的时候需要转换为函数参数加 `(IMP)cfuntionResolver`,手动添加方法签名。
@@ -2454,7 +2454,7 @@ void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
为什么是 `__forwarding__` 方法。我们可以根据 Xcode 崩溃窥探一二 为什么是 `__forwarding__` 方法。我们可以根据 Xcode 崩溃窥探一二
![](./../assets/runtime-forwardingFailed.png) ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-forwardingFailed.png)
```c ```c
int __forwarding__(void *frameStackPointer, int isStret) { int __forwarding__(void *frameStackPointer, int isStret) {
@@ -2498,7 +2498,7 @@ int __forwarding__(void *frameStackPointer, int isStret) {
完整流程如下 完整流程如下
<img src="./../assets/runtime-forwarding.png" style="zoom:40%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-forwarding.png" style="zoom:40%" />
@@ -2532,13 +2532,13 @@ int __forwarding__(void *frameStackPointer, int isStret) {
Person 类不存在对象方法 makeliving PersonHelper 类存在。 Person 类不存在对象方法 makeliving PersonHelper 类存在。
<img src="./../assets/RuntimeCallUnKnownMethod.png" style="zoom:25%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeCallUnKnownMethod.png" style="zoom:25%" />
调用对象不存在的方法,则会抛出错误。同时在 Runtime 动态消息解析阶段,`resolveInstanceMethod` 没有处理对象方法,所以会报错 调用对象不存在的方法,则会抛出错误。同时在 Runtime 动态消息解析阶段,`resolveInstanceMethod` 没有处理对象方法,所以会报错
方法1因为动态消息解析没有处理则会开始走消息转发阶段。消息转发首先会调用 `- (id)forwardingTargetForSelector:(SEL)aSelector` 方法。(如果是对象方法则调用 `- (id)forwardingTargetForSelector:(SEL)aSelector`,如果是类方法则调用 `+ (id)forwardingTargetForSelector:(SEL)aSelector` 方法1因为动态消息解析没有处理则会开始走消息转发阶段。消息转发首先会调用 `- (id)forwardingTargetForSelector:(SEL)aSelector` 方法。(如果是对象方法则调用 `- (id)forwardingTargetForSelector:(SEL)aSelector`,如果是类方法则调用 `+ (id)forwardingTargetForSelector:(SEL)aSelector`
<img src="./../assets/RuntimeMethodForwardingDemo1.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeMethodForwardingDemo1.png" style="zoom:30%" />
方法2如果消息转发里`forwardingTargetForSelector` 返回了 nil则开始调用方法签名 `methodSignatureForSelector` 方法和 `forwardInvocation` 方法 方法2如果消息转发里`forwardingTargetForSelector` 返回了 nil则开始调用方法签名 `methodSignatureForSelector` 方法和 `forwardInvocation` 方法
@@ -2568,13 +2568,13 @@ Person 类不存在对象方法 makeliving PersonHelper 类存在。
注意:`methodSignatureForSelector` 如果返回 nil则 `forwardInvocation` 不会执行 注意:`methodSignatureForSelector` 如果返回 nil则 `forwardInvocation` 不会执行
<img src="./../assets/RuntimeMethodForwardingDemo2.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeMethodForwardingDemo2.png" style="zoom:30%" />
上述方式不够优雅,针对方法签名的获取太被动,方法改变了,方法签名处需要调整,很麻烦。 上述方式不够优雅,针对方法签名的获取太被动,方法改变了,方法签名处需要调整,很麻烦。
<img src="./../assets/RuntimeMethodForwardingWithMethodSignature.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeMethodForwardingWithMethodSignature.png" style="zoom:30%" />
@@ -2601,11 +2601,11 @@ Person 类不存在对象方法 makeliving PersonHelper 类存在。
} }
``` ```
<img src="./../assets/RuntimeMethodForwardingByClassMethod.png" style="zoom:30%"/> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeMethodForwardingByClassMethod.png" style="zoom:30%"/>
<img src="./../assets/RuntimeMethodForwardingByClassMethod2.png" style="zoom:30%"/> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeMethodForwardingByClassMethod2.png" style="zoom:30%"/>
@@ -2632,7 +2632,7 @@ OC 中方法调用的本质就是利用 runtime 发消息,发消息也给 rece
3. 如果在当前类的方法列表中没有找对方法实现,则根据类对象的 superclass 在父类的类对象的方法列表中继续查找 3. 如果在当前类的方法列表中没有找对方法实现,则根据类对象的 superclass 在父类的类对象的方法列表中继续查找
<img src="./../assets/MethodCacheLookUpProcess.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MethodCacheLookUpProcess.png" style="zoom:30%" />
先根据 superclass 找到父类然后在父类中也按照上面3点进行递归查找 先根据 superclass 找到父类然后在父类中也按照上面3点进行递归查找
@@ -2757,7 +2757,7 @@ objc_msgSendSuper(arg, sel_registerName("class"))
我们对 iOS 项目`[super viewDidLoad]` 下符号断点,发现`objc_msgSendSuper2` 我们对 iOS 项目`[super viewDidLoad]` 下符号断点,发现`objc_msgSendSuper2`
![](./../assets/runtime-super.png) ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/runtime-super.png)
查看 objc4 源代码发现是一段汇编实现。 查看 objc4 源代码发现是一段汇编实现。
@@ -2845,7 +2845,7 @@ call - 调用函数
也可以在 Xcode 上可视化面板操作,路径为:菜单栏 `Product -> Perform Action -> Assemble "ViewController.m"` 也可以在 Xcode 上可视化面板操作,路径为:菜单栏 `Product -> Perform Action -> Assemble "ViewController.m"`
<img src="./../assets/XcodeAssembleClass.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeAssembleClass.png" style="zoom:30%">
@@ -2871,7 +2871,7 @@ call - 调用函数
Demo1 Demo1
<img src="./../assets/RuntimeIsKindOfClassDemo1.png" style="zoom:40%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeIsKindOfClassDemo1.png" style="zoom:40%" />
@@ -2899,7 +2899,7 @@ Demo1
Demo2 Demo2
<img src="./../assets/RuntimeIsKindOfClassDemo2.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeIsKindOfClassDemo2.png" style="zoom:30%" />
下面面2个判断都是调用类方法的 `isMemberOfClass` 、`isKindOfClass` 下面面2个判断都是调用类方法的 `isMemberOfClass` 、`isKindOfClass`
@@ -2917,7 +2917,7 @@ Demo2
可以看到 `+(BOOL)isMemberOfClass:(Class)cls` 方法内部就是对当前类获取类对象,然后与传递进来的 cls 判断是否相等。由于是 `[Student isMemberOfClass:[Student class]])` `Student` 类调用类方法 `+isMemberOfClass` 所以类对象的类对象也就是元类对象cls 参数也就是 `[Student class]` 是一个类对象,元类对象等于类对象吗?显然不是 可以看到 `+(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` 同理分析。作用是当前类的元类,是否是右边传入对象的元类或者元类的子类。 `+(BOOL)isKindOfClass:(Class)cls` 同理分析。作用是当前类的元类,是否是右边传入对象的元类或者元类的子类。
@@ -2958,7 +2958,7 @@ QA
综合练习: 综合练习:
<img src="./../assets/RuntimeIsKindOfClassUnitDemo.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeIsKindOfClassUnitDemo.png" style="zoom:30%" />
@@ -2966,16 +2966,12 @@ QA
## Runtime 刁钻题 ## Runtime 刁钻题
能否向编译后的类添加实例变量class_ro_t 不可以添加。但可以向动态创建的类添加。 ### NSObject 的内存布局、isa、对象属性访问原理
> 这道题目设计super 调用的本质、函数栈空间向下增长、runtime 消息调用本质isa、访问对象的成员变量找到 isa约过前面的8字节按照成员变量的大小去找成员 > 这道题目设计super 调用的本质、函数栈空间向下增长、runtime 消息调用本质isa、访问对象的成员变量找到 isa约过前面的8字节按照成员变量的大小去找成员
> >
> 因为实例对象里存的就是isa + 各个成员变量的值 > 因为实例对象里存的就是isa + 各个成员变量的值
```objective-c ```objective-c
@interface Person : NSObject @interface Person : NSObject
@property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *name;
@@ -2999,22 +2995,20 @@ QA
程序运行什么结果? 程序运行什么结果?
<img src="./../assets/RuntimeQA1.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeQA1.png" style="zoom:30%">
为什么会方法调用成功?为什么 name 打印出为 @"杭城小刘" 为什么会方法调用成功?为什么 name 打印出为 @"杭城小刘"。我们来分析下:
我们来分析下: 第一、**方法调用本质就是寻找 isa 进行消息发送**
1.**方法调用本质就是寻找 isa 进行消息发送**
```objective-c ```objective-c
Person *person = [[Person alloc] init]; Person *person = [[Person alloc] init];
[person sayHi]; [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 字符串。 上面代码可以类比类调用方法的流程。 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 再看一个变体1
<img src="./../assets/RuntimeQA2.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeQA2.png" style="zoom:30%">
打印输出是因为 `*p` 类似 isa 指针。本身占用8字节空间然后访问 `self->_name` 就是 `base + 8 = isa地址 + 8 ` 出的内存就是 name`*p` 是在栈中加8就是向上声明的变量当前情况下 `Address(*p) + 8` 就是 temp 变量。所以输出 `<NSObject: 0x****>`
再看一个变体2 再看一个变体2
<img src="./../assets/RuntimeQA3.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeQA3.png" style="zoom:30%">
分析: 搞懂的小伙伴不迷惑了。没搞懂其实就是没搞懂**栈地址由高到低,向下生长** 和 `super` 调用的本质。
搞懂的小伙伴不迷惑了。没搞懂其实就是没搞懂**栈地址由高到低,向下生长** 和 `super` 调用的本质。
再强调一句,根据指针寻找成员变量 _name 的过程其实就是根据内存偏移找对象的过程。在变体2中isa 地址就是 class 的地址,所以按照`地址 +8` 的策略,其实前一个局部变量。 再强调一句,根据指针寻找成员变量 _name 的过程其实就是根据内存偏移找对象的过程。在变体2中isa 地址就是 class 的地址,所以按照`地址 +8` 的策略,其实前一个局部变量。
@@ -3069,7 +3063,11 @@ objc_msgSendSuper(arg, sel_registerName("viewDidLoad"));
所以此时的“前一个局部变量” 也就是结构体 `objc_super` 类型的 arg。arg 是一个结构体,结构体第一个成员变量就是 self所以“前一个局部变量” 也就是 selfViewController 所以此时的“前一个局部变量” 也就是结构体 `objc_super` 类型的 arg。arg 是一个结构体,结构体第一个成员变量就是 self所以“前一个局部变量” 也就是 selfViewController
![](./../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个字节会命中哪个 可能会疑问,知道了 super 调用本质知道了会产生一个局部变量的结构体但是结构体里面2个成员变量找属性的时候isa 下的8个字节会命中哪个
@@ -3079,12 +3077,44 @@ objc_msgSendSuper(arg, sel_registerName("viewDidLoad"));
<img src="./../assets/RuntimeQA.png" style="zoom: 80%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeQA.png" style="zoom: 80%">
### runtime 对象方法、类方法的查找过程熟悉吗?
下面的代码会 crash 吗?
```objective-c
id rs = [NSObject valueForKey:@"isa"];
NSLog(@"%@", rs);
```
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeCallValueForKeyOnObject.png" style="zoom:50%" />
不会 Crash。输出的是 NSObject 的元类对象。因为 NSObject 调用的是类方法,先去元类对象的方法列表中查找,发现没有 `valueForKey` 方法。则继续从 NSObject 元类对象的父类对象,也就是 NSObject 的类对象上查找 `valueForKey` 方法。
因为分类 `NSObject(NSKeyValueCoding)` 实现了 `-valueForKey` 方法。所以不会 crash在类对象方法中访问 isa也就是获取类对象的 isa也就是 NSObject 的元类对象。
查看了 objc 源码,会发现很多 NSObject 的基础方法:`+ (id)init`、`- (id)init` 等均有 `+` 、`-` 方法。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSObjectImpleByClassAndInstance.png" style="zoom:30%" />
为什么这么设计?猜测是为了代码的健壮。
- 因为存在继承关系,所有的对象的基类需要有个源头。即使是 NSObject 也是如此。
- 调用对象方法的本质就是根据对象的 isa 找到类对象,然后从方法缓存中去查找方法实现,有就调用,没有就从类对象的方法列表中查找,找到则写入方法缓存并调用,没有则根据 superclass 的类对象继续查找...,一直找到 NSObject 还是找不到,则走 Runtime 消息转发流程。
- 调用类方法的本质是根据对象的 isa 找到类对象,再根据类对象的 isa 找到元类对象,从元类对象的方法方法列表中查找方法实现,如果没有则继续向上查找,直到找到基类的元类对象,也就是 NSObject如果找不到则走消息转发流程
- 但是 Apple 的设计是为了 NSObject 元类对象的父类也要有个东西去接着,于是就让 NSObject 的类对象来充当 。
这也就是为什么 NSObject 子类对象调用 `+` 类方法不 crash 的原因。
## 应用场景 ## 应用场景
### 统计 App 中未响应的方法。给 NSObject 添加分类 `NSObject+ExceptionHunter.m`,利用 NSProxy 实现 ### 统计 App 中未响应的方法。给 NSObject 添加分类 `NSObject+ExceptionHunter.m`,利用 NSProxy 实现
@@ -3121,7 +3151,7 @@ Person *p = [Person new];
object_setClass(p, [Student class]); 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>)` runtime 中 copy、create 等出来的内存,不使用的时候需要手动释放`objc_disposeClassPair(newClass>)`
@@ -3229,7 +3259,7 @@ free(properties);
<img src="./../assets/RuntimeDicToObject.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RuntimeDicToObject.png" style="zoom:30%" />
不够健壮体现在: 不够健壮体现在:
@@ -3369,3 +3399,7 @@ OC 是一门动态性很强的编程语言,允许很多操作推迟到程序
- 无痕埋点 - 无痕埋点
- 热修复 - 热修复
可以认为 oc 中对象上一个指向 CLassObject 地址的变量 `id obj = &ClassObject`
而对象的实例变量 `void *ivar = &obj + offset(N*size)` 。根据 isa 也就是对象的基地址,然后偏移访问 ivar。

View File

@@ -5,3 +5,14 @@
可以设计实现一个线程池涉及几个角色、任务队列、调度者如何调度、线程池的有哪些策略。iOS GCD 的线程池策略可以类比 Java 中的4个线程池策略明白不同语言设计的共同之处 可以设计实现一个线程池涉及几个角色、任务队列、调度者如何调度、线程池的有哪些策略。iOS GCD 的线程池策略可以类比 Java 中的4个线程池策略明白不同语言设计的共同之处
- https://juejin.im/post/6855807995570618375 - https://juejin.im/post/6855807995570618375
串行队列DQF_WIDTH(1)
队列和线程的区别?

View File

@@ -1,33 +1,29 @@
# fishhook 原理 # 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 函数”加了引号,带着问题往下看 为什么对 “c 函数”加了引号,带着问题往下看
Hook NSLog上 Demo
```objectivec
static void (*SystemLog)(NSString *format, ...); ### Hook 系统 c 函数
- (void)viewDidLoad {
[super viewDidLoad]; 以 NSLog 为例。
struct rebinding NSLogRebinding = {
"NSLog", <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishHookWithCFunction.png" style="zoom:30%" />
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 成功了。 可以看到 hook 成功了。
@@ -39,10 +35,49 @@ struct rebinding {
}; };
``` ```
### Hook 自定义 c 函数
自定义一个 c 函数 `handleTouchAction` ,发现没有 hook 成功。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishHookWithUserCFunction.png" style="zoom:30%" />
不禁令人好奇,同样是 c 函数,为什么系统 c 函数可以 hook自定义的 c 函数无法 hook带着问题继续探究
## 原理窥探 ## 原理窥探
FishHook 是 FaceBook 提供的一个可以动态修改链接 Mach-O 文件的工具。利用 Mach-O 文件的加载原理通过修改懒加载和非懒加载2个表的指针达到 hook 系统 C 函数的目的。
### Mach-O 文件权限
Mach-O 分为代码段、数据段...
- 代码段:可读、可执行、不可写
- 数据段:可读、可写、不可执行
### 系统共享缓存
我们知道 NSLog 的函数实现在 Foundation 库中,而我们开发自己写的其他函数则在自身可执行文件中,也就是 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%的性能损失,但换来程序在空间上的节省和程序构建和升级的灵活性,是值得的。 但也带来了坏处因为都是程序每次装载的时候进行重新链接。有解决方案叫做延迟绑定Lazy binding可使得动态链接对性能的影响减的最小。据估算动态链接相比静态链接存在大约5%的性能损失,但换来程序在空间上的节省和程序构建和升级的灵活性,是值得的。
地址无关代码PIC
装载时重定位是解决动态模块中有绝对地址引用的方案之一。但其存在一个很大缺点,是指令部分无法在多个进程间共享,这样就是失去动态链接节省内存这一优势。我们还需要一种更优雅的方案,希望程序模块中的共享指令部分在装载时不需要因为装载地址改变而改变,所以实现的基本想法就是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本,这个方案就是 地址无关代码Position Independent CodePIC 技术。
如何解决?
编译阶段给 NSLog 一个默认地址,在应用启动时,通过重定向,把真正的地址重新写入到 Mach-O 中,但效率很低。 后来诞生了 `PIC` 技术
### PIC 技术
装载时重定位是解决动态模块中有绝对地址引用的方案之一。但其存在一个很大缺点,是指令部分无法在多个进程间共享,这样就是失去动态链接节省内存这一优势。
我们还需要一种更优雅的方案,希望程序模块中的共享指令部分在装载时不需要因为装载地址改变而改变,所以实现的基本想法就是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本,这个方案就是 地址无关代码Position Independent CodePIC 技术。
Position-Independent Code即位置无关代码。这是一种编译技术允许生成的代码在内存中的任何位置执行而不需要任何修改。这在动态链接库dylib中非常重要因为它允许系统动态地将库加载到内存中的不同位置而不需要重新链接。
优点是:
- 共享代码:多个应用程序可以共享同一个动态库的实例,节省内存并减少启动时间。
- 动态链接器dyld可以优化符号绑定过程提高应用程序的启动速度
- 支持代码重定位,使得应用程序更新和修补更加灵活
写的业务代码里面假如某一行调用了 `NSLog`,那么在编译阶段,使用 NSLog 只是 IDE 提供了功能,让你可以看到声明而已。编译后的可执行文件,还是不知道 NSLog 的具体函数地址。这个底层是如何工作的呢? 写的业务代码里面假如某一行调用了 `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 说明只有在用到的时候才去绑定。下断点验证下
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishHookMachO.png" style="zoom:40%" />
第二步:在 `NSLog` 处打断点,触发后 LLDB 模式下 `image list` 查看所有的 image。可以看到第一个 image 就是 App 主程序镜像,且 image 开始地址为 `0x0000000100da5000`
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishHookDemoImageList.png" style="zoom:40%" />
第三步:进入 LLDB 模式,根据 image base 地址 + offset 计算 NSLog 的地址。即 `memory read 0x0000000102eec000+0xC000`,查看下内存信息
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSLogFakeAddress.png" style="zoom:40%" />
第四步:断点过下一行,即执行过一次 NSLog。然后再通过 LLDB 根据地址查看汇编代码,`dis -s addr` 查看
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLDBNSLogAddressSymbol.png" style="zoom:40%" />
第五步:继续过断点,等执行完 `rebind_symbols` 再看看内存信息。可以看到再 rebind 之后,地址变了。然后根据地址查看汇编代码,发现已经是我们自定义的函数了。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishhookResult.png" style="zoom:40%" />
第一步: 在 `Lazy Symbol Pointers` 懒加载符号表中看到第一个符号 `NSLog`索引为1。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishHookMachO1.png" style="zoom:30%" />
第二步:根据索引,在 `Dynamic Symbol Tables` 动态符号表中看到第一条数据,是 NSLog 相关的。其 Data 值 `00000084` 是十六进制的,换算为十进制就是 132。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishHookMachO2.png" style="zoom:30%" />
第三步:根据第二步得到的角标,在 `Symbol Table` 符号表中查找第132个位置。可以看到其 Data 值 `000000AA` 是偏移值。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishHookMachO3.png" style="zoom:30%" />
第四步:在 `String Table` 中,第一个位置 `0000CFE4` 加上偏移值 `0xAA` ,等于 `0xD08E`,如下图所示,就是 `NSLog` 符号真实的地址。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishHookMachO4.png" style="zoom:30%" />
`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 做的事情就是将系统的符号表,将符号表中的特定符号对应的地址,修改为自定义的函数地址。起到了 hook 作用。也就是说外部的 c 函数,在 iOS 中的调用属于**动态调用**。
知道了 fishhook 的工作原理,我们就知道 fishhook 是 hook 不了自己写的 c 函数了。因为自定义函数是没有通过符号去寻找函数真正地址的这个过程。而系统库是通过符号去绑定真实地址的。可以通过 `rebind_symbols` 这个名称得以印证。 知道了 fishhook 的工作原理,我们就知道 fishhook 是 hook 不了自己写的 c 函数了。因为自定义函数是没有通过符号去寻找函数真正地址的这个过程。而系统库是通过符号去绑定真实地址的。
## 纠错
之前同事问了个问题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) 还是 TextRO

View File

@@ -28,7 +28,7 @@ NSInteger age = 27;
用指令`xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转为 c++ 用指令`xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转为 c++
<img src="./../assets/BlockViaClangC.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/BlockViaClangC.png" style="zoom:25%">
`ViewController.m` 的 `viewDidLoad` 函数为 `_I_ViewController_viewDidLoad` `ViewController.m` 的 `viewDidLoad` 函数为 `_I_ViewController_viewDidLoad`
@@ -200,7 +200,7 @@ struct __ViewController__viewDidLoad_block_desc_0 {
} }
``` ```
<img src="./../assets/MockBlockDemo.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MockBlockDemo.png" style="zoom:25%">
@@ -214,7 +214,7 @@ struct __ViewController__viewDidLoad_block_desc_0 {
<img src="./../assets/block-structure.png" style="zoom:40%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/block-structure.png" style="zoom:40%">
@@ -237,11 +237,11 @@ printBlock();
用 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 用 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++
<img src="./../assets/SimpleBlockExploreDemo.png" style="zoom:40%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SimpleBlockExploreDemo.png" style="zoom:40%">
概括如下: 概括如下:
<img src="./../assets/SimpleSimpleBlockCaptureStructure.png" style="zoom:40%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SimpleSimpleBlockCaptureStructure.png" style="zoom:40%">
@@ -257,7 +257,7 @@ printAgeBlock();
用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码 用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码
<img src="./../assets/AutoVariableCaptureByBlock.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/AutoVariableCaptureByBlock.png" style="zoom:25%">
代码分析: 代码分析:
@@ -293,7 +293,7 @@ printInfoBlock();
用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码 用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码
<img src="./../assets/StaticVariableCaptureByBlock.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StaticVariableCaptureByBlock.png" style="zoom:25%">
@@ -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++ 代码 用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 代码
<img src="./../assets/GlobalVariableWillNotCaptureByBlock.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/GlobalVariableWillNotCaptureByBlock.png" style="zoom:25%">
@@ -409,7 +409,7 @@ block 截获变量可以分为:
用指令 `xcrun --sdk iphoneos clang -arch arm64 Person.m -rewrite-objc -o Person-arm64.cpp` 转换为 c++ 代码 用指令 `xcrun --sdk iphoneos clang -arch arm64 Person.m -rewrite-objc -o Person-arm64.cpp` 转换为 c++ 代码
<img src="./../assets/BlockWillCaptureMethodSelfVariable.png" style="zoom:25%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/BlockWillCaptureMethodSelfVariable.png" style="zoom:25%">
@@ -442,13 +442,13 @@ block 截获变量可以分为:
我们知道 block 可以看成是一个 oc 对象,所以它有类型,写个 Demo1 验证下 我们知道 block 可以看成是一个 oc 对象,所以它有类型,写个 Demo1 验证下
<img src="./../assets/BlockClassType.png" style="zoom:40%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/BlockClassType.png" style="zoom:40%">
也就是说: `__NSGlobalBlock__` -> `NSBlock` -> `NSObject`。 也就是说: `__NSGlobalBlock__` -> `NSBlock` -> `NSObject`。
继续验证Demo2 继续验证Demo2
<img src="./../assets/OCBlockType.png" style="zoom:40%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OCBlockType.png" style="zoom:40%">
同时利用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++ 同时利用指令 `xcrun --sdk iphoneos clang -arch arm64 ViewController.m -rewrite-objc -o ViewController-arm64.cpp` 转换为 c++
@@ -473,7 +473,7 @@ block 的类型可以通过 isa 或者 class 方法查看,最终都是继承
这3种 block 在内存中的排布如下图: 这3种 block 在内存中的排布如下图:
<img src="./../assets/block-memorylayout.png" style="zoom:40%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/block-memorylayout.png" style="zoom:40%">
@@ -485,7 +485,7 @@ Demo
由于 ARC 默认会做一些优化,我们将工程的 ARC 关掉Build Setting 里 Automatic Reference Counting 设置为 No 由于 ARC 默认会做一些优化,我们将工程的 ARC 关掉Build Setting 里 Automatic Reference Counting 设置为 No
<img src="./../assets/OCStackBlockCrash.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/OCStackBlockCrash.png" style="zoom:30%">
分析: 分析:
@@ -497,7 +497,7 @@ Demo
当 `__NSStackBlock__` 调用 copy 方法后会变为 `__NSMallocBlock__`。如下图: 当 `__NSStackBlock__` 调用 copy 方法后会变为 `__NSMallocBlock__`。如下图:
<img src="./../assets/FixStackBlockIssueWithCopy.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FixStackBlockIssueWithCopy.png" style="zoom:30%">
@@ -531,7 +531,7 @@ Demo 也同时发现,当对 `__NSGlobalBlock__` 调用 copy ,不会变为 `
MRC 下 block 作为函数的返回值是比较危险的。在方法内部,也就是栈上定义的 block函数调用结束后可能一些相关数据就释放了存在潜在风险。 MRC 下 block 作为函数的返回值是比较危险的。在方法内部,也就是栈上定义的 block函数调用结束后可能一些相关数据就释放了存在潜在风险。
<img src="./../assets/BlockAsFunctionReturnValueIsDangerous.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/BlockAsFunctionReturnValueIsDangerous.png" style="zoom:30%">
@@ -539,13 +539,13 @@ MRC 下 block 作为函数的返回值是比较危险的。在方法内部,也
MRC 下如果函数返回值是 block且 block 内做了 auto 变量捕获的逻辑,编译器会报错:`Returning block that lives on the local stack`。此时 block 应该为 `__NSStackBlock__` MRC 下如果函数返回值是 block且 block 内做了 auto 变量捕获的逻辑,编译器会报错:`Returning block that lives on the local stack`。此时 block 应该为 `__NSStackBlock__`
<img src="./../assets/MRCCompileFailedWhenBlockCaptureAutoVarsiableAndAsReturnValue.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MRCCompileFailedWhenBlockCaptureAutoVarsiableAndAsReturnValue.png" style="zoom:30%">
改为 ARC看看 改为 ARC看看
<img src="./../assets/CompileWillCallCopyWhenBlockCaptureAutoVarsiableAndAsReturnValueInARC.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CompileWillCallCopyWhenBlockCaptureAutoVarsiableAndAsReturnValueInARC.png" style="zoom:30%">
也就是说ARC 模式下,当 block 捕获了 auto 变量并且作为函数返回值的时候ARC 会自动调用 copy 方法,将 `__NSStackBlock__` 变为 `__NSMallocBlock__` 也就是说ARC 模式下,当 block 捕获了 auto 变量并且作为函数返回值的时候ARC 会自动调用 copy 方法,将 `__NSStackBlock__` 变为 `__NSMallocBlock__`
@@ -624,11 +624,11 @@ ARC 下:如果 block 调用 copy 方法,则 block 仍旧为 `__NSMallocBloc
MRC 下,栈内捕获了 auto 变量的 block 为 `__NSStackBlock__` MRC 下,栈内捕获了 auto 变量的 block 为 `__NSStackBlock__`
<img src="./../assets/LocalBlockInMRC.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LocalBlockInMRC.png" style="zoom:30%">
改为 ARC 改为 ARC
<img src="./../assets/LocalBlockInARCWillCallCopy.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LocalBlockInARCWillCallCopy.png" style="zoom:30%">
@@ -656,31 +656,31 @@ MRC 下,栈内捕获了 auto 变量的 block 为 `__NSStackBlock__`
ARC 下block 对象捕获了 auto 外部变量,是一种 `__NSMallocBlock__`,捕获的对象将会在 block 销毁后销毁 ARC 下block 对象捕获了 auto 外部变量,是一种 `__NSMallocBlock__`,捕获的对象将会在 block 销毁后销毁
<img src="./../assets/ARCObjectWillReleasedWhenLeaveScope.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ARCObjectWillReleasedWhenLeaveScope.png" style="zoom:30%">
MRC 下block 对象捕获了 auto 外部变量,是一种 `__NSMallocBlock__`,因为是 MRC所以需要手动管理内存。会发现对象将在离开作用域后立马销毁不会被 block 所捕获。 MRC 下block 对象捕获了 auto 外部变量,是一种 `__NSMallocBlock__`,因为是 MRC所以需要手动管理内存。会发现对象将在离开作用域后立马销毁不会被 block 所捕获。
<img src="./../assets/MRCObjectCannotCaptureByMallocBlock.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MRCObjectCannotCaptureByMallocBlock.png" style="zoom:30%">
MRC对 block 加 copy变为 `__NSMallocBlock__` 呢? MRC对 block 加 copy变为 `__NSMallocBlock__` 呢?
<img src="./../assets/MRCObjectWillReleaseWhenBlockIsReleased.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/MRCObjectWillReleaseWhenBlockIsReleased.png" style="zoom:30%">
ARC 下对 block 引用的对象加 `__weak` 修饰呢? ARC 下对 block 引用的对象加 `__weak` 修饰呢?
<img src="./../assets/ARCWeakObjectWillReleaseWhenLeaveScope.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ARCWeakObjectWillReleaseWhenLeaveScope.png" style="zoom:30%">
用指令 `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 参数 用指令 `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 参数
<img src="./../assets/WeakObjectCapturedByBlock.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WeakObjectCapturedByBlock.png" style="zoom:30%">
如果对 Person 不加 `__weak` 修饰block 结构体内部将会是`__strong`。 如果对 Person 不加 `__weak` 修饰block 结构体内部将会是`__strong`。
<img src="./../assets/StrongObjectCapturedByBlock.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StrongObjectCapturedByBlock.png" style="zoom:30%">
@@ -724,11 +724,11 @@ static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_objec
Demo1: Demo1:
<img src="./../assets/ObjectWillReleaseWhenGCDTimeout.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ObjectWillReleaseWhenGCDTimeout.png" style="zoom:30%">
Demo2 Demo2
<img src="./../assets/WeakObjectWillReleaseWhenLeaveScope.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/WeakObjectWillReleaseWhenLeaveScope.png" style="zoom:30%">
@@ -738,13 +738,13 @@ Demo2
Demo3 Demo3
<img src="./../assets/DoubleGCDMainQueueTaskObjectWillReleaseAfterTotalTime.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/DoubleGCDMainQueueTaskObjectWillReleaseAfterTotalTime.png" style="zoom:30%">
因为 GCD 是给 MainQueue 添加任务的所以是串行2个任务前后按照3s、1s 后打印。由于最晚的一个任务是访问强指针对象,所以不会释放。等到 GCD 全部执行完后Person 才释放。 因为 GCD 是给 MainQueue 添加任务的所以是串行2个任务前后按照3s、1s 后打印。由于最晚的一个任务是访问强指针对象,所以不会释放。等到 GCD 全部执行完后Person 才释放。
Demo4 Demo4
<img src="./../assets/DoubleGCDMainQueueTaskObjectWillReleaseAfterStrongReference.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/DoubleGCDMainQueueTaskObjectWillReleaseAfterStrongReference.png" style="zoom:30%">
@@ -829,7 +829,7 @@ MyBlock block = ^{
转为 C++ 转为 C++
<img src="./../assets/BlockChangeVariableUse__Block.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/BlockChangeVariableUse__Block.png" style="zoom:30%">
```c++ ```c++
struct __Block_byref_age_0 { struct __Block_byref_age_0 {
@@ -873,7 +873,7 @@ block 内部的函数在修改 age 的时候其实就是通过 `__main_block_imp
<img src="./../assets/block-forwarding.png" style="zoom:40%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/block-forwarding.png" style="zoom:40%">
@@ -885,7 +885,7 @@ QA为什么`__block` 变量的 `__Block_byref_age_0` 结构体并不在 block
看个有趣的例子,验证下 __block 的效果 看个有趣的例子,验证下 __block 的效果
<img src="./../assets/Block__BLOCKAssignObject.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Block__BLOCKAssignObject.png" style="zoom:30%" />
转换成 c++ 可以看到 转换成 c++ 可以看到
@@ -930,7 +930,7 @@ __attribute__((__blocks__(byref))) __Block_byref_num2_0 num2 = {(void*)0,(__Bloc
对` __block` 修饰的对象clang 转换为 c++ 后如下: 对` __block` 修饰的对象clang 转换为 c++ 后如下:
<img src="./../assets/BlockChangeVariableUse__BlockOnObject.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/BlockChangeVariableUse__BlockOnObject.png" style="zoom:30%">
@@ -943,7 +943,7 @@ __attribute__((__blocks__(byref))) __Block_byref_num2_0 num2 = {(void*)0,(__Bloc
注意: 注意:
<img src="./../assets/NSMutableArrayDonotNeedBlockToUseArray.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSMutableArrayDonotNeedBlockToUseArray.png" style="zoom:30%">
@@ -1013,7 +1013,7 @@ int main(int argc, const char * argv[]) {
我们将断点设置到 NSLog 这里,打印出自定义结构体 `__main_block_impl_0` 中的 age 。 我们将断点设置到 NSLog 这里,打印出自定义结构体 `__main_block_impl_0` 中的 age 。
<img src="./../assets/Block-variableAddress.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Block-variableAddress.png" style="zoom:30%">
```c ```c
// 0x0000000105231f70 // 0x0000000105231f70
@@ -1100,7 +1100,7 @@ in block: age = 28, address is 0x600000464938
<img src="./../assets/BlockChangeOuterValue.png" style="zoom:100%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/BlockChangeOuterValue.png" style="zoom:100%">
分析: 分析:
@@ -1150,7 +1150,7 @@ in block: age = 28, address is 0x600000464938
那么` __forwarding` 的作用是什么?为什么这么设计 那么` __forwarding` 的作用是什么?为什么这么设计
<img src="./../assets/block_forwarding.png" style="zoom:15%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/block_forwarding.png" style="zoom:15%">
@@ -1168,17 +1168,17 @@ in block: age = 28, address is 0x600000464938
Demo1 Demo1
<img src="./../assets/BlockVariableDemo1.png" style="zoom:20%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/BlockVariableDemo1.png" style="zoom:20%">
<img src="./../assets/block-strong-object-memoery.png" style="zoom:30%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/block-strong-object-memoery.png" style="zoom:30%">
Demo2 Demo2
<img src="./../assets/BlockVariableDemo2.png" style="zoom:20%"> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/BlockVariableDemo2.png" style="zoom:20%">
<img src="./../assets/block-weak-object-memoery.png" style="zoom:30%" > <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/block-weak-object-memoery.png" style="zoom:30%" >
@@ -1304,7 +1304,7 @@ p.block();
`__unsafe_retained` 因为不安全所以不推荐,`__block` 因为使用繁琐,且必须等到调用 block 才会释放内存所以不推荐。ARC 下最佳用 `__weak` `__unsafe_retained` 因为不安全所以不推荐,`__block` 因为使用繁琐,且必须等到调用 block 才会释放内存所以不推荐。ARC 下最佳用 `__weak`
![](./../assets/block_object_cycle.png) ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/block_object_cycle.png)

File diff suppressed because it is too large Load Diff

View File

@@ -151,7 +151,7 @@ TCP 会利用另一种机制来解决超时重传带来的时间等待问题,
因为 HTTP 无连接,客户端和服务端交互的时候,打开一个 TCP 连接,然后交互,然后关闭 TCP 连接。下次需要交互的时候,继续打开一个 TCP 连接,继续交互,最后又关闭 TCP 连接。 因为 HTTP 无连接,客户端和服务端交互的时候,打开一个 TCP 连接,然后交互,然后关闭 TCP 连接。下次需要交互的时候,继续打开一个 TCP 连接,继续交互,最后又关闭 TCP 连接。
<img src="./../assets/ HTTPContinuesousLink.png" style="zoom:40%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ HTTPContinuesousLink.png" style="zoom:40%" />
@@ -180,7 +180,7 @@ Session、Cookie 都是针对 HTTP 协议无状态特点的补偿。
Cookie 主要用来记录用户状态,区分用户;状态保存在客户端。 Cookie 主要用来记录用户状态,区分用户;状态保存在客户端。
<img src="./../assets/CookieLifeCycle.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CookieLifeCycle.png" style="zoom:30%" />
客户端请求服务端的时候,服务端生成 Cookie通过 HTTP 响应报文Header 部分中 `Set-Cookie` 首部字段设置 Cookie。 客户端请求服务端的时候,服务端生成 Cookie通过 HTTP 响应报文Header 部分中 `Set-Cookie` 首部字段设置 Cookie。
@@ -214,7 +214,7 @@ Session 也用来记录用户状态,区分用户。只不过状态保存在服
Session 需要依赖于 Cookie 机制。 Session 需要依赖于 Cookie 机制。
<img src="./../assets/SessionWorkflow.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/SessionWorkflow.png" style="zoom:30%" />
@@ -328,7 +328,7 @@ ETag 即 Entity Tag代表资源的唯一标识。主要用来解决修改时
代理体现在头信息上就是字段 `Via`,是一个通用字段,请求头或响应头里都可以出现。每当报文经过一个代理节点,代理服务器就会把自身的信息追加到字段的末尾,就像是经手人盖了一个章。如果通信链路中有很多中间代理,就会在 Via 里形成一个链表,这样就可以知道报文究竟走过了多少个环节才到达了目的地。 代理体现在头信息上就是字段 `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 地址就是客户端的地址 `X-Forwarded-For` 的字面意思是“为谁而转发”形式上和“Via”差不多也是每经过一个代理节点就会在字段里追加一个信息。但“Via”追加的是代理主机名或者域名而“X-Forwarded-For”追加的是请求方的 IP 地址。所以,在字段里最左边的 IP 地址就是客户端的地址
@@ -365,7 +365,7 @@ Host: www.xxx.com\r\n
HTTP 传输链路上,不只是客户端有缓存,服务器上的缓存也是非常有价值的,可以让请求不必走完整个后续处理流程,"就近"获得响应结果。特别是对于那些“读多写少”的数据,例如突发热点新闻、爆款商品的详情页,一秒钟内可能有成千上万次的请求。即使仅仅缓存数秒钟,也能够把巨大的访问流量挡在外面,让 RPSrequest per second降低好几个数量级减轻应用服务器的并发压力对性能的改善是非常显著的。 HTTP 传输链路上,不只是客户端有缓存,服务器上的缓存也是非常有价值的,可以让请求不必走完整个后续处理流程,"就近"获得响应结果。特别是对于那些“读多写少”的数据,例如突发热点新闻、爆款商品的详情页,一秒钟内可能有成千上万次的请求。即使仅仅缓存数秒钟,也能够把巨大的访问流量挡在外面,让 RPSrequest 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 解析查询方式:
- 递归查询,核心就是“我去给你问一下” - 递归查询,核心就是“我去给你问一下”
<img src="./../assets/DNSRecursiveQuery.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/DNSRecursiveQuery.png" style="zoom:30%" />
客户端根据网址去请求服务器之前,会先获取 IP 地址信息。 客户端根据网址去请求服务器之前,会先获取 IP 地址信息。
@@ -486,7 +486,7 @@ DNS 解析查询方式:
- 迭代查询,核心就是“我告诉你谁可能知道” - 迭代查询,核心就是“我告诉你谁可能知道”
<img src="./../assets/DNSIteratorQuery.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/DNSIteratorQuery.png" style="zoom:30%" />
- 客户端发送请求的时候问一下本地 DNS 服务器,该域名对应的 IP 地址是什么,本地 DNS 服务器说“我不知道,你去问问根域名服务器,它可能知道” - 客户端发送请求的时候问一下本地 DNS 服务器,该域名对应的 IP 地址是什么,本地 DNS 服务器说“我不知道,你去问问根域名服务器,它可能知道”
@@ -506,7 +506,7 @@ DNS 解析查询方式:
##### 什么是 DNS 劫持 ##### 什么是 DNS 劫持
<img src="./../assets/DNSHacker.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/DNSHacker.png" style="zoom:30%" />
@@ -531,13 +531,13 @@ QADNS 劫持和 HTTP 的关系是什么?
从“使用 DNS 协议向 DNS 服务器的53端口请求” 变成“使用 HTTP 协议向 HTTP 服务器的80端口请求” 从“使用 DNS 协议向 DNS 服务器的53端口请求” 变成“使用 HTTP 协议向 HTTP 服务器的80端口请求”
<img src="./../assets/HTTPDNSServerProcess.png" style="zoom:25%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/HTTPDNSServerProcess.png" style="zoom:25%" />
客户端通过 IP 直连的方式,向 DNS 服务器,通过 HTTP Get 请求的方式,携带域名参数,然后响应一个具体的 IP 地址值给客户端。剩余流程就是拿着请求后的 IP 地址去完成其他逻辑。 客户端通过 IP 直连的方式,向 DNS 服务器,通过 HTTP Get 请求的方式,携带域名参数,然后响应一个具体的 IP 地址值给客户端。剩余流程就是拿着请求后的 IP 地址去完成其他逻辑。
- 长连接 - 长连接
<img src="./../assets/LongConnectionAvoidDNSHacker.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LongConnectionAvoidDNSHacker.png" style="zoom:30%" />
在客户端和业务服务器之间,建立一个长连接 Server可以理解成代理服务器。 在客户端和业务服务器之间,建立一个长连接 Server可以理解成代理服务器。
@@ -551,7 +551,7 @@ QADNS 劫持和 HTTP 的关系是什么?
#### DNS 解析转发 #### DNS 解析转发
<img src="./../assets/DNSParseRedirect.png" style="zoom:20%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/DNSParseRedirect.png" style="zoom:20%" />
比如某移动 App 发起网络请求,移动 DNS 服务器为了节省资源,将请求转发到某电信 DNS 服务器,用于帮助移动 DNS 服务器,解析域名,获取对应的 IP 地址。 比如某移动 App 发起网络请求,移动 DNS 服务器为了节省资源,将请求转发到某电信 DNS 服务器,用于帮助移动 DNS 服务器,解析域名,获取对应的 IP 地址。

View File

@@ -60,7 +60,7 @@ UDP 不止支持一对一的传输方式,同样支持一对多,多对多,
发送方的 UDP 对应用程序交下来的报文,在添加首部后就向下交付 IP 层。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文 发送方的 UDP 对应用程序交下来的报文,在添加首部后就向下交付 IP 层。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文
<img src="./../assets/UDPOrientedPackage.png" style="zoom:40%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/UDPOrientedPackage.png" style="zoom:40%" />
@@ -96,13 +96,13 @@ UDP 在发送消息时,在传输层直接就将一个消息打包成一个完
复用UDP 多端口复用指的是在 UDP 通信中,可以通过一个 UDP 端口同时处理多个不同的应用程序或服务的通信。这种技术允许多个应用程序共享同一个 UDP 端口进行通信,而不需要为每个应用程序分配独立的端口。通过在接收数据时区分不同的目标端口,可以实现对多个应用程序的数据传输和处理。这种方式可以提高网络资源的利用率和简化网络配置,但需要确保在接收端能够正确解析和处理来自不同端口的数据。 复用UDP 多端口复用指的是在 UDP 通信中,可以通过一个 UDP 端口同时处理多个不同的应用程序或服务的通信。这种技术允许多个应用程序共享同一个 UDP 端口进行通信,而不需要为每个应用程序分配独立的端口。通过在接收数据时区分不同的目标端口,可以实现对多个应用程序的数据传输和处理。这种方式可以提高网络资源的利用率和简化网络配置,但需要确保在接收端能够正确解析和处理来自不同端口的数据。
<img src="./../assets/UDPMultiplePortCommonUse.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/UDPMultiplePortCommonUse.png" style="zoom:30%" />
分用UDP 多端口分用是指在 UDP 通信中,可以通过一个应用程序或服务同时监听和处理多个不同的 UDP 端口。这种技术允许一个应用程序在同一时间接收来自多个不同 UDP 端口的数据包,并根据端口信息来区分和处理这些数据。通过 UDP 多端口分用,一个应用程序可以灵活地处理多个 UDP 端口上的数据,实现更高效的网络通信和数据处理 分用UDP 多端口分用是指在 UDP 通信中,可以通过一个应用程序或服务同时监听和处理多个不同的 UDP 端口。这种技术允许一个应用程序在同一时间接收来自多个不同 UDP 端口的数据包,并根据端口信息来区分和处理这些数据。通过 UDP 多端口分用,一个应用程序可以灵活地处理多个 UDP 端口上的数据,实现更高效的网络通信和数据处理
<img src="./../assets/UDPMultiplePortDivideUse.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/UDPMultiplePortDivideUse.png" style="zoom:30%" />
#### 差错检测 #### 差错检测
@@ -137,7 +137,7 @@ HTTP/1.0 为每次 HTTP 请求/相应都建立一条新的 TCP 链接。因此
#### 为什么需要3次握手 #### 为什么需要3次握手
<img src="./../assets/TCPThressTimesBuild.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TCPThressTimesBuild.png" style="zoom:30%" />
@@ -168,7 +168,7 @@ HTTP/1.0 为每次 HTTP 请求/相应都建立一条新的 TCP 链接。因此
#### 为什么需要4次挥手 #### 为什么需要4次挥手
<img src="./../assets/TCPFourTimesUnBuild.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TCPFourTimesUnBuild.png" style="zoom:30%" />
@@ -211,13 +211,13 @@ HTTP/1.0 为每次 HTTP 请求/相应都建立一条新的 TCP 链接。因此
- 无差错情况 - 无差错情况
<img src="./../assets/TCPStopAndWaitProtocolNormal.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TCPStopAndWaitProtocolNormal.png" style="zoom:30%" />
这张图是正常传输的情况。没有发生任何错误 这张图是正常传输的情况。没有发生任何错误
- 超时重传 - 超时重传
<img src="./../assets/TCPStopAndWaitProtocolResendWhenTimeout.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TCPStopAndWaitProtocolResendWhenTimeout.png" style="zoom:30%" />
这张图是一个超时重传的情况。客户端给服务端发送了 M1 分组报文,由于网络环境比较差,丢失或者滞留,或者被劫持了篡改了,当劫持篡改的情况下服务端接收到 M1 后,会判断篡改后丢弃该报文。在期许的时间内,客户端没有收到分组报文 M1 的确认,认为发生了超时。触发重传机制。然后启动一个分组报文 M1 的重传,此时网络正常,服务端收到 M1并发送确认给客户端。客户端收到确认报文后再发送 M2... 这张图是一个超时重传的情况。客户端给服务端发送了 M1 分组报文,由于网络环境比较差,丢失或者滞留,或者被劫持了篡改了,当劫持篡改的情况下服务端接收到 M1 后,会判断篡改后丢弃该报文。在期许的时间内,客户端没有收到分组报文 M1 的确认,认为发生了超时。触发重传机制。然后启动一个分组报文 M1 的重传,此时网络正常,服务端收到 M1并发送确认给客户端。客户端收到确认报文后再发送 M2...
@@ -227,13 +227,13 @@ HTTP/1.0 为每次 HTTP 请求/相应都建立一条新的 TCP 链接。因此
- 确认丢失 - 确认丢失
<img src="./../assets/TCPStopAndWaitProtocolAckMiss.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TCPStopAndWaitProtocolAckMiss.png" style="zoom:30%" />
客户端发送一个分组报文 M1服务端收到后发送一个确认报文但这个确认报文丢失了。此时客户端依旧通过超时定时器判断在期许的时间内没有收到来自服务端的确认报文触发超时重传策略。重新发送分组报文 M1服务端收到 M1 后,由于服务端已经接收过分组报文 M1 了此时服务端做2件事丢失重传的 M1 报文;重传确认 M1 报文。客户端收到 M1 确认报文,然后继续发送 M2... 客户端发送一个分组报文 M1服务端收到后发送一个确认报文但这个确认报文丢失了。此时客户端依旧通过超时定时器判断在期许的时间内没有收到来自服务端的确认报文触发超时重传策略。重新发送分组报文 M1服务端收到 M1 后,由于服务端已经接收过分组报文 M1 了此时服务端做2件事丢失重传的 M1 报文;重传确认 M1 报文。客户端收到 M1 确认报文,然后继续发送 M2...
- 确认迟到 - 确认迟到
<img src="./../assets/TCPStopAndWaitProtocolAckLater.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TCPStopAndWaitProtocolAckLater.png" style="zoom:30%" />
客户端发送 M1 报文,服务端收到 M1 后发送确认报文,但是由于网络情况不好,传输较慢,客户端在期许时间范围内没有收到确认报文。客户端在超时定时器的作用下,判断超时,触发重传策略。重新发送报文 M1服务端收到重传的 M1 后,由于服务端之前已经接受过 M1 报文所以做2件事情丢弃重传的 M1 报文;重传 M1 确认报文。 客户端发送 M1 报文,服务端收到 M1 后发送确认报文,但是由于网络情况不好,传输较慢,客户端在期许时间范围内没有收到确认报文。客户端在超时定时器的作用下,判断超时,触发重传策略。重新发送报文 M1服务端收到重传的 M1 后,由于服务端之前已经接受过 M1 报文所以做2件事情丢弃重传的 M1 报文;重传 M1 确认报文。
@@ -246,7 +246,7 @@ HTTP/1.0 为每次 HTTP 请求/相应都建立一条新的 TCP 链接。因此
#### 面向字节流 #### 面向字节流
TCP 不像 UDP 那样一个个报文独立传输,而是在不保留报文边界的情况下以字节流方式进行传输。 TCP 不像 UDP 那样一个个报文独立传输,而是在不保留报文边界的情况下以字节流方式进行传输。
<img src="./../assets/TCPOrientedByteBuffer.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TCPOrientedByteBuffer.png" style="zoom:30%" />
发送方在进行数据发送的时候,在 TCP 层面会有一个发送缓存。接收方也有一个接收缓存。 发送方在进行数据发送的时候,在 TCP 层面会有一个发送缓存。接收方也有一个接收缓存。
@@ -279,14 +279,14 @@ TCP 不像 UDP 那样一个个报文独立传输,而是在不保留报文边
从左到右,字节编号,序号逐渐增大。 从左到右,字节编号,序号逐渐增大。
<img src="./../assets/TCPWindowElement.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TCPWindowElement.png" style="zoom:30%" />
- 可用窗口,主要是尽快要发送的部分 - 可用窗口,主要是尽快要发送的部分
- 重传队列,主要是发送但未被确认的部分 - 重传队列,主要是发送但未被确认的部分
当发送但未被确认的部分,收到确认之后,窗口将进行合拢。假设 7、8、9 被发送后,变成下面的状态 当发送但未被确认的部分,收到确认之后,窗口将进行合拢。假设 7、8、9 被发送后,变成下面的状态
<img src="./../assets/TCPWindowMove.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TCPWindowMove.png" style="zoom:30%" />
7、8、9变成了发送未被确认的状态10、11、12变成了需要尽快发送的状态窗口右移。 7、8、9变成了发送未被确认的状态10、11、12变成了需要尽快发送的状态窗口右移。
@@ -302,7 +302,7 @@ TCP 不像 UDP 那样一个个报文独立传输,而是在不保留报文边
在接收方侧: 在接收方侧:
<img src="./../assets/TCPWindowProcess.png" syle="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TCPWindowProcess.png" syle="zoom:30%" />
@@ -322,7 +322,7 @@ TCP 不像 UDP 那样一个个报文独立传输,而是在不保留报文边
##### 慢开始、拥塞避免 ##### 慢开始、拥塞避免
<img src="./../assets/TCPCongestionControl.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TCPCongestionControl.png" style="zoom:30%" />
@@ -342,7 +342,7 @@ TCP 实现可靠传输依赖的是 **超时重传** 机制。TCP 在发送完数
这个是默认的情况,但是传输效率还是有点低,所以 TCP 为了更高的效率,采取了快重传机制。什么是快重传? 这个是默认的情况,但是传输效率还是有点低,所以 TCP 为了更高的效率,采取了快重传机制。什么是快重传?
<img src="./../assets/TCPQuickResendSample.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TCPQuickResendSample.png" style="zoom:30%" />
- 左边是发送方,右边是接收方 - 左边是发送方,右边是接收方
- 发送了序号为1的报文后接收方收到回复了一个 ACK2 的报文ACK2 的意思就是“我收到报文1了接下来我希望收到序号为2的报文” - 发送了序号为1的报文后接收方收到回复了一个 ACK2 的报文ACK2 的意思就是“我收到报文1了接下来我希望收到序号为2的报文”
@@ -366,7 +366,7 @@ TCP 实现可靠传输依赖的是 **超时重传** 机制。TCP 在发送完数
##### 快恢复 ##### 快恢复
<img src="./../assets/TCPSlipWindowProcess.png" style="zoom:40%"/> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TCPSlipWindowProcess.png" style="zoom:40%"/>

View File

@@ -21,7 +21,7 @@
### 对象适配器 ### 对象适配器
<img src="./../assets/ObjectAdapter.png" style="zoom:30%" /> <img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ObjectAdapter.png" style="zoom:30%" />
假设一个类存在年代久远,如果需要适配,则需要创建一个适配对象。然后被适配的对象以成员变量的形式集成到适配对象中。 假设一个类存在年代久远,如果需要适配,则需要创建一个适配对象。然后被适配的对象以成员变量的形式集成到适配对象中。

BIN
assets/.DS_Store vendored

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
assets/AspectMockBlock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
assets/AspectsProcess.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
assets/BlockAndQueue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 KiB

BIN
assets/CALevel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
assets/DLOpenPtrace.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

BIN
assets/DataSignProcess.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
assets/FishHookMachO.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 KiB

BIN
assets/FishHookMachO1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
assets/FishHookMachO2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
assets/FishHookMachO3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
assets/FishHookMachO4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
assets/FishhookResult.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

BIN
assets/NSLogFakeAddress.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

BIN
assets/PTRACEDFlag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 KiB

BIN
assets/PtraceSafeMethod.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Some files were not shown because too many files have changed in this diff Show More