Runtime 在实际开发中的应用

runtime再实际开发中主要应用

1.动态添加一个类

2.通过runtime获取一个类的所有属性,我们可以做些什么

   2.1. 打印一个类的所有ivar, property 和 method(简单直接的使用)

   2.2.动态变量控制

   3.3.在NSObject的分类中增加方法来避免使用KVC赋值的时候出现崩溃

    3.4.自动的归档和解档

    3.5.字典转模型

3.利用runtime的动态交换方法实现,我们可以做什么?

     3.1. 方法简单的交换  

     3.2. 拦截系统方法(Swizzle 黑魔法),也可以说成对系统的方法进行替换

      3.3. 运行时实现多继承的效果

4.动态添加方法

5.利用运行时set和get这两个API,可以让类别可以添加属性

6.万能界面跳转(使用了runtime的N多个方法 

7.插件开发



一、动态添加一个类(“KVO”的实现是利用了runtime能够动态添加类)

原来当你对一个对象进行观察时, 系统会自动新建一个类继承自原类, 然后重写被观察属性的setter方法. 然后重写的setter方法会负责在调用原setter方法前后通知观察者. 然后把原对象的isa指针指向这个新类, 我们知道, 对象是通过isa指针去查找自己是属于哪个类, 并去所在类的方法列表中查找方法的, 所以这个时候这个对象就自然地变成了新类的实例对象.

就像KVO一样, 系统是在程序运行的时候根据你要监听的类, 动态添加一个新类继承自该类, 然后重写原类的setter方法并在里面通知observer的.

那么, 如何动态添加一个类呢? 直接上代码:

// 创建一个类(size_t extraBytes该参数通常指定为0, 该参数是分配给类和元类对象尾部的索引ivars的字节数。)

Class clazz = objc_allocateClassPair([NSObjectclass],"GoodPerson",0);// 添加ivar// @encode(aType) : 返回该类型的C字符串class_addIvar(clazz,"_name",sizeof(NSString*), log2(sizeof(NSString*)), @encode(NSString*));

class_addIvar(clazz,"_age",sizeof(NSUInteger), log2(sizeof(NSUInteger)), @encode(NSUInteger));// 注册该类objc_registerClassPair(clazz);// 创建实例对象idobject = [[clazz alloc] init];// 设置ivar[object setValue:@"Tracy"forKey:@"name"];

Ivar ageIvar = class_getInstanceVariable(clazz,"_age");

object_setIvar(object, ageIvar, @18);// 打印对象的类和内存地址NSLog(@"%@", object);// 打印对象的属性值NSLog(@"name = %@, age = %@", [object valueForKey:@"name"], object_getIvar(object, ageIvar));// 当类或者它的子类的实例还存在,则不能调用objc_disposeClassPair方法object =nil;// 销毁类objc_disposeClassPair(clazz);

运行结果为:

2016-09-04 17:04:08.328 Runtime-实践篇[13699:1043458] 

2016-09-04 17:04:08.329 Runtime-实践篇[13699:1043458] name = Tracy, age = 18

这样, 我们就在程序运行时动态添加了一个继承自NSObject的GoodPerson类, 并为该类添加了name和age成员变量.

二、通过runtime获取一个类的所有属性,我们可以做些什么

1. 打印一个类的所有ivar, property 和 method(简单直接的使用)

Person *p = [[Person alloc] init];

[p setValue:@"Kobe"forKey:@"name"];

[p setValue:@18forKey:@"age"];//    p.address = @"广州大学城";p.weight=110.0f;// 1.打印所有ivarsunsignedintivarCount =0;// 用一个字典装ivarName和valueNSMutableDictionary*ivarDict = [NSMutableDictionarydictionary];

Ivar *ivarList = class_copyIvarList([p class], &ivarCount);for(inti =0; i < ivarCount; i++){NSString*ivarName = [NSStringstringWithUTF8String:ivar_getName(ivarList[i])];idvalue = [p valueForKey:ivarName];if(value) {

ivarDict[ivarName] = value;

}else{

ivarDict[ivarName] = @"值为nil";

}

}// 打印ivarfor(NSString*ivarName in ivarDict.allKeys) {NSLog(@"ivarName:%@, ivarValue:%@",ivarName, ivarDict[ivarName]);

}// 2.打印所有propertiesunsignedintpropertyCount =0;// 用一个字典装propertyName和valueNSMutableDictionary*propertyDict = [NSMutableDictionarydictionary];

