天天看點

iOS-RunTime介紹及使用一、RunTime概念二、OC調用方法在RunTime中的具體實作

一、RunTime概念

RunTime簡稱運作時,我們總是聽說OC是動态語言運作時機制,也就是系統在運作時候的一些機制,其中最重要的是消息機制。C語言,函數的調用在編譯的時候會決定調用哪個函數,如果調用未實作的函數就會報錯,而OC語言屬于動态調用過程,在編譯時并不能決定真正調用哪個函數,隻有在真正的運作的時候才會根據函數的名稱找到對應函數來調用,當調用該對象上某個方法,而該對象上沒有實作這個方法的時候,可以通過“消息轉發”進行解決,也就是說,在編譯截斷,OC可以調用任何函數,即使是這個函數沒有實作,隻要聲明過就不會報錯。

二、OC調用方法在RunTime中的具體實作

《一》RunTime消息機制

消息機制是運作時裡面最重要的機制,OC是動态語言,本質都是發送消息,每個方法在運作時會被動态轉化為消息發送,即:

objc_msgSend(receiver, selector)

比如:

  • OC代碼執行個體方法調用底層的實作:
BackView *backView = [[BackView alloc] init];
[backView changeBgColor];

//編譯時底層轉化
//objc對象的isa指針指向他的類對象,進而可以找到對象上的方法
//SEL:方法編号,根據方法編号就可以找到對應方法的實作。
[backView performSelector:@selector(changeBgColor)];
//performSelector本質即為運作時,發送消息,誰做事情就調用誰 
objc_msgSend(backView, @selector(changeBgColor));
// 帶參數
objc_msgSend(backView, @selector(changeBgColor:),[UIColor RedColor]);
           
  • OC代碼類方法調用底層的實作
//本質是将類名轉化成類對象,初始化方法其實是建立類對象。
[BackView changeBgColor];
//BackView 隻是表示一個類名,調用方法其實是用的類對象去調用的。(類對象既然稱為對象,那它也是一個執行個體。類對象中也有一個isa指針指向它的元類(meta class),即類對象是元類的執行個體。元類内部存放的是類方法清單,根元類的isa指針指向自己,superclass指針指向NSObject類。)
//編譯時底層轉化

//RunTime 調用類方法同樣,類方法也是類對象去調用,是以需要擷取類對象,然後使用類對象去調用方法
Class backViewClass = [BackView class];
[backViewClass performSelector:@selector(changeBgColor)];
//performSelector本質即為運作時,發送消息,誰做事情就調用誰 

//類對象發送消息
objc_msgSend(backViewClass, @selector(changeBgColor));
// 帶參數
objc_msgSend(backViewClass, @selector(changeBgColor:),[UIColor RedColor]);
           

selector(SEL):是一個SEL方法選擇器。

SEL其主要作用是快速的通過SEL其主要作用是快速的通過方法名字查找到對應方法的函數指針,然後調用其函數。SEL其本身是一個Int類型的位址,位址中存放着方法的名字。

對于一個類中。每一個方法對應着一個SEL。是以一個類中不能存在2個名稱相同的方法,即使參數類型不同,因為SEL是根據方法名字生成的,相同的方法名稱隻能對應一個SEL。

  • 消息傳遞的底層實作

    這裡我們要先說一下,一個Objc對象如何進行記憶體布局的,我們先看一下objc_class源碼:

// runtime.h(類在runtime中的定義)

struct objc_class {
  Class isa OBJC_ISA_AVAILABILITY; //isa指針指向Meta Class,因為Objc的類的本身也是一個Object,為了處理這個關系,runtime就創造了Meta Class,當給類發送[NSObject alloc]這樣消息時,實際上是把這個消息發給了Class Object
  #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; // 方法緩存,對象接到一個消息會根據isa指針查找消息對象,這時會在method Lists中周遊,如果cache了,常用的方法調用時就能夠提高調用的效率。
  struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 協定連結清單
  #endif
  } OBJC2_UNAVAILABLE;
           

objc對象記憶體布局:

<1>所有父類的成員變量和自己的成員變量都會存放在該對象所對應的存儲空間中。
<2>每個對象内部都有一個isa指針,指向它的類對象,類對象中存放着本對象的:
    1、對象方法清單(對象能夠接受的消息清單,儲存再它所對應的類對象中)
    2、成員變量的清單
    3、屬性清單
           

