天天看點

【ios學習】淺談Runtime

一、Runtime基石:Objective-C對象模型

1、對象
  • 每一個對象都是類的執行個體, 類中儲存對象的方法清單;當一個對象方法被調用時,類會首先查找它本身是否有該方法的實作,如果沒有,則會向它的父類查找該方法,直到NSObject(根類);
  • 類是元類 (metaclass) 的執行個體;元類儲存類方法清單;當一個類方法被調用時,元類會首先查找它本身是否有該類方法的實作,如果沒有,則會向它的父類查找該方法,直到NSObject(根類);
2、isa指針
  • 對象的isa指針指向所屬的類,類的isa指針指向所屬的元類;所有的元類的 isa指針都會指向一個根元類 (root metaclass)。根元類的 isa指針指向自己,行成了一個閉環。
  • 在64 位 CPU 下,isa 的内部結構有變化。具體檢視用 isa 承載對象的類資訊
  • 對象、isa指針、類、元類、根元類的關系如下圖:
【ios學習】淺談Runtime
3、對象布局
  • 執行個體變量(包括父類)都儲存在對象本身的存儲空間内;執行個體方法儲存在類中,類方法儲存在元類中;父類的執行個體方法儲存在各級 super class 中,父類的類方法儲存在各級 super meta class;
    //對象組成 --start--
    isa pointer
    rootClass's vars
    penultimate superClass's vars
    ...
    superClass's vars
    Class's vars
    //對象組成 --end--
    
    typedef struct objc_class *Class;
     //類的結構
     struct objc_class{
      struct objc_class* isa;                              //指向元類
      struct objc_class* super_class;                //指向父類
      const char* name;
      long version;
      long info;
      long instance_size;
      struct objc_ivar_list* ivars;                         //執行個體變量清單
      struct objc_method_list** methodLists;      //方法清單
      struct objc_cache* cache;
      struct objc_protocol_list* protocols;           //協定清單
    };
    
    //執行個體變量的結構
    struct objc_ivar {
        char *ivar_name  OBJC2_UNAVAILABLE;
        char *ivar_type  OBJC2_UNAVAILABLE;
        int ivar_offset  OBJC2_UNAVAILABLE;
    #ifdef __LP64__
        int space        OBJC2_UNAVAILABLE;
    #endif
    }      

    說明1:對象中儲存指向類的isa指針 以及 各級的 執行個體變量(ivar),這個記憶體結構在編譯時就确定下來了,不能在編譯時給對象增加執行個體變量。

    說明2:類的記憶體布局有isa指針、super_class指針、執行個體變量清單、方法清單和協定清單,其中執行個體變量(var)包含了變量的名稱、類型、偏移等。

二、Runtime核心:消息發送和轉發

Runtime賦予了OC了諸多動态特性,使其可以在運作時可以做一些事情;主要表現為:動态類型(在運作時才檢查對象類型)和 動态綁定(接到消息後,由運作環境決定執行哪部分代碼)

1、消息發送(Message)
  • Objective-C 中的方法調用,實質上是在底層用objc_msgSend()實作消息發送,其核心在于:根據SEL(選擇器)開始找到IMP;其中SEL是執行個體方法的指針,可以看做方法名字元串;IMP是函數指針,指向方法實作的位址。
    //調用方法  
    [obj doSomething];
    //在編譯時候轉換
    objc_msgSend(obj,@selector(doSomething))      
  • objc_msgSend的定義如下:
    // self是接收者,接收該消息的類的執行個體
        // _cmd是選擇器,要處理的消息的selector
         // ... 是需傳入的參數,參數個數不定
        objc_msgSend(id self, SEL _cmd, ...)      
  • objc_msgSend的發送流程:先在Class中的緩存查找imp(沒緩存則初始化緩存),如果沒找到,則向父類的Class查找。如果一直查找到根類仍舊沒有實作,就走消息轉發(_objc_msgForward)了。
  • 給nil發送消息不會有什麼作用,但是傳回值有些差別,具體如下:
    a) 如果方法傳回值是 對象,傳回nil
    b) 如果方法傳回值是 指針類型,其指針大小為小于或者等于sizeof(void*),float,double,long double 或者 long long 的整型标量
    c) 如果方法傳回值是 結構體,發送給 nil 的消息将傳回0。結構體中各個字段的值将都是0。
    d) 如果方法傳回值不是 上述提到的幾種情況,那麼發送給 nil 的消息的傳回值将是未定義的。      
2-1、消息轉發(Message Forwarding)

消息轉發解決的是:查找IMP(方法實作)失敗後的處理;經曆動态方法解析、 備用接收者和 完整的消息轉發三個過程,其流程如下圖:

【ios學習】淺談Runtime
  • 動态方法解析:接收到未知消息時,Runtime向目前類發送+resolveInstanceMethod:或+resolveClassMethod:消息,在這裡可以添加缺失的方法,傳回YES,重新發送消息,否則繼續下一步;
  • 備用接收者:動态方法解析中沒能處理,Runtime會向forwardingTargetForSelector:發消息,如果該方法傳回了一個非nil或非self對象,恰好該對象實作了這個方法,那麼該對象就成了消息的接收者,消息就被分發到該對象。
  • 完整消息轉發:前兩個都沒能處理好,Runtime發送methodSignatureForSelector:消息,擷取selector對應方法的簽名;如果有方法簽名傳回,則根據方法簽名建立描述消息的NSInvocation,向目前對象發送forwardInvocation:消息;如果沒有方法簽名傳回,傳回nil,向目前對象發送doesNotRecognizeSelector:消息,應用Crash退出。
