Runtime系列教程一

998 阅读3分钟

Runtime基础

1.Runtime相关的头文件

学习Runtime第一手源码,看下面三个头文件基本就可以了。

#import <objc/runtime.h>
#import <objc/message.h>
#import <objc/objc.h>

2.Runtime几个重要的结构体

从结构体中看出,其实实例内部就只有一个isa指针,指向自己的类。

// 实例
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

下面是关于类的结构体的说明。

// 类
struct objc_class {
    // 类的isa指针指向自己的元类
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    // super_class指向父类
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    // 成员变量
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    // 方法
    struct objc_method_list * _Nullable * _Nullable** methodLists                    OBJC2_UNAVAILABLE;
    // 方法缓存
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    // 遵循的协议
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

类的结构体中定义类相关的属性,下面着重说明实例变量数组和方法数组两个结构体。 objc_ivar_list成员变量数组

// 成员变量
struct objc_ivar {
    // 变量名
    char * _Nullable ivar_name                               OBJC2_UNAVAILABLE;
    // 变量类型
    char * _Nullable ivar_type                               OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}                                                            OBJC2_UNAVAILABLE;
// 可以认为是一个数组,数组的元素就是objc_ivar
struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
}

objc_method_list方法数组

struct objc_method {
    // 方法名
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    // 方法实际执行的函数
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
// 可以看成是存储方法的一个数组,每个元素objc_method
struct objc_method_list {
    struct objc_method_list * _Nullable obsolete             OBJC2_UNAVAILABLE;
    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}

3.相关函数

3.1获取类名

根据一个类动态的获取类名,但是class_getName返回的是C语言,需要通过NSString转成OC的字符串。

+ (NSString *)fetchClassName:(Class)class {
    const char *className = class_getName(class);
    return [NSString stringWithUTF8String:className];
}

3.2获取成员变量

下面的fetchIvarList通过class_copyIvarList可以获取到实例变量列表,然后通过ivar_getName获取变量名,ivar_getTypeEncoding获取变量的类型,再把变量名和类型通过一个字典保存到数组中。

+ (NSArray *)fetchIvarList:(Class)class {
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList(class, &count);
    NSMutableArray *mutableList = [NSMutableArray new];
    for (int i = 0; i < count; i++) {
        NSMutableDictionary *dictM = [NSMutableDictionary new];
        const char *ivarName = ivar_getName(ivarList[i]);
        const char *ivarType = ivar_getTypeEncoding(ivarList[i]);
        dictM[@"type"] = [NSString stringWithUTF8String:ivarType];
        dictM[@"ivarName"] = [NSString stringWithUTF8String:ivarName];
        [mutableList addObject:dictM];
    }
    // 这里记得释放,要不然内存泄露
    free(ivarList);
    return [NSArray arrayWithArray:mutableList];

}

3.3获取属性

其实获取属性和获取成员变量的方式基本一致,只是调用的方法有点不太一样class_copyPropertyList获取的是成员变量数组,property_getName获取的是变量名,方法调用很类似。

+ (NSArray *)fetchPropertyList:(Class)class {
    unsigned int count = 0;
    objc_property_t *propertyList = class_copyPropertyList(class, &count);
    NSMutableArray *mutableList = [NSMutableArray new];
    for (int i = 0; i < count; i++) {
        const char *propertyName = property_getName(propertyList[i]);
        [mutableList addObject:[NSString stringWithUTF8String:propertyName]];
    }
    // 一样的也是需要释放
    free(propertyList);
    return [NSArray arrayWithArray:mutableList];
}

3.4获取类的实例方法

封装的fetchMethodList获取类实例的方法列表,class_copyMethodList的用法跟上面类似,但是要注意一点,获取的数组内部的元素其实是Method,需要通过method_getName获取方法的选择子SEL,然后再通过NSStringFromSelector获取方法名。

+ (NSArray *)fetchMethodList:(Class)class {
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(class, &count);
    NSMutableArray *mutalbelist = [NSMutableArray new];
    for (int i = 0; i < count; i++) {
        SEL methodSel = method_getName(methodList[i]);
        NSString *methodName = NSStringFromSelector(methodSel);
        [mutalbelist addObject:methodName];
    }
    free(methodList);
    return [NSArray arrayWithArray:mutalbelist];
}

3.5动态添加方法

动态方法添加在实际开发中可能用的比较少,但是在消息转发和崩溃处理的时候可以动态的给对象添加方法,防止程序崩溃。下面是通过class_addMethod给当前实例对象self添加一个run的方法,run方法只是一个SEL,对应实际执行的函数是myRun;第一个参数是需要添加方法的类,第二个参数是添加的方法名,第三个参数是方法实际执行的函数,第四个是方法参数的@encode指令,关于@encode指令的介绍请参考这里,对于@encode的说明和使用如果哪位大佬有对应的博客可以留言学习,对这一块也不是很了解,v@:函数的类型,(返回值+参数类型) v:void @对象->self :表示SEL->_cmd

class_addMethod(object_getClass(self), @selector(run:), (IMP)myRun, "v@:");
[self performSelector:@selector(run:) withObject:@"哈哈"];
// IMP至少要带两个参数self和_cmd,其他的参数参数可以添加到后面用逗号隔开
void myRun(id self, SEL _cmd, id obj) {
    NSLog(@"sel:%@, obj:%@", NSStringFromSelector(_cmd), obj);
}
// 打印的结果如下
sel:run:, obj:哈哈

3.6方法交换

方法交换在整个苹果的体系下应用非常广泛,在我们实际的开发中也经常用到,后续在Runtime应用场景会介绍详细的用法。下面简单的介绍方法交换,前提是class内部已经实现了firstSelsecondSel对应的实现,否则无法找到方法实现,会崩溃,class_getInstanceMethod可以通过SEL选择子在class中找到对应的Method,再调用method_exchangeImplementations就可以实现两个方法的交换。

+ (void)methodSwap:(Class)class firstSel:(SEL)firstSel secondSel:(SEL)secondSel {
    Method firstMethod = class_getInstanceMethod(class, firstSel);
    Method secondMethod = class_getInstanceMethod(class, secondSel);
    method_exchangeImplementations(firstMethod, secondMethod);
}

我们可以简单在ViewController中测试一下,看看是不是真的互换了?在load方法中把两个方法交换。从打印信息可以看到,两个方法的调用顺序其实已经反过来了,说明确实是调换了方法。

+(void)load {
    [super load];
    [self methodSwap:self firstSel: @selector(sayHello1) secondSel: @selector(sayHello2)];
}
- (void)viewDidLoad {
    [self sayHello1];
    [self sayHello2];
}
- (void)sayHello1 {
    NSLog(@"sayHello1");
}
- (void)sayHello2 {
    NSLog(@"sayHello2");
}
// 打印结果如下
2021-10-20 18:09:18.280476+0800 abffff[5660:200359] sayHello2
2021-10-20 18:09:18.280632+0800 abffff[5660:200359] sayHello1