【iOS 底层原理】Runtime

runtime 介绍

Objective-C 是一门动态性比较强的编程语言,跟 C、C++ 等语言有着很大的不同,Objective-C 的动态性是由 Runtime API 来支撑的,Runtime API 提供的接口基本都是C语言的,源码由C\C++\汇编语言编写。

一、isa 本质

每个OC对象都含有一个 isa 指针,arm64 之前,isa仅仅是一个指针,保存着对象或类对象内存地址,在 arm64 架构之后,apple对isa进行了优化,变成了一个共用体(union)结构,同时使用位域来存储更多的信息。

image.png

所以 arm64 之后,isa 指针并不是直接指向类对象或者元类对象,而是需要 &ISA_MASK 通过位运算才能获取到类对象或者元类对象的地址。

1.背景知识介绍

如下,char 类型占据 8 字节,使用它的后三位来表示 tall rich handsome 三个 bool 值。例如 _tallRichHansome 的值为 0b 0000 0010 ,那么只使用8个二进制位中的最后3个,分别为其赋值0或者1来代表 tall、rich、handsome 的值。

@interface Person()
{
    char _tallRichHandsome;
}
image.png
取值

假如 char 类型的成员变量中存储的二进制为 0b 0000 0010 如果想将倒数第2位的值也就是 rich 的值取出来,可以使用&进行按位与运算进而取出相应位置的值。
&:按位与,同真为真,其他都为假。

// 示例
// 取出倒数第三位 tall
  0000 0010
& 0000 0100
------------
  0000 0000  // 取出倒数第三位的值为0,其他位都置为0

// 取出倒数第二位 rich
  0000 0010
& 0000 0010
------------
  0000 0010 // 取出倒数第二位的值为1,其他位都置为0

按位与可以用来取出特定的位,想取出哪一位就将那一位置为1,其他为都置为0,然后同原数据进行按位与计算,即可取出特定的位。

getter 方法改写如下

#define TallMask 0b00000100 // 4
#define RichMask 0b00000010 // 2
#define HandsomeMask 0b00000001 // 1

- (BOOL)tall
{
    return !!(_tallRichHandsome & TallMask);
}
- (BOOL)rich
{
    return !!(_tallRichHandsome & RichMask);
}
- (BOOL)handsome
{
    return !!(_tallRichHandsome & HandsomeMask);
}

上述代码中(_tallRichHandsome & TallMask)的值为0000 0010也就是2,但是我们需要的是一个BOOL类型的值 0 或者 1 ,那么!!2就将 2 先转化为 0 ,之后又转化为 1。相反如果按位与取得的值为 0 时,!!0将 0 先转化为 1 之后又转化为 0。
因此使用!!两个非操作将值转化为 0 或者 1 来表示相应的值。

掩码 : 上述代码中定义了三个宏,用来分别进行按位与运算而取出相应的值,一般用来按位与(&)运算的值称之为掩码。

上述宏定义可以使用<<(左移)优化成如下代码

#define TallMask (1<<2) // 0b00000100 4
#define RichMask (1<<1) // 0b00000010 2
#define HandsomeMask (1<<0) // 0b00000001 1
设值

设值即是将某一位设值为0或者1,可以使用|(按位或)操作符。 | : 按位或,只要有一个1即为1,否则为0。
如果想将某一位置为1的话,那么将原本的值与掩码进行按位或的操作即可,例如我们想将tall置为1

// 将倒数第三位 tall置为1
  0000 0010  // _tallRichHandsome
| 0000 0100  // TallMask
------------
  0000 0110 // 将tall置为1,其他位值都不变

如果想将某一位置为0的话,需要将掩码按位取反(~ : 按位取反符),之后在与原本的值进行按位与操作即可。

// 将倒数第二位 rich置为0
  0000 0010  // _tallRichHandsome
& 1111 1101  // RichMask按位取反
------------
  0000 0000 // 将rich置为0,其他位值都不变

set 代码改写如下

- (void)setTall:(BOOL)tall
{
    if (tall) { // 如果需要将值置为1  // 按位或掩码
        _tallRichHandsome |= TallMask;
    }else{ // 如果需要将值置为0 // 按位与(按位取反的掩码)
        _tallRichHandsome &= ~TallMask; 
    }
}
- (void)setRich:(BOOL)rich
{
    if (rich) {
        _tallRichHandsome |= RichMask;
    }else{
        _tallRichHandsome &= ~RichMask;
    }
}
- (void)setHandsome:(BOOL)handsome
{
    if (handsome) {
        _tallRichHandsome |= HandsomeMask;
    }else{
        _tallRichHandsome &= ~HandsomeMask;
    }
}
位域

将上述代码进行优化,使用结构体位域,可以使代码可读性更高。 位域声明 位域名 : 位域长度;

使用位域需要注意以下3点:

  1. 如果一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。
  2. 位域的长度不能大于数据类型本身的长度,比如int类型就不能超过32位二进位。
  3. 位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。

使用位域进行优化

@interface Person()
{
    struct {
        char handsome : 1; // 位域,代表占用一位空间
        char rich : 1;  // 按照顺序只占一位空间
        char tall : 1; 
    }_tallRichHandsome;
}
共用体

为了使代码存储数据高效率的同时,有较强的可读性,可以使用共用体来增强代码可读性,同时使用位运算来提高数据存取的效率。

#define TallMask (1<<2) // 0b00000100 4
#define RichMask (1<<1) // 0b00000010 2
#define HandsomeMask (1<<0) // 0b00000001 1

@interface Person()
{
    union {
        char bits;
       // 结构体仅仅是为了增强代码可读性,无实质用处
        struct {
            char tall : 1;
            char rich : 1;
            char handsome : 1;
        };
    }_tallRichHandsome;
}
@end

