iOS

《Objective-C高级编程》三篇总结之一:引用计数篇

2019-11-29  本文已影响0人  四月_Hsu

手动内存管理

在 Xcode4.2 版本以后,自动引用计数 ARC 已经是默认有效了。但是这里还是先分析一下手动内存管理 MRC,方便我们对 iOS 开发的内存管理有更清晰的认识。

参考:《Objective-C 高级编程》干货三部曲(一):引用计数篇

Objective-C高级编程.jpg

写在前面:

  1. NSObject 已经开源,所以 alloc/retain/release/dealloc 的真实实现方案,也不用像书籍作者说的那样参考 GNUstep 源码去推测:
  1. 可通过下面两个方法查看汇编输出:
    参考:iOS 获取汇编输出方法
  1. 或者查看文件转成 C++ 后的源码。在文件目录下执行命令行:
$ clang -rewrite-objc MyClass.m

然后在同一目录下会多出一个 MyClass.cpp 文件,双击打开即可。

内存管理的思考方式

内存管理更加客观、正确的思考方式是:

iOS 内存管理经常用到的词有:生成持有释放销毁。如下:

对象操作 Objective-C 方法 引用计数变化
生成并持有对象 alloc/new/copy/mutableCopy 等方法 +1
持有对象 retain方法 +1
释放对象 release方法 -1
废气对象 dealloc方法

借用书中图,直观感受如下:

引用计数的内存管理.png

下面分条列举说明。

自己生成的对象,自己所持有

使用以下名称开头的方法名,意味着自己生成的方法,只有自己持有:

根据上述 使用以下名称开头的方法名,下列名称也意味着自己生成并持有对象:

但是对于以下名称,即使使用了 alloc/new/copy/mutableCopy 开头,也并不属于同一类别的方法:

