iOS RunTime 底层原理探究

什么是RunTime

OC是一门动态性比较强的编程语言 跟C,C++等静态语言有很大的不同。

静态语言:如C语言 编译阶段就要决定调用哪个函数 如果函数未实现就会报错。

动态语言:编译阶段并不能决定真正调用哪个函数 只要函数声明过 没有实现也不会报错。

OC之所以被称为动态语言 就是因为它把一些决定性的工作从编译阶段推迟到运行阶段。OC代码的运行不仅需要编译器,还需要运行时系统(Runtime Sytem)来执行编译后的代码。

RunTime 是一套底层纯C语言的API。OC代码最终都会被编译器编译为运行时代码。然后通过消息机制决定函数调用的方式。这也是OC作为动态语言使用的基础。

isa详解

要想学习RunTime 首先要了解它底层的一些常用的数据结构 比如isa指针。

之前我们总是认为OC中的每个对象都包含着一个isa指针,实例对象的isa指针 指向类对象 类对象的isa指针指向元类对象 元类对象的isa指向基类。

那么isa中只有这些信息吗,其实我们可以再深入的探究一下的。在arm64的架构中isa指针并不是直接指向类对象 而是要进行一次位运算 本身的isa指针地址 &ISA_MASK 才能得到类对象或者元类对象。在arm64之前isa就是一个普通的指针,存储着Class meta-Class对象的内存地址。arm64架构开始,对isa指针进行了优化,变成了一个共用体结构,还使用位域来存储更多的信息。所以在arm64架构中,我们拿到isa指针地址后 还要进行&ISA_MASK才能得到Class meta-Class对象的地址。

如果你看RunTime的源码 你会发现objc_object中的isa指针已经变成isa_t这种共用体结构了。里面通过位域技术存储了更多的信息。

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits; //存放所有的数据 一共64位 下面struct结构体中的属性 写在前面的在低地址位置 也就是位数的最右边

#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; //占1位 0代表普通指针 代表着isa只存储着Class meta-Class对象的内存地址 1 代表优化过 使用位域存储着更多的信息
        uintptr_t has_assoc        : 1;//是否设置过关联对象 没有释放更快
        uintptr_t has_cxx_dtor     : 1;//是否有C++的析构函数 没有释放的更快
        uintptr_t shiftcls         : 33;//存储着Class meta-Class对象的内存地址信息
        uintptr_t magic            : 6;//用于在调试时分辨对象是否未完成初始化
        uintptr_t weakly_referenced : 1;//是否被弱指针指向过
        uintptr_t deallocating      : 1;//对象是否正在释放
        uintptr_t has_sidetable_rc  : 1;//引用计数是否过大 无法存储在isa中 如果为1 那么引用计数会存储在一个叫SideTable的类的属性中
        uintptr_t extra_rc          : 19;//存储的值是引用计数减1
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
}

很显然位域技术可以用更小的内存 存储更多的信息 比如BOOL值 一般存储一个BOOL值需要一个字节 但是如果使用位域技术 一个自己 0000 0000 用每一位代表一个二进制的信息 一个字节就可以存储8个BOOL值的信息了。

Class的结构

struct objc_class : objc_object {
    // Class ISA;
    objc_class;
    Class superclass;
    cache_t cache;             // 方法缓存
    class_data_bits_t bits;    // 用于获取具体的类信息 &FAST_DATA_MASK 得到 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; //指向了另一张表 ro_t 只读表

    method_array_t methods; //方法列表 二维数组 method_array_t 装着 method_list_t 里面装着method_t
    property_array_t properties; //属性信息 二维数组
    protocol_array_t protocols; //协议信息 二维数组

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
}

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; //方法信息 一维数组 method_list_t 装着method_t 
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars; //属性信息

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

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

上面就是objc_class的结构了 可以清晰的看到 存储了isa指针 属性信息 方法信息 协议信息 成员变量等重要信息。

class_rw_t 里面的methods properties protocols 是二维数组 是可读可写的 包含了类的初始内容(初始化时候已有的属性 协议 方法) 分类的内容。

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

为什么class_rw_t 和 class_ro_t 都存储了类的初始信息呢 是不是感觉有点浪费呢?还记得我们OC中的分类吗,一个类初始的方法 协议 成员变量 属性等信息其实是存储在class_ro_t中的,但是在运行阶段RunTime系统会把class_ro_t存储的方法 属性 协议等信息和分类中的方法 协议 属性等信息 一并合并到class_rw_t中,并且分类的方法靠前。所以class_rw_t存储的原始信息是这样来的。class_rw_t一开始是不存在的 在运行的时候合并的时候 创建出来的。一开始bits是指向class_ro_t的 我们设置了class_rw_t后 才指向class_rw_t的。

