iOS 之 runtime 詳解 api(一)
什麼是runtime?
runtime
在iOS中是“執行時”的含義,是一套用c語言寫的api,很多人會用但是也僅僅用過最最常用的幾個函式,這次,我將詳細的帶著大家探索下 runtime
的API,runtime裡面最主要的兩個檔案 <objc/runtime.h>
和 <objc/message.h>
,這一章就說下 <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_getClass
和 objc_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 = %disClass_class = %disClass_metaClass = %d",isClass_objc,isClass_class,isClass_metaClass); }
執行結果:
isClass_objc = 0isClass_class = 1isClass_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
是新類所繼承的類,如果為 nil
, superclass
就預設為根類,也就是 NSObject
, extraBytes
是在類和元類物件的末尾為索引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
相當於
strong
和
nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC
相當於
copy
和
nonatomic
OBJC_ASSOCIATION_RETAIN
相當於
strong
和
atomic
OBJC_ASSOCIATION_COPY
相當於
copy
和
atomic
關於分類的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
檔案設定屬性並沒有用,呼叫的時候會發生閃退,這是因為系統並沒有自動為屬性生成 Set
和 Get
方法,所以,我們用上面三個方法來手動關聯物件。
我們建立一個 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
還有很多其他的用法我們下一篇見。