Runtime总结

741 阅读13分钟

概述

Objective-C语言推迟很多来自编译时和链接时的决策到runtime。只要可能,OC都是动态处理事情。OC的消息机制称为“动态消息机制”。在动态消息机制中,对一个对象发送消息,对象能否响应消息已经不重要,只要别的对象可以响应消息,消息就可以转发给其他对象去响应消息。接收消息的对象要解析处理消息,如果不能处理,系统会对消息的发送者回传一个消息(OC是doesNotRecognizeSelector)。苹果官方的Runtime编程指南。苹果维护的Runtime开源代码。

消息传递和转发

接下来会介绍OC里对一个对象发送消息,消息是怎么传递以及转发的,类的结构又起到什么作用的。 在OC中,对一个对象发送消息,直到抛出异常会经过以下流程:

  1. 消息传递
  2. 消息动态解析
  3. 消息转发
  4. doesNotRecognizeSelector 这个流程后面会详细谈到,在这之前需要做一些准备工作,接着往下看吧。

Class

objc_class结构体的定义:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    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和objc_method_list的定义

//objc_ivar_list
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;
}                                                            OBJC2_UNAVAILABLE;

//objc_method_list
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;
}                                                            OBJC2_UNAVAILABLE;

objc_object与id

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

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

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

objc_cache

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method _Nullable buckets[1]                              OBJC2_UNAVAILABLE;
};

消息传递

在OC中,消息直到runtime时才会绑定到方法实现中。给一个对象发消息,编译器会转化为消息objc_msgSend表达式:

objc_msgSend(receiver, selector)
//或者
objc_msgSend(receiver, selector, arg1, arg2, ...)

objc_msgSend会根据不同的receiver找到它的SEL方法选择器,然后会调用方法实现,最后再把返回值传递回来。objc_msgSend是怎么找到方法的,并将消息进行传递的?

一个初始化的对象实例的结构体是objc_object,objc_object中的第一个变量是isa,通过isa指针可以找到它的类。每个类中包含两个重要的元素。一个指向父类的指针;一个类的方法调度表。方法调度表中存在记录着方法选择器和方法实现的地址关联的条目。

来自苹果网站的图

当一个对象被传送给一个对象时,objc_msgSend会根据对象的isa指针找到对应的类,然后在类的调度表中查找方法选择器。如果找不到它会跟objc_class中的super_class指针找到父类,然后在父类的调度表中查找方法选择器。每次都找不到,就会一直在类的继承链中逐个查找,直到达到NSObject类。找到选择器后,就会调用这个方法,并将接收到的数据结构传递给它。为了加快消息传递的过程,runtime系统会缓存调用过的方法和方法地址。每个类都有自己的缓存,缓存中包含继承的方法和自定义的方法。在查找调度表前,消息传递回先检查接收者的类的缓存。 以上就是消息传递的流程,可以看出方法是动态绑定消息的。

元类

通过objc_class结构体我们可以有个isa指针,这个指针是指向哪的。我们知道objc_object中的isa指针是直向实例对象的类的,isa是个类指针。还有objc_class里只有存储实例的变量和实例方法。哪类方法存在哪里?答案是类中的isa指针指向类的元类。更多关于元类的信息可以点击这里

看下面的这个runtime的动态生成类的方法。可以看出元类和类的关系。

Class objc_allocateClassPair(Class superclass, const char *name, 
                             size_t extraBytes)
{
    Class cls, meta;

    rwlock_writer_t lock(runtimeLock);

    // 如果 Class 名字已存在或父类没有通过认证则创建失败
    if (getClass(name)  ||  !verifySuperclass(superclass, true/*rootOK*/)) {
        return nil;
    }

    //分配空间
    cls  = alloc_class_for_subclass(superclass, extraBytes);
    meta = alloc_class_for_subclass(superclass, extraBytes);

    //构建meta和class的关系
    objc_initializeClassPair_internal(superclass, name, cls, meta);

    return cls;
}