method_t

struct method_t {
    SEL name; //函数名 SEL代表方法或者函数名字 一般叫做选择器
    const char *types; //编码(返回值类型 参数类型) 是个字符串 根据encode指令编写的 比如 v代表void @代表id类型 :代表SEL类型
    IMP imp; // 指向函数的指针 IMP代表函数的具体实现

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};

不同类中如果有相同的方法名 他们的选择器是相同的 即SEL相同 可通过@selector() 或者 sel_registerName()获取

 cache_t 方法缓存

cache 用散列表来缓存曾经调用过的方法 可以提高方法的查找速度.

我们都知道当向对象发送一个消息的时候,对象会通过自己的isa指针找到类对象或者元类对象存储的方法列表中找到并实现,这个需要遍历方法列表寻找,如果在类对象或者元类对象的方法列表中找不到该方法,类对象和元类对象还会通过自己的superClass指针到自己的父类对像或者父类元类对象的方法列表中遍历寻找,直到找到该方法的实现。如果我们常用的方法每次都这样寻找会很麻烦。所以苹果对我们对象每次调用多的方法 都缓存到类对象或者元类对象的cache中。这样每次调用方法,会先通过isa指针找到类对象或者元类对象的cache列表中查找,如果找到直接调用。找不到,在走以上过程,找到了就缓存到cache列表中。极大的提高了效率

 缓存cache_t的底层结构

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

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

散列表为什么比较快呢,散列表可以避免遍历直接找到目标。

原理是这样的。存储的时候 通过一定的规则 得到一个索引 那么我们就将存储的内容放到这个索引对应的位置。取出的时候可以按照规则直接得到索引,迅速找到。

方法缓存的索引规则其实是通过@selector('方法名') & _mask = 数组中的索引,得到这个索引后直接将该方法封装成bucket_t存储到该索引位置。取出时根据相同的规则得到索引,直接取值。

如果@selector('方法名') & _mask 得到索引值已经存储了东西 那么会存储到@selector('方法名') & (_mask -1)的位置。取出的时候也是如果发现key和调用的方法名不对,那么会@selector('方法名') & (_mask -1)得到一个新的索引值重新取。以此类推

如果索引之间有间距 直接填充NULL 所以是空间换时间 散列表就是哈希表

有了Cache_t这个结构体那么消息转发机制 就变成了这样先通过isa找到类对象或者元类对象 然后在起cache的方法列表中查找方法,如果找不到,再遍历其存储的方法列表查找,如果找到,调用并缓存起来,找不到通过superClass找到父类对象或者父元类对象 先在其cache的方法列表中查询,找到调用并缓存到自己的类或者元类。找不到在从其存储的方法列表中查询 直到找到调用并缓存到自己类或者元类的cache列表中。

objc_msgSend() 消息机制

MJPerson *person = [[MJPerson alloc] init];
[person personTest];
// objc_msgSend(person, @selector(personTest));
// 消息接收者(receiver):person
// 消息名称:personTest

OC方法的调用 消息机制 给方法调用者 发送消息

objc_msgSend的执行流程可以分为3大阶段

1.消息发送 2.动态方法解析 3 消息转发

消息发送阶段就是我们上面讲的消息寻找流程 如果能找到就调用 如果不能就进入第二个阶段 允许我们动态的创建方法 如果这个阶段我们没做任何事情 那么就进入到第三个阶段 消息转发 可能会找其他对象去调用者饿高方法 如果这三个流程都走了 还是没找到方法 那就报找不到方法的错误了。

消息发送阶段我们已经讲的很清楚了。下面我们来讲一下动态解析阶段

1.先判断是否曾经有过动态解析 如果有 直接进入消息转发阶段

2.如果没有 调用+resolveInstanceMethod或者+resolveClassMethod方法 我们可以动态的添加实现 标记为已经动态解析 然后再次进入消息发送阶段

#import "Person.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p = [[Person alloc] init];
    [p test];
    [Person classMethod];
}

@interface Person : NSObject

- (void)test;

+(void) classMethod;

@end

#import "Person.h"
#import <objc/runtime.h>

@implementation Person

