iOS-runtime的理解

什麼是runtime

官方描述: The Objective-C language defers as many decisions as it can from compile time and link time to runtime. 「儘可能將決定放到運行的時候,而不是在編譯和連接過程」數組

runtime是一個C語言庫,包含了不少底層的純C語言API。 平時編寫的OC代碼中,程序運行,其實最終都是轉成了runtime的C語言代碼,runtime算是OC的幕後工做者 。緩存

  • 特色

OC與其餘語言不一樣的一點就是,函數調用採用了消息轉發的機制,但直到程序運行以前,消息都沒有與任何方法綁定起來。只有在真正運行的時候,纔會根據函數的名字來,肯定該調用的函數。markdown

runtime 是有個兩個版本的:數據結構

Objective-C 1.0使用的是legacy,在2.0使用的是modern。 如今通常來講runtime都是指modern。less

1. isa指針

首先要了解它底層的一些經常使用數據結構,好比isa指針。ide

當建立一個新對象時,會爲它分配一段內存,該對象的實例變量也會被初始化。第一個變量就是一個指向它的類的指針(isa)。 經過isa指針,一個對象能夠訪問它的類,並經過它的類來訪問全部父類。函數

  • 一個實例對象,在runtime中用結構體表示
// 描述類中的一個方法
typedef struct objc_method *Method;

// 實例變量
typedef struct objc_ivar *Ivar;

// 類別Category
typedef struct objc_category *Category;

// 類中聲明的屬性
typedef struct objc_property *objc_property_t;
複製代碼

查看runtime源碼能夠看到關於isa結構。佈局

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
};
複製代碼

下面的代碼對isa_t中的結構體進行了位域聲明,地址從nonpointer起到extra_rc結束,從低到高進行排列。位域也是對結構體內存佈局進行了一個聲明,經過下面的結構體成員變量能夠直接操做某個地址。位域總共佔8字節,全部的位域加在一塊兒正好是64位。優化

小提示:unionbits能夠操做整個內存區,而位域只能操做對應的位。ui

define ISA_BITFIELD                                                      \
      uintptr_t nonpointer        : 1;   //指針是否優化過                                   \
      uintptr_t has_assoc         : 1;   //是否有設置過關聯對象,若是沒有,釋放時會更快                                   \
      uintptr_t has_cxx_dtor      : 1; 	 //是否有C++的析構函數(.cxx_destruct),若是沒有,釋放時會更快                                     \
      uintptr_t shiftcls          : 33; //存儲着Class、Meta-Class對象的內存地址信息 \
      uintptr_t magic             : 6;  //用於在調試時分辨對象是否未完成初始化                                     \
      uintptr_t weakly_referenced : 1;  //是否有被弱引用指向過,若是沒有,釋放時會更快                                     \
      uintptr_t deallocating      : 1;  //對象是否正在釋放                                     \
      uintptr_t has_sidetable_rc  : 1;  //引用計數器是否過大沒法存儲在isa中                                     \
      uintptr_t extra_rc          : 19 //裏面存儲的值是引用計數器減1
#   	define RC_ONE   (1ULL<<45)
#   	define RC_HALF  (1ULL<<18)
複製代碼
  • nonpointer

0:表明普通的指針,存儲着Class、Meta-Class對象的內存地址。 1:表明優化過,使用位域存儲更多的信息。

  • has_assoc

是否有設置過關聯對象。若是沒有,釋放時會更快。

  • has_cxx_dtor

是否有C++的析構函數.cxx_destruct若是沒有,釋放時會更快。

  • shiftcls

存儲着Class、Meta-Class對象的內存地址信息

  • magic

用於在調試時,分辨對象是否未完成初始化

  • weakly_referenced

是否有被弱引用指向過。若是沒有,釋放時會更快

  • deallocating

對象是否正在釋放

  • extra_rc

裏面存儲的值是引用計數器減1

  • has_sidetable_rc

引用計數器是否過大沒法存儲在isa中 若是爲1,那麼引用計數會存儲在一個叫SideTable的類的屬性中

2. class結構

結構體

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;   // 父類
    cache_t cache;    //方法緩存
    class_data_bits_t bits;    // 用於獲取具體的類的信息
}
複製代碼

查看源碼(只保留了主要代碼)

  • class_rw_t
struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods; //方法列表
    property_array_t properties; //屬性列表
    protocol_array_t protocols; // 協議列表

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
}
複製代碼

其中的methods、properties、protocols是二維數組,是可讀可寫的,包含了類的初始內容、分類的內容。

  • method_array_t
