查看原文
其他

深度揭秘Runtime原理

刘小壮 搜狐技术产品 2019-05-01

点击蓝字关注我们

OC语言是一门动态语言,会将程序的一些决定工作从编译期推迟到运行期。由于 OC语言运行时的特性,所以其不只需要依赖编译器,还需要依赖运行时环境。

OC语言在编译期都会被编译为 C语言的 Runtime代码,二进制执行过程中执行的都是 C语言代码。而 OC的类本质上都是结构体,在编译时都会以结构体的形式被编译到二进制中。 Runtime是一套由 CC++、汇编实现的 API,所有的方法调用都叫做发送消息。

根据 Apple官方文档的描述,目前 OC运行时分为两个版本, ModernLegacy。二者的区别在于 Legacy在实例变量发生改变后,需要重新编译其子类。 Modern在实例变量发生改变后,不需要重新编译其子类。

Runtime不只是一些 C语言的 API,其由 ClassMetaClassInstanceObject组成,是一套完整的面向对象的数据结构。所以研究Runtime整体的对象模型,比研究API是怎么实现的更有意义。

对象模型

下面图中表示了对象间 isa的关系,以及类的继承关系。

Runtime源码可以看出,每个对象都是一个 objc_object的结构体,在结构体中有一个isa指针,该指针指向自己所属的类,由Runtime负责创建对象。

类被定义为 objc_class结构体, objc_class结构体继承自 objc_object,所以类也是对象。在应用程序中,类对象只会被创建一份。在 objc_class结构体中定义了对象的 method listprotocolivar list等,表示对象的行为。

既然类是对象,那类对象也是其他类的实例。所以 Runtime中设计出了 metaclass,通过 metaclass来创建类对象,所以类对象的 isa指向对应的 metaclass。而 metaclass也是一个对象,所有元类的 isa都指向其根元类,根原类的 isa指针指向自己。通过这种设计, isa的整体结构形成了一个闭环。

  1. // 精简版定义

  2. typedef struct objc_class *Class;

  3. struct objc_class : objc_object {

  4.    // Class ISA;

  5.    Class superclass;

  6. }

  7. struct objc_object {

  8.    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

  9. };

在对象的继承体系中,类和元类都有各自的继承体系,但它们都有共同的根父类 NSObject,而 NSObject的父类指向 nil。需要注意的是,上图中 RootClass(Class)NSObject类对象,而 RootClass(Meta)NSObject的元类对象。

基础定义

objc-private.h文件中,有一些项目中常用的基础定义,这是最新的 objc-723中的定义,可以来看一下。

  1. typedef struct objc_class *Class;

  2. typedef struct objc_object *id;

  3. typedef struct method_t *Method;

  4. typedef struct ivar_t *Ivar;

  5. typedef struct category_t *Category;

  6. typedef struct property_t *objc_property_t;

IMP

RuntimeIMP本质上就是一个函数指针,其定义如下。在 IMP中有两个默认的参数 idSELid也就是方法中的 self,这和 objc_msgSend()函数传递的参数一样。

  1. typedef void (*IMP)(void /* id, SEL, ... */ );

Runtime中提供了很多对于 IMP操作的 API,下面就是不分 IMP相关的函数定义。我们比较常见的是 method_exchangeImplementations函数,MethodSwizzling就是通过这个 API实现的。

Method

Method用来表示方法,其包含 SELIMP,下面可以看一下 Method结构体的定义。

  1. typedef struct method_t *Method;

  2. struct method_t {

  3.    SEL name;

  4.    const char *types;

  5.    IMP imp;

  6. };

在运行过程中是这样。

Xcode进行编译的时候,只会将 XcodeCompileSources.m声明的方法编译到 MethodList,而 .h文件中声明的方法对 MethodList没有影响。

Property

Runtime中定义了属性的结构体,用来表示对象中定义的属性。 @property修饰符用来修饰属性,修饰后的属性为 objc_property_t类型,其本质是 property_t结构体。其结构体定义如下。

  1. typedef struct property_t *objc_property_t;

  2. struct property_t {

  3.    const char *name;

  4.    const char *attributes;

  5. };

