runtime 入门与简介

方法调用 objc_msgSend

调用方法(函数)是语言经常使用的功能,在 Objective-C 中专业一点的叫法是 传递消息(pass a message)。Objective-C 的方法调用都是 动态绑定 ,而C语言中函数调用方式是 静态绑定 ( static binding ),也就是说,在编译时期就能决定和知道在运行时所调用的函数。

以下面代码为例:

void sayHello(){
}
void sayGoodBye(){
}
void saySomething(int type){
    if(type == 0){
        sayHello();
    }else{
        sayGoodBye();
    }
}

基本上,上面的代码在编译的时候编译器就知道 sayHellosayGoodBye 两个函数的存在,函数地址是硬编码在指令之中的。但是如果换一种写法:

void sayHello(){
}
void sayGoodBye(){
}
void saySomething(int type){
    void (*something) ();
    if(type == 0){
        something = sayHello;
    }else{
        something = sayGoodBye;
    }
    something();
}

这就得使用 动态绑定 ,待调用的函数地址需要到运行时才能读取出来。
在 Objective-C 中,对某一个对象传递消息,会用动态绑定机制来决定到底是调用哪个方法。而Objective-C是 C 的超集,底层是由 C语言实现,但是对象接收消息后会调用哪个方法都是在运行期决定。

给对象发送消息可以这么来写:

id object = [list objectAtIndex:1];

在这行代码中, list 称为 接收者objectAtIndex 叫做 选择器, 选择器和参数合起来称为消息。当编译器看到这行代码的时候,会换成标准的C语言函数调用:

void objc_msgSend(id self, SEL cmd, ...);
id lastObject = objc_msgSend(list, @selector(objectAtIndex:), parameter);

objc_msgSend 这个函数可以接收两个及两个以上的参数,第一个参数是接收者,第二个参数是选择器,后面的参数是保持顺序的原来消息传递的参数,objc_msgSend会依据接收者和选择器来决定调用哪个方法,首先在接收者的方法列表中寻找,如果找不到就会沿着继承体系去向上一层一层的寻找,如果仍旧找不到就会执行消息转发(message forwarding)
当消息第一次传递之后,objc_msgSend 会将匹配结果进行缓存,下次会直接调用方法。消息传递除了objc_msgSend之外在特殊情况下还会有其他的方法来处理:

  • objc_msgSend_stret 如果待发送的消息返回一个结构体,就会调用这个函数来处理。

  • objc_msgSend_fpret 如果消息返回的是浮点数,就会调用这个函数进行处理。

  • objc_msgSendSuper 如果要传递消息给父类。

总结:

  • 消息由 接收者、选择器及参数构成,给某对象 发送消息( invoke a message ) 也就相当于在该对象上调用方法。

  • 发送给某对象的全部消息都要有动态消息派发系统( dynamic message dispatch system ) 来处理。

消息转发

在上面介绍了运行时的消息传递机制,但是却没有说对象收到消息却无法解读该怎么办。本篇博客就着重介绍当消息传递时无法解读的时候就会启动的 消息转发机制( message forwarding )

开发可能经常会遇到这种情况:

2016-04-20 13:14:07.391 runtime[1096:22076] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[AutoDictionary setDate:]: unrecognized selector sent to instance 0x100302f50'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff9f2d94f2 __exceptionPreprocess + 178
    1   libobjc.A.dylib                     0x00007fff90db3f7e objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff9f3431ad -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
    3   CoreFoundation                      0x00007fff9f249571 ___forwarding___ + 1009
    4   CoreFoundation                      0x00007fff9f2490f8 _CF_forwarding_prep_0 + 120
    5   runtime                             0x0000000100001c1c main + 124
    6   libdyld.dylib                       0x00007fff91df85ad start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

这个异常信息是由 NSObjectdoesNotRecognizeSelector: 方法抛出来的,本来是给 AutoDictionary 的一个实例对象发送消息,但是该对象并没有 setDate: 方法,所以消息转发给了 NSObject ,最后抛出异常。

先看下消息处理机制流程图:

runtime 入门与简介

消息转发分为两阶段三步,第一阶段先看接受消息的对象能不能自己处理这个无法解读的消息,这一步可以动态的添加方法去解读接受这个消息;第二阶段是先看看对象自己不能处理这个消息,能不能交给其他对象来进行处理,在这一步如果仍然无法解读消息,那么就会走最后一步:把和消息有关的所有细节封装到一个 NSInvocation 中,再询问一次对象是否能解决。
看下三个方法:

