自动引用计数和@autoreleasepool

1、[NSObject alloc]在创建完对象后,会让该对象的retainCount+1,后续的init为初始化该对象属性的函数。
若alloc成功后,不把该地址引用赋值给某个指针变量NSObject *obj,则该新创建的对象的引用计数始终为1。
则整个程序运行的声明周期内,该内存都不会被回收,malloc、new也是同样的道理,所以会引起内存泄露。

2、ARC下虽然不能直接调用retainCount查看引用计数,但也可以通过以下几种方式work round。

方法一:[obj valueForKey:@"retainCount"];
这个方法测了一下经常不准,但好处是在引用计数==0,obj被回收以后不会报错,另外两种会报错。

方法二:OBJC_EXTERN int _objc_rootRetainCount(id);
_objc_rootRetainCount(obj);

方法三:CFGetRetainCount((__bridge CFTypeRef)obj);

============================================

关于autoreleasepool

自动释放池是NSAutoreleasePool的实例,其中包含了收到autorelease消息的对象。当一个自动释放池自身被销毁(dealloc)时,它会给池中每一个对象发送一个release消息。
如果你给一个对象多次发送autorelease消息,那么当自动释放池销毁时,这个对象也会收到同样数目的release消息。
需要注意的是:如果在delloc里面没有调用[super delloc],这样做没有问题,只是delloc被多次调用。但如果有[super delloc],则第二次重复调用会产生exc_bad_access或者signal sigabrt。所以还是不要在一个autoreleasepool里对同一个对象做一次以上的autorelease函数调用,何况这么做没有任何意义。
可以看出,一个自动释放的对象,它至少能够存活到自动释放池销毁的时候。这样看来它是一种延迟释放机制,这样保证局部堆上的变量能够被外部正常使用。

以下写法也是一样生效的:

NSObject *obj = [NSObject new];
@autoreleasepool{
    [obj autorelease];
// 1. [obj release];
// 2. [obj retain];
}
// 到这里的时候obj就被释放了

可见并不是必须在alloc的时候调用autorelease,也不是必须在@autoreleasepool内部调用alloc创建对象,总之只要是在@autoreleasepool内部调用过autorelease的对象,都会在@autoreleasepool作用域结束的时候调用release,仅此而已。
如果在注释1的位置直接调用release,那么obj会因为retainCount==0立刻被回收,而不会等到autoreleasepool作用域结束,虽然在那是又执行一遍release,但是没有意义了。
如果在注释2的位置再次调用retain,那么就是到autoreleasepool作用域结束调用一次release,那么因为retainCount==1, obj对象引用的内存还是不会回收,要么在调用一次release,要么再调用一次autorelease。
也就是说,alloc/new/copy/retain是一组增加retainCount的,release和autoRelease是一组减少retainCount的,要释放内存第一组和第二组的调用次数要完全一致。

小结:

  • 只有在自动释放池中调用了对象的autorelease方法,这个对象才会被存储到这个自动释放池之中.

  • 如果只是将对象的创建代码写在自动释放之中,而没有调用对象的autorelease方法.是不会将这个对象存储到这个自动释放池之中的.
    对象的创建可以在自动释放池的外面,在自动释放池之中,调用对象的autorelease方法,就可以将这个对象存储到这个自动释放池之中.

  • 如果对象的autorelease方法的调用放在自动释放池的外面,是无法将其存储的这个自动释放池之中的.

  • autorelease 的调用只有放在自动释放池之中 才可以讲其存储到自动释放池之中, 对象的创建可以在外面

  • 当自动释放池结束的时候.仅仅是对存储在自动释放池中的对象发送1条release消息 而不是销毁对象.

  • 如果在自动释放池中,调用同1个对象的autorelease方法多次.就会将对象存储多次到自动释放池之中.
    在自动释放池结束的时候.会为对象发送多条release消息.
    所以,1个自动释放池之中,只autorelease1次,只将这个对象放1次, 否则就会出现野指针错误.

  • 如果在自动释放池中,调用了存储到自动释放中的对象的release方法.
    在自动释放池结束的时候,还会再调用对象的release方法.
    这个时候就有有可能会造成野指针操作.

  • 将对象存储到自动释放池,并不会使对象的引用计数器+1 所以其好处就是:创建对象将对象存储在自动释放池,就不需要在写个release了.

  • 自动释放池可以嵌套.

  • 调用对象的autorelease方法,会讲对象加入到当前自动释放池之中
    只有在当前自动释放池结束的时候才会像对象发送release消息.

