1. 程式人生 > >Runtime原始碼解析和實戰使用

Runtime原始碼解析和實戰使用

文章目錄

Runtime-原始碼分析

1.類的初始化 在外部是如何實現的?
2.初始化過程中runtime 起到了什麼作用?

類的結構體

類是繼承於物件的:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    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();
    }
......
 }

objc_class中有定義了三個變數 ,superclass 是一個objc_class的結構體,指向的本類的父類的objc_class結構體。cache用來處理已經呼叫方法的快取。 class_data_bits_t 是objc_class 的關鍵,很多變數都是根據 bits來實現的。

物件的初始化

在物件初始化的時候,一般都會呼叫 alloc+init 方法進行例項化,或者通過new 方法。


- 第一步:呼叫系統的alloc 方法 或者new 方法(其中`new`方法直接呼叫的`callAlloc init`)
+ (instancetype)alloc OBJC_SWIFT_UNAVAILABLE("use object initializers instead");

+(id)alloc{
    return _objc_rootAlloc(self);
}
  • 第二步: runtime 內部實現呼叫objc_rootAlloc 方法
// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

  • 第三步: callAlloc 方法實現,解析:
    callAlloc 方法在建立物件的地方有兩種方式,一種是通過calloc 開闢記憶體,然後通過obj->initInstanceIsa(cls, dtor) 函式初始化這塊記憶體。 第二種是直接調class_createInstance 函式,由內部實現初始化邏輯 ;
static ALWAYS_INLINE id
    callAlloc(Class cls, bool checkNil, bool allocWithZone=false) {
    if (fastpath(cls->canAllocFast())) {
    bool dtor = cls->hasCxxDtor();
    id obj = (id)calloc(1, cls->bits.fastInstanceSize()); 
    if (slowpath(!obj)) return callBadAllocHandler(cls); 
    obj->initInstanceIsa(cls, dtor);
    return obj;
    } else {
    id obj = class_createInstance(cls, 0);
    if (slowpath(!obj)) return callBadAllocHandler(cls); return obj;
  }
}

但是在最新的objc-723 中,呼叫canAllocFast() 函式直接返回false ,所以只會執行上面所述的第二個else 程式碼塊。

bool canAllocFast(){
    return false;
}

初始化的程式碼最終會呼叫到 _class_createInstanceFromZone 函式,這個函式是初始化的關鍵程式碼。然後通過instanceSize 函式返回的 size,並通過calloc 函式分配記憶體,初始化isa_t 指標。

size_t size = cls->instanceSize(extraBytes);
obj->initIsa(cls);

訊息的傳送機制

在OC 中方法呼叫時通過Runtime 來實現的,runtime 進行方法呼叫本質上是傳送訊息,通過objc_msgSend()函式來進行訊息的傳送
[MyClass classMethod] 在runtime執行時被轉換為 ((void ()(id, SEL))(void )objc_msgSend)((id)objc_getClass("MyClass"), sel_registerName("classMethod"));
相關的demo可見我個人的github,在目錄檔案中,我將main.m進行了OC->C的轉換:RuntimeDemo
上述的方法可以理解為 向一個objc_class傳送了一個SEL 。

OC中每一個Method 的結構體如下:

struct objc_method {
    SEL _Nonnull method_name                    
    char * _Nullable method_types              
    IMP _Nonnull method_imp                                 
}

在新的objc_runtime_new.hobjc_method已經沒有使用了,使用的是如下的結構體,其引入的方式也發生了改變,不是直接定義在objc_class類中,而是通過getLoadMethod方法來實現間接的呼叫。

struct method_t {
    SEL name;
    const char *types;
    IMP 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; }
    };
};

objc_msgSend 就是通過SEL 來進行遍歷查詢的,如果兩個類定義了相同名稱的方法,它們的SEL 就是一樣的。

