feat: Zombie 探针

This commit is contained in:
LiuBinPeng
2022-05-26 15:22:20 +08:00
parent 4f11b95363
commit 538801e651
7 changed files with 339 additions and 84 deletions

View File

@@ -255,6 +255,96 @@ static inline bool _objc_isTaggedPointer(const void * _Nullable ptr)
1000
```
tips某些对象虽然是 TaggedPointer 类型,但是打印 class 发现不是,猜测可能是系统用类簇隐藏了某些实现细节。比如下面
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/NSTaggedPointerOfNSNumber.png)
针对 NSNumber 的 TaggedPoniter 的 case查看 class 打印出 `__NSCFNumber`。但根据源码和内存高地址位分析确实是 TaggedPoniter。
疑问是:为什么 NSNumber 的 TaggedPpinter case 下打印 class 是 `__NSCFNumber`。如果是类簇隐藏细节实现,为什么同样 KVO 也改变了 isa但是命名是一个新的名字而不是类簇的实现
和朋友讨论后有2种观点观点不是独立的而是并且同时成立的。对错难以判定仅供参考
- 类簇,为了隐藏细节实现
- KVO 和当前 case 不一致。类簇是系统类的设计KVO 是针对开发者写的对象所以没有类簇,只能动态生成类,改变原类的 isa命名为 `NSKVONotifying_***` 这样的规则。
## 类簇
类簇Class Cluster )是抽象工厂模式在 OC 数组中的实现NSArray、NSNumber、NSString、NSDictionary 都有体现。借口简单性和拓展性的权衡体现。系统隐藏了较多实现细节,只暴露出简单接口。
```objectivec
- (void)classCus
{
id obj1 = [NSArray alloc]; // __NSPlaceholderArray
id obj2 = [NSMutableArray alloc]; // __NSPlaceholderArray
id obj3 = [obj1 init]; // __NSArray0
id obj4 = [obj2 init]; // __NSArrayM
NSLog(@"%@ %@ %@ %@", obj1, obj2, obj3, obj4);
}
```
调用 alloc 之后产生的是 `__NSPlaceholderArray` 不符合预期。继续调用 init 发现满足期望了。所以猜测 `__NSPlaceholderArray`  是一个中间对象,后续的 init 方法就是给中间对象发消息,再由它做工厂,生成真的对象,这里的 `__NSArray0``__NSArrayM` 对应 NSArray、NSMutableArray
Foundation用了静态实例地址方式来实现伪代码如下
```objectivec
static __NSPlacehodlerArray *GetPlaceholderForNSArray() {
static __NSPlacehodlerArray *instanceForNSArray;
if (!instanceForNSArray) {
instanceForNSArray = [[__NSPlacehodlerArray alloc] init];
}
return instanceForNSArray;
}
static __NSPlacehodlerArray *GetPlaceholderForNSMutableArray() {
static __NSPlacehodlerArray *instanceForNSMutableArray;
if (!instanceForNSMutableArray) {
instanceForNSMutableArray = [[__NSPlacehodlerArray alloc] init];
}
return instanceForNSMutableArray;
}
// NSArray实现
+ (id)alloc {
if (self == [NSArray class]) {
return GetPlaceholderForNSArray()
}
}
// NSMutableArray实现
+ (id)alloc {
if (self == [NSMutableArray class]) {
return GetPlaceholderForNSMutableArray()
}
}
// __NSPlacehodlerArray实现
- (id)init {
if (self == GetPlaceholderForNSArray()) {
self = [[__NSArrayI alloc] init];
}
else if (self == GetPlaceholderForNSMutableArray()) {
self = [[__NSArrayM alloc] init];
}
return self;
}
```
另外 iOS Foundation 对静态不可变空对象(当前 case 为数组)做了优化
```objectivec
NSArray *a1 = [[NSArray alloc] init];
NSArray *a2 = [[NSArray alloc] init];
NSArray *a3 = [[NSArray alloc] init];
(lldb) p a1
(__NSArray0 *) $0 = 0x0000000109f50a10 @"0 elements"
(lldb) p a2
(__NSArray0 *) $1 = 0x0000000109f50a10 @"0 elements"
(lldb) p a3
(__NSArray0 *) $2 = 0x0000000109f50a10 @"0 elements"
(lldb)
```
若干个不可变的空数组间没有任何特异性,返回一个静态对象。
## OC 对象内存管理
iOS 中使用引用计数来管理 OC 对象的内存。一个新创建的 OC 对象引用计数默认是1当引用计数减为 0OC 对象就会销毁,释放其占用的内存空间
@@ -947,9 +1037,9 @@ struct FinishARCDealloc : EHScopeStack::Cleanup {
- ARC 模式下 `[super dealloc]` 由 llvm 编译器自动插入(CodeGen)
## 自动释放池底层原理探索
## AutoreleasePool 底层原理探索
上 Demo
### 单 AutoreleasePool 的 case
```objectivec
int main(int argc, const char * argv[]) {
@@ -1069,6 +1159,8 @@ static inline void *push() {
}
```
### 多 AutoreleasePool 的 case
来个骚一些的例子
```objectivec
@@ -1097,6 +1189,8 @@ main 方法内部3个 autoreleasepool 底层怎么样工作的?
紧接着第二个大括号结束,第二个结构体对象析构函数执行,内部调用 `AutoreleasePoolPage::pop` 方法,会从最后一个入栈的对象开始发送 release 消息,直到遇到 `POOL_BOUNDARY` 对象。
所以嵌套的AutoreleasePool就非常简单了pop的时候总会释放到上次push的位置为止多层的pool就是多个哨兵对象而已就像剥洋葱一样每次一层互不影响
第一个同理。
小窍门,对于上述原理的分析可以用源码中看到的 `AutoreleasePoolPage` 对象的 `printAll` 方法。
@@ -1851,9 +1945,33 @@ static inline id *autoreleaseFast(id obj) {
查看 NSObject autorelease 方法调用链路可以看到最后还是调用 AutoreleasePoolPage 的 add 方法(会判断有没有页、有没有满)
## autorelease 对象什么时候调用 release 方法
### 容器类会自动添加 AutoreleasePool
其实也就是 autorelease 和 RunLoop 的关系。
系统容器类,在使用 block 枚举器的时候,内部会自动创建 AutoreleasePool
```objectivec
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
@autoreleasepool {
<#statements#>
}
}];
```
所以,我们老老实实写的 for、while 循环中需要手加局部 AutoreleasePool。推荐使用系统提供的容器类的 block 枚举器。
### autorelease 对象什么时候调用 release 方法
每当进行一次`objc_autoreleasePoolPush`调用时runtime向当前的AutoreleasePoolPage中add进一个`哨兵对象`值为0也就是个nil那么这一个page就变成了下面的样子
![](http://ww2.sinaimg.cn/large/51530583gw1elj5z7hawej20ji0dewff.jpg)
`objc_autoreleasePoolPush`的返回值正是这个哨兵对象的地址,被`objc_autoreleasePoolPop(哨兵对象)`作为入参,于是:
1. 根据传入的哨兵对象地址找到哨兵对象所处的page
2. 在当前page中将晚于哨兵对象插入的所有autorelease对象都发送一次`- release`消息,并向回移动`next`指针到正确位置
3. 补充2从最新加入的对象一直向前清理可以向前跨越若干个page直到哨兵所在的page
其次AutoreleasePool 和 RunLoop 的也有关系
iOS 在主线程的 Runloop 中注册了2个 Observer
@@ -1877,4 +1995,146 @@ iOS 在主线程的 Runloop 中注册了2个 Observer
- 11 没任务将要休眠,调用 `objc_autoreleasePoolPop`
可以看到 objc_autoreleasePoolPush、objc_autoreleasePoolPop 成对调用,贯穿 RunLoop
可以看到 objc_autoreleasePoolPush、objc_autoreleasePoolPop 成对调用,贯穿 RunLoop
## 内存问题典型 case
### OC 中有没有不对内存进行强持有的集合类型?
NSHashMap、NSMapTable 都可以描述 key、value 的内存修饰。
数组有 NSPointerArray 内部持有的是对象的指针,并非直接保存对象。不过 oc 转指针需要加 `(__bridge void*)` 进行修饰。NSPointerArray 的构造方法中可以通过 NSPointerFunctionsOptions 来声明内存的控制。
```objectivec
- (void)viewDidLoad {
[super viewDidLoad];
Person *p1 = [[Person alloc] init];
Person *p2 = [[Person alloc] init];
Person *p3 = [[Person alloc] init];
NSPointerArray *arrays = [[NSPointerArray alloc] initWithOptions:NSPointerFunctionsWeakMemory];
// NSMutableArray *array = [NSMutableArray array];
// [array addObject:p1];
// [array addObject:p2];
// [array addObject:p3];
[arrays addPointer:(__bridge void *)p1];
[arrays addPointer:(__bridge void *)p2];
[arrays addPointer:(__bridge void *)p3];
p1 = nil;
p2 = nil;
// 断点设置到 NSLog可以看到 Person 马上释放了
NSLog(@"%@", arrays);
}
2022-05-24 21:57:27.071793+0800 TTTTW[63427:2087468] -[Person dealloc]
2022-05-24 21:57:27.071916+0800 TTTTW[63427:2087468] -[Person dealloc]
(lldb)
```
### NSError 内存泄漏的 case
同事问了一个问题,下面的代码存在什么问题?
据说是 Zoom 这个公司的面试题,看了下其实就是考察 NSError 有没有踩过坑。怎么理解呢
```objectivec
- (BOOL)isZoomUserWithUserID:(NSInteger)userID error:(NSError **)error
{
@autoreleasepool {
NSString *errorMessage = [[NSString alloc] initWithFormat:@"the user is not zoom user"];
if (userID == 100) {
*error = [NSError errorWithDomain:@"com.test" code:userID userInfo:@{NSLocalizedDescriptionKey: errorMessage}];
return NO;
}
}
return YES;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self test];
}
- (void)test {
for (NSInteger index = 0; index <= 100; index++) {
NSString *str;
str = [NSString stringWithFormat:@"welcome to zoom:%ld", index];
str = [str stringByAppendingString:@" user"];
NSError *error = NULL;
if ([self isZoomUserWithUserID:index error:&error]) {
NSLog(@"%@", str);
} else {
NSLog(@"%@", error);
}
}
}
```
这段代码运行会 crash信息如下
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/NSErrorZombieCrash.png)
原因是 NSError 构造方法内部会加 autorelease。源码如下
```objectivec
#define AUTORELEASE(object) [(id)(object) autorelease]
+ (id) errorWithDomain: (NSErrorDomain)aDomain
code: (NSInteger)aCode
userInfo: (NSDictionary*)aDictionary
{
NSError *e = [self allocWithZone: NSDefaultMallocZone()];
e = [e initWithDomain: aDomain code: aCode userInfo: aDictionary];
return AUTORELEASE(e);
}
```
MRC 下的 `[(id)(object) autorelease]` 等价于 ARC 下的 `id __autoreleasing obj`
所以这个问题的本质就是 `autoreleasepool``__autoreleasing` 的问题
> `__autoreleasing` is used to denote arguments that are passed by reference (`id *`) and are autoreleased on return.
`__autoreleasing` 修饰的变量会被添加到当前的 autoreleasepool 中。
方法的 Out Parameters 参数会自动添加 __autoreleasing 属性。当方法参数里面有 Out Parameters 参数时,就是有指针的指针类型时,编译器会自动为参数加上`__autoreleasing` 属性。改如下
```objectivec
- (BOOL)isZoomUserWithUserID:(NSInteger)userID error:(NSError **)error
{
NSError *temp;
@autoreleasepool {
NSString *errorMessage = [[NSString alloc] initWithFormat:@"the user is not zoom user"];
if (userID == 100) {
temp = [NSError errorWithDomain:@"com.test" code:userID userInfo:@{NSLocalizedDescriptionKey: errorMessage}];
}
}
*error = temp;
return YES;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self test];
}
- (void)test {
for (NSInteger index = 0; index <= 100; index++) {
NSString *str;
str = [NSString stringWithFormat:@"welcome to zoom:%ld", index];
str = [str stringByAppendingString:@" user"];
NSError * __autoreleasing error = NULL;
if ([self isZoomUserWithUserID:index error:&error]) {
NSLog(@"%@", str);
} else {
NSLog(@"%@", error);
}
}
}
```
我写了个僵尸对象检测工具,效果如下
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/assets/ZombieSniffer.png)
可以定位僵尸对象,并且打印出具体堆栈,并模拟系统行为调用 `abort` 。对监控原理和工具实现感兴趣的可以查看这里[带你打造一套 APM 监控系统-内存监控之野指针/内存泄漏监控](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.74.md#zombieSniffer)
Demo [👇这里](https://github.com/FantasticLBP/BlogDemos/tree/master/僵尸对象探针)