iOS Out-Of-Memory 原理闡述及方案調研

什么是 OOM?

OOM 的全稱是 Out-Of-Memory,是由于 iOS 的 Jetsam 機制造成的一種“另類” Crash,它不同于常規的 Crash,通過 Signal 捕獲等 Crash 監控方案無法捕獲到 OOM 事件。

為什么會發生 oom?

目前猜測兩種情況會造成 OOM,

  1. 系統整體內存使用較高,系統基于優先級殺死優先級較低的 App
  2. 當前使用的 App 達到了 “high water mark”,也就是達到了系統對單個 App 的內存限制,系統會將你 Kill

驗證方案 1 :

XNU 中 https://opensource.apple.com/source/xnu/xnu-3248.20.55/bsd/sys/kern_memorystatus.h.auto.htmlhttps://opensource.apple.com/source/xnu/xnu-3789.70.16/bsd/kern/kern_memorystatus.c.auto.html 提供了一些函數和宏,我們可以在 root 權限下使用這些宏和函數來獲取當前狀態下的所有 App 的 oom 內存閾值,并且基于 PID 甚至可以修改進程的 內存閾值,達到增大 oom內存閾值的效果。

對我們最有用的信息如下:

// 獲取進程的 pid、優先級、狀態、內存閾值等信息
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;
 
 
// 基于下面這些宏可以達到查詢內存閾值等信息,也可以修改內存閾值等
/* 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++;
    }
}

然后通過 MonekyDev 提供的 Command-line Tool 工具將程序注入到越獄設備(當時的測試環境為5s、iOS 9.1)中去,通過 SSH 連接到設備,然后通過終端運行該程序。就可以得到 dump 的信息。如下所示:

PID:  9967  Priority: 3 User Data: 0    Limit: 6    State:0x38 Tracked,IdleExit,Dirty
PID: 11151  Priority: 3 User Data: 0    Limit: 6    State:0x38 Tracked,IdleExit,Dirty
PID: 11154  Priority: 3 User Data: 0    Limit:10    State:0x38 Tracked,IdleExit,Dirty
PID: 11165  Priority: 3 User Data: 0    Limit: 6    State:0x38 Tracked,IdleExit,Dirty
PID: 11499  Priority: 3 User Data: 0    Limit:18    State:0x28 Tracked,Dirty
PID: 10039  Priority: 4 User Data: 2100 Limit:108   State:0x00
PID:  9981  Priority: 7 User Data: 0    Limit:10    State:0x08 Tracked
PID:  9977  Priority: 7 User Data: 0    Limit:20    State:0x08 Tracked
PID:  9979  Priority: 7 User Data: 0    Limit:25    State:0x38 Tracked,IdleExit,Dirty
PID: 10021  Priority: 7 User Data: 0    Limit: 6    State:0x08 Tracked
PID: 11575  Priority:10 User Data: 10100    Limit:650   State:0x00
PID:   103  Priority:11 User Data: 0    Limit:96    State:0x08 Tracked
PID: 11442  Priority:11 User Data: 0    Limit:38    State:0x08 Tracked
PID:    67  Priority:12 User Data: 0    Limit:24    State:0x28 Tracked,Dirty
PID:    31  Priority:14 User Data: 0    Limit:650   State:0x08 Tracked
PID:    45  Priority:14 User Data: 0    Limit: 9    State:0x08 Tracked

以上代碼中,Priority:10 的進程就是我測試的 好好學習 App,此時 App 在前臺并且活躍,所以優先級是 10,并且得到 oom 內存閾值是 650

驗證方案 2 :

當我們的 App 由于 jetsam 被殺死的時候,在手機中會有系統日志,從手機設置-隱私-分析這條操作路徑中,可以拿到JetsamEvent 開頭的日志。這些日志中就可以獲取一些關于 App 的內存信息,以我的 6s 為例,pageSize * rpages 的值獲取的值便是閾值,同時日志中也表明原因是 "reason" : "per-process-limit" (并不是所有的 JetsamEvent 中都可以拿到準確的閾值,有的存在偏差。。。)

"pageSize" : 16384
{
    "uuid" : "b8d6682c-5903-3007-b9c2-561d1e6ca9d5",
    "states" : [
      "frontmost",
      "resume"
    ],
    "killDelta" : 18859,
    "genCount" : 0,
    "age" : 1775369503,
    "purgeable" : 0,
    "fds" : 50,
    "coalition" : 691,
    "rpages" : 89600,
    "reason" : "per-process-limit",
    "pid" : 960,
    "cpuTime" : 1.6920809999999999,
    "name" : "MemoryLimitTest",
    "lifetimeMax" : 34182
}

驗證方案 3:

可以通過大量的測試來尋找它的oom 內存閾值是多少,StackOverFlow 上已經存在一個清單,該清單列舉了一些常見設備的 oom 閾值。該清單閾值和真實閾值存在偏差,我猜測原有有二,第一,它取內存的時機不可能完全和 oom 時機吻合,只能盡可能接近這個時機,第二,他取內存的方法和 XNU 中 jetsam 機制所用的內存獲取方式不一致。正確獲取內存的方式下面會闡述。

Results of testing with the utility Split wrote (link is in his answer):
device: (crash amount/total amount/percentage of total)
iPad1: 127MB/256MB/49%
iPad2: 275MB/512MB/53%
iPad3: 645MB/1024MB/62%
iPad4: 585MB/1024MB/57% (iOS 8.1)
iPad Mini 1st Generation: 297MB/512MB/58%
iPad Mini retina: 696MB/1024MB/68% (iOS 7.1)
iPad Air: 697MB/1024MB/68%
iPad Air 2: 1383MB/2048MB/68% (iOS 10.2.1)
iPad Pro 9.7": 1395MB/1971MB/71% (iOS 10.0.2 (14A456))
iPad Pro 10.5”: 3057/4000/76% (iOS 11 beta4) 
iPad Pro 12.9” (2015): 3058/3999/76% (iOS 11.2.1)
iPad Pro 12.9” (2017): 3057/3974/77% (iOS 11 beta4)
iPod touch 4th gen: 130MB/256MB/51% (iOS 6.1.1)
iPod touch 5th gen: 286MB/512MB/56% (iOS 7.0)
iPhone4: 325MB/512MB/63%
iPhone4s: 286MB/512MB/56%
iPhone5: 645MB/1024MB/62%
iPhone5s: 646MB/1024MB/63%
iPhone6: 645MB/1024MB/62% (iOS 8.x)
iPhone6+: 645MB/1024MB/62% (iOS 8.x)
iPhone6s: 1396MB/2048MB/68% (iOS 9.2)
iPhone6s+: 1392MB/2048MB/68% (iOS 10.2.1)
iPhoneSE: 1395MB/2048MB/69% (iOS 9.3)
iPhone7: 1395/2048MB/68% (iOS 10.2)
iPhone7+: 2040MB/3072MB/66% (iOS 10.2.1)
iPhone X: 1392/2785/50% (iOS 11.2.1)

https://stackoverflow.com/questions/5887248/ios-app-maximum-memory-budget/15200855#15200855

如何正確度量 App 的使用內存

常見的獲取 App 內存的方式是使用 resident_size 代碼如下:

#import <mach/mach.h>

- (int64_t)memoryUsage {
    int64_t memoryUsageInByte = 0;
    struct task_basic_info taskBasicInfo;
    mach_msg_type_number_t size = sizeof(taskBasicInfo);
    kern_return_t kernelReturn = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t) &taskBasicInfo, &size);

    if(kernelReturn == KERN_SUCCESS) {
        memoryUsageInByte = (int64_t) taskBasicInfo.resident_size;
        NSLog(@"Memory in use (in bytes): %lld", memoryUsageInByte);
    } else {
        NSLog(@"Error with task_info(): %s", mach_error_string(kernelReturn));
    }

    return memoryUsageInByte;
}

而正確的方式應該是使用 phys_footprint,因為 Apple 就是用的這個指標,和 Apple 保持一致才能說明問題。可以看源碼驗證一下:https://opensource.apple.com/source/xnu/xnu-3789.70.16/bsd/kern/kern_memorystatus.c.auto.html

#import <mach/mach.h>

- (int64_t)memoryUsage {
    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(@"Memory in use (in bytes): %lld", memoryUsageInByte);
    } else {
        NSLog(@"Error with task_info(): %s", mach_error_string(kernelReturn));
    }

    return memoryUsageInByte;
}

oom 定位的方案

方案1:

最早看到 oom 相關的方案是 FaceBook 的一篇博客中講到的,https://code.facebook.com/posts/1146930688654547/reducing-fooms-in-the-facebook-ios-app/,通過排除法來統計 OOM 率是多少。當然這種方案統計的結果多少會與實際數據存在誤差,比如 ApplicationState 不準確,watchdog 也被統計在 oom 中之類的。

方案2:

近期騰訊也開源了自己的 OOM 定位方案,OOMDetector 組件:https://github.com/Tencent/OOMDetector 。這種方案通過利用 libmalloc 中的 malloc_logger 函數指針,可以通過堆棧來幫助開發定位大內存。但是也存在一些缺陷,就是頻繁的 dump 堆棧對 App 性能造成了影響,只能灰度一小部分用戶來進行數據統計和定位。

方案3:

基于近期的發現,可以在線下獲取 App 的 high water mark,也就是 oom 內存閾值。 那么就產生了方案3

  • 監控內存增長,在達到 high water mark 附近的時候,dump 內存信息,獲取對象名稱、對象個數、各對象的內存值;如果穩定可以全量開啟,不會有性能問題
  • OOMDetector 可以拿到分配內存的堆棧,對于定位到代碼層面更加有效;可以灰度開放
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容