runtime在实际开发中的应用

本文大部分转载自:https://www.fuqionglin.com
一、动态添加一个类
#####(“KVO”的实现是利用了runtime能够动态添加类)
原来当你对一个对象进行观察时, 系统会自动新建一个类继承自原类, 然后重写被观察属性的setter方法. 然后重写的setter方法会负责在调用原setter方法前后通知观察者. 然后把原对象的isa指针指向这个新类, 我们知道, 对象是通过isa指针去查找自己是属于哪个类, 并去所在类的方法列表中查找方法的, 所以这个时候这个对象就自然地变成了新类的实例对象.
就像KVO一样, 系统是在程序运行的时候根据你要监听的类, 动态添加一个新类继承自该类, 然后重写原类的setter方法并在里面通知observer的.
那么, 如何动态添加一个类呢? 直接上代码:

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

Class clazz = objc_allocateClassPair([NSObject class], "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);
// 创建实例对象 id object = [[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);

运行结果:

2017-10-24 21:04:08.328 Runtime-实践篇[13699:1043458] 

2017-10-24 21: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:@18 forKey:@"age"];

//    p.address = @"广州大学城";

p.weight = 110.0f;


// 1.打印所有ivars unsigned int ivarCount = 0; // 用一个字典装ivarName和value NSMutableDictionary *ivarDict = [NSMutableDictionary dictionary]; Ivar *ivarList = class_copyIvarList([p class], &ivarCount); for(int i = 0; i < ivarCount; i++){ NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivarList[i])]; id value = [p valueForKey:ivarName];
if (value) { ivarDict[ivarName] = value; } else { ivarDict[ivarName] = @"值为nil"; } } // 打印ivar for (NSString *ivarName in ivarDict.allKeys) { NSLog(@"ivarName:%@, ivarValue:%@",ivarName, ivarDict[ivarName]); }
// 2.打印所有properties unsigned int propertyCount = 0; // 用一个字典装propertyName和value NSMutableDictionary *propertyDict = [NSMutableDictionary dictionary]; objc_property_t *propertyList = class_copyPropertyList([p class], &propertyCount); for(int j = 0; j < propertyCount; j++){ NSString *propertyName = [NSString stringWithUTF8String:property_getName(propertyList[j])]; id value = [p valueForKey:propertyName];
if (value) { propertyDict[propertyName] = value; } else { propertyDict[propertyName] = @"值为nil"; } } // 打印property for (NSString *propertyName in propertyDict.allKeys) { NSLog(@"propertyName:%@, propertyValue:%@",propertyName, propertyDict[propertyName]); }
// 3.打印所有methods unsigned int methodCount = 0; // 用一个字典装methodName和arguments NSMutableDictionary *methodDict = [NSMutableDictionary dictionary]; Method *methodList = class_copyMethodList([p class], &methodCount); for(int k = 0; k < methodCount; k++){ SEL methodSel = method_getName(methodList[k]); NSString *methodName = [NSString stringWithUTF8String:sel_getName(methodSel)];
unsigned int argumentNums = method_getNumberOfArguments(methodList[k]);
methodDict[methodName] = @(argumentNums - 2); // -2的原因是每个方法内部都有self 和 selector 两个参数 } // 打印method for (NSString *methodName in methodDict.allKeys) { NSLog(@"methodName:%@, argumentsCount:%@", methodName, methodDict[methodName]);

打印结果:

2017-10-24 23:06:49.070 Runtime-实践篇[13723:1044813] ivarName:_name, ivarValue:Kobe

2017-10-24 23:06:49.071 Runtime-实践篇[13723:1044813] ivarName:_age, ivarValue:18

2017-10-24 23:06:49.071 Runtime-实践篇[13723:1044813] ivarName:_weight, ivarValue:110

2017-10-24 23:06:49.072 Runtime-实践篇[13723:1044813] ivarName:_address, ivarValue:值为nil

2017-10-24 23:06:49.072 Runtime-实践篇[13723:1044813] propertyName:address, propertyValue:值为nil

2017-10-24 23:06:49.072 Runtime-实践篇[13723:1044813] propertyName:weight, propertyValue:110

2017-10-24 23:06:49.073 Runtime-实践篇[13723:1044813] methodName:setWeight:, argumentsCount:1

2017-10-24 23:06:49.073 Runtime-实践篇[13723:1044813] methodName:weight, argumentsCount:0

2017-10-24 23:06:49.074 Runtime-实践篇[13723:1044813] methodName:setAddress:, argumentsCount:1

2017-10-24 23:06:49.074 Runtime-实践篇[13723:1044813] methodName:address, argumentsCount:0

