侧边栏壁纸
博主头像
落叶人生博主等级

走进秋风,寻找秋天的落叶

  • 累计撰写 130555 篇文章
  • 累计创建 28 个标签
  • 累计收到 9 条评论
标签搜索

目 录CONTENT

文章目录

Objective-C Runtime:常用的几个应用场景

2022-06-30 星期四 / 0 评论 / 0 点赞 / 27 阅读 / 24622 字

对一个知识点的深入学习,有利于我们加深对代码的理解,更重要的是要学会使用它们,以提高日常开发的效率。下面是几个在平时开发中会使用到的场景:Method Swizzling给分类增加属性实现字典转模型M

对一个知识点的深入学习,有利于我们加深对代码的理解,更重要的是要学会使用它们,以提高日常开发的效率。下面是几个在平时开发中会使用到的场景:

  • Method Swizzling
  • 给分类增加属性
  • 实现字典转模型

Method Swizzling

Method Swizzling是一项很强大的改变现有方法实现的技术,比子类化替换方法更加灵活。再来看下Method的数据结构:

typedef struct objc_method *Method;struct objc_method {    SEL method_name;//方法名称    char *method_types;    IMP method_imp;//方法的具体实现}

从结构中可以看出方法的名称method_name与方法的实现method_imp是一一对应的,Method Swizzling的原理就是动态地改变它们的对应关系,以达到替换方法实现的目的。通常我们利用这项技术把系统的某个方法替换成自定义的方法,根据具体的需求添加一些功能。

下面通过一个示例,看看Method Swizzling到底有什么用?假设现在项目中需要进行页面统计,我们可以怎么做?

  1. 直接修改项目中的每一个ViewController
  2. 子类化ViewController,让项目中的ViewController都继承自这些子类。

第一种方式会产生大量重复代码,而且很容易遗漏一些界面,非常难维护;第二种方式比第一种方式好一点,但是需要子类化系统提供的所有类型的ViewController,假如后续开发的新界面忘记继承这些子类,一样会导致遗漏。Method Swizzling就是解决这类问题的最佳方式。

下面通过个人在实际开发中的一个使用场景,来看看Method Swizzling的具体实现。我们在维护一个项目的时候,为了解决特定界面上的bug首先要找到对应的ViewController,此时可以通过搜索界面上的关键字来做到快速定位目标界面,但是如果项目比较大又比较复杂该方法有时就不是很靠谱了,我的做法是利用Method Swizzling直接在控制台打印出当前界面的名称,代码如下:

@interface UIViewController (Log)@end
@implementation UIViewController (Log)+ (void)load {#ifdef DEBUG    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        Class cls = [self class];        SEL originalSel = @selector(viewWillAppear:);        SEL swizzledSel = @selector(rl_viewWillAppear:);        Method originalMethod = class_getInstanceMethod(cls, originalSel);        Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel);        BOOL success = class_addMethod(cls, originalSel, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));        if (success) {            class_replaceMethod(cls, swizzledSel, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));        } else {            method_exchangeImplementations(originalMethod, swizzledMethod);        }    });#endif}- (void)rl_viewWillAppear:(BOOL)animated {    NSString *clsName = NSStringFromClass([self class]);    NSLog(@"当前界面是:%@", clsName);        [self rl_viewWillAppear:animated];}@end

Method Swizzling的写法比较固定,就代码而言,有几点需要说明:

  1. #ifdef DEBUG这个条件编译不是必须的,这里是因为我的这个需求,我只需要开发阶段在控制台打印,发布阶段是不需要的。
  2. 通常,我们将代码放在+load方法中,runtime会自动调用+load方法,且分类中的+load方法不会覆盖主类中的+load方法。
  3. 我们通过dispatch_once来保证Method Swizzling的逻辑只会处理一次,因为runtime只会调用一次+load方法,但是你不能保证+load方法不会被人为触发,虽然一般没人会这么干。
  4. 我们使用Method Swizzling是为了给程序增加功能,而不是完全替换掉某个功能,所以我们需要在自定义的实现中调用原始的实现。代码中调用了class_addMethod方法,并且依据它的结果处理了两种情况:
    1. 一个类如果包含了特定方法名对应的实现,那么class_addMethod方法会返回NO,这种情况下只需要简单的通过method_exchangeImplementations交换两个方法的实现就可以了。
    2. 如果class_addMethod方法返回YES,说明了主类没有实现要被替换的方法,而是继承了父类的实现。这种情况下,一开始通过class_getInstanceMethod获取到的originalSel指向的就是父类的实现。由于add_classMethod方法现在已经成功地将originalSel指向了自定义方法的实现,所以接下来就是通过class_replaceMethod将自定义方法指向原先父类的实现,已达到交换实现的目的。

给分类增加属性

通常在主类中,如下这行代码,系统会自动生成成员变量_rlName,以及与之对应的getter/setter方法。

@property (nonatomic, copy) NSString *rlName;

但是如果在分类中,我们写上同样的代码,然后在程序中调用getter/setter方法,编译正常,而程序运行时会抛出unrecognized selector sent to instance ...的异常并crash,这是为什么呢?因为分类中没法添加成员变量,所以在分类中通过@property声明的属性,编译器既不会生成对应的成员变量,也不会生成getter/setter方法。我们来看看分类的结构:

typedef struct objc_category *Category;struct objc_category {    char *category_name;//分类名称    char *class_name;//类名    struct objc_method_list *instance_methods;//实例方法列表    struct objc_method_list *class_methods;//类方法列表    struct objc_protocol_list *protocols;//协议列表} 

从结构可以看出分类中没有成员变量列表,这也就解释了为什么说分类中不能添加成员变量。我们所说的给分类添加属性,其实是通过runtime来给对象关联一个对象,注意关联对象不是成员变量!

runtime提供了3个关联对象的操作函数:

/**  * 设置关联对象 *  * object   关联对象的源对象 * key      关联对象的key,将来通过这个key取出关联对象 * value    关联对象的值,可以通过传nil来清空一个key对应的关联对象 * policy   关联对象的策略 */void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);     /**  * 取出关联对象 */                         id objc_getAssociatedObject(id object, const void *key);/**  * 清空源对象的所有关联对象 */ void objc_removeAssociatedObjects(id object);

关联对象涉及到了5种策略:

OBJC_ASSOCIATION_ASSIGN            //对关联对象进行弱引用OBJC_ASSOCIATION_RETAIN_NONATOMIC  //对关联对象进行强引用(非原子的)OBJC_ASSOCIATION_COPY_NONATOMIC    //对关联对象进行copy引用(非原子的)OBJC_ASSOCIATION_RETAIN            //对关联对象进行强引用OBJC_ASSOCIATION_COPY              //对关联对象进行copy引用

所以我们实现给分类添加属性的原理就是:通过关联对象,手动实现getter/setter方法。以下是实现的示例代码:

@interface NSObject (Associate)@property (nonatomic, copy) NSString *rlName;@end
@implementation NSObject (Associate)- (void)setRlName:(NSString *)rlName {    objc_setAssociatedObject(self, @selector(rlName), rlName, OBJC_ASSOCIATION_COPY_NONATOMIC);}- (NSString *)rlName {    return objc_getAssociatedObject(self, _cmd);}

实现字典转模型

我们可以通过两种方式实现字典转模型:1.KVC2.runtime,其实本质上都是通过调用-setValue:forKey:方法,但是两种方式的思路是不一样的。KVC的方式是遍历需要转换的字典,然后赋值到对象中的同名成员变量,如果字典很大,就会有很多我们不需要的键值,自然效率就低了;runtime的方式则是遍历对象的成员列表,以成员变量名称为键到字典中取出对应的值赋给对象,这种方式只取出自己需要的数据,效率相对更高。这里着重介绍通过runtime方式实现字典转模型。我们自定义三个类RLCarRLBookRLPerson:

//.h@interface RLCar : NSObject@property (copy, nonatomic) NSString *brand;@property (assign, nonatomic) CGFloat price;@end//.m@implementation RLCar@end
//.h@interface RLBook : NSObject@property (copy, nonatomic) NSString *bookName;@property (assign, nonatomic) CGFloat bookPrice;@end//.m@implementation RLBook@end
//.h@class RLBook;typedef NS_ENUM(NSInteger, RLSex) {    RLSexUnknow = 0,    RLSexMale,    RLSexFemale};@interface RLPerson : NSObject@property (copy, nonatomic) NSString *name;@property (assign, nonatomic) RLSex sex;@property (assign, nonatomic) NSInteger age;@property (strong, nonatomic) RLBook *book;@property (strong, nonatomic) NSArray *cars;@end//.m@implementation RLPerson@end

我们自定义的三个类之间的关系,涉及到了字典转模型的核心内容:

  1. 对象包含普通类型的成员变量;
  2. 对象包含类类型的成员变量;
  3. 对象包含一个数组成员变量:数组中存放其他类类型的对象。

字典转模型,最关键的是通过runtime获取对象的成员列表,先通过下面的关键代码,来看看上面的RLPerson类成员变量的情况:

unsigned int count = 0;Ivar *ivarList = class_copyIvarList(self, &count);for (int i = 0; i < count; i ++) {    Ivar ivar = ivarList[i];    // 获取属性名称     NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];    // 获取属性类型    NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];    NSLog(@"%@ %@", propertyName, propertyType);}
_name @"NSString"_sex q_age q_book @"RLBook"_cars @"NSArray"

我们来对比分析一下打印结果:

  1. 获取到的属性名称是带下划线的(_),如果我们要利用属性名称作为key到字典中取值,首先得去掉下划线;
  2. _book属性对应的类型是RLBook,正如我们在RLPerson中声明的一样。其实这种情况下我们通过book从字典中取出来的值也是一个字典,结合属性类型,我们就能解析出RLBook了,有点递归的意思。这就是解决对象包含类类型的成员变量的思路;
  3. _cars属性对应的类型是NSArray,我们通过RLPerson可以知道,这里数组中存放的是RLCar类型的数据,但是runtime在这里是无法知道数组中存放的数据是什么类型的。我们要想办法告诉它,于是我们可以声明一个协议方法,遇到这种情况,通过实现这个协议方法,来让runtime知道数组中数据的类型。这是解决对象包含一个数组成员变量的思路。

下面展示完整的字典转模型的代码,为了让所有的继承NSObject的子类都可以有转模型的方法,我们给NSObject添加分类:

//.h@protocol RLModel <NSObject>@optional+ (NSDictionary *)rl_objectClassInArray;@end@interface NSObject (RLModel) <RLModel>+ (instancetype)rl_modelWithDictionary:(NSDictionary *)dataDic;@end//.m@implementation NSObject (RLModel)+ (instancetype)rl_modelWithDictionary:(NSDictionary *)dataDic {    id obj = [[self alloc] init];        // 通过runtime获取属性列表    unsigned int count = 0;    Ivar *ivarList = class_copyIvarList(self, &count);    for (int i = 0; i < count; i ++) {        Ivar ivar = ivarList[i];        // 获取属性名称  (带有下划线,如:_name)        NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];        // 获取属性类型        NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];        //NSLog(@"%@ %@", propertyName, propertyType);                NSString *key = [propertyName substringFromIndex:1];        id value = dataDic[key];                // 二级转换,值为字典并且属性类型不是字典则需要字典转模型        if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"]) {            // 根据属性类型创建对象 @"RLBook"            NSRange range = [propertyType rangeOfString:@"/""];            propertyType = [propertyType substringFromIndex: range.location + range.length];            range = [propertyType rangeOfString:@"/""];            propertyType = [propertyType substringToIndex:range.location];                        Class modelClass = NSClassFromString(propertyType);            if (modelClass) {                value = [modelClass rl_modelWithDictionary:value];            }        }                // 三级转换,值为数组        if ([value isKindOfClass:[NSArray class]]) {            // 判断类有没有实现rl_ObjectClassInArray方法            if ([self respondsToSelector:@selector(rl_objectClassInArray)]) {                NSString *clazzStr = [self rl_objectClassInArray][key];                NSMutableArray *temp = [NSMutableArray array];                for (NSDictionary *dic in value) {                    Class clazz = NSClassFromString(clazzStr);                    if (clazz) {                        [temp addObject:[clazz rl_modelWithDictionary:dic]];                    }                }                value = temp;            }        }                if (value) {            [obj setValue:value forKey:key];        }    }        return obj;}@end

当然在RLPerson.m中我们需要实现+rl_objectClassInArray方法:

+ (NSDictionary *)rl_objectClassInArray {    return @{@"cars" : @"RLCar"};}

参考

.
.

广告 广告

评论区