iOS开发

OC中给空对象(或者已经被销毁的对象)发送消息程序会Crash吗

2017-11-22  本文已影响42人  DreamMmMmM

首先,OC中向nil发消息,程序是不会崩溃的。

因为OC的函数调用都是通过objc_msgSend进行消息发送来实现的,相对于C和C++来说,对于空指针的操作会引起Crash的问题,而objc_msgSend会通过判断self来决定是否发送消息,如果self为nil,那么selector也会为空,直接返回,所以不会出现问题。视方法返回值,向nil发消息可能会返回nil(返回值为对象)、0(返回值为一些基础数据类型)或0X0(返回值为id)等。但是对[NSNull

null]对象发送消息时,是会crash的,因为这个NSNull类只有一个null方法。

当然,如果一个对象已经被释放了(引用计数为0了),那么这个时候再去调用方法肯定是会Crash的,因为这个时候这个对象就是一个野指针(指向僵尸对象(对象的引用计数为0,指针指向的内存已经不可用)的指针)了,安全的做法是释放后将对象重新置为nil,使它成为一个空指针,大家可以在关闭ARC后手动release对象验证一下。

nil的定义是null pointer to object-c object,指的是一个OC对象指针为空,本质就是(id)0,是OC对象的字面0值

不过这里有必要提一点就是OC中给空指针发消息不会崩溃的语言特性,原因是OC的函数调用都是通过objc_msgSend进行消息发送来实现的,相对于C和C++来说,对于空指针的操作会引起Crash的问题,而objc_msgSend会通过判断self来决定是否发送消息,如果self为nil,那么selector也会为空,直接返回,所以不会出现问题。

这里补充一点,如果一个对象已经被释放了,那么这个时候再去调用方法肯定是会Crash的,因为这个时候这个对象就是一个野指针了,安全的做法是释放后将对象重新置为nil,使它成为一个空指针,大家可以在关闭ARC后手动release对象验证一下。

NSString *name = @"Allen";

if (name != nil && [name isEqualToString:@"Allen"]) {
    NSLog(@"name: %@", name);
} else {
    NSLog(@"name is nil");
}

//or
if ([name isEqualToString:@"Allen"]) {
    NSLog(@"name: %@", name);
} else {
    NSLog(@"name is nil");
}

上面的两种判断都是正确的,我们不必担心当name为nil时调用isEqualToString会出现Crash,但是我还是想说,在使用一个对象之前判断它是否为nil是一个很好的习惯,个人觉得有两个原因:

  1. 降低时间复杂度(感觉可以这么说吧),如果你增加了nil的判断,那么不需要对空指针发送消息了,发消息其实是件费时的操作。详情可以看这里
  2. 把判断为空养成习惯其实是好事,这样在你切换语言时也不容易出错。

Nil的定义是null pointer to object-c class,指的是一个类指针为空。本质就是(class)0,OC类的字面零值。

Class class = [NSString class];
if (class != Nil) {
    NSLog(@"class name: %@", class);
}

NULL的定义是null pointer to primitive type or absence of data,指的是一般的基础数据类型为空,可以给任意的指针赋值。本质就是(void *)0,是C指针的字面0值。

NSInteger *pointerA = NULL;

NSInteger pointerB = 10;

pointerA = &pointerB;

NSLog(@”%ld”, *pointerA);

我们要尽量不去将NULL初始化OC对象,可能会产生一些异常的错误,要使用nil,NULL主要针对基础数据类型。

NSNull好像没有什么具体的定义(懵),它包含了唯一一个方法+(NSNull*)null,[NSNull null]是一个对象,用来表示零值的单独的对象。

NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];

NSString *nameOne = @”Allen”;

NSString *nameTwo = [NSNull null]; // 不用使用 nil,nil在字典,数组中有特殊含义–元素结束标记

NSString *nameThree = @"Tom";
[dictionary setObject:nameOne forKey:@"nameOne"];
[dictionary setObject:nameTwo forKey:@"nameTwo"];
[dictionary setObject:nameThree forKey:@"nameThree"];
NSLog(@"names: %@", dictionary);

