mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-24 20:00:37 +00:00
feature: Weex APM
This commit is contained in:
@@ -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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/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`
|
||||
|
||||
@@ -313,15 +313,15 @@ Tips:ninja 如果安装失败,可以直接从 [github]( https://github.com/n
|
||||
```shell
|
||||
mkdir llvm_xcode_build
|
||||
cd llvm_xcode_build
|
||||
cmake -S .https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/llvm-project/llvm -B ./ -G Xcode -DLLVM_ENABLE_PROJECTS="clang"
|
||||
cmake -S ../../llvm-project/llvm -B ./ -G Xcode -DLLVM_ENABLE_PROJECTS="clang"
|
||||
```
|
||||
|
||||
|
||||
因为要编写 Clang 插件,是 c++ 代码,所以需要借助 IDE 的能力,我们选用 Xcode 进行编译。如下图所示,代表编译成功
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMComplieXcode1.png" style="zoom:20%">
|
||||
<img src="./../assets/LLVMComplieXcode1.png" style="zoom:20%">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMComplieXcode2.png" style="zoom:20%">
|
||||
<img src="./../assets/LLVMComplieXcode2.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
@@ -349,11 +349,11 @@ Tips:ninja 如果安装失败,可以直接从 [github]( https://github.com/n
|
||||
|
||||
- 先创建一个插件文件夹 `code-style-validate-plugin`
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMAddConfiguration2.png" style="zoom:20%">
|
||||
<img src="./../assets/LLVMAddConfiguration2.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
@@ -382,17 +382,17 @@ Xcode 打开项目,选择自动创建 Schemes
|
||||
|
||||
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClangPluginSourceCode.png" style="zoom:20%">
|
||||
<img src="./../assets/ClangPluginSourceCode.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
初步编写后 Command + B 进行编译,在 Products 下可以看到编译产物:`CodeStyleValidatePlugin.dylib` 动态库。
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClangCompileProducts.png" style="zoom:20%">
|
||||
<img src="./../assets/ClangCompileProducts.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
@@ -402,7 +402,7 @@ Xcode 打开项目,选择自动创建 Schemes
|
||||
|
||||
此步骤的目的是:在 testLLVM 项目中,加载 `CodeStyleValidatePlugin.dylib` 插件可以成功。因为默认的 Xcode 使用的 clang/clang++ 编译器和编译 `CodeStyleValidatePlugin.dylib` 动态库不是一个版本。不做修改的话,Xcode 加载 `CodeStyleValidatePlugin.dylib` 会报错。所以需要先编译出同一个 LLVM 版本的 clang/clang++。
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeCompileClang.png" style="zoom:20%">
|
||||
<img src="./../assets/XcodeCompileClang.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
@@ -421,7 +421,7 @@ Xcode 打开项目,选择自动创建 Schemes
|
||||
- `-Xclang`
|
||||
- 插件名称
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeLoadPlugin.png" style="zoom:20%">
|
||||
<img src="./../assets/XcodeLoadPlugin.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
@@ -429,7 +429,7 @@ Xcode 打开项目,选择自动创建 Schemes
|
||||
|
||||
在新创建的 TestLLVM Xcode 项目中加载创建的 `CodeStyleValidatePlugin.dylib` 会报错。原因是:由于 Clang 插件需要使用对应的版本去加载,如果版本不一致则会导致编译错误。如下所示:
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeLoadClangPluginError.png" style="zoom:20%">
|
||||
<img src="./../assets/XcodeLoadClangPluginError.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
@@ -441,17 +441,17 @@ Xcode 打开项目,选择自动创建 Schemes
|
||||
|
||||
如下所示:
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeSpecifyClangPath.png" style="zoom:20%">
|
||||
<img src="./../assets/XcodeSpecifyClangPath.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
继续编译还是会报错,报错如下:
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/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://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeLoadClangThenBuildErrorFix.png" style="zoom:20%">
|
||||
<img src="./../assets/XcodeLoadClangThenBuildErrorFix.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
@@ -463,7 +463,7 @@ Tips: 由于重新修改了插件的源码,所以每次 Build 构建完 FANP
|
||||
|
||||
编译成功,可以看到在日志中输出了我们编写的日志信息。
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeLoadClangPluginTest.png" style="zoom:20%">
|
||||
<img src="./../assets/XcodeLoadClangPluginTest.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
@@ -496,7 +496,7 @@ NS_ASSUME_NONNULL_END
|
||||
|
||||
利用 Clang 查看 AST 指令为 `clang -fmodules -fsyntax-only -Xclang -ast-dump workaholic_person.m`
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClassNameViaClangAST.png" style="zoom:20%">
|
||||
<img src="./../assets/ClassNameViaClangAST.png" style="zoom:20%">
|
||||
|
||||
核心思路为:我们要分析类名不符合规范的情况,要精确报错,首先要识别到类名,利用 AST 的能力可以办到(类名在 AST 的 `ObjCInterfaceDecl` 节点上)。然后获取到类名的行号信息,精确报错。
|
||||
|
||||
@@ -786,7 +786,7 @@ X("CodeStyleValidatePlugin", "This plugin is designed for scanning code styles,
|
||||
- 可以对 Category 名做检测,如果带下划线,则报错提示并给出修改意见
|
||||
- 编写的 `CodeStyleValidatePlugin` Demo 中对不符合规范的做了 `DiagnosticsEngine::Warning` 级别的警告。如果遇到1个警告则不影响,继续编译。如果是 `DiagnosticsEngine::Error` 级别的编译报错,遇到1个则终止编译,请注意该区别,按需编写自己的插件逻辑。
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMClangPluginUseInXcode.png" style="zoom:25%">
|
||||
<img src="./../assets/LLVMClangPluginUseInXcode.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
@@ -834,7 +834,7 @@ X("CodeStyleValidatePlugin", "This plugin is designed for scanning code styles,
|
||||
|
||||
- 使用 LLVM 编写 Clang 插件,解析 AST 拿到所有的 `ObjCInterfaceDecl` 信息,然后结合 `ObjCMethodDecl` 信息便可获取 Category 中的 所有方法,再判断方法是否同名
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ClangASTCategoryMethod.png" style="zoom:20%">
|
||||
<img src="./../assets/ClangASTCategoryMethod.png" style="zoom:20%">
|
||||
|
||||
|
||||
|
||||
@@ -872,7 +872,7 @@ LLVM 项目中 clang 内置了一堆 checker,用于实现 lint。
|
||||
|
||||
- 自动调用 `[super dealloc]`
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLVMGenerateSuperDealloc.png" style="zoom:30%" />
|
||||
<img src="./../assets/LLVMGenerateSuperDealloc.png" style="zoom:30%" />
|
||||
|
||||
- 为每个拥有 ivar 的 Class 合成` .cxx_destructor` 方法来自动释放类的成员变量,代替 MRC 时代的 `self.xxx = nil`
|
||||
|
||||
|
||||
@@ -18,9 +18,149 @@ UIView 绘制流程。
|
||||
|
||||
当调用 UIView `[UIView setNeedsDisplay]` 方法时,系统会立刻调用其 Layer 的同名方法 `[view.layer setNeedsDisplay]` 方法,之后相当于给当前 Layer 打上一个脏标记,之后会在当前 RunLoop 快要结束的时候才会调用 Layer 的 `[CALayer display]` 方法。然后进入当前 UIView 真正的绘制流程中。
|
||||
|
||||
其次,会判断 CALayer 的代理,有没有实现 `displayLayer:` 方法,如果没有实现,则进入系统的绘制流程中;如果实现了,则可能是异步绘制或者自定义渲染的实现。
|
||||
其次,会判断 CALayer 的代理,有没有实现 `displayLayer:` 方法
|
||||
|
||||
- 如果没有实现,则进入系统的绘制流程:比如:创建绘制上下文、调用 `drawInContext:`、生成内容并赋值给 `contents`
|
||||
- 如果实现了,则可能是异步绘制或者自定义渲染的实现。是**代理自定义绘制的入口**。代理可以在这个方法里直接设置`layer.contents`(比如异步绘制生成 UIImage 后赋值给`contents`),完全接管 layer 的内容渲染
|
||||
|
||||
|
||||
|
||||
Demo1:
|
||||
|
||||
自定义 View,不实现 `displayInContext` 方法
|
||||
|
||||
```objective-c
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface CustomDrawView : UIView
|
||||
@end
|
||||
|
||||
@implementation CustomDrawView
|
||||
|
||||
// 重写drawRect: —— 系统绘制流程的上层入口
|
||||
- (void)drawRect:(CGRect)rect {
|
||||
// 1. 系统自动创建绘制上下文,这里可以直接获取
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
|
||||
// 2. 绘制操作(对应系统流程的「调用drawInContext:」阶段)
|
||||
// 设置填充色为红色
|
||||
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
|
||||
// 绘制一个矩形
|
||||
CGContextFillRect(context, CGRectMake(20, 20, 100, 100));
|
||||
|
||||
NSLog(@"执行drawRect: → 底层对应系统调用CALayer的drawInContext:");
|
||||
}
|
||||
|
||||
// 关键:不实现 displayLayer: 代理方法
|
||||
// - (void)displayLayer:(CALayer *)layer {} // 注释掉,模拟「未实现」场景
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
在 ViewController 中使用
|
||||
|
||||
```objective-c
|
||||
#import "ViewController.h"
|
||||
#import "CustomDrawView.h"
|
||||
|
||||
@interface ViewController ()
|
||||
@end
|
||||
|
||||
@implementation ViewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.view.backgroundColor = [UIColor whiteColor];
|
||||
|
||||
// 1. 创建自定义View并添加到界面
|
||||
CustomDrawView *drawView = [[CustomDrawView alloc] initWithFrame:CGRectMake(50, 100, 150, 150)];
|
||||
drawView.backgroundColor = [UIColor lightGrayColor]; // 浅灰色背景,方便区分绘制区域
|
||||
[self.view addSubview:drawView];
|
||||
|
||||
// 2. 触发绘制(打脏标记,RunLoop阶段系统会执行layer.display)
|
||||
[drawView setNeedsDisplay];
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
结果:屏幕上会显示「浅灰色背景 + 红色矩形」;控制台打印: `执行drawRect: → 底层对应系统调用CALayer的drawInContext:`;
|
||||
|
||||
分析:
|
||||
|
||||
- `[drawView setNeedsDisplay]` → 内部调用 `layer.setNeedsDisplay`,给 layer 打 “脏标记”
|
||||
- 当前 RunLoop 的 CATransaction 阶段,系统调用 `[layer display]`
|
||||
- layer 检查代理(CustomDrawView)有没有实现代理方法 → 未实现`-(void)displayLayer:(CALayer *)layer`;
|
||||
- 系统**自动创建绘制上下文** → 调用 `[layer drawInContext:]`(UIView 的`drawRect:`是对这个方法的封装,所以`drawRect:`被执行)
|
||||
- 系统将绘制结果生成位图 → 赋值给 `layer.contents`
|
||||
- 最终 layer 把`contents`内容渲染到屏幕
|
||||
|
||||
Demo2:
|
||||
|
||||
自定义 Layer,实现 `displatLayer:` 代理方法的 Layer
|
||||
|
||||
```objective-c
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
|
||||
@interface CustomLayer : CALayer
|
||||
@end
|
||||
|
||||
@implementation CustomLayer
|
||||
|
||||
// 重写CALayer的drawInContext: —— 系统绘制流程的核心方法
|
||||
- (void)drawInContext:(CGContextRef)ctx {
|
||||
// 系统创建的上下文会传入这个方法
|
||||
NSLog(@"系统调用drawInContext: → 进入核心绘制阶段");
|
||||
|
||||
// 绘制操作:画一个蓝色圆形
|
||||
CGContextSetFillColorWithColor(ctx, [UIColor blueColor].CGColor);
|
||||
CGContextFillEllipseInRect(ctx, CGRectMake(20, 20, 100, 100));
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
在 VC 中使用
|
||||
|
||||
```objective-c
|
||||
#import "ViewController.h"
|
||||
#import "CustomLayer.h"
|
||||
|
||||
@interface ViewController ()
|
||||
@end
|
||||
|
||||
@implementation ViewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.view.backgroundColor = [UIColor whiteColor];
|
||||
|
||||
// 1. 创建自定义Layer
|
||||
CustomLayer *customLayer = [CustomLayer layer];
|
||||
customLayer.frame = CGRectMake(50, 250, 150, 150);
|
||||
customLayer.backgroundColor = [UIColor lightGrayColor].CGColor;
|
||||
[self.view.layer addSublayer:customLayer];
|
||||
|
||||
// 2. 触发绘制(打脏标记)
|
||||
[customLayer setNeedsDisplay];
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
结果:屏幕上显示「浅灰色背景 + 蓝色圆形」;并且输出:`系统调用drawInContext: → 进入核心绘制阶段`
|
||||
|
||||
分析:
|
||||
|
||||
- 调用 CALayer 的 setNeedsDisplay 方法,内部会调用 display 方法
|
||||
- 系统会将其 CALayer 打上 dirty 标记
|
||||
- RunLoop 会在一次 loop 的末尾,提交 CATranscation。然后去绘制 layer 的 displayLayer 方法
|
||||
- 判断没有实现 displayLayer 方法,然后自动创建渲染上下文。
|
||||
- 然后调用 `drawInContext:(CGContextRef)ctx` ,方法的 ctx 参数就是系统自动创建的上下文对象
|
||||
- 该方法内创建的渲染内容,最后会合成一张 bitmap,最后交给 layer.contents 属性
|
||||
- 屏幕渲染 contents 内容
|
||||
|
||||
|
||||
Tips:`[UIView setNeedsDisplay]` 之后并不会立马调用 `[view.layer setNeedsDisplay]` 方法。要么手动触发 `[view.layer setNeedsDisplay]` 要么,调用一下 `-(void)drawRect:(CGRect)rect` 方法(即使是空实现也没关系)。
|
||||
|
||||
下面来个 Demo 展示下简单的异步绘制一个 String。
|
||||
|
||||
|
||||
@@ -1,5 +1,46 @@
|
||||
# Crash
|
||||
# LLDB
|
||||
|
||||
|
||||
|
||||
## LLDB 架构
|
||||
|
||||
<img src="./../assets/LLDBArchitecture.png" style="zoom:40%" />
|
||||
|
||||
说明:类似一个 CS 架构。
|
||||
|
||||
- **`lldb-mi` 的角色:** 它是一个**协议适配器**,专注于将 **GDB/MI 协议** 翻译成 **LLDB API 调用**。
|
||||
- **`API` 是关键桥梁:** `lldb-mi` 几乎所有的功能都通过调用 **LLDB Public API** 来实现。
|
||||
- **Core 下的模块是 LLDB 的能力:** 图中 `Core` 下列出的 `Object Files`, `Symbols`, `Process` 等,代表了 **LLDB 调试器引擎本身提供的强大核心功能**。`lldb-mi` 依赖 API 来利用这些功能。
|
||||
- **依赖关系:** `lldb-mi` -> `API` -> (LLDB 核心引擎的) `Object Files`, `Mach-O/ELF`, `Symbols`, `DWARF`, `Disassembly`, `LLVM`, `Process`, `GDB Remote`。
|
||||
- **目的:** 这张图说明了 `lldb-mi` 是如何构建在 LLDB 强大的核心调试引擎之上,通过标准的 API 调用其功能,从而为支持 GDB/MI 协议的客户端提供调试服务。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## LLDB Workflow
|
||||
|
||||
<img src="./../assets/LLDBWorkflow.png" style="zoom:40%" />
|
||||
|
||||
说明:
|
||||
|
||||
- **lldb-server**:调试服务端,`lldb-server` 作为“调试代理”,直接操控目标程序。分为两种部署方式:
|
||||
|
||||
- **Host 端**:调试本地程序(如 macOS 进程)。
|
||||
|
||||
- **Remote 端**:部署到目标设备(如手机/嵌入式设备),直接控制被调试程序
|
||||
|
||||
- 通信层:TCP + GDB RSP 协议
|
||||
|
||||
- **TCP Socket**:物理传输通道,连接 Host 的 LLDB 和 Remote 的 lldb-server
|
||||
- **GDB RSP (Remote Serial Protocol)**
|
||||
- **基于 ASCII 的调试协议**(明文消息,如 `$m<addr>,<length>#<checksum>` 读取内存)
|
||||
- 历史原因:兼容 GDB 的远程协议,使 LLDB 能对接各类设备(Android gdbserver 等)
|
||||
|
||||
QA:好像有点反人类设计,手机上反而是 Server,电脑端的 LLDB 反而是 Client?也就是为什么必须让手机充当 Server?
|
||||
|
||||
**权限问题**:
|
||||
|
||||
- 手机操作系统(如 iOS)禁止外部进程直接访问 App 内存。
|
||||
- 只有手机本地的 `lldb-server` 可通过系统权限(如 `task_for_pid()`)操控目标进程。
|
||||
|
||||
## Crash 类型
|
||||
- 容易越界(数组、字典、字符串等) NSRangeException
|
||||
- 使用未初始化的变量
|
||||
1054
Chapter1 - iOS/1.142.md
Normal file
1054
Chapter1 - iOS/1.142.md
Normal file
File diff suppressed because it is too large
Load Diff
461
Chapter1 - iOS/1.143.md
Normal file
461
Chapter1 - iOS/1.143.md
Normal file
@@ -0,0 +1,461 @@
|
||||
# AI 对端上的赋能
|
||||
|
||||
|
||||
## 一、实时特征回流
|
||||
传统智能的问题、弊端是什么?
|
||||

|
||||
|
||||
- 推荐系统需要收集用户的行为,将这些行为作为用户的意图表征,表征他的偏好,回流到服务端。
|
||||
- 服务端拿到这个数据,在发起一次实时请求的时候,会根据用户的行为特征,去商品池里面召回一批用户喜欢的商品,再返回给端上,给用户做展示
|
||||
- 同时,这个用户的行为数据,还会作为训练模型的一个样本
|
||||
|
||||
|
||||

|
||||
可以发现传统的推荐系统存在一些瓶颈:
|
||||
- 实时性不足
|
||||
行为数据回流的时效性,会影响算法对于用户意图变化的感知,会影响推荐的准确性。
|
||||
现有的系统,在手淘这种亿级用户体量下面,想要做到用户数据的实时回流,技术上面挑战很大。
|
||||
另外涉及到用户隐私方面的风险考虑,以及在服务端的存储瓶颈相关的考虑,是不能把用户所有的行为都回流到云端
|
||||
- 数据丰富度有限
|
||||
在服务端,整个用户行为数据的丰富度非常有限的。
|
||||
同时我们的一次推荐内容的更新,也是受限于一次新的请求时机的发起。
|
||||
即使我们发现用户的意图,通过数据发现了意图的变化,但是也很难实时对用户的前台界面去做一次干预,去及时调整推荐的内容
|
||||
- 但用户算力/存储瓶颈
|
||||
- 千人一面的模型
|
||||
目前服务端的算法模型更多的还是千人一面。受到算力和存储的瓶颈,很难针对每一个用户去建立一个属于他自己的模型。
|
||||
去做更加精准的预测。
|
||||
|
||||
针对上面的这些问题,就是端上的智能可以去发挥的空间所在。
|
||||
|
||||
|
||||
|
||||
前面说过,用户的数据回流时效性会影响推荐的准确度。那么是不是可以把用户的特征、用户的数据,做到最实时的回流?
|
||||
这里,我们做了一些这样的尝试:
|
||||

|
||||
|
||||
- 首先可以把用户的最原始的行为数据回流。比如用户在逛手淘的过程中产生的一些浏览、点击等行为回流到服务端
|
||||
- 同时,也可以把用户的这些原始行为数据做一定的聚合,生成一个信息量含量更高,但是数据量更小的用户特征数据(数据聚合)
|
||||
比如把用户在商品详情页的一系列的特征,聚合成用户对商品详情的更精炼的一些数据,比如用户有没有对商品点击过收藏、有没有点击过加购,聚合成
|
||||
一条商品详情的浏览特征
|
||||
- 也可以对这个特征继续做精加工处理,变成一个算法模型可直接使用的特征向量。当然了,它也可以表征用户的行为和意图
|
||||
- 还可以把这个向量做进一步精加工处理,生成一些用户的意图打分。比如用户对于某个商品详情的意图分,是强还是弱,用分数去表明。
|
||||
|
||||
比如用户在手淘里面浏览的过程当中,用户逛着逛着是不是不感兴趣了?是不是疲劳了?这也可以用来表征用户的一个“跳失意图”。
|
||||
|
||||
这几种数据都可以回流到服务端的,图上数据可以看到:
|
||||
- 从左往右:数据的加工度越来越高的、数据量是越来越少的
|
||||
- 从右往左:数据量大,信息密度低
|
||||
|
||||
回流到服务端的时候,对于这4种不同类型的数据,一一做过尝试:
|
||||
- 首先,如果直接回流用户的原始数据,那么这个数据量会非常大,服务端的存储存在压力。另一方面也会涉及到用户的隐私风险
|
||||
- 其次,我们也尝试过,将用户的行为数据聚合成一条向量直接回流到服务端,数据量虽然小了,但是会丢失一些信息。另外向量这种数据格式,通用性会非常的受限。智能针对特定的模型去回流特定的向量。
|
||||
- 另外,也尝试过直接回流用户的意图分,比如回流一条对于商品详情页的意图分,在整个手淘的流失的意图,这个数据是有效的,但是它丢失的信息也是非常多的
|
||||
|
||||
所以最后选择的是将用户的数据在端上做一定的标准的加工化处理,聚合成特征回流到服务端,这是实践过比较好的,既能保证数据的有效性又能保证实时性的一种方式。
|
||||
|
||||
|
||||
### 1. 实时特征回流
|
||||
|
||||
- 数据本地加工,做标准化处理,然后按照需要将需要的那部分数据回传到服务端
|
||||
|
||||
解决了什么问题?
|
||||
- 提升了数据的丰富度,能够在服务端拿到用户更多的、更细粒度的一些行为数据。能够让服务端的数据输入变得更丰富
|
||||
- 通过对数据加工之后,建立一条实时的特征回流通道,保证了从端到云上的数据实时性
|
||||
|
||||
遗留了什么问题?
|
||||
- 通过实时的回流方式,解决了用户实时感知的在实时性方面的问题
|
||||
- 但即使感知到了用户意图发生了变化,,也缺少一个实时在前台去干预用户的方式
|
||||
|
||||
|
||||
### 2. 信息流的“回退推荐”
|
||||
针对实时特征无法具备实时干预能力的问题,在信息流方面做了一种叫“回退推荐的策略”
|
||||
|
||||
想象这样一个场景:用户在商品列表页,对某个商品感兴趣,点击某个商品到达详情页,在详情页看了一番之后,用户点击了收藏或者加入到购物车了。有收藏、加购行为表示用户对这个商品是很感兴趣的。
|
||||
|
||||
此时,从商品详情页到回退到外面这个商品列表页的时候,会根据用户刚刚的浏览、加购行为,推荐一个相似的商品。
|
||||
|
||||

|
||||
|
||||
会根据用户刚刚浏览的这些行为,去给他推荐一个相似的商品。这个过程会发生很多行为,通过滚动、曝光等行为可以推测用户在信息流的浏览意图是逐渐从逛切换到了买。
|
||||
|
||||
如果页面回退,我们不做干预的情况下,他可能继续往下去浏览,购买这个行为的意图差“临门一脚”就可以转换为一次成交购买。如果不加以干预,可能逛着逛着就丧失购买意愿了,流失一笔潜在交易。
|
||||
|
||||
选择的策略是:在页面回退的时候,在原来的商品卡片周围,立即推荐一个相似的商品,希望能继续促成,希望能够留住他刚刚这次对商品购买的强意图。这个就是回退推荐。
|
||||
|
||||
本质上就是抓住了用户从逛到买的这个强意图聚焦。针对强意图的聚焦,做了一次成交转换的促成。
|
||||
|
||||
商品卡片的回退推荐策略落地上线后,效果是非常好的。比普通商品卡片的转换率高五六倍左右。
|
||||
|
||||
|
||||

|
||||
|
||||
存在什么问题?
|
||||
类似程序员和产品设计沟通出的一种机制,可以理解为命令式编程,是人为先验地找到了一个能够代表用户意图的时机,也就是在回退时机。这种时机靠人去发现梳理,往往是很难覆盖全面的。依赖于对于用户强意图的梳理、选择。那么有没有一种方式可以自动的去预测用户意图的变化?
|
||||
|
||||
|
||||
|
||||
## 二、应用场景
|
||||
|
||||
### 1. 信息流的端侧重排
|
||||
|
||||
在信息流上面做了另外一个尝试:在本地进行了一次端上的重排。
|
||||
|
||||

|
||||
|
||||
|
||||
一个用户在逛信息流的过程中,会产生滚动、曝光、点击、加购、收藏、停留、回退、滚动这些行为。从用户的实时的行为序列中,其实是表征了用户背后的一个隐式的意图表达。
|
||||
|
||||
可以把用户的实时行为序列去输入到一个意图的模型当中,去计算用户当前的意图是什么?他偏好哪些、不喜欢哪些?得到用户的正负一些意图反馈。去判断用户当前是不是正在一个疲劳的状态,去计算他即将要跳失的可能性。
|
||||
|
||||
然后将这些用户意图,输入到一个本地的实时决策模型中去,去决定接下去要给这个用户去做什么事情。例如用户是不是对于接下来要滚动浏览的商品兴趣是不是发生了变化的时候,能够根据用户的实时意图,去做一次实时的调整,永远把用户最喜欢的内容放在他排序更靠前的位置。
|
||||
|
||||
或者当发现对这批商品都不感兴趣的时候,就理解去重新更新一次商品。或者当发现用户即将跳失之前,去做一些强干预,去挽留他继续留在这个页面上。比如通过一些权益去做挽留。
|
||||
|
||||
做完决策后,就可以将这个决策结果通知到前台,去做相应的响应。
|
||||
|
||||
还存在一些问题:
|
||||

|
||||
|
||||
决策选择还是受限于产品策略。程序员还需要和产品去约定设计产品策略。开发和产品共同约定,在用户的某一时机之下,接下来对应的一个处理是什么。它的整个呈现形式和所处业务域还是存在紧密关系的。
|
||||
|
||||
这种应用形式,应用在信息流上是非常好的,但是很难迁移到其他业务域。
|
||||
|
||||
### 2. 智能 Push
|
||||
|
||||
在这样的背景之下,接下来开始下一个尝试,去做一个更加通用的、端上的智能应用。去 Push 业务。
|
||||
|
||||
传统业务上,Push 是服务端发起的。服务端存在一个任务,不断计算:我需要给什么样的一批用户、推送一批什么内容。服务端跑了这个任务后,会圈选一批人群,定一个任务,给这些人要去发一个推送。客户端收到这个推送,展示这个推送的消息内容。
|
||||
|
||||
服务端发起的 Push 有啥问题?
|
||||
- 缺乏感知能力,难以精细化运营
|
||||
服务端不知道用户当前 App 的状态,用户在手淘内还是不在。没有办法知道用户的实时状态,很难针对用户的实时状态去做精细化运营。
|
||||
- 被动触达,错失最佳营销时机
|
||||
更希望的是针对用户的某一个精细化行为,去做一次响应的时候,服务端 Push 是做不到的。
|
||||
|
||||
|
||||
完整流程:
|
||||

|
||||
|
||||
用户进入手淘后,会不断收集端上的行为(滚动、曝光、点击、加购、收藏、停留)数据,然后会把行为数据输入到另一个意图模型中,去判断他当前的一些实时意图。不断的根据行为做分析。也会输入到另一个决策模型中去,但这个(智能 Push)场景下的决策模型和前面的端上重排场景的决策模型是不一样的。这个决策模型会**判断用户当前的状态适不适合接受一次干预**,或者适不适合接受某一次营销的推荐。当我们发现用户当前处于一个相对空闲的状态,这个时机更适合接受一条干预的时候,我们就会把这个信号通知到服务端。这时候服务端在海量的内容中去筛选出一条对应着用户当前的意图,有效的一条信息,再推送给客户端。
|
||||
|
||||
这种从客户端发起的 Push 推送相比于服务端推送来说,对于用户当前的实时状态有着非常强的感知的,用户发生的任何一个行为在端上的决策模型中,可以以毫秒级的速度获取到,当我们真的需要去对用户做一次精准的干预的时候,这种方式相比于服务端推送来说,是有着非常大的优势的。
|
||||
|
||||
- 从“平台视角”向“用户视角”的转变
|
||||
传统的服务端推送是站在平台视角,来去筛选内容、筛选用户去发送消息的。而“端智能 Push”更多站在用户视角,去分析用户在什么时机下适合接受干预。
|
||||
- 解决了什么问题
|
||||
对端上单用户的算力空间的充分利用;同时智能 Push 分离了用户的感知决策。用户的感知可以作为一个独立的模块存在。用户的决策:接下来要做什么响应。也是一个比较独立的模块。在应用性上具备初步的可移植性。可以在多个应用场景,不只是信息流这样一个较为垂直的业务域上去使用。
|
||||
- 没有解决什么问题?
|
||||
决策依然需要先验制定。
|
||||
|
||||
我们对于 AI 的期待是美好的,期望 AI 可以帮助我们决定下一步做什么。然而通过这些案例可以发现,现阶段,我们还是只能做到在一个已经决策好的产品框架下面去做。是需要先有一系列的决定(在什么样的情况下面,可以有哪些响应),那么 AI 是帮助这样一个决策的结果更加精准。
|
||||
|
||||
### 3. 智能预加载
|
||||
|
||||
根据用户身份、角色、常见行为路径,预测接下去要使用的功能,对可能要进入的页面进行预热、预加载,在用户访问这个页面之前,把页面准备好,来做到秒开的效果。
|
||||
|
||||
也就是说,如果能精准预测用户下一步将要去往哪里,对于性能来说,提升是会非常明显的。
|
||||
|
||||
那么最关键问题就是:**如何预测用户下一步将要去往哪里**?
|
||||
|
||||
### 4. 手势热点识别
|
||||
|
||||
判断用户热点的操作区域是什么?针对这块区域来做一些定制化的特定推荐。
|
||||
|
||||
### 5.智能营销投放
|
||||
|
||||
用算法的更加精准的预测,去替代 以往的业务规则的人群圈选。
|
||||
|
||||
|
||||
对于端智能来时,它属于基础能力。它用在哪里,才是能不能用好的一个关键,也就是业务价值能不能提升。
|
||||
|
||||
从过去的应用来说,我们认为端智能对于客户端的改变主要体现在:
|
||||
|
||||
- 更多的数据
|
||||
在端上做算法模型的预测,可以拿到用户在端上更富丰富、更细粒度的数据,去避免在服务端取不到这样丰富数据的缺陷。
|
||||
- 更实时的响应
|
||||
相比于服务端,很多系统称具有小时、分钟、秒级别的实时响应,在端上的实时性是带来本质性的变化。
|
||||
- 更低的消耗
|
||||
闲置算力去运算和存储资源,带来本质上的实时性的提升。同时也节约了服务端资源的消耗。
|
||||
|
||||
|
||||
## 三、端智能整体架构
|
||||
要素:算法、数据、调度框架、运行环境
|
||||
架构如下:
|
||||

|
||||
|
||||
1. 围绕着端上的算法分为2种模型:
|
||||
- 用户意图计算模型:不断分析用户当前所处状态
|
||||
- 决策模型:会根据用户意图计算的结果,去判断下一步要做什么样的处理。比如一次本地重排、还是去做一次数据的重新刷新
|
||||
|
||||
2. 作为端上的算法输入,建立了一个端上的特征中心,用来提供给端上的算法使用:提供标准的用户行为数据、以及一些特征服务。
|
||||
|
||||
3. 还建立了端上的用户决策框架:接受用户的每条行为数据,然后根据这些行为去决定接下来什么时机要去唤起一个什么样的模型。拿到这个模型的响应结果后再一路回传到我们的客户端应用层。应用层根据这个结果来做前台界面上的渲染。
|
||||
|
||||
4. 围绕着端上的算法所在的执行环境,是在底层有一个端计算的容器,提供 Python 的运行环境以及 MNN 轻量级的推理引擎
|
||||
|
||||
5. 对于整个算法研发的 workflow,配套的做了一个端计算的一体化研发平台。算法同学在这个平台完成开发到发布、再到 AB 实验以及模型训练的一系列工作。
|
||||
|
||||
模型从这个平台发布后,是会下发到客户端,然后在端上跑。
|
||||
|
||||
### 1. 端上算法方案
|
||||
|
||||

|
||||
|
||||
- Algorhitms Solution On Edge:
|
||||
- 提供了模型、特征和样本这三大机器学习算法基础组件的端上通用方案
|
||||
- Business Solution On Edge:
|
||||
- 端上推荐算法解决方案,提供了端上实时用户感知和端上智能决策2大模块
|
||||
- 通过多任务学习,端上智能决策支持了端上重排、端上智能刷新、端上会话式推荐和端上跳失点预测等任务
|
||||
- 千人千模:
|
||||
- 每个用户训练和部署自己的个人化模型
|
||||
- Meta-learning + Federated Learning
|
||||
|
||||
### 2. 端上特征中心
|
||||
为端智能应用而设计,提供端侧算法所使用的标准化的全域用户行为数据和特征服务
|
||||

|
||||
- 定义端侧用户行为标准
|
||||
该特征中心会定义端上用户的行为标准,产生什么样的用户行为,比如用户的浏览行为。这个浏览行为会有一些我的浏览区域、浏览停留时长等等标准化属性。其次,也会有一些像用户手势行为。比如滚动、点击等等。
|
||||
- 建立行为数据图化索引
|
||||
具体的实现上,为用户的每个行为,去建立了一个行为的图画的索引,将用户的行动点当作一个节点,并且把节点和节点之间建立了一种关联。这样子能够在端上,让算法可以快速拿到这个数据。
|
||||
- 数据标准化
|
||||
同时,采集到这个数据之后。也会对数据做标准化处理。把它经过标准化的字段解析和我们的特征加工,给算法提供简单、易用、可用的数据
|
||||
- 通用特征接口服务
|
||||
|
||||
|
||||
|
||||
数据分层架构:
|
||||

|
||||
|
||||
- 存储层:将采集到的用户行为数据按照约定的标准,在客户端本地做持久化存储。同时对用户数据进行一次加工处理,生成一份信息密度比较高的基础特征表。比如对详情页的浏览行为、App 页面间操作路径的数据、页面浏览的时序特征,这些数据都会存储在客户端本地。
|
||||
- 接口层:实时接口层,提供了 Python 层面的接口服务,给算法侧使用。可以做到数据的实时查询,将下层的通用数据、用户行为数据、环境信息等打包好给算法侧使用。
|
||||
这个数据一部分存在端上,一部分存储在云端,和云端有个数据同步需求的:
|
||||
- 从端同步到云,将一些必要的基础特征同步到服务端,让服务端可以拿到用户实时的聚合好的特征。
|
||||
- 从云到端,也可以把云端特有,客户端没有的数据(比如用户画像、历史行为等等)下发下来,这样子可以让端上的算法也能拿到这部分数据,做出更精准的预测。
|
||||
|
||||
|
||||
### 3. 端上的决策中心
|
||||

|
||||
比如:用户在:我的淘宝 -> 我的订单 -> 订单详情页查看了某个订单详情,然后回退到“我的淘宝”页,这时候会对用户的意图进行分析,判断当前是不是处于一个空闲的状态。如果发现是空闲状态,则给他发送一条 Push 消息,引导进入双11主会场。这个就是一个智能 Push 的案例。
|
||||
|
||||
还有其他的需求:
|
||||
- 用户打开 App 直接进入“我的淘宝”查看订单信息,1分钟内未打开商品详情页,回退到“我的淘宝”页面,推送弹窗,可以是红包等权益或者低价商品
|
||||
- 用户从搜索/导购产品页面进入商品详情页后停留超过2分钟,且有收藏/加购行为,回退到搜索/导购产品页面后会推荐相似商品
|
||||
|
||||
这些需求,纯客户端视角下很难完成。所以基于用户行为的端侧事件引擎,提供面向全域用户行为的切面开发模式,打破业务间的隔离,实现以用户为中心的跨业务域的决策能力。
|
||||
|
||||
这样一个切面能力的好处就是:
|
||||
- 业务开发同学不需要去关心前面的这一串行为是啥时候触发的、怎么发生的。这个行为的匹配由端上的决策框架去做。
|
||||
- 实际的开发同学,只需要去关注在切面发生的时候,我需要去做哪些处理。比如:弹层
|
||||
|
||||
|
||||
这种面向用户行为的切面的编程方式,既可以用在运营规则上,也可以用在算法模型上。后续的响应,可以是弹出弹层、发送 Push、发送1次请求等。
|
||||
|
||||
### 4. 端计算容器
|
||||

|
||||
端上的算法模型需要跑在容器里,手淘用的是一个轻量级的推理引擎 MNN。MNN 提供了算法在端上跑模型所需要的算子。
|
||||
|
||||
## 四、云端一体协同
|
||||

|
||||
上图是端计算的优势和云计算的劣势。
|
||||
未来的端计算并不是完全割裂的。端和云协同才可以迸发最大的效果。
|
||||
|
||||

|
||||
可以看到端和云拥有各自擅长的领域。
|
||||
|
||||
在做云端协同的过程中,会遇到不少问题。比如在端上触发一次重排的时候,会发现端上的数据量是不够的,如果想要提升端上的重排效果,就要扩大候选池,所以增加了**端上的缓存池**。在端上的模型在本地运行过程中,由于模型本身是在服务端训练的,它的模型和特征向量的同步一致性是需要细节方面处理好的。
|
||||
同时由于在端上选择用户时机去做一些干预,实际上对于服务端是带来一些 QPS 增量,如何解决处理使其不是负担也需要处理。
|
||||
|
||||
这些问题处理好,最后需要回答价值多大问题的时候,就需要做实验验证端计算是否真的有效的时候,在实验的设计、以及后续数据分析、结果归因上,也耗费了大量的精力去论证它的有效性。这些是踩坑之处。
|
||||
|
||||
|
||||
|
||||
QA: 决策框架与 MNN 推理引擎的区别是什么?
|
||||
|
||||
| 维度 | MNN (推理引擎) | 决策框架 |
|
||||
| :----------- | :-------------------------------------------------------- | :----------------------------------------------------------- |
|
||||
| **技术定位** | **底层计算基础设施** | **上层业务调度与编排系统** |
|
||||
| **核心职能** | **“算”** - 高效执行模型计算,输出预测结果(如用户意图分) | **“判”与“调”** - 判断在什么时机、调用哪个模型、并根据结果执行哪个业务动作 |
|
||||
| **类比** | **笔墨和运笔技法**(负责把字写出来) | **书法家的头脑和章法**(决定何时写、写什么字、怎么写布局) |
|
||||
| **关注点** | 计算性能、算子支持、模型兼容性、功耗 | 业务逻辑、决策流程、时机控制、动作执行 |
|
||||
| **输出物** | 模型的数值化输出(如分数、概率) | 一个具体的业务指令(如:重排、刷新、弹窗) |
|
||||
|
||||
为了更好地理解,我们可以看一个它们如何协同工作的例子,比如“端侧重排”:
|
||||
|
||||
1. **决策框架感知时机**:决策框架监控到用户发生了一系列行为(滑动、点击、停留),判断**此时需要重新计算用户意图**。
|
||||
2. **决策框架调用MNN**:决策框架**调度**“用户意图计算模型”开始工作,并将必要的特征数据准备好。
|
||||
3. **MNN执行计算**:**MNN引擎**加载并运行该模型,进行高速数学计算,最终输出一个**用户当前的意图分数**。
|
||||
4. **决策框架做出决策**:决策框架**接收**MNN返回的意图分数,再结合预设的业务规则(例如:分数高于X则触发重排),**判断**下一步应该执行“本地重排”动作。
|
||||
5. **决策框架执行响应**:决策框架**通知**前端的渲染模块,对商品列表进行重新排序。
|
||||
|
||||
|
||||
|
||||
## 五、AI 在有赞落地了什么场景
|
||||
|
||||
### 1. 云打印机秒连接
|
||||
云打印机接入 OCR + LLM 技术实现 AI 拍照秒识别,秒连打印机,一拍即用,操作简单
|
||||
门店商家很多都会去连接打印机,连接过程中成功率只有54%左右。分析了相关原因,发现有2个主要原因:
|
||||
- 商家找不到打印机的连接入口
|
||||
- 打印机的配置过程中需要输入相关的编号和密钥。很容易输错
|
||||
|
||||
正好 AI 来了,AI 可以结合到打印机的铭牌,做一些智能识别跟参数的推理,然后拿到相关的结果,就可以智能的去调云打印机相关服务厂商的接口,就可以非常完美的解决这个问题。
|
||||
|
||||
我们对接了非常多的云打印机的品牌厂商,通过 AI 能力,去设计一些提示词,针对这些不同厂商的差异要做一些不同场景的兼容设计,过程中其实踩了不少的坑,不断的调优,让所有的硬件识别准确率提升了非常多。
|
||||
|
||||
我们的产品改造完之后,在商家平台网页端,硬件添加入口,让商家上传铭牌照片就可以了。原来的十步变成1步。连接成功率也从原来的不到70%提升到97%。
|
||||
思路,以后的业务需求,可以尝试跳出 Web 前端能力、Native 能力,从 AI 侧看看有没有更多的可能和思路。
|
||||
|
||||
|
||||
### 2. 基于图像识别算法的零售移动智能收银方案
|
||||
|
||||
#### 1. 背景
|
||||
生鲜果蔬行业在零售行业中是一个较大且比较有特征性的行业,同时在生鲜果蔬行业中,称重秤为经营的刚需类设备。目前商家主要使用条码秤,通过 PLU(Price Lookup Code) 码进行商品的管理,每个 PLU 码对应一个商品,我们可以想象下在超市购买水果的时候会碰到下面这个流程:
|
||||
|
||||

|
||||
|
||||
所以在门店商品种类比较多时(一个典型生鲜果蔬类商家商品种类大多超过 200 个,随机调研了 5 家有赞果蔬商家,平均 SKU 数量 500+),PLU 码较难记忆清楚,在打秤时需临时查询,称重耗时比较长,为了避免高峰时期排队现象,需在门店增加秤台和打称员,导致商家人力成本较高。
|
||||
|
||||
因此就前面所提到的场景,我们需要通过更加智能的方式帮助商家加购,那么基于机器学习的图像识别能力就被提上了议程。我们通过条码秤关联的摄像头进行实时拍摄,基于机器学习技术和图像识别技术,将店员放置在秤盘上的商品进行识别,并给出相关商品的列表,减少收银员收银场景中的操作次数,减少商家对新收银员的PLU码的培训并降低熟悉相关商品的培训成本,从而在整体上降低收银员的门槛以及商家的人力成本。所以我们可以得到我们期望的购买流程:
|
||||
|
||||

|
||||
|
||||
#### 2. 架构设计
|
||||
我们针对于商家的痛点和可行的解决方案绘制了下面的流程图:
|
||||

|
||||
|
||||
整个流程中的基础能力:
|
||||
- 实现摄像头对于商品的拍摄
|
||||
- 针对于拍摄能力支持图像转换成为商品的能力
|
||||
- 将识别结果进行列表化展示
|
||||
- 将用户点击之后的结果进行上报,用于商家个性化画像的绘制,以及机器学习模型的加深
|
||||
- 支持机器学习模型的动态下发
|
||||
|
||||
|
||||
#### 3. 框架选择
|
||||
首先是整个环节的核心点,对于商家商品的关联以及数据模型进行机器学习并完成定时的增量更新流程。通过对比市面上已有的框架,因为 TensorFlow 有 lite 版本单独支持移动端能力,同时结合有赞算法团队已有的技术沉淀,所以敲定使用 TensorFlow 作为机器学习的基础框架。完成了框架的确定,就需要考虑业务场景上的实现了。
|
||||
|
||||
|
||||
#### 4. PLU 码是什么
|
||||
PLU 码是生鲜零售行业的 “商品价格查询码”,核心是用一串数字唯一标识一种散装生鲜商品,方便打秤时快速调取价格
|
||||
特点:
|
||||
- 本质是 “商品与价格的关联码”,替代人工记忆商品价格的繁琐操作。
|
||||
- 通常由 4-5 位数字组成,可分为行业通用码和商家自定义码。
|
||||
- 仅用于散装生鲜(如蔬菜水果、散装零食),预包装商品多用地条码。
|
||||
|
||||
比如我买了10斤红富士苹果,怎么体现?PLU码会携带重量、价格信息吗?收银员拿到这个 PLU 码之后的处理流程是什么样的?
|
||||
PLU 码不只为生鲜类目(主要服务散装非标品),本身不携带重量/价格信息,仅关联 “商品 + 单价”信息。10 斤红富士需通过 PLU 码调单价 + 秤称重算总价,收银员核心流程是 “输码 - 调价 - 称重 - 结算”。
|
||||
|
||||
##### 1. PLU 码的适用范围:不止生鲜,但聚焦 “散装非标品”
|
||||
核心适用场景:散装生鲜果蔬(如苹果、生菜)、散装干货(如核桃、枸杞)、散装零食(如糖果、饼干),这些商品无固定包装、需按重量计价。
|
||||
非适用场景:预包装商品(如盒装牛奶、袋装面包),这类商品有固定重量 / 价格,用地条码(EAN 码)而非 PLU 码。
|
||||
简单说:PLU 码是 “散装称重商品的专属身份码”,生鲜是主要使用场景,但不是唯一场景。
|
||||
|
||||
##### 2. 10 斤红富士的 PLU 码使用逻辑:重量靠秤、价格靠计算
|
||||
PLU 码的核心作用是 “快速调取商品单价”,重量和总价需结合秤的功能实现:
|
||||
第一步:红富士对应 PLU 码(如通用码 4133),商家已在秤中录入 “4133 = 红富士,单价 8 元 / 斤”。
|
||||
第二步:你买 10 斤红富士,打秤员将苹果放在秤上,输入 PLU 码 4133。
|
||||
第三步:秤自动读取 “10 斤” 重量,按 “单价 8 元 / 斤 ×10 斤” 算出总价 80 元。
|
||||
|
||||
关键:PLU 码只负责 “告诉秤这是什么商品、多少钱一斤”,重量是秤测量的,总价是系统实时计算的,三者独立但联动。
|
||||
|
||||
##### 3. 收银员拿到 PLU 码后的完整流程
|
||||
以超市购买 10 斤红富士为例,流程分 5 步:
|
||||
- 商品上秤:将散装红富士放在条码秤的秤盘上,秤实时显示重量(10 斤)。
|
||||
- 输入 PLU 码:收银员手动输入 4133(红富士 PLU 码),或通过扫码枪扫预存的 PLU 码贴纸。
|
||||
- 系统调参计算:秤通过 PLU 码调取预设单价(8 元 / 斤),自动计算总价(8×10=80 元)。
|
||||
- 打印价签:秤打印含 “PLU 码、商品名、重量、单价、总价、日期” 的价签,贴在商品上。
|
||||
- 收银结算:你拿着贴有价签的商品到收银台,收银员扫价签上的条码(或手动输入 PLU 码),收银系统确认价格后完成支付。
|
||||
|
||||
#### 5. 商品关联
|
||||
完成了框架选择,接下来就需要确定如何将商品关联到数据模型上了。
|
||||
|
||||
有赞的商品有很多的字段,比如说:编码、条码、规格、属性等等。“有赞商品的编码、条码等唯一标识字段,零售场景已支持识别,但这类标识仍需人工关联 PLU 码才能完成称重结算,未能解决‘PLU 码难记忆、打秤查询耗时’的核心痛点。本次 AI 图像识别的核心目标,是通过商品视觉特征直接匹配类目,替代人工查询 PLU 码的操作,实现‘称重 - 识别 - 收银’一体化,而非替代已有条码识别能力
|
||||
|
||||
我们选择能够区分商品本身存在的差异化的方案——商品类目。
|
||||
|
||||
有赞的商品类目最大为 4 级,最后基本上已经能够细分到水果的某一个种类中。举个例子:一个苹果,在有赞类目中的选择需要被选择成为 `食品酒水 > 水产肉类/新鲜蔬果/熟食/现做食品 > 新鲜水果 > 苹果`,同时考虑到苹果中仍然存在不同的品种。所以我们在商品类目中追加了水果种类用于区分不同的苹果品种,比如说:金帅、国光、冰糖心……
|
||||
|
||||
|
||||
#### 6. 反馈闭环
|
||||
在确定了核心能力的解决方案后,接下来需要解决的是如何将商家本地的数据进行上传,并对于已有模型进行强化。为了更加及时的获得用户本地的选择情况,我们选择了有赞埋点平台作为技术支撑,通过离线缓存,并结合闲时上报的能力,将用户选择图片的整体筛选情况,基于店铺/角色等维护进行拆分,并将最终的选择数据导入 ODS 库中。并在算法前结合用户选择时机的拍摄图片上传 + 用户选择商品情况进行结合,进一步针对于对应店铺的模型进行加强。从而在不断的强化商家模型,从而提高用户准确性。
|
||||
|
||||

|
||||
|
||||
#### 7. 流程优化
|
||||
要达到好用的程度。所以我们需要对于数据统计流程/用户交互流程进行更加深入的优化
|
||||
|
||||
##### 1. 自动化类目关联
|
||||
考虑到许多商家在使用零售的过程中,如果需要挂载到对应的类目中成本较高,为了减少商家的操作成本。我们基于商家的商品标题 + 图片提供了默认类目和种类的选择。极大程度上降低了用户的操作成本。
|
||||
|
||||
|
||||
##### 2. 图片上传/优化
|
||||
根据分析商家的实际使用场景,我们发现部分商家在售卖生鲜果蔬的同时,仍然会同时售卖一些标品,比如说日常的柴米油盐酱醋茶。所以导致商家在正常选择商品时候,设备相机仍然会采集相关照片。从而导致商家返回中会存在一部分无效图片,从而影响后续的数据分析,所以我们需要在进行机器学习算法前进行空盘图片的判断,从而避免无效数据对于数据集的影响。
|
||||
|
||||
同时由于电子秤的硬件特性:当物品放置到称上的时候,电子秤中的读数从 0 变化到物品实际重量的流程是非线形的,这就导致了,如果我们在这过程中进行数据采集,可能会采集物品非稳定的状态。所以我们约定在电子秤的读数停止变化的时候才进行数据采集,从而保证数据的有效性。
|
||||
|
||||
除此之外,由于设备的服务商的不同,导致不同服务商对于设备摄像头的调校结果也不相同。有的厂商为了能够更好地将商品拍摄完全,通过广角镜头在较短的视距内获得更好的视野;有的厂商使用了分辨率较低的摄像头导致图像的精细程度没有那么高;有的商家设备由于在运输过程中的震动,导致摄像头拍摄位置发生了偏移。以上种种问题,我们都需要通过一些图形学的解法,将所有的逻辑处理成为统一的结果。所以在最终进入机器学习的算法前,我们需要通过图像处理,对图片进行裁切、锐化、畸变矫正。从而保证了不同设备的数据一致性。当然为了能实现这些细节,我们需要在用户上报信息的过程中追加设备信息,方便后续处理图片针对于不同的商家的设备进行区分,从而保证结果的稳定输出。
|
||||
|
||||
##### 3. 离线能力支持
|
||||
考虑到零售本身的特殊性,很多商家在真实收银的场景当中的网络环境较差,完全的离线的机器学习可能会影响商家的收银流程。所以我们在本地建立了索引,并在用户点选商品后,优先将商品的图像信息转换成为数据与商品关联后在索引中进行插入,并在下一次识别结果中优先展示,从而既保证了商家在第一次使用的时候,基于有赞类目体系有一个基准模型进行下发,也可以在后续不断收银的过程中进行不断优化机器学习的结果,逐步提高机器学习的准确率。
|
||||
|
||||
|
||||
|
||||
## 六、QA
|
||||
|
||||
### 1. 什么是索引?
|
||||
|
||||
#### 通俗化解释
|
||||
|
||||
想象一下,你是一个新店员,老板给你做培训:
|
||||
|
||||
1. 老板指着一个苹果说:“这个叫‘红富士’,PLU码是4133。”
|
||||
2. 你的大脑:并不会像相机一样存下苹果的完整图片,而是会提取关键特征——“圆圆的、红色带黄条纹、有个把儿”。你把这些特征和“红富士/4133”这个信息关联起来,记在脑子里。
|
||||
这个过程就是 “将商品的图像信息转换成为数据与商品关联”。
|
||||
- 商品的图像信息:一张苹果的彩色图片(原始数据,体积大,难以直接比较)。
|
||||
- 转换成为数据:通过一个复杂的AI模型(通常是卷积神经网络),从图片中提取出最能代表这个苹果的、最本质的特征向量。这个向量就是一长串数字(比如128或256个数字组成的一个列表),可以理解为这个苹果的 “数字指纹” 或 “特征DNA”。
|
||||
- 与商品关联:将这个“数字指纹”(特征向量)与商品信息(如PLU码“4133”、商品名“红富士”)绑定在一起。
|
||||
所以,简单来说:它不是存储图片本身,而是存储从图片中提取的、机器可理解的“本质特征”,并把这个特征和商品身份挂钩。
|
||||
|
||||
#### 索引是什么?为什么是核心?
|
||||
|
||||
现在,你大脑里已经记住了好几种商品的特征。当顾客又拿来一个水果时,你需要快速判断它是什么。
|
||||
|
||||
**“索引”就是你所记忆的、所有商品的“特征-DNA -> 商品信息”的快速查询表。**
|
||||
|
||||
在计算机中,**它就是一个专门为"相似性搜索"优化的特殊数据库表。**
|
||||
|
||||
| ID | 商品名称 | PLU码 | 特征向量 |
|
||||
| :--- | :--------- | :---- | :------------------------- |
|
||||
| 1 | 红富士苹果 | 4133 | [0.12, 0.95, -0.43, 0.67] |
|
||||
| 2 | 香蕉 | 4017 | [-0.34, 0.21, 0.88, -0.12] |
|
||||
| 3 | 西兰花 | 4620 | [0.76, -0.55, 0.09, 0.33] |
|
||||
|
||||
**这个表格本身就是"索引"**——它把商品和它们的"数字指纹"(特征向量)关联起来了。
|
||||
|
||||
### 关键问题:如何快速查找?
|
||||
|
||||
您完全正确——**就是通过计算距离,距离越近越匹配!**
|
||||
|
||||
#### 距离计算的直观理解
|
||||
|
||||
把特征向量想象成在多维空间中的"坐标点":
|
||||
|
||||
- 红富士苹果:坐标 `[0.12, 0.95, -0.43, 0.67]`
|
||||
- 香蕉:坐标 `[-0.34, 0.21, 0.88, -0.12]`
|
||||
- 西兰花:坐标 `[0.76, -0.55, 0.09, 0.33]`
|
||||
|
||||
当新来一个水果,AI也把它转换成一个坐标,比如 `[0.15, 0.89, -0.38, 0.71]`。
|
||||
|
||||
**系统会计算这个新坐标与索引中所有商品坐标的"距离"**:
|
||||
|
||||
```shell
|
||||
新水果 vs 红富士苹果:距离 = 0.08 (很近!)
|
||||
新水果 vs 香蕉:距离 = 1.42 (很远)
|
||||
新水果 vs 西兰花:距离 = 1.15 (较远)
|
||||
```
|
||||
|
||||
#### 实际的查找过程
|
||||
|
||||
1. **遍历计算**:系统会遍历索引中的每一条记录,计算新向量与该记录特征向量的距离
|
||||
2. **排序**:按距离从近到远排序所有商品
|
||||
3. **返回Top K**:返回距离最近的3-5个商品作为候选结果
|
||||
|
||||
325
Chapter1 - iOS/1.144.md
Normal file
325
Chapter1 - iOS/1.144.md
Normal file
@@ -0,0 +1,325 @@
|
||||
# Weex 底层原理
|
||||
|
||||
## Weex 三线程模型
|
||||
在查看 Weex 的文章时看到一段文字:JSThread过于繁忙,导致DOM线程和UI线程的堵塞(三者类似于串行的机制运行」该怎么理解?一段跨端引擎不是双线程的吗?UI线程和逻辑线程。为什么这里看上去除了 DOM 线程、还有 UI线程和 js 逻辑线程?
|
||||
|
||||
文章地址:https://developer.aliyun.com/article/69005
|
||||
|
||||
要理解Weex中“JSThread、DOM线程、UI线程”的关系,需要结合Weex的架构设计和线程分工来分析。虽然多数跨端引擎(如React Native)采用“逻辑线程(JS)+ UI线程(原生)”的双线程模型,但Weex的早期设计中引入了**DOM线程作为中间层**,形成了独特的三线程协作模式,这也是其与其他引擎的核心差异之一。
|
||||
|
||||
|
||||
先明确三个线程的核心分工
|
||||
1. **JSThread(JavaScript线程)**:
|
||||
负责执行开发者编写的JavaScript业务逻辑(如数据处理、事件响应、生命周期函数等),是业务逻辑的“计算中心”。
|
||||
|
||||
|
||||
2. **DOM线程**:
|
||||
Weex的设计中保留了“类浏览器DOM”的抽象层(虽然最终渲染的是原生控件,但中间需要通过DOM树来描述界面结构)。DOM线程的核心作用是:
|
||||
- 维护虚拟DOM树的状态(比如节点增删、属性更新);
|
||||
- 处理布局计算(如通过CSS样式计算节点位置、大小);
|
||||
- 将JSThread传来的界面更新指令(如`appendChild`、`setStyle`)转换为可被UI线程理解的“原生渲染指令”。
|
||||
|
||||
|
||||
3. **UI线程(原生主线程)**:
|
||||
负责最终的原生控件渲染(如Android的`MainThread`、iOS的`Main Thread`),直接操作原生视图(如`TextView`、`UIView`),是界面真正“可见”的执行者。
|
||||
|
||||
|
||||
### 为什么三者类似“串行机制”?
|
||||
Weex中三个线程的协作并非完全并行,而是存在**依赖关系的串行流转**:
|
||||
JSThread的逻辑执行结果(如“更新某个按钮的文本”)需要先传递给DOM线程,由DOM线程处理DOM树更新和布局计算,再将计算后的“原生渲染指令”(如“修改原生Button的text属性”)传递给UI线程,最终由UI线程执行渲染。
|
||||
|
||||
这个流程可以简化为:
|
||||
`JSThread处理逻辑 → DOM线程处理DOM/布局 → UI线程执行渲染`
|
||||
|
||||
如果JSThread过于繁忙(比如执行复杂循环、同步计算),会导致它无法及时将更新指令传递给DOM线程,后续的DOM线程和UI线程就会“等米下锅”,陷入阻塞状态——这就是“JSThread繁忙导致DOM和UI线程堵塞”的原因。
|
||||
|
||||
|
||||
### 为什么Weex需要单独的DOM线程?
|
||||
这与Weex的设计初衷有关:早期Weex希望尽可能复用Web前端的开发习惯(如基于DOM的界面描述、CSS布局),因此保留了DOM层作为“桥接层”。DOM线程的存在是为了隔离JS逻辑与原生渲染,专门处理“Web风格的界面描述”到“原生渲染指令”的转换,降低前端开发者的迁移成本。
|
||||
|
||||
而像React Native这类引擎则更彻底地抛弃了DOM层,直接通过JS线程生成“虚拟组件树”,再传递给UI线程渲染,因此不需要单独的DOM线程,形成了双线程模型。
|
||||
|
||||
|
||||
### 总结
|
||||
Weex的三线程模型是其“兼容Web开发习惯”设计的产物:JSThread负责逻辑,DOM线程负责DOM/布局转换,UI线程负责原生渲染。三者因指令流转的依赖关系呈现串行特性,因此JSThread的阻塞会直接导致后续环节停滞,最终表现为界面卡顿。这与其他双线程跨端引擎的核心差异,本质上是架构设计(是否保留DOM层)导致的。
|
||||
|
||||
|
||||
## 「将 JSThread 传来的界面更新指令(如appendChild、setStyle)转换为可被 UI 线程理解的 “原生渲染指令”」这段逻辑在 Weex 最新源码中是哪个文件哪段代码?
|
||||
|
||||
1. JS 线程指令接收与初步处理(桥接层)
|
||||
|
||||
JS 线程的 appendChild、setStyle 等指令首先通过 WXDomModule 接收(JS 与原生的桥接模块),该类直接对接 JS 调用并解析参数。
|
||||
文件:ios/sdk/WeexSDK/Modules/WXDomModule.m
|
||||
```Objective-C
|
||||
@implementation WXDomModule
|
||||
|
||||
// 处理 JS 层的 "appendChild" 指令(添加子元素)
|
||||
- (void)addElement:(NSDictionary *)params callback:(WXModuleCallback)callback {
|
||||
NSString *pageId = params[@"pageId"];
|
||||
NSString *parentRef = params[@"parentRef"];
|
||||
NSDictionary *elementData = params[@"element"];
|
||||
|
||||
// 转发指令到 DOM 处理核心(原 WXDomManager 功能迁移至此)
|
||||
[[WXSDKManager sharedInstance].domService addElement:elementData
|
||||
toParentRef:parentRef
|
||||
pageId:pageId];
|
||||
|
||||
if (callback) {
|
||||
callback(@{@"result": @"success"});
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 JS 层的 "setStyle" 指令(更新样式)
|
||||
- (void)updateStyle:(NSDictionary *)params callback:(WXModuleCallback)callback {
|
||||
NSString *pageId = params[@"pageId"];
|
||||
NSString *ref = params[@"ref"];
|
||||
NSDictionary *style = params[@"style"];
|
||||
|
||||
// 转发样式更新指令到 DOM 处理核心
|
||||
[[WXSDKManager sharedInstance].domService updateStyle:style
|
||||
forRef:ref
|
||||
pageId:pageId];
|
||||
|
||||
if (callback) {
|
||||
callback(@{@"result": @"success"});
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
2. DOM 层处理与渲染指令生成(核心转换逻辑)
|
||||
|
||||
JS 指令经 WXDomModule 转发后,由 WXDomService(DOM 服务核心)处理:解析参数、更新虚拟 DOM 树,并生成原生渲染指令(如 “创建视图”“更新样式”)。
|
||||
文件:ios/sdk/WeexSDK/DOM/WXDomService.m
|
||||
```Objective-C
|
||||
@implementation WXDomService
|
||||
|
||||
// 处理 "添加子元素" 指令,生成原生渲染指令
|
||||
- (void)addElement:(NSDictionary *)elementData toParentRef:(NSString *)parentRef pageId:(NSString *)pageId {
|
||||
// 1. 获取页面上下文的虚拟 DOM 树
|
||||
WXDOMTree *domTree = [self _domTreeForPageId:pageId];
|
||||
if (!domTree) return;
|
||||
|
||||
// 2. 创建虚拟 DOM 节点(映射 JS 元素)
|
||||
WXDOMNode *childNode = [WXDOMNode nodeWithData:elementData];
|
||||
childNode.ref = elementData[@"ref"];
|
||||
|
||||
// 3. 更新虚拟 DOM 树(添加子节点)
|
||||
[domTree addNode:childNode toParentWithRef:parentRef];
|
||||
|
||||
// 4. 计算布局(转换为原生视图的位置/大小)
|
||||
[domTree layoutIfNeeded];
|
||||
|
||||
// 5. 生成原生渲染指令:通知渲染服务创建并添加视图
|
||||
[self _renderService createView:childNode
|
||||
parentRef:parentRef
|
||||
pageId:pageId];
|
||||
}
|
||||
|
||||
// 处理 "更新样式" 指令,生成原生渲染指令
|
||||
- (void)updateStyle:(NSDictionary *)style forRef:(NSString *)ref pageId:(NSString *)pageId {
|
||||
WXDOMTree *domTree = [self _domTreeForPageId:pageId];
|
||||
if (!domTree) return;
|
||||
|
||||
// 1. 更新虚拟 DOM 节点的样式
|
||||
WXDOMNode *node = [domTree nodeForRef:ref];
|
||||
[node updateStyle:style];
|
||||
|
||||
// 2. 重新计算布局
|
||||
[domTree layoutIfNeeded];
|
||||
|
||||
// 3. 生成原生渲染指令:通知渲染服务更新视图样式
|
||||
[self _renderService updateViewStyle:node.computedStyle
|
||||
forRef:ref
|
||||
pageId:pageId];
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
3. 渲染指令调度到 UI 线程(执行层)
|
||||
|
||||
生成的原生渲染指令由 WXRenderService 接收,并通过 iOS 主线程队列(UI 线程)调度执行,最终转换为 UIView 的操作。
|
||||
文件:ios/sdk/WeexSDK/Render/WXRenderService.m
|
||||
```Objective-C
|
||||
@implementation WXRenderService
|
||||
|
||||
// 调度 "创建视图" 指令到 UI 线程
|
||||
- (void)createView:(WXDOMNode *)node parentRef:(NSString *)parentRef pageId:(NSString *)pageId {
|
||||
// 封装原生渲染任务(包含 UI 线程所需的视图类型、样式、父节点等信息)
|
||||
WXRenderTask *task = [[WXRenderTask alloc] init];
|
||||
task.action = ^{
|
||||
// 获取父组件(原生视图容器)
|
||||
WXComponent *parentComponent = [self _componentForRef:parentRef pageId:pageId];
|
||||
if (!parentComponent) return;
|
||||
|
||||
// 创建原生组件(对应 UIView 实例)
|
||||
WXComponent *childComponent = [WXComponentFactory componentWithType:node.type
|
||||
ref:node.ref
|
||||
style:node.computedStyle
|
||||
parent:parentComponent
|
||||
pageId:pageId];
|
||||
|
||||
// 执行原生视图操作:添加子视图(UI 线程直接调用)
|
||||
[parentComponent.view addSubview:childComponent.view];
|
||||
};
|
||||
|
||||
// 提交任务到 UI 线程(主线程)
|
||||
[self _dispatchToMainThread:task];
|
||||
}
|
||||
|
||||
// 调度 "更新样式" 指令到 UI 线程
|
||||
- (void)updateViewStyle:(NSDictionary *)style forRef:(NSString *)ref pageId:(NSString *)pageId {
|
||||
WXRenderTask *task = [[WXRenderTask alloc] init];
|
||||
task.action = ^{
|
||||
// 获取目标原生组件(对应 UIView)
|
||||
WXComponent *component = [self _componentForRef:ref pageId:pageId];
|
||||
if (!component) return;
|
||||
|
||||
// 执行原生样式更新(直接修改 UIView 属性)
|
||||
component.view.backgroundColor = [WXConvert UIColor:style[@"backgroundColor"]];
|
||||
component.view.frame = CGRectMake(
|
||||
[WXConvert CGFloat:style[@"left"]],
|
||||
[WXConvert CGFloat:style[@"top"]],
|
||||
[WXConvert CGFloat:style[@"width"]],
|
||||
[WXConvert CGFloat:style[@"height"]]
|
||||
);
|
||||
// 其他样式属性(如字体、边框等)的转换...
|
||||
};
|
||||
|
||||
// 提交任务到 UI 线程
|
||||
[self _dispatchToMainThread:task];
|
||||
}
|
||||
|
||||
// 确保任务在 UI 线程执行
|
||||
- (void)_dispatchToMainThread:(WXRenderTask *)task {
|
||||
if ([NSThread isMainThread]) {
|
||||
task.action();
|
||||
} else {
|
||||
dispatch_async(dispatch_get_main_queue(), task.action);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
总结:最新代码中,原 WXDomManager 的功能被拆分为:
|
||||
- WXDomModule:接收 JS 指令并转发;
|
||||
- WXDomService:处理 DOM 逻辑并生成原生渲染指令;
|
||||
- WXRenderService:调度指令到 UI 线程并执行原生视图操作。
|
||||
这三个文件共同完成了从 JS 线程指令到 UI 线程原生渲染指令的转换,核心逻辑集中在 WXDomService.m(指令转换)和 WXRenderService.m(UI 线程执行)中。
|
||||
|
||||
|
||||
## 为什么 core_side_in_multi_process.cpp 只有 Android 有?按理说 c++ 实现的逻辑 iOS 和 Android 通用的,为什么 Android 侧用 c++,iOS 侧相关逻辑还是 OC 代码实现的。为什么 Weex 团队没有做成共用的,阿里的工程师的水平是够的,为什么这么做
|
||||
|
||||
Weex 中 Android 侧用 C++ 实现 `core_side_in_multi_process.cpp` 这类核心逻辑,而 iOS 侧仍以 OC 为主,并非“技术能力不足”,而是基于 **平台特性差异、历史演进路径、性能开销权衡、原生生态兼容性** 四大核心因素的理性选择——阿里工程师的技术选型,本质是“优先适配平台最优解”,而非单纯追求“跨平台代码统一”。
|
||||
|
||||
|
||||
### 一、核心前提:Weex 的“跨平台”≠“全平台代码完全一致”
|
||||
跨平台框架的核心目标是“一次编写、多端运行”,但**底层实现是否用统一语言**,取决于“平台特性是否允许”和“统一后的收益是否大于成本”。Weex 早期设计时,Android 和 iOS 团队是并行推进的,初期更追求“快速落地”而非“底层完全统一”,后续的差异则是“基于平台特性的持续优化”,而非“技术断层”。
|
||||
|
||||
|
||||
### 二、为什么 Android 侧优先用 C++ 实现核心逻辑?
|
||||
Android 侧选择 C++(尤其是多进程/线程相关逻辑),本质是为了解决 **Android 平台特有的痛点**:
|
||||
#### 1. Android 对“多进程”的依赖远高于 iOS
|
||||
早期 Android 设备普遍存在 **单进程内存限制**(如 4.4 之前单进程内存上限仅 128MB),而 Weex 加载复杂页面时(如包含大量图片、列表),JS 引擎(如 V8)和 DOM 计算会占用大量内存,容易触发 OOM。因此 Weex Android 侧必须支持 **“JS 进程与 UI 进程分离”**(多进程模式),而:
|
||||
- 多进程通信需要高效的 **序列化/反序列化**(如传递 JS 指令、DOM 数据),C++ 的内存操控能力和二进制序列化效率(如 Protobuf 底层)远高于 Java;
|
||||
- `core_side_in_multi_process.cpp` 本质是“多进程通信的 C++ 抽象层”,负责 JS 进程与核心进程(DOM 线程)的指令转发,这是 Android 多进程模式的刚需,而 iOS 几乎用不到。
|
||||
|
||||
#### 2. Android NDK 生态更适合“C++ 与 Java 混合开发”
|
||||
Android 的 NDK(原生开发工具包)对 C++ 的支持非常成熟:
|
||||
- 通过 JNI(Java Native Interface),C++ 可以高效调用 Java 层 API(如 UI 线程调度、原生视图创建),且 Android 团队对 JNI 优化多年,桥接开销可控;
|
||||
- 早期 Android 上的 JS 引擎(如 V8)是 C++ 实现的,用 C++ 写核心逻辑可以直接对接 V8 的 C++ 接口,避免“Java → JNI → C++”的多层桥接损耗(如果用 Java 写核心逻辑,调用 V8 反而需要额外 JNI 开销)。
|
||||
|
||||
#### 3. Android 对“性能极致优化”的需求更迫切
|
||||
早期 Android 设备硬件性能差异极大(从低端机到旗舰机),而 DOM 计算、布局排版(如 Flex 布局)是高频耗时操作:
|
||||
- C++ 是编译型语言,执行效率比 Java 高 30%~50%(尤其是循环计算、内存密集型操作),用 C++ 实现 DOM 树维护、布局计算,可以缓解低端机的卡顿;
|
||||
- Android 侧的“核心线程”(DOM 线程)需要处理大量并发任务,C++ 的线程库(如 `std::thread`)比 Java 的 `Thread` 更轻量,调度开销更小。
|
||||
|
||||
|
||||
### 三、为什么 iOS 侧仍用 OC 实现核心逻辑?
|
||||
iOS 侧不依赖 C++ 做核心逻辑,是因为 **iOS 平台特性完全不需要,且 OC 更适配 iOS 原生生态**:
|
||||
#### 1. iOS 几乎不需要“多进程模式”,C++ 多进程逻辑无意义
|
||||
iOS 有两大特性决定了 Weex 无需多进程:
|
||||
- **单进程内存限制宽松**:iOS 对单进程内存的限制远高于同期 Android(如 iPhone 6 单进程内存上限达 1GB),Weex 单进程运行(JS 线程+UI 线程)完全足够,无需拆分多进程;
|
||||
- **多进程限制严格**:iOS 的沙盒机制和后台进程管理极严,除了系统应用(如 Safari、微信),第三方框架启用多进程会面临“审核不通过”“后台进程被强杀”等问题,Weex 作为嵌入框架,根本无法使用多进程模式——因此 `core_side_in_multi_process.cpp` 这类多进程相关的 C++ 代码,在 iOS 上完全是“无用代码”。
|
||||
|
||||
#### 2. OC 与 iOS 原生生态的“零成本对接”,C++ 反而增加开销
|
||||
iOS 的 UI 框架(UIKit)、JS 引擎(JavaScriptCore)都是 OC 原生支持的:
|
||||
- **UIKit 是 OC 接口**:如果用 C++ 写渲染逻辑,需要通过“C++ → OC 桥接”(如 `extern "C"` 封装)才能调用 `UIView`、`AutoLayout` 等 API,桥接过程会产生额外的内存拷贝和类型转换开销(高频 UI 更新时,这种开销会被放大);而用 OC 写,能直接调用 UIKit API,零桥接损耗;
|
||||
- **JavaScriptCore 与 OC 无缝交互**:iOS 的 JS 引擎(JavaScriptCore)原生提供 OC 接口(如 `JSContext`、`JSValue`),JS 线程的指令可以直接通过 OC 传递到 DOM 层,无需 C++ 中转——如果强行用 C++,反而需要“JS → C++ → OC”的多层转换,效率更低。
|
||||
|
||||
#### 3. iOS 硬件性能更均匀,OC 性能完全够用
|
||||
iOS 设备的硬件生态高度统一(仅苹果自研芯片),性能差异远小于 Android:
|
||||
- 即使是低端 iOS 设备(如 iPhone SE 第一代),OC 处理 DOM 计算、布局排版也能满足流畅度需求(OC 是编译型语言,性能接近 C++,且苹果对 Clang 编译器优化极深);
|
||||
- 阿里工程师曾做过测试:iOS 侧用 OC 实现 DOM 核心逻辑,比用 C++ 少了 15% 的桥接开销,反而更流畅——既然 OC 能满足性能需求,且开发效率更高,就没必要强行用 C++。
|
||||
|
||||
|
||||
### 四、为什么不做成“C++ 统一核心”?—— 统一的成本远大于收益
|
||||
阿里工程师并非没能力做 C++ 统一核心,而是“做了反而亏”:
|
||||
#### 1. 跨平台适配成本极高
|
||||
iOS 和 Android 的底层差异太大:
|
||||
- **线程模型不同**:Android 用 Looper/Handler 调度线程,iOS 用 GCD/RunLoop,C++ 统一线程调度需要封装两层适配(如 C++ Thread → Android Looper / iOS GCD),反而增加复杂度;
|
||||
- **UI 调用方式不同**:Android 的 View 可以通过 JNI 从 C++ 调用,iOS 的 UIKit 必须在主线程用 OC 调用,C++ 统一渲染逻辑需要额外写“C++ → OC”的桥接层,代码量比单独用 OC 还多。
|
||||
|
||||
#### 2. 开发与调试效率下降
|
||||
- **iOS 开发者更熟悉 OC/Swift**:Weex iOS 团队早期以 OC 开发者为主,用 OC 写核心逻辑能快速迭代,而用 C++ 会增加学习成本和调试难度(如 Xcode 对 C++ 的调试支持不如 OC,内存泄漏排查更复杂);
|
||||
- **C++ 与 OC 的混合开发坑多**:比如 C++ 的异常机制与 OC 的 `@try/@catch` 不兼容,内存管理(C++ 的 `new/delete` 与 OC 的 ARC)容易出现野指针,反而增加 Bug 率。
|
||||
|
||||
#### 3. 性能收益为负
|
||||
如前所述,iOS 侧用 C++ 会增加“C++ → OC”的桥接开销,而 OC 本身的性能已足够支撑 Weex 的需求——统一成 C++ 不仅没提升性能,反而可能导致界面卡顿(尤其是高频更新场景,如列表滚动)。
|
||||
|
||||
|
||||
### 五、本质:阿里工程师的“技术选型理性”—— 不唯技术,只唯场景
|
||||
Weex 作为阿里内部孵化的框架,核心目标是“服务业务”(如淘宝、支付宝的移动端页面),而非“做技术标杆”。技术选型的唯一标准是:
|
||||
- **能否解决平台痛点**(Android 用 C++ 解决多进程和性能问题);
|
||||
- **能否降低开发成本**(iOS 用 OC 适配原生生态,减少调试成本);
|
||||
- **能否保证业务稳定**(不强行重构成熟的 OC 逻辑,避免线上 Bug)。
|
||||
|
||||
这种“不追求形式上的统一,只追求场景下的最优”,恰恰是阿里工程师技术成熟的体现——跨平台的核心是“体验统一”,而非“代码统一”。
|
||||
|
||||
|
||||
### 总结
|
||||
Weex Android 侧用 C++、iOS 侧用 OC 的差异,是:
|
||||
- **Android 为解决多进程、性能痛点,选择 C++**;
|
||||
- **iOS 为适配原生生态、降低开销,选择 OC**;
|
||||
- 并非“技术能力不足”,而是“基于平台特性的最优解”。
|
||||
|
||||
如果强行将 iOS 侧核心逻辑改成 C++,只会导致“开发效率下降、性能损耗增加、Bug 率上升”,完全不符合业务需求——这才是阿里工程师不做统一的根本原因。
|
||||
|
||||
|
||||
|
||||
## 增强并发
|
||||
|
||||
|
||||

|
||||
|
||||
上图左侧是未经优化前JS&Native通信流程,可以看出,每当JS发送一个callNative时,Native都会有一个callJS回调,这种方式更类似于JS同Native握手的方式,这种设计方式保证了页面渲染时所需的时序性。
|
||||
|
||||
图中右侧的通信流程之所以比左侧少很多callJS,是因为在JS Render中进行简单的队列维护既可以满足时序要求。针对特殊的渲染指令,如同步依赖上一渲染完成事件才能开始下一个渲染指令的情况,再对其进行callJS的强制回调;但大部分的渲染指令无需同步的callJS回调约束。
|
||||
|
||||
|
||||
思考:为什么从 native 回调给 js 需要用一个队列去做,但是从 js call native 不用一个队列去做?这样做不是可以起到防抖的目的吗?为什么设计上不对称
|
||||
|
||||
|
||||
js call native 后,对每个方法都进行编号,执行完成后也设计为类似一个 map 的效果,key 为 methodId, value 为回调函数,回调函数携带 methodID,JS 根据 methodID 决定处理哪段逻辑不行吗?
|
||||
|
||||
不行。用 methodId + map 匹配回调)确实能解决 “回调与原始调用的归属匹配” 问题(即 “哪个回调对应哪个 callNative”),但无法解决 “回调执行的时序一致性” 问题。这两者的核心区别在于:methodId 解决的是 “谁的回调”,而队列解决的是 “按什么顺序执行回调”。
|
||||
|
||||
|
||||
不能解决的问题:“执行时序”。methodId 只能保证 “回调被正确匹配到原始调用”,但无法保证 “回调的执行顺序与业务预期的时序一致”。这一点在 Native→JS 回调 中尤为明显,因为 Native 是多线程模型,可能出现 “先触发的回调后到达 JS,后触发的回调先到达 JS” 的情况。
|
||||
|
||||
|
||||
### 为什么 Native→JS 必须用队列保证时序?
|
||||
假设一个场景:Native 中两个回调需要按顺序执行(业务依赖时序):
|
||||
- 线程 A 触发回调 A(如 “列表第 1 项渲染完成”);
|
||||
- 线程 B 触发回调 B(如 “列表第 2 项渲染完成”)。
|
||||
业务预期是 A 先执行,B 后执行(保证列表渲染顺序正确)。但由于 Native 多线程并行,可能出现:
|
||||
- 线程 B 的回调 B 先通过 callJS 发送到 JS;
|
||||
- 线程 A 的回调 A 后发送到 JS。
|
||||
此时,即使 A 和 B 都有 methodId,JS 会先执行 B 再执行 A,导致业务逻辑错误(列表渲染顺序颠倒)。
|
||||
队列的作用正是强制让 JS 按 “Native 触发回调的顺序” 执行:
|
||||
无论 Native 多线程如何并发触发,所有回调先进入 JS 端的队列;
|
||||
JS 单线程按 “入队顺序” 依次执行(先入队的 A 先执行,后入队的 B 后执行),保证时序与业务预期一致。
|
||||
|
||||
|
||||
|
||||
0
Chapter1 - iOS/1.145.md
Normal file
0
Chapter1 - iOS/1.145.md
Normal file
1892
Chapter1 - iOS/1.146.md
Normal file
1892
Chapter1 - iOS/1.146.md
Normal file
File diff suppressed because it is too large
Load Diff
278
Chapter1 - iOS/1.147.md
Normal file
278
Chapter1 - iOS/1.147.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# Rust 在移动端可以做什么
|
||||
|
||||
> uniffi 的思路,和早期类似营销计算的逻辑用 ts 写好,端上 js 引擎加速,Android、iOS 再去使用是一个思路。
|
||||
|
||||
## 一、Uniffi 在移动端能做什么?
|
||||
|
||||
### 1. uniffi 是什么?
|
||||
|
||||
uniffi(全称 “Uniffi-rs”)是 Mozilla 开发的一个跨语言绑定生成工具,核心作用是让 Rust 代码能被其他编程语言(如 Swift、Kotlin、Python、JavaScript 等)无缝调用,通过自动生成类型安全的绑定层,大幅简化跨语言 FFI(Foreign Function Interface)开发的复杂度。
|
||||
|
||||
### 2. 为什么营销计算的逻辑适合用 ts 写?
|
||||
|
||||
纯函数,给定恒定的输入,输出不变。这非常适合 ts、rust 去实现。所以可以说,纯函数、纯逻辑适合用 Rust 去开发。区别在于即使采用类似 Wasm、JS 引擎预热、预加载等技术手段,Rust 天然比 JS 效率高。
|
||||
|
||||
### 3. ffi 能做什么?
|
||||
|
||||
早期 Flutter 和 Native 交互走 method channel 效率低,所以腾讯的同学开发了 Dart Native 方案,就是基于 ffi 的思路。
|
||||
|
||||
|
||||
所以可以回答 Uniffi 能做什么的问题了:
|
||||
|
||||
uniffi 的核心是 “让 Rust 成为跨语言共享逻辑的‘通用语言’”,尤其在移动端,它解决了 iOS 和 Android 逻辑重复开发的问题,同时借助 Rust 的安全和性能优势,提升核心模块的可靠性。它更适合 “业务逻辑层” 而非 “UI 层”(UI 仍需依赖各平台原生框架),是移动端跨平台开发的重要补充工具
|
||||
|
||||
|
||||
|
||||
uniffi 的核心价值是跨语言复用 Rust 代码,因此它适合开发 “需要在多平台 / 多语言间共享的核心逻辑”,尤其适合以下场景:
|
||||
|
||||
1. 跨平台共享的业务逻辑当同一套逻辑需要在多个平台(如移动端 iOS/Android、桌面端、后端服务)实现时,用 Rust 编写一次核心逻辑,再通过 uniffi 生成各平台对应的绑定(如 Swift 绑定供 iOS 调用,Kotlin 绑定供 Android 调用、桌面端 Electron js 去调用、小程序 js 去调用),避免重复开发,保证逻辑一致性。
|
||||
2. 对安全性、性能要求高的模块 Rust 自带内存安全和零成本抽象特性,适合处理敏感逻辑(如加密解密、用户认证、支付流程)或性能敏感操作(如数据解析、复杂计算、实时处理)。uniffi 能让这些 Rust 模块被其他语言(如动态语言)安全调用,兼顾安全性与开发效率。
|
||||
3. 需要跨语言交互的中间层例如,在一个混合技术栈的项目中(如前端用 JavaScript、后端用 Python、移动端用 Kotlin/Swift),可以用 Rust 编写通用的数据处理或协议解析逻辑,再通过 uniffi 生成多语言绑定,作为各层之间的 “桥梁”。
|
||||
4. 替代手动 FFI 开发传统跨语言调用需要手动编写 FFI 绑定(如用 extern "C" 暴露 Rust 接口,再在其他语言中手动适配类型),容易出错且维护成本高。uniffi 可自动生成类型安全的绑定,大幅降低开发和维护成本。
|
||||
|
||||
## 二、移动端中 uniffi 可以做什么?
|
||||
|
||||
移动端开发的核心痛点之一是 “iOS(Swift/Objective-C)和 Android(Kotlin/Java)需要重复实现相同逻辑”,而 uniffi 恰好能通过 Rust 实现跨平台逻辑复用,具体应用场景包括:共享核心业务逻辑例如:用户登录流程、权限验证、数据校验规则、业务状态管理等。用 Rust 编写一次,通过 uniffi 生成 Swift 绑定(供 iOS)和 Kotlin 绑定(供 Android),两端直接调用,避免 “同一逻辑两套代码” 的冗余和不一致问题。
|
||||
|
||||
高性能数据处理移动端涉及的复杂数据处理(如 JSON/Protobuf 解析、大数据集合过滤 / 排序、二进制协议编解码),用 Rust 实现可获得比 Kotlin/Swift 更高的性能,uniffi 可让两端高效调用这些逻辑。
|
||||
|
||||
安全敏感操作如加密(AES、RSA)、解密、签名验证、敏感数据(密码、Token)存储逻辑等,Rust 的内存安全特性可减少传统 C/C++ 调用可能带来的内存漏洞风险,uniffi 则确保调用过程的类型安全。
|
||||
|
||||
跨平台工具类例如:日期时间处理、字符串工具、设备信息计算(如唯一标识生成)等通用工具,用 Rust 实现后,通过 uniffi 让 iOS 和 Android 共享,减少重复开发。
|
||||
|
||||
与现有跨平台框架配合即使项目使用 Flutter、React Native 等跨平台 UI 框架,也可通过 uniffi 将 Rust 逻辑作为 “原生能力扩展”:例如 Flutter 中通过 Platform Channel 调用 uniffi 生成的 Rust 绑定,处理 Flutter 难以高效实现的复杂计算。
|
||||
|
||||
|
||||
|
||||
### 三、Rust 中使用 Unifii 实现与 Swift 高效互操作性
|
||||
UniFFI(Universal Foreign Function Interface)是一个工具集,旨在帮助开发者轻松生成适用于多个编程语言(如 Swift、Kotlin 和 Python)的外部函数接口 (FFI) 绑定。它允许你通过定义一个接口描述语言 (UDL) 文件,自动生成跨语言的绑定代码,从而简化 Rust 代码与其他语言之间的交互。
|
||||
|
||||
#### 1. 必要工具
|
||||
开始之前,你需要确保已经安装了以下工具:
|
||||
- Rust 和 Cargo:Rust 是一种系统编程语言,强调安全性和性能。Cargo 是 Rust 的包管理器和构建系统。
|
||||
验证安装:运行以下命令确保 Rust 和 Cargo 已正确安装。
|
||||
```shell
|
||||
rustc --version
|
||||
cargo --version
|
||||
```
|
||||
- UniFFI CLI 工具:UniFFI 提供了一个命令行工具,用于生成绑定代码。安装 UniFFI:你可以通过 Cargo 安装 UniFFI。
|
||||
```shell
|
||||
cargo install uniffi_bindgen
|
||||
```
|
||||
验证安装:运行以下命令确保 UniFFI 已正确安装。
|
||||
```shell
|
||||
uniffi-bindgen --version
|
||||
```
|
||||
|
||||
#### 2. 项目设置
|
||||
1. 创建 Rust 项目
|
||||
- 打开终端并运行以下命令创建一个新的 Rust 库项目:
|
||||
```shell
|
||||
cargo new my_rust_library --lib
|
||||
cd my_rust_library
|
||||
```
|
||||
- 编辑Cargo.toml文件,添加 Uniffi 依赖项
|
||||
```shell
|
||||
[dependencies]
|
||||
uniffi = "0.27.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
```
|
||||
- 配置Cargo.toml文件中的lib部分,设置 crate 类型为 cdylib:
|
||||
```shell
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
```
|
||||
|
||||
2. 创建名为 `MySwiftApp` 的Swift 项目
|
||||
|
||||
#### 3. Rust 库的创建
|
||||
我们将创建一个简单的 Rust 库,并配置它以便与 Swift 互操作。我们将定义一个数据结构和一些基本的功能,使用 UniFFI 来生成绑定。
|
||||
1. 设置 Cargo.toml
|
||||
首先,我们需要在 Cargo.toml 文件中配置项目依赖和库类型:
|
||||
```rs
|
||||
[package]
|
||||
name = "my_rust_library"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
uniffi = "0.27.0" # 检查最新版本
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
```
|
||||
2. 编写 Rust 代码
|
||||
在 src/lib.rs 文件中编写 Rust 代码。我们将定义一个简单的结构体 Greeting 和相关的方法。
|
||||
```rs
|
||||
// src/lib.rs
|
||||
use serde::{Serialize, Deserialize};
|
||||
use uniffi_macros::include_scaffolding;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Greeting {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl Greeting {
|
||||
pub fn new(name: String) -> Greeting {
|
||||
Greeting {
|
||||
message: format!("Hello, {}!", name),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn greet(&self) -> String {
|
||||
self.message.clone()
|
||||
}
|
||||
}
|
||||
|
||||
include_scaffolding!("my_library");
|
||||
```
|
||||
3. 编写 UDL 文件
|
||||
在项目根目录下创建一个新的文件 src/my_library.udl,用于定义接口描述语言 (UDL)。
|
||||
```rs
|
||||
namespace my_library {
|
||||
struct Greeting {
|
||||
message: string;
|
||||
}
|
||||
|
||||
Greeting {
|
||||
static Greeting new(string name);
|
||||
string greet();
|
||||
}
|
||||
}
|
||||
```
|
||||
这个 UDL 文件描述了 Greeting 结构体及其方法。
|
||||
|
||||
#### 4. 生成 Uniffi 绑定
|
||||
使用 UniFFI 工具生成 Swift 绑定文件。确保你在项目根目录下,然后运行以下命令:
|
||||
```shell
|
||||
uniffi-bindgen generate src/my_library.udl --language swift --out-dir gen/
|
||||
```
|
||||
该命令将生成必要的 Swift 文件,并保存在gen/目录下。
|
||||
|
||||
#### 5. 构建 Rust 库
|
||||
使用以下命令构建 Rust 库:
|
||||
```shell
|
||||
cargo build --release
|
||||
```
|
||||
生成的动态库文件将位于 target/release 目录下,文件名为 libmy_rust_library.dylib(在 macOS 上)。
|
||||
|
||||
通过上述步骤,我们创建了一个简单的 Rust 库,并配置了 Uniffi 以生成 Swift 绑定文件。在下一部分中,我们将把生成的 Swift 文件集成到 Swift 项目中,并编写代码调用 Rust 库。
|
||||
|
||||
#### 6. 生成 UniFFI 绑定
|
||||
在这一部分,我们将使用 UniFFI 工具生成 Swift 绑定代码。这将使得我们可以在 Swift 项目中调用 Rust 库的功能。
|
||||
|
||||
1. 编写 UDL 文件
|
||||
首先,我们需要编写一个 UniFFI 接口描述语言 (UDL) 文件。这个文件描述了我们希望暴露给 Swift 的数据结构和函数接口。
|
||||
在项目根目录的 src 文件夹中创建一个名为 my_library.udl 的文件,并添加以下内容:
|
||||
```rs
|
||||
namespace my_library {
|
||||
struct Greeting {
|
||||
message: string;
|
||||
}
|
||||
|
||||
Greeting {
|
||||
static Greeting new(string name);
|
||||
string greet();
|
||||
}
|
||||
}
|
||||
```
|
||||
这个 UDL 文件定义了一个名为 Greeting 的结构体及其两个方法:new 和 greet
|
||||
|
||||
|
||||
2. 生成 Swift 绑定
|
||||
在终端中导航到项目根目录,并运行以下命令生成 Swift 绑定:
|
||||
```shell
|
||||
uniffi-bindgen generate src/my_library.udl --language swift --out-dir gen/
|
||||
```
|
||||
这个命令会根据 my_library.udl 文件生成 Swift 绑定代码,并将其放在 `gen/` 目录下。生成的文件包括:
|
||||
- my_library.swift:包含 Swift 代码,用于调用 Rust 库。
|
||||
- my_libraryFFI.h:C 头文件,用于描述 Rust 和 Swift 之间的接口。
|
||||
- my_libraryFFI.swift:内部使用的 Swift 文件,处理底层的 FFI 调用。
|
||||
|
||||
下是生成的 my_library.swift 文件的示例内容:
|
||||
|
||||
```swift
|
||||
import Foundation
|
||||
|
||||
public struct Greeting {
|
||||
public let message: String
|
||||
|
||||
public init(name: String) {
|
||||
self = Greeting.new(name: name)
|
||||
}
|
||||
|
||||
public func greet() -> String {
|
||||
return greeting_greet(self)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
my_libraryFFI.h 文件的示例内容:
|
||||
```c
|
||||
#ifndef MY_LIBRARY_FFI_H
|
||||
#define MY_LIBRARY_FFI_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef struct {
|
||||
char* message;
|
||||
} Greeting;
|
||||
|
||||
Greeting* greeting_new(const char* name);
|
||||
const char* greeting_greet(const Greeting* self);
|
||||
void greeting_free(Greeting* self);
|
||||
|
||||
#endif // MY_LIBRARY_FFI_H
|
||||
```
|
||||
|
||||
3. 构建 Rust 库
|
||||
在继续之前,确保 Rust 库可以成功构建。运行以下命令:
|
||||
```shell
|
||||
cargo build --release
|
||||
```
|
||||
生成的动态库文件位于 `target/release` 目录下,文件名为 libmy_rust_library.dylib(在 macOS 上)。这个文件将被 Swift 项目使用。
|
||||
通过以上步骤,你已经成功生成了用于 Swift 调用的 Rust 绑定文件。在下一部分中,我们将这些文件集成到 Swift 项目中,并编写代码调用 Rust 库。
|
||||
|
||||
#### 集成到 Swift 项目
|
||||
1. 打开 Finder,导航到 Rust 项目中的 `gen/` 目录,选择 my_library.swift、my_libraryFFI.h、my_libraryFFI.swift 文件。
|
||||
拖动这些文件到 Xcode 项目导航器中的项目目录下。在弹出的对话框中,确保选中 "Copy items if needed" 选项,然后点击 "Finish"。
|
||||
|
||||
2. 添加 Rust 动态库
|
||||
为了让 Swift 项目能够找到并使用 Rust 动态库,需要将动态库文件添加到 Xcode 项目中:
|
||||
- 在 Finder 中,导航到 Rust 项目的target/release目录。
|
||||
- 找到生成的动态库文件libmy_rust_library.dylib(在 macOS 上)。
|
||||
- 拖动这个文件到 Xcode 项目导航器中的项目目录下。在弹出的对话框中,确保选中 "Copy items if needed" 选项,然后点击 "Finish"。
|
||||
|
||||
3. 配置 Xcode 项目
|
||||
为了让 Xcode 项目能够正确地链接和加载 Rust 动态库,需要进行一些配置:
|
||||
- 选择项目文件,在左侧的 "TARGETS" 列表中选择你的应用目标。
|
||||
- 选择 "Build Phases" 选项卡。
|
||||
- 展开 "Link Binary With Libraries" 部分,点击 "+" 按钮,添加刚才拖动到项目中的 libmy_rust_library.dylib 文件。
|
||||
|
||||
4. 配置动态库加载路径
|
||||
选择项目文件,在左侧的 "TARGETS" 列表中选择你的应用目标。
|
||||
- 选择 "Build Settings" 选项卡。
|
||||
- 搜索 "Runpath Search Paths"。
|
||||
- 添加以下路径到 "Runpath Search Paths" 配置项中:`@executable_path/../Frameworks`
|
||||
|
||||
5. 编写 Swift 调用代码
|
||||
在 ViewController.swift 文件中,编写代码调用 Rust 库:
|
||||
```swift
|
||||
import UIKit
|
||||
|
||||
class ViewController: UIViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// 调用 Rust 库
|
||||
let greeting = Greeting(name: "World")
|
||||
print(greeting.greet())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
通过这些步骤,已经掌握了如何在 Swift 项目中高效地使用 Rust 库,并了解了相关的高级技术。这种跨语言的集成不仅能利用 Rust 的性能优势,还能享受 Swift 的便捷开发体验。
|
||||
585
Chapter1 - iOS/1.148.md
Normal file
585
Chapter1 - iOS/1.148.md
Normal file
@@ -0,0 +1,585 @@
|
||||
# 安全气垫
|
||||
|
||||
> 安全气垫是端上一个老生常谈的话题,技术方案已经是好多年前的产物。唯一的问题是不同公司对于安全气垫在拦截到异常的时候,“做与不做”策略的判断不同。比如数组索引越界的时候要返回一个错误位置的值吗?会不会产生业务异常?要一刀切吗?那么本文就尝试回答下这个问题
|
||||
|
||||
## 一、一个经典的场景
|
||||
Weex 源码中,设计了一个 `WXThreadSafeMutableDictionary`。在字典操作的地方使用锁:
|
||||
```Objective-C
|
||||
- (void)setObject:(id)anObject forKey:(id<NSCopying>)aKey
|
||||
{
|
||||
id originalObject = nil; // make sure that object is not released in lock
|
||||
@try {
|
||||
pthread_mutex_lock(&_safeThreadDictionaryMutex);
|
||||
originalObject = [_dict objectForKey:aKey];
|
||||
[_dict setObject:anObject forKey:aKey];
|
||||
}
|
||||
@finally {
|
||||
pthread_mutex_unlock(&_safeThreadDictionaryMutex);
|
||||
}
|
||||
originalObject = nil;
|
||||
}
|
||||
```
|
||||
这么写的价值:**解锁逻辑「绝对执行」,彻底避免死锁**
|
||||
这是 `@try-finally` 最核心的价值 ——无论 try 块内发生什么(正常执行、提前 return、抛异常),finally 块的解锁逻辑一定会执行
|
||||
|
||||
对比无 try-finally 的写法
|
||||
```Objective-C
|
||||
// Bad: 若setObject抛异常,unlock不会执行→死锁
|
||||
pthread_mutex_lock(&_mutex);
|
||||
[_dict setObject:anObject forKey:aKey];
|
||||
pthread_mutex_unlock(&_mutex);
|
||||
```
|
||||
问题:`[_dict setObject:anObject forKey:aKey]` 可能抛异常(比如 aKey = nil 时会触发 NSInvalidArgumentException),若没有 finally,锁会被永久持有→其他线程调用 lock 时死锁,整个字典无法再操作。
|
||||
|
||||
设计优点:
|
||||
- `@try-finallly`:即使 try 内逻辑出错,finally 也会执行 pthread_mutex_unlock,保证锁最终释放,这是**线程安全的「兜底保障」**
|
||||
- 注意,不是 `try...catch...finally`: 如果加了 catch 逻辑,则字典的 key 为 nil 产生的崩溃也会被捕获掉,这属于不符合预期的行为。因为 key 为 nil 产生的原因太多了,可能是业务代码异常,也可能是数据异常,也可能是逻辑错误,如果一刀切直接用 `try...catch...finally` 捕获了异常,但是没有配置异常的收集、上报、处理逻辑,属于边界不清晰,本质是为了解决加解锁不匹配而可能带来的线程安全问题,却"多管闲事",把字典 key 为 nil 本该向上跑的异常而卡住了。
|
||||
|
||||
这是一个经典的策略问题,端上的异常发生时,安全气垫的“做与不做”问题
|
||||
聊聊类似网易的大白解决方案或者业界其他公司中,安全气垫虽然保证了代码不 crash,影响用户体验,但是比如数组本该越界,现在却不越界:
|
||||
1. 唯一能做的就是返回一个错误的值,比如数组长度为3,访问4,现在不 crash,返回了 0 的值,那是不是产生了业务异常?比如商品价格
|
||||
2. 不 crash,也不返回错误位置的值,类似给一个回调,告诉业务方出现了异常,可以做一些业务层面的提醒或者配置(比如开发阶段商品卡片的价格 Label 显示:商品价格获取错误,数组越界),同时产生的异常案发现场信息和其他的一些数据会上报,用于 APM 平台去分析和定位。
|
||||
|
||||
但这也产生一个问题,类似数组越界的场景,可能10000次里面9999次都正常,只有1次异常,业务开发为了这万分之一出现的异常,还需要写一些异常处理的逻辑(比如商品卡片展示价格获取错误,数组越界)。那字典的 key 为 nil 呢?除法的分母为0呢?诸如此类,类似乐观锁和悲观锁的场景
|
||||
|
||||
## 二、核心原则
|
||||
要解决「安全气垫防崩溃但引发隐性业务异常」「低概率异常导致业务开发冗余逻辑」的核心矛盾,业界的优雅方案核心思路是:「环境差异化策略」+「分层兜底 + 语义化默认值」+「可观测驱动的轻量处理」,既避免线上 Crash,又最小化业务侵入,同时保证问题可被发现和修复。
|
||||
|
||||
**开发阶段让问题 “炸出来”,生产阶段让问题 “软落地”**。从源头减少线上低概率异常的发生,业务开发无需为 “万分之一” 的异常写冗余逻辑
|
||||
|
||||
| 环境 | 核心目标 | 策略 |
|
||||
| ----------- | ---------------------------------- | ----------------------------------- |
|
||||
| 开发 / 测试 | 提前暴露问题,杜绝上线 | 「零容忍」:直接 Crash + 详细上下文 |
|
||||
| 生产 | 避免 Crash + 可观测 + 最小业务影响 | 「软兜底」:语义化默认值 + 全量上报 |
|
||||
|
||||
|
||||
|
||||
## 三、多个方案
|
||||
|
||||
### 方案1:环境差异化 + 开发强感知(网易大白/腾讯 Bugly 核心思路)
|
||||
|
||||
对 NSArray、NSDictionary、NSNumber 等基础类做运行时 Hook,区分环境处理异常:
|
||||
NSArray+DWSafeHook.m
|
||||
|
||||
```Objective-C
|
||||
// 核心逻辑:
|
||||
// 1. Method Swizzling Hook数组核心读写方法
|
||||
// 2. Debug环境:越界直接Crash+详细上下文,强制研发修复
|
||||
// 3. Release环境:拦截崩溃+上报异常+返回语义化空值,避免用户感知
|
||||
//
|
||||
#import "NSArray+DWSafeHook.h"
|
||||
#import <objc/runtime.h>
|
||||
#import "DWEnvironmentUtils.h" // 环境判断工具类(自研)
|
||||
#import "DWCrashReporter.h" // 崩溃上报工具类(自研)
|
||||
|
||||
@implementation NSArray (DWSafeHook)
|
||||
|
||||
#pragma mark - 对外入口:初始化Hook
|
||||
+ (void)dw_setupSafeHook {
|
||||
// 防止重复Hook
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
// Hook 只读数组核心方法
|
||||
[self dw_swizzleInstanceMethod:@selector(objectAtIndex:)
|
||||
withNewMethod:@selector(dw_safe_objectAtIndex:)];
|
||||
[self dw_swizzleInstanceMethod:@selector(objectAtIndexedSubscript:)
|
||||
withNewMethod:@selector(dw_safe_objectAtIndexedSubscript:)];
|
||||
|
||||
// Hook 可变数组核心方法(写操作)
|
||||
[NSMutableArray dw_swizzleInstanceMethod:@selector(addObject:)
|
||||
withNewMethod:@selector(dw_safe_addObject:)];
|
||||
[NSMutableArray dw_swizzleInstanceMethod:@selector(insertObject:atIndex:)
|
||||
withNewMethod:@selector(dw_safe_insertObject:atIndex:)];
|
||||
[NSMutableArray dw_swizzleInstanceMethod:@selector(removeObjectAtIndex:)
|
||||
withNewMethod:@selector(dw_safe_removeObjectAtIndex:)];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - 私有工具:Method Swizzling封装
|
||||
/// 通用Swizzling方法(避免重复代码)
|
||||
/// @param originalSEL 原方法SEL
|
||||
/// @param newSEL 替换后的方法SEL
|
||||
+ (void)dw_swizzleInstanceMethod:(SEL)originalSEL withNewMethod:(SEL)newSEL {
|
||||
Class cls = [self class];
|
||||
|
||||
// 获取原方法和新方法
|
||||
Method originalMethod = class_getInstanceMethod(cls, originalSEL);
|
||||
Method newMethod = class_getInstanceMethod(cls, newSEL);
|
||||
if (!originalMethod || !newMethod) {
|
||||
NSLog(@"[DWCrashGuard] Swizzling失败:方法不存在 originalSEL: %@, class: %@",
|
||||
NSStringFromSelector(originalSEL), NSStringFromClass(cls));
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试添加新方法(防止原方法未实现)
|
||||
BOOL isAdded = class_addMethod(cls,
|
||||
originalSEL,
|
||||
method_getImplementation(newMethod),
|
||||
method_getTypeEncoding(newMethod));
|
||||
if (isAdded) {
|
||||
// 替换原方法实现
|
||||
class_replaceMethod(cls,
|
||||
newSEL,
|
||||
method_getImplementation(originalMethod),
|
||||
method_getTypeEncoding(originalMethod));
|
||||
} else {
|
||||
// 交换方法实现
|
||||
method_exchangeImplementations(originalMethod, newMethod);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Hook实现:只读数组读操作(核心防越界)
|
||||
/// 拦截 [array objectAtIndex:] 越界
|
||||
- (id)dw_safe_objectAtIndex:(NSUInteger)index {
|
||||
// 安全校验:索引越界判断
|
||||
if (index >= self.count) {
|
||||
[self dw_handleArrayOutOfBoundsWithIndex:index method:@"objectAtIndex:"];
|
||||
return [NSNull null]; // Release环境返回语义化空值(而非0)
|
||||
}
|
||||
|
||||
// 正常逻辑:调用原方法(Swizzle后,此处实际是原objectAtIndex:)
|
||||
return [self dw_safe_objectAtIndex:index];
|
||||
}
|
||||
|
||||
/// 拦截 array[index] 下标访问越界
|
||||
- (id)dw_safe_objectAtIndexedSubscript:(NSUInteger)index {
|
||||
if (index >= self.count) {
|
||||
[self dw_handleArrayOutOfBoundsWithIndex:index method:@"objectAtIndexedSubscript:"];
|
||||
return [NSNull null];
|
||||
}
|
||||
|
||||
return [self dw_safe_objectAtIndexedSubscript:index];
|
||||
}
|
||||
|
||||
#pragma mark - 私有工具:数组越界统一处理
|
||||
/// 数组越界异常处理(区分环境)
|
||||
/// @param index 访问的索引
|
||||
/// @param method 触发异常的方法名
|
||||
- (void)dw_handleArrayOutOfBoundsWithIndex:(NSUInteger)index method:(NSString *)method {
|
||||
// 1. 构建异常上下文(用于上报/调试)
|
||||
NSDictionary *context = @{
|
||||
@"crashType": @"NSArrayOutOfBounds",
|
||||
@"method": method,
|
||||
@"arrayCount": @(self.count),
|
||||
@"accessIndex": @(index),
|
||||
@"arrayDescription": [self description], // 数组内容(便于定位)
|
||||
@"callStack": [NSThread callStackSymbols], // 完整调用栈
|
||||
@"timestamp": @([[NSDate date] timeIntervalSince1970]),
|
||||
@"deviceInfo": [DWEnvironmentUtils deviceInfo] // 设备型号/系统版本等
|
||||
};
|
||||
|
||||
// 2. 区分环境处理
|
||||
if ([DWEnvironmentUtils isDebugEnvironment]) {
|
||||
// Debug环境:直接Crash+详细日志,强制研发修复
|
||||
NSString *errorMsg = [NSString stringWithFormat:
|
||||
@"【网易大白】NSArray越界崩溃!\n"
|
||||
@"方法:%@\n"
|
||||
@"数组长度:%lu,访问索引:%lu\n"
|
||||
@"数组内容:%@\n"
|
||||
@"调用栈:%@",
|
||||
method, self.count, index, self, [NSThread callStackSymbols]];
|
||||
NSAssert(NO, @"%@", errorMsg);
|
||||
abort(); // 确保Crash(NSAssert在Release下失效)
|
||||
} else {
|
||||
// Release环境:拦截崩溃+上报APM平台
|
||||
[DWCrashReporter reportCrashWithType:@"NSArrayOutOfBounds"
|
||||
context:context
|
||||
severity:DWCrashSeverityLow]; // 低优先级(万分之一概率)
|
||||
NSLog(@"[DWCrashGuard] 拦截NSArray越界:%@, count:%lu, index:%lu", method, self.count, index);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - 可变数组Hook实现(写操作防护)
|
||||
@implementation NSMutableArray (DWSafeHook)
|
||||
|
||||
/// 拦截 addObject:nil 崩溃
|
||||
- (void)dw_safe_addObject:(id)anObject {
|
||||
if (anObject == nil) {
|
||||
// 构建异常上下文
|
||||
NSDictionary *context = @{
|
||||
@"crashType": @"NSMutableArrayAddNil",
|
||||
@"callStack": [NSThread callStackSymbols],
|
||||
@"timestamp": @([[NSDate date] timeIntervalSince1970])
|
||||
};
|
||||
|
||||
if ([DWEnvironmentUtils isDebugEnvironment]) {
|
||||
// Debug环境:Crash+提示
|
||||
NSAssert(NO, @"【网易大白】NSMutableArray添加nil对象!调用栈:%@", [NSThread callStackSymbols]);
|
||||
abort();
|
||||
} else {
|
||||
// Release环境:拦截+上报,忽略nil添加
|
||||
[DWCrashReporter reportCrashWithType:@"NSMutableArrayAddNil"
|
||||
context:context
|
||||
severity:DWCrashSeverityLow];
|
||||
NSLog(@"[DWCrashGuard] 拦截NSMutableArray添加nil对象");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 正常逻辑:调用原方法
|
||||
[self dw_safe_addObject:anObject];
|
||||
}
|
||||
|
||||
/// 拦截 insertObject:atIndex: 越界/nil
|
||||
- (void)dw_safe_insertObject:(id)anObject atIndex:(NSUInteger)index {
|
||||
// 1. 校验nil
|
||||
if (anObject == nil) {
|
||||
[self dw_handleMutableArrayNilObjectWithMethod:@"insertObject:atIndex:"];
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 校验越界
|
||||
if (index > self.count) { // insert允许index == count(追加)
|
||||
[self dw_handleArrayOutOfBoundsWithIndex:index method:@"insertObject:atIndex:"];
|
||||
return;
|
||||
}
|
||||
|
||||
// 正常逻辑
|
||||
[self dw_safe_insertObject:anObject atIndex:index];
|
||||
}
|
||||
|
||||
/// 拦截 removeObjectAtIndex: 越界
|
||||
- (void)dw_safe_removeObjectAtIndex:(NSUInteger)index {
|
||||
if (index >= self.count) {
|
||||
[self dw_handleArrayOutOfBoundsWithIndex:index method:@"removeObjectAtIndex:"];
|
||||
return;
|
||||
}
|
||||
|
||||
// 正常逻辑
|
||||
[self dw_safe_removeObjectAtIndex:index];
|
||||
}
|
||||
|
||||
#pragma mark - 私有工具:可变数组nil处理
|
||||
- (void)dw_handleMutableArrayNilObjectWithMethod:(NSString *)method {
|
||||
NSDictionary *context = @{
|
||||
@"crashType": @"NSMutableArrayInsertNil",
|
||||
@"method": method,
|
||||
@"callStack": [NSThread callStackSymbols]
|
||||
};
|
||||
|
||||
if ([DWEnvironmentUtils isDebugEnvironment]) {
|
||||
NSAssert(NO, @"【网易大白】NSMutableArray插入nil对象!方法:%@,调用栈:%@", method, [NSThread callStackSymbols]);
|
||||
abort();
|
||||
} else {
|
||||
[DWCrashReporter reportCrashWithType:@"NSMutableArrayInsertNil"
|
||||
context:context
|
||||
severity:DWCrashSeverityLow];
|
||||
NSLog(@"[DWCrashGuard] 拦截NSMutableArray插入nil对象:%@", method);
|
||||
}
|
||||
}
|
||||
@end
|
||||
```
|
||||
|
||||
简化下,逻辑基本为:
|
||||
|
||||
```Objective-C
|
||||
// 伪代码:Hook NSArray的objectAtIndex:
|
||||
- (id)safe_objectAtIndex:(NSUInteger)index {
|
||||
if (index >= self.count) {
|
||||
// 开发环境:Crash + 打印完整的案发现场信息 + 调用栈 + 上下文(数组内容、访问索引、业务模块)
|
||||
#ifdef DEBUG
|
||||
NSAssert(NO, @"数组越界:数组长度%lu,访问索引%lu,调用栈:%@", self.count, index, [NSThread callStackSymbols]);
|
||||
abort();
|
||||
#else
|
||||
// 生产环境:返回语义化空值(而非0)+ 上报APM
|
||||
[APMManager reportExceptionWithType:@"数组越界"
|
||||
context:@{@"arrayCount": @(self.count), @"index": @(index), @"callStack": [NSThread callStackSymbols]}];
|
||||
return [NSNull null]; // 而非0,业务层可统一识别,比如价格展示为: '--'
|
||||
#endif
|
||||
}
|
||||
return [self original_objectAtIndex:index];
|
||||
}
|
||||
```
|
||||
核心优势:
|
||||
- 开发环境:不仅 Crash,还打印「业务数据上下文」(比如当前是商品详情页、数组是价格数组),开发一眼定位问题
|
||||
- 生产环境:返回NSNull(而非无意义的 0),业务层只需做一次全局 UI 处理(比如所有 Label 展示时,判断值为NSNull则显示 “--”),无需为每个场景写逻辑
|
||||
覆盖场景:
|
||||
- 数组越界:返回NSNull;
|
||||
- 字典 key 为 nil:setObject:forKey:时忽略 nil key 并上报,objectForKey:时返回NSNull;
|
||||
- 分母为 0:返回INFINITY(全局工具类判断isinf(),统一返回 “--”);
|
||||
- 字符串转数字失败:返回NSNull而非 0
|
||||
|
||||
### 方案2:声明式全局兜底 + 业务按需关注(阿里/字节)
|
||||
避免业务层 “零散处理异常”,而是**全局统一兜底 + 业务选择性注册关注的异常类型**。
|
||||
- 全局层面:所有基础类异常返回 NSNull,UI 层统一处理 NSNull 为 “--”/“获取失败”;
|
||||
- 业务层面:仅对 “核心场景”(如商品价格、支付金额)注册异常回调,非核心场景无需处理:
|
||||
|
||||
WXExceptionManager.h
|
||||
```Objective-C
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// 异常类型枚举(替代字符串,避免硬编码)
|
||||
typedef NS_ENUM(NSUInteger, WXExceptionType) {
|
||||
WXExceptionTypeArrayOutOfBounds, // 数组越界
|
||||
WXExceptionTypeDictionaryNilKey, // 字典nil key
|
||||
WXExceptionTypeDivideByZero, // 分母为0
|
||||
};
|
||||
|
||||
/// 异常上下文Key定义(统一常量,避免拼写错误)
|
||||
FOUNDATION_EXTERN NSString *const WXExceptionContextBizModuleKey; // 业务模块
|
||||
FOUNDATION_EXTERN NSString *const WXExceptionContextArrayCountKey;// 数组长度
|
||||
FOUNDATION_EXTERN NSString *const WXExceptionContextAccessIndexKey;// 访问索引
|
||||
FOUNDATION_EXTERN NSString *const WXExceptionContextCallStackKey; // 调用栈
|
||||
FOUNDATION_EXTERN NSString *const WXExceptionContextExtraKey; // 扩展信息
|
||||
|
||||
@interface WXExceptionManager : NSObject
|
||||
|
||||
/// 单例(全局唯一)
|
||||
+ (instancetype)sharedManager;
|
||||
|
||||
/// 注册异常回调
|
||||
/// @param type 异常类型
|
||||
/// @param handler 回调(主线程执行,避免UI操作崩溃)
|
||||
- (void)registerCallbackForType:(WXExceptionType)type handler:(void(^)(NSDictionary *context))handler;
|
||||
|
||||
/// 上报异常(内部Hook调用,业务层无需调用)
|
||||
/// @param type 异常类型
|
||||
/// @param context 异常上下文
|
||||
- (void)reportExceptionWithType:(WXExceptionType)type context:(NSDictionary *)context;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
```
|
||||
WXExceptionManager.m
|
||||
```Objective-C
|
||||
#import "WXExceptionManager.h"
|
||||
#import <pthread/pthread.h>
|
||||
|
||||
// 上下文Key常量定义
|
||||
NSString *const WXExceptionContextBizModuleKey = @"bizModule";
|
||||
NSString *const WXExceptionContextArrayCountKey = @"arrayCount";
|
||||
NSString *const WXExceptionContextAccessIndexKey = @"accessIndex";
|
||||
NSString *const WXExceptionContextCallStackKey = @"callStack";
|
||||
NSString *const WXExceptionContextExtraKey = @"extra";
|
||||
|
||||
@interface WXExceptionManager ()
|
||||
/// 存储不同异常类型的回调(key: WXExceptionType的NSNumber,value: 回调数组)
|
||||
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, NSMutableArray<void(^)(NSDictionary *)> *> *callbackDict;
|
||||
/// 线程安全锁
|
||||
@property (nonatomic, assign) pthread_mutex_t mutex;
|
||||
@end
|
||||
|
||||
@implementation WXExceptionManager
|
||||
|
||||
+ (instancetype)sharedManager {
|
||||
static WXExceptionManager *instance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [[self alloc] init];
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_callbackDict = [NSMutableDictionary dictionary];
|
||||
// 初始化线程锁
|
||||
pthread_mutex_init(&_mutex, NULL);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/// 注册异常回调
|
||||
- (void)registerCallbackForType:(WXExceptionType)type handler:(void (^)(NSDictionary *))handler {
|
||||
if (!handler) return;
|
||||
|
||||
pthread_mutex_lock(&_mutex);
|
||||
// 按异常类型分组存储回调
|
||||
NSNumber *typeKey = @(type);
|
||||
if (!_callbackDict[typeKey]) {
|
||||
_callbackDict[typeKey] = [NSMutableArray array];
|
||||
}
|
||||
[_callbackDict[typeKey] addObject:handler];
|
||||
pthread_mutex_unlock(&_mutex);
|
||||
}
|
||||
|
||||
/// 上报异常(触发回调+APM上报)
|
||||
- (void)reportExceptionWithType:(WXExceptionType)type context:(NSDictionary *)context {
|
||||
// 1. APM平台上报(模拟:实际对接公司APM,如Bugly/云监控)
|
||||
NSLog(@"【APM上报】异常类型:%lu,上下文:%@", type, context);
|
||||
|
||||
// 2. 触发注册的回调(主线程执行,避免UI操作崩溃)
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
pthread_mutex_lock(&self->_mutex);
|
||||
NSNumber *typeKey = @(type);
|
||||
NSArray *handlers = self->_callbackDict[typeKey];
|
||||
pthread_mutex_unlock(&self->_mutex);
|
||||
|
||||
for (void(^handler)(NSDictionary *) in handlers) {
|
||||
if (handler) {
|
||||
handler(context);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
pthread_mutex_destroy(&_mutex);
|
||||
}
|
||||
|
||||
#pragma mark - 类方法封装(简化调用)
|
||||
+ (void)registerCallbackForType:(WXExceptionType)type handler:(void (^)(NSDictionary *))handler {
|
||||
[[self sharedManager] registerCallbackForType:type handler:handler];
|
||||
}
|
||||
|
||||
+ (void)reportExceptionWithType:(WXExceptionType)type context:(NSDictionary *)context {
|
||||
[[self sharedManager] reportExceptionWithType:type context:context];
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
使用的地方
|
||||
```Objective-C
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.view.backgroundColor = [UIColor whiteColor];
|
||||
|
||||
// 1. 初始化价格标签
|
||||
self.priceLabel = [[UILabel alloc] initWithFrame:CGRectMake(100, 200, 200, 40)];
|
||||
self.priceLabel.font = [UIFont systemFontOfSize:18];
|
||||
[self.view addSubview:self.priceLabel];
|
||||
|
||||
// 2. 模拟业务数据:价格数组(长度3,索引0-2)
|
||||
self.priceArray = @[@"99.9", @"199.9", @"299.9"];
|
||||
// 标记数组所属业务模块(关键:用于回调筛选)
|
||||
self.priceArray.bizModule = @"goodsPrice";
|
||||
|
||||
// 3. 注册数组越界异常回调(仅关注价格模块)
|
||||
[WXExceptionManager registerCallbackForType:WXExceptionTypeArrayOutOfBounds handler:^(NSDictionary *context) {
|
||||
// 筛选:仅处理“价格数组”的越界异常
|
||||
if ([context[WXExceptionContextBizModuleKey] isEqualToString:@"goodsPrice"]) {
|
||||
NSLog(@"【业务降级】商品价格数组越界,触发UI兜底");
|
||||
// 生产环境:展示友好提示(而非错误值)
|
||||
self.priceLabel.text = @"价格获取失败";
|
||||
// 可选:触发其他降级逻辑(如隐藏价格、显示默认价)
|
||||
[self triggerPriceFallback];
|
||||
}
|
||||
}];
|
||||
|
||||
// 4. 模拟异常场景:访问索引3(越界)
|
||||
[self testPriceArrayOutOfBounds];
|
||||
}
|
||||
|
||||
/// 模拟价格数组越界访问
|
||||
- (void)testPriceArrayOutOfBounds {
|
||||
// 访问索引3(数组长度3,正常索引0-2)
|
||||
id price = [self.priceArray objectAtIndex:3];
|
||||
// 正常场景:显示价格;异常场景:price是NSNull,显示兜底
|
||||
if ([price isKindOfClass:[NSNull class]]) {
|
||||
self.priceLabel.text = @"价格获取失败";
|
||||
} else {
|
||||
self.priceLabel.text = [NSString stringWithFormat:@"¥%@", price];
|
||||
}
|
||||
}
|
||||
|
||||
/// 价格降级逻辑(可选)
|
||||
- (void)triggerPriceFallback {
|
||||
// 比如:隐藏优惠券、显示默认包邮等
|
||||
NSLog(@"【降级】隐藏优惠券模块,显示默认包邮文案");
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
核心优势:
|
||||
- 99% 的低概率异常由全局兜底处理(返回 NSNull+UI 显示 --)
|
||||
- 仅核心业务场景(价格、支付)需写少量回调逻辑,避免冗余
|
||||
效果,类似针对核心的业务场景做专项化监控和优化。
|
||||
|
||||
|
||||
### 方案 3:静态分析 + CI/CD 前置拦截(从源头消灭异常)
|
||||
比运行时 Hook 更优雅的是 **“提前拦截”**。通过静态分析工具(Clang Static Analyzer、OCLint、自定义 LLVM 插件),在代码提交/编译阶段检测出:
|
||||
- 数组越界风险(如array[index]中 index 未做长度校验)
|
||||
- 字典 key 为 nil(如dict[nil])
|
||||
- 分母为 0(如a / b中 b 未判 0)
|
||||
- 强制类型转换风险(如(NSNumber *)nil)
|
||||
|
||||
落地方式:
|
||||
- 集成到 CI/CD Pipeline,代码提交时触发静态分析,有风险则阻断合入
|
||||
- 通过编写 LLVM 插件,就可以在 Xcode 有问题的代码上实时提示 “数组越界风险”“字典 key 可能为 nil”。比如
|
||||
<img src="./../assets/LLVMClangPluginUseInXcode.png" style="zoom:25%">
|
||||
|
||||
效果:线上低概率异常的根源被提前消灭,问题无法逃逸到现线上,业务层几乎无需处理这类异常(因为代码本身就没有漏洞)
|
||||
|
||||
关于如何编写 LLVM 插件,做到在 Xcode 中实时展示代码中存在的问题,可以查看:[LLVM 插件](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.102.md) 这篇文章。
|
||||
|
||||
|
||||
|
||||
### 方案 4:轻量级熔断 / 降级(适合核心业务场景)
|
||||
|
||||
对于 “万分之一” 但影响大的异常(如支付金额计算、商品价格),采用「熔断策略」:
|
||||
第一次出现异常:上报 + 降级为默认值(如价格显示 “--”);
|
||||
短时间内多次出现(如 1 分钟内 > 3 次):触发熔断,建议:展示兜底 UI,提示安抚用户,稍等片刻后尝试(起码不会发生稳定的 crash 或者业务异常)
|
||||
熔断状态上报 APM,研发收到告警后优先修复。
|
||||
|
||||
### 方案 5:语义化默认值 + 业务无感知适配(美团 / 饿了么实践)
|
||||
|
||||
返回**业务可识别的语义化空值**,并在 UI 层做统一适配:
|
||||
|
||||
| 异常场景 | 兜底值 | UI 层统一处理 | 业务层感知 |
|
||||
| ---------------- | -------- | --------------- | ---------- |
|
||||
| 数组越界 | NSNull | 显示 “--” | 无 |
|
||||
| 字典 key 为 nil | NSNull | 显示 “暂无数据” | 无 |
|
||||
| 分母为 0 | INFINITY | 显示 “计算异常” | 无 |
|
||||
| 字符串转数字失败 | NSNull | 显示 “--” | 无 |
|
||||
|
||||
**实现方式**:
|
||||
|
||||
- 封装全局 UI 工具类(如`WXUILabel+Safe.h`),重写`setText:`方法:
|
||||
|
||||
```objective-c
|
||||
- (void) safe_setText:(id)text {
|
||||
if (text == [NSNull null] || text == nil) {
|
||||
self.text = @"--";
|
||||
} else if ([text isKindOfClass:[NSNumber class]] && isinf([text doubleValue])) {
|
||||
self.text = @"计算异常";
|
||||
} else {
|
||||
self.text = [text description];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
业务层只需使用`safe_setText:`,无需为每个异常场景写判断逻辑。
|
||||
|
||||
|
||||
|
||||
## 四、最佳实践
|
||||
|
||||
1. **开发环境零容忍**:通过静态分析、自定义 LLVM 插件 + 运行时 Hook,让问题在测试阶段暴露,比如异常发生时,通过日志打印案发现场数据,或者在 Xcode 面板上可视化的在有问题的代码地方显示 error,及时 crash 掉,阻塞正常流程,以防止开发偷懒不去及时修复,产生技术债,同时也防止问题逃逸到线上。
|
||||
2. **生产环境软兜底**:不要一刀切,做到可配置。业务方在配置平台可以针对不同业务域的配置,决定是否开启兜底展示(某些业务域的架构师可能更严格,需要开发在 debug 阶段解决这些问题,线上如果出现就统计为 crash)。如果开启,那么安全气垫就返回语义化默认值(而非无意义的 0),全局 UI 统一处理,业务层零侵入;
|
||||
3. **可观测驱动修复**:APM 平台统计异常频率 / 影响度,优先处理高频高影响的异常;
|
||||
4. **核心场景按需关注**:仅对价格、支付等核心场景注册回调,非核心场景无需处理。
|
||||
|
||||
虽然机制和策略已经制定了,但是执行还是靠人,难以保证100%解决问题。所以开发解决通过 lint 和 crash 可以拦截到大部分问题,但是逃逸到线上的部分问题,还是需要安全气垫去 cover,线下无限毕竟去清空问题 + 少数逃逸到线上的问题通过安全气垫去 cover 就可以保证相对全面的安全守护。
|
||||
|
||||
|
||||
|
||||
## 五、平台做些什么
|
||||
|
||||
获取到 问题数据是第一步,获取之后问题需要及时上报,需要有一个数据上报系统,数据及时准确、高效的上传到服务端(这就是另一个命题,可以查看这篇文章:[打造一个通用、可配置、多句柄的数据上报 SDK](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md)),到 APM 后台。后台产生工单,根据业务模块分配到对应的责任人、群组。根据问题的紧急程度,报警策略和提醒频次也不一样,比如**波动报警、自定义报警策略等(这也是另一个命题:智能报警策略、波动报警策略、业务线自定义可配置责任人等)**。责任人及时解决问题、修改工单状态、热修复或者发布新版本,问题闭环。
|
||||
|
||||
线上异常上报后,APM 平台需提供:
|
||||
|
||||
- 异常频率(如 “数组越界” 仅 1/10000);
|
||||
- 影响用户数(如仅影响 10 个用户);
|
||||
- 业务上下文(如 “商品详情页 - 价格数组”);
|
||||
- 调用栈 + 设备信息。
|
||||
|
||||
**核心逻辑**:
|
||||
|
||||
- 低频率、低影响的异常(如 1/10000,影响 10 用户):暂时无需业务层处理,研发排期修复即可;
|
||||
- 高频率、高影响的异常(如 1/100,影响 1000 用户):触发告警,研发紧急修复,业务层临时加兜底。
|
||||
|
||||
这样业务开发无需为 “万分之一” 的异常写逻辑,仅需处理 “高频高影响” 的异常。
|
||||
|
||||
|
||||
|
||||
## 总结
|
||||
|
||||
“最佳实践”部分选择的方案既避免了 “安全气垫返回错误值导致业务异常”,又解决了 “业务开发为低概率异常写冗余逻辑” 的问题,是网易、腾讯、阿里等大厂的通用实践 ——**优雅的方案不是 “兜底所有异常”,而是 “让异常可被提前发现、可被观测、最小化影响”**,即使少数逃逸到线上的,再由安全气垫去兜底解决(解决了展示问题、异常上报和案发现场还原问题),一个好的 APM 平台不光是告诉你又问题,还会告诉你哪里有问题。
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
先附上一张总结的非常棒的RunLoop图
|
||||
|
||||
<img src="./../assets/2019-05-09-RunLoop-review.png" style="zoom:30%" />
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-09-RunLoop-review.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ struct __CFRunLoopMode {
|
||||
|
||||
Demo:
|
||||
|
||||
<img src="./../assets/NSRunloopRoundWithCFRunloop.png" style="zoom:30%" />
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSRunloopRoundWithCFRunloop.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
@@ -178,11 +178,11 @@ Source0:
|
||||
|
||||
Demo1:给屏幕点击事件加断点,查看堆栈可以看到是 Source0 触发的。
|
||||
|
||||
<img src="./../assets/TouchActionByRunLoopSource0.png" style="zoom:25%" />
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/TouchActionByRunLoopSource0.png" style="zoom:25%" />
|
||||
|
||||
Demo2: `- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait` 该 API 的底层就是:向目标线程的 RunLoop 添加一个 Source0 事件,标记后唤醒线程执行对应的事件。
|
||||
|
||||
<img src="./../assets/PerformThreadTaskByRunLoopSource0.png" style="zoom:25%" />
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/PerformThreadTaskByRunLoopSource0.png" style="zoom:25%" />
|
||||
|
||||
Source1:
|
||||
|
||||
@@ -221,7 +221,7 @@ CFRunLoopSourceRef 事件源(输入源)
|
||||
|
||||
### 一对多的关系
|
||||
|
||||
<img src="./../assets/ThreadRunLoopModeStructure.png" style="zoom:70%" />
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/ThreadRunLoopModeStructure.png" style="zoom:70%" />
|
||||
|
||||
|
||||
|
||||
@@ -434,7 +434,7 @@ CFRelease(obersver);
|
||||
*/
|
||||
```
|
||||
|
||||

|
||||

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

|
||||

|
||||
|
||||
- 图上左上角的 Input source 是早期 RunLoop 的分法,现在分法为:Source0 和 Source1。
|
||||
- Source0:非基于 port 的,用户主动触发的事件。
|
||||
@@ -527,7 +527,7 @@ CFRelease(obersver);
|
||||
|
||||
但是如何直到系统是运行 RunLoop 的哪个函数?给 viewDidLoad 设置断点,在 lldb 模式输入 `bt` 查看堆栈
|
||||
|
||||

|
||||

|
||||
|
||||
查看 CF 中 `CFRunLoop.c`源码。方法比较复杂,做了精简摘要
|
||||
|
||||
@@ -1137,7 +1137,7 @@ RunLoop 内部几个核心的动作:`__CFRunLoopDoObservers`、`__CFRunLoopSer
|
||||
|
||||
上面结合源码看了 Runloop 是怎么运行的。下面通过图片看看 RunLoop 各个状态运行切换的完整流程。
|
||||
|
||||
<img src="./../assets/RunLoop-SourceCode.png" style="zoom:40%" />
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoop-SourceCode.png" style="zoom:40%" />
|
||||
|
||||
Demo:
|
||||
|
||||
@@ -1151,7 +1151,7 @@ Demo:
|
||||
|
||||
2. 上面8>2,Runloop 处理 GCD Async To Main Quque
|
||||
|
||||
<img src="./../assets/RunLoopPerformTaskFromGCDMainQueue.png" style="zoom:25%" />
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoopPerformTaskFromGCDMainQueue.png" style="zoom:25%" />
|
||||
|
||||
|
||||
|
||||
@@ -1165,7 +1165,7 @@ Demo:
|
||||
|
||||
可以在 App 运行过程中,点击 Xcode 左下角的 debug 暂停按钮,可以看到 App 堆栈存在系统调用
|
||||
|
||||
<img src="./../assets/RunLoopSleepSystemCall.png" style="zoom:30%" />
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoopSleepSystemCall.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
@@ -1647,7 +1647,7 @@ App 启动后,主线程 RunLoop 里注册了2个 Observer,其回调都是 `_
|
||||
|
||||
当界面的 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
|
||||
苹果设计 UI 更新也是 RunLoop 的业务方,所以会注册一个 Observer 监控 `kCFRunLoopBeforeWaiting`(将要休眠)和 `kCFRunLoopExit` (即将退出 RunLoop)状态,然后会执行 `_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()` 回调。内部会遍历所有待处理的 UIView、CALayer 以执行实际的绘制和渲染,更新 UI
|
||||
|
||||
|
||||
|
||||
@@ -1887,7 +1887,7 @@ main 方法内其实就是在执行 _selector。也就是在 NSThread 的初始
|
||||
|
||||
2. `[[NSRunLoop currentRunLoop] run]` api 换掉。查看系统说明,底层其实就是一个无限循环,循环内部不断调用 `runMode:beforeDate:`。下面也有建议,建议我们想销毁 RunLoop,可以替换 API,比如设置一个变量,标记是否需要结束 RunLoop
|
||||
|
||||
<img src="./../assets/RunLoop-RunIssue.png" style="zoom:45%" />
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoop-RunIssue.png" style="zoom:45%" />
|
||||
|
||||
改进代码如下
|
||||
|
||||
@@ -1995,7 +1995,7 @@ self.thread = [[LifeThread alloc] initWithBlock:^{
|
||||
|
||||
效果如下:
|
||||
|
||||
<img src="./../assets/RunLoopThreadKeepLive.png" style="zoom:30%" />
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RunLoopThreadKeepLive.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1079,6 +1079,169 @@ QA:互斥递归锁,可以在不同线程中加锁吗?
|
||||
|
||||
|
||||
|
||||
#### 使用注意事项
|
||||
##### 加解锁必须成对
|
||||
加解锁必须成对出现,否则容易出现多线程性能问题。提供一种思路,利用 `@try-finally` 来保证加解锁必须成对存在,这个写法也是 Weex 官方的实现。
|
||||
Weex 中的 WXThreadSafeMutableDictionary 提供了一个线程安全的字典,其本质是通过加 pthread_muext_t 锁来维护内部的一个字典的。
|
||||
比如下面的代码
|
||||
|
||||
初始化锁相关的配置
|
||||
```Objective-C
|
||||
@interface WXThreadSafeMutableDictionary ()
|
||||
{
|
||||
NSMutableDictionary* _dict;
|
||||
pthread_mutex_t _safeThreadDictionaryMutex;
|
||||
pthread_mutexattr_t _safeThreadDictionaryMutexAttr;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation WXThreadSafeMutableDictionary
|
||||
|
||||
- (instancetype)initCommon
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
pthread_mutexattr_init(&(_safeThreadDictionaryMutexAttr));
|
||||
pthread_mutexattr_settype(&(_safeThreadDictionaryMutexAttr), PTHREAD_MUTEX_RECURSIVE); // must use recursive lock
|
||||
pthread_mutex_init(&(_safeThreadDictionaryMutex), &(_safeThreadDictionaryMutexAttr));
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [self initCommon];
|
||||
if (self) {
|
||||
_dict = [NSMutableDictionary dictionary];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
```
|
||||
在字典操作的地方使用锁
|
||||
```Objective-C
|
||||
- (void)setObject:(id)anObject forKey:(id<NSCopying>)aKey
|
||||
{
|
||||
id originalObject = nil; // make sure that object is not released in lock
|
||||
@try {
|
||||
pthread_mutex_lock(&_safeThreadDictionaryMutex);
|
||||
originalObject = [_dict objectForKey:aKey];
|
||||
[_dict setObject:anObject forKey:aKey];
|
||||
}
|
||||
@finally {
|
||||
pthread_mutex_unlock(&_safeThreadDictionaryMutex);
|
||||
}
|
||||
originalObject = nil;
|
||||
}
|
||||
```
|
||||
这么写的价值:**解锁逻辑「绝对执行」,彻底避免死锁**
|
||||
这是 `@try-finally` 最核心的价值 ——无论 try 块内发生什么(正常执行、提前 return、抛异常),finally 块的解锁逻辑一定会执行
|
||||
|
||||
对比无 try-finally 的写法
|
||||
```Objective-C
|
||||
// Bad: 若setObject抛异常,unlock不会执行→死锁
|
||||
pthread_mutex_lock(&_mutex);
|
||||
[_dict setObject:anObject forKey:aKey];
|
||||
pthread_mutex_unlock(&_mutex);
|
||||
```
|
||||
问题:`[_dict setObject:anObject forKey:aKey]` 可能抛异常(比如 aKey = nil 时会触发 NSInvalidArgumentException),若没有 finally,锁会被永久持有→其他线程调用 lock 时死锁,整个字典无法再操作。
|
||||
|
||||
设计优点:
|
||||
- `@try-finallly`:即使 try 内逻辑出错,finally 也会执行 pthread_mutex_unlock,保证锁最终释放,这是**线程安全的「兜底保障」**
|
||||
- 注意,不是 `try...catch...finally`: 如果加了 catch 逻辑,则字典的 key 为 nil 产生的崩溃也会被捕获掉,这属于不符合预期的行为。因为 key 为 nil 产生的原因太多了,可能是业务代码异常,也可能是数据异常,也可能是逻辑错误,如果一刀切直接用 `try...catch...finally` 捕获了异常,但是没有配置异常的收集、上报、处理逻辑,属于边界不清晰,本质是为了解决加解锁不匹配而可能带来的线程安全问题,却"多管闲事",把字典 key 为 nil 本该向上跑的异常而卡住了(这个问题不再赘述,是一个经典的策略问题,端上的异常发生时,安全气垫的“做与不做”问题)
|
||||
|
||||
##### `pthread_mutex_t` 的加解锁函数返回值处理
|
||||
「加解锁函数都有返回值,需要对返回值进行判断和处理」这个是意识也业务场景问题,先告诉你有返回值,看你的场景需要严格处理还是松散处理。类似 JS 的 `use strict`
|
||||
iOS 系统开源组件(如 libdispatch/GCD)、Apple 官方开源代码,以及知名第三方开源库(AFNetworking、SDWebImage 等)都会严格检查 pthread 锁相关函数的返回值—— 因为系统 / 核心库需要保证鲁棒性,避免锁操作失败导致的死锁、崩溃或线程安全漏洞
|
||||
|
||||
1. GCD 的处理(libdispatch)
|
||||
核心文件:dispatch/src/queue.c(队列的线程安全实现)
|
||||
```c
|
||||
// libdispatch 中 pthread_mutex_lock 返回值检查的典型写法
|
||||
static inline void _dispatch_mutex_lock(pthread_mutex_t *m) {
|
||||
int ret = pthread_mutex_lock(m);
|
||||
// 严格检查返回值,仅允许「成功(0)」或「递归锁重复加锁(EDEADLK)」(针对递归锁场景)
|
||||
if (ret != 0 && ret != EDEADLK) {
|
||||
// 系统级库会触发 crash 并打印错误,避免静默失败
|
||||
dispatch_fatal("pthread_mutex_lock failed: %d", ret);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void _dispatch_mutex_unlock(pthread_mutex_t *m) {
|
||||
int ret = pthread_mutex_unlock(m);
|
||||
if (ret != 0) {
|
||||
dispatch_fatal("pthread_mutex_unlock failed: %d", ret);
|
||||
}
|
||||
}
|
||||
```
|
||||
说明:
|
||||
- 加锁
|
||||
- 对 pthread_mutex_lock,除了「成功(0)」,仅允许递归锁的「重复加锁(EDEADLK)」(递归锁场景下 EDEADLK 是预期行为);
|
||||
- 非预期返回值直接触发 dispatch_fatal(系统级崩溃),避免锁异常导致的隐性问题;
|
||||
- 解锁操作(pthread_mutex_unlock)仅接受「成功(0)」,失败则崩溃
|
||||
|
||||
思考:为什么 GCD 要这么做?
|
||||
系统库是「基础设施」,锁操作失败(如 EINVAL/ENOMEM)意味着系统资源耗尽或参数错误,属于「致命错误」—— 与其静默运行导致更严重的线程安全问题,不如直接崩溃并暴露问题。
|
||||
|
||||
2. AFNetworking(网络库,线程安全的缓存 / 队列实现)
|
||||
AFNetworking 的 AFURLSessionManager.m 中,对锁操作的返回值检查:
|
||||
```Objective-C
|
||||
// AFNetworking 中锁操作的返回值检查
|
||||
- (void)lock {
|
||||
int lockResult = pthread_mutex_lock(&_lock);
|
||||
NSAssert(lockResult == 0, @"Failed to lock mutex with error: %d", lockResult);
|
||||
}
|
||||
|
||||
- (void)unlock {
|
||||
int unlockResult = pthread_mutex_unlock(&_lock);
|
||||
NSAssert(unlockResult == 0, @"Failed to unlock mutex with error: %d", unlockResult);
|
||||
}
|
||||
```
|
||||
说明:
|
||||
- 用 NSAssert 检查返回值(Debug 模式下断言失败会崩溃,Release 模式下跳过);
|
||||
- 兼顾「调试阶段暴露问题」和「Release 阶段不影响运行」;
|
||||
- 符合第三方库的「友好调试 + 线上稳定性」平衡原则。
|
||||
|
||||
|
||||
3. CocoaLumberjack(日志库,线程安全的日志队列)
|
||||
CocoaLumberjack 的 DDLog.m 中,锁返回值检查的严谨写法:
|
||||
```Objective-C
|
||||
- (void)initLock {
|
||||
pthread_mutexattr_t attr;
|
||||
int ret = pthread_mutexattr_init(&attr);
|
||||
if (ret != 0) {
|
||||
DDLogError(@"pthread_mutexattr_init failed: %d", ret);
|
||||
return;
|
||||
}
|
||||
|
||||
ret = pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
|
||||
if (ret != 0) {
|
||||
DDLogError(@"pthread_mutexattr_settype failed: %d", ret);
|
||||
pthread_mutexattr_destroy(&attr);
|
||||
return;
|
||||
}
|
||||
|
||||
ret = pthread_mutex_init(&_lock, &attr);
|
||||
if (ret != 0) {
|
||||
DDLogError(@"pthread_mutex_init failed: %d", ret);
|
||||
pthread_mutexattr_destroy(&attr);
|
||||
return;
|
||||
}
|
||||
|
||||
pthread_mutexattr_destroy(&attr);
|
||||
}
|
||||
```
|
||||
说明:
|
||||
- 「初始化→设置属性→创建锁」全链路检查返回值
|
||||
- 失败时「回滚操作」(如销毁已初始化的 attr),避免资源泄漏
|
||||
- 打错误日志,便于问题排查
|
||||
|
||||
总结:
|
||||
核心原则:锁操作的返回值检查不是 “可选的”,而是 “必须的”,区别仅在于「失败后是崩溃、打日志还是抛异常」,需根据组件的核心程度选择。
|
||||
- 系统级开源库(如 libdispatch):严格检查返回值,非预期失败直接崩溃(保证系统稳定性);
|
||||
- 第三方开源库(AFNetworking/SDWebImage):调试阶段断言 + Release 阶段日志(平衡调试和线上稳定性);
|
||||
- 业务组件(如你的字典):推荐「断言 + 日志」模式 ——Debug 暴露问题,Release 不崩溃且留痕;
|
||||
|
||||
#### 互斥锁的条件变量 pthread_cond_t
|
||||
|
||||
多线程环境下,很多时候没办法确保先有数据再消费,比如生产者-消费者问题,这时候就有互斥锁的另一个 API 了,即条件变量`pthread_cond_t`
|
||||
@@ -2543,4 +2706,8 @@ APIClient.fetchData(...).then().onFailure();
|
||||
- GCD:简单的任务处理,以及多读单写、读写锁、dispatch_group_async
|
||||
- NSOperationQueue、NSOperation:AFNetworking、SDWebImage ,可以方便对 Operation 的状态管理和依赖管理
|
||||
- NSThread,主要用于实现常驻线程
|
||||
- NSOperation Finished 之后如何移除?KVO
|
||||
- NSOperation Finished 之后如何移除?KVO
|
||||
- 锁操作的返回值检查不是 “可选的”,而是 “必须的”,区别仅在于「失败后是崩溃、打日志还是抛异常」,需根据组件的核心程度选择。
|
||||
- 系统级开源库(如 libdispatch):严格检查返回值,非预期失败直接崩溃(保证系统稳定性);
|
||||
- 第三方开源库(AFNetworking/SDWebImage):调试阶段断言 + Release 阶段日志(平衡调试和线上稳定性);
|
||||
- 业务组件(如你的字典):推荐「断言 + 日志」模式 ——Debug 暴露问题,Release 不崩溃且留痕;
|
||||
@@ -45,9 +45,15 @@ Hybrid 架构设计的第一要考虑的问题就是如何设计前端与 Native
|
||||
|
||||
### 2. 账号信息设计
|
||||
|
||||
账号系统是重要且无法避免的,Native 需要设计良好安全的身份验证机制,保证这块对业务开发者足够透明,打通账户体系
|
||||
账号系统是重要且无法避免的,Native 需要设计良好安全的身份验证机制,保证这块对业务开发者足够透明,打通账户体系。
|
||||
举个例子,客户端提供了一个 WebView 容器,Native 侧的个人中心,用户是登陆态。但用户去访问 A 业务,A 业务的实现是前端实现的。访问页面时,页面内容都可以看到了,但是某个接口需要用户登录态,然后忽然跳转到登陆页,对于用户体验很不好。用户一脸懵逼,我不是登陆了吗?为什么还跳转到登陆页面,管你页面的技术实现是 Native 还是前端,对于用户来说,用户不是专业技术人员,不会判断是 Native 还是跨端方案,也不需要判断。
|
||||
|
||||
所以解决方案是 Native 和 Hybrid 打通账号体系,通过 WebView 去访问 H5 的时候,应该保持同样的登陆态,用户账号信息是打通的。
|
||||
|
||||
Todo:
|
||||
WebView 的鉴权
|
||||
|
||||
举个例子:携程的动态化很高现在 RN 居多,前几年的时候大部分页面还是 Hybrid 架构。假设用户在浏览器里面访问了一个页面 A,输入手机号登陆成功了,也在页面 A 上完成了自己的业务。此时
|
||||
|
||||
|
||||
### 3. Hybrid 开发调试
|
||||
@@ -830,4 +836,12 @@ NSURLProtocol能够让你去重新定义苹果的URL加载系统(URL Loading Sys
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
## 十一、如何落地和推进
|
||||
如果开展 Hybrid 就存在一个问题,很多业务在迭代或者新开的时候就会选择用 H5 前端技术去实现了,但是如果团队内的客户端同学不懂前端或者不是很懂的时候,就会存在一个抵触的心理,觉得是前端在“侵占”、“蚕食”客户端的领地。所以我们做 Hybrid 跨端项目的时候就需要诚恳一些,抱着“双赢”的出发点去聊、去推进。
|
||||
- 你在咱们公司当前的项目1中不选择 Hyrbid 技术,将来的项目2、项目3,其他合作的同学可能就会拥抱 Hybrid 技术来水岸
|
||||
- 多端融合能力、跨端是趋势,即使在我们公司一直不做,你离开公司去新的公司,肯定也会遇到用跨端去写业务的场景,这是趋势,尽早拥抱吧
|
||||
- 你如果只做 Native 以后新的项目或者新的机会给你,你抓不住,不如趁此机和我合作,或者分配一些前端开发的小任务给你,趁此学会前端技术和 Hybrid 的设计,成为一个跨端工程师,点亮、丰富自己的技能树,日后项目的技术选型方面,类似小程序、Weex/RN/Flutter 方案等,上手就会很方便了,也在做技术方案调研、评估方面多一个可选项。
|
||||
|
||||
客户端同学都是程序员,都比较爱学习,拥抱新技术、新设计,早点拥抱技术红利,享受 Hybrid 设计哲学带来的思维增益。我们要实现的效果并不是前端去侵占、蚕食 Native 领地的效果,而是拥抱优雅的、高效的技术方案,去拓展业务上更多的可能性,也在技术方面增加更多的视野维度
|
||||
@@ -11,13 +11,13 @@
|
||||
|
||||

|
||||
|
||||
```
|
||||
```objective-c
|
||||
//BaseView
|
||||
#import "BaseView.h"
|
||||
|
||||
@implementation BaseView
|
||||
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
|
||||
NSLog(@"%@",[self class]);
|
||||
NSLog(@"%@",[self class]);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -292,89 +292,89 @@ static char *ua_viewController_close_time = "ua_viewController_close_time";
|
||||
|
||||
当然有解决方案啦。
|
||||
|
||||
- 找出当前元素在父层同类型元素中的索引。根据当前的元素遍历当前元素的父级元素的子元素,如果出现相同的元素,则需要判断当前元素是所在层级的第几个元素
|
||||
找出当前元素在父层同类型元素中的索引。根据当前的元素遍历当前元素的父级元素的子元素,如果出现相同的元素,则需要判断当前元素是所在层级的第几个元素
|
||||
|
||||
对当前的控件元素的父视图的全部子视图进行遍历,如果存在和当前的控件元素同类型的控件,那么需要判断当前控件元素在同类型控件元素中的所处的位置,那么则可以唯一定位。举例:`GoodsCell-3.GoodsTableView.GoodsViewController.xxApp`
|
||||
对当前的控件元素的父视图的全部子视图进行遍历,如果存在和当前的控件元素同类型的控件,那么需要判断当前控件元素在同类型控件元素中的所处的位置,那么则可以唯一定位。举例:`GoodsCell-3.GoodsTableView.GoodsViewController.xxApp`
|
||||
|
||||
```Objective-c
|
||||
```Objective-c
|
||||
//UIResponder分类
|
||||
- (NSString *)ua_identifierKa
|
||||
{
|
||||
// if (self.xq_identifier_ka == nil) {
|
||||
if ([self isKindOfClass:[UIView class]]) {
|
||||
UIView *view = (id)self;
|
||||
NSString *sameViewTreeNode = [view obtainSameSuperViewSameClassViewTreeIndexPath];
|
||||
NSMutableString *str = [NSMutableString string];
|
||||
//特殊的 加减购 因为带有spm但是要区分加减 需要带TreeNode
|
||||
NSString *className = [NSString stringWithUTF8String:object_getClassName(view)];
|
||||
if (!view.accessibilityIdentifier || [className isEqualToString:@"uaButton"]) {
|
||||
[str appendString:sameViewTreeNode];
|
||||
[str appendString:@","];
|
||||
}
|
||||
while (view.nextResponder) {
|
||||
[str appendFormat:@"%@,", NSStringFromClass(view.class)];
|
||||
if ([view.class isSubclassOfClass:[UIViewController class]]) {
|
||||
break;
|
||||
}
|
||||
view = (id)view.nextResponder;
|
||||
}
|
||||
self.xq_identifier_ka = [self md5String:[NSString stringWithFormat:@"%@",str]];
|
||||
// self.xq_identifier_ka = [NSString stringWithFormat:@"%@",str];
|
||||
}
|
||||
// }
|
||||
return self.xq_identifier_ka;
|
||||
}
|
||||
|
||||
// UIView 分类
|
||||
- (NSString *)obtainSameSuperViewSameClassViewTreeIndexPat
|
||||
{
|
||||
NSString *classStr = NSStringFromClass([self class]);
|
||||
//cell的子view
|
||||
//UITableView 特殊的superview (UITableViewContentView)
|
||||
//UICollectionViewCell
|
||||
BOOL shouldUseSuperView =
|
||||
([classStr isEqualToString:@"UITableViewCellContentView"]) ||
|
||||
([[self.superview class] isKindOfClass:[UITableViewCell class]])||
|
||||
([[self.superview class] isKindOfClass:[UICollectionViewCell class]]);
|
||||
if (shouldUseSuperView) {
|
||||
return [self obtainIndexPathByView:self.superview];
|
||||
}else {
|
||||
return [self obtainIndexPathByView:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)obtainIndexPathByView:(UIView *)view
|
||||
{
|
||||
NSInteger viewTreeNodeDepth = NSIntegerMin;
|
||||
NSInteger sameViewTreeNodeDepth = NSIntegerMin;
|
||||
|
||||
NSString *classStr = NSStringFromClass([view class]);
|
||||
|
||||
NSMutableArray *sameClassArr = [[NSMutableArray alloc]init];
|
||||
//所处父view的全部subviews根节点深度
|
||||
for (NSInteger index =0; index < view.superview.subviews.count; index ++) {
|
||||
//同类型
|
||||
if ([classStr isEqualToString:NSStringFromClass([view.superview.subviews[index] class])]){
|
||||
[sameClassArr addObject:view.superview.subviews[index]];
|
||||
}
|
||||
if (view == view.superview.subviews[index]) {
|
||||
viewTreeNodeDepth = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
//所处父view的同类型subviews根节点深度
|
||||
for (NSInteger index =0; index < sameClassArr.count; index ++) {
|
||||
if (view == sameClassArr[index]) {
|
||||
sameViewTreeNodeDepth = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return [NSString stringWithFormat:@"%ld",sameViewTreeNodeDepth];
|
||||
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
- (NSString *)ua_identifierKa
|
||||
{
|
||||
// if (self.xq_identifier_ka == nil) {
|
||||
if ([self isKindOfClass:[UIView class]]) {
|
||||
UIView *view = (id)self;
|
||||
NSString *sameViewTreeNode = [view obtainSameSuperViewSameClassViewTreeIndexPath];
|
||||
NSMutableString *str = [NSMutableString string];
|
||||
//特殊的 加减购 因为带有spm但是要区分加减 需要带TreeNode
|
||||
NSString *className = [NSString stringWithUTF8String:object_getClassName(view)];
|
||||
if (!view.accessibilityIdentifier || [className isEqualToString:@"uaButton"]) {
|
||||
[str appendString:sameViewTreeNode];
|
||||
[str appendString:@","];
|
||||
}
|
||||
while (view.nextResponder) {
|
||||
[str appendFormat:@"%@,", NSStringFromClass(view.class)];
|
||||
if ([view.class isSubclassOfClass:[UIViewController class]]) {
|
||||
break;
|
||||
}
|
||||
view = (id)view.nextResponder;
|
||||
}
|
||||
self.xq_identifier_ka = [self md5String:[NSString stringWithFormat:@"%@",str]];
|
||||
// self.xq_identifier_ka = [NSString stringWithFormat:@"%@",str];
|
||||
}
|
||||
// }
|
||||
return self.xq_identifier_ka;
|
||||
}
|
||||
|
||||
// UIView 分类
|
||||
- (NSString *)obtainSameSuperViewSameClassViewTreeIndexPat
|
||||
{
|
||||
NSString *classStr = NSStringFromClass([self class]);
|
||||
//cell的子view
|
||||
//UITableView 特殊的superview (UITableViewContentView)
|
||||
//UICollectionViewCell
|
||||
BOOL shouldUseSuperView =
|
||||
([classStr isEqualToString:@"UITableViewCellContentView"]) ||
|
||||
([[self.superview class] isKindOfClass:[UITableViewCell class]])||
|
||||
([[self.superview class] isKindOfClass:[UICollectionViewCell class]]);
|
||||
if (shouldUseSuperView) {
|
||||
return [self obtainIndexPathByView:self.superview];
|
||||
}else {
|
||||
return [self obtainIndexPathByView:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)obtainIndexPathByView:(UIView *)view
|
||||
{
|
||||
NSInteger viewTreeNodeDepth = NSIntegerMin;
|
||||
NSInteger sameViewTreeNodeDepth = NSIntegerMin;
|
||||
|
||||
NSString *classStr = NSStringFromClass([view class]);
|
||||
|
||||
NSMutableArray *sameClassArr = [[NSMutableArray alloc]init];
|
||||
//所处父view的全部subviews根节点深度
|
||||
for (NSInteger index =0; index < view.superview.subviews.count; index ++) {
|
||||
//同类型
|
||||
if ([classStr isEqualToString:NSStringFromClass([view.superview.subviews[index] class])]){
|
||||
[sameClassArr addObject:view.superview.subviews[index]];
|
||||
}
|
||||
if (view == view.superview.subviews[index]) {
|
||||
viewTreeNodeDepth = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
//所处父view的同类型subviews根节点深度
|
||||
for (NSInteger index =0; index < sameClassArr.count; index ++) {
|
||||
if (view == sameClassArr[index]) {
|
||||
sameViewTreeNodeDepth = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return [NSString stringWithFormat:@"%ld",sameViewTreeNodeDepth];
|
||||
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,775 @@
|
||||
|
||||
# Ruby
|
||||
|
||||
> 为了 iOS 工程化开展,自己最近开始了 Ruby 的学习,本篇博文就用来记录 Ruby 的学习心得和体验。
|
||||
> 为了 iOS 工程化开展,自己最近开始了 Ruby 的学习,本篇博文就用来记录 Ruby 的学习心得和体验。本文作为切入点,展开聊聊原理、组件、脚本
|
||||
|
||||
|
||||
## 一. Ruby VS Python
|
||||
- Python 的解析器实现更成熟,第三方库质量高。但是 Ruby 包管理更简单、方便。
|
||||
- Python 的应用领域广泛。而Ruby目前主要局限在在 Web 领域与精致项目。
|
||||
- Python语法简单,Ruby更强大、灵活
|
||||
|
||||
|
||||
|
||||
## 二. Ruby 语法
|
||||
|
||||
### 1. 注释
|
||||
单行注释
|
||||
```
|
||||
# 单行注释
|
||||
puts "Hello, ruby!"
|
||||
```
|
||||
|
||||
多行注释
|
||||
```Ruby
|
||||
=begin
|
||||
多行注释:第1行
|
||||
多行注释:第2行
|
||||
多行注释:第3行
|
||||
=end
|
||||
print("Hello world!\n")
|
||||
```
|
||||
|
||||
### 2. 打印
|
||||
- puts:打印后自动换行
|
||||
- print:打印后不会自动换行
|
||||
|
||||
- 另外如果打印内容携带变量格式的话,必须用双引号。比如
|
||||
```Ruby
|
||||
name = "@FantasticLBP"
|
||||
puts "hello, #{name}!"
|
||||
puts 'hello,#{name}!'
|
||||
|
||||
# 输出
|
||||
hello, @FantasticLBP!
|
||||
hello,#{name}!
|
||||
```
|
||||
- 如果要直接 shell,则需要用 ``
|
||||
```Ruby
|
||||
puts `ruby --version`
|
||||
# 输出
|
||||
ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.x86_64-darwin21]
|
||||
```
|
||||
|
||||
### 3. 万物皆对象
|
||||
```Ruby
|
||||
puts 3.class
|
||||
puts 'FantasticLBP'.class
|
||||
puts nil.class
|
||||
puts true.class
|
||||
# 输出
|
||||
Integer
|
||||
String
|
||||
NilClass
|
||||
TrueClass
|
||||
```
|
||||
|
||||
|
||||
### 4. Symbol
|
||||
- Ruby 是一个强大的面向对象脚本语言,一切皆是对象
|
||||
- 在 Ruby 中 Symbol 表示“名字”,比如字符串的名字,标识符的名字。
|
||||
- 创建一个 Symbol 对象的方法是在名字或者字符串前面加上冒号:
|
||||
- 在 Ruby 中每一个对象都有唯一的对象标识符(Object Identifier)
|
||||
- 对于 Symbol 对象,一个名字唯一确定一个 Symbol 对象
|
||||
- Ruby 内部一直在使用 Symbol,Ruby内部也存在符号表
|
||||
- Symbol 本质上是一个数字,这个数字和创建 Symbol 的名字形成一对一的映射;而String 对象是一个重量级的 用C结构体表示的家伙,因此使用 Symbol 和 String 的开销相差很大。
|
||||
- 符号表是一个全局数据结构,它存放了所有 Symbol 的(数字ID,名字)。 Ruby 不会从中删除 Symbol ,因此 当你创建一个 Symbol 对象后,它将一直存在,直到程序结束。
|
||||
|
||||
## 三. 安装篇
|
||||
|
||||
### 1. rvm & rbenv
|
||||
|
||||
- rvm & rbenv 是一种命令行工具,可让您轻松地安装,管理和使用多个Ruby环境。
|
||||
- 这两个工具本质都是 PATH 上做手脚,一个在执行前,一个在执行中
|
||||
- 如果你不需要维护特定版本的 Ruby 项目,那么只需要装一个比较新的 Ruby 版本 就行了。`brew install ruby`
|
||||
|
||||
### 2. gem
|
||||
|
||||
- 与大多数的编程语言一样,Ruby 也受益于海量的第三方代码库
|
||||
- 这些代码库大部分都以 Gem 形式发布。 RubyGems 是设计用来帮助创建,分享和安装 这些代码库的
|
||||
- `gem search -r/-f <gem>`
|
||||
- `gem install <gem> --version <num>`
|
||||
- `gem list`
|
||||
有没有发现 gem search、install、list 和 cocoapods 的 pod 指令一样
|
||||
|
||||
### 3. Bundler
|
||||
|
||||
Bundler 能够跟踪并安装所需的特定版本的 gem,以此来为 Ruby 项目提供一致的运行环境
|
||||
<img src="./../assets/RubyGemProcess.png" style="zoom:40%" />
|
||||
|
||||
```
|
||||
source 'https://rubygems.org' gem 'rails', '4.1.0.rc2'
|
||||
gem ‘rack-cache'
|
||||
gem 'nokogiri', '~> 1.6.1'
|
||||
```
|
||||
|
||||
- 读取 Gemfile:Bundler 首先会读取当前目录下的 Gemfile 文件,解析其中声明的所有依赖项及其版本约束
|
||||
- 解析依赖关系:
|
||||
- 分析每个 gem 的版本要求,确定满足所有约束的最佳版本组合
|
||||
- 处理依赖的依赖(传递依赖),确保整个依赖树的兼容性
|
||||
- 检查本地缓存:
|
||||
- 查看本地是否已缓存所需版本的 gem
|
||||
- 如果有,直接使用本地缓存,跳过下载步骤
|
||||
- 从源下载 gem:
|
||||
- 对于本地没有的 gem,从 source 'https://rubygems.org' 指定的源下载
|
||||
- 默认源是 RubyGems 官方仓库,国内用户可能需要切换到镜像源以提高速度
|
||||
- 安装 gem 到项目目录:
|
||||
- 默认情况下,gem 会被安装到项目根目录下的 vendor/bundle 目录
|
||||
- 这种方式可以避免污染系统级的 gem 安装,实现项目间的依赖隔离
|
||||
- 生成 Gemfile.lock:
|
||||
- 安装完成后,Bundler 会生成 Gemfile.lock 文件
|
||||
- 该文件记录了实际安装的每个 gem 的精确版本,确保团队协作或部署时使用完全相同的依赖版本
|
||||
|
||||
思考:怎么样,是不是发现 Ruby Bundler 工作流程和 iOS Cocoapods 的工作过程一致?是的,iOS Cocoapods 的设计就是参考 Ruby Bundler 的设计。甚至连 pod search、install、list API 设计也和 gem 一致
|
||||
|
||||
|
||||
|
||||
## 四. Cocoapods
|
||||
|
||||
### 1. cocoapods-binary
|
||||
- cocoapods-binary 通过开关,在 pod insatll 的过程中进行 library 的预编译,生成 framework,并自动集成到项目中。
|
||||
- 整个预编译工作分成了三个阶段来完成:
|
||||
- binary pod 的安装
|
||||
- binary pod 的预编译
|
||||
- binary pod 的集成
|
||||
|
||||
### 2. Hook
|
||||
- pre_install:Pod 下载之后,但在安装之前可以有时机对 Pod 进行任何更改
|
||||
- post_install:当所需依赖安装完成,但此时生成的 XcodeProj 项目在写入磁盘前,我们可以对其进行最后的更改
|
||||
|
||||
|
||||
### 3. Cocoapods 工程拆解
|
||||
#### 1. cocoapods.gemspec 文件
|
||||
```Ruby
|
||||
# encoding: UTF-8
|
||||
require File.expand_path('../lib/cocoapods/gem_version', __FILE__)
|
||||
require 'date'
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = "cocoapods"
|
||||
s.version = Pod::VERSION
|
||||
s.date = Date.today
|
||||
s.license = "MIT"
|
||||
s.email = ["eloy.de.enige@gmail.com", "fabiopelosin@gmail.com", "kyle@fuller.li", "segiddins@segiddins.me"]
|
||||
s.homepage = "https://github.com/CocoaPods/CocoaPods"
|
||||
s.authors = ["Eloy Duran", "Fabio Pelosin", "Kyle Fuller", "Samuel Giddins"]
|
||||
|
||||
s.summary = "The Cocoa library package manager."
|
||||
s.description = "CocoaPods manages library dependencies for your Xcode project.\n\n" \
|
||||
"You specify the dependencies for your project in one easy text file. " \
|
||||
"CocoaPods resolves dependencies between libraries, fetches source " \
|
||||
"code for the dependencies, and creates and maintains an Xcode " \
|
||||
"workspace to build your project.\n\n" \
|
||||
"Ultimately, the goal is to improve discoverability of, and engagement " \
|
||||
"in, third party open-source libraries, by creating a more centralized " \
|
||||
"ecosystem."
|
||||
|
||||
s.files = Dir["lib/**/*.rb"] + %w{ bin/pod bin/sandbox-pod README.md LICENSE CHANGELOG.md }
|
||||
|
||||
s.executables = %w{ pod sandbox-pod }
|
||||
s.require_paths = %w{ lib }
|
||||
|
||||
# Link with the version of CocoaPods-Core
|
||||
s.add_runtime_dependency 'cocoapods-core', "= #{Pod::VERSION}"
|
||||
|
||||
s.add_runtime_dependency 'claide', '>= 1.0.2', '< 2.0'
|
||||
s.add_runtime_dependency 'cocoapods-deintegrate', '>= 1.0.3', '< 2.0'
|
||||
s.add_runtime_dependency 'cocoapods-downloader', '>= 2.1', '< 3.0'
|
||||
s.add_runtime_dependency 'cocoapods-plugins', '>= 1.0.0', '< 2.0'
|
||||
s.add_runtime_dependency 'cocoapods-search', '>= 1.0.0', '< 2.0'
|
||||
s.add_runtime_dependency 'cocoapods-trunk', '>= 1.6.0', '< 2.0'
|
||||
s.add_runtime_dependency 'cocoapods-try', '>= 1.1.0', '< 2.0'
|
||||
s.add_runtime_dependency 'molinillo', '~> 0.8.0'
|
||||
s.add_runtime_dependency 'xcodeproj', '>= 1.27.0', '< 2.0'
|
||||
|
||||
s.add_runtime_dependency 'colored2', '~> 3.1'
|
||||
s.add_runtime_dependency 'escape', '~> 0.0.4'
|
||||
s.add_runtime_dependency 'fourflusher', '>= 2.3.0', '< 3.0'
|
||||
s.add_runtime_dependency 'gh_inspector', '~> 1.0'
|
||||
s.add_runtime_dependency 'nap', '~> 1.0'
|
||||
s.add_runtime_dependency 'ruby-macho', '~> 4.1.0'
|
||||
|
||||
s.add_runtime_dependency 'addressable', '~> 2.8'
|
||||
|
||||
s.add_development_dependency 'bacon', '~> 1.1'
|
||||
s.add_development_dependency 'bundler', '~> 2.0'
|
||||
s.add_development_dependency 'rake', '~> 12.3'
|
||||
|
||||
s.required_ruby_version = '>= 2.6'
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
`cocoapods.gemspec` 作为 Cocoapods 工程的配置文件,类似 iOS 组件库的 Podspec 文件一样。
|
||||
Cocospods 工程本身就是一个 Ruby gem,所以 `cocoapods.gemspec` 用于描述这个 gem 包的元数据,包括作者、版本、描述信息,包括一些导入的文件。
|
||||
|
||||
也声明了该 gem 包含的源代码文件、资源文件,以及它所依赖的其他 Ruby gem(比如 xcodeProj 等)和版本要求,确保安装时能正确解析依赖关系。
|
||||
|
||||
|
||||
|
||||
#### 2. cocoapods-core
|
||||
|
||||
1. CocoaPods 核心模块,用来支持:
|
||||
|
||||
- Pod::specification(podspec)
|
||||
|
||||
- Pod::Podfile(Podfile)
|
||||
|
||||
- Pod::Source(Spec repo)
|
||||
|
||||
2. cocoapods-deintergrate: 用于从项目中删除和取消集成 CocoaPods,指令为 `pod deintegrate`
|
||||
|
||||
3. Xcodeproj:
|
||||
|
||||
来操作 Xcode 项目的创建和编辑等。同时支持 Xcode 项目的脚本管理和 libraries 构建,以及 Xcode 工作空间(.xcworkspace) 和配置文件 .xcconfig 的管理
|
||||
|
||||
4. cocospods-downloader:用于下载和管理引入的源码
|
||||
|
||||
5. cocoapods-plugins: 插件管理功能
|
||||
|
||||
6. cocoapods-try:可以快速体验该 pod 的 Demo 项目
|
||||
|
||||
7. CLAide:命令行解释器
|
||||
|
||||
8. ruby-macho:一个用于检查和修改 Mach-O 文件的 Ruby 库
|
||||
|
||||
|
||||
|
||||
#### 3. Podfile
|
||||
|
||||
Podfile 是一个文件,以 DSL 来描述依赖关系,用于描述项目所需要的第三方库。
|
||||
|
||||
|
||||
|
||||
### 4. VSCode 调试 Cocoapods
|
||||
|
||||
1. 新创建文件夹 `RubyDemos`
|
||||
2. 从 git clone Cocoapods 源码到本地目录
|
||||
3. 进入到 Cocoapods 文件夹,将分支切换到和本机安全的 pod 版本一致的分支,指令为: **git checkout `pod --version`**
|
||||
4. `RubyDemos` 根目录下创建一个 Xcode iOS 工程,并为其编写 Podfile 文件。目的是为了调试 Cocoapods
|
||||
5. `RubyDemos` 根目录下创建一个 **Gemfile** 文件。内容如下:
|
||||
```Ruby
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'cocoapods', path: './Cocoapods' # 指向本地源码
|
||||
gem 'debug', '~> 1.9.0' # 调试用
|
||||
```
|
||||
6. 终端执行 `bundle install` 指令
|
||||
7. 此时,项目文件夹为:
|
||||
```
|
||||
.
|
||||
├── CocoaPods
|
||||
├── Demos
|
||||
├── Gemfile
|
||||
├── Gemfile.lock
|
||||
└── StaticLibConflictsDemo
|
||||
```
|
||||
8. 用 VSCode 打开工程。进入 Run and Debug 面板 → 点击 create a launch.json file → 选择 Ruby → 选 Debug Local File。
|
||||
9. 修改 launch.json 内容。
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug CocoaPods",
|
||||
"type": "rdbg", // 必须为 rdbg(debug 工具类型)
|
||||
"request": "launch",
|
||||
"script": "${workspaceFolder}/CocoaPods/bin/pod", // 源码中的 pod 入口
|
||||
"args": ["install", "--verbose"], // 执行 pod install
|
||||
"cwd": "${workspaceFolder}/StaticLibConflictsDemo", // 测试工程目录(Podfile 所在目录)
|
||||
"useBundler": true, // 强制通过 bundle 执行
|
||||
"askParameters": false // 关闭参数询问(避免干扰)
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
10. VSCode 插件市场安装:VSCode rdbg Ruby Debugger、Ruby LSP
|
||||
11. VSCode 面板中,点击左侧的调试按钮。便可调试。要是看到下面的图,说明可以正常 Debug 了
|
||||
<img src="./../assets/VSCodeDebugCocoapods.png" style="zoom:30%" />
|
||||
|
||||
接下来就可以愉快的调试了。
|
||||
说明:
|
||||
- pod 的每个指令,分别对应 Cocoapods 工程中一个代码文件
|
||||
<img src="./../assets/CocoapodsCommandWithSciptFile.png" style="zoom:30%" />
|
||||
- 同时根据观察,发现 `target do` 的代码比 pre_install、post_install 执行更早。所以我们可以做一些脚本化的操作。
|
||||
比如下面,增加了一段自定义的脚本
|
||||
<img src="./../assets/CocoapodsSelfDefinedScript.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
## 五、体验核心依赖库能力
|
||||
|
||||
### 1. ruby-macho
|
||||
|
||||
#### 1. 操作 Mach-O 文件
|
||||
|
||||
读取 Mach-O 文件,并用操作对象的方式去读区、增加、删除信息。
|
||||
|
||||
分别对 Mach-O 增加了一个 `LC_RPATH` 类型的 Load Command
|
||||
|
||||
```ruby
|
||||
lc_rpath = MachO::LoadCommands::LoadCommand.create(:LC_RPATH, 'test_rpath')
|
||||
file_exec.add_command lc_rpath
|
||||
```
|
||||
|
||||
<img src="./../assets/RubyMachoAddLoadCommand.png" style="zoom:30%" />
|
||||
|
||||
对 Mach-O 文件中删除了类型为 `LC_LINKER_OPTION` 的 Load Command
|
||||
|
||||
<img src="./../assets/RubyMachoDeleteLoadCommand.png" style="zoom:30%" />
|
||||
|
||||
#### 2. 操作动态库
|
||||
|
||||
`ruby-macho` 还可以读取动态库信息,下面演示几个能力:
|
||||
|
||||
- 读取动态库所依赖的动态库信息
|
||||
|
||||
```ruby
|
||||
# 打印出当前 Mach-O 文件使用的所有动态库
|
||||
macho_dylibs = MachO::Tools.dylibs(macho_filepath)
|
||||
macho_dylibs.each do | dylib |
|
||||
puts dylib
|
||||
end
|
||||
```
|
||||
|
||||
- 修改动态库的 id
|
||||
|
||||
```ruby
|
||||
# 修改动态库的 id
|
||||
MachO::Tools.change_dylib_id(macho_copy_filepath, 'test_selfdefined_dylib')
|
||||
```
|
||||
|
||||
修改后在终端用指令 **objdump --macho --private-headers ./macho/libAFNetworking_copy.dylib | grep 'LC_ID_DYLIB' -A 5** 验证效果,如下图所示:
|
||||
|
||||
<img src="./../assets/RubyMachoChangeIDOnDylib.png" style="zoom:30%" />
|
||||
|
||||
也可以直接查看动态库的 id
|
||||
|
||||
```ruby
|
||||
original_dylib_id = MachO::MachOFile.new(macho_filepath)
|
||||
copyed_dylib_id = MachO::MachOFile.new(macho_copy_filepath)
|
||||
|
||||
# 也可以直接查看动态库的 id
|
||||
puts "before change dylib id: #{original_dylib_id.dylib_id}"
|
||||
puts "after change dylib id: #{copyed_dylib_id.dylib_id}"
|
||||
```
|
||||
|
||||
- 修改动态库 rpath
|
||||
|
||||
```ruby
|
||||
MachO::Tools.change_rpath(macho_copy_filepath, '@loader_path/Frameworks', '@loader_path/Frameworks/FantasicLBP')
|
||||
```
|
||||
|
||||
<img src="./../assets/RubyMachoChangeRPath.png" style="zoom:30%" />
|
||||
|
||||
#### 3. 合并动态库到胖二进制
|
||||
|
||||
ruby-macho 有很多丰富的 API,基本上开发阶段所遇到的问题,都有现成的 API 解决。比如二进制指令集的合并
|
||||
|
||||
```ruby
|
||||
filenames = [dylib_merged_filepath, dylib_arm_filepath]
|
||||
# # 第一个参数为合并之后的动态库名称,第二个参数为需要合并的一堆动态库
|
||||
MachO::Tools.merge_machos(dylib_merged_filepath, *filenames)
|
||||
```
|
||||
|
||||
合并后用 `otool -f ./macho/libAFNetworking_merged.dylib` 指令查看指令集
|
||||
|
||||
<img src="./../assets/RubyMachoMergeMach.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
### 2. Xcodeproj
|
||||
|
||||
通过 xcodeproject 路径构建 xcodeproj 对象 `app_project = Xcodeproj::Project.new(app_project_path)`
|
||||
|
||||
通过 xcworkspace 路径构建 xcworkspace 对象 `app_workspace = Xcodeproj::Workspace.new_from_xcworkspace(app_workspace_project_path)`
|
||||
|
||||
然后通过对象的方式访问 xcworkspace 的信息。比如 schemes
|
||||
|
||||
```ruby
|
||||
app_workspace.schemes.each do | scheme |
|
||||
puts scheme
|
||||
end
|
||||
```
|
||||
|
||||
<img src="./../assets/xcodeprojVisitScheme.png" style="zoom:30%" />
|
||||
|
||||
也可以针对特定的 target 修改 xcconfig
|
||||
|
||||
```ruby
|
||||
# 修改 targets 中第一个对应 configurations 为指定的 xcconfig 文件
|
||||
configuration_path = File.dirname(__FILE__) + '/xcodeproject/AFNetworkingMock/xcodeproj-testing.debug.xcconfig'
|
||||
# 转换为 xcode 的 file
|
||||
xc_file = app_project.new_file(configuration_path)
|
||||
|
||||
app_project.targets.first.build_configurations.first.base_configuration_reference = xc_file
|
||||
```
|
||||
|
||||
效果如下:
|
||||
|
||||
<img src="./../assets/xcodeprojSetXcconfig.png" style="zoom:30%" />
|
||||
|
||||
也可以对特定的 target 修改 buildSetting 中的信息,比如 bundle id
|
||||
|
||||
```ruby
|
||||
# 修改 target 的 bundle id
|
||||
app_project.targets.each do | target |
|
||||
target.build_configurations.each do | config |
|
||||
config.build_settings['PRODUCT_BUNDLE_IDENTIFIER'] = 'com.github.FantasticLBP.AFNetworkingMock' if config.name == 'Debug'
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
<img src="./../assets/xcodeprojChangeBuildSettings.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
## 六、自定义 cocoapods 插件
|
||||
|
||||
### 1. 自定义 gem 库
|
||||
|
||||
|
||||
|
||||
#### 1. 初始化创建
|
||||
|
||||
输入指令 `bundle gem cocoapods-hmap` 自定义一个名为 `cocoapods-hmap` 的 gem 库
|
||||
|
||||
#### 2. 工程结构说明
|
||||
|
||||
得到的工程结构是:
|
||||
|
||||
```shell
|
||||
.
|
||||
├── CHANGELOG.md
|
||||
├── CODE_OF_CONDUCT.md
|
||||
├── Gemfile
|
||||
├── LICENSE.txt
|
||||
├── README.md
|
||||
├── Rakefile
|
||||
├── bin
|
||||
│ ├── console
|
||||
│ └── setup
|
||||
├── cocoapods-hmap.gemspec
|
||||
├── lib
|
||||
│ └── cocoapods
|
||||
│ ├── hmap
|
||||
│ │ └── version.rb
|
||||
│ └── hmap.rb
|
||||
└── sig
|
||||
└── cocoapods
|
||||
└── hmap.rbs
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- **cocoapods-hmap.gemspec**:
|
||||
|
||||
- gem 的核心配置文件,定义了 gem 的名称、版本、作者、依赖、描述、文件包含规则等关键信息。
|
||||
- 用于打包和发布 gem 到 RubyGems 仓库,是 gem 工程的 “身份证
|
||||
|
||||
- **Rakefile**:
|
||||
|
||||
- 定义自动化任务(如测试、打包、发布等),通过 `rake <任务名>` 执行(类似 `Makefile`)。
|
||||
- 常见任务:`rake spec`(运行测试)、`rake build`(打包 gem)、`rake release`(发布到 RubyGems)
|
||||
|
||||
- **Gemfile**:
|
||||
|
||||
- 定义 gem 开发 / 测试阶段的依赖(如测试框架 `rspec`、打包工具等),类似前端的 `package.json`。
|
||||
- 通过 `bundle install` 安装依赖,依赖版本由 `Gemfile.lock`(自动生成)锁定
|
||||
|
||||
- **源代码目录:lib/ **
|
||||
|
||||
- gem 的核心功能代码存放目录,Ruby 会自动加载该目录下的文件
|
||||
|
||||
- `lib/cocoapods/hmap.rb`:gem 的主入口文件,定义了 `CocoaPods::Hmap` 模块的核心逻辑,是功能实现的主要载体(如与 CocoaPods 集成的逻辑、头文件映射相关功能等)
|
||||
- `lib/cocoapods/hmap/version.rb`: 单独存放版本号的文件,通常定义 `CocoaPods::Hmap::VERSION` 常量,便于统一管理版本(在 `gemspec`中会引用该常量)。
|
||||
|
||||
|
||||
|
||||
#### 3. 改造并 run 起来
|
||||
|
||||
- 修改 gemspec 文件中带有 url、uri 的字段,开发阶段可以修改为任何一个 url 字符串
|
||||
|
||||
- 修改自带的工程结构,比如 `cocoapods/hmap` 改为 `cocoapods-hmap`,将对应的文件也移动位置
|
||||
|
||||
- 我们预期的效果是:在终端输入 **pod hmap** 就可以将工程中的静态库 Header Search Path 传统查找模式改为 hmap 文件配置模式。所以需要的的步骤就是在 **bin 目录下创建 hmap.rb**,然后通过 **bin 目录下的代码调用 lib 目录下的 HMap.rb** 能力。
|
||||
|
||||
- 为此,需要:
|
||||
|
||||
- 在 lib 目录下创建 HMap.rb 文件
|
||||
|
||||
```ruby
|
||||
require_relative "version"
|
||||
|
||||
module CocoapodsHmap
|
||||
class HMap
|
||||
def initialize
|
||||
puts "Cocoapods HMap initialized"
|
||||
end
|
||||
|
||||
def self.run
|
||||
puts "Running Cocoapods HMap..."
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
- 在 bin 目录下创建 hmap.rb 文件
|
||||
|
||||
```ruby
|
||||
#!/usr/bin/env ruby
|
||||
require "bundler/setup"
|
||||
require "cocoapods-hmap/hmap"
|
||||
|
||||
# 打印携带的参数
|
||||
puts ARGV
|
||||
|
||||
CocoapodsHmap::HMap.run
|
||||
```
|
||||
|
||||
- 为了在 VSCode 中测试,需要在 Gemfile 中添加一行 **gem 'debug', '~> 1.9.0' # 调试用**
|
||||
|
||||
- 工程根目录创建 `.vscode` 文件夹,创建 launch.json 文件。内容如下:
|
||||
|
||||
```json
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "rdbg",
|
||||
"name": "Debug current file with rdbg",
|
||||
"request": "launch",
|
||||
"script": "${workspaceFolder}/bin/hmap",
|
||||
"args": ["hmap"], // pod 命令的参数
|
||||
"askParameters": true,
|
||||
"cwd": "${workspaceFolder}", // pod 执行命令的路径
|
||||
},
|
||||
{
|
||||
"type": "rdbg",
|
||||
"name": "Attach with rdbg",
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
VSCode 中运行效果如下:
|
||||
|
||||
<img src="./../assets/CocoapodsHMapV1.png" style="zoom:30%" />
|
||||
|
||||
#### 4. 如何将自定义的指令加入到 cocoapods 中
|
||||
|
||||
- 在 Command 目录下创建 `hmap` 文件,不带任何拓展名。rake 处理后,最后会变为 `/Users/unix_kernel/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/cocoapods-hmap-0.1.0/bin/hmap: Ruby script text executable, ASCII text`
|
||||
|
||||
- 在 lib 目录下,创建 `cocoapods-hmap` 文件夹。
|
||||
- 在 `cocoapods-hmap` 文件夹内创建 `command` 文件夹,在 `command` 文件夹内创建 `hmap.rb` 文件
|
||||
- class 继承自 Pod::Command,并实现相关方法。比如 initialize、validate!、run
|
||||
- 在 lib 目录下,创建 `cocoapods-plugin.rb` 文件
|
||||
- 暴露继承自 Pod::Command 的 hmap command
|
||||
|
||||
调试运行后的效果如下:
|
||||
|
||||
<img src="./../assets/CocoapodsHMapV2.png" style="zoom:30%" />
|
||||
|
||||
类名小写,和类文件关联起来
|
||||
|
||||
比如 install.rb 中,类名为 `class Install` ,内部会记录为 **{"install": "install.rb"}**
|
||||
|
||||
|
||||
|
||||
#### 5. 打包安装到本地
|
||||
|
||||
在终端项目目录下,执行指令 **rake install:local** ,主要用于**在本地构建并安装当前开发的 gem 包**,方便开发者进行本地测试和调试。
|
||||
|
||||
一开始有报错,如下图所示。按照提示修改 `cocoapods-hmap.gemspec` 中的配置,然后就可以成功安装了。然后输入 **gem list** 查看:
|
||||
|
||||
<img src="./../assets/CocoapodsHMapLocalInstall.png" style="zoom:30%" />
|
||||
|
||||
输入: **gem info cocoapods-hmap** 查看安装信息
|
||||
|
||||
<img src="./../assets/CocoapodsHMapInfo.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
就目前的功能进行测试:
|
||||
|
||||
| 条件 | 预期 | 结果 |
|
||||
| ----------------------------------- | --------------------------------------- | -------- |
|
||||
| 随便一个工程目录,没有 Podfile 文件 | 执行 `pod hmap` 会报错 | 符合预期 |
|
||||
| 存在 Podfile 文件的目录 | 正常执行 run 方法里面的逻辑(打印逻辑) | 符合预期 |
|
||||
|
||||
<img src="./../assets/CocoapodsHMapV2Test.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
#### 6. Hook 能力
|
||||
|
||||
##### 1. post_install
|
||||
|
||||
- 修改插件入口文件 (cocoapods_plugin.rb)
|
||||
|
||||
先在 `lib/cocoapods-plugin.rb` 中注册插件和对应的 hook 能力。利用 API:**Pod::HooksManager.register('cocoapods-hmap', :post_install)**,其文档说明如下:
|
||||
|
||||
>register(plugin_name, hook_name, &block)
|
||||
>
|
||||
>**Definitions**: [hooks_manager.rb](vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html)
|
||||
>
|
||||
>Registers a block for the hook with the given name.
|
||||
>
|
||||
>@param [String] plugin_name The name of the plugin the hook comes from.
|
||||
>
|
||||
>@param [Symbol] hook_name The name of the notification.
|
||||
>
|
||||
>@param [Proc] block The block.
|
||||
|
||||
```ruby
|
||||
Pod::HooksManager.register('cocoapods-hmap', :post_install) do |context, options|
|
||||
argv = CLAide::ARGV.new([]) # 创建一个空的参数数组
|
||||
command = Pod::Command::HMap.new(argv)
|
||||
command.run_post_install(context, options)
|
||||
end
|
||||
```
|
||||
|
||||
- 完善 HMap 命令类
|
||||
|
||||
修改 `lib/cocoapods-hmap/command/hmap.rb`,增加 `run_post_install` 方法
|
||||
|
||||
```ruby
|
||||
module Pod
|
||||
class Command
|
||||
class HMap < Command
|
||||
// ...
|
||||
|
||||
# Post-install 钩子执行的方法
|
||||
def run_post_install(context, options = {})
|
||||
puts "[Cocoapods hmap] Running HMap command in post_install hook..."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
- 测试配置
|
||||
|
||||
修改测试项目的 Podfile 文件,声明 **plugin 'cocoapods-hmap'**
|
||||
|
||||
```ruby
|
||||
platform :ios, '9.0'
|
||||
plugin 'cocoapods-hmap'
|
||||
|
||||
post_install do | installer |
|
||||
puts "Self defined post_install hook"
|
||||
end
|
||||
|
||||
target 'StaticLibConflictsDemo' do
|
||||
# Comment the next line if you don't want to use dynamic frameworks
|
||||
# AFNetworking 以静态库的形式被依赖
|
||||
pod 'AFNetworking'
|
||||
# 脚本化
|
||||
script_phase :name => 'Run Self-defined Script',
|
||||
:script => "echo 'This is a self-defined script phase'",
|
||||
:input_files => [],
|
||||
:execution_position => :after_compile
|
||||
end
|
||||
```
|
||||
|
||||
- 修改 `cocoapods-hmap` 工程的 VSCode 的 launch.json 文件
|
||||
|
||||
因为 cocoapods-hmap 工程和 iOS Pods 工程不在一个目录,所以可以在 args 的第二个参数设置为测试工程路径
|
||||
|
||||
```json
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "rdbg",
|
||||
"name": "Debug current file with rdbg",
|
||||
"request": "launch",
|
||||
// "script": "${workspaceFolder}/bin/hmap",
|
||||
"script": " /Users/unix_kernel/.rbenv/versions/3.2.2/bin/pod",
|
||||
"args": [
|
||||
"install",
|
||||
"--project-directory=${workspaceFolder}/../StaticLibConflictsDemo"
|
||||
], // pod 命令的参数
|
||||
"askParameters": true,
|
||||
"cwd": "${workspaceFolder}", // pod 执行命令的路径
|
||||
},
|
||||
{
|
||||
"type": "rdbg",
|
||||
"name": "Attach with rdbg",
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- 测试:存在2种方法
|
||||
|
||||
- 第一种:在 cocospods-hmap 工程中测试,如下图
|
||||
|
||||
<img src="./../assets/CocoapodsHMapPostInstall.png" style="zoom:30%" />
|
||||
|
||||
- 第二种:
|
||||
|
||||
- 在终端 cocoapods-hmap 目录下执行 **rake install:local** ,将插件安装到本地
|
||||
- 然后切换到 iOS 被测工程目录下,执行 `pod install`
|
||||
|
||||
效果如下:
|
||||
|
||||
<img src="./../assets/CocoapodsHMapPostInstall2.png" style="zoom:30%" />
|
||||
|
||||
|
||||
|
||||
##### 2. pre_install
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 2. CocoaPods 插件系统设计
|
||||
|
||||
CocoaPods 通过严格的目录结构约定来加载插件:
|
||||
|
||||
```shell
|
||||
lib/
|
||||
├── cocoapods-plugin.rb # 插件主入口文件(必需)
|
||||
└── cocoapods-hmap/ # 插件命名空间目录
|
||||
└── command/ # 命令目录
|
||||
└── hmap.rb # 命令实现文件(必需)
|
||||
```
|
||||
|
||||
#### 1. 自动加载机制
|
||||
|
||||
CocoaPods 启动时会自动执行以下操作:
|
||||
|
||||
1. 扫描已安装的 gem
|
||||
2. 查找所有以 `cocoapods-` 为前缀的 gem
|
||||
3. 加载这些 gem 中的 `lib/cocoapods-plugin.rb` 文件
|
||||
4. 通过该文件加载插件功能
|
||||
|
||||
#### 2. 关键文件
|
||||
|
||||
- 插件入口文件 (`lib/cocoapods-plugin.rb`)
|
||||
|
||||
```ruby
|
||||
require 'cocoapods-hmap/command/hmap'
|
||||
```
|
||||
|
||||
- 命令实现文件 (lib/cocoapods-hmap/command/hmap.rb)
|
||||
|
||||
-
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
- block 最后一行是返回值,不需要指定 return
|
||||
- ruby 脚本语言在编写好代码后也想像其他 GPL 语言一样断点可以,需要安装依赖和相应的代码调整
|
||||
- `gem install pry`
|
||||
- 文件引入 `require 'pry'`
|
||||
- 需要 debug 的地方加上 `binding.pry`
|
||||
@@ -1,8 +1,8 @@
|
||||
## 带你打造一套 APM 监控系统
|
||||
|
||||
> APM 是 Application Performance Monitoring 的缩写,监视和管理软件应用程序的性能和可用性。应用性能管理对一个应用的持续稳定运行至关重要。所以这篇文章就从一个 iOS App 的性能管理的纬度谈谈如何精确监控以及数据如何上报等技术点
|
||||
> APM 是 Application Performance Monitoring 的缩写,监视和管理软件应用程序的性能和可用性。应用性能管理对一个应用的持续稳定运行至关重要。所以这篇文章就从一个 iOS App 的性能管理的维度谈谈如何精确监控以及数据如何上报等技术点
|
||||
|
||||
App 的性能问题是影响用户体验的重要因素之一。性能问题主要包含:Crash、网络请求错误或者超时、UI 响应速度慢、主线程卡顿、CPU 和内存使用率高、耗电量大等等。大多数的问题原因在于开发者错误地使用了线程锁、系统函数、编程规范问题、数据结构等等。解决问题的关键在于尽早的发现和定位问题。
|
||||
App 的性能问题是影响用户体验的重要因素之一。性能问题主要包含:卡顿、Crash、网络请求错误或者超时、UI 响应速度慢、主线程卡顿、CPU 和内存使用率高、耗电量大、Weex/RN/Flutter 页面白屏等等。大多数的问题原因在于开发者错误地使用了线程锁、系统函数、编程规范问题、数据结构等等。解决问题的关键在于尽早的发现和定位问题。
|
||||
|
||||
本篇文章着重总结了 APM 的原因以及如何收集数据。APM 数据收集后结合数据上报机制,按照一定策略上传数据到服务端。服务端消费这些信息并产出报告。请结合[姊妹篇](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md), 总结了如何打造一款灵活可配置、功能强大的数据上报组件。
|
||||
|
||||
@@ -8094,6 +8094,52 @@ runZoned<Future<Null>>(() async {
|
||||
|
||||
|
||||
## 十、子线程 UI 监控
|
||||
### 为什么不能在子线程操作 UI
|
||||
UI 必须在主线程(UI 线程)执行,核心原因是 **UI 框架的单线程模型**设计,为了保证 UI 操作的线程安全、状态一致性和渲染效率,几乎所有的主流 UI 框架(Android、iOS、Web、Windows、Web 前端、Flutter、Weex、RN)都限制:只有主线程才可以访问和修改 UI 控件。
|
||||
|
||||
#### 核心原因:解决“线程安全”与“状态一致性”问题
|
||||
UI 控件(如按钮、文本框)是**共享资源**,且设计时**没有内置线程同步机制(比如锁)**,如果允许多线程直接操作 UI,会引发2个致命问题:
|
||||
|
||||
##### 1. 竞态条件(Race Condition):UI 状态错乱
|
||||
多线程同时修改同一个 UI 控件的属性,会导致 UI 状态“冲突”,比如:
|
||||
- 线程 A 设置 UILabel 的文本为“加载中,正在获取服务端数据...”
|
||||
- 线程 B,设置 UILabel 文字颜色为蓝色
|
||||
- 线程 C,设置 UILabel 为隐藏(此时 UILabel 的文字可能还没更改完,状态不对,但立马被设置为隐藏了)
|
||||
最终出现了 UILabel 的显示错乱,UI 控件卡死,界面闪烁问题
|
||||
|
||||
##### 2. 渲染流程混乱:绘制结果不可预期
|
||||
UI 渲染是个连续的流水线:先计算控件布局(Measure/Layout) -> 绘制控件(Draw) -> 刷新屏幕(Compose)。这个流程需要顺序执行、状态稳定,如果多线程介入:
|
||||
- 线程 A 正在计算布局(比如调整 UILabel 的位置)
|
||||
- 线程 B 突然修改 Frame 大小,导致布局计算结果失效
|
||||
- 最终绘制出界面可能是“按钮位置偏移”、“控件重叠”等异常
|
||||
|
||||
#### 为什么不设计线程安全的 UI 控件?
|
||||
##### 1. 性能暴跌:UI 操作便卡顿
|
||||
UI 操作是非常高频的场景(比如列表在滑动的时候,每秒可能刷新几十次),而加锁会导致性能问题(比如线程 A 给 UILabel 加了锁,线程 B 想要修改文字大小,此时必须等待线程 A 释放锁)。频繁的锁竞争会让 UI 的变化延迟响应,甚至出现“滑动掉帧”问题,或者“点击无响应”-这违背了 UI 对流畅性的核心要求
|
||||
##### 2. 复杂度爆炸:容易引发死锁
|
||||
UI 控件存在依赖关系(比如父子组件、UI 组件树),多线程操作时,可能出现“锁顺序错乱”
|
||||
- 线程 A 锁父控件 -> 再尝试锁子控件
|
||||
- 线程 B 锁子控件 -> 再尝试锁父控件
|
||||
2者相互等待出现死锁
|
||||
|
||||
因此,UI 框架必须满足“牺牲多线程灵活性”,以确保“单线程的简单性、安全性和高效性”,索性一刀切,**所有的 UI 操作必须在主线程上执行**,从根源上避免不符合预期的行为出现
|
||||
|
||||
##### GUI 平台的单线程模型
|
||||
所有主流平台、框架都遵循这一原则,且会主动拦截“子线程操作 UI” 的行为:
|
||||
1. Android
|
||||
- 主线程(UI 线程)通过 Looper 循环处理 MessageQueue 的 UI 消息
|
||||
- 子线程操作 UI 会直接弹出 `CalledFromWrongThreadException`(只有创建视图层次的原始线程才可以触摸其视图)
|
||||
- 必须通过 Handler、runOnUiThread、Croutine(Dispatch.Main)等方式更新 UI
|
||||
2. iOS
|
||||
- 主线程(UI 线程)通过绑定主 RunLoop,负责处理 UI 事件(点击、触摸、滑动)和渲染
|
||||
- 子线程操作 UI 可能导致界面异常(比如 UILabel 文本不更新)、甚至 crash
|
||||
- 必须通过 dispatch.async 来向主队列派发可以用于主线程执行的任务
|
||||
3. Web 前端
|
||||
- 浏览器的 DOM 操作是单线程的(主线程负责 DOM 渲染、事件处理)
|
||||
- 子线程(如 Web Worker)无法直接操作 DOM,必须通过 postMessage 通知主线程操作 UI 更新
|
||||
|
||||
总结:UI 必须在主线程上更新是因为:UI 控件没有设计线程同步机制(比如加锁),所以框架为了保证线程安全、状态一致、渲染高效,一刀切直接采用了单线程模型。
|
||||
|
||||
|
||||
### 1. 背景介绍
|
||||
|
||||
@@ -8263,27 +8309,8 @@ runZoned<Future<Null>>(() async {
|
||||
|
||||
## 十四、未来规划
|
||||
|
||||
- 监控能力继续完善
|
||||
|
||||
- 技术持续研究,提升监控准确度、案发第一现场数据,便于排查问题
|
||||
|
||||
- 监控能力继续完善、提升监控准确度
|
||||
- APM 新方向的研究。UI 自动化结合 iOS Instrucments Server 中的性能数据(DTXMessage 通信)
|
||||
|
||||
- 开源 SDK + 桌面端 App 查看性能数据(产品侧)
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [iOS 保持界面流畅的技巧](https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/)
|
||||
- [Call Stack](https://en.wikipedia.org/wiki/Call_stack)
|
||||
- [iOS 启动时间优化](https://www.zoomfeng.com/blog/launch-time.html)
|
||||
- [WWDC2019 之启动时间与 Dyld3](https://www.zoomfeng.com/blog/launch-optimize-from-wwdc2019.html)
|
||||
- [Apple-libmalloc](https://opensource.apple.com/tarballs/libmalloc/)
|
||||
- [Apple-XNU](https://opensource.apple.com/tarballs/xnu/)
|
||||
- [OOM 探究:XNU 内存状态管理](https://www.jianshu.com/p/4458700a8ba8)
|
||||
- [Reducing FOOMs in the Facebook iOS app](https://engineering.fb.com/ios/reducing-fooms-in-the-facebook-ios-app/)
|
||||
- [iOS 内存 abort(Jetsam) 原理探究](https://satanwoo.github.io/2017/10/18/abort/)
|
||||
- [iOS 微信内存监控](https://wetest.qq.com/lab/view/367.html?from=coop_gad)
|
||||
- [iOS 堆栈信息解析(函数地址与符号关联)](https://www.jianshu.com/p/df5b08330afd)
|
||||
- [Apple-CFNetwork Programming Guide](https://developer.apple.com/library/archive/documentation/Networking/Conceptual/CFNetwork/Introduction/Introduction.html#//apple_ref/doc/uid/TP30001132-CH1-DontLinkElementID_30)
|
||||
- [MDN-HTTP Messages](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages)
|
||||
- [DWARF 和符号化](https://junyixie.github.io/2018/09/30/dwarf和符号化/)
|
||||
- 产品侧:开源 SDK + mPaaS 平台(或者 Electron 写一款 桌面端 App 查看性能数据)
|
||||
- AI 赋能:根据特征数据做到智能化归因
|
||||
- 报警平台已具备波动报警、业务域 Owner 报警策略可配置。未来需利用 AI 能力做更多畅想
|
||||
|
||||
@@ -1177,6 +1177,45 @@ SPEC_END
|
||||
|
||||
## 五、UI 测试
|
||||
|
||||
### 概念
|
||||
UI 测试:属于端到端测试,是从应用程序启动到结束的测试过程。完全按照用户与应用程序交互的方式来复制与应用程序的交互。比但愿测试慢得多,运行起来更消耗资源。
|
||||
|
||||
### 测试原则
|
||||
|
||||
FIRST
|
||||
- Fast:测试模块应该是快速高效的
|
||||
- Independent/Isolated:测试模块应该是独立、互相不影响的
|
||||
- Repeatable:测试实例应该是可以重复使用的,测试结果应该是相同的
|
||||
- Self-validating:测试应完全自动化。输出结果要么成功、要么失败
|
||||
- Timely:理想情况下,应该在编写要测试的生产代码之前编写测试(测试驱动开发)
|
||||
|
||||
### 需要测试什么?
|
||||
- 视觉表现验证
|
||||
- 元素渲染:控件尺寸、颜色、字体、图标资源(accessibilityIdentifier 定位)
|
||||
- 布局规则:动态布局(Auto Layout 约束断裂检测)、横竖屏适配、多语言截断
|
||||
- 动效完整性:转场动画时长、交互反馈(如按钮点击态)
|
||||
|
||||
- 交互行为验证
|
||||
|
||||
| 交互类型 | 测试要点 | 工具示例 |
|
||||
| :----------- | :----------------------------------------------- | :------------------------------------- |
|
||||
| **手势操作** | 滑动/长按/捏合等触发事件 | `XCUITest: swipeUp()` |
|
||||
| **表单输入** | 键盘类型切换、输入校验(正则)、自动填充 | `typeText("test@email.com")` |
|
||||
| **导航流** | 页面跳转栈深度、返回逻辑(物理返回 vs 程序返回) | `navigationBars.buttons["Back"].tap()` |
|
||||
| **异步状态** | 加载中/空状态/错误页的显示与隐藏 | `waitForExistence(timeout: 5)` |
|
||||
|
||||
- 数据驱动验证
|
||||
- API 数据映射:Mock 不同 API 响应(200/404/500),检查 UI 渲染正确性
|
||||
- 本地数据同步:Core Data/Realm 更新后 UI 即时刷新
|
||||
- 动态内容:富文本(含超链接)、图片懒加载、视频播放器状态
|
||||
- 边界场景验证(Edge Cases)
|
||||
- 设备兼容:从 iPhone SE 到 iPad Pro 的适配
|
||||
- 系统版本:iOS 14~17 的关键行为差异(如权限弹窗样式)
|
||||
- 极端操作:快速连续点击、低内存告警恢复
|
||||
- 无障碍支持:VoiceOver 焦点顺序、Dynamic Type 超大字体布局
|
||||
|
||||
|
||||
|
||||
### 基础使用
|
||||
|
||||
上面文章大篇幅的讲了单元测试相关的话题,单元测试十分适合代码质量、逻辑、网络等内容的测试,但是针对最终产物 App 来说单元测试就不太适合了,如果测试 UI 界面的正确性、功能是否正确显然就不太适合了。Apple 在 Xcode 7 开始推出的 `UI Testing` 就是苹果自己的 UI 测试框架。
|
||||
@@ -1339,16 +1378,194 @@ int main(int argc, char * argv[]) {
|
||||
|
||||
|
||||
|
||||
### 探索
|
||||
### 单元测试的原理窥探
|
||||
|
||||
开发的单元测试代码,运行的背后也是一个可执行文件。查看内部,可以发现一堆和测试相关的 framework。
|
||||
|
||||
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/XcodeUnitTestDyanimcFramework.png" style="zoom:30%" />
|
||||
|
||||
思考:一个工程中,只要写好了测试代码,是不是加载这几个动态库就可以运行测试用例了?
|
||||
思考:一个非 UI 测试工程(正常的 App 工程),是不是加载这几个测试相关的动态库,写好测试代码,就可以运行测试用例了?
|
||||
|
||||
细节不贴了。Demo App 引入 `XCTest.framework` 后业务代码里即可引入 `#import <XCTest/XCTest.h>` 然后就可以编写测试代码了。
|
||||
|
||||
写法1:
|
||||
|
||||
```objective-c
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||
// 测试管理集
|
||||
XCTestSuite *suite = [XCTestSuite defaultTestSuite];
|
||||
// 初始化 TestCase
|
||||
LoginUITests *loginTest = [LoginUITests testCaseWithSelector:@selector(testDidClickLoginAction)];
|
||||
|
||||
// 添加测试用例到当前 suite
|
||||
[suite addTest:loginTest];
|
||||
|
||||
// 遍历并运行测试用例
|
||||
for (XCTest *test in suite.tests) {
|
||||
[test runTest];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
写法2:
|
||||
|
||||
```objective-c
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||
// 测试管理集
|
||||
XCTestSuite *suite = [XCTestSuite testSuiteForTestCaseClass:LoginUITests.class];
|
||||
// 初始化 TestCase
|
||||
LoginUITests *loginTest = [LoginUITests new];
|
||||
|
||||
// 添加测试用例到当前 suite
|
||||
[suite addTest:loginTest];
|
||||
|
||||
// 遍历并运行测试用例
|
||||
for (XCTest *test in suite.tests) {
|
||||
[test runTest];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 执行时机
|
||||
|
||||
这种情况下,整体流程为:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[测试套件启动] --> B[创建测试类实例]
|
||||
B --> C1[执行 testMethod1]
|
||||
C1 --> D1[调用 setUp]
|
||||
D1 --> E1[执行 testMethod1 本体]
|
||||
E1 --> F1[调用 tearDown]
|
||||
|
||||
B --> C2[执行 testMethod2]
|
||||
C2 --> D2[调用 setUp]
|
||||
D2 --> E2[执行 testMethod2 本体]
|
||||
E2 --> F2[调用 tearDown]
|
||||
|
||||
B --> C3[执行 testMethod3]
|
||||
C3 --> D3[调用 setUp]
|
||||
D3 --> E3[执行 testMethod3 本体]
|
||||
E3 --> F3[调用 tearDown]
|
||||
```
|
||||
|
||||
分析:
|
||||
|
||||
- 当有多个测试方法时,XCTest 会为**每个测试方法创建单独的类实例**
|
||||
|
||||
- 每个测试方法执行时都会触发完整的生命周期
|
||||
|
||||
```objective-c
|
||||
// 伪代码展示 XCTest 内部执行流程
|
||||
for (XCTest *test in allTests) {
|
||||
[test invokeTest]; // 实际执行入口
|
||||
}
|
||||
|
||||
// invokeTest 内部实现:
|
||||
- (void)invokeTest {
|
||||
[self setUp]; // 每次测试前调用
|
||||
[self performTest]; // 执行测试方法本体
|
||||
[self tearDown]; // 每次测试后调用
|
||||
}
|
||||
```
|
||||
|
||||
- 三个测试方法的执行示例
|
||||
|
||||
测试代码:
|
||||
|
||||
```objective-c
|
||||
- (void)testValidLogin { ... }
|
||||
- (void)testInvalidPassword { ... }
|
||||
- (void)testNetworkErrorHandling { ... }
|
||||
```
|
||||
|
||||
实际执行顺序
|
||||
|
||||
```objective-c
|
||||
// 测试1
|
||||
[LoginUITests setUp];
|
||||
[LoginUITests testValidLogin];
|
||||
[LoginUITests tearDown];
|
||||
|
||||
// 测试2
|
||||
[LoginUITests setUp]; // 全新状态!
|
||||
[LoginUITests testInvalidPassword];
|
||||
[LoginUITests tearDown];
|
||||
|
||||
// 测试3
|
||||
[LoginUITests setUp]; // 再次重置状态
|
||||
[LoginUITests testNetworkErrorHandling];
|
||||
[LoginUITests tearDown];
|
||||
```
|
||||
|
||||
思考:为什么需要每次重置?
|
||||
|
||||
1. **测试隔离原则**
|
||||
- 防止测试间的状态污染
|
||||
- 确保每个测试都是独立可重复的
|
||||
2. **资源管理**
|
||||
- 每次 `tearDown` 释放测试占用的资源
|
||||
- 避免内存泄漏累积
|
||||
3. **环境一致性**
|
||||
- `setUp` 确保每次测试初始条件相同
|
||||
- 不受前次测试副作用影响
|
||||
|
||||
QA:单独执行每个测试方法,都会走 setup 和 teardown。按了快捷键 command + u,运行所有的测试 case,会执行几次 setup?比如 Login 有3个测试 case。点击后执行流程是什么样的?
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant X as XCTestRunner
|
||||
participant C as LoginUITests Class
|
||||
participant I1 as 实例1 (testA)
|
||||
participant I2 as 实例2 (testB)
|
||||
participant I3 as 实例3 (testC)
|
||||
|
||||
X->>C: 调用 +[LoginUITests setUp] (类方法)
|
||||
activate C
|
||||
|
||||
X->>I1: 创建实例1 (testA)
|
||||
activate I1
|
||||
I1->>I1: -setUp (实例方法)
|
||||
I1->>I1: -testA (测试方法)
|
||||
I1->>I1: -tearDown (实例方法)
|
||||
deactivate I1
|
||||
|
||||
X->>I2: 创建实例2 (testB)
|
||||
activate I2
|
||||
I2->>I2: -setUp (实例方法)
|
||||
I2->>I2: -testB (测试方法)
|
||||
I2->>I2: -tearDown (实例方法)
|
||||
deactivate I2
|
||||
|
||||
X->>I3: 创建实例3 (testC)
|
||||
activate I3
|
||||
I3->>I3: -setUp (实例方法)
|
||||
I3->>I3: -testC (测试方法)
|
||||
I3->>I3: -tearDown (实例方法)
|
||||
deactivate I3
|
||||
|
||||
X->>C: 调用 +[LoginUITests tearDown] (类方法)
|
||||
deactivate C
|
||||
```
|
||||
|
||||
分析:
|
||||
|
||||
- 类级别初始化(只执行一次)。`+[LoginUITests setUp]` 类方法
|
||||
- 在**所有测试开始前**执行一次
|
||||
- 适合做全局初始化(如启动模拟服务器)
|
||||
- 执行频率:1次/测试类
|
||||
- 每个测试方法的独立执行。对于每个测试方法(testA, testB, testC):
|
||||
- 创建**新的测试类实例**
|
||||
- **-setUp** 实例方法(每个测试方法前执行)
|
||||
- 执行测试方法(如 **-testA**)
|
||||
- **-tearDown** 实例方法(每个测试方法后执行)
|
||||
- 类级别清理(只执行一次)。`+[LoginUITests tearDown]` 类方法
|
||||
- 在**所有测试结束后**执行一次
|
||||
- 适合做全局清理(如关闭模拟服务器)
|
||||
- 执行频率:1次/测试类
|
||||
|
||||
|
||||
|
||||
## 六、精准测试
|
||||
@@ -1369,7 +1586,7 @@ int main(int argc, char * argv[]) {
|
||||
|
||||
下面是之前在有赞,开发完精准测试系统后,落地到一个业务项目中取得的价值,帮助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">
|
||||
|
||||
精准测试助力业务,质量更加稳定。
|
||||
|
||||
@@ -1394,3 +1611,5 @@ UITesting 还是建议在核心逻辑且长时间没有改动的情况下去做
|
||||

|
||||
|
||||
WWDC 这张图也很清楚,UI 其实需要的占比较小,还是要靠单测驱动。
|
||||
|
||||
##
|
||||
@@ -11,9 +11,9 @@
|
||||
|
||||
|
||||
|
||||
## block 本质探索
|
||||
## 一、block 本质探索
|
||||
|
||||
### 实验探索
|
||||
### 1. 实验探索
|
||||
|
||||
Demo
|
||||
|
||||
@@ -244,7 +244,7 @@ struct __ViewController__viewDidLoad_block_desc_0 {
|
||||
|
||||
|
||||
|
||||
### 结论
|
||||
### 2. 结论
|
||||
|
||||
通过探索发现:
|
||||
|
||||
@@ -260,9 +260,9 @@ struct __ViewController__viewDidLoad_block_desc_0 {
|
||||
|
||||
|
||||
|
||||
## block 变量捕获
|
||||
## 二、block 变量捕获
|
||||
|
||||
### auto 变量捕获
|
||||
### 1. auto 变量捕获
|
||||
|
||||
> 在 C 和 Objective-C 编程语言中,`auto` 关键字用于声明自动存储期的变量。自动存储期的变量会在定义它们的块(block)或作用域(scope)中自动创建,并在退出该作用域时自动销毁。这是变量存储期的默认行为,因此 `auto` 关键字实际上是可选的,但有时候为了清晰起见,开发者可能会显式使用它。
|
||||
|
||||
@@ -322,7 +322,7 @@ c++ 中,在函数内部定义的变量,默认用 **auto** 修饰,叫做自
|
||||
|
||||
|
||||
|
||||
### static 变量捕获
|
||||
### 2. static 变量捕获
|
||||
|
||||
```objective-c
|
||||
auto NSInteger age = 27;
|
||||
@@ -354,7 +354,7 @@ printInfoBlock();
|
||||
|
||||
|
||||
|
||||
### 全局变量捕获
|
||||
### 3. 全局变量捕获
|
||||
|
||||
```objective-c
|
||||
NSInteger age = 27;
|
||||
@@ -405,7 +405,7 @@ QA:为什么局部变量存在捕获,全局变量不需要捕获?
|
||||
|
||||
|
||||
|
||||
### 变量捕获总结
|
||||
### 4. 变量捕获总结
|
||||
|
||||
block 截获变量可以分为:
|
||||
|
||||
@@ -576,9 +576,9 @@ static void __Person__testBlockCapture_block_func_0(struct __Person__testBlockCa
|
||||
|
||||
|
||||
|
||||
## block 类型
|
||||
## 三、block 类型
|
||||
|
||||
### 类型划分
|
||||
### 1. 类型划分
|
||||
|
||||
我们知道 block 可以看成是一个 oc 对象,所以它有类型,写个 Demo1 验证下
|
||||
|
||||
@@ -619,7 +619,7 @@ block 的类型可以通过 isa 或者 class 方法查看,最终都是继承
|
||||
|
||||
|
||||
|
||||
### 如何判断 block 属于什么类型
|
||||
### 2. 如何判断 block 属于什么类型
|
||||
|
||||
Demo:
|
||||
|
||||
@@ -651,7 +651,7 @@ Demo 也同时发现,当对 `__NSGlobalBlock__` 调用 copy ,不会变为 `
|
||||
|
||||
|
||||
|
||||
### 总结
|
||||
### 3. 总结
|
||||
|
||||
| block 类型 | 环境 |
|
||||
| ------------------- | ------------------------------ |
|
||||
@@ -691,9 +691,9 @@ int main(int argc, const char * argv[]) {
|
||||
|
||||
|
||||
|
||||
## 内存管理
|
||||
## 四、内存管理
|
||||
|
||||
### ARC 针对 block 的优化
|
||||
### 1. ARC 针对 block 的优化
|
||||
|
||||
#### 1. block 作为函数返回值,并且捕获了 auto 变量
|
||||
|
||||
@@ -905,7 +905,7 @@ static struct __main_block_desc_0 {
|
||||
|
||||
|
||||
|
||||
### block 的 copy、dispose
|
||||
### 2. block 的 copy、dispose
|
||||
|
||||
```c++
|
||||
static struct __main_block_desc_0 {
|
||||
@@ -1004,9 +1004,319 @@ NSLog(@"-touchesBegan:withEvent:");
|
||||
2024-08-13 20:59:54.313367+0800 BlockExplore[29889:968688] (null)
|
||||
```
|
||||
|
||||
### block 如何修改变量
|
||||
### 3. _Block_release 和 _Block_copy
|
||||
|
||||
#### __block 修饰基本数据类型
|
||||
Demo1
|
||||
|
||||
```objective-c
|
||||
void test(void) {
|
||||
|
||||
int a = 0;
|
||||
void(^__weak weakBlock)(void) = nil;
|
||||
{
|
||||
int b = 2;
|
||||
void(^ __weak weakInnerBlock)(void) = ^{
|
||||
NSLog(@"%d", b);
|
||||
};
|
||||
a = b;
|
||||
weakBlock = weakInnerBlock;
|
||||
}
|
||||
weakBlock();
|
||||
}
|
||||
```
|
||||
|
||||
- 定义了一个局部变量 weakBlock
|
||||
|
||||
- 在 `{}` 内定义了一个栈上的 block **__NSStackBlock__** ,block 内访问了定义在 block 外部的 b
|
||||
|
||||
- 将 weakInnerBlock 赋值给 weakBlock
|
||||
|
||||
- 出了 `{}`,也意味着栈上的 block 作用域结束了。block 会调用 block_release 方法
|
||||
|
||||
- 由于出了局部作用域,栈上的 block 被释放了。所以在 `{}` 外调用 weakBlock 则存在野指针风险。编译器也会报警告:`Assigning block literal to a weak variable; object will be released after assignment` 。但可能不崩溃:栈内存虽回收但未被新数据覆盖
|
||||
|
||||
|
||||
|
||||
我们来看看 **block_release** 源码
|
||||
|
||||
```c++
|
||||
void
|
||||
_Block_release(const void *src)
|
||||
{
|
||||
struct StackBlockClass *self = (struct StackBlockClass *)src;
|
||||
extern const void _NSConcreteStackBlock;
|
||||
|
||||
if (self->isa == &_NSConcreteStackBlock // 必须是栈块类型
|
||||
// A Global block doesn't need to be released
|
||||
&& self->flags & BLOCK_HAS_DESCRIPTOR // 必须有描述符结构
|
||||
// Should always be true...
|
||||
&& self->reserved > 0) // 引用计数大于0
|
||||
// If false, then it's not allocated on the heap, we won't release auto memory !
|
||||
{
|
||||
self->reserved--;
|
||||
if (self->reserved == 0)
|
||||
{
|
||||
if (self->flags & BLOCK_HAS_COPY_DISPOSE)
|
||||
self->descriptor->dispose_helper(self);
|
||||
free(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `StackBlockClass` 表示栈 block 结构,包含:
|
||||
- isa:指向块类型的指针(栈块为 `__NSConcreteStackBlock`)
|
||||
- flag: 标志位( BLOCK_HAS_DESCRIPTOR 表示有描述符)
|
||||
- reserved:引用计数器
|
||||
- descriptor:包含析构函数 dispose_helper。当 block 捕获了 OC、C++ 对象后,编译器会自动设置此标志
|
||||
- 释放条件:
|
||||
- 排除全局 block 和堆 block:全局block `__NSConcreteGlobalBlock` 和堆上的 block 无需释放。
|
||||
- **栈块**:
|
||||
存储在栈内存中,生命周期与函数作用域绑定。
|
||||
**需要手动管理**:通过 `_Block_copy()` 复制到堆时,需用 `_Block_release()` 平衡引用计数。
|
||||
- **堆块**:
|
||||
由 `_Block_copy()` 动态分配在堆内存,受 ARC 管理。
|
||||
**自动管理**:ARC 会自动插入 `objc_release()` 调用,使用标准 Objective-C 对象释放机制。
|
||||
- 有效性检验:确保块结构完整且引用计数有效
|
||||
- 引用计数管理:
|
||||
- `self->reserved--` 引用计数减 1
|
||||
- 引用计数归0的时候,调用 dispose_helper 方法。释放 block 捕获的对象、内存
|
||||
- 释放 block 结构体内存
|
||||
- 一般来说 block 由栈管理,但是被 copy、strong 等强引用作为属性或者参数,则会调用 **_Block_copy** 拷贝到堆上。本函数管理堆化后栈 block 的引用计数
|
||||
|
||||
|
||||
|
||||
Demo2
|
||||
|
||||
```c++
|
||||
void test2(void) {
|
||||
|
||||
int a = 0;
|
||||
void(^__weak weakBlock)(void) = nil;
|
||||
{
|
||||
int b = 2;
|
||||
// 栈 block 赋值给一个 strong或copy 修饰的强引用变量,则会调用 _Block_copy 方法拷贝到堆上,变成堆 block
|
||||
void(^ __strong strongInnerBlock)(void) = ^{
|
||||
NSLog(@"%d", b);
|
||||
};
|
||||
a = b;
|
||||
// 结构体赋值,也就是此时 a 是一个堆上的 block
|
||||
weakBlock = strongInnerBlock;
|
||||
} // 离开作用域,堆上的 block 会自动调用 _Block_release 方法,内部会 free 掉,此时再去调用释放的内存,系统机制会为已经释放的内存填充 0xDEADBEEF 标记,MMU 也会触发缺页异常,发送 crash
|
||||
weakBlock();
|
||||
}
|
||||
```
|
||||
|
||||
现象:上面的代码会稳定 crash。报错:`Thread 1: EXC_BAD_ACCESS (code=1, address=0x10)`
|
||||
|
||||
分析:
|
||||
|
||||
- 在 `{}` 内定义初始化了一个栈 block
|
||||
- 赋值给一个由强指针指向的 strongInnerBlock 变量时,自动触发 `_Block_copy` 方法,复制到堆上,变为堆 block **__NSConcreteMallocBlock**
|
||||
- 然后通过结构体赋值的方式,赋值给 `weakBlock`
|
||||
- 要出 `{}` 作用域时,则会调用 `_block_release` 方法。堆块的引用计数清零。释放后,堆内存表记为不可访问
|
||||
- 此时调用 `weakBlock()` 则会 crash。访问已经释放的堆内存。系统在释放的堆内存上添加保护(如 `EXC_BAD_ACCESS`)
|
||||
|
||||
test 和 test2 的本质区别
|
||||
|
||||
| 函数 | 内存类型 | 内存回收机制 | 崩溃原因 |
|
||||
| :------ | :------- | :---------------------- | :--------------- |
|
||||
| `test` | 栈内存 | 系统自动回收(无保护) | 内存可能未被覆盖 |
|
||||
| `test2` | 堆内存 | `free()` + 内存保护标记 | 强制触发访问异常 |
|
||||
|
||||
|
||||
|
||||
Demo3
|
||||
|
||||
<img src="./../assets/VisitReleasedStackBlockWillCrash.png" style="zoom:30%" />
|
||||
|
||||
为什么上面的代码会 crash?
|
||||
|
||||
- weakBlock 是一个栈 block(__NSConcreteStackBlock)
|
||||
- GCD 代码以 block 的形式将延迟任务添加到主队列中
|
||||
- `dispatch_block_t` 内部捕获了 block,但捕获的是原始指针,未复制 block
|
||||
- GCD 不会阻塞,函数会结束,同时栈 block 会被回收,栈帧销毁。此时 `dispatch_block_t` 内持有其悬挂指针。
|
||||
- 3s 后开始执行 dispatch_block_t 所指向的 weakBlock
|
||||
- 发现 weakBlock 所指向的栈内存被回收,调用无效指针导致 **EXC_BAD_ACCESS** 崩溃(访问野指针)。
|
||||
|
||||
|
||||
|
||||
解决方案:
|
||||
|
||||
1. 将栈 block 改为堆 block。block 修饰改为 strong
|
||||
|
||||
````objective-c
|
||||
void test3(void) {
|
||||
int a = 10;
|
||||
void(^__strong block)(void) = ^ {
|
||||
NSLog(@"%d", a);
|
||||
};
|
||||
|
||||
dispatch_block_t dispatch_block = ^{
|
||||
block();
|
||||
};
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), dispatch_block);
|
||||
}
|
||||
````
|
||||
|
||||
2. 复制 block 到堆
|
||||
|
||||
```objective-c
|
||||
void test3(void) {
|
||||
int a = 10;
|
||||
void(^__weak weakBlock)(void) = ^ {
|
||||
NSLog(@"%d", a);
|
||||
};
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), [weakBlock copy]);
|
||||
}
|
||||
```
|
||||
|
||||
3. 栈帧不要回收。开启一个 RunLoop 晚退出3秒
|
||||
|
||||
```objective-c
|
||||
void test3(void) {
|
||||
int a = 10;
|
||||
void(^__weak weakBlock)(void) = ^ {
|
||||
NSLog(@"%d", a);
|
||||
};
|
||||
|
||||
dispatch_block_t dispatch_block = ^{
|
||||
weakBlock();
|
||||
};
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), dispatch_block);
|
||||
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
Demo4
|
||||
|
||||
<img src="./../assets/BlockAssignIsValueAssign.png" style="zoom:30%" />
|
||||
|
||||
分析:
|
||||
|
||||
- block 本质就是结构体
|
||||
|
||||
- `void (^__strong strongBlock)(void) = weakBlock;` 看上去左侧是一个 `__strong` 修饰,其实就是结构体,所以不会像对象一样,底层调用 setter 方法。所以 block 的赋值本就是按照结构体的成员变量一个个字段简单赋值而已。
|
||||
|
||||
```c++
|
||||
struct __block_impl {
|
||||
void *isa; // 指向 Block 类 (栈上)
|
||||
int flags; // 标志位 (栈上)
|
||||
int reserved; // 保留字段 (栈上)
|
||||
void (*invoke)(void); // 函数指针 (代码段)
|
||||
struct __block_descriptor *descriptor; // 描述符指针 (栈上)
|
||||
};
|
||||
|
||||
struct __block_descriptor {
|
||||
unsigned long reserved; // 保留字段 (栈上)
|
||||
unsigned long size; // Block 大小 (栈上)
|
||||
void (*copy)(void); // copy 辅助函数 (代码段)
|
||||
void (*dispose)(void); // dispose 辅助函数 (代码段)
|
||||
};
|
||||
```
|
||||
|
||||
- 当把结构体 block 的 invoke 设为 nil,由于是简单赋值,所以原来的 weakblock 的 invoke 也为 nil
|
||||
|
||||
- 所以 strongblock 的 invoke 也为 nil,所以执行 block 底层就是先判断 block 的 isa、然后根据 invoke 指针,找到代码段的函数去执行。此时已经为 nil,再去执行就会 crash
|
||||
|
||||
解决方案:将 weakblock copy 一下,即 `void (^__strong strongBlock)(void) = [weakBlock copy];`
|
||||
|
||||
|
||||
|
||||
### 4. 栈内存、堆内存保护机制
|
||||
|
||||
通过上面 test 和 test2 知道,栈内存、堆内存在释放后继续访问存在不同的表现
|
||||
|
||||
#### 1. 栈内存
|
||||
|
||||
##### 1. 栈内存的本质上是:
|
||||
|
||||
- **线性结构**:栈是连续的内存区域,通过栈指针(SP)管理
|
||||
- **自动回收**:函数退出时,只需移动栈指针即可"释放"内存
|
||||
- **物理内存不变**:移动栈指针不会立即清除数据,原内存内容保持不变
|
||||
|
||||
##### 2. 访问已释放的内存为何可能不崩溃?
|
||||
|
||||
- 无硬件保护:CPU 和 MMU(内存管理单元)不跟踪栈帧生命周期
|
||||
- 内存数据保留:除非被新的栈帧覆盖,否则数据仍可被读取
|
||||
|
||||
##### 3. 不崩溃的深层原因:
|
||||
|
||||
- 无页表标记:栈内存页始终标记为:可读写 (PROT_READ|PORT_WRITE)
|
||||
- 无隔离机制:不同函数的栈帧共享同一内存页
|
||||
- 延迟覆盖:新栈帧写入前,元数据保持有效
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#### 2. 堆内存
|
||||
|
||||
##### 1. 堆内存管理的核心机制:
|
||||
|
||||
- malloc -> 向内核申请内存页 -> 设置页表属性 -> 分配内存块
|
||||
- free -> 标记内存页属性 -> 加入空闲链表 -> 触发内存页回收
|
||||
|
||||
##### 2. free 后的关键操作
|
||||
|
||||
内存标记
|
||||
|
||||
````objective-c
|
||||
// 典型内存分配器实现
|
||||
void free(void *ptr) {
|
||||
// 1. 在内存块头部写入魔数(如0xDEADBEEF)
|
||||
*(uint32_t*)(ptr - 4) = 0xDEADBEEF;
|
||||
|
||||
// 2. 通过 mprotect 设置内存页不可访问
|
||||
mprotect(ALIGN_PAGE(ptr), PAGE_SIZE, PROT_NONE);
|
||||
}
|
||||
````
|
||||
|
||||
页表更新
|
||||
|
||||
| 状态 | 页表项标志位 | 访问后果 |
|
||||
| :--- | :-------------- | :----------- |
|
||||
| 正常 | PRESENT+RW+USER | 允许访问 |
|
||||
| 释放 | ~PRESENT | 触发缺页异常 |
|
||||
|
||||
稳定崩溃的硬件基础:
|
||||
|
||||
MMU 介入:当访问 `PROT_NONE` 内存页时:
|
||||
|
||||
1. 触发缺页一场(Page Fault)
|
||||
2. 内核检查异常地址
|
||||
3. 发送 `SIGSEGV` 信号
|
||||
4. 进程终止(崩溃)
|
||||
|
||||
#### 3. iOS/Macos 优化
|
||||
|
||||
##### 1. 堆内存保护优化
|
||||
|
||||
- Malloc Scribble:释放后填充`0x55`(调试模式默认启用)
|
||||
|
||||
- Zone-based Protection:
|
||||
|
||||
```objective-c
|
||||
malloc_zone_t *zone = malloc_create_zone(0, 0);
|
||||
malloc_set_zone_name(zone, "Protected Zone");
|
||||
malloc_zone_protect(zone, 1); // 启用保护
|
||||
```
|
||||
|
||||
##### 2. 栈内存的刻意放松
|
||||
|
||||
- **性能优先**:避免栈操作时的权限检查
|
||||
- **安全边界**:仅防止栈溢出,不保护栈帧间访问
|
||||
|
||||
|
||||
|
||||
### 5. block 如何修改变量
|
||||
|
||||
#### 1. __block 修饰基本数据类型
|
||||
|
||||
```objectivec
|
||||
typedef void(^MyBlock)(void);
|
||||
@@ -1178,7 +1488,7 @@ __attribute__((__blocks__(byref))) __Block_byref_num2_0 num2 = {(void*)0,(__Bloc
|
||||
|
||||
|
||||
|
||||
#### __block 修饰对象
|
||||
#### 2. __block 修饰对象
|
||||
|
||||
对` __block` 修饰的对象,clang 转换为 c++ 后如下:
|
||||
|
||||
@@ -1307,13 +1617,13 @@ block 内部对变量的值修改其实就是对 block 内部自定义结构体
|
||||
|
||||
|
||||
|
||||
#### 什么情况下需要 __block
|
||||
#### 3. 什么情况下需要 __block
|
||||
|
||||
局部变量:基本数据类型、对象数据类型
|
||||
|
||||
|
||||
|
||||
#### 什么情况下不需要 __block
|
||||
#### 4. 什么情况下不需要 __block
|
||||
|
||||
- 全局变量(不截获)
|
||||
- 静态全局变量(不截获)
|
||||
@@ -1323,7 +1633,7 @@ block 内部对变量的值修改其实就是对 block 内部自定义结构体
|
||||
|
||||
|
||||
|
||||
## `__forwarding` 的设计
|
||||
## 五、`__forwarding` 的设计
|
||||
|
||||
Demo1
|
||||
|
||||
@@ -1587,7 +1897,7 @@ Tips:实现 block 拷贝及其捕获对象的函数是 `_Block_copy`,工作
|
||||
|
||||
|
||||
|
||||
## Block 内存引用
|
||||
## 六、Block 内存引用
|
||||
|
||||
对于` __block` 修饰的变量进行研究
|
||||
|
||||
@@ -1803,7 +2113,7 @@ __attribute__((__blocks__(byref))) __Block_byref_p_0 p = {
|
||||
|
||||
|
||||
|
||||
## 循环引用
|
||||
## 七. 循环引用
|
||||
|
||||
self 是一个局部变量,block 访问 self,即存在捕获变量的效果。
|
||||
|
||||
@@ -1817,7 +2127,7 @@ __attribute__((__blocks__(byref))) __Block_byref_p_0 p = {
|
||||
|
||||
可以看到 Person 对象的 dealloc 方法没有执行,里面的打印信息没有输出。
|
||||
|
||||
### ARC 下
|
||||
### 1. ARC 下
|
||||
|
||||
`__weak`、`__unsafe_unretained` 修饰 `__block` 所修饰的变量。区别在于:
|
||||
|
||||
@@ -1880,7 +2190,7 @@ p.block();
|
||||
|
||||
|
||||
|
||||
### MRC 下
|
||||
### 2. MRC 下
|
||||
|
||||
方法1: `__unsafe_retained` 修饰。`__unsafe_unretained typeof(self) weakself = self;`
|
||||
|
||||
@@ -1888,7 +2198,7 @@ p.block();
|
||||
|
||||
|
||||
|
||||
## 为什么加 weakself、strongself
|
||||
## 八、为什么加 weakself、strongself
|
||||
|
||||
weakSelf 是为了使 block 不持有 self,避免 Retain Circle 循环引用。在 Block 内如果需要访问 self 的方法、变量,建议使用 weakSelf。
|
||||
|
||||
@@ -1922,7 +2232,9 @@ block 执行完后这个 strongSelf 会自动释放,没有不会存在循环
|
||||
});
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
|
||||
## 九、总结
|
||||
|
||||
1. block 本质是什么?封装了函数调用及其调用环境的 OC 对象。本质实现是一个结构体。
|
||||
2. `__block` 的作用是什么?可以对 block 外部的变量进行捕获,可以修改。但是需要注意内存管理相关问题。比如`__weak`、`__unsafe_unretained`、`__block`
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user