iOS 全网最新objc4 可调式/编译源码
编译好的源码的下载地址

序言

在前面文章类的结构中,咱们剖析了bits的结构,isa以及superclass是为指针类型,还剩下一个cache没有剖析,cache望文生义便是缓存相关的,今天就来看一下cache是怎么个原理。

cache的数据结构

先自界说一个类LGPerson,代码完成

iOS底层之类的cache分析

LLDB输出数据结构

LLDB调试输出,检查cache的数据

iOS底层之类的cache分析
有几个要害数据:_bucketsAndMaybeMask_maybeMask_flags_occupied_originalPreoptCache

cache源码数据结构

然后咱们再看cache_t的源码结构

iOS底层之类的cache分析
总结

  • _bucketsAndMaybeMask:一个uintptr_t类型的指针;
  • 联合体:一个结构体和preopt_cache_t结构体类型的指针变量_originalPreoptCache
  • _maybeMask: mask_t泛型的变量;
  • _flagsuint16_t类型变量;
  • _occupieduint16_t类型变量
  • preopt_cache_tpreopt_cache_t结构体类型的指针变量;

这儿咱们还无从知道cache是怎样缓存的数据,以及缓存的是什么数据,是属性仍是办法呢?

cache缓存数据类型

既然经过cache_t的数据结构看不出来,那咱们就找办法

缓存应该有增修改查等办法,那就从这些办法下手吧。经过阅览源码,咱们看到有一个insert办法和copyCacheNolock办法

iOS底层之类的cache分析
insert办法中,刺进的是SELIMP,由此能够看出cache缓存的数据是办法method,然后再看一下insert的完成,找一下SELIMP是缓存在哪里。

cache缓存的存储位置

iOS底层之类的cache分析
这儿很明显是一个bucket_t类型的b,调用set办法刺进SELIMP以及相关的Class

看一下bucket_t的结构。

iOS底层之类的cache分析
这儿咱们能够简单总结一下类中cache_t的结构

iOS底层之类的cache分析

cache缓存数据输出检查

现在咱们已经找到了cache缓存的办法是存在bucket_t中,并且bucket_t有成员变量_sel_imp,在insert中是经过办法buckets()获取到的bucket_t,那咱们就找到输出一下。

LLDB找到cache缓存数据

cache_t的结构体界说中,正好有buckets()办法,那咱们在LLDB中获取到cache的地址变量就能够输出bucket_t

iOS底层之类的cache分析

声明一个LGPerson类型的变量p,并调用目标办法sayHello

iOS底层之类的cache分析
然后咱们用LLDB调试输出信息

iOS底层之类的cache分析
咱们成功获取到了bucket_t类型的$3,但当我检查$3的内容是缺仍是空值。why!!!why!!!why!!!

仍是回归到insert源码,看一下到底是怎么刺进的缓存吧。

iOS底层之类的cache分析
天呢!漏了一个细节,这儿缓存刺进的时分是用了hash算法取下标的办法,那咱们上面取到的第一个bucket_t的就可能为空值
既然这样,buckets()的存储结构是一个哈希数组,那咱们就继续往下面找bucket_t

iOS底层之类的cache分析
这儿的_sel中的Value_imp中的Value明显和上面的不一样了,不再是nil0,那咱们能够猜想这是一个有效的bucket_t。 找到bucket_t结构体中的办法sel()imp(),输出一下

iOS底层之类的cache分析

iOS底层之类的cache分析
Done!!
这儿咱们成功找到了缓存的办法sayHello,可是我发现在LLDB这样调试很是费事,并且还依赖于源码的运行环境,假如有系统升级或许源码有更新,编译不了源码,莫非只能GG吗,所以能不能脱离源码编译环境也能搞定上面的步骤呢

脱离源码剖析cache

咱们的意图是获取cache_t里边的bucket_tcache_t是在objc_class里边,那咱们就依照源码objc_class的结构去自界说一个类似的结构体,这样就能够经过NSLog输出获取的内容信息。

typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
struct lg_bucket_t {
  SEL _sel;
  IMP _imp;
};
struct lg_cache_t {
  struct lg_bucket_t * _buckets;
  mask_t             _maybeMask;
  uint16_t       _flags;
  uint16_t       _occupied;
};
struct lg_class_data_bits_t {
  uintptr_t bits;
};
struct lg_objc_class {
  Class isa;
  Class superclass;
  struct lg_cache_t cache;       // formerly cache pointer and vtable
  struct lg_class_data_bits_t bits;
};
int main(int argc, const char * argv[]) {
  @autoreleasepool {
    // insert code here...
    LGPerson * p = [LGPerson alloc];
    [p sayHello];
    Class pClass = [LGPerson class];
    struct lg_objc_class *lg_class = ( __bridge struct lg_objc_class *)(pClass);
    NSLog(@" - %hu - %u",lg_class->cache._occupied,lg_class->cache._maybeMask);
    for (int i = 0; i < lg_class->cache._maybeMask; i++) {
      struct lg_bucket_t bucket = lg_class->cache._buckets[i];
      NSLog(@"SEL = %@ --- IMP = %p", NSStringFromSelector(bucket._sel), bucket._imp);
    }
    NSLog(@"Hello, World!");
  }
  return 0;
}

运行上面的代码,检查输出

iOS底层之类的cache分析

成功输出,这儿的_occupied为1,_maybeMask为3,咱们再调两个办法sayHello_1sayHello_2验证一下。

