iOS基础:深入内存管理-从所有权修饰符开始

一、谁适合看本文

为了不浪费大家时间,我把这个写在最前面。

    id __weak obj1 = nil;
    {
        id __strong obj2 = [[NSObject alloc] init];
        obj1 = obj2;
        NSLog(@"%@", obj1);
    }
    NSLog(@"%@", obj1);

如果你能一下子说出输出什么,并且脑子里清晰的知道在以上代码中谁持有谁,那么,前辈好😃
如果觉得还是有点乱或者反应不过来,那就往下看呗,而且我们是同一类人。🙄并且我向你保证,看完绝对舒畅无比。

注意

本篇文章不会解释内存管理的思考方式!
本篇文章不会解释引用计数的工作原理!
本文会一行行代码分析所有权修饰符的使用以及内存管理的原理。

二、前言

在当下,大多应用都是在ARC环境中开发的,因此内存管理这块知识点常常被人遗忘。但是并不意味着有了ARC,开发人员就不需要了解内存管理,或者说只要大概了解就行了。

最近几天在深入了解苹果的内存管理机制,也有所收货,我打算记录在博客中和大家分享。个人认为,内存管理的思考方式和引用计数的工作原理都比较简单,大致看一下博客就能理解,所以本文不提了。本文主要会举例来介绍四个所有权修饰符,然后分析例子,最后理解内存管理。

三、所有权修饰符

在ARC环境中,id类型和对象类型与c语言类型不同,它的类型上必须附加所有权修饰符。
所有权修饰符一共有四种:

__strong
__weak
__unsafe_unretained
__autoreleasing

下面我会一个个来介绍。

1.__strong

a.默认情况下,修饰符为__strong

在上面所有权修饰符的描述中,有说到类型必须附加所有权修饰符,但是你是不是有所疑惑,我们平时的代码中,很少有看到这样的修饰符啊。
其实是这样的,在没有明确指定所有权修饰符的情况下,默认用__strong,所以以下两行代码完全一致。

id obj = [[NSObject alloc] init];
id __strong obj = [[NSObject alloc] init];

b.自己生成并持有的对象情况下__strong的使用

{
    // 自己生成并持有对象
    id __strong obj = [[NSObject alloc] init]; // 对象A
    // obj为强引用,持有对象A
}
// obj的作用域结束了,所以obj释放自己持有的对象A
// 对象A不被持有了,就废弃对象A

如上例子,这是最经常遇到的情况了。短短三行代码,其实做了很多事情。
因为是第一个例子,为了照顾基础差的同学,我再补充一点:
为了便于理解,我们可以认为对象A是一条狗,obj是遛狗的人,强引用就是狗链子。当obj超出作用域的时候,链子断了,狗因为不再有链子拴着它,就逃跑了。

c.不是自己生成并持有的对象情况下__strong的使用

我们知道使用alloc/copy/new等方法或以他们开头的方法可以获得自己生成并持有的对象,而其他方法如array就只能取得非自己生成并持有的对象。(这就是为什么在非ARC中,使用array方法初始化后需要retain才能持有)
那么在这种情况下,系统又做了那些事情呢?

{
    // 取得非自己生成并持有对象
    id __strong obj = [NSArray array]; // 对象
    // obj为强引用,持有NSArray的对象
}
// obj的作用域结束了,所以obj释放自己持有的对象
// 对象不被持有了,就废弃对象

到这里可以发现不管使用[[NSObject alloc] init]或者[NSArray array],对象的所有者和对象的生命周期都是明确的,不会发生错误。
同时我们可以大胆的猜测一下,这就是在ARC环境中,[NSArray array][[NSArray alloc] init]没差别的原因。

d.__strong修饰的变量之间的赋值

{
    id __strong obj0 = [[NSObject alloc] init];
    id __strong obj1 = [[NSObject alloc] init];
    id __strong obj2 = nil;
    obj0 = obj1;
    obj2 = obj0;
    obj1 = nil;
    obj0 = nil;
    obj2 = nil;
}

上面的代码是变量之间的相互赋值,你能不能像我刚刚那样一行行分析出来呢?

