iOS 的那些事儿

OC-Runtime的理解和简单使用

2017-11-02  本文已影响570人  楚槟夕

Runtime是OC里面非常重要的一个概念,它是OC的底层实现,也正是因为Runtime,OC成为一个动态语言,并且拥有了面向对象的能力。这篇文章,将详细说明Runtime的各种知识,并且能够实际运用。

学习Runtime可以使我们更加清楚地了解OC语言的底层实现,从而可以运用它去实现很多OC语言实现不了的功能(比如给Category添加属性)。

在没有接触Runtime之前,我们对OC的类和对象只有概念上的理解,并不知道它本质上是什么。现在我们来看看它们的底层定义,我们先从我们经常使用的id入手,在<objc/objc.h>中,我们找到这一段定义:

/// A pointer to an instance of a class.
typedef struct objc_object *id;

这里说明id是一个指向objc_object结构体的指针,而注释又说,这个指针指向一个类的实例对象,所以我们知道了,objc_object结构体就代表了一个类的实例对象:

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

objc_object这个结构体里面,只有一个isa,这个isa的类型是Class,并且是不能为空的。顾名思义,这个肯定就是这个对象的类型了。
然后我们再去找找Class又是什么东西:

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

这里我们可以看到,Class是一个指向objc_class结构体的指针。所以我们知道了,isa是一个指向objc_class的指针,即通过它可以找到一个对象的类。所以,id类型其实就是一个指向任意类型实例的指针。接着,我们再去看看objc_class是什么东西:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                       OBJC2_UNAVAILABLE;  // 父类

    const char *name                        OBJC2_UNAVAILABLE;  // 类名
    long version                            OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
    long info                               OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识

    long instance_size                      OBJC2_UNAVAILABLE;  // 类的实例变量大小
    struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 类的成员变量链表

    struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定义的链表
    struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法缓存

    struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 协议链表
#endif

} OBJC2_UNAVAILABLE;

我们先从第一行可以得知,OC的类里面,也有一个isa指针,而这个指针指向了这个类的类型,由此我们可以推断出来,其实OC的类本质上也是一个对象,因为它也有自己的类。
在OC里面,每一个类的isa指针都指向它的元类,最终指向NSObjectNSObject的元类是它自己。而NSObject的父类则是nil。这张图很好的说明了isasuper_class的区别:


接着,我们可以看到,一个类的结构体里面,还有它的父类,类名,版本号,类信息,变量大小,变量列表,方法列表,方法缓存,协议列表这些东西。
这里也解释了为什么前面的objc_object里面只有一个isa指针,是因为只要有了这个指针,就能够找到这个类里面的所有方法和属性。
看了这些,我们就对OC的类和对象有了更深刻的理解。接下来,我们再去探究一下OC是怎么调用方法的。

OC的方法调用底层是给某个对象发送某个方法。并且,在编译的时候并没有确定具体调用哪个方法,只有在运行时才能确定。我们来验证一下,首先随便创建一个空的命令行项目:



然后创建一个Person类,然后写一个空的方法:

.h
@interface Person : NSObject

- (void)run;

@end

.m
- (void)run {
    
}

然后打开终端,cd到刚刚创建的main.m文件所在的文件夹下,执行clang -rewrite-objc main.m
这时候,就可以在文件夹里面看到一个main.cpp文件,打开,拉到最下面,就可以看到这一段代码:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));
    }
    return 0;
}

可以看到我们刚刚写的方法编译过之后全部变成了objc_msgSend的方式,甚至allocinit方法也变成了这样。这就说明OC方法的底层调用都是objc_msgSend实现的,而objc_msgSend就是消息发送。同时,也验证了OC底层就是用Runtime实现的C语言代码。

接下来我们来研究一下这个objc_msgSend。要使用它,得首先导入#import <objc/message.h>,然后就可以使用了。但是,我们打出来这个发现没有任何参数提示:


这是因为苹果公司不建议我们这么用了。我们可以在Build Setting里面,找到

改成NO,再回去敲入objc_msgSend,就可以看到提示了:

具体的参数含义是:
id _Nullable selfid类型我们前面知道,它可以指向任意OC对象,这地方就代表着给谁发消息,也就是调用谁的方法。
...:三个点代表参数列表/可扩展参数。
SEL _Nonnull op:SEL又是什么呢?到这里,我们就得提一下OC里面的方法了。老规矩,我们先去找定义,在<objc/runtime.h>中:
struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

