本文章是我照着南峰子的runtime博客写的,加上了一点自己的见解,作为自己的学习笔记,侵删。
附上原博客地址:南峰子的技术博客
如有错误的地方欢迎指出。
runtime 运行时之一:类与对象
Object-C是一门动态语言,它将很多静态语言在编译和连接时期做的事情放到了运行时来做。
优势:代码更具有灵活性。
这种特性意味着Objective-C不仅需要一个编译器,还需要一个运行时系统来执行编译的代码。这个运行时系统就是Objc Runtime
。
Objc Runtime
其实就是一个runtime
库,它基本上是用C和汇编写的。这个库使得C语言有了面向对象的能力。
Runtime库主要做了下面几件事情:
- 封装:在这个库中,对象可以用C语言的结构体表示,方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被Runtime函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和他们的方法了。
- 找出方法的最终执行代码:当程序执行
[object doSomething]
时,会向消息接受者(object)发送一条消息(doSomething),runtime会根据消息接收者是否能响应该消息而做出不同的反应。这将在后面详细介绍。
所以我们会说Objective-C的底层是C语言。
Objective-C runtime目前有两个版本:Modern runtime和Legacy runtime。Modern Runtime覆盖了64位的Mac OS X Apps,还有iOS Apps,Legacy Runtime是早期用来给32位 Mac OS X Apps 用的,也就是可以不用管就是了。
类和对象基础数据结构
Class
Objective-C类是由Class类型来表示的,他实际上是一个指向objc_class
结构体的指针。它的定义如下:
typedef struct objc_class *Class;
查看objc/runtime.h
中objc_class
结构体的定义如下:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
#endif
} OBJC2_UNAVAILABLE;
在定义中,下面几个字段使我们感兴趣的
-
isa
:在Object-C中,所有实例的isa指针指向类,所有的类自身也是一个对象,这个对象的Class
也有一个isa
指针,它指向metaClass
(元类),我们会在后面介绍它。 -
super_class
:指向该类的父类,如果该类已经是最顶层的跟类(如NSObject或NSProxy),则super_class
为NULL。 -
cache
:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。 -
version
:我们使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例布局的改变。
objc_cache
上面提到了objc_class
结构体中的 cache
字段,它用于缓存调用过的方法。这个字段是一个指向objc_cache
结构体的指针,其定义如下:
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
-
mask
:一个整数,指定分配的缓存的buckets
的总数。在方法查找过程中,Objective-C runtime使用这个字段来确定开始线性查找数组的索引位置。指向方法selector
的指针与该字段做一个AND
位操作(index = (mask & selector))
。这可以作为一个简单的hash
散列算法。 -
occupied
:一个整数,指定实际占用的缓存bucket
的总数。 -
buckets
:指向Method
数据结构指针的数组。这个数组可能包括不超过msak+1个元素。需要注意的是,指针可能是NULL,表示这个缓存Bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能随着时间而增长。
元类(Meta Class)
所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用方法)
NSArray *array = [NSArray array];
这个例子中,+array
消息发送给了NSArray类,这个NSArray也是一个对象,那么它也是一个objc_object
指针,它包含一个指向其类的一个isa
指针。为了调用+array
方法,这个类的isa
指针必须指向包含这些类方法的一个objc_object
结构体。这就是meta-class
(元类)。
meta-class是一个类对象的类
我们向一个对象发送消息的时候,runtime会在这个对象所属的这个类的方法列表中查找方法,而向一个类发送消息的时候,会在这个类的meta-class
的方法列表中查找方法。
meta-class
存储着一个类的所有类方法。每一个类都有单独的meta-class
,因为每个类的类方法不可能完全相同。
Object-C的设计者让所有的meta-class
的isa
都指向基类的meta-class
,以此作为它们的所属类。所以,任何NSObject继承体系下的meta-class的isa指针都是NSObject的meta-class,而基类的meta-class指向它自己。这样就形成了一个完美的闭环。
下图很好的说明了meta-class、类和对象的关系:
也就是说实例的isa指向类,类对象的isa指向这个类的meta-class(元类),而meta-class的isa指向NSOject的meta-class,NSObject的元类的isa指向本身
类与对象操作函数
runtime提供了大量的函数来操作类与对象。类的方法大部分是以class_
作为前缀。
注:所有copy操作返回的列表都需要用
free()
函数手动释放。
类相关操作函数
// 获取类的类名
const char * class_getName ( Class cls );
// 获取类的父类
Class class_getSuperclass ( Class cls );
// 判断给定的Class是否是一个元类
BOOL class_isMetaClass ( Class cls );
// 获取实例大小 size_t用%zu输出
size_t class_getInstanceSize ( Class cls );
// 获取类中指定名称实例成员变量的信息
Ivar class_getInstanceVariable ( Class cls, const char *name );
// 获取类成员变量的信息
Ivar class_getClassVariable ( Class cls, const char *name );
// 添加成员变量
BOOL class_addIvar ( Class cls, const char *name, size_t size, uint8_t alignment, const char *types );
// 获取整个成员变量列表
Ivar * class_copyIvarList ( Class cls, unsigned int *outCount );
// 获取指定的属性
objc_property_t class_getProperty ( Class cls, const char *name );
// 获取属性列表
objc_property_t * class_copyPropertyList ( Class cls, unsigned int *outCount );
// 为类添加属性
BOOL class_addProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );
// 替换类的属性
void class_replaceProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );
方法(methodLists)
// 添加方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );
// 获取实例方法
Method class_getInstanceMethod ( Class cls, SEL name );
// 获取类方法
Method class_getClassMethod ( Class cls, SEL name );
// 获取所有方法的数组
Method * class_copyMethodList ( Class cls, unsigned int *outCount );
// 替代方法的实现
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );
// 返回方法的具体实现
IMP class_getMethodImplementation ( Class cls, SEL name );
IMP class_getMethodImplementation_stret ( Class cls, SEL name );
// 类实例是否响应指定的selector
BOOL class_respondsToSelector ( Class cls, SEL sel );
-
class_addMethod
会覆盖父类的方法,但不会取代本类中已存在的实现,如果本类中国有同名的实现,则会返回NO。可以使用method_serImplementation
来修改已经实现的方法。 -
一个Objective-C方法是一个简单的C函数,它至少有两个参数
self
和_cmd
,所以我们的实现函数(IMP参数指向的函数)也至少需要两个参数,如下:
void myMethodIMP(id self, SEL _cmd) {
// implementation...
}
这里我查了一下IMP与SEL的区别:
- IMP是真正的函数指针,指向方法实现。
- Objective-C在运行时所有的方法可以看成一张表,而SEL可以看做是表中每一条的索引。
-
- (IMP)methodForSelector:(SEL)aSelector
方法可以根据一个实例SEL得到该方法的IMP(函数指针)+ (IMP)instanceMethodForSelector:(SEL)aSelector
通过类的SEL返回IMP。 - 个人见解:SEL和IMP是映射关系,SEL可以通过runtime来更改它的IMP(函数指针),而IMP是不能更改它指向的函数,也就是说每一个c函数对应一个IMP,而SEL是动态的。
与成员变量不同的是,我们可以为类动态添加方法,不管这个类是否存在。
-
class_getInstanceMethod
、class_getClassMethod
函数会去搜索父类的实现,而class_copyMethodList
不会搜索父类的实现。
-
class_copyMethodList
函数返回包含所有实例方法的列表,而不包含类方法。如果需要获取到类方法,可以使用object_getClass()
来得到meta-class从而得到类方法,简便写法:class_copyMethodList(object_getClass(cls),&count)
-
class_replaceMethod
函数:如果类中不存在name指定的方法则添加一个方法,如果类中存在name指定的方法则替代它的实现。 -
class_getMethodImplementation
函数,该函数会在向类实例发送消息时被调用,返回一个指向方法实现函数的指针。如果这个类无法响应selector
返回的函数指针可能是一个指向runtime内部的函数,这也是运行时消息转发机制的一部分。 -
class_respondsTpSelector
函数和NSObject类的respondsToSelector:
和instancesRespondToSelector:
作用相同。
协议(objc_protocal_list)
// 添加协议
BOOL class_addProtocol ( Class cls, Protocol *protocol );
// 返回类是否实现指定的协议
BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );
// 返回类实现的协议列表
Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount );
-
class_conformsToProtocol
可以用NSObject类的conformsToProtocol
方法来替代。
版本(version)
// 获取版本号
int class_getVersion ( Class cls );
// 设置版本号
void class_setVersion ( Class cls, int version );
动态创建类和对象
runtime的强大之处在于它能在运行时创建类和对象
动态创建类
// 创建类实例
id class_createInstance ( Class cls, size_t extraBytes );
// 在指定位置创建类实例
id objc_constructInstance ( Class cls, void *bytes );
// 销毁类实例
void * objc_destructInstance ( id obj );
-
class_createInstance
函数在创建实例是,会在默认的内存区域中为类分配内存。extraBytes
参数表示分配的额外字节数。这些额外字节数可用于动态添加方法和变量。该函数在ARC环境下无法使用。
调用class_createInstance
的效果与+alloc
方法相似。不过使用class_createInstance
时,我们需要知道它的类是什么。
-
objc_constructInstance
函数:在指定的位置(bytes)创建实例。 -
objc_destructInstance
函数:销毁一个类的实例,但不会释放并移除任何与其相关的引用。
实例操作函数
- 针对整个对象进行操作的函数。
// 返回指定对象的一份拷贝
id object_copy ( id obj, size_t size );
// 释放指定对象占用的内存
id object_dispose ( id obj );
如果有两个类分别为A类和B类,并且B类是A类的子类。B类添加了一些额外的属性。现在我们创建了一个A类的实例对象,想要在运行时将这个对象转化为B类的实例对象,从而添加B类的属性。这种情况下,B类的实例对象会比A类的实例对象分配的空间大。此时,我们就需要使用以上几个函数来处理这种情况。
NSObject *a = [[NSObject alloc] init];
id newB = object_copy(a, class_getInstanceSize(MyClass.class));
object_setClass(newB, MyClass.class);
object_dispose(a);
2.针对对象实例变量进行操作的函数
// 修改类实例的实例变量的值
Ivar object_setInstanceVariable ( id obj, const char *name, void *value );
// 获取对象实例变量的值
Ivar object_getInstanceVariable ( id obj, const char *name, void **outValue );
// 返回指向给定对象分配的任何额外字节的指针
void * object_getIndexedIvars ( id obj );
// 返回对象中实例变量的值
id object_getIvar ( id obj, Ivar ivar );
// 设置对象中实例变量的值
void object_setIvar ( id obj, Ivar ivar, id value );
3.针对对象的类进行操作的函数
// 返回给定对象的类名
const char * object_getClassName ( id obj );
// 返回对象的类
Class object_getClass ( id obj );
// 设置对象的类
Class object_setClass ( id obj, Class cls );
获取类定义
Object-C动态运行库会自动注册我们代码中定义的多有类。我们也可以在运行时创建类定义并使用objc_addClass
函数注册它们。Runtime提供了一系列函数来获取类定义相关的信息,这些函数主要包括:
// 获取已注册的类定义的列表
int objc_getClassList ( Class *buffer, int bufferCount );
// 创建并返回一个指向所有已注册类的指针列表
Class * objc_copyClassList ( unsigned int *outCount );
// 返回指定类的类定义
Class objc_lookUpClass ( const char *name );
Class objc_getClass ( const char *name );
Class objc_getRequiredClass ( const char *name );
// 返回指定类的元类
Class objc_getMetaClass ( const char *name );
-
objc_getClassList
函数:获取已注册的类定义的列表。 - 获取类定义的方法有三个:
objc_lookUpClass
,objc_getClass
和objc_getRequiredClass
。如果类在运行时未注册,则objc_lookUpClass
会返回nil,而objc_getClass
会调用类处理回调,并再次确认类是否注册,如果确认未注册,再返回nil。而objc_getRequiredClass
函数的操作与objc_getClass
相同,只不过如果没有找到类,则会杀死进程。 -
objc_getMetaClass
函数:如果指定的类没有注册,则该函数会调用类处理回调,并再次确认类是否注册,如果确认未注册,再返回nil。不过,每个类定义都必须有一个有效的元类定义,所以这个函数总是会返回一个元类定义,不管它是否有效。
网友评论