ios内存管理(整理篇)

iOS内存管理机制

前言

今天学习了IOS中的内存管理机制,其中一些片段是由其他文章搬过来的,我记得有篇文章是这样写到,站在巨人的肩膀上,我只是一个整理者加了一些自己的理解

为什么要学习内存管理

虽然 ARC 帮我们解决了引用计数的大部分问题,一些年轻的 iOS 开发者仍然会做不好内存管理工作。他们甚至不能理解常见的循环引用问题,而这些问题会导致内存泄漏,最终使得应用运行缓慢或者被系统终止进程。

什么是内存管理

官方解释:软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源

知识的补充

1、知识内存区分

我们的智能设备都有RAM(运行内存)和ROM(硬盘)。
RAM:内部存储
ROM:外部存储。
而CPU直接访问的是RAM,如果想访问外部存储,则数据须先放到RAM中才能被CPU访问。

2、RAM和ROM的特点和区别

RAM:运行内存,CPU可以直接访问,访问速度快,不能够断电存储(断电会丢失数据–不稳定)
ROM:存储型内存,CPU不可以直接访问,访问速度慢,可以掉电存储–稳定

3、RAM和ROM的协同工作

由于RAM不支持掉电存储,所以App程序一般存储在ROM中。
手机里面使用的ROM基本都是NandFlash(闪存),CPU是不能直接访问的,而是需要 文件系统/驱动程序 将其读到RAM里面,CPU才可以访问的。

内存分区

内存即指的是RAM,可以分为5个区域

  • 栈区:由系统管理。地址从高到低分配。大量的局部变量,深递归,函数循环调用都可能耗尽栈内存而造成程序崩溃
  • 堆区:需要我们自己管理内存,alloc申请内存release释放内存。创建的对象也都放在这里。 地址是从低到高分配。堆是所有程序共享的内存,当N个这样的内存得不到释放,堆区会被挤爆,程序立马瘫痪。这就是内存泄漏
  • 全局区/静态区(staic):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后有系统释放。
  • 常量区:常量字符串就是放在这里的,还有const常量。
  • 代码区:存放App代码,App程序会拷贝到这里。

ios内存管理(整理篇)_第1张图片区与区之间是不可以联系的哦!

内存分配

我们需要管理的是堆上的内存

  • 非OC对象(基础数据类型)存储在栈上
  • OC对象存储在堆上

引用计数

通常来说:当创建(alloc)一个对象的实例并在堆上申请内存时,对象的引用计数就为1,在其他对象中需要持有(retain)这个对象时,就需要把该对象的引用计数加1,需要释放一个对象时,就将该对象的引用计数减1,直至对象的引用计数为0,对象的内存会被立刻释放。

很文艺的解释是怎样的呢?

记得在《寻梦环游记》里对于一个人的死亡是这样定义的:当这个这个世界上最后一个人都忘记你时,就迎来了终极死亡。类比于引用计数,就是每有一个人记得你时你的引用计数加1,每有一个人忘记你时,你的引用计数减1,当所有人都忘记你时,你就消失了,也就是从内存中释放了。

如果再深一层,包含我们后面要介绍的ARC中的强引用和弱引用的话,那这个记住的含义就不一样了。强引用就是你挚爱的亲人,朋友等对你比较重要的人记得你,你的引用计数才加1。

而弱引用就是那种路人,一面之缘的人,他们只是对你有一个印象,他们记得你是没有用的,你的引用计数不会加1。当你挚爱的人都忘记你时,你的引用计数归零,你就从这个世界上消失了,而这些路人只是感觉到自己记忆中忽然少了些什么而已。

MRC手动管理引用计数

对象操作 引用计数的变化
alloc/new/copy/mutableCopy等 +1
retain +1
release -1
dealloc -

法则:

  • 自己生成的对象,自己持有。
  • 非自己生成的对象,自己也能持有。
  • 不在需要自己持有对象的时候,释放。
  • 非自己持有的对象无需释放。
/*
 * 自己生成并持有该对象
 */
 id obj0 = [[NSObeject alloc] init];
 id obj1 = [NSObeject new];
/*
 * 持有非自己生成的对象
 */
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有

[obj retain]; // 自己持有对象
/*
 * 不在需要自己持有的对象的时候,释放
 */
id obj = [[NSObeject alloc] init]; // 此时持有对象

[obj release]; // 释放对象
/*
 * 指向对象的指针仍就被保留在obj这个变量中
 * 但对象已经释放,不可访问
 */

 * 非自己持有的对象无法释放
 */
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有

