iOS-底層原理36:內存優化(一) 野指針探測

iOS 底層原理 文章匯總

本文主要講解兩種野指針檢測的原理及實現

技術點:野指針探測

本文的主要目的是理解野指針的形成過程以及如何去檢測野指針

引子

在介紹野指針之前,首先說下目前的異常處理類型,附上蘋果官網鏈接

異常類型

異常大致可以分為兩類:

  • 1、軟件異常:主要是來自kill()、pthread_kill()、iOS中的NSException未捕獲、absort等

  • 2、硬件異常:硬件的信號始于處理器trap,是和平臺相關的,野指針崩潰大部分是硬件異常

而在處理異常時,需要關注兩個概念

  • Mach異常Mach層捕獲
  • UNIX信號BSD層獲取

iOS中的POSIX API就是通過Mach之上的BSD層實現的,如下圖所示


層圖示
  • Mach 是一個受 Accent 啟發而搞出的Unix兼容系統。

  • BSD層是建立在Mach之上,是XNU中一個不可分割的一部分。BSD負責提供可靠的、現代的API

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

所以,綜上所述,Mach異常和UNIX信號存在對應的關系


Mach異常和UNIX信號對應關系
  • 1、硬件異常流程:硬件異常 -> Mach異常 -> UNIX信號
  • 2、軟件異常流程:軟件異常 -> UNIX信號

Mach異常與UNIX信號的轉換

下面是Mach異常UNIX信號 的轉換關系代碼,來自 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;
}
  • 將其對應關系匯總成一個表格,如下所示


    Mach異常和UNIX信號對應表格
  • 其中Mach異常有以下

Mach異常 說明
EXC_BAD_ACCESS 不能訪問的內存
EXC_BAD_INSTRUCTION 非法或未定義的指令或操作數
EXC_ARITHMETIC 算術異常(例如除以0)。iOS 默認是不啟用的,所以我們一般不會遇到
EXC_EMULATION 執行打算用于支持仿真的指令
EXC_SOFTWARE 軟件生成的異常,我們在 Crash 日志中一般不會看到這個類型,蘋果的日志里會是 EXC_CRASH
EXC_BREAKPOINT 跟蹤或斷點
EXC_SYSCALL UNIX 系統調用
EXC_MACH_SYSCALL Mach 系統調用
  • UNIX信號有以下幾種
UNIX信號 說明
SIGSEGV 段錯誤。訪問未分配內存、寫入沒有寫權限的內存等。
SIGBUS 總線錯誤。比如內存地址對齊、錯誤的內存類型訪問等。
SIGILL 執行了非法指令,一般是可執行文件出現了錯誤
SIGFPE 致命的算術運算。比如數值溢出、NaN數值等。
SIGABRT 調用 abort() 產生,通過 pthread_kill() 發送。
SIGPIPE 管道破裂。通常在進程間通信產生。比如采用FIFO(管道)通信的兩個進程,讀管道沒打開或者意外終止就往管道寫,寫進程會收到SIGPIPE信號。根據蘋果相關文檔,可以忽略這個信號。
SIGSYS 系統調用異常。
SIGKILL 此信號表示系統中止進程。崩潰報告會包含代表中止原因的編碼。exit(), kill(9) 等函數調用。iOS 系統殺進程,如 watchDog 殺進程。
SIGTRAP 斷點指令或者其他trap指令產生。

野指針

所指向的對象被釋放或者收回,但是該指針沒有作任何的修改,以至于該指針仍舊指向已經回收的內存地址。這個指針就是野指針

野指針分類

這個參考騰訊Bugly團隊的總結,大致分為兩類

  • 內存沒被覆蓋
  • 內存被覆蓋

如下圖所示


騰訊Bugly總結

為什么OC野指針的crash這么多?
我們一般在app發版前,都會經過多輪的自測、內側、灰度測試等,按照常理來說,大部分的crash應該都被覆蓋了,但是由于野指針的隨機性,使得經常在測試時不會出現crash,而是在線上出現crash,這對app體驗來說是非常致命的

而野指針的隨機性問題大致可以分為兩類:

  • 1、跑不進出錯的邏輯,執行不到出錯的代碼,這種可以通過提高測試場景覆蓋率來解決
  • 2、跑進有問題的邏輯,但是野指針指向的地址并不一定會導致crash,原因是因為:野指針其本質是一個指向已經刪除的對象受限內存區域指針。這里說的OC野指針,是指OC對象釋放后指針未置空而導致的野指針。這里不必現的原因是因為dealloc執行后只是告訴系統,這片內存我不用了,而系統并沒有讓這片內存不能訪問

野指針解決思路

這里主要是借鑒Xcode中的兩種處理方案:


xcode圖示
  • 1、Malloc Scribble ,其官方解釋如下:申請內存 alloc 時在內存上填0xAA,釋放內存 dealloc 在內存上填 0x55
    Malloc Scribble官方解釋
  • 2、Zombie Objects,其官方解釋如下:一個對象已經解除了它的引用,已經被釋放掉,但是此時仍然是可以接受消息,這個對象就叫做Zombie Objects(僵尸對象)。這種方案的重點就是將釋放的對象,全都轉為僵尸對象
    Zombie Objects官方解釋

