iOS面向切面編程AOP實(shí)踐

什么是AOP

AOP:Aspect Oriented Programming认轨,譯為面向切面編程。

在不修改源代碼的情況下月培,通過運(yùn)行時(shí)給程序添加統(tǒng)一功能的技術(shù)嘁字。

我覺得其中有兩層涵義:

  • 第一:不修改源代碼,即盡可能的解耦杉畜。
  • 第二:添加統(tǒng)一的功能纪蜒,即我們能實(shí)現(xiàn)的是添加統(tǒng)一的單一的功能,在某處使用AOP此叠,我們只能實(shí)現(xiàn)一項(xiàng)單一的功能纯续。如:日志記錄。當(dāng)然你可以添加多個(gè)AOP的模塊到項(xiàng)目中灭袁,每一個(gè)實(shí)現(xiàn)不同功能猬错,但是每一個(gè)功能必須是單一的。

主要功能:日志記錄茸歧,性能統(tǒng)計(jì)等兔魂。

iOS中如何實(shí)現(xiàn)AOP

有心的讀者可能會(huì)發(fā)現(xiàn),我在上面的AOP簡(jiǎn)介中并沒有原話搬用百度百科的AOP簡(jiǎn)介举娩,因?yàn)檫@是一篇iOS的AOP教程,在OC中我們就是用運(yùn)行時(shí)來給實(shí)現(xiàn)AOP的。(我們基本不會(huì)使用預(yù)編譯方式來實(shí)現(xiàn)AOP)

在iOS中實(shí)現(xiàn)AOP的核心技術(shù)是Runtime,使用Runtime的Method Swizzling黑魔法铜涉,我們可以移花接木智玻,在運(yùn)行時(shí)將方法的具體實(shí)現(xiàn)添油加醋、偷梁換柱芙代。

點(diǎn)此移步了解Method Swizzling

AOP技術(shù)實(shí)現(xiàn)

越是底層的框架越是難用吊奢,任何語言皆是如此,同樣Method Swizzling也不例外纹烹。那是否有一個(gè)第三庫(kù)页滚,可以讓我們輕松駕馭Method Swizzling黑魔法呢?

當(dāng)然有铺呵,而且不止一個(gè)裹驰,其中最著名的要數(shù)Aspects,Aspects的使用非常簡(jiǎn)單片挂,整個(gè)庫(kù)封裝為兩個(gè)方法:

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

實(shí)際為同一個(gè)方法幻林,這兩個(gè)方法是同名不同類型的方法,一個(gè)是靜態(tài)類方法音念,一個(gè)是成員方法沪饺。
使用這個(gè)方法可以給類的實(shí)例方法添加一個(gè)Block,并且對(duì)這個(gè)類的所有對(duì)象都會(huì)起作用闷愤。

所有的調(diào)用,都會(huì)是線程安全的整葡。Aspects 使用了Objective-C 的消息轉(zhuǎn)發(fā)機(jī)會(huì),會(huì)有一定的性能消耗。所有對(duì)于過于頻繁的調(diào)用,不建議使用 Aspects讥脐。Aspects更適用于視圖/控制器相關(guān)的等每秒調(diào)用不超過1000次的代碼遭居。

代碼示例

在調(diào)試應(yīng)用時(shí),使用Aspects動(dòng)態(tài)添加日志記錄功能。

[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
    //NSLog(@"??????Appear:--> %@", aspectInfo.instance);(為什么不使用此方式攘烛,請(qǐng)查看評(píng)論)
    NSLog(@"??????Appear:--> %@", NSStringFromClass([aspectInfo.instance class]));
} error:NULL];

通過這段代碼魏滚,我們給UIViewController的viewWillAppear:方法添加了一個(gè)鉤子,每當(dāng)在調(diào)用viewWillAppear:后就會(huì)執(zhí)行block中的代碼坟漱。在此我們打印了一段Log(加上emoji表情就更好找log啦)鼠次,通過log我們可以看到當(dāng)前顯示的頁(yè)面的VC名稱,從而快速定位到該類芋齿。還可以在ViewController的Dealloc時(shí)打印log:

[UIViewController aspect_hookSelector:NSSelectorFromString(@"dealloc") withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo) {
        //NSLog(@"??????Dealloc:---->: %@", aspectInfo.instance);(為什么不使用此方式腥寇,請(qǐng)查看評(píng)論)
        NSLog(@"??????Dealloc:---->: %@", NSStringFromClass([aspectInfo.instance class]));
    } error:NULL];

與上一段代碼的微小差別是Selector換成了NSSelectorFromString(@"dealloc"),而不是@selector(dealloc)觅捆,這是因?yàn)樵贏RC下面是不能直接手動(dòng)調(diào)用Dealloc的赦役,@selector(dealloc)會(huì)被編譯器直接報(bào)錯(cuò)。

通過這個(gè)log栅炒,我們可以知道ViewController是否釋放掂摔,如果沒有釋放很可能就是有循環(huán)引用术羔,這時(shí)你務(wù)必仔細(xì)檢查你的代碼,這在性能調(diào)試和debug中非常有用乙漓。

AOP實(shí)戰(zhàn)

在實(shí)際的項(xiàng)目開發(fā)中级历,事件統(tǒng)計(jì)是很多APP都會(huì)添加一項(xiàng)重要功能,它能統(tǒng)計(jì)用戶的行為叭披、商品的銷售狀況寥殖、商品查看數(shù)據(jù)等,今天的AOP實(shí)戰(zhàn)是利用AOP實(shí)現(xiàn)APP事件統(tǒng)計(jì)涩蜘。

這樣統(tǒng)計(jì)嚼贡?

假設(shè)產(chǎn)品有這么個(gè)需求:當(dāng)用戶在詳情頁(yè)點(diǎn)擊添加到購(gòu)物車按鈕時(shí),記錄一下事件同诫。我們實(shí)現(xiàn)起來大概會(huì)是這樣

- (void)onBuyButtonClicked:(id)sender
{
    [XXXAnalytics track:eventName properties:properties];
}

這個(gè)需求就這樣輕松搞定了粤策,但細(xì)細(xì)想想還是有不少問題的:

  • 頁(yè)面上會(huì)有其他的 Button,可能每個(gè) Button 都要放上這么一段代碼剩辟。
  • 這些統(tǒng)計(jì)其實(shí)跟具體的業(yè)務(wù)無關(guān)掐场,沒必要跟業(yè)務(wù)代碼混雜在一起,不優(yōu)雅贩猎。
  • 當(dāng)改版或者重構(gòu)時(shí)熊户,有可能忘了把相應(yīng)的事件統(tǒng)計(jì)代碼遷移過去。
使用AOP實(shí)現(xiàn)統(tǒng)計(jì)

基于上面的問題吭服,需要將事件統(tǒng)計(jì)這段代碼抽離嚷堡,與具體點(diǎn)擊事件邏輯代碼解耦。通過AOP在運(yùn)行時(shí)將事件統(tǒng)計(jì)的代碼加入到方法中正是這個(gè)問題的最佳解艇棕。代碼大概如下:

[PBAGoodsDetailViewController aspect_hookSelector:@selector(onBuyButtonClicked:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
        [XXXAnalytics track:eventName properties:properties];
    } error:NULL];
多個(gè)事件蝌戒?

當(dāng)然事件統(tǒng)計(jì)往往需要統(tǒng)計(jì)多個(gè)事件,這時(shí)我們只要對(duì)該方法稍微抽象一下就可以了沼琉,代碼如下:

- (void)setupAnalytics
{
    [self trackEventWithClass:aViewController selector:@seletor(onBuyButtonTapped:) event:kSomeEventYouDefined];
    [self trackEventWithClass:bViewController selector:@seletor(followButtonTapped:) event:kAnotherEventYouDefined];
    // ...
}
- (void)trackEventWithClass:(Class)klass selector:(SEL)selector event:(NSString *)event
{
[klass aspect_hookSelector:@selector(selector) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
    [XXXAnalytics track:eventName properties:properties];
    } error:NULL];
}
使用plist文件配置事件統(tǒng)計(jì)

當(dāng)事件非常多時(shí)北苟,你的setupAnalytics方法將會(huì)變得越來越長(zhǎng),而且不好維護(hù)打瘪。如果我們可以利用一張表格來配置事件統(tǒng)計(jì)友鼻,看起來會(huì)更加直觀簡(jiǎn)潔。
使用Xcode創(chuàng)建一個(gè)plist文件闺骚,其文件結(jié)構(gòu)如圖:


EventList.plish

使用類名作為字典的鍵彩扔,值為一個(gè)數(shù)組,數(shù)組內(nèi)存放該類下的事件列表僻爽,每個(gè)事件包含事件ID(EventId)和觸發(fā)事件的方法名稱(MethodName)虫碉。
在AppDelegate.m中,添加事件統(tǒng)計(jì)的代碼如下:

- (void)setupAnalytics
{
    //設(shè)置事件統(tǒng)計(jì)
    //放到異步線程去執(zhí)行
    __weak typeof(self) ws = self;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //讀取配置文件胸梆,獲取需要統(tǒng)計(jì)的事件列表
        NSString *path = [[NSBundle mainBundle] pathForResource:@"EventList" ofType:@"plist"];
        NSDictionary *eventStatisticsDict = [[NSDictionary alloc] initWithContentsOfFile:path];
        for (NSString *classNameString in eventStatisticsDict.allKeys) {
            //使用運(yùn)行時(shí)創(chuàng)建類對(duì)象
            const char * className = [classNameString UTF8String];
            //從一個(gè)字串返回一個(gè)類
            Class newClass = objc_getClass(className);
            NSArray *pageEventList = [eventStatisticsDict objectForKey:classNameString];
            for (NSDictionary *eventDict in pageEventList) {
                //事件方法名稱
                NSString *eventMethodName = eventDict[@"MethodName"];
                SEL seletor = NSSelectorFromString(eventMethodName);
                
                NSString *eventId = eventDict[@"EventId"];
                
                [self trackEventWithClass:newClass selector:seletor event:eventId];
            }
        }
    });
}

至此敦捧,一切好像都好像完美了须板,但人生總是充滿了變數(shù)。

事件需要傳遞參數(shù)

一個(gè)陽光明媚的上午兢卵,產(chǎn)品跑過來和我說事件統(tǒng)計(jì)需要傳遞一些參數(shù)逼纸,比如點(diǎn)擊查看商品詳情事件需要傳遞商品ID和商品名稱。我當(dāng)時(shí)心中就一萬只草泥馬在奔騰济蝉,但是沒辦法呀!我們只是搬磚的程序猿菠发,只能低頭默默的改王滤。好不容易設(shè)計(jì)好的架構(gòu),眼看就要打回原形滓鸠。后來仔細(xì)研究一番發(fā)現(xiàn)雁乡,其實(shí)Aspects是可以通過Block獲取到方法傳遞的參數(shù)的,馬上心情好了許多糜俗,修改思路馬上再腦海形成踱稍。

首先,將Block改為^(id<AspectInfo> aspectInfo, NSDictionary *dict)悠抹,第一個(gè)參數(shù)一定要為id<AspectInfo> aspectInfo,后面接方法傳遞的對(duì)應(yīng)類型的參數(shù)珠月,這樣便可以接收到方法調(diào)用傳遞的參數(shù)。但是每一個(gè)事件需要傳遞的參數(shù)都各不相同楔敌,那我們要如何配置呢啤挎?
我的方案是:在plist的事件字典中加入一個(gè)鍵為Params,值為數(shù)組的鍵值對(duì)卵凑。修改后配置文件如下:

EventListV2

統(tǒng)計(jì)代碼:

- (void)setupAnalytics
{
    //設(shè)置事件統(tǒng)計(jì)
    //放到異步線程去執(zhí)行
    __weak typeof(self) ws = self;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //讀取配置文件庆聘,獲取需要統(tǒng)計(jì)的事件列表
        NSString *path = [[NSBundle mainBundle] pathForResource:@"EventList" ofType:@"plist"];
        NSDictionary *eventStatisticsDict = [[NSDictionary alloc] initWithContentsOfFile:path];
        for (NSString *classNameString in eventStatisticsDict.allKeys) {
            //使用運(yùn)行時(shí)創(chuàng)建類對(duì)象
            const char * className = [classNameString UTF8String];
            //從一個(gè)字串返回一個(gè)類
            Class newClass = objc_getClass(className);
            NSArray *pageEventList = [eventStatisticsDict objectForKey:classNameString];
            for (NSDictionary *eventDict in pageEventList) {
                //事件方法名稱
                NSString *eventMethodName = eventDict[@"MethodName"];
                SEL seletor = NSSelectorFromString(eventMethodName);
                NSString *eventId = eventDict[@"EventId"];
                NSArray *params = eventDict[@"Params"];
                [self trackEventWithClass:newClass selector:seletor event:eventId params:params];
            }
        }
    });
}

統(tǒng)計(jì)方法:

- (void)trackEventWithClass:(Class)klass selector:(SEL)selector event:(NSString *)event params:(NSArray *)paramNames
{
    [klass aspect_hookSelector:@selector(selector) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, NSDictionary *dict) {
        //定義與事件相關(guān)的屬性信息
        NSMutableDictionary *properties = [NSMutableDictionary dictionary];
        //如果有參數(shù),那么把參數(shù)名和參數(shù)值拼接在eventID之后
        if (paramNames.count > 0) {
            if ([dict isKindOfClass:[NSDictionary class]]) {
                //獲取dict
                for (NSString *paramName in paramNames) {
                    //添加所需參數(shù)
                    NSString *paramValue = [dict objectForKey:paramName];
                    properties[paramName] = paramValue;
                }
            }
        }

        [XXXAnalytics track:eventName properties:properties];
    } error:NULL];
}

將需要傳遞的參數(shù)以字典格式作為方法的第一個(gè)參數(shù)勺卢,Params中配置事件統(tǒng)計(jì)需要傳遞的參數(shù)的Key伙判,通過此方法可以傳遞任何我們需要傳遞的參數(shù),使用plist快速黑忱、靈活配置需要傳遞的參數(shù)宴抚。實(shí)戰(zhàn)內(nèi)容到此基本結(jié)束,我們使用AOP已經(jīng)實(shí)現(xiàn)了一個(gè)低耦合杨何、可靈活配置的事件統(tǒng)計(jì)酱塔。

還有一些挑戰(zhàn)

在使用Aspects中我發(fā)現(xiàn),如果方法為類方法時(shí)危虱,并不會(huì)回調(diào)block羊娃。在調(diào)用aspect_hookSelector:withOptions:usingBlock:時(shí),報(bào)Aspects: Block signature <NSMethodSignature: 0x7fa13345ce60> doesn't match (null).錯(cuò)誤提示埃跷,意思是block不匹配蕊玷,其根本原因在于無法使用Class獲取該Class的類方法邮利,通過runtime只能獲取到成員方法,而類方法需要使用該Class的MetaClass獲取垃帅,MateClass可以使用object_getClass(newClass)得到延届。代碼如下:

[ws trackEventWithClass:object_getClass(newClass) selector:seletor event:eventId params:params];