对象结构分析

对象间分析

在OC中绝大多数类都是继承自 NSObject的( NSProxy例外),类与类之间都会存在继承关系。通过子类创建对象时,继承链中所有成员变量都会存在对象中。

例如下图中,父类是 UIViewController,具有一个 view属性。子类 UserCenterViewController继承自 UIViewController,并定义了两个新属性。这时如果通过子类创建对象,就会同时包含着三个实例变量。

但是类的结构在编译时都是固定的,如果想要修改类的结构需要重新编译。如果上线后用户安装到设备上,新版本的iOS系统中更新了父类的结构,也就是 UIViewController的结构,为其加入了新的实例变量,这时用户更新新的iOS系统后就会导致问题。

原来 UIViewController的结构中增加了 childViewControllers属性,这时候和子类的内存偏移就发生冲突了。只不过, Runtime有检测内存冲突的机制,在类生成实例变量时,会判断实例变量是否有地址冲突,如果发生冲突则调整对象的地址偏移,这样就在运行时解决了地址冲突的问题。

内存布局

类的本质是结构体,在结构体中包含一些成员变量,例如 method listivar list等,这些都是结构体的一部分。 method、protocolproperty的实现这些都可以放到类中,所有对象调用同一份即可,但对象的成员变量不可以放在一起,因为每个对象的成员变量值都是不同的。

创建实例对象时,会根据其对应的Class分配内存,内存构成是ivars+isa_t。并且实例变量不只包含当前 Classivars,也会包含其继承链中的 ivarsivars的内存布局在编译时就已经决定,运行时需要根据 ivars内存布局创建对象,所以 Runtime不能动态修改 ivars,会破坏已有内存布局。

(上图中,“x”表示地址对其后的空位)

以上图为例,创建的对象中包含所属类及其继承者链中,所有的成员变量。因为对象是结构体,所以需要进行地址对其,一般OC对象的大小都是8的倍数。

也不是所有对象都不能动态修改ivars,如果是通过runtime动态创建的类,是可以修改ivars的。这个在后面会有讲到。

ivar读写

实例变量的 isa_t指针会指向其所属的类,对象中并不会包含 methodprotocolpropertyivar等信息,这些信息在编译时都保存在只读结构体 class_ro_t中。在 class_ro_tivarsconst只读的,在 image loadcopyclass_rw_t中时,是不会 copy ivars的,并且 class_rw_t中并没有定义 ivars的字段。

在访问某个成员变量时,直接通过 isa_t找到对应的 objc_class,并通过其 class_ro_tivar list做地址偏移,查找对应的对象内存。正是由于这种方式,所以对象的内存地址是固定不可改变的。

方法传参

当调用实例变量的方法时,会通过 objc_msgSend()发起调用,调用时会传入 selfSEL。函数内部通过 isa在类的内部查找方法列表对应的 IMP,传入对应的参数并发起调用。如果调用的方法时涉及到当前对象的成员变量的访问,这时候就是在 objc_msgSend()内部,通过类的 ivar list判断地址偏移,取出 ivar并传入调用的 IMP中的。

调用 super的方式时则调用 objc_msgSendSuper()函数实现,调用时将实例变量的父类传进去。但是需要注意的是,调用 objc_msgSendSuper函数时传入的对象,也是当前实例变量,所以是在向自己发送父类的消息。具体可以看一下 [selfclass][superclass]的结果,结果应该都是一样的。

在项目中经常会通过 [superxxx]的方式调用父类方法,这是因为需要先完成父类的操作,当然也可以不调用,视情况而定。以经常见到的自定义 init方法中,经常会出现 if(self=[superinit])的调用,这是在完成自己的初始化之前先对父类进行初始化,否则只初始化自身可能会存在问题。在调用 [superinit]时如果返回 nil,则表示父类初始化失败,这时候初始化子类肯定会出现问题,所以需要做判断。

