iOS底層探究-----Method Swizzling

前言

這是我參與8月更文挑戰的第10天,活動詳情查看:8月更文挑戰編程

基本原理

咱們都知道,類的最終父類是NSObject,在程序編譯後,在底層能夠發現類就是一個結構體,每一個類都有一個 isa 指針,可以訪問到結構體裏面的數據。方法查找的是時候,是在類的方法列表裏面,經過SEL查找對應的IMP安全

你們或多或少聽到過iOS黑魔法,也就是方法交換。同時蘋果的運行時 runtime 也提供了一個很好的環境。利用OCRuntime特性,動態改變SEL(方法編號)和IMP(方法實現)的對應關係,達到OC方法調用流程改變的目的。主要用於OC方法。下面用兩個圖例來展現下:性能優化

  • 交換前(正常狀況):

882505D3-1FCC-4CDB-AF73-61E743933956.png

  • 交換後:

A085AFD2-4E52-49C6-9EDC-7F3BD513C942.png

根據這兩張圖,咱們能稍微明白些這個交換的是怎麼一回事。Runtime提供了交換兩個SELIMP對應關係的函數:markdown

OBJC_EXPORT void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製代碼

Runtime機制對於AOP面向切面編程提供良好的支持。在OC中,可利用Method Swizzling實現AOP,其中AOPAspect Oriented Programming)是一種編程的思想,一樣面向對象編程OOP也一種編程的思想,可是AOPOOP有本質的區別:函數

  • OOP編程思想,他更加傾向於對業務模塊的封裝,同時也可以劃分出更爲清晰的業務邏輯單元;
  • AOP編程思想,是面向切面進行提取封裝,提取各個模塊中的公共部分,這樣能提升模塊的複用率,下降業務之間的耦合性;

API 介紹

  • 經過SEL獲取方法Method

獲取實例方法post

OBJC_EXPORT Method _Nullable
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)  OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
複製代碼

獲取類方法性能

OBJC_EXPORT Method _Nullable
class_getClassMethod(Class _Nullable cls, SEL _Nonnull name) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
複製代碼
  • IMPgetter/setter方法:

獲取某個方法的實現優化

OBJC_EXPORT IMP _Nonnull
method_getImplementation(Method _Nonnull m) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製代碼

設置一個方法的實現編碼

OBJC_EXPORT IMP _Nonnull
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製代碼
  • 獲取方法實現的編碼類型
OBJC_EXPORT const char * _Nullable
method_getTypeEncoding(Method _Nonnull m) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製代碼
  • 添加方法實現
OBJC_EXPORT void
class_addMethods(Class _Nullable, struct objc_method_list * _Nonnull) OBJC2_UNAVAILABLE;
複製代碼
  • 替換方法的IMP
OBJC_EXPORT IMP _Nullable
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製代碼
  • 交換兩個方法的IMP
OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製代碼
  • 儘可能放在單利裏面,這樣能保證只調用一次,保證安全。

案例分析

交換方法的使用

建立一個類LGPerson,而後建立LGTeacher繼承LGPerson,使用以下代碼:spa

// LGPerson .h 
@interface LGPerson : NSObject 

- (void)person_instanceMethod; 

@end 

// LGPerson.m 
@implementation LGPerson 

- (void)person_instanceMethod {
    NSLog(@"\n 打印 person_instanceMethod: %s\n", __func__); 
} 

@end 

// LGTeacher.h 
@implementation LGTeacher

+ (void)load {
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ [LGRuntimeUtil 
    lg_methodSwizzlingWithClass:self 
                         oriSEL:@selector(person_instanceMethod) 
                    swizzledSEL:@selector(teacher_instanceMethod)]; 
    });
} 

- (void)teacher_instanceMethod {
    NSLog(@"\n 打印 teacher_instanceMethod: %s\n", __func__); 
} 
    
@end 

// 封裝LGRuntimeUtil.m 
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL { 

    if (!cls) NSLog(@"傳入的交換類不能爲空");
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL); 
    Method swizzleMethod = class_getInstanceMethod(cls, swizzledSEL); 
    method_exchangeImplementations(oriMethod, swizzleMethod); 
}
複製代碼

