iOS開發-初識runtime

一.runtime的基本屬性


SEL

  • objc_msgSend函數第二個參數類型為SEL,它是selector在Objc中的表示類型(Swift中是Selector類)。selector是方法選擇器,可以理解為區分方法的 ID,而這個 ID 的數據結構是SEL:
typedef struct objc_selector *SEL;
  • 其實它就是個映射到方法的C字符串,你可以用 Objc 編譯器命令@selector()或者 Runtime 系統的sel_registerName函數來獲得一個SEL類型的方法選擇器。

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的技術,詳見官方文檔

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;

可以看到運行時一個類還關聯了它的超類指針,類名,成員變量,方法,緩存,還有附屬的協議

  • PS:
    objc_class結構體中:ivarsobjc_ivar_list指針;methodLists是指向objc_method_list指針的指針。也就是說可以動態修改methodLists的值來添加成員方法,這也是Category實現的原理,同樣解釋了Category不能添加屬性的原因。任性的話可以在Category中添加@dynamic的屬性,并利用運行期動態提供存取方法或干脆動態轉發;或者干脆使用關聯度對象(AssociatedObject)
    其中objc_ivar_listobjc_method_list分別是成員變量列表和方法列表:
   struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
struct objc_method_list {
struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;
    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}

如果你C語言不是特別好,可以直接理解為objc_ivar_list結構體存儲著objc_ivar數組列表,而objc_ivar結構體存儲了類的單個成員變量的信息;同理objc_method_list結構體存儲著objc_method數組列表,而objc_method結構體存儲了類的某個方法的信息。

  • 最后要提到的還有一個objc_cache,顧名思義它是緩存,它在objc_class的作用很重要,在后面會講到。
  • 不知道你是否注意到了objc_class中也有一個isa對象,這是因為一個ObjC類本身同時也是一個對象,為了處理類和對象的關系,runtime 庫創建了一種叫做元類 (Meta Class) 的東西,類對象所屬類型就叫做元類,它用來表述類對象本身所具備的元數據。類方法就定義于此處,因為這些方法可以理解成類對象的實例方法。每個類僅有一個類對象,而每個類對象僅有一個與之相關的元類。當你發出一個類似[NSObject alloc]的消息時,你事實上是把這個消息發給了一個類對象(Class Object),這個類對象必須是一個元類的實例,而這個元類同時也是一個根元類 (root meta class) 的實例。所有的元類最終都指向根元類為其超類。所有的元類的方法列表都有能夠響應消息的類方法。所以當 [NSObject alloc]這條消息發給類對象的時候,objc_msgSend()會去它的元類里面去查找能夠響應消息的方法,如果找到了,然后對這個類對象執行方法調用。 有趣的是根元類的超類是NSObject,而isa指向了自己,而NSObject的超類為nil,也就是它沒有超類。

Method

  • Method是一種代表類中的某個方法的類型。
typedef struct objc_method *Method;

objc_method在上面的方法列表中提到過,它存儲了方法名,方法類型和方法實現:

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}                                                          OBJC2_UNAVAILABLE;

方法名類型為SEL,前面提到過相同名字的方法即使在不同類中定義,它們的方法選擇器也相同。
方法類型method_types是個char指針,其實存儲著方法的參數類型和返回值類型。
method_imp指向了方法的實現,本質上是一個函數指針,后面會詳細講到。

Ivar

  • Ivar是一種代表類中實例變量的類型。
typedef struct objc_ivar *Ivar;
  • objc_ivar在上面的成員變量列表中也提到過:
-(NSString *)nameWithInstance:(id)instance {
    unsigned int numIvars = 0;
    NSString *key=nil;
    Ivar * ivars = class_copyIvarList([self class], &numIvars);
    for(int i = 0; i < numIvars; i++) {
        Ivar thisIvar = ivars[i];
        const char *type = ivar_getTypeEncoding(thisIvar);
        NSString *stringType =  [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
        if (![stringType hasPrefix:@"@"]) {
            continue;
        }
        if ((object_getIvar(self, thisIvar) == instance)) {//此處若 crash 不要慌!
            key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
            break;
        }
    }
    free(ivars);
    return key;
}

class_copyIvarList 函數獲取的不僅有實例變量,還有屬性。但會在原本的屬性名前加上一個下劃線。

IMP

  • IMPobjc.h中的定義是:
typedef id (*IMP)(id, SEL, ...);

它就是一個函數指針,這是由編譯器生成的。當你發起一個 ObjC消息之后,最終它會執行的那段代碼,就是由這個函數指針指定的。而IMP 這個函數指針就指向了這個方法的實現。既然得到了執行某個實例某個方法的入口,我們就可以繞開消息傳遞階段,直接執行方法。
你會發現IMP指向的方法與objc_msgSend函數類型相同,參數都包含idSEL類型。每個方法名都對應一個SEL類型的方法選擇器,而每個實例對象中的SEL對應的方法實現肯定是唯一的,通過一組idSEL參數就能確定唯一的方法實現地址;反之亦然。

Cache

  • runtime.hCache的定義如下:
typedef struct objc_cache *Cache
  • 還記得之前objc_class結構體中有一個struct objc_cache *cache吧,它到底是緩存啥的呢,先看看objc_cache的實現:
struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};