结构体定义

OC1.0中, Runtime很多定义都写在 NSObject.h文件中。后来可能苹果也不太想让开发者知道 Runtime内部的实现,所以就把源码定义从 NSObject中搬到 Runtime中了。而且之前的定义也不用了,通过 OBJC_TYPES_DEFINED预编译指令,将之前的代码废弃调了。

现在 NSObject中的定义非常简单,直接就是一个 Class类型的 isa变量,其他信息都隐藏起来了。

  1. @interface NSObject <NSObject> {

  2. #pragma clang diagnostic push

  3. #pragma clang diagnostic ignored "-Wobjc-interface-ivars"

  4.    Class isa  OBJC_ISA_AVAILABILITY;

  5. #pragma clang diagnostic pop

  6. }

这是最新的一些常用 Runtime定义,和之前的定义也不太一样了,用了最新的结构体对象,之前的结构体也都废弃了。

  1. typedef struct objc_class *Class;

  2. typedef struct objc_object *id;

  3. typedef struct method_t *Method;

  4. typedef struct ivar_t *Ivar;

  5. typedef struct category_t *Category;

  6. typedef struct property_t *objc_property_t;

objc_object

在OC中每个对象都是一个结构体,结构体中都包含一个isa的成员变量,其位于成员变量的第一位。isa的成员变量之前都是 Class类型的,后来苹果将其改为 isa_t

  1. struct objc_object {

  2. private:

  3.    isa_t isa;

  4. };

OC中的类和元类也是一样,都是结构体构成的。由于类的结构体定义继承自 objc_object,所以其也是一个对象,并且具有对象的 isa特征。

所以可以通过 isa_t来查找对应的类或元类,查找方法应该是通过 uintptr_t类型的 bits,通过按位操作来查找 isa_t指向的类的地址。

实例对象或类对象的方法,并不会定义在各个对象中,而是都定义在 isa_t指向的类中。查找到对应的类后,通过类的 class_data_bits_t类型的 bits结构体查找方法,对象、类、元类都是同样的查找原理。

isa_t

isa_t是一个 union的结构对象, union类似于 C++结构体,其内部可以定义成员变量和函数。在 isa_t中定义了 clsbitsisa_t三部分,下面的 struct结构体就是 isa_t的结构体构成。

下面对 isa_t中的结构体进行了位域声明,地址从 nonpointer起到 extra_rc结束,从低到高进行排列。位域也是对结构体内存布局进行了一个声明,通过下面的结构体成员变量可以直接操作某个地址。位域总共占8字节,所有的位域加在一起正好是64位。

小提示: unionbits可以操作整个内存区,而位域只能操作对应的位。

下面的代码是不完整代码,只保留了 arm64部分,其他部分被忽略掉了。

  1. union isa_t

  2. {

  3.    isa_t() { }

  4.    isa_t(uintptr_t value) : bits(value) { }

  5.    Class cls;

  6.    uintptr_t bits;

  7. # if __arm64__

  8. #   define ISA_MASK        0x0000000ffffffff8ULL

  9. #   define ISA_MAGIC_MASK  0x000003f000000001ULL

  10. #   define ISA_MAGIC_VALUE 0x000001a000000001ULL

  11.    struct {

  12.        uintptr_t nonpointer        : 1; // 是32位还是64位

  13.        uintptr_t has_assoc         : 1; // 对象是否含有或曾经含有关联引用,如果没有关联引用,可以更快的释放对象

  14.        uintptr_t has_cxx_dtor      : 1; // 表示是否有C++析构函数或OC的析构函数

  15.        uintptr_t shiftcls          : 33; // 对象指向类的内存地址,也就是isa指向的地址

  16.        uintptr_t magic             : 6; // 对象是否初始化完成

  17.        uintptr_t weakly_referenced : 1; // 对象是否被弱引用或曾经被弱引用

  18.        uintptr_t deallocating      : 1; // 对象是否被释放中

  19.        uintptr_t has_sidetable_rc  : 1; // 对象引用计数太大,是否超出存储区域

  20.        uintptr_t extra_rc          : 19; // 对象引用计数

  21. #       define RC_ONE   (1ULL<<45)

  22. #       define RC_HALF  (1ULL<<18)

  23.    };

  24. # elif __x86_64__

  25. // ····

  26. # else

  27. // ····

  28. # endif

  29. };

