概述
常說Objective-C是一門動態語言,那么問題來了,這個動態
表現在那些方面呢?
其實最主要的表現就是Objective-C將很多靜態語言在編譯和鏈接時做的事情放到了運行時去做,它在運行時實現了對類、方法、成員變量、屬性等信息的管理機制。
同時,運行時機制為我們開發過程提供很多便利之處,比如:
- 在運行時創建或者修改一個類;
- 在運行時修改成員變量、屬性等;
- 在運行時進行消息分發和分發綁定;
......
與之對應實現的就是Objective-C的Runtime機制。
Objective-C的Runtime目前有兩個版本:Leagcy Runtime
和Moden Runtime
。Leagcy Runtime
是最早期給32位Mac OX Apps
使用的,而Moden Runtime
是給64位Mac OX Apps
和iOS Apps
使用的。
Runtime基本是C和匯編編寫的,有一系列函數和數據結構組成的,具有公共接口的動態共享庫,可見蘋果為了動態系統的高效而作出的努力,你可以在這里下載到蘋果維護的開源代碼。
同時,GNU也有一個開源的Runtime版本,他們在努力保持一致。其頭文件都存放在/usr/include/objc
目錄下。在Objective-C Runtime Reference中,有對Runtime函數使用細節的文檔。
類與對象
類的數據結構(Class)
類的數據結構可以在objc/runtime.h
源碼中找到,如下所示:
struct objc_class {
//isa指針指向Class
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE; // 父類
const char * _Nonnull name OBJC2_UNAVAILABLE; // 類名
long version OBJC2_UNAVAILABLE; // 類的版本信息,默認為0
long info OBJC2_UNAVAILABLE; // 類信息,供運行時使用的一些位標識
long instance_size OBJC2_UNAVAILABLE; // 類的實例變量大小
// 類的成員變量列表
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
// 方法定義列表
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
// 方法緩存
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
// 協議列表
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
在Objective-C中類
是由Class
表示的,Class
是一個指向struct objc_class
的指針。
typedef struct objc_class *Class;
在這個類的數據結構中,有幾個字段需要解釋一下:
isa:在大多數的面向對象的語言中,都有類和對象的概念。其中,對象是類的實例,是通過類數據結構的定義創建出來的,對象的isa指針是指向其所屬類的。同時,在Objective-C語言中,類本身也是一個對象,類作為對象時isa指針指向元類(Meta Class),后面會詳解;
super_class:指向該類的父類,如果該類已經是根類(NSObject 或 NSProxy),則 其super_class 為NULL;
version:該字段可以獲取類的版本信息,在對象的序列化中可以通過類的版本信息來標識出不同版本的類定義中實例變量布局的改變。
objc_cache與cache
上文object_class
中結構體中的cache
字段,是用來緩存使用過的方法。這個字段是一個指向objc_cache
的指針,具體數據結構如下所示:
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
字段的具體描述如下:
mask:整數類型,指定分配的緩存
bucket
的總數。在方法查找過程中,Objective-C runtime
使用這個字段來確定開始線性查找數組的索引位置。指向方法selector
的指針與該字段做一個AND
位操作(index = (mask & selector))。這可以看作為一個簡單的hash
散列算法;occupied
:一個整數,指定實際占用的緩存bucket
的總數;buckets
:指向Method
數據結構指針的數組。這個數組可能包含不超過mask+1
個元素。需要注意的是,指針可能是NULL
,表示這個緩存bucket
沒有被占用。另外被占用的bucket
可能是不連續的。這個數組可能會隨著時間而增長。
關于上文object_class
中結構體中的cache
字段,對它的解釋如下:
cache:用于緩存最近使用的方法,一個對象可響應的方法列表中通常只有一部分是經常被調用的,cache 則是用來緩存最常調用的方法,從而避免每次方法調用時都去查找對象的整個方法列表,提升性能。
在一些結構較為復雜的類關系中,一個對象的響應方法可能來自于繼承的類結構中,此情況下查找相應的響應方法時就會比較耗時,通常使用cache緩存可以減低查找時間;
舉個栗子:
NSDictionary *dic= [[NSDictionary alloc] init];//執行過程
其緩存調用方法的流程:
-
[NSDictionary alloc]
先被執行。由于NSDictionary
沒有+alloc
方法,于是去父類NSObject
中去查找;
-
- 檢測
NSObject
是否響應+alloc
方法,發現響應,于是檢測NSDictionary
類,并根據其所需的內存空間大小開始分配內存空間,然后把isa
指針指向NSDictionary
類。同時,+alloc
方法也被加進對應類的cache
列表里;
- 檢測
- 3.執行
-init
方法,如果NSDictionary
響應該方法,則直接將該方法加入cache
列表;如果不響應,則去父類查找; - 在后期的操作中,如果再以
[[NSDictionary alloc] init]
這種方式來創建數組,則會直接從cache
中取出相應的方法,直接調用。
- 在后期的操作中,如果再以
元類(Meta Class)
上面講到,有時候類也是一個對象,這種類對象是某一種類的實例,這種類就是元類(Meta Class)。
好比類與對應的實例描述一樣,元類則是類作為對象的描述。元類中方法列表對應的是類方法(Class Method)列表,這正是類作為一個對象所需要的。
當調用該方法[NSArray alloc]
時,Runtime就會在對應的元類方法列表查找其類對應的方法,并匹配調用。
Meta Class是類對象的類。
官方的解釋如下所示:
Since a class is an object, it must be an instance of some other class: a metaclass. The metaclass is the description of the class object, just like the class is the description of ordinary instances. Class methods are described by the metaclass on behalf of the class object, just like instance methods are described by the class on behalf of the instance objects.
至此,又有了新的疑問:元類又是誰的實例呢?它的isa又指向誰呢?答案如下圖所示:
由上圖可以看出,元類的isa都指向根元類(Root Meta Class),即元類都是根元類的實例。
而根元類(Root Meta Class)的isa則指向自己,這樣就不會無休止的關聯下去了。
圖中同樣展示類和元類的繼承關系,非常清晰易懂。
類的實例數據結構
在 Objective-C 中類的實例的數據結構是定義在struct objc_object
中(objc/objc.h):
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
可以看出,這個結構體只有一個字段,即指向該實例所屬類的isa
指針。
這個指針跟上面介紹的類的isa
不一樣:類的isa
指向對應的元類(Meta Class
),實例的isa
則是指向對應的類(Class
),而這個Class
里包含上述所講的數據:父類、類名、方法列表等等。
當我們向一個類的實例發送消息時,Runtime
會根據實例對象的isa
找到這個實例對象所屬的類,然后再在這個類的方法列表和其父類的方法列表中查找與消息相對應的selector
指向的方法,進而執行目標方法。
當創建某一個類的實例時,分配的內存中會包含一個objc_object
數據結構,然后是類的實例變量的相關數據。
NSObject
類的alloc
和allocWithZone:
方法是使用函數class_createInstance
來創建objc_object
數據結構。
我們常見的id
是一個struct objc_object
類型的指針。id
類型的對象可以轉換為任何一種類型的對象,它的作用有點類似 C 語言中的 void *
指針類型。
/// A pointer to an instance of a class.
typedef struct objc_object *id;
相關函數
Objective-C的Runtime
我們提供了很多運行時狀態跟類與對象相關的函數。類的操作方法大部分是以class_
為前綴的,而對象的操作方法大部分是以objc_
或object_
為前綴,具體以分類的形式進行討論。
類相關函數
類的相關函數大部分是與objc_class
結構體各個字段相關的方法。
類名
// 獲取類的類名
const char * class_getName ( Class cls );
- 如果
cls
傳入nil
時,則返回nil
字符串;
父類(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
。
實例變量大小(instance_size)
// 獲取實例大小
size_t class_getInstanceSize ( Class cls );
成員變量(ivars)及屬性
在objc_class
中,所有的成員變量、屬性的信息是放在鏈表ivars
中的。ivars
是一個數組,數組中每個元素是指向Ivar
(變量信息)的指針。
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 );
// 獲取整個成員變量列表
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()來釋放這個數組。
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 );
3.MAC OS X
系統支持使用垃圾回收器,Runtime
提供了幾個函數來確定一個對象的內存區域是否可以被垃圾回收器掃描,以處理strong/weak
引用。這幾個函數定義如下:
const uint8_t * class_getIvarLayout ( Class cls );
void class_setIvarLayout ( Class cls, const uint8_t *layout );
const uint8_t * class_getWeakIvarLayout ( Class cls );
void class_setWeakIvarLayout ( Class cls, const uint8_t *layout );
通常情況下,我們不需要去主動調用這些方法;在調用objc_registerClassPair
時,會生成合理的布局。
方法(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_addMethod
的實現會覆蓋父類的方法實現,但不會取代本類中已存在的實現,如果本類中包含一個同名的實現,則函數會返回NO。如果要修改已存在實現,可以使用
method_setImplementation
。一個Objective-C
方法是一個簡單的C
函數,它至少包含兩個參數self
和_cmd
。所以,我們的實現函數(IMP
參數指向的函數)至少需要兩個參數,如下所示:
void myMethodIMP(id self, SEL _cmd)
{
// implementation ....
}
與成員變量不同的是,我們可以為類動態添加方法,不管這個類是否已存在。
參數types
是一個描述傳遞給方法的參數類型的字符數組,這就涉及到類型編碼。
class_getInstanceMethod
、class_getClassMethod
函數,與class_copyMethodList
不同的是,這兩個函數都會去搜索父類的實現;class_copyMethodLis
t函數,返回包含所有實例方法的數組。如果需要獲取類方法,則可以使用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:
方法來達到相同目的。
協議(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()
手動釋放。
版本(version)
// 獲取版本號
int class_getVersion ( Class cls );
// 設置版本號
void class_setVersion ( Class cls, int version );
其它
runtime
還提供了兩個不直接使用的函數來供CoreFoundation
的tool-free bridging
使用,即:
Class objc_getFutureClass ( const char *name );
void objc_setFutureClass ( Class cls, const char *name );
使用上述函數時,需要特別的注意一下細節信息和使用規范,具體可以查閱 Objective-C Runtime Reference。
動態創建類與對象
Runtime提供在運行時創建類與對象的方法。
動態創建類
// 創建一個新類和元類
Class objc_allocateClassPair ( Class superclass, const char *name, size_t extraBytes );
// 銷毀一個類及其相關聯的類
void objc_disposeClassPair ( Class cls );
// 在應用中注冊由objc_allocateClassPair創建的類
void objc_registerClassPair ( Class cls );
objc_allocateClassPair
函數:如果我們要創建一個根類,則superclass
指定為Nil
。extraBytes
通常指定為0,該參數是分配給類和元類對象尾部的索引ivars
的字節數;創建一個新類,首先,我們需要調用
objc_allocateClassPair
。然后使用諸如class_addMethod
、class_addIvar
等函數來為新創建的類添加方法、實例變量和屬性等。完成這些后,我們需要調用
objc_registerClassPai
r函數來注冊類,之后這個新類就可以在程序中使用了;實例方法和實例變量應該添加到類自身上,而類方法應該添加到類的元類上;
objc_disposeClassPair
函數用于銷毀一個類,不過需要注意的是,如果程序運行中還存在類或其子類的實例,則不能調用針對類調用該方法,在后面的栗子中也有該方面的講解。
動態創建對象
// 創建類實例
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
時,我們需要確切的知道我們要用它來做什么。在下面的例子中,我們用NSString來測試一下該函數的實際效果:
- (void)testInstanceMethod{
id theObject = class_createInstance([NSString class], sizeof(unsigned));
id str1 = [theObject init];
NSLog(@"%@", [str1 class]);
id str2 = [[NSString alloc] initWithString:@"test"];
NSLog(@"%@", [str2 class]);
}
輸出結果:
2018-03-21 22:55:18.503665+0800 RuntimeUsage[2774:32008] NSString
2018-03-21 22:55:21.624606+0800 RuntimeUsage[2774:32008] __NSCFConstantString
可以看到,使用
class_createInstance
函數獲取的是NSString
實例,而不是類簇中的默認占位符類__NSCFConstantString
;objc_constructInstance
函數:在指定的位置(bytes
)創建類實例;objc_destructInstance
函數:銷毀一個類的實例,但不會釋放并移除任何與其相關的引用;
實例操作函數
實例操作函數主要是針對我們創建的實例對象的一系列操作函數。
我們可以使用這組函數來從實例對象中獲取我們想要的一些信息,如實例對象中變量的值。這組函數可以分為三小類:
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 );
獲取類定義
Objective-C
動態運行庫會自動注冊我們代碼中定義的所有的類。
我們也可以在運行時創建類定義并使用objc_addClass
函數來注冊它們。Runtime
提供了一系列函數來獲取類定義相關的信息,這些函數主要包括:
// 獲取已注冊的類定義的列表
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 );
-
objc_getClassList
函數:獲取已注冊的類定義的列表。我們不能假設從該函數中獲取的類對象是繼承自NSObject
體系的,所以在這些類上調用方法是,都應該先檢測一下這個方法是否在這個類中實現。
下面代碼演示了該函數的用法:
- (void)testGetNumberClass{
int numClasses;
Class *classes = NULL;
numClasses = objc_getClassList(NULL, 0);
if (numClasses > 0) {
classes = (Class *) malloc(sizeof(Class) * numClasses);
numClasses = objc_getClassList(classes, numClasses);
NSLog(@"count of classes:%d", numClasses);
for (int i = 0; i < numClasses; i ++) {
Class cls = classes[I];
NSLog(@"class name: %s", class_getName(cls));
}
}
free(classes);
}
輸出的結果:
2018-03-21 23:58:57 RuntimeUsage[3378:65534] count of classes:11809
2018-03-21 23:59 RuntimeUsage[3378:65534] class name: _CNZombie_
2018-03-21 23:59 RuntimeUsage[3378:65534] class name: JSExport
2018-03-21 23:59 RuntimeUsage[3378:65534] class name: NSLeafProxy
2018-03-21 23:59 RuntimeUsage[3378:65534] class name: NSProxy
2018-03-21 23:59 RuntimeUsage[3378:65534] class name: _UITargetedProxy
2018-03-21 23:59 RuntimeUsage[3378:65534] class name: _UIViewServiceReplyControlTrampoline
2018-03-21 23:59:04 RuntimeUsage[3378:65534] class name: _UIViewServiceReplyAwaitingTrampoline
···· 還有很多
獲取類定義的方法有三個:
objc_lookUpClass
、objc_getClass
和objc_getRequiredClass
。如果類在運行時未注冊,則
objc_lookUpClass
會返回nil
,而objc_getClass
會調用類處理回調,并再次確認類是否注冊,如果確認未注冊,再返回nil
。而
objc_getRequiredClass
函數的操作與objc_getClass
相同,只不過如果沒有找到類,則會殺死進程。objc_getMetaClass
函數:如果指定的類沒有注冊,則該函數會調用類處理回調,并再次確認類是否注冊,如果確認未注冊,再返回nil。不過,每個類定義都必須有一個有效的元類定義,所以這個函數總是會返回一個元類定義,不管它是否有效。
運行時操作操作類與對象的示例代碼
-
實例、類、父類、元類關系結構的示例代碼
首先,創建繼承關系為Animal->Dog->NSObject
的幾個類,然后使用Runtime的方法打印其中的關系,運行結果如下所示:
- (void)verifyClassTypeRelation{
//Use `object_getClass` get Class's `isa`
Dog *aDog = [[Dog alloc] init];
Class dogCls = object_getClass(aDog);
NSLog(@"isa->%@ , super_class->%@", dogCls, class_getSuperclass(dogCls));
// print:isa->Dog, super_class->Animal
Class dogMetaCls = objc_getMetaClass([NSStringFromClass(dogCls) UTF8String]);
if (class_isMetaClass(dogMetaCls)) {
NSLog(@"YES, metaCls->%@ , metaCls's super_Class->%@, metaCls's isa->%@", dogMetaCls, class_getSuperclass(dogMetaCls), object_getClass(dogMetaCls));
//print: YES, metaCls->Dog , metaCls's super_Class->Animal, metaCls's isa->NSObject
}else{
NSLog(@"NO");
}
Animal *animal = [[Animal alloc] init];
Class animalCls = object_getClass(animal);
NSLog(@"isa->%@ , super_class->%@", animalCls, class_getSuperclass(animalCls));
//print: isa->Animal , super_class->NSObject
Class animalMetaCls = objc_getMetaClass([NSStringFromClass(animalCls) UTF8String]);
if (class_isMetaClass(animalMetaCls)) {
NSLog(@"YES, metaCls->%@ , metaCls's super_Class->%@, metaCls's isa->%@", animalMetaCls, class_getSuperclass(animalMetaCls), object_getClass(animalMetaCls));
//print:YES, metaCls->Animal , metaCls's super_Class->NSObject, metaCls's isa->NSObject
}else{
NSLog(@"NO");
}
Class viewMetaCls = objc_getMetaClass([NSStringFromClass([UIView class]) UTF8String]);
if (class_isMetaClass(viewMetaCls)) {
NSLog(@"YES, metaCls->%@ , metaCls's super_Class->%@, metaCls's isa->%@", viewMetaCls, class_getSuperclass(viewMetaCls), object_getClass(viewMetaCls));
//print:YES, metaCls->UIView , metaCls's super_Class->UIResponder, metaCls's isa->NSObject
}
Class rootMetaCls = objc_getMetaClass([NSStringFromClass([NSObject class]) UTF8String]);
if (class_isMetaClass(rootMetaCls)) {
NSLog(@"YES, metaCls->%@ , metaCls's super_Class->%@, metaCls's isa->%@", rootMetaCls, class_getSuperclass(rootMetaCls), object_getClass(rootMetaCls));
//print:YES, metaCls->NSObject , metaCls's super_Class->NSObject, metaCls's isa->NSObject
}
}
打印信息如下所示:
isa->Dog, super_class->Animal
YES, metaCls->Dog , metaCls's super_Class->Animal, metaCls's isa->NSObject
isa->Animal , super_class->NSObject
YES, metaCls->Animal , metaCls's super_Class->NSObject, metaCls's isa->NSObject
YES, metaCls->UIView , metaCls's super_Class->UIResponder, metaCls's isa->NSObject
YES, metaCls->NSObject , metaCls's super_Class->NSObject, metaCls's isa->NSObject
需要特別注意一下,Object_getClass
可以獲取當前對象的isa
。以Dog
類打印信息為例,解釋一下具體實現的原理:
isa->Dog, super_class->Animal
YES, metaCls->Dog , metaCls's super_Class->Animal, metaCls's isa->NSObject
首先,通過
Object_getClass
獲取實例aDog
的Class(isa)為Dog
;然后,通過
class_getSuperclass
獲取Dog
的父類為Animal
類;通過
objc_getMetaClass
指定類名,獲取對應的元類,通過class_isMetaClass
方法可以判斷一個類是否為指定類的元類,這里確認后,打印出YES
,打印出的元類名稱為Dog
;打印元類父類為Animal
;在通過Object_getClass
獲取元類的isa,指向NSObject
。
同理可得,Animal
和UIView
打印信息解釋同上。NSobject
,它元類的isa指針還是指向自己的類——NSobject
。打印的信息與上述的關系圖保持一致。
動態操作類與實例的代碼
動態創建類的源碼
/***********************************************************************
* _objc_allocateFutureClass
* Allocate an unresolved future class for the given class name.
* Returns any existing allocation if one was already made.
* Assumes the named class doesn't exist yet.
* Locking: acquires runtimeLock
**********************************************************************/
Class _objc_allocateFutureClass(const char *name)
{
rwlock_writer_t lock(runtimeLock);
Class cls;
NXMapTable *map = futureNamedClasses();
if ((cls = (Class)NXMapGet(map, name))) {
// Already have a future class for this name.
return cls;
}
cls = _calloc_class(sizeof(objc_class));
addFutureNamedClass(name, cls);
return cls;
}
- (void)dynamicAddMethod{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
//1、Create and register class, add method to class
Class cls = objc_allocateClassPair([Animal class], "Lion", 0);// 當為`Cat`時,返回的創建類cat地址為0x0,將`Cat`作為關鍵字
//method: 返回`int32_t`,type使用`i`;參數:`id self`,type使用`@`;`SEL _cmd`,type使用`:`;
//`NSDictionary *dic`,type使用`@`.綜上,type使用'i@:@'
///具體類型可參照 https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
BOOL isAddSuccess = class_addMethod(cls, @selector(howlMethod), (IMP)testRuntimeMethodIMP, "i@:@");
NSLog(@"%@", isAddSuccess ? @"添加方法成功" : @"添加方法失敗");
//You can only register a class once.
objc_registerClassPair(cls);
//2、Create instance of class
id whiteCat = [[cls alloc] init];
NSLog(@"%@, %@", object_getClass(whiteCat), class_getSuperclass(object_getClass(whiteCat)));
// Print: Lion, Animal
Class whiteCatCls = object_getClass(whiteCat);
Class metaCls = objc_getMetaClass([NSStringFromClass(whiteCatCls) UTF8String]);
if (class_isMetaClass(metaCls)) {
NSLog(@"YES, %@, %@, %@", metaCls, class_getSuperclass(metaCls), object_getClass(metaCls));
// Print: YES, Lion, Animal, NSObject
}else{
NSLog(@"NO");
}
//3、Method of class
unsigned int methodCount = 0;
Method *methods = class_copyMethodList(cls, &methodCount);
for (int32_t i = 0; i < methodCount; i ++) {
Method aMethod = methods[I];
NSLog(@"%@, %s", NSStringFromSelector(method_getName(aMethod)), method_getTypeEncoding(aMethod));
//print:howlMethod, I@:@
}
free(methods);
//4、call method
int32_t result = (int)[whiteCat performSelector:@selector(howlMethod) withObject:@{@"name":@"lion", @"sex": @"male"}];
NSLog(@"%d", result);//print:9
//5、destory instance and class
whiteCat = nil;
// Do not call this function if instances of the cls class or any subclass exist.
objc_disposeClassPair(cls);
#pragma clang diagnostic pop
}
打印的信息如下所示:
添加方法成功
Lion, Animal
YES, Lion, Animal, NSObject
howlMethod, I@:@
testRuntimeMethodIMP: {
name = lion;
sex = male;
}
9
在執行objc_allocateClassPair
中,類的名稱設置為Cat
時,創建出的Class
的地址始終指向0x0
,創建類失敗。
猜測其中的原因可能是Cat
與內部的關鍵字沖突了,導致類創建失敗,改為cat
或者其他的都可以創建成功;
- 在上面的代碼中,在運行時動態創建了
Animal
的一個子類:Lion
;接著為這個類添加了方法和實現; - 打印了
Lion
的類、父類、元類相關信息; - 遍歷和打印了
Lion
的方法的相關信息; - 調用了
Lion
的方法; - 最后銷毀了實例和類。
針對上述代碼,有幾點需要特殊說明一下:
對于
#pragma clang diagnostic...
幾行代碼,是用于忽略編譯器對于未聲明@selector的警告信息的,在代碼中,我們動態地為一個類添加方法,不會事先聲明的;class_addMethod()
函數的最后一個參數types
是描述方法返回值和參數列表的字符串。我們的代碼中的用到的
i@:@
四個字符分別對應著:返回值int32_t
、參數id self
、參數SEL _cmd
、參數NSDictionary *dic
。這個其實就是類型編碼(Type Encoding)的概念。在 Objective-C 中,為了協助 Runtime 系統,編譯器會將每個方法的返回值和參數列表編碼為一個字符串,這個字符串會與方法對應的 selector 關聯。更詳細的知識可以查閱 Type Encodings;
使用
objc_registerClassPair()
函數需要注意,不能注冊已經注冊過的類;使用
objc_disposeClassPair()
函數時需要注意,當一個類的實例和子類還存在時,不能去銷毀一個類,謹記;
isKindOf 和 isMemberOf
舉個栗子:
@interface TestMetaClass: NSObject
@end
@implementation TestMetaClass
@end
int main(int argc, char * argv[]) {
@autoreleasepool {
BOOL result1 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL result2 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL result3 = [(id)[TestMetaClass class] isMemberOfClass:[TestMetaClass class]];
BOOL result4 = [(id)[TestMetaClass class] isKindOfClass:[TestMetaClass class]];
NSLog(@"%d %d %d %d", result1, result2, result3, result4);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
//log
2018-02-09 16:45:54.048040+0800 RuntimeUsage[9220:5754652] 0 1 0 0
關于isMemberOfClass和isKindOfClass在Object.mm中的實現,具體如下:
- (BOOL)isMemberOf:aClass
{
return isa == (Class)aClass;
}
- (BOOL)isKindOf:aClass
{
Class cls;
for (cls = isa; cls; cls = cls->superclass)
if (cls == (Class)aClass)
return YES;
return NO;
}
在result1中,從
isMemberOf
看出NSObject class
的isa
第一次會指向NSObject
的Meta Class
,因此NSObject
的Meta Class
與NSObject class
是不相等,返回FALSE
;在result2中,
isKindOf
第一次指向NSObject
的Meta Class
,接著執行superclass
時,根元類NSObject
的Meta Class
根據上面所講的其superclass
指針會閉環指向NSObject class
,從而結果值為TRUE
;在result3中,
isa
會指向TestMetaClass
的Meta Class
,與TestMetaClass Class
不相等,結果值為FALSE
;在result4中,第一次是
TestMetaClass Meta Class
,第二次super class
后就是NSObject Meta Class
,結果值為FALSE
;
以上再次驗證了,NSObject Meta Class
的isa
指針指向自身,其super class
指向NSObject
。
小結
本文著重講解了在Runtime時類與對象相關方法和數據結構,通過這些講解可以讓大家對Objective-C底層類與對象實現有大致的了解,并且可以為大家平常編程過程提供一些思路上的啟發。
測試使用的栗子(Demo)都在籃子里