iOS OOM處理

OOM是什么?

OOM的全稱是out of memory,字面意思也就是指內存超出了限制。在iOS中的OOM是由操作系統的Jetsam機制出發的crash的一種。由OOM導致的crash無法通過監控singal獲取異常信息,所以對于OOM的監控只能間接實現。

OOM和棧溢出有關系嗎?

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

OOM 產生的原因

說說Jetsam機制

Jetsam時iOS系統的單獨的進程,對于內存管理則是BSD層創建的優先級最高的常駐線程VM_memorystatus,可以管理系統的內存占用,當發現內存緊張時候會根據優先級殺掉其他應用程序進程。可以簡單理解為內存管理的的奔潰處理機制就是Jetsam機制。
macOS或者windows系統,當應用程序緊張的時候,可以通過SWAP內存交換機制實現把物理內存中的一部分內容交換到磁盤上去,利用磁盤空間擴展內存空間。對于移動設備來說一般沒有內存交換機制,原因在于移動設備的存儲介質也就是閃存,而閃存的性能和使用壽命是無法和電腦硬盤相比的,所以當內存緊張時,就會系統的Jetsam就會殺死應用程序。

Compressed memory

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

Jetsam機制殺死進程的順序

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

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

進程優先級的聲明,數值越大表明優先級越高,前臺進程JETSAM_PRIORITY_FOREGROUND(10)大于后臺進程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

關于didReceiveMemoryWarning方法

可以在UIViewController中實現didReceiveMemoryWarning方法,也可以在AppDelegate中實現applicationDidReceiveMemoryWarning:方法,也可以在注冊UIApplicationDidReceiveMemoryWarningNotification通知處理。

出現OOM前一定會調用didReceiveMemoryWarning么?

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

觸發didReceiveMemoryWarning之后一定會導致OOM嗎?

顯示也是不會的,因為當收到內存警告,如果之后內存下降了,也不會導致OOM

內存閥值的獲取

可以在即將到達內存閥值時,處理對象釋放嗎?

理論上可以這么處理的,例如我們知道了當前設備/系統的內存閥值,我們定義一個范圍,例如監控到當前內存為內存閥值的80%,此處通過開源框架KSCrash或者BSBacktraceLogger獲取,當然也可以自己實現,可以參考文章《iOS 如何抓取線程的“方法調用棧”?》。對于內存分析,僅僅實現對于堆棧的回溯是不夠的,我們更關心的是找出內存最大的對象,進行引用關系的分析,在Debug下可以參考FLEX庫的實現,通過malloc_get_all_zones可以獲取所有堆區的對象,通過objc_getClass獲取對應的對象名,通過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);
            }
        }
    }
首先我們要了解內存閥值的獲取

可以開辟子線程,循環申請1M的內存直到出現收到內存警告和獲取OOM的值為止。至于為什么是申請IM的內存,在apple的A7處理器之前,物理內存和虛擬內存都是按照4KB進行分頁的,但是A7之后虛擬內存是按照16KB分頁的,物理內存還是4KB分頁,如果申請的內存顆粒度小于虛擬內存頁的大小意義不大,另外考慮到統計的顆粒度,也可以是其他的數值。

//獲取內存當前占用,不需要使用taskInfo. resident_size獲取,因為resident_size不準確
- (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上有人對不同設備的OOM閥值進行了統計

如何監控內存分配

FBAllocationTracker

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

  • app版本號是否發生了改變
  • app是否發生了crash
  • 是否為用戶手動退出
  • 操作系統版本升級
  • 之前進程終止是否在后臺

此方案因為涉及NSObject的alloc/delloc進行全量的hock,所以對于性能會有一定的影響。且不一定繼承自NSObject的對象都會走alloc方法,例如:NSData創建對象的類靜態方法沒有調用+[NSObject alloc],里面實現是調用C方法NSAllocateObject來創建對象,也就是說這類方式創建的OC對象無法通過hook來獲取OC類名。因為Fundation框架沒有開源,但Core Foundation框架的源代碼,以及通過調用NSObject類進行內存管理部分的源代碼是公開的。但是Fundation部分可以通過GNU step的libobjc2獲取

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

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

2018年QQ開源了他們自己的監控組件OOMDetector。主要參考了系統的libmalloc庫中stack_logging_disk.c文件,可以通過malloc_logger實現對于malloc/free的監控,而__syscall_logger可以實現對于vm_allocate, vm_deallocate, mmap, munmap的監控。但是__syscall_logger是私有方法,所以在OOMDetector組件中使用中也提到了建議只是在Debug環境下使用。

// 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函數

#define USE_VM_LOGGER

#ifdef USE_VM_LOGGER //監控非malloc方式直接申請內存
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函數

#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年開源了內存和幀率CPU耗電等監控方案matrix,其中有涉及到OOM情況的監控。修改malloc_default_zone函數返回的malloc_zone_t結構體里的malloc、free等函數指針,也是可以監控堆內存分配,效果等同于malloc_logger;
apple源碼中的_malloc_zone_t結構如下

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函數調用
    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 函數調用
    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內存監控使用的是fishhock庫的rebind_symbols函數,具體的代碼邏輯如下

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

//開始處理dispatch相關的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);
}

