Objective-C 如何hook系统私有类?
Tip:在读这篇文章之前,最好对 SEL、IMP 有一定的了解。否则很容易懵的,得不偿失哦~
众所周知,OC中Runtime黑魔法的强大!也叫做Method Swizzling,在很多博客中,给了个例子,比如你要hook UIViewController的viewDidAppear:animated方法。通常就是创建一个UIViewController的Category,然后在+load方法中,做以下处理:
+ (void)load {
[super load];
Method didFromMethod = class_getInstanceMethod([self class], @selector(viewDidAppear:));
Method didToMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidAppear:));
method_exchangeImplementations(didFromMethod, didToMethod);
}
- (void)swizzlingViewDidAppear:(BOOL)animated {
NSString *str = NSStringFromClass(self.class);
if(![str containsString:@"UI"]){
NSLog(@"统计打点: %@", str);
}
[self swizzlingViewDidAppear:YES];
}
这样做有没有问题呢?我的回答:当然有!稍后做出解释。
好,进入今天的主题,因为UIViewController是public的,我们可以创建Category,可以继承。但私有类怎么办呢?本来我是想以“UITableViewIndex”这个类为例,这个类是UITableView右边的索引。但是为了可以清晰的看到hook之后方法原实现有没有执行,因此我们用自定义的类来实现。此方法没有局限性,强烈推荐。
创建两个测试用的类:
- MyClass
//------- .h -------
#import <Foundation/Foundation.h>
@interface MyClass : NSObject
- (void)eat;
@end
//------- .m -------
#import "MyClass.h"
@implementation MyClass
- (void)eat {
NSLog(@"我喜欢吃牛排 - %@", self.class);
}
@end
- SecondClass,继承于MyClass,是否重写-eat方法,分为两种情况
//------- .h -------
#import "MyClass.h"
@interface SecondClass : MyClass
@end
//------- .m -------
#import "SecondClass.h"
@implementation SecondClass
//这里待会儿要分两种情况测试,要把下面的注释掉
- (void)eat {
[super eat];
NSLog(@"黑胡椒的");
}
@end
我们创建一个工具类来专门处理。继承于NSObject,暂且命名为HookTool,在合适的地方,引入HookTool头文件。然后重写它的+load方法,重点就在这里:
#import "HookTool.h"
#import <objc/runtime.h>
@implementation HookTool
+ (void)load {
[super load];
//步骤一
Method originEat = class_getInstanceMethod(NSClassFromString(@"SecondClass"), NSSelectorFromString(@"eat"));
//步骤二
Method swizzlingEat = class_getInstanceMethod([self class], @selector(swizzlingEat));
//步骤三
BOOL a = class_addMethod(NSClassFromString(@"SecondClass"), @selector(swizzlingEat), method_getImplementation(originEat), method_getTypeEncoding(originEat));
//步骤四
BOOL c = class_addMethod(NSClassFromString(@"SecondClass"), NSSelectorFromString(@"eat"), method_getImplementation(swizzlingEat), method_getTypeEncoding(swizzlingEat));
NSLog(@"结果:%d, %d", a, c);
//步骤五
if (!c) {
method_exchangeImplementations(originEat, swizzlingEat);
}
}
- (void)swizzlingEat {
//步骤六
[self swizzlingEat];
NSLog(@"再喝点红酒。");
}
@end
- 步骤一:这里为什么用NSClassFromString,以及NSSelectorFromString?假装是私有类嘛!这还要问?
首先获取到-eat方法本身的实现,不管SecondClass有没有重写,都可以获取到的,因为本身的实现在SecondClass中,所以注意第一个参数。
步骤二:然后获取HookTool中,我们要hook的方法。实现在本类中,所以注意第一个参数。
重点在这里,精髓所在!这里容易懵,打起精神来!
- 步骤三:尝试往SecondClass中添加一个方法,方法名为-swizzlingEat,而其实现是-eat本身的实现,a的结果一定是YES,即添加成功的。如果不成功,证明SecondClass中已经存在同名的方法,改名吧。
为什么要向SecondClass添加-swizzlingEat呢?因为我们要让它本身的实现执行的,[self swizzlingEat],这里self是SecondClass(步骤六会提到),否则就是unrecognizeSelectorSentToInstance…..找不到方法了。
步骤四:尝试往SecondClass中,添加一个方法,方法名为-eat,而其实现是这个.m中的-swizzlingEat,c一定是NO吗?这里分两种情况:
- 如果SecondClass重写了-eat方法,那么此时c就是NO
- 如果SecondClass没有重写-eat方法,那么此时c就是YES
为什么呢?我们来看看这个说明
/**
* 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);
看看note里面的,class_addMethod这个函数,将会重写方法,但是如果已经被重写,那么将不会替换。我们要利用的就是这个特性。
步骤五:所以,是否交换方法的实现,要取决于条件的。
- 如果SecondClass重写了-eat方法,那么此时c为NO,我们这个时候就要交换方法,因为我们要确保SecondClass的-eat本身的实现要走一遍的。这才是成功的hook,其实这个时候的操作,就跟其它博客提到的用Category来hook的原理是一样的。
- 如果SecondClass没有重写-eat方法,那么此时c为YES,我们这个时候什么都不用做,因为之前BOOL c = class_addMethod…… 这里我们利用它的特性,帮他重写了-eat方法,而实现正好是我们下面的swizzlingEat,完美实现需求~
步骤六:这里再次调用[self swizzlingEat]的原因就不用再说了吧?但是有一点要强调一下,这里的self,可不是HookTool,而是SecondClass的实例。所以,如果在-swizzlingEat中访问self,编辑器会显示self是HookTool类型,此时需要“类型声明”一下,或者利用performSelector的方式。类型声明即
UIView *view = (UIView *)self;
也有人喜欢叫“强制转换”,我倒觉得不是很合适。
(PS: 在Category中,不会有这种疑问~因为始终都是那个类) 如果不知道为什么,可以百度一下一个炙手可热的面试题:OC的消息是如何传递和响应的?
总结:此方法没有局限性,可以hook任意类。不管是私有的还是公有的,甚至你动态添加的。
PS:如果是私有类,你可以先NSLog它的所有方法和属性,来决定你到底要hook哪些方法。注意参数和返回值!参数好说,你可以统统用id接收,然后打印,再修改。returnType也是有办法的,如果是@就是对象,如果是f就是浮点型,如果是v就是void,这些东西就不详细解释了。感兴趣的可以百度~
说到这里,你们可能就发现了,其实那个判断,是可以去掉的,也就是说,你可以选择性的写或者不写method_exchangeImplementations函数,因为你打印出所有的方法,你就知道它到底有没有重写父类的方法了~
补充:
疑问解答:
按照上面你的说法,如果不进行判断,直接交换呢?就是说上面步骤五,不管c是YES还是NO,我都交换方法,会怎样呢?
这里分两种情况,为NO的时候,和本例的情况一样,就不解释了。
如果为YES,你还是交换了方法的话。此时,你成功的将SecondClass的-eat重写,并将它的实现放在了-swizzlingEat中。看似没问题吧?确实,你用SecondClass的实例怎么玩都没问题,但是它的父类MyClass,就会有问题!一旦调用了MyClass的-eat方法,实则调用-swizzlingEat方法,而我们没有给MyClass添加-swizzlingEat方法,所以就会unrecognizeSelectorSentToInstance,直接crash!
为什么MyClass的-eat也会调用-swizzlingEat呢?因为你用method_exchangeImplementations函数将它们的IMP交换了呀~
所以判断是非常有必要的!class_addMethod的两次能否跳过?
不可以!因为步骤四是判断SecondClass是否已经重写-eat方法,从而保证了我们之后要不要交换它的IMP
而步骤五是将-swizzlingEat添加到SecondClass中,原因上面解释过了,因为 self 不是HookTool,而是SecondClass的instance,所以SecondClass必须有-swizzlingEat方法!为什么调用class_addMethod的时候,步骤三和步骤四,-eat方法第二个参数是-swizzlingEat的IMP,而-swizzlingEat是-eat的IMP?
只为了一个目的:就是不影响它的默认实现!换句通俗易懂的但不太准确的话就是:“为了调用super”。
现在我们反过来思考,如果添加的时候,-eat方法就是它本身自己的IMP,那么我们最后想hook,就必须交换方法!那交换之后我们还要保证它原有的实现有效,最终的结果就是死循环!
想一想,SecondClass调用-eat的时候,会走HookTool的-swizzlingEat,在里面,我们为了调用它的默认实现,会写[self swizzlingEat]
,这里self是谁?是SecondClass的instance!那么也就会调用SecondClass的-swizzlingEat方法。SecondClass的-swizzlingEat方法的实现是-eat,那么就会走HookTool的-swizzlingEat,如此一来,造成死循环。