objc_method 中具體引數解析如下:

  • SEL 指的就是第一步中解析方法呼叫得到的 sel_registerName(“methodName”)的返回值。
  • method_types 指的是返回值的型別和引數。以返回值為開始,依次把引數拼接在後面,型別對應表格連結[TYPE EDCODING]。(聯想一哈,這個東西也是類似於property_gerAttrubute一樣,有對應的型別關係,某個字元意味著某種型別)
  • IMP_Method 引數 是一個函式指標,指向objc_method所對應的實現部分。
objc_msgSend 工作原理

當一個物件被建立,系統會為通過上述的callalloc 函式分配一個記憶體size 並給他初始化一個isa 指標,可以通過指標訪問其類物件,並且通過對類物件訪問其所有繼承者鏈中的類。

  1. objc_msgSend 底層實現沒有完全的暴露出來,但是通過原始碼中的objc-msg-simulator-x86_64.s的第672行程式碼開始可以看到部分實現,也可以通過Xcode斷點來檢視執行的堆疊資訊。其實現原理主要是通過2個方法來完成,首先是CacheLookup方法,在快取中沒有存在的情況下會去執行 __objc_msgSend_uncachedMethodTable查詢SEL

    	GetIsaCheckNil NORMAL		// r10 = self->isa, or return zero
    	CacheLookup NORMAL, CALL	// calls IMP on success
    
    	GetIsaSupport NORMAL
    	NilTestReturnZero NORMAL
    
    // cache miss: go search the method lists
    LCacheMiss:
    	// isa still in r10
    	MESSENGER_END_SLOW
    	jmp	__objc_msgSend_uncached
    
    	END_ENTRY _objc_msgSend
    

    __objc_msgSend_uncached 方法查詢

    	STATIC_ENTRY __objc_msgSend_uncached
    	UNWIND __objc_msgSend_uncached, FrameWithNoSaves
    
    	// THIS IS NOT A CALLABLE C FUNCTION
    	// Out-of-band x16 is the class to search
    	
    	MethodTableLookup
    	br	x17
    
    	END_ENTRY __objc_msgSend_uncached
    
    
    	STATIC_ENTRY __objc_msgLookup_uncached
    	UNWIND __objc_msgLookup_uncached, FrameWithNoSaves
    
    	// THIS IS NOT A CALLABLE C FUNCTION
    	// Out-of-band x16 is the class to search
    	
    	MethodTableLookup
    	ret
    
  2. 在執行MethodTableLookup方法時其中呼叫到了__class_lookupMethodAndLoadCache3 去找到需要的Class引數和SEL,內部實現找IMP 的是操作 方法是lookUpImpOrForward

  3. 當物件接受到訊息時,runtime會沿著訊息函式的isa查詢對應的類物件,然後是先在objc_cache中去查詢當前的SEL 的快取,如果快取中存在SEL,就直接返回該IMP也就是該實現方法的指標。

  4. 如果cache 中不存在快取,需要先判斷該類是否已經被建立,如果沒有,則將類例項化,第一次呼叫當前類的話,執行initialized 程式碼,再開始讀取這個類的快取,還是沒有的情況下才在method list 中查詢方法selector。本類如果沒有,就會到父類的method list中去查詢快取和method list 中的SEL,直到NSObject類 。

//如果快取在就直接返回
 if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }
 runtimeLock.read();
// 看看類有沒有被初始化,沒有初始化就直接初始化
    if (!cls->isRealized()) {
        // Drop the read-lock and acquire the write-lock.
        // realizeClass() checks isRealized() again to prevent
        // a race while the lock is down.
        runtimeLock.unlockRead();
        runtimeLock.write();

        realizeClass(cls);

        runtimeLock.unlockWrite();
        runtimeLock.read();
    }
   //走一遍 initialized 方法
 if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }
 retry:    
    runtimeLock.assertReading();

4.如果在類的繼承體系中都沒有找到SEL,則會進行動態訊息解析,給自己保留處理找不到方法的機會,

// 沒有找到該方法,會執行下面的分解方法
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

其中_class_resolveMethod 的原始碼解析為:

if(!cls->isMetaClass()){
   _class_resolveInstanceMethod(cls, sel, inst);
}else{
    _class_resolveClassMethod(cls, sel, inst);
     if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
}
  1. 動態訊息解析如果沒有做出響應,則進入動態訊息轉發階段,如果還沒有人響應,就會觸發doesNotRecognizeSelector 此時可以在動態訊息轉發階段做一些處理,否則就會Crash.

訊息轉發機制

訊息轉發機制的實現:
首先從上述的_class_resolveMethod可以方法可以看到,在找不到相關實現方法的時候,最終執行的都是_class_resolveInstanceMethod方法,那我們就從這個方法來進行剖析。

  1. _class_resolveMethod方法實現所屬類動態方法的解析,其中主要的函式_class_resolveInstanceMethod 方法本質還是給指定類傳送一個objc_msgSend訊息。經過各層級查詢後還是沒有,就會返回nil。但是iOS提供了使用者處理返回nil 後會出現閃退的方案,也就是resolveInstanceMethod方法,從 option 鍵檢視描述可以得到其內部實現的是 addMethod方法。
  2. 在物件所屬類不能動態新增方法後,runtime又提供了其他物件可以處理這個未知的SEL的方法,相關方法宣告如下:
    - (id)forwardingTargetForSelector:(SEL)aSelector;
  3. 在上述2種方法都沒有被實現的情況下,就只剩下最後一次機會,那就是訊息重定向。這個時間runtime會把SEL封裝成NSInvocation物件,然後呼叫:
    - (void)forwardInvocation: (NSInvocation*)invocation;
    如果這個類不能處理,就會呼叫其父類,知道NSObject也沒有找到這個方法就會報錯doesNotRecognizeSelector 丟擲異常,並且閃退.

上述的訊息動態轉發是要人為的去實現,如果沒實現在動態轉發,在執行到動態解析之後就會發生閃退。

實戰使用

Runtime 為類別動態新增屬性

思考:

  1. 類的屬性是怎麼實現的?
  2. 在類別中新增屬性為什麼會不成功?
  3. 使用動態時實現在類別中新增屬性的原理是什麼?