============================================

在创建对象的时候,调用autorelease,就能将该对象放到autoreleasepool中。利用autoreleasepool的延迟释放来管理内存。autoreleasepool这么重要,可是我们在实际开发中并没有手动创建autoreleasepool,却没有内存泄露。这是为什么呢?其实没有手动创建并不代表它不会被创建,那么它是什么时候创建的呢?

iOS程序默认在main函数中会加入@autoreleasepool,但这个基本没什么用,因为它的作用域是整个main函数结束,也就是说延迟release的时间点是程序退出之前,那么既然程序都退出了,所有资源和内存都已经被操作系统回收了,还释放个什么劲啊?

所以实际上iOS程序在运行过程中,远不止我们自己实现的代码里会添加@autoreleasepool,autoreleasepool的数据存储结构是一个嵌套结构,也就是说可以在一个autoreleasepool内部嵌套另一个autoreleasepool并且可以无限嵌套下去。这有点类似函数的栈变量压栈和出栈的逻辑。所以runtime自动调用和创建的autoreleasepool对程序开发者来说基本是透明的。

App启动后,系统在主线程runLoop里注册两个Observser,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其优先级最高,保证创建释放池发生在其他所有回调之前。第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 优先级最低,保证其释放池子发生在其他所有回调之后。在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被runLoop创建好的AutoreleasePool环绕着。这就解释了,为什么即使程序中只有main函数的最外层设置autoreleasepool,所有的内存也不是在main运行完后一股脑一起释放的原因,可以说最外层的autoreleasepool只是为了保护所有没有被内建的autoreleasepool包裹住的部分内存自动回收用的。

通过下面的例子,我们来看一下,runLoop创建的autoreleasepool是不是真的帮我们管理了内存。

__weak id reference = nil;  
- (void)viewDidLoad {  
    [super viewDidLoad];  
    NSString *str = [NSString stringWithFormat:@"autoreleasePool"];  
    // str是一个autorelease对象,设置一个weak的引用来观察它  
    reference = str;  
    NSLog(@"%@", reference); // Console: autoreleasePool  
}  
- (void)viewWillAppear:(BOOL)animated {  
    [super viewWillAppear:animated];  
    NSLog(@"%@", reference); // Console: autoreleasePool  
}  
- (void)viewDidAppear:(BOOL)animated {  
    [super viewDidAppear:animated];  
    NSLog(@"%@", reference); // Console: (null)  
}  

这个实验同时也证明了viewDidLoad和viewWillAppear是在同一个runloop调用的,而viewDidAppear是在之后的某个runloop调用的。由于这个vc在loadView之后便add到了window层级上,所以viewDidLoad和viewWillAppear是在同一个runloop调用的,因此在viewWillAppear中,这个autorelease的变量依然有值。
当然,我们也可以不用等到当前runLoop结束,选择手动干预Autorelease对象的释放时机:

- (void)viewDidLoad {  
    [super viewDidLoad];  
    @autoreleasepool {  
        NSString *str = [NSString stringWithFormat:@"autoreleasePool"];  
    }  
    NSLog(@"%@", str); // Console: (null)  
}  

通过上面的例子,可以看出,没有调用release也做到了内存管理。可是大家注意到了,str对象没有调用autorelease方法啊,怎么被放到autoreleasepool进行管理的呢?其实静态方法已经在内部自动调用了autorelease方法,所有这里不需要再调用。
更进一步讲,ARC在进行内存释放管理的时候,如果某个对象是在独立作用域中创建并且使用,没有外层作用域对它的引用的话(没有外层作用域指针指向,也没有从函数返回值中返回),则很可能编译的时候使用release立刻释放。但若不是这样,则一定使用autorelease来减少复杂度,比如上面的stringWithFormat静态方法,就是在其函数内执行了[NSString alloc],但其创建对象的引用作为返回值进行了返回,所以必然会添加autorelease,因为如果是使用release的话,由于编译器必须在return函数前面来调用release函数,而调用release函数后引用计数==0立刻释放了该对象,则无论如何都会返回一个已经被释放的野指针作为函数返回值,唯一的办法就只能是把release延迟调用,来起码等到调用者拿到对象的引用并决定是weak还是strong之后再延迟释放。

