iOS-浅谈OC中的Class对象

2019-05-16  本文已影响0人  晴天ccc

目录

  • Class对象
    ----class的结构
    ----class_rw_t的结构
    ----class_ro_t的结构结
    ---metho_t的结构
    ---Type Encoding
  • cache_t方法缓存
    ----方法调用的问题思考
    ----cache_t结构
    ----向缓存中存入方法
    ----从缓存中查找方法
    ----补充:散列表索引号计算
    ----实战拓展+方法/缓存方法调用过程解析
    ----方法查找过程

下面我们就通过查看objc源码,发现class对象底层为objc_class结构体。最终的Class的数据底层结构如下图:

在前面文章提到,OC对象有三种数据类型,里面就包含了Class对象。

下面是具体分析

struct objc_class : objc_object {
    // Class ISA;              // 继承来的isa指针
    Class superclass;          // 父类指针
    cache_t cache;             // 方法缓存 formerly cache pointer and vtable
    class_data_bits_t bits;    // 获取具体的类信息 class_rw_t * plus custom rr/alloc flags
    
    class_rw_t *data() const {
        return bits.data();
    }
    // .... more
    // .... more 
    // .... more 代码经过精简展示
}

从中我们看到Class结构体中包含了isa共用体superclass指针方法缓存cachebits

struct class_data_bits_t {
    friend objc_class;

    // Values are the FAST_ flags above.
    uintptr_t bits;
private:
    bool getBit(uintptr_t bit) const
    {
        return bits & bit;
    }

    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    // .... more 代码经过精简展示
};

由位运算知识点可知,bits保存了Class的一些信息,通过bits & FAST_DATA_MASK可以获取类的class_rw_t信息。

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint16_t witness;
#if SUPPORT_INDEXED_ISA
    uint16_t index;
#endif
    
    Class firstSubclass;
    Class nextSiblingClass;

    const class_ro_t *ro() const { }    // 里面内容暂时省略
    const method_array_t methods() const { }    // 里面内容暂时省略
    const property_array_t properties() const { }    // 里面内容暂时省略
    const protocol_array_t protocols() const { }    // 里面内容暂时省略
    // .... more 代码经过精简展示
};

class_rw_t包含了方法列表、属性列表、协议列表等信息。
还包含了一个class_ro_t结构体,里面保存了class的类名、成员变量等初始信息。

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif
    union {
        const uint8_t * ivarLayout;
        Class nonMetaclass;
    };
    explicit_atomic<const char *> name;
    void *baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
    // .... more 代码经过精简展示
};

实际上class_rw_t内部保存的是method_array_tproperty_array_tprotocol_array_t类型。和method_list_t*意义相同,是二维数组。

  • class_rw_t内部的methods、properties、protocols都是二维数组,是可读可写的,包含了类的初始内容、分类的内容。
  • 比如methods内部保存了method_list_t对象>method_list_t内部保存了真正的方法_method_t,类初始方法列列表会排在最后,后编译的分类中的方法会保存在methods前面。

class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一维数组,是只读的,包含了类的初始内容。

struct method_t {
    SEL name; // 函数名
    const char *types; // 编码(返回值类型、参数类型)
    IMP imp; //指向函数的指针()
};
  • IMP代表函数的具体实现
  • types包含了函数返回值、参数编码的字符串。
  • SEL代表方法\函数名,一般叫做选择器,底层结构跟char *类似
    可以通过@selector()和sel_registerName()获得。
    可以通过sel_getName()和NSStringFromSelector()转成字符串。
    不同类中相同名字的方法,所对应的方法选择器是相同的。

iOS中提供了一个叫做@encode的指令,可以将具体的类型表示成字符串编码,可以通过打印查看:

    NSLog(@"@encode(int) = %s", @encode(int));
    NSLog(@"@encode(void) = %s", @encode(void));
    NSLog(@"@encode(id) = %s", @encode(id));
    NSLog(@"@encode(SEL) = %s", @encode(SEL));
    @encode(int) = i
    @encode(void) = v
    @encode(id) = @
    @encode(SEL) = :

比如下面两个方法,test方法对应metho_t中的types值就是v16@0:8testAge:height:方法对应metho_t中的types值就是v24@0:8i16i20

// v16@0:8
- (void)test;

// v24@0:8i16i20
- (void)testAge:(int)age height:(int)height;

test方法默认会带有两个参数:

- (void)testSelf:(id)self _cmd:(SEL)_cmd;

也可以简写成v@:

方法缓存cache_t

