Runtime的几点用法总结

来源:互联网 发布:北通 手柄 知乎 编辑:程序博客网 时间:2024/04/30 12:21

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;@endNS_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;@endNS_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;@endNS_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;@endNS_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;@endNS_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;@endNS_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;      }

流程图:
这里写图片描述

原文链接:http://www.jianshu.com/p/e8e523046237

0 0
原创粉丝点击