这里用 驼峰(CameCase 命名法来区分。

非自己生成的对象,自己也能持有

在 alloc/new/copy/mutableCopy 方法以外取得的对象,默认并不持有对象,可以通过 retain 来持有对象:

/// 取得非自己生成并持有的对象。此时,obj 取得对象的存在,但自己并不持有
id obj = [NSMutableArray array];
/// 自己持有对象
[obj retain];

不再需要自己持有的对象时释放

自己持有的对象,一旦不需要,持有者有义务释放该对象。释放使用 release 方法。

/// 自己生成并持有对象
id obj = [[NSObject alloc] init];
/// 释放对象。指向该对象的指针仍然保留在 obj 中,貌似可以访问。
/// 但对象一经释放,决不可访问,否则会发生崩溃。
[obj release];

通过 retain 持有非自己生成的对象时,也需要使用 release 释放:

/// 生成
id obj = [NSMutableArray array];
/// 持有
[obj retain];
/// 释放
[obj release];

生成并持有对象,方法实现如下,注意 allocObject 符合前面生成并持有对象的命名规范:

-(id)allocObject {
    /// 生成并持有对象
    id obj = [[NSObject alloc] init];
    /// 让自己持有对象
    return obj;
}
/// 此时,不用调用 retain。obj1 已经持有该对象
id obj1 = [obj0 allocObject];

生成对象,默认不持有,需要手动持有,实现如下:

-(id)object {
    /// 生成并持有对象
    id obj = [[NSObject alloc] init];
    /// 取得对象的存在。但是放弃对对象的持有。即加入自动释放池。
    [obj autorelease];
    /// 此时,obj 并不能持有该对象了
    return obj;
}

/// 获取对象,并不持有
id obj1 = [obj0 object];
/// 持有对象。即引用计数 +1
[obj1 retain];

使用 autorelease 方法,可以使取得对象的存在,但是自己不持有对象。 autorelease 提供这样的功能:使对象在超出指定的生存范围时能够自动并正确的释放(调用 release 方法)。如下图所示:

release与autorelease区别.png

无法释放非自己持有的对象

对于用 alloc/new/copy/mutableCopy 生成并持有的对象,或者是 retain 持有的对象,由于持有者是自己,所以在不需要该对象时自己需要将其释放。而由此之外得到的对象绝对不能释放,若在程序中释放了非自己持有的对象,会引发崩溃。例如过度释放对象:

/// 自己生成并持有对象
id obj = [[NSObject alloc] init];
/// 释放对象
[obj release];
/// 对象已经释放后再次释放
[obj release];
/// 此时,程序崩溃。
/// 崩溃分析:对象已废弃,访问废弃对象时崩溃。野指针

或者在 取得的对象已存在,但自己不持有该对象 时释放,也会引发崩溃:

id obj = [NSMutableArray array];
[obj release];
///释放非自己持有的对象,崩溃

alloc/retain/release/dealloc 实现

NSObject 源码并没有公开(已开源),这里参考开源的 GNUstep 源码来推测 NSObject 内部的实现细节。主要是要从实现的角度来理解内存管理的方式。为了明确重点,部分引用做了修改。

alloc 方法

id obj = [NSObject alloc];

调用该方法,源码实现如下:

/// GNUstep/modules/core/base/Source/NSObject.m alloc
+(id) alloc {
    return [self allocWithZone: NSDefaultMallocZone()];
}

+(id)allocWithZone: (NSZone *)z {
    return NSAllocateObject(self, 0, z);
}

通过 allocWithZone 类方法调用 NSAllocateObject 函数分配了对象,下面看下 NSAllocateObject 函数:

/// GNUstep/modules/core/base/Source/NSObject.m NSAllocateObject
struct obj_layout {
    NSUInteger retained;
};
inline id
NSAllocateObject(Class aClass, NSUInteger extraBytes, NSZone *zone) {
    int size = 计算容纳对象所需要的内存大小
    id new = NSZoneMalloc(zone, size);
    memset(new, 0, size);
    new = (id) & ((struct obj_layout *) new)[1];
}

NSAllocateObject 函数通过调用 NSZoneMalloc 函数来分配存放对象所需要的内存空间。之后将该空间置为 0,最后返回作为对象而使用的指针。

NSZone 是为防止内存碎片化而引入的数据结构。目前该接口效率低,且使源代码更加复杂。

下面是去掉 NSZone 之后简化的源代码:

struct obj_layout {
    NSUInteger retained;
};
+(id) alloc {
    int size = sizeof(struct obj_layout) + 对象大小;
    struct obj_layout *p = (struct obj_layout *)calloc(1, size);
    return (id)(p + 1);
}

alloc 类方法使用 struct obj_layout 中的 retained 整数来保存引用计数,并将其写入对象内存头部,该对象内存块全部置为 0 后返回。下图为 GNUstep 实现 alloc 类方法返回对象示意图:

alloc返回对象示意图.png

对象的引用计数可以通过 retainCount 实例方法获取。源码实现如下:

/// GNUstep/modules/core/base/Source/NSObject.m  retainCount
-(NSUInteger) retainCount {
    return NSExtraRefCount(self) + 1;
}
inline NSUInteger
NSExtraRefCount(id anObject) {
    return ((struct obj_layout *)anObject)[-1].retained;
}

可以看到,给NSExtraRefCount传入anObject以后,通过访问对象内存头部的.retained变量,来获取引用计数。

retain 方法

/// GNUstep/modules/core/base/Source/NSObject.m  retain
-(id) retain {
    NSIncrementExtraRefCount(self);
    return self;
}
inline void
NSIncrementExtraRefCount(id anObject) {
    if (((struct obj_layout *)anObject)[-1].retained == UINT_MAX - 1)
        [NSException raise: NSInternalInconsistencyException format: @"NSIncrementExtraRefCount() asked to increment too far"];
    ((struct obj_layout *)anObject)[-1].retained++;
}

可以看出,如果已有的引用计数过大,会执行异常代码。正常情况下,只运行了使 retained 变量加 1 的 retained++ 代码。

release 方法

/// GNUstep/modules/core/base/Source/NSObject.m release
-(void) release {
    if (NSDecrementExtraRefCountWasZero(self))
        [self dealloc];
}

BOOL
NSDecrementExtraRefCountWasZero(id anObject) {
    if (((struct obj_layout *)anObject)[-1].retained == 0) {
        return YES;
    } else {
        ((struct obj_layout*)anObject)[-1].retained--;
        return NO;
    }
}

当 retained 变量大于 0 时减 1,等于 0 时调用 dealloc 实例方法,废弃对象。

dealloc 方法

/// GNUstep/modules/core/base/Source/NSObject.m dealloc
-(void) dealloc {
    NSDeallocateObject(self);
}
inline void
NSDeallocateObject(id anObject) {
    struct obj_layout *o = &((struct obj_layout *)anObject)[-1];
    free(o);
}

上述代码废弃由 alloc 分配的内存块。

以上便是 GNUstep 中 alloc/retain/release/dealloc 的实现,具体总结如下:

好了,下面看一下苹果的实现吧。

苹果的实现

因为 NSObject 源码并没有公开(已开源),这里利用 Xcode 的调试器 lldb 和 iOS 大概追溯其实现过程。在 NSObject 类的 alloc 类方法上设置断点,追踪程序的执行。以下列出执行调用的方法和函数:

上面频繁出现的 __CFDoExternRefOperation 是开源代码 CFRuntime.c 的 __CFDoExternRefOperation 函数,
源码如下:

CF_EXPORT uintptr_t __CFDoExternRefOperation(uintptr_t op, id obj) {
    if (nil == obj) HALT;
    uintptr_t idx = EXTERN_TABLE_IDX(obj);
    uintptr_t disguised = DISGUISE(obj);
    CFSpinLock_t *lock = &__NSRetainCounters[idx].lock;
    CFBasicHashRef table = __NSRetainCounters[idx].table;
    uintptr_t count;
    switch (op) {
    case 300:   // increment
    case 350:   // increment, no event
        __CFSpinLock(lock);
    CFBasicHashAddValue(table, disguised, disguised);
        __CFSpinUnlock(lock);
        if (__CFOASafe && op != 350) __CFRecordAllocationEvent(__kCFObjectRetainedEvent, obj, 0, 0, NULL);
        return (uintptr_t)obj;
    case 400:   // decrement
        if (__CFOASafe) __CFRecordAllocationEvent(__kCFObjectReleasedEvent, obj, 0, 0, NULL);
    case 450:   // decrement, no event
        __CFSpinLock(lock);
        count = (uintptr_t)CFBasicHashRemoveValue(table, disguised);
        __CFSpinUnlock(lock);
        return 0 == count;
    case 500:
        __CFSpinLock(lock);
        count = (uintptr_t)CFBasicHashGetCountOfKey(table, disguised);
        __CFSpinUnlock(lock);
        return count;
    }
    return 0;
}

下面是其简化后的代码实现:

/// CF/CFRuntime.c __CFDoExternRefOperation
int ____CFDoExternRefOperation(uintptr_t op, id obj) {
    CFBasicHashRef table = 取得对象的散列表(obj);
    int count;
    switch(op) {
        case Operation_retainCount:
            count = CFBasicHashGetCountOfKey(table, obj);
            return count;
        case Operation_retain:
            count = CFBasicHashAddValue(table, obj);
            return count;
        case Operation_release:
            count = CFBadicHashRemoveValue(table, obj);
            return 0 == count;
    }
}

__CFDoExternRefOperation 函数按照 retainCount/retain/release 操作进行分发,调用不同的函数。猜测, NSObject 类的 retainCount/retain/release 实例方法也许如下面代码表示:

-(NSUInteger) retainCount {
    return (NSUInteter) __CFDoExternRefOperation(Operation_retainCount, self);
}

-(id)retain {
    return (id)__CFDoExternRefOperation(Operation_retain, self);
}

-(void) release {
    return __CFExternRefOperation(Operation_release, self);
}

从 __CFDoExternRefOperation 函数以及由此函数调用的各个函数名看出,苹果的实现大概是采用散列表(引用技数表)来管理引用计数的,如下图所示:

散列计数表管理引用计数.png

GNUstep 将引用计数保存到对象内存块头部的变量中,好处如下:

苹果很可能采用引用计数表管理引用计数,这样的好处是:

尤其是第二,在调试时有着举足轻重的作用,即使出现故障导致对象占用的内存损坏,只要引用计数表没坏,就能够确认各内存块地址。

NSObject.mm 源码实现

alloc
/// Objc源码/objc4-756.2/runtime/NSObject.mm
+ (id)alloc {
    return _objc_rootAlloc(self);
}

而 _objc_rootAlloc 实现为:

/// Objc源码/objc4-756.2/runtime/NSObject.mm
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

callAlloc 方法开始实现具体细节:

/// Objc源码/objc4-756.2/runtime/NSObject.mm
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate 
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        // No alloc/allocWithZone implementation. Go straight to the allocator.
        // fixme store hasCustomAWZ in the non-meta class and 
        // add it to canAllocFast's summary
        if (fastpath(cls->canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif

    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}

其实可以看到,和上面的猜测大致是一样的。这里也不再具体扩展,需要时可以从源码层面看下 release、dealloc 等的具体实现。

autorelease

autorelease 介绍

当 autorelease 管理的对象超出其作用域后,对象实例的 release 方法会被调用。autorelease 的具体使用方法如下:

NSAutoreleasePool的生存周期.png

对所有调用过 autorelease 实例方法的对象,在废弃 NSAutoreleasePool 时,都将主动调用 release 方法:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];   /// 等同于 [obj release];

程序并非一定要使用 NSAutoreleasePool 对象来工作。但是在大量产生 autorelease 的对象时,只要不废弃 NSAutoreleasePool 对象,即 RunLoop 不进入睡眠状态,那么生成的对象就不能被释放,因此有时会产生内存不足的现象。如大量循环中对图片做复杂操作。这种情况下,就会产生大量的 autorelease 对象,内存激增:

for (int i  = 0; i < 图片数; i++) {
    /*
    * 读入图像
    * 大量产生 autorelease 对象
    * 由于没有废弃 NSAutoreleasePool 对象,最终导致内存不足。
    */
}
大量产生autorelease对象.png

再次情况下,有必要在适当的地方生成、持有、废弃 NSAutoreleasePool 对象:

for (int i = 0; i < imageArray.count; i++) {
    // 临时 Pool
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    UIImage *image = imageArray[i];
    [image doSomething];
    [pool drain];
}
适当释放autorelease对象.png

可能会出的面试题:什么时候会创建自动释放池?
答:运行循环检测到事件并启动后,就会创建自动释放池,而且子线程的 runloop 默认是不工作的,无法主动创建,必须手动创建。
举个例子:
自定义的 NSOperation 类中的 main 方法里就必须添加自动释放池。否则在出了作用域以后,自动释放对象会因为没有自动释放池去处理自己而造成内存泄露。

GNUstep autorelease 实现

同上,先看一下 GNUstep 的源码实现:

/// GNUstep/modules/core/base/Source/NSObject.m autorelease
-(id) autorelease {
    [NSAutoreleasePool addObject: self];
}

autorelease 方法的本质就是调用 NSAutoreleasePool 对象的 addObject 类方法。下面是作者假想后简化的 NSAutoreleasePool 源代码实现:

/// GNUstep/modules/core/base/Source/NSAutoreleasePool.m addObject
+(void) addObjecty: (id)anObj {
    NSAutoreleasePool *pool = 取得正在使用的 NSAutoreleasePool 对象;
    if (pool != nil) {
        [pool addObject: anObj];
    } else {
        NSLog()
    }
}

-(void) addObject: (id)anObj {
    [array addObject: anObj];
}

addObject 类方法就是调用正在使用的 NSAutoreleasePool 对象的 addObject 实例方法,然后这个对象就被追加到正在使用的 NSAutoreleasePool 对象的数组中。

下面看一下使用 drain 实例方法废弃正在使用的 NSAutoreleasePool 对象的过程:

/// GNUstep/modules/core/base/Source/NSAutoreleasePool.m drain
-(void) drain {
    [self dealloc];
}

-(void) dealloc {
    [self emptyPool];
    [array release];
}

-(void) emptyPool {
    for (id obj in array) {
        [obj release];
    }
}

虽然调用了好几个方法,可以确定对于数组中的所有对象都调用了 release 实例方法。

苹果的实现

可通过 objc 库的 https://opensource.apple.com/source/objc4/objc4-750.1/runtime/NSObject.mm.auto.html 来确认苹果中 autorelease 的实现。

/// /objc4/objc4-750.1/runtime/NSObject.mm AutoreleasePoolPage

/// 这里的源码非常长。。。感兴趣的可以自己去看下。
class AutoreleasePoolPage {
}

下面还是结合书中总结的来做分析吧。核心方法是一样的:

class AutoreleasePoolPage {
    static inline voiod *push() {
        //  相当于生成或者持有 NSAutoreleasePool 类对象;
    }
    
    static inline void *pop(void *token) {
        // 相当于废弃 NSAutoreleasePool 类对象;
        releaseAll();
    }
    
    static inline id autorelease(id obj) {
        /*
        * 相当于 NSAutoreleasePool 类的 addObject 类方法;
        * AutoreleasePoolPage *page = 取得正在使用的 AutoreleasePoolPage 实例;
        * page -> add(obj);
        */
    }
    
    id *add(id obj) {
        // 将对象追加到内部数组中;
    }
    
    void releaseAll() {
        // 调用内部数组中对象的 release 方法
    }
};

/// 进栈
void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage :: push();
}