- (void) test
{
    // obj0持有对象A的强引用
    id __strong obj0 = [[NSObject alloc] init]; // 对象A
    // obj1持有对象B的强引用
    id __strong obj1 = [[NSObject alloc] init]; // 对象B
    // obj2不持有对象    
    id __strong obj2 = nil;
    
    
    // obj0 持有了 obj1持有的对象(对象B) 的强引用
    // 也就是说,对象B现在被obj0和obj1同时持有(狗被两条链子拴着。。。)
    // 同时,对象A没有被持有了,就废弃了
    obj0 = obj1;
    
    // obj2 持有了 obj0持有的对象(对象B) 的强引用
    // 也就是说,对象B现在被obj0和obj1还有obj2同时持有 (三条链子了。。。)
    obj2 = obj0;
    
    // obj1释放了对象B (剩下两条了)
    obj1 = nil;
    // obj0释放了对象B (就剩一条了)
    obj0 = nil;
    // 最后obj2释放了对象B (没绳子了)
    obj2 = nil;
    // 对象B没有人持有,就销毁了。
}

注意啊,这里可别把持有,销毁,释放这几个动词弄糊涂了。我当初可是在这个坑里呆了好几年呢。
持有:变量对对象的。
释放:变量对对象的。
销毁:系统对对象的。

e.__strong修饰的变量在方法参数上的使用

新建一个继承NSObject的类

// .h
@interface Model : NSObject
{
    id __strong obj;
}
- (void)setObj:(id __strong)obj;
@end
//.m
@implementation Model
- (void)setObj:(id)obj
{
    _obj = obj;
}
@end

然后使用这个类

- (void) test
{
    id __strong model = [[Model alloc] init];
    
    [model setObj:[[NSObject alloc] init]];
}

下面和我一起分析一下吧

- (void) test
{
    // model 持有 Model对象
    id __strong model = [[Model alloc] init];
    
    // Model对象的obj变量 持有NSObject对象
    [model setObj:[[NSObject alloc] init]];
}
// model作用域结束,强引用失效
// 所以Model对象没有持有者,被销毁
// 同时Model对象的obj变量也被废弃,NSObject对象没有持有者
// NSObject对象被废弃

2.__weak

现在看到__weak我的第一反应就是循环引用来了😫
不知道你对循环引用是否彻底了解。下面我们还是和上面例子一样,一行行分析。

a.相互持有问题

这里还是使用1.e创建的类。

- (void) test
{
    // model1 持有 ModelA
    id __strong model1 = [[Model alloc] init]; //ModelA
    // model2 持有 ModelB
    id __strong model2 = [[Model alloc] init]; //ModelB
    
    // ModelA的obj变量 持有ModelB强引用
    // ModelB现在被model1和ModelA的obj同时持有
    [model1 setObj:model2];
    
    // ModelB的obj变量 持有ModelA强引用
    // ModelA现在被model2和ModelB的obj同时持有
    [model2 setObj:model1];
}
// model1 作用域结束,强引用失效
// 所以自动释放ModelA对象,这时ModelA被ModelB的obj持有(还有一条链子)
// model2 作用域结束,强引用失效
// 所以自动释放ModelB对象,这时ModelB被ModelA的obj持有(还有一条链子)
// 内存泄漏

解决办法:将Model类中obj变量的修饰词变成__weak

// .h
@interface Model : NSObject
{
    id __weak obj;
}
- (void)setObj:(id __strong)obj;

其他都不需要变,就可以解决循环引用了,具体分析我就不写了。

b.对象废弃时,弱引用失效并置nil

    id __weak obj1 = nil;
    {
        id __strong obj2 = [[NSObject alloc] init];
        obj1 = obj2;
        NSLog(@"%@", obj1);
    }
    NSLog(@"%@", obj1);

以上代码输出如下:

2017-04-21 15:20:10.645 MRCTest[42868:3529966] <NSObject: 0x60800001c180>
2017-04-21 15:20:10.645 MRCTest[42868:3529966] (null)

可以发现第二次输出时候,obj1已经为空了。分析:

    id __weak obj1 = [[NSObject alloc] init];
    
    {
        // obj2持有NSObject对象强引用
        id __strong obj2 = [[NSObject alloc] init];
        
        // obj1持有NSObject对象的弱引用
        obj1 = obj2;
        
        NSLog(@"%@", obj1);
    }
    
    //obj2作用域结束,obj2释放NSObject对象
    //NSObject对象无持有者,被销毁
    //obj1弱引用失效,obj1=nil
    NSLog(@"%@", obj1);

