iOS内存管理基础

作者: 王大吉Rock | 来源:发表于2018-12-25 17:03 被阅读2次

    主要分析了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的生命周期较短,当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的时候,闭包以弱引用的形式来捕获实例。

    相关文章

      网友评论

        本文标题:iOS内存管理基础

        本文链接:https://www.haomeiwen.com/subject/ixfxlqtx.html