这里我们可以看到,OC的方法里面,有三个东西,一个是SEL,一个是char * ,一个是IMP,这个char *我们知道是C语言的字符串,这个地方是一组描述方法的参数类型的字符数组,后面我们会详细了解这个东西,这里先不管它。SEL这个地方通过命名我们可以看出来是方法名,IMP我们就完全看不出了,它们具体是怎么定义的呢?还是在<objc/objc.h>中:

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

这里,我们可以看到,IMP其实就是一个指向方法实现的指针,而SEL则是一个objc_selector结构体,源码中我们找不到SEL的定义,经过查阅资料得知,它完全可以理解为一个char *,也就是说,其实它就是方法名的字符串,也就是一个方法的标签。
知道这些以后,我们就可以用消息发送来改写以前的OC代码,比如:
前面的Person类的run方法的调用:

objc_msgSend(p, @selector(run));

为了方便测试,我们给run方法写一个简单的实现:

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

然后运行一下:



完美!

甚至Person类对象的声明都可以用发送消息的方式来完成(解耦合):

Person *p = objc_msgSend([Person class], @selector(alloc));
p = objc_msgSend(p,@selector(init));

再运行一下,一样可以得到之前的结果,这里就不贴图了,跟上面那个图一样。
Runtime为我们提供了直接通过类名获取类的函数:

objc_getClass(char * _Nonnull name);

和得到一个SEL的函数:

sel_registerName(const char * _Nonnull str);

这样我们可以继续改进之前的代码:

id p = objc_msgSend(objc_msgSend(objc_getClass("Person"), sel_registerName("alloc")),sel_registerName("init"));
objc_msgSend(p,sel_registerName("run"));;

一样可以运行得到结果。
到了这一步,是不是就跟我们之前看到的编译后的OC代码一样了?我们甚至不需要导入Person.h头文件,就可以直接获取创建它的实例,并且执行方法,完成了解耦。

看了上面的一些代码,不知道你有没有考虑过一个问题。发送消息的时候,我们只需要填一些字符串参数之类的就可以了,完全不知道有没有这个方法,如果没有这个方法会发生什么事情呢?
接下来,我们做个试验:



直接crash掉了。调用方法时,如果在方法在对象的类继承体系中没有找到,那怎么办?一般情况下,程序在运行时就会Crash掉,抛出 unrecognized selector sent to …类似这样的异常信息。但在抛出异常之前,还有三次机会按以下顺序让你拯救程序。这就涉及到以下4个方法:

首先会调用+ resolveInstanceMethod:(对应实例方法)或+ resolveClassMethod:(对应类方法)方法,让你添加方法的实现。如果你添加方法并返回YES,那系统在运行时就会重新启动一次消息发送的过程。
我们这里测试一下,增加一个wahaha方法的实现,看看是否可以顺利运行,首先,我们在Person.m中导入#import <objc/message.h>,然后,利用

class_addMethod(Class  _Nullable __unsafe_unretained cls,SEL  _Nonnull name,IMP  _Nonnull imp, const char * _Nullable types)

来增加一个方法及实现,其中,第一个参数填self,第二个参数填wahaha的SEL,可以用@selector(wahaha),也可以用之前用过的sel_registerName("wahaha"),第三个参数需要一个imp,我们知道IMP是指向方法实现的指针,这里我们可以用imp_implementationWithBlock(id _Nonnull block)来实现,最后一个参数我们之前也见过,就是方法定义里面的的method_types,这个东西该怎么写呢?我们先去查一下官方文档:

types
An array of characters that describe the types of the arguments to >the method. For possible values, see Objective-C Runtime >Programming Guide > Type Encodings. >Since the function must take at least two arguments—self and _cmd, the second and third characters must be “@:” (the first character is the return type).

这里面说明这个是一组描述方法的参数类型的字符数组,并且,每个方法都有两个被隐含的参数,一个是self(代表当前对象),一个是_cmd(代表当前对象的SEL),所以第二个和第三个字符必须是@:,而第一个字符是返回值,所以一个没有参数的方法,它的types就是"v@:"。至于什么类型对应什么字符,可以去上面的链接中找。所以我们这里可以直接用"v@:"