// 询问对象是否自己处理,是返回YES,一般会在这个方法里面动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel;

// 这一步询问对象把消息交给哪个对象来进行处理
- (id)forwardingTargetForSelector:(SEL)aSelector;

// 如果走到这一步的话,就把消息的所有信息封装成 NSInvocation 对象进行 "最后通牒"
- (void)forwardInvocation:(NSInvocation *)anInvocation;

来一段代码示例:
新建一个 AutoDictionary 类,添加一个 NSDate 类型的 date 属性,在实现文件里面用 @dynamic date; 禁止自动生成存取方法,这样当代码中给 AutoDictionary 实例对象的 date属性赋值时就会出现消息无法解读的现象。
.h 文件:

@interface AutoDictionary : NSObject

@property (nonatomic, strong) NSDate *date;

@end

.m 实现文件代码内容:

@interface AutoDictionary()
@property (nonatomic, strong) NSMutableDictionary *backingStore;

/**
 *  该类仅在实现文件 实现了
 *  - (NSDate *)date
 *  - (void)setDate:(NSDate *)date
 *  两个方法,用于处理 AutoDictionary 无法解读的消息
 */
@property (nonatomic, strong) MethodCreator *methodCreator;
@end
@implementation AutoDictionary

@dynamic date;

- (instancetype)init{
    if (self = [super init]) {
        self.backingStore = [NSMutableDictionary dictionary];
        self.methodCreator = [MethodCreator new];
    }
    return self;
}

#pragma mark - 消息转发机制 :1.动态添加方法 2.后备消息接收者 3.封装NSInvocation,最后通牒
// 3. 封装NSInvocation,最后通牒
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    
}
// 2. 无法接受消息,选择由谁来接受
- (id)forwardingTargetForSelector:(SEL)aSelector{
    return self.methodCreator;
}
// 1. 动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSString *selString = NSStringFromSelector(sel);
    
    if ([selString hasPrefix:@"set"]) {
        class_addMethod(self, sel, (IMP)autoDictSetter, "");
    }else{
        class_addMethod(self, sel, (IMP)autoDictGetter, "");
    }
    
    return YES;
}

id autoDictGetter (id self, SEL _cmd){
    
    AutoDictionary *dict = self;
    NSString *key = NSStringFromSelector(_cmd);
    return [dict.backingStore objectForKey:key];
}

void autoDictSetter (id self, SEL _cmd, id value){
    
    AutoDictionary *dict = self;
    
    NSString *selString = NSStringFromSelector(_cmd);
    
    NSString *key = [selString substringWithRange:NSMakeRange(3, selString.length-4)];
    
    key = [key lowercaseStringWithLocale:[NSLocale currentLocale]];
    
    if (value) {
        [dict.backingStore setObject:value forKey:key];
    }else{
        [dict.backingStore removeObjectForKey:key];
    }
}

@end

测试代码:

AutoDictionary *dict = [AutoDictionary new];
dict.date = [NSDate date];
NSLog(@"dict.date = %@",dict.date);

给对象、分类添加实例变量

在开发中有时候想给对象实例添加个变量来存储数据,但又无法直接声明,比如说既有类的分类。这个时候我们就可以通过 关联对象 在运行时给对象关联一个 对象 来存储数据。(注意:并不是真实的添加了一个实例变量)

关联对象 可以给某个对象关联其他对象并用key来区分其他对象。需要注意的是,存储对象的时候要指明 存储策略,用来维护对象的内存管理语义。存储策略是 objc_AssociationPolicy 枚举定义,以下是存储策略对应的 @property属性:

存储策略类型对应的@property属性
OBJC_ASSOCIATION_ASSIGNweak
OBJC_ASSOCIATION_RETAIN_NONATOMICstrong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMICcopy, nonatomic
OBJC_ASSOCIATION_RETAINstrong
OBJC_ASSOCIATION_COPYcopy

用下面的方法可以管理关联对象:

// 这个方法可以根据指定策略给对象关联对象值
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

// 这个方法可以获取对象关联对象值
id objc_getAssociatedObject(id object, const void *key)

