iOS中Runtime常用示例

Runtime的內容大概有:動態獲取類名、動態獲取類的成員變量、動態獲取類的屬性列表、動態獲取類的方法列表、動態獲取類所遵循的協議列表、動態添加新的方法、類的實例方法實現的交換、動態屬性關聯、消息發送與消息轉發機制

1、獲取類名

動態的獲取類名是比較簡單的,使用class_getName(Class)就可以在運行時來獲取類的名稱。class_getName()函數返回的是一個char類型的指針,也就是C語言的字符串類型,所以我們要將其轉換成NSString類型,然后再返回出去。下方的+fetchClassName:方法就是我們封裝的獲取類名的方法,如下所示:

獲取類名.png

2、獲取成員變量

下方這個+fetchIvarList:這個方法就是我們封裝的獲取類的成員變量的方法。當然我們在獲取成員變量時,可以用ivar_getTypeEncoding()來獲取相應成員變量的類型。使用ivar_getName()來獲取相應成員變量的名稱。下方就是對獲取成員變量的功能的封裝。返回的是一個數組,數組的元素是一個字典,而字典中存儲的就是相應成員變量的名稱和類型。


獲取成員變量.png

下方就是調用上述方法獲取的TestClass類的成員變量。當然在運行時就沒有什么私有和公有之分了,只要是成員變量就可以獲取到。在OC中的給類添加成員屬性其實就是添加了一個成員變量和getter以及setter方法。所以獲取的成員列表中肯定帶有成員屬性,不過成員屬性的名稱前方添加了下劃線來與成員屬性進行區分。我們也可以獲取成員變量的類型,下方的_var1是NSInteger類型,動態獲取到的是q字母,其實是NSInteger的符號。而i就表示int類型,c表示Bool類型,d表示double類型,f則就表示float類型。當然這些基本類型都是由一個字母代替的,如果是引用類型的話,則直接就是一個字符串了,比如NSArray類型就是"@NSArray"。


out.png

3.獲取成員屬性

上面獲取的是類的成員變量,那么下方這個+fetchPropertyList:獲取的就是成員屬性。當然此刻獲取的只包括成員屬性,也就是那些有setter或者getter方法的成員變量。下方主要是使用了class_copyPropertyList(Class,&count)來獲取的屬性列表,然后通過for循環通過property_getName()來獲取每個屬性的名字。當然使用property_getName()獲取到的名字依然是C語言的char類型的指針,所以我們還需要將其轉換成NSString類型,然后放到數組中一并返回。如下所示:


獲取成員變量.png

下方這個截圖就是調用上述方法獲取的TestClass的所有的屬性,當然dynamicAddProperty是我們使用Runtime動態給TestClass添加的,所以也是可以獲取到的。當然我們獲取到的屬性的名稱為了與其對應的成員變量進行區分,成員屬性的名字前邊是沒有下劃線的。

out.png

4、獲取類的實例方法

接下來我們就來封裝一下獲取類的實例方法列表的功能,下方這個+fetchMethodList:就是我們封裝的獲取類的實例方法列表的函數。在下方函數中,通過class_copyMethodList()方法獲取類的實例方法列表,然后通過for循環使用method_getName()來獲取每個方法的名稱,然后將方法的名稱轉換成NSString類型,存儲到數組中一并返回。具體代碼如下所示:


獲取類的實例方法.png

下方這個截圖就是上述方法在TestClass上運行的結果,其中打印了TestClass類的所有實例方法,當然其中也必須得包含成員屬性的getter和setter方法。當然TestClass類目中的方法也是必須能獲取到的。結果如下所示:


out.png

5、獲取協議列表

下方是獲取我們類所遵循協議列表的方法,主要使用了class_copyProtocolList()來獲取列表,然后通過for循序使用protocol_getName()來獲取協議的名稱,最后將其轉換成NSString類型放入數組中返回即可。


獲取協議列表.png

下方就是我們獲取到的TestClass類所遵循的協議列表:


out.png

6、動態添加方法實現

下方就是動態的往相應類上添加方法以及實現。下方的+addMethod方法有三個參數,第一個參數是要添加方法的類,第二個參數是方法的SEL,第三個參數則是提供方法實現的SEL。稍后在消息發送和消息轉發時會用到下方的方法。下方主要是使用class_getInstanceMethod()和method_getImplementation()這兩個方法相結合獲取相應SEL的方法實現。下方的IMP其實就是Implementation的方法縮寫,獲取到相應的方法實現后,然后再調用class_addMethod()方法將IMP與SEL進行綁定即可。具體做法如下所示。


動態添加方法實現.png

7、方法實現交換

下方就是講類的兩個方法的實現進行交換。如果將MethodA與MethodB的方法實現進行交換的話,調用MethodA時就會執行MethodB的內容,反之亦然。


方法實現交換.png

8、實現NSCoding的自動歸檔與解檔

- (void)encodeWithCoder:(NSCoder *)encoder

{
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([Movie class], &count);

    for (int i = 0; i<count; i++) {
        // 取出i位置對應的成員變量
        Ivar ivar = ivars[i];
        // 查看成員變量
        const char *name = ivar_getName(ivar);
        // 歸檔
        NSString *key = [NSString stringWithUTF8String:name];
        id value = [self valueForKey:key];
        [encoder encodeObject:value forKey:key];
    }
    free(ivars);
}

- (id)initWithCoder:(NSCoder *)decoder
{
    if (self = [super init]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([Movie class], &count);
        for (int i = 0; i<count; i++) {
        // 取出i位置對應的成員變量
        Ivar ivar = ivars[i];
        // 查看成員變量
        const char *name = ivar_getName(ivar);
       // 歸檔
       NSString *key = [NSString stringWithUTF8String:name];
      id value = [decoder decodeObjectForKey:key];
       // 設置到成員變量身上
        [self setValue:value forKey:key];

        }
        free(ivars);
    } 
    return self;
}

9、字典轉模型
通過上面的coding協議的實現方法可以看到我們一樣可以通過runtime去實現字典轉模型,最近面試的時候也看到過面試題是字典轉模型的實現,機制就是KVC,但是如果是一層的字典轉模型(外層全是字符串,數字),如果有字典包含字典或者包含數組或者更多層,還需要在仔細考慮

  • 普通的一層字典轉模型
   // 創建對應模型對象
    id objc = [[self alloc] init];

    unsigned int count = 0;

    // 1.獲取成員屬性數組
    Ivar *ivarList = class_copyIvarList(self, &count);

    // 2.遍歷所有的成員屬性名,一個一個去字典中取出對應的value給模型屬性賦值
    for (int i = 0; i < count; i++) {

        // 2.1 獲取成員屬性
        Ivar ivar = ivarList[i];

        // 2.2 獲取成員屬性名 C -> OC 字符串
       NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];

        // 2.3 _成員屬性名 => 字典key
        NSString *key = [ivarName substringFromIndex:1];

        // 2.4 去字典中取出對應value給模型屬性賦值
        id value = dict[key];

        // 獲取成員屬性類型
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        }
  • 內層數組,字典的轉換
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType containsString:@"NS"]) { 

             //  是字典對象,并且屬性名對應類型是自定義類型
            // 處理類型字符串 @\"User\" -> User
            ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
            ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
            // 自定義對象,并且值是字典
            // value:user字典 -> User模型
            // 獲取模型(user)類對象
            Class modalClass = NSClassFromString(ivarType);

            // 字典轉模型
            if (modalClass) {
                // 字典轉模型 user
                value = [modalClass objectWithDict:value];
            }

        }

        if ([value isKindOfClass:[NSArray class]]) {
            // 判斷對應類有沒有實現字典數組轉模型數組的協議
            if ([self respondsToSelector:@selector(arrayContainModelClass)]) {

                // 轉換成id類型,就能調用任何對象的方法
                id idSelf = self;

                // 獲取數組中字典對應的模型
                NSString *type =  [idSelf arrayContainModelClass][key];

                // 生成模型
                Class classModel = NSClassFromString(type);
                NSMutableArray *arrM = [NSMutableArray array];
                // 遍歷字典數組,生成模型數組
                for (NSDictionary *dict in value) {
                    // 字典轉模型
                    id model =  [classModel objectWithDict:dict];
                    [arrM addObject:model];
                }

                // 把模型數組賦值給value
                value = arrM;

            }
        }

