iOS 内存管理

一.内存管理基础

一个程序的可执行文件在内存中的结果,从大的角度可以分为两个部分:只读部分和可读写部分。只读部分包括程序代码(.text)和程序中的常量(.rodata)。可读写部分(也就是变量)大致可以分成下面几个部分:

  • .data: 初始化了的全局变量和静态变量

  • .bss: 即 Block Started by Symbol, 未初始化的全局变量和静态变量

  • heap: 堆,使用 malloc, realloc, 和 free 函数控制的变量,堆在所有的线程,共享库,和动态加载的模块中被共享使用

  • stack: 栈,函数调用时使用栈来保存函数现场,自动变量(即生命周期限制在某个 scope 的变量)也存放在栈中。

总结来说,分为五大区

 栈区(stack)
 堆区(heap)
 全局/静态区(bss)
 文字/常量区(data)
 代码区(code)

data 和 bss 区

这两个区经常放在一起说,因为他们都是用来存储全局变量和静态变量的,区别在于 data 区存放的是初始化过的, bss 区存放的是没有初始化过的,例如:

int val = 3;
char string[] = "Hello World";

这两个变量的会一开始被存储在 .text 中(因为值是写在代码里面的),在程序启动时会拷贝到 .data 去区中。

而不初始化的话,像下面这样:

static int i;

这个变量就会被放在 bss 区中。

静态变量和全局变量

这两个概念都是很常见的概念,又经常在一起使用,很容易造成混淆。

全局变量:在一个代码文件当中,一个变量要么定义在函数中,要么定义在在函数外面。当定义在函数外面时,这个变量就有了全局作用域,成为了全局变量。全局变量不光意味着这个变量可以在整个文件中使用,也意味着这个变量可以在其他文件中使用(这种叫做 external linkage)。当在两个文件中同时定义了相等同的变量,比如int a = 10;在 Link 过程中会产生重复定义错误,因为有两个全局的 a 变量,Linker 不知道应该使用哪一个。为了避免这种情况,就需要引入 static。

静态变量: 指使用 static 关键字修饰的变量,static 关键字对变量的作用域进行了限制,具体的限制如下:

  • 在函数外定义:全局变量,但是只在当前文件中可见(叫做 internal linkage)

  • 在函数内定义:全局变量,但是只在此函数内可见(同时,在多次函数调用中,变量的值不会丢失)

  • (C++)在类中定义:全局变量,但是只在此类中可见

对于全局变量来说,为了避免上面提到的重复定义错误,我们可以在一个文件中使用 static,另一个不使用。这样使用 static 的就会使用自己的 a 变量,而没有用 static 的会使用全局的 a 变量。当然,最好两个都使用 static,避免更多可能的命名冲突。

注意: static 跟不可改变没有关系,不可改变的变量使用 const 关键字修饰,注意不要混淆。

extern 是 C 语言中另一个关键字,用来指示变量或函数的定义在别的文件中,使用 extern 可以在多个源文件中共享某个变量, extern 跟 static 在含义上是“水火不容”的,一个表示不能在别的地方用,一个表示要去别的地方找。

栈区(stack)

  • 栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}” 中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外, 在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值 也会被存放回栈中。由于栈的后进先出特点,所以 栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
  • 指针都存在栈区,用于指向分配在堆区的内存的地址。

堆区(heap)

  • 堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张); 当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
  • 堆向高地址扩展的数据结构,是不连续的内存区域。程序员负责在何时释放内存(如用free或delete),在iOS的ARC程序中,系统自动管理计数器,计数器为0的时候,在当次的runloop结束后,释放掉内存。堆中的所有东西都是匿名的,这样不能按名字访问,而只能通过指针访问。
  • 对于堆来讲,频繁的new/delete势必会造成内存空间的不连续性,从而造成大量的碎片 ,使程序效率降低。

代码区(code)

  • 通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等,这些常量放在只读数据段(data segment)中,也有叫做常量区的说法。
    [图片上传失败...(image-68f922-1632816750360)]

一.内存分配

在iOS中数据是存在在堆和栈中的,然而我们的内存管理管理的是堆上的内存,栈上的内存并不需要我们管理。

  • 非OC对象(基础数据类型)存储在栈上
  • OC对象存储在堆上
int a = 10; //栈
    
int b = 20; //栈
    
Car *c = [[Car alloc] init];

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 或者其它未知行为

其中 非自己生成的对象,且该对象存在,但自己不持有 这个特性是使用autorelease来实现的,示例代码如下:

- (id) getAObjNotRetain {
    id obj = [[NSObject alloc] init]; // 自己持有对象
    [obj autorelease]; // 取得的对象存在,但自己不持有该对象
    return obj;
}

autorelease 使得对象在超出生命周期后能正确的被释放(通过调用release方法)。在调用 release 后,对象会被立即释放,而调用 autorelease 后,对象不会被立即释放,而是注册到 autoreleasepool 中,经过一段时间后 pool结束,此时调用release方法,对象被释放。

