Objective-C Runtime 运行时之一: 类与对象及
目录:
1. 什么是Runtime?
2. 与 Runtime 系统进行交互
3. Runtime 类与对象
-
- 1 类与对象基础数据结构
- 3.1.1. 类对象(class object)
- 3.1.2. 实例对象(id/objc_object)
- 3.1.3. 元类(Metaclass)
-
3.2. 类与对象操作函数
- 3.2.1. 类相关操作函数
- 3.2.1.1. 针对类的相关操作函数
- 3.2.1.2. 获取类定义
- 3.2.1.3. 成员变量(ivars)及属性
- 3.2.1.4 方法(methodLists)
- 3.2.1.5. 协议(objc_protocol_list)
- 3.2.1.6. 其它
- 3.2.1.7. 实例(Example)
- 3.2.2. 动态创建类和对象
- 3.2.3. 实例操作函数
- 3.2.4. 获取类定义
- 3.2.1. 类相关操作函数
4. 消息
- 4.1. 获得方法地址
- 4.2. 消息发送机制(objc_msgSend函数 )
- 4.3. 使用隐藏的参数
5. 消息转发
- 5.1. 动态方法解析 (resolveInstanceMethod:)
- 5.2. 备用接收者 (forwardingTargetForSelector:)
- 5.3. 完整的消息转发
6. 消息转发和多重继承
7. 消息转发和类继承
第一章.什么是Runtime
Objective-C 语言 将很多操作尽可能的从 编译
和链接
时推迟到运行时
。只要有可能,Objective-C 总是使用动态的方式
来解决问题。这意味着 Objective-C 语言不仅需要一个编译器
,同时也需要一个运行时系统
来执行编译好的代码。这儿的运行时系统
扮演的角色类似于 Objective-C 语言的操作系统
,Objective-C 基于该系统来工作
。
运行时系统
是一个公开接口的动态库,有一些数据结构
和函数的集合
组成,这些数据结构和函数的声明 头文件 存放于 /usr/include/objc目录下,这些函数支持用纯C的函数来实现和Objective-C同样的功能。还有一些函数构成了NSObject 类方法的基础
。这些函数使得访问运行时系统接口和提供开发工具成为可能,这意味着我们使用时只需要引入objc/Runtime.h
头文件即可
常见面试题:
1.说说什么是runtime?平时项目中有用过么?
2.了解runtime吗?是什么?
参考答案:(
自己总结的,若有误或有好的答案,请留言
)
runtime简称运行时
,是系统在运行的时候的一些机制,其中最主要的是消息机制
,由一些数据结构
和函数的集合
组成,是一套纯C写的API。Objective-C 基于运行时系统
来工作。因为Objective-C 语言允许很多操作尽可能的从编译和链接时推迟到运行时
。只要有可能,Objective-C 总是使用动态的方式
来解决问题。这意味着 Objective-C 语言不仅需要一个编译器
,同时也需要一个运行时系统
来执行编译好的代码
。这儿的运行时系统
扮演的角色类似于Objective-C
语言的操作系统
。
3.为什么需要Runtime?
参考答案:(
自己总结的,若有误或有好的答案,请留言
)
Objective-C 是一门动态性
比较强的编程语言,它会将一些工作放在代码运行时才处理而并非编译时。而在运行时
,我们所编写的OC代码会转换成完整的运行时代码。OC的函数调用称为消息发送,属于动态调用过程。在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用
。因此,编译器是不够的,我们还需要一个运行时系统(Runtime system)来处理编译后的代码。
第二章.与 Runtime 系统进行交互
Objective-C 程序有三种途径和运行时系统交互:
- 通过Objective-C源代码
- 通过类NSObject的方法
- 通过运行时系统的函数
-
2.1 通过Objective-C源代码
大部分情况下,运行时系统在后台自动运行,我们只需编写和编译 Objective-C 源代码。当您编译 Objective-C 类 和 方法
时,编译器为实现语言动态特性
将自动创建一些 数据结构 和 函数
。这些数据结构包含 类定义
和 协议类定义
中的信息,如在Objective-C 2.0 程序设计语言中定义类和协议类一节所讨论 的 类的对象
和 协议类的对象
,方法选标
,实例变量
模板,以及其它来自于源代码的信息
。
运行时系统
的主要功能就是根据源代码中的表达式发送消息
,如"消息”一节所述。
-
2.2 通过类NSObject的方法
Cocoa程序中绝大部分类都是NSObject类的子类
,所以大部分都继承了NSObject类的方法,因而继承 了NSObject的行为
。(NSProxy类是个例外;更多细节参考““消息转发”一节。)然而,某些情况下,NSObject
类 仅仅
定义了完成某件事情的模板
,而没有提供所有需要的代码。例如,NSObject 类定义了 description
方法,返回该类内容的字符串表示,该方法主要是用来调试程序 (GDB 中的 print-object 方法就是直接打印出该方法返回的字符串)。NSObject
类中 该方法(description)的实现 并不知道子类中的内容
,所以它只是返回类的名字和对象的地址
。那 NSObject
的子类可以重新实现该方法,以提供更多的信息。例如,NSArray 类改写了该方法来返回 NSArray 类包含的每个对象的内容。
还有一些 NSObject 的方法只是简单地从 Runtime 系统中获取信息,从而允许对象进行一定程度的自我检查。例如:
- -
class
方法 : 返回对象的类; -
-isKindOfClass:
和-isMemberOfClass:
方法 : 检查对象是否存在于指定的类的继承体系中(也可以说成是判断 是否是其子类或者父类或者当前类的成员变量); -
-respondsToSelector:
: 检查对象能否响应指定的消息; -
-conformsToProtocol:
: 检查对象是否实现了指定协议类的方法; -
-methodForSelector:
: 返回指定方法实现的地址。
-
2.3 通过运行时系统的函数
尽管大部分情况下运行时系统的函数
在 Objective-C 程序不是必须的,但是有时候对于 Objecitve-C 程序来说某些函数是非常有用的。 这些函数的文档参见 Objective-C Runtime Reference,也就是 Runtime API 文档。
第三章. Runtime 类与对象
3.1 类与对象基础数据结构
-
3.1.1. 类对象(
class object
)
Objective-C的类
是由Class 类型
来表示的,它实际上是一个指向objc_class结构体
的指针
。在 objc.h 和 runtime.h 中找到对 class 的定义如下:
typedef struct objc_class *Class;
Class 是一个 objc_class
结构类型的指针;那 objc_class 又是怎样一个结构体呢?且看:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
objc_class 结构体的各成员介绍如下:
isa:是一个 objc_class
类型的指针,它指向metaClass(元类)
。所有的类自身也是一个对象,那对象
是对象
,类自身
也是对象
,是不是有点混淆?
别急,Objective-C 中 一个术语来区分这两种不同的对象:
- <1>
类对象(class object)
与<2>实例对象(instance object)
。
Objective-C还对类对象
与实例对象
中的isa
所指向的类结构
作了不同的命名:
- 1>.
类对象(objc_class)
中的isa
指向类结构
被称作元类(Metaclass)
,元类(Metaclass)
就是类对象的类
,每个类都有自己的元类,每个元类又有自己的isa,metaclass
存储类的static类成员变量
与static类成员方法
(+开头的方法
),这也是Objective-C的类方法使用元类的根本原因; - 2>.
实例对象中的 isa
指向类结构称作class
(普通的
),class 结构
存储类的
普通成员变量
与普通成员方法
(-开头的方法
**)。
super_class:指向该类的父类
!如果该类已经是最顶层的根类
(如 NSObject 或 NSProxy),那么 super_class
就为 NULL
。
struct objc_super {
//指定类的实例.
__unsafe_unretained _Nonnull id receiver;
//指定特定的消息实例的超类.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class是第一个要搜索的类 */
};
name:一个 C 字符串,指示类的名称
。我们可以在运行期,通过这个名称查找到该类
(通过:id objc_getClass
(const char *aClassName))或该类的 metaclass
(id objc_getMetaClass
(const char *aClassName));
version:类的版本信息
,默认初始化为 0。我们可以在运行期对其进行修改(class_setVersion)或获取(class_getVersion)。
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;
//该类的成员变量链表,是objc_ivar_list结构体指针
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
}
Ivar是表示实例变量的类型,其实际是一个指向objc_ivar结构体的指针,其定义如下:
//变量结构体---名称,类型,偏移字节和占用的空间
struct objc_ivar {
char * _Nullable ivar_name OBJC2_UNAVAILABLE; //名称
char * _Nullable ivar_type OBJC2_UNAVAILABLE; //变量类型
int ivar_offset OBJC2_UNAVAILABLE; // 基地址偏移字节
#ifdef __LP64__
int space OBJC2_UNAVAILABLE; //占用的空间
#endif
}
methodLists:与 info 的一些标志位有关
,CLS_METHOD_ARRAY 标识位决定其指向的东西(是指向单个 objc_method_list
还是一个 objc_method_list 指针数组
),如果 info 设置了 CLS_CLASS
则 objc_method_list
存储实例方法
,如果设置的是 CLS_META
则存储类方法
;
方法链表结构体:
struct objc_method_list {
struct objc_method_list * _Nullable obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
对象的每个方法的结构体,SEL是方法选择器,是HASH后的值,可以通过这个值找到函数体的实现,IMP 是函数指针
struct objc_method {
SEL _Nonnull method_name //SEL是方法选择器 OBJC2_UNAVAILABLE;
char *_Nullable method_types //方法类型(参数列表),
// `method_types `是个char 指针, `存储`方法的`参数类型`和`返回值类型`。 OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp //method_imp 指向了`方法的实现`,
//本质是一个`函数指针`。 OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE
cache:指向 objc_cache
的指针,用来缓存最近使用的方法
,以提高效率。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,提高了调用的效率
它用于缓存调用过的方法。这个字段是一个指向objc_cache结构体的指针,其定义如下:
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
该结构体的字段描述如下:
- mask:一个整数,指定
分配的缓存bucket的总数
。在方法查找过程中,Objective-C runtime 使用这个字段来确定开始线性查找数组的索引位置
。指向方法selector的指针与该字段做一个AND位操作(index = (mask & selector))。这可以作为一个简单的hash散列算法。 - occupied:一个整数,指定
实际占用的缓存bucket的总数
。 - buckets:指向
Method数据结构指针的数组
。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。
protocols:指向 objc_protocol_list 的指针
,存储该类声明要遵守的正式协议
。
-
3.1.2. 实例对象(
id/objc_object
)
id
指向一个类的实例对象(objc_object)
的指针。id
被定义在 objc/objc.h 目录下,它的数据结构是:
typedef struct objc_object *id;
其中 objc_object
的底层定义
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
可以看到,iOS中很重要的id
实际上就是objc_object
的指针.而NSObject的第一个对象就是Class类型的isa。因此 id可以标示所有基于NSObject的对象。对象可以通过 isa 指针找到其所属的类
。isa
是一个 Class 类型
的成员变量
。
这样,当我们向一个Objective-C对象发送消息时,Runtime会根据实例对象的isa指针
找到这个实例对象所属的类
。然后,Runtime会在类的方法列表
或者是父类的方法列表
中去寻找与消息对应的selector
指向的方法
,找到后即运行这个方法。
注意: isa 指针在代码运行时并不 总指向实例对象所属的类型,所以不能依靠它来确定类型,要想确定类型还是需要用对象的 -class 方法。
-
3.1.3. 元类(
Metaclass
)
meta-class
是一个类对象的类
。当我们向一个对象发送消息
时,runtime会在这个对象所属的这个类的方法列表(-号方法列表
)中查找方法;而向一个类发送消息
时,会在这个类的meta-class
的方法列表(+号方法列表
)中查找。
meta-class
之所以重要,是因为它存储着一个类的所有类方法(+号方法列表)
。每个类都会有一个单独的meta-class
,因为每个类的类方法基本不可能完全相同。
再深入一下,meta-class
也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去, ObjC的设计者让所有的meta-class
的isa
指向基类(·根类·)的meta-class
,以此作为它们的所属类
。即,任何NSObject继承体系下的meta-class
都使用NSObject的meta-class作为自己的所属类
,而基类(·根类·)的meta-class
的isa指针
是指向它自己
。这样就形成了一个完美的闭环。
通过上面的描述,再加上对objc_class结构体中super_class指针的分析,我们就可以描绘出类及相应meta-class类的一个继承体系了。子类
,父类
,根类
(这些都是普通 class
)以及其对应
的 metaclass
的isa
与 super_class
之间关系 如下图所示
- 规则一:类的
实例对象的 isa
指向该类
;该类的 isa
指向该类的 metaclass
;
(理解:Class
的objc_object
的isa
指向该 Class 类,该类(
objc_class)的
isa指向
Class metaClass`) - 规则二:类的
super_class
指向其父类
,如果该类为根类
则值为NULL
;
(理解:Class
的super_class
指向该Class
的父类
,如果该类Class
是最顶层的类
,那么该Class
的super_class
值为NULL
) - 规则三:
metaclass
的isa
指向根 metaclass
,如果该metaclass
是根 metaclass
则指向自身
;
(理解:不管是子类还是父类
的metaclass
的isa
一律 都指向根 metaclass
,如果该 metaclass 是根 metaclass
则指向自身
) - 规则四:
metaclass
的super_class
指向父 metaclass
,如果该 metaclass 是根 metaclass
则指向该 metaclass
对应的类,也就是指向根类
而不是自身metaclass;
注意:
根元类的superclass
不是nil而是根类
。对于ObjC原生的类,根元类
的父类
就是系统的根类NSOject
。但根类不一定是NSObject,因为后面介绍的objc_allocateClassPair
函数也可创建出一个根类。
那么 class 与 metaclass 有什么区别呢?
class
是instance object
的类 类型
。当我们向实例对象发送消息
(实例方法
)时,我们在该实例对象
的 class 结构
的 methodlists方法列表
中去查找响应的函数,如果没找到匹配的响应函数则在该 class
的父类
中的 methodlists 方法列表
去查找(查找链为上图的中间那一排)。如下面的代码中,向str 实例对象
发送 lowercaseString
消息,会在 NSString 类结构
(NSString 的objc_class)的 methodlists 方法列表
中去查找lowercaseString
的响应函数。
NSString * str;
[str lowercaseString];
metaclass
是 class object
的类 类型
。当我们向类对象发送消息(类方法)
时,我们在该类对象的 metaclass(元类) 结构
的 methodlists
中去查找响应的函数,如果没有找到匹配的响应函数则在该 metaclass 的父类
中的 methodlists
去查找(查找链为上图的最右边那一排)。如下面的代码中,向 NSString 类对象
发送 stringWithString 消息,会在 NSString
的 metaclass
类结构的 methodlists
中去查找 stringWithString
的响应函数。
[NSString stringWithString:@"str"]; //stringWithString 是 + 号方法
好,至此我们明白了类的结构层次,来看张图会更明显:
类存储示意图总结:
ObjC 为每个类的定义生成两个 objc_class ,一个即普通的 class,另一个即 metaclass。我们可以在运行时
创建 这两个 objc_class
数据结构,然后使用 objc_addClass
动态地创建新的类定义。**这个够动态够强大的吧?
3.2. 类与对象操作函数
runtime提供了大量的函数来操作类与对象。类的操作方法
大部分是以class_
为前缀的,而对象的操作方法
大部分是以objc_
或object_
为前缀。下面我们将根据这些方法的用途来分类讨论这些方法的使用。
- 3.2.1. 类相关操作函数
我们可以回过头去看看objc_class
的定义,runtime提供的操作类的方法
主要就是针对这个结构体中的各个字段的
。下面我们分别介绍这一些的函数。并在最后以实例来演示这些函数的具体用法。
3.2.1.1 针对类进行操作的函数
针对类进行操作的函数主要有:
/**
*返回类的名称。
*
* @param cls一个类对象。
* @return 类的名称,如果 cls是 Nil,则为空字符串。
*/
//获取类的类名
const char * class_getName ( Class cls );
/**
*返回类的超类。
* @param cls一个类对象。
* @return类的超类,或者Nil if cls是根类,如果cls是Nil,则为Nil。
* @note你 通常应该使用NSObject的超类方法 ,而不是这个函数。
*/
// 获取类的父类
Class class_getSuperclass ( Class cls );
/**
*返回一个布尔值,指示类对象是否是元类。
* @param cls一个类对象。
* @return YES如果cls是元类,如果cls是非元类,则为NO,如果cls为Nil,则为NO。
*/
// 判断给定的Class是否是一个元类
BOOL class_isMetaClass ( Class cls );
//返回类的实例大小
size_t
class_getInstanceSize(Class _Nullable cls);
/**
*返回给定类的指定 实例变量的Ivar 。
*
* @param cls 您希望获取其实例变量的类。
* @param name 要获取的实例变量定义的名称。
* @return 指向包含有关信息的Ivar数据结构的指针
* 由name指定的实例变量。
*/
// 返回给定类的指定实例变量的Ivar
Ivar class_getInstanceVariable(Class cls, const char * name);
/**
*返回给定类的指定 类变量 的Ivar。
*
* @param cls 您希望获取其 类变量 的类定义。
* @param name 要获取的 类变量 定义的名称。
* @return 返回一个指向Ivar数据结构的指针,该结构包含有关name指定的 类变量 的信息。
*/
// 返回 给定类的指定类变量的Ivar
Ivar class_getClassVariable ( Class cls, const char *name );
/**
*向类添加新的实例变量。
*
* @ cls 你要操作的类名
* @ name 成员变量名称
* @ size 开辟字节长度
* @ alignment 对齐方式
* @ types 参数类型 “@”
* @return 如果成功添加了实例变量,则为YES,否则为NO
*(例如,该类已包含具有该名称的实例变量)。
* @note: 只能在 objc_allocateClassPair 之后和 objc_registerClassPair
之前调用此函数。不支持 将实例变量添加到 现有类。
* @note : 该类 不能是元类 。 不支持将实例变量添加到元类。
* @note:实例变量的最小对齐字节数为1 << align。 实例的最小对齐方式
*变量取决于ivar的类型和机器架构。
*对于任何指针类型的变量,传递log2(sizeof(pointer_type))。
*/
// 向类添加新的实例变量
BOOL class_addIvar ( Class cls, const char *name, size_t size,
uint8_t alignment, const char *types );
/**
*描述类声明的 实例变量列表。
*
* @param cls要操作类。
* @param outCount返回时,包含返回数组的长度。
*如果outCount为NULL,则不返回长度。
* @return 类型为 Ivar的指针数组 ,描述类声明的实例变量。
* 不包括超类 声明的 任何实例变量。 该数组包含* outCount
*指针后跟一个NULL终止符。 您必须使用free() 释放数组。
*如果类声明没有实例变量,或者cls为Nil,则返回NULL并且* outCount为0。
*/
// 获取整个实例变量列表,获取的不仅有@property声明的变量还有不是@property声明的变量。
Ivar * class_copyIvarList(Class cls, unsigned int * outCount) ;
/**
*设置给定类的Ivar布局。
*
* @param cls要修改的类。
* @param layout cls的 Ivars的布局。
*/
void class_setIvarLayout ( Class cls, const uint8_t *layout );
/**
*返回给定类的 Ivar布局的描述。
*
* @param cls要检查的类。
* @return cls的 Ivar布局的描述。
*/
const uint8_t * class_getIvarLayout ( Class cls );
/**
*为给定的类设置弱Ivars的布局。
*
* @param cls要修改的类。
* @param layout cls的弱Ivars布局。
*/
void class_setWeakIvarLayout ( Class cls, const uint8_t *layout );
/**
*返回给定类的弱Ivars布局的描述。
*
* @param cls要检查的类。
* @return 对 cls的弱 Ivars布局的描述。
*/
const uint8_t * class_getWeakIvarLayout ( Class cls );
/**
*返回具有给定类的给定名称的属性。
*
* @param cls您要操作类。
* @param name 要操作的属性的名称。
* @return 返回描述属性的 objc_property_t 类型的 指针,
如果类没有声明具有该名称的属性,则为NULL;如果cls为Nil,则为NULL。
*/
// 返回具有给定类的给定名称的属性
objc_property_t class_getProperty ( Class cls, const char *name );
/**
描述 类声明 的 属性列表。
*
* @param cls是您要操作的类。
* @param *outCount ,输出指针,指明返回objc_property_t类型数组的大小。
*如果 outCount 为 NULL,则 不返回长度。
*@返回 objc_property_t 类型 的 指针数组 ,该数组描述 类声明的属性 。
不包括超类 声明的任何属性。数组包含*outCount指针,后跟NULL结束符。
必须使用free()释放数组。
*
*如果cls声明没有属性,或者cls为Nil,则返回NULL和*outCount为0。
*/
// 获取类声明 的 属性列表,获取到的只有 @property声明的变量
objc_property_t * class_copyPropertyList ( Class cls,
unsigned int *outCount );
/**
*向具有 给定名称 和 实现的类 添加 新方法。
*
* @param cls 要添加方法的类。
* @param name 一个选择器,指定要添加的方法的名称的选择器。
* @param imp 是新方法的实现函数,它是新方法的实现。
该函数必须至少有两个参数 - self和_cmd。
* @param types 是 一个字符数组,用于描述方法参数的类型。
* @return YES如果成功添加方法,否则为NO
*(例如,该类已包含具有该名称的方法实现)。
* class_addMethod的实现会覆盖父类的方法实现,
*但不会替换此类中的已存在的实现。
*要更改现有实现,请使用method_setImplementation。
*/
//向具有给定名称和实现的类添加新方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );
/**
*返回给定类的指定实例方法。
*
* @param cls您要操作的类。
* @param name 指定要检索的方法的选择器。
* @return 与指定的选择器的实现 相对应的方法
* 由 cls指定的类的名称,或 如果指定的类或其为NULL
*父类不包含具有指定选择器的实例方法。
此函数在父类中搜索实现,而 class_copyMethodList则不在。
*/
// 返回给定类的指定实例方法
Method class_getInstanceMethod ( Class cls, SEL name );
/**
*返回指向描述给定类的给定类方法的数据结构的指针。
*
* @param cls指向类定义的指针。 传递包含要检索的方法的类。
* @param name类型为 SEL的指针。 传递要检索的方法的选择器。
* @return 一个指向 方法数据结构的指针,对应于该实现
*由aSelector指定的选择器,用于由aClass指定的类,如果指定,则为NULL
*类或其父类不包含具有指定选择器的实例方法。
* 请注意,此函数在父类中搜索实现,而 class_copyMethodList没有。
*/
// 获取类方法,根据给定类和给定selector获取Method 的数据结构的指针
Method class_getClassMethod ( Class cls, SEL name );
/**
*描述类实现的实例方法。
* @param cls您要操作的类。
* @param outCount返回时,包含返回数组的长度。
*如果outCount为NULL,则不返回长度。
*
* @return 描述实例方法的Method类型的指针数组
*由类实现 - 不包括父类实现的任何实例方法。
*该数组包含* outCount指针,后跟一个NULL终止符。 您必须使用free()释放数组。
*
*如果cls没有实现实例方法,或者cls为Nil,则返回NULL并且* outCount为0。
*
* 要获取类的类方法,请使用 class_copyMethodList (object_getClass(cls),&count)。
*
* 获取可由父类实现的方法的实现,使用 class_getInstanceMethod
或 class_getClassMethod。
*/
// 获取所有实例方法(-号方法)的数组
Method * class_copyMethodList ( Class cls, unsigned int *outCount );
/**
*替换给定类的方法的实现。
*
* @param cls 您要修改的类。
* @param name 一个选择器,用于标识要 替换其实现的方法。
* @param imp 由 cls标识 的 类的名称 标识的方法的新实现。
* @param types一组字符,用于描述方法参数的类型。
*由于该函数必须至少有两个参数 - self和_cmd,第二个和第三个字符
*必须是“@:”(第一个字符是返回类型)。
*
* @return 返回 由 cls标识的类的 name标识的方法的先前实现。
* @note 此函数以两种不同的方式运行:
* - 如果 name指定的方法尚不存在,则添加它,就像调用了 class_addMethod一样。
*由 types指定的类型编码用于给定。
* - 如果由 name标识的方法确实存在,则将其 IMP替换为调用method_setImplementation。
*忽略类型指定的类型编码。
*/
// 替代方法的实现,替换给定类的方法的实现。
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );
/**
返回一个函数指针,如果一个特定的消息被发送到一个类的实例中,该函数指针将被调用。
*
* @param cls 你想要操作的类。
* @param name 一个选择器。
*
* @return 一个函数指针,如果用类的实例调用[object name],
就会调用它;如果cls是Nil,就会调用NULL。
*
* class_getMethodImplementation可能比method_getImplementation
(class_getInstanceMethod, cls, name)更快。
*
* @注意返回的函数指针可能是运行时内部的函数,而不是一个实际的方法实现。
例如,如果类的实例不响应选择器,返回的函数指针将成为运行时消息转发机制的一部分。
*/
// 返回方法的具体实现
IMP class_getMethodImplementation ( Class cls, SEL name );
/**
返回一个函数指针,如果一个特定的消息被发送到一个类的实例中,该函数指针将被调用。
*
* @param cls 你想要操作的类。
* @param name 一个选择器。
* @return 调用 [object name] 时将调用的函数指针
*带有类的实例,或者 NULL(如果 cls是 Nil)。
*/
IMP class_getMethodImplementation_stret ( Class cls, SEL name );
// 类的实例是否响应指定的selector
BOOL class_respondsToSelector ( Class cls, SEL sel );
/**
*向类添加属性。
*
* @param cls 要修改的类。
* @param name 属性的名称。
* @param attributes 属性特性数组。
* @param attributeCount属性中的属性数。
*
* @return如果已成功添加属性,则为YES,否则为NO(例如,类已具有该属性)。
*/
// 为类添加属性
BOOL class_addProperty ( Class cls, const char *name,
const objc_property_attribute_t *attributes, unsigned int attributeCount );
/**
*替换类的属性。
*
* @param cls要修改的类。
* @param name属性的名称。
* @param attributes 属性特性数组。
* @param attributeCount 属性中的属性数。
*/
// 替换类的属性
void class_replaceProperty ( Class cls, const char *name,
const objc_property_attribute_t *attributes, unsigned int attributeCount );
// 添加协议
BOOL class_addProtocol ( Class cls, Protocol *protocol );
// 返回类是否实现指定的协议
BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );
// 返回类实现的协议列表
Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount );
// 获取版本号
int class_getVersion ( Class cls );
// 设置版本号
void class_setVersion ( Class cls, int version );
-
class_getInstanceVariable
函数:获取 类 中指定名称实例成员变量的信息
。它返回一个指向包含name
指定的成员变量信息
的objc_ivar结构体的指针
(Ivar)。 -
class_getClassVariable
函数:可以获取类成员变量的信息
。目前没有找到关于Objective-C中类变量的信息,一般认为Objective-C不支持类变量。注意,返回的列表不包含父类的成员变量和属性。 -
class_addIvar
函数:可以添加成员变量
。 Objective-C不支持往已存在的类中添加实例变量,因此不管是系统库提供的类,还是我们自定义的类,都无法动态添加成员变量。但如果我们通过运行时来创建一个类的话,又应该如何给它添加成员变量呢?
这时我们就可以使用class_addIvar函数了。不过需要注意的是,这个方法只能在objc_allocateClassPair
函数与objc_registerClassPair
之间调用。另外,这个类也不能是元类
。成员变量的按字节最小对齐量是1<<alignment。这取决于ivar的类型和机器的架构。如果变量的类型是指针类型,则传递log2(sizeof(pointer_type))。 -
class_copyIvarList
函数:可以获取整个成员变量列表
。它返回一个指向成员变量信息的数组,数组中每个元素
是指向该成员变量信息的objc_ivar结构体的指针
。这个数组不包含在父类中声明的变量。outCount指针返回数组的大小。需要注意的是,我们必须使用free()来释放这个数组。 -
class_addMethod
函数:向具有给定名称和实现的类添加新方法。class_addMethod
的实现会覆盖
父类的方法实现
,但不会取代
本类中已存在的实现,如果本类中包含一个同名的实现,则函数会返回NO。如果要修改已存在实现
,可以使用method_setImplementation
。一个Objective-C方法是一个简单的C函数,它至少包含两个参数–self和_cmd。所以,我们的实现函数(IMP参数指向的函数)至少需要两个参数,如下所示:
void myMethodIMP(id self, SEL _cmd)
{
// implementation ....
}
与成员变量不同的是
,我们可以为类动态添加方法
,不管
这个类是否已存在
。
另外,参数types
是一个描述传递给方法的参数类型的字符数组,这就涉及到类型编码
。
-
class_getInstanceMethod 获取实例方法
函数、class_getClassMethod 获取类方法
函数,与class_copyMethodList 获取所有方法的数组
不同的是,这两个函数都会去搜索父类的实现
。 -
class_copyMethodList
函数:获取所有方法的数组。返回包含所有实例方法的数组
。
如果需要·获取类方法·,则可以使用class_copyMethodList
(object_getClass(cls)
,&count
)(一个类的实例方法
是定义在元类里面
)。该列表不包含父类实现的方法。outCount
参数返回方法的个数
。在获取到列表后,我们需要使用free()方法来释放它
。 -
class_replaceMethod
函数:替代方法的实现
。
该函数的行为可以分为两种:- 如果类中
不存在name
指定的方法,则类似于class_addMethod
函数一样会添加方法; - 如果类中
已存在name
指定的方法,则类似于method_setImplementation
一样修改已存在实现
来替代原方法的实现
。
- 如果类中
-
class_getMethodImplementation
函数 :返回方法的具体实现
。该函数在向类实例发送消息时
会被调用,并返回一个指向方法实现函数
的指针
。这个函数会比method_getImplementation(class_getInstanceMethod(cls, name))更快
。返回的函数指针可能是一个指向runtime内部的函数
,而不一定是方法的实际实现
。例如,如果类实例无法响应selector
,则返回的函数指针将是运行时消息转发机制的一部分
。 -
class_respondsToSelector
函数,我们通常使用NSObject类的respondsToSelector:
或instancesRespondToSelector:
方法来达到相同目的。 -
class_conformsToProtocol
函数:返回 类是否实现指定的协议
。你通常应该使用NSObject类的conformsToProtocol:
方法来替代,而不是这个函数。 -
class_copyProtocolList
函数:返回 类实现的协议列表
。在使用后我们需要使用free()手动释放
。 -
在MAC OS X系统中**,我们可以使用垃圾回收器。runtime提供了
class_setIvarLayout
、class_getIvarLayout
、class_setWeakIvarLayout
、class_getWeakIvarLayout
、几个函数来确定一个对象的内存区域是否可以被垃圾回收器扫描
,以处理strong/weak
引用。但通常情况下,我们不需要去主动调用
这些方法;在调用objc_registerClassPair
时,会生成合理的布局。在此不详细介绍这些函数 -
3.2.1.2 获取类定义
Objective-C动态运行库会自动注册我们代码中定义的所有的类。我们也可以在运行时创建类定义并使用objc_addClass函数来注册它们。runtime提供了一系列函数来获取类定义相关的信息,这些函数主要包括:
// 获取已注册的类定义的列表
int objc_getClassList ( Class *buffer, int bufferCount );
// 创建并返回一个指向所有已注册类的指针列表
Class * objc_copyClassList ( unsigned int *outCount );
/**
*返回指定类的类定义。
*
* @param name 要查找的类的名称。
* @return 指定类的Class对象,如果该类未在Objective-C运行时注册,
则为nil。
* @note: objc_getClass 与此函数的不同之处在于:
1.如果未注册类,则 objc_getClass 调用类处理程序回调,
2.然后再次检查以查看该类是否已注册。
3. 此函数不会调用类处理程序回调。
*/
// 返回指定类的类定义
Class objc_lookUpClass ( const char *name );
Class objc_getClass ( const char *name );
/**
*返回指定类的类定义。
*
* @param name要查找的类的名称。
* @return指定类的Class对象。
* @note :此函数与 objc_getClass相同,但如果找不到类,则会终止进程。
* @note ZeroLink使用此函数,如果没有找到类,
则会出现没有ZeroLink的编译时链接错误。
*/
Class objc_getRequiredClass ( const char *name );
// 返回指定类的元类
Class objc_getMetaClass ( const char *name );
-
获取类定义的方法有三个:
objc_lookUpClass
,objc_getClass
和objc_getRequiredClass
。如果类在运行时未注册
,则objc_lookUpClass
会返回nil,而objc_getClass
会调用类处理回调,并再次确认
类是否注册,如果确认未注册,再返回nil。而objc_getRequiredClass
函数的操作与objc_getClass
相同,只不过如果没有找到类,则会杀死进程
。 -
objc_getMetaClass
函数:如果指定的类没有注册
,则该函数会调用类处理回调,并再次确认
类是否注册,如果确认未注册,再返回nil
。不过,每个类定义都必须有一个有效的元类定义,所以这个函数总是会返回一个元类定义,不管它是否有效。 -
objc_getClassList
函数:获取已注册
的类定义的列表
。我们不能假设从该函数中获取的类对象是继承自NSObject体系的,所以在这些类上调用方法是,都应该先检测一下这个方法是否在这个类中实现。
-
3.2.1.3
成员变量(ivars)及属性 -
3.2.1.4
方法(methodLists) -
3.2.1.5
协议(objc_protocol_list) -
3.2.1.6
. 其它
由于篇幅太长,不能写在一篇文章中,具体详情参考:Objective-C Runtime 运行时之二:成员变量、属性和方法
- 3.2.2. 动态创建类和对象
runtime的强大之处在于它能在运行时创建类和对象
。
我们来如何在运行
时动态创建类。下面这个函数就是应用前面讲到的Class,MetaClass的概念,在运行时动态创建一个类。这个函数来自《Inside Mac OS X-The Objective-C Programming Language》。
- 1>.
动态创建类
动态创建类
涉及到以下几个函数:
/**
*创建一个新类和元类。
*
* @param superclass 用作新类的父类的类,或者 Nil用于创建新的根类。
* @param name 用作新类名的字符串。该字符串将被复制。
* @param extraBytes 在类和元类对象的末尾为索引的ivars分配的字节数。通常应该是 0。
*
* @return 新类,如果无法创建类,则为Nil(例如,所需的名称已在使用中)。
*
* @note 您可以通过调用 object_getClass(newClass)来获取指向新元类的指针。
* @note要创建一个新类,请先调用 objc_allocateClassPair。
*然后使用 class_addMethod和 class_addIvar等函数设置类的属性。
*完成构建类后,请调用 objc_registerClassPair。新类现在可以使用了。
* @note应将实例方法和实例变量添加到类本身。
*类方法应添加到元类中。
*/
// 创建一个新类和元类
Class objc_allocateClassPair ( Class superclass, const char *name,
size_t extraBytes );
// 销毁一个类及其相关联的类
void objc_disposeClassPair ( Class cls );
// 在应用中注册由objc_allocateClassPair创建的类
void objc_registerClassPair ( Class cls );
为了创建一个新类,我们需要调用objc_allocateClassPair
。然后使用诸如class_addMethod
,class_addIvar
等函数来为新创建的类添加方法
、实例变量
和属性
等。完成这些后,我们需要调用objc_registerClassPair
函数来注册类
,之后这个新类就可以在程序中使用了。
实例方法
和实例变量
应该添加到类自身
上,而类方法
应该添加到类的元类
上。
-
objc_disposeClassPair
函数:用于销毁一个类。不过需要注意的是,如果程序运行中
还存在类或其子类的实例
,则不能调用
针对类调用该方法。
在前面介绍元类时,我们已经有接触到这几个函数了,在此我们再举个实例来看看这几个函数的使用。
实例(Example)
//-----------------------------------------------------------
@interface MyClass : NSObject <NSCopying, NSCoding>
- (void)beReplacedMethod;
@end
//-----------------------------------------------------------
// MyClass.m
#import "MyClass.h"
@interface MyClass ()
@end
@implementation MyClass
- (void)beReplacedMethod {
NSLog(@"Adds an IMP to your new method in a new class");
}
@end
//-----------------------------------------------------------
- (void)viewDidLoad {
MyClass *myClass = [[MyClass alloc] init];
Class cls = myClass.class;
// 类名
NSLog(@"class name: %s", class_getName(cls));
NSLog(@"=====================================");
// 父类
NSLog(@"The name of the superclass of MyClass is: %s", class_getName(class_getSuperclass(cls)));
NSLog(@"=====================================");
// 是否是元类
NSLog(@"MyClass is %@ a meta-class", (class_isMetaClass(cls) ? @"" : @"not"));
NSLog(@"======================================");
Class meta_class = objc_getMetaClass(class_getName(cls));
NSLog(@"%s's meta-class is %s", class_getName(cls), class_getName(meta_class));
NSLog(@"======================================");
// 变量实例大小
NSLog(@"instance size: %zu", class_getInstanceSize(cls));
NSLog(@"======================================");
// 创建一个新类和元类 MyClass.class为新类MySubClass的父类
Class cls = objc_allocateClassPair(MyClass.class, "MySubClass", 0);
/*
您必须使用Objective-C运行时系统注册方法名称才能
获得方法的选择器,然后才能将方法添加到类定义中。
如果方法名称已经注册,此函数只返回选择器。
*/
SEL sel = sel_registerName ("beReplacedMethod");
// class_getMethodImplementation 返回方法的具体实现
//获取 MyClass 类中 beReplacedMethod 方法选择器的 方法实现地址IMP
IMP imp = class_getMethodImplementation([MyClass class],sel );
//给新创建的 MySubClass 类 添加名称为 findInSelf的新方法,imp 是新方法的方法实现地址
class_addMethod(cls, @selector(findInSelf),imp , "v@:");
/**
* @note: 只能在 objc_allocateClassPair 之后和 objc_registerClassPair
之前调用此函数。不支持 将实例变量添加到 现有类。
* @note : 该类 不能是元类 。 不支持将实例变量添加到元类。
*/
// 添加实例变量
class_addIvar(cls, "_ivar1", sizeof(NSString *),
log(sizeof(NSString *)), "i");
//属性的特性(attribute)
objc_property_attribute_t type = {"T", "@\"NSString\""};
//属性的特性(attribute)
objc_property_attribute_t ownership = { "C", "" };
//属性的特性(attribute)
objc_property_attribute_t backingivar = { "V", "_ivar1"};
//属性的特性数组
objc_property_attribute_t attrs[] = {type, ownership, backingivar};
// 为类添加属性
class_addProperty(cls, "property2", attrs, 3);
// 在应用中注册由objc_allocateClassPair创建的类
objc_registerClassPair(cls);
//-----------------------------------------------------------
//开始调用
id instance = [[cls alloc] init];
//在这里使用 performSelector 来向新类的对象发送消息,
//可以避免编译警告信息(因为我们并没有声明该类及其可响应的消息)
[instance performSelector:@selector(findInSelf)];
}
程序的输出如下:
class name: MyClass
The name of the superclass of MyClass is: NSObject
MyClass is not a meta-class
MyClass's meta-class is MyClass
instance size: 48
Adds an IMP to your new method in a new class
- 2>.动态创建对象
动态创建对象
的函数如下:
// 创建类实例,为该类中的类分配内存,默认的malloc内存区域
id class_createInstance ( Class cls, size_t extraBytes );
// 在指定位置创建类实例
id objc_constructInstance ( Class cls, void *bytes );
// 销毁类实例
void * objc_destructInstance ( id obj );
-
class_createInstance
函数:创建实例时,会在默认的内存区域
为类分配内存
,调用class_createInstance
的效果与+alloc
方法类似。extraBytes
参数表示分配的额外字节数
。这些额外的字节可用于存储在类定义中
所定义的实例变量之外
的实例变量
。该函数在ARC环境下无法使用。
实例操作函数
实例操作函数
主要是针对我们创建的实例对象
的一系列操作函数,我们可以使用这组函数来从实例对象中获取我们想要的一些信息,如实例对象中变量的值。
// 返回指定对象的一份拷贝
id object_copy ( id obj, size_t size );
// 释放指定对象占用的内存
id object_dispose ( id obj );
/**
*更改类实例的实例变量的值。
*
* @param obj 指向类实例的指针。 传递包含要修改其值的实例变量的对象。
* @param name :一个C字符串。 传递要修改其值的实例变量的名称。
* @param value: 实例变量的新值。
* @return : 指向Ivar数据结构的指针,该结构定义由name指定的实例变量的类型和名称。
* @note: 具有已知内存管理的实例变量(例如ARC强和弱)使用该内存管理。
分配具有未知内存管理的实例变量,就好像它们是unsafe_unretained一样。
*/
// 修改类实例的实例变量的值
Ivar object_setInstanceVariable ( id obj, const char *name, void *value );
// 获取对象实例变量的值, outValue: 返回时包含指向实例变量值的指针。
Ivar object_getInstanceVariable ( id obj, const char *name, void **outValue );
// 返回指向给定对象分配的任何额外字节的指针
void * object_getIndexedIvars ( id obj );
/**
*读取对象中实例变量的值。
*
* @param obj包含要读取其值的实例变量的对象。
* @param ivar Ivar描述要读取其值的实例变量。
* @return : 由ivar指定的实例变量的值,如果object为nil,则为nil。
* @note:如果已知实例变量的Ivar,则object_getIvar比object_getInstanceVariable快。
*/
//如果使用ARC,则在获取`不是对象`的`返回值`时,执行项目会导致项目崩溃。但是如果`自己创建的类`,然后`自己添加的属性`,就可以用这个函数获取返回值(不管什么类型)
// 返回对象中实例变量的值
id object_getIvar ( id obj, Ivar ivar );
// 设置对象中实例变量的值
void object_setIvar ( id obj, Ivar ivar, id value );
// 返回给定对象的类名
const char * object_getClassName ( id obj );
// 返回对象的类
Class object_getClass ( id obj );
/**
*设置对象的类。
*
* @param obj要修改的对象。
* @param cls一个类对象。
* @return对象类的先前值,或如果对象为nil,则为Nil。
*/
// 设置对象的类
Class object_setClass(id obj, Class cls);
- 如果实例变量的
Ivar
已经知道
,那么调用object_getIvar
会比object_getInstanceVariable
函数快
,相同情况下,object_setIvar
也比object_setInstanceVariable
快。
常见面试题
1.runtime 怎么添加成员变量,属性,方法等?
2.能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
参考答案:(自己总结的,若有误或更好的答案,请留言
)
- 不能向编译后得到的类中增加实例变量;
- 能向运行时创建的类中添加实例变量;
- 分析如下:
- 因为
编译后
的类已经注册
在runtime中,类结构体中的objc_ivar_list
实例变量的链表和instance_size
实例变量的内存大小已经确定
,同时runtime 会调用class_setIvarLayout 或 class_setWeakIvarLayout来处理strong weak
引用,所以不能向存在的类中添加实例变量 -
运行时创建的类
是可以添加
实例变量,调用class_addIvar
函数,但是得在调用objc_allocateClassPair
之后,objc_registerClassPair
之前,原因同上。
- 因为
所谓静态语言
,就是在程序运行前决定了所有的类型判断,类的所有成员
、方法
在编译阶段
就确定
好了内存地址
。也就意味着所有类对象
只能访问属于自己
的成员变量
和方法
,否则编译器直接报错。
添加属性 添加方法而
动态语言
,恰恰相反,类型的判断
、类的成员变量
、方法的内存地址都是在程序的运行阶段才最终确定,并且还能动态的添加成员变量和方法。也就意味着你调用一个不存在的方法时,编译也能通过,甚至一个对象它是什么类型并不是表面我们所看到的那样,只有运行之后才能决定其真正的类型。相比于静态语言,动态语言具有较高的灵活性和可订阅性。而oc,正是一门动态语言。
小结
在这一章中我们介绍了Runtime
运行时中与类和对象相关的数据结构,通过这些数据函数,我们可以管窥Objective-C底层面向对象实现的一些信息。另外,通过丰富的操作函数,可以灵活地对这些数据进行操作。
参考资料:
Objective-C Runtime Programming Guide
Objective-CRuntime Reference
http://southpeak.github.io/2014/10/25/objective-c-runtime-1/