RunTime之常用場景

在上篇中記錄了幾個常用的api的介紹,這篇主要系統的整理一下在平常項目開發中經常用到RunTime的場景,分別為"發送消息","消息轉發","交換方法","動態添加方法","給分類添加屬性"幾種場景,通常第一種和第二種大多是共同作用的,接下來就分別介紹.
1.發送消息(void objc_msgSend)
在面向對象編程中,對象調用方法叫做"發送消息"。在編譯時,程序的源代碼就會從對象發送消息轉換成Runtime的objc_msgSend函數調用。如我們平時寫的源碼[person say]其實就會轉換成objc_msgSend(person, selector)其中selector可以理解成方法的唯一標示,是根據函數名以及參數生成的,這也就是為什么在同一個文件中不能有同名的函數的原因,但是在不同的文件中可以,因為每個文件中的實例方法都是分開的,就算是生成的selectoer相同但是存在兩個不同的地方,找起來也不會出問題;
objc_msgSend函數的調用過程

第一步:檢測這個selector是不是要忽略的。
第二步:檢測這個target是不是nil對象。nil對象發送任何一個消息都會被忽略掉。
第三步:調用實例方法時,它會首先在自身isa指針指向的類(class)methodLists中查找法,如果找不到則會通過class的
super_class指針找到父類的類對象結構體,然后從methodLists中查找該方法,如果仍然找不到,則繼續通過super
_class向上一級父類結構體中查找,直至根class;當我們調用某個某個類方法時,它會首先通過自己的isa指針找到
metaclass(也就是元類),并從其中methodLists中查找該類方法,如果找不到則會通過metaclass的super_class指針找到父類的
metaclass對象結構體,然后從methodLists中查找該方法,如果仍然找不到,則繼續通過super_class向上一級父類
結構體中查找,直至根metaclass;
如果以上散步都找不到的話如果不進行其他的處理就會crash,報出找不到此方法的錯誤;但是如果我們在消息轉發的過程中做處理的話就不同了,下面咱們就來看一下在消息轉發的過程中怎么來做處理.

2.消息轉發

第一步:
+(BOOL)resolveInstanceMethod:(SEL)sel;+(BOOL)resolveClassMethod:(SEL)sel;
當我們調用一個沒有實現的方法的時候會通過上面兩個方法來判斷是否可以找到該方法的實現,如果返回NO,則會進入下一步(說明在解析方法的這一
步不做處理,在轉發的其他過程處理,則處理進入第二個步驟),如果返回YES,說明要在此方法中處理,則可以通過class_addMethod來動態添加方法
的實現,消息得到處理,結束

第二步:
- (id)forwardingTargetForSelector:(SEL)aSelector
從字面上來理解這個方法就是轉發這個方法到一個其他的target,也就是說如果在這個方法中返回一個可以執行這個方法的對象,也使這個消息得到處
理.比如你在Person類和Animal類中同時聲明了-(void)eat方法,但是只實現了Person類中的方法,沒實現Animal類中方法,但是你在控制器中又
調用了Animal中-(void)eat方法,如果不做處理肯定會crash,但是如果在消息轉發的第二部也就是本方法中返回一個Person的對象,你會發現它執
行的是Person類中eat方法.
當然你在這一步中也可以不作處理也就是返回nil(沒有指定可以實現這個方法的對象),則會進入消息轉發的下一步.

第三步:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
如果在上一步返回nil則會進入此方法生成方法簽名,如果此步驟方法中返回nil的話,直接調用doesNotRecognizeSelector:返回異常
如果正常生成方法簽名,則進行最后一步。

第四步:
- (void)forwardInvocation:(NSInvocation *)anInvocation
我們可以通過anInvocation對象做很多處理,
比如修改實現方法,修改響應對象等,如果方法調用成功,則結束。
如果失敗,則進入`doesNotRecognizeSelector`方法,
若我們沒有實現這個方法,那么就會crash

實例如下:
嘗試在方法轉發第一步中規避crash

首先在`Person`類中 在.h中聲明兩個方法,但不去實現:
 -(void)unKnowSel_obj; 
+(void)unKonwSel_class;
 在.m中實現這兩個方法:
 -(void)noObjMethod{ 
NSLog(@"未實現這個實例方法");
 } 