c.__weak的小结

__weak可以避免循环引用,也可以通过查看__weak修饰的变量是否为nil判断赋值的对象是否已经废弃。
但是__weak只能在iOS5以上以及OS X Lion上使用。在iOS4和OS X Snow Leopard中,只能用__unsafe_unretained代替。

3.__unsafe_unretained

还是上一个例子


    id __unsafe_unretained obj1 = [[NSObject alloc] init];
    
    {
        // obj2持有NSObject对象强引用
        id __strong obj2 = [[NSObject alloc] init];
        
        // obj1持有NSObject对象的弱引用
        obj1 = obj2;
        
        NSLog(@"%@", obj1);
    }
    
    //obj2作用域结束,obj2释放NSObject对象
    //NSObject对象无持有者,被销毁
    NSLog(@"%@", obj1);
    // obj1表示的对象已经被废弃
    // 出现垂悬指针

结果:

__unsafe__unretained垂悬指针.png

程序奔溃。
也就是说,在使用__unsafe__unretained修饰符时,赋值给附有__strong修饰符的变量时,需要确保被赋值的对象确实存在。

4.__autoreleasing

这篇文章本应该四月二十三日就发布出来,但是却拖到了今天,就因为这个__autoreleasing,因为里面涉及到了很多知识点,在总结的时候,自己又搞糊涂了。
因为我认为这篇文章总结的是所有权修饰符,所以在这里不花大篇幅去做实验。我准备新建一篇文章来专门写__autoreleasing涉及到的知识点。
下面就大致写一下__autoreleasing的使用。

a.__autoreleasing替代autorelease方法

在ARC中,是不能使用autorelease方法和NSAutoreleasePool类。那么如何使用自动释放池呢?其实�在ARC中,使用@autoreleasepool可以代替NSAutoreleasePool。以下两段代码的作用是一致的。

    //在ARC无效时
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [obj autorelease];
    [pool drain];
    //在ARC有效时
    @autoreleasepool {
        id __autoreleasing obj2;
        obj2 = obj;
    }

这里的__autoreleasing的作用就是告诉编译器,obj2变量是__autoreleasing类型的,它能延缓obj2释放对象。

b.延缓释放对象

下面举个例子:

    id __unsafe_unretained obj0;
    @autoreleasepool {
        {
        id __autoreleasing obj1 = [[NSObject alloc] init];
        obj0 = obj1;
        }
        NSLog(@"%@", obj0);
    }
    NSLog(@"%@", obj0);

结果:第一行输出有结果,第二行输出时程序奔溃。
分析:

    // 定义一个__unsafe_unretained的变量obj0
    id __unsafe_unretained obj0;
    @autoreleasepool {
        {
            //obj为__autoreleasing类型的,因此NSObject对象被放入到了自动释放池中
            id __autoreleasing obj = [[NSObject alloc] init];
            obj0 = obj;
        }
        // obj的作用域结束,如果obj为strong类型的,就会释放NSObject对象
        // 但是这里的obj为__autoreleasing类型,因此不释放
        // 正常打印
        NSLog(@"%@", obj0);
    }
    //autoreleasepool块结束,autoreleasepool中的对象被释放
    //所以obj0变成了垂悬指针,奔溃
    NSLog(@"%@", obj0);

c.__autoreleasing在一些情况下会默认使用

但是像以上的显示的使用__autoreleasing修饰符是比较少见的,有些情况下编译器会自动帮我们加上。
1.对象作为函数的返回值,编译器会自动将其注册到自动释放池。
//由于return使得对象变量超出其作用域,所以该强引用对应的自己持有的对象会被自动释放,但该对象作为函数的返回值,编译器会自动将其注册到autoreleasepool
2.id的指针或对象的指针在没有显示指定时会被附加上_autoreleasing修饰符。

下面本该举一堆例子,但是就像前面说的,例子放这里:深入内存管理:让人头疼的autorelease

四、实战

说了那么多,可能会觉得无聊,实战才是最能帮助理解的。

实战一

我现在有个需求,现在有一个视图控制器,当它出现后,它的子视图暂时先不显示出来,当用户点击屏幕后再显示出来。
如果我的代码是这样的,能实现吗?