main 文件裏面初始化這兩個類,都調用 person_instanceMethod 方法:

LGPerson  *person = [LGPerson alloc] init];
    [person person_instanceMethod];
    
    LGTeacher  *teacher = [LGTeacher alloc] init];
    [teacher person_instanceMethod];
複製代碼

可是,打印出來的方法名,卻都是 teacher_instanceMethod 。那麼就說明替換成功了。

  • 由於person_instanceMethodSEL 找到的是teacher_instanceMethodIMP,因此找到的就是teacher_instanceMethod方法;

  • teacher_instanceMethodSEL 找到的倒是person_instanceMethodIMP,但IMP對應的是person_instanceMethod方法,再繼續根據person_instanceMethod方法的 SEL 找到的是交換後的IMP,因此找到了teacher_instanceMethod方法。

遞歸問題

就是在 teacher_instanceMethod 方法裏面,再次調用 teacher_instanceMethod,代碼以下:

- (void)teacher_instanceMethod { 
    [self teacher_instanceMethod]; 
    NSLog(@"\n 打印 teacher_instanceMethod: %s\n", __func__);
}
複製代碼

運行以後,直接報錯:

BA08173A-4A03-4AA2-8395-7372712C9B43.png

爲何會這樣了?

  • 對於LGTeacher而言調用person_instanceMethod就是調用LGTeacher:teacher_instanceMethod-> LGPerson:person_instanceMethod

  • 對於LGPerson調用person_instanceMethod是調用LGTeacher:teacher_instanceMethod -> LGPerson:teacher_instanceMethod。而LGPerson沒有實現teacher_instanceMethod,因此報錯。

因此交換方法必定是去交換本身的方法

  • 爲何要調用本身呢?

由於有時候,在作一些處理的時候,須要保持原來的邏輯,因此須要再次調用本類。

  • 那怎樣才能避免這類的狀況了?

能夠經過class_addMethod去嘗試添加要交換的方法。

性能優化一

  • class_addMethod方法的使用,咱們可使用這個方法來添加要交換的方法:

    • 若是添加成功,說明在本類中沒有這個方法,可是能夠經過class_replaceMethod進行替換,其內部會調用class_addMethod進行添加的方法;

    • 若是添加不成功,就說明類裏面有這個方法,則經過method_exchangeImplementations進行交換

  • 代碼以下:

+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL { 
    if (!cls) NSLog(@"傳入的交換類不能爲空"); 
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL); 
    
    // 添加要交換的方法 
    BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod)); 
    
    if (success) {
        // 添加成功 - 進行替換 - 沒有父類進行處理 (重寫一個) 
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); 
    } else { 
        // 本身有的話就 
        method_exchangeImplementations(oriMethod, swiMethod); 
    } 
}
複製代碼

性能優化二

根據上面的使用案例,若是子類和父類都沒有實現person_instanceMethod這個方法,在子類裏面調用[self teacher_instanceMethod]時,就會產生遞歸,若是不處理,就回報錯。

怎麼解決了?若是該方法不存在,能夠在添加方法後,再給此方法添加一個空的實現,也就是至關於增長一個不作任何事情的IMP,代碼以下:

+ (void)ssl_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{ 
    if (!cls) NSLog(@"傳入的交換類不能爲空"); 
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL); 
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL); 
    
    if (!oriMethod) { 
        // 在 oriMethod 爲 nil 時,替換後將swizzledSEL複製一個不作任何事的空實現 
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); 
        
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ NSLog(@"來了一個空的 imp"); })); 
    } 
    
    // 嘗試添加你要交換的方法 
    BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod)); 
    
    if (success) { 
        // 添加成功說明本身沒有 - 替換 - 父類重寫一個 
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); 
    } else { 
         // 本身有 - 交換 
         method_exchangeImplementations(oriMethod, swiMethod); 
    } 
}
複製代碼
相關文章
相關標籤/搜索