码无界IOS And AndroidiOS iOS Developer

iOS高级开发 RunTime机制讲解六---应用实例

2016-03-31  本文已影响250人  kevinLY

通过上面的几篇文章,对Runtime的说明已经算是详细了,对runtime的用途感觉还是很局限,接下来通过具体的实例看下runtime具体能做什么,废话不多说,开始吧
同样还是采用解说+代码的形式,完整的实例代码点击下载DEMO:https://github.com/sleepsheep/RunTimeDemo06,你也可以直接看DEMO,里面也有详细的注释
具体的应用场景有哪些呢

  • 一、添加私有成员变量

一、添加私有成员变量
相信大家都是用过分类,在分类中我们只能添加方法而不能添加成员变量,有时候我们真的需要在分类中添加成员变量的时候我们该怎么办呢?这时候我们就可以使用runtime来实现。
首先先了解几个runtime提供的方法

//关联对象
/** 例:A类包含一个name属性
 * @param object 被关联的对象 (A类)
 * @param key 关联的key 唯一性
 * @param value 关联的对象 (name属性)
 * @param policy objc_AssociationPolicy 是一个枚举,决定了对象是否被释放的策略
   当对象被释放时,会根据这个策略来决定是否释放关联的对象,当策略是RETAIN/COPY时,会释放(release)关联的对象,当是ASSIGN,将不会释放。
 */
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

//获取关联的对象
/** 
 * @param object 被关联的对象
 * @param 关联的key,唯一
 */
id objc_getAssociatedObject(id object, const void *key)

//移除关联的对象
void objc_removeAssociatedObjects(id object)

现在回到上面的问题,如何在一个分类中添加一个属性呢?
下面以给一个button添加回调为例:
创建一个分类CallBack

typedef void(^callBack)(UIButton *);
@interface UIButton (Callback)

- (instancetype)initWithFrame:(CGRect)frame callback:(callBack)callback;

@end

#import "UIButton+Callback.h"
#import <objc/runtime.h>

@interface UIButton()

@property (nonatomic, copy) callBack callback;//私有成员变量

@end

@implementation UIButton (Callback)

- (instancetype)initWithFrame:(CGRect)frame callback:(callBack)callback {
    self = [self initWithFrame:frame];
    if (self) {
        self.callback = callback;
        [self addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside];
    }
    
    return self;
}

- (callBack)callback {
    return objc_getAssociatedObject(self, @selector(callback));
}

- (void)setCallback:(callBack)callback {
    objc_setAssociatedObject(self, @selector(callback), callback, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (void)buttonClick:(UIButton *)button {
    self.callback(button);
}

@end
 
    //1、01添加私有成员变量
    UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(20, 200, 300, 100) callback:^(UIButton *btn) {
        NSLog(@"~~~");
    }];
    button.backgroundColor = [UIColor orangeColor];
    [self.view addSubview:button];

二、添加共有属性
上面的例子是我们不需要把在分类中添加的属性暴露在外面,但是假如我们需要把属性暴露在外面呢,当然了 这是废话 直接在属性放到h文件里面就可以了

@class CustomTabBar;
@interface UITabBarController (Custom)

@property (nonatomic, strong) CustomTabBar *customTabBar;

@end

#import "UITabBarController+Custom.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation UITabBarController (Custom)

- (CustomTabBar *)customTabBar {
    return objc_getAssociatedObject(self, @selector(customTabBar));
}

- (void)setCustomTabBar:(CustomTabBar *)customTabBar {
    objc_setAssociatedObject(self, @selector(customTabBar), customTabBar, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

//...其他的可以操作customTabBar的方法

@end

这样我们就可以随意的操作自定义的tabbar了

三、方法交换
在讲解方法交换之前,我们首先必须对Method有一定的了解
看一下Runtime里面的定义

typedef struct objc_method *Method;

struct objc_method {
    SEL method_name           OBJC2_UNAVAILABLE;  方法名
    char *method_types        OBJC2_UNAVAILABLE;  
    IMP method_imp            OBJC2_UNAVAILABLE;  指向方法实现的指针
}                             

交换的原理:一个方法包含一个方法名和一个指向方法实现的指针,这两者是一一对应的关系:
Asel---AIMP
Bsel---BIMP
即A的方法名指向的A的方法的实现,B的方法名指向的是B的方法的实现
交换后
Asel---BIMP
Bsel---AIMP

先看代码:

@interface UIImage (Hook)

+ (void)load;

@end

#import "UIImage+Hook.h"
#import <objc/runtime.h>

@implementation UIImage (Hook)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class selfClass = object_getClass([self class]);
        
        SEL sel1 = @selector(imageNamed:);
        Method method1 = class_getClassMethod(selfClass, sel1);
        
        SEL sel2 = @selector(myImageNamed:);
        Method method2 = class_getClassMethod(selfClass, sel2);
        
        BOOL flag = class_addMethod(selfClass, sel1, method_getImplementation(method1), method_getTypeEncoding(method1));
        if (flag) {//添加成功说明没有实现,没有实现只能替换
            class_replaceMethod(selfClass, sel1, method_getImplementation(method2), method_getTypeEncoding(method2));
        } else {
            method_exchangeImplementations(method1, method2);
        }
    });
}

