阅读 380

内存管理

  1. 什么是自动引用计数?

对引用采取自动计数的技术从而达到内存管理。

  1. MRC下的内存管理
  • 概述:系统对一个对象的管理通常是采用引用计数的方案。
    举个栗子:高中条件太差自习室只有一个灯,晚上第一来自习的人需要把灯打开(在此打开即创建对象)进行照明,而之后的每一个人进来的人都需要照明(需要照明即持有对象),每一个离开的人不再需要照明(不再需要照明即释放对象),最后一个离开的人需要把灯关闭(把灯关闭即销毁对象)。由此可看通过计数需要照明的人数可以很好的管理灯是否开启或者关闭。

  • 内存管理原则:

    1. 自己生成的对象自己持有。
    2. 非自己生成的对象,自己也可以持有。
    3. 自己持有的对象不需要使用的时候释放。
    4. 非自己持有的对象无法释放。

    • 自己生成的对象自己持有:凡是以alloc、new、copy、mutableCopy生成的对象或者以它们开头的方法名均代表自己生成的对象自己持有。
    /**
     * ① 自己生成并持有对象
     */
    id obj = [[NSObject alloc] init];
    
    // 此时obj 的 retainCount=1
    NSLog(@"obj 的引用计数为:%li", [obj retainCount]);
    
    /**
     * ③ 不需要使用自己持有的对象时释放
     */ 
    [obj release];
    复制代码
    #import "NewObject.h"
    
    @implementation NewObject
    -(NSObject *)newMyObj{
      NSObject *obj = [[NSObject alloc] init];
      return obj;
    }
    @end
    
    #import "ViewController.h"
    #import "NewObject.h"
    
    @implementation ViewController
    
    - (void)viewDidLoad {
      /**
       * ① 自己生成并持有对象
       */
      NewObject *newObj = [[NewObject alloc] init];
      
      /**
       * 因为以new开头的实例方法,所以也意味这自己生成并持有对象
       */
      NSObject *obj2 = [newObj newMyObj];
      
      //obj2的retainCount=1
      NSLog(@"obj2 的引用计数为:%li", [obj2 retainCount]);
      
      /**
       * 在此发送release方法程序运行正常,说明指针obj2持有该对象
       */
      [obj2 release];
      
      // ③ 用完需要将自己持有的对象释放
      [newObj release];
    }
    
    @end
    复制代码
    • 非自己生成的对象,自己也可以持有:用alloc、new、copy、mutableCopy以外的方法生成的对象都是非自己生成的对象,比如一些类方法如NSMutableArray的array方法。但是通过它们生成的对象,该变量并不会持有这个对象,我们还需手动通过retain进行该对象的持有。
      /**
       *  ② 取得非自己生成的对象,但是自己不持有。
       *     想要持有,自己可以通过调用retain方法进行持有。
       */ 
      NSMutableArray *arr = [NSMutableArray array];
      
      /**
       * ④ 无法释放非自己持有的对象
       * 即自动释放池中的对象我们无法释放,因为不是自己持有的。
       */ 
      // [arr release];
      
      /**
       * retain 可以让自己持有该对象
       */ 
      [arr retain];
      
      /**
       * 首先通过类方法array返回自动释放池中的对象,其次我们自己又通过
       * retain方法持有了对象,所以arr的retainCount=2
       */  
      NSLog(@"arr 的引用计数为:%li", [arr retainCount]);
      
      /**
       *  ③ 不在需要自己持有的对象时 进行释放
       */ 
      [arr release];
    复制代码
    • 调用[NSMutableArray array]方法使取得的对象存在,但自己不持有对象,其内部是先通过alloc生成并持有对象,然后将该对象通过调用autorelease方法加入到自动释放池中。我们通过array取得的对象其实是从自动释放池中获取的,而该自动释放池的对象什么时候释放,取决于当前Runloop本次迭代结束或者应用程序退出,pool结束时会自动调用release方法。

    综上所知:用alloc、new、copy、mutableCopy方法生成并持有的对象或者通过retain方法持有的对象,在不需要使用的时候都需要调用release方法进行释放。

    • autorelease自动释放池
      在主程序NSRunLoop每次迭代的过程中都会创建一个NSAutoreleasePool对象,本次迭代结束就会销毁该对象。所以我们在MRC中可以不创建NSAutoreleasePool对象,但是如果在本次迭代过程中产生大量autorelease对象,会导致内存严重不足的情况。比如for循环读入大量图片,从而会产生大量autorelease对象,进而导致内存不足。
    for(int i = 0; i < 10000; i++ ){
    /**
     * 读入图片,产生大量autorelease对象。
     * 由于没有废弃NSAutoreleasePool对象,
     * 最终导致内存严重不足。
     */
    }
    复制代码

    在此我们可以通过生成NSAutoreleasePool对象来解决内存暴涨的问题。

    for(int i = 0; i < 10000; i++ ){
      NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
      /**
       * 每创建一个pool对象,就读入一张图片,从而生成一个autorelease对象。
       */
      [pool drain];
      /**
       * 通过调用[pool drain],autorelease的对象被一起release.
       */
    }
    复制代码

    如果有多个NSAutoreleasePool对象嵌套,则调用过autorelease方法的对象会被添加到最近的NSAutoreleasePool对象中。

  1. ARC下的内存管理

