1. 程式人生 > >runtime介紹及基本使用

runtime介紹及基本使用

1. 概念

        runtime(執行時系統),是一套基於C語言API,包含在 <objc/runtime.h>和<objc/message.h>中,執行時系統的功能是在執行期間(而不是編譯期或其他時機)通過程式碼去動態的操作類(獲取類的內部資訊和動態操作類的成員),如建立一個新類、為某個類新增一個新的方法或者為某個類新增例項變數、屬性,或者交換兩個方法的實現、獲取類的屬性列表、方法列表等和Java中的反射技術類似。

2. 探索

        程式最終執行的是二進位制的可執行檔案,編譯器需要將OC程式碼轉換為執行時程式碼,再將執行時程式碼經過一些處理成最終的二進位制可執行檔案
mian.m
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [[NSObject alloc] init];
    }
    return 0;
}
使用終端命令列切換到mian.m檔案所在的目錄下並執行: clang -rewrite-objc main.m
在mian.m檔案目錄所在的位置會有一個 main.cpp檔案,開啟檔案可以看到OC程式碼都被轉換成執行時runtime程式碼了

runtime.h和message.h中的方法一般以objc_、 class_、 method_、 property_、 ivar_、 protocol_、object_、  sel_等作為字首,用字首表明操作的物件

3.常用功能

1.動態交換兩個方法的實現
2.動態新增物件的成本變數和成員方法
3.獲取某個類的所有成員變數和成員方法
4.實現NSCoding的自動歸檔和自動解檔
5.實現字典和模型的自動轉換
6.為類別新增屬性(我們知道類別是不能擴充套件屬性的,只能擴充套件方法,但可以執行時可以實現,通過為類增加屬性)

4.runtime常用的資料型別

OC原始碼最終會翻譯成執行時程式碼,而runtime是一套C語言API,也就是說OC的資料型別最終也會翻譯成C語言中的資料型別
Objective-C ------->  runtime
類(Class)             objc_class*
id                            objc_object*
方法(Method)       objc_method
變數(Ivar)              objc_ivar*
struct objc_class {
    Class isa;

    Class super_class                                        
    const char *name                                         
    long version                                             
    long info                                                
    long instance_size                                       
    struct objc_ivar_list *ivars                             
    struct objc_method_list **methodLists                    
    struct objc_cache *cache                                 
    struct objc_protocol_list *protocols    
};
typedef struct objc_class* Class;


struct objc_object {
    Class isa;
};
typedef struct objc_object* id;

struct objc_method {
    SEL method_name,       // 方法名
    char* method_types,    // 方法的引數型別
    IMP method_imp         // 方法實現程式碼的指標
};
typedef objc_method Method;

struct objc_ivar {
    char *ivar_name                                          
    char *ivar_type                                          
    int ivar_offset                                         
    int space                                              
}  
typedef struct objc_ivar* Ivar;

5.runtime 常用API

// 建立類對
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes) 

// 新增方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) 

// 新增屬性
BOOL class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *types)

// 註冊類對
void  objc_registerClassPair(Class cls) 

// 向某個物件傳送某個訊息
id objc_msgSend(id self, SEL op, ...)

// 獲取某個類的類方法
Method class_getClassMethod(Class cls, SEL name)

// 獲取某個類的例項方法
Method class_getInstanceMethod(Class cls, SEL name)

// 為類新增一個屬性
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

// 獲取屬性對應的值
id objc_getAssociatedObject(id object, const void *key)

// 獲取例項變數列表
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)

// 獲取方法的型別(方法的簽名,返回值型別,引數型別)
const char *method_getTypeEncoding(Method m)

6.使用runtime

 示例1:交換兩個方法的實現

<span style="font-weight: normal;">#import <Foundation/Foundation.h>
@interface Person : NSObject

@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *address;
@property (assign, nonatomic)int age;
@property (assign, nonatomic)double hight;
@property (assign, nonatomic)BOOL gender;

+ (void)run;
- (void)study;
@end
//--------------------------------------------------------------------------------------
#import "Person.h"
@implementation Person

+ (void)run {
    NSLog(@"run。。。");
}

- (void)study {
    NSLog(@"study...");
}
@end

//--------------------------------------------------------------------------------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Method runMethod = class_getClassMethod([Person class], @selector(run));
        Method studyMethod = class_getInstanceMethod([Person class], @selector(study));
        method_exchangeImplementations(runMethod, studyMethod);
        
        [Person run];  // 列印 study...
        [[[Person alloc] init] study];   // 列印 run。。。
    }
    return 0;
}</span>

例項2:為類新增屬性

