前言
回顧筆者的
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
過了,那么可能會出現意料之外的錯誤
尾言
離上次寫博客過去也有一個多月了,期間經歷了忙碌的春節,以及項目趕工,都沒什么時間靜下來寫博客。最近筆者還報了自考本科,目標是當一個會畫畫的碼農,從此就失去了周末的雙休了。哎,心疼一下自己。最后放上新手的畫畫作業,高能預警!!!