該文章屬于劉小壯原創(chuàng),轉(zhuǎn)載請注明:劉小壯

attribute
__attribute__是一套編譯器指令,被GNU和LLVM編譯器所支持,允許對于__attribute__增加一些參數(shù),做一些高級檢查和優(yōu)化。
__attribute__的語法是,在后面加兩個括號,然后寫屬性列表,屬性列表以逗號分隔。在iOS中,很多例如NS_CLASS_AVAILABLE_IOS的宏定義,內(nèi)部也是通過__attribute__實現(xiàn)的。
__attribute__((attribute1, attribute2));
下面是一些__attribute__的常用屬性,更完整的屬性列表可以到llvm的官網(wǎng)查看。

objc_subclassing_restricted
objc_subclassing_restricted屬性表示被修飾的類不能被其他類繼承,否則會報下面的錯誤。
__attribute__((objc_subclassing_restricted))
@interface TestObject : NSObject
@property (nonatomic, strong) NSObject *object;
@property (nonatomic, assign) NSInteger age;
@end
@interface Child : TestObject
@end
錯誤信息:
Cannot subclass a class that was declared with the 'objc_subclassing_restricted' attribute
objc_requires_super
objc_requires_super屬性表示子類必須調(diào)用被修飾的方法super,否則報黃色警告。
@interface TestObject : NSObject
- (void)testMethod __attribute__((objc_requires_super));
@end
@interface Child : TestObject
@end
警告信息:(不報錯)
Method possibly missing a [super testMethod] call
constructor / destructor
constructor屬性表示在main函數(shù)執(zhí)行之前,可以執(zhí)行一些操作。destructor屬性表示在main函數(shù)執(zhí)行之后做一些操作。constructor的執(zhí)行時機是在所有load方法都執(zhí)行完之后,才會執(zhí)行所有constructor屬性修飾的函數(shù)。
__attribute__((constructor)) static void beforeMain() {
NSLog(@"before main");
}
__attribute__((destructor)) static void afterMain() {
NSLog(@"after main");
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"execute main");
}
return 0;
}
執(zhí)行結(jié)果:
debug-objc[23391:1143291] before main
debug-objc[23391:1143291] execute main
debug-objc[23391:1143291] after main
在有多個constructor或destructor屬性修飾的函數(shù)時,可以通過設(shè)置優(yōu)先級來指定執(zhí)行順序。格式是__attribute__((constructor(101)))的方式,在屬性后面直接跟優(yōu)先級。
__attribute__((constructor(103))) static void beforeMain3() {
NSLog(@"after main 3");
}
__attribute__((constructor(101))) static void beforeMain1() {
NSLog(@"after main 1");
}
__attribute__((constructor(102))) static void beforeMain2() {
NSLog(@"after main 2");
}
在constructor中根據(jù)優(yōu)先級越低,執(zhí)行順序越高。而destructor則相反,優(yōu)先級越高則執(zhí)行順序越高。
overloadable
overloadable屬性允許定義多個同名但不同參數(shù)類型的函數(shù),在調(diào)用時編譯器會根據(jù)傳入?yún)?shù)類型自動匹配函數(shù)。這個有點類似于C++的函數(shù)重載,而且都是發(fā)生在編譯期的行為。
__attribute__((overloadable)) void testMethod(int age) {}
__attribute__((overloadable)) void testMethod(NSString *name) {}
__attribute__((overloadable)) void testMethod(BOOL gender) {}
int main(int argc, const char * argv[]) {
@autoreleasepool {
testMethod(18);
testMethod(@"lxz");
testMethod(YES);
}
return 0;
}
objc_runtime_name
objc_runtime_name屬性可以在編譯時,將Class或Protocol指定為另一個名字,并且新名字不受命名規(guī)范制約,可以以數(shù)字開頭。
__attribute__((objc_runtime_name("TestObject")))
@interface Object : NSObject
@end
NSLog(@"%@", NSStringFromClass([TestObject class]));
執(zhí)行結(jié)果:
TestObject
這個屬性可以用來做代碼混淆,例如寫一個宏定義,宏定義內(nèi)部實現(xiàn)混淆邏輯。例如通過MD5對Object做混淆,32位的混淆結(jié)果就是497031794414a552435f90151ac3b54b,誰能看出來這是什么類。如果怕彩虹表匹配出來,再增加加鹽邏輯。
cleanup
通過cleanup屬性,可以指定給一個變量,當(dāng)變量作用域結(jié)束時執(zhí)行一個函數(shù),而不是變量被釋放時。在指定的函數(shù)中,可以傳入一個形參,參數(shù)就是cleanup修飾的變量,形參是一個地址。
static void releaseBefore(NSObject **object) {
NSLog(@"%@", *object);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
TestObject *object __attribute__((cleanup(releaseBefore))) = [[TestObject alloc] init];
}
return 0;
}
如果遇到同一個代碼塊中,同時出現(xiàn)多個cleanup屬性時,在代碼塊作用域結(jié)束時,會以添加的順序進行調(diào)用。
并且cleanup只適用于修飾局部變量,如果是靜態(tài)變量則不能使用cleanup,會報下面的錯誤。
static NSObject *__attribute__((cleanup(releaseBefore)))object;
// 警告
'cleanup' attribute only applies to local variables
unused
還有一個屬性很實用,在項目里經(jīng)常會有未使用的變量,會報一個黃色警告。有時候可能會通過其他方式獲取這個對象,所以不想出現(xiàn)這個警告,可以通過unused屬性消除這個警告。
NSObject *object __attribute__((unused)) = [[NSObject alloc] init];
系統(tǒng)定義
在系統(tǒng)里也大量使用了__attribute__關(guān)鍵字,只不過系統(tǒng)不會直接在外部使用__attribute__,一般都是將其定義為宏定義,以宏定義的形式出現(xiàn)在外面。
// NSLog
FOUNDATION_EXPORT void NSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2) NS_NO_TAIL_CALL;
#define NS_FORMAT_FUNCTION(F,A) __attribute__((format(__NSString__, F, A)))
// 必須調(diào)用父類的方法
#define NS_REQUIRES_SUPER __attribute__((objc_requires_super))
// 指定初始化方法,必須直接或間接調(diào)用修飾的方法
#define NS_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
ORM
對象關(guān)系映射(Object Relational Mapping),簡稱ORM,用于面向?qū)ο笳Z言中不同系統(tǒng)數(shù)據(jù)之間的轉(zhuǎn)換。
可以通過對象關(guān)系映射來實現(xiàn)JSON轉(zhuǎn)模型,使用比較多的是Mantle、MJExtension、YYKit、JSONModel等框架,這些框架在進行轉(zhuǎn)換的時候,都是使用Runtime的方式實現(xiàn)的。
Mantle使用和MJExtension有些類似,只不過MJExtension使用起來更加方便。Mantle在使用時主要是通過繼承的方式處理,而MJExtension是通過Category處理,代碼依賴性更小,無侵入性。
性能評測
這些第三方中Mantle功能最強大,但是太臃腫,使用起來性能比其他第三方都差一些。JSONModel、MJExtension這些第三方幾乎都在一個水平級,YYKit相對來說性能可以比肩手寫賦值代碼,性價比最高。
對于模型轉(zhuǎn)換需求不是太大的工程來說,盡量用YYKit來進行轉(zhuǎn)換性能會更好一些。功能可能略遜于MJExtension,我個人還是比較習(xí)慣用MJExtension。
實現(xiàn)思路
也可以自己實現(xiàn)模型轉(zhuǎn)換的邏輯,以字典轉(zhuǎn)模型為例,大體邏輯如下:
- 創(chuàng)建一個
Category用來做模型轉(zhuǎn)換,對外提供方法并傳入字典對象。 - 通過
Runtime對應(yīng)的函數(shù),獲取屬性列表并遍歷,根據(jù)屬性名從字典中取出對應(yīng)的對象。 - 通過
KVC將從字典中取出的值,賦值給對象。 - 有時候會遇到多層嵌套的情況,例如字典包含數(shù)組,數(shù)組中還是一個字典。這種情況就可以做判斷,如果模型對象是數(shù)組則取出字典對應(yīng)字段的數(shù)組,然后遍歷數(shù)組再調(diào)用字典賦值的方法。
下面簡單實現(xiàn)了一個字典轉(zhuǎn)模型的代碼,通過Runtime遍歷屬性列表,并根據(jù)屬性名取出字典中的對象,然后通過KVC進行賦值操作。調(diào)用方式和MJExtension、YYModel類似,直接通過模型類調(diào)用類方法即可。如果想在其他類中也使用的話,應(yīng)該把下面的實現(xiàn)寫在NSObject的Category中,這樣所有類都可以調(diào)用。
// 調(diào)用部分
NSDictionary *dict = @{@"name" : @"lxz",
@"age" : @18,
@"gender" : @YES};
TestObject *object = [TestObject objectWithDict:dict];
// 實現(xiàn)代碼
@interface TestObject : NSObject
@property (nonatomic, copy ) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) BOOL gender;
+ (instancetype)objectWithDict:(NSDictionary *)dict;
@end
@implementation TestObject
+ (instancetype)objectWithDict:(NSDictionary *)dict {
return [[TestObject alloc] initWithDict:dict];
}
- (instancetype)initWithDict:(NSDictionary *)dict {
self = [super init];
if (self) {
unsigned int count = 0;
objc_property_t *propertys = class_copyPropertyList([self class], &count);
for (int i = 0; i < count; i++) {
objc_property_t property = propertys[i];
const char *name = property_getName(property);
NSString *nameStr = [[NSString alloc] initWithUTF8String:name];
id value = [dict objectForKey:nameStr];
[self setValue:value forKey:nameStr];
}
free(propertys);
}
return self;
}
@end
通過Runtime可以獲取到對象的Method List、Property List等,不只可以用來做字典模型轉(zhuǎn)換,還可以做很多工作。例如還可以通過Runtime實現(xiàn)自動歸檔和反歸檔,下面是自動進行歸檔操作。
// 1.獲取所有的屬性
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([NJPerson class], &count);
// 遍歷所有的屬性進行歸檔
for (int i = 0; i < count; i++) {
// 取出對應(yīng)的屬性
Ivar ivar = ivars[i];
const char * name = ivar_getName(ivar);
// 將對應(yīng)的屬性名稱轉(zhuǎn)換為OC字符串
NSString *key = [[NSString alloc] initWithUTF8String:name];
// 根據(jù)屬性名稱利用KVC獲取數(shù)據(jù)
id value = [self valueForKeyPath:key];
[encoder encodeObject:value forKey:key];
}
free(ivars);
我寫了一個簡單的Category,可以自動實現(xiàn)NSCoding、NSCopying協(xié)議。這是開源地址:EasyNSCoding
Runtime面試題
題1
下面的代碼輸出什么?
@implementation Son : Father
- (id)init {
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
答案:都輸出Son。
第一個NSLog輸出Son肯定是不用說的。
第二個輸出中,[super class]會被轉(zhuǎn)換為下面代碼。
struct objc_super objcSuper = {
self,
class_getSuperclass([self class]),
};
id (*sendSuper)(struct objc_super*, SEL) = (void *)objc_msgSendSuper;
sendSuper(&objcSuper, @selector(class));
super的調(diào)用會被轉(zhuǎn)換為objc_msgSendSuper的調(diào)用,并傳入一個objc_super類型的結(jié)構(gòu)體。結(jié)構(gòu)體有兩個參數(shù),第一個就是接受消息的對象,第二個是[super class]對應(yīng)的父類。
struct objc_super {
__unsafe_unretained _Nonnull id receiver;
__unsafe_unretained _Nonnull Class super_class;
};
由此可知,雖然調(diào)用的是[super class],但是接受消息的對象還是self。然后來到父類Father的class方法中,輸出self對應(yīng)的類Son。
題2
下面代碼的結(jié)果?
BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
答案:
除了第一個是YES,其他三個都是NO。
在推測結(jié)果之前,首先要明白兩個問題。isKindOfClass和isMemberOfClass的區(qū)別是什么?
isKindOfClass:class,調(diào)用該方法的對象所屬的類,繼承者鏈中包含傳入的class則返回YES。
isMemberOfClass:class,調(diào)用改方法的對象所屬的類,必須是傳入的class則返回YES。
我們從Runtime源碼的角度來分析一下結(jié)果。
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
平時開發(fā)過程中只會接觸到對象方法的isKindOfClass和isMemberOfClass,但是在NSObject類中還隱式的實現(xiàn)了類方法版本。不只這兩個方法,其他NSObject中的對象方法,都有其對應(yīng)的類方法版本。因為在OC中,類和元類也都是對象。這四個調(diào)用由于都是類對象發(fā)起調(diào)用的,所以最終執(zhí)行的都是類方法版本。
先把Runtime的對象模型拿出來,方便后面的分析。

第一次調(diào)用方是NSObject類對象,調(diào)用isKindOfClass方法傳入的也是類對象。因為調(diào)用類的class方法,會把類自身直接返回,所以還是類對象自己。
然后進入到for循環(huán)中,會從NSObject的元類開始遍歷,所以第一次NSObject meta class != NSObject class,匹配失敗。第二次循環(huán)將tcls設(shè)置為superclass的NSObject class,NSObject class == NSObject class,匹配成功。
NSObject能匹配成功,是因為這個類比較特殊,在第二次獲取superclass的時候,NSObject元類的superclass就是NSObject的類對象,所以會匹配成功。而其他三種匹配,則都會失敗,各位同學(xué)可以去自己分析一下剩下三種。
題3
下面的代碼會?Compile Error / Runtime Crash / NSLog…?
@interface NSObject (Sark)
+ (void)foo;
@end
@implementation NSObject (Sark)
- (void)foo {
NSLog(@"IMP: -[NSObject (Sark) foo]");
}
@end
// 測試代碼
[NSObject foo];
[[NSObject new] performSelector:@selector(foo)];
答案:
全都正常輸出,編譯和運行都沒有問題。
這道題和上一道題很相似,第二個調(diào)用肯定沒有問題,第一個調(diào)用后會從元類中查找方法,然而方法并不在元類中,所以找元類的superclass。方法定義在是NSObject的Category,由于NSObject的對象模型比較特殊,元類的superclass是類對象,所以從類對象中找到了方法并調(diào)用。
題4
下面的代碼會?Compile Error / Runtime Crash / NSLog…?
@interface Sark : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Sark
- (void)speak {
NSLog(@"my name's %@", self.name);
}
@end
// 測試代碼
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Sark class];
void *obj = &cls;
[(__bridge id)obj speak];
}
@end
答案:
正常執(zhí)行,不會導(dǎo)致Crash。
執(zhí)行[Sark class]后獲取到類對象,然后通過obj指針指向獲取到的類對象首地址,這就構(gòu)成了對象的基本結(jié)構(gòu),可以進行正常調(diào)用。
原題出處
Sunnyxx-神經(jīng)病院objc runtime入院考試
題5
為什么MRC下沒有weak?
其實MRC下并不是沒有weak,在MRC環(huán)境下也可以通過Runtime源碼調(diào)用weak源碼的。weak源碼定義在Private Headers私有文件夾下,需要引入#import "objc-internal.h"文件。
以以下ARC的源碼為例,定義了一個TestObject類型的對象,并用一個weak指針指向已創(chuàng)建對象。
int main(int argc, const char * argv[]) {
@autoreleasepool {
TestObject *object = [[TestObject alloc] init];
__weak TestObject *newObject = object;
}
return 0;
}
這段代碼會被編譯器轉(zhuǎn)移為下面代碼,這段代碼中的兩個函數(shù)就是weak的實現(xiàn)函數(shù),在MRC下也可以調(diào)用這兩個函數(shù)。
objc_initWeak(&newObject, object);
objc_destroyWeak(&newObject);
題6
相同的一個類,創(chuàng)建不同的對象,怎樣實現(xiàn)指定的某個對象在dealloc時打印一段文字?
這個問題最簡單的方法就是在類的.h文件里,定義一個標(biāo)記屬性,如果屬性被賦值為YES,則在dealloc中打印文字。但是,這種實現(xiàn)方式顯然不是面試官想要的,會被直接pass~
可以參考KVO的實現(xiàn)方案,在運行時動態(tài)創(chuàng)建一個類,這個類是對象的子類,將新創(chuàng)建類的dealloc實現(xiàn)指向自定義的IMP,并在IMP中打印一段文字。將對象的isa設(shè)置為新創(chuàng)建的類,當(dāng)執(zhí)行dealloc方法時就會執(zhí)行isa所指向的新類。
思考
小問題
什么叫做技術(shù)大牛,怎樣就表示技術(shù)強?
我前段時間看過一句話,我感覺可以解釋上面的問題:“市面上所有應(yīng)用的功能,產(chǎn)品提出來我都能做”。
這句話并不夠全面,應(yīng)該不只是做出來,而是更好的做出來。這個好要從很多方面去評估,性能、可維護性、完成時間、產(chǎn)品效果等,如果這些都做的很好,那足以證明這個人技術(shù)很強大。
Runtime有什么用?
Runtime是比較偏底層的,但是研究這么深有什么用嗎,有什么實際意義嗎?
Runtime當(dāng)然是由實際用處的,先不說整個OC都是通過Runtime實現(xiàn)的。例如現(xiàn)在需要實現(xiàn)消息轉(zhuǎn)發(fā)的功能,這時候就需要用到Runtime,或者是攔截方法,也需要用到Method Swizzling,除了這些,還有更多的用法待我們?nèi)グl(fā)掘。
不只是使用,其實最重要的是,通過Runtime了解一個語言的設(shè)計。Runtime中不只是各種函數(shù)調(diào)用,從整體來看,可以明白OC的對象模型是什么樣的。
簡書由于排版的問題,閱讀體驗并不好,布局、圖片顯示、代碼等很多問題。所以建議到我Github上,下載Runtime PDF合集。把所有Runtime文章總計九篇,都寫在這個PDF中,而且左側(cè)有目錄,方便閱讀。

下載地址:Runtime PDF
麻煩各位大佬點個贊,謝謝!??