2-2、避免消息轉發的辦法

在消息轉發三個過程中,未知消息的處理過程越往後,代價越大;一般我們可以這麼做 盡可能避免消息轉發,可以這麼做:

  • 調用delegate 方法前檢查方法是否實作(respondsToSelector:), 隻有實作了(respondsToSelector:傳回YES) ,才去真正調用delegate 方法。
    if([self.delegate respondsToSelector: @selector(sayHello)]) {
        [self.delegate sayHello];
    }      
  • 直接調用方法,少用performSelector:;因為在直接調用方法時,編譯自動校驗,如果方法不存在,編譯器會直接報錯;而使用performSelector:的話一定是在運作時候才能發現,如果此方法不存在就會崩潰。
    //直接使用方法調用,少使用performSelector
    [dog sayHello];
    // [dog performSelector:@selector(sayHello) withObject:nil];      
  • 使用performSelector:,最好先判斷方法是否實作(respondsToSelector:),隻有實作了(respondsToSelector:傳回YES) ,才去調用performSelector:方法。
    //respondsToSelector:和performSelector:組合使用
        if ([dog respondsToSelector:@selector(sayHello)])         {
        [dog performSelector:@selector(sayHello)];
     }      
  • 強制類型轉換,先判斷對象是否屬于強制轉換後的類
    if([data isKindOfClass:[NSDictionary class]]){
      //
    }      

三、Runtime特性和應用

1、分類(Category)
  • 原理:對象的方法定義都儲存在類的可變區域中,修改methodLists指針指向的指針的值,就可以實作動态地為某一個類增加成員方法。(但是對象布局在編譯時候就固定了,結構體的大小并不能動态變化,在運作時不能增加執行個體變量)。
  • 通過關聯objc_setAssociatedObject 和 objc_getAssociatedObject方法可以變相地給對象增加執行個體變量,并不會真正改變了對象的記憶體結構。
  • 通過Category新增的方法,會插入到方法清單的前部;如果有和原來方法重名,在運作時,順序查找時,一旦找到對應名字的方法,就不再查找,導緻原來方法得不到機會,這是Category新增的方法和原方法重名,原有方法失效的原因。
  • 作用:給現有的類添加方法;将一個類的實作拆分成多個獨立的源檔案;聲明私有的方法。
2、關聯對象(Associated Objects)
  • 原理:Category不能給一個已有類添加執行個體變量,但是可以通過關聯對象添加屬性;但是關聯對象不會改變對象的記憶體布局,新增的屬性是添加到和對象位址關聯的哈希表中;
  • Associated Objects 相關的三個方法
    objc_setAssociatedObject    //添加關聯對象
    objc_getAssociatedObject    //擷取關聯對象
    objc_removeAssociatedObjects  // 删除所有關聯對象      
  • 作用:為現有的類添加私有變量以幫助實作細節;為現有的類添加公有屬性;為 KVO 建立一個關聯的觀察者
3、方法混寫(Method Swizzling)
  • 原理:在運作時交換方法實作(IMP)
  • 作用:可以利用它hook原有的方法,插入自己的業務需求,
4、鍵值觀察(KVO)

觀察者模式在Objective-C的應用之一,借助Runtime特性,實作自動鍵值觀察;使用了isa swizzling機制。具體描述如下:

  • 當某個類的對象第一次被觀察時,系統就會在運作期動态地建立該類的一個子類,在這個子類中重寫基類中被觀察屬性的 setter 方法,實作真正的通知機制;
  • 派生類還重寫了 class 方法以“欺騙”外部調用者,系統将對象的 isa 指針指向這個新誕生的子類,實質上這個對象就成為該派生類的對象了,因而在該對象上對 setter 的調用就會調用重寫的 setter,進而激活鍵值通知機制。
  • 此外,派生類還重寫了 dealloc 方法來釋放資源。

說明:KVC(鍵值編碼)是不通過存取方法,而通過屬性名稱字元串間接通路屬性的機制,沒有用到isa swizzling機制。

5、NSProxy
  • OC是單繼承的,但是可以利用NSProxy實作一下“僞多繼承”,具體參考NSProxy——少見卻神奇的類
  • 項目中,主要是利用NSProxy做消息轉發的代理類,如弱引用代理類,可以打破循環引用。
    @interface FLWeakProxy : NSProxy
    + (instancetype)weakProxyForObject:(id)targetObject;
    @end
    
    @interface FLWeakProxy ()
    @property (nonatomic, weak) id target;
    @end
    
    @implementation FLWeakProxy
    
    #pragma mark Life Cycle
    //類沒有定義預設的init方法.
    + (instancetype)weakProxyForObject:(id)targetObject{
        FLWeakProxy *weakProxy = [FLWeakProxy alloc];
        weakProxy.target = targetObject;
        return weakProxy;
    }
    
    #pragma mark Forwarding Messages
    - (id)forwardingTargetForSelector:(SEL)selector{
    // Keep it lightweight: access the ivar directly
    return _target;
    }
    
    - (void)forwardInvocation:(NSInvocation *)invocation{
        void *nullPointer = NULL;
        [invocation setReturnValue:&nullPointer];
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector{
        return [NSObject instanceMethodSignatureForSelector:@selector(init)];
    }
    @end      
    說明: NSProxy非常适合做消息轉發的代理類,能自動轉發中定義的接口和NSObject的Category中定義的方法,如果使用NSObject來做,不能自動轉發NSObject的Category中定義、respondsToSelector:、isKindOfClass:這兩個方法。