iOS OOM處理

OOM是什么佩厚?

OOM的全稱是out of memory,字面意思也就是指內(nèi)存超出了限制说订。在iOS中的OOM是由操作系統(tǒng)的Jetsam機(jī)制出發(fā)的crash的一種抄瓦。由OOM導(dǎo)致的crash無法通過監(jiān)控singal獲取異常信息,所以對(duì)于OOM的監(jiān)控只能間接實(shí)現(xiàn)陶冷。

OOM和棧溢出有關(guān)系嗎钙姊?

程序在運(yùn)行過程中,為了臨時(shí)存取數(shù)據(jù)的需要埂伦,一般都要分配一些內(nèi)存空間摸恍,通常稱這些空間為緩沖區(qū)。如果向緩沖區(qū)中寫入超過其本身長度的數(shù)據(jù)赤屋,以致于緩沖區(qū)無法容納立镶,就會(huì)造成緩沖區(qū)以外的存儲(chǔ)單元被改寫,這種現(xiàn)象就稱為緩沖區(qū)溢出 緩沖區(qū)長度一般與用戶自己定義的緩沖變量的類型有關(guān)类早。棧溢出就是緩沖區(qū)溢出的一種媚媒。
一般的奔潰日志上會(huì)有"Stack Guard"字樣,一般會(huì)提示EXC_BAD_ACCESS

OOM 產(chǎn)生的原因

說說Jetsam機(jī)制

Jetsam時(shí)iOS系統(tǒng)的單獨(dú)的進(jìn)程涩僻,對(duì)于內(nèi)存管理則是BSD層創(chuàng)建的優(yōu)先級(jí)最高的常駐線程VM_memorystatus缭召,可以管理系統(tǒng)的內(nèi)存占用栈顷,當(dāng)發(fā)現(xiàn)內(nèi)存緊張時(shí)候會(huì)根據(jù)優(yōu)先級(jí)殺掉其他應(yīng)用程序進(jìn)程∏断铮可以簡單理解為內(nèi)存管理的的奔潰處理機(jī)制就是Jetsam機(jī)制萄凤。
macOS或者windows系統(tǒng),當(dāng)應(yīng)用程序緊張的時(shí)候搪哪,可以通過SWAP內(nèi)存交換機(jī)制實(shí)現(xiàn)把物理內(nèi)存中的一部分內(nèi)容交換到磁盤上去靡努,利用磁盤空間擴(kuò)展內(nèi)存空間。對(duì)于移動(dòng)設(shè)備來說一般沒有內(nèi)存交換機(jī)制晓折,原因在于移動(dòng)設(shè)備的存儲(chǔ)介質(zhì)也就是閃存惑朦,而閃存的性能和使用壽命是無法和電腦硬盤相比的,所以當(dāng)內(nèi)存緊張時(shí)漓概,就會(huì)系統(tǒng)的Jetsam就會(huì)殺死應(yīng)用程序漾月。

Compressed memory

iOS 上沒有Disk swap機(jī)制,取而代之使用 Compressed memory胃珍。從 OS X Mavericks Core Technology Overview 文檔中可以了解到該技術(shù)在內(nèi)存緊張時(shí)能夠?qū)⒆罱褂眠^的內(nèi)存占用壓縮至原有大小的一半以下梁肿,并且能夠在需要時(shí)解壓復(fù)用。它在節(jié)省內(nèi)存的同時(shí)提高了系統(tǒng)的響應(yīng)速度觅彰,其特點(diǎn)可以歸結(jié)為:
Shrinks memory usage 減少了不活躍內(nèi)存占用
Improves power efficiency 改善電源效率吩蔑,通過壓縮減少磁盤IO帶來的損耗
Minimizes CPU usage 壓縮/解壓十分迅速,能夠盡可能減少 CPU 的時(shí)間開銷
Is multicore aware 支持多核操作
本質(zhì)上缔莲,Compressed memory 也是 Dirty memory
因此哥纫, memory footprint = dirty size + compressed size ,這也就是我們需要并且能夠嘗試去減少的內(nèi)存占用痴奏。
NSCache 分配的內(nèi)存實(shí)際上是 Purgeable Memory蛀骇,可以由系統(tǒng)自動(dòng)釋放。NSCache 與 NSPureableData 的結(jié)合使用既能讓系統(tǒng)根據(jù)情況回收內(nèi)存读拆,也可以在內(nèi)存清理的同時(shí)移除相關(guān)對(duì)象擅憔。

Jetsam機(jī)制殺死進(jìn)程的順序