由编译器和运行时库自动的帮助我们处理“引用计数”的相关部分。同样也遵循内存管理的原则。只是ARC是通过所有权修饰符进行记述的。

  • 所有权修饰符(id类型或者对象类型上必须附加所有权修饰符)
    1. __strong修饰符:默认 的所有权修饰符,即持有 对象的强引用(引用计数+1) 的变量 在超出其作用域时强引用失效,而对象因为没有被强引用而废弃。
    2. __weak修饰符:持有 对象的弱引用(引用计数不会+1),相当于只取得该对象,并不持有。另外当该弱引用变量持有的对象被废弃时,该变量将自动失效并被赋值为nil。通常用于解决循环引用的问题。
    3. __unsafe_unretained修饰符:不会持有对象的强引用(即引用计数不会+1),对象被废弃后附有__unsafe_unretained修饰符的指针还指向原来对象的地址,即容易发生悬垂指针。
    4. __autoreleasing修饰符:相当于ARC失效时调用autorelease方法,即对象被注册到autoreleasepool,引用计数+1。

    获取引用计数数值的函数:uintptr_t _objc_rootRetainCount(id obj);
    • __strong修饰符(默认可以省略)
    {
      /**
       * 自己生成并持有对象。
       * 因为obj为强引用,所以自己持有对象
       */ 
      id __strong obj = [[NSObject alloc] init];
      // 引用计数为:1
      NSLog(@"引用计数:%lu", _objc_rootRetainCount(obj));
    }//变量obj超出作用域后,强引用失效,对象的所有者不存在,因此废弃对象
    复制代码
    {
      /**
       * 非自己生成的对象,自己也可以持有。
       * 因为arr为强引用,所以自己持有对象
       */
      id __strong arr = [NSMutableArray array];
      /**
       * 通过类方法array返回的对象是存放到自动释放池中的对象,其次
       * 变量arr又对该对象有一个强引用,所以此时引用计数为:2
       */  
      NSLog(@"引用计数:%lu", _objc_rootRetainCount(obj));
    }//变量arr超出作用域后,强引用失效,对象的所有者不存在,因此废弃对象
    复制代码
    {
      // obj1生成并持有对象A
      id __strong obj1 = [[NSObject alloc] init];
      // obj2生成并持有对象B
      id __strong obj2 = [[NSObject alloc] init];
      id __strong obj3 = nil;
      
      /**
       * obj1持有变量obj2赋值过来的对象B的强引用。
       * 因为obj1被重新赋值,所以原先持有的对对象A的强引用失效。对
       * 象A的所有者不存在,因此废弃对象A
       */
      obj1 = obj2; //此时obj1、obj2强持有对象B
      obj3 = obj1; //此时obj1、obj2、obj3强持有对象B
      //此时引用计数为:3
      NSLog(@"引用计数:%lu", _objc_rootRetainCount(obj2));
      
      /**
       * 因为nil赋值给变量obj1,所以对对象B的强引用失效。
       * 此时仅剩 obj2、obj3对对象B有强引用
       */
      obj1 = nil;
      
      /**
       * 因为nil赋值给变量obj2,所以对对象B的强引用失效。
       * 此时仅剩 obj3对对象B有强引用
       */
      obj2 = nil;
      
      /**
       * 因为nil赋值给变量obj3,所以对对象B的强引用失效。
       * 此时没有任何变量对对象B有强引用,废弃对象B。
       */
      obj3 = nil;
    }
    复制代码
    综上所知:通过变量作用域结束、成员变量所属对象废弃、再或者对变量赋值为nil都可以做到“不再需要自己持有的对象时释放”(即废弃带有__strong修饰符的变量)。

    弊端:容易发生循环引用从而造成内存泄露。(内存泄露:应当废弃的对象在超出其生产周期后继续存在。)eg:
    @interface Test : NSObject
    {
      id __strong obj_;
    }
    -(void)setObject:(id __strong)obj;
    @end
    
    @implementation Test
    -(void)setObject:(id __strong)obj{
      obj_ = obj;
    }
    @end
    复制代码
    {
      // test1生成并持有Test对象A
      Test *test1 = [[Test alloc] init];
      // test2生成并持有Test对象B
      Test *test2 = [[Test alloc] init];
      // 此时持有 Test对象B 的变量有:Test对象A的obj_和test2
      [test1 setObject:test2];
      // 此时持有 Test对象A 的变量有:Test对象B的obj_和test1
      [test2 setObject:test1];
    }
    /**
     * 超出作用域后,test1变量的强引用失效,test2变量的强引用失效。
     * 此时对于 Test对象A 的强引用仅剩 Test对象B的obj_,
     * 而对于 Test对象B 的强引用仅剩 Test对象A的obj_。
     * 即超出变量作用域后,对象未被释放。还有一种情况也会造成内存泄露
     * [test1 setObject:test1];这是一种自循环引用的情况。解决方法就是
     * 接下来要讲的__weak修饰符。
     */
    复制代码
    • __weak修饰符
    id __weak obj = [[NSObject alloc] init];
    复制代码
    该代码编译时会报错,该变量持有对象的弱引用,即使把自己生成并持有的对象赋值给该变量,生成的对象也会被立即释放。我们可以将自己生成并持有的对象赋值给附有__strong修饰符的变量,然后再把该变量赋值给带有__weak修饰符的变量。如下:
    id __weak obj2 = nil;
    {
       id __strong obj1 = [[NSObject alloc] init];
       
       // obj2变量持有生成对象的弱引用
       obj2 = obj1;
       
       NSLog(@"在作用域内:%@",obj2);
       
       //有且仅有一个强引用指向该对象,引用计数为:1
       NSLog(@"obj的引用计数:%lu", _objc_rootRetainCount(obj));
       
       /**
        * obj2虽然是一个弱引用变量,本身不会使引用计数+1,但是在 “使用” 这
        * 个弱引用变量时,是从自动释放池中获取的,即通
        * 过objc_loadWeakRetained函数获取带有__weak修饰符的对象并retain。
        * 然后通过objc_autorelease函数将对象注册到自动释放池中。使用的时候
        * 是从自动释放池中获取的对象,使用完之后会立即释放,这样既保证了使用
        * 该弱引用变量过程中该变量不会被释放,有保证使用完之后又不会影响该对
        * 象真正的引用计数。另外变量obj1本身又对该对象有一个强引用,故引用计数为:2
        */
       NSLog(@"obj2的引用计数:%lu", _objc_rootRetainCount(obj2));
    }
    NSLog(@"出作用域后:%@",obj2);
    /**
     * 因为变量obj1超出其作用域,其强引用失效。
     * 生成的对象没有强引用变量指向它,所以废弃该对象。
     * 由于对象被废弃,所以持有该对象弱引用的变量obj2的弱引用失效,并将其赋值为nil。
     */
    复制代码
    因为附有__weak修饰符的变量不持有对象的强引用,所以超出其强引用变量的作用域后对象被废弃,借此特性我们可以解决循环引用的问题。如上文循环引用的例子可以修改为:
    @interface Test : NSObject
    {
      id __weak obj_;
    }
    -(void)setObject:(id __strong)obj;
    @end
    复制代码
    总结:__weak修饰符提供的功能包括两点:①若附有__weak修饰符的变量所指向的对象被废弃,则将nil赋值给该变量。②使用附有__weak修饰符的变量,即是使用注册到autoreleasepool中的对象。
    • __unsafe_unretained修饰符
     id __unsafe_unretained obj2 = nil;
    {
       //自己生成并持有对象
       id __strong obj1 = [[NSObject alloc] init];
       // obj2变量既不持有对象的强引用也不持有对象的弱引用
       obj2 = obj1;
       NSLog(@"在作用域内:%@",obj2);
       // obj2和obj1一样引用计数都为:1
       NSLog(@"obj2的引用计数:%lu", _objc_rootRetainCount(obj2));
    }
    NSLog(@"出作用域后:%@",obj2);
    /**
     * 因为变量obj1超出其作用域,其强引用失效。
     * 生成的对象没有强引用指针指向它,所以废弃该对象。
     * 由于对象被废弃,所以访问已经被废弃的对象会报错!(悬垂指针)
     */
    复制代码
    结果输出:
    在作用域内:<NSObject: 0x753e180>
    /** 代表出作用域后,该指针还指向原来对象的地址,即该变量为置为nil。**/
    出作用域后:<NSObject: 0x753e180>
    复制代码
    • __autoreleasing修饰符
    @autoreleasepool{ // 等价于 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
       id __autoreleasing obj1 = obj; // 等价于 [obj autorelease];
    }// 等价于 [pool drain];
    复制代码
    {
      id __strong obj = [[NSObject alloc] init];
      @autoreleasepool {
          id __autoreleasing o = obj;
          /**
           * 变量obj本身对该对象有个强引用,另外自动释放池中的变量o对该对象
           * 也有个强引用,所以此时该对象的引用计数为:2
           */ 
          NSLog(@"自动释放池中的引用计数:%lu", _objc_rootRetainCount(obj));
      }
      /**
       * 变量o超出自动释放池本身作用域后,仅剩变量obj本身
       * 对该对象有个强引用,,所以此时该对象的引用计数为:1
       */ 
      NSLog(@"引用计数:%lu", _objc_rootRetainCount(obj));
    }
    复制代码
    通常我们不会显示的调用__autoreleasing修饰符,在以下几种情况下,系统会为我们自己加上
    1. 使用附有__weak修饰符的变量时,我们其实是访问注册到autoreleasepool中的对象。使用时自动释放池对该对象有一个强引用,使用完后系统会立即调用objc_release()方法以便于不影响对象的引用计数。
      id __weak obj1 = obj;
      NSLog(@"class=%@",[obj1 class]);
      复制代码
      等价于
      id __weak obj1 = obj;
      id __autoreleasing temp = obj1;
      NSLog(@"class=%@",[temp class]);
      复制代码
    2. 对象 作为除alloc/new/copy/mutableCopy以外的 方法的返回值 时,我们最终访问的是注册到autoreleasepool中的对象。例如:NSMutableArray的类方法array,返回的对象就是注册到autoreleasepool中的对象。
    3. id类型指针或对象的指针在没有显示指定时会被附加上__autoreleasing修饰符。eg:NSError **pError; 等价于 NSError * __autoreleasing *pError;
  • 补充:带有__strong修饰符、__weak修饰符、__autoreleasing修饰符这三种修饰符中的任意一种自动变量其初始化值都为nil。
