四、内存管理

Reference:
Effective Objective-C 2.0
理解 iOS 的内存管理
内存管理基础
Objective-C 中的内存分配
iOS中的block是如何持有对象的

程序都是加载到内存中运行,了解APP工作机制,第一步先了解APP是如何使用内存的。

简述

程序加载到内存中运行时,在内存中分静态数据(只读)和动态数据(读写)两部分,其中动态部分中堆的分配与释放由程序做内存管理,iOS中使用引用计数机制配合自动释放池来做内存管理,又使用ARC来简化引用计数和自动释放的编程:

  • 引用计数:对于堆中的对象,想要保留时使其引用计数+1,想要释放时使其引用计数-1,当对象的引用计数为0时,系统会销毁对象回收内存;
  • ARC技术:依赖编译器的静态分析,在编译时为对象插入保留、释放、自动释放等引用计数管理代码,并在运行时对引用计数管理代码做优化;
  • 自动释放:自动释放标记使对象引用计数延迟-1,即自动释放池销毁时,对象在此自动释放池管理范围内被标记了几次,就会做几次引用计数-1;

概览

iOS内存管理

要点

  • 内存中静态数据不会发生变化,栈则由系统自动管理,编程中内存管理是对堆的分配和回收;
  • iOS中使用引用计数来管理内存,又使用ARC来简化引用计数的编程;
  • 自动释放池满足了对象归当前调用者所有,但又需要延迟释放的引用计数管理需求;
  • ARC中指向对象的指针都需表明自己对对象的所有权,普通指针通过所有权修饰符指明,属性的成员变量则通过属性修饰符指明;
  • 推荐用__autoreleasing来修饰结果参的形参,这样可以减少ARC的开销;
  • 现在的ARC中虽然不再需要使用@autoreleasepool { }来消除大量的局部对象所造成的内存峰值,但依然可以保留这种写法以兼容较老的版本;
  • CoreFoundation中的对象不由ARC管理,当和ARC管理的OC对象互相转换时,需视情况指明对象所有权是否发生变化;
  • ARC中需要注意对象间相互引用、performSelector相关接口和异常捕获时可能出现的内存泄露;
  • block分为全局block、栈block和堆block,MRC中需要持有一个block时,使用它的copy总是没错,ARC中可以将block直接赋值给变量,因为ARC会视情况自动copy;
  • ARC中block会保留其捕获的环境局部变量对对象的引用,所以可能造成循环引用,可以使用弱引用指针来避免循环引用,还可以结合临时强引用指针来避免block执行期间对象被释放;
  • NSString和NSMutableString都是抽象类,iOS中字符串的实际类型为NSCFString、NSCFConstantString和NSTaggedPointerString,它们在内存中的生命周期和管理方式各不相同;
  • 当出现内存问题时,Xcode自带的僵尸对象可以调试野指针问题,Instruments中的Leaks可以检查内存泄露问题,三方框架FBRetainCycleDetector可以检查对象的循环引用问题;

详情

1.内存模型

程序在内存中运行时分静态数据和动态数据:静态数据只读,存储在代码段(.text)和数据段(.data + .bbs),中这部分数据占用的内存在运行时不会发生变化,在程序终止后被清理;动态数据可读写,存储在栈(stack)和堆(heap)中,这部分数据占用的内存会随着程序运行而动态变化,其中栈的使用由系统管理,而堆的的使用则需要程序进行管理。

内存模型

栈由高位向低位增长,是因程序中函数运行而临时占用的内存,由系统的入栈和出栈操作来自动管理。堆由低位向高位增长,需要由程序自行管理,编程中的内存管理即是指对堆的分配与回收,运行中不再使用的内存却未回收便是内存泄露,还在使用的内存却被回收了便是野指针问题。

栈的内存变化:每一个函数的调用就会向栈中压入一个函数的帧,函数帧中包括函数的形参、局部变量和返回地址等信息,有多少层函数调用就会压多少个帧,当函数执行结束时其函数帧就会出栈,并回到函数的返回地址继续执行,随着函数执行时函数帧的入栈与出栈,程序对栈的占用也随之发生变化。

