什么是自动引用计数(ARC)

在 Objective-C 中采用 Automatic Reference Counting (ARC) 机制,让编译器来进行内存管理。在新一代 Apple LLVM 编译器中设置ARC为有效状态,就无需再次键入retain或者release代码,这在降低程序崩溃、内存泄漏等风险的问时,很大程度上减少了开发程序的工作量。编译器完全清楚目标对象,并能立刻释放那些不再被使用的对象。如此一来,应用程序将具有可预测性,且能流畅运行,速度也将大幅提升。

苹果的实现

NSObject 类的源代码没有公开,此处利用 Xcode 的调试器和 iOS 大概追溯出其实现过程。

在 NSObject 类的 alloc 类方法上设置断点,追踪程序的执行。以下列出了执行所调用的方法和函数。

  • +alloc
  • +allocWithZone:
  • class_createlnstance
  • calloc

alloc 类方法首先调用 allocWithZone: 类方法,然后调用 class_ createlnstance 函数,最后通过调用 calloc 来分配内存块。class_createlnstance 函数的源代码可以通过 objc4 库中的 runtime/objc-runtime-new.mm 进行确认。

retainCount/retain/release 实例方法又是怎样实现的呢?同刚才的方法一样,下面列出各个方法分别调用的方法和函数。

-retainCount
_CFDoExternRefOperation
CFBasicHashGetCountOfKey

-retain
_CFDoExternRefOperation
CFBasicHashAddValue

-release
_CFDoExternRefOperation
CFBasicHashRemoveValue
(CFBasicHashRemoveValue 返回 0 时,-release 调用 dealloc )

各个方法都通过同一个调用了 _CFDoExternRefOperation 函数,调用了一系列名称相似的函数。如这些函数名的前缀 “CF” 所示,它们包含于 CoreFoundation 框架源代码中,即是 CFRuntime.c_CFDoExternRefOperation 函数。为了理解其实现,下面简化了CFDoExternRefOperation 函数后的源代码。

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:
        CFBasicHashAddValue(table,obj);
        return obj;
    
    case OPERATION_release:
        count = CFBasicHashRemoveValue(table,obj);
        return 0 == count;
    }
}

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

-(NSUInteger)retainCount
{
    return (NSUInteger) _CFDoExternRefOperation (OPERATION_retainCount, self);
}

-(id)retain
{
    return (id) _CFDoExternRefOperation (OPERATION_retain, self);
}

-(void)release
{
    return _CFDoExternRefOperation(OPERATION_release, self);
}

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

20170703149905265688152.png

通过引用计数表管理引用计数的好处如下:

  • 对象用内存块的分配无需考虑内存块头部。
  • 引用计数表各记录中存有内存块地址,可从各个记录追溯到各对象的内存块。

这里特别要说的是,第二条这一特性在调试时有着举足轻重的作用。即使出现故障导致对象占用的内存块损坏,但只要引用计数表没有被破坏,就能够确认各内存块的位置。如图所示:

20170703149905287931529.png

另外,在利用工具检测内存泄漏时,引用计数表的各记录也有助于检测各对象的持有者是否存在。

通过以上解说即可理解苹果的实现。

autorelease

autorelease 就是自动释放,这看上去很像 ARC,但实际上更类似于 C 语言中的自动变量的特性。

autorelease 会像 C 语言的自动变量那样来对待对象实例。当超出其作用域时,对象实例的 release 方法就会被调用。但和 C 语言的自动变量不同的是,程序员可以设定变量的作用域。

autorelease 具体使用方法如下:

  1. 生成并持有 NSAutoreleasePool 对象
  2. 调用已分配对象的 autorelease 方法
  3. 废弃 NSAutoreleasePool 对象

20170626149845880678893.png

NSAutoreleasePool 对象的生命周期相当于 C 语言变量的作用域。对于所有调用过 autorelease 实例方法的对象,在废弃 NSAutoreleasePool 对象时,都将调用 release 实例方法。

用源码表示如下:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
id obj = [[NSObject alloc]init];
[obj autorelease];
[pool drain];

上述代码中的 [pool drain] 相当于 [obj release]

Cocoa 框架中,相当于程序主循环的 NSRunLoop 或者在其他程序可运行的地方,对 NSAutoreleasePool 对象进行生成、持有和废弃处理。因此,程序开发者不一定非得使用 NSAutoreleasePool 对象进行开发工作。

20170626149846494350491.png

尽管如此,但在大量产生 autorelease 的对象时,只要不废弃 NSAutoreleasePool 对象,那么生成的对象就不能释放,因此有时会产生内存不足的现象。典型的例子是读入大量图像的同时改变其尺寸。图像文件读入到 NSData 对象,并从中生成 UIImage 对象,改变其尺寸后生成新的 UIImage 对象。这种情况下,就会大量产生 autorelease 对象。

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

在此情况下,有必要在适当的地方生成、持有或废弃 NSAutoreleasePool 对象。

for(int i = 0;i < 图像数;++i){
    
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
    // 读入图像,大量产生 autorelease 的对象
    
    [pool drain];
    
    // 通过 [pool drain],autorelease 的对象被一起 release
}

另外,Cocoa 框架中也有很多类似方法用于返回 autorelease 的对象。比如 NSMutableArray 类的 arrayWithCapacity 类方法。

id array = [NSMutableArray arrayWithCapacity:1];

此源代码等同于以下源代码:

id array = [[[NSMutableArray alloc]initWithCapacity:1] autorelease];

苹果的实现

可通过 objc4 库的 runtime/objc-arr.mm 来确认苹果中 autorelease 的实现。

class AutoreleasePoolPage
{
    static inline void *push ()
    {
        相当于生成或持有NSAutoreleasePool类对象;
    }
    
    static inline void *pop ( void *token )
    {
        相当于废弃NSAutoreleasePool类对象;
        releaseAll ();
    }
    
    static inline id autorelease ( id obj )
    {
        相当于 `NSAutoreleasePool` 类的 `addObject` 类方法
        AutoreleasePoolPage *autoreleasePoolPage = 取得正在使用的AutoreleasePoolPage实例;
        autoreleasePoolPage->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);
}

C++ 类中虽然有动态数组的实现,但其行为和 GNUstep 的实现完全相同。

我们使用调试器来观察一下 NSAutoreleasePool 类方法和 autorelease 方法的运行过程。如下所示,这些方法调用了关联于 objc4autorelease 实现的函数。

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

id obj = [[NSObject alloc] init];

[obj autorelease];
/* 等同于 objc_autorelease ( obj ) */

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

另外,可通过 NSAutoreleasePool 类中的调试用非公开类方法 showPools 来确认己被 autorelease 的对象的状况。showPools 会将现在的 NSAutoreleasePool 的状况输出到控制台。

[NSAutoreleasePool showPools];

NSAutoreleasePool 类的 showPools 类方法只能在 iOS 中使用,作为替代,在现在的运行时系统中我们使用调试用非公开函数 _objc_autoreleasePoolPrint( )。

20170703149905378046358.png


提高调用Objective-C方法的速度

GNUstep 中的 autorelease 实际上是用一种特殊的方法来实现的。这种方法能够高效地运行0S X、iOS 用应用程序中频繁调用的 autorelease 方法,它被称为"IMP Caching"。

在进行方法调用时,为了解决类名/方法名以及取得方法运行时的函数指针,要在框架初始化时对其结果值进行缓存。

id autorelease_class = [NSAutoreleasePool class];
SEL autorelease_sel = @selector(addObject:);
IMP autorelease_imp =[autorelease_class methodForSelector:autorelease_sel];

实际的方法调用就是使用缓存的结果值。

-(id ) autorelease
{
    (*autorelease_imp )( autorelease_class, autorelease_sel, self );
}

这就是 IMP Caching 的方法调用。虽然同以下源代码完全相同,但从运行效率上看,即使它依赖于运行环境,一般而言速度也是其他方法的2倍。

-(id)autorelease
{
    [NSAutoreleasePool addObject:self];
}

autorelease NSAutoreleasePool 对象

提问:如果 autorelease NSAutoreleasePool 对象会如何?

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

[pool autorelease];

回答:发生异常

*** Terminating app due to uncaught exception 'NSInvalidArgumentException'

reason: '*** -(NSAutoreleasePool autorelease]:
    Cannot autorelease an autorelease pool'

通常在使用 Objective-C,也就是 Foundation 框架时,无论调用哪一个对象的 autorelease 实例方法,实现上是调用的都是 NSObject 类的 autorelease 实例方法。但是对于 NSAutoreleasePool 类,autorelease 实例方法已被该类重载,因此运行时就会出错。


ARC 规则

