美文网首页
iOS 内存相关的基础知识

iOS 内存相关的基础知识

作者: yyggzc521 | 来源:发表于2021-09-18 16:32 被阅读0次

    为什么不能直接访问物理内存?

    1. 内存不够用。
    2. 内存数据不安全。

    内存管理方案

    相关知识点

    • taggedPointer: 专门用来处理小对象,例如NSNumber、NSDate、小NSString等
    • Nonpointer_isa: 非指针类型的isa
    • SideTables:散列表,包括引用计数列表和弱引用表
    1. TaggedPointer
      TaggedPointer 是一种高效节省内存空间的方法,是一个特别的指针,它分为两部分:

    一部分直接保存数据 ;
    另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址
    先看看原有的对象为什么会浪费内存?

    假设存储一个 NSNumber 对象,值是一个整数。
    正常情况下,如果这个整数只是一个 NSInteger 的普通变量,那么它所占用的内存是与 CPU 的位数有关,在 32 位 CPU 下占 4 个字节,在 64 位 CPU 下是占 8 个字节的。
    而指针类型的大小也与 CPU 位数相关, 32 位下为 4 个字节,64 位下是 8 个字节。

    为了改进上面提到的内存占用和效率问题,苹果提出了Tagged Pointer对象。
    由于 NSNumber、NSDate 一类的变量本身的值需要占用的内存大小常常不需要 8 个字节


    NSNumber之Tagged Pointer
    • Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate
    • Tagged Pointer指针的值不再是地址了,而是真正的值。实际上它不再是一个对象了,只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free。
    • 在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。

    EX

    • NSCFConstantString:字符串常量,是一种编译时常量,retainCount值很大,对其操作,不会引起引用计数变化,存储在字符串常量区
    • NSCFString:是在运行时创建的NSString子类,创建后引用计数会加1,存储在堆上
    • NSTaggedPointerString:标签指针,是苹果在64位环境下对NSString、NSNumber等对象做的优化。

    对于NSString对象来说,当字符串是由数字、英文字母组合且长度小于10时,会自动成为NSTaggedPointerString类型,存储在常量区。
    当有中文或者其他特殊符号时,会直接成为__NSCFString类型,存储在堆区

    对于NSString来说,当字符串较小时,建议直接通过@""初始化,因为存储在常量区,可以直接进行读取。会比WithFormat初始化方式更加快速

    NSNumber 存储的数据不大时,NSNumber *指针是伪指针Tagged Pointer;
    NSNumber存储的数据很大时,NSNumber * 指针一般指针,指向NSNumber 实例的地址

    1. NONPOINTER_ISA

    nonpointer:表示是否对 isa 指针开启指针优化
    0:纯isa指针,1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等
    extra_rc:表示该对象的引⽤计数值,如对象的引⽤计数为10,那么extra_rc为9。如果引⽤计数⼤于10,则需要使用到has_sidetable_rc

    1. 散列表(sideTables)
      参考
      在 runtime 中,有四个数据结构非常重要,分别是 SideTables,SideTable,weak_table_t和weak_entry_t。它们和对象的引用计数,以及 weak引用 相关

    四个数据结构的关系?

    在 runtime 内存空间中,SideTables是一个8个元素长度 的hash数组,里面存储了 SideTable。SideTables 的 hash键值 就是一个 对象obj的 address。
    因此可以说,一个obj对应了一个 SideTable。但是一个 SideTable会对应多个 obj。因为 SideTable 的数量只有64个,所以会有很多 obj 共用同一个 SideTable(如果是真机环境下最大就是 8 张表)
    SideTables 是多张表的形式,就是考虑到性能问题,当所有对象都共用一张表的话,因为要考虑到多线程的问题,当对引用计数操作的时候就会对表的加锁和关锁,会比较消耗性能,当使用多张表的时候,系统可以根据一定的算法,对不使用的表进行内存回收,而不是持续占用空间。但是也不能每个对象开一张表,因为开表的内存太大了,对象很多的话就会有很多的内存开辟与回收,也会很消耗性能。所以表的数量要在一个合理的范围内。

    SideTable

    而在一个 SideTable 中,又有两个成员,分别是

    RefcountMap refcnts;        // 对象引用计数相关 map
    weak_table_t weak_table;    // 对象弱引用相关 table
    
    • 其中,refcents 是一个 hash map,其key是obj的地址,而value,则是obj对象的引用计数。
    • 而 weak_table 则存储了 弱引用obj 的指针的地址,其本质是一个以 obj 地址为 key,弱引用obj 的指针的地址作为 value 的 hash表。hash表 的节点类型是 weak_entry_t

    一个NSObject对象占用多少字节?

    系统分配了16个字节给NSObject对象(通过malloc_size函数获得),但是NSObject对象内部只使用了8个字节空间

    有内存对齐的原因,结构体的大小必须是最大成员大小(16)的倍数

    截屏2021-09-16 下午11.46.28.png

    内存阈值

    Apple 并没有准确的文档说明每个设备的内存限制。对于设备的内存 OOM 阈值大概有以下几个方法获取。这里获取的限制最好是在重启 iPhone 以后,使得设备清空 RAM 缓存。

    1. 方法一: Jetsam 日志

    Jetsam 机制可以理解为操作系统为控制内存资源过度使用而采用的一种管理机制。Jetsam是一个独立运行的进程,每个进程都有一个内存阈值,一旦超过这个阈值,Jetsam将立即杀死该进程。

    当我们的应用被 Jetsam 机制杀死时,手机会生成系统日志。在手机系统设置隐私分析中,找到以 JetSamEvent. 的开头的系统日志。在这些日志中,你可以获取一些关于应用程序的内存信息。可以在日志的开头,看到了pageSize,并找到了 perprocesslimit 项(不是所有日志都有,但是可以找到它)

    从 Jetsam 日志中通过使用项目的 rpages * pageSize 可以得到 OOM 的阈值。

    {"bug_type":"298","timestamp":"2020-10-15 17:29:58.79
     +0100","os_version":"iPhone OS 14.2
     (18B5061e)","incident_id":"B04A36B1-19EC-4895-B203-6AE21BE52B02"
    }
    {
      "crashReporterKey" :
     "d3e622273dd1296e8599964c99f70e07d25c8ddc",
      "kernel" : "Darwin Kernel Version 20.1.0: Mon Sep 21 00:09:01
     PDT 2020; root:xnu-7195.40.113.0.2~22\/RELEASE_ARM64_T8030",
      "product" : "iPhone12,1",
      "incident" : "B04A36B1-19EC-4895-B203-6AE21BE52B02",
      "date" : "2020-10-15 17:29:58.79 +0100",
      "build" : "iPhone OS 14.2 (18B5061e)",
      "timeDelta" : 7,
      "memoryStatus" : {
      "compressorSize" : 96635,
      "compressions" : 3009015,
      "decompressions" : 2533158,
      "zoneMapCap" : 1472872448,
      "largestZone" : "APFS_4K_OBJS",
      "largestZoneSize" : 41271296,
      "pageSize" : 16384,
      "uncompressed" : 257255,
      "zoneMapSize" : 193200128,
      "memoryPages" : {
        "active" : 45459,
        "throttled" : 0,
        "fileBacked" : 34023,
        "wired" : 49236,
        "anonymous" : 55900,
        "purgeable" : 12,
        "inactive" : 40671,
        "free" : 5142,
        "speculative" : 3793
      }
    },
      "largestProcess" : "AppStore",
      "genCounter" : 1,
      "processes" : [
      {
        "uuid" : "7607487f-d2b1-3251-a2a6-562c8c4be18c",
        "states" : [
          "daemon",
          "idle"
        ],
        "age" : 3724485992920,
        "purgeable" : 0,
        "fds" : 25,
        "coalition" : 68,
        "rpages" : 229,
        "priority" : 0,
        "physicalPages" : {
          "internal" : [
            6,
            183
          ]
        },
        "pid" : 350,
        "cpuTime" : 0.066796999999999995,
        "name" : "SBRendererService",
        "lifetimeMax" : 976
      },
    .
    .
    {
      "uuid" : "f71f1e2b-a7ca-332d-bf87-42193c153ef8",
      "states" : [
        "daemon",
        "idle"
      ],
      "lifetimeMax" : 385,
      "killDelta" : 13595,
      "age" : 94337735133,
      "purgeable" : 0,
      "fds" : 50,
      "genCount" : 0,
      "coalition" : 320,
      "rpages" : 382,
      "priority" : 1,
      "reason" : "highwater",
      "physicalPages" : {
        "internal" : [
          327,
          41
        ]
      },
      "pid" : 2527,
      "idleDelta" : 41601646,
      "name" : "wifianalyticsd",
      "cpuTime" : 0.634077
    },
    .
    .
    
    
    1. 方法二: 网上资料

    ios app maximum memory budget

    大致就是60%

    Split 工具

    1. 方法三: 主动触发 didReceiveMemoryWarning

    当内存不够用时,iOS 会发出内存警告,告知进程去清理自己的内存, 在当前页面(Controller)中,这个方法是 - (void)didReceiveMemoryWarning。可以通过不停地增加内存,来获取当前设备的 OOM 阈值。

    RAM 和 ROM

    • RAM(random access memory)随机存储内存,这种存储器在断电时将丢失其存储内容,故主要用于存储短时间使用的程序
    • ROM(Read-Only Memory)只读内存,是一种只能读出事先所存数据的固态半导体存储器

    App 启动时,系统会将 App 程序从ROM 中拷贝到内存(RAM),然后在RAM 里面执行代码

    内存分页

    虚拟内存以page(页)为单位进行管理(每页的容量,32位机器大小为 4 KB 而 64 位机器大小为 16 KB)

    内存页也有分类,一般来说分为 Clean Memory 、 Dirty Memory 和 Compressed Memory

    • Clean Memory
      例如,image.jpg,Frameworks

    • Dirty Memory
      所有堆分配对象(例如 malloc,Array,NSCache,UIViews,String 和 图像解码缓冲区,例如CGRasterData,ImageIO 和 Frameworks)都会是 Dirty Memory

    • Compressed Memory
      内存压缩主要执行两个操作

    1. 压缩未访问的页面
    2. 访问时解压缩页面

    压缩内存能够在内存紧张时将最近使用的内存使用率压缩到原始大小的一半以下,并在需要时可以解压缩和重新使用。它不仅节省了内存,而且提高了系统的响应速度。

    例如,当我们使用 NSDictionary 来缓存数据时,假设现在我们已经使用了 3 页内存,当我们不访问它时,它可能被压缩为 1 页,而当我们再次使用它时,它将被解压缩为 3 页

    内存分区

    • 代码区:函数体的二进制代码
    • 常量区:常量字符串,const常量
    • 全局/静态区:全局变量和静态变量、静态全局变量
    • 堆(heap):存OC对象,地址越来越淡,一般由程序员分配释放
    • 栈(stack):函数的参数值,局部变量的值、对象指针地址等

    栈区和堆区的比较

    • 分配方式不同
      栈是自动分配和释放,堆是由程序员来分配和释放

    • 申请大小的限制
      栈区:容量大小一般是 2M,比较小
      堆区:不连续的,空间比较大

    • 申请效率的比较
      栈:系统自动分配,速度较快,但是不受程序员控制。
      堆:由 alloc 分配的内存,速度较慢,并且容易产生内存碎片。

    ARC

    ARC 背后的原理是依赖编译器的静态分析能力,通过在编译时找出合理的插入引用计数管理代码,从而彻底解放程序员。

    自动释放池

    自动释放池就是一个双向列表

    参考资料:
    从 OOM 到 iOS 内存管理

    相关文章

      网友评论

          本文标题:iOS 内存相关的基础知识

          本文链接:https://www.haomeiwen.com/subject/dtaqgltx.html