Jetsam機(jī)制殺死進(jìn)程的順序一般基于應(yīng)用程序的優(yōu)先級(jí)確定的,優(yōu)先級(jí)低的進(jìn)程先于優(yōu)先級(jí)高的進(jìn)程被殺死檐晕。在iOS系統(tǒng)中應(yīng)用程序的優(yōu)先級(jí)時(shí)不可能高于操作系統(tǒng)和內(nèi)核的暑诸。前臺(tái)的應(yīng)用程序的優(yōu)先級(jí)高于后臺(tái)應(yīng)用程序,對(duì)于多個(gè)后臺(tái)程序的優(yōu)先級(jí)也是不完全一樣的辟灰,系統(tǒng)會(huì)對(duì)每一個(gè)進(jìn)程的優(yōu)先級(jí)進(jìn)行動(dòng)態(tài)調(diào)整个榕。例如如果耗費(fèi)CPU太多就降低優(yōu)先級(jí),如果一個(gè)線程過度挨餓CPU則會(huì)提升其優(yōu)先級(jí)芥喇。
需要注意的是西采,JETSAM不一定只殺一個(gè)進(jìn)程,他可能會(huì)大殺特殺继控,殺掉N多進(jìn)程械馆。

typedef struct memstat_bucket {
    TAILQ_HEAD(, proc) list;//一個(gè)TAILQ_HEAD的雙向鏈表胖眷,用來存放這個(gè)優(yōu)先級(jí)下面的進(jìn)程
    int count;//進(jìn)程的個(gè)數(shù),也就是上面list的數(shù)量
} memstat_bucket_t;
//內(nèi)核里面對(duì)于所有的進(jìn)程都有一個(gè)優(yōu)先級(jí)的分布霹崎,通過一個(gè)數(shù)組維護(hù)珊搀,數(shù)組每一項(xiàng)是一個(gè)進(jìn)程的list。這個(gè)數(shù)組的大小是JETSAM_PRIORITY_MAX + 1
因?yàn)閍pple的內(nèi)核xnu代碼是開源的尾菇,我們可以從kern_memorystatus.h中獲取到相關(guān)的

進(jìn)程優(yōu)先級(jí)的聲明,數(shù)值越大表明優(yōu)先級(jí)越高错沽,前臺(tái)進(jìn)程JETSAM_PRIORITY_FOREGROUND(10)大于后臺(tái)進(jìn)程JETSAM_PRIORITY_BACKGROUND(3)

#ifndef SYS_MEMORYSTATUS_H
#define SYS_MEMORYSTATUS_H

#include <stdint.h>
#include <sys/time.h>
#include <sys/proc.h>
#include <sys/param.h>
#include <mach_debug/zone_info.h>

#define MEMORYSTATUS_ENTITLEMENT "com.apple.private.memorystatus"

#define JETSAM_PRIORITY_REVISION                  2
#define JETSAM_PRIORITY_IDLE_HEAD                -2
/* The value -1 is an alias to JETSAM_PRIORITY_DEFAULT */
#define JETSAM_PRIORITY_IDLE                      0
#define JETSAM_PRIORITY_IDLE_DEFERRED         1 /* Keeping this around till all xnu_quick_tests can be moved away from it.*/
#define JETSAM_PRIORITY_AGING_BAND1       JETSAM_PRIORITY_IDLE_DEFERRED
#define JETSAM_PRIORITY_BACKGROUND_OPPORTUNISTIC  2
#define JETSAM_PRIORITY_AGING_BAND2       JETSAM_PRIORITY_BACKGROUND_OPPORTUNISTIC
#define JETSAM_PRIORITY_BACKGROUND                3
#define JETSAM_PRIORITY_ELEVATED_INACTIVE     JETSAM_PRIORITY_BACKGROUND
#define JETSAM_PRIORITY_MAIL                      4
#define JETSAM_PRIORITY_PHONE                     5
#define JETSAM_PRIORITY_UI_SUPPORT                8
#define JETSAM_PRIORITY_FOREGROUND_SUPPORT        9
#define JETSAM_PRIORITY_FOREGROUND               10
#define JETSAM_PRIORITY_AUDIO_AND_ACCESSORY      12
#define JETSAM_PRIORITY_CONDUCTOR                13
#define JETSAM_PRIORITY_HOME                     16
#define JETSAM_PRIORITY_EXECUTIVE                17
#define JETSAM_PRIORITY_IMPORTANT                18
#define JETSAM_PRIORITY_CRITICAL                 19
#define JETSAM_PRIORITY_MAX                      21

/* TODO - tune. This should probably be lower priority */
#define JETSAM_PRIORITY_DEFAULT                  18
#define JETSAM_PRIORITY_TELEPHONY                19

/* Compatibility */
#define DEFAULT_JETSAM_PRIORITY                  18
#define DEFERRED_IDLE_EXIT_TIME_SECS             10
#define KEV_MEMORYSTATUS_SUBCLASS                 3

如何捕獲OOM

關(guān)于didReceiveMemoryWarning方法

