ios method swizzling

zachwang發表於2016-06-30

背景

最近在整理專案邏輯的時候,發現一個問題:就是打點統計,經常和程式碼業務邏輯混在了一起,耦合性很強,並且經常容易出錯。於是就在思考怎樣對這一塊進行優化。

其實,對這方面的討論一直也比較多,比如繼承基類,但是這樣很容易使程式碼變得臃腫。另一個比較好的辦法就是利用 method swizzling, hook 住需要打點的方法,將打點統計從業務邏輯中分離出來,而且額外工作量不大。最後就想從這方面去嘗試,當然並沒有自己造輪子,而是借用了 github 上的一個開源庫,Aspects。這個庫的程式碼量比較小,總共就一個類檔案,使用起來也比較方便,比如你想統計某個 controller 的 viewwillappear 的呼叫次數,你只需要引入 Aspect.h 標頭檔案,然後在合適的地方初始化如下程式碼即可。

 #pragma mark - addKvLogAspect
    - (void)addKvLogAspect {
        //想法tab開啟
        [self aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
           //統計打點
            NSLog(@"");
        }error:NULL];
    }

oc的動態性及函式的呼叫

看到上面這段程式碼大家應該有所感覺了,沒錯,它基本上就是基於 method swizzling 實現的。本篇文章暫時並不打算對 aspects 的程式碼進行解析(以後,可能會寫一篇這樣的文字),在這裡就簡單的記錄一下我個人對於 method swizzling 的理解。

ios 開發人員都知道, oc 是一門動態語言。這個動態性怎麼理解呢,知乎上有網友這麼總結過:

  1. 類和物件都是 id , 在給你一個 id 的前提下無法直觀的知道這個物件是類物件還是類本身. 簡單的可以簡化成 runtime 管理的都是 id ( id 的本質其實是 objc_object , objc_class 頭部其實就是 id, 也就是 isa ).

  2. Class 在 objc 中是動態建立的, selector、method、 imp、protocol 等都是隨後繫結上去的(即所謂的執行時繫結).

  3. 通過 runtime 能夠查出當前執行時環境中所有的類, 每個類中的方法, 每個類訊息的繫結, 每個類的實現的協議, 每個協議的定義, 每個類當前的訊息快取等一切你想知道的東西.

  4. 類的方法(訊息)呼叫是間接的.

動態性比較常用的地方就是你可以在執行時動態的改變函式呼叫的執行,可以給物件動態的新增函式,甚至動態生成一個全新的類。method swizzling 就是利用這個動態性,在執行時改變了函式呼叫的指向,從而使函式最終呼叫到自己定義的方法中去,那麼,這個過程是怎樣實現的呢?

瞭解 method swizzling 之前有必要先了解一下 oc 函式的呼叫過程,這裡先簡單介紹幾個概念:

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 進行快取等等)。

swizzling 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 的指向而已。不過這裡有幾點需要說明:

  1. 這個swizzling只更改本物件的方法的呼叫,並不會影響起父類,子類的呼叫情況。也就是在子類controller呼叫viewWillAppear還是正常的呼叫viewWillAppear,但是,當呼叫[super viewWillAppear:animated]的時候,會呼叫到上面的 [self swizzling_viewwillAppear:animated].

  2. 細心的朋友或許會發現,上面swizzling_viewwillAppear的實現又呼叫了[self swizzling_viewwillAppear:animated] , 這樣會不會形成迴圈呼叫了?其實不會,因為已經更改了@seletor(swizzling_viewwillAppear:)對應的imp,呼叫[self swizzling_viewwillAppear:animated],實際上相當於呼叫了[self viewWillAppear:animated],並不會形成迴圈呼叫。

相關文章