上述代码中使用位运算这种比较高效的方式存取值,使用union共用体来对数据进行存储。增加读取效率的同时增强代码可读性。
其中 _tallRichHandsome 共用体只占用一个字节,因为结构体中tall、rich、handsome都只占一位二进制空间,所以结构体只占一个字节,而char类型的bits也只占一个字节,他们都在共用体中,因此共用一个字节的内存即可。
其中_tallRichHandsome共用体只占用一个字节,因为结构体中tall、rich、handsome都只占一位二进制空间,所以结构体只占一个字节,而char类型的bits也只占一个字节,他们都在共用体中,因此共用一个字节的内存即可。

2.isa 源码分析

isa 源码如下

// 截取objc_object内部分代码
struct objc_object {
private:
    isa_t isa;
}

isa指针其实是一个isa_t类型的共用体,来到isa_t内部查看其结构

// 精简过的isa_t共用体
union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

#if SUPPORT_PACKED_ISA
# if __arm64__      
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    #       define RC_ONE   (1ULL<<45)
    #       define RC_HALF  (1ULL<<18)
    };

# elif __x86_64__     
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 8;
#       define RC_ONE   (1ULL<<56)
#       define RC_HALF  (1ULL<<7)
    };

# else
#   error unknown architecture for packed isa
# endif
#endif

isa_t 是 union 类型共同体。共用体中有一个结构体,结构体内部分别定义了一些变量,变量后面的值代表的是该变量占用多少个字节,也就是位域。
共用体:共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。

源码中通过共用体的形式存储了64位的值,这些值在结构体中被展示出来,通过对bits进行位运算而取出相应位置的值。

3.获取 Class、Meta-Class 信息

isa 位域中的 shiftcls 位中存储着 Class、Meta-Class 对象的内存地址信息,我们之前在 OC 对象的本质中提到过,对象的 isa 指针需要同 ISA_MASK 经过一次&(按位与)运算才能得出真正的 Class 对象地址。

image.png

ISA_MASK 的值为 0x0000000ffffffff8ULL,ISA_MASK 的值转化为二进制中有33位都为1。另外 ISA_MASK 最后三位的值为0,那么任何数同ISA_MASK按位与运算之后,得到的最后三位必定都为0,因此任何类对象或元类对象的内存地址最后三位必定为0,转化为十六进制末位必定为8或者0。

4.isa 中存储的信息及作用

struct {
    // 0代表普通的指针,存储着Class,Meta-Class对象的内存地址。
    // 1代表优化后的使用位域存储更多的信息。
    uintptr_t nonpointer        : 1; 

   // 是否有设置过关联对象,如果没有,释放时会更快
    uintptr_t has_assoc         : 1;

    // 是否有C++析构函数,如果没有,释放时会更快
    uintptr_t has_cxx_dtor      : 1;

    // 存储着Class、Meta-Class对象的内存地址信息
    uintptr_t shiftcls          : 33; 

    // 用于在调试时分辨对象是否未完成初始化
    uintptr_t magic             : 6;

    // 是否有被弱引用指向过。
    uintptr_t weakly_referenced : 1;

    // 对象是否正在释放
    uintptr_t deallocating      : 1;

    // 引用计数器是否过大无法存储在isa中
    // 如果为1,那么引用计数会存储在一个叫SideTable的类的属性中
    uintptr_t has_sidetable_rc  : 1;

    // 里面存储的值是引用计数器减1
    uintptr_t extra_rc          : 19;
};

注意:只要设置过关联对象或者弱引用引用过对象 has_assoc 和 weakly_referenced 的值就会变成1,不论之后是否将关联对象置为 nil 或断开弱引用。

如果没有设置过关联对象,对象释放时会更快,这是因为对象在销毁时会判断是否有关联对象进而对关联对象释放。参考对象销毁的源码:

void *objc_destructInstance(id obj) 
{
    if (obj) {
        Class isa = obj->getIsa();
        // 是否有c++析构函数
        if (isa->hasCxxDtor()) {
            object_cxxDestruct(obj);
        }
        // 是否有关联对象,如果有则移除
        if (isa->instancesHaveAssociatedObjects()) {
            _object_remove_assocations(obj);
        }
        objc_clear_deallocating(obj);
    }
    return obj;
}

二、class 的结构

class 内部结构如下:

struct objc_class : objc_object {
    // Class 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() { 
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
}
class_rw_t* data() {
    return (class_rw_t *)(bits & FAST_DATA_MASK);
}

image.png

1.class_rw_t

bits & FAST_DATA_MASK位运算之后,可以得到class_rw_t,而class_rw_t中存储着方法列表、属性列表以及协议列表,来看一下class_rw_t部分代码

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods; // 方法列表
    property_array_t properties; // 属性列表
    protocol_array_t protocols; // 协议列表

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
};

上述源码中,method_array_t、property_array_t、protocol_array_t其实都是二维数组,来到method_array_t、property_array_t、protocol_array_t内部看一下。这里以method_array_t为例,method_array_t本身就是一个数组,数组里面存放的是数组method_list_t,method_list_t里面最终存放的是method_t

class method_array_t : 
    public list_array_tt<method_t, method_list_t> 
{
    typedef list_array_tt<method_t, method_list_t> Super;

 public:
    method_list_t **beginCategoryMethodLists() {
        return beginLists();
    }
    
    method_list_t **endCategoryMethodLists(Class cls);

    method_array_t duplicate() {
        return Super::duplicate<method_array_t>();
    }
};


class property_array_t : 
    public list_array_tt<property_t, property_list_t> 
{
    typedef list_array_tt<property_t, property_list_t> Super;

 public:
    property_array_t duplicate() {
        return Super::duplicate<property_array_t>();
    }
};


class protocol_array_t : 
    public list_array_tt<protocol_ref_t, protocol_list_t> 
{
    typedef list_array_tt<protocol_ref_t, protocol_list_t> Super;

 public:
    protocol_array_t duplicate() {
        return Super::duplicate<protocol_array_t>();
    }
};

class_rw_t里面的methods、properties、protocols是二维数组,是可读可写的,其中包含了类的初始内容以及分类的内容。
以method_array_t为例,图示其中的结构。

image.png

image.png

2.class_ro_t

