iOS 修改私有方法總結(jié)
1.Category添加方法
原理:
通過(guò)Category添加方法婉宰,優(yōu)先于類中相同方法的特點(diǎn)貌笨,進(jìn)行私有函數(shù)修改耳奕。
category中的方法位于類方法列表的前面衅斩,在進(jìn)行方法查找時(shí)盆顾,會(huì)先找到category中的方法,所以會(huì)優(yōu)先執(zhí)行Category中方法的目的矛渴,達(dá)到攔截原有私有方法的目的椎扬。
副作用:
- 不能直接調(diào)用 super,除了特殊的 + load 方法外具温,其他分類中的方法蚕涤,在被調(diào)用之前,并不會(huì)去調(diào)用原始方法铣猩,這將導(dǎo)致外部在使用這個(gè)類的時(shí)候揖铜,永遠(yuǎn)調(diào)用不到原始的實(shí)現(xiàn),尤其是一些系統(tǒng)類內(nèi)部互相調(diào)用的情況达皿,將會(huì)無(wú)法預(yù)估天吓;
- 當(dāng)項(xiàng)目中存在多個(gè)分類復(fù)寫了同一個(gè)私有方法后,最終調(diào)用哪個(gè)分類中的方法將由編譯順序所決定峦椰,這又充滿了不確定性龄寞,因?yàn)榉诸愖陨聿⒉恢肋@個(gè)私有方法是否已經(jīng)被其他分類所復(fù)寫;
- 一旦通過(guò)分類復(fù)寫私有方法汤功,它的影響將會(huì)是全局的物邑,所有用到這個(gè)類的地方,都將變成分類調(diào)用
2.Method Swizzling
原理:
通過(guò)使用runtime方法,交換函數(shù)imp地址色解,在調(diào)用原函數(shù)是茂嗓,就變成調(diào)用新函數(shù),達(dá)到修改的目的科阎。
示例代碼:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// When swizzling a class method, use the following:
// Class class = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(class, originalSelector);
// Method swizzledMethod = class_getClassMethod(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);
}
});
}
副作用:
- 只能執(zhí)行一次述吸,多次執(zhí)行會(huì)有問(wèn)題。
- 要避免hook掉函數(shù)的父函數(shù)锣笨。
- hook方法中含有_cmd這種函數(shù)蝌矛,會(huì)出現(xiàn)未知問(wèn)題。
- 不支持實(shí)例對(duì)象的hook
使用場(chǎng)景:
3.objc_msgForward消息轉(zhuǎn)發(fā)
原理:
在 OC 中票唆,向一個(gè)對(duì)象發(fā)送消息后朴读,最終會(huì)全部轉(zhuǎn)化到 objc_msgSend
中屹徘,繼而在方法列表中通過(guò) SEL
開始查找對(duì)應(yīng)的 IMP
走趋,如果沒有查到對(duì)應(yīng)的方法,則 IMP
會(huì)返回 objc_msgForward / objc_msgForward_stret
噪伊,從而觸發(fā)未知消息轉(zhuǎn)發(fā)流程簿煌,即 resolveInstanceMethod:
、forwardingTargetForSelector:
和 forwardInvocation:
鉴吹。
Aspects主要是利用了forwardInvocation
進(jìn)行轉(zhuǎn)發(fā)姨伟,Aspects
其實(shí)利用和kvo
類似的原理,通過(guò)動(dòng)態(tài)創(chuàng)建子類的方式豆励,把對(duì)應(yīng)的對(duì)象isa
指針指向創(chuàng)建的子類夺荒,然后把子類的forwardInvocation
的IMP替成__ASPECTS_ARE_BEING_CALLED__
,假設(shè)要hook
的方法名XX
,在子類中添加一個(gè)Aspects_XX
的方法良蒸,然后將Aspects_XX
的IMP
指向原來(lái)的XX
方法的IMP
技扼,這樣方便后面調(diào)用原始的方法,再把要hook
的方法XX
的IMP
指向_objc_msgForward
嫩痰,這樣就進(jìn)入了消息轉(zhuǎn)發(fā)流程剿吻,而forwardInvocation
的IMP
被替換成了__ASPECTS_ARE_BEING_CALLED__
,這樣就會(huì)進(jìn)入__ASPECTS_ARE_BEING_CALLED__
進(jìn)行攔截處理串纺,這樣整個(gè)流程大概結(jié)束丽旅。
示例代碼:
[UIViewController aspect_hookSelector:NSSelectorFromString(@"dealloc") withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info) {
NSLog(@"Controller is about to be deallocated: %@", [info instance]);
} error:NULL];
副作用:
- Aspect 中使用了 OSSpinLockLock 來(lái)保證線程安全,但自旋鎖已經(jīng)被發(fā)現(xiàn)為存在優(yōu)先級(jí)調(diào)度的問(wèn)題
- Aspect 中利用了 sub class 來(lái)實(shí)現(xiàn)消息轉(zhuǎn)發(fā)纺棺,但這種繼承鏈消息轉(zhuǎn)發(fā)存在缺陷榄笙,子類和父類如果同時(shí) hook 一個(gè)方法,便會(huì)造成死循環(huán)MessageThrottle Safety
4.基于libbfi動(dòng)態(tài)調(diào)用C函數(shù)
原理:
使用libffi
中的ffi_closure_alloc
構(gòu)造與原方法參數(shù)一致的"函數(shù)" -- stingerIMP
祷蝌,以替換原方法函數(shù)指針茅撞;此外,生成了原方法和Block
的調(diào)用的參數(shù)模板cif
和blockCif
。方法調(diào)用時(shí)乡翅,最終會(huì)調(diào)用到void _st_ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata)
, 在該函數(shù)內(nèi)鳞疲,可獲取到方法調(diào)用的所有參數(shù)、返回值位置蠕蚜,主要通過(guò)ffi_call根據(jù)cif調(diào)用原方法實(shí)現(xiàn)和切面block尚洽。AOP庫(kù) Stinger和BlockHook
就是使用libbfi做的。
示例代碼:
BlockHook使用
void(^block)(void) = ^() {
NSLog(@"This is global block!");
};
[block block_hookWithMode:BlockHookModeAfter usingBlock:^(BHToken *token) {
NSLog(@"After global block!");
}];
使用場(chǎng)景:
目前使用場(chǎng)景較多靶累,執(zhí)行效率較高腺毫,Stinger和BlockHook都是基于libbfi進(jìn)行開發(fā)的。
5.基于橋的全量方法 Hook 方案 - TrampolineHook
原理:
把一個(gè)我們要替換的原方法 IMP A 取出來(lái)挣柬,保存起來(lái)潮酒。
給這個(gè)原方法塞一個(gè)動(dòng)態(tài)分配的可執(zhí)行地址 B。
當(dāng)執(zhí)行這個(gè)原方法的時(shí)候邪蛔,會(huì)跳轉(zhuǎn)到 可執(zhí)行地址 B急黎。
這個(gè) B 經(jīng)過(guò)一段簡(jiǎn)短的運(yùn)算操作,可以獲取到原先保存的 IMP A侧到。
在跳轉(zhuǎn)回 IMP A 之前勃教,統(tǒng)一攔截函數(shù)先做些事情,比如檢查是不是主線程調(diào)用之類的匠抗。但這里的這些操作時(shí)使用匯編來(lái)進(jìn)行故源,執(zhí)行效率更高。
示例代碼:
THInterceptor *interceptor = [THInterceptor sharedInterceptorWithFunction:(IMP)myInterceptor];
Method m = class_getInstanceMethod([UIView class], @selector(initWithFrame:));
IMP imp = method_getImplementation(m);
THInterceptorResult *interceptorResult = [interceptor interceptFunction:imp];
if (interceptorResult.state == THInterceptStateSuccess) {
method_setImplementation(m, interceptorResult.replacedAddress); // 設(shè)置替換的地址
}
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)]; // 執(zhí)行到這一行時(shí)汞贸,會(huì)調(diào)用 myInterceptor 方法
使用場(chǎng)景:
目前作者只編寫arm64匯編代碼绳军,還不夠完善,不能在線上使用
參考文章: