内容简介:OC语言是一门动态语言,会将程序的一些决定工作从编译期推迟到运行期。由于OC语言运行时的特性,所以其不只需要依赖编译器,还需要依赖运行时环境。
该文章属于<简书 — 刘小壮>原创,转载请注明:
<简书 — 刘小壮> https://www.jianshu.com/p/ce97c66027cd
Runtime
是
iOS
系统中重要的组成部分,面试也是必问的问题,所以
Runtime
是一个
iOS
工程师必须掌握的知识点。
现在市面上有很多关于 Runtime
的学习资料,也有不少高质量的,但是大多数质量都不是很高,而且都只介绍某个点,并不全面。
这段时间正好公司内部组织技术分享,我分享的主题就是 Runtime
,我把分享的资料发到博客,大家一起学习交流。
文章都是我的一些笔记,和平时的技术积累。个人水平有限,文章有什么问题还请各位大神指导,谢谢!:grin:
描述
OC语言是一门动态语言,会将程序的一些决定工作从编译期推迟到运行期。由于OC语言运行时的特性,所以其不只需要依赖编译器,还需要依赖运行时环境。
OC语言在编译期都会被编译为 C语言 的 Runtime
代码,二进制执行过程中执行的都是C语言代码。而OC的类本质上都是结构体,在编译时都会以结构体的形式被编译到二进制中。 Runtime
是一套由C、C++、汇编实现的API,所有的方法调用都叫做发送消息。
根据 Apple
官方文档的描述,目前OC运行时分为两个版本, Modern
和 Legacy
。二者的区别在于 Legacy
在实例变量发生改变后,需要重新编译其子类。 Modern
在实例变量发生改变后,不需要重新编译其子类。
Runtime
不只是一些C语言的API,其由 Class
、 Meta Class
、 Instance、Class Instance
组成, 是一套完整的面向对象的数据结构。所以研究Runtime整体的对象模型,比研究API是怎么实现的更有意义。
使用Runtime
Runtime
是一个共享动态库,其目录位于 /usr/include/objc
,由一系列的C函数和结构体构成。和 Runtime
系统发生交互的方式有三种,一般都是用前两种:
- 使用OC源码
直接使用上层OC源码,底层会通过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;
IMP
在 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); // ....
获取IMP
通过定义在 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
。
IMP for block
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
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
没有影响。
Property
在 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的。这个在后面会有讲到。
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
函数时传入的对象,也是当前实例变量,所以是在向自己发送父类的消息。具体可以看一下 [self class]
和 [super class]
的结果,结果应该都是一样的。
在项目中经常会通过 [super xxx]
的方式调用父类方法,这是因为需要先完成父类的操作,当然也可以不调用,视情况而定。以经常见到的自定义 init
方法中,经常会出现 if (self = [super init])
的调用,这是在完成自己的初始化之前先对父类进行初始化,否则只初始化自身可能会存在问题。在调用 [super init]
时如果返回 nil
,则表示父类初始化失败,这时候初始化子类肯定会出现问题,所以需要做判断。
参考资料
苹果开源代码不建议去Github,上面的版本一般更新不及时,建议去苹果的开源官网。
Apple Opensource
简书由于排版的问题,阅读体验并不好,布局、图片显示、代码等很多问题。所以建议到我 Github
上,下载 Runtime PDF
合集。把所有 Runtime
文章总计九篇,都写在这个 PDF
中,而且左侧有目录,方便阅读。
下载地址: Runtime PDF
麻烦各位大佬点个赞,谢谢!:grin:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Java编程思想 (第4版)
[美] Bruce Eckel / 陈昊鹏 / 机械工业出版社 / 2007-6 / 108.00元
本书赢得了全球程序员的广泛赞誉,即使是最晦涩的概念,在Bruce Eckel的文字亲和力和小而直接的编程示例面前也会化解于无形。从Java的基础语法到最高级特性(深入的面向对象概念、多线程、自动项目构建、单元测试和调试等),本书都能逐步指导你轻松掌握。 从本书获得的各项大奖以及来自世界各地的读者评论中,不难看出这是一本经典之作。本书的作者拥有多年教学经验,对C、C++以及Java语言都有独到......一起来看看 《Java编程思想 (第4版)》 这本书的介绍吧!