二级指针与ARC鲜为人知的特性

先看一眼熟知的代码

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSData *data = [@"{\"key\":\"value\"}" dataUsingEncoding:NSUTF8StringEncoding];
    
    NSError *error = nil;
    id dataObj = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
    if (error) {
        NSLog(@"解析JSON出错。 error : %@",error);
    } else {
        NSLog(@"解析JSON正确。 dataObj : %@",dataObj);
    }
}
复制代码

上述代码中,出现了NSError的实例。该实例是用来代表发生了某种错误。在ARC中因为使用异常处理会形成内存管理的不便(可能形成内存泄露,或者加入大量样板代码),因此用NSError代表发生了错误是一种不错的选择,苹果的API中也大量使用了NSError。objective-c

这里请关注[NSJSONSerialization JSONObjectWithData:data options:0 error:&error]的最后一个参数:error:(NSError **)error;。该方法使用了二级指针做为参数传入,经由此参数能够将方法中新建立的NSError对象回传给调用者,因此该参数也称为“输出参数”。从这种类型的参数入手,后面咱们将讨论一个很严肃的问题~函数

咱们来实现一个相似的方法(也就是方法里新建立一个对象回传给调用者)

1. 不用二级指针我直接传个view进方法里不就能够建立一个view了吗?

代码:this

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIView *thisIsNilView = nil;		// 声明一个view,可是还有没建立
    NSLog(@"1. thisIsNilView指向的实例 : %@",thisIsNilView);
    [self createView:thisIsNilView];
    NSLog(@"4. thisIsNilView指向的实例 : %@",thisIsNilView);
}

- (void)createView:(UIView *)view {
    NSLog(@"2. 方法里的view指向的实例 : %@",view);
    view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    NSLog(@"3. 方法里的view指向的实例 : %@",view);
}
复制代码

看起来很简单呢,我声明一个空的thisIsNilView,传到一个createView:方法里,方法里会帮我建立一个view,那么thisIsNilView不就有值了?spa

让咱们看看运行结果:3d

1. thisIsNilView指向的实例 : (null)
 2. 方法里的view指向的实例 : (null)
 3. 方法里的view指向的实例 : <UIView: 0x7f956ee00220; frame = (100 100; 100 100); layer = <CALayer: 0x600000029420>>
 4. thisIsNilView指向的实例 : (null)
复制代码

哪里出问题了?方法里明明建立出了一个view啊?指针

咱们来探究探究究竟是哪里出了问题。code

回想下thisIsNilView是个什么东西?恩,是个指向UIView的指针(是个指针、是个指针、是个指针),那么咱们来看看指针在方法里是否正确指向了生成的UIView实例。cdn

我改动了下代码:对象

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIView *thisIsNilView = nil;
    NSLog(@"1.0 thisIsNilView指向的实例 : %@",thisIsNilView);
    NSLog(@"1.1 thisIsNilView指针的地址 : %p",&thisIsNilView);
    NSLog(@"--------- 开始执行createView:方法 ---------");
    [self createView:thisIsNilView];
    NSLog(@"--------- 执行createView:方法结束 ---------");
    NSLog(@"4.0 thisIsNilView指向的实例 : %@",thisIsNilView);
    NSLog(@"4.1 thisIsNilView指针的地址 : %p",&thisIsNilView);
}

- (void)createView:(UIView *)view {
    NSLog(@"2.0 方法里的view指向的实例 : %@",view);
    NSLog(@"2.1 方法里的view指针的地址 : %p",&view);
    view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    NSLog(@"3.0 方法里的view指向的实例 : %@",view);
    NSLog(@"3.1 方法里的view指针的地址 : %p",&view);
}
复制代码

为了方便查看结果,加了几行打印~blog

1.0 thisIsNilView指向的实例 : (null)
 1.1 thisIsNilView指针的地址 : 0x16fd35f18
 
 --------- 开始执行createView:方法 ---------
 2.0 方法里的view指向的实例 : (null)
 2.1 方法里的view指针的地址 : 0x16fd35ee8
 
 3.0 方法里的view指向的实例 : <UIView: 0x12de0b6a0; frame = (100 100; 100 100); layer = <CALayer: 0x610000034640>>
 3.1 方法里的view指针的地址 : 0x16fd35ee8
 --------- 执行createView:方法结束 ---------
 
 4.0 thisIsNilView指向的实例 : (null)
 4.1 thisIsNilView指针的地址 : 0x16fd35f18
复制代码

额,好像thisIsNilView这个指针(位于0x16fd35f18这块内存区域中)传入方法后变成另一个指针(位于0x16fd35ee8这块内存区域中)了啊。

插个内存图理解下:

第一步:

我是配图

第二步:

我是配图

第三步:

我是配图

第四步:

我是配图

为什么第二步进入方法后会凭空多出一个指针?哦忘了说,指针也是个变量,指针做为参数传递的时候,指针“自己”也是值传递,也就是说复制了一个“与原指针指向相同内存地址”的指针。好像有点绕,其实就是第二步的图。

