从 Objective-C 和 Swift 看字典的性能优化(2)

2021-05-14 10:39:59 浏览数 (1)

NSMutableDictionary 的创建流程

本小节以下面的代码为例介绍 NSMutableDictionary 的创建过程

代码语言:javascript复制
NSMutableDictionary *mutableDic = [NSMutableDictionary dictionary];

通过下面的指令,我们可以发现 NSMutableDictionary 类并不存在类方法 [NSMutableDictionary dictionary]

代码语言:javascript复制
(lldb) b  [NSMutableDictionary dictionary]
Breakpoint 11: no locations (pending).
WARNING:  Unable to resolve breakpoint to any actual locations.

所以,我们需要通过条件断点的方式添加合适的断点

代码语言:javascript复制
(lldb) bmessage  [NSMutableDictionary dictionary]
Setting a breakpoint at  [NSDictionary dictionary] with condition (void*)object_getClass((id)$x0) == 0x00000001fbbbae68
Breakpoint 19: where = CoreFoundation` [NSDictionary dictionary], address = 0x00000001a2404ec0

添加断点后,我们会发现 [NSMutableDictionary dictionary] 会跳转到下面的汇编执行初始化任务:

代码语言:javascript复制
CoreFoundation` [NSDictionary dictionary]:
    0x1a2404ec0 < 0>:  stp    x29, x30, [sp, #-0x10]!
    0x1a2404ec4 < 4>:  mov    x29, sp

    # objc_alloc
    0x1a2404ec8 < 8>:  bl     0x1a2585f14               ; symbol stub for: -[NSMutableArray replaceObject:].cold.1
    
    # initWithObjects:forKeys:count:
    0x1a2404ecc < 12>: adrp   x8, 308195
    0x1a2404ed0 < 16>: add    x1, x8, #0xd8             ; =0xd8 
    0x1a2404ed4 < 20>: mov    x2, #0x0
    0x1a2404ed8 < 24>: mov    x3, #0x0
    0x1a2404edc < 28>: mov    x4, #0x0
    # 发送消息
    0x1a2404ee0 < 32>: bl     0x1a2153e28
    0x1a2404ee4 < 36>: ldp    x29, x30, [sp], #0x10
    
    # objc_autorelease
    0x1a2404ee8 < 40>: b      0x1a2585f44               ; symbol stub for: -[NSMutableArray replaceObjectsAtIndexes:withObjects:].cold.1

该步骤与 __NSDictionaryI 类型,同样会依次执行以下三个任务:

  1. 通过 objc_alloc 申请一块区域,并初始化 isa 等信息
  2. 通过 -[NSDictionary initWithObjects:forKeys:count:] 实例化
  3. 通过 objc_autorelease 将实例放到自动释放池()

指令复用

值得注意的是,因为 CoreFoundation 动态库存在很多对 objc_alloc 函数的调用。所以,很多可以复用的汇编指令片段会被提取到单个函数中。

以对 objc_alloc 的调用为例,汇编指令都被会提取并放到一个单独的函数:-[NSMutableArray replaceObject:].cold.1

受 arm64 固定指令长度影响,调用函数需要3个指令

可复用指令

代码语言:javascript复制
CoreFoundation`-[NSMutableArray replaceObject:].cold.1:
    0x1a2585f14 < 0>: adrp   x16, 82705
    0x1a2585f18 < 4>: add    x16, x16, #0x28           ; =0x28 
->  0x1a2585f1c < 8>: br     x16

image

NSDate 创建时,同样存在对 -[NSMutableArray replaceObject:].cold.1 的调用

objc_alloc

__NSPlaceholderDictionary 的创建过程类似,可变字典同样会通过 objc_alloc 进行层层转发,并跳转到 [NSDictionary allocWithZone:] 进行下一步处理。

[NSDictionary allocWithZone:]

[NSDictionary allocWithZone:] 会先判断 self 的类型

image

检测到 NSMutableDictionary 类型后,会调到 160 行后进行安全检测,并调用 __NSDictionaryMutablePlaceholder 进行下一步处理

安全检测相关知识可以搜索关键字 clang stack_chk_guard stackprotector

image

__NSDictionaryMutablePlaceholder

__NSPlaceholderDictionary 的创建过程类似,__NSDictionaryMutablePlaceholder 同样会将 __NSPlaceholderDictionary 的实例赋值给 $x0 寄存器并返回

注意下面的地址 0x1f3d07178,本文还会再次讲到

image

-[__NSPlaceholderDictionary initWithObjects:forKeys:count:]

-[__NSPlaceholderDictionary initWithObjects:forKeys:count:] 会根据情况对入参进行一系列的判断校验,比如 keysNULL 并且 count 大于 0 时,会抛出异常

入参校验可以参考下面汇编代码的注释:

代码语言:javascript复制
CoreFoundation`-[__NSPlaceholderDictionary initWithObjects:forKeys:count:]:
    0x1a24097b8 < 0>:   stp    x29, x30, [sp, #-0x10]!
    0x1a24097bc < 4>:   mov    x29, sp

    # keys 非 NULL,则跳转到  16
    0x1a24097c0 < 8>:   cbnz   x3, 0x1a24097c8           ; < 16>
    
    # keys 是 NULL,count 大于 0,抛出异常
    0x1a24097c4 < 12>:  cbnz   x4, 0x1a24098a8           ; < 240>
    
    # 校验 count 数量是否过大,如果过大抛出异常
    0x1a24097c8 < 16>:  lsr    x8, x4, #61
    0x1a24097cc < 20>:  cbnz   x8, 0x1a24098b0           ; < 248>
    
    # count 等于 0,调整到  52 处理
    0x1a24097d0 < 24>:  cbz    x4, 0x1a24097ec           ; < 52>
      
      ## 对 keys 进行非空校验
      
      # 开始计数,寄存器 x8 = 0
      0x1a24097d4 < 28>:  mov    x8, #0x0
      # 读取当前 key
      0x1a24097d8 < 32>:  ldr    x9, [x3, x8, lsl #3]
      # 如果某个 key 是 nil, 则抛出异常
      0x1a24097dc < 36>:  cbz    x9, 0x1a2409898           ; < 224>
      # 计数自增1
      0x1a24097e0 < 40>:  add    x8, x8, #0x1              ; =0x1 
      # 判断是否等于 count
      0x1a24097e4 < 44>:  cmp    x4, x8
      # 如果不等于 count,则调到  32 进行判断,直到遍历结束
      0x1a24097e8 < 48>:  b.ne   0x1a24097d8               ; < 32>
    
    # values 不等于 NULL,则调到  60 处理
    0x1a24097ec < 52>:  cbnz   x2, 0x1a24097f4           ; < 60>
    # values 等于 NULL, count 不等于 0,则调到  256 抛出异常
    0x1a24097f0 < 56>:  cbnz   x4, 0x1a24098b8           ; < 256>
    
    # values 等于 NULL,count 等于 0,调到  88 进行处理
    0x1a24097f4 < 60>:  cbz    x4, 0x1a2409810           ; < 88>
    
      ## 对 values 进行非空校验
      0x1a24097f8 < 64>:  mov    x8, #0x0
      0x1a24097fc < 68>:  ldr    x9, [x2, x8, lsl #3]
      0x1a2409800 < 72>:  cbz    x9, 0x1a24098a0           ; < 232>
      0x1a2409804 < 76>:  add    x8, x8, #0x1              ; =0x1 
      0x1a2409808 < 80>:  cmp    x4, x8
      0x1a240980c < 84>:  b.ne   0x1a24097fc               ; < 68>

    # 获取 ___immutablePlaceholderDictionary
->  0x1a2409810 < 88>:  adrp   x8, 334078
    0x1a2409814 < 92>:  add    x8, x8, #0x168            ; =0x168 
    # self 等于 ___immutablePlaceholderDictionary 时,调整到  144 进行创建任务
    0x1a2409818 < 96>:  cmp    x0, x8
    0x1a240981c < 100>: b.eq   0x1a2409848               ; < 144>
    
    # 获取 ___mutablePlaceholderDictionary
    0x1a2409820 < 104>: adrp   x8, 334078
    0x1a2409824 < 108>: add    x8, x8, #0x178            ; =0x178 
    
    # self 等于 ___immutablePlaceholderDictionary 时,跳转到  264 中断
    0x1a2409828 < 112>: cmp    x0, x8
    0x1a240982c < 116>: b.ne   0x1a24098c0               ; < 264>
    
    ## 可变字典创建任务
    0x1a2409830 < 120>: mov    x0, x3
    0x1a2409834 < 124>: mov    x1, x2
    0x1a2409838 < 128>: mov    x2, x4
    0x1a240983c < 132>: mov    w3, #0x3
    0x1a2409840 < 136>: ldp    x29, x30, [sp], #0x10
    0x1a2409844 < 140>: b      0x1a256c344               ; __NSDictionaryM_new
    
    ## 不可变字典创建任务
    0x1a2409848 < 144>: cmp    x4, #0x1                  ; =0x1 
    0x1a240984c < 148>: b.eq   0x1a2409868               ; < 176>
    
    0x1a2409850 < 152>: cbnz   x4, 0x1a240987c           ; < 196>
    0x1a2409854 < 156>: adrp   x8, 334061
    0x1a2409858 < 160>: add    x8, x8, #0xdd0            ; =0xdd0 
    0x1a240985c < 164>: ldr    x0, [x8]
    0x1a2409860 < 168>: ldp    x29, x30, [sp], #0x10
    0x1a2409864 < 172>: b      0x1a2153e58

    
    0x1a2409868 < 176>: ldr    x0, [x3]
    0x1a240986c < 180>: ldr    x1, [x2]
    0x1a2409870 < 184>: mov    w2, #0x1
    0x1a2409874 < 188>: ldp    x29, x30, [sp], #0x10
    0x1a2409878 < 192>: b      0x1a2462a90               ; __NSSingleEntryDictionaryI_new
    
    0x1a240987c < 196>: mov    x0, x3
    0x1a2409880 < 200>: mov    x1, x2
    0x1a2409884 < 204>: mov    x2, #0x0
    0x1a2409888 < 208>: mov    x3, x4
    0x1a240988c < 212>: mov    w4, #0x1
    0x1a2409890 < 216>: ldp    x29, x30, [sp], #0x10
    0x1a2409894 < 220>: b      0x1a2450e0c               ; __NSDictionaryI_new
    
    0x1a2409898 < 224>: mov    x0, x8
    0x1a240989c < 228>: bl     0x1a25841f4               ; -[__NSPlaceholderDictionary initWithObjects:forKeys:count:].cold.5
    0x1a24098a0 < 232>: mov    x0, x8
    0x1a24098a4 < 236>: bl     0x1a25841c4               ; -[__NSPlaceholderDictionary initWithObjects:forKeys:count:].cold.4
    0x1a24098a8 < 240>: mov    x0, x4
    0x1a24098ac < 244>: bl     0x1a2584134               ; -[__NSPlaceholderDictionary initWithObjects:forKeys:count:].cold.1
    
    # '*** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: count (0) of objects array is ridiculous'
    0x1a24098b0 < 248>: mov    x0, x4
    0x1a24098b4 < 252>: bl     0x1a2584164               ; -[__NSPlaceholderDictionary initWithObjects:forKeys:count:].cold.2

    0x1a24098b8 < 256>: mov    x0, x4
    0x1a24098bc < 260>: bl     0x1a2584194               ; -[__NSPlaceholderDictionary initWithObjects:forKeys:count:].cold.3
    0x1a24098c0 < 264>: brk    #0x1

前面提到过地址 0x00000001d1c67178。在实际运行中, -[__NSPlaceholderDictionary initWithObjects:forKeys:count:] 会通过 self 的地址判断是否需要生成可变类型的实例

image

如果需要生成可变类型,会将 keys、objects、count、常量3 当做参数传给 __NSDictionaryM_new 函数

image

__NSDictionaryM_new

与不可变字典的处理类似,__NSDictionaryM_new 同样将 count 与 常量数组 __NSDictionaryCapacities的值进行判断,并创建合适的实例

首先,我们先看看根据汇编反推到的字典内部结构

代码语言:javascript复制
struct KKDic_t {
    __strong Class isa;
    struct {
        const id *buffer;
        union {
            struct {
                unsigned long mutations;
            };
            struct {
                unsigned int muts;
                unsigned int other;
            };
            struct {
                unsigned mutbits : 31;
                unsigned copyKeys : 1;
                unsigned used : 25;
                unsigned kvo : 1;
                unsigned szidx : 6;
            };
        } state;
    } storage;
    struct __cow_state_t *cow;
};
  1. used 代表已经使用的空间大小,与开发者常用的 count 属性对应
  2. mutbits 代表对字典变更的次数。初始化时是1,增删会加1
  3. szidx 通过搭配 常量数组 __NSDictionarySizes ,获取字典的容量
  4. copyeKeys: 代表需要复制 key
代码语言:javascript复制
CoreFoundation`__NSDictionaryM_new:
    第0个参数是 keys
    第1个参数是 keys
    第2个参数是 keys
    第3个参数是 一个 NS_OPTIONS:可变字典Option 

    根据汇编反推出的含义:
    typedef NS_OPTIONS(NSUInteger, 可变字典Option) {
        NSEnumeration初始化复制key = (1UL << 0), // 可以查看函数指令  520  556 处的逻辑
        NSEnumeration其它情况复制key = (1UL << 1),  
        NSEnumeration屏蔽对Value进行Retain = (1UL << 2),  
    };

    0x1a256c344 < 0>:   sub    sp, sp, #0xb0             ; =0xb0 
    0x1a256c348 < 4>:   stp    x28, x27, [sp, #0x50]
    0x1a256c34c < 8>:   stp    x26, x25, [sp, #0x60]
    0x1a256c350 < 12>:  stp    x24, x23, [sp, #0x70]
    0x1a256c354 < 16>:  stp    x22, x21, [sp, #0x80]
    0x1a256c358 < 20>:  stp    x20, x19, [sp, #0x90]
    0x1a256c35c < 24>:  stp    x29, x30, [sp, #0xa0]
    0x1a256c360 < 28>:  add    x29, sp, #0xa0            ; =0xa0 
    
    # 临时存储 keys 和 常量3 到栈上
    0x1a256c364 < 32>:  stp    x0, x3, [sp, #0x38]
 
    # x20 = x2,代表 count
    0x1a256c368 < 36>:  mov    x20, x2
    
    # values 放到 [sp, #0x30]
    0x1a256c36c < 40>:  str    x1, [sp, #0x30]

    ##### 一、下面的代码是计算 szidx 和字典的容量
    为了降低冲突,容量通常会大于 count;比如 count 等于 6,容量是 7
    
    1. 准备参数
    # x19 初始化为 0
    0x1a256c370 < 44>:  mov    x19, #0x0
    
    # x8代表 szidx * 8,初始化时是 0
    0x1a256c374 < 48>:  mov    x8, #0x0
    
    # 后续用来更新 szidx 
    0x1a256c378 < 52>:  mov    x9, #0x400000000000000
    
    # x10 代表 __NSDictionaryCapacities
    0x1a256c37c < 56>:  adrp   x10, 464
    0x1a256c380 < 60>:  add    x10, x10, #0x900          ; =0x900 
    (lldb) image lookup -a $x10
    Address: CoreFoundation[0x000000018069c900] (CoreFoundation.__TEXT.__const   1749472)
    Summary: CoreFoundation`__NSDictionaryCapacities
    
    2. 通过循环方式查找 szidx 对应的值
    终止条件是 count 过大导致异常,或者  __NSDictionaryCapacities[szidx]>=count
    # x11 代表 __NSDictionaryCapacities[szidx]
    0x1a256c384 < 64>:  ldr    x11, [x10, x8]
    # 和 count 进行比较
    0x1a256c388 < 68>:  cmp    x11, x20
    #大于等于 count 时终止遍历,转到  96
    0x1a256c38c < 72>:  b.hs   0x1a256c3a4               ; < 96>
    # 指向下一个值,x8  = 0x8
    0x1a256c390 < 76>:  add    x8, x8, #0x8              ; =0x8 
    
    # x19 实际上就是 0x400000000000000 * szidx
    0x1a256c394 < 80>:  add    x19, x19, x9
    后面会将循环次数存到结构体的 szidx 位置
            struct {
                unsigned mutbits : 31;
                unsigned copyKeys : 1;
                unsigned used : 25;
                unsigned kvo : 1;
                unsigned szidx : 6;
            };
    
    
    # 安全校验:移量是否等于 0x140
    0x1a256c398 < 84>:  cmp    x8, #0x140                ; =0x140 
    # 不等于才能转到  64 继续遍历
    0x1a256c39c < 88>:  b.ne   0x1a256c384               ; < 64>
    # 等于  0x140 时触发中断,代表出现了字典无法处理的情况
    0x1a256c3a0 < 92>:  brk    #0x1


    3. 从 常量数组 取出桶的容量
    x22 保存 从 __NSDictionarySizes[szidx] 取出的字典容量
    0x1a256c3a4 < 96>:  adrp   x9, 464
    0x1a256c3a8 < 100>: add    x9, x9, #0xa40            ; =0xa40 
    0x1a256c3ac < 104>: ldr    x22, [x9, x8]    

    (lldb) image lookup -a $x9
        Address: CoreFoundation[0x000000018069ca40] (CoreFoundation.__TEXT.__const   1749792)
        Summary: CoreFoundation`__NSDictionarySizes
    
    ##### 二、创建空间

    # 取出类 __NSDictionaryM
    0x1a256c3b0 < 108>: adrp   x0, 366160
    0x1a256c3b4 < 112>: add    x0, x0, #0x268            ; =0x268 
    (lldb) image lookup -a $x0
      Address: CoreFoundation[0x00000001d9b1c268] (CoreFoundation.__DATA_DIRTY.__objc_data   8680)
      Summary: (void *)0x00000001fbbbc290: __NSDictionaryM

    # 调用 objc_opt_self,等价于 [__NSDictionaryM self]
    0x1a256c3b8 < 116>: bl     0x1a25860a0               ; symbol stub for: -[NSSet intersectsOrderedSet:].cold.1
    
    # 调用 __CFAllocateObject 创建对象
    0x1a256c3bc < 120>: mov    x1, #0x0
    0x1a256c3c0 < 124>: bl     0x1a251b1d4               ; __CFAllocateObject
    
    ##### 三、更新 storage 

    1、动态读取 实例变量 __NSDictionaryM.storage 的偏移量
    0x1a256c3c4 < 128>: adrp   x10, 367773
    0x1a256c3c8 < 132>: ldrsw  x8, [x10, #0xdb8]
      Address: CoreFoundation[0x00000001da169db8] (CoreFoundation.__DATA.__objc_ivar   1192)
      Summary: CoreFoundation`__NSDictionaryM.storage
    
    # x8 指向 storage 位置
    0x1a256c3cc < 136>: add    x8, x0, x8
    
    2、初始化状态,等价于  dic->storage.state.muts = 1;
    0x1a256c3d0 < 140>: mov    w9, #0x1
    0x1a256c3d4 < 144>: str    w9, [x8, #0x8]
    
    3、更新 copyKeys
    # 再次动态读取 实例变量 __NSDictionaryM.storage 的偏移量
    0x1a256c3d8 < 148>: ldrsw  x8, [x10, #0xdb8]
    
    # 将 dic 临时存储到栈上
    0x1a256c3dc < 152>: str    x0, [sp, #0x10]
    
    # x21 指向 storage 位置
    0x1a256c3e0 < 156>: add    x21, x0, x8
    
    # x8 就是之前刚存入的 state
    0x1a256c3e4 < 160>: ldr    x8, [x21, #0x8]
    
    # 取出函数开头部分保存的 可变字典Option
    0x1a256c3e8 < 164>: ldr    x9, [sp, #0x40]
    # 可变字典Option<<30
    0x1a256c3ec < 168>: lsl    w9, w9, #30
    # x9 = (可变字典Option<<30)&0x80000000 = 0x80000000
    0x1a256c3f0 < 172>: and    x9, x9, #0x80000000
    通过和 x9 进行 orr 操作
    等价于根据 **可变字典Option & NSEnumeration其它情况复制key ** 决定是否将结构体的 copyKeys 置为 1
    struct {
                unsigned mutbits : 31;
                unsigned copyKeys : 1;
                unsigned used : 25;
                unsigned kvo : 1;
                unsigned szidx : 6;
            };
    
    
    # x10 = 0xffffffff7fffffff    
    0x1a256c3f4 < 176>: mov    x10, #-0x80000001
    # 0x03ffffff7fffffff
    0x1a256c3f8 < 180>: movk   x10, #0x3ff, lsl #48
     前面两个指令等价于下面这个复杂的逻辑
    (lldb) p/x ((long)0-0x80000001)&(((long)0x3ff<<48)|(((long)1<<48)-1))
    (long) $57 = 0x 03ff ffff 7fff ffff
 
     # mutbits 与 x10 进行 and 计算
    0x1a256c3fc < 184>: and    x8, x8, x10
    前面的几个指令相当于对结构体的 copyKeys 和 szidx 做了置0操作。
            struct {
                unsigned mutbits : 31;
                unsigned copyKeys : 1;
                unsigned used : 25;
                unsigned kvo : 1;
                unsigned szidx : 6;
            };
    

    # 或计算 
    0x1a256c400 < 188>: orr    x9, x19, x9
    x9 等价于下面的计算,其中 3是 __NSDictionaryM_new 函数接收的一个参数
    
    (lldb) p/x ((long)3<<30)&0x80000000|(0x0400000000000000*循环次数)
    高位的 0x04000000 代表循环1次,0x08000000 代表循环2次,依次类推
    
    低位的 0x80000000,代表是否进行 copyKeys,传入的 __NSDictionaryM_new 函数接收的**可变字典Option & NSEnumeration其它情况复制key ** 决定是否置为 0x80000000
    (long) $12 = 0x 0400 0000 8000 0000
        
            struct {
                unsigned mutbits : 31;
                unsigned copyKeys : 1;
                unsigned used : 25;
                unsigned kvo : 1;
                unsigned szidx : 6;
            };
    

    # x8 保存最后的新结构体 
    0x1a256c404 < 192>: orr    x8, x9, x8
    
    # 存储到 dic->storage.state
    0x1a256c408 < 196>: str    x8, [x21, #0x8]
    
    #### 空字典执行逻辑 ####
    下面的箭头代表跳转
    0.  152 将 dic 临时存储到栈上
    1.  200 判断大小是否等于0
    2.  228 dic->storage.buffer 置为 NULL
    3.  664 cow 置为 NULL

    # x20 是字典的大小,如果等于0,跳转到  228 
    0x1a256c40c < 200>: cbz    x20, 0x1a256c428          ; < 228>
    
    # x22 代表字典容量
    0x1a256c410 < 204>: ubfiz  x1, x22, #4, #32
    
    # ubfiz 指令与下面的代码等价,代表最多有个 UINT32_MAX 个键值对 
    NSLog(@"%x", INT32_MAX); // 7fffffff
    NSLog(@"%x", UINT32_MAX); // ffffffff
    (lldb) p/x  (0xabcdef1234567890&UINT32_MAX)<<4
    (unsigned long) $12 = 0x0000000345678900
    # 左移 4 位 的原因是:键值对需要2个指针保存 key 和 value,64位系统需要 8*2 的空间
    
    # 调用 calloc 申请一块合适的内存,内存大小是 1 * (size << 4)
    0x1a256c414 < 208>: mov    w0, #0x1
    0x1a256c418 < 212>: bl     0x1a2585770               ; symbol stub for: -[NSArray indexesOfObjectsWithOptions:passingTest:].cold.1

    # 更新字典的buffer *(dic->storage.buffer) 
    0x1a256c41c < 216>: mov    x24, x0
    0x1a256c420 < 220>: str    x0, [x21]
    0x1a256c424 < 224>: b      0x1a256c430               ; < 236>
    
    # 字典大小等于0时,将 x24 置为0,并将 dic->storage.buffer 置为 NULL
    0x1a256c428 < 228>: mov    x24, #0x0
    0x1a256c42c < 232>: str    xzr, [x21]
    
    # 函数入口处曾经将 keys 和 values 指针临时保存到 [sp, #0x38],count 保存到 x20
    # 下面的代码会依次校验 keys values size 是否为0,如果是 0,则跳转到  664 处理
    0x1a256c430 < 236>: ldr    x8, [sp, #0x38]
    0x1a256c434 < 240>: cbz    x8, 0x1a256c5dc           ; < 664>
    0x1a256c438 < 244>: ldr    x8, [sp, #0x30]
    0x1a256c43c < 248>: cbz    x8, 0x1a256c5dc           ; < 664>
    0x1a256c440 < 252>: cbz    x20, 0x1a256c5dc          ; < 664>
    
    # 后面会遍历 keys,x23 代表第几个
    0x1a256c444 < 256>: mov    x23, #0x0

    # x22 是容量,x26 后面用于将 key 打散的分母
    0x1a256c448 < 260>: and    x26, x22, #0xffffffff
    # buffer 的前 n 个是 key,后 n 个是 value,所以这里通过 buffer 容量*8 ,计算出 value 存储的起始区域
    0x1a256c44c < 264>: add    x8, x24, w22, uxtw #3
    # values 起始区域备用
    0x1a256c450 < 268>: str    x8, [sp, #0x48]

    # 获取 一个名为 hash 的 Selector 并存到  [sp, #0x20] 备用
    0x1a256c454 < 272>: adrp   x8, 309224
    0x1a256c458 < 276>: add    x8, x8, #0x6ba            ; =0x6ba 
    将 hash 和 count 存储到 [sp, #0x20] 和 [sp, #0x28]
    0x1a256c45c < 280>: stp    x8, x20, [sp, #0x20]

      (lldb) image lookup -a $x8
            Address: libobjc.A.dylib[0x00000001cbcb46ba] (libobjc.A.dylib.__OBJC_RO   9668282)
            Summary: 
      (lldb) po (SEL)$x8
      "hash"

    # 获取一个名为 copyWithZone: 的 Selector,并存到 [sp, #0x8] 备用
    0x1a256c460 < 284>: adrp   x8, 308270
    0x1a256c464 < 288>: add    x8, x8, #0xbba            ; =0xbba 
    0x1a256c468 < 292>: str    x8, [sp, #0x8]
    (lldb) image lookup -va ((($pc>>12) 308270)<<12) 0xbba
      Address: libobjc.A.dylib[0x00000001cb8fabba] (libobjc.A.dylib.__OBJC_RO   5761978)
      Summary: 
      (lldb) image lookup -a $x8
      Address: libobjc.A.dylib[0x00000001cb8fabba] (libobjc.A.dylib.__OBJC_RO   5761978)
      (lldb) po (SEL)$x8
      "copyWithZone:"

    
    # 将字典取出来; 152 曾经将 dic 存到 [sp, #0x10]
    0x1a256c46c < 296>: ldr    x8, [sp, #0x10]

    # 将字典的 storage 地址存储到 [sp, #0x18]
    0x1a256c470 < 300>: add    x8, x8, #0x8              ; =0x8 
    0x1a256c474 < 304>: str    x8, [sp, #0x18]

    # x25 保存 ___NSDictionaryM_DeletedMarker
    0x1a256c478 < 308>: adrp   x25, 333723
    0x1a256c47c < 312>: add    x25, x25, #0x1a8          ; =0x1a8 
    (lldb) image lookup -a ((($pc>>12) 333723)<<12) 0x1a8
      Address: CoreFoundation[0x00000001d1c671a8] (CoreFoundation.__DATA_CONST.__const   1939560)
      Summary: CoreFoundation`___NSDictionaryM_DeletedMarker

            
            # 获取 keys;  32 指令将 keys 存到了 [sp, #0x38]
            0x1a256c480 < 316>: ldr    x8, [sp, #0x38]
            
            # 开始取出 key
            0x1a256c484 < 320>: ldr    x27, [x8, x23, lsl #3]
            0x1a256c488 < 324>: mov    x0, x27
            # 开始取出 "hash"
            0x1a256c48c < 328>: ldr    x1, [sp, #0x20]
            # 发送消息    
            0x1a256c490 < 332>: bl     0x1a2153e28
            
            # 字典容量等于 0 时,直接调到  448 继续执行
            0x1a256c494 < 336>: mov    x20, x22
            0x1a256c498 < 340>: cbz    w22, 0x1a256c504          ; < 448>

    # 下面两个指令相当于 hash%字典容量,代表即将插入的桶相对起始位置的偏移量。以下简称“桶偏移量1”
    ; x8  = x0/x26
    ; x21 = x0-x8*x26
    ; x21 = x0-(x0/x26)*x26
    ; x21 = x0%x26
    0x1a256c49c < 344>: udiv   x8, x0, x26
    0x1a256c4a0 < 348>: msub   x21, x8, x26, x0

    # 获取一个名为 isEqual: 的 Selector
    0x1a256c4a4 < 352>: adrp   x8, 310043
    0x1a256c4a8 < 356>: add    x28, x8, #0x3a            ; =0x3a
    (lldb) p/x ((($pc>>12) 310043)<<12) 0x3a
    (unsigned long) $18 = 0x00000001ee08703a
    (lldb) po (SEL)$18
    "isEqual:"

    # x22 等于桶的可用容量,后面 key 的 hash冲突后会 1
    0x1a256c4ac < 360>: mov    x22, x26
    0x1a256c4b0 < 364>: mov    x19, x26

        # x24 指向 buffer,x21 是“桶偏移量1”。冲突后,x21 就会向后顺移一位,直到找到合适的位置
        # 所以, x0 指向桶的位置存储的内容
        0x1a256c4b4 < 368>: ldr    x0, [x24, x21, lsl #3]

        # 判断“桶偏移量1”位置是否有旧值,没有旧值,直接调到  464 处理
        0x1a256c4b8 < 372>: cbz    x0, 0x1a256c514           ; < 464>

    # 此处开始处理  “桶偏移量1”存在旧值(hash 冲突)的场景

    # 判断是否被标为 ___NSDictionaryM_DeletedMarker ,相同直接调到  412 处理
    0x1a256c4bc < 376>: cmp    x0, x25
    0x1a256c4c0 < 380>: b.eq   0x1a256c4e0               ; < 412>

        # 判断旧的位置和即将存入的 key 地址是否相同,如果相同,进入  456 的逻辑(key 被放到“桶偏移量1”)
        0x1a256c4c4 < 384>: cmp    x0, x27
        0x1a256c4c8 < 388>: b.eq   0x1a256c50c               ; < 456>

    # 退化逻辑,通过 isEqual: 判断两个 key 是否相同
    0x1a256c4cc < 392>: mov    x1, x28
    0x1a256c4d0 < 396>: mov    x2, x27
    # 发送消息
    0x1a256c4d4 < 400>: bl     0x1a2153e28
    
    # 如果不等,则说明 key 真的冲突了,需要进入 key 冲突处理逻辑(计算新的“桶偏移量2”,并放入 key)
    0x1a256c4d8 < 404>: tbz    w0, #0x0, 0x1a256c4e8     ; < 420>
    # 如果相同,进入  456 的逻辑(key 被放到“桶偏移量1”)
    0x1a256c4dc < 408>: b      0x1a256c50c               ; < 456>

    # 如果 x19 仍然等于 字典容量: x26,则 x19=x21,否则保持原样
    0x1a256c4e0 < 412>: cmp    x19, x26
    0x1a256c4e4 < 416>: csel   x19, x21, x19, eq

    loc_1804cc4e8:
    # key hash 冲突处理;
    # 1. 将“桶偏移量1” 1
    0x1a256c4e8 < 420>: add    x8, x21, #0x1             ; =0x1 
    # 2. 相加后是否超过 桶的数量
    0x1a256c4ec < 424>: cmp    x8, x26
    # 3. 如果小于,则x9=0,否则等于 相加后的值
    0x1a256c4f0 < 428>: csel   x9, xzr, x26, lo
    # 4. 再将 x8-x9;
    0x1a256c4f4 < 432>: sub    x21, x8, x9
    上面的指令合并后如下:
    if {(x21 1)<x26) {
        x21 = x8-x9 = x8-0 = x21 1-0 = x21 1;
    } else {
        x21 = x8 -x9 = x8 - x26 = x21 1-x26;
    }
    简单来说就是“桶偏移量2” = (“桶偏移量1” 1) % 桶的数量

    # x22 等于桶的可用容量,减一后,再从   368 开始处理,会判断 “桶偏移量2” 位置是否存在冲突
    0x1a256c4f8 < 436>: subs   x22, x22, #0x1            ; =0x1 
    0x1a256c4fc < 440>: b.ne   0x1a256c4b4               ; < 368>

    0x1a256c500 < 444>: b      0x1a256c51c               ; < 472>
    0x1a256c504 < 448>: mov    x19, #0x0
    0x1a256c508 < 452>: b      0x1a256c51c               ; < 472>

    x19=x21
    0x1a256c50c < 456>: mov    x19, x21
    0x1a256c510 < 460>: b      0x1a256c51c               ; < 472>

        # 如果 x19 仍然等于 x26,则 x19 等于 x21,否则保持原样
        0x1a256c514 < 464>: cmp    x19, x26
        0x1a256c518 < 468>: csel   x19, x21, x19, eq

    # 又一次取出 “桶偏移量“对应的桶存储的内容,如果不存在内容,则转到  512 处理
    0x1a256c51c < 472>: ldr    x8, [x24, x19, lsl #3]
    0x1a256c520 < 476>: cbz    x8, 0x1a256c544           ; < 512>

    # 取出 buffer 对应 values 存储区域的起始位置
    0x1a256c524 < 480>: ldr    x8, [sp, #0x48]
    # 取出当前 key 对应的 value 
    0x1a256c528 < 484>: ldr    x0, [x8, x19, lsl #3]
    0x1a256c52c < 488>: cmp    x0, #0x1                  ; =0x1 
    x22 等于 桶的数量
    0x1a256c530 < 492>: mov    x22, x20
    x20 是 count
    0x1a256c534 < 496>: ldr    x20, [sp, #0x28]
    lt 代表小于0或者无序,如果 value 的值小于1,则调到  616 执行对 value 的处理
    0x1a256c538 < 500>: b.lt   0x1a256c5ac               ; < 616>
    # 否则,调用 objc_release 释放旧的值
    0x1a256c53c < 504>: bl     0x1a2153e4c
    然后再跳转到  616 执行对 value 的处理
    0x1a256c540 < 508>: b      0x1a256c5ac               ; < 616>

    # 找到了合适的位置,开始存储 key

    # [sp, #0x38] 是 keys
    0x1a256c544 < 512>: ldr    x8, [sp, #0x38]
    # x23 代表当前在处理的 key 索引,x0 等于 key
    0x1a256c548 < 516>: ldr    x0, [x8, x23, lsl #3]

    # [sp, #0x40] 存储了函数接收的一个控制参数,
    0x1a256c54c < 520>: ldr    x8, [sp, #0x40]
    如果参数末位非0,则跳转到 556 处理
    0x1a256c550 < 524>: tbnz   w8, #0x0, 0x1a256c570     ; < 556>

    否则,将 key 的直接直接存入对应位置
    0x1a256c554 < 528>: str    x0, [x24, x19, lsl #3]

    key 和 1 比较
    0x1a256c558 < 532>: cmp    x0, #0x1                  ; =0x1 

    x22 等于 桶的数量
    x20 是 count
    0x1a256c55c < 536>: mov    x22, x20
    0x1a256c560 < 540>: ldr    x20, [sp, #0x28]

    如果是空,再跳转
    0x1a256c564 < 544>: b.lt   0x1a256c588               ; < 580>
    对 key 进行 retain 操作
    0x1a256c568 < 548>: bl     0x1a2153e58
    0x1a256c56c < 552>: b      0x1a256c588               ; < 580>
    
        # 获取前面保存的 SEL: copyWithZone: 
        0x1a256c570 < 556>: ldr    x1, [sp, #0x8]
        0x1a256c574 < 560>: mov    x2, #0x0
        # 发送消息,目的是将 key 进行复制
        0x1a256c578 < 564>: bl     0x1a2153e28

        # 将复制的 key 存到对应的桶;x24 指向 buffer,加上偏移量 x19 后就可以执行真正的桶
        0x1a256c57c < 568>: str    x0, [x24, x19, lsl #3]

    ; x22 等于 桶的数量
    ; x20 是 count

    0x1a256c580 < 572>: mov    x22, x20
    0x1a256c584 < 576>: ldr    x20, [sp, #0x28]
    
    # 动态读取 实例变量 __NSDictionaryM.storage 的偏移量
    0x1a256c588 < 580>: adrp   x8, 367773
    0x1a256c58c < 584>: ldrsw  x8, [x8, #0xdb8]
      Address: CoreFoundation[0x00000001da169db8] (CoreFoundation.__DATA.__objc_ivar   1192)
      Summary: CoreFoundation`__NSDictionaryM.storage
    
    
    取出 dic->storage 
    0x1a256c590 < 588>: ldr    x11, [sp, #0x18]

    取出 dic->storage.state
    0x1a256c594 < 592>: ldr    x9, [x11, x8]

        下面的几个指令等价于 dic->storage.state.used  = 1;

        used 宽度是 24位,右侧有32位(mutbits 和 copyKeys)
        先对 used 1 操作
        0x1a256c598 < 596>: mov    x10, #0x100000000
        0x1a256c59c < 600>: add    x10, x9, x10

        逻辑右移,移除 (mutbits 和 copyKeys)
        0x1a256c5a0 < 604>: lsr    x10, x10, #32

        位域操作,将 x10 持有的 24 位的 used 更新 x9
        0x1a256c5a4 < 608>: bfi    x9, x10, #32, #25

        将新的 state 放回 dic->storage.state
        0x1a256c5a8 < 612>: str    x9, [x11, x8]


    ; x22 等于 桶的数量
    ; x20 是 count

    # 取出 values
    0x1a256c5ac < 616>: ldr    x8, [sp, #0x30]
    # 取出与 key 配对的 value
    0x1a256c5b0 < 620>: ldr    x0, [x8, x23, lsl #3]

    # 再次取出 buffer 对应的 values 
    0x1a256c5b4 < 624>: ldr    x8, [sp, #0x48]
    # 把 value 存进去
    0x1a256c5b8 < 628>: str    x0, [x8, x19, lsl #3]

    # 取出参数**可变字典Option**
    0x1a256c5bc < 632>: ldr    x8, [sp, #0x40]
    看看是否有 **NSEnumeration屏蔽对Value进行Retain**,如果有,则跳过对 value 的 objc_retain 操作
    0x1a256c5c0 < 636>: tbnz   w8, #0x2, 0x1a256c5d0     ; < 652>
        0x1a256c5c4 < 640>: cmp    x0, #0x1                  ; =0x1 
        0x1a256c5c8 < 644>: b.lt   0x1a256c5d0               ; < 652>

        对 value 进行 objc_retain 操作
        0x1a256c5cc < 648>: bl     0x1a2153e58

    # x23 代表第几个 key,先  1
    0x1a256c5d0 < 652>: add    x23, x23, #0x1            ; =0x1 
    后与 x20 count 比较
    0x1a256c5d4 < 656>: cmp    x23, x20
    不等则继续遍历
    0x1a256c5d8 < 660>: b.ne   0x1a256c480               ; < 316>

    # 动态获取 cow 的偏移量
    0x1a256c5dc < 664>: adrp   x8, 367773
    0x1a256c5e0 < 668>: ldrsw  x8, [x8, #0xdbc]
    (lldb) image lookup -a ((($pc>>12) 367773)<<12) 0xdbc
      Address: CoreFoundation[0x00000001da169dbc] (CoreFoundation.__DATA.__objc_ivar   1196)
      Summary: CoreFoundation`__NSDictionaryM.cow

    # 取出字典; 152 曾经将 dic 临时存储到栈上
    0x1a256c5e4 < 672>: ldr    x0, [sp, #0x10]

    # 等价于 dic->cow = NULL
    0x1a256c5e8 < 676>: add    x8, x0, x8
    0x1a256c5ec < 680>: stlr   xzr, [x8]
    
    恢复栈
    0x1a256c5f0 < 684>: ldp    x29, x30, [sp, #0xa0]
    0x1a256c5f4 < 688>: ldp    x20, x19, [sp, #0x90]
    0x1a256c5f8 < 692>: ldp    x22, x21, [sp, #0x80]
    0x1a256c5fc < 696>: ldp    x24, x23, [sp, #0x70]
    0x1a256c600 < 700>: ldp    x26, x25, [sp, #0x60]
    0x1a256c604 < 704>: ldp    x28, x27, [sp, #0x50]
    0x1a256c608 < 708>: add    sp, sp, #0xb0             ; =0xb0 
    0x1a256c60c < 712>: ret    

注意:-[NSSet intersectsOrderedSet:].cold.1 等价于调用 objc_opt_self

image

objc_autorelease

objc_autorelease 内部的逻辑比较简单:

  1. 如果 nil,直接返回
  2. 如果是 TaggedPointer,直接返回
  3. 如果类有自定义的 retain 或者 release 方法,则通过调用 [objc autorelease]
  4. 判断 x30 寄存器地址指向的指令是否等于 0xaa1d03fd arm64 中,fd 03 1d aa 等价于 mov fp, fp
  5. 如果等于,则将1存储到 tls
  6. 如果不等,则转发到 objc_object::rootAutorelease2() 进行下一步处理
代码语言:javascript复制
libobjc.A.dylib`objc_autorelease:
    0x1b6894d20 < 0>:  cmp    x0, #0x1                  ; =0x1 
    0x1b6894d24 < 4>:  b.lt   0x1b6894d5c               ; < 60>
    0x1b6894d28 < 8>:  ldr    x8, [x0]
    0x1b6894d2c < 12>: and    x8, x8, #0xffffffff8
    0x1b6894d30 < 16>: ldrb   w8, [x8, #0x20]
    0x1b6894d34 < 20>: tbz    w8, #0x2, 0x1b6894d64     ; < 68>
    0x1b6894d38 < 24>: ldr    w8, [x30]
    0x1b6894d3c < 28>: mov    w9, #0x3fd
    0x1b6894d40 < 32>: movk   w9, #0xaa1d, lsl #16
    0x1b6894d44 < 36>: cmp    w8, w9
    0x1b6894d48 < 40>: b.ne   0x1b6894d60               ; < 64>
    0x1b6894d4c < 44>: mrs    x8, TPIDRRO_EL0
    0x1b6894d50 < 48>: and    x8, x8, #0xfffffffffffffff8
    0x1b6894d54 < 52>: mov    w9, #0x1
    0x1b6894d58 < 56>: str    x9, [x8, #0x160]
    0x1b6894d5c < 60>: ret    
    0x1b6894d60 < 64>: b      0x1b6893a50               ; objc_object::rootAutorelease2()
    0x1b6894d64 < 68>: adrp   x8, 226945
    0x1b6894d68 < 72>: add    x1, x8, #0xaba            ; =0xaba 
    0x1b6894d6c < 76>: b      0x1b6873460               ; objc_msgSend

总结

本文分享了很多通过 lldb 指令分析汇编和内存的小技巧,并对可变字典创建过程进行了逐步的分析,为下一篇分析 cow 机制打下了基础

0 人点赞