iOS之runtime詳解api(一)

hoping_sir發表於2019-03-02

什麼是runtime?

runtime在iOS中是“執行時”的含義,是一套用c語言寫的api,很多人會用但是也僅僅用過最最常用的幾個函式,這次,我將詳細的帶著大家探索下runtime的API,這一章就說下<objc/runtime.h>這個檔案裡的API,並且我會把不適用於ARC和不支援64位的API剔除掉。

1.Class相關

首先,我們先看一個簡單的函式:

const char * _Nonnull
class_getName(Class _Nullable cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼

這個函式是通過傳入Class型別的cls來得到Class的名字。那我們測試下這個函式:

-(void)getName {
    const char* name = class_getName([Person class]);
    NSLog(@"name = %s",name);
}
複製程式碼

其中[Person class]OC中獲得Class的方法,當然,你也可以用runtime裡面的objc_getClass等函式,後面我也會講到。 執行結果:

name = Person
複製程式碼

我們可以看到列印出來的結果就是類的名字。 上面既然用到了[Person class],那我們就看下在runtime[Person class]的替代函式,都是通過名字來獲得Class

Class _Nullable
objc_getClass(const char * _Nonnull name)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

Class _Nullable
objc_lookUpClass(const char * _Nonnull name)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

Class _Nonnull
objc_getRequiredClass(const char * _Nonnull name)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
複製程式碼

那這三個有什麼區別,從結論上講,objc_getClassobjc_lookUpClass的效果是一致的,在最新的原始碼裡面,這兩個方法呼叫的底層也是一致的,當你要找的類不存在的話,就返回nil,而objc_getRequiredClass裡你要找的類不存在的話,就會崩潰。下面我們來測試下,我們建立一個Person類。

-(void)getClass {
    const char* name = "Person1";
    const char* name_exist = "Person";
    Class class1_exist = objc_getClass(name_exist );
    NSLog(@"class1_exist = %@",class1_exist);

    Class class1 = objc_getClass(name);
    NSLog(@"class1 = %@",class1);
    Class class2_exist = objc_lookUpClass(name_exist );
    NSLog(@"class2_exist = %@",class2_exist);
    Class class2 = objc_lookUpClass(name );
    NSLog(@"class2 = %@",class2);

    Class class3_exist = objc_getRequiredClass(name_exist );
    NSLog(@"class3_exist = %@",class3_exist);
    Class class3 = objc_getRequiredClass(name );
    NSLog(@"class3 = %@",class3);
}
複製程式碼

執行結果:

2019-02-21 16:58:39.173892+0800 Runtime-Demo[91840:2890084] class1_exist = Person
2019-02-21 16:58:39.173939+0800 Runtime-Demo[91840:2890084] class1 = (null)
2019-02-21 16:58:39.173951+0800 Runtime-Demo[91840:2890084] class2_exist = Person
2019-02-21 16:58:39.173960+0800 Runtime-Demo[91840:2890084] class2 = (null)
2019-02-21 16:58:39.173969+0800 Runtime-Demo[91840:2890084] class3_exist = Person
objc[91840]: link error: class 'Person1' not found.
複製程式碼

最後也確實崩潰了,所以大家使用objc_getRequiredClass這個函式時候要慎重小心。 除了用名字獲得類物件以外,還可以用例項物件來獲取:

Class _Nullable
object_getClass(id _Nullable obj)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼

我們測試下:

-(void)getClassWithObjc {
    Person* person = [Person new];
    Class class = object_getClass(person);
    NSLog(@"class = %@",class);
}
複製程式碼

執行結果:

class = Person
複製程式碼

完全沒問題。 Class不僅可以代表類物件,也可以代表元類物件,下面這個函式就是通過名字獲取元類物件。

Class _Nullable
objc_getMetaClass(const char * _Nonnull name)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
複製程式碼

如果你讀過原始碼的話,你就會清楚元類物件儲存的是類方法,類物件儲存的是例項方法,在後面講到Method相關的API的時候,我們在具體講他們之間的區別。

講到元類物件,我們還要關注下這個函式,

BOOL
class_isMetaClass(Class _Nullable cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼

這個函式是用來判斷是否是元類物件。

-(void)isMetaClass {
    const char* name = "Person";
    BOOL isMetaClass1 = class_isMetaClass(objc_getMetaClass(name ));
    BOOL isMetaClass2 = class_isMetaClass(objc_getClass(name));
    NSLog(@"objc_getMetaClass = %d,objc_getClass = %d",isMetaClass1,isMetaClass2);
}
複製程式碼

執行結果:

objc_getMetaClass = 1,objc_getClass = 0
複製程式碼

我們可以看到objc_getMetaClass生成才是元類物件,objc_getClass生成的只是類物件。 那麼有沒有函式區分類(元類)物件和例項物件呢?當然有:

BOOL
object_isClass(id _Nullable obj)
OBJC_AVAILABLE(10.10, 8.0, 9.0, 1.0, 2.0);
複製程式碼

這個方法只要是類物件或者元類物件都會返回YES:

-(void)isClass {
    Person* person = [Person new];
    BOOL isClass_objc = object_isClass(person);
    BOOL isClass_class = object_isClass(objc_getClass("Person"));
    BOOL isClass_metaClass = object_isClass(objc_getMetaClass("Person"));

    NSLog(@"isClass_objc = %d  isClass_class = %d  isClass_metaClass = %d",isClass_objc,isClass_class,isClass_metaClass);
}
複製程式碼

執行結果:

isClass_objc = 0  isClass_class = 1  isClass_metaClass = 1
複製程式碼

當然也可以獲得父類物件。

Class _Nullable
class_getSuperclass(Class _Nullable cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼

我們新建一個繼承Person的類Student,然後我們通過Student類來獲得Person類。

-(void)getSuperclass {
    Class class = class_getSuperclass(objc_getClass("Student"));
    NSLog(@"class = %@",class);
}
複製程式碼

執行結果:

class = Person
複製程式碼

Student的父類確實是Person

我們知道OC裡面可以強轉型別,當然,runtime裡面也有相關方法

Class _Nullable
object_setClass(id _Nullable obj, Class _Nonnull cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼

這個方法的意思是給一個例項物件設定新的類,返回舊的類

-(void)setClass {
    Student* student = [Student new];
    Class class = object_setClass(student, objc_getClass("Person"));
    NSLog(@"oldClass = %@",class);
    NSLog(@"newStudent = %@",student);
}
複製程式碼

執行結果:

2019-02-21 17:38:17.388341+0800 Runtime-Demo[92493:2904857] oldClass = Student
2019-02-21 17:38:17.388413+0800 Runtime-Demo[92493:2904857] newStudent = <Person: 0x282dd8b50>
複製程式碼

我們可以看出開始的時候student的類是Student,用了object_setClass後就是Person類了。 runtime的動態性還可以動態新增類,下面四個函式分別表示為一個類分配記憶體,註冊一個類,複製一個類,銷燬一個類

Class _Nullable
objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name,
                       size_t extraBytes)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼

建立一個新類,superclass是新類所繼承的類,如果為nilsuperclass就預設為根類,也就是NSObjectextraBytes是在類和元類物件的末尾為索引ivars分配的位元組數。這一般是0,name是新類的名字。

void
objc_registerClassPair(Class _Nonnull cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼

註冊類,如果這個類objc_allocateClassPair好了,就必須objc_registerClassPair才能使用。

Class _Nonnull
objc_duplicateClass(Class _Nonnull original, const char * _Nonnull name,
                    size_t extraBytes)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼

這個方法在系統KVO的底層用過,系統不推薦我們自己用。

 void
objc_disposeClassPair(Class _Nonnull cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼

objc_disposeClassPair只能銷燬通過objc_allocateClassPair建立的類。 我們寫個demo來測試這些方法,objc_duplicateClass官方不建議使用,那麼我們就不測試這函式。

-(void)classLifeCycle {
    Class class = objc_allocateClassPair(objc_getClass("Person"), "Teacher" , 0);
    const char* name = class_getName(class);
    Class allocateClass = objc_getClass(name);
    NSLog(@"allocateClass = %@",allocateClass);
    
    objc_registerClassPair(class);
    Class registerClass = objc_getClass(name);
    NSLog(@"registerClass = %@",registerClass);
    
    objc_disposeClassPair(class);
    Class disposeClass = objc_getClass(name);
    NSLog(@"disposeClass = %@",disposeClass);
}
複製程式碼

執行結果:

2019-02-22 09:37:52.705001+0800 Runtime-Demo[99587:3143177] allocateClass = (null)
2019-02-22 09:37:52.705049+0800 Runtime-Demo[99587:3143177] registerClass = Teacher
2019-02-22 09:37:52.705071+0800 Runtime-Demo[99587:3143177] disposeClass = (null)
複製程式碼

我們可以知道如果僅僅只是objc_allocateClassPair的話,你是找不到這個類的,必須再objc_registerClassPair才可以找到,objc_disposeClassPair則是把類銷燬掉,所以再實際開發中,如果我們不再使用自建類的時候,就要及時銷燬,節省記憶體。

下面兩個函式是關於整個工程的類列表的函式:

Class _Nonnull * _Nullable
objc_copyClassList(unsigned int * _Nullable outCount)
OBJC_AVAILABLE(10.7, 3.1, 9.0, 1.0, 2.0);
複製程式碼

這個函式是獲得所有註冊類的列表,我們試用下:

-(void)copyClassList {
    unsigned int outCount;
    Class *classes = objc_copyClassList(&outCount);
    NSLog(@"outCount = %d",outCount);
    for (int i = 0; i < outCount; i++) {
        NSLog(@"%s", class_getName(classes[i]));
    }
    free(classes);
}
複製程式碼

執行結果:

2019-02-22 09:52:12.218871+0800 Runtime-Demo[99840:3149922] outCount = 15765
2019-02-22 09:52:12.218939+0800 Runtime-Demo[99840:3149922] _CNZombie_
2019-02-22 09:52:12.218953+0800 Runtime-Demo[99840:3149922] JSExport
2019-02-22 09:52:12.218963+0800 Runtime-Demo[99840:3149922] NSLeafProxy
......
......
複製程式碼

我們看到註冊的類有15765個。 objc_getClassList也是獲取註冊類的方法.

int
objc_getClassList(Class _Nonnull * _Nullable buffer, int bufferCount)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
複製程式碼

第一個引數buffer已分配好記憶體空間的陣列指標,bufferCount是陣列的個數,如果bufferCount的數量小於實際的陣列數量,那麼buffer返回的是所有陣列集合的任意一個子類。如果buffer為NULL,那麼bufferCount為0。無論那種情況,返回結果都是當前註冊類的總數。

-(void)getClassList {
    int bufferCount = 4;
    Class* buffer = (Class*)malloc(sizeof(Class)* bufferCount);
    int count1 = objc_getClassList(buffer, bufferCount);
    for (unsigned int i =0; i <bufferCount; i++) {
        NSLog(@"name = %s",class_getName(buffer[i]));
    }
    NSLog(@"count1 = %d",count1);

    int count2 = objc_getClassList(NULL, 0);
    NSLog(@"count2 = %d",count2);
}
複製程式碼

執行結果:

2019-02-22 10:14:34.487051+0800 Runtime-Demo[354:3159864] name = _CNZombie_
2019-02-22 10:14:34.487145+0800 Runtime-Demo[354:3159864] name = JSExport
2019-02-22 10:14:34.487158+0800 Runtime-Demo[354:3159864] name = NSLeafProxy
2019-02-22 10:14:34.487173+0800 Runtime-Demo[354:3159864] name = NSProxy
2019-02-22 10:14:34.487186+0800 Runtime-Demo[354:3159864] count1 = 15765
2019-02-22 10:14:34.493662+0800 Runtime-Demo[354:3159864] count2 = 15765
複製程式碼
size_t
class_getInstanceSize(Class _Nullable cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼

返回類例項的大小。

-(void)getInstanceSize {
    size_t size = class_getInstanceSize(objc_getClass("Person"));
    NSLog(@"size = %zu",size);
}
複製程式碼

執行結果

size = 8
複製程式碼

一個沒有變數或屬性的繼承於NSObject的類佔有8個位元組。 還有個方法是:

id _Nullable
class_createInstance(Class _Nullable cls, size_t extraBytes)
OBJC_RETURNS_RETAINED
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
複製程式碼

這是一個建立例項的方法,cls是要建立的類,extraBytes是額外的位元組記憶體,用來儲存類定義中的例項變數之外的其他例項變數。在原始碼中alloc方法底層就是用的這個函式。那麼,我們用這個函式來初始化Person類:

-(void)createInstance {
    Person* person = class_createInstance(objc_getClass("Person"), 0);
    NSLog(@"%@",person);
}
複製程式碼

執行結果:

<Person: 0x60000343d2f0>
複製程式碼

確實能夠成功建立出來。 最後剩下兩個方法:

int
class_getVersion(Class _Nullable cls)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

void
class_setVersion(Class _Nullable cls, int version)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
複製程式碼

這兩個方法都和version有關,這個version在實際中我也沒發現用處,可能是在改變類的變數或者方法時給定一個標識.

-(void)version {
    int verson = class_getVersion(objc_getClass("Person"));
    NSLog(@"version = %d",verson);
    class_setVersion(objc_getClass("Person"), 10);
    int newVersion = class_getVersion(objc_getClass("Person"));
    NSLog(@"newVersion = %d",newVersion);
}
複製程式碼

執行結果

2019-02-22 11:29:57.325309+0800 Runtime-Demo[526:167322] version = 0
2019-02-22 11:29:57.325349+0800 Runtime-Demo[526:167322] newVersion = 10
複製程式碼

2.objc_category or Category相關

下面我們將使用runtime裡面最最常用的api,也就是給分類繫結物件,這裡,我們先了解下,一個列舉:

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. */
};
複製程式碼

objc_AssociationPolicy是一個列舉,裡面的列舉值分別代表要新增的屬性的修飾型別。 OBJC_ASSOCIATION_ASSIGN相當於weak OBJC_ASSOCIATION_RETAIN_NONATOMIC相當於strongnonatomic OBJC_ASSOCIATION_COPY_NONATOMIC相當於copynonatomic OBJC_ASSOCIATION_RETAIN相當於strongatomic OBJC_ASSOCIATION_COPY相當於copyatomic 關於分類的runtime函式,主要有下面3個:

void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

void
objc_removeAssociatedObjects(id _Nonnull object)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
複製程式碼

含義分別為設定關聯物件,獲得關聯物件,刪除關聯物件。 我們知道如果在分類的.h檔案設定屬性並沒有用,呼叫的時候會發生閃退,這是因為系統並沒有自動為屬性生成SetGet方法,所以,我們用上面三個方法來手動關聯物件。 我們建立一個 Person的分類Person+Actor.h,在.h檔案裡新建一個新屬性@property(nonatomic, assign)float actingSkill而不做其他任何處理,這時候,.m檔案就會有警告。

螢幕快照 2019-02-23 下午12.41.52.png
這就告訴我們需要手動實現setActingSkill:actingSkill方法: .m檔案

#import "Person+Actor.h"
#import <objc/runtime.h>
static const char* key = "actingSkill";
@implementation Person (Actor)

-(void)setActingSkill:(float)actingSkill {
    NSNumber *actingSkillObjc = [NSNumber numberWithFloat:actingSkill];
    objc_setAssociatedObject(self, key, actingSkillObjc, OBJC_ASSOCIATION_RETAIN);
}

-(float)actingSkill {
    NSNumber *actingSkillObjc = objc_getAssociatedObject(self, key);
    return [actingSkillObjc floatValue];
}

@end
複製程式碼

這時候就繫結好了。 在ViewController裡面去使用下這個屬性

-(void)testCategory {
    _person = [Person new];
    _person.actingSkill = 0.1;
    NSLog(@"actingSkill = %f",_person.actingSkill);
}
複製程式碼

執行結果:

actingSkill = 0.100000
複製程式碼

說明set和get方法都成功了。 那麼還有一個objc_removeAssociatedObjects方法還沒用,這個方法是解除繫結,為了測試這個效果,我們在ViewController裡面touchesBegan裡面去呼叫這個方法。

-(void)testCategory {
    _person = [Person new];
    _person.actingSkill = 0.1;
    NSLog(@"actingSkill = %f",_person.actingSkill);
}


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    if (_person) {
        objc_removeAssociatedObjects(_person);
        NSLog(@"actingSkill2 = %f",_person.actingSkill);
    }
}
複製程式碼

執行結果:

2019-02-23 13:21:13.090961+0800 Runtime-Demo[2964:201009] actingSkill = 0.100000
2019-02-23 13:24:24.585347+0800 Runtime-Demo[2964:201009] actingSkill2 = 0.000000
複製程式碼

之前繫結的結果被移除了。 今天我們這一篇就講到這,runtime還有很多其他的用法我們下一篇見。 對了,這個是demo,喜歡的可以點個星。

相關文章