Objective-C runtime機制(3)——method swizzling
方法替換,又稱為method swizzling
,是一個比較著名的runtime黑魔法。網上有很多的實現,我們這裡直接講最正規的實現方式以及其背後的原理。
Method Swizzling
在進行方法替換前,我們要考慮兩種情況:
- 要替換的方法在target class中有實現
- 要替換的方法在target class中沒有實現,而是在其父類中實現
對於第一種情況,很簡單,我們直接呼叫method_exchangeImplementations即可達成方法。
而對於第二種情況,我們要仔細想想了。
因為在target class中沒有對應的方法實現,方法實際上是在target class的父類中實現的,因此當我們要交換方法實現時,其實是交換了target class父類的實現
比如,我想替換UIViewController
類中的methodForSelector:
方法,其實該方法是在其父類NSObject
類中實現的。如果我們直接呼叫method_exchangeImplementations
,則會替換掉NSObject
的方法。這樣當我們在別的地方,比如UITableView
中再呼叫methodForSelector:
方法時,其實會呼叫到父類NSObject
,而NSObject
的實現,已經被我們替換了。
為了避免這種情況,我們在進行方法替換前,需要檢查target class是否有對應方法的實現,如果沒有,則要講方法動態的新增到class
+(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
方法來對調originalMethod
和swizzledMethod
即可。
這裡有兩個細節,一個是在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物件,NSObject
的class
方法以及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 method
和 swizzled method
。Method
資料型別在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 class
和swizzled 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_addMethod
和class_replaceMethod
其實都是呼叫的addMethod
方法,區別只是bool replace
引數,一個是NO
,不會替換原始實現,另一個是YES
,會替換原始實現。
method_exchangeImplementations
如果class_addMethod
失敗,則說明target class
中的original method
是在target class
中有定義的,這時候,我們直接呼叫method_exchangeImplementations
交換實現即可。method_exchangeImplementations
實現很簡單,就是交換兩個Method
的IMP
:
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);
}
值得注意的地方
在寫這篇博文的時候,筆者曾做過這個實驗,在UIViewController
的Category
中,測試
- (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_getInstanceMethod
,runtime
除了查詢當前類,還會沿著繼承鏈
向上查詢對應的Method。
所以,這裡就造成了,class_getInstanceMethod
可以得到viewWillAppear:
對應的Method
,而在class_replaceMethod
中,卻找不到viewWillAppear:
對應的IMP
。
如果不瞭解背後的實現,確實很難理解這種看似矛盾的結果。