Runtime的使用

Runtime简介
runtime顾名思义就是运行时,OC就是运行时机制,其中最主要是消息机制。
对于C语言,函数的调用在编译的时候会决定调用那个函数。
对于OC函数,属于动态调用过程,在编译时并不能真正决定调用那个函数,只有在真正运行时才会根据函数名称找到相应的函数来调用。
Runtime作用
一.实现多继承Multiple Inheritance
根据forwardingTargetForSelector:方法可以发现,一个类可以做到继承多个类的效果,只需要在消息转发时转发给其他对象即可模拟出多继承的效果。

在官方文档上记录这样一个例子:

1194012-d635696fe66b05b8.png

在OC程序中可以借用消息转发机制来实现多继承的功能。 在上图中,一个对象对一个消息做出回应,类似于另一个对象中的方法借过来或是“继承”过来一样。 在图中,warrior实例转发了一个negotiate消息到Diplomat实例中,执行Diplomat中的negotiate方法,结果看起来像是warrior实例执行了一个和Diplomat实例一样的negotiate方法,其实执行者还是Diplomat实例。

这使得不同继承体系分支下的两个类可以“继承”对方的方法,这样一个类可以响应自己继承分支里面的方法,同时也能响应其他不相干类发过来的消息。在上图中Warrior和Diplomat没有继承关系,但是Warrior将negotiate消息转发给了Diplomat后,就好似Diplomat是Warrior的超类一样。

消息转发提供了许多类似于多继承的特性,但是他们之间有一个很大的不同:

多继承:合并了不同的行为特征在一个单独的对象中,会得到一个重量级多层面的对象。

消息转发:将各个功能分散到不同的对象中,得到的一些轻量级的对象,这些对象通过消息通过消息转发联合起来。
二.方法交换Method Swizzling
对于runtime,很多人第一印象就是黑魔法Method Swizzling。毕竟这是runtime使用最广泛的一个方法,毕竟这是Runtime里面很强大的一部分,它可以通过Runtime的API实现更改任意的方法,理论上可以在运行时通过类名/方法名hook到任何 OC 方法,替换任何类的实现以及新增任意类。

  1. Method Swizzling原理

Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。而且Method Swizzling也是iOS中AOP(面相切面编程)的一种实现方式,我们可以利用苹果这一特性来实现AOP编程。

Method Swizzling本质上就是对IMP和SEL进行交换。
2.Method Swizzling使用
一般我们使用都是新建一个分类,在分类中进行Method Swizzling方法的交换。交换的代码模板如下:

import <objc/runtime.h>

@implementation NSObject (Extension)

  • (void)originalClassSelector:(SEL)originalSelector withSwizzledClassSelector:(SEL)swizzledSelector{

    Method originalMethod = class_getClassMethod(self, originalSelector);

    Method swizzledMethod = class_getClassMethod(self, swizzledSelector);

    method_exchangeImplementations(originalMethod, swizzledMethod);

}

@end

@implementation UIFont (KCFont)

  • (void)load{

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

      [self originalClassSelector:@selector(systemFontOfSize:) withSwizzledClassSelector:@selector(kc_systemFontOfSize:)];
    
      [self originalClassSelector:@selector(boldSystemFontOfSize:) withSwizzledClassSelector:@selector(kc_boldSystemFontOfSize:)];
    
      [self originalClassSelector:@selector(italicSystemFontOfSize:) withSwizzledClassSelector:@selector(kc_italicSystemFontOfSize:)];
    
      [self originalClassSelector:@selector(fontWithName:size:) withSwizzledClassSelector:@selector(kc_fontWithName:size:)];
    

    });

}

  • (UIFont *)kc_systemFontOfSize:(CGFloat)fontSize{

    return [self kc_systemFontOfSize:fontSize(SizeScale>1?SizeScale0.9:SizeScale)];

}

  • (UIFont *)kc_boldSystemFontOfSize:(CGFloat)fontSize{

    return [self kc_boldSystemFontOfSize:fontSize(SizeScale>1?SizeScale0.9:SizeScale)];

}

  • (UIFont *)kc_italicSystemFontOfSize:(CGFloat)fontSize{

    return [self kc_italicSystemFontOfSize:fontSize(SizeScale>1?SizeScale0.9:SizeScale)];

}

  • (UIFont *)kc_fontWithName:(NSString *)fontName size:(CGFloat)fontSize{

    return [self kc_fontWithName:fontName size:fontSize(SizeScale>1?SizeScale0.9:SizeScale)];

}

