demoiOS开发iOS Developer

iOS 开发之runtime使用小结

2017-06-02  本文已影响94人  重驹

我们一般用runtime做以下这些事情:

 1.动态交换两个方法的实现(同时也可以替换系统的方法)
 2.获得某个类的所有成员方法、所有成员变量
 3.动态的改变属性值、增加一个属性
 4.动态的增加一个方法
 5.实现NSCoding的自动归档和解档
 6.实现字典转模型的自动转换

一、使用runtime如何交换两个方法的实现,拦截系统自带的方法调用功能。

1.交换两个方法的实现:

首先我们创建一个工程,使用市面上最常用的一个例子,创建一个Person类,写两个类方法、两个实例方法。
.h

#import <Foundation/Foundation.h>

@interface Person : NSObject
+ (void)eat;
+ (void)sleep;
- (void)study;
- (void)playGame;
@end

.m

#import "Person.h"

@implementation Person
+ (void)eat{
    NSLog(@"哥么 吃了");
}
+ (void)sleep{
    NSLog(@"哥么 睡了");
}
- (void)study{
    NSLog(@"小伙儿 在学习");
}
- (void)playGame{
    NSLog(@"小伙儿 在打游戏");
}
@end

预备东西已经准备完全,现在开始交换两个方法。
在用runtime交换两个方法的时候,首先需要倒入头文件#import <objc/runtime.h>。之后你需要了解下面三个方法是干什么用的。

//获得某个类的类方法
Method class_getClassMethod(Class cls , SEL name)
//获得某个类的实例对象方法
Method class_getInstanceMethod(Class cls , SEL name)
//交换两个方法的实现
void method_exchangeImplementations(Method m1 , Method m2)

了解了上面的知识点后,就进入我们的主题了:
案例1:交换两个类方法

//交换类方法
Method method1 = class_getClassMethod([Person class], @selector(eat));
Method method2 = class_getClassMethod([Person class], @selector(sleep));
method_exchangeImplementations(method1, method2);
[Person eat];
[Person sleep];

案例2:交换实例方法

//交换实例方法
Method method3 = class_getInstanceMethod([Person class], @selector(study));
Method method4 = class_getInstanceMethod([Person class], @selector(playGame));
method_exchangeImplementations(method3, method4);
Person *person = [[Person alloc] init];
[person study];
[person playGame];

2.拦截系统方法:

例如在iOS7出来之后,设计风格偏向于扁平化 扁平化设计风格介绍 ,有些公司为了适应趋势,会做出图片的大批量更改,如果不想一张一张替换原先的图片,就可以使用runtime截获imageNamed方法,作出相应的处理:
我们创建一个UIImage的分类,在.m文件里里重写一个类方法

+ (UIImage *)LF_imageNamed:(NSString *)name {
double version = [[UIDevice currentDevice].systemVersion doubleValue];
if (version >= 7.0) {
    name = [name stringByAppendingString:@"_change"];
}
  return [UIImage LF_imageNamed:name];
}

我们可以在load方法中将重写的方法跟imageNamed方法互换:

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

上面内容的Demo在这里

二、使用runtime获得某个类的所有成员方法、所有成员变量

class_copyIvarList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)

上面的方法是获取属性需要用的方法

class_addMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, <#const char *types#>)

上面的方法是获取类中所有方法的方法
首先创建一个继承NSObject的Student类,在.h文件中

#import <Foundation/Foundation.h>
@interface Student : NSObject
@property (assign, nonatomic) int age;
- (void)study;
- (void)sleep;

@end

.m

#import "Student.h"
@interface Student()
{
    NSString *name;
}
@end
@implementation Student
//初始化person属性
-(instancetype)init{
    self = [super init];
    if(self) {
        name = @"Tom";
        self.age = 12;
    } 
    return self;
}
- (void)study{
    NSLog(@"学生要学习");
}
- (void)sleep{
    NSLog(@"学生要睡觉");
}
//输出person对象时的方法:
-(NSString *)description{
    return [NSString stringWithFormat:@"name:%@ age:%d",name,self.age];
}
@end

