iOS面向切面編程筆記:UIButton按鈕防連點、NSArray數組越界、數據打點

面向切面編程參考:React Native面向切面編程ios

iOS中的實現方式:

ObjC 中實現 AOP 最直接的方法就是使用 Runtime 中的 Method Swizzling。使用Aspects, 能夠不須要繁瑣的手工調用 Method Swizzlinggit

iOS中的應用場景一:數據統計

所謂 AOP 其實就是給你的程序提供一個可拆卸的組件化能力。好比你的 APP 須要用到事件統計功能, 不管你是用 UMeng, Google Analytics, 仍是其餘的統計平臺等等, 你應該都會寫過相似的代碼:github

- (void)viewDidLoad {
    [super viewDidLoad];
    [Logger log:@"View Did Load"];
    // 下面初始化數據
}
複製代碼

在視圖控制器開始加載的時候,用 Logger 類記錄一個統計事件。 其實 viewDidLoad 方法自己的邏輯並非爲了完成統計,而是進行一些初始化操做。這就致使了一個設計上的瑕疵, 數據統計的代碼和咱們實際的業務邏輯代碼混雜在一塊兒了。隨着業務邏輯代碼不斷增多,相似的混雜也會愈來愈多,這樣的耦合勢必會增長維護的成本。AOP 其實就是在不影響程序總體功能的狀況下,將 Logger 這樣的邏輯,從主業務邏輯中抽離出來的能力。有了 AOP 以後, 咱們的業務邏輯代碼就變成了這樣:編程

- (void)viewDidLoad {
    [super viewDidLoad];
    // 下面初始化數據
}
複製代碼

這裏再也不會出現 Logger 的統計邏輯的代碼,可是統計功能依然是生效的。 固然,不出如今主業務代碼中,不表明統計代碼就消失了。 而是用 AOP 模式 hook 到別的地方去了。swift

優勢:數組

  • 一、業務隔離 ,解耦。剝離開主業務和統計業務。
  • 二、即插即用。在預發佈環境和發佈環境測試的時候,不想記錄統計數據,只須要把統計業務邏輯模塊去掉便可。
  • 三、若是你在哪一天想換一個統計平臺, 那麼你不須要處處改代碼了, 只須要把統計層面的代碼修改一下就能夠。

缺點:安全

  • 一、代碼不夠直觀
  • 二、使用不當,出現Bug比較難於調試

iOS中的應用場景二:防止按鈕連續點擊

網上有一篇文章iOS---防止UIButton重複點擊的三種實現方式,通過實踐發現文章能夠做爲一個demo來演示,在真實的項目開發中是不實用的。由於sendAction:to:forEvent:方法是UIControl的方法,全部繼承自UIControl的類的這個方法都會被替換,好比UISwitch。下面是針對這篇文章的改進版,確保只有UIButton的改方法被HOOK:bash

#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 // 在調用了一次後清除(只能在對象方法中使用)
複製代碼

三個重要參數以下:

// 一、被HOOK的元類、類或者實例
@property (nonatomic, unsafe_unretained, readonly) id instance;
// 二、方法參數列表
@property (nonatomic, strong, readonly) NSArray *arguments;
// 三、原來的方法
@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)

相關文章
相關標籤/搜索