Runtime的几点用法总结

作者: 懒得起名的伊凡 | 来源:发表于2015-10-15 16:08 被阅读769次

    Objective-C是一门动态(运行时)语言。它将很多静态语言在编译和链接时期做的事放到了运行时候来做,这就使得我们写代码时候更具灵活性,如可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。这就意味着Objective-C不仅需要一个编译器,还需要一个运行时系统来执行编译代码,即使Objc Runtime。
    所做的事

    • 在这个库中,对象使用C语言中的结构体表示,方法使用C函数来实现。这些结构体和方法,在被runtime函数封装之后,我们就可以在运行时创建,检查,修改类、对象和他们的方法了。
    • 找出方法的最终执行代码 runtime 根据消息接受者是否能响应该消息而做出不同的反应。

    给类别Category添加属性

    比如说我们需要在类别中添加一个 NSString 类型的属性,直接在 .h 文件添加 @property(nonatomic,copy) NSString *categoryProperty;,这时候使用点语法进行调用的话,程序会出现crash错误 :Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ViewController setCategoryProperty:]: unrecognized selector sent to instance 0x7ff661e43dd0'。这种状况的原因其实很简单,只是没有实现setter和getter方法而已,所以我们的问题就转为实现setter 和 getter方法。
    一言不合就要上代码了,主要记录两种类型数据的处理方式。例子为给UIImage添加了两个属性,没什么具体含义,主要记录用法:

    //.h
    #import <UIKit/UIKit.h>
    NS_ASSUME_NONNULL_BEGIN
    @interface UIImage (JYAdd)
    
    @property(nonatomic,strong) NSString *name;
    @property(nonatomic,assign) CGFloat add;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    //.m
    #import "UIImage+JYAdd.h"
    #import <objc/runtime.h>
    
    @implementation UIImage (JYAdd)
    #pragma mark - 添加属性
    
    - (void)setName:(NSString *)name
    {
        [self willChangeValueForKey:NSStringFromSelector(@selector(name))];
        objc_setAssociatedObject(self, _cmd, name, OBJC_ASSOCIATION_COPY);
        [self didChangeValueForKey:NSStringFromSelector(@selector(name))];
    }
    
    - (NSString *)name
    {
        return objc_getAssociatedObject(self, @selector(setName:));
    }
    
    - (void)setAdd:(CGFloat)add
    {
        [self willChangeValueForKey:NSStringFromSelector(@selector(add))];
        
        //区别在这里,区别在这里
        NSValue *value = [NSValue value:&add withObjCType:@encode(CGFloat)];
        objc_setAssociatedObject(self, _cmd, value, OBJC_ASSOCIATION_RETAIN);
        
        [self didChangeValueForKey:NSStringFromSelector(@selector(add))];
    }
    
    - (CGFloat)add
    {
        CGFloat cValue = {0};
        NSValue *value = objc_getAssociatedObject(self, @selector(setAdd:));
        [value getValue:&cValue];
        return cValue;
    }
    
    @end
    

    参考了YY的实现,直接使用YY的宏定义其实也挺好的啊,代码不贴了,反正也有。

    利用runtime来替换已有的系统方法

    例子,初始化UIImage的时候,在不同的系统版本中添加不同的风格的切图,怎么就是和UIImage过不去了。

    //.h
    #import <UIKit/UIKit.h>
    NS_ASSUME_NONNULL_BEGIN
    @interface UIImage (JYAdd)
    /*!
     @brief 如果调用这个,其实调用的是原来系统的方法,因为他两交换了实现
     
     @note 为防止误用,可以不声明该方法
     */
    + (nonnull UIImage *)jy_imageNamed:(NSString *)name;
    @end
    
    NS_ASSUME_NONNULL_END
    
    //.m
    
    
    #import "UIImage+JYAdd.h"
    #import <objc/runtime.h>
    
    @implementation UIImage (JYAdd)
    
    + (void)load
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Class class = [self class];
            
            SEL originalSelector = @selector(imageNamed:);
            SEL swizzledSelector = @selector(jy_imageNamed:);
            /*
             //实例方法
            Method originalMethod = class_getInstanceMethod(class, originalSelector);
            Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
            */
            
            //类方法
            Method originalMethod = class_getClassMethod(class, originalSelector);
            Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
            
            //有就不添加了,没有就添加。有则改之无则加勉吧。
            class_addMethod(class, originalSelector, class_getMethodImplementation(class, originalSelector), method_getTypeEncoding(originalMethod));
            
            class_addMethod(class, swizzledSelector, class_getMethodImplementation(class, swizzledSelector), method_getTypeEncoding(swizzledMethod));
            
            //交换他两的实现
            method_exchangeImplementations(originalMethod, swizzledMethod);
    
        });
    }
    
    #pragma mark - 交换系统方法
    
    + (nonnull UIImage *)jy_imageNamed:(NSString *)name
    {
        /*!
         在这里实现我们所需要做的操作
         */
        double systemVersion = [[[UIDevice currentDevice]systemVersion]floatValue];
        if (systemVersion >= 9.0) {
            name = [name stringByAppendingString:@"_os"];
        }
        
        UIImage *image = [UIImage jy_imageNamed:name];//这个地方很关键
        return  image;
    }
    @end
    

    方法+ (void)load不是这里的重点,简单知道一下:一般情况下,类别中的方法会重写掉主类里面相同命名的方法,但+load:是个特例,当一个类被读到内存的时候,runtime会给这个类以及他的每一个类别都发送一个 +load:消息(知道这一点很重要)。
    Note:注意交换方法只能执行一次,不要总是执行,load的意义在这儿也有体现的。

    还有一点是,尝试添加原 selector 是为了做一层保护,因为如果这个类没有实现 originalSelector ,但是其父类实现了,那么 class_getInstanceMethod 返回的将是父类的方法。这样就导致了 method_exchangeImplementations 替换的是父类的方法。所以先尝试添加 originalSelector ,如果已经存在,再用 method_exchangeImplementations 把原来的方法的实现交换成新方法的实现。

    可以借用了大神 @sunnyxx的开源项目 FDFullscreenPopGesture来理解该方面,感谢。

    实现自动归档和自动解档

    其实归档的实现很简单,只不过就是实现协议<NSCoding>,需要说明一点的是:** 实现了 ‘NSCoding’协议,就可以支持数据类和数据流间的编码和解码,而数据流可以持久化到硬盘。**所以,这就有趣了。
    我怎么那么喜欢上代码,哈哈

    
    //.h
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    @interface JYEncodeModel : NSObject<NSCoding>
    
    @property(nonatomic,strong) NSString *name;
    @property(nonatomic,assign) int age;
    @property(nonatomic,assign) NSRange range;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    //.m
    #import "JYEncodeModel.h"
    #import "NSObject+JYEncode.h"
    
    NSString *const kEncodeName = @"name";
    NSString *const kEncodeAge = @"age";
    NSString *const kEncodeRange = @"range";
    
    @implementation JYEncodeModel
    
    - (instancetype)initWithCoder:(NSCoder *)aDecoder
    {
        self = [super init];
        if (self) {
            _name = [aDecoder decodeObjectForKey:kEncodeName];
            _age = [aDecoder decodeIntForKey:kEncodeAge];
            _range = [[aDecoder decodeObjectForKey:kEncodeRange] rangeValue];
        }
        return self;
    }
    
    - (void)encodeWithCoder:(NSCoder *)aCoder
    {
        [aCoder encodeObject:self.name forKey:kEncodeName];
        [aCoder encodeInt:self.age forKey:kEncodeAge];
        [aCoder encodeObject:[NSValue valueWithRange:self.range] forKey:kEncodeRange];
    }
    
    @end
    

    可问题在于,像这样只有三个属性需要我们,可以这样写,那如果有三十个属性呐,身为一个会偷懒的猴子自然要找点版本偷懒了,所以,我又要写代码了

    //.h
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    @interface JYEncodeModel : NSObject<NSCoding>
    
    @property(nonatomic,strong) NSString *name;
    @property(nonatomic,assign) int age;
    @property(nonatomic,assign) NSRange range;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    //.m
    #import "JYEncodeModel.h"
    #import <objc/runtime.h>
    
    @implementation JYEncodeModel
    
    - (instancetype)initWithCoder:(NSCoder *)aDecoder
    {
        self = [super init];
        if (self) {
                unsigned int count = 0;
                Ivar *ivars = class_copyIvarList([self class], &count);
                for (int i = 0; i < count; i++) {
                    Ivar ivar = ivars[i];
                    const char *name = ivar_getName(ivar);
                    NSString *key = [NSString stringWithUTF8String:name];
                    id value = [aDecoder decodeObjectForKey:key];
                    [self setValue:value forKey:key];
                }
                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++) {
            Ivar ivar = ivars[i];
            const char *name = ivar_getName(ivar);
            NSString *key = [NSString stringWithUTF8String:name];
            id value = [self valueForKey:key];
            [aCoder encodeObject:value forKey:key];
        }
        free(ivars);
    }
    
    @end
    
    简化一下使用方式

    而这样每一个模型类都要写着无聊的代码,而大部分类都是继承自NSObject,所以,我们可以实现一个NSObject类别来专门做这件事。

    //.h
    
    #import <Foundation/Foundation.h>
    
    @interface NSObject (JYEncode)
    
    - (instancetype)jy_initWithCoder:(NSCoder *)aDecoder;
    - (void)jy_encodeWithCoder:(NSCoder *)aCoder;
    
    @end
    
    //.m
    
    #import "NSObject+JYEncode.h"
    #import <objc/runtime.h>
    
    @implementation NSObject (JYEncode)
    
    - (instancetype)jy_initWithCoder:(NSCoder *)aDecoder
    {
        if (!aDecoder) return self;
        if (self == (id)kCFNull) return self;
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([self class], &count);
        for (int i = 0; i < count; i++) {
            //取出i对应位置的成员变量
            Ivar ivar = ivars[i];
            //查看成员变量
            const char *name = ivar_getName(ivar);
            //归档
            NSString *key = [NSString stringWithUTF8String:name];
            id value = [aDecoder decodeObjectForKey:key];
            //设置到成员变量身上
            [self setValue:value forKey:key];
        }
        free(ivars);
        return self;
    }
    
    - (void)jy_encodeWithCoder:(NSCoder *)aCoder
    {
        if (!aCoder) return;
        if (self == (id)kCFNull) {
            [((id<NSCoding>)self)encodeWithCoder:aCoder];
            return;
        }
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([self class], &count);
        
        for (int i = 0; i < count; i++) {
            Ivar ivar = ivars[i];
            const char *name = ivar_getName(ivar);
            NSString *key = [NSString stringWithUTF8String:name];
            id value = [self valueForKey:key];
            [aCoder encodeObject:value forKey:key];
        }
        free(ivars);
    }
    
    @end
    
    

    这样在使用的时候只需要简单的引用一下就可以了,为了验证可行性,每种类型的数据我都添加了一个,事实证明是可以的

    //.h
    
    #import <Foundation/Foundation.h>
    #import "JYEncodeSubModel.h"
    
    /*!
     实现 ‘NSCoding’协议,支持数据类和数据流间的编码和解码,而数据流可以持久化到硬盘。 
     */
    NS_ASSUME_NONNULL_BEGIN
    @interface JYEncodeModel : NSObject<NSCoding>
    
    @property(nonatomic,strong) NSString *name;
    @property(nonatomic,assign) int num;
    @property(nonatomic,assign) NSRange range;
    @property(nonatomic,strong) NSDate *date;
    
    @property(nonatomic,strong) NSDictionary *dict;
    @property(nonatomic,strong) NSArray *array;
    
    @property(nonatomic,strong) JYEncodeSubModel *subModel;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    //.m
    
    #import "JYEncodeModel.h"
    #import "NSObject+JYEncode.h"
    @implementation JYEncodeModel
    
    - (instancetype)initWithCoder:(NSCoder *)aDecoder
    {
        return [self jy_initWithCoder:aDecoder];
    }
    
    - (void)encodeWithCoder:(NSCoder *)aCoder
    {
        [self jy_encodeWithCoder:aCoder];
    }
    
    @end
    
    
    //附上JYEncodeSubModel的内容
    
    //.h
    #import <Foundation/Foundation.h>
    NS_ASSUME_NONNULL_BEGIN
    @interface JYEncodeSubModel : NSObject<NSCoding>
    
    @property(nonatomic,strong) NSString *subName;
    
    @end
    NS_ASSUME_NONNULL_END
    
    //.m
    #import "JYEncodeSubModel.h"
    #import "NSObject+JYEncode.h"
    @implementation JYEncodeSubModel
    
    - (instancetype)initWithCoder:(NSCoder *)aDecoder
    {
        return [self jy_initWithCoder:aDecoder];
    }
    
    - (void)encodeWithCoder:(NSCoder *)aCoder
    {
        [self jy_encodeWithCoder:aCoder];
    }
    
    @end
    

    所以,尽情添加吧,我自岿然不动。

    实现模型类的转化

    其实原理很简单,就是遍历模型中属性的名字,去数据字典中取值,如果取到就进行赋值。

    消息转发

    objc_msgSend方法的使用

    objc_msgSend(receiver,selector)
    

    或者传入参数

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

    当一个message被发送给object,会根据object的isa 指针找到类结构里的方法,如果不能找到,一直顺着父类寻找该方法的实现,直到NSObject类。

    为加快速度,runtime system 会缓存使用过的selector和方法地址。

    • 通过object的isa指针找到他的class
    • 在class的method_list中找到方法
    • 如果class中没有找到方法,继续往superclass中查找
    • 一旦找到这个函数,执行对应的方法实现 (IMP)
    • 找不到 Dynamic Method Resolution(动态方法决议) 如果是实例方法,调用+ (BOOL)resolveInstanceMethod:(SEL)sel,如果是类方法,调用+ (BOOL)resolveClassMethod:(SEL)sel,这样可以让程序在运行时动态的为一个selector提供实现。如果返回YES,运行时系统会重启一次消息的发送过程,调动动态添加方法。
          + (BOOL)resolveInstanceMethod:(SEL)sel
          {
              if (sel == @selector(foo)) {
                  class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "V@:");
              }
              return [super resolveInstanceMethod:sel];
          }
    
          void dynamicMethodIMP(id self,SEL _cmd){
              NSLog(@"%s",__PRETTY_FUNCTION__);
          }
    
          + (BOOL)resolveClassMethod:(SEL)sel
          {
              return [super resolveClassMethod:sel];
          }
    

    Note:Objective-C的方法本质上是一个至少包含了两个参数(id self,SEL _cmd)的C函数。

    • Message Forwarding(消息转发)
      分两步:
      1、首先运行时系统会调用- (id)forwardingTargetForSelector:(SEL)aSelector方法,如果这个方法中返回的不是nil或者self,运行时系统将把消息发送给返回的那个对象
      2、如果- (id)forwardingTargetForSelector:(SEL)aSelector返回的是nil或者self,运行时系统首先会调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法来获得方法签名,方法签名记录了方法的参数和返回值的信息,如果-methodSignatureForSelector 返回的是nil, 运行时系统会抛出unrecognized selector exception,程序到这里就结束了
          - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
          {
              NSMethodSignature *signature =[super methodSignatureForSelector:aSelector];
              if (!signature) {
                  //获取指定对象的方法签名
                  signature = [target methodSignatureForSelector:aSelector];
              }
        
              return signature;
          }
    
          - (void)forwardInvocation:(NSInvocation *)anInvocation
          {
              //检测target是否实现来该方法
              if ([target respondsToSelector:anInvocation.selector]) {
                  //如果实现了,在这儿将方法分发到对象中去 。可利用这个实现多重代理
                  [anInvocation invokeWithTarget:target];
              }
          }
    
          - (id)forwardingTargetForSelector:(SEL)aSelector
          {
              return nil;
          }
    

    或者

    
    // 第一步:我们不动态添加方法,返回NO,进入第二步;
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
        return NO;
    }
    
    // 第二部:我们不指定备选对象响应aSelector,进入第三步;
    - (id)forwardingTargetForSelector:(SEL)aSelector
    {
        return nil;
    }
    
    // 第三步:返回方法选择器,然后进入第四部;
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 
    {
        if ([NSStringFromSelector(aSelector) isEqualToString:@"sing"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
        return [super methodSignatureForSelector:aSelector];
    }
    
    // 第四部:这步我们修改调用对象
    - (void)forwardInvocation:(NSInvocation *)anInvocation 
    {
        // 我们改变调用对象为People
        NewClass *newTarget = [[NewClass alloc] init];
        [anInvocation invokeWithTarget:newTarget];
    }
    

    流程图:


    相关文章

      网友评论

        本文标题:Runtime的几点用法总结

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