Objective-C基礎之七(Runtime用法)

super關鍵字

super實際上是OC爲咱們提供的一個關鍵字,主要是繼承體系中用來調用類從父類繼承過來的屬性和方法,它只是一個標記,若是是使用super去調用方法,本質其實仍是拿到當前類對象,而後從其父類的緩存和方法列表進行查找。下面咱們就經過源碼來進一步探索super的底層實現。git

源碼解讀

  • 首先,先建立XLPerson類,而且在XLPerson.m中增長以下方法:
- (instancetype)init
{
    self = [super init];
    if (self) {
        NSLog(@"init");
    }
    return self;
}
複製代碼
  • 而後經過如下指令,將XLPerson.m轉成C++代碼
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc XLPerson.m
複製代碼
  • 查看生成的XLPerson.cpp文件,找到對應的init方法以下:
static instancetype _I_XLPerson_init(XLPerson * self, SEL _cmd) {
    self = objc_msgSendSuper((__rw_objc_super){
        (id)self,
        (id)class_getSuperclass(objc_getClass("XLPerson"))
    }, sel_registerName("init"));
    if (self) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_f3_lg91hwts5rjdlzjph0sn82m80000gp_T_XLPerson_d841fc_mi_0);
    }
    return self;
}
複製代碼
  • 能夠發現,在init方法中,[super init]最終轉換成了objc_msgSendSuper函數,函數具備兩個參數。github

  • 而後到objc源碼中查找objc_msgSendSuper函數的實現,發如今源碼中存在objc_msgSendSuper和objc_msgSendSuper2兩個函數,那麼到底super最終執行的是哪一個函數呢?能夠經過Xcode斷點,查看彙編代碼來找到最終調用的方法。數組

    • 首先咱們在XLPerson的init函數中打個斷點,而後在main函數中建立XLPerson對象,而且打開Xcode如下設置緩存

    • 而後運行程序,查看彙編代碼,能夠明確看出,super最終調用的是objc_msgSendSuper2函數bash

  • 而後在源碼中查看objc_msgSendSuper2函數聲明以下,objc_msgSendSuper2的前兩個參數分別對應上文中的結構體__rw_objc_super和sel_registerName("init")markdown

