美文网首页iOS-RuntimeRumtime
Runtime:OC对象、类、元类的本质

Runtime:OC对象、类、元类的本质

作者: 意一ineyee | 来源:发表于2019-10-02 13:28 被阅读0次
零、Runtime是什么
一、OC对象的本质
二、OC类的本质
三、OC元类的本质
四、Runtime关于对象、类、元类的常用API

零、Runtime是什么


Runtime即运行时,它是一个库,这个库是用C、C++、汇编语言编写的,提供的API基本都是C语言的。正是由于这个库的存在,才使得OC具备了面向对象的能力,也使得OC成为了一门动态语言,比如:

  • OC对象其实是基于C/C++结构体实现的,OC方法其实是基于C/C++函数实现的,OC对象调用方法其实就是通过C/C++结构体里的指针找到具体的C/C++函数来执行,而这一切都是在Runtime库里实现的。
  • 我们编写的代码一般都要经过编译链接过程(buildcmd+b)来生成可执行文件,然后再运行这个可执行文件。那么对于静态语言来说,你编译链接成什么,程序在运行时肯定就执行什么,而对于动态语言来说,你编译链接成什么,程序在运行时可不一定就执行什么,也就是说在运行时我们还是能决定到底要执行什么,比如我们可以在运行时动态地为某个方法添加实现,或者把消息转发给别的对象等等,这一切也都是Runtime库在支撑的,这就使得我们编写代码可以更加灵活。

2006年苹果发布了OC 2.0,其中对Runtime的很多API做了改进,并把OC 1.0中Runtime的很多API标记为“不可用”、“无效”、“将来会被废弃”等。

但是两套API的核心实现思路还是一样的,而旧API比较简单,所以我们会分析旧API,然后看看新API作了哪些变化,这里有最新的Runtime源码

一、OC对象的本质


1、OC 1.0

通过查看Runtime的源码(objc.h文件),我们得到OC对象的定义如下(伪代码):

typedef struct objc_object *id; // id类型的本质就是一个objc_object类型的结构体指针,所以它可以指向任意一个OC对象

struct objc_object {
    Class isa; // 一个Class类型的结构体指针,存储着一个地址,指向该对象所属的类

    // 自定义的成员变量,存储着该对象这些成员变量具体的值
    NSSring *_name; // “张三”
    NSSring *_sex; // “男”
    int _age; // 33
};

可见OC对象的本质就是一个objc_object类型的结构体,该结构体内部只有一个固定的成员变量isa,它是一个Class类型的结构体指针,存储着一个地址,指向该对象所属的类。当然OC对象内部还可能有很多我们自定义的成员变量,存储着该对象这些成员变量具体的值。

2、OC 2.0

通过查看Runtime的源码(objc-private.h文件),我们得到OC对象的定义如下(伪代码):

typedef struct objc_object *id;

struct objc_object {
    isa_t isa; // 一个isa_t类型的共用体

    // 自定义的成员变量,存储着该对象这些成员变量具体的值
    NSSring *_name; // “张三”
    NSSring *_sex; // “男”
    int _age; // 33
}

union isa_t {
    Class cls;
    
    unsigned long bits; // 8个字节,64位
    struct { // 其实所有的数据都存储在成员变量bits里面,因为外界只访问它,而这个结构体则仅仅是用位域来增加代码的可读性,让我们看到bits里面相应的位上存储着谁的数据
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
        unsigned long nonpointer        : 1;
        unsigned long has_assoc         : 1;
        unsigned long has_cxx_dtor      : 1;
        unsigned long shiftcls          : 33; // 对象所属类的地址信息
        unsigned long magic             : 6;
        unsigned long weakly_referenced : 1;
        unsigned long deallocating      : 1;
        unsigned long has_sidetable_rc  : 1;
        unsigned long extra_rc          : 19;
# endif
    };
};

可见OC对象的本质还是一个objc_object类型的结构体,该结构体内部也还是只有一个固定的成员变量isa只不过64位操作系统以后,对isa做了内存优化,它不再直接是一个指针,而是一个isa_t类型的共用体,它同样占8个字节,但其中只有33位用来存储对象所属类的地址信息,还有19位用来存储(对象的引用计数 - 1),其它位上则存储着各种各样的标记信息。

  • nonpointer:占1位,标记isa是否经过内存优化。如果值为0,代表isa没经过内存优化,它就是一个普通的isa指针,64位全都用来存储该对象所属类的地址;如果值为1,代表isa经过了内存优化,只有33位用来存储对象所属类的地址信息,其它位则另有用途。
  • has_assoc:占1位,标记当前对象是否有关联对象,如果没有,对象销毁时会更快。
  • has_cxx_dtor:占1位,标记当前对象是否使用过C++析构函数,如果没有,对象销毁时会更快。
  • shiftcls:占33位,存储着当前对象所属类的地址信息。
  • magic:占1位,用来标记在调试时当前对象是否未完成初始化。
  • weakly_referenced:占1位,标记弱引用表里是否有当前对象的弱指针数组——即是否被弱指针指向着、是否有弱引用,如果没有,对象销毁时会更快。
  • deallocating:占1位,标记当前对象是否正在释放。
  • has_sidetable_rc:占1位,标记引用计数表里是否有当前对象的引用计数,如果没有,对象销毁时会更快。
  • extra_rc:占19位,存储着(对象的引用计数 - 1),存值范围为0~255。