在OC里,有类方法这么一说,也就是类可以响应消息,类也是个对象。当发送消息到类的时候,消息传递会根据类的isa指针去元类的调度表里查找方法选择器。 元类也是个类,结构体组成和类一样的,里面也存在isa指针,就意味着元类还需要一个类,为了避免重复指向类,达到闭环的目的。每个元类都把NSObject的元类当做它们的类。然后根类的元类的isa指针就指向自己的类也就是NSObject类。每个元类可以根据super_class指针找到父元类,以此可以找到NSObject类的元类。

可以验证下这个观点。

//NSObject+Test.h
@interface NSObject (Test)
- (void)speak;
@end

@implementation NSObject (Test)
- (void)speak {
    NSLog(@"NSObject+Test,speak.");
}
@end
//Person
@interface Person : NSObject
+(void)speak;
@end
@implementation Person

@end

Person *person = [Person new];
[Person speak];
[person speak];

输出结果为:

NSObject+Test,speak.
NSObject+Test,speak.

从结果来说,可以证明对Person发送消息,Person的元类中没有实现speak方法,然后会根据isa指针找到NSObject的元类,NSObject的元类中也没有speak方法实现,在由NSObject的元类的isa指针找到NSObject类,然后在方法调度表中找到speak方法,然后执行。

在来看下整体的消息传递流程图:

最后,苹果为什么设计元类呢?把元类类的方法放类里不也可以实现吗?思考下为啥?以下是个人见解: objc_msgSend函数中主要参数是receiver和selector,其他的是参数。对一个对象发送消息,按照现有的设计来说就是走上面我们介绍过的流程。如果把元类中的类方法放在类中,objc_msgSend函数无法确定消接收者是类还是实例对象,也无法确定selector是类方法还是实例方法,需要引进额外的参数去告诉objc_msgSend这些信息。objc_msgSend方法内部就需要去做根据不同情况作出不同情况处理。这样的设计就变得糟糕,如果后面苹果引进元类的类,又要在objc_msgSend的基础上添加额外的参数,这样objc_msgSend的实现就变得复杂,这样的设计破坏了设计模式的单一职责原则。设计元类可以避免这种编程方式,同时还让不同的类各司其职,复用消息传递的机制。

动态方法解析

当消息传递流程结束还没找到响应的selector,会进入动态方法解析流程,这时候可以动态提供一个方法实现去响应消息。可以通过resolveInstanceMethod: 和 resolveClassMethod:方法分别给实例的选择器和类方法动态的提供一个实现。

@interface Person : NSObject
@property(nonatomic,copy)NSString * name;
@end

@implementation Person
@dynamic name;

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(name)){
        class_addMethod([self class], sel,(IMP)dynamicName , "#@:");
        return YES;
    }
    return  [super resolveInstanceMethod:sel];
}
NSString * dynamicName(id self,SEL _cmd){
    return @"dynamic name is ***";
}

@end

在Person.h文件中声明name属性,在.m的类实现中用@dynamic修饰name属性,让编译器不帮你生成setter和getter方法,然后手动实现。继承自Person类的子类的name属性的setter和getter也需要子类去实现。 在外部调用person实例的name的getter方法,在类实现里动态提供一个方法实现去响应消息。

重定向接收者

如果没有处理动态方法解析,会调下面的forwardingTargetForSelector:方法。这时候系统给一个替换消息接收者的机会给你。

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(name)){
        return [Student new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

确保新的接收者可以响应消息,否则会导致程序crash。同时新的接受者不可以是self,否则进入死循环 。

消息转发

如何以上两种都没有处理消息,会进入最后的消息转发流程。系统会调用forwardInvocation:和methodSignatureForSelector:方法。

-(void)forwardInvocation:(NSInvocation *)anInvocation {
    Student *student = [Student new];
    if ([student respondsToSelector:@selector(name)]){
        return [anInvocation invokeWithTarget:student];
    }
    return [super forwardInvocation:anInvocation];
}

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature){
        signature = [Student instanceMethodSignatureForSelector:@selector(name)];
    }
    return signature;
}

