iOS面試之runtime
iOS~runtime理解
什麼是runtime
我們寫的程式碼在程式中執行過程中都會被轉化成runtime的C程式碼,例如[target doSomething];會被轉化成objc_msgSend(target, @selector(doSomething));。
OC中一切都是被設計成了物件,我們都知道一個類被初始化成一個例項,這個例項是一個物件。實際上一個類的本質上也是一個物件,在runtime中用結構體表示
相關定義:
//描述類中的一個方法
typedef struct objc_method *Method;
//例項變數
typedef struct objc_ivar *ivar;
//類別
Typedef struct objc_category *category;
//類中宣告的屬性
Typedef struct objc_property*objc_property_t;
類在runtime中表示
//類在runtime中表示
Struct objc_class {
Classisa;//指標,顧名思義,表示是一個什麼
//例項的
}
#if !_OBJC2_
Classsuper_Class; //指向父類
Constchar *name;//類名
Longversion;
Longinfo;
Longinstance_size;
Structobjc_ivar_list *ivars
Structobjc_method_list **methodLists;//方法列表
Structobjc_cache *cache;//快取
Structobjc_protocol_list *protocols;//協議列表
#endif
} OBJC2_UNAVAILABLE;
獲取列表
有時候會有這樣的需求,我們需要知道當前類中每隔屬性的名字(比如字典轉模型,字典的key值和模型物件的屬性名稱不匹配)。
我們可以通過runtime的一系類方法獲取類的一些資訊(包括屬性列表,方法列表,成員變數列表,和遵循的協議列表)。
Unsigned int count;
//獲取屬性列表
Objc_property_t *propertyList =class_copyPropertyList([self class], &count);
For(unsigned int i = 0;i<count;i++){
Constchar *propertyName = property_getName(propertyList[i]);
NSLog(@”property--à%@”,[NSString stringwithUT8String:propertyName]);
}
//獲取方法列表
Method *methodList = class_copyMethodList([selfclass], &count);
For(unsigned int I ;i<count;i++){
IvarmyIvar = ivarList[i];
Constchar *ivarList[i];
NSLog(@”ivar------%@”, [NSStringwithUTF8String:ivarName]);
}
//獲取協議列表
__unsafe_unnetained Protocol **protocolList= class_copyProtocolList([self class], &count);
For(unsigned int i;i<count;i++){
Protocol*myProtocol = protocolList[i];
Constchar *protocolName = protocol_getName(myProtocal);
NSLog(@”protocol-----%@”,[NSStringwithUTF8String:protolName]);
}
在Xcode上跑一下看看輸出,需要給你當前的類寫幾個屬性,成員變數,方法和協議,不然獲取的列表是沒有東西的
注意:呼叫這些獲取列表的方法別忘記匯入標頭檔案#import <objc/runtime.h>
方法呼叫
讓我們看一下方法呼叫執行時的過程(參照前文類在runtime中的表示)
如果用例項物件呼叫例項方法,會到例項的isa指標指向的物件(也就是類物件)操作。如果呼叫的是類方法,就會到類物件的isa指標指向的物件(也就是元類物件)中操作
1、 首先,在相應操作的物件中的快取方法列表中找呼叫的方法,如果找到轉向相應實現並執行
2、 如果沒有找到,在相應操作的物件中的方法列表中找呼叫的方法,如果找到,轉向相應實現執行
3、 如果沒有找到,去父類指標指向的物件中執行1,2。
4、 以此類推,如果一直到根類還沒找到,轉向攔截呼叫。
5、 如果沒有重寫攔截呼叫的方法,程式報錯。
以上的過程給我帶來的啟發:
1、 重寫父類的方法,並沒有覆蓋父類的方法,只是在當前類中找到了這個方法就不會再到父類中去找
2、 如果想呼叫已經重寫父類方法的父類的實現,只需要使用super這個編譯器標識,它會在執行時跳過在當前的類物件中尋找方法的過程。
攔截呼叫
在方法呼叫中說到了,如果沒有找到方法就會轉向攔截呼叫。
那麼什麼是攔截呼叫呢
攔截呼叫就是,再找不到呼叫的方法程式崩潰之前,你有機會通過重寫NSObject的四個方法來處理
+(BOOL)resolveClassMethod(SEL)sel;
+(BOOL)resolveInstanceMethod(SEL)sel;
//後兩個方法需要轉發到起到的類處理
-(id)forwardingTargetForSelector(SEL)sel;
- (void)forwardInvocation(NSInvocation*)anInvocation;
第一個方法是當你呼叫一個不存在的類方法的時候,會呼叫這個方法,預設返回NO,你可以加上自己的處理然後返回YES.
第二個方法和第一個方法和第一個方法相似,只不過處理的是例項方法。
第三個方法是將你呼叫的不存在的方法重定向到一個其他聲明瞭這個方法的類,只需要你返回一個有這個方法的target。
第四個方法是將你呼叫的不存在的大包NSinvocation傳給你。昨晚你自己的處理後,呼叫invokeWithTarget;方法讓某個target觸發這個方法。
動態新增方法
重寫了攔截呼叫的方法並且返回了YES,我們要怎麼處理呢?
有一個方法是根據傳進來的SEL型別的selector動態新增一個方法。
首先從外部隱式呼叫一個不存在的方法:
//隱式呼叫方法
[target performSelector:@selector(resolveAdd:)withObject:@”test”];
然後再target物件內部重寫攔截呼叫的方法,動態新增方法。
Void runAddMethod(id self, SEL _cmd,NSString *string) {
NSLog(@”addC IMP”, string);
}
+(BOOL)resloveInstanceMethod(SEL)sel {
//給本類動態新增一個方法
If([NSStringFromSeletor(sel) isEqualToString:@”resolveAdd:”]){
Class_addMethod(self,sel, (IMP)runAddMethod, “[email protected]:*”);
}
ReturnYES;
}
其中class_addMethod的四個引數分別是
1、 class cls 給哪個類新增方法,本例中是self
2、 SEL name 新增的方法,本例中是重寫的攔截呼叫傳進來的selector。
3、 IMP imp 方法的實現,C方法的方法實現可以直接獲得。如果是OC 方法,可以用+(IMP)instanceMethodForSelector(SEL)aSelector;獲取方法的實現
4、 “[email protected]:*”方法的簽名,代表有一個引數的方法。
關聯物件
現在你準備用一個系統的類,但系統的類並不能滿足你的需求,你需要額外新增一個屬性。
這種情況的一般解決方法就是繼承
但是,只新增一個屬性,就去繼承一個類,總覺得太麻煩了,
這個時候,runtime的關聯屬性就發揮他的作用了。
//首先定義一個全域性變數,用它的地址作為關聯物件的Key
Static char associatedObjectKey;
//設定關聯物件
Objc_setAssociatedObject(target,&associatedObjectKey);
NSLog(@”AssociatedObjected = %@”, string);
Objc_setAssociatedObject 的四個引數:
1、 id object 給誰設定關聯物件。
2、 const void *key 關聯物件唯一的key,獲取時會用到。
3、 id value 關聯物件
4、 objc_AssociationPolicy關聯策略,有以下幾種策略:
enum{
OBJC_ASSOCIATION_ASSION= 0;
OBJC_ASSOCIATION_RETAIN_NONATOMIC= 1;
OBJC_ASSOCIATION_COPY_NONATOMIC= 3;
OBJC_ASSOCIATION_RETAIN= 01401;
OBJC_ASSOCIATION_COPY= 01403;
};
如果你熟悉OC,看名字應該知道這幾種策略的意思了吧。
objc_getAssociatedObject 的兩個引數
1、 id object 獲取誰的關聯物件。
2、 const void *key 根據這個唯一的key獲取關聯物件。
其實,你還可以把新增和獲取關聯物件的方法寫在你需要用到的這個功能的類的類別中,方便使用。
//新增關聯物件
- (void)addAssoiatedObject:(id)object{
-
Objc_setAssociatedObject(self, @selector(getAssociatedObject),object, OBJC_ASSOCIATION_TETAIN_NONATOMIC);
- }
//獲取關聯物件
-(id)getAssociationObject {
Return objc_getAssociationObject(self,_cmd);
}
注意:這裡面我們把getAssociatedObject方法的地址作為唯一的key,_cmd代表呼叫方法的地址。
方法交換
方法交換, 顧名思義,就是將兩個方法的實現交換,例如,將A方法和B 方法交換,呼叫A方法的時候,就會執行B方法中的程式碼,反之亦然
#import “UIViewController+swizzling.h”
#import <objc/runtime.h>
@implementation UIViewController (swizzing)
//load方法會在類第一次載入的時候被呼叫
//呼叫的時間比較靠前,適合在這個方法裡做方法交換
+(void)load {
//方法交換應該被保證,在程式中只會執行一次
Static dispatch_once_tonceToken;
Dispatch_once(&onceToken,^{
SELsystemSel = @seletor(viewWillAppear:);
//自己實現的將要被交換的方法的selector
SELswizzSel = @selecor(swiz_viewwillAppear:);
//兩個方法的method
MethodsystemMethod = class_getInstanceMethod([self class], systemSel);
MethodswizzMethod = class_getInstanceMethod([self class], swizzSel);
//首先動態新增方法,實現是被交換的方法,返回值表示新增成功還是失敗
BOOLisAdd = class_addMethod(self, systemSel, method_getImplementation(systemMethod),method_getTypeEncoding(swizzMethod));
If(isAdd) {
//如果成功,說明類中不存在這個方法的實現
//將被交換方法的實現替換到這個並不存在的實現
Class_replaceMethod(self,swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
}else {
//否則,交換兩個方法的實現
Method_exchangeImplementations(systemMethod,swizzMethod);
}
});
}
- (void)swiz_viewWillAppear(BOOL)animated{
//這時候呼叫自己,看起來像是死迴圈
//但是其實自己的實現已經被替換了
NSLog(@”swizzle”);
- }
@end;
在一個自己定義的viewController中重寫viewWillAppear
- (void)viewWillAppear(BOOL)animated{
-
[super viewWillAppear:animated];
-
NSLog(@”viewWillAppear”);
- }
- Run起來看看輸出吧
我的理解
1、 方法交換對於我來說更像是實現了一種思想的最佳技術:AOP面向切面程式設計。
2、 既然是切面程式設計,就一定不要忘記, 交換完再調回自己。
3、 一定要保證只交換一次,否則就會很亂
4、 最後這個技術很危險,謹慎使用
完
長按下圖,識別二維碼,關注“一個人的零與一”,獲取更多iOS開發的技巧和經驗。