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

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

  • 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"};
}
复制代码

参考

转载于:https://juejin.im/post/5d3ff9d8f265da03e921a4fb

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值