//如果消息发送阶段没有找到方法 就会走到动态解析阶段 会调用这个方法
//我们有机会在这个方法里 动态添加方法的实现
//如果我们已经动态添加了方法 又回回到第一阶段 消息发送阶段
+(BOOL)resolveInstanceMethod:(SEL)sel {
    //方案一 如果方法没被实现 回调用我们写的other方法
    if (sel == @selector((test))) {
        Method otherMethod = class_getInstanceMethod(self, @selector(other));
        //相当于放到class_rw_t即类存储的方法列表里面了 所以再次回到消息发送阶段会从类存储的方法列表里找到
        class_addMethod(self, sel, method_getImplementation(otherMethod), method_getTypeEncoding(otherMethod));
        //代表已经实现了动态解析
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

//如果调用的类方法没被实现 可在这个方法里面动态实现
+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector((classMethod))) {
        Method classMethod = class_getClassMethod(self, @selector(classOtherMethod));
        //注意传参事元类对象
        class_addMethod(object_getClass(self), sel, method_getImplementation(classMethod), method_getTypeEncoding(classMethod));
        return YES;
    }
    return [super resolveClassMethod:sel];
}

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

+ (void) classOtherMethod {
    NSLog(@"%s",__func__);
}

@end

如果第二阶段 我们也没做什么,那么就会进入消息转发阶段。将消息转发给别人。就是自己没能力处理 看看别人是否有能力处理。

调用forwradingTargetForSelector:返回一个对象 让这个对象接收这个消息 走这个对象的消息发送阶段

#import "Person.h"
#import "Student.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p = [[Person alloc] init];
    [p test];
    [Person classMethod];
    Student *s = [[Student alloc] init];
    [s test];
}

#import "Student.h"
#import "Person.h"

@implementation Student

//消息转发阶段
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        //给Person对象 发送test消息
        return [[Person alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end

如果消息转发阶段我们没有实现forwardingTargetForSelector:方法 或者该方法返回nil 系统还是给我们提供了一个流程来处理这个问题

会调用 methodSignatureForSelector:返回这个方法的签名  这些信息会包装到NSInvocation对象中 然后调用 forwardInvocation:方法 我们可以修改NSInvocation的调用对象 让另外一个对象调用这个方法。 达到和实现forwardingTargetForSelector:方法一样的效果

#import "Student.h"
#import "Person.h"

@implementation Student

//消息转发阶段
//- (id)forwardingTargetForSelector:(SEL)aSelector {
//    if (aSelector == @selector(test)) {
//        //给Person对象 发送test消息
//        return [[Person alloc] init];
//    }
//    return [super forwardingTargetForSelector:aSelector];
//}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        return nil;
    }
    return [super forwardingTargetForSelector:aSelector];
}
//如果没有实现forwardingTargetForSelector 或者forwardingTargetForSelector 返回nil 会调用这个方法 获取方法签名 然后调用forwardInvocation:方法
//方法签名 返回值类型 参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        // v void 返回值为空 16:所有参数的大小 @0 id类型的参数从0开始 :SEL类型的参数 从第8位开始   返回nil 不会调用forwardingInvocation:方法 就报错了
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
    return [super methodSignatureForSelector:aSelector];
}
//NSInvocation封装了一个方法调用 包括 方法的调用者 方法 方法参数
//anInvocation.target; 方法调用者
//anInvocation.selector;//方法
//anInvocation getArgument: atIndex: 参数
- (void)forwardInvocation:(NSInvocation *)anInvocation {
//    anInvocation.target = [[Person alloc] init];
//    //调用函数
//    [anInvocation invoke];
    [anInvocation invokeWithTarget:[[Person alloc] init]];
}

@end

如果我们实现methodSignatureForSelector:和forwardInvocation:方法 仅仅能达到和forwardingTargetForSelector:一样的效果,那么下面的也太麻烦了。是不是下面的方法还能实现一些不同的效果呢。我们可以看一下 事实是,只要我们进入到forwardingInvocation:方法 我们可以做任何事情 我们甚至可以不给出转发者 只打印一下也是可以的。

- (void)forwardInvocation:(NSInvocation *)anInvocation {
//    anInvocation.target = [[Person alloc] init];
//    //调用函数
//    [anInvocation invoke];
//    [anInvocation invokeWithTarget:[[Person alloc] init]];
    //不给出转发者 只打印一下
    NSLog(@"哈哈哈");
}

