美文网首页
认识OC运行时系统Runtime

认识OC运行时系统Runtime

作者: 读行笔记 | 来源:发表于2020-11-23 22:38 被阅读0次

简介

我们都知道Objective-C是一门动态语言,也就是说它会将很多工作从编译器推迟到运行时,比如类、方法、属性的确定。这意味着Objective-C不仅仅需要编译器,也需要一个运行时系统。

这个运行时Runtime系统赋予Objective-C这门语言强大的能力,让开发者可以在程序运行时创建、修改类及其对象和它们的属性、方法,同时也使得C语言具有了面向对象的能力。

Objective-C的Runtime不但强大,而且高效,因为它是用C语言和汇编语言写成的。具体一点,它是由一系列数据结构和函数组成的动态公共接口,在/usr/bin/objc/目录下,在项目中只需引入<objc/runtime.h>即可看见所有接口。

类与对象

数据结构

在实际操作中,我们可以从三个层面与Runtime系统进行交互,分别是通过OC源码、NSObject定义的方法,以及通过直接调用runtime函数。而这三种方式的运行,都依赖于一些基础性定义,下面就来谈谈。

id

如果要问OC中“动态性”最强的类(型)是哪个,那结果一定是id,因为id可以在编译期代表任意类型而不受约束,这就给程序提供了非常灵活的可能性。

从定义中可以看出,id是一个指向objc_object结构体的指针,objc_object结构体中只有一个名为isa的Class类型,而Class又是一个指向objc_class结构体的指针。

/// id是一个指向objc_object的指针
/// 来自https://opensource.apple.com/source/objc4/objc4-646/runtime/objc-private.h.auto.html
typedef struct objc_object *id;

/// objc_object是一个只含有名为isa的Class组成的结构体
struct objc_object {
    private:
        isa_t isa;

    public:
        // ISA() assumes this is NOT a tagged pointer object
        Class ISA();

        // getIsa() allows this to be a tagged pointer object
        Class getIsa();

        void initIsa(Class cls /*indexed=false*/);
        void initClassIsa(Class cls /*indexed=maybe*/);
        void initProtocolIsa(Class cls /*indexed=maybe*/);
        void initInstanceIsa(Class cls, bool hasCxxDtor);
        // ...

    private:
        void initIsa(Class newCls, bool indexed, bool hasCxxDtor);

        // Slow paths for inline control
        id rootAutorelease2();
        bool overrelease_error();

    #if SUPPORT_NONPOINTER_ISA
        // Unified retain count manipulation for nonpointer isa
        id rootRetain(bool tryRetain, bool handleOverflow);
        bool rootRelease(bool performDealloc, bool handleUnderflow);
        id rootRetain_overflow(bool tryRetain);
        bool rootRelease_underflow(bool performDealloc);
        // ...
    #endif

    #if !NDEBUG
        bool sidetable_present();
    #endif
};


/// Class是一个指向objc_class的指针
typedef struct objc_class *Class;


/// OBJC1对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;


/// OBJC2对objc_object的定义
/// 来自https://opensource.apple.com/source/objc4/objc4-646/runtime/objc-runtime-new.h.auto.html
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    class_rw_t *data() { 
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
    // ...
}

由此,我们可以得知:对象objc_object的isa指针指向类objc_class,而类objc_class的isa指针又指向类的类,即元类,所有的元类指向根类rootclass,rootclass指向自己,这是水平方向的继承体系,而在垂直方向上的继承体系内,objc_class还有指向父类的superclass指针。另外,为了提高性能,还有缓存cache等。

对象和类的继承关系

SEL

在运行时,SEL表示方法的名称,它是类内唯一的C字符串。可以通过sel_registerName在运行时注册SEL。

SEL的定义如下:

typedef struct objc_selector *SEL;

IMP

SEL是方法的名称,IMP则是方法的具体实现,是一个函数指针,是由编译器生产的。当通过OC给对象发送消息时,最终执行的就是IMP所指的代码。SEL和IMP是成对出现的。

IMP的定义如下:

typedef void (*IMP)(void id, SEL, ... );

Method

顾名思义,Method表示一个方法,由三部分组成:

  • SEL,方法名称;
  • IMP,方法实现;
  • types,参数类型和返回值类型。

它的定义如下:

typedef struct method_t *Method;

struct method_t {
    SEL name;
    const char *types;
    IMP imp;

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};

操作指南

能够通过OC实现的功能,比如setter、getter、创建对象、添加方法等等,都可以通过Runtime提供的能力实现。下面就单纯从语言设计层面了解一下。

类对象

类定义相关

// 获取当前已注册的所有类
int objc_getClassList ( Class *buffer, int bufferCount );

// 创建并返回一个指向所有已注册类的指针列表
Class * objc_copyClassList ( unsigned int *outCount );

// 返回指定类的类定义
Class objc_lookUpClass ( const char *name );
Class objc_getClass ( const char *name );
Class objc_getRequiredClass ( const char *name );

// 返回指定类的元类
Class objc_getMetaClass ( const char *name );

通过实例对象获取类的信息

