《Effective Objective-C 2.0》读书笔记---第五章

前言


ARC几乎把所有内存管理事宜都交由编译器来决定,开发者只需专注于业务逻辑。


第29条:理解引用计数


引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1.若保留计数为正,则对象继续存活。当保留计数降为0时,对象就被销毁了。


在对象声明周期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。


第30条:以ARC简化引用计数


Clang编译器项目带有一个“静态分析器”(static analyzer)用于指明程序里引用计数出问题的地方。那么既然可以查明内存管理问题,应该也可以根据需要,预先加入适当的保留或释放操作以避免这些问题。自动引用计数这一思路正是源于此。自动引用计数所做的事情与其名称相符,就是自动管理引用计数。


使用ARC时一定要注意,引用计数实际上还是要执行的,只不过保留与释放操作现在是由ARC自动为你添加。


因为ARC会通过调用其底层c语言版本的retain,release,autorelease操作,这样跳过OC消息派发,性能更好,因此你不能再调用这些方法以及dealloc方法来管理内存(编译器会直接报错),以免对ARC造成干扰。


MRC下,new/alloc/copy/mutablecopy开头的创建方法,其返回对象归调用者所有,也就是调用者要负责释放对象,其他的方法使用autorelease交由自动释放池延迟释放。

ARC下,new/alloc/copy/mutablecopy开头的创建方法,编译器会直接返回,其他的编译器会帮忙autorelease


除了会自动调用“保留”与“释放”方法外,使用ARC还可以执行一些手工操作很难甚至无法完成的优化,例如,在编译期,ARC会把能够互相抵消的retain、release、autorelease操作约简。如果发现在同一个对象上执行了多次“保留”与“释放”操作,那么ARC有时可以成对地移除这两个操作。这里参考《关于__autoreleasing,你真的懂了吗?》有更详细的讲解。


变量的内存管理语义

__strong:默认语义,保留此值

__weak:不保留此值

__unsafe_unretained:参考《理解__unsafe_unretained》

__autoreleasing:参考《关于__autoreleasing,你真的懂了吗?》


ARC会借用OC++的一项特性来生成清理例程(cleanup routine)。回收OC++对象时,待回收的对象会调用所有C++对象的析构函数(destructor)。编译器如果发现某个对象里面含有C++对象,就会生成名为.cxx_destruct的方法。而ARC则借助此特性,在该方法中生成清理内存所需的代码以及执行调用超类的dealloc方法。因此OC对象实例不用我们手动在dealloc方法中释放了。


不过,如果有非OC得对象,比如CoreFoundation中的对象或是由malloc分配在堆汇总的内存,那么仍然需要自己清理。


第31条:在delloc方法中只释放引用并解除监听


在dealloc方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的“键值观察”(KVO)或NSNotificationCenter等通知,不要做其他事情。如下:


- (void)dealloc {    CFRelease(coreFoundationObject);    [[NSNotificationCenter defaultCenter] removeObserver:self];}


如果对象持有文件描述符,套接字,大块内存等系统资源,那么应该专门编写一个方法来释放此种资源,因为不是所有的对象在程序的生命周期都会释放,这样的类要和其使用者约定:用完资源后必须调用close方法,这样资源对象的生命周期就变得更加明确了。


如果对象管理者某些资源,那么在dealloc中也要调用“清理方法”,以防开发者忘记清理这些资源。

close和dealloc正确实现如下:

- (void)close {    /* clean up resources */    _closed = YES;}- (void)dealloc {    if (!_closed) {        NSLog(@"ERROR: close was not called before dealloc!");        [self close];    }}


执行异步任务的方法不应该在delloc中调用,只能在正常状态下执行的那些方法也不应该在dealloc里调用,因为此时对象已处于正在回收的状态了。


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


以下语句在try中发生异常时不会使程序终止。

@try {    EOCSomeClass *object = [[EOCSomeClass alloc] init];    [object doSomethingThatMayThrow];    [object release];}@catch (...) {    NSLog(@"Whoops, there was an error. Oh well...");}


如果doSomethingThatMayThrow发生异常,则执行终止并跳转到@catch中,release就不会执行,就会发生内存泄漏,解决方法如下:

EOCSomeClass *object;@try {    object = [[EOCSomeClass alloc] init];    [object doSomethingThatMayThrow];}@catch (...) {    NSLog(@"Whoops, there was an error. Oh well...");}@finally {    [object release];}


ARC下,由于不能调用release,object就真的会内存泄漏,ARC并不会自动处理。


