iOS 面向切面編程AOP實踐

本文參考了網(wǎng)上的文章揽碘,結(jié)合自己理解,配合友盟統(tǒng)計唱较。

什么是AOP

AOP:Aspect Oriented Programming哥纫,譯為面向切面編程.
在不修改源代碼的情況下霉旗,通過運行時給程序添加統(tǒng)一功能的技術。
我覺得其中有兩層涵義:

第一:不修改源代碼蛀骇,即盡可能的解耦厌秒。
第二:添加統(tǒng)一的功能,即我們能實現(xiàn)的是添加統(tǒng)一的單一的功能,在某處使用AOP,我們只能實現(xiàn)一項單一的功能兰绣。如:日志記錄薄腻。當然你可以添加多個AOP的模塊到項目中,每一個實現(xiàn)不同功能棉姐,但是每一個功能必須是單一的屠列。
主要功能:日志記錄,性能統(tǒng)計等伞矩。

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

有心的讀者可能會發(fā)現(xiàn)笛洛,我在上面的AOP簡介中并沒有原話搬用百度百科的AOP簡介,因為這是一篇iOS的AOP教程乃坤,在OC中我們就是用運行時來給實現(xiàn)AOP的苛让。(我們基本不會使用預編譯方式來實現(xiàn)AOP)
在iOS中實現(xiàn)AOP的核心技術是Runtime,使用Runtime的Method Swizzling黑魔法。

AOP技術實現(xiàn)

越是底層的框架越是難用湿诊,任何語言皆是如此狱杰,同樣Method Swizzling也不例外。那是否有一個第三庫厅须,可以讓我們輕松駕馭Method Swizzling黑魔法呢仿畸?
當然有,而且不止一個朗和,其中最著名的要數(shù)Aspects错沽,Aspects的使用非常簡單,整個庫封裝為兩個方法:

+ (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;

實際為同一個方法眶拉,這兩個方法是同名不同類型的方法千埃,一個是靜態(tài)類方法,一個是成員方法忆植。
使用這個方法可以給類的實例方法添加一個Block放可,并且對這個類的所有對象都會起作用皿曲。

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

代碼示例

在調(diào)試應用時,使用Aspects動態(tài)添加日志記錄功能备韧。

[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
    //NSLog(@"???Appear:--> %@", aspectInfo.instance);
    NSLog(@"???Appear:--> %@", NSStringFromClass([aspectInfo.instance class]));
} error:NULL];

通過這段代碼劫樟,我們給UIViewController的viewWillAppear:方法添加了一個鉤子,每當在調(diào)用viewWillAppear:后就會執(zhí)行block中的代碼织堂。在此我們打印了一段Log(加上emoji表情就更好找log啦)叠艳,通過log我們可以看到當前顯示的頁面的VC名稱,從而快速定位到該類易阳。還可以在ViewController的Dealloc時打印log:

[UIViewController aspect_hookSelector:NSSelectorFromString(@"dealloc") withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo) {
        //NSLog(@"???Dealloc:---->: %@", aspectInfo.instance);
        NSLog(@"???Dealloc:---->: %@", NSStringFromClass([aspectInfo.instance class]));
    } error:NULL];

與上一段代碼的微小差別是Selector換成了NSSelectorFromString(@"dealloc")附较,而不是@selector(dealloc),這是因為在ARC下面是不能直接手動調(diào)用Dealloc的潦俺,@selector(dealloc)會被編譯器直接報錯拒课。

通過這個log,我們可以知道ViewController是否釋放事示,如果沒有釋放很可能就是有循環(huán)引用早像,這時你務必仔細檢查你的代碼,這在性能調(diào)試和debug中非常有用肖爵。

AOP實戰(zhàn)

在實際的項目開發(fā)中卢鹦,事件統(tǒng)計是很多APP都會添加一項重要功能,它能統(tǒng)計用戶的行為劝堪、商品的銷售狀況冀自、商品查看數(shù)據(jù)等,今天的AOP實戰(zhàn)是利用AOP實現(xiàn)APP事件統(tǒng)計秒啦。

這樣統(tǒng)計熬粗?

假設產(chǎn)品有這么個需求:當用戶在詳情頁點擊添加到購物車按鈕時,記錄一下事件帝蒿。我們實現(xiàn)起來大概會是這樣

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

這個需求就這樣輕松搞定了荐糜,但細細想想還是有不少問題的:

  • 頁面上會有其他的 Button,可能每個 Button 都要放上這么一段代碼葛超。
  • 這些統(tǒng)計其實跟具體的業(yè)務無關暴氏,沒必要跟業(yè)務代碼混雜在一起,不優(yōu)雅绣张。
  • 當改版或者重構(gòu)時答渔,有可能忘了把相應的事件統(tǒng)計代碼遷移過去。

使用AOP實現(xiàn)統(tǒng)計

基于上面的問題侥涵,需要將事件統(tǒng)計這段代碼抽離沼撕,與具體點擊事件邏輯代碼解耦宋雏。通過AOP在運行時將事件統(tǒng)計的代碼加入到方法中正是這個問題的最佳解。代碼大概如下:

[PBAGoodsDetailViewController aspect_hookSelector:@selector(onBuyButtonClicked:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
        [XXXAnalytics track:eventName properties:properties];
    } error:NULL];

多個事件务豺?

當然事件統(tǒng)計往往需要統(tǒng)計多個事件磨总,這時我們只要對該方法稍微抽象一下就可以了,代碼如下:

- (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)計

當事件非常多時笼沥,你的setupAnalytics方法將會變得越來越長蚪燕,而且不好維護。如果我們可以利用一張表格來配置事件統(tǒng)計奔浅,看起來會更加直觀簡潔馆纳。
使用Xcode創(chuàng)建一個plist文件,其文件結(jié)構(gòu)如圖:


plist.png

使用類名作為字典的鍵汹桦,值為一個數(shù)組鲁驶,數(shù)組內(nèi)存放該類下的事件列表,每個事件包含事件ID(EventId)和觸發(fā)事件的方法名稱(MethodName)舞骆。

在AppDelegate.m中钥弯,添加事件統(tǒng)計的代碼如下:

- (void)setupAnalytics
{
    //設置事件統(tǒng)計
    //放到異步線程去執(zhí)行
    __weak typeof(self) ws = self;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //讀取配置文件,獲取需要統(tǒng)計的事件列表
        NSString *path = [[NSBundle mainBundle] pathForResource:@"EventList" ofType:@"plist"];
        NSDictionary *eventStatisticsDict = [[NSDictionary alloc] initWithContentsOfFile:path];
        for (NSString *classNameString in eventStatisticsDict.allKeys) {
            //使用運行時創(chuàng)建類對象
            const char * className = [classNameString UTF8String];
            //從一個字串返回一個類
            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ù)

首先葛作,將Block改為^(id<AspectInfo> aspectInfo, NSDictionary *dict)寿羞,第一個參數(shù)一定要為id<AspectInfo> aspectInfo,后面接方法傳遞的對應類型的參數(shù)猖凛,這樣便可以接收到方法調(diào)用傳遞的參數(shù)赂蠢。但是每一個事件需要傳遞的參數(shù)都各不相同,那我們要如何配置呢辨泳?
方案是:在plist的事件字典中加入一個鍵為Params虱岂,值為數(shù)組的鍵值對。如上圖:

- (void)setupAnalytics
{
    //設置事件統(tǒng)計
    //放到異步線程去執(zhí)行
    __weak typeof(self) ws = self;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //讀取配置文件菠红,獲取需要統(tǒng)計的事件列表
        NSString *path = [[NSBundle mainBundle] pathForResource:@"EventList" ofType:@"plist"];
        NSDictionary *eventStatisticsDict = [[NSDictionary alloc] initWithContentsOfFile:path];
        for (NSString *classNameString in eventStatisticsDict.allKeys) {
            //使用運行時創(chuàng)建類對象
            const char * className = [classNameString UTF8String];
            //從一個字串返回一個類
            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];
            }
        }
    });
}

- (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) {
        //定義與事件相關的屬性信息
        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ù)以字典格式作為方法的第一個參數(shù),Params中配置事件統(tǒng)計需要傳遞的參數(shù)的Key试溯,通過此方法可以傳遞任何我們需要傳遞的參數(shù)蔑滓,使用plist快速、靈活配置需要傳遞的參數(shù)遇绞。實戰(zhàn)內(nèi)容到此基本結(jié)束键袱,我們使用AOP已經(jīng)實現(xiàn)了一個低耦合、可靈活配置的事件統(tǒng)計摹闽。

處理類方法

在使用Aspects中我發(fā)現(xiàn)蹄咖,如果方法為類方法時,并不會回調(diào)block付鹿。
查看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)用了拍谐。暫時使用沒有遇到什么問題,不過目測應該是有bug的馏段,不然Aspects的開發(fā)者早就加了這判斷轩拨。
Demo:https://github.com/zbwbb/AOPDemo

Aspects的坑

1.無法為類方法添加hooking(通過上面的方法暫時可以解決,不過還是不太建議使用)
2.Block無法自動判斷參數(shù)個數(shù)院喜,自動匹配亡蓉。如果你添加一個無參的方法,而Block中有跟一個參數(shù)喷舀,那么你會收到Block不匹配的錯誤砍濒。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市硫麻,隨后出現(xiàn)的幾起案子爸邢,更是在濱河造成了極大的恐慌,老刑警劉巖拿愧,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件杠河,死亡現(xiàn)場離奇詭異,居然都是意外死亡浇辜,警方通過查閱死者的電腦和手機券敌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來柳洋,“玉大人待诅,你說我怎么就攤上這事⌒芰停” “怎么了卑雁?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長绪囱。 經(jīng)常有香客問我测蹲,道長,這世上最難降的妖魔是什么毕箍? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任弛房,我火速辦了婚禮,結(jié)果婚禮上而柑,老公的妹妹穿的比我還像新娘文捶。我一直安慰自己荷逞,他們只是感情好,可當我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布粹排。 她就那樣靜靜地躺著种远,像睡著了一般。 火紅的嫁衣襯著肌膚如雪顽耳。 梳的紋絲不亂的頭發(fā)上坠敷,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天,我揣著相機與錄音射富,去河邊找鬼膝迎。 笑死,一個胖子當著我的面吹牛胰耗,可吹牛的內(nèi)容都是我干的限次。 我是一名探鬼主播,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼柴灯,長吁一口氣:“原來是場噩夢啊……” “哼卖漫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起赠群,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤羊始,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后查描,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體突委,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年叹誉,在試婚紗的時候發(fā)現(xiàn)自己被綠了鸯两。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡长豁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出忙灼,到底是詐尸還是另有隱情匠襟,我是刑警寧澤,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布该园,位于F島的核電站酸舍,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏里初。R本人自食惡果不足惜啃勉,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望双妨。 院中可真熱鬧淮阐,春花似錦叮阅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至状您,卻和暖如春勒叠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背膏孟。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工眯分, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人柒桑。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓颗搂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親幕垦。 傳聞我的和親對象是個殘疾皇子丢氢,可洞房花燭夜當晚...
    茶點故事閱讀 43,697評論 2 351

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