本文主要講解兩種野指針檢測(cè)的原理及實(shí)現(xiàn)
技術(shù)點(diǎn):野指針探測(cè)
本文的主要目的是理解野指針的形成過(guò)程以及如何去檢測(cè)野指針
引子
在介紹野指針之前品洛,首先說(shuō)下目前的異常處理類型,附上蘋果官網(wǎng)鏈接)
異常類型
異常大致可以分為兩類:
1器净、
軟件異常
:主要是來(lái)自kill()座硕、pthread_kill()、iOS中的NSException未捕獲票渠、absort等2颂鸿、
硬件異常
:硬件的信號(hào)始于處理器trap衬以,是和平臺(tái)相關(guān)的扰藕,野指針崩潰大部分是硬件異常
而在處理異常時(shí)缓苛,需要關(guān)注兩個(gè)概念
-
Mach異常
:Mach層
捕獲 -
UNIX信號(hào)
:BSD層
獲取
iOS中的POSIX API就是通過(guò)Mach之上的BSD層實(shí)現(xiàn)的,如下圖所示
Mach
是一個(gè)受 Accent 啟發(fā)而搞出的Unix兼容系統(tǒng)邓深。BSD
層是建立在Mach之上未桥,是XNU中一個(gè)不可分割的一部分。BSD負(fù)責(zé)提供可靠的芥备、現(xiàn)代的APIPOSIX
表示可移植操作系統(tǒng)接口(Portable Operating System Interface)
所以冬耿,綜上所述,Mach異常和UNIX信號(hào)存在對(duì)應(yīng)的關(guān)系
- 1萌壳、硬件異常流程:硬件異常 -> Mach異常 -> UNIX信號(hào)
- 2亦镶、軟件異常流程:軟件異常 -> UNIX信號(hào)
Mach異常與UNIX信號(hào)的轉(zhuǎn)換
下面是Mach異常
與 UNIX信號(hào)
的轉(zhuǎn)換關(guān)系代碼,來(lái)自 xnu
中的 bsd/uxkern/ux_exception.c
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;
}
-
將其對(duì)應(yīng)關(guān)系匯總成一個(gè)表格袱瓮,如下所示
其中Mach異常有以下
Mach異常 | 說(shuō)明 |
---|---|
EXC_BAD_ACCESS |
不能訪問(wèn)的內(nèi)存 |
EXC_BAD_INSTRUCTION |
非法或未定義的指令或操作數(shù) |
EXC_ARITHMETIC |
算術(shù)異常(例如除以0)缤骨。iOS 默認(rèn)是不啟用的,所以我們一般不會(huì)遇到 |
EXC_EMULATION |
執(zhí)行打算用于支持仿真的指令 |
EXC_SOFTWARE |
軟件生成的異常懂讯,我們?cè)?Crash 日志中一般不會(huì)看到這個(gè)類型荷憋,蘋果的日志里會(huì)是 EXC_CRASH |
EXC_BREAKPOINT |
跟蹤或斷點(diǎn) |
EXC_SYSCALL |
UNIX 系統(tǒng)調(diào)用 |
EXC_MACH_SYSCALL |
Mach 系統(tǒng)調(diào)用 |
- UNIX信號(hào)有以下幾種
UNIX信號(hào) | 說(shuō)明 |
---|---|
SIGSEGV |
段錯(cuò)誤。訪問(wèn)未分配內(nèi)存褐望、寫入沒(méi)有寫權(quán)限的內(nèi)存等。 |
SIGBUS |
總線錯(cuò)誤串前。比如內(nèi)存地址對(duì)齊瘫里、錯(cuò)誤的內(nèi)存類型訪問(wèn)等。 |
SIGILL |
執(zhí)行了非法指令荡碾,一般是可執(zhí)行文件出現(xiàn)了錯(cuò)誤 |
SIGFPE |
致命的算術(shù)運(yùn)算谨读。比如數(shù)值溢出、NaN數(shù)值等坛吁。 |
SIGABRT |
調(diào)用 abort() 產(chǎn)生劳殖,通過(guò) pthread_kill() 發(fā)送铐尚。 |
SIGPIPE |
管道破裂。通常在進(jìn)程間通信產(chǎn)生哆姻。比如采用FIFO(管道)通信的兩個(gè)進(jìn)程宣增,讀管道沒(méi)打開(kāi)或者意外終止就往管道寫,寫進(jìn)程會(huì)收到SIGPIPE信號(hào)矛缨。根據(jù)蘋果相關(guān)文檔爹脾,可以忽略這個(gè)信號(hào)。 |
SIGSYS |
系統(tǒng)調(diào)用異常箕昭。 |
SIGKILL |
此信號(hào)表示系統(tǒng)中止進(jìn)程灵妨。崩潰報(bào)告會(huì)包含代表中止原因的編碼。exit(), kill(9) 等函數(shù)調(diào)用落竹。iOS 系統(tǒng)殺進(jìn)程泌霍,如 watchDog 殺進(jìn)程。 |
SIGTRAP |
斷點(diǎn)指令或者其他trap指令產(chǎn)生述召。 |
野指針
所指向的對(duì)象被釋放或者收回
朱转,但是該指針沒(méi)有作任何的修改
,以至于該指針仍舊指向已經(jīng)回收的內(nèi)存地址
桨武。這個(gè)指針就是野指針
野指針?lè)诸?/strong>
這個(gè)參考騰訊Bugly團(tuán)隊(duì)的總結(jié)肋拔,大致分為兩類
- 內(nèi)存沒(méi)被覆蓋
- 內(nèi)存被覆蓋
如下圖所示
為什么OC野指針的crash這么多?
我們一般在app發(fā)版前呀酸,都會(huì)經(jīng)過(guò)多輪的自測(cè)凉蜂、內(nèi)側(cè)、灰度測(cè)試
等性誉,按照常理來(lái)說(shuō)窿吩,大部分的crash應(yīng)該都被覆蓋了,但是由于野指針的隨機(jī)性
错览,使得經(jīng)常在測(cè)試時(shí)不會(huì)出現(xiàn)crash纫雁,而是在線上出現(xiàn)crash
,這對(duì)app體驗(yàn)來(lái)說(shuō)是非常致命的
而野指針的隨機(jī)性問(wèn)題大致可以分為兩類:
- 1倾哺、跑不進(jìn)出錯(cuò)的邏輯轧邪,執(zhí)行不到出錯(cuò)的代碼,這種可以通過(guò)
提高測(cè)試場(chǎng)景覆蓋率
來(lái)解決 - 2羞海、跑進(jìn)有問(wèn)題的邏輯忌愚,但是野指針指向的地址并不一定會(huì)導(dǎo)致crash,原因是因?yàn)椋?code>野指針其本質(zhì)是一個(gè)指向
已經(jīng)刪除的對(duì)象
或受限內(nèi)存區(qū)域
的指針
却邓。這里說(shuō)的OC野指針
硕糊,是指OC對(duì)象釋放后指針未置空而導(dǎo)致的野指針
。這里不必現(xiàn)的原因是因?yàn)?code>dealloc執(zhí)行后只是告訴系統(tǒng),這片內(nèi)存我不用了简十,而系統(tǒng)并沒(méi)有讓這片內(nèi)存不能訪問(wèn)
野指針解決思路
這里主要是借鑒Xcode中的兩種處理方案:
- 1檬某、Malloc Scribble ,其官方解釋如下:申請(qǐng)內(nèi)存
alloc
時(shí)在內(nèi)存上填0xAA
螟蝙,釋放內(nèi)存dealloc
在內(nèi)存上填0x55
恢恼。
- 2、Zombie Objects胶逢,其官方解釋如下:一個(gè)對(duì)象已經(jīng)解除了它的引用厅瞎,已經(jīng)被釋放掉,但是此時(shí)仍然是可以接受消息初坠,這個(gè)對(duì)象就叫做
Zombie Objects
(僵尸對(duì)象)和簸。這種方案的重點(diǎn)就是將釋放的對(duì)象,全都轉(zhuǎn)為僵尸對(duì)象
兩種方案對(duì)比
1碟刺、
僵尸對(duì)象
相比Malloc Scribble
锁保,不需要考慮會(huì)不會(huì)崩潰的問(wèn)題
,只要野指針指向僵尸對(duì)象半沽,那么再次訪問(wèn)野指針就一定會(huì)崩潰2爽柒、僵尸對(duì)象這種方式,
不如Malloc Scribble覆蓋面廣
者填,可以通過(guò)hook free方法將c函數(shù)也包含在其中
1浩村、Malloc Scribble
思路:當(dāng)訪問(wèn)到對(duì)象內(nèi)存中填充的是0xAA、0x55
時(shí)占哟,程序就會(huì)出現(xiàn)異常
申請(qǐng)內(nèi)存
alloc
時(shí)在內(nèi)存上填0xAA
心墅,釋放內(nèi)存
dealloc
在內(nèi)存上填0x55
。
以上的申請(qǐng)和釋放的填充分別對(duì)應(yīng)一下兩種情況
- 申請(qǐng):沒(méi)有做初始化就直接被訪問(wèn)
- 釋放:釋放后訪問(wèn)
所以綜上所述榨乎,針對(duì)野指針怎燥,我們的解決辦法是:在對(duì)象釋放時(shí)做數(shù)據(jù)填充0x55
即可。關(guān)于對(duì)象的釋放流程可以參考這篇文章iOS-底層原理 33:內(nèi)存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底層分析
野指針探測(cè)實(shí)現(xiàn)1
這個(gè)實(shí)現(xiàn)主要依據(jù)騰訊Bugly工程師:陳其鋒的分享蜜暑,在其代碼中的主要思路是
- 1铐姚、通過(guò)
fishhook
替換C函數(shù)
的free
方法為自定義的safe_free
,類似于Method Swizzling - 2肛捍、在
safe_free
方法中對(duì)已經(jīng)釋放變量的內(nèi)存
隐绵,填充0x55
,使已經(jīng)釋放變量不能訪問(wèn)
拙毫,從而使某些野指針的crash從不必現(xiàn)安變成必現(xiàn)
氢橙。為了
防止填充0x55的內(nèi)存被新的數(shù)據(jù)內(nèi)容填充
,使野指針crash變成不必現(xiàn)恬偷,在這里采用的策略是,safe_free不釋放這片內(nèi)存,而是自己保留著
袍患,即safe_free方法中不會(huì)真的調(diào)用free坦康。同時(shí)為了
防止系統(tǒng)內(nèi)存過(guò)快消耗
(因?yàn)橐A魞?nèi)存),需要在保留的內(nèi)存大于一定值時(shí)釋放一部分
诡延,防止被系統(tǒng)殺死滞欠,同時(shí),在收到系統(tǒng)內(nèi)存警告
時(shí)肆良,也需要釋放一部分內(nèi)存
- 3筛璧、發(fā)生crash時(shí),得到的崩潰信息有限惹恃,不利于問(wèn)題排查夭谤,所以這里采用代理類(即繼承自
NSProxy
的子類),重寫消息轉(zhuǎn)發(fā)的三個(gè)方法(參考這篇文章iOS-底層原理 14:消息流程分析之 動(dòng)態(tài)方法決議 & 消息轉(zhuǎn)發(fā))巫糙,以及NSObject的實(shí)例方法朗儒,來(lái)獲取異常信息。但是這的話参淹,還有一個(gè)問(wèn)題醉锄,就是NSProxy只能做OC對(duì)象的代理,所以需要在safe_free中增加對(duì)象類型的判斷
以下是完整的野指針探測(cè)實(shí)現(xiàn)代碼
-
引入fishhook
實(shí)現(xiàn)NSProxy的代理子類
<!--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方法的具體實(shí)現(xiàn)
<!--1、MISafeFree.h-->
@interface MISafeFree : NSObject
//系統(tǒng)警告時(shí)开呐,用函數(shù)釋放一些內(nèi)存
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類的實(shí)例變量大小
static size_t kMIZombieSize;
//用于表示調(diào)用free函數(shù)
static void(* orig_free)(void *p);
//用于保存已注冊(cè)的類的集合
static CFMutableSetRef registeredClasses = nil;
/*
用來(lái)保存自己保留的內(nèi)存
- 1、隊(duì)列要線程安全或者自己加鎖
- 2负蚊、這個(gè)隊(duì)列內(nèi)部應(yīng)該盡量少申請(qǐng)和釋放堆內(nèi)存
*/
struct DSQueue *_unfreeQueue = NULL;
//用來(lái)記錄自己保存的內(nèi)存的大小
int unfreeSize = 0;
//最多存儲(chǔ)的內(nèi)存神妹,大于這個(gè)值就釋放一部分
#define MAX_STEAL_MEM_SIZE 1024*1024*100
//最多保留的指針個(gè)數(shù),超過(guò)就釋放一部分
#define MAX_STEAL_MEM_NUM 1024*1024*10
//每次釋放時(shí)釋放的指針數(shù)量
#define BATCH_FREE_NUM 100
@implementation MISafeFree
#pragma mark - Public Method
//系統(tǒng)警告時(shí)家妆,用函數(shù)釋放一些內(nèi)存
void free_safe_mem(size_t freeNum){
#ifdef DEBUG
//獲取隊(duì)列的長(zhǎng)度
size_t count = ds_queue_length(_unfreeQueue);
//需要釋放的內(nèi)存大小
freeNum = freeNum > count ? count : freeNum;
//遍歷并釋放
for (int i = 0; i < freeNum; i++) {
//獲取未釋放的內(nèi)存塊
void *unfreePoint = ds_queue_get(_unfreeQueue);
//創(chuàng)建內(nèi)存塊申請(qǐng)的大小
size_t memSize = malloc_size(unfreePoint);
//原子減操作鸵荠,多線程對(duì)全局變量進(jìn)行自減
__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){
//獲取自己保留的內(nèi)存的大小
int unFreeCount = ds_queue_length(_unfreeQueue);
//保留的內(nèi)存大于一定值時(shí)就釋放一部分
if (unFreeCount > MAX_STEAL_MEM_NUM*0.9 || unfreeSize>MAX_STEAL_MEM_SIZE) {
free_safe_mem(BATCH_FREE_NUM);
}else{
//創(chuàng)建p申請(qǐng)的內(nèi)存大小
size_t memSize = malloc_size(p);
//有足夠的空間才覆蓋
if (memSize > kMIZombieSize) {
//指針強(qiáng)轉(zhuǎn)為id對(duì)象
id obj = (id)p;
//獲取指針原本的類
Class origClass = object_getClass(obj);
//判斷是不是objc對(duì)象
char *type = @encode(typeof(obj));
/*
- strcmp 字符串比較
- CFSetContainsValue 查看已注冊(cè)類中是否有origClass這個(gè)類
如果都滿足,則將這塊內(nèi)存填充0x55
*/
if (strcmp("@", type) == 0 && CFSetContainsValue(registeredClasses, origClass)) {
//內(nèi)存上填充0x55
memset(obj, 0x55, memSize);
//將自己類的isa復(fù)制過(guò)去
memcpy(obj, &kMIZombieIsa, sizeof(void*));
//為obj設(shè)置指定的類
object_setClass(obj, [MIZombieProxy class]);
//保留obj原本的類
((MIZombieProxy*)obj).originClass = origClass;
//多線程下int的原子加操作伤极,多線程對(duì)全局變量進(jìn)行自加蛹找,不用理會(huì)線程鎖了
__sync_fetch_and_add(&unfreeSize, (int)memSize);
//入隊(duì)
ds_queue_put(_unfreeQueue, p);
}else{
orig_free(p);
}
}else{
orig_free(p);
}
}
}
//加載野指針自定義類
void loadZombieProxyClass(){
registeredClasses = CFSetCreateMutable(NULL, 0, NULL);
//用于保存已注冊(cè)類的個(gè)數(shù)
unsigned int count = 0;
//獲取所有已注冊(cè)的類
Class *classes = objc_copyClassList(&count);
//遍歷,并保存到registeredClasses中
for (int i = 0; i < count; i++) {
CFSetAddValue(registeredClasses, (__bridge const void *)(classes[i]));
}
//釋放臨時(shí)變量?jī)?nèi)存
free(classes);
classes = NULL;
kMIZombieIsa = objc_getClass("MIZombieProxy");
kMIZombieSize = class_getInstanceSize(kMIZombieIsa);
}
//初始化以及free符號(hào)重綁定
bool init_safe_free(){
//初始化用于保存內(nèi)存的隊(duì)列
_unfreeQueue = ds_queue_create(MAX_STEAL_MEM_NUM);
//dlsym 在打開(kāi)的庫(kù)中查找符號(hào)的值哨坪,即動(dòng)態(tài)調(diào)用free函數(shù)
orig_free = (void(*)(void*))dlsym(RTLD_DEFAULT, "free");
/*
rebind_symbols:符號(hào)重綁定
- 參數(shù)1:rebindings 是一個(gè)rebinding數(shù)組庸疾,其定義如下
struct rebinding {
const char *name; // 目標(biāo)符號(hào)名
void *replacement; // 要替換的符號(hào)值(地址值)
void **replaced; // 用來(lái)存放原來(lái)的符號(hào)值(地址值)
};
- 參數(shù)2:rebindings_nel 描述數(shù)組的長(zhǎng)度
*/
//重綁定free符號(hào),讓它指向自定義的safe_free函數(shù)
rebind_symbols((struct rebinding[]){{"free", (void*)safe_free}}, 1);
return true;
}
@end
- 測(cè)試
- (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];
}
打印結(jié)果如下
2当编、Zombie Objects
僵尸對(duì)象
可以用來(lái)檢測(cè)內(nèi)存錯(cuò)誤(
EXC_BAD_ACCESS
)届慈,它可以捕獲任何闡釋訪問(wèn)壞內(nèi)存的調(diào)用給僵尸對(duì)象發(fā)送消息的話,它仍然是可以響應(yīng)的,然后會(huì)發(fā)生崩潰金顿,并輸出錯(cuò)誤日志來(lái)顯示野指針對(duì)象調(diào)用的類名和方法
蘋果的僵尸對(duì)象檢測(cè)原理
首先我們來(lái)看下Xcode中僵尸對(duì)象是如何實(shí)現(xiàn)的臊泌,具體操作步驟可以參考這篇文章iOS Zombie Objects(僵尸對(duì)象)原理探索
- 從
dealloc
的源碼中,我們可以看到“Replaced by NSZombie”
揍拆,即對(duì)象釋放
時(shí)渠概,NSZombie 將在 dealloc 里做替換
,如下所示
所以僵尸對(duì)象的生成過(guò)程偽代碼如下
//1嫂拴、獲取到即將deallocted對(duì)象所屬類(Class)
Class cls = object_getClass(self);
//2播揪、獲取類名
const char *clsName = class_getName(cls)
//3、生成僵尸對(duì)象類名
const char *zombieClsName = "_NSZombie_" + clsName;
//4筒狠、查看是否存在相同的僵尸對(duì)象類名猪狈,不存在則創(chuàng)建
Class zombieCls = objc_lookUpClass(zombieClsName);
if (!zombieCls) {
//5、獲取僵尸對(duì)象類 _NSZombie_
Class baseZombieCls = objc_lookUpClass(“_NSZombie_");
//6窟蓝、創(chuàng)建 zombieClsName 類
zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0);
}
//7罪裹、在對(duì)象內(nèi)存未被釋放的情況下銷毀對(duì)象的成員變量及關(guān)聯(lián)引用。
objc_destructInstance(self);
//8运挫、修改對(duì)象的 isa 指針状共,令其指向特殊的僵尸類
objc_setClass(self, zombieCls);
-
當(dāng)僵尸對(duì)象再次被訪問(wèn)時(shí),將進(jìn)入消息轉(zhuǎn)發(fā)流程谁帕,開(kāi)始處理僵尸對(duì)象訪問(wèn)峡继,輸出日志并發(fā)生crash
所以僵尸對(duì)象觸發(fā)流程偽代碼如下
//1、獲取對(duì)象class
Class cls = object_getClass(self);
//2匈挖、獲取對(duì)象類名
const char *clsName = class_getName(cls);
//3碾牌、檢測(cè)是否帶有前綴_NSZombie_
if (string_has_prefix(clsName, "_NSZombie_")) {
//4、獲取被野指針對(duì)象類名
const char *originalClsName = substring_from(clsName, 10);
//5儡循、獲取當(dāng)前調(diào)用方法名
const char *selectorName = sel_getName(_cmd);
//6舶吗、輸出日志
Log(''*** - [%s %s]: message sent to deallocated instance %p", originalClsName, selectorName, self);
//7、結(jié)束進(jìn)程
abort();
所以綜上所述择膝,這中野指針探測(cè)方式的思路是:dealloc
方法的替換誓琼,其關(guān)鍵是調(diào)用objc_destructInstance
來(lái)解除對(duì)象的關(guān)聯(lián)引用
野指針探測(cè)實(shí)現(xiàn)2
這種方式的思路主要是來(lái)源sindrilin的源碼,其主要思路是:
-
野指針檢測(cè)流程
1肴捉、開(kāi)啟野指針檢測(cè)
2腹侣、設(shè)置監(jiān)控到野指針時(shí)的回調(diào)block恶迈,在block中打印信息呻右,或者存儲(chǔ)堆棧
3、檢測(cè)到野指針是否crash
4胧瓜、最大內(nèi)存占用空間
5窃页、是否記錄dealloc調(diào)用棧
-
6跺株、監(jiān)控策略
1)只監(jiān)控自定義對(duì)象
2)白名單策略
3)黑名單策略
4)監(jiān)控所有對(duì)象
7复濒、交換NSObject的dealloc方法
-
觸發(fā)野指針
1、開(kāi)始處理對(duì)象
-
2帖鸦、是否達(dá)到替換條件
- 1)根據(jù)監(jiān)控策略芝薇,是否屬于要檢測(cè)的類
- 2)空間是否足夠
3、如果符合條件作儿,則獲取對(duì)象,并解除引用馋劈,如果不符合則正常釋放攻锰,即調(diào)用原來(lái)的dealloc方法
4、向?qū)ο髢?nèi)填充數(shù)據(jù)
5妓雾、賦值僵尸對(duì)象的類指針替換isa
6娶吞、對(duì)象+dealloc調(diào)用棧,保存在僵尸對(duì)象中
7械姻、根據(jù)情況是否清理內(nèi)存和對(duì)象
通過(guò)僵尸對(duì)象檢測(cè)的實(shí)現(xiàn)思路
1妒蛇、通過(guò)OC中
Mehod Swizzling
,交換根類NSObject和NSProxy
的dealloc
方法為自定義的dealloc
方法2楷拳、為了
避免內(nèi)存空間釋放后被重寫造成野指針
的問(wèn)題绣夺,通過(guò)字典存儲(chǔ)被釋放的對(duì)象
,同時(shí)設(shè)置在30s后調(diào)用dealloc方法將字典中存儲(chǔ)的對(duì)象釋放欢揖,避免內(nèi)存增大
3陶耍、為了獲取更多的崩潰信息,這里同樣需要?jiǎng)?chuàng)建NSProxy的子類
具體實(shí)現(xiàn)
1她混、創(chuàng)建NSProxy的子類烈钞,其實(shí)現(xiàn)與上面的
MIZombieProxy
是一模一樣的2、hook dealloc函數(shù)的具體實(shí)現(xiàn)
<!--1坤按、MIZombieSniffer.h-->
@interface MIZombieSniffer : NSObject
/*!
* @method installSniffer
* 啟動(dòng)zombie檢測(cè)
*/
+ (void)installSniffer;
/*!
* @method uninstallSnifier
* 停止zombie檢測(cè)
*/
+ (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);
//野指針探測(cè)器是否開(kāi)啟
static BOOL _enabled = NO;
//根類
static NSArray *_rootClasses = nil;
//用于存儲(chǔ)被釋放的對(duì)象
static NSDictionary<id, NSValue*> *_rootClassDeallocImps = nil;
//白名單
static inline NSMutableSet *__mi_sniffer_white_lists(){
//創(chuàng)建白名單集合
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){
//獲取對(duì)象的類
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);
//根據(jù)類名獲取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 :接收一個(gè)block參數(shù)臭脓,將其拷貝到堆中酗钞,返回一個(gè)trampoline
可以讓block當(dāng)做任何一個(gè)類的方法的實(shí)現(xiàn),即當(dāng)做類的方法的IMP來(lái)使用
*/
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) {
//獲取對(duì)象的類
Class currentClass = [obj class];
//獲取類名
NSString *clsName = NSStringFromClass(currentClass);
//判斷該類是否在白名單類
if ([__mi_sniffer_white_lists() containsObject: clsName]) {
//如果在白名單內(nèi)算吩,則直接釋放對(duì)象
__mi_dealloc(obj);
} else {
//修改對(duì)象的isa指針,指向MIZombieProxy
/*
valueWithBytes:objCType 創(chuàng)建并返回一個(gè)包含給定值的NSValue對(duì)象佃扼,該值會(huì)被解釋為一個(gè)給定的NSObject類型
- 參數(shù)1:NSValue對(duì)象的值
- 參數(shù)2:給定值的對(duì)應(yīng)的OC類型偎巢,需要使用編譯器指令@encode來(lái)創(chuàng)建
*/
NSValue *objVal = [NSValue valueWithBytes: &obj objCType: @encode(typeof(obj))];
//為obj設(shè)置指定的類
object_setClass(obj, [MIZombieProxy class]);
//保留對(duì)象原本的類
((MIZombieProxy *)obj).originClass = currentClass;
//設(shè)置在30s后調(diào)用dealloc將存儲(chǔ)的對(duì)象釋放,避免內(nèi)存空間的增大
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(30 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
__unsafe_unretained id deallocObj = nil;
//獲取需要dealloc的對(duì)象
[objVal getValue: &deallocObj];
//設(shè)置對(duì)象的類為原本的類
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實(shí)現(xiàn)
IMP originalDeallocImp = __mi_swizzleMethodWithBlock(oriMethod, swizzledDeallocBlock);
//設(shè)置IMP的具體實(shí)現(xiàn)
[deallocImps setObject: [NSValue valueWithBytes: &originalDeallocImp objCType: @encode(typeof(IMP))] forKey: NSStringFromClass(rootClass)];
}
//_rootClassDeallocImps字典存儲(chǔ)交換后的IMP實(shí)現(xiàn)
_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實(shí)現(xiàn)
[[_rootClassDeallocImps objectForKey:clsName] getValue:&originDeallocImp];
NSParameterAssert(originDeallocImp);
//獲取原本的dealloc實(shí)現(xiàn)
Method oriMethod = class_getInstanceMethod([rootClass class], NSSelectorFromString(@"dealloc"));
//還原dealloc的實(shí)現(xiàn)
method_setImplementation(oriMethod, originDeallocImp);
}];
//釋放
[_rootClassDeallocImps release];
_rootClassDeallocImps = nil;
}
@end
- 3兼耀、測(cè)試
@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);
}
打印崩潰信息如下