前言
Method-swizzling一度被称为ios中的黑魔法,开启了ios开发的另一扇窗户--AOP(面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术)
aspect就是基于Method-Swizzling实现的一套api
Method-swizzling原理就是利用方法交换,可以在原方法执行前后嵌入新的实现等逻辑
Method与IMP
在ios中了解Method-Swizzling之前,先了解一下Method和IMP
Method为方法的结构体,其结构如下,里面有方法名、方法类型、IMP指针,可以看出IMP为Method的一个参数,实际上为指向实现函数的指针
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
Method与IM的一些获取方法
介绍Method-Swizzling之前先介绍一下获取Method和IMP的一些获取方法
class_getInstanceMethod为获取实例方法的函数,从当前类class中查找方法,其源码如下,先通过lookUpImpOrNil查找imp是否为_objc_msgForward_impcache,如果不是就走_class_getMethod获取Method,最终走到getMethod_nolock方法中,在里面,通过getMethodNoSuper_nolock方法查找当前类是否存在该方法,找不到继续到父类查找,因此如果最终找不到,会返回nil
Method class_getInstanceMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
// Search method lists, try method resolver, etc.
lookUpImpOrNil(cls, sel, nil,
NO/*initialize*/, NO/*cache*/, YES/*resolver*/);
return _class_getMethod(cls, sel);
}
static method_t *
getMethod_nolock(Class cls, SEL sel)
{
method_t *m = nil;
runtimeLock.assertLocked();
while (cls && ((m = getMethodNoSuper_nolock(cls, sel))) == nil) {
cls = cls->superclass;
}
return m;
}
class_getClassMethod为获取类方法的函数,直接从其元类中查找方法,源码如下,和后面和对象方法一致
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
return class_getInstanceMethod(cls->getMeta(), sel);
}
class_getMethodImplementation这个是通过Class和sel获取IMP的一个方法,可以看到,如果找不到IMP指针会返回一个_objc_msgForward的方法指针,即消息转发,注意不能用它作IMP判空处理
IMP class_getMethodImplementation(Class cls, SEL sel)
{
IMP imp;
if (!cls || !sel) return nil;
imp = lookUpImpOrNil(cls, sel, nil,
YES/*initialize*/, YES/*cache*/, YES/*resolver*/);
// Translate forwarding function to C-callable external version
if (!imp) {
return _objc_msgForward;
}
return imp;
}
class_getMethodImplementation是通过Method来获取IMP的一个方法,通过源码可以看出,如果不存在则返回nil可以做判空处理
IMP
method_getImplementation(Method m)
{
return m ? m->imp : nil;
}
addMethod为像某个类添加或者替换新的方法,如果添加本类已存在的方法则会添加失败(不算父类),如果替换方法,则直接替换原方法的imp实现,其源码实现如下
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) {
result = m->imp;
} else {
result = _method_setImplementation(cls, m, imp);
}
} else {
// fixme optimize
method_list_t *newlist;
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);
cls->data()->methods.attachLists(&newlist, 1);
flushCaches(cls);
result = nil;
}
return result;
}
class_addMethod向某个类添加方法,最终会调用addMethod方法,replace传No,即该类存在该方法则替换失败,不去父类查找
class_replaceMethod将某个类的某个方法替换成另一个方法,最终会调用addMethod方法,replace传YES,直接替换imp
method_exchangeImplementations直接交换两个函数的imp
源码如下所示:
BOOL
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
if (!cls) return NO;
mutex_locker_t lock(runtimeLock);
return ! addMethod(cls, name, imp, types ?: "", NO);
}
IMP
class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
{
if (!cls) return nil;
mutex_locker_t lock(runtimeLock);
return addMethod(cls, name, imp, types ?: "", YES);
}
void method_exchangeImplementations(Method m1, Method m2)
{
if (!m1 || !m2) return;
mutex_locker_t lock(runtimeLock);
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);
updateCustomRR_AWZ(nil, m1);
updateCustomRR_AWZ(nil, m2);
}
Method-Swizzling实现
前面了解了一些基本方法的实现逻辑,下面来尝试一下交换方法,了解其中的一些坑点
我们假设有一个类LSPerson继承自NSObject,LSStudent继承自LSPerson,LSPerson的分类LSPerson(Category)
为了方便,在LSStudent的load方法中交换方法,来实现
Method-Swizzling交换前注意
load坑点:在load方法中交换会存在一种情况,原来有类和分类两个load如果同时交换会交换回来
initialize坑点:另外为了优化应用启动速度,大家可能会吧方法交换放到initialize方法中,需要注意的是,该方法调用在正常的方法查找流程当中,如果该方法为父类方法,则调用子类时,该方法会被重复调用,因此在这里交换需要使用单例来进行交换(实际应用可以在需要使用的时候,主动调用方法进行交换)
Method-Swizzling交换实现
首先使用class_getInstanceMethod方法获取需要交换的原方法和新方法,通过前面提到的获取方法的源码逻辑可以得知,如果子类没有获取到刚方法,那么会从父类方法中获取,父类也没有,就返回为nil
Method oriMethod = class_getInstanceMethod([self class], @selector(print));
Method swiMethod = class_getInstanceMethod([self class], @selector(ls_print));
既然交换新方法,那么默认新方法为用户实现,一定存在,此时可能会存在一种情景,被交换的方法由于某些原因(改了继承类、删了方法、未实现某个协议等),导致原方法没有实现,那么需要进行判断(毕竟交换的目的一般是为了使用原方法,且可以拥有新功能)
如果原方法不存在,为了保证功能正常使用,仍然需要交换方法,那么将为实现的原方法更新为用户实现的新方法,并且将新方法给定一个空逻辑的IMP实现,实现代码如下
//查看原方法是否存在
if (!oriMethod) {
//原方法不存在,给原方法赋值新方法,新方法给空方法
class_addMethod([self class], @selector(print), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^{}));
return;
}
如果原方法存在那么,根据class_getInstanceMethod方法逻辑,可能当前类没有实现该方法,而子类实现了,那么也是可以获取了
此时如果直接交换,就会将子类新方法将会和父类的原方法交换,那么将会产生一个严重的问题,调用父类原方法相当于调用了子类的新方法,原有父类方法失效。
存在的坑之一:如果该新方法调用了子类的其他方法,且父类不存在子类调用的其他方法,用父类的实例变量调用了该方法,则会因找不到方法而崩溃
解决方案:
通过class_addMethod方法为当前子类添加原方法,实现为新方法的实现,其源码会获取当前类的该方法实现(不包括父类),如果存在则不添加,返回false,否则添加返回true
因此如果父类没有实现该方法,调用class_addMethod会给原方法赋值新的方法,接下来只需要将新方法通过class_replaceMethod的实现直接指向原来的父类方法实现;如果没有实现,直接使用method_exchangeImplementations交换两者的imp实现即可
//向子类添加原方法,实现为新方法,如果添加成功,证明原方法不存在,并添加了一个新的方法占用原方法
BOOL isAdd = class_addMethod([self class], @selector(print), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (isAdd) {
//添加成功后,替换本类中新添加方法为原有的父类方法实现,并未与父类交换
class_replaceMethod([self class], @selector(ls_print), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else {
//说明原方法在子类中存在,直接交换即可
method_exchangeImplementations(oriMethod, swiMethod);
}
整体实现逻辑如下所示
+ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method oriMethod = class_getInstanceMethod([self class], @selector(print));
Method swiMethod = class_getInstanceMethod([self class], @selector(ls_print));
//查看原方法是否存在
if (!oriMethod) {
//原方法不存在,给原方法赋值新方法,新方法给空方法
class_addMethod([self class], @selector(print), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^{}));
return;
}
//向子类添加原方法,实现为新方法,如果添加成功,证明原方法不存在,并添加了一个新的方法占用原方法
BOOL isAdd = class_addMethod([self class], @selector(print), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (isAdd) {
//添加成功后,替换本类中新添加方法为原有的父类方法实现,并未与父类交换
class_replaceMethod([self class], @selector(ls_print), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else {
//说明原方法在子类中存在,直接交换即可
method_exchangeImplementations(oriMethod, swiMethod);
}
});
}
Method-Swizzling应用案例
平常我们使用到的NSArray,通常给数组内的元素添加内容,会以快捷式的方式来进行赋值,例如:
NSArray *list = @[@"阿斯蒂芬"];
NSLog(@"%@", [list objectAtIndex:2]);
NSLog(@"%@", list[2]);
此时会发现因为越界崩溃,崩溃内容如下
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSSingleObjectArrayI objectAtIndex:]: index 2 beyond bounds [0 .. 0]'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff399f7d07 __exceptionPreprocess + 250
1 libobjc.A.dylib 0x00007fff727625bf objc_exception_throw + 48
2 CoreFoundation 0x00007fff39923b66 -[__NSSingleObjectArrayI objectAtIndex:] + 112
3 LSTest 0x0000000100002d97 main + 119
4 libdyld.dylib 0x00007fff73909cc9 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
会发现崩溃的类不是在NSArray类中,因此我们通过Method-Swizzling交换类的时候不能以NSArray的形式来hook数组objectAtIndex方法,要以__NSSingleObjectArrayI类创建分类的形式来交换原有方法,来处理
注意:这里为了测试方便简化案例,就直接交换了,可以自行补全放到合适的位置
简要实现如下所示:
@interface NSArray (extersion)
@end
@implementation NSArray (Extersion)
+ (void)load {
//这里为了测试方便就直接交换了
Method oriMethod = class_getInstanceMethod([self class], @selector(ls_objectAtIndex:));
Method swiMethod = class_getInstanceMethod(NSClassFromString(@"__NSSingleObjectArrayI"), @selector(objectAtIndex:));
method_exchangeImplementations(oriMethod, swiMethod);
}
- (id)ls_objectAtIndex:(NSUInteger)index {
if (index >= self.count) {
NSLog(@"我不存在,溜了溜了");
return nil;
}
return [self ls_objectAtIndex:index];
}
@end
会发现不崩溃了,并打印出最新越界的提示log,可以自行尝试hook其他操作
2021-02-01 18:24:12.731595+0800 LSTest[19362:206621] 我不存在,溜了溜了
2021-02-01 18:24:12.731720+0800 LSTest[19362:206621] 我不存在,溜了溜了
案例就介绍到这里了