1. 程式人生 > >Objective-C runtime機制(3)——method swizzling

Objective-C runtime機制(3)——method swizzling

方法替換,又稱為method swizzling,是一個比較著名的runtime黑魔法。網上有很多的實現,我們這裡直接講最正規的實現方式以及其背後的原理。

Method Swizzling

在進行方法替換前,我們要考慮兩種情況:

  1. 要替換的方法在target class中有實現
  2. 要替換的方法在target class中沒有實現,而是在其父類中實現

對於第一種情況,很簡單,我們直接呼叫method_exchangeImplementations即可達成方法。

而對於第二種情況,我們要仔細想想了。
因為在target class中沒有對應的方法實現,方法實際上是在target class的父類中實現的,因此當我們要交換方法實現時,其實是交換了target class父類的實現

。這樣當其他地方呼叫這個父類的方法時,也會呼叫我們所替換的方法,這顯然使我們不想要的。

比如,我想替換UIViewController類中的methodForSelector:方法,其實該方法是在其父類NSObject類中實現的。如果我們直接呼叫method_exchangeImplementations,則會替換掉NSObject的方法。這樣當我們在別的地方,比如UITableView中再呼叫methodForSelector:方法時,其實會呼叫到父類NSObject,而NSObject的實現,已經被我們替換了。

為了避免這種情況,我們在進行方法替換前,需要檢查target class是否有對應方法的實現,如果沒有,則要講方法動態的新增到class

的method list中。

+(void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //要特別注意你替換的方法到底是哪個性質的方法
        // When swizzling a Instance method, use the following:
                Class class = [self class];

        // When swizzling a class method, use the following:
// Class class = object_getClass((id)self); SEL originalSelector = @selector(systemMethod_PrintLog); SEL swizzledSelector = @selector(ll_imageName); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); 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); } }); } 作者:春田花花幼兒園 連結:https://www.jianshu.com/p/a6b675f4d073 來源:簡書 著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

這是網上的一段程式碼例子,比較工整。

來解釋一下:
這裡我們用class_addMethod方法來檢查target class是否有方法實現。如果target class沒有實現對應方法的話,則class_addMethod會返回true,同時,會將方法新增到target class中。如果target class已經有對應的方法實現的話,則class_addMethod呼叫失敗,返回false,這時,我們直接呼叫
method_exchangeImplementations方法來對調originalMethodswizzledMethod即可。

這裡有兩個細節,一個是在class_addMethod方法中,我們傳入的SEL是originalSelector,而實現是swizzledMethod IMP,這樣就等同於調換了方法。當add method成功後,我們又呼叫

 if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
 } 

class_replaceMethod方法其實在內部會首先嚐試呼叫class_addMethod,將方法新增到class中,如果新增失敗,則說明class已經存在該方法,這時,會呼叫method_setImplementation來設定方法的IMP

if (didAddMethod) 中,我們將swizzledMethod的IMP設定為了originalMethod IMP,完成了方法交換。

第二個細節是這段註釋:

+(void)load {
//要特別注意你替換的方法到底是哪個性質的方法
        // When swizzling a Instance method, use the following:
                Class class = [self class];

        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
...
}

結合+(void)load方法的呼叫時機,它是由runtime在將class載入入記憶體中所呼叫的類方法。因此,我們一般會在這裡面進行方法交換,因為時機是很靠前的。

這裡要注意,在類方法中,self是一個類物件而不是例項物件。

當我們要替換類方法時,其實是要替換類物件所對應元類中的方法,要獲取類物件的元類,需要呼叫
object_getClass方法,它會返回ISA(),而類物件ISA(),恰好是元類

當我們要替換例項方法時,需要找到例項所對應的類,這時,就需要呼叫[self class],雖然self類物件,但是+ class會返回類物件自身,也就是例項物件所對應的類。

這段話說的比較繞,如果模糊的同學可以結合上一章最後類,元類的關係進行理解。

附帶class方法的實現原始碼:

NSObject.mm

+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}

Method swizzling原理

就如之前所說,runtime中所謂的黑魔法,只不過是基於runtime底層資料結構的應用而已。

現在,我們就一次剖析在method swizzling中所用到的runtime函式以及其背後實現和所依賴的資料結構。

class & object_getClass

要進行方法替換,首先要清楚我們要替換哪個類中的方法,即target class

// When swizzling a Instance method, use the following:
        Class class = [self class];

// When swizzling a class method, use the following:
        Class class = object_getClass((id)self);

我們有兩種方式獲取Class物件,NSObjectclass方法以及runtime函式object_getClass。這兩種方法的具體實現,還是有差別的。

class

先看NSObject的方法class,其實有兩個版本,一個是例項方法,一個是類方法,其原始碼如下:

+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}

當呼叫者是類物件時,會呼叫類方法版本,返回類物件自身。而呼叫者是例項物件時,會呼叫例項方法版本,在該版本中,又會呼叫runtime方法object_getClass

