iOS

OC的runtime机制和基本使用场景

2018-11-29  本文已影响28人  无敌大闸蟹

Runtime即我们通常叫的运行时,也就是程序在运行的时候做的事情。是 Objective-C底层的一套C语言的API,是 iOS 内部的核心之一,我们平时编写的 Objective-C 代码,底层都是基于它来实现的,Objective-C代码编译后,其实都是Runtime形式的C语言代码。
OC会将一些工作放在代码运行时才处理而并非编译时。也就是说,有很多类和成员变量在我们编译的时是不知道的,而在运行时,我们所编写的代码会转换成完整的确定的代码运行。
因此,只靠编译器是不够的,我们还需要一个运行时系统来处理编译后的代码。

所以一些用OC不太好实现的功能 我们可以利用runtime来实现 比如:
1:动态交换两个方法的实现(常用于交换系统方法)
2:动态添加对象的成员变量和成员方法
3:获得某个类的所有属性变量属性及方法
4:实现分类Category中可以增加属性
5:拦截系统自带的方法调用,比如拦截viewDidLoad
67891.。。。。。。。。等等等

先来看动态交换两个方法 用到的方法是method_exchangeImplementations
举个例子

    [self fuck];
    [self dontFuck];
    Method method1 = class_getInstanceMethod([self class], @selector(fuck));
    Method method2 = class_getInstanceMethod([self class], @selector(dontFuck));
    //类方法用class_getClassMethod(<#Class  _Nullable __unsafe_unretained cls#>, <#SEL  _Nonnull name#>)
    method_exchangeImplementations(method1, method2);
    [self fuck];
    [self dontFuck];
- (void)fuck
{
    NSLog(@"fuck");
}

- (void)dontFuck
{
    NSLog(@"dontFuck");
}

再看系统方法的拦截交换

比如遇到需求 iOS9 以上的版本需要使用另一套图片, 这时候需要在一个个使用的地方判断版本来加载不同的图片吗? 这样会不会太繁琐呢? 有好的解决方法吗?
这时候就可以使用Swizzle, 来拦截UIImage的 imageName这个加载图片的系统方法, 来交换成我们自己的方法
创建一个UIImage的分类:(UIImage+Category);
在分类中实现一个自定义方法,方法中写要在系统方法中加入的语句,比如版本判断修改图片名

//自定义方法
+ (UIImage *)new_ImageNamed:(NSString *)name {
    double version = [[UIDevice currentDevice].systemVersion doubleValue];
    if (version >= 9.0) {
        name = [name stringByAppendingString:@"_ios9"];
    }
    return [UIImage new_ImageNamed:name]; //方法交换后, 调用imageNamed方法, 让有加载图片的功能
}

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

+ (void)load {
    //获取两个类的类方法
    Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
    Method m2 = class_getClassMethod([UIImage class], @selector(new_ImageNamed:));
    //开始交换方法实现
    method_exchangeImplementations(m1, m2); 
}

在使用中, 如果iOS9以上版本使用另一版本的图片, 就可以交换系统的方法, 直接使用 imageNamed方法, 调用的是new_ImageNamed的实现

再看分类Category中创建属性
一般情况下在 iOS 分类中是无法直接设置属性的,如果在分类的声明中写 @property 只能为其生成 get 和 set 方法的声明,但无法生成成员变量,就是虽然点语法能调用出来,但程序执行后会crash
但是利用runtime可以巧妙的实现
比如给UIView添加一个count和一个viewName 在Category的.h中

@property (nonatomic, assign) NSInteger count;

@property (nonatomic, strong) NSString *viewName;

.m中

- (void)setCount:(NSInteger)count
{
    //将name值和对象关联起来, 将name值存储到当前对象中
    /*参数:
     object: 给哪个对象设置属性;
     key: 一个属性对应一个key, 存储后需要通过这个key取出值, key可为double,int等任意类型, 建议用char可节省字节;
     value: 给属性设置的值;
     policy: 存储策略 (assign, copy, retain);
     */
    objc_setAssociatedObject(self, @selector(count), [NSNumber numberWithInteger:count], OBJC_ASSOCIATION_ASSIGN);
}

- (NSInteger)count
{
    return [objc_getAssociatedObject(self, @selector(count)) integerValue];
}

- (void)setViewName:(NSString *)viewName
{
    objc_setAssociatedObject(self, @selector(viewName), viewName, OBJC_ASSOCIATION_COPY);
}

- (NSString *)viewName
{
    return objc_getAssociatedObject(self, @selector(viewName));
}

这样所有的UIView及其子类都带有这两个属性了

    UIView *testView = [UIView new];
    testView.count = 5;
    testView.viewName = @"testView";
    NSLog(@"%ld,%@",(long)testView.count,testView.viewName);

这里提一下 如果新增的属性和系统的一样 可能会走系统的方法也可能会走你新增的方法 所以命名时需要注意些

再看获取类的所有属性变量和属性
假如有个类是这样的

@interface Person : NSObject

@property (nonatomic, strong) NSString *father;

@property (nonatomic, strong) NSString *mother;

@property (nonatomic, assign) NSInteger age;

@property (nonatomic, strong) NSString *sister;

@end

你需要获取model的所有属性及值可以用class_copyPropertyList获取

    Person *person = [Person new];
    person.father = @"父亲的名字";
    person.mother = @"母亲的名字";
    person.age = 18;
    //获取所有成员变量
    /*
     参数:
     1.哪个类
     2.接收值的地址, 用于存放属性的个数
     3.返回值: 存放所有获取到的属性, 可调出名字和类型
     */
    unsigned int count;
    objc_property_t *propertyArray = class_copyPropertyList([person class], &count);
    for (int i = 0; i < count; i ++) {
        objc_property_t property = propertyArray[i];
        NSString *key = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
        id value = [person valueForKey:key];
        NSLog(@"%@ = %@",key,value);
    }
打印结果为 输出结果.png

再看字典转模型 基本就是处理
1.字典的key和模型的属性匹配不上;
2.模型中嵌套模型(模型属性是另外一个模型对象);
3.数组中装着模型(模型的属性是一个数组,数组中是一个个对象)
三种情况
第一种 runtime 是先遍历模型所有属性,再去字典中根据属性名找对应值进行赋值,多余的键值对也当然不会去看了;另外一种是模型属性数量大于字典的键值对,这时候由于属性没有对应值会被赋值为nil,就会导致crash,我们只需加一个判断即可
第二种 需要利用 runtime 的ivar_getTypeEncoding 方法获取模型对象类型,对该模型对象类型再进行字典转模型,也就是进行递归
第三种 需要获取数组里的元素再进去第一步和第二部的判断

再看动态添加方法 如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决
比如Person类现在是没有实现eat方法的

    Person *person = [Person new];
    [person performSelector:@selector(eat) withObject:nil];
    NSLog(@"%@",person.father);

可以在Person.m中#import "objc/message.h"

void effect(id self, SEL _cmd) {
    [self setValue:@"父亲" forKey:@"father"];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if(sel ==NSSelectorFromString(@"eat")) {
        // class: 给哪个类添加方法
        // SEL: 添加哪个方法
        // IMP: 方法实现 => 函数 => 函数入口 => 函数名
        // type: 方法类型:void用v来表示,id参数用@来表示,SEL用:来表示
        class_addMethod(self, sel, (IMP)effect,"v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
打印出的person.father值为 image.png

未完待续·····

上一篇下一篇

猜你喜欢

热点阅读