iOS runtime實戰,一次性解決調試火葬場的坑

本文首發掘金,原文連接ios

iOS runtime實戰,一次性解決調試火葬場的坑git

提及來這個黑魔法,仍是幾年前道聽途說的一個概念,徹底不懂這個究竟是作什麼的,這邊文章就是學習中的筆記,也是系列教程的第一篇,主要是理解黑魔法的運做原理,並在實戰中運用,使用中要注意的地方。github

原理

系統中查找IMP是根據SEL的,並且他們是一一對應的, 首先,讓咱們經過兩張圖片來了解一下Method Swizzling的實現原理安全

系統中的原來的對應關係: bash

圖1
黑魔法使用以後的關係:

圖2
上邊圖一中, SEL1中對應的 IMP1SEL2對應的是 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_viewDidloadviewDidLoadIMP是交換了。 當咱們重複調用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中實現viewDidloadfy_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交換selimp使sel指向新的block,執行sel會執行blockimp,不走轉發消息的路徑,性能更高。

在看似牛逼的代碼,其實隱藏着更大的漏洞,當B繼承於A,A繼承於UIViewController,B本身實現了initialize則B則漏掉了統計。另外A的統計數據會夾雜着B的數據,致使統計數據會失真,

這種狀況改怎麼處理呢?

C viewDidload會執行B,當B viewDidload會執行A,其實從子類會重複統計了父類

方案做出少量改動便可解決這個問題。

在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
複製代碼

喜歡的給個start哦

參考資料

相關文章
相關標籤/搜索