iOS runtime(一)(runtime 分析理解)

  本文主要是我對學習runtime或其它知識過程當中的串聯起來寫成的,裏面也包括了引用外部相關的內容,也包括本身的理解,對runtime作個總結記錄的同時把runtime的各個知識點銜接起來,但願能對讀者有所幫助,文章有點長,但願讀者作好心理準備。在仔細讀完這文章後相信你們都對runtime有必定的理解。那麼學了runtime有什麼用呢,這就是我下一篇文章 iOS runtime (二) (開源庫分析) 要寫的內容,主要講解的是一些優秀的開源庫是如何運用runtime,提升咱們的開發效率的。html


(一)runtime是什麼?ios

  從程序設計語言開始git

  Objective-C語言它是擴充C的面向對編程語言。OC語言是一門動態語言,它將不少靜態語言在編譯和連接時期作的事放到了運行時來處理,也正由於這樣,才讓它能在C語言的基本上賦予更多的特性,如:面向對象,runtime 運行時等。github

  runtime是什麼?編程

  我認爲runtime應該包括了兩部分:runtime 系統和runtime接口緩存

  runtime 系統:當咱們的iOS程序啓動時,其實runtime系統其實已經運行起來了,它至關於爲OC語言而出現的操做系統又或者說一個運行庫。它會爲咱們的代碼所寫的全部繼承於NSObject類生成對應的元類(後面後講),類(類對象),而後編譯器和runtime系統相互協做才得以讓OC可以實現如:運行時動態生成類、對象以和方法,消息傳遞機制,消息轉發機制,以及Method Swizzling等等安全

  runtime接口:runtime接口是一套底層的C語言API,包含不少強大實用的C語言數據類型和C語言函數,平時咱們編寫的OC代碼,底層都是基於runtime接口實現的。網絡

  咱們日常開發當中,其實有已經有意無心已經跟runtime打交道。由於與runtime打交道主要有三種方式:併發

  (1)日常使用OC寫代碼,當代碼中使用到OC的類與方法,runtime系統其實已經在隱式的被使用着。app

  (2)使用NSObject的某些方法如:isKindOfClass:、isMemberOfClass:等等的這些接口時,就是顯示使用runtime接口提供的功能。

  (3)runtime提供的一套C語言API。


 (二)對象模型

首先咱們要區分兩個名詞,類對象(即類,這樣稱乎是由於在OC中類也是一個對象),實例對象(經過某個類建立的具體實例)。

Objective-C類是由Class類型來表示的,它其實是一個指向objc_class結構體的指針。它的定義以下:

typedef struct objc_class *Class;

查看objc/runtime.h中objc_class結構體的定義以下:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
 
#if !__OBJC2__
    Class super_class                       OBJC2_UNAVAILABLE;  // 父類
    const char *name                        OBJC2_UNAVAILABLE;  // 類名
    long version                            OBJC2_UNAVAILABLE;  // 類的版本信息,默認爲0
    long info                               OBJC2_UNAVAILABLE;  // 類信息,供運行期使用的一些位標識
    long instance_size                      OBJC2_UNAVAILABLE;  // 該類的實例變量大小
    struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 該類的成員變量鏈表
    struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定義的鏈表
    struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法緩存
    struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 協議鏈表
#endif
 
} OBJC2_UNAVAILABLE;

在__OBJC2__之前,咱們是能夠看到結構體的各個成員的,下面簡單描述一下與對象模型相關的兩個字段:

isa:每一個對象(包括類對象和實例對象)中都會包含它,代表當前的對象是屬於哪一個類的。實例對象的isa指針指向它的類對象。而類對象的isa指針指向它的元類(metaclass)。後面會介紹到。

super_class:指向它的父類的指針。

meta-class是一個類對象的類。

一、當咱們向一個對象發送消息時,會到這個實例對象所屬的這個類對象的方法列表中查找方法

二、而向一個類對象發送消息時,會在這個類對象的meta-class的方法列表中查找。