三、屬性關聯

屬性關聯說白了就是在類目中動態的為我們的類添加相應的屬性,如果看過之前發布的對Masonry框架源碼解析的博客的話,對下方的屬性關聯并不陌生。在Masonry框架中就利用Runtime的屬性關聯在UIView的類目中給UIView添加了一個約束數組,用來記錄添加在當前View上的所有約束。下方就是在TestClass的類目中通過objc_getAssociatedObject()和objc_setAssociatedObject()兩個方法為TestClass類添加了一個dynamicAddProperty屬性。上面我們獲取到的屬性列表中就含有該動態添加的成員屬性。

下方就是屬性關聯的具體代碼,如下所示。


屬性關聯.png

四、消息處理與消息轉發

在Runtime中不得不提的就是OC的消息處理和消息轉發機制。當然網上也有不少相關資料,本篇博客為了完整性,還是要聊一下消息處理與消息轉發的。當你調用一個類的方法時,先在本類中的方法緩存列表中進行查詢,如果在緩存列表中找到了該方法的實現,就執行,如果找不到就在本類中的方列表中進行查找。在本類方列表中查找到相應的方法實現后就進行調用,如果沒找到,就去父類中進行查找。如果在父類中的方法列表中找到了相應方法的實現,那么就執行,否則就執行下方的幾步。

當調用一個方法在緩存列表,本類中的方法列表以及父類的方法列表找不到相應的實現時,到程序崩潰階段中間還會有幾步讓你來挽救。接下來就來看看這幾步該怎么走。

1.消息處理(Resolve Method)

當在相應的類以及父類中找不到類方法實現時會執行+resolveInstanceMethod:這個類方法。該方法如果在類中不被重寫的話,默認返回NO。如果返回NO就表明不做任何處理,走下一步。如果返回YES的話,就說明在該方法中對這個找不到實現的方法進行了處理。在該方法中,我們可以為找不到實現的SEL動態的添加一個方法實現,添加完畢后,就會執行我們添加的方法實現。這樣,當一個類調用不存在的方法時,就不會崩潰了。具體做法如下所示:

消息處理.png

2、消息快速轉發

如果不對上述消息進行處理的話,也就是+resolveInstanceMethod:返回NO時,會走下一步消息轉發,即-forwardingTargetForSelector:。該方法會返回一個類的對象,這個類的對象有SEL對應的實現,當調用這個找不到的方法時,就會被轉發到SecondClass中去進行處理。這也就是所謂的消息轉發。當該方法返回self或者nil, 說明不對相應的方法進行轉發,那么就該走下一步了。

消息轉發.png

3.消息常規轉發

如果不將消息轉發給其他類的對象,那么就只能自己進行處理了。如果上述方法返回self的話,會執行-methodSignatureForSelector:方法來獲取方法的參數以及返回數據類型,也就是說該方法獲取的是方法的簽名并返回。如果上述方法返回nil的話,那么消息轉發就結束,程序崩潰,報出找不到相應的方法實現的崩潰信息。

在+resolveInstanceMethod:返回NO時就會執行下方的方法,下方也是講該方法轉發給SecondClass,如下所示:


消息常規轉發.png

五、objc_msgSend,SEL,id,Class

1.objc_msgSend

/* Basic Messaging Primitives
 *
 * On some architectures, use objc_msgSend_stret for some struct return types.
 * On some architectures, use objc_msgSend_fpret for some float return types.
 * On some architectures, use objc_msgSend_fp2ret for some float return types.
 *
 * These functions must be cast to an appropriate function pointer type 
 * before being called. 
 */

