ARC 处理原理
- arc 是oc 编译器的特性,而不是运行时特性或者垃圾回收机制,ARC 所做的只不过是在代码编译阶段为你自动在合适的位置插入 release 或 autorelease,只要没有强指针指向对象,对接就会释放。
- 前端编译器
- 前端编译器会为“拥有的”每一个对象插入一个 release 语句。如果对象的所有权修饰符是__strong,那么它就是被拥有的。如果在某个方法内创建一个对象,前端编译器会在方法末尾自动插入 release 语言可以销毁他。而类拥有的对象(实例变量、属性)会在 dealloc 方法内被释放。此外,由编译器生成的代码甚至会比自己写的 release 语言的性能还要好,因为编译器可以做出一些假设。在 ARC 中,没有类可以覆盖 release 方法,也没有调用他的必要。ARC 会通过直接调用 objc_release 来优化调用过程。而对于 retain 也是同样的方法。ARC 会调用 objc_retain 来取代保留消息。
- ARC 优化器
- 虽然前端编译器听起来很厉害的样子,但代码证有时仍会出现几个对 retain 和 release 的重复调用。ARC 优化器负责移除多余的 retain 和 release 语句,确保生成的代码运行速度高于手动引用计数的代码。
下面对 oc 内存管理描述错误的是?
- A. 当使用 ARC 类管理内存时,代码中不可以出现 autorelease
- B. autoreleasepool 在 drain 的时候会释放在其中分配的对象
- C. 当使用 ARC 来管理内存的时,在线程中大量分配对象而不用 autoreleasepool 则可能会造成内存泄漏
- D. 在使用 ARC 的项目中不能使用 NSZone
- A。理由:ARC 只是在大多时候编译器自动为我们添加上内存管理的代码,只是我们的源代码看不到而已,但是在编译时,编译器会添加上相关的内存管理代码。对于自动释放池,在 drain 时会讲自动释放池中所有对象的引用计数减一,若引用计数为0,则会自动释放掉其内存。如果在线程中需要大量的分配内存,我们理应添加上自动释放池,以防内存泄漏。比如在 for 循环中要分配大量的内存处理数据,那么我们应该在 for 循环内添加自动释放池,在每个循环后就讲内存释放掉,防止内存泄漏。在 ARC 项目中,自然不能手动使用 NSZone,也不能调用父类的 dealloc。
MRC 文件和 ARC 工程混合编程时,需要在文件的 compiler Flags 上添加参数 -fno-objc-arc
什么情况下使用 weak 关键字,相比 assign 有什么不同?
- 在 ARC 中,有可能出现循环引用的时候,往往通过让其中一端使用 weak 来解决,比如 delegate 代理属性。自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak,自定义 IBOutlet 控件属性一般也使用 weak,当然,也可以是 strong。
- weak 此特性表明该属性定义一种“非拥有关系”。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特性同 assign 的设置方法只会执行纯量类型的简单赋值操作。
- assign 可以用于非 oc 对象,而 weak 必须使用 oc 对象。
调用对象的 release 方法会销毁对象吗?
- 不会,调用 release 方法只是将对象的引用计数器-1,当对象的引用计数器为0的时候会调用了对象的 dealloc 方法才能进行释放对象内存。
自动释放池常见代码
for (int i = 0; i < someLargeNumber; ++i){
NSString *string = @"abc";
string = [string lowercaseString];
string = [string stringByAppendingString:@"xyz"];
NSLog(@"%@", string);
}
- 上面代码存在问题:问题处在每执行一次循环,就会有一个 string 加到当前runloop 中的自动释放池中,只有当自动释放池被 release 的时候,自动释放池中的标示了 autorelease 的这些数据所占用的内存空间才能被释放掉。假设,当someLargeNumber 大道一定程度的时候,内存空间将被耗尽而没有释放掉,所以就出现内存溢出的现象。
- 解决办法1:如果 i 比较大,可以用@autoreleasepool{}解决,放在 for 循环结束后,销毁对象,解决占据栈区内存问题
- 解决方案2:如果 i 玩命大,一次循环都会造成自动释放池被填满,自动释放池房子 for 循环内,每次循环都将上一次创建的对象 release
for (int i = 0; i < 1000; ++i) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
string = [string lowercaseString];
string = [string stringByAppendingString:@"xyz"];
NSLog(@"%@", string);
//释放池
[pool drain];
}
OC 对象的内存布局是怎样的?
- 由于 oc 中没有多继承,因此其内存布局还是很简单的,就是最前面有一个 isa 指针,然后父类的实例变量存放在子类的成员变量之前。
看下面的程序,第一个 NSLog 会输出什么?这时 str 的 retainCount 是多少?第二个和第三个呢?为什么?
NSMutableArray *array = [[NSMutableArray array] retain];
NSString *str = [NSString stringWtihFormat:@"test"];
[str retain];
[array addObject:str];
NSLog(@"%@%d", str, [str retainCount]);
[str retain];
[str release];
[str release];
NSLog(@"%@%d", str, [str retainCount]);
[array removeAllObjects];
NSLog(@"%@%d", str, [str retainCount]);
str 的 retainCount 创建+1,retain+1,进入数字自动+1 -> 3
retain+1, release-1, release-1 -> 2
数组删除所有对象,所有数组内的对象自动-1 ->1
回答 person 的 retainCount 值,为什么?
Person *person = [[Person alloc] init];
self.person = person;
创建的时候 person 的 retainCount 是1
在 self.person 时,如果是 assgin,person 的retainCount 不变,仍为1,若是 retain person 的 retainCount 的值加1,变为2
若是 copy person 的 retainCount 值不便,仍为1
什么时候需要在程序中创建内存池?
- 用户自己创建的数据线程,则需要创建该线程的内存池。
如果我们不创建内存池,是否有内存池提供给我们?
- 界面线程维护着自己的内存池,用户自己创建的数据线程,则需要改线程的内存池。
苹果是如何实现 autoreleasepool 的?
- autoreleasepool 以一个队列的形式实现,主要通过下列3个函数完成
- objc_autoreleasepoolPush
- objc_autoreleasepoolPop
- objc_autorelease
- 看函数名就可以知道,对 autorelease 分别执行 push 和 pop 操作。销毁对象时执行 release 操作。
objc 使用什么机制管理对象内存?
- 通过引用计数器(retainCount)的机制来决定对象是否要被释放。每次 runloop 完成一次循环的时候,都会检查对象的 retainCount,如果 retainCount 为0,说明该对象没有地方需要继续使用了,可以释放掉了。
为什么要进行内存管理?
- 因为移动设备的内存极其有限,当一个程序所占内存达到一定值时,系统会发出内存警告。当程序达到更大的值时,程序会闪退,影响用户体验,为了保证程序的运行流程,必须进行内存管理。
内存管理范围
- 管理所有继承自 NSObject的对象,对基本数据类型无效。是因为对象和其他数据类型在系统中的存储空间不一样,其他局部变量主要存储在栈区(因为基本数据类型占用的存储空间是固定的,一般放于栈区)。而对象存储在堆中,当代码块结束时,这个代码块所涉及到的所有局部变量都会自动弹栈清空,指向对象的指针也会被回收,这时对象就没有指针指向,但依然存在堆内存中,造成内存泄漏。
objc 使用什么机制管理对象内存(或者内存管理方式有哪些?)
- MRC(manual retain-release) 手动内存管理
- ARC(automatic reference counting)自动引用计数
- 垃圾回收(Garbage collection)。但是 iOS 不支持垃圾回收,ARC 作为 LLVM3.0编译器的一项特性,在 iOS5.0(xcode4)版本后推出。
- ARC 的判断准则,只要没有强指针指向对象,对象就会被释放。
内存管理原则
- 只要还有人在使用这个对象,那么这个对象就不会被回收。
- 只有你想使用这个对象,那么就应该让这个对象的引用计数加1
- 当你不想使用这个对象,应该让这个对象引用计数器减1
- 谁创建由谁来 release。如果通过 alloc、new、copy 来创建一个对象,当你不想使用这个对象的时候必须要调用 release或者 autorelease 让引用计数减1。不是你创建的就不用你负责 release。
- 谁 retain 谁 release。只要你调用了 retain,无论这个对象如何生成,都需要调用 release。
- 总结:有加就应该有减,曾让某个对象计数器加1,就应该让其在最后减1
内存管理研究的对象
- 野指针:指针变量没有进行初始化或者指向的空间已经被释放。
- 使用野指针调用对象方法,会报异常,程序崩溃。
- 通常再掉完 release 方法后,把保存对象指针的地址清空,赋值为nil,找 oc 中没有空指针异常,所以[nil retain];调用方法不会有异常。
- 内存泄漏
- 如 Person *person = [Person new];(对象提前赋值 nil 或者被清空)在栈区的 person 已经被释放了,而堆区 new 产生的对象还没有释放,就会造成内存泄漏。
- MRC 引用计数器模式下,造成内存泄漏。1.没有配对释放,不符合内存管理原则。2.对象提前赋值 nil 或者清空,导致 release 不起作用。
- 僵尸对象:堆中已经被释放的对象(retainCount = 0)
- 空指针:指的是赋值为空。nil
如何判断一个对象已经被销毁了
- 重写 dealloc 方法,对象销毁时,会调用,重写一定要[super dealloc] retainCount=0,使用 retain 能否复活对象
- 已经释放的对象无法复活
对象与对象之间存在的关系
- 继承关系
- 组合关系(是一种强烈包含关系)
- 依赖关系(对象作为方法参数传递)
对象的组合关系中,确保成员变量不被提前释放?
- 重写 set 方法,在 set 方法中,retain 该对象。
成员变量对象,在哪里配对释放
- dealloc 中
对象组合关系中,内存泄漏有哪几种情况?
- set 方法没有 retain 该对象
- 没有 release 旧对象
- 没有判断向 set 方法中传入的是否为同一个对象
正确重写 set 方法
- 判断是否为同一个对象
- release 旧对象
- retain 新对象
分别描述内存管理的要点、autorelease、release、NSAutoreleasePool?并说明 autorelease 是什么时候被 release 的?简述什么时候由你释放对象,什么时候不释放?[NSAutoreleasePool release] 和 [NSAutoreleasePool drain]有什么区别?
- 内存管理要点:oc 使用引用计数机制(retainCount)来管理内存
- 内存每被引用一次,该内存的引用计数加1,每被释放一次减1
- 当引用计数为0的时候,调用该对象的 dealloc 方法,来彻底销毁这个该对象。
- alloc、allocWithZone、new(带初始化)时,该对象的引用计数+1
- retain:手动为该对象引用计数+1
- copy:对象引用计数+1; 注意 copy 的 oc 数据类型是否有 mutable,如有为深拷贝,新对象计数为1,如果没有为浅拷贝,计数+1
- mutableCopy:生成一个新对象,新对象引用计数为1
- release:手动为该对象引用计数-1
- autorelease:把该对象放入自动释放池,当自动释放池释放时,其内的对象引用计数-1
- NSAutoreleasePool:NSAutoreleasePool 是通过接受对象向他发送的 autorelease 消息,记录该对象的 release 消息,当自动释放池被销毁时,会自动向自动释放中的对象发送 release 消息。
- autorelease 是在自动释放池被销毁,向池中的对象发送 release,只能释放自己拥有的对象。
- 区别是:在引用计数环境下(在不适用 ARC 情况下),两者基本是一样的,在 GC(垃圾回收机制)环境下,release 是一个 no-op(无效操作),所以无论是不是 GC 都使用 drain
自动释放池是什么,如何工作?
- 什么是自动释放池:用来存储多个对象类型的指针变量
- 自动释放池对池内对象的作用:存入池内的对象,当自动释放池被毁掉时,会对池内对象全部做一次 release 操作。
- 对象如何加入池中:调用对象的 autorelease 方法
- 自动释放池能嵌套吗?:能
- 自动释放池何时被销毁?:简单的看,autorelease 的“}”执行完成后。而实际情况是 autorelease 对象在当前的 runloop 迭代结束时是否,而它能够释放的原因是系统在每个 runloop 迭代中都加入了自动释放池 push 和 pop
- 多次调用对象的 autorelease 方法会导致野指针异常
- 自动释放池的作用:将对象和自动释放池建立关系,池子内调用 autorelease,在自动释放池销毁时销毁对象,延迟 release 销毁时。
自动释放池什么时候释放?
- 通过 observer 监听 runloop 的状态,一旦监听到 runloop 即将进入睡眠等状态,就释放自动释放池(kCFRunLoopBeforeWaiting)
iPhone OS 有没有垃圾回收?autorelease 和垃圾回收(GC)有什么关系?
- iOS 中没有垃圾回收。autorelease 只是延迟释放,gc 是每隔一段时间询问程序,看是否有无指针指向的对象,若有,就将它回收。他们两者没有关系。
ARC 问题
- 什么是 ARC 机制:自动引用计数
- 系统判定对象是否被销毁的依据:指向对象的强指针是否被销毁
- ARC 的本质:对 retainCount 计算,创建+1,清空指针-1或者达到 autorelease 的大括号-1
- ARC 的目的:不需要程序员关心 retain 和 release 操作
- 如何解决 ARC 机制下类的相互引用:.h 文件中使用@class 关键字声明一个类,两端不能都用强指针,一端用 strong 一端用 weak
ARC 通过什么方式帮助开发者管理内存?
- ARC 相对于 MRC,不是在编译时添加 retain、release、autorelease 这么简单。应该是编译期和运行期两部分共同帮助开发者管理内存。
- 在编译期,ARC 用的是更底层的 C 接口实现的 retain、release、autorelease,这样做性能更好,也是为什么不能在 ARC 环境下手动 retain、release、autorelease,同时对同一上下文的同一对象的 retain、release、autorelease 操作进行优化(即忽略不必要的操作)
- ARC 也包含运行期组件,这个地方做的优化比较复杂,但也不能被忽略,手动去做未比优化得好,因此直接交给编译器来优化。
开发项目时你是怎么检查内存泄漏
- 静态分析 analyze
- instruments 工具里面有一个 leak 可以动态分析。如果在 block 中多次使用 weakself 的话,可以在 block 中先使用 strongself,防止 block 运行时 weakself被意外释放
- 对于 MRC,讲 weak 改为 block 即可。
设计一个简单的图片内存缓存器(移除策略是一定要说)
- 内存缓存是个通用话题,每个平台都会用到。ceche 算法会影响到整个 app 的表现。了解 cache 策略及其各自的特点。
- 常见的有 FIFO、LRU、LFU 等等。由于 NSCache 的缓存策略不透明,一些 App 开发者会选择自己做一套 cache 机制,其实并不难。
- FIFO:新访问的数据插入 FIFO 队列尾部,数据在 FIFO 队列中顺序移动;淘汰 FIFO 队列头部的数据
- LRU:新数据插入到链表头部,每当缓存数据命中,则将数据迁移到链表头部;当链表满的时候,将链表尾部的数据丢弃。
- LFU:新加入数据插入到队列尾部(因为引用计数为1);队列中的数据被访问后,引用计数增加,队列重新排序;当需要淘汰数据的时候,将已经排序的列表最后的数据块删除;
常见的出现内存循环引用的场景有哪些?
- 定时器(NSTimer):NSTimer 经常会被作为某个类的成员变量,而 NSTimer 初始化时,要指定 self 为 target,容易造成循环引用(self->timer->self)。另外,若 timer 一直处于 validate 的状态,则其引用计数将始终大于0,因此不再使用定时器以后,应该先调用 invalidate 方法
- block 的使用:block 在 copy 时都会对 block 内部用到的对象进项强引用(ARC)或者 retainCount 加1(非 ARC)。在 ARC 与 MRC 环境下对 block 使用不当会引起循环引用问题,一般表现为,某个类将 block 作为自己的属性变量,然后该类在 block 的方法体里面又使用了该类本身,简单的说就是 self.someBlock=Type var{[self dosomething]};或者 self.otherVar = XXX;或者_otherVar=...};出现循环引用问题是:self->block-self 或者 self->block->_ivar(成员变量)
- 代理(delegate):在委托问题上出现循环问题已经是老生常谈了,规避该问题就是在的时候声明 delegate 是用 assgin(MRC)或者 weak(ARC)
对象添加到通知中心中,当通知中心发送通知时,这个对象已经被释放了,可能会出现什么问题?
- 其实这种只是考查对通知的简单应用。通知时一堆多的关系,主要使用场景是跨模块传值。当某个对象加入通知中心后,若在对象被销毁前不将该对象从通知中心移除,当发送通知时,就会造成崩溃。这是很常见的。所以在添加到通知中心后,一定要在释放前移除。
ARC 下不显示指定任何属性关键时,默认的关键字都有哪些?
- 对于基本类型默认关键字是:atomic,readwrite,assign
- 对于 oc 对象,atomic,readwrite,strong