每一個類對象都會有一個單獨的meta-class。meta-class也是一個類,也能夠向它發送一個消息,那麼它的isa又是指向什麼呢?爲了避免讓這種結構無限延伸下去,Objective-C的設計者讓全部的meta-class的isa指向基類的meta-class,以此做爲它們的所屬類。即,任何NSObject繼承體系下的meta-class都使用NSObject的meta-class做爲本身的所屬類,而基類的meta-class的isa指針是指向它本身。這樣就造成了一個的閉環。

假設咱們有以下代碼:

@interface SuperClass : NSObject
@end

@interface SubClass : SuperClass
@end

@implementation SubClass

- (void)message

{

}

@end

 

根據上面的說明,對象模型以下圖,圖摘自網絡:

   

 對於NSObject繼承體系來講,圖中的Root class即爲NSObject.

那麼咱們知道了OC中的對象模型是這樣子的,那蘋果爲何這樣作,這樣作有什麼用呢?接下來就到第二部分。


 

(三)消息機制

  在iOS中,咱們要區分一下方法與函數。方法是屬於類或者對象的,而函數則不必定,能夠獨立於類與對象以外。函數的調用是一步到位,程序直接跳到函數的地址去執行。而方法的調用,它是對類和對象而言,向對應的類或對象發送一條消息,經過runtime系統的消息機制,找到實際要調用的函數地址,而後跳到地址中執行。當咱們執行上一部分Subclass類的以下代碼時

[subClassInstance message]

編譯器調用的實際上是

objc_msgSend(subClassInstance, selector) //selector參數,編譯器傳遞的會是sel_registerName("message")。

若是方法帶有參數,那麼調的會是

objc_msgSend(subClassInstance, selector, arg1, arg2, ...)

 還記得咱們上一部分講的對象模型嗎?objc_class中有一個屬性

struct objc_method_list **methodLists;這個裏面存儲的是類的方法鏈表。鏈表內每一個元數都是一個方法。那麼方法又是什麼,咱們能夠在runtime.h中能夠看到,每一個方法其實也是一個結構體。

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;  //選擇器,Objective-C在編譯時,會依據每個方法的名字、參數序列,生成一個惟一的整型標識(Int類型的地址),這個標識就是SEL
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;  //IMP其實是一個函數指針,指向方法實現的地址
}                                    

就上一部分SubClass中的而然,SubClass中的methodLists中就會有一個方法。method_name的值爲:sel_registerName("message"),method_imp則指向messge方法實現的地址。(這個地址是編譯連接期已經決定了的,這裏我還這樣理解,咱們能夠運行時爲類添加新的方法,修改方法的實現把它修改爲另外一個已經存在的實現(這就是後面會說到的Method swizzling),可是不能在運行時建立一個新的實現。)  

  那麼objc_msgSend會幫咱們完成動態綁定的全部事情:經過傳進來的receiver subClassInstance,及selector找到對應的Method。method中已經關聯了方法實現的地址,接下來就把參數做爲方法實現的參數傳進去進行調用,把調用的反回值做爲objc_msgSend的返回值。這就是OC中的消息發送。

下圖演示了這樣一個消息的基本框架,如下基原本自官方文檔截圖及翻譯:

當消息發送給一個對象時,objc_msgSend經過對象的isa指針獲取到類的結構體,而後在方法分發表裏面查找方法的selector。若是沒有找到selector,則經過objc_msgSend結構體中的指向父類的指針找到其父類,並在父類的分發表裏面查找方法的selector。依此,會一直沿着類的繼承體系到達NSObject類。一旦定位到selector,函數會就獲取到了實現的入口點,並傳入相應的參數來執行方法的具體實現。若是最後沒有定位到selector,則會走消息轉發流程,這個咱們在稍後討論。

  如今先回頭看看對象模型中objc_class中的struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法緩存