// 获取给定实例对象的类名
const char * object_getClassName ( id obj );
// 获取实例对象的类
Class object_getClass ( id obj );
// 设置实例对象的类
Class object_setClass ( id obj, Class cls );

实例对象

对整个对象的操作

// 返回指定对象的一份拷贝
id object_copy ( id obj, size_t size );
// 释放指定对象占用的内存
id object_dispose ( id obj );

操作实例变量

// 修改实例变量的值
Ivar object_setInstanceVariable ( id obj, const char *name, void *value );
// 获取实例变量
Ivar object_getInstanceVariable ( id obj, const char *name, void **outValue );
// 获取实例变量的值
id object_getIvar ( id obj, Ivar ivar );
// 设置实例变量
void object_setIvar ( id obj, Ivar ivar, id value );

对象关联

在常规代码实践中,如果要让一个对象和另一个对象产生关联,只有一个办法——让另一个对象成为它的属性property,这需要事先定义好,也就是要在编译期之前确定好。而如果要在运行时动态关联对象,只有通过Runtime提供的能力才能事先。

方法包含:

  • objc_setAssociatedObject:设置
  • objc_getAssociatedObject:获取
  • objc_removeAssociatedObjects:删除

关联策略包含:

  • OBJC_ASSOCIATION_ASSIGN
  • OBJC_ASSOCIATION_RETAIN_NONATOMIC
  • OBJC_ASSOCIATION_COPY_NONATOMIC
  • OBJC_ASSOCIATION_RETAIN
  • OBJC_ASSOCIATION_COPY

static char dynamicKey;

- (void)setAndGetDynamicObject{
    if (objc_getAssociatedObject(self, &dynamicKey)) {
        objc_removeAssociatedObjects(objc_getAssociatedObject(self, &dynamicKey));
    }

    id objcSet = [self generateDynamicObject];
    objc_setAssociatedObject(self, &dynamicKey, objcSet, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    id objcGet = objc_getAssociatedObject(self, &dynamicKey);
    
    NSLog(@"my dynamic object is %@", objcGet);
}

- (id)generateDynamicObject{
    NSArray *objecs = @[@1, @3, @"Wang", @YES];
    NSUInteger idx = arc4random()%4;
    return [objecs objectAtIndex:idx];
}

消息

Objective-C把一切操作称之为发送消息,比如[someone doSth]表示给someone发送了一条名为doSth的消息。只有在运行时,消息才会和实现进行绑定。

发送消息

objc_msgSend

所有的消息,最终都会被编译器用objc_msgSend发送给接收者,第一个参数是消息接受者receiver,第二个是一个Selector,如果有其他参数直接跟在后面。

objc_msgSend(receiver, selector)
objc_msgSend(receiver, selector, arg1, arg2, ...)

objc_msgSend通过前两个参数receiverselector便可以容易的定位到具体的方法实现,然后将参数传递给具体的方法并执行,最后返回执行结果。

这里的关键之处在于前面提到的对象和类的数据结构objc_objectobjc_class。对于objc_class的数据结构,里面包含指向父类superclass的指针,以及一个方法分派表,将所有的方法selector和IMP对应起来。而对于objc_object的数据结构,里面的isa指针指向它所属的类,也就是指向一个objc_class

这样,对象和类的继承体系就被互相联系在一起了,如果某个对象在类的方法分派表中找不见对应的方法,则顺着它的继承关系依次在父类、父类的父类…中寻找,直到最顶层的根类NSObject,如果还没有找见,则给出异常信息。

当然,在实现细节上还有其他要点,比如为了提高消息被处理的速度,运行时系统会缓存方法selector和IMP。

另外,编译器会根据情况在objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或 objc_msgSendSuper_stret四个方法中挑选一个来调用。如果消息是传递给超类,那么会调用名字带有Super的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有stret的函数。

对象继承关系

self _cmd

objc_msgSend找到对应的方法后,在传递参数时,还会附带传递两个隐藏参数self_cmd,前者代表消息的实际接收者,后者表示此消息的selector。实际上,它们是在编译期自动被加入的。

- (id)strange {
    id  target = getTheReceiver();
    SEL method = getTheMethod();
 
    if ( target == self || method == _cmd )
        return nil;
    return [target performSelector:method];
}

methodForSelector:

动态绑定很灵活,但在某些特殊的场景中将受到性能影响,比如要在短时间内给某个对象多次发送消息时。

这时候,更加合理的做法是先通过NSObject定义的methodForSelector:获取方法的地址,然后直接调用,这将省去不少时间开销。

void (*setter)(id, SEL, BOOL);
int i;
 
setter = (void (*)(id, SEL, BOOL))[target
    methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
    setter(targetList[i], @selector(setFilled:), YES);

消息转发

如果消息接收者并不能响应某个消息,那么在运行时系统给出异常之前,还会通过消息转发机制寻求其他可能。具体会经过下面三个流程:

  • 动态方法解析
  • 重定向接收者
  • 最后的转发

动态方法解析

可以先通过实现 resolveInstanceMethod:resolveClassMethod:,然后通过class_addMethod动态添加一个方法的实现。

void dynamicMethodIMP(id self, SEL _cmd) {
    NSLog(@"dynamic method implementation: %@", NSStringFromSelector(_cmd));
}

// 实例方法的动态解析
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    SEL aSel = NSSelectorFromString(@"someDynamicMethod");
    if (sel == aSel) {
        class_addMethod([self class], aSel, (IMP)dynamicMethodIMP, "v@:");
    }
    return [super resolveInstanceMethod:sel];
}

// 类方法的动态解析
+ (BOOL)resolveClassMethod:(SEL)sel{
    SEL aSel = NSSelectorFromString(@"someDynamicMethod");
    if (sel == aSel) {
        class_addMethod([self class], aSel, (IMP)dynamicMethodIMP, "v@:");
    }
    return [super resolveClassMethod:sel];
}

重定向接收者

如果经过上面的动态方法解析之后,依然无法处理消息。这时候运行时系统就会通过 forwardingTargetForSelector: 提供一次替换消息接收者的机会。注意:新替换的对象一定不能为self,因为这样会进入死循环。

@implementation RuntimeExample{
    RunLoopExample *runloop;
}

- (instancetype)init{
    if ((self = [super init])) {
        runloop = [[RunLoopExample alloc] init];
    }
    return self;
}

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"forwarding target");
    if ([NSStringFromSelector(aSelector) isEqualToString:NSStringFromSelector(@selector(addTimerForCommonMode))]) {
        NSLog(@"forwarding to 'runloop'");
        return runloop;
    }
    return [super forwardingTargetForSelector:aSelector];
}

