Objective-C 黑魔法如何hook系统私有类?实现真正的Method Swizzling!

版权声明:欢迎大家积极分享!交流。关注我~ https://blog.csdn.net/qinqi376990311/article/details/80076919

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吗?这里分两种情况:

    1. 如果SecondClass重写了-eat方法,那么此时c就是NO
    2. 如果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这个函数,将会重写方法,但是如果已经被重写,那么将不会替换。我们要利用的就是这个特性。

  • 步骤五:所以,是否交换方法的实现,要取决于条件的。

    1. 如果SecondClass重写了-eat方法,那么此时c为NO,我们这个时候就要交换方法,因为我们要确保SecondClass的-eat本身的实现要走一遍的。这才是成功的hook,其实这个时候的操作,就跟其它博客提到的用Category来hook的原理是一样的。
    2. 如果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函数,因为你打印出所有的方法,你就知道它到底有没有重写父类的方法了~


补充:

疑问解答:

  1. 按照上面你的说法,如果不进行判断,直接交换呢?就是说上面步骤五,不管c是YES还是NO,我都交换方法,会怎样呢?
    这里分两种情况,为NO的时候,和本例的情况一样,就不解释了。
    如果为YES,你还是交换了方法的话。此时,你成功的将SecondClass的-eat重写,并将它的实现放在了-swizzlingEat中。看似没问题吧?确实,你用SecondClass的实例怎么玩都没问题,但是它的父类MyClass,就会有问题!一旦调用了MyClass的-eat方法,实则调用-swizzlingEat方法,而我们没有给MyClass添加-swizzlingEat方法,所以就会unrecognizeSelectorSentToInstance,直接crash!
    为什么MyClass的-eat也会调用-swizzlingEat呢?因为你用method_exchangeImplementations函数将它们的IMP交换了呀~
    所以判断是非常有必要的!

  2. class_addMethod的两次能否跳过?
    不可以!因为步骤四是判断SecondClass是否已经重写-eat方法,从而保证了我们之后要不要交换它的IMP
    而步骤五是将-swizzlingEat添加到SecondClass中,原因上面解释过了,因为 self 不是HookTool,而是SecondClass的instance,所以SecondClass必须有-swizzlingEat方法!

  3. 为什么调用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,如此一来,造成死循环。

猜你喜欢

转载自blog.csdn.net/qinqi376990311/article/details/80076919