那麼在object_getClass中,又做了什麼呢?

object_getClass

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

實現很簡單,就是呼叫了物件的getIsa()方法。這裡我們可以簡單的理解為就是返回了物件的isa指標。

如果物件是例項物件isa返回例項物件所對應的類物件
如果物件是類物件isa返回類物件所對應的元類物件

我們在回過頭來看這段註釋(注意這裡的前提是在+load()方法中,self類物件):

// When swizzling a Instance method, use the following:
        Class class = [self class];

// When swizzling a class method, use the following:
        Class class = object_getClass((id)self);

當我們要調換例項方法,則需要修改例項物件所對應的類物件的方法列表,因為這裡的self已經是一個類物件,所有呼叫class方法其實會返回其自身,即例項物件對應的類物件

// When swizzling a Instance method, use the following:
        Class class = [self class];

當我們要調換類方法,則需要修改類物件所對應的元類物件的方法列表,因此要呼叫object_class方法,它會返回物件的isa,而類物件isa,則恰是類物件對應的元類物件:

// When swizzling a class method, use the following:
        Class class = object_getClass((id)self);

class_getInstanceMethod

確認了class後,我們就需要準備方法呼叫的原材料:originalMethod methodswizzled methodMethod資料型別在runtime中的定義為:

typedef struct method_t *Method;

struct method_t {
    SEL name;
    const char *types;
    IMP imp;

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};

我們所說的類的方法列表中,就是儲存的method_t型別。

Method資料型別的例項,如果自己建立的話,會比較麻煩,尤其是如何填充IMP,但我們可以從現有的class 方法列表中取出一個method來。很簡單,只需要呼叫class_getInstanceMethod方法。

class_getInstanceMethod方法究竟做了什麼呢?就像我們剛才說的一樣,它就是在指定的類物件中的方法列表中去取SEL所對應的Method


/***********************************************************************
* class_getInstanceMethod.  Return the instance method for the
* specified class and selector.
**********************************************************************/
Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;        
    lookUpImpOrNil(cls, sel, nil, 
                   NO/*initialize*/, NO/*cache*/, YES/*resolver*/);
    return _class_getMethod(cls, sel);
}

class_getInstanceMethod 首先呼叫了lookUpImpOrNil,其實它的內部實現和普通的訊息流程是一樣的(內部會呼叫上一章中說所的訊息查詢函式lookUpImpOrForward),只不過對於訊息轉發得到的IMP,會替換為nil

在進行了一波訊息流程之後,呼叫_class_getMethod方法

static Method _class_getMethod(Class cls, SEL sel)
{
    rwlock_reader_t lock(runtimeLock);
    return getMethod_nolock(cls, sel);
}

static method_t *
getMethod_nolock(Class cls, SEL sel)
{
    method_t *m = nil;
    runtimeLock.assertLocked();
    assert(cls->isRealized());
    // 核心:沿著繼承鏈,向上查詢第一個SEL所對應的method
    while (cls  &&  ((m = getMethodNoSuper_nolock(cls, sel))) == nil) {
        cls = cls->superclass;
    }

    return m;
}

// getMethodNoSuper_nolock 方法實質就是在查詢class的訊息列表
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    assert(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

class_addMethod

當我們獲取到target classswizzled method後,首先嚐試呼叫class_addMethod方法將swizzled method新增到target class中。

這樣做的目的在於:如果target class中沒有要替換的original method,則會直接將swizzled method 作為original method的實現新增到target class中。如果target class中確實存在original method,則class_addMethod會失敗並返回false,我們就可以直接呼叫method_exchangeImplementations 方法來實現方法替換。這就是下面一段邏輯程式碼的意義:

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);
        }

我們先來看class_addMethod 是怎麼實現的。其實到了這裡,相信大家不用看程式碼也能猜的出來,class_addMethod 其實就是將我們提供的method,插入到target class的方法列表中。事實是這樣的嗎,看原始碼:

BOOL 
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
    if (!cls) return NO;

    rwlock_writer_t lock(runtimeLock);
    return ! addMethod(cls, name, imp, types ?: "", NO);
}

static IMP 
addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
    IMP result = nil;

    runtimeLock.assertWriting();

    assert(types);
    assert(cls->isRealized());

    method_t *m;
    if ((m = getMethodNoSuper_nolock(cls, name))) {
        // 方法已經存在
        if (!replace) { // 如果選擇不替換,則返回原始的方法,新增方法失敗
            result = m->imp;
        } else {  // 如果選擇替換,則返回原始方法,同時,替換為新的方法
            result = _method_setImplementation(cls, m, imp);
        }
    } else {
        // 方法不存在, 則在class的方法列表中新增方法, 並返回nil
        method_list_t *newlist;
        newlist = (method_list_t *)calloc(sizeof(*newlist), 1);
        newlist->entsizeAndFlags = 
            (uint32_t)sizeof(method_t) | fixed_up_method_list;
        newlist->count = 1;
        newlist->first.name = name;
        newlist->first.types = strdupIfMutable(types);
        newlist->first.imp = imp;

        prepareMethodLists(cls, &newlist, 1, NO, NO);
        cls->data()->methods.attachLists(&newlist, 1);
        flushCaches(cls);

        result = nil;
    }

    return result;
}

