記一次針對UIViewController的AOP嘗試

記一次針對UIViewController的AOP嘗試


前言

    最近在看casa大牛博客的架構系列其中的一章 iOS應用架構談 view層的組織和調用方案。在「是否有必要讓業務方統一派生ViewController」這一觀點上,casa舉了在阿里工做時的例子,他發現當在作Demo時搭建環境是一件很痛苦很麻煩的事情,另外當要把作好的Demo合到項目中去時須要修改各類繼承關係,而且要提早去考慮接入後父類代碼可能會形成的影響。casa認爲統一派生沒有必要,建議使用AOP的方式。
開始我表示認同,由於確實是遇到過想要搭建簡單的環境可是出現各類代碼耦合的問題,我不想在原來的龐大的項目上另拉分支去搞,由於每次編譯都要很久;另外複製粘貼代碼搭建環境的話各類黏連讓人煩到死,因此我便根據casa大牛的思路進行了嘗試,這篇文章就是記錄了我此次嘗試的過程,而且在嘗試的過程也發現了一些問題並引起了一些思考。html


目錄

  • AOP思想介紹以及實現方案
  • 針對於UIViewController的AOP實現
  • 發現的一些問題
  • 總結與思考

AOP思想介紹以及實現方案

1.AOP思想

先貼上百度百科上AOP的解釋說明ios

在軟件業,AOP爲Aspect Oriented Programming的縮寫,意爲:面向切面編程,經過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。
MMP,什麼鬼?? 徹底不理解。多是理解能力比較差的緣故,起初不是很理解,對於AOP我總以爲是什麼望塵莫及的高深思想,直到看了casa大牛寫的解釋才明白這種思想其實很簡單。 2015-04-28 09:28補:關於AOP

我本身理解,AOP其實就是攔截一個流程中的某幾個狀態,並執行本身自定義操做的一種思想
在一個在程序執行1,2,3,4步的間隙跑到別處去執行你自定義的代碼,這種作法就是面向切片編程。git

使用AOP的好處:假如你要在某幾個步驟中間作一些操做,好比作一些埋點或者監聽、統計之類的事情,不用AOP也同樣能夠達到目的,直接往步驟之間塞代碼。可是事實狀況每每很複雜,直接塞進去的代碼頗有多是跟原業務無關的代碼,在同一份代碼文件裏面摻雜多種業務,這會帶來業務間耦合,有時你可能以爲只是多了幾句代碼,可是隨着後續需求的迭代,而且在團隊開發中常常會存在多人修改同一份類文件,這種耦合會變得愈來愈粘合。那麼爲了下降這種耦合度,因此使用AOP的思想來剝離這部分代碼。github

其實我以爲代理模式中向外拋出的各類狀態方法,這種作法很像AOP。好比UIScrollView的代理方法,使得在UIScrollView處在各個階段時,將狀態拋出給代理類去作本身想作的事,而代理類中的處理代碼與UIScrollView自身的處理邏輯無關,不存在耦合,因此我我的以爲這些代理方法很像AOP中的切片。objective-c

2.實現方案

使用Method Swizzing

利用Objective-C的runtime特性,能夠在運行時交換方法實現,使得你能夠將本身自定義的代碼注入指定的方法內。編程