每一個類都有一個方法清單Method List,儲存着類裡面所有的方法,根據SEL傳入的方法編号找到方法,然後找到方法的實作,然後在方法的實作裡面實作。

  • 消息發送動态查找對應的方法

    <1>執行個體對象調用方法後,底層調用

    [objc performSelector:@selector(SEL)];

    方法,編譯器将代碼轉化為

    objc_msgSend(receiver, selector)

    <2>在

    objc_msgSend

    函數中,首先通過

    objc

    isa

    指針找到

    objc

    對應的

    class

    ,在

    class

    中先去

    cache

    中通過

    SEL

    查找對應函數的

    method

    ,如果找到則通過

    method

    中的函數指針跳轉到對應的函數中去執行。

    <3>如果在

    cacha

    中未找到,再去

    methodList

    中查找,如果能找到,則将

    method

    加入到

    cache

    中,以友善下次查找,并通過

    method

    中的函數指針跳轉到對應的函數中去執行。

    <4>如果在

    methodlist

    中未找到,則去

    superClass

    中去查找,如果能找到,則将

    method

    加入到

    cache

    中,以友善下次查找,并通過

    method

    中的函數指針跳轉到對應的函數中去執行。
  • 消息傳遞的過程

    <接上↑>objc在向一個對象發送消息時,runtime庫會根據對象的isa指針找到該對象實際所屬的類,然後在該類中的方法清單以及其父類方法清單中尋找方法運作,即:

    objc_msgSend(receiver, selector)

    。如果,在最頂層的父類中依然找不到相應的方法時,程式在運作時會挂掉并抛出異常unrecognized selector sent to XXX 。但是在這之前,objc的運作時會給出三次拯救程式崩潰的機會:

    <1> Method resolution

    objc運作時會調用

    +resolveInstanceMethod:

    或者

    +resolveClassMethod:

    (執行個體方法和類方法),讓你有機會提供一個函數實作。如果你添加了函數,那運作時系統就會重新啟動一次消息發送的過程,否則 ,運作時就會移到下一步,消息轉發(Message Forwarding)。

    <2> Message Forwarding

    • <1>Fast forwarding

      如果目标對象實作了

      -forwardingTargetForSelector:

      ,Runtime 這時就會調用這個方法,給你把這個消息轉發給其他對象的機會。 隻要這個方法傳回的不是nil和self,整個消息發送的過程就會被重新開機,當然發送的對象會變成你傳回的那個對象。否則,就會繼續

      Normal Fowarding

      。 這裡叫Fast,隻是為了差別下一步的轉發機制。因為這一步不會建立任何新的對象,但下一步轉發會建立一個NSInvocation對象,是以相對更快點。
    • <2>Normal forwarding

      這一步是Runtime最後一次給你挽救的機會。首先它會發送

      -methodSignatureForSelector:

      消息獲得函數的參數和傳回值類型。如果

      -methodSignatureForSelector:

      傳回nil,Runtime則會發出

      -doesNotRecognizeSelector:

      消息,程式這時也就挂掉了。如果傳回了一個函數簽名,Runtime就會建立一個NSInvocation對象并發送

      -forwardInvocation:

      消息給目标對象。

《二》使用RunTime動态的添加對象的成員變量和方法

  • 動态添加方法

    動态給某各類添加方法,相當于懶加載機制。這裡我們以執行個體方法為例,首先我們先不實作對象方法,當調用

    performSelector:

    方法的時候,再去動态加載方法調用。

    [bg performSelector:@selector(changeBgColor)];

    當編譯時是不會報錯的,運作時才會報錯,因為這裡我們BaseView類中并沒有實作

    changeBgColor

    這個方法,當去類的

    Method List

    中發現找不到

    changeBgColor

    方法,會報錯找不到這個方法。這裡我們就用到了上面提到的消息轉發機制。當調用了沒有實作的對象方法時,就會調用

    +(BOOL)resolveInstanceMethod:(SEL)sel

    方法,當調用了沒有實作的類方法的時候,就會調用+(BOOL)resolveClassMethod:(SEL)sel方法。是以通過這兩個方法就可以動态添加方法,參數sel即表示沒有實作的方法。一個objective - C方法最終都是一個C函數,預設任何一個方法都有兩個參數。self : 方法調用者 _cmd : 調用方法編号。我們可以使用函數class_addMethod為類添加一個方法以及實作。

    動态添加方法:

+(BOOL)resolveInstanceMethod:(SEL)sel
{
    // 動态添加changeBgColor方法
    // 首先判斷sel是不是changeBgColor方法 也可以轉化成字元串進行比較。    
    if (sel == @selector(changeBgColor)) {
    /** 
     第一個參數: cls:給哪個類添加方法
     第二個參數: SEL name:添加方法的編号
     第三個參數: IMP imp: 方法的實作,函數入口,函數名可與方法名不同(建議與方法名相同)
     第四個參數: types :方法類型,需要用特定符号,參考API
     */
      class_addMethod(self, sel, (IMP) newChangeBgColor , "[email protected]:");
        // 處理完傳回YES
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void newChangeBgColor(id self ,SEL _cmd)
{

}
           

types:方法類型表:

iOS-RunTime介紹及使用一、RunTime概念二、OC調用方法在RunTime中的具體實作

type方法類型表.png

  • 動态添加變量

    1-> 動态擷取類中的所有屬性(包括私有)

Ivar *ivar = class_copyIvarList([self.baseView class], &count);
           

2->周遊屬性找到對應屬性字段

const char *varName = ivar_getName(var);
           

3->修改對應的字段

object_setIvar(self.baseView, var, @"newName");
           

具體:

-(void)addNewName{
    unsigned int count = 0;
    Ivar *ivar = class_copyIvarList([baseView class], &count);
    for (int i = 0; i<count; i++) {
        Ivar var = ivar[i];
        const char *varName = ivar_getName(var);
        NSString *name = [NSString stringWithUTF8String:varName];

        if ([name isEqualToString:@"oldName"]) {
            object_setIvar(baseView, var, @"newName");
            break;
        }
    }
  
    self.nameLabel.text = baseView.oldName;
}
           

《三》動态交換方法

當遇到所使用的系統方法或者不可修改的靜态庫方法功能不夠時,需要給此類方法擴充一些功能。比如我們有個BaseView類中有個changeBgColor的方法,此時我們想在這個方法裡做些操作,我們定義一個newChangeBgColor的方法。因為交換隻需進行一次,是以我們在BaseView的Categary中的load方法中,當加載分類的時候交換方法即可。交換方法的本質其實是交換兩個方法的實作

即:

1根據SEL方法編号在Method List中找到方法

2交換兩個IMP指針指向的方法實作

iOS-RunTime介紹及使用一、RunTime概念二、OC調用方法在RunTime中的具體實作

交換方法内部實作.png

+(void)load
{
    // 擷取要交換的兩個方法
    // 擷取類方法  用Method 接受一下
    // class :擷取哪個類方法 
    // SEL :擷取方法編号,根據SEL就能去對應的類找方法。
    Method oldChangeColorMethod = class_getClassMethod([UIImage class], @selector(changeBgColor));
    // 擷取第二個類方法
    Method newChangeColorMethod = class_getClassMethod([UIImage class], @selector(newChangeBgColor));
    // 交換兩個方法的實作 方法一 ,方法二。
    method_exchangeImplementations(oldChangeColorMethod, newChangeColorMethod);
    // IMP其實就是 implementation的縮寫:表示方法實作。
}
           

注意:交換方法的時候newMethod裡就不能再調用oldMethod方法了,因為調用oldMethod方法實質上相當于調用newMethod方法,會循環引用造成死循環。

《四》RunTim動态添加屬性

XCode運作在Category的.h檔案聲明@property編譯通過,但運作時如果沒有runtime處理,進行指派取值,就會報錯。

  • @property的本質是什麼

    @property = ivar + getter + setter;

    說人話:
“屬性”(property)有兩大概念:ivar(執行個體變量)、存取方法(access method = getter + setter)。

“屬性”(property)作為OC的一項特性,主要的作用就在于封裝對象總的資料。OC 對象通常會把其所需要的資料儲存為各種執行個體變量,執行個體變量一般通過“存取方法”(access method)來通路,其中,“擷取方法”(getter)用于讀取變量值,而“設定方法”(setter)用于寫入變量值。在正規的OC編碼風格中,存取方法有着嚴格的命名規範,正因為有了這種嚴格的命名規範,是以OC可以根據名稱自動建立出存取方法,其實也可以把屬性當做一種關鍵字,可以表示:

編譯器會自動寫出一套存取方法,用以通路給定類型中具有給定名稱的變量,是以你也可以這麼說:@property=getter+setter;

比如下面的這個類:

@interface Person : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end
           

上述代碼寫出來的類與下面這種寫法等效:

@interface Person : NSObject
- (NSString *)firstName;
- (void)setFirstName:(NSString *)firstName;
- (NSString *)lastName;
- (void)setLastName:(NSString *)lastName;
@end
           

而objc_property是一個結構體,包括name和attributes,定義如下:

struct property_t {
    const char *name;
    const char *attributes;
};
           

而attributes本質是objc_property_attribute_t,定義了property的一些屬性,定義如下:

/// Defines a property attribute
typedef struct {
    const char *name;           /**< The name of the attribute */
    const char *value;          /**< The value of the attribute (usually empty) */
} objc_property_attribute_t;
           

而attributes的具體内容是什麼呢?其實,包括:類型,原子性,記憶體語義和對應的執行個體變量。

例如:我們定義一個string的[email protected] (nonatomic, copy) NSString *string;

,通過 property_getAttributes(property)

擷取到attributes并列印出來之後的結果為[email protected]"NSString",C,N,V_string

其中T就代表類型,可參閱Type Encodings,C就代表Copy,N代表nonatomic,V就代表對于的執行個體變量。

ivar、getter、setter 是如何生成并添加到這個類中的?

“自動合成”( autosynthesis)

完成屬性定義後,編譯器會自動編寫通路這些屬性所需的方法,此過程叫做“自動合成”(autosynthesis)。需要強調的是,這個過程由編譯 器在編譯期執行,是以編輯器裡看不到這些“合成方法”(synthesized method)的源代碼。除了生成方法代碼 getter、setter 之外,編譯器還要自動向類中添加适當類型的執行個體變量,并且在屬性名前面加下劃線,以此作為執行個體變量的名字。在前例中,會生成兩個執行個體變量,其名稱分别為 _firstName 與 _lastName。也可以在類的實作代碼裡通過 @synthesize 文法來指定執行個體變量的名字.

@implementation Person
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
           

屬性是怎麼實作的呢?

1、OBJC_IVAR_$類名$屬性名稱 :該屬性的“偏移量” (offset),這個偏移量是“寫死” (hardcode),表示該變量距離存放對象的記憶體區域的起始位址有多遠。

2、setter 與 getter 方法對應的實作函數

3、ivar_list :成員變量清單

4、method_list :方法清單

5、prop_list :屬性清單

也就是說我們每次在增加一個屬性,系統都會在 ivar_list 中添加一個成員變量的描述,在 method_list 中增加 setter 與 getter 方法的描述,在屬性清單中增加一個屬性的描述,然後計算該屬性在對象中的偏移量,然後給出 setter 與 getter 方法對應的實作,在 setter 方法中從偏移量的位置開始指派,在 getter 方法中從偏移量開始取值,為了能夠讀取正确位元組數,系統對象偏移量的指針類型進行了類型強轉.

如何在@protocol和category中使用@property?

1、在 protocol 中使用 property 隻會生成 setter 和 getter 方法聲明,我們使用屬性的目的,是希望遵守我協定的對象能實作該屬性

2、category 使用 @property 也是隻會生成 setter 和 getter 方法的聲明,但是不會自動生成私有屬性,如果我們真的需要給 category 增加屬性的實作,需要借助于運作時的兩個函數:

1、動态添加屬性
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
參數一:id object : 給哪個對象添加屬性,這裡要給自己添加屬性,用self。
參數二:void * == id  key : 屬性名,根據key擷取關聯對象的屬性的值,在objc_getAssociatedObject中通過次key獲得屬性的值并傳回。
參數三:id value : 關聯的值,也就是set方法傳入的值給屬性去儲存。
參數四:objc_AssociationPolicy policy : 政策,屬性以什麼形式儲存。
>>>>>
 typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
 OBJC_ASSOCIATION_ASSIGN = 0,  // 指定一個弱引用相關聯的對象
 OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相關對象的強引用,非原子性
 OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  // 指定相關的對象被複制,非原子性
 OBJC_ASSOCIATION_RETAIN = 01401,  // 指定相關對象的強引用,原子性
 OBJC_ASSOCIATION_COPY = 01403     // 指定相關的對象被複制,原子性   
};
           
獲得屬性
objc_getAssociatedObject(id object, const void *key);
參數一:id object : 擷取哪個對象裡面的關聯的屬性。
參數二:void * == id  key : 什麼屬性,與objc_setAssociatedObject中的key相對應,即通過key值取出value。
此時已經成功給NSObject添加name屬性,并且NSObject對象可以通過點文法為屬性指派。
           

下面這個也是一樣的:

-(void)setName:(NSString *)name
{
    objc_setAssociatedObject(self, @"name",name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)name
{
    return objc_getAssociatedObject(self, @"name");    
}
           
《RunTime字典轉模型》

通過給NSObject添加分類,聲明并實作使用Runtime字典轉模型的類方法:

+ (instancetype)modelWithDict:(NSDictionary *)dict
           

KVC字典轉模型和RunTime轉模型的差別:

KVC:KVC字典轉模型實作原理是周遊字典中所有Key,然後去模型中查找相對應的屬性名,要求屬性名與Key必須一一對應,字典中所有key必須在模型中存在。

RunTime:RunTime字典轉模型實作原理是周遊模型中的所有屬性名,然後去字典查找相對應的Key,也就是以模型為準,模型中有哪些屬性,就去字典中找那些屬性。

RunTime字典轉模型的優點:當伺服器傳回的資料過多,而我們隻使用其中很少一部分時,沒有用的屬性就沒有必要定義成屬性浪費不必要的資源。隻儲存最有用的屬性即可。

字典轉模型簡要過程:

1、建立模型對象

2、使用class_copyIvarList方法copy成員屬性清單

unsigned int count = 0;
Ivar *ivarList = class_copyIvarList(self, &count);
           

參數一:__unsafe_unretained Class cls : 擷取哪個類的成員屬性清單。這裡是self,因為誰調用分類中類方法,誰就是self。

參數二:unsigned int *outCount : 無符号int型指針,這裡建立unsigned int型count,&count就是他的位址,保證在方法中可以拿到count的位址為count指派。傳出來的值為成員屬性總數。

傳回值:Ivar * : 傳回的是一個Ivar類型的指針 。指針預設指向的是數組的第0個元素,指針+1會向高位址移動一個Ivar機關的位元組,也就是指向第一個元素。Ivar表示成員屬性。

3、周遊成員屬性清單,獲得屬性清單

for (int i = 0 ; i < count; i++) {
     // 擷取成員屬性
     Ivar ivar = ivarList[i];
}
           

4、使用ivar_getName(ivar)獲得成員屬性名,因為成員屬性名傳回的是C語言字元串,将其轉化成OC 字元串

NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
或者ivar_getTypeEncoding(ivar)方法
           

5、獲得的成員屬性名是帶的成員屬性,去掉,獲得屬性名,也就是字典的key。

// 擷取key
NSString *key = [propertyName substringFromIndex:1];
           

6、擷取字典中key對應的Value。

id value = dict[key];
           

7、給模型屬性指派,并将模型傳回

if (value) {
// KVC指派:不能傳空
[objc setValue:value forKey:key];
}
return objc;
           

二級模型轉化方法:

+ (instancetype)modelWithDict:(NSDictionary *)dict{
    // 1.建立對應類的對象
    id objc = [[self alloc] init];
    // count:成員屬性總數
    unsigned int count = 0;
   // 獲得成員屬性清單和成員屬性數量
    Ivar *ivarList = class_copyIvarList(self, &count);
    for (int i = 0 ; i < count; i++) {
        // 擷取成員屬性
        Ivar ivar = ivarList[i];
        // 擷取成員名
       NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 擷取key
        NSString *key = [propertyName substringFromIndex:1];
        // 擷取字典的value key:屬性名 value:字典的值
        id value = dict[key];
        // 擷取成員屬性類型
        NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        // 二級轉換
        // value值是字典并且成員屬性的類型不是字典,才需要轉換成模型
        if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"]) {
            // 進行二級轉換
            // 擷取二級模型類型進行字元串截取,轉換為類名
            NSRange range = [propertyType rangeOfString:@"\""];
            propertyType = [propertyType substringFromIndex:range.location + range.length];
            range = [propertyType rangeOfString:@"\""];
            propertyType = [propertyType substringToIndex:range.location];
            // 擷取需要轉換類的類對象
           Class modelClass =  NSClassFromString(propertyType);
           // 如果類名不為空則進行二級轉換
            if (modelClass) {
                // 傳回二級模型指派給value
                value =  [modelClass modelWithDict:value];
            }
        }
        if (value) {
            // KVC指派:不能傳空
            [objc setValue:value forKey:key];
        }
    }
    // 傳回模型
    return objc;
}
           

總結

上述對RunTime的總結隻是一些自己平時的積累,借鑒了一些好的博文資料加上自己的一些了解,還有很多東西沒有了解到位,還請多多指教。