众所周知,Apple从OS X Lion和iOS 5引入了新的内存管理功能——自动引用计数(ARC)功能。这些功能对于我们开发者说也是需要去了解的一个重要知识点。
自动引用计数
|
|
ARC的机制可以用开关房间里的灯的事例来说明:
- 进入房间的人需要灯光照明。
- 离开房间的人不需要灯光照明。
- 如果离开房间的人因不需要照明而把灯关掉,那房间里剩下的人则不能得到照明。
解决办法就是使房间还有至少1人的情况下保持开灯,无人时关灯。
为了判断是否还有人在房间里,我们导入计数功能来计算“需要照明的人数”:
- 第一个人进入房间,“需要照明人数”+1,计数值由0变为1
- 之后每有一个人进入房间,“需要照明人数”就+1。
- 每当有人离开房间,“需要照明人数”就-1 。
- 最后一个人下班离开房间时,“需要照明人数”就从1减到了0,所以要关灯。
我们将这个事例套入到objc的中,对应的关系如下:
对照明设备所做操作 | 对objc对象所做动作 |
---|---|
开灯 | 生成对象 |
需要照明 | 持有对象 |
不需要照明 | 释放对象 |
关灯 | 废弃对象 |
上述中的生成
、持有
、释放
、废弃
概念可以对应objc的这些方法:
对象操作 | Objective-C方法 |
---|---|
生成并持有对象 | alloc/new/copy/mutableCopy等方法 |
持有对象 | retain方法 |
释放对象 | release方法 |
废弃对象 | dealloc方法 |
ARC规则
在ARC有效的情况下编译源代码,需要遵守一定的规则:
- 不能使用/retain/release/retainCount/autorelease
- 不能使用NSAllocateObject/NSDeallocateObject
- 须遵守内存管理的方法命名规则
- 不要显式调用dealloc(别用[super dealloc])
- 使用@autoreleasepool块替代NSAutoreleasePool
- 不能使用NSZone
- 对象型变量不能作为C语言结构体(struct/union)的成员
- 转换id和void *(__bridge)
引用计数的储存
objc中有些对象如果支持使用TaggedPointer,苹果会直接将其指针值作为引用计数返回。
如果当前设备是64位环境并且使用objective-c 2.0,那么“一些”对象会使用其isa
指针的一部分控件来存储它的引用计数。
TaggedPointer
判断当前对象是否在使用TaggedPointer(观察标志位是否为1)
|
Objc_object *
看似很陌生,其实它的简写就是我们常看到的id
(typedef struct objc_object *id;
)。它的isTaggedPointer
方法经常会在操作引用计数时用到,因为这决定了存储引用计数的策略。
isa指针(NONPOINTER_ISA)
用64 bit存储地址太浪费了,于是优化存储方案,用一部分额外空间存储其他内容。
|
|
SUPPORT_NONPOINTER_ISA
用于标记是否支持优化的isa
指针。
字面意思是isa
的内容不再是类的指针了,还包含了更多的信息,例如引用计数,析构状态,被其他weak变量引用情况。
|
|
以下是isa
指针中变量的对应含义
变量名 | 含义 |
---|---|
indexed | 0表示普通的isa 指针,1表示使用优化,存储引用计数 |
has_assoc | 表示该对象是否包含associated object,如果没有,则析构时会更快 |
has_cxx_dtor | 表示该对象是否有C++或者ARC的析构函数,如果没有,则析构时更快 |
shiftcls | 类的指针 |
magic | 固定值为0xd2,用于在调试时分辨对象是否未完成初始化。 |
weakly_referenced | 表示该对象是否有过weak 对象,如果没有,则析构时更快 |
deallocating | 表示该对象是否正在析构 |
has_sidetable_rc | 表示该对象的引用计数值是否过大无法存储在isa 指针中 |
extra_rc | 存储引用计数值减一后的结果 |
在64位环境下,优化的isa
指针并不是就一定会存储引用计数,19bit的iOS系统保存引用计数不一定够,这19位保存的是引用计数值减一后的值。
has_sidetable_rc
的值如果为1,那么引用计数会存储在一个叫SideTable
的类的属性中。
散列表
散列表来存储引用计数具体是用DenseMap
类来实现,这个类中包含好多映射实例到其引用计数的键值对,并支持用DenseMapIterator
迭代器快速查找遍历这些键值对。
键值对的格式:
- 键的类型为
DisguisedPtr<objc_object>
,DisguisedPtr
类是对objc_object *
指针及其一些操作进行的封装,内容我们可以理解为对象的内存地址 - 值的类型为
__darwin_size_t
。这里保存的值其实就是引用计数减一。
用散列表保存引用计数的优势也很明显,即如果出现故障导致对象的内存块损坏,只要引用计数表没有被破坏,依然可以顺藤摸瓜找到内存块的地址。
获取引用计数
在非ARC环境,可以使用retainCount
方法获取某个对象的引用计数,其会调用objc_object
的rootRetainCount()
方法:
|
|
ARC时代除了使用Core Foundation库中的CFGetRetainCount()
,也可以用Runtime的_objc_rootRetainCount(id obj)
方法来获取引用计数,此时需要引入<objc/runtime.h>
头文件。这个函数也是调用objc_object
的rootRetainCount()
方法:
|
|
rootRetainCount()
方法对引用计数存储逻辑进行了判断。
除开TaggedPointer
和isa
指针的存储方式,会用sidetable_retainCount()
方法:
|
|
sidetable_retainCount()
方法的逻辑就是先从SideTable
的静态方法获取当前实例对应的SideTable
对象,其refcnts
属性就是之前说的存储引用计数的散列表,这里将其类型简写为RefcountMap
:
|
|
然后引用计数表中用迭代器查找当前实例对应的键值对,获取引用计数值,并在此基础上加一返回。
我们可以看到有一个it->second >> SIDE_TABLE_RC_SHIFT
方法将键值对的值做了向右移位的操作
|
|
可以看出第一个bit表示该对象是否有过weak
对象。
第二个bit表示对象是否正在析构。
第三个bit开始才是存储引用计数数值的地方。
所以要向右移两位。
可以用SIDE_TABLE_RC_ONE
对引用计数+1和-1
用SIDE_TABLE_RC_PINNED
来判断是否引用计数值有可能溢出。
_objc_rootRetainCount(id obj)
对于已释放的对象以及不正确的对象地址,有时也返回 “1”。它所返回的引用计数只是某个给定时间点上的值,该方法并未考虑到系统稍后会把自动释放吃池清空,因而不会将后续的释放操作从返回值里减去。clang 会尽可能把 NSString 实现成单例对象,其引用计数会很大。
如果使用了 TaggedPointer,NSNumber 的内容有可能就不再放到堆中,而是直接写在宽敞的64位栈指针值里。其看上去和真正的 NSNumber 对象一样,只是使用 TaggedPointer 优化了下,但其引用计数可能不准确。
SideTable
这里提一下SideTable
,它用于管理引用计数表和weak
表,并使用spinlock_lock
自旋锁来防止操作表结构时可能的竞态条件。它用一个64*128大小的uint8_t
静态数组保存所有SideTable
实例,提供三个公有属性
|
|
还有一个工厂方法
|
|
weak
表的作用是在对象执行dealloc
的时候将所有指向该对象的weak
指针的值设为nil
,避免悬空指针。
|
|
苹果用一个全局weak
表保存所有weak
引用,将对象作为键,weak_entry_t
作为值。weak_entry_t
中保存了所有指向该对象的weak
指针。
修改引用计数
retain和release
非ARC下,可用retain
和release
方法对引用计数进行加一减一操作,它们分别调用了_objc_rootRetain(id obj)
和_objc_rootRelease(id obj)
函数。后两者在ARC下也可以使用。
最后这两个函数会调用objc_object
的两个方法:
|
|
这个实现和获得引用计数类似,先是看是否支持TaggedPointer
,否则就用SideTable
里的refcnts
。
sidetable_retain()
将引用计数加一后返回对象。
sidetable_release()
返回是否要执行dealloc
方法
|
|
it->second < SIDE_TABLE_DEALLOCATING
查看存储的引用计数是否为0,这也是为什么之前存储引用计数时存的是真正的引用计数值减一后的值,是为了防止负数的产生。
如果查看存储的引用计数为0,则将对象标记为正在析构(it->second |= SIDE_TABLE_DEALLOCATING
),并发送dealloc
消息,返回YES
。
否则将引用计数减一(it->second -+ SIDE_TABLE_RC_ONE
)。
Core Foundation库中也提供了增减引用计数的方法。
|
|
CFBridgingRetain
和CFBridgingRelease
这两个方法本质是使用__bridge_retained
和__bridge_transfer
告诉编译器此处需要如何修改引用计数。
除此之外,objc很多实现还是靠Runtime实现的,Objective-C Runtime源码中有些地方明确注明//Replaced by CF
,意思就是说,这块任务被Core Foundation
承包了。
iOS的内存管理
引用计数机制是有些复杂,但是设计到内存管理的话,我们其实不必过多纠结引用计数,直接用以下思路来看待会好一些:
- 自己生成的对象,自己持有。
- 非自己生成的对象,自己也可以持有。
- 不再需要自己持有的对象时释放。
- 非自己持有的对象无法释放。
自己生成的对象,自己持有
使用以下名称开头的方法名意味着自己生成的对象只有自己持有:
- alloc
- new
- copy
- mutableCopy
alloc/new
|
|
一般来说,[NSObject new]
与[[NSObject alloc] init]
是完全一样的。
区别在于alloc
因将关联对象内存分配到相邻区域从而更加省时省力。
alloc
也可以用自定义的init
方法(例如initWithFrame
)而new只能用默认的init。
|
|
看得出来alloc
和new
最终都会调用callAlloc
,默认使用Objective-C 2.0且忽视垃圾回收和NSZone。
后续调用顺序为:
|
|
calloc()
函数相比于malloc()
优点在于它将分配的内存区域初始化为0,相当于malloc()
后再memset()
方法再初始化一次。
copy
copy
用基于NSCopying方法约定,由各类实现的copyWithZone:
方法生成并持有对象的副本。与copy
方法类似。
mutableCopy
利用基于NSMutableCopying
方法约定,由各类实现的mutableCopyWithZone:
方法生成并持有对象的副本。
区别在于,copy
方法生成不可变更的对象,而mutableCopy
生成可变更的对象。类似NSArray
与NSMutableArray
类对象的差异。这里还涉及到一个深浅拷贝的知识点。
两个方法虽然生成的是对象的副本,但是同alloc
、new
一样在自己生成并持有对象这点上没有改变。
非自己生成的对象,自己也能持有
用上述方法以外的方法(即用alloc
、new
、copy
和mutableCopy
以外的方法)取得的对象,因为非自己生成持有,所以自己不是该对象的持有者。
|
|
通过retain
方法,非自己生成的对象跟用alloc/new/copy/mutableCopy
生成并持有的对象一样名称为了自己所持有的。
不再需要自己持有的对象时释放
自己持有的对象,一旦不需要,持有者有义务释放该对象。释放使用release
方法。
|
|
如果要用某个方法生成对象,并将其返还给该方法的调用方,则需要以下方法
|
|
注意,allocObject
方法符合以alloc/new/copy/mutableCopy方法开头
并用驼峰拼写法命名的命名规则。因此它与alloc
方法生成并持有对象的情况完全相同。
若使取得的对象存在,但自己不持有对象,就需要这样
|
|
上例中,我们使用了autorelease
方法。用该方法,可以使取得的对象存在,但自己不持有对象。
autorelease
提供这样的功能,使对象在超出指定的生存范围时能够自动并正确地释放(调用release
)。
使用[NSmutableArray array]
方法取得谁都不持有的对象,就是通过autorelease
实现的。
无法释放非自己持有的对象
对于用alloc/new/copy/mutableCopy
方法生成并持有的对象,或是用retain
方法持有的对象,由于持有者是自己,所以在不需要该对象时需要将其释放。
而由此以外所得到的对象绝对不能释放。倘若在应用程序中释放了非自己持有的对象就会造成崩溃。
|
|
如以上例子,释放非自己持有的对象会造成程序崩溃,因此绝对不要去释放非自己持有的对象。
所有权修饰符
Objc为了处理对象,可将变量类型定义为id类型或各种对象类型。
所谓对象类型就是指向NSObject这样的Objective-C类的指针,例如”NSObject “。id类型用于隐藏对象类型的类名部分,相当于C语言中常用的”void “
ARC有效时,id类型和对象类型同C语言其他类型不同,其类型上必须附有所有权修饰符。
所有权修饰符一共有4种:
- __strong 修饰符
- __weak 修饰符
- __ unsafe_unretained 修饰符
- __autoreleasing修饰符
__strong 修饰符
__strong修饰符是id类型和对象类型默认的所有权修饰符。也就是说以下源代码中的id变量,实际上被附加了所有权修饰符
|
|
ARC无效时,则这样表述
|
|
这段代码表面上无任何变化,再看一下下面的代码
|
|
此源代码制定了C语言的变量的作用域。ARC无效时,该源码可记为:
|
|
为了释放生成并持有的对象,增加调用release方法的代码。该源码进行的动作同先前ARC有效时的动作完全一样。
如此源码所示,附有__strong修饰符的变量obj在超出其变量作用域时,即在该变量被废弃时,会释放其被赋予的对象。
__strong修饰符表示对对象的”强引用”。持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放。
上文中的代码是自己生成自己持有的情况,那么在取得非自己生成并持有的对象时又会如何?
|
|
__strong修饰的变量,不仅只在变量作用域中,在赋值上也能正确管理其对象的所有者。
另外,__strong修饰符同后面要说到的__weak修饰符和__autoreleasing修饰符一起,可以保证将附有这些修饰符的自动变量初始化为nil。
|
|
__weak修饰符
看起来好像通过__strong修饰符,编译器就可以完美进行内存管理,但是事实却并非如此。
使用引用计数式内存管理中必然会发生“循环引用”问题,而光靠__strong是无法解决这一重大问题的。
|
|
这个时候就需要_weak修饰符出场了。\_weak修饰符和__strong相反,提供弱引用。弱引用不能持有对象实例。
首先需要声明的是,__weak不能用来直接声明变量
|
|
以上代码会导致生成的对象立即释放(因为弱引用并不持有对象)。如果像以下这种情况的话,就没有警告了
|
|
__weak修饰符还有一个优点——在持有某对象的弱引用时,若该对象被废弃,则弱引用自动失效且置为nil:
|
|
像这样,使用_weak修饰符可避免循环引用。通过检查有\_weak修饰符修饰的变量是否为nil,可判断被赋值对象是否被废弃。
_weak修饰符只能用于iOS5以上以及OS X Lion以上版本的应用程序。在之前的版本可用\_unsafe_unretained修饰符来代替。
__unsafe_unretained修饰符
__unsafe_unretained修饰符是不安全的所有权修饰符。它不属于编译器的内的存管理对象。
它也与__weak一样,不能直接生成变量,但是它也有不同的地方
|
|
也就是说,最后一行NSLog只是碰巧运行而已,虽然访问了已经被废弃的对象,但是应用程序在个别运行情况下才会崩溃。我想这也是它为什么不成为不安全(unsafe)的修饰符的原因了吧。
在使用__unsafeunretained修饰符时,赋值给附有\_strong修饰符的变量时有必要确保被赋值对象确实存在。
__autoreleasing修饰符
ARC有效时不能使用autorelease方法,也不能使用NSAutoreleasePool类。
那么我们用autoreleas的方法就有所不同
|
|
从以上代码可以看出,我们用@autorelease
块来替代NSAutoreleasePool
类对象生成、持有以及放弃这一范围。
另外ARC有效时,要通过将对象赋值给附加了__autoreleasing修饰符的变量来替代调用autorelease方法。即对象被注册到autoreleasepool。
在取得非自己生成并持有的对象时,
虽然可以用alloc/new/copy/mutableCopy以外的方法来获得对象,但该对象已被注册到了autoreleasepool。由于编译器会检查方法是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回值注册到autoreleasepool。
|
|
以下为使用_weak修饰符的例子,虽然\_weak修饰符是为了避免循环引用而使用的,但在访问附有__weak修饰符的变量时,实际上必定要访问注册到autoreleasepool的对象。
|
|
因为__weak修饰符支持有对象的弱引用,在访问引用对象的过程中,该对象有可能被废弃。把它注册到autoreleasepool中后,在@autoreleasepool块结束之前都能确保该对象存在。
这里要特别说明的是,id的指针或者对象的指针在没有显示指定时会被附上__autoreleasing修饰符。