class method_array_t : 
    public list_array_tt<method_t, method_list_t> 
{
    typedef list_array_tt<method_t, method_list_t> Super;

 public:
    method_list_t **beginCategoryMethodLists() {
        return beginLists();
    }
    
    method_list_t **endCategoryMethodLists(Class cls);

    method_array_t duplicate() {
        return Super::duplicate<method_array_t>();
    }
};
複製代碼

方法列表 中存放着不少一維數組method_list_t,而每個method_list_t中存放着method_t。method_t中是對應方法的imp指針、名字、類型等方法信息。

  • method_t
struct method_t {
    SEL name; //函數名
    const char *types; //編碼(返回值類型,參數類型)
    MethodListIMP imp; //指向函數的指針(函數地址)

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};
複製代碼

IMP:表明函數的具體實現 SEL:表明方法、函數名,通常叫作選擇器。 types:包含了函數返回值、參數編碼的字符串

關於SEL:

能夠經過@selector()sel_registerName()得到 能夠經過sel_getName()NSStringFromSelector()轉成字符串 不一樣類中相同名字的方法,所對應的方法選擇器是相同的。即,不一樣類的相同SEL是同一個對象。

  • class_ro_t
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;//instance對象佔用的內存空間
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name; //類名
    method_list_t * baseMethodList; //方法列表
    protocol_list_t * baseProtocols; //協議列表
    const ivar_list_t * ivars; //成員變量列表

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};
複製代碼

class_ro_t裏面的baseMethodList、baseProtocols、ivars、baseProperties是一維數組,是隻讀的,包含了類的初始內容

3. Type Encoding

iOS中提供了一個叫作@encode的指令,能夠將具體的類型表示成字符串編碼。好比:

+(int)testWithNum:(int)num{
    return num;
}
複製代碼

上面的方法能夠用 i20@0:8i16來表示:

i表示返回值是int類型,20是參數總共20字節

@表示第一個參數是id類型,0表示第一個參數從第0個字節開始 :表示第二個參數是SEL類型。8表示第二個參數從第8個字節開始。 i表示第三個參數是int類型,16表示第三個參數從第16個字節開始 第三個參數從第16個字節開始,是Int類型,佔用4字節。總共20字節

4. 方法緩存

用散列表來緩存曾經調用過的方法,能夠提升方法的查找速度。 結構體 cache_t

struct cache_t {
    struct bucket_t *_buckets; // 散列表
    mask_t _mask; //散列表的長度 -1
    mask_t _occupied; //已經緩存的方法數量
}

// 其中的 散列表
struct bucket_t {
    MethodCacheIMP _imp; //函數的內存地址
    cache_key_t _key;   //SEL做爲Key
}
複製代碼
  • cache_t中如何查找方法
// 散列表中查找方法緩存
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    assert(k != 0);

    bucket_t *b = buckets();
    mask_t m = mask();
    mask_t begin = cache_hash(k, m);
    mask_t i = begin;
    do {
        if (b[i].key() == 0  ||  b[i].key() == k) {
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}
複製代碼

其中,根據key和散列表長度減1 mask 計算出下標 key & mask,取出的值若是key和當初傳進來的Key相同,就說明找到了。不然,就不是本身要找的方法,就有了hash衝突,把i的值加1,繼續計算。以下代碼:

// 計算下標
static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask);
}


//hash衝突的時候
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}
複製代碼
  • cache_t的擴容

當方法緩存太多的時候,超過了容量的3/4s時候,就須要擴容了。擴容是,把原來的容量增長爲2倍。

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
			...
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        // 來到這裏說明,超過了3/4,須要擴容
        cache->expand();
    }

  		 ...
}

// 擴容
enum {
    INIT_CACHE_SIZE_LOG2 = 2,
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)
};

// cache_t的擴容
void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    
    uint32_t oldCapacity = capacity();
    // 擴容爲原來的2倍
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;

    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        // mask overflow - can't grow further
        // fixme this wastes one bit of mask
        newCapacity = oldCapacity;
    }

    reallocate(oldCapacity, newCapacity);
}
複製代碼

5. 消息轉發機制

OC方法調用的本質是,消息轉發機制。好比: 對象instance 調用dotest方法[instance1 dotest]; 底層會轉化爲:objc_msgSend(instance1, sel_registerName("dotest")); OC中方法的調用,其實都是轉換爲objc_msgSend函數的調用。

實例對象中存放着 isa 指針以及實例變量。由 isa 指針找到實例對象所屬的類對象 (類也是對象)。類中存放着實例方法列表。在這個列表中,方法的保存形式是 SEL 做 key,IMP 做value。

這是在編譯時根據方法名,生成惟一標識SELIMP 其實就是函數指針 ,指向最終的函數實現。

整個 Runtime 的核心就是 objc_msgSend(receiver, @selector (message)) 函數,經過給類發送 SEL 以傳遞消息,找到匹配的 IMP 再獲取最終的實現。

