初识runtime

作者: nuclear | 来源:发表于2016-06-28 00:13 被阅读812次

    大家都知道OC是动态语言,其主要特征就是动态绑定,消息转发。我们在调用NS方法的时候,runtime其实就已经在背后默默为我们干活了。还记得刚开始学习iOS时,我一听到发送消息就自然而然的联想到---调用方法,这实在是太委屈强大的OC了。或许很多人会有和我一样的看法,别急,看完本篇文章之后,你的看法就会发生改变了。

    本文尝试讲解OC中类,对象等在runtime中的结构,还有动态绑定,发送消息,消息转发,动态添加方法,调换方法等几个方面讲解runtime的基础知识,后面会用一个demo告诉大家具体的使用方法。

    runtime的源码是开源的,附上传送门Objective-C Runtime源码

    OC的动态特性

    OC拓展了C语言,加入了smallTalk的消息传递和面向对象的特点,它是一个动态语言,这个动态体现在三个方面。

    动态类型

    例如id类型的实例,其类型需要等到运行时才能决定,在编译时id就是一个通用类型,通常我们会这么干

    id anIns;
    if ([anIns isKindOfClass:[SomeClass class]]) {
        SomeClass *sClass = (SomeClass *)anIns;
        //do something after introspection
    }
    

    动态绑定

    基于动态类型,实例所属的类在运行时才确定,动态绑定就是在这个实例类型确定后将具体的属性和方法绑定到这个实例上,这些属性和方法包括没有在类上实现的,所以利用这个特性,我们可以动态的添加属性和方法。

    在标准的C语言中:

    #improt <studio.h>
    
    void printHello() {
        printf("Hello, World!\n");
    }
    void printBye() {
        printf("GoodBye, World!\n");
    }
    
    void doIt(int type) {
        if (type == 0) {
            printHello();
        }else {
            printBye();
        }
    }
    

    这两个方法的具体实现都是在编译时就已经知道了,编译器直接调用了这个方法,这个函数的地址已经被硬编码。

    如果把上面的代码改成这样呢:

    #improt <studio.h>
    
    void printHello() {
        printf("Hello, World!\n");
    }
    void printBye() {
        printf("GoodBye, World!\n");
    }
    
    void doIt(int type) {
    
        void (*fnc)();
    
        if (type == 0) {
            fnc = printHello;
        }else {
            fnc = printBye;
        }
        
        fnc();
        
        reuturn 0;
    }
    
    

    修改之后,fnc的具体实现需要等到运行时才会知道,与第一个例子相比,调用fnc时取出fnc的地址。

    动态加载

    根据需求加载所需要的资源,这点很容易理解,对于iOS开发来说,基本就是根据不同的机型做适配。最经典的例子就是在Retina设备上加载@2x,@3x的图片,而在老一些的普通屏设备上加载原图。

    object、class在runtime中的结构

    object

    在runtime中,object是一个结构体,包含一个指向自己所属的类的指针isa

    struct objc_object {
        Class isa OBJC_ISA_AVAILABILITY;
    }
    

    isa指针指向的是它的类别:Class,也就是它所属的类。

    这个Class

    typedeef objc_class *Class
    

    它是一个指向objc_class结构体的指针

    对于id

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

    Class

    对于Class,它在runtime中的定义是这样的:

    struct objc_class {
        Class isa OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
        Class super_class;
        const char *name;   //类名
        long version;       //版本号
        long info;          //信息
        long instane_size;  //实例变量占用内存大小
        struct objc_ivar_List_ *ivars           //实例变量列表
        struct objc_method_list **methodlists  //方法列表
        struct objc_cache *chche                //方法缓存列表
        struct objc_protocol_list *protocols    //协议列表
    #end if 
    
    }   OBJC2_UNAVAILABLE
    

    isa 指向所属的Class。

    整个结构的示意图:

    object结构示意object结构示意

    object&class

    由object和class的代码可以知道,类与对象相比只是多了实例变量和方法列表等,类和对象都是对象(有点拗口),分别是类对象和实例对象。

    在object中的isa指针指向的是对应的类结构:Class,Class其中存放的是普通成员变量和实例方法(-开头);
    在class中的is指针指向的是metaClass,metaClass中存放的是静态成员变量和类方法(+开头)。

    所有的metaclass中isa指针都是指向根metaclass,而根metaclass则指向自身。根metaclass是通过继承根类产生的,与根class结构体成员一致,不同的是根metaclass的isa指针指向自身

    明白什么是objc_msgSend()

    在OC中[objc foo]不会立即执行foo方法的代码,而是在运行时给objc发送foo的消息,这个消息可能会由objc来处理,也可能被转发给另一个对象,对于不同的消息也可以对应一个方法来实现,而这个机制中最重要的方法就是objc_msgSend()

    [objc msgName:param]为例,objc是接收者(receiver),msgName:param是选择器(selector),而param就是消息(message),message会被编译器转为标准的C函数:void objc_msgSend(id self, SEL op, ...),而[objc msgName:param]会被转换成:

    id returnValue = objc_msgSend(objc, @selector(msgName:), param);
    

    在消息传递过程中,会通过objcisa指针找到对应的类,然后在objc_method_list中查找@selector(msgName:),找不到会按照同样的方式在继承树往上去查找,都找不到的话会抛出异常unrecognized selector send to instance ...,就会发生消息转发

    有人可能会问,如果每次消息传递都这样去遍历,那效率岂不是太低了吗?别担心,OC早就考虑到这点了,注意到结构里面的objc_cache,这个的作用就是在消息传递查找SEL的时候,一旦查找到对应的方法就将它存入缓存中,下一次进来首先去缓存中去查找,找不到了再往上遍历,这样效率就大大提高了。�

    消息传递过程发生了什么--message forward

    一个类通过已经被编译过的实行方法来确定是否会对某个消息发出响应,但是如果发送了一个不能识别的消息给一个类,在编译时是无法发现的(通过调用performSelector:方法),因为所以的方法都能在编译时动态添加到方法列表中,当接收者无法识别某个消息时,开发者可以通过消息转发(message forward)机制处理未能识别的message

    上面提到的,当消息传递过程中找不到对应的方法时,会抛出unrecognzed selector send to instace ...的错误,即找不到指定的方法,在此之前可以在三个方法中实现补救。

    消息转发示意图消息转发示意图

    1、resolveInstanceMethod

    将未能识别的消息动态添加到接收者的类中,resolveInstanceMethod方法返回的是一个BOOL类型的值,用于判断是否接收这消息。

    先声明两个类,FatherSon,在son中定义个方法:eat。在Father中创建Son实例,然后调用sonrun方法(这个run方法在Son代码中是未实现的)。

    father

    #import <Foundation/Foundation.h>
    @interface Father : NSObject
    @end
    
    
    #import "Father.h"
    #import "Son.h"
    @implementation Father
    
    - (void)son {
        Son *s = [[Son alloc] init];
        [s performSelector:@selector(run)];
    }
    
    @end
    

    son

    #import <Foundation/Foundation.h>
    @interface Son : NSObject
    @end
    
    
    #import "Son.h"
    #import <objc/runtime.h>
    @implementation Son
    
    void testRun() {
        [Son.new  eat];
        NSLog(@"son is runing");
    }
    
    - (void)eat {
        NSLog(@"son is eating");
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        //判断方法是否是run
        if ([NSStringFromSelector(sel) isEqualToString:@"run"]) {
            class_addMethod([self class], sel, (IMP)testRun, "v@:@");
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    @end
    

    打印输出:

    2016-06-27 16:51:49.150 RuntimeDemo[2074:304851] son is eating
    2016-06-27 16:51:51.313 RuntimeDemo[2074:304851] son is runing
    

    @selector(run)被动态添加到了Son的类方法列表中。

    2、forwardindTargetWithSelctor:(SEL)aSelector

    resloveInstanceMethod:返回NO之后,会进入forwardindTargetWithSelctor:方法。在这个方法中,返回的对象就是message的接收者,然后会回到resloveInstanceMethod方法,从新开始消息转发过程,如果返回nil则会进入下一个方法中去判断是否响应这个消息。

    我们讲上面的代码修改了下变成了这样

    father

    #import <Foundation/Foundation.h>
    @interface Father : NSObject
    @end
    
    
    #import "Father.h"
    #import "Son.h"
    @implementation Father
    
    - (void)son {
        Son *s = [[Son alloc] init];
        [s performSelector:@selector(run)];
    }
    
    static void fatherRun() {
        NSLog(@"father is runing");
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == @selector(run)) {
            class_addMethod([self class], sel, (IMP)fatherRun, "v@:@");
            return NO;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    @end
    

    son

    #import <Foundation/Foundation.h>
    @interface Son : NSObject
    @end
    
    
    #import "Son.h"
    #import <objc/runtime.h>
    @implementation Son
    
    - (void)eat {
        NSLog(@"son is eating");
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        return NO;
    }
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        [self eat];
        return [Father new];
    }
    @end
    

    打印:

    2016-06-27 21:32:21.273 RuntimeDemo[3128:717793] son is eating
    2016-06-27 21:32:21.276 RuntimeDemo[3128:717793] father is runing
    

    这就是runtime的神奇之处,消息的接收者由“本应该是”的Son转变为了Father

    3、forwardingInvocation:anInvocation

    resolveInstanceWithSelector:返回NO,forwardingTargetWithSelector:返回nil的时候,就会进入到下一个环节,使用NSInvocation来实现消息转发。

    在此之前,需要调用methodSignatureForSelector:返回选择器

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        if ([NSStringFromSelector(aSelector) isEqualToString:@"run"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
        return [super methodSignatureForSelector:aSelector];
    }
    

    修改上面的代码为:

    father

    #import <Foundation/Foundation.h>
    @interface Father : NSObject
    @end
    
    
    #import "Father.h"
    #import "Son.h"
    @implementation Father
    
    - (void)son {
        Son *s = [[Son alloc] init];
        [s performSelector:@selector(run)];
    }
    
    - (void)run {
        NSLog(@"father is running");
    }
    
    @end
    

    son

    #import <Foundation/Foundation.h>
    @interface Son : NSObject
    @end
    
    
    #import "Son.h"
    #import <objc/runtime.h>
    @implementation Son
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        
        return NO;
    }
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
    
        return nil;
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    
        if ([NSStringFromSelector(aSelector) isEqualToString:@"run"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
        return [super methodSignatureForSelector:aSelector];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
    
        Father *f = [[Father alloc] init];
        //改变selector
        [anInvocation setSelector:@selector(run)];
        //在这里指定消息接收者,如果不指定的话还是会抛出找不到方法的异常
        [anInvocation invokeWithTarget:f];
    }
    
    @end
    

    打印:

    2016-06-27 22:26:51.604 RuntimeDemo[3605:852405] father is running
    

    我们已经成功的将消息转发给了Father实例。以上就是消息转发的流程和具体实践。

    使用objc_associate()为category动态添加实例变量

    众所周知,在category中是不允许添加额外的属性的,使用objc_setAssociate()能够将一个变量通过指定的key值讲实例与实例变量绑定在一起,在读取的时候值调用objc_getAssociate(),在指定的实例中通过key将变量取出,可以简单理解成字典一样存取

    这两个方法长这样:

    //setter,就像字典中的 setValue:ForKey:
    void objc_setAssociatedOject(id object, void *key, id value, objc_AssociationPolicy policy)
    
    
    //getter,就像字典中的 objectForKey
    id objc_getAssociatedObject(id object, void *key)
    
    //remove,就像字典中的 removeAllObject
    void objc_removeAssocaitedObjected(id object)
    

    在setter方法中,objc_AssociateionPolicy类型相当于属性的strong,assign,copy等,它具有以下几个值:

    typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
        OBJC_ASSOCIATION_ASSIGN = 0,               //assing
        OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,     //nonatomic, retain                                       
        OBJC_ASSOCIATION_COPY_NONATOMIC = 3,       //nonatomic, copy                                       
        OBJC_ASSOCIATION_RETAIN = 01401,           //retain                                       
        OBJC_ASSOCIATION_COPY = 01403              //copy
    };
    

    上面说到的,使用这个机制,就像字典一样设置、读取key-value,但是它与字典最重要的不同在于:

    Though the key is treated purely as an opaque pointer.whereas with a dictionary,keys are regarded equal if they return YES for isEqual:,the key for associated objects must be the exact same point for them to match.For this reason,it is common to use static global variables for the keys.

    尽管key被认为是一个不透明的指针,在字典中,只有isEqual:返回的结果是YES,那么就认为这个两个key是相等的,但是在associatedObject方法中,key值必须是严格相等的,所以通常会使用静态的全局变量表示。

    在SDWebImage的对UIImageView的分类--UIImageView + WebCache中的获取当前图片的URL方法:

    - (NSURL *)sd_imageURL {
        return objc_getAssociatedObject(self, &imageURLKey);
    }
    

    而赋值方法是在下载图片的方法中:

    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    

    除此之外还有设置indicatorStyle的方法:

    static char TAG_ACTIVITY_STYLE;     //key
    
    /*
        对key的处理也可以设置为具体的值:
        static void *TEST_KEY = "TEST_KEY";
        objc_setAssociatedObject(self, TEST_KEY, value, OBJC_ASSOCIATION_RETAIN)
        objc_getAssociatedObject(self, TEST_KEY);
    */
    
    .h
    - (void)setIndicatorStyle:(UIActivityIndicatorViewStyle)style;
    
    .m
    - (void)setIndicatorStyle:(UIActivityIndicatorViewStyle)style{
        objc_setAssociatedObject(self, &TAG_ACTIVITY_STYLE, [NSNumber numberWithInt:style], OBJC_ASSOCIATION_RETAIN);
    }
    - (int)getIndicatorStyle{
        return [objc_getAssociatedObject(self, &TAG_ACTIVITY_STYLE) intValue];
    }
    

    SEL,Method,Message,IMP

    在进入下一个环节Method Swizzling之前,需要有一些理论准备,否则可能会晕晕的,这些概念在实战环节也会用到,那么,开始吧。

    Selector

    a Selector is the name of a method.

    Selector是一个方法的名称,例如咱们都非常熟悉的alloc, init, release, dictionaryWithObjectsAndKeys:, setObject:forKey:在开发过程中,指定按钮的点击事件时常用到的@selector(doSomething:),就是指定了方法名。

    message:

    a message is a selector and the arguments you are sending with it.

    message就是包含有参数的Selector,例如[dictionary setObject:obj forKey:key],;这里的Selector就是setObject:forKey:

    method

    a method is a combination of a selector and an implementation (and accompanying metadata).

    method是Selector和implementation的结合

    还有:

    IMP

    IMP就是implementation的缩写。

    the actual executable code of a method. Its type at runtime is an IMP, and it's really just a function pointer.

    好吧,这个概念应该比较不会混淆,不过需要注意的是,implementation在runtime中就是一个函数指针

    一个类维护一个运行时可接收的消息分发表;分发表中的每个入口是一个方法(Method),其中key是一个特定名称,即选择器(SEL),其对应一个实现(IMP),即指向底层C函数的指针。

    method swizzling

    method swizzling可以说是runtime的黑魔法,它可以交换两个方法的IMP,拦截系统的方法,添加更多的功能,例如:调用方法后自动log输出,再也不用傻傻的一次次去NSLog了,关于method swizzling更多的内容,可以看看我之前写的黑魔法 - Method Swizzling

    实战

    动态创建类,对象,方法,实例变量

    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    #import <objc/message.h>
    
    static void speak(id self, SEL _cmd, id some) {
        NSLog(@"some of %@ %@ years old named %@ say: %@",
              [self class],
              [self valueForKey:@"age"],
              object_getIvar(self, class_getInstanceVariable([self class], "name")) ,some);
    }
    
    int main(int argc, const char * argv[]) {
        
        @autoreleasepool {
            
            //创建一个 Person 的类
            Class Person = objc_allocateClassPair([NSObject class], "Person", 0);
            
            //添加一个 name 的实例变量,现在是为赋值状态,仅仅是在objc_class的ivars里面添加了ivar
            class_addIvar(Person, "name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
            class_addIvar(Person, "age", sizeof(int), sizeof(int), @encode(int));
            
            //注册一个speak的方法
            SEL sayHello = sel_registerName("sayHello:");
            
            //往 Person 的 methodLists里面添加方法
            class_addMethod(Person, sayHello, (IMP)speak, "v@:@");
            
            
            //在创建实例之前需要先注册类
            objc_registerClassPair(Person);
            
            //初始化 Person 实例
            id person = [[Person alloc] init];
            
            //为name,age赋值
            [person setValue:@"xiaoming" forKey:@"name"];
    
            Ivar age = class_getInstanceVariable(Person, "age");
            object_setIvar(person, age, @25);
            
            //发送消息
            //objc_msgSend(person, sayHello, @"大家好!");
            
            ((void(*)(id, SEL,id))objc_msgSend)(person, sayHello, @"Hello World!");
        }
        return 0;
    }
    

    获取全部属性,实例,方法

    - (NSDictionary *)allIvars {
        
        NSLog(@"=======ivars=========");
        
        unsigned int count = 0;
        NSMutableDictionary *allIvar = @{}.mutableCopy;
        
        Ivar *ivars = class_copyIvarList([self class], &count);
        
        if (count == 0) { return allIvar; }
        
        for (NSUInteger i = 0; i < count; i++) {
            char const *ivarName = ivar_getName(ivars[i]);
            NSString *name = [NSString stringWithUTF8String:ivarName];
            id ivar = [self valueForKey:name];
            if (ivar) {
                allIvar[name] = ivar;
            }else {
                allIvar[name] = @"value为nil";
            }
        }
        //数组指针需要用free去释放
        free(ivars);
        
        NSLog(@"=======ivars=========");
        
        return allIvar;
    }
    
    - (NSDictionary *)allProperty {
        
        NSLog(@"=======properties=========");
        
        unsigned int count = 0;
        NSMutableDictionary *allProperty = @{}.mutableCopy;
        
        objc_property_t *properties = class_copyPropertyList([self class], &count);
        
        if (count == 0) { return allProperty; }
        
        for (NSUInteger i = 0; i < count; i++) {
            char const *propertyName = property_getName(properties[i]);
            NSString *name = [NSString stringWithUTF8String:propertyName];
            id property = [self valueForKey:name];
            if (property) {
                allProperty[name] = property;
            }else {
                allProperty[name] = @"property为nil";
            }
        }
        //数组指针需要用free去释放
        free(properties);
        
        NSLog(@"=======properties=========");
        
        return allProperty;
    }
    
    - (NSDictionary *)allMethod {
        
        NSLog(@"=======methods=========");
        
        unsigned int count = 0;
        NSMutableDictionary *allMethod = @{}.mutableCopy;
        
        Method *methods = class_copyMethodList([self class], &count);
        
        if (count == 0) { return allMethod; }
        
        for (NSUInteger i = 0; i < count; i++) {
            SEL sel = method_getName(methods[i]);
            char const *mehtodName = sel_getName(sel);
            NSString *name = [NSString stringWithUTF8String:mehtodName];
    
            int arguments = method_getNumberOfArguments(methods[i]);
            allMethod[name] = @(arguments-2);
        }
        //数组指针需要用free去释放
        free(methods);
        
        NSLog(@"=======methods=========");
        
        return allMethod;
    }
    

    统一归档、解档

    通常情况下,使用NSCoding归档,解档的时候是一个变量一个属性进行的,假设一个类中有1000个变量呢,那不是要疯了吗?好在咱们有runtime这个神器,如果能够直接取出全部的实例变量列表,那不就是一个for循环的事情了吗?

    //解档
    - (instancetype)initWithCoder:(NSCoder *)aDecoder {
        self = [super init];
        
        if (!self) return nil;
        
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([self class], &count);
        
        for (int i = 0 ; i < count; i++) {
            char const *ivarName = ivar_getName(ivars[i]);
            NSString *name = [NSString stringWithUTF8String:ivarName];
            
            id value = [aDecoder decodeObjectForKey:name];
            [self setValue:value forKey:name];
        }
        free(ivars);
        
        return self;
    }
    
    //归档
    - (void)encodeWithCoder:(NSCoder *)aCoder {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([self class], &count);
        
        for (int i = 0 ; i < count; i++) {
            char const *ivarName = ivar_getName(ivars[i]);
            NSString *name = [NSString stringWithUTF8String:ivarName];
            
            id value = [self valueForKey:name];
            [aCoder encodeObject:value forKey:name];
        }
        free(ivars);
    }
    

    阅读原文

    相关文章

      网友评论

      本文标题:初识runtime

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