探索 Objective-C 的对象所有权修饰符

引言

大家都知道,Objective-C 有 4 个对象所有权修饰符(Ownership Qualifier),分别是:

1
2
3
4
__strong
__weak
__unsafe_unretained
__autoreleasing

在日常开发中日常用的最多的就是 __weak,几乎都是为了解决循环引用的问题。__strong 是一个变量默认的修饰符,不需要写。__unsafe_unretained 因为它的不安全,基本上不会去使用它。__autoreleasing 见得最多的地方估计就是 NSError * __autoreleasing * 这个类型声明了,似乎也没有地方需要自己写。

看到 NSError * __autoreleasing * 这个类型,不禁会思考,这个 __autoreleasing 为啥放在两个星号中间呢,如果放在两个星号的左边,或者放在两个星号的右边行不行呢?

于是又开始思考另一个相关的问题。我们知道声明常量的时候,const 的位置不同,实际含义就会不同。比如:

1
NSObject const *object;

这里 object 是一个指针,指向一个 NSObject 类型的常量(通常把这种指针叫做指向常量的指针,或者常量指针)
这样声明的结果是 object 本身可以修改,但是 object 指向的内容不能修改。

1
NSObject * const object;

这里的 object 是一个常量,它的类型是一个指针(通常把这种类型叫做指针常量)
这样声明的结果是 object 本身不可以修改,但是 object 指向的内容可以修改。
但是因为 Objective-C 不支持对对象指针的解引用进行赋值,因此 object 指向的内容也无法修改。即 *str1 = *str2; 这类操作是不允许的。

1
const NSString *str = @"this is a string";

上面代码中的 const 其实是毫无意义的。

还有 object 本身和 object 指向的内容都不能修改的情况,即:

1
NSObject const * const object;

那么作为类比,对于 __strong 修饰符来说:

1
2
NSObject * __strong o;
NSObject __strong * o;

上面两句代码有什么区别?第二行中的 * 和 o 之间,日常开发时是不写空格的,这里只是为了对齐。

NSObject * __strong o;NSObject __strong *o; 的区别

探索步骤1,转成 C++ 代码

1
2
3
4
5
int main(int argc, const char * argv[]) {
NSObject * __strong o1;
NSObject __strong *o2;
return 0;
}

将上面这个 main 函数转成 C++ 代码后的结果为:

1
2
3
4
5
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,查看汇编代码

1
2
3
4
int main(int argc, const char * argv[]) {
NSObject * __strong o1;
return 0;
}
1
2
3
4
int main(int argc, const char * argv[]) {
NSObject __strong *o2;
return 0;
}

分别查看上面两个 main 函数的汇编代码,会发现结果都是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

所以这两种顺序的写法的效果应该是一样的。

所以其实:

1
2
3
NSObject * __strong o;
NSObject __strong *o;
__strong NSObject *o

上面这三种写法都是一样的。

对于另外三个修饰也都是一样,我们可以测试一下 __autoreleasing 修饰符。先随便创建一个类里面写上如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
@implementation Cat

+ (instancetype)cat {
Cat *cat = [[Cat alloc] init];
return cat;
}

- (void)dealloc {
NSLog(@"%s", __func__);
}

@end

然后编写 main 函数:

1
2
3
4
5
6
7
8
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 的例子:

1
+ (nullable id)JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt error:(NSError **)error;

这个方法的 error 参数用来在解析失败的时候提供具体错误信息,需要这么来使用它:

1
2
NSError *error = nil;
[NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];

这里的 error 参数是一个输出参数,它输出的是一个包含错误信息的 NSError 对象。
输出参数必须是一个指针,如果不是指针,参数就是值传递,函数对参数的修改无法作用到函数外部。

那为什么不用一级指针?

首先先看一下 NSError 这个类的公共属性,会发现都是 readonly 的,也就是说 NSError 是一个不可变对象,它包含的信息在创建对象的时候就确定了,不能再更改。因此这个对象必须由 JSONObjectWithData:options:error: 这个方法内部实现来创建,不能由调用方创建。

如果这里用一级指针,由于调用方不能创建 NSError 对象,且 Objective-C 不允许在栈上给对象分配内存空间,下面这种写法是不可以的:

1
2
NSError error;
[NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];

只能这么写:

1
2
NSError *error = nil;
[NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:error];

这么写的结果是给 error 参数传递了一个 nil 值。于是 JSONObjectWithData:options:error: 的内部实现在创建 NSError 对象时,不知道应该把它创建在哪个地址上,导致调用方也无法接收到这个对象。

所以下面这种标准写法:

1
2
NSError *error = nil;
[NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];

的实际含义是:

  1. 在栈上创建了一个指针叫 error,指向的类型是 NSError,并把指针指向的位置初始化为 nil。
  2. &error 获取这个指针本身在栈上的地址,它是有值的,传给 error 参数的值不是 nil。
  3. JSONObjectWithData:options:error: 方法的内部在堆上创建一个 NSError 对象,并把传入的栈上的地址指向这个 NSError 对象。
  4. 调用方就可以通过 error 变量得到这个 NSError 对象了。

所以这里为什么要用二级指针应该解释清楚了。

NSError * __autoreleasing *__autoreleasing 修饰符的位置

现在来探索一下 __autoreleasing 修饰符为什么要写在两个星号中间。

假设定义一个二级指针的变量:

1
2
Cat *cat = [Cat cat];
Cat **p = &cat;

如果直接上面这样写,编译器会报错 Pointer to non-const type ‘Cat *’ with no explicit ownership,意思是有一个指针没有显式地指定拥有关系,从这句话来理解,这里的指针是二级指针的内层指针。

1
2
Cat *cat = [Cat cat];
Cat ** __strong p = &cat;

把代码改成上面代码,编译后依然报相同的错误,所以可以得出结论,在两个星号右边写修饰符,修饰的可能是外层指针。外层指针的具体修饰情况后面再讨论。

于是把 __strong 往左边移动一个星星。

1
2
Cat *cat = [Cat cat];
Cat * __strong *p = &cat;

编译通过,可以正常运行。

再改变 __strong 的位置做几次尝试:

1
2
Cat *cat = [Cat cat];
Cat __strong **p = &cat;
1
2
Cat *cat = [Cat cat];
__strong Cat **p = &cat;

发现都可以正常编译运行,所以:

1
2
3
Cat * __strong *p;
Cat __strong **p
__strong Cat **p

这三种写法的效果是一样的,修饰符都是在修饰内层指针。
那为什么苹果自己要把 __autoreleasing 写在两个星星中间呢?可能是苹果自己的一种习惯或编码规范吧。

上面用 __strong 来举例是为了代码简洁,实际上如果要使用 __autoreleasing, cat 本身也要指明是 __autoreleasing。原因应该大家都清楚。

1
2
Cat * __autoreleasing cat = [Cat cat];
Cat * __autoreleasing *p = &cat;

对于二级指针外层指针的拥有关系的修饰

那么外层指针的拥有关系,会想到如果是下面的代码会发生什么呢?

1
2
3
4
5
6
Cat * __strong * __weak p = nil;
{
Cat *cat = [Cat cat];
p = &cat;
}
NSLog(@"%@", *p);

结果打印 (null),似乎是合理的,weak 指针会在指向的对象释放自动至为 nil。

1
2
3
4
5
6
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 *