class_ro_t 中也有存储方法、属性、协议列表,另外还有成员变量列表。接着来看一下 class_ro_t 部分代码

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#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;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

class_ro_t *ro是只读的,内部直接存储的直接就是method_list_t、protocol_list_t 、property_list_t类型的一维数组,数组里面分别存放的是类的初始信息,以method_list_t为例,method_list_t中直接存放的就是method_t,但是是只读的,不允许增加删除修改。

image.png

注意:class_ro_t 中包含 ivar_list_t,但 class_rw_t 中不包含,说明不允许动态添加成员变量,初始化时就确定了成员变量。但 property_list_t 可以。

3.class_ro_t 与 class_rw_t 的合并

class_rw_t中的methods是二维数组的结构,并且可读可写,因此可以动态的添加方法,并且更加便于分类方法的添加。参考 category 源码可以发现,attachList函数内通过memmove 和 memcpy两个操作将分类的方法列表合并在本类的方法列表中。那么此时就将分类的方法和本类的方法统一整合到一起了。

其实一开始类的方法,属性,成员变量属性协议等等都是存放在class_ro_t中的,当程序运行的时候,需要将分类中的列表跟类初始的列表合并在一起的时,就会将class_ro_t中的列表和分类中的列表合并起来存放在class_rw_t中,也就是说class_rw_t中有部分列表是从class_ro_t里面拿出来的。

运行时调用的 realizeClass 部分源码如下:

static Class realizeClass(Class cls)
{
    runtimeLock.assertWriting();

    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    if (!cls) return nil;
    if (cls->isRealized()) return cls;
    assert(cls == remapClass(cls));

    // 最开始cls->data是指向ro的
    ro = (const class_ro_t *)cls->data();

    if (ro->flags & RO_FUTURE) { 
        // rw已经初始化并且分配内存空间
        rw = cls->data();  // cls->data指向rw
        ro = cls->data()->ro;  // cls->data()->ro指向ro
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else { 
        // 如果rw并不存在,则为rw分配空间
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1); // 分配空间
        rw->ro = ro;  // rw->ro重新指向ro
        rw->flags = RW_REALIZED|RW_REALIZING;
        // 将rw传入setData函数,等于cls->data()重新指向rw
        cls->setData(rw); 
    }
}

类的初始信息本来其实是存储在class_ro_t中的,并且ro本来是指向cls->data()的,也就是说bits.data()得到的是ro,但是在运行过程中创建了class_rw_t,并将cls->data指向rw,同时将初始信息ro赋值给rw中的ro。最后在通过setData(rw)设置data。那么此时bits.data()得到的就是rw,之后再去检查是否有分类,同时将分类的方法,属性,协议列表整合存储在class_rw_t的方法,属性及协议列表中。

4.class_rw_t 中是方法相关数据结构

method_t

method_array_t中最终存储的是method_t,method_t是对方法、函数的封装,每一个方法对象就是一个method_t。通过源码看一下method_t的结构体

struct method_t {
    SEL name;  // 函数名
    const char *types;  // 编码(返回值类型,参数类型)
    IMP imp; // 指向函数的指针(函数地址)
};

image.png
SEL

SEL代表方法\函数名,一般叫做选择器,底层结构跟char *类似 typedef struct objc_selector *SEL;,可以把SEL看做是方法名字符串。

SEL可以通过@selector()和sel_registerName()获得

SEL sel1 = @selector(test);
SEL sel2 = sel_registerName("test");

或者

char *string = sel_getName(sel1);
NSString *string2 = NSStringFromSelector(sel2);

注:不同类中相同名字的方法,所对应的方法选择器是相同的。SEL仅仅代表方法的名字,并且不同类中相同的方法名的SEL是全局唯一的。

NSLog(@"%p,%p", sel1,sel2);
Runtime-test[23738:8888825] 0x1017718a3,0x1017718a3
types

types包含了函数返回值,参数编码的字符串。通过字符串拼接的方式将返回值和参数拼接成一个字符串,来代表函数返回值及参数。

苹果官方对 types 的描述如下


image.png

举例说明:v16@0:8

- (void) test;

 v    16      @     0     :     8
void         id          SEL
// 16表示参数的占用空间大小,id后面跟的0表示从0位开始存储,id占8位空间。
// SEL后面的8表示从第8位开始存储,SEL同样占8位空间

方法都默认有两个参数的,id类型的self,和SEL类型的_cmd,而上述通过对types的分析同时也验证了这个说法。

复杂例子:

- (int)testWithAge:(int)age Height:(float)height
{
    return 0;
}
  i    24    @    0    :    8    i    16    f    20
int         id        SEL       int        float
// 参数的总占用空间为 8 + 8 + 4 + 4 = 24
// id 从第0位开始占据8位空间
// SEL 从第8位开始占据8位空间
// int 从第16位开始占据4位空间
// float 从第20位开始占据4位空间

iOS提供了@encode的指令,可以将具体的类型转化成字符串编码。

NSLog(@"%s",@encode(int));
NSLog(@"%s",@encode(float));
NSLog(@"%s",@encode(id));
NSLog(@"%s",@encode(SEL));

// 打印内容
Runtime-test[25275:9144176] i
Runtime-test[25275:9144176] f
Runtime-test[25275:9144176] @
Runtime-test[25275:9144176] :
IMP

IMP代表函数的具体实现,存储的内容是函数地址。也就是说当找到imp的时候就可以找到函数实现,进而对函数进行调用。

5.方法缓存机制 cache_t

回到类对象结构体,成员变量cache就是用来对方法进行缓存的。

struct objc_class : objc_object {
    // Class 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() { 
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
}
image.png

cache_t cache;用来缓存曾经调用过的方法,可以提高方法的查找速度。

回顾方法调用过程:调用方法的时候,需要去方法列表里面进行遍历查找。如果方法不在列表里面,就会通过superclass找到父类的类对象,在去父类类对象方法列表里面遍历查找。

如果方法需要调用很多次的话,那就相当于每次调用都需要去遍历多次方法列表,为了能够快速查找方法,apple设计了cache_t来进行方法缓存。

每当调用方法的时候,会先去cache中查找是否有缓存的方法,如果没有缓存,在去类对象方法列表中查找,以此类推直到找到方法之后,就会将方法直接存储在cache中,下一次在调用这个方法的时候,就会在类对象的cache里面找到这个方法,直接调用了。

cache_t 如何进行缓存(cache_t 内部结构)
struct cache_t {
    struct bucket_t *_buckets; // 散列表 数组
    mask_t _mask; // 散列表的长度 -1
    mask_t _occupied; // 已经缓存的方法数量
};

bucket_t是以数组的方式存储方法列表的,看一下bucket_t内部结构

struct bucket_t {
private:
    cache_key_t _key; // SEL作为Key
    IMP _imp; // 函数的内存地址
};

bucket_t中存储着SEL和_imp,通过key->value的形式,以SEL为key,函数实现的内存地址 _imp为value来存储方法。

image.png

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

散列函数及散列表原理

cache_fill 及 cache_fill_nolock 函数

void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
    mutex_locker_t lock(cacheUpdateLock);
    cache_fill_nolock(cls, sel, imp, receiver);
#else
    _collecting_in_critical();
    return;
#endif
}

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();
    // 如果没有initialize直接return
    if (!cls->isInitialized()) return;
    // 确保线程安全,没有其他线程添加缓存
    if (cache_getImp(cls, sel)) return;
    // 通过类对象获取到cache 
    cache_t *cache = getCache(cls);
    // 将SEL包装成Key
    cache_key_t key = getKey(sel);
   // 占用空间+1
    mask_t newOccupied = cache->occupied() + 1;
   // 获取缓存列表的缓存能力,能存储多少个键值对
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // 如果为空的,则创建空间,这里创建的空间为4个。
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // 如果所占用的空间占总数的3/4一下,则继续使用现在的空间
    }
    else {
       // 如果占用空间超过3/4则扩展空间
        cache->expand();
    }
    // 通过key查找合适的存储空间。
    bucket_t *bucket = cache->find(key, receiver);
    // 如果key==0则说明之前未存储过这个key,占用空间+1
    if (bucket->key() == 0) cache->incrementOccupied();
    // 存储key,imp 
    bucket->set(key, imp);
}
reallocate 函数

通过上述源码看到reallocate函数负责分配散列表空间,来到reallocate函数内部。

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    // 旧的散列表能否被释放
    bool freeOld = canBeFreed();
    // 获取旧的散列表
    bucket_t *oldBuckets = buckets();
    // 通过新的空间需求量创建新的散列表
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    assert(newCapacity > 0);
    assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
    // 设置Buckets和Mash,Mask的值为散列表长度-1
    setBucketsAndMask(newBuckets, newCapacity - 1);
    // 释放旧的散列表
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}

上述源码中首次传入reallocate函数的newCapacity为INIT_CACHE_SIZE,INIT_CACHE_SIZE是个枚举值,也就是4。因此散列表最初创建的空间就是4个。

expand ()函数

当散列表的空间被占用超过3/4的时候,散列表会调用expand ()函数进行扩展,我们来看一下expand ()函数内散列表如何进行扩展的。

void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    // 获取旧的散列表的存储空间
    uint32_t oldCapacity = capacity();
    // 将旧的散列表存储空间扩容至两倍
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
    // 为新的存储空间赋值
    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        newCapacity = oldCapacity;
    }
    // 调用reallocate函数,重新创建存储空间
    reallocate(oldCapacity, newCapacity);
}

上述源码中可以发现散列表进行扩容时会将容量增至之前的2倍。

find 函数

最后来看一下散列表中如何快速的通过key找到相应的bucket呢?我们来到find函数内部

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    assert(k != 0);
    // 获取散列表
    bucket_t *b = buckets();
    // 获取mask
    mask_t m = mask();
    // 通过key找到key在散列表中存储的下标
    mask_t begin = cache_hash(k, m);
    // 将下标赋值给i
    mask_t i = begin;
    // 如果下标i中存储的bucket的key==0说明当前没有存储相应的key,将b[i]返回出去进行存储
    // 如果下标i中存储的bucket的key==k,说明当前空间内已经存储了相应key,将b[i]返回出去进行存储
    do {
        if (b[i].key() == 0  ||  b[i].key() == k) {
            // 如果满足条件则直接reutrn出去
            return &b[i];
        }
    // 如果走到这里说明上面不满足,那么会往前移动一个空间重新进行判定,知道可以成功return为止
    } while ((i = cache_next(i, m)) != begin);

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}

函数cache_hash (k, m)用来通过key找到方法在散列表中存储的下标,来到cache_hash (k, m)函数内部

static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask);
}

可以发现cache_hash (k, m)函数内部仅仅是进行了key & mask的按位与运算,得到下标即存储在相应的位置上

_mask

通过上面的分析我们知道_mask的值是散列表的长度减一,那么任何数通过与_mask进行按位与运算之后获得的值都会小于等于_mask,因此不会出现数组溢出的情况。

总结

当第一次使用方法时,消息机制通过isa找到方法之后,会对方法以SEL为keyIMP为value的方式缓存在cache的_buckets中,当第一次存储的时候,会创建具有4个空间的散列表,并将_mask的值置为散列表的长度减一,之后通过SEL & mask计算出方法存储的下标值,并将方法存储在散列表中。举个例子,如果计算出下标值为3,那么就将方法直接存储在下标为3的空间中,前面的空间会留空。

当散列表中存储的方法占据散列表长度超过3/4的时候,散列表会进行扩容操作,将创建一个新的散列表并且空间扩容至原来空间的两倍,并重置_mask的值,最后释放旧的散列表,此时再有方法要进行缓存的话,就需要重新通过SEL & mask计算出下标值之后在按照下标进行存储了。

如果一个类中方法很多,其中很可能会出现多个方法的SEL & mask得到的值为同一个下标值,那么会调用cache_next函数往下标值-1位去进行存储,如果下标值-1位空间中有存储方法,并且key不与要存储的key相同,那么再到前面一位进行比较,直到找到一位空间没有存储方法或者key与要存储的key相同为止,如果到下标0的话就会到下标为_mask的空间也就是最大空间处进行比较。

当要查找方法时,并不需要遍历散列表,同样通过SEL & mask计算出下标值,直接去下标值的空间取值即可,同上,如果下标值中存储的key与要查找的key不相同,就去前面一位查找。这样虽然占用了少量控件,但是大大节省了时间,也就是说其实apple是使用空间换取了存取的时间。

通过一张图更清晰的看一下其中的流程。

image.png

三、方法调用本质

OC 代码转换为 C++ 代码,探寻方法调用的本质

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
[person test];
//  --------- c++底层代码
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("test"));

方法调用分为3个阶段

  • 消息发送阶段:负责从类及父类的缓存列表及方法列表查找方法。
  • 动态解析阶段:如果消息发送阶段没有找到方法,则会进入动态解析阶段,负责动态的添加方法实现。
  • 消息转发阶段:如果也没有实现动态解析方法,则会进行消息转发阶段,将消息转发给可以处理消息的接受者来处理。

如果消息转发也没有实现,就会报方法找不到的错误,无法识别消息,unrecognzied selector sent to instance

msg_send 源码流程:


image.png

1.消息发送

runtime 中消息发送的代码是闭源的,只能通过汇编去查看,在 runtime 源码中搜索 objc_msgSend 查看其内部实现,在 objc-msg-arm64.s 汇编文件可以知道 objc_msgSend 函数的实现

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    MESSENGER_START

    cmp x0, #0          // nil check and tagged pointer check
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
    ldr x13, [x0]       // x13 = isa
    and x16, x13, #ISA_MASK // x16 = class  
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

上述汇编源码中会首先判断消息接受者reveiver的值。 如果传入的消息接受者为nil则会执行LNilOrTagged,LNilOrTagged内部会执行LReturnZero,而LReturnZero内部则直接return0。

如果传入的消息接受者不为nill则执行CacheLookup,内部对方法缓存列表进行查找,如果找到则执行CacheHit,进而调用方法。否则执行CheckMiss,CheckMiss内部调用__objc_msgSend_uncached。

__objc_msgSend_uncached内会执行MethodTableLookup也就是方法列表查找,MethodTableLookup内部的核心代码__class_lookupMethodAndLoadCache3也就是c语言函数_class_lookupMethodAndLoadCache3

c语言_class_lookupMethodAndLoadCache3函数内部则是对方法查找的核心源代码。

image.png
_class_lookupMethodAndLoadCache3 函数
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
lookUpImpOrForward 函数
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    // initialize = YES , cache = NO , resolver = YES
    IMP imp = nil;
    bool triedResolver = NO;
    runtimeLock.assertUnlocked();

    // 缓存查找, 因为cache传入的为NO, 这里不会进行缓存查找, 因为在汇编语言中CacheLookup已经查找过
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    runtimeLock.read();
    if (!cls->isRealized()) {
        runtimeLock.unlockRead();
        runtimeLock.write();
        realizeClass(cls);
        runtimeLock.unlockWrite();
        runtimeLock.read();
    }
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
    }

 retry:    
    runtimeLock.assertReading();

    // 防止动态添加方法,缓存会变化,再次查找缓存。
    imp = cache_getImp(cls, sel);
    // 如果查找到imp, 直接调用done, 返回方法地址
    if (imp) goto done;

    // 查找方法列表, 传入类对象和方法名
    {
        // 根据sel去类对象里面查找方法
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            // 如果方法存在,则缓存方法,
            // 内部调用的就是 cache_fill 上文中已经详细讲解过这个方法,这里不在赘述了。
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            // 方法缓存之后, 取出imp, 调用done返回imp
            imp = meth->imp;
            goto done;
        }
    }

    // 如果类方法列表中没有找到, 则去父类的缓存中或方法列表中查找方法
    {
        unsigned attempts = unreasonableClassCount();
        // 如果父类缓存列表及方法列表均找不到方法,则去父类的父类去查找。
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // 查找父类的缓存
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // 在父类中找到方法, 在本类中缓存方法, 注意这里传入的是cls, 将方法缓存在本类缓存列表中, 而非父类中
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    // 执行done, 返回imp
                    goto done;
                }
                else {
                    // 跳出循环, 停止搜索
                    break;
                }
            }
            
            // 查找父类的方法列表
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                // 同样拿到方法, 在本类进行缓存
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                // 执行done, 返回imp
                goto done;
            }
        }
    }
    
    // ---------------- 消息发送阶段完成 ---------------------

    // ---------------- 进入动态解析阶段 ---------------------
    // 上述列表中都没有找到方法实现, 则尝试解析方法
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        triedResolver = YES;
        goto retry;
    }

    // ---------------- 动态解析阶段完成 ---------------------

    // ---------------- 进入消息转发阶段 ---------------------
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();
    // 返回方法地址
    return imp;
}
getMethodNoSuper_nolock 函数

方法列表中查找方法

getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();
    assert(cls->isRealized());
    // cls->data() 得到的是 class_rw_t
    // class_rw_t->methods 得到的是methods二维数组
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
         // mlists 为 method_list_t
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }
    return nil;
}

上述源码中getMethodNoSuper_nolock函数中通过遍历方法列表拿到method_list_t最终通过search_method_list函数查找方法

search_method_list函数
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    // 如果方法列表是有序的,则使用二分法查找方法,节省时间
    if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // 否则则遍历列表查找
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }
    return nil;
}
findMethodInSortedMethodList函数内二分查找实现原理
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    assert(list);

    const method_t * const first = &list->first;
    const method_t *base = first;
    const method_t *probe;
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    // >>1 表示将变量n的各个二进制位顺序右移1位,最高位补二进制0。
    // count >>= 1 如果count为偶数则值变为(count / 2)。如果count为奇数则值变为(count-1) / 2 
    for (count = list->count; count != 0; count >>= 1) {
        // probe 指向数组中间的值
        probe = base + (count >> 1);
        // 取出中间method_t的name,也就是SEL
        uintptr_t probeValue = (uintptr_t)probe->name;
        if (keyValue == probeValue) {
            // 取出 probe
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
           // 返回方法
            return (method_t *)probe;
        }
        // 如果keyValue > probeValue 则折半向后查询
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}
_class_lookupMethodAndLoadCache3函数内部消息发送的整个流程
image.png

如果消息发送阶段没有找到方法,就会进入动态解析方法阶段。

image.png

2.动态解析阶段

当本类包括父类cache包括class_rw_t中都找不到方法时,就会进入动态方法解析阶段。我们来看一下动态解析阶段源码。

动态解析的方法

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

_class_resolveMethod函数内部,根据类对象或元类对象做不同的操作

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

上述代码中可以发现,动态解析方法之后,会将triedResolver = YES;那么下次就不会在进行动态解析阶段了,之后会重新执行retry,会重新对方法查找一遍。也就是说无论我们是否实现动态解析方法,无论动态解析方法是否成功,retry之后都不会在进行动态的解析方法了。

动态解析流程图


image.png

3.消息转发

如果我们自己也没有对方法进行动态的解析,那么就会进行消息转发

imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);

自己没有能力处理这个消息的时候,就会进行消息转发阶段,会调用_objc_msgForward_impcache函数。
通过搜索可以在汇编中找到__objc_msgForward_impcache函数实现,__objc_msgForward_impcache函数中调用__objc_msgForward进而找到__objc_forward_handler。

objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

我们发现这仅仅是一个错误信息的输出。 其实消息转发机制是不开源的,但是我们可以猜测其中可能拿返回的对象调用了objc_msgSend,重走了一遍消息发送,动态解析,消息转发的过程。最终找到方法进行调用。

消息转发流程


image.png
image.png

4.使用动态解析/转发机制

动态解析

动态解析对象方法时,会调用+(BOOL)resolveInstanceMethod:(SEL)sel方法。 动态解析类方法时,会调用+(BOOL)resolveClassMethod:(SEL)sel方法。

这里以实例对象为例通过代码来看一下动态解析的过程

@implementation Person
- (void) other {
    NSLog(@"%s", __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // 动态的添加方法实现
    if (sel == @selector(test)) {
        // 获取其他方法 指向method_t的指针
        Method otherMethod = class_getInstanceMethod(self, @selector(other));
        
        // 动态添加test方法的实现
        class_addMethod(self, sel, method_getImplementation(otherMethod), method_getTypeEncoding(otherMethod));
        
        // 返回YES表示有动态添加方法
        return YES;
    }
    
    NSLog(@"%s", __func__);
    return [super resolveInstanceMethod:sel];
}

@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person test];
    }
    return 0;
}
// 打印结果
// -[Person other]

在resolveInstanceMethod:方法内部使用class_addMethod动态的添加方法实现。

这里需要注意class_addMethod用来向具有给定名称和实现的类添加新方法,class_addMethod将添加一个方法实现的覆盖,但是不会替换已有的实现。也就是说如果上述代码中已经实现了-(void)test方法,则不会再动态添加方法,这点在上述源码中也可以体现,因为一旦找到方法实现就直接return imp并调用方法了,不会再执行动态解析方法了。

class_addMethod 函数

    /** 
     第一个参数: cls:给哪个类添加方法
     第二个参数: SEL name:添加方法的名称
     第三个参数: IMP imp: 方法的实现,函数入口,函数名可与方法名不同(建议与方法名相同)
     第四个参数: types :方法类型,需要用特定符号,参考API
     */
class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)

class_getInstanceMethod 函数

// 获取其他方法 指向method_t的指针
Method otherMethod = class_getInstanceMethod(self, @selector(other));

总结

image.png

消息转发

当本类没有实现方法,并且没有动态解析方法,就会调用forwardingTargetForSelector函数,进行消息转发,我们可以实现forwardingTargetForSelector函数,在其内部将消息转发给可以实现此方法的对象。

如果forwardingTargetForSelector函数返回为nil或者没有实现的话,就会调用methodSignatureForSelector方法,用来返回一个方法签名,这也是我们正确跳转方法的最后机会。

如果methodSignatureForSelector方法返回正确的方法签名就会调用forwardInvocation方法,forwardInvocation方法内提供一个NSInvocation类型的参数,NSInvocation封装了一个方法的调用,包括方法的调用者,方法名,以及方法的参数。在forwardInvocation函数内修改方法调用对象即可。

如果methodSignatureForSelector返回的为nil,就会来到doseNotRecognizeSelector:方法内部,程序crash提示无法识别选择器unrecognized selector sent to instance。

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    // 返回能够处理消息的对象
    if (aSelector == @selector(driving)) {
        // 返回nil则会调用methodSignatureForSelector方法
        return nil; 
        // return [[Car alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

// 方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(driving)) {
       // return [NSMethodSignature signatureWithObjCTypes: "v@:"];
       // return [NSMethodSignature signatureWithObjCTypes: "v16@0:8"];
       // 也可以通过调用Car的methodSignatureForSelector方法得到方法签名,这种方式需要car对象有aSelector方法
        return [[[Car alloc] init] methodSignatureForSelector: aSelector];

    }
    return [super methodSignatureForSelector:aSelector];
}

//NSInvocation 封装了一个方法调用,包括:方法调用者,方法,方法的参数
//    anInvocation.target 方法调用者
//    anInvocation.selector 方法名
//    [anInvocation getArgument: NULL atIndex: 0]; 获得参数
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
//   anInvocation中封装了methodSignatureForSelector函数中返回的方法。
//   此时anInvocation.target 还是person对象,我们需要修改target为可以执行方法的方法调用者。
//   anInvocation.target = [[Car alloc] init];
//   [anInvocation invoke];
    [anInvocation invokeWithTarget: [[Car alloc] init]];
}

