美文网首页
【iOS小结】内存管理

【iOS小结】内存管理

作者: WellsCai | 来源:发表于2017-11-03 15:07 被阅读0次

    MRC下的内存管理

    引用计数的思考

    Objective-C中的内存管理,也就是引用计数。
    有关内存管理的方法是包含在Cocoa框架中。Cocoa框架中Foundation框架类库的NSObject类担负内存管理的职责。

    Cocoa框架、Foundation框架和NSObject类的关系.png

    内存管理的思考方式

    1.自己生成的对象,自己所持有。

    使用alloc、new、copy、mutableCopy开头的方法名意味着自己生成的对象并且自己持有。(例:alloc、allocMyObject是,allocate不是)

    2.非自己生成的对象,自己也能持有。

    NSMutableArray类的array类方法。

    //取得非自己生成并持有的对象
    id obj = [NSMutableArray array];
    [obj retain];
    

    array的内部实现:

    - (id)object{
      id obj = [[NSObject alloc] init];
      //自己持有对象
      [obj autorelease];
      //取得的对象存在,但自己不持有对象
      return obj;
    }
    
    3.不再需要自己持有对象时释放。

    自己持有的对象,一旦不再需要,有义务释放该对象。释放使用release方法。

    4.无法释放非自己持有对象。
    id obj = [NSMutableArray array];
    //取得的对象存在,但自己不持有对象,所以会导致程序崩溃!
    [obj release];
    

    alloc、retain、release、dealloc实现

    我们先从GNUstep中了解相关的实现:

    //用来保存引用计数
    struct obj_layout{
        NSUInteger retained;
    };
    
    + (id)alloc{
        return [self allocWithZone:NSDefaultMallocZone()];
    }
    + (id)allocWithZone:(struct _NSZone *)zone{
        return NSAllocsteObject(self, 0 ,zone);
    }
    
    inline id NSAllocsteObject(Class aClass, NSUInteger extraBytes,NSZone *zone){
        int size = sizeof(struct obj_layout) + 对象大小 + extraBytes;//计算容纳对象所需内存大小
        //指向新空间的指针(此时是指向obj_layout结构体)
        id new = NSZoneMalloc(zone, size);
        //将该空间置为0
        menset(new, 0 ,size);
        //将指针指向对象
        new = (id)&((struct obj_layout *) new)[1];
    }
    

    alloc类方法用struct obj_layout的retained整数来保存引用计数,并将其写入对象内存头部,该对象内存全部置0后返回。

    alloc返回对象的内存图.png

    对象的引用计数可通过retainCount实例方法取得,本质也是从struct obj_layout的retained获取。
    retain、release本质也是对retained值的修改,retain方法使retained变量加1,而release方法使retained变量减1,当retained变量等于0时调用dealloc实例方法,废弃对象(通过free内存空间)。

    GNUstep将引用计数保存在对象占用内存块头部的变量,而苹果的实现是保存在引用计数表(散列表,键为内存块地址,值为引用计数)的记录中。优点如下:

    • 在对象分配内存块时就无需考虑内存块头部。
    • 通过引用计数表的记录追溯到各对象的内存块。
    通过引用计数表追溯对象.png
    autorelease

    autorelease就是自动释放,类似于C语言中自动变量(局部变量)的特性,超出其作用域,对象实例的release实例方法被调用。
    autorelease的使用方法如下:
    (1)生成并持有NSAutoreleasePool对象;
    (2)调用已分配对象的autorelease实例方法;
    (3)废弃NSAutoreleasePool对象。

    NSAutoreleasePool对象的生存周期.png

    NSAutoreleasePool对象的生存周期相当于C语言变量的作用域。对所有调用过autorelease实例方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。代码如下:

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    id obj = [[NSObject alloc] init];
    [obj autorelease];
    [pool drain];  //此处obj会调用release
    

    苹果在主线程 RunLoop 里注册了两个 Observer:
    第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。优先级最高,保证创建释放池发生在其他所有回调之前。
    第二个 Observer 监视了两个事件: BeforeWaiting(准备进入睡眠) 和 Exit(即将退出Loop):

    • BeforeWaiting(准备进入睡眠)时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;
    • Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。优先级最低,保证其释放池子发生在其他所有回调之后。
    NSRunLoop和NSAutoreleasePool关系.png

    当然,在ARC情况下我们使用@autoreleasepool{}替代NSAutoreleasePool。看了main方法的代码,你会发现整个应用都在autoreleasepool块中,意味着所有的autorelease对象最后都会被回收,不会导致内存泄露。

    在一些特定的情况下,需要我们自己手动创建自动释放池:

    • 创建很多临时对象的循环时
      在循环中使用自动释放池可以为每个迭代释放内存。虽然迭代前后最终的内存使用相同,但你的应用的最大内存需求可以大大降低。
    //场景:读入大量图片同时改变其尺寸
    for (int i = 0; i < 图像数; i++) {
            NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        
            /*
             *读入图像,大量产生autorelease对象
             *如果没有废弃NSAutoreleasePool对象,会导致内存增加,最终内存不足
             */
    
            [pool drain];//autorelease对象被一起release
        }
    
    • 创建一个子线程时
      每个线程都将有它自己的自动释放池。不像主线程有统一生成的代码,对于任何自定义的线程,必须创建自己的自动释放池。
      该例子我就用@autoreleasepool来表示:
    //新线程的入口函数
    - (void)mtThreadStart:(id)obj{
        @autoreleasepool{
            //新线程的代码
        }
    }
    
    autorelease的实现

    苹果的实现和GNUstep相同,我们就以GNUstep的源码来了解其原理。
    当对象调用autorelease时,本质上调用了NSAutoreleasePool的adObject类方法,追加到NSAutoreleasePool对象中的数组。

    - (id)autorelease{
        [NSAutoreleasePool addObject:self];
    }
    

    NSAutoreleasePool类的实现:

    + (void)addObject:(id)anObj{
        //嵌套情况下是最内侧的NSAutoreleasePool对象
        NSAutoreleasePool *pool = 取得正在使用的NSAutoreleasePool对象;
        if (pool != nil) {
            [pool addObject:anObj];
        }else{
            NSLog(@"NSAutoreleasePool对象非存在状态下调用autorelease");
        }
    }
    - (void)addObject:(id)anObj{
        [array addObject:anObj];
    }
    

    当NSAutoreleasePool调用drain实例方法时,让数组中的对象release,并销毁自己。

    - (void)drain{
        [self dealloc];
    }
    - (void)dealloc{
        [self emptyPool];
        [array release];
    }
    - (void)emptyPool{
        for (id obj in array) {
            [obj release];
        }
    }
    

    ARC下的内存管理

    Objective-C中为了处理对象,可将变量类型定义为id类型或者各种对象类型。所谓对象类型就是指向NSObject这样的Objective-C类的指针,例如“NSObject *”。id 类型用于隐藏对象类型的类名部分,相当于C语言的“void *”。
    ARC有效时,id 类型和对象类型通C语言其他类型不同,其类型必须附加所有权修饰符。(用于内存管理,不必再键入retain和release)

    • __strong 修饰符
    • __weak 修饰符
    • __unsafe_unretained 修饰符
    • __autoreleasing 修饰符
    __strong 修饰符

    __strong 修饰符是id 类型和对象类型默认的所有权修饰符。__strong表示对对象的“强引用”。持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放。

    {
        //因为obj为强引用,所以自己生成并持有对象
        id __strong obj = [[NSObject alloc] init];
        
    }
    /*
     *因为变量obj超出作用域,强引用失效
     *所以自动释放自己持有的对象。
     *对象的所有者不存在,因此废弃该对象。
     */
    

    __strong 修饰的变量不仅在变量作用域中,赋值上也可以正确的管理其对象的所有者。

    id __strong obj0 = [[NSObject alloc] init]; /*对象A*/
        /*
         *obj0持有对象A的强引用
         */
        
        id __strong obj1 = [[NSObject alloc] init]; /*对象B*/
        /*
         *obj1持有对象B的强引用
         */
        
        id __strong obj2 = nil;
        /*
         *obj2不持有对象
         */
        
        obj1 = obj0;
        /*
         * obj0持有obj1赋值的对象B的强引用,对持有的对象A的强引用失效。
         * 对象A的所有者不存在,因此废弃对象A
         *
         * 此时对象B的强引用变量为obj0和obj
         */
        
        obj2 = obj0;
        /*
         * obj2持有obj0赋值的对象B的强引用。
         *
         * 此时对象B的强引用变量为obj0和obj1,obj2
         */
        
        obj1 = nil;
        /*
         * 因为nil赋值给obj1,所以对对象B的强引用无效。
         *
         * 此时对象B的强引用变量为obj0和obj2
         */
        
        obj0 = nil;
        /*
         * 因为nil赋值给obj0,所以对对象B的强引用无效。
         *
         * 此时对象B的强引用变量为obj2
         */
        
        obj2 = nil;
        /*
         * 因为nil赋值给obj2,所以对对象B的强引用无效。
         *
         * 对象B的所有者不存在,因此废弃对象B
         */
    

    当然,也可以在方法参数上,使用附有__strong 修饰符的变量。

    {
        id __strong test = [[Test alloc] init];
        /*
         * test持有Test对象的强引用
         */
        
        [test setObject:[NSObject alloc] init]];
        /*
         * test 对象的obj成员持有NSObject的强引用
         */
    }
    /*
     * 因为test变量超出其作用域,强引用失效,所以自动释放Test对象
     * Test对象的所有者不存在,因此废弃该对象
     *
     * 废弃Test对象的同时,成员obj也被废弃,对NSObject的强引用也失效
     * NSObject对象的所有者不存在,因此废弃该对象
     */
    

    __strong 修饰符的内部实现:

    /*自己生成并持有对象*/
    id __strong obj = [[NSObject alloc] init];
    
    //以下为编译器模拟代码
    id obj = objc_msgSend(NSObject ,@selector(alloc)); 
    objc_msgSend(obj , @selector(init));
    objc_release(obj);
    
    /* 非自己生成但持有对象*/
    id __strong obj = [NSMutableArray array];
    
    //以下为编译器模拟代码
    id obj = objc_msgSend(NSObject ,@selector(array)); 
    objc_retainAutoreleasedReturnValue(obj);
    objc_release(obj);
    
    //其中array的实现:
    + (id)array{
        return [[NSMutableArray alloc] init];
    
      //编译器模拟代码
       id obj = objc_msgSend(NSMutableArray ,@selector(alloc)); 
       objc_msgSend(obj , @selector(init));
       return objc_autoreleaseReturnValue(obj);
    }
    

    objc_autoreleaseReturnValue方法本来需要把对象注册到autoreleasepool中,这边苹果有个优化,objc_autoreleaseReturnValue紧接objc_retainAutoreleasedReturnValue会省略autoreleasepool注册。直接传递到方法或函数的调用方。

    __weak 修饰符

    当会发生循环引用时,__strong 修饰符就不适用了。循环引用容易发生内存泄露。所谓内存泄露就是应当废弃的对象在超出其生命周期后继续存在。以下有两种情况:

    类成员变量的循环引用.png 自引用.png

    带__weak 修饰符的变量(弱引用)不持有对象,所以在超出其变量作用域时,对象即被释放,且处于nil被赋值的状态。

    id __weak obj1 = nil;
    {
        id __strong obj0 = [[NSObject alloc] init];
        /*
         * obj0变量为强引用,所以自己持有对象
         */
        
        obj1 = obj0;
        /*
         * obj1持有obj0的弱引用
         */
        
        NSLog(@"%@",obj1);// 输出<NSObject:)x753e180>
    }
    /*
     * 因为obj0变量超出其作用域,强引用失效,所以自动释放NSObject对象
     * NSObject对象的无持有者,因此废弃该对象
     *
     * 废弃对象的同时,持有该对象弱引用的obj1变量的弱引用失效,nil赋值给obj1
     */
    NSLog(@"%@",obj0);// 输出(null)
    

    __weak 修饰符的内部实现:
    我们来看看__weak内部怎么实现以下功能的:
    ① 若附有__weak修饰符的变量所引用的对象被废弃,则将赋值给该变量

    {
       id __weak obj1 = obj;
    }
    
    /* 编译器的模拟代码 */
    id obj1;
    objc_initWeak(&obj1, obj);
    objc_destroyWeak(&obj1);
    

    objc_initWeak函数将附有__weak修饰符的变量初始化为0后,会将赋值的对象作为参数调用objc_storeWeak函数。

    /* 编译器的模拟代码 */
    id obj1;
    obj1 = 0;
    objc_storeWeak(&obj1, obj);
    objc_storeWeak(&obj1,0);
    

    objc_storeWeak函数会把第二参数的赋值对象的地址作为key,将第一参数的附有__weak修饰符的变量的地址注册到runtime维护的weak表中。如果第二参数为0,则把变量的地址从weak表中删除。
    weak表和引用计数表都是runtime维护的散列表。通过废弃对象的地址,可以高速获取到附有__weak修饰符的变量的地址(有可能多个),然后赋值为nil,从weak表中删除该记录,从引用技术表中删除废弃对象为key的记录。

    ② 使用附有__weak修饰符的变量,即是使用注册到autoreleasepool中的对象

    {
       id __weak obj1 = obj;//必须是有个强引用的来赋值给obj1,不然会马上被释放掉
       NSLog(@"%@",obj1);
    }
    
    /* 编译器的模拟代码 */
    id obj1;
    objc_initWeak(&obj1, obj);
    id temp = obj_loadWeakReteined(&obj1);//取出obj1对象retain
    objc_autorelease(temp);//注册到autoreleasepool中
    NSLog(@"%@",obj1);
    objc_destroyWeak(&obj1);
    

    因此在使用附有__weak修饰符的变量时,最好先暂时赋值给附有__strong修饰符的变量再使用,防止多次注册到autoreleasepool(一次使用就注册一次)。

    __unsafe_unretained 修饰符

    __unsafe_unretained 修饰符是不安全的所以权修饰符。其修饰的变量不属于编译器的内存管理对象,同附有__weak 修饰符的变量一样,因为自己生成并持有的对象不能继续为自己所有,所以生成的对象会立即被释放。但是和__weak不同的是,__unsafe_unretained在弱引用失效时不会被置为nil,如果后面被访问到,程序会崩溃(悬垂指针)。

    __autoreleasing 修饰符

    ARC下会用@autoreleasepool块来替代NSAutoreleasePool类,用附有__autoreleasing修饰符的变量来替代autorelease方法。(在@autoreleasepool块中不加__autoreleasing也是可以释放的)

    @autoreleasepool和__autorelease修饰符.png
    以下本质上就用到__autoreleasing修饰符:
    1. 在访问__weak修饰符的变量时必须访问到autoreleasepool的对象。因为在访问__weak对象中,该对象有可能被废弃。所以要把访问的对象注册到autoreleasepool中,确保autoreleasepool结束前该对象存在。
    id  __weak obj0 = obj1;
    //本质上编译器生成了以下这句代码,并输出[temp class]
    //id __autoreleasing temp = obj0;
       
    NSLog(@"Class = %@",[obj0 class]);
    

    2. __autoreleasing在ARC中主要用在参数传递返回值和引用传递参数的情况下。
    比如常用的NSError。如果你的error定义为了strong型,编译器会帮你隐式地做如下事情,保证最终传入函数的参数依然是个__autoreleasing类型的引用。

    NSError *error; 
    NSError *__autoreleasing tempError = error; // 编译器添加 
    if (![data writeToFile:filename options:NSDataWritingAtomic error:&tempError]) 
    { 
      error = tempError; // 编译器添加 
      NSLog(@"Error: %@", error); 
    }
    

    所以为了提高效率,避免这种情况,我们一般在定义error的时候将其声明为__autoreleasing类型的:

    NSError *__autoreleasing error;
    

    error指向的对象在创建出来后,被放入到了autoreleasing pool中,等待使用结束后的自动释放,函数外error的使用者并不需要关心*error指向对象的释放。
    另外,所有这种指针的指针 (NSError **)的函数参数如果不加修饰符,编译器会默认将他们认定为__autoreleasing类型。

    //除非显式得给value声明了__strong,否则value默认就是__autoreleasing的。
    - (NSString *)doSomething:(NSNumber **)value
    {
            // do something  
    }
    

    3.某些类的方法会隐式地使用自己的autorelease pool,在这种时候使用__autoreleasing类型要特别小心。
    比如NSDictionary的[enumerateKeysAndObjectsUsingBlock]方法:

    - (void)loopThroughDictionary:(NSDictionary *)dict error:(NSError **)error
    {
        [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop){
              @autoreleasepool  // 被隐式创建
          {
                  if (there is some error && error != nil)
                  {
                        *error = [NSError errorWithDomain:@"MyError" code:1 userInfo:nil];
                  }
              }
        }];
        // *error 在这里已经被dict的做枚举遍历时创建的autorelease pool释放掉了 :(  
    } 
    

    __autoreleasing 修饰符的内部实现:

    @autoreleasepool{
            id __autoreleasing obj = [[NSObject alloc] init];
        }
    
        /* 编译器模拟代码 */
        id pool = objc_autoreleasePoolPush();
        id obj = objc_msgSend(NSObject,@selector(alloc));
        objc_msgSend(obj,@selector(init));
        objc_autorelease(obj);
        objc_autoreleasePoolPop(pool);
    
    @autoreleasepool{
            id __autoreleasing obj = [NSMutableArray array];
        }
    
        /* 编译器模拟代码 */
        id pool = objc_autoreleasePoolPush();
        id obj = objc_msgSend(NSMutableArray,@selector(array));
        objc_retainAutoreleaseReturnValue(obj);
        objc_autorelease(obj);
        objc_autoreleasePoolPop(pool);
    

    所以,本质上是和autorelease的实现一样的。从代码中我们也可以看出,一个线程维护着一个autoreleasePool的栈。我们可以通过_objc_autoreleasePoolPrint函数来观察注册到autoreleasepool中的引用对象

    从MRC到ARC的转变

    ARC有效时,要遵守以下规则:

    • 不能使用retain/release/retainCount/autorelease
    • 不能使用NSAllocteObject/NSdeallocObject
    • 须遵守内存管理的方法命名规则
    • 不能显式调用dealloc(只能用于已注册代理或观察对象)
    • 使用@autoreleasepool块代替NSAutoreleasePool
    • 不能使用区域(NSZone)
    • 对象型变量不能作为C语言结构体的成员(可强制转换为void *,或者附加__unsafe_unretained 修饰符)
    • 显式转换“id”和“void *”
      非ARC下,这两个类型是可以直接赋值的
    id obj = [NSObject alloc] init];
    void *p = obj;
    id o = p;
    

    但是在ARC下就会引起编译错误,需要通过__bridege来转换。

    id obj = [[NSObject alloc] init];
    void *p = (__bridge void*)obj;//显式转换
    id o = (__bridge id)p;//显式转换
    

    ARC有效时,Objective-C的属性声明的属性和所有权修饰符存在对应关系。

    属性和修饰符关系.png

    相关文章

      网友评论

          本文标题:【iOS小结】内存管理

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