方法替換淋肾,又稱(chēng)為method swizzling,是一個(gè)比較著名的runtime黑魔法龄恋。網(wǎng)上有很多的實(shí)現(xiàn)凶伙,我們這里直接講最正規(guī)的實(shí)現(xiàn)方式以及其背后的原理郭毕。
Method Swizzling
在進(jìn)行方法替換前,我們要考慮兩種情況:
- 要替換的方法在target class中有實(shí)現(xiàn)
- 要替換的方法在target class中沒(méi)有實(shí)現(xiàn)铣卡,而是在其父類(lèi)中實(shí)現(xiàn)
對(duì)于第一種情況偏竟,很簡(jiǎn)單,我們直接調(diào)用method_exchangeImplementations即可達(dá)成方法敞峭。
而對(duì)于第二種情況踊谋,我們要仔細(xì)想想了旋讹。
因?yàn)樵趖arget class中沒(méi)有對(duì)應(yīng)的方法實(shí)現(xiàn),方法實(shí)際上是在target class的父類(lèi)中實(shí)現(xiàn)的睦疫,因此當(dāng)我們要交換方法實(shí)現(xiàn)時(shí)鞭呕,其實(shí)是交換了target class父類(lèi)的實(shí)現(xiàn)。這樣當(dāng)其他地方調(diào)用這個(gè)父類(lèi)的方法時(shí)瓦糕,也會(huì)調(diào)用我們所替換的方法,這顯然使我們不想要的亥揖。
比如圣勒,我想替換UIViewController類(lèi)中的methodForSelector:方法,其實(shí)該方法是在其父類(lèi)NSObject類(lèi)中實(shí)現(xiàn)的胡控。如果我們直接調(diào)用method_exchangeImplementations旁趟,則會(huì)替換掉NSObject的方法锡搜。這樣當(dāng)我們?cè)趧e的地方,比如UITableView中再調(diào)用methodForSelector:方法時(shí)凡傅,其實(shí)會(huì)調(diào)用到父類(lèi)NSObject肠缔,而NSObject的實(shí)現(xiàn),已經(jīng)被我們替換了槽华。
為了避免這種情況趟妥,我們?cè)谶M(jìn)行方法替換前,需要檢查target class是否有對(duì)應(yīng)方法的實(shí)現(xiàn)亲雪,如果沒(méi)有疚膊,則要講方法動(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);
}
});
}
這是網(wǎng)上的一段代碼例子灌砖,比較工整周崭。
這里我們用class_addMethod方法來(lái)檢查target class是否有方法實(shí)現(xiàn)。如果target class沒(méi)有實(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方法來(lái)對(duì)調(diào)originalMethod和swizzledMethod即可。
這里有兩個(gè)細(xì)節(jié)挨队,一個(gè)是在class_addMethod方法中蒿往,我們傳入的SEL是originalSelector熄浓,而實(shí)現(xiàn)是swizzledMethodIMP省撑,這樣就等同于調(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中,如果添加失敗浅侨,則說(shuō)明class已經(jīng)存在該方法证膨,這時(shí)央勒,會(huì)調(diào)用method_setImplementation來(lái)設(shè)置方法的IMP。
在if (didAddMethod)中稳吮,我們將swizzledMethod的IMP設(shè)置為了originalMethodIMP井濒,完成了方法交換瑞你。
第二個(gè)細(xì)節(jié)是這段注釋?zhuān)?/p>
+(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)用的類(lèi)方法撞蚕。因此过牙,我們一般會(huì)在這里面進(jìn)行方法交換寇钉,因?yàn)闀r(shí)機(jī)是很靠前的。
這里要注意谦秧,在類(lèi)方法中撵溃,self是一個(gè)類(lèi)對(duì)象而不是實(shí)例對(duì)象缘挑。
當(dāng)我們要替換類(lèi)方法時(shí),其實(shí)是要替換類(lèi)對(duì)象所對(duì)應(yīng)元類(lèi)中的方法诲宇,要獲取類(lèi)對(duì)象的元類(lèi),需要調(diào)用
object_getClass方法鹅心,它會(huì)返回ISA()纺荧,而類(lèi)對(duì)象的ISA(),恰好是元類(lèi)虐秋。
當(dāng)我們要替換實(shí)例方法時(shí),需要找到實(shí)例所對(duì)應(yīng)的類(lèi)用押,這時(shí)靶剑,就需要調(diào)用[self class]桩引,雖然self是類(lèi)對(duì)象,但是+ class會(huì)返回類(lèi)對(duì)象自身血崭,也就是實(shí)例對(duì)象所對(duì)應(yīng)的類(lèi)厘灼。
這段話說(shuō)的比較繞设凹,如果模糊的同學(xué)可以結(jié)合上一章最后類(lèi),類(lèi)和元類(lèi)的關(guān)系進(jìn)行理解月匣。
附帶class方法的實(shí)現(xiàn)源碼:
NSObject.mm
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
Method swizzling原理
就如之前所說(shuō)锄开,runtime中所謂的黑魔法胀蛮,只不過(guò)是基于runtime底層數(shù)據(jù)結(jié)構(gòu)的應(yīng)用而已糯钙。
現(xiàn)在,我們就一次剖析在method swizzling中所用到的runtime函數(shù)以及其背后實(shí)現(xiàn)和所依賴(lài)的數(shù)據(jù)結(jié)構(gòu)狡刘。
class & object_getClass
要進(jìn)行方法替換困鸥,首先要清楚我們要替換哪個(gè)類(lèi)中的方法疾就,即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è)是類(lèi)方法寨闹,其源碼如下:
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
當(dāng)調(diào)用者是類(lèi)對(duì)象時(shí),會(huì)調(diào)用類(lèi)方法版本沈善,返回類(lèi)對(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)的類(lèi)對(duì)象。
如果對(duì)象是類(lèi)對(duì)象建蹄,isa返回類(lèi)對(duì)象所對(duì)應(yīng)的元類(lèi)對(duì)象洞慎。
我們?cè)诨剡^(guò)頭來(lái)看這段注釋?zhuān)ㄗ⒁膺@里的前提是在+load()方法中嘿棘,self是類(lèi)對(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)換實(shí)例方法鸟妙,則需要修改實(shí)例對(duì)象所對(duì)應(yīng)的類(lèi)對(duì)象的方法列表,因?yàn)檫@里的self已經(jīng)是一個(gè)類(lèi)對(duì)象花椭,所有調(diào)用class方法其實(shí)會(huì)返回其自身矿辽,即實(shí)例對(duì)象對(duì)應(yīng)的類(lèi)對(duì)象:
// When swizzling a Instance method, use the following:
Class class = [self class];
當(dāng)我們要調(diào)換類(lèi)方法,則需要修改類(lèi)對(duì)象所對(duì)應(yīng)的元類(lèi)對(duì)象的方法列表雕蔽,因此要調(diào)用object_class方法萎羔,它會(huì)返回對(duì)象的isa碳默,而類(lèi)對(duì)象的isa嘱根,則恰是類(lèi)對(duì)象對(duì)應(yīng)的元類(lèi)對(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ù)類(lèi)型在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; }
};
};
我們所說(shuō)的類(lèi)的方法列表中涌攻,就是存儲(chǔ)的method_t類(lèi)型。
Method數(shù)據(jù)類(lèi)型的實(shí)例芝此,如果自己創(chuàng)建的話因痛,會(huì)比較麻煩鸵膏,尤其是如何填充IMP,但我們可以從現(xiàn)有的class 方法列表中取出一個(gè)method來(lái)用僧。很簡(jiǎn)單责循,只需要調(diào)用class_getInstanceMethod方法攀操。
class_getInstanceMethod方法究竟做了什么呢?就像我們剛才說(shuō)的一樣歹垫,它就是在指定的類(lèi)對(duì)象中的方法列表中去取SEL所對(duì)應(yīng)的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首先調(diào)用了lookUpImpOrNil暮芭,其實(shí)它的內(nèi)部實(shí)現(xiàn)和普通的消息流程是一樣的(內(nèi)部會(huì)調(diào)用上一章中說(shuō)所的消息查找函數(shù)lookUpImpOrForward),只不過(guò)對(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中沒(méi)有要替換的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 方法來(lái)實(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);
}
我們先來(lái)看class_addMethod是怎么實(shí)現(xiàn)的鲤妥。其實(shí)到了這里拱雏,相信大家不用看代碼也能猜的出來(lái)铸抑,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返回成功凶硅,則說(shuō)明我們已經(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中沒(méi)有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;
}
通過(guò)源碼對(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 失敗,則說(shuō)明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);
}
值得注意的地方
在寫(xiě)這篇博文的時(shí)候驻债,筆者曾做過(guò)這個(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)樵赾lass_replaceMethod方法中合呐,如果target class已經(jīng)存在SEL對(duì)應(yīng)的方法實(shí)現(xiàn),則會(huì)返回其old IMP笙以,并替換為new IMP淌实。本來(lái)以為result會(huì)返回viewWillAppear:的實(shí)現(xiàn),但結(jié)果卻是返回了nil猖腕。這是怎么回事呢拆祈?
究其根本,原來(lái)是因?yàn)槲沂窃赨IViewController的子類(lèi)ViewController中調(diào)用的exchangeImp方法放坏,那么object_getClass(self),其實(shí)會(huì)返回子類(lèi)ViewController而不是UIViewController老玛。
在class_replaceMethod中淤年,runtime僅會(huì)查找當(dāng)前類(lèi)aClass钧敞,即ViewController的方法列表,而不會(huì)向上查詢(xún)其父類(lèi)UIViewController的方法列表麸粮。這樣自然就找不到viewWillAppear:的實(shí)現(xiàn)啦溉苛。
而對(duì)于class_getInstanceMethod,runtime除了查找當(dāng)前類(lèi)弄诲,還會(huì)沿著繼承鏈向上查找對(duì)應(yīng)的Method炊昆。
所以,這里就造成了威根,class_getInstanceMethod可以得到viewWillAppear:對(duì)應(yīng)的Method凤巨,而在class_replaceMethod中,卻找不到viewWillAppear:對(duì)應(yīng)的IMP洛搀。
如果不了解背后的實(shí)現(xiàn)敢茁,確實(shí)很難理解這種看似矛盾的結(jié)果。