Objective-C基础之七(Runtime用法)

2,686 阅读14分钟

super关键字

super其实是OC为我们提供的一个关键字,主要是继承体系中用来调用类从父类继承过来的属性和方法,它只是一个标记,如果是使用super去调用方法,本质其实还是拿到当前类对象,然后从其父类的缓存和方法列表进行查找。下面我们就通过源码来进一步探索super的底层实现。

源码解读

  • 首先,先创建XLPerson类,并且在XLPerson.m中增加如下方法:
- (instancetype)init
{
    self = [super init];
    if (self) {
        NSLog(@"init");
    }
    return self;
}
  • 然后通过以下指令,将XLPerson.m转成C++代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc XLPerson.m
  • 查看生成的XLPerson.cpp文件,找到对应的init方法如下:
static instancetype _I_XLPerson_init(XLPerson * self, SEL _cmd) {
    self = objc_msgSendSuper((__rw_objc_super){
        (id)self,
        (id)class_getSuperclass(objc_getClass("XLPerson"))
    }, sel_registerName("init"));
    if (self) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_f3_lg91hwts5rjdlzjph0sn82m80000gp_T_XLPerson_d841fc_mi_0);
    }
    return self;
}
  • 可以发现,在init方法中,[super init]最终转换成了objc_msgSendSuper函数,函数具有两个参数。

  • 然后到objc源码中查找objc_msgSendSuper函数的实现,发现在源码中存在objc_msgSendSuper和objc_msgSendSuper2两个函数,那么到底super最终执行的是哪个函数呢?可以通过Xcode断点,查看汇编代码来找到最终调用的方法。

    • 首先我们在XLPerson的init函数中打个断点,然后在main函数中创建XLPerson对象,并且打开Xcode以下设置

    • 然后运行程序,查看汇编代码,可以明确看出,super最终调用的是objc_msgSendSuper2函数

  • 然后在源码中查看objc_msgSendSuper2函数声明如下,objc_msgSendSuper2的前两个参数分别对应上文中的结构体__rw_objc_super和sel_registerName("init")

