OC底层原理41:内存优化(一)野指针探测

张建 lol

前言

本文主要讲解两种 野指针检测 的原理和实现

技术点:野指针探测

本文的主要目的是理解 野指针 的形成过程以及如果去 检测野指针

引子

在介绍野指针之前,首先说下目前的异常处理类型,附 苹果官网链接

异常类型

异常类型分为两类:

  • 软件异常:主要来自 kill()、phread_kill()、iOS中的NSException未捕获、absort等

  • 硬件异常:硬件的信号处理器trap,是和平台相关的,野指针崩溃大部分是硬件异常

而处理异常时,需要关注两个概念

  • Mach异常:Mach 层异常

  • UNIX信号:BSD层获取

iOS中的POSIX API就是通过 Mach 之上的 BSD 层实现的,如下图所示:

  • Mach 是一个受 Acccent 启发而搞出的Unix系统

  • BSD 层是建立在 Mach 之上,是XNU中一个不可分割的一部分,BSD负责提供可靠的、现代的API

  • POSIX 表示可移植操作系统接口(Portable Operation System Interface)

所以,综上所述,Mach异常和UNIX信号存在对应的关系

  • 硬件异常流程:硬件异常 -> Mach异常 -> UNIX信号
  • 软件异常流程:软件异常 -> UNIX异常

Mach异常与UNIX信号的转换

下面是 Mach异常UNIX信号 的转换关系代码,来自 xnu 中的 bsd/uxkern/ux_exception.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
switch(exception) {
case EXC_BAD_ACCESS:
if (code == KERN_INVALID_ADDRESS)
*ux_signal = SIGSEGV;
else
*ux_signal = SIGBUS;
break;

case EXC_BAD_INSTRUCTION:
*ux_signal = SIGILL;
break;

case EXC_ARITHMETIC:
*ux_signal = SIGFPE;
break;

case EXC_EMULATION:
*ux_signal = SIGEMT;
break;

case EXC_SOFTWARE:
switch (code) {

case EXC_UNIX_BAD_SYSCALL:
*ux_signal = SIGSYS;
break;
case EXC_UNIX_BAD_PIPE:
*ux_signal = SIGPIPE;
break;
case EXC_UNIX_ABORT:
*ux_signal = SIGABRT;
break;
case EXC_SOFT_SIGNAL:
*ux_signal = SIGKILL;
break;
}
break;

case EXC_BREAKPOINT:
*ux_signal = SIGTRAP;
break;
}
  • 将其对应关系汇总成一个表格,如下所示

  • 其中 Mach异常有以下

  • UNIX信号有以下几种

野指针

指向的 对象被释放或收回,但是该 指针没有做任何的修改,以至于 该指针仍指向已经回收的内存地址,这个指针就是 野指针

野指针分类

这个参考腾讯Bugly团队的总结,大致分为两类

  • 内存没被覆盖
  • 内存被覆盖

如下图所示

为什么OC野指针的crash这么多?

我们一般在APP发版前,都会经过多轮的 自测、内侧、灰度测试 等,按照常理来说,大部分的crash应该都被覆盖了,但是由于 野指针的随机性,使得经常在测试时不会出现crash,而是在 线上出现crash,这对App体验来说是非常致命的

而野指针的随机性问题大致可以分为两类:

  • 跑不进出错的逻辑,执行不到出错的代码,这种可以通过 提高测试场景覆盖率 来解决
  • 跑进有问题的逻辑,但是野指针指向的地址并不一定会导致crash,原因是因为:野指针 其本质是指向 已经删除的对象受限内存区域 的指针,这里说的 OC野指针,是指 OC对象释放后指针未置空而导致的野指针。这里不必现的原因是因为 dealloc 执行后只是告诉系统,这片 内存我不用了而系统并没有让这片内存不能访问

野指针解决思路

这里主要是借鉴Xcode中的两种处理方案:

  • Malloc Scrrbble,其官方解释 如下:申请内存 alloc 时在内存上填 0xAA,释放内存 dealloc 在内存上填 0x55

  • Zombie Objects,其 官方解释 如下:一个对象已经解除了它的引用,已经释放掉,但是此时仍然是可以接收消息,这个对象就叫做 Zombie Objects(僵尸对象)。这种方案的重点就是 将释放的对象,全部转为僵尸对象