上面說的是方法第一次被調用的流程。在咱們每次調用過一個方法後,這個方法就會被緩存到cache列表中,下次調用的時候runtime就會優先去cache中查找,若是cache沒有,纔去methodLists中查找方法,走上述流程。因此OC的對象模型是消息傳遞機制中方法查找的基礎。

  接下來咱們會講消息的轉發機制,如上面所說:若是最後沒有定位到selector,則會走消息轉發流程。實在不必重複造輪子,對於這部分詳情能夠參考消息轉發,做者已經寫得夠好了。但爲了避免跳連接也能有個好的理解,我下面是對連接內容的刪簡版,去掉具體代碼的實現。

一、當沒有找到SEL的IMP時,resolveInstanceMethod方法就會被調用,它給類利用class_addMethod添加方法的機會。

二、通過resolveInstanceMethod若是對象仍是不能執行到對應的IMP那麼就進入下一個階段,進入到

流程到了這裏,系統給了個將這個SEL轉給其餘對象的機會。

三、若是第二步返回的是nil或Self,那就就會進入methodSignatureForSelector這個函數和後面的forwardInvocation:是最後一個尋找IML的機會。這個函數讓重載方有機會拋出一個函數的簽名,再由後面的forwardInvocation:去執行。

真正執行從methodSignatureForSelector:返回的NSMethodSignature。在這個函數裏能夠將NSInvocation屢次轉發到多個對象中,這也是這種方式靈活的地方。(forwardingTargetForSelector只能以Selector的形式轉向一個對象)。


 

(四)Method Swizzling 

   在第三部分,咱們討論了消息機制,基於消息機制,纔會有Method Swizzling,Method Swizzling是改變一個selector的實際實現的技術。經過這一技術,咱們能夠在運行時經過修改類的分發表中selector對應的函數,來修改方法的實現。

  下面摘自Method Swizzling一文。

  例如,咱們想跟蹤在程序中每個view controller展現給用戶的次數:固然,咱們能夠在每一個view controller的viewDidAppear中添加跟蹤代碼;可是這太過麻煩,須要在每一個view controller中寫重複的代碼。建立一個子類多是一種實現方式,但須要同時建立UIViewController, UITableViewController, UINavigationController及其它UIKit中view controller的子類,這一樣會產生許多重複的代碼。

這種狀況下,咱們就可使用Method Swizzling,如在代碼所示:

#import <objc/runtime.h>

@implementation UIViewController (Tracking)

+ (void)load {
        static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];         
        // When swizzling a class method, use the following:
                    // Class class = object_getClass((id)self);

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
                class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
        [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}
@end

在這裏,咱們經過method swizzling修改了UIViewController的@selector(viewWillAppear:)對應的函數指針,使其實現指向了咱們自定義的xxx_viewWillAppear的實現。這樣,當UIViewController及其子類的對象調用viewWillAppear時,都會打印一條日誌信息。

上面的例子很好地展現了使用method swizzling來一個類中注入一些咱們新的操做。固然,還有許多場景可使用method swizzling,在此很少舉例。在此咱們說說使用method swizzling須要注意的一些問題:

Swizzling應該老是在+load中執行

在Objective-C中,運行時會自動調用每一個類的兩個方法。+load會在類初始加載時調用,+initialize會在第一次調用類的類方法或實例方法以前被調用。這兩個方法是可選的,且只有在實現了它們時纔會被調用。因爲method swizzling會影響到類的全局狀態,所以要儘可能避免在併發處理中出現競爭的狀況。+load能保證在類的初始化過程當中被加載,並保證這種改變應用級別的行爲的一致性。相比之下,+initialize在其執行時不提供這種保證—事實上,若是在應用中沒爲給這個類發送消息,則它可能永遠不會被調用。

Swizzling應該老是在dispatch_once中執行

與上面相同,由於swizzling會改變全局狀態,因此咱們須要在運行時採起一些預防措施。原子性就是這樣一種措施,它確保代碼只被執行一次,無論有多少個線程。GCD的dispatch_once能夠確保這種行爲,咱們應該將其做爲method swizzling的最佳實踐。

 注意事項

