Runtime(1)

文章:Mike_zh/iOS-Runtime知识点整理ian/Objective-C Runtime 1小时入门教程

 

一、简介

OC 是一门动态语言,所以它会把一些决定工作从编译链接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system)来执行编译后的代码,它是整个 OC 运行框架的一块基石。

Runtime 其实有两个版本:modernlegacy。我们现在用的 Objective-C 2.0 采用的是现行(Modern)版的 Runtime 系统,只能运行在 iOSOS X 10.5 之后的 64 位程序中。而 OS X 较老的 32 位程序仍采用 Objective-C 1 中的 Legacy 版本。

当更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。

Runtime 基本是用 C 和汇编(437 版本开始较多使用 mm 文件,但是仍用 C 语法)实现的,可见苹果为了动态系统的高效而作出了很多努力。runtime源码,苹果和 GNU 各自维护一个开源的 runtime 版本,两个版本在努力的保持一致。

 

二、Runtime 相关的头文件

iossdkusr/include/objc 文件夹下面有这样几个文件

List.h
NSObjCRuntime.h
NSObject.h
Object.h
Protocol.h
a.txt
hashtable.h
hashtable2.h
message.h
module.map
objc-api.h
objc-auto.h
objc-class.h
objc-exception.h
objc-load.h
objc-runtime.h
objc-sync.h
objc.h
runtime.h

都是和运行时相关的头文件,其中主要使用的函数定义在 message.hruntime.h 这两个文件中。 在 message.h 中主要包含了一些向对象发送消息的函数,这是 OC 对象方法调用的底层实现。 runtime.h 是运行时最重要的文件,其中包含了对运行时进行操作的方法。 主要包括:

1、操作对象的类型的定义

/// An opaque type that represents a method in a class definition.  一个类型,代表着类定义中的一个方法
typedef struct objc_method *Method;

/// An opaque type that represents an instance variable.  代表实例(对象)的变量
typedef struct objc_ivar *Ivar;

/// An opaque type that represents a category.  代表一个分类
typedef struct objc_category *Category;

/// An opaque type that represents an Objective-C declared property.  代表OC声明的属性
typedef struct objc_property *objc_property_t;

// Class 代表一个类,它在 objc.h 中这样定义的 typedef struct objc_class *Class;
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

这些类型的定义,对一个类进行了完全的分解,将类定义或者对象的每一个部分都抽象为一个类型 type,对操作一个类属性和方法非常方便。OBJC2_UNAVAILABLE 标记的属性是 Ojective-C 2.0 不支持的,但实际上可以用响应的函数获取这些属性,例如:如果想要获取 Classname 属性,可以按如下方法获取:

Class cls = obj.class;
// NSLog(@"%s", cls->name); // 用这种方法已经不能获取 name 了因为OBJC2_UNAVAILABLE
const char * clsName = class_getName(cls);
NSLog(@"%s", clsName);

 

2、函数的定义

操作对象的方法一般以 object_ 开头
操作类的方法一般以 class_ 开头
操作类或对象的方法的方法一般以 method_ 开头
操作成员变量的方法一般以 ivar_ 开头
操作属性的方法一般以 property_ 开头
操作协议的方法一般以 protocol_ 开头

objc_ 开头的方法,则是 runtime 最终的管家,可以获取内存中类的加载信息、类的列表、关联对象和关联属性等操作。

根据以上的函数的前缀可以大致了解到层级关系。

// 使用 runtime 对当前的应用中加载的类进行打印
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    unsigned int count = 0;
    
    Class * clsList = objc_copyClassList(&count);
    
    for (int i = 0; i < count; i++) {
        const char * clsName = class_getName(clsList[i]);
        NSLog(@"%s", clsName);
    }
}

 

三、技术点
// 简单的定义了一个成员变量和两个属性
@interface Person : NSObject
{
    @private
         CGFloat _height;
}
@property (nonatomic, copy) NSString * name;
@property (nonatomic, assign) NSInteger age;

@end

 