-fobjc-arc-exception这个编译器标志用来开启自动处理的功能,开启以后,ARC能生成这种安全处理异常所用的附加代码,即跟踪待清理的对象,从而在抛出异常时将其释放。


这样做的坏处是严重影响运行期的性能,即便在不抛出异常时也是如此。而且添加进来的额外代码还会明显增加应用程序的大小。


有种情况编译器会自动把-fobjc-arc-exception打开,就是处于OC++模式时,因为C++处理异常所用的代码与ARC实现的附加代码类似,所以令ARC加入自己的代码以安全处理异常,其性能损失并不太大。


第33条:以弱引用避免保留环


扩展:

//MyObjectA

@class MyObjectB;

@interface MyObjectA : NSObject

@property (nonatomic, strong) MyObjectB *objectB;

@end

@implementation MyObjectA

- (void)dealloc
{
    NSLog(@"%s",__func__);
}

@end

//MyObjectB

@class MyObjectA;

@interface MyObjectB : NSObject

@property (nonatomic, strong) MyObjectA *objectA;

@end

@implementation MyObjectB

- (void)dealloc
{
    NSLog(@"%s",__func__);
}

@end


//执行

    MyObjectA *objectA = [[MyObjectA alloc]init];
    MyObjectB *objectB = [[MyObjectB alloc]init];
    
    NSLog(@"objectA = %ld",_objc_rootRetainCount(objectA));
    NSLog(@"objectB = %ld",_objc_rootRetainCount(objectB));
    
    objectA.objectB = objectB;
    objectB.objectA = objectA;
    
    NSLog(@"objectA = %ld",_objc_rootRetainCount(objectA));
    NSLog(@"objectB = %ld",_objc_rootRetainCount(objectB));
    
    objectA = nil;
    objectB = nil;

打印信息

objectA = 1

objectB = 1

objectA = 2

objectB = 2


objectA置空时,只是引用计数减1,因为还有objectB强引用这它,因此它不会释放,

objectB置空时,也只是引用计数减1,因为还有objectA(之前没能释放掉)强引用这它,因此它不会释放,


这就是强引用环objectA和objectB最后都没有被释放掉,并发生内存泄漏,这两个指针已经无效了。解决方法就是将其中一个语义改为weak,如下:

@property (nonatomic, weak) MyObjectB *objectB;


打印信息如下:

objectA = 1

objectB = 1

objectA = 2

objectB = 1

-[MyObjectB dealloc]

-[MyObjectA dealloc]


第34条:以“自动释放池块”降低内存峰值


参考《理解自动释放池》


第35条:用“僵尸对象”调试内存管理问题


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


开启方式:编辑应用程序的Scheme,在对话框左侧选“Run”,然后切换到“Diagnostics”分页。然后勾选“Enable Zombie Objects”选项。


僵尸对象的工作原理是什么?它的实现代码深植于Objective-C的运行期程序库、Foundation框架及CoreFoundation框架中。系统在即将回收对象时,如果发现通过环境变量启用了僵尸对象功能,那么还将执行一个附加步骤。这一步骤就是把对象转化为僵尸对象,而不彻底回收。


#import <Foundation/Foundation.h>

#import <objc/runtime.h>

@interface EOCClass : NSObject

@end

@implementation EOCClass

@end

void PrintClassInfo(id obj) 

   Class cls = object_getClass(obj); 

   Class superCls = class_getSuperclass(cls);

    NSLog(@"=== %s : %s ===",          class_getName(cls), class_getName(superCls));

}

int main(int argc, char *argv[]) 