分類(類別)是用來擴充套件方法的,不能擴充套件屬性,但並不是說類別中不能寫 @proprty, 如果類別中有 @proprty,意思是說為該屬性生產getter&&setter方法,但是不生成帶下劃線的例項變數。
新建一個類別為列表新增屬性,並重寫getter&&setter
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface Person (AddProperty)

@property (copy, nonatomic) NSString *name;

@end


#import "Person+AddProperty.h"
@implementation Person (AddProperty)
- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name {
    return objc_getAssociatedObject(self, "name");
}
@end

//------------------------------------------------------------------------------
#import <Foundation/Foundation.h>
#import "Person+AddProperty.h"
#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.name = @"小紅";
        
        NSLog(@"使用類別(分類)間接為類新增屬性, person.name = %@", person.name);
    }
    return 0;
}

程式解釋:當程式通過點語法呼叫 person.name = @"小紅" 的時候實際上是執行的[person setName:@"小紅"]; 
即呼叫相應的Set方法,該方法使用runtime動態的為類關聯一個屬性並賦值;當執行person.name 的時候,
實際上是執行[person name]; 即呼叫相應的Get方法,使用runtime獲取關聯的屬性值。
使用類別也是可以做到為類新增屬性的。(當會的知識增多時,就會發現之前認為對的可能都是錯的)

示例3:獲取例項變數列表

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList([Person class], &outCount);
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            const char* name = ivar_getName(ivar);
            const char* type = ivar_getTypeEncoding(ivar);
            
            NSLog(@"%s :%s", name, type);
        }
        free(ivars);
    }
    return 0;
}

2016-07-19 17:48:28.788 Runtime[13864:3404989] _gender :c
2016-07-19 17:48:28.789 Runtime[13864:3404989] _age :i
2016-07-19 17:48:28.789 Runtime[13864:3404989] _address :@"NSString"
2016-07-19 17:48:28.789 Runtime[13864:3404989] _hight :d
Program ended with exit code: 0

示例4:動態建立類並新增方法

使用執行時系統API以動態方式建立類的步驟:
1.新建一個類及元類
2.向這個類新增方法和例項變數
3.註冊新建的類
static void display(id self, SEL _cmd){
    NSLog(@"invoke method with selector %@ on %@ instance", NSStringFromSelector(_cmd), [self class]);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    // 1. 建立一個類對
    Class WidgetClass = objc_allocateClassPair([NSObject class], "Widget", 0);
    
    // 2. 為該類新增一個方法
    class_addMethod(WidgetClass, @selector(display), (IMP)display, "[email protected]:");
    
    // 3. 為該類新增一個例項變數
    class_addIvar(WidgetClass, "height", sizeof(id), rint(log2(sizeof(id))), @encode(id));
    
    // 4. 註冊類對
    objc_registerClassPair(WidgetClass);
    
    // 5. 建立例項變數並賦值、呼叫方法
    id widge = [[WidgetClass alloc] init];
    [widge setValue:@(15) forKey:[NSString stringWithUTF8String:"height"]];
    NSLog(@"Widge instance height = %@", [widge valueForKey:[NSString stringWithUTF8String:"height"]]);
    objc_msgSend(widge, NSSelectorFromString(@"display"));
    
    // 6. 動態方式新增一個屬性
    objc_setAssociatedObject(widge, @"width", @(10), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    // 7. 獲取
    id result = objc_getAssociatedObject(widge, @"width");
    NSLog(@"Widget instance width = %@", result);
    }
    return 0;
}

示例5:歸檔解檔

先來看一個常用的寫法:

#import <Foundation/Foundation.h>
@interface Student : NSObject <NSCoding>

@property (copy, nonatomic) NSString *name;
@property (assign, nonatomic)int age;
@property (assign, nonatomic)double weight;
@property (copy, nonatomic)NSArray *hobby;
@property (copy, nonatomic)NSDictionary *others;

@end

#import "Student.h"

#define knameKey @"name"
#define kageKey @"age"
#define kweightKey @"weight"
#define khobbyKey @"hobby"
#define kothersKey @"others"

@implementation Student
- (void)encodeWithCoder:(NSCoder *)aCoder {
    [aCoder encodeObject:_name forKey:knameKey];
    [aCoder encodeInt:_age forKey:kageKey];
    [aCoder encodeDouble:_weight forKey:kweightKey];
    [aCoder encodeObject:_hobby forKey:khobbyKey];
    [aCoder encodeObject:_others forKey:kothersKey];
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        _name = [aDecoder decodeObjectForKey:knameKey];
        _age = [aDecoder decodeIntForKey:kageKey];
        _weight = [aDecoder decodeDoubleForKey:kweightKey];
        _hobby = [aDecoder decodeObjectForKey:khobbyKey];
        _others = [aDecoder decodeObjectForKey:kothersKey];
    }
    
    return self;
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    Student *student = [[Student alloc] init];
    student.name = @"小紅";
    student.age = 25;
    student.weight = 100.5;
    student.hobby = @[@"吃", @"喝", @"玩", @"樂"];
    student.others = @{@"phone": @"1234567890", @"wechat": @"123456"};
    
    NSString *path = @"/Users/macmini/Documents/Test/Student.plist";
    [NSKeyedArchiver archiveRootObject:student toFile:path];
    
    Student *xiaoming = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
    NSLog(@"xiaoming:%@", xiaoming);

    }
    return 0;
}	

普通的寫法每個類如果要歸檔解檔的話都要實現NSCoding的協議方法,該方法的實現都很類似,可以使用runtime進行簡化操作,儘可能的達到至簡
#import "Student.h"
#import <objc/runtime.h>

@implementation Student

// 獲取所有成員變數進行迴圈編碼
- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int outCount = 0;
    Ivar *ivars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i++) {
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivars[i])];
        id value = [self valueForKey:key];
        
        [aCoder encodeObject:value forKey:key];
    }
    
    free(ivars);
}

- (id)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i++) {
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivars[i])];
            id value = [self valueForKey:key];
            
            [self setValue:value forKey:key];
        }
        
        free(ivars);
    }
    
    return self;
}

@end
普通寫法每個類都要寫一次,使用runtime雖然每個類都要寫一次,但是程式碼都是完全一樣的,可以直接貼上複製,
既然都是一樣的,我們可以使用類別(分類)給NSObject增加這兩個方法,這樣就能簡化程式碼
//該實現也實現了對父類屬性進行歸檔解檔的實現
#import "NSObject+Archive.h"
#import <objc/runtime.h>

@implementation NSObject (Archive)

// 先對當前類進行編碼,然後對父類進行編碼,如果父類是NSObject就結束編碼
- (void)encode:(NSCoder *)aCoder {
    Class clazz = self.class;
    while (clazz && clazz != [NSObject class]) {
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList(clazz, &outCount);
        for (int i = 0; i < outCount; i++) {
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivars[i])];
            id value = [self valueForKey:key];
            
            [aCoder encodeObject:value forKey:key];
        }
        
        free(ivars);
        clazz = [clazz superclass];
    }
}

- (void)decode:(NSCoder *)aDecoder {
    Class clazz = self.class;
    while (clazz && clazz != [NSObject class]) {
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList(clazz, &outCount);
        for (int i = 0; i < outCount; i++) {
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivars[i])];
            id value = [aDecoder decodeObjectForKey:key];
            
            [self setValue:value forKey:key];
        }
        
        free(ivars);
        clazz = [clazz superclass];
    }
}

@end
#import <Foundation/Foundation.h>
#import "Person.h"

// 繼承Person類
@interface Student : Person <NSCoding>

@property (copy, nonatomic) NSString *name;
@property (assign, nonatomic)double weight;
@property (copy, nonatomic)NSArray *hobby;
@property (copy, nonatomic)NSDictionary *others;

@end


#import "Student.h"
#import "NSObject+Archive.h"
#import <objc/runtime.h>

@implementation Student
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        [self decode:aDecoder];
    }
    
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [self encode:aCoder];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    Student *student = [[Student alloc] init];
    student.name = @"小紅";
    student.age = 25;
    student.weight = 100.5;
    student.hobby = @[@"吃", @"喝", @"玩", @"樂"];
    student.others = @{@"phone": @"1234567890", @"wechat": @"123456"};
    
    student.address = @"父類屬性address";
    student.hight = 180.5;
    student.gender = YES;
    
    NSString *path = @"/Users/macmini/Documents/Test/Student.plist";
    [NSKeyedArchiver archiveRootObject:student toFile:path];
    
    Student *xiaoming = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
    NSLog(@"xiaoming:%@", xiaoming);
    }
    return 0;
}	

該方式可以簡化每個類中歸檔和解檔的程式碼量,但仍可以進行再簡化,就是將NSCoding的實現定義成巨集
建立一個.h檔案
#ifndef Coding_h
#define Coding_h

#import "NSObject+Archive.h"

#define CodingImplemention \
- (instancetype)initWithCoder:(NSCoder *)aDecoder {\
if (self = [super init]) {\
    [self decode:aDecoder];\
}\
    return self;\
}\
\
- (void)encodeWithCoder:(NSCoder *)aCoder {\
    [self encode:aCoder];\
}

#endif /* Coding_h */
#import "Student.h"
#import "Coding.h"
@implementation Student

CodingImplemention

@end
在Student.m 檔案中只需一個單詞即可實現歸檔解檔,可以看到已經達到至簡了示例6:使用runtime將字典轉為模型
字典轉模型需要考慮三種特殊情況
1.當字典中的key和模型的屬性匹配不上
2.模型中巢狀模型
3.陣列中的元素是模型