OBJC_EXPORT id _Nullable
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
複製代碼
  • 查看結構體objc_super的源碼以下,其中objc_super有兩個成員變量,一個是receiver,對應上文結構體中的self。一個是super_class,對應上文結構體中的class_getSuperclass(objc_getClass("XLPerson")。
/// Specifies the superclass of an instance. 
struct objc_super {
    //class的實例對象,用來接收消息
    __unsafe_unretained _Nonnull id receiver;
    //父類對象,用來決定方法查找的起點
    __unsafe_unretained _Nonnull Class super_class;
    /* super_class is the first class to search */
};
複製代碼
  • 雖然objc_msgSendSuper2函數聲明中第一個參數是objc_super類型的結構體,可是在實際傳參過程中,objc_msgSendSuper2函數第一個參數傳遞的實際上是objc_super2類型的結構體,在下文的彙編代碼中能夠看出。
struct objc_super2 {
    //當前消息接收者,實例對象
    id receiver;
    //當前類對象
    Class current_class;
};
複製代碼
  • 再次查找objc_msgSendSuper2函數的實現,發現objc_msgSendSuper2函數是由彙編來實現的,彙編代碼以下
ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame
//p0其實就是x0寄存器,p16其實就是x16寄存器
//p0中存放着真正的消息接收者,而p16存放着當前class對象
ldp	p0, p16, [x0]		// p0 = real receiver, p16 = class
//拿到x16中保存的class對象地址,經過地址偏移拿到它的superclass的地址,從新賦值給p16寄存器
ldr	p16, [x16, #SUPERCLASS] // p16 = class->superclass
//調用CacheLookup進行方法查找,而p16就是CacheLookup的參數
CacheLookup NORMAL

END_ENTRY _objc_msgSendSuper2
複製代碼

在objc_msgSendSuper2中,第一個參數是一個objc_super2類型的結構體。在arm64彙編中,x0寄存器通常用來存放參數或者返回值,此處的x0就存放告終構體objc_super2的地址值。架構

ldp p0, p16, [x0]表示從x0寄存器裏存放的地址開始,取前8個字節的地址賦值給p0,取後8個字節的地址賦值給p16,此時,p0寄存器的值就是objc_super2結構體中receiver的地址,p16的值就是objc_super2結構體中current_class的地址。app

ldr p16, [x16, #SUPERCLASS]其實就是拿到current_class的superclass,[x16, #SUPERCLASS]其實就是將x16中存放的內存地址偏移8個字節。而Class底層結構中,第一個成員變量是isa指針,佔用8個字節,第二個成員就是superclass。而x16裏存放的地址值就是Class的地址值,偏移8個字節其實就是拿到了superclass指針。這也印證了上文中所說的,objc_msgSendSuper2函數的第一個參數實際上是objc_super2類型的結構體。iphone

SUPERCLASS其實就是當前架構下指針所佔字節數,在arm64架構中,指針類型佔8個字節。函數

拿到superclass指針以後,將superclass的地址值存放在p16寄存器中,而p16寄存器的值就是CacheLookup函數的參數,CacheLookup會到superclass的方法列表中去查找對應的方法。可是真正的消息接收者仍是當前的receiver。

super總結

  • 經過super來調用方法時,底層會轉換成objc_msgSendSuper2函數
  • objc_msgSendSuper2函數函數傳遞至少兩個參數,第一個參數是一個objc_super2類型的結構體,內部有兩個成員,第一個成員是當前實例對象自己receiver,第二個成員是當前實例對象對應的類對象current_class。
  • 當調用[super xxxx]方法方法時,其實不是給receiver的superclass發送消息,而是給當前的receiver發送消息。可是在執行消息查找時,會首先拿到current_class的superclass,而後到superclass的方法緩存和方法列表中查詢方法。
  • 也就是說,objc_super2的第一個成員變量receiver決定了誰是真正的消息接收者,而第二個成員變量current_class的superclass其實就決定了當前消息從哪裏開始進行查找。

通常狀況下,給對象receiver發送一個消息,首先會到receiver的緩存列表或者方法列表中去查找,找不到纔會到superclass中去查找,而super則表示首先會給receiver發送一個消息,可是會先到recever的superclass中進行方法查找。這就是爲何使用super會調用父類方法。

Demo解析

首先建立XLPerson類,而後建立XLTeacher類繼承自XLPerson類,而後在XLTeacher.m中增長以下代碼

@implementation XLTeacher

- (instancetype)init
{
    self = [super init];
    if (self) {
        NSLog(@"%@", [self class]);     
        NSLog(@"%@", [super class]);    
        
        NSLog(@"%@", [self superclass]);
        NSLog(@"%@", [super superclass]);
    }
    return self;
}

@end
複製代碼

要想知道打印結果,還須要知道class和superclass的底層實現,在objc源碼的NSObject.mm中能夠看到具體的實現,以下

@implementation NSObject

+ (id)self {
    return (id)self;
}

- (id)self {
    return self;
}

+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}

+ (Class)superclass {
    return self->superclass;
}

- (Class)superclass {
    return [self class]->superclass;
}

@end
複製代碼
  1. [self class],經過self(當前XLTeacher的實例對象)調用class方法其實就是給self發送一條@selector(class)消息,因爲XLTeacher的實例對象以及它的父類都沒有實現class方法,因此最終會查找到基類NSObject的方法列表中,最終找到方法實現,返回object_getClass(self);,也就是當前類對象XLTeacher。
  2. [super class],其實也是向self發送一條@selector(class)消息,只不過會先到XLTea的父類中去查找方法實現,因爲父類沒有實現class方法,因此最終找到基類NSObject的class方法,返回object_getClass(self);,結果也是XLTeacher。
  3. [self superclass],向self發送一條@selector(superclass),首先會在XLTeacher的緩存和方法列表中查找,XLTeacher沒有實現此方法,所以會到XLTeacher的superclass中查找,父類也未實現,最後會找到NSObject中的superclass方法,返回[self class]->superclass,也就是XLPerson。
  4. [super superclass],利用super調用superclass方法,其實仍是向self發送@selector(superclass)消息,可是會直接先從XLTeacher的父類XLPerson中查找方法實現,因爲XLPerson未實現此方法,最終會調用NSObject的superclass方法,返回[self class]->superclass,也就是XLPerson。

isKindOfClass和isMemberOfClass

源碼解析

首先,查看源碼中NSObject.mm的實現

@implementation NSObject
+ (BOOL)isMemberOfClass:(Class)cls {
    //經過object_getClass獲取當前類對象的isa所指向的元類對象與cls進行比較
    return object_getClass((id)self) == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
    //比較當前實例對象的isa所指向的類對象與cls
    return [self class] == cls;
}

+ (BOOL)isKindOfClass:(Class)cls {  
    //遍歷當前類對象的元類對象以及它的父類的元類對象,若是和cls相等就返回YES
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
    //遍歷當前實例對象所對應的類對象以及它的父類,若是和cls相等就返回YES
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
@end
複製代碼
  • 類方法+isMemberOfClass:是經過isa獲取當前類對象的元類對象,與參數中的對象進行對比,若是相等則返回YES。
  • 對象方法-isMemberOfClass:是經過isa獲取當前實例對象的類對象,與參數中的對象進行對比,若是相等,則返回YES。
  • 類方法+isKindOfClass:經過遍歷當前類對象所對應的元類對象以及父類所對應的元類對象,若是遍歷到的元類對象與參數中的對象相等,則返回YES。
  • 對象方法-isKindOfClass:經過遍歷當前實例對象對應的類對象以及它的父類,若是遍歷到的類對象和參數中的對象相等,則返回YES。

Demo解析

建立XLPerson對象,繼承自NSObject。而後在main函數中增長如下代碼

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        BOOL result1 = [NSObject isMemberOfClass:[NSObject class]];
        BOOL result2 = [NSObject isKindOfClass:[NSObject class]];
        BOOL result3 = [XLPerson isMemberOfClass:[XLPerson class]];
        BOOL result4 = [XLPerson isKindOfClass:[XLPerson class]];
        
        NSLog(@"%d %d %d %d", result1, result2, result3, result4);
        
        XLPerson *person = [[XLPerson alloc] init];
        BOOL result5 = [person isMemberOfClass:[NSObject class]];
        BOOL result6 = [person isKindOfClass:[NSObject class]];
        BOOL result7 = [person isMemberOfClass:[XLPerson class]];
        BOOL result8 = [person isKindOfClass:[XLPerson class]];
        
        NSLog(@"%d %d %d %d", result5, result6, result7, result8);
    }
    return 0;
}
複製代碼

類方法

  1. [NSObject isMemberOfClass:[NSObject class]]會返回NO,緣由是object_getClass(NSObject)拿到的是元類對象,而[NSObject class]是類對象,二者不相等。
  2. [NSObject isKindOfClass:[NSObject class]]會返回YES,本來獲取NSObject的元類對象和[NSObject class]來進行對比,應該是不相等,可是比較特殊的一點是NSObject的元類對象的superclass指向它的類對象,因此此處返回YES。此處能夠參考Objective-C基礎之一(深刻理解OC對象)中isa和superclass的總結。
  3. [XLPerson isMemberOfClass:[XLPerson class]]會返回NO,獲取到XLPerson的元類對象和[XLPerson class]進行比較,二者不相等。
  4. [XLPerson isKindOfClass:[XLPerson class]]會返回NO,首先會拿到XLPerson的元類對象和[XLPerson class]對比,發現不相等,而後會經過XLPerson的superclass拿到父類的元類和[XLPerson class]進行對比,發現仍是不相等,一直遍歷到基類NSObject的元類,NSObject的元類的superclass指向NSObject的類對象,所以最後一次遍歷是拿NSObject的類對象和[XLPerson class]進行比較,確定不相等,因此返回NO。

實例方法

  1. [person isMemberOfClass:[NSObject class]]返回NO,緣由是拿person的類對象和NSObject比較,顯然不相等。
  2. [person isKindOfClass:[NSObject class]]返回YES,由於XLPerson本來就是繼承自NSObject,而isKindOfClass則是經過繼承鏈最終能找到NSObject的類對象和參數[NSObject class]進行對比,二者相等。
  3. [person isMemberOfClass:[XLPerson class]]返回YES,person的類對象就是XLPerson。
  4. [person isKindOfClass:[XLPerson class]]返回YES,緣由同上。

Runtime應用

使用runtime查看私有成員變量

利用runtime,咱們能夠遍歷類的全部屬性或者成員變量,拿到屬性或者成員變量咱們就能夠作不少事情,好比結合KVC給系統類對象的私有屬性賦值,或者將字典轉換成模型等等。

訪問系統類的私有屬性而且賦值

想給系統類的私有屬性賦值,前提是須要知道系統類有哪些私有屬性,首先,建立NSObject的分類NSObject+Ivar,以下

@interface NSObject (Ivar)

+ (void)logIvar;

@end

@implementation NSObject (Ivar)

+ (void)logIvar{
    unsigned int count;
    Ivar *ivars = class_copyIvarList(self, &count);
    for (int i = 0; i < count; i++) {
        //取出成員變量
        Ivar ivar = ivars[i];
        NSString *ivarName = [[NSString alloc] initWithUTF8String:ivar_getName(ivar)];
        NSLog(@"%@", ivarName);
    }
    free(ivars);
}

@end
複製代碼

而後以UITextField爲例,調用[UITextField logIvar]方法,就能夠打印出UITextField的全部成員變量,假如咱們須要修改輸入框提示語的文字顏色,就能夠找到其中的_placeholderLabel,而後經過KVC來拿到對應的屬性進行設置

UITextField *field = [[UITextField alloc] initWithFrame:CGRectMake(100, 100, 200, 30)];
field.placeholder = @"我是提示語";
[self.view addSubview:field];
[field setValue:[UIColor redColor] forKey:@"_placeholderLabel.textColor"];
複製代碼

在iOS 13以後是不容許訪問系統類的私有成員變量的,此處僅僅是用做功能演示,運行會報錯。

字典轉模型

OC開發過程當中,涉及到不少字典轉模型的需求,尤爲是和後臺進行交互。若是不使用runtime的話,就須要本身建立方法,本身去實現。以下

@interface XLPerson : NSObject

@property(nonatomic, copy)NSString *name;
@property(nonatomic, assign)int age;

+ (instancetype)personWithDic:(NSDictionary *)dic;

@end

@implementation XLPerson

+ (instancetype)personWithDic:(NSDictionary *)dic{
    XLPerson *person = [[XLPerson alloc] init];
    person.name = dic[@"name"];
    person.age = [dic[@"age"] intValue];
    
    return person;
}

@end
複製代碼

上述方法能夠實現字典轉模型的需求,可是若是存在字典套字典的這種狀況,那麼上述方法實現起來就會很是麻煩,所以,runtime的做用就體現了出來,典型的字典轉模型的工具如MJExtension也是利用runtime來實現。

建立NSObject的分類NSObject+Json,而後增長以下方法

@interface NSObject (Json)

+ (instancetype)objectWithDictionary:(NSDictionary *)dictionary;

@end

@implementation NSObject (Json)

+ (instancetype)objectWithDictionary:(NSDictionary *)dictionary{
    id object = [[self alloc] init];
    unsigned int count;
    Ivar *ivars = class_copyIvarList(self, &count);
    for (int i = 0; i < count; i++) {
        //取出成員變量
        Ivar ivar = ivars[i];
        NSMutableString *ivarName = [[NSMutableString alloc] initWithUTF8String:ivar_getName(ivar)];
        //移除字符串以前的_
        [ivarName deleteCharactersInRange:NSMakeRange(0, 1)];
        
        //給對應的屬性設值
        id value = dictionary[ivarName];
        if (value) {
            [object setValue:value forKey:ivarName];
        }
    }
    free(ivars);
    return object;
}

@end
複製代碼

使用方式以下

NSDictionary *dic = @{@"name" : @"張三", @"age" : @12};
XLPerson *person = [XLPerson objectWithDictionary:dic];
NSLog(@"%@ %d", person.name, person.age);
複製代碼

若是想要實現字典嵌套字典轉模型的方法,能夠參考MJExtension的實現。

runtime之方法交換(Method Swizzing)

以前的文章中說到過,類的全部對象方法都是存放在類對象的方法列表中,也就是存放在objc_class中的class_rw_t中,class_rw_t的源碼以下

struct class_rw_t {
    uint32_t flags;             //用來存放類的一些基本信息
    uint32_t version;           //版本號

    const class_ro_t *ro;       //class_ro_t類型指針

    method_array_t methods;     //方法列表
    property_array_t properties;//屬性列表
    protocol_array_t protocols; //協議列表
}

複製代碼

method_array_t是一個二維數組,內部存放了method_list_t,每一個method_list_t都存放着一個分類的全部方法,二維數組的最後一個method_list_t則存放着類編譯時就生成的全部方法,而method_list_t中存放的就是method_t,每個方法對應一個method_t,結構以下

struct method_t {
    SEL name;           //方法選擇器(方法名)
    const char *types;  //方法簽名
    MethodListIMP imp;  //方法實現的地址
}
複製代碼

在method_t中,imp則表示方法實現的地址,所以,若是想實現方法交換,只須要修改imp便可,而runtime就是經過交換兩個method_t的imp來實現方法交換的。

建立XLPerson類,以下

@interface XLPerson : NSObject

- (void)run;
- (void)sleep;

@end

@implementation XLPerson

+ (void)initialize
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method methodRun = class_getInstanceMethod(self, @selector(run));
        Method methodSleep = class_getInstanceMethod(self, @selector(sleep));
        method_exchangeImplementations(methodRun, methodSleep);
    });
}