// 打印内容
// 消息转发[5781:2164454] car driving

NSInvocation

methodSignatureForSelector方法中返回的方法签名,在forwardInvocation中被包装成NSInvocation对象,NSInvocation提供了获取和修改方法名、参数、返回值等方法,也就是说,在forwardInvocation函数中我们可以对方法进行最后的修改。

生成NSMethodSignature

image.png

四、super本质

1.举例

#import "Student.h"
@implementation Student
- (instancetype)init
{
    if (self = [super init]) {
        NSLog(@"[self class] = %@", [self class]);
        NSLog(@"[self superclass] = %@", [self superclass]);
        NSLog(@"----------------");
        NSLog(@"[super class] = %@", [super class]);
        NSLog(@"[super superclass] = %@", [super superclass]);

    }
    return self;
}
@end

输出内容

Runtime-super[6601:1536402] [self class] = Student
Runtime-super[6601:1536402] [self superclass] = Person
Runtime-super[6601:1536402] ----------------
Runtime-super[6601:1536402] [super class] = Student
Runtime-super[6601:1536402] [super superclass] = Person

上述代码中可以发现无论是self还是super调用class或superclass的结果都是相同的。

[super run]; 转化为底层源码内部其实调用的是 objc_msgSendSuper 函数。

objc_msgSendSuper函数内传递了两个参数。__rw_objc_super结构体和sel_registerName("run")方法名。

__rw_objc_super结构体内传入的参数是self和class_getSuperclass(objc_getClass("Student"))也就是Student的父类Person

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);

objc_msgSendSuper中传入的结构体是objc_super,我们来到objc_super内部查看其内部结构。 我们通过源码查找objc_super结构体查看其内部结构。

// 精简后的objc_super结构体
struct objc_super {
    __unsafe_unretained _Nonnull id receiver; // 消息接受者
    __unsafe_unretained _Nonnull Class super_class; // 消息接受者的父类
    /* super_class is the first class to search */ 
    // 父类是第一个开始查找的类
};

从objc_super结构体中可以发现receiver消息接受者仍然为self,superclass仅仅是用来告知消息查找从哪一个类开始。从父类的类对象开始去查找。

image.png

super调用方法的消息接受者receiver仍然是self,只是从父类的类对象开始去查找方法。

class的底层实现如下面代码所示

+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}
+ (Class)superclass {
    return self->superclass;
}

- (Class)superclass {
    return [self class]->superclass;
}

class内部实现是根据消息接受者返回其对应的类对象,最终会找到基类的方法列表中,而self和super的区别仅仅是self从本类类对象开始查找方法,super从父类类对象开始查找方法,因此最终得到的结果都是相同的。

2.objc_msgSendSuper2 函数

super 底层真正调用的函数时 objc_msgSendSuper2 函数


image.png

3.isKindOfClass 与 isMemberOfClass

isKindOfClass isKindOfClass对象方法底层实现

- (BOOL)isMemberOfClass:(Class)cls {
   // 直接获取实例类对象并判断是否等于传入的类对象
    return [self class] == cls;
}

- (BOOL)isKindOfClass:(Class)cls {
   // 向上查询,如果找到父类对象等于传入的类对象则返回YES
   // 直到基类还不相等则返回NO
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

isMemberOfClass 判断左边是否刚好等于右边类型。 isKindOfClass 判断左边或者左边类型的父类是否刚好等于右边类型。 注意:类方法内部是获取其元类对象进行比较

五、runtime API

1.类相关API

1. 动态创建一个类(参数:父类,类名,额外的内存空间)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)

2. 注册一个类(要在类注册之前添加成员变量)
void objc_registerClassPair(Class cls) 

3. 销毁一个类
void objc_disposeClassPair(Class cls)

示例:
void run(id self , SEL _cmd) {
    NSLog(@"%@ - %@", self,NSStringFromSelector(_cmd));
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 创建类 superclass:继承自哪个类 name:类名 size_t:格外的大小,创建类是否需要扩充空间
        // 返回一个类对象
        Class newClass = objc_allocateClassPair([NSObject class], "Student", 0);
        
        // 添加成员变量 
        // cls:添加成员变量的类 name:成员变量的名字 size:占据多少字节 alignment:内存对齐,最好写1 types:类型,int类型就是@encode(int) 也就是i
        class_addIvar(newClass, "_age", 4, 1, @encode(int));
        class_addIvar(newClass, "_height", 4, 1, @encode(float));
        
        // 添加方法
        class_addMethod(newClass, @selector(run), (IMP)run, "v@:");
        
        // 注册类
        objc_registerClassPair(newClass);
        
        // 创建实例对象
        id student = [[newClass alloc] init];
    
        // 通过KVC访问
        [student setValue:@10 forKey:@"_age"];
        [student setValue:@180.5 forKey:@"_height"];
        
        // 获取成员变量
        NSLog(@"_age = %@ , _height = %@",[student valueForKey:@"_age"], [student valueForKey:@"_height"]);
        
        // 获取类的占用空间
        NSLog(@"类对象占用空间%zd", class_getInstanceSize(newClass));
        
        // 调用动态添加的方法
        [student run];
        
    }
    return 0;
}

// 打印内容
// Runtime应用[25605:4723961] _age = 10 , _height = 180.5
// Runtime应用[25605:4723961] 类对象占用空间16
// Runtime应用[25605:4723961] <Student: 0x10072e420> - run

注意
类一旦注册完毕,就相当于类对象和元类对象里面的结构就已经创建好了。
因此必须在注册类之前,添加成员变量。方法可以在注册之后再添加,因为方法是可以动态添加的。
创建的类如果不需要使用了 ,需要释放类。

4. 获取isa指向的Class,如果将类对象传入获取的就是元类对象,如果是实例对象则为类对象
Class object_getClass(id obj)

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        NSLog(@"%p,%p,%p",object_getClass(person), [Person class],
              object_getClass([Person class]));
    }
    return 0;
}
// 打印内容
Runtime应用[21115:3807804] 0x100001298,0x100001298,0x100001270