執行流程能夠分爲3大階段:消息發送->動態方法解析->消息轉發

  • 消息發送階段:

首先判斷receiver是否爲空 若是不爲空,從receiverClass的緩存中,查找方法。(找到了就調用) 若是沒找到,就從receiverClass的class_rw_t中查找方法。(找到就調用,並緩存) 若是沒找到,就去receiverClassd的父類的緩存中查找。 若是沒找到,就從父類的class_rw_t中查找方法。 若是沒找到,就看是否還有父類,有就繼續查父類的緩存,方法列表。

由上述知道,去查緩存、方法列表、查父類等這些操做以後,都沒有找到這個方法的實現,這時若是後面不作處理,必然拋出異常:

...due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[xxx xxxx]: unrecognized selector sent to instance 0x100f436c0’

若是沒有父類,說明消息發送階段結束,那麼就進入第二階段,動態方法解析階段。

  • 動態方法解析:

在此,能夠給未找到的方法,動態綁定方法實現。或者給某個方法重定向。

源碼:

// 動態方法解析
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) { //若是不是元類對象
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else { // 是元類對象
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
複製代碼

其中的resolveClassMethodresolveInstanceMethod默認是返回NO

+ (BOOL)resolveClassMethod:(SEL)sel {
    return NO;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}
複製代碼
  • 在動態解析階段,能夠重寫resolveInstanceMethod並添加方法的實現。

假如,沒有找到run這個方法:

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
     if (sel == @selector( run )) {
        // 獲取其餘方法 實例方法 或類方法,做爲run的實現
        Method method = class_getInstanceMethod(self, @selector(test));

        // 動態添加test方法的實現
        class_addMethod(self, sel,
                        method_getImplementation(method),
                        method_getTypeEncoding(method));
				
       // 返回YES表明有動態添加方法  其實這裏返回NO,也是能夠的,返回YES只是增長了一些打印
        return NO;
     }
     return [super resolveInstanceMethod:sel];
}
複製代碼

上面的代碼,就至關於,調用run的時候,實際上調用的是test。

若是前面消息發送 和動態解析階段,都沒有對方法進行處理,咱們還有最後一個階段。以下

  • 消息轉發

____forwarding___這個函數中,交代了消息轉發的邏輯。可是不開源。

先判斷forwardingTargetForSelector的返回值。有,就向這個返回值發送消息,讓它調用方法。 若是返回nil,就調用methodSignatureForSelector方法,有就調用forwardInvocation

其中的參數是一個 NSInvocation 對象,並將消息所有屬性記錄下來。 NSInvocation 對象包括了Selector、target 以及其餘參數。其中的實現僅僅是改變了 target 指向,使消息保證可以調用。

假若發現本類沒法處理,則繼續查找父類,直至 NSObject 。若是methodSignatureForSelector方法返回nil,就調用doesNotRecognizeSelector:方法。

應用舉例:

場景1:

類Person只定義了方法run但沒有實現,另外有類Car實現了方法run。

如今Person中,重寫forwardingTargetForSelector返回Car對象

// 消息轉發
- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(run)) {
        return [[Car alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
複製代碼

這時,當person實例調用run方法時,會變成car實例調用run方法。

證實forwardingTargetForSelector返回值不爲空的話,就向這個返回值發送消息,也就是 objc_msgSend(返回值, SEL)

場景2:

若是前面的forwardingTargetForSelector返回爲空。底層就會調用 methodSignatureForSelector 獲取方法簽名後,再調用 forwardInvocation

所以:能夠重寫這兩個方法:

// 方法簽名:返回值類型、參數類型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(run)) {
       return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
     return [super methodSignatureForSelector:aSelector];
}


- (void)forwardInvocation:(NSInvocation *)anInvocation{

   [anInvocation invokeWithTarget:[[Car alloc] init]];
}
複製代碼

這樣,依然能夠調用到car的run方法。

NSInvocation封裝了一個方法調用,包括:方法調用者、方法名、方法參數

anInvocation.target 方法調用者 anInvocation.selector 方法名 [anInvocation getArgument:NULL atIndex:0]

補充: 一、消息轉發的forwardingTargetForSelector、methodSignatureForSelector、forwardInvocation不只支持實例方法,還支持類方法。不過系統沒有提示,須要寫成實例方法,而後把前面的-改爲+便可。

+(IMP)instanceMethodForSelector:(SEL)aSelector{
    
}

-(IMP)methodForSelector:(SEL)aSelector{
    
}
複製代碼

二、只能向運行時動態建立的類添加ivars,不能向已經存在的類添加ivars。 這是由於在編譯時,只讀結構體class_ro_t就被肯定,在運行時不可更改。

相關文章
相關標籤/搜索