iOS 性能监控2 Memory信息

Memory

我们可以联想:内存使用情况是否也可以通过类似CPU的方式获取到呢?答案是肯定的。

相关系统原理

内存是有限且系统共享的资源,一个程序占用越多,系统和其他程序所能用的就越少。程序启动前都需要先加载到内存中,并且在程序运行过程中的数据操作也会占用一定的内存资源。减少内存占用也能同时减少其对 CPU 时间维度上的消耗,从而使不仅使 App 以及整个系统也都能表现的更好。

MacOS 和 iOS 都采用了虚拟内存技术来突破物理内存的大小限制,每个进程都有一段由多个大小相同的页(Page)所构成的逻辑地址空间。处理器和内存管理单元(MMU,Memory Management Unit)维护着由逻辑地址到物理地址的 页面映射表(简称 页表),当程序访问逻辑内存地址时,由 MMU 根据页表将逻辑地址转换为真实的物理地址。在早期的苹果设备中,每个页的大小为 4KB;基于 A7 和 A8 处理器的系统为 64 位程序提供了 16KB 的虚拟内存分页和 4KB 的物理内存分页;在 A9 之后,虚拟内存和物理内存的分页大小都达到了 16KB。

虚拟内存分页(Virtual Page,VP)有两种类型:

  • Clean:指能够被系统清理出内存且在需要时能重新加载的数据,包括:
    • 内存映射文件
    • Frameworks 中的 __DATA_CONST 部分
    • 应用的二进制可执行文件
  • Dirty:指不能被系统回收的内存占用,包括:
    • 所有堆上的对象
    • 图片解码缓冲数据
    • Framework 中的 DATA 和 DATA_DIRTY 部分

由于内存容量和读写寿命的限制,iOS 上没有 Disk Swap 机制,取而代之使用 Compressed Memory 技术。 Disk Swap 是指在 macOS 以及一些其他桌面操作系统中,当内存可用资源紧张时,系统将内存中的内容写入磁盘中的backing store(Swapping out),并且在需要访问时从磁盘中再读入 RAM(Swapping in)。与大多数 UNIX 系统不同的是,macOS 没有预先分配磁盘中的一部分作为 backing store,而是利用引导分区所有可用的磁盘空间。

苹果最初只是公开了从 OS X Mavericks 开始使用 Compressed Memory 技术,但 iOS 系统也从 iOS 7 开始悄悄地使用。

Compressed Memory 技术在内存紧张时能够将最近使用过的内存占用压缩至原有大小的一半以下,并且能够在需要时解压复用。它在节省内存的同时提高了系统的响应速度,其特点可以归结为:

  • 减少了不活跃内存占用
  • 改善电源效率,通过压缩减少磁盘 IO 带来的损耗
  • 压缩/解压非常快,能够尽可能减少 CPU 的时间开销
  • 支持多核操作

本质上,Compressed Memory 也是 Dirty Memory。因此,memory footprint = dirty size + compressed size,这也是我们需要并且能够尝试去减少的内存占用。

代码实现

在 /usr/include/mach/task_info.h 中,我们可以看到 mach_task_basic_info 和 task_basic_info 结构体的定义,分别如下所示。事实上,苹果公司已经不建议再使用 task_basic_info 结构体了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define MACH_TASK_BASIC_INFO     20         /* always 64-bit basic info */
struct mach_task_basic_info {
mach_vm_size_t virtual_size; /* virtual memory size (bytes) */
mach_vm_size_t resident_size; /* resident memory size (bytes) */
mach_vm_size_t resident_size_max; /* maximum resident memory size (bytes) */
time_value_t user_time; /* total user run time for
terminated threads */
time_value_t system_time; /* total system run time for
terminated threads */
policy_t policy; /* default policy for new threads */
integer_t suspend_count; /* suspend count for task */
};
/* localized structure - cannot be safely passed between tasks of differing sizes */
/* Don't use this, use MACH_TASK_BASIC_INFO instead */
struct task_basic_info {
integer_t suspend_count; /* suspend count for task */
vm_size_t virtual_size; /* virtual memory size (bytes) */
vm_size_t resident_size; /* resident memory size (bytes) */
time_value_t user_time; /* total user run time for
terminated threads */
time_value_t system_time; /* total system run time for
terminated threads */
policy_t policy; /* default policy for new threads */
};

