类结构Cache分析

1,080 阅读2分钟

001-cache数据结构

翻看2020WWDC,我们能看到类里面有个Method cache

image.png

翻看objc源码,可以看到类的结构也可以看到cache_t cache,接下来探究一下

struct objc_class : objc_object {
    …
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    …
}

因为是类结构里面的cache,所以先拿到类对象

image.png

打印类地址

(lldb) p/x pClass
(Class) $0 = 0x00000001000084b8

平移16个单位(加0x10)

(lldb) p (cache_t *)0x00000001000084c8
(cache_t *) $1 = 0x00000001000084c8

打印$1

p *$1
(cache_t) $2 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4298576216
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 0
        }
      }
      _flags = 32808
      _occupied = 0
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0000802800000000
      }
    }
  }
}

再看源码cache_t结构

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;
#if __LP64__
            uint16_t                   _flags;
#endif
            uint16_t                   _occupied;
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };
...
}

完全一致,再查看源码

    ...
public:
    // The following four fields are public for objcdt's use only.
    //下面四个字段是公共的,仅供objcdt使用。
    // objcdt reaches into fields while the process is suspended
    //当进程挂起时,objcdt进入字段
    // hence doesn't care for locks and pesky little details like this
    //因此不关心锁和像这样烦人的小细节
    // and can safely use these.
    //可以安全地使用这些。
    unsigned capacity() const;
    struct bucket_t *buckets() const;
    Class cls() const;

#if CONFIG_USE_PREOPT_CACHES
    const preopt_cache_t *preopt_cache() const;
#endif
    mask_t occupied() const;
    ...

可以看到有一个bucket_t类型的结构体指针buckets,查看bucket_t

...
struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif
...

就是impsel,可以得出如下结构

classDiagram
Class --> cache_t
cache_t --> bucket_t
Class :  Class ISA
Class : Class superclass
Class:  cache_t cache
Class:  class_data_bits_t bits

class cache_t{
buckets
}

class bucket_t{
imp
sel
}

接下来进行LLDB调试,验证一下

002-cache底层LLDB分析

测试代码如下 image.png 先拿到类对象

(lldb) p/x pClass
(Class) $0 = 0x00000001000084b8

平移16个字节,拿到cache地址

(lldb) p (cache_t *)0x00000001000084c8
(cache_t *) $1 = 0x00000001000084c8

打印$1

(lldb) p *$1
(cache_t) $2 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4298576216
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 0
        }
      }
      _flags = 32808
      _occupied = 0
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0000802800000000
      }
    }
  }
}

获取对象的buckets

(lldb) p $2.buckets()
(bucket_t *) $3 = 0x0000000100371158
(lldb) p *$3
(bucket_t) $4 = {
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = nil
    }
  }
}

返回nil,想想应该是没有缓存,方法列表如下:

@interface ApplePerson : NSObject{
    NSString *salary;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *workNumber;

- (void)sayHenXi;
+ (void)sayNiuBi;
@end

lldb调用sayHenXi

(lldb) p [p sayHenXi]
2021-10-11 16:42:38.792443+0800 ObjcBuild[40603:4400942] -[ApplePerson sayHenXi]

再次跑之前的调试代码

(lldb) p/x pClass
(Class) $0 = 0x00000001000084b8
(lldb) p (cache_t *)0x00000001000084c8
(cache_t *) $1 = 0x00000001000084c8
(lldb) p *$1
(cache_t) $2 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 844429232542976
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 0
        }
      }
      _flags = 32808
      _occupied = 1
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0001802800000000
      }
    }
  }
}

拿到$2,是想要的cache_t类型,它有一个buckets的属性

public:
    …
    unsigned capacity() const;
    struct bucket_t *buckets() const;
    Class cls() const;
    …

获取buckets()

(lldb) p $2.buckets()
(bucket_t *) $3 = 0x0000000100933a60

得到bucket_t *类型的$3,查看buckets

image.png

buckets

指向方法数据结构的指针数组。这个数组不能包含超过掩码+ 1的项。注意,指针可以是NULL,
表示缓存桶未被占用,并且被占用的桶可能不是连续的。这个数组可能会随着时间的推移而增长。

既然是数组array,那挨个试,数组从0开始

(lldb) p $3[0]
(bucket_t) $4 = {
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = nil
    }
  }
}

获取sel方法

(lldb) p $4.sel()
(SEL) $5 = (null)

nil 那就从接着1

(lldb) p $3[1]
(bucket_t) $6 = {
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 45576
    }
  }
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
}

再获取sel方法

(lldb) p $6.sel()
(SEL) $7 = "sayHenXi"

最终拿到了sayHenXi的方法,再获取imp,先看源码

inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
    …
}

传入两个值,一个bucket_t类型指针,先传个nil,第二个是类对象

(lldb) p $6.imp(nil,pClass)
(IMP) $8 = 0x00000001000036b0 (ObjcBuild`-[ApplePerson sayHenXi])

获取到的IMP类型的对象[ApplePerson sayHenXi]就是ApplePerson类的函数指针