系统会先调用methodSignatureForSelector:方法,需要提供一个指定的响应者的实例方法签名给它。签名中记录着方法参数,返回值,参数个数,方法大小等消息。然后系统会根据签名创建NSInvocation对象,然后调用forwardInvocation:方法。forwardInvocation:方法的anInvocationcation参数中封装着未处理消息的seletor,target,参数和返回值信息。可以借由forwardInvocation:方法把未处理的消息分发出去。

最后如果消息转发也无法处理消息,系统会调用doesNotRecognizeSelector:方法然后会抛出一个doesNotRecognizeSelector错误终止程序。

Runtime函数

Runtime系统是由一系列的函数和数据结构组成的公共接口动态共享库。在/usr/include/objc目录下可以看到头文件,可以用其中一些函数通过C语言实现objectivec中一样的功能。

runtime有很多的函数可以操作类和对象。类相关的是class为前缀,对象相关操作是objc或object_为前缀。runtime的函数过多,这里就不一一罗列,下面通过动态穿件类、方法交换、给分类添加属性来展示runtime部分函数用法。

动态创建类

我们来动态生成一个Women类,在给这个类添加一个childrens实例变量和hair属性。

//创建一个women新类和元类
Class womenCls = objc_allocateClassPair(perClass, "Women", 0);
//添加一个childrens变量
class_addIvar(womenCls, "children", sizeof(NSInteger), log(sizeof(NSInteger)), "l");
//添加一个hair属性
objc_property_attribute_t type = {"T", "@\"NSString\""};//变量类型
objc_property_attribute_t ownership = { "C", "" };// C = copy N = nonatomic
objc_property_attribute_t backingivar = { "V", "_hair"};//实例名称
objc_property_attribute_t attrs[] = {type, ownership, backingivar};
class_addProperty(womenCls, "hair", attrs, 3);
//在应用中注册由objc_allocateClassPair创建的类
objc_registerClassPair(womenCls);

class_addIvar函数为新类添加方法,实例变量和属性要在objc_registerClassPair()方法之前,不然不会生效。动态生成的类要注销掉,在运行中还存在这个类或存在子类实例就不能在调用注册方法。系统会抛出EXC_BAD_ACCESS异常错误。

objc_property_attribute_t也是结构体如下:

typedef struct objc_property *objc_property_t;
typedef struct {
     const char *name; // 特性名
     const char *value; // 特性值
} objc_property_attribute_t;

上面给Women类注册hair属性时添加的类型可以查看苹果文档。还可以通过下面的代码去获取一个类的属性,看到它的属性的类型。

id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
    objc_property_t property = properties[i];
    fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}

回到正题上,添加下面的代码查看新生成的类中实例和方法。

unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(womenCls, &outCount);
for (int i = 0; i < outCount; i++) {
    Ivar ivar = ivars[i];
    NSLog(@"womenCls ivar:%s",ivar_getName(ivar));
}
free(properties);
//方法操作
Method *methods = class_copyMethodList(womenCls, &outCount);
    for (int i = 0; i < outCount; i++) {
        Method method = methods[i];
        NSLog(@"method's signature: %s", method_getName(method));
}
free(methods);

运行完代码发现Women类的添加的属性并没有生成实例,setter和getter方法。我们知道property = ivar + setter + getter。这里并没有生成,只是声明了这个属性。需要我们手动去添加实例,setter和getter方法。

class_addProperty(womenCls, "hair", attrs, 3);
class_addIvar(womenCls, "_hair", sizeof(NSString *), log(sizeof(NSString *)), "i");
class_addMethod(womenCls, @selector(chest), (IMP)imp_chest, "v@:");
class_replaceMethod(womenCls, @selector(run), (IMP)imp_grow, "v@:");
class_addMethod(womenCls, @selector(setHair:), (IMP)imp_setHair, "v@:@");
class_addMethod(womenCls, @selector(hair), (IMP)imp_hair, "@@:");

这里的v表示函数返回值为void,@表示对象,:表示SEL。可以查看苹果文档. 添加完运行下代码,可以发现Women类中已经存在hair实例,还有setter和getter方法。