為了更好的監控OOM,Matrix在Debug環境下也使用了私有API,通過下面宏的定義可以看出

#ifdef DEBUG
#define USE_PRIVATE_API
#endif

而正式開始監控OOM的方法為enable_memory_logging

int enable_memory_logging(const char *log_dir)
{
    err_code = MS_ERRC_SUCCESS;
    //允許使用私有API時候,通過"stack_logging_enable_logging"判斷當前環境是否有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;
    }
    //準備處理hock alloc
    if (!prepare_object_event_logger(log_dir)) {
        return err_code;
    }
    //監控系統的malloc_logger方法
    malloc_logger = __memory_event_callback;
    
#ifdef USE_PRIVATE_API
    // 如果在debug環境,則監控私有方法__syscall_logger
    syscall_logger = (malloc_logger_t **)dlsym(RTLD_DEFAULT, "__syscall_logger");
    if (syscall_logger != NULL) {
        *syscall_logger = __memory_event_callback;
    }
#endif
    //至此,表明可以正常監控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內部以一個數組維護
    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環境,也就是說可以使用私有API,則通過__CFObjectAllocSetLastAllocEventNameFunction和__CFOASafe實現對于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管控的內存,采用的是alloc和Debug下的私有方法__CFOASafe和__CFObjectAllocSetLastAllocEventNameFunction處理。

malloc_logger

對于前文提到的OOMDetector和Matrix開源庫中出現的對于malloc_logger回調函數的監控。malloc_logger是如何監控到應用層面的內存分配的呢?
在 libmalloc庫中的以下關于內存相關的方法 malloc_zone_malloc, malloc_zone_calloc, malloc_zone_valloc, malloc_zone_realloc, malloc_zone_free, malloc_zone_free_definite_size, malloc_zone_memalign等函數內部都會調用malloc_logger,所以我們只需要監控malloc_logger就可以實現對于內存的alloc與free的監控。

對于內存監控數據的存儲

sqlite

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

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

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

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

微信和QQ采用的都是二叉樹存儲。對于為什么不采用數據庫,因為在app啟動后,需要創建大量對象以及釋放大量對象,采用二叉樹結構比較合適,一般情況下二叉樹的時間復雜度為log(N),但是傳統二叉樹在操作后可能變為單鏈表情形,此時的時間復雜度就為O(N)了。

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


LabImage_c7bb49f11f4345daa138e1c797c6b9d2.png

數據上報

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


vtufqxef2i.jpeg

實際開發代碼層面如何避免

避免內存泄露

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

緩存盡量使用NSCache

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

合理使用自動釋放池

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

對于圖片的操作

對于iamge縮放的操作,可以用如下代碼實現

//常見的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;
}
//節約內存的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的時候,及時清空圖片的緩存。另外,對于下發或者使用的圖片,盡量避免圖片的縮放。如果是大圖片也可以減少圖片壓縮的空間。

參考文獻:
https://juejin.cn/post/6844903902169710600
http://satanwoo.github.io/2017/10/18/abort/
https://blog.csdn.net/TuGeLe/article/details/104004692
http://www.lxweimin.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.lxweimin.com/p/8187eddbe422
https://www.dazhuanlan.com/xayljq/topics/1667837
http://www.lxweimin.com/p/b3b2aa3722a4
https://blog.csdn.net/cdy15626036029/article/details/81014959

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

推薦閱讀更多精彩內容