- (void)run{
    
    NSLog(@"%s", __func__);
}

- (void)sleep{
    NSLog(@"%s", __func__);
}
@end

複製代碼

調用run方法和sleep方法會發現二者的實現被交換了,此處展現的是對象方法的交換,類方法交換和對象方法交換相似,更多方法交換實現能夠參考Aspect

runtime經常使用Api

動態類

//動態建立一個類(參數:父類,類名,額外的內存空間)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)

//註冊一個類(要在類註冊以前添加成員變量)
void objc_registerClassPair(Class cls) 

//銷燬一個類
void objc_disposeClassPair(Class cls)

//獲取isa指向的Class
Class object_getClass(id obj)

//設置isa指向的Class
Class object_setClass(id obj, Class cls)

//判斷一個OC對象是否爲Class
BOOL object_isClass(id obj)

//判斷一個Class是否爲元類
BOOL class_isMetaClass(Class cls)

//獲取父類
Class class_getSuperclass(Class cls)

複製代碼

成員變量

//獲取一個實例變量信息
Ivar class_getInstanceVariable(Class cls, const char *name)

//拷貝實例變量列表(最後須要調用free釋放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)

//設置和獲取成員變量的值
void object_setIvar(id obj, Ivar ivar, id value)
id object_getIvar(id obj, Ivar ivar)

