内存管理一直是开发者们津津乐道的话题,iOS开发中的内存管理也当然也不例外。本文将对iOS开发中内存管理相关问题作较详细描述,从MRC、ARC到现在的Swift自动内存管理,就作者所了解的内容一一作介绍,欢迎拍砖给建议。
一、内存区域介绍
要管理内存,我们就必须要对应用程序运行在内存中的状态有所了解,需要知道哪些需要我们的应用程序去管理,哪些是由系统自动管理,而不需要我们操心。程序运行过程中使用到的可编程内存大致可以分为:
- 全局/静态存储区,全局变量和静态变量的存储区域
- 栈区,在函数执行过程中,函数内局部变量的存储单元可以在栈上创建,函数执行结束后这些存储单元自动被释放。
- 堆区,亦称动态分配区,由程序在运行过程中动态申请分配和管理的区,通常说的内存管理,基本是指对于这一内存区块的管理
- 常量区,存储程序运行过程中用到的各种常量,不允许修改
来段代码说明一下吧
int a = 0; //全局初始化区
char *p1; //全局未初始化区
@implementation test
- (void)test:(int)para { //para在栈上
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "1234"; //1234在常量区,p3在栈上
static int c = 0; //全局(静态)初始化区
p1 = (char *)malloc(10); //分配来的10个字节的区域在堆区
}
@end
二、iOS内存管理的黄金法则(Swift不适用,Swift自动管理内存)
- 谁创建谁释放
- 谁retain谁释放
一句话,谁让retainCount计数器增加,谁负责让它减少
1)Object-C中MRC内存管理的一些规则 A、使用alloc, new, copy或者mutableCopy等以及调用addObject等方法时,引用计数器+1,使用release时,引用计数器-1,当引用计数器为0时,对象被释放 B、Property Attributes包括retain和assign retain,相当于ARC中的strong assign,相当于ARC中的weak 2)Object-C中ARC内存管理的一些规则 A、同样使用alloc, new, copy或者mutableCopy等以及调用addObject等方法时,引用计数器+1,使用release时,引用计数器-1,当引用计数器为0时,对象被释放 B、Property Attributes除了包括MRC中的属性外,增加了 strong,强引用 weak,弱引用 三、那些内存管理中的坑 1)循环引用 相信这个坑是绝大多数开发人员都遇到过的坑。循环引用即A持有了B,B持有了A,导致无论是先释放A还是B,总是被对方持有,而导致双方始终无法释放的内存泄漏问题。来个网上多次用的代码例子吧,A和B的亲密关系:)
class A {
let b: B
init() {
b = B()
b.a = self
}
deinit {
print("A deinit")
}
}
class B {
var a: A?
deinit {
print("B deinit")
}
}
解决这种亲密关系导致的循环引用,采用弱引用即可,将上面class B的代码改成:
class B {
weak var a: A? //增加weak权限修饰符,弱化引用关系
deinit {
print("B deinit")
}
}
2) Block中的坑 要想知道坑在何处,首先得了解Block可以访问的变量范围,Block中可访问的变量范围有: A、全局变量(包含在Block中声明的静态变量),Block可以直接访问 B、被当作参数传入Block块中的变量(类似函数参数) C、和Block块属同一作用域的栈变量会被当作常量在Block中捕获 D、和Block块属同一作用域,但被__block修饰的变量会以引用的方式被Block捕获,且是可变的 E、在Block块中声明的变量如同函数中声明的变量一样 Block在访问对象变量(即类对象)时有两条隐含的retain对象规则,即: A、如果访问类属性对象变量,则Block会强引用self,即retain一次类对象本身 B、如果访问了局部对象变量,则Block会强引用局部变量自身一次 由这两条规则,我们很容易就知道Block中循环引用的坑,代码如下:
//规则一导致的循环引用
dispatch_async(queue, ^{
doSomethingWithObject(instanceVariable); //访问属性
});
//规则二导致的循环引用
id localVariable = instanceVariable;
dispatch_async(queue, ^{
doSomethingWithObject(localVariable); //访问局部变量
});
破解Block中的循环引用,代码修改为如下:
__block id weakSelf = self; //MRC
//__unsafe_unretained __block id weakSelf = self; //ARC
dispatch_async(queue, ^{
doSomethingWithObject(weakSelf.instanceVariable); //访问属性
});
__block id localVariable = instanceVariable; //MRC
//__unsafe_unretained __block id localVariable = instanceVariable //ARC
dispatch_async(queue, ^{
doSomethingWithObject(localVariable); //访问局部变量
});
3)闭包中循环引用 闭包中变量的访问范围及持有变了隐含规则同Block,此处直接上代码解释循环引用问题
class A: NSObject {
let name: String = "A"
lazy var printName: () -> () = {
print("A's name is \(self.name)") //此处自动retain一次self,导致循环引用
}
deinit {
print("A deinit")
}
}
let instanceA: A = A()
instanceA.printName() //此处只会打印出"A's name is A",不会打印出"A deinit"
代码修改为:
class A: NSObject {
let name: String = "A"
lazy var printName: () -> () = {
[weak self] in
if let weakSelf = self {
print("A's name is \(weakSelf.name)")
}
}
deinit {
print("A deinit")
}
}
let instanceA: A = A()
instanceA.printName() //此处会打印出"A's name is A"和"A deist"说明循环引用已被打破,对象正常释放
4)NSTimer中的对象retain问题 先看一下NSTimer中定义的函数声明及参数说明吧,然后再来解释
class func scheduledTimerWithTimeInterval(_ ti: NSTimeInterval, target aTarget: AnyObject, selector aSelector: Selector, userInfo userInfo: AnyObject?, repeats yesOrNo: Bool) -> NSTimer
repeats参数被设置成YES时,target中的对象将永远不会被释放,只有调用invalidate方法之后才会释放target对象,从而释放接收处理target对象。看下面代码中的注释及输出结果比较
class A: NSObject {
var timer: NSTimer?
override init() {
super.init()
self.timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "printName", userInfo: nil, repeats: true)
}
deinit {
print("A deinit")
}
func printName() {
print("name = A")
}
}
//初始化一个对象,同时触发timer
var instanceA: A? = A()
instanceA = nil //此处即使置为nil,也不会释放对象instanceA,因为timer中还持有该对象,会不停的输出"name = A"
下面增加一个特定条件下触发invalidate方法的功能,比如执行了3次之后就触发invalidate。
class A: NSObject {
var timer: NSTimer?
var times: Int = 0
override init() {
super.init()
self.timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "printName", userInfo: nil, repeats: true)
}
deinit {
print("A deinit")
}
func printName() {
if self.times >= 3 {
self.timer?.invalidate() //这个invalidate为什么不写在deist函数里?看客可以想想
}
print("name = A")
self.times++
}
}
输出结果为:
说明对象被正常释放
5)performSelector中的对象retain问题 函数的声明和参数就不赘述了,还是重点看看里面有关参数retain部分的解释吧,如图中红色线框标准部分:
只有当执行完成之后才会释放target和argument对象,它的执行前提条件是:1)时间到;2)满足指定的Loop Modes。因此在发起该方法的类销毁之前该方法不一定会被执行,因此就会存在内存泄漏的风险。能否在dealloc或deinit中释放呢?请看客考虑 释放该方法中retain的对象,系统也提供了对应的API,即
- cancelPerformSelector:target:argument:
其他可能存在的循环引用或内存泄漏等与内存管理相关的内容,待发现后再一一补充吧,先到此为止。