共用体也是C语言的一种数据类型,和结构体差不多,都可以定义很多的成员变量,但两者的主要区别就在于内存的使用。

一个结构体占用的内存等于它所有成员变量占用内存之和,而且要遵守内存对齐规则,而一个共用体占用的内存等于它最宽成员变量占用的内存。结构体里所有的成员变量各自有各自的内存,而共用体里所有的成员变量共用这一块内存。所以共用体可以更加节省内存,但是我们要把数据处理好,否则很容易出现数据覆盖。

3、对象内部的其它成员变量

我们通常说的OC对象一般是指狭义的OC对象——即实例对象,而广义的OC对象则包括实例对象、类、元类。每个类可以创建出N多个实例对象,一个实例对象占用一份内存,它们的成员变量除了isa存储的值一样外,其它成员变量都存储着该对象这些成员变量具体的值。

INEPerson *person1 = [[INEPerson alloc] init];
person1.name = @"张三";
person1.sex = @"男";
person1.age = 33; // person1的age内存中存储的是数值33

INEPerson *person2 = [[INEPerson alloc] init];
person2.name = @"李四";
person2.sex = @"男";
person2.age = 44; // person2的age内存中存储的是数值44

NSLog(@"%p", person1);// 0x10054bd70
NSLog(@"%p", person2);// 0x10054bb80

二、OC类的本质


1、OC 1.0

通过查看Runtime的源码(runtime.h文件),我们得到OC类的定义如下(伪代码):

typedef struct objc_class *Class; // Class类型的本质就是一个objc_class类型的结构体指针,所以它可以指向任意一个OC类

struct objc_class {
    Class isa;
    Class super_class;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    const ivar_list_t *ivars;

    cache_t cache;
   
    const char *name;
    long instance_size;
    long version;
    long info;
};

可见OC类的本质就是一个objc_class类型的结构体,该结构体内部有若干个成员变量,其中有几个是我们重点关注的:

  • isa指针:存储着一个地址,指向该类所属的类,即元类。(面向对象编程中,我们常说“万事万物皆对象”,所以类也是一个对象)
  • superclass指针:存储着一个地址,指向该类的父类。
  • methods:数组指针,存储着该类所有的实例方法信息。
  • properties:数组指针,存储着该类所有的属性信息。
  • protocols:数组指针,存储着该类所有遵守过的协议信息。
  • ivars:数组指针,存储着该类所有的成员变量信息。
  • cache:结构体,存储着该类所有的方法缓存信息。

2、OC 2.0

通过查看Runtime的源码(objc-runtime-new.h文件),我们得到OC类的定义如下(伪代码):

typedef struct objc_class *Class;

struct objc_class : objc_object {
//    isa_t isa; // objc_class继承自objc_object,所以不考虑内存对齐的前提下,可以直接把isa成员变量搬过来
    Class superclass;
    
    class_data_bits_t bits; // 存储着该类的具体信息,按位与掩码FAST_DATA_MASK便可得到class_rw_t
    
    cache_t cache; // 存储着该类所有的方法缓存信息
}

// class_rw_t结构体就是该类的可读可写信息(rw即readwrite)
struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro; // 该类的只读信息

    method_array_t methods; // 存储着该类所有的实例方法信息,包括分类的
    property_array_t properties; // 存储着该类所有的属性,包括分类的
    protocol_array_t protocols; // 存储着该类所有遵守过的协议,包括分类的

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
}

// class_ro_t结构体就是该类的只读信息(ro即readonly)
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList; // 存储着该类原来的实例方法信息
    protocol_list_t * baseProtocols; // 存储着该类原来遵守过的协议信息
    const ivar_list_t * ivars; // 存储着该类原来的成员变量信息

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties; // 存储着该类原来的属性信息
}

可见OC类的本质还是一个objc_class类型的结构体,只不过它的内部结构套了好几层,但我们重点关注的那几个成员变量都还是可以顺利找到的。

其实在编译时,bits成员变量按位与掩码得到的是class_ro_t结构体,该结构体内部存储着我们在类本身定义的方法、属性、协议、成员变量。而在运行时系统才生成了一个class_rw_t结构体,并把类本身的方法、属性、协议和分类里的方法、属性、协议合并到class_rw_t结构体的中,同时设置class_rw_t结构体为可读可写,class_ro_t结构体为只读,bits成员变量按位与掩码得到class_rw_t结构体。

3、获取某个对象所属的类

每个类只有一个,它在内存中只有一份。我们可以通过实例对象的-class方法或Runtime的APIobject_getClass函数来获取某个对象所属的类:

#import <objc/runtime.h>

NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];

