小码哥iOS学习笔记第十二天:Class结构

一、Class的结构

  • 通过查看源码, 可以得出Class的底层结构如下图

  • 一开始class_data_bits_t bits;指向ro, 在加载的过程中创建了rw, 此时的指向顺序是bits->rw->ro

二、class_rw_t

  • class_rw_t里面的methodspropertiesprotocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容

三、class_ro_t

  • class_ro_t里面的baseMethodListbaseProtocolsivarsbaseProperties是一维数组,是只读的,包含了类的初始内容

四、method_t

  • method_t是对方法\函数的封装, method_t结构如下

  • IMP代表函数的具体实现

  • SEL代表方法\函数名,一般叫做选择器,底层结构跟char *类似
    • 可以通过@selector()sel_registerName()获得
    • 可以通过sel_getName()NSStringFromSelector()转成字符串
    • 不同类中相同名字的方法,所对应的方法选择器是相同的

  • types包含了函数返回值、参数编码的字符串

  • iOS中提供了一个叫做@encode的指令,可以将具体的类型表示成字符串编码

五、方法缓存

  • Class内部结构中有个方法缓存(cache_t),用散列表哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度

  • 调用方法时的查找过程
以对象方法为例:
通过isa找到class
先会先从cache中查找, 如果有方法, 直接调用
如果没有方法, 在自身的class->bits->rw中查找方法
如果找到方法直接调动,并将方法缓存到cache中
如果没有找到, 会通过superclass找到父类, 从父类->bits->rw中查找方法
如果在父类中找到方法, 就直接调用, 同时将方法存到自己(不是父类的)的cache中, 如果一直找不到, 就进入下一个阶段
复制代码
散列表存储方法
  • 一开始, 系统会分配给cache_t->_buckets一段内存, 假设第一次分配了足够存储3个方法的内存

  • 此时cache_tmask等于2, 即_buckets的长度 - 1
  • 当存储方法时, 会用SEL & mask, 获取到一个数字, 用这个数字做为索引, 将该方法存储到_buckets
  • 假如有以下代码, Person继承自NSObject, 有三个方法eatrunsay

  • Student继承自Person, 有三个方法learningexercisesdancing

  • 当一个Student实例调用learning方法时, 就会用@selector(learning) & _buckets.mask来获取存储的索引,然后将learning方法存放到Student类对象cache
  • 假设获取到的索引值为0, 那么散列表的结构类似下图

  • 如果Student实例调用父类Personeat方法时, 根据@selector(eat) & _buckets.mask的结果将becket_t插入到相应位置
  • 假设@selector(eat) & _buckets.mask结果是2, 那么散列表结构类似下图

  • 即使eatPerson中的方法, 但是Student调用,也会存到Studentcache

  • 当多个数都&一个固定值时, 那么肯定就有重复的可能出现, 比如

 1001 1010          1000 1011
&0000 1010         &0000 1010
-----------       ------------
 0000 1010          0000 1010
复制代码
  • 所以通过多个SEL & _buckets.mask获取到的值就有可能是重复的
  • 此时,存储方法的索引就会-1, 然后在查找-1后所在位置是否空缺, 如果有空缺就会存储
  • 如果Student实例调用exercises方法, 会计算@selector(exercises) & _buckets.mask的结果作为索引值
  • 假设@selector(exercises) & _buckets.mask的结果是2, 此时因为索引2的位置已经存储eat方法, 所以索引会-1变成1, 然后查看1的位置是否空缺, 如果空缺就会存储, 如下图

  • 如果Student实例继续调用dancing方法, 此时cache->buckets已经存储满
  • 那么buckets的容量会扩大一倍, 容量变为6,重新计算cache->mask的值为5, 接着清空buckets中之前缓存的所有方法
  • 然后计算@selector(dancing) & _buckets.mask的值, 如果此时的结果是3, 那么散列表的结构类似下图

  • 因为已经清空过cache中的所有方法, 所以此时只存储dancing方法

六、使用代码验证cache存储流程

准备代码
  • Person继承自NSObject, 有一个对象方法personTest

  • Student继承自Person, 有一个对象方法studentTest

  • GoodStudent继承自Student, 有一个对象方法goodStudentTest

方法调用后, 会缓存在cache中
  • main.mm文件中有如下代码, 并在1921行打断点

  • 执行程序, 查看personClass的结构吗可以看到cache中的_buckets长度是_mask + 1 = 4, 此时已经存储一个方法, 即刚调用过的init方法

  • 接下来过掉第一个断点, 可以看到personTest已经被存到了_buckets

  • 下面使用代码查看更复杂的存储方式, 首先有如下代码, 在19, 20, 21, 23四处打上断点
  • 执行程序, 可以看到此时_buckets中只存了一个方法

  • 过掉19行断点, 可以看到有一个方法被存到_buckets中, 这个方法就是goodStudentTest

  • 过掉20行断点, 可以看到又一个方法被存到_buckets中, 这个方法就是studentTest, 此时_buckets中已经存放了三个方法

  • 接着过掉21行断点, 此时_buckets进行了扩容,从新计算了mask, 同时清空了缓存的方法, 并将新调用的personTest存到了_buckets