ARM64架构下, isa_t以以下结构进行布局。在不同的 CPU架构下,布局方式会有所不同,但参数都是一样的。

下面是已经初始化后的 isa_t结构体的布局,以及各个结构体成员在结构体中的位置。

objc_class

Runtime中类也是一个对象,类的结构体 objc_class是继承自 objc_object的,具备对象所有的特征。在 objc_class中定义了三个成员变量, superclass是一个 objc_class类型的指针,指向其父类的 objc_class结构体。 cache用来处理已调用方法的缓存。

bitsobjc_class的主角,其内部只定义了一个 uintptr_t类型的 bits成员变量,存储了 class_rw_t的地址。 bits中还定义了一些基本操作,例如获取 class_rw_traw isa状态、是否 swift等函数。 objc_class结构体中定义的一些函数,其内部都是通过 bits实现的。

  1. struct objc_class : objc_object {

  2.    // Class ISA;

  3.    Class superclass;

  4.    cache_t cache;            

  5.    class_data_bits_t bits;    

  6.    class_rw_t *data() {

  7.        return bits.data();

  8.    }

  9.    void setData(class_rw_t *newData) {

  10.        bits.setData(newData);

  11.    }

  12.    // .....

  13. }

objc_class的源码可以看出,可以通过 bits结构体的 data()函数,获取 class_rw_t指针。我们进入源代码中看一下,可以看出是通过对 uintptr_t类型的 bits变量,做位运算查找对应的值。

  1. class_rw_t* data() {

  2.    return (class_rw_t *)(bits & FAST_DATA_MASK);

  3. }

uintptr_t本质上是一个 unsignedlongtypedefunsignedlong在64位处理器中占8字节,正好是64位二进制。通过 FAST_DATA_MASK转换为二进制后,是取 bits中的47-3的位置,正好是取出 class_rw_t指针。

在OC中一个指针的长度是47,例如打印一个 UIViewController的地址是 0x7faf1b580450,转换为二进制是 11111111010111100011011010110000000010001010000,最后面三位是占位的,所以在取地址的时候会忽略最后三位。

  1. // 查找第0位,表示是否swift

  2. #define FAST_IS_SWIFT           (1UL<<0)

  3. // 当前类或父类是否定义了retain、release等方法

  4. #define FAST_HAS_DEFAULT_RR     (1UL<<1)

  5. // 类或父类需要初始化isa

  6. #define FAST_REQUIRES_RAW_ISA   (1UL<<2)

  7. // 数据段的指针

  8. #define FAST_DATA_MASK          0x00007ffffffffff8UL

  9. // 11111111111111111111111111111111111111111111000 总共47位

因为在 bits中最后三位是没用的,所以可以用来存储一些其他信息。在 class_data_bits_t还定义了三个宏,用来对后三位做位运算。

class_ro_t和class_rw_t

class_data_bits_t相关的有两个很重要结构体, class_rw_tclass_ro_t,其中都定义着 method listprotocol listpropertylist等关键信息。

  1. struct class_rw_t {

  2.    uint32_t flags;

  3.    uint32_t version;

  4.    const class_ro_t *ro;

  5.    method_array_t methods;

  6.    property_array_t properties;

  7.    protocol_array_t protocols;

  8.    Class firstSubclass;

  9.    Class nextSiblingClass;

  10.    char *demangledName;

  11. };

