内存管理系列
# 什么是内存管理
是指软件 (opens new window)运行时对计算机内存 (opens new window)资源的分配和使用的技术。其最主要的目的是如何高效、快速的分配,并且在适当的时候释放和回收内存资源
体现在iOS的内存管理,就是对对像引用计数的管理
# Objective-C 中的内存分配
在 Objective-C 中,对象通常是使用 alloc 方法在堆上创建的。 [NSObject alloc] 方法会在对堆上分配一块内存,按照NSObject的内部结构填充这块儿内存区域。
一旦对象创建完成,就不可能再移动它了。因为很可能有很多指针都指向这个对象,这些指针并没有被追踪。因此没有办法在移动对象的位置之后更新全部的这些指针。
# MRC 与 ARC
Objective-C中提供了两种内存管理机制:MRC(MannulReference Counting)和 ARC(Automatic Reference Counting),分别提供对内存的手动和自动管理,来满足不同的需求。现在苹果推荐使用 ARC 来进行内存管理。
ARC和MRC是iOS中两种不同的内存管理机制。ARC是自动引用计数,由编译器在编译时自动插入内存管理代码,而MRC是手动引用计数,开发者需要手动管理对象的引用计数。
什么是引用计数,原理是什么?
引用计数可以有效的管理对象的生命周期。当我们创建一个新的对象的时候,它的引用计数为1,当有一个新的指针指向这个对象的时候,我们将其引用计数加1,当某个指针不再指向这个对象时,我们将其引用计数减1,当对象的引用计数变为0时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象摧毁,回首内存。
由于引用计数简单有效,出来oc语言外,微软的COM(Component Object Model)、C++11(C++11提供了引用计数的智能指针share_prt)等语言也提供了基于引用计数的内存管理方式。
# 为什么需要引用计数?
引用计数真正派上用场的场景是在面向对象的程序设计架构中,用于对象之间的传递和共享数据。我们举个栗子看一下:
假如对象A生成了一个对象M,需要调用对象B的某一个方法,将对象M作为参数传递过去。在没有引用计数的情况下,一般内存管理的原则是“谁申请谁释放”,那么对象A就需要在对象B不再需要对象M的时候,将对象M摧毁掉。但对象B可能只是临时用一下对象M,也可能觉得对象M很重要,将它设置成自己的一个变量,在这种情况下,什么时候摧毁对象M就成为一个难题了
方案一:
这种情况,有一种很黄很暴力的做法,就是对象A在调用完对象B之后,马上销毁参数对象M,然后对象B需要将参数另外复制一份,生成另一个对象M2,然后自己管理对象M2的生命期。但是这种做法有一个很大的问题,就是它会带来更多的内存申请、复制、释放的工作。本来一个可以复用的对象,因为不方便管理它的生命期,就简单的把它摧毁,又重新构造一份一样,真的影响性能。
方案二:
我们还有另外一种方法,就是对象A在构造完对象M之后,始终不销毁对象M,由对象B来完成对象M的销毁工作。如果对象B需要长时间使用对象M,就不销毁它,如果只是临时用一下,就可以马上销毁。这种做法看似很好的解决了对象复制的问题,但是它强烈的依赖于A、B两个对象的配合,代码维护者需要明确的记住这种编程的约定。而且,由于对象M的申请是在对象A中,而释放在对象B中,使得它的内存管理代码分布在两个对象中,管理起来也是废了老逼劲了。
如果这个时候,情况在复杂一些,举个恶心的栗子,对象B需要再向对象C传递对象M,那么这个对象在对象C中又不能让对象C管理。所以这种方式带来的复杂性更加大,不可取。 所以,bb了这么多,就想说引用计数很好的解决了这个问题,在参数M传递的过程中,哪些对象需要长时间使用这个对象,就把它的引用计数加1,使用完了再把引用计数减1.所有的对象都遵循这个规则的话,对象的生命期的管理工作就完全交给了引用计数 了,成功甩锅。我们也可以很方便的享受到共享对象带来的快感和好处。
所以,bb了这么多,就想说引用计数很好的解决了这个问题,在参数M传递的过程中,哪些对象需要长时间使用这个对象,就把它的引用计数加1,使用完了再把引用计数减1.所有的对象都遵循这个规则的话,对象的生命期的管理工作就完全交给了引用计数 了,成功甩锅。我们也可以很方便的享受到共享对象带来的快感和好处。
# 不要向已经释放的对象发送消息
有的萌萌哒的小伙伴想测试一下当对象释放的时候,其retainCount是否变成了0,他们的代码如下:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSObject * object = [[NSObject alloc]init];
NSLog(@"Reference Count = %lu",[object retainCount]);
[object release];
NSLog(@"Reference Count = %lu",[object retainCount]);
return YES;
}
2
3
4
5
6
7
8
9
10
11
12
13
如果真的这么做了,你得到的结果可能 是这样的:
ReferenceCountText[26104:15162772] Reference Count = 1 ReferenceCountText[26104:15162772] Reference Count = 1
为什么第二次的输出不是0 尼?咋回事儿,这是因为该对象的内存已经被回收了,而我们向一个已经回收的对像发了一个retainCount消息,所以它的输出结果应该是不确定的,如果该对象所占的内存被复用了,那么就可能直接崩。这就是我为什么说上面的结果是可能出现的。
那为什么在这个对象被回收之后,这个不确定的值是1而不是0呢,刨根问底拦不住,让我们好好刨一刨,这是因为当最后一次执行release时,系统知道马上就要回收内存了,就没有必要在将retainCount减1了,因为不管减不减1,该对象都肯定被释放,没跑了,肯定是得弄它。而在对象被回收后,它所有的内存区域,包括retainCount变的就是毫无意义。不将这个引用计数从1变0 ,可以减少一次内存操作,加速对象的回收。
# MRC
对象操作的四个类别
| 对象操作 | OC中对应的方法 | 对应的 retainCount 变化 |
|---|---|---|
| 生成并持有对象 | alloc/new/copy/mutableCopy等 | +1 |
| 持有对象 | retain | +1 |
| 释放对象 | release | -1 |
| 废弃对象 | dealloc | - |
# 四个法则
- 自己生成的对象,自己持有。
- 非自己生成的对象,自己也能持有。
- 不在需要自己持有对象的时候,释放。
- 非自己持有的对象无需释放。
点击查看
/*
* 自己生成并持有该对象
*/
id obj0 = [[NSObeject alloc] init];
id obj1 = [NSObeject new];
/*
* 持有非自己生成的对象
*/
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有
[obj retain]; // 自己持有对象
/*
* 不在需要自己持有的对象的时候,释放
*/
id obj = [[NSObeject alloc] init]; // 此时持有对象
[obj release]; // 释放对象
/*
* 指向对象的指针仍就被保留在obj这个变量中
* 但对象已经释放,不可访问
*/
/*
* 非自己持有的对象无法释放
*/
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有
[obj release]; // ~~~此时将运行时crash 或编译器报error~~~ 非 ARC 下,调用该方法会导致编译器报 issues。此操作的行为是未定义的,可能会导致运行时 crash 或者其它未知行为
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
其中 非自己生成的对象,且该对象存在,但自己不持有 这个特性是使用autorelease来实现的,示例代码如下:
- (id) getAObjNotRetain {
id obj = [[NSObject alloc] init]; // 自己持有对象
[obj autorelease]; // 取得的对象存在,但自己不持有该对象
return obj;
}
2
3
4
5
autorelease 使得对象在超出生命周期后能正确的被释放(通过调用release方法)。在调用 release 后,对象会被立即释放,而调用 autorelease 后,对象不会被立即释放,而是注册到 autoreleasepool 中,经过一段时间后 pool结束,此时调用release方法,对象被释放。
在MRC的内存管理模式下,与对变量的管理相关的方法有:retain, release 和 autorelease。retain 和 release 方法操作的是引用记数,当引用记数为零时,便自动释放内存。并且可以用 NSAutoreleasePool 对象,对加入自动释放池(autorelease 调用)的变量进行管理,当 drain 时回收内存。
# ARC
ARC 是苹果引入的一种自动内存管理机制,会根据引用计数自动监视对象的生存周期,实现方式是在编译时期自动在已有代码中插入合适的内存管理代码以及在 Runtime 做一些优化。
# 变量标识符
在ARC中与内存管理有关的变量标识符,有下面几种:
- __strong
- __weak
- __unsafe_unretained
- __autoreleasing __strong 是默认使用的标识符。只有还有一个强指针指向某个对象,这个对象就会一直存活。
__weak 声明这个引用不会保持被引用对象的存活,如果对象没有强引用了,弱引用会被置为 nil
__unsafe_unretained 声明这个引用不会保持被引用对象的存活,如果对象没有强引用了,它不会被置为 nil。如果它引用的对象被回收掉了,该指针就变成了野指针。
__autoreleasing 用于标示使用引用传值的参数(id *),在函数返回时会被自动释放掉。
变量标识符的用法如下:
Number* __strong num = [[Number alloc] init];
注意 __strong 的位置应该放到 * 和变量名中间,放到其他的位置严格意义上说是不正确的,只不过编译器不会报错。
# 引用循环
当两个对象互相持有对方的强引用,并且这两个对象的引用计数都不是0的时候,便造成了引用循环。
要想破除引用循环,可以从以下几点入手:
- 注意变量作用域,使用 autorelease 让编译器来处理引用
- 使用弱引用(weak)
- 当实例变量完成工作后,将其置为nil
# Autorelease Pool
Autorelase Pool 提供了一种可以允许你向一个对象延迟发送release消息的机制。当你想放弃一个对象的所有权,同时又不希望这个对象立即被释放掉(例如在一个方法中返回一个对象时),Autorelease Pool 的作用就显现出来了。
所谓的延迟发送release消息指的是,当我们把一个对象标记为autorelease时:
NSString* str = [[[NSString alloc] initWithString:@"hello"] autorelease];
这个对象的 retainCount 会+1,但是并不会发生 release。当这段语句所处的 autoreleasepool 进行 drain 操作时,所有标记了 autorelease 的对象的 retainCount 会被 -1。即 release 消息的发送被延迟到 pool 释放的时候了。
在 ARC 环境下,苹果引入了 @autoreleasepool 语法,不再需要手动调用 autorelease 和 drain 等方法。
# Autorelease Pool 的用处
在 ARC 下,我们并不需要手动调用 autorelease 有关的方法,甚至可以完全不知道 autorelease 的存在,就可以正确管理好内存。因为 Cocoa Touch 的 Runloop 中,每个 runloop circle 中系统都自动加入了 Autorelease Pool 的创建和释放。
当我们需要创建和销毁大量的对象时,使用手动创建的 autoreleasepool 可以有效的避免内存峰值的出现。因为如果不手动创建的话,外层系统创建的 pool 会在整个 runloop circle 结束之后才进行 drain,手动创建的话,会在 block 结束之后就进行 drain 操作。详情请参考苹果官方文档。一个普遍被使用的例子如下:
for (int i = 0; i < 100000000; i++)
{
@autoreleasepool
{
NSString* string = @"ab c";
NSArray* array = [string componentsSeparatedByString:string];
}
}
2
3
4
5
6
7
8
如果不使用 autoreleasepool ,需要在循环结束之后释放 100000000 个字符串,如果 使用的话,则会在每次循环结束的时候都进行 release 操作。
# Autorelease Pool 进行 Drain 的时机
如上面所说,系统在 runloop 中创建的 autoreleaspool 会在 runloop 一个 event 结束时进行释放操作。我们手动创建的 autoreleasepool 会在 block 执行完成之后进行 drain 操作。需要注意的是:
- 当 block 以异常(exception)结束时,pool 不会被 drain
- Pool 的 drain 操作会把所有标记为 autorelease 的对象的引用计数减一,但是并不意味着这个对象一定会被释放掉,我们可以在 autorelease pool 中手动 retain 对象,以延长它的生命周期(在 MRC 中)。
# main.m 中 Autorelease Pool 的解释
大家都知道在 iOS 程序的 main.m 文件中有类似这样的语句:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
2
3
4
5
在面试中问到有关 autorelease pool 有关的知识也多半会问一下,这里的 pool 有什么作用,能不能去掉之类。在这里我们分析一下。
根据苹果官方文档, UIApplicationMain 函数是整个 app 的入口,用来创建 application 对象(单例)和 application delegate。尽管这个函数有返回值,但是实际上却永远不会返回,当按下 Home 键时,app 只是被切换到了后台状态。
同时参考苹果关于 Lifecycle 的官方文档,UIApplication 自己会创建一个 main run loop,我们大致可以得到下面的结论:
- main.m 中的 UIApplicationMain 永远不会返回,只有在系统 kill 掉整个 app 时,系统会把应用占用的内存全部释放出来。
- 因为(1), UIApplicationMain 永远不会返回,这里的 autorelease pool 也就永远不会进入到释放那个阶段
- 在 (2) 的基础上,假设有些变量真的进入了 main.m 里面这个 pool(没有被更内层的 pool 捕获),那么这些变量实际上就是被泄露的。这个 autorelease pool 等于是把这种泄露情况给隐藏起来了。
- UIApplication 自己会创建 main run loop,在 Cocoa 的 runloop 中实际上也是自动包含 autorelease pool 的,因此 main.m 当中的 pool 可以认为是没有必要的。
在基于 AppKit 框架的 Mac OS 开发中, main.m 当中就是不存在 autorelease pool 的,也进一步验证了我们得到的结论。不过因为我们看不到更底层的代码,加上苹果的文档中不建议修改 main.m ,所以我们也没有理由就直接把它删掉(亲测,删掉之后不影响 App 运行,用 Instruments 也看不到泄露)。
# Autorelease Pool 与函数返回值
如果一个函数的返回值是指向一个对象的指针,那么这个对象肯定不能在函数返回之前进行 release,这样调用者在调用这个函数时得到的就是野指针了,在函数返回之后也不能立刻就 release,因为我们不知道调用者是不是 retain 了这个对象,如果我们直接 release 了,可能导致后面在使用这个对象时它已经成为 nil 了。
为了解决这个纠结的问题, Objective-C 中对对象指针的返回值进行了区分,一种叫做 retained return value,另一种叫做 unretained return value。前者表示调用者拥有这个返回值,后者表示调用者不拥有这个返回值,按照“谁拥有谁释放”的原则,对于前者调用者是要负责释放的,对于后者就不需要了。
按照苹果的命名 convention,以 alloc, copy, init, mutableCopy 和 new 这些方法打头的方法,返回的都是 retained return value,例如 [[NSString alloc] initWithFormat:],而其他的则是 unretained return value,例如 [NSString stringWithFormat:]。我们在编写代码时也应该遵守这个 convention。
我们分别在 MRC 和 ARC 情况下,分析一下两种返回值类型的区别。
# 内存管理面试题
# 1.什么是内存泄漏?
内存泄漏指动态分配内存的对象在使用完后没有被系统回收内存,导致对象始终占有着内存,属于内存管理出错, (例如一个对象或者变量使用完成后没有释放,这个对象一直占用着内存),一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
# 2. 什么是僵尸对象?
已经被销毁的对象(不能再使用的对象),内存已经被回收的对象。一个引用计数器为0对象被释放后就变为了僵尸对象;
# 3. 野指针
野指针又叫做'悬挂指针', 野指针出现的原因是因为指针没有赋值,或者指针指向的对象已经释放了, 比如指向僵尸对象;野指针可能会指向一块垃圾内存,给野指针发送消息会导致程序崩溃 比如:
NSObject *obj = [NSObject new];
[obj release]; // obj 指向的内存地址已经释放了,
obj 如果再去访问的话就是野指针错误了.
野指针错误形式在Xcode中通常表现为:Thread 1:EXC_BAD_ACCESS,因为你访问了一块已经不属于你的内存。
2
3
4
# 4.什么是空指针?
空指针不同于野指针,他是一个没有指向任何内存的指针,空指针是有效指针,值为nil,NULL,Nil,0等,给空指针发送消息不会报错,不会响应消息;
# 5. OC对象的内存管理机制?
在iOS中,使用引用计数来管理OC对象的内存
一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
内存管理的经验总结
当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
可以通过以下私有函数来查看自动释放池的情况
extern void _objc_autoreleasePoolPrint(void);
# 6. OC中有GC垃圾回收机制吗?,iPhone上GC吗?
垃圾回收(GC),就是程序中用于处理废弃不用的内存对象的机制,防止内存泄露 OC本身是支持垃圾回头得,不过只支持MAC OSX平台, iOS 平台不支持
# 7.在OC中与 Alloc 语义相反的是 release 还是 dealloc?
alloc 与 dealloc 语义相反,alloc 是创建变量,dealloc是释放变量 retain 与 release 语义相反, retain 保留一个对象,引用计数器+1, release 使引用计数器 -1;
# 8.什么是内存溢出?
当程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个int,但给它存了long才能存下的数,那就是内存溢出。
# 9.内存区域分布
在iOS开发过程中,为了合理的分配有限的内存空间,将内存区域分为五个区,由低地址向高地址分类分别是:代码区、常量区、全局静态区、堆、栈。
代码段 -- 程序编译产生的二进制的数据 常量区 -- 存储常量数据,通常程序结束后由系统自动释放 全局静态区 -- 全局区又可分为未初始化全局区:.bss段和初始化全局区:data段。全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,在程序结束后有系统释放。 堆(heap) -- 程序运行过程中,动态分配的内存 栈(stack) -- 存放局部变量,临时变量
# 10.堆区和栈取的区别
按管理方式分
- 对于栈来讲,是由系统编译器自动管理,不需要程序员手动管理
- 对于堆来讲,释放工作由程序员手动管理,不及时回收容易产生内存泄露
按分配方式分
- 堆是动态分配和回收内存的,没有静态分配的堆
- 栈有两种分配方式:静态分配和动态分配
- 静态分配是系统编译器完成的,比如局部变量的分配
- 动态分配是有alloc函数进行分配的,但是栈的动态分配和堆是不同的,它的动 态分配也由系统编译器进行释放,不需要程序员手动管理
# 11.怎么保证多人开发进行内存泄露的检查.
- 使用Analyze进行代码的静态分析
- 为避免不必要的麻烦, 多人开发时尽量使用ARC
- 使用leaks 进行内存泄漏检测
- 使用一些三方工具
# 12.block在ARC中和MRC中的用法有什么区别,需要注意什么?
对于没有引用外部变量的Block,无论在ARC还是非ARC下,类型都是 NSGlobalBlock,这种类型的block可以理解成一种全局的block,不 需要考虑作用域问题。同时,对他进行Copy或者Retain操作也是无效的
都需要应注意避免循环引用,ARC 下使用__weak 来解决,MRC下使用__Block 来解决;
# ARC 都帮我们做了什么?
LLVM + Runtime 会为我们代码自动插入 retain 和 release 以及 autorelease等代码,不需要我们手动管理
# 15.weak指针的实现原理
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。
runtime对注册的类, 会进行布局,对于weak对象会放入一个hash表中。 用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键, 在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil。
# 16.方法里有局部对象,出了方法后会立即释放吗
如果是普通的 局部对象 会立即释放 如果是放在了 autoreleasePool 自动释放池,在 runloop 迭代结束的时候释放
# 17.MRC情况下怎么做单例模式
创建单例设计模式的基本步骤 : ·
- 声明一个单件对象的静态实例,并初始化为nil。
- 创建一个类的类工厂方法,当且仅当这个类的实例为nil时生成一个该类 的实例
- 实现NScopying协议, 覆盖allocWithZone:方法,确保用户在直接分配和 初始化对象时,不会产 生另一个对象。
- 覆盖release、autorelease、retain、retainCount方法, 以此确保单例的 状态。
- 在多线程的环境中,注意使用@synchronized关键字或GCD,确保静态实 例被正确的创建和初始化。
# 18.非OC对象如何管理内存?
非OC对象,其需要手动执行释放操作例:CGImageRelease(ref),否则会造成大量的内存泄漏导致程序崩溃。其他的对于CoreFoundation框架下的某些对象或变量需要手动释放、C语言代码中的malloc等需要对应free。
# 19. CADisplayLink、NSTimer会出现的问题,以及解决办法?
- CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用
- CADisplayLink、NSTimer都是基于 runloop 实现的.runloop 会对 CADisplayLink、NSTimer进行强引用, CADisplayLink、NSTimer又 会对 target 进行引用,造成循环引用
# 解决方案1使用block
// 内部使用 WeakSelf,并在视图消失前,关闭定时器
__weak __typeof(self)weakSelf = self;
NSTimer * timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer");
}];
self.timer= timer;
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
2
3
4
5
6
7
# 解决方案2.使用代理对象(NSProxy)