打印cache中存储的方法
  • main函数中的代码修改为如下所示

  • 运行程序, 可以看到cache->_buckets中存储的方法, 其中包含了initgoodStudentTeststudentTest

  • 在将代码修改为如下所示, goodStudent先调用5个方法, 在打印缓存的方法列表

  • 执行程序, 可以看到下面的打印

  • 在上面已经知道, 在执行[goodStudent personTest];时已经进行了扩容, 所以此时的容量是8, 由于清空了之前缓存的方法, 所以现在_buckets中只存储了三个方法
通过索引获取方法
  • main函数中的代码修改为如下所示

  • 执行代码, 可以看到打印信息, 已经将personTest的方法和地址打印出来

  • 我们可以通过控制台来验证打印的确实就是personTest方法

  • 我们可以用同样的方式, 打印这三个方法

  • 执行程序, 可以看到三个方法地址的打印

  • 但是可以看到personTestgoodStudentTest取出的地址是相同的
  • 我们可以明确的知道这两个方法不同, 那么取出地址相同的原因就是SEL & mask得到的索引值相同
  • 通过打印, 也可以看到索引值是相同的

  • 之前已经说过, 不同的数字&一个固定的数, 肯定会出现重复的情况, 所以取出的方法也就会是同一个
  • 此时取方法, 需要将索引-1然后再去取出方法, 如果取出的是空或者方法名不对, 那么就继续-1, 再去取方法, 以此类推

  • 索引我们通过下面的方式, 就能取出缓存中正确的方法

  • 执行程序, 可以看到取出的方法全部正确

  • MJClassInfo.h文件中代码如下
#import <Foundation/Foundation.h>

#ifndef MJClassInfo_h
#define MJClassInfo_h

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
# endif

#if __LP64__
typedef uint32_t mask_t;
#else
typedef uint16_t mask_t;
#endif
typedef uintptr_t cache_key_t;

#if __arm__  ||  __x86_64__  ||  __i386__
// objc_msgSend has few registers available.
// Cache scan increments and wraps at special end-marking bucket.
#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}

#elif __arm64__
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

#else
#error unknown architecture
#endif

struct bucket_t {
    cache_key_t _key;
    IMP _imp;
};

struct cache_t {
    bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
    
    IMP imp(SEL selector)
    {
        mask_t begin = _mask & (long long)selector;
        mask_t i = begin;
        do {
            if (_buckets[i]._key == 0  ||  _buckets[i]._key == (long long)selector) {
                return _buckets[i]._imp;
            }
        } while ((i = cache_next(i, _mask)) != begin);
        return NULL;
    }
};

struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
};

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
};

struct method_list_t : entsize_list_tt {
    method_t first;
};

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    uint32_t alignment_raw;
    uint32_t size;
};

struct ivar_list_t : entsize_list_tt {
    ivar_t first;
};

struct property_t {
    const char *name;
    const char *attributes;
};

struct property_list_t : entsize_list_tt {
    property_t first;
};

struct chained_property_list {
    chained_property_list *next;
    uint32_t count;
    property_t list[0];
};

typedef uintptr_t protocol_ref_t;
struct protocol_list_t {
    uintptr_t count;
    protocol_ref_t list[0];
};

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;  // instance对象占用的内存空间
#ifdef __LP64__
    uint32_t reserved;
#endif
    const uint8_t * ivarLayout;
    const char * name;  // 类名
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;  // 成员变量列表
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
};

struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;
    method_list_t * methods;    // 方法列表
    property_list_t *properties;    // 属性列表
    const protocol_list_t * protocols;  // 协议列表
    Class firstSubclass;
    Class nextSiblingClass;
    char *demangledName;
};

#define FAST_DATA_MASK          0x00007ffffffffff8UL
struct class_data_bits_t {
    uintptr_t bits;
public:
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
};

/* OC对象 */
struct mj_objc_object {
    void *isa;
};

/* 类对象 */
struct mj_objc_class : mj_objc_object {
    Class superclass;
    cache_t cache;
    class_data_bits_t bits;
public:
    class_rw_t* data() {
        return bits.data();
    }
    
    mj_objc_class* metaClass() {
        return (mj_objc_class *)((long long)isa & ISA_MASK);
    }
};

#endif /* MJClassInfo_h */
复制代码

转载于:https://juejin.im/post/5c84a096e51d4539d9564f4e

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值