iOS底層原理探究-Runtime

極客學偉發表於2018-05-04

Runtime

0. 概述

Objective-C Runtime 使得C具有了物件導向的能力,在程式執行時建立,檢查,修改類,物件和它們的方法。Runtime 是 C和彙編寫的,這裡www.opensource.apple.com/source/objc…可以下載Apple維護的開原始碼,GUN也有一個開源的Runtime版本,它們都努力保持一致。Apple官方的runtime程式設計指南

1、Runtime 函式

Runtime 系統是由一系列的函式和資料結構組成的公共介面動態共享庫,在/user/includeobjc 目錄下可以看到標頭檔案,可以用到其中一些函式通過C語言實現Objective-C中一樣的功能。蘋果官方文件 developer.apple.com/library/mac… 裡有詳細的Runtime 函式文件。

2. Class 和 NSObject 基礎資料結構

2.1 Class

objc/runtime.h 中objc_class 結構體的定義如下:

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指標查詢訊息物件,這時會在methodLists中遍歷,如果cache了,常用的方法呼叫時就能夠提高呼叫的效率。
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 協議連結串列
#endif

} OBJC2_UNAVAILABLE;

複製程式碼

objc_ivar_list 和 objc_method_list 的定義

//objc_ivar_list結構體儲存objc_ivar陣列列表
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;

//objc_method_list結構體儲存著objc_method的陣列列表
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;
}
複製程式碼

2.2 objc_object 和 id

objc_object 是一個類的例項結構體,objc/objc.h 中 objc_object是一個類的例項結構體定義如下:

struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};

typedef struct objc_object *id;
複製程式碼

向object傳送訊息時,Runtime 庫會根據object的isa指標找到這個例項object所屬於的類,然後在類的方法列表以及父類的方法列表中尋找對應的方法執行。id 是一個objc_object結構型別的指標,這個型別的物件能轉換成任何一種物件。

2.3 objc_cache

objc_class 結構體中cache欄位用於快取呼叫過的method。cache指標指向objc_cache結構體,這個結構體定義如下:

struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE; //指定分配快取bucket的總數。runtime使用這個欄位確定線性查詢陣列的索引位置
unsigned int occupied OBJC2_UNAVAILABLE; //實際佔用快取bucket總數
Method buckets[1] OBJC2_UNAVAILABLE; //指向Method資料結構指標的陣列,這個陣列的總數不能超過mask+1,但是指標是可能為空的,這就表示快取bucket沒有被佔用,陣列會隨著時間增長。
};
複製程式碼

2.4 Meta Class

meta class 是一個類物件的類,當向物件傳送訊息時,runtime 會在這個物件所屬類方法列表中查詢傳送訊息對應的方法,但當向類傳送訊息時,runtime就會在這個類的meta class方法列表中查詢。所有的meta class,包括Root class,SuperClass, SubClass的isa都指向Root clas的meta class,這樣能夠形成一個閉環。

meta class 關係圖

3.Runtime 類與物件操作函式

Runtime 有很多函式可以操作類和物件。類相關的是class為字首,物件相關相關的函式是 objc 或者 object 為字首。

3.1類相關操作函式

name

// 獲取類的類名
const cahr * class_getName (Class cls);
複製程式碼

super_class 和 meta_class

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

// 判斷給定的Class是否是一個meta class
BOOL class_isMetaClass (Class cls);
複製程式碼

instance_size

// 獲取例項大小
size_t class_getInstanceSize (Class cls);
複製程式碼

3.2 成員變數(ivars)及屬性

3.2.1 成員變數操作函式

// 獲取類中指定名稱例項成員變數的資訊
Ivar class_getInstanceVariable (Class cls, const char *name);

// 獲取類成員變數的資訊
Ivar class_getClassVariable (Class cls, const char *name);

// 新增成員變數
BOOL class_addIvar (Class cls, const char *name, size_t size, uint8_t alignment, const char *types);  //只能向在runtime時建立的類新增成員變數,這個方法只能在objc_allocateClassPair函式與objc_registerClassPair之間呼叫。另外,這個類也不能是元類。

// 獲取整個成員變數列表
Ivar * class_copyIvarList (Class cls, unsigned int *outCount); // 必須使用free()來釋放這個陣列

複製程式碼

測試成員變數

//成員變數
- (void)testIvar {
    BOOL isSuccessAddIvar = class_addIvar([NSString class], "_phone", sizeof(id), log2(sizeof(id)), "@");
    if (isSuccessAddIvar) {
        NSLog(@"Add Ivar success");
    }else{
        NSLog(@"Add Ivar error");
    }
    unsigned int outCount;
    Ivar *ivarList = class_copyIvarList([People class], &outCount);
    for (int i = 0; i < outCount; i++) {
        Ivar ivar = ivarList[i];
        const char *ivarName = ivar_getName(ivar);
        const char *type = ivar_getTypeEncoding(ivar);
        ptrdiff_t offset = ivar_getOffset(ivar);
        NSLog(@"ivar:%s, offset:%zd, type:%s", ivarName, offset, type);
    }
}

複製程式碼

3.2.2 屬性操作函式

// 獲取指定的屬性
objc_property_t class_getProperty(Class cls, const char *name);

// 獲取屬性列表
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);
複製程式碼

針對ivar來操作的,不過它只操作那些property的值,包括擴充套件中的property。

測試屬性

- (void)testProperty {    
    objc_property_attribute_t attribute1 = {"T", "@\"NSString\""};
    objc_property_attribute_t attribute2 = {"C", ""};
    objc_property_attribute_t attribute3 = {"N", ""};
    objc_property_attribute_t attribute4 = {"V", "_addProperty"};
    objc_property_attribute_t attributesList[] = {attribute1, attribute2, attribute3, attribute4};
    BOOL isSuccessAddProperty = class_addProperty([People class], "addProperty", attributesList, 4);
    if (isSuccessAddProperty) {
        NSLog(@"Add Property Success");
    }else{
        NSLog(@"Add Property Error");
    }
    unsigned int outCount;
    objc_property_t * propertyList = class_copyPropertyList([People class], &outCount);
    for (unsigned int i = 0; i < outCount; i++) {
        objc_property_t property = propertyList[i];
        const char *propertyName = property_getName(property);
        const char *attribute = property_getAttributes(property);
        NSLog(@"propertyName: %s, attribute: %s", propertyName, attribute);
        unsigned int attributeCount;
        objc_property_attribute_t *attributeList = property_copyAttributeList(property, &attributeCount);
        for (unsigned int i = 0; i < attributeCount; i++) {
            objc_property_attribute_t attribute = attributeList[i];
            const char *name = attribute.name;
            const char *value = attribute.value;
            NSLog(@"attribute name: %s, value: %s",name,value);
        }
    }
}

複製程式碼

執行結果

2018-05-01 17:14:52.957653+0800 RuntimeDemo[24515:910260] Add Property Success
2018-05-01 17:14:52.957871+0800 RuntimeDemo[24515:910260] propertyName: addProperty, attribute: T@"NSString",C,N,V_addProperty
2018-05-01 17:14:52.958034+0800 RuntimeDemo[24515:910260] attribute name: T, value: @"NSString"
2018-05-01 17:14:52.958175+0800 RuntimeDemo[24515:910260] attribute name: C, value:
2018-05-01 17:14:52.958309+0800 RuntimeDemo[24515:910260] attribute name: N, value:
2018-05-01 17:14:52.958452+0800 RuntimeDemo[24515:910260] attribute name: V, value: _addProperty
2018-05-01 17:14:52.958575+0800 RuntimeDemo[24515:910260] propertyName: name, attribute: T@"NSString",C,N,V_name
2018-05-01 17:14:52.958732+0800 RuntimeDemo[24515:910260] attribute name: T, value: @"NSString"
2018-05-01 17:14:52.958850+0800 RuntimeDemo[24515:910260] attribute name: C, value:
2018-05-01 17:14:52.958983+0800 RuntimeDemo[24515:910260] attribute name: N, value:
2018-05-01 17:14:52.959096+0800 RuntimeDemo[24515:910260] attribute name: V, value: _name
2018-05-01 17:14:52.959225+0800 RuntimeDemo[24515:910260] propertyName: age, attribute: T@"NSNumber",&,N,V_age
2018-05-01 17:14:52.959319+0800 RuntimeDemo[24515:910260] attribute name: T, value: @"NSNumber"
2018-05-01 17:14:52.959420+0800 RuntimeDemo[24515:910260] attribute name: &, value:
2018-05-01 17:14:52.959646+0800 RuntimeDemo[24515:910260] attribute name: N, value:
2018-05-01 17:14:52.959847+0800 RuntimeDemo[24515:910260] attribute name: V, value: _age
2018-05-01 17:14:52.960024+0800 RuntimeDemo[24515:910260] propertyName: sex, attribute: TQ,N,V_sex
2018-05-01 17:14:52.960186+0800 RuntimeDemo[24515:910260] attribute name: T, value: Q
2018-05-01 17:14:52.960365+0800 RuntimeDemo[24515:910260] attribute name: N, value:
2018-05-01 17:14:52.960584+0800 RuntimeDemo[24515:910260] attribute name: V, value: _sex
2018-05-01 17:14:52.960737+0800 RuntimeDemo[24515:910260] propertyName: address, attribute: T@"NSString",C,N,V_address
2018-05-01 17:14:52.960928+0800 RuntimeDemo[24515:910260] attribute name: T, value: @"NSString"
2018-05-01 17:14:52.961101+0800 RuntimeDemo[24515:910260] attribute name: C, value:
2018-05-01 17:14:52.961274+0800 RuntimeDemo[24515:910260] attribute name: N, value:
2018-05-01 17:14:52.961463+0800 RuntimeDemo[24515:910260] attribute name: V, value: _address
複製程式碼
T 是固定的,放在第一個
@”NSString” 代表這個property是一個字串物件
& 代表強引用,其中與之並列的是:’C’代表Copy,’&’代表強引用,’W’表示weak,assign為空,預設為assign。R 代表readOnly屬性,readwrite時為空
N 區分的nonatomic和atomic,預設為atomic,atomic為空,’N’代表是nonatomic
V_exprice V代表變數,後面緊跟著的是成員變數名,代表這個property的成員變數名為_exprice
複製程式碼

property_getAttributes 說明

3.2.3 協議相關函式

// 新增協議
BOOL class_addProtocol ( Class cls, Protocol *protocol );
 
// 返回類是否實現指定的協議
BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );
 
// 返回類實現的協議列表
Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount );
複製程式碼

測試協議

@protocol PeopleProcol <NSObject>
@end

- (void)testProtocol {
    // 新增協議
    Protocol *p = @protocol(PeopleProcol);
    if (class_addProtocol([People class], p)) {
        NSLog(@"Add Protoclol Success");
    }else{
        NSLog(@"Add protocol Fail");
    }
    if (class_conformsToProtocol([People class], p)) {
        NSLog(@"實現了 PeopleProcol 協議");
    }else{
        NSLog(@"沒有實現 PeopleProcol 協議");
    }
    unsigned int outCount;
    Protocol *__unsafe_unretained *protocolList = class_copyProtocolList([People class], &outCount);
    for (unsigned int i = 0; i < outCount; i++) {
        Protocol *p = protocolList[i];
        const char *protocolName = protocol_getName(p);
        NSLog(@"協議名稱: %s",protocolName);
    }
}
複製程式碼

執行結果


2018-05-01 17:29:12.580433+0800 RuntimeDemo[25007:940310] Add Protoclol Success
2018-05-01 17:29:12.580591+0800 RuntimeDemo[25007:940310] 實現了 PeopleProcol 協議
2018-05-01 17:29:12.580707+0800 RuntimeDemo[25007:940310] 協議名稱: PeopleProcol
複製程式碼

3.2.4 版本號

- (void)testVersion {
    int version = class_getVersion([People class]);
    NSLog(@"version %d",version);
    
    class_setVersion([People class], 10086);
    
    int nerVersion = class_getVersion([People class]);
    NSLog(@"nerVersion %d",nerVersion);
}
複製程式碼

執行結果

2018-05-01 17:38:29.593821+0800 RuntimeDemo[25266:956588] version 0
2018-05-01 17:38:29.593972+0800 RuntimeDemo[25266:956588] nerVersion 10086
複製程式碼

3.3 動態建立類和物件

3.3.1. 動態建立類

// 建立一個新類和元類
Class objc_allocateClassPair (Class superclass, const char *name, size_t extraBytes);

// 銷魂一個類及其相關聯的類
void objc_disposeClassPair (Class cls);

// 在應用中註冊由objc_allocateClassPair建立類
void objc_registerClassPair (Class cls);
複製程式碼

其中:

(1)objc_allocateClassPair函式:如果我們要建立一個根類,則superclass指定為Nil。extraBytes通常指定為0,該引數是分配給類和元類物件尾部的索引ivars的位元組數。

(2)為了建立一個新類,我們需要呼叫objc_allocateClassPair。然後使用諸如class_addMethod,class_addIvar等函式來為新建立的類新增方法、例項變數和屬性等。完成這些後,我們需要呼叫objc_registerClassPair函式來註冊類,之後這個新類就可以在程式中使用了。

(3)例項方法和例項變數應該新增到類自身上,而類方法應該新增到類的元類上。

(4)objc_disposeClassPair只能銷燬由objc_allocateClassPair建立的類,當有例項存在或者它的子類存在時,呼叫這個函式會丟擲異常。

測試程式碼:

- (void)testAddClass {
    Class TestClass = objc_allocateClassPair([NSObject class], "myClass", 0);
    if (class_addIvar(TestClass, "myIvar", sizeof(NSString *), sizeof(NSString *), "@")) {
        NSLog(@"Add Ivar Success");
    }
    class_addMethod(TestClass, @selector(method1:), (IMP)method0, "v@:");
    // 註冊這個類到runtime才可使用
    objc_registerClassPair(TestClass);
    
    // 生成一個例項化物件
    id myObjc = [[TestClass alloc] init];
    NSString *str = @"qiuxuewei";
    //給剛剛新增的變數賦值
    //object_setInstanceVariable(myobj, "myIvar", (void *)&str);在ARC下不允許使用
    [myObjc setValue:str forKey:@"myIvar"];
    [myObjc method1:10086];
}
- (void)method1:(int)a {
}
void method0(id self, SEL _cmd, int a) {
    Ivar v = class_getInstanceVariable([self class], "myIvar");
    id o = object_getIvar(self, v);
    NSLog(@"%@ \n int a is %d", o,a);
}
複製程式碼

執行結果:

2018-05-01 22:30:30.159096+0800 RuntimeDemo[31292:1162987] Add Ivar Success
2018-05-01 22:30:30.159344+0800 RuntimeDemo[31292:1162987] qiuxuewei 
 int a is 10086
複製程式碼

3.3.2. 動態建立物件

// 建立類的例項
id class_createInstance (Class cls, size_t extraBytes);

// 在指定位置建立類例項
id objc_constructInstance (Class cls, void *bytes);

// 銷燬類例項
void * objc_destructInstance (id obj);

複製程式碼

class_createInstance函式:建立例項時,會在預設的記憶體區域為類分配記憶體。extraBytes參數列示分配的額外位元組數。這些額外的位元組可用於儲存在類定義中所定義的例項變數之外的例項變數。該函式在ARC環境下無法使用。

呼叫class_createInstance的效果與+alloc方法類似。不過在使用class_createInstance時,我們需要確切的知道我們要用它來做什麼。

測試程式碼

- (void)testCreteInstance {
    id testInstance = class_createInstance([NSString class], sizeof(unsigned));
    id str1 = [testInstance init];
    NSLog(@"%@",[str1 class]);
    id str2 = [[NSString alloc] initWithString: @"Test"];
    NSLog(@"%@",[str2 class]);
}
複製程式碼

執行結果:

2018-05-01 23:43:25.941205+0800 RuntimeDemo[32783:1223167] NSString
2018-05-01 23:43:25.941364+0800 RuntimeDemo[32783:1223167] __NSCFConstantString
複製程式碼

3.3.3. 其他類和物件相關的操作函式

// 獲取已註冊的類定義的列表
int objc_getClassList(Class *buffer, int bufferCount);

// 建立並返回一個指向所有已註冊類的指標列表
Class * objc_copyClassList (unsigned int * outCount);

// 返回指定類的類定義
Class objc_lookUpClass ( const char *name );
Class objc_getClass ( const char *name );
Class objc_getRequiredClass ( const char *name );
 
// 返回指定類的元類
Class objc_getMetaClass ( const char *name );

複製程式碼

物件


// 返回指定物件的一份拷貝
id object_copy ( id obj, size_t size );
 
// 釋放指定物件佔用的記憶體
id object_dispose ( id obj );

// 修改類例項的例項變數的值
Ivar object_setInstanceVariable ( id obj, const char *name, void *value );
 
// 獲取物件例項變數的值
Ivar object_getInstanceVariable ( id obj, const char *name, void **outValue );
 
// 返回指向給定物件分配的任何額外位元組的指標
void * object_getIndexedIvars ( id obj );
 
// 返回物件中例項變數的值
id object_getIvar ( id obj, Ivar ivar );
 
// 設定物件中例項變數的值
void object_setIvar ( id obj, Ivar ivar, id value );

// 返回給定物件的類名
const char * object_getClassName ( id obj );
 
// 返回物件的類
Class object_getClass ( id obj );
 
// 設定物件的類
Class object_setClass ( id obj, Class cls );

複製程式碼

獲取類的定義

// 獲取已註冊的類定義的列表
int objc_getClassList (Class *)

複製程式碼

3.3.4. 應用例項

1. Json 轉 Model

操作函式

- (instancetype)initWithDict:(NSDictionary *)dict {
    if (self = [self init]) {
        NSMutableArray <NSString *>*keys = [NSMutableArray array];
        NSMutableArray <NSString *>*attributes = [NSMutableArray array];
        
        unsigned int outCount;
        objc_property_t * propertyList = class_copyPropertyList([self class], &outCount);
        for (unsigned int i = 0; i < outCount; i++) {
            objc_property_t property = propertyList[i];
            const char *name = property_getName(property);
            NSString *propertyName = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
            [keys addObject:propertyName];
            
            const char *attribute = property_getAttributes(property);
            NSString *attributeName = [NSString stringWithCString:attribute encoding:NSUTF8StringEncoding];
            [attributes addObject:attributeName];
        }
        free(propertyList);
        for (NSString *key in keys) {
            if ([dict valueForKey:key]) {
                [self setValue:[dict valueForKey:key] forKey:key];
            }
        }
    }
    return self;
}

複製程式碼
2. 快速歸解檔
遵循 NSCoding 協議
// 歸檔
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        unsigned int outCount;
        Ivar * ivarList = class_copyIvarList([self class], &outCount);
        for (unsigned int i = 0; i < outCount; i++) {
            Ivar ivar = ivarList[i];
            NSString *key = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
            [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
        }
    }
    return self;
}
// 解檔
- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int outCount;
    Ivar * ivarList = class_copyIvarList([self class], &outCount);
    for (unsigned int i = 0; i < outCount; i++) {
        Ivar ivar = ivarList[i];
        NSString *key = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
        [aCoder encodeObject:[self valueForKey:key] forKey:key];
    }
}
複製程式碼

測試

- (void)testCoder {
    NSString *key = @"peopleKey";
    People * people = [[People alloc] init];
    people.name = @"邱學偉";
    people.age = @18;
    NSData *peopleData = [NSKeyedArchiver archivedDataWithRootObject:people];
    [[NSUserDefaults standardUserDefaults] setObject:peopleData forKey:key];
    
    NSData *testData = [[NSUserDefaults standardUserDefaults] objectForKey:key];
    People *testPeople = [NSKeyedUnarchiver unarchiveObjectWithData:testData];
    NSLog(@"%@",testPeople.name);
}
複製程式碼
3. 關聯物件
// 關聯物件
void objc_setAssociatedObject (id object, const void * key, id value, objc_AssociationPolicy policy);

// 獲取關聯的物件
id objc_getAssociatedObject (id object, const void * key);

// 移除關聯的物件
void objc_removeAssociatedObjects (id object);
複製程式碼

引數說明

id object : 被關聯的物件 
const void *key : 關聯的key, set和get 需統一
id value : 關聯的物件
objc_AssociationPolicy policy : 記憶體管理的策略
複製程式碼

objc_AssociationPolicy policy的enum值有:、


typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {

    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */

};

複製程式碼

應用例項

//
//  People+Category.h
//  RuntimeDemo
//
//  Created by 邱學偉 on 2018/5/3.
//  Copyright © 2018年 邱學偉. All rights reserved.
//

#import "People.h"
@interface People (Category)
/**
 新增屬性
 */
@property (nonatomic, copy) NSString *blog;
@end

複製程式碼
//
//  People+Category.m
//  RuntimeDemo
//
//  Created by 邱學偉 on 2018/5/3.
//  Copyright © 2018年 邱學偉. All rights reserved.
//

#import "People+Category.h"
#import <objc/runtime.h>
@implementation People (Category)
static const char * cPeopleBlogKey = "cPeopleBlogKey";
- (NSString *)blog {
    return objc_getAssociatedObject(self, cPeopleBlogKey);
}
- (void)setBlog:(NSString *)blog {
    objc_setAssociatedObject(self, cPeopleBlogKey, blog, OBJC_ASSOCIATION_COPY);
}
@end
複製程式碼

4. 方法與訊息

4.1 SEL

SEL 又叫方法選擇器, 是表示一個方法的selector的指標,其定義如下

typedef  struct objc_selector *SEL;
複製程式碼

方法的selector用於表示執行時方法的名字,Objective-C在編譯時,會根據每一個方法的名字,引數序列,生成一個唯一的整型標示(Int型別的地址),這個標識就是SEL. 如下

