iOS 面向切面(AOP)編程 —— Aspects & BlockHook

前言:

Aspect Oriented Programming (AOP,面向切面編程) 在 Objective-C 社區(qū)內(nèi)沒有那么有名,但是 AOP 在運(yùn)行時(shí)可以有巨大威力。 但是因?yàn)闆]有事實(shí)上的標(biāo)準(zhǔn),Apple 也沒有開箱即用的提供,也顯得不重要,開發(fā)者都不怎么考慮它。

—— 引用自禪與 Objective-C 編程藝術(shù)

但在實(shí)際項(xiàng)目中有時(shí)需要集成統(tǒng)計(jì)SDK,比如 Google Analytics, Flurry, MatomoTracker, 等等。一般情況下是直接將統(tǒng)計(jì)代碼寫到對應(yīng)的地方,比如需要統(tǒng)計(jì)某個(gè)界面的展示次數(shù)會(huì)將代碼寫在viewDidAppear:方法內(nèi),這就造成了很大的入侵性,并且view controller里的代碼將變糟糕起來。這時(shí)候就需要通過使用AOP將統(tǒng)計(jì)代碼單獨(dú)分離出來,這樣view controller不會(huì)被其它代碼污染,并且單獨(dú)分離出來以后擴(kuò)展或者更換其它統(tǒng)計(jì)SDK會(huì)方便很多。

在對類的特點(diǎn)方法進(jìn)行切面可以使用Aspects,但是在一些特殊情況下統(tǒng)計(jì)代碼需要寫在block的回調(diào)內(nèi),這時(shí)就需要用上BlockHook,比如需要在某個(gè)網(wǎng)絡(luò)請求成功的block回調(diào)內(nèi),這時(shí)候就需要Aspects & BlockHook配合使用。本文針對Aspects & BlockHook將分成兩個(gè)部分來講,主要講如何使用和使用中遇到的坑。

Aspects:

Aspects一個(gè)基于runtime的輕量級AOP開源框架,作者Peter Steinberger
,主要是對方法進(jìn)行Hook,該框架簡單易用,源碼不到千行卻非常健全,考慮到了很多關(guān)于Hook方面的安全問題。

基本用法:

Aspects暴露了兩個(gè)方法(方法名一樣),分別對應(yīng)類方法和實(shí)例方法,下面為使用示例:

SEL selektor = NSSelectorFromString(@"loginWithAccount:password:block:");
Class clazz = objc_getMetaClass(@"HYLoginNetwork".UTF8String);//類方法
//Class clazz = NSClassFromString(@"HYLoginNetwork");//實(shí)例方法
[clazz aspect_hookSelector:selektor
               withOptions:AspectPositionAfter//在Hook方法 執(zhí)行完成之后 執(zhí)行usingBlock里的代碼
                usingBlock:^(id<AspectInfo> aspectInfo, NSString *account, NSString *password, id block) {
                //需要執(zhí)行的代碼...
                }
                     error:nil];

通過字符串的方式創(chuàng)建selektor方法名和clazz對象,這樣可以減少過多的引入頭文件,輸入錯(cuò)誤的方法名或?qū)ο竺麜r(shí)會(huì)輸出錯(cuò)誤日志。方法返回的AspectToken對象可以通過remove方法取消Hook。AspectOptions代表何時(shí)執(zhí)行usingBlock的代碼。usingBlock的參數(shù)是動(dòng)態(tài)參數(shù),除了第一個(gè)參數(shù)aspectInfo是固定的外,其它參數(shù)是Hook的方法對應(yīng)的參數(shù)(按順序排列)。

AspectPositions:

typedef NS_OPTIONS(NSUInteger, AspectOptions) {
    AspectPositionAfter   = 0,            /// 在原始實(shí)現(xiàn)后調(diào)用(默認(rèn))
    AspectPositionInstead = 1,            /// 將替換原始實(shí)現(xiàn)。
    AspectPositionBefore  = 2,            /// 在原始實(shí)現(xiàn)之前調(diào)用。
    
    AspectOptionAutomaticRemoval = 1 << 3 /// 執(zhí)行一次后移除Hook
};

AspectInfo:

/// AspectInfo協(xié)議是usingBlock的第一個(gè)參數(shù)。
@protocol AspectInfo <NSObject>
- (id)instance; /// 當(dāng)前Hook的實(shí)例。

- (NSInvocation *)originalInvocation;/// 被 Hook 方法的原始 invocation

- (NSArray *)arguments;/// 被 Hook 方法的所有參數(shù)裝箱。 這是懶惰的(懶加載的)。
@end

方法有返回值?獲取返回值:

    id returnValue;
    [aspectInfo.originalInvocation getReturnValue:&returnValue];

BlockHook:

BlockHook是由楊蕭玉編寫并開源的框架,基于 libffi 實(shí)現(xiàn)了對 Objective-C Block 的 hook。

基本用法:
[clazz aspect_hookSelector:selektor
               withOptions:AspectPositionBefore //當(dāng)block是__NSStackBlock__類型的情況下要在這個(gè)方法執(zhí)行前(AspectPositionBefore)copy到堆上
                usingBlock:^(id<AspectInfo> aspectInfo) {
                        
                    __unsafe_unretained id block = [self getLastArgument:aspectInfo];
                    [block block_hookWithMode:BlockHookModeAfter//在block執(zhí)行完之后調(diào)用
                                   usingBlock:^(BHToken *token, NSInteger code){
                                       dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
                                                      ^{
                                                          //需要執(zhí)行的代碼...
                                                      });
                                           
                                   }];
                        
                }
                                       error:nil];

BlockHookMode:

typedef NS_ENUM(NSUInteger, BlockHookMode) {
    BlockHookModeAfter,      /// 在原始實(shí)現(xiàn)后調(diào)用
    BlockHookModeInstead,    /// 將替換原始實(shí)現(xiàn)
    BlockHookModeBefore,     /// 在原始實(shí)現(xiàn)之前調(diào)用
    BlockHookModeDead,       /// 在block銷毀之后調(diào)用
};

BlockHook的API是參照Aspects寫的,所以懂得Aspects的一看就懂。和Aspects一樣,方法返回的BHToken對象可以通過remove方法取消Hook。BlockHookMode代表何時(shí)執(zhí)行usingBlock的代碼。usingBlock的參數(shù)是動(dòng)態(tài)參數(shù),除了第一個(gè)參數(shù)BHToken是固定的外,其它參數(shù)是Hook的Block對應(yīng)的參數(shù)(按順序排列)。

但是需要注意的是,當(dāng)block是__NSStackBlock__類型的情況下要在這個(gè)方法執(zhí)行前(AspectPositionBefore)讓系統(tǒng)把Block copy,否則Hook不到這個(gè)Block。而且需要調(diào)用NSInvocationretainArguments方法,主動(dòng)讓NSInvocation把Block copy到堆上,否則從NSInvocation獲取的__NSStackBlock__類型block不會(huì)銷毀。

-(id)getLastArgument:(id<AspectInfo>)aspectInfo{
    [aspectInfo.originalInvocation retainArguments];
    __unsafe_unretained id block;
    //取最后一個(gè)參數(shù)(網(wǎng)絡(luò)請求成功的blcok)
    NSInteger index = aspectInfo.originalInvocation.methodSignature.numberOfArguments - 1;
    [aspectInfo.originalInvocation getArgument:&block atIndex:index];
    return block;
}

參考資料:

面向切面編程之 Aspects 源碼解析及應(yīng)用
從 Aspects 源碼中我學(xué)到了什么?
iOS 如何實(shí)現(xiàn)Aspect Oriented Programming (上)
Hook Objective-C Block with Libffi

寫在最后:

原文:https://www.hlzhy.com/?p=109
當(dāng)初為了讓BlockHook配合Aspects可沒少折騰啊,集成libffi.a問題(現(xiàn)在作者直接把libffi.a和相關(guān)頭文件集成在項(xiàng)目里了),__NSStackBlock__的問題也讓我困惑了好久?,F(xiàn)在寫出這篇文章了似乎也并不是現(xiàn)象中的那么復(fù)雜??。
最后,如果此文章對你有幫助,希望給個(gè)??。有什么問題歡迎在評論區(qū)探討

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容