引言
大家都知道,Objective-C 有 4 个对象所有权修饰符(Ownership Qualifier),分别是:
__strong
__weak
__unsafe_unretained
__autoreleasing
在日常开发中日常用的最多的就是 __weak
,几乎都是为了解决循环引用的问题。__strong
是一个变量默认的修饰符,不需要写。__unsafe_unretained
因为它的不安全,基本上不会去使用它。__autoreleasing
见得最多的地方估计就是 NSError * __autoreleasing *
这个类型声明了,似乎也没有地方需要自己写。
看到 NSError * __autoreleasing *
这个类型,不禁会思考,这个 __autoreleasing
为啥放在两个星号中间呢,如果放在两个星号的左边,或者放在两个星号的右边行不行呢?
于是又开始思考另一个相关的问题。我们知道声明常量的时候,const 的位置不同,实际含义就会不同。比如:
NSObject const *object;
这里 object 是一个指针,指向一个 NSObject 类型的常量(通常把这种指针叫做指向常量的指针,或者常量指针)
这样声明的结果是 object 本身可以修改,但是 object 指向的内容不能修改。
NSObject * const object;
这里的 object 是一个常量,它的类型是一个指针(通常把这种类型叫做指针常量)
这样声明的结果是 object 本身不可以修改,但是 object 指向的内容可以修改。
但是因为 Objective-C 不支持对对象指针的解引用进行赋值,因此 object 指向的内容也无法修改。即 *str1 = *str2;
这类操作是不允许的。
const NSString *str = @"this is a string";
上面代码中的 const 其实是毫无意义的。
还有 object 本身和 object 指向的内容都不能修改的情况,即:
NSObject const * const object;
那么作为类比,对于 __strong 修饰符来说:
NSObject * __strong o;
NSObject __strong * o;
上面两句代码有什么区别?第二行中的 * 和 o 之间,日常开发时是不写空格的,这里只是为了对齐。
NSObject * __strong o;
和 NSObject __strong *o;
的区别
探索步骤1,转成 C++ 代码
int main(int argc, const char * argv[]) {
NSObject * __strong o1;
NSObject __strong *o2;
return 0;
}
将上面这个 main 函数转成 C++ 代码后的结果为:
int main(int argc, const char * argv[]) {
NSObject __attribute__((objc_ownership(strong))) *o1;
NSObject * __attribute__((objc_ownership(strong))) o2;
return 0;
}
看上去 __strong
被类似宏替换的操作替换成了 __attribute__((objc_ownership(strong)))
,看不出什么东西。
探索步骤2,查看汇编代码
int main(int argc, const char * argv[]) {
NSObject * __strong o1;
return 0;
}
int main(int argc, const char * argv[]) {
NSObject __strong *o2;
return 0;
}
分别查看上面两个 main 函数的汇编代码,会发现结果都是:
XXXXXX`main:
0x100003ec4 <+0>: sub sp, sp, #0x30
0x100003ec8 <+4>: stp x29, x30, [sp, #0x20]
0x100003ecc <+8>: add x29, sp, #0x20
0x100003ed0 <+12>: stur wzr, [x29, #-0x4]
0x100003ed4 <+16>: stur w0, [x29, #-0x8]
0x100003ed8 <+20>: str x1, [sp, #0x10]
0x100003edc <+24>: add x0, sp, #0x8
0x100003ee0 <+28>: mov x1, #0x0
0x100003ee4 <+32>: str xzr, [sp, #0x8]
0x100003ee8 <+36>: stur wzr, [x29, #-0x4]
0x100003eec <+40>: bl 0x100003f3c ; symbol stub for: objc_storeStrong
0x100003ef0 <+44>: ldur w0, [x29, #-0x4]
0x100003ef4 <+48>: ldp x29, x30, [sp, #0x20]
0x100003ef8 <+52>: add sp, sp, #0x30
0x100003efc <+56>: ret
所以这两种顺序的写法的效果应该是一样的。
所以其实:
NSObject * __strong o;
NSObject __strong *o;
__strong NSObject *o
上面这三种写法都是一样的。
对于另外三个修饰也都是一样,我们可以测试一下 __autoreleasing
修饰符。先随便创建一个类里面写上如下代码:
@implementation Cat
+ (instancetype)cat {
Cat *cat = [[Cat alloc] init];
return cat;
}
- (void)dealloc {
NSLog(@"%s", __func__);
}
@end
然后编写 main 函数:
int main(int argc, const char * argv[]) {
Cat * __autoreleasing cat = nil;
@autoreleasepool {
cat = [Cat cat];
}
NSLog(@"outside of the autoreleasepool");
return 0;
}
可以发现,无论 __autoreleasing
写在哪个位置,都可以让 cat 对象在打印 outside of the autoreleasepool 之前就被销毁。
所以对于一级指针,修饰符放在哪个位置都一样,那么对于二级指针呢?
在探讨二级指针之前,先了解一下 NSError * __autoreleasing *
是什么意思,以及为什么这里要写成这样。
为什么 NSError * __autoreleasing *
要用二级指针?
其实 NSError * __autoreleasing *
是 MRC(或者叫 MRR)时代的写法,在 ARC 时代,可以省去 __autoreleasing
修饰符,直接写成 NSError **
,编译器会自动对某些变量设为 __autoreleasing
。所以对于 ARC 的 NSError **
类型,编译器还是会把它处理成 NSError * __autoreleasing *
的语义。
在 ARC 下,用 Xcode 的代码自动补全功能时,自动补全的代码还是会对这个类型给出 NSError *__autoreleasing _Nullable * _Nullable
的提示。
为了说明 NSError * __autoreleasing *
的含义,首先要先明白这里为什么要用二级指针。
举一个经常使用的 API,NSJSONSerialization 的例子:
+ (nullable id)JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt error:(NSError **)error;
这个方法的 error 参数用来在解析失败的时候提供具体错误信息,需要这么来使用它:
NSError *error = nil;
[NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
这里的 error 参数是一个输出参数,它输出的是一个包含错误信息的 NSError 对象。
输出参数必须是一个指针,如果不是指针,参数就是值传递,函数对参数的修改无法作用到函数外部。
那为什么不用一级指针?
首先先看一下 NSError 这个类的公共属性,会发现都是 readonly 的,也就是说 NSError 是一个不可变对象,它包含的信息在创建对象的时候就确定了,不能再更改。因此这个对象必须由 JSONObjectWithData:options:error:
这个方法内部实现来创建,不能由调用方创建。
如果这里用一级指针,由于调用方不能创建 NSError 对象,且 Objective-C 不允许在栈上给对象分配内存空间,下面这种写法是不可以的:
NSError error;
[NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
只能这么写:
NSError *error = nil;
[NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:error];
这么写的结果是给 error 参数传递了一个 nil 值。于是 JSONObjectWithData:options:error:
的内部实现在创建 NSError 对象时,不知道应该把它创建在哪个地址上,导致调用方也无法接收到这个对象。
所以下面这种标准写法:
NSError *error = nil;
[NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
的实际含义是:
- 在栈上创建了一个指针叫 error,指向的类型是 NSError,并把指针指向的位置初始化为 nil。
- 用
&error
获取这个指针本身在栈上的地址,它是有值的,传给 error 参数的值不是 nil。 -
JSONObjectWithData:options:error:
方法的内部在堆上创建一个 NSError 对象,并把传入的栈上的地址指向这个 NSError 对象。 - 调用方就可以通过 error 变量得到这个 NSError 对象了。
所以这里为什么要用二级指针应该解释清楚了。
NSError * __autoreleasing *
中 __autoreleasing
修饰符的位置
现在来探索一下 __autoreleasing
修饰符为什么要写在两个星号中间。
假设定义一个二级指针的变量:
Cat *cat = [Cat cat];
Cat **p = &cat;
如果直接上面这样写,编译器会报错 Pointer to non-const type 'Cat *' with no explicit ownership,意思是有一个指针没有显式地指定拥有关系,从这句话来理解,这里的指针是二级指针的内层指针。
Cat *cat = [Cat cat];
Cat ** __strong p = &cat;
把代码改成上面代码,编译后依然报相同的错误,所以可以得出结论,在两个星号右边写修饰符,修饰的可能是外层指针。外层指针的具体修饰情况后面再讨论。
于是把 __strong
往左边移动一个星星。
Cat *cat = [Cat cat];
Cat * __strong *p = &cat;
编译通过,可以正常运行。
再改变 __strong
的位置做几次尝试:
Cat *cat = [Cat cat];
Cat __strong **p = &cat;
Cat *cat = [Cat cat];
__strong Cat **p = &cat;
发现都可以正常编译运行,所以:
Cat * __strong *p;
Cat __strong **p
__strong Cat **p
这三种写法的效果是一样的,修饰符都是在修饰内层指针。
那为什么苹果自己要把 __autoreleasing
写在两个星星中间呢?可能是苹果自己的一种习惯或编码规范吧。
上面用 __strong
来举例是为了代码简洁,实际上如果要使用 __autoreleasing
, cat 本身也要指明是 __autoreleasing
。原因应该大家都清楚。
Cat * __autoreleasing cat = [Cat cat];
Cat * __autoreleasing *p = &cat;
对于二级指针外层指针的拥有关系的修饰
那么外层指针的拥有关系,会想到如果是下面的代码会发生什么呢?
Cat * __strong * __weak p = nil;
{
Cat *cat = [Cat cat];
p = &cat;
}
NSLog(@"%@", *p);
结果打印 (null),似乎是合理的,weak 指针会在指向的对象释放自动至为 nil。
Cat * __strong * __strong p = nil;
{
Cat *cat = [Cat cat];
p = &cat;
}
NSLog(@"%@", *p);
那么这样呢,运行代码,发现结果还是 (null)。为啥???
其实上面两个代码在运行时编译器都会报出警告,__strong' only applies to Objective-C object or block pointer types; type here is 'Cat *__strong *'
。意思是说修饰符只支持指向 OC 对象或者 Block 的指针类型。
二级指针的外层指针(其实就是二级指针本身)指向的是一个指针,并不是 OC 对象或 Block。这里编译器会忽略对外层指针的修饰符,实际上类型还是 Cat * __strong *
。