1. 程式人生 > >一次方法適配實踐

一次方法適配實踐

前言

回顧筆者的runtime系列文章,發現實踐略少,恰好近來一位朋友入職新公司後進行codereview時遇到了一個問題,和他討論後製定了一個使用runtime的方案來解決問題,正好記錄下這個方案。

問題

在朋友的專案中存在一個非同步獲取沙盒檔案的介面,偽實現如下:

12345678910 #define BLOCK_SAFE_CALLS(_b_, _f_, _e_) if (_b_) { _b_(_f_, _e_); }-(void)asyncFetchAllFoldersWithCompleteBlock:(void(^)(NSArray *,NSError *))complete{BEGIN_OPERATION_DISPATCHERXXXFetchFlodersOperation *operation=[self.XXXSession fetchAllFoldersOperation
];[operation start:^(NSError *error,NSArray *folders){BLOCK_SAFE_CALLS(completeBlock,folders,error);}];END_OPERATION_DISPATCHER}

由於未知原因,在執行期間,這個方法總在前至多3次呼叫時出現error,為了避免呼叫該方法時還需要在回撥中實現重新嘗試的程式碼,需要把重試程式碼的業務放到這個方法中。

方案1:不修改原介面的基礎上新增遞迴呼叫

123456789101112131415161718 #define BLOCK_SAFE_CALLS(_b_, _f_, _e_) if (_b_) { _b_(_f_, _e_); }-(void)asyncFetchAllFoldersWithCompleteBlock:(void(^)(NSArray *,NSError *))completeBlock{[selfasyncFetchAllFoldersWithCompleteBlock:completeBlock retryTime:3];}-(void)asyncFetchAllFoldersWithCompleteBlock:(void(^)(NSArray *,NSError *))completeBlock retryTime:(int)retryTime{BEGIN_OPERATION_DISPATCHERXXXFetchFlodersOperation *operation=[self.XXXSession fetchAllFoldersOperation];[operation start:^(NSError *error,NSArray *folders){if(error&&retryTime>0){[selfasyncFetchAllFoldersWithCompleteBlock:completeBlock retryTime:retryTime-1];}else{BLOCK_SAFE_CALLS(completeBlock,folders,error);}}];END_OPERATION_DISPATCHER}

借鑑於遞迴思想,提供一個額外的介面傳入一個標記(程式碼中為retryTime)以此作為是否在呼叫發生錯誤後重新嘗試。且上面的方案對現有程式碼的改動是最小的,幾乎無侵害。(然而朋友說不允許修改原介面實現,因此方案作廢)

方案2:提供額外的介面來完成操作

12345678910111213141516171819202122 @interfaceXXXXX: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);[selfasyncFetchAllFoldersWithCompleteBlock:^(NSArray *folders,NSError *error){if(error&&retryTime>0){NSLog(@"failed error: %@",error);[selfasyncFetchAllFoldersWithCompleteBlock:completeBlock retryTime:retryTime-1];}else{completeBlock(folders,error);}}];}@end

此方案通過巨集定義NS_DEPRECATED_IOS標記原介面為摒棄方法,但是這樣一來所有呼叫原介面的程式碼都要重新進行修改:

不談工作量,朋友說他只有修改當前類實現檔案的權力,其他外界程式碼不允許修改。因此,方案作廢

方案3:method_swizzling

由於原介面程式碼以及介面呼叫不允許改動,留給我們選擇的餘地就不多了,恰好還有AOP的方式可以來解決這個問題。當然相比起其他兩個方案程式碼數量要多得多,通過交換方法實現的方式將方法的呼叫實際上轉到我們新增的介面中:

123456789101112131415161718192021 +(void)load{aop_method_exchange([selfclass],@selector(AOPAsyncFetchAllFoldersWithCompleteBlock:),@selector(asyncFetchAllFoldersWithCompleteBlock:));}-(void)AOPAsyncFetchAllFoldersWithCompleteBlock:(void(^)(NSArray *,NSError *))completeBlock{NSParameterAssert(completeBlock);[selfasyncFetchAllFoldersWithCompleteBlock:^(NSArray *folders,NSError *error){completeBlock(folders,error);}retryTime:3];}-(void)asyncFetchAllFoldersWithCompleteBlock:(void(^)(NSArray *,NSError *))completeBlock retryTime:(int)retryTime{NSParameterAssert(completeBlock);[selfAOPAsyncFetchAllFoldersWithCompleteBlock:^(NSArray *folders,NSError *error){if(error&&retryTime>0){[selfasyncFetchAllFoldersWithCompleteBlock:completeBlock retryTime:retryTime-1];}else{completeBlock(folders,error);}}];}

實際上方案3是結合了方案1與方案2的優點以及避開了兩者的缺點,即使刪除新增的程式碼,原有程式碼不會受到任何影響。缺點在於如果方法本身已經被hook過了,那麼可能會出現意料之外的錯誤

尾言

離上次寫部落格過去也有一個多月了,期間經歷了忙碌的春節,以及專案趕工,都沒什麼時間靜下來寫部落格。最近筆者還報了自考本科,目標是當一個會畫畫的碼農,從此就失去了週末的雙休了。哎,心疼一下自己。