OC中基于Runtime机制hook函数的多种方法

1,648 阅读5分钟

前言

在项目开发的过程中,有时会不可避免地遇到必须修改一些现有的系统方法、三方库方法的情况。下面对各种场景和方法进行一个汇总,便于后续更高效、合理地解决问题。 下面针对两种场景分别给出相对简单、适用的解决方式。

公共函数封装

在分析具体场景之前,先对常用操作进行封装,至少可以提高一下代码可读性。

#import <objc/runtime.h>
// 返回值类型为id的函数指针
typedef id(*_IMP)(id,SEL,NSUInteger);
// 返回void的函数指针(不可用于指向返回对象指针的方法实现)
typedef void (*_VIMP)(id,SEL,BOOL);

void exchangeMethod(Class aClass, SEL oldSEL, SEL newSEL)
{
    Method oldMethod = class_getInstanceMethod(aClass, oldSEL);
    assert(oldMethod);
    Method newMethod = class_getInstanceMethod(aClass, newSEL);
    assert(newMethod);
    method_exchangeImplementations(oldMethod, newMethod);
}

// 该函数的设计也可改为支持不同名的方法替换,多传一个newSel即可。
void replaceImplementation(Class newClass, Class hookedClass, SEL sel, IMP* oldImp)
{
    Method old = class_getInstanceMethod(hookedClass, sel);
    IMP newImp = class_getMethodImplementation(newClass, sel);
    *oldImp = method_setImplementation(old, newImp);
}

​ 第一个函数是对两个Method的实现,也就是IMP指向的函数体进行交换,第二个函数是直接对一个method的实现进行修改。

已对外暴露的类

这种情况比较好处理,因为类是已经暴露给我们了,那么开发人员直接就可以基于该类去创建一个Caegory,并新增类或者实例的方法。然后进行方法交换,达到我们的目的。

@implementation UIViewController (RuntimeDemo)
// 最简单的一种hook方法,适用于已经对外暴露的类,基于该类写一个Category,并实现一个新的方法即可。
+ (void)load
{
    // 交换两个方法的实现
    exchangeMethod([UIViewController class],@selector(viewDidAppear:),@selector(newViewDidAppear:));
}
- (void)newViewDidAppear:(BOOL)animated{
    NSLog(@"%@ newViewDidAppear",[self class] );
    [self newViewDidAppear:animated];
}

@end

未对外暴露的类

对于这种类,我们无法创建分类,所以要使用其他的方式。可以借助任意一个类去解决这个问题。第一种方式是通过class_addMethod给类添加方法,这个方法是属于我们自己的某个类的,然后对要hook的方法和我们自己的方法进行交换。

// 通过exchangeMethod来交换类的两个方法,适用于未对外暴露的类(无法通过写该类的Category来增加方法)。
+ (void)load
{
    BOOL isAddSuccess = class_addMethod(objc_getClass("NSString"), @selector(newSubstringFromIndex:), class_getMethodImplementation([self class], @selector(newSubstringFromIndex:)), "@@:L");
    if (isAddSuccess) {
        exchangeMethod(objc_getClass("NSString"),@selector(substringFromIndex:),@selector(newSubstringFromIndex:));
    }
}
- (NSString *)newSubstringFromIndex:(NSUInteger)index{
    NSLog(@"%@ newSubstringFromIndex",[self class] );

    NSString *returnValue = [self newSubstringFromIndex:index];
    NSLog(@"newSubstringFromIndex: %@",returnValue);
    return returnValue;
}

除此之外,还有一种方法,不需要给原类add method,也不需要做方法交换,直接修改对应方法的实现即可。:

static _IMP oldSubstringFromIndex = NULL;
+ (void)load {
    Class hookedClass = objc_getClass("NSString");
    SEL sel = @selector(substringFromIndex:);
    replaceImplementation([self class], hookedClass, sel, &oldSubstringFromIndex);
}
- (NSString *)substringFromIndex:(NSUInteger)index{
    NSLog(@"%@ substringFromIndex after modify",[self class] );

    NSString *returnValue = oldSubstringFromIndex(self,@selector(substringFromIndex:),index);
    NSLog(@"newSubstringFromIndex: %@",returnValue);
    return returnValue;
}

以上看到的都是我们自己写一个方法,然后将它的IMP对接到目标方法上。下面给出不写方法,直接写函数实现的方式。借助这种方式,我们可以直接通过block或者C风格函数给出函数的实现,解决问题。

