在iOS開(kāi)發(fā)中, Method Swizzling想必大家都不陌生, 可以以此來(lái)對(duì)方法進(jìn)行hook, 做一些我們希望做的事情, 比如頁(yè)面進(jìn)入退出, 可以對(duì)viewWillAppear及viewWillDisappear進(jìn)行hook, 從而進(jìn)行一些埋點(diǎn)日志相關(guān)的事情铜跑。
那么, Method Swizzling的原理到底是怎樣的呢? 這個(gè)問(wèn)題, 即使沒(méi)自己研究過(guò), 大多數(shù)人也有所耳聞, 簡(jiǎn)單來(lái)說(shuō), 無(wú)非就是修改方法的imp指向, 讓其指向我們hook的方法。如果是這樣的話, 我們是否可以不用Runtime提供的API如method_setImplementation栓拜、method_exchangeImplementation等函數(shù)而通過(guò)對(duì)象及方法的內(nèi)存布局來(lái)實(shí)現(xiàn)呢? 答案是肯定的, 下面便是我在此過(guò)程中的一些探索和理解簿透。
本文描述大部分內(nèi)容對(duì)開(kāi)發(fā)沒(méi)有太大幫助, 但是對(duì)于更加了解運(yùn)行時(shí)方法調(diào)用有一定幫助曙蒸。
直接賦值Method的IMP進(jìn)行hook
要想通過(guò)方法的內(nèi)存布局來(lái)修改, 一定要對(duì)方法的內(nèi)存布局有所了解, 查看源碼可以知道Method的內(nèi)存布局如下所示:
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; }
};
};
上面結(jié)構(gòu)中, 很容易就找到我們想要的東西IMP, 話不多少, 趕緊進(jìn)行hook闷尿。
@implementation Person
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];
SEL originalSelector = @selector(sayHello);
Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
struct method_t *method = (struct method_t *)originalMethod;
method->imp = (IMP)hookedSayHello;
});
}
- (void)sayHello {
NSLog(@"Hello, everybody!");
}
void hookedSayHello (id self, SEL _cmd, ...) {
NSLog(@"This is hooked sayHello");
}
@end
然后再main.m中調(diào)用:
Person *person = [[Person alloc] init];
[person sayHello];
遇到的問(wèn)題, 還是調(diào)用原來(lái)的方法實(shí)現(xiàn)
此時(shí)卻發(fā)現(xiàn), 打印出來(lái)的卻和我想象不太一樣, 仍然是調(diào)用了原來(lái)的sayHello方法, 而且打個(gè)斷點(diǎn)發(fā)現(xiàn)method的imp指針也確實(shí)指向了 void hookedSayHello (id self, SEL _cmd, ...) 這個(gè)函數(shù), 這確實(shí)有些讓人捉摸不透。
淺嘗輒止--method _setImplementation
于是懷疑人生的我, 又使用Runtime提供的API method_setImplementation進(jìn)行相同操作, 發(fā)現(xiàn)和以往一樣, 毫無(wú)問(wèn)題, 那么一定是做了一些處理, 查其源碼, 發(fā)現(xiàn)了一個(gè)很可疑的函數(shù) flushCaches, 見(jiàn)名知意, 清除緩存衔彻。
static IMP
_method_setImplementation(Class cls, method_t *m, IMP imp)
{
runtimeLock.assertWriting();
if (!m) return nil;
if (!imp) return nil;
IMP old = m->imp;
m->imp = imp;
// Cache updates are slow if cls is nil (i.e. unknown)
// RR/AWZ updates are slow if cls is nil (i.e. unknown)
// fixme build list of classes whose Methods are known externally?
flushCaches(cls);
updateCustomRR_AWZ(cls, m);
return old;
}
/***********************************************************************
* _objc_flush_caches
* Flushes all caches.
* (Historical behavior: flush caches for cls, its metaclass,
* and subclasses thereof. Nil flushes all classes.)
* Locking: acquires runtimeLock
**********************************************************************/
static void flushCaches(Class cls)
{
runtimeLock.assertWriting();
mutex_locker_t lock(cacheUpdateLock);
if (cls) {
foreach_realized_class_and_subclass(cls, ^(Class c){ // 遍歷子類
cache_erase_nolock(c);
});
}
else {
foreach_realized_class_and_metaclass(^(Class c){
cache_erase_nolock(c);
});
}
}
// Reset this entire cache to the uncached lookup by reallocating it.
// This must not shrink the cache - that breaks the lock-free scheme.
void cache_erase_nolock(Class cls)
{
cacheUpdateLock.assertLocked();
cache_t *cache = getCache(cls);
mask_t capacity = cache->capacity();
if (capacity > 0 && cache->occupied() > 0) {
auto oldBuckets = cache->buckets();
auto buckets = emptyBucketsForCapacity(capacity);
cache->setBucketsAndMask(buckets, capacity - 1); // also clears occupied
cache_collect_free(oldBuckets, capacity);
cache_collect(false);
}
}
如上述源碼可知, 在flushCaches函數(shù)中, 這個(gè)函數(shù)會(huì)把當(dāng)前類本身, 當(dāng)前類的元類以及當(dāng)前類的子類的方法緩存全部清空, 這里我們也可以自己驗(yàn)證一下,
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];
SEL originalSelector = @selector(sayHello);
Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
// method_setImplementation(originalMethod, (IMP)hookedSayHello); //Runtime API, 可以發(fā)現(xiàn)cache被清除了, 可以打開(kāi)注釋, 驗(yàn)證結(jié)果
struct method_t *method = (struct method_t *)originalMethod;
// method->imp = (IMP)hookedSayHello; // 直接復(fù)制imp指針
struct my_objc_class *clz = (__bridge struct my_objc_class *)aClass;
uint32_t cacheCount = clz->cache.capacity();
NSLog(@"cacheCount : %d", cacheCount);
for (NSInteger i = 0; i < cacheCount; i++) {
char *key = (char *)((clz->cache._buckets + i)->_key);
// 這里設(shè)置一下
printf("%ld - %s\n", i, key); // 測(cè)試
});
}
當(dāng)調(diào)用Runtime API method_setImplementation, 打印如下圖所示:
當(dāng)直接給imp指針賦值, 打印如下圖所示:
可以看出, 當(dāng)直接給imp指針復(fù)制, 不清除方法緩存, 其中打印的sayHello正是我們hook的方法, 之前的疑惑也一掃而空, 雖然方法的imp指向發(fā)生了改變, 但是方法緩存中的sayHello對(duì)應(yīng)的imp并沒(méi)有發(fā)生改變薇宠。
我們知道, Objective-C通過(guò)方法緩存來(lái)提升方法調(diào)用速度, 緩存中找不到, 再去類對(duì)象的方法列表中去查找, 調(diào)用后便加入到方法緩存中, 這點(diǎn)也可以通過(guò)objc_msgSend的源碼來(lái)確認(rèn), objc_msgSend的源碼是匯編實(shí)現(xiàn)的, 即使看不懂匯編也沒(méi)事, 通過(guò)旁邊的注釋, 大概來(lái)看出來(lái)調(diào)用流程: 在方法緩存中尋找, 找到直接返回方法IMP, 否則調(diào)用__objc_msgSend_uncached, 去方法列表中查找。
/// objc_msgSend, 除去一些nil驗(yàn)證檢測(cè)后, 調(diào)用 CacheLookup LOOKUP
LLookup_GetIsaDone:
CacheLookup LOOKUP // returns imp
/// CacheLookup
.macro CacheHit
.if $0 == NORMAL
MESSENGER_END_FAST
br x17 // call imp
.elseif $0 == GETIMP
mov x0, x17 // return imp
ret
.elseif $0 == LOOKUP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz x9, LGetImpMiss
.elseif $0 == NORMAL
cbz x9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz x9, __objc_msgLookup_uncached
作怪到底--自己修改方法緩存對(duì)應(yīng)的imp
既然都到這里, 不妨嘗試自己去修改方法緩存中對(duì)應(yīng)imp艰额。其實(shí)從Objective-C Runtime層面來(lái)說(shuō), 對(duì)象澄港、方法、block等都是以結(jié)構(gòu)體的形式存在內(nèi)存中, 想去改對(duì)象的屬性, 方法的實(shí)現(xiàn)會(huì)是block的實(shí)現(xiàn), 都是要對(duì)它們的內(nèi)存布局有所了解柄沮。
前面的分析把疑惑基本解決了, 現(xiàn)在要做的就比較簡(jiǎn)單是了, 只需要將方法緩存以及其他需要用到的結(jié)構(gòu)體如對(duì)象回梧、方法等的結(jié)構(gòu)抽出來(lái), 自己聲明一個(gè)結(jié)構(gòu)體, 把需要用上的成員變量和方法帶上即可, 不需要用上可以直接刪除废岂。
struct bucket_t {
cache_key_t _key;
IMP _imp;
};
struct cache_t {
bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
public:
struct bucket_t *buckets();
mask_t mask();
mask_t occupied();
void incrementOccupied();
void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
void initializeToEmpty();
mask_t capacity();
bool isConstantEmptyCache();
bool canBeFreed();
static size_t bytesForCapacity(uint32_t cap);
static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
void expand();
void reallocate(mask_t oldCapacity, mask_t newCapacity);
struct bucket_t * find(cache_key_t key, id receiver);
static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
};
接下來(lái), 只需要將load方法中添加一點(diǎn)代碼進(jìn)行驗(yàn)證即可:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];
// Class aClass = self; // 不給self發(fā)消息, cache不會(huì)生成, 結(jié)果就和我們的預(yù)想一樣
SEL originalSelector = @selector(sayHello);
Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
// method_setImplementation(originalMethod, (IMP)hookedSayHello); //Runtime API, 可以發(fā)現(xiàn)cache被清除了, 可以打開(kāi)注釋, 驗(yàn)證結(jié)果
struct method_t *method = (struct method_t *)originalMethod;
method->imp = (IMP)hookedSayHello;
// cache問(wèn)題, 因?yàn)?已經(jīng)和 imp緩存了, 直接會(huì)調(diào)用原來(lái)方法
// method_setImplementation 中有個(gè)函數(shù) flushCache -> cache_erase_nolock, 會(huì)重新設(shè)置 cache
// 修改cache
struct my_objc_class *clz = (__bridge struct my_objc_class *)aClass;
uint32_t cacheCount = clz->cache.capacity();
NSLog(@"cacheCount : %d", cacheCount);
for (NSInteger i = 0; i < cacheCount; i++) {
char *key = (char *)((clz->cache._buckets + i)->_key);
// 這里設(shè)置一下
printf("%ld - %s\n", i, key); // 測(cè)試
if (key) {
NSString *selectorName = [NSString stringWithUTF8String:key];
if ([selectorName isEqualToString:@"sayHello"]) {
(clz->cache._buckets + i)->_imp = (IMP)hookedSayHello;
}
}
}
});
}
發(fā)現(xiàn)打印的確實(shí)是我們希望的實(shí)現(xiàn), 當(dāng)然這里只是一個(gè)簡(jiǎn)單的類, 對(duì)于有子類的情況沒(méi)做驗(yàn)證, 如果有子類的情況下, 還是比較復(fù)雜的, 對(duì)于子類是否實(shí)現(xiàn)了該方法也是有區(qū)別的, 這也許也是 method_setImplementation 直接暴力地將當(dāng)前類和子類的緩存都清空的原因吧!
總結(jié)
通過(guò)本次探索, 對(duì)方法調(diào)用以及底層的一些流程有了一定的了解, 雖然對(duì)于開(kāi)發(fā)確實(shí)沒(méi)太大幫助, 但對(duì)于理解底層機(jī)制有一定幫助。在日常學(xué)習(xí)中, 可以配合源碼, 通過(guò)自己的嘗試, 一定可以對(duì)相關(guān)知識(shí)有更深刻地理解狱意。