本文首发掘金,原文连接ios
提及来这个黑魔法,仍是几年前道听途说的一个概念,彻底不懂这个究竟是作什么的,这边文章就是学习中的笔记,也是系列教程的第一篇,主要是理解黑魔法
的运做原理,并在实战中运用,使用中要注意的地方。github
系统中查找IMP
是根据SEL
的,并且他们是一一对应
的, 首先,让咱们经过两张图片来了解一下Method Swizzling
的实现原理安全
系统中的原来的对应关系: bash
SEL1
中对应的
IMP1
,
SEL2
对应的是
IMP2
,由于业务须要,咱们将
SEL2
对应的
IMP2
和咱们新增的
SEL3
对应的
IMP3
互相交换,交换以后则是如图2所示。
交换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
又恢复到原来的样子。post
+(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
,其实从子类会重复统计了父类
方案做出少量改动便可解决这个问题。
关键代码
//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
复制代码
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
复制代码
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
复制代码