<简书 — 刘小壮> https://www.jianshu.com/p/ce97c66027cdhtml
Runtime
是iOS
系统中重要的组成部分,面试也是必问的问题,因此Runtime
是一个iOS
工程师必须掌握的知识点。git如今市面上有不少关于
Runtime
的学习资料,也有很多高质量的,可是大多数质量都不是很高,并且都只介绍某个点,并不全面。github这段时间正好公司内部组织技术分享,我分享的主题就是
Runtime
,我把分享的资料发到博客,你们一块儿学习交流。面试文章都是个人一些笔记,和平时的技术积累。我的水平有限,文章有什么问题还请各位大神指导,谢谢!性能优化
**OC语言是一门动态语言,会将程序的一些决定工做从编译期推迟到运行期。**因为OC语言运行时的特性,因此其不仅须要依赖编译器,还须要依赖运行时环境。数据结构
OC语言在编译期都会被编译为C语言的Runtime
代码,二进制执行过程当中执行的都是C语言代码。而OC的类本质上都是结构体,在编译时都会以结构体的形式被编译到二进制中。Runtime
是一套由C、C++、汇编实现的API,全部的方法调用都叫作发送消息。并发
根据Apple
官方文档的描述,目前OC运行时分为两个版本,Modern
和Legacy
。两者的区别在于Legacy
在实例变量发生改变后,须要从新编译其子类。Modern
在实例变量发生改变后,不须要从新编译其子类。app
Runtime
不仅是一些C语言的API,其由Class
、Meta Class
、Instance、Class Instance
组成,是一套完整的面向对象的数据结构。因此研究Runtime总体的对象模型,比研究API是怎么实现的更有意义。ide
Runtime
是一个共享动态库,其目录位于/usr/include/objc
,由一系列的C函数和结构体构成。和Runtime
系统发生交互的方式有三种,通常都是用前两种:函数
Runtime
为其提供运行支持,上层不须要关心Runtime
运行。NSObject
在OC代码中绝大多数的类都是继承自NSObject
的,NSProxy
类例外。Runtime
在NSObject
中定义了一些基础操做,NSObject
的子类也具有这些特性。Runtime
动态库 上层的OC源码都是经过Runtime
实现的,咱们通常不直接使用Runtime
,直接和OC代码打交道就能够。使用Runtime
须要引入下面两个头文件,一些基础方法都定义在这两个文件中。
#import <objc/runtime.h> #import <objc/message.h>
下面图中表示了对象间isa
的关系,以及类的继承关系。
从Runtime
源码能够看出,每一个对象都是一个objc_object
的结构体,在结构体中有一个isa指针,该指针指向本身所属的类,由Runtime负责建立对象。
类被定义为objc_class
结构体,objc_class
结构体继承自objc_object
,因此类也是对象。在应用程序中,类对象只会被建立一份。在objc_class
结构体中定义了对象的method list
、protocol
、ivar list
等,表示对象的行为。
既然类是对象,那类对象也是其余类的实例。因此Runtime
中设计出了meta class
,经过meta class
来建立类对象,因此类对象的isa
指向对应的meta class
。而meta class
也是一个对象,全部元类的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
。须要注意的是,上图中Root Class(Class)
是NSObject
类对象,而Root Class(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;
在Runtime
中IMP
本质上就是一个函数指针,其定义以下。在IMP
中有两个默认的参数id
和SEL
,id
也就是方法中的self
,这和objc_msgSend()
函数传递的参数同样。
typedef void (*IMP)(void /* id, SEL, ... */ );
Runtime
中提供了不少对于IMP
操做的API
,下面就是不分IMP
相关的函数定义。咱们比较常见的是method_exchangeImplementations
函数,Method Swizzling
就是经过这个API
实现的。
OBJC_EXPORT void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); OBJC_EXPORT IMP _Nonnull method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); OBJC_EXPORT IMP _Nonnull method_getImplementation(Method _Nonnull m) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); OBJC_EXPORT IMP _Nullable class_getMethodImplementation(Class _Nullable cls, SEL _Nonnull name) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); // ....
经过定义在NSObject
中的下面两个方法,能够根据传入的SEL
获取到对应的IMP
。methodForSelector:
方法不仅实例对象能够调用,类对象也能够调用。
- (IMP)methodForSelector:(SEL)aSelector; + (IMP)instanceMethodForSelector:(SEL)aSelector;
例以下面建立C函数指针用来接收IMP
,获取到IMP
后能够手动调用IMP
,在定义的C函数中须要加上两个隐藏参数。
void (*function) (id self, SEL _cmd, NSObject object); function = (id self, SEL _cmd, NSObject object)[self methodForSelector:@selector(object:)]; function(instance, @selector(object:), [NSObject new]);
经过这些API
能够进行一些优化操做。若是遇到大量的方法执行,能够经过Runtime
获取到IMP
,直接调用IMP
实现优化。
TestObject *object = [[TestObject alloc] init]; void(*function)(id, SEL) = (void(*)(id, SEL))class_getMethodImplementation([TestObject class], @selector(testMethod)); function(object, @selector(testMethod));
在获取和调用IMP
的时候须要注意,每一个方法默认都有两个隐藏参数,因此在函数声明的时候须要加上这两个隐藏参数,调用的时候也须要把相应的对象和SEL
传进去,不然可能会致使Crash
。
Runtime
还支持block
方式的回调,咱们能够经过Runtime
的API
,将原来的方法回调改成block
的回调。
// 类定义 @interface TestObject : NSObject - (void)testMethod:(NSString *)text; @end // 类实现 @implementation TestObject - (void)testMethod:(NSString *)text { NSLog(@"testMethod : %@", text); } @end // runtime IMP function = imp_implementationWithBlock(^(id self, NSString *text) { NSLog(@"callback block : %@", text); }); const char *types = sel_getName(@selector(testMethod:)); class_replaceMethod([TestObject class], @selector(testMethod:), function, types); TestObject *object = [[TestObject alloc] init]; [object testMethod:@"lxz"]; // 输出 callback block : lxz
Method
用来表示方法,其包含SEL
和IMP
,下面能够看一下Method
结构体的定义。
typedef struct method_t *Method; struct method_t { SEL name; const char *types; IMP imp; };
在运行过程当中是这样。
在Xcode
进行编译的时候,只会将Xcode
的Compile Sources
中.m
声明的方法编译到Method List
,而.h
文件中声明的方法对Method List
没有影响。
在Runtime
中定义了属性的结构体,用来表示对象中定义的属性。@property
修饰符用来修饰属性,修饰后的属性为objc_property_t
类型,其本质是property_t
结构体。其结构体定义以下。
typedef struct property_t *objc_property_t; struct property_t { const char *name; const char *attributes; };
能够经过下面两个函数,分别获取实例对象的属性列表,和协议的属性列表。
objc_property_t * class_copyPropertyList(Class cls,unsigned int * outCount) objc_property_t * protocol_copyPropertyList(Protocol * proto,unsigned int * outCount)
能够经过下面两个方法,传入指定的Class
和propertyName
,获取对应的objc_property_t
属性结构体。
objc_property_t class_getProperty(Class cls,const char * name) objc_property_t protocol_getProperty(Protocol * proto,const char * name,BOOL isRequiredProperty,BOOL isInstanceProperty)
在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的。**这个在后面会有讲到。
实例变量的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
函数时传入的对象,也是当前实例变量,因此是在向本身发送父类的消息。具体能够看一下[self class]
和[super class]
的结果,结果应该都是同样的。
在项目中常常会经过[super xxx]
的方式调用父类方法,这是由于须要先完成父类的操做,固然也能够不调用,视状况而定。以常常见到的自定义init
方法中,常常会出现if (self = [super init])
的调用,这是在完成本身的初始化以前先对父类进行初始化,不然只初始化自身可能会存在问题。在调用[super init]
时若是返回nil
,则表示父类初始化失败,这时候初始化子类确定会出现问题,因此须要作判断。
Apple Runtime Program Guild 维基百科-Objective-C 维基百科-Clang 维基百科-GCC(GNU)
简书因为排版的问题,阅读体验并很差,布局、图片显示、代码等不少问题。因此建议到我Github
上,下载Runtime PDF
合集。把全部Runtime
文章总计九篇,都写在这个PDF
中,并且左侧有目录,方便阅读。
下载地址:Runtime PDF 麻烦各位大佬点个赞,谢谢!