_IMP substringFromIndex_IMP;
_VIMP viewDidAppear_VIMP;
id (*originSubstringToIndex_IMP) (id self, SEL _cmd, NSUInteger index);
+ (void)load
{
    // 返回值为void型的方法替换
    Method viewDidAppearMethod = class_getInstanceMethod(objc_getClass("UIViewController"), @selector(viewDidAppear:));
    //经测试,viewDidLoad_VIMP用_IMP或者_VIMP均可。
    viewDidAppear_VIMP = method_getImplementation(viewDidAppearMethod);
    // 此处需注意,直接定义函数实现时,需要带隐藏参数,block的方式只能带一个id隐藏参数,不能带SEL隐藏参数
    method_setImplementation(viewDidAppearMethod, imp_implementationWithBlock(^(id target, BOOL animated){
        viewDidAppear_VIMP(target,@selector(viewDidLoad),animated);
        NSLog(@"%@ did Appear",[target class] );
    }));


    // 返回值为String型的方法替换
        Method substringFromIndexMethod = class_getInstanceMethod(objc_getClass("NSString"), @selector(substringFromIndex:));

        substringFromIndex_IMP = method_getImplementation(substringFromIndexMethod);
        method_setImplementation(substringFromIndexMethod, imp_implementationWithBlock(^(id target, int index){
            NSLog(@"FromIndex:source NSString is %@",target);
            return substringFromIndex_IMP(target,@selector(substringFromIndex:),index);
        }));

    // 另一种替换方法的形式,不使用block,使用函数名
    Method substringToIndexMethod = class_getInstanceMethod(objc_getClass("NSString"), @selector(substringToIndex:));
    if (substringToIndexMethod) {
        originSubstringToIndex_IMP = method_getImplementation(substringToIndexMethod);
        method_setImplementation(substringToIndexMethod, newSubstringToIndex);
    }
    else {
        NSLog(@"selector substringToIndex: not found");
    }
}

//直接定义一个方法实现,C风格,需要带隐藏的两个参数。
id newSubstringToIndex(id self, SEL _cmd, NSUInteger index)
{
    NSLog(@"ToIndex:source NSString is %@",self);
    return originSubstringToIndex_IMP(self, _cmd, index);
}

通过测试发现,不带返回值的函数指针不能指向带返回值的函数,反之则可以。当然也可以不用typedef,像originSubstringToIndex_IMP这样直接定义也可以。在load方法中,总共替换了三个方法,前两种使用了imp_implementationWithBlock,需要注意的时,这种方式下,block的入参里没有隐藏参数SEL,只有一个id和函数的其他参数,而直接编写函数的话,则需要同时带上两个隐藏参数。

除了可以替换类的实例方法,也可以替换类方法,需要将class_getInstanceMethod换成class_getClassMethod。

最后说一下IMP的定义。

// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP) (void /* id, SEL,...*/);
#else
typedef id (*IMP) (id, SEL, ...);
#endif

有时会看到这种函数定义方式:

IMP (*originSubstringToIndex_IMP) (id self, SEL _cmd, NSUInteger index);

这种写法我理解应该是有不妥的,上面对originSubstringToIndex_IMP的定义中,IMP代表的是originSubstringToIndex_IMP这个函数指针的返回值类型。使用时不出错的原因,目前根据测试发现是指针型的数据类型可以被其他类型隐式转换。类似于id类型可以被其他类型转换。id本质是一个void *的指针。

如何直接调用实例的基类方法

有时会遇到这样的场景,一个未对外暴露的类SubClassA,继承自类ClassA,并重写了ClassA的实例方法methodA。那么我们想将SubClassA对象的methodA改成super mehtodA,改如何做?对于一个已对外暴露的类来说,很简单,直接基于该类写个分类,使用super关键字即可实现调用基类方法。那么对于未对外暴露的类,其实也不难,了解了super的实现原理,就能够通过以下方式,直接调用基类方法。打开objc/message.h,看到以下定义:

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};

OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

那调用基类方法其实就是构造一个objc_super,并传入当前子类对象指针,和它的基类,然后调用objc_msgSendSuper即可。例如我们现在拿到了一个基类是UINavigationController的子类实例a,想越过它重写的方法,直接调用基类方法:

struct objc_super superclass = {a,a.superclass};
((void(*)(struct objc_super *,SEL,BOOL))objc_msgSendSuper)(&superclass,@selector(popToRootViewControllerAnimated:),YES);

完整示例

github.com/RadonW/Runt…

参考

developer.apple.com/library/arc…

blog.csdn.net/hursing/art…