// 工具方法
+ (void)swizzleMethod:(SEL)origSelector withMethod:(SEL)newSelector {
    Class class = self;
    
    Method originalMethod = class_getInstanceMethod(class, origSelector);
    Method swizzledMethod = class_getInstanceMethod(class, newSelector);
    
    BOOL didAddMethod = class_addMethod(class,
                                        origSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {
        class_replaceMethod(class,
                            newSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

// UIViewController+AOP類中:
+ (void)load {
    [UIViewController swizzleMethod:@selector(viewDidLoad) withMethod:@selector(aop_viewDidLoad)];
}

- (void)aop_viewDidLoad {
    [self aop_viewDidLoad];
    // 添加自定義的代碼
    ...
}

使用Aspects

另外可使用Aspects,一個現成關於的Method Swizzing的框架。
上面的代碼只須要改寫成以下代碼:緩存

// UIViewController+AOP類中:
+ (void)load {
    NSError *error;
    [UIViewController aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
        UIViewController *vc = aspectInfo.instance;
        [vc aop_viewDidLoad];
    } error:&error];
    if (error) NSLog(@"%@", error);
}

- (void)aop_viewDidLoad {
    [self aop_viewDidLoad];
    // 添加自定義的代碼
    ...
}

關於Ascepts的使用比較簡單,只有兩個方法架構

clipboard.png

其中參數:app

selector:將要hook的方法
options:block參數的調用時機,能夠設置block在原方法執行前/後執行,或者徹底取代原方法
block:注入的block代碼
error:錯誤回調對象

關於block中對原方法入參的獲取:(以UIViewController的presentViewController:animated:completion:方法爲例)框架

[UIViewController aspect_hookSelector:@selector(presentViewController:animated:completion:) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> aspectInfo) {
        UIViewController *vc    = aspectInfo.instance;
        NSArray *arguments      = aspectInfo.arguments;
        [vc aop_presentViewController:arguments[0] animated:[arguments[1] boolValue] completion:arguments[2]];
    } error:nil];

具體使用還能夠參考該框架在github上的介紹:Aspects


針對UIViewController的AOP實現

1.思路

這篇文章的想法是原由於casa大牛的建議,將UIViewController派生改成AOP方式實現,因此我打算將以前寫的ZCPViewController改寫爲UIViewController+AOP。

1.分析原來繼承方式實現的ZCPViewController。通常派生會重寫生命週期方法,而且會加一些自定義的方法和屬性。
2.對於生命週期方法固然是直接hook,原本也就是想要作這個的;
3.對於新增的屬性和方法,若是不使用繼承的話,只能採用分類的形式附加到UIViewController上;
4.對於分類的使用必定要考慮好做用域,由於分類會在任何直接或間接引入頭文件的地方生效。(請看做用域的相關補充)
5.作Demo測試效果

2.分析

咱們先看下派生類的功能:設置控制器的view背景顏色爲白色,監聽了鍵盤事件用於當點擊view的時候收起鍵盤。

ZCPViewController.h
clipboard.png

ZCPViewController.m
clipboard.png
clipboard.png
clipboard.png

3.將生命週期方法拆分出來

咱們須要建立一個分類單獨處理hook的生命週期方法。

UIViewController+AOP.m
clipboard.png

其中:
在load方法中使用Aspects去hook viewDidLoad和viewDidDisappear:方法;
在block中轉而調用相應的aop_viewDidLoad和aop_DidDisappear:方法去處理相關邏輯。至於爲何不直接在block中寫處理代碼,是由於大量代碼堆積在load方法內,代碼的可讀性太差。

4.將屬性和方法拆分出來

將生命週期方法拆分後,會發現出現了好多編譯錯誤,這是由於找不到方法和屬性的緣由,不要急,如今咱們把屬性和方法也拆到分類中。

UIViewController+Property.h
clipboard.png

UIViewController+Property.m
clipboard.png

此處使用了runtime動態添加屬性的寫法,更爲詳細的使用方法請搜索objc_setAssociatedObject函數的使用。

UIViewController+Method.h
clipboard.png

UIViewController+Method.m
clipboard.png

最後,咱們在UIViewController+AOP頭文件中引入方法分類和屬性分類便可消除編譯錯誤。

UIViewController+AOP.h
clipboard.png

5.分類做用域的考慮

因爲使用ZCPViewController時,只須要導入ZCPViewController.h就可使用其暴露出來的全部公有方法和屬性,所以也應當在導入UIViewController+AOP的地方可使用全部的分類方法和屬性。因此UIViewController+AOP選擇了在.h文件中引入UIViewController+Property和UIViewController+Method。

補充:
其實當你將分類引入項目的時候,會自動編譯.m文件(也就是會自動將.m文件加入到TARGETS->Build Phases->Compile Sources中),此時在項目的任何地方,即便不導入該分類的頭文件,也能夠經過performSelector:等方式去調用該分類方法,且不會出錯!
因此沒必要太糾結分類是在.h中導入仍是.m中導入,由於其做用域在你引入項目時就是全局生效的!
另外若是你在分類中重寫了該類的方法,則整個項目都會被影響。
這也是爲何apple官方不建議你在分類中重寫方法的緣由,由於它形成的破壞是全局的,而不是僅僅侷限於你導入分類頭文件的地方。

6.作Demo測試效果

經過上面的步驟,咱們成功的將ZCPViewController拆分開來,以後咱們建立控制器時能夠直接繼承UIViewController。

讓咱們作一個小Demo來試一試成果吧?。

建立一個項目,而後在自動生成的ViewController類中加一個UITextField,預計的效果是:

1.控制檯打印aop_viewDidLoad;
2.點擊輸入框彈出鍵盤,而後點擊視圖空白位置鍵盤會收起。
ViewController.m代碼以下:
clipboard.png

運行起來後控制檯打印:
clipboard.png

看上去很完美?,咱們再試試點擊輸入框:

clipboard.png

點擊後:
clipboard.png

納尼,當點擊輸入框的時候整個屏幕居然變白了~變白了~白了~ ?

最後在打斷點找了好久以後,發現了問題出在這個地方:
clipboard.png

緣由是當鍵盤彈出來的時候,管理鍵盤的控制器也是一個UIViewController的子類,因此當在aop_viewDidLoad方法中設置view的背景顏色時,一樣也會改變鍵盤控制器的view背景顏色。

問題已經搞明白了,解決的話須要把aop_viewDidLoad方法中設置背景顏色的代碼放到ViewController的viewDidLoad中去實現,

UIViewController+AOP.m
clipboard.png

ViewController.m
clipboard.png

驗證一下,發現沒有問題了。
clipboard.png


發現的一些問題

問題

1.hook UIViewController的viewDidLoad方法會影響到其全部的派生類。

繼承UIViewController的不只僅是本身自定義的類,UIKit框架中也有不少類繼承UIViewController,此外可能還會有一些第三方框架(不多),那麼在hook UIViewController的viewDidLoad方法後會通通影響到這些派生類。

2.Category影響範圍較大

Xcode在將Category預編譯後會影響整個項目,而不是隻是在引入頭文件的地方。

這個問題是在作Demo的時候無心中發現的。

針對問題2的Demo驗證

Demo1

有三個控制器,繼承關係爲:UIViewController -> ZCPViewController -> ViewController。最後裝載到window上的視圖爲ViewController。

下面爲各個類的代碼:

// ZCPViewController.h
@interface ZCPViewController : UIViewController

@end

// ZCPViewController.m
@implementation ZCPViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"ZCPViewController viewDidLoad");
}

