Link Search Menu Expand Document

内存管理

几个名次解释

内存泄漏 指动态分配内存的对象在使用完后没有被系统回收,导致该对象始终占用内存,又无法通过代码访问,属于内存管理出错。如果出现大量内存泄漏,那么会导致系统内存不足的问题。

安全释放 指释放掉不再使用的对象的同时不造成内存泄漏或者悬挂指针。

僵尸对象 一个引用计数为0的Objective-C对象被释放后就变成了僵尸对象。僵尸对象的内存已经被系统回收,虽然该对象可能还存在,数据依然在内存中,但僵尸对象已经是不稳定对象了,不可以再访问或使用,它的内存随时都可能被别的对象申请而占用。僵尸对象所占用的内存是正常的,不会导致内存泄漏。“use after free” issue

野指针 又叫“悬挂指针”,野指针出现的原因是指针没有赋值,或者指针指向的对象已经被释放掉了。野指针指向一块随机的垃圾内存,向他们发送消息回报 EXC_BAD_ACCESS 错误导致程序崩溃。

空指针 不同于野指针,它是一个没有指向任何内容的指针。空指针是有效指针,值为nil、NULL、Nil或0等,给空指针发送消息不会报错,只是不响应消息而已,应该给野指针及时赋予零值使其变成空指针,避免内存报错。

引用计数(RC)与垃圾回收(GC)

Objective-C语言本身是支持垃圾回收机制的,仅限于 MAC 桌面系统,iOS中不支持

垃圾回收 是宏观的,对整体进行内存管理。将所有对象看作是一个集合,然后在 GC 循环中定时检测活动对象和非活动对象,及时将用不到的非活动对象释放掉以避免内存泄漏,也就是说用不到的垃圾对象是交给GC管理的。

引用计数 是局部性的,开发者需要管理每个对象的引用计数,单个对象引用计数为0后马上释放掉。ARC是一种改进,由编译器帮助开发者自动管理引用计数(自动在合适的时机发送 release 和 retain 消息)。 自动释放池 可以看作是一个局部的垃圾回收,将部分垃圾对象集中释放,相对于单个释放会有一定延迟。

数组越界为什么会崩溃?

数组是一段连续的空间可以通过下标索引访问,当越界时可能访问到系统保护的区域或者正在保存其他内容的区域。

重复释放为什么会崩溃?

因为一个已经被释放的对象就成了僵尸对象,指向它的指针就是野指针,如果给僵尸对象发送消息就会崩溃因为它已经被释放了。

App内存过大崩溃,怎么发现,怎么解决?

iOS 设备没有内存交换机制,当内存占用过大时会进行释放,先后台再前台,先占用内存大的再到占用小的,而如果要释放应用的内存就要停止应用的运行也就是跟崩溃一样。

解决内存占用过多的方式:

  1. 谨慎使用/申请大内存
  2. 及时释放掉不需要的资源
  3. 谨慎单例中引用占用内存大的资源
  4. 使用缓存策略,要有缓存淘汰机制
  5. 图片等资源优化,使用压缩、缓存等技术
  6. 延迟加载,占用内存大的资源用到时才加载
  7. 使用 Instruments 等工具检查内存
  8. 在配置中申请增加获取更大内存的权限

对引用计数的理解,ARC MRC

ARC 会自动计算对象的引用计数并在合适的时机给对象发送 copy/release/retain/autorelease 消息。

ARC 会对引用计数相关方法进行优化来尽量减少方法的调用。

assign 可以用于 OC 对象吗?为什么?

不可以,如果用 assign 修饰了 OC 对象,那么这个对象在创建完成后会被立即释放,因为没有一个强引用那么它的引用计数就是 0。

assign 修饰的变量 setter 方法不会进行释放旧值和引用新值的操作,不符合引用计数规则,会有问题。

Autorelease 对象什么时候释放?

在没有手动加 Autorelease Pool 的情况下,Autorelease 对象是在当前 runloop 迭代结束时释放的,而它能够释放的原因是系统在每个 runloop 迭代中都加入自动释放池 push 和 pop。

SideTable 维护了一个 spinlock_t 的锁和 weak_table_t 主要作用是在操作 weak_table_t 的时候加锁。

weak_table_t 是一个 全局弱引用表 ,主要保存了 weak_entry_t 的数组以及哈希过程中用到的数据等。 通过 size_t begin = hash_pointer(referent) & weak_table->mask; 找到对象的 weak_table_t。

weak_entry_t 保存了被弱引用对象和弱引用对象地址数组。

设置弱引用时会根据对象地址得到的哈希值找到 weak_table_t 的起始地址,之后遍历 weak_table_t 中的 entry 数组,得到要添加到的位置,之后把新的弱引用放到这个位置。

当弱引用对象销毁时,同样根据对象地址找到 weak_table_t,之后从 weak_entry_t 中的弱引用数组中找到要移除的弱引用对象将其置为 nil,当 weak_entry_t 中的弱引用数组被清空时把这个数组释放并将 weak_table_t 的 weak_entries 数组中对应的 weak_entry_t 内存清除。

常见的内存泄漏有哪些?怎么避免?

  1. 解决 NSTimer 循环引用问题。
    1. 自己封装一个类在这个类里进行计时。这个类可以继承自 NSObject,但是最好是继承自 NSProxy,在这个类里面需要进行消息转发。
    2. 使用 iOS10 之后可用的不带 target 参数的启动计时器方法
    3. 给 NSTimer 添加分类,分类中用带有 block 的方法将 target 设置成 NSTimer 本身
  2. 代理 delegate 用 strong 修饰(应该用 weak)
  3. block 捕获外部指针时需要使用 __weak

TaggerPointer 简述

Tagged Pointer 是一种特殊的内存优化技术,用于存储小的对象(如NSNumber、NSDate等)的值,而不需要分配堆上的内存。它通过在指针的低位存储一些标记位(Tag)来表示对象的内容,从而避免了常规指针所需的内存分配和管理开销。

Tagged Pointer 管理的对象因为不在堆上,所以不需要考虑引用计数的问题。

自动释放池 auto release pool 简述

auto release pool 的实现是使用了以栈为节点的双向链表。每一个栈中除了包含结构体本身的属性所需空间外都用来存储 autorelease 对象。

每创建一个新的自动释放池都会申请一个栈并把一个哨兵(POOL_BOUNDARY)对象压栈,之后挨个把 autorelease 对象压栈,当一个栈满后申请一个新的栈,并设置链表指针。

嵌套自动释放池就直接在当前栈顶压入哨兵对象,然后把新池中的 autorelease 对象继续压栈。

每结束一个自动释放池就从栈顶开始释放对象直到哨兵对象。

自动释放池中的对象释放时机是在当前自动释放池结束的时候,对于整个应用的自动释放池是在 runloop 进入休眠时。应用启动后会向 runloop 注册启动和休眠以及退出的通知来监听对应时机以便创建和释放自动释放池。