2019独角兽企业重金招聘Python工程师标准>>>

该文章属于<简书 — 刘小壮>原创,转载请注明:

<简书 — 刘小壮> https://www.jianshu.com/p/ce97c66027cd


RuntimeiOS系统中重要的组成部分,面试也是必问的问题,所以Runtime是一个iOS工程师必须掌握的知识点。

现在市面上有很多关于Runtime的学习资料,也有不少高质量的,但是大多数质量都不是很高,而且都只介绍某个点,并不全面。

这段时间正好公司内部组织技术分享,我分享的主题就是Runtime,我把分享的资料发到博客,大家一起学习交流。

文章都是我的一些笔记,和平时的技术积累。个人水平有限,文章有什么问题还请各位大神指导,谢谢!


描述

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

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

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

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

使用Runtime

Runtime是一个共享动态库,其目录位于/usr/include/objc,由一系列的C函数和结构体构成。和Runtime系统发生交互的方式有三种,一般都是用前两种:

  1. 使用OC源码 直接使用上层OC源码,底层会通过Runtime为其提供运行支持,上层不需要关心Runtime运行。
  2. NSObject 在OC代码中绝大多数的类都是继承自NSObject的,NSProxy类例外。RuntimeNSObject中定义了一些基础操作,NSObject的子类也具备这些特性。
  3. 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 listprotocolivar 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

RuntimeIMP本质上就是一个函数指针,其定义如下。在IMP中有两个默认的参数idSELid也就是方法中的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获取到对应的IMPmethodForSelector:方法不只实例对象可以调用,类对象也可以调用。

- (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方式的回调,我们可以通过RuntimeAPI,将原来的方法回调改为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用来表示方法,其包含SELIMP,下面可以看一下Method结构体的定义。

typedef struct method_t *Method;struct method_t {SEL name;const char *types;IMP imp;
};

在运行过程中是这样。

Xcode进行编译的时候,只会将XcodeCompile 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)

可以通过下面两个方法,传入指定的ClasspropertyName,获取对应的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 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函数时传入的对象,也是当前实例变量,所以是在向自己发送父类的消息。具体可以看一下[self class][super class]的结果,结果应该都是一样的。

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

参考资料

Apple Runtime Program Guild 维基百科-Objective-C 维基百科-Clang 维基百科-GCC(GNU)

苹果开源代码不建议去Github,上面的版本一般更新不及时,建议去苹果的开源官网。 Apple Opensource

简书由于排版的问题,阅读体验并不好,布局、图片显示、代码等很多问题。所以建议到我Github上,下载Runtime PDF合集。把所有Runtime文章总计九篇,都写在这个PDF中,而且左侧有目录,方便阅读。

下载地址:Runtime PDF 麻烦各位大佬点个赞,谢谢!

转载于:https://my.oschina.net/u/3781127/blog/2877460

探秘Runtime - Runtime介绍相关推荐

  1. Java的Runtime类介绍

    JVM运行在系统之上,而对于系统相关的信息获取,java将其封装在RunTime类当中,通过阅读源码可知,RunTime是单例模式,其构造方法私有,必须通过getRunTime方法获取RunTime ...

  2. Runtime - 常用的runtime API介绍

    其实也不咋常用,毕竟用runtime都少 #import <objc/runtime.h> 使用runtime之前一定要导入这个 Class object_getClass(id obj) ...

  3. iOS运行时-使用Runtime向Category中添加属性以及运行时介绍

    前言 了解OC的都应该知道,在一般情况下,我们是不能向Category中添加属性的,只能添加方法,但有些情况向,我们确实需要向Category中添加属性,而且很多系统的API也有一些在Category ...

  4. java runtime是什么_java中Runtime类详细介绍

    Runtime类描述了虚拟机一些信息.该类采用了单例设计模式,可以通过静态方法 getRuntime()获取Runtime类实例.下面演示了获取虚拟机的内存信息: package Main; publ ...

  5. RunTime 入门

    原文链接:http://www.jianshu.com/p/59992507f875 这是一篇浅显实用 易记 易理解的关于runtime的解读. Runtime 中的方法主要以五个单词开头--clas ...

  6. java基础----Runtime类的使用(一)

    这里面我们对java中的Runtime类做一个简单的了解介绍.若不常想到无常和死,虽有绝顶的聪明,照理说也和呆子一样. Runtimeo类的使用 一.得到系统内存的一些信息 @Test public ...

  7. java的runtime类,java中Runtime类的简单应用

    1.简介 JVM中,Runtime类是唯一与JVM的运行状态相关的类,并且默认情况下提供此类的实例化对象. 由于每个JVM进程中只允许运行时类的一个对象,因此默认情况下该类的构造方法是私有化的,这表明 ...

  8. Objective-C Runtime的数据类型

    转自:http://www.cnblogs.com/whyandinside/archive/2013/02/26/2933552.html Class Objective-C是支持反射的,先来了解一 ...

  9. Java—System类和Runtime类

    System类 System类介绍   System类代表Java程序运行平台,程序不能创建该对象,但是System类提供了直接调用的类方法和类变量.   System类提供标准输入.标准输出.错误输 ...

  10. go-issues#14592 runtime: let idle OS threads exit 内核线程暴增与线程回收问题

    issues#14592 前言 CPU线程与OS线程 为什么会产生空闲线程 如何限制最大线程数量 怎么回收空闲线程 总结 前言   在社区issues#14592可以看到,go中的空闲线程是不会自动回 ...

最新文章

  1. Leetcode 152. 乘积最大子序列 解题思路及C++实现
  2. mysql数据库周考_周考三
  3. oracle 数据 时间间隔,如何掌握 Oracle 中的时间间隔型数据
  4. Flex itemReanderer(转)
  5. android 360加固 例子,[原创]利用VirtualApp实现360加固的soHook简单例子
  6. Win32ASM学习[10]:传送指令
  7. Emacs sql-mode 自动连接数据库 (当前仅支持MySQL)
  8. 通过判断流的头 判断文件类型
  9. 【生活中的逻辑谬误】止于分析和简化主义
  10. 泛微OA流程插入JS代码块
  11. python经纬度批量定位 绘制高清地图
  12. ISE UCF 写法
  13. @Cacheable使用详解
  14. iOS开发常用代码片段:总有你用得上的功能
  15. Google AppSheet: 无需编程构建零代码应用
  16. 修车案例 | 2017 款荣威 i6 车驾驶人侧车窗组合开关失灵
  17. GitHub开源成语接龙查询器,已收录16765个成语
  18. 给Python漫画分集标题下载工具开发Qt界面
  19. PDF格式转换器绿色版下载
  20. Python的tushare库实现沪深300 指数数据分析——CAMP模型

热门文章

  1. 让Delphi的DataSnap发挥最大效率
  2. 水晶报表-推拉模式实例
  3. 群体智能优化算法之蚁群优化算法(ACO)
  4. 基于二进制粒子群算法的配电网故障诊断- 附代码
  5. matlab利用geotiffread读取tif文件报错:‘错误使用 tifflib, 无法打开 TIFF 文件’
  6. endnote9安装
  7. listview 加载的时候报java.lang.IllegalArgumentException: column ‘_id’ does not exist 错误
  8. Java设计模式------工厂模式-------工厂方法模式
  9. mysql systemctl开机启动_Linux 开机启动项命令:chkconfig 和 systemctl
  10. oschina mysql limit_【MySQL】LIMIT 的用法