修改后雖然不會(huì)報(bào)錯(cuò),但是依然不會(huì)觸發(fā)block贸诚。查看Aspects的github介紹發(fā)現(xiàn)方庭,Aspects壓根就不支持類方法,這讓我很是苦惱酱固。不過按道理應(yīng)該是可以的械念,于是和同事討論了一下,就使用Method Swizzling做了交換兩個(gè)類方法的試驗(yàn)运悲,結(jié)果是成功了龄减。

查看Aspects的源代碼發(fā)現(xiàn),Aspects交換的是成員方法班眯。無奈最后只能修改Aspects的源代碼希停,我在其中一方法中加入了Class類型判斷,如果是MetaClass署隘,那么就初始化為類方法宠能,而非成員方法。代碼如下:

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    NSCParameterAssert(selector);
    Class klass = aspect_hookClass(self, error);
    //TODO:Edit bu JackYong
    Method targetMethod;
    IMP targetMethodIMP;
    if (class_isMetaClass(klass)) {
        targetMethod = class_getClassMethod(klass, selector);
        targetMethodIMP = method_getImplementation(targetMethod);
    } else {
        targetMethod = class_getInstanceMethod(klass, selector);
        targetMethodIMP = method_getImplementation(targetMethod);
    }

修改后block和往常一樣被調(diào)用了定踱。暫時(shí)使用沒有遇到什么問題棍潘,不過目測(cè)應(yīng)該是有bug的,不然Aspects的開發(fā)者早就加了這判斷崖媚。
Demo:https://github.com/yongca887/AOPDemo

Aspects的坑
  • 1.無法為類方法添加hooking(通過上面的方法暫時(shí)可以解決亦歉,不過還是不太建議使用)
  • 2.Block無法自動(dòng)判斷參數(shù)個(gè)數(shù),自動(dòng)匹配畅哑。如果你添加一個(gè)無參的方法肴楷,而Block中有跟一個(gè)參數(shù),那么你會(huì)收到Block不匹配的錯(cuò)誤荠呐。

參考
iOS 統(tǒng)計(jì)打點(diǎn)那些事

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末赛蔫,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子泥张,更是在濱河造成了極大的恐慌呵恢,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件媚创,死亡現(xiàn)場(chǎng)離奇詭異渗钉,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門鳄橘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來声离,“玉大人,你說我怎么就攤上這事瘫怜∈趸玻” “怎么了?”我有些...
    開封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵鲸湃,是天一觀的道長(zhǎng)赠涮。 經(jīng)常有香客問我,道長(zhǎng)暗挑,這世上最難降的妖魔是什么世囊? 我笑而不...
    開封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮窿祥,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蝙寨。我一直安慰自己晒衩,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開白布墙歪。 她就那樣靜靜地躺著听系,像睡著了一般。 火紅的嫁衣襯著肌膚如雪虹菲。 梳的紋絲不亂的頭發(fā)上靠胜,一...
    開封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音毕源,去河邊找鬼浪漠。 笑死,一個(gè)胖子當(dāng)著我的面吹牛霎褐,可吹牛的內(nèi)容都是我干的址愿。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼冻璃,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼响谓!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起省艳,我...
    開封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤娘纷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后跋炕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赖晶,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年枣购,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了嬉探。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片擦耀。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖涩堤,靈堂內(nèi)的尸體忽然破棺而出眷蜓,到底是詐尸還是另有隱情,我是刑警寧澤胎围,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布吁系,位于F島的核電站,受9級(jí)特大地震影響白魂,放射性物質(zhì)發(fā)生泄漏汽纤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一福荸、第九天 我趴在偏房一處隱蔽的房頂上張望蕴坪。 院中可真熱鬧,春花似錦敬锐、人聲如沸背传。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽径玖。三九已至,卻和暖如春颤介,著一層夾襖步出監(jiān)牢的瞬間梳星,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工滚朵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留冤灾,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓辕近,卻偏偏與公主長(zhǎng)得像瞳购,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子亏推,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容