=========================================
继续我们的主题。我们知道autoreleasepool是一个自动释放池,那么它到底是一个什么样的数据结构呢?我们在命令行中使用 clang -rewrite-objc main.m 让编译器重新改写这个文件,编译完后,会在该文件目录下多一个.cpp文件。打开这个文件。滚到最底部。可以看到如下代码:(删除掉多余的代码)
很遗憾的是这里只能看到autoreleasepool被编译的部分,如果还有NSObject *obj = [[NSObject alloc] init];这种定义,是看不到编译器自动在最后加上的[obj release]调用的,可能是这部分属于llvm的特性,用clang的重写是看不到的。

int main(int argc, const charchar * argv[]) {  
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;  
    }  
    return 0;  
}  

在这个文件中,有一个非常奇怪的 __AtAutoreleasePool 的结构体,前面的注释写到/* @autoreleasepopl */ 。也就是说@autoreleasepool {} 被转换为:__AtAutoreleasePool __autoreleasepool。那么__AtAutoreleasePool又是什么?在文件中可以找到__AtAutoreleasePool数据结构如下:

struct __AtAutoreleasePool {  
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}  
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}  
  voidvoid * atautoreleasepoolobj;  
};  

它是一个结构体,该结构体会在初始化时调用 objc_autoreleasePoolPush() 方法,会在析构时调用objc_autoreleasePoolPop 方法。所以我们可以进一步将main函数中的代码改写为如下:

int main(int argc, const charchar * argv[]) {    
    {  
        voidvoid * atautoreleasepoolobj = objc_autoreleasePoolPush();  
  
        // do whatever you want  
  
        objc_autoreleasePoolPop(atautoreleasepoolobj);  
    }  
    return 0;  
}  

@autoreleasepool 只是帮助我们少写了这两行代码而已,让代码看起来更美观,然后要根据上述两个方法来分析自动释放池的实现。objc_autoreleasePoolPush 和 objc_autoreleasePoolPop 的实现:

voidvoid *objc_autoreleasePoolPush(void) {    
    return AutoreleasePoolPage::push();  
}  
  
void objc_autoreleasePoolPop(voidvoid *ctxt) {    
    AutoreleasePoolPage::pop(ctxt);  
}  

__AtAutoreleasePool的Push和Pop方法看上去方法看上去是对 AutoreleasePoolPage对应静态方法push和pop的封装。
那么AutoreleasePoolPage又是一个什么东东呢?,它的定义可以在NSObject.mm文件中看到,定义如下:

class AutoreleasePoolPage {    
    magic_t const magic;  
    idid *next;  
    pthread_t const thread;  
    AutoreleasePoolPage * const parent;  
    AutoreleasePoolPage *child;  
    uint32_t const depth;  
    uint32_t hiwat;  
};  

=========================================

以上的过程说明了autoreleasepool可以由系统调用自动多次嵌套,而且只对调用了autorelease的对象起作用。
ARC是在编译阶段由LLVM自动加入reatin,release,autorelease等操作函数,那么对于栈作用域的函数内部变量来说,会在函数退出之前对局部变量引用的对象执行release操作,当retainCount==0的时候立刻执行回收内存操作。而@autoreleasepool可以让这个操作提前,例如:

- (void) doSomething {

__weak NSMutableArray *wcollection;
@autoreleasepool{

     NSMutableArray *collection = @[].mutableCopy;
     wcollection = collection;

     for (int i = 0; i < 10e6; ++i) {

        NSString *str = [NSString stringWithFormat:@"hi + %d", i];

        [collection addObject:str];

    }
    NSLog(@"%d",_objc_rootRetainCount(collection)); //打印1
    NSLog(@"finished!");
}
NSLog(@"%d",[wcollection valueForKey:@"retainCount"]); //打印0
     // 其他的代码
    NSLog(@"completed!");
}