2.引用计数

iOS中使用引用计数来做内存管理,对象的创建方法为其在堆中分配内存,并将引用计数初始为1,对象创建后想要保留时使其引用计数+1,想要释放时使其引用计数-1,当对象的引用计数为0时,系统会销毁对象回收内存。

引用计数手动管理方法:

  • retain
  • release
  • autorelease

引用计数初始为1的创建方法:

  • alloc
  • new
  • copy
  • mutableCopy

通过以上方法创建的对象归调用者所有,需要调用者在使用后将其引用计数-1

3.ARC技术

ARC编程时指向对象的引用(指针)都需要表明对对象的所有权,普通指针通过所有权修饰符指明(如果没有显示声明所有权修饰符,则缺省为__strong),属性的成员变量则通过属性修饰符指明,编译时则依据指针对对象的所有权添加引用计数管理代码(引用计数管理的相关C函数,而非MRC中的OC方法),运行时对于引用计数管理代码又做了适当的优化。ARC既简化了引用计数管理代码的编程,又优化了引用计数管理代码的执行,Objective-C和Objective-C++的对象都可以依赖ARC进行内存管理,CoreFoundation中的对象则需要手动管理引用计数。

ARC中的所有权修饰符

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

  • __strong:强引用,默认的修饰符,使引用计数+1,对应属性修饰符中的strongcopyretain
  • __weak:弱引用,不会引起引用计数变化,对象释放后会自动指向nil,对应属性修饰符中的weak
  • __unsafe_unretained:不安全的弱引用,不会引起引用计数变化,对象释放后不会自动指向nil,容易造成野指针,但比__weak性能更好,对应属性修饰符中的assignunsafe_unretained;
  • __autoreleasing:自动释放引用,会使引用计数+1,并在自动释放池释放时延迟-1,常用于申明结果参的形参。

NSNotificationCenter对Observer的引用在iOS9之前是unsafe_unretained的,需要在对象dealloc前removeObserver,否则容易出现野指针问题,iOS9之后对Observer的引用改为了weak,在对象dealloc时没有removeObserver也不再有野指针的问题。

ARC与CoreFoundation

CoreFoundation中的对象不由ARC管理,依然需要手动管理引用计数,ARC中CF对象和OC对象进行相互转换时,需要通过关键字指明转换后对象所有权的变化:

  • __bridge:用于CF和OC对象的相互转换,不发生所有权的变化,引用计数不变。CF对象转OC对象,则对象依然需要在CF中手动管理引用计数;OC对象转CF对象,则对象由ARC管理,CF中可以不对其做手动的引用计数管理。
  • __bridge_retained:等效于CFBridgingRetain,用于OC对象转CF对象,CF保留一次对象的所有权,引用计数+1。相当于转换完成后再调用了一次CFRetain
  • __bridge_transfer:等效于CFBridgingRelease,用于CF对象转OC对象,CF释放一次对象的所有权,引用计数-1。相当于转换完成后再调用了一次CFRelease

ARC中的内存泄露

ARC为程序自动添加了引用计数管理代码,但如果编程过程中不注意,依然可能出现内存泄露的情况:

  1. 循环引用造成的内存泄露:A直接或间接的保留了B,B又直接或间接的保留了A,就会出现循环引用,这在block的使用中尤其容易出现,循环引用的对象在互相等对方释放自己,最终是都不会释放,造成内存泄露。ARC可以使用弱引用来避免循环引用的出现,也可以在出现循环引用后手动解除引用环。
  2. performSelector相关API造成的内存泄露:当派发的选择子不明确的时候,ARC中编译器不知道是否应该为返回的对象添加释放的代码,所以ARC选择不添加释放代码的保守做法,这样当选择子是new、alloc、copy和mutableCopy这些应当由调用者释放返回对象的方法时,就会造成内存泄露。可以添加额外的判断,当选择子是需要调用者释放返回对象的方法时,将返回对象转为CoreFoundation对象,再用__bridge_transfer转回OC对象,使其引用计数-1来避免内存泄露。
  3. 异常捕获代码造成的内存泄露:异常捕获代码@try{ } @catch{ } @finally{ }@try{ }中出现异常时会跳过后面的代码,进入@catch{ }中继续执行,如果@try{ }中创建并持有了临时对像,那么即使ARC为其添加了释放代码,也不会被执行,如此便会造成内存泄露。解决的办法是避免在@try{ }中使用new、alloc、copy和mutableCopy等方法来创建并持有对象,或者为文件添加-fobjc-arc-exceptions标识让ARC生成额外的代码来做处理(影响运行期性能),更推荐的做法其实是避免使用异常捕获代码。

