开发者社区 > 博文 > 深入解析iOS内存
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

深入解析iOS内存

  • 京麦研发团队
  • 2022-01-20
  • IP归属:未知
  • 42320浏览

    内存占用

    系统是按页分配内存的,每个page通常是16KB,页的又有:干净页、脏页两种类型,APP消耗的内存就是:Memory in use = Number of pages * Page size

    在iOS中将App占用的内存分为3种:Dirty、Compressed、Clean

    通常计算app内存占用:脏内存和压缩内存的合并不算进干净内存,计算公式为

    Memory footprint = Dirty memory + Compressed memory

    干净内存(Clean Memory): 指的是那些尚未被写入或被系统清出并重新加载的数据,举例来说:这些可能是内存映射文件、还有像是存储在磁盘上但已加载到内存的图片或者它们也可能是框架

    • app 的二进制可执行文件

    • framework 中的 _DATA_CONST 段

    • 文件映射的内存

    • 未写入数据的内存

    脏内存(dirty memory):包含你的程序写入内存,也包括了所有的堆分配内存像是你使用的malloc函数、已解密的图像缓冲以及框架;

    压缩内存(compressed memory):是指任何被内存压缩器进行过压缩的那些近期未被访问过的脏页,这些页面会在被访问时被解压缩,要注意在iOS系统里没有swap分区的概念它专属于macOS系统的;

    虚拟内存:虚拟意味着不是实际的,是app所需的内存但不一定要使用内存;Virtual Memory = Clean Memory + Dirty Memory

    常驻内存:真正加载到物理内存中的内存,它意味着所有脏内存和部分干净内存Resident Memory = Dirty Memory + Clean Memory that loaded in physical memory;


    如何获取准确的内存占用呢?

    咱们先看下Mach task 的内存使用信息存放在mach_task_basic_info结构体中,其中resident_size为应用使用的物理内存大小,virtual_size为虚拟内存大小,在task_info中:

    #define MACH_TASK_BASIC_INFO     20         /* always 64-bit basic info */
    struct mach_task_basic_info {
            mach_vm_size_t  virtual_size;       /* virtual memory size (bytes) */
            mach_vm_size_t  resident_size;      /* resident memory size (bytes) */
            mach_vm_size_t  resident_size_max;  /* maximum resident memory size (bytes) */
            time_value_t    user_time;          /* total user run time for
                                                   terminated threads */
            time_value_t    system_time;        /* total system run time for
                                                   terminated threads */
            policy_t        policy;             /* default policy for new threads */
            integer_t       suspend_count;      /* suspend count for task */
    };

    获取方式通过task_info API根据指定的flaver类型,返回target_task的信息,在task.h中:

    kern_return_t task_info
    (
        task_name_t target_task,
        task_flavor_t flavor,
        task_info_t task_info_out,
        mach_msg_type_number_t *task_info_outCnt
    );

    分析了主流的开源库发现使用的都是resident_size但是发现它的值和Xcode检测出来的数值差值很大,深挖了下phy_foorprint得到的数值是最接近Xcode的数值的具体实现如下:

    + (NSInteger)useMemoryForApp{
        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)
        {
            int64_t memoryUsageInByte = (int64_t) vmInfo.phys_footprint;
            return (NSInteger)(memoryUsageInByte/1024/1024);
        }
        else
        {
            return -1;
        }
    }

    那么什么phy_foorprint获取的比较接近呢,通过分析XNU源码 可以找到答案,在 osfmk/kern/task.c 文件里有对于phy_foorprint 的详细注释如下:

    /*
     * phys_footprint
     *   Physical footprint: This is the sum of:
     *     + (internal - alternate_accounting)
     *     + (internal_compressed - alternate_accounting_compressed)
     *     + iokit_mapped
     *     + purgeable_nonvolatile
     *     + purgeable_nonvolatile_compressed
     *     + page_table
     *
     * internal
     *   The task's anonymous memory, which on iOS is always resident.
     *
     * internal_compressed
     *   Amount of this task's internal memory which is held by the compressor.
     *   Such memory is no longer actually resident for the task [i.e., resident in its pmap],
     *   and could be either decompressed back into memory, or paged out to storage, depending
     *   on our implementation.
     *
     * iokit_mapped
     *   IOKit mappings: The total size of all IOKit mappings in this task, regardless of
         clean/dirty or internal/external state].
     *
     * alternate_accounting
     *   The number of internal dirty pages which are part of IOKit mappings. By definition, these pages
     *   are counted in both internal *and* iokit_mapped, so we must subtract them from the total to avoid
     *   double counting.
     */

    OOM的处理

    iOS系统没有传统的磁盘交换系统,取而代之它使用内存压缩器,它是在iOS7中被引入的内存压缩器,接收未访问的内存页并压缩它们这实际上可以创建更多的空间,但是在访问时压缩器会对它们进行解压以便读取内存;

    使用3个页存储缓存字典

    一段时间不使用后系统会自动对其进行压缩到一页中

    当使用时会被解压,用于访问;

    当 App 收到内存警告时, 一般通过释放缓存腾出内存空间, 但是因为 Compressed 的原因, 可能会导致意想不到的结果.还是之前的例子:

    内存警告之前

    内存警告-解压后

    清理、释放后

    比如上面的情况, 使用 Dictionary 对数据进行了缓存, 当收到内存警告将 Dictionary 进行释放时, 因为访问了 Dictionary, 首先需要对 Dictionary 进行解压缩, 会多占用 2 个 clean 的内存空间. 将对象释放之后, Dictionary 变为 1 个空间大小. 对 Dictionary 解压的操作和释放内存的初衷是相违背的, 可能会给系统带来问题。所以, iOS 建议使用 NSCache代替 NSDictionary 作为缓存, NSCache 是 actually purgeable, 可以避免上面的错误


    高级的工具用于分析和研究App的内存占用情况

    Xcode内存测量计

    Instruments

    Allocations 分析由你的app所分配的堆

    Leaks 检查一个进程中的内存泄漏

    VM Tracker 为脏内存以及交互内存即iOS中的压缩内存分别提供了独立的追踪,并且告诉你关于常驻大小的信息。

    Memory debugger

    Virtual memory trace 虚拟内存跟踪

    memory trace 

    image

    图片是内存中的大对象,一般图片的大小决定内存,不是文件的大小

    如图片大小2048 px * 1536 px, 图片文件大小为590KB,放到内存中图片所占内存大小:2048 pixels x 1536 pixels x 4 bytes per pixel 约等于 10M

    图片为什么占用这么大的内存

    Load阶段,将文件加载进内存;Decode阶段,将图片解压缩;Render阶段,显示图片

    那么在实际开发中该怎么做呢

    停止使用UIGraphicsBeginImageContextWithOptions , 每个像素4个字节,开始使用UIGraphicsImageRenderer , iOS 10引入, iOS 12中自动为你选择最好的图像格式。使用UIGraphicsImageRenderer的方式节约75%的内存。

    我们不应该用UIImage进行缩小,如果我们用UIImage绘图,由于内部坐标空间变换,这种方法性能并不高,它也会解压缩内存中的整个图像。

    取而代之,我们可以使用ImageIO框架,使用streaming API,只会生成结果尺寸大小的图像,能节省内存峰值。

    参考资料:WWDC2018 iOS Memory Deep Divehttps://github.com/apple/darwin-xnu