在编译后 class_data_bits_t指向的是一个 class_ro_t的地址,这个结构体是不可变的(只读)。在运行时,才会通过 realizeClass函数将 bits指向 class_rw_t

  1. struct class_ro_t {

  2.    uint32_t flags;

  3.    uint32_t instanceStart;

  4.    uint32_t instanceSize;

  5.    uint32_t reserved;

  6.    const uint8_t * ivarLayout;

  7.    const char * name;

  8.    method_list_t * baseMethodList;

  9.    protocol_list_t * baseProtocols;

  10.    const ivar_list_t * ivars;

  11.    const uint8_t * weakIvarLayout;

  12.    property_list_t *baseProperties;

  13. };

在程序开始运行后会初始化 Class,在这个过程中,会把编译器存储在 bits中的 class_ro_t取出,然后创建 class_rw_t,并把 ro赋值给 rw,成为 rw的一个成员变量,最后把 rw设置给 bits,替代之前 bits中存储的 ro

假设创建一个类 LXZObject,继承自 NSObject,并为其加入一个 testMethod方法,不做其他操作。因为在编译后 objc_classbits对应的是 class_ro_t结构体,所以我们打印一下结构体的成员变量,看一下编译后的 class_ro_t是什么样的。

  1. struct class_ro_t {

  2.  flags = 128

  3.  instanceStart = 8

  4.  instanceSize = 8

  5.  reserved = 0

  6.  ivarLayout = 0x0000000000000000 <no value available>

  7.  name = 0x0000000100000f7a "LXZObject"

  8.  baseMethodList = 0x00000001000010c8

  9.  baseProtocols = 0x0000000000000000

  10.  ivars = 0x0000000000000000

  11.  weakIvarLayout = 0x0000000000000000 <no value available>

  12.  baseProperties = 0x0000000000000000

  13. }

经过打印可以看出,一个类的 class_ro_t中只会包含当前类的信息,不会包含其父类的信息,在 LXZObject类中只会包含 namebaseMethodList两个字段,而 baseMethodList中只有一个 testMethod方法。由此可知, class_rw_t结构体也是一样的。

程序加载

加载过程

在应用程序启动后,由 dyld(thedynamiclink editor)进行程序的初始化操作。大概流程就像下面列出的步骤,其中第3、4、5步会执行多次,在 ImageLoader加载新的 image进内存后就会执行一次。

  1. 在引用程序启动后,由 dyld将应用程序加载到二进制中,并完成一些文件的初始化操作。

  2. Runtime向 dyld中注册回调函数。

  3. 通过 ImageLoader将所有 image加载到内存中。

  4. dyld在 image发生改变时,主动调用回调函数。

  5. Runtime接收到 dyld的函数回调,开始执行 map_images、 load_images等操作,并回调 +load方法。

  6. 调用 main()函数,开始执行业务代码。

ImageLoaderimage的加载器, image可以理解为编译后的二进制。

下面是在 Runtimemap_images函数打断点,观察回调情况的汇编代码。可以看出,调用是由 dyld发起的,由 ImageLoader通知 dyld进行调用。

关于 dyld我并没有深入研究,有兴趣的同学可以到Github上下载源码研究一下。

动态加载

一个OC程序可以在运行过程中动态加载和链接新类或 Category,新类或 Category会加载到程序中,其处理方式和其他类是相同的。动态加载还可以做许多不同的事,动态加载允许应用程序进行自定义处理。

OC提供了 objc_loadModules运行时函数,执行 Mach-O中模块的动态加载,在上层 NSBundle对象提供了更简单的访问 API

map images

Runtime加载时,会调用 _objc_init函数,并在内部注册三个函数指针。其中 map_images函数是初始化的关键,内部完成了大量 Runtime环境的初始化操作。

map_images函数中,内部也是做了一个调用中转。然后调用到 map_images_nolock函数,内部核心就是 _read_images函数。

  1. void _objc_init(void)

  2. {

  3.    // .... 各种init

  4.    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

  5. }

  6. void map_images(unsigned count, const char * const paths[],

  7.           const struct mach_header * const mhdrs[])

  8. {

  9.    rwlock_writer_t lock(runtimeLock);

  10.    return map_images_nolock(count, paths, mhdrs);

  11. }

  12. void map_images_nolock(unsigned mhCount, const char * const mhPaths[],

  13.                  const struct mach_header * const mhdrs[])

  14. {

  15.    if (hCount > 0) {

  16.        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);

  17.    }

  18. }