1、获取属性/成员变量列表

使用 class_copyIvarList() 函数获取成员变量的列表,使用 class_copyPropertyList() 函数获取属性列表:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    Class cls = NSClassFromString(@"Person");   // Class cls = Person.class;
    
    unsigned int count = 0;
    // 获取成员变量数组
    Ivar * ivarList = class_copyIvarList(cls, &count);
    for (int i = 0; i < count; i++) {
        // 获取成员变量名
        const char * ivarName = ivar_getName(ivarList[i]); 
        NSLog(@"%s", ivarName);
    }
    
    // 获取属性数组
    objc_property_t * ptyList = class_copyPropertyList(cls, &count);
    for (int i = 0; i < count; i++) {
        const char * ptyName = property_getName(ptyList[i]);
        NSLog(@"%s", ptyName);
    }
}

2018-11-04 17:28:03.905326+0800 Demo[5894:1444503] _height
2018-11-04 17:28:03.905486+0800 Demo[5894:1444503] _name
2018-11-04 17:28:03.905616+0800 Demo[5894:1444503] _age
2018-11-04 17:28:03.905745+0800 Demo[5894:1444503] name
2018-11-04 17:28:03.905877+0800 Demo[5894:1444503] age

从这里就可以看出 @property 做了三件事:

①、生成一个带下划线的成员变量
②、生成这个成员变量的 set 方法
③、生成这个成员变量的 get 方法

因此会输出三个成员变量 _height_age_name。并且从上面可知 ivarList 能够获取到 @property 关键字定义的属性 ,而 propertyList 不能获取到成员变量。即用 ivarList 可以获取到所有的成员变量和属性。

@property (nonatomic, copy, readonly) NSString * name;  // 只读属性

- (NSString *)name
{
    return @"job";
}

2018-11-04 17:52:52.690815+0800 Demo[6025:1474196] _height
2018-11-04 17:52:52.691025+0800 Demo[6025:1474196] _age
2018-11-04 17:52:52.691159+0800 Demo[6025:1474196] name
2018-11-04 17:52:52.691308+0800 Demo[6025:1474196] age

当只读属性 name 重写了 getter 方法时,无论使用 ivarList 还是使用 propertyList 都无法获取到 _name 成员变量。

一个 readonly 的属性,到底是 didSet+set 好,还是重写 getter 好?

大部分的 readonly 的属性是计算型的,依赖于其他属性,因此可以使用 didSet+set,也就是在其他属性的 set 方法内,将只读属性 set。 但是 didSet+set 有时候完全没有必要,不符合懒加载的规则,浪费了计算能力,用重写 getter 的方法好一些。

KVC 时,想要获取全部的成员变量和属性, 怎么办呢?

首先要了解 setValue:forKeyPath: 方法的底层实现:

①、首先去类的方法列表去寻找有没有 setter 方法,如果有,就直接调用 [obj setXX:value]
②、查找有没有成员变量 _XX,如果有 _XX = value
③、查找有没有成员变量 XX,如果有 XX = value
④、如果都没有找到,直接报错。

Terminating app due to uncaught exception 'NSUnknownKeyException', 
reason: '[<Person 0x102bb7388> setValue:forUndefinedKey:]: 
this class is not key value coding-compliant for the key name.'

首先,只读属性为什么要为它赋值呢,因此对它进行 kvc 也不合情理。

另外,对于重写了 getter 的只读属性而言:如果对 propertyList 的属性一次使用 kvc,就会报错,因此为保证代码正常,不能使用 propertyList 的属性进行 kvc

使用 ivaList 时是无法获取到重写了 getter 的只读属性,因此是 kvc 的最佳方案。再者,使用 propertyList 无法获取成员变量 _height,无法对成员变量进行赋值。而使用 ivaList 是可以将需要赋值的成员变量都获取的。

要想不对 _height 成员变量赋值,在 kvc 时又可以这样改进一下,通过 ivarList 获取,去掉 propertyList 中没有的成员变量,这样就过滤掉了 _height