Cache為方法調用的性能進行優化,通俗地講,每當實例對象接收到一個消息時,它不會直接在isa指向的類的方法列表中遍歷查找能夠響應消息的方法,因為這樣效率太低了,而是優先在Cache中查找。Runtime系統會把被調用的方法存到Cache中(理論上講一個方法如果被調用,那么它有可能今后還會被調用),下次查找的時候效率更高。

Property

  • @property標記了類中的屬性,這個不必多說大家都很熟悉,它是一個指向objc_property結構體的指針:
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//這個更常用

可以通過class_copyPropertyListprotocol_copyPropertyList方法來獲取類和協議中的屬性:

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)

返回類型為指向指針的指針,因為屬性列表是個數組,每個元素內容都是一個objc_property_t指針,而這兩個函數返回的值是指向這個數組的指針。
舉個栗子,先聲明一個類:

@interface Lender : NSObject {
    float alone;
}
@property float alone;
@end

你可以用下面的代碼獲取屬性列表:

id LenderClass = objc_getClass("Lender");
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);

你可以用property_getName函數來查找屬性名稱:

const char *property_getName(objc_property_t property)

你可以用class_getPropertyprotocol_getProperty通過給出的名稱來在類和協議中獲取屬性的引用:

objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)

你可以用property_getAttributes函數來發掘屬性的名稱和@encode類型字符串:

const char *property_getAttributes(objc_property_t property)

把上面的代碼放一起,你就能從一個類中獲取它的屬性啦:

id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
    objc_property_t property = properties[i];
    fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}

對比下 class_copyIvarList 函數,使用 class_copyPropertyList函數只能獲取類的屬性,而不包含成員變量。但此時獲取的屬性名是不帶下劃線的。

二.runtime常用用法


1.獲取類的成員變量

- (IBAction)getClassMember:(UIButton *)sender {
    
    // 定義一個變量接收屬性數目
    unsigned int outCount = 0;
    
    // 獲得person類的成員變量列表
    Ivar *ivarArray = class_copyIvarList([UIView class], &outCount);
    
    // 遍歷列表
    for (int i = 0; i < outCount; ++i) {
        Ivar ivar = ivarArray[i];
        // 獲得成員變量名
        const char *ch = ivar_getName(ivar);
        // 獲得成員變量類型
        const char *type = ivar_getTypeEncoding(ivar);
        // C字符串轉OC字符串
        NSString *varName = [NSString stringWithUTF8String:ch];
        NSString *varType = [NSString stringWithUTF8String:type];
        NSLog(@"%@  %@",varType,varName);
    }
    // 釋放
    free(ivarArray);
}

2.獲得類中的所有屬性

- (IBAction)getClassProperty:(UIButton *)sender {
    
    unsigned int count = 0;
    
    objc_property_t *propertyArray = class_copyPropertyList([Person class], &count);
    
    for (int i = 0; i < count; ++i) {
        
        objc_property_t property = propertyArray[i];
        
        const char *name = property_getName(property);
        
        NSString *proName = [NSString stringWithUTF8String:name];
        
        NSLog(@"%@",proName);
    }
    // 釋放
    free(propertyArray);
}

3.獲得類的全部方法

- (IBAction)getClassMethod:(UIButton *)sender {
    // 定義一個變量接收方法數量
    unsigned int count = 0;
    // 獲得類的方法
    Method *methodArray = class_copyMethodList([Person class], &count);
    // 遍歷類的方法
    for (int i = 0; i < count; ++i) {
        Method method = methodArray[i];
        // 獲得方法名
        SEL sel = method_getName(method);
        // 獲得方法的實現
//        IMP imp = method_getImplementation(method);
        NSLog(@"%@",NSStringFromSelector(sel));
    }
    // 釋放
    free(methodArray);
}

4.獲得類的全部協議