@end
// ViewController.h
@interface ViewController : ZCPViewController

@end

// ViewController.m
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"ViewController viewDidLoad");
}

@end

另外有兩個分類,分別hook了ViewController和ZCPViewController的viewDidLoad方法

// ZCPViewController+AOP.h
@implementation ZCPViewController (AOP)

+ (void)load {
    NSError *error;
    [ZCPViewController aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
        ZCPViewController *vc = aspectInfo.instance;
        [vc aop_viewDidLoad];
    } error:&error];
    if (error) NSLog(@"%@", error);
}

- (void)aop_viewDidLoad {
    NSLog(@"ZCPViewController+AOP aop_viewDidLoad");
}

@end
// ViewController+AOP.m

@implementation ViewController (AOP)

+ (void)load {
    NSError *error;
    [ViewController aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
        ViewController *vc = aspectInfo.instance;
        [vc aop_viewDidLoad];
    } error:&error];
    if (error) NSLog(@"%@", error);
}

- (void)aop_viewDidLoad {
    NSLog(@"ViewController+AOP aop_viewDidLoad");
}

@end

(注意,我在ViewController.h和.m文件中沒有導入任何Category的頭文件。!!)
在運行起來以後會發現控制檯打印:
clipboard.png

就是說咱們不能在一個繼承樹上hook兩個相同的方法。

而後咱們把ViewController+AOP中的hook刪掉,只保留ZCPViewController+AOP的hook

// ViewController+AOP.m
@implementation ViewController (AOP)

- (void)aop_viewDidLoad {
    NSLog(@"ViewController+AOP aop_viewDidLoad");
}

@end

而後咱們跑起來,看控制檯打印的信息:
clipboard.png

有沒有以爲很詭異,咱們明明hook了ZCPViewController的viewDidLoad方法,也沒有去導入ViewController+AOP.h這個頭文件,怎麼會打印「ViewController+AOP aop_viewDidLoad」,按理說不是應該打印「ZCPViewController+AOP aop_viewDidLoad」嗎?鬧鬼了???

如今咱們來捋一下代碼執行的順序:

-> run
-> ViewController: viewDidLoad
-> ViewController: [super viewDidLoad]
-> ZCPViewController: viewDidLoad
-> ZCPViewController: NSLog(@"ZCPViewController viewDidLoad")
-> ZCPViewController+AOP: hookBlock
-> ZCPViewController+AOP: [vc aop_viewDidLoad]
-> ??
-> ViewController: NSLog(@"ViewController viewDidLoad")

如今有沒有發現問題,在??上面[vc aop_viewDidLoad]這句。vc對象是ViewController實例,當在執行aop_viewDidLoad方法的時候,根據Objective-C語言繼承類的方法執行順序和其動態特性,這句代碼執行後會先判斷ViewController類是否能響應aop_viewDidLoad方法,若是能夠響應則執行,若是不行則判斷父類可否響應該方法。
這就說明了一個結果:ViewController類能夠響應ViewController+AOP中寫的aop_viewDidLoad方法!!

根據控制檯打印的結果,猜測應該是正確的,Category中寫的方法都會被響應到。

Demo2