+ (void)load {
    SEL sel = @selector(testMethod);
    NSLog(@"Programmer sel = %p",sel);
}
- (void)testMethod {
    NSLog(@"testMethod");
}
複製程式碼

兩個類之間,不管它們是父類與子類的關係,還是之間沒有這種關係,只要方法名相同,那麼方法的SEL就是一樣的。每一個方法都對應著一個SEL。所以在Objective-C同一個類(及類的繼承體系)中,不能存在2個同名的方法,即使引數型別不同也不行。相同的方法只能對應一個SEL。這也就導致Objective-C在處理相同方法名且引數個數相同但型別不同的方法方面的能力很差. 當然,不同的類可以擁有相同的selector,這個沒有問題。不同類的例項物件執行相同的selector時,會在各自的方法列表中去根據selector去尋找自己對應的IMP。

本質上,SEL只是一個指向方法的指標(準確的說,只是一個根據方法名hash化了的KEY值,能唯一代表一個方法),它的存在只是為了加快方法的查詢速度。這個查詢過程我們將在下面討論。

我們可以在執行時新增新的selector,也可以在執行時獲取已存在的selector,我們可以通過下面三種方法來獲取SEL:

(1)sel_registerName函式

(2)Objective-C編譯器提供的@selector()

(3)NSSelectorFromString()方法

4.2 IMP

IMP 是一個函式指標,指向方法實現的首地址。

id (*IMP)(id,SEL,...)
複製程式碼

這個函式使用當前CPU架構實現的標準的C呼叫約定。第一個引數是指向self的指標(如果是例項方法,則是類例項的記憶體地址;如果是類方法,則是指向元類的指標),第二個引數是方法選擇器(selector),接下來是方法的實際引數列表。

前面介紹過的SEL就是為了查詢方法的最終實現IMP的。由於每個方法對應唯一的SEL,因此我們可以通過SEL方便快速準確地獲得它所對應的IMP,查詢過程將在下面討論。取得IMP後,我們就獲得了執行這個方法程式碼的入口點,此時,我們就可以像呼叫普通的C語言函式一樣來使用這個函式指標了。

通過取得IMP,我們可以跳過Runtime的訊息傳遞機制,直接執行IMP指向的函式實現,這樣省去了Runtime訊息傳遞過程中所做的一系列查詢操作,會比直接向物件傳送訊息高效一些。

4.3 Method

Method 用於表示類定義中的方法,定義如下:

typedef struct objc_method *Method;

struct objc_method {
     SEL method_name OBJC2_UNAVAILABLE; // 方法名
     char *method_types OBJC2_UNAVAILABLE; //是個char指標,儲存著方法的引數型別和返回值型別
     IMP method_imp OBJC2_UNAVAILABLE; // 方法實現,函式指標
}
複製程式碼

該結構體中包含一個SEL和IMP,實際上相當於在SEL和IMP之間作了一個對映。有了SEL,我們便可以找到對應的IMP,從而呼叫方法的實現程式碼。

4.4 objc_method_description

objc_method_description定義了一個Objective-C方法,其定義如下:

struct objc_method_description { SEL name; char *types; };
複製程式碼

4.5 Method 相關操作函式

 // 呼叫指定方法的實現
    id method_invoke (id receiver, Method m, ...);
    
    // 呼叫返回一個資料結構的方法的實現
    void method_invoke_stret (id receiver, Method m, ...);
    
    // 獲取方法名
    SEL method_getName (Method m);
    
    // 獲取方法的實現
    IMP method_getImplementation (Method m);
    
    // 獲取描述方法引數和返回值型別的字串
    const char * method_getTypeEncoding (Method m);
    
    // 獲取方法的返回值型別的字串
    char * method_copyReturnType ( Method m );
    
    // 獲取方法的指定位置引數的型別字串
    char * method_copyArgumentType ( Method m, unsigned int index );
    
    // 通過引用返回方法的返回值型別字串
    void method_getReturnType ( Method m, char *dst, size_t dst_len );
    
    // 返回方法的引數的個數
    unsigned int method_getNumberOfArguments ( Method m );
    
    // 通過引用返回方法指定位置引數的型別字串
    void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
    
    // 返回指定方法的方法描述結構體
    struct objc_method_description * method_getDescription ( Method m );
    
    // 設定方法的實現
    IMP method_setImplementation ( Method m, IMP imp );
    
    // 交換兩個方法的實現
    void method_exchangeImplementations ( Method m1, Method m2 );
複製程式碼

(1)method_invoke函式,返回的是實際實現的返回值。引數receiver不能為空。這個方法的效率會比method_getImplementation和method_getName更快。

(2)method_getName函式,返回的是一個SEL。如果想獲取方法名的C字串,可以使用sel_getName(method_getName(method))。

(3)method_getReturnType函式,型別字串會被拷貝到dst中。

(4)method_setImplementation函式,注意該函式返回值是方法之前的實現。

4.6 方法選擇器

// 返回給定選擇器指定的方法的名稱
const char * sel_getName ( SEL sel );

// 在Objective-C Runtime系統中註冊一個方法,將方法名對映到一個選擇器,並返回這個選擇器
SEL sel_registerName ( const char *str );

// 在Objective-C Runtime系統中註冊一個方法
SEL sel_getUid ( const char *str );

// 比較兩個選擇器
BOOL sel_isEqual ( SEL lhs, SEL rhs );
複製程式碼

sel_registerName函式:在我們將一個方法新增到類定義時,我們必須在Objective-C Runtime系統中註冊一個方法名以獲取方法的選擇器。

4.7 方法呼叫流程

1. 訊息傳送

在Objective-C中,訊息直到執行時才繫結到方法實現上。編譯器會將訊息表示式[receiver message]轉化為一個訊息函式的呼叫,即objc_msgSend。這個函式將訊息接收者和方法名作為其基礎引數,如以下所示:

objc_msgSend(receiver, selector)
複製程式碼

如果訊息中還有其他引數,則該方法的形式如下所示:

objc_msgSend(receiver, selector,arg1, arg2, ...);
複製程式碼

這個函式完成了動態繫結的所有事情:

(1)首先它找到selector對應的方法實現。因為同一個方法可能在不同的類中有不同的實現,所以我們需要依賴於接收者的類來找到的確切的實現。

(2)它呼叫方法實現,並將接收者物件及方法的所有引數傳給它。

(3)最後,它將實現返回的值作為它自己的返回值。

訊息的關鍵在於結構體 objc_class, 這個結構體有兩個欄位是我們在分發訊息的時候關注的:

  1. 指向父類的指標。
  2. 一個類的方法分發表,即methodLists

當我們建立一個新物件時,先為其分配記憶體,並初始化其成員變數。其中isa指標也會被初始化,讓物件可以訪問類及類的繼承體系。

下圖演示了這樣一個訊息的基本框架:

訊息的基本框架

當訊息傳送給一個物件時,objc_msgSend通過物件的isa指標獲取到類的結構體,然後在方法分發表裡面查詢方法的selector。如果沒有找到selector,則通過objc_msgSend結構體中的指向父類的指標找到其父類,並在父類的分發表裡面查詢方法的selector。依此,會一直沿著類的繼承體系到達NSObject類。一旦定位到selector,函式會就獲取到了實現的入口點,並傳入相應的引數來執行方法的具體實現。如果最後沒有定位到selector,則會走訊息轉發流程