以上的例子中,如果不使用局部的@autoreleasepool(但是还是要保留{}以减小collection的作用域),则collection引用的对象虽然在{}作用域的最后会被release,并且将其中add过的str对象都做release操作,但由于str在通过静态方法创建的时候进行了一次持有并且用autorelease释放,所以str还占用的内存要在autoreleasepool drain的时候才能释放。另外还能确定在超过函数作用域的对象多次引用(比如doSomething中return collection),或者多线程并发引用同一个外部对象的情况下,想通过添加release第一时间释放的难度太大,所以ARC会选择使用autorelease,在更外层更好控制的地方使用autoreleasepool进行延迟释放。

而如果在collection使用的外部套上@autoreleasepool,则执行完成后会立刻释放str所占用的内存,也就是说会降低整个函数执行过程中,内存占用的峰值。另外大量的释放内存也会占用不少cpu时间,所以在打印完finished!以后,到//其他代码执行之前,会卡住一段时间,从执行监视器视图看,这段时间内程序占用的内存会持续下降。

for(int i=0;i<10e6;i++){
[[NSString alloc] initWithFormat:@"hi:%d",i];
__weak NSString *str = [[NSString alloc] initWithFormat:@"hi:%d",i];
NSLog(@"%@",str); // 打印nil
NSString *str2 = [[NSString alloc] initWithFormat:@"hi:%d",i];
NSLog(@"%@",str2);// 打印字符串
}

ARC的情况下,第一种写法编译器会立刻优化为[[[NSString alloc] initWithFormat:@"hi:%d",i] release],所以虽然cpu在不断的执行和创建销毁对象,但内存不变化。
第二种写法用弱引用接收跟不接收一下不能增加引用计数,则同理。打印为nil
第三种写法有区别,可以打印出字符串,编译器会在{}作用域的最后一行加上[str2 release],而不是alloc之后立刻回收。
可以想象为一个虚拟的强引用指针
NSString *temp = [alloc init];
第一种写法什么都不做
第二种写法_weak NSString *str = temp;
第三种写法NSString *str2 = temp;
[temp release];
// alloc之后的代码

MRC的情况下就比较悲剧了,__weak在MRC下不可用编译不通过,另外两种写法都会因为没有release让内存持续增长,尤其是第一种写法,想release都没有办法。

=========================================

__autoreleasing修饰符的作用

切换到ARC之后,每个指向OC对象的指针,都被赋上了所有权修饰符。一共有__strong、__weak、__unsafe_unretained和__autoreleasing这样四种所有权修饰符。

当一个对象被赋值给一个使用__autoreleasing修饰符修饰的指针时,相当于这个对象在MRC下被发送了autorelease消息,也就是说它被注册到了autorelease pool中。

全局变量和实例变量是无法用__autoreleasing来修饰的,不然编译器会报错。
而局部变量用__autoreleasing修饰后,其指向的对象,在当前autorelease pool结束之前不会被回收,即使其生命的作用域结束。也就是说__autoreleasing让编译器改变了默认的处理原则。

__weak NSObject *weakObj1;
__weak NSObject *weakObj2;

{
    __autoreleasing NSObject *obj1 = [[NSObject alloc] init];
    weakObj1 = obj1; //weakObj1指向的对象已被注册到autorelease pool

    __strong NSObject *obj2 = [[NSObject alloc] init];
    weakObj2 = obj2;//weakObj2指向的对象没有被注册到autorelease pool
}
//局部变量obj1和obj2的作用域结束,
//此时weakObj2指向的对象不再被强引用,因此被回收;
//而obj1指向的对象仍然在autorelease pool中

NSLog(@"%@", weakObj1);//输出<NSObject: 0x100206030>,因为此刻weakObj1在autorelease pool中,不会因为obj1作用域的结束而被回收
NSLog(@"%@", weakObj2);//输出null

