主要分析了iOS的内存区域、ARC机制、循环引用的解决方案
1 iOS内存与存储区域:
1.1 栈区,由编译器自动分配和释放,函数的定义和调用、函数的参数、局部变量等,栈和进程是一一对应的,跟堆一样,在程序执行期间可以动态的扩张和收缩,效率比较高。
1.2 堆区,由程序员分配和释放,如果程序员不释放,那程序结束时将会由系统回收,在iOS中alloc都是存储在堆上。在RAC环境下,编译器会在合适的时候为oc对象添加上release操作,在线程runloop退出或者休眠时销毁这些对象。
1.3 全局区(静态区),全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量存储在一个区域,全局初始化区,未初始化的全局变量和静态变量存储在相邻的另一块区域,全局未初始化区,程序结束后由系统释放。如,类、协议和结构体的定义。
1.4 常量区,存放字符串常量等,不需要再修改,程序结束后由系统释放。
1.5 代码区,存储函数的二进制代码。
iOS内存存储区域例子:
int a = 0; // 全局初始化区域
char *p1; // 全局非初始化区域
main()
{
int b; // 栈
char s[] = "abc"; //栈
char *p2; // 栈
char *p3 = "123456"; // p3在栈中, 123456在常量区
static int c = 0; // 全局初始化区
p1 = (char *)malloc(10); // 堆
p2 = (char *)malloc(20); // 堆
strcpy(p1, "123456"); // 123456在常量区,编译器h可能会将它与p3指向的“123456”优化成一个地方
}
2 iOS的内存管理机制--ARC
2.1 什么是ARC:
ARC:自动引用计数
在oc和swift都是使用自动引用计数(ARC)来追踪和管理内存的使用,在大部分情况下我们不需要管理实例的生命周期,ARC会自动释放不再使用的实例内存。
引用计数只针对类的实例,由于值类型不是引用类型,不能使用引用计数,值类型存储在栈上,由系统管理销毁。
2.2 ARC是如何工作:
- 当类的实例被创建的时候,ARC会给实例分配一块内存来存储实例的信息,当实例被销毁的时候,ARC将会把实例销毁,并释放内存,以确保不被使用的实例不占用内存。
- 当实例被销毁的时候,不能访问其属性和方法,否则会导致程序的奔溃。
- 为了保证实例在使用的时候不被销毁,ARC会记录实例被多少属性、变量所引用,当实例赋值给一个属性、变量的时候,实例的引用计数会加一,实例和变量之间创建一个强引用,而属性、变量会持有着这个实例,只要强引用还在,这个实例将不会销毁,当实例不被任何变量引用时,实例的引用计数将会变成0,此时ARC会销毁实例并收回内存。
2. ARC实践:
class Person {
var name : String = ""
init(name: String) {
self.name = name;
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
var p1: Person?
var p2: Person?
var p3: Person?
p1 = Person(name: "wangdaji")
p2 = p1
p3 = p1
// p1 p2 p3 指向的都是同一个实例
printAddress(values: p1!)
printAddress(values: p2!)
printAddress(values: p3!)
创建一个Person类实例,ARC为实例分配内存。将实例赋值给p1、p2、p3,那么p1、p2、p3和这个实例之间分别创建了一个强引用,p1、p2、p3分别持有这个对象。
p1、p2、p3和实例之间的引用关系1将p1、p2变量为nil时,只有p3强引用着实例,实例不会被销毁。
p1 = nil
p2 = nil
p1、p2、p3和实例之间的引用关系2
当p3变量也为nil时,实例没有被任何属性或变量强引用着,ARC将会销毁实例,并释放出内存。
p3 = nil
p1、p2、p3和实例之间的引用关系3
3 多个类实例之间的循环强引用
3.1 循环强引用
ARC可以追踪一个新创建实例的引用计数,并在实例不再使用的时候自动释放内存。如果两个实例都作为另一个实例的属性,实例之间就保持着强引用(相互持有),实例的引用计数永远不会为0,这样就会导致循环强引用。
举个强引用的例子:
(1)创建Person类、Car类,并创建两者的实例,ARC会为这些实例分配内存并分配person实例的car属性、car实例的person属性的存储空间。
class Person {
var name : String = ""
var car : Car?
init(name: String) {
self.name = name;
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
class Car {
var name : String = ""
var person : Person?
init(name: String) {
self.name = name;
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
var person: Person?
var car: Car?
person = Person(name: "wangdaji")
car = Car(name: "wangdaji' car")
person?.car = car
car?.person = person
其引用的效果图:
循环强引用1
取消person和car设置为nil
person = nil
car = nil
这种情况下虽然打破了person和car变量对Person和Car实例的强引用,但是Person实例和Car实例之间存在着强引用,Person实例持有Car实例,Car实例持有Person实例,两个实例之间相互持有对方,引用计数无法为0,ARC就无法销毁这两个实例。引用图:
循环强引用2当然,强引用不仅仅存在于两个实例之间,也可能存在于多个实例之间。
循环强引用3.png
只要实例之间出现直接或者间接的引用(持有),那就有可能出现循环引用,最终导致内存泄漏,野指针的情况出现。
3.2 解决循环强引用:
定义 | 使用场景 | 使用方法 | |
---|---|---|---|
弱引用 | 弱引用不会对持有的实例保持强引用,对实例的引用是弱引用,并持有实例,但引用计数不会加一 | 当持有的实例有更短的生命周期 | 声明的属性或是变量前加weak :对实例进行弱引用,弱引用总是可选类型 |
无主引用 | 无主引用不会对持有的实例保持强引用,引用计数不会加一,引用的实例必须要有更长的生命周期 | 当持有的实例有更长的声明周期 | 声明的属性或是变量前加unowned :对实例进行无主引用,修饰的属性是非可选的 |
使用不安全的无主引用是一个不安全的操作。
3.2.1 弱引用
因为car对person来说有着更短的生命周期,所以将Person类中属性car前添加weak
关键字,Person实例中的属性car对Car实例保持弱引用。修改代码:
class Person {
var name : String = ""
weak var car : Car?
init(name: String) {
self.name = name;
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
引用关系图:
可以看到Person实例的car属性虽然引用Car实例,其之间创建的是弱引用,Car实例的引用计数依旧为1。
此时Car实例的引用计数等于0,很快被ARC自动销毁了。Car实例销毁后,之间的强引用也被销毁了,ARC会将Person实例的引用计数=1,由于依旧被person变量强引用着,所以不会被ARC销毁,当person变量为nil后,Person实例才会被ARC销毁。
3.2.2 无主引用
无主引用要求被修饰的属性不能为nil,不能是非可选的,并且具有更长的声明周期。上面的例子可以这样理解:一个人拥有一辆车,人有着更长的生命周期,所以Car类中的person可以被unowned
修饰。将到吗修改成:
class Person {
var name : String = ""
var car : Car?
init(name: String) {
self.name = name;
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
class Car {
var name : String = ""
unowned var person : Person
init(name: String, person: Person) {
self.name = name
self.person = person
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
var person: Person?
person = Person(name: "wangdaji")
person!.car = Car(name: "奔驰", person: person!)
其引用关系图:
Person实例持有Car实例,之间创建的是强引用,Car实例对Person实例之间创建的是无主引用。
当person变量为nil后:没有对Person实例的强引用了,其引用计数为0,Person实例被ARC销毁,之间的强引用也会被销毁,Car实例也会被销毁。
3.2.3 闭包的循环强引用
闭包的循环引用:和类实例之间的循环引用类似,闭包的循环引用发生在实例和闭包之间的,闭包作为实例的属性,而闭包内部也持有(捕捉)了该实例,这样就会造成循环引用:
class Person {
var name : String = ""
lazy var run : () -> String = {
return "\(self.name) + running"
}
init(name: String) {
self.name = name;
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
var person : Person? = Person(name: "wangdaji")
print(person?.run() ?? "")
其引用关系:
ARC引用计数11.png
可以使用捕获列表和无主引用来解决例子中的循环问题。修改定义闭包的代码:
// lazy var run : () -> String = { [weak self] in
// return "\(self?.name) + running"
// }
lazy var run : () -> String = { [unowned self] in
return "\(self.name) + running"
}
其引用关系:
选择无主引用还是弱引用需结合实际的情况来使用:在闭包和捕获的实例总是相互引用并总是同时销毁,使用无主引用。在闭包中的捕获实例可能会为nil的时候,闭包以弱引用的形式来捕获实例。
网友评论