内容简介: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:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Google's PageRank and Beyond
Amy N. Langville、Carl D. Meyer / Princeton University Press / 2006-7-23 / USD 57.50
Why doesn't your home page appear on the first page of search results, even when you query your own name? How do other web pages always appear at the top? What creates these powerful rankings? And how......一起来看看 《Google's PageRank and Beyond》 这本书的介绍吧!