注意在.m文件中我们声明了私有实例变量,这个我们也是可以通过runtime获取到的。
我们在ViewController中进行获取全部属性 和全部方法的实现代码

unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([Student class], &outCount);

// 遍历所有成员变量
for (int i = 0; i < outCount; i++) {
    // 取出i位置对应的成员变量
    Ivar ivar = ivars[i];
    const char *name = ivar_getName(ivar);
    const char *type = ivar_getTypeEncoding(ivar);
    NSLog(@"成员变量名:%s 成员变量类型:%s",name,type);
}
// 注意释放内存!
free(ivars);

上面是获取所有成员变量的方法,获取的结果如下:

2017-05-27 14:09:53.786 runtime-归档解档[4081:960384] 成员变量名:name 成员变量类型:@"NSString"
2017-05-27 14:09:53.787 runtime-归档解档[4081:960384] 成员变量名:_age 成员变量类型:i

其中的i代表的是int类型。
这里经常在其他博主文章那里会看到有人问" "和没有下划线的问题。这里就不得不提到另一个方法copyPropertyList。copyPropertyList和copyIvarList的区别就在于后者能够获取到"{}"中的成员变量,而前者只能获取到“@property”声明的属性变量。而copyIvarList获取到的一般都会主动在成员变量名称前面加上“”,当然如果是“{}”中的成员变量copyIvarList也不会主动加“”。

三、动态的改变属性值、增加一个属性

//改变属性值
- (IBAction)changeVariable:(UIButton *)sender {
    NSLog(@"打印当前对象 -- %@",student);
    unsigned int count = 0;
    Ivar *variLists = class_copyIvarList([Student class], &count);
    Ivar ivar = variLists[0];//这里我们知道第一个参数是name,所以就直接取第一个元素
    const char *str = ivar_getName(ivar);
    NSLog(@"得到的Ivar是 -- %s",str);

    object_setIvar(student, ivar, @"Mars");
    NSLog(@"改变之后的student:%@",student);
}

怎样获取属性,在第二块已经说过了,这里我们获取到属性之后,使用