+(void)noClassMethod{ 
NSLog(@"未實現這個類方法"); 
} 
并且重寫消息轉發的方法: 
// 當一個對象調用未實現的方法,會調用這個方法處理,并且會把對應的方法列表傳過來.
 //注意:實例方法是存在于當前對象對應的類的方法列表中
 +(BOOL)resolveInstanceMethod:(SEL)sel{ 
SEL aSel = NSSelectorFromString(@"noObjMethod"); 
Method aMethod = class_getInstanceMethod(self, aSel);
 class_addMethod(self, sel, method_getImplementation(aMethod), "v@:"); 
return YES; 
} 
// 當一個類調用未實現的方法,會調用這個方法處理,并且會把對應的方法列表傳過來. 
//注意:類方法是存在于類的元類的方法列表中 
+(BOOL)resolveClassMethod:(SEL)sel{ 
SEL aSel = NSSelectorFromString(@"noClassMethod"); 
Method aMethod = class_getClassMethod(self, aSel); 
class_addMethod(object_getClass(self), sel, method_getImplementation(aMethod), "v@:"); return YES; 
} 
在VC中調用未實現的兩個方法:
Person* person = [[Person alloc] init]; 
[person unKnowSel_obj];
[Person unKonwSel_class];

打印結果

RuntimeTest[4503:948902] 未實現這個實例方法
RuntimeTest[4503:948902] 未實現這個類方法

接下來我們嘗試在消息轉發第二部中去處理(注意把消息轉發中的第一步方法注釋掉或返回NO)

聲明一個`Boss`類,并在.m中實現方法:
 @implementation Boss 
-(void)unKnowSel_obj{ 
NSLog(@"unKnowSel_obj_Boss"); 
} 
@end 
在`Person`類中重寫方法:
 -(id)forwardingTargetForSelector:(SEL)aSelector{
 return [[Boss alloc] init]; 
} 
在VC中調用未實現的兩個方法: 
Person* person = [[Person alloc] init]; 
[person unKnowSel_obj];

打印結果

RuntimeTest[4540:956249] unKnowSel_obj_Boss

接下來接著測試消息轉發第三四步

在`Person`類中重寫方法:
 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { 
if ([NSStringFromSelector(aSelector) isEqualToString:@"unKnowSel_obj"]) { 
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
 } 
return [super methodSignatureForSelector:aSelector];
 }
 -(void)forwardInvocation:(NSInvocation *)anInvocation{
 [anInvocation invokeWithTarget:[[Boss alloc] init]]; 
}
 在VC中調用未實現的兩個方法: Person* person = [[Person alloc] init];
 [person unKnowSel_obj];

大家可以自行測試一下結果

3.交換方法(method_exchangeImplementations)
首先來理一理SEL,Method,IMP之間的關系
SELselector在Objective-C中的表示類型,而selector可以理解為區別方法的ID。
IMP是“implementation”的縮寫,它是由編譯器生成的一個函數指針。當你發起一個消息后,這個函數指針決定了最終執行哪段代碼.
Method是這樣typedef struct objc_method *Method;聲明的;是一個objc_method類型的結構體,在看objc_method是如下這樣的

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;//方法名
    char *method_types                                       OBJC2_UNAVAILABLE;//方法類型,是一個char指針,存儲著方法的參數類型和返回值類型
    IMP method_imp                                           OBJC2_UNAVAILABLE;//方法實現
} 

也就是說當我們調用一個函數時,編譯過程中會根據函數名以及參數等信息生成一個可以表示這個函數的一個唯一標示,就是selector,并且會根據函數的實現以及函數的地址生成一個IMP(函數指針)來指向函數的真正內容,一般來說SEL和IMP是一一對應的,當運行時,就會根據唯一的標示來找到函數的實現來打到目的,因此動態交換方法其實就是在運行時,把SEL和IMP的關系進行重新整理,讓原來的SEL指向另外一個IMP函數地址來打到交換的目的.實例如下

