runtime——消息機制

1453882669997129.jpg

本文受權轉載,做者:Sindri的小巢(簡書java

從異常提及算法

咱們都知道,在iOS中存在這麼一個通用類類型id,它能夠用來表示任何對象的類型 —— 這意味着咱們使用id類型的對象調用任何一個方法,編譯器都不會進行報錯。好比下面這段代碼:編程

1
2
id wrongArr = @ "This is a NSString instance." ;
[wrongArr addObject: @ "The operate will crash your application" ];

不出意外的,編譯器會給你這麼一個信息而後華麗麗的崩潰了。相信幾乎全部的開發者們在開發生涯中都遇到過這種崩潰信息:數組

1
-[__NSCFConstantString addObject:]: unrecognized selector sent to instance 0x10675c060

很簡單,咱們朝着一個地址爲0x10675c060的實例對象發送了不屬於這個對象的方法。這句話不是instance 0x10675c060 called unrecognized selector,而是消息發送錯誤。實際上,咱們每一次對OC對象的方法調用都是一次消息的發送緩存

783864-d2544915fc753f76.jpg

消息發送異常性能優化

關於靜態語言和動態語言app

這裏要先介紹計算機的開發語言的一個專業名詞:動態語言和靜態語言。確切的說,OC是一門動態語言。動態語言和靜態語言二者的區別以下:ide

  • 靜態語言: 靜態語言在運行前會進行類型判斷,類的全部成員、方法都會在編譯階段肯定好內存地址。類成員只能訪問屬於本身的方法和變量,像上面的調用代碼沒法經過編譯,會直接引發編譯器報錯。但由於如此,靜態語言結構規範、便於調試、且能夠進行多樣的性能優化。常見的靜態語言包括java/C++/C等函數

  • 動態語言:大部分的判斷工做被推遲到運行時進行,類的成員變量、方法地址都在運行時確認。能夠在運行時動態的添加類成員、方法等。具備較高的靈活性和可定製性、便於閱讀,但方法一般沒法進行內聯等優化性能

兩種語言孰優孰略本人不在這裏作判斷,可是要知道的是smalltalk是動態語言的鼻祖,更是OC發展的最大推進力。在smalltalk中,全部的東西都是對象(或者都應該被當作對象),例如表達式2 + 3被理解成向對象2發送了消息+,其中接收的參數是 3

消息發送

在前篇runtime-屬性與變量中咱們導入過runtime的頭文件實現了一鍵歸檔功能,今天咱們要導入另一個文件:

在OC中,調用一個方法的格式以下:

1
[davin playWith: friend];

在方法調用的時候,runtime會將上面的方法調用轉換成一個C語言的函數調用,表示朝着davin發送了一個playWith:消息,並傳入了friend這個參數:

1
objc_msgSend(davin, @selector(playWith:), friend);

那麼在這個C語言函數中發生了什麼事情?編譯器是如何找到這個類的方法的呢?蘋果開源了runtime的實現代碼,其中爲了高度優化性能,蘋果使用匯編實現了這個函數(源碼處於Source/objc-msg-arm.s文件下):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*****************************************************************
  *
  * id objc_msgSend(id self, SEL    _cmd,...);
  *
  *****************************************************************/
     ENTRY objc_msgSend
     MESSENGER_START
     cbz    r0, LNilReceiver_f   // 判斷消息接收者是否爲nil
     ldr    r9, [r0]    // r9 = self->isa
     CacheLookup NORMAL   // 到緩存中查找方法
LCacheMiss:          // 方法未緩存
     MESSENGER_END_SLOW
     ldr    r9, [r0,  #ISA]
     b    __objc_msgSend_uncached
LNilReceiver:       // 消息接收者爲nil處理
     mov    r1,  #0
     mov    r2,  #0
     mov    r3,  #0
     FP_RETURN_ZERO
     MESSENGER_END_NIL
     bx    lr
LMsgSendExit:
     END_ENTRY objc_msgSend

即便不懂彙編,上面的代碼經過註釋後也足以讓各位一窺究竟。從上述代碼中咱們能夠看到一個方法調用過程當中發生的事情,包括:

  • 判斷接收者是否爲nil,若是爲nil,清空寄存器,消息發送返回nil

  • 到類緩存中查找方法,若是存在直接返回方法

  • 沒有找到緩存,到類的方法列表中依次尋找

查找方法實現是經過_class_lookupMethodAndLoadCache3這個奇怪的函數完成的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
     return  lookUpImpOrForward(cls, sel, obj,
                           YES /*initialize*/ , NO /*cache*/ , YES /*resolver*/ );
}
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
                    bool initialize, bool cache, bool resolver)
{
     Class curClass;
     IMP methodPC = nil;
     Method meth;
     bool triedResolver = NO;
     methodListLock.assertUnlocked();
     // 若是傳入的cache爲YES,到類緩存中查找方法緩存
     if  (cache) {
         methodPC = _cache_getImp(cls, sel);
         if  (methodPC)  return  methodPC;
     }
     // 判斷類是否已經被釋放
     if  (cls == _class_getFreedObjectClass())
         return  (IMP) _freedHandler;
     // 若是類未初始化,對其進行初始化。若是這個消息是initialize,那麼直接進行類的初始化
     if  (initialize  &&  !cls->isInitialized()) {
         _class_initialize (_class_getNonMetaClass(cls, inst));
     }
  retry:
     methodListLock.lock();
     // 忽略在GC環境下的部分消息,好比retain、release等
     if  (ignoreSelector(sel)) {
         methodPC = _cache_addIgnoredEntry(cls, sel);
         goto done;
     }
     // 遍歷緩存方法,若是找到,直接返回
     methodPC = _cache_getImp(cls, sel);
     if  (methodPC) goto done;
     // 遍歷類自身的方法列表查找方法實現
     meth = _class_getMethodNoSuper_nolock(cls, sel);
     if  (meth) {
         log_and_fill_cache(cls, cls, meth, sel);
         methodPC = method_getImplementation(meth);
         goto done;
     }
     // 嘗試向上遍歷父類的方法列表查找實現
     curClass = cls;
     while  ((curClass = curClass->superclass)) {
         // Superclass cache.
         meth = _cache_getMethod(curClass, sel, _objc_msgForward_impcache);
         if  (meth) {
             if  (meth != (Method)1) {
                 log_and_fill_cache(cls, curClass, meth, sel);
                 methodPC = method_getImplementation(meth);
                 goto done;
             }
             else  {
                 // Found a forward:: entry in a superclass.
                 // Stop searching, but don't cache yet; call method
                 // resolver for this class first.
                 break ;
             }
         }
         // 查找父類的方法列表
         meth = _class_getMethodNoSuper_nolock(curClass, sel);
         if  (meth) {
             log_and_fill_cache(cls, curClass, meth, sel);
             methodPC = method_getImplementation(meth);
             goto done;
         }
     }
     // 沒有找到任何的方法實現,進入消息轉發第一階段「動態方法解析」
     // 調用+ (BOOL)resolveInstanceMethod: (SEL)selector
     // 徵詢接收者所屬的類是否可以動態的添加這個未實現的方法來解決問題
     if  (resolver  &&  !triedResolver) {
         methodListLock.unlock();
         _class_resolveMethod(cls, sel, inst);
         triedResolver = YES;
         goto retry;
     }
     // 仍然沒有找到方法實現進入消息轉發第二階段「備援接收者」
     // 前後會調用 -(id)forwardingTargetForSelector: (SEL)selector
     // 以及 - (void)forwardInvocation: (NSInvocation*)invocation 進行最後的補救
     // 若是補救未成功拋出消息發送錯誤異常
     _cache_addForwardEntry(cls, sel);
     methodPC = _objc_msgForward_impcache;
  done:
     methodListLock.unlock();
     assert(!(ignoreSelector(sel)  &&  methodPC != (IMP)&_objc_ignored_method));
     return  methodPC;
}