在MRC的内存管理模式下,与对变量的管理相关的方法有:retain, release 和 autorelease。retain 和 release 方法操作的是引用记数,当引用记数为零时,便自动释放内存。并且可以用 NSAutoreleasePool 对象,对加入自动释放池(autorelease 调用)的变量进行管理,当 drain 时回收内存。

当我们调用非 alloc,init 系的方法来初始化对象时(通常是工厂方法),我们不需要负责变量的释放,可以当成普通的临时变量来使用:

NSString *name = [NSString stringWithFormat:@"%@ %@", firstName, lastName];
self.name = name
// 不需要执行 [name release]

ARC介绍

ARC其实也是基于引用计数,只是编译器在编译时期自动在已有代码中插入合适的内存管理代码(包括 retain、release、copy、autorelease、autoreleasepool)以及在 Runtime 做一些优化。

现在的iOS开发基本都是基于ARC的,所以开发人员大部分情况都是不需要考虑内存管理的,因为编译器已经帮你做了。为什么说是大部分呢,因为底层的 Core Foundation 对象由于不在 ARC 的管理下,所以需要自己维护这些对象的引用计数。

还有就算循环引起情况就算由于互相之间强引用,引用计数永远不会减到0,所以需要自己主动断开循环引用,使引用计数能够减少。

所有权修饰符

Objective-C编程中为了处理对象,可将变量类型定义为id类型或各种对象类型。 ARC中id类型和对象类其类型必须附加所有权修饰符。

其中有以下4种所有权修饰符:

  • __strong
  • __weak
  • __unsafe_unretaied
  • __autoreleasing

所有权修饰符和属性的修饰符对应关系如下所示:

  • assign 对应的所有权类型是 __unsafe_unretained
  • copy 对应的所有权类型是 __strong
  • retain 对应的所有权类型是 __strong
  • strong 对应的所有权类型是 __strong
  • unsafe_unretained对应的所有权类型是__unsafe_unretained
  • weak 对应的所有权类型是 __weak

__strong

__strong 表示强引用,对应定义 property 时用到的 strong。当对象没有任何一个强引用指向它时,它才会被释放。如果在声明引用时不加修饰符,那么引用将默认是强引用。当需要释放强引用指向的对象时,需要保证所有指向对象强引用置为 nil。__strong 修饰符是 id 类型和对象类型默认的所有权修饰符。

原理:

{
    id __strong obj = [[NSObject alloc] init];
}
//编译器的模拟代码
id obj = objc_msgSend(NSObject,@selector(alloc));
objc_msgSend(obj,@selector(init));

// 出作用域的时候调用
objc_release(obj);

虽然ARC有效时不能使用release方法,但由此可知编译器自动插入了release。

对象是通过除alloc、new、copy、multyCopy外方法产生的情况

{
    id __strong obj = [NSMutableArray array];
}

结果与之前稍有不同:

//编译器的模拟代码
id obj = objc_msgSend(NSMutableArray,@selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_release(obj);

objc_retainAutoreleasedReturnValue函数主要用于优化程序的运行。它是用于持有(retain)对象的函数,它持有的对象应为返回注册在autoreleasePool中对象的方法,或是函数的返回值。像该源码这样,在调用array类方法之后,由编译器插入该函数。

而这种objc_retainAutoreleasedReturnValue函数是成对存在的,与之对应的函数是objc_autoreleaseReturnValue。它用于array类方法返回对象的实现上。下面看看NSMutableArray类的array方法通过编译器进行了怎样的转换:

+ (id)array
{
    return [[NSMutableArray alloc] init];
}
//编译器模拟代码
+ (id)array
{
    id obj = objc_msgSend(NSMutableArray,@selector(alloc));
    objc_msgSend(obj,@selector(init));
    
    // 代替我们调用了autorelease方法
    return objc_autoreleaseReturnValue(obj);
}

我们可以看见调用了objc_autoreleaseReturnValue函数且这个函数会返回注册到自动释放池的对象,但是,这个函数有个特点,它会查看调用方的命令执行列表,如果发现接下来会调用objc_retainAutoreleasedReturnValue则不会将返回的对象注册到autoreleasePool中而仅仅返回一个对象。达到了一种最优效果。如下图:
[图片上传失败...(image-6bf000-1632816750360)]

__weak

__weak 表示弱引用,对应定义 property 时用到的 weak。弱引用不会影响对象的释放,而当对象被释放时,所有指向它的弱引用都会自定被置为 nil,这样可以防止野指针。使用__weak修饰的变量,即是使用注册到autoreleasePool中的对象。__weak 最常见的一个作用就是用来避免循环循环。需要注意的是,__weak 修饰符只能用于 iOS5 以上的版本,在 iOS4 及更低的版本中使用 __unsafe_unretained 修饰符来代替。

__weak 的几个使用场景:

  • 在 Delegate 关系中防止循环引用。
  • 在 Block 中防止循环引用。
  • 用来修饰指向由 Interface Builder 创建的控件。比如:@property (weak, nonatomic) IBOutlet UIButton *testButton;。

原理:

    {
        id __weak obj = [[NSObject alloc] init];
    }

编译器转换后的代码如下:

    id obj;
    id tmp = objc_msgSend(NSObject,@selector(alloc));
    objc_msgSend(tmp,@selector(init));
    objc_initweak(&obj,tmp);
    objc_release(tmp);
    objc_destroyWeak(&object);

对于__weak内存管理也借助了类似于引用计数表的散列表,它通过对象的内存地址做为key,而对应的__weak修饰符变量的地址作为value注册到weak表中,在上述代码中objc_initweak就是完成这部分操作,而objc_destroyWeak
则是销毁该对象对应的value。当指向的对象被销毁时,会通过其内存地址,去weak表中查找对应的__weak修饰符变量,将其从weak表中删除。所以,weak在修饰只是让weak表增加了记录没有引起引用计数表的变化。

对象通过objc_release释放对象内存的动作如下:

  1. objc_release
  2. 因为引用计数为0所以执行dealloc
  3. _objc_rootDealloc
  4. objc_dispose
  5. objc_destructInstance
  6. objc_clear_deallocating

而在对象被废弃时最后调用了objc_clear_deallocating,该函数的动作如下:

  1. 从weak表中获取已废弃对象内存地址对应的所有记录
  2. 将已废弃对象内存地址对应的记录中所有以weak修饰的变量都置为nil
  3. 从weak表删除已废弃对象内存地址对应的记录
  4. 根据已废弃对象内存地址从引用计数表中找到对应记录删除
  5. 据此可以解释为什么对象被销毁时对应的weak指针变量全部都置为nil,同时,也看出来销毁weak步骤较多,如果大量使用weak的话会增加CPU的负荷。

还需要确认一点是:使用__weak修饰符的变量,是使用注册到autoreleasePool中的对象。

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 语法,不再需要手动调用 autoreleasedrain 等方法。

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];
    }
}

