探秘Runtime - Runtime的應(yīng)用

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


attribute

__attribute__是一套編譯器指令,被GNULLVM編譯器所支持,允許對于__attribute__增加一些參數(shù),做一些高級檢查和優(yōu)化。

__attribute__的語法是,在后面加兩個括號,然后寫屬性列表,屬性列表以逗號分隔。在iOS中,很多例如NS_CLASS_AVAILABLE_IOS的宏定義,內(nèi)部也是通過__attribute__實現(xiàn)的。

__attribute__((attribute1, attribute2));

下面是一些__attribute__的常用屬性,更完整的屬性列表可以到llvm的官網(wǎng)查看。

官網(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

在有多個constructordestructor屬性修飾的函數(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屬性可以在編譯時,將ClassProtocol指定為另一個名字,并且新名字不受命名規(guī)范制約,可以以數(shù)字開頭。

__attribute__((objc_runtime_name("TestObject")))
@interface Object : NSObject
@end

NSLog(@"%@", NSStringFromClass([TestObject class]));

執(zhí)行結(jié)果:
TestObject

這個屬性可以用來做代碼混淆,例如寫一個宏定義,宏定義內(nèi)部實現(xiàn)混淆邏輯。例如通過MD5Object做混淆,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。

YYKit作者評測

實現(xiàn)思路

也可以自己實現(xiàn)模型轉(zhuǎn)換的邏輯,以字典轉(zhuǎn)模型為例,大體邏輯如下:

  1. 創(chuàng)建一個Category用來做模型轉(zhuǎn)換,對外提供方法并傳入字典對象。
  2. 通過Runtime對應(yīng)的函數(shù),獲取屬性列表并遍歷,根據(jù)屬性名從字典中取出對應(yīng)的對象。
  3. 通過KVC將從字典中取出的值,賦值給對象。
  4. 有時候會遇到多層嵌套的情況,例如字典包含數(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)寫在NSObjectCategory中,這樣所有類都可以調(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 ListProperty 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)NSCodingNSCopying協(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。然后來到父類Fatherclass方法中,輸出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é)果之前,首先要明白兩個問題。isKindOfClassisMemberOfClass的區(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ā)過程中只會接觸到對象方法的isKindOfClassisMemberOfClass,但是在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è)置為superclassNSObject 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。方法定義在是NSObjectCategory,由于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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容