KVO & KVC & Runtime
一、语言类型了解
1. 动态语言
-
定义:动态语言是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构。
主要动态语言:
Object-C
、C#
、JavaScript
、PHP
、Python
、Erlang
。Object-C
语言的动态语言特性得益于它的Runtime
机制,Runtime
不是语言特性,而是运行时环境。
2. 静态语言
-
定义:与动态语言相对应的,运行时结构不可变的语言就是静态语言。如
Java
、C
、C++
。C语言调用一个方法其实就是跳到内存中的某一点并开始执行一段代码。没有任何动态的特性,因为这在编译时就决定好了。
3. 三个容易混淆的名词
-
Dynamic Programming Language (动态语言或动态编程语言)
-
Dynamically Typed Language (动态类型语言)
-
Statically Typed Language (静态类型语言)
- 动态类型语言:指在运行期间才去做数据类型检查的语言,说的是数据类型。
- 静态类型语言:在编译期间(或运行之前)确定的,编写代码的时候要明确确定变量的数据类型。
- 动态语言:说的是运行时改变结构,说的是代码结构。
4. 语言类型参考资料
二、Runtime 源码
1. 为什么需要了解runtime?
-
Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。
-
Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。理解 Objective-C 的 Runtime 机制可以帮我们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。了解 Runtime ,要先了解它的核心 - 消息传递 (Messaging)。
-
Objective-C 中,[object foo] 语法并不会立即执行 foo 这个方法的代码。它是在运行时给 object 发送一条叫 foo 的消息。这个消息,也许会由 object 来处理,也许会被转发给另一个对象,或者不予理睬假装没收到这个消息。多条不同的消息也可以对应同一个方法实现。这些都是在程序运行的时候决定的。
-
事实上,在编译时你写的 Objective-C 函数调用的语法都会被翻译成一个 C 的函数调用 - objc_msgSend() 。比如,下面两行代码就是等价的:
[array insertObject:foo atIndex:5]; ⤵️ objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);
2. runtime是什么?
- runtime是一套比较底层的纯C语言API, 属于1个C语言库, 包含了很多底层的C语言API。
- 平时编写的OC代码, 在程序运行过程中, 其实最终都是转成了runtime的C语言代码, runtime算是OC的幕后工作者。
- Objective-C需要runtime系统来动态创建类和对象,进行消息发送和转发。
3. runtime能干些什么事?
- 在程序运行过程中, 动态创建一个类(比如KVO的底层实现,如何自己动手实现 KVO)。
- 在程序运行过程中, 动态地为某个类添加属性\方法, 修改属性值\方法。
- 遍历一个类的所有成员变量(属性)\所有方法。
4. 源码解析
OC类的实例对象最终被翻译成objc_object
结构体:
//类
typedef struct objc_class *Class;
//对象
typedef struct objc_object {
Class isa;
} *id;
每个Objective-C对象都有一个隐藏的数据结构,这个数据结构是Objective-C对象的第一个成员变量,它就是isa指针。
这个isa到底是什么呢?官方解释如下:
一个对象(Object)的isa指向了这个对象的类(Class),而这个对象的类(Class)的isa指向了metaclass。这样我们就可以找到静态方法和变量了。
Objective-C的运行时是动态的,它能让你在运行时为类添加方法或者去除方法以及使用反射。这在其它语言是不多见的。
- Class 是一个 objc_class 结构类型的指针。
- id(任意对象)是一个 objc_object 结构类型的指针,其第一个成员是一个 objc_class 结构类型的指针。
- 引申解读:内存布局以一个 objc_class 指针为开始的所有东东都可以当做一个 object 来对待。
objc_class
结构体:
struct objc_class {
Class isa;
Class super_class ;
const char *name ;
long version ;
long info ;
long instance_size ;
struct objc_ivar_list *ivars ;
struct objc_method_list **methodLists ;
struct objc_cache *cache ;
struct objc_protocol_list *protocols ;
} OBJC2_UNAVAILABLE;
struct objc_method_list {
struct objc_method_list *obsolete;
int method_count;
#ifdef __LP64__
int space;
#endif
/* variable length structure */
struct objc_method method_list[1];
};
struct objc_method {
SEL method_name; /*函数名*/
char *method_types; /*表示函数类型的字符串*/
IMP method_imp; /*函数的实现IMP*/
};
简单介绍一下各个结构体的成员变量
-
name: 一个 C 字符串,指示类的名称。
我们可以在运行期,通过这个名称查找到该类(通过:id objc_getClass(const char *aClassName))或该类的 metaclass(id objc_getMetaClass(const char *aClassName));
-
version: 类的版本信息,默认初始化为 0。
-
info: 供运行期使用的一些位标识。
- CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含实例方法和变量;
- CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
- CLS_INITIALIZED (0x4L) 表示该类已经被运行期初始化了,这个标识位只被 objc_addClass 所设置;
- CLS_POSING (0x8L) 表示该类被 pose 成其他的类;(poseclass 在 ObjC 2.0 中被废弃了);
- CLS_MAPPED (0x10L) 为 ObjC 运行期所使用
- CLS_FLUSH_CACHE (0x20L) 为 ObjC 运行期所使用
- CLS_GROW_CACHE (0x40L) 为 ObjC 运行期所使用
- CLS_NEED_BIND (0x80L) 为 ObjC 运行期所使用
- CLS_METHOD_ARRAY (0x100L) 该标志位指示 methodlists 是指向一个 objc_method_list 还是 一个包含 objc_method_list 指针的数组;
-
instance_size:该类的实例变量大小(包括从父类继承下来的实例变量);
-
ivars: 指向 objc_ivar_list 的指针,存储每个实例变量的内存地址,如果该类没有任何实例变量则为NULL;
-
methodLists: 与 info 的一些标志位有关,CLS_METHOD_ARRAY 标识位决定其指向的东西(是指 向单个 objc_method_list 还是一个 objc_method_list 指针数组),如果 info 设置了 CLS_CLASS 则 objc_method_list 存储实例方法,如果设置的是 CLS_META 则存储类方法;
-
cache: 指向 objc_cache 的指针,用来缓存最近使用的方法,以提高查找方法的效率,加快运行速度;
举 objc_msgSend(obj, foo) 这个例子来说:
-
首先,通过 obj 的 isa 指针找到它的 class ;
-
在 class 的 method list 找 foo ;
-
如果 class 中没到 foo,继续往它的 superclass 中找 ;
-
一旦找到 foo 这个函数,就去执行它的实现IMP .
但这种实现有个问题,效率低。但一个 class 往往只有 20% 的函数会被经常调用,可能占总调用次数的 80% 。每个消息都需要遍历一次 objc_method_list 并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是 objc_class 中另一个重要成员 objc_cache 做的事情 - 再找到 foo 之后,把 foo 的 method_name 作为 key ,method_imp 作为 value 给存起来。当再次收到 foo 消息的时候,可以直接在 cache 里找到,避免去遍历 objc_method_list.
-
-
protocols: 指向 objc_protocol_list 的指针,存储该类声明要遵守的正式协议。
在Objective-C中,任何类的定义都是对象。类和类的实例(对象)没有任何本质上的区别。任何对象都有isa指针。
图示1
图示2
class 与 metaclass的区别
- class 是 instance object 的类类型。
- 当我们向实例对象发送消息(实例方法)时,我们在该实例对象的 class 结构的 methodlists 中去查找响应的函数,如果没找到匹配的响应函数则在该 class 的父类中的 methodlists 去查找。
- metaclass 是 class object 的类类型。
- 当我们向类对象发送消息(类方法)时,我们在该类对象的 metaclass 结构的 methodlists 中去查找响应的函数,如果没有找到匹配的响应函数则在该metaclass 的父类中的 methodlists 去查找。
规则如下
规则一: 类的实例对象的 isa 指向该类;该类的 isa 指向该类的 metaclass;
规则二: 类的 super_class指向其父类,如果该类为根类则值为 NULL;
规则三: metaclass 的 isa 指向根 metaclass,如果该 metaclass是根 metaclass 则指向自身;
规则四: metaclass 的 super_class 指向父 metaclass,如果该metaclass 是根 metaclass 则指向 该 metaclass 对应的类;
一句话总结
ObjC 为每个类的定义生成两个 objc_class ,一个即普通的 class,另一个即 metaclass。我们可以在 运行期创建这两个 objc_class 数据结构,然后使用 objc_addClass将 class注册到运行时系统中,以此实现动态地创建一个新的类。
简单了解runtime相关函数
-
增加
- 增加函数:class_addMethod
- 增加实例变量:class_addIvar
- 增加属性:@dynamic标签,或者class_addMethod,因为属性其实就是由getter和setter函数组成
- 增加Protocol:class_addProtocol
-
获取
- 获取函数列表及每个函数的信息(函数指针、函数名等等):class_getClassMethod method_getName ...
- 获取属性列表及每个属性的信息:class_copyPropertyList property_getName
- 获取类本身的信息,如类名等:class_getName class_getInstanceSize
- 获取变量列表及变量信息:class_copyIvarList
-
替换
- 将实例替换成另一个类:object_setClass
- 替换类方法的定义:class_replaceMethod
-
其他常用方法
- 交换两个方法的实现: method_exchangeImplementations
- 设置一个方法的实现:method_setImplementation
5. Runtime 参考资料
- 对象内存结构中的 isa 指针是用来做什么的?
- 详解Objective-C的isa与meta-class
- iOS 开发 深入浅出Runtime运行时之类与对象的结构
- Objective-C Runtime之消息传递流程
- Method Swizzling 和 AOP 实践
- Objective-C内存布局
三、KVO & KVC
1. KVO (Key-Value Observing)
- 概念
KVO 是 Objective-C 对观察者模式(Observer Pattern)的实现。也是 Cocoa Binding 的基础。当被观察对象的某个属性发生更改时,观察者对象会获得通知。
有意思的是,你不需要给被观察的对象添加任何额外代码,就能使用 KVO 。这是怎么做到的?
- KVO 实现机制
KVO 的实现也依赖于 Objective-C 强大的 Runtime 。
简单概述下 KVO 的实现:
- 当你观察一个对象时,一个新的类会动态被创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。自然,重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象值的更改。最后把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。
- 原来,这个中间类,继承自原本的那个类。不仅如此,Apple 还重写了 -class 方法,企图欺骗我们这个类没有变,就是原本那个类。
- 更具体的信息,去跑一下 Mike Ash 的那篇 文章 里的代码就能明白,这里就不再重复。
2. KVC 参考资料
3. KVC (Key-Value Coding)
- KVC全称是Key Value Coding,定义在NSKeyValueCoding.h文件中,是一个非正式协议。
- KVC提供了一种间接访问其属性方法或成员变量的机制,可以通过字符串来访问对应的属性方法或成员变量。
4. KVC 参考资料
💖 本期技术站点推荐 💖
- NSHipster.cn 中文 / NSHipster.com 英文
NSHipster 关注被忽略的 Objective-C、Swift 和 Cocoa 特性。每周更新。