可以在UIViewController中實(shí)現(xiàn)didReceiveMemoryWarning方法耀里,也可以在AppDelegate中實(shí)現(xiàn)applicationDidReceiveMemoryWarning:方法蜡峰,也可以在注冊(cè)UIApplicationDidReceiveMemoryWarningNotification通知處理秒啦。

出現(xiàn)OOM前一定會(huì)調(diào)用didReceiveMemoryWarning么即舌?

答案當(dāng)然是不一定的佣盒。因?yàn)閐idReceiveMemoryWarning調(diào)用實(shí)在主線程的,如果瞬間申請(qǐng)了大塊內(nèi)存顽聂,而此時(shí)主線程正忙于其他的事情肥惭,此時(shí)會(huì)導(dǎo)致發(fā)生了OOM而無法獲取didReceiveMemoryWarning調(diào)用。

觸發(fā)didReceiveMemoryWarning之后一定會(huì)導(dǎo)致OOM嗎紊搪?

顯示也是不會(huì)的蜜葱,因?yàn)楫?dāng)收到內(nèi)存警告,如果之后內(nèi)存下降了耀石,也不會(huì)導(dǎo)致OOM

內(nèi)存閥值的獲取

可以在即將到達(dá)內(nèi)存閥值時(shí)牵囤,處理對(duì)象釋放嗎?

理論上可以這么處理的滞伟,例如我們知道了當(dāng)前設(shè)備/系統(tǒng)的內(nèi)存閥值揭鳞,我們定義一個(gè)范圍,例如監(jiān)控到當(dāng)前內(nèi)存為內(nèi)存閥值的80%诗良,此處通過開源框架KSCrash或者BSBacktraceLogger獲取汹桦,當(dāng)然也可以自己實(shí)現(xiàn),可以參考文章《iOS 如何抓取線程的“方法調(diào)用椉”舞骆?》。對(duì)于內(nèi)存分析径荔,僅僅實(shí)現(xiàn)對(duì)于堆棧的回溯是不夠的督禽,我們更關(guān)心的是找出內(nèi)存最大的對(duì)象,進(jìn)行引用關(guān)系的分析总处,在Debug下可以參考FLEX庫的實(shí)現(xiàn)狈惫,通過malloc_get_all_zones可以獲取所有堆區(qū)的對(duì)象,通過objc_getClass獲取對(duì)應(yīng)的對(duì)象名鹦马,通過class_getInstanceSize獲取單個(gè)對(duì)象的大小胧谈。

kern_return_t result = malloc_get_all_zones(mach_task_self(), &memory_reader, &zones, &zoneCount);
    if (result == KERN_SUCCESS) {
        for (unsigned int i = 0; i < zoneCount; i++) {
            malloc_zone_t *zone = (malloc_zone_t *)zones[i];
            3.
            if (zone->introspect && zone->introspect->enumerator) {
                zone->introspect->enumerator(mach_task_self(), (__bridge void *)(block), MALLOC_PTR_IN_USE_RANGE_TYPE, zones[i], &memory_reader, &range_callback);
            }
        }
    }
首先我們要了解內(nèi)存閥值的獲取

可以開辟子線程忆肾,循環(huán)申請(qǐng)1M的內(nèi)存直到出現(xiàn)收到內(nèi)存警告和獲取OOM的值為止。至于為什么是申請(qǐng)IM的內(nèi)存菱肖,在apple的A7處理器之前客冈,物理內(nèi)存和虛擬內(nèi)存都是按照4KB進(jìn)行分頁的,但是A7之后虛擬內(nèi)存是按照16KB分頁的稳强,物理內(nèi)存還是4KB分頁场仲,如果申請(qǐng)的內(nèi)存顆粒度小于虛擬內(nèi)存頁的大小意義不大,另外考慮到統(tǒng)計(jì)的顆粒度退疫,也可以是其他的數(shù)值渠缕。

//獲取內(nèi)存當(dāng)前占用,不需要使用taskInfo. resident_size獲取褒繁,因?yàn)閞esident_size不準(zhǔn)確
- (int)limitSizeOfMemory {
    if (@available(iOS 13.0, *)) {
        return os_proc_available_memory() / 1024.0 / 1024.0;;
    } else {
        task_vm_info_data_t taskInfo;
        mach_msg_type_number_t infoCount = TASK_VM_INFO_COUNT;
        kern_return_t kernReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&taskInfo, &infoCount);

        if (kernReturn != KERN_SUCCESS) {
            return 0;
        }
        return (int) taskInfo.phys_footprint / 1024.0 / 1024.0;
     }
    return 0;
}

stackoverflow上有人對(duì)不同設(shè)備的OOM閥值進(jìn)行了統(tǒng)計(jì)

如何監(jiān)控內(nèi)存分配

FBAllocationTracker