两种方案对比

  • 僵尸对象 相比 Malloc Scribble,不需要考虑会不会崩溃的问题,只要野指针指向僵尸对象,那么再次访问野指针就一定会崩溃

  • 僵尸对象这种方式,不如 Malloc Scribble覆盖面广,可以通过 hook free 方法将c函数也包含在其中

Malloc Scribble

思路:当访问到对象内存中填写的是 0xAA、0x55 时,程序就会出现异常

  • 申请内存 alloc 时在内存上填 0xAA
  • 释放内存 dealloc 使在内存上填 0x55

以上的申请和释放的填充分别对应以下两种情况

  • 申请:没有错初始化就直接被访问
  • 释放:释放后访问

所以综上所述,针对野指针,我们的解决办法是:在对象释放时做数据填充 0x55 即可,关于对象的释放流程可以参考这篇文章 OC底层原理35:内存管理(一)TaggedPointer/retain/release/dealloc/

野指针探测实现1

这个实现主要依据 腾讯Bugly工程师:陈其锋 的分享,在其代码的主要思路是:

  • 通过 fishhook 替换 c函数free 方法为自定义的 safe_free,类似于 Method Swizzling

  • safe_free 方法中对 已经释放变量的内存,填充 0x55 ,使已经释放变量 不能访问,从而使某些野指针的crash从不必现变成 必现

    • 为了 防止填充0x55的内存内新的数据内容填充,使野指针crash变成不必现,在这里采用的策略是,safe_free不释放这片内存,而是自己保留着,即safe_free方法中不会真的调用free。

    • 同时为了 防止系统内存过快消耗(因为要保留内存),需要在 保留内存大于一定值时释放一部分,防止被系统杀死,同时,在收到 系统内存警告 时,也需要 释放一部分内存

  • 发生crash时,得到的崩溃信息有限,不利于排查问题,所以这里采用代理类(即集成自 NSProxy的子类),重写消息转发的三个方法(参考这篇文章 OC底层原理14-3:消息流程分析之动态方法决议 & 消息转发),以及NSObject的实例方法,来获取异常信息。但是这样的话,还有一个问题,就是NSProxy只能做OC对象的代理,所以需要在safe_free中增加对象类型的判断

以下是完整的野指针探测实现代码

  • 引入fishhook

  • 实现NSProxy的代理子类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<!--1、MIZombieProxy.h-->
@interface MIZombieProxy : NSProxy

@property (nonatomic, assign) Class originClass;

@end

<!--2、MIZombieProxy.m-->
#import "MIZombieProxy.h"

@implementation MIZombieProxy

- (BOOL)respondsToSelector:(SEL)aSelector{
return [self.originClass instancesRespondToSelector:aSelector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.originClass instanceMethodSignatureForSelector:sel];
}

- (void)forwardInvocation: (NSInvocation *)invocation
{
[self _throwMessageSentExceptionWithSelector: invocation.selector];
}

#define MIZombieThrowMesssageSentException() [self _throwMessageSentExceptionWithSelector: _cmd]
- (Class)class{
MIZombieThrowMesssageSentException();
return nil;
}
- (BOOL)isEqual:(id)object{
MIZombieThrowMesssageSentException();
return NO;
}
- (NSUInteger)hash{
MIZombieThrowMesssageSentException();
return 0;
}
- (id)self{
MIZombieThrowMesssageSentException();
return nil;
}
- (BOOL)isKindOfClass:(Class)aClass{
MIZombieThrowMesssageSentException();
return NO;
}
- (BOOL)isMemberOfClass:(Class)aClass{
MIZombieThrowMesssageSentException();
return NO;
}
- (BOOL)conformsToProtocol:(Protocol *)aProtocol{
MIZombieThrowMesssageSentException();
return NO;
}
- (BOOL)isProxy{
MIZombieThrowMesssageSentException();
return NO;
}

- (NSString *)description{
MIZombieThrowMesssageSentException();
return nil;
}

#pragma mark - MRC
- (instancetype)retain{
MIZombieThrowMesssageSentException();
return nil;
}
- (oneway void)release{
MIZombieThrowMesssageSentException();
}
- (void)dealloc
{
MIZombieThrowMesssageSentException();
[super dealloc];
}
- (NSUInteger)retainCount{
MIZombieThrowMesssageSentException();
return 0;
}
- (struct _NSZone *)zone{
MIZombieThrowMesssageSentException();
return nil;
}


