深度揭秘Runtime原理
OC语言是一门动态语言,会将程序的一些决定工作从编译期推迟到运行期。由于
OC
语言运行时的特性,所以其不只需要依赖编译器,还需要依赖运行时环境。
OC
语言在编译期都会被编译为C
语言的Runtime
代码,二进制执行过程中执行的都是C
语言代码。而OC
的类本质上都是结构体,在编译时都会以结构体的形式被编译到二进制中。Runtime
是一套由C
、C++
、汇编实现的API
,所有的方法调用都叫做发送消息。根据
Apple
官方文档的描述,目前OC
运行时分为两个版本,Modern
和Legacy
。二者的区别在于Legacy
在实例变量发生改变后,需要重新编译其子类。Modern
在实例变量发生改变后,不需要重新编译其子类。
Runtime
不只是一些C
语言的API
,其由Class
、MetaClass
、InstanceObject
组成,是一套完整的面向对象的数据结构。所以研究Runtime整体的对象模型,比研究API是怎么实现的更有意义。
对象模型
下面图中表示了对象间 isa
的关系,以及类的继承关系。
从 Runtime
源码可以看出,每个对象都是一个 objc_object
的结构体,在结构体中有一个isa指针,该指针指向自己所属的类,由Runtime负责创建对象。
类被定义为 objc_class
结构体, objc_class
结构体继承自 objc_object
,所以类也是对象。在应用程序中,类对象只会被创建一份。在 objc_class
结构体中定义了对象的 method list
、 protocol
、 ivar list
等,表示对象的行为。
既然类是对象,那类对象也是其他类的实例。所以 Runtime
中设计出了 metaclass
,通过 metaclass
来创建类对象,所以类对象的 isa
指向对应的 metaclass
。而 metaclass
也是一个对象,所有元类的 isa
都指向其根元类,根原类的 isa
指针指向自己。通过这种设计, isa
的整体结构形成了一个闭环。
// 精简版定义
typedef struct objc_class *Class;
struct objc_class : objc_object {
// Class ISA;
Class superclass;
}
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
在对象的继承体系中,类和元类都有各自的继承体系,但它们都有共同的根父类 NSObject
,而 NSObject
的父类指向 nil
。需要注意的是,上图中 RootClass(Class)
是 NSObject
类对象,而 RootClass(Meta)
是 NSObject
的元类对象。
基础定义
在 objc-private.h
文件中,有一些项目中常用的基础定义,这是最新的 objc-723
中的定义,可以来看一下。
typedef struct objc_class *Class;
typedef struct objc_object *id;
typedef struct method_t *Method;
typedef struct ivar_t *Ivar;
typedef struct category_t *Category;
typedef struct property_t *objc_property_t;
IMP
在 Runtime
中 IMP
本质上就是一个函数指针,其定义如下。在 IMP
中有两个默认的参数 id
和 SEL
, id
也就是方法中的 self
,这和 objc_msgSend()
函数传递的参数一样。
typedef void (*IMP)(void /* id, SEL, ... */ );
Runtime
中提供了很多对于 IMP
操作的 API
,下面就是不分 IMP
相关的函数定义。我们比较常见的是 method_exchangeImplementations
函数,MethodSwiz
zling
就是通过这个 API
实现的。
Method
Method
用来表示方法,其包含 SEL
和 IMP
,下面可以看一下 Method
结构体的定义。
typedef struct method_t *Method;
struct method_t {
SEL name;
const char *types;
IMP imp;
};
在运行过程中是这样。
在 Xcode
进行编译的时候,只会将 Xcode
的 CompileSources
中 .m
声明的方法编译到 MethodList
,而 .h
文件中声明的方法对 MethodList
没有影响。
Property
在 Runtime
中定义了属性的结构体,用来表示对象中定义的属性。 @property
修饰符用来修饰属性,修饰后的属性为 objc_property_t
类型,其本质是 property_t
结构体。其结构体定义如下。
typedef struct property_t *objc_property_t;
struct property_t {
const char *name;
const char *attributes;
};
对象结构分析
对象间分析
在OC中绝大多数类都是继承自 NSObject
的( NSProxy
例外),类与类之间都会存在继承关系。通过子类创建对象时,继承链中所有成员变量都会存在对象中。
例如下图中,父类是 UIViewController
,具有一个 view
属性。子类 UserCenterViewController
继承自 UIViewController
,并定义了两个新属性。这时如果通过子类创建对象,就会同时包含着三个实例变量。
但是类的结构在编译时都是固定的,如果想要修改类的结构需要重新编译。如果上线后用户安装到设备上,新版本的iOS系统中更新了父类的结构,也就是 UIViewController
的结构,为其加入了新的实例变量,这时用户更新新的iOS系统后就会导致问题。
原来 UIViewController
的结构中增加了 childViewControllers
属性,这时候和子类的内存偏移就发生冲突了。只不过, Runtime
有检测内存冲突的机制,在类生成实例变量时,会判断实例变量是否有地址冲突,如果发生冲突则调整对象的地址偏移,这样就在运行时解决了地址冲突的问题。
内存布局
类的本质是结构体,在结构体中包含一些成员变量,例如 method list
、 ivar list
等,这些都是结构体的一部分。 method、protocol
、 property
的实现这些都可以放到类中,所有对象调用同一份即可,但对象的成员变量不可以放在一起,因为每个对象的成员变量值都是不同的。
创建实例对象时,会根据其对应的Class分配内存,内存构成是ivars+isa_t。并且实例变量不只包含当前 Class
的 ivars
,也会包含其继承链中的 ivars
。 ivars
的内存布局在编译时就已经决定,运行时需要根据 ivars
内存布局创建对象,所以 Runtime
不能动态修改 ivars
,会破坏已有内存布局。
(上图中,“x”表示地址对其后的空位)
以上图为例,创建的对象中包含所属类及其继承者链中,所有的成员变量。因为对象是结构体,所以需要进行地址对其,一般OC对象的大小都是8的倍数。
也不是所有对象都不能动态修改ivars,如果是通过runtime动态创建的类,是可以修改ivars的。这个在后面会有讲到。
ivar读写
实例变量的 isa_t
指针会指向其所属的类,对象中并不会包含 method
、 protocol
、 property
、 ivar
等信息,这些信息在编译时都保存在只读结构体 class_ro_t
中。在 class_ro_t
中 ivars
是 const
只读的,在 image load
时 copy
到 class_rw_t
中时,是不会 copy ivars
的,并且 class_rw_t
中并没有定义 ivars
的字段。
在访问某个成员变量时,直接通过 isa_t
找到对应的 objc_class
,并通过其 class_ro_t
的 ivar list
做地址偏移,查找对应的对象内存。正是由于这种方式,所以对象的内存地址是固定不可改变的。
方法传参
当调用实例变量的方法时,会通过 objc_msgSend()
发起调用,调用时会传入 self
和 SEL
。函数内部通过 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
变量,其他信息都隐藏起来了。
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
这是最新的一些常用 Runtime
定义,和之前的定义也不太一样了,用了最新的结构体对象,之前的结构体也都废弃了。
typedef struct objc_class *Class;
typedef struct objc_object *id;
typedef struct method_t *Method;
typedef struct ivar_t *Ivar;
typedef struct category_t *Category;
typedef struct property_t *objc_property_t;
objc_object
在OC中每个对象都是一个结构体,结构体中都包含一个isa的成员变量,其位于成员变量的第一位。isa
的成员变量之前都是 Class
类型的,后来苹果将其改为 isa_t
。
struct objc_object {
private:
isa_t isa;
};
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
中定义了 cls
、 bits
、 isa_t
三部分,下面的 struct
结构体就是 isa_t
的结构体构成。
下面对 isa_t
中的结构体进行了位域声明,地址从 nonpointer
起到 extra_rc
结束,从低到高进行排列。位域也是对结构体内存布局进行了一个声明,通过下面的结构体成员变量可以直接操作某个地址。位域总共占8字节,所有的位域加在一起正好是64位。
小提示: union
中 bits
可以操作整个内存区,而位域只能操作对应的位。
下面的代码是不完整代码,只保留了 arm64
部分,其他部分被忽略掉了。
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1; // 是32位还是64位
uintptr_t has_assoc : 1; // 对象是否含有或曾经含有关联引用,如果没有关联引用,可以更快的释放对象
uintptr_t has_cxx_dtor : 1; // 表示是否有C++析构函数或OC的析构函数
uintptr_t shiftcls : 33; // 对象指向类的内存地址,也就是isa指向的地址
uintptr_t magic : 6; // 对象是否初始化完成
uintptr_t weakly_referenced : 1; // 对象是否被弱引用或曾经被弱引用
uintptr_t deallocating : 1; // 对象是否被释放中
uintptr_t has_sidetable_rc : 1; // 对象引用计数太大,是否超出存储区域
uintptr_t extra_rc : 19; // 对象引用计数
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
# elif __x86_64__
// ····
# else
// ····
# endif
};
在 ARM64
架构下, isa_t
以以下结构进行布局。在不同的 CPU
架构下,布局方式会有所不同,但参数都是一样的。
下面是已经初始化后的 isa_t
结构体的布局,以及各个结构体成员在结构体中的位置。
objc_class
在 Runtime
中类也是一个对象,类的结构体 objc_class
是继承自 objc_object
的,具备对象所有的特征。在 objc_class
中定义了三个成员变量, superclass
是一个 objc_class
类型的指针,指向其父类的 objc_class
结构体。 cache
用来处理已调用方法的缓存。
bits
是 objc_class
的主角,其内部只定义了一个 uintptr_t
类型的 bits
成员变量,存储了 class_rw_t
的地址。 bits
中还定义了一些基本操作,例如获取 class_rw_t
、 raw isa
状态、是否 swift
等函数。 objc_class
结构体中定义的一些函数,其内部都是通过 bits
实现的。
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache;
class_data_bits_t bits;
class_rw_t *data() {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
// .....
}
从 objc_class
的源码可以看出,可以通过 bits
结构体的 data()
函数,获取 class_rw_t
指针。我们进入源代码中看一下,可以看出是通过对 uintptr_t
类型的 bits
变量,做位运算查找对应的值。
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
uintptr_t
本质上是一个 unsignedlong
的 typedef
, unsignedlong
在64位处理器中占8字节,正好是64位二进制。通过 FAST_DATA_MASK
转换为二进制后,是取 bits
中的47-3的位置,正好是取出 class_rw_t
指针。
在OC中一个指针的长度是47,例如打印一个 UIViewController
的地址是 0x7faf1b580450
,转换为二进制是 11111111010111100011011010110000000010001010000
,最后面三位是占位的,所以在取地址的时候会忽略最后三位。
// 查找第0位,表示是否swift
#define FAST_IS_SWIFT (1UL<<0)
// 当前类或父类是否定义了retain、release等方法
#define FAST_HAS_DEFAULT_RR (1UL<<1)
// 类或父类需要初始化isa
#define FAST_REQUIRES_RAW_ISA (1UL<<2)
// 数据段的指针
#define FAST_DATA_MASK 0x00007ffffffffff8UL
// 11111111111111111111111111111111111111111111000 总共47位
因为在 bits
中最后三位是没用的,所以可以用来存储一些其他信息。在 class_data_bits_t
还定义了三个宏,用来对后三位做位运算。
class_ro_t和class_rw_t
和 class_data_bits_t
相关的有两个很重要结构体, class_rw_t
和 class_ro_t
,其中都定义着 method list
、 protocol list
、 propertylist
等关键信息。
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
};
在编译后 class_data_bits_t
指向的是一个 class_ro_t
的地址,这个结构体是不可变的(只读)。在运行时,才会通过 realizeClass
函数将 bits
指向 class_rw_t
。
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
uint32_t reserved;
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
};
在程序开始运行后会初始化 Class
,在这个过程中,会把编译器存储在 bits
中的 class_ro_t
取出,然后创建 class_rw_t
,并把 ro
赋值给 rw
,成为 rw
的一个成员变量,最后把 rw
设置给 bits
,替代之前 bits
中存储的 ro
。
假设创建一个类 LXZObject
,继承自 NSObject
,并为其加入一个 testMethod
方法,不做其他操作。因为在编译后 objc_class
的 bits
对应的是 class_ro_t
结构体,所以我们打印一下结构体的成员变量,看一下编译后的 class_ro_t
是什么样的。
struct class_ro_t {
flags = 128
instanceStart = 8
instanceSize = 8
reserved = 0
ivarLayout = 0x0000000000000000 <no value available>
name = 0x0000000100000f7a "LXZObject"
baseMethodList = 0x00000001000010c8
baseProtocols = 0x0000000000000000
ivars = 0x0000000000000000
weakIvarLayout = 0x0000000000000000 <no value available>
baseProperties = 0x0000000000000000
}
经过打印可以看出,一个类的 class_ro_t
中只会包含当前类的信息,不会包含其父类的信息,在 LXZObject
类中只会包含 name
和 baseMethodList
两个字段,而 baseMethodList
中只有一个 testMethod
方法。由此可知, class_rw_t
结构体也是一样的。
程序加载
加载过程
在应用程序启动后,由 dyld(thedynamiclink editor)
进行程序的初始化操作。大概流程就像下面列出的步骤,其中第3、4、5步会执行多次,在 ImageLoader
加载新的 image
进内存后就会执行一次。
在引用程序启动后,由
dyld
将应用程序加载到二进制中,并完成一些文件的初始化操作。Runtime
向dyld
中注册回调函数。通过
ImageLoader
将所有image
加载到内存中。dyld
在image
发生改变时,主动调用回调函数。Runtime
接收到dyld
的函数回调,开始执行map_images
、load_images
等操作,并回调+load
方法。调用
main()
函数,开始执行业务代码。
ImageLoader
是 image
的加载器, image
可以理解为编译后的二进制。
下面是在 Runtime
的 map_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
函数。
void _objc_init(void)
{
// .... 各种init
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
void map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
rwlock_writer_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
void map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
{
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
}
在 _read_images
函数中完成了大量的初始化操作,函数内部代码量比较大,下面是精简版带注释的源代码。
先整体梳理一遍 _read_images
函数内部的逻辑:
加载所有类到类的
gdb_objc_realized_classes
表中。对所有类做重映射。
将所有
SEL
都注册到namedSelectors
表中。修复函数指针遗留。
将所有
Protocol
都添加到protocol_map
表中。对所有
Protocol
做重映射。初始化所有非懒加载的类,进行
rw
、ro
等操作。遍历已标记的懒加载的类,并做初始化操作。
处理所有
Category
,包括Class
和MetaClass
。初始化所有未初始化的类。
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
开始的调用,然后调到 Runtime
的 load_images
函数中。 load_images
函数是通过 _dyld_objc_notify_register
函数,将自己的函数指针注册给 dyld
的。
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
在 load_images
函数中主要做了两件事,首先通过 prepare_load_methods
函数准备 Classload list
和 Categoryload list
,然后通过 call_load_methods
函数调用已经准备好的两个方法列表。
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!hasLoadMethods((const headerType *)mh)) return;
prepare_load_methods((const headerType *)mh);
call_load_methods();
}
initialize
和 load
方法类似的也有 initialize
方法, initialize
方法也是由 Runtime
进行调用的,自己不可以直接调用。与 load
方法不同的是, initialize
方法是在第一次调用类所属的方法时,才会调用 initialize
方法,而 load
方法是在 main
函数之前就全部调用了。所以理论上来说 initialize
可能永远都不会执行,如果当前类的方法永远不被调用的话。
下面我们研究一下
initialize
在Runtime
中的源码。
在向对象发送消息时, lookUpImpOrForward
函数中会判断当前类是否被初始化,如果没有被初始化,则先进行初始化再调用类的方法。
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver);
// ....省略好多代码
// 第一次调用当前类的话,执行initialize的代码
if (initialize && !cls->isInitialized()) {
_class_initialize (_class_getNonMetaClass(cls, inst));
}
// ....省略好多代码
在进行初始化的时候,和 load
方法的调用顺序一样,会按照继承者链先初始化父类。 _class_initialize
函数中关键的两行代码是 callInitialize
和 lockAndFinishInitializing
的调用。
// 第一次调用类的方法,初始化类对象
void _class_initialize(Class cls)
{
Class supercls;
bool reallyInitialize = NO;
// 递归初始化父类。initizlize不用显式的调用super,因为runtime已经在内部调用了
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
_class_initialize(supercls);
}
{
monitor_locker_t lock(classInitLock);
if (!cls->isInitialized() && !cls->isInitializing()) {
cls->setInitializing();
reallyInitialize = YES;
}
}
if (reallyInitialize) {
_setThisThreadIsInitializingClass(cls);
if (MultithreadedForkChild) {
performForkChildInitialize(cls, supercls);
return;
}
@try {
// 通过objc_msgSend()函数调用initialize方法
callInitialize(cls);
}
@catch (...) {
@throw;
}
@finally {
// 执行initialize方法后,进行系统的initialize过程
lockAndFinishInitializing(cls, supercls);
}
return;
}
else if (cls->isInitializing()) {
if (_thisThreadIsInitializingClass(cls)) {
return;
} else if (!MultithreadedForkChild) {
waitForInitializeToComplete(cls);
return;
} else {
_setThisThreadIsInitializingClass(cls);
performForkChildInitialize(cls, supercls);
}
}
}
通过 objc_msgSend
函数调用 initialize
方法。
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}
需要注意的是, initialize
方法和 load
方法不太一样, Category
中定义的 initialize
方法会覆盖原方法而不是像 load
方法一样都可以调用。
Runtime实战
ORM
对象关系映射 (ObjectRelationalMapping)
,简称 ORM
,用于面向对象语言中不同系统数据之间的转换。
可以通过对象关系映射来实现 JSON
转模型,使用比较多的是 Mantle
、 MJExtension
、 YYKit
、 JSONModel
等框架,这些框架在进行转换的时候,都是使用 Runtime
的方式实现的。
Mantle
使用和 MJExtension
有些类似,只不过 MJExtension
使用起来更加方便。 Mantle
在使用时主要是通过继承的方式处理,而 MJExtension
是通过 Category
处理,代码依赖性更小,无侵入性。
性能评测
这些第三方中 Mantle
功能最强大,但是太臃肿,使用起来性能比其他第三方都差一些。 JSONModel
、 MJExtension
这些第三方几乎都在一个水平级, YYKit
相对来说性能可以比肩手写赋值代码,性价比最高。
对于模型转换需求不是太大的工程来说,尽量用 YYKit
来进行转换性能会更好一些。功能可能略逊于 MJExtension
,我个人还是比较习惯用 MJExtension
。
YYKit作者评测:
https://blog.ibireme.com/2015/10/23/ios_model_framework_benchmark/
实现思路
也可以自己实现模型转换的逻辑,以字典转模型为例,大体逻辑如下:
创建一个
Category
用来做模型转换,对外提供方法并传入字典对象。通过
Runtime
对应的函数,获取属性列表并遍历,根据属性名从字典中取出对应的对象。通过
KVC
将从字典中取出的值,赋值给对象。有时候会遇到多层嵌套的情况,例如字典包含数组,数组中还是一个字典。这种情况就可以做判断,如果模型对象是数组则取出字典对应字段的数组,然后遍历数组再调用字典赋值的方法。
下面简单实现了一个字典转模型的代码,通过 Runtime
遍历属性列表,并根据属性名取出字典中的对象,然后通过 KVC
进行赋值操作。调用方式和 MJExtension
、 YYModel
类似,直接通过模型类调用类方法即可。如果想在其他类中也使用的话,应该把下面的实现写在 NSObject
的 Category
中,这样所有类都可以调用。
// 调用部分
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
可以获取到对象的 MethodList
、 PropertyList
等,不只可以用来做字典模型转换,还可以做很多工作。例如还可以通过 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
,可以自动实现 NSCoding
、 NSCopying
协议。这是开源地址:
https://github.com/DeveloperErenLiu/EasyNSCoding
小思考
下面的代码会?
CompileError
/ RuntimeCrash
/ 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
。方法定义在是 NSObject
的 Category
,由于 NSObject
的对象模型比较特殊,元类的 superclass
是类对象,所以从类对象中找到了方法并调用。
由于公众号字数限制,狐厂的刘小壮同学在Github上还为大家准备了十万字的 PDF
~
如果你也对 Runtime
感兴趣,欢迎点击 阅读原文
到作者的Github上下载 Runtime
PDF
合集。
扫一扫 关注搜狐技术产品
一起做技术大神