iOS中最早可以追溯到Facebook的FBAllocationTracker庫亦鳞。實(shí)現(xiàn)原理為hook NSObject的alloc/dealloc方法,保存實(shí)例對(duì)象澜汤。對(duì)于部分類(NSCFTimer蚜迅、NSAutoreleasePool舵匾、NSTaggedPointerStringCStringContainer)由于性能和crash原因俊抵,忽略hook。在下一次啟動(dòng)時(shí)通過排除發(fā)分析上一次的結(jié)束進(jìn)程的原因是否為OOM導(dǎo)致的坐梯。從如下幾個(gè)緯度采取排除法進(jìn)行分析徽诲,如果終止進(jìn)程不是下面行為導(dǎo)致的,則判定為FOOM(前臺(tái)應(yīng)用OOM)吵血,因?yàn)楹笈_(tái)應(yīng)用的OOM可能是前臺(tái)應(yīng)用占用內(nèi)存過大被動(dòng)導(dǎo)致的系統(tǒng)強(qiáng)殺谎替。

  • app版本號(hào)是否發(fā)生了改變
  • app是否發(fā)生了crash
  • 是否為用戶手動(dòng)退出
  • 操作系統(tǒng)版本升級(jí)
  • 之前進(jìn)程終止是否在后臺(tái)

此方案因?yàn)樯婕癗SObject的alloc/delloc進(jìn)行全量的hock,所以對(duì)于性能會(huì)有一定的影響蹋辅。且不一定繼承自NSObject的對(duì)象都會(huì)走alloc方法钱贯,例如:NSData創(chuàng)建對(duì)象的類靜態(tài)方法沒有調(diào)用+[NSObject alloc]秩命,里面實(shí)現(xiàn)是調(diào)用C方法NSAllocateObject來創(chuàng)建對(duì)象,也就是說這類方式創(chuàng)建的OC對(duì)象無法通過hook來獲取OC類名霹菊。因?yàn)镕undation框架沒有開源旋廷,但Core Foundation框架的源代碼待诅,以及通過調(diào)用NSObject類進(jìn)行內(nèi)存管理部分的源代碼是公開的卑雁。但是Fundation部分可以通過GNU step的libobjc2獲取

+ (id)alloc {
        return [self allocWithZone:NSDefaultMallocZone()];
}

+ (id)allocWithZone:(struct _NSZone *)zone {
        return NSAllocateObject(self, 0, z);
}
OOMDetector

2018年QQ開源了他們自己的監(jiān)控組件OOMDetector。主要參考了系統(tǒng)的libmalloc庫中stack_logging_disk.c文件鬼吵,可以通過malloc_logger實(shí)現(xiàn)對(duì)于malloc/free的監(jiān)控齿椅,而__syscall_logger可以實(shí)現(xiàn)對(duì)于vm_allocate, vm_deallocate, mmap, munmap的監(jiān)控涣脚。但是__syscall_logger是私有方法遣蚀,所以在OOMDetector組件中使用中也提到了建議只是在Debug環(huán)境下使用。

// We set malloc_logger to NULL to disable logging, if we encounter errors
// during file writing
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;

extern malloc_logger_t *__syscall_logger;   // use this to set up syscall logging (e.g., vm_allocate, vm_deallocate, mmap, munmap)

OOMDetector組件使用的聲明
__syscall_logger函數(shù)

#define USE_VM_LOGGER

#ifdef USE_VM_LOGGER //監(jiān)控非malloc方式直接申請(qǐng)內(nèi)存
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* __syscall_logger;
#endif

malloc_logger函數(shù)

#import <Foundation/Foundation.h>
#import <malloc/malloc.h>
#import "CStackHelper.h"

#ifdef __cplusplus
extern "C" {
#endif
    extern malloc_zone_t *global_memory_zone;
    
    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 common_stack_logger(uint32_t type, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3, uintptr_t result, uint32_t backtrace_to_skip);
    
#ifdef __cplusplus
    }
#endif
matrix

微信在2019年開源了內(nèi)存和幀率CPU耗電等監(jiān)控方案matrix玖喘,其中有涉及到OOM情況的監(jiān)控累奈。修改malloc_default_zone函數(shù)返回的malloc_zone_t結(jié)構(gòu)體里的malloc赠群、free等函數(shù)指針,也是可以監(jiān)控堆內(nèi)存分配,效果等同于malloc_logger;
apple源碼中的_malloc_zone_t結(jié)構(gòu)如下

typedef struct _malloc_zone_t {
void    *reserved1;    /* RESERVED FOR CFAllocator DO NOT USE */
    void    *reserved2;    /* RESERVED FOR CFAllocator DO NOT USE */
    size_t     (* MALLOC_ZONE_FN_PTR(size))(struct _malloc_zone_t *zone, const void *ptr); /* returns the size of a block or 0 if not in this zone; must be fast, especially for negative answers */
    void     *(* MALLOC_ZONE_FN_PTR(malloc))(struct _malloc_zone_t *zone, size_t size);//malloc函數(shù)調(diào)用
    void     *(* MALLOC_ZONE_FN_PTR(calloc))(struct _malloc_zone_t *zone, size_t num_items, size_t size); /* same as malloc, but block returned is set to zero */
    void     *(* MALLOC_ZONE_FN_PTR(valloc))(struct _malloc_zone_t *zone, size_t size); /* same as malloc, but block returned is set to zero and is guaranteed to be page aligned */
    void     (* MALLOC_ZONE_FN_PTR(free))(struct _malloc_zone_t *zone, void *ptr);//free 函數(shù)調(diào)用
    void     *(* MALLOC_ZONE_FN_PTR(realloc))(struct _malloc_zone_t *zone, void *ptr, size_t size);
    void     (* MALLOC_ZONE_FN_PTR(destroy))(struct _malloc_zone_t *zone);
    const char    *zone_name;

    unsigned    (* MALLOC_ZONE_FN_PTR(batch_malloc))(struct _malloc_zone_t *zone, size_t size, void **results, unsigned num_requested);

    struct malloc_introspection_t    * MALLOC_INTROSPECT_TBL_PTR(introspect);
    unsigned    version;

    void *(* MALLOC_ZONE_FN_PTR(memalign))(struct _malloc_zone_t *zone, size_t alignment, size_t size);

    void (* MALLOC_ZONE_FN_PTR(free_definite_size))(struct _malloc_zone_t *zone, void *ptr, size_t size);

    size_t     (* MALLOC_ZONE_FN_PTR(pressure_relief))(struct _malloc_zone_t *zone, size_t goal);

    boolean_t (* MALLOC_ZONE_FN_PTR(claimed_address))(struct _malloc_zone_t *zone, void *ptr);

} malloc_zone_t;

而在matrix中對(duì)于malloc_zone_t修改部分如下

static malloc_zone_t *inter_zone = malloc_create_zone(getpagesize(), 0);

#pragma mark Allocation/Deallocation Function without Logging

void *inter_malloc(uint64_t memSize)
{
    __set_thread_to_ignore_logging(current_thread_id(), true);
    void *allocatedMem = inter_zone->malloc(inter_zone, (size_t)memSize);
    __set_thread_to_ignore_logging(current_thread_id(), false);
    return allocatedMem;
}

void *inter_realloc(void *oldMem, size_t newSize)
{
    __set_thread_to_ignore_logging(current_thread_id(), true);
    void *newMem = inter_zone->realloc(inter_zone, oldMem, newSize);
    __set_thread_to_ignore_logging(current_thread_id(), false);
    return newMem;
}

void inter_free(void *ptr)
{
    __set_thread_to_ignore_logging(current_thread_id(), true);
    inter_zone->free(inter_zone, ptr);
    __set_thread_to_ignore_logging(current_thread_id(), false);
}

matrix中對(duì)于dispatch內(nèi)存監(jiān)控使用的是fishhock庫的rebind_symbols函數(shù)窝爪,具體的代碼邏輯如下