3.Method Swizzling注意点
(1).Swizzling应该总在+load中执行
Objective-C在运行时会自动调用类的两个方法+load和+initialize。+load会在类初始加载时调用, +initialize方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的 +initialize方法是永远不会被调用的。所以Swizzling要是写在+initialize方法中,是有可能永远都不被执行。
和+initialize比较+load能保证在类的初始化过程中被加载。
关于+load和+initialize的比较可以参看这篇文章《Objective-C +load vs +initialize》
(2).Swizzling应该总是在dispatch_once中执行
Swizzling会改变全局状态,所以在运行时采取一些预防措施,使用dispatch_once就能够确保代码不管有多少线程都只被执行一次。这将成为Method Swizzling的最佳实践。
(3).Swizzling在+load中执行时,不要调用[super load]

4.Method Swizzling使用场景

Method Swizzling使用场景其实有很多,在一些特殊的开发需求中适时的使用黑魔法,可以做法神来之笔的效果。这里就举3种常见的场景。

1.实现AOP

AOP官方文档,很多热更新都是使用这种方法来实现的,比如JSPatch等等。(前段时间苹果限制了具有热更新功能的APP上线)

2.实现无埋点统计

如果app统计需求,并且要自己实现一套埋点逻辑,那么这里用到Swizzling是很合适的选择。看到一篇分析的挺精彩的无埋点的文章,推荐大家阅读。
iOS动态性(二)可复用而且高度解耦的用户统计埋点实现

3.实现异常保护

日常开发我们经常会遇到NSArray数组越界的情况,苹果的API也没有对异常保护,所以需要我们开发者开发时候多多留意。关于Index有好多方法,objectAtIndex,removeObjectAtIndex,replaceObjectAtIndex,exchangeObjectAtIndex等等,这些设计到Index都需要判断是否越界。

常见做法是给NSArray,NSMutableArray增加分类,增加这些异常保护的方法,不过如果原有工程里面已经写了大量的AtIndex系列的方法,去替换成新的分类的方法,效率会比较低。这里可以考虑用Swizzling做。
三、动态关联属性Associated Object
Associated Objects是Objective-C 2.0中Runtime的特性之一。众所周知,在 Category 中,我们无法添加@property,因为添加了@property之后并不会自动帮我们生成实例变量以及存取方法。那么,我们现在就可以通过关联对象来实现在 Category 中添加属性的功能了。
1.用法
借用环信里面关于加载框的实现方式

import "UIViewController+HUD.h"

import "MBProgressHUD.h"

import <objc/runtime.h>

define IS_IPHONE_5 ( fabs( ( double )[ [ UIScreen mainScreen ] bounds ].size.height - ( double )568 ) < DBL_EPSILON )

static const void *HttpRequestHUDKey = &HttpRequestHUDKey;

