iOS之Runtime(二)

一:@@@《基础篇》@@@

二:@@@《应用篇》@@@

本篇将会结合Rutime其动态特性,总结Rutime的具体应用实例。Runtime在开发中的应用大致分为以下几个方面:


image.png

一、动态方法交换: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. 动态方法交换示例

代码示例:在视图控制中,定义两个实例方法 playGFA 与 playGFB,然后执行交换

// 导入运行时文件
#import <objc/runtime.h>

//  调用方法
- (void)viewDidLoad {
    [super viewDidLoad];
    [self startExchangeFunAction];
}

-(void)startExchangeFunAction{
    
    // 说明: 获取类方法 class_getClassMethod
    //   1. 获取实例方法A B
    Method funA=class_getInstanceMethod([self class], @selector(playGFA));
    Method funB=class_getInstanceMethod([self class], @selector(playGFB));
    
    // 2. 交换方法A和B
    method_exchangeImplementations(funA, funB);
    
    // 3. 调用两个方法
    [self playGFA];
    [self playGFB];
}
-(void)playGFA{
    NSLog(@"快到碗里来!");
}
-(void)playGFB{
    NSLog(@"来了,老弟(❤)");
}

最终运行结果:

TestModel[90052:13941169] 来了,老弟(❤)
TestModel[90052:13941169] 快到碗里来!

2. 拦截并替换系统方法

拦截并替换系统方法的示例:实现不同机型上的字体都按照比例适配,我们可以拦截系统 UIFont 的 systemFontOfSize: 方法,具体操作如下:

在当前工程中添加UIFont的分类:UIFont +CustomFont ,并在其中添用以替换的方法。

// UIFont+CustomFont.h 头文件
#import <UIKit/UIKit.h>
@interface UIFont (CustomFont)
@end


// UIFont+CustomFont.m 头文件
#import "UIFont+CustomFont.h"
#import "objc/runtime.h"   // 导入运行时文件

@implementation UIFont (CustomFont)

+(void)load{
    Method systemFontMethod = class_getClassMethod([UIFont class], @selector(systemFontOfSize:));
    Method replaceFontMethod = class_getClassMethod([UIFont class], @selector(replaceSystemFontFun:));
    method_exchangeImplementations(systemFontMethod, replaceFontMethod);
}

+(UIFont *)replaceSystemFontFun:(CGFloat)fontSize{
    
    CGFloat screenWValue=[UIScreen mainScreen].bounds.size.width;
    CGFloat ratioValue=screenWValue/375.0;
    
    return [UIFont replaceSystemFontFun:fontSize*ratioValue];
}

@end
  
  
  
运用一下:

切换不同的模拟器,观察在不同机型上文字的大小:
  
 - (void)viewDidLoad {
    [super viewDidLoad];
   
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 40)];
    
    label.text = @"一句话,一辈子";
    label.font = [UIFont systemFontOfSize:20];
    label.textColor=[UIColor whiteColor];
    label.backgroundColor=[UIColor darkGrayColor];
    [self.view addSubview:label];
 
}

二、实现分类添加新属性

给分类添加属性,我们还需借助Runtime的关联对象(Associated Objects)特性,它能够帮助我们在运行阶段将任意的属性关联到一个对象上,下面是相关的三个方法:

