天天看点

iOS开发之内存管理的前世今生

       内存管理一直是开发者们津津乐道的话题,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
           
iOS开发之内存管理的前世今生

          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++
         }
     }
           

    输出结果为:            

iOS开发之内存管理的前世今生

           说明对象被正常释放

          5)performSelector中的对象retain问题             函数的声明和参数就不赘述了,还是重点看看里面有关参数retain部分的解释吧,如图中红色线框标准部分:             

iOS开发之内存管理的前世今生

             只有当执行完成之后才会释放target和argument对象,它的执行前提条件是:1)时间到;2)满足指定的Loop Modes。因此在发起该方法的类销毁之前该方法不一定会被执行,因此就会存在内存泄漏的风险。能否在dealloc或deinit中释放呢?请看客考虑        释放该方法中retain的对象,系统也提供了对应的API,即

- cancelPerformSelector:target:argument:
           

       其他可能存在的循环引用或内存泄漏等与内存管理相关的内容,待发现后再一一补充吧,先到此为止。           

继续阅读