最近在整理項目邏輯的時候,發現一個問題:就是打點統計,常常和代碼業務邏輯混在了一塊兒,耦合性很強,而且常常容易出錯。因而就在思考怎樣對這一塊進行優化。html
其實,對這方面的討論一直也比較多,好比繼承基類,可是這樣很容易使代碼變得臃腫。另外一個比較好的辦法就是利用 method swizzling, hook 住須要打點的方法,將打點統計從業務邏輯中分離出來,並且額外工做量不大。最後就想從這方面去嘗試,固然並無本身造輪子,而是借用了 github 上的一個開源庫,Aspects。這個庫的代碼量比較小,總共就一個類文件,使用起來也比較方便,好比你想統計某個 controller 的 viewwillappear 的調用次數,你只須要引入 Aspect.h 頭文件,而後在合適的地方初始化以下代碼便可。ios
#pragma mark - addKvLogAspect - (void)addKvLogAspect { //想法tab打開 [self aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) { //統計打點 NSLog(@""); }error:NULL]; }
看到上面這段代碼你們應該有所感受了,沒錯,它基本上就是基於 method swizzling 實現的。本篇文章暫時並不打算對 aspects 的代碼進行解析(之後,可能會寫一篇這樣的文字),在這裏就簡單的記錄一下我我的對於 method swizzling 的理解。git
ios 開發人員都知道, oc 是一門動態語言。這個動態性怎麼理解呢,知乎上有網友這麼總結過:github
類和對象都是 id , 在給你一個 id 的前提下沒法直觀的知道這個對象是類對象仍是類自己. 簡單的能夠簡化成 runtime 管理的都是 id ( id 的本質實際上是 objc_object , objc_class 頭部其實就是 id, 也就是 isa ).緩存
Class 在 objc 中是動態建立的, selector、method、 imp、protocol 等都是隨後綁定上去的(即所謂的運行時綁定).app
經過 runtime 可以查出當前運行時環境中全部的類, 每一個類中的方法, 每一個類消息的綁定, 每一個類的實現的協議, 每一個協議的定義, 每一個類當前的消息緩存等一切你想知道的東西.iview
類的方法(消息)調用是間接的.函數
動態性比較經常使用的地方就是你能夠在運行時動態的改變函數調用的執行,能夠給對象動態的添加函數,甚至動態生成一個全新的類。method swizzling 就是利用這個動態性,在運行時改變了函數調用的指向,從而使函數最終調用到本身定義的方法中去,那麼,這個過程是怎樣實現的呢?優化
瞭解 method swizzling 以前有必要先了解一下 oc 函數的調用過程,這裏先簡單介紹幾個概念:ui
1) oc 的類是由 Class 類型來表示的,定義以下:
typedef struct objc_class *Class;
它實際上是一個指向 objc_class 的指針,結構體以下:
struct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; // 父類 const char *name OBJC2_UNAVAILABLE; // 類名 long version OBJC2_UNAVAILABLE; // 類的版本信息,默認爲0 long info OBJC2_UNAVAILABLE; // 類信息,供運行期使用的一些位標識 long instance_size OBJC2_UNAVAILABLE; // 該類的實例變量大小 struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 該類的成員變量鏈表 struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定義的鏈表 struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法緩存 struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 協議鏈表 #endif } OBJC2_UNAVAILABLE;
2)類的實例 也是一個結構體 objc_object
struct objc_object { Class isa OBJC_ISA_AVAILABILITY; }; typedef struct objc_object *id;
這裏也就是咱們說的 id 對象,oc 裏面全部的對象都能用 id 表示。
這裏的字段含義暫時不作過多的解釋,有興趣的同窗能夠去網上找找。這裏介紹兩個屬性值:
isa:在 oc 中,類自己也被當成一個對象來處理。對於一個實例對象而言,isa 指針指向了這個對象的類(上面的 objc_class ),而類的 isa 指針指向了它的元類 ( metaclass 元類其實也是一種 objc_class ).,關於metaclass能夠參考這裏的分析。
methodLists:方法列表,記錄了全部的方法。(這裏只是實例方法,類方法須要經過isa去元類中尋找)
另外oc中一個方法的調用有以下幾個關鍵的部分:
sel又叫選擇器,它表明了一個方法的selector的指針。selector用於表達運行時的方法的名字。
SEL sel = @selector(method);
sel在一個類中是惟一的,並且是徹底依賴方法名,也就是說下面兩個函數
- (void)setDimension:(NSInteger)dimension { } - (void)setDimension:(float)dimension { }
會提示Duplicate declaration錯誤,由於儘管它們有不一樣的參數類型,可是因爲方法名徹底相同會致使sel相同,違背了sel惟一性的原則,這也是oc語法和其餘語法的不一樣。
IMP:一個函數指針,指向了方法實現的首地址
Method:它是類定義中表示方法的一個結構體,以下
struct objc_method { SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE; }
有了上面的鋪墊,就能更好的說明函數調用的整個過程了, 在 oc 中函數的調用形式是[ target * ],能夠理解爲[ receiver message ],也就是向 receiver 發送消息的過程。這個會被解析成以下形式 objc_msgSend(receiver,selector,arg1,...) ,也就是告訴 receiver ,我要發消息給你 selector 對應的方法, arg1 表示要傳遞給方法的參數。 receiver 收到這個通知後,會根據 objc_object 的(這裏先以實例方法爲例) isa 指針找到對象對應的 class 結構體,而後遍歷 methodlist 找到 method ,最後經過 method 找到對應的 imp 指針,而後 根據 imp 指針找到最終的函數實現。固然具體細節要比這複雜的多,(好比爲了提升效率,會對 method 進行緩存等等)。
基於上面的一些基本瞭解以後,咱們設想一下,若是要在運行時動態的改變函數的調用,改怎麼作呢?
上面咱們說過,調用函數時,會動態的根據 sel 尋找響應的 imp 指針,這就給了咱們啓發,試想一下,若是咱們改變了 sel 和 imp 的對應關係,那麼是否是也就意味着咱們改變了函數的調用關係? 接下來咱們能夠用代碼來驗證。
仍是以 hook uiviewcontroller 的 viewwillappear 爲例,實現以下:
- (void)viewDidLoad { [super viewDidLoad]; Class class = [self class]; SEL originalSelector = @selector(viewWillAppear:); SEL swizzledSelector = @selector(swizzling_viewWillAppear:); //get method Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); /** * 這裏實際上是在加了一個保護,若是class_addMethod返回no,說明originalSelector已經有存在的實現了,這個時候,咱們將 originalMethod,swizzledMethod直接替換掉就號了,若是尚未對應的實現,那麼直接添加進去,並更改原來swizzledSelector對應的實現 */ //exchange imp BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } } // 咱們本身實現的方法,也就是和self的viewDidLoad方法進行交換的方法。 - (void)swizzling_viewWillAppear:(BOOL)animated { // 咱們在這裏加一個判斷,將系統的UIViewController的對象剔除掉 NSLog(@"swizzling_viewWillAppear"); [self swizzling_viewWillAppear:animated]; } - (void)viewWillAppear:(BOOL)animated { NSLog(@"viewWillAppear"); [super viewWillAppear:animated]; }
運行結果以下
2016-06-30 13:56:31.186 Test[50266:6677359] swizzling_viewWillAppear 2016-06-30 13:56:31.187 Test[50266:6677359] viewWillAppear
和上面所說的函數的調用過程對比就會發現實際上是同樣的。本質上就是在運行時,就是在運行時更改 sel 對應的 imp 的指向而已。不過這裏有幾點須要說明:
這個swizzling只更改本對象的方法的調用,並不會影響起父類,子類的調用狀況。也就是在子類controller調用viewWillAppear仍是正常的調用viewWillAppear,可是,當調用[super viewWillAppear:animated]的時候,會調用到上面的 [self swizzling_viewwillAppear:animated].
細心的朋友或許會發現,上面swizzling_viewwillAppear的實現又調用了[self swizzling_viewwillAppear:animated] , 這樣會不會造成循環調用了?其實不會,由於已經更改了@seletor(swizzling_viewwillAppear:)對應的imp,調用[self swizzling_viewwillAppear:animated],實際上至關於調用了[self viewWillAppear:animated],並不會造成循環調用。