实际上"引用计数式内存管理"的本质部分在 ARC 中并没有改变。就像"自动引用计数"这个名称表示的那样,ARC 只是自动地帮组我们处理"引用计数"部分。

内存管理的思考方式

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

  • 自己生成的对象,自己所持有。
  • 非自己生成的对象,自己也能持有。
  • 自己持有的对象不再需要时释放。
  • 非自己持冇的对象无法释放。

这一思考方式在ARC有效时也是可行的。只是在源代码的记述方法上稍有不同。

所有权修饰符

Objective-C 编程中为了处理对象,可将变量类型定义为 id 类型或各种对象类型。

所谓对象类型就是指向 NSObject 这样的 Objective-C 类的指针,例如 "NSObject*"。id 类型用于隐藏对象类型的类名部分,相当于 C 语言中常用的 "void *"。

ARC 环境时,id 类型和对象类型同 C 语言其他类型不同,其类型上必须附加所有权修饰符。

所有权修饰符一共有4种。

  • _strong 修饰符
  • _weak 修饰符
  • _unsafe_unretained 修饰符
  • _autoreleasing 修饰符
_strong修饰符

_strong 修饰符是 id 类型和对象类型默认的所有权修饰符。也就是说,以下源代码中的 id 变量,实际上被附加了所有权修饰符。

id obj = [[NSObject alloc] init];

id 和对象类型在没有明确指定所有权修饰符时,默认为 _strong 修饰符。上面的源代码与以下相同。

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

该源码在 MRC 环境该如何表述呢?

/* MRC */
id obj = [[NSObject alloc] init];

该源代码一看则明,目前在表面上并没有任何变化。再看看下面的代码。

{
    id _strong obj = [[NSObject alloc]init];
}

此源代码明确指定了 C 语言的变量的作用域。MRC 环境时,该源代码可记述如下:

/* MRC */
{
    id obj = [[NSObject alloc] init];
    [obj release];
}

为了释放生成并持有的对象,增加了调用 release 方法的代码。该源代码进行的动作同先前 ARC 环境时的动作完全一样。

如此源代码所示,附有 _strong 修饰符的变量 obj 在超出其变量作用域时,即在该变量被废弃时,会释放其被赋予的对象。

如 “strong” 这个名称所示,_strong 修饰符表示对对象的 “强引用”。持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放。

当然,附有 __strong 修饰符的变量之间可以相互赋值。

正如苹果宣称的那样,通过 __strong 修饰符,不必再次键入 retain 或者 release,完美地满足了 “引用计数式内存管理的思考方式”:

  • 自己生成的对象,自己所持有。
  • 非自己生成的对象,自己也能持有。
  • 不再需要自己持有的对象时释放。
  • 非自己持有的对象无法释放。

前两项“自己生成的对象,自己持有”和“非自己生成的对象,自己也能持有”只需通过对带_strong 的修饰符的变景赋值便可达成。通过废弃带 _strong 修饰符的变量(变量作用域结束或是成员变量所属对象废弃)或者对变量赋值,都可以做到“不再需要自己持有的对象时释放”。

最后一项“非自己持有的对象无法释放”,由于不必再次键入 release,所以原本就不会执行。这些都满足于引用计数式内存管理的思考方式。

因为 id 类型和对象类型的所有权修饰符默认为 _strong 修饰符,所以不需要写上 “__strong”。使ARC 环境及简单的编程遵循了 Objective-C 内存管理的思考方式。

_weak修饰符

看起来好像通过 _strong 修饰符编译器就能够完美地进行内存管理。但是遗憾的是,仅通过 _strong 修饰符是不能解决有些重大问题的。

这里提到的重大问题就是引用计数式内存管理中必然会发生"循环引用"的问题。

20170703149905469057068.png

例如,前面出现的带有 _strong 修饰符的成员变量在持有对象时,很容易发生循环引用。

©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;
};
@end

以下为循环引用。

{
    id test0 = [[Test alloc] init];
    id test1 = [[Test alloc] init];
    [testO setObject:test1];
    [test1 setObject:testO];
}

为便于理解,下面写出了生成并持有对象的状态。

{
    /*
    *	test0持有Test对象A的强引用
    */
    id test0 = [[Test alloc] init];/*对象A */
    
    /*
    *	test1持有Test对象B的强引用
    */
    id test1 = [[Test alloc] init];/*对象B */
    
    /*
    * Test对象A的obj_成员变置持有Test对象B的强引用。
    * 此时,持有Test对象B的强引用的变量为Test 对象 A 的 obj_和test1。
    */
    [test0 setObject:test1];
    
    /*
    * Test 对象 B 的 obj_ 成员变置持有 Test 对象 A 的强引用。
    * 此时,持有 Test 对象 A 的强引用的变置为 Test 对象 B 的 obj_ 和test0。
    */
    [testl setObject:test0];
}


/*
* 因为 test0 变置超出其作用域,强引用失效,
* 所以自动释放 Test 对象 A。
* 因为 testl 变置超出其作用域,强引用失效,
* 所以自动释放 Test 对象 B。
* 此时,持有 Test 对象 A 的强引用的变置为 Test 对象 B 的 obj_。
*
* 此时,持有 Test 对象 B 的强引用的变置为 Test 对象 A 的 obj_。
*
* 发生内存泄漏!
*/

循环引用容易发生内存泄漏。所谓内存泄漏就是应当废弃的对象在超出其生存周期后继续存在。此代码的本意是赋予变量 test0 的对象 A 和赋予变量 test1 的对象 B 在超出其变量作用域时被释放,即在对象不被任何变量持有的状态下予以废弃。但是,循环引用使得对象不能被再次废弃。

像下面这种情况,虽然只有一个对象,但在该对象持有其自身时,也会发生循环引用(自引用)。

id test = [[Test alloc] init];
[test setObject:test];

怎么样才能避免循环引用呢?看到 _strong 修饰符就会意识到了,既然有 strong,就应该有与之对应的 weak。也就是说,使用 _weak 修饰符可以避免循环引用。

_weak 修饰符修饰符相反,提供弱引用。弱引用不能持有对象实例。

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

变景 obj 上附加了 _weak 修饰符。实际上如果编译以下代码,编译器会发出聱告,

warning: assigning retained obj to weak variable; obj will be
    released after assignment [-Ware-unsafe-retained-assign]
    id _weak obj = [[NSObject alloc]init];

此源代码将自己生成并持有的对象赋值给附有 _weak 修饰符的变量 obj。即变量 obj 持有对持有对象的弱引用。因此,为了不以自己持有的状态来保存自己生成并持有的对象,生成的对象会立即被释放。编译器对此会给出警告。如果像下面这样,将对象赋值给附有 _strong 修饰符的变量之后再赋值给附有 _weak 修饰符的变量,就不会发生警告了。

{
    id _strong obj0 = [[NSObject alloc] init];
    id _weak obj1 = obj0;
}

下面确认对象的持有状况。

{
    // 自己生成并持有对象
    id _strong obj0 = [[NSObject alloc]init];
    
    // 因为 obj0 变量为强引用,所以自己持有对象。
    
    // obj1 变量持有生成对象的弱引用
    id _weak obj1 = obj0;
}

/*
    因为 obj0 变量超出其作用域,强引用失效,所以自动释放其持有的对象。
    因为对象的所有者不存在,所以废弃该对象。
*/

因为带 _weak 修饰符的变量(即弱引用)不持有对象,所以在超出其变最作用域时,对象即被释放。如果像下面这样将先前可能发生循环引用的类成员变量改成附有 _weak 修饰符的成员变量的话,该现象便可避免。

@interface Test : NSObject

{
    id _weak obj_;
}

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

@end

20170703149906419276443.png

_weak 修饰符还有另一优点。在持有某对象的弱引用时,若该对象被废弃,则此弱引用将自动失效且处于nil 被赋值的状态(空弱应用)。如以下代码所示。

id _weak obj1 = nil;
{
    id _strong obj0 = [[NSObject alloc]init];
    objl = obj0;
    
    NSLog(@"A: %@", obj1);
}

NSLog(@"B: %@", obj1);

此源代码执行结果如下:

A: <NSObject: 0x753el80>
B: (null)

像这样,使用 _weak 修饰符可避免循环引用,通过检查附有 _weak 修饰符的变量是否为nil,可以判断被赋值的对象是否己废弃。

规则

在 ARC 的环境下编译源代码,必须遵守一定的规则。下由就是具体的 ARC 的规则

  1. 不能使用 retain/release/retainCount/autorelease
  2. 不能使用 NSAllocateObject/NSDeallocateObject
  3. 须遵守内存管理的方法命名规则
  4. 不要显式调用 dealloc
  5. 使用 @autoreleasepool 块替代 NSAutoreleasePool
  6. 不能使用区域(NSZone)
  7. 对象型变量不能作为C语言结构体(struct/union)的成员
  8. 显式转换“id”和“void *”