+ (UIImage *)myImageNamed:(NSString *)imageName {
    NSString *imageNameNew = [NSString stringWithFormat:@"%@%@", @"new_", imageName];
    return [self myImageNamed:imageNameNew];
}


调用参考ViewController

    注:引入工程中的图片的名字是new_123.png
    //3、03方法交换
    [UIImage load];
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(20, 200, 400, 400)];
    UIImage *image = [UIImage imageNamed:@"123"];
    imageView.image = image;
    [self.view addSubview:imageView];

说明:

//获取一个类的objc_class
object_getClass(id obj)
//根据方法所在的类和类SEL获取方法(类方法)
Method class_getClassMethod(Class cls, SEL name)
//根据方法所在的类和类SEL获取方法(对象方法)
Method class_getInstanceMethod(Class cls, SEL name)
//根据objc_method获取方法的实现
IMP method_getImplementation(Method m) 
//根据objc_method获取一个方法编码类型
const char *method_getTypeEncoding(Method m) 
/** 
 * 给一个类添加一个方法
 *
 * @param cls The class to which to add a method.
 * @param name A selector that specifies the name of the method being added.
 * @param imp A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd.
 * @param types An array of characters that describe the types of the arguments to the method. 
 * 
 * @return YES if the method was added successfully, otherwise NO 
 *  (for example, the class already contains a method implementation with that name).
 * 如果这个方法添加成功,就返回成功,否则失败,例如:如果这个类已经包含了你要添加的方法名字的实现 就返回失败
 *
 * @note class_addMethod will add an override of a superclass's implementation, 
 *  but will not replace an existing implementation in this class. 
 *  To change an existing implementation, use method_setImplementation.
 */
BOOL class_addMethod(Class cls, SEL name, IMP imp,  const char *types)
//替换一个方法的实现
IMP class_replaceMethod(Class cls, SEL name, IMP imp,   const char *types)
//交换两个方法
void method_exchangeImplementations(Method m1, Method m2)

四、解归档

@interface Person : NSObject

- (id)initWithCoder:(NSCoder *)decoder;

- (void)encodeWithCoder:(NSCoder *)encoder;

@end

#import "Person.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation Person

/*!
 *  @author yangL, 16-03-30 18:03:39
 *
 *  @brief 使用runtime进行归档原理:
    1、获取待归档的类的所有的成员变量 
    2、获取成员变量的名称key 
    3、根据key和decoder获取到value
    4、归档返回
 *
 *  @return self
 */