object_setIvar(<#id obj#>, <#Ivar ivar#>, <#id value#>)

这个方法,进行设置属性的值。

2017-05-27 14:41:24.113 runtime-归档解档[4081:960384] 打印当前对象 -- name:Tom age:12
2017-05-27 14:41:24.114 runtime-归档解档[4081:960384] 得到的Ivar是 -- name
2017-05-27 14:41:24.114 runtime-归档解档[4081:960384] 改变之后的student:name:Mars age:12

从输出的log中,我们可以看出,初始值name是Tom,在用object_setIvar这个方法进行了设置属性之后,name变成了Mars,这样就做到动态的修改属性值的效果了。

动态的添加一个属性,我们往往是创建某个类的分类,之后在这个分类里面做处理,譬如这里,我们给Student类添加一个属性,我们在Student的分类.h里面生命一个属性

@property (nonatomic,assign)float height; //新属性

在分类的.m文件里面

#import "Student+category.h"
#import <objc/runtime.h> 

const char * str = "myKey"; //做为key,字符常量 必须是C语言字符串;

@implementation Student (category)

-(void)setHeight:(float)height{
    NSNumber *num = [NSNumber numberWithFloat:height];
/*
 第一个参数是需要添加属性的对象;
 第二个参数是属性的key;
 第三个参数是属性的值,类型必须为id,所以此处height先转为NSNumber类型;
 第四个参数是使用策略,是一个枚举值,类似@property属性创建时设置的关键字,可从命名看出各枚举的意义;
 objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
 */
objc_setAssociatedObject(self, str, num, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

//提取属性的值:
-(float)height{
    NSNumber *number = objc_getAssociatedObject(self, str);
return [number floatValue];
}

@end

这里主要的是objc_setAssociatedObject 和 objc_getAssociatedObject两个方法的 调用。
在ViewController中对新添加的属性进行赋值,之后再用Student类的实例对象调用这个属性的get方法。

//增加属性
- (IBAction)addVariable:(UIButton *)sender {

student.height = 12;           //给新属性height赋值
NSLog(@"%f",[student height]); //访问新属性值
}

打印的结果如下:

动态增加属性.png

四、动态的增加一个方法

//添加一个新的方法
- (IBAction)addNewMethod:(UIButton *)sender {
/* 动态添加方法:
 第一个参数表示Class cls 类型;
 第二个参数表示待调用的方法名称;
 第三个参数(IMP)myAddingFunction,IMP一个函数指针,这里表示指定具体实现方法myAddingFunction;
 第四个参数表方法的参数,0代表没有参数;
 */
class_addMethod([student class], @selector(addNewMethod), (IMP)myAddingFunction, 0);
//调用方法 
[student performSelector:@selector(addNewMethod)];
}    

//具体的实现(方法的内部都默认包含两个参数Class类和SEL方法,被称为隐式参数。)
int myAddingFunction(id self, SEL _cmd){
    NSLog(@"新增的addNewMethod方法已经加入");
    return 1;
}

- (void)addNewMethod{
    NSLog(@"啦啦啦");
}

动态的添加一个方法,主要的是对runtime中class_addMethod这个方法的使用。addNewMethod是给Student对象调用的方法名,实际的调用的是函数myAddingFunction,写功能性代码也是写在这里的。
上面二三四点的Demo地址

五、使用runtime实现NSCoding的自动归档和解档

这里只是处理一些常用的类型,像const void *这样类似的类型是不支持kvc的,没做处理。如果想了解推荐去看下 标哥的技术博客 。下面开始说怎样利用runtime实现NSCoding的自动归档和解档。
新建一个项目,添加一个继承NSObject的LFUserInfo类,遵守了NSCoding协议之后,我们就可以在实现文件中实现-encodeWithCoder:方法来归档和-initWithCoder:解档。
.h文件:

#import <Foundation/Foundation.h>
@interface LFUserInfo : NSObject<NSCoding>
@property (copy  , nonatomic) NSString *username;
@property (assign, nonatomic) int age;
@property (assign, nonatomic) double height;
@property (strong, nonatomic) NSNumber *phoneNumber;
@end

.m文件

#import "LFUserInfo.h"
#import <objc/runtime.h>
@implementation LFUserInfo
- (NSArray *)ignoredNames {
    return @[@"_height",@"_phoneNumber"];
}
//解档数据
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super init]) {
        unsigned int count = 0;
        Ivar *ivarLists = class_copyIvarList([self class], &count);
        for (int i = 0 ; i<count; i++) {
            Ivar ivar = ivarLists[i];
            const char *key = ivar_getName(ivar);
            NSString *keyStr  = [NSString stringWithUTF8String:key];
        
            //忽略不需要归档的元素
            if ([[self ignoredNames] containsObject:keyStr]) {
                continue;
            }
            //进行解档取值
            id value = [aDecoder decodeObjectForKey:keyStr];
        
              //利用KVC对属性赋值
                [self setValue:value forKey:keyStr];
          }
        free(ivarLists);
    }
    return self;
}
//归档数据
-(void)encodeWithCoder:(NSCoder *)aCoder{
    unsigned int count = 0;
    Ivar *ivarLIst = class_copyIvarList([self class], &count);
    for (int i = 0; i<count; i++) {
        Ivar ivar = ivarLIst[i];
        const char *key = ivar_getName(ivar);
        NSString *keyStr = [NSString stringWithUTF8String:key];
        // 忽略不需要解档的属性
        if ([[self ignoredNames] containsObject:keyStr]) {
            continue;
        }
        //利用KVC取值
        id value = [self valueForKey:keyStr];
        [aCoder encodeObject:value forKey:keyStr];
    }
    free(ivarLIst);
}
@end