_read_images函数中完成了大量的初始化操作,函数内部代码量比较大,下面是精简版带注释的源代码。

先整体梳理一遍 _read_images函数内部的逻辑:

  1. 加载所有类到类的 gdb_objc_realized_classes表中。

  2. 对所有类做重映射。

  3. 将所有 SEL都注册到 namedSelectors表中。

  4. 修复函数指针遗留。

  5. 将所有 Protocol都添加到 protocol_map表中。

  6. 对所有 Protocol做重映射。

  7. 初始化所有非懒加载的类,进行 rw、 ro等操作。

  8. 遍历已标记的懒加载的类,并做初始化操作。

  9. 处理所有 Category,包括 Class和 MetaClass

  10. 初始化所有未初始化的类。

load images

在项目中经常用到 load类方法, load类方法的调用时机比 main函数还要靠前。 load方法是由系统来调用的,并且在整个程序运行期间,只会调用一次,所以可以在 load方法中执行一些只执行一次的操作。

一般 MethodSwizzling都会放在 load方法中执行,这样在执行 main函数前,就可以对类方法进行交换。可以确保正式执行代码时,方法肯定是被交换过的。

如果对一个类添加Category后,并且重写其原有方法,这样会导致Category中的方法覆盖原类的方法。但是load方法却是例外,所有Category和原类的load方法都会被执行。

load方法由 Runtime进行调用,下面我们分析一下 load方法的实现, load的实现源码都在 objc-loadmethod.mm中。

在一个新的工程中,我们创建一个 TestObject类,并在其 load方法中打一个断点,看一下系统的调用堆栈。

从调用栈可以看出,是通过系统的动态链接器 dyld开始的调用,然后调到 Runtimeload_images函数中。 load_images函数是通过 _dyld_objc_notify_register函数,将自己的函数指针注册给 dyld的。

  1. void _objc_init(void)

  2. {

  3.    static bool initialized = false;

  4.    if (initialized) return;

  5.    initialized = true;

  6.    // fixme defer initialization until an objc-using image is found?

  7.    environ_init();

  8.    tls_init();

  9.    static_init();

  10.    lock_init();

  11.    exception_init();

  12.    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

  13. }

load_images函数中主要做了两件事,首先通过 prepare_load_methods函数准备 Classload listCategoryload list,然后通过 call_load_methods函数调用已经准备好的两个方法列表。

  1. void

  2. load_images(const char *path __unused, const struct mach_header *mh)

  3. {

  4.    if (!hasLoadMethods((const headerType *)mh)) return;

  5.    prepare_load_methods((const headerType *)mh);

  6.    call_load_methods();

  7. }

initialize

load方法类似的也有 initialize方法, initialize方法也是由 Runtime进行调用的,自己不可以直接调用。与 load方法不同的是, initialize方法是在第一次调用类所属的方法时,才会调用 initialize方法,而 load方法是在 main函数之前就全部调用了。所以理论上来说 initialize可能永远都不会执行,如果当前类的方法永远不被调用的话。

下面我们研究一下 initializeRuntime中的源码。

在向对象发送消息时, lookUpImpOrForward函数中会判断当前类是否被初始化,如果没有被初始化,则先进行初始化再调用类的方法。

  1. IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver);

  2. // ....省略好多代码

  3. // 第一次调用当前类的话,执行initialize的代码

  4. if (initialize  &&  !cls->isInitialized()) {

  5.    _class_initialize (_class_getNonMetaClass(cls, inst));

  6. }

  7. // ....省略好多代码