/// 出栈
void *objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage :: pop(ctxt)
}

/// 在内部释放
id *objc_autorelease(id obj) {
    return AutoreleasePoolPage :: autorelease(obj);
}

下面通过外部调用来对比分析:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// 等同于  objc_autoreleasePoolPush()

id obj = [[NSObject alloc] init];
[obj autorelease];
// 等同于 objc_autorelease(obj)

[NSAutoreleasePool showPools];
/// 非公开类方法。showPools 会将现在 NSAutoreleasePool 的状况输出到控制台

[pool drain];
// 等同于 objc_autoreleasePoolPop(pool)

可能出的面试题: 苹果如何实现 NSAutoreleasePool 的? 参考答案: NSAutorelease 以一个队列数组的形式实现,主要使用三个方法:objc_autoreleasePoolPush(进栈)、objc_autoreleasePoolPop(出栈)、objc_autorelease(释放内部)。

另外,如果 autorelease NSAutoreleasePool 对象,回引发崩溃。

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[pool autorelease];

因为对于 NSAutoreleasePool 来说,autorelease 已被重载。

ARC 自动引用计数

内存管理的思考方式

引用计数式内存管理,就是思考 ARC 所引起的变化。

可以看到,思想和 MRC 是一样的,区别主要是我们不需要在显式的调用,在源代码的记述方法上有所不同。

