但是无论在生活中还是编程中,意外总会发生,通常情况下,在向操作系统申请内存这一步很少会出现问题,操作系统本身的稳定性比应用程序要强很多。问题大多出现在内存释放的时候。问题可能有两种:
一种是已经不需要使用的对象我们将指针变量直接清除了,但却没有告诉操作系统回收这块内存,此后程序中没有地方存储这块内存的地址,这块内存将永远无法使用和回收。这种情况下,这块内存就变成了无主内存且操作系统并不知道,就产生了我们常说的内存泄露问题,随着应用的运行时间越来越长,内存泄露可能越来越多最终导致内存不够用,程序无法再正常运行。
另一种是我们告诉操作系统要回收这块内存,并且这块内存也真正的被回收了,但是程序中依然有指针变量存储着这个地址没有清空,此时这个指针就变成了也指针,因为它所指向的内存已经回收,这块内存具体是又被利用了还是依然存放着原来的数据我们都一无所知。此后如果不小心又通过这个指针使用了这块内存的数据,无论读写都将产生各种千奇百怪的问题,且我们很难定位。本文我们主要聊的就是这类野指针问题的产生原因与定位方法。
#import <Foundation/Foundation.h>
@interface MyObject : NSObject
@property(copy) NSString *name;
@end
在ViewController类中编写如下测试代码:
#import "ViewController.h"
#import "MyObject.h"
@interface ViewController ()
@property (nonatomic, unsafe_unretained)MyObject *object;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
MyObject *object = [[MyObject alloc] init];
self.object = object;
self.object.name = @"HelloWorld";
void *p = (__bridge void *)(self.object);
NSLog(@"%p,%@",self.object,self.object.name);
NSLog(@"%p,%@",p, [(__bridge MyObject *)p name]);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%p",self->_object);
NSLog(@"%@",self.object.name);
}
@end
这里我们手动的造出了一个会出现野指针问题的场景,ViewController类的object属性声明为的unsafe_unretained,这个修饰符的意思是当前属性不被ARC所管理,其所引用的对象释放后,此指针也不会被置空。上面代码我们在viewDidLoad方法中创建了一个MyObject对象,并复制给了当前控制器的object属性,由于栈内对象的生命周期为当前代码块内有效,因此当viewDidLoad方法结束后,此内存就会被回收,此时object指针就成了野指针。
我们可以在viewDidLoad方法的最后打上断点,观察当前MyOject对象的内存分配地址,如下:
可以看到,当次运行时,object对象分配的内存地址为0x600001e542d0(每次运行都会不同),后面访问对象的属性实际上就是对此内存中数据的访问,我们如果知道了内存地址,也可以直接使用地址进行访问,不一定要有变量,例如上图中通过LLDB中的po指令可以直接向内存地址发送消息,效果和通过变量调用对象方法是一样的。
之后,我们可以在运行后点击一下当前页面,大部分情况下都会出现地址异常Crash,我们可以通过LLDB输出下线程的堆栈信息,如下:
还有时候,程序可能会直接Crash到main方法中,输入更奇怪的堆栈信息,如下:
如上图所示,堆栈信息提示我们调用了数组的name方法,这其实就是因为此块内存被重新分配了。
我们只创建了一个没有任何逻辑的Demo项目,野指针的问题都如此多样,如果是在实际项目中,出了野指针问题我们更难找到问题源头。并且在ARC环境下,上面示例的场景其实很好排查到,更多产生野指针的原因是多线程不安全的读写数据造成的,结合多线程使用,野指针的问题则更加难查。
再次运行工程,程序Crash后将输出如下信息:
*** -[MyObject retain]: message sent to deallocated instance 0x600000670670
我们可以明确的知道是MyObject对象的内存问题导致了野指针崩溃。
Xcode的僵尸对象功能虽然好用,但是只能调试时使用,更多时候我们产生的野指针问题都是线上环境的,而且无法复现,这个功能就显得非常鸡肋的。我们能否不依赖Xcode来实现野指针的监控呢?首先我们需要先搞明白Xcode中僵尸对象的实现原理。
// Replaced by NSZombies
- (void)dealloc {
_objc_rootDealloc(self);
}
从注释可以看到,系统实现的僵尸对象的确是处理dealloc方法了,推测其实通过Runtime替换了NSObject的dealloc方法。在CoreFoundation的源码中也有部分关于Zombies的内容,在CFRuntime.c中可以看到如下代码:
extern void __CFZombifyNSObject(void); // from NSObject.m
void _CFEnableZombies(void) {
}
其中,_CFEnableZombies比较好理解,它应该是来表示是否开启僵尸对象功能的,应该和我们在Xcode中设置的环境变量功能一致,__CFZombifyNSObject从注释可以知道,应该是对僵尸对象的实现。我们在Xcode中添加一个__CFZombifyNSObject的符号断点,断点后内容如下所示:
看到这里的汇编,你应该不会太陌生,我们把核心的伪代码提出来,大致如下:
// 定义字符串
define "NSObject"
// 用来获取NSObject类
objc_lookUpClass "NSObject"
// 定义字符串
define "dealloc"
define "__dealloc_zombie"
// 获取dealloc方法的实现
class_getInstanceMethod "NSObject" "dealloc"
// 获取__dealloc_zombie方法的实现
class_getInstanceMethod "NSObject" "__dealloc_zombie"
// 交换dealloc与__dealloc_zombie的方法实现
method_exchangeImplementations "dealloc" "__dealloc_zombie"
和我们想的差不多,下面我们可以再添加一个__dealloc_zombie的符号断点,看一看__dealloc_zombie方法是怎么实现的,如下:
CoreFoundation`-[NSObject(NSObject) __dealloc_zombie]:
-> 0x10ef77c49 <+0>: pushq %rbp
0x10ef77c4a <+1>: movq %rsp, %rbp
0x10ef77c4d <+4>: pushq %r14
0x10ef77c4f <+6>: pushq %rbx
0x10ef77c50 <+7>: subq $0x10, %rsp
0x10ef77c54 <+11>: movq 0x2e04fd(%rip), %rax ; (void *)0x0000000110021970: __stack_chk_guard
0x10ef77c5b <+18>: movq (%rax), %rax
0x10ef77c5e <+21>: movq %rax, -0x18(%rbp)
0x10ef77c62 <+25>: testq %rdi, %rdi
0x10ef77c65 <+28>: js 0x10ef77d04 ; <+187>
0x10ef77c6b <+34>: movq %rdi, %rbx
0x10ef77c6e <+37>: cmpb $0x0, 0x488703(%rip) ; __CFConstantStringClassReferencePtr + 7
0x10ef77c75 <+44>: je 0x10ef77d1d ; <+212>
0x10ef77c7b <+50>: movq %rbx, %rdi
0x10ef77c7e <+53>: callq 0x10eff4b52 ; symbol stub for: object_getClass
0x10ef77c83 <+58>: leaq -0x20(%rbp), %r14
0x10ef77c87 <+62>: movq $0x0, (%r14)
0x10ef77c8e <+69>: movq %rax, %rdi
0x10ef77c91 <+72>: callq 0x10eff464e ; symbol stub for: class_getName
0x10ef77c96 <+77>: leaq 0x242db5(%rip), %rsi ; "_NSZombie_%s"
0x10ef77c9d <+84>: movq %r14, %rdi
0x10ef77ca0 <+87>: movq %rax, %rdx
0x10ef77ca3 <+90>: xorl %eax, %eax
0x10ef77ca5 <+92>: callq 0x10eff4570 ; symbol stub for: asprintf
0x10ef77caa <+97>: movq (%r14), %rdi
0x10ef77cad <+100>: callq 0x10eff4ab0 ; symbol stub for: objc_lookUpClass
0x10ef77cb2 <+105>: movq %rax, %r14
0x10ef77cb5 <+108>: testq %rax, %rax
0x10ef77cb8 <+111>: jne 0x10ef77cd7 ; <+142>
0x10ef77cba <+113>: leaq 0x2427aa(%rip), %rdi ; "_NSZombie_"
0x10ef77cc1 <+120>: callq 0x10eff4ab0 ; symbol stub for: objc_lookUpClass
0x10ef77cc6 <+125>: movq -0x20(%rbp), %rsi
0x10ef77cca <+129>: movq %rax, %rdi
0x10ef77ccd <+132>: xorl %edx, %edx
0x10ef77ccf <+134>: callq 0x10eff4a62 ; symbol stub for: objc_duplicateClass
0x10ef77cd4 <+139>: movq %rax, %r14
0x10ef77cd7 <+142>: movq -0x20(%rbp), %rdi
0x10ef77cdb <+146>: callq 0x10eff482e ; symbol stub for: free
0x10ef77ce0 <+151>: movq %rbx, %rdi
0x10ef77ce3 <+154>: callq 0x10eff4a5c ; symbol stub for: objc_destructInstance
0x10ef77ce8 <+159>: movq %rbx, %rdi
0x10ef77ceb <+162>: movq %r14, %rsi
0x10ef77cee <+165>: callq 0x10eff4b6a ; symbol stub for: object_setClass
0x10ef77cf3 <+170>: cmpb $0x0, 0x48867f(%rip) ; __CFZombieEnabled
0x10ef77cfa <+177>: je 0x10ef77d04 ; <+187>
0x10ef77cfc <+179>: movq %rbx, %rdi
0x10ef77cff <+182>: callq 0x10eff482e ; symbol stub for: free
0x10ef77d04 <+187>: movq 0x2e044d(%rip), %rax ; (void *)0x0000000110021970: __stack_chk_guard
0x10ef77d0b <+194>: movq (%rax), %rax
0x10ef77d0e <+197>: cmpq -0x18(%rbp), %rax
0x10ef77d12 <+201>: jne 0x10ef77d3d ; <+244>
0x10ef77d14 <+203>: addq $0x10, %rsp
0x10ef77d18 <+207>: popq %rbx
0x10ef77d19 <+208>: popq %r14
0x10ef77d1b <+210>: popq %rbp
0x10ef77d1c <+211>: retq
0x10ef77d1d <+212>: movq 0x2e0434(%rip), %rax ; (void *)0x0000000110021970: __stack_chk_guard
0x10ef77d24 <+219>: movq (%rax), %rax
0x10ef77d27 <+222>: cmpq -0x18(%rbp), %rax
0x10ef77d2b <+226>: jne 0x10ef77d3d ; <+244>
0x10ef77d2d <+228>: movq %rbx, %rdi
0x10ef77d30 <+231>: addq $0x10, %rsp
0x10ef77d34 <+235>: popq %rbx
0x10ef77d35 <+236>: popq %r14
0x10ef77d37 <+238>: popq %rbp
0x10ef77d38 <+239>: jmp 0x10eff44c8 ; symbol stub for: _objc_rootDealloc
0x10ef77d3d <+244>: callq 0x10eff443e ; symbol stub for: __stack_chk_fail
汇编内容较多,整体流程是比较清晰的,伪代码如下:
// 获取当前类
object_getClass
// 通过当前类获取当前类型
class_getName
// 将_NSZombie_拼接上当前类名
zombiesClsName = "_NSZombie_%s" + className
// 获取zombiesClsName类
objc_lookUpClass zombiesClsName
// 判断是否已经存在zombiesCls
if not zombiesCls:
// 如果不存在
// 现获取"_NSZombie_"类
cls = objc_lookUpClass "_NSZombie_"
// 复制出一个cls类,类名为zombiesClsName
objc_duplicateClass cls zombiesClsName
// 字符串变量释放
free zombiesClsName
// objc中原本的对象销毁方法
objc_destructInstance(self)
// 将当前对象的类修改为zombiesCls
object_setClass zombiesCls
// 判断是否开启了僵尸对象功能
if not __CFZombieEnabled:
// 如果没开启 将当前内存释放掉
free
上面的伪代码基本是__dealloc_zombie方法实现的整体过程,在objc源码中,NSObject类原本的dealloc方法实现路径如下:
- (void)dealloc {
_objc_rootDealloc(self);
}
void _objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}
inline void objc_object::rootDealloc()
{
// taggedPointer无需回收内存
if (isTaggedPointer()) return; // fixme necessary?
// nonpointer为1表示不只是地址,isa中包含了其他信息
// weakly_referenced表示是否有弱引用
// has_assoc 表示是否有关联属性
// has_cxx_dtor 是否需要C++或Objc析构
// has_sidetable_rc是否有散列表计数引脚
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
// 如果都没有 直接回收内存
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
id object_dispose(id obj)
{
if (!obj) return nil;
// 进行内存回收前的销毁工作
objc_destructInstance(obj);
free(obj);
return nil;
}
可以看到,__dealloc_zombie与真正的dealloc的实现其实只差了当前内存的回收部分,objc_destructInstance方法会正常执行的,这个方法实现如下:
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// C++ 析构
if (cxx) object_cxxDestruct(obj);
// 移除关联属性
if (assoc) _object_remove_assocations(obj);
// 弱引用表和散列表的清除
obj->clearDeallocating();
}
return obj;
}
通过上面的分析,我们发现,其实系统实现的僵尸对象非常安全,并不对正常代码的运行产生负面作用,唯一的影响在于内存不回收会增加内存的使用负担,但是可以通过某些策略来进行释放。
// _YHZombie_.h
#import <Foundation/Foundation.h>
@interface _YHZombie_ : NSObject
@end
// _YHZombie_.m
#import "_YHZombie_.h"
@implementation _YHZombie_
// 调用这个对象对的所有方法都hook住进行LOG
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%p-[%@ %@]:%@",self ,[NSStringFromClass(self.class) componentsSeparatedByString:@"_YHZombie_"].lastObject, NSStringFromSelector(aSelector), @"向已经dealloc的对象发送了消息");
// 结束当前线程
abort();
}
@end
在新建一个NSObject的类别,用来替换dealloc方法,如下:
// NSObject+YHZombiesNSObject.h
#import <Foundation/Foundation.h>
@interface NSObject (YHZombiesNSObject)
@end
// NSObject+YHZombiesNSObject.m
#import "NSObject+YHZombiesNSObject.h"
#import <objc/objc.h>
#import <objc/runtime.h>
@implementation NSObject (YHZombiesNSObject)
+(void)load {
[self __YHZobiesObject];
}
+ (void)__YHZobiesObject {
char *clsChars = "NSObject";
Class cls = objc_lookUpClass(clsChars);
Method oriMethod = class_getInstanceMethod(cls, NSSelectorFromString(@"dealloc"));
Method newMethod = class_getInstanceMethod(cls, NSSelectorFromString(@"__YHDealloc_zombie"));
method_exchangeImplementations(oriMethod, newMethod);
}
- (void)__YHDealloc_zombie {
const char *className = object_getClassName(self);
char *zombieClassName = NULL;
asprintf(&zombieClassName, "_YHZombie_%s", className);
Class zombieClass = objc_getClass(zombieClassName);
if (zombieClass == Nil) {
zombieClass = objc_duplicateClass(objc_getClass("_YHZombie_"), zombieClassName, 0);
}
objc_destructInstance(self);
object_setClass(self, zombieClass);
if (zombieClassName != NULL)
{
free(zombieClassName);
}
}
@end
上面代码,除了一些容错判断没有加之外,思路和系统的僵尸对象一模一样。
再次运行我们的测试代码,在访问到野指针的时候将百分百的产生异常中断,并输出如下:
0x600003a8c2e0-[MyObject name]:向已经dealloc的对象发送了消息
现在,我们已经从原理上简单实现了一个不依赖于Xcode的野指针监控工具。
typedef struct {
NSString *name;
} MyStruct;
在使用此结构体时,如果初始化之前进行了使用或内存回收后进行了使用都可能会出现野指针问题,如下:
MyStruct *p;
p = malloc(sizeof(MyStruct));
// 此时内存中的数据不可控 可能是之前未擦除的
printf("%x\n", *((int *)p));
// 使用可能会出现野指针问题
NSLog(@"%@", p->name);
// 进行内存数据的初始化
p->name = @"HelloWorld";
// 回收内存
free(p);
// 此时内存中的数据不可控
NSLog(@"%@", p->name);
我们可以思考下,出现上面野指针场景的主要原因是:
1. 获取到分配的内存后,如果此内存之前有过使用,数据此时是不可控的,当前指针直接使用此数据会有问题。
2.回收内存后,当前内存中的数据是不可控的,可能有别人或之前未清除的指针使用到。
无论是上面哪种场景,此野指针问题都有非常大的随机性,难以调试。因此我们核心要处理的地方在于把随机性改为必然性,即想办法让使用到这些有问题的内存时直接Crash,而不是可能Crash。要处理场景1很容易,我们可以hook住C中的malloc方法,分配了内存后直接将一个约定好的异常数据写入内存,这样在初始化之前使用到此数据时必然产生Crash。对于场景2,我们可以hook住C中的free方法,回收内存后将一个约定好的异常数据直接写入此内存,下次如果此内存没有被再分配,使用到它后也必然产生Crash。Xcode提供的Malloc Scribble调试功能,就是用这种思路实现的。
开启Xcode的Malloc Scribble选项,运行上面代码,效果如下图所示:
可以看到,在malloc分配内存之后,所有字节都被填入了0xAA,未初始化前使用就会必然产生Crash。这与Apple官方文档的解释是一致的,但是在free之后,内存数据获取到的可能并不是文档所说的0x55,是因为这块内存可能被其他内容覆写了。官网文档描述如下:
我们也可以手动根据Malloc Scribble的思路来实现一个将野指针问题从随机变成必然的工具,只需要重写系统的malloc相关的函数与free函数即可。对于C语言函数的Hook,我们可以直接使用fishhook库:
https://github.com/facebook/fishhook
导入上面库后,新建一个命名为YHMallocScrbble的类,实现如下:
// YHMallcScrbble.h
#import <Foundation/Foundation.h>
@interface YHMallcScrbble : NSObject
@end
// YHMallcScrbble.m
#import "YHMallcScrbble.h"
#import "fishhook.h"
#import "malloc/malloc.h"
void * (*orig_malloc)(size_t __size);
void (*orig_free)(void * p);
void *_YHMalloc_(size_t __size) {
void *p = orig_malloc(__size);
memset(p, 0xAA, __size);
return p;
}
void _YHFree_(void * p) {
size_t size = malloc_size(p);
memset(p, 0x55, size);
orig_free(p);
}
@implementation YHMallcScrbble
+ (void)load {
rebind_symbols((struct rebinding[2]){{"malloc", _YHMalloc_, (void *)&orig_malloc}, {"free", _YHFree_, (void *)&orig_free}}, 2);
}
@end
这样我们就实现了将野指针问题从随机变成必然,并且通用C指针。
相比僵尸对象方案,Malloc Scribble方法可以通用C指针,并且真正实现了对对象内存的回收,不会暂用内存。但是也有很大的弊端,比如对于free后写入的0x55在很多情况下都是无效的,因为这块内存可能又被其他地方改写了,导致Crash依然是随机的。当然我们也可以在自定义的free方法中不调用原系统的free,使得这块内存强制不能分配出去,这样其实和僵尸对象方案就比较类似了。并且相对僵尸对象方案,Malloc Scribble只能一定程度上将随机变成必然,方便问题的暴露,但是对开发者来说,并没有太多的信息告诉我们具体是什么类型的数据出的问题,排查还是有难度。
专注技术,懂的热爱,愿意分享,做个朋友
总的来说,我对ruby还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用
在控制台中反复尝试之后,我想到了这种方法,可以按发生日期对类似activerecord的(Mongoid)对象进行分组。我不确定这是完成此任务的最佳方法,但它确实有效。有没有人有更好的建议,或者这是一个很好的方法?#eventsisanarrayofactiverecord-likeobjectsthatincludeatimeattributeevents.map{|event|#converteventsarrayintoanarrayofhasheswiththedayofthemonthandtheevent{:number=>event.time.day,:event=>ev
我有一个表单,其中有很多字段取自数组(而不是模型或对象)。我如何验证这些字段的存在?solve_problem_pathdo|f|%>... 最佳答案 创建一个简单的类来包装请求参数并使用ActiveModel::Validations。#definedsomewhere,atthesimplest:require'ostruct'classSolvetrue#youcouldevencheckthesolutionwithavalidatorvalidatedoerrors.add(:base,"WRONG!!!")unlesss
好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信
如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象
我在Rails工作并有以下类(class):classPlayer当我运行时bundleexecrailsconsole然后尝试:a=Player.new("me",5.0,"UCLA")我回来了:=>#我不知道为什么Player对象不会在这里初始化。关于可能导致此问题的操作/解释的任何建议?谢谢,马里奥格 最佳答案 havenoideawhythePlayerobjectwouldn'tbeinitializedhere它没有初始化很简单,因为你还没有初始化它!您已经覆盖了ActiveRecord::Base初始化方法,但您没有调
我有一个服务模型/表及其注册表。在表单中,我几乎拥有服务的所有字段,但我想在验证服务对象之前自动设置其中一些值。示例:--服务Controller#创建Action:defcreate@service=Service.new@service_form=ServiceFormObject.new(@service)@service_form.validate(params[:service_form_object])and@service_form.saverespond_with(@service_form,location:admin_services_path)end在验证@ser
这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下
我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的
我想让一个yaml对象引用另一个,如下所示:intro:"Hello,dearuser."registration:$introThanksforregistering!new_message:$introYouhaveanewmessage!上面的语法只是它如何工作的一个例子(这也是它在thiscpanmodule中的工作方式。)我正在使用标准的rubyyaml解析器。这可能吗? 最佳答案 一些yaml对象确实引用了其他对象:irb>require'yaml'#=>trueirb>str="hello"#=>"hello"ir