美文网首页
iOS - 内存管理(一)之MRR

iOS - 内存管理(一)之MRR

作者: FKSky | 来源:发表于2020-07-04 17:59 被阅读0次

    1.前言

    好久没有写过东西了,想了很久写写啥,想来想去还是来讲讲iOS最基本的也是很重要的内存管理,虽然也是烂大街的内容了,但是针对一些入门不就的同学,我希望很多表达用比较直白的方式来进行,我也一直觉得一个好的教程就应该针对一些难点用非常白话的方式来解释清楚,能让别人越容易理解才表示自己理解的越清晰。同时我在讲解一个东西的时候,尽量不去涉及衍生到其他相关层的内容,或者其他更底层的实现,因为这些东西就属于另外一个话题了,而对于理解当前目标知识可能会产生困扰的作用。
    这篇文章适合已经有少许的objective-c的编程知识的,基本语法方面不做讲解。

    2.简述

    Objective-C有两种内存管理方式,分别是MRR(manual retain-release)ARC(automatic reference counting)。其实没有MRC这一说,我猜可能是因为后来出现了ARC才有人为了对称把MRR叫成MRC,不过名字怎么叫这些都不重要。
    简单来说就是一个是手动管理,一个自动管理。现在基本没有人用手动管理了(有的话也是狠人,何必跟自己过不去),但是要理解内存管理就必须要懂得MRR的原理,ARC不过是编译器自动为我们加上了内存管理的一些标识。
    对于内存管理最重要的概念从上面的名字全称就能知晓一二,那就是retainreleasereference counting

    苹果官方文档解释两种内存管理方式。千万别听信它后半段说的:“你如果用ARC就不用看下面MRR的内容了”

    3.MRR

    3.1本质

    学任何东西先了解它最最根本的原理,我先不说官方文档讲的什么持有对象、所有权、巴拉巴拉之类的定义,稍后再慢慢引入进来。
    记住:MRR的本质就是手动管理对象的retainCount数值,从而管理了对象的销毁和内存的释放。
    可以这样理解任何一个你创建的对象都有一个对应的retainCount值,刚创建出来它是1,你可以对这个对象使用release方法,这个对象的retainCount值就会-1,当他变成变成0,系统就会它释放掉。

    //刚创建出来TestObj的retainCount = 1
    TestObj *obj = [[TestObj alloc] init];
    //执行了一次release后,retainCount = 0,
    [obj release];
    //走完这一行后,这个TestObj就被销毁了,对应的内存也就释放了
    .
    .
    .
    

    是不是非常简单,说白了就是要弄明白什么时候写这个release。

    3.2 retainCount的变化

    3.2.1 设一方法

    首先我们来讲能让retainCount变化的最基本的方法,就是创建对象的方法,记住alloc/new/copy/mutableCopy这4个。
    这4个最基本的里面的最基本的一个就是alloc方法。new方法下面实际上就是使用了alloc init,区别不大,这个不展开说。所以alloc和new可以归为一类,都是初始化一个新的对象,并且这个新对象的retainCount是等于1的。
    copy和mutableCopy从名字就可以看出,他们是拷贝一个已有的对象A,新创建一个一模一样内部数据的副本对象B,原始对象A的retainCount还是维持不变,新的副本对象B的retainCount等于1,至于copy和mutableCopy的区别,我会再下一篇里面详细再讲,同时刚刚这句话从某种层面上来说不太严谨!,至于为什么,也在下一篇专门讲copy的文章里详细说。在这一篇里大家就把它们两个当成一个复制方法,就是拷贝出一个新的retainCount等于1的对象。
    总结:以后见到alloc/new/copy/mutableCopy这4个名称的方法,或者以这4个名称作为开头的方法,就知道这是返回了一个新的对象,它的retainCount被设置为1了。

    3.2.2 加一方法

    加一方法比较简单,记得一个就行了,就是retain。对一个已经创建了的对象调用retain方法,它的retainCount就会增加一,并且它的返回值也是当前这个对象。

    3.2.3 减一方法

    减一方法也比较简单,就两个,一个是release,一个是autorelease。只要对一个对象调用这两个方法,它retainCount就会减少1。至于这两个release方法对区别,再下文再说明,目前就知道他们会是retainCount减一就行了。

    3.2.4 综合示例
    苹果官方文档的示例图

    上面这个图是苹果的内存管理官方文档里的,注意那个中心浅蓝色的圆环才是表示一个创建的对象,时间顺序是从左往右。
    1.一开始通过alloc/init创建了一个新的对象,retainCount = 1;
    2.然后调用了一次retian,retainCount = 2;
    3.再使用了copy复制了一个新的副本对象出来,注意图中copy执行完后分叉出来一条虚线,那个新的对象retainCount等于1,原本的还是等于2
    4.看上面那个条实线的,连续调用了2次release,retainCount = 2 - 1- 1 = 0,然后就被销毁了。
    5.看下面的虚线的,对副本对象调用了一次release,retainCount = 1 -1 = 0,然后也被销毁了。
    通过这个示例图和上面的说明应该都能明白这个几个方法的作用了,换句话来说,一个对象被创建出来,就要有一个配对的release出现,你要对它另外用了几次retain,那还要补上对应数量的release。这里的难点其实就在,什么时候在什么位置写retian或者release。

    3.3 内存管理的规则

    内存管理的核心规则就是:谁持有该对象,谁就有责任去释放(即有责任去调用release)
    如果你有责任释放,你不释放就是渎职,结果就是内存泄漏,也就是有块内存在你程序运行时不能用了,浪费了,你这样浪费的多了,你程序占用的内存就越来越大。
    如果不是你持有的,你瞎去释放,结果就是运行时错误,你的程序会crash。
    ps:这样看好像渎职没有多管闲事严重,至少自己程序还能继续运行。

    3.3 内存管理的规则的解析

    接下来针对“谁持有该对象,谁就有责任去释放“这个规则做解析,不理解的同学肯定会问什么叫做持有?千万不要误解持有就是有个设个变量等于一个对象就是持有了这个对象,这是不正确的。另外持有是针对一个固定的一个域、一个范围来说,可以简单理解为在一个{ } 大括号内,通常是一个方法里,要管理好这个范围里的你持有的对象。马上详细说

    3.3.1你持有你所创建的

    还记得上面的“设一方法”么?只要是通过alloc/new/copy/mutableCopy这4个名字开头的方法创建的一个新的对象,你就是持有他了。你就有责任在它的作用域内,当你不在需要使用它的时候,调用一个release方法。

    //假如有个玩电脑的方法
    - (void)playComputer
    {
      //创建一个新电脑,因为是通过alloc创建的,表示你持有了
      Computer *computer = [[Computer alloc] init];
      //你用电脑干了很多事
      [computer checkEmail];
      [computer surfOnline];
      [computer playGTA];
       
      //你用完了这个电脑,你有责任释放这个没用电脑
      [computer release];
    }
    

    注意使用非上述的创建对象方法,创建的对象你并不持有!!!比如:

    {
      //常见的就是一些创建对象用的便利的类方法
      //通过array类方法,也能快速创建一个NSArray对象出来,但是注意,你并不持有这个对象,
      NSArray *array = [NSArray array];
    
      //你没有责任在这个释放它!
      [array release];//不可能写这一句
    }
    

    肯定有同学看到这个有点懵,这也太蛋疼了,一会要写一会不要写,我们就来详细解释一下,这个时候我们就需要先讲解一下上面留下的一个问题,release和autorelease是什么区别?

    release就是马上让对象的retainCount减一,减完一以后如果retainCount是0了,这个对象就销毁了,没有了。
    而autorelease,是一种延迟减一的方法,它将对象放入了一个autorelease pool里,当这个pool完蛋的时候,会对这个pool里面所有调用过autorelease的对象,统一对他们调用一次release方法。相当于pool是一个垃圾管理员,对象调用autorelease就被丢给垃圾管理员了,在垃圾管理员快下班的时候,它对它兜里的所有垃圾执行一次真正的release,这个时候这些垃圾才被销毁了。
    这个autorelease pool就不在这里详细正经的认真解释,我在后续篇章再讲,只要先理解记住release是马上减一,autorelease是在未来合适的时候系统自动帮它减一。

    我们继续接回前面,我们来猜一下array这个创建方法是怎么写的

    //假设NSArray的array类方法
    + (NSArray *)array
    {
      //创建一个新的对象
      NSArray *array = [[NSArray alloc] init];
    
      /*
      记得上面的规则么?array既然是这个方法通过alloc创建的,那么这个方法就持有了这个对象,它就有责任释放它。
      但是你不能在这里使用release,因为你要在这里调用了release,
      这个对象retainCount就马上等于0了,马上被销毁了,我还怎么将它当成返回值返回回去?
      所以这里只能用autorelease,表示我是有责任释放它的,我也履行了我的责任
      只是它不是马上被销毁,而是将来肯定会被销毁的。
      我只要将alloc和release做了一对匹配就算完成了任务。
      */
      [array autorelease];
    
      //返回新建的array
      return array;
    }
    

    通过上面我们就能理解了,所有非alloc/new/copy/mutableCopy开头的方法创建的对象,他们本身的retainCount已经平衡了,你不做任何行为,他们都会被销毁掉的。

    再看一个苹果官方文档里的例子,这是一个Person类的一个实例方法,调用它就会通过当前Person的firstName和lastName拼接成一个全名,并且返回该字符串

    - (NSString *)fullName {
      //因为是使用alloc创建的,所有fullName这个方法就拥有了这个对象,就有释放它的责任
      //但是因为要将这个字符串返回给调用方
      //所有使用了autorelease而不是release来履行它持有者的责任
      NSString *string = [[[NSString alloc] initWithFormat:@"%@ %@",
                                                      self.firstName, 
                                                      self.lastName] autorelease];
      return string;
    }
    

    注意:在MRR模式下,大家自定义的对象的自定义的方法都要遵守这种命名规则来实现。
    你要是自定义了一个方法,方法名用了alloc/new/copy/mutableCopy这4个东西开头,那么你就不能在这个方法里release或者autorelease这个方法的返回值对象。
    你的自定义方法名不是这4个词开头的,你就要保证这个方法的返回值对象是autorelease过的

    3.3.2 你可以持有一个你只有使用权的对象

    我们现在知道,上面的这个例子,这个作用域并不拥有这个array对象,只有它的使用权

    {
      //并不拥有array对象,但是可以使用它,可以调用它的方法,读取它的属性值
      NSArray *array = [NSArray array];
    }
    

    我们如果想持有这个对象,以防这个对象用着用着因为特殊原因销毁了怎么办?
    很简单那就对它调用一次retain,就表示你拥有了。其实本质的原理也还是我们之前说过的,retian就将对象的retainCount马上加一了,只要我这个目前的持有者不去调用release或者autorelease,它就不会被突然销毁掉,我就能放心使用。

    {
      //并不拥有array对象,但是可以使用它,可以调用它的方法,读取它的属性值
      NSArray *array = [NSArray array];
      //我现在持有了它
      [array retain];
      //随便用,不用担心它突然没了
      .
      .
      .
      //用完了,记得减一
      [array release];
    }
    

    题外话,其实大家也能看出,因为持有不持有这个说法的本质还是对于retainCount的操作,所以上面说的也不是绝对的。即使你retain了一个对象,持有了它。但是有种情况这个对象也会被别人意外销毁,就是别人对这个对象调用了多次release。这个应该很容易理解吧。

    所以MRR模式下程序员要特别注意内存管理问题,大家一定都要按照上面说的,约定好的规则写代码,不然很容易出现内存问题

    3.3.3 setter方法

    如果我们自定的类有任何一个object对象类型的属性,我们就需要保证这个属性值在我们还要使用的时候就不能被销毁,也就是说必须一直持有它。通过下面这个例子类说明setter方法应该怎么写。
    假设一个自定义的Person类有一个叫_moneyAmount的实例变量,我们为它写一个set方法应该按下面这样写

    - (void)setMoneyAmount:(NSNumber *)newMoneyAmount
    {
      /*
      为了保险,先对传进来的新的newMoneyAmount对象retain一次,
      再release掉我们之前保存的那个旧的_moneyAmount
      因为假如新的newMoneyAmount和旧的_moneyAmount是同一个对象,我们不先retain直接release的话,这个对象很可能就销毁了。
      而先执行retain,就能避免这个意外情况。
      */
      [newMoneyAmount retain];
      [_moneyAmount release];
      _moneyAmount = newMoneyAmount
    }
    
    
    3.3.4 dealloc方法

    Objective-C里面的基类NSObject有一个dealloc方法,看名字就知道这个方法和创建方法alloc是反过来的。这个方法会自动触发,当这个对象没有拥有者的时候,或者说retainCount等于0了,即将被销毁了的时候触发执行。需要大家在这个方法里,释放这个对象所持有的资源,以上面那个例子来说就是要释放掉它所只有的_moneyAmount。

    - (void)dealloc
    {
      [_moneyAmount release];
    
      [super dealloc];
    }
    

    苹果的文档也提到,不要依赖于dealloc在对象销毁的时候才去释放一些稀缺资源,比如网络链接,缓存读取对象等等,因为dealloc有可能因为一些特殊原因或者bug会延迟或者避开执行了,所以对于这类资源应该提前主动的去释放,不然如果太多的类似稀缺资源没有被释放,可能就会影响到下次到再使用。当然对于上面的例子或者其他的一些普通属性值,就没太大关系。

    4.结语

    其实MRR的内存管理不复杂,把基本规则理解了,写代码时候遵守规则就没什么问题。如果理解了这里的基本内容可以进入下一篇iOS - 内存管理(二)之Copy

    相关文章

      网友评论

          本文标题:iOS - 内存管理(一)之MRR

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