Runtime的几点用法总结

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];
}

流程图:


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,537评论 33 466
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,162评论 0 7
  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,128评论 0 9
  • ---整理中删繁就简,学会尝试与聚焦 1、朋友这几天组织整理房间的活动,我把电脑、办公室、书房和衣物都整理了一遍。...
    阿白不急阅读 251评论 1 2