四种所有权修饰符

ARC 有效时,id 类型或者对象类型必须附加所有权修饰符。所有权修饰符一共有四种,如下:

下面挨个看一下吧。

__strong 修饰符

ARC 环境下,__strong 是属性的默认修饰符。

__strong 使用方法
id obj = [[NSObject alloc] init];

等同于:

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

__strong 修饰符表示对对象的“强引用”,该对象的持有状态如下:

{
    // 自己生成并持有对象。因为强引用,所以持有。
    id __strong obj = [[NSObject alloc] init];
}
/// obj 超出作用域,强引用失效。所以自动释放持有的对象。

对于非自己生成,并持有的对象,亦是如此:

{
    // 取得非自己生成并持有的对象
    id __strong obj = [NSMutableArray array];
}
/// 超出作用域,强引用失效

附有 __strong 修饰符的变量之间也可以相互赋值:

// 生成对象A。obj0 持有对象 A 的强引用
id __strong obj0 = [[NSObject alloc] init];  
// 生成对象B。obj1 持有对象 B 的强引用
id __strong obj1 = [[NSObject alloc] init];
// obj2 不持有任何对象
id __strong obj2 = nil;
// obj0 持有对象 B 的强引用。此时对象 A 因为不再被强引用,被废弃。
// 此时对象 B 被变量 obj0 和 obj1 共同持有。
obj0 = obj1;
// obj2 持有对象 B 的强引用。
// 此时对象 B 被变量 obj0、obj1、obj2 持有。
obj2 = obj0;
// obj1 不再强引用对象 B
obj1 = nil;
// obj0 不再强引用对象 B
obj0 = nil;
// obj2 不再强引用对象 B
obj2 = nil;
// 此时,对象 B 不再被任何变量强引用,被废弃。