在OC的编程中并推荐编写异常安全代码(异常捕获),因为OC的语言设计中认为只有严重到需要程序终止的错误才应该抛出异常,那么既然抛出异常时程序应该终止,也就无需捕获异常书写异常安全代码。对于可被接受或进一步处理的错误,OC中推荐通过返回nil/0或使用NSError来传递错误信息,并在接收到错误信息后做相应处理。

4.自动释放

有时不想再保留对象但又不想对象被马上释放时,比如函数中生成的对象作为函数的返回值,可以使用autorelease来标记对象,让对象在其所在的自动释放池drain时自动释放,达到延迟释放的效果。

autorelease与__autorelease

MRC和ARC中分别用autorelease和__autorelease为对象做自动释放标记,在一个自动释放池管理范围内对象被标记了几次自动释放,自动释放池drain时就会为其做几次引用计数-1。

ARC中通常用来__autorelease声明结果参(二级指针形参)的实参,关于这种使用场景需要清楚的有:

  1. 强引用的局域对象在出了作用域时引用计数-1,__autoreleasing指向的局域对象,会延迟释放,即离开作用域引用计数也不会-1,而是在当前自动释放池释放时才引用计数-1;
  2. 强引用形参会使引用计数+1,但是是函数作用域的局域对象,出了函数作用域引用计数-1,__autoreleasing修饰的形参不会使引用计数+1,出了函数作用域也不会-1;
  3. 当形参是结果参时(二级指针)如果传入的实参未做autoreleasing修饰,为结果参赋值时,ARC会添加一个autoreleasing的指针指向所赋的结果,使结果参返回后延迟释放;如果实参已经有__autoreleasing修饰,则在为结果参赋值时ARC不会添加额外的代码。

autoreleasePool

关于自动释放池需要清楚的有以下几点:

  1. 每个线程通过自己的自动释放池栈维护着线程中添加的自动释放池,对象被标记为自动释放时会添加到栈顶的自动释放池中;
  2. 每个线程都会为其自动释放池栈中创建一个默认自动释放池,主线程会监听main runloop的活动并通过入栈和出栈来周期性的新建和销毁自动释放池,子线程的默认自动释放池在使用时懒加载,在线程退出时清空;
  3. @autoreleasepool { }添加了自动释放池,但在离开其作用域后就已经释放了,可用其来做精细内存管理;
  4. 在MRC和早期的ARC中推荐用@autoreleasepool { }来消除内存峰值,现在的ARC中生成的局部对象,在离开局部作用域后就已经被释放了,已经不需要自动释放池来消除内存峰值,但依然可以保留这种写法以兼容老版本。

5.Block与内存管理

Block自身的内存管理

block其实是一个结构体,但也有一个指向类的isa指针,所以也可视为一种特殊的对象。block的isa指向抽象类NSBlock的子类__NSMallocBlock____NSStackBlock____NSGlobalBlock__,根据其实际类的不同可将block分为堆block、栈block和全局block,它们在内存中有不同的管理方法:

  • 全局block(__NSGlobalBlock__):和C函数一样储存在代码段,无需内存管理;
  • 栈block(__NSStackBlock__):类似于函数中的局部变量,由系统自动管理;
  • 堆block(__NSMallocBlock__):和普通对象一样由引用计数进行管理,支持ARC;

