Runtime 应用

参考文章:

1、Objctive-C Runtime
2、梧雨北辰
3、jackyshan
4、人仙儿a
本文主要是参考梧雨北辰的文章,并在该作者的文章之上添加自己理解的内容。侵权必删

Runtime应用框架.png

1、方法魔法(Method Swizzling)

实现动态方法交换(Method Swizzling )是Runtime中最具盛名的应用场景,其原理是:通过Runtime获取到方法实现的地址,进而动态交换两个方法的功能。使用到关键方法如下:

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

1.1 动态方法交换示例

- (void)printA{
    NSLog(@"打印A......");
}

- (void)printB{
    NSLog(@"打印B......");
}

//交换方法的实现,并测试打印
Method methodA = class_getInstanceMethod([self class], @selector(printA));
Method methodB = class_getInstanceMethod([self class], @selector(printB));
method_exchangeImplementations(methodA, methodB);

[self printA];  ///打印B......
[self printB];  ///打印A......

1.2 拦截并替换系统方法

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(jkviewDidLoad);
        
        Method originalMethod = class_getInstanceMethod(class,originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);
        
        //judge the method named  swizzledMethod is already existed.
        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        // if swizzledMethod is already existed.
        if (didAddMethod) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        }
        else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)jkviewDidLoad {
    NSLog(@"替换的方法");
    
    [self jkviewDidLoad];
}

- (void)viewDidLoad {
    NSLog(@"自带的方法");
    
    [super viewDidLoad];
}

@end

swizzling应该只在+load中完成。 在 Objective-C 的运行时中,每个类有两个方法都会自动调用。+load 是在一个类被初始装载时调用,+initialize 是在应用第一次调用该类的类方法或实例方法前调用的。两个方法都是可选的,并且只有在方法被实现的情况下才会被调用。

swizzling应该只在dispatch_once 中完成,由于swizzling 改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatchdispatch_once满足了所需要的需求,并且应该被当做使用swizzling 的初始化单例方法的标准。

1.3 KVO实现

全称是Key-value observing,翻译成键值观察。提供了一种当其它对象属性被修改的时候能通知当前对象的机制。再MVC大行其道的Cocoa中,KVO机制很适合实现modelcontroller类之间的通讯。

KVO的实现依赖于 Objective-C 强大的 Runtime,当观察某对象 A 时,KVO 机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性 keyPathsetter 方法。setter 方法随后负责通知观察对象属性的改变状况。

Apple 使用了 isa-swizzling 来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A 的新类,该类继承自对象A的本类,且 KVO 为 NSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter方法之前和之后,通知所有观察对象属性值的更改情况。

我们通过例子来验证一下,首先我们检测一个类的

NSLog(@"kvo之前 self -> isa: %@",object_getClass(runtime));
NSLog(@"kvo之前 self class : %@",[runtime class]);

测试代码如下:

RuntimeTestManager *runtime = [[RuntimeTestManager alloc] init];
runtime.name = @"zhangsan";
NSLog(@"kvo之前 self -> isa: %@",object_getClass(runtime));
NSLog(@"kvo之前 self class : %@",[runtime class]);
    
[runtime addObserver:self forKeyPath:@"_name" options:NSKeyValueObservingOptionInitial context:nil];
_runtimeManager = runtime;
    
NSLog(@"kvo之后 self -> isa: %@",object_getClass(runtime));
NSLog(@"kvo之后 self class : %@",[runtime class]);

测试结果为:

2018-11-27 17:19:43.221175+0800 Runtime[7499:2757536] kvo之前 self -> isa: RuntimeTestManager
2018-11-27 17:19:43.221217+0800 Runtime[7499:2757536] kvo之前 self class : RuntimeTestManager
2018-11-27 17:19:43.221441+0800 Runtime[7499:2757536] kvo之后 self -> isa: NSKVONotifying_RuntimeTestManager
2018-11-27 17:19:43.221460+0800 Runtime[7499:2757536] kvo之后 self class : RuntimeTestManager

在这个过程,被观察对象的 isa 指针从指向原来的 RuntimeTestManager 类,被KVO 机制修改为指向系统新创建的子类NSKVONotifying_ RuntimeTestManager类,来实现当前类属性值改变的监听;

所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为NSKVONotifying_ RuntimeTestManager的类,就会发现系统运行到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_ RuntimeTestManager 的中间类,并指向这个中间类了。

子类setter方法剖析

