Runtime

作者: 碎梦_aimee | 来源:发表于2019-08-05 14:15 被阅读0次


    Runtime的特性主要是消息(方法)的传递,如果消息(方法)在对象中找不到,就会进行转发。

    Runtime的介绍:

    Objective-C 是一个基于C语言的扩展,并且在C语言的基础上增加了面向对象特性和Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。

    高级编程语言想要成为可执行文件能被计算机识别,首先要转化成汇编语言,然后在汇编成机器语言。因为机器语言是计算机能够识别的唯一语言,所以任何语言最终都要转化成机器语言才能被计算机所识别。所以OC语言必须要经过转化成C语言然后才能进行汇编成机器语言从而被识别。从OC到C就是通过runtime来实现的。

    而OC是面向对象的,C是面向过程的,所以这也就需要将 面向对象的类  转化成  面向过程的结构体

    Runtime消息转发:

       1.1 OC的方法调用流程

       下面以实例对象调用方法[blackDog walk]为例描述方法调用的流程:

    1、编译器会把`[blackDog walk]`转化为`objc_msgSend(blackDog,SEL)`,SEL为@selector(walk)。

    2、Runtime会在blackDog对象所对应的Dog类的方法缓存列表里查找方法的SEL(先查找缓存表里面的方法,有则直接取,没有则需要遍历整个methodList表)

    3、如果没有找到,则在Dog类的方法分发表查找方法的SEL。(类由对象isa指针指向,方法分发表即methodList)

    4、如果没有找到,则在其父类(设Dog类的父类为Animal类)的方法分发表里查找方法的SEL(父类由类的superClass指向)

    5、如果没有找到,则沿继承体系继续下去,最终到达NSObject类。

    6、如果在234的其中一步中找到,则定位了方法实现的入口,执行具体实现

    7、如果最后还是没有找到,会面临两种情况:``(1) 如果是使用`[blackDog walk]`的方式调用方法````(2) 使用`[blackDog performSelector:@selector(walk)]`的方式调用方法

    1.2 消息转发流程

    1、动态方法解析接收到未知消息时(假设blackDog的walk方法尚未实现),runtime会调用+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)(这一步以方法是否被实现为准,如果方法没有被实现,不管返回值是YES or NO ,都会继续传递给下一级)

    2、备用接收者如果以上方法没有做处理,runtime会调用- (id)forwardingTargetForSelector:(SEL)aSelector方法。如果该方法返回了一个非nil(也不能是self)的对象,而且该对象实现了这个方法,那么这个对象就成了消息的接收者,消息就被分发到该对象。适用情况:通常在对象内部使用,让内部的另外一个对象处理消息,在外面看起来就像是该对象处理了消息。比如:blackDog让女朋友whiteDog来接收这个消息

    3、完整消息转发在- (void)forwardInvocation:(NSInvocation*)anInvocation方法中选择转发消息的对象,其中anInvocation对象封装了未知消息的所有细节,并保留调用结果发送到原始调用者。比如:blackDog将消息完整转发給主人dogOwner来处理

    实现一个完整转发的例子如下:

    #import "ViewController.h"

     #import "objc/runtime.h" 

     @interface Person: NSObject 

     @end 

     @implementation Person 

     - (void)foo {   

     NSLog(@"Doing foo");//Person的foo函数 

     @end

     @interface ViewController () 

     @end

     @implementation ViewController

     - (void)viewDidLoad {   

     [super viewDidLoad];    // Do any additional setup after loading the view, typically from a nib.    //执行foo函数  

      [self performSelector:@selector(foo)]; 

     + (BOOL)resolveInstanceMethod:(SEL)sel {   

     return YES;//返回YES,进入下一步转发 

     - (id)forwardingTargetForSelector:(SEL)aSelector {   

     return nil;//返回nil,进入下一步转发 

     - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {  

      if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {    

        return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名,进入forwardInvocation    

    }        

    return [super methodSignatureForSelector:aSelector]; 

     - (void)forwardInvocation:(NSInvocation *)anInvocation {    

    SEL sel = anInvocation.selector;       

     Person *p = [Person new];    

    if([p respondsToSelector:sel]) {     

       [anInvocation invokeWithTarget:p];   

     }    else {      

      [self doesNotRecognizeSelector:sel]; 

       }    }

     @end


    Runtime的使用:

      1.1 json->model

      原理描述:用runtime提供的函数遍历Model自身所有属性,如果属性在json中有对应的值,则将其赋值。

      核心方法:在NSObject的分类中添加方法

    - (instancetype)initWithDict:(NSDictionary*)dict {

        if(self= [selfinit]) {

            //(1)获取类的属性及属性对应的类型

            NSMutableArray * keys = [NSMutableArray array];

            NSMutableArray * attributes = [NSMutableArray array];

            /*

             * 例子

             * name = value3 attribute = T@"NSString",C,N,V_value3

             * name = value4 attribute = T^i,N,V_value4

             */

            unsignedintoutCount;

            objc_property_t * properties = class_copyPropertyList([self class], &outCount);

            for(inti = 0; i < outCount; i ++) {

                objc_property_tproperty = properties[i];

                //通过property_getName函数获得属性的名字

                NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];

                [keysaddObject:propertyName];

                //通过property_getAttributes函数可以获得属性的名字和@encode编码

                NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];

                [attributesaddObject:propertyAttribute];

            }

            //立即释放properties指向的内存

            free(properties);

            //(2)根据类型给属性赋值

            for(NSString* keyinkeys) {

                if([dictvalueForKey:key] ==nil)continue;

                [selfsetValue:[dictvalueForKey:key]forKey:key];

            }

        }

        return self;

    }

    //注释:::::现在直接使用一下方法也可

    [self setValuesForKeysWithDictionary:dic];

     1.2 一键序列化

    原理描述:用runtime提供的函数遍历Model自身所有属性,并对属性进行encode和decode操作。

    核心方法:在Model的基类中重写方法:

    - (id)initWithCoder:(NSCoder*)aDecoder {

        if(self = [super init]) {

            unsigned int outCount;

            Ivar* ivars =class_copyIvarList( [self class] , &outCount);

            for(inti = 0; i < outCount; i ++) {

                Ivar ivar = ivars[i];

                NSString * key = [NSString stringWithUTF8String: ivar_getName(ivar)];

                [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];

            }

        }

        return self;

    }

    - (void)encodeWithCoder:(NSCoder*)aCoder {

        unsigned int outCount;

        Ivar* ivars =class_copyIvarList ( [self class], &outCount);

        for(inti = 0; i < outCount; i ++) {

            Ivarivar = ivars[i];

            NSString * key = [NSString stringWithUTF8String: ivar_getName(ivar)];

            [aCoder encodeObject:[self valueForKey:key]  forKey:key];

        }

    }

    打印结果:

     2018-04-01 13:00:45.423385+0800 ocram[88353:23279961] Doing foo

    1.3 访问私有变量

    我们知道,OC中没有真正意义上的私有变量和方法,要让成员变量私有,要放在m文件中声明,不对外暴露。如果我们知道这个成员变量的名称,可以通过runtime获取成员变量,再通过getIvar来获取它的值。方法:

    Ivar ivar = class_getInstanceVariable([Model class],"_str1");

    NSString* str1 = object_getIvar(model, ivar);

    1.4. 关联对象(Objective-C Associated Objects)给分类增加属性

       我们都是知道分类是不能自定义属性和变量的。下面通过关联对象实现给分类添加属性。

       关联对象Runtime提供了下面几个接口:

     //关联对象 

    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)

    参数解释

    id object:被关联的对象

    const void*key:关联的key,要求唯一

    id value:关联的对象

    objc_AssociationPolicy policy:内存管理的策略

    下面实现一个UIView的Category添加自定义属性defaultColor。

    #import "ViewController.h"

    #import "objc/runtime.h"

    @interfaceUIView (DefaultColor)

    @property (nonatomic, strong) UIColor *defaultColor;

    @end

    @implementation UIView (DefaultColor)

    @dynamic  defaultColor;

    //也可以使用方法的指针地址作为唯一的key 例如:@selector(defaultColor)

    static char  kDefaultColorKey;

    - (void)setDefaultColor:(UIColor*)defaultColor {

        objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    }

    - (id)defaultColor {

        return objc_getAssociatedObject(self, &kDefaultColorKey);

    }

    @end

    @interfaceViewController ()

    @end

    @implementation ViewController

    - (void)viewDidLoad {

        [superviewDidLoad];

        // Do any additional setup after loading the view, typically from a nib.

         UIView*test = [UIView new];

        test.defaultColor = [UIColor blackColor];

        NSLog(@"%@", test.defaultColor);

    }

    @end

    打印结果:

    2018-04-01 15:41:44.977732+0800 ocram[2053:63739]

    UIExtendedGrayColorSpace 0 1

    1.5 Method Swizzling(俗称黑魔法)

    简单说就是进行方法交换

    method Swizzling原理

     每个类都维护一个方法(Method)列表,Method则包含SEL和其对应IMP的信息,方法交换做的事情就是把SEL和IMP的对应关系断开,并和新的IMP生成对应关系

    *****在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。

    ****每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP。

    交换方法的几种实现方式

      利用 method_exchangeImplementations 交换两个方法的实现

      利用 class_replaceMethod 替换方法的实现

      利用 method_setImplementation 来直接设置某个方法的IMP

    使用实例:

         1.5.1方法添加

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

    class_addMethod([self class], sel, (IMP)fooMethod,"v@:");

    @cls 被添加方法的类

    @name 添加的方法的名称的SEL

    @imp 方法的实现。该函数必须至少要有两个参数,self,_cmd

    @类型编码

         1.5.2方法替换

    下面实现一个替换ViewController的viewDidLoad方法的例子

    @implementation ViewController

    + (void)load {

        static dispatch_once_tonceToken;

        dispatch_once(&onceToken, ^{

            Class class = [self class];

            SEL originalSelector =@selector(viewDidLoad);

            SEL swizzledSelector =@selector(jkviewDidLoad);

            Method  originalMethod = class_getInstanceMethod(class,originalSelector);

            Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);

           //judge the method named  swizzledMethod is already existed.

            BOOL  didAddMethod =class_addMethod(class, originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));

            // if swizzledMethod is already existed.

            if(didAddMethod) {

                class_replaceMethod(class, swizzledSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));

            }    else{

                method_exchangeImplementations(originalMethod, swizzledMethod);

            }

        });

    }

    - (void)jkviewDidLoad {

        NSLog(@"替换的方法");

     [self jkviewDidLoad];

    }

    - (void)viewDidLoad {

        NSLog(@"自带的方法");

        [superviewDidLoad];

    }

    @end

    swizzling应该只在+load中完成。 在Objective-C的运行时中,每个类有两个方法都会自动调用。+load是在一个类被初始装载时调用,+initialize是在应用第一次调用该类的类方法或实例方法前调用的。两个方法都是可选的,并且只有在方法被实现的情况下才会被调用。

    swizzling应该只在dispatch_once中完成,由于swizzling改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatch 的 dispatch_once满足了所需要的需求,并且应该被当做使用swizzling的初始化单例方法的标准。

    1.6   KVO实现

    全称是Key-value observing,翻译成键值观察。提供了一种当其它对象属性被修改的时候能通知当前对象的机制。再MVC大行其道的Cocoa中,KVO机制很适合实现model和controller类之间的通讯。

    KVO的实现依赖于Objective-C强大的Runtime,当观察某对象A时,KVO机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性keyPath的setter方法。setter方法随后负责通知观察对象属性的改变状况。

    Apple使用了isa-swizzling来实现KVO。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A的新类,该类继承自对象A的本类,且KVO为NSKVONotifying_A重写观察属性的setter方法,setter方法会负责在调用原setter方法之前和之后,通知所有观察对象属性值的更改情况。

    KVO的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和didChangeValueForKey:,在存取数值的前后分别调用 2 个方法:

    被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该keyPath的属性值即将变更;

    当改变发生后,didChangeValueForKey:被调用,通知系统该keyPath的属性值已经变更;之后,observeValueForKey:ofObject:change:context:也会被调用。且重写观察属性的setter方法这种继承方式的注入是在运行时而不是编译时实现的

    - (void)setName:(NSString*)newName { 

      [selfwillChangeValueForKey:@"name"];//KVO 在调用存取方法之前总调用 

    [supersetValue:newName forKey:@"name"];//调用父类的存取方法 [selfdidChangeValueForKey:@"name"];//KVO 在调用存取方法之后总调用

    }

    参考文章:

    https://www.jianshu.com/p/07b6c4a40a90

    https://www.jianshu.com/p/6ebda3cd8052

    https://www.jianshu.com/p/75849d7ea832

    相关文章

      网友评论

          本文标题:Runtime

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