- (id)initWithCoder:(NSCoder *)decoder {
    if (self = [super init]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([self class], &count);//获取到类的所有的成员变量 count代表个数
        
        for (int i = 0; i < count; i++) {
            Ivar ivar = ivars[i];
            
            NSString *key = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
            NSString *value = [decoder decodeObjectForKey:key];
            
//            [self setValue:value forKey:key];
            [self setValue:value forKeyPath:key];
        }
        
        free(ivars);
    }
    
    return self;
}

//解归档
/*!
 *  @author yangL, 16-03-30 18:03:50
 *
 *  @brief 使用runtime进行解归档的原理:
    1、获取待归档的类的所有的成员变量
    2、获取成员变量的名称key
    3、根据key和self获取到value
    4、解归档

 *  @param encoder
 */
- (void)encodeWithCoder:(NSCoder *)encoder {
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivars[i];
        
        NSString *key = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
        
//        NSString *value = [self valueForKey:key];
        id value = [self valueForKeyPath:key];
        
        [encoder encodeObject:value forKey:key];
    }
    
    free(ivars);
}

@end

五、数据模型转化
我们平常也会用到数据模型转化,但是会有一些弊端,一种情况是根据KVC转化,但是这样可能会有一些值为空的情况不好处理;另外一种情况是一个值一个值的校验然后,然后再赋值,这样当变量比较多的时候也会特别的麻烦;
接下来就用Runtime实现json到model的转化;

@interface NSObject (Extension)

- (void)setDiction:(NSDictionary *)dict;

+ (instancetype)objectWithDic:(NSDictionary *)dict;

@end

#import "NSObject+Extension.h"
#import <objc/runtime.h>

@implementation NSObject (Extension)

/*!
 *  @author yangL, 16-03-31 10:03:19
 *
 *  @brief 字典转化为模型的方法
 *
 *  @param dict 待转化的字典
 */
- (void)setDiction:(NSDictionary *)dict {
    Class selfclass = self.class;
    
    //循环遍历 类 (本类 或 所有父类)
    while (selfclass && selfclass != [NSObject class]) {
        
        unsigned int count = 0;
        //获取到待转化的模型的所有的变量
        Ivar* ivars = class_copyIvarList(selfclass, &count);
        
        for (int i = 0; i < count; i++) {
            Ivar ivar = ivars[i];
            
            //获取变量的名字 但是注意 这里获取到的变量前面会有一个下划线 '_'
            NSString *keyTemp = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
            //去掉‘_’ 以便和dict中的key匹配
            NSString *key = [keyTemp substringFromIndex:1];
            //获取字典中对应的key的value
            id value = [dict objectForKey:key];
            //如果字典中没有这个值那么value将会是一个空值
            if (value == nil) {
                continue;
            }
            
            /*
             这里进行下说明:字符串和类的encodingType都是以@开头、int --> i、float--> f
             */
            //如果类中包含另一个类的对象,也将这个类对象的字典转化为模型
            NSString *encodeType = [NSString stringWithCString:ivar_getTypeEncoding(ivar) encoding:NSUTF8StringEncoding];
            //对象会以 @名字 的形式出现 但是NSString也会以这个形式出现
            NSRange range = [encodeType rangeOfString:@"@"];
            //说明是字符串或者是对象
            if (range.location != NSNotFound) {
                //forType是类名或者NSString
                NSString *forType = [encodeType substringWithRange:NSMakeRange(2, encodeType.length - 3)];
                //不是字符串,说明是类
                if (![forType isEqualToString:@"NSString"]) {
                    //字符串转化为类
                    Class classS = NSClassFromString(forType);
                    //解析类中的字段
                    value = [classS objectWithDic:value];
                }
            }
            [self setValue:value forKey:key];
        }
        //官方文档说明,使用完后必须释放
        free(ivars);
        selfclass = [selfclass superclass];
    }
}

+ (instancetype)objectWithDic:(NSDictionary *)dict {
    NSObject *object = [[self alloc] init];
    [object setDiction:dict];
    return object;
}

@end

本人也是初涉Runtime,欢迎指正、交流;

上一篇 下一篇

猜你喜欢

热点阅读