不能使用 retain/release/retainCount/autorelease

内存管理是编译器的工作,因为没有必要使用内存管理的方法。

不能使用 NSAllocateObject/NSDeallocateObject

一般通过调用 NSObject 类的 alloc 类方法来生成并持有 Objective-C 对象。

id obj = [NSObject alloc];

但是就如 GNUstep 的 alloc 实现所示,实际上是通过直接调用 NSAllocateObject 函数来生成并持有对象的。

在 ARC 环境时,禁止使用 NSAllocateObject 函数。同 retain 等方法一样,如果使用便会引起编译错误。

error: 'NSAllocateObject is unavailable:
not available in automatic reference counting mode

同样地,也禁止使用用于释放对象的 NSDeallocateObject 函数。

须遵守内存管理的方法命名规则

在 MRC 环境时,用于对象生成/持有的方法必须遵守以下的命名规则。

  • alloc
  • new
  • copy
  • mutableCopy

以上述名称开始的方法在返回对象时,必须返回给调用方所应当持有的对象。这在 ARC 环境时也一样,返回的对象完全没有改变。只是在 ARC 环境下要追加一条命名规则。

  • init

以 init 开始的方法的规则要比 alloc/new/copy/mutableCopy 更严格。该方法必须是实例方法,并且必须要返回对象。返回的对象应为 id 类型或该方法声明类的对象类型,抑或是该类的超类型或子类型。该返回对象并不注册到 autoreleasepool 上。基本上只是对 alloc 方法返回值的对象进行初始化处理并返回该对象。

以下为使用该方法的源代码。

id obj = [[NSObject alloc]init];

如此源代码所示,init 方法会初始化 alloc 方法返回的对象,然后原封不动地返还给调用方。下面我们来看看以init开始的方法的命名规则。

-(id) initWithObject:(id)obj;

该方法声明遵守了命名规则,但下面这个方法虽然也以init开始,却没有返回对象,因此不能使用。

-(void) initThisObject;

另外,下例虽然也是以 init 开始的方法但并不包含在上述命名规则里。请注意。

-(void) initialize;

不要显式调用 dealloc

无论 ARC 还是 MRC,只要对象的所有者都不持有该对象,该对象就被废弃。对象被废弃时,都会调用对象的 dealloc 方法

-(void)dealloc
{
    // 此处运行该对象被废弃时必须实现的代码
}

在 MRC 环境下必须像下面这样调用 [super dealloc]

-(void)dealloc
{
    // 此处运行该对象被废弃时必须实现的代码
    [super dealloc];
}

ARC 会自动对此进行处理,因此不必写 [super dealloc]。

属性

在 ARC 环境下,Objective-C 类的属性也会发生变化。

@property (nonatomic, strong) nsstring *name;

在 ARC 环境下,以下可作为这种属性声明中使用的属性来用。

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

以上各种属性赋值给指定的属性中就相当于赋值给附加各属性对应的所有权修饰符的变量中。只有 copy 属性不是简单的斌值,它赋值的是通过 NSCopying 接口的copyWithZone: 方法复制赋值源所生成的对象。

另外,在声明类成员变量时,如果同属性声明中的属性不一致则会引起编译错误,比如下面这种情况。

id obj;

在声明 id 型 obj 成员变量时,像下面这样,定义其属性声明为weak。

@property (nonatomic, weak) id obj;

编译器出现如下错误。

error: existing ivar 'obj' for _weak property 'obj' must be _weak
    @synthesize obj;
    
    note: property declared here
    @property (nonatomic, weak) id obj;

此时,成员变量的声明中需要附加 __weak 修饰符。

id __weak obj;

或者使用 strong 属性来代替 weak 属性。

@property (nonatomic, stong) id obj;

数组

以下是将附有 _strong 修饰符的变量作为静态数组使用的情况。

id objs[10];

_weak 修饰符,_autoreleasing 修饰符以及 _unsafe_unretained 修饰符也与此相同。

id _weak objs[10];

