什么是runtime?
runtime 是OC底層的一套C語言庫 (<objc/runtime.h>),基本上是用C和匯編寫的,這個庫使得OC具有了面向對象的能力。runtime做的事前就是加載類的信息,進行方法的分發和轉發之類的。
類與對象的本質
class
Objective-C類是由Class類型來表示的,它實際上是一個指向objc_class結構體的指針。定義如下:
typedef struct objc_class *Class;
查看objc/runtime.h中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; // 類的版本信息,默認為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; // 方法緩存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 協議鏈表
#endif
} OBJC2_UNAVAILABLE;
在上述objc_class結構體定義中,我們主要關注以下幾個字段:
- isa: 在OC中,類本身也是一個對象,類的isa指針指向metaClass(元類),而實例對象的isa指針指向類。
- super_class: isa用于自省確定所屬類,super_class確定繼承關系 。所以super_class指向該類的父類,如果該類已經是最頂層的根類(如NSObject或NSProxy),則super_class為NULL。
- cache: 用于緩存最近使用的方法。一個接收者對象接收到一個消息時,它會根據isa指針去查找能夠響應這個消息的對象。在實際使用中,這個對象只有一部分方法是常用的,很多方法其實很少用或者根本用不上。這種情況下,如果每次消息來時,我們都是methodLists中遍歷一遍,性能勢必很差。這時,cache就派上用場了。在我們每次調用過一個方法后,這個方法就會被緩存到cache列表中,下次調用的時候runtime就會優先去cache中查找,如果cache沒有,才去methodLists中查找方法。這樣,對于那些經常用到的方法的調用,但提高了調用的效率。
- version: 使用這個字段來提供類的版本信息。這對于對象的序列化非常有用,它可以讓我們識別出不同類定義版本中實例變量布局的改變。
類的實例的結構體 objc_object
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;
這個結構體只有一個字段,即指向其類的isa指針。向一個OC實例對象發送消息時,runtime庫根據這個實例對象的isa指針找到該實例對象所屬的類,runtime庫會在類的方法列表及父類的方法列表中去尋找與消息對應的selector指向的方法,找到后即運行這個方法。
當創建某個類的實例對象時,分配的內存包含一個objc_object結構體,然后是該類到父類直到根類的成員變量數據。NSObject類的alloc和allocWithZone:方法使用函數class_createInstance來創建objc_object結構體。
另外,常見的id類型,它是一個objc_object結構類型的指針。它的存在可以讓我們實現類似于C++中泛型的一些操作。該類型的對象可以轉換為任何一種對象,有點類似于C語言中void *指針類型的作用。
元類(Meta Class)
在上面我們提到,所有的類自身也是一個對象,我們可以向這個對象發送消息(即調用類方法)。如:
NSArray *array = [NSArray array];
+array消息發送給了NSArray類,而這個NSArray也是一個對象,既然是對象,那么它也有一個objc_object結構體,該結構體內包含一個isa指針,指向該對象所屬的類。那么這些就有一個問題了,NSArray對象本身就是一個類,這里的isa指針指向什么呢?不能指向他自己吧?為了調用+array方法,這個類的isa指針必須指向一個包含這些類方法的一個objc_class結構體。這就引出了meta-class(元類)的概念。
當我們向一個對象發送消息時,runtime會在這個對象所屬的這個類的方法列表中查找方法;而向一個類發送消息時,會在這個類的meta-class的方法列表中查找。
meta-class之所以重要,是因為它存儲著一個類的所有類方法。每個類都會有一個單獨的meta-class,因為每個類的類方法基本不可能完全相同。
類與對象相關的操作函數
runtime提供了大量的函數來操作類與對象。類的操作方法大部分是以class為前綴的,而對象的操作方法大部分是以objc或object_為前綴。下面我們將根據這些方法的用途來分類討論這些方法的使用。
類相關操作函數
觀察objc_class的定義,runtime提供的操作類的方法主要就是針對這個結構體中的各個字段的。
(1) 類名(name)
// 獲取類的類名
const char * class_getName ( Class cls );
對于class_getName函數,如果傳入的cls為Nil,則返回一個空字符串。
(2) 父類(super_class)和元類(meta-class)
// 獲取類的父類
Class class_getSuperclass ( Class cls );
// 判斷給定的Class是否是一個元類
BOOL class_isMetaClass ( Class cls );
class_getSuperclass函數,當cls為Nil或者cls為根類時,返回Nil。不過通常我們可以使用NSObject類的superclass方法來達到同樣的目的。
class_isMetaClass函數,如果是cls是元類,則返回YES;如果否或者傳入的cls為Nil,則返回NO。
(3) 實例變量大小(instance_size)
// 獲取實例大小
size_t class_getInstanceSize ( Class cls );
.
(4) 成員變量(ivars)及屬性
在objc_class中,所有的成員變量、屬性的信息是放在鏈表ivars中的。ivars是一個數組,數組中每個元素是指向Ivar(變量信息)的指針。runtime提供了豐富的函數來操作這一字段。
// 獲取類中指定名稱實例成員變量的信息
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 );
// 獲取整個成員變量列表
Ivar * class_copyIvarList ( Class cls, unsigned int *outCount );
class_getInstanceVariable函數,它返回一個指向包含name指定的成員變量信息的objc_ivar結構體的指針(Ivar)。
class_getClassVariable函數,目前沒有找到關于Objective-C中類變量的信息,一般認為Objective-C不支持類變量。注意,返回的列表不包含父類的成員變量和屬性。
Objective-C不支持往已存在的類中添加實例變量,因此不管是系統庫提供的提供的類,還是我們自定義的類,都無法動態添加成員變量。但如果我們通過運行時來創建一個類的話,又應該如何給它添加成員變量呢?這時我們就可以使用class_addIvar函數了。不過需要注意的是,這個方法只能在objc_allocateClassPair函數與objc_registerClassPair之間調用。另外,這個類也不能是元類。成員變量的按字節最小對齊量是1<<alignment。這取決于ivar的類型和機器的架構。如果變量的類型是指針類型,則傳遞log2(sizeof(pointer_type))。
class_copyIvarList函數,它返回一個指向成員變量信息的數組,數組中每個元素是指向該成員變量信息的objc_ivar結構體的指針。這個數組不包含在父類中聲明的變量。outCount指針返回數組的大小。需要注意的是,我們必須使用free()來釋放這個數組。
// 獲取指定的屬性
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 );
(5) 方法(methodLists)
方法操作主要有以下函數:
// 添加方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );
// 獲取實例方法
Method class_getInstanceMethod ( Class cls, SEL name );
// 獲取類方法
Method class_getClassMethod ( Class cls, SEL name );
// 獲取所有方法的數組
Method * class_copyMethodList ( Class cls, unsigned int *outCount );
// 替代方法的實現
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );
// 返回方法的具體實現
IMP class_getMethodImplementation ( Class cls, SEL name );
IMP class_getMethodImplementation_stret ( Class cls, SEL name );
// 類實例是否響應指定的selector
BOOL class_respondsToSelector ( Class cls, SEL sel );
我們可以為類動態添加方法,不管這個類是否已存在。
class_getInstanceMethod class_getClassMethod函數,與class_copyMethodList不同的是,這兩個函數都會去搜索父類的實現。
class_copyMethodList函數,返回包含所有實例方法的數組,如果需要獲取類方法,則可以使用class_copyMethodList(object_getClass(cls), &count)(一個類的實例方法是定義在元類里面)。該列表不包含父類實現的方法。outCount參數返回方法的個數。在獲取到列表后,我們需要使用free()方法來釋放它。
class_replaceMethod函數,該函數的行為可以分為兩種:如果類中不存在name指定的方法,則類似于class_addMethod函數一樣會添加方法;如果類中已存在name指定的方法,則類似于method_setImplementation一樣替代原方法的實現。
class_getMethodImplementation函數,該函數在向類實例發送消息時會被調用,并返回一個指向方法實現函數的指針。這個函數會比method_getImplementation(class_getInstanceMethod(cls, name))更快。返回的函數指針可能是一個指向runtime內部的函數,而不一定是方法的實際實現。例如,如果類實例無法響應selector,則返回的函數指針將是運行時消息轉發機制的一部分。
class_respondsToSelector函數,我們通常使用NSObject類的respondsToSelector:或instancesRespondToSelector:方法來達到相同目的。
(6) 協議(objc_protocol_list)
協議相關的操作包含以下函數:
// 添加協議
BOOL class_addProtocol ( Class cls, Protocol *protocol );
// 返回類是否實現指定的協議
BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );
// 返回類實現的協議列表
Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount );
class_conformsToProtocol函數可以使用NSObject類的conformsToProtocol:方法來替代。
class_copyProtocolList函數返回的是一個數組,在使用后我們需要使用free()手動釋放。
實例相關的操作函數
實例操作函數主要是針對我們創建的實例對象的一系列操作函數,我們可以使用這組函數來從實例對象中獲取我們想要的一些信息,如實例對象中變量的值。這組函數可以分為三小類:
(1) 對實例對象進行操作的函數
// 返回指定對象的一份拷貝
id object_copy ( id obj, size_t size );
// 釋放指定對象占用的內存
id object_dispose ( id obj );
有這樣一種場景,假設我們有類A和類B,且類B是類A的子類。類B通過添加一些額外的屬性來擴展類A。現在我們創建了一個A類的實例對象,并希望在運行時將這個對象轉換為B類的實例對象,這樣可以添加數據到B類的屬性中。這種情況下,我們沒有辦法直接轉換,因為B類的實例會比A類的實例更大,沒有足夠的空間來放置對象。此時,我們就要以使用以上幾個函數來處理這種情況,如下代碼所示:
NSObject *a = [[NSObject alloc] init];
id newB = object_copy(a, class_getInstanceSize(MyClass.class));
object_setClass(newB, MyClass.class);
object_dispose(a);
(2) 操作對象的實例變量
// 修改類實例的實例變量的值
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 );
如果實例變量的Ivar已經知道,那么調用object_getIvar會比object_getInstanceVariable函數快,相同情況下,object_setIvar也比object_setInstanceVariable快。
(3) 操作對象的類
// 返回給定對象的類名
const char * object_getClassName ( id obj );
// 返回對象的類
Class object_getClass ( id obj );
// 設置對象的類
Class object_setClass ( id obj, Class cls );