回想下C语言基础中的参数传递:基本数据类型是复制一份进行传递,可是指针传递是引用传递,能够修改变量自己的内容。说是这样说,可是不够全面。指针传递其实也是个复制传递,只不过复制的是“指针”,可是“复制后的指针”中的内容(也就是指针指向的地址)仍是指向了原来指向的内容。

这个指针复制传递仍是有那么点儿绕,咱们用指针与int基本类型作个对比:

int a = 10;

int *p = &a;

对应关系:

a 是个 int 类型的变量;

a 的内容是 10;

p 是个 int * 类型的变量(俗称指针);

p 的内容是 a这个变量在内存中的地址(好比0xa);

函数:

void testIntCopy(int b) {
    int c = b;
}

void testPointCopy(int *pointer) {
    printf("%p",pointer);
}
复制代码

在testIntCopy中传入a,那么将会拷贝一份a的内容:10(数值) 到 b(int类型的变量) 中。以后就能够正常使用了。

在testPointCopy中传入p,那么将会拷贝一份p的内容:指向a在内存中的地址(如0xa) 到 pointer(int *类型的变量) 中。以后就能够正常使用了,好比修改pointer指向的内存中的值。

这样子理解是否是轻松一点?那么以前第二步的图就能够理解了。

这说明了一个问题:一级指针做为参数传递没法修改原指针指向的值。


2. 那得用二级指针才能在方法里建立并回传给调用者一个view是吗?

是否是咱们先上个代码看看:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIView *thisIsNilView = nil;
    NSLog(@"1.0 thisIsNilView指向的实例 : %@",thisIsNilView);
    NSLog(@"1.1 thisIsNilView指针的地址 : %p",&thisIsNilView);
    NSLog(@"--------- 开始执行createViewWithSecRankPointer:方法 ---------");
    [self createViewWithSecRankPointer:&thisIsNilView];
    NSLog(@"--------- 执行createViewWithSecRankPointer:方法结束 ---------");
    NSLog(@"4.0 thisIsNilView指向的实例 : %@",thisIsNilView);
    NSLog(@"4.1 thisIsNilView指针的地址 : %p",&thisIsNilView);
}

- (void)createViewWithSecRankPointer:(UIView **)view {
    NSLog(@"2.0 方法里的*view指向的实例 : %@",*view);
    NSLog(@"2.1 方法里的*view指针的地址 : %p",view);
    *view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    NSLog(@"3.0 方法里的*view指向的实例 : %@",*view);
    NSLog(@"3.1 方法里的*view指针的地址 : %p",view);
}
复制代码

注意方法已经不是原来的方法了,注意方法里所打印的东西也已经有所变动。

看结果前咱们先分析分析这些代码究竟干了什么:

1. 有一个UIView * 类型的指针: thisIsNilView ,而后应该还有一个指向thisIsNilView这个指针的指针:咱们姑且假设它为thisIsNilViewFatherPointer。

2. 咱们要进入createViewWithSecRankPointer:方法了!按照上文讲的“指针值传递”,咱们传递了thisIsNilViewFatherPointer的值(也就是thisIsNilView的地址)给了createViewWithSecRankPointer:方法。此时方法里的view(二级指针),应该是个thisIsNilViewFatherPointer指针的拷贝,但指向的仍是thisIsNilView这个指针(内容从thisIsNilViewFatherPointer拷贝过来了嘛)。

3. 好的,我既然能够拿到thisIsNilView这个指针了(经过*view),那么我总算能够修改thisIsNilView这个指针的指向了,让thisIsNilView指向一个全新建立的UIView实例把!!!

4. 执行完方法了,那么thisIsNilView这个指针应该指向的是刚才在方法里新建立的view,那么咱们就完成了一个“输出参数”了对吗。

看看执行结果:

1.0 thisIsNilView指向的实例 : (null)
 1.1 thisIsNilView指针的地址 : 0x16fd75f18
 
 --------- 开始执行createViewWithSecRankPointer:方法 ---------
 2.0 方法里的*view指向的实例 : (null)
 2.1 方法里的*view指针的地址 : 0x16fd75f10
 3.0 方法里的*view指向的实例 : <UIView: 0x15bd07ff0; frame = (100 100; 100 100); layer = <CALayer: 0x174033f20>>
 3.1 方法里的*view指针的地址 : 0x16fd75f10
 --------- 执行createViewWithSecRankPointer:方法结束 ---------
 
 4.0 thisIsNilView指向的实例 : <UIView: 0x15bd07ff0; frame = (100 100; 100 100); layer = <CALayer: 0x174033f20>>
 4.1 thisIsNilView指针的地址 : 0x16fd75f18
复制代码

很好,执行方法完毕后thisIsNilView有值了!并且仍是方法里新建立的UIView实例!

等等!好像哪里有点不对!

为什么方法里的*view(也就是thisIsNilView指针)和方法外面的thisIsNilView不是同一个?????