+(void)load{
    [super load];
//    交換實例方法
//    Method fromMethod = class_getInstanceMethod([self class], @selector(btAction:));
//    Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingbtAction:));
//    //添加判斷
//    if (!class_addMethod([self class], @selector(btAction:), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
//        method_exchangeImplementations(fromMethod, toMethod);
//    }
    //使用封裝的方法進行測試
    [self swizzleSelectorOriginalSel:@selector(btAction:) withSwizzledSelector:@selector(swizzlingbtAction:)];
}
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.view.backgroundColor = [UIColor lightGrayColor];
    self.bt = [UIButton buttonWithType:UIButtonTypeSystem];
    _bt.frame = CGRectMake(100, 100, 100, 100);
    [_bt setTitle:@"RunTime" forState:UIControlStateNormal];
    [self.view addSubview:_bt];
    _bt.backgroundColor = [UIColor redColor];
    [_bt addTarget:self action:@selector(btAction:) forControlEvents:UIControlEventTouchUpInside];

}
-(void)btAction:(UIButton *)bt{
    NSLog(@"原方法");
}
-(void)swizzlingbtAction:(UIButton *)bt{
    NSLog(@"替換方法");
    UIViewController *vc = [Mediar remotViewControllerWithClaStr:@"OneViewController"];
    [self presentViewController:vc animated:YES completion:nil];
}
#pragma mark - 封裝替換方法
+(void)swizzleSelectorOriginalSel:(SEL)originalSelector withSwizzledSelector:(SEL)swizzledSelector {
    Class class = [self class];
    
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    BOOL didAddMethodInit=class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    
    if (didAddMethodInit) {
        class_addMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }else{
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

在項目中有時會處理數組越界問題,你在NSArray的分類中添加安全取值方法,注意新添加的安全方法在分類中最后還是添加在了NSArray的methodlist中,但是由于數組是類簇的原因,因此平時調用的取值方法實際上如果數組是空的則會生成一個__NSArray0的對象,如果只有個元素則生成__NSSingleObjectArrayI,如果多與1個則是中__NSArrayI;但是對于可變數組來說都是__NSArrayM,不管是空數組還是只有一個元素的數組還是多個元素的數組;下面來看一個例子:

首先在分類中添加一個安全取值的方法
@implementation NSArray (safe)
-(id)objectAtIndexSafe:(NSUInteger)index{
    if (index>=self.count) {
        return nil;
    }
    else{
        return [self objectAtIndexSafe:index];
    }
}
@end
然后在+(void)load方法中交換方法
+(void)load{
    Method originalMethod_id = class_getInstanceMethod([NSArray class], NSSelectorFromString(@"objectAtIndexSafe:"));
    Method swizzledMethod_id = class_getInstanceMethod(NSClassFromString(@"__NSArrayM"), NSSelectorFromString(@"objectAtIndex:"));
    method_exchangeImplementations(originalMethod_id, swizzledMethod_id);
//當然你也可以不用exchange直接用setImplementaion來換掉IMP,但是這樣的話分類中添加的方法中注意不能用自身調用自身
   // method_setImplementation(swizzledMethod_id, method_getImplementation(originalMethod_id));

    Method originalMethod_id1 = class_getInstanceMethod([NSArray class], NSSelectorFromString(@"objectAtIndexSafe:"));
    Method swizzledMethod_id1 = class_getInstanceMethod(NSClassFromString(@"__NSArray0"), NSSelectorFromString(@"objectAtIndex:"));
    method_exchangeImplementations(originalMethod_id1, swizzledMethod_id1);

    Method originalMethod_id2 = class_getInstanceMethod([NSArray class], NSSelectorFromString(@"objectAtIndexSafe:"));
    Method swizzledMethod_id2 = class_getInstanceMethod(NSClassFromString(@"__NSArray0"), NSSelectorFromString(@"objectAtIndex:"));
    method_exchangeImplementations(originalMethod_id2, swizzledMethod_id2);
}
接下來在-(void)viewdidload中測試
    NSArray *arr2 = [[NSArray alloc]init];
    NSString *str2 = [arr2 objectAtIndex:1];
    NSLog(@"%@",str2);

打印結果為:

RuntimeTest3[43662:8248532] (null)

4.給分類添加屬性
可以參考之前寫的"UIButton擴大響應范圍"的那篇文章.

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

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,789評論 0 9
  • 繼上Runtime梳理(四) 通過前面的學習,我們了解到Objective-C的動態特性:Objective-C不...
    小名一峰閱讀 777評論 0 3
  • 轉載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 772評論 0 2
  • 本文轉載自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex閱讀 799評論 0 1
  • 這篇文章完全是基于南峰子老師博客的轉載 這篇文章完全是基于南峰子老師博客的轉載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,649評論 33 466