iOS底层之类的cache分析

iOS底层之类的cache分析
这儿发生了蹊跷,_occupied为1,_maybeMask变成了7,而缓存中只要办法sayHello_2,咱们调用的sayHellosayHello_1却不在缓存中。既然这样,那就从头捋一遍源码,看看是不是又漏下什么细节了。

cache底层原理剖析

对于底层原理剖析,就从cache_t的刺进办法insert入手

insert源码

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
  runtimeLock.assertLocked();
  // Never cache before +initialize is done
  if (slowpath(!cls()->isInitialized())) {
    return;
  }
  if (isConstantOptimizedCache()) {
    _objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
          cls()->nameForLogging());
  }
#if DEBUG_TASK_THREADS
  return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
  mutex_locker_t lock(cacheUpdateLock);
#endif
  ASSERT(sel != 0 && cls()->isInitialized());
  // Use the cache as-is if until we exceed our expected fill ratio.
  mask_t newOccupied = occupied() + 1;
  unsigned oldCapacity = capacity(), capacity = oldCapacity;
  if (slowpath(isConstantEmptyCache())) { // 1.判断当时缓存是否为空的
    // Cache is read-only. Replace it.
    if (!capacity) capacity = INIT_CACHE_SIZE; // 1 << 2 = 4
    reallocate(oldCapacity, capacity, /* freeOld */false); //拓荒内存
  }
  else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
    // Cache is less than 3/4 or 7/8 full. Use it as-is.
  }
#if CACHE_ALLOW_FULL_UTILIZATION
  else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
    // Allow 100% cache utilization for small buckets. Use it as-is.
  }
#endif
  else {
    capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
    if (capacity > MAX_CACHE_SIZE) {
      capacity = MAX_CACHE_SIZE;
    }
    reallocate(oldCapacity, capacity, true);
  }
  bucket_t *b = buckets();
  mask_t m = capacity - 1;
  mask_t begin = cache_hash(sel, m);
  mask_t i = begin;
  // Scan for the first unused slot and insert there.
  // There is guaranteed to be an empty slot.
  do {
    if (fastpath(b[i].sel() == 0)) {
      incrementOccupied();
      b[i].set<Atomic, Encoded>(b, sel, imp, cls());
      return;
    }
    if (b[i].sel() == sel) {
      // The entry was added to the cache by some other thread
      // before we grabbed the cacheUpdateLock.
      return;
    }
  } while (fastpath((i = cache_next(i, m)) != begin));
  bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}

整个代码流程咱们按调用次数散布解析

第一次调用办法insert

iOS底层之类的cache分析
第一次刺进缓存时

  1. _occupied的值为0,所以newOccupied为1;
  2. capacity()取的是_maybeMask的值,所以oldCapacitycapacity值都为0;
  3. isConstantEmptyCache判断当时缓存是否为空,条件成立,进入if语句;
  4. capacity值为0,赋值为INIT_CACHE_SIZEINIT_CACHE_SIZE = 1 << 2值为4;
  5. 调用reallocate拓荒内存;

iOS底层之类的cache分析
reallocate办法中,拓荒新内存,然后调用setBucketsAndMask办法,使cache_t中的成员变量_bucketsAndMaybeMask_maybeMask_occupied做相关

iOS底层之类的cache分析
之后是再拓荒的缓存空间中存入SELIMP

iOS底层之类的cache分析
在存入SELIMP的办法中,有对IMP进行编码,实际上存入的是编码后的newImp

iOS底层之类的cache分析
imp编码的源代码

iOS底层之类的cache分析

iOS底层之类的cache分析
咱们用到的CACHE_IMP_ENCODING状况为CACHE_IMP_ENCODING_ISA_XOR,所以上面的编码算法是imp & cls

咱们知道了第一次调用办法,会拓荒空间为4缓存空间,当咱们调用更多办法的时分,应该在什么时分扩容呢?

四分之三扩容

当咱们不是第一次调用办法时,就会进入一个剩余空间容量判断

iOS底层之类的cache分析

  • newOccupied:进入缓存的第几个办法;
  • CACHE_END_MARKER:宏界说值为1;
  • cache_fill_ratio(capacity): capacity * 3 / 4容量的3/4值;

这儿咱们知道当新的调用办法进入缓存时

  1. 假如不满足扩容条件,就会继续往拓荒的缓存空间刺进一条缓存数据。比方:调用sayHello_1时,newOccupied为2,capacity为4, 2 + 1 <= 4 *3 / 4的条件满足。
  2. 假如到达扩容条件,就会先拓荒2倍的新内存,然后再刺进新的缓存数据。比方:调用sayHello_2时,newOccupied为3,capacity为4, 3 + 1 <= 4 *3 / 4的条件不满足,就会进入else语句,拓荒2倍容量的新内存。


在拓荒新内存中调用办法reallocate时,传入的最后一个参数freeOldtrue,会把旧的缓存空间整理释放掉,不会copy缓存数据到新的缓存空间,这也是为什么调用sayHello_2时,输出的只要sayHello_2

iOS底层之类的cache分析

总结

关于objc_class中的cache的原理剖析,咱们先是检查cache_t数据结构,依据数据结构咱们无法知道其工作原理,然后咱们经过结构体中的办法去找头绪,最后锁定insert办法,依据insert办法来大致了解整个缓存刺进的流程。
cache_t的工作原理流程图:

iOS底层之类的cache分析