大家都知道,OC是一門動態語言 ,傳統的靜態語言如C語言,在編譯的時候已經決定了要調用的方法。而對于像OC這樣的動態語言,由于有runtime機制,所以編譯階段并不知道要調用的具體方法實現,在運行的時候才會去調用,這也導致了一些case比如給某個target指定了action的SEL,但是沒有對這個SEL對應的方法做具體實現,在編譯的時候非常No Problem,但是在運行的時候就會因為沒有找到對應的實現而crash。而這種情況也是我們經常會碰到的。
那么該怎么處理這種case呢?
研究了runtime的一些原理以及OC對象的內存布局 ,我們可以得出這么幾個結論:
1、OC對象調用方法會用到選擇器selector(也就是SEL),通過SEL來找到具體的實現IMP
2、SEL是具體方法的一個索引,IMP是具體實現的函數指針,兩者配合可以快速找到具體實現的方法并調用
3、每個OC的類其實就是個結構體,查看objc/runtime.h中objc_class結構體的定義如下
struct objc_class
{
struct objc_class* isa;//isa指針指向本類
struct objc_class* super_class;//父類指針,可以根據此找到父類
const char* name;//類名
long version;//類的版本信息,默認為0(一般不怎么關注)
long info;//類信息,供運行期使用的一些位標識
long instance_size;// 該類的實例變量大小
struct objc_ivar_list* ivars;//成員變量列表
struct objc_method_list** methodLists;//方法列表
struct objc_cache* cache;//方法緩存
struct objc_protocol_list* protocols;//協議方法列表
};
4、對象查找方法實現的過程:
1 先到cache方法緩存中找,有則調用,沒有則下一步
2 到methodLists方法列表中找,有則調用,沒有則下一步
3 本類中沒找到,就到super_class的methodLists中找,若還沒有,就一直往上找,找到最頂層還沒有的話就crash了。
但是在crash之前,其實有3個消息轉發的補救方式,來防止crash。當然若沒有采取什么"補救方式",就直接crash了。這三個補救方式簡單講就是:
1 動態向類中加入缺失的方法
2 指定處理消息的接受者(備用的接受者,就像快遞電話來了你沒空,可以找同學來幫忙簽收一下)
3 使用NSInvocation進行消息轉發
這三種補救方法涉及到的API有:
//動態方法解析
+(BOOL) resolveInstanceMethod:(SEL)selector
+(BOOL)resolveClassMethod:(SEL)sel
//備用接收者
-(id)forwardingTargetForSelector:(SEL)aSelector
//NSInvocation進行消息轉發
-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector
-(void)forwardInvocation:(NSInvocation *)anInvocation
并且runtime會依次按以上順序調用,去看你是否有補救。
我們先人為造一個crash的場景:
建立一個Person類,Person.h
#import@interface Person : NSObject
@property(nonatomic,copy)NSString *name;
@property(nonatomic,strong)NSNumber *age;
@property(nonatomic,weak)id delegate;
@end
Person.m
#import "Person.h"
@implementation Person
@end
在控制器中 創建一個Person對象,
Person *pson = [[Person alloc] init];
pson.name = @"wc";
pson.age = @99;
[pson performSelector:@selector(eat) withObject:nil];
顯然這里我們并沒有時間eat方法,程序未找到eat的具體實現而直接崩潰。
補救措施1:
在+(BOOL) resolveInstanceMethod:(SEL)selector中動態添加eat方法。
+(BOOL) resolveInstanceMethod:(SEL)selector
{
NSString *selectorStr = NSStringFromSelector(selector);
if ([selectorStr isEqualToString:@"eat"])
{
class_addMethod(self, selector, (IMP)eat, "v@:");
return YES;
}
return [self resolveClassMethod:selector];
}
具體實現:
void eat()
{
NSLog(@"person eat");
}
補救措施2:使用備用接收者,此時,回調用這個方法-(id)forwardingTargetForSelector:(SEL)aSelector,這里為peson增加一個代理屬性,將person沒有實現的方法,交給代理去做
#import@interface Person : NSObject
@property(nonatomic,copy)NSString *name;
@property(nonatomic,strong)NSNumber *age;
//代理,將person沒有實現的方法,交給代理去做
@property(nonatomic,weak)id delegate;
@end
//為pson對象添加了代理
Person *pson = [[Person alloc] init];
pson.name = @"wc";
pson.age = @99;
pson.delegate = self;
[pson performSelector:@selector(eat) withObject:nil];
//代理實現eat方法
-(void)eat
{
NSLog(@"viewcontroller eat");
}
//這里返回代理,讓代理去完成
-(id)forwardingTargetForSelector:(SEL)aSelector
{
return _delegate;
}
當然這里不一定要用到代理,只要是你在這個上下文中可以訪問到的合適的對象就行。
如果到這一步還沒有采取補救措施,只能啟動完整地消息轉發機制了。
補救措施3:
-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector
-(void)forwardInvocation:(NSInvocation *)anInvocation
默認的-(void)forwardInvocation:(NSInvocation *)anInvocation的實現是調用-(void)doesNotRecognizeSelector:(SEL)aSelector方法,在該方法中將會拋出異常,導致程序崩潰。
-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *methodSignature = nil;
NSString *selectorStr = NSStringFromSelector(aSelector);
if ([selectorStr isEqualToString:@"eat"])
{
methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
//方法簽名
return methodSignature;
}
return [self methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"forwordInvocation");
SEL selector = anInvocation.selector;
if ([_delegate respondsToSelector:selector])
{
[anInvocation invokeWithTarget:_delegate];
}
else
{
return[self forwardInvocation:anInvocation];
}
}
這里消息轉發,完全實現交給了我代理去做了。
參考鏈接:http://blog.csdn.net/liangliang103377/article/details/39007683