OC的消息轉發

大家都知道,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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容