方法替換虱肄,又稱為method swizzling
滩愁,是一個(gè)比較著名的runtime黑魔法。網(wǎng)上有很多的實(shí)現(xiàn)瞧筛,我們這里直接講最正規(guī)的實(shí)現(xiàn)方式以及其背后的原理。
Method Swizzling
在進(jìn)行方法替換前导盅,我們要考慮兩種情況:
- 要替換的方法在target class中有實(shí)現(xiàn)
- 要替換的方法在target class中沒有實(shí)現(xiàn)较幌,而是在其父類中實(shí)現(xiàn)
對(duì)于第一種情況,很簡(jiǎn)單白翻,我們直接調(diào)用method_exchangeImplementations即可達(dá)成方法乍炉。
而對(duì)于第二種情況,我們要仔細(xì)想想了滤馍。
因?yàn)樵趖arget class中沒有對(duì)應(yīng)的方法實(shí)現(xiàn)岛琼,方法實(shí)際上是在target class的父類中實(shí)現(xiàn)的,<font color=red>因此當(dāng)我們要交換方法實(shí)現(xiàn)時(shí)巢株,其實(shí)是交換了target class父類的實(shí)現(xiàn)</font>衷恭。這樣當(dāng)其他地方調(diào)用這個(gè)父類的方法時(shí),也會(huì)調(diào)用我們所替換的方法纯续,這顯然使我們不想要的随珠。
比如,我想替換UIViewController
類中的methodForSelector:
方法猬错,其實(shí)該方法是在其父類NSObject
類中實(shí)現(xiàn)的窗看。如果我們直接調(diào)用method_exchangeImplementations
,則會(huì)替換掉NSObject
的方法倦炒。這樣當(dāng)我們?cè)趧e的地方显沈,比如UITableView
中再調(diào)用methodForSelector:
方法時(shí),其實(shí)會(huì)調(diào)用到父類NSObject
逢唤,而NSObject
的實(shí)現(xiàn)拉讯,已經(jīng)被我們替換了。
為了避免這種情況鳖藕,我們?cè)谶M(jìn)行方法替換前魔慷,需要檢查target class是否有對(duì)應(yīng)方法的實(shí)現(xiàn),如果沒有著恩,則要講方法動(dòng)態(tài)的添加到class
的method list中院尔。
+(void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//要特別注意你替換的方法到底是哪個(gè)性質(zhì)的方法
// 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);
}
});
}
作者:春田花花幼兒園
鏈接:http://www.reibang.com/p/a6b675f4d073
來源:簡(jiǎn)書
著作權(quán)歸作者所有蜻展。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處邀摆。
這是網(wǎng)上的一段代碼例子纵顾,比較工整。
來解釋一下:
這里我們用class_addMethod
方法來檢查target class是否有方法實(shí)現(xiàn)栋盹。如果target class沒有實(shí)現(xiàn)對(duì)應(yīng)方法的話施逾,則class_addMethod
會(huì)返回true
,同時(shí)例获,會(huì)將方法添加到target class中汉额。如果target class已經(jīng)有對(duì)應(yīng)的方法實(shí)現(xiàn)的話,則class_addMethod
調(diào)用失敗躏敢,返回false
,這時(shí)整葡,我們直接調(diào)用
method_exchangeImplementations
方法來對(duì)調(diào)originalMethod
和swizzledMethod
即可件余。
這里有兩個(gè)細(xì)節(jié),一個(gè)是在class_addMethod
方法中遭居,我們傳入的SEL
是originalSelector啼器,而實(shí)現(xiàn)是swizzledMethod IMP
,這樣就等同于調(diào)換了方法俱萍。當(dāng)add method成功后端壳,我們又調(diào)用
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
}
class_replaceMethod
方法其實(shí)在內(nèi)部會(huì)首先嘗試調(diào)用class_addMethod
,將方法添加到class中枪蘑,如果添加失敗损谦,則說明class已經(jīng)存在該方法,這時(shí)岳颇,會(huì)調(diào)用method_setImplementation
來設(shè)置方法的IMP
照捡。
在if (didAddMethod)
中,我們將swizzledMethod的IMP
設(shè)置為了originalMethod IMP
话侧,完成了方法交換栗精。
第二個(gè)細(xì)節(jié)是這段注釋:
+(void)load {
//要特別注意你替換的方法到底是哪個(gè)性質(zhì)的方法
// 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);
...
}
結(jié)合+(void)load
方法的調(diào)用時(shí)機(jī),它是由runtime在將class加載入內(nèi)存中所調(diào)用的類方法
瞻鹏。因此悲立,我們一般會(huì)在這里面進(jìn)行方法交換,因?yàn)闀r(shí)機(jī)是很靠前的新博。
這里要注意薪夕,在類方法中,self
是一個(gè)類對(duì)象
而不是實(shí)例對(duì)象赫悄。
當(dāng)我們要替換類方法時(shí)寥殖,其實(shí)是要替換類對(duì)象所對(duì)應(yīng)元類
中的方法玩讳,要獲取類對(duì)象的元類
,需要調(diào)用
object_getClass
方法嚼贡,它會(huì)返回ISA()
熏纯,而類對(duì)象
的ISA()
,恰好是元類
。
當(dāng)我們要替換實(shí)例方法時(shí)粤策,需要找到實(shí)例所對(duì)應(yīng)的類樟澜,這時(shí),就需要調(diào)用[self class]
叮盘,雖然self
是類對(duì)象
秩贰,但是+ class
會(huì)返回類對(duì)象自身,也就是實(shí)例對(duì)象所對(duì)應(yīng)的類柔吼。
這段話說的比較繞毒费,如果模糊的同學(xué)可以結(jié)合上一章最后類,類
和元類
的關(guān)系進(jìn)行理解愈魏。
附帶class
方法的實(shí)現(xiàn)源碼:
NSObject.mm
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
Method swizzling原理
就如之前所說觅玻,<font color=blue>runtime中所謂的黑魔法,只不過是基于runtime底層數(shù)據(jù)結(jié)構(gòu)的應(yīng)用而已培漏。</font>
現(xiàn)在溪厘,我們就一次剖析在method swizzling中所用到的runtime函數(shù)以及其背后實(shí)現(xiàn)和所依賴的數(shù)據(jù)結(jié)構(gòu)。
class & object_getClass
要進(jìn)行方法替換牌柄,首先要清楚我們要替換哪個(gè)類中的方法畸悬,即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對(duì)象,NSObject
的class
方法以及runtime
函數(shù)object_getClass
珊佣。這兩種方法的具體實(shí)現(xiàn)蹋宦,還是有差別的。
class
先看NSObject的方法class咒锻,其實(shí)有兩個(gè)版本妆档,一個(gè)是實(shí)例方法,一個(gè)是類方法虫碉,其源碼如下:
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
當(dāng)調(diào)用者是類對(duì)象時(shí)贾惦,會(huì)調(diào)用類方法版本,返回類對(duì)象自身敦捧。而調(diào)用者是實(shí)例對(duì)象時(shí)须板,會(huì)調(diào)用實(shí)例方法版本,在該版本中兢卵,又會(huì)調(diào)用runtime
方法object_getClass
习瑰。
那么在object_getClass
中,又做了什么呢秽荤?
object_getClass
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
實(shí)現(xiàn)很簡(jiǎn)單甜奄,就是調(diào)用了對(duì)象的getIsa()
方法柠横。這里我們可以簡(jiǎn)單的理解為就是返回了對(duì)象的isa
指針。
如果對(duì)象是實(shí)例對(duì)象
课兄,isa
返回實(shí)例對(duì)象所對(duì)應(yīng)的類對(duì)象
牍氛。
如果對(duì)象是類對(duì)象
,isa
返回類對(duì)象所對(duì)應(yīng)的元類對(duì)象
烟阐。
我們?cè)诨剡^頭來看這段注釋(注意這里的前提是在+load()
方法中搬俊,self
是類對(duì)象
):
// 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);
當(dāng)我們要調(diào)換<font color=orange>實(shí)例方法</font>,則需要修改實(shí)例對(duì)象
所對(duì)應(yīng)的類對(duì)象
的方法列表蜒茄,因?yàn)檫@里的self
已經(jīng)是一個(gè)類對(duì)象
唉擂,所有調(diào)用class
方法其實(shí)會(huì)返回其自身,即實(shí)例對(duì)象
對(duì)應(yīng)的類對(duì)象
:
// When swizzling a Instance method, use the following:
Class class = [self class];
當(dāng)我們要調(diào)換<font color=orange>類方法</font>檀葛,則需要修改類對(duì)象
所對(duì)應(yīng)的元類對(duì)象
的方法列表玩祟,因此要調(diào)用object_class
方法,它會(huì)返回對(duì)象的isa
屿聋,而類對(duì)象
的isa
空扎,則恰是類對(duì)象
對(duì)應(yīng)的元類對(duì)象
:
// When swizzling a class method, use the following:
Class class = object_getClass((id)self);
class_getInstanceMethod
確認(rèn)了class后,我們就需要準(zhǔn)備方法調(diào)用的原材料:originalMethod method
和 swizzled method
胜臊。Method
數(shù)據(jù)類型在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; }
};
};
我們所說的<font color=blue>類的方法列表</font>中勺卢,就是存儲(chǔ)的method_t
類型伙判。
Method
數(shù)據(jù)類型的實(shí)例象对,如果自己創(chuàng)建的話,會(huì)比較麻煩宴抚,尤其是如何填充IMP
勒魔,但我們可以從現(xiàn)有的class 方法列表
中取出一個(gè)method來。很簡(jiǎn)單菇曲,只需要調(diào)用class_getInstanceMethod
方法冠绢。
class_getInstanceMethod
方法究竟做了什么呢?<font color=blue>就像我們剛才說的一樣常潮,它就是在指定的類對(duì)象
中的方法列表中去取SEL
所對(duì)應(yīng)的Method
</font>弟胀。
/***********************************************************************
* 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
首先調(diào)用了lookUpImpOrNil
,其實(shí)它的內(nèi)部實(shí)現(xiàn)和普通的消息流程是一樣的(內(nèi)部會(huì)調(diào)用上一章中說所的消息查找函數(shù)lookUpImpOrForward
)喊式,只不過對(duì)于消息轉(zhuǎn)發(fā)得到的IMP
孵户,會(huì)替換為nil
。
在進(jìn)行了一波消息流程之后岔留,調(diào)用_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());
// 核心:沿著繼承鏈夏哭,向上查找第一個(gè)SEL所對(duì)應(yīng)的method
while (cls && ((m = getMethodNoSuper_nolock(cls, sel))) == nil) {
cls = cls->superclass;
}
return m;
}
// getMethodNoSuper_nolock 方法實(shí)質(zhì)就是在查找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
當(dāng)我們獲取到target class
和swizzled method
后,首先嘗試調(diào)用class_addMethod
方法將swizzled method
添加到target class
中献联。
這樣做的目的在于:如果target class
中沒有要替換的original method
竖配,則會(huì)直接將swizzled method
作為original method
的實(shí)現(xiàn)添加到target class
中何址。如果target class
中確實(shí)存在original method
,則class_addMethod
會(huì)失敗并返回false
进胯,我們就可以直接調(diào)用method_exchangeImplementations
方法來實(shí)現(xiàn)方法替換用爪。這就是下面一段邏輯代碼的意義:
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
是怎么實(shí)現(xiàn)的。其實(shí)到了這里龄减,相信大家不用看代碼也能猜的出來项钮,class_addMethod
其實(shí)就是將我們提供的method,插入到target class的方法列表中希停。事實(shí)是這樣的嗎烁巫,看源碼:
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))) {
// 方法已經(jīng)存在
if (!replace) { // 如果選擇不替換,則返回原始的方法宠能,添加方法失敗
result = m->imp;
} else { // 如果選擇替換亚隙,則返回原始方法,同時(shí)违崇,替換為新的方法
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
返回成功,則說明我們已經(jīng)為target class
添加上了SEL為original SEL
羞延,并且其實(shí)現(xiàn)是swizzled method
渣淳。至此,我們方法交換完成了一半伴箩,現(xiàn)在我們將swizzled method
替換為original method
入愧。
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
}
這里,我們調(diào)用了class_replaceMethod
方法嗤谚。它的內(nèi)部邏輯是這樣的:1. 如果target class
中沒有SEL
的對(duì)應(yīng)實(shí)現(xiàn)棺蛛,則會(huì)為target class
添加上對(duì)應(yīng)實(shí)現(xiàn)。 2. 如果target class
中已經(jīng)有了SEL
對(duì)應(yīng)的方法巩步,則會(huì)將SEL
對(duì)應(yīng)的原始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))) {
// 方法已經(jīng)存在
if (!replace) { // 如果選擇不替換椅野,則返回原始的方法终畅,添加方法失敗
result = m->imp;
} else { // 如果選擇替換,則返回原始方法竟闪,同時(shí)离福,替換為新的方法
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;
}
通過源碼對(duì)比可以發(fā)現(xiàn),class_addMethod
和class_replaceMethod
其實(shí)都是調(diào)用的addMethod
方法瘫怜,區(qū)別只是bool replace
參數(shù)术徊,一個(gè)是NO
,不會(huì)替換原始實(shí)現(xiàn)鲸湃,另一個(gè)是YES
赠涮,會(huì)替換原始實(shí)現(xiàn)子寓。
method_exchangeImplementations
如果class_addMethod
失敗,則說明target class
中的original method
是在target class
中有定義的笋除,這時(shí)候斜友,我們直接調(diào)用method_exchangeImplementations
交換實(shí)現(xiàn)即可。method_exchangeImplementations
實(shí)現(xiàn)很簡(jiǎn)單垃它,就是交換兩個(gè)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);
}
值得注意的地方
在寫這篇博文的時(shí)候鲜屏,筆者曾做過這個(gè)實(shí)驗(yàn),在UIViewController
的Category
中国拇,測(cè)試
- (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);
}
因?yàn)樵?code>class_replaceMethod方法中洛史,如果target class
已經(jīng)存在SEL
對(duì)應(yīng)的方法實(shí)現(xiàn),則會(huì)返回其old IMP
酱吝,并替換為new IMP
也殖。本來以為result
會(huì)返回viewWillAppear:
的實(shí)現(xiàn),但結(jié)果卻是返回了nil
务热。這是怎么回事呢忆嗜?
究其根本,原來是因?yàn)槲沂窃?code>UIViewController的子類ViewController
中調(diào)用的exchangeImp
方法崎岂,那么object_getClass(self)
捆毫,其實(shí)會(huì)返回子類ViewController
而不是UIViewController
。
在class_replaceMethod
中冲甘,runtime
僅會(huì)查找當(dāng)前類aClass
绩卤,即ViewController
的方法列表,而不會(huì)向上查詢其父類UIViewController
的方法列表损合。這樣自然就找不到viewWillAppear
:的實(shí)現(xiàn)啦省艳。
而對(duì)于class_getInstanceMethod
娘纷,runtime
除了查找當(dāng)前類嫁审,還會(huì)沿著繼承鏈
向上查找對(duì)應(yīng)的Method。
所以赖晶,這里就造成了律适,class_getInstanceMethod
可以得到viewWillAppear:
對(duì)應(yīng)的Method
,而在class_replaceMethod
中遏插,卻找不到viewWillAppear:
對(duì)應(yīng)的IMP
捂贿。
如果不了解背后的實(shí)現(xiàn),確實(shí)很難理解這種看似矛盾的結(jié)果胳嘲。