iOS OOM處理

OOM是什么?

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

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

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

OOM 產(chǎn)生的原因

說說Jetsam機(jī)制

Jetsam時iOS系統(tǒng)的單獨的進(jìn)程,對于內(nèi)存管理則是BSD層創(chuàng)建的優(yōu)先級最高的常駐線程VM_memorystatus,可以管理系統(tǒng)的內(nèi)存占用,當(dāng)發(fā)現(xiàn)內(nèi)存緊張時候會根據(jù)優(yōu)先級殺掉其他應(yīng)用程序進(jìn)程??梢院唵卫斫鉃閮?nèi)存管理的的奔潰處理機(jī)制就是Jetsam機(jī)制。
macOS或者windows系統(tǒng),當(dāng)應(yīng)用程序緊張的時候,可以通過SWAP內(nèi)存交換機(jī)制實現(xiàn)把物理內(nèi)存中的一部分內(nèi)容交換到磁盤上去,利用磁盤空間擴(kuò)展內(nèi)存空間。對于移動設(shè)備來說一般沒有內(nèi)存交換機(jī)制,原因在于移動設(shè)備的存儲介質(zhì)也就是閃存,而閃存的性能和使用壽命是無法和電腦硬盤相比的,所以當(dāng)內(nèi)存緊張時,就會系統(tǒng)的Jetsam就會殺死應(yīng)用程序。

Compressed memory

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

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

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

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

進(jìn)程優(yōu)先級的聲明,數(shù)值越大表明優(yōu)先級越高,前臺進(jìn)程JETSAM_PRIORITY_FOREGROUND(10)大于后臺進(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中實現(xiàn)didReceiveMemoryWarning方法,也可以在AppDelegate中實現(xiàn)applicationDidReceiveMemoryWarning:方法,也可以在注冊UIApplicationDidReceiveMemoryWarningNotification通知處理。

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

答案當(dāng)然是不一定的。因為didReceiveMemoryWarning調(diào)用實在主線程的,如果瞬間申請了大塊內(nèi)存,而此時主線程正忙于其他的事情,此時會導(dǎo)致發(fā)生了OOM而無法獲取didReceiveMemoryWarning調(diào)用。

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

顯示也是不會的,因為當(dāng)收到內(nèi)存警告,如果之后內(nèi)存下降了,也不會導(dǎo)致OOM

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

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

理論上可以這么處理的,例如我們知道了當(dāng)前設(shè)備/系統(tǒng)的內(nèi)存閥值,我們定義一個范圍,例如監(jiān)控到當(dāng)前內(nèi)存為內(nèi)存閥值的80%,此處通過開源框架KSCrash或者BSBacktraceLogger獲取,當(dāng)然也可以自己實現(xiàn),可以參考文章《iOS 如何抓取線程的“方法調(diào)用?!保俊?/a>。對于內(nèi)存分析,僅僅實現(xiàn)對于堆棧的回溯是不夠的,我們更關(guān)心的是找出內(nèi)存最大的對象,進(jìn)行引用關(guān)系的分析,在Debug下可以參考FLEX庫的實現(xiàn),通過malloc_get_all_zones可以獲取所有堆區(qū)的對象,通過objc_getClass獲取對應(yīng)的對象名,通過class_getInstanceSize獲取單個對象的大小。

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)申請1M的內(nèi)存直到出現(xiàn)收到內(nèi)存警告和獲取OOM的值為止。至于為什么是申請IM的內(nèi)存,在apple的A7處理器之前,物理內(nèi)存和虛擬內(nèi)存都是按照4KB進(jìn)行分頁的,但是A7之后虛擬內(nèi)存是按照16KB分頁的,物理內(nèi)存還是4KB分頁,如果申請的內(nèi)存顆粒度小于虛擬內(nèi)存頁的大小意義不大,另外考慮到統(tǒng)計的顆粒度,也可以是其他的數(shù)值。

//獲取內(nèi)存當(dāng)前占用,不需要使用taskInfo. resident_size獲取,因為resident_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上有人對不同設(shè)備的OOM閥值進(jìn)行了統(tǒng)計

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

FBAllocationTracker

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

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

