mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-25 04:17:17 +00:00
feature: dyld && LD 链接器
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
# 离屏渲染
|
||||
|
||||
## 什么是离屏渲染
|
||||
什么是在屏渲染?
|
||||
在当前屏幕渲染,指的是 GPU 的渲染操作是在当前用于显示的屏幕缓冲区中进行的。
|
||||
|
||||
如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的 Frame Buffer(帧缓冲区),作为像素数据存储区域,然后由显示控制器把帧缓冲区的数据显示到屏幕上。如果因为面临一些限制,比如阴影、光栅、遮罩等,CPU 无法把渲染结果直接写入 Frame Buffer,而是先暂时把中间的临时状态保存在额外的内存区域,之后再写入 Frame Buffer,那么这个过程被称为离屏渲染。
|
||||
系统如果没有直接把渲染结果直接写入到 GPU FrameBuffer 中,则认为发生了一次离屏渲染。(离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作)
|
||||
|
||||
@@ -60,13 +63,17 @@ self.imageView.layer.cornerRadius = YES;
|
||||
```
|
||||
|
||||
## 离屏渲染的影响?
|
||||
触发离屏渲染后,会增加 GPU 的工作量,CPU + GPU 工作时间可能会超过一次渲染周期,会发生 UI 卡顿。
|
||||
|
||||
离屏渲染,指的是 GPU 在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。由上面的一个结论视图和圆角的大小对帧率并没有什么影响,数量的多少才显著影响性能。可以知道离屏渲染耗时是发生在离屏这个动作上面,而不是渲染。为什么离屏这么耗时?原因主要有创建缓冲区和上下文切换。创建新的缓冲区代价都不算大,付出最大代价的是上下文切换。
|
||||
上下文切换,不管是在GPU渲染过程中,还是广为人知的进程切换,上下文切换都是一个相当耗时的操作。首先要保存当前屏幕渲染环境,然后切换到一个新的绘制环境,申请绘制资源,初始化环境,然后开始一个绘制,绘制完毕后销毁这个绘制环境,如需要切换到On-Screen Rendering 或者再开始一个新的离屏渲染重复之前的操作。
|
||||
|
||||
一次mask发生了两次离屏渲染和一次主屏渲染。即使忽略昂贵的上下文切换,一次mask需要渲染三次才能在屏幕上显示,这已经是普通视图显示3陪耗时,若再加上下文环境切换,一次mask就是普通渲染的n(n>3)倍以上耗时操作
|
||||
一次 mask 发生了两次离屏渲染和一次主屏渲染。即使忽略昂贵的上下文切换,一次mask需要渲染三次才能在屏幕上显示,这已经是普通视图显示3陪耗时,若再加上下文环境切换,一次 mask 就是普通渲染的n(n>3)倍以上耗时操作
|
||||
|
||||
正常流程:App Source Code -> CPU -> Frame Buffer -> Dispaly
|
||||
离屏渲染流程:App Source Code -> CPU -> Off Screen Frame Buffer -> Frame Buffer -> Dispaly
|
||||
|
||||
|
||||
## 如何优化?
|
||||
- 针对 shadow 可以增加 shadowPath
|
||||
- 针对圆角可以增加贝塞尔曲线或者一张图片实现(类似遮罩)。
|
||||
|
||||
@@ -10,7 +10,7 @@ LLVM 不是 low level virtual machine 的缩写,就是项目名称。
|
||||
|
||||
## 结构
|
||||
|
||||

|
||||

|
||||
|
||||
LLVM 由三部分构成:
|
||||
|
||||
@@ -22,7 +22,7 @@ LLVM 由三部分构成:
|
||||
|
||||
|
||||
|
||||

|
||||

|
||||
|
||||
正是由于这样的设计,使得 LLVM 具备很多有点:
|
||||
|
||||
@@ -42,7 +42,7 @@ LLVM 现在被作为实现各种静态和运行时编译语言的通用基础结
|
||||
|
||||
广义上来讲,LLVM 说的是一种架构。狭义上来讲,LLVM 强调的是偏后端部分,如下图的除了 clang 编译前端外的部分,包括优化器和编译后端,统称为 LLVM 后端。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLVMFullStructure.png" style="zoom:45%">
|
||||
<img src="./../assets/LLVMFullStructure.png" style="zoom:45%">
|
||||
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ Clang 相较于 GCC,具备下面优点:
|
||||
|
||||
- 设计清晰简单,容易理解,易于扩展增强
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ clang -ccc-print-phases main.m
|
||||
|
||||
展示如下:
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLVMDisplayPhases.png" style="zoom:45%">
|
||||
<img src="./../assets/LLVMDisplayPhases.png" style="zoom:45%">
|
||||
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ clang -ccc-print-phases main.m
|
||||
|
||||
查看 preprocessor (预处理)的结果:`clang -E main.m`。预处理主要做的事情就是头文件导入( include、import)、宏定义替换等。展示如下:
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLVMPreProcessorPhase.png" style="zoom:35%">
|
||||
<img src="./../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` 查看具体做了什么
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLVMAnalysize.png" style="zoom:35%">
|
||||
<img src="./../assets/LLVMAnalysize.png" style="zoom:35%">
|
||||
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ clang -ccc-print-phases main.m
|
||||
|
||||
语法分析阶段,生成语法树(AST,Abstract Syntax Tree)。使用指令 `clang -fmodules -fsyntax-only -Xclang -ast-dump main.m` 查看
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLVMASTAnalysis.png" style="zoom:35%">
|
||||
<img src="./../assets/LLVMASTAnalysis.png" style="zoom:35%">
|
||||
|
||||
对 main.m 的代码进行改造
|
||||
|
||||
@@ -142,7 +142,7 @@ void test(int a, int b) {
|
||||
|
||||
再次查看 AST 可以加深理解
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLVMASTAnalysis2.png" style="zoom:35%">
|
||||
<img src="./../assets/LLVMASTAnalysis2.png" style="zoom:35%">
|
||||
|
||||
其中:
|
||||
|
||||
@@ -154,7 +154,7 @@ void test(int a, int b) {
|
||||
|
||||
也就是先运算蓝色框内的值,然后用结果和红色框内的进行相减。所以这是很标准的树形结构。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLVMASTTreeDemo.png" style="zoom:10%">
|
||||
<img src="./../assets/LLVMASTTreeDemo.png" style="zoom:10%">
|
||||
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ LLVM IR 有3种表示格式:
|
||||
|
||||
- text:便于阅读的文本格式,类似于汇编语言,推展名为 `.ll`。使用指令 `clang -S -emit-llvm main.m` 进行转换
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLVMIRType1.png" style="zoom:30%">
|
||||
<img src="./../assets/LLVMIRType1.png" style="zoom:30%">
|
||||
|
||||
学过 arm64 汇编的话看这段 IR 很眼熟,汇编里 `load` 相关的指令都是从内存中装载数据,比如 `ldr`、`ldur` 、`ldp`。`store` 相关的指令是往内存中写入数据,比如 `str`、 `stur`、 `stp`
|
||||
|
||||
@@ -304,9 +304,9 @@ Tips:ninja 如果安装失败,可以直接从 [github]( https://github.com/n
|
||||
|
||||
因为要编写 Clang 插件,是 c++ 代码,所以需要借助 IDE 的能力,我们选用 Xcode 进行编译。如下图所示,代表编译成功
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLVMComplieXcode1.png" style="zoom:20%">
|
||||
<img src="./../assets/LLVMComplieXcode1.png" style="zoom:20%">
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLVMComplieXcode2.png" style="zoom:20%">
|
||||
<img src="./../assets/LLVMComplieXcode2.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
@@ -334,11 +334,11 @@ Tips:ninja 如果安装失败,可以直接从 [github]( https://github.com/n
|
||||
|
||||
- 先创建一个插件文件夹 `code-style-validate-plugin`
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLVMAddConfiguration1.png" style="zoom:20%">
|
||||
<img src="./../assets/LLVMAddConfiguration1.png" style="zoom:20%">
|
||||
|
||||
- 编辑 `CMakeLists.txt` 文件,在最后添加 `add_clang_subdirectory(code-style-validate-plugin)`
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLVMAddConfiguration2.png" style="zoom:20%">
|
||||
<img src="./../assets/LLVMAddConfiguration2.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
@@ -367,17 +367,17 @@ Xcode 打开项目,选择自动创建 Schemes
|
||||
|
||||
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeOpenLLVMProject.png" style="zoom:30%">
|
||||
<img src="./../assets/XcodeOpenLLVMProject.png" style="zoom:30%">
|
||||
|
||||
选择 Target 为 `CodeStyleValidatePlugin`,源代码所在文件夹为 `Sources/Loadable modules`,然后选中 CodeStyleValidatePlugin.cpp` 文件进行编写逻辑
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClangPluginSourceCode.png" style="zoom:20%">
|
||||
<img src="./../assets/ClangPluginSourceCode.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
初步编写后 Command + B 进行编译,在 Products 下可以看到编译产物:`CodeStyleValidatePlugin.dylib` 动态库。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClangCompileProducts.png" style="zoom:20%">
|
||||
<img src="./../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++。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeCompileClang.png" style="zoom:20%">
|
||||
<img src="./../assets/XcodeCompileClang.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
@@ -406,7 +406,7 @@ Xcode 打开项目,选择自动创建 Schemes
|
||||
- `-Xclang`
|
||||
- 插件名称
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeLoadPlugin.png" style="zoom:20%">
|
||||
<img src="./../assets/XcodeLoadPlugin.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
@@ -414,7 +414,7 @@ Xcode 打开项目,选择自动创建 Schemes
|
||||
|
||||
在新创建的 TestLLVM Xcode 项目中加载创建的 `CodeStyleValidatePlugin.dylib` 会报错。原因是:由于 Clang 插件需要使用对应的版本去加载,如果版本不一致则会导致编译错误。如下所示:
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeLoadClangPluginError.png" style="zoom:20%">
|
||||
<img src="./../assets/XcodeLoadClangPluginError.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
@@ -426,17 +426,17 @@ Xcode 打开项目,选择自动创建 Schemes
|
||||
|
||||
如下所示:
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeSpecifyClangPath.png" style="zoom:20%">
|
||||
<img src="./../assets/XcodeSpecifyClangPath.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
继续编译还是会报错,报错如下:
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeLoadClangPluginError2.png" style="zoom:20%">
|
||||
<img src="./../assets/XcodeLoadClangPluginError2.png" style="zoom:20%">
|
||||
|
||||
解决方案为:在 `Build Settings` 栏目中搜索 `index`,将 `Enable Index-Wihle-Building Functionality` 的 ` Default` 改为 `NO`。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeLoadClangThenBuildErrorFix.png" style="zoom:20%">
|
||||
<img src="./../assets/XcodeLoadClangThenBuildErrorFix.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
@@ -448,7 +448,7 @@ Tips: 由于重新修改了插件的源码,所以每次 Build 构建完 FANP
|
||||
|
||||
编译成功,可以看到在日志中输出了我们编写的日志信息。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcodeLoadClangPluginTest.png" style="zoom:20%">
|
||||
<img src="./../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`
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClassNameViaClangAST.png" style="zoom:20%">
|
||||
<img src="./../assets/ClassNameViaClangAST.png" style="zoom:20%">
|
||||
|
||||
核心思路为:我们要分析类名不符合规范的情况,要精确报错,首先要识别到类名,利用 AST 的能力可以办到(类名在 AST 的 `ObjCInterfaceDecl` 节点上)。然后获取到类名的行号信息,精确报错。
|
||||
|
||||
@@ -771,7 +771,7 @@ X("CodeStyleValidatePlugin", "This plugin is designed for scanning code styles,
|
||||
- 可以对 Category 名做检测,如果带下划线,则报错提示并给出修改意见
|
||||
- 编写的 `CodeStyleValidatePlugin` Demo 中对不符合规范的做了 `DiagnosticsEngine::Warning` 级别的警告。如果遇到1个警告则不影响,继续编译。如果是 `DiagnosticsEngine::Error` 级别的编译报错,遇到1个则终止编译,请注意该区别,按需编写自己的插件逻辑。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/LLVMClangPluginUseInXcode.png" style="zoom:25%">
|
||||
<img src="./../assets/LLVMClangPluginUseInXcode.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
@@ -819,7 +819,7 @@ X("CodeStyleValidatePlugin", "This plugin is designed for scanning code styles,
|
||||
|
||||
- 使用 LLVM 编写 Clang 插件,解析 AST 拿到所有的 `ObjCInterfaceDecl` 信息,然后结合 `ObjCMethodDecl` 信息便可获取 Category 中的 所有方法,再判断方法是否同名
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ClangASTCategoryMethod.png" style="zoom:20%">
|
||||
<img src="./../assets/ClangASTCategoryMethod.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
> 有人聊起来 NSNotification 可以在不同的线程发和接收吗?对于不知道或者不确定的知识,有必要探究记录下
|
||||
|
||||
|
||||
|
||||
## NSNotificationCenter
|
||||
|
||||
```objectivec
|
||||
@@ -24,6 +26,8 @@
|
||||
|
||||
直接上 GUNStep 源码探索下
|
||||
|
||||
|
||||
|
||||
### Observation
|
||||
|
||||
```c
|
||||
@@ -38,6 +42,8 @@ typedef struct Obs {
|
||||
|
||||
结构体存储了 observer、selector 信息。此外可以看出,是一个链表结构(next),指向注册了同一个通知的下一个观察者。
|
||||
|
||||
|
||||
|
||||
### NCTbl
|
||||
|
||||
```c
|
||||
@@ -55,11 +61,13 @@ typedef struct NCTbl {
|
||||
|
||||
- nemeless:同于保存添加观察者时没有传递 NotificationName 的情况
|
||||
|
||||
|
||||
|
||||
### named Table
|
||||
|
||||
该表用于存储添加观察者时传了 NotificationName 的情况。也就是 Named Table 中,NotificationName 作为 key。在使用系统 API 注册观察者的时候还可以传入 object 参数,表示只监听该对象发出的通知,所以还需要一张表存储 object 和 observer 的对应关系,object 为 key,observer 为 value。
|
||||
|
||||

|
||||

|
||||
|
||||
- 第一个 MapTable key 为 notificationName,value 为另一个 MapTable(子 Table)
|
||||
|
||||
@@ -71,7 +79,7 @@ typedef struct NCTbl {
|
||||
|
||||
nameless Table 结构较为简单,因为没有 notificationName,所以就一层 MapTable。key 为 object,value 为链表,存储所有的观察者。
|
||||
|
||||

|
||||

|
||||
|
||||
### wildcard
|
||||
|
||||
|
||||
@@ -1,5 +1,60 @@
|
||||
# iOS 界面渲染流程
|
||||
|
||||
> 下面几个问题你熟悉吗?
|
||||
>
|
||||
> - 为什么调用 `[UIView serNeedsDisplay]` 并没有立刻发生当前视图的绘制工作?
|
||||
|
||||
|
||||
|
||||
## 视图显示原理
|
||||
|
||||
为什么调用 `[UIView setNeedsDisplay]` 并没有立刻发生当前视图的绘制工作?
|
||||
|
||||
UIView 绘制流程。
|
||||
|
||||
<img src="./../assets/UIViewRefreshProcess.png" style="zoom:60%" />
|
||||
|
||||
|
||||
|
||||
当调用 UIView `[UIView setNeedsDisplay]` 方法时,系统会立刻调用其 Layer 的同名方法 `[view.layer setNeedsDisplay]` 方法,之后相当于给当前 Layer 打上一个脏标记,之后会在当前 RunLoop 快要结束的时候才会调用 Layer 的 `[CALayer display]` 方法。然后进入当前 UIView 真正的绘制流程中。
|
||||
|
||||
其次,会判断 CALayer 的代理,有没有实现 `displayLayer:` 方法,如果没有实现,则进入系统的绘制流程中;如果实现了,则可能是异步绘制或者自定义渲染的实现。
|
||||
|
||||
Tips:`[UIView setNeedsDisplay]` 之后并不会立马调用 `[view.layer setNeedsDisplay]` 方法。要么手动触发 `[view.layer setNeedsDisplay]` 要么,调用一下 `-(void)drawRect:(CGRect)rect` 方法(即使是空实现也没关系)。
|
||||
|
||||
下面来个 Demo 展示下简单的异步绘制一个 String。
|
||||
|
||||
<img src="./../assets/AsyncUILabelRender.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
接下来看看系统的绘制实现流程:
|
||||
|
||||
<img src="./../assets/UIViewSystemRenderProcess.png" style="zoom:60%" />
|
||||
|
||||
如何实现异步绘制?
|
||||
|
||||
`[layer.delegate displayPlayer:]`
|
||||
|
||||
- 代理负责生成对应的 bitmap
|
||||
- 设置该 bitmap 作为 layer.contents 属性的值
|
||||
|
||||
|
||||
|
||||
<img src='./../assets/AsyncRenderProcessAPI.png' style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 渲染机制
|
||||
|
||||

|
||||
|
||||
@@ -346,7 +346,7 @@ Xcode 选择 products,show In Finder。然后上上层的 `Intermediates.noind
|
||||
|
||||
5. 在 `CodeCoverageAnalysis2` 目录下利用指令 `lcov -c -d . -o CodeCoverage2.info` 生成新的一份覆盖率信息 `CodeCoverage2.info`
|
||||
|
||||
6. 然后利用 `locv -a` 指令合并2个 `.info` 文件。指令为 `lcov -a CodeCoverage2.info -a ./../CodeCoverageAnalysis/CodeCoverage.info -o CodeCoverageCombined.info`
|
||||
6. 然后利用 `locv -a` 指令合并2个 `.info` 文件。指令为 `lcov -a CodeCoverage2.info -a https://github.com/FantasticLBP/knowledge-kit/raw/master/CodeCoverageAnalysis/CodeCoverage.info -o CodeCoverageCombined.info`
|
||||
|
||||
7. 然后利用 `genhtml` 生成合并后的覆盖率可视化 html 文件 `genhtml -o html CodeCoverageCombined.info`
|
||||
|
||||
@@ -396,7 +396,7 @@ Ruby 脚本利用 [xcodeproj](https://github.com/CocoaPods/Xcodeproj) 对每个
|
||||
|
||||
```ruby
|
||||
require 'xcodeproj'
|
||||
CONFIG_DIR = Pathname.new(File.join(File.dirname(__FILE__), "../../../..")).realpath
|
||||
CONFIG_DIR = Pathname.new(File.join(File.dirname(__FILE__), ".https://github.com/FantasticLBP/knowledge-kit/raw/master/../..")).realpath
|
||||
CONFIG_FILE = File.join(CONFIG_DIR, "CodeCoverageConfig.rb")
|
||||
|
||||
def update(args)
|
||||
@@ -427,7 +427,7 @@ update(ARGV)
|
||||
代码不变的情况下,发现 QA 或者开发自己测试的情况下,发现代码覆盖率不高,测试没有全面,则继续测试。这样生成多分 `.gcda` 文件,
|
||||
|
||||
- 生成覆盖率:`lcov -c -d {$SOURCE} -o {$DEST_INFO}`,比如 `lcov -c -d . -o CodeCoverage2.info`
|
||||
- 合并覆盖率:`lcov -a {$SOURCE_INFO_1} -a {$SOUCE_INFO_2} -o {$DEST_INFO}`,比如 `lcov -a CodeCoverage2.info -a ./../CodeCoverageAnalysis/CodeCoverage.info -o CodeCoverageCombined.info`
|
||||
- 合并覆盖率:`lcov -a {$SOURCE_INFO_1} -a {$SOUCE_INFO_2} -o {$DEST_INFO}`,比如 `lcov -a CodeCoverage2.info -a https://github.com/FantasticLBP/knowledge-kit/raw/master/CodeCoverageAnalysis/CodeCoverage.info -o CodeCoverageCombined.info`
|
||||
|
||||
|
||||
|
||||
@@ -759,7 +759,7 @@ xcrun llvm-cov show\
|
||||
|
||||
我这边具体指令为:
|
||||
|
||||
`xcrun llvm-cov show -use-color -format=html -arch=x86_64 -instr-profile=SwiftCodeCoverage.profdata SwiftCodeCoverage ./../ -output-dir ./SwiftCodeCoverageReport `
|
||||
`xcrun llvm-cov show -use-color -format=html -arch=x86_64 -instr-profile=SwiftCodeCoverage.profdata SwiftCodeCoverage https://github.com/FantasticLBP/knowledge-kit/raw/master/ -output-dir ./SwiftCodeCoverageReport `
|
||||
|
||||
第十一步,查看整体的覆盖率信息与单个文件的覆盖率,查看代码执行情况
|
||||
|
||||
@@ -791,7 +791,7 @@ xcrun llvm-cov show\
|
||||
|
||||
3. 利用指令将 `.txt` 改为 `.profdata` 格式。`xcrun llvm-profdata merge SwiftCodeCoverageCombined.txt -o SwiftCodeCoverageCombinedFromText.profdata`
|
||||
|
||||
4. 再根据合并后的 `.profdata` 生成 html 覆盖率报告。`xcrun llvm-cov show -use-color -format=html -arch=x86_64 -instr-profile=SwiftCodeCoverageCombinedFromText.profdata SwiftCodeCoverage ./../ -output-dir ./SwiftCodeCoveragCombinedReportFromText`
|
||||
4. 再根据合并后的 `.profdata` 生成 html 覆盖率报告。`xcrun llvm-cov show -use-color -format=html -arch=x86_64 -instr-profile=SwiftCodeCoverageCombinedFromText.profdata SwiftCodeCoverage https://github.com/FantasticLBP/knowledge-kit/raw/master/ -output-dir ./SwiftCodeCoveragCombinedReportFromText`
|
||||
|
||||
效果如下:
|
||||
|
||||
|
||||
@@ -403,3 +403,29 @@ OrderSumitValidatorFactory {
|
||||
|
||||
最后选什么?组合优于继承,个人倾向使用责任链模式去组织代码。关于责任链设计模式的文章也可以看这篇[文章](./../Chapter6%20-%20Design%20Pattern/6.23.md)
|
||||
|
||||
|
||||
|
||||
## 拓展
|
||||
|
||||
如果业务真的是高频迭代变化,但校验顺序不变的话,甚至可以做成多端协定后,对应业务校验编号和业务关联,动态下发
|
||||
|
||||
```json
|
||||
// Version1
|
||||
{
|
||||
"validatorRuleOrder": ["1", "4", "3", "2"]
|
||||
}
|
||||
|
||||
// Version2
|
||||
{
|
||||
"validatorRuleOrder": ["1", "3", "4", "2"]
|
||||
}
|
||||
```
|
||||
|
||||
App 动态请求,然后执行业务逻辑。需思考一些问题:
|
||||
|
||||
- 网络请求慢怎么处理?
|
||||
- 需不需要缓存?
|
||||
- 有缓存的花,更新策略是什么?
|
||||
- 需不需要内置的产品逻辑?
|
||||
|
||||
当然,这不在本篇文章范畴内,不做展开。
|
||||
|
||||
234
Chapter1 - iOS/1.135.md
Normal file
234
Chapter1 - iOS/1.135.md
Normal file
@@ -0,0 +1,234 @@
|
||||
# 框架设计
|
||||
|
||||
|
||||
|
||||
## 图片框架
|
||||
|
||||
### 角色
|
||||
|
||||
- Manager
|
||||
- 内存
|
||||
- 磁盘
|
||||
- 网络
|
||||
- Code Manager
|
||||
- 图片解码
|
||||
- 图片压缩/解压缩
|
||||
|
||||
|
||||
|
||||
### 图片读写过程
|
||||
|
||||
- 以图片 url 的 hash 值为 key,存储
|
||||
|
||||
|
||||
|
||||
### 读取过程
|
||||
|
||||
<img src="./../assets/SDWebImageProcess.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
### 内存设计
|
||||
|
||||
- 内存存储的空间
|
||||
|
||||
- 10kb 以下,使用场景多,设计50张容量
|
||||
- 100kb 以下,使用场景次之,设计20张容量
|
||||
- 100kb 以上,使用场景最小,设计10张容量
|
||||
|
||||
- 淘汰策略
|
||||
|
||||
队列实现,先进先出。
|
||||
|
||||
|
||||
|
||||
### 淘汰策略
|
||||
|
||||
LRU,最近最久未使用算法。比如3天内没有使用过的,则认为需要被淘汰。
|
||||
|
||||
|
||||
|
||||
### 淘汰时机
|
||||
|
||||
- 定时检查,比如 30分钟检查一次
|
||||
|
||||
- 提高检查频率:
|
||||
|
||||
- 每次进行图片读写时
|
||||
- 前后台切换时
|
||||
|
||||
|
||||
|
||||
### 磁盘设计
|
||||
|
||||
- 存储方式
|
||||
- 大小限制(如200MB)
|
||||
- 淘汰策略:如果某一张图片存储时间距今已经超过7天
|
||||
|
||||
|
||||
|
||||
### 网络设计
|
||||
|
||||
- 图片请求的最大并发量
|
||||
- 请求超时策略。超时重试1次,再次超时则取消
|
||||
- 请求优先级
|
||||
|
||||
|
||||
|
||||
### 图片解码
|
||||
|
||||
对于不同格式的图片,图片解码怎么处理?
|
||||
|
||||
应用**策略模式**,对不同图片格式进行解码。一方面可以解码不同格式、另一个方面替换解码算法,对于稳定性有帮助
|
||||
|
||||
在哪个阶段进行解码?
|
||||
|
||||
磁盘读取后、网络请求返回后
|
||||
|
||||
|
||||
|
||||
### 线程处理
|
||||
|
||||
|
||||
|
||||
## 阅读时长记录器
|
||||
|
||||
<img src="./../assets/ReadTimeCounter.png" style="zoom:30%" />
|
||||
|
||||
### 记录器种类
|
||||
|
||||
- 页面式:普通的 push、pop 页面
|
||||
- feed 流式:类似 weibo 这种 feed 流式的记录
|
||||
- 自定义式:可拓展性的体现,面向未来
|
||||
|
||||
|
||||
|
||||
QA:为什么要有不同类型的记录器?
|
||||
|
||||
- 基于不同分类场景提供的关于记录的封装、适配
|
||||
-
|
||||
|
||||
### 记录数据存储
|
||||
|
||||
- 内存缓存
|
||||
- 磁盘存储
|
||||
|
||||
|
||||
|
||||
### 准确性
|
||||
|
||||
数据收集(存储)、上报(移除)2个核心流程。准确性也和这2个方面息息相关。
|
||||
|
||||
- 定时写磁盘 从内存中 flush 到本地磁盘。定时器1分钟 flush 一次
|
||||
- 限定内存缓存条数。超过该条数,即写磁盘。内存记录每满10条 flush 一次
|
||||
|
||||
|
||||
|
||||
### 上传策略
|
||||
|
||||
思考:
|
||||
|
||||
- 需要立马上传吗?每收集到1次页面阅读时长就需要立马上传1次吗?ROI 衡量。性能、线程数
|
||||
- 关于延时上传的场景有哪些?
|
||||
|
||||
|
||||
|
||||
上传时机:
|
||||
|
||||
- 定时器,比如每5分钟上传1次。
|
||||
- 前后台切换,比如从后台切换到前台触发1次上传逻辑
|
||||
- 无网切换到有网
|
||||
|
||||
|
||||
|
||||
### 网络上传效率
|
||||
|
||||
自定义报文,高效传输。
|
||||
|
||||
iOS 小端序,网络大端序。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 复杂页面架构设计
|
||||
|
||||
- MVVM
|
||||
|
||||
- Redux 数据流
|
||||
|
||||
|
||||
|
||||
## 客户端架构
|
||||
|
||||
|
||||
|
||||
## 业务之间解耦后的通信方式
|
||||
|
||||
- openURL
|
||||
- 依赖注入:中间层
|
||||
|
||||
|
||||
|
||||
## AFNetworking
|
||||
|
||||
### 主要类关系图
|
||||
|
||||
<img src="./../assets/AFNetworkingClassArch.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
### AFURLSessionManager
|
||||
|
||||
- 创建和管理 NSURLSession、NSURLSessionTask
|
||||
- 实现 NSURLSessionDelegate 协议代理方法,处理网络请求的重定向、认证、网络数据的处理
|
||||
- 引入 AFSecurityPolicy,用来保证请求安全
|
||||
- 引入 AFNetworkReachabilityManager 监控网络状态
|
||||
|
||||
|
||||
|
||||
## SDWebImage
|
||||
|
||||
### 架构图
|
||||
|
||||
<img src="./../assets/SDWebImageArch.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
## 图片加载流程
|
||||
|
||||
- 查找内存缓存
|
||||
- 查找磁盘缓存
|
||||
- 网络下载图片并磁盘缓存
|
||||
|
||||
|
||||
|
||||
## AsyncDisplayKit
|
||||
|
||||
### 主要处理问题
|
||||
|
||||
主要通过减轻主线程压力,尽量将一些可以放到子线程的任务都放到子线程处理,减轻主线程压力
|
||||
|
||||
主要分3方面:
|
||||
|
||||
- UI 布局 layout:文本宽高计算、视图布局计算
|
||||
- 渲染 Rendering:文本渲染、图片解码、图形绘制
|
||||
- UIKit Objects:对象创建、调整、销毁
|
||||
|
||||
|
||||
|
||||
### 基本原理
|
||||
|
||||
<img src="./../assets/AsyncDisplayKitArch.png" style="zoom:30%" />
|
||||
|
||||
- UIView 作为 CALayer 的代理实现。CALayer 作为 UIView 的实例变量,负责展示规工作。
|
||||
|
||||
- 针对 UIView 的修改,都抽象为针对 ASNode 的修改,这些修改可以在子线程进行。针对 ASNode 的修改和提交,会对其进行封装,提交到一个全局容器中。
|
||||
- 对 Runloop 状态进行监听,进入休眠前,ASDK 执行该 loop 内提交的所有任务。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
142
Chapter1 - iOS/1.136.md
Normal file
142
Chapter1 - iOS/1.136.md
Normal file
@@ -0,0 +1,142 @@
|
||||
|
||||
|
||||
## 多环境配置
|
||||
多环境配置的3种方式:
|
||||
- 多 target 配置
|
||||
- Scheme 多 target 进行环境配置
|
||||
- xconfig 文件配置
|
||||
|
||||
QA:Target、Scheme 的关系是什么?
|
||||
- Project:包含了项目所有的代码、资源文件,所有信息
|
||||
- Scheme:对于指定 Target 的环境配置
|
||||
- Target:对于指定代码和资源文件的具体构建方式
|
||||
|
||||
|
||||
|
||||
## 多环境配置的不同方式
|
||||
|
||||
### 多 Target 的方式
|
||||
|
||||
针对需要多配置的项目,在 Xcode 中,对其进行复制,存在多个 Target。搭配不同的宏定义,来实现控制逻辑的效果。
|
||||
|
||||
注意:duplicate 之后,target 虽然多了一份,但是代码和资源不变,所以
|
||||
|
||||
<img src="./../assets/MultipleTargetProjectConfig.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
<img src="./../assets/XcodeMacroSupportedWithOCAndSwift.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
- OC:Build Settings -> Preprocessor Macros 里面的 Debug/Release 模式下添加自定义宏。比如在 debug 模式下 `IsOCDebug = 1`
|
||||
- Swift:Build Settings -> Other Swift Flags 里的 Debug/Release 模式下添加自宏定义。注意命名有格式要求:`-D + 宏名称`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
当对某个 Target “Duplicate” 之后,会产生一份新的 plist 文件
|
||||
|
||||
<img src="./../assets/MultiplePListAfterDuplicateTarget.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
思考:该方式还是存在的问题:多个 info.plist、配置比较乱
|
||||
|
||||
|
||||
|
||||
### 多 Scheme 的方式
|
||||
|
||||
针对一个 Target 可以添加多个 Scheme,步骤如下
|
||||
|
||||
<img src="./../assets/XcodeAddScheme.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
这样的创建好之后,该 Target 存在3个 Scheme 了。有了 Scheme 有什么作用呢?设置宏定义的时候可以针对不同的 Scheme 进行设置。
|
||||
|
||||
<img src="./../assets/AddMacroForDifferentScheme.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
针对 OC、Swift 分别设置了很多宏定义,接下去需要跑 Beta 配置的代码,怎么办?
|
||||
|
||||
<img src="./../assets/XcodeSwitchSchemeManually.png" style="zoom:30%" />
|
||||
|
||||
点击 Edit Scheme,在 Run 里面选择对应的 Scheme。
|
||||
|
||||
但这样好像蛮烦的,每次运行不同配置的代码,都需要手动切换 Scheme。有没有什么办法解决切换问题呢。
|
||||
|
||||
|
||||
|
||||
创建实体 Scheme
|
||||
|
||||
创建 Scheme 步骤:Xcode -> New Scheme,再弹出的方框内,选择对应的 Target,然后输入需要创建的 Scheme 名称。此次我们创建了:Debug、Beta 2个新的 Scheme。
|
||||
|
||||
<img src="./../assets/XcodeCreateScheme.png" style="zoom:40%" />
|
||||
|
||||
|
||||
|
||||
创建好之后,可以看到实体 Scheme 和虚拟 Scheme 存在多对多的关系。但我们可以基于此,选择实体的 Scheme,然后在 Run 里面 “Build Configuration” 里面选择对应的 Scheme 与之对应。
|
||||
|
||||
<img src="./../assets/XcodeSchemeMatchWithConfigScheme.png" style="zoom:40%" />
|
||||
|
||||
|
||||
|
||||
创建之后就可以根据 Scheme 设置值,在 `Build Settings -> User-Defined` 下添加自定义的字段,同时可以根据 Scheme 设置不同的值。
|
||||
|
||||
设置后的值怎么使用?将自定义的变量用 plist 存储。之后读取再使用。
|
||||
|
||||
完整如下图:
|
||||
|
||||
<img src="./../assets/SetValueUseDifferentSchemeAndUseViaPlist.png" style="zoom:40%" />
|
||||
|
||||
|
||||
|
||||
切换不同的 Scheme,可以运行不同的效果,当前 case 下,选择 Debug Scheme,输出不同结果 `HOST_URL: http://www.debug.baidu.com`
|
||||
|
||||
|
||||
|
||||
思考:目前的方案已经优雅不少,但是还是存在,自定义宏的时候需要选择不同的 Scheme,过程繁琐。
|
||||
|
||||
|
||||
|
||||
### Xcconfig
|
||||
|
||||
Xcode 自带的 Configuration Settings File 可以支持自定义一些宏,还可以修改 Build Settings 里面的选项。
|
||||
|
||||
创建步骤如下:
|
||||
|
||||
<img src="./../assets/XcodeCreateXCConfig.png" style="zoom:30%" />
|
||||
|
||||
文件命名为:`文件夹名称-项目名称.scheme名称.xcconfig`,比如 `Config-Xcconfig.debug.xcconfig`
|
||||
|
||||
几个 Scheme 就创建几个对应的 Xcconfig 文件。
|
||||
|
||||
|
||||
|
||||
修改和完善创建的 Xcconfig 配置文件里的内容。之后在 Xcode 的 Project 选项下,找到 Configurations,选择对应的 Scheme,然后选择右边对应的 Xcconfig 文件。如下图
|
||||
|
||||
<img src="./../assets/XcodeSpecifySchemeWithConfig.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
我们只在 `Config-Xcconfig.debug.xcconfig` 文件中添加了 `OTHER_LDFLAGS = -framework "AFNetworking"`,Xcode 切换到 debug scheme 下,然后 Command + B 编译。
|
||||
|
||||
<img src="./../assets/XcodeDebugXcconfigSpecifyLDLinkFlags.png" style="zoom:30%" />
|
||||
|
||||
验证结果:
|
||||
|
||||
- 编译前切换到 Build Settings 后,查看 ”Other Linker Flags“ 的 Debug 项为空
|
||||
- 编译后切换到 Build Settings 后,查看 ”Other Linker Flags“ 的 Debug 项为 `-framework ”AFNetworking“`
|
||||
|
||||
因为 Xcconfig 文件,具有操作和修改 Build Settings 的能力,所以用好 Xcconfig 文件,不只可以实现替代宏定义和切换繁琐的问题,还可以实现很多其他手动修改 Build Settings 的问题。
|
||||
|
||||
|
||||
|
||||
说明:在 Xcode Build Settings 手动配置的信息,和通过 Xcconfig 方式编写的信息,不会冲突。
|
||||
|
||||
对于 xcconfig 文件,我们其实并不陌生、因为在使用 Cocoapods 的时候就已经在使用这个文件了,只是很多人不知道其中变量的含义。
|
||||
0
Chapter1 - iOS/1.137.md
Normal file
0
Chapter1 - iOS/1.137.md
Normal file
@@ -1,10 +1,49 @@
|
||||
# Swift、OC混编
|
||||
|
||||
```
|
||||
1、在oc文件中使用swift文件。
|
||||
选中项目TARGETS->Building Settings->搜索“Objective-C Genereated Interface Header Name”对应的名字。
|
||||
在oc文件中需要使用swift的地方,头文件导入上一步对应的名字。
|
||||
## apinotes 文件
|
||||
|
||||
|
||||
|
||||
经常在 Swift、OC 混编的时候,系统会给方法命名等做一些优化,比如 OC 侧的枚举,在 Swift 就是结构体。为了代码规范或者某些因素考量,我们需要做一些约定,不让编译器自动处理,比如一些常见的宏:
|
||||
|
||||
- `NS_SWIFT_NAME`
|
||||
- `NS_TYPED_EXTENSIABLE_ENUM`
|
||||
- `NS_REFINED_FOR_SWIFT`
|
||||
|
||||
宏来配置存在弊端,手动去处理一个工程、一个 SDK 的话,假设有10000个方法,工作量太大。
|
||||
|
||||
Xcode 推出解决方案:
|
||||
|
||||
- 创建 `SDK名称.apinotes` 文件
|
||||
- 放到 SDK 根目录下
|
||||
- 按照 yaml 格式,编写内容
|
||||
|
||||
比如:
|
||||
|
||||
```yaml
|
||||
---
|
||||
Name: PersonFramework
|
||||
Classes:
|
||||
- Name: WorkHard
|
||||
# SwiftName: WorkHardAtSwift
|
||||
Methods:
|
||||
- Selector: "upgradeToLeader:"
|
||||
Parameters:
|
||||
- Position: 0
|
||||
Nullability: O
|
||||
MethodKind: Instance
|
||||
SwiftPrivate: true
|
||||
# Availability: nonswift // WorkHard 类的 upgradeToLeader 方法,在 Swift 侧不允许调用
|
||||
#AvailabilityMsg: "prefer 'deinit'" // 如果调用,则提示对应的信息
|
||||
- Selector: "initWithName:"
|
||||
MethodKind: Instance
|
||||
DesignatedInit: true
|
||||
```
|
||||
|
||||
更多格式,请参考 [clang::APINOTES](https://clang.llvm.org/docs/APINotes.html)
|
||||
|
||||
|
||||
|
||||
该方案是 Apple 标准做法,不是骚操作,Objc 源码中也有使用。如下所示
|
||||
|
||||
<img src="./../assets/APINoteInObjcSourceCode.png" style="zoom:30%" />
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# RunLoop 探究
|
||||
|
||||
> 为什么 main 函数可以保持一直运行而不退出?
|
||||
>
|
||||
> 卡顿如何监控
|
||||
|
||||
|
||||
|
||||
## RunLoop 是什么
|
||||
|
||||
- 运行循环
|
||||
@@ -11,7 +17,7 @@
|
||||
|
||||
- 处理App中的各种事件(比如触摸事件、定时器事件等)
|
||||
|
||||
- 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
|
||||
- 节省CPU资源,提高程序性能:该做事时做事,该休息时休息。休息和工作是从用户态切换到内核态,内核态切换到用户态的不断切换
|
||||
|
||||
- ......
|
||||
|
||||
@@ -29,13 +35,17 @@
|
||||
|
||||
先附上一张总结的非常棒的RunLoop图
|
||||
|
||||

|
||||
<img src="./../assets/2019-05-09-RunLoop-review.png" style="zoom:30%" />
|
||||
|
||||
和
|
||||
|
||||

|
||||
|
||||
## RunLoop API
|
||||
一言以蔽之,什么是 RunLoop?为什么 main 函数可以保持一直运行而不退出?
|
||||
|
||||
iOS 侧 main 函数中,调用 UIApplicationMain 方法,内部启动主线程的 RunLoop,RunLoop 是一个事件循环的维护机制。有事情做的时候做事情(Source0、Source1),没有事做的时,从用户态到内核态的切换,去实现线程休眠。避免资源浪费。
|
||||
|
||||
|
||||
|
||||
## RunLoop 几个重要角色
|
||||
|
||||
### 获取 RunLoop
|
||||
|
||||
@@ -65,6 +75,8 @@ NSRunLoop 是对 CFRunLoopRef 的一层 OC 包装,所以要了解 RunLoop 的
|
||||
|
||||
- RunLoop 在第一次获取时创建,在线程结束时消失
|
||||
|
||||
|
||||
|
||||
### RunLoop 相关的5个类
|
||||
|
||||
- CFRunLoopRef
|
||||
@@ -103,6 +115,8 @@ struct __CFRunLoopMode {
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
|
||||
### CFRunLoopModeRef 代表 RunLoop 的运行模式
|
||||
|
||||
- 一个 RunLoop 包含若干个 Mode,每个 Mode 包含若干个 Source/Timer/Observer
|
||||
@@ -110,9 +124,7 @@ struct __CFRunLoopMode {
|
||||
- 如果需要切换 Mode,只能退出 RunLoop,则以一个 Mode 进入
|
||||
- 如果 Mode 里没有任何 Source0/Source1/Timer/Observer,RunLoop 会立马退出
|
||||
|
||||
QA:为什么一个 RunLoop 需要创建这么多 Mode?
|
||||
|
||||
这样做的目的是为了分隔开不同组的 Source/Timer/Observer 互不影响(性能、功能)。
|
||||
|
||||
系统默认注册了5个Mode
|
||||
|
||||
@@ -122,6 +134,26 @@ QA:为什么一个 RunLoop 需要创建这么多 Mode?
|
||||
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
|
||||
- kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
|
||||
|
||||
|
||||
|
||||
Demo:
|
||||
|
||||
<img src="./../assets/NSRunloopRoundWithCFRunloop.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
结论:NSRunLoop 是对 CFRunLoop 的一层包装。
|
||||
|
||||
|
||||
|
||||
QA:为什么一个 RunLoop 需要创建这么多 Mode?
|
||||
|
||||
这样做的目的是为了分隔开不同组的 Source/Timer/Observer 互不影响(性能、功能)。
|
||||
|
||||
在UITableView场景下,不同的 RunLoop Mode 主要是与 UITableView 的滑动优化和事件处理相关的。当 UITableView 滑动时,RunLoop的运行模式会从默认的 CFRunLoopDefaultMode 切换到 CFRunLoopTrackingMode,此 Mode 下 RunLoop 主要关注于处理与滑动相关的触摸事件和动画效果,而忽略其他类型的事件,如定时器事件。这是因为如果同时处理所有类型的事件,可能会导致滑动不流畅,影响用户体验。之前添加到 CFRunLoopDefaultMode 上的事件通知(如定时器事件)可能无法被及时处理,这就是为什么在UITableView 滑动时,添加到主线程的 NSTimer 可能会停止执行的原因。
|
||||
|
||||
|
||||
|
||||
### Source0、Source1、Timer、Observers 是什么
|
||||
|
||||
```c
|
||||
@@ -138,12 +170,18 @@ RunLoop 在各个 Mode 下做事情,其实就是在处理某个 Mode 中的 So
|
||||
|
||||
Source0:
|
||||
|
||||
- 屏幕触摸事件处理(非基于 Port 的事件),对应需要手动触发的事件,对应官方文档Input Source 中的 Custom 和 `performSelector:onThread` 事件源。
|
||||
- 屏幕触摸事件处理(非基于 Port 的事件),对应需要手动触发的事件,对应官方文档 Input Source 中的 Custom 和 `performSelector:onThread` 事件源。
|
||||
|
||||
- `performSelector:onThread:`
|
||||
|
||||
- 数组
|
||||
|
||||
Demo:给屏幕点击事件加断点,查看堆栈可以看到是 Source0 触发的。
|
||||
|
||||
<img src="./../assets/TouchActionByRunLoopSource0.png" style="zoom:25%" />
|
||||
|
||||
|
||||
|
||||
Source1:
|
||||
|
||||
- 基于 Port 的线程间通信,可以主动唤醒 RunLoop
|
||||
@@ -156,7 +194,7 @@ Timers:
|
||||
|
||||
- NSTimer
|
||||
|
||||
- `performSelector:withObject:afterDelay:`
|
||||
- `performSelector:withObject:afterDelay:`,底层也是 Timer
|
||||
|
||||
Observers:
|
||||
|
||||
@@ -179,6 +217,101 @@ CFRunLoopSourceRef 事件源(输入源)
|
||||
- Source0:非基于 port 的,用户主动触发的事件
|
||||
- Source1: 基于 port的,通过内核在线程间相互发送消息
|
||||
|
||||
|
||||
|
||||
### 一对多的关系
|
||||
|
||||
<img src="./../assets/ThreadRunLoopModeStructure.png" style="zoom:70%" />
|
||||
|
||||
|
||||
|
||||
#### RunLoopTimer 的封装
|
||||
|
||||
- `+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;`
|
||||
- `+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;`
|
||||
- `- (void)performSelector:(SEL)aSelector withObject: (id)argument afterDelay: (NSTimeInterval)seconds inModes: (NSArray*)modes;`
|
||||
- `+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;`
|
||||
- `- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;`
|
||||
|
||||
|
||||
|
||||
#### CFRunLoopSource
|
||||
|
||||
Source 是 RunLoop 的数据源抽象类(protocol)
|
||||
|
||||
RunLoop 定义了2个 Version 的 Source:
|
||||
|
||||
- Source0:处理 App 内部事件、App 自己负责管理(触发),如 UIEvent、CGSocket
|
||||
- Source1:由 RunLoop 和内核管理,Mach port 驱动,如 CFMachPort、CFMessagePort。
|
||||
|
||||
定义如下:
|
||||
|
||||
```c++
|
||||
struct __CFRunLoopSource {
|
||||
CFRuntimeBase _base;
|
||||
uint32_t _bits;
|
||||
pthread_mutex_t _lock;
|
||||
CFIndex _order; /* immutable */
|
||||
CFMutableBagRef _runLoops;
|
||||
union {
|
||||
CFRunLoopSourceContext version0; /* immutable, except invalidation */
|
||||
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
|
||||
} _context;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
CFIndex version;
|
||||
void * info;
|
||||
const void *(*retain)(const void *info);
|
||||
void (*release)(const void *info);
|
||||
CFStringRef (*copyDescription)(const void *info);
|
||||
Boolean (*equal)(const void *info1, const void *info2);
|
||||
CFHashCode (*hash)(const void *info);
|
||||
void (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
|
||||
void (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
|
||||
void (*perform)(void *info);
|
||||
} CFRunLoopSourceContext;
|
||||
|
||||
typedef struct {
|
||||
CFIndex version;
|
||||
void * info;
|
||||
const void *(*retain)(const void *info);
|
||||
void (*release)(const void *info);
|
||||
CFStringRef (*copyDescription)(const void *info);
|
||||
Boolean (*equal)(const void *info1, const void *info2);
|
||||
CFHashCode (*hash)(const void *info);
|
||||
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
|
||||
mach_port_t (*getPort)(void *info);
|
||||
void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
|
||||
#else
|
||||
void * (*getPort)(void *info);
|
||||
void (*perform)(void *info);
|
||||
#endif
|
||||
} CFRunLoopSourceContext1;
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### CFRunLoopObserver
|
||||
|
||||
向外部报告 RunLoop 当前状态的更改。框架中很多机制都是由 RunLoopObserver 触发,比如 CAAnimation、AutoReleasePool。
|
||||
|
||||
系统或者开发者很多都是 RunLoop 的业务方。
|
||||
|
||||
|
||||
|
||||
#### CFRunLoopMode
|
||||
|
||||
Mode 是 iOS App 滑动流畅的关键。
|
||||
|
||||
不同任务被添加到不同 Mode 中去。
|
||||
|
||||
UITrackingMode 模式下,核心关注滚动时 UI 流畅相关逻辑。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### CFRunLoopObserverRef 监听 RunLoop 状态变化
|
||||
|
||||
```objective-c
|
||||
@@ -212,7 +345,7 @@ CFRelease(obersver);
|
||||
|
||||
```objective-c
|
||||
//给 RunLoop 添加监听者
|
||||
- (void)testRunLoopObserver{
|
||||
- (void) {
|
||||
|
||||
//创建监听者
|
||||
// CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopAllActivities, <#Boolean repeats#>, <#CFIndex order#>, <#CFRunLoopObserverCallBack callout#>, <#CFRunLoopObserverContext *context#>)
|
||||
@@ -301,7 +434,7 @@ CFRelease(obersver);
|
||||
*/
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
上个实验是在主线程对 RunLoop 进行的监听,但是由于是主线程是由系统创建的,所以系统也创建了对应的主 RunLoop,所以我们看不到 RunLoop 创建的状态,为了模拟完整的状态,我们开启子线程,在子线程中模拟
|
||||
|
||||
@@ -371,20 +504,22 @@ CFRelease(obersver);
|
||||
*/
|
||||
```
|
||||
|
||||
## RunLoop 内部运行原理
|
||||
|
||||

|
||||
|
||||
## RunLoop 运行原理
|
||||
|
||||
### 运行原概要
|
||||
|
||||

|
||||
|
||||
- 图上左上角的 Input source 是早期 RunLoop 的分法,现在分法为:Source0 和 Source1。
|
||||
- Source0:非基于 port 的,用户主动触发的事件。
|
||||
- Source1:基于 port,通过内核和其它线程互相发送消息
|
||||
- RunLoop 我们不能自己手动创建,而是可以通过 [NSRunLoop currentRunLoop] 方法获取,类似于懒加载。系统底层的做法是在全局维护了一个字典,字典的 key 和 value 分别是当前的线程和线程对应的 RunLoop,如果新开辟的线程没有对应的 RunLoop,系统则为其创建 RunLoop,并将其写入字典(线程、为其创建的 RunLoop)
|
||||
|
||||
## RunLoopMode 的概念
|
||||
|
||||

|
||||
|
||||
## 底层实现
|
||||
### 源码探究
|
||||
|
||||
内部就是 do-while 的循环,在这个循环内部不断处理各种任务(Timer、Source、Observer)
|
||||
|
||||
@@ -392,7 +527,7 @@ CFRelease(obersver);
|
||||
|
||||
但是如何直到系统是运行 RunLoop 的哪个函数?给 viewDidLoad 设置断点,在 lldb 模式输入 `bt` 查看堆栈
|
||||
|
||||

|
||||

|
||||
|
||||
查看 CF 中 `CFRunLoop.c`源码。方法比较复杂,做了精简摘要
|
||||
|
||||
@@ -409,6 +544,8 @@ SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterva
|
||||
}
|
||||
```
|
||||
|
||||
`CFRunLoopRunSpecific` 方法就是系统启动 RunLoop 的入口。内部先通知 Runloop 的观察者进入 Runloop 了,然后 调用 `__CFRunLoopRun` 执行核心逻辑(处理 timers、source 事件、block),最后告诉观察者退出 Runloop。
|
||||
|
||||
我们继续看看 `__CFRunLoopRun` 。源码很多很乱,对无关代码进行裁剪,便于理解流程逻辑
|
||||
|
||||
```c
|
||||
@@ -472,10 +609,10 @@ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInter
|
||||
} while (1);
|
||||
// 通知 Observers:结束休眠
|
||||
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
|
||||
|
||||
handle_msg:;
|
||||
|
||||
handle_msg:; // 判断是怎么唤醒的 Runloop
|
||||
__CFRunLoopSetIgnoreWakeUps(rl);
|
||||
|
||||
//
|
||||
if (MACH_PORT_NULL == livePort) {
|
||||
CFRUNLOOP_WAKEUP_FOR_NOTHING();
|
||||
// handle nothing
|
||||
@@ -490,7 +627,7 @@ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInter
|
||||
__CFArmNextTimerInMode(rlm, rl);
|
||||
}
|
||||
}
|
||||
|
||||
// 被 Timer 唤醒,执行代码。
|
||||
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
|
||||
CFRUNLOOP_WAKEUP_FOR_TIMER();
|
||||
// On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
|
||||
@@ -532,8 +669,7 @@ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInter
|
||||
|
||||
voucher_mach_msg_revert(voucherState);
|
||||
os_release(voucherCopy);
|
||||
|
||||
} while (0 == retVal);
|
||||
} while (0 == retVal); // 当 retVal == 0 的时候结束 Runloop
|
||||
return retVal;
|
||||
}
|
||||
```
|
||||
@@ -931,9 +1067,7 @@ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInter
|
||||
}
|
||||
```
|
||||
|
||||
__CFRunLoopModeIsEmpty
|
||||
|
||||
此函数的作用就是判断这个 Mode 下面有没有 source0、source1、timer,只要存在就说明当前 Mode 不是空的,同时看看这个 Mode 是不是属于当前的 RunLoop
|
||||
`__CFRunLoopModeIsEmpty`函数的作用就是判断这个 Mode 下面有没有 source0、source1、timer,只要存在就说明当前 Mode 不是空的,同时看看这个 Mode 是不是属于当前的 RunLoop
|
||||
|
||||
```objective-c
|
||||
// expects rl and rlm locked
|
||||
@@ -993,9 +1127,51 @@ __CFRunLoopModeIsEmpty
|
||||
}
|
||||
```
|
||||
|
||||
## RunLoop 休眠原理
|
||||
|
||||
本质上就是函数 `__CFRunLoopServiceMachPort` 来控制实现休眠。查看 CFRunLoop.c 源码可以发现,是由 `mach_msg` 实现的。不是平时的在应用层 API 上 sleep 休眠的。比如 while 循环。`mach_msg` 休眠是从用户态切换到内核态,内核 api 控制休眠,做到真正节省影响的作用,等到由新消息来到,继续切换到用户态。
|
||||
|
||||
RunLoop 内部几个核心的动作:`__CFRunLoopDoObservers`、`__CFRunLoopServiceMachPort` 、`__CFRunLoopDoTimers` 方法内部实现调用的还是 `__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__`、`__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__` 等方法,均以 `__CFRUNLOOP_IS_CALLING_OUT_TO_` 方法名作为开头。可以在堆栈上得以体现。
|
||||
|
||||
|
||||
|
||||
### 运行流程
|
||||
|
||||
上面结合源码看了 Runloop 是怎么运行的。下面通过图片看看 RunLoop 各个状态运行切换的完整流程。
|
||||
|
||||
<img src="./../assets/RunLoop-SourceCode.png" style="zoom:30%" />
|
||||
|
||||
Demo:
|
||||
|
||||
1. 上面第4步的 blocks 是指可以给 RunLoop 添加 Block 任务。
|
||||
|
||||
```objective-c
|
||||
CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopDefaultMode, ^{
|
||||
NSLog(@"runloop block task");
|
||||
});
|
||||
```
|
||||
|
||||
2. 上面8>2,Runloop 处理 GCD Async To Main Quque
|
||||
|
||||
<img src="./../assets/RunLoopPerformTaskFromGCDMainQueue.png" style="zoom:25%" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### RunLoop 休眠原理
|
||||
|
||||
> Runloop 在处理完 timer、source、block 后会检查有没有 source1 事件,没有则休眠。这个休眠是 while 循环死等吗?怎么实现的?
|
||||
|
||||
可以在 App 运行过程中,点击 Xcode 左下角的 debug 暂停按钮,可以看到 App 堆栈存在系统调用
|
||||
|
||||
<img src="./../assets/RunLoopSleepSystemCall.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
本质上就是函数 `__CFRunLoopServiceMachPort` 来控制实现休眠。查看 CFRunLoop.c 源码可以发现,是由 `mach_msg` 实现的。不是平时的在应用层 API 上 sleep 休眠的。比如 while 循环。
|
||||
|
||||
**`mach_msg` 休眠是从用户态切换到内核态,内核 api 控制休眠,做到真正节省资源的作用,等到由新消息来到,继续切换到用户态**。能力更底层,效果更好,从而更加省电
|
||||
|
||||
```c
|
||||
static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) {
|
||||
@@ -1050,6 +1226,16 @@ static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### RunLoop 是如何响应用户操作的?
|
||||
|
||||
用户交互事件首先在 IOHID 层生成 HIDEvent,然后向事件处理线程的 Source1 的 mach port 发送 HIDEvent 消息,Source1 的回调函数将事件转化为 UIEvent 并筛选需要处理的事件推入待处理事件队列,向主线程的事件处理 Source0 发送信号,并唤醒主线程,主线程检查到事件处理 Source0 有待处理信号后,触发 Source0 的回调函数,从待处理事件队列中提取 UIEvent,最后进入 hit-test 等 UIEvent 事件响应流程
|
||||
|
||||
等待梳理完善。
|
||||
|
||||
|
||||
|
||||
## CFRunLoopTimerRef 是基于时间的触发器
|
||||
|
||||
- 基本上说就是 NSTimer,它会收到 RunLoopMode 的影响
|
||||
@@ -1170,6 +1356,8 @@ if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentM
|
||||
// ...
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Mach Port 跨线程通信
|
||||
|
||||
1. Mach IPC 基于 Mach 内核实现进程间通讯。
|
||||
@@ -1310,6 +1498,8 @@ if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentM
|
||||
|
||||
可以看到每次在点击屏幕时调用 `CFRunLoopWakeUp` 尝试唤醒 RunLoop,然后监听 RunLoop 的 _wakeUpPort,都可以在回调中获取到消息。
|
||||
|
||||
|
||||
|
||||
## RunLoop 应用场景
|
||||
|
||||
- 控制线程生命周期(线程保活)
|
||||
@@ -1320,6 +1510,8 @@ if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentM
|
||||
|
||||
- 性能优化
|
||||
|
||||
|
||||
|
||||
### NSTimer 经常会不准确,原因是什么?
|
||||
|
||||
NSTimer 在创建的时候经常会指派到特定的 NSRunLoopMode 中去,举个例子,默认创建的NSTimer 是被添加到 NSRunLoopDefaultMode 中去,当你的页面上有 UIScrollView 或者子类的时如果被拖动了,当前 RunLoop 的 NSRunloopMode 会从 NSDefaultRunLoopMode 转变为 UITrackingRunLoopMode 。遇到这种情况你需要精确的 NSTimer 的话,在创建好 NSTimer 之后,设置 RunLoopMod 为 NSRunLoopCommonModes。
|
||||
@@ -1388,23 +1580,33 @@ NSTimer 会受 NSRunLoopMode 影响,GCD 的 timer 则不会。
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
|
||||
### ImageView显示(PerformSelector)
|
||||
|
||||
UITableView 在滚动的时候一个优化点之一就是 UIImageView 的显示,通常需要根据网络去下载图片。所以如果用户快速滚动列表的时候,如果立马下载并显示图片的话,势必会对 UI 的刷新产生影响,直观的表现就是会卡顿,**FPS** 达不到60。
|
||||
|
||||
利用 RunLoop 可以实现这个效果,就是给下载并显示图片的方法指定 **NSRunLoopMode**。
|
||||
UITableView 在滚动的时候一个优化点之一就是 UIImageView 的显示,通常需要根据网络去下载图片。所以如果用户快速滚动列表的时候,如果立马下载并显示图片的话,势必会对 UI 的刷新产生影响,直观的表现就是会卡顿,FPS 达不到60。
|
||||
|
||||
```objectivec
|
||||
- (void)downloadAndShowImage{
|
||||
self.imageview.image = [UIImage imageNamed:@"test"];
|
||||
}
|
||||
|
||||
- (IBAction)clickLoadIMage:(id)sender {
|
||||
//[self.imageview performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"test"] afterDelay:2];
|
||||
[self performSelector:@selector(downloadAndShowImage) withObject:nil afterDelay:2 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]];
|
||||
}
|
||||
|
||||
- (void)downloadAndShowImage{
|
||||
self.imageview.image = [UIImage imageNamed:@"test"];
|
||||
}
|
||||
```
|
||||
|
||||
知道 RunLoop 的工作原理,就清楚 UITableView(任何 UIScrollView 子类)在滚动的时候,RunLoop 会处于 `UITrackingRunLoopMode`,那么可以将图片下载或者解码显示的逻辑放到 `NSDefaultRunLoopMode` 中
|
||||
|
||||
```objective-c
|
||||
[self performSelector:@selector(downloadAndShowImage) withObject:nil afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 自动释放池
|
||||
|
||||
自动释放池什么时候创建和释放?
|
||||
@@ -1421,6 +1623,8 @@ App 启动后,主线程 RunLoop 里注册了2个 Observer,其回调都是 `_
|
||||
|
||||
总结版:在主线程执行的代码,通常是写在事件回调、Timer 回调内的,这些回调都会被 RunLoop 自身状态相关的 AutoreleasePool 所包裹,所以会自动管理内存,开发者不需要手动创建 AutoreleasePool。
|
||||
|
||||
|
||||
|
||||
### 事件响应
|
||||
|
||||
系统注册了 Source1(基于 Mach port)用来接收系统事件,其回调函数为 `__IOHIDEventSystemClientQueueCallback`
|
||||
@@ -1429,18 +1633,24 @@ App 启动后,主线程 RunLoop 里注册了2个 Observer,其回调都是 `_
|
||||
|
||||
`_UIApplicationHandleEventQueue` 会把 `IOHIDEvent` 处理并包装成 UIEvent 进行处理和分发(其中包括 UIGesture、屏幕旋转等)。
|
||||
|
||||
|
||||
|
||||
### 手势识别
|
||||
|
||||
`_UIApplicationHandleEventQueue` 识别到一个手势时,首先会调用 cancel 将当前的 touchBegin/End/Move 系统回调打断,然后系统会将对应的 `UIGestureRecognizer ` 标记为待处理。
|
||||
|
||||
苹果注册了一个 Observer 监控 RunLoop 的 `kCFRunLoopBeforeWaiting`(将要休眠)状态,回调为 `_UIGestureRecognizerUpdateObserver`,其内部会获取所有刚被标记为待处理的 UIGestureRecognizer,并执行对应的回调。
|
||||
|
||||
|
||||
|
||||
### UI 刷新
|
||||
|
||||
当界面的 Frame 改变,或者更改 UIView、CALayer 的层次时,或者调用了 UIView、CALayer 的 setNeedsLayout、setNeedsDisplay 方法后,这个 UIView、CALayer 会被标记为待处理(类比前端的 Virtual Dom Diff,标记为 dirty),并被提交到一个全局容器中。
|
||||
|
||||
苹果设计 UI 更新也是 RunLoop 的业务方,所以会注册一个 Obserger 监控 `kCFRunLoopBeforeWaiting`(将要休眠)和 `kCFRunLoopExit` (即将退出 RunLoop)状态,然后会执行 `_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()` 回调。内部会遍历所有待处理的 UIView、CALayer 以执行实际的绘制和渲染,更新 UI
|
||||
|
||||
|
||||
|
||||
### RunLoop 空闲时做一些任务
|
||||
|
||||
```objectivec
|
||||
@@ -1465,6 +1675,8 @@ App 启动后,主线程 RunLoop 里注册了2个 Observer,其回调都是 `_
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Crash 防护
|
||||
|
||||
利用监控手段,比如 C/OC crash、Signal、Mach 异常,当监控到异常之后,正常来说会发生闪退等,体验较差。某些场景下希望 App 从异常中恢复,重新启动,这个可以利用 RunLoop 实现。
|
||||
@@ -1494,37 +1706,162 @@ CFRelease(allModes);
|
||||
|
||||
### 线程保活
|
||||
|
||||
为什么线程做完事情就会退出?
|
||||
|
||||
NSThread 的一个工作流程如下:
|
||||
|
||||
`start() -> 创建 pthread -> main() -> [target performSelector:selector] -> exit`
|
||||
|
||||
NSThread 需要保活。为什么会死掉?看看 gnu 源码
|
||||
|
||||
```c++
|
||||
- (void) start
|
||||
{
|
||||
pthread_attr_t attr;
|
||||
|
||||
if (_active == YES)
|
||||
{
|
||||
[NSException raise: NSInternalInconsistencyException
|
||||
format: @"[%@-%@] called on active thread",
|
||||
NSStringFromClass([self class]),
|
||||
NSStringFromSelector(_cmd)];
|
||||
}
|
||||
if (_cancelled == YES)
|
||||
{
|
||||
[NSException raise: NSInternalInconsistencyException
|
||||
format: @"[%@-%@] called on cancelled thread",
|
||||
NSStringFromClass([self class]),
|
||||
NSStringFromSelector(_cmd)];
|
||||
}
|
||||
if (_finished == YES)
|
||||
{
|
||||
[NSException raise: NSInternalInconsistencyException
|
||||
format: @"[%@-%@] called on finished thread",
|
||||
NSStringFromClass([self class]),
|
||||
NSStringFromSelector(_cmd)];
|
||||
}
|
||||
|
||||
/* Make sure the notification is posted BEFORE the new thread starts.
|
||||
*/
|
||||
gnustep_base_thread_callback();
|
||||
|
||||
/* The thread must persist until it finishes executing.
|
||||
*/
|
||||
RETAIN(self);
|
||||
|
||||
/* Mark the thread as active while it's running.
|
||||
*/
|
||||
_active = YES;
|
||||
|
||||
errno = 0;
|
||||
pthread_attr_init(&attr);
|
||||
/* Create this thread detached, because we never use the return state from
|
||||
* threads.
|
||||
*/
|
||||
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
||||
/* Set the stack size when the thread is created. Unlike the old setrlimit
|
||||
* code, this actually works.
|
||||
*/
|
||||
if (_stackSize > 0)
|
||||
{
|
||||
pthread_attr_setstacksize(&attr, _stackSize);
|
||||
}
|
||||
// 设置回调函数
|
||||
if (pthread_create(&pthreadID, &attr, nsthreadLauncher, self))
|
||||
{
|
||||
DESTROY(self);
|
||||
[NSException raise: NSInternalInconsistencyException
|
||||
format: @"Unable to detach thread (last error %@)",
|
||||
[NSError _last]];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
看看 pthread 创建后的回调函数
|
||||
|
||||
```c++
|
||||
static void *
|
||||
nsthreadLauncher(void *thread)
|
||||
{
|
||||
NSThread *t = (NSThread*)thread;
|
||||
|
||||
setThreadForCurrentThread(t);
|
||||
|
||||
/*
|
||||
* Let observers know a new thread is starting.
|
||||
*/
|
||||
if (nc == nil)
|
||||
{
|
||||
nc = RETAIN([NSNotificationCenter defaultCenter]);
|
||||
}
|
||||
// 发送通知
|
||||
[nc postNotificationName: NSThreadDidStartNotification
|
||||
object: t
|
||||
userInfo: nil];
|
||||
// 设置线程名
|
||||
[t _setName: [t name]];
|
||||
// 调用 main 方法
|
||||
[t main];
|
||||
// 线程退出
|
||||
[NSThread exit];
|
||||
// Not reached
|
||||
return NULL;
|
||||
}
|
||||
```
|
||||
|
||||
看了源码,会发现 NSThread 调用 start 内部就会调用 `[NSThread exit]` 所以会退出。要想常驻,就需要在 main 方法做 runloop 保活。
|
||||
|
||||
```c++
|
||||
- (void) main
|
||||
{
|
||||
if (_active == NO)
|
||||
{
|
||||
[NSException raise: NSInternalInconsistencyException
|
||||
format: @"[%@-%@] called on inactive thread",
|
||||
NSStringFromClass([self class]),
|
||||
NSStringFromSelector(_cmd)];
|
||||
}
|
||||
|
||||
[_target performSelector: _selector withObject: _arg];
|
||||
}
|
||||
```
|
||||
|
||||
main 方法内其实就是在执行 _selector。也就是在 NSThread 的初始化方法中,传入的 selector 中进行 runloop 保活逻辑。
|
||||
|
||||
|
||||
|
||||
应用场景:经常在子线程中处理某些逻辑的场景。如果销毁再创建再销毁再创建效率很低,这个情况下就需要线程保活。
|
||||
|
||||
```objectivec
|
||||
@interface LBPThread : NSThread
|
||||
```objective-c
|
||||
@interface LifeThread : NSThread
|
||||
@end
|
||||
@implementation LBPThread
|
||||
@implementation LifeThread
|
||||
- (void)dealloc{
|
||||
NSLog(@"%s", __func__);
|
||||
}
|
||||
@end
|
||||
|
||||
@interface ViewController ()
|
||||
@property (nonatomic, strong) LBPThread *task;
|
||||
@property (nonatomic, strong) LifeThread *thread;
|
||||
@end
|
||||
|
||||
@implementation ViewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.task = [[LBPThread alloc] initWithTarget:self selector:@selector(run) object:nil];
|
||||
[self.task start];
|
||||
self.thread = [[LifeThread alloc] initWithTarget:self selector:@selector(run) object:nil];
|
||||
[self.thread start];
|
||||
}
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
|
||||
[self performSelector:@selector(test) onThread:self.task withObject:nil waitUntilDone:NO];
|
||||
}
|
||||
- (void)test{
|
||||
NSLog(@"沿用保活的线程,做事情:%@", [NSThread currentThread]);
|
||||
NSLog(@"沿用保活的线程,处理任务:%@", [NSThread currentThread]);
|
||||
}
|
||||
// 该方法仅用于线程保活
|
||||
- (void)run{
|
||||
NSLog(@"%s %@", __func__, [NSThread currentThread]);
|
||||
// 给 RunLoop 的某个 Mode 里添加 Source/Timer/Observer
|
||||
// 给 RunLoop 的某个 Mode 里添加 Source/Timer/Observer。其中 addPort 就是 Source1
|
||||
[[NSRunLoop currentRunLoop] addPort:[[NSMachPort alloc] init] forMode:NSDefaultRunLoopMode];
|
||||
[[NSRunLoop currentRunLoop] run];
|
||||
NSLog(@"task finished");
|
||||
@@ -1534,7 +1871,7 @@ CFRelease(allModes);
|
||||
|
||||
默认创建的 NSThread 会在 NSDefaultRunLoopMode 模式下运行,当 UI 滑动则进入 UITrackingMode 模式,所以 NSThread 的方法会停止。
|
||||
|
||||
线程保活就是给方法内部添加 RunLoop,但是新创建的 RunLoop 运行肯定是基于某个 Mode,RunLoop 持续运行(保活)的前提是必须有 Timer、Sources、Observer。所以添加了 NSMachPort。
|
||||
线程保活就是给方法内部添加 RunLoop,但是新创建的 RunLoop 运行肯定是基于某个 Mode,RunLoop 持续运行(保活)的前提是必须有 Timer、Sources、Observer。所以在 Demo 中我们添加了 `NSMachPort`。
|
||||
|
||||
上面的代码存在问题:
|
||||
|
||||
@@ -1542,67 +1879,64 @@ CFRelease(allModes);
|
||||
|
||||
2. LBPThread 线程不会死亡,假如我们需要在某个时机让保活线程销毁,现在是办不到的
|
||||
|
||||
3. RunLoop 不会停止
|
||||
|
||||
改进:
|
||||
|
||||
1. Thread 换种 api `-(instancetype)initWithBlock:(void (^)(void))block`,线程不持有 self
|
||||
|
||||
2. `[[NSRunLoop currentRunLoop] run]` api 换掉。查看系统说明,底层其实就是一个无限循环,循环内部不断调用 `runMode:beforeDate:`。下面也有建议,建议我们想销毁 RunLoop,可以替换 API,比如设置一个变量,标记是否需要结束 RunLoop
|
||||
|
||||

|
||||
<img src="./../assets/RunLoop-RunIssue.png" style="zoom:45%" />
|
||||
|
||||
改进代码如下
|
||||
|
||||
```objectivec
|
||||
@interface ViewController ()
|
||||
@property (strong, nonatomic) LBPThread *task;
|
||||
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
|
||||
@end
|
||||
```objective-c
|
||||
__weak ViewController *weakself = self;
|
||||
self.thread = [[LifeThread alloc] initWithBlock:^{
|
||||
NSLog(@"RunLoop Start");
|
||||
[[NSRunLoop currentRunLoop] addPort:[[NSMachPort alloc] init] forMode:NSDefaultRunLoopMode];
|
||||
while (weakself && !weakself.needStopThread) {
|
||||
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
|
||||
}
|
||||
NSLog(@"RunLoop Stop");
|
||||
}];
|
||||
[self.thread start];
|
||||
|
||||
@implementation ViewController
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
__weak typeof(self) weakSelf = self;
|
||||
self.stopped = NO;
|
||||
self.task = [[LBPThread alloc] initWithBlock:^{
|
||||
NSLog(@"%@----begin----", [NSThread currentThread]);
|
||||
// 往RunLoop里面添加Source\Timer\Observer
|
||||
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
|
||||
while (weakSelf && !weakSelf.isStoped) {
|
||||
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
|
||||
}
|
||||
NSLog(@"%@----end----", [NSThread currentThread]);
|
||||
}];
|
||||
[self.task start];
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||
if(!self.thread) return;
|
||||
[self performSelector:@selector(threadTask) onThread:self.thread withObject:nil waitUntilDone:YES];
|
||||
}
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
|
||||
{
|
||||
if (!self.task) return;
|
||||
[self performSelector:@selector(test) onThread:self.task withObject:nil waitUntilDone:NO];
|
||||
#pragma mark - 线程相关
|
||||
- (void)threadTask {
|
||||
NSLog(@"线程任务 %@", [NSThread currentThread]);
|
||||
}
|
||||
// 子线程需要执行的任务
|
||||
- (void)test {
|
||||
NSLog(@"沿用保活的线程,做事情:%@", [NSThread currentThread]);
|
||||
|
||||
- (void)stopThread {
|
||||
if(!self.thread) return;
|
||||
[self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:YES];
|
||||
}
|
||||
- (IBAction)stop {
|
||||
if (!self.task) return;
|
||||
// 在子线程调用stop
|
||||
[self performSelector:@selector(stopThread) onThread:self.task withObject:nil waitUntilDone:YES];
|
||||
}
|
||||
// 用于停止子线程的RunLoop
|
||||
- (void)stopThread{
|
||||
// 设置标记为NO
|
||||
self.stopped = YES;
|
||||
// 停止RunLoop
|
||||
|
||||
- (void)stop {
|
||||
self.needStopThread = YES;
|
||||
CFRunLoopStop(CFRunLoopGetCurrent());
|
||||
NSLog(@"%s %@", __func__, [NSThread currentThread]);
|
||||
self.task = nil;
|
||||
self.thread = nil;
|
||||
}
|
||||
- (void)dealloc{
|
||||
|
||||
- (void)dealloc {
|
||||
NSLog(@"%s", __func__);
|
||||
[self stopThread];
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
效果如下:
|
||||
|
||||
<img src="./../assets/RunLoopThreadKeepLive.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
注意:
|
||||
|
||||
- 如果 `stop` 方法内部的 `waitUntilDone` 为 NO,则会出现 Crash。因为该参数代表后续代表会不会等该 selector 执行完毕。因为为 NO,所以 ViewController 执行 dealloc 了,所以 `dealloc` 方法和 Thread 内部的 block 同时进行,不能确保在 block 内部执行的时候 dealloc 有没有执行完,访问 weakSelf.isStoped 可能会 crash
|
||||
@@ -1629,16 +1963,9 @@ CFRelease(allModes);
|
||||
}
|
||||
```
|
||||
|
||||
线程保活后如何暂停?
|
||||
线程保活的目的是保证线程处于激活状态,而不是使用强指针让线程不要释放。为让其处于激活状态就需要使用 RunLoop。
|
||||
|
||||
```
|
||||
[thread cancel];
|
||||
thread = nil;
|
||||
// 指针 nil,还是被 RunLoop 持有。
|
||||
// 也不行。CFRunLoopStop([NSRunLoop currentRunLoop].getCFRunLoop);
|
||||
```
|
||||
|
||||
主线程为什么
|
||||
|
||||
### 线程封装
|
||||
|
||||
@@ -1751,3 +2078,124 @@ typedef void (^LBPPermenantThreadTask)(void);
|
||||
}
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 卡顿监控
|
||||
|
||||
RunLoop 监控卡顿,可以查看 [带你打造一套 APM 系统](./1.4.md) 文章
|
||||
|
||||
|
||||
|
||||
### AsyncDisplayKit
|
||||
|
||||
卡顿主要原因是 CPU/GPU 高负荷工作(mask/cornerRadius/drawrect/opaque 带来的 offscreen rendering/blending 等),或者任务在时间分配下不均衡。
|
||||
|
||||
Autolayout 布局性能瓶颈。约束的计算会随着 View 数量和层级的增长呈指数级增长,且必须在主线程执行。
|
||||
|
||||
并行效率低。大多数情况下,主线程繁忙,其他子线程空余。所以思路是把主线程的任务转移一部分给其他线程进行异步处理,主线程带来性能提升
|
||||
|
||||
AsyncDisplayKit 主要针对:
|
||||
|
||||
- 渲染:对于大量文字、图片混合在一起时,而文字区域的大小和布局,恰恰依赖着渲染结果。ASDK 尽可能走后台线程进行渲染,完成后再同步回到主线程相应的 UIView
|
||||
- 布局。ASDK 抛弃了 Autolayout,实现了自己的布局和缓存
|
||||
- 系统对象的创建和销毁。UIKit 封装了 CALayer 以支持出没灯显示以外的操作。耗时也增加了,这些操作也需要在主线程进行。ASDK 基于 Node 的设计,突破了 UIKit 线程的限制。
|
||||
|
||||
|
||||
|
||||
ASDK 创建了 ASDisplayNode 对象,内部封装了 UIView/CALayer,具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor,这些属性都可以在子线程更改,这样可以实现将排版和绘制放到后台线程,但最终都需要将 View 的更改同步到主线程的 UIView/CALayer 中去。
|
||||
|
||||
这个同步时机,就是利用 RunLoop 实现的。系统的 UIKit/QuartzCore 也是 RunLoop 的业务方,同样,我们可以模仿系统行为,将针对 View 的改动,在主线程 RunLoop 添加一个 Observer,监听 `kCFRunLoopBeforeWaiting` 、`kCFRunLoopExit` 状态,当收到回调时,遍历所有之前加入到队列中待处理的事务,然后一一执行。
|
||||
|
||||
```objective-c
|
||||
+ (void)registerTransactionGroupAsMainRunloopObserver:(_ASAsyncTransactionGroup *)transactionGroup
|
||||
{
|
||||
ASDisplayNodeAssertMainThread();
|
||||
static CFRunLoopObserverRef observer;
|
||||
ASDisplayNodeAssert(observer == NULL, @"A _ASAsyncTransactionGroup should not be registered on the main runloop twice");
|
||||
// defer the commit of the transaction so we can add more during the current runloop iteration
|
||||
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
|
||||
CFOptionFlags activities = (kCFRunLoopBeforeWaiting | // before the run loop starts sleeping
|
||||
kCFRunLoopExit); // before exiting a runloop run
|
||||
CFRunLoopObserverContext context = {
|
||||
0, // version
|
||||
(__bridge void *)transactionGroup, // info
|
||||
&CFRetain, // retain
|
||||
&CFRelease, // release
|
||||
NULL // copyDescription
|
||||
};
|
||||
|
||||
observer = CFRunLoopObserverCreate(NULL, // allocator
|
||||
activities, // activities
|
||||
YES, // repeats
|
||||
INT_MAX, // order after CA transaction commits
|
||||
&_transactionGroupRunLoopObserverCallback, // callback
|
||||
&context); // context
|
||||
CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes);
|
||||
CFRelease(observer);
|
||||
}
|
||||
|
||||
static void _transactionGroupRunLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
|
||||
{
|
||||
ASDisplayNodeCAssertMainThread();
|
||||
_ASAsyncTransactionGroup *group = (__bridge _ASAsyncTransactionGroup *)info;
|
||||
[group commit];
|
||||
}
|
||||
|
||||
- (void)commit
|
||||
{
|
||||
ASDisplayNodeAssertMainThread();
|
||||
|
||||
if ([_containers count]) {
|
||||
NSHashTable *containersToCommit = _containers;
|
||||
_containers = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality];
|
||||
|
||||
for (id<ASAsyncTransactionContainer> container in containersToCommit) {
|
||||
// Note that the act of committing a transaction may open a new transaction,
|
||||
// so we must nil out the transaction we're committing first.
|
||||
_ASAsyncTransaction *transaction = container.asyncdisplaykit_currentAsyncTransaction;
|
||||
container.asyncdisplaykit_currentAsyncTransaction = nil;
|
||||
[transaction commit];
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,67 +1,280 @@
|
||||
# NSTimer 中的内存泄露
|
||||
# NSTimer、CSDisplayLink 中的内存泄露
|
||||
|
||||
NSTimer、CADisplayLink 的 基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES block:nil]` 和当前的 VC 都会互相持有,造成环,会存在内存泄漏问题
|
||||
|
||||
|
||||
## CADisplayLink 内存泄漏
|
||||
|
||||
<img src="./../assets/CSDisplayLinkMemoryLeak.png" style="zoom:30%" />
|
||||
|
||||
可以看到 CADisplayLink 和 VC,VC 和 CADisplayLink 互相持有,造成内存泄漏,没有释放。即使页面离开,定时器还在继续运行,不断打印。
|
||||
|
||||
|
||||
|
||||
## NSTimer 内存泄漏
|
||||
|
||||
### 对比实验
|
||||
|
||||
NSTimer 的基础 API `[NSTimer scheduledTimersWithTimeInterval:1 repeat:YES block:nil]` 和当前的 VC 都会互相持有,造成环,会存在内存泄漏问题
|
||||
|
||||
Demo 如下:
|
||||
|
||||
<img src="./../assets/NSTimerMemoeryLeakDemo.png" style="zoom:30%" />
|
||||
|
||||
但是当使用 `[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerTask) userInfo:nil repeats:NO];` repeats 为 NO 的时候,好像不会内存泄漏。这是为什么?
|
||||
|
||||
<img src="./../assets/NSTimerMemoeryNotLeakWhenRepeatNO.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
### 源码分析
|
||||
|
||||
查看 gnu 源码发现
|
||||
|
||||
```objective-c
|
||||
@interface ViewController()
|
||||
@property (nonatomic, strong) NSTimer *timer;
|
||||
@end
|
||||
|
||||
@implementation ViewController
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1
|
||||
target:self
|
||||
selector:@selector(p_doSomeThing)
|
||||
userInfo:nil
|
||||
repeats:YES];
|
||||
|
||||
// NSTimer.m
|
||||
+ (NSTimer*) timerWithTimeInterval: (NSTimeInterval)ti
|
||||
target: (id)object
|
||||
selector: (SEL)selector
|
||||
userInfo: (id)info
|
||||
repeats: (BOOL)f
|
||||
{
|
||||
return AUTORELEASE([[self alloc] initWithFireDate: nil
|
||||
interval: ti
|
||||
target: object
|
||||
selector: selector
|
||||
userInfo: info
|
||||
repeats: f]);
|
||||
}
|
||||
|
||||
- (void)p_doSomeThing {
|
||||
// doSomeThing
|
||||
}
|
||||
|
||||
- (void)p_stopDoSomeThing {
|
||||
[self.timer invalidate];
|
||||
self.timer = nil;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self.timer invalidate];
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
内部调用下面的函数
|
||||
|
||||
```objective-c
|
||||
- (id) initWithFireDate: (NSDate*)fd
|
||||
interval: (NSTimeInterval)ti
|
||||
target: (id)object
|
||||
selector: (SEL)selector
|
||||
userInfo: (id)info
|
||||
repeats: (BOOL)f
|
||||
{
|
||||
if (ti <= 0.0)
|
||||
{
|
||||
ti = 0.0001;
|
||||
}
|
||||
if (fd == nil)
|
||||
{
|
||||
_date = [[NSDate_class allocWithZone: NSDefaultMallocZone()]
|
||||
initWithTimeIntervalSinceNow: ti];
|
||||
}
|
||||
else
|
||||
{
|
||||
_date = [fd copyWithZone: NSDefaultMallocZone()];
|
||||
}
|
||||
_target = RETAIN(object);
|
||||
_selector = selector;
|
||||
_info = RETAIN(info);
|
||||
if (f == YES)
|
||||
{
|
||||
_repeats = YES;
|
||||
_interval = ti;
|
||||
}
|
||||
else
|
||||
{
|
||||
_repeats = NO;
|
||||
_interval = 0.0;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
```
|
||||
|
||||
外面的 repeat 根据传递的布尔值,内部赋值给 _repeats 参数。
|
||||
|
||||
内部会自动调用 fire
|
||||
|
||||
```objective-c
|
||||
- (void) fire
|
||||
{
|
||||
/* We check that we have not been invalidated before we fire.
|
||||
*/
|
||||
if (NO == _invalidated) {
|
||||
if ((id)_block != nil) {
|
||||
CALL_BLOCK(_block, self);
|
||||
} else {
|
||||
id target;
|
||||
|
||||
/* We retain the target so it won't be deallocated while we are using
|
||||
* it (if this timer gets invalidated while we are firing).
|
||||
*/
|
||||
target = RETAIN(_target);
|
||||
|
||||
if (_selector == 0) {
|
||||
NS_DURING {
|
||||
[(NSInvocation*)target invoke];
|
||||
}
|
||||
NS_HANDLER {
|
||||
NSLog(@"*** NSTimer ignoring exception '%@' (reason '%@') "
|
||||
@"raised during posting of timer with target %s(%s) "
|
||||
@"and selector '%@'",
|
||||
[localException name], [localException reason],
|
||||
GSClassNameFromObject(target),
|
||||
GSObjCIsInstance(target) ? "instance" : "class",
|
||||
NSStringFromSelector([target selector]));
|
||||
}
|
||||
NS_ENDHANDLER
|
||||
} else {
|
||||
NS_DURING {
|
||||
[target performSelector: _selector withObject: self];
|
||||
}
|
||||
NS_HANDLER {
|
||||
NSLog(@"*** NSTimer ignoring exception '%@' (reason '%@') "
|
||||
@"raised during posting of timer with target %p and "
|
||||
@"selector '%@'",
|
||||
[localException name], [localException reason], target,
|
||||
NSStringFromSelector(_selector));
|
||||
}
|
||||
NS_ENDHANDLER
|
||||
}
|
||||
RELEASE(target);
|
||||
}
|
||||
|
||||
if (_repeats == NO) {
|
||||
[self invalidate];
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
可以看到如果 repeat 为 NO ,则会执行 `[target performSelector: _selector withObject: self];` 调用1次方法,然后会执行 `invalidate` 函数,`invalidate` 实现如下
|
||||
|
||||
```objective-c
|
||||
- (void) invalidate
|
||||
{
|
||||
/* OPENSTEP allows this method to be called multiple times. */
|
||||
_invalidated = YES;
|
||||
if (_target != nil)
|
||||
{
|
||||
DESTROY(_target);
|
||||
}
|
||||
if (_info != nil)
|
||||
{
|
||||
DESTROY(_info);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
可以看到当 target 和 info 存在的时候,都会在 `invalidate` 方法中被 destory,也就是释放。
|
||||
|
||||
```
|
||||
#define DESTROY(object) ({ \
|
||||
void *__o = (void*)object; \
|
||||
object = nil; \
|
||||
[(id)__o release]; \
|
||||
})
|
||||
#endif
|
||||
```
|
||||
|
||||
结论:通过 gnu 可以看到,NSTimer 会对传入的 target、info 对象进行持有强引用,当 repeat 参数为 NO 的时候,则会立马通过 performSelector 的方式执行定时器任务,然后执行 invalidate 方法,对其内部引用的 object、info 进行释放。
|
||||
|
||||
|
||||
|
||||
上面的代码主要是利用定时器重复执行 p_doSomeThing 方法,在合适的时候调用 p_stopDoSomeThing 方法使定时器失效。
|
||||
|
||||
能看出问题吗?在开始讨论上面代码问题之前,需要对 NSTimer 做一点说明。NSTimer 的 `scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:` 方法的最后一个参数为 YES 时,NSTimer 会保留目标对象,等到自身失效才释放目标对象。执行完任务后,一次性的定时器会自动失效;重复性的定时器,需要主动调用 invalidate 方法才会失效。
|
||||
|
||||
当前的 VC 和 定时器互相引用,造成循环引用。
|
||||
当前的 VC 和 定时器互相引用,造成循环引用。所以思路如下:
|
||||
|
||||
如果能在合适的时机打破循环引用就不会有问题了
|
||||
|
||||
1. 控制器不再强引用定时器
|
||||
2. 定时器不再保留当前的控制器
|
||||
|
||||
## 解决方案:
|
||||
|
||||
### 替换 NSTimer API
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 改用 block 的方式替换 API,不再持有 target
|
||||
|
||||
<img src="./../assets/NSTimerFixMemoryLeakIssueByBlockAPI.png" style="zoom:30%" />
|
||||
|
||||
该种方式,控制器 (self)强引用 timer,timer 强引用 block,block 弱引用 self,3者没有形成环。
|
||||
|
||||
|
||||
|
||||
### 采用系统 NSProxy 代替自定义的中间类
|
||||
|
||||
<img src="./../assets/NSTimerMemoryLeakFixedByNSProxy.png" style="zoom:30%" />
|
||||
|
||||
注意:继承自 NSProxy 的类,不能 init。
|
||||
|
||||
|
||||
|
||||
QA:自己写的继承自 NSObject 的代理对象和继承自 NSProxy 的代理有何区别?看上去反而是自定义的 NSObject 使用更简单呀?
|
||||
|
||||
答:**NSProxy 效率更高**。NSProxy 的主要作用是为消息转发提供一个通用的接口,是一个继承自 NSObject 的对象,虽然看上去 API 更简单,写法简单,但内部运行的时候还是基于 isa 去查找类对象、元类对象的 cache 中查找,找不到再去 class_rw_t 中查找,找不到再从 superclass 找父类的类对象、元类对象...流程,最后还是找不到,则走 runtime 的动态方法解析、消息转发阶段。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
看一段神奇的代码
|
||||
|
||||
<img src="./../assets/NSProxyAndNSObjectMethodImpl.png" style="zoom:30%" />
|
||||
|
||||
为什么打印出 `0 1`?
|
||||
|
||||
分析:
|
||||
|
||||
- p1 是 `TimerProxy` 类,继承于 NSObject 所以就不是 UIViewController 类型。
|
||||
|
||||
- p2 是 `MethodProxy` 类,继承自 NSProxy,当调用 isKindOfClass 这个方法的时候,也会进行消息转发,即调用 `forwardInvocation` 方法,其内部实现 `[invocation invokeWithTarget:self.target];` 则触发 self.target 的逻辑。此时 self.target 就是 self,所以上面的 `[p2 isKindOfClass:[self class]]` 等价于 `[self isKindOfClass:[self.class]]`,所以为 1。
|
||||
|
||||
也就是说继承自 NSProxy 类的对象,调用方法的时候,会自动走消息转发的流程。
|
||||
|
||||
这一点可以查看 GUN 查看下源码印证。`NSProxy.m`
|
||||
|
||||
```objectivec
|
||||
__weak typeof(self) weakSelf = self;
|
||||
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
|
||||
[weakSelf timerTest];
|
||||
}];
|
||||
- (BOOL) isKindOfClass: (Class)aClass
|
||||
{
|
||||
NSMethodSignature *sig;
|
||||
NSInvocation *inv;
|
||||
BOOL ret;
|
||||
sig = [self methodSignatureForSelector: _cmd];
|
||||
inv = [NSInvocation invocationWithMethodSignature: sig];
|
||||
[inv setSelector: _cmd];
|
||||
[inv setArgument: &aClass atIndex: 2];
|
||||
[self forwardInvocation: inv];
|
||||
[inv getReturnValue: &ret];
|
||||
return ret;
|
||||
}
|
||||
```
|
||||
|
||||
可以看到内部直接调用了消息转发。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### GCD Timer
|
||||
|
||||
CADisplayLink、NSTimer 都是依靠 RunLoop 实现的,所以当 RunLoop 任务繁重的时候,定时器可能不准。
|
||||
**CADisplayLink、NSTimer 都是依靠 RunLoop 实现的,所以当 RunLoop 任务繁重的时候,定时器可能不准。**
|
||||
|
||||
GCD 的定时器会更加准时,底层依赖系统内核。
|
||||
|
||||
|
||||
<img src="./../assets/RunLoop-SourceCode.png" style="zoom:30%" />
|
||||
|
||||
假设一个 NSTimer 被加到 RunLoop 开头,NSTimer 执行周期为1s,RunLoop 前面任务繁重,第一次走完一个完整的 RunLoop 需要0.4s,然后从头检测 NSTimer 有没有到时间,发现还没到继续执行 RunLoop 后续逻辑。后面遇到卡顿任务了,第二次 RunLoop 用了0.5s,然后从头检测 NSTimer 有没有到时间,0.4+0.5还不到时间,继续跑,第三次 RunLoop 比较轻松,耗时0.2s,再判断定时器时间有没有到,则此次已经0.4+0.5+0.2=1.1s了,此时 NSTimer 的事件被执行,此时精确度已经不够了(每次 RunLoop 的执行时间不固定)
|
||||
|
||||
如果 NSTimer 被添加到了一个特定的模式,当滚动视图时, RunLoop 会切换到 `UITrackingRunLoopMode`,如果 NSTimer 没有被添加到这个模式,它就不会触发。
|
||||
|
||||
当 RunLoop 没有事件可处理时,它会进入休眠状态。这意味着即使定时器的时间间隔到了,但 `RunLoop` 可能还在休眠中,因此定时器不会立即触发。
|
||||
|
||||
|
||||
|
||||
网上有些针对 FPS 帧率的检测是基于 CADisplayLink 计算的,所以这种方案不准确。具体可以查看文章:[带你打造一套 APM 监控系统](./1.74.md)
|
||||
|
||||
|
||||
|
||||
GCD 的 timer 会更加准时,底层依赖系统内核,不依赖 RunLoop。
|
||||
|
||||
```objectivec
|
||||
@property (nonatomic, strong) dispatch_source_t timer;
|
||||
@@ -86,27 +299,17 @@ self.timer = timerSource;
|
||||
|
||||
GCD timer 不依赖 RunLoop,系统底层驱动,所以会更加准确。因为和 RunLoop 无关,所以和 UI 滚动,RunLoop mode 切换到 UITrackingMode 也不影响 GCD timer。
|
||||
|
||||
|
||||
|
||||
### 打破循环引用,NSTimer target 自定义
|
||||
|
||||
```objectivec
|
||||
@interface LBPProxy : NSObject
|
||||
+ (instancetype)proxyWithObject:(id)target;
|
||||
@property (nonatomic, weak) id target;
|
||||
@end
|
||||
<img src="./../assets/NSTimerMemoryLeakFixedByProxy.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@implementation LBPProxy
|
||||
+ (instancetype)proxyWithObject:(id)target{
|
||||
LBPProxy *proxy = [[LBPProxy alloc] init];
|
||||
proxy.target = target;
|
||||
return proxy;
|
||||
}
|
||||
- (id)forwardingTargetForSelector:(SEL)aSelector{
|
||||
return self.target;
|
||||
}
|
||||
@end
|
||||
|
||||
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[LBPProxy proxyWithObject:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
|
||||
```
|
||||
|
||||
### 高精度定时器封装
|
||||
|
||||
@@ -239,122 +442,7 @@ dispatch_semaphore_t semaphore_;
|
||||
|
||||

|
||||
|
||||
### NSProxy
|
||||
|
||||
```objectivec
|
||||
#import "LBPProxy.h"
|
||||
@implementation LBPProxy
|
||||
+ (instancetype)proxyWithObject:(id)target{
|
||||
LBPProxy *proxy = [LBPProxy alloc];
|
||||
proxy.target = target;
|
||||
return proxy;
|
||||
}
|
||||
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
|
||||
return [self.target methodSignatureForSelector:sel];
|
||||
}
|
||||
- (void)forwardInvocation:(NSInvocation *)invocation{
|
||||
// 方法1
|
||||
invocation.target = self.target;
|
||||
[invocation invoke];
|
||||
// 方法2
|
||||
[invocation invokeWithTarget:self.target];
|
||||
}
|
||||
@end
|
||||
|
||||
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[LBPProxy proxyWithObject:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
|
||||
```
|
||||
|
||||
QA: 自己写的继承自 NSObject 的代理对象和继承自 NSProxy 的代理有何区别?
|
||||
|
||||
NSProxy 效率更高。继承自 NSObject 的代理,内部运行的时候还是存在方法查找(isa、superclass、cache、methods)流程。
|
||||
|
||||
看一段神奇的代码
|
||||
|
||||
`LBPProxy`
|
||||
|
||||
```objectivec
|
||||
@interface LBPProxy : NSObject
|
||||
+ (instancetype)proxyWithObject:(id)target;
|
||||
@property (nonatomic, weak) id target;
|
||||
@end
|
||||
@implementation LBPProxy
|
||||
+ (instancetype)proxyWithObject:(id)target{
|
||||
LBPProxy *proxy = [LBPProxy alloc];
|
||||
proxy.target = target;
|
||||
return proxy;
|
||||
}
|
||||
- (id)forwardingTargetForSelector:(SEL)aSelector{
|
||||
return self.target;
|
||||
}
|
||||
@end
|
||||
```
|
||||
|
||||
`LBPProxy2`
|
||||
|
||||
```objectivec
|
||||
@interface LBPProxy2 : NSProxy
|
||||
+ (instancetype)proxyWithObject:(id)target;
|
||||
@property (nonatomic, weak) id target;
|
||||
@end
|
||||
@implementation LBPProxy2
|
||||
+ (instancetype)proxyWithObject:(id)target{
|
||||
LBPProxy2 *proxy = [LBPProxy2 alloc];
|
||||
proxy.target = target;
|
||||
return proxy;
|
||||
}
|
||||
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
|
||||
return [self.target methodSignatureForSelector:sel];
|
||||
}
|
||||
- (void)forwardInvocation:(NSInvocation *)invocation{
|
||||
// 方法1
|
||||
invocation.target = self.target;
|
||||
[invocation invoke];
|
||||
// 方法2
|
||||
[invocation invokeWithTarget:self.target];
|
||||
}
|
||||
@end
|
||||
```
|
||||
|
||||
main.m
|
||||
|
||||
```objectivec
|
||||
ViewController *vc = [[ViewController alloc] init];
|
||||
LBPProxy *p1 = [LBPProxy proxyWithObject:vc];
|
||||
LBPProxy2 *p2 = [LBPProxy2 proxyWithObject:vc];
|
||||
NSLog(@"%d %d",
|
||||
[p1 isKindOfClass:[UIViewController class]],
|
||||
[p2 isKindOfClass:[UIViewController class]]);
|
||||
appDelegateClassName = NSStringFromClass([AppDelegate class]);
|
||||
// 0 1
|
||||
```
|
||||
|
||||
为什么打印出 `0 1`。
|
||||
|
||||
分析:
|
||||
|
||||
- p1 是 LBPProxy 类,继承于 NSObject 所以就不是 UIViewController 类型。
|
||||
|
||||
- p2 是 LBPProxy2 类,继承自 NSProxy,当调用 isKindOfClass 这个方法的时候,也会进行消息转发,即调用 `forwardInvocation` 方法,其内部实现 `[invocation invokeWithTarget:self.target];` 则触发 self.target 的逻辑。此时 self.target 就是 VC,所以为 1。
|
||||
|
||||
这一点可以查看 GUN 查看下源码印证。`NSProxy.m`
|
||||
|
||||
```objectivec
|
||||
- (BOOL) isKindOfClass: (Class)aClass
|
||||
{
|
||||
NSMethodSignature *sig;
|
||||
NSInvocation *inv;
|
||||
BOOL ret;
|
||||
sig = [self methodSignatureForSelector: _cmd];
|
||||
inv = [NSInvocation invocationWithMethodSignature: sig];
|
||||
[inv setSelector: _cmd];
|
||||
[inv setArgument: &aClass atIndex: 2];
|
||||
[self forwardInvocation: inv];
|
||||
[inv getReturnValue: &ret];
|
||||
return ret;
|
||||
}
|
||||
```
|
||||
|
||||
可以看到内部直接调用了消息转发。
|
||||
|
||||
### 采用 Block 的形式为 NSTimer 增加分类
|
||||
|
||||
@@ -476,6 +564,8 @@ __strong __typeof(&*weakSelf)self = weakSelf;
|
||||
|
||||
iOS 10 中,定时器 api 增加了 block 方法,实现原理与此类似,这里采用分类为 NSTimer 增加 block 参数的方法,最终的行为一致
|
||||
|
||||
|
||||
|
||||
## 检测
|
||||
|
||||
根据 Instrucments 提供的工具的工作原理,写一个野指针探针工具去发现并定位问题。具体见[野指针监控工具](./1.74.md#zombieSniffer)
|
||||
@@ -1,6 +1,282 @@
|
||||
# KVC && KVO
|
||||
|
||||
## 一、基本用法-字典快速赋值
|
||||
> KVO 的实现原理是什么?如何手动触发 KVO?本文来探索下 iOS 中 KVO 底层细节
|
||||
|
||||
|
||||
|
||||
## 底层实现分析
|
||||
|
||||
Demo1:创建 Person 类,点击事件里触发属性值的改变。
|
||||
|
||||
<img src="./../assets/XCodoKVOIsaInspect.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
分析:
|
||||
|
||||
- 添加过 KVO 的 person1,isa 为系统利用 Runtime 技术动态创建的类,名字为 `NSKVONotifying_Person`
|
||||
- 没有添加过 KVO 的 person2,isa 为 Person 的类对象
|
||||
- `.height = 177` 本质是 `[self.person1 setHeight:177]` 也就是调用 set 方法。
|
||||
|
||||
在内存中的结构如下图
|
||||
|
||||
<img src="./../assets/UnusedKVOIsaStructure.png" style="zoom:25%">
|
||||
|
||||
整个流程分析下:
|
||||
|
||||
- self.person2 调用 setHeight 的时候,首先根据 self.person2 实例对象的 isa 找到 Person 类对象,然后在方法列表中找到 setHeight 方法,然后进行调用
|
||||
- self.person1 调用 setHeight 的时候,首先根据 self.person1 实例对象的 isa 找到 `NSKVONotifying_Person` 类对象,然后在方法列表中找到 setHeight 方法,然后进行调用。内部实现中,会调用 Foundation 的 `_NSSetIntValueAndNotify` 方法。
|
||||
- 然后调用: willSet、super setHeight、didSet 方法。
|
||||
|
||||
当我们按照 KVO 后动态生成的类名去创建一个新的类的时候,Xcode 会报错:`[general] KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class`。因为自己创建的类名和系统将要动态创建的类名冲突了,并且 KVO 监听失效
|
||||
|
||||
<img src="./../assets/SelfClassNameConflictsWithKVOClass.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
Demo2:
|
||||
|
||||
<img src="./../assets/KVOMethodImplAddressAndIsaInspect.png" style="zoom:25%">
|
||||
|
||||
分析:
|
||||
|
||||
- 可以看到在对 self.person1 添加 KVO 之后,self.person1 的类对象改变了,也就是 self.person1的 isa 改变了,变为系统动态生成的新类
|
||||
- 在对 self.person1 添加 KVO 之后,self.person1 的 setHeight 方法的实现变了,添加前后 self.person2 的 setHeight 方法,都是 Person 类对象的 setHeight 方法实现。KVO 添加前后都未改变
|
||||
- 利用 `(IMP)方法地址` 查看,没有进行 KVO 的 `setHeight` 是在 `KVOExplore -[Person setHeight:] at Person.h:14)` 里。添加过 KVO 的是在 `Foundation _NSSetLongLongValueAndNotify` 里。且 `_NSSetLongLongValueAndNotify`是个 c 语言函数。
|
||||
|
||||
|
||||
|
||||
Demo3:
|
||||
|
||||
<img src="./../assets/KVOMethodImplAddressWithDouble.png" style="zoom:25%">
|
||||
|
||||
可以看到我们将 KVO 的数据类型改为 double 后,原本的 setHeight 是 `_NSSetLongLongValueAndNotify`,现在是 `_NSSetDoubleValueAndNotify`
|
||||
|
||||
也可以借助于 `nm` 来查看所有 Foundation 关于 KVO 的方法 `nm Foundation | grep ValueAndNotify ` (需要自己提取真机上的 Foundation 符号表)
|
||||
|
||||
|
||||
|
||||
### NSSet**ValueAndNotify 的内部实现
|
||||
|
||||
<img src="./../assets/UsedKVOIsaStructure.png" style="zoom:25%">
|
||||
|
||||
来对 Person 类增加一些打印方法
|
||||
|
||||
<img src="./../assets/KVOKeyMethodPrint.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
可以看出内部实现是:
|
||||
|
||||
- 调用 `willChangeVlueForKey`
|
||||
|
||||
- 调用原本的 setter
|
||||
|
||||
- 调用 `didChangeValueForKey`。在 `didChangeValueForKey` 内部会调用 KVO 的 `- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context` 方法
|
||||
|
||||
|
||||
|
||||
```objective-c
|
||||
@interface Person()
|
||||
@property (nonatomic, assign) double height;
|
||||
@end
|
||||
@implementation Person
|
||||
- (void)setHeight:(double)height {
|
||||
_height = height;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface NSKVONotifying_Person: Person()
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSKVONotifying_Person
|
||||
- (void)setHeight:(double)height {
|
||||
[self setDoubleValueAndNotify:height];
|
||||
}
|
||||
|
||||
- (void)setDoubleValueAndNotify:(double)height {
|
||||
[self willChangeValueForKey:@"height"];
|
||||
[super setHeight: height];
|
||||
[self didChangeValueForKey:@"height"];
|
||||
}
|
||||
|
||||
- (void)didChangeValueForKey:(NSString *)key {
|
||||
[observer observeValueForKeyPath:key ofObject:self change:@{} context:nil];
|
||||
}
|
||||
|
||||
- (Class)class {
|
||||
return [Person class];
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 重写 class 方法
|
||||
|
||||
<img src="./../assets/KVOOverrrideClassMethod.png" style="zoom:25%">
|
||||
|
||||
可以看到利用 runtime api,在添加 KVO 之后,类对象为 `NSKVONotifying_Person`
|
||||
|
||||
但是利用 class 方法,添加 KVO 之后,获取类对象依旧为 Person。
|
||||
|
||||
好处是:屏蔽了 KVO 底层内部实现,隐藏了 `NSKVONotifying_Person` 的存在,通过 `-(Class)class` 方法告诉开发者添加 KVO 之后的类,依旧是 Person,本质上是继承自 Person 的类对象,能力没有改变。
|
||||
|
||||
|
||||
|
||||
### KVO 类的所有方法
|
||||
|
||||
<img src="./../assets/PrintKVOClassAllMethodName.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
利用 runtime api,打印添加 KVO 后,动态创建的 NSKVONotifying_Person 都存在什么方法?
|
||||
|
||||
- setHeight:
|
||||
- class
|
||||
- dealloc
|
||||
- _isKVOA
|
||||
|
||||
QA:为什么新创建的类没有 getter?
|
||||
|
||||
因为新创建的类是子类,父类中存在 getter。子类中增加的方法只是为了触发 KVO,getter 不影响。
|
||||
|
||||
|
||||
|
||||
### 修改成员变量的值可以触发 KVO 吗
|
||||
|
||||
<img src="./../assets/KVOCannotInvokeWhenChangeIvarDirectly.png" style="zoom:25%">
|
||||
|
||||
我们将 Person 类的成员变量暴露出来,在点击事件里修改,发现不能触发 KVO。
|
||||
|
||||
也就是说,触发 KVO 的本质是必须要有 setter,且触发 setter。直接修改成员变量,是不会触发 setter 的。
|
||||
|
||||
QA:如何手动触发?
|
||||
|
||||
手动调用 `willChangeValueForKey`、 `didChangeValueForKey`
|
||||
|
||||
<img src="./../assets/KVOInvokeWhenChangeIvarDirectlyMustCallWillChangeAndDidChange.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
QA:请描述系统如何实现一个对象的 KVO?KVO 的本质是什么
|
||||
|
||||
- 系统利用 runtime 的能力,动态创建一个监听对象的类的子类,子类命名格式为: `NSKVONotifying_类名`。 并且让 instance 对象的 isa 指向这个全新的子类
|
||||
- 当修改 instance 对象的属性时,会调用 Foundation 框架的 `_NSSet***ValueAndNotify` c 函数
|
||||
- 然后调用:
|
||||
- `willChangeValueForKey`
|
||||
- 原来的 setter
|
||||
- `didChangeValueForKey`,且 didChangeValueForKey 内部会触发监听器(Observer)的监听方法(`- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context`)。也就是发消息
|
||||
|
||||
QA:如何手动触发 KVO?
|
||||
|
||||
上面剖析了 KVO 的系统实现,所以手动触发 KVO 就需要调用 `willChangeValueForKey` 和 `didChangeValueForKey`
|
||||
|
||||
|
||||
|
||||
### 当没有observer观察任何一个property时,删除动态创建的子类
|
||||
|
||||
`[self.person removeObserver:self forKeyPath:@"height"];` 该代码调用后,会删除动态创建的子类。
|
||||
|
||||
|
||||
|
||||
## KVC
|
||||
|
||||
`setValueForKey` 用来设置对象的一层属性值修改。
|
||||
|
||||
`setValueForKeyPath` 可以设置对象的某个属性(属性本身是对象)的属性值修改
|
||||
|
||||
|
||||
|
||||
### KVC 设值原理
|
||||
|
||||
KVC 之后会触发 KVO 吗?
|
||||
|
||||
<img src="./../assets/KVCWillTriggerKVO.png" style="zoom:25%">
|
||||
|
||||
发现 KVC 触发了 KVO。
|
||||
|
||||
问题来了:为什么 KVC 会触发 KVO?探究下 `setValueForKey`
|
||||
|
||||
整个流程如下
|
||||
|
||||
<img src="./../assets/KVC-process.png" style="zoom:45%">
|
||||
|
||||
`[self.person setValue:@10 forKey:@"age"]` 会先调用 `setKey:` 同名的方法,找不到则调用 `_setKey:` 的方法,如果还是找不到则调用 `+(BOOL)accessInstanceVariableDirectlt`,如果该方法返回 YES,则可以直接修改成员变量的值,会按照 `_key`、`_isKey`、`key`、`isKey` 的顺序寻找成员变量,如果找到则直接赋值,没找到则抛出异常 `NSUnknownKeyException`
|
||||
|
||||
```
|
||||
@implementation Person
|
||||
- (void)setAge:(int)age
|
||||
{
|
||||
_age = age;
|
||||
}
|
||||
|
||||
- (void)_setAge:(int)age
|
||||
{
|
||||
_age = age;
|
||||
}
|
||||
|
||||
+ (BOOL)accessInstanceVariableDirectlt
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 直接修改成员变量会触发 KVO 吗?
|
||||
|
||||
不会。KVO 的实现原理就是在 setter 方法内,调用 `willChangeValueForKey`、`didChangeValueForKey` ,所以直接修改成员变量不会触发 KVO。
|
||||
|
||||
想要触发,可以手动调用上面2个 API。
|
||||
|
||||
```objective-c
|
||||
- (void)setName:(NSString *)name {
|
||||
[self willChangeValueForKey:@"name"];
|
||||
_name = name;
|
||||
[self didChangeValueForKey:@"name"];
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### KVC 取值原理
|
||||
|
||||
`valueForKey` 原理
|
||||
|
||||
- 按照 getKey、key、isKey、_key 的顺序寻找方法实现,找到则直接调用方法,返回值
|
||||
- 如果没找到则调用 `+(BOOL)accessInstanceVariableDirectly` 方法,询问是否可以访问成员变量
|
||||
- 为 NO 则调用 `valueForUndefinedKey `并抛出 `NSUnknownKeyException`异常
|
||||
- 为 YES 则按照 ` __key`、`_isKey`、`key`、`isKey` 的顺序访问成员变量。找到哪个则返回值
|
||||
|
||||
- 都没找到则调用 `valueForUndefinedKey `并抛出 `NSUnknownKeyException`异常
|
||||
|
||||
|
||||
|
||||
<img src="./../assets/KVC-get-process.png" style="zoom:45%">
|
||||
|
||||
|
||||
|
||||
### KVC 会破坏面向对象的原则吗?
|
||||
|
||||
KVC 有违背面向对象编程思想吗?如果一个类的成员变量是私有的,也没有在 `.h` 中公开一些方法去设置、修改成员变量,那么外部直接通过 KVC 去修改值,是有违背面向对象编程思想的。
|
||||
|
||||
KVC 提供了对应的能力,去保护或者说支持面向对象的原则。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 基本用法-字典快速赋值
|
||||
|
||||
KVC 可以将字典里面和 model 同名的 property 进行快速赋值 `setValuesForKeysWithDictionary`。
|
||||
|
||||
@@ -84,11 +360,11 @@ self.person.dog.weight = 50;
|
||||
}
|
||||
```
|
||||
|
||||
## 二、 KVO 的本质
|
||||
|
||||
kVO 是Objective-C 对观察者模式的实现。也是 Cocoa Binding 的基础。
|
||||
|
||||
### 几个基本的知识点
|
||||
|
||||
|
||||
## 几个基本的知识点
|
||||
|
||||
1. KVO 观察者和属性被观察的对象之间不是强引用的关系
|
||||
|
||||
@@ -127,15 +403,11 @@ kVO 是Objective-C 对观察者模式的实现。也是 Cocoa Binding 的基础
|
||||
@end
|
||||
```
|
||||
|
||||
8. 手动触发 KVO?调用 willChangeValueForKey、didChangeValueForKey
|
||||
|
||||
9.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 三、 实现机制
|
||||
## 实现机制
|
||||
|
||||
> Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ...
|
||||
|
||||
@@ -145,22 +417,43 @@ Apple 文档告诉我们:被观察对象的 `isa指针` 会指向一个中间
|
||||
|
||||
- KVO 是基于 Runtime 机制实现的
|
||||
|
||||
- 当某个类的属性第一次被观察的时候,系统会在运行期动态的创建该类的一个派生类。在派生类中重写任何被观察属性的 setter 方法。派生类在真正实现`通知机制`
|
||||
- 当某个类的属性第一次被观察的时候,系统会在运行期动态的创建该类的一个派生类(子类)。在派生类中重写任何被观察属性的 setter 方法。派生类在真正实现`通知机制`
|
||||
|
||||
- 如果当前类为 Person,则生成的派生类名称为 `NSKVONotifying_Person`
|
||||
- 如果当前类为 Person,则生成的派生(子类)类名称为 `NSKVONotifying_Person`
|
||||
|
||||
- 每个类对象中都有一个 `isa指针` 指向当前类,当一个类对象第一次被观察的时候,系统会偷偷将 isa 指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是当前派生类的 `setter` 方法
|
||||
- 每个类对象中都有一个 `isa指针` 指向当前类,当一个类对象第一次被观察的时候,系统会偷偷将 isa 指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是当前派生类的 `setter` 方法
|
||||
|
||||
- 键值观察通知依赖于 NSObject 的两个方法:`willChangeValueForKey:、didChangeValueForKey:` 。在一个被观察属性改变之前,调用 `willChangeValueForKey:` 记录旧的值。在属性值改变之后调用 `didChangeValueForKey:`,从而 `observeValueForKey:ofObject:change:context:` 也会被调用。
|
||||
|
||||

|
||||

|
||||
|
||||
为什么要选择是继承的子类而不是分类呢?
|
||||
子类在继承父类对象,子类对象调用调方法的时候先看看当前子类中是否有方法实现,如果不存在方法则通过 isa 指针顺着继承链向上找到父类中是否有方法实现,如果父类种也不存在方法实现,则继续向上找...直到找到 NSObject 类为止,系统会抛出几次机会给程序员补救,如果未做处理则奔溃
|
||||
|
||||
关于分类与子类的关系可以看看我之前的 [文章](1.50.md).
|
||||
|
||||
## 四、 模拟实现系统的 KVO
|
||||
|
||||
|
||||
## QA
|
||||
|
||||
iOS 用什么方式实现对一个对象的 KVO?(KVO 的本质是什么)
|
||||
|
||||
- 当对一个对象使用了 KVO 监听,系统会修改这个对象的 isa 指针,改为一个指向通过 Runtime 动态创建的子类
|
||||
- 子类拥有自己对监听属性的 setter 实现,内部会调用
|
||||
- willChangeValueForKey
|
||||
- 原来的 setter 实现
|
||||
- didChangeValueForKey,这个方法内部又会调用监听器的 监听方法 `[observer observeValueForKey:ofObject:change:context:]`
|
||||
|
||||
如何手动触发 KVO?
|
||||
|
||||
```objective-c
|
||||
[p willChangeValueForKey:@"height"];
|
||||
[p didChangeValueForKey:@"height"];
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 模拟实现系统的 KVO
|
||||
|
||||
1. 创建被观察对象的子类
|
||||
2. 重写观察对象属性的 set 方法,同时调用 `willChangeValueForKey、didChangeValueForKey`
|
||||
@@ -267,54 +560,3 @@ KVO 的改装:
|
||||
|
||||
|
||||
|
||||
## 五、 KVC
|
||||
|
||||
`setValueForKey` 用来设置对象的一层属性值修改。
|
||||
|
||||
`setValueForKeyPath` 可以设置对象的某个属性(属性本身是对象)的属性值修改
|
||||
|
||||
|
||||
|
||||
KVC 之后会触发 KVO。为什么?探究下 `setValueForKey`
|
||||
|
||||
`[self.person setValue:@10 forKey:@"age"]` 会先调用 `setKey:` 同名的方法,找不到则调用 `_setKey:` 的方法,如果还是找不到则调用 `+(BOOL)accessInstanceVariableDirectlt`,如果该方法返回 YES,则可以直接修改成员变量的值,会按照 _key、_isKey、key、isKey 的顺序寻找成员变量,如果找到则直接赋值,没找到则抛出异常 NSUnknownKeyException
|
||||
|
||||
整个流程如下
|
||||
|
||||

|
||||
|
||||
```
|
||||
@implementation Person
|
||||
- (void)setAge:(int)age
|
||||
{
|
||||
_age = age;
|
||||
}
|
||||
|
||||
- (void)_setAge:(int)age
|
||||
{
|
||||
_age = age;
|
||||
}
|
||||
|
||||
+ (BOOL)accessInstanceVariableDirectlt
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
valueForKey 原理
|
||||
|
||||
- 按照 getKey、key、isKey、_key 的顺序寻找方法实现
|
||||
|
||||
- 找到则直接调用方法,返回值
|
||||
|
||||
- 如果没找到则调用 accessInstanceVariableDirectlt 方法,询问是否可以访问成员变量。为 NO 则抛出异常
|
||||
|
||||
- 为 YES 则按照 __key、isKey、key、isKey 的顺序访问成员变量。找到哪个则返回值
|
||||
|
||||
- 都没找到则抛出异常
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -267,4 +267,14 @@ Pod::Spec.new do |s|
|
||||
CMD
|
||||
end
|
||||
```
|
||||
## 15.
|
||||
## 15. `pod lib create` 报错 `Ignoring ffi-1.16.3 because its extensions are not built`
|
||||
|
||||
开始可能发现错误
|
||||
|
||||
Ignoring ffi-1.16.3 because its extensions are not built. Try: gem pristine ffi --version 1.16.3
|
||||
类似这样的错误
|
||||
```
|
||||
sudo gem install --user-install rexml
|
||||
sudo gem install --user-install xcodeproj
|
||||
```
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ BSS段(bss segment):通常用来存储程序中未被初始化的全局变
|
||||
|
||||
代码段(code segment):通常是指用来存储程序可执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量。
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ struct NSObject_IMPL {
|
||||
|
||||
因此可以知道,OC 的类底层是由 c/c++ 的继承实现的。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/OCObjectLayoutWhenISA.png" style="zoom:45%">
|
||||
<img src="./../assets/OCObjectLayoutWhenISA.png" style="zoom:45%">
|
||||
|
||||
由于 obj 对象没有任何属性和方法,只有一个 isa 指针,且类的本质就是结构体,所以当结构体只有1个成员时,该成员的地址值,就是该结构体的地址。
|
||||
|
||||
@@ -118,7 +118,7 @@ struct Student_IMPL {
|
||||
|
||||
类的本质是结构体,结构体成员内存紧挨着。内存布局如图所示:
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/StudentClassLayout.png" style="zoom:45%">
|
||||
<img src="./../assets/StudentClassLayout.png" style="zoom:45%">
|
||||
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ struct Student_IMPL {
|
||||
|
||||
如果上述结论正确,那是不是可以声明一个 ` Student_IMPL` 类型的结构体指针,指向 st 指针指向的对象。然后通过结构体指针访问成员变量,看看取值是不是正确的
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/StructPointerVistorClassIvars.png" style="zoom:25%">
|
||||
<img src="./../assets/StructPointerVistorClassIvars.png" style="zoom:25%">
|
||||
|
||||
发现是可以正确访问的。
|
||||
|
||||
@@ -193,7 +193,7 @@ struct Student_IMPL {
|
||||
|
||||
为什么 `class_getInstanceSize([Person class])` 也是16,不是8+4吗?因为存在内存对齐,结构体的大小必须是最大成员大小的倍数(Person 中也就是8的倍数)
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/StudentClassExtendsFromPersonClass.png" style="zoom:25%">
|
||||
<img src="./../assets/StudentClassExtendsFromPersonClass.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
@@ -330,12 +330,12 @@ int main(int argc, const char * argv[]) {
|
||||
|
||||
`Person *p1 = [Person new];` 这句代码在内存分配原理如下图所示
|
||||
|
||||

|
||||

|
||||
|
||||
**结论**
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
**可以 看到Person类的3个对象p1、p2、p3的isa的值相同。**
|
||||
|
||||
@@ -707,7 +707,7 @@ iOS 中,系统分配内存,都是16的倍数。pageSize?系统在分配内
|
||||
GUN 都存在内存对齐这个概念。
|
||||
`sizeof` 本质是运算符。在 Xcode 编译后就替换为真正的值。通过指令 `xcrun --sdk iphoneos clang -arch arm64 -S -emit-llvm ViewController.m -o ViewController.ll` 查看 IR。
|
||||
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/XcoedeViewSizeOfViaAssembly.png" style="zoom:25%">
|
||||
<img src="./../assets/XcoedeViewSizeOfViaAssembly.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
@@ -718,21 +718,21 @@ GUN 都存在内存对齐这个概念。
|
||||
|
||||
`objc_getClass()` 如果传递 instance 实例对象,返回 class 类对象;传递 Class 类对象,返回 meta-class 元类对象;传递 meta-class 元对象,则返回 NSObject(基类)的 meta-class 对象
|
||||
|
||||

|
||||

|
||||
|
||||
instance 的 isa 指向 Class。当调用方法时,通过 instance 的 isa 找到 Class,最后找到对象方法的实现进行调用
|
||||
class 的 isa 指向 meta-class。当调用类方法的时,通过 class 的 isa 找到 meta-class,最后找到类方法进行调用。
|
||||
|
||||

|
||||

|
||||
|
||||
当 Student 实例对象调用 Person 的对象方法时,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,在类对象的对象方法列表中找到方法实现并调用。
|
||||
当 Stduent 实例对象调用 init 方法时候,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,找到 Person Claas 的 superClass 到 NSObject 类对象,在 NSObject 类对象的方法列表中找到 `init` 方法并调用。
|
||||
|
||||

|
||||

|
||||
|
||||
当 Stduent 对象调用类方法的时候,先根据 isa 找到 Student 的元类对象,然后在元类对象的 superclass 找到 Person 的元类对象,再根据 Person 元类对象的 superClass 找到 NSObject 的元类对象。最后找到元类对象的方法列表,调用到对象方法。
|
||||
|
||||

|
||||

|
||||
|
||||
```objectivec
|
||||
@interface Student : NSObject
|
||||
@@ -937,7 +937,7 @@ class_rw_t *personMetaClassData = personClass->metaClass()->data();
|
||||
内存对齐是指数据在内存中存储时按照一定规则对齐到特定的地址上。在 iOS 开发中,内存对齐是为了提高内存访问的效率和性能。内存对齐的原因主要包括以下几点:
|
||||
1. 提高访问速度:内存对齐可以使数据在内存中的存储更加高效,因为大部分计算机体系结构都要求数据按照特定的边界对齐,这样可以减少内存访问的次数,提高访问速度。CPU访问非对齐的内存时需要进行多次拼接。
|
||||
如下图,比如需要读取从[2, 5]的内存,需要分别读取两次,然后还需要做位移的运算,最后才能得到需要的数据。这中间的损耗就会影响访问速度。
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/MemoryAlignReason.png" style="zoom:30%">
|
||||
<img src="./../assets/MemoryAlignReason.png" style="zoom:30%">
|
||||
|
||||
2. 为了方便移植。CPU是一块块的进行进行内存访问。有一些硬件平台不允许随机访问,只能访问对齐后的内存地址,否则会报异常。
|
||||
很多 CPU(如基于 Alpha,IA-64,MIPS,和 SuperH 体系的)拒绝读取未对齐数据。当一个程序要求这些 CPU 读取未对齐数据时,这时 CPU 会进入异常处理状态并且通知程序不能继续执行。举个例子,在 ARM,MIPS,和 SH 硬件平台上,当操作系统被要求存取一个未对齐数据时会默认给应用程序抛出硬件异常。
|
||||
@@ -1017,7 +1017,7 @@ NSLog(@"%zd", malloc_size(temp));
|
||||
成员变量占用8字节对齐,每个对象的第一个都是 isa 指针,必须要占用8字节。举例一个极端 case,假设 n 个对象,其中 m 个对象没有成员变量,只有 isa 指针占用8字节,其中的 n-m个对象既有 isa 指针,又有成员变量。每个类交错排列,那么 CPU 在访问对象的时候会耗费大量时间去识别具体的对象。很多时候会取舍,这个 case 就是时间换空间。以16字节对齐,会加快访问速度(参考链表和数组的设计)
|
||||
|
||||
上述是 Apple 官方的角度出发探究的,其他系统,比如 Linux 也是存在内存对齐的。由于 Linux 也是采用 GNU 的东西,所以探索下 GNU 下 glibc malloc 的实现。从[这里](https://ftp.gnu.org/gnu/glibc/)下载 glibc 源码。然后拖到 Xcode 中查看
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/GlibcInXcodeProject.png" style="zoom:25%">
|
||||
<img src="./../assets/GlibcInXcodeProject.png" style="zoom:25%">
|
||||
|
||||
可以看到 GNU 源码里面,内存对齐 `MALLOC_ALIGNMENT`
|
||||
在 i386 里面是16,在非 i386 里面有个判断
|
||||
@@ -1031,7 +1031,7 @@ NSLog(@"%zd", malloc_size(temp));
|
||||
# define INTERNAL_SIZE_T size_t
|
||||
```
|
||||
在 Xcode 打印输出, `__alignof__ (long double)` 为16,`sizeof(size_t)` 为8,即 `2 * SIZE_SZ` = 16,所以不管怎么看,在 GUN 里面内存对齐一定都是16.
|
||||
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/GLibcMallocAlignment.png" style="zoom:25%">
|
||||
<img src="./../assets/GLibcMallocAlignment.png" style="zoom:25%">
|
||||
Todo: 研究探索 libmalloc 源码
|
||||
|
||||
|
||||
@@ -1154,3 +1154,42 @@ Class objc_getClass(const char *aClassName)
|
||||
|
||||
`-(Class)class`、`+(Class)class`:返回的是类对象
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## QA
|
||||
|
||||
### 对象的 isa 指向什么?
|
||||
- Instance 对象的 isa 指向类对象(Class)
|
||||
- Class 对象的 isa 指向元类对象(Meta-Class)
|
||||
- Meta-Class 对象的 isa 指向基类的元类对象(Meta-Class)
|
||||
|
||||
但注意:
|
||||
- 实例对象的 isa 需要与 ISA_MASK 按位与之后才可以得到类对象的地址值。
|
||||
- 类对象的 isa 需要与 ISA_MASK 按位与之后才可以得元类对象的地址值。
|
||||
|
||||
|
||||
### OC 的类信息存放在哪?
|
||||
- Instance 对象:成员变量具体的值,存放在实例对象中
|
||||
```
|
||||
struct NSObject_IMPL {
|
||||
class isa;
|
||||
}
|
||||
|
||||
struct Person_IMPL {
|
||||
struct NSObject_IMPL NSObject_IAVRS;
|
||||
int _age;
|
||||
int _height;
|
||||
}
|
||||
|
||||
//
|
||||
struct Person_IMPL {
|
||||
class isa;
|
||||
int _age;
|
||||
int _height;
|
||||
}
|
||||
```
|
||||
- Class 对象:属性信息、对象方法信息、成员变量信息、协议信息、superclass、isa 存放在类对象中。
|
||||
- Meta-Class 对象:类方法信息,存放在元类对象中。
|
||||
|
||||
|
||||
@@ -1154,6 +1154,8 @@ MemoryStatus 机制会开启一个 memorystatus_jetsam_thread 的线程,它负
|
||||
|
||||
当监控线程发现某 App 有内存压力时,就发出通知,此时有内存的 App 就去执行 `didReceiveMemoryWarning` 代理方法。在这个时机,我们还有机会做一些内存资源释放的逻辑,也许会避免 App 被系统杀死。
|
||||
|
||||
|
||||
|
||||
**源码角度查看问题**
|
||||
|
||||
iOS 系统内核有一个数组,专门维护线程的优先级。数组的每一项是一个包含进程链表的结构体。结构体如下:
|
||||
@@ -1785,7 +1787,7 @@ for (NSInteger index = 0; index < 10000000; index++) {
|
||||
现象:
|
||||
|
||||
1. 在 viewDidLoad 也就是主线程中内存消耗过大,系统并不会发出低内存警告,直接 Crash。因为内存增长过快,主线程很忙。
|
||||
2. 多线程的情况下,App 因内存增长过快,会收到低内存警告,AppDelegate 中的`applicationDidReceiveMemoryWarning` 先执行,随后是当前 VC 的 `didReceiveMemoryWarning`。
|
||||
2. 多线程的情况下,App 因内存增长过快,会收到低内存警告,AppDelegate 中的`applicationDidReceiveMemoryWarning` 先执行,随后是当前 VC 的 `didReceiveMemoryWarning`。也可以注册 `UIApplicationDidReceiveMemoryWarningNotification` 通知,此时获取一下内存情况一般就是 high Water 线。
|
||||
|
||||
结论:
|
||||
|
||||
@@ -2052,7 +2054,7 @@ for (NSInteger index = 0; index < 10000000; index++) {
|
||||
|
||||
其他的开发习惯就不一一描述了,良好的开发习惯和代码意识是需要平时注意修炼的。
|
||||
|
||||
### 7.
|
||||
### 7. Memory Graph
|
||||
|
||||
在使用了一波业界优秀的的内存监控工具后发现了一些问题,比如 `MLeaksFinder`、`OOMDetector`、`FBRetainCycleDetector`等都有一些问题。比如 `MLeaksFinder` 因为单纯通过 VC 的 push、pop 等检测内存泄露的情况,会存在误报的情况。`FBRetainCycleDetector` 则因为对象深度优先遍历,会有一些性能问题,影响 App 性能。`OOMDetector` 因为没有合适的触发时机。
|
||||
|
||||
@@ -3699,56 +3701,44 @@ CFNetwork 使用 CFReadStreamRef 来传递数据,使用回调函数的形式
|
||||
@implementation NetworkDelegateProxy
|
||||
|
||||
#pragma mark - life cycle
|
||||
```
|
||||
|
||||
- (instancetype)sharedInstance {
|
||||
static NetworkDelegateProxy *_sharedInstance = nil;
|
||||
|
||||
static dispatch_once_t onceToken;
|
||||
|
||||
dispatch_once(&onceToken, ^{
|
||||
_sharedInstance = [NetworkDelegateProxy alloc];
|
||||
|
||||
- (instancetype)sharedInstance {
|
||||
static NetworkDelegateProxy *_sharedInstance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
_sharedInstance = [NetworkDelegateProxy alloc];
|
||||
});
|
||||
|
||||
return _sharedInstance;
|
||||
|
||||
return _sharedInstance;
|
||||
}
|
||||
|
||||
#pragma mark - public Method
|
||||
|
||||
- (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate {
|
||||
NetworkDelegateProxy *instance = [NetworkDelegateProxy sharedInstance];
|
||||
instance->_originalTarget = originalTarget;
|
||||
instance->_NewDelegate = newDelegate;
|
||||
return instance;
|
||||
}
|
||||
|
||||
- (void)forwardInvocation:(NSInvocation *)invocation {
|
||||
if ([_originalTarget respondsToSelector:invocation.selector]) {
|
||||
|
||||
[invocation invokeWithTarget:_originalTarget];
|
||||
[((NSURLSessionAndConnectionImplementor *)_NewDelegate) invoke:invocation];
|
||||
|
||||
}
|
||||
- (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate {
|
||||
NetworkDelegateProxy *instance = [NetworkDelegateProxy sharedInstance];
|
||||
instance->_originalTarget = originalTarget;
|
||||
instance->_NewDelegate = newDelegate;
|
||||
return instance;
|
||||
}
|
||||
|
||||
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
|
||||
return [_originalTarget methodSignatureForSelector:sel];
|
||||
|
||||
- (void)forwardInvocation:(NSInvocation *)invocation {
|
||||
if ([_originalTarget respondsToSelector:invocation.selector]) {
|
||||
[invocation invokeWithTarget:_originalTarget];
|
||||
[((NSURLSessionAndConnectionImplementor *)_NewDelegate) invoke:invocation];
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
|
||||
return [_originalTarget methodSignatureForSelector:sel];
|
||||
}
|
||||
@end
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
- 创建一个对象,实现 NSURLConnection、NSURLSession、NSIuputStream 代理方法
|
||||
|
||||
```objective-c
|
||||
// NetworkImplementor.m
|
||||
|
||||
#pragma mark-NSURLConnectionDelegate
|
||||
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
|
||||
- (void)connection:(NSURLConnection *)connection didFailWithErrorbo:(NSError *)error {
|
||||
NSLog(@"%s", __func__);
|
||||
}
|
||||
|
||||
|
||||
@@ -1075,6 +1075,19 @@ QA:一个被测方法,有诸多 case,为什么不写在一个测试方法
|
||||
|
||||
|
||||
|
||||
### 15. Swift 测试用例代码测试 OC 被测代码
|
||||
|
||||
编写 Swift 测试代码去测试 OC 被测类的时候,需要做一些处理:
|
||||
|
||||
1. 主工程不管是不是混编,但为了让 Swift 测试代码,可以访问到 OC 被测类,需要创建一个 Swift 文件,系统会自动创建 bridge 文件,且需要在 `AppTestingExplore-Bridging-Header.h` 文件中导出需要被测的头文件
|
||||
2. 在 Swift 测试文件中,导入主工程 module。
|
||||
|
||||
<img src="./../assets/SwiftTestingForOCClass.png" style="zoom:30%; align:left;">
|
||||
|
||||
|
||||
|
||||
`@testable` 是 Swift 语言的一个特性,它允许测试用例访问应用程序或框架中标记为 `internal` 或 `private` 的属性、方法和其他成员。这样做可以在不改变访问级别的情况下编写测试用例,从而保持代码的封装性和安全性。使用 `@testable` 可以增强测试覆盖率,因为它允许测试那些通常因为访问级别限制而无法测试的内部实现细节。同时,它还有助于保持代码的封装性,因为不需要将内部实现细节暴露为 `public` 就可以进行单元测试。此外,`@testable` 提高了测试的灵活性,在不修改代码访问级别的情况下,能够对代码进行全面的测试
|
||||
|
||||
|
||||
|
||||
## 四、网络测试
|
||||
@@ -1164,6 +1177,8 @@ SPEC_END
|
||||
|
||||
## 五、UI 测试
|
||||
|
||||
### 基础使用
|
||||
|
||||
上面文章大篇幅的讲了单元测试相关的话题,单元测试十分适合代码质量、逻辑、网络等内容的测试,但是针对最终产物 App 来说单元测试就不太适合了,如果测试 UI 界面的正确性、功能是否正确显然就不太适合了。Apple 在 Xcode 7 开始推出的 `UI Testing` 就是苹果自己的 UI 测试框架。
|
||||
|
||||
很多 UI 自动化测试框架的底层实现都依赖于 `Accessibility`,也就是 App 可用性。`UI Accessibility` 是 iOS 3.0 引入的一个人性化功能,帮助身体不便的人士方便使用 App。
|
||||
@@ -1298,6 +1313,44 @@ Accessibility 通过对 UI 元素进行分类和标记。分类成类似按钮
|
||||
|
||||
|
||||
|
||||
### 经验心得
|
||||
|
||||
UI 测试另一个问题是,某些 UI 方法比如 AppDelegate 里包含太多 SDK 的或者拉接口的场景,启动会比较慢,测试的诉求是:单个测试 case 需要快速运行。而 UI 测试聚焦的不是借口业务逻辑,所以期望 AppDelegate 里的拉接口这样的逻辑不要走,太慢影响测试速度。
|
||||
|
||||
理论分析:如果可以从 `NSClassFromString(@"XCTestCase")` 方式获取到值,说明是测试环境,可以简化 AppDelegate 逻辑。
|
||||
|
||||
具体做法是在开发阶段预留测试口子。非测试模式,走正常的业务逻辑;测试模式,走简化版 AppDelegate 逻辑。
|
||||
|
||||
第一步:改造 `main.m`
|
||||
|
||||
```objective-c
|
||||
int main(int argc, char * argv[]) {
|
||||
NSString * appDelegateClassName;
|
||||
@autoreleasepool {
|
||||
// Setup code that might create autoreleased objects goes here.
|
||||
id testClass = NSClassFromString(@"XCTestCase");
|
||||
appDelegateClassName = testClass ? @"TestMockAppDelegate" : NSStringFromClass([AppDelegate class]);
|
||||
}
|
||||
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
|
||||
}
|
||||
```
|
||||
|
||||
第二步:创建 mock 的简化版 `TestMockAppDelegate`,可以剔除一些 UI 测试不关心的逻辑。甚至只需要完成这个方法基础实现都可以。
|
||||
|
||||
|
||||
|
||||
### 探索
|
||||
|
||||
开发的单元测试代码,运行的背后也是一个可执行文件。查看内部,可以发现一堆和测试相关的 framework。
|
||||
|
||||
<img src="./../assets/XcodeUnitTestDyanimcFramework.png" style="zoom:30%" />
|
||||
|
||||
思考:一个工程中,只要写好了测试代码,是不是加载这几个动态库就可以运行测试用例了?
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 六、精准测试
|
||||
|
||||
精准测试是最近很火的一个概念,但是也不算在概念阶段,很多公司都落地并实施了精准测试。单测是开发者为了方法级别写的测试用例。精准测试是代码级别的测试覆盖。
|
||||
@@ -1316,10 +1369,12 @@ Accessibility 通过对 UI 元素进行分类和标记。分类成类似按钮
|
||||
|
||||
下面是之前在有赞,开发完精准测试系统后,落地到一个业务项目中取得的价值,帮助2位 QA 发现漏测的代码,倒逼 QA 去设计更完善的测试 case、分析覆盖率低是开发者的兜底代码太多还是真的漏掉了业务 case。
|
||||
|
||||
<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">
|
||||
<img src="./../assets/iOS_PreciousUnitTest1.png" style="zoom:20%;display:inline"> <img src="./../assets/iOS_PreciousUnitTest2.png" style="zoom:20%;display:inline">
|
||||
|
||||
精准测试助力业务,质量更加稳定。
|
||||
|
||||
精准测试怎么实现?核心问题是 iOS 侧开发语言有 OC、Swift,分别对应不同的编译器:clang、swiftc,插桩手段不一样。具体实现原理和细节可以看这篇文章:[精准测试最佳实践](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.108.md)
|
||||
|
||||
|
||||
|
||||
## 七、 测试经验总结
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
### 悲观锁
|
||||
|
||||
对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定会有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
|
||||
这种线程一旦得到锁,其他需要锁的线程就挂起。共享资源每次只给一个线程使用,其他线程阻塞,用完再把资源转让给其他线程。传统的关系型数据库就用到很多悲观锁这种几只,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。
|
||||
这种线程一旦得到锁,其他需要锁的线程就挂起。共享资源每次只给一个线程使用,其他线程阻塞,用完再把资源转让给其他线程。传统的关系型数据库就用到很多悲观锁这种机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。
|
||||
|
||||
### 乐观锁
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
#### 2. CAS 算法
|
||||
|
||||
**compare and swap(比较与交换)** ,是一种有名的**无锁算法**。 无锁编程,即在不实用锁的情况下实现多线程之间的数据同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫做**非阻塞同步(Non-blocking Synchorization)**。CAS 算法涉及到的三个操作数
|
||||
**compare and swap(比较与交换)** ,是一种有名的**无锁算法**。 无锁编程,即在不使用锁的情况下实现多线程之间的数据同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫做**非阻塞同步(Non-blocking Synchorization)**。CAS 算法涉及到的三个操作数
|
||||
|
||||
- 需要读写的内存值 V
|
||||
- 进行比较的值 A
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user