第一種情況分:當字典中欄位多個類中的屬性時,不用做任何處理,因為runtime是獲取類中的所有 屬性並迴圈的,字典中多的就不用管了。當類的欄位多於字典中的欄位,根據該欄位去屬性中獲取值如果獲取不到就繼續下次迴圈;
第二種情況:利用runtime的ivar_getTypeEncoding方法獲取例項變數的資料型別,如果是自定義的類也需要呼叫轉換
第三種情況:第二種情況能夠判斷資料型別,就可以知道是否為陣列,但是我們不知道數組裡面的資料型別,我們可以提供一個函式,讓使用者指定陣列元素的資料型別

@interface School : NSObject

@property (copy, nonatomic)NSString * ID;
@property (copy, nonatomic)NSString * name;

@end

#import "School.h"

@implementation School

@end
#import <Foundation/Foundation.h>

@interface Address : NSObject

@property (copy, nonatomic)NSString * ID;
@property (copy, nonatomic)NSString * address;

@end

#import "Address.h"

@implementation Address

@end
#import <Foundation/Foundation.h>
#import "School.h"

@interface User : NSObject

@property (copy, nonatomic)NSString * ID;
@property (copy, nonatomic)NSString * name;
@property (assign, nonatomic)int age;
@property (strong, nonatomic)School *school;
@property (copy, nonatomic)NSArray *address;

@end

#import "User.h"

@implementation User

- (NSDictionary *)eleTypeForArray {
    return @{@"address": @"Address"};
}

@end
#import <Foundation/Foundation.h>

@interface NSObject (Dict2Model)

- (void)setDict:(NSDictionary *)dict;
+ (instancetype)initWithDict:(NSDictionary *)dict;
- (NSDictionary *)eleTypeForArray;

@end


#import "NSObject+Dict2Model.h"
#import <objc/runtime.h>

@implementation NSObject (Dict2Model)

- (void)setDict:(NSDictionary *)dict {
    Class clazz = self.class;
    while (clazz && clazz != [NSObject class]) {
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList(clazz, &outCount);
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            key = [key substringFromIndex:1];  // 去掉例項變數中的下劃線
            
            id value = dict[key];
            NSLog(@"%d: key:%@  value:%@", i, key, value);
            
            // 1. 如果類的例項變數多於字典中key
            if (value == nil) {
                continue;
            }
            
            // 2. 例項變數型別以@開頭並且字首不是NS開頭的(排除系統類) "@Student"
            NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
            NSRange range = [type rangeOfString:@"@"];
            if (range.location != NSNotFound) {
                type = [type substringWithRange:NSMakeRange(2, type.length - 3)];
                if (![type hasPrefix:@"NS"]) {
                    Class class = NSClassFromString(type);
                    value = [class initWithDict:value];
                } else if ([type isEqualToString:@"NSArray"]) {
                    NSArray *array = (NSArray *)value;
                    NSMutableArray *mArray = [NSMutableArray array];
                    
                    id class;
                    if ([self respondsToSelector:@selector(eleTypeForArray)]) {
                        NSString *eleTypeStr = [[self eleTypeForArray] objectForKey:key];
                        class = NSClassFromString(eleTypeStr);
                    } else {
                        NSLog(@"陣列型別不明確!");
                        return;
                    }
                    
                    for (int i = 0; i < array.count; i++) {
                        id obj = [class initWithDict:value[i]];
                        [mArray addObject:obj];
                    }
                    value = mArray;
                }
            }
            
            [self setValue:value forKeyPath:key];
        }
        
        free(ivars);
        clazz = [clazz superclass];
    }
}

+ (instancetype)initWithDict:(NSDictionary *)dict {
    NSObject *obj = [[self alloc] init];
    [obj setDict:dict];
    
    return obj;
}
@end
Student.json
{
    "name" : "Tom",
    "age" : 20,
    "weight" : "181",
    "school":{
        "ID":1,
        "name":"北京大學"
    },
    "address" : [
                  {
                   "ID":1,
                   "address":"上海市"
                  },
                  {
                     "ID":2,
                     "address":"北京市"
                  }
               ]
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSData *jsonData = [NSData dataWithContentsOfFile:@"/Users/macmini/Documents/Test/RuntimeWidget/RuntimeWidget/Student.json"];
	NSDictionary *userDict = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:NULL];
	User *user = (User *)[User initWithDict:userDict];
	School *school = user.school;
	Address *address = user.address[0];
	NSLog(@"User: %@, %d, %@, %@", user.name, user.age, school.name, address.address);
    }
    return 0;
}
 2016-07-21 08:57:26.089 Runtime[15421:3827096] User: Tom, 20, 北京大學, 上海市
Program ended with exit code: 0