唐巧:理解 iOS 的内存管理

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友 微信微信
查看查看337 回复回复3 收藏收藏 分享淘帖 转播转播 分享分享 微信
查看: 337|回复: 3
收起左侧

唐巧:理解 iOS 的内存管理

[复制链接]
电音之王 发表于 2016-7-27 17:13:26 | 显示全部楼层 |阅读模式
快来登录
获取优质的苹果资讯内容
收藏热门的iOS等技术干货
拷贝下载Swift Demo源代码
订阅梳理好了的知识点专辑




1、远古时代的故事

那些经历过手工管理内存(MRC)时代的人们,一定对 iOS 开发中的内存管理记忆犹新。那个时候大约是 2010 年,国内 iOS 开发刚刚兴起,tinyfool 大叔的大名已经如雷贯耳,而我还是一个默默无闻的刚毕业的小子。那个时候的 iOS 开发过程是这样的:

我们先写好一段 iOS 的代码,然后屏住呼吸,开始运行它,不出所料,它崩溃了。在 MRC 时代,即使是最牛逼的 iOS 开发者,也不能保证一次性就写出完美的内存管理代码。于是,我们开始一步一步调试,试着打印出每个怀疑对象的引用计数(Retain Count),然后,我们小心翼翼地插入合理的 retain 和 release 代码。经过一次又一次的应用崩溃和调试,终于有一次,应用能够正常运行了!于是我们长舒一口气,露出久违的微笑。


是的,这就是那个年代的 iOS 开发者,通常情况下,我们在开发完一个功能后,需要再花好几个小时,才能把引用计数管理好。

苹果在 2011 年的时候,在 WWDC 大会上提出了自动的引用计数(ARC)。ARC 背后的原理是依赖编译器的静态分析能力,通过在编译时找出合理的插入引用计数管理代码,从而彻底解放程序员。

在 ARC 刚刚出来的时候,业界对此黑科技充满了怀疑和观望,加上现有的 MRC 代码要做迁移本来也需要额外的成本,所以 ARC 并没有被很快接受。直到 2013 年左右,苹果认为 ARC 技术足够成熟,直接将 macOS(当时叫 OS X)上的垃圾回收机制废弃,从而使得 ARC 迅速被接受。

2014 年的 WWDC 大会上,苹果推出了 Swift 语言,而该语言仍然使用 ARC 技术,作为其内存管理方式

为什么我要提这段历史呢?就是因为现在的 iOS 开发者实在太舒服了,大部分时候,他们根本都不用关心程序的内存管理行为。但是,虽然 ARC 帮我们解决了引用计数的大部分问题,一些年轻的 iOS 开发者仍然会做不好内存管理工作。他们甚至不能理解常见的循环引用问题,而这些问题会导致内存泄漏,最终使得应用运行缓慢或者被系统终止进程。

所以,我们每一个 iOS 开发者,需要理解引用计数这种内存管理方式,只有这样,才能处理好内存管理相关的问题。




2、什么是引用计数

引用计数(Reference Count)是一个简单而有效的管理对象生命周期的方式。当我们创建一个新对象的时候,它的引用计数为 1,当有一个新的指针指向这个对象时,我们将其引用计数加 1,当某个指针不再指向这个对象是,我们将其引用计数减 1,当对象的引用计数变为 0 时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。由于引用计数简单有效,除了 Objective-C 和 Swift 语言外,微软的 COM(Component Object Model )、C++11(C++11 提供了基于引用计数的智能指针 share_prt)等语言也提供了基于引用计数的内存管理方式。

唐巧:理解 iOS 的内存管理 1

唐巧:理解 iOS 的内存管理 - 敏捷大拇指 - 唐巧:理解 iOS 的内存管理 1


为了更形象一些,我们再来看一段 Objective-C 的代码。新建一个工程,因为现在默认的工程都开启了自动的引用计数 ARC(Automatic Reference Count),我们先修改工程设置,给 AppDelegate.m 加上 -fno-objc-arc 的编译参数(如下图所示),这个参数可以启用手工管理引用计数的模式。

唐巧:理解 iOS 的内存管理 2

唐巧:理解 iOS 的内存管理 - 敏捷大拇指 - 唐巧:理解 iOS 的内存管理 2