這是官方的聲明,從這個函數的注釋可以看出來了,這是個最基本的用于發送消息的函數。另外,這個函數并不能發送所有類型的消息,只能發送基本的消息。比如,在一些處理器上,我們必須使用objc_msgSend_stret來發送返回值類型為結構體的消息,使用objc_msgSend_fpret來發送返回值類型為浮點類型的消息,而又在一些處理器上,還得使用objc_msgSend_fp2ret來發送返回值類型為浮點類型的消息。
最關鍵的一點:無論何時,要調用objc_msgSend函數,必須要將函數強制轉換成合適的函數指針類型才能調用。
從objc_msgSend函數的聲明來看,它應該是不帶返回值的,但是我們在使用中卻可以強制轉換類型,以便接收返回值。另外,它的參數列表是可以任意多個的,前提也是要強制函數指針類型。
其實編譯器會根據情況在objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或 objc_msgSendSuper_stret四個方法中選擇一個來調用。如果消息是傳遞給超類,那么會調用名字帶有”Super”的函數;如果消息返回值是數據結構而不是簡單值時,那么會調用名字帶有”stret”的函數。

2.SEL

objc_msgSend函數第二個參數類型為SEL,它是selector在Objc中的表示類型(Swift中是Selector類)。selector是方法選擇器,可以理解為區分方法的 ID,而這個 ID 的數據結構是SEL:
typedef struct objc_selector *SEL;
其實它就是個映射到方法的C字符串,你可以用 Objc 編譯器命令@selector()或者 Runtime 系統的sel_registerName函數來獲得一個SEL類型的方法選擇器。
不同類中相同名字的方法所對應的方法選擇器是相同的,即使方法名字相同而變量類型不同也會導致它們具有相同的方法選擇器,于是 Objc 中方法命名有時會帶上參數類型(NSNumber一堆抽象工廠方法),Cocoa 中有好多長長的方法哦。

3.id

objc_msgSend第一個參數類型為id,大家對它都不陌生,它是一個指向類實例的指針:
typedef struct objc_object *id;
那objc_object又是啥呢:
struct objc_object { Class isa; };
objc_object結構體包含一個isa指針,根據isa指針就可以順藤摸瓜找到對象所屬的類。
PS:isa指針不總是指向實例對象所屬的類,不能依靠它來確定類型,而是應該用class方法來確定實例對象的類。因為KVO的實現機理就是將被觀察對象的isa指針指向一個中間類而不是真實的類,這是一種叫做 isa-swizzling 的技術,詳見官方文檔.

4.Class

之所以說isa是指針是因為Class其實是一個指向objc_class結構體的指針:
typedef struct objc_class *Class;
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;
    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;

可以看到運行時一個類還關聯了它的超類指針,類名,成員變量,方法,緩存,還有附屬的協議。
在objc_class結構體中:ivars是objc_ivar_list指針;methodLists是指向objc_method_list指針的指針。也就是說可以動態修改 *methodLists 的值來添加成員方法,這也是Category實現的原理.
參考:
http://www.cocoachina.com/ios/20170301/18804.html
http://www.lxweimin.com/p/46dd81402f63
http://www.lxweimin.com/p/19f280afcb24

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,775評論 0 9
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,947評論 18 139
  • 對于從事 iOS 開發人員來說,所有的人都會答出【runtime 是運行時】什么情況下用runtime?大部分人能...
    夢夜繁星閱讀 3,732評論 7 64
  • 這篇文章完全是基于南峰子老師博客的轉載 這篇文章完全是基于南峰子老師博客的轉載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,636評論 33 466
  • 清晨,第一縷陽光已經升起,照耀著這五光十色的世界。華章一如既往的在清晨跑步。現在他又多了一項堅持,買好早飯接沈君上...
    楊燕卿閱讀 466評論 1 6