//動態添加成員變量(已經註冊的類是不能動態添加成員變量的)
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)

//獲取成員變量的相關信息
const char *ivar_getName(Ivar v)
const char *ivar_getTypeEncoding(Ivar v)

複製代碼

屬性操做

//獲取一個屬性
objc_property_t class_getProperty(Class cls, const char *name)

//拷貝屬性列表(最後須要調用free釋放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

//動態添加屬性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                  unsigned int attributeCount)

//動態替換屬性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                      unsigned int attributeCount)

//獲取屬性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)
複製代碼

方法操做

//得到一個實例方法、類方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)

//方法實現相關操做
IMP class_getMethodImplementation(Class cls, SEL name) 
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2) 

//拷貝方法列表(最後須要調用free釋放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)

//動態添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

//動態替換方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)

//獲取方法的相關信息(帶有copy的須要調用free去釋放)
SEL method_getName(Method m)
IMP method_getImplementation(Method m)
const char *method_getTypeEncoding(Method m)
unsigned int method_getNumberOfArguments(Method m)
char *method_copyReturnType(Method m)
char *method_copyArgumentType(Method m, unsigned int index)

//選擇器相關
const char *sel_getName(SEL sel)
SEL sel_registerName(const char *str)

//用block做爲方法實現
IMP imp_implementationWithBlock(id block)
id imp_getBlock(IMP anImp)
BOOL imp_removeBlock(IMP anImp)
複製代碼
相關文章
相關標籤/搜索