二级指针与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。

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

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

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

代码:

- (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不就有值了?

让我们看看运行结果:

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

哪里出问题了?方法里明明创建出了一个view啊?

我们来探究探究到底是哪里出了问题。

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

我改动了下代码:

- (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);
}

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

 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为了校准语义,会进行“自动补全”功能。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容

  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,125评论 29 470
  • __block和__weak修饰符的区别其实是挺明显的:1.__block不管是ARC还是MRC模式下都可以使用,...
    LZM轮回阅读 3,284评论 0 6
  • 1.写一个NSString类的实现 +(id)initWithCString:(c*****t char *)nu...
    韩七夏阅读 3,747评论 2 37
  • iOS面试小贴士 ———————————————回答好下面的足够了------------------------...
    不言不爱阅读 1,962评论 0 7
  • 临早, 临晚。 万物并作, 不临其尽。 人如此, 循环, 不解。
    借酒消愁阅读 166评论 0 0