iOS 底层探索07——iOS方法缓存(中)

1,124 阅读10分钟

这是我参与更文挑战的第9天,活动详情查看: 更文挑战

前言

我们已经探究过方法缓存中的插入流程,今天我们继续再深入探究下方法缓存的细节及上层触发时机;

方法缓存关键细节

为什么扩容是在容量的 3/4 时进行?

  1. 3/4作为负载因子是大多数数据结构算法的共识,负载因子在0.75时空间的利用率是相对较大的;
  2. cache存入方法时是根据hash算法计算出来的值作为存储下标,缓存空间的剩余大小对下标是否冲突至关重要,当3/4作为负载因子发生hash冲突的几率相对较低;

_bucketsAndMaybeMask的作用

  1. _bucketsAndMaybeMask里面存放的是bucket内存的首地址bucketMask的地址; 官方解释:
// _bucketsAndMaybeMask is a buckets_t pointer;
// _maybeMask is the buckets mask
  1. 获取_bucketsAndMaybeMask之后通过内存平移即可获取下一个bucket的地址;
  2. buckets()方法实际上也是对_bucketsAndMaybeMask里面的值进行操作,获取的bucket_t; 代码如下
struct bucket_t *cache_t::buckets() const
{
    uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
    return (bucket_t *)(addr & bucketsMask);//_bucketsAndMaybeMask的地址和bucketsMask进行与运算;
}
static constexpr uintptr_t bucketsMask = ~0ul;//bucketsMask的所有位都为1;
  1. bucketMask定义在不同的架构下取值不同;
#if defined(__arm64__) && __LP64__//真机或__LP64__
    #if TARGET_OS_OSX || TARGET_OS_SIMULATOR//模拟器或Mac
    #define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS//高16位大地址
    #else
    #define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16//高16位
    #endif
#elif defined(__arm64__) && !__LP64__//真机或不等于__LP64__
    #define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4//低4位
#else
    #define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED //1
#endif

buckets取值

  1. buckets()方法实际上也是对_bucketsAndMaybeMask里面的值进行操作,获取的bucket_t;
  2. buckets()返回的数据是bucket_t类型的$n,但我们在取值的时候经常使用p $n[1]这种利用下标取值的方式,看起来bucket_t的取值很像数组的取值方式,实际上bucket()并不是数组
  3. buckets()返回的bucket_t只是一个结构体,但存放的bucket_t时候生成的空间是一块连续的内存空间,所以只要知道了当前bucket_t的位置,就可以通过内存平移获得下一个相邻的区域的内存;
  4. 使用p $n[1]p $n+1 是一样的效果;
  5. 数组也是使用内存平移的方式进行的取值,进行+1操作时,平移单位是根据数组内部的数据类型决定的;

hash函数(cache_hash)和二次hash函数(cache_next)

  1. 首次hash函数cache_hash
mask_t m = capacity - 1; // 容量-1:4-1=3
mask_t begin = cache_hash(sel, m);//根据容量-1和sel作为参数获取hash值
mask_t i = begin;

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    uintptr_t value = (uintptr_t)sel;//将sel转为无符号长整型
#if CONFIG_USE_PREOPT_CACHES
    value ^= value >> 7;
#endif
    return (mask_t)(value & mask);//用sel的转换值和mask进行按位与得到结果;
}
  1. 二次hash函数cache_next

cache_next(i, m)//再次哈希使用的是当前的位置和容量-1作为参数进行cache_next计算

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;//如果i不为0,返回i-1,否则返回mask(容量-1);也可以理解为判断发生冲突的位置是不是在buckets的最开头,如果不在最开头就直接前移,如果在最开头就直接跳到容量-1的位置,再依次向前,直到再次遇到一开始的begin位置,此时说明循环了一圈了还没找到空位置插,坏缓存了;
}

insert()的闭环流程

  1. 为了找到insert()的插入时机,我们在insert()方法中下断点,查看调用堆栈信息;
  2. 栈是一种LIFO的数据结构,index = 0 是最后调用的,从0开始依次查看调用信息,发现log_and_fill_cache方法调用了cache.insert方法;

005.jpg

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cls->cache.insert(sel, imp, receiver);
}
  1. 查看objc-cache.mm文件,可以根据注释看到insert()的插入时机最上层是通过objc_msgSend触发的,接下来我们就一起看下objc_msgSend

image.png

objc_msgSend

编译时

  1. 编译时 顾名思义就是正在编译的时候. 编译器源代码翻译成机器能识别的代码(当然只是⼀般意义上这么说,实际上可能只是翻译成某个中间状态的语⾔)。编译时通过语法分析词法分析等编译时类型检查(静态类型检查)来发现代码中的errorswarning等编译时的错误信息.
  2. 静态检查不会把代码放内存中运⾏起来,⽽只是当作⽂本来扫描检查,⼀些⼈说编译时还分配内存啥的肯定是错误的说法;

