野指针就是指向一个已删除的对象或者受限内存区域的指针。 咱们写C++的时候强调指针初始化为NULL,强调用完后也为其赋值为NULL,谁分配的谁回收,来避免野指针的问题。 比较常见的就是这个指针指向的内存,在别处被回收了,可是这个指针不知道,依然还指向这块内存。 MRC 时代由于引用计数手动控制,因此内存很容易在别处被回收。ARC解决了大部分这种问题。、 在iOS9以前,系统库的delegate
和target-action
有一部分是assign(unsafe_unretain)
的形式,这时候若是内存在别处被回收了,也是会出现野指针的。 因此iOS9以后这些地方就改为了weak内存修饰符,内存被回收的时候经过weak表,把这些指针设为nil。也大幅度减小了野指针的出现。c++
若是如今在工程中依然频繁出现野指针,几乎能够确定是错误地使用了内存。git
对于Mach
、Unix
、NSException
三种不一样层级的crash,NSException比较好说,能够直接定位到OC代码。问题主要来自EXC_BAD_ACCESS(SIGSEGV)
这种异常,难以在咱们的应用代码中定位。github
实际咱们遇到Mach Exception
绝大部分都是野指针的问题。SIGSEGV/SIGABRT/SIGTRAP 比较多见。 野指针问题表现千奇百怪,并且由于崩溃的地方并非形成野指针的地方,并且难以重现,因此问题每每难以定位。 macos
Mach Exception
。
这是目前帮助最大的调试模式。实现原理就是 hook 住了对象的dealloc方法,经过调用本身的__dealloc_zombie
方法来把对象进行僵尸化。xcode
id object_dispose(id obj) {
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
复制代码
正常的对象释放方法如上,可是僵尸对象调用了objc_destructInstance
后就直接return了,再也不free(obj);
。同时生成一个"_NSZombie_" + clsName
类名,调用objc_setClass(self, zombieCls);
修改对象的 isa 指针,令其指向特殊的僵尸类。 若是这个对象再次收到消息,objc_msgsend
的时候,调用abort()崩溃并打印出调用的方法。bash
野指针指向的内存没有被覆盖的时候,或者被覆盖成能够访问的内存的时候,不必定会出现崩溃。这个时候向对象发送消息,不必定会崩溃(可能恰好有这个方法),或者向已经释放的对象发送消息。 可是若是野指针指向的是僵尸对象,那就必定会崩溃了,会崩溃在僵尸对象第一次被其它消息访问的时候。app
僵尸对象必须在链接Xcode中debug的时候使用,若是咱们想跟咱们的崩溃收集工具集成在一块儿,就须要本身实现相似Zombie Object的东西。 逻辑是经过hook住NSObject的根类的dealloc方法,而后在新的dealloc方法中将原本即将释放的对象的isa指针改成指向咱们建立的一个新的僵尸类。iphone
iOS使用代码排查野指针错误 和 开发本身的NSZombie这两篇文章里介绍了在代码里实现相似Zoombie Object的方法,然而其实是没法使用的,这两种实现跟 Zombie Object 实现上不小的区别,实际应用中有大量误判的状况。ide
误判的缘由主要是dealloc的实现和僵尸类的实现跟Zombie Object不同。 参考Apple的源码,能够看到Apple是彻底调用了objc_destructInstance
函数的。而其它人的实现要么没有调用这个函数,要么只作了一部分。对于一个OC对象的dealloc来讲,主要包括两部分,一部分是objc_destructInstance
,一部分是free(self)
。objc_destructInstance
里包括了移除弱引用,移除关联对象,c++析构等等。这些逻辑不能省略。函数
- (void)dealloc
{
const char *className = object_getClassName(self);
char *zombieClassName = NULL;
do {
//...
Class zombieClass = objc_getClass(zombieClassName);
objc_destructInstance(self); //关键
object_setClass(self, zombieClass);
} while (0);
if (zombieClassName != NULL)
{
free(zombieClassName);
}
}
复制代码
而对于僵尸类的实现,Zombie Object的实现简洁并且有效。不像其它人的实现那么臃肿。就只是申明了一个没有任何方法的根类而已,因此任何消息发给它都会crash。
NS_ROOT_CLASS
@interface _NSZombie_ {
Class isa;
}
@end
复制代码
因此我从Apple的源码中提取出来的一套实现NSZombie,跟Zombie Object的实现保证彻底一致,解决误判的状况。
Scribble 工具可以在alloc的时候填上0xAA,dealloc的时候填上0x55,就是对象释放后在内存上填上不可访问的数据,若是再次访问对象就会必现crash。
Bugly的这篇文章如何定位Obj-C野指针随机Crash 就是采用这种方式提升crash率,来方便定位问题。 为了避免限制在xcode中使用,本身在代码中实现了相似的逻辑。经过fishhook去hook free
函数的方法,实现以下:
void safe_free(void* p){
size_tmemSiziee=malloc_size(p);
memset(p,0x55, memSiziee);
orig_free(p);
return;
}
复制代码
虽然已经给被释放的对象写上了0x55,可是若是是内存在被访问(触发crash)以前被其它覆盖了,则可能没法触发crash。 这种状况也很多见。 因此Bugly为了内存不被覆盖,就再也不调用free来释放这个内存。保持这个内存一直在。 这样的原理就很是相似Zombie Object
了。
制造crash的方式也是采用修改rsa指针的方式,当对象收到消息的时候abort()。
将malloc/free
函数进行了替换。在malloc函数中额外的分配了禁止访问区域的内存。 在free函数中将全部分配的内存区域设为禁止访问,并放到了隔离区域的队列中(保证在必定的时间内不会再被malloc函数分配)。 若是访问到禁止访问的区域,就直接crash。
对CPU影响2~5⨉, 增长内存消耗 2~3⨉。
可以检查出来的问题:
不能用于检查内存泄漏。有些文章说ASan能检查内存泄漏是不对的,Google的LSan能够,可是Xcode的Asan不行。
以前介绍的工具都是提升崩溃几率,以拿到崩溃的对象和内存地址。拿到崩溃的对象以后也很难定位,由于崩溃地方离释放的地方已经很远了。并且有些对象在工程中初始化了不少个,不知道是对应的哪一个地方出了问题。因此若是能知道对象是在哪初始化的就行了。 Malloc Stack 可以记录下来全部对象的malloc调用时的堆栈信息。而后咱们执行命令:
script import lldb.macosx.heap
malloc_info --stack-history 0x7fbf0dd4f5c0
复制代码
就能够在lldb中打印出来该对象初始化位置的堆栈信息。 Malloc Stack可是有两个巨大的缺点,一个是只能在模拟器上使用,第二是没有打印出dealloc的信息。若是想在真机上使用须要越狱。
公司内部的大神开发的的lldb插件,基于Malloc Stack开发的,经过调用私有函数拿到Malloc Stack记录的数据。可以支持真机调试,可以打印出dealloc的堆栈信息。 能打印出dealloc的缘由是hook了-dealloc方法,调用__disk_stack_logging_log_stack
函数记录当前的堆栈信息。
遇到的这个例子多是比较经典的野指针,崩溃日志中出现了各类各样的表现。
第一种表现是dealloc对象时崩溃:
0 libsystem_kernel.dylib 0x252fac5c __pthread_kill + 4
1 libsystem_c.dylib 0x2528f0ac abort + 103
2 libsystem_malloc.dylib 0x25324ef6 free + 431
3 libobjc.A.dylib 0x24e13e08 object_dispose + 19
4 Foundation 0x25de3cf2 -[NSIndexPath dealloc] + 66
5 libobjc.A.dylib 0x24e24f66 objc_object::sidetable_release(bool) + 150
6 libsystem_blocks.dylib 0x25243ac2 _Block_release + 215
7 CoreFoundation 0x25583384 -[__NSArrayI dealloc] + 64
5 libobjc.A.dylib 0x24e24f66 objc_object::sidetable_release(bool) + 150
9 UIKit 0x29e934f2 __runAfterCACommitDeferredBlocks + 310
10 UIKit 0x29e9f7da __cleanUpAfterCAFlushAndRunDeferredBlocks + 90
11 UIKit 0x29bddb1c __afterCACommitHandler + 84
复制代码
能够看到这里彻底是系统library的崩溃,跟工程代码毫无关系,最开始也是一头雾水。 这里只有两个线索,一个是NSIndexPath
,另外一个是只发生于10.3.3以前的iphone5机型上。 由于10.3.3是iphone5支持的最后一个版本,因此用户量并很多。
第二种表现是objc_msgsend, isEqual:
是经过读取ARM寄存器lr
获取到的方法名,这个是Bugly帮咱们查到的。
0 libobjc.A.dylib 0x1a1b0dd6 objc_msgSend (isEqual:) + 15
1 UIKit 0x201afdfa -[UICollectionReusableView _setLayoutAttributes:] + 60
2 UIKit 0x209d0280 -[UICollectionView _applyLayoutAttributes:toView:] + 138
3 UIKit 0x209daf26 ___88-[UICollectionView _dequeueReusableViewOfKind:withIdentifier:forIndexPath:viewCategory:]_block_invoke + 28
4 UIKit 0x2015b5c2 +[UIView(Animation) performWithoutAnimation:] + 84
5 UIKit 0x209dae40 -[UICollectionView _dequeueReusableViewOfKind:withIdentifier:forIndexPath:viewCategory:] + 2156
6 UIKit 0x201af68a -[UICollectionView dequeueReusableCellWithReuseIdentifier:forIndexPath:] + 160
7 XXXXXXProject 0x00404c02 -[XXXXXXCollectionView collectionView:cellForItemAtIndexPath:] (XXXXXXClass.m:77)
8 UIKit 0x209cf850 -[UICollectionView _createPreparedCellForItemAtIndexPath:withLayoutAttributes:applyAttributes:isFocused:notify:] + 420
9 UIKit 0x201af5e0 -[UICollectionView _createPreparedCellForItemAtIndexPath:withLayoutAttributes:applyAttributes:] + 42
10 UIKit 0x201ad7f6 -[UICollectionView _updateVisibleCellsNow:] + 4076
11 UIKit 0x201a83d6 -[UICollectionView layoutSubviews] + 398
12 UIKit 0x2014b482 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1224
复制代码
这里线索就比较丰富,能够找到对应的类了,XXXXXXProject
是咱们的工程,明显崩溃在UICollectionView
中。在重用collectionViewCell的过程当中,调用_setLayoutAttributes
的方法,在+60的位置调用了isEqual:
,通过反编译这个方法得知调用isEqual:
的对象的是UICollectionViewLayoutAttributes
(反编译过程省略)。 这里也是只发生于10.3.3以前的iphone5机型上。因此基本肯定是同一个问题。
可是并无什么了卵用,正如以前所说的,野指针崩溃的地方跟出错的地方相去甚远。 惟一能肯定的地方,就是引发崩溃的对象是NSIndexPath
。
第三种表现比较奇怪,报[UITransitionView initialize] unrecognized selector
,这个类一脸懵逼。不知道在哪使用过
Exception Type: NSInvalidArgumentException(SIGABRT)
Exception Codes: -[UITransitionView initialize]: unrecognized selector sent to instance 0x165f22c0 at 0x1c4d1acc
Crashed Thread: 0
0 CoreFoundation 0x1cd03b3d ___exceptionPreprocess + 129
1 libobjc.A.dylib 0x1bf8b067 objc_exception_throw + 31
2 CoreFoundation 0x1cd08fd1 ___methodDescriptionForSelector + 1
3 CoreFoundation 0x1cd070c3 ____forwarding___ + 697
4 CoreFoundation 0x1cc2fdc8 _CF_forwarding_prep_0 + 24
5 libobjc.A.dylib 0x1bf8bbad _CALLING_SOME_+initialize_METHOD + 23
6 libobjc.A.dylib 0x1bf8bdf3 __class_initialize + 579
7 libobjc.A.dylib 0x1bf92c15 _lookUpImpOrForward + 173
8 libobjc.A.dylib 0x1bf92b65 __class_lookupMethodAndLoadCache3 + 27
9 libobjc.A.dylib 0x1bf991af __objc_msgSend_uncached + 15
10 UIKit 0x21f98167 -[UICollectionViewLayoutAttributes isEqual:] + 95
11 UIKit 0x21f97dfb -[UICollectionReusableView _setLayoutAttributes:] + 61
12 UIKit 0x227b8281 -[UICollectionView _applyLayoutAttributes:toView:] + 139
13 UIKit 0x227c2f27 ___88-[UICollectionView _dequeueReusableViewOfKind:withIdentifier:forIndexPath:viewCategory:]_block_invoke + 29
14 UIKit 0x21f435c3 +[UIView(Animation) performWithoutAnimation:] + 85
15 UIKit 0x227c2e41 -[UICollectionView _dequeueReusableViewOfKind:withIdentifier:forIndexPath:viewCategory:] + 2157
16 UIKit 0x21f9768b -[UICollectionView dequeueReusableCellWithReuseIdentifier:forIndexPath:] + 161
复制代码
看下面的堆栈就发现仍是同一个问题,可是为啥会报这么奇怪的错? 这就是野指针的表现。这一块内存被别的东西覆盖了。
实际上还有其它的表现,可是比较具备表明性的就这三个了。从崩溃日志中只能获得有限的信息,一个是这个是野指针问题。第二个是这个野指针对象极可能是一个NSIndexPath
对象(也不能彻底肯定)。
若是不知道是野指针的问题,就很容易误入歧途,花大量时间在研究UICollectionView
或者在研究UITransitionView
上。其实都是浪费时间,由于形成野指针的地方地方已经很远了。
正如Bugly这篇文章说的,定位野指针最重要仍是增大野指针出现的几率。 因此此次我是采用Zombie Object,而且限制在iPhone5 和 iOS10.3.3的状况下重现的。
通过屡次重现,肯定了是NSIndexPath
的问题,并且全部的UICollectionView
和UITableView
都受到了影响。因此我开始怀疑是否是工程中有全局的代码被hook了。果真不出所料:
- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
if (kiOS9Later) {
if ([NSStringFromSelector(invocation.selector) isEqualToString:@"collectionView:didSelectItemAtIndexPath:"]) {
//无痕打点
__unsafe_unretained UICollectionView *collectionView = nil;
id indexPath;
[invocation getArgument:&collectionView atIndex:2];
[invocation getArgument:&indexPath atIndex:3];
[FPPVHelper reportMTAEventId:[collectionView hotTagId] Index:[indexPath row] info:nil];
}
}
}
复制代码
这是某一段神奇的打点代码,不知道谁写的。很明显indexPath
此处的修饰符应为__unsafe_unretained
,若是为strong
的话对象在这里就会被ARC释放掉,然而由于传递的是C指针,其它地方的某个指针不知道这里释放了,依然指向了这里。产生了野指针。
在iOS9以前的tableview的delegate和datasource都是assign
内存修饰符的。iOS9以后才使用weak
。
// iOS 8 以前
@property(nonatomic, assign) id<UITableViewDataSource> dataSource
@property(nonatomic, assign) id<UITableViewDelegate> delegate
// iOS 9 以后
@property(nonatomic, weak, nullable) id<UITableViewDataSource> dataSource
@property(nonatomic, weak, nullable) id<UITableViewDelegate> delegate
复制代码
这种状况,若是delegate
比tableview
自己更早被释放,此时的dataSource
就会成为一个野指针。常见的状况好比block调用延长了tableview的生命周期,就可能会发生这种状况,致使野指针crash。 通常崩溃日志里是objc_msgsend + 15
的崩溃,崩溃在delegate或者datasource的方法里。
解决方法也很简单,在dealloc的时候把dataSource和delegate设为nil便可。
- (void)dealloc
{
_tableView.delegate = nil;
_tableView.dataSource = nil;
}
复制代码
崩溃堆栈也是最多见的objc_msgSend,这里能够看到是工程中hook的某个方法崩溃了
libobjc.A.dylib objc_msgSend (pv_gestureRecongizerAction:)
UIKit -[UIGestureRecognizer _updateGestureWithEvent:buttonEvent:]
UIKit ____UIGestureRecognizerUpdate_block_invoke662
UIKit __UIGestureRecognizerRemoveObjectsFromArrayAndApplyBlocks
UIKit __UIGestureRecognizerUpdate
SEGV_ACCERR
复制代码
咱们本身的代码以下,就是在addGestureRecognizer
方法中加了一层调用,加了一层target-action。这至关因而给gestureRecognizer
加了两个target-action
-(void)pv_addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {
[gestureRecognizer addTarget:self action:@selector(pv_gestureRecongizerAction:)];
[self pv_addGestureRecognizer:gestureRecognizer];
}
复制代码
因为target对于gesture来讲在iOS8上也是相似assign的,因此这里就是self被释放了,变成野指针了,可是gestureRecognizer的target依然指向了self的内存。 当self已经被释放了,可是gestureRecognizer还没被释放的时候就会发生这种状况。
野指针定位有几个关键:
Mach Exception
大多数都是野指针的问题,崩溃日志里最常见objc_msgSend
和unrecognized selector sent to
等等。并且每每跟iOS SDK版本和iphone型号有关。 认识到野指针的问题后,就没必要要拘泥于崩溃日志,由于崩溃的地方离崩溃的缘由比较远了。Zombie Object/Scribble/Aasn
均可以。我的认为本身实现的Zombie Object最好,既能够脱离Xcode debug的限制,使用又比较简单。malloc stack/lzMalloc
找到野指针指向对象初始化的位置和dealloc的位置,判断是否过早释放等。