@property (nonatomic, weak) NSTimer * timer;
@property (nonatomic, strong) NSThread * thread;
@property (nonatomic, strong, readonly) AModel * a;  // 自定义对象

{
     unsigned int count = 0;
     objc_property_t * propertyList = class_copyPropertyList(self.class, &count);
    
     for (int i = 0; i < count; i++) {
          NSLog(@"%s", property_getAttributes(propertyList[i]));
     }
}

2018-11-05 15:09:37.839596+0800 Demo[39749:288880] T@"NSTimer",W,N,V_timer
2018-11-05 15:09:37.839692+0800 Demo[39749:288880] T@"NSThread",&,N,V_thread
2018-11-05 15:09:37.839771+0800 Demo[39749:288880] T@"AModel",R,N,V_a

通过 property_getAttributes() 方法获取属性的参数。

 

四、应用场景

1KVC字典转模型

获取属性/成员列表一个重要的应用就是:一次取出模型中的属性/成员变量,根据变量名获取字典中的 key 然后取出对应的 value,使用 setValue: forKeyPath: 方法设置值。

为什么要这样,而不再使用方法 setValuesForKeysWithDictionary:。因为在 setValuesForKeysWithDictionary: 方法内部会执行这样一个过程:

①、遍历字典里面的所有 key,取出 key
②、取出 keyvalue,即 dict[key]
③、使用方法 [setValue:value forKeyPath:key] 给模型的属性/成员变量进行赋值。

因此,开发中经常遇到的字典中的 key 比模型中多时,会出现的 this class is not key-value compliant for ‘xxx’ 这个 bug,是因为模型中没有这个属性/成员变量。当模型中的属性比字典中多时,使用 setValuesForKeysWithDictionary: ,多出来的属性是对象类型时为 null,基本数据类型时会有一个系统默认值(如 int0)。

因此使用逐一为属性赋值的方法进行 KVC

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
     Class cls = Person.class;
     unsigned int count = 0;
    
     Person * person = [[Person alloc] init];
     NSDictionary * dict = @{  @"name" : @"Tom", @"age" : @19, @"height": @175 };
    
     Ivar * ivars = class_copyIvarList(cls, &count);
    
     for (int i = 0; i < count; i++) {
          const char * clsName = ivar_getName(ivars[i]);
          NSString * name = [NSString stringWithUTF8String:clsName];
          NSString * key = [name substringFromIndex:1];  // 去掉'_'
          [person setValue:dict[key] forKey:key];
     }
}

2018-11-04 19:42:16.964474+0800 Demo[6425:1574210] height:175.0000,name:Tom,age:19,time:(null)

使用这种方式进行 kvc,即使字典中的 key 多的时候也不会有 bug

但新的问题出现了,如果模型中的属性比字典中的 key 多便会出现 bug,而且如果多的是对象类型不会有 bug,该属性的值为 null,如果是基本数据类型就会出错 could not set nil as the value for the key ‘xxx’

setObject:forKey: 如果 valuenil 会直接报错;setValue:forKey: 则不会,会赋值 nil。具体可以看文档说明。

解决基础类型被赋值 nilbug:可以在 [setValue:value forKeyPath:key] 方法调用之前取出属性对应的类型,如果类型是基本数据类型,value 替换为默认值(如 int 对应默认值为 0)。

runtime 提供的 ivar_getTypeEncoding() 函数可以获取到属性的类型。Type Encodings

for (int i = 0; i < count; i++) {
     const char * ivarName = ivar_getName(ivars[i]);
     NSString * name = [NSString stringWithUTF8String:ivarName];
     NSString * key  = [name substringFromIndex:1];
    
     const char * coding = ivar_getTypeEncoding(ivars[i]); // 获取类型
     NSString * strCode = [NSString stringWithUTF8String:coding];
     id value = dict[key];

     if ([strCode isEqualToString:@"f"]) {  // 判断类型是否是 float
          value = @(0.0);
     }
    
     [person setValue:value forKey:key];
}

