本文首發掘金,原文連接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
複製代碼