您的位置:首页 > 移动开发 > IOS开发

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

2016-01-19 16:08 387 查看
       内存管理一直是开发者们津津乐道的话题,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:


       其他可能存在的循环引用或内存泄漏等与内存管理相关的内容,待发现后再一一补充吧,先到此为止。
          
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息