- (IBAction)getClassProtocal:(UIButton *)sender {
    
    unsigned int count = 0;
    // 獲得指向該類遵循的所有協議的數組指針
    __unsafe_unretained Protocol **protocolArray = class_copyProtocolList([self class], &count);
    
    for (int i = 0; i < count; ++i) {
        Protocol *protocol = protocolArray[i];
        
        const char *name = protocol_getName(protocol);
        NSString *protocolName = [NSString stringWithUTF8String:name];
        NSLog(@"%@",protocolName);
    }
    // 釋放
    free(protocolArray);
}

5.動態改變成員變量

- (IBAction)changeClassMember:(UIButton *)sender {
    
//    self.student.name = @"willphonez";
    
    // 定義一個變量保存成員變量數量
    unsigned int count = 0;
    // 獲得成員變量的數組的指針
    Ivar *ivarArray = class_copyIvarList([self.student class], &count);
    // 遍歷成員變量數組
    for (int i = 0; i < count; ++i) {
        Ivar ivar = ivarArray[i];
        // 獲得成員變量名
        NSString *ivarName =[NSString stringWithUTF8String:ivar_getName(ivar)];
        // 根據成員變量名找到成員變量
        if ([ivarName isEqualToString:@"_scret"]) {
            // 對成員變量重新賦值
            object_setIvar(self.student, ivar, @"aibaozi");
            break;
        }
    }
    // 釋放
    free(ivarArray);
    // 打印結果
//    NSLog(@"%@",self.student.name);
    [self.student run];
}

6.動態交換類方法

- (IBAction)changeClassMethod:(UIButton *)sender {
    // 獲得兩個方法
    Method run = class_getInstanceMethod([Person class], @selector(run));
    Method eat = class_getInstanceMethod([Person class], @selector(eat));
    // 交換對象方法實現
    method_exchangeImplementations(run, eat);
    // 調用方法,或發現兩個方法的實現交換了
    [self.student run];
    [self.student eat];
    
    // runtime修改的是類,不是單一的對象,一次修改,在下次編譯前一直有效
    Person *p = [[Person alloc] init];
    [p run];
    [p eat];
    
    // 也可以在category中添加自己的方法去替換 (包括系統類的方法)
    [Person sleep];
}

7.動態添加方法

- (IBAction)addClassMethod:(UIButton *)sender {
    
    // 添加方法的實現到fromCity方法
    class_addMethod([self.student class], @selector(fromCity:),(IMP)fromCityAnswer, nil);
    
    // runtime修改的是類,不是單一的對象,一次修改,在下次編譯前一直有效
    
    if ([self.student respondsToSelector:@selector(fromCity:)]) {
        
        [self.student performSelector:@selector(fromCity:) withObject:@"廣州"];
        
    } else {
        NSLog(@"無可奉告");
    }
}

void fromCityAnswer(id self, SEL _cmd, NSString *string) {
    
    NSLog(@"我來自:%@",string);
}

8.動態給category擴展屬性

- (IBAction)addCategoryProperty:(UIButton *)sender {
    
    self.student.girlFriend = @"baozijun";
    NSLog(@"%@",self.student.girlFriend);
}
  • Person+ZWF.h文件
#import "Person.h"

@interface Person (ZWF)
// 擴展屬性
@property (nonatomic, strong) NSString *girlFriend;

@end
  • Person+ZWF.m文件
#import "Person+ZWF.h"
#import <objc/runtime.h>

@implementation Person (ZWF)

char friend;

- (NSString *)girlFriend
{
    // 獲取相關連得對象時使用Objc函數objc_getAssociatedObject
   return objc_getAssociatedObject(self, &friend);
}

- (void)setGirlFriend:(NSString *)girlFriend
{
    // 創建關聯要使用objc_setAssociatedObject
    // 4個參數依次是(源對象, 關鍵字, 關聯對象, 關聯策略)
    objc_setAssociatedObject(self, &friend, girlFriend, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

+ (void)load
{
    // 獲得兩個類方法
    Method sleep = class_getClassMethod(self, @selector(sleep));
    Method noSleep = class_getClassMethod(self, @selector(noSleep));
    // 交換兩個類方法的實現
    method_exchangeImplementations(sleep, noSleep);
}

+ (void)noSleep
{
    NSLog(@"%s",__func__);
}

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

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,774評論 0 9
  • 本文轉載自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex閱讀 796評論 0 1
  • 簡介 Runtime 又叫運行時,是一套底層的 C 語言 API,其為 iOS 內部的核心之一,我們平時編寫的 O...
    專業男神經閱讀 917評論 0 2
  • 我們常常會聽說 Objective-C 是一門動態語言,那么這個「動態」表現在哪呢?我想最主要的表現就是 Obje...
    Ethan_Struggle閱讀 2,231評論 0 7
  • 轉載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 768評論 0 2