本文是<<iOS開(kāi)發(fā)高手課>> 第十四篇學(xué)習(xí)筆記.
OOM 的全稱是 Out-Of-Memory,是由于 iOS 的 Jetsam 機(jī)制造成的一種“另類” Crash,它不同于常規(guī)的 Crash,通過(guò) Signal 捕獲等 Crash 監(jiān)控方案無(wú)法捕獲到 OOM 事件。
JetSam 機(jī)制,指的是操作系統(tǒng)為了控制內(nèi)存資源過(guò)度使用而采用的一種資源管控機(jī)制。
2種情況觸發(fā) OOM:
- 系統(tǒng)由于整體內(nèi)存使用過(guò)高,會(huì)基于優(yōu)先級(jí)策略殺死優(yōu)先級(jí)較低的 App;
- 當(dāng)前 App 達(dá)到了 "highg water mark" ,系統(tǒng)也會(huì)強(qiáng)殺當(dāng)前 App(超過(guò)系統(tǒng)對(duì)當(dāng)前單個(gè) App 的內(nèi)存限制值)
通過(guò) JetsamEvent 日志計(jì)算內(nèi)存限制值
想要了解不同機(jī)器在不同系統(tǒng)版本的情況下,對(duì) App 的內(nèi)存限制是怎樣的,有一種方法就是查看手機(jī)中以 JetsamEvent 開(kāi)頭的系統(tǒng)日志(我們可以從設(shè)置 -> 隱私 -> 分析中看到這些日志)。
在這些系統(tǒng)日志中,查找崩潰原因時(shí)我們需要關(guān)注 per-process-limit 部分的 rpages。rpages 表示的是 ,App 占用的內(nèi)存頁(yè)數(shù)量;per-process-limit 表示的是,App 占用的內(nèi)存超過(guò)了系統(tǒng)對(duì)單個(gè) App 的內(nèi)存限制。
所有 App 徹底退出,只跑了一個(gè)為了測(cè)試內(nèi)存臨界值的 Demo App。循環(huán)申請(qǐng)內(nèi)存,ViewController 代碼如下
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableArray *array = [NSMutableArray array];
for (NSInteger index = 0; index < 10000000; index++) {
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
UIImage *image = [UIImage imageNamed:@"AppIcon"];
imageView.image = image;
[array addObject:imageView];
}
}
崩潰后這部分日志的結(jié)構(gòu)如下:
{
"uuid" : "fa38f53d-55ba-37e0-8392-800ea0a88018",
// states:當(dāng)前應(yīng)用的運(yùn)行狀態(tài),對(duì)于這個(gè)應(yīng)用而言是正在前臺(tái)運(yùn)行的狀態(tài),這類崩潰我們稱之為FOOM(Foreground Out Of Memory);與此相對(duì)應(yīng)的也有應(yīng)用程序在后臺(tái)發(fā)生的 OOM 崩潰,這類崩潰我們稱之為BOOM(Background Out Of Memory)。
"states" : [
"frontmost",// 前臺(tái)
],
"lifetimeMax" : 1488,
"killDelta" : 20204,
"age" : 229126847641,
"purgeable" : 0,
"fds" : 50,
"genCount" : 0,
"coalition" : 386,
// rpages:是resident pages的縮寫(xiě),表明進(jìn)程當(dāng)前占用的內(nèi)存頁(yè)數(shù)量,
"rpages" : 89600,
"priority" : 0,
// reason:表明進(jìn)程被終止的的原因,Heimdallr-Example這個(gè)應(yīng)用被終止的原因是超過(guò)了操作系統(tǒng)允許的單個(gè)進(jìn)程物理內(nèi)存占用的上限。
"reason" : "per-process-limit",
"pid" : 4493,
"idleDelta" : 35156829,
"name" : "fp",
"cpuTime" : 8.6578689999999998
},
現(xiàn)在,我們已經(jīng)知道了內(nèi)存頁(yè)數(shù)量 rpages 為 89600,只要再知道內(nèi)存頁(yè)大小的值,就可以計(jì)算出系統(tǒng)對(duì)單個(gè) App 限制的內(nèi)存是多少了。
"memoryStatus" : {
"compressorSize" : 29982,
"compressions" : 10864669,
"decompressions" : 7348104,
"zoneMapCap" : 1109458944,
"largestZone" : "APFS_4K_OBJS",
"largestZoneSize" : 29573120,
// pageSize:指的是當(dāng)前設(shè)備物理內(nèi)存頁(yè)的大小,當(dāng)前設(shè)備是iPhoneXs Max,大小是 16KB,蘋(píng)果 A7 芯片之前的設(shè)備物理內(nèi)存頁(yè)大小則是 4KB。
"pageSize" : 16384,
"uncompressed" : 87247,
"zoneMapSize" : 147537920,
"memoryPages" : {
"active" : 58130,
"throttled" : 0,
"fileBacked" : 38218,
"wired" : 30456,
"anonymous" : 77619,
"purgeable" : 8645,
"inactive" : 53608,
"free" : 8058,
"speculative" : 4099
}
},
內(nèi)存頁(yè)大小的值,我們也可以在 JetsamEvent 開(kāi)頭的系統(tǒng)日志里找到,也就是 pageSize 的值, 16384。
我們就可以計(jì)算出當(dāng)前 App 的內(nèi)存限制值:pageSize * rpages / 1024 /1024 =16384 * 89600 / 1024 / 1024 得到的值是 1400 MB,即 1.4G。
并不是所有的 JetsamEvent 中都可以拿到準(zhǔn)確的閾值,有的存在偏差。。。
比如有些JetsamEvent日志里, rpages 非常小,可能是在后臺(tái),內(nèi)存吃緊被殺死的.
不同手機(jī) OOM 臨界值不同
這些 JetsamEvent 日志,都是系統(tǒng)在殺掉 App 后留在手機(jī)里的。在查看這些日志時(shí),我們就會(huì)發(fā)現(xiàn),很多日志都是 iOS 系統(tǒng)內(nèi)核強(qiáng)殺掉那些優(yōu)先級(jí)不高,并且占用的內(nèi)存超過(guò)限制的 App 后留下的。
這些日志屬于系統(tǒng)級(jí)的,會(huì)存在系統(tǒng)目錄下。App 上線后開(kāi)發(fā)者是沒(méi)有權(quán)限獲取到系統(tǒng)目錄內(nèi)容的,也就是說(shuō),被強(qiáng)殺掉的 App 是無(wú)法獲取到系統(tǒng)級(jí)日志的,只能線下設(shè)備通過(guò)連接 Xcode 獲取到這部分日志。獲取到 Jetsam 后,就能夠算出系統(tǒng)對(duì) App 設(shè)置的內(nèi)存限制值。
翻閱XNU源碼的時(shí)候我們可以看到在Jetsam機(jī)制終止進(jìn)程的時(shí)候最終是通過(guò)發(fā)送SIGKILL異常信號(hào)來(lái)完成的。
/*
* The jetsam no frills kill call
* Return: 0 on success
* error code on failure (EINVAL...)
*/
static int
jetsam_do_kill(proc_t p, int jetsam_flags, os_reason_t jetsam_reason)
{
int error = 0;
error = exit_with_reason(p, W_EXITCODE(0, SIGKILL), (int *)NULL, FALSE, FALSE, jetsam_flags, jetsam_reason);
return error;
}
#define SIGKILL 9 /* kill (cannot be caught or ignored) */
iOS 系統(tǒng)是怎么發(fā)現(xiàn) Jetsam
iOS 系統(tǒng)會(huì)開(kāi)啟優(yōu)先級(jí)最高的線程 vm_pressure_monitor 來(lái)監(jiān)控系統(tǒng)的內(nèi)存壓力情況,并通過(guò)一個(gè)堆棧來(lái)維護(hù)所有 App 的進(jìn)程。另外,iOS 系統(tǒng)還會(huì)維護(hù)一個(gè)內(nèi)存快照表,用于保存每個(gè)進(jìn)程內(nèi)存頁(yè)的消耗情況。
當(dāng)監(jiān)控系統(tǒng)內(nèi)存的線程發(fā)現(xiàn)某 App 內(nèi)存有壓力了,就發(fā)出通知,內(nèi)存有壓力的 App 就會(huì)去執(zhí)行對(duì)應(yīng)的代理,也就是你所熟悉的 didReceiveMemoryWarning 代理。通過(guò)這個(gè)代理,你可以獲得最后一個(gè)編寫(xiě)邏輯代碼釋放內(nèi)存的機(jī)會(huì)。這段代碼的執(zhí)行,就有可能會(huì)避免你的 App 被系統(tǒng)強(qiáng)殺。
系統(tǒng)在強(qiáng)殺 App 前,會(huì)先做優(yōu)先級(jí)判斷 , iOS 系統(tǒng)內(nèi)核里有一個(gè)數(shù)組,專門(mén)用于維護(hù)線程的優(yōu)先級(jí)。這個(gè)優(yōu)先級(jí)規(guī)定就是:內(nèi)核用線程的優(yōu)先級(jí)是最高的,操作系統(tǒng)的優(yōu)先級(jí)其次,App 的優(yōu)先級(jí)排在最后。并且,前臺(tái) App 程序的優(yōu)先級(jí)是高于后臺(tái)運(yùn)行 App 的;線程使用優(yōu)先級(jí)時(shí),CPU 占用多的線程的優(yōu)先級(jí)會(huì)被降低。
iOS 系統(tǒng)在因?yàn)閮?nèi)存占用原因強(qiáng)殺掉 App 前,至少有 6 秒鐘的時(shí)間可以用來(lái)做優(yōu)先級(jí)判斷。同時(shí),JetSamEvent 日志也是在這 6 秒內(nèi)生成的。
通過(guò) XNU 獲取內(nèi)存限制值
除了 JetSamEvent 日志外,我們還可以通過(guò) XNU 來(lái)獲取內(nèi)存的限制值。
在 XNU 中,有專門(mén)用于獲取內(nèi)存上限值的函數(shù)和宏。我們可以通過(guò) memorystatus_priority_entry
這個(gè)結(jié)構(gòu)體,得到進(jìn)程的優(yōu)先級(jí)和內(nèi)存限制值。
// 獲取進(jìn)程的 pid、優(yōu)先級(jí)、狀態(tài)、內(nèi)存閾值等信息,priority 表示的是進(jìn)程的優(yōu)先級(jí),limit 就是我們想要的進(jìn)程內(nèi)存限制值。
typedef struct memorystatus_priority_entry {
pid_t pid;
int32_t priority;
uint64_t user_data;
int32_t limit;
uint32_t state;
} memorystatus_priority_entry_t;
// 基于下面這些宏可以達(dá)到查詢內(nèi)存閾值等信息,也可以修改內(nèi)存閾值等
/* Commands */
#define MEMORYSTATUS_CMD_GET_PRIORITY_LIST 1
#define MEMORYSTATUS_CMD_SET_PRIORITY_PROPERTIES 2
#define MEMORYSTATUS_CMD_GET_JETSAM_SNAPSHOT 3
#define MEMORYSTATUS_CMD_GET_PRESSURE_STATUS 4
#define MEMORYSTATUS_CMD_SET_JETSAM_HIGH_WATER_MARK 5 /* Set active memory limit = inactive memory limit, both non-fatal */
#define MEMORYSTATUS_CMD_SET_JETSAM_TASK_LIMIT 6 /* Set active memory limit = inactive memory limit, both fatal */
#define MEMORYSTATUS_CMD_SET_MEMLIMIT_PROPERTIES 7 /* Set memory limits plus attributes independently */
#define MEMORYSTATUS_CMD_GET_MEMLIMIT_PROPERTIES 8 /* Get memory limits plus attributes */
#define MEMORYSTATUS_CMD_PRIVILEGED_LISTENER_ENABLE 9 /* Set the task's status as a privileged listener w.r.t memory notifications */
#define MEMORYSTATUS_CMD_PRIVILEGED_LISTENER_DISABLE 10 /* Reset the task's status as a privileged listener w.r.t memory notifications */
/* Commands that act on a group of processes */
#define MEMORYSTATUS_CMD_GRP_SET_PROPERTIES 100
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "kern_memorystatus.h"
#define NUM_ENTRIES 1024
char *state_to_text(int State)
{
// Convert kMemoryStatus constants to a textual representation
static char returned[80];
sprintf (returned, "0x%02x ",State);
if (State & kMemorystatusSuspended) strcat(returned,"Suspended,");
if (State & kMemorystatusFrozen) strcat(returned,"Frozen,");
if (State & kMemorystatusWasThawed) strcat(returned,"WasThawed,");
if (State & kMemorystatusTracked) strcat(returned,"Tracked,");
if (State & kMemorystatusSupportsIdleExit) strcat(returned,"IdleExit,");
if (State & kMemorystatusDirty) strcat(returned,"Dirty,");
if (returned[strlen(returned) -1] == ',')
returned[strlen(returned) -1] = '\0';
return (returned);
}
int main (int argc, char **argv)
{
struct memorystatus_priority_entry memstatus[NUM_ENTRIES];
size_t count = sizeof(struct memorystatus_priority_entry) * NUM_ENTRIES;
// call memorystatus_control
int rc = memorystatus_control (MEMORYSTATUS_CMD_GET_PRIORITY_LIST, // 1 - only supported command on OS X
0, // pid
0, // flags
memstatus, // buffer
count); // buffersize
if (rc < 0) { perror ("memorystatus_control"); exit(rc);}
int entry = 0;
for (; rc > 0; rc -= sizeof(struct memorystatus_priority_entry))
{
printf ("PID: %5d\tPriority:%2d\tUser Data: %llx\tLimit:%2d\tState:%s\n",
memstatus[entry].pid,
memstatus[entry].priority,
memstatus[entry].user_data,
memstatus[entry].limit,
state_to_text(memstatus[entry].state));
entry++;
}
}
通過(guò) XNU 的宏獲取內(nèi)存限制,需要有 root 權(quán)限,而 App 內(nèi)的權(quán)限是不夠的,所以正常情況下,作為 App 開(kāi)發(fā)者你是看不到這個(gè)信息的。
通過(guò)內(nèi)存警告獲取內(nèi)存限制值
還可以利用 didReceiveMemoryWarning 這個(gè)內(nèi)存壓力代理事件來(lái)動(dòng)態(tài)地獲取內(nèi)存限制值。
iOS 系統(tǒng)在強(qiáng)殺掉 App 之前還有 6 秒鐘的時(shí)間,足夠你去獲取記錄內(nèi)存信息了。那么,如何獲取當(dāng)前內(nèi)存使用情況呢?
iOS 系統(tǒng)提供了一個(gè)函數(shù) task_info, 可以幫助我們獲取到當(dāng)前任務(wù)的信息。關(guān)鍵代碼如下:
#import <sys/sysctl.h>
#import <mach/mach.h>
struct mach_task_basic_info info;
mach_msg_type_number_t size = sizeof(info);
kern_return_t kl = task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &size);
代碼中,task_info_t 結(jié)構(gòu)里包含了一個(gè) resident_size 字段,用于表示使用了多少內(nèi)存。這樣,我們就可以獲取到發(fā)生內(nèi)存警告時(shí),當(dāng)前 App 占用了多少內(nèi)存。代碼如下:
struct mach_task_basic_info info;
mach_msg_type_number_t size = sizeof(info);
kern_return_t kl = task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &size);
if (kl != KERN_SUCCESS)
{
return NSNotFound;
}
float used_mem = info.resident_size;
return used_mem;
NSLog(@"使用了 %f MB 內(nèi)存", used_mem / 1024.0f / 1024.0f);
經(jīng)過(guò)測(cè)試,上面代碼獲取的內(nèi)存和Xcode上顯示的不一樣.
內(nèi)存信息存在 task_info.h (完整路徑 usr/include/mach/task.info.h)文件的 task_vm_info 結(jié)構(gòu)體中,其中 phys_footprint 就是物理內(nèi)存的使用,而不是駐留內(nèi)存 resident_size。結(jié)構(gòu)體里和內(nèi)存相關(guān)的代碼如下:
struct task_vm_info {
mach_vm_size_t virtual_size; // 虛擬內(nèi)存大小
integer_t region_count; // 內(nèi)存區(qū)域的數(shù)量
integer_t page_size;
mach_vm_size_t resident_size; // 駐留內(nèi)存大小
mach_vm_size_t resident_size_peak; // 駐留內(nèi)存峰值
...
/* added for rev1 */
mach_vm_size_t phys_footprint; // 物理內(nèi)存
...
可以使用下面的函數(shù)
int64_t memoryUsageInByte = 0;
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) {
memoryUsageInByte = (int64_t) vmInfo.phys_footprint;
NSLog(@"使用了 %f MB 內(nèi)存", memoryUsageInByte/1024.0f/1024.0f);
} else {
NSLog(@"Error with task_info(): %s", mach_error_string(kernelReturn));
}
適用于 iOS13 系統(tǒng)的獲取方式
if (@available(iOS 13.0, *)) {
return os_proc_available_memory() / 1024.0 / 1024.0;
}
我們可以通過(guò) os_proc_available_memory 獲取到當(dāng)前可以用內(nèi)存,通過(guò) phys_footprint 獲取到當(dāng)前 App 占用內(nèi)存,2者的和也就是當(dāng)前設(shè)備的內(nèi)存上限,超過(guò)即觸發(fā) Jetsam 機(jī)制。
定位內(nèi)存問(wèn)題信息收集
現(xiàn)在,我們已經(jīng)可以通過(guò)三種方法來(lái)獲取內(nèi)存上限值了,而且通過(guò)內(nèi)存警告的方式還能夠動(dòng)態(tài)地獲取到這個(gè)值。有了這個(gè)內(nèi)存上限值以后,你就可以進(jìn)行內(nèi)存問(wèn)題的信息收集工作了。
要想精確地定位問(wèn)題,我們就需要 dump 出完整的內(nèi)存信息,包括所有對(duì)象及其內(nèi)存占用值,在內(nèi)存接近上限值的時(shí)候,收集并記錄下所需信息,并在合適的時(shí)機(jī)上報(bào)到服務(wù)器里,方便分析問(wèn)題。
獲取到了每個(gè)對(duì)象的內(nèi)存占用量還不夠,你還需要知道是誰(shuí)分配的內(nèi)存,這樣才可以精確定位到問(wèn)題的關(guān)鍵所在。一個(gè)對(duì)象可能會(huì)在不同的函數(shù)里被分配了內(nèi)存并被創(chuàng)建了出來(lái),當(dāng)這個(gè)對(duì)象內(nèi)存占用過(guò)大時(shí),如果不知道是在哪個(gè)函數(shù)里創(chuàng)建的話,問(wèn)題依然很難精確定位出來(lái)。
內(nèi)存分配函數(shù)malloc
和 calloc
等默認(rèn)使用的是nano_zone
。nano_zone
是 256B 以下小內(nèi)存的分配,大于 256B 的時(shí)候會(huì)使用 scalable_zone
來(lái)分配。
如果主要是針對(duì)大內(nèi)存的分配監(jiān)控,可以只針對(duì) scalable_zone 進(jìn)行分析,同時(shí)也可以過(guò)濾掉很多小內(nèi)存分配監(jiān)控。比如,malloc 函數(shù)用的是 malloc_zone_malloc,calloc 用的是 malloc_zone_calloc。
malloc_zone_malloc 函數(shù)的實(shí)現(xiàn)
void *
malloc_zone_malloc(malloc_zone_t *zone, size_t size)
{
return _malloc_zone_malloc(zone, size, MZ_NONE);
}
static void *
_malloc_zone_malloc(malloc_zone_t *zone, size_t size, malloc_zone_options_t mzo)
{
MALLOC_TRACE(TRACE_malloc | DBG_FUNC_START, (uintptr_t)zone, size, 0, 0);
void *ptr = NULL;
if (malloc_check_start) {
internal_check();
}
if (size > MALLOC_ABSOLUTE_MAX_SIZE) {
goto out;
}
ptr = zone->malloc(zone, size); // if lite zone is passed in then we still call the lite methods
// 在 zone 分配完內(nèi)存后就開(kāi)始使用 malloc_logger 進(jìn)行進(jìn)行記錄
if (os_unlikely(malloc_logger)) {
malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE, (uintptr_t)zone, (uintptr_t)size, 0, (uintptr_t)ptr, 0);
}
MALLOC_TRACE(TRACE_malloc | DBG_FUNC_END, (uintptr_t)zone, size, (uintptr_t)ptr, 0);
out:
if (os_unlikely(ptr == NULL)) {
malloc_set_errno_fast(mzo, ENOMEM);
}
return ptr;
}
其他使用 scalable_zone 分配內(nèi)存的函數(shù)的方法也類似,所有大內(nèi)存的分配,不管外部函數(shù)是怎么包裝的,最終都會(huì)調(diào)用 malloc_logger 函數(shù)。
可以 去 Hook 這個(gè)函數(shù),加上自己的統(tǒng)計(jì)記錄就能夠通盤(pán)掌握內(nèi)存的分配情況。出現(xiàn)問(wèn)題時(shí),將內(nèi)存分配記錄的日志撈上來(lái),你就能夠跟蹤到導(dǎo)致內(nèi)存不合理增大的原因了。
malloc_logger hook代碼
typedef void(malloc_logger_t)(
uint32_t type,
uintptr_t arg1,
uintptr_t arg2,
uintptr_t arg3,
uintptr_t result,
uint32_t num_hot_frames_to_skip);
extern malloc_logger_t *malloc_logger;
void malloc_logger_impl(uint32_t type,uintptr_t arg1,uintptr_t arg2,uintptr_t arg3,uintptr_t result,uint32_t num_hot_frames_to_skip){
printf("%d-%lu-%lu-%lu-%lu-%d\n",type,arg1,arg2,arg3,result,num_hot_frames_to_skip);
}
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
appDelegateClassName = NSStringFromClass([AppDelegate class]);
malloc_logger = malloc_logger_impl;
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
收集上報(bào)
分配堆棧可以用 backtrace 函數(shù)捕獲,但捕獲到的地址是虛擬內(nèi)存地址,不能從符號(hào)表 dsym 解析符號(hào)。所以還要記錄每個(gè) image 加載時(shí)的偏移 slide,符號(hào)表地址 = 堆棧地址 - slide。
降低OOM
基于內(nèi)存快照
抖音有一篇文章講述了基于內(nèi)存快照,降低OOM的文章:https://juejin.cn/post/6885144933997494280
簡(jiǎn)單總結(jié):
線上 Memory Graph 采集內(nèi)存快照主要是為了獲取當(dāng)前運(yùn)行狀態(tài)下所有內(nèi)存對(duì)象以及對(duì)象之間的引用關(guān)系,用于后續(xù)的問(wèn)題分析。主要需要獲取的信息如下:
- 所有內(nèi)存的節(jié)點(diǎn),以及其符號(hào)信息(如OC/Swift/C++ 實(shí)例類名,或者是某種有特殊用途的 VM 節(jié)點(diǎn)的 tag 等)。
- 節(jié)點(diǎn)之間的引用關(guān)系,以及符號(hào)信息(偏移,或者實(shí)例變量名),OC/Swift成員變量還需要記錄引用類型。
由于采集的過(guò)程發(fā)生在程序正常運(yùn)行的過(guò)程中,為了保證不會(huì)因?yàn)椴杉瘍?nèi)存快照導(dǎo)致程序運(yùn)行異常,整個(gè)采集過(guò)程需要在一個(gè)相對(duì)靜止的運(yùn)行環(huán)境下完成。因此,整個(gè)快照采集的過(guò)程大致分為以下幾個(gè)步驟:
- 掛起所有非采集線程。
- 獲取所有的內(nèi)存節(jié)點(diǎn),內(nèi)存對(duì)象引用關(guān)系以及相應(yīng)的輔助信息。
- 寫(xiě)入文件。
- 恢復(fù)線程狀態(tài)。
具體的過(guò)程
內(nèi)存節(jié)點(diǎn)的獲取
程序的內(nèi)存都是由虛擬內(nèi)存組成的,每一塊單獨(dú)的虛擬內(nèi)存被稱之為VM Region,通過(guò) mach 內(nèi)核的vm_region_recurse/vm_region_recurse64
函數(shù)我們可以遍歷進(jìn)程內(nèi)所有VM Region,并通過(guò)vm_region_submap_info_64結(jié)構(gòu)體獲取以下信息:
- 虛擬地址空間中的地址和大小。
- Dirty 和 Swapped 內(nèi)存頁(yè)數(shù),表示該VM Region的真實(shí)物理內(nèi)存使用。
- 是否可交換,Text 段、共享 mmap 等只讀或隨時(shí)可以被交換出去的內(nèi)存,無(wú)需關(guān)注。
- user_tag,用戶標(biāo)簽,用于提供該VM Region的用途的更準(zhǔn)確信息。
kern_return_t krc = KERN_SUCCESS;
vm_address_t address = 0;
vm_size_t size = 0;
uint32_t depth = 1;
pid_t pid = getpid();
char buf[PATH_MAX];
while (1) {
struct vm_region_submap_info_64 info;
mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64;
krc = vm_region_recurse_64(mach_task_self(), &address, &size, &depth, (vm_region_info_64_t)&info, &count);
if (krc == KERN_INVALID_ADDRESS){
break;
}
if (info.is_submap){
depth++;
} else {
//do stuff
proc_regionfilename(pid, address, buf, sizeof(buf));
printf("Found VM Region: %08x to %08x (depth=%d) user_tag:%s name:%s\n", (uint32_t)address, (uint32_t)(address+size), depth, [visualMemoryTypeString(info.user_tag) cStringUsingEncoding:NSUTF8StringEncoding], buf);
address += size;
}
}
內(nèi)存節(jié)點(diǎn)大致分為這幾類:
- App的二進(jìn)制文件在內(nèi)存的映射(如OnlineMemoryGraphDemo)
- 動(dòng)態(tài)庫(kù)在內(nèi)存中的映射(如libBacktraceRecording.dylib,libdispatch.dylib等)
- 系統(tǒng)或自定義字體等資源(SFUI.ttf, PingFang.ttc)
- 棧區(qū)(STACK name:/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64)
- Malloc Zone,Malloc Zone分為Nano和Scalable,Nano分配16B~256B(16B的整數(shù)倍)的內(nèi)存,Scalable分配256B以上的大內(nèi)存
大多數(shù) VM Region 作為一個(gè)單獨(dú)的內(nèi)存節(jié)點(diǎn),僅記錄起始地址和 Dirty、Swapped 內(nèi)存作為大小,以及與其他節(jié)點(diǎn)之間的引用關(guān)系;而 libmalloc 維護(hù)的堆內(nèi)存所在的 VM Region 則由于往往包含大多數(shù)業(yè)務(wù)邏輯中的 Objective-C 對(duì)象、C/C++對(duì)象、buffer 等,可以獲取更詳細(xì)的引用信息
在 iOS 系統(tǒng)中為了避免所有的內(nèi)存分配都使用系統(tǒng)調(diào)用產(chǎn)生性能問(wèn)題,相關(guān)的庫(kù)負(fù)責(zé)一次申請(qǐng)大塊內(nèi)存,再在其之上進(jìn)行二次分配并進(jìn)行管理,提供給小塊需要?jiǎng)討B(tài)分配的內(nèi)存對(duì)象使用,稱之為堆內(nèi)存。
程序中使用到絕大多數(shù)的動(dòng)態(tài)內(nèi)存都通過(guò)堆進(jìn)行管理,在 iOS 操作系統(tǒng)上,主要的業(yè)務(wù)邏輯分配的內(nèi)存都通過(guò)libmalloc進(jìn)行管理,部分系統(tǒng)庫(kù)為了性能也會(huì)使用自己的單獨(dú)的堆管理,例如WebKit內(nèi)核使用bmalloc,CFNetwork也使用自己獨(dú)立的堆,在這里我們只關(guān)注libmalloc內(nèi)部的內(nèi)存管理狀態(tài),而不關(guān)心其它可能的堆(即這部分特殊內(nèi)存會(huì)以VM Region的粒度存在,不分析其內(nèi)部的節(jié)點(diǎn)引用關(guān)系)。
我們可以通過(guò)malloc_get_all_zones獲取libmalloc內(nèi)部所有的zone,并遍歷每個(gè)zone中管理的內(nèi)存節(jié)點(diǎn),獲取 libmalloc 管理的存活的所有內(nèi)存節(jié)點(diǎn)的指針和大小。
獲取所有Malloc Zone
vm_address_t *zones = NULL;
unsigned int zoneCount = 0;
kern_return_t result = malloc_get_all_zones(TASK_NULL, memory_reader_callback, &zones, &zoneCount);
if (result == KERN_SUCCESS) {
for (unsigned int i = 0; i < zoneCount; i++) {
malloc_zone_t *zone = (malloc_zone_t *)zones[i];
printf("Found zone name:%s\n", zone->zone_name);
}
}
獲取Zone內(nèi)所有分配的節(jié)點(diǎn)
malloc_introspection_t *introspection = zone->introspect;
if (!introspection) {
continue;
}
void (*lock_zone)(malloc_zone_t *zone) = introspection->force_lock;
void (*unlock_zone)(malloc_zone_t *zone) = introspection->force_unlock;
// Callback has to unlock the zone so we freely allocate memory inside the given block
malloc_object_enumeration_block_t callback = ^(__unsafe_unretained id object, __unsafe_unretained Class actualClass) {
unlock_zone(zone);
block(object, actualClass);
lock_zone(zone);
};
BOOL lockZoneValid = mallocPointerIsReadable((void *)lock_zone);
BOOL unlockZoneValid = mallocPointerIsReadable((void *)unlock_zone);
// There is little documentation on when and why
// any of these function pointers might be NULL
// or garbage, so we resort to checking for NULL
// and whether the pointer is readable
if (introspection->enumerator && lockZoneValid && unlockZoneValid) {
lock_zone(zone);
introspection->enumerator(TASK_NULL, (void *)&callback, MALLOC_PTR_IN_USE_RANGE_TYPE, (vm_address_t)zone, memory_reader_callback, &vm_range_recorder_callback);
unlock_zone(zone);
}
符號(hào)化
獲取內(nèi)存節(jié)點(diǎn)之后,我們需要為每個(gè)節(jié)點(diǎn)找到更加詳細(xì)的類型名稱,用于后續(xù)的分析。
- 對(duì)于 VM Region 內(nèi)存節(jié)點(diǎn),我們可以通過(guò) user_tag 賦予它有意義的符號(hào)信息;
- 堆內(nèi)存對(duì)象包含 raw buffer,Objective-C/Swift、C++等對(duì)象。對(duì)于 Objective-C/Swift、C++這部分,我們通過(guò)內(nèi)存中的一些運(yùn)行時(shí)信息,嘗試符號(hào)化獲取更加詳細(xì)的信息。
Objective/Swift 對(duì)象的符號(hào)化相對(duì)比較簡(jiǎn)單,Swift在內(nèi)存布局上兼容了Objective-C,也有isa指針,objc相關(guān)方法可以作用于兩種語(yǔ)言的對(duì)象上。只要保證 isa 指針合法,對(duì)象實(shí)例大小滿足條件即可認(rèn)為正確。
獲取所有OC/SwiftClass類型
CFMutableSetRef registeredClasses;
unsigned int updateRegisteredClasses() {
if (!registeredClasses) {
registeredClasses = CFSetCreateMutable(NULL, 0, NULL);
} else {
CFSetRemoveAllValues(registeredClasses);
}
unsigned int count = 0;
Class *classes = objc_copyClassList(&count);
for (unsigned int i = 0; i < count; i++) {
CFSetAddValue(registeredClasses, (__bridge const void *)(classes[i]));
}
free(classes);
return count;
}
判斷isa是否合法
typedef struct {
Class isa;
} malloc_maybe_object_t;
void vm_range_recorder_callback(task_t task, void *context, unsigned type, vm_range_t *ranges, unsigned rangeCount) {
if (!context) {
return;
}
for (unsigned int i = 0; i < rangeCount; i++) {
vm_range_t range = ranges[i];
malloc_maybe_object_t *tryObject = (malloc_maybe_object_t *)range.address;
Class tryClass = NULL;
#ifdef __arm64__
// See http://www.sealiesoftware.com/blog/archive/2013/09/24/objc_explain_Non-pointer_isa.html
extern uint64_t objc_debug_isa_class_mask WEAK_IMPORT_ATTRIBUTE;
tryClass = (__bridge Class)((void *)((uint64_t)tryObject->isa & objc_debug_isa_class_mask));
#else
tryClass = tryObject->isa;
#endif
// 1\. 判斷是否為OC/SwiftObject
if (CFSetContainsValue(registeredClasses, (__bridge const void *)(tryClass))) {
(*(malloc_object_enumeration_block_t __unsafe_unretained *)context)((__bridge id)tryObject, tryClass);
}
// 2\. 判斷是否是一個(gè)保護(hù)type_info的C++對(duì)象
else if ([CPPObjectUtil cppTypeInfoName:(void *)range.address] != NULL) {
NSLog(@"Find a Cpp Object:%s!", [CPPObjectUtil cppTypeInfoName:(void *)range.address]);
}
}
}
C++對(duì)象根據(jù)是否包含虛表可以分成兩類。對(duì)于不包含虛表的對(duì)象,因?yàn)槿狈\(yùn)行時(shí)數(shù)據(jù),無(wú)法進(jìn)行處理。
對(duì)于對(duì)于包含虛表的對(duì)象, 可以通過(guò) std::type_info 和以下幾個(gè) section 的信息獲取對(duì)應(yīng)的類型信息。
- type_name string - 類名對(duì)應(yīng)的常量字符串,存儲(chǔ)在__TEXT/__RODATA段的__const section中。
- type_info - 存放在__DATA/__DATA_CONST段的__const section中。
- vtable - 存放在__DATA/__DATA_CONST段的__const section中。
如何判斷是不是一個(gè)C++對(duì)象
獲取App二進(jìn)制加載到內(nèi)存中起始地址,_dyld_register_func_for_add_image方法當(dāng)App的二進(jìn)制或者動(dòng)態(tài)庫(kù)等MachO格式的文件映射到內(nèi)存后,啟動(dòng)App時(shí)的回調(diào),我們可以通過(guò)這個(gè)拿到App執(zhí)行二進(jìn)制的起始地址,從而拿到段中的C++類型信息。
獲取App二進(jìn)制起始地址
/*
* The following functions allow you to install callbacks which will be called
* by dyld whenever an image is loaded or unloaded. During a call to _dyld_register_func_for_add_image()
* the callback func is called for every existing image. Later, it is called as each new image
* is loaded and bound (but initializers not yet run). The callback registered with
* _dyld_register_func_for_remove_image() is called after any terminators in an image are run
* and before the image is un-memory-mapped.
*/
extern void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide)) __OSX_AVAILABLE_STARTING(__MAC_10_1, __IPHONE_2_0);
獲取所有C++type_info
typedef std::vector<struct segment_command_64 const *> Segment64Vector;
typedef std::set<uint64_t *> CxxTypeInfoSet;
static Segment64Vector *segments_64 = NULL;
static CxxTypeInfoSet *cxxTypeInfoSet = NULL;
// 記錄Data Segment中__const段的有效最大最小地址,合法的C++ type_info地址不會(huì)超出這里
uint64_t dataConstMinAddress = NULL;
uint64_t dataConstMaxAddress = NULL;
static void dyld_callback(const struct mach_header *mhp, intptr_t vmaddr_slide)
{
// 這里只分析App的二進(jìn)制
if (mhp->filetype != MH_EXECUTE) {
return;
}
segments_64 = new Segment64Vector();
cxxTypeInfoSet = new CxxTypeInfoSet();
size_t header_size = sizeof(struct mach_header_64);
uint64_t *load_comandPtr = (uint64_t *)((unsigned char *)mhp + header_size);
uint64_t address = (uint64_t)((uint64_t *)mhp);
uint32_t ptrSize = sizeof(uint64_t);
for (int i=0; i<mhp->ncmds; i++) {
struct load_command *load_command = (struct load_command *)load_comandPtr;
segments_64->push_back((struct segment_command_64 const *)load_command);
NSString *cmdType = loadCommandMap[@(load_command->cmd)];
NSLog(@"dyld_callback load_command cmd:%@", cmdType);
// 分析 Data Segment中__const段,獲取有效最大最小地址
if (load_command->cmd == LC_SEGMENT_64) {
struct segment_command_64 *segment_64 = (struct segment_command_64 *)load_command;
if (strcmp(segment_64->segname, "__DATA") == 0) {
const struct section_64 *sec = (struct section_64 *)(segment_64 + 1);
for (int j=0; j<segment_64->nsects; j++) {
if (strcmp(sec[j].sectname, "__const") == 0) {
dataConstMinAddress = (((uint64_t)(uint64_t *)mhp) + sec[j].offset);
dataConstMaxAddress = (((uint64_t)(uint64_t *)mhp) + sec[j].offset + sec[j].size);
}
}
}
}
// 分析動(dòng)態(tài)鏈接段的信息,獲取App內(nèi)C++ type_info的地址
else if (load_command->cmd == LC_DYLD_INFO ||
load_command->cmd == LC_DYLD_INFO_ONLY) {
struct dyld_info_command *dyldCommand = (struct dyld_info_command *)load_command;
uint8_t *bytePtr = (uint8_t *)((uint8_t *)mhp + dyldCommand->bind_off); // Dynamic Loader Info Bind部分的起始地址
uint64_t dyldMaxAddress = (((uint64_t)(uint64_t *)mhp) + dyldCommand->bind_off + dyldCommand->bind_size);
uint64_t doBindLocation = *((uint64_t *)bytePtr);
int32_t libOrdinal = 0;
uint32_t type = 0;
int64_t addend = 0;
NSString * symbolName = nil;
uint32_t symbolFlags = 0;
BOOL isDone = NO;
while (((uint64_t)(uint64_t *)bytePtr) < dyldMaxAddress) {
uint8_t byte = read_int8(&bytePtr);
uint8_t opcode = byte & BIND_OPCODE_MASK;
uint8_t immediate = byte & BIND_IMMEDIATE_MASK;
NSLog(@"dyld_callback load_command opcode:%d, immediate:%d", opcode, immediate);
switch (opcode)
{
case BIND_OPCODE_DONE:
// The lazy bindings have one of these at the end of each bind.
isDone = YES;
doBindLocation = (*((uint64_t *)bytePtr) + 1);
break;
case BIND_OPCODE_SET_DYLIB_ORDINAL_IMM:
libOrdinal = immediate;
break;
case BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB:
libOrdinal = (uint32_t)read_uleb128(&bytePtr);
break;
case BIND_OPCODE_SET_DYLIB_SPECIAL_IMM:
{
// Special means negative
if (immediate == 0)
{
libOrdinal = 0;
}
else
{
int8_t signExtended = immediate | BIND_OPCODE_MASK; // This sign extends the value
libOrdinal = signExtended;
}
} break;
case BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM:
symbolFlags = immediate;
symbolName = read_string(&bytePtr);
break;
case BIND_OPCODE_SET_TYPE_IMM:
type = immediate;
break;
case BIND_OPCODE_SET_ADDEND_SLEB:
addend = read_sleb128(&bytePtr);
break;
//
case BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB:
{
uint32_t segmentIndex = immediate;
uint64_t val = read_uleb128(&bytePtr);
if (segmentIndex < segments_64->size())
{
address += (*segments_64)[segmentIndex]->fileoff + val;
}
} break;
case BIND_OPCODE_ADD_ADDR_ULEB:
{
uint64_t val = read_uleb128(&bytePtr);
address += val;
} break;
case BIND_OPCODE_DO_BIND:
{
// 獲取C++ type_info地址
NSLog(@"dyld_callback Bind SymbolName:%@", symbolName);
if ([symbolName hasPrefix:@"__ZTVN10__cxxabi"]) {
std::type_info *type_info = (std::type_info *)address;
NSLog(@"std::type_info name:%s address:%p", type_info->name(), type_info);
cxxTypeInfoSet->insert((uint64_t *)address);
}
doBindLocation = *((uint64_t *)bytePtr);
address += ptrSize;
} break;
case BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB:
{
uint64_t startNextBind = *((uint64_t *)bytePtr);
uint64_t val = read_uleb128(&bytePtr);
doBindLocation = startNextBind;
address += ptrSize + val;
} break;
case BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED:
{
uint32_t scale = immediate;
// 獲取C++ type_info地址
if ([symbolName hasPrefix:@"__ZTVN10__cxxabi"]) {
std::type_info *type_info = (std::type_info *)address;
NSLog(@"std::type_info name:%s address:%p", type_info->name(), type_info);
cxxTypeInfoSet->insert((uint64_t *)address);
}
doBindLocation = *((uint64_t *)bytePtr);
address += ptrSize + scale * ptrSize;
} break;
case BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB:
{
uint64_t startNextBind = *((uint64_t *)bytePtr);
uint64_t count = read_uleb128(&bytePtr);
uint64_t skip = read_uleb128(&bytePtr);
for (uint64_t index = 0; index < count; index++)
{
doBindLocation = startNextBind;
address += ptrSize + skip;
}
} break;
default:
break;
}
}
}
load_comandPtr = (uint64_t *)((unsigned char *)load_comandPtr + load_command->cmdsize);
}
}
判斷一個(gè)地址是否為一個(gè)C++Object(有type_info的)
typedef std::set<uint64_t *> CxxTypeInfoSet;
static CxxTypeInfoSet *cxxTypeInfoSet = NULL;
+ (const char *) cppTypeInfoName:(void *) ptr {
uint64_t *typeInfoPtr = (uint64_t*)(*((uint64_t *)ptr) - 8);
uint64_t typeInfoAddress = (uint64_t)typeInfoPtr;
if (typeInfoAddress >= dataConstMinAddress && typeInfoAddress < dataConstMaxAddress) {
uint64_t *typeInfo = (uint64_t *)(*typeInfoPtr);
if (cxxTypeInfoSet->find(typeInfo) != cxxTypeInfoSet->end()) {
const char *name = ((std::type_info *)typeInfo)->name();
return name;
}
}
return NULL;
}
在 iOS 系統(tǒng)內(nèi),還有一類特殊的對(duì)象,即CoreFoundation。除了我們熟知的CFString、CFDictionary外等,很多很多系統(tǒng)庫(kù)也使用 CF 對(duì)象,比如CGImage、CVObject等。從它們的 isa 指針獲取的Objective-C類型被統(tǒng)一成__NSCFType。由于 CoreFoundation 類型支持實(shí)時(shí)的注冊(cè)、注銷(xiāo)類型,為了細(xì)化這部分的類型,我們通過(guò)逆向拿到 CoreFoundation 維護(hù)的類型 slot 數(shù)組的位置并讀取其數(shù)據(jù),保證能夠安全的獲取準(zhǔn)確的類型。
UIImage *image = [UIImage imageNamed:@""];
CFTypeID typeId = CFGetTypeID(image.CGImage);
CFStringRef className = CFCopyTypeIDDescription(typeId);
NSLog(@"%@",className);
引用關(guān)系的構(gòu)建
整個(gè)內(nèi)存快照的核心在于重新構(gòu)建內(nèi)存節(jié)點(diǎn)之間的引用關(guān)系。在虛擬內(nèi)存中,如果一個(gè)內(nèi)存節(jié)點(diǎn)引用了其它內(nèi)存節(jié)點(diǎn),則對(duì)應(yīng)的內(nèi)存地址中會(huì)存儲(chǔ)指向?qū)Ψ降闹羔樦怠S幸韵路桨福?/p>
- 遍歷一個(gè)內(nèi)存節(jié)點(diǎn)中所有可能存儲(chǔ)了指針的范圍獲取其存儲(chǔ)的值 A。
- 搜索所有獲得的節(jié)點(diǎn),判斷 A 是不是某一個(gè)內(nèi)存節(jié)點(diǎn)中任何一個(gè)字節(jié)的地址,如果是,則認(rèn)為是一個(gè)引用關(guān)系。
- 對(duì)所有內(nèi)存節(jié)點(diǎn)重復(fù)以上操作。
對(duì)于一些特定的內(nèi)存區(qū)域,為了獲取更詳細(xì)的信息用于排查問(wèn)題,對(duì)棧內(nèi)存以及 Objective-C/Swift 的堆內(nèi)存進(jìn)行了一些額外的處理。
其中,棧內(nèi)存也以VM Region的形式存在,棧上保存了臨時(shí)變量和 TLS 等數(shù)據(jù),獲取相應(yīng)的引用信息可以幫助排查諸如 autoreleasepool 造成的內(nèi)存問(wèn)題。由于棧并不會(huì)使用整個(gè)棧內(nèi)存,為了獲取 Stack 的引用關(guān)系,根據(jù)寄存器以及棧內(nèi)存獲取當(dāng)前的棧可用范圍,排除未使用的棧內(nèi)存造成的無(wú)效引用。
而對(duì)于Objective-C/Swift對(duì)象,由于運(yùn)行時(shí)包含額外的信息,我們可以獲得Ivar的強(qiáng)弱引用關(guān)系以及Ivar的名字,帶上這些信息有助于我們分析問(wèn)題。 通過(guò)獲得Ivar的偏移,如果找到的引用關(guān)系的偏移和Ivar的偏移一致,則認(rèn)為這個(gè)引用關(guān)系就是這個(gè)Ivar,可以將Ivar相關(guān)的信息附加上去。
參考鏈接