/ iOS 开发

Runtime 基础(一)

Objective-C 是一门动态语言,它把很多在编译时做的事情放到了运行时来处理。由于这个特性,我们可以很灵活的使用 runtime 这个黑魔法来完成一些静态语言做不到的事情,比如使用 JSPatch 给应用打 patch,使用 Mantle 映射 json 和 Model。runtime 是使用 C 和汇编实现的一个运行时库,使得在 C 语言的基础上增加了面向对象的特性,从而有了 Objective-C 这个语言。

Class 和 id

在 objc.h 文件中,我们可以看到 runtime 中 Class 的定义:

/// Class 的定义,一个指向 objc_class 结构体类型的指针。
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

下面是 id 的定义:

// id 的定义,一个指向类的实例的指针。
/// A pointer to an instance of a class.
typedef struct objc_object *id;

objc_class 的结构体定义位于 runtime.h 文件中,具体定义如下:

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;
/* Use `Class` instead of `struct objc_class *` */

objc_object 的结构体定义如下:

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

在 Objective-C 中,类和对象(类的实例)都是对象,任何对象都有 isa 指针。Class 是一个指向 objc_class 结构体类型的指针,id 是一个指向 objc_object 类型的指针。

isa:对象中的 isa 指向该对象的类;类中的 isa 指针指向 meteClass(元类),元类的 isa 最终都指向一个根元类,根元类的 isa 指向自身。

super_class:所有的类中都都有一个 super_class 指针,指向该类的父类。如果该类是根类,super_class 为 nil。

objc_ivar_list:该类中的成员变量列表。

objc_method_list:该类的方法列表。当 info 为 CLS_CLASS0x1L) 时,该类为普通的类,存储的是实例方法,为 CLS_META (0x2L) 时,该类为 meteClass,包含类方法。

objc_cache:objc_cache 缓存了最近使用过的方法地址。由于大多数方法在运行时并不会经常用到,如果没有缓存,需要每次都去遍历方法列表,效率比较低。缓存可以将最近使用的方法都暂时存起来,查找的时候优先从 cache 中查找,这样能提高查找效率。

objc_protocol_list:存储了该类声明遵守的协议列表。

下面这张图是类的继承关系(来自 Apple Developer 文档):

应用

通过以上这些知识,我们可以使用 runtime 提供的一些方法来完成一些很 cool 的操作。

获取类名和属性列表

Mantle 是 Objective-C 中一个非常易用的 json 和 Model 映射库。默认情况下,我们只需要将 Model 中的属性名和 json 中的字段保持一致就可以了,Mantle 会帮我们自动的将 json 中的 key 和 Model 中的属性做映射。其中就需要知道这个类的类名,以及属性列表等信息,而这些信息通过 runtime 很容易就能拿到。

// 定义一个类 Person
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
-(void)sayHello;
@end

获取类名:

#import <objc/objc.h>
#import <objc/runtime.h>

Class anyClass = [Person class];
NSLog(@"class_name = %s",class_getName(anyClass));

输出结果:

2017-11-12 20:28:34.552335+0800 runtime[1738:5571753] class_name = Person

获取属性列表:

首先要定义一个 int 类型的 outCount,用来接收属性个数,使用 class_copyPropertyList 方法可以获取到该类下面的成员变量,返回一个 objc_property_t 列表。遍历该列表,可以通过 property_getName 获得属性对应的 name。调用完毕后需要使用 free 释放 property_list。

unsigned int outCount = 0;
objc_property_t *property_list = class_copyPropertyList([self class], &outCount);
for (int i = 0; i < outCount; i++){
    objc_property_t item = property_list[i];
    const char *name = property_getName(item);
	NSLog(@"name = %s", name);
}
free(property_list)

输出结果:

2017-11-12 21:17:07.928565+0800 runtime[2481:5657488] name = name
2017-11-12 21:17:07.928931+0800 runtime[2481:5657488] name = age

获取方法名列表

和获取属性列表的方法类似,我们也可以获取到该类的方法列表:

unsigned int outCount = 0;
Method *method_list = class_copyMethodList(currentPeopleClass1, &outCount);
for(int i = 0; i < outCount; i++){
	Method method = method_list[i];
	NSLog(@"method_name = %s",method_getName(method));
}
free(method_list);

输出结果:

2017-11-12 21:23:42.177998+0800 runtime[2626:5672002] method_name = sayHello
2017-11-12 21:23:42.178180+0800 runtime[2626:5672002] method_name = .cxx_destruct
2017-11-12 21:23:42.178284+0800 runtime[2626:5672002] method_name = name
2017-11-12 21:23:42.178401+0800 runtime[2626:5672002] method_name = setName:
2017-11-12 21:23:42.178507+0800 runtime[2626:5672002] method_name = setAge:
2017-11-12 21:23:42.178599+0800 runtime[2626:5672002] method_name = age

可以看到方法列表中有我们自己定义的一个方法,还有两个属性 name 和 age 的 get、set 方法。咦!还有个从未见过的 .cxx_destruct 方法,查询后得知,这个方法是编译器自动生成的。在ARC中,一般不需要再重写 dealloc 方法,ARC 借用 Objective-C++ 的特性,Objective-C++ 对象在释放时会调用所有持有对象的析构方法,当当编译器发现对象包含C++对象时,会生成 .cxx_destruct 方法,ARC 借助这个方法,在其中执行清除内存的代码。