#pragma mark - private
- (void)_throwMessageSentExceptionWithSelector:(SEL)selector{
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"(-[%@ %@]) was sent to a zombie object at address: %p", NSStringFromClass(self.originClass),NSStringFromSelector(selector), self] userInfo:nil];
}
@end
  • hook free 方法的具体实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
<!--1、MISafeFree.h-->
@interface MISafeFree : NSObject

//系统警告时,用函数释放一些内存
void free_safe_mem(size_t freeNum);

@end

<!--2、MISafeFree.m-->
#import "MISafeFree.h"
#import "queue.h"
#import "fishhook.h"
#import "MIZombieProxy.h"

#import <dlfcn.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>

//用于保存zombie类
static Class kMIZombieIsa;
//用于保存zombie类的实例变量大小
static size_t kMIZombieSize;

//用于表示调用free函数
static void(* orig_free)(void *p);
//用于保存已注册的类的集合
static CFMutableSetRef registeredClasses = nil;
/*
用来保存自己保留的内存
- 1、队列要线程安全或者自己加锁
- 2、这个队列内部应该尽量少申请和释放堆内存
*/
struct DSQueue *_unfreeQueue = NULL;
//用来记录自己保存的内存的大小
int unfreeSize = 0;

//最多存储的内存,大于这个值就释放一部分
#define MAX_STEAL_MEM_SIZE 1024*1024*100
//最多保留的指针个数,超过就释放一部分
#define MAX_STEAL_MEM_NUM 1024*1024*10
//每次释放时释放的指针数量
#define BATCH_FREE_NUM 100

@implementation MISafeFree

#pragma mark - Public Method
//系统警告时,用函数释放一些内存
void free_safe_mem(size_t freeNum){
#ifdef DEBUG
//获取队列的长度
size_t count = ds_queue_length(_unfreeQueue);
//需要释放的内存大小
freeNum = freeNum > count ? count : freeNum;
//遍历并释放
for (int i = 0; i < freeNum; i++) {
//获取未释放的内存块
void *unfreePoint = ds_queue_get(_unfreeQueue);
//创建内存块申请的大小
size_t memSize = malloc_size(unfreePoint);
//原子减操作,多线程对全局变量进行自减
__sync_fetch_and_sub(&unfreeSize, (int)memSize);
//释放
orig_free(unfreePoint);
}
#endif
}

#pragma mark - Life Circle

+ (void)load{
#ifdef DEBUG
loadZombieProxyClass();
init_safe_free();
#endif
}

#pragma mark - Private Method
void safe_free(void* p){

//获取自己保留的内存的大小
int unFreeCount = ds_queue_length(_unfreeQueue);
//保留的内存大于一定值时就释放一部分
if (unFreeCount > MAX_STEAL_MEM_NUM*0.9 || unfreeSize>MAX_STEAL_MEM_SIZE) {
free_safe_mem(BATCH_FREE_NUM);
}else{
//创建p申请的内存大小
size_t memSize = malloc_size(p);
//有足够的空间才覆盖
if (memSize > kMIZombieSize) {
//指针强转为id对象
id obj = (id)p;
//获取指针原本的类
Class origClass = object_getClass(obj);
//判断是不是objc对象
char *type = @encode(typeof(obj));
/*
- strcmp 字符串比较
- CFSetContainsValue 查看已注册类中是否有origClass这个类

如果都满足,则将这块内存填充0x55
*/
if (strcmp("@", type) == 0 && CFSetContainsValue(registeredClasses, origClass)) {
//内存上填充0x55
memset(obj, 0x55, memSize);
//将自己类的isa复制过去
memcpy(obj, &kMIZombieIsa, sizeof(void*));
//为obj设置指定的类
object_setClass(obj, [MIZombieProxy class]);
//保留obj原本的类
((MIZombieProxy*)obj).originClass = origClass;
//多线程下int的原子加操作,多线程对全局变量进行自加,不用理会线程锁了
__sync_fetch_and_add(&unfreeSize, (int)memSize);
//入队
ds_queue_put(_unfreeQueue, p);
}else{
orig_free(p);
}
}else{
orig_free(p);
}
}
}