Swizzling一般被稱做是一種黑魔法,容易產生不可預知的行爲和沒法預見的後果。雖然它不是最安全的,但若是聽從如下幾點預防措施的話,仍是比較安全的:

一、老是調用方法的原始實現(除非有更好的理由不這麼作):API提供了一個輸入與輸出約定,但其內部實現是一個黑盒。Swizzle一個方法而不調用原始實現可能會打破私有狀態底層操做,從而影響到程序的其它部分。

二、避免衝突:給自定義的分類方法加前綴,從而使其與所依賴的代碼庫不會存在命名衝突。

三、明白是怎麼回事:簡單地拷貝粘貼swizzle代碼而不理解它是如何工做的,不只危險,並且會浪費學習Objective-C運行時的機會。閱讀Objective-C Runtime Reference和查看<objc/runtime.h>頭文件以瞭解事件是如何發生的。


 (五)成員變量與屬性

  成員變量

  此時再次看回objc_class中的struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 該類的成員變量鏈表

  鏈表中包含類的全部成員變量,裏面的每一個元素都是一個Ivar.

  Ivar是表示實例變量的類型,其實際是一個指向objc_ivar結構體的指針,其定義以下:

typedef struct objc_ivar *Ivar; 
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 
} 

這裏咱們注意第三個成員 ivar_offset。它表示基地址偏移字節。

在編譯咱們的類時,編譯器生成了一個 ivar佈局。咱們對 ivar 的訪問就能夠經過 對象地址 + ivar偏移字節的方法。

使用Non Fragile ivars時,Runtime會進行檢測來調整類中新增的ivar的偏移量。 這樣咱們就能夠經過 對象地址 + 基類大小 + ivar偏移字節的方法來計算出ivar相應的地址,並訪問到相應的ivar。詳情參考文章,裏面也說明了爲何runtime容許動態添加方法和屬性,可是不容許添加成員變量(objc_setAssociatedObject除外的方式)。

屬性

一、定義:

objc_property_t:聲明的屬性的類型,是一個指向objc_property結構體的指針

typedef struct objc_property *objc_property_t;

二、操做函數:

// 獲取全部屬性
class_copyPropertyList

說明:使用class_copyPropertyList並不會獲取無@property聲明的成員變量

// 獲取屬性名
property_getName
// 獲取屬性特性描述字符串
property_getAttributes
// 獲取全部屬性特性
property_copyAttributeList

說明:property_getAttributes函數返回objc_property_attribute_t結構體列表,objc_property_attribute_t結構體包含name和value,經常使用的屬性以下:

屬性類型  name值:T value:變化的
編碼類型  name值:C(copy) &(strong) W(weak) 空(assign) 等 value:無
非/原子性 name值:空(atomic) N(Nonatomic)  value:無
變量名稱  name值:V  value:變化

成員變量中講了這麼多,若是感受還有點模糊,沒有關係,下面經過一個例子,你們就能明白了,動手作一下會更好理解,也能夠熟悉一下相關API。此時新建一個工程。添加一個類MyValueAndProperty以下:

 

//MyValueAndProperty.h
#import <Foundation/Foundation.h>

@interface MyValueAndProperty : NSObject
{
    BOOL _myValue;
}
@property (nonatomic, strong) NSString *myProperty;
@end
//MyValueAndProperty.m

#import "MyValueAndProperty.h"

@implementation MyValueAndProperty
@end

 

接着就是在ViewController中使用,這裏我簡單的把代碼放在了viewDidLoad中。