运行时

  1. 运⾏时就是代码通过dyld被装载到内存中执行的过程运⾏时类型检查就与前⾯讲的编译时类型检查(或者静态类型检查)不⼀样.不是简单的扫描代码.⽽是在内存中做操作和判断.
  2. Runtime有两个版本 ⼀个Legacy版本(早期版本) ,⼀个Modern版本(现⾏版本)
  • 早期版本对应的编程接⼝:Objective-C 1.0
  • 现⾏版本对应的编程接⼝:Objective-C 2.0
  • 早期版本⽤于Objective-C 1.0, 32位的Mac OS X的平台上
  • 现⾏版本:iPhone程序Mac OS X v10.5Mac OS X v10.5`以后的系统中的 64 位程序
  1. 关于OC语言的更多信息可以在苹果官方文档进行查阅
  1. Runtime调用方式一共有3种
  • OC方法:[p sayNB];
  • NSObject提供的API:isKindofClass...;
  • objc的下层APIobjc_msg_send方法:class_getInstanceSize...;
  1. Runtime3种调用方式对应的发起者信息如下

002.jpg

objc_msgSend

  1. 利用clangOC文件改写为c++文件,通过还原后的cpp文件可以查看到一部分runtime调用方式;
  2. OC的方法调用在runtime层面变成了消息发送objc_msgSend(id _Nullable self, SEL _Nonnull op, ...),方法调用包含两个元素:
  • 消息接受者
  • 消息主体:sel+参数
//重写前OC方法调用
GCPerson *person = [GCPerson alloc];
[person sayHello];

//重写后变成了objc_msgSend
static void _I_GCTeacher_sayHello(GCTeacher * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_fw_k53y7bbd2rx7sdzz_s85z7840000gn_T_main_cd505b_mi_0,__func__);
}

objc_msgSendSuper

  1. 查看改写后的cpp文件时我们发现除了objc_msgSend之外,还有一个objc_msgSendSuper方法;猜测是直接向父类发送消息;
  2. 查看objc源码中对objc_msgSendSuper的定义如下,
OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
  1. objc_msgSendSuper的参数objc_super定义如下
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;//消息接收者
    /* super_class is the first class to search */
    __unsafe_unretained _Nonnull Class super_class;//方法最先查找的class是super_class,如果super_class查找不到会查找super_class的父类
};
  1. 通过构建objc_super结构体偶为参数,我们可以主动调用objc_msgSendSuper方法
        struct objc_super gc_objc_super;
        gc_objc_super.receiver = person;消息接受者
        gc_objc_super.super_class = GCPerson.class;//方法最先查找的class是super_class,如果super_class查找不到会查找super_class的父类
        objc_msgSendSuper(&gc_objc_super,@selector(sayHello));

objc_msgSend汇编查看

由于OC语言的动态特性,基本上所有方法都是通过objc_msgSend进行处理的,这样一个高频调用的底层方法,从效率层面考虑最好是使用接近机器语言的汇编语言来实现,同时汇编语言基于地址也更加安全,所以系统也是用汇编进行实现的;下面一起来看下objc_msgSend在在libobjc库中的汇编层面的实现吧

  1. objc源码中搜索objc_msgSend

  2. 关闭所有文件,查看后缀为.s的汇编文件 003.jpg

  3. 找到objc_msgSend的汇编实现

006.jpg 4. 分析_objc_msgSend汇编部分源码

ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame

	cmp	p0, #0			// nil check and tagged pointer check// 比较p0和#0的差值
#if SUPPORT_TAGGED_POINTERS  //判断是否支持Taggedpointer类型
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)//如果支持Taggedpointer类型按照LNilOrTagged处理
#else
	b.eq	LReturnZero  // 返回nil如果不支持Taggedpointer类型返回nil
#endif
	ldr	p13, [x0]		// p13 = isa,[x0]存放的是class
    //src, needs_auth, auth_address
	GetClassFromIsa_p16 p13, 1, x0	//从 GetClassFromIsa_p16 中获取clsss:p16 = class
// receiver->class 获取class,去class 中找method cache
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
	END_ENTRY _objc_msgSend

bucket中IMP的获取

按位异或操作操作

  1. ^也叫做按位异或操作,如果 c = a ^ b;a = c ^ b;
  2. b叫做无实际作用,主要是为了生成异或结果而存在;

方法编码

  1. bucket_t中用一个无符号长整型的数值来存储IMPexplicit_atomic<uintptr_t> _imp;,而IMP本身是地址,所以存储时set方法进行了编码
void bucket_t::set(bucket_t *base, SEL newSel, IMP newImp, Class cls)
{
//这里对IMP和Class进行了编码
    uintptr_t newIMP = (impEncoding == Encoded
                        ? encodeImp(base, newImp, newSel, cls)
                        : (uintptr_t)newImp);
    if (atomicity == Atomic) {
        _imp.store(newIMP, memory_order_relaxed);
        if (_sel.load(memory_order_relaxed) != newSel) {
            mega_barrier();
            _sel.store(newSel, memory_order_relaxed);
        }
    } else {
        _imp.store(newIMP, memory_order_relaxed);
        _sel.store(newSel, memory_order_relaxed);
    }
}

//IMP编码的方法
    uintptr_t encodeImp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, IMP newImp, UNUSED_WITHOUT_PTRAUTH SEL newSel, Class cls) const {
        if (!newImp) return 0;
        return (uintptr_t)newImp ^ (uintptr_t)cls;//按位异或
    }
  1. IMP的获取方法IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)也需要进行解码,具体操作是用 当前取出的IMP数值class进行异或操作
return (IMP)(imp ^ (uintptr_t)cls);
  1. class是上述编码解码操作时用到的盐;

class中的delegate

  1. delegate 作为class中的一个成员,获取方法跟method property没有太大的区别,也是通过LLDB调试 来从class中获取的;
  2. 但是我们最终通过protocols()方法获取的protocol_array_tprotocol_array_t里面存储的是protocol_ref_tprotocol_ref_t是一个uintptr_t类型的,同时注释说明了他是未映射的:// protocol_t *, but unremapped,不能直接打印相关信息,而我们需要的是可以查看相关信息的 protocol_t类型;
  3. 基于以上信息我们重新搜索protocol_ref_t,最终发现用来映射protocol_ref_t的方法remapProtocol(),它的内部实现是直接强转,我们也可以强转protocol_ref_tprotocol_t
static ALWAYS_INLINE protocol_t *remapProtocol(protocol_ref_t proto)
{
    runtimeLock.assertLocked();
    // Protocols in shared cache images have a canonical bit to mark that they
    // are the definition we should use
    if (((protocol_t *)proto)->isCanonical())
        return (protocol_t *)proto;//直接强转protocol_t返回
    protocol_t *newproto = (protocol_t *)
        getProtocol(((protocol_t *)proto)->mangledName);
    return newproto ? newproto : (protocol_t *)proto;//直接强转protocol_t返回
}

LLDB调试代码

2021-06-26 21:59:08.559358+0800 KCObjcBuild[81571:7062064] GCTeacher
(lldb) p/x pClass //获取当前class地址
(Class) $0 = 0x0000000100004908 GCTeacher
(lldb) p/x 0x0000000100004908+0x20 //内存偏移获取bits
(long) $1 = 0x0000000100004928
(lldb) p (class_data_bits_t *)$1//强转为bits
(class_data_bits_t *) $2 = 0x0000000100004928
(lldb) p $2->data()//获取bits中的data
(class_rw_t *) $3 = 0x0000000101131b70
(lldb) p *$3//打印data目标地址的内容:class_rw_t
(class_rw_t) $4 = {
  flags = 2148007936
  witness = 0
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = {
      Value = 4294984032
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}
(lldb) p $4.protocols()//从class_rw_t中获取protocols
(const protocol_array_t) $5 = {
  list_array_tt<unsigned long, protocol_list_t, RawPtr> = {
     = {
      list = {
        ptr = 0x0000000100004270
      }
      arrayAndFlag = 4294984304
    }
  }
}
(lldb) p $5.list  //获取protocols的list
(const RawPtr<protocol_list_t>) $6 = {
  ptr = 0x0000000100004270
}
(lldb) p $6.ptr  //获取protocols的ptr
(protocol_list_t *const) $7 = 0x0000000100004270
(lldb) p *$7  //获取ptr中的protocol_list_t
(protocol_list_t) $8 = (count = 1, list = protocol_ref_t [] @ 0x00007f922ab3a268) 
(lldb) p $8.list[0]//从protocol_list_t获取index为0的protocol_ref_t
(protocol_ref_t) $10 = 4294986152
(lldb) p (protocol_t *)$10//将protocol_ref_t强转为protocol_t
(protocol_t *) $11 = 0x00000001000049a8
(lldb) p *$11//取出protocol_t中存储的数据
(protocol_t) $12 = {
  objc_object = {
    isa = {
      bits = 4298453192
      cls = Protocol
       = {
        nonpointer = 0
        has_assoc = 0
        has_cxx_dtor = 0
        shiftcls = 537306649
        magic = 0
        weakly_referenced = 0
        unused = 0
        has_sidetable_rc = 0
        extra_rc = 0
      }
    }
  }
  mangledName = 0x0000000100003e89 "TestDelegate"
  protocols = 0x0000000100004338
  instanceMethods = 0x0000000000000000
  classMethods = 0x0000000000000000
  optionalInstanceMethods = 0x0000000100004350
  optionalClassMethods = 0x0000000000000000
  instanceProperties = 0x0000000000000000
  size = 96
  flags = 0
  _extendedMethodTypes = 0x0000000100004370
  _demangledName = 0x0000000000000000
  _classProperties = 0x0000000000000000
}
(lldb) p *$12.optionalInstanceMethods//获取protocol_t中的optionalInstanceMethods
(method_list_t) $15 = {
  entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 24, count = 1)
}
(lldb) p $15.get(0).big()//打印optionalInstanceMethods中的index=0的数据
(method_t::big) $17 = {
  name = "testDelegate"
  types = 0x0000000100003ec6 "v16@0:8"
  imp = 0x0000000000000000
}
(lldb) 

通过以上方式,最终成功获取到了protocol_t的相关信息,也对之前探索的Class内容做了补充;