//加载野指针自定义类
void loadZombieProxyClass(){
registeredClasses = CFSetCreateMutable(NULL, 0, NULL);

//用于保存已注册类的个数
unsigned int count = 0;
//获取所有已注册的类
Class *classes = objc_copyClassList(&count);
//遍历,并保存到registeredClasses中
for (int i = 0; i < count; i++) {
CFSetAddValue(registeredClasses, (__bridge const void *)(classes[i]));
}
//释放临时变量内存
free(classes);
classes = NULL;

kMIZombieIsa = objc_getClass("MIZombieProxy");
kMIZombieSize = class_getInstanceSize(kMIZombieIsa);
}

//初始化以及free符号重绑定
bool init_safe_free(){
//初始化用于保存内存的队列
_unfreeQueue = ds_queue_create(MAX_STEAL_MEM_NUM);
//dlsym 在打开的库中查找符号的值,即动态调用free函数
orig_free = (void(*)(void*))dlsym(RTLD_DEFAULT, "free");
/*
rebind_symbols:符号重绑定
- 参数1:rebindings 是一个rebinding数组,其定义如下
struct rebinding {
const char *name; // 目标符号名
void *replacement; // 要替换的符号值(地址值)
void **replaced; // 用来存放原来的符号值(地址值)
};
- 参数2:rebindings_nel 描述数组的长度
*/
//重绑定free符号,让它指向自定义的safe_free函数
rebind_symbols((struct rebinding[]){{"free", (void*)safe_free}}, 1);
return true;
}

@end
  • 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)viewDidLoad {
[super viewDidLoad];

id obj = [[NSObject alloc] init];
self.assignObj = obj;

// [MIZombieSniffer installSniffer];
}
- (IBAction)mallocScribbleAction:(id)sender {

UIView* testObj = [[UIView alloc] init];
[testObj release];
for (int i = 0; i < 10; i++) {
UIView* testView = [[UIView alloc] initWithFrame:CGRectMake(0,200,CGRectGetWidth(self.view.bounds), 60)];
[self.view addSubview:testView];
}
[testObj setNeedsLayout];

}

打印结果如下

Zombie Objects

僵尸对象

  • 可以用来检测内存错误(EXC_BAD_ACCESS),它可以捕获任何阐释访问坏内存的调用

  • 给僵尸对象发送消息的话,它仍然是可以响应的,然后会发生崩溃,并输出错误日志来显示野指针对象调用的类名和方法

苹果的僵尸对象检测原理

首先我们来看下Xcode中僵尸对象是如何实现的,具体操作步骤可以参考这篇文章 iOS Zombie Objects(僵尸对象)原理探索

  • dealloc 的源码中,我们可以看到 Replaced by NSZombie,即 对象释放 时,NSZombiedealloc 里做替换,如下所示

所以僵尸对象的生成过程伪代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1、获取到即将deallocted对象所属类(Class)
Class cls = object_getClass(self);

// 2、获取类名
const char *clsName = class_getName(cls)

// 3、生成僵尸对象类名
const char *zombieClsName = "_NSZombie_" + clsName;

// 4、查看是否存在相同的僵尸对象类名,不存在则创建
Class zombieCls = objc_lookUpClass(zombieClsName);
if (!zombieCls) {
// 5、获取僵尸对象类 _NSZombie_
Class baseZombieCls = objc_lookUpClass(“_NSZombie_");

// 6、创建 zombieClsName 类
zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0);
}
// 7、在对象内存未被释放的情况下销毁对象的成员变量及关联引用。
objc_destructInstance(self);

// 8、修改对象的 isa 指针,令其指向特殊的僵尸类
objc_setClass(self, zombieCls);
  • 当僵尸对象再次被访问时,将进入消息转发流程,开始处理僵尸对象访问,输出日志并发生crash

所以僵尸对象触发流程伪代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//1、获取对象class
Class cls = object_getClass(self);

//2、获取对象类名
const char *clsName = class_getName(cls);

