美文网首页锻炼吃饭的家伙理论iOS开发基础
iOS基础:深入内存管理-从所有权修饰符开始

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

作者: Jabber_YQ | 来源:发表于2017-04-27 16:24 被阅读542次

    一、谁适合看本文

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

        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>
    

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

    相关文章

      网友评论

      • 不上火喝纯净水:其实楼主可以从引用计数的角度来探讨变量释放的问题。 另外最后的实战二中的__strong这种用法它并不能保证在执行该block的时候self变量不为nil.
        Jabber_YQ:@不上火喝纯净水 我知道您的意思了,也就是说在block执行前如果对象被销毁那么block中仍然为空。但是我的描述也没问题啊,在执行过程中,一旦被持有,block外部就无法干扰了。
        谢谢您补充,我加上去:blush:
        不上火喝纯净水:首先,block的变量捕捉即拷贝会使外部变量引用计数+1,但对外部的weak修饰的变量不会增加引用计数。所以我们使用weak来打破循环引用。

        其次,block中的临时变量,如果是strong来修饰的话,会把引用对象retain引用计数+1,然后在block作用域结束的时候释放该变量的引用计数。但是这是在该block执行的时候。

        所以结果就是,如果block执行前self为nil的话,那么整个block中的使用的weakself 和 strongself都将是nil了。如果执行前不是nil,然后执行到strong临时变量赋值那一句,它的引用计数就加1了,于是别的地方都对self做了release,但还有block中的最后一个引用技术,所以self不会释放。
        Jabber_YQ:@不上火喝纯净水 那请问这样写的作用是什么呀

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

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