上面就是一個方法調用的所有過程。主要分爲三個部分:

  • 查找是否存在對應的方法緩存,若是存在直接返回調用

爲了優化性能,方法的緩存使用了散列表的方式,在下一部分會進行比較詳細的講述

  • 未找到緩存,到類自己或順着類結構向上查找方法實現,返回的method_t *類型也被命名爲Method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//非加鎖狀態下查找方法實現
static method_t * getMethodNoSuper_nolock(Class cls, SEL sel)
{
     runtimeLock.assertLocked();
     assert(cls->isRealized());
     // fixme nil cls?
     // fixme nil sel?
     for  (auto mlists = cls->data()->methods.beginLists(),
             end = cls->data()->methods.endLists();
              mlists != end;
                ++mlists)
     {
         method_t *m = search_method_list(*mlists, sel);
         if  (m)  return  m;
     }
     return  nil;
}
// 搜索方法列表
static method_t * search_method_list(const method_list_t *mlist, SEL sel)
{
     int methodListIsFixedUp = mlist->isFixedUp();
     int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
     if  (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
           // 對有序數組進行線性探測
           return  findMethodInSortedMethodList(sel, mlist);
     else  {
         // Linear search of unsorted method list
         for  (auto& meth : *mlist) {
             if  (meth.name == sel)  return  &meth;
         }
     }
#if DEBUG
     // sanity-check negative results
     if  (mlist->isFixedUp()) {
         for  (auto& meth : *mlist) {
             if  (meth.name == sel) {
                 _objc_fatal( "linear search worked when binary search did not" );
             }
         }
     }
#endif
     return  nil;
}

若是在這個步驟中找到了方法的實現,那麼將它加入到方法緩存中以便下次調用能快速找到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 記錄而且緩存方法
static void log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
     if  (objcMsgLogEnabled) {
         bool cacheIt = logMessageSend(implementer->isMetaClass(),
                                 cls->nameForLogging(),
                                 implementer->nameForLogging(),
                                 sel);
         if  (!cacheIt)  return ;
     }
#endif
     cache_fill (cls, sel, imp, receiver);
}
//在無加鎖狀態下緩存方法
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
     cacheUpdateLock.assertLocked();
     if  (!cls->isInitialized())  return ;
     if  (cache_getImp(cls, sel))  return ;
     cache_t *cache = getCache(cls);
     cache_key_t key = getKey(sel);
     // 若是緩存佔用不到3/4,進行緩存。
     mask_t newOccupied = cache->occupied() + 1;
     mask_t capacity = cache->capacity();
     if  (cache->isConstantEmptyCache()) {
         cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
     }
     else  if  (newOccupied expand();
     }
     bucket_t *bucket = cache->find(key, receiver);
     if  (bucket->key() == 0) cache->incrementOccupied();
     bucket->set(key, imp);
}