如果不使用 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]));
    }
}

在面试中问到有关 autorelease pool 有关的知识也多半会问一下,这里的 pool 有什么作用,能不能去掉之类。在这里我们分析一下。

根据苹果官方文档, UIApplicationMain 函数是整个 app 的入口,用来创建 application 对象(单例)和 application delegate。尽管这个函数有返回值,但是实际上却永远不会返回,当按下 Home 键时,app 只是被切换到了后台状态。

同时参考苹果关于 Lifecycle 的官方文档,UIApplication 自己会创建一个 main run loop,我们大致可以得到下面的结论:

  1. main.m 中的 UIApplicationMain 永远不会返回,只有在系统 kill 掉整个 app 时,系统会把应用占用的内存全部释放出来。

  2. 因为(1), UIApplicationMain 永远不会返回,这里的 autorelease pool 也就永远不会进入到释放那个阶段

  3. 在 (2) 的基础上,假设有些变量真的进入了 main.m 里面这个 pool(没有被更内层的 pool 捕获),那么这些变量实际上就是被泄露的。这个 autorelease pool 等于是把这种泄露情况给隐藏起来了。

  4. UIApplication 自己会创建 main run loop,在 Cocoa 的 runloop 中实际上也是自动包含 autorelease pool 的,因此 main.m 当中的 pool 可以认为是没有必要的。

在基于 AppKit 框架的 Mac OS 开发中, main.m 当中就是不存在 autorelease pool 的,也进一步验证了我们得到的结论。不过因为我们看不到更底层的代码,加上苹果的文档中不建议修改 main.m ,所以我们也没有理由就直接把它删掉(亲测,删掉之后不影响 App 运行,用 Instruments 也看不到泄露)。

Core Foundation 对象的内存管理

底层的 Core Foundation 对象,在创建时大多以 XxxCreateWithXxx 这样的方式创建,例如:

// 创建一个 CFStringRef 对象
CFStringRef str= CFStringCreateWithCString(kCFAllocatorDefault, “hello world", kCFStringEncodingUTF8);

// 创建一个 CTFontRef 对象
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);

对于这些对象的引用计数的修改,要相应的使用 CFRetain 和 CFRelease 方法。如下所示:

// 创建一个 CTFontRef 对象
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);

// 引用计数加 1
CFRetain(fontRef);

// 引用计数减 1
CFRelease(fontRef);

对于 CFRetainCFRelease 两个方法,读者可以直观地认为,这与 Objective-C 对象的 retainrelease 方法等价。

所以对于底层 Core Foundation 对象,我们只需要延续以前手工管理引用计数的办法即可。

除此之外,还有另外一个问题需要解决。在 ARC 下,我们有时需要将一个 Core Foundation 对象转换成一个 Objective-C 对象,这个时候我们需要告诉编译器,转换过程中的引用计数需要做如何的调整。这就引入了bridge相关的关键字,以下是这些关键字的说明:

  • __bridge: 只做类型转换,不修改相关对象的引用计数,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。
  • __bridge_retained:类型转换后,将相关对象的引用计数加 1,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。
  • __bridge_transfer:类型转换后,将该对象的引用计数交给 ARC 管理,Core Foundation 对象在不用时,不再需要调用 CFRelease 方法。

你可能感兴趣的:(iOS 内存管理)