1.什么是 ARC?
ARC
是 iOS 5
引入的内存管理新功能 -- 自动引用计数
。它的工作原理大致是这样:当我们编译源码时,编译器会分析源码中每个对象的生命周期,然后基于这些对象的生命周期,来添加相应的引用计数操作代码。所以,ARC
是工作在编译期的一种技术方案。
这样的好处是:编译之后,ARC
与非 MRC
代码是没有什么差别的,所以二者可以在源码中共存。实际上,你可以通过编译参数 -fno-objc-arc
来关闭部分源代码的 ARC
特性。由于 ARC
能够深度分析每一个对象的生命周期,它能够做到比 MRC
更加高效。
例如在一个函数中,对一个对象刚开始有一个引用计数 +1
的操作,之后又紧接着有一个 -1
的操作,那么编译器就可以把这两个操作都优化掉。
2 ARC 的核心思想?
- 自己生成的对象,自己持有
- 非自己生成的对象,自己可以持有
- 自己持有的对象不再需要时,需要对其进行释放
- 非自己持有的对象无法释放
3 ARC 在使用时应该遵循的原则?
- 不能使用
retain
、release
、retainCount
、autorelease
。 - 不可以使用
NSAllocateObject
、NSDeallocateObject
。 - 必须遵守内存管理方法的命名规则。
- 不需要显示的调用
Dealloc
。 - 使用
@autoreleasePool
来代替NSAutoreleasePool
。 - 不可以使用区域
NSZone
。 - 对象性变量不可以作为
C
语言的结构体成员。 - 显示转换
id
和void*
。
4 ARC 在编译时做了哪些工作?
- 自动调用
保留(retain)
与释放(release)
的方法 - 相对于垃圾回收这类内存管理方案,ARC 不会带来运行时的额外开销,所以对于应用的运行效率不会有影响。
ARC
会把能够互相抵消retain
、release
、autorelease
,操作简化,如果发现在同一个对象上执行了多次保留与释放操作,那么ARC
有时可以成对的移除这两个操作。
5 ARC 在运行时做了哪些工作?
主要是指
weak
关键字。weak
修饰的变量能够在引用计数为0
时被自动设置成nil
,显然是有运行时逻辑在工作的。关于原因会单独开一个问题为了保证向后兼容性,
ARC
在运行时检测到类函数中的autorelease
后紧跟其后retain
,此时不直接调用对象的autorelease
方法,而是改为调用objc_autoreleaseReturnValue
。
objc_autoreleaseReturnValue
会检视当前方法返回之后即将要执行的那段代码,若那段代码要在返回对象上执行retain
操作,则设置全局数据结构中的一个标志位,而不执行autorelease
操作,与之相似,如果方法返回了一个自动释放的对象,而调用方法的代码要保留此对象,那么此时不直接执行retain
,而是改为执行objc_retainAoutoreleasedReturnValue
函数。此函数要检测刚才提到的标志位,若已经置位,则不执行retain
操作,设置并检测标志位,要比调用autorelease
和retain
更快。
_myPerson = [ECOPerson personWithName:@“Bob”];
ECOPerson * tmp = [ECOPerson personWithName:@“Bob”];
_myPerson = [tmp retain];
6 函数返回一个对象时,会对对象autorelease么?为什么?autorelease是什么时候释放的?
会对对象 autorelease
,因为需要在稍后释放对象,从而给调用者留下足够长的时间,使其可以在需要时先保留返回值。此方法可以保证对象在跨越方法调用边界时一定存活。
除非你有自己的自动释放池,否则这个时机就是当前线程,当前事件循环结束时,就是 RunLoop
结束时(observer -> beforeWaiting)。
// 情况一:
@autoreleasepool {
NSObject * obj = [NSObject new];
[obj autorelease];
NSLog(@"%d",[obj retainCount]); //1
}
// 情况二:
NSObject * obj = [NSObject new];
[obj autorelease];
[obj autorelease];
NSLog(@"%d",[obj retainCount]);
NSLog(@"%d",[obj retainCount]);
// 崩溃
// 情况三:
NSObject * obj;
@autoreleasepool {
obj = [NSObject new];
[obj autorelease];
NSLog(@"%d",[obj retainCount]); // 1
}
//crash 出了大括号就已经被销毁了
NSLog(@"%d",[obj retainCount]);
7 为什么已经有了 ARC ,还需要 @autoreleasePool?
提到 OC
的 RC
,首先要横向对比一下 Android
的 GC(垃圾回收机制)
,GC
的内存回收是集中式回收(定期回收),而 RC
的回收是伴随整个运行时的,所以 android
机器有种 时“卡”时“流畅” 的感觉,而 iOS
总体比较均匀,缺乏像 GC
的集中式回收内存的类似机制,所以猜测 Pool
的产生也是弥补 RC
的这一不足,在 RC
基础上进行内存优化的一种手段。
8 简要阐述内存相关的关键字?
Strong
Strong
修饰符表示指向并持有该对象,其修饰对象的引用计数会加1。该对象只要引用计数不为0就不会被销毁。当然可以通过将变量强制赋值nil
来进行销毁。
Weak
weak
修饰符指向但是并不持有该对象,引用计数也不会加1。在Runtime
中对该属性进行了相关操作,无需处理,可以自动销毁。weak
用来修饰对象,多用于避免循环引用的地方。weak
不可以修饰基本数据类型。
assign
assign
主要用于修饰基本数据类型,
例如NSInteger
,CGFloat
,存储在栈中,内存不用程序员管理。assign
是可以修饰对象的,但是会出现问题。
copy
copy
关键字和strong
类似,copy
多用于修饰有可变类型的不可变对象上NSString
,NSArray
,NSDictionary
上。
__unsafe_unretain
__unsafe_unretain
类似于weak
,但是当对象被释放后,指针已然保存着之前的地址,被释放后的地址变为僵尸对象
,访问被释放的地址就会出问题,所以说他是不安全的。
__autoreleasing
将对象赋值给附有
__autoreleasing
修饰的变量等同于ARC
无效时调用对象的autorelease
方法,实质就是扔进了自动释放池。
9 说一下什么是悬垂指针?什么是野指针?
悬垂指针
指针指向的内存已经被释放了,但是指针还存在,这就是一个
悬垂指针
或者说迷途指针
野指针
没有进行初始化的指针,其实都是
野指针
10 内存管理默认的关键字?
MRC
@property (atomic,readWrite,retain) NSString *name;
ARC
@property (atomic,readWrite,strong) NSString *name;
10 __weak 和 __unsafe_unretain 的区别?
__weak
是__unsafe_unretain
升级版,__unsafe_unretain
在指向的内存地址销毁后,指针本身并不会自动销毁,这也就造成了野指针,之后容易造成 Crash。__weak
在指向的内存销毁后,可以将指针变量置为nil
,这样更加安全。
11 __weak 修饰的变量在地址被释放后,为何被置为 nil?
在 Runtime
中专门维护了一个用于存储 weak
指针变量的 weak
表,这实际上是一个 Hash
表。这个表 key
是 weak指针
所指向的内存地址,value
是指向这个内存地址的所有 weak指针
,实际上是一个数组。
过程可以总结为3步
- 1、初始化时:
runtime
会调用objc_initWeak
函数,初始化一个新的weak指针
指向对象的地址。 - 2、添加引用时:
objc_initWeak
函数会调用objc_storeWeak()
函数,objc_storeWeak()
的作用是更新指针指向,创建对应的弱引用表。 - 3、释放时,调用
clearDeallocating
函数。clearDeallocating
函数首先根据对象地址获取所有weak指针
地址的数组,然后遍历这个数组把其中的数据设为nil
,最后把这个entry
从weak表
中删除,最后清理对象的记录。
12 为什么在 MRC
已经有 __weak
的情况下,还需要 _unsafe_unretain
。?
兼容性考虑。
iOS4
以及之前还没有引入weak
,这种情况想表达弱引用的语义只能使用unsafe_unretained
。这种情况现在已经很少见了。性能考虑。使用
weak
对性能有一些影响,因此对性能要求高的地方可以考虑使用unsafe_unretained
替换weak
。
一个例子是YYModel
的实现,为了追求更高的性能,其中大量使用unsafe_unretained
作为变量标识符。
13如何打破循环引用?
- 注意变量作用域,使用
autorelease
让编译器来处理引用。 - 使用弱引用(
__weak
)。 - 当实例变量完成工作后,将其置为
nil
。
14能不能用 assign
修饰 Nsobject
类型?
也可以,但有可能出问题。
使用 assign
修饰 Nsobject
类型,赋值之后会被立即释放,对应的属性也就变成了野指针。
运行时跑到属性有关操作会直接崩溃掉。
15 内存中的5大区都是什么?
- 栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其 操作方式类似于数据结构中的栈。
- 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
- 全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的 全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后由系统释放。
- 文字常量区:常量字符串就是放在这里的。 程序结束后由系统释放。
- 程序代码区:存放函数体的二进制代码。
16 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
了。
17 简要说一下自动引用计数的数据存储结构?
可以参考这篇文章 : 黑幕背后的Autorelease