MatrixAsyncHook.m類
//hock處理的宏
#define BEGIN_HOOK(func) \
ks_rebind_symbols((struct ks_rebinding[2]){{#func, WRAP(func), (void *)&ORIFUNC(func)}}, 1);

//開始處理dispatch相關(guān)的hock
- (void)beginHook
{
    // 1. hook dispatch
    BEGIN_HOOK(dispatch_async);
    BEGIN_HOOK(dispatch_after);
    BEGIN_HOOK(dispatch_barrier_async);
    
    BEGIN_HOOK(dispatch_async_f);
    BEGIN_HOOK(dispatch_after_f);
    BEGIN_HOOK(dispatch_barrier_async_f);
}

為了更好的監(jiān)控OOM纷跛,Matrix在Debug環(huán)境下也使用了私有API,通過下面宏的定義可以看出

#ifdef DEBUG
#define USE_PRIVATE_API
#endif

而正式開始監(jiān)控OOM的方法為enable_memory_logging

int enable_memory_logging(const char *log_dir)
{
    err_code = MS_ERRC_SUCCESS;
    //允許使用私有API時(shí)候贫奠,通過"stack_logging_enable_logging"判斷當(dāng)前環(huán)境是否有debug工具在使用
#ifdef USE_PRIVATE_API 
    // stack_logging_enable_logging
    int *stack_logging_enable_logging = (int *)dlsym(RTLD_DEFAULT, "stack_logging_enable_logging");
    if (stack_logging_enable_logging != NULL && *stack_logging_enable_logging != 0) {
        is_debug_tools_running = true;
    }
#endif

    // Check whether there's any analysis tool process logging memory.
    if (is_debug_tools_running || is_being_debugged()) {
        return MS_ERRC_ANALYSIS_TOOL_RUNNING;
    }
    
    logging_is_enable = true;
        //初始化二叉樹存儲(chǔ)保存對(duì)象的alloc事件
    allocation_event_writer = open_or_create_allocation_event_db(log_dir);
    if (allocation_event_writer == NULL) {
        return err_code;
    }
    //初始化二叉樹保存棧幀
    stack_frames_writer = open_or_create_stack_frames_db(log_dir);
    if (stack_frames_writer == NULL) {
        return err_code;
    }
    
    //event_buffer = open_or_create_allocation_event_buffer(log_dir);
    event_buffer = open_or_create_allocation_event_buffer_static();
    if (event_buffer == NULL) {
        return err_code;
    }
    //初始化處理的pthread
    if (__prepare_working_thread() == false) {
        __malloc_printf("create writing thread fail");
        return MS_ERRC_WORKING_THREAD_CREATE_FAIL;
    }
    //通過dyld判斷是否可以獲取images信息
    if (!prepare_dyld_image_logger(log_dir)) {
        return err_code;
    }
    //準(zhǔn)備處理hock alloc
    if (!prepare_object_event_logger(log_dir)) {
        return err_code;
    }
    //監(jiān)控系統(tǒng)的malloc_logger方法
    malloc_logger = __memory_event_callback;
    
#ifdef USE_PRIVATE_API
    // 如果在debug環(huán)境望蜡,則監(jiān)控私有方法__syscall_logger
    syscall_logger = (malloc_logger_t **)dlsym(RTLD_DEFAULT, "__syscall_logger");
    if (syscall_logger != NULL) {
        *syscall_logger = __memory_event_callback;
    }
#endif
    //至此唤崭,表明可以正常監(jiān)控OOM
    return MS_ERRC_SUCCESS;
}

在其中的prepare_object_event_logger方法簡要分析如下

bool prepare_object_event_logger(const char *log_dir)
{
        //加鎖
    object_types_mutex = __malloc_lock_init();
    object_types_file = __init_object_type_file(log_dir);
    if (object_types_file == NULL) {
        return false;
    }
    
    // Insert vm memory type names脖律,需要處理的vm memory類型谢肾,這個(gè)在matrix內(nèi)部以一個(gè)數(shù)組維護(hù)
    for (int i = 0; i < sizeof(vm_memory_type_names) / sizeof(char *); ++i) {
        uintptr_t str_hash = __string_hash(vm_memory_type_names[i]);
        uint32_t type = object_types_file->object_type_list->size() + 1;
        object_types_file->object_type_exists->insert(str_hash);
        object_types_file->object_type_list->insert(object_type(type, vm_memory_type_names[i]));
    }
    
#ifdef USE_PRIVATE_API
    //如果是在Debug環(huán)境,也就是說可以使用私有API状您,則通過__CFObjectAllocSetLastAllocEventNameFunction和__CFOASafe實(shí)現(xiàn)對(duì)于NSData等對(duì)象的hock處理。
    // __CFObjectAllocSetLastAllocEventNameFunction
    object_set_last_allocation_event_name_funcion = (void (**)(void *, const char *))dlsym(RTLD_DEFAULT, "__CFObjectAllocSetLastAllocEventNameFunction");
    if (object_set_last_allocation_event_name_funcion != NULL) {
        *object_set_last_allocation_event_name_funcion = object_set_last_allocation_event_name;
    }
    
    // __CFOASafe
    object_record_allocation_event_enable = (bool *)dlsym(RTLD_DEFAULT, "__CFOASafe");
    if (object_record_allocation_event_enable != NULL) {
        *object_record_allocation_event_enable = true;
    }
#endif
    //對(duì)于NSObjct的alloc方法的hock
    nsobject_hook_alloc_method();
    return true;
}

對(duì)于非allocation/deallocation管控的內(nèi)存拌汇,采用的是alloc和Debug下的私有方法__CFOASafe和__CFObjectAllocSetLastAllocEventNameFunction處理魁淳。

malloc_logger

對(duì)于前文提到的OOMDetector和Matrix開源庫中出現(xiàn)的對(duì)于malloc_logger回調(diào)函數(shù)的監(jiān)控与倡。malloc_logger是如何監(jiān)控到應(yīng)用層面的內(nèi)存分配的呢纺座?
在 libmalloc庫中的以下關(guān)于內(nèi)存相關(guān)的方法 malloc_zone_malloc, malloc_zone_calloc, malloc_zone_valloc, malloc_zone_realloc, malloc_zone_free, malloc_zone_free_definite_size, malloc_zone_memalign等函數(shù)內(nèi)部都會(huì)調(diào)用malloc_logger息拜,所以我們只需要監(jiān)控malloc_logger就可以實(shí)現(xiàn)對(duì)于內(nèi)存的alloc與free的監(jiān)控。

對(duì)于內(nèi)存監(jiān)控?cái)?shù)據(jù)的存儲(chǔ)

sqlite

在SQLite中,每一個(gè)表(含多字段)都用一個(gè)唯一的B-tree存儲(chǔ)少欺,數(shù)據(jù)庫有多個(gè)表就有多個(gè)B-tree喳瓣。而B-樹的性能總是等價(jià)于二分查找(與M值無關(guān))。
有人對(duì)移動(dòng)端常用數(shù)據(jù)庫性能做了比較赞别,SQLite 3在對(duì)于1萬條簡單數(shù)據(jù)查詢基本在331ms畏陕,而WCDB為690ms。而一個(gè)app啟動(dòng)時(shí)的對(duì)象創(chuàng)建和銷毀數(shù)量是巨大的仿滔,以微信為例惠毁,在啟動(dòng)10秒內(nèi),已經(jīng)創(chuàng)建了80萬對(duì)象崎页,釋放了50萬仁讨。保守估計(jì),按照sqlite3計(jì)算操作數(shù)據(jù)庫的時(shí)間基本在3s左右实昨。

數(shù)據(jù)庫種類 SQLite3 realm WCDB
簡單查詢一萬次耗時(shí) 331ms 699ms 690ms
9萬條數(shù)據(jù)基礎(chǔ)上連續(xù)單條插入一萬條數(shù)據(jù)耗時(shí) 1462ms 32851ms 750ms
dispatch 100個(gè)block來查詢一萬次耗時(shí) 150ms 205ms 199ms
二叉樹

二叉查找樹又稱二叉搜索樹洞豁,二叉排序樹,特點(diǎn)如下:

  1. 左子樹上所有結(jié)點(diǎn)值均小于根結(jié)點(diǎn)
  2. 右子樹上所有結(jié)點(diǎn)值均大于根結(jié)點(diǎn)
  3. 結(jié)點(diǎn)的左右子樹本身又是一顆二叉查找樹
  4. 二叉查找樹中序遍歷得到結(jié)果是遞增排序的結(jié)點(diǎn)序列荒给。
    查找最好時(shí)間復(fù)雜度O(logN)丈挟,最壞時(shí)間復(fù)雜度O(N)。和B-Tree時(shí)間復(fù)雜度是一樣的志电。
平衡二叉樹

微信和QQ采用的都是二叉樹存儲(chǔ)曙咽。對(duì)于為什么不采用數(shù)據(jù)庫,因?yàn)樵赼pp啟動(dòng)后挑辆,需要?jiǎng)?chuàng)建大量對(duì)象以及釋放大量對(duì)象例朱,采用二叉樹結(jié)構(gòu)比較合適,一般情況下二叉樹的時(shí)間復(fù)雜度為log(N)鱼蝉,但是傳統(tǒng)二叉樹在操作后可能變?yōu)閱捂湵砬樾稳鬣停藭r(shí)的時(shí)間復(fù)雜度就為O(N)了。

Matrix中是用數(shù)組實(shí)現(xiàn)二叉樹魁亦。具體做法是父結(jié)點(diǎn)的左右孩子由以往的指針類型改成整數(shù)類型渔隶,代表孩子在數(shù)組的下標(biāo);刪除結(jié)點(diǎn)時(shí)洁奈,被刪除的結(jié)點(diǎn)存放上一個(gè)被釋放的結(jié)點(diǎn)所在數(shù)組下標(biāo)间唉。


LabImage_c7bb49f11f4345daa138e1c797c6b9d2.png

數(shù)據(jù)上報(bào)

對(duì)于數(shù)據(jù)上報(bào),采取的是對(duì)上次結(jié)束進(jìn)程的判斷利术,基本邏輯和Facebook的開源框架FBAllocationTracker基本類似呈野,具體的流程如下


vtufqxef2i.jpeg

實(shí)際開發(fā)代碼層面如何避免

避免內(nèi)存泄露

內(nèi)存泄露會(huì)導(dǎo)致對(duì)象無法釋放,從而長駐內(nèi)存印叁,如果內(nèi)存泄露的對(duì)象數(shù)量一旦很多極易引發(fā)OOM被冒。常見的內(nèi)存泄露包括的block的操作军掂,以及Fundation框架操作的時(shí)候及時(shí)free對(duì)象,UIGraphicsBeginImageContext和UIGraphicsEndImageContext的成對(duì)出現(xiàn)等姆打。在Debug環(huán)境可以使用Memeory Graph進(jìn)行分析分寸泄露良姆,當(dāng)然也可以解除第三方工具,如MemoryLeaks處理等幔戏。

緩存盡量使用NSCache

