内存管理

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

推荐阅读更多精彩内容