一次方法适配

674 阅读3分钟

前言


回顾笔者的runtime系列文章,发现实践略少,恰好近来一位朋友入职新公司后进行codereview时遇到了一个问题,和他讨论后制定了一个使用runtime的方案来解决问题,正好记录下这个方案。

问题

在朋友的项目中存在一个异步获取沙盒文件的接口,伪实现如下:

#define BLOCK_SAFE_CALLS(_b_, _f_, _e_) if (_b_) { _b_(_f_, _e_); }

- (void)asyncFetchAllFoldersWithCompleteBlock: (void(^)(NSArray *, NSError *))complete {
    BEGIN_OPERATION_DISPATCHER
    XXXFetchFlodersOperation * operation = [self.XXXSession fetchAllFoldersOperation];
    [operation start: ^(NSError * error, NSArray * folders) {
        BLOCK_SAFE_CALLS(completeBlock, folders, error);
    }];
    END_OPERATION_DISPATCHER
}

由于未知原因,在运行期间,这个方法总在前至多3次调用时出现error,为了避免调用该方法时还需要在回调中实现重新尝试的代码,需要把重试代码的业务放到这个方法中。

方案1:不修改原接口的基础上添加递归调用

#define BLOCK_SAFE_CALLS(_b_, _f_, _e_) if (_b_) { _b_(_f_, _e_); }

- (void)asyncFetchAllFoldersWithCompleteBlock: (void(^)(NSArray *, NSError *))completeBlock {
    [self asyncFetchAllFoldersWithCompleteBlock: completeBlock retryTime: 3];
}

- (void)asyncFetchAllFoldersWithCompleteBlock: (void (^)(NSArray *, NSError *))completeBlock retryTime: (int)retryTime {
    BEGIN_OPERATION_DISPATCHER
    XXXFetchFlodersOperation * operation = [self.XXXSession fetchAllFoldersOperation];
    [operation start: ^(NSError * error, NSArray * folders) {
        if (error && retryTime > 0) {
            [self asyncFetchAllFoldersWithCompleteBlock: completeBlock retryTime: retryTime - 1];
        } else {
            BLOCK_SAFE_CALLS(completeBlock, folders, error);
        }
    }];
    END_OPERATION_DISPATCHER
}

借鉴于递归思想,提供一个额外的接口传入一个标记(代码中为retryTime)以此作为是否在调用发生错误后重新尝试。且上面的方案对现有代码的改动是最小的,几乎无侵害。(然而朋友说不允许修改原接口实现,因此方案作废)

方案2:提供额外的接口来完成操作

@interface XXXXX: NSObject

- (void)asyncFetchAllFoldersWithCompleteBlock: (void(^)(NSArray *, NSError *))completeBlock NS_DEPRECATED_IOS(2_0, 5_0);
- (void)asyncFetchAllFoldersWithCompleteBlock: (void (^)(NSArray *, NSError *))completeBlock retryTime: (int)retryTime;

@end

@implementation XXXXX

- (void)asyncFetchAllFoldersWithCompleteBlock: (void (^)(NSArray *, NSError *))completeBlock retryTime: (int)retryTime {
    NSParameterAssert(completeBlock);
    [self asyncFetchAllFoldersWithCompleteBlock: ^(NSArray * folders, NSError * error) {
        if (error && retryTime > 0) {
            NSLog(@"failed error: %@", error);
            [self asyncFetchAllFoldersWithCompleteBlock: completeBlock retryTime: retryTime - 1];
        } else {
            completeBlock(folders, error);
        }
    }];
}

@end

此方案通过宏定义NS_DEPRECATED_IOS标记原接口为摒弃方法,但是这样一来所有调用原接口的代码都要重新进行修改:

不谈工作量,朋友说他只有修改当前类实现文件的权力,其他外界代码不允许修改。因此,方案作废

方案3:method_swizzling

由于原接口代码以及接口调用不允许改动,留给我们选择的余地就不多了,恰好还有AOP的方式可以来解决这个问题。当然相比起其他两个方案代码数量要多得多,通过交换方法实现的方式将方法的调用实际上转到我们新增的接口中:

+ (void)load {
    aop_method_exchange([self class], @selector(AOPAsyncFetchAllFoldersWithCompleteBlock:), @selector(asyncFetchAllFoldersWithCompleteBlock:));
}

- (void)AOPAsyncFetchAllFoldersWithCompleteBlock: (void (^)(NSArray *, NSError *))completeBlock {
    NSParameterAssert(completeBlock);
    [self asyncFetchAllFoldersWithCompleteBlock: ^(NSArray * folders, NSError * error) {
        completeBlock(folders, error);
    } retryTime: 3];
}

- (void)asyncFetchAllFoldersWithCompleteBlock: (void (^)(NSArray *, NSError *))completeBlock retryTime: (int)retryTime {
    NSParameterAssert(completeBlock);
    [self AOPAsyncFetchAllFoldersWithCompleteBlock: ^(NSArray * folders, NSError * error) {
        if (error && retryTime > 0) {
            [self asyncFetchAllFoldersWithCompleteBlock: completeBlock retryTime: retryTime - 1];
        } else {
            completeBlock(folders, error);
        }
    }];
}

实际上方案3是结合了方案1与方案2的优点以及避开了两者的缺点,即使删除新增的代码,原有代码不会受到任何影响。缺点在于如果方法本身已经被hook过了,那么可能会出现意料之外的错误

尾言

离上次写博客过去也有一个多月了,期间经历了忙碌的春节,以及项目赶工,都没什么时间静下来写博客。最近笔者还报了自考本科,目标是当一个会画画的码农,从此就失去了周末的双休了。哎,心疼一下自己。最后放上新手的画画作业,高能预警!!!