若是在類自身中沒有找到方法實現,那麼循環獲取父類,重複上面的查找動做,找到後再將方法緩存到本類而非父類的緩存中

  • 未找到任何方法實現,觸發消息轉發機制進行最後補救

其中消息轉發分爲兩個階段,第一個階段咱們能夠經過動態添加方法以後讓編譯器再次執行查找方法實現的過程;第二個階段稱做備援的接收者,就是找到一個接盤俠來處理這個事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
     // 非beta類的狀況下直接調用 resolveInstanceMethod 方法
     if  (! cls->isMetaClass()) {
         _class_resolveInstanceMethod(cls, sel, inst);
     }
     else  {
         // 先調用 resolveClassMethod 請求動態添加方法
         // 而後進行一次查找判斷是否處理完成
         // 若是沒有添加,再調用 resolveInstanceMethod 方法
         _class_resolveClassMethod(cls, sel, inst);
         if  (!lookUpImpOrNil(cls, sel, inst,
                       NO /*initialize*/ , YES /*cache*/ , NO /*resolver*/ ))
         {
             _class_resolveInstanceMethod(cls, sel, inst);
         }
     }
}

方法緩存

在上一篇runtime文章中筆者已經說過對於OC的每個對象來講,本質上都是一個objc_class的結構體封裝,在最新的runtime源碼的objc-runtime-new.h中,objc_class的結構以下(筆者已經略去了大部分的函數):

1
2
3
4
5
6
7
8
9
10
11
12
struct objc_class : objc_object {
     Class superclass;           // Class ISA;
     cache_t cache;              // formerly cache pointer and vtable
     class_data_bits_t bits;     // class_rw_t * plus custom rr/alloc flags
     class_rw_t *data() {
         return  bits.data();
     }
     void setData(class_rw_t *newData) {
     bits.setData(newData);
     }
     // .........
}

結構一目瞭然,很明顯cache存儲着咱們在方法調用中須要查找的方法緩存。做爲緩存方法的cache採用了散列表,以此來大幅度提升檢索的速度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))
struct cache_t {
     struct bucket_t *_buckets;
     mask_t _mask;
     mask_t _occupied;
     // functions
}
// cache method
buckets = (cache_entry **)cache->buckets;
for  (index = CACHE_HASH(sel, cache->mask);
      buckets[index] != NULL;
      index = (index+1) & cache->mask)
{ }
buckets[index] = entry;

