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
https://www.jianshu.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
https://www.jianshu.com/p/8187eddbe422
https://www.dazhuanlan.com/xayljq/topics/1667837
https://www.jianshu.com/p/b3b2aa3722a4
https://blog.csdn.net/cdy15626036029/article/details/81014959

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容