OBJC_EXPORT id _Nullable
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
  • 查看结构体objc_super的源码如下,其中objc_super有两个成员变量,一个是receiver,对应上文结构体中的self。一个是super_class,对应上文结构体中的class_getSuperclass(objc_getClass("XLPerson")。
/// Specifies the superclass of an instance. 
struct objc_super {
    //class的实例对象,用来接收消息
    __unsafe_unretained _Nonnull id receiver;
    //父类对象,用来决定方法查找的起点
    __unsafe_unretained _Nonnull Class super_class;
    /* super_class is the first class to search */
};
  • 虽然objc_msgSendSuper2函数声明中第一个参数是objc_super类型的结构体,但是在实际传参过程当中,objc_msgSendSuper2函数第一个参数传递的其实是objc_super2类型的结构体,在下文的汇编代码中可以看出。
struct objc_super2 {
    //当前消息接收者,实例对象
    id receiver;
    //当前类对象
    Class current_class;
};
  • 再次查找objc_msgSendSuper2函数的实现,发现objc_msgSendSuper2函数是由汇编来实现的,汇编代码如下
ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame
//p0其实就是x0寄存器,p16其实就是x16寄存器
//p0中存放着真正的消息接收者,而p16存放着当前class对象
ldp	p0, p16, [x0]		// p0 = real receiver, p16 = class
//拿到x16中保存的class对象地址,通过地址偏移拿到它的superclass的地址,重新赋值给p16寄存器
ldr	p16, [x16, #SUPERCLASS]	// p16 = class->superclass
//调用CacheLookup进行方法查找,而p16就是CacheLookup的参数
CacheLookup NORMAL

END_ENTRY _objc_msgSendSuper2

在objc_msgSendSuper2中,第一个参数是一个objc_super2类型的结构体。在arm64汇编中,x0寄存器一般用来存放参数或者返回值,此处的x0就存放了结构体objc_super2的地址值。

ldp p0, p16, [x0]表示从x0寄存器里存放的地址开始,取前8个字节的地址赋值给p0,取后8个字节的地址赋值给p16,此时,p0寄存器的值就是objc_super2结构体中receiver的地址,p16的值就是objc_super2结构体中current_class的地址。

ldr p16, [x16, #SUPERCLASS]其实就是拿到current_class的superclass,[x16, #SUPERCLASS]其实就是将x16中存放的内存地址偏移8个字节。而Class底层结构中,第一个成员变量是isa指针,占用8个字节,第二个成员就是superclass。而x16里存放的地址值就是Class的地址值,偏移8个字节其实就是拿到了superclass指针。这也印证了上文中所说的,objc_msgSendSuper2函数的第一个参数其实是objc_super2类型的结构体。

SUPERCLASS其实就是当前架构下指针所占字节数,在arm64架构中,指针类型占8个字节。

拿到superclass指针之后,将superclass的地址值存放在p16寄存器中,而p16寄存器的值就是CacheLookup函数的参数,CacheLookup会到superclass的方法列表中去查找对应的方法。但是真正的消息接收者还是当前的receiver。

super总结

  • 通过super来调用方法时,底层会转换成objc_msgSendSuper2函数
  • objc_msgSendSuper2函数函数传递至少两个参数,第一个参数是一个objc_super2类型的结构体,内部有两个成员,第一个成员是当前实例对象本身receiver,第二个成员是当前实例对象对应的类对象current_class。
  • 当调用[super xxxx]方法方法时,其实不是给receiver的superclass发送消息,而是给当前的receiver发送消息。但是在执行消息查找时,会首先拿到current_class的superclass,然后到superclass的方法缓存和方法列表中查询方法。
  • 也就是说,objc_super2的第一个成员变量receiver决定了谁是真正的消息接收者,而第二个成员变量current_class的superclass其实就决定了当前消息从哪里开始进行查找。

一般情况下,给对象receiver发送一个消息,首先会到receiver的缓存列表或者方法列表中去查找,找不到才会到superclass中去查找,而super则表示首先会给receiver发送一个消息,但是会先到recever的superclass中进行方法查找。这就是为什么使用super会调用父类方法。

Demo解析

首先创建XLPerson类,然后创建XLTeacher类继承自XLPerson类,然后在XLTeacher.m中增加如下代码

@implementation XLTeacher

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

@end

要想知道打印结果,还需要知道class和superclass的底层实现,在objc源码的NSObject.mm中可以看到具体的实现,如下

@implementation NSObject

+ (id)self {
    return (id)self;
}

- (id)self {
    return self;
}

+ (Class)class {
    return self;
}

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

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

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

@end
  1. [self class],通过self(当前XLTeacher的实例对象)调用class方法其实就是给self发送一条@selector(class)消息,由于XLTeacher的实例对象以及它的父类都没有实现class方法,所以最终会查找到基类NSObject的方法列表中,最终找到方法实现,返回object_getClass(self);,也就是当前类对象XLTeacher。
  2. [super class],其实也是向self发送一条@selector(class)消息,只不过会先到XLTea的父类中去查找方法实现,由于父类没有实现class方法,所以最终找到基类NSObject的class方法,返回object_getClass(self);,结果也是XLTeacher。
  3. [self superclass],向self发送一条@selector(superclass),首先会在XLTeacher的缓存和方法列表中查找,XLTeacher没有实现此方法,因此会到XLTeacher的superclass中查找,父类也未实现,最后会找到NSObject中的superclass方法,返回[self class]->superclass,也就是XLPerson。
  4. [super superclass],利用super调用superclass方法,其实还是向self发送@selector(superclass)消息,但是会直接先从XLTeacher的父类XLPerson中查找方法实现,由于XLPerson未实现此方法,最终会调用NSObject的superclass方法,返回[self class]->superclass,也就是XLPerson。

isKindOfClass和isMemberOfClass

源码解析

首先,查看源码中NSObject.mm的实现

@implementation NSObject
+ (BOOL)isMemberOfClass:(Class)cls {
    //通过object_getClass获取当前类对象的isa所指向的元类对象与cls进行比较
    return object_getClass((id)self) == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
    //比较当前实例对象的isa所指向的类对象与cls
    return [self class] == cls;
}

+ (BOOL)isKindOfClass:(Class)cls {  
    //遍历当前类对象的元类对象以及它的父类的元类对象,如果和cls相等就返回YES
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
    //遍历当前实例对象所对应的类对象以及它的父类,如果和cls相等就返回YES
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
@end
  • 类方法+isMemberOfClass:是通过isa获取当前类对象的元类对象,与参数中的对象进行对比,如果相等则返回YES。
  • 对象方法-isMemberOfClass:是通过isa获取当前实例对象的类对象,与参数中的对象进行对比,如果相等,则返回YES。
  • 类方法+isKindOfClass:通过遍历当前类对象所对应的元类对象以及父类所对应的元类对象,如果遍历到的元类对象与参数中的对象相等,则返回YES。
  • 对象方法-isKindOfClass:通过遍历当前实例对象对应的类对象以及它的父类,如果遍历到的类对象和参数中的对象相等,则返回YES。

Demo解析

创建XLPerson对象,继承自NSObject。然后在main函数中增加以下代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        BOOL result1 = [NSObject isMemberOfClass:[NSObject class]];
        BOOL result2 = [NSObject isKindOfClass:[NSObject class]];
        BOOL result3 = [XLPerson isMemberOfClass:[XLPerson class]];
        BOOL result4 = [XLPerson isKindOfClass:[XLPerson class]];
        
        NSLog(@"%d %d %d %d", result1, result2, result3, result4);
        
        XLPerson *person = [[XLPerson alloc] init];
        BOOL result5 = [person isMemberOfClass:[NSObject class]];
        BOOL result6 = [person isKindOfClass:[NSObject class]];
        BOOL result7 = [person isMemberOfClass:[XLPerson class]];
        BOOL result8 = [person isKindOfClass:[XLPerson class]];
        
        NSLog(@"%d %d %d %d", result5, result6, result7, result8);
    }
    return 0;
}

类方法

  1. [NSObject isMemberOfClass:[NSObject class]]会返回NO,原因是object_getClass(NSObject)拿到的是元类对象,而[NSObject class]是类对象,两者不相等。
  2. [NSObject isKindOfClass:[NSObject class]]会返回YES,原本获取NSObject的元类对象和[NSObject class]来进行对比,应该是不相等,但是比较特殊的一点是NSObject的元类对象的superclass指向它的类对象,所以此处返回YES。此处可以参考Objective-C基础之一(深入理解OC对象)中isa和superclass的总结。
  3. [XLPerson isMemberOfClass:[XLPerson class]]会返回NO,获取到XLPerson的元类对象和[XLPerson class]进行比较,两者不相等。
  4. [XLPerson isKindOfClass:[XLPerson class]]会返回NO,首先会拿到XLPerson的元类对象和[XLPerson class]对比,发现不相等,然后会通过XLPerson的superclass拿到父类的元类和[XLPerson class]进行对比,发现还是不相等,一直遍历到基类NSObject的元类,NSObject的元类的superclass指向NSObject的类对象,因此最后一次遍历是拿NSObject的类对象和[XLPerson class]进行比较,肯定不相等,所以返回NO。

实例方法

  1. [person isMemberOfClass:[NSObject class]]返回NO,原因是拿person的类对象和NSObject比较,显然不相等。
  2. [person isKindOfClass:[NSObject class]]返回YES,因为XLPerson原本就是继承自NSObject,而isKindOfClass则是通过继承链最终能找到NSObject的类对象和参数[NSObject class]进行对比,两者相等。
  3. [person isMemberOfClass:[XLPerson class]]返回YES,person的类对象就是XLPerson。
  4. [person isKindOfClass:[XLPerson class]]返回YES,原因同上。

Runtime应用

使用runtime查看私有成员变量

利用runtime,我们可以遍历类的所有属性或者成员变量,拿到属性或者成员变量我们就可以做很多事情,比如结合KVC给系统类对象的私有属性赋值,或者将字典转换成模型等等。

访问系统类的私有属性并且赋值

想给系统类的私有属性赋值,前提是需要知道系统类有哪些私有属性,首先,创建NSObject的分类NSObject+Ivar,如下

@interface NSObject (Ivar)

+ (void)logIvar;

@end

@implementation NSObject (Ivar)

+ (void)logIvar{
    unsigned int count;
    Ivar *ivars = class_copyIvarList(self, &count);
    for (int i = 0; i < count; i++) {
        //取出成员变量
        Ivar ivar = ivars[i];
        NSString *ivarName = [[NSString alloc] initWithUTF8String:ivar_getName(ivar)];
        NSLog(@"%@", ivarName);
    }
    free(ivars);
}

@end

然后以UITextField为例,调用[UITextField logIvar]方法,就可以打印出UITextField的所有成员变量,假如我们需要修改输入框提示语的文字颜色,就可以找到其中的_placeholderLabel,然后通过KVC来拿到对应的属性进行设置

UITextField *field = [[UITextField alloc] initWithFrame:CGRectMake(100, 100, 200, 30)];
field.placeholder = @"我是提示语";
[self.view addSubview:field];
[field setValue:[UIColor redColor] forKey:@"_placeholderLabel.textColor"];

在iOS 13之后是不允许访问系统类的私有成员变量的,此处仅仅是用作功能演示,运行会报错。

字典转模型

OC开发过程中,涉及到很多字典转模型的需求,尤其是和后台进行交互。如果不使用runtime的话,就需要自己创建方法,自己去实现。如下

@interface XLPerson : NSObject

@property(nonatomic, copy)NSString *name;
@property(nonatomic, assign)int age;

+ (instancetype)personWithDic:(NSDictionary *)dic;

@end

@implementation XLPerson

+ (instancetype)personWithDic:(NSDictionary *)dic{
    XLPerson *person = [[XLPerson alloc] init];
    person.name = dic[@"name"];
    person.age = [dic[@"age"] intValue];
    
    return person;
}

@end

上述方法可以实现字典转模型的需求,但是如果存在字典套字典的这种情况,那么上述方法实现起来就会非常麻烦,因此,runtime的作用就体现了出来,典型的字典转模型的工具如MJExtension也是利用runtime来实现。

创建NSObject的分类NSObject+Json,然后增加如下方法

@interface NSObject (Json)

+ (instancetype)objectWithDictionary:(NSDictionary *)dictionary;

@end

@implementation NSObject (Json)

+ (instancetype)objectWithDictionary:(NSDictionary *)dictionary{
    id object = [[self alloc] init];
    unsigned int count;
    Ivar *ivars = class_copyIvarList(self, &count);
    for (int i = 0; i < count; i++) {
        //取出成员变量
        Ivar ivar = ivars[i];
        NSMutableString *ivarName = [[NSMutableString alloc] initWithUTF8String:ivar_getName(ivar)];
        //移除字符串之前的_
        [ivarName deleteCharactersInRange:NSMakeRange(0, 1)];
        
        //给对应的属性设值
        id value = dictionary[ivarName];
        if (value) {
            [object setValue:value forKey:ivarName];
        }
    }
    free(ivars);
    return object;
}

@end

使用方式如下

NSDictionary *dic = @{@"name" : @"张三", @"age" : @12};
XLPerson *person = [XLPerson objectWithDictionary:dic];
NSLog(@"%@ %d", person.name, person.age);

如果想要实现字典嵌套字典转模型的方法,可以参考MJExtension的实现。

runtime之方法交换(Method Swizzing)

之前的文章中说到过,类的所有对象方法都是存放在类对象的方法列表中,也就是存放在objc_class中的class_rw_t中,class_rw_t的源码如下

struct class_rw_t {
    uint32_t flags;             //用来存放类的一些基本信息
    uint32_t version;           //版本号

    const class_ro_t *ro;       //class_ro_t类型指针

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

method_array_t是一个二维数组,内部存放了method_list_t,每个method_list_t都存放着一个分类的所有方法,二维数组的最后一个method_list_t则存放着类编译时就生成的所有方法,而method_list_t中存放的就是method_t,每一个方法对应一个method_t,结构如下

struct method_t {
    SEL name;           //方法选择器(方法名)
    const char *types;  //方法签名
    MethodListIMP imp;  //方法实现的地址
}

在method_t中,imp则表示方法实现的地址,因此,如果想实现方法交换,只需要修改imp即可,而runtime就是通过交换两个method_t的imp来实现方法交换的。

创建XLPerson类,如下

@interface XLPerson : NSObject

- (void)run;
- (void)sleep;

@end

@implementation XLPerson

+ (void)initialize
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method methodRun = class_getInstanceMethod(self, @selector(run));
        Method methodSleep = class_getInstanceMethod(self, @selector(sleep));
        method_exchangeImplementations(methodRun, methodSleep);
    });
}

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

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

调用run方法和sleep方法会发现两者的实现被交换了,此处展示的是对象方法的交换,类方法交换和对象方法交换类似,更多方法交换实现可以参考Aspect

runtime常用Api

动态类

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

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

//销毁一个类
void objc_disposeClassPair(Class cls)

//获取isa指向的Class
Class object_getClass(id obj)

//设置isa指向的Class
Class object_setClass(id obj, Class cls)

//判断一个OC对象是否为Class
BOOL object_isClass(id obj)

//判断一个Class是否为元类
BOOL class_isMetaClass(Class cls)

//获取父类
Class class_getSuperclass(Class cls)

成员变量

//获取一个实例变量信息
Ivar class_getInstanceVariable(Class cls, const char *name)

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

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

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

//获取成员变量的相关信息
const char *ivar_getName(Ivar v)
const char *ivar_getTypeEncoding(Ivar v)

属性操作

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

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

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

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

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

方法操作

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

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

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

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

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

//获取方法的相关信息(带有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)

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

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