什么是AOP
AOP:Aspect Oriented Programming,譯為面向切面編程。
在不修改源代碼的情況下,通過(guò)運(yùn)行時(shí)給程序添加統(tǒng)一功能的技術(shù)。
我覺(jué)得其中有兩層涵義:
- 第一:不修改源代碼,即盡可能的解耦。
- 第二:添加統(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)介中并沒(méi)有原話搬用百度百科的AOP簡(jiǎn)介,因?yàn)檫@是一篇iOS的AOP教程,在OC中我們就是用運(yùn)行時(shí)來(lái)給實(shí)現(xiàn)AOP的。(我們基本不會(huì)使用預(yù)編譯方式來(lái)實(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)
越是底層的框架越是難用,任何語(yǔ)言皆是如此,同樣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è)方法是同名不同類(lèi)型的方法,一個(gè)是靜態(tài)類(lèi)方法,一個(gè)是成員方法。
使用這個(gè)方法可以給類(lèi)的實(shí)例方法添加一個(gè)Block,并且對(duì)這個(gè)類(lèi)的所有對(duì)象都會(huì)起作用。
所有的調(diào)用,都會(huì)是線程安全的。Aspects 使用了Objective-C 的消息轉(zhuǎn)發(fā)機(jī)會(huì),會(huì)有一定的性能消耗。所有對(duì)于過(guò)于頻繁的調(diào)用,不建議使用 Aspects。Aspects更適用于視圖/控制器相關(guān)的等每秒調(diào)用不超過(guò)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];
通過(guò)這段代碼,我們給UIViewController的viewWillAppear:方法添加了一個(gè)鉤子,每當(dāng)在調(diào)用viewWillAppear:后就會(huì)執(zhí)行block中的代碼。在此我們打印了一段Log(加上emoji表情就更好找log啦),通過(guò)log我們可以看到當(dāng)前顯示的頁(yè)面的VC名稱,從而快速定位到該類(lèi)。還可以在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ò)。
通過(guò)這個(gè)log,我們可以知道ViewController是否釋放,如果沒(méi)有釋放很可能就是有循環(huán)引用,這時(shí)你務(wù)必仔細(xì)檢查你的代碼,這在性能調(diào)試和debug中非常有用。
AOP實(shí)戰(zhàn)
在實(shí)際的項(xiàng)目開(kāi)發(fā)中,事件統(tǒng)計(jì)是很多APP都會(huì)添加一項(xiàng)重要功能,它能統(tǒng)計(jì)用戶的行為、商品的銷(xiāo)售狀況、商品查看數(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)物車(chē)按鈕時(shí),記錄一下事件。我們實(shí)現(xiàn)起來(lái)大概會(huì)是這樣
- (void)onBuyButtonClicked:(id)sender
{
[XXXAnalytics track:eventName properties:properties];
}
這個(gè)需求就這樣輕松搞定了,但細(xì)細(xì)想想還是有不少問(wèn)題的:
- 頁(yè)面上會(huì)有其他的 Button,可能每個(gè) Button 都要放上這么一段代碼。
- 這些統(tǒng)計(jì)其實(shí)跟具體的業(yè)務(wù)無(wú)關(guān),沒(méi)必要跟業(yè)務(wù)代碼混雜在一起,不優(yōu)雅。
- 當(dāng)改版或者重構(gòu)時(shí),有可能忘了把相應(yīng)的事件統(tǒng)計(jì)代碼遷移過(guò)去。
使用AOP實(shí)現(xiàn)統(tǒng)計(jì)
基于上面的問(wèn)題,需要將事件統(tǒng)計(jì)這段代碼抽離,與具體點(diǎn)擊事件邏輯代碼解耦。通過(guò)AOP在運(yùn)行時(shí)將事件統(tǒng)計(jì)的代碼加入到方法中正是這個(gè)問(wèn)題的最佳解。代碼大概如下:
[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ì)變得越來(lái)越長(zhǎng),而且不好維護(hù)。如果我們可以利用一張表格來(lái)配置事件統(tǒng)計(jì),看起來(lái)會(huì)更加直觀簡(jiǎn)潔。
使用Xcode創(chuàng)建一個(gè)plist文件,其文件結(jié)構(gòu)如圖:

使用類(lèi)名作為字典的鍵,值為一個(gè)數(shù)組,數(shù)組內(nèi)存放該類(lè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)建類(lèi)對(duì)象
const char * className = [classNameString UTF8String];
//從一個(gè)字串返回一個(gè)類(lèi)
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è)陽(yáng)光明媚的上午,產(chǎn)品跑過(guò)來(lái)和我說(shuō)事件統(tǒng)計(jì)需要傳遞一些參數(shù),比如點(diǎn)擊查看商品詳情事件需要傳遞商品ID和商品名稱。我當(dāng)時(shí)心中就一萬(wàn)只草泥馬在奔騰,但是沒(méi)辦法呀!我們只是搬磚的程序猿,只能低頭默默的改。好不容易設(shè)計(jì)好的架構(gòu),眼看就要打回原形。后來(lái)仔細(xì)研究一番發(fā)現(xiàn),其實(shí)Aspects是可以通過(guò)Block獲取到方法傳遞的參數(shù)的,馬上心情好了許多,修改思路馬上再腦海形成。
首先,將Block改為^(id<AspectInfo> aspectInfo, NSDictionary *dict),第一個(gè)參數(shù)一定要為id<AspectInfo> aspectInfo,后面接方法傳遞的對(duì)應(yīng)類(lèi)型的參數(shù),這樣便可以接收到方法調(diào)用傳遞的參數(shù)。但是每一個(gè)事件需要傳遞的參數(shù)都各不相同,那我們要如何配置呢?
我的方案是:在plist的事件字典中加入一個(gè)鍵為Params,值為數(shù)組的鍵值對(duì)。修改后配置文件如下:

統(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)建類(lèi)對(duì)象
const char * className = [classNameString UTF8String];
//從一個(gè)字串返回一個(gè)類(lèi)
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,通過(guò)此方法可以傳遞任何我們需要傳遞的參數(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),如果方法為類(lèi)方法時(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不匹配,其根本原因在于無(wú)法使用Class獲取該Class的類(lèi)方法,通過(guò)runtime只能獲取到成員方法,而類(lèi)方法需要使用該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壓根就不支持類(lèi)方法,這讓我很是苦惱。不過(guò)按道理應(yīng)該是可以的,于是和同事討論了一下,就使用Method Swizzling做了交換兩個(gè)類(lèi)方法的試驗(yàn),結(jié)果是成功了。
查看Aspects的源代碼發(fā)現(xiàn),Aspects交換的是成員方法。無(wú)奈最后只能修改Aspects的源代碼,我在其中一方法中加入了Class類(lèi)型判斷,如果是MetaClass,那么就初始化為類(lèi)方法,而非成員方法。代碼如下:
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í)使用沒(méi)有遇到什么問(wèn)題,不過(guò)目測(cè)應(yīng)該是有bug的,不然Aspects的開(kāi)發(fā)者早就加了這判斷。
Demo:https://github.com/yongca887/AOPDemo
Aspects的坑
- 1.無(wú)法為類(lèi)方法添加hooking(通過(guò)上面的方法暫時(shí)可以解決,不過(guò)還是不太建議使用)
- 2.Block無(wú)法自動(dòng)判斷參數(shù)個(gè)數(shù),自動(dòng)匹配。如果你添加一個(gè)無(wú)參的方法,而B(niǎo)lock中有跟一個(gè)參數(shù),那么你會(huì)收到Block不匹配的錯(cuò)誤。