mach_task_basic_info 结构体存储了 Mach task 的内存使用信息,其中 resident_size 是 App 使用的驻留内存大小,virtual_size 是 App 使用的虚拟内存大小。

如下所示为内存使用情况的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
// 当前 app 内存使用量
+ (NSInteger)useMemoryForApp {
struct mach_task_basic_info info;
mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT;

kern_return_t kr = task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t) &info, &count);
if (kr == KERN_SUCCESS) {
return info.resident_size;
} else {
return -1;
}
}

然而,我用 通过此方法获取到的内存信息与 Instruments 中的 Activity Monitor 采集到的内存信息进行比较,发现前者要多出将近 100MB。经过调研发现,苹果使用了上述的 Compressed Memory,我猜测:resident_size 可能是将 Compressed Memory 解压后所统计到的一个数值。真实的物理内存的值应该是 task_vm_info 结构体中的 pyhs_footprint 成员的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#define TASK_VM_INFO            22
#define TASK_VM_INFO_PURGEABLE 23
struct task_vm_info {
mach_vm_size_t virtual_size; /* virtual memory size (bytes) */
integer_t region_count; /* number of memory regions */
integer_t page_size;
mach_vm_size_t resident_size; /* resident memory size (bytes) */
mach_vm_size_t resident_size_peak; /* peak resident size (bytes) */

mach_vm_size_t device;
mach_vm_size_t device_peak;
mach_vm_size_t internal;
mach_vm_size_t internal_peak;
mach_vm_size_t external;
mach_vm_size_t external_peak;
mach_vm_size_t reusable;
mach_vm_size_t reusable_peak;
mach_vm_size_t purgeable_volatile_pmap;
mach_vm_size_t purgeable_volatile_resident;
mach_vm_size_t purgeable_volatile_virtual;
mach_vm_size_t compressed;
mach_vm_size_t compressed_peak;
mach_vm_size_t compressed_lifetime;

/* added for rev1 */
mach_vm_size_t phys_footprint;

/* added for rev2 */
mach_vm_address_t min_address;
mach_vm_address_t max_address;
};

因此,正确的内存使用情况的代码实现应该如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 当前 app 内存使用量
+ (NSInteger)useMemoryForApp {
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t kernelReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
if (kernelReturn == KERN_SUCCESS) {
int64_t memoryUsageInByte = (int64_t) vmInfo.phys_footprint;
return memoryUsageInByte / 1024 / 1024;
} else {
return -1;
}
}

设备所有物理内存大小

1
[NSProcessInfo processInfo].physicalMemory

设备使用的内存

获取当前设备的 Memory 使用情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int64_t getUsedMemory()
{
size_t length = 0;
int mib[6] = {0};

int pagesize = 0;
mib[0] = CTL_HW;
mib[1] = HW_PAGESIZE;
length = sizeof(pagesize);
if (sysctl(mib, 2, &pagesize, &length, NULL, 0) < 0)
{
return 0;
}

mach_msg_type_number_t count = HOST_VM_INFO_COUNT;

vm_statistics_data_t vmstat;

if (host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&vmstat, &count) != KERN_SUCCESS)
{
return 0;
}

int wireMem = vmstat.wire_count * pagesize;
int activeMem = vmstat.active_count * pagesize;
return wireMem + activeMem;
}

设备可用的内存

获取当前设备可用的 Memory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (uint64_t)availableMemory {
vm_statistics64_data_t vmStats;
mach_msg_type_number_t infoCount = HOST_VM_INFO_COUNT;
kern_return_t kernReturn = host_statistics(mach_host_self(),
HOST_VM_INFO,
(host_info_t)&vmStats,
&infoCount);

if (kernReturn != KERN_SUCCESS) {
return NSNotFound;
}

return vm_page_size * (vmStats.free_count + vmStats.inactive_count);
}

读者可能会看到有些代码会使用 vm_statistics_data_t 结构体,但是这个结构体是32位机器的,随着 Apple 逐渐放弃对32位应用的支持,所以建议读者还是使用 vm_statistics64_data_t 64位的结构体。