美文网首页iOS面试专题
Runtime 在实际开发中的应用

Runtime 在实际开发中的应用

作者: 十里杏坡 | 来源:发表于2017-11-27 17:51 被阅读0次

    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”

    相关文章

      网友评论

        本文标题:Runtime 在实际开发中的应用

        本文链接:https://www.haomeiwen.com/subject/bhnyextx.html