此方案因為涉及NSObject的alloc/delloc進(jìn)行全量的hock,所以對于性能會有一定的影響。且不一定繼承自NSObject的對象都會走alloc方法,例如:NSData創(chuàng)建對象的類靜態(tài)方法沒有調(diào)用+[NSObject alloc],里面實現(xiàn)是調(diào)用C方法NSAllocateObject來創(chuàng)建對象,也就是說這類方式創(chuàng)建的OC對象無法通過hook來獲取OC類名。因為Fundation框架沒有開源,但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實現(xiàn)對于malloc/free的監(jiān)控,而__syscall_logger可以實現(xiàn)對于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方式直接申請內(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中對于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中對于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時候,通過"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;
        //初始化二叉樹存儲保存對象的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類型,這個在matrix內(nèi)部以一個數(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實現(xiàn)對于NSData等對象的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
    //對于NSObjct的alloc方法的hock
    nsobject_hook_alloc_method();
    return true;
}

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

malloc_logger

對于前文提到的OOMDetector和Matrix開源庫中出現(xiàn)的對于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)部都會調(diào)用malloc_logger,所以我們只需要監(jiān)控malloc_logger就可以實現(xiàn)對于內(nèi)存的alloc與free的監(jiān)控。

對于內(nèi)存監(jiān)控數(shù)據(jù)的存儲

sqlite

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

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

二叉查找樹又稱二叉搜索樹,二叉排序樹,特點如下:

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

微信和QQ采用的都是二叉樹存儲。對于為什么不采用數(shù)據(jù)庫,因為在app啟動后,需要創(chuàng)建大量對象以及釋放大量對象,采用二叉樹結(jié)構(gòu)比較合適,一般情況下二叉樹的時間復(fù)雜度為log(N),但是傳統(tǒng)二叉樹在操作后可能變?yōu)閱捂湵砬樾?,此時的時間復(fù)雜度就為O(N)了。

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


LabImage_c7bb49f11f4345daa138e1c797c6b9d2.png

數(shù)據(jù)上報

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


vtufqxef2i.jpeg

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

避免內(nèi)存泄露

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

緩存盡量使用NSCache

因為NSCahe內(nèi)部apple幫我們實現(xiàn)了在內(nèi)存達(dá)到閥值時(非OOM觸發(fā)閥值),內(nèi)部會根據(jù)LRC算法主動處理對象的釋放。對于緩存部分盡量使用NSCache。

合理使用自動釋放池

通常autoreleased的對象在runloop結(jié)束時才釋放。如果在一些大型循環(huán)中,此時內(nèi)存會瞬間增長,而自動釋放池可以更及時的釋放對象。

對于圖片的操作

對于iamge縮放的操作,可以用如下代碼實現(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; 
}

對于圖片緩存的操作,可以在頁面pop或dimiss的時候,及時清空圖片的緩存。另外,對于下發(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.itdecent.cn/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.itdecent.cn/p/8187eddbe422
https://www.dazhuanlan.com/xayljq/topics/1667837
http://www.itdecent.cn/p/b3b2aa3722a4
https://blog.csdn.net/cdy15626036029/article/details/81014959

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文是< > 第十四篇學(xué)習(xí)筆記. OOM 的全稱是 Out-Of-Memory,是由于 iOS 的 Jetsam ...
    forping閱讀 704評論 0 0
  • 什么是 OOM? OOM 的全稱是 Out-Of-Memory,是由于 iOS 的 Jetsam 機(jī)制造成的一種“...
    Joy___閱讀 7,025評論 3 37
  • 內(nèi)存優(yōu)化是性能優(yōu)化的重頭戲,因此這部分也花了很多時間來梳理。老規(guī)矩,先上大綱: 一、基礎(chǔ)知識 1.1 Androi...
    Stan_Z閱讀 15,487評論 3 46
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月,有人笑有人哭,有人歡樂有人憂愁,有人驚喜有人失落,有的覺得收獲滿滿有...
    陌忘宇閱讀 8,896評論 28 54
  • 人工智能是什么?什么是人工智能?人工智能是未來發(fā)展的必然趨勢嗎?以后人工智能技術(shù)真的能達(dá)到電影里機(jī)器人的智能水平嗎...
    ZLLZ閱讀 4,115評論 0 5

友情鏈接更多精彩內(nèi)容