区块链技术博客
www.b2bchain.cn

runtime中的消息缓存讲解求职学习资料

本文介绍了runtime中的消息缓存讲解求职学习资料,有助于帮助完成毕业设计以及求职,是一篇很好的资料。

对技术面试,学习经验等有一些体会,在此分享。

前言

在OC中,向一个对象发送消息,编译器会转换成objc_msgSend(receiver,SEL,args)方法,其中reveiver就是接收消息的对象,而SEL是消息的名称字符串,args是这个消息包含的参数。例如:

[view addSubview: subview]; =>   objc_msgSend(view,"addSubview",subview);

Runtime库通过函数objc_msgSend以及OC对象中的隐藏isa成员来实现运行时方法查找,每个对象的isa中保存着这个对象的类对象指针。类对象中保存着这个类所声明的一些数据结构,比如 方法,属性等。例如上例中会在view的类对象中寻找addSubview方法,首先是在cache_t *cache 中去寻找这个方法,如果没有找到,转而从method_array_t methods中查找,如果没有找到,会根据super_class 找到自己的父类的类对象,如此循环查找,如果还未找到,就会走消息转发的三个步骤,本文主要讲解第一个过程提到的数据结:cache_t

数据结构

可以在objc-runtime-new.h中看到cache_t的数据结构

struct cache_t {   // 哈希桶列表   struct bucket_t *_buckets;   // 掩码,这里是 列表长度减一,用于 与操作   mask_t _mask;   // 已经缓存的个数   mask_t _occupied; }  struct bucket_t {   //忽略对于不同cpu架构的代码组织方式,数据成员不变   //sel 转成无符号整型指针,就是普通意义上的key   cache_key_t _key;   // 方法实现,IMP   MethodCacheIMP _imp; }  // mask_t === uint32_t // cache_key_t === uintptr_t // MethodCacheIMP === IMP

在objc-cache.mm文件头部,有关于缓存的说明:

为了加速,objc_msgSend函数在读取cache的时候没有获取锁,而是变更所有的cache,这个操作是为了在在更改cache的同时,避免msgSend读取到错误的结果,或者是由于数据不一致而导致的崩溃。

当缓存扩展以后,会丢掉之前的缓存结果,这些旧的缓存不会立即释放,因为可能会存在并发的msgSend仍然在使用旧的缓存。所以 只是将内存与缓存的数据结构断开链接,并将其放置在垃圾名单上。collect_in_critical函数会检查所有的线程,来判定这些旧的缓存是否可以释放掉。

为了防止并发操作干扰数据的正确性,所有的函数在修改缓存的时候必须 要获取cacheUpdateLock。释放弃用的缓存的函数也要获取cacheUpdateLock并使用collection_in_critical()函数刷新缓存。cacheUpdateLock还可以用来保护大型方法缓存块的自定义的allocator。

缓存读取:

  • objc_msgSend
  • cache_getImp

写入缓存:

  • cache_fill
  • cache_expand
  • cache_create
  • bcopy
  • flush_caches
  • cache_flush
  • cache_collect_free

从这个简短的文档里面(这段注释很旧了,有些函数已经被删除了),我们可以知道在执行msgSend函数的时候,会去执行读取cahche,cache的读取操作是cache_getImp(cls,sel),这段代码是用汇编写的。

存入操作

下面就来看一下相关的方法:

void cache_fill(Class cls, SEL sel, IMP imp, id receiver) {//省略锁的代码   #if !DEBUG_TASK_THREADS   //class没有初始化就不写入cache     if (cls->isInitialized()) {         cache_t *cache = getCache(cls);       //获取到类对象的cache,将sel、imp插入到缓存中         cache->insert(cls, sel, imp, receiver);     }   #else   // 收集 多线程信息     _collecting_in_critical();   #endif }

在之前的版本中,对应的函数是

 mutex_locker_t lock(cacheUpdateLock);  cache_fill_nolock(cls, sel, imp, receiver);

在最新的版本中,代码已经改成上面的了,所以就以新的代码来分析吧

void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver) {     // Use the cache as-is if it is less than 3/4 full     mask_t newOccupied = occupied() + 1;     unsigned oldCapacity = capacity(), capacity = oldCapacity;     if (slowpath(isConstantEmptyCache())) {                 //空的cache,用INIT_CACHE_SIZE初始化,默认是4         if (!capacity) capacity = INIT_CACHE_SIZE;         reallocate(oldCapacity, capacity, false);     }     else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) {       //cache的容量小于3/4 忽略不做处理     }     else {       //大于3/4,需要扩容,乘以2         capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;         if (capacity > MAX_CACHE_SIZE) {           //对最大容量做一个限制             capacity = MAX_CACHE_SIZE;         }         reallocate(oldCapacity, capacity, true);     }    //获取buckets,散列表     bucket_t *b = buckets();   //设置掩码,为最大容易减1     mask_t m = capacity - 1;   //cache_hash 根据与运算,获取初始下标     mask_t begin = cache_hash(sel, m);     mask_t i = begin;    //根据上面计算出来的index,查找是该index是否已经有值,没有值就将该imp插入。 如果发生哈希碰撞就会计算下一个index,开放寻址法。 如果bucket中的sel与将要存入的sel是同一个,说明在获取cacheUpdateLock之前有其他的线程已经加入了。直接return即可     do {       // 0 代表该bucket没有使用         if (fastpath(b[i].sel() == 0)) {           // occupied + 1             incrementOccupied();             b[i].set<Atomic, Encoded>(sel, imp, cls);             return;         }         if (b[i].sel() == sel) {             return;         }       //寻找下一个地址index     } while (fastpath((i = cache_next(i, m)) != begin)); }

第9行 reallocate函数对buckets进行重分配。根据新的容量开辟一个新的buckets,然后对新的buckets初始化mask、occupied等值,然后将旧的buckets放入cache_collect_free函数中进行资源释放。从这里可以看出,当进行扩容的时候,缓存的方法会被遗弃,只是容量的增加。

第29行的cache_hash函数就是将sel 转成无符号整型指针,然后与mask掩码进行与操作。因为mask 总是等与cache的容量减一,而且cache的容量总是4的倍数,并且容量始终保持存有空闲的1/4,所以进行与操作就可以保证地址是有效的,在遍历buckets的时候就不会出现越界、死循环等问题。

第45行的cache_next函数就是在产生哈希碰撞的时候,对index进行减一,然后进行新一轮的循环处理。 在这里可以看出,虽然buckets是一个数组,但是对这个数组进行的操作是随机的,可以将对index的计算抽象成一个黑盒以后,对于这个buckets数据结构来看,就是一个散列表数据结构。

缓存查找

在msgSend函数执行的时候,会在类对象中进行缓存查找,如果命中缓存就去执行对应的函数。这块的代码在objc-msg-arm.s文件中,有兴趣的可以去看看

大致的伪代码如下

   //遍历缓存哈希桶并查找缓存中的方法实现。     IMP  imp = NULL;     //cmd与cache中的mask进行与计算得到哈希桶中的索引,来查找方法是否已经放入缓存cache哈希桶中。     int index =  cls->cache.mask & op;     while (true) {         //如果缓存哈希桶中命中了对应的方法实现,则保存到imp中并退出循环。         if (cls->cache.buckets[index].key == op) {               imp = cls->cache.buckets[index].imp;               break;         }         //方法实现并没有被缓存,并且对应的桶的数据是空的就退出循环         if (cls->cache.buckets[index].key == NULL) {              break;         }         //如果哈希桶中对应的项已经被占用但是又不是要执行的方法,则通过开地址法来继续寻找缓存该方法的桶。         if (index == 0) {             index = cls->cache.mask;  //从尾部寻找         }         else {             index--;   //索引减1继续寻找。         }     } /*end while*/    //4执行方法实现或方法未命中缓存处理函数     if (imp != NULL)          return imp(receiver, op,  ...); //这里的... 是指传递给objc_msgSend的OC方法中的参数。     else          return objc_msgSend_uncached(receiver, op, cls, ...); }

前言

在OC中,向一个对象发送消息,编译器会转换成objc_msgSend(receiver,SEL,args)方法,其中reveiver就是接收消息的对象,而SEL是消息的名称字符串,args是这个消息包含的参数。例如:

[view addSubview: subview]; =>   objc_msgSend(view,"addSubview",subview);

Runtime库通过函数objc_msgSend以及OC对象中的隐藏isa成员来实现运行时方法查找,每个对象的isa中保存着这个对象的类对象指针。类对象中保存着这个类所声明的一些数据结构,比如 方法,属性等。例如上例中会在view的类对象中寻找addSubview方法,首先是在cache_t *cache 中去寻找这个方法,如果没有找到,转而从method_array_t methods中查找,如果没有找到,会根据super_class 找到自己的父类的类对象,如此循环查找,如果还未找到,就会走消息转发的三个步骤,本文主要讲解第一个过程提到的数据结:cache_t

数据结构

可以在objc-runtime-new.h中看到cache_t的数据结构

struct cache_t {   // 哈希桶列表   struct bucket_t *_buckets;   // 掩码,这里是 列表长度减一,用于 与操作   mask_t _mask;   // 已经缓存的个数   mask_t _occupied; }  struct bucket_t {   //忽略对于不同cpu架构的代码组织方式,数据成员不变   //sel 转成无符号整型指针,就是普通意义上的key   cache_key_t _key;   // 方法实现,IMP   MethodCacheIMP _imp; }  // mask_t === uint32_t // cache_key_t === uintptr_t // MethodCacheIMP === IMP

在objc-cache.mm文件头部,有关于缓存的说明:

为了加速,objc_msgSend函数在读取cache的时候没有获取锁,而是变更所有的cache,这个操作是为了在在更改cache的同时,避免msgSend读取到错误的结果,或者是由于数据不一致而导致的崩溃。

当缓存扩展以后,会丢掉之前的缓存结果,这些旧的缓存不会立即释放,因为可能会存在并发的msgSend仍然在使用旧的缓存。所以 只是将内存与缓存的数据结构断开链接,并将其放置在垃圾名单上。collect_in_critical函数会检查所有的线程,来判定这些旧的缓存是否可以释放掉。

为了防止并发操作干扰数据的正确性,所有的函数在修改缓存的时候必须 要获取cacheUpdateLock。释放弃用的缓存的函数也要获取cacheUpdateLock并使用collection_in_critical()函数刷新缓存。cacheUpdateLock还可以用来保护大型方法缓存块的自定义的allocator。

缓存读取:

  • objc_msgSend
  • cache_getImp

写入缓存:

  • cache_fill
  • cache_expand
  • cache_create
  • bcopy
  • flush_caches
  • cache_flush
  • cache_collect_free

从这个简短的文档里面(这段注释很旧了,有些函数已经被删除了),我们可以知道在执行msgSend函数的时候,会去执行读取cahche,cache的读取操作是cache_getImp(cls,sel),这段代码是用汇编写的。

存入操作

下面就来看一下相关的方法:

void cache_fill(Class cls, SEL sel, IMP imp, id receiver) {//省略锁的代码   #if !DEBUG_TASK_THREADS   //class没有初始化就不写入cache     if (cls->isInitialized()) {         cache_t *cache = getCache(cls);       //获取到类对象的cache,将sel、imp插入到缓存中         cache->insert(cls, sel, imp, receiver);     }   #else   // 收集 多线程信息     _collecting_in_critical();   #endif }

在之前的版本中,对应的函数是

 mutex_locker_t lock(cacheUpdateLock);  cache_fill_nolock(cls, sel, imp, receiver);

在最新的版本中,代码已经改成上面的了,所以就以新的代码来分析吧

void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver) {     // Use the cache as-is if it is less than 3/4 full     mask_t newOccupied = occupied() + 1;     unsigned oldCapacity = capacity(), capacity = oldCapacity;     if (slowpath(isConstantEmptyCache())) {                 //空的cache,用INIT_CACHE_SIZE初始化,默认是4         if (!capacity) capacity = INIT_CACHE_SIZE;         reallocate(oldCapacity, capacity, false);     }     else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) {       //cache的容量小于3/4 忽略不做处理     }     else {       //大于3/4,需要扩容,乘以2         capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;         if (capacity > MAX_CACHE_SIZE) {           //对最大容量做一个限制             capacity = MAX_CACHE_SIZE;         }         reallocate(oldCapacity, capacity, true);     }    //获取buckets,散列表     bucket_t *b = buckets();   //设置掩码,为最大容易减1     mask_t m = capacity - 1;   //cache_hash 根据与运算,获取初始下标     mask_t begin = cache_hash(sel, m);     mask_t i = begin;    //根据上面计算出来的index,查找是该index是否已经有值,没有值就将该imp插入。 如果发生哈希碰撞就会计算下一个index,开放寻址法。 如果bucket中的sel与将要存入的sel是同一个,说明在获取cacheUpdateLock之前有其他的线程已经加入了。直接return即可     do {       // 0 代表该bucket没有使用         if (fastpath(b[i].sel() == 0)) {           // occupied + 1             incrementOccupied();             b[i].set<Atomic, Encoded>(sel, imp, cls);             return;         }         if (b[i].sel() == sel) {             return;         }       //寻找下一个地址index     } while (fastpath((i = cache_next(i, m)) != begin)); }

第9行 reallocate函数对buckets进行重分配。根据新的容量开辟一个新的buckets,然后对新的buckets初始化mask、occupied等值,然后将旧的buckets放入cache_collect_free函数中进行资源释放。从这里可以看出,当进行扩容的时候,缓存的方法会被遗弃,只是容量的增加。

第29行的cache_hash函数就是将sel 转成无符号整型指针,然后与mask掩码进行与操作。因为mask 总是等与cache的容量减一,而且cache的容量总是4的倍数,并且容量始终保持存有空闲的1/4,所以进行与操作就可以保证地址是有效的,在遍历buckets的时候就不会出现越界、死循环等问题。

第45行的cache_next函数就是在产生哈希碰撞的时候,对index进行减一,然后进行新一轮的循环处理。 在这里可以看出,虽然buckets是一个数组,但是对这个数组进行的操作是随机的,可以将对index的计算抽象成一个黑盒以后,对于这个buckets数据结构来看,就是一个散列表数据结构。

缓存查找

在msgSend函数执行的时候,会在类对象中进行缓存查找,如果命中缓存就去执行对应的函数。这块的代码在objc-msg-arm.s文件中,有兴趣的可以去看看

大致的伪代码如下

   //遍历缓存哈希桶并查找缓存中的方法实现。     IMP  imp = NULL;     //cmd与cache中的mask进行与计算得到哈希桶中的索引,来查找方法是否已经放入缓存cache哈希桶中。     int index =  cls->cache.mask & op;     while (true) {         //如果缓存哈希桶中命中了对应的方法实现,则保存到imp中并退出循环。         if (cls->cache.buckets[index].key == op) {               imp = cls->cache.buckets[index].imp;               break;         }         //方法实现并没有被缓存,并且对应的桶的数据是空的就退出循环         if (cls->cache.buckets[index].key == NULL) {              break;         }         //如果哈希桶中对应的项已经被占用但是又不是要执行的方法,则通过开地址法来继续寻找缓存该方法的桶。         if (index == 0) {             index = cls->cache.mask;  //从尾部寻找         }         else {             index--;   //索引减1继续寻找。         }     } /*end while*/    //4执行方法实现或方法未命中缓存处理函数     if (imp != NULL)          return imp(receiver, op,  ...); //这里的... 是指传递给objc_msgSend的OC方法中的参数。     else          return objc_msgSend_uncached(receiver, op, cls, ...); }

前言

在OC中,向一个对象发送消息,编译器会转换成objc_msgSend(receiver,SEL,args)方法,其中reveiver就是接收消息的对象,而SEL是消息的名称字符串,args是这个消息包含的参数。例如:

[view addSubview: subview]; =>   objc_msgSend(view,"addSubview",subview);

Runtime库通过函数objc_msgSend以及OC对象中的隐藏isa成员来实现运行时方法查找,每个对象的isa中保存着这个对象的类对象指针。类对象中保存着这个类所声明的一些数据结构,比如 方法,属性等。例如上例中会在view的类对象中寻找addSubview方法,首先是在cache_t *cache 中去寻找这个方法,如果没有找到,转而从method_array_t methods中查找,如果没有找到,会根据super_class 找到自己的父类的类对象,如此循环查找,如果还未找到,就会走消息转发的三个步骤,本文主要讲解第一个过程提到的数据结:cache_t

数据结构

可以在objc-runtime-new.h中看到cache_t的数据结构

struct cache_t {   // 哈希桶列表   struct bucket_t *_buckets;   // 掩码,这里是 列表长度减一,用于 与操作   mask_t _mask;   // 已经缓存的个数   mask_t _occupied; }  struct bucket_t {   //忽略对于不同cpu架构的代码组织方式,数据成员不变   //sel 转成无符号整型指针,就是普通意义上的key   cache_key_t _key;   // 方法实现,IMP   MethodCacheIMP _imp; }  // mask_t === uint32_t // cache_key_t === uintptr_t // MethodCacheIMP === IMP

在objc-cache.mm文件头部,有关于缓存的说明:

为了加速,objc_msgSend函数在读取cache的时候没有获取锁,而是变更所有的cache,这个操作是为了在在更改cache的同时,避免msgSend读取到错误的结果,或者是由于数据不一致而导致的崩溃。

当缓存扩展以后,会丢掉之前的缓存结果,这些旧的缓存不会立即释放,因为可能会存在并发的msgSend仍然在使用旧的缓存。所以 只是将内存与缓存的数据结构断开链接,并将其放置在垃圾名单上。collect_in_critical函数会检查所有的线程,来判定这些旧的缓存是否可以释放掉。

为了防止并发操作干扰数据的正确性,所有的函数在修改缓存的时候必须 要获取cacheUpdateLock。释放弃用的缓存的函数也要获取cacheUpdateLock并使用collection_in_critical()函数刷新缓存。cacheUpdateLock还可以用来保护大型方法缓存块的自定义的allocator。

缓存读取:

  • objc_msgSend
  • cache_getImp

写入缓存:

  • cache_fill
  • cache_expand
  • cache_create
  • bcopy
  • flush_caches
  • cache_flush
  • cache_collect_free

从这个简短的文档里面(这段注释很旧了,有些函数已经被删除了),我们可以知道在执行msgSend函数的时候,会去执行读取cahche,cache的读取操作是cache_getImp(cls,sel),这段代码是用汇编写的。

存入操作

下面就来看一下相关的方法:

void cache_fill(Class cls, SEL sel, IMP imp, id receiver) {//省略锁的代码   #if !DEBUG_TASK_THREADS   //class没有初始化就不写入cache     if (cls->isInitialized()) {         cache_t *cache = getCache(cls);       //获取到类对象的cache,将sel、imp插入到缓存中         cache->insert(cls, sel, imp, receiver);     }   #else   // 收集 多线程信息     _collecting_in_critical();   #endif }

在之前的版本中,对应的函数是

 mutex_locker_t lock(cacheUpdateLock);  cache_fill_nolock(cls, sel, imp, receiver);

在最新的版本中,代码已经改成上面的了,所以就以新的代码来分析吧

void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver) {     // Use the cache as-is if it is less than 3/4 full     mask_t newOccupied = occupied() + 1;     unsigned oldCapacity = capacity(), capacity = oldCapacity;     if (slowpath(isConstantEmptyCache())) {                 //空的cache,用INIT_CACHE_SIZE初始化,默认是4         if (!capacity) capacity = INIT_CACHE_SIZE;         reallocate(oldCapacity, capacity, false);     }     else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) {       //cache的容量小于3/4 忽略不做处理     }     else {       //大于3/4,需要扩容,乘以2         capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;         if (capacity > MAX_CACHE_SIZE) {           //对最大容量做一个限制             capacity = MAX_CACHE_SIZE;         }         reallocate(oldCapacity, capacity, true);     }    //获取buckets,散列表     bucket_t *b = buckets();   //设置掩码,为最大容易减1     mask_t m = capacity - 1;   //cache_hash 根据与运算,获取初始下标     mask_t begin = cache_hash(sel, m);     mask_t i = begin;    //根据上面计算出来的index,查找是该index是否已经有值,没有值就将该imp插入。 如果发生哈希碰撞就会计算下一个index,开放寻址法。 如果bucket中的sel与将要存入的sel是同一个,说明在获取cacheUpdateLock之前有其他的线程已经加入了。直接return即可     do {       // 0 代表该bucket没有使用         if (fastpath(b[i].sel() == 0)) {           // occupied + 1             incrementOccupied();             b[i].set<Atomic, Encoded>(sel, imp, cls);             return;         }         if (b[i].sel() == sel) {             return;         }       //寻找下一个地址index     } while (fastpath((i = cache_next(i, m)) != begin)); }

第9行 reallocate函数对buckets进行重分配。根据新的容量开辟一个新的buckets,然后对新的buckets初始化mask、occupied等值,然后将旧的buckets放入cache_collect_free函数中进行资源释放。从这里可以看出,当进行扩容的时候,缓存的方法会被遗弃,只是容量的增加。

第29行的cache_hash函数就是将sel 转成无符号整型指针,然后与mask掩码进行与操作。因为mask 总是等与cache的容量减一,而且cache的容量总是4的倍数,并且容量始终保持存有空闲的1/4,所以进行与操作就可以保证地址是有效的,在遍历buckets的时候就不会出现越界、死循环等问题。

第45行的cache_next函数就是在产生哈希碰撞的时候,对index进行减一,然后进行新一轮的循环处理。 在这里可以看出,虽然buckets是一个数组,但是对这个数组进行的操作是随机的,可以将对index的计算抽象成一个黑盒以后,对于这个buckets数据结构来看,就是一个散列表数据结构。

缓存查找

在msgSend函数执行的时候,会在类对象中进行缓存查找,如果命中缓存就去执行对应的函数。这块的代码在objc-msg-arm.s文件中,有兴趣的可以去看看

大致的伪代码如下

   //遍历缓存哈希桶并查找缓存中的方法实现。     IMP  imp = NULL;     //cmd与cache中的mask进行与计算得到哈希桶中的索引,来查找方法是否已经放入缓存cache哈希桶中。     int index =  cls->cache.mask & op;     while (true) {         //如果缓存哈希桶中命中了对应的方法实现,则保存到imp中并退出循环。         if (cls->cache.buckets[index].key == op) {               imp = cls->cache.buckets[index].imp;               break;         }         //方法实现并没有被缓存,并且对应的桶的数据是空的就退出循环         if (cls->cache.buckets[index].key == NULL) {              break;         }         //如果哈希桶中对应的项已经被占用但是又不是要执行的方法,则通过开地址法来继续寻找缓存该方法的桶。         if (index == 0) {             index = cls->cache.mask;  //从尾部寻找         }         else {             index--;   //索引减1继续寻找。         }     } /*end while*/    //4执行方法实现或方法未命中缓存处理函数     if (imp != NULL)          return imp(receiver, op,  ...); //这里的... 是指传递给objc_msgSend的OC方法中的参数。     else          return objc_msgSend_uncached(receiver, op, cls, ...); }

部分转自互联网,侵权删除联系

赞(0) 打赏
部分文章转自网络,侵权联系删除b2bchain区块链学习技术社区 » runtime中的消息缓存讲解求职学习资料
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

b2b链

联系我们联系我们