id __strong obj1; // 等价于 id __strong obj1 = nil;
id __weak obj2; // 等价于 id __weak obj2 = nil;
id __autoreleasing obj3; // 等价于 id __autoreleasing obj3 = nil;
复制代码
  • ARC有效的情况下必须遵守的规则:

    1. 不能使用retain、release、retainCount、autorelease。
    2. 不能使用NSAllocateObject、NSDeallocateObject。
    3. 必须遵循内存管理的方法命名规则。
    4. 不要显式调用dealloc。(在ARC无效时,在dealloc方法中需要显式的调用[super dealloc]。)
    5. 使用@autoreleasepool块替代NSAutoreleasePool。
    6. 不能使用区域(NSZone)。
    7. 对象型变量不能作为C语言结构体的成员。(因为C语言规约上没有方法来管理结构体成员的生命周期。ARC上内存管理的工作是交由编译器和运行时库共同完成的,C语言的局部变量是使用该变量的作用域来管理的。)
    8. 显式转换id 和 void *。(在MRC情况下隐式转换两者是没有问题的,但是ARC情况下会引起编译错误。显示转换需要借助于__bridge 或者__bridge_retained/__bridge_transfer,其中__bridge容易发生悬垂指针)

    • __bridge: 转换后不改变对象的持有状况,容易产生悬垂指针或内存泄露。
    id obj = [[NSObject alloc] init];
    void *p = (__bridge void *)obj;
    id o = (__bridge id)p;
    复制代码
    • __bridge_retained:转换类似于retain,可使要转换赋值的变量也持有所赋值的对象。
    id obj = [[NSObject alloc] init];
    void *p = (__bridge_retained void *)obj;
    复制代码

    以上源代码在ARC无效时等价于

    id obj = [[NSObject alloc] init];
    void *p = obj;
    [(id)p retain];
    复制代码
    • __bridge_transfer:转换类似于release,被转换的变量所持有的对象 在该变量被赋值给转换目标变量后释放。
    id obj = (__bridge_transfer id)p;
    复制代码

    以上源代码在ARC无效时等价于

    id obj = p;
    [obj retain];
    [(id)p release];
    复制代码
    • OC对象与Core Foundation对象可以通过免费桥Toll-Free Bridge进行转换,即CFBridgingRetaion和CFBridgingRelease,该函数具体实现如下:
    CFTypeRef CFBridgingRetain(id x){
       return (__bridge_retained CFTypeRef)x;
    }
    
    id CFBridgingRelease(CFTypeRef x){
       return (__bridge_transfer id)x;
    }
    复制代码

    eg:将生成并持有的NSMutableArray对象作为Core Foundation对象来处理。

    CFMutableArrayRef cfObj = NULL;
    {
       //自己生成并持有对象的强引用
       id obj = [[NSMutableArray alloc] init];
       cfObj = CFBridgingRetain(obj);
       //通过变量obj的强引用和CFBridgingRetain最终引用计数为:2
       printf("retain count:%d\n",CFGetRetainCount(cfObj));
    }
    //超出作用域变量obj的强引用失效,所以此时引用计数为:1
    printf("retain count after the scope = :%d\n",CFGetRetainCount(cfObj));
    //最后将对象CFRelease,对象的引用计数为:0,故对象被废弃。
    CFRelease(cfObj);
    复制代码

    使用__bridge替代CFBridgingRetain进行转换

    CFMutableArrayRef cfObj = NULL;
    {
       //自己生成并持有对象的强引用
       id obj = [[NSMutableArray alloc] init];
       cfObj = (__bridge CFMutableArrayRef)obj;
       /**
        * 因为__bridge转换不改变对象的持有状况,而此时只要变量obj的强引用,
        * 故引用计数为:1
        */
       printf("retain count:%d\n",CFGetRetainCount(cfObj));
    }
    /**
     * 超出作用域变量obj的强引用失效,所以此时引用计数为:0。该对象被废弃
     * 之后对被废弃的对象访问会出错(悬垂指针)
     */
    printf("retain count after the scope = :%d\n",CFGetRetainCount(cfObj));
    
    CFRelease(cfObj);
    复制代码

    eg:使用Core Foundation的API生成并持有的对象,作为NSMutableArray对象来处理。

    {
      // 自己生成并持有对象
      CFMutableArrayRef cfObj= CFArrayCreateMutable(kCFAllocatorDefault,0, NULL);
      //此时引用计数为:1
      printf("retain count:%d\n",CFGetRetainCount(cfObj));
      id obj = CFBridgingRelease(cfObj);
      /**
       * 因为赋值给附有__strong修饰符修饰的变量,所以obj强持有该对象,
       * 又因为调用了CFBridgingRelease所以赋值给obj变量之后,又释放了
       * 之前的对象。所以此时引用计数仍为:1,也就是变量obj强持有的对象
       */
      printf("retain count after the cast = :%d\n",CFGetRetainCount(cfObj));
    }//超出作用域后,变量obj的强引用失效,对象没有了所有者所以被废弃。
    复制代码

    使用__bridge替代CFBridgingRelease进行转换

    {
      // 自己生成并持有对象
      CFMutableArrayRef cfObj= CFArrayCreateMutable(kCFAllocatorDefault,0, NULL);
      //此时引用计数为:1
      printf("retain count:%d\n",CFGetRetainCount(cfObj));
      id obj = (__bridge CFMutableArrayRef)cfObj;
      /**
       * 因为赋值给附有__strong修饰符修饰的变量,所以obj强持有该对象,
       * 此时使用Core Foundation的API生成并持有对象,并且变量obj
       * 也强持有的对象,所以此时引用计数为:2
       */
      printf("retain count after the cast = :%d\n",CFGetRetainCount(cfObj));
    }
    /**
     * 超出作用域后,变量obj的强引用失效,对象被释放,但是
     * 此时通过Core Foundation的API生成并持有对象还存在,引用计数还为:1,
     * 所以发生内存泄露。
     */
    复制代码
  • 属性和所有权修饰符的对应关系

    1. assign 对应于 __unsafe_unratained修饰符。
    2. copy 对应于 __strong修饰符(但是赋值的是被复制的对象)。
    3. retain 对应于 __strong修饰符。
    4. strong 对应于 __strong修饰符。
    5. unsafe_unretained 对应于 __unsafe_unretained修饰符。
    6. weak 对应于 __weak修饰符。

    • 属性声明中属性要与所有权修饰符一致。例如:
    @property (nonatomic, weak) id obj;
    复制代码

    该代码会报编译错误,因为强引用变量赋给了weak修饰的属性。应改为:

    @property (nonatomic, weak) id __weak obj;
    //或者 @property (nonatomic, strong) id obj;
    复制代码
文章分类
iOS
文章标签