Runtime的應用
attribute
__attribute__是一套編譯器指令,被GNU和LLVM編譯器所支持,允許對於__attribute__增加一些參數,做一些高級檢查和優化。
__attribute__的語法是,在後面加兩個括弧,然後寫屬性列表,屬性列表以逗號分隔。在iOS中,很多例如NS_CLASS_AVAILABLE_IOS的宏定義,內部也是通過__attribute__實現的。
__attribute__((attribute1, attribute2));
下面是一些__attribute__的常用屬性,更完整的屬性列表可以到llvm的官網查看。
官網示例
objc_subclassing_restricted
objc_subclassing_restricted屬性表示被修飾的類不能被其他類繼承,否則會報下面的錯誤。
__attribute__((objc_subclassing_restricted))
@interfaceTestObject : NSObject
@property(nonatomic, strong) NSObject *object;
@property(nonatomic, assign) NSInteger age;
@end
@interfaceChild : TestObject
@end
錯誤信息:
Cannot subclass aclassthatwasdeclaredwiththe"objc_subclassing_restricted"attribute
objc_requires_super
objc_requires_super屬性表示子類必須調用被修飾的方法super,否則報黃色警告。
@interfaceTestObject:NSObject
- (void)testMethod __attribute__((objc_requires_super));
@end
@interfaceChild:TestObject
@end
警告信息:(不報錯)
Method possibly missing a [supertestMethod] call
constructor / destructor
constructor屬性表示在main函數執行之前,可以執行一些操作。destructor屬性表示在main函數執行之後做一些操作。constructor的執行時機是在所有load方法都執行完之後,才會執行所有constructor屬性修飾的函數。
__attribute__((constructor))staticvoidbeforeMain(){
NSLog(@"before main");
}
__attribute__((destructor))staticvoidafterMain(){
NSLog(@"after main");
}
intmain(intargc,constchar* argv[]){
@autoreleasepool{
NSLog(@"execute main");
}
return;
}
執行結果:
debug-objc[23391:1143291] before main
debug-objc[23391:1143291] execute main
debug-objc[23391:1143291] after main
在有多個constructor或destructor屬性修飾的函數時,可以通過設置優先順序來指定執行順序。格式是__attribute__((constructor(101)))的方式,在屬性後面直接跟優先順序。
__attribute__((constructor(103)))staticvoidbeforeMain3(){
NSLog(@"after main 3");
}
__attribute__((constructor(101)))staticvoidbeforeMain1(){
NSLog(@"after main 1");
}
__attribute__((constructor(102)))staticvoidbeforeMain2(){
NSLog(@"after main 2");
}
在constructor中根據優先順序越低,執行順序越高。而destructor則相反,優先順序越高則執行順序越高。
overloadable
overloadable屬性允許定義多個同名但不同參數類型的函數,在調用時編譯器會根據傳入參數類型自動匹配函數。這個有點類似於C++的函數重載,而且都是發生在編譯期的行為。
__attribute__((overloadable))voidtestMethod(intage){}
__attribute__((overloadable))voidtestMethod(NSString *name){}
__attribute__((overloadable))voidtestMethod(BOOL gender){}
intmain(intargc,constchar* argv[]){
@autoreleasepool{
testMethod(18);
testMethod(@"lxz");
testMethod(YES);
}
return;
}
objc_runtime_name
objc_runtime_name屬性可以在編譯時,將Class或Protocol指定為另一個名字,並且新名字不受命名規範制約,可以以數字開頭。
__attribute__((objc_runtime_name("TestObject")))
@interfaceObject:NSObject
@end
NSLog(@"%@",NSStringFromClass([TestObjectclass]));
執行結果:
TestObject
這個屬性可以用來做代碼混淆,例如寫一個宏定義,宏定義內部實現混淆邏輯。例如通過MD5對Object做混淆,32位的混淆結果就是497031794414a552435f90151ac3b54b,誰能看出來這是什麼類。如果怕彩虹表匹配出來,再增加加鹽邏輯。
cleanup
通過cleanup屬性,可以指定給一個變數,當變數釋放之前執行一個函數。指定的函數執行的時間,是在dealloc之前的。在指定的函數中,可以傳入一個形參,參數就是cleanup修飾的變數,形參是一個地址。
staticvoidreleaseBefore(NSObject **object){
NSLog(@"%@", *object);
}
intmain(intargc,constchar* argv[]){
@autoreleasepool {
TestObject *object__attribute__((cleanup(releaseBefore))) = [[TestObject alloc] init];
}
return;
}
如果遇到同一個代碼塊中,同時出現多個cleanup屬性時,在代碼塊作用域結束時,會以添加的順序進行調用。
unused
還有一個屬性很實用,在項目里經常會有未使用的變數,會報一個黃色警告。有時候可能會通過其他方式獲取這個對象,所以不想出現這個警告,可以通過unused屬性消除這個警告。
NSObject*object __attribute__((unused)) = [[NSObjectalloc] init];
系統定義
在系統里也大量使用了__attribute__關鍵字,只不過系統不會直接在外部使用__attribute__,一般都是將其定義為宏定義,以宏定義的形式出現在外面。
// NSLog
FOUNDATION_EXPORTvoidNSLog(NSString *format, ...)NS_FORMAT_FUNCTION(1,2)NS_NO_TAIL_CALL;
#defineNS_FORMAT_FUNCTION(F,A) __attribute__((format(__NSString__, F, A)))
// 必須調用父類的方法
#defineNS_REQUIRES_SUPER __attribute__((objc_requires_super))
// 指定初始化方法,必須直接或間接調用修飾的方法
#defineNS_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
ORM
對象關係映射(Object Relational Mapping),簡稱ORM,用於面向對象語言中不同系統數據之間的轉換。
可以通過對象關係映射來實現JSON轉模型,使用比較多的是Mantle、MJExtension、YYKit、JSONModel等框架,這些框架在進行轉換的時候,都是使用Runtime的方式實現的。
Mantle使用和MJExtension有些類似,只不過MJExtension使用起來更加方便。Mantle在使用時主要是通過繼承的方式處理,而MJExtension是通過Category處理,代碼依賴性更小,無侵入性。
性能評測
這些第三方中Mantle功能最強大,但是太臃腫,使用起來性能比其他第三方都差一些。JSONModel、MJExtension這些第三方几乎都在一個水平級,YYKit相對來說性能可以比肩手寫賦值代碼,性價比最高。
對於模型轉換需求不是太大的工程來說,盡量用YYKit來進行轉換性能會更好一些。功能可能略遜於MJExtension,我個人還是比較習慣用MJExtension。
實現思路
也可以自己實現模型轉換的邏輯,以字典轉模型為例,大體邏輯如下:
創建一個Category用來做模型轉換,對外提供方法並傳入字典對象。
通過Runtime對應的函數,獲取屬性列表並遍歷,根據屬性名從字典中取出對應的對象。
通過KVC將從字典中取出的值,賦值給對象。
有時候會遇到多層嵌套的情況,例如字典包含數組,數組中還是一個字典。這種情況就可以做判斷,如果模型對象是數組則取出字典對應欄位的數組,然後遍曆數組再調用字典賦值的方法。
下面簡單實現了一個字典轉模型的代碼,通過Runtime遍歷屬性列表,並根據屬性名取出字典中的對象,然後通過KVC進行賦值操作。調用方式和MJExtension、YYModel類似,直接通過模型類調用類方法即可。如果想在其他類中也使用的話,應該把下面的實現寫在NSObject的Category中,這樣所有類都可以調用。
// 調用部分
NSDictionary*dict = @{@"name":@"lxz",
@"age": @18,
@"gender": @YES};
TestObject *object = [TestObject objectWithDict:dict];
// 實現代碼
@interfaceTestObject:NSObject
@property(nonatomic,copy)NSString*name;
@property(nonatomic,assign)NSIntegerage;
@property(nonatomic,assign)BOOLgender;
+ (instancetype)objectWithDict:(NSDictionary*)dict;
@end
@implementationTestObject
+ (instancetype)objectWithDict:(NSDictionary*)dict {
return[[TestObject alloc] initWithDict:dict];
}
- (instancetype)initWithDict:(NSDictionary*)dict {
self= [superinit];
if(self) {
unsignedintcount =;
objc_property_t *propertys = class_copyPropertyList([selfclass], &count);
for(inti =; i
objc_property_t property = propertys[i];
constchar*name = property_getName(property);
NSString*nameStr = [[NSStringalloc] initWithUTF8String:name];
idvalue = [dict objectForKey:nameStr];
[selfsetValue:value forKey:nameStr];
}
free(propertys);
}
returnself;
}
@end
通過Runtime可以獲取到對象的Method List、Property List等,不只可以用來做字典模型轉換,還可以做很多工作。例如還可以通過Runtime實現自動歸檔和反歸檔,下面是自動進行歸檔操作。
// 1.獲取所有的屬性
unsignedintcount =;
Ivar *ivars = class_copyIvarList([NJPersonclass], &count);
// 遍歷所有的屬性進行歸檔
for(inti =; i
// 取出對應的屬性
Ivar ivar = ivars[i];
constchar* name = ivar_getName(ivar);
// 將對應的屬性名稱轉換為OC字元串
NSString*key = [[NSStringalloc] initWithUTF8String:name];
// 根據屬性名稱利用KVC獲取數據
idvalue = [selfvalueForKeyPath:key];
[encoder encodeObject:value forKey:key];
}
free(ivars);
Runtime面試題
題1
下面的代碼輸出什麼?
@implementationSon:Father
- (id)init {
self= [superinit];
if(self) {
NSLog(@"%@",NSStringFromClass([selfclass]));
NSLog(@"%@",NSStringFromClass([superclass]));
}
returnself;
}
@end
答案:都輸出Son。
第一個NSLog輸出Son肯定是不用說的。
第二個輸出中,[super class]會被轉換為下面代碼。
structobjc_super objcSuper = {
self,
class_getSuperclass([selfclass]),
};
id(*sendSuper)(structobjc_super*, SEL) = (void*)objc_msgSendSuper;
sendSuper(&objcSuper,@selector(class));
super的調用會被轉換為objc_msgSendSuper的調用,並傳入一個objc_super類型的結構體。結構體有兩個參數,第一個就是接受消息的對象,第二個是[super class]對應的父類。
structobjc_super {
__unsafe_unretained_Nonnullidreceiver;
__unsafe_unretained_Nonnull Class super_class;
};
由此可知,雖然調用的是[super class],但是接受消息的對象還是self。然後來到父類Father的class方法中,輸出self對應的類Son。
題2
下面代碼的結果?
BOOLres1 = [(id)[NSObjectclass] isKindOfClass:[NSObjectclass]];
BOOLres2 = [(id)[NSObjectclass] isMemberOfClass:[NSObjectclass]];
BOOLres3 = [(id)[Sarkclass] isKindOfClass:[Sarkclass]];
BOOLres4 = [(id)[Sarkclass] isMemberOfClass:[Sarkclass]];
答案:
除了第一個是YES,其他三個都是NO。
在推測結果之前,首先要明白兩個問題。isKindOfClass和isMemberOfClass的區別是什麼?
isKindOfClass:class,調用該方法的對象所屬的類,繼承者鏈中包含傳入的class則返回YES。
isMemberOfClass:class,調用改方法的對象所屬的類,必須是傳入的class則返回YES。
我們從Runtime源碼的角度來分析一下結果。
+ (BOOL)isMemberOfClass:(Class)cls {
returnobject_getClass((id)self) == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return[selfclass] == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
for(Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if(tcls == cls)returnYES;
}
returnNO;
}
- (BOOL)isKindOfClass:(Class)cls {
for(Class tcls = [selfclass]; tcls; tcls = tcls->superclass) {
if(tcls == cls)returnYES;
}
returnNO;
}
平時開發過程中只會接觸到對象方法的isKindOfClass和isMemberOfClass,但是在NSObject類中還隱式的實現了類方法版本。不只這兩個方法,其他NSObject中的對象方法,都有其對應的類方法版本。因為在OC中,類和元類也都是對象。這四個調用由於都是類對象發起調用的,所以最終執行的都是類方法版本。
先把Runtime的對象模型拿出來,方便後面的分析。
對象模型
第一次調用方是NSObject類對象,調用isKindOfClass方法傳入的也是類對象。因為調用類的class方法,會把類自身直接返回,所以還是類對象自己。
然後進入到for循環中,會從NSObject的元類開始遍歷,所以第一次NSObject meta class != NSObject class,匹配失敗。第二次循環將tcls設置為superclass的NSObject class,NSObject class == NSObject class,匹配成功。
NSObject能匹配成功,是因為這個類比較特殊,在第二次獲取superclass的時候,NSObject元類的superclass就是NSObject的類對象,所以會匹配成功。而其他三種匹配,則都會失敗,各位同學可以去自己分析一下剩下三種。
題3
下面的代碼會?Compile Error / Runtime Crash / NSLog…?
@interfaceNSObject(Sark)
+ (void)foo;
@end
@implementationNSObject(Sark)
- (void)foo {
NSLog(@"IMP: -[NSObject (Sark) foo]");
}
@end
// 測試代碼
[NSObjectfoo];
[[NSObjectnew] foo];
答案:
全都正常輸出,編譯和運行都沒有問題。
這道題和上一道題很相似,第二個調用肯定沒有問題,第一個調用後會從元類中查找方法,然而方法並不在元類中,所以找元類的superclass。方法定義在是NSObject的Category,由於NSObject的對象模型比較特殊,元類的superclass是類對象,所以從類對象中找到了方法並調用。
題4
下面的代碼會?Compile Error / Runtime Crash / NSLog…?
@interfaceSark:NSObject
@property(nonatomic,copy)NSString*name;
@end
@implementationSark
- (void)speak {
NSLog(@"my name"s %@",self.name);
}
@end
// 測試代碼
@implementationViewController
- (void)viewDidLoad {
[superviewDidLoad];
idcls = [Sarkclass];
void*obj = &cls;
[(__bridgeid)obj speak];
}
@end
答案:
正常執行,不會導致Crash。
執行[Sark class]後獲取到類對象,然後通過obj指針指向獲取到的類對象首地址,這就構成了對象的基本結構,可以進行正常調用。
原題出處
題5
為什麼MRC下沒有weak?
其實MRC下並不是沒有weak,在MRC環境下也可以通過Runtime源碼調用weak源碼的。weak源碼定義在Private Headers私有文件夾下,需要引入#import "objc-internal.h"文件。
以以下ARC的源碼為例,定義了一個TestObject類型的對象,並用一個weak指針指向已創建對象。
intmain(intargc,constchar* argv[]){
@autoreleasepool {
TestObject *object= [[TestObject alloc] init];
__weak TestObject *newObject =object;
}
return;
}
這段代碼會被編譯器轉移為下面代碼,這段代碼中的兩個函數就是weak的實現函數,在MRC下也可以調用這兩個函數。
objc_initWeak(&newObject,object);
objc_destroyWeak(&newObject);
題6
相同的一個類,創建不同的對象,怎樣實現指定的某個對象在dealloc時列印一段文字?
這個問題最簡單的方法就是在類的.h文件里,定義一個標記屬性,如果屬性被賦值為YES,則在dealloc中列印文字。但是,這種實現方式顯然不是面試官想要的,會被直接pass~
可以參考KVO的實現方案,在運行時動態創建一個類,這個類是對象的子類,將新創建類的dealloc實現指向自定義的IMP,並在IMP中列印一段文字。將對象的isa設置為新創建的類,當執行dealloc方法時就會執行isa所指向的新類。
思考
小問題
什麼叫做技術大牛,怎樣就表示技術強?
我前段時間看過一句話,我感覺可以解釋上面的問題:「市面上所有應用的功能,產品提出來我都能做」。
這句話並不夠全面,應該不只是做出來,而是更好的做出來。這個好要從很多方面去評估,性能、可維護性、完成時間、產品效果等,如果這些都做的很好,那足以證明這個人技術很強大。
Runtime有什麼用?
Runtime是比較偏底層的,但是研究這麼深有什麼用嗎,有什麼實際意義嗎?
Runtime當然是由實際用處的,先不說整個OC都是通過Runtime實現的。例如現在需要實現消息轉發的功能,這時候就需要用到Runtime,或者是攔截方法,也需要用到Method Swizzling,除了這些,還有更多的用法待我們去發掘。
不只是使用,其實最重要的是,通過Runtime了解一個語言的設計。Runtime中不只是各種函數調用,從整體來看,可以明白OC的對象模型是什麼樣的。
簡書由於排版的問題,閱讀體驗並不好,布局、圖片顯示、代碼等很多問題。所以建議到我Github上,下載Runtime PDF合集。把所有Runtime文章總計九篇,都寫在這個PDF中,而且左側有目錄,方便閱讀。
Runtime PDF
麻煩各位大佬點個贊,謝謝!
作者:劉小壯
鏈接:https://www.jianshu.com/p/ce97c66027cd
※iOS crash 日誌堆棧解析
※蘋果、Google、同花順等傳統互聯網巨頭相繼增加數字貨幣行情……
TAG:Cocoa開發者社區 |