objc_property_t *propertyList = class_copyPropertyList([p class], &propertyCount);for(intj =0; j < propertyCount; j++){NSString*propertyName = [NSStringstringWithUTF8String:property_getName(propertyList[j])];idvalue = [p valueForKey:propertyName];if(value) {

propertyDict[propertyName] = value;

}else{

propertyDict[propertyName] = @"值为nil";

}

}// 打印propertyfor(NSString*propertyName in propertyDict.allKeys) {NSLog(@"propertyName:%@, propertyValue:%@",propertyName, propertyDict[propertyName]);

}// 3.打印所有methodsunsignedintmethodCount =0;// 用一个字典装methodName和argumentsNSMutableDictionary*methodDict = [NSMutableDictionarydictionary];

Method *methodList = class_copyMethodList([p class], &methodCount);for(intk =0; k < methodCount; k++){

SEL methodSel = method_getName(methodList[k]);NSString*methodName = [NSStringstringWithUTF8String:sel_getName(methodSel)];unsignedintargumentNums = method_getNumberOfArguments(methodList[k]);

methodDict[methodName] = @(argumentNums -2);// -2的原因是每个方法内部都有self 和 selector 两个参数}// 打印methodfor(NSString*methodName in methodDict.allKeys) {NSLog(@"methodName:%@, argumentsCount:%@", methodName, methodDict[methodName]);

}

打印结果为 :

2. 动态变量控制

在程序中,XiaoMing的age是10,后来被runtime变成了20,来看看runtime是怎么做到的:

-(void)changeAge{unsignedintcount =0;//动态获取XiaoMing类中的所有属性[当然包括私有]Ivar *ivar = class_copyIvarList([self.xiaoMingclass], &count);//遍历属性找到对应age字段for(inti =0; i

Ivar var = ivar[i];constchar*varName = ivar_getName(var);NSString*name = [NSStringstringWithUTF8String:varName];if([name isEqualToString:@"_age"]) {//修改对应的字段值成20object_setIvar(self.xiaoMing, var, @"20");break;

}

}NSLog(@"XiaoMing's age is %@",self.xiaoMing.age);

}

3. 在NSObject的分类中增加方法来避免使用KVC赋值的时候出现崩溃

在有些时候我们需要通过KVC去修改某个类的私有变量,但是又不知道该属性是否存在,如果类中不存在该属性,那么通过KVC赋值就会crash,这时也可以通过运行时进行判断。同样我们在NSObject的分类中增加如下方法。

/** * 判断类中是否有该属性 * *@paramproperty 属性名称 * *@return判断结果 */-(BOOL)hasProperty:(NSString *)property {

     BOOL flag = NO;

    u_int count =0;

    Ivar *ivars = class_copyIvarList([self class], &count);for(inti =0; i < count; i++)              {constchar*propertyName = ivar_getName(ivars[i]);

    NSString *propertyString = [NSString             stringWithUTF8String:propertyName];if([propertyString  isEqualToString:property]){

flag = YES;

}

}

}

4. 自动的归档和解档

博主在学习 Runtime 之前,归档的时候是酱紫写的:

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

[aCoder encodeObject:self.name forKey:@"name"];

[aCoder encodeObject:self.ID forKey:@"ID"];

}

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

if(self = [superinit]) {

self.ID = [aDecoder decodeObjectForKey:@"ID"];

self.name = [aDecoder decodeObjectForKey:@"name"];

}

returnself;

}

那么问题来了,如果当前 Model 有100个属性的话,就需要写100行这种代码

[aCoder encodeObject:self.name forKey:@"name"];

想想都头疼,通过 Runtime 我们就可以轻松解决这个问题:

1.使用 class_copyIvarList 方法获取当前 Model 的所有成员变量.

2.使用 ivar_getName 方法获取成员变量的名称.

3.通过 KVC 来读取 Model 的属性值(encodeWithCoder:),以及给 Model 的属性赋值(initWithCoder:).

举个栗子,新建一个 Model 类,其.m文件如下:

#import "TestModel.h"

#import #import @implementation TestModel

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

unsigned int outCount = 0;

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

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

Ivarvar= vars[i];