{

    EOCClass *obj = [[EOCClass allocinit];

    NSLog(@"Before release:");

    PrintClassInfo(obj);    [obj release];

    NSLog(@"After release:"); 

    PrintClassInfo(obj);

    NSString *desc = [obj description];

}


打印信息如下:

Before release:

=== EOCClass : NSObject ===

After release:

=== _NSZombie_EOCClass : nil ===

*** -[EOCClass description]: message sent to deallocatedinstance 0x7fc821c02a00


问题1:为什么PrintClassInfo函数能正常运行,而description会引发崩溃。

问题2:_NSZombie_EOCClass哪里来的。

问题3:释放以后EOCClass的父类怎么为nil了


解答:

运行期系统如果发现NSZombieEnabled环境变量已设置,就会首先创建_NSZombie_模板类,并把delloc方法”调配“(swizzle)成下面的版本:

// Obtain the class of the object being deallocated

Class cls = object_getClass(self);

// Get the class's name

const char *clsName = class_getName(cls);

// Prepend _NSZombie_ to the class name

const char *zombieClsName = "_NSZombie_" + clsName;

// See if the specific zombie class exists

Class zombieCls = objc_lookUpClass(zombieClsName);

// If the specific zombie class doesn't exist,

// then it needs to be created

if (!zombieCls) 

{

    // Obtain the template zombie class called _NSZombie_

    Class baseZombieCls = objc_lookUpClass("_NSZombie_");

    // Duplicate the base zombie class, where the new class's

    // name is the prepended string from above

    zombieCls = objc_duplicateClass(baseZombieCls,                                    zombieClsName, 0);

}

// Perform normal destruction of the object being deallocated

objc_destructInstance(self);

// Set the class of the object being deallocated

// to the zombie class

objc_setClass(self, zombieCls);

// The class of 'self' is now _NSZombie_OriginalClass


在类列表中查找有没有"_NSZombie_" + clsName这个类,如果没有,则找出_NSZombie_类,并将_NSZombie_复制到以"_NSZombie_" + clsName命名的类,然后销毁当前对象,但是并不执行free(),也就是当前对象的内存暂不被系统回收,最后将之前复制出来的以"_NSZombie_" + clsName命名的僵尸对象赋值到当前对象。


也就是说dealloc执行完以后,当前待回收的对象就变成了名为"_NSZombie_" + clsName的僵尸对象了,(解答问题2)


_NSZombie_类和NSObject一样,也是根类,(解答问题3)该类只有一个实例变量,叫做isa,所有NSObjective-C的根类都必须有此变量。由于这个轻量级的类没有实现任何方法,所以发给它的全部消息都要经过“完整的消息转发机制”。


在完整的消息转发机制中,__forwarding__是核心,调试程序时,大家可能在栈回溯消息里看见过这个函数。它首先要做的事情就包括检查接收消息的对象所属的类名。若名称前缀为NSZombie,则表明消息接收者是僵尸对象,需要特殊处理。此时会打印一条消息,其中指明了僵尸对象所受到的消息及原来所属的类,然后应用程序就终止了。伪代码如下:(解答问题1)


// Obtain the object's class

Class cls = object_getClass(self);

// Get the class's name

const char *clsName = class_getName(cls);

// Check if the class is prefixed with _NSZombie_

if (string_has_prefix(clsName, "_NSZombie_")

 {

    // If so, this object is a zombie

    // Get the original class name by skipping past the

    // _NSZombie_, i.e. taking the substring from character 10

    const char *originalClsName = substring_from(clsName, 10

);

    // Get the selector name of the message

    const char *selectorName = sel_getName(_cmd); 

   // Log a message to indicate which selector is

    // being sent to which zombie

    Log("*** -[%s %s]: message sent to deallocated instance %p",        originalClsName, selectorName, self); 

   // Kill the application

    abort();

}


第36条:不要使用retainCount


虽然ARC下面这个方法已经被废弃了,但是还是小提一下在MRC下面的使用,书的作者说不要使用它,因为它的值不准确,其实我觉得,只要清楚了底层的一些原理,有些情况下这个方法返回的值还是准确的。并不是完全不能使用。当然只是调试使用。

NSString *string = @"Some string";
NSLog(@"string retainCount = %lu", [string retainCount]);
NSNumber *numberI = @1;
NSLog(@"numberI retainCount = %lu", [number retainCount]);
NSNumber *numberF = @3.141f;
NSLog(@"numberF retainCount = %lu", [numberFloat retainCount]);

打印信息:

string retainCount = 18446744073709551615

numberI retainCount = 9223372036854775807

numberF retainCount = 1


原因:前两个值很明显是错的,因为它们都是单例对象,系统会尽可能把NSStirng实现成单例对象。如果字符串像本例所举的这样,是个编译常量(compile-time constant),那么就可以这样来实现了。在这用情况下,编译器会把NSString对象所表示的数据放到应用程序的二进制文件里,这样的话,运行程序时就可以直接用了,无须再创建NSString对象。NSNumber也类似,它使用了一种叫做“标签指针”(tagged pointer)的概念来标注特定类型的数值。这种做法不使用NSNumber对象,而是把与数值有关的全部消息都放在指针值里面。运行期系统会在消息派发(参见第11条)期间检测到这种标签指针,并对它执行相应操作,使其行为看上去和真正的NSNumber对象一样。这种优化只在某些场合使用,比如范例中的浮点数对象就没有优化,所以其保留计数就是1。


这里参考《采用Tagged Pointer的字符串》


猜你喜欢

转载自blog.csdn.net/junjun150013652/article/details/53839369