美文网首页IOS开发者学习笔记iOS TechRunTime
二级指针与ARC不为人知的特性

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

作者: 从来吃不胖 | 来源:发表于2017-01-08 14:36 被阅读577次

先看一眼熟知的代码

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

相关文章

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

    先看一眼熟知的代码 上述代码中,出现了NSError的实例。该实例是用来表明发生了某种错误。在ARC中由于使用异常...

  • 二级指针

    二级指针做函数输出特性 指针数组的使用 一维数组

  • __autoreleasing ARC下关于Objective-

    最近在重温iOS与OSX多线程和内存管理这本书,发现平常没注意到的问题。 在ARC下 如何使用二级指针 NSO...

  • 02-C语言的指针

    02-C语言的指针 目标 C语言指针释义 指针用法 指针与数组 指针与函数的参数 二级指针 函数指针 指针在C中很...

  • C语言基础及指针④函数指针

    接续上篇C语言基础及指针③函数与二级指针 在上一篇中 , 我们学习了函数与二级指针 , 函数和java中的方法类似...

  • Objective-C的间接指针

    昨日的一篇写的__autoreleasing ARC下关于Objective-C二级指针 让我有点不一样的想法 感...

  • 九、自动引用计数ARC @GeekBand

    ARC OC默认的内存管理机制 受ARC管理的对象 OC对象指针 Block指针 使用attribute((NSO...

  • 面试问题记录 2

    1 ARC与MRC的内存管理 以及是如何实现的 ? 答:MRC 和 ARC 都是编译器特性,(Objective-...

  • ARC

    ARC又叫自动引用计数. ARC的判断准则:只要没有强指针指向对象,就会释放对象。 指针分两种: 1)强指针,默认...

  • ARC中强指针与弱指针

    ARC是苹果为了简化程序员对内存的管理,推出的一套内存管理机制使用ARC机制,对象的申请和释放工作会在运行时,由编...

网友评论

  • 22a3fbb48a09:楼主提到一级指针作为参数传递无法修改原指针指向的值,这句话不适用于基本类型吧,int和bool等基本类型,是可以直接通过基本类型改变实参的,楼主怎么看
    从来吃不胖:@AliceJordan 基本类型没问题的
    AliceJordan:基本类型的话作为指针传递的话 应该是是可以的吧
    ‘’‘
    void changea (int* b)
    {
    printf("b1--%d\n",*b);
    *b = *b +1;
    printf("b2--%d\n",*b);
    }

    int main(int argc, char *argv[]) {
    int* a;
    int b =1;
    a = &b;
    printf("a1---%d\n",*a);
    changea(a);
    printf("a2---%d\n",*a);
    }
    ’‘’
    从来吃不胖:不能改变啊。int a = 10;把a传给一个函数,在函数内改变a,也并不能改变a的值啊
  • RYANIM:感谢, 今天遇到这个引用传递的疑惑, 正好解答了.
    从来吃不胖:@RYANIM 有幸:smile:
  • 我是C:这个nil 在什么区? 静态区吗?
    从来吃不胖:https://stackoverflow.com/questions/1309535/why-does-nsarray-arraywithobjects-require-a-terminating-nil
  • 我是C:问个问题,在方法调用返回的时候,方法里的变量,实例不是都释放了吗? __autoreleasing 修饰的对象,释放tmp 还是释放 实参?
    从来吃不胖:@我是C 实参就是实际传入的参数,在苹果的例子里,就是那个 NSError * __autoreleasing tmp。但是我们在写的时候并没有写这个变量,这个是编译器加上去的,我们的目的是希望赋值到 NSError * __strong error; 这个变量上去,所以出了方法,必须给error = tmp,才能够实现我们原来的目的啊
    我是C:@从来吃不胖 感谢回复! __autoreleasing 修饰的是tmp 临时的那个,苹果是为了保证释放tmp 这个对象,然而 实参=tmp? 有点懵
    从来吃不胖:方法里临时创建的变量必然是释放的。但是这里在方法里是创建变量赋值给一个 __autoreleasing 修饰的指针,那么会延迟释放,在方法外部又将 tmp 重新赋值给原来的 __strong 修饰的变量,从而达到不释放这个变量的目的
  • 我是C:函数调用,传参的时候本质都是值拷贝 ,变量值拷贝一份,变量地址其实是变了,变量值拷贝和实参值是一样的,
    比如传指针的时候,形参和实参,指针的本身地址是不一样的,但是指针指向的地址是一样的

    从来吃不胖:是这样的
  • 阿两sama:请教一下,当实参为strong的可变数组时,为什么形参改变会影响到实参
    从来吃不胖:@阿两sama 操作指针与赋值指针是两种操作哦。在文章最后的总结里,第一点说的是:指针作为参数传递的时候,指针本身是值传递。
    打个比方,在-(void)xxx:(NSMutableArray *)array方法外面的mutableArray是你(一个NSMutableArray *指针),你有一条绳子牵着一条狗(NSMutableArray对象)。进方法的时候,由于指针作为参数传递是值传递,也就是说,这时候,方法内部有一个我(一个NSMutableArray *指针),你把绳子交给我,我牵着这条狗了(NSMutableArray对象),我可以使唤狗去吃东西(addObject:),可以让狗拉东西(removeObject:)。但是我并没有办法让你去牵我家里的大藏獒(另一个NSMutableArray对象)。这是一级指针作为参数没有办法做到的场景
    阿两sama:@从来吃不胖 感谢楼主回复(*^_^*)。我说的不是二级指针,是一级指针-(void)xxx:(NSMutableArray *)array 这种情况,直接操作array就是操作原来的mutableArray,但是我不明白为什么可变数组就不用拷贝指针了呢。还有我发现NSMutableString也会存在形参改变影响到实参。
    从来吃不胖:你说的请况,是指 形参是二级指针,如 -(void)xxx:(NSMutableArray **)array,然后外界传入的是 __strong NSMutableArray *mutableArray吗?
    如果是这种情况,在方法内部,通过*array不就可以拿到一个 NSMutableArray * __autoreleasing tempArr了吗?通过直接操作这个指针,其实就是操作原来的 mutableArray 了

本文标题:二级指针与ARC不为人知的特性

本文链接:https://www.haomeiwen.com/subject/mabqbttx.html