C本身是一個靜態語言,數據類型和代碼運行的結果都是在編譯的時候確定的。而Objective-C的runtime機制賦予了C一個新的活力,即運行時機制。這也就是說,OC代碼或者C代碼在編譯過後的機器碼並不能得出運行結果。而這個結果需要在運行的時候才能獲得,這樣就給了我們一個新的操縱代碼的空間,也就是運行時。在OC中,運行時是一段提前寫完的一個模塊的代碼。可以這麼說,OC的運行時就是這段代碼賦予的。
前幾篇文章中,我提到了objc_msgSend的流程,是為了讓大家對runtime的過程有一個大致的了解。但是,對於大部分人來說,比起原理,更關注的是怎麼用。所以本章的內容就是我在runtime小序曲,從運行時多態看這股神秘力量中提到的runtime的除了objc_msgSend的另外兩種應用:NSObject的方法和runtime的函數。
學習進度:
runtime小序曲,從運行時多態看這股神秘力量
runtime進行曲,objc_msgSend的前世今生(一)
runtime進行曲,objc_msgSend的前世今生(二)
runtime變奏曲,那些藏在runtime中的接口(一)
runtime變奏曲,那些藏在runtime中的接口(二)
一、NSObject的方法
前幾天,和群裡的一位騷年討論了runtime的問題。他的看法是,runtime並沒有什麼用,不用runtime照樣可以開發。其實,前半句並沒有什麼毛病,runtime確實沒有什麼用,因為大部分開發工作基本用不著(其實我們公司用的蠻多的)。問題出在第二句,不用runtime就可以開發。OC作為一種高級語言,能讓你方便的使用它的一些接口。比如:
- (BOOL)respondsToSelector:(SEL)aSelector;
這個方法大家應該經常使用,尤其是在使用delegate的場合,基本是必用的。那麼,我們從邏輯上看這個方法。從runtime小序曲,從運行時多態看這股神秘力量中我就說過,OC沒法在編譯時刻確定一個對象的類型。而這個方法是判斷一個繼承自NSObject的class有沒有實現一個SEL。很明顯,編譯時刻做不了這件事,它是在運行時刻做的。所以,不用runtime就可以開發是錯誤的。其實,這種事情並不罕見,大部分人不想學習runtime的原因也是如此。
runtime是C和匯編寫的,看不懂。
runtime開發用不著。
換個方式再說一下,runtime賦予C面向對象的能力,所以有了OC。那麼說實話,只要你用到class,其實都是和runtime相關的,怎麼可能避開。好像跑題了,現在我們回來。其實,和respondsToSelector:類似的OC方法還有很多,下述我會整理一些常用的。
// 在usr/include中的objc/runtime.h可以查看 // 獲取對象對應的class - (Class)class; // 判斷一個對象或者類是不是某個class或者這個class的派生類 - (BOOL)isKindOfClass:(Class)aClass; // 判斷一個對象或者類是不是某個class - (BOOL)isMemberOfClass:(Class)aClass; // 判斷一個對象或者類對應的objc_class裡面是否實現了某個協議 - (BOOL)conformsToProtocol:(Protocol *)aProtocol; // 判斷一個對象或者類對應的objc_class裡面有沒有某個方法 - (BOOL)respondsToSelector:(SEL)aSelector;
二、runtime中的數據結構
一中舉出了一些OC的runtime方法,很易懂,因為OC就是我們的開發語言,下面我會一個個解讀runtime中的C語言的接口。因為C中沒有class的概念,只有struct,所以在介紹C語言接口之前,這裡我將挨個介紹在runtime中使用到的常見結構體。
1、objc_class
類結構體,對應class。前幾篇文章提到最多的一個結構。
objc_class
struct objc_class { // 指向元類的的指針,如果本身是元類,則指向rootMeta Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ // 指向父類 Class super_class OBJC2_UNAVAILABLE; // 類名 const char *name OBJC2_UNAVAILABLE; // 版本號,可以用runtime方法set,get long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; // 成員變量列表,存儲成員變量 struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 方法列表 struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法cache,msgSend遍歷繼承鏈的時候需要輔助使用 struct objc_cache *cache OBJC2_UNAVAILABLE; // 協議列表 struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; #endif
} OBJC2_UNAVAILABLE;
OBJC2_UNAVAILABLE宏定義是蘋果在 Objc 中對系統運行版本進行約束的操作,為的是兼容非Objective-C 2.0的遺留邏輯,但我們仍能從中獲得一些有價值的信息,有興趣的可以查看源代碼,見底部文獻。
2、與objc_class直接相關
這裡面舉出了存儲於上述objc_class結構體之中的一些相關結構體。
objc_method
// 存在objc_method_list裡面 struct objc_method { // 方法名 SEL method_name OBJC2_UNAVAILABLE; // 參數,返回值編碼 char *method_types OBJC2_UNAVAILABLE; // 方法地址指針 IMP method_imp OBJC2_UNAVAILABLE; }
objc_method_description
// 方法描述 struct objc_method_description { // 方法名 SEL name; // 參數,返回值編碼 char *types; };
objc_protocol_list
struct objc_protocol_list { // 指向下一個objc_protocol_list的指針 struct objc_protocol_list *next; long count; // 協議結構 Protocol *list[1]; };
objc_ivar
struct objc_ivar { // 成員變量名 char *ivar_name OBJC2_UNAVAILABLE; // 成員變量的類型,比如@"NSString" 代表NSString char *ivar_type OBJC2_UNAVAILABLE; int ivar_offset OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif }
3、與分類相關
分類是不會在編譯時刻綁定到對應的類裡的,只能在運行時動態綁定。所以這裡把它拿出來。
objc_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; // 分類實現的協議 struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; }
分類和類擴展區別:
extension看起來很像一個匿名的category,但是extension和有名字的category幾乎完全是兩個東西。 extension在編譯期決議,它就是類的一部分,在編譯期和頭文件裡的@interface以及實現文件裡的@implement一起形成一個完整的類,它伴隨類的產生而產生,亦隨之一起消亡。但是category則完全不一樣,它是在運行期決議的。如想學習更多category相關底層,參見:http://tech.meituan.com/DiveIntoCategory.html。
4、屬性相關
這裡為什麼把屬性拿出來呢?因為上述的兼容OC1.0的objc_class裡面沒有property。看了runtime的源碼之後,發現property有一個具體的存儲地點,也在objc_class(實質是繼承了objc_object)中,至於上述objc_class沒有,只是因為apple不想讓我們看見。如想了解更多,參考:runtime源碼的objc-runtime-new.mm和objc-private.h。
objc_property_t
// 屬性對應的結構體objc_property,至於具體結構,一般看不著 typedef struct objc_property *objc_property_t; // 在objc-runtime-old.h中的結構體作為參考吧 struct old_property { // 屬性名 const char *name; // objc_property_attribute_t的char*表示 const char *attributes; };
objc_property_attribute_t
// 這是用來修飾表示屬性的一些修飾詞,比如nonatomic、copy等等 typedef struct { const char *name; const char *value; } objc_property_attribute_t;
objc_property_attribute_t具體參考:https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW1。
三、runtime的函數
到這也就到了正菜了。本章我會把runtime庫中public部分所有和class相關的api都調用一遍,解釋一遍。至於ivar相關、method相關、protocol相關、property相關和具體應用,且待下回分解。
前提
// 為獲取class的protocol准備 @protocol AProtocol - (void)aProtocolMethod; @end // 為獲取class的相關信息 @interface A : NSObject { NSString *strA; } @property (nonatomic, assign) NSUInteger uintA; @end @implementation A @end // 為為class添加方法准備 void aNewMethod() { NSLog(@"aNewMethod"); } void aReplaceMethod() { NSLog(@"aReplaceMethod"); }
class相關使用場景1(class一些基礎信息獲取):
// 代碼 // 獲取類名 const char *a = class_getName([A class]); NSLog(@"%s", a); // a // 獲取父類 Class aSuper = class_getSuperclass([A class]); NSLog(@"%s", class_getName(aSuper)); // b // 判斷是否是元類 BOOL aIfMeta = class_isMetaClass([A class]); BOOL aMetaIfMeta = class_isMetaClass(objc_getMetaClass("A")); NSLog(@"%i %i", aIfMeta, aMetaIfMeta); // c // 類大小 size_t aSize = class_getInstanceSize([A class]); NSLog(@"%zu", aSize); // d // 獲取和設置類版本號 class_setVersion([A class], 1); NSLog(@"%d", class_getVersion([A class])); // e // 獲取工程中所有的class,包括系統class unsigned int count3; int classNum = objc_getClassList(NULL, count3); NSLog(@"%d", classNum); // f // 獲取工程中所有的class的數量 objc_copyClassList(&count3); NSLog(@"%d", classNum); // g Class aClass; // 獲取name為"A"的class aClass = objc_getClass("A"); NSLog(@"%s", class_getName(aClass)); // h // 獲取name為"A"的class,比getClass少了一次檢查 aClass = objc_lookUpClass("A"); NSLog(@"%s", class_getName(aClass)); // i // 獲取name為"A"的class,找不到會crash aClass = objc_getRequiredClass("A"); NSLog(@"%s", class_getName(aClass)); // j // 獲取name為"A"的class元類 Class aMetaClass = objc_getMetaClass("A"); NSLog(@"%d", class_isMetaClass(aMetaClass)); // k // 輸出 2017-01-21 12:15:55.909 block[2493:1919841] A // a 2017-01-21 12:15:55.912 block[2493:1919841] NSObject // b 2017-01-21 12:15:55.913 block[2493:1919841] 0 1 // c 2017-01-21 12:15:55.913 block[2493:1919841] 24 // d 2017-01-21 12:15:55.914 block[2493:1919841] 1 // e 2017-01-21 12:44:32.401 block[5103:1948802] 4733 // f 2017-01-21 12:44:32.402 block[5103:1948802] 4733 // g 2017-01-21 12:44:32.402 block[5103:1948802] A // h 2017-01-21 12:44:32.403 block[5103:1948802] A // i 2017-01-21 12:44:32.403 block[5103:1948802] A // j 2017-01-21 12:44:32.403 block[5103:1948802] 1 // k
class相關使用場景2(class中的ivar和property)
// 代碼 // 獲取類實例成員變量,只能取到本類的,父類的訪問不到 Ivar aInstanceIvar = class_getInstanceVariable([A class], "strA"); NSLog(@"%s", ivar_getName(aInstanceIvar)); // a // 獲取類成員變量,相當於class_getInstanceVariable(cls->isa, name),感覺除非給metaClass添加成員,否則不會獲取到東西 Ivar aClassIvar = class_getClassVariable([A class], "strA"); NSLog(@"%s", ivar_getName(aClassIvar)); // b // 往A類添加成員變量不會成功的。因為class_addIvar不能給現有的類添加成員變量,也不能給metaClass添加成員變量,那怎麼添加,且往後看 if (class_addIvar([A class], "intA", sizeof(int), log2(sizeof(int)), @encode(int))) { NSLog(@"綁定成員變量成功"); // c } // 獲取類中的ivar列表,count為ivar總數 unsigned int count; Ivar *ivars = class_copyIvarList([A class], &count); NSLog(@"%i", count); // d // 獲取某個名為"uIntA"的屬性 objc_property_t aPro = class_getProperty([A class], "uintA"); NSLog(@"%s", property_getName(aPro)); // e // 獲取類的全部屬性 class_copyPropertyList([A class], &count); NSLog(@"%i", count); // f // 創建objc_property_attribute_t,然後動態添加屬性 objc_property_attribute_t type = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; //type objc_property_attribute_t ownership0 = { "C", "" }; // C = copy objc_property_attribute_t ownership = { "N", "" }; //N = nonatomic objc_property_attribute_t backingivar = { "V", [[NSString stringWithFormat:@"_%@", @"aNewProperty"] UTF8String] }; //variable name objc_property_attribute_t attrs[] = { type, ownership0, ownership, backingivar }; if(class_addProperty([A class], "aNewProperty", attrs, 4)) { // 只會增加屬性,不會自動生成set,get方法 NSLog(@"綁定屬性成功"); // g } // 創建objc_property_attribute_t,然後替換屬性 objc_property_attribute_t typeNew = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; //type objc_property_attribute_t ownership0New = { "C", "" }; // C = copy objc_property_attribute_t ownershipNew = { "N", "" }; //N = nonatomic objc_property_attribute_t backingivarNew = { "V", [[NSString stringWithFormat:@"_%@", @"uintA"] UTF8String] }; //variable name objc_property_attribute_t attrsNew[] = { typeNew, ownership0New, ownershipNew, backingivarNew }; class_replaceProperty([A class], "uintA", attrsNew, 4); // 這有個很大的坑。替換屬性指的是替換objc_property_attribute_t,而不是替換name。如果替換的屬性class裡面不存在,則會動態添加這個屬性 objc_property_t pro = class_getProperty([A class], "uintA"); NSLog(@"123456 %s", property_getAttributes(pro)); // h // class_getIvarLayout、class_setIvarLayout、class_getWeakIvarLayout、class_setWeakIvarLayout用來設定和獲取成員變量的weak、strong。參見http://blog.sunnyxx.com/2015/09/13/class-ivar-layout/ // 輸出 2017-01-21 12:44:32.377 block[5103:1948802] strA // a 2017-01-21 12:44:32.377 block[5103:1948802] (null) // b 2017-01-21 12:44:32.377 block[5103:1948802] 2 // d 2017-01-21 12:44:32.377 block[5103:1948802] uintA // e 2017-01-21 12:44:32.378 block[5103:1948802] 1 // f 2017-01-21 12:44:32.378 block[5103:1948802] 綁定屬性成功 // g 2017-01-21 12:44:32.379 block[5103:1948802] 123456 T@"NSString",C,N,V_uintA // h
class相關使用場景3(class中的method)
// 代碼 // 動態添加方法 class_addMethod([A class], @selector(aNewMethod), (IMP)aNewMethod, "v"); // 向元類動態添加類方法 class_addMethod(objc_getMetaClass("A"), @selector(aNewMethod), (IMP)aNewMethod, "v"); // 獲取類實例方法 Method aMethod = class_getInstanceMethod([A class], @selector(aNewMethod)); // 獲取元類中類方法 Method aClassMethod = class_getClassMethod([A class], @selector(aNewMethod)); NSLog(@"%s", method_getName(aMethod)); // a NSLog(@"%s", method_getName(aClassMethod)); // b // 獲取類中的method列表 unsigned int count1; Method *method = class_copyMethodList([A class], &count1); // 多了一個方法,打印看出.cxx_destruct,只在arc下有,析構函數 NSLog(@"%i", count1); // c NSLog(@"%s", method_getName(method[2])); // d // 替換方法,其實是替換IMP class_replaceMethod([A class], @selector(aNewMethod), (IMP)aReplaceMethod, "v"); // 調用aNewMethod,其實是調用了aReplaceMethod [[A new] performSelector:@selector(aNewMethod)]; // aReplaceMethod會輸出 e // 獲取類中某個SEL的IMP IMP aNewMethodIMP = class_getMethodImplementation([A class], @selector(aNewMethod)); aNewMethodIMP(); // 會調用aReplaceMethod的輸出 f // 獲取類中某個SEL的IMP IMP aNewMethodIMP_stret = class_getMethodImplementation_stret([A class], @selector(aNewMethod)); aNewMethodIMP_stret(); // 會調用aReplaceMethod的輸出 g // 判斷A類中有沒有一個SEL if(class_respondsToSelector([A class], @selector(aNewMethod))) { NSLog(@"存在這個方法"); // h } // 輸出 2017-01-21 12:44:32.379 block[5103:1948802] aNewMethod // a 2017-01-21 12:44:32.379 block[5103:1948802] aNewMethod // b 2017-01-21 12:44:32.380 block[5103:1948802] 4 // c 2017-01-21 12:44:32.380 block[5103:1948802] setUintA: // d 2017-01-21 12:44:32.380 block[5103:1948802] aReplaceMethod // e 2017-01-21 12:44:32.381 block[5103:1948802] aReplaceMethod // f 2017-01-21 12:44:32.381 block[5103:1948802] aReplaceMethod // g 2017-01-21 12:44:32.381 block[5103:1948802] 存在這個方法 // h
class相關使用場景4(動態創建類)
// 代碼 // 動態創建一個類和其元類 Class aNewClass = objc_allocateClassPair([NSObject class], "aNewClass", 0); // 添加成員變量 if (class_addIvar(aNewClass, "intA", sizeof(int), log2(sizeof(int)), @encode(int))) { NSLog(@"綁定成員變量成功"); // a } // 注冊這個類,之後才能用 objc_registerClassPair(aNewClass); // 銷毀這個類和元類 objc_disposeClassPair(aNewClass); // 輸出 2017-01-21 12:44:32.382 block[5103:1948802] 綁定成員變量成功 // a class相關使用場景5(class中的protocol) // 代碼 // 添加protocol到class if(class_addProtocol([A class], @protocol(AProtocol))) { NSLog(@"綁定Protocol成功"); // a } // 查看類是不是遵循protocol if(class_conformsToProtocol([A class], @protocol(AProtocol))) { NSLog(@"A遵循AProtocol"); // b } // 獲取類中的protocol unsigned int count2; Protocol *__unsafe_unretained *aProtocol = class_copyProtocolList([A class], &count2); NSLog(@"%s", protocol_getName(aProtocol[0])); // c // 輸出 2017-01-21 12:44:32.381 block[5103:1948802] 綁定Protocol成功 // a 2017-01-21 12:44:32.381 block[5103:1948802] A遵循AProtocol // b 2017-01-21 12:44:32.382 block[5103:1948802] AProtocol // c
四、小結
本章介紹了runtime的一些OC方法和runtime的數據結構,另外把runtime庫public部分中所有與class相關的C方法都介紹了。下一節,將繼續介紹runtime庫public部分中中的其他的api。(其實,我覺得runtime學習不要太關注應用,准確的說,每一個api都是一種應用)
五、文獻
1、https://developer.apple.com/reference/objectivec/1657527-objective_c_runtime?language=objc
2、https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html
3、http://blog.sunnyxx.com/2015/09/13/class-ivar-layout/
4、http://tech.meituan.com/DiveIntoCategory.html
5、https://github.com/opensource-apple/objc4