@interface TestViewController ()
@property (nonatomic, weak) UIView *subView;
@end

@implementation TestViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor whiteColor];
    
    UIView *view = [[UIView alloc] init];
    view.frame = CGRectMake(100, 100, 100, 100);
    view.backgroundColor = [UIColor redColor];
    self.subView = view;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self.view addSubview:self.subView];
}

换一个方法,如果是这样的呢?

@interface TestViewController ()
@property (nonatomic, weak) UIView *subView;
@end

@implementation TestViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor whiteColor];
    
    UIView *view = [[UIView alloc] init];
    view.frame = CGRectMake(100, 100, 100, 100);
    view.backgroundColor = [UIColor redColor];
    view.hidden = YES;
    [self.view addSubview:view];
    self.subView = view;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.subView.hidden = NO;
}

其实这里的重点在于addSubview方法,它会使得self.view持有view,所以即使subView为weak,过了view的作用域后,view对象也不会被销毁。当然在第一种方法中,把subView的修饰词改为strong也是可以的。

实战二

    __weak __typeof(self)weakSelf = self;
    AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
        __strong __typeof(weakSelf)strongSelf = weakSelf;

        strongSelf.networkReachabilityStatus = status;
        if (strongSelf.networkReachabilityStatusBlock) {
            strongSelf.networkReachabilityStatusBlock(status);
        }

    };

这是AFN的一段代码,其中,在block外使用的是__weak修饰词,原因大家都知道,而为什么block内使用的是__strong呢?
其实这样写的目的是为了保证在block执行过程中该变量不会被释放掉。这也是一个绝妙的方法啊。

五、总结

这篇文章很基础,但是在之前学习中很多知识点都被我忽略了。近期也在看《OC高级编程》这本书,希望能在其中巩固基础并了解底层实现。
最后推荐《OC高级编程 iOS与OS X 多线程和内存管理》。这本书虽然很多语句都不通顺,词不达意,但是多看几遍还是能基本理解它的意思的。

六、补充

感谢不上火喝纯净水补充。
在实战二中,使用__weak__strong结合的方法有一点需要注意,那就是在block执行前如果self为空,那么block中不管是weakSelf或strongSelf都为空。
我做了实验:

    NSObject *obj = [[NSObject alloc] init];
    typeof(obj) __weak weakObj = obj;
    NSLog(@"block外1:%@", obj);
    
    void (^testBlock) () = ^{
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            typeof(obj) __strong strongObj = weakObj;
            NSLog(@"block内1:%@", strongObj);
            [NSThread sleepForTimeInterval:3];
            NSLog(@"block内2:%@", strongObj);
        });
    };

    obj = nil;
    testBlock();
    NSLog(@"block外2:%@", obj);

打印出:

2017-05-02 17:53:16.225 MRCTest[63062:6914426] block外1:<NSObject: 0x6080000087d0>
2017-05-02 17:53:16.225 MRCTest[63062:6914426] block外2:(null)
2017-05-02 17:53:16.225 MRCTest[63062:6914539] block内1:(null)
2017-05-02 17:53:19.228 MRCTest[63062:6914539] block内2:(null)

的确是都为空。

下面代码体现这样组合的作用:

    NSObject *obj = [[NSObject alloc] init];
    typeof(obj) __weak weakObj = obj;
    NSLog(@"block外1:%@", obj);
    
    void (^testBlock) () = ^{
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            typeof(obj) __strong strongObj = weakObj;
            NSLog(@"block内1:%@", strongObj);
            [NSThread sleepForTimeInterval:3];
            NSLog(@"block内2:%@", strongObj);
        });
    };
    
    testBlock();
    sleep(1);
    obj = nil;
    NSLog(@"block外2:%@", obj);

打印出:

2017-05-02 17:55:48.022 MRCTest[63087:6917651] block外1:<NSObject: 0x60800000dd10>
2017-05-02 17:55:48.022 MRCTest[63087:6917744] block内1:<NSObject: 0x60800000dd10>
2017-05-02 17:55:49.023 MRCTest[63087:6917651] block外2:(null)
2017-05-02 17:55:51.027 MRCTest[63087:6917744] block内2:<NSObject: 0x60800000dd10>

确实能不受外界干扰,继续持有。

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

推荐阅读更多精彩内容