[obj release]; // ~~~此时将运行时crash 或编译器报error~~~ 非 ARC 下,调用该方法会导致编译器报 issues。此操作的行为是未定义的,可能会导致运行时 crash 或者其它未知行为


- (id) getAObjNotRetain {
    id obj = [[NSObject alloc] init]; // 自己持有对象
    
    [obj autorelease]; // 取得的对象存在,但自己不持有该对象
    
    return obj;
}

使用autorelease方法可以使取得的对象存在,但自己不持有对象。autorelease 使得对象在超出生命周期后能正确的被释放(通过调用release方法)。在调用 release 后,对象会被立即释放,而调用 autorelease 后,对象不会被立即释放,而是注册到 autoreleasepool 中,经过一段时间后 pool结束,此时调用release方法,对象被释放。

像[NSMutableArray array] [NSArray array]都可以取得谁都不持有的对象,这些方法都是通过autorelease实现的。

ARC自动管理引用计数

所有权修饰符

Objective-C编程中为了处理对象,可将变量类型定义为id类型或各种对象类型。 ARC中id类型和对象类其类型必须附加所有权修饰符。

  • __strong
  • __weak
  • __unsafe_unretaied
  • __autoreleasing

这里有一个很不错的解释放在这里。 所有权修饰符解释
这篇文章中对strong这样说到:为了便于理解,我们可以认为对象A是一条狗,obj是遛狗的人,强引用就是狗链子。当obj超出作用域的时候,链子断了,狗因为不再有链子拴着它,就逃跑了。

而我更认为更像是你复制了一条一摸一样的狗,再用链子栓住。

强烈建议大家亲自动手写写这里面代码,能更好的帮助你了解这里面的区别。

ARC下的内存管理问题

ARC 能够解决 iOS 开发中 90% 的内存管理问题,但是另外还有 10% 内存管理,是需要开发者自己处理的,这主要就是与底层 Core Foundation 对象交互的那部分,底层的 Core Foundation 对象由于不在 ARC 的管理下,所以需要自己维护这些对象的引用计数。

对于 ARC 盲目依赖的 iOS 新人们,由于不知道引用计数,他们的问题主要体现在:

  • 过度使用 block 之后,无法解决循环引用问题。
  • 遇到底层 Core Foundation对象,需要自己手工管理它们的引用计数时,显得一筹莫展。

循环引用(Reference Cycle)问题

引用计数这种管理内存的方式虽然很简单,但是有一个比较大的瑕疵,即它不能很好的解决循环引用问题。如下图所示:对象 A 和对象 B,相互引用了对方作为自己的成员变量,只有当自己销毁时,才会将成员变量的引用计数减 1。因为对象 A 的销毁依赖于对象 B 销毁,而对象 B 的销毁与依赖于对象 A 的销毁,这样就造成了我们称之为循环引用(Reference Cycle)的问题,这两个对象即使在外界已经没有任何指针能够访问到它们了,它们也无法被释放。

ios内存管理(整理篇)_第2张图片不止两对象存在循环引用问题,多个对象依次持有对方,形式一个环状,也可以造成循环引用问题,而且在真实编程环境中,环越大就越难被发现。下图是 4 个对象形成的循环引用问题。

ios内存管理(整理篇)_第3张图片使用弱引用

弱引用虽然持有对象,但是并不增加引用计数,这样就避免了循环引用的产生。在 iOS 开发中,弱引用通常在 delegate 模式中使用。举个例子来说,两个 ViewController A 和 B,ViewController A 需要弹出 ViewController B,让用户输入一些内容,当用户输入完成后,ViewController B 需要将内容返回给 ViewController A。这个时候,View Controller 的 delegate 成员变量通常是一个弱引用,以避免两个 ViewController 相互引用对方造成循环引用问题,如下所示:
ios内存管理(整理篇)_第4张图片
弱引用的实现原理是这样,系统对于每一个有弱引用的对象,都维护一个表来记录它所有的弱引用的指针地址。这样,当一个对象的引用计数为 0 时,系统就通过这张表,找到所有的弱引用指针,继而把它们都置成 nil。

Core Foundation对象的内存管理

// 创建一个 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 方法。

最后

因为自己也是刚学习完ios内存管理,没有很深刻的理解和体会,怕浅薄的理解误导大家,所以这篇文章更像是一篇笔记整理文。

下附文章摘抄地址:

iOS里的内存管理

理解 iOS 的内存管理

iOS基础:深入内存管理-从所有权修饰符开始

iOS基础:深入内存管理-让人头疼的autorelease

如有侵权,请联系删除

你可能感兴趣的:(ios内存管理(整理篇))