深刻理解 iOS 内存管理

本文探讨了在iOS中内存管理的命名规则、引用计数的实现机制以及 weak 变量的内部实现。git

本文同时发表于个人我的博客程序员

本文首次发表于2015年,虽然如今 MRC 使用的不多,但 ARC 与 MRC 并无本质上的区别,了解其背后的原理十分重要。github

Overview


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:

  • alloc
  • new
  • copy
  • mutableCopy

还有一个更为严格的规则:任何以 init 为前缀的方法必须遵照下列规则:

  • 该方法必须是实例方法;
  • 该方法必须返回类型为id或其所属class、superclass、subclass 的对象;
  • 该方法返回的 object 不能是 autorelese,即方法调用者持有返回的 object。

『方法调用者持有该 object』也就意味着该 object 的内存问题须要调用方管理。

在此以外的任何方法返回的 object,其调用方都不持有,即返回的应该是 autorelease object。

例外:以 allocate、newer、copying、mutableCopyed 为前缀的方法以及 initialize 方法不在上述规则以内。

编译器根据上述规则,很容易就能判断出在代码1中须要处理对象str的内存问题,而代码2返回的是 autorelease object,故不须要其处理。

在编码过程当中,应该严格按照上述规则执行!

那问题来了,若是在编码过程当中硬是不遵照上述规则如何? 首先,你就被排除在优秀程序员以外了!MRC 时代,不按上述规则写出的代码在内存管理上极难维护,很容易出现内存泄漏或屡次释放的问题。

在 MRC 下,上述代码经过 Analyze 作静态分析时会给出如上图所示的 warning。

ARC 与 MRC 混用

因为不少项目经历了 MRC 到 ARC 的时代,所以 ARC 与 MRC 在项目中同时存在的状况大有所在。 若是,此时不遵照上述命名规则会出现问题吗?

运行结果:
crash!

缘由在于在 ARC 的文件中调用了以 alloc 为前缀的方法,根据命名规则,此时编译器认为 allocString 方法将返回一个须要其管理内存(release)的对象。上述 ARC 的代码将被编译器处理为:

然而,在 MRC 中的 allocString方法并未遵照命名规则,其返回的是一个 autorelease object。最终效果就是 release 了一个 autorelease object!这类问题,咱们除了知道出问题对象的类型,crash 堆栈没有任何帮助,所以在大型项目中排查此类问题有必定的难度(工做量)。
那么,在 ARC 的文件中不遵照命名规则会出问题吗?
这段代码运行正常没有 crash,缘由在于 allocString 方法自己及调用者都在 ARC 管理范畴之类,编译器很清楚该如何处理。即使如此,做为优秀的程序员,在平常 coding 过程当中仍是应该遵照命名规则。

Inside Reference Counting


前文已提到,不管是 ARC 仍是 MRC,其本质都是经过引用计数(Reference Counting)来管理内存。

若是让你设计一套引用计数机制,你会怎么作? 嗯,这是个不错的面试题! 其实,该问题的答案不外乎两种:

  • 在对象内部管理引用计数;
  • 经过外部结构(如:hash 表)统一管理引用计数。

GNUstep’s Implementation of Reference Counting

GUNstep 实现了一套兼容 Cocoa Framework 的 Framework,做为开源代码咱们来看看它是如何处理引用计数的:

经过整理,删除非必要的代码,GUNstep 实现的 alloc方法如上所示。能够看到,其使用了一个结构体 obj_layout来保存引用计数,同时该结构体被附在所生成 object 的头部。 object内存布局以下图所示(引自《Objective-C高级编程》):

Apple’s Implementation of Reference Counting

因为 Apple 现已来源了相关的代码,使得咱们能够进一步一探究竟。Apple 全部的来源代码均可以在此找到:Apple Opensource

首先,咱们来看看 Apple 是如何实现 retain 方法的:

若是抛开 Apple 所作的优化,其 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.

releaseretainCount等相关方法的代码在该开源代码中也能找到,在此再也不赘述。

简单地说,Apple 经过全局的 map 来记录Reference Counting,其key 为 object 地址,value 为引用计数值。

那么,GNUstep 与 Apple 的实现方案各有什么优劣点? GNUstep 的方案从实现的角度看简单明了,而 Apple 的方案可以更好的把控系统内存使用状况,对调试有必定的帮助。

Inside Weak


weak 无疑是 ARC 送给咱们的一大利器,经过它基本能消灭 delegate 引发的 dangling pointer 问题。这得益于 weak 指针指向的 object 在 dealloc 时,该指针会被置为 nil。 那么,系统是如何处理 weak 变量的呢?

上面这个简单的代码片断会被编译器处理成以下所示的 pseudo code:
注:如下代码已通过整理以便阅读、抓住重点。

经过上述代码能够看到,对于 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 对应的条目。

weaktable

谈到 weak,天然少不了要说到 weaktable:

在 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 中移除掉。

Toll-Free Bridge cast


__bridge cast

在 ARC 下,id 与 void*之间再也不像 MRC 时代能够任意转换,在 ARC 下若需在二者之间转换可使用__bridge cast。 在使用时需注意,在将 id 转换为 void*时,其再也不在 ARC 的内存管理范畴内,极有可能出现dangling pointer。

__bridge_retained cast

__bridge_retained的做用是使得被赋值变量持有赋值 object。

上述 ARC 代码与下面的 MRC 代码在内存管理上是等价的

__bridge_transfer cast

__bridge_transfer的做用是使得赋值 object 在赋值后被 release。

上面两段代码是等价的,正如__bridge_transfer名称所示,其做用是将持有权从赋值 object 转到被赋值变量。

在 ARC 下,须要转换的状况发生在Objective-C object 与 Core Foundation object 间,此时可使用如下方法:

小结


关于内存管理有不少的问题值得探讨,随着 Apple 相关代码的开源,一切的私密都能在源码中找到答案。

相关文章
相关标签/搜索