iOS开发

runtime 运行时之一:类与对象

2017-11-15  本文已影响0人  阳光下的小泡沫丶

本文章是我照着南峰子的runtime博客写的,加上了一点自己的见解,作为自己的学习笔记,侵删。
附上原博客地址:南峰子的技术博客

如有错误的地方欢迎指出。

runtime 运行时之一:类与对象

Object-C是一门动态语言,它将很多静态语言在编译和连接时期做的事情放到了运行时来做。

优势:代码更具有灵活性。

这种特性意味着Objective-C不仅需要一个编译器,还需要一个运行时系统来执行编译的代码。这个运行时系统就是Objc Runtime

Objc Runtime其实就是一个runtime库,它基本上是用C和汇编写的。这个库使得C语言有了面向对象的能力。

Runtime库主要做了下面几件事情:

  1. 封装:在这个库中,对象可以用C语言的结构体表示,方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被Runtime函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和他们的方法了。
  2. 找出方法的最终执行代码:当程序执行 [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.hobjc_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;

在定义中,下面几个字段使我们感兴趣的

  1. isa:在Object-C中,所有实例的isa指针指向类,所有的类自身也是一个对象,这个对象的Class也有一个isa指针,它指向metaClass(元类),我们会在后面介绍它。
  2. super_class:指向该类的父类,如果该类已经是最顶层的跟类(如NSObject或NSProxy),则super_class为NULL。
  3. cache:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。
  4. 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;
};
  1. mask:一个整数,指定分配的缓存的buckets的总数。在方法查找过程中,Objective-C runtime使用这个字段来确定开始线性查找数组的索引位置。指向方法selector的指针与该字段做一个AND位操作(index = (mask & selector))。这可以作为一个简单的hash散列算法。
  2. occupied:一个整数,指定实际占用的缓存bucket的总数。
  3. 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-classisa都指向基类的meta-class,以此作为它们的所属类。所以,任何NSObject继承体系下的meta-class的isa指针都是NSObject的meta-class,而基类的meta-class指向它自己。这样就形成了一个完美的闭环。

下图很好的说明了meta-class、类和对象的关系:

image.png

也就是说实例的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 );
void myMethodIMP(id self, SEL _cmd) {
    // implementation...
}

这里我查了一下IMP与SEL的区别:

  1. IMP是真正的函数指针,指向方法实现。
  2. Objective-C在运行时所有的方法可以看成一张表,而SEL可以看做是表中每一条的索引。
  3. - (IMP)methodForSelector:(SEL)aSelector 方法可以根据一个实例SEL得到该方法的IMP(函数指针)+ (IMP)instanceMethodForSelector:(SEL)aSelector通过类的SEL返回IMP。
  4. 个人见解:SEL和IMP是映射关系,SEL可以通过runtime来更改它的IMP(函数指针),而IMP是不能更改它指向的函数,也就是说每一个c函数对应一个IMP,而SEL是动态的。

与成员变量不同的是,我们可以为类动态添加方法,不管这个类是否存在。

协议(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 );

版本(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的效果与+alloc方法相似。不过使用class_createInstance时,我们需要知道它的类是什么。

实例操作函数

  1. 针对整个对象进行操作的函数。
// 返回指定对象的一份拷贝
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 );
上一篇下一篇

猜你喜欢

热点阅读