const char *name = ivar_getName(var);

NSString *key = [NSString stringWithUTF8String:name];

// 注意kvc的特性是,如果能找到key这个属性的setter方法,则调用setter方法

// 如果找不到setter方法,则查找成员变量key或者成员变量_key,并且为其赋值

// 所以这里不需要再另外处理成员变量名称的“_”前缀

id value = [self valueForKey:key];

[aCoder encodeObject:value forKey:key];

}

}

- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder{

if(self = [superinit]) {

unsigned int outCount = 0;

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

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

Ivarvar= vars[i];

const char *name = ivar_getName(var);

NSString *key = [NSString stringWithUTF8String:name];

id value = [aDecoder decodeObjectForKey:key];

[self setValue:value forKey:key];

}

}

returnself;

}

@end

完整的自动归档代码在这里 :https://github.com/daiweiping/RuntimeLearn

5. 字典转模型

最开始博主是这样用字典给 Model 赋值的:

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

if(self = [superinit]) {

self.age = dict[@"age"];

self.name = dict[@"name"];

}

returnself;

}

可想而知,遇到的问题跟归档时候一样(后来使用MJExtension),这里我们稍微来学习一下其中原理,字典转模型的时候:

1.根据字典的 key 生成 setter 方法

2.使用 objc_msgSend 调用 setter 方法为 Model 的属性赋值(或者 KVC)

模型转字典的时候:

1.调用 class_copyPropertyList 方法获取当前 Model 的所有属性

2.调用 property_getName 获取属性名称

3.根据属性名称生成 getter 方法

4.使用 objc_msgSend 调用 getter 方法获取属性值(或者 KVC)

代码如下:

#import "NSObject+KeyValues.h"

#import #import @implementation NSObject (KeyValues)

//字典转模型

+(id)objectWithKeyValues:(NSDictionary *)aDictionary{

id objc = [[self alloc] init];

for(NSString *keyinaDictionary.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:@"@\"TestModel\""]) {

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);

}

}

returnobjc;

}

//模型转字典

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

}

}

}

returndict;

}

@end

完整代码在这里  https://github.com/daiweiping/RuntimeLearn

三、利用runtime的动态交换方法实现,我们可以做什么?

1. 方法简单的交换

创建一个Person类,类中实现以下两个类方法,并在.h 文件中声明

+ (void)run {NSLog(@"跑");

}

+ (void)study {NSLog(@"学习");

}

控制器中调用,则先打印跑,后打印学习

[Person run];

[Person study];

下面通过runtime 实现方法交换,类方法用class_getClassMethod,对象方法用class_getInstanceMethod

// 获取两个类的类方法

Methodm1=class_getClassMethod([Personclass], @selector(run));

Methodm2=class_getClassMethod([Personclass], @selector(study));

// 开始交换方法实现method_exchangeImplementations(m1, m2);// 交换后,先打印学习,再打印跑!

[Person run];

[Person study];

2. 拦截系统方法(Swizzle 黑魔法),也可以说成对系统的方法进行替换

由于某种原因,我们要改变这个方法的实现,但是又不能去动它的源代码(系统的方法或者一些开源库出现问题的时候),这个时候runtime就派上用场了。

需求:比如iOS6 升级 iOS7 后需要版本适配,根据不同系统使用不同样式图片(拟物化和扁平化),如何通过不去手动一个个修改每个UIImage的imageNamed:方法就可以实现为该方法中加入版本判断语句?

步骤:

1、为UIImage建一个分类(UIImage+Category)

2、在分类中实现一个自定义方法,方法中写要在系统方法中加入的语句,比如版本判断

+ (UIImage*)xh_imageNamed:(NSString*)name {doubleversion = [[UIDevice currentDevice].systemVersiondoubleValue];if(version >=7.0) {// 如果系统版本是7.0以上,使用另外一套文件名结尾是‘_os7’的扁平化图片name = [name stringByAppendingString:@"_os7"];

}return[UIImagexh_imageNamed:name];

}

3、分类中重写UIImage的load方法,实现方法的交换(只要能让其执行一次方法交换语句,load再合适不过了)

+ (void)load {// 获取两个类的类方法Method m1 = class_getClassMethod([UIImageclass],@selector(imageNamed:));

Method m2 = class_getClassMethod([UIImageclass],@selector(xh_imageNamed:));// 开始交换方法实现method_exchangeImplementations(m1, m2);

}

