017-深入学习 ARC 及内存管理
自动引用计数(Automatic Reference Counting)是指在内存管理中对引用计数进行自动计数的技术,适用条件为:
- Xcode 4.2 以上
- LLVM 编译器 3.0 以上
- 设置 ARC 有效
iOS 内存管理的实现
内存管理的思考方式
- 自己生成的对象,自己持有
- 非自己生成的对象,自己也可以持有
- 不再需要自己持有的对象时释放
- 非自己持有的对象无法释放
这四条规则在 MRC 和 ARC 中都适用,但略有差别,要注意这里的自己,可以理解为对象的使用环境,也可以理解为编程人员自身。
对象操作与 OC 方法的对应
对象操作 | OC 方法 |
---|---|
生成并持有对象 | alloc/new/copy/mutableCopy系列方法 |
持有对象 | retain |
释放对象 | release |
废弃对象 | dealloc |
方法实现
由于包含 NSObject 的 Foundation 框架没有公开,NSObject 包含的 alloc/new/copy 等方法也就无法看到源码,但是可以参考 Cocoa 框架的互换框架 GNUstep。
alloc
id obj = [NSObject alloc];
实现如下
+ (id) alloc
{
return [self allocWithZone: NSDefaultMallocZone()];
}
+ (id) allocWithZone: (NSZone*)z
{
return NSAllocateObject (self, 0, z);
}
struct obj_layout {
char padding[__BIGGEST_ALIGNMENT__ - ((UNP % __BIGGEST_ALIGNMENT__)
? (UNP % __BIGGEST_ALIGNMENT__) : __BIGGEST_ALIGNMENT__)];
gsrefcount_t retained;
};
NSAllocateObject (Class aClass, NSUInteger extraBytes, NSZone *zone)
{
id new;
int size;
NSCAssert((!class_isMetaClass(aClass)), @"Bad class for new object");
size = class_getInstanceSize(aClass) + extraBytes + sizeof(struct obj_layout);
if (zone == 0)
{
zone = NSDefaultMallocZone();
}
new = NSZoneMalloc(zone, size);
if (new != nil)
{
memset (new, 0, size);
new = (id)&((obj)new)[1];
object_setClass(new, aClass);
AADD(aClass, new);
}
if (0 == cxx_construct)
{
cxx_construct = sel_registerName(".cxx_construct");
cxx_destruct = sel_registerName(".cxx_destruct");
}
callCXXConstructors(aClass, new);
return new;
}
简单来说,NSAllocteObject 通过 NSZoneMalloc 函数来分配存放对象的内存空间,置 0 后返回。
简化版代码如下
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);
}
obj_layout
结构体用于存放 retained 引用计数,最后返回的指针不包含它。
获得一个对象的引用计数可以用 retainCount
方法。
- (NSUInteger) retainCount
{
return NSExtraRefCount(self) + 1;
}
inline NSUInteger
NSExtraRefCount(id anObject)
{
return ((obj)anObject)[-1].retained;
}
在 C 语言中对于一个指针 p 调用 p[-1] 相当于 p - 1,获取指针所指地址前一位的地址。
这里定义每一个对象都有一个隐式的为 1 的引用计数,同时有一个外部引用计数,所以实际上 alloc 之后的外部引用计数为 0,所以调用 retainCount 方法的返回值为 1。
retain
[obj retain];
实现如下
- (id) retain
{
NSIncrementExtraRefCount(self);
return self;
}
inline void
NSIncrementExtraRefCount(id anObject)
{
BOOL tooFar = NO;
#if defined(GSATOMICREAD)
/* I've seen comments saying that some platforms only support up to
* 24 bits in atomic locking, so raise an exception if we try to
* go beyond 0xfffffe.
*/
if (GSAtomicIncrement((gsatomic_t)&(((obj)anObject)[-1].retained))
> 0xfffffe)
{
tooFar = YES;
}
#else /* GSATOMICREAD */
NSLock *theLock = GSAllocationLockForObject(anObject);
[theLock lock];
if (((obj)anObject)[-1].retained > 0xfffffe)
{
tooFar = YES;
}
else
{
((obj)anObject)[-1].retained++;
}
[theLock unlock];
#endif /* GSATOMICREAD */
if (YES == tooFar)
{
static NSHashTable *overrun = nil;
[gnustep_global_lock lock];
if (nil == overrun)
{
overrun = NSCreateHashTable(NSNonRetainedObjectHashCallBacks, 0);
}
if (0 == NSHashGet(overrun, anObject))
{
NSHashInsert(overrun, anObject);
}
else
{
tooFar = NO;
}
[gnustep_global_lock lock];
if (YES == tooFar)
{
NSString *base;
base = [NSString stringWithFormat: @"<%s: %p>",
class_getName([anObject class]), anObject];
[NSException raise: NSInternalInconsistencyException
format: @"NSIncrementExtraRefCount() asked to increment too far"
@" for %@ - %@", base, anObject];
}
}
}
主要是检查是否数值过大而溢出,未溢出则会把 retained 的变量自增一。
release
[obj release];
实现如下
- (oneway void) release
{
if (NSDecrementExtraRefCountWasZero(self))
{
# ifdef OBJC_CAP_ARC
objc_delete_weak_refs(self);
# endif
[self dealloc];
}
}
inline BOOL
NSDecrementExtraRefCountWasZero(id anObject)
{
if (((struct obj_layout *)anObject)[-1].retained == 0) {
return YES;
} else {
((struct obj_layout *)anObject)[-1].retained == 0) {
return NO;
}
}
}
这里可以理解下,当 retained 值为 0 的时候,调用 retainCount 方法返回 1,此时调用 release 方法就会执行 dealloc 方法,因为没有变量再持有这个对象。
dealloc
- (void) dealloc
{
NSDeallocateObject (self);
}
inline void
NSDeallocateObject(id anObject)
{
Class aClass = object_getClass(anObject);
if ((anObject != nil) && !class_isMetaClass(aClass))
{
obj o = &((obj)anObject)[-1];
NSZone *z = NSZoneFromPointer(o);
/* Call the default finalizer to handle C++ destructors.
*/
(*finalize_imp)(anObject, finalize_sel);
AREM(aClass, (id)anObject);
if (NSZombieEnabled == YES)
{
GSMakeZombie(anObject, aClass);
if (NSDeallocateZombies == YES)
{
NSZoneFree(z, o);
}
}
else
{
object_setClass((id)anObject, (Class)(void*)0xdeadface);
NSZoneFree(z, o);
}
}
return;
}
NSDeallocateObject 会释放掉最开始 alloc 开辟的内存空间。
苹果的实现略有不同,没有用结构体而是用散列表(引用计数表)来管理,后面对于 __weak
变量也用到了散列表。散列表中键值为内存块地址的散列值,这样可以方便地从各个记录追溯到各对象的内存块,也有助于检测各对象的持有者是否存在。
autorelease 与实现
autorelease 在 MRC 和 ARC 中使用都比较广泛,在 MRC 中使用方法如下:
- 生成并持有 NSAutoreleasePool 对象
- 调用已分配对象的 autorelease 实例方法
- 废弃 NSAutoreleasePool 对象
而在 ARC 中用 @autoreleasingpool 和 __autoreleasing
修饰符来实现。
同时在 Cocoa 框架中,相当于程序主循环的 NSRunLoop 或者在其他程序可运行的地方,对 NSAutoreleasePool 对象进行生成、持有和废弃处理,所以不一定非要手动创建 NSAutoreleasePool 对象。但是如果是大量产生 autorelease 对象的场景,只要不废弃 NSAutoreleasePool 对象,生成的对象就不会被释放,可能产生内存不够的现象。
在 GNUstep 中
[obj autorelease]
实现如下
- (id) autorelease
{
if (double_release_check_enabled)
{
NSUInteger release_count;
NSUInteger retain_count = [self retainCount];
release_count = [autorelease_class autoreleaseCountForObject:self];
if (release_count > retain_count)
[NSException
raise: NSGenericException
format: @"Autorelease would release object too many times.\n"
@"%"PRIuPTR" release(s) versus %"PRIuPTR" retain(s)",
release_count, retain_count];
}
(*autorelease_imp)(autorelease_class, autorelease_sel, self);
return self;
}
关键的语句是最后一句(*autorelease_imp)(autorelease_class, autorelease_sel, self);
,这里用到了 "IMP Caching" 技术来缓存经常被调用的方法的结果值,实际的方法调用就是使用缓存的结果值
autorelease_class = [NSAutoreleasePool class];
autorelease_sel = @selector(addObject:);
autorelease_imp = [autorelease_class methodForSelector: autorelease_sel];
所以实际上它等同于下面的语句
[NSAutoreleasePool addObject:self];
而 NSAutoreleasePool 实际上是维护一个列表,在列表内存储各个 autorelease 对象。
苹果封装了一些方法,用动态数组来存储 authorelease 对象,下面是简化代码
class AutoreleasePoolPage
{
static inline void *push()
{
生成或持有 NSAutoreleasePool 对象
}
static inline void pop(void *token)
{
废弃 NSAutoreleasePool 对象
releaseAll();
}
static inline id autorelease(id obj)
{
相当于 addObject
}
void releaseAll()
{
依次调用数组中对象的 release 实例方法
}
}
有两个非公开类方法可以确认并打印出 autoreleasePool 的状态
-
[NSAutoreleasePool showPools] 这个方法在 ARC 下不可用
-
_objc_autoreleasePoolPrint()
这个方法使用前要先声明,
extern void _objc_autoreleasePoolPrint();
要注意一点,对于 Foundation 框架中的对象调用 autorelease 方法时,实际是调用 NSObject 的 autorelease 方法,但是 NSAutoreleasePool 类的 autorelease 方法被重栽了所以调用会发生异常。
ARC 规则
iOS 内存管理的思考方式本质在 ARC 中并未改变,但是 ARC 将实现方式做了改动,能便捷可靠地管理对象的生命周期。
ARC 权限可以设置为单个文件的属性,ARC 和 非 ARC 文件可以共存于一个项目中。
所有权修饰符
ARC 有效时,id 类型和 OC 对象类型与 C 语言其他类型不同,必须加上所有权修饰符
- __strong 默认修饰符
- __weak
- __unsafe_unretained
- _autoreleasing
__strong
修饰符可以用于方法参数上,这样传入的参数就会被使用环境强持有。
- (void)setObject: (id __strong) target
__strong
、__weak
和 __autoreleasing
可以保证将附有这些修饰符的自动变亮初始化为 nil。
__weak
修饰符在持有某个对象弱引用时,当对象被废弃,此弱引用会自动失效且置为 nil,不会造成野指针问题。
__unsafe_unretained
修饰符主要用于兼容 iOS4 以下版本,它修饰的变量不属于编译器的内存管理对象,同时在对象被废弃时不会被自动置为 nil,因此可能造成野指针问题。
由于 ARC 下不能手动使用 autorelease 方法和 NSAutoreleasePool 对象,因此用 @autoreleasingpool 和 __autoreleasing
修饰符来代替。
虽然 __strong
修饰符是对于 id 类型和 OC 对象类型的默认修饰符,但不适用于 id 指针和对象指针,id 指针和对象指针的默认修饰符是 autoreleasing。
NSError *realError = nil;
NSError **errorPointer = &realError;
由于对象指针赋值时所有权必须一致,因此会报编译错误
NSError *realError = nil;
NSError * __strong *errorPointer = &realError;
《Pro multithreading and memory management for iOS and OSX》 在 autoreleasing 修饰符这部分写的很混乱,也可能是翻译的原因,英文版也有很多困惑的地方。
规则
-
不能使用 retain/release/retainCount/autorelease
-
不能使用 NSAllocateObject/NSDealloateObject
NSAllocateObject 和 NSDeallocateObject 是 alloc 等方法的内部实现,所以也不能使用。
-
遵守内存管理的方法命名规则
alloc/new/copy/mutableCopy 等驼峰式前缀的方法必须返回给调用方所应当持有的对象,init 驼峰式前缀方法必须是实例方法,返回对象为 id 类型或该方法声明类的对象类型,返回对象不会注册到 autoreleasePool 中。
-
不显式调用 dealloc 方法
-
使用 @autoreleasingpool 和
__autoreleasing
修饰符代替 NSAutoreleasePool -
不能使用 NSZone
-
OC 对象不能作为 C 语言结构体成员
C 语言无法管理 OC 对象的生命周期,可以用 void * 或
__unsafe_unretained
修饰符
桥接转换
如上所述,OC 对象生命周期由编译器管理,C 语言无法管理,如果想把 OC 对象转换为 C 变量,在 MRC 中可以简单转换,但是在 ARC 中必须进行桥接转换,这一点在使用 Core Foundation 框架时很常见,因为 Core Foundation 框架包含的是 C 语言接口。
id obj = [[NSObject alloc] init];
void *p = (__bridge void *)obj;
id o = (__bridge id)p;
__bridge
转换的安全性与 __unsafe_unretained
相近,因此可能造成野指针问题。
__bridge_retained
将 OC 对象转换为 CF 对象,CF 对象需要负责用 CFRelease 等方法释放对象
void *p = (__bridge_retained void *)obj;
__bridge__transfer
将 Core Foundation 对象转换为 OC 对象,同时 CF 对象放弃持有。
id o = (__bridge_transfer id)p;
在 CF 框架中有一组对应 API
- CFBridgingRelease --
__bridge__transfer
- CFBridgingRetain --
__bridge_retained
属性
ARC 中属性具有的特性与所有权修饰符的对应关系
属性声明的属性 | 所有权修饰符 |
---|---|
assign | __unsafe_unretained |
copy | __strong |
retain | __strong |
strong | _strong |
unsafe_unretained |
__unsafe_unretained |
weak | __weak |
其中 copy 属性的赋值是通过 NSCopying 接口的 copyWithZone: 方法。
ARC 实现
__strong 实现
对于 alloc/new/copy/mutableCopy 方法等,其模拟源代码如下
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_release(obj);
而对于非 alloc 等方法,实现源码略有不同
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_release(obj);
这里用到一个新的方法 objc_retainAutoreleasedReturnValue
,而在返回对象的方法,例如这里的 array 方法等,有一个配对的方法
+ (id) array
{
id obj = objc_mgSend(NSMutableArray, @selector(alloc));
objc_msgSend(obj, @selector(init));
return objc_autoreleaseReturnValue(obj);
}
这里这两个方法其实是一种优化策略,如果 objc_autoreleaseReturnValue()
检测使用该函数的方法或函数调用方的执行命令列表,如果方法或调用方在调用了该方法后会紧接着调用 objc_retainAutoreleasedReturnValue
,则该函数会取消注册到 autoreleasePool 的操作,并且将结果保存到 thread-local storage 中,也就是一个线程局部存储,这一部分存储空间只作为某个线程专有的存储。然后被调用函数直接返回这个 object,同时外部接收到返回值后去检查 TLS 中刚好有这个对象,就直接返回,不进行retain 操作。相当于节约了一次 release 和 retain 的操作。
但是这样做的前提是被调用方要能得知外部调用方的环境是 ARC 还是 非 ARC,这里会用到 __builtin_return_address
方法,作用是得到函数的返回地址,参数表示层数,于是内部的被调用方加入偏移值就可以查看外部的调用方的汇编指令,从而检测出外部是否调用了 objc_retainAutoreleasedReturnValue
这个方法。
__weak 实现
前面提过引用计数的存储是用引用计数表来实现的,这里对于 __weak
对象也是如此。以对象地址作为主键,存储多个 __weak
修饰符的变量,对它们进行统一管理。
{
id __weak obj1 = obj;
}
这句声明赋值语句的实现如下
id obj1;
obj1_initWeak(&obj1, obj);
obj1_destroyWeak(&obj1);
实际上也就是
id obj1;
obj1 = 0;
objc_storeWeak(obj1, obj);
objc_storeWeak(&obj1, 0);
这里 objc_storeWeak
函数会把第二餐素的赋值对象的地址作为键值,将第一参数变量地址注册到 weak 表中。如第二参数为 0,则把变量地址从 weak 表中删除。
当废弃对象时,将进行以下操作:
- 从 weak 表中获取废弃对象的地址为键值的记录
- 将包含在记录中的所有附有
__weak
修饰符变量的地址置为 nil - 从 weak 表中删除该记录
- 从引用计数表中删除废弃对象的地址为键值的记录
因此如果大量使用 __weak
变量就会消耗相应的 CPU 资源。
另一方面,在 使用 附有 _weak
修饰符的变量时变量会被注册到 autoreleasePool 中。
{
id __weak boj1 = obj;
NSLog(@"%@", obj1);
}
其实现如下
id obj1;
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@", tmp);
objc_destroyWeak(&obj1);
这里强调在使用该变量时才会注册到 autoreleasePool 中。由于每次使用时都会注册一遍,所以当多次使用时会注册大量变量到 autoreleasePool 中,因此最好暂时用 __strong
变量暂存一下,因为 __strong
变量只会被注册一次。
关于注册到 autoreleasePool 的操作无法被验证
id obj = [[NSObject alloc] init];
id __weak obj1 = obj;
NSLog(@"pre %lu", _objc_rootRetainCount(obj));
NSLog(@"%@", [obj1 class]);
NSLog(@"after %lu", _objc_rootRetainCount(obj));
按照 《Pro multithreading and memory management for iOS and OSX》 的说法打印出来应该是使用前为 1,使用后变成 2,实际打印结果
pre 1
NSObject
pre 1