2. 隱藏引數

objc_msgSend 有兩個隱藏引數

  1. 訊息接收物件
  2. 方法的selector

這兩個引數為方法的實現提供了呼叫者的資訊。之所以說是隱藏的,是因為他們在定義方法的原始碼中沒有宣告。他們是在編譯時被插入實現程式碼的。

雖然這些引數沒有顯示宣告,但在程式碼中仍然可以引用它們。我們可以使用self來引用接收者物件,使用_cmd 來引用選擇器。如下程式碼:

- strange
{
    id  target = getTheReceiver();
    SEL method = getTheMethod();
    if ( target == self || method == _cmd )
        return nil;
    return [target performSelector:method];
}
複製程式碼

當然,這兩個引數我們用的比較多的是self,_cmd 在實際中用得比較少。

3. 獲取方法地址

Runtime中方法的動態繫結讓我們寫程式碼時更具靈活性,如我們可以把訊息轉發給我們想要的物件,或者隨意交換一個方法的實現等。不過靈活性的提升也帶來了效能上的一些損耗。畢竟我們需要去查詢方法的實現,而不像函式呼叫來得那麼直接。當然,方法的快取一定程度上解決了這一問題。

我們上面提到過,如果想要避開這種動態繫結方式,我們可以獲取方法實現的地址,然後像呼叫函式一樣來直接呼叫它。特別是當我們需要在一個迴圈內頻繁地呼叫一個特定的方法時,通過這種方式可以提高程式的效能。

NSObject類提供了methodForSelector:方法,讓我們可以獲取到方法的指標,然後通過這個指標來呼叫實現程式碼。我們需要將methodForSelector:返回的指標轉換為合適的函式型別,函式引數和返回值都需要匹配上。

這裡需要注意的就是函式指標的前兩個引數必須是id和SEL。

當然這種方式只適合於在類似於for迴圈這種情況下頻繁呼叫同一方法,以提高效能的情況。另外,methodForSelector:是由Cocoa執行時提供的;它不是Objective-C語言的特性。

- (void)testCommonMethod {
    for (int i = 0; i < 10000; i++) {
        [self logMethod:i];
    }
    //執行時長: Test Case '-[RuntimeDemoTests testCommonMethod]' passed (2.311 seconds).
}

- (void)testRuntimeMethod {
    void(*logM)(id, SEL, int);
    IMP imp = [self methodForSelector:@selector(logMethod:)];
    logM = (void(*)(id, SEL, int))imp;
    for (int i = 0; i < 10000; i++) {
        logM(self, @selector(logMethod:), i);
    }
    //執行時長: Test Case '-[RuntimeDemoTests testRuntimeMethod]' passed (2.199 seconds).
}
複製程式碼

4. 訊息轉發

當一個物件能接收一個訊息時,就會走正常的方法呼叫流程。但如果一個物件無法接收指定訊息時,又會發生什麼事呢?預設情況下,如果是以[object message]的方式呼叫方法,如果object無法響應message訊息時,編譯器會報錯。但如果是以perform…的形式來呼叫,則需要等到執行時才能確定object是否能接收message訊息。如果不能,則程式崩潰。

通常,當我們不能確定一個物件是否能接收某個訊息時,會先呼叫respondsToSelector:來判斷一下。如下程式碼所示:

// perform方法要求傳入引數必須是物件,如果方法本身的引數是int,直接傳NSNumber會導致得到的int引數不正確
if ([self respondsToSelector:@selector(logMethod:)]) {
        [self performSelector:@selector(logMethod:) withObject:[NSNumber numberWithInt:10086]];
    }
複製程式碼

當一個物件無法接收某一訊息時,就會啟動所謂”訊息轉發(message forwarding)“機制,通過這一機制,我們可以告訴物件如何處理未知的訊息。預設情況下,物件接收到未知的訊息,會導致程式崩潰,通過控制檯,我們可以看到以下異常資訊:

unrecognized selector sent to instance 0x100111940

這段異常資訊實際上是由NSObject的”doesNotRecognizeSelector”方法丟擲的。不過,我們可以採取一些措施,讓我們的程式執行特定的邏輯,而避免程式的崩潰。

訊息轉發機制基本上分為三個步驟:

(1)動態方法解析

(2)備用接收者

(3)完整轉發

