runtime运行时机制快速入门

/ 0

Objective-C 是在 C 语言的基础上扩展了一些面向对象的特性和消息转发机制的动态语言,将编译时、链接时的一些操作都放到运行时去处理。所以 Objective-C 不仅仅需要编译器,还需要 runtime 运行时机制来动态创建类和对象,还有消息的发送和转发。

那么什么是runtime运行时机制呢?

runtime 是一套底层的 C 语言库,我们平时写的 Objective-C 代码在运行过程中其实都是转为 runtime 来执行的,Objective-C 的类转成 runtime 的结构体,Objective-C 的方法转为 runtim 的 objc_msgSend() 函数,可以说 runtime 就是 Objective-C 的操作系统。

本篇文章不会深入讲解 runtime 机制,因为作者也不是大牛。如果本文中有错误之处,请在文章底部留言指出,六阿哥感激不尽!下面来看看 runtime 一些常见用法和示例。

发送消息

在 Objective-C 中调用一个方法,其实就是向对象发送一条 SEL 消息,这也就是 Objective-C 的消息机制。首先导入两个头文件 message.h,在 message.h 中其实已经导入了 runtime.h 。像对象发送消息我们使用 objc_msgSend() 函数,但调用时发现这个函数默认是没有参数的,我们进入 message.h 中发现:

#if !OBJC_OLD_DISPATCH_PROTOTYPES
OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )
__OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
__OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
#else

OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
__OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

OBJC_EXPORT id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
__OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
#endif

默认是调用下面这个函数

OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )

我们将Build Settings -> Apple LLVM 7.0 -Preprocessing -> Enable Strict Checking of objc_msgSend Calls修改为No,再次调用这个方法,就可以传递参数了。首先我们创建一个 Person 类用于测试学习,在这个类中定义 -run 和 +run 方法。

Person.h

#import <Foundation/Foundation.h>

@interface Person : NSObject

- (void)run;

+ (void)run;
@end

Person.m

#import "Person.h"

@implementation Person

- (void)run {
    NSLog(@"-run");
}

+ (void)run {
    NSLog(@"+run");
}
@end

然后在 ViewController.m 中使用运行时分别调用这两个方法。[Person class] 是获取 Person 类对象,扩展:https://blog.6ag.cn/470.html

ViewController.m

#import "ViewController.h"
#import <objc/message.h>
#import "Person.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person = [[Person alloc] init];
    
    // 向person对象发送一条run消息
    objc_msgSend(person, @selector(run));
    
    // 向Person对象发送一条run消息。注意类其实也是对象,在博客前面已经详细讲解过了
    objc_msgSend([Person class], @selector(run));
    
}

@end

控制台输出:

Snip20151116_3

获取对象成员列表

通过 runtime 运行时我们可以获取一个类的实例变量列表、属性列表、方法列表、协议列表,下面我们通过两个案例来演示下具体获取方式。首先我们在 Person.h 中添加两个实例变量和两个 property 属性:

Person.h

#import <Foundation/Foundation.h>

@interface Person : NSObject {
    NSString *_name;
    int _age;
}

@property (nonatomic, assign) float height;
@property (nonatomic, assign) float weight;

- (void)run;

+ (void)run;
@end

然后在 ViewController.m 中使用运行时来获取实例变量列表,并打印出每个实例变量的名称。

ViewController.m

#import "ViewController.h"
#import <objc/message.h>
#import "Person.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 存储实例变量列表中实例变量的数量
    unsigned int ivarCount = 0;
    
    // 返回一个数组,数组中的元素都是Ivar
    Ivar *ivarList = class_copyIvarList([Person class], &ivarCount);
    
    for (int i = 0; i < ivarCount; i++) {
        Ivar ivar = ivarList[i];
        
        // 获取实例变量的名称
        const char *name = ivar_getName(ivar);
        
        NSLog(@"%s",name);
    }
}

@end

控制台输出:

Snip20151116_4

再使用运行时获取对象的方法列表,并将方法名转为字符串打印输出到控制台。

ViewController.m

#import "ViewController.h"
#import <objc/message.h>
#import "Person.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 存储方法列表中方法的数量
    unsigned int methodCount = 0;
    
    // 返回一个数组,数组中的元素都是Method
    Method *methodList = class_copyMethodList([Person class], &methodCount);
    
    for (int i = 0; i < methodCount; i++) {
        
        Method method = methodList[i];
        
        // 获取对应方法选择器
        SEL methodSel = method_getName(method);
        
        // 将方法选择器转为字符串
        NSString *methodName = NSStringFromSelector(methodSel);
        
        NSLog(@"%@",methodName);
        
    }
}

@end

控制台输出:

Snip20151116_5

动态添加对象成员