//3、检测是否带有前缀_NSZombie_
if (string_has_prefix(clsName, "_NSZombie_")) {
//4、获取被野指针对象类名
const char *originalClsName = substring_from(clsName, 10);

 //5、获取当前调用方法名
 const char *selectorName = sel_getName(_cmd);
  
 //6、输出日志
 Log(''*** - [%s %s]: message sent to deallocated instance %p", originalClsName, selectorName, self);

 //7、结束进程
 abort();

所以综上所述,这野指针探测方式的思路是:dealloc 方法的替换,其关键是调用 objc_destructInstance 来解除对象的关联引用

野指针探测实现2

这种思路主要来源 sindrilin 的源码,其主要思路是:

野指针检测流程

  • 开启野指针检测

  • 设值监控到野指针的回调block,在block中打印信息,或者存储堆栈

  • 检测到野指针是否crash

  • 最大内存占用空间

  • 是否记录dealloc调用栈

  • 监控策略

    • 只监控自定义对象

    • 白名单策略

    • 黑名单策略

    • 监控所有对象

  • 交换NSObject的dealloc方法

触发野指针

  • 开始处理对象

  • 是否达到替换条件

    • 根据监控策略,是否属于要检测的类
    • 空间是否足够
  • 如果符合条件,则获取对象,并解除引用,如果不符合则正常释放,即调用原来的dealloc方法

  • 向对象内填充数据

  • 赋值僵尸对象的类指针替换isa

  • 对象+dealloc调用栈,保存在僵尸对象中

  • 根据情况是否清理内存和对象

通过僵尸对象检测的实现思路

  • 通过OC中 Method Swizzling,交换 根类NSObject和NSProxy 的 dealloc 方法为 自定义的dealloc 方法

  • 为了 避免内存空间释放后被重写造成野指针 的问题,通过 字典存储被释放的对象,同时设置在 30s后调用dealloc方法将字典中存储的对象释放,避免内存增大

  • 为了获取更多的崩溃信息,这里同样需要创建NSProxy的子类

具体实现

  • 创建NSProxy的子类,其实现与上面的 MIZombieProxy 是一模一样的

  • hook dealloc 函数的具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
<!--1、MIZombieSniffer.h-->
@interface MIZombieSniffer : NSObject

/*!
* @method installSniffer
* 启动zombie检测
*/
+ (void)installSniffer;

/*!
* @method uninstallSnifier
* 停止zombie检测
*/
+ (void)uninstallSnifier;

/*!
* @method appendIgnoreClass
* 添加白名单类
*/
+ (void)appendIgnoreClass: (Class)cls;

@end

<!--2、MIZombieSniffer.m-->
#import "MIZombieSniffer.h"
#import "MIZombieProxy.h"
#import <objc/runtime.h>

//
typedef void (*MIDeallocPointer) (id objc);
//野指针探测器是否开启
static BOOL _enabled = NO;
//根类
static NSArray *_rootClasses = nil;
//用于存储被释放的对象
static NSDictionary<id, NSValue*> *_rootClassDeallocImps = nil;

//白名单
static inline NSMutableSet *__mi_sniffer_white_lists(){
//创建白名单集合
static NSMutableSet *mi_sniffer_white_lists;
//单例初始化白名单集合
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
mi_sniffer_white_lists = [[NSMutableSet alloc] init];
});
return mi_sniffer_white_lists;
}


static inline void __mi_dealloc(__unsafe_unretained id obj){
//获取对象的类
Class currentCls = [obj class];
Class rootCls = currentCls;

//获取非NSObject和NSProxy的类
while (rootCls != [NSObject class] && rootCls != [NSProxy class]) {
//获取rootCls的父类,并赋值
rootCls = class_getSuperclass(rootCls);
}
//获取类名
NSString *clsName = NSStringFromClass(rootCls);
//根据类名获取dealloc的imp指针
MIDeallocPointer deallocImp = NULL;
[[_rootClassDeallocImps objectForKey:clsName] getValue:&deallocImp];

if (deallocImp != NULL) {
deallocImp(obj);
}
}

//hook交换dealloc
static inline IMP __mi_swizzleMethodWithBlock(Method method, void *block){
/*
imp_implementationWithBlock :接收一个block参数,将其拷贝到堆中,返回一个trampoline
可以让block当做任何一个类的方法的实现,即当做类的方法的IMP来使用
*/
IMP blockImp = imp_implementationWithBlock((__bridge id _Nonnull)(block));
//method_setImplementation 替换掉method的IMP
return method_setImplementation(method, blockImp);
}

@implementation MIZombieSniffer

//初始化根类
+ (void)initialize
{
_rootClasses = [@[[NSObject class], [NSProxy class]] retain];
}

#pragma mark - public
+ (void)installSniffer{
@synchronized (self) {
if (!_enabled) {
//hook根类的dealloc方法
[self _swizzleDealloc];
_enabled = YES;
}
}
}

