iOS Runtime 的应用场景

前言

上次简单介绍了 Runtime 的原理,和 Runtime 常用的操作。下面就来介绍一下常见的几种应用场景。

1. AOP 面向切面编程

来看一下 百度百科 上对「AOP」的解释:

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

画重点,对业务逻辑进行分离,降低耦合度。

假设现在有这样一个需求,我们要对应用中所有按钮的点击事件进行上报,统计每个按钮被点击的次数。

首先我们要明确,统计功能应该与业务无关,即统计代码不应该与业务代码耦合在一起。因此用上面「AOP」的思想来实现是合适的,而 Runtime 给我们提供了这样一条途径。因为当按钮点击时,会调用 sendAction:to:forEvent: 方法,所以我们可以使用 Method Swizzling 来修改该方法,在其中添加上报的逻辑。来看代码:

// UIButton+Swizzling.m
+ (void)load {

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        RSSwizzleInstanceMethod([self class],
                                @selector(sendAction:to:forEvent:),
                                RSSWReturnType(void),
                                RSSWArguments(SEL action, id target, UIEvent *event),
                                RSSWReplacement({

            NSString *name = NSStringFromClass([self class]);

            NSLog(@"UIButton+Swizzling:%@ 按钮被点击--上报", name);

            RSSWCallOriginal(action, target, event);

        }), RSSwizzleModeAlways, NULL);

    });
}

注意:尽管上面的需求也可以用继承一个基类的方式来实现,但是如果此时已经有很多类继承自 UIButton ,则修改起来会很麻烦,其次我们也不能保证后续的所有按钮都继承这个基类。另外上面提到,统计逻辑不应该和业务逻辑耦合,如果为了统计的需求去修改业务代码,也是不可取的(除非迫不得已)。因此上面利用 Method Swizzling 的方式更为合适,也更为简洁。

2. 字典转模型

我们可以用 KVC 来实现字典转模型,方法是调用 setValuesForKeysWithDictionary: 。但这种方法要求 Model 的属性和 NSDictionarykey 一一对应,否则就会报错。这里可以用 Runtime 配合 KVC ,来实现更灵活的字典转模型。

下面为 NSObject 添加一个分类,添加一个初始化方法,来看代码:

// NSObject+JSONExtension.h
- (instancetype)initWithDictionary:(NSDictionary *)dictionary {

    self = [self init];

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

尝试调用:

NSDictionary *info = @{@"title": @"标题", @"count": @(1), @"test": @"hello"};
ObjectA *objectA = [[ObjectA alloc] initWithDictionary:info];
NSLog(@"%@", objectA.title);     // 输出:标题
NSLog(@"%ld", (long)objectA.count);         // 输出:1

注意:在实际的应用中,会有更多复杂的情况需要考虑,比如字典中包含数组、对象等。这里只是做个简单示例。

3. 进行归解档

「归档」是将对象序列化存入沙盒文件的过程,会调用 encodeWithCoder: 来序列化。「解档」是将沙盒文件中的数据反序列化读入内存的过程,会调用 initWithCoder: 来反序列化。

通常来说,归解档需要对实例对象的各个属性依次进行归档和解档,十分繁琐且易出错。这里我们参照「字典转模型」的例子,通过获取类的所有属性,实现自动归解档。

触发对象归档可以调用 NSKeyedArchiver+ archiveRootObject:toFile: 方法;触发对象解档可以调用 NSKeyedUnarchiver+ unarchiveObjectWithFile: 方法。

注: xib 文件在载入的时候,也会触发 initWithCoder: 方法,可见读取 xib 文件也是一个解档的过程。

首先在 NSObject 的分类中添加两个方法:

// NSObject+JSONExtension.m
- (void)initAllPropertiesWithCoder:(NSCoder *)coder {

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

NSObject 的子类中实现归解档方法:

// ObjectA.m
- (id)initWithCoder:(NSCoder *)aDecoder{

    self = [super init];
    if (self) {
        [self initAllPropertiesWithCoder:aDecoder];
    }
    return self;
}

-(void)encodeWithCoder:(NSCoder *)aCoder{

    [self encodeAllPropertiesWithCoder:aCoder];
}

尝试调用:

NSDictionary *info = @{@"title": @"标题11", @"count": @(11)};
NSString *path = [NSString stringWithFormat:@"%@/objectA.plist", NSHomeDirectory()];

// 归档
ObjectA *objectA = [[ObjectA alloc] initWithDictionary:info];
[NSKeyedArchiver archiveRootObject:objectA toFile:path];

// 解档
ObjectA *objectB = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
NSLog(@"%@", objectB.title);      // 输出:标题11
NSLog(@"%ld", (long)objectB.count);       // 输出:11

注:上面的代码逻辑并不完善,只是做简单示例用。

4. 逆向开发

在「逆向开发」中,会用到一个叫 class-dump 的工具。这个工具可以将已脱壳的 APP 的所有类的头文件导出,为分析 APP 做准备。这里也是利用 Runtime 的特性,将存储在mach-O文件中的 @interface@protocol 信息提取出来,并生成对应的 .h 文件。

5. 热修复

「热修复」是一种不需要发布新版本,通过动态下发修复文件来修复 Bug 的方式。比如 JSPatch,就是利用 Runtime 强大的动态能力,对出问题的代码段进行替换。

源码

请到 GitHub 上查看完整例子。