Person *per =  [[Person alloc] init ];
[per run];
[per run];
  • instance对象的方法在类对象中存储着。class对象的方法在元类对象中存储着。
  • instance对象调用对象方法的轨迹:
    isa找到class,方法不存在,就通过superclass找父类。
  • class对象调用类方法的轨迹:
    isa找到meta-class,方法不存在,就通过superclass找父类的meta-class,
    如果基类中也不存在,则去基类的class对象中查找同名对象方法。

如果一直调用run方法,每次按照这种调用,效率是很低的。所以设计了一种cache结构来优化性能。

cache_t用散列表来缓存曾经调用过的方法,可以提高方法的查找速度,避免多次执行方法查找逻辑。

散列表(Hash table,也叫哈希表):是根据关键码值(Key value)而直接进行访问的数据结构。
它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

代码如下:

struct cache_t {
    bucket_t *_buckets; // 散列表
    mask_t _mask; // 散列长度-1
    mask_t _occupied; // 已缓存的方法数量
};

struct bucket_t {
    cache_key_t _key; // SEL作为key
    IMP _imp; // 函数内存地址
};
  • 例如散列表_buckets初始长度是10,那么_mask是9,_occupied可能是3。

按照上面思考的逻辑,第一次执行run方法的时候,系统会把run方法缓存到_buckets数组中,类型是bucket_t

key   =  @selector(run)
_img  =  run的地址
  • 在查找方法时,通过sel(方法名)作为key,找到对应的bucket_t,从而找到_imp(实现)进行调用。
  • 补充:SEL = @selector(run)
  • _buckets散列表(哈希)索引计算方法:sel & mask
  • 比如调用了方法@selector(run),需要先【计算出散列表索引】,再将bucket_t插入到索引位置。
  • _buckets其他位置如果没有信息会留空,是以空间换时间的方案。

举例:@selector(run) & _mask = 6
如果多个方法,同理在相应序号插入方法即可:@selector(eat) & _mask = 2则在序号2插入该方法即可。

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
    value ^= value >> 7;
#endif
    return (mask_t)(value & mask);
}

如果不同的方法通计算出来的索引值相同,就需要解决哈希冲突,苹果的方法时让索引-1:

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}
  • 写入:计算出来的索引位置已经保存了bucket_t,则让索引-1,如果还不行继续-1,索引小于0时,将索引设置成mask(_buckets长度-1)从最后的位置继续向前遍历,直到找到空位置,进行插入操作。
  • 读取:计算出来的索引位置对应的bucket_t中的SEL如果和传入的SEL不一致,就让索引-1查找,如果还不相同,则继续-1,索引小于0时将索引设置成mask(_buckets长度-1),从最后位置继续向前遍历,直到找到SEL相同的bucket_t。
  • 扩容:当_buckets空间不够时会进行扩容操作(原有空间大小*2),会更新mask的值,并且清空_buckets。

我们创建三个类:Person、Student、GoodStudent继承关系如下代码所示,分别实现方法:run、studentRun、goodStudentRun

#import <Foundation/Foundation.h>
@interface Person : NSObject
- (void)run;
@end

#import "Person.h"
@interface Student : Person
- (void)studentRun;
@end

#import "Student.h"
@interface GoodStudent : Student
- (void)goodStudentRun;
@end

Person *per =  [[Person alloc] init ];
[per run];
[per run];

方法/缓存方法调用过程:

  • 第一次调用run方法的时候,通过instance对象perisa指针找到Person类对象
  • 先在cache这个散列表中查找有无该方法缓存,通过sel(方法名)作为key,找到对应的bucket_t,结果肯定空的的,该方法不存在。
  • 然后通过bitsclass_rw_tproperties方法列表中查找run方法,结果找到了,进行方法执行。
  • 然后把相关信息规整成bucket_t,存入Person类对象cache散列表中。
  • 第二次调用run方法的时候,直接在Person类对象cache这个散列表中找到缓存方法并调用即可。
Student *stu =  [[Student alloc] init ];
[stu run];
[stu run];

子类调用父类方法的过程:

  • 调用过程和上面类似,instance对象stu在自己class方法也就是Student对象中查找有无run方法,结果是没有的。
  • 通过superclass指针去父类中进行查找,顺序是先查父类的cache,再查父类的properties
  • 因为上面[per run];执行过,所以在父类的cache中有缓存。
  • 在父类的cache中查找到run方法,直接调用执行。
  • 查然后在自己class对象的cache中也会缓存一份。
  • 当再此执行[stu run];的时候,直接在自己的class对象的cache中查找调用即可。

isasuperclass中说明了OC方法的调用轨迹,这里做一下补充,在查找过程中需要先查找方法缓存,过程如下:

上一篇下一篇

猜你喜欢

热点阅读