+ (void)uninstallSnifier{
@synchronized (self) {
if (_enabled) {
//还原dealloc方法
[self _unswizzleDealloc];
_enabled = NO;
}
}
}

//添加百名单
+ (void)appendIgnoreClass:(Class)cls{
@synchronized (self) {
NSMutableSet *whiteList = __mi_sniffer_white_lists();
NSString *clsName = NSStringFromClass(cls);
[clsName retain];
[whiteList addObject:clsName];
}
}

#pragma mark - private
+ (void)_swizzleDealloc{
static void *swizzledDeallocBlock = NULL;

//定义block,作为方法的IMP
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
swizzledDeallocBlock = (__bridge void *)[^void(id obj) {
//获取对象的类
Class currentClass = [obj class];
//获取类名
NSString *clsName = NSStringFromClass(currentClass);
//判断该类是否在白名单类
if ([__mi_sniffer_white_lists() containsObject: clsName]) {
//如果在白名单内,则直接释放对象
__mi_dealloc(obj);
} else {
//修改对象的isa指针,指向MIZombieProxy
/*
valueWithBytes:objCType 创建并返回一个包含给定值的NSValue对象,该值会被解释为一个给定的NSObject类型
- 参数1:NSValue对象的值
- 参数2:给定值的对应的OC类型,需要使用编译器指令@encode来创建
*/
NSValue *objVal = [NSValue valueWithBytes: &obj objCType: @encode(typeof(obj))];
//为obj设置指定的类
object_setClass(obj, [MIZombieProxy class]);
//保留对象原本的类
((MIZombieProxy *)obj).originClass = currentClass;

//设置在30s后调用dealloc将存储的对象释放,避免内存空间的增大
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(30 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
__unsafe_unretained id deallocObj = nil;
//获取需要dealloc的对象
[objVal getValue: &deallocObj];
//设置对象的类为原本的类
object_setClass(deallocObj, currentClass);
//释放
__mi_dealloc(deallocObj);
});
}
} copy];
});

//交换了根类NSObject和NSProxy的dealloc方法为originalDeallocImp
NSMutableDictionary *deallocImps = [NSMutableDictionary dictionary];
//遍历根类
for (Class rootClass in _rootClasses) {
//获取指定类中dealloc方法
Method oriMethod = class_getInstanceMethod([rootClass class], NSSelectorFromString(@"dealloc"));
//hook - 交换dealloc方法的IMP实现
IMP originalDeallocImp = __mi_swizzleMethodWithBlock(oriMethod, swizzledDeallocBlock);
//设置IMP的具体实现
[deallocImps setObject: [NSValue valueWithBytes: &originalDeallocImp objCType: @encode(typeof(IMP))] forKey: NSStringFromClass(rootClass)];
}
//_rootClassDeallocImps字典存储交换后的IMP实现
_rootClassDeallocImps = [deallocImps copy];
}

+ (void)_unswizzleDealloc{
//还原dealloc交换的IMP
[_rootClasses enumerateObjectsUsingBlock:^(Class rootClass, NSUInteger idx, BOOL * _Nonnull stop) {
IMP originDeallocImp = NULL;
//获取根类类名
NSString *clsName = NSStringFromClass(rootClass);
//获取hook后的dealloc实现
[[_rootClassDeallocImps objectForKey:clsName] getValue:&originDeallocImp];

NSParameterAssert(originDeallocImp);
//获取原本的dealloc实现
Method oriMethod = class_getInstanceMethod([rootClass class], NSSelectorFromString(@"dealloc"));
//还原dealloc的实现
method_setImplementation(oriMethod, originDeallocImp);
}];
//释放
[_rootClassDeallocImps release];
_rootClassDeallocImps = nil;
}

@end
  • 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface ViewController ()

@property (nonatomic, assign) id assignObj;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

id obj = [[NSObject alloc] init];
self.assignObj = obj;

[MIZombieSniffer installSniffer];
}
- (IBAction)zombieObjectAction:(id)sender {

NSLog(@"%@", self.assignObj);

}

打印崩溃信息如下

  • Post title:OC底层原理41:内存优化(一)野指针探测
  • Post author:张建
  • Create time:2021-07-22 14:51:26
  • Post link:https://redefine.ohevan.com/2021/07/22/OC底层原理/OC底层原理41:内存优化(一)野指针探测/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.