本文是<<iOS開發(fā)高手課>> 第十四篇學習筆記.
OOM 的全稱是 Out-Of-Memory,是由于 iOS 的 Jetsam 機制造成的一種“另類” Crash昆雀,它不同于常規(guī)的 Crash,通過 Signal 捕獲等 Crash 監(jiān)控方案無法捕獲到 OOM 事件呻粹。
JetSam 機制闸翅,指的是操作系統(tǒng)為了控制內(nèi)存資源過度使用而采用的一種資源管控機制舟铜。
2種情況觸發(fā) OOM:
- 系統(tǒng)由于整體內(nèi)存使用過高贵扰,會基于優(yōu)先級策略殺死優(yōu)先級較低的 App仇穗;
- 當前 App 達到了 "highg water mark" ,系統(tǒng)也會強殺當前 App(超過系統(tǒng)對當前單個 App 的內(nèi)存限制值)
通過 JetsamEvent 日志計算內(nèi)存限制值
想要了解不同機器在不同系統(tǒng)版本的情況下戚绕,對 App 的內(nèi)存限制是怎樣的鹃两,有一種方法就是查看手機中以 JetsamEvent 開頭的系統(tǒng)日志(我們可以從設(shè)置 -> 隱私 -> 分析中看到這些日志)栓票。
在這些系統(tǒng)日志中旱捧,查找崩潰原因時我們需要關(guān)注 per-process-limit 部分的 rpages脏里。rpages 表示的是 ,App 占用的內(nèi)存頁數(shù)量瓷马;per-process-limit 表示的是拴还,App 占用的內(nèi)存超過了系統(tǒng)對單個 App 的內(nèi)存限制。
所有 App 徹底退出欧聘,只跑了一個為了測試內(nèi)存臨界值的 Demo App。循環(huán)申請內(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:當前應用的運行狀態(tài)怀骤,對于這個應用而言是正在前臺運行的狀態(tài)费封,這類崩潰我們稱之為FOOM(Foreground Out Of Memory);與此相對應的也有應用程序在后臺發(fā)生的 OOM 崩潰蒋伦,這類崩潰我們稱之為BOOM(Background Out Of Memory)弓摘。
"states" : [
"frontmost",// 前臺
],
"lifetimeMax" : 1488,
"killDelta" : 20204,
"age" : 229126847641,
"purgeable" : 0,
"fds" : 50,
"genCount" : 0,
"coalition" : 386,
// rpages:是resident pages的縮寫,表明進程當前占用的內(nèi)存頁數(shù)量痕届,
"rpages" : 89600,
"priority" : 0,
// reason:表明進程被終止的的原因韧献,Heimdallr-Example這個應用被終止的原因是超過了操作系統(tǒng)允許的單個進程物理內(nèi)存占用的上限。
"reason" : "per-process-limit",
"pid" : 4493,
"idleDelta" : 35156829,
"name" : "fp",
"cpuTime" : 8.6578689999999998
},
現(xiàn)在研叫,我們已經(jīng)知道了內(nèi)存頁數(shù)量 rpages 為 89600锤窑,只要再知道內(nèi)存頁大小的值,就可以計算出系統(tǒng)對單個 App 限制的內(nèi)存是多少了嚷炉。
"memoryStatus" : {
"compressorSize" : 29982,
"compressions" : 10864669,
"decompressions" : 7348104,
"zoneMapCap" : 1109458944,
"largestZone" : "APFS_4K_OBJS",
"largestZoneSize" : 29573120,
// pageSize:指的是當前設(shè)備物理內(nèi)存頁的大小渊啰,當前設(shè)備是iPhoneXs Max,大小是 16KB申屹,蘋果 A7 芯片之前的設(shè)備物理內(nèi)存頁大小則是 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)存頁大小的值,我們也可以在 JetsamEvent 開頭的系統(tǒng)日志里找到哗讥,也就是 pageSize 的值, 16384嚷那。
我們就可以計算出當前 App 的內(nèi)存限制值:pageSize * rpages / 1024 /1024 =16384 * 89600 / 1024 / 1024 得到的值是 1400 MB,即 1.4G杆煞。
并不是所有的 JetsamEvent 中都可以拿到準確的閾值魏宽,有的存在偏差。索绪。湖员。
比如有些JetsamEvent日志里, rpages 非常小,可能是在后臺,內(nèi)存吃緊被殺死的.
不同手機 OOM 臨界值不同
這些 JetsamEvent 日志,都是系統(tǒng)在殺掉 App 后留在手機里的瑞驱。在查看這些日志時娘摔,我們就會發(fā)現(xiàn),很多日志都是 iOS 系統(tǒng)內(nèi)核強殺掉那些優(yōu)先級不高唤反,并且占用的內(nèi)存超過限制的 App 后留下的凳寺。
這些日志屬于系統(tǒng)級的,會存在系統(tǒng)目錄下彤侍。App 上線后開發(fā)者是沒有權(quán)限獲取到系統(tǒng)目錄內(nèi)容的肠缨,也就是說,被強殺掉的 App 是無法獲取到系統(tǒng)級日志的盏阶,只能線下設(shè)備通過連接 Xcode 獲取到這部分日志晒奕。獲取到 Jetsam 后,就能夠算出系統(tǒng)對 App 設(shè)置的內(nèi)存限制值。
翻閱XNU源碼的時候我們可以看到在Jetsam機制終止進程的時候最終是通過發(fā)送SIGKILL異常信號來完成的脑慧。
/*
* 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)會開啟優(yōu)先級最高的線程 vm_pressure_monitor 來監(jiān)控系統(tǒng)的內(nèi)存壓力情況魄眉,并通過一個堆棧來維護所有 App 的進程。另外闷袒,iOS 系統(tǒng)還會維護一個內(nèi)存快照表坑律,用于保存每個進程內(nèi)存頁的消耗情況。
當監(jiān)控系統(tǒng)內(nèi)存的線程發(fā)現(xiàn)某 App 內(nèi)存有壓力了囊骤,就發(fā)出通知晃择,內(nèi)存有壓力的 App 就會去執(zhí)行對應的代理,也就是你所熟悉的 didReceiveMemoryWarning 代理也物。通過這個代理宫屠,你可以獲得最后一個編寫邏輯代碼釋放內(nèi)存的機會。這段代碼的執(zhí)行焦除,就有可能會避免你的 App 被系統(tǒng)強殺激况。
系統(tǒng)在強殺 App 前,會先做優(yōu)先級判斷 , iOS 系統(tǒng)內(nèi)核里有一個數(shù)組膘魄,專門用于維護線程的優(yōu)先級乌逐。這個優(yōu)先級規(guī)定就是:內(nèi)核用線程的優(yōu)先級是最高的,操作系統(tǒng)的優(yōu)先級其次创葡,App 的優(yōu)先級排在最后浙踢。并且,前臺 App 程序的優(yōu)先級是高于后臺運行 App 的灿渴;線程使用優(yōu)先級時洛波,CPU 占用多的線程的優(yōu)先級會被降低。
iOS 系統(tǒng)在因為內(nèi)存占用原因強殺掉 App 前骚露,至少有 6 秒鐘的時間可以用來做優(yōu)先級判斷蹬挤。同時,JetSamEvent 日志也是在這 6 秒內(nèi)生成的棘幸。
通過 XNU 獲取內(nèi)存限制值
除了 JetSamEvent 日志外焰扳,我們還可以通過 XNU 來獲取內(nèi)存的限制值。
在 XNU 中误续,有專門用于獲取內(nèi)存上限值的函數(shù)和宏吨悍。我們可以通過 memorystatus_priority_entry
這個結(jié)構(gòu)體,得到進程的優(yōu)先級和內(nèi)存限制值蹋嵌。
// 獲取進程的 pid育瓜、優(yōu)先級、狀態(tài)栽烂、內(nèi)存閾值等信息,priority 表示的是進程的優(yōu)先級躏仇,limit 就是我們想要的進程內(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;
// 基于下面這些宏可以達到查詢內(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++;
}
}
通過 XNU 的宏獲取內(nèi)存限制钙态,需要有 root 權(quán)限慧起,而 App 內(nèi)的權(quán)限是不夠的菇晃,所以正常情況下册倒,作為 App 開發(fā)者你是看不到這個信息的。
通過內(nèi)存警告獲取內(nèi)存限制值
還可以利用 didReceiveMemoryWarning 這個內(nèi)存壓力代理事件來動態(tài)地獲取內(nèi)存限制值磺送。
iOS 系統(tǒng)在強殺掉 App 之前還有 6 秒鐘的時間驻子,足夠你去獲取記錄內(nèi)存信息了。那么估灿,如何獲取當前內(nèi)存使用情況呢崇呵?
iOS 系統(tǒng)提供了一個函數(shù) task_info, 可以幫助我們獲取到當前任務(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)里包含了一個 resident_size 字段,用于表示使用了多少內(nèi)存汗销。這樣犹褒,我們就可以獲取到發(fā)生內(nèi)存警告時,當前 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)過測試,上面代碼獲取的內(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;
}
我們可以通過 os_proc_available_memory 獲取到當前可以用內(nèi)存宙枷,通過 phys_footprint 獲取到當前 App 占用內(nèi)存,2者的和也就是當前設(shè)備的內(nèi)存上限茧跋,超過即觸發(fā) Jetsam 機制慰丛。
定位內(nèi)存問題信息收集
現(xiàn)在,我們已經(jīng)可以通過三種方法來獲取內(nèi)存上限值了瘾杭,而且通過內(nèi)存警告的方式還能夠動態(tài)地獲取到這個值诅病。有了這個內(nèi)存上限值以后,你就可以進行內(nèi)存問題的信息收集工作了富寿。
要想精確地定位問題睬隶,我們就需要 dump 出完整的內(nèi)存信息,包括所有對象及其內(nèi)存占用值页徐,在內(nèi)存接近上限值的時候苏潜,收集并記錄下所需信息,并在合適的時機上報到服務(wù)器里变勇,方便分析問題恤左。
獲取到了每個對象的內(nèi)存占用量還不夠贴唇,你還需要知道是誰分配的內(nèi)存,這樣才可以精確定位到問題的關(guān)鍵所在飞袋。一個對象可能會在不同的函數(shù)里被分配了內(nèi)存并被創(chuàng)建了出來戳气,當這個對象內(nèi)存占用過大時,如果不知道是在哪個函數(shù)里創(chuàng)建的話巧鸭,問題依然很難精確定位出來瓶您。
內(nèi)存分配函數(shù)malloc
和 calloc
等默認使用的是nano_zone
。nano_zone
是 256B 以下小內(nèi)存的分配纲仍,大于 256B 的時候會使用 scalable_zone
來分配呀袱。
如果主要是針對大內(nèi)存的分配監(jiān)控,可以只針對 scalable_zone 進行分析郑叠,同時也可以過濾掉很多小內(nèi)存分配監(jiān)控夜赵。比如,malloc 函數(shù)用的是 malloc_zone_malloc乡革,calloc 用的是 malloc_zone_calloc寇僧。
malloc_zone_malloc 函數(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)存后就開始使用 malloc_logger 進行進行記錄
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ù)是怎么包裝的嘁傀,最終都會調(diào)用 malloc_logger 函數(shù)。
可以 去 Hook 這個函數(shù)推穷,加上自己的統(tǒng)計記錄就能夠通盤掌握內(nèi)存的分配情況心包。出現(xiàn)問題時,將內(nèi)存分配記錄的日志撈上來馒铃,你就能夠跟蹤到導致內(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);
}
收集上報
分配堆棧可以用 backtrace 函數(shù)捕獲区宇,但捕獲到的地址是虛擬內(nèi)存地址娃殖,不能從符號表 dsym 解析符號。所以還要記錄每個 image 加載時的偏移 slide议谷,符號表地址 = 堆棧地址 - slide炉爆。
降低OOM
基于內(nèi)存快照
抖音有一篇文章講述了基于內(nèi)存快照,降低OOM的文章:https://juejin.cn/post/6885144933997494280
簡單總結(jié):
線上 Memory Graph 采集內(nèi)存快照主要是為了獲取當前運行狀態(tài)下所有內(nèi)存對象以及對象之間的引用關(guān)系,用于后續(xù)的問題分析卧晓。主要需要獲取的信息如下:
- 所有內(nèi)存的節(jié)點芬首,以及其符號信息(如OC/Swift/C++ 實例類名,或者是某種有特殊用途的 VM 節(jié)點的 tag 等)逼裆。
- 節(jié)點之間的引用關(guān)系郁稍,以及符號信息(偏移,或者實例變量名)胜宇,OC/Swift成員變量還需要記錄引用類型耀怜。
由于采集的過程發(fā)生在程序正常運行的過程中恢着,為了保證不會因為采集內(nèi)存快照導致程序運行異常,整個采集過程需要在一個相對靜止的運行環(huán)境下完成财破。因此掰派,整個快照采集的過程大致分為以下幾個步驟:
- 掛起所有非采集線程。
- 獲取所有的內(nèi)存節(jié)點左痢,內(nèi)存對象引用關(guān)系以及相應的輔助信息靡羡。
- 寫入文件。
- 恢復線程狀態(tài)抖锥。
具體的過程
內(nèi)存節(jié)點的獲取
程序的內(nèi)存都是由虛擬內(nèi)存組成的亿眠,每一塊單獨的虛擬內(nèi)存被稱之為VM Region,通過 mach 內(nèi)核的vm_region_recurse/vm_region_recurse64
函數(shù)我們可以遍歷進程內(nèi)所有VM Region磅废,并通過vm_region_submap_info_64結(jié)構(gòu)體獲取以下信息:
- 虛擬地址空間中的地址和大小。
- Dirty 和 Swapped 內(nèi)存頁數(shù)荆烈,表示該VM Region的真實物理內(nèi)存使用拯勉。
- 是否可交換,Text 段憔购、共享 mmap 等只讀或隨時可以被交換出去的內(nèi)存宫峦,無需關(guān)注。
- user_tag玫鸟,用戶標簽导绷,用于提供該VM Region的用途的更準確信息。
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é)點大致分為這幾類:
- App的二進制文件在內(nèi)存的映射(如OnlineMemoryGraphDemo)
- 動態(tài)庫在內(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 作為一個單獨的內(nèi)存節(jié)點,僅記錄起始地址和 Dirty押桃、Swapped 內(nèi)存作為大小葵萎,以及與其他節(jié)點之間的引用關(guān)系;而 libmalloc 維護的堆內(nèi)存所在的 VM Region 則由于往往包含大多數(shù)業(yè)務(wù)邏輯中的 Objective-C 對象唱凯、C/C++對象羡忘、buffer 等,可以獲取更詳細的引用信息
在 iOS 系統(tǒng)中為了避免所有的內(nèi)存分配都使用系統(tǒng)調(diào)用產(chǎn)生性能問題磕昼,相關(guān)的庫負責一次申請大塊內(nèi)存卷雕,再在其之上進行二次分配并進行管理,提供給小塊需要動態(tài)分配的內(nèi)存對象使用掰烟,稱之為堆內(nèi)存爽蝴。
程序中使用到絕大多數(shù)的動態(tài)內(nèi)存都通過堆進行管理沐批,在 iOS 操作系統(tǒng)上,主要的業(yè)務(wù)邏輯分配的內(nèi)存都通過libmalloc進行管理蝎亚,部分系統(tǒng)庫為了性能也會使用自己的單獨的堆管理九孩,例如WebKit內(nèi)核使用bmalloc,CFNetwork也使用自己獨立的堆发框,在這里我們只關(guān)注libmalloc內(nèi)部的內(nèi)存管理狀態(tài)躺彬,而不關(guān)心其它可能的堆(即這部分特殊內(nèi)存會以VM Region的粒度存在,不分析其內(nèi)部的節(jié)點引用關(guān)系)梅惯。
我們可以通過malloc_get_all_zones獲取libmalloc內(nèi)部所有的zone宪拥,并遍歷每個zone中管理的內(nèi)存節(jié)點,獲取 libmalloc 管理的存活的所有內(nèi)存節(jié)點的指針和大小铣减。
獲取所有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é)點
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);
}
符號化
獲取內(nèi)存節(jié)點之后她君,我們需要為每個節(jié)點找到更加詳細的類型名稱,用于后續(xù)的分析葫哗。
- 對于 VM Region 內(nèi)存節(jié)點缔刹,我們可以通過 user_tag 賦予它有意義的符號信息;
- 堆內(nèi)存對象包含 raw buffer劣针,Objective-C/Swift校镐、C++等對象。對于 Objective-C/Swift捺典、C++這部分鸟廓,我們通過內(nèi)存中的一些運行時信息,嘗試符號化獲取更加詳細的信息襟己。
Objective/Swift 對象的符號化相對比較簡單引谜,Swift在內(nèi)存布局上兼容了Objective-C,也有isa指針稀蟋,objc相關(guān)方法可以作用于兩種語言的對象上煌张。只要保證 isa 指針合法,對象實例大小滿足條件即可認為正確退客。
獲取所有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\. 判斷是否是一個保護type_info的C++對象
else if ([CPPObjectUtil cppTypeInfoName:(void *)range.address] != NULL) {
NSLog(@"Find a Cpp Object:%s!", [CPPObjectUtil cppTypeInfoName:(void *)range.address]);
}
}
}
C++對象根據(jù)是否包含虛表可以分成兩類骏融。對于不包含虛表的對象,因為缺乏運行時數(shù)據(jù)萌狂,無法進行處理档玻。
對于對于包含虛表的對象, 可以通過 std::type_info 和以下幾個 section 的信息獲取對應的類型信息。
- type_name string - 類名對應的常量字符串茫藏,存儲在__TEXT/__RODATA段的__const section中误趴。
- type_info - 存放在__DATA/__DATA_CONST段的__const section中。
- vtable - 存放在__DATA/__DATA_CONST段的__const section中务傲。
如何判斷是不是一個C++對象
獲取App二進制加載到內(nèi)存中起始地址凉当,_dyld_register_func_for_add_image方法當App的二進制或者動態(tài)庫等MachO格式的文件映射到內(nèi)存后枣申,啟動App時的回調(diào),我們可以通過這個拿到App執(zhí)行二進制的起始地址看杭,從而拿到段中的C++類型信息忠藤。
獲取App二進制起始地址
/*
* 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地址不會超出這里
uint64_t dataConstMinAddress = NULL;
uint64_t dataConstMaxAddress = NULL;
static void dyld_callback(const struct mach_header *mhp, intptr_t vmaddr_slide)
{
// 這里只分析App的二進制
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);
}
}
}
}
// 分析動態(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);
}
}
判斷一個地址是否為一個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),還有一類特殊的對象贮缅,即CoreFoundation榨咐。除了我們熟知的CFString、CFDictionary外等谴供,很多很多系統(tǒng)庫也使用 CF 對象块茁,比如CGImage、CVObject等憔鬼。從它們的 isa 指針獲取的Objective-C類型被統(tǒng)一成__NSCFType龟劲。由于 CoreFoundation 類型支持實時的注冊、注銷類型轴或,為了細化這部分的類型,我們通過逆向拿到 CoreFoundation 維護的類型 slot 數(shù)組的位置并讀取其數(shù)據(jù)仰禀,保證能夠安全的獲取準確的類型照雁。
UIImage *image = [UIImage imageNamed:@""];
CFTypeID typeId = CFGetTypeID(image.CGImage);
CFStringRef className = CFCopyTypeIDDescription(typeId);
NSLog(@"%@",className);
引用關(guān)系的構(gòu)建
整個內(nèi)存快照的核心在于重新構(gòu)建內(nèi)存節(jié)點之間的引用關(guān)系。在虛擬內(nèi)存中答恶,如果一個內(nèi)存節(jié)點引用了其它內(nèi)存節(jié)點饺蚊,則對應的內(nèi)存地址中會存儲指向?qū)Ψ降闹羔樦怠S幸韵路桨福?/p>
- 遍歷一個內(nèi)存節(jié)點中所有可能存儲了指針的范圍獲取其存儲的值 A悬嗓。
- 搜索所有獲得的節(jié)點污呼,判斷 A 是不是某一個內(nèi)存節(jié)點中任何一個字節(jié)的地址,如果是包竹,則認為是一個引用關(guān)系燕酷。
- 對所有內(nèi)存節(jié)點重復以上操作。
對于一些特定的內(nèi)存區(qū)域周瞎,為了獲取更詳細的信息用于排查問題苗缩,對棧內(nèi)存以及 Objective-C/Swift 的堆內(nèi)存進行了一些額外的處理。
其中声诸,棧內(nèi)存也以VM Region的形式存在酱讶,棧上保存了臨時變量和 TLS 等數(shù)據(jù),獲取相應的引用信息可以幫助排查諸如 autoreleasepool 造成的內(nèi)存問題彼乌。由于棧并不會使用整個棧內(nèi)存泻肯,為了獲取 Stack 的引用關(guān)系渊迁,根據(jù)寄存器以及棧內(nèi)存獲取當前的棧可用范圍灶挟,排除未使用的棧內(nèi)存造成的無效引用琉朽。
而對于Objective-C/Swift對象,由于運行時包含額外的信息膏萧,我們可以獲得Ivar的強弱引用關(guān)系以及Ivar的名字漓骚,帶上這些信息有助于我們分析問題。 通過獲得Ivar的偏移榛泛,如果找到的引用關(guān)系的偏移和Ivar的偏移一致蝌蹂,則認為這個引用關(guān)系就是這個Ivar,可以將Ivar相關(guān)的信息附加上去曹锨。
參考鏈接