iOS面向切面程式設計筆記:UIButton按鈕防連點、NSArray陣列越界、資料打點

在路上重名了啊發表於2018-12-11

面向切面程式設計參考: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 的訊息轉發機會,會有一定的效能消耗.所有對於過於頻繁的呼叫,不建議使用 AspectsAspects更適用於檢視/控制器相關的等每秒呼叫不超過1000次的程式碼。
  • 當應用於某個類時(使用類方法新增鉤子),不能同時hook父類和子類的同一個方法;否則會引起迴圈呼叫問題.但是,當應用於某個類的示例時(使用例項方法新增鉤子),不受此限制.
  • 使用KVO時,最好在 aspect_hookSelector: 呼叫之後新增觀察者,否則可能會引起崩潰.

參考連結

ios 針對陣列越界的崩潰優化

Aspects原始碼解析

面向切面 Aspects 原始碼閱讀

iOS---防止UIButton重複點選的三種實現方式

Aspects– iOS的AOP面向切面程式設計的庫

Objc 黑科技 - Method Swizzle 的一些注意事項

Aspects– iOS的AOP面向切面程式設計的庫

iOS 如何實現Aspect Oriented Programming (上)

iOS資料埋點統計方案選型(附Demo):執行時Method Swizzling機制與AOP程式設計(面向切面程式設計)

Aspects原始碼解讀:動態Block呼叫(不定引數的Block)

相關文章