iOS内存管理

作者: xiao彰 | 来源:发表于2017-07-05 11:48 被阅读0次

    Objective-C,顾名思义,是一门超C的语言,自从ARC(Auto Reference Count)出现了之后,我们就很少会去关注关于内存管理这方面的事情了,这些功能的设计者和实现者们为此付出的努力值得我们称赞,但是如果我们对此不知不顾的话,一旦出现了内存泄漏的问题的话,也是个坑,所以先去尝试着去了解它,对我们也不失是一件好事。下面是c语言中的内存分配方式:

    栈区(stack):栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。栈内存分配运算内置于处理器的指令集,效率很高,但是分配的内存容量有限,比如iOS中栈区的大小是2M。(栈系统提供的功能,特点是快速高效,缺点是有限制,数据不灵活;而堆是函数库提供的功能,特点是灵活方便,数据适应面广泛,但是效率有一定降低。)

    堆区(heap):就是通过new、malloc、realloc分配的内存块,它们的释放编译器不去管,由我们的应用程序去释放。如果应用程序没有释放掉,操作系统会自动回收。分配方式类似于链表。

    静态区:全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后,由系统释放。

    常量区:常量存储在这里,不允许修改的。

    代码区:存放函数体的二进制代码。

    iOS开发中,数据类型有两种,一种是int 、float等基本数据类型,而基本数据类型是放在栈上,由系统进行管理,还有一种就是对象,存储在堆上,如果一个对象创建并使用后并没有得到及时的释放,就会一直占用着内存,当这样的对象一直占据着内存得不到释放,这就是我们常说的内存泄漏,严重的话会导致程序崩溃。Objective-C 中的内存管理,其实主要是讲的在堆区上面对内存的管理,而iOS的内存管理机制就是引用计数(reference counting):

    1)每个对象都有一个关联的整数,称为引用计数器

    2)当代码需要使用该对象时,则将对象的引用计数加1

    3)当代码结束使用该对象时,则将对象的引用计数减1

    4)当引用计数的值变为0时,表示对象没有被任何代码使用,此时对象将被释放。

    其实对应的也可以用开关房间的灯为例来说明引用计数的机制。

    1)当第一个人进入办公室时,他需要使用灯,于是开灯,引用计数为1

    2)每当多一个人进入办公室时,引用计数加1

    3)当有一个人离开办公室时,引用计数减1,

    4)当引用计数为0时,也就是最后一个人离开办公室时,他不再需要使用灯,关灯离开办公室。(释放对象)

    1. Objective-c语言中的MRC(MannulReference Counting)

    在MRC的内存管理模式下,与对变量的管理相关的方法有:retain,release和autorelease。retain和release方法操作的是引用记数,当引用记数为零时,便自动释放内存。并且可以用NSAutoreleasePool对象,对加入自动释放池(autorelease调用)的变量进行管理,当drain时回收内存。

    1)retain,retain是一个实例方法,只能由对象调用,它的作用是使这个对象的内存空间的引用计数加1,并不会新开辟一块内存空间,通常于赋值是调用,该方法的作用是将内存数据的所有权附给另一指针变量,引用数加1,即retainCount+= 1 ; 如:对象2=[对象1 retain];表示对象2同样拥有这块内存的所有权。若只是简单地赋值,如:对象2=对象1;那么当对象1的内存空间被释放的时候,对象2便会成为野指针,再对对象2进行操作便会造成内存错误。

    2)release,该方法是释放指针变量对内存数据的所有权,引用数减1,即retainCount-= 1,若引用计数变为0则系统会立刻释放掉这块内存。如果引用计数为0的基础上再调用release,便会造成过度释放,使内存崩溃;

    3)autorelease,:autorelease是一个实例方法,同样只能由对象调用,它的作用于release类似,但不是立刻减1,相当于一个延迟的release,通常用于方法返回值的释放,如便利构造器。autorelease会在程序走出自动释放池时执行,通常系统会自动生成自动释放池(即使是MRC下),也可以自己设定自动释放池,如:

    @autoreleasepool{

             obj= [[NSObject alloc]init];

             [obj autorelease];

    }

    当程序走出“}”时obj的引用计数就会减1.

    当然自动释放池还有一种写法

    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc]init];

    //这里写代码

    [pool release];

    但是使用自动释放池需要注意:

    1)自动释放池实质上只是在释放的时候給池中所有对象对象发送release消息,不保证对象一定会销毁,如果自动释放池向对象发送release消息后对象的引用计数仍大于1,对象就无法销毁。

    2)自动释放池中的对象会集中同一时间释放,如果操作需要生成的对象较多占用内存空间大,可以使用多个释放池来进行优化。比如在一个循环中需要创建大量的临时变量,可以创建内部的池子来降低内存占用峰值。

    3)autorelease不会改变对象的引用计数

    在mrc中程序员们要遵循下面的基本原则:

    1)当你通过new、alloc、copy或mutableCopy方法创建一个对象时,它的引用计数为1,当不再使用该对象时,应该向对象发送release或者autorelease消息释放对象。

    2)当你通过其他方法获得一个对象时,如果对象引用计数为1且被设置为autorelease,则不需要执行任何释放对象的操作;

    3)如果你打算取得对象所有权,就需要保留对象并在操作完成之后释放,且必须保证retain和release的次数对等。

    简单得来说就是谁分配谁释放。

    2. Objective-c语言中的ARC下的内存管理(Automatic Reference Counting)

    ARC是iOS 5推出的新功能,全称叫 ARC(Automatic Reference Counting)。简单地说,就是代码中自动加入了retain/release,原先需要手动添加的用来处理内存管理的引用计数的代码可以自动地由编译器完成了。该机能在 iOS 5/ Mac OS X 10.7 开始导入,利用 Xcode4.2 可以使用该机能。简单地理解ARC,就是通过指定的语法,让编译器(LLVM 3.0)在编译代码时,自动生成实例的引用计数管理部分代码。有一点,ARC并不是GC,它只是一种代码静态分析(Static Analyzer)工具。

    使用ARC有什么好处呢?

    其实大家都知道,自从有了arc之后写Objective-C的代码变得简单多了,因为我们不需要担心烦人的内存管理,担心内存泄露了

    代码的总量变少了,看上去清爽了不少,也节省了劳动力

    代码高速化,由于使用编译器管理引用计数,减少了低效代码的可能性

    但是实质上ARC只能解决iOS开发中的90%的内存管理问题,还有10%的内存管理是需要程序员自己处理的,主要是循环引用和对底层Core Foundatin的调用。

    循环引用,其实简单得来讲就是对象A和对象B,互相引用对方作为自己的成员变量,但是由于引用计数的管理方式,只有对象自己销毁的时候,才会将成员变量的引用计数减1,然而对象A和对象B由于都引用了对方,都得不到销毁,这样的情况就是循环引用。当然这只是循环引用的一种情况,多个对象依次持有对方,形成一个环状,也可能造成循环引用,一般来说,环越大就越难被发现。

    一般来说,解决循环引用的方法有两个

    1)第一个方法是我知道这里会造成循环引用,但是我在使用完了这个功能、方法之后我主动去破除这个循环引用的情况,主动断开循环引用这种操作依赖于程序员自己手工显式地控制,相当于回到了以前 “谁申请谁释放” 的内存管理年代,它依赖于程序员自己有能力发现循环引用并且知道在什么时机断开循环引用回收内存(这通常与具体的业务逻辑相关),所以这种解决方法并不常用,更常见的办法是使用弱引用 (weak reference) 的办法。

    2)弱引用虽然持有对象,但是并不增加引用计数,这样就避免了循环引用的产生。在 iOS 开发中,弱引用通常在 delegate 模式中使用。弱引用的实现原理是这样,系统对于每一个有弱引用的对象,都维护一个表来记录它所有的弱引用的指针地址。这样,当一个对象的引用计数为 0 时,系统就通过这张表,找到所有的弱引用指针,继而把它们都置成 nil。一般在block中使用不当也会造成循环引用,例如在使用MJRefresh这个常用的刷新第三方库的时候,要使用__weak 来实现弱引用,避免循环引用

    __weak typeof(self) weakself = self;

    self.table.mj_header = [MJRefreshGifHeader headerWithRefreshingBlock:^{

           weakself.page = 1;

           weakself.table.mj_footer.hidden = YES;

           [weakself sendRequest];

    }];

    Core Foundation 对象的内存管理

    下面我们就来简单介绍一下对底层 Core Foundation 对象的内存管理。底层的 Core Foundation 对象,在创建时大多以 XxxCreateWithXxx 这样的方式创建,例如:

    // 创建一个 CFStringRef 对象

    CFStringRef str= CFStringCreateWithCString(kCFAllocatorDefault, “hello world", kCFStringEncodingUTF8);

    // 创建一个 CTFontRef 对象

    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);

    对于这些对象的引用计数的修改,要相应的使用 CFRetain 和 CFRelease 方法。如下所示:

    // 创建一个 CTFontRef 对象

    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);

    // 引用计数加 1

    CFRetain(fontRef);

    // 引用计数减 1

    CFRelease(fontRef);

    对于 CFRetain 和 CFRelease 两个方法,读者可以直观地认为,这与 Objective-C 对象的 retain 和 release 方法等价。

    所以对于底层 Core Foundation 对象,我们只需要延续以前手工管理引用计数的办法即可。

    除此之外,还有另外一个问题需要解决。在 ARC 下,我们有时需要将一个 Core Foundation 对象转换成一个 Objective-C 对象,这个时候我们需要告诉编译器,转换过程中的引用计数需要做如何的调整。这就引入了bridge相关的关键字,以下是这些关键字的说明:

    __bridge: 只做类型转换,不修改相关对象的引用计数,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。

    __bridge_retained:类型转换后,将相关对象的引用计数加 1,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。

    __bridge_transfer:类型转换后,将该对象的引用计数交给 ARC 管理,Core Foundation 对象在不用时,不再需要调用 CFRelease 方法。

    我们根据具体的业务逻辑,合理使用上面的 3 种转换关键字,就可以解决 Core Foundation 对象与 Objective-C 对象相对转换的问题了。

    在项目中,其实我们也可以用Instruments来检测我们项目的内存泄漏的问题。

    参考文章:

    1理解 iOS 的内存管理

    2iOS内功篇:内存管理

    3iOS内存管理:基本概念与原理

    相关文章

      网友评论

        本文标题:iOS内存管理

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