「這是我參與8月更文挑戰的第10天,活動詳情查看:8月更文挑戰」編程
咱們都知道,類的最終父類是NSObject
,在程序編譯後,在底層能夠發現類就是一個結構體,每一個類都有一個 isa
指針,可以訪問到結構體裏面的數據。方法查找的是時候,是在類的方法列表裏面,經過SEL
查找對應的IMP
。安全
你們或多或少聽到過iOS黑魔法
,也就是方法交換
。同時蘋果的運行時 runtime
也提供了一個很好的環境。利用OC
的Runtime
特性,動態改變SEL
(方法編號)和IMP
(方法實現)的對應關係,達到OC
方法調用流程改變的目的。主要用於OC
方法。下面用兩個圖例來展現下:性能優化
根據這兩張圖,咱們能稍微明白些這個交換的是怎麼一回事。Runtime
提供了交換兩個SEL
和IMP
對應關係的函數: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
,其中AOP
(Aspect Oriented Programming
)是一種編程的思想,一樣面向對象編程OOP
也一種編程的思想,可是AOP
和OOP
有本質的區別:函數
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);
複製代碼
IMP
的getter/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_instanceMethod
的 SEL
找到的是teacher_instanceMethod
的IMP
,因此找到的就是teacher_instanceMethod
方法;
而teacher_instanceMethod
的 SEL
找到的倒是person_instanceMethod
的IMP
,但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__);
}
複製代碼
運行以後,直接報錯:
爲何會這樣了?
對於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);
}
}
複製代碼