/**
 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)

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

一个代码示例:为 UIImage 增加一个分类:UIImage+AddUrl,并为其设置关联属性urlString(图片网络链接属性),相关代码如下:

// #import "UIImage+AddUrl.h" // 头文件中

#import <UIKit/UIKit.h>
@interface UIImage (AddUrl)

// 添加的属性urlString
@property (nonatomic,copy)NSString *urlString;

// 移除关联的属性操作
-(void)removeAssociatedObjectAction;

@end
 
  
 
// #import "UIImage+AddUrl.m" // 实现文件中
#import "UIImage+AddUrl.h"
#import <objc/runtime.h>
  
@implementation UIImage (AddUrl)

// Setting方法
-(void)setUrlString:(NSString *)urlString{
    // 设置关联的属性
    objc_setAssociatedObject(self, @selector(urlString), urlString, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

// Getting方法
-(NSString *)urlString{
    // 获取关联的属性
    return objc_getAssociatedObject(self, @selector(urlString));
}

// 移除关联的属性操作
-(void)removeAssociatedObjectAction{
    objc_removeAssociatedObjects(self);
}

@end
  
  
  
在控制器中导入 #import "UIImage+AddUrl.h"

- (void)viewDidLoad {
    [super viewDidLoad];
    [self addCategoryPropertyAction];
}

// 使用分类中的属性
-(void)addCategoryPropertyAction{

    UIImage *imageObj=[[UIImage alloc]init];
    imageObj.urlString=@"https://www.baidu.com";
    NSLog(@"图片的地址是: %@",imageObj.urlString);
  
    [imageObj removeAssociatedObjectAction];  // 移除关联属性操作
    NSLog(@"移除关联后的属性值:%@",imageObj.urlString);
}
  
最终运行结果:


TestModel[95333:14775645] 图片的地址是: https://www.baidu.com
TestModel[95333:14775645] 移除关联后的属性值:(null)

三、获取类的详细信息

1. 获取属性列表( class_copyPropertyList )

unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i = 0; i<count; i++) {
    const char *propertyName = property_getName(propertyList[i]);
    NSLog(@"下标是:(%d): 属性名:%@",i,[NSString stringWithUTF8String:propertyName]);
}
free(propertyList);

2. 获取所有成员变量( class_copyIvarList )

unsigned int count;
Ivar *ivarList = class_copyIvarList([self class], &count);
for (int i= 0; i<count; i++) {
    Ivar ivar = ivarList[i];
    const char *ivarName = ivar_getName(ivar);
    NSLog(@"下标是:(%d): 成员变量名:%@", i, [NSString stringWithUTF8String:ivarName]);
}
free(ivarList);

3.获取所有方法( class_copyMethodList )

unsigned int count;
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i = 0; i<count; i++) {
    Method method = methodList[i];
    SEL mthodName = method_getName(method);
    NSLog(@"下标是:(%d) 方法名是:%@",i,NSStringFromSelector(mthodName));
}
free(methodList);

4.获取当前遵循的所有协议( class_copyProtocolList )

unsigned int count;
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (int i=0; i<count; i++) {
    Protocol *protocal = protocolList[i];
    const char *protocolName = protocol_getName(protocal);
    NSLog(@"protocol(%d): %@",i, [NSString stringWithUTF8String:protocolName]);
}
free(propertyList);

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

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

Runtime 源码中的 IMP作为函数指针指向方法的实现。通过它,我们可以绕开发送消息的过程来提高函数调用的效率。当我们需要持续大量重复调用某个方法的时候,会十分有用,具体代码示例如下:

void (*setter)(id, SEL, BOOL);
int i;

setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ ){
   setter(targetList[i], @selector(setFilled:), YES);
}
    

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

该部分可以参考基础篇中内容,这里不再重复赘述,只是大概做出一些总结。

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)

六、动态操作属性

1.动态修改属性变量

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

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

Person *ps = [[Person alloc] init];
NSLog(@"昵称是: %@",[ps valueForKey:@"nickName"]); //null

//第一步:遍历对象的所有属性
unsigned int count;
Ivar *ivarList = class_copyIvarList([ps 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:@"_nickName"]) {
      
        //第三步:匹配到对应的属性,然后修改;注意属性带有下划线
        object_setIvar(ps, ivar, @"降龙十八掌");
    }
}
NSLog(@"昵称是: %@",[ps valueForKey:@"nickName"]); 

总结:此过程类似 KVC 的取值和赋值

2.实现 NSCoding 的自动归档和解档

归档是一种常用的轻量型文件存储方式,但是它有个弊端:在归档过程中,若一个Model有多个属性,我们不得不对每个属性进行处理,非常繁琐。

归档操作主要涉及两个方法:encodeObject 和 decodeObjectForKey,现在,我们可以利用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 *ps = [[Person alloc] init];
ps.name = @"乔峰";
ps.age  = 23;
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(@"名字:%@,年龄:%ld",person.name,person.age); 

3. 实现字典与模型的转换

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

image.png

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

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

{
    "id":"2462079046",
    "name": "乔帮主",
    "age":"23",
    "weight":140,
    "address":{
               "country":"中国",
               "province": "河南"
              },
    "courses":[{
               "name":"Chinese",
               "desc":"语文课"},
               {
               "name":"Math",
               "desc":"数学课"},
               {
               "name":"English",
               "desc":"英语课"}
              ]
}

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

步骤1:创建NSObject的分类 NSObject+LengModel ,用于实现字典转模型

头文件( NSObject+LengModel.h )如下

#import <Foundation/Foundation.h>

@interface NSObject (LengModel)

// 提供字典转模型的类方法
+(instancetype _Nullable )leng_modelWithDictionary:(NSDictionary *)dictionary;

@end

// 模型协议,协议方法可以返回一个字典,表明特殊字段的处理规则
@protocol LengModelProtocol<NSObject>

@optional

+ (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;

@end

实现文件( NSObject+LengModel.m )如下

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

@implementation NSObject (LengModel)

+(instancetype _Nullable )leng_modelWithDictionary:(NSDictionary *)dictionary{
    
    // 1.创建当前模型对象
    id object = [[self alloc]init];
    
    // 2.获取模型对象的成员变量列表
    unsigned int count = 0 ;
    Ivar *ivarList = class_copyIvarList([self class], &count);
    
    // 3.遍历ivarList中所有成员变量,以其属性名为key,在字典中查找Value
    for (int i= 0; i<count; i++) {
        
        // 3.1获取成员属性
        Ivar ivar = ivarList[i];
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 3.2截取成员变量名:去掉成员变量前面的"_"号
        NSString *propertyName = [ivarName substringFromIndex:1];
        
        // 3.3以属性名为key,在字典中查找value
        id value = dictionary[propertyName];
        
        // 4.获取成员变量类型, 因为ivar_getTypeEncoding获取的类型是"@\"NSString\""的形式
        // 所以我们要做以下的替换
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];// 替换:
        // 4.1去除转义字符:@\"name\" -> @"name"
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        // 4.2去除@符号
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
        
        // 5.对特殊成员变量进行处理:
        // 判断当前类是否实现了协议方法,获取协议方法中规定的特殊变量的处理方式
        NSDictionary *perpertyTypeDic;
        if([self respondsToSelector:@selector(modelContainerPropertyGenericClass)]){
            perpertyTypeDic = [self performSelector:@selector(modelContainerPropertyGenericClass) withObject:nil];
        }
        // 5.1处理:字典的key与模型属性不匹配的问题,如id->uid
        id anotherName = perpertyTypeDic[propertyName];
        if(anotherName && [anotherName isKindOfClass:[NSString class]]){
            value =  dictionary[anotherName];
        }
        // 5.2.处理:模型嵌套模型
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
            Class modelClass = NSClassFromString(ivarType);
            if (modelClass != nil) {
                //将被嵌套字典数据也转化成Model
                value = [modelClass leng_modelWithDictionary:value];
            }
        }
        // 5.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 leng_modelWithDictionary:itemDic];
                [itemArray addObject:model];
            }
            value = itemArray;
        }
        // 6.使用KVC方法将Vlue更新到object中
        if (value != nil) {
            [object setValue:value forKey:propertyName];
        }
    }
    
    free(ivarList); // 释放C指针
    
    return object;
}

@end

步骤2:分别创建各个数据模型Student、AddressObj 和 ProjectObj

在 Student.h 头文件如下

#import <Foundation/Foundation.h>
#import "NSObject+LengModel.h"

@class AddressObj,ProjectObj;

@interface Student : NSObject<LengModelProtocol> // 遵守协议

@property (nonatomic,copy)NSString *IDValue;

@property (nonatomic,copy)NSString *name;

@property (nonatomic,copy)NSString *age;

@property (nonatomic,assign)NSInteger weight;

@property (nonatomic,strong)AddressObj *address;

@property (nonatomic,strong)NSArray<ProjectObj *> *courses;

@end

@interface AddressObj : NSObject

@property (nonatomic,copy)NSString *country;

@property (nonatomic,copy)NSString *province;

@end

@interface ProjectObj : NSObject

@property (nonatomic,copy)NSString *name;

@property (nonatomic,copy)NSString *desc;

@end

在 Student.m 头文件如下

#import "Student.h"

@implementation Student

+(nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass{
    // 需要特别处理的属性
    return @{@"courses" : [ProjectObj class],@"IDValue":@"id"};
}

@end

@implementation AddressObj

@end

@implementation ProjectObj

@end
步骤3:测试字典转模型操作
#import "Student.h"

// JSON转为模型
-(void)useJsonToModelAction{
    
    // 加载本地JSON数据
    NSString *path=[[NSBundle mainBundle]pathForResource:@"Jiao" ofType:@"json"];
    NSData *data=[[NSData alloc] initWithContentsOfFile:path];
    NSDictionary *jsonData=[NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
    
    // 字典转模型
    Student *studentObj=[Student leng_modelWithDictionary:jsonData];
    
}

最终转为的模型数据如下

image.png

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容