性能是客户端永恒的主题,我们希望用户在使用APP时,能够获得极致的性能体验,关注内存占用也正是因为这一点。
(温馨提示:本片内容来源自WWDC21:Detect and diagnose memory issues)
想了解更多WWDC2021内容的小伙伴,可以阅读我以下文章,欢迎多多交流和指正
Faster application activations
减少APP的内存占用,可以提高APP在后台存活的机会,从而让APP更快的被激活。
设备的内存空间是有限的,监控APP的内存使用情况,能够防止系统为了回收内存而主动终止APP。
这样即使APP在后台运行,也能够保存当前用户的使用状态,从而避免再次从内存加载导致的耗时。
Responsive experience
更好的响应速度
策略性的将APP的内容加载到内存里,能够避免在用户使用APP时,因系统回收内存增加的等待耗时。
Complex features
让用户体验更丰富的功能
像加载视频、展示动画,这些复杂的功能往往需要占用更多的内存。因此有策略的使用内存,可以避免在用户使用APP的复杂功能时,被系统因内存占用问题而终止。
Wider device compatibility
更好的设备兼容性
让内存空间不太充足的老设备也能更好的使用APP的功能
内存占用结构
Clean Memory
Dirty Memory
Clean Memory被分配,并写入内容后成为”脏内存“,脏内存包括:
- 所有的堆分配(malloc)
- 解码图像缓冲区
- Frameworks
Compressed Memory
压缩内存特指脏页(Dirty Pages)中暂未被访问的部分(Unaccessed pages),会在访问后解压,成为脏内存。
(下文会详细介绍脏页(Dirty Pages)的概念)
注:iOS中没有内存交换(Memory Swap)的概念,你可能在使用Instrument工具时,见到过
Swapped
字段,实际上所指的是Compressed Memory
内存占用 = Dirty Memory + Compressed Memory
内存画像工具
XCTest
通过单元测试和U测试中的XCTest来检测开发环境的性能指标
XCTest可以测量以下性能指标:
- 内存使用情况
- CPU使用
- 磁盘写入情况
- 卡顿率
- 执行时间(完成某一任务所花的全部时间)
- APP启动时间
通过XCTest检测内存使用情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14func testSaveMeal() {
let app = XCUIApplication()
let options = XCTMeasuireOptions()
options.invocationOptions = [.manullyStart]
measure(metrics: [XCTMemoryMetric(application: app)],
options:options) {
app.launch()
startMeasureing()
app.cells.firstMatch.buttons["Save meal"].firstMatch.tap()
let savedButton = app.cells.firstMatch.buttons["Saved"].firstMatch
XCTAssertTure(savedButton.waitForExistence(timeout: 30))
}
}XCTest在Xcode 13中新增的两项收集性能情况的诊断产物
开启:
enablePerformanceTestsDiagnostics YES
1
2
3
4
5xcodebuild test
-project MealPannerApp.xcodeproj
-scheme PerformanceTests
-destination platform=iOS,name="iPhone"
-enablePerformanceTestsDiagnostics YESKtrace files
Ktrace file能够打开,并展示以下分析报告:
- 当卡顿发生时的渲染通道的状态
- 因主线程阻塞,导致的Hang(APP无法响应用户的输入或者行为超过250ms,即记作一个hang)
Memory graphs
Memory graph展示APP进程的地址空间的快照
我们可以生成任务开始(pre_XXX.memgraph)和结束(post_XXX.memgraph)两个时机的内存快照,从而查看这一阶段内存占用的变化。
MetricKit & Xcode Organizer
检测线上环境的内存指标
内存问题
内存泄漏(Leaks)
最常见内存泄漏的原因的是循环引用
通过命令行查看memgraph文件
堆大小问题(Heap size issues)
堆是进程地址空间中储存动态分配的对象的段(section),有策略性的加载和释放内存,可以避免内存峰值过高引发OOM。
- 堆分配回归(Heap allocation regressions)
- 碎片化(Fragmentation)
堆分配回归
- 使用
vmmap -summary
对memgraph文件进行分析
我们主要关心
Dirty Size
和Swapped Size
两列数据
对任务开始(pre_XXX.memgraph)和结束(post_XXX.memgraph)两个时机的内存快照进行对比
可以在下方看到按类聚合的,各类对象占用内存的大小
从图中可以看到,
non-object
类型的数据,占用内存约13M。在Swift中,non-object
通常代表原始分配字节(raw malloced bytes)打印memgraph文件中,类型为
non-object
且占用内存超过500k的对象打印对象的引用树
可以通过
malloc_history -fullStacks
打印对象的分配调用栈当不确定是哪个对象有问题时,可以通过
leaks --referenceTree
,自顶向下打印进程的所有内存中最可疑的对象- 可以通过
--groupByType
参数,按type聚合简化打印结果。
- 可以通过
碎片化
先介绍两个概念:页和”脏页“
- 页(Pages):是最小的独立的内存单元
- 一旦页中写入任何数据,都会使整页变成”脏页“
当内存准备写入新的数据时,系统会优先尝试使用脏页中的空闲内存,而当即将分配的内存过大时,即使空闲内存的总大小足够,但其并不是一段连续的内存空间,仍会开辟一个新的脏页去写入这些数据。
这些无法被使用的空闲内存就是”碎片化内存“
再例如以下这种情况:我们分配的对象总共使用了4个页,但每个页的占用空间只占50%。
当我们释放这些对象后,这些4个脏页的内存空间仍有50%的碎片化内存。
最理想化的状态应该将所有的分配的对象放在两页,如下图:
这样一旦释放这些对象时,仅会留有两个脏页,并且不存在碎片化内存。
我们的目标:
- 让分配的对象尽可能连续且拥有同样的生命周期
- 碎片化率保持在25%以下
- 使用自动释放池
- 特别注意长时间运行的进程
vmman -summary xx.memgraph
查看碎片化内存情况DefaultMallocZone
平时开发者只需要关注这部分的内存空间,因为这是我们进行堆分配默认结束的地方
MallocStackLoggingLiteZone
这个空间是所有堆分配结束的地方
使用Instruments工具中的Allocations track查看内存情况
其中的Destroyed和Persisted分别对应上面所描述的内存释放后的空闲内存(free memory)和仍未释放的内存(remaining objects)
回顾下我们的总体排查流程: