理解 iOS 和 macOS 的内存管理

在 iOS 和 macOS 应用的开发中,不管是使用 Objective-C 仍是使用 swift 都是经过引用计数策略来进行内存管理的,可是在平常开发中80%(这里,我瞎说的,8020 原则嘛😆)以上的状况,咱们不须要考虑内存问题,由于 Objective-C 2.0 引入的自动引用计数(ARC)技术为开发者们自动的完成了内存管理这项工做。ARC 的出现,在必定程度上拯救了当时刚入门的 iOS 程序员们,若是是没有接触过内存管理的开发者,在第一次遇到僵尸对象时必定是吓得发抖😱😱😱My Brains~。可是 ARC 只是在代码层面上自动添加了内存管理的代码,并不能真正的自动内存管理,以及一些高内存消耗的特殊场景咱们必需要进行手动内存管理,因此理解内存管理是每个 iOS 或者 macOS 应用开发者的必备能力。程序员

本文将会介绍 iOS 和 macOS 应用开发过程当中,如何进行内存管理,以及介绍一些内存管理使用的场景,帮助你们解决内存方面的问题,本文将会重点介绍内存管理的逻辑、思路,而不是相似教你分分钟手写 weak 的实现,之类的问题,毕竟你们通常拧螺丝比较多,至于✈️🚀🛸的制造技艺嘛,仍是要靠万能的 Google 了。算法

本文实际上是内存管理的起点,而不是结束,各位 iOS 大佬们确定会发现不少东西在本文中是找不到的,由于这里的内容很是基础,只是帮助初学 iOS 的同窗们可以快速理解如何管理内存而写的。编程

什么是内存管理

不少人接触到内存管理能够追溯到大学时候的 C 语言程序设计课程,在大学中为数很少的实践型语言课程中相信 C 语言以及 C 语言中的指针是不少人的噩梦,而且这个噩梦延续到了 C++,固然这个是后话了。因此 Java 之类的,拥有垃圾回收机制的语言,也就慢慢的变得愈来愈受欢迎(大雾🤪🤪🤪)。swift

内存管理基本原则:bash

在须要的时候分配内存,在不须要的时候释放内存

这里来一段简单的 C 代码~微信

#define BUFFER_SIZE 128

void dosth() {
    char *some_string = malloc(BUFFER_SIZE);
    // 对 some_string 作各类操做
    free(some_string);
}
复制代码

这么一句话看起来彷佛不是很复杂,可是光这一个内存管理,管得无数英雄尽折腰啊,由于实际的代码并不会像上面那么简单,好比上面我要把字符串 some_string 返回出来的话要怎么办呢?(我不会回答你的👻)markdown

iOS 的内存管理

内存引用计数(Reference Counting,RC)以及 MRC

Objective-C 和 Swift 的内存管理策略都是引用计数,什么是引用计数呢?下面是 wiki 上摘抄而来的内容:多线程