KVO 的键值观察通知依赖于 NSObject 的两个法:willChangeValueForKey:didChangeValueForKey:,在存取数值的前后分别调用 2 个方法:被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该keyPath 的属性值已经变更;之后,observeValueForKey:ofObject:change:context:也会被调用。且重写观察属性的setter 方法这种继承方式的注入是在运行时而不是编译时实现的。KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:

- (void)setName:(NSString *)newName { 
      [self willChangeValueForKey:@"name"];    //KVO 在调用存取方法之前总调用 
      [super setValue:newName forKey:@"name"]; //调用父类的存取方法 
      [self didChangeValueForKey:@"name"];     //KVO 在调用存取方法之后总调用
}

2、类目添加新属性

在我们的日常开发中,分类可以为原有类扩展功能,复写原有类方法。但是分类不支持添加成员变量。尽管我们可以在分类中直接声明属性,但是由于不能生成成员变量,所以直接调用这些属性还会造成崩溃。为了实现分类添加属性,Runtime为我们添加了关联对象方法。它能够帮助我们在运行阶段将任意的属性关联到一个对象上。方法如下:

/**
 1.给对象设置关联属性
 @param object 需要设置关联属性的对象,即给哪个对象关联属性
 @param key 关联属性对应的key,可通过key获取这个属性,
 @param value 给关联属性设置的值
 @param policy 关联属性的存储策略(对应Property属性中的assign,copy,retain等)
 OBJC_ASSOCIATION_ASSIGN             @property(assign)。
 OBJC_ASSOCIATION_RETAIN_NONATOMIC   @property(strong, nonatomic)。
 OBJC_ASSOCIATION_COPY_NONATOMIC     @property(copy, nonatomic)。
 OBJC_ASSOCIATION_RETAIN             @property(strong,atomic)。
 OBJC_ASSOCIATION_COPY               @property(copy, atomic)。
 */
void objc_setAssociatedObject(id _Nonnull object,
                              const void * _Nonnull key,
                              id _Nullable value,
                              objc_AssociationPolicy policy)
/**
 2.通过key获取关联的属性
 @param object 从哪个对象中获取关联属性
 @param key 关联属性对应的key
 @return 返回关联属性的值
 */
id _Nullable objc_getAssociatedObject(id _Nonnull object,
                                      const void * _Nonnull key)
/**
 3.移除对象所关联的属性
 @param object 移除某个对象的所有关联属性
 */
void objc_removeAssociatedObjects(id _Nonnull object)

接下来我用一个例子说明:

#import 

@interface UIViewController (custome)

@property (nonatomic, copy) NSString *age;

@end

#import "UIViewController+custome.h"
#import 

@implementation UIViewController (custome)