兩種方案對比

  • 1、僵尸對象 相比 Malloc Scribble不需要考慮會不會崩潰的問題,只要野指針指向僵尸對象,那么再次訪問野指針就一定會崩潰

  • 2、僵尸對象這種方式,不如Malloc Scribble覆蓋面廣,可以通過hook free方法將c函數也包含在其中

1、Malloc Scribble

思路:當訪問到對象內存中填充的是0xAA、0x55時,程序就會出現異常

  • 申請內存 alloc 時在內存上填0xAA,

  • 釋放內存 dealloc 在內存上填 0x55。

以上的申請和釋放的填充分別對應一下兩種情況

  • 申請:沒有做初始化就直接被訪問
  • 釋放:釋放后訪問

所以綜上所述,針對野指針,我們的解決辦法是:在對象釋放時做數據填充0x55即可。關于對象的釋放流程可以參考這篇文章iOS-底層原理 33:內存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底層分析

野指針探測實現1

這個實現主要依據騰訊Bugly工程師:陳其鋒的分享,在其代碼中的主要思路是

  • 1、通過fishhook替換C函數free方法為自定義的safe_free,類似于Method Swizzling
  • 2、在safe_free方法中對已經釋放變量的內存,填充0x55,使已經釋放變量不能訪問,從而使某些野指針的crash從不必現安變成必現。
    • 為了防止填充0x55的內存被新的數據內容填充,使野指針crash變成不必現,在這里采用的策略是,safe_free不釋放這片內存,而是自己保留著,即safe_free方法中不會真的調用free。

    • 同時為了防止系統內存過快消耗(因為要保留內存),需要在保留的內存大于一定值時釋放一部分,防止被系統殺死,同時,在收到系統內存警告時,也需要釋放一部分內存

  • 3、發生crash時,得到的崩潰信息有限,不利于問題排查,所以這里采用代理類(即繼承自NSProxy的子類),重寫消息轉發的三個方法(參考這篇文章iOS-底層原理 14:消息流程分析之 動態方法決議 & 消息轉發),以及NSObject的實例方法,來獲取異常信息。但是這的話,還有一個問題,就是NSProxy只能做OC對象的代理,所以需要在safe_free中增加對象類型的判斷

以下是完整的野指針探測實現代碼

  • 引入fishhook


    引入fishhook
  • 實現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方法的具體實現
<!--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
  • 測試
- (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];
    
}

打印結果如下


測試結果

2、Zombie Objects

僵尸對象

  • 可以用來檢測內存錯誤(EXC_BAD_ACCESS),它可以捕獲任何闡釋訪問壞內存的調用

  • 給僵尸對象發送消息的話,它仍然是可以響應的,然后會發生崩潰,并輸出錯誤日志來顯示野指針對象調用的類名和方法

蘋果的僵尸對象檢測原理
首先我們來看下Xcode中僵尸對象是如何實現的,具體操作步驟可以參考這篇文章iOS Zombie Objects(僵尸對象)原理探索

  • dealloc的源碼中,我們可以看到“Replaced by NSZombie”,即對象釋放時, NSZombie 將在 dealloc 里做替換,如下所示
    Zombie Objects原理-01

    所以僵尸對象的生成過程偽代碼如下
//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


    Zombie Objects原理-02

    所以僵尸對象觸發流程偽代碼如下

//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的源碼,其主要思路是:

  • 野指針檢測流程
    • 1、開啟野指針檢測

    • 2、設置監控到野指針時的回調block,在block中打印信息,或者存儲堆棧

    • 3、檢測到野指針是否crash

    • 4、最大內存占用空間

    • 5、是否記錄dealloc調用棧

    • 6、監控策略

      • 1)只監控自定義對象

      • 2)白名單策略

      • 3)黑名單策略

      • 4)監控所有對象

    • 7、交換NSObject的dealloc方法

  • 觸發野指針
    • 1、開始處理對象

    • 2、是否達到替換條件

      • 1)根據監控策略,是否屬于要檢測的類
      • 2)空間是否足夠
    • 3、如果符合條件,則獲取對象,并解除引用,如果不符合則正常釋放,即調用原來的dealloc方法

    • 4、向對象內填充數據

    • 5、賦值僵尸對象的類指針替換isa

    • 6、對象+dealloc調用棧,保存在僵尸對象中

    • 7、根據情況是否清理內存和對象

通過僵尸對象檢測的實現思路

  • 1、通過OC中Mehod Swizzling,交換根類NSObject和NSProxydealloc方法為自定義的dealloc方法

  • 2、為了避免內存空間釋放后被重寫造成野指針的問題,通過字典存儲被釋放的對象,同時設置在30s后調用dealloc方法將字典中存儲的對象釋放,避免內存增大

  • 3、為了獲取更多的崩潰信息,這里同樣需要創建NSProxy的子類

具體實現

  • 1、創建NSProxy的子類,其實現與上面的MIZombieProxy是一模一樣的

  • 2、hook dealloc函數的具體實現

<!--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
  • 3、測試
@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運行結果

參考文章

補充

github源碼鏈接

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容