在實際開發場景中,有時候我們需要在調用系統方法,或者某個類的方法的時候,增加自己的一些邏輯操作,這時候可以采用 方法交換 的方式去實現這個需求。這種方式也被稱為 黑魔法(Method swizzling)或者 hook,網上也有很多這方面的文檔解釋,在這里主要是記錄一下,hook
的時候遇到的問題。
場景一:對某個類自身的方法進行 hook 操作
什么意思呢?舉個例子,NSString
這個類,有一個 substringToIndex:
方法,這個方法是在 NSString+NSStringExtensionMethods
這樣的一個分類里面。
需求:在使用 substringToIndex:
方法的時候,希望能在里面增加一些邏輯判斷,比如判斷當前傳入的 index
是否在當前字符串范圍之內。
NSString *string = @"abcd";
NSLog(@"%@", [string substringToIndex:10]);
這里傳入的 10,字符串沒有這么長的長度,如果直接使用系統的方法,程序運行起來,立馬發生閃退。
下面,進行 hook
操作
-
給
NSString
新建一個分類,并在 load 方法中進行hook
操作+ (void)load { // 系統方法 Method system_method = class_getInstanceMethod([self class], @selector(substringToIndex:)); // 將要替換系統方法 Method my_method = class_getInstanceMethod([self class], @selector(yxc_substringToIndex:)); // 進行交換 method_exchangeImplementations(system_method, my_method); } - (NSString *)yxc_substringToIndex:(NSUInteger)to { // 判斷傳入的數值是否大于當前字符串的范圍,如果大于的話,取當前字符串的最大長度 if (to > self.length) { to = self.length; } return [self yxc_substringToIndex:to]; }
這樣就
hook
完成了,查看結果:substringToIndex進行 hook 之后結果
這樣看起來,hook
操作很簡單,沒有什么問題,但是這只是一種情況。
場景二:對某個類的父類或者基類的方法進行 hook 操作
下面,對 init
這個方法進行 hook
操作。
-
因為
NSString
特殊性,在這里不再用NSString
進行舉例了,新建一個Person
類,繼承于NSObject
;再給Person
類創建一個分類,然后按照上面的方式對Person
的init
方法進行hook
。+ (void)load { Class cls = [self class]; Method system_method = class_getInstanceMethod(cls, @selector(init)); Method my_method = class_getInstanceMethod(cls, @selector(yxc_init)); method_exchangeImplementations(system_method, my_method); } - (instancetype)yxc_init { NSLog(@"%s", __func__); return [self yxc_init]; }
通過
alloc
和init
創建一個Person
對象,并未出現異常。-
緊接著創建一個
NSObject
對象,這時候問題出現了,程序進入死循環,并且報yxc_init:
方法找不到。hook init方法報錯分析:
-
init
方法并不是Person
類本身的實例(對象)方法,而是父類NSObject
的方法。由于Person
本身沒有該方法,所以class_getInstanceMethod
獲取到的方法是通過Person
的superclass
指針從NSObject
類中獲取到了init
這個方法。 -
method_exchangeImplementations
操作將NSObject
的init
方法的實現與Person
類的yxc_init
方法的實現進行互換了,這時候調用init
方法實際上是調用了yxc_init
方法。 - 創建一個
Person
對象時,調用init
方法,運行時會去查找yxc_init
的實現,因為yxc_init
方法是Person
自身的方法,所以查找到了直接調用。(消息發送機制) - 而創建一個
NSObject
對象時,調用init
方法,運行時去查找yxc_init
方法的時候,NSObject
是沒有這個方法,這個方法存在于Person
類中,所以查找完畢,還是找不到這個方法,就拋異常了。
-
正確的 hook
做法是,先將 init
方法添加到 Person
類中,如果這個類當前有這個方法(而不是父類),則不添加,直接 exchange
,否則添加了 init
方法,然后再將 yxc_init
方法的實現設置成 init
方法的實現。
+ (void)load {
Class cls = [self class];
// 1. 獲取到父類的 init 方法
Method system_method = class_getInstanceMethod(cls, @selector(init));
// 2. 獲取到當前類的 yxc_init 方法
Method my_method = class_getInstanceMethod(cls, @selector(yxc_init));
// 3. 先將 init 方法添加到當前類中,并且將 yxc_init 作為 init 方法的實現
BOOL addSuccess = class_addMethod(cls,
@selector(init),
method_getImplementation(my_method),
method_getTypeEncoding(my_method));
// 4. 判斷 init 添加到當前類中是否成功
if (addSuccess) {
// 4.1 方法添加成功,則意味著當前類在添加之前并沒有 init 方法,添加成功后就進行方法替換,將 init 方法的實現替換成 yxc_init 方法的實現
class_replaceMethod(cls,
@selector(yxc_init),
method_getImplementation(system_method),
method_getTypeEncoding(system_method));
} else {
// 4.2 方法添加失敗,說明當前類已存在該方法,直接進行方法交換
method_exchangeImplementations(system_method, my_method);
}
}
- (instancetype)yxc_init {
NSLog(@"%s", __func__);
return [self yxc_init];
}
運行結果顯示:
通過這樣的方式進行對 父類或者基類 方法的 hook
,最終沒有發現其他異常,以此記錄。
最后封裝一下 hook 邏輯操作
/// hook 方法
/// @param cls 類
/// @param originSelector 將要 hook 掉的方法
/// @param swizzledSelector 新的方法
/// @param clsMethod 類方法
+ (void)hookMethod:(Class)cls originSelector:(SEL)originSelector swizzledSelector:(SEL)swizzledSelector classMethod:(BOOL)clsMethod {
Method origin_method;
Method swizzled_method;
if (clsMethod) {
// 類方法
origin_method = class_getClassMethod(cls, originSelector);
swizzled_method = class_getClassMethod(cls, swizzledSelector);
} else {
// 實例(對象)方法
origin_method = class_getInstanceMethod(cls, originSelector);
swizzled_method = class_getInstanceMethod(cls, swizzledSelector);
}
BOOL addSuccess = class_addMethod(cls,
originSelector,
method_getImplementation(swizzled_method),
method_getTypeEncoding(swizzled_method)
);
if (addSuccess) {
class_replaceMethod(cls,
swizzledSelector,
method_getImplementation(origin_method),
method_getTypeEncoding(origin_method)
);
} else {
method_exchangeImplementations(origin_method, swizzled_method);
}
}
類簇(Class Clusters)
Class Clusters
(類簇)是抽象工廠模式在iOS下的一種實現,眾多常用類,如 NSString
,NSArray
,NSDictionary
,NSNumber
都運作在這一模式下,它是接口簡單性和擴展性的權衡體現,在我們完全不知情的情況下,偷偷隱藏了很多具體的實現類,只暴露出簡單的接口。
下面對 NSArray
進行類簇講解
系統會創建 __NSPlaceholderArray
、 __NSSingleObjectArrayI
、 __NSArray0
、 __NSArrayM
等一些類簇,下面對這些類簇進行 hook
操作
+ (void)load {
[self hookOriginClass:NSClassFromString(@"__NSPlaceholderArray") currentClass:[NSArray class] originSelector:@selector(initWithObjects:count:) swizzledSelector:@selector(yxc_initWithObjects:count:) classMethod:NO];
[self hookOriginClass:NSClassFromString(@"__NSSingleObjectArrayI") currentClass:[NSArray class] originSelector:@selector(objectAtIndex:) swizzledSelector:@selector(yxc_objectAtIndex:) classMethod:NO];
[self hookOriginClass:NSClassFromString(@"__NSArray0") currentClass:[NSArray class] originSelector:@selector(objectAtIndex:) swizzledSelector:@selector(yxc_objectAtIndex1:) classMethod:NO];
[self hookOriginClass:NSClassFromString(@"__NSArrayM") currentClass:[NSArray class] originSelector:@selector(objectAtIndexedSubscript:) swizzledSelector:@selector(yxc_objectAtIndexedSubscript:) classMethod:NO];
}
這樣就對數組中的一些方法進行 hook 完了,而且也并沒有什么問題。
到這里,就有一個疑問:在這里替換同一個 SEL
為 objectAtIndex:
,而這個方法是屬于 NSArray
這個類,為什么這里替換了兩次,彼此都沒有影響到,按理來說根據同一個 SEL
獲取到的 IMP
進行 replace
或者 exchange
,那么最后生效的應該是最后一次進行 hook
的方法實現,但是經過發現,沒有受影響。
首先類簇是需要繼承于原來那個類,在原來那個類的基礎上衍生了許多類出來,下面我們用代碼證明這一點。
Class __NSArrayM = NSClassFromString(@"__NSArrayM");
Class __NSArray0 = NSClassFromString(@"__NSArray0");
Class __NSSingleObjectArrayI = NSClassFromString(@"__NSSingleObjectArrayI");
Class __NSPlaceholderArray = NSClassFromString(@"__NSPlaceholderArray");
NSLog(@"__NSArrayM -> superclass : %@", class_getSuperclass(__NSArrayM));
NSLog(@"__NSArray0 -> superclass : %@", class_getSuperclass(__NSArray0));
NSLog(@"__NSSingleObjectArrayI -> superclass : %@", class_getSuperclass(__NSSingleObjectArrayI));
NSLog(@"__NSPlaceholderArray -> superclass : %@", class_getSuperclass(__NSPlaceholderArray));
輸出結果:
既然 SEL 是 NSArray 的方法,為什么在 hook 的時候,能 hook 到每個類簇對應的想法?
猜想:是不是每個類簇,都實現了 objectAtIndex:
這個方法,導致根據 SEL
獲取到方法實現是不相同的
下面進行驗證這個猜想
+ (void)load {
[self hookOriginClass:NSClassFromString(@"__NSPlaceholderArray") currentClass:[NSArray class] originSelector:@selector(initWithObjects:count:) swizzledSelector:@selector(yxc_initWithObjects:count:) classMethod:NO];
NSLog(@"交換前");
[self logInfo];
[self hookOriginClass:NSClassFromString(@"__NSSingleObjectArrayI") currentClass:[NSArray class] originSelector:@selector(objectAtIndex:) swizzledSelector:@selector(yxc_objectAtIndex:) classMethod:NO];
NSLog(@"__NSSingleObjectArrayI交換后");
[self logInfo];
[self hookOriginClass:NSClassFromString(@"__NSArray0") currentClass:[NSArray class] originSelector:@selector(objectAtIndex:) swizzledSelector:@selector(yxc_objectAtIndex1:) classMethod:NO];
NSLog(@"__NSArray0交換后");
[self logInfo];
[self hookOriginClass:NSClassFromString(@"__NSArrayM") currentClass:[NSArray class] originSelector:@selector(objectAtIndexedSubscript:) swizzledSelector:@selector(yxc_objectAtIndexedSubscript:) classMethod:NO];
}
+ (void)logInfo {
Class singleObjectCls = NSClassFromString(@"__NSSingleObjectArrayI");
Class __NSArray0Cls = NSClassFromString(@"__NSArray0");
Class currentCls = [self class];
SEL selector = @selector(objectAtIndex:);
Method singleObjectClsMethod = class_getInstanceMethod(singleObjectCls, selector);
Method __NSArray0ClsMethod = class_getInstanceMethod(__NSArray0Cls, selector);
Method currentMethod = class_getInstanceMethod(currentCls, selector);
IMP singleObjectClsMethodIMP = method_getImplementation(singleObjectClsMethod);
IMP __NSArray0ClsMethodIMP = method_getImplementation(__NSArray0ClsMethod);
IMP currentIMP = method_getImplementation(currentMethod);
NSLog(@"selector : %p, singleObjectClsMethod : %p, __NSArray0ClsMethod : %p, currentMethod : %p, singleObjectClsMethodIMP : %p, __NSArray0ClsMethodIMP : %p, currentIMP : %p",
selector, singleObjectClsMethod, __NSArray0ClsMethod, currentMethod, singleObjectClsMethodIMP, __NSArray0ClsMethodIMP, currentIMP);
}
以上代碼,在 hook
objectAtIndex:
方法之前和 hook
完一個、兩個之后對 SEL
、class
、Method
、IMP
信息輸出
2020-11-02 20:02:42.598040+0800 Block[32615:646190] 交換前==================
2020-11-02 20:02:42.598612+0800 Block[32615:646190] selector : 0x7fff7256d44e, singleObjectClsMethod : 0x7fff85fe75c0, __NSArray0ClsMethod : 0x7fff85fcd260, currentMethod : 0x7fff85fda938, singleObjectClsMethodIMP : 0x7fff2e31daf6, __NSArray0ClsMethodIMP : 0x7fff2e41e13b, currentIMP : 0x7fff2e4629fe
2020-11-02 20:02:42.598878+0800 Block[32615:646190] __NSSingleObjectArrayI交換后======================
2020-11-02 20:02:42.598970+0800 Block[32615:646190] selector : 0x7fff7256d44e, singleObjectClsMethod : 0x7fff85fe75c0, __NSArray0ClsMethod : 0x7fff85fcd260, currentMethod : 0x7fff85fda938, singleObjectClsMethodIMP : 0x100003750, __NSArray0ClsMethodIMP : 0x7fff2e41e13b, currentIMP : 0x7fff2e4629fe
2020-11-02 20:02:42.599166+0800 Block[32615:646190] __NSArray0交換后===================
2020-11-02 20:02:42.599275+0800 Block[32615:646190] selector : 0x7fff7256d44e, singleObjectClsMethod : 0x7fff85fe75c0, __NSArray0ClsMethod : 0x7fff85fcd260, currentMethod : 0x7fff85fda938, singleObjectClsMethodIMP : 0x100003750, __NSArray0ClsMethodIMP : 0x1000037d0, currentIMP : 0x7fff2e4629fe
根據輸出的地址,可以看出根據不同的類簇獲取到的 Method 的方法結構體地址也是不同一個,還有方法實現的地址也是不同一塊存儲空間,那就證明了猜想,根據 SEL 獲取到的 Method 和 IMP 不同一個,可能是在每個類簇內部對父類NSArray 的 objectAtIndex:
重新實現了一下,導致獲取到的并不是同一個。
為了驗證是否子類重寫了父類的方法獲取到的并不是同一個(原理來講是不同一個的,下面用代碼來驗證這個想法)
新建一個 Person
類,并且聲明一個 test
對象方法并實現,然后創建一個 Student
類,繼承于 Person
類,先不重寫父類的 test
方法。
Student
未重寫父類 Person
的 test
方法,通過各自獲取到的 Method
和 IMP
的地址都是同一個
下面 Student
進行重寫 test
方法
這時候發現,通過各自獲取 Method
和 IMP
的地址已經不一樣了
這就驗證了以上的猜想,在類簇內部中,會對父類的一些方法進行重寫。這就導致可能某一個方法,在一個類簇中已經進行了 hook,但是可能還是會出現方法名相同,但是類名不一樣的方法報錯,就像上面的 objectAtIndex:
方法一樣,如果只是對 __NSSingleObjectArrayI
進行了替換或者交換方法操作,但是并沒有對 __NSArray0
進行同樣的操作,那么還是會出現索引超出界面,沒有達到預防的效果。
class_addMethod
函數官方文檔描述
/**
* Adds a new method to a class with a given name and implementation.
*
* @param cls The class to which to add a method.
* @param name A selector that specifies the name of the method being added.
* @param imp A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd.
* @param types An array of characters that describe the types of the arguments to the method.
*
* @return YES if the method was added successfully, otherwise NO
* (for example, the class already contains a method implementation with that name).
*
* @note class_addMethod will add an override of a superclass's implementation,
* but will not replace an existing implementation in this class.
* To change an existing implementation, use method_setImplementation.
*/
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
下面對 class_addMethod
進行源碼分析
/// cls 類名
/// name 方法名
/// imp 方法實現
/// types 方法簽名
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
// 沒有傳入 類名 直接返回 NO
if (!cls) return NO;
mutex_locker_t lock(runtimeLock);
// 開始添加方法,對返回的結果進行取反,這里返回的是一個 IMP 類型的結果
return ! addMethod(cls, name, imp, types ?: "", NO);
}
/// cls 類名
/// name 方法名
/// imp 方法實現
/// types 方法簽名
/// replace 是否直接替換,這里傳入的是 NO
static IMP addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
IMP result = nil;
runtimeLock.assertLocked();
checkIsKnownClass(cls);
ASSERT(types);
ASSERT(cls->isRealized());
method_t *m;
// 查找該方法
if ((m = getMethodNoSuper_nolock(cls, name))) {
// already exists 已經存在該方法
if (!replace) {
// 當 replace 為 NO 時,直接返回該方法的實現
result = m->imp;
} else {
// 當 replace 為 YES 時,通過 _method_setImplementation,直接將方法進行替換
result = _method_setImplementation(cls, m, imp);
}
} else {
// 該方法不存在,對傳入的類進行動態添加方法
auto rwe = cls->data()->extAllocIfNeeded();
// fixme optimize
// 創建一個方法列表
method_list_t *newlist;
// 分配內存,并設置好 method_list_t 的值
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);
// 開始合并
rwe->methods.attachLists(&newlist, 1);
flushCaches(cls);
result = nil;
}
return result;
}
/// cls 類名
/// sel 方法名
static method_t *getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
// for 循環遍歷,根據傳入的 sel 方法進行查找當前類是否有該方法
auto const methods = cls->data()->methods();
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
// <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
// caller of search_method_list, inlining it turns
// getMethodNoSuper_nolock into a frame-less function and eliminates
// any store from this codepath.
// 查找傳入的方法列表是否有 sel 方法
method_t *m = search_method_list_inline(*mlists, sel);
// 找到了返回
if (m) return m;
}
return nil;
}
ALWAYS_INLINE static method_t *search_method_list_inline(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
// 根據不同方式進行查找
if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
// 有序查找
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
// 無序查找
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
#if DEBUG
// sanity-check negative results
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
#endif
return nil;
}
// 二分查找方法
ALWAYS_INLINE static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
ASSERT(list);
const method_t * const first = &list->first;
const method_t *base = first;
const method_t *probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)probe->name;
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
return (method_t *)probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
在使用 class_addMethod
添加方法時,只會在當前的類進行查找方法,并不會像 消息機制
那樣在當前類找不到,就去父類查找。在當前類查找不到,就在當前類動態添加方法并設置實現;如果查找到了就不做操作,返回查找到的方法實現,然后通過取反操作,返回添加結果。
class_replaceMethod
函數官方文檔描述
/**
* Replaces the implementation of a method for a given class.
*
* @param cls The class you want to modify.
* @param name A selector that identifies the method whose implementation you want to replace.
* @param imp The new implementation for the method identified by name for the class identified by cls.
* @param types An array of characters that describe the types of the arguments to the method.
* Since the function must take at least two arguments—self and _cmd, the second and third characters
* must be “@:” (the first character is the return type).
*
* @return The previous implementation of the method identified by \e name for the class identified by \e cls.
*
* @note This function behaves in two different ways:
* - If the method identified by \e name does not yet exist, it is added as if \c class_addMethod were called.
* The type encoding specified by \e types is used as given.
* - If the method identified by \e name does exist, its \c IMP is replaced as if \c method_setImplementation were called.
* The type encoding specified by \e types is ignored.
*/
OBJC_EXPORT IMP _Nullable
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
查看 `` 源碼
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
{
if (!cls) return nil;
mutex_locker_t lock(runtimeLock);
// 調用 addMethod 方法,但是此時 addMethod 方法中的 replace 參數傳入的是 YES
return addMethod(cls, name, imp, types ?: "", YES);
}
通過上面的 addMethod
源碼分析
- 當查找到方法已存在,直接通過
_method_setImplementation
方法將傳入的方法實現,設置為查找目標方法的實現 - 當查找到方法不存在,動態添加到當前類中
下面查看一下 _method_setImplementation
方法的實現原理
static IMP _method_setImplementation(Class cls, method_t *m, IMP imp)
{
runtimeLock.assertLocked();
if (!m) return nil;
if (!imp) return nil;
// 將舊的實現取出
IMP old = m->imp;
// 直接將新的實現方法設置到 method_t 的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);
adjustCustomFlagsForMethodChange(cls, m);
// 返回舊的實現
return old;
}
查看 method_exchangeImplementations
的方法實現原理
void method_exchangeImplementations(Method m1, Method m2) {
if (!m1 || !m2) return;
mutex_locker_t lock(runtimeLock);
// 直接將傳入的兩個 Method 方法實現進行互換
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);
adjustCustomFlagsForMethodChange(nil, m1);
adjustCustomFlagsForMethodChange(nil, m2);
}
查看 class_getInstanceMethod
方法底層實現原理
Method class_getInstanceMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
// This deliberately avoids +initialize because it historically did so.
// This implementation is a bit weird because it's the only place that
// wants a Method instead of an IMP.
#warning fixme build and search caches
// Search method lists, try method resolver, etc.
lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
#warning fixme build and search caches
return _class_getMethod(cls, sel);
}
static Method _class_getMethod(Class cls, SEL sel)
{
mutex_locker_t lock(runtimeLock);
return getMethod_nolock(cls, sel);
}
static method_t *getMethod_nolock(Class cls, SEL sel)
{
method_t *m = nil;
runtimeLock.assertLocked();
// fixme nil cls?
// fixme nil sel?
ASSERT(cls->isRealized());
// 遍歷當前類是否有該方法,如果沒有就遍歷父類
while (cls && ((m = getMethodNoSuper_nolock(cls, sel))) == nil) {
cls = cls->superclass;
}
return m;
}
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
// 在這里傳入的對象是元類對象
return class_getInstanceMethod(cls->getMeta(), sel);
}