2017-10-24 23:06:49.074 Runtime-实践篇[13723:1044813] methodName:.cxx_destruct, argumentsCount


2. 动态变量控制

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

-(void)changeAge{

     unsigned int count = 0;

     //动态获取XiaoMing类中的所有属性[当然包括私有] 

     Ivar *ivar = class_copyIvarList([self.xiaoMing class], &count);

     //遍历属性找到对应age字段 

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

         Ivar var = ivar[i];

         const char *varName = ivar_getName(var);

         NSString *name = [NSString stringWithUTF8String:varName];

         if ([name isEqualToString:@"_age"]) {

             //修改对应的字段值成20

             object_setIvar(self.xiaoMing, var, @"20");

             break;

         }

     }

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

 }


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

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

/**

 *  判断类中是否有该属性

 *

 *  @param property 属性名称

 *

 *  @return 判断结果

 */

-(BOOL)hasProperty:(NSString *)property {

    BOOL flag = NO;

    u_int count = 0;

    Ivar *ivars = class_copyIvarList([self class], &count);

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

        const char *propertyName = ivar_getName(ivars[i]);

        NSString *propertyString = [NSString stringWithUTF8String:propertyName];

        if ([propertyString isEqualToString:property]){

            flag = YES;

        }

    }

}



4. 自动的归档和解档

Runtime自动归档和解档

5. 字典转模型

Runtime实现字典转模型
三、利用runtime的动态交换方法实现,我们可以做什么?

1. 方法简单的交换

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

+ (void)run {

    NSLog(@"跑");

}

+ (void)study {

    NSLog(@"学习");

}




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

// 获取两个类的类方法

Method m1 = class_getClassMethod([Person class], @selector(run));

Method m2 = class_getClassMethod([Person class], @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 {

    double version = [[UIDevice currentDevice].systemVersion doubleValue];

    if (version >= 7.0) {

        // 如果系统版本是7.0以上,使用另外一套文件名结尾是‘_os7’的扁平化图片

        name = [name stringByAppendingString:@"_os7"];

    }

    return [UIImage xh_imageNamed:name];

}



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

+ (void)load {

    // 获取两个类的类方法

    Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));

    Method m2 = class_getClassMethod([UIImage class], @selector(xh_imageNamed:));

    // 开始交换方法实现

    method_exchangeImplementations(m1, m2);

}

注意:自定义方法中最后一定要再调用一下系统的方法,让其有加载图片的功能,但是由于方法交换,系统的方法名已经变成了我们自定义的方法名(有点绕,就是用我们的名字能调用系统的方法,用系统的名字能调用我们的方法),这就实现了系统方法的拦截!
利用以上思路,我们还可以给 NSObject 添加分类,统计创建了多少个对象,给控制器添加分类,统计有创建了多少个控制器,特别是公司需求总变的时候,在一些原有控件或模块上添加一个功能,建议使用该方法!
引自于:https://halfrost.com/how_to_use_runtime/#2

1.实现AOP
AOP的例子在上一篇文章中举了一个例子,在下一章中也打算详细分析一下其实现原理,这里就一笔带过。
2.实现埋点统计
如果app有埋点需求,并且要自己实现一套埋点逻辑,那么这里用到Swizzling是很合适的选择。优点在开头已经分析了,这里不再赘述。看到一篇分析的挺精彩的埋点的文章,推荐大家阅读。
3.实现异常保护
日常开发我们经常会遇到NSArray数组越界的情况,苹果的API也没有对异常保护,所以需要我们开发者开发时候多多留意。关于Index有好多方法,objectAtIndex,removeObjectAtIndex,replaceObjectAtIndex,exchangeObjectAtIndex等等,这些设计到Index都需要判断是否越界。
常见做法是给NSArray,NSMutableArray增加分类,增加这些异常保护的方法,不过如果原有工程里面已经写了大量的AtIndex系列的方法,去替换成新的分类的方法,效率会比较低。这里可以考虑用Swizzling做。

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

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

四、动态添加方法

开发使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。
经典面试题:有没有使用performSelector,其实主要想问你有没有动态添加过方法。
简单使用:

@implementation ViewController


- (void)viewDidLoad { [super viewDidLoad]; // 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
@implementation Person // void(*)() // 默认方法都有两个隐式参数, void eat(id self,SEL sel) { NSLog(@"%@ %@",self,NSStringFromSelector(sel)); } // 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来. // 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法 + (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(eat)) { // 动态添加eat方法 // 第一个参数:给哪个类添加方法 // 第二个参数:添加方法的方法编号 // 第三个参数:添加方法的函数实现(函数地址) // 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd class_addMethod(self, @selector(eat), eat, "v@:"); } return [super resolveInstanceMethod: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 给属性赋值和取值

char nameKey;


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

六:

万能界面跳转

在你的开发过程中,是否遇到过如下的需求:
在tableView类型的展示列表中,点击每个cell中人物头像都可以跳转到人物详情,可参见微博中的头像,同理包括转发、评论按钮、各种链接及linkcard。跳转到任意页面
(1)产品要求,某个页面的不同banner图,点击可以跳转到任何一个页面,可能是原生的页面A、页面B,或者是web页C。
(2)在web页面,可以跳转到任何一个原生页面。
(3)在远程推送中跳转到任意指定的页面。
以上2种需求,我想大多数开发者都遇到过,并且可以实现这种功能。毕竟,这是比较基础的功能。但是代码未必那么优雅。
利用runtime动态生成对象、属性、方法这特性,我们可以先跟服务端商量好,定义跳转规则,比如要跳转到A控制器,需要传属性id、type,那么服务端返回字典给我,里面有控制器名,两个属性名跟属性值,客户端就可以根据控制器名生成对象,再用kvc给对象赋值,这样就搞定了。
举例:比如根据推送规则跳转对应界面HSFeedsViewController
HSFeedsViewController.h:
进入该界面需要传的属性

@interface HSFeedsViewController : UIViewController

// 注:根据下面的两个属性,可以从服务器获取对应的频道列表数据

/** 频道ID */

@property (nonatomic, copy) NSString *ID;

/** 频道type */

@property (nonatomic, copy) NSString *type;

@end


AppDelegate.m 中添加以下代码片段:
推送过来的消息规则

// 这个规则肯定事先跟服务端沟通好,跳转对应的界面需要对应的参数

NSDictionary *userInfo = @{

                           @"class": @"HSFeedsViewController",

                           @"property": @{

                                        @"ID": @"123",

                                        @"type": @"12"

                                   }

                           };


跳转界面:

- (void)push:(NSDictionary *)params

{

    // 类名

    NSString *class =[NSString stringWithFormat:@"%@", params[@"class"]];

    const char *className = [class cStringUsingEncoding:NSASCIIStringEncoding];

    // 从一个字串返回一个类

    Class newClass = objc_getClass(className);

    if (!newClass)

    {

        // 创建一个类

        Class superClass = [UIViewController class];

        newClass = objc_allocateClassPair(superClass, className, 0);

        // 注册你创建的这个类

        objc_registerClassPair(newClass);

    }

    // 创建对象

    id instance = [[newClass alloc] init];

    // 对该对象赋值属性

    NSDictionary * propertys = params[@"property"];

    [propertys enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {

        // 检测这个对象是否存在该属性

        if ([self checkIsExistPropertyWithInstance:instance verifyPropertyName:key]) {

            // 利用kvc赋值

            [instance setValue:obj forKey:key];

        }

    }];

    // 获取导航控制器

    UITabBarController *tabVC = (UITabBarController *)self.window.rootViewController;

    UINavigationController *pushClassStance = (UINavigationController *)tabVC.viewControllers[tabVC.selectedIndex];

    // 跳转到对应的控制器

    [pushClassStance pushViewController:instance animated:YES];


检查对象是否存在该属性

- (BOOL)checkIsExistPropertyWithInstance:(id)instance verifyPropertyName:(NSString *)verifyPropertyName

{

    unsigned int outCount, i;

    // 获取对象里的属性列表

    objc_property_t * properties = class_copyPropertyList([instance

                                                           class], &outCount);

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

        objc_property_t property =properties[i];

        //  属性名转成字符串

        NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];

        // 判断该属性是否存在

        if ([propertyName isEqualToString:verifyPropertyName]) {

            free(properties);

            return YES;

        }

    }

    free(properties);

    return NO;

}


七、插件开发
插件入门
XCode 有个很坑爹的地方,就是它并不官方支持插件开发,官方没有文档,XCode 也没有开源,但由于 XCode 是 Objective-C 写的,OC 动态性太强大,导致在这么封闭的情况下民间还是可以做出各种插件,其核心开发方式就是:
dump 出 Xcode 所有头文件,知道 Xcode 里有哪些类和接口。
通过头文件方法名猜测方法的作用,swizzle 这些方法,插入自己的代码实现插件逻辑。
通过 NSNotificationCenter 监听各种事件的发生。
更详细的开发教程网上有不少文章,有兴趣的自行搜索吧。


点评



:runtime 经典的应用要属 JSPatch
热修复,以及AOP 思想的



Aspects,还有类似YYModel 这样的json to model 的库。在其他语言也有 runtime
类似的机制,了解 runtime更能让你深刻理解语言的特性。

如果感觉这篇文章不错可以点击在看:point_down: