前言 本文主要讲解两种 野指针检测
的原理和实现
技术点:野指针探测 本文的主要目的是理解 野指针
的形成过程以及如果去 检测野指针
引子 在介绍野指针之前,首先说下目前的异常处理类型,附 苹果官网链接
异常类型 异常类型分为两类:
而处理异常时,需要关注两个概念
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; }
野指针 指向的 对象被释放或收回
,但是该 指针没有做任何的修改
,以至于 该指针仍指向已经回收的内存地址
,这个指针就是 野指针
野指针分类 这个参考腾讯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 思路:当访问到对象内存中填写的是 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从不必现变成 必现
发生crash时,得到的崩溃信息有限,不利于排查问题,所以这里采用代理类(即集成自 NSProxy的子类),重写消息转发的三个方法(参考这篇文章 OC底层原理14-3:消息流程分析之动态方法决议 & 消息转发 ),以及NSObject的实例方法,来获取异常信息。但是这样的话,还有一个问题,就是NSProxy只能做OC对象的代理,所以需要在safe_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 <!--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
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 僵尸对象
苹果的僵尸对象检测原理
首先我们来看下Xcode中僵尸对象是如何实现的,具体操作步骤可以参考这篇文章 iOS Zombie Objects(僵尸对象)原理探索
从 dealloc
的源码中,我们可以看到 Replaced by NSZombie
,即 对象释放
时,NSZombie
将 dealloc
里做替换,如下所示
所以僵尸对象的生成过程伪代码如下
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 的源码,其主要思路是:
野指针检测流程
触发野指针
通过僵尸对象检测的实现思路
通过OC中 Method Swizzling,交换 根类NSObject和NSProxy 的 dealloc 方法为 自定义的dealloc 方法
为了 避免内存空间释放后被重写造成野指针 的问题,通过 字典存储被释放的对象,同时设置在 30s后调用dealloc方法将字典中存储的对象释放,避免内存增大
为了获取更多的崩溃信息,这里同样需要创建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 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); }
打印崩溃信息如下