IOS基础和进阶开发

iOS内存管理基础

2018-12-25  本文已影响2人  王大吉Rock

主要分析了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是如何工作:

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的生命周期较短,当car变量为nil后,其引用关系图:

此时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的时候,闭包以弱引用的形式来捕获实例。

上一篇 下一篇

猜你喜欢

热点阅读