AOP,也就是面向切面編程,能夠經過預編譯方式或運行期動態代理實如今不修改源代碼的狀況下給程序動態統一添加功能的一種技術。git
在不修改源代碼的狀況下給程序動態添加功能,咱們通常稱之爲hook,在iOS中有幾種方案能夠去實現github
在這系列文章裏面將會探討我所瞭解的基於Method Swizzling和消息轉發的hook。編程
其實在服務端開發中,Spring以及Spring家族產品早已大殺四方,名揚天下。做爲Spring 基石之一的AOP思想更是發光發熱,在各類語言,各類平臺上,AOP編程思想都是作出了不可磨滅的貢獻。緩存
像在Java的後臺開發中,如日誌輸出,Spring Security OAuth2 的鑑權控制,請求攔截等都是AOP的經典應用,像這些與業務無關,可是又散佈在各個業務的需求,都是比較適合用AOP解決的。ruby
但話說回來,對於iOS中的OC開發者,AOP的實現方式有哪些呢?markdown
從語言特性上,OC沒有像JAVA那樣的語言特性,沒有註解。不能便捷且無侵入的去添加切面和起點。可是,OC有Runtime!有Runtime!有Runtime! 經過Runtime,咱們也能夠實現AOP編程。前面提到的Method Swizzling和基於消息轉發的實現Hook都是經過Rumtime去實現的。框架
咱們以前對一個方法進行hook,通常都是寫一個Category,而後在Category寫以下代碼(以hook viewDidAppear爲例)函數
+ (void)load {
Class class = [self class];
SEL originSEL = @selector(viewDidAppear:);
SEL swizzleSEL = @selector(swizzleViewDidAppear:);
Method originMethod = class_getInstanceMethod(class, originSEL);
Method swizzleMethod = class_getInstanceMethod(class, swizzleSEL);
BOOL didAddMethod = class_addMethod(class, originSEL,
method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzleSEL,
method_getImplementation(originMethod),
method_getTypeEncoding(originMethod));
} else {
method_exchangeImplementations(originMethod,
swizzleMethod);
}
}
// 咱們本身實現的方法,也就是和self的swizzleViewDidAppear方法進行交換的方法。
- (void)swizzleViewDidAppear:(BOOL)animated {
[self swizzleViewDidAppear:animated];
//埋點操做
//...........
}
複製代碼
其實這個的實現思路很簡單,就是交換兩個方法的實現地址(在上面就是viewDidAppear和swizzleViewDidAppear),而後在新的方法調用原有的方法,這樣就能夠在不修改原來的方法的代碼的狀況下動態添加內容,如圖所示工具
利用Method Swizzling,能夠實現Hook,並且是由於基於imp的交換,因此方法的執行速度快 可是從上面的代碼可知,這個方案有一下幾個弊端oop
另外關於Method Swizzling的弊端iOS界的毒瘤-MethodSwizzling
iOS中有一個老牌的基於消息轉發的AOP框架Aspects,可是本文所講述和使用的是本人本身寫的一個AOP工具,SFAspect。SFAspect核心的原理借鑑了Aspects,都是經過消息轉發去實現Hook。
爲何重複的去造一個輪子呢?由於基於我對AOP的理解以及iOS開發的一些習慣,我去了作了一些功能上的補充,如
前面兩點其實很好理解,主要是爲了提升Hook的靈活性和準確性,那爲何要中止切面後的代碼的執行呢?其實這一點我認爲很重要,尤爲對於驗證的需求來講。舉個例子,假設登錄服務類B登錄操做須要接收帳號和密碼參數,咱們能夠利用Hook對B的登錄操做進行參數校驗,對B類的登錄操做進行一個前置的Hook,若是帳號或密碼爲空,則在Hook中中止後續操做,以防沒必要要的調用。
接下來簡單說一下基於消息轉發的Hook(在另一篇文章會詳細講述)
相對基於Method Swizzling實現的實現,基於消息轉發的便捷性和動態性更強,可是有一點,基於消息轉發的hook的速度是慢於Method Swizzling的,Method Swizzling是直接交換方法的實現地址,而消息轉發的方案每一次調用方法都須要進入到消息轉發流程,對於被hook的方法,在被hook期間,方法緩存也至關於失效狀態。
SFAspect的實現原理在下一篇文章詳細描述
pod 'SFAspect'
複製代碼
[self.vc hookSel:@selector(viewWillAppear:) withIdentify:@"1" withPriority:0 withHookOption:(HookOptionPre) withBlock:^(SFAspectModel *aspectModel, HookState state) {
BOOL animated = NO;
NSInvocation *invocation = aspectModel.originalInvocation;
//參數從2開始,由於方法執行的時候隱式攜帶了兩個參數:self 和 _cmd,self是方法調用者,_cmd是被調用f方法的sel
[invocation getArgument:&animated atIndex:2];
NSLog(@"準備執行viewWillAppear,參數animated的值爲%d",animated);
//改變參數
animated = NO;
[invocation setArgument:&animated atIndex:2];
}];
[self.vc hookSel:@selector(viewWillAppear:) withIdentify:@"2" withPriority:0 withHookOption:(HookOptionAfter) withBlock:^(SFAspectModel *aspectModel, HookState state) {
BOOL animated = NO;
NSInvocation *invocation = aspectModel.originalInvocation;
//參數從2開始,由於方法執行的時候隱式攜帶了兩個參數:self 和 _cmd,self是方法調用者,_cmd是被調用f方法的sel
[invocation getArgument:&animated atIndex:2];
NSLog(@"執行viewWillAppear後,參數animated的值爲%d",animated);
//也能夠經過invocation獲取返回值,詳情參考消息轉發過程當中NSInvocation的用法
}];
複製代碼
[self.vc hookSel:@selector(sayHiTo:withVCTitle:) withIdentify:@"3" withPriority:0 withHookOption:(HookOptionPre) withBlock:^(SFAspectModel *aspectModel, HookState state) {
NSLog(@"hook單個對象的類方法");
}];
複製代碼
[SFHookViewController hookAllClassSel:@selector(viewWillAppear:) withIdentify:@"1" withPriority:0 withHookOption:(HookOptionPre) withBlock:^(SFAspectModel *aspectModel, HookState state) {
BOOL animated = NO;
NSInvocation *invocation = aspectModel.originalInvocation;
[invocation getArgument:&animated atIndex:2];
NSLog(@"準備執行viewWillAppear,參數animated的值爲%d",animated);
}];
複製代碼
[SFHookViewController hookAllClassSel:@selector(sayHiTo:withVCTitle:) withIdentify:@"1" withPriority:0 withHookOption:(HookOptionPre) withBlock:^(SFAspectModel *aspectModel, HookState state) {
BOOL animated = NO;
NSInvocation *invocation = aspectModel.originalInvocation;
[invocation getArgument:&animated atIndex:2];
NSLog(@"hook全部對象的類方法");
}];
複製代碼
[self.vc hookSel:@selector(viewWillAppear:) withIdentify:@"1" withPriority:0 withHookOption:(HookOptionPre) withBlock:^(SFAspectModel *aspectModel, HookState state) {
NSLog(@"準備執行viewWillAppear,執行的優先級是%d",aspectModel.priority);
}];
[self.vc hookSel:@selector(viewWillAppear:) withIdentify:@"2" withPriority:1 withHookOption:(HookOptionPre) withBlock:^(SFAspectModel *aspectModel, HookState state) {
NSLog(@"準備執行viewWillAppear,執行的優先級是%d",aspectModel.priority);
}];
複製代碼
[self.vc hookSel:@selector(viewWillAppear:) withIdentify:@"1" withPriority:0 withHookOption:(HookOptionPre) withBlock:^(SFAspectModel *aspectModel, HookState state) {
NSLog(@"準備執行viewWillAppear,執行的優先級是%d",aspectModel.priority);
}];
//移除hook後hook裏面的block不執行
[self.vc removeHook:@selector(viewWillAppear:) withIdentify:@"1" withHookOption:(HookOptionPre)];
複製代碼
[self.vc hookSel:@selector(viewWillAppear:) withIdentify:@"1" withPriority:0 withHookOption:(HookOptionPre) withBlock:^(SFAspectModel *aspectModel, HookState state) {
//pre是在方法前執行
NSLog(@"pre-準備執行viewWillAppear");
}];
[self.vc hookSel:@selector(viewWillAppear:) withIdentify:@"2" withPriority:0 withHookOption:(HookOptionAfter) withBlock:^(SFAspectModel *aspectModel, HookState state) {
//after是在方法前執行
NSLog(@"after-執行viewWillAppear後");
}];
[self.vc hookSel:@selector(viewWillAppear:) withIdentify:@"3" withPriority:0 withHookOption:(HookOptionAround) withBlock:^(SFAspectModel *aspectModel, HookState state) {
//around是在方法先後執行
if(state == HookStatePre){
NSLog(@"around準備執行viewWillAppear");
}
if (state == HookStateAfter) {
NSLog(@"around-準備執行viewWillAppear");
}
}];
複製代碼
__block CFAbsoluteTime startTime;
HookBLock block = ^(SFAspectModel *aspectModel, HookState state) {
//控制兩秒內不可再次點擊button
CFAbsoluteTime linkTime = (CFAbsoluteTimeGetCurrent() - startTime);
if (linkTime< 2) {
[aspectModel stop];//中止操做
// [aspectModel stopWithBlock:^{
// //中止並拋出異常
// }];
}else{
startTime = CFAbsoluteTimeGetCurrent();
}
};
[UIButton hookSel:@selector(sendAction:to:forEvent:) withIdentify:@"22" withPriority:1 withHookOption:(HookOptionPre) withBlock:block];
複製代碼
這是一個很簡單的應用,新建一個專門的埋點的類,在load方法中對須要被埋點的方法進行hook便可
+(void)load{
HookBLock block = ^(SFAspectModel *aspectModel, HookState state) {
//埋點操做
NSLog(@"//埋點操做");
};
[SFViewController1 hookAllClassSel:@selector(sayGoodDayTo:withVCTitle:) withIdentify:@"33" withPriority:1 withHookOption:(HookOptionPre) withBlock:block];
[SFHookViewController hookAllClassSel:@selector(sayHiTo:withVCTitle:) withIdentify:@"33" withPriority:1 withHookOption:(HookOptionPre) withBlock:block];
[SFViewController1 sayGoodDayTo:@"1" withVCTitle:@"1"];
[SFHookViewController sayHiTo:@"2" withVCTitle:@"2"];
}
複製代碼
當咱們的正式環境某個頁面出現崩潰的錯誤時,或是提交給蘋果審覈的時候,咱們能夠經過對頁面跳轉進行Hook,實現阻止用戶進入到某個頁面的需求。就拿hook方法pushViewController舉例 假設SFHookViewController出現了問題,要替換成SFViewController頁面
-(void)hookErrorPage{
//假設這裏是從線上獲取到出問題的頁面和替換的頁面
NSMutableDictionary *errorPageInfoDic = [NSMutableDictionary dictionary];
[errorPageInfoDic setObject:@"SFHookViewController" forKey:@"page_key"];//有問題的頁面
[errorPageInfoDic setObject:@"SFViewController1" forKey:@"jump_router"];//替換的頁面
__block NSMutableArray<NSMutableDictionary *> *errorPageList = [NSMutableArray array];
[errorPageList addObject:errorPageInfoDic];
if(errorPageList.count > 0){
//注意要使用block,由於在hook的block裏面對invocation的操做須要捕獲
__block UIViewController *vc = nil;
__block UIViewController *maintainVC = nil;
__block NSString *vcName;
//hook pushViewController,控制跳轉行爲
[UINavigationController hookSel:@selector(pushViewController:animated:) withIdentify:@"1" withPriority:0 withHookOption:(HookOptionPre) withBlock:^(SFAspectModel *aspectModel, HookState state) {
__block NSInvocation *invocation = aspectModel.originalInvocation;
//參數從2開始,由於方法執行的時候隱式攜帶了兩個參數:self 和 _cmd,self是方法調用者,_cmd是被調用f方法的sel
[invocation getArgument:&vc atIndex:2];
for (int i = 0; i < errorPageList.count; i++) {
NSDictionary *dic = errorPageList[i];
vcName = [dic objectForKey:@"page_key"];
if ([vcName isEqualToString:NSStringFromClass([vc class])]) {
//建立替換的頁面
maintainVC = [[NSClassFromString([dic valueForKey:@"jump_router"]) alloc] initWithNibName:[dic valueForKey:@"jump_router"] bundle:nil];
maintainVC.view.backgroundColor =[UIColor redColor];
if(maintainVC){
//替換頁面
[invocation setArgument:&maintainVC atIndex:2];
}
}
}
}];
}
[self.navigationController pushViewController:[[SFHookViewController alloc] initWithNibName:@"SFHookViewController" bundle:nil] animated:YES];
}
複製代碼
有些時候,咱們須要控制操做的間隔,舉個例子,有時候咱們會防止按鈕的段時間內屢次點擊,這種狀況也能夠經過Hook去控制。由於UIController的事件都是經過sendAction:to:forEvent:去調用的,咱們能夠經過hook UiButton的類去實現這種需求,以下
__block CFAbsoluteTime startTime;
HookBLock block = ^(SFAspectModel *aspectModel, HookState state) {
//控制兩秒內不可再次點擊button
CFAbsoluteTime linkTime = (CFAbsoluteTimeGetCurrent() - startTime);
if (linkTime< 2) {
[aspectModel stop];//中止操做
}else{
startTime = CFAbsoluteTimeGetCurrent();
}
};
[UIButton hookSel:@selector(sendAction:to:forEvent:) withIdentify:@"22" withPriority:1 withHookOption:(HookOptionPre) withBlock:block];
複製代碼
由於SFAspect中被hook的方法和hook裏面的操做是按順序執行,因此被hook的方法和hook裏面的操做至關因而鏈式調用,這裏不作代碼展現,以下圖所示
經過Hook咱們還能夠實現不少的需求,只要經過在方法調用先後能夠去作的事情,經過Hook都能實現
其實AOP不是必須的,可是AOP編程是一個開發利器,有不少的應用場景咱們均可以經過Aop去實現。