1、讲一下对iOS内存管理的理解
看到这个问题,我准备是从MRC和ARC结合引用计数机制来讲解下内存管理,想到的是比较常规和浅显,更深的原理回答为:(三种方案的结合)
- TaggedPointer:主要是针对类似于NSNumber的小对象类型
- NONPOINTER_ISA (64位系统下)
- 第一位的0和1代表的是纯isa指针还是NONPOINTER_ISA指针
- 第二位代码是否有关联对象
- 第三位代表是否有C++代码
- 接下来33位代表执行的内存地址
- 弱引用的标记
- 是否dealloc的标记等
- 散列表(包括引用计数表和weak表)
- SideTables在非嵌入式的64位系统中,有64张SideTable表
- 每一张SideTable主要是由三部分组成,自旋锁、引用计数表、弱引用表
- 全局的引用计数之所以不存在同一张表中,是为了避免资源竞争,解决效率问题
- 引用计数表引入了分离锁的概念,将一张表拆分成多个部分,对他们分别加锁,可以实现并发操作,提高效率
2、使用ARC应该遵循的原则?
- 不能使用retain、release、retainCount、autorelease
- 不能使用NSAllocateObject、NSDeallocateObject
- 不能使用NSZone
- 不能显示调用dealloc
- 必须遵守内存管理方法的命名规则
- 对象性变量不可以作为C语言结构体成员
- 使用@aotureleasePool来代替NSAutoreleasePool
- 显示转换id和void *
3、 ARC自动管理内存的原则
- 自己生成的对象,自己持有
- 非自己生成的对象,自己可以持有
- 自己持有的对象不再需要时,需要对其进行释放
- 非自己持有的对象不能释放
4、访问__weak修饰的变量,是否已经注册在了@autoreleasePool中?为什么?
__weak修饰符引用的变量属于弱引用,当然已经注册在了@autoreleasePool中,以延长对象的生命周期,否则创建即释放销毁
5、ARC的retainCount是怎么存储的?
存在64张哈希表中,根据哈希算法去查找所在的位置,不需要遍历,非常快捷
引用计数表(哈希表): 通过指针的地址,查找到引用计数的地址,大大提升查找效率,通过DisguisedPtr(objc_object)函数存储,同时也通过这个函数查找,避免循环遍历
6、简要说一下@autoreleasePool的数据结构
简单说是双向链表,头尾相接,有parent和child指针
每创建一个释放池,会在首部创建一个哨兵对象,作为标记
最外层释放池的顶端会有一个next指针。当链表的容量满了,就会在链表的顶端,指向下一张表
7、__weak和__unsafe_unretain的区别
__weak修饰的指针变量在其引用的对象内存地址销毁后,自动置为nil
__unsafe_unretain不会置为nil,容易出现悬垂指针,发生奔溃,但是效率比__weak高
8、ARC情况下,为什么还是需要@autoreleasePool的存在?
避免内存峰值过高,及时释放不需要的内存空间
9、__weak修饰的变量,如何实现在变量没有强引用后自动置为nil?
详细解答
对象的弱引用__weak表也是一张哈希表
被__weak修饰符引用的对象地址是key,所有指向这块内存地址的指针会被添加在一个数组里,值为Value,当对象的内存地址销毁后,数组里面所有对象被置为nil
10、说说对strong、copy、assign、weak、__unsafe_unretain、__autoreleasing关键词的理解
- strong:表示修饰的指针强引用指向的对象,使得对象的引用计数加1,该对象只要引用计数不为0就不会被销毁,可以通过将变量强制赋值nil来进行销毁
- copy:和strong类型,多用于修饰可变类型的不可变对象上,例如NSString、NSArray、NSDictionary
- assign:主要用来修饰基本数据类型,存储在栈中,无需程序员管理;可以用来修饰对象,但是会出现问题
- weak:修饰的指针弱引用指针的对象,不持有对象,对象的引用计数不加1,对象销毁时,修饰的指针自动置为nil。用来修饰对象,避免循环引用,不可以修饰基本数据类型。
- __unsafe_unretain:类似于weak,当时对象被释放后,修饰的指针依然保存着之前的地址,被释放后的地址变为僵尸对象,访问被释放的地址就会出问题,所以是不安全的
- __autoreleasing:将对象赋值给__autorelease修饰的指针等同于在MRC情况下调用对象的autorelease方法,即将对象添加进自动释放池
11、ARC在编译时做了哪些工作?
根据代码的上下文语境,在合适的位置插入了retain和release
12、ARC在运行时做了哪些工作?
- 主要是__weak关键字。修饰的指针引用的对象引用计数为0时自动设置为nil,运行时在工作
- 为了保证向后兼容性,ARC 在运行时检测到类函数中的 autorelease 后紧跟其后 retain,此时不直接调用对象的 autorelease 方法,而是改为调用 objc_autoreleaseReturnValue。 objc_autoreleaseReturnValue 会检视当前方法返回之后即将要执行的那段代码,若那段代码要在返回对象上执行 retain 操作,则设置全局数据结构中的一个标志位,而不执行 autorelease 操作,与之相似,如果方法返回了一个自动释放的对象,而调用方法的代码要保留此对象,那么此时不直接执行 retain ,而是改为执行 objc_retainAoutoreleasedReturnValue函数。此函数要检测刚才提到的标志位,若已经置位,则不执行 retain 操作,设置并检测标志位,要比调用 autorelease 和retain更快。
13、函数返回一个对象时,会对对象autorelease么?为什么?
会,为了延长返回对象的生命周期,给其他使用者留足够的调用时间
14、说一下垂悬指针和野指针?
- 垂悬指针:指针指向的内存已经被释放了,但是指针还存在,这是一个悬垂指针或者迷途指针
- 野指针:没有进行初始化的指针,其实都是野指针
15、属性默认的关键字是什么?
- MRC:对于对象依次是 atomic,readWrite,retain
- ARC:对于对象依次是 atomic,readWrite,strong
如果是基本数据类型,retain/strong变成assign
16、内存中的5大区分别是什么?
- 栈区:由编译器自动分配管理释放,存放函数的参数值、局部变量的值等,操作方式类似于数据结构中的栈
- 堆区:一般是由程序分配管理释放,如果程序员不释放,程序结束时可能由OS回收。与数据结构中的堆是两回事,分配方式类似于链表
- 全局静态区:全局变量和静态变量存储在一起,初始化的全局变量和静态区在一块区域;没有初始化的全局变量和没有初始化的静态变量在相邻的另一块区域,程序结构后由系统释放
- 常量区:存放的是常量字符串。程序结构后由系统释放
- 代码区:存放函数体的二进制代码
17、深拷贝和浅拷贝,集合类深拷贝如何实现
- 深拷贝:内存拷贝,重新开启一块内存区域
- 浅拷贝:指针拷贝,让目标对象指针和源对象指向同一片内存区域
- 集合类深拷贝通过归档、解档实现
源对象类型 | 拷贝方式 | 目标对象类型 | 拷贝类型 |
---|---|---|---|
mutable对象 | copy | 不可变 | 深拷贝 |
mutable对象 | mutablCopy | 可变 | 深拷贝 |
immutable对象 | copy | 不可变 | 浅拷贝 |
immutable对象 | mutableCopy | 可变 | 深拷贝 |
18、BAD_ACCESS在什么情况下出现?
访问了已经被销毁的内存空间,就会报这个错误。根本原因是由悬垂指针没有被释放
19、讲一下@dynamic关键字
@dynamic意味着编译器不会帮助我们自动合成setter和getter方法,需要我们手动实现。
20、@autoreleasePool的释放时机?
主要考察的是自动释放池与RunLoop的关系
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 了。