根据咱们上述4点严谨的分析,方法里的*view应该就是thisIsNilView这个指针无误!

在实践结果里,方法内部出现了一个位于0x16fd75f10内存地址中的指针,而后让这个指针指向了一个新建立的UIView实例,然鹅这和thisIsNilView这个指针(位于0x16fd75f18内存地址)有毛线关系?????然鹅出了方法thisIsNilView竟然仍是指向了那个新建立的对象!!!!!

画个内存图看看先:

第一步:

我是配图

第二步:

我是配图

第三步:

我是配图

第四步:

我是配图

这里真的有两个很神奇的地方:

1 第二步为什么会多出一个指针?

2 第四步为什么会把原先指向nil的thisIsNilView指向了新建立的UIView对象?


3. 总算要说说ARC鲜为人知的特性了

单从上述代码时没法解释为什么会产生这种现象的。

在浏览官方文档《Transitioning to ARC Release Notes》的时候,偶然发现有这么一段:

我是配图

文中提到,二级指针做为参数“一般”都是__autoreleasing修饰的,注意一般这个词,后面会提到。当实际传入的参数为__strong修饰的时候,编译器会建立一个用__autoreleasing修饰的临时变量tmp,用来和方法参数的修饰符匹配,方法执行完毕后再从新用tmp赋值回error。 (苹果这么作主要是为了保证在方法内部建立出来的对象可以被良好地释放,由于createViewWithSecRankPointer:方法不能保证调用者在拿到这个对象后可以合理释放掉) 编译器的这种行为恰好可以印证咱们上述“很神奇”的两个地方:

1. tmp变量恰好就是第二步中多出的那个指针0x16fd75f10,用这个临时变量来保存新建立的UIView对象

2. error = tmp恰好对应咱们的第四步,出了方法后从新赋值给原来的变量thisIsNilView

BUT:咱们的方法参数并非(UIView * __autoreleasing *)这种类型啊,咱们是(UIView **)类型呢。其实苹果文档里说的“一般”是有依据的:

编译器会把指向OC对象的指针的二级指针参数自动加上__autoreleasing修饰符。

咱们能够经过Xcode自动补全功能一窥究竟:

我是配图

4. 咱们反过来验证下ARC鲜为人知的特性

既然文档里说了,__strong__autoreleasing语义不符,因此编译器会这么作,那么若是咱们使用__autoreleasing修饰了thisIsNilView指针呢。

看看修改后的代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIView * __autoreleasing thisIsNilView = nil;
    NSLog(@"1.0 thisIsNilView指向的实例 : %@",thisIsNilView);
    NSLog(@"1.1 thisIsNilView指针的地址 : %p",&thisIsNilView);
    NSLog(@"--------- 开始执行createViewWithSecRankPointer:方法 ---------");
    [self createViewWithSecRankPointer:&thisIsNilView];
    NSLog(@"--------- 执行createViewWithSecRankPointer:方法结束 ---------");
    NSLog(@"4.0 thisIsNilView指向的实例 : %@",thisIsNilView);
    NSLog(@"4.1 thisIsNilView指针的地址 : %p",&thisIsNilView);
}

- (void)createViewWithSecRankPointer:(UIView **)view {
    NSLog(@"2.0 方法里的*view指向的实例 : %@",*view);
    NSLog(@"2.1 方法里的*view指针的地址 : %p",view);
    *view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    NSLog(@"3.0 方法里的*view指向的实例 : %@",*view);
    NSLog(@"3.1 方法里的*view指针的地址 : %p",view);
}
复制代码

直接看看执行结果:

1.0 thisIsNilView指向的实例 : (null)
 1.1 thisIsNilView指针的地址 : 0x16fde9f18
 
 --------- 开始执行createViewWithSecRankPointer:方法 ---------
 2.0 方法里的*view指向的实例 : (null)
 2.1 方法里的*view指针的地址 : 0x16fde9f18
 3.0 方法里的*view指向的实例 : <UIView: 0x10020c4a0; frame = (100 100; 100 100); layer = <CALayer: 0x170222740>>
 3.1 方法里的*view指针的地址 : 0x16fde9f18
 --------- 执行createViewWithSecRankPointer:方法结束 ---------
 
 4.0 thisIsNilView指向的实例 : <UIView: 0x10020c4a0; frame = (100 100; 100 100); layer = <CALayer: 0x170222740>>
 4.1 thisIsNilView指针的地址 : 0x16fde9f18
复制代码

在语义相符的状况下,传入的就是&thisIsNilView无误,编译器不会添加额外代码。

  • 补充一点:createViewWithSecRankPointer:方法就算内部不建立对象,参数也会被编译器自动加上__autoreleasing。

总结下这篇文章讲了什么

1. 指针做为参数传递的时候,指针自己是值传递。

2. 为什么用一级指针传入参数没法成为“输出参数”。

3. 二级指针做为参数传递时,ARC为了校准语义,会进行“自动补全”功能。

相关文章
相关标签/搜索