然后,我们在中输入如下代码,可以通过 Log 看到相应的引用计数的变化。

[Objective-C] 纯文本查看 复制代码
- (BOOL)application:(UIApplication *)application 
       didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{    
    NSObject *object = [[NSObject alloc] init];    
    NSLog(@"Reference Count = %u", [object retainCount]);
    NSObject *another = [object retain];
    NSLog(@"Reference Count = %u", [object retainCount]);
    [another release];
    NSLog(@"Reference Count = %u", [object retainCount]);
    [object release];    // 到这里时,object 的内存被释放了
    return YES;
}


运行结果:

[Objective-C] 纯文本查看 复制代码
Reference Count = 1
Reference Count = 2
Reference Count = 1


对 Linux 文件系统比较了解的同学可能发现,引用计数的这种管理方式类似于文件系统里面的硬链接。在 Linux 文件系统中,我们用 ln 命令可以创建一个硬链接(相当于我们这里的 retain),当删除一个文件时(相当于我们这里的 release),系统调用会检查文件的 link count 值,如果大于 1,则不会回收文件所占用的磁盘区域。直到最后一次删除前,系统发现 link count 值为 1,则系统才会执行直正的删除操作,把文件所占用的磁盘区域标记成未用。




3、我们为什么需要引用计数

从上面那个简单的例子中,我们还看不出来引用计数真正的用处。因为该对象的生命期只是在一个函数内,所以在真实的应用场景下,我们在函数内使用一个临时的对象,通常是不需要修改它的引用计数的,只需要在函数返回前将该对象销毁即可。

引用计数真正派上用场的场景是在面向对象的程序设计架构中,用于对象之间传递和共享数据。我们举一个具体的例子:

假如对象 A 生成了一个对象 M,需要调用对象 B 的某一个方法,将对象 M 作为参数传递过去。在没有引用计数的情况下,一般内存管理的原则是 “谁申请谁释放”,那么对象 A 就需要在对象 B 不再需要对象 M 的时候,将对象 M 销毁。但对象 B 可能只是临时用一下对象 M,也可能觉得对象 M 很重要,将它设置成自己的一个成员变量,那这种情况下,什么时候销毁对象 M 就成了一个难题。

唐巧:理解 iOS 的内存管理 3

唐巧:理解 iOS 的内存管理 - 敏捷大拇指 - 唐巧:理解 iOS 的内存管理 3


对于这种情况,有一个暴力的做法,就是对象 A 在调用完对象 B 之后,马上就销毁参数对象 M,然后对象 B 需要将参数另外复制一份,生成另一个对象 M2,然后自己管理对象 M2 的生命期。但是这种做法有一个很大的问题,就是它带来了更多的内存申请、复制、释放的工作。本来一个可以复用的对象,因为不方便管理它的生命期,就简单的把它销毁,又重新构造一份一样的,实在太影响性能。如下图所示:

唐巧:理解 iOS 的内存管理 4

唐巧:理解 iOS 的内存管理 - 敏捷大拇指 - 唐巧:理解 iOS 的内存管理 4


我们另外还有一种办法,就是对象 A 在构造完对象 M 之后,始终不销毁对象 M,由对象 B 来完成对象 M 的销毁工作。如果对象 B 需要长时间使用对象 M,它就不销毁它,如果只是临时用一下,则可以用完后马上销毁。这种做法看似很好地解决了对象复制的问题,但是它强烈依赖于 AB 两个对象的配合,代码维护者需要明确地记住这种编程约定。而且,由于对象 M 的申请是在对象 A 中,释放在对象 B 中,使得它的内存管理代码分散在不同对象中,管理起来也非常费劲。如果这个时候情况再复杂一些,例如对象 B 需要再向对象 C 传递对象 M,那么这个对象在对象 C 中又不能让对象 C 管理。所以这种方式带来的复杂性更大,更不可取。

唐巧:理解 iOS 的内存管理 5

唐巧:理解 iOS 的内存管理 - 敏捷大拇指 - 唐巧:理解 iOS 的内存管理 5


所以引用计数很好的解决了这个问题,在参数 M 的传递过程中,哪些对象需要长时间使用这个对象,就把它的引用计数加 1,使用完了之后再把引用计数减 1。所有对象都遵守这个规则的话,对象的生命期管理就可以完全交给引用计