点击查看
.h
// 解决循环引用问题
@interface MyProxy : NSProxy
- (instancetype)initWithObjc:(id)objc;
+ (instancetype)proxyWithObjc:(id)objc;
.m
@interface MyProxy()
@property(nonatomic,weak) id objc;
@end
@implementation MyProxy
- (instancetype)initWithObjc:(id)objc{
self.objc = objc;
return self;
}
+ (instancetype)proxyWithObjc:(id)objc{
return [[self alloc] initWithObjc:objc];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [self.objc methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
if ([self.objc respondsToSelector:invocation.selector]) {
[invocation invokeWithTarget:self.objc];
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 什么是Tagged Pointer?
- 从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储
- 在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值
- 使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中
- 当指针不够存储数据时,才会使用动态分配内存的方式来存储数据
# 22. 内存泄漏可能会出现的几种原因?
- 第一种可能:第三方框架不当使用;
- 第二种可能:block循环引用;
- 第三种可能:delegate循环引用;
- 第四种可能:NSTimer循环引用
- 第五种可能:非OC对象内存处理
- 第六种可能:地图类处理
- 第七种可能:大次数循环内存暴涨
# 23. ARC下什么样的对象由 Autoreleasepool 管理
当使用alloc/new/copy/mutableCopy开始的方法进行初始化时,会生成并持有对象(也就是不需要pool管理,系统会自动的帮他在合适位置release),不需要pool进行管理
一般类方法创建的对象需要使用Autoreleasepool进管理
# 24. 如何实现AutoreleasePool?
AutoreleasePool(自动释放池)其实并没有自身的结构,他是基于多个AutoreleasePoolPage(一个C++类)以双向链表组合起来的结构; 可以通过 push操作添加对象,pod 操作弹出对象,以及通过 release 操作释放对象;
# 25. AutoreleasePoolPage的结构?以及如何 push 和 pod 的?

调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址 调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY id *next指向了下一个能存放autorelease对象地址的区域
# 26.Autoreleasepool 与 Runloop 的关系
主线程默认为我们开启 Runloop,Runloop 会自动帮我们创建Autoreleasepool,并进行Push、Pop 等操作来进行内存管理
iOS在主线程的Runloop中注册了2个Observer
第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush() 第2个Observer 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()
# 27.子线程默认不会开启 Runloop,那出现 Autorelease 对象如何处理?不手动处理会内存泄漏吗?
在子线程你创建了 Pool 的话,产生的 Autorelease 对象就会交给 pool 去管理。如果你没有创建 Pool ,但是产生了 Autorelease 对象,就会调用 autoreleaseNoPage 方法。在这个方法中,会自动帮你创建一个 hotpage(hotPage 可以理解为当前正在使用的 AutoreleasePoolPage,如果你还是不理解,可以先看看 Autoreleasepool 的源代码,再来看这个问题 ),并调用 page->add(obj)将对象添加到 AutoreleasePoolPage 的栈中,也就是说你不进行手动的内存管理,也不会内存泄漏啦!StackOverFlow 的作者也说道,这个是 OS X 10.9+和 iOS 7+ 才加入的特性。
# 简要说一下@autoreleasePool的数据结构??
简单说是双向链表,每张链表头尾相接,有 parent、child指针
每创建一个池子,会在首部创建一个 哨兵 对象,作为标记
最外层池子的顶端会有一个next指针。当链表容量满了,就会在链表的顶端,并指向下一张表。
# iOS内存管理方式
# Tagged Pointer(小对象)
Tagged Pointer 专门用来存储小的对象,例如 NSNumber 和 NSDate
Tagged Pointer 指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free
在内存读取上有着 3 倍的效率,创建时比以前快 106 倍
objc_msgSend 能识别 Tagged Pointer,比如 NSNumber 的 intValue 方法,直接从指针提取数据
使用 Tagged Pointer 后,指针内存储的数据变成了 Tag + Data,也就是将数据直接存储在了指针中
# NONPOINTER_ISA (指针中存放与该对象内存相关的信息)
苹果将 isa 设计成了联合体,在 isa 中存储了与该对象相关的一些内存的信息,原因也如上面所说,并不需要 64 个二进制位全部都用来存储指针。
isa 的结构:
// x86_64 架构
struct {
uintptr_t nonpointer : 1; // 0:普通指针,1:优化过,使用位域存储更多信息
uintptr_t has_assoc : 1; // 对象是否含有或曾经含有关联引用
uintptr_t has_cxx_dtor : 1; // 表示是否有C++析构函数或OC的dealloc
uintptr_t shiftcls : 44; // 存放着 Class、Meta-Class 对象的内存地址信息
uintptr_t magic : 6; // 用于在调试时分辨对象是否未完成初始化
uintptr_t weakly_referenced : 1; // 是否被弱引用指向
uintptr_t deallocating : 1; // 对象是否正在释放
uintptr_t has_sidetable_rc : 1; // 是否需要使用 sidetable 来存储引用计数
uintptr_t extra_rc : 8; // 引用计数能够用 8 个二进制位存储时,直接存储在这里
};
// arm64 架构
struct {
uintptr_t nonpointer : 1; // 0:普通指针,1:优化过,使用位域存储更多信息
uintptr_t has_assoc : 1; // 对象是否含有或曾经含有关联引用
uintptr_t has_cxx_dtor : 1; // 表示是否有C++析构函数或OC的dealloc
uintptr_t shiftcls : 33; // 存放着 Class、Meta-Class 对象的内存地址信息
uintptr_t magic : 6; // 用于在调试时分辨对象是否未完成初始化
uintptr_t weakly_referenced : 1; // 是否被弱引用指向
uintptr_t deallocating : 1; // 对象是否正在释放
uintptr_t has_sidetable_rc : 1; // 是否需要使用 sidetable 来存储引用计数
uintptr_t extra_rc : 19; // 引用计数能够用 19 个二进制位存储时,直接存储在这里
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
这里的 has_sidetable_rc 和 extra_rc,has_sidetable_rc 表明该指针是否引用了 sidetable 散列表,之所以有这个选项,是因为少量的引用计数是不会直接存放在 SideTables 表中的,对象的引用计数会先存放在 extra_rc 中,当其被存满时,才会存入相应的 SideTables 散列表中,SideTables 中有很多张 SideTable,每个 SideTable 也都是一个散列表,而引用计数表就包含在 SideTable 之中。
# 散列表(引用计数表、弱引用表)
引用计数要么存放在 isa 的 extra_rc 中,要么存放在引用计数表中,而引用计数表包含在一个叫 SideTable 的结构中,它是一个散列表,也就是哈希表。而 SideTable 又包含在一个全局的 StripeMap 的哈希映射表中,这个表的名字叫 SideTables。
当一个对象访问 SideTables 时:
首先会取得对象的地址,将地址进行哈希运算,与 SideTables 中 SideTable 的个数取余,最后得到的结果就是该对象所要访问的 SideTable
在取得的 SideTable 中的 RefcountMap 表中再进行一次哈希查找,找到该对象在引用计数表中对应的位置
如果该位置存在对应的引用计数,则对其进行操作,如果没有对应的引用计数,则创建一个对应的 size_t 对象,其实就是一个 uint 类型的无符号整型
弱引用表也是一张哈希表的结构,其内部包含了每个对象对应的弱引用表 weak_entry_t,而 weak_entry_t 是一个结构体数组,其中包含的则是每一个对象弱引用的对象所对应的弱引用指针。
# 循环引用
iOS内存中的分区有:堆、栈、静态区。其中,栈和静态区是操作系统自己管理回收,不会造成循环引用。在堆中的相互引用无法回收,有可能造成循环引用。
循环引用的实质:多个对象相互之间有强引用,不能施放让系统回收。
解决循环引用一般是将 strong 引用改为 weak 引用。
# 循环引用场景分析及解决方法
- 如: 在使用UITableView 的时候,将 UITableView 给 Cell 使用,cell 中的 strong 引用会造成循环引用。
// controller1
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { TestTableViewCell *cell =[tableView dequeueReusableCellWithIdentifier:@"UITableViewCellId" forIndexPath:indexPath]; cell.tableView = tableView; return cell; }
// cell @interface TestTableViewCell : UITableViewCell @property (nonatomic, strong) UITableView *tableView; // strong 造成循环引用 @end
解决:strong 改为 weak
``` objectivec
// cell
@interface TestTableViewCell : UITableViewCell
@property (nonatomic, weak) UITableView *tableView; // strong 改为 weak
@end
2
3
4
5
6
block
block在copy时都会对block内部用到的对象进行强引用的。 self.testObject.testCircleBlock = ^{ [self doSomething]; };1
2
3
4
self将block作为自己的属性变量,而在block的方法体里面又引用了 self 本身,此时就很简单的形成了一个循环引用。
应该将 self 改为弱引用
__weak typeof(self) weakSelf = self;
self.testObject.testCircleBlock = ^{
__strong typeof (weakSelf) strongSelf = weakSelf;
[strongSelf doSomething];
};
2
3
4
5
在 ARC 中,在被拷贝的 block 中无论是直接引用 self 还是通过引用 self 的成员变量间接引用 self,该 block 都会 retain self。
- Delegate
delegate 属性的声明如下:
@property (nonatomic, weak) id <TestDelegate> delegate;
如果将 weak 改为 strong,则会造成循环引用
// self -> AViewController
BViewController *bVc = [BViewController new];
bVc = self;
[self.navigationController pushViewController: bVc animated:YES];
// 假如是 strong 的情况
// bVc.delegate ===> AViewController (也就是 A 的引用计数 + 1)
// AViewController 本身又是引用了 <BViewControllerDelegate> ===> delegate 引用计数 + 1
// 导致: AViewController <======> Delegate ,也就循环引用啦
2
3
4
5
6
7
8
9
10
11
- NSTimer
NSTimer 的 target 对传入的参数都是强引用(即使是 weak 对象)

点击查看
#import <Foundation/Foundation.h>
@interface NSTimer (YPQBlocksSupport)
+ (NSTimer *)ypq_scheduledTimeWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats;
@end
#import "NSTimer+YPQBlocksSupport.h"
@implementation NSTimer (YPQBlocksSupport)
+ (NSTimer *)ypq_scheduledTimeWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats
{
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(ypq_blockInvoke:) userInfo:[block copy]
repeats:repeats];
}
- (void)ypq_blockInvoke:(NSTimer *)timer
{
void (^block)() = timer.userInfo;
if(block)
{
block();
}
}
@end
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
使用方式:
__weak ViewController * weakSelf = self;
[NSTimer ypq_scheduledTimeWithTimeInterval:4.0f
block:^{
ViewController * strongSelf = weakSelf;
[strongSelf afterThreeSecondBeginAction];
}
repeats:YES];
2
3
4
5
6
7
计时器保留其目标对象,反复执行任务导致的循环,确实要注意,另外在dealloc的时候,不要忘了调用计时器中的 invalidate方法。
# dealloc的实现流程
Dealloc的调用流程概括来讲大概有5个基本步骤:
首先调用 _objc_rootDealloc()
接下来调用 rootDealloc()
这时候会判断是否可以被释放,判断的依据主要有5个,判断是否有以下五种情况
- NONPointer_ISA
- weakly_reference
- has_assoc
- has_cxx_dtor
- has_sidetable_rc 如果有以上五中任意一种,将会调用 object_dispose()方法,做下一步的处理。如果没有之前五种情况的任意一种,则可以执行释放操作,C函数的 free()。
执行完毕。
点击查看
- (void)dealloc {
_objc_rootDealloc(self);
}
void_objc_rootDealloc(id obj)
{
assert(obj);
obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
// TaggedPointer并不需要进行释放操作
if (isTaggedPointer()) return; // fixme necessary?
// 判断是否不包含下列这五种情况
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
// 不包含任意一种,则直接释放
free(this);
}
else {
// 包含则调用object_dispose()
object_dispose((id)this);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# object_dispose() 调用流程
直接调用 objc_destructInstance() 之后会调用 C函数的 free()进行释放。
点击查看
id
object_dispose(id obj)
{
if (!obj) return nil;
// 销毁实例,会对内部做一系列的析构操作
objc_destructInstance(obj);
// 析构完成后释放内存
free(obj);
return nil;
}
2
3
4
5
6
7
8
9
10
11
# objc_destructInstance() 调用流程
先判断 hasCxxDtor,如果有 C++ 的相关内容,要调用 object_cxxDestruct() ,销毁 C++ 相关的内容。 再判断 hasAssocitatedObjects,如果有的话,要调用 object_remove_associations(),销毁关联对象的一系列操作。 然后调用 clearDeallocating()。 执行完毕。
void *objc_destructInstance(id obj)
{
// 该函数只销毁实例而不会释放内存
if (obj) {
// 一次性读取所有flags以提高性能。
bool cxx = obj->hasCxxDtor(); // 是否存在c++相关代码
bool assoc = obj->hasAssociatedObjects(); // 是否存在关联对象
// 下列执行的先后顺序很重要
if (cxx) object_cxxDestruct(obj); // 调用C++析构函数
if (assoc) _object_remove_assocations(obj); // 删除关联引用
obj->clearDeallocating(); // 调用ARC ivar清理
}
return obj;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# clearDeallocating() 调用流程
先执行 sideTable_clearDellocating()。 再执行 weak_clear_no_lock,在这一步骤中,会将指向该对象的弱引用指针置为 nil。 接下来执行 table.refcnts.eraser(),从引用计数表中擦除该对象的引用计数。 至此为止,Dealloc 的执行流程结束。
点击查看
inline void
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// 清除所有weak表 & 清除额外的保留计数并释放位
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// clearDeallocating()的慢路径, 用于含有 non-pointer isa 的对象
// 作用与sidetable_clearDeallocating()相同,都是清除weak引用和引用计数
clearDeallocating_slow();
}
assert(!sidetable_present());
}
void
objc_object::sidetable_clearDeallocating()
{
SideTable& table = SideTables()[this];
//清除所有weak表项
//清除额外的保留计数并释放位
// (fixme warn or abort if extra retain count == 0 ?)
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
// 将指向该对象的弱引用指针置为nil
weak_clear_no_lock(&table.weak_table, (id)this);
}
// 从引用计数表中擦除该对象的引用计数
table.refcnts.erase(it);
}
table.unlock();
}
// 由dealloc调用; 取消所有指向该指针的弱指针
// 提供的对象,使其不再可以使用。
void
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
/// XXX shouldn't happen, but does with mismatched CF/objc
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}
// 弱引用指针
weak_referrer_t *referrers;
size_t count;
if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == referent) {
// 将指针置为nil
*referrer = nil;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
// 从该区域的弱引用表中将其删除
weak_entry_remove(weak_table, entry);
}
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
assert(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
SideTable& table = SideTables()[this];
table.lock();
if (isa.weakly_referenced) {
// 将指向该对象的弱引用指针置为nil
weak_clear_no_lock(&table.weak_table, (id)this);
}
if (isa.has_sidetable_rc) {
// 从引用计数表中擦除该对象的引用计数
table.refcnts.erase(this);
}
table.unlock();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# 调用时机
我们可以从sidetable_release函数的实现来窥出端倪,它会在给对象发送release消息的时候调用,sidetable_release方法首先获取对象的引用计数,对引用计数相关标志位做操作,若对象实例可以被释放,将通过objc_msgSend发送SEL_dealloc消息(调用对象的dealloc方法)。
点击查看
uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];
bool do_dealloc = false;
table.lock();
// 获取对象的引用计数
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) {
do_dealloc = true;
table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) { // it->second 的值的是引用计数减一
// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
do_dealloc = true;
it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
it->second -= SIDE_TABLE_RC_ONE;
}
table.unlock();
// 调用dealloc
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
从sidetable_release 源码中,我们可得知,其调用线程为最后一个调用release方法的线程,当需要释放对象时,向对象实例发送SEL_dealloc(即dealloc)消息。
也就是说,dealloc并不总是在主线程中被调用,它有可能在任何线程被调用,这就需要注意一点,就是在dealloc中进行UIKit相关API的操作(UIKit相关API只能在主线程操作)。
# Objective-C是如何实现内存管理的
Objective-C的内存管理本质上是通过引用计数实现的,每次RunLoop都会检查对象的引用计数,如果引用计数为0,那么说明该对象已经没有再被使用了,此时可以对其进行释放了。其中,引用计数可以大体分为3种:MRC、ARC和内存池。 那么引用计数是如何操作的呢?其实不论哪种引用计数方式,它们本质上都是在合适的时机将对象的引用计数加1或者减1。 所以对于引用计数可以总结如下:
1)使对象引用计数加1的常见操作有alloc、copy、retain。
2)使对象引用计数减1的常见操作有release、autorealease。 自动释放池是一个统一来释放一组对象的容器,在向对象发送autorelease消息时,对象并没有立即释放,而是将对象加入到最新的自动释放池(即将该对象的引用交给自动释放池,之后统一调用release),自动释放池会在程序执行到作用域结束的位置时进行drain释放操作,这个时候会对池中的每一个对象都发送release消息来释放所有对象。这样其实就实现了这些对象的延迟释放。 自动释放池释放的时机指自动释放池内的所有对象是在什么时候释放的,这里要提到程序的运行周期RunLoop。对于每一个新的RunLoop,系统都会隐式地创建一个autorelease pool,RunLoop结束时自动释放池便会进行对象释放操作。autorelease和release的区别主要是引用计数减1的时机不同,autorelease是在对象的使用真正结束的时候才做引用计数减1,而不是收到消息立马释放。 retain、release和autorelease的内部实现代码如下:
1(id)retain { /* 对象引用计数加1*/ NSIncrementExtraRefCount(self); return self; }
(void)release { /对象引用计数减1,之后如果引用计数为0,那么释放/ if(NSDecrementExtraRefCountWasZero(self)) {
NSDeallocateObject(self);} }
(id)autorelease { /* 添加对象到自动释放池*/ [NSAutoreleasePool addObject:self]; return self; }