- (void)setAge:(NSString *)age {
    ///添加成员变量
    objc_setAssociatedObject(self, @selector(setAge:), age, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)age {
    ///获取成员变量
    return objc_getAssociatedObject(self, @selector(setAge:));
}

@end

注意:key与关联属性一一对应,我们必须确保其全局唯一性,常用我们使用@selector(methodName)作为key。

3、获取类的详细信息

1.1 获取属性列表

unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i = 0; i

1.2 获取所有成员变量

Ivar *ivarList = class_copyIvarList([self class], &count);
for (int i= 0; i

1.3 获取所有方法

Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i = 0; i

1.4 获取当前遵循的所有协议

__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (int i=0; i

注意:C语言中使用Copy操作的方法,要注意释放指针,防止内存泄漏

4、解决同一方法高频率调用的效率问题

5、方法动态解析与消息转发

方法的动态解析和消息转发,详细内容请参考上篇文章。

5.1 动态方法解析:动态添加方法

Runtime足够强大,能够让我们在运行时动态添加一个未实现的方法,这个功能主要有两个应用场景:
场景1:动态添加未实现方法,解决代码中因为方法未找到而报错的问题;
场景2:利用懒加载思路,若一个类有很多个方法,同时加载到内存中会耗费资源,可以使用动态解析添加方法。方法动态解析主要用到的方法如下:

//OC方法:
//类方法未找到时调起,可于此添加类方法实现
+ (BOOL)resolveClassMethod:(SEL)sel

//实例方法未找到时调起,可于此添加实例方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel

//Runtime方法:
/**
 运行时方法:向指定类中添加特定方法实现的操作
 @param cls 被添加方法的类
 @param name selector方法名
 @param imp 指向实现方法的函数指针
 @param types imp函数实现的返回值与参数类型
 @return 添加方法是否成功
 */
BOOL class_addMethod(Class _Nullable cls,
                     SEL _Nonnull name,
                     IMP _Nonnull imp,
                     const char * _Nullable types)

5.2 解决方法无响应崩溃问题

执行OC方法其实就是一个发送消息的过程,若方法未实现,我们可以利用方法动态解析与消息转发来避免程序崩溃,这主要涉及下面一个处理未实现消息的过程:
消息转发流程图

其他相关方法如下:

///重定向类方法的消息接收者,返回一个类
- (id)forwardingTargetForSelector:(SEL)aSelector

///重定向实例方法的消息接受者,返回一个实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector

///消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;

- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector;

6、动态操作属性

6.1 动态修改属性变量

现在假设这样一个情况:我们使用第三方框架里的Person类,在特殊需求下想要更改其私有属性name,这样的操作我们就可以使用Runtime可以动态修改对象属性。

基本思路:首先使用Runtime获取Peson对象的所有属性,找到name,然后使用ivar的方法修改其值。具体的代码示例如下:

    Person *per = [[Person alloc] init];
    per.name = @"zhagnsan";
    NSLog(@"======== %@",per.name);
    
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList([per class], &count);
    
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivarList[i];
        const char *ivarName = ivar_getName(ivar);
        NSString *propertyName = [NSString stringWithUTF8String:ivarName];
        if ([propertyName isEqualToString:@"_name"]) {
            object_setIvar(per, ivar, @"李四");
        }
    }
    free(ivarList); //释放指针
    NSLog(@"------- %@",per.name);

执行结果为:

2018-11-29 11:17:06.050754+0800 Runtime[34451:175705] ======== zhagnsan
2018-11-29 11:17:06.050844+0800 Runtime[34451:175705] ------- 李四

6.2 实现 NSCoding 的自动归档和解档

归档是一种常用的轻量型文件存储方式,但是它有个弊端:在归档过程中,若一个Model有多个属性,我们不得不对每个属性进行处理,非常繁琐。归档操作主要涉及两个方法:encodeObjectdecodeObjectForKey,现在,我们可以利用Runtime来改进它们,关键的代码示例如下:

//原理:使用Runtime动态获取所有属性
//解档操作
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
    self = [super init];
    if (self) {
        unsigned int count = 0;
        
        Ivar *ivarList = class_copyIvarList([self class], &count);
        for (int i = 0; i < count; i++) {
            Ivar ivar = ivarList[i];
            const char *ivarName = ivar_getName(ivar);
            NSString *key = [NSString stringWithUTF8String:ivarName];
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        free(ivarList); //释放指针
    }
    return self;
}

//归档操作
- (void)encodeWithCoder:(NSCoder *)aCoder{
    unsigned int count = 0;
    
    Ivar *ivarList = class_copyIvarList([self class], &count);
    for (NSInteger i = 0; i < count; i++) {
        Ivar ivar = ivarList[i];
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        id value = [self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
    free(ivarList); //释放指针
}

测试如下:

//--测试归档
Person *per = [[Person alloc] init];
per.name = @"zhagnsan";
per.age  = 18;
NSString *temp = NSTemporaryDirectory();
NSString *fileTemp = [temp stringByAppendingString:@"person.archive"];
[NSKeyedArchiver archiveRootObject:ps toFile:fileTemp];

//--测试解档
NSString *temp = NSTemporaryDirectory();
NSString *fileTemp = [temp stringByAppendingString:@"person.henry"];
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:fileTemp];
NSLog(@"person-name:%@,person-age:%ld",person.name,person.age); 
//person-name:zhagnsan,person-age:18

6.3 实现字典与模型的转换

字典数据转模型的操作在项目开发中很常见,通常我们会选择第三方如YYModel;其实我们也可以自己来实现这一功能,主要的思路有两种:KVC、Runtime,总结字典转化模型过程中需要解决的问题如下:


字典转模型

现在,我们使用Runtime来实现字典转模型的操作,大致的思路是这样:
借助Runtime可以动态获取成员列表的特性,遍历模型中所有属性,然后以获取到的属性名为key,在JSON字典中寻找对应的值value;再将每一个对应Value赋值给模型,就完成了字典转模型的目的。

首先准备下面的JSON数据用于测试:

{
    "id":"2462079046",
    "name": "梧雨北辰",
    "age":"18",
    "weight":140,
    "address":{
            "country":"中国",
            "province": "河南"
            },
    "courses":[{
               "name":"Chinese",
               "desc":"语文课"
    },{
               "name":"Math",
               "desc":"数学课"
    },{
               "name":"English",
               "desc":"英语课"
    }
    ]
}

具体的代码实现流程如下:

步骤1:创建NSObject的类目NSObject+ZSModel,用于实现字典转模型
@interface NSObject (ZSModel)
+ (instancetype)zs_modelWithDictionary:(NSDictionary *)dictionary;
@end

//ZSModel协议,协议方法可以返回一个字典,表明特殊字段的处理规则
@protocol ZSModel
@optional
+ (nullable NSDictionary *)modelContainerPropertyGenericClass;
@end;
#import "NSObject+ZSModel.h"
#import 
@implementation NSObject (ZSModel)
+ (instancetype)zs_modelWithDictionary:(NSDictionary *)dictionary{
    
    //创建当前模型对象
    id object = [[self alloc] init];
    //1.获取当前对象的成员变量列表
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList([self class], &count);
    
    //2.遍历ivarList中所有成员变量,以其属性名为key,在字典中查找Value
    for (int i= 0; i @"name"
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        //3.2去除@符号
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
        
        //4.对特殊成员变量进行处理:
        //判断当前类是否实现了协议方法,获取协议方法中规定的特殊变量的处理方式
        NSDictionary *perpertyTypeDic;
        if([self respondsToSelector:@selector(modelContainerPropertyGenericClass)]){
            perpertyTypeDic = [self performSelector:@selector(modelContainerPropertyGenericClass) withObject:nil];
        }
        
        //4.1处理:字典的key与模型属性不匹配的问题,如id->uid
        id anotherName = perpertyTypeDic[propertyName];
        if(anotherName && [anotherName isKindOfClass:[NSString class]]){
            value =  dictionary[anotherName];
        }
        
        //4.2.处理:模型嵌套模型
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
            Class modelClass = NSClassFromString(ivarType);
            if (modelClass != nil) {
                //将被嵌套字典数据也转化成Model
                value = [modelClass zs_modelWithDictionary:value];
            }
        }
        
        //4.3处理:模型嵌套模型数组
        //判断当前Vaue是一个数组,而且存在协议方法返回了perpertyTypeDic
        if ([value isKindOfClass:[NSArray class]] && perpertyTypeDic) {
            Class itemModelClass = perpertyTypeDic[propertyName];
            //封装数组:将每一个子数据转化为Model
            NSMutableArray *itemArray = @[].mutableCopy;
            for (NSDictionary *itemDic  in value) {
                id model = [itemModelClass zs_modelWithDictionary:itemDic];
                [itemArray addObject:model];
            }
            value = itemArray;
        }
        
        //5.使用KVC方法将Vlue更新到object中
        if (value != nil) {
            [object setValue:value forKey:propertyName];
        }
    }
    free(ivarList); //释放C指针
    return object;
}
@end
步骤2:分别创建各个数据模型Student、Address、Course

Student类:

//Student.h文件
#import "NSObject+ZSModel.h"
#import "AddressModel.h"
#import "CourseModel.h"
@interface StudentModel : NSObject //遵循协议
//普通属性
@property (nonatomic, copy) NSString *uid;
@property(nonatomic,copy)NSString *name;
@property (nonatomic, assign) NSInteger age;
//嵌套模型
@property (nonatomic, strong) AddressModel *address;
//嵌套模型数组
@property (nonatomic, strong) NSArray *courses;
@end
#import "StudentModel.h"
@implementation StudentModel
+ (NSDictionary *)modelContainerPropertyGenericClass {
    //需要特别处理的属性
    return @{@"courses" : [CourseModel class],@"uid":@"id"};
}
@end

Address类:

//AddressModel.h文件
@interface AddressModel : NSObject
@property (nonatomic, copy) NSString *country;  //国籍
@property (nonatomic, copy) NSString *province; //省份
@property (nonatomic, copy) NSString *city;     //城市
@end

//-----------------优美的分割线------------------------
//AddressModel.m文件
#import "AddressModel.h"
@implementation AddressModel
@end

Course类:

//读取JSON数据
NSDictionary *jsonData = [FileTools getDictionaryFromJsonFile:@"Student"];
NSLog(@"%@",jsonData);

//字典转模型
StudentModel *student = [StudentModel zs_modelWithDictionary:jsonData];
CourseModel *courseModel = student.courses[0];
NSLog(@"%@",courseModel.name);

步骤4:测试字典转模型操作

//读取JSON数据
NSDictionary *jsonData = [FileTools getDictionaryFromJsonFile:@"Student"];
NSLog(@"%@",jsonData);

//字典转模型
StudentModel *student = [StudentModel zs_modelWithDictionary:jsonData];
CourseModel *courseModel = student.courses[0];
NSLog(@"%@",courseModel.name);

效果如下:


image.png

你可能感兴趣的:(Runtime 应用)