iOS關(guān)于runtime機(jī)制

本文主要針對(duì)<objc/message.h>進(jìn)行討論
首先導(dǎo)入上面的頭文件

消息發(fā)送

在OC中,調(diào)用一個(gè)方法的格式如下:

[davin playWith: friend];

在方法調(diào)用的時(shí)候,runtime會(huì)將上面的方法調(diào)用轉(zhuǎn)換成一個(gè)C語言的函數(shù)調(diào)用,表示朝著davin發(fā)送了一個(gè)playWith:消息,并傳入了friend這個(gè)參數(shù):

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

那么在這個(gè)C語言函數(shù)中發(fā)生了什么事情?編譯器是如何找到這個(gè)類的方法的呢?蘋果開源了 runtime的實(shí)現(xiàn)代碼,其中為了高度優(yōu)化性能,蘋果使用匯編實(shí)現(xiàn)了這個(gè)函數(shù)(源碼處于Source/objc-msg-arm.s
文件下):

/*****************************************************************
 *
 * 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

即使不懂匯編,上面的代碼通過注釋后也足以讓各位一窺究竟。從上述代碼中我們可以看到一個(gè)方法調(diào)用過程中發(fā)生的事情,包括:
判斷接收者是否為nil,如果為nil,清空寄存器,消息發(fā)送返回nil

到類緩存中查找方法,如果存在直接返回方法

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

查找方法實(shí)現(xiàn)是通過

_class_lookupMethodAndLoadCache3

這個(gè)奇怪的函數(shù)完成的:

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;    
    }

    // 判斷類是否已經(jīng)被釋放
    if (cls == _class_getFreedObjectClass())
        return (IMP) _freedHandler;

    // 如果類未初始化,對(duì)其進(jìn)行初始化。如果這個(gè)消息是initialize,那么直接進(jìn)行類的初始化
    if (initialize  &&  !cls->isInitialized()) {
        _class_initialize (_class_getNonMetaClass(cls, inst));
    }

 retry:
    methodListLock.lock();

    // 忽略在GC環(huán)境下的部分消息,比如retain、release等
    if (ignoreSelector(sel)) {
        methodPC = _cache_addIgnoredEntry(cls, sel);
        goto done;
    }

    // 遍歷緩存方法,如果找到,直接返回
    methodPC = _cache_getImp(cls, sel);
    if (methodPC) goto done;

    // 遍歷類自身的方法列表查找方法實(shí)現(xiàn)
    meth = _class_getMethodNoSuper_nolock(cls, sel);
    if (meth) {
        log_and_fill_cache(cls, cls, meth, sel);
        methodPC = method_getImplementation(meth);
        goto done;
    }

    // 嘗試向上遍歷父類的方法列表查找實(shí)現(xiàn)
    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;
        }
    }

    // 沒有找到任何的方法實(shí)現(xiàn),進(jìn)入消息轉(zhuǎn)發(fā)第一階段“動(dòng)態(tài)方法解析”
    // 調(diào)用+ (BOOL)resolveInstanceMethod: (SEL)selector
    // 征詢接收者所屬的類是否能夠動(dòng)態(tài)的添加這個(gè)未實(shí)現(xiàn)的方法來解決問題
    if (resolver  &&  !triedResolver) {
        methodListLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        triedResolver = YES;
        goto retry;
    }

    // 仍然沒有找到方法實(shí)現(xiàn)進(jìn)入消息轉(zhuǎn)發(fā)第二階段“備援接收者”
    // 先后會(huì)調(diào)用 -(id)forwardingTargetForSelector: (SEL)selector 
    // 以及 - (void)forwardInvocation: (NSInvocation*)invocation 進(jìn)行最后的補(bǔ)救
    // 如果補(bǔ)救未成功拋出消息發(fā)送錯(cuò)誤異常
    _cache_addForwardEntry(cls, sel);
    methodPC = _objc_msgForward_impcache;

 done:
    methodListLock.unlock();

    assert(!(ignoreSelector(sel)  &&  methodPC != (IMP)&_objc_ignored_method));
    return methodPC;
}

上面就是一個(gè)方法調(diào)用的全部過程。主要分為三個(gè)部分:
查找是否存在對(duì)應(yīng)的方法緩存,如果存在直接返回調(diào)用為了優(yōu)化性能,方法的緩存使用了散列表的方式,在下一部分會(huì)進(jìn)行比較詳細(xì)的講述

未找到緩存,到類本身或順著類結(jié)構(gòu)向上查找方法實(shí)現(xiàn),返回的method_t *類型也被命名為Method

//非加鎖狀態(tài)下查找方法實(shí)現(xiàn) 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)) { // 對(duì)有序數(shù)組進(jìn)行線性探測(cè) 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; }

如果在這個(gè)步驟中找到了方法的實(shí)現(xiàn),那么將它加入到方法緩存中以便下次調(diào)用能快速找到:

// 記錄并且緩存方法
  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);
  }

  //在無加鎖狀態(tài)下緩存方法
  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,進(jìn)行緩存。
      mask_t newOccupied = cache->occupied() + 1;
      mask_t capacity = cache->capacity();
      if (cache->isConstantEmptyCache()) {
          cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
      }
      else if (newOccupied <= capacity / 4 * 3) {
      }
      else {
          // 擴(kuò)充緩存。為了性能,擴(kuò)充后原有緩存方法全部移除
          cache->expand();
      }
      bucket_t *bucket = cache->find(key, receiver);
      if (bucket->key() == 0) cache->incrementOccupied();
      bucket->set(key, imp);
  }

如果在類自身中沒有找到方法實(shí)現(xiàn),那么循環(huán)獲取父類,重復(fù)上面的查找動(dòng)作,找到后再將方法緩存到本類而非父類的緩存中
未找到任何方法實(shí)現(xiàn),觸發(fā)消息轉(zhuǎn)發(fā)機(jī)制進(jìn)行最后補(bǔ)救
其中消息轉(zhuǎn)發(fā)分為兩個(gè)階段,第一個(gè)階段我們可以通過動(dòng)態(tài)添加方法之后讓編譯器再次執(zhí)行查找方法實(shí)現(xiàn)的過程;第二個(gè)階段稱作備援的接收者,就是找到一個(gè)接盤俠來處理這個(gè)事件

void _class_resolveMethod(Class cls, SEL sel, id inst) { // 非beta類的情況下直接調(diào)用 resolveInstanceMethod 方法 if (! cls->isMetaClass()) { _class_resolveInstanceMethod(cls, sel, inst); } else { // 先調(diào)用 resolveClassMethod 請(qǐng)求動(dòng)態(tài)添加方法 // 然后進(jìn)行一次查找判斷是否處理完成 // 如果沒有添加,再調(diào)用 resolveInstanceMethod 方法 _class_resolveClassMethod(cls, sel, inst); if (!lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { _class_resolveInstanceMethod(cls, sel, inst); } } }

方法緩存在上一篇runtime文章中筆者已經(jīng)說過對(duì)于OC的每一個(gè)對(duì)象來說,本質(zhì)上都是一個(gè)objc_class的結(jié)構(gòu)體封裝,在最新的runtime源碼的objc-runtime-new.h
中,objc_class的結(jié)構(gòu)如下(筆者已經(jīng)略去了大部分的函數(shù)):

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);
    }
    // .........
}

結(jié)構(gòu)一目了然,很明顯cache存儲(chǔ)著我們?cè)诜椒ㄕ{(diào)用中需要查找的方法緩存。作為緩存方法的cache采用了散列表,以此來大幅度提高檢索的速度:

#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;

在每次調(diào)用完未被緩存的方法時(shí),下面的那段緩存方法的代碼就會(huì)調(diào)用。蘋果利用了sel的指針地址和mask做了一個(gè)簡(jiǎn)單的位運(yùn)算,然后找到一個(gè)空槽存儲(chǔ)起來。 以此我們可以推出從緩存中查找sel實(shí)現(xiàn)的代碼CacheLookup,但是為了高度優(yōu)化性能,蘋果同樣喪心病狂的使用匯編完成了查找的步驟,官方給出的注釋足夠我們大致看明白這段代碼:

.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

具體的源碼可以從蘋果開源這里下載,這個(gè)方法蘋果已經(jīng)注釋的足夠清晰了。
上面所有的操作都是對(duì)方法的緩存、查找操作,那么方法究竟是什么?在OC中方法被抽象成的數(shù)據(jù)類Method
,如果了解并且使用過runtime的讀者們可能了解這個(gè)類型,其結(jié)構(gòu)如下:

struct old_method {
    SEL method_name;
    char *method_types;
    IMP method_imp;
};

typedef struct method_t *Method;
方法的實(shí)現(xiàn)代碼,你可以把它看做一個(gè)block。事實(shí)上,后者確實(shí)可以轉(zhuǎn)換成一個(gè)IMP類型來實(shí)現(xiàn)某些黑魔法。method_types方法的參數(shù)編碼,什么意思?在屬性與變量中我說過每一種數(shù)據(jù)類型有著自己對(duì)應(yīng)的字符編碼,這個(gè)表示方法返回值、參數(shù)的字符編碼,比如-(void)playWith:(id)
的字符編碼為v@:@

method_name
顧名思義,方法的名字。通常我們使用@selector()
的方式獲取一個(gè)方法的sel地址,這個(gè)被用來進(jìn)行散列計(jì)算存儲(chǔ)方法的imp實(shí)現(xiàn)。由于SEL類型采用了散列的算法,因此如果同一個(gè)類中存在同樣名字的方法,那么就會(huì)導(dǎo)致方法的imp地址無法唯一化。這也是蘋果不允許同名不同參數(shù)類型的方法存在的原因

消息轉(zhuǎn)發(fā)

通常情況下,在我們調(diào)用不屬于某個(gè)對(duì)象的方法的時(shí)候,我們的應(yīng)用就會(huì)崩潰crash,比如筆者經(jīng)歷過好幾次因?yàn)楹笈_(tái)返回的NSNull類型導(dǎo)致了測(cè)試反饋應(yīng)用閃退。通過上面的方法調(diào)用源碼我們可以看到并不是沒有找到方法實(shí)現(xiàn)就直接發(fā)生了崩潰,在崩潰之前編譯器會(huì)進(jìn)行消息轉(zhuǎn)發(fā)機(jī)制,總共給了我們?nèi)螜C(jī)會(huì)來避免這樣的崩潰并盡可能的找到方法的響應(yīng)者。

1

首先先看第一階段。我們都知道,在iOS開發(fā)當(dāng)中我們需要非常的注意用戶體驗(yàn)。單純的是因?yàn)閿?shù)據(jù)類型錯(cuò)誤而導(dǎo)致應(yīng)用出現(xiàn)閃退,這樣的處理會(huì)極大的影響使用app的用戶。因此,我們可以通過class_addMethod這個(gè)函數(shù)來動(dòng)態(tài)的添加這種錯(cuò)誤的處理(類可以在objc_registerClassPair完成類的注冊(cè)之后動(dòng)態(tài)的添加方法,但不允許動(dòng)態(tài)添加屬性,參考category
機(jī)制)

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, "@@:")
    }
}

在第二階段最開始的時(shí)候,這時(shí)候已經(jīng)默許了你并不想使用消息接收者來響應(yīng)這個(gè)方法,所以我們需要找到消息接盤俠
—— 這并不是一件壞事。在iOS中不支持多繼承,盡管我們可以通過協(xié)議和組合模式實(shí)現(xiàn)偽多繼承。偽多繼承和多繼承的區(qū)別在于:多繼承是將多個(gè)類的功能組合到一個(gè)對(duì)象當(dāng)中,而偽多繼承多個(gè)類的功能依舊分布在不同對(duì)象當(dāng)中,但是對(duì)象彼此對(duì)消息發(fā)送者透明
。那么,如果我們消息轉(zhuǎn)發(fā)給另一個(gè)對(duì)象可以用來實(shí)現(xiàn)這種偽多繼承。

@interface Person: NSObject@property (nonatomic, strong) NSNumber * age;@end@implementation Person- (id)forwardingTargetForSelector: (SEL)aSelector{ // 甚至可以通過runtime遍歷自己屬性找到可以響應(yīng)方法的接盤俠 NSString * selName = NSStringFromSelector(aSelector); if ([selName hasSuffix: @"Value"]) { return self.age; } return nil;}@end// View controllerid p = [[Person alloc] init];[p setAge: @(18)];NSLog(@"%lu, %.2f", [p integerValue], [p doubleValue]); //18, 18.00

如果你依舊沒有為這個(gè)方法找到另外一個(gè)調(diào)用者,那么阻止你app閃退的最后時(shí)刻到來了。runtime需要生成一個(gè)methodSignature變量來組裝,這將通過調(diào)用消息接收者的-(NSMethodSignature *)methodSignatureForSelector:
獲取,這個(gè)變量包含了方法的參數(shù)類型、參數(shù)個(gè)數(shù)以及消息接收者等信息。接著把這個(gè)變量組裝成一個(gè)NSInvocation對(duì)象進(jìn)行最后一次的消息轉(zhuǎn)發(fā),調(diào)用接收者的-forwardInvocation:來進(jìn)行最后的挽救機(jī)會(huì)。這意味著我們可以盡情的對(duì)invocation
做任何事情,包括隨意修改參數(shù)值、消息接收者等。我最常拿來干的事情就是減少數(shù)組的遍歷工作:

@implementation NSArray(LXDRuntime)- (void)forwardInvocation: (NSInvocation *)anInvocation{ for (id item in self) { if ([item respondsToSelector: anInvocation.selector]) { [anInvocation invokeWithTarget: item]; } }}@end

總的來說整個(gè)消息發(fā)送的過程可以歸納成下面這張圖:


1

雖然消息轉(zhuǎn)發(fā)可以幫助我們顯著的減少app的閃退率,但是在開發(fā)階段千萬不要加入這些特性。最好是在app申請(qǐng)上架的那個(gè)階段再加,這樣不至于app其他消息發(fā)送異常被我們忽略了。

消息機(jī)制黑魔法

上面筆者講解了關(guān)于一個(gè)調(diào)用方法之中發(fā)生的事情,確實(shí)非常的復(fù)雜。同樣的這些特性也非常值得我們?nèi)W(xué)習(xí)使用,runtime提供了一系列關(guān)于Method的方法給我們實(shí)現(xiàn)面向切面編程的工作。這些工作包括了替換原有方法實(shí)現(xiàn),交換方法實(shí)現(xiàn)等等工作。
假設(shè)現(xiàn)在我需要一個(gè)圓角按鈕,并且保證點(diǎn)擊觸發(fā)事件的范圍必須要這個(gè)圓之內(nèi),那么通過一個(gè)UIButton+LXDRuntime的擴(kuò)展來替換舊有-pointInside:withEvent:
方法

@interface UIButton(LXDRuntime)@property (nonatomic, assign) BOOL roundTouchEnable;@endconst 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;}// 替換方法實(shí)現(xiàn)+ (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

那么當(dāng)我需要我的按鈕只響應(yīng)圓形點(diǎn)擊區(qū)域的時(shí)候,只需要設(shè)置button.roundTouchEnable = YES,就會(huì)自動(dòng)實(shí)現(xiàn)了圓形點(diǎn)擊的判斷。除了上面的上面的方法替換,還有另一個(gè)常用的黑魔法是交換兩個(gè)方法的實(shí)現(xiàn)。歸功于Method的特殊結(jié)構(gòu),將方法名字sel
跟代碼實(shí)現(xiàn)imp分隔開來。你可以把imp當(dāng)做是一個(gè)block代碼塊,而交換實(shí)現(xiàn)的操作就相當(dāng)于把這兩個(gè)block交換了。

@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 controllerPerson * 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

上面的代碼交換了nameage
的實(shí)現(xiàn),用圖示來表示:

1

應(yīng)該不難看出,method_exchangeImplementations
之所以被推崇的原因在于這種方式交換實(shí)現(xiàn)的時(shí)候不會(huì)導(dǎo)致原有的方法實(shí)現(xiàn)發(fā)生改變(從頭到尾,ageIMPnameIMP都沒有進(jìn)行任何的修改),當(dāng)然了,它的缺點(diǎn)也是非常明顯的:
多人開發(fā)對(duì)同一個(gè)方法都進(jìn)行方法替換/交換時(shí),會(huì)使得業(yè)務(wù)邏輯復(fù)雜,非常的不利于調(diào)試

被交換的方法實(shí)現(xiàn)會(huì)直接的影響到所有該類的實(shí)例對(duì)象以及子類,不適用于單個(gè)對(duì)象的實(shí)現(xiàn)

可以說runtime提供的這些黑魔法都是雙刃劍,合理的運(yùn)用能讓我們更加的強(qiáng)大。另外,除了Method的黑魔法,還要提到一個(gè)IMP相關(guān)的使用陷阱。上文說過,IMPblock是非常相似的東西,前者可以跟函數(shù)指針強(qiáng)制轉(zhuǎn)換,因此可以看做是一個(gè)特殊的block,同樣的系統(tǒng)提供了兩者相互轉(zhuǎn)換的方法:imp_implementationWithBlockimp_getBlock。按照上面說的,當(dāng)調(diào)用方法轉(zhuǎn)換成消息轉(zhuǎn)發(fā)的時(shí)候,objc_msgSend自身已經(jīng)存在了兩個(gè)參數(shù)id object以及SEL aSelector,那么按照這種思路IMPblock
的切換應(yīng)該是這樣的:

+ (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];

上面這段代碼會(huì)crash的非常無厘頭,提示你EXC_BAD_ACCESS
錯(cuò)誤。重要的事情說三遍:

block參數(shù)不存在SEL!!
block參數(shù)不存在SEL!!
block參數(shù)不存在SEL!!

上面的代碼只要去掉了SEL aSelector
這個(gè)參數(shù),這段代碼就能正常執(zhí)行。

本文參考自Bison的技術(shù)博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,055評(píng)論 0 9
  • 我們常常會(huì)聽說 Objective-C 是一門動(dòng)態(tài)語言,那么這個(gè)「動(dòng)態(tài)」表現(xiàn)在哪呢?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,336評(píng)論 0 7
  • 本文轉(zhuǎn)載自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex閱讀 887評(píng)論 0 1
  • 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,889評(píng)論 33 466
  • 一個(gè)職前培訓(xùn),讓我莫名其妙地快要被洗腦了。我能說自己本來只是想去看看,總是多學(xué)點(diǎn)東西對(duì)自己有好處的,在這個(gè)不學(xué)...
    JiajiaYU閱讀 189評(píng)論 0 0

友情鏈接更多精彩內(nèi)容