_unsafe_unretained 修饰符以外的 _strong/_weak/ autorelcasing修饰符保证其指定的变量初始化为 nil。同样地,附有 _strong/_weak/_autoreleasing 修饰符变量的数组也保证其初始化为 nil。下面我们就来看看数组中使用附有 _strong 修饰符变量的例子:

{
    id objs[2];

    objs[0] = [[NSObject alloc]init];
    objs[l] = [NSMutableArray array];
}

数组超出其变量作用域时,数组中各个附有 _strong 修饰符的变量也随之失效,其强引用消失,所赋值的对象也随之释放。这与不使用数组的情形完全一样。

将附有 _strong 修饰符的变量作为动态数组来使用时又如何呢?在这种情况下,根据不同的目的选择使用 NSMutableArray、NSMutableDictionary、NSMutableSet 等 Foundation 框架的容器。这些容器会恰当地持有追加的对象并为我们管理这些对象。

像这样使用容器虽然更为合适,但在 C 语言的动态数组中也可以使用附有 _strong 修饰符的变量,只是必须要遵守一些事项。以下按顺序说明。

声明动态数组用指针。

id _strong *array = nil;

如前所述,由于 “id*" 类型默认为 “id_autoreleasing*" 类型,所以有必要显式指定为 _strong 修饰符。另外,虽然保证了附有 _strong 修饰符的 id 型变量被初始化为 nil,但并不保证附有 _strong 修饰符的 id 指针型变量被初始化为 nil。

另外,使用类名时如下记述。

NSObject * _strong *array = nil;

其次,使用 calloc 函数确保想分配的附有 _strong 修饰符变量的容量占有的内存块。

array = (id __strong *) calloc(entries, sizeof(id));

该源代码分配了 entries 个所需的内存块。由于使用附有 _strong 修饰符的变量前必须先将
其初始化为 nil,所以这里使用使分配区域初始化为0的 calloc 函数来分配内存。不使用 calloc
数,在用 malloc 函数分配内存后可用 memset 等函数将内存填充为0。

但是,像下面的源代码这样,将 nil 代入到 malloc 函数所分配的数组各元素中来初始化是非常危险的。

array = (id _strong *) malloc(sizeof(id) * entries);

for (NSUInteger i * 0; i < entries; ++i)
    array[i] = nil;

这是因为由于 malloc 函数分配的内存区域没有被初始化为0,因此 nil 会被赋值给附有 _strong 修饰符的并被赋值了随机地址的变量中,从而释放一个不存在的对象。在分配内存时推荐使用 calloc 函数。

像这样,通过 calloc 函数分配的动态数组就能完全像静态数组一样使用。

array[0] = [[NSObject alloc] init];

但是,在动态数组中操作附有 _strong 修饰符的变量与静态数组有很大差异,需要自己释放所有的元素。

如以下源代码所示,在只是简单地用 free 函数废弃了数组用内存块的情况下,数组各元素所赋值的对象不能再次释放,从而引起内存泄漏。

free(array);

这是因为在静态数组中,编译器能够根据变量的作用域自动插入释放赋值对象的代码,而在动态数组中,编译器不能确定数组的生存周期,所以无从处理。如以下源代码所示,一定要将 nil 赋值给所有元素中,使得元素所赋值对象的强引用失效,从而释放那些对象。在此之后,使用 free 函数废弃内存块。

for (NSUInteger i * 0; i < entries; ++i)
    array[i] = nil;
    
free(array);

同初始化时的注意事项相反,即使用 memset 等函数将内存填充为0也不会释放所赋值的对象。这非常危险,只会引起内存泄漏。对于编译器,必须明确地使用赋值给附有 _strong 修饰符变量的源代码。所以请注意,必须将 nil 赋值给所有数组元素。

另外,使用 memcpy 函数拷贝数组元素以及 realloc 函数重新分配内存块也会有危险,由于数组元素所赋值的对象有可能被保留在内存中或是重复被废弃,所以这两个函数也禁止使用。

再者,我们也可以像使用 _strong 修饰符那样使用附有 _weak 修饰符变量的动态数组。在 _autoreleasing 修饰符的情况下,因为与设想的使用方法有差异,所以最好不要使用动态数组。由于_unsafe_unretained 修饰符在编译器的内存管理对象之外,所以它与 void* 类型一样,只能作为C语言的指针类型来使用。

本文是根据Objective-C高级编程第一章整理,作学习和参考之用

关于内存管理可以查看一下唐巧大神博客的讲解:
理解 iOS 的内存管理-唐巧