[轉載]iOS runtime實戰(zhàn),一次性解決調試火葬場的坑

本文首發(fā)掘金,原文鏈接

iOS runtime實戰(zhàn),一次性解決調試火葬場的坑

說起來這個黑魔法,還是幾年前道聽途說的一個概念,完全不懂這個到底是做什么的,這邊文章就是學習中的筆記,也是系列教程的第一篇,主要是理解黑魔法的運作原理,并在實戰(zhàn)中運用,使用中要注意的地方。

原理

系統(tǒng)中查找IMP是根據(jù)SEL的,而且他們是一一對應的,
首先,讓我們通過兩張圖片來了解一下Method Swizzling的實現(xiàn)原理

系統(tǒng)中的原來的對應關系:


圖1

黑魔法使用之后的關系:

圖2

上邊圖一中,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_viewDidloadviewDidLoadIMP是交換了。
當我們重復調用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)viewDidloadfy_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交換selimp使sel指向新的block,執(zhí)行sel會執(zhí)行blockimp,不走轉發(fā)消息的路徑,性能更高。

在看似牛逼的代碼,其實隱藏著更大的漏洞,當B繼承于A,A繼承于UIViewController,B自己實現(xiàn)了initialize則B則漏掉了統(tǒng)計。另外A的統(tǒng)計數(shù)據(jù)會夾雜著B的數(shù)據(jù),導致統(tǒng)計數(shù)據(jù)會失真,

image

這種情況改怎么處理呢?

C viewDidload會執(zhí)行B,當B viewDidload會執(zhí)行A,其實從子類會重復統(tǒng)計了父類

方案作出少許改動即可解決這個問題。

image

在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

喜歡的給個start哦

參考資料

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容