探祕Runtime - Runtime的應用

該文章屬於<簡書 — 劉小壯>原創,轉載請註明:

<簡書 — 劉小壯> https://www.jianshu.com/p/4a22a39b69c5html


博客配圖


attribute

__attribute__是一套編譯器指令,被GNULLVM編譯器所支持,容許對於__attribute__增長一些參數,作一些高級檢查和優化。ios

__attribute__的語法是,在後面加兩個括號,而後寫屬性列表,屬性列表以逗號分隔。在iOS中,不少例如NS_CLASS_AVAILABLE_IOS的宏定義,內部也是經過__attribute__實現的。git

__attribute__((attribute1, attribute2));

下面是一些__attribute__的經常使用屬性,更完整的屬性列表能夠到llvm的官網查看。github

官網示例

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屬性表示子類必須調用被修飾的方法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函數執行以前,能夠執行一些操做。destructor屬性表示在main函數執行以後作一些操做。constructor的執行時機是在全部load方法都執行完以後,纔會執行全部constructor屬性修飾的函數。app

__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;
}

執行結果:
debug-objc[23391:1143291] before main
debug-objc[23391:1143291] execute main
debug-objc[23391:1143291] after main

在有多個constructordestructor屬性修飾的函數時,能夠經過設置優先級來指定執行順序。格式是__attribute__((constructor(101)))的方式,在屬性後面直接跟優先級。框架

__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中根據優先級越低,執行順序越高。而destructor則相反,優先級越高則執行順序越高。函數

overloadable

overloadable屬性容許定義多個同名但不一樣參數類型的函數,在調用時編譯器會根據傳入參數類型自動匹配函數。這個有點相似於C++的函數重載,並且都是發生在編譯期的行爲。佈局

__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指定爲另外一個名字,而且新名字不受命名規範制約,能夠以數字開頭。

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

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

執行結果:
TestObject

這個屬性能夠用來作代碼混淆,例如寫一個宏定義,宏定義內部實現混淆邏輯。例如經過MD5Object作混淆,32位的混淆結果就是497031794414a552435f90151ac3b54b,誰能看出來這是什麼類。若是怕彩虹表匹配出來,再增長加鹽邏輯。

cleanup

經過cleanup屬性,能夠指定給一個變量,當變量釋放以前執行一個函數。指定的函數執行的時間,是在dealloc以前的。在指定的函數中,能夠傳入一個形參,參數就是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;
}

若是遇到同一個代碼塊中,同時出現多個cleanup屬性時,在代碼塊做用域結束時,會以添加的順序進行調用。

unused

還有一個屬性很實用,在項目裏常常會有未使用的變量,會報一個黃色警告。有時候可能會經過其餘方式獲取這個對象,因此不想出現這個警告,能夠經過unused屬性消除這個警告。

NSObject *object __attribute__((unused)) = [[NSObject alloc] init];

系統定義

在系統裏也大量使用了__attribute__關鍵字,只不過系統不會直接在外部使用__attribute__,通常都是將其定義爲宏定義,以宏定義的形式出如今外面。

// 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)))

// 必須調用父類的方法
#define NS_REQUIRES_SUPER __attribute__((objc_requires_super))

// 指定初始化方法,必須直接或間接調用修飾的方法
#define NS_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))

ORM

對象關係映射(Object Relational Mapping),簡稱ORM,用於面嚮對象語言中不一樣系統數據之間的轉換。 能夠經過對象關係映射來實現JSON轉模型,使用比較多的是MantleMJExtensionYYKitJSONModel等框架,這些框架在進行轉換的時候,都是使用Runtime的方式實現的。

Mantle使用和MJExtension有些相似,只不過MJExtension使用起來更加方便。Mantle在使用時主要是經過繼承的方式處理,而MJExtension是經過Category處理,代碼依賴性更小,無侵入性。

性能評測

這些第三方中Mantle功能最強大,可是太臃腫,使用起來性能比其餘第三方都差一些。JSONModelMJExtension這些第三方几乎都在一個水平級,YYKit相對來講性能能夠比肩手寫賦值代碼,性價比最高。

對於模型轉換需求不是太大的工程來講,儘可能用YYKit來進行轉換性能會更好一些。功能可能略遜於MJExtension,我我的仍是比較習慣用MJExtension

YYKit做者評測

實現思路

也能夠本身實現模型轉換的邏輯,以字典轉模型爲例,大致邏輯以下:

  1. 建立一個Category用來作模型轉換,對外提供方法並傳入字典對象。
  2. 經過Runtime對應的函數,獲取屬性列表並遍歷,根據屬性名從字典中取出對應的對象。
  3. 經過KVC將從字典中取出的值,賦值給對象。
  4. 有時候會遇到多層嵌套的狀況,例如字典包含數組,數組中仍是一個字典。這種狀況就能夠作判斷,若是模型對象是數組則取出字典對應字段的數組,而後遍歷數組再調用字典賦值的方法。

下面簡單實現了一個字典轉模型的代碼,經過Runtime遍歷屬性列表,並根據屬性名取出字典中的對象,而後經過KVC進行賦值操做。調用方式和MJExtensionYYModel相似,直接經過模型類調用類方法便可。若是想在其餘類中也使用的話,應該把下面的實現寫在NSObjectCategory中,這樣全部類均可以調用。

// 調用部分
NSDictionary *dict = @{@"name" : @"lxz",
                       @"age" : @18,
                       @"gender" : @YES};
TestObject *object = [TestObject objectWithDict:dict];

// 實現代碼
@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等,不僅能夠用來作字典模型轉換,還能夠作不少工做。例如還能夠經過Runtime實現自動歸檔和反歸檔,下面是自動進行歸檔操做。

// 1.獲取全部的屬性
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([NJPerson class], &count);
// 遍歷全部的屬性進行歸檔
for (int i = 0; i < count; i++) {
    // 取出對應的屬性
    Ivar ivar = ivars[i];
    const char * name = ivar_getName(ivar);
    // 將對應的屬性名稱轉換爲OC字符串
    NSString *key = [[NSString alloc] initWithUTF8String:name];
    // 根據屬性名稱利用KVC獲取數據
    id value = [self valueForKeyPath:key];
    [encoder encodeObject:value forKey:key];
}
free(ivars);

我寫了一個簡單的Category,能夠自動實現NSCodingNSCopying協議。這是開源地址: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]會被轉換爲下面代碼。

struct objc_super objcSuper = {
    self,
    class_getSuperclass([self class]),
};
id (*sendSuper)(struct objc_super*, SEL) = (void *)objc_msgSendSuper;
sendSuper(&objcSuper, @selector(class));

super的調用會被轉換爲objc_msgSendSuper的調用,並傳入一個objc_super類型的結構體。結構體有兩個參數,第一個就是接受消息的對象,第二個是[super class]對應的父類。

struct objc_super {
    __unsafe_unretained _Nonnull id receiver;
    __unsafe_unretained _Nonnull Class super_class;
};

由此可知,雖然調用的是[super class],可是接受消息的對象仍是self。而後來到父類Fatherclass方法中,輸出self對應的類Son

題2

下面代碼的結果?

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

在推測結果以前,首先要明白兩個問題。isKindOfClassisMemberOfClass的區別是什麼? isKindOfClass:class,調用該方法的對象所屬的類,繼承者鏈中包含傳入的class則返回YESisMemberOfClass:class,調用改方法的對象所屬的類,必須是傳入的class則返回YES

咱們從Runtime源碼的角度來分析一下結果。

+ (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;
}

平時開發過程當中只會接觸到對象方法的isKindOfClassisMemberOfClass,可是在NSObject類中還隱式的實現了類方法版本。不僅這兩個方法,其餘NSObject中的對象方法,都有其對應的類方法版本。由於在OC中,類和元類也都是對象。這四個調用因爲都是類對象發起調用的,因此最終執行的都是類方法版本。

先把Runtime的對象模型拿出來,方便後面的分析。

對象模型

第一次調用方是NSObject類對象,調用isKindOfClass方法傳入的也是類對象。由於調用類的class方法,會把類自身直接返回,因此仍是類對象本身。

而後進入到for循環中,會從NSObject的元類開始遍歷,因此第一次NSObject meta class != NSObject class,匹配失敗。第二次循環將tcls設置爲superclassNSObject classNSObject class == NSObject class,匹配成功。

NSObject能匹配成功,是由於這個類比較特殊,在第二次獲取superclass的時候,NSObject元類的superclass就是NSObject的類對象,因此會匹配成功。而其餘三種匹配,則都會失敗,各位同窗能夠去本身分析一下剩下三種。

題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)];

答案: 全都正常輸出,編譯和運行都沒有問題。

這道題和上一道題很類似,第二個調用確定沒有問題,第一個調用後會從元類中查找方法,然而方法並不在元類中,因此找元類的superclass。方法定義在是NSObjectCategory,因爲NSObject的對象模型比較特殊,元類的superclass是類對象,因此從類對象中找到了方法並調用。

題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

答案: 正常執行,不會致使Crash

執行[Sark class]後獲取到類對象,而後經過obj指針指向獲取到的類對象首地址,這就構成了對象的基本結構,能夠進行正常調用。

原題出處

Sunnyxx-神經病院objc runtime入院考試

題5

爲何MRC下沒有weak

其實MRC下並非沒有weak,在MRC環境下也能夠經過Runtime源碼調用weak源碼的。weak源碼定義在Private Headers私有文件夾下,須要引入#import "objc-internal.h"文件。

以如下ARC的源碼爲例,定義了一個TestObject類型的對象,並用一個weak指針指向已建立對象。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        TestObject *object = [[TestObject alloc] init];
        __weak TestObject *newObject = object;
    }
    return 0;
}

這段代碼會被編譯器轉移爲下面代碼,這段代碼中的兩個函數就是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

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

相關文章
相關標籤/搜索