我发觉面试的时候很喜欢问属性(@property)的修饰符,什么 assign、retain、copy、weak、strong 的区别之类。尤其喜欢问 copy 属性。本文就和大家一起来详细扒一扒copy 修饰符相关内容,一起来看看吧,希望对大家学习Objective-C有所帮助。
什么时候需要 copy 修饰符?
典型是这种
NSMutableString* string = [[NSMutableString alloc] initWithString"Hello, World"];
Test* test = [[Test alloc] init];
test.title = string;
[string appendString"Hello World"];
外面创建一个可变的对象,之后给 Test.title 赋值。这里 title 属性,需要使用 copy, 不应该用 retain。不然之后外部对象改变了,就会引起 title 的改变。
同理,NSMutableArray, NSMutableDictionary 也是如此,比如 UINavigationController.viewControllers 属性就是使用copy。不过复制容器代价会更大些。我们更经常看到的是 NSString 的 copy。
copy 和 mutableCopy
Cocoa 很多容器类都有两种类型,一种是可变(可以修改)的,比如 NSMutableString,一种是不可变(不可修改)的,比如 NSString。
指所以区分可变不可变,是因为不可变对象会更简单。比如写代码时,因为知道对象不可变,就可以一开始就分配好固定的内存,刚刚好,不多不少;假如是可变对象,就可能需要预留一点空间。再比如,在多线程时,访问不可变的对象可以不上锁,因为它状态根本不可变,自然也就没有同步的需要;而可变对象就需要上锁。
copy 也有两种,从语意上定义,使用 copy 会复制出不可变的对象,而使用 mutableCopy 会复制出对应的可变类型对象。但实际上,copy 和 mutableCopy 是由各个类独立实现的,最终 copy 得到的是否真是不可变对象,实际上是不确定的。比如
NSMutableString* string0 = [[NSMutableString alloc] initWithString"Hello, World"];NSLog(@"%@", NSStringFromClass([[string0 copy] class]));
NSMutableString* string1 = [[NSMutableString alloc] init];NSLog(@"%@", NSStringFromClass([[string1 copy] class]));
会打印出 __NSCFString 和 __NSCFConstantString。这说明 string0 的 copy 产生 NSMutableString 对象,而 string1 copy产生了 NSString 对象。
我们写代码的时候应该依赖于语意(或者接口定义),而不能依赖于具体实现,实现会更容易变化。copy 应该根据语意,将对象都当成不可修改的,这样肯定没有错。
copy 语意和实现
copy 语意上是复制一份数据,前一份数据修改之后,不会影响后一份。但语意和实现是分离的,具体实现上,是不是真的复制一份,是不固定的,是由各个类独立实现的。
NSString* str = @"Hello, World";NSString* str1 = [str copy];NSLog(@"%p, %p", str, str1);
比如对于 NSString 等不可变对象的时候,调用 copy 会直接返回自身。你会看到 [str copy] 得到的对象,其实就是 str。
这样做,并没有违背 copy 语意。对于不可变对象,本来就不能修改,自然也就不会影响复制后的数据,也就没有必要复制了。。更应该从语意上(从接口)中看,不要过于依赖于实现。在这点上,有很多人没有区分好语意和实现。面试的时候,就发觉面试官特别喜欢纠结于 copy 是否真的复制了。
更具体一些,
copy 最终会调用 NSCopying 协议的函数:
- (id)copyWithZonenullable NSZone *)zone;
mutableCopy 会调用 NSMutableCopying 协议的函数:
- (id)mutableCopyWithZonenullable NSZone *)zone;
每个不同的类,都可以根据自己的具体情况,实现自身的 copy 和 mutableCopy 语意。
随便提一下,NSZone 这个类是用来封装具体的内存分配和释放,有点像内存池。以前的内存比较紧张,根据不同的情况使用不同的内存分配方式,也很有必要。但现在 NSZone 属于历史遗留问题了,基本都为 nil, 基本不用管它。你就算想自己重新写一个 NSZone, 也很难写得比系统内置的好。
翻了一下 objc runtime 的源码,设置属性最终会调用 reallySetProperty 函数,里面有这样的代码:
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
最后习题
这其实是我面试过程中的题目。
#import
@interface Test : NSObject {
}@property (nonatomic, copy) NSMutableArray* array;@end
@implementation Test
@end
int main(int argc, const char* argv[]) {
@autoreleasepool {
NSMutableArray* array = [[NSMutableArray alloc] init];
Test* test = [[Test alloc] init];
test.array = array; // (1)
NSLog(@"%@", NSStringFromClass([test.array class]));
}
return 0;
}
在语句(1)test.array = array; 之后,test.array 究竟实际上是什么类型。
我面试过程当中,推断应该是调用 copy, 这样实际上应该是 NSArray 类型。但我当时不确定,我不清楚编译器是否可能智能到看到 NSMutableArray 类型,就将其转化成 mutableCopy。
回家后,我试了试,打印出类型。确实是 NSArray 类型。
这种题目已经稍微有点刁难了。我写了这样久 Objective-C 代码,还从来没有看到过属性中类型为可变的,属性的类型为可变,本来就有点问题。
来源:网络