因?yàn)镹SCahe內(nèi)部apple幫我們實(shí)現(xiàn)了在內(nèi)存達(dá)到閥值時(shí)(非OOM觸發(fā)閥值)玛追,內(nèi)部會(huì)根據(jù)LRC算法主動(dòng)處理對(duì)象的釋放。對(duì)于緩存部分盡量使用NSCache闲延。

合理使用自動(dòng)釋放池

通常autoreleased的對(duì)象在runloop結(jié)束時(shí)才釋放痊剖。如果在一些大型循環(huán)中,此時(shí)內(nèi)存會(huì)瞬間增長垒玲,而自動(dòng)釋放池可以更及時(shí)的釋放對(duì)象陆馁。

對(duì)于圖片的操作

對(duì)于iamge縮放的操作,可以用如下代碼實(shí)現(xiàn)

//常見的UIimage縮放寫法: 
- (UIImage *)scaleImage:(UIImage *)image newSize:(CGSize)newSize{
    UIGraphicsBeginImageContextWithOptions(newSize, NO, 0);
    [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return newImage;
}
//節(jié)約內(nèi)存的ImageIO縮放寫法:
+ (UIImage *)scaledImageWithData:(NSData *)data withSize:(CGSize)size scale:(CGFloat)scale orientation:(UIImageOrientation)orientation{
    CGFloat maxPixelSize = MAX(size.width, size.height);
    CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil);
    NSDictionary *options = @{(__bridge id)kCGImageSourceCreateThumbnailFromImageAlways : (__bridge id)kCFBooleanTrue,
                              (__bridge id)kCGImageSourceThumbnailMaxPixelSize : [NSNumber numberWithFloat:maxPixelSize]};
    CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(sourceRef, 0, (__bridge CFDictionaryRef)options);
    UIImage *resultImage = [UIImage imageWithCGImage:imageRef scale:scale orientation:orientation];
    CGImageRelease(imageRef);
    CFRelease(sourceRef);
    return resultImage; 
}

對(duì)于圖片緩存的操作合愈,可以在頁面pop或dimiss的時(shí)候叮贩,及時(shí)清空?qǐng)D片的緩存。另外佛析,對(duì)于下發(fā)或者使用的圖片益老,盡量避免圖片的縮放。如果是大圖片也可以減少圖片壓縮的空間寸莫。

參考文獻(xiàn):
https://juejin.cn/post/6844903902169710600
http://satanwoo.github.io/2017/10/18/abort/
https://blog.csdn.net/TuGeLe/article/details/104004692
http://www.reibang.com/p/7a8fafa1ba34
https://opensource.apple.com/source/xnu/xnu-4570.41.2/bsd/sys/kern_memorystatus.h.auto.html
https://zhuanlan.zhihu.com/p/138755187
https://stackoverflow.com/questions/5887248/ios-app-maximum-memory-budget/15200855#15200855
https://wetest.qq.com/lab/view/367.html
https://github.com/Tencent/matrix
http://www.cocoachina.com/articles/485753
http://www.reibang.com/p/8187eddbe422
https://www.dazhuanlan.com/xayljq/topics/1667837
http://www.reibang.com/p/b3b2aa3722a4
https://blog.csdn.net/cdy15626036029/article/details/81014959

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末捺萌,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子膘茎,更是在濱河造成了極大的恐慌桃纯,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件披坏,死亡現(xiàn)場離奇詭異态坦,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)刮萌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門驮配,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人着茸,你說我怎么就攤上這事∷雠裕” “怎么了涮阔?”我有些...
    開封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長灰殴。 經(jīng)常有香客問我敬特,道長掰邢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任伟阔,我火速辦了婚禮辣之,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘皱炉。我一直安慰自己怀估,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開白布合搅。 她就那樣靜靜地躺著多搀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪灾部。 梳的紋絲不亂的頭發(fā)上康铭,一...
    開封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音赌髓,去河邊找鬼从藤。 笑死,一個(gè)胖子當(dāng)著我的面吹牛锁蠕,可吹牛的內(nèi)容都是我干的夷野。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼匿沛,長吁一口氣:“原來是場噩夢啊……” “哼扫责!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起逃呼,我...
    開封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤鳖孤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后抡笼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體苏揣,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年推姻,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了平匈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡藏古,死狀恐怖增炭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情拧晕,我是刑警寧澤隙姿,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站厂捞,受9級(jí)特大地震影響输玷,放射性物質(zhì)發(fā)生泄漏队丝。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一欲鹏、第九天 我趴在偏房一處隱蔽的房頂上張望机久。 院中可真熱鬧,春花似錦赔嚎、人聲如沸膘盖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽衔憨。三九已至,卻和暖如春袄膏,著一層夾襖步出監(jiān)牢的瞬間践图,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來泰國打工沉馆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留码党,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓斥黑,卻偏偏與公主長得像揖盘,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子锌奴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容