method_getTypeEncoding() 函数可以获取到方法类型编码

{
     Method m = class_getInstanceMethod(self.class, @selector(do:at:on:));
    
     NSLog(@"%s", method_getTypeEncoding(m));
}
- (BOOL)do:(NSString *)something at:(char)place on:(int)count;

2018-11-05 14:42:30.891829+0800 Demo[38588:270099] B32@0:8@16c24i28

property_getAttributes() 函数可以获取到属性的参数。Declared Properties

 

2NSCoding 归档和解档

获取属性/成员列表另外一个重要的应用就是进行归档和解档,其原理和上面的 kvc 基本上一样:

- (void)encodeWithCoder:(NSCoder *)aCoder
{
     unsigned int count = 0;
     Ivar * ivars = class_copyIvarList(self.class, &count);

     for (int i = 0; i < count; i++) {
          const char * ivarName = ivar_getName(ivars[i]);
          NSString * name = [NSString stringWithUTF8String:ivarName];
          NSString * key  = [name substringFromIndex:1];
        
          id value = [self valueForKey:key];  // 取出 key 对应的 value
          [aCoder encodeObject:value forKey:key];   // 编码
     }
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
     if (self = [super init]) {

          unsigned int count = 0;
          Ivar * ivars = class_copyIvarList(self.class, &count);

          for (int i = 0; i < count; i++) {
               const char * ivarName = ivar_getName(ivars[i]);
               NSString * name = [NSString stringWithUTF8String:ivarName];
               NSString * key = [name substringFromIndex:1];
            
               id value = [aDecoder decodeObjectForKey:key];  // 解码
               [self setValue:value forKey:key];  // 设置 key 对应的 value
          }
     }
     return self;    
}

 

3、交换方法实现

交换两个方法的实现一般写在类的 load 方法里面,因为 load 方法会在程序运行前加载一次,而 initialize 方法会在类或者子类第一次使用的时候调用,当有分类的时候会调用多次。

+ (void)load
{
     static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{

          Method orginalMethod = class_getClassMethod([UIImage class], @selector(imageNamed:));
          Method swizzleMethod = class_getClassMethod([UIImage class], @selector(my_imageNamed:));
        
          //方法交换
          method_exchangeImplementations(orginalMethod, swizzleMethod);
     });
}

+ (UIImage *)my_imageNamed:(NSString *)name
{
     return [self my_imageNamed:name];
}

注意的是

①、可以交换的两个方法的参数必须是匹配的,参数的类型一致。
②、如果想在 my_imageNamed: 的内部调用 imageNamed: 方法,此时调用 [self my_imageNamed:name] 实际上是在调用 imageName: 的代码实现。

任何一个方法都有两个重要的属性:SEL 方法的编号,IMP 方法的实现。方法的调用过程实际上是根据 SEL 去寻找 IMP

 

4、类/对象的关联对象

关联对象不是为类/对象添加属性或者成员变量(因为在设置关联后也无法通过 ivarList 或者 propertyList 取得) ,而是为类添加一个相关的对象,通常用于存储类信息,例如存储类的属性列表数组,为将来字典转模型的方便。 例如,将属性的名称存到数组中设置关联

/* 参数 1 : 关联到对象
   参数 2 : 关联的 key,可以是任意类型
   参数 3 : 被关联的对象
   参数 4 : 关联引用的规则
           enum {
                OBJC_ASSOCIATION_ASSIGN = 0,
                OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
                OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
                OBJC_ASSOCIATION_RETAIN = 01401,
                OBJC_ASSOCIATION_COPY = 01403
           };
*/
objc_setAssociatedObject(self, key, value, OBJC_ASSOCIATION_COPY_NONATOMIC);

id value = objc_getAssociatedObject(self, key);

 

5、动态添加方法,拦截未实现的方法

每个类都有继承自 NSObject 的两个类方法

+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;

一个适用于类方法,一个适用于对象方法。