引用计数是计算机编程语言中的一种内存管理技术,是指将资源(能够是对象内存磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术能够实现自动资源管理的目的。同时引用计数还能够指使用引用计数技术回收未使用资源的垃圾回收算法。app

当建立一个对象的实例并在堆上申请内存时,对象的引用计数就为1,在其余对象中须要持有这个对象时,就须要把该对象的引用计数加1,须要释放一个对象时,就将该对象的引用计数减1,直至对象的引用计数为0,对象的内存会被马上释放。框架

来源:zh.wikipedia.org/wiki/引用计数

彷佛有点抽象,这里使用 setter 方法的经典实现做为例子咱们来看下代码~

- (void)setSomeObject:(NSObject *aSomeObject) {
	if (_someObject != aSomeObject) {
		id oldValue = _someObject;
		_someObject = [aSomeObject retain];  // aSomeObject retain count +1
		[oldValue release];  // oldValue retain count -1
	}
}
复制代码

接下来咱们图解下这部分代码,图中,矩形为变量(指针),圆圈为实际对象,剪头表示变量指向的对象

1

2

3

4

上面的写法是 MRC 时代的经典方式,这里就很少说了,由于本文的目的是让你们理解 ARC 下的内存管理。

人工内存管理时代 —— Manual Reference Counting(MRC)

人工管理内存引用计数的方法叫作 Manual Reference Counting(MRC),在上一节的最后,咱们已经看到了内存管理的一些些代码,也看到了内存管理时发生了一些什么,由于 MRC 是 ARC 的基础,为了更好地理解 ARC,下面是我对 iOS,macOS 下内存管理的总结:

对象之间存在持有关系,是否被持有,决定了对象是否被销毁

也就是说,对于引用计数的内存管理,最重要的事情是理清楚对象之间的持有关系,而不关注实际的引用数字,也就是逻辑关系清楚了,那么实际的引用数也就不会出问题了。

例子 这里引用《Objective-C 高级编程》里面办公室的灯的例子,不过咱们稍微改改

  1. 自习室有一个灯,灯能够建立灯光,老师要求你们节约用电,只有在有人须要使用的时候才打开灯
  2. 同窗 A 来看书,他打开了灯(建立灯光) —— A 持有灯光
  3. 同窗 B,C,D 也来看书,他们也须要灯光 —— B,C,D 分别持有灯光
  4. 这时候 A,B,C 回宿舍了,他们不须要开灯了 —— A,B,C 释放了灯光
  5. 因为这时候 D 还须要灯光,因此灯一直是打开的 —— D 依然持有灯光
  6. 当 D 离开自习室时 —— D 释放了灯光
  7. 这时候自习室里面已经没有人须要灯光了,因而灯光被释放了(灯被关了)

上面的例子“灯光”就是咱们的被持有的对象,同窗们是持有“灯光”的对象,在这个场景,只要咱们理清楚谁持有了“灯光”,那么咱们就能完美的控制“灯光”,不至于没人的时候“灯光”一直存在致使浪费电(内存泄漏),也不至于有同窗须要“灯光”的时候“灯光”被释放。

这里看上去很简单,可是实际项目中将会是这样的场景不断的叠加,从而产生很是复杂的持有关系。例子中的同窗 A,B,C,D,自习室以及灯也是被其余对象持有的。因此对于最小的一个场景,咱们再来一遍:

对象之间存在持有关系,是否被持有,决定了对象是否被销毁

创造力的解放 —— Automatic Reference Counting(ARC)

可是平时你们会发现历来没用过 retainrelease 之类的函数啊?特别是刚入门的同窗,CoreFoundation 也没有使用过就更纳闷了

缘由很简单,由于这个时代咱们用上了 ARC,ARC 号称帮助程序员管理内存,而不少人曲解了“帮助”这个词,在布道的时候都会说:

ARC 已是自动内存管理了,咱们不须要管理内存

这是一句误导性的话,ARC 只是帮咱们在代码中他能够推断的部分,自动的添加了 retainrelease 等代码,可是并不表明他帮咱们管理内存了,实际上 ARC 只是帮咱们省略了部分代码,在 ARC 没法推断的部分,是须要咱们告诉 ARC 如何管理内存的,因此就算是使用 ARC,本质依然是开发者本身管理内存,只是 ARC 帮咱们把简单状况搞定了而已

可是,就算是 ARC 仅仅帮咱们把简单的状况搞定了,也很是大的程度上解放了你们的创造力、生产力,由于毕竟不少时候内存管理代码都是会被漏写的,而且因为漏写的时候不必定会发现问题,而是随着程序运行才会出现问题,在开发后期解决起来其实挺麻烦的

ARC 下的内存管理

那么咱们来讲说 ARC 中如何进行内存管理,固然核心仍是这句话:对象之间存在持有关系,是否被持有,决定了对象是否被销毁,固然咱们补充一句话:ARC 中的内存管理,就是理清对象之间的持有关系

strongweak

在上面一节中,其实你们应该发现只写了 retain,是由于 MRC 的时代只有 retainreleaseautorelease 这几个手动内存管理的函数。而 strongweak__weak 之类的关键字是 Objective-C 2.0 跟着 ARC 一块儿引入的,能够认为他们就是 ARC 时代的内存管理代码

对于属性 strongweakassigncopy 告诉 ARC 如何构造属性对应变量的 setter 方法,对于内存管理的意义来讲,就是告诉编译器对象属性和对象之间的关系,也就是说平时开发过程当中,一直在使用的 strongweak 其实就是在作内存管理,只是大部分时间你们没有意识到而已

  • strong:设置属性时,将会持有(retain)对象
  • weak:设置属性时,不会持有对象,而且在对象被释放时,属性值将会被设置为 nil
  • assign:设置属性时,不会持有对象(仅在属性为基本类型时使用,由于基本类型不是对象,不存在释放)
  • copy:设置属性时,会调用对象的 copy 方法获取对象的一个副本并持有(对于不可变类型很是有用)

通常状况下,咱们都会使用 strong 来描述一个对象的属性,也就是大部分场景下,对象都会持有他的属性,那么下面看下不会持有的状况

属性描述的场景 —— delegate 模式

这里用经典的 UITableViewDelegateUITableViewDataSource 来进行举例

UITableView 的 delegate 和 datasource 应该是学习 iOS 开发过程当中最先接触到的 iOS 中的 delegate 模式 在不少的的例子中,教导咱们本身开发的对象,使用的 delegate 的属性要设置为 weak 的,可是不多有说为何(由于循环引用),更少有人会说为何会产生循环引用,接下来这里用 UITableView 的来详解下

先看 UITableView 中的定义

@interface UITableView : UIScrollView <NSCoding, UIDataSourceTranslating>
// Other Definations ...
@property (nonatomic, weak, nullable) id <UITableViewDataSource> dataSource;
@property (nonatomic, weak, nullable) id <UITableViewDelegate> delegate;
// Other Definations ...
@end
复制代码

接下来看下 UITableViewController 中通常的写法

@interface XXXTableViewController : UITableViewController

@property (nonatomic, strong) UITableView *tableView;

@end

@implementation XXXTableViewController()

- (void)viewDidLoad {
	[super viewDidLoad];
	self.tableView.delegate = self;
	self.tableView.dataSource = self;
}

@end
复制代码

下面用一个图梳理一下持有关系

持有关系

图上有三个对象关系

  1. controller 持有 tableViewstrong 属性
  2. tableView 没有持有 conntrollerweak 属性
  3. 其余对象持有 controllerstrong 属性

那么当第三个关系被打破时,也就是没有对象持有 controller 了(发生 [controller release],这时候 controller 会释放他全部的内存,发生下面的事情:

  1. 其余对象调用 [controller release],没有对象持有 controllercontroller 开始释放内存(调用 dealloc
  2. [tableView release],没有对象持有 tableView 内存被释放
  3. controller 内存被释放

由于 weak 属性不会发生持有关系,因此上面过程完成后,都没有任何对象持有 tableViewcontroller 因而都被释放

假设上面对象关系中的 2 变为 tableView 持有 conntrollerstrong 属性

那么当第三个关系被打破时,也就是没有对象持有 controller 了(发生 [controller release],这时候 controller 会释放他全部的内存,发生下面的事情:

  • 其余对象调用 [controller release]tableView 依然持有 controllercontroller 不会释放内存(不会调用 dealloc

这样,tableViewcontroller 互相持有,可是没有任何对象在持有他们,可是他们不会被释放,由于都有一个对象持有着他们,因而内存泄漏,这种状况是一种简单的循环引用

因此,这就是为何咱们写的代码若是会使用到 delegate 模式,须要将 delegate 的属性设置为 weak,可是从上面例子咱们能够理解到,并非 delegate 须要 weak 而是由于出现了 delegate 和使用 delegate 的对象互相持有(循环引用),那么若是咱们的代码中不会出现循环引用,那么使用 weak 反而会出错(delegate 被过早的释放),不过这种时候每每有其余对象会持有 delegate

上面其实只描述了最简单的循环引用场景,在复杂的场景中,可能会有不少个对象依次持有直到循环,面对各类各样复杂的场景,本文认为解决内存问题的方法都是,针对每一个对象,每一个类,理清他们之间的持有关系,也就是:

对象之间存在持有关系,是否被持有,决定了对象是否被销毁,ARC 中的内存管理,就是理清对象之间的持有关系

__weak__strong

strongweak 是在设置属性的时候使用的,__weak__strong 是用于变量的,这两个关键字在开发的过程当中不会频繁的用到,是由于若是没有指定,那么变量默认是经过 __strong 修饰的,不过当咱们须要使用这两个关键字的时候,那么也将是咱们面对坑最多的状况的时候 —— block 的使用

  • __strong:变量默认的修饰符,对应 property 的 strong,会持有(这里能够认为是当前代码块持有)变量,这里的持有至关于在变量赋值后调用 retain 方法,在代码块结束时调用 release 方法
  • __weak:对应 property 的 weak,一样在变量被释放后,变量的值会变成 nil
变量描述符场景 —— block 的循环引用

下面咱们来看个日常常常会遇到的场景,考虑下面的代码:

// 文件 Dummy.h
@interface Dummy : NSObject

@property (nonatomic, strong) void (^do_block)();

- (void)do_sth:(NSString *)msg;

@end

// 文件 Dummy.m
@interface Dummy()
@end

@implementation Dummy

- (void)do_sth:(NSString *)msg {
    NSLog(@"Enter do_sth");
    self.do_block = ^() {
        [self do_sth_inner:msg];
    };
    self.do_block();
    NSLog(@"Exit do_sth");
}

- (void)do_sth_inner:(NSString *)msg {
    NSLog(@"do sth inner: %@", msg);
}

@end

// 文件 AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    Dummy *dummy = [[Dummy alloc] init];
    [dummy do_sth:@"hello"];
    return YES;
}
复制代码

新建一个空白的单页面 iOS 应用,这里你们必定知道结果了,在控制台会输出这样的内容:

2018-11-15 22:56:34.281346+0800 iOSPlayground[42178:5466855] Enter do_sth
2018-11-15 22:56:34.281445+0800 iOSPlayground[42178:5466855] do sth inner: hello
2018-11-15 22:56:34.281536+0800 iOSPlayground[42178:5466855] Exit do_sth
复制代码

固然相信你们已经看出问题来了,上面的代码会形成循环引用,固然不少时候咱们在学习写 iOS 代码的时候,都会有人教导过咱们 block 里面的 self 是会存在循环引用的(如上代码的结果),必需要使用 __weak,那么为何呢?这里依然回到上面的内存管理原则,咱们来梳理一下持有关系,首先这里有一个基础知识,那就是 block 是一个对象,而且他会持有全部他捕获的变量,这里咱们来看下内存持有关系:

持有关系

一样,咱们来分析下这个持有关系

  1. self 对象持有了 do_block 对象
  2. 因为 selfdo_block 中使用了,因此 do_block 的代码区块持有了 self
  3. 其余对象(这里是 AppDelegate 实例)经过变量的方式持有对外的 dummy 对象

那么在咱们的代码执行到 -application:didFinishLaunchingWithOptions: 最后一行的时候,因为代码块的结束,ARC 将会对块内产生的对象分别调用 release 释放对象,这时候,上面 3 的持有关系被打破了

可是,因为 1,2 这两条持有关系存在,因此不管是 self 对象,仍是 do_sth block 他们都至少被一个对象所持有,因此,他们没法被释放,而且也没法被外界所访问到,造成了循环引用致使内存泄漏,经过 Xcode 提供的内存图(Debug Memeory Graph)咱们也能够看到,这一现象:

内存图

那么这里的解决方法就是,进行下面的修改:

- (void)do_sth:(NSString *)msg {
    NSLog(@"Enter do_sth");
    __weak typeof(self) weakself = self;
    self.do_block = ^() {
        [weakself do_sth_inner:msg];
    };
    self.do_block();
    NSLog(@"Exit do_sth");
}
复制代码

这样打破了上面持有关系 2 中,do_block 持有 self 的问题,这样就和上面描述 delegate 的场景同样了

变量描述符场景 —— block 的循环引用 2

接下来看下另一个循环引用的场景,Dummy 类的定义不变,使用方法作一些调整:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    Dummy *dummy = [[Dummy alloc] init];    
    dummy.do_block = ^{
        [dummy do_sth_inner:@"hello2"];
    };
    dummy.do_block();
    return YES;
}
复制代码

奇怪,这里没有 self 了啊,为何依然循环引用了啊?接着继续看持有关系图:

持有关系

是否是和上一个场景很像?由于就是同样的,只是一个视野在类的内部,另外一个视野在类的外部,在类的内部那就是 selfdo_block 互相持有,造成循环引用;在类的外部那就是 dummydo_block 互相持有,造成循环应用

一点我的经验

实际项目确定不会是本文中这么明显简单的场景,可是再多复杂的场景确定是这些简单的场景不断的嵌套组合而成,因此保证代码内存没有问题的最好的方法是每次遇到须要处理内存场景时,仔细分析对象间的持有关系,也就是保证组成复杂场景的每一个小场景都没有问题,那么基本就不会出现问题了,对于出现内存管理出现问题的状况,通常咱们都能定位到是某一部分代码内存泄漏了,那么直接分析那部分代码的持有关系是否正确

iOS macOS 开发中的内存管理不要在乎引用计数,引用计数是给运行时看的东西,做为人类咱们须要在乎对象间的持有关系,理清持有关系那么就代表引用计数不会有问题

结语

到此对于内存管理的思路算是结束了,可是就像本文一开始所说的,这里并非结束而是开始,接下来建议你们在有了必定经验后能够再去深刻了解下面的内容:

  • Core Foundation 框架的内存管理,没有 ARC 的眷顾
  • Core Foundation 框架和 Objective-C 的内存交互 —— Toll-Free Bridging,ARC 和 CF 框架的桥梁
  • Objective-C 高级编程 —— 《iOS 与 OS X 多线程和内存管理》,我从这本书里面收益良多
  • Swift 下的内存管理,分清 weakunowned 有什么区别,逻辑依然是理清持有关系
  • C 语言入门,Objective-C 源自于 C 语言,全部 C 语言的招式在 Objective-C 中都好用,在某些特殊场景会一定会用到

最后欢迎你们订阅个人微信公众号 Little Code

Little Code

  • 公众号主要发一些开发相关的技术文章
  • 谈谈本身对技术的理解,经验
  • 也许会谈谈人生的感悟
  • 本人不是很高产,可是力求保证质量和原创
相关文章
相关标签/搜索