類的屬性實現原理
  • 在類中使用@property,系統會自動生成帶__ 的成員變數,和該變數的Setter 和getter 方法。 也就是意味著 一個成員屬性(property) 就相當於 成員變數+setter+getter 方法
    unsigned int invarCount = 0;
    Ivar *invars = class_copyIvarList([Human class], &invarCount);
    for (NSInteger i =0; i<invarCount; i++) {
        Ivar ivar = invars[i];
        NSLog(@"獲取到的成員變數%s",ivar_getName(ivar));
    }
    unsigned int  outCount = 0;
    Method *method = class_copyMethodList([Human class], &outCount);
    for (NSInteger i= 0; i< outCount; i++) {
        NSLog(@"method%s", sel_getName(method_getName(method[i]))); // 4
    }
    objc_property_t *propertys = class_copyPropertyList([Human class], &outCount);
    for (unsigned int i = 0; i<outCount; i++) {
        objc_property_t property = propertys[i];
        NSString *propertyName = [[NSString alloc]initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
        NSLog(@"propertyName%@",propertyName);
    }

解析:使用property 會自動生成成員變數和getter、setter 函式

類別中直接新增屬性剖析
  • 在類的類別中新增屬性,系統不會生成該屬性的 成員變數+Setter+getter 方法
@interface UIImage (SubImage)
@property(nonatomic,strong)NSString *imageString;
@end

輸出log 為: 獲取到的成員變數_imageRef
獲取到的成員變數_scale
獲取到的成員變數_imageFlags
獲取到的成員變數_flipsForRightToLeftLayoutDirection
獲取到的成員變數_traitCollection
獲取到的成員變數_vectorImageSupport
獲取到的成員變數_imageAsset
獲取到的成員變數_alignmentRectInsets

解析:category 它是在執行期決議的。 因為在執行期即編譯完成後,物件的記憶體佈局已經確定。

使用runtime 為類別新增屬性

思考:
1.runtime 為什麼能給類別新增屬性?
2.runtime 實現給類別新增屬性的原理是什麼?

在OC 中,類別即category 也是一個結構體categroy_t,具體的定義如下:

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

從上述的程式碼我們不難看出category 是帶有協議、例項方法、類方法的引數的,而在runtime 進行初始化即呼叫objc_init的時候,最後會有呼叫_dyld_objc_notify_register(&map_images, load_images, unmap_image)。在其內部有一個_read_images的操作會去取出當前類對應的category 陣列,並將其中的每個category_t物件取出,最終執行addUnattachedCategoryForClass 函式新增到category 雜湊表中。然後通過remethodizeClass方法來新增到指定的Class上。

具體原始碼見:objc-runtime-new.mm

  • 在給類別新增屬性的時候需要通過runtime 來為該屬性手動實現getter 和setter 方法
//實現程式碼的getter 方法
-(NSTimeInterval)timeInterval{
    return [objc_getAssociatedObject(self, _cmd)doubleValue];
}

實現程式碼的setter 方法

-(void)setTimeInterval:(NSTimeInterval)timeInterval{
    objc_setAssociatedObject(self, @selector(timeInterval), @(timeInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

解析:通過runtime 可以對類的屬性動態繫結 生成getter 和setter 方法,從category的結構體中可以看到,但是卻無法生存例項變數。

參考資料:

美團技術團隊-category的深入理解

Runtime 實現方法交換

Method Swizzle 實現的原理

在現實開發中,我們會遇到一些需求,比如為防止按鈕重複點選、檢測所有介面執行viewdidload 、之類的操作時,Method 可以起到很不錯的效果。這中程式設計方式也是屬於 面向切向程式設計(AOP)的一種實現方式。在iOS 中一個典型的第三方框架 Aspects

  • 在上面曾講到過objc_methodSEL 就代表著方法名稱,IMP 代表著對應的實現方法。所以我們可以講 Methodswizzle 做的就是將兩個方法的SELIMP進行對應的交換。
    cd1c1c58f7c219d8829117f7e954cacd.png
    如圖所示,對應的SEL 是指向對應的IMP 的,方法交換要實現的就是把SEL 指向的IMP 方法進行交換。
    c3e7c1b6ce66f851a86c3f0e3a8702c3.png

  • 在實現把對應的方法進行交換時,我們通常會在一個類的類別中來實現,在load方法中執行所要交換的兩個方法。 因為load方法在程式執行時就被呼叫載入到記憶體中了,有關loadinitialize 之間的差別其中有一點就是呼叫時機,load 在這個類中只會被呼叫一次 ,而initialize 在第一次傳送訊息的時候才會呼叫。所以在load中來實現方法交換會更加的合適。

Methoad swizzle 實現程式碼
  • 首先要獲取到當前用於交換的方法 。runtime 提供了2種形式來獲取:

1.獲取Method ,交換的方法為例項方法

class_getInstanceMethod([self Class],SEL oldSel);

2.獲取Method ,交換的方法為類方法

class_getClassMethod([self Class],SEL oldSel);
  • 對需要進行交換的方法進行驗證,保證該類實現了這個方法,而不是他的父類實現了,這樣就達不到想要的交換本類方法的效果。class_addMethod在runtime 內部實現註釋為 :
    新增一個新的方法到指定類,併為其指定方法名和實現方法(即SELIMP)。在新增成功時,返回YES,否則NO,該方法將新增超類的實現覆蓋,但是不會替換此類中的已經存在的實現方法,要更改現有實現方法,請使用method_setImplementation.
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 

在返回失敗的時候,說明該方法本身就已經存在,只要執行交換操作就可以了,否則就執行replace 操作。

  • 實現方法之間的交換
method_exchangeImplementations(oldMethod, newMethod);

注意:在使用方法交換的時候要記得使用 單例,為了避免出現第一次交換之後,第二次又給換回來的情況。

runtime 原始碼下載