feature: Weex APM

This commit is contained in:
FantasticLBP
2025-12-30 21:07:15 +08:00
parent 1142064d28
commit 7ac7513900
158 changed files with 9880 additions and 280 deletions

View File

@@ -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`