声明一个block时,block可以使用环境变量,对环境中全局变量和静态变量的使用不会影响block的类型,可如果使用了环境中的局部变量,声明的block将会是一个栈block,如果没有使用环境中的局部变量,则将会是一个全局block。对栈block进行copy会得到一个其在堆中的拷贝,而栈block的生命周期仅限于其声明时的作用域,所以若要持有一个栈block,MRC中需要手动调用栈block的copy来使用堆block,而ARC中则在将栈block赋值给变量时就自动进行了copy,而又因为全局block和堆block的copy返回的都是其自身,所以在需要持有一个block时,使用它的copy总是没错

Block中对象的内存管理

ARC中block会保留其捕获的环境局部变量对对象的引用,也就是说如果block中出现的环境局部变量是一个强引用指针,则block也会对该指针指向的对象强引用,如果捕获的环境局部变量是弱引用指针,则block对指针指向的对象也是弱应用,至于环境局部变量是自动释放引用指针,则不能在block中使用。

因为block的上述特性,如果block中要使用的局部变量是一个强引用指针,且指针指向的对象直接或间接的又强引用了该block,则会出现循环引用,造成内存泄露。针对这种情况,可以先用一个弱引用指针指向要使用的对象,再在block中使用弱引用指针,则能避免循环引用;而如果还需要防止block中代码执行期间,弱引用指针指向的对象被释放掉,则又可以在block中用先用一个临时的强引用指针来持有弱引用指针指向的对象,这个临时的强引用指针,作为block的局部变量,对对象的持有在block的作用域结束时即会释放。

6.NSString的内存问题

iOS中NSString和NSMutableString都是抽象类,字符串的实际类型为:NSCFString、NSCFConstantString和NSTaggedPointerString,它们在内存中的存储和管理方式都各不相同:

  • __NSCFString:对象字符串,运行时创建,存储在堆中,通过引用计数管理;
  • __NSCFConstantString:常量字符串,编译期创建,存储在常量区,无需内存管理;
  • NSTaggedPointerString:变量字符串,运行时创建,存储在指针变量中(栈中),系统自动管理;

在创建一个字符串时,得到的字符串的实际类型遵循以下规律:

  1. NSCFConstantString和NSTaggedPointerString是NSString的直接子类,NSCFString是NSMutableString的直接子类,所以创建一个可变字符串和对字符串mutableCopy得到的必然是__NSCFString;
  2. 字面量语法生成的NSString在编译期创建,实际类型为NSCFConstantString,再次创建相同的NSCFConstantString时不会重复创建,而是指向相同的常量字符串;
  3. withFormart:系列方法创建的NSString在运行时创建,实际类型为NSCFString或NSTaggedPointerString:当字符串支持Tagged Pointer优化时则创建的NSString实际类型为NSTaggedPointerString,否则为NSCFString;
  4. withString:系列方法和copy得到的NSString实际类型和原字符串实际类型一致;

Tagged Pointer:由于在64位系统中一个指针的长度已经达到了8个字节,对于一些信息量较少的对象(如NSSting、NSNumber、NSDate等),其本身需要的内存空间可能都不足8个字节,再将其创建在堆中,并用一个8个字节长度的指针去指向它,就有一些性能和内存上的浪费,针对这种情况苹果为这些类引入了各自的Tagged Pointer对象进行优化 – 用指针的前后各半个字节存储对象的标记信息,中间的7个字节存储对象的值,对象不再保存在堆中,而是直接保存在指针中。对于NSString,如果字符串可以被ASCII编码、六位编码或五位编码,并被存储在7个字节中,则运行时创建的NSString实际类型为NSTaggedPointerString,但即使是五位编码(五位表示一个字符),7个字节中最多保存(7*8)/5 = 11个字符,所以运行时创建的NSString如果大于11个字符则必然为__NSCFString

7.内存问题调试

可以使用Xcode自带的僵尸对象功能来调试野指针问题,使用Instruments中的Leaks来检查内存是否泄露,当出现内存泄露时可以使用三方框架FBRetainCycleDetector来检查对象的循环引用,至于相关工具如何使用,在本篇中则不做赘述。

-------------This article is over, thank you for reading -------------