之后我们在ViewController中进行归档解档操作

归档解档后.png
从打印的结果可以看到,我们忽视的参数,在归档解档后被忽略,其他的属性都可以归档解档操作。关于runtime实现归档解档其实不难,主要就是通过runtime动态的获取到相关的属性之后,进行操作就行了。关于归档解档的Demo在这里

六、实现字典转模型的自动转换

这里只是最简单的一层字典直接转模型的处理,多层数据结构的字典转模型不再本Demo范畴。
首先我们创建几个比较正常的属性列表,譬如此Demo中,我们创建一个LFStudentmodel的model类,声明三种类型属性变量:

@property (strong,nonatomic) NSString *name;
@property (strong,nonatomic) NSString *schoolName;
@property (strong,nonatomic) NSString *unitState;

第二步,我们创建一个继承于NSObject的分类NSObject (Category),在.h文件中创建一个类方法

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

在.m文件中实现这个方法:

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

@implementation NSObject (Category)

+ (NSArray *)propertList
{
unsigned int count = 0;
//获取模型属性, 返回值是所有属性的数组 objc_property_t
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
NSMutableArray *arr = [NSMutableArray array];
//便利数组
for (int i = 0; i< count; i++) {
    //获取属性
    objc_property_t property = propertyList[i];
    //获取属性名称
    const char *cName = property_getName(property);
    NSString *name = [[NSString alloc]initWithUTF8String:cName];
    //添加到数组中
    [arr addObject:name];
}
    //释放属性组
    free(propertyList);
    return arr.copy;
}

  + (instancetype)modelWithDict:(NSDictionary *)dict
  {
id obj = [self new];
// 遍历属性数组
for (NSString *property in [self propertList]) {
    // 判断字典中是否包含这个key
    if (dict[property]) {
        // 使用 KVC 赋值
        [obj setValue:dict[property] forKey:property];
    }
}
return obj;
}


@end

之后将我们这个分类的头文件导入到LFStudentmodel模型类中就可以实现,字典转模型的功效了。
之后我们看一下字典转模型的实操
创建一个数据管理类

#import "LFDataManager.h"
#import "LFStudentmodel.h"

@implementation LFDataManager

- (NSArray *)getSourceDataArray
{
NSArray *sourceArray = @[@{@"name":@"Tom",@"schoolName":@"aaa",@"unitState":@"111"},@{@"name":@"Sum",@"schoolName":@"bbb",@"unitState":@"222"},@{@"name":@"Amy",@"schoolName":@"ccc",@"unitState":@"333"},@{@"name":@"Evy",@"schoolName":@"ddd",@"unitState":@"444"},@{@"name":@"Any",@"schoolName":@"eee",@"unitState":@"555"}];
NSMutableArray *mArray = [NSMutableArray new];
for (int i=0; i<sourceArray.count; i++) {
    LFStudentmodel *stuModel = [LFStudentmodel modelWithDict:sourceArray[i]];
    [mArray addObject:stuModel];
}
return mArray;
}

最后我们在ViewController中调用展示下数据

- (void)viewDidLoad {
[super viewDidLoad];
LFDataManager *manager = [LFDataManager new];
NSArray *dataArray = [manager getSourceDataArray];
for (int i =0; i<dataArray.count; i++) {
    LFStudentmodel *model = dataArray [i];
    NSLog(@"dataArray --- name=  %@ schoolName=  %@   unitState = %@",model.name,model.schoolName,model.unitState);
}


}

结果如下:

1111.png
现在市面上很多主流解析模型的三方MJExtension、YYModel等思想也是通过runtime进行封装解析,最主要的还是setValue -- forkey这个大招来操作的。有兴趣的可以去深入了解下解析模型的三方底层实现方式。
Demo地址
喜欢的朋友请不吝你的👍,GitHub上点个star啥的。大神觉得写的有毛病的地方,还请指教,谢谢!
上一篇下一篇

猜你喜欢

热点阅读