5. 设置isa指向的Class,可以动态的修改类型。例如修改了person对象的类型,也就是说修改了person对象的isa指针的指向,中途让对象去调用其他类的同名方法。
Class object_setClass(id obj, Class cls)

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person run];
        
        object_setClass(person, [Car class]);
        [person run];
    }
    return 0;
}
// 打印内容
Runtime应用[21147:3815155] -[Person run]
Runtime应用[21147:3815155] -[Car run]
最终其实调用了car的run方法

6. 用于判断一个OC对象是否为Class
BOOL object_isClass(id obj)

// 判断OC对象是实例对象还是类对象
NSLog(@"%d",object_isClass(person)); // 0
NSLog(@"%d",object_isClass([person class])); // 1
NSLog(@"%d",object_isClass(object_getClass([person class]))); // 1 
// 元类对象也是特殊的类对象

7. 判断一个Class是否为元类
BOOL class_isMetaClass(Class cls)
8. 获取类对象父类
Class class_getSuperclass(Class cls)

2.成员变量 API

1. 获取一个实例变量信息,描述信息变量的名字,占用多少字节等
Ivar class_getInstanceVariable(Class cls, const char *name)

2. 拷贝实例变量列表(最后需要调用free释放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)

3. 设置和获取成员变量的值
void object_setIvar(id obj, Ivar ivar, id value)
id object_getIvar(id obj, Ivar ivar)

4. 动态添加成员变量(已经注册的类是不能动态添加成员变量的)
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)

5. 获取成员变量的相关信息,传入成员变量信息,返回C语言字符串
const char *ivar_getName(Ivar v)
6. 获取成员变量的编码,types
const char *ivar_getTypeEncoding(Ivar v)

示例:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 获取成员变量的信息
        Ivar nameIvar = class_getInstanceVariable([Person class], "_name");
        // 获取成员变量的名字和编码
        NSLog(@"%s, %s", ivar_getName(nameIvar), ivar_getTypeEncoding(nameIvar));
        
        Person *person = [[Person alloc] init];
        // 设置和获取成员变量的值
        object_setIvar(person, nameIvar, @"xx_cc");
        // 获取成员变量的值
        object_getIvar(person, nameIvar);
        NSLog(@"%@", object_getIvar(person, nameIvar));
        NSLog(@"%@", person.name);
        
        // 拷贝实例变量列表
        unsigned int count ;
        Ivar *ivars = class_copyIvarList([Person class], &count);

        for (int i = 0; i < count; i ++) {
            // 取出成员变量
            Ivar ivar = ivars[i];
            NSLog(@"%s, %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
        }
        
        free(ivars);

    }
    return 0;
}

// 打印内容
// Runtime应用[25783:4778679] _name, @"NSString"
// Runtime应用[25783:4778679] xx_cc
// Runtime应用[25783:4778679] xx_cc
// Runtime应用[25783:4778679] _name, @"NSString"

3.属性 API

1. 获取一个属性
objc_property_t class_getProperty(Class cls, const char *name)

2. 拷贝属性列表(最后需要调用free释放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

3. 动态添加属性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                  unsigned int attributeCount)

4. 动态替换属性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                      unsigned int attributeCount)

5. 获取属性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)

4.方法 API

1. 获得一个实例方法、类方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)

2. 方法实现相关操作
IMP class_getMethodImplementation(Class cls, SEL name) 
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2) 

3. 拷贝方法列表(最后需要调用free释放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)

4. 动态添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

5. 动态替换方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)

6. 获取方法的相关信息(带有copy的需要调用free去释放)
SEL method_getName(Method m)
IMP method_getImplementation(Method m)
const char *method_getTypeEncoding(Method m)
unsigned int method_getNumberOfArguments(Method m)
char *method_copyReturnType(Method m)
char *method_copyArgumentType(Method m, unsigned int index)

7. 选择器相关
const char *sel_getName(SEL sel)
SEL sel_registerName(const char *str)

8. 用block作为方法实现
IMP imp_implementationWithBlock(id block)
id imp_getBlock(IMP anImp)
BOOL imp_removeBlock(IMP anImp)

六、runtime 应用

1.设置UITextField占位文字的颜色

image.png

2.字典转模型

  • 利用Runtime遍历所有的属性或者成员变量
  • 利用KVC设值

3.替换方法实现

  • class_replaceMethod
  • method_exchangeImplementations

七、面试题

1.下列代码中Person继承自NSObject,Student继承自Person,写出下列代码输出内容。

#import "Student.h"
@implementation Student
- (instancetype)init
{
    if (self = [super init]) {
        NSLog(@"[self class] = %@", [self class]);
        NSLog(@"[self superclass] = %@", [self superclass]);
        NSLog(@"----------------");
        NSLog(@"[super class] = %@", [super class]);
        NSLog(@"[super superclass] = %@", [super superclass]);

    }
    return self;
}
@end

输出内容

Runtime-super[6601:1536402] [self class] = Student
Runtime-super[6601:1536402] [self superclass] = Person
Runtime-super[6601:1536402] ----------------
Runtime-super[6601:1536402] [super class] = Student
Runtime-super[6601:1536402] [super superclass] = Person

上述代码中可以发现无论是self还是super调用class或superclass的结果都是相同的。

2.讲一下 OC 的消息机制

OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)
objc_msgSend底层有3大阶段
消息发送(当前类、父类中查找)、动态方法解析、消息转发

3.具体应用

  • 利用关联对象(AssociatedObject)给分类添加属性
  • 遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
  • 交换方法实现(交换系统的方法)
  • 利用消息转发机制解决方法找不到的异常问题
  • ......

4.如下

image.png

isMemberOfClass 判断左边是否刚好等于右边类型。 isKindOfClass 判断左边或者左边类型的父类是否刚好等于右边类型。 注意:类方法内部是获取其元类对象进行比较

image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容