本文首发掘金,原文链接
说起来这个黑魔法,还是几年前道听途说的一个概念,完全不懂这个到底是做什么的,这边文章就是学习中的笔记,也是系列教程的第一篇,主要是理解黑魔法
的运作原理,并在实战中运用,使用中要注意的地方。
原理
系统中查找IMP
是根据SEL
的,而且他们是一一对应
的,
首先,让我们通过两张图片来了解一下Method Swizzling
的实现原理
系统中的原来的对应关系:
图1
黑魔法使用之后的关系:
图2上边图一中,
SEL1
中对应的IMP1
,SEL2
对应的是IMP2
,因为业务需要,我们将SEL2
对应的IMP2
和我们新增的SEL3
对应的IMP3
互相交换,交换之后则是如图2所示。
运用
Method Swizzling使用
交换method
函数
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);
我们测试验证一下理论:
+(void)load{
[UIViewController exchange];
}
+ (void)exchange{
Method m1 = class_getInstanceMethod(UIViewController.class, @selector(viewDidLoad));
Method m2 = class_getInstanceMethod(self, @selector(fy_viewDidload));
NSLog(@"viewDidLoad excheng before:%p",method_getImplementation(m1));
NSLog(@"fy_viewDidload excheng before:%p",method_getImplementation(m2));
if (!class_addMethod(self, @selector(fy_viewDidload), method_getImplementation(m2), method_getTypeEncoding(m2))) {
method_exchangeImplementations(m1, m2);
}
NSLog(@"viewDidLoad excheng after:%p",method_getImplementation(m1));
NSLog(@"fy_viewDidload excheng after:%p",method_getImplementation(m2));
}
输出:
viewDidLoad excheng before:0x10a74adf9
fy_viewDidload excheng before:0x106ca5040
viewDidLoad excheng after:0x106ca5040
fy_viewDidload excheng after:0x10a74adf9
可以看出来,交换之后fy_viewDidload
和viewDidLoad
的IMP
是交换了。
当我们重复调用2次,应该IMP
又恢复到原来的样子。
+(void)load{
[UIViewController exchange];
[UIViewController exchange];
}
//交换第一次
viewDidLoad excheng before:0x111626df9
fy_viewDidload excheng before:0x10db81040
viewDidLoad excheng after:0x10db81040
fy_viewDidload excheng after:0x111626df9
//交换第二次
viewDidLoad excheng before:0x10db81040
fy_viewDidload excheng before:0x111626df9
viewDidLoad excheng after:0x111626df9
fy_viewDidload excheng after:0x10db81040
可以看出来,IMP
又被换回去了。
我们在+load
中调用,原因是+load
方法是只会执行一次,具有线程安全的,所以不用考虑并发问题。在编译阶段load
是根据文件先后顺序编译的,所以我们可以把交换文件放到第一个位子。
那么我们看一下系统加载文件的顺序:
就拿我们页面统计来说,这个而需求很多公司都常见,有些sdk
是在ViewController
基类中的viewDidload
中调用统计函数。那么我们该如何做一个对业务无侵入的代码呢?
那么我们在ViewController
新建Category
,然后在+load
中实现viewDidload
和fy_viewDidload
的交换,则在fy_viewDidload
可以添加统计代码。
static NSMutableSet *set;
+(void)initialize{
[self fy_countViewDidLoad];
}
//统计其他的子类的viewDidLoad方法时长
+ (void)fy_countViewDidLoad{
if (set== nil) {
set = [[NSMutableSet alloc]init];
}
//UIViewcontroller的子类统计
if ([self isSubclassOfClass:UIViewController.class] &&
self != UIViewController.class) {
if ([set containsObject:self]) {
return;
}else{
[set addObject:self];
}
SEL sel = @selector(viewDidLoad);
Method m1 = class_getInstanceMethod(self, @selector(viewDidLoad));
IMP imp1 = method_getImplementation(m1);
// id,SEL 必须传,否则到了执行imp1Func(i,s);内部的id是nil,导致函数无法执行。
void(*imp1Func)(id,SEL) = (void*)imp1;//imp1原始方法地址
void (^block)(id,SEL) = ^(id i,SEL s){
//code here
printf("开始\n");
NSDate *date =[NSDate new];
imp1Func(i,s);
NSLog(@"%@ time:%d", NSStringFromClass(self),(int)[[NSDate date] timeIntervalSinceDate:date]);
printf("结束\n");
};
IMP imp2 = imp_implementationWithBlock(block);
class_replaceMethod(self, sel, imp2, method_getTypeEncoding(m1));
}
}
首先记录原本的函数imp
,使用class_replaceMethod
交换sel
和imp
使sel
指向新的block
,执行sel
会执行block
的imp
,不走转发消息的路径,性能更高。
在看似牛逼的代码,其实隐藏着更大的漏洞,当B继承于A,A继承于UIViewController
,B自己实现了initialize
则B则漏掉了统计。另外A的统计数据会夹杂着B的数据,导致统计数据会失真,
这种情况改怎么处理呢?
当C
viewDidload
会执行B
,当B
viewDidload
会执行A
,其实从子类会重复统计了父类
方案作出少许改动即可解决这个问题。
image在A的子类A2中统计A的加载次数,在B2中统计B的加载次数,在C2中统计C的加载次数,可以做到精准统计。
关键代码
//B2
[self addObserver:[YQKVOObserver shared] forKeyPath:kUniqueFakeKeyPath options:NSKeyValueObservingOptionNew context:nil];
// Setup remover of KVO, automatically remove KVO when VC dealloc.
YQKVORemover *remover = [[YQKVORemover alloc] init];
remover.target = self;
remover.keyPath = kUniqueFakeKeyPath;
objc_setAssociatedObject(self, &kAssociatedRemoverKey, remover, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// NSKVONotifying_ViewController
Class kvoCls = object_getClass(self);
class_addMethod(kvoCls, @selector(viewDidLoad), (IMP)fy_viewDidLoad, originViewDidLoadEncoding);
static void fy_viewDidLoad(UIViewController *kvo_self, SEL _sel) {
Class kvo_cls = object_getClass(kvo_self);
Class origin_cls = class_getSuperclass(kvo_cls);
IMP origin_imp = method_getImplementation(class_getInstanceMethod(origin_cls, _sel));
assert(origin_imp != NULL);
void (*func)(UIViewController *, SEL) = (void (*)(UIViewController *, SEL))origin_imp;
CFAbsoluteTime beginTime = CFAbsoluteTimeGetCurrent();
func(kvo_self, _sel);
CFAbsoluteTime endTime = CFAbsoluteTimeGetCurrent();
//这里统计加载时间长度和次数
}
在需要统计类的子类中统计,的确是一种不错的选择,精准统计时间和次数,而且不影响性能
runtime一时爽,一直用一直爽,调试火葬场
用的时候感觉很爽,可以做这么🐂的事,但是其他同学来调试的时候,出问题了也非常的难找。
我做这个工具可以记录runtime
的黑魔法日志,使用起来也很简单。
platform :ios, '9.0'
use_frameworks!
target 'MyApp' do
pod 'FYMSL'
end
函数生命周期和耗时操作回调
// 每个函数的回调,独立可以单独设置的。
FYVCcall *cll = [FYVCcall shared];
[cll setCallback:^(CFAbsoluteTime loadTime, UIViewController * _Nonnull vc, NSString * _Nonnull funcName,NSString *str) {
const char *clsName = NSStringFromClass(vc.class).UTF8String;
printf("cls:%s func:%s %f %s \n",clsName,funcName.UTF8String,loadTime,str.UTF8String);
}];
输出日志:
cls:ViewController func:viewDidLoad 2.001058 2019 09-03 16:25:45
cls:ViewController func:viewWillAppear: 0.000000 2019 09-03 16:25:45
cls:ViewController func:viewDidAppear: 0.000000 2019 09-03 16:25:45
查看MethodSwizzling总记录
NSLog(@"%@",[FYNodeManger shared].description);
↴:替换 ⇄ :交换
举个例子:
例子1:test2 交换到test1,然后交换到test3,最终imp是0x105c6c630
⇄ | + test2 -> test1 -> test3 -> imp:0x105c6c630
例子2:test1 的imp替换到0x105c6c660,然后又替换到0x105c6c690,又替换到0x105c6c600,
又交换到了test2,又交换到了test3->又交换到了test4
↴ | + test1 -> imp:0x105c6c660
↴ | + test1 -> imp:0x105c6c690
↴ | + test1 -> imp:0x105c6c600
⇄ | + test1 -> test2 -> imp:0x105c6c600
⇄ | + test1 -> test3 -> imp:0x105c6c630
⇄ | + test1 -> test4 -> imp:0x105c6c660
查看单一SEL单一记录
NSLog(@"\n%@",[FYNodeManger objectForSEL:@"test1"]);
↴ | + test1 -> imp:0x10b5de550
↴ | + test1 -> imp:0x10b5de580
↴ | + test1 -> imp:0x10b5de4f0
⇄ | + test1 -> test2 -> imp:0x10b5de4f0
⇄ | + test1 -> test3 -> imp:0x10b5de520
⇄ | + test1 -> test4 -> imp:0x10b5de550
网友评论