点击上方“iOS开发”,选择“置顶公众号”

关键时刻,第一时间送达!

野指针

当所指向的对象被释放或者收回,但是对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,此情况下该指针便称野指针

野指针异常堪称crash界的半壁江山,相比起NSException而言,野指针有这么两个特点:

  • 随机性强

尽管大公司已经有各种单元、行为、自动化以及人工化测试,尽量的去模拟用户的使用场景,但野指针异常总是能巧妙的避开测试,在线上大发神威。原因绝不仅仅在于测试无法覆盖所有的使用场景

造成野指针是多样化的:首先内存被释放后不代表内存会立刻被覆写或者数据受到破坏,这时候访问这块内存也不一定会出错。其次,多线程技术带来了复杂的应用运行环境,在这个环境下,未加保护的数据可能是致命的。此外,设计不够严谨的代码同样也是造成野指针异常的重要原因之一

  • 难以定位

NSException是高抽象层级上的封装,这意味着它可以提供更多的错误信息给我们参考。而野指针几乎出自于C语言层面,往往我们能获得的只有系统栈信息,单单是定位错误代码位置已经很难了,更不要说去重现修复

定位

解决野指针最大的难点在于定位。通常线上出现了crash需要修复时,开发者最重要的一个步骤是重现crash。而上文提到了野指针的两个特性会阻碍我们定位问题,对于这两个特性,确实也能做一些对应的处理来降低它们的干扰性:

  • 采集辅助信息

辅助信息包括设备信息、用户行为等信息,往往可以用来重现问题。比如用户行为可以形成用户使用路径,从而重现用户使用场景。而在发生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。这也是截止文章出来为止,匪夷所思的疑难杂症

  • 作者:sindri的小巢

  • 链接:http://www.jianshu.com/p/4c8a68bd066c

  • iOS开发整理发布,转载请联系作者授权

【点击成为Java大神】

iOS监控-野指针定位相关推荐

  1. iOS 野指针定位:野指针嗅探器

    程序员大咖 点击右侧关注,免费进阶高级! 作者:林大鹏天地 链接:https://www.jianshu.com/p/9fd4dc046046 一. 前言 最近最近被指派去解决一些线上的崩溃问题,经常 ...

  2. iOS野指针定位总结

    成因 野指针就是指向一个已删除的对象或者受限内存区域的指针. 我们写C++的时候强调指针初始化为NULL,强调用完后也为其赋值为NULL,谁分配的谁回收,来避免野指针的问题. 比较常见的就是这个指针指 ...

  3. iOS---------关于野指针定位总结

    iOS野指针定位总结 转载于:https://www.cnblogs.com/KiVen2015/p/11202055.html

  4. iOS多线程同时操作同一内存造成野指针

    iOS多线程同时操作同一内存造成野指针 iOS多线程同时操作同一内存造成野指针,原因:崩溃线程崩中使用指针的真正创建与销毁地方在另另外一个线程中,崩溃线程只是使用这个指针拷贝. 这两个操作发送在两个线 ...

  5. exc_bad_access(code=1, address=0x789870)野指针错误

    原因: exc_bad_access(code=1, address=0x789870)野指针错误,主要的原因是,当某个对象被完全释放,也就是retainCount,引用计数为0后.再去通过该对象去调 ...

  6. OC-引用计数器,内存管理,野指针

    总结 全局断点 →-->+-->Add Exception Breakpoint 开启僵尸监听 打开Edit scheme -->Diagnostics-->Enable Zo ...

  7. linux野指针追踪,一个erlang nif野指针的追踪过程

    概述 最近半年, 经常出现一些奇怪的bug. CPU非常高, 但etop并没有red特别高的进程. 内存占用非常高, 和etop看到的内存不吻合. coredump. 因为erlang层面无法定位问题 ...

  8. C语言中“野指针”、“悬空指针”是什么?

    目录 1."野指针"(wild pointer) 2."悬空指针"(dangling pointer) 1."野指针"(wild point ...

  9. C中的野指针—如何避免

    转自:http://www.cnblogs.com/viviwind/archive/2012/08/14/2638810.html 先看一个例子: struct student{ char* nam ...

最新文章

  1. POSIX正则表达式 验证电子邮件地址
  2. python学习之 字符串前'r'的用法
  3. C#打印0到100的素数
  4. sql server数据库查询超时报错
  5. 嵌入式linux系统移植的四大步骤_如何移植开源软件到嵌入式Linux系统
  6. java config 类_Spring ----JavaConfig类代替XML配置Bean
  7. java中tcp传图片_Java学习之TCP上传图片
  8. Hyper-V 3.0服务器虚拟化:打造坚固的云
  9. 十三、mysql 分区之 RANGE LIST
  10. netbean的安装及jdk安装和环境变量设置
  11. 《大秦帝国》:中国文明正源的强势生存
  12. Java--------面向对象
  13. Android studio MacBook快捷键
  14. Stone Ocean(https://acs.jxnu.edu.cn/problem/GYM103495E)
  15. MugLife静态照片变3D动画算法研究
  16. ROS和ROS2.0到底该用哪个呢?
  17. 一些著名软件的开发语言概述
  18. apk系统签名小技巧
  19. 干货分享|串流游戏软件大比拼
  20. 凸包问题-Graham-Scan算法-python实现

热门文章

  1. 大连文思海辉php面试题,文思海辉前端面试题
  2. 20-10-28 安装PHPBB3论坛和Joomla网站
  3. vue3 组件篇 Affix
  4. 2021---长安“战疫”网络安全卫士守护赛 Writeup
  5. 【opencv】目标跟踪之MOSSE算法配合模板匹配实现初始滤波器的自动初始化
  6. john the ripper密码破解工具
  7. 问答搜索 整合全网问答平台
  8. 干货!用神经网络来表达隐式场景流
  9. BI的作用,体现在企业的哪些方面
  10. Linux性能优化全景指南(建议收藏)