美文网首页
Autorelease和ARC

Autorelease和ARC

作者: 轰炸机上调鸡尾酒 | 来源:发表于2017-11-24 20:48 被阅读47次

    NSAutoreleasePool 和 @autoreleasepool

    1. NSAutoreleasePool 和 @autoreleasepool都是是Cocoa 用来支持引用计数内存管理机制的类, 当一个autorelease pool(自动释放池)被drain(销毁)的时候会对pool里的对象发送一条或者多条release的消息。

    2. NSAutoreleasePool仅能在MRC下使用,ARC下只能使用@autoreleasepool,它比NSAutoreleasePool类效率更高;鼓励用它来代替NSAutoreleasePool的位置。

    3. 一个对象可以放入同一个池中多次,在这种情况下,每次将对象放入池中时都会收到一条释放消息。 程序中至少存在一个自动释放池, 否则autoreleased对象将不能对应收到release消息而导致内存泄露.

    4. NSAutoreleasePool对象不能retain, 不能autorelease, 所以drain方法(或者release方法, 但是这两者有所不同, 下文会说)可以直接释放内存. 你应该在同一个上下文(调用创建这个池的同一个方法, 函数或者循环体)中drain一个自动释放池。

    5. MRC下需要对象调用autorelease才会入池, ARC下可以通过__autoreleasing修饰符, 否则的话看方法名, 非alloc/new/copy/mutableCopy开头的方法编译器都会自动帮我们调用autorelease方法。

    注意: 主线程是默认在当前runloop循环结束的时候统一对加入到自动释放池中的对象发送release消息,如果当前这个对象引用计数为1的话那么它就将被销毁。

    AutoreleasePool的释放有如下两种情况:
    1.是Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop循环中都加入了自动释放池Push和Pop。
    2.是手动调用AutoreleasePool的释放方法(drain方法)来销毁AutoreleasePool

    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    NSString* nsstring;
    char* cstring = "Hello CString";
    nsstring = [NSString stringWithUTF8String:cstring];
    [pool release];(这一行代码就是在给pool发送drain消息了)
    

    补充知识:使用ARC之后一个类方法(非alloc/new/copy/mutableCopy的初始化方法)生成的对象,没有任何附加标示,ARC怎么知道生成的对象是不是autorelease的呢?

    @interface Sark : NSObject  
    + (instancetype)sarkWithMark:(NSString *)mark;       // 1  
    - (instancetype)initWithMark:(NSString *)mark;       // 2  
    @end 
    

    1、生成autorelease对象。
    2、生成普通对象,而现在ARC不能调用autorelease,使用时怎么能知道呢?
    其实NS定义了下面三个编译属性

    #define NS_RETURNS_RETAINED __attribute__((ns_returns_retained))  
    #define NS_RETURNS_NOT_RETAINED __attribute__((ns_returns_not_retained))  
    #define NS_RETURNS_INNER_POINTER __attribute__((objc_returns_inner_pointer)) 
    

    这三个属性是Clang自己使用的标示,除非特殊情况不要自己使用,但是这些对理解ARC是很有帮助的。

    • NS_RETURNS_RETAINED
      init和initWithMark都属于init的家族方法。
      对于以alloc,init,copy,mutableCopy,new开头的家族的方法,后面默认加NS_RETURNS_RETAINED标识.ARC在会在调用方法外围要加上内存管理代码:retain+release。

    • NS_RETURNS_NOT_RETAINED
      sarkWithMark方法,则是不带alloc,init,copy,mutableCopy,new开头的方法,默认添加NS_RETURNS_NOT_RETAINED标识.标识返回的对象已经在方法内部做过autorelease了。

    • NS_RETURNS_INNER_POINTER
      这个只是做返回纯C语言的指针变量,ARC外围不必做内存管理的操作。

    这里还要介绍一个概念,Method family:

    An Objective-C method may fall into a method family, which is a conventional set of behaviors ascribed to it by the Cocoa conventions.
    

    指的是命名上表示一类型的方法,比如- init和- initWithMark:都属于init的family于是乎,编译器约定,对于alloc,init,copy,mutableCopy,new这几个家族的方法,后面默认加NS_RETURNS_RETAINED标识;而其他不指名标识的family的方法默认添加NS_RETURNS_NOT_RETAINED标识。

    也就是说刚才的方法,在编译器看来是这样的:

    @interface Sark : NSObject  
    + (instancetype)sarkWithMark:(NSString *)mark NS_RETURNS_NOT_RETAINED; // 1  
    - (instancetype)initWithMark:(NSString *)mark NS_RETURNS_RETAINED;     // 2  
    @end 
    

    这也就是为什么ARC下面,不能把一个属性定义成名字是这样的:

    @property (nonatomic, copy) NSString *newString; // 编译器不允许 
    
    • newString就成了new家族的方法,ARC在外围添加内存管理代码的时候会加上retain+release,从而导致内存管理错误。
      对于NS_RETURNS_INNER_POINTER这货,主要使用在返回的是一个对象的内部C指针的情况。

    ARC中显示或者隐式方法调用对引用计数的影响

    OC中我们采取[target action]的方式调用方法,但在消息转发或者其他runtime参与的方法调用时,我们会用其他的书写方式来实现[target action]的一样的功能.姑且称[target action]为显示调用,其他的书写方式为隐式调用。(NSInvocation的使用就是最好的非明文调用)

    • 隐式调用工厂方法
    - (void)ImplicitFunc{
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[NSMethodSignature signatureWithObjCTypes:"@@:"]];
        invocation.target = self;
        invocation.selector = @selector(createDic);
        [invocation invoke];
        
         //这样写的话会直接崩溃  ①
        NSDictionary * dict;
        [invocation getReturnValue:&dict];
    
        //正确做法  ②
        //NSDictionary * dict;
        //void * result;
        //[invocation getReturnValue:&result];
        //dict = (__bridge id)result;
    }
    
    -(NSDictionary *)createDic{
        return [[NSDictionary alloc]init];
    }
    
    
    • 显示调用工厂方法
     -(void)explictFunc{
      [self  createDic];
    }
    

    在隐式调用的例子中中我们没有对son进行显式的赋值,而是传入 getReturnValue:方法中去获取返回值,这样的赋值后 ARC 没有自动给这个变量插入retain语句,但退出作用域时还是自动插入了release语句,导致这个变量多释放了一次,导致crash。

    然而我们采用正确的做法,多了一个bridge就不crash了呢?

    (__bridge T) op:告诉编译器在 bridge 的时候不要多做任何事情,__bridge 源在哪端,哪端管理对象的释放。
    
    // objc to cf或者c
    NSString *nsStr = [self createSomeNSString];
    CFStringRef cfStr = (__bridge CFStringRef)nsStr;
    CFUseCFString(cfStr);
    // CFRelease(cfStr); 不需要
    //源在Objc端 ARC管理内存
    
    // cf或者c to objc
    CFStringRef hello = CFStringCreateWithCString(kCFAllocatorDefault, "hello", kCFStringEncodingUTF8);
    NSString *world = (__bridge NSString *)(hello);
    CFRelease(hello); // 需要
    [self useNSString:world];
    //源在CF端 需要我们自己管理内存
    

    [invocation getReturnValue:&result];将变量值写入了一个C指针, ARC 没有自动给这个变量插入retain语句。
    dict = (__bridge id)result;这里完成了对象的retain。
    ARC在退出方法的作用域时给对象加上release。完美!

    Autorelease原理

    AutoreleasePoolPage

    而这两个函数都是对AutoreleasePoolPage的简单封装,所以自动释放机制的核心就在于这个类。

    AutoreleasePoolPage是一个C++实现的类

    AutoreleasePoolPage.jpg
    • AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)
      AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
    • AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址
    • 上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
    • 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入

    所以,若当前线程中只有一个AutoreleasePoolPage对象,并记录了很多autorelease对象地址时内存如下图:

    4069Byte.jpg

    图中的情况,这一页再加入一个autorelease对象就要满了(也就是next指针马上指向栈顶),这时就要执行上面说的操作,建立下一页page对象,与这一页链表连接完成后,新page的next指针被初始化在栈底(begin的位置),然后继续向栈顶添加新对象。

    所以,向一个对象发送- autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置

    释放时刻

    每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil),那么这一个page就变成了下面的样子,于是:

    哨兵对象.jpg

    objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是:

    • 根据传入的哨兵对象地址找到哨兵对象所处的page
    • 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page,并向回移动next指针到正确位置。

    运行时优化(Thread Local Storage)

    hread Local Storage(TLS)线程局部存储,目的很简单,将一块内存作为某个线程专有的存储,以key-value的形式进行读写,比如在非arm架构下,使用pthread提供的方法实现:

    void* pthread_getspecific(pthread_key_t);
    int pthread_setspecific(pthread_key_t , const void *);

    id objc_retainAutoreleaseReturnValue(id value)

    *Precondition:*  `value`  is null or a pointer to a valid object.
    If `value` is null, this call has no effect. Otherwise, it performs a retain operation followed by the operation described in [objc_autoreleaseReturnValue](http://clang.llvm.org/docs/AutomaticReferenceCounting.html#arc-runtime-objc-autoreleasereturnvalue) . Equivalent to the following code:
    
    id objc_retainAutoreleaseReturnValue(id value){
    return objc_autoreleaseReturnValue(objc_retain(value));
    }
    
    Always returns value.
    

    objc_retainAutoreleasedReturnValue函数主要用于最优化程序运行,用于在alloc/new/copy/mutableCopy以外的方法。

    • 如果value为空这个函数无效。
    • 如果函数调用成功,它将接受一个来自最近调用的函数或者它调用的的,一个TLS引用计数的移交。
    • 如果调用失败,它执行一个和object_retain 完全一样的引用计数保留操作。

    id objc_retainAutoreleasedReturnValue(id value)

    Precondition: value is null or a pointer to a valid object.
    If value is null, this call has no effect. Otherwise, it attempts to accept a hand off of a retain count from a call to objc_autoreleaseReturnValue on value in a recently-called function or something it calls. If that fails, it performs a retain operation exactly like objc_retain.
    Always returns value.
    

    objc_retainAutoreleasedReturnValue和objc_retainAutoreleasedReturnValue是成对出现的。

    • 如果value为空,函数无效。
    • 如果函数调用成功,它会执行一个高效的引用计数的移交。
    • 如果调用失败,那么这个对象会被自动释放,像objc_autorelease一样。

    对于类似[NSArray array]的工厂方法,正常调用的情况下
    工厂方法内由objc_autoreleaseReturnValue将对象放入Thread Local Storage;
    工厂方法内由objc_retainAutoreleasedReturnValue将对象由Thread Local Storage取出.
    简单的说就是中转不走autoreleasepool,由Thread Local Storage代劳,这样对于工厂方法而言避免使用autoreleasepool对象,调用方和被调方利用TLS做中转,很有默契的免去了对返回值的内存管理。

    当然走优化路径是有要求的:工厂方法的调用方与被调用方都支持ARC,因为只有这样方法内的·objc_autoreleaseReturnValue·与·objc_retainAutoreleasedReturnValue·才会配套使用.很多系统库还可能是MRC实现的,这样的系统类调用工厂方法生成的对象还是得进autoreleasepool。

    那也就是说ARC下只要调用方和被调方都用ARC编译时,所建立的对象都不加入autoreleasepool.更简单的说我们自己写的类,调用工厂方法生成对象都不会放入autoreleasepool。

    于是问题又来了,假如被调方和主调方只有一边是ARC环境编译的该咋办?(比如我们在ARC环境下用了非ARC编译的第三方库,或者反之)
    只能动用更高级的黑魔法。

    __builtin_return_address

    这个内建函数原型是char *__builtin_return_address(int level),作用是得到函数的返回地址,参数表示层数,如__builtin_return_address(0)表示当前函数体返回地址,传1是调用这个函数的外层函数的返回值地址,以此类推。

    • (int)foo {
      NSLog(@"%p", __builtin_return_address(0)); // 根据这个地址能找到下面ret的地址
      return 1;
      }
      // caller
      int ret = [sark foo];
      看上去也没啥厉害的,不过要知道,函数的返回值地址,也就对应着调用者结束这次调用的地址(或者相差某个固定的偏移量,根据编译器决定)
      也就是说,被调用的函数也有翻身做地主的机会了,可以反过来对主调方干点坏事。
      回到上面的问题,如果一个函数返回前知道调用方是ARC还是非ARC,就有机会对于不同情况做不同的处理。

    Autorelease和RunLoop

    App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

    第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

    第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

    在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

    相关文章

      网友评论

          本文标题:Autorelease和ARC

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