原始碼證明,我們的猜想是正確的:)

class_replaceMethod

如果class_addMethod返回成功,則說明我們已經為target class新增上了SEL為original SEL,並且其實現是swizzled method。至此,我們方法交換完成了一半,現在我們將swizzled method替換為original method

 if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
 } 

這裡,我們呼叫了class_replaceMethod 方法。它的內部邏輯是這樣的:1. 如果target class中沒有SEL的對應實現,則會為target class新增上對應實現。 2. 如果target class中已經有了SEL對應的方法,則會將SEL對應的原始IMP,替換為新的IMP

IMP 
class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
{
    if (!cls) return nil;

    rwlock_writer_t lock(runtimeLock);
    return addMethod(cls, name, imp, types ?: "", YES);
}

static IMP 
addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
    IMP result = nil;

    runtimeLock.assertWriting();

    assert(types);
    assert(cls->isRealized());

    method_t *m;
    if ((m = getMethodNoSuper_nolock(cls, name))) {
        // 方法已經存在
        if (!replace) { // 如果選擇不替換,則返回原始的方法,新增方法失敗
            result = m->imp;
        } else {  // 如果選擇替換,則返回原始方法,同時,替換為新的方法
            result = _method_setImplementation(cls, m, imp);
        }
    } else {
        // 方法不存在, 則在class的方法列表中新增方法, 並返回nil
        method_list_t *newlist;
        newlist = (method_list_t *)calloc(sizeof(*newlist), 1);
        newlist->entsizeAndFlags = 
            (uint32_t)sizeof(method_t) | fixed_up_method_list;
        newlist->count = 1;
        newlist->first.name = name;
        newlist->first.types = strdupIfMutable(types);
        newlist->first.imp = imp;

        prepareMethodLists(cls, &newlist, 1, NO, NO);
        cls->data()->methods.attachLists(&newlist, 1);
        flushCaches(cls);

        result = nil;
    }

    return result;
}

通過原始碼對比可以發現,class_addMethodclass_replaceMethod其實都是呼叫的addMethod方法,區別只是bool replace引數,一個是NO,不會替換原始實現,另一個是YES,會替換原始實現。

method_exchangeImplementations

如果class_addMethod 失敗,則說明target class中的original method是在target class中有定義的,這時候,我們直接呼叫method_exchangeImplementations交換實現即可。method_exchangeImplementations 實現很簡單,就是交換兩個MethodIMP:


void method_exchangeImplementations(Method m1, Method m2)
{
    if (!m1  ||  !m2) return;

    rwlock_writer_t lock(runtimeLock);

    IMP m1_imp = m1->imp;
    m1->imp = m2->imp;
    m2->imp = m1_imp;


    // RR/AWZ updates are slow because class is unknown
    // Cache updates are slow because class is unknown
    // fixme build list of classes whose Methods are known externally?

    flushCaches(nil);

    updateCustomRR_AWZ(nil, m1);
    updateCustomRR_AWZ(nil, m2);
}

值得注意的地方

在寫這篇博文的時候,筆者曾做過這個實驗,在UIViewControllerCategory中,測試

- (void)exchangeImp {
    Class aClass = object_getClass(self);
    SEL originalSelector = @selector(viewWillAppear:);
    SEL swizzledSelector = @selector(sw_viewWillAppearXXX:);

    Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);
    IMP result = class_replaceMethod(aClass, originalSelector,method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    NSLog(@"result is %p", result);
}

因為在class_replaceMethod方法中,如果target class已經存在SEL對應的方法實現,則會返回其old IMP,並替換為new IMP。本來以為result會返回viewWillAppear:的實現,但結果卻是返回了nil。這是怎麼回事呢?

究其根本,原來是因為我是在UIViewController的子類ViewController中呼叫的exchangeImp方法,那麼object_getClass(self),其實會返回子類ViewController而不是UIViewController

class_replaceMethod中,runtime僅會查詢當前類aClass,即ViewController的方法列表,而不會向上查詢其父類UIViewController的方法列表。這樣自然就找不到viewWillAppear:的實現啦。

而對於class_getInstanceMethodruntime除了查詢當前類,還會沿著繼承鏈向上查詢對應的Method。

所以,這裡就造成了,class_getInstanceMethod可以得到viewWillAppear:對應的Method,而在class_replaceMethod中,卻找不到viewWillAppear:對應的IMP

如果不瞭解背後的實現,確實很難理解這種看似矛盾的結果。