//ViewController.m
#import
"ViewController.h" #import <objc/runtime.h> #import "MyValueAndProperty.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; unsigned int propertyCount = 0; MyValueAndProperty *myValueAndProperty = [[MyValueAndProperty alloc] init]; objc_property_t *properties = class_copyPropertyList([myValueAndProperty class], &propertyCount); for (unsigned int i = 0; i < propertyCount; i++) { objc_property_t property = properties[i]; //屬性名 const char * name = property_getName(property); //屬性描述 const char * propertyAttr = property_getAttributes(property); NSLog(@"屬性描述爲 %s 的 %s ", propertyAttr, name); //屬性的特性 unsigned int attrCount = 0; objc_property_attribute_t * attrs = property_copyAttributeList(property, &attrCount); for (unsigned int j = 0; j < attrCount; j ++) { objc_property_attribute_t attr = attrs[j]; const char * name = attr.name; const char * value = attr.value; NSLog(@"屬性的描述:%s 值:%s", name, value); } } unsigned int valueCount = 0; Ivar *ivars = class_copyIvarList([myValueAndProperty class], &valueCount); for (unsigned int i = 0; i < valueCount; i++) { Ivar ivar = ivars[i]; const char *name = ivar_getName(ivar); const char *type = ivar_getTypeEncoding(ivar); NSLog(@"成員變量名:%s, 類型:%s", name, type); } // Do any additional setup after loading the view, typically from a nib. }

接着運行程序,輸出結果爲:

2016-06-29 11:44:11.326 ValueAndProperty[45184:53842645] 屬性描述爲 T@"NSString",&,N,V_myProperty 的 myProperty 
2016-06-29 11:44:11.326 ValueAndProperty[45184:53842645] 屬性的描述:T 值:@"NSString"
2016-06-29 11:44:11.326 ValueAndProperty[45184:53842645] 屬性的描述:& 值:
2016-06-29 11:44:11.327 ValueAndProperty[45184:53842645] 屬性的描述:N 值:
2016-06-29 11:44:11.327 ValueAndProperty[45184:53842645] 屬性的描述:V 值:_myProperty
2016-06-29 11:44:11.327 ValueAndProperty[45184:53842645] 成員變量名:_myValue, 類型:B
2016-06-29 11:44:11.327 ValueAndProperty[45184:53842645] 成員變量名:_myProperty, 類型:@"NSString"

說明:T@"NSString",&,N,V_myProperty 這句是對屬性的整體描述,接下來四行是對描述的分解,每一個描述特徵以逗號做爲分隔,因此會有四個描述特徵,一、有name爲T的value爲@"NSString"(即屬性爲NSString類型), 二、name爲&(即編碼類型strong),三、name爲N(即非原子性),四、name爲V,值爲_myProperty(即編譯器給咱們生成一個成員變量_myProperty)。接下來最後打印出來的兩行:就是把全部的成員變量打印出來,從打印出來的結果就能夠知道確實是有_myProperty成員變量。

讀到這裏可能你們會以爲那些&,N,V,是怎麼來的,有什麼,或產生疑惑。不要緊,接下來就會講解這部份內容。


 

(六)類型編碼(Type Encodings)

  下面主要講解一下理論性的東西。首先類型編碼是什麼回事?類型編碼就是編譯器把全部的方法(包括它的返回值、參數、等等),屬性,編譯成一個個具備必定規則的字符串保存起來,咱們能夠經過runtime相關的API接口獲取到

要獲取方法的Type Encodings可以使用下面接口:

const char *method_getTypeEncoding(Method m);

獲取屬性的Type Encodings可以使用下面接口:

const char *property_getAttributes(objc_property_t property)

那麼這裏規則是怎麼樣的?規則有點多,至關於語言的言法,這裏就不進行截圖和講解,詳情查看官方文檔Type Encodings章節。想看中文的能夠參考文章。重點是要明白這節開頭說的加粗那句話。

到這裏,咱們就知道第五節的數些字符是怎麼來的。


 

  總結:到這裏本文對runtime相關內容的討論已經完成,固然還有runtime還有不少其它方面的東西沒有講,如:id類型、對象關聯等等。這些讀者能夠本身去研究。接下來就會分析runtime實踐中優秀的開源庫iOS runtime (二) (開源庫分析),以便知道學習了runtime這麼多知識點後,究竟能夠怎麼用,能夠用來作些什麼事情。

相關文章
相關標籤/搜索