=========================================

方法名检查

@interface XSQObject : NSObject

+ (NSString *)newHelloWorldString;
+ (NSString *)allocHelloWorldString;
+ (NSString *)copyHelloWorldString;

+ (NSString *)helloWorldString;
+ (NSString *)initHelloWorldString;
+ (NSString *)shitHelloWorldString;

@end

@implementation XSQObject

+ (NSString *)newHelloWorldString {
    return [[NSString alloc] initWithCString:"HelloWorld" encoding:NSUTF8StringEncoding];
}
+ (NSString *)allocHelloWorldString {
    return [[NSString alloc] initWithCString:"HelloWorld" encoding:NSUTF8StringEncoding];
}
+ (NSString *)copyHelloWorldString {
    return [[NSString alloc] initWithCString:"HelloWorld" encoding:NSUTF8StringEncoding];
}

+ (NSString *)helloWorldString {
    return [[NSString alloc] initWithCString:"HelloWorld" encoding:NSUTF8StringEncoding];
}
+ (NSString *)initHelloWorldString {
    return [[NSString alloc] initWithCString:"HelloWorld" encoding:NSUTF8StringEncoding];
}
+ (NSString *)shitHelloWorldString {
    return [[NSString alloc] initWithCString:"HelloWorld" encoding:NSUTF8StringEncoding];
}

@end

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        __weak NSString *newHelloWorldString = [XSQObject newHelloWorldString];
        __weak NSString *allocHelloWorldString = [XSQObject allocHelloWorldString];
        __weak NSString *copyHelloWorldString = [XSQObject copyHelloWorldString];
        //上面三个都有warning:
        //Assigning retained object to weak variable; object will be released after assignment
        NSLog(@"%@", newHelloWorldString);//输出null
        NSLog(@"%@", allocHelloWorldString);//输出null
        NSLog(@"%@", copyHelloWorldString);//输出null
        
        __weak NSString *helloWorldString = [XSQObject helloWorldString];
        __weak NSString *initHelloWorldString = [XSQObject initHelloWorldString];
        __weak NSString *shitHelloWorldString = [XSQObject shitHelloWorldString];
        //上面三个没有warning
        
        NSLog(@"%@", helloWorldString);//输出HelloWorld
        NSLog(@"%@", initHelloWorldString);//输出HelloWorld
        NSLog(@"%@", shitHelloWorldString);//输出HelloWorld
        
    }
    return 0;
}

虽然[XSQObject helloWorldString]和[XSQObject newHelloWorldString]两个方法的实现完全一样,但是它们返回的对象被赋值给__weak指针后,前者仍然存在,而后者则被销毁了。如果再加入@autorelease块做点实验,可以发现helloWorldString指向的对象其实已被注册到autorelease pool中。

对比内存管理原则,这就像是在MRC下,不以alloc/new/copy/mutableCopy开头的方法,会对返回的对象发送autorelease消息一样。而事实上,在ARC下,编译器会检查方法名是否以alloc/new/copy/mutableCopy开头,如果不是,则自动将返回的对象注册到autorelease pool中。

在一些特殊的情况下,程序员也可以手动给某些方法加上其他标记,来覆盖被编译器隐式加上的标记。

需要注意的一点是,clang的ARC文档描述如下:

Methods in the alloc, copy, init, mutableCopy, and new families are implicitly marked attribute((ns_returns_retained)). This may be suppressed by explicitly marking the method attribute((ns_returns_not_retained)).

但其实还有一个更详细的默认规则限定,那就是init用作实例方法的开头,其他的用作静态方法的开头。这就是为什么上面的例子中用init开头的静态方法却没有警告和预期结果的原因。

=========================================

更详细的内容可以参考
http://blog.csdn.net/shxwork/article/details/51437003
http://blog.csdn.net/shxwork/article/details/51363306
http://www.jianshu.com/p/1b66c4d47cd7 (强烈推荐)
http://www.jianshu.com/p/32265cbb2a26(强烈推荐)
http://www.jianshu.com/p/ec2c854b2efd

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

推荐阅读更多精彩内容