前言
在项目开发的过程中,有时会不可避免地遇到必须修改一些现有的系统方法、三方库方法的情况。下面对各种场景和方法进行一个汇总,便于后续更高效、合理地解决问题。 下面针对两种场景分别给出相对简单、适用的解决方式。
公共函数封装
在分析具体场景之前,先对常用操作进行封装,至少可以提高一下代码可读性。
#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);