也可以给类的成员变量或方法属性加上 __strong 修饰符:

@interface Test: NSObject {
    id __strong obj_;
}

-(void)setObject: (id __strong)obj;

因为 id 类型和对象类型的所有权修饰符默认为 __strong 修饰符,所以通常不需要写上 __strong。

__strong 实现
{
    id __strong obj = [[NSObject alloc] init];
}

通过 clang 获取程序汇编输出,或者 cpp 文件,结合 objc4 库源码,可以分析程序执行的流程。

其实这部分都是模拟代码,通过总结分析得出的结论。深入解构objc_msgSend函数的实现 这篇文章作者通过把汇编语言转换成 C 语言来具体分析 Objective-C 的消息转发机制,分析的很深入。

/// 编译器模拟代码
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_release(obj);

作为对比,看一下转化 C++ 后的执行:

id obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));

可以看出,即使 ARC 不支持 release,实际上编辑器还是自动插入了 release。通过 objc_msgSend 来传递消息。

而使用 alloc/new/copy/mutableCopy 以外方法生成的对象,又有一些不一样:

{
    id __strong obj = [NSMutableArray array];
}

编译器的模拟代码:

id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleaseReturnValue(obj);
objc_release(obj)

objc_retainAutoreleaseReturnValue 函数主要用于程序最优化执行。该函数持有的对象应该是:注册到 autoreleasePool 中对象的方法,或者函数的返回值。

这种 objc_retainAutoreleaseReturnValue 函数是成对的,与之相对的函数是: objc_autoreleaseReturnValue。来看一下它的使用:

+(id)array {
    return [[NSMutableArray alloc] init];
}