NSMutableArray *array = [[NSMutableArray alloc] init];
[array addObject:nameOne];
[array addObject:nameTwo];
[array addObject:nameThree];
NSLog(@"names : %@", array);

NSNull主要用在不能使用nil的场景下,比如NSMutableArray是以nil作为数组结尾判断的,所以如果想插入一个空的对象就不能使用nil,NSMutableDictionary也是类似,我们不能使用nil作为一个object,而要使用NSNull

以上转自:

文/北辰明(简书作者)

原文链接:http://www.jianshu.com/p/2ea9c3f737ea

向nil发送消息

在Objective-C中向nil发送消息是完全有效的——只是在运行时不会有任何作用。Cocoa中的几种模式就利用到了这一点。发向nil的消息的返回值也可以是有效的:

如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil)。例如:Person * motherInlaw = [ aPerson spouse] mother]; 如果spouse对象为nil,那么发送给nil的消息mother也将返回nil。

如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void),float,double,long double 或者long long的整型标量,发送给nil的消息将返回0*。

如果方法返回值为结构体,正如在《Mac OS X ABI 函数调用指南》,发送给nil的消息将返回0。结构体中各个字段的值将都是0。其他的结构体数据类型将不是用0填充的。

• 如果方法的返回值不是上述提到的几种情况,那么发送给nil的消息的返回值将是未定义的。

Objective-C 是以 C 语言为基础的,PC 上,在 C 语言中对空指针进行操作,程序会由于越界访问而出现保护错进而崩溃,但是 Objective-C 中为什么不会崩溃呢?

原因需要从源代码中寻找,

下面是 objc_msgSend 的 arm 版汇编代码片段:

在 arm 的函数调用过程中,一般用 r0-r4 传递参数,用 r0 传递返回值。对应 objc_msgSend,第一个参数为 self,返回值也是 self,都放在 r0(a1)中。

/********************************************************************
 * idobjc_msgSend(idself, SELop, ...)

 * On entry: a1 is the message receiver,

 *                  a2 is the selector
 * ********************************************************************/

ENTRY objc_msgSend

# check whether receiver is nil

teq     a1, #0

moveq   a2, #0

bxeq    lr

teq 指令说明:

TEQ Rn, Operand2 The TEQ instruction performs a bitwise Exclusive OR

operation on the value in Rn and the value of Operand2.

测试 self 是否为空。

moveq 指令说明:

如果self为空,则将 selector 也设置为空。

bx 指令说明:

在 arm 中 bx lr 用来返回到调用子程序的地方(即:返回到调用者),此处是:如果 self 为空,就返回到调用objc_msgSend 的地方继续执行。

总之:

如果传递给 objc_msgSend 的 self 参数是 nil,该函数不会执行有意义的操作,直接返回。

理解一下nil,NULL和[NSNull null]的区别:

nil用来给对象赋值(Objective-C中的任何对象都属于id类型),NULL则给任何指针赋值,NULL和nil不能互换,nil用于类指针赋值(在Objective-C中类也是一个对象,是类的meta-class的实例,有关meta-class参见资料【译】Objective-C 中的 Meta-class 是什么?), 而NSNull则用于集合操作,用在数组或字典中要添加某个内容为空的情况。

虽然它们表示的都是空值,但使用的场合完全不同。所以在编码时严格按照变量类型来赋值,将正确的空值赋给正确的类型,使代码易于阅读和维护,也不易引起错误。

示例如下:

id object = nil;  
// 判断对象不为空  
if (object) {  
}       
// 判断对象为空  
if (object == nil) {  
}  

// 数组初始化,空值结束  
NSArray *array = [[NSArray alloc] initWithObjects:@"First", @"Second", nil];  
// 判断数组元素是否为空  
NSString *element = [array objectAtIndex:2];  
if ((NSNull *)element == [NSNull null]) {  
}  

//做项目时遇到,要判断数组元素是否为空,以下写法,都无效
if(!element)
if([element length]>0)
if(element == NULL)
if(element == nil)

// 判断字典对象的元素是否为空  
NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:  
    @"iPhone", @"First", @"iPad", @"Second", nil];  
NSString *value = [dictionary objectForKey:@"First"];  
if ((NSNull *)value == [NSNull null]) {  
}  

总结一下:

1. nil:一般赋值给空对象;

2. NULL:一般赋值给nil之外的其他空值。如SEL等,如:

[NSApp beginSheet:sheet modalForWindow:mainWindow
    modalDelegate:nil // 使用nil指向一个空对象
   didEndSelector:NULL // 指向空
     contextInfo:NULL]; 

3. 当向nil发送消息时,不会有异常,程序将继续执行下去;

4. 而向NSNull的对象发送消息时会收到异常,因为NSNull只有一个方法:+ (NSNull *) null;,会“没有这样的selector”的错误。

[NSNull null]是一个对象,他用在不能使用nil的场合。因为在NSArray和NSDictionary中nil有特殊的含义。但是在有些时候,确实需要用到这样的空值,比如在字典中,电话簿中”Jack”关键字下有电话号码、家庭住址、Email等等信息,但是现在只知道他的电话号码,这种不知道其他信息的情况下为了消除一些歧义,有必要将它们设置为空,所以Cocoa提供了NSNull类。

[dictionary setObject:[NSNull null], forKey:"Email"];
if(EmailAdress == [NSNull null])
{
  //to do something...
}

因为Object-C的集合对象,如NSArray、NSDictionary、NSSet等,都有可能包含NSNull对象,所以,如果以下代码中的item为NSNull,则会引起程序崩溃:

NSString *item=[NSArray objectAtIndex:i];
if([item isEqualToString:@"TestNumber"]) {
    // do someThing
}

// 以下代码是常见的错误,release对象没有设置为nil,从而引起程序崩溃。
id someObject=[[Object alloc] init];
//...
[someObject release];
//...
if(someObject) {
    //crash here
}

上面会发生常见的“EXC_BAD_ACCESS”错误,也就是野指针错误。因为someObject指针指向的那块内存的引用计数已经为0了,所以那块内存已经不可以访问了,但是someObject指针并没有设为nil,所以会报野指针错误,那块内存地址中的僵尸对象已经无法使用。

一个可以研究一下的问题:

// 下面dealloc方法中,三句话的区别

-(void) dealloc {
    self.test = nil; 
    [_test release];
    test = nil;
}

最简单的:[_test release];,这句话就是将对象的引用计数减1,所谓引用计数就是看有多少个指针(强引用)指向某个内存实体。当release一次,对应的指针就减少一个,release到0时,表示这块内存真正归还给系统了。

其次,self.test = nil;,这句话牵扯到属性的getter和setter方法,在使用MRC(手动引用计数)的时候,方法如下:

// 属性的setter方法
- (void)setTest:(NSString *)newString {
    // 如果新值跟旧值一样就不用赋值了
    if (_test != newString) {
        // 新值和旧值不一样时,由于现在对旧值有强引用,需要先引用计数减1
        [_test release];
        // 然后将新值赋给旧值,而且不能直接_test = newString,这种方式是浅拷贝,只是把指针地址赋值过去了,对应那块内存的引用计数并没有变化,这样就会导致两个指针指向了一块引用计数为1的内存空间,有引起野指针的潜在风险,那么为了避免这个问题,就需要进行retain,使引用计数加1
        _test = [newString retain];
    }
}

// 属性的get方法
- (NSSString *)test {
    return _test;
}

所以self.test = nil;这句话就变成如下:

if (_test != nil) {
        [_test release];
        _test = [nil retain];
    }

所做的事情就是:retain nil对象。在这之前已经先release了旧的对象,这个方法优点是成员变量连指向随机数据的机会都没有,而通过别的方式,就可能会出现指向随机数据的情况。当release了之后,万一有别的方法要用要存取它,如果它已经dealloc了,可能就会crash,而指向nil之后,就不会发生错误了。nil说白了就是计数器为0,这么说吧,当真正release一个对象的时候,NSLog是打印不了它指向的内存控件的,而当nil的时候,是可以打印出来指向的一个内存空间。

所以也不难解释test = nil;了, 单纯的这种用法可以说是自己给自己制造内存泄露,这里可以这么理解,就是相当于将指向对象的指针直接和对象一刀两断了。直接让test指向nil,而内存实体不会消失,也不会有系统回收。

上一篇下一篇

猜你喜欢

热点阅读