mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-25 12:27:15 +00:00
feat: Zombie 探针
This commit is contained in:
@@ -255,6 +255,96 @@ static inline bool _objc_isTaggedPointer(const void * _Nullable ptr)
|
||||
1000
|
||||
```
|
||||
|
||||
tips:某些对象虽然是 TaggedPointer 类型,但是打印 class 发现不是,猜测可能是系统用类簇隐藏了某些实现细节。比如下面
|
||||
|
||||

|
||||
|
||||
针对 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,当引用计数减为 0,OC 对象就会销毁,释放其占用的内存空间
|
||||
@@ -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就变成了下面的样子:
|
||||
|
||||

|
||||
|
||||
`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,信息如下
|
||||
|
||||

|
||||
|
||||
原因是 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我写了个僵尸对象检测工具,效果如下
|
||||
|
||||

|
||||
|
||||
可以定位僵尸对象,并且打印出具体堆栈,并模拟系统行为调用 `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/僵尸对象探针)
|
||||
Reference in New Issue
Block a user