编译器的模拟代码:

+(id)array {
    id obj = objc_msgSend(NSMutableArray, @selector(alloc));
    objc_msgSend(obj, @selector(init));
    return objc_autoreleaseReturnValue(obj);
}

objc_autoreleaseReturnValue: 返回注册到 autoreleasePool 的对象。

书中说,objc_retainAutoreleaseReturnValue 和 objc_autoreleaseReturnValue 方法配合使用可以不将对象注册到 autoreleasePool中,如下图:

省略autoreleasePool注册.png

__weak 修饰符

当带有 __strong 修饰符的变量在持有对象时,如果多个对象相互持有,很容易发生循环引用。

循环引用容易发生内存泄漏。所谓内存泄漏就是应当被废弃的对象在超出其生存周期后继续存在。

__weak 用法
@interface Test: NSObject {
    id __strong obj_;
}
-(void)setObject:(id __strong)obj;
@end

@implementation Test
-(id)init {
    self = [super init];
    return self;
}

-(void)setObject:(id __strong)obj {
    obj_ = obj;
}

以下为循环引用:

{
    id test0 = [[Test alloc] init];     // test0 强引用对象 A
    id test1 = [[Test alloc] init];     // test1 强引用对象 B
    [test0 setObject: test1];           // test0 强引用对象 B
    [test1 setObject: test0];           // test1 强引用对象 A
    
}

或者,对象持有自身时,也会发生循环引用:

{
    id test = [[Test alloc] init];
    [test setObject: test];
}
循环引用示意图.png

使用 __weak 修饰符可以避免循环使用。通过检查附有 __weak 修饰符的变量是否为 nil,可以判断被赋值的对象是否已经被废弃。

__weak 的另一个优点就是,在持有某对象的弱引用时,若该对象被废弃,则此弱引用自动失效且被置为 nil 状态。

通过下面代码看下 __weak 的特性:

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

改代码会出现编译警告,因为 __weak 并不直接持有对象,所以 obj 会被立即释放掉。修改如下即可:

id __strong obj0 = [[NSObject alloc] init];
id __weak obj1 = obj0;

所以,针对上面循环引用的问题,作出修改,即可避免循环引用了:

@interface Test: NSObject {
    id __weak obj_;
}
-(void)setObject:(id __strong)obj;
@end
弱引用避免循环引用.png
__weak 实现

通过前面说明,可以看到 __weak 有如下魔法般的效果:

首先,通过研究 __weak 内部实现,再来看下上面提出的的一个问题:

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

编译器处理该源码时,模拟如下:

/// 编译器的模拟代码
id obj;
id tmp = objc_msgSend(NSObjct, @selector(alloc));
objc_msgSend(tmp, @selector(init));
objc_initWeak(&obj, tmp);
objc_release(tmp);
objc_destroyWeak(&obj);

假如 [[NSObject alloc] init] 生成了对象 A,因为 __weak 不能持有对象,编译器认为对象 A 没有持有者,就通过 objc_release(tmp); 函数释放和废弃对象 A,所以赋值失败,编译警告。

下面,再来通过合理使用 __weak 分析其内部的实现:

/// 假设 obj 附加了 __strong 修饰符且对象被赋值
{
    id __weak obj1 = obj;
}

该源代码可转换为如下形式:

/// 编译器的模拟代码
id obj1;
objc_initWeak(&obj1, obj);
id temp = objc_loadWeakRetained(&obj1);
objc_autorelease(temp);
objc_destroyWeak(&obj1);

大量的使用 __weak 修饰符修饰的变量,注册到 autoreleasepool 的对象也会大量增加。因此使用附有 __weak 修饰符的变量时,最好先暂时赋值给附有 __strong 修饰符的变量后在使用。如下:

{
    id __weak 0 = obj;
    NSLog(@"1 %@", o);      // o 注册到 autoreleasepool 1 次
    NSLog(@"2 %@", o);      // o 注册到 autoreleasepool 2 次
    NSLog(@"3 %@", o);      // o 注册到 autoreleasepool 3 次
    NSLog(@"4 %@", o);      // o 注册到 autoreleasepool 4 次
}

即每次使用,都会注册到 autoreleasepool 中。如果先赋值给 __strong 变量:

{
    id __weak 0 = obj;
    id __strong tmp = o;    // o 注册到 autoreleasepool 1 次
    NSLog(@"1 %@", tmp);      
    NSLog(@"2 %@", tmp);      
    NSLog(@"3 %@", tmp);    
    NSLog(@"4 %@", tmp);    
}