在代码中调用没有实现的方法时,也就是 sel 标识的方法没有实现,都会先调用这两个方法中的一个拦截。 通常的做法是在 resolve 的内部指定 sel 对应的 IMP,从而完成方法的动态创建和调用两个过程,也可以不指定 IMP 打印错误信息后直接返回。

// 每个方法的内部都默认包含两个参数,被称为隐式参数:id self 和 SEL _cmd
void method(id self, SEL _cmd) {

}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
     if ([NSStringFromSelector(sel) isEqualToString:@"doSomething"]) {       
     
          /* 参数 4 : const char *types 方法的类型

             要注意函数至少有 self 和 _cmd 参数,第二个和第三个字符必须是 “@:”。

             如果想要再增加参数,就可以从实现的第三个参数算起:
                 class_addMethod(self, sel, method, "v@:@"); // 多一个对象类型参数增加了 @

                 void method(id self, SEL _cmd, NSString * name) {  }

             返回值:YES if the method was found and added to the receiver, otherwise NO.
          */ 
          class_addMethod(self, sel, method, "v@:");  // 为 sel 指定实现为 method
     }
     return YES;
}

 

6、动态创建一个类

动态创建一个类,为这个类添加成员变量和方法,并创建这个类型的对象:

#import <objc/message.h>

void sayFunction(id self, SEL _cmd, id param) {
    NSLog(@"%ld岁的%@在%@说%@", [object_getIvar(self, class_getInstanceVariable([self class], "_age")) integerValue], object_getIvar(self, class_getInstanceVariable([self class], "_name")), object_getIvar(self, class_getInstanceVariable([self class], "schoolName")), param);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 创建 Student 类。参数 1 : 父类   参数 2 : 类名   参数 3 : 通常为 0
    Class StudentClass = objc_allocateClassPair(NSObject.class, "Student", 0);
    
    // 添加一个 NSString 的变量,第四个参数是对齐方式,第五个参数是参数类型
    // 必须在 objc_allocateClassPair and 和 objc_registerClassPair 之间调用
    if (class_addIvar(StudentClass, "schoolName", sizeof(NSString *), 0, "@")) {
        NSLog(@"添加成员变量成功");
    }
    
    // 添加 NSString * _name 成员变量
    class_addIvar(StudentClass, "_name", sizeof(NSString *), 0, @encode(NSString *));
    // 添加 int _age 成员变量
    class_addIvar(StudentClass, "_age", sizeof(int), 0, @encode(int));
    
    // 为 Student 类添加方法 "v@:" 这种写法见参数类型连接
    SEL sel = sel_registerName("sayFunction:");
    if (class_addMethod(StudentClass, sel, (IMP)sayFunction, "v@:@")) {
        NSLog(@"添加方法成功");
    }
    
    // 注册这个类到 runtime 系统中就可以使用了
    objc_registerClassPair(StudentClass);
    
    // 使用创建的类
    id student = [[StudentClass alloc] init];
    
    // 给刚刚添加的变量赋值
    // object_setInstanceVariable(student, "schoolName", (void *)&str);在ARC下不允许使用
    [student setValue:@"清华大学" forKey:@"schoolName"];
    
    // KVC 动态改变实例变量
    [student setValue:@"Tom" forKey:@"name"];
    
    // 从类中获取成员变量Ivar
    Ivar ageIvar = class_getInstanceVariable(StudentClass, "_age");
    // 为peopleInstance的成员变量赋值
    object_setIvar(StudentClass, ageIvar, @18);
    
    // 调用 sayFunction 方法,也就是给 student 这个接受者发送 sayFunction: 这个消息
    objc_msgSend(student, "sayFunction:", @"你好~"); 
    // [student performSelector:sel withObject:@"你好~"]; // 动态调用未显式在类中声明的方法
    
    student = nil;
    StudentClass = nil;

//    objc_disposeClassPair(StudentClass);
}

直接使用 objc_msgSend() 会报错 Too many arguments to function call, expected 0, have 3,此时需要在 Target -> Build Settings -> 搜索 msg -> 修改为 NO

You may also like...