注意:自定义方法中最后一定要再调用一下系统的方法,让其有加载图片的功能,但是由于方法交换,系统的方法名已经变成了我们自定义的方法名(有点绕,就是用我们的名字能调用系统的方法,用系统的名字能调用我们的方法),这就实现了系统方法的拦截!

利用以上思路,我们还可以给 NSObject 添加分类,统计创建了多少个对象,给控制器添加分类,统计有创建了多少个控制器,特别是公司需求总变的时候,在一些原有控件或模块上添加一个功能,建议使用该方法!

交换原理:

交换之前:


交换之后:


3. 运行时实现多继承的效果

既然方法我们可以拦截,可以交换,那么实现多继承的效果就留给读者自己思考了(避免篇幅太长,后续在博客中再来探讨这个问题)

四、动态添加方法

开发使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。

经典面试题:有没有使用performSelector,其实主要想问你有没有动态添加过方法。

简单使用:

@implementationViewController- (void)viewDidLoad {

[superviewDidLoad];// Do any additional setup after loading the view, typically from a nib.Person *p = [[Person alloc] init];// 默认person,没有实现eat方法,可以通过performSelector调用,但是会报错。// 动态添加方法就不会报错[p performSelector:@selector(eat)];

}@end@implementationPerson// void(*)()// 默认方法都有两个隐式参数,voideat(idself,SEL sel)

{NSLog(@"%@ %@",self,NSStringFromSelector(sel));

}// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.// 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法+ (BOOL)resolveInstanceMethod:(SEL)sel

{if(sel ==@selector(eat)) {// 动态添加eat方法// 第一个参数:给哪个类添加方法// 第二个参数:添加方法的方法编号// 第三个参数:添加方法的函数实现(函数地址)// 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmdclass_addMethod(self,@selector(eat), eat,"v@:");

}return[superresolveInstanceMethod:sel];

}@end

五、利用运行时set和get这两个API,可以让类别可以添加属性

步骤:

1、创建一个类别,比如给任何一个对象都添加一个name属性,就是NSObject添加分类(NSObject+Category)

2、先在.h 中@property 声明出get 和 set 方法,方便点语法调用

     @property(nonatomic,copy)NSString *name;

3、在.m 中重写set 和 get 方法,内部利用runtime 给属性赋值和取值

charnameKey;

- (void)setName:(NSString*)name {// 将某个值跟某个对象关联起来,将某个值存储到某个对象中objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);

}

- (NSString*)name {returnobjc_getAssociatedObject(self, &nameKey);

}

六、万能界面跳转(使用了runtime的N多个方法)

(特别是根据推送内容跳不同界面) http://www.jianshu.com/writer#/notebooks/16974385/notes/20278179

 http://blog.csdn.net/coyote1994/article/details/52472670

七、插件开发

插件入门

XCode 有个很坑爹的地方,就是它并不官方支持插件开发,官方没有文档,XCode 也没有开源,但由于 XCode 是 Objective-C 写的,OC 动态性太强大,导致在这么封闭的情况下民间还是可以做出各种插件,其核心开发方式就是:

dump 出 Xcode 所有头文件,知道 Xcode 里有哪些类和接口。

通过头文件方法名猜测方法的作用,swizzle 这些方法,插入自己的代码实现插件逻辑。

通过 NSNotificationCenter 监听各种事件的发生。

更详细的开发教程网上有不少文章,有兴趣的自行搜索吧。

八、Jspath 热更新 也是使用运行时,jspatch 基本上算是黑科技,在线修复版本bug,微信都使用了这个技术,详情百度“JSPatch”

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,633评论 0 9
  • Objective-C中有两个NSObject,一个是NSObject类,另一个是NSObject协议。而其中NS...
    ScaryMonsterLyn阅读 754评论 0 2
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,109评论 0 7
  • 对于从事 iOS 开发人员来说,所有的人都会答出【runtime 是运行时】什么情况下用runtime?大部分人能...
    梦夜繁星阅读 3,673评论 7 64
  • 6月底开始的一场旅行,至今仍未结束。 美国的公路旅行虽然辛苦,但收获了久违的内心的放松,这是我记忆中美国的味道吧。...
    Union街阅读 158评论 0 0