相当于我们调用test 方法 实现的是forwardInvocation:里面的内容。类方法也有消息转发机制 只要把消息转发机制的方法变成类方法就行了 意思就是-变为+号。因为消息转发机制的三个方法 都是用消息接收着直接调用的。如果你传的是实例对象 那就是实例方法 你传的是个类对象 那就是类方法。

super 关键字

struct objc_super {
    __unsafe_unretained _Nonnull id receiver; // 消息接收者
    __unsafe_unretained _Nonnull Class super_class; // 消息接收者的父类
};

在arm64中 objc_super 的构成是一个消息接收者 和 一个消息接收者的父类

- (void)run {
    // super调用的receiver仍然是MJStudent对象
    // 但是调用的方法 先从父类的cache找 然后从父类的method_list中找
    [super run];
//    struct objc_super arg = {self, [MJPerson class]};
//    objc_msgSendSuper(arg, @selector(run));
//    NSLog(@"MJStudet.......");
}

可以看到虽然调用父类的run方法,但是从objc_msgSendSuper(arg, @selector(run));可以看到消息接收者仍然是子类 只不过执行消息发送的时候是从父类开始的。
 [super message]的底层实现
 1.消息接收者仍然是子类对象
 2.从父类开始查找方法的实现

- (instancetype)init
{
    if (self = [super init]) {
        NSLog(@"[self class] = %@", [self class]); // MJStudent
        NSLog(@"[self superclass] = %@", [self superclass]); // MJPerson

        NSLog(@"--------------------------------");

        // objc_msgSendSuper({self, [MJPerson class]}, @selector(class));
        NSLog(@"[super class] = %@", [super class]); // MJStudent
        NSLog(@"[super superclass] = %@", [super superclass]); // MJPerson
    }
    return self;
}

结果为什么是这样呢?其实我们都知道 class方法,是在基类(NSObject)实现的。所以上面的代码调用的其实都是一个方法。接收者都是self 而class 和 superClass的实现是这样的

- (Class)class
{
    return object_getClass(self);
}

- (Class)superclass
{
    return class_getSuperclass(object_getClass(self));
}

所以上面的结局也都是可以理解的了。

RunTime相关的API

#import "ViewController.h"
#import <objc/runtime.h>
#import "MJPerson.h"
#import "MJCar.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //动态创建一个类
    Class newClass = objc_allocateClassPair([NSObject class], "MJDog", 0);
    //添加成员变量 第一个参数 为谁添加 第二个 成员变量的名字 第三个 成员变量的大小 第四个对齐方式 一般传1 第五个成员变量的类型
    class_addIvar(newClass, "_age", 4, 1, @encode(int));
    class_addIvar(newClass, "_weight", 4, 1, @encode(int));
    //注册类 如果要添加成员变量和方法 要在这个方法之前调用 因为类管理的成员变量信息是只读的class_ro_t中 注册之后就不能更改了
    //也就是说 已有的类不能再动态的添加成员变量了 但是方法可以 方法是存储在类中class_rw_t中的可读可写
    objc_registerClassPair(newClass);
    //这个dog 就属于MJDog这个类了
    id dog = [[newClass alloc] init];
    [dog setValue:@10 forKey:@"_age"];
    [dog setValue:@20 forKey:@"_weight"];
    NSLog(@"%@",[dog class]); //MJDog
    NSLog(@"%zd and %@",class_getInstanceSize(newClass),[dog valueForKey:@"_age"]); //16 isa 8 _age 4 _weight 4
}

- (void)test {
    MJPerson *person = [[MJPerson alloc] init];
    //获取类对象
    NSLog(@"%p and %p",object_getClass(person),[person class]);
    //获取元类对象
    NSLog(@"%p",object_getClass([person class]));
    [person run];
    //设置类对象指向的isa
    object_setClass(person, [MJCar class]);
    [person run];
    
    //判断一个OC对象是否为calss
    object_isClass(person);
    NSLog(@"%d and %d and %d",object_isClass(person),object_isClass([MJPerson class]),object_isClass(object_getClass([MJPerson class])));
    //是否为一个元类
    class_isMetaClass(object_getClass([MJPerson class]));
}


@end

获取和设置成员变量

//获取成员变量
- (void) getIvar {
    Ivar ageIvar = class_getInstanceVariable([MJCar class], "_age");
    NSLog(@"%s %s",ivar_getName(ageIvar),ivar_getTypeEncoding(ageIvar));
    //设置或者获取成员变量的值
    MJCar *car = [[MJCar alloc] init];
    Ivar name = class_getInstanceVariable([MJCar class], "_name");
    object_setIvar(car, name, @"123");
    NSLog(@"%@",object_getIvar(car, name));
}

