内存管理模块
看目录
1. 为何有堆栈,说说堆栈的区别,分别存放什么,为什么要有堆和栈,对象为何放到堆上,有没有栈对象
反正是因为 汇编时期慢慢演变过来的,经过很多人的摸索,人们发现变量主要是两种形式,一种内容短小(比如一个int整数),需要频繁访问,但是生命周期很短,通常只在一个方法内存活,而另一种内容可能很多(比如很长一个字符串),可能不需要太频繁的访问,但生命周期较长,通常很多个方法中可能都要用到,那么自然将这两类变量分开就显得比较理性,一类存储在栈区,通常是局部变量、操作符栈、函数参数传递和返回值,另一类存储在堆区,通常是较大的结构体(或者OOP中的对象)、需要反复访问的全局变量。堆区就是各种慢,申请内存慢,访问慢,修改慢,释放慢,整理慢(或者说GC垃圾回收),但优点也不言而喻,访问随机灵活,空间超大,在不超可用内存的情况下你要多大就给多大。
栈区就像临时工,干完就跑,所以超快,但是缺点也很多,比如生命周期短,一般只能在一个方法内存活,又比如你需要事先知道需要多大的栈(事实上绝大多数语言栈区要分配的大小编译期就确定了,Java就是这样),而且通常最大栈区可用内存都很小,你不可能往栈区里堆很多数据。
iOS不使用栈存对象和上面原因差不多
空间限制:前面讲到栈是一块连续的内存区域,所以栈的容量大小是系统预先设定好的,iOS 是1M,因此对象如果都在栈上创建不太现实,而堆只要物理内存不告警可以无限制使用。
生命周期不可控:Objective-C变量有效范围是由 “{}” 包含的块来决定的,也就是说栈对象的生命周期仅限于其所在的块里,出了块立马会被释放。一个对象被创建以后有可能会通过方法调用传递到别的方法,当栈对象的创建方法返回时,栈对象会被一起 pop 出栈而释放,导致其没法在别处被继续持有。此时 retain 操作会失效,除非用 copy 方法在想持有该栈对象的地方重新拷贝一份属于自己的栈对象。因此,栈对象会给对象的内存管理造成相当大的麻烦。
其实OC中是有栈对象的,那就是block,这也是为什么我们在使用block时要注意很多点,比如想持有一个block要用copy将block从栈拷贝到堆上。
因为ARC环境下,一旦Block赋值就会触发copy,block就会copy到堆上,这种情况是NSMallocBlock。ARC环境下也是存在NSStackBlock
的时候,这种情况下,block就在栈上。
MRC环境下,只有copy,block才会被复制到堆上,否则,block一直都在栈上,是__NSStackBlock。
2. 说说什么是引用计数技术+iOS里面如何实践这个技术的
计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术可以实现自动资源管理的目的。同时引用计数还可以指使用引用计数技术回收未使用资源的垃圾回收算法。
当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为1,在其他对象中需要持有这个对象时,就需要把该对象的引用计数加1,需要释放一个对象时,就将该对象的引用计数减1,直至对象的引用计数为0,对象的内存会被立刻释放。
iOS是如何应用这个技术的:
MRC下:
自己生成的对象,自己持有。
非自己生成的对象,自己也能持有。
不再需要自己持有的对象时释放。
非自己持有的对象无法释放。
当程序调用方法名以alloc、new、copy、mutableCopy开头的方法来创建对象时,该对象的引用计数加1。
程序调用对象的retain方法时,该对象的引用计数加1。
程序调用对象的release方法时,该对象的引用计数减1。
上面是MRC下生效的。
ARC下:ARC 是苹果引入的一种自动内存管理机制,会根据引用计数自动监视对象的生存周期,实现方式是在编译时期自动在已有代码中插入合适的内存管理代码以及在 Runtime 做一些优化。
ARC原则:判断一个对象是否需要释放不是通过引用计数来进行判断的,而是通过强指针来进行判断的,只要还有一个强指针变量指向对象,对象就会保持在内存中.
有变量标识符+属性标识符
__strong 是默认使用的标识符。只有还有一个强指针指向某个对象,这个对象就会一直存活
__weak 声明这个引用不会保持被引用对象的存活,如果对象没有强引用了,弱引用会被置为 nil
__unsafe_unretained 声明这个引用不会保持被引用对象的存活,如果对象没有强引用了,它不会被置为 nil。如果它引用的对象被回收掉了,该指针就变成了野指针。
__autoreleasing 用于标示使用引用传值的参数(id ),在函数返回时会被自动释放掉
@property (assign/retain/strong/weak/unsafe_unretained/copy) Number num
assign表明 setter 仅仅是一个简单的赋值操作,通常用于基本的数值类型,例如CGFloat和NSInteger
strong 表明属性定义一个拥有者关系。当给属性设定一个新值的时候,首先这个值进行 retain ,旧值进行 release ,然后进行赋值操作
weak 表明属性定义了一个非拥有者关系。当给属性设定一个新值的时候,这个值不会进行 retain,旧值也不会进行 release, 而是进行类似 assign 的操作。不过当属性指向的对象被销毁时,该属性会被置为nil。
unsafe_unretained 的语义和 assign 类似,不过是用于对象类型的,表示一个非拥有(unretained)的,同时也不会在对象被销毁时置为nil的(unsafe)关系。
copy 类似于 strong,不过在赋值时进行 copy 操作而不是 retain 操作。通常在需要保留某个不可变对象(NSString最常见),并且防止它被意外改变时使用。
3. 内存泄露所有场景+解决办法
循环引用
两个对象互相持有对象,这个可以设置弱引用解决。
两个对象互相持有对象,这个可以设置弱引用解决。
NSTimer的target持有self。NSTimer会造成循环引用,timer会强引用target即self,一般self又会持有timer作为属性,这样就造成了循环引用。
那么,如果timer只作为局部变量,不把timer作为属性呢?同样释放不了,因为在加入runloop的操作中,timer被强引用。而timer作为局部变量,是无法执行invalidate的,所以在timer被invalidate之前,self也就不会被释放。
解决办法是 分类添加block执行方法(iOS10已经添加这种api了)+添加中间件的方式 timer引用weakobj weakobj弱引用中间件。(中间件)
单例属性不释放
3. autorelease使用+原理
Autorelase Pool 提供了一种可以允许你向一个对象延迟发送release消息的机制。当你想放弃一个对象的所有权,同时又不希望这个对象立即被释放掉(例如在一个方法中返回一个对象时),Autorelease Pool 的作用就显现出来了。
当这段语句所处的 autoreleasepool 进行 drain 操作时,所有标记了 autorelease 的对象的 retainCount 会被 -1。即 release 消息的发送被延迟到 pool 释放的时候了。在 ARC 环境下,苹果引入了 @autoreleasepool 语法,不再需要手动调用 autorelease 和 drain 等方法。
用处
在 ARC 下,我们并不需要手动调用 autorelease 有关的方法,甚至可以完全不知道 autorelease 的存在,就可以正确管理好内存。因为 Cocoa Touch 的 Runloop 中,每个 runloop circle 中系统都自动加入了 Autorelease Pool 的创建和释放。当我们需要创建和销毁大量的对象时,使用手动创建的 autoreleasepool 可以有效的避免内存峰值的出现。
原理
在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop
ARC下,我们使用@autoreleasepool{}来使用一个AutoreleasePool随后编译器将其改写成下面的样子:
void *context = objc_autoreleasePoolPush();
// {}中的代码
objc_autoreleasePoolPop(context);
而这两个函数都是对AutoreleasePoolPage的简单封装,所以自动释放机制的核心就在于这个类。
要点
AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)
AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址
上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入
,在主线程的 NSRunLoop对象在每个event loop 开始前,系统会自动创建一个 autoreleasepool ,并在 event loop 结束时 drain。
autorelease
autorelease
4. swift, android flutter如何做内存管理的?
Swift 是自动管理内存的,这也就是说,我们不再需要操心内存的申请和分配。当我们通过初始化创建一个对象时,Swift 会替我们管理和分配内存。而释放的原则遵循了自动引用计数 (ARC) 的规则:当一个对象没有引用的时候,其内存将会被自动回收。但是,所有的自动引用计数机制都有一个从理论上无法绕过的限制,那就是循环引用 (retain cycle) 的情况。在 Swift 中除了 weak 以外,还有另一个冲着编译器叫喊着类似的 "不要引用我" 的标识符,那就是 unowned。它们的区别在哪里呢?如果您是一直写 Objective-C 过来的,那么从表面的行为上来说 unowned 更像以前的 unsafe_unretained,而 weak 就是以前的 weak。用通俗的话说,就是 unowned 设置以后即使它原来引用的内容已经被释放了,它仍然会保持对被已经释放了的对象的一个 "无效的" 引用,它不能是 Optional 值,也不会被指向 nil。如果你尝试调用这个引用的方法或者访问成员属性的话,程序就会崩溃。而 weak 则友好一些,在引用的内容被释放后,标记为 weak 的成员将会自动地变成 nil (因此被标记为 @weak 的变量一定需要是 Optional 值)。关于两者使用的选择,Apple 给我们的建议是如果能够确定在访问时不会已被释放的话,尽量使用 unowned,如果存在被释放的可能,那就选择用 weak。
dart,iOS,android内存管理简单比较
5.weak和assign的区别 + weak生命过程(创建到nil)
assign:诞生于MRC,assign是指针赋值,不对引用计数操作,使用之后如果没有置为nil,可能就会产生悬空指针。一般是对C基本数据类型成员变量的声明,适用于基本数据类型如int,float,struct等,当然也可以用在对象类型成员变量上,只是其代表的意义只是单纯的指针赋值。举例如果把对象A的指针赋值给assign声明的成员变量B,则B只是简单地保存此指针的值,且并不持有对象A,也就意味着如果A被销毁,则B就指向了一个已经被销毁的对象,如果再对其发送消息会引发崩溃。
weak:诞生于ARC,weak表示的是一个弱引用,这个引用不会增加对象的引用计数,并且在所指向的对象被释放之后,weak指针会被置为nil。避免悬空指针可能产生崩溃的问题。weak引用通常是用于处理循环引用的问题。弱引用,在对象释放后置为 nil,避免错误的内存访问。用更通俗的话来表述是:weak 可以在不增加对象的引用计数的同时,又使得指针的访问是安全的。(可以利用这个特性实现weak singleton 让单例对象在不再使用后释放)
weak被释放为nil,需要对对象整个释放过程了解,如下是对象释放的整体流程:
1、调用objc_release
2、因为对象的引用计数为0,所以执行dealloc
3、在dealloc中,调用了_objc_rootDealloc函数
4、在_objc_rootDealloc中,调用了object_dispose函数
5、调用objc_destructInstance
6、最后调用objc_clear_deallocating。
objc_clear_deallocating该函数的动作如下:
1、从weak表中获取废弃对象的地址为键值的记录
2、将包含在记录中的所有附有 weak修饰符变量的地址,赋值为nil
3、将weak表中该记录删除
4、从引用计数表中删除废弃对象的地址为键值的记录
weak是Runtime维护了一个hash(哈希)表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址)数组.
这个就是大概的流程,具体详细代码比较麻烦负责,用到的时候再看吧。
iOS 中 weak 的实现
网友评论