本文探讨了在iOS中内存管理的命名规则、引用计数的实现机制以及 weak 变量的内部实现。git
本文同时发表于个人我的博客程序员
本文首次发表于2015年,虽然如今 MRC 使用的不多,但 ARC 与 MRC 并无本质上的区别,了解其背后的原理十分重要。github
Memory Management 在 C 语言体系中一直是个重要的话题,它们没有像 Java 那样的 Garbage Collection,内存管理彻底由程序员负责。所以,稍有不慎就会出现 Memory Leak 或 Dangling Pointer 等严重问题,这对于程序来讲是致命的!面试
Objective-C 做为在 C 语言基础上发展起来的面向对象语言,自身天然也没有内存管理机制。所以,做为 iOS 程序员的咱们也须要当心翼翼地处理着内存问题。然而,这一切随着 ARC 的到来有很大的改观。编程
由 iOS5 和 Xcode4.2 内置的编译器 LLVM3.0 共同支持的 ARC(Automatic Reference Counting),如其名称所示实现了内存的自动管理。简单地说,其实质就是将内存管理的工做由程序员转交给编译器来完成,固然某些特性须要 runtime 的支持。数组
与 ARC 相比,咱们将手动内存管理机制称做 MRC(Mannul Reference Counting),就像 ARC 与 MRC 名称所展现的那样,二者从内存管理的本质上讲没有区别,都是经过引用计数(Reference Counting)机制管理内存。不一样的是,在 ARC 中内存管理相关的代码由编译器在编译代码时自动插入。性能优化
那么问题来了~bash
ARC 下编译器如何自动插入内存管理代码?更直白点,编译器如何知道在某处须要插入相关的代码?app
首先,能想到的是在类的 dealloc
方法中,对该类的实例对象所持有的成员变量(strong)执行 release 操做。ide
那么,对于局部变量,编译器如何管理内存?
{
NSString *str = [[NSString alloc] initWithFormat:@"test ARC"];
NSLog(@"%@", str);
}
复制代码
咱们知道,在 ARC 下,上面的代码片断会被编译器处理成(仅是示例,编译器最终的处理可能不彻底一致):
{
NSString * str = [[NSString alloc] initWithFormat:@"test ARC"];
NSLog(@"%@", str);
[str release]; // 编译器插入了 release
}
复制代码
而下面的代码片断,编译器没有为内存管理添加任何代码:
{
NSString *str = [NSString stringWithFormat:@"test ARC"];
NSLog(@"%@", str);
}
复制代码
为何编译器在处理上述两段代码时,采起不一样的态度?
答案很明显,代码2返回的是 autorelese 对象,其已被归入内存管理之中,故不须要编译器再做处理,而代码1却没有。
嗯,问题彷佛已获得完美的解答! 然而,这一切都是站在人的角度去分析的,编译器如何知道代码1须要其管理内存,而代码2不须要?
ok,这就是本节主题:『命名规则』要解决的问题。
任何如下列名称为前缀的方法,若其返回值为 object,则方法调用者持有该 object:
还有一个更为严格的规则:任何以 init 为前缀的方法必须遵照下列规则:
id
或其所属class、superclass、subclass 的对象;『方法调用者持有该 object』也就意味着该 object 的内存问题须要调用方管理。
在此以外的任何方法返回的 object,其调用方都不持有,即返回的应该是 autorelease object。
例外:以 allocate、newer、copying、mutableCopyed 为前缀的方法以及 initialize 方法不在上述规则以内。
编译器根据上述规则,很容易就能判断出在代码1中须要处理对象str
的内存问题,而代码2返回的是 autorelease object,故不须要其处理。
在编码过程当中,应该严格按照上述规则执行!
那问题来了,若是在编码过程当中硬是不遵照上述规则如何? 首先,你就被排除在优秀程序员以外了!MRC 时代,不按上述规则写出的代码在内存管理上极难维护,很容易出现内存泄漏或屡次释放的问题。
因为不少项目经历了 MRC 到 ARC 的时代,所以 ARC 与 MRC 在项目中同时存在的状况大有所在。 若是,此时不遵照上述命名规则会出现问题吗?
缘由在于在 ARC 的文件中调用了以 alloc
为前缀的方法,根据命名规则,此时编译器认为 allocString
方法将返回一个须要其管理内存(release)的对象。上述 ARC 的代码将被编译器处理为:
allocString
方法并未遵照命名规则,其返回的是一个 autorelease object。最终效果就是 release 了一个 autorelease object!这类问题,咱们除了知道出问题对象的类型,crash 堆栈没有任何帮助,所以在大型项目中排查此类问题有必定的难度(工做量)。
allocString
方法自己及调用者都在 ARC 管理范畴之类,编译器很清楚该如何处理。即使如此,做为优秀的程序员,在平常 coding 过程当中仍是应该遵照命名规则。
前文已提到,不管是 ARC 仍是 MRC,其本质都是经过引用计数(Reference Counting)来管理内存。
若是让你设计一套引用计数机制,你会怎么作? 嗯,这是个不错的面试题! 其实,该问题的答案不外乎两种:
GUNstep 实现了一套兼容 Cocoa Framework 的 Framework,做为开源代码咱们来看看它是如何处理引用计数的:
alloc
方法如上所示。能够看到,其使用了一个结构体
obj_layout
来保存引用计数,同时该结构体被附在所生成 object 的头部。 object内存布局以下图所示(引自《Objective-C高级编程》):
因为 Apple 现已来源了相关的代码,使得咱们能够进一步一探究竟。Apple 全部的来源代码均可以在此找到:Apple Opensource。
首先,咱们来看看 Apple 是如何实现 retain
方法的:
retain
方法最终会调用上图所示的
sidetable_retain
方法。
在继续以前有必要介绍一下新朋友:SideTable
SideTable
的成员变量中,彷佛看到了熟悉的味道!是的,没错,其中的
RefcountMap
就是引用计数表,而
weak_table_t
则是弱引用表(weak table).
RefcountMap 则是一个简单的 map,其 key 为 object 内存地址,value 为引用计数值。
经过SideTable
源码,还能够得出以下结论:
SideTable
实例,它们保存在 static 成员变量table_buf
中;[在 iOS 平台上有8个这样的实例(SIDE_TABLE_STRIPE = 8)]table_buf
中相应的SideTable
实例上。这里之因此会存在多个SideTable
实例,object 映射到不一样SideTable
实例上,猜想是出于性能优化的目的,避免SideTable
中的 reference table、weak table 过大。
回到上面的sidetable_retain
方法,其首先经过 object 的地址找到对应的 sidetale,而后经过 RefcountMap
将该 object 的引用计数加1.
release
、retainCount
等相关方法的代码在该开源代码中也能找到,在此再也不赘述。
简单地说,Apple 经过全局的 map 来记录Reference Counting,其key 为 object 地址,value 为引用计数值。
那么,GNUstep 与 Apple 的实现方案各有什么优劣点? GNUstep 的方案从实现的角度看简单明了,而 Apple 的方案可以更好的把控系统内存使用状况,对调试有必定的帮助。
weak 无疑是 ARC 送给咱们的一大利器,经过它基本能消灭 delegate 引发的 dangling pointer 问题。这得益于 weak 指针指向的 object 在 dealloc 时,该指针会被置为 nil。 那么,系统是如何处理 weak 变量的呢?
经过上述代码能够看到,对于 weak 变量,系统将以 weak 指针变量的地址(&weakNum)、用于赋值的 object(num)为参数调用objc_initWeak
方法:
objc_initWeak
方法进一步调用
objc_storeWeak
方法,在该方法中,以赋值object(num)对应的 sidetable 中的 weaktable、赋值object(num)以及 weak 变量的指针为参数调用
weak_register_no_lock
方法。 在
objc_storeWeak
方法中,还能够看到,对于 weak 引用会在赋值object的引用计数表中设置弱引用标志位(SIDE_TABLE_WEAKLY_REFERENCED),具体缘由有待深究。 固然,在
objc_storeWeak
方法中作的最后一件事情就是将赋值对象的地址赋给 weak 指针。
weak_register_no_lock
方法首先检查赋值object在 weak table 中是否存在相应的条目,若存在则直接在其中添加该 weak 变量的信息,若不存在则插入赋值 object 对应的条目。
谈到 weak,天然少不了要说到 weaktable:
weak_entry_t
类型的数组:weak_entries。在
weak_entry_t
结构体中,包含赋值 object 的指针以及全部指向该赋值 object 的 weak 变量列表(weak_referrer_t *referrers)。
那么,在 weaktable 中如何经过object 找到其对应的 entry?
最后一个问题,在 weak 指针所指向的 object 被 dealloc 时,weak 指针会被置为 nil。那么问题来了,若是 weak 变量的生命周期在其指向的 object 以前就结束了,会如何?
若在 weak 变量生命周期结束后,其所使用的内存块被从新利用赋上了新值,而此时上述 object 被 dealloc,若根据 weak table 中的条目将其对应的 weak 变量一一置为 nil,则上述被从新利用的变量也将被清0,这显然是不合适的。
在上面提到的 pseudo code 中,咱们看到在 weak 变量 weakNum
生命周期结束时,调用了objc_destroyWeak
方法,没错,该方法就是用于解决上述问题的。 objc_destroyWeak
方法最终会调用weak_unregister_no_lock
方法,其会将 weak 变量从 weak table 中移除掉。
在 ARC 下,id 与 void*
之间再也不像 MRC 时代能够任意转换,在 ARC 下若需在二者之间转换可使用__bridge cast
。 在使用时需注意,在将 id 转换为 void*
时,其再也不在 ARC 的内存管理范畴内,极有可能出现dangling pointer。
__bridge_retained的做用是使得被赋值变量持有赋值 object。
__bridge_transfer的做用是使得赋值 object 在赋值后被 release。
在 ARC 下,须要转换的状况发生在Objective-C object 与 Core Foundation object 间,此时可使用如下方法:
关于内存管理有不少的问题值得探讨,随着 Apple 相关代码的开源,一切的私密都能在源码中找到答案。