美文网首页将来跳槽用
[iOS] Effective Objective-C ——内存

[iOS] Effective Objective-C ——内存

作者: 木小易Ying | 来源:发表于2019-10-13 21:21 被阅读0次

    29. 理解引用计数

    retain 递增保留计数。
    release 递减保留计数。
    autorelease 待稍后清理"自动释放池"(autorelease pool)时,再递减保留计数。

    调用者已通过alloc方法表达了想令该对象继续存活下去的意愿。不过请注意,这并不是说对象此时的保留计数必定是1。在alloc或"initWithInt:"方法的实现代码中,也许还有其他对象也保留了此对象,所以,其保留计数可能会大于1。能够肯定的是: 保留计数至少为1。保留计数这个概念就应该这样来理解才对。绝不应该说保留计数一定是某个值,只能说你所执行的操作是递增了该计数还是递减了该计数。

    为避免不经意间使用了无效对象,一般调用完release之后都会清空指针。这就能保证不会出现可能指向无效对象的指针,这种指针通常称为"悬挂指针"(dangling pointer)。比方说,可以这样编写代码来防止此情况发生:

    NSNumber *number = [[NSNumber alloc] initWithInt:1337];
    [array addObject:number];
    [number release];
    number = nil;
    

    **※ 自动释放池 **

    在Objective-C的引用计数架构中,自动释放池是一项重要特性。调用release会立刻递减对象的保留计数(而且还有可能令系统回收此对象),然而有时候可以不调用它,改为调用autorelease,此方法会在稍后递减计数(相当于执行一次release),通常是在下一次"事件循环"(event loop)时递减,不过也可能执行得更早些(参见第34条)。

    这个具体之后再讨论吧~

    30. 以ARC简化引用计数

    由于ARC会自动执行retain、release、autorelease等操作,所以直接在ARC下调用这些内存管理方法是非法的。具体来说,不能调用下列方法:

    - retain
    - release
    - autorelease
    - dealloc
    

    例如:

    if ([self shouldLogMessage]) {
        NSString *message = [[NSString alloc] initWithFormat:@"I am object, %p", self];
        NSLog(@"message = %@", message);
        //Added by ARC
        [message release];
    }
    

    实际上,ARC在调用这些方法时,并不通过普通的Objective-C消息派发机制,而是直接调用其底层的C语言版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多CPU周期。比方说,ARC会调用与retain等价的底层函数objec_retain。这也是不能覆写retain、release或autorelease的缘由,因为这些方法从来不会被直接调用。笔者在本节后面的文字中将用等价的Objective-C方法来指代与之相关的底层C语言版本,这对于那些手动管理过引用计数的开发者来说更易理解。


    ※ 使用ARC时必须遵循的方法命名规则

    将内存管理语义在方法名中表示出来早已成为Objective-C的惯例,而ARC则将之确立为硬性规定。这些规则简单地体现在方法名上。若方法名以下列词语开头,则其返回的对象归调用者所有:

    - alloc
    - new
    - copy
    - mutableCopy
    
    • 归调用者所有的意思是: 调用上述四种方法的那段代码要负责释放方法所返回的对象。也就是说,这些对象的保留计数是正值,而调用了这四种方法的那段代码要将其中一次保留操作抵消掉。如果还有其他对象保留此对象,并对其调用了autorelease,那么保留计数的值可能比1大,这也是retainCount方法不太有用的原因之一(参见第36条)。

    • 若方法名不以上述四个词语开头,则表示其所返回的对象并不归调用者所有。在这种情况下,返回的对象会自动释放,所以其值在跨越方法调用边界后依然有效。要想使对象多存活一段时间,必须令调用者保留它才行。

    这里其实再一次解释了autorelease~ 举个例子:

    - (EOCPerson*)newPerson {
        EOCPerson *person = [[EOCPerson alloc] init];
        return person;
        /**
         * The method name begins with `new’, and since `person’ 
         * already has an unbalanced +1 reference count from the 
         * `alloc’, no retains, releases or autoreleases are 
         * required when returning.
         */
    }
    
    - (EOCPerson*)somePerson {
        EOCPerson *person = [[EOCPerson alloc] init];
        return person;
        /**
         * The method name does not begin with one of the "owning" 
         * prefixes, therefore ARC will add an autorelease when 
         * returning `person’.
         * The equivalent manual reference counting statement is:
         *   return [person autorelease];
         */
    }
    
    - (void)doSomething {
        EOCPerson *personOne = [self newPerson];
        // …
    
        EOCPerson *personTwo = [self somePerson];
        // …
    
        /**
         * At this point, `personOne’ and `personTwo’ go out of 
         * scope, therefore ARC needs to clean them up as required. 
         * - `personOne’ was returned as owned by this block of 
             code, so it needs to be released.
         * - `personTwo’ was returned not owned by this block of 
             code, so it does not need to be released.
         * The equivalent manual reference counting cleanup code 
         * is:
         *    [personOne release];
         */
    }
    

    也就是说ARC会自己根据我们的方法名是不是以alloc/new/copy/mutablecopy来开头,在末尾为我们返回实例判断要不要加autorelease,如果加了,那么在使用方法的时候,结尾就不用再次release;如果没有加autorelease,在使用时出了作用域以后会加一次release。

    除了会自动调用"保留"与"释放"方法外,使用ARC还有其他好处,它可以执行一些手动操作很难甚至无法完成的优化。例如,在编译期,ARC会把能够互相抵消的retain、release、autorelease操作约简。如果发现在同一个对象上执行了多次"保留"与"释放"操作,那么ARC有时可以成对地移除这两个操作。

    举个优化例子:

    // From a class where _myPerson is a strong instance variable
    _myPerson = [EOCPerson personWithName:@"Bob Smith"];
    
    //调用"personWithName:"方法会返回新的EOCPerson对象,而此方法在返回对象之前,为其调用了autorelease方法。由于实例变量是个强引用,所以编译器在设置其值的时候还需要执行一次保留操作。因此,前面那段代码与下面这段手工管理引用计数的代码等效:
    EOCPerson *tmp = [EOCPerson personWithName:@"Bob Smith"];
    _myPerson = [tmp retain];
    

    然而刚刚autorelease之后就retain,其实是相抵消的。编译时期为了向后兼容并没有对这点优化,而运行时其实是做了检查避免了先release再retain的,这样会更快。

    在方法中返回自动释放的对象时,不再直接调用对象的autorelease方法,而是改为调用objc_autoreleaseReturnValue,设置全局数据结构中的一个标志位,此函数会检视当前方法返回之后即将要执行的那段代码,是否要retain返回对象,若要retain,那么不执行autorelease;若不retain,那么需要再执行autorelease。

    执行到retain那一句的时候只要检查那个标志位,如果已经设置了,说明之前有autorelease并且没有执行,于是这里也不用真的去retain了,相互抵消;如果没有设置标志位,就执行retain。

    @interface EOCClass : NSObject {
        id _object;
    }
    
    @implementation EOCClass
    - (void)setup {
        _object = [EOCOtherClass new];
    }
    @end
    
    ARC会自动改写为:
    - (void)setup {
        id tmp = [EOCOtherClass new];
        _object = [tmp retain];
        [tmp release];
    }
    
    由于retain后立刻release,所以其实抵消了,优化后其实仍旧是:
    _object = [EOCOtherClass new];
    

    一些内存修饰符:

    • __strong: 默认语义,保留此值
    • __unsafe_unretained: 不保留此值,这么做可能不安全,因为等到再次使用变量时,其对象可能已经回收了。
    • __weak: 不保留此值,但是变量可以安全使用,因为如果系统把这个对象回收了,那么变量也会自动清空。(常用与解决retain cycle)
    • __autoreleasing: 把对象"按引用传递"(pass by reference)给方法时,使用这个特殊的修饰符。此值在方法返回时自动释放。

    __autoreleasing是神马呢?其实使用__autoreleasing 修饰符对应MRC调用 autorelease方法,因为ARC是不能调用autorelease的,所以可以通过__autoreleasing修饰把对象放入autorelease pool,可参考:https://www.jianshu.com/p/0258ed2133ff


    ※ ARC如何清理实例变量

    ARC之后一般不需再编写dealloc方法,ARC会借用Objective-C++的一项特性来生成清理例程:编译器如果发现某个对象里含有C++对象,就会生成名为.cxx_destruct析构的方法,方法中有自动调用超类dealloc的方法。回收Objective-C++对象时,待回收的对象会调用所有C++对象的析构方法,利用该方法中生成清理内存所需的代码。

    非Objective-C的对象,如CoreFoundation中的对象或是由malloc()分配在堆中的内存,仍需要清理

    // ARC下:
    - (void)dealloc
    {
        CFRelease(_coreFoundationObject);
        free(_heapAllocatedMemoryBlob)
    }
    

    ARC只负责管理Objective-C对象的内存。尤其要注意: CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease。

    31. 在dealloc方法中只释放引用并解除监听

    那么,应该在dealloc方法中做些什么呢?主要就是释放对象所拥有的引用,也就是把所有Objective-C对象都释放掉,ARC会通过自动生成的.cxx_destruct方法(参见第30条),在dealloc中为你自动添加这些释放代码。对象所拥有的其他非Objective-C对象也要释放。比如CoreFoundation对象就必须手动释放,因为它们是由纯C的API所生成的。

    在dealloc方法中,通常还要做一件事,就是把原来配置过的观测行为都清理掉(KVO以及Observer)。如果用NSNotificationCenter给此对象订阅过某种通知,那么一般应该在这里注销。这样的话,通知系统就不再把通知发给回收后的对象了,若是还向其发送通知,则必然会令应用程序崩溃。

    但是最新的ios系统已经不需要在dealloc里面取消Observer了哈,监听也会由系统自动取消啦。

    如果不是ARC,在dealloc里面还要调用super dealloc哈。

    但不是所有资源都应该统一在dealloc里面释放的,有些大块资源不能等它自动释放,如果用完了就应该释放掉,例如各种Connection。

    因为如果内存泄漏,根本就不会走dealloc,那么这些大块内存就不能被释放掉了。

    系统并不保证每个创建出来的对象的dealloc都会执行。极个别情况下,当应用程序终止时,仍有对象处于存活状态,这些对象没有收到dealloc消息。在Mac OS X及iOS应用程序所对应的application delegate中,都含有一个会于程序终止时调用的方法。如果一定要清理某些对象,那么可在此方法中调用那些对象的“清理方法”。

    - (void)applicationWillTerminate:(UIApplication *)application
    

    ※ dealloc里面不能做什么

    • 不要在里面随便调用其他方法。
      如果在这里调用的方法又要异步执行某些任务,或是又要继续调用他们自己的某些方法,那么等到那些任务执行完毕时,系统已经把当前这个待回收的对象彻底摧毁了。

    • 调用dealloc方法的那个线程会执行“最终的释放操作”(final release),令对象的保留计数降为0,而某些方法必须在特性的线程里(比如主线程)调用才行。若在dealloc里调用了那些方法,则无法保证当前这个线程就是那些方法所需要的线程。

    • 在dealloc里也不要调用属性的存取方法。
      因为有人可能会覆写这些方法,并于其中做一些无法在回首阶段安全之行的操作。此外,属性可能正处于“键值观测”(Key-Value Observation, KVO)机制的监控之下,该属性的观察者(observer)可能会在属性值改变时“保留”或使用这个即将回收的对象。

    32. 编写“异常安全代码”时留意内存管理问题

    OC和C++都是有异常的(纯C是木有的),他们可以互相进行异常处理,也就是OC抛出的,会被C++ catch。

    使用MRC处理异常的时候应该注意内存释放,因为如果抛出了异常,可能释放的代码不会跑到,于是最好将release放到finally:

    @try{
        EOCSomeClass *object = [[EOCSomeClass alloc] init];
        [object doSomethingThatMayThrow];
    }
    @catch(...){
        Nslog(@"There was an error!");
    }
    
    // 无论是否发生异常,@finally块中的代码都会执行
    @finally{
        [object release];
    }
    

    而ARC里面是没有release方法的,并且其实ARC也没有默认会处理异常的内存问题,原因是ios只有在非常严重的问题的时候才会抛出异常,并且这个时候一般应用即将被终止,内存泄漏也就没有意义了,于是不需要处理。

    如果打开编译器的-fobjc-arc-exceptions标志,可以开启ARC生成安全处理异常所用的附加代码。(尽量不要开启)只是这段代码会严重影响运行期的性能,即使不抛出异常。这种场景主要是Objective-C++会用到,因为c++经常会抛出异常,如果有大量异常捕获操作时,应考虑重构代码,用21条的NSError式错误信息传递法来取代异常。

    33. 以弱引用避免retain cycle

    retain cycle导致内存泄漏

    一般来说,如果不拥有某对象,那就不要保留它。这条规则对collection例外,collection虽然并不直接拥有其内容,但是它要代表自己所属的那个对象来保留这些元素。

    34. 以“自动释放池”降低内存峰值

    释放对象有两种方式:

    • 调用release方法,使其保留计数立即递减
    • 调用autorelease方法,将其加入”自动释放池“中。自动释放池用于存放那些需要在稍后某个时刻释放的对象。清空自动释放池时,系统会向其中的对象发送release消息。

    下面这段代码中的花括号定义了自动释放池的范围。自动释放池于左花括号处创建,并于对应的右花括号处自动清空。位于自动释放池范围内的对象,将在此范围末尾处收到 release 消息。自动释放池可以嵌套。系统在自动释放对象时,会把它放到最内层的池里。比方说:

     @autoreleasepool {
      NSString *string = [NSString stringWithFormat:@"1= %i", 1];
       @autoreleasepool {
          NSNumber *number = [NSNumber numberWithInt:1];
        }
    }
    

    将自动释放池嵌套用的好处是,可以借此控制应用程序的内存峰值,使其不致过高。

    是否应该用池来优化效率,完全取决于具体的应用程序。首先得监控内存用量,判断其中有没有需要解决的问题,如果没完成这一步,那就别急着优化。尽管自动释放池块的开销不太大,但毕竟还是有的,所以尽量不要建立额外的自动释放池。

    35. 使用僵尸对象调试内存管理问题

    调试内存管理问题很令人头疼。大家都知道,向已回收的对象发送消息是不安全的。这么做有时可以,有时不行。具体可行与否,完全取决于对象所占内存有没有为其他内容所复写。而这块内存有没有移作他用,又无法确定,因此,应用程序只是偶尔崩溃。在没有崩溃的情况下,那块内存可能只复用了其中一部分,所以部分对象中的某些二进制数据依然有效。还有一种可能,就是那块内存恰好为另外一个有效且存货的对象所占据。在这种情况下,运行期系统会把消息转发到新对象那里,而此对象也许能应答,也许不能。如果能,那程序就不崩溃,可你会觉得奇怪:为什么收到消息的对象不是预想的那个呢?若新对象无法响应选择子,则程序依然会崩溃。

    所幸Cocoa提供了“僵尸对象“(Zombie Object)这个非常方便的功能。启用这项调试功能之后,运行期系统会把所有已经回收的实例转化为特殊的”僵尸对象“,而不是真正回收他们。这种对象所在的核心内存无法重用,因此不可能遭到复写。僵尸对象收到消息之后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。将NSZombieEnabled环境变量设为YES,即可开启此功能。

    给僵尸对象发消息后,控制台会打印消息,而应用程序会终止。打印出来的消息就像这样:-[CFString respondsToSelector:] message sent to deallocated instance 0x7ff9e9c080e0

    在Xcode里面可以这么设置(Edit Schema):


    开启僵尸调试

    僵尸调试具体原理

    举个例子,如果有个类是Zombie,当他变为僵尸对象后,也就是dealloc以后,对象所属的类会由Zombie变为_NSZombie_Zombie。但是,这个新类是从哪里来的呢?代码中没有定义过这样一个类。而且,在启用僵尸对象后,如果编译器每看到一种可能变成僵尸的对象,就创建一个与之对应的类,那也低效了。

    _NSZombie_Zombie实际上是在运行期生成的,当首次碰到Zombie类的对象要变成僵尸对象时,就会创建那么一个类。在创建的过程中用到了运行期程序库里得函数,它们的功能很强大,可以操作类列表(class list)。僵尸类(zombie class)是从名为NSZombie的模板类里复制出来的。这些僵尸类没有多少事情可做,只是充当一个标记。

    这个过程其实就是NSObject的dealloc方法所做的事。运行期系统如果发现NSZombieEnabled环境变量已设置,那么就把”dealloc“方法的“调配”(swizzle)成一个会执行生成_NSZombie_XXX类代码的版本。执行到程序末尾时,对象所属的类已经变为_NSZombie_OriginalClass了,其中OriginalClass指的是原类名。

    代码中的关键之处在于:对象所占内存没有(通过调用free()方法)释放,因此,这块内存不可复用。虽说内存泄漏了,但这只是个调试手段,发布正式应用程序时不会把这项功能打开,所以这种泄漏问题无关紧要。

    但是,系统为何要给每个变为僵尸的类都创建一个对应的模型呢?这是因为,给僵尸对象发消息之后,系统可由此知道该对象原来所属的类。假如把所有僵尸对象都归到NSZombie类里,那原来的类名就丢了。

    僵尸类的作用会在消息转发过程中体现出来。NSZombie类(以及所有从该类拷贝出来的类)并未实现任何方法。此类没有超类,因此和NSObject一样,也是个“根类”,该类只有一个实例变量,叫做isa,所有Objective-C的根类都必须有此变量。

    由于这个轻量级的类没有实现任何方法,所以发给它的全部消息都要经过“完整的消息转发机制”。在完整的消息转发机制中,_ _ forwarding _ _是核心,调试程序时,大家可能在栈回溯消息里看见过这个函数。它首先要做的事情就包括检查接收消息的对象所属的类名。若名称前缀为NSZombie,则表明消息接收者是僵尸对象,需要特殊处理。此时会打印一条消息,其中指明了僵尸对象所收到的消息及原来所属的类,然后应用程序就终止了。

    • 系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使改对象变为僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接收者的消息,然后终止应用程序。

    总结一下这个过程:

    1. object dealloc被调配到了新的方法,会在运行时生成一个_NSZombie_XXX的类,并且不会释放object的内存
    2. 将object的isa指针指向新的_NSZombie_XXX类
    3. 当object接收到消息以后,由于_NSZombie_XXX类并没有处理这个消息,消息就会进行转发,转发时会通过类名前缀看是不是僵尸类,并打印消息

    36. 不要使用retainCount

    1. 它所返回的保留计数只是某个给定时间点上的值。并未考虑到系统会稍后把自动释放池清空,因而不会将后续的释放操作从返回值里减去,所以此值就未必能真实反应实际的保留计数了。

    2. reatinCount可能永远不返回0,因为有时系统会优化对象的释放行为,在保留计数还是1的时候就把它回收了。

    3. 而且对于NSString和NSNumber而言其实retainCount没意义,他们都是单例,也解决了之前的为什么autorelease pool都不能释放掉NSString字面量的问题。

    ※ NSString与NSNumber

    NSString *string = @"Some string";
    NSLog(@"string retainCount = %lu",[string retainCount]);
    
    NSNumber *numberI = @1;
    NSLog(@"numberI retainCount = %lu",[numberI retainCount]);
    
    NSNumber *numberF = @3.14f;
    NSLog(@"numberF retainCount = %lu",[numberF retainCount]);
    
    运行结果:
    string retainCount = 18446744073709551615
    numberI retainCount = 9223372036854775807
    numberF retainCount = 1
    

    第一个对象的保留计数是2的64次方减1,第二个是2的63次方减一。由于二者都是单例对象,所以其保留计数都很大。系统会尽可能把NSString实现成单例对象,NSNumber也类似。

    如果字符串像本例所举的这样,是个编译常量(compile-time constant),那么就可以这样来实现了。在这用情况下,编译器会把NSString对象所表示的数据放到应用程序的二进制文件里,这样的话,运行程序时就可以直接用了,无须再创建NSString对象。

    NSNumber也类似,它使用了一种叫做“标签指针”(tagged pointer)的概念来标注特定类型的数值。这种做法不使用NSNumber对象,而是把与数值有关的全部消息都放在指针值里面。运行期系统会在消息派发(参见第11条)期间检测到这种标签指针,并对它执行相应操作,使其行为看上去和真正的NSNumber对象一样。这种优化只在某些场合使用,比如范例中的浮点数对象就没有优化,所以其保留计数就是1。

    对于单例对象来说,保留计数永远不会变,保留及释放都是空操作。而且两个单例之间的计数其实也不会一致。

    相关文章

      网友评论

        本文标题:[iOS] Effective Objective-C ——内存

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