// 这个方法可以删除指定对象的全部关联对象值
void objc_removeAssociatedObjects(id object)

对于关联对象这个OC特性,我们可以把对象想象成一个 NSDictionary,关联对象需要一个 key( 类型是 opaque pointer,无类型的指针 ) 来区分,我们可以把要添加的变量名作为 key ,把变量的值作为关联的对象来存储到 ”对象“ 这个 NSDictionary 中。
所以,关联对象的

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

方法类似于字典的

[dict setObject: forKey:]

方法。

在存储和获取关联对象时需要用一个相等的 key ,因为是给 Class 的实例对象关联对象,所以一般用静态变量来做 key

说的再多,不如上段代码!

比如说,我们给 NSString 实例加上个 NSDate 类型的 date 变量。什么?给字符串加个日期变量是要干袅?我要给字符串过个生日不行吗! 别闹,举个栗子嘛!(捂脸逃跑~~~)

首先,我们先给 NSString 新建个名为 RT 的 category。
在头文件中有个 NSDate 类型的 date 属性:

//  NSString+RT.h
//  runtime
#import <Foundation/Foundation.h>

@interface NSString (RT)

@property (nonatomic, strong) NSDate *date;

@end

在分类中的属性只会生成 getset 方法,并不会生成变量。
所以我们需要重写 getset 方法,关联对象以变相实现添加变量,在现实文件中:

//  NSString+RT.m
//  runtime
#import <objc/runtime.h>
#import "NSString+RT.h"

@implementation NSString (RT)

static void *runtime_date_key = "date";
- (NSDate *)date{
    return objc_getAssociatedObject(self, runtime_date_key);
}

- (void)setDate:(NSDate *)date{
    objc_setAssociatedObject(self, runtime_date_key, date, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

需要注意的是,关联对象用到的 key 是个无类型的指针,一般来说是静态来修饰。
另外,给对象关联的只能是对象,如果是 intfloat 等类型需要 NSNumber 进行包装。
因为 date 是强引用和非原子属性,所以关联策略用 OBJC_ASSOCIATION_RETAIN_NONATOMIC

然后执行代码:

NSString *string = @"runtimeTestString";
string.date = [NSDate date];
NSLog(@"string.date = %@",string.date);

输出结果:

2016-04-12 21:27:31.099 runtime[2837:103727] string.date = 2016-04-12 13:27:31 +0000

注意:

  • 定义关联对象时需要指定内存管理语义,用来模拟对象对变量的拥有关系

  • 尽量避免使用关联对象,因为如果出现bug不易于问题排查

iOS 开发中的 AOP

Objective-C 中,类的方法列表会把选择器的名称映射到方法的实现上,这样 动态消息转发系统 就可以以此找到需要调用的方法。这些方法是以函数指针的形式来表示,这种指针叫做 IMP
如下:

runtime 入门与简介

id (*IMP) (id, SEL, ...)

Objective-C 的 runtime 机制以此提供了获取和交换映射IMP的的接口:

// 获取方法
Method class_getInstanceMethod(Class cls, SEL name);

// 交换两个方法
void method_exchangeImplementations(Method m1, Method m2)

我们可以通过上面两个方法来进行选择器和所映射的IMP进行交换:

runtime 入门与简介

来,直接上代码示例,比如我们的要实现功能是在每个控制器的viewDidLoad方法里面log一下,一般有三种实现方式:

  1. 直接修改每个页面的 view controller 代码,简单粗暴;

  2. 子类化 view controller ,并让我们的 view controller 都继承这些子类;

  3. 使用 Method Swizzling 进行 hook,以达到 AOP 编程的思想

第一种实现的代码是在每个类的里面都这么写:

- (void)viewDidLoad {
    [super viewDidLoad];
    DDLog();
}

第二种是只在基类里面写。然后所有的控制器都继承这个基类。
最后一种是最佳的解决方案:

@implementation UIViewController (Log)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(log_viewDidLoad);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling

- (void)log_viewDidLoad{
    [self log_viewDidLoad];
    DDLog(...);
}

@end

注意:

  • 为什么使用 + (void)load ?因为父类、子类和分类的该方法是分别调用,互不影响,而且是在类被加载的时候必定会调用的方法。



本文首发于https://iosgg.cn/tags/#Objc/runtime

相关推荐