美文网首页
我所理解的内存管理:2、ARC相关

我所理解的内存管理:2、ARC相关

作者: 杨淳引 | 来源:发表于2016-08-18 16:58 被阅读39次

    6、ARC仍然遵循MRC的内存管理方式,4个基本规则在ARC下仍然是有效的,区别只在于:MRC模式下需要手动键入retain、release等方法来遵循这些规则,ARC环境下则是编译器自动在适当位置插入retain、release等方法去遵循这些管理规则,你不再需要去调用retain、release或者autorelease方法。

    7、ARC新增了几个变量修饰符(variable qualifiers),分别是:__strong、__weak、__autoreleasing和__unsafe_unretained,ARC使用这些修饰符来管理retainCount,以遵循4个内存管理规则。

    8、__strong、__weak和__autoreleasing修饰符都能使附有这些修饰符的变量初始化为nil,即是说:

    id __strong obj0;
    id __weak obj1;
    id __autoreleasing obj2;
    

    等同于:

    id __strong obj0 = nil;
    id __weak obj1 = nil;
    id __autoreleasing obj2 = nil;
    

    __unsafe_unretained则没有这个效果。

    __strong

    9、__strong是默认的修饰符(qualifier),即是说:当一个变量没有标识任何修饰符的时候,就相当于被标识了__strong。
    当一个对象有几个标识为__strong的指针指向它的时候,它就至少有几个retainCount;所以当一个对象有__strong指针指向它时,它就永远不会被释放。

    10、使用以下代码初始化一个对象:

    id obj = [[NSObject alloc] init];
    

    在MRC模式下,生成这个NSObject对象并用名为obj的指针指向它,它默认会有1个引用计数(因为使用了alloc方法),如果没有其他指针指向它的话,在调用[obj release]之前,这个NSObject对象都会存活着。所以这个NSObject对象的生命周期是从alloc生成到调用release释放;
    在ARC模式下,这句初始化代码其实相当于:

    id __strong obj = [[NSObject alloc] init];
    

    生成这个NSObject对象并用__strong指针obj指向它,由obj指针持有对NSObject对象的引用,NSObject对象的引用计数为1。在这个obj变量超出它的作用域之前,强引用一直都有效,直到obj变量超出作用域后,它对NSObject对象的强引用才会失效,此时NSObject对象被释放。所以这个NSObject对象的生命周期是从alloc生成到obj变量超出作用域后没有强引用指向它而被释放;

    11、另外,在MRC环境下,一个变量要持有一个非自己生成的对象,需要这么做:

    id obj = [NSMutableArray array];
    [obj retain];
    

    需要手动去调用retain方法实现“持有”这个操作。
    在ARC环境下,你只需要这么写:

    id obj = [NSMutableArray array];
    

    由于obj变量默认标识了__strong修饰符,这种变量在指向非自己生成的对象时,编译器会在这之后自动插入一句[obj retain]代码。所以在ARC环境下使用__strong修饰符的变量不再需要手动去retain一个对象。

    12、比较在MRC和ARC下,一个对象的生存周期如下:


    13、在ARC环境下,对于标识了__strong的变量,它是如何遵循4个内存管理呢?来回顾一下这4个规则:
    (1)、自己会持有自己生成的对象:
    根据上文10便可知道这一点在ARC环境下和在MRC环境下是一样的;
    (2)、你也可以通过retain去持有对象:
    根据上文11也可知道在ARC环境下已不需要再调用retain方法;
    (3)、当你不需要再用到一个对象的时候,你必须释放掉你对这个对象的持有:
    在ARC环境下已经不能手动调用release方法或autorelease方法来释放对象,所以也没有了“主动释放”掉对象这种说法了。编译器会在持有对象的变量失效的时候,自动插入适当的释放方法;
    (4)、你不能去释放你没持有的对象:
    与第(3)点同理,这种情况是不可能发生的了。
    所以可以看到,在ARC环境下使用标识了__strong修饰符的变量的时候,内存管理也同样遵循了这4条规则。

    14、但是如果仅仅使用__strong修饰符的话,有可能会出现引用循环,导致内存泄漏。以下就是一个引用循环的例子:
    (1)、新建一个类Somthing,类内也有一个strong的Something属性:



    (2)、正常情况下,初始化两个Something对象,然后不再做任何操作。此时sth1变量和sth2变量分别持有两个对象,当超出两个变量的作用域之后,强应用失效,两个对象也就被正常释放了,如下图:



    (3)、如果加入以下的两句代码:

    假设sth1所指向的Something对象为S1,sth2所指向的Something对象为S2。那么这两句代码使得S1对象的属性sth指向了S2对象,同时S2对象的属性sth也指向了S1对象。

    此时sth1和S2对象都直接或间接地持有了S1,同时sth2和S1对象也都直接或间接地持有了S2,S1和S2的retainCount都是2。当超出两个变量的作用域之后,sth1和sth2对对象的强引用失效,此时就只剩下S1和S2的相互持有了,它们的retainCount都降成了1。
    由于S1和S2仍然有retainCount,所以不会被自动释放。而且再没有任何外部指针指向这两个对象,也就无法再对这两个对象进行任何操作了,它们就形成了一个引用循环,会一直存活在内存里,造成了内存泄漏。
    (4)、使用Instruments可以清楚地查看到这个引用循环:


    __weak

    15、引用循环是对象之间的相互引用达到一种平衡造成的,为了解决这个问题,就必须要打破这个平衡。
    那么有两种处理方式可以打破这个平衡:
    (1)、一种是在某一方的引用不再需要之后,将引用指向nil,那么另一方的retainCount就会降为0并被释放,连锁导致其他循环引用的对象也能正确释放。
    这种处理方式的缺点在于,在编写代码的时候需要准确地找到“某一方的引用不再需要”这个点进行置nil,管理起来会很烦杂;
    (2)、另一种处理方法就相对清晰了,就是使用一个和强引用相对应的弱引用,表示一种不持有的引用方式,在强引用失效这个时间点,让弱引用自动置nil,也就可以打破循环引用的平衡了。

    16、__weak修饰符就表示这样一种弱引用的关系:
    使用__weak修饰符标识的变量并不会影响它所指向的对象的retainCount,当一个对象的retainCount变为0了之后(即是没有__strong的引用指向这个对象之后),所有指向这个对象的__weak变量都会自动被置为指向nil。

    17、那么使用__weak修饰符就可以解决上文的引用循环问题了。
    将Something类的sth属性标识为weak,那么在上文14中的S1对象对S2对象的间接持有就是弱引用,S2的retainCount只有1,当超出sth2的作用域之后,sth2对S2的强引用失效,S2的retainCount降为0,S1对象对S2对象的间接持有也就被置为了nil。
    S2对象对S1对象的间接持有也是同理。
    这样,就在sth1和sth2变量超出其作用域之后,S1对象和S2对象就会顺利被释放了,规避了引用循环的产生。

    __unsafe_unretained

    18、__unsafe_unretained和__weak类似,都是标识了一种弱引用,它修饰的变量同样不会持有指向的对象,对对象的retainCount没有任何影响。
    但是__unsafe_unretained和__weak还是有所区别的,当没有强引用指向__unsafe_unretained变量所指向的对象时,对象会被释放,但是__unsafe_unretained变量不会被自动置向nil,它会变成悬挂指针,非常危险。

    __autoreleasing

    19、虽然在ARC模式下,不能再使用autorelease方法,也不能使用NSAutoreleasePool类,但是其实autorelease功能还是能起作用的,主要就是通过使用__autoreleasing修饰符。
    在ARC模式下,使用@autoreleasepool来代替NSAutoreleasePool,使用__autoreleasing来代替autorelease方法,如下图:


    20、__autoreleasing修饰符一般不需要显式地使用,比如在下面这个例子中:

    NSError *error;
    BOOL OK = [myObject performOperationWithError:&error];
    if (!OK) {
      ...         
    }
    

    根据前文已知道,NSError *error其实相当于

    NSError * __strong error;
    

    而performOperationWithError:方法的定义是:

    - (BOOL)performOperationWithError:(NSError * __autoreleasing *)error;
    

    编译器会自动将上方的这段代码改写成:

    NSError * __strong error;
    NSError * __autoreleasing tmp = error;
    BOOL OK = [myObject performOperationWithError:&tmp];
    error = tmp;
    if (!OK) {
        ...         
    }
    

    在变量定义和方法定义之间的差别会导致编译器自动创建了一个标识为__autoreleasing的临时变量tmp,然后使用这个tmp代替error供方法使用。这种情况下,编译器会自动生成修饰为__autoreleasing的变量,不需要显式地定义。

    21、为什么20中的performOperationWithError:方法的参数需要标识为__autoreleasing呢?
    假设performOperationWithError:方法的实现大致是这样:

    - (BOOL)performOperationWithError:(NSError * __autoreleasing *)error {
        ...         
        //如果发生了错误
        *error = [[NSError alloc] initWithDomain:myAppDomain code:userCode userInfo:nil];
        return NO;
    }
    

    可以看到,方法内部会创建一个NSError对象并把它赋值给*error,为了不让这个NSError对象在方法结束后被释放掉,于是*error变量需要标识为__autoreleasing,把这个NSError对象注册到当前的autoreleasel pool中,那么在方法结束后,外部定义的NSError变量就仍然能访问到这个对象。

    22、id的指针和对象的指针(比如id *objp或NSObject **objp)在没有显式指定的情况下,就会被附上__autoreleasing修饰符。

    23、有时候,不使用__autoreleasing修饰符也能达到autorelease的效果,比如在一个方法内,将某个创建的对象被作为方法的返回值使用时,编译器是会自动将其注册到autoreleasepool的。

    __block

    24、除了ARC新添加的这4个修饰符之外,还有__block这个修饰符也值得一提。

    25、__block的主要作用是标识一个变量可以在Block中可以被修改,它在ARC环境下和在MRC环境下是有区别的:
    在MRC模式下,使用__block修饰的变量在Block中不会被retain;在ARC模式下使用__block修饰的变量在Block中会像正常变量一样被retain。
    所以在MRC模式下,__block会有两个效果:
    ①、表示这个变量在Block中不被retain;
    ②、表示这个变量在Block中可被修改;
    在ARC模式下,__block就只剩一个效果了:
    ①、表示这个变量在Block中可被修改;
    但是也有例外的情况:使用static标识的变量和全局变量不需加上__block就可以在Block中修改。

    26、如果在ARC模式下,使用__block标识的变量想要获得像MRC模式下的效果(不retain),可以这么写:

    __unsafe_unretained __block id x;
    

    但是使用__unsafe_unretained有可能会造成悬挂指针,所以不建议这么做。如果你不需要适配iOS4或iOS4以下,你可以使用__weak来得到这个效果:

    __weak __block id x;
    

    27、与使用__strong、__weak或__unsafe_unretained修饰的其他变量不同的是,被 __block 修饰的变量在块中保存的是变量的地址。(其他保存的是变量的值)

    28、__block和__weak修饰符的区别:
    ①、__block不管是ARC还是MRC模式下都可以使用,不止可以修饰对象,还可以修饰基本数据类型。而__weak只能在ARC模式下使用,只能修饰对象,不能修饰基本数据类型;
    ②、__block对象可以在Block中被重新赋值,__weak不可以。

    ARC的规则

    29、相对于MRC模式,ARC模式增加了以下这些编码规则。如果你违背了这些规则,编译器会发出警告:
    (1)、<b>不允许直接调用dealloc方法,也不允许直接调用retain、release、autorelease方法</b>:
    当然也不允许使用@selector(retain)、@selector(release)这些selector。
    在dealloc方法内可以做一些诸如释放实例变量、将delegate设置为nil等操作,但是不再需要在方法尾调用super的dealloc方法。
    但是允许使用CFRetain、CFRelease等这些Core Foundation对象的方法,具体用法可参照后文的Toll-Free Briging;
    (2)、<b>不允许使用NSAllocateObject或者NSDeallocateObject函数</b>:
    实际上,在使用alloc方法为对象分配内存的时候,是会调用到NSAllocateObject函数的,runtime会自动调用dealloc方法,它同样会调用到NDeallocateObject函数。ARC模式下不允许直接调用这两个函数;
    (3)、<b>不允许使用对象型变量作为C语言结构体的成员</b>:
    这是因为如果把一个对象型变量放在结构体内,因为C语言的规约并没有方法可以管理结构体成员的生命周期,就导致这个对象的生命周期也无从管理了,这是ARC所不允许的。
    如果一定要包含对象型变量,一般来说,就不要使用结构体了,应该定义一个类来处理。
    或者可以将这个对象型变量成员标识为__unsafe_unretained,这样编译器就不会管理这个成员的生命周期了,但是这种方法处理不好很容易出现内存泄漏;
    (4)、<b>不允许直接转换id变量和void*变量</b>:
    在MRC模式下,以下代码是允许的:

    id obj = [[NSObject alloc] init];
    void *p = obj;
    id o = p;
    [o release];
    

    可以看到,id变量和void*变量可以自由转换。但是在ARC模式下,这种转换方式就是不允许的了,如果需要进行转换,需要使用Toll-Free Briging,详见后文;
    (5)、<b>不允许使用NSAutoreleasePool对象</b>:
    在ARC模式下,使用@autoreleasepool块来代替NSAutoreleasePool对象,由于@autoreleasepool块在MRC模式下也是生效的,所以只用@autoreleasepool块就可以了;
    (6)、<b>不允许使用NSZone</b>;
    (7)、<b>不允许使用new开头来命名方法</b>:
    所以属性名也不要命名为new开头,除非你另外给这个属性指定一个不以new开头的getter方法。

    相关文章

      网友评论

          本文标题:我所理解的内存管理:2、ARC相关

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