本文首發(fā)掘金,原文鏈接
說起來這個黑魔法,還是幾年前道聽途說的一個概念,完全不懂這個到底是做什么的,這邊文章就是學習中的筆記,也是系列教程的第一篇,主要是理解黑魔法的運作原理,并在實戰(zhàn)中運用,使用中要注意的地方。
原理
系統(tǒng)中查找IMP是根據(jù)SEL的,而且他們是一一對應的,
首先,讓我們通過兩張圖片來了解一下Method Swizzling的實現(xiàn)原理
系統(tǒng)中的原來的對應關系:
黑魔法使用之后的關系:
上邊圖一中,
SEL1中對應的IMP1,SEL2對應的是IMP2,因為業(yè)務需要,我們將SEL2對應的IMP2和我們新增的SEL3對應的IMP3互相交換,交換之后則是如圖2所示。
運用
Method Swizzling使用
交換method函數(shù)
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方法是只會執(zhí)行一次,具有線程安全的,所以不用考慮并發(fā)問題。在編譯階段load是根據(jù)文件先后順序編譯的,所以我們可以把交換文件放到第一個位子。
那么我們看一下系統(tǒng)加載文件的順序:
就拿我們頁面統(tǒng)計來說,這個而需求很多公司都常見,有些sdk是在ViewController基類中的viewDidload中調用統(tǒng)計函數(shù)。那么我們該如何做一個對業(yè)務無侵入的代碼呢?
那么我們在ViewController新建Category,然后在+load中實現(xiàn)viewDidload和fy_viewDidload的交換,則在fy_viewDidload可以添加統(tǒng)計代碼。
static NSMutableSet *set;
+(void)initialize{
[self fy_countViewDidLoad];
}
//統(tǒng)計其他的子類的viewDidLoad方法時長
+ (void)fy_countViewDidLoad{
if (set== nil) {
set = [[NSMutableSet alloc]init];
}
//UIViewcontroller的子類統(tǒng)計
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 必須傳,否則到了執(zhí)行imp1Func(i,s);內部的id是nil,導致函數(shù)無法執(zhí)行。
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));
}
}
首先記錄原本的函數(shù)imp,使用class_replaceMethod交換sel和imp使sel指向新的block,執(zhí)行sel會執(zhí)行block的imp,不走轉發(fā)消息的路徑,性能更高。
在看似牛逼的代碼,其實隱藏著更大的漏洞,當B繼承于A,A繼承于UIViewController,B自己實現(xiàn)了initialize則B則漏掉了統(tǒng)計。另外A的統(tǒng)計數(shù)據(jù)會夾雜著B的數(shù)據(jù),導致統(tǒng)計數(shù)據(jù)會失真,
這種情況改怎么處理呢?
當C viewDidload會執(zhí)行B,當B viewDidload會執(zhí)行A,其實從子類會重復統(tǒng)計了父類
方案作出少許改動即可解決這個問題。
在A的子類A2中統(tǒng)計A的加載次數(shù),在B2中統(tǒng)計B的加載次數(shù),在C2中統(tǒng)計C的加載次數(shù),可以做到精準統(tǒng)計。
關鍵代碼
//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();
//這里統(tǒng)計加載時間長度和次數(shù)
}
在需要統(tǒng)計類的子類中統(tǒng)計,的確是一種不錯的選擇,精準統(tǒng)計時間和次數(shù),而且不影響性能
runtime一時爽,一直用一直爽,調試火葬場
用的時候感覺很爽,可以做這么??的事,但是其他同學來調試的時候,出問題了也非常的難找。
我做這個工具可以記錄runtime的黑魔法日志,使用起來也很簡單。
platform :ios, '9.0'
use_frameworks!
target 'MyApp' do
pod 'FYMSL'
end
函數(shù)生命周期和耗時操作回調
// 每個函數(shù)的回調,獨立可以單獨設置的。
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