你可能會以爲不相信,那讓咱們再來作一個嘗試,刪掉前面寫的關於ZCPViewController+AOP的相關代碼和NSLog代碼,只保留下面的代碼

// ViewController.m
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    if ([self respondsToSelector:@selector(aop_viewDidLoad)]) {
        [self performSelector:@selector(aop_viewDidLoad)];
    }
}

@end
// ViewController+AOP
@implementation ViewController (AOP)

- (void)aop_viewDidLoad {
    NSLog(@"ViewController+AOP aop_viewDidLoad");
}

@end

話很少說,看控制檯打印結果吧:
clipboard.png

動態語言就是這麼牛。猜測再次獲得了驗證。


總結與思考

1.仍是有必要統一派生
2.hook框架類的方法不肯定性太大
3.要理解Category的設計初衷,規範使用
4.AOP的應用場景

1.仍是有必要統一派生

先說一下對於本篇文章主要作的事情的思考吧,經過了上面的一番嘗試對於casa大牛的用AOP方式徹底取代統一派生的想法不敢苟同。緣由有以下幾點。

1.統一派生的作法是封裝UIViewController,在建立新控制器的時候去繼承它,將公共的任務和配置交給這個統一的父類去處理。
以ZCPViewController爲例,統一派生的方式你在ZCPViewController中寫的公共代碼的做用範圍是其全部的自定義子類。而採用AOP方式hook UIViewController的話,這些公共代碼的做用範圍是全部UIViewController的子類。
其實你寫這些公共代碼的初衷只是想要針對本身自定義的這些子類。
針對做用範圍比較之下我以爲使用繼承更爲合理一些。

ps: 後面我又想到一種解決辦法,建立一個空的ZCPViewController,而後把hook UIViewController的代碼都改成hook ZCPViewController。這樣改寫後,雖然使用了AOP方式,可是做用範圍被侷限在ZCPViewController和其子類中。這種想法與使用繼承方式的初衷一致。

2.其實即便是使用了AOP,搭建環境仍然很麻煩,由於若是你想作一個Demo須要一部分項目環境,你仍舊須要將這些分類拷出來導入Demo中,而以前繼承方式寫代碼的這部分耦合如今轉移到分類中的代碼裏了。剝離起來仍是很煩。
舉個例子:假設繼承方式ZCPViewController的viewDidLoad方法中寫了關於處理本地緩存的代碼,用到了ZCPCache類,而ZCPCache又用到了其餘的xxx類。如今使用AOP後,這部分代碼跑到了aop_viewDidLoad中。當咱們要搭建環境時,都須要將ZCPCache相關的一系列類拷出來,仍是拔出蘿蔔帶出泥。
其次在將Demo合併到本來的項目中去時仍舊須要考慮hook的處理代碼和Demo代碼之間的相互影響。

3.使用Category處理本來派生類添加的屬性須要使用runtime,寫起來麻煩並且比較怪異。

綜上所述,我覺的仍是有必要統一派生。可是針對於這些痛點,我以爲更應該從代碼的結構上下手,將代碼分塊剝離,儘可能下降塊與塊之間的耦合。

2.hook框架類的方法不肯定性太大

咱們已經在上面的Demo中發現了hook框架類的問題,你在aop_viewDidLoad中設置背景顏色時不管如何也想不到會影響鍵盤控制器。這種地毯式轟炸的方式仍是頗有可能會誤傷友軍的,因爲UIKit並未開源,因此沒法肯定對如今框架形成的影響,另外或許之後apple會更新UIKit,對之後框架的影響也很難預測。且用且當心。

3.要理解Category的設計初衷,規範使用

Category的設計初衷是對原有類擴充一組方法,好比MGBox框架裏面的UIView+MGEasyFrame類中有一組處理frame的方法,top、left、bottom、right等等。
此外儘可能不要去添加屬性和重寫方法。緣由在上面已經說了不少次了。

4.AOP的應用場景

那麼說了這麼多,AOP思想有什麼應用場景呢?
其實咱們想一下,作Demo時遇問題是因爲hook方法注入的代碼與UIViewController自身有密切的關係(這也是爲了嘗試不使用派生,結果卻不太好)。AOP的應用場景主要用在注入與源類無關的代碼。好比像統計、埋點,或者本地存儲之類的,只要是與源類無耦合或者不影響到源類便可。咱們注入的代碼既然肯定與UIViewController無關不會影響到它,那麼爲何不開心的抽取出來呢。

總結

因此我以爲在使用時,與UIViewController有關的代碼寫到統一派生類中,無關的代碼可使用AOP抽取出來。這也是本篇文章嘗試下來最後的結論。


參考文章:

iOS應用架構談 view層的組織和調用方案
漫談iOS AOP編程之路

相關文章
相關標籤/搜索