书中原话是: 在 “tmp = o;” 时对象仅登录到 autoreleasepool 中 1 次。

这里再看下 Swift 中常用的写法,大概也是这个原因:

loginVC.loginSuccess = { [weak self] phoneNum in
    guard let strongSelf = self else {
        return
    }
}

这里重点说两个方法:

简单来说,就是 objc_storeWeak(&obj1, obj) 通过第二个参数决定是注册变量地址到 weak 表,还是删除地址。

通常面试到 weak 问题时,都会问下 weak 的实现原理,主要问的就是对 weak 表的理解。

weak 表与引用计数表相同,作为散列表被实现。如果使用 weak 表,将废弃对象的地址作为键值进行检索,就能高速获取对应的附有 __weak 修饰符的变量的地址。另外,由于一个对象可以赋值给多个附有 __weak 修饰符的变量,所以一个键值,可注册多个变量的地址。

当对象被释放时,执行流程是:

由上可知,大量使用 __weak 修饰符的变量,会消耗相应的 CPU 资源。我们只在需要避免循环吗引用时使用 __weak 修饰符即可。

另外,实际上存在着不支持 __weak 修饰符的类。但是这种类极为罕见。如 NSMachPortallowsWeakReferenceretainWeakReference 等。知道就好了。

_unsafe_unretained 修饰符

在 iOS4 以及 OSX Snow Leopard 的应用程序中代替 __weak 修饰符的。附有 _unsafe_unretained 修饰符的变量不属于编译器的内存管理对象。

这里有了解即可,不在细说。

__autoreleasing 修饰符

__autoreleasing 使用方法

在 ARC 有效时,用 @autoreleasepool 块代替 MRC 下 NSAutoreleasePool 类,用附有 __autoreleasing 修饰符的变量代替 MRC 下 autorelease 方法,即对象被注册到 autoreleasepool。

arc-autorelease.png

通常,我们并不需要显式的调用 __autoreleasing 修饰符。

在访问附有 __autoreleasing 修饰符的变量时,实际上必定要访问注册到 autoreleasepool 的对象。

id obj0 = [[NSObject alloc] init];
id __weak obj1 = obj0;
NSLog(@"class = %@", [obj1 class]);     // NSObject

以下源代码与其相同:

id obj0 = [[NSObject alloc] init];
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class = %@", [tmp class]);     // NSObject

为什么在访问附有 __autoreleasing 修饰符的变量时,必须要访问注册到 autoreleasepool 的对象呢?这是因为 __weak 修饰符只持有对象的弱引用,而在访问引用对象的过程中,该对象有可能被废弃,如果把要访问的对象注册到 autoreleasepool 中,那么在 @autoreleasepool 块结束之前都能确保该对象存在。因此,使用 __weak 修饰符的变量就要访问注册到 autoreleasepool 中的对象。

上面这段话是书中原话,但是结合上文 __weak 的实现原理,感觉这么描述不是很对。应该说,附有 __weak 修饰符的变量持有的对象,如果该对象不在 autoreleasepool 中,则编译器会将该对象注册到 autoreleasepool 中,并提供给该变量使用。这个只是个人理解,作为参考。

__autoreleasing 内部实现

将对象赋值给附有 __autoreleasing 修饰符的变量,等同于 ARC 无效时调用对象的 autorelease 方法。

@autoreleasepool {
    id __autoreleasing obj = [[NSObject alloc] init];
}

该源码主要将 NSObject 类对象注册到 autoreleasepool 中,可作如下转换:

/// 编译器的模拟代码
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);

这里能够看到 pool 入栈、执行autorelease、出栈 三个方法。在 MRC 下有过详细说明。

在 alloc/new/copy/mutableCopy 方法群之外的方法中使用注册到 autoreleasepool 中的对象,会有一些区别:

@autoreleasepool {
    id __autoreleasing obj = [NSMutableArray array];
}

编译器模拟代码如下:

id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleaseReturnValue(obj);
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);

注册到 autoreleasepool 的方法 objc_autorelease 并没有变。

引用计数

参考:iOS ARC下获取引用计数(retain count)

在 ARC 有效时,是无法正常查看一个类当前的引用计数的。不过,可以通过下面三个方法来获取到:

  1. 使用 KVC
  2. 使用私有 API
  3. 使用CFGetRetainCount

