什么是内存管理?
是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。
Objective-C语言本身是支持垃圾回收机制的,但有平台局限性,仅限于Mac桌面系统开发中,而在iPhone和iPad等苹果移动终端设备中是不支持垃圾回收机制的。
虽然苹果并没有明确每个App在运行期间可以使用的内存最大值,但是有开发者进行了实验和统计,一般在占用系统内存超过20%的时候会有内存警告,而超过50%的时候,就很容易Crash了,所以内存使用率还是尽量要少
内存分区:
堆:一般由程序员分配释放,若程序员不释放,则可能会引起内存泄漏。其类似于链表。
栈:由编译器自动分配释放,存放函数的参数值,局部变量等值。其操作方式类似于数据结构中的栈。
栈区(heap):由系统去管理。地址从高到低分配。先进后出。会存一些局部变量,函数跳转跳转时现场保护,这些系统都会帮我们自动实现,无需我们干预。所以大量的局部变量,深递归,函数循环调用都可能耗尽栈内存而造成程序崩溃。
堆区(stack):需要我们自己管理内存,alloc申请内存release释放内存。创建的对象也都放在这里。 地址是从低到高分配。堆是所有程序共享的内存,当N个这样的内存得不到释放,堆区会被挤爆,程序立马瘫痪。这就是内存泄漏。
全局区/静态区(staic):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后有系统释放。
常量区:常量字符串就是放在这里的,还有const常量。
代码区:存放App代码,App程序会拷贝到这里。
在iOS中数据是存在在堆和栈中的,然而我们的内存管理管理的是堆上的内存,栈上的内存并不需要我们管理。
非OC对象(基础数据类型)存储在栈上
OC对象存储在堆上
内存管理:
基本原则:谁创建,谁释放,谁引用,谁管理
管理范围:任何继承了NSObject的对象,对其他基本数据类型(int、char、float、double、struct、enum等)无效
四个法则:
自己生成的对象,自己持有;非自己生成的对象,自己也能持有;
不在需要自己持有对象的时候,释放;非自己持有的对象无需释放。
原理:通过引用计数来实现的,当我们新创建一个对象时,它的引用计数为1,当有其它对象持有并指向这个对象时,引用计数就+1 当指针不在指向这个对象时,引用计数就减1 当这个对象的引用计数为0时,说明对象不再被任何指针引用,该对象销毁 进而内存回收
管理方式:自动引用计数 ARC 、手动内存计数 MRC:、自动释放池 autoreleasePool:
MRC:需要我们手动的创建并申请内存 然后在手动的释放 当你使用copy mutablecopy alloc生成并持有对象时 或使用retan持有对象必须有一个release或者autorelease 与之对应 一旦对象的引用计数为0 系统会自动发送一条dealloc消息 将对象销毁并内存回收。
ARC:是编译器在编译时期自动在已有的代码中插入合适的内存管理代码以及runtime做的一些优化;
在某个方法里面创建一个对象 前端编辑器在方法的末尾自动添加release语句销毁 类拥有的对象在dealloc中释放.
当代码中出现多对retain/release重复调用时,ARC优化器负责移除多余的retain/release语句
内存泄露:
定义:程序中已分配的堆内存由于某种原因未释放或无法释放,而造成系统的浪费
ARC下哪些情况会造成内存泄漏:
第一种可能:第三方框架不当使用;解决方法:
第二种可能:block循环引用;解决方法:
第三种可能:delegate循环引用;解决方法:
第四种可能:NSTimer循环引用,解决方法:
第五种可能:非OC对象内存处理,解决方法:需要手动释放 CGImageRelease(ref)
第六种可能:地图类处理,解决方法:使用完毕后 地图地理置为nil
第七种可能:大次数循环内存暴涨, 解决方法:使用@autoreleasepool
破除循环引用的方法:
1.注意变量作用域,使用 autorelease 让编译器来处理引用
2.使用弱引用(weak)
3.当实例变量完成工作后,将其置为nil
检查方法:
Instrument 的 Leaks / Allocations
使用Analyze进行代码的静态分析
MLeaksFinder:
原理:MLeaksFinder一开始是从UIViewController入手的,UIViewController在POP或dismiss之后该控制器及其上的view,view的subviews都会被释放掉,MleaksFinder就是在控制器POP或dismiss之后去查看该控制器和其上的view是否都被释放掉。
具体的方法是,为基类 NSObject 添加一个方法 willDealloc 方法,该方法的作用是,先用一个弱指针指向 self,并在一小段时间(3秒)后,通过这个弱指针调用 -assertNotDealloc,而 -assertNotDealloc 主要作用是直接中断言。这样,当我们认为某个对象应该要被释放了,在释放前调用这个方法,如果3秒后它被释放成功,weakSelf 就指向 nil,不会调用到 -assertNotDealloc 方法,也就不会中断言,如果它没被释放(泄露了),-assertNotDealloc 就会被调用中断言。这样,当一个 UIViewController 被 pop 或 dismiss 时(我们认为它应该要被释放了),我们遍历该 UIViewController 上的所有 view,依次调 -willDealloc,若3秒后没被释放,就会中断言。
内存溢出:
当程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个int,但给它存了long才能存下的数,那就是内存溢出
解决办法:
僵尸对象:
已经被销毁的对象(不能再使用的对象),内存已经被回收的对象。一个引用计数器为0对象被释放后就变为了僵尸对象;
解决办法:默认情况下,Xcode是不会管僵尸对象的,使用一块被释放的内存也不会报错。为了方便调试,应该开启僵尸对象监控方法:Edit scheme——>Diagnostics——>Enable Zombie Objects
autoreleasePool创建和释放:
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了
也就是说AutoreleasePool创建是在一个RunLoop事件开始之前(push),AutoreleasePool释放是在一个RunLoop事件即将结束之前(pop)。
AutoreleasePool里的Autorelease对象的加入是在RunLoop事件中,AutoreleasePool里的Autorelease对象的释放是在AutoreleasePool释放时
autoreleasePool实现原理:
当对象从autoreleasepool中生成并调用autorelease时,该对象就会注册到处于栈顶的自动释放池中,当经过pool结束时,pool会将每个标记autorelease的对象release一次 如果这个对象的retaincountd等于0则这个对象可以被销毁.
AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成的栈结构(分别对应结构中的parent指针和child指针),主要通过下列三个函数完成:
由`objc_autoreleasePoolPush`作为自动释放池作用域的第一个函数
使用`objc_autorelease`将对象加入自动释放池
由`objc_autoreleasePoolPop`作为自动释放池作用域的最后一个函数 该过程主要分为两步:
page->releaseUntil(stop),对栈顶(page->next)到stop地址(POOL_SENTINEL)之间的所有对象调用objc_release(),进行引用计数减1
清空page对象page->kill(),有两句注释
当添加到上限时,新的对象就会加入到下一个page,所以构成了双向链表的结构
AutoreleasePool 与 runloop 与线程是一一对应的关系
使用场景:
当程序有大量中间临时变量产生时,避免内存使用峰值过高,及时释放内存的场景
这样一来,每次循环结束,我们都会将临时对象放在这个池里面,而不是线程的主池里面。
对象什么时候释放:
使用 @autoreleasepool,会在大括号结束时释放,
而调用 autorelease 后,对象不会被立即释放,而是注册到 autoreleasepool 中,经过一段时间后 pool结束,此时调用release方法,对象被释放
不使用 @autoreleasepool,这个会由系统自动释放,释放时机是在当前 runloop 结束时释放,因为系统会自动为每个 runloop 执行自动释放池的 push 和 pop 操作
内存在iOS中的结构和组成:
Tagged Pointer:
从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储,在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值,使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中,当指针不够存储数据时,才会使用动态分配内存的方式来存储数据
Tagged Pointer 指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free
散列表:是一个Sidetables的哈希映射表 对象的引用计数都存在SideTables散列表中 一个散列表包含众多的SideTable,每个SideTable由spinlock_t 自旋锁 RefcountMap 引用计数表 weak_table_t 弱引用表三部分组成。
多个Side Table组成而不是一个Side Table 是因为多线程访问可能加锁 这样可以提高效率
全局的 引用计数 之所以不存在同一张表中,是为了避免资源竞争,解决效率的问题。
引用计数表 中引入了 分离锁的概念,将一张表分拆成多个部分,对他们分别加锁,可以实现并发操作,
NONPOINTER_ISA:(指针中存放与该对象内存相关的信息) 苹果将 isa 设计成了联合体,在 isa 中存储了与该对象相关的一些内存的信息,原因也如上面所说,并不需要 64 个二进制位全部都用来存储指针。
对象的销毁:
1.当一个对象的引用计数器值为0时,那么它将被销毁,其占用的内存被系统回收
2.当一个对象被销毁时,系统会自动向对象发送一条dealloc消息
3.一般会重写dealloc方法,在这里释放相关资源,dealloc就像对象的遗言
4.一旦重写了dealloc方法,就必须调用[super dealloc],并且放在最后面调用
5.不要直接调用dealloc方法
6.一旦对象被回收了,它占用的内存就不再可用,坚持使用会导致程序崩溃(野指针错误)
内存管理常见面试题:
1.僵尸对象、野指针、空指针分别指什么,有什么区别?
野指针:指针变量没有进行初始化或指向的空间已经被释放。
使用野指针调用对象方法,会报异常,程序崩溃。
通常再调用完release方法后,把保存对象指针的地址清空,赋值为nil,找oc中没有空指针异常,所以[nil retain]调用方法不会有异常。
僵尸对象 : 堆中已经被释放的对象(retainCount = 0)
空指针 : 指针赋值为空,nil
2.如果一个对象释放前被加到了NotificationCenter中,不在NotificationCenter中remove这个对象可能会出现什么问题?
3.为什么很多内置的类,如TableViewController的delegate的属性是assign不是retain?
4.内存管理的几条原则是什么?
5.按照默认法则,哪些关键字生成的对象需要手动释放?
6.哪些对象不需要手动释放会自动进入释放池?在和property结合的时候怎样有效的避免内存泄露?
7.retain、release和autorelease的底层实现Objective-C是如何实现内存管理的?
8.autorealease pool自动释放池是什么?
什么是自动释放池:用来存储多个对象类型的指针变量
自动释放池对池内对象的作用:存入池内的对象,当自动释放池被销毁时,会对池内对象全部做一次release操作
对象如何加入池中:调用对象的autorelease方法
自动释放池能嵌套使用吗:能
自动释放池何时被销毁 :简单的看,autorelease的"}"执行完以后。而实际情况是Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop
多次调用对象的autorelease方法会导致:野指针异常
自动释放池的作用:将对象与自动释放池建立关系,池子内调用autorelease,在自动释放池销毁时销毁对象,延迟release销毁时间
通过Observer监听RunLoop的状态,一旦监听到RunLoop即将进入睡眠等待状态,就释放自动释放池(kCFRunLoopBeforeWaiting)
9.autorelease的对象是在什么时候被release的?autorelease和release有什么区别
10.引用计数为0的引用实例是立即回收么?
对象的内存销毁时间表,分四个步骤:
1. 调用 -release :引用计数变为零
* 对象正在被销毁,生命周期即将结束.
* 不能再有新的 __weak 弱引用, 否则将指向 nil.
* 调用 [self dealloc]
2. 子类 调用 -dealloc
* 继承关系中最底层的子类 在调用 -dealloc
* 如果是 MRC 代码 则会手动释放实例变量们(iVars)
* 继承关系中每一层的父类 都在调用 -dealloc
3. NSObject 调 -dealloc
* 只做一件事:调用 Objective-C runtime 中的 object_dispose() 方法
4. 调用 object_dispose()
* 为 C++ 的实例变量们(iVars)调用 destructors
* 为 ARC 状态下的 实例变量们(iVars) 调用 -release
* 解除所有使用 runtime Associate方法关联的对象
* 解除所有 __weak 引用
* 调用 free()
11. objc使用什么机制管理对象内存?
通过引用计数器(retainCount)的机制来决定对象是否需要释放。 每次runloop完成一个循环的时候,都会检查对象的 retainCount,如果retainCount为0,说明该对象没有地方需要继续使用了,可以释放掉了。
MRC(manual retain-release)手动内存管理
ARC(automatic reference counting)自动引用计数
ARC的判断准则, 只要没有强指针指向对象, 对象就会被释放.
12.内存的几块区域以及职能?
13.不手动指定autoreleasepool的前提下,一个autorealese对象在什么时刻释放?(比如在一个vc的viewDidLoad中创建)
分两种情况:手动干预释放时机,系统自动去释放。
手动干预释放时机-指定 autoreleasepool 就是所谓的:当前作用域大括号结束时释放。
系统自动去释放-不手动指定 autoreleasepool
Autorelease对象出了作用域之后,会被添加到最近一次创建的自动释放池中,并会在当前的runloop连续结束时释放。
从程序启动到加载完成是一个完整的运行循环,然后会停下来,等待用户交互,用户的每一次交互都会启动一次运行循环,来处理用户所有的点击事件,触摸事件。
我们都知道: 所有autorelease的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中。
但是如果每次都放进应用程序的main.m中的autoreleasepool中,迟早有被撑满的一刻。这个过程中必定有一个释放的动作。何时?
在一次完整的运行循环结束之前,会被销毁。
那什么时间会创建自动释放池?运行循环检测到事件并启动后,就会创建自动释放池。
@autoreleasepool当自动释放池被销毁或替换时,会向自动释放池中的所有对象发送release消息,释放自动释放池中的所有对象。
如果在一个vc的viewDidLoad中创建一个自动释放对象,那么该对象会在viewDidAppear方法执行前就被销毁了。
14.苹果是如何实现autoreleasepool的?
autoreleasepool以一个队列数组的形式实现,主要通过下列三个函数完成.
• objc_autoreleasepoolPush(压入)
• objc_autoreleasepoolPop(弹出)
• objc_autorelease(释放内部)
看函数名就可以知道,对autorelease分别执行push,和pop操作。销毁对象时执行release操作。
15.一个objc对象如何进行内存布局?(考虑有父类的情况)?
由于Objective-C中没有多继承,因此其内存布局还是很简单的,就是:最前面有个isa指针,然后父类的实例变量存放在子类的成员变量之前
所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中。
每一个对象内部都有一个isa指针,指向他的类对象,类对象中放置着本对象的
1.对象方法列表(对象能够接收的消息列表,保存在它所对应的类对象中)
2.成员变量的列表,
3. 属性列表,
它内部也有一个isa指针指向元对象(元类),元对象内部放置的是类方法列表,类对象内部还有一个超类的指针,指向他的父类对象。
根对象就是NSObject,它的超类指针指向nil
16. Autoreleasepool所使用的数据结构是什么?AutoreleasePoolPage结构体了解么?
17.了解iOS的内存管理吗?内存管理的范围?
这里说下swift里的内存管理:
delgate照样weak修饰,闭包前面用[weak self],swift里的新东西,unowned,举例,如果self在闭包被调用的时候可能为空,则用weak,反之亦然,如果为空时使用了unowned,程序会崩溃,类似访问了悬挂指针,在oc中类似于unsafe_unretained,类似assign修饰了oc对象,对象被销毁后,被unowned修饰的对象不会为空,但是unowned访问速度更快,因为weak需要unwarp后才能使用
管理所有继承自NSObject的对象, 对基本数据类型无效.是因为对象和其他数据类型在系统中存储的空间不一样,其他局部变量主要存储在栈区(因为基本数据类型占用的存储空间是固定的,一般存放于栈区),而对象存储于堆中,当代码块结束时,这个代码块所涉及到的所有局部变量会自动弹栈清空,指向对象的指针也会被回收,这时对象就没有指针指向,但依然存在于堆内存中,造成内存泄露.
Block内存管理:由于使用block很容易造成循环引用,因此一定要小心内存管理问题。最好在基类controller下重写dealloc,加一句打印日志,表示类可以得到释放。如果出现无打印信息,说明这个类一直得不到释放,表明很有可能是使用block的地方出现循环引用了。对于block中需要引用外部controller的属性或者成员变量时,一定要使用弱引用,特别是成员变量像_testId这样的,很多人都没有使用弱引用,导致内存得不到释放。
18.了解ARC么,什么时候会出现循环强引用问题?引用计数为0的引用实例是立即回收么?
19.了解autoreleasepool么,作用和使用场景是什么?内部声明的变量在什么时候释放?
20.内存泄漏可能会出现的几种原因,聊聊你的看法?
第一种可能:第三方框架不当使用;
第二种可能:block循环引用;
第三种可能:delegate循环引用;****
****第四种可能:**NSTimer循环引用******
第五种可能:非OC对象内存处理****
解决方法:非OC对象,其需要手动执行释放操作例:CGImageRelease(ref),否则会造成大量的内存泄漏导致程序崩溃。
其他的对于CoreFoundation框架下的某些对象或变量需要手动释放、C语言代码中的malloc等需要对应free
第六种可能:地图类处理****
解决方法:地图是比较耗费App内存的,因此在根据文档实现某地图相关功能的同时,需要注意内存的正确释放,大体需要注意的有需在使用完毕时将地图、代理等滞空为nil;
注意地图中标注(大头针)的复用,并且在使用完毕时清空标注数组等
第七种可能:大次数循环内存暴涨****
21. 对象释放执行dealloc方法的流程?
_objc_rootDealloc() ——> rootDealloc() 判断是否可以释放
{
1. non_pointer_isa 判断当前对象是否使用了非指针型的isa
2. weakly_referenced 当前对象是否有弱引用指针指向它
3. has_assoc 当前对象是否有关联对象
4. has_cxx_dtor 当前对象是否涉及C++的内容
5. has_sidetable_rc 当前对象的引用对象是否通过引用技术表存储的
}
都满足才可以释放 调用C函数的free() 否则调用函数object_dispose进行进一步清理
{
通过调用object_distructInstance()销毁实例 调用C函数的free()
}
网友评论