@implementation UIViewController (HUD)

  • (MBProgressHUD *)HUD{

    return objc_getAssociatedObject(self, HttpRequestHUDKey);

}

  • (void)setHUD:(MBProgressHUD *)HUD{

    objc_setAssociatedObject(self, HttpRequestHUDKey, HUD, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

来说明一下这些参数的意义:

1.id object 设置关联对象的实例对象

2.const void *key 区分不同的关联对象的 key。
3.id value 关联的对象
4.objc_AssociationPolicy policy 关联对象的存储策略,它是一个枚举,与property的attribute 相对应。
关联对象3种使用场景
1.为现有的类添加私有变量
2.为现有的类添加公有属性
3.为KVO创建一个关联的观察者。
四、动态增加方法
在消息发送阶段,如果在父类中也没有找到相应的IMP,就会执行resolveInstanceMethod方法。在这个方法里面,我们可以动态的给类对象或者实例对象动态的增加方法。

//动态添加方法

  • (BOOL)resolveInstanceMethod:(SEL)sel{

    if ([NSStringFromSelector(sel) isEqualToString:@"sayHi:"]) {

      class_addMethod(self, sel, (IMP)abc, "v@:@");
    

    }

    return YES;

}
五、NSCoding的自动归档和自动解档
总所周知,项目开发中会需要对自定义的对象进行保存,而保存需要实现NSCoding协议的encodeWithCoder和initWithCoder方法,在项目需求变更或者属性字段很多的情况下,需要手写很多重复的代码,维护起来也很麻烦。这时自动实现就显得很有优势。
用runtime实现的思路就比较简单,快速枚举属性列表,然后利用getter和setter就可以完成encodeWithCoder和initWithCoder了。

pragma mark - 归档/解归档

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

    NSArray *propertyNames = [[self class] properties];

    for (NSString *propertyName in propertyNames) {

      //首字母大写
    
      NSString *setPropertyName = propertyName.capitalizedString;
    
      //NSString* firstCharacter = [propertyName substringToIndex:1].uppercaseString;
    
      //拼接上set关键字
    
      setPropertyName = [NSString stringWithFormat:@"set%@:",setPropertyName];
    
      
    
      //获取set方法
    
      SEL setSel = NSSelectorFromString(setPropertyName);
    
      
    
      [self performSelector:setSel withObject:[aDecoder decodeObjectForKey:propertyName]];
    

    }

    return self;

}

  • (void)encodeWithCoder:(NSCoder *)enCoder{

    NSArray *propertyNames = [[self class] properties];

    for (NSString *propertyName in propertyNames) {

      //获取get方法
    
      SEL getSel = NSSelectorFromString(propertyName);
    
      [enCoder encodeObject:[self performSelector:getSel] forKey:propertyName];
    

    }

}
五、字典和模型相互转化

1.字典转模型

1.调用 class_getProperty 方法获取当前 Model 的所有属性。
2.调用 property_copyAttributeList 获取属性列表。
3.根据属性名称生成 setter 方法。
4.使用 objc_msgSend 调用 setter 方法为 Model 的属性赋值(或者 KVC)

+(id)objectWithKeyValues:(NSDictionary *)aDictionary{
id objc = [[self alloc] init];
for (NSString *key in aDictionary.allKeys) {
id value = aDictionary[key];

    /*判断当前属性是不是Model*/
    objc_property_t property = class_getProperty(self, key.UTF8String);
    unsigned int outCount = 0;
    objc_property_attribute_t *attributeList = property_copyAttributeList(property, &outCount);
    objc_property_attribute_t attribute = attributeList[0];
    NSString *typeString = [NSString stringWithUTF8String:attribute.value];

    if ([typeString isEqualToString:@"@\"Student\""]) {
        value = [self objectWithKeyValues:value];
    }

    //生成setter方法,并用objc_msgSend调用
    NSString *methodName = [NSString stringWithFormat:@"set%@%@:",[key substringToIndex:1].uppercaseString,[key substringFromIndex:1]];
    SEL setter = sel_registerName(methodName.UTF8String);
    if ([objc respondsToSelector:setter]) {
        ((void (*) (id,SEL,id)) objc_msgSend) (objc,setter,value);
    }
    free(attributeList);
}
return objc;

}

这段代码里面有一处判断typeString的,这里判断是防止model嵌套,比如说Student里面还有一层Student,那么这里就需要再次转换一次,当然这里有几层就需要转换几次。

2.模型转字典

这里是上一部分字典转模型的逆步骤:

1.调用 class_copyPropertyList 方法获取当前 Model 的所有属性。
2.调用 property_getName 获取属性名称。
3.根据属性名称生成 getter 方法。
4.使用 objc_msgSend 调用 getter 方法获取属性值(或者 KVC)

//模型转字典
-(NSDictionary *)keyValuesWithObject{
unsigned int outCount = 0;
objc_property_t *propertyList = class_copyPropertyList([self class], &outCount);
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
for (int i = 0; i < outCount; i ++) {
objc_property_t property = propertyList[i];

    //生成getter方法,并用objc_msgSend调用
    const char *propertyName = property_getName(property);
    SEL getter = sel_registerName(propertyName);
    if ([self respondsToSelector:getter]) {
        id value = ((id (*) (id,SEL)) objc_msgSend) (self,getter);

        /*判断当前属性是不是Model*/
        if ([value isKindOfClass:[self class]] && value) {
            value = [value keyValuesWithObject];
        }

        if (value) {
            NSString *key = [NSString stringWithUTF8String:propertyName];
            [dict setObject:value forKey:key];
        }
    }

}
free(propertyList);
return dict;

}

中间注释那里的判断也是防止model嵌套,如果model里面还有一层model,那么model转字典的时候还需要再次转换,同样,有几层就需要转换几次。
六、实现对对象的全打印
在开发中会遇到需要打印对象的需求,而直接打印对象会打印出地址,这显然不是你想要的,你需要的是打印对象的所有属性及其值。
runtime通过重写description方法实现对对象的全打印
1、获取属性列表

  • (NSArray *)properties{
    unsigned int outcount = 0;
    Ivar *ivars = class_copyIvarList(self, &outcount);
    NSMutableArray *arrayM = [NSMutableArray array];
    for ( int i = 0; i< outcount; i ++) {
    NSString *name = @(ivar_getName(ivars[i]));
    NSString *key = [name substringFromIndex:1];
    [arrayM addObject:key];
    }
    free(ivars);
    return [arrayM copy];
    }

2、重写description

  • (NSString *)description{
    NSMutableString *descriptionString = [NSMutableString stringWithFormat:@"\n"];
    NSArray *properNames = [[self class] properties];
    for (NSString *propertyName in properNames) {
    SEL getSel = NSSelectorFromString(propertyName);
    NSString *propertyNameString = [NSString stringWithFormat:@"%@ - %@\n",propertyName,[self performSelector:getSel]];
    [descriptionString appendString:propertyNameString];
    }
    return descriptionString;
    }
    实现原理通过getter方法获取属性的值,实现一一对应的拼接。当然实现一个NSObject的category即可实现对所有继承至NSObject的对象的打印。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容