在进行初始化的时候,和 load方法的调用顺序一样,会按照继承者链先初始化父类。 _class_initialize函数中关键的两行代码是 callInitializelockAndFinishInitializing的调用。

  1. // 第一次调用类的方法,初始化类对象

  2. void _class_initialize(Class cls)

  3. {

  4.    Class supercls;

  5.    bool reallyInitialize = NO;

  6.    // 递归初始化父类。initizlize不用显式的调用super,因为runtime已经在内部调用了

  7.    supercls = cls->superclass;

  8.    if (supercls  &&  !supercls->isInitialized()) {

  9.        _class_initialize(supercls);

  10.    }

  11.    {

  12.        monitor_locker_t lock(classInitLock);

  13.        if (!cls->isInitialized() && !cls->isInitializing()) {

  14.            cls->setInitializing();

  15.            reallyInitialize = YES;

  16.        }

  17.    }

  18.    if (reallyInitialize) {

  19.        _setThisThreadIsInitializingClass(cls);

  20.        if (MultithreadedForkChild) {

  21.            performForkChildInitialize(cls, supercls);

  22.            return;

  23.        }

  24.        @try {

  25.            // 通过objc_msgSend()函数调用initialize方法

  26.            callInitialize(cls);

  27.        }

  28.        @catch (...) {

  29.            @throw;

  30.        }

  31.        @finally {

  32.            // 执行initialize方法后,进行系统的initialize过程

  33.            lockAndFinishInitializing(cls, supercls);

  34.        }

  35.        return;

  36.    }

  37.    else if (cls->isInitializing()) {

  38.        if (_thisThreadIsInitializingClass(cls)) {

  39.            return;

  40.        } else if (!MultithreadedForkChild) {

  41.            waitForInitializeToComplete(cls);

  42.            return;

  43.        } else {

  44.            _setThisThreadIsInitializingClass(cls);

  45.            performForkChildInitialize(cls, supercls);

  46.        }

  47.    }

  48. }

通过 objc_msgSend函数调用 initialize方法。

  1. void callInitialize(Class cls)

  2. {

  3.    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);

  4.    asm("");

  5. }

需要注意的是, initialize方法和 load方法不太一样, Category中定义的 initialize方法会覆盖原方法而不是像 load方法一样都可以调用。

Runtime实战

ORM

对象关系映射 (ObjectRelationalMapping),简称 ORM,用于面向对象语言中不同系统数据之间的转换。 可以通过对象关系映射来实现 JSON转模型,使用比较多的是 MantleMJExtensionYYKitJSONModel等框架,这些框架在进行转换的时候,都是使用 Runtime的方式实现的。

Mantle使用和 MJExtension有些类似,只不过 MJExtension使用起来更加方便。 Mantle在使用时主要是通过继承的方式处理,而 MJExtension是通过 Category处理,代码依赖性更小,无侵入性。

性能评测

这些第三方中 Mantle功能最强大,但是太臃肿,使用起来性能比其他第三方都差一些。 JSONModelMJExtension这些第三方几乎都在一个水平级, YYKit相对来说性能可以比肩手写赋值代码,性价比最高。

对于模型转换需求不是太大的工程来说,尽量用 YYKit来进行转换性能会更好一些。功能可能略逊于 MJExtension,我个人还是比较习惯用 MJExtension

YYKit作者评测

https://blog.ibireme.com/2015/10/23/ios_model_framework_benchmark/

实现思路

也可以自己实现模型转换的逻辑,以字典转模型为例,大体逻辑如下:

  1. 创建一个 Category用来做模型转换,对外提供方法并传入字典对象。

  2. 通过 Runtime对应的函数,获取属性列表并遍历,根据属性名从字典中取出对应的对象。

  3. 通过 KVC将从字典中取出的值,赋值给对象。

  4. 有时候会遇到多层嵌套的情况,例如字典包含数组,数组中还是一个字典。这种情况就可以做判断,如果模型对象是数组则取出字典对应字段的数组,然后遍历数组再调用字典赋值的方法。