最终的转发

如果经过上面两个途径依然无法处理某个消息,那么就会进入比较”沉重“的最后一个消息转发步骤了。

通过重写forwardInvocation:这个在NSObject中定义的方法,根据它的唯一参数——一个封装了原始消息相关信息的NSInvocation对象,将其解构、分析之后给出对应的处理措施。

要注意的是,重写forwardInvocation:时,还要重写另一个方法methodSignatureForSelector:,用来生成forwardInvocation:的唯一参数NSInvocation的方法签名。

@implementation RuntimeExample{
    RuntimeHelper *anotherObject;
}

- (instancetype)init{
    if ((self = [super init])) {
        anotherObject = [[RuntimeHelper alloc] init];
    }
    return self;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    SEL sel = [anInvocation selector];
    if ([anotherObject respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:anotherObject];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature && [anotherObject respondsToSelector:aSelector]) {
        signature = [anotherObject methodSignatureForSelector:aSelector];
    }
    return signature;
}

另外,基于消息转发机制,还能模拟继承和多继承。具体而言,就是在forwardInvocation:中将所有继承对象的方法按规则分派出去,就像是自己的方法一样。

虽然OC的消息转发机制很强大也很灵活,但应该谨慎使用,因为它不但让程序的逻辑变得更加复杂,而且增加了程序的开销,使用过多必然会降低性能。

Method Swizzle

通过上面这些方法已经灵活地实现多数功能,但还是有点限制——需要操纵源码,对于无法获取源码的,比如Apple官方提供的一些能力,显然无法直接修改。在这种情况下,可以用Method Swizzle进行“偷梁换柱”,用一个方法替换另一个方法就能实现直接无法实现的功能。

比如下面的例子在运行时替换了两个方法的实现。

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL sel1 = @selector(swizzlingMethod1);
        SEL sel2 = @selector(swizzlingMethod2);
        
        Method m1 = class_getInstanceMethod([self class], sel1);
        Method m2 = class_getInstanceMethod([self class], sel2);
        
        // 如果此类还没有对应的方法,先注册该方法
        BOOL methodAdded = class_addMethod([self class], sel1, method_getImplementation(m2), method_getTypeEncoding(m2));
        
        if (methodAdded) {
            NSLog(@"method add and add");
            class_replaceMethod([self class], sel2, method_getImplementation(m1), method_getTypeEncoding(m1));
        } else {
            // 否则,直接交换
            NSLog(@"methods exchange");
            method_exchangeImplementations(m1, m2);
        }
    });
}

- (void)swizzlingMethod1{
    NSLog(@"swizzling method: %s", __FUNCTION__);
}

- (void)swizzlingMethod2{
    NSLog(@"swizzing method: %s", __FUNCTION__);
}

经过实践发现,确实可以轻松的交换两个方法的实现。不过,这种做法会增加程序调试难度,应该尽可能减少使用。另外,也有一些注意事项:

  • Method Swizzling的合适时机是+load或者+initialize,前者表示第一次加载时,后者表示第一次使用时的初始化工作。
  • Method Swizzling应该总是在dispatch_once中执行,保证有且仅有一次运行机会。

对于Method Swizzling最常见的使用场景大概就是替换某些系统方法的默认实现,以便自动化处理一些工作,比如实现一个可自动记录日志和埋点数据的viewWillAppear:,通过替换UIViewController的原有viewWillAppear:,就可以实现一个通用的日志收集方案。

参考

相关文章

网友评论

      本文标题:认识OC运行时系统Runtime

      本文链接:https://www.haomeiwen.com/subject/errbiktx.html