1. 動態方法解析
物件在接收到未知的訊息時,首先會呼叫所屬類的類方法+resolveInstanceMethod:(例項方法)或者+resolveClassMethod:(類方法)。在這個方法中,我們有機會為該未知訊息新增一個”處理方法”“。不過使用該方法的前提是我們已經實現了該”處理方法”,只需要在執行時通過class_addMethod函式動態新增到類裡面就可以了。如下程式碼所示:
複製程式碼
void functionForMethod (id self, SEL _cmd) {
    NSLog(@"functionForMethod");
}
/// 呼叫未實現物件方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selName = NSStringFromSelector(sel);
    if ([selName isEqualToString:@"methodNull"]) {
        class_addMethod(self.class, sel, (IMP)functionForMethod, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}
/// 呼叫未實現類方法
+ (BOOL)resolveClassMethod:(SEL)sel {
    NSString *selName = NSStringFromSelector(sel);
    if ([selName isEqualToString:@"methodNull"]) {
        //v@:表示返回值和引數,可以在蘋果官網檢視Type Encoding相關文件 https://developer.apple.com/library/mac/DOCUMENTATION/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
        class_addMethod(self.class, sel, (IMP)functionForMethod, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}
複製程式碼
2. 備用接受者
如果在上一步無法處理訊息,則Runtime會繼續調以下方法:
複製程式碼
- (id)forwardingTargetForSelector:(SEL)aSelector
複製程式碼

如果一個物件實現了這個方法,並返回一個非nil的結果,則這個物件會作為訊息的新接收者,且訊息會被分發到這個物件。當然這個物件不能是self自身,否則就是出現無限迴圈。當然,如果我們沒有指定相應的物件來處理aSelector,則應該呼叫父類的實現來返回結果。

使用這個方法通常是在物件內部,可能還有一系列其它物件能處理該訊息,我們便可借這些物件來處理訊息並返回,這樣在物件外部看來,還是由該物件親自處理了這一訊息。如下程式碼所示:

//
//  People.m
//  RuntimeDemo
//
//  Created by 邱學偉 on 2018/4/27.
//  Copyright © 2018年 邱學偉. All rights reserved.
//

#import "People.h"
#import <objc/runtime.h>

@interface XWPeople : NSObject
- (void)people2log;
@end
@implementation XWPeople
- (void)people2log {
    NSLog(@"people2log");
}
@end

@interface People () <NSCoding> {
}
@property (nonatomic, strong) XWPeople *xw_people;
@end
@implementation People

//// 1 級處理
void functionForMethod (id self, SEL _cmd) {
    NSLog(@"functionForMethod");
}
/// 呼叫未實現物件方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selName = NSStringFromSelector(sel);
    if ([selName isEqualToString:@"methodNull"]) {
        class_addMethod(self.class, sel, (IMP)functionForMethod, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}
/// 呼叫未實現類方法
+ (BOOL)resolveClassMethod:(SEL)sel {
    NSString *selName = NSStringFromSelector(sel);
    if ([selName isEqualToString:@"methodNull"]) {
        //v@:表示返回值和引數,可以在蘋果官網檢視Type Encoding相關文件 https://developer.apple.com/library/mac/DOCUMENTATION/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
        class_addMethod(self.class, sel, (IMP)functionForMethod, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

/// 2 級處理
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSString *selName = NSStringFromSelector(aSelector);
    if ([selName isEqualToString:@"people2log"]) {
        return self.xw_people;
    }
    return [super forwardingTargetForSelector:aSelector];
}
- (XWPeople *)xw_people {
    if(!_xw_people){
        _xw_people = [[XWPeople alloc] init];
    }
    return _xw_people;
}
@end
複製程式碼

這一步合適於我們只想將訊息轉發到另一個能處理該訊息的物件上。但這一步無法對訊息進行處理,如操作訊息的引數和返回值。

3. 完整轉發

如果在上一步還不能處理未知訊息,則唯一能做的就是啟用完整的訊息轉發機制了。此時會呼叫以下方法:

- (void)forwardInvocation:(NSInvocation *)anInvocation

複製程式碼

執行時系統會在這一步給訊息接收者最後一次機會將訊息轉發給其它物件。物件會建立一個表示訊息的NSInvocation物件,把與尚未處理的訊息有關的全部細節都封裝在anInvocation中,包括selector,目標(target)和引數。我們可以在forwardInvocation方法中選擇將訊息轉發給其它物件。

forwardInvocation:方法的實現有兩個任務:

(1)定位可以響應封裝在anInvocation中的訊息的物件。這個物件不需要能處理所有未知訊息。

(2)使用anInvocation作為引數,將訊息傳送到選中的物件。anInvocation將會保留呼叫結果,執行時系統會提取這一結果並將其傳送到訊息的原始傳送者。

不過,在這個方法中我們可以實現一些更復雜的功能,我們可以對訊息的內容進行修改,比如追回一個引數等,然後再去觸發訊息。另外,若發現某個訊息不應由本類處理,則應呼叫父類的同名方法,以便繼承體系中的每個類都有機會處理此呼叫請求。

還有一個很重要的問題,我們必須重寫以下方法:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
複製程式碼

訊息轉發機制使用從這個方法中獲取的資訊來建立NSInvocation物件。因此我們必須重寫這個方法,為給定的selector提供一個合適的方法簽名。

完整的示例如下所示:

//
//  People.m
//  RuntimeDemo
//
//  Created by 邱學偉 on 2018/4/27.
//  Copyright © 2018年 邱學偉. All rights reserved.
//

#import "People.h"
#import <objc/runtime.h>

@interface XWPeople : NSObject
- (void)people2log;
- (void)people3log;
@end
@implementation XWPeople
- (void)people2log {
    NSLog(@"people2log");
}

- (void)people3log {
    NSLog(@"people3log");
}
@end

@interface People () <NSCoding> {
}
@property (nonatomic, strong) XWPeople *xw_people;
@end
@implementation People

//// 1 級處理
void functionForMethod (id self, SEL _cmd) {
    NSLog(@"functionForMethod");
}
/// 呼叫未實現物件方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selName = NSStringFromSelector(sel);
    if ([selName isEqualToString:@"methodNull"]) {
        class_addMethod(self.class, sel, (IMP)functionForMethod, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}
/// 呼叫未實現類方法
+ (BOOL)resolveClassMethod:(SEL)sel {
    NSString *selName = NSStringFromSelector(sel);
    if ([selName isEqualToString:@"methodNull"]) {
        //v@:表示返回值和引數,可以在蘋果官網檢視Type Encoding相關文件 https://developer.apple.com/library/mac/DOCUMENTATION/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
        class_addMethod(self.class, sel, (IMP)functionForMethod, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

/// 2 級處理
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSString *selName = NSStringFromSelector(aSelector);
    if ([selName isEqualToString:@"people2log"]) {
        return self.xw_people;
    }
    return [super forwardingTargetForSelector:aSelector];
}
- (XWPeople *)xw_people {
    if(!_xw_people){
        _xw_people = [[XWPeople alloc] init];
    }
    return _xw_people;
}

/// 3 級處理
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        if ([XWPeople instancesRespondToSelector:aSelector]) {
            signature = [XWPeople instanceMethodSignatureForSelector:aSelector];
        }
    }
    return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([XWPeople instancesRespondToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:self.xw_people];
    }
}

@end
複製程式碼

NSObject的forwardInvocation:方法實現只是簡單呼叫了doesNotRecognizeSelector:方法,它不會轉發任何訊息。這樣,如果不在以上所述的三個步驟中處理未知訊息,則會引發一個異常。

從某種意義上來講,forwardInvocation:就像一個未知訊息的分發中心,將這些未知的訊息轉發給其它物件。或者也可以像一個運輸站一樣將所有未知訊息都傳送給同一個接收物件。這取決於具體的實現。

4、訊息轉發與多重繼承

回過頭來看第二和第三步,通過這兩個方法我們可以允許一個物件與其它物件建立關係,以處理某些未知訊息,而表面上看仍然是該物件在處理訊息。通過這種關係,我們可以模擬“多重繼承”的某些特性,讓物件可以“繼承”其它物件的特性來處理一些事情。不過,這兩者間有一個重要的區別:多重繼承將不同的功能整合到一個物件中,它會讓物件變得過大,涉及的東西過多;而訊息轉發將功能分解到獨立的小的物件中,並通過某種方式將這些物件連線起來,並做相應的訊息轉發。

不過訊息轉發雖然類似於繼承,但NSObject的一些方法還是能區分兩者。如respondsToSelector:和isKindOfClass:只能用於繼承體系,而不能用於轉發鏈。便如果我們想讓這種訊息轉發看起來像是繼承,則可以重寫這些方法,如以下程式碼所示:

- (BOOL)respondsToSelector:(SEL)aSelector   {
       if ( [super respondsToSelector:aSelector] )         
       		return YES;     
       else {          
       		/* Here, test whether the aSelector message can     *            
      		 * be forwarded to another object and whether that  *            
      		* object can respond to it. Return YES if it can.  */      
       }
      return NO;  
}
複製程式碼

4.6 Method Swizzling

4.6.1 概述

Objective-C 中的 Method Swizzling 是一項異常強大的技術,它可以允許我們動態地替換方法的實現,實現 Hook 功能,是一種比子類化更加靈活的“重寫”方法的方式。

4.6.2 原理

Objective-C同一個類(及類的繼承體系)中,不能存在2個同名的方法,即使引數型別不同也不行。所以下面兩個方法在 runtime 看來就是同一個方法:

- (void)viewWillAppear:(BOOL)animated;
- (void)viewWillAppear:(NSString *)string;
複製程式碼

而下面兩個方法卻是可以共存的

- (void)viewWillAppear:(BOOL)animated;
+ (void)viewWillAppear:(BOOL)animated;
複製程式碼

因為例項方法和類方法是分別儲存在類物件和元類物件中的。

原則上,方法的名稱 name 和方法的實現 imp 是一一對應的,而 Method Swizzling 的原理就是動態地改變它們的對應關係,以達到替換方法實現的目的

原有方法和實現的對應關係如下圖:

原有方法和實現的對應關係

通過runtime可實現:

runtime 調整的對應關係

在OC語言的runtime特性中,呼叫一個物件的方法就是給這個物件傳送訊息。是通過查詢接收訊息物件的方法列表,從方法列表中查詢對應的SEL,這個SEL對應著一個IMP(一個IMP可以對應多個SEL),通過這個IMP找到對應的方法呼叫。

在每個類中都有一個Dispatch Table,這個Dispatch Table本質是將類中的SEL和IMP(可以理解為函式指標)進行對應。而我們的Method Swizzling就是對這個table進行了操作,讓SEL對應另一個IMP。

4.6.3 使用注意

  • Swizzling應該總在+load中執行:Objective-C在執行時會自動呼叫類的兩個方法+load和+initialize。+load會在類初始載入時呼叫,和+initialize比較+load能保證在類的初始化過程中被載入
  • Swizzling應該總是在dispatch_once中執行:swizzling會改變全域性狀態,所以在執行時採取一些預防措施,使用dispatch_once就能夠確保程式碼不管有多少執行緒都只被執行一次。這將成為method swizzling的最佳實踐。
  • Selector,Method和Implementation:這幾個之間關係可以這樣理解,一個類維護一個執行時可接收的訊息分發表,分發表中每個入口是一個Method,其中key是一個特定的名稱,及SEL,與其對應的實現是IMP即指向底層C函式的指標。

4.6.4 應用例項

1. 替換方法實現
//  ViewController+Method.m
//  RuntimeDemo
//
//  Created by 邱學偉 on 2018/5/4.
//  Copyright © 2018年 邱學偉. All rights reserved.
//

#import "ViewController+Method.h"
#import <objc/runtime.h>

@implementation ViewController (Method)
+ (void)load {
    [super load];
    [self exchangeMethod];
}

/// runtime 交換方法
+ (void)exchangeMethod {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originSel = @selector(viewWillAppear:);
        SEL swizzledSel = @selector(xw_viewWillAppear:);
        
        Method originMethod = class_getInstanceMethod(class, originSel);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSel);
        
        //先嚐試給源方法新增實現,這裡是為了避免源方法沒有實現的情況
        BOOL isAddMethod = class_addMethod(class, originSel, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (isAddMethod) {
            class_replaceMethod(class, swizzledSel, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
        }else{
            method_exchangeImplementations(originMethod, swizzledMethod);
        }
    });
}
- (void)xw_viewWillAppear:(BOOL)animation {
    [self xw_viewWillAppear:animation];
    NSLog(@"xw_viewWillAppear - %@",self);
}
@end

複製程式碼
2、Method Swizzling類簇

在我們專案開發過程中,經常因為NSArray陣列越界或者NSDictionary的key或者value值為nil等問題導致的崩潰,我們可以嘗試使用前面知識對NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等類進行Method Swizzling,但是結果發現Method Swizzling根本就不起作用,到底為什麼呢?

這是因為Method Swizzling對NSArray這些的類簇是不起作用的。因為這些類簇類,其實是一種抽象工廠的設計模式。抽象工廠內部有很多其它繼承自當前類的子類,抽象工廠類會根據不同情況,建立不同的抽象物件來進行使用。例如我們呼叫NSArray的objectAtIndex:方法,這個類會在方法內部判斷,內部建立不同抽象類進行操作。

所以也就是我們對NSArray類進行操作其實只是對父類進行了操作,在NSArray內部會建立其他子類來執行操作,真正執行操作的並不是NSArray自身,所以我們應該對其“真身”進行操作。

下面我們實現了防止NSArray因為呼叫objectAtIndex:方法,取下標時陣列越界導致的崩潰:

#import "NSArray+ MyArray.h"
#import "objc/runtime.h"
@implementation NSArray MyArray)
+ (void)load {
    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(my_objectAtIndex:));
    method_exchangeImplementations(fromMethod, toMethod);
}

- (id)my_objectAtIndex:(NSUInteger)index {
    if (self.count-1 < index) {
        // 這裡做一下異常處理,不然都不知道出錯了。
        @try {
            return [self my_objectAtIndex:index];
        }
        @catch (NSException *exception) {
            // 在崩潰後會列印崩潰資訊,方便我們除錯。
            NSLog(@"---------- %s Crash Because Method %s  ----------\n", class_getName(self.class), __func__);
            NSLog(@"%@", [exception callStackSymbols]);
            return nil;
    }
        @finally {}
    } else {
        return [self my_objectAtIndex:index];
    }
}
@end
複製程式碼

常見類簇真身:

類簇

5. Protocol 和 Category

5.1 Category

指向分類的結構體的指標

typedef struct objc_category *Category;

struct objc_category {
     char *category_name OBJC2_UNAVAILABLE; // 分類名
     char *class_name OBJC2_UNAVAILABLE; // 分類所屬的類名
     struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 例項方法列表
     struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 類方法列表,Meta Class方法列表的子集
     struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分類所實現的協議列表
}
複製程式碼

示例程式碼

//
//  main.m
//  RuntimeDemo
//
//  Created by 邱學偉 on 2018/4/27.
//  Copyright © 2018年 邱學偉. All rights reserved.
//

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

@interface NSObject (Fuck)
+ (void)foo;
@end

@implementation NSObject (Fuck)
- (void)foo {
    NSLog(@"我是Foo %@",[self class]);
}
@end

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        [NSObject foo];
        [[NSObject new] foo];
        
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

複製程式碼

輸出:

2018-05-04 16:23:26.643100+0800 RuntimeDemo[48558:2377362] 我是Foo NSObject
2018-05-04 16:23:26.644354+0800 RuntimeDemo[48558:2377362] 我是Foo NSObject
複製程式碼

objc runtime載入後NSObject的Sark Category被載入,標頭檔案+(void)foo沒有IMP,只會出現一個warning。被加到Class的Method list裡的方法只有-(void)foo,Meta Class的方法列表裡沒有。

執行[NSObject foo]時,會在Meta Class的Method list裡找,找不著就繼續往super class裡找,NSObject Meta Clas的super class是NSObject本身,這時在NSObject的Method list裡就有foo這個方法了,能夠正常輸出。

執行[[NSObject new] foo]就簡單的多了,[NSObject new]生成一個例項,例項的Method list是有foo方法的,於是正常輸出。

如果換做其他類就會報錯了

5.2 Protocol

Protocol其實就是一個物件結構體

typedef struct objc_object Protocol;
複製程式碼

操作函式:

// 返回指定的協議
Protocol * objc_getProtocol ( const char *name );
// 獲取執行時所知道的所有協議的陣列
Protocol ** objc_copyProtocolList ( unsigned int *outCount );
// 建立新的協議例項
Protocol * objc_allocateProtocol ( const char *name );
// 在執行時中註冊新建立的協議
void objc_registerProtocol ( Protocol *proto ); //建立一個新協議後必須使用這個進行註冊這個新協議,但是註冊後不能夠再修改和新增新方法。
// 為協議新增方法
void protocol_addMethodDescription ( Protocol *proto, SEL name, const char *types, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 新增一個已註冊的協議到協議中
void protocol_addProtocol ( Protocol *proto, Protocol *addition );
// 為協議新增屬性
void protocol_addProperty ( Protocol *proto, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 返回協議名
const char * protocol_getName ( Protocol *p );
// 測試兩個協議是否相等
BOOL protocol_isEqual ( Protocol *proto, Protocol *other );
// 獲取協議中指定條件的方法的方法描述陣列
struct objc_method_description * protocol_copyMethodDescriptionList ( Protocol *p, BOOL isRequiredMethod, BOOL isInstanceMethod, unsigned int *outCount );
// 獲取協議中指定方法的方法描述
struct objc_method_description protocol_getMethodDescription ( Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 獲取協議中的屬性列表
objc_property_t * protocol_copyPropertyList ( Protocol *proto, unsigned int *outCount );
// 獲取協議的指定屬性
objc_property_t protocol_getProperty ( Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 獲取協議採用的協議
Protocol ** protocol_copyProtocolList ( Protocol *proto, unsigned int *outCount );
// 檢視協議是否採用了另一個協議
BOOL protocol_conformsToProtocol ( Protocol *proto, Protocol *other );

複製程式碼

6. 補充

6.1 Super

在Objective-C中,如果我們需要在類的方法中呼叫父類的方法時,通常都會用到super,如下所示:

@interface MyViewController: UIViewController
@end
@implementation MyViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // do something
    ...
}
@end
複製程式碼

super與self不同。self是類的一個隱藏引數,每個方法的實現的第一個引數即為self。而super並不是隱藏引數,它實際上只是一個”編譯器標示符”,它負責告訴編譯器,當呼叫viewDidLoad方法時,去呼叫父類的方法,而不是本類中的方法。而它實際上與self指向的是相同的訊息接收者。為了理解這一點,我們先來看看super的定義

struct objc_super { id receiver; Class superClass; };
複製程式碼

這個結構體有兩個成員:

(1)receiver:即訊息的實際接收者

(2)superClass:指標當前類的父類

當我們使用super來接收訊息時,編譯器會生成一個objc_super結構體。就上面的例子而言,這個結構體的receiver就是MyViewController物件,與self相同;superClass指向MyViewController的父類UIViewController。

接下來,傳送訊息時,不是呼叫objc_msgSend函式,而是呼叫objc_msgSendSuper函式,其宣告如下:

id objc_msgSendSuper ( struct objc_super *super, SEL op, ... );
複製程式碼

該函式第一個引數即為前面生成的objc_super結構體,第二個引數是方法的selector。該函式實際的操作是:從objc_super結構體指向的superClass的方法列表開始查詢viewDidLoad的selector,找到後以objc->receiver去呼叫這個selector,而此時的操作就是如下方式了:

objc_msgSend(objc_super->receiver, @selector(viewDidLoad))
複製程式碼

由於objc_super->receiver就是self本身,所以該方法實際與下面這個呼叫是相同的

objc_msgSend(self, @selector(viewDidLoad))
複製程式碼

如下:

+ (void)load {
    [super load];
    NSLog(@"self class: %@", self.class);
    NSLog(@"super class: %@", super.class);
}
複製程式碼

輸出:

2018-05-04 15:19:45.264902+0800 RuntimeDemo[47032:2208798] self class: ViewController
2018-05-04 15:19:45.265792+0800 RuntimeDemo[47032:2208798] super class: ViewController

複製程式碼

6.2 庫相關操作

庫相關的操作主要是用於獲取由系統提供的庫相關的資訊,主要包含以下函式:

// 獲取所有載入的Objective-C框架和動態庫的名稱
const char ** objc_copyImageNames ( unsigned int *outCount );

// 獲取指定類所在動態庫
const char * class_getImageName ( Class cls );

// 獲取指定庫或框架中所有類的類名
const char ** objc_copyClassNamesForImage ( const char *image, unsigned int *outCount );
複製程式碼

通過這幾個函式,我們可以瞭解到某個類所有的庫,以及某個庫中包含哪些類。如下程式碼所示:

- (void)testImage {
    NSLog(@"獲取指定類所在動態庫");
    NSLog(@"UIView's Framework: %s", class_getImageName(NSClassFromString(@"UIView")));
    NSLog(@"獲取指定庫或框架中所有類的類名");
    unsigned int outCount;
    const char ** classes = objc_copyClassNamesForImage(class_getImageName(NSClassFromString(@"UIView")), &outCount);
    for (int i = 0; i < outCount; i++) {
        NSLog(@"class name: %s", classes[i]);
    }
}
複製程式碼

輸出:

2018-05-04 15:30:51.342342+0800 RuntimeDemo[47333:2253385] 獲取指定類所在動態庫
2018-05-04 15:30:51.342499+0800 RuntimeDemo[47333:2253385] UIView's Framework: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/UIKit.framework/UIKit
2018-05-04 15:30:51.342620+0800 RuntimeDemo[47333:2253385] 獲取指定庫或框架中所有類的類名
2018-05-04 15:30:51.343164+0800 RuntimeDemo[47333:2253385] class name: UIGestureKeyboardIntroduction
2018-05-04 15:30:51.343269+0800 RuntimeDemo[47333:2253385] class name: _UIPreviewPresentationPlatterView
2018-05-04 15:30:51.343364+0800 RuntimeDemo[47333:2253385] class name: UIKeyboardUISettings
2018-05-04 15:30:51.343456+0800 RuntimeDemo[47333:2253385] class name: _UIFocusScrollManager
2018-05-04 15:30:51.343550+0800 RuntimeDemo[47333:2253385] class name: _UIPickerViewTopFrame
2018-05-04 15:30:51.343655+0800 RuntimeDemo[47333:2253385] class name: _UIOnePartImageView
2018-05-04 15:30:51.343749+0800 RuntimeDemo[47333:2253385] class name: _UIPickerViewSelectionBar
。。。。。。。。。。。。。。。。。。。。。
複製程式碼

6.3 塊操作

我們都知道block給我們帶到極大的方便,蘋果也不斷提供一些使用block的新的API。同時,蘋果在runtime中也提供了一些函式來支援針對block的操作,這些函式包括:

// 建立一個指標函式的指標,該函式呼叫時會呼叫特定的block
IMP imp_implementationWithBlock ( id block );

// 返回與IMP(使用imp_implementationWithBlock建立的)相關的block
id imp_getBlock ( IMP anImp );

// 解除block與IMP(使用imp_implementationWithBlock建立的)的關聯關係,並釋放block的拷貝
BOOL imp_removeBlock ( IMP anImp );
複製程式碼

imp_implementationWithBlock函式:引數block的簽名必須是method_return_type ^(id self, method_args …)形式的。該方法能讓我們使用block作為IMP。如下程式碼所示:

- (void)testBlock {
    IMP imp = imp_implementationWithBlock(^(id obj, NSString *str) {
        NSLog(@"testBlock - %@",str);
    });
    class_addMethod(self.class, @selector(testBlock:), imp, "v@:@");
    [self performSelector:@selector(testBlock:) withObject:@"邱學偉!"];
}
複製程式碼

輸出:

2018-05-04 15:41:47.221228+0800 RuntimeDemo[47587:2282146] testBlock - 邱學偉!
複製程式碼

6.4 弱引用操作

操作函式:

// 載入弱引用指標引用的物件並返回
id objc_loadWeak ( id *location );

// 儲存__weak變數的新值
id objc_storeWeak ( id *location, id obj );
複製程式碼

objc_loadWeak函式:該函式載入一個弱指標引用的物件,並在對其做retain和autoreleasing操作後返回它。這樣,物件就可以在呼叫者使用它時保持足夠長的生命週期。該函式典型的用法是在任何有使用__weak變數的表示式中使用。

objc_storeWeak函式:該函式的典型用法是用於__weak變數做為賦值物件時。

相關文章