id instance = [[womenCls alloc] init];
[instance performSelector:@selector(setHair:) withObject:@"black"];
NSLog(@"women hair:%@",[instance performSelector:@selector(hair)]);

然后生成实例然后调用下hair属性的setter方法,在调用hairgetter方法,可以看到属性的setter和getter方法可以正常使用了。 performSelector:方法自多可以传两个参数,当需要2个以上参数时候可以使用NSInvocation和Object_sendMsg来实现。

方法交换

Method Swizzling可以在运行时改变selector对应的函数来修改Method的实现。

@interface Person : NSObject
- (void)run;
- (void)sleep;
@end
@implementation Person
- (void)run {
    NSLog(@"Person can run");
}

- (void)sleep {
    NSLog(@"Person can sleep");
}
@end

我们使用Method Swizzling来交换run和sleep的方法实现。具体实现:

+ (void)load {
   static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalSEL = @selector(run);
        SEL swizzledSEL = @selector(sleep);
        Class class = [self class];
        Method originalMethod = class_getInstanceMethod(class, originalSEL);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL);
    //确保要交换的方法存在,就先用class_addMethod和class_replaceMethod函数添加和替换两个方法实现。但如果已经有了要替换的方法,就调用method_exchangeImplementations函数交换两个方法的Implementation。
        BOOL didAddMethod = class_addMethod(class, originalSEL, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if(didAddMethod) {
            class_replaceMethod(class, swizzledSEL, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        }else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
        
    });
}

Person *person = [Person new];
[person run];
person sleep];

可以看到控制台输出结果如下。

Person can sleep
Person can run

关于使用method swizzling的问题

  • Swizzling应该总在+load中执行,OC会在运行时调用+load方法。+load会在类初始加载时调用。+initialize只有在app中第一次给某个类发送消息的时候, 系统才在发送消息之前,先调用+initialize方法。
  • Swizzling应该总是在dispatch_once中执行:swizzling会改变全局状态,所以在运行时采取一些预防措施,使用dispatch_once就能够确保代码不管有多少线程都只被执行一次。这将成为method swizzling的最佳实践。
  • 项目中可能多个地方使用Swizzling对同一个方法进行交换,交换的名字可能是一样的,这时候会造成问题。假如要交换UIView的setFrame:方法到my_setFrame:。可以使用static void MySetFrame(id self, SEL _cmd, CGRect frame); static void (*SetFrameIMP)(id self, SEL _cmd, CGRect frame);方法代替OC方法的命名方式解决问题。
  • 改变代码的行为。像上面例子run变成sleep。还有如果要交换的方法需要调用super父类的实现,交换后就改变原来的代码行为。交换需要慎重。
  • 代码难以理解。
  • 代码难以调试。 更多的method swizzling使用问题可以点击这里查看。

分类添加属性

可以使用管理对象在运行时添加成员变量

@interface Person (Addtion)
@property (nonatomic,assign)NSNumber *age;
@end
const void *ageKey = &ageKey;
@implementation Person (Addtion)
-(void)setAge:(NSNumber *)age {
    objc_setAssociatedObject(self, ageKey, age, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(NSNumber *)age {
    return objc_getAssociatedObject(self, ageKey);
}
@end

总结

整体来说,objc_msgSend是OC发送消息的核心,以及类和实例的结构体的设计对消息传递起了很重要的作用。无论实例方法的查找还是类方法的查找都复用了消息传递同一套机制。消息传递结束无法处理消息会提供机会给你去动态生成方法去响应消息,如果也没有处里动态方法解析,可以重定向消息接受者,乃至进入完整的消息转发。这一系列的流程都表明了OC的消息是动态绑定到方法里的。然后我们可以可以借由runtime的函数除实现了OC类一样的效果,还可以实现更多黑魔法似的功能,例如方法交换等。

参考文献

Objective-C Runtime Programming Guide

Objc Runtime 总结

What are the Dangers of Method Swizzling in Objective-C?

Why is MetaClass in Objective-C?

function/bind的救赎

为什么要设计metaclass

What is a meta-class in Objective-C?