在每次調用完未被緩存的方法時,下面的那段緩存方法的代碼就會調用。蘋果利用了sel的指針地址和mask作了一個簡單的位運算,而後找到一個空槽存儲起來。 以此咱們能夠推出從緩存中查找sel實現的代碼CacheLookup,可是爲了高度優化性能,蘋果一樣喪心病狂的使用匯編完成了查找的步驟,官方給出的註釋足夠咱們大體看明白這段代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.macro CacheLookup
     ldrh    r12, [r9,  #CACHE_MASK]    // r12 = mask
     ldr    r9, [r9,  #CACHE]    // r9 = buckets
. if  $0 == STRET  ||  $0 == SUPER_STRET
     and    r12, r12, r2         // r12 = index = SEL & mask
. else
     and    r12, r12, r1         // r12 = index = SEL & mask
.endif
     add    r9, r9, r12, LSL  #3    // r9 = bucket = buckets+index*8
     ldr    r12, [r9]         // r12 = bucket->sel
2:
. if  $0 == STRET  ||  $0 == SUPER_STRET
     teq    r12, r2
. else
     teq    r12, r1
.endif
     bne    1f
     CacheHit $0
1:
     cmp    r12,  #1
     blo    LCacheMiss_f         // if (bucket->sel == 0) cache miss
     it    eq             // if (bucket->sel == 1) cache wrap
     ldreq    r9, [r9,  #4]        // bucket->imp is before first bucket
     ldr    r12, [r9,  #8]!        // r12 = (++bucket)->sel
     b    2b
.endmacro

具體的源碼能夠從蘋果開源這裏下載,這個方法蘋果已經註釋的足夠清晰了。

上面全部的操做都是對方法的緩存、查找操做,那麼方法到底是什麼?在OC中方法被抽象成的數據類型是Method,若是瞭解而且使用過runtime的讀者們可能瞭解這個類型,其結構以下:

1
2
3
4
5
6
struct old_method {
     SEL method_name;
     char *method_types;
     IMP method_imp;
};
typedef struct method_t *Method;
  • method_imp方法的實現代碼,你能夠把它看作一個block。事實上,後者確實能夠轉換成一個IMP類型來實現某些黑魔法。

  • method_types方法的參數編碼,什麼意思?在屬性與變量中我說過每一種數據類型有着本身對應的字符編碼,這個表示方法返回值、參數的字符編碼,好比-(void)playWith:(id)的字符編碼爲v@:@

  • method_name顧名思義,方法的名字。一般咱們使用@selector()的方式獲取一個方法的sel地址,這個被用來進行散列計算存儲方法的imp實現。因爲SEL類型採用了散列的算法,所以若是同一個類中存在一樣名字的方法,那麼就會致使方法的imp地址沒法惟一化。這也是蘋果不容許同名不一樣參數類型的方法存在的緣由

消息轉發

一般狀況下,在咱們調用不屬於某個對象的方法的時候,咱們的應用就會崩潰crash,好比筆者經歷過好幾回由於後臺返回的NSNull類型致使了測試反饋應用閃退。經過上面的方法調用源碼咱們能夠看到並非沒有找到方法實現就直接發生了崩潰,在崩潰以前編譯器會進行消息轉發機制,總共給了咱們三次機會來避免這樣的崩潰並儘量的找到方法的響應者。

1464059012694122.jpg

消息轉發階段

首先先看第一階段。咱們都知道,在iOS開發當中咱們須要很是的注意用戶體驗。單純的是由於數據類型錯誤而致使應用出現閃退,這樣的處理會極大的影響使用app的用戶。所以,咱們能夠經過class_addMethod這個函數來動態的添加這種錯誤的處理(類能夠在objc_registerClassPair完成類的註冊以後動態的添加方法,但不容許動態添加屬性,參考category機制)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
id wrongTypeGetter(id object, SEL sel) {
     return  nil;
}
void wrongTypeSetter(id object, SEL sel, id value) {
     // do nothing
}
+ (BOOL)resolveInstanceMethod: (SEL)selector
{
     NSString * selName = NSStringFromSelector(selector);
     if  ([sel hasPrefix: @ "set" ]) {
         class_addMethod(self, selector, (IMP)wrongTypeSetter,  "v@:@" );
     else  {
         class_addMethod(self, selector, (IMP)wrongTypeGetter,  "@@:" )
     }
}

在第二階段最開始的時候,這時候已經默許了你並不想使用消息接收者來響應這個方法,因此咱們須要找到消息接盤俠 —— 這並非一件壞事。在iOS中不支持多繼承,儘管咱們能夠經過協議和組合模式實現僞多繼承。僞多繼承和多繼承的區別在於:多繼承是將多個類的功能組合到一個對象當中,而僞多繼承多個類的功能依舊分佈在不一樣對象當中,可是對象彼此對消息發送者透明。那麼,若是咱們消息轉發給另外一個對象能夠用來實現這種僞多繼承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface Person: NSObject
@property (nonatomic, strong) NSNumber * age;
@end
@implementation Person
- (id)forwardingTargetForSelector: (SEL)aSelector
{
     // 甚至能夠經過runtime遍歷本身屬性找到能夠響應方法的接盤俠
     NSString * selName = NSStringFromSelector(aSelector);
     if  ([selName hasSuffix: @ "Value" ]) {
         return  self.age;
     }
     return  nil;
}
@end
// View controller
id p = [[Person alloc] init];
[p setAge: @(18)];
NSLog(@ "%lu, %.2f" , [p integerValue], [p doubleValue]);     //18, 18.00

若是你依舊沒有爲這個方法找到另一個調用者,那麼阻止你app閃退的最後時刻到來了。runtime須要生成一個methodSignature變量來組裝,這將經過調用消息接收者的-(NSMethodSignature *)methodSignatureForSelector:獲取,這個變量包含了方法的參數類型、參數個數以及消息接收者等信息。接着把這個變量組裝成一個NSInvocation對象進行最後一次的消息轉發,調用接收者的-forwardInvocation:來進行最後的挽救機會。這意味着咱們能夠盡情的對invocation作任何事情,包括隨意修改參數值、消息接收者等。我最常拿來乾的事情就是減小數組的遍歷工做:

1
2
3
4
5
6
7
8
9
10
@implementation NSArray(LXDRuntime)
- (void)forwardInvocation: (NSInvocation *)anInvocation
{
     for  (id item  in  self) {
         if  ([item respondsToSelector: anInvocation.selector]) {
             [anInvocation invokeWithTarget: item];
         }
     }
}
@end

總的來講整個消息發送的過程能夠概括成下面這張圖:

1464059057867047.jpg

消息發送全過程

雖然消息轉發能夠幫助咱們顯著的減小app的閃退率,可是在開發階段千萬不要加入這些特性。最好是在app申請上架的那個階段再加,這樣不至於app其餘消息發送異常被咱們忽略了。

消息機制黑魔法

上面筆者講解了關於一個調用方法之中發生的事情,確實很是的複雜。一樣的這些特性也很是值得咱們去學習使用,runtime提供了一系列關於Method的方法給咱們實現面向切面編程的工做。這些工做包括了替換原有方法實現,交換方法實現等等工做。

假設如今我須要一個圓角按鈕,而且保證點擊觸發事件的範圍必需要這個圓以內,那麼經過一個UIButton+LXDRuntime的擴展來替換舊有-pointInside:withEvent:方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@interface UIButton(LXDRuntime)
@property (nonatomic, assign) BOOL roundTouchEnable;
@end
const void * LXDRoundTouchEnableKey = & LXDRoundTouchEnableKey;
@implementation UIButton(LXDRuntime)
- (BOOL)roundTouchEnable
{
     return  [objc_getAssociatedObject(self, LXDRoundTouchEnableKey) boolValue];
}
- (void)setRoundTouchEnable: (BOOL)roundTouchEnable
{
     objc_setAssociatedObject(self, LXDRoundTouchEnableKey, @(roundTouchEnable), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)replacePointInside: (CGPoint)point withEvent: (UIEvent *)event
{
     if  (CGRectGetWidth(self.frame) != CGRectGetHeight(self.frame)
         || !self. roundTouchEnable)
     {
         return  [ super  pointInside: point withEvent: event];
     }
     CGFloat radius = CGRectGetWidth(self.frame) / 2;
     CGPoint offset = CGPointMake(point.x - radius, point.y - radius);
     return  sqrt(offset.x * offset.x + offset.y * offset.y) <= radius;
}
// 替換方法實現
+ (void)initialize
{
     [ super  initialize];
     Method replaceMethod = class_getInstanceMethod([self class], @selector(replacePointInside:withEvent:));
     Method originMethod = class_getInstanceMethod([self class], @selector(pointInside:withEvent:));
     method_setImplementation(originMethod, method_getImplementation(replaceMethod));
}
@end

那麼當我須要個人按鈕只響應圓形點擊區域的時候,只須要設置button.roundTouchEnable  = YES,就會自動實現了圓形點擊的判斷。除了上面的上面的方法替換,還有另外一個經常使用的黑魔法是交換兩個方法的實現。歸功於Method的特殊結構,將方法名字sel跟代碼實現imp分隔開來。你能夠把imp當作是一個block代碼塊,而交換實現的操做就至關於把這兩個block交換了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@interface Person : NSObject
@property (nonatomic, strong) NSString * name;
@property (nonatomic, strong) NSNumber * age;
@end
@implementation Person
+ (void)initialize
{
     [ super  initialize];
     Method ageGetter = class_getInstanceMethod([self class], @selector(age));
     Method nameGetter = class_getInstanceMethod([self class], @selector(name));
     method_exchangeImplementations(ageGetter, nameGetter);
}
// View controller
Person * p = [[Person alloc] init];
p.age = @(56);
p.name = @ "Job Steve" ;
NSLog(@ "%@ is %@ year old" , p.name, p.age);
// LXDRuntimeDemo[7316:244912] 56 is Job Steve year old
@end

上面的代碼交換了name和age的實現,用圖示來表示:

1464059111845457.jpg

方法交換

應該不難看出,method_exchangeImplementations之因此被推崇的緣由在於這種方式交換實現的時候不會致使原有的方法實現發生改變(從頭至尾,age的IMP跟name的IMP都沒有進行任何的修改),固然了,它的缺點也是很是明顯的:

  • 多人開發對同一個方法都進行方法替換/交換時,會使得業務邏輯複雜,很是的不利於調試

  • 被交換的方法實現會直接的影響到全部該類的實例對象以及子類,不適用於單個對象的實現

能夠說runtime提供的這些黑魔法都是雙刃劍,合理的運用能讓咱們更加的強大。另外,除了Method的黑魔法,還要提到一個IMP相關的使用陷阱。上文說過,IMP跟block是很是類似的東西,前者能夠跟函數指針強制轉換,所以能夠看作是一個特殊的block,一樣的系統提供了二者相互轉換的方法:imp_implementationWithBlock和imp_getBlock。按照上面說的,當調用方法轉換成消息轉發的時候,objc_msgSend自身已經存在了兩個參數id object以及SEL aSelector,那麼按照這種思路IMP和block的切換應該是這樣的:

1
2
3
4
5
6
7
8
9
10
+ (void)initialize
     void (^requestBlock)(id object, SEL aSelector, id URL, id parameters) =
         ^(id object, SEL aSelector, id URL, id parameters) {
         // do some networking request
     };
     IMP requestIMP = imp_implementationWithBlock(requestBlock);
     class_addMethod([self class], @selector(networkReuqest:parameters:), requestIMP,  "v@:@@" );
}
// View controller
[self performSelector: @selector(networkReuqest:parameters:) withObject: URL withObject: parameters];

上面這段代碼會crash的很是無厘頭,提示你EXC_BAD_ACCESS錯誤。重要的事情說三遍:

block參數不存在SEL!!

block參數不存在SEL!!

block參數不存在SEL!!

上面的代碼只要去掉了SEL aSelector這個參數,這段代碼就能正常執行。

尾話

runtime對於每個iOS開發者來講,都應該去了解。經過runtime的源碼實現,咱們能夠看到蘋果爲了性能優化武裝到牙齒的行爲,也能看到咱們書寫代碼深層之中鮮爲人知的實現。做爲runtime系列第二篇(第一篇),糾結了我好久纔開始動工,期間看源碼看的頭都大了,可是確實對我在開發的認識以及代碼的結構上有了更多的瞭解。最後,本文無代碼,仍是奉上蘋果的runtime源碼地址