iOS-底層原理36:內(nèi)存優(yōu)化(一) 野指針探測(cè)

iOS 底層原理 文章匯總

本文主要講解兩種野指針檢測(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)代的API

  • POSIX表示可移植操作系統(tǒng)接口(Portable Operating System Interface)

所以冬耿,綜上所述,Mach異常和UNIX信號(hào)存在對(duì)應(yīng)的關(guān)系


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異常和UNIX信號(hào)對(duì)應(yīng)表格
  • 其中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)存被覆蓋

如下圖所示


騰訊Bugly總結(jié)

為什么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中的兩種處理方案:


xcode圖示
  • 1檬某、Malloc Scribble ,其官方解釋如下:申請(qǐng)內(nèi)存 alloc 時(shí)在內(nèi)存上填0xAA螟蝙,釋放內(nèi)存 dealloc 在內(nèi)存上填 0x55恢恼。
    Malloc Scribble官方解釋
  • 2、Zombie Objects胶逢,其官方解釋如下:一個(gè)對(duì)象已經(jīng)解除了它的引用厅瞎,已經(jīng)被釋放掉,但是此時(shí)仍然是可以接受消息初坠,這個(gè)對(duì)象就叫做Zombie Objects(僵尸對(duì)象)和簸。這種方案的重點(diǎn)就是將釋放的對(duì)象,全都轉(zhuǎn)為僵尸對(duì)象
    Zombie Objects官方解釋

兩種方案對(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


    引入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é)果如下


測(cè)試結(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 里做替換,如下所示
    Zombie Objects原理-01

    所以僵尸對(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


    Zombie Objects原理-02

    所以僵尸對(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和NSProxydealloc方法為自定義的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);
    
}

打印崩潰信息如下


Zombie Objects運(yùn)行結(jié)果

參考文章

補(bǔ)充

github源碼鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末压昼,一起剝皮案震驚了整個(gè)濱河市求冷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌窍霞,老刑警劉巖匠题,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異但金,居然都是意外死亡韭山,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門冷溃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)钱磅,“玉大人,你說(shuō)我怎么就攤上這事似枕「堑” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵凿歼,是天一觀的道長(zhǎng)褪迟。 經(jīng)常有香客問(wèn)我,道長(zhǎng)答憔,這世上最難降的妖魔是什么味赃? 我笑而不...
    開(kāi)封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮攀唯,結(jié)果婚禮上洁桌,老公的妹妹穿的比我還像新娘。我一直安慰自己侯嘀,他們只是感情好另凌,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著戒幔,像睡著了一般吠谢。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上诗茎,一...
    開(kāi)封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天工坊,我揣著相機(jī)與錄音,去河邊找鬼敢订。 笑死王污,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的楚午。 我是一名探鬼主播昭齐,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼矾柜!你這毒婦竟也來(lái)了阱驾?” 一聲冷哼從身側(cè)響起就谜,我...
    開(kāi)封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎里覆,沒(méi)想到半個(gè)月后丧荐,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡喧枷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年虹统,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片割去。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡窟却,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出呻逆,到底是詐尸還是另有隱情,我是刑警寧澤菩帝,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布咖城,位于F島的核電站,受9級(jí)特大地震影響呼奢,放射性物質(zhì)發(fā)生泄漏宜雀。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一握础、第九天 我趴在偏房一處隱蔽的房頂上張望辐董。 院中可真熱鬧,春花似錦禀综、人聲如沸简烘。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)孤澎。三九已至,卻和暖如春欠窒,著一層夾襖步出監(jiān)牢的瞬間覆旭,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工岖妄, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留型将,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓荐虐,卻偏偏與公主長(zhǎng)得像七兜,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子缚俏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容