Class class1 = [object1 class];
Class class2 = [object2 class];
Class class3 = object_getClass(object1);
Class class4 = object_getClass(object1);

NSLog(@"%p", class1);// 0x7fff93310140
NSLog(@"%p", class2);// 0x7fff93310140
NSLog(@"%p", class3);// 0x7fff93310140
NSLog(@"%p", class4);// 0x7fff93310140

三、OC元类的本质


所谓元类,是指一个类所属的类,我们每创建一个类,系统就会自动帮我们创建好该类所属的类——即元类。元类和类的本质其实都是objc_class,只不过它们的用途不一样,类的methods成员变量里存储着该类所有的实例方法,而元类的methods成员变量里存储着该类所有的类方法。

获取某个类所属的类——元类

每个元类也只有一个,它在内存中也只有一份。我们可以通过Runtime的APIobject_getClass函数来获取某个类所属的类——元类,只不过要把一个类作为参数传进去:

#import <objc/runtime.h>

Class metaClass = object_getClass([NSObject class]);
NSLog(@"%p", metaClass);// 0x7fff933100f0

玩儿一下,既然说元类是类的类,那能不能通过class方法来获取元类呢?

Class metaClass1 = [[NSObject class] class];
NSLog(@"%p", metaClass1);// 0x7fff93310140

我们发现metaClass1metaClass的地址值是不一样的,这就表明不能通过class方法来获取元类。因为[NSObject class]里的NSObject并不是一个实例对象,而是一个类,所以它调用的class方法其实不是实例方法-class,而是类方法+class实例方法-class确实是返回该对象所属的类,而类方法+class则是返回该类本身。下面是Runtime的源码(NSObject.mm文件):

// 返回类本身
+ (Class)class {
    return self; 
}
// 返回该对象所属的类
- (Class)class {
    return object_getClass(self); 
}


// 这两个也可以认为是统一的,反正都是返回父类,你管它是当前类的父类还是当前对象所属类的父类呢
+ (Class)superclass {
    return self->superclass; 
}
- (Class)superclass {
    return [self class]->superclass; 
}


// 这两个是统一的,可以理解为:当前对象是不是某个类的实例
+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass(self) == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}


// 这两个是统一的,可以理解为:当前对象是不是某个类或其子类的实例
+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

四、Runtime关于对象、类、元类的常用API


// 获取一个对象所属的类
Class object_getClass(id obj);
// 设置一个对象所属的类
Class object_setClass(id obj, Class cls);
// 获取一个类的父类
Class class_getSuperclass(Class cls);


// 判断一个对象是不是Class
BOOL object_isClass(id obj);
// 判断一个类是不是MetaClass
BOOL class_isMetaClass(Class cls);


// 动态创建一个类(参数:父类,类名,额外的内存空间通常传0即可)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes);
// 注册一个类(创建好一个类,在注册之前,我们通常会调用class_addIvar、class_addMethod等函数为类添加成员变量、方法等)
void objc_registerClassPair(Class cls);
// 销毁一个类
void objc_disposeClassPair(Class cls);

相关文章

  • Runtime:OC对象、类、元类的本质

    零、Runtime是什么一、OC对象的本质二、OC类的本质三、OC元类的本质四、Runtime关于对象、类、元类的...

  • iOS runtime

    文章目录 OC中类和对象的本质 实例对象,类,元类的关系 类的属性 类的方法 消息发送机制 Runtime api...

  • OC对象的本质<二> 实例对象,类对象,元类对象

    OC对象的本质<一> OC对象的分类 OC对象可以分为三类,分别是实例对象,类对象,元类对象。 实例对象(inst...

  • 01 iOS底层原理 - OC对象本质探究

    废话不多说,要了解OC对象的本质,先要明确一点,都有哪些是属于OC的对象:实例对象,类对象,元类对象。 一,Obj...

  • iOS开发 Runtime 流程图

    你了解多少Runtime? isa 指针的理解 实例对象 isa 指向类对象 类对象指 isa 向元类对象 元类对...

  • iOS Runtime笔记

    相信大家对上图应该不陌生,图中说明了OC中对象的本质以及对象、类与元类的关系,这个也是OC的基础,属于runtim...

  • iOS runtime笔记一

    参考资料 南峰子的runtime 一【OC刨根问底】Runtime简单粗暴理解 对象的理解,元类(meta cla...

  • OC对象的本质(上)

    iOS | OC对象本质 | Objective-C 什么是OC语言,OC对象、类的本质是什么,OC对象的内存布局...

  • Category实现原理

    依赖runtime 动态的将分类的方法和类方法合并到类对象和元类对象的方法列表中 (对实例对象 类对象 元类对...

  • runtime - iOS类对象、实例对象、元类对象

    理解类与对象的本质对于掌握一门语言是至关重要的,本文将从结构类型的角度探讨OC的类对象、实例对象、元类对象(Met...

网友评论

    本文标题:Runtime:OC对象、类、元类的本质

    本文链接:https://www.haomeiwen.com/subject/edtrpctx.html