App开发中的内存安全问题
内存问题显然是APP开发的一大需要解决的难题,无论是运行内存占用、包体积大小还是APP运行时的内存泄漏问题,都是我们在日常开发中需要特别重视的点。要理解和解决这些问题,我们需要对内存有着系统的认识和理解。
内存占用的组成
一个app占用的内存可分为三个部分,即
- Memory written by an app
- Dirty 脏内存
- Compressed 压缩内存
- clean 干净的内存
dirty 脏内存
dirty指的是已经被app写入过的内存
组成有
- dirty
- 所有的heap allocation(堆内存分配),当在oc中使用
malloc
分配内存时,申请到的内存就是在堆区上 - 音视频解码的buffer缓冲
- framework中的
__DATA
和__DATA_DIRTY
段
- 所有的heap allocation(堆内存分配),当在oc中使用
Compressed memory 压缩内存
Compressed memory 是将 Dirty memory 中最近没有访问过的内存,使用内存压缩器对 Dirty page 进行压缩。这些 page 会在被访问时解压缩。注意 iOS 是没有交换内存(Disk swap)技术的,交换内存是 MacOS 特有的。macOS同时使用swap技术和内存压缩技术。
- Compressed memory技术具有的特点有
- Shrinks memory usage 减少了不活跃内存占用
- Improves power efficiency 改善电源效率,通过压缩减少磁盘IO带来的损耗
- Minimizes CPU usage 压缩/解压十分迅速,能够尽可能减少 CPU 的时间开销
- Is multicore aware 支持多核操作
Clean 干净的内存
Clean Memory 是还没有被写入的内存或可以被 page out 的内存。指的是还没有被加载到内存或者能够被系统清理出内存且在需要时能重新加载的数据。
- 包括
- Memory mapped files (内存映射文件)
- 加载到内存中的磁盘上的图像
- Frameworks 中的
__DATA_CONST
部分 - 应用的二进制可执行文件
合理管理内存的好处
Faster application activations(更快的应用程序激活)
系统的RAM是有限的,在iOS系统中,如果你的app在切换到后台时仍占据了较高的内存,这时你的程序进程可能就会被系统kill掉。所以为了更快地将app从后台切换到前台(热启动),我们应该在app切换到后台时释放掉部分占用内存较大的资源,在app切换回前台时重新加载。
Responsive experience(快速响应的使用体验)
app更小的内存占用可以让系统更快地运行你的程序,从而给出更快的响应速度。更丝滑的交互响应可以让你的app的用户体验更加好。
Complex features(复杂的功能)
一般来说复杂的功能需要使用更多的运行内存,合理地管理app的内存占用可以支撑app中复杂功能的运行,同时使得app不会使用过多的运行内存。
Wider device compatibility(广泛的设备的兼容性)
较老旧的设备上RAM的大小一般较小,更小的内存占用可以使你的app在老旧的设备上运行地更加流畅。
常见的几个内存问题
- Leaks
- Heap size issues
Leaks 内存泄漏
内存泄漏是在app开发中非常常见的内存问题,并且一不小心可能就会写出一段有内存泄漏隐患的代码,尤其是在各种block
中。
- 它可以被细分成两类
- 对象没有任何引用,但没有被释放 (MRC下,在ARC下当对象没有任何引用时系统会帮我们回收内存,但MRC下如果没有release则内存不释放)
- 循环引用,对象无法释放 (当两个对象相互持有对方的强引用,且两个对象都没有其他的外部引用时,就出现了循环引用,导致了内存泄漏)
Heap size issues
堆是进程地址空间的一部分,用来存储动态生成的对象。所以 堆的大小也对内存占用起到了至关重要的影响。
其中主要会容易出现两个问题
- Heap allocation regressions 堆分配回归 ?
- Fragmentation 碎片化
堆分配回归会导致内存的增加,因为内存在堆上比之前增加了更多的对象。治理策略有
- 移除无用内存分配
- 减少过大内存的分配
- 不再使用的内存需要释放
- 需要的时候再分配内存
Fragmentation 碎片化
- 产生的原因:当进程的 dirty page 没有被 100% 占用时,就会产生碎片化。
内存治理的工具
- 主要有两类工具
- 可视化工具
- 命令行工具
可视化工具
可视化工具有
- Xcode集成,如Memory Report
- instrument相关工具
Memory Report
- project运行起来后在debug页面即可看到Memory Report,但它展示的只是内存的整体情况和整体的增加和减小,没有具体详细的内存使用报告。
Product->Analyze
- 基于编译器的静态分析
- 所以静态分析能够检查出一些内存泄露问题,一些动态执行引起的内存泄露需要其他工具来检查。
instrument
- leaks 用于检测程序运行过程中的内存泄露,并记录对象的历史信息。
- Allocations 追踪程序的虚拟内存占用和堆信息,提供对象的类名、大小以及调用栈等信息。
- Zombies 用于检测程序运行过程中的僵尸对象,并记录对象的产生过程,调用堆栈及位置。
- VM Tracker 能够区分程序运行时前文所述的各种内存类型占用情况,Instruments User Guide 中给出了各个参数的具体定义。
命令行工具
VMMP
- vmmap 能够打印出进程信息,所有分配给该进程的 VMRegions 以及 VMRegion 的种类、内存占用信息等内容。利用 –summary 则能够根据不同的 region type 打印出详细的内存占用类型和信息。这里需要注意的是 SWAPPED SIZE 在 iOS 上指的是 Compressed memory size 且其值表示压缩前的占用大小。
Leaks
- leaks 追踪堆中的对象,打印出进程中内存泄露情况、调用堆栈以及循环引用信息。利用 –traceTree 和指定对象的地址,leaks 还能以树形结构打印出对象的相关引用。
heap
- heap 会打印出所有在堆上的对象信息,默认按类数量排序,也可以通过 -sortBySize 按大小排序,对于追踪堆中较大的对象十分有帮助。找到目标对象后,通过 -address 获得所有/指定类的地址,继而可以利用 malloc_history 寻找其调用堆栈信息。
malloc_history
malloc_history App.memgraph --fuStacks [address]
使用上述命令能够获得我们知道地址的对象的调用堆栈信息,它能够得到的比 memory inspector 中 Backtrace 更加详细。但是需要开启 Dignostics 中的 Malloc Stack 选项,才能通过 malloc_history 获得 memgraph 记录的调用堆栈信息。