实践如下:

    id obj = [[NSObject alloc] init];
    id __weak obj1 = obj;
    // KVC
    NSLog(@"count10 = %@",[obj valueForKey:@"retainCount"]);      // 1
    NSLog(@"count11 = %@",[obj1 valueForKey:@"retainCount"]);     // 2
    
    NSLog(@"address0 = %p", obj);           // 0x600001757be0
    NSLog(@"address1 = %p", obj1);          // 0x600001757be0
    
    // 私有API
    OBJC_EXTERN int _objc_rootRetainCount(id);
    NSLog(@"count20 = %d",_objc_rootRetainCount(obj));             // 1
    NSLog(@"count21 = %d",_objc_rootRetainCount(obj1));            // 2
    
    // 使用CFGetRetainCount
    NSLog(@"count30 = %zd", CFGetRetainCount((__bridge CFTypeRef)(obj)));       // 1
    NSLog(@"count30 = %zd", CFGetRetainCount((__bridge CFTypeRef)(obj1)));      // 2
    
    @autoreleasepool {
        id anObj = [[NSObject alloc] init];
        id __autoreleasing o = anObj;
        NSLog(@"count40 = %@",[anObj valueForKey:@"retainCount"]);      // 2
        NSLog(@"count40 = %@",[o valueForKey:@"retainCount"]);          // 2
    }

为什么 weak 变量指向对象的引用计数改变了,其实我不是很确定,虽然对象地址一样,但是可能是 weak 表导致的。这里不是特别清楚。

ARC 规则

在 ARC 有效的情况下编译源代码,必须遵守一定的规则:

1. 不能使用 retain/release/retainCount/autorelease

ARC 有效时,禁止使用 retain/release/retainCount/autorelease。否则会编译报错。

2. 不能使用 NSAllocateObject/NSDeallocateObjct

ARC 有效时,使用 NSAllocateObject/NSDeallocateObjct 会编译报错。

3. 必须遵循内存管理的方法命名规则

对象的生成/持有的方法必须遵循以下命名规则:

前四种方法和 MRC 下一样。而关于init方法的要求则更为严格:

4. 不要显式调用 dealloc

对象被废弃时,不论 ARC 是否有效,都会调用对象的 dealloc 方法。

ARC 无效时:

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

ARC 有效时,dealloc 无法显式调用,否则编译报错。ARC 会自动处理这个方法,因此也不比书写 [super dealloc]。dealloc 中只需书写废弃对象时所要做的操作即可:

-(void) {
    // 处理
}

5. 使用 @autoreleasepool 代替 NSAutoreleasePool

ARC 有效时,使用 NSAutoreleasePool 会编译报错。

6. 不能使用区域 NSZone

不管 ARC 是否有效,区域 NSZone 在现在的运行时系统(编译器宏 OBJC2 被设定的环境)中已单纯地被忽略。并且 ARC 有效时,使用 NSZone 会编译报错。

7. 对象型变量不能作为 C 语言结构体(struct/union)的成员

C语言的结构体如果存在Objective-C对象型变量,便会引起错误,因为C语言在规约上没有方法来管理结构体成员的生存周期。

备注:
这里存在疑问,书中说:

struct Data {
    NSMutableArray *array;
}

会存在编译错误,但是我测试时,并没有编译错误。不只是版本升级后修改了,还是我理解有问题。

8. 显式转换 id 和 void*

非ARC下,这两个类型是可以直接赋值的:

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

但是在ARC下就会引起编译错误。为了避免错误,我们需要通过__bridege来转换。

id obj = [[NSObject alloc] init];
void *p = (__bridge void*)obj;//显式转换
id o = (__bridge id)p;//显式转换

书中用了大量的篇幅介绍桥接,这里暂时不做扩展了。

属性

属性的声明和所有权修饰符的对应关系:

属性关键字 所有权修饰符
assign __unsafe_unretained 修饰符
copy __strong 修饰符(但是赋值的是被复制的对象)
retain __strong 修饰符
strong __strong 修饰符
unsafe_unretained __unsafe_unretained 修饰符
weak __weak 修饰符

数组

__unsafe_unretained 修饰符以外的 __strong/__weak/__autoreleasing 修饰符保证其指定的变量初始化为 nil。

书中说了一些 C 语言相关的数组处理,不在细说。

后记

不论是作为面试知识,还是对 Objective-C 有更深入的了解,引用计数都值得我们深入学习下。

《Objective-C高级编程》三篇总结之一:引用计数篇

《Objective-C高级编程》三篇总结之二:Block篇

《Objective-C高级编程》三篇总结之三:GCD篇

上一篇 下一篇

猜你喜欢

热点阅读