使用 runtime 可以动态创建对象并为对象添加实例变量和方法,值得注意的是我们不能为已经存在的对象添加实例变量和方法。因为已经存在的对象中的所有成员在类加载 +load 方法中已经加载到内存中,并且不会再改变。这里的动态创建对象指代的是我们动态的创建一个类继承自 Person 类,并为我们动态创建的这个对象添加实例变量、方法等。

ViewController.m

#import "ViewController.h"
#import <objc/message.h>
#import "Person.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 动态创建Student类
    Class Student = objc_allocateClassPair([Person class], "Student", 0);
    
    // 动态为Student类添加实例变量 studyId, 类型是 NSString
    BOOL isAddIvar = class_addIvar(Student, "studyId", sizeof(NSString *), 0, "@");
    
    // 判断是否添加成功
    if (!isAddIvar) {
        NSLog(@"添加实例变量失败");
    }
    
    // 创建Student对象为我们动态添加的实例变量赋值
    id student = [[Student alloc] init];
    
    // 使用kvc为对象的属性设置值
    [student setValue:@"100号" forKey:@"studyId"];
    
    NSLog(@"studyId = %@",[student valueForKey:@"studyId"]);

}

@end

控制器输出:

Snip20151116_6

继续为这个动态创建的对象添加方法,添加方法。

ViewController.m

#import "ViewController.h"
#import <objc/message.h>
#import "Person.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 动态创建Student类
    Class Student = objc_allocateClassPair([Person class], "Student", 0);

    // 创建Student对象为我们动态添加的实例变量赋值
    id student = [[Student alloc] init];
    
    // 动态为Student类添加 study 方法
    BOOL isAddMethod = class_addMethod([Student class], @selector(study), (IMP)study_imp, "v@:");
    
    // 判断是否添加方法成功
    if (!isAddMethod) {
        NSLog(@"添加方法失败");
    }
    
    // 使用我们动态添加的方法
    [student study];
}

- (void)study {
    NSLog(@"study");
}

// 方法的实现 id self, SEL _cmd 是C语言函数默认的两个参数
void study_imp(id self, SEL _cmd) {
    NSLog(@"study_imp");
}

@end

控制台输出:

Snip20151116_7

交换方法的实现

在 Person.m 文件中添加一个 +sleep 方法,并在类加载 +load 时使用 runtime 来替换 +run 方法的实现。为什么要在 +load 方法中替换呢?因为程序运行时 +load 方法会将程序中所有类加载到数据段,而我们不能对已经加载进数据段的类的方法进行修改。

Person.m

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

@implementation Person

+ (void)load {
    
    // 获取原来的方法
    Method runMethod = class_getClassMethod([Person class], @selector(run));
    
    // 获取新的方法
    Method sleepMethod = class_getClassMethod([Person class], @selector(sleep));
    
    // 交换两个方法的实现
    method_exchangeImplementations(runMethod, sleepMethod);
    
}

- (void)run {
    NSLog(@"-run");
}

+ (void)run {
    NSLog(@"+run");
}

+ (void)sleep {
    NSLog(@"-sleep");
}

@end

在 ViewController.m 文件中调用 -run 方法。

ViewController.m

#import "ViewController.h"
#import <objc/message.h>
#import "Person.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 向person对象发送一条run消息
    objc_msgSend([Person class], @selector(run));
}

@end

控制台输出:

Snip20151117_1

为分类添加成员变量

我们知道在 Category 中只能扩展方法而不能扩展属性,但是使用 runtime 运行时却是可以实现的。那么如何实现?我们来新建一个 Person+Extension 分类,对 Person 类扩展一个属性。

Person+Extension.h

#import "Person.h"

@interface Person (Extension)

@property (nonatomic, copy) NSString *price;

@end

然后直接在 ViewController.m 中使用这个属性。

ViewController.m

#import "ViewController.h"
#import <objc/message.h>
#import "Person.h"
#import "Person+Extension.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person = [[Person alloc] init];
    
    // 调用分类中的属性
    person.price = @"无价";
    
    NSLog(@"price = %@",person.price);
}
@end

然后程序就崩溃了,崩溃原因是没有找到 -setPrice 方法。所以,在 Category 中直接使用 property 不会生成带下划线的成员变量和对应的 set、get 方法。

Snip20151117_3

我们使用 runtime 运行时来解决这个问题,在 Person+Extension.m 中手动实现。

Person+Extension.m

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

@implementation Person (Extension)

static const void *price = "price";

- (void)setPrice:(NSString *)price {
    objc_setAssociatedObject(self, "price", price, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)price {
    return objc_getAssociatedObject(self, "price");
}

@end

然后再次执行我们的程序,就不会报错了。控制器输出:

QQ20151117-0@2x

示例代码下载:https://blog.6ag.cn/demo/runtime.zip