获取成员变量列表 和 获取属性列表

- (void)getIvarList {
    unsigned int count;
    Ivar *ivars = class_copyIvarList([MJCar class], &count);
    for (NSInteger i = 0; i < count ; i ++ ) {
        Ivar ivar = ivars[i];
        const char *cname = ivar_getName(ivar);
        NSString *name = [NSString stringWithCString:cname encoding:NSUTF8StringEncoding];
        NSLog(@"%@",name);
    }
    free(ivars);
    
    //获取属性列表
    unsigned int number;
    objc_property_t *propertys = class_copyPropertyList([MJCar class], &number);
    for (NSInteger i = 0; i < number; i ++) {
        objc_property_t property = propertys[i];
        const char *cname = property_getName(property);
        NSString *name = [NSString stringWithCString:cname encoding:NSUTF8StringEncoding];
        NSLog(@"%@",name);
        //属性的特性
        unsigned int attrCount = 0;
        objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
        for (unsigned int j = 0; j < attrCount; j ++) {
            objc_property_attribute_t attr = attrs[j];
            const char * name = attr.name;
            const char * value = attr.value;
            NSLog(@"属性的描述:%s 值:%s", name, value);
        }
    }
    free(propertys);
}

讲一下OC的消息机制

OC中的方法调用其实都是转成了objc_msgSend函数的调用,给接收者(方法调用者)发送了一条消息(selector)

objc_msgSend底层有三大阶段 消息发送 动态解析 消息转发阶段

消息发送:

1.判断接收者是否为nil 如果为nil 直接返回

2.实例对象调用方法 先通过isa找到类对象 然后在类对象的缓存方法列表中查询 如果找到直接调用 找不到 再从类对象存储的方法列表中遍历查找 找到调用 并且缓存到类对象的cache列表中。找不到 通过superClass指针,找到父类对象 重复上述过程。类对象调用方法,先通过isa找到元类对象 然后在元类对象的缓存方法列表中查询 如果找到直接调用 找不到 再从元类对象存储的方法列表中遍历查找 找到调用 并且缓存到元类对象的cache列表中。如果还找不到 通过superClass指针 找到找到父元类对象 重复上述过程。

动态解析

如果上面的消息发送阶段到了基类仍没有找到方法实现 就会到达动态解析阶段 这个阶段会调用两个方法resolveInstanceMethod:(实例对象调用) 或者 resolveClassMethod:(类对象调用) 在这两个方法中 系统允许我们动态的添加一些方法的实现(存储到class_rw_t中)。如果实现了该方法 会标记为已经动态实现过 会再走一遍消息发送流程。事实上只要你重写了这两个方法 都会被标记为已经实现了动态解析。

消息转发

如果动态解析阶段 我们还是没做什么事情 那么就会进入到消息转发阶段 这个阶段我们可以指定一个其他的对象 来接收这个消息。我们可以实现forwardingTargetForSelector:来指定对象实现 如果没有指定对象 那么系统会调用methodSignatureForSelector:来获取一个方法签名(如果返回nil 就报错找不到方法)  如果methodSigntureForSelector:没有返回nil 那么就调用 forwardInvocation:方法 这个方法我们可以做任何处理

什么是RunTime?平时项目中有用过吗?

OC 是一门动态语言,相比C或者C++编译完成后就已经确定代码的结果,OC可以在运行的时候动态的改变类的实现 添加属性 修改方法的实现。这一切都是基于运行时的机制。

RunTIme就是C语言封装的一套底层的API 封装了很多动态性相关的函数。

平时我们写的一些代码 底层都是转换成了RunTImeAPI进行调用。

1.给分类添加属性 关联对象

2.遍历类的所有成员变量,然后访问私有变量 或者实现字典转模型 归档接档

3.交换方法的实现(一般是交换系统的方法实现,可以实现在调用系统方法的同时,实现自己的一些逻辑)

4.利用消息转发机制 解决一些方法找不到的问题

@dynamic 告诉编译器 不用生成getter和setter方法 也不会自动生成成员变量 等到运行时再添加方法的实现

@synthesize 关键字 可以自动生成getter和setter方法 并且生成一个_xxx的成员变量

原文地址:https://www.cnblogs.com/huanying2000/p/13961581.html