//如果增加了方法并返回YES,就会重新发送消息并处理,返回NO,则进入下一步
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == sel_registerName("wahaha")) {
        class_addMethod(self, sel_registerName("wahaha"), imp_implementationWithBlock(^(){
            NSLog(@"wahaha");
        }), "v@:");
    }
    return YES;
}

运行结果:


怎么样,是不是很神奇?如果上面返回NO,则会进入完整的消息转发机制(full forwarding mechanism),这里又分为两个步骤:

这个时候,如果实现了- forwardingTargetForSelector:方法,系统就会进入该方法继续处理消息,这个方法的作用是把之前没办法处理的消息转发给别的对象去处理:

//返回一个对象继续处理消息
- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == sel_registerName("wahaha")) {
        return [Dog new];
    }
    return nil;
}

这里我们新建了一个Dog类,实现了wahaha方法,所以我们直接返回一个Dog的实例,最后运行结果如上,这里就不贴图了。

如果上一步也没有对消息进行处理,则会进入最后一步,这里涉及到两个方法。它首先调用methodSignatureForSelector:方法来获取函数的参数和返回值,如果返回为nil,程序会Crash掉,并抛出unrecognized selector sent to instance异常信息。如果返回一个函数签名,系统就会创建一个NSInvocation对象并调用-forwardInvocation:方法。我们同样在这里对之前的消息进行处理一次:

//返回方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == sel_registerName("wahaha")) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

//转发消息
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    Dog *dog = [Dog new];
    if ([dog respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:dog];
    }
}

以上就是OC消息传递过程中发生的事情,利用这些我们可以在很多地方对一个消息做处理。但是我们该怎么选择呢?

  1. 动态方法解析:由于Method Resolution不能像消息转发那样可以交给其他对象来处理,所以只适用于在原来的类中代替掉。
  2. 快速消息转发:其他对象,使用范围更广,不只是限于原来的对象。
  3. 普通消息转发:它一样可以消息转发,但它能通过NSInvocation对象获取更多消息发送的信息,例如:target、selector、arguments和返回值等信息。

同时需要注意的是,消息转发过程中,步骤越往后,处理消息的代价就越大,最好能在第一步就处理完,这样的话,运行期系统可以将此方法缓存。如果这个类的实例还会再接收到同名选择子,那么根本无须再次启动消息转发流程。
通过消息发送,我们其实已经对Runtime做了一个简单的运用了。接下来,我们再多一些探讨。

  1. 在程序运行的时候动态添加一个类
  2. 在程序运行的时候动态的修改一个类的属性和方法
  3. 在程序运行的时候遍历一个类的所有属性

Runtime有很多方法,可以在文档中一一查看,不同功能的方法通过前缀区分,比如说class_就是对类的操作,objc_就是对对象的操作,等等,都比较好理解。

了解这些以后,我们再回头看之前提过的一个问题,就是给Category增加属性
我们先看一下,如果直接给Category增加属性会发生什么,我们给Person类创建一个Play分类,然后添加一个属性:

@interface Person (Play)

@property (nonatomic, strong) NSString *gameName;

@end

但是我们使用的时候会发现,直接就Crash了:


我们现在应该就能明白,这是因为找不到Getter,同时也没有Setter,我们可以给它添加这两个方法,但是我们在给它添加的时候,发现在Category里面根本就没有_gameName这个变量,所以没办法像别的类那样直接添加,这时候,我们就可以利用Runtime的objc_setAssociatedObjectobjc_getAssociatedObject来实现:
- (NSString *)gameName{
    return objc_getAssociatedObject(self, _cmd);
}
- (void)setGameName:(NSString *)gameName {
    objc_setAssociatedObject(self, @selector(gameName), gameName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

再次运行,就可以正常使用这个属性了:

通过以上简单的例子,我们可以看得出来,Runtime是OC代码的底层实现,所以很多OC代码不支持的事情,我们都可以通过Runtime自己去实现,具体在什么场景下使用要根据实际需求做具体分析。接下来,我会用Runtime和之前Block种提到的相关技术自己实现KVO,并且进行一些改造。另外,本篇文章的代码可以在我的github上查看,点击前往

上一篇下一篇

猜你喜欢

热点阅读