原文连接html
当所指向的对象被释放或者收回,可是对该指针没有做任何的修改,以致于该指针仍旧指向已经回收的内存地址,此状况下该指针便称野指针git
野指针异常堪称crash界
的半壁江山,相比起NSException
而言,野指针有这么两个特色:github
随机性强
web
尽管大公司已经有各类单元、行为、自动化以及人工化测试,尽可能的去模拟用户的使用场景,但野指针异常老是能巧妙的避开测试,在线上大发神威。缘由毫不仅仅在于测试没法覆盖全部的使用场景算法
形成野指针是多样化的:首先内存被释放后不表明内存会马上被覆写
或者数据受到破坏
,这时候访问这块内存也不必定会出错。其次,多线程技术
带来了复杂的应用运行环境,在这个环境下,未加保护的数据多是致命的。此外,设计不够严谨的代码
一样也是形成野指针异常的重要缘由之一安全
难以定位
多线程
NSException
是高抽象层级上的封装,这意味着它能够提供更多的错误信息给咱们参考。而野指针几乎出自于C语言
层面,每每咱们能得到的只有系统栈信息,单单是定位错误代码位置已经很难了,更不要说去重现修复架构
解决野指针最大的难点在于定位。一般线上出现了crash
须要修复时,开发者最重要的一个步骤是重现crash
。而上文提到了野指针的两个特性会阻碍咱们定位问题,对于这两个特性,确实也能作一些对应的处理来下降它们的干扰性:ide
采集辅助信息
函数
辅助信息包括设备信息、用户行为等信息,每每能够用来重现问题。好比用户行为能够造成用户使用路径
,从而重现用户使用场景。而在发生crash
时,采集当前页面信息,配合用户使用路径
能够快速的定位到问题发生的大概位置。通过验证,辅助信息
确实有效的减小了系统栈
对于问题重现的干扰
提升野指针崩溃率
因为野指针不必定会发生崩溃这一特性,即使咱们经过堆栈信息
和辅助信息
肯定了大体范围,不表明咱们能顺利的重现crash
。一个优秀的野指针崩溃能够形成一天开发,三天debug
,假如野指针的崩溃不是随机的,那么问题就简单的多
Xcode
提供了Malloc Scribble
对已释放内存进行数据填充,从而保证野指针访问是必然崩溃的。另外,Bugly
借鉴这一原理,经过修改free
函数,对已释放对象进行非法数据填充,也有效的提升了野指针的崩溃率
Zombie Objects
Zombie Objects
是一种彻底不一样的野指针调试机制,将释放的对象标记为Zombie
对象,再次给Zombie
对象发送消息时,发生crash
而且输出相关的调用信息。这套机制同时定位了发生crash
的类对象以及有相对清晰的调用栈
整理一下上述的内容,能够看到目前存在辅助信息+对象内存填充
以及Zombie Objects
这两种主要的应对方式。拿前者来讲,填充已释放对象的内存风险高,通过尝试Xcode9
的Malloc Scribble
启动后已经不会填充对象的内存地址。其次,填充内存须要去hook
更加底层的API
,这意味着对代码能力要求更高。所以,借鉴Zombie Objects
的实现思路去定位野指针异常是一个可行的方案
转发是一项有趣的机制,它经过在通讯双方中间,插入一个中间层。发送方再也不耦合接收方,它只须要将数据发送给中间层,由中间层来派发给具体的接收方。基于转发的思想,能够作许多有趣的东西:
消息转发
iOS
的消息机制让咱们能够给对象发送一个未注册的消息,一般这会引起unrecognized selector
异常。可是在抛出异常以前,存在一个消息转发
机制,容许咱们从新指定消息的接收方来处理这个消息。正是这一机制实现了防unrecognized selector crash
的可行化
打破引用环
循环引用是ARC
环境下最容易出现的内存问题,当多个对象之间的引用造成了引用环时,极有可能会致使环中的对象都没法被释放。借鉴Proxy
的方式,能够实现破坏引用环的做用。XXShield以插入WeakProxy
层的方式实现了防crash
路由转发
组件化是项目体量达到必定程度时必须考虑的架构方案,将项目拆分基础组件和业务组件,加入中间层实现组件间解耦的效果。因为业务组件之间互不依赖,所以须要合适的方案实现组件通讯,路由设计是一种经常使用的通讯方式。各个模块实现canOpenURL:
接口来判断是否处理对应的跳转逻辑,模块将参数信息拼接在url
中传递:
都说消息发送
是Objective-C
的核心机制,任何一个对象方法调用都会被转换成objc_msgSend
的方式执行。这一过程当中涉及到一个重要的变量:isa
指针。多数开发者对isa
指针停留在它指向了类的类结构自己的地址,用来表示对象的类型。可是实际上isa
指针要比咱们想一想的复杂的多,好比objc_msgSend
依赖于isa
来完成消息的查找,经过阅读经过汇编解读 objc_msgSend能够了解更详细的匹配过程:
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
};
复制代码
因为方法调用与isa
指针相关,所以若是咱们修改一个类的isa
指针使其指向一个目标类,那么能够实现对象方法调用的拦截,也能够称做对象方法转发。咱们并不能直接修改isa
指针,但runtime
提供了一个object_setclass
接口容许咱们动态的对某个类进行重定位
ClassA
被重定位成ClassB
须要保证两个类的内存结构是对齐的,不然可能会发生超出意外的问题
通常来讲咱们都不该该违背重定位类的内存结构对齐原则。但在野指针问题中,对象拥有的内存被释放后是不肯定状态,所以作适当的破坏
并不必定是坏事,只是记住在最终释放对象内存时,应当再次重定位回来,防止内存泄漏的风险
借鉴于Zombie Objects
的机制,咱们能够实现一套类Zombie Proxy
机制。经过重定位类型
的作法,在对象dealloc
以前将其isa
指针指向一个目标类,实现后续调用的转发。而目标类中全部的方法调用都采用NSException
的机制抛出异常,而且输出调用对象的实际类型和调用方法帮助定位:
重定位后的类因为其实际用于转发的用途,更符合Proxy
的属性,所以我将其设置为NSProxy
的子类,多数人可能不知道iOS
一共有NSProxy
跟NSObject
两个根类。另外,为了实现对retain
等内存管理相关方法的重写,目标类应该设置为不支持ARC
:
@interface LXDZombieProxy : NSProxy
@property (nonatomic, assign) Class originClass;
@end
@implementation LXDZombieProxy
- (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];
}
#define LXDZombieThrowMesssageSentException() [self _throwMessageSentExceptionWithSelector: _cmd]
- (id)retain
{
LXDZombieThrowMesssageSentException();
return nil;
}
- (oneway void)release
{
LXDZombieThrowMesssageSentException();
}
- (id)autorelease
{
LXDZombieThrowMesssageSentException();
return nil;
}
- (void)dealloc
{
LXDZombieThrowMesssageSentException();
[super dealloc];
}
- (NSUInteger)retainCount
{
LXDZombieThrowMesssageSentException();
return 0;
}
@end
复制代码
因为iOS
的方法其实是以向上调用
的链式机制实现的,所以只须要hook
掉两个根类的dealloc
方法就能保证对对象类型的重定位。在hook
掉dealloc
以后有几个须要注意的点:
对象的释放
因为咱们须要实现转发机制,这表明着本该释放的对象在类型重定位后不能被释放。随着时候时间的推移,重定位类对象的数量会愈来愈多。根据经验来讲,通常的野指针在30s
内被再次访问的几率很大,所以咱们能够在类型重定位完成后延后30s
释放对象。或者能够构建一个Zombie Pool
,当内存占用达到必定大小时,使用恰当的算法淘汰
白名单机制
并非全部的类对象都被监控,好比系统私有类
、监控相关工具类
、明确不存在野指针的类
等。咱们须要一个全局的白名单系统,来确保这些类的dealloc
是正常执行的,无需被转发
潜在的crash
经过method_setImplementation
替换dealloc
的代码实现,因为我采用block
转IMP
的方式来实现的方式,会对捕获的外界对象进行引用。而对象在重定位后,任何调用都会引起crash
,所以须要针对这种状况作对应的处理
为了知足保证对象可以在达成释放条件完成内存的回收,须要存储根类的dealloc
原实现,以根类类名做为key
存储在全局字典中。而且提供接口__lxd_dealloc
来完成对象的释放工做:
static inline void __lxd_dealloc(__unsafe_unretained id obj) {
Class currentCls = [obj class];
Class rootCls = currentCls;
while (rootCls != [NSObject class] && rootCls != [NSProxy class]) {
rootCls = class_getSuperclass(rootCls);
}
NSString *clsName = NSStringFromClass(rootCls);
LXDDeallocPointer deallocImp = NULL;
[[_rootClassDeallocImps objectForKey: clsName] getValue: &deallocImp];
if (deallocImp != NULL) {
deallocImp(obj);
}
}
NSMutableDictionary *deallocImps = [NSMutableDictionary dictionary];
for (Class rootClass in _rootClasses) {
IMP originalDeallocImp = __lxd_swizzleMethodWithBlock(class_getInstanceMethod(rootClass, @selector(dealloc)), swizzledDeallocBlock);
[deallocImps setObject: [NSValue valueWithBytes: &originalDeallocImp objCType: @encode(typeof(IMP))] forKey: NSStringFromClass(rootClass)];
}
复制代码
在对象的dealloc
被调起以后,检测对象类型是否存在白名单中。若是存在,直接继续完成对对象的释放工做。不然的话,延后30s
进行释放工做。为了解除block
引用形成的crash
,使用NSValue
存储对象信息以及使用__unsafe_unretained
来防止临时变量的引用:
swizzledDeallocBlock = [^void(id obj) {
Class currentClass = [obj class];
NSString *clsName = NSStringFromClass(currentClass);
/// 若是为白名单,则不重定位类的类型
if ([__lxd_sniff_white_list() containsObject: clsName]) {
__lxd_dealloc(obj);
} else {
NSValue *objVal = [NSValue valueWithBytes: &obj objCType: @encode(typeof(obj))];
object_setClass(obj, [LXDZombieProxy class]);
((LXDZombieProxy *)obj).originClass = currentClass;
/// 延后30秒释放对象,避免形成内存的浪费
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(30 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
__unsafe_unretained id deallocObj = nil;
[objVal getValue: &deallocObj];
object_setClass(deallocObj, currentClass);
__lxd_dealloc(deallocObj);
});
}
} copy];
复制代码
具体的实现代码能够下载LXDZombieSniffer
野指针问题是访问了非法内存致使的crash
,也就是说要符合两个条件:内存非法
以及指针地址不为NULL
。在iOS
中存在三种不一样修饰的指针:
__strong
默认修饰符。修饰的指针在赋值以后,会对指向的对象执行一次retain
操做,指针不因对象的生命周期变化而改变
__unsafed_unretained
非安全对象指针修饰符。修饰的指针不会持有指向对象,也不因对象的生命周期发生变化而改变,等同于assign
__weak
弱对象指针修饰符。修饰的指针不会持有指向对象,在对象的生命周期结束而且内存被回收时,修饰的指针内容会被重置为nil
根据野指针异常的引起条件来讲,三种修饰指针只有__strong
和__unsafed_unretained
能够致使野指针访问异常。可是在使用类别重定位
以后,本该释放的对象会被延时或者不释放,也就是本该被重置的弱指针也不会发生重置,这时使用弱指针访问对象应该会被转发到ZombieProxy
当中发生crash
:
__weak id weakObj = nil;
@autoreleasepool {
NSObject *obj = [NSObject new];
weakObj = obj;
}
/// The operate should be crashed
NSLog(@"%@", weakObj);
复制代码
然而在上面的测试中,发现即使对象被重定位为Zombie
而且被阻止释放以后,weakObj
依旧被成功的设置成了nil
。而后通过objc_runtime源码运行和添加断点测试以后,也没有weak
指针被重置的调用。甚至使用了LLVM
的watch set var weakObj
监控弱指针,依旧没法找到调用。但weakObj
在dealloc
调用以后,无论对象有没有被释放,都被重置成了nil
。这也是截止文章出来为止,匪夷所思的疑难杂症