下面简单实现了一个字典转模型的代码,通过 Runtime遍历属性列表,并根据属性名取出字典中的对象,然后通过 KVC进行赋值操作。调用方式和 MJExtensionYYModel类似,直接通过模型类调用类方法即可。如果想在其他类中也使用的话,应该把下面的实现写在 NSObjectCategory中,这样所有类都可以调用。

  1. // 调用部分

  2. NSDictionary *dict = @{@"name" : @"lxz",

  3.                       @"age" : @18,

  4.                       @"gender" : @YES};

  5. TestObject *object = [TestObject objectWithDict:dict];

  6. // 实现代码

  7. @interface TestObject : NSObject

  8. @property (nonatomic, copy  ) NSString *name;

  9. @property (nonatomic, assign) NSInteger age;

  10. @property (nonatomic, assign) BOOL gender;

  11. + (instancetype)objectWithDict:(NSDictionary *)dict;

  12. @end

  13. @implementation TestObject

  14. + (instancetype)objectWithDict:(NSDictionary *)dict {

  15.    return [[TestObject alloc] initWithDict:dict];

  16. }

  17. - (instancetype)initWithDict:(NSDictionary *)dict {

  18.    self = [super init];

  19.    if (self) {

  20.        unsigned int count = 0;

  21.        objc_property_t *propertys = class_copyPropertyList([self class], &count);

  22.        for (int i = 0; i < count; i++) {

  23.            objc_property_t property = propertys[i];

  24.            const char *name = property_getName(property);

  25.            NSString *nameStr = [[NSString alloc] initWithUTF8String:name];

  26.            id value = [dict objectForKey:nameStr];

  27.            [self setValue:value forKey:nameStr];

  28.        }

  29.        free(propertys);

  30.    }

  31.    return self;

  32. }

  33. @end

通过 Runtime可以获取到对象的 MethodListPropertyList等,不只可以用来做字典模型转换,还可以做很多工作。例如还可以通过 Runtime实现自动归档和反归档,下面是自动进行归档操作。

  1. // 1.获取所有的属性

  2. unsigned int count = 0;

  3. Ivar *ivars = class_copyIvarList([NJPerson class], &count);

  4. // 遍历所有的属性进行归档

  5. for (int i = 0; i < count; i++) {

  6.    // 取出对应的属性

  7.    Ivar ivar = ivars[i];

  8.    const char * name = ivar_getName(ivar);

  9.    // 将对应的属性名称转换为OC字符串

  10.    NSString *key = [[NSString alloc] initWithUTF8String:name];

  11.    // 根据属性名称利用KVC获取数据

  12.    id value = [self valueForKeyPath:key];

  13.    [encoder encodeObject:value forKey:key];

  14. }

  15. free(ivars);

我写了一个简单的 Category,可以自动实现 NSCodingNSCopying协议。这是开源地址:

https://github.com/DeveloperErenLiu/EasyNSCoding

小思考

下面的代码会?

CompileError / RuntimeCrash / NSLog…?


  1. @interface NSObject (Sark)


  2. + (void)foo;

  3. @end

  4. @implementation NSObject (Sark)

  5. - (void)foo {

  6.    NSLog(@"IMP: -[NSObject (Sark) foo]");

  7. }

  8. @end

  9. // 测试代码

  10. [NSObject foo];

  11. [[NSObject new] performSelector:@selector(foo)];

答案:

全都正常输出,编译和运行都没有问题。

这道题和上一道题很相似,第二个调用肯定没有问题,第一个调用后会从元类中查找方法,然而方法并不在元类中,所以找元类的 superclass。方法定义在是 NSObjectCategory,由于 NSObject的对象模型比较特殊,元类的 superclass是类对象,所以从类对象中找到了方法并调用。

由于公众号字数限制,狐厂的刘小壮同学在Github上还为大家准备了十万字的 PDF~

如果你也对 Runtime感兴趣,欢迎点击 阅读原文 到作者的Github上下载 Runtime PDF合集。

    

扫一扫 关注搜狐技术产品

一起做技术大神


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存