面向切面程式設計參考:React Native面向切面程式設計
iOS中的實現方式:
ObjC
中實現 AOP
最直接的方法就是使用 Runtime
中的 Method Swizzling
。使用Aspects
, 可以不需要繁瑣的手工呼叫 Method Swizzling
。
iOS中的應用場景一:資料統計
所謂 AOP 其實就是給你的程式提供一個可拆卸的元件化能力。比如你的 APP 需要用到事件統計功能, 無論你是用 UMeng, Google Analytics, 還是其他的統計平臺等等, 你應該都會寫過類似的程式碼:
- (void)viewDidLoad {
[super viewDidLoad];
[Logger log:@"View Did Load"];
// 下面初始化資料
}
複製程式碼
在檢視控制器開始載入的時候,用 Logger 類記錄一個統計事件。 其實 viewDidLoad 方法本身的邏輯並不是為了完成統計,而是進行一些初始化操作。這就導致了一個設計上的瑕疵, 資料統計的程式碼和我們實際的業務邏輯程式碼混雜在一起了。隨著業務邏輯程式碼不斷增多,類似的混雜也會越來越多,這樣的耦合勢必會增加維護的成本。AOP 其實就是在不影響程式整體功能的情況下,將 Logger 這樣的邏輯,從主業務邏輯中抽離出來的能力。有了 AOP 之後, 我們的業務邏輯程式碼就變成了這樣:
- (void)viewDidLoad {
[super viewDidLoad];
// 下面初始化資料
}
複製程式碼
這裡不再會出現 Logger 的統計邏輯的程式碼,但是統計功能依然是生效的。 當然,不出現在主業務程式碼中,不代表統計程式碼就消失了。 而是用 AOP 模式 hook 到別的地方去了。
優點:
- 1、業務隔離 ,解耦。剝離開主業務和統計業務。
- 2、即插即用。在預釋出環境和釋出環境測試的時候,不想記錄統計資料,只需要把統計業務邏輯模組去掉即可。
- 3、如果你在哪一天想換一個統計平臺, 那麼你不需要到處改程式碼了, 只需要把統計層面的程式碼修改一下就可以。
缺點:
- 1、程式碼不夠直觀
- 2、使用不當,出現Bug比較難於除錯
iOS中的應用場景二:防止按鈕連續點選
網上有一篇文章iOS---防止UIButton重複點選的三種實現方式,經過實踐發現文章可以作為一個demo來演示,在真實的專案開發中是不實用的。因為sendAction:to:forEvent:
方法是UIControl的方法,所有繼承自UIControl的類的這個方法都會被替換,比如UISwitch。下面是針對這篇文章的改進版,確保只有UIButton的改方法被HOOK:
#import <UIKit/UIKit.h>
@interface UIButton (FixMultiClick)
@property (nonatomic, assign) NSTimeInterval clickInterval;
@end
#import "UIButton+FixMultiClick.h"
#import <objc/runtime.h>
#import <Aspects/Aspects.h>
@interface UIButton ()
@property (nonatomic, assign) NSTimeInterval clickTime;
@end
@implementation UIButton (FixMultiClick)
-(NSTimeInterval)clickTime {
return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
-(void)setClickTime:(NSTimeInterval)clickTime {
objc_setAssociatedObject(self, @selector(clickTime), @(clickTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSTimeInterval)clickInterval {
return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
-(void)setClickInterval:(NSTimeInterval)clickInterval {
objc_setAssociatedObject(self, @selector(clickInterval), @(clickInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
+(void)load {
[UIButton aspect_hookSelector:@selector(sendAction:to:forEvent:)
withOptions:AspectPositionInstead
usingBlock:^(id<AspectInfo> info){
UIButton *obj = info.instance;
if(obj.clickInterval <= 0){
[info.originalInvocation invoke];
}
else{
if ([NSDate date].timeIntervalSince1970 - obj.clickTime < obj.clickInterval) {
return;
}
obj.clickTime = [NSDate date].timeIntervalSince1970;
[info.originalInvocation invoke];
}
} error:nil];
}
@end
複製程式碼
iOS中的應用場景三:NSArray的陣列越界
crash的具體幾種情況
- 取值:index超出array的索引範圍
- 新增:插入的object為nil或者Null
- 插入:index大於count、插入的object為nil或者Null
- 刪除:index超出array的索引範圍
- 替換:index超出array的索引範圍、替換的object為nil或者Null
解決思路: HOOK
系統方法,替換為自定義的安全方法
#import <Foundation/Foundation.h>
@interface NSArray (Aspect)
@end
#import "NSArray+Aspect.h"
#import <objc/runtime.h>
@implementation NSArray (Aspect)
/**
* 對系統方法進行替換
*
* @param systemSelector 被替換的方法
* @param swizzledSelector 實際使用的方法
* @param error 替換過程中出現的錯誤訊息
*
* @return 是否替換成功
*/
+ (BOOL)systemSelector:(SEL)systemSelector customSelector:(SEL)swizzledSelector error:(NSError *)error{
Method systemMethod = class_getInstanceMethod(self, systemSelector);
if (!systemMethod) {
return NO;
}
Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
if (!swizzledMethod) {
return NO;
}
if (class_addMethod([self class], systemSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
class_replaceMethod([self class], swizzledSelector, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
}
else{
method_exchangeImplementations(systemMethod, swizzledMethod);
}
return YES;
}
/**
NSArray 是一個類簇
*/
+(void)load{
[super load];
// 越界:初始化的空陣列
[objc_getClass("__NSArray0") systemSelector:@selector(objectAtIndex:)
customSelector:@selector(emptyObjectIndex:)
error:nil];
// 越界:初始化的非空不可變陣列
[objc_getClass("__NSSingleObjectArrayI") systemSelector:@selector(objectAtIndex:)
customSelector:@selector(singleObjectIndex:)
error:nil];
// 越界:初始化的非空不可變陣列
[objc_getClass("__NSArrayI") systemSelector:@selector(objectAtIndex:)
customSelector:@selector(safe_arrObjectIndex:)
error:nil];
// 越界:初始化的可變陣列
[objc_getClass("__NSArrayM") systemSelector:@selector(objectAtIndex:)
customSelector:@selector(safeObjectIndex:)
error:nil];
// 越界:未初始化的可變陣列和未初始化不可變陣列
[objc_getClass("__NSPlaceholderArray") systemSelector:@selector(objectAtIndex:)
customSelector:@selector(uninitIIndex:)
error:nil];
// 越界:可變陣列
[objc_getClass("__NSArrayM") systemSelector:@selector(objectAtIndexedSubscript:)
customSelector:@selector(mutableArray_safe_objectAtIndexedSubscript:)
error:nil];
// 越界vs插入:可變數插入nil,或者插入的位置越界
[objc_getClass("__NSArrayM") systemSelector:@selector(insertObject:atIndex:)
customSelector:@selector(safeInsertObject:atIndex:)
error:nil];
// 插入:可變數插入nil
[objc_getClass("__NSArrayM") systemSelector:@selector(addObject:)
customSelector:@selector(safeAddObject:)
error:nil];
}
- (id)safe_arrObjectIndex:(NSInteger)index{
if (index >= self.count || index < 0) {
NSLog(@"this is crash, [__NSArrayI] check index (objectAtIndex:)") ;
return nil;
}
return [self safe_arrObjectIndex:index];
}
- (id)mutableArray_safe_objectAtIndexedSubscript:(NSInteger)index{
if (index >= self.count || index < 0) {
NSLog(@"this is crash, [__NSArrayM] check index (objectAtIndexedSubscript:)") ;
return nil;
}
return [self mutableArray_safe_objectAtIndexedSubscript:index];
}
- (id)singleObjectIndex:(NSUInteger)idx{
if (idx >= self.count) {
NSLog(@"this is crash, [__NSSingleObjectArrayI] check index (objectAtIndex:)") ;
return nil;
}
return [self singleObjectIndex:idx];
}
- (id)uninitIIndex:(NSUInteger)idx{
if ([self isKindOfClass:objc_getClass("__NSPlaceholderArray")]) {
NSLog(@"this is crash, [__NSPlaceholderArray] check index (objectAtIndex:)") ;
return nil;
}
return [self uninitIIndex:idx];
}
- (id)safeObjectIndex:(NSInteger)index{
if (index >= self.count || index < 0) {
NSLog(@"this is crash, [__NSArrayM] check index (objectAtIndex:)") ;
return nil;
}
return [self safeObjectIndex:index];
}
- (void)safeInsertObject:(id)object atIndex:(NSUInteger)index{
if (index>self.count) {
NSLog(@"this is crash, [__NSArrayM] check index (insertObject:atIndex:)") ;
return ;
}
if (object == nil) {
NSLog(@"this is crash, [__NSArrayM] check object == nil (insertObject:atIndex:)") ;
return ;
}
[self safeInsertObject:object atIndex:index];
}
- (void)safeAddObject:(id)object {
if (object == nil) {
NSLog(@"this is crash, [__NSArrayM] check index (addObject:)") ;
return ;
}
[self safeAddObject:object];
}
- (id)emptyObjectIndex:(NSInteger)index {
NSLog(@"this is crash, [__NSArray0] check index (objectAtIndex:)") ;
return nil;
}
@end
複製程式碼
驗證
- (void)viewDidLoad {
[super viewDidLoad];
NSArray *arr1 = @[@"1",@"2"];
NSLog(@"[arr1 objectAtIndex:9527] = %@", [arr1 objectAtIndex:9527]);
NSLog(@"[arr1 objectAtIndexedSubscript:9527] = %@", [arr1 objectAtIndexedSubscript:9527]);
NSArray *arr2 = [[NSArray alloc]init];
NSLog(@"[arr2 objectAtIndex:9527] = %@", [arr1 objectAtIndex:9527]);
NSLog(@"[arr2 objectAtIndexedSubscript:9527] = %@", [arr1 objectAtIndexedSubscript:9527]);
NSArray *arr3 = [[NSArray alloc] initWithObjects:@"1",nil];
NSLog(@"[arr3 objectAtIndex:9527] = %@", [arr1 objectAtIndex:9527]);
NSLog(@"[arr3 objectAtIndexedSubscript:2] = %@", [arr3 objectAtIndexedSubscript:2]);
NSArray *arr4 = [NSArray alloc];
NSLog(@"[arr4 objectAtIndex:9527] = %@", [arr4 objectAtIndex:9527]);
NSLog(@"[arr4 objectAtIndexedSubscript:9527] = %@", [arr4 objectAtIndexedSubscript:9527]);
NSMutableArray *arr5 = [NSMutableArray array];
NSLog(@"[arr5 objectAtIndex:9527] = %@", [arr4 objectAtIndex:9527]);
NSLog(@"[arr5 objectAtIndexedSubscript:2] = %@", [arr5 objectAtIndexedSubscript:2]);
NSMutableArray *arr6 = [NSMutableArray array];
[arr6 addObject:nil];
[arr6 insertObject:nil atIndex:4];
[arr6 insertObject:@3 atIndex:4];
}
複製程式碼
Aspects實用介紹
Aspects是一個基於Method Swizzle
的iOS函式替換的第三方庫,他可以很好的實現勾取一個類或者一個物件的某個方法,支援在方法執行前(AspectPositionBefore)
/執行後(AspectPositionAfter)
或替代原方法執行(AspectPositionInstead)
。
pod "Aspects"
複製程式碼
需要匯入的標頭檔案
:
#import <Aspects/Aspects.h>
複製程式碼
對外的兩個重要介面
宣告如下:
第一個:HOOK一個類的所有例項的指定方法
/// 為一個指定的類的某個方法執行前/替換/後,新增一段程式碼塊.對這個類的所有物件都會起作用.
///
/// @param block 方法被新增鉤子時,Aspectes會拷貝方法的簽名資訊.
/// 第一個引數將會是 `id<AspectInfo>`,餘下的引數是此被呼叫的方法的引數.
/// 這些引數是可選的,並將被用於傳遞給block程式碼塊對應位置的引數.
/// 你甚至使用一個沒有任何引數或只有一個`id<AspectInfo>`引數的block程式碼塊.
///
/// @注意 不支援給靜態方法新增鉤子.
/// @return 返回一個唯一值,用於取消此鉤子.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
複製程式碼
第二個:HOOK一個類例項的指定方法
/// 為一個指定的物件的某個方法執行前/替換/後,新增一段程式碼塊.只作用於當前物件.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
複製程式碼
options有如下選擇:
AspectPositionAfter = 0, // 在原方法呼叫完成以後進行呼叫
AspectPositionInstead = 1, // 取代原方法
AspectPositionBefore = 2, // 在原方法呼叫前執行
AspectOptionAutomaticRemoval = 1 << 3 // 在呼叫了一次後清除(只能在物件方法中使用)
複製程式碼
三個重要引數
如下:
// 1、被HOOK的元類、類或者例項
@property (nonatomic, unsafe_unretained, readonly) id instance;
// 2、方法引數列表
@property (nonatomic, strong, readonly) NSArray *arguments;
// 3、原來的方法
@property (nonatomic, strong, readonly) NSInvocation *originalInvocation;
// 執行原來的方法
[originalInvocation invoke];
複製程式碼
基本使用
+(void)Aspect {
// 在類UIViewController所有的例項執行viewWillAppear:方法完畢後做一些事情
[UIViewController aspect_hookSelector:@selector(viewWillAppear:)
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> info) {
NSString *className = NSStringFromClass([[info instance] class]);
NSLog(@"%@", className);
} error:NULL];
// 在例項myVc執行viewWillAppear:方法完畢後做一些事情
UIViewController* myVc = [[UIViewController alloc] init];
[myVc aspect_hookSelector:@selector(viewWillAppear:)
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> info) {
id instance = info.instance; //呼叫的例項物件
id invocation = info.originalInvocation; //原始的方法
id arguments = info.arguments; //引數
[invocation invoke]; //原始的方法,再次呼叫
} error:NULL];
// HOOK類方法
Class metalClass = objc_getMetaClass(NSStringFromClass(UIViewController.class).UTF8String);
[metalClass aspect_hookSelector:@selector(ClassMethod)
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> info) {
NSLog(@"%@", HOOK類方法);
} error:NULL];
}
複製程式碼
注意:
Aspects
對類族無效,比如NSArray
需要使用系統方法對每個子類單獨hook
。- 所有的呼叫,都會是執行緒安全的。
Aspects
使用了Objective-C
的訊息轉發機會,會有一定的效能消耗.所有對於過於頻繁的呼叫,不建議使用Aspects
。Aspects
更適用於檢視/控制器相關的等每秒呼叫不超過1000次的程式碼。- 當應用於某個類時(使用類方法新增鉤子),不能同時
hook
父類和子類的同一個方法;否則會引起迴圈呼叫問題.但是,當應用於某個類的示例時(使用例項方法新增鉤子),不受此限制.- 使用KVO時,最好在
aspect_hookSelector:
呼叫之後新增觀察者,否則可能會引起崩潰.
參考連結
Objc 黑科技 - Method Swizzle 的一些注意事項
iOS 如何實現Aspect Oriented Programming (上)
iOS資料埋點統計方案選型(附Demo):執行時Method Swizzling機制與AOP程式設計(面向切面程式設計)