iOS 关于block 、__weak、 __strong 对象引用计数的思考

开篇寄语:

说到iOS下对象的释放一般都会想到引用计数这个概念,引用计数是否为 0 决定着对象是否要被回收。

声明一个临时变量,这个指针是存在栈区的,这个栈区的指针保存的内容是一个已在堆区开辟空间的对象地址。存在栈区的指针的创建与释放是由系统控制的,而堆区对象则需要手动创建及控制销毁。

问题一、__weak 、__strong 、__unsafe_unretained 修饰符对对象引用计数的影响

先上一段未加任何修饰符的代码,如下:
    Person * p2;
    {
        Person * p = [[Person alloc] init];
        Person * p1 = p;
        p2 = p;
        NSLog(@"p 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p)));
        NSLog(@"p1 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p1)));
        NSLog(@"p2 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p2)));
    }
    NSLog(@"p2 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p2)));
    NSLog(@"p2 = %@",p2);

打印结果

如果你正在面试,或者正准备跳槽,不妨看看我精心总结的面试资料:https://gitee.com/Mcci7/i-oser 来获取一份详细的大厂面试资料 为你的跳槽加薪多一份保障

可以看到对于 p 的引用就是始终为 3,这里也没有任何异议,因为 3 个栈区的指针指向了同一块内存地址,引用计数就变为 3,这里有一个注意的地方,

代码在 p 对象 声明前后是加了一对 {} 这样的目的是 p 指针在出了 {} 后是会被系统销毁,那么,p、 p1 指针销毁后其指向的堆区内存对象引用计数就会相应 -1,注意看红框内打印的引用计数值,此时就只变成了 p2 在引用这块内存。

由于对象的引用计数未清 0 ,所以,先打印的是 p2 然后,在触发了对象销毁。

__strong 程序默认的变量修饰符就是 __strong ,所以,打印的结果与不加修饰符的一致。
    __strong Person * p2;
    {
        Person * p = [[Person alloc] init];
        Person * p1 = p;
        p2 = p;
        NSLog(@"p 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p)));
        NSLog(@"p1 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p1)));
        NSLog(@"p2 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p2)));
    }
    NSLog(@"p2 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p2)));
    NSLog(@"p2 = %@",p2);

打印结果

__weak** 修饰

    __weak Person * p2;
    {
        Person * p = [[Person alloc] init];
        Person * p1 = p;
        p2 = p;
        NSLog(@"p 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p)));
        NSLog(@"p1 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p1)));
        NSLog(@"p2 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p2)));
    }
    NSLog(@"p2 = %@",p2);

打印如下

首先,这里有个小问题,用 __weak 修饰的 p2 引用计数为啥还是 3 ,这里可以这样理解,系统在使用 __weak 修饰的对象时会创建一个临时变量,这个临时变量会增加对象的引用计数,一旦代码执行完成,这个临时变量就会销毁,所以,这里打印的是 3 ,但它不会影响程序运行的最终结果,因为这个临时变量会及时销毁,可以联想一下 block__strong 的作用。这里需要提出一点,一个对象的属性在用 . 语法的时候或者 set 方法的时候,引用计数也会加一,但这个加一并不会在代码执行完成后 -1 ,但该操作不会影响对象的释放。

其次,这里可以看到 Person 对象先进行销毁了,然后再打印的 p2 = null ,因为出来 {} 作用域 Person 对象就销毁了,所以先执行了销毁操作, __weak 修饰的 p2 被系统置为了 nil,对 nil 发送消息 iOS 下并不会崩溃。

__unsafe_unretained** 修饰

    __unsafe_unretained Person * p2;
    {
        Person * p = [[Person alloc] init];
        Person * p1 = p;
        p2 = p;
        NSLog(@"p 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p)));
        NSLog(@"p1 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p1)));
        NSLog(@"p2 引用计数---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p2)));
    }
    NSLog(@"p2 = %@",p2);

打印如下

首先,这里的 p2 引用计数并没像 __weak 的那样会 +1,再次,这里在 Person 对象作用域外执行打印 p2 时候崩溃了,

由于 __unsafe_unretained 修饰的变量并不会在对象销毁后置为 nil,所以,程序访问了 野指针 崩溃了。

问题二、block 对引用计数的影响

    Person * p = [Person alloc];
    void(^block)(void) = ^{
        NSLog(@"p---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p)));
    };
    block();

打印如下

这的 Person 的引用计数为 3

两个疑问:1 为什么引用计数为 3;2、这里对象销毁了,是否不存在循环引用?

疑问一、这里来看看 block 里对 Person 对象都干了什么?打印一下地址信息

    Person * p = [Person alloc];
    NSLog(@"block 前 栈区 p 地址 = %p,p 堆区内存地址 = %p",&p,p);
    void(^block)(void) = ^{
        NSLog(@"block 后 栈区 p 地址 = %p,p 堆区内存地址 = %p",&p,p);
        NSLog(@"p---%ld",CFGetRetainCount(( __bridge CFTypeRef)(p)));
    };
    block();

打印如下

这里可以看到堆区的内存地址并没有变化,其实就是指向同一个对象,但是指针的地址变了,变到堆区了,也就是在 block 里面 copy 出来一个指向同一个堆区地址指针,这样就好理解了,block 的内部持有了外部变量引用计数 +1,但方法本身结束时候便会自动消减这次引用计数。

copy 出了一个指针,指针指向的内存地址还是 Person 对象,引用计数再次 +1,这样,执行一次 block,对象的引用计数 +2

疑问二、为啥没有导致循环引用?

Person 对象并没有持有 block,二者并没有互相引用,block 执行完以后 Person 对象也就可以去释放了,所以,不存在循环引用的问题。

上面的 block 的类型为 NSMallocBlock ,用 __weak 修饰一下 block 变为 NSStackBlock 类型看看有什么变化。

这里有个警告,意思是这个 block 在赋值之后就会被销毁,但是 block 释放是在调用完成之后才会释放的,所以,这里有警告,当后面再用 __weak 修饰的 block 时候,它还可以再堆内存地址中找到这 block,这里不会在赋值后立即释放。

打印如下

首先,这里的 block 变为了 NSStackBlock 类型,block 里面 Person 对象的地址也由原来的 堆区地址 变为了 栈区地址Person 对象的引用计数为 2。这里猜测,代码块内部并没有增加引用计数。

但是 NSStackBlock 类型的 block 很少用,因为调用一次就会释,但大多数业务场景是 block 要有随时处理逻辑的能力。

NSStackBlockNSMallocBlock 类型的 block 本质都属于对象,内存地址都堆区。但是,不同的类型决定了 block 的释放时机。

本文仅仅是思考与总结,有不对地方欢迎指针,互相学习。

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

推荐阅读更多精彩内容