Objective-C(八)对象的本质及分类

本文是Objective-C系列的第8篇,主要讲述OC对象的底层结构,以及分类:实例对象、类对象、元类对象。git

在上一篇Objective-C(七)对象内存分析分析后,咱们得知了一个类在内存中的存储。github

可是,咱们只分析了类的成员变量和属性,咱们知道,一个OC对象,还包括方法、协议等极其重要的信息,那么它们在哪里,又是如何存储,如何用的呢?数组

本篇在此进一步分析Objective-C类体系的分类及其在内存中的完整分布。bash

为此,咱们先要进行一些准备工做。app

1、准备

1.1 对象的分类

Objective-C中的对象,简称OC对象,主要能够分为3种:布局

  • instance对象(实例对象)post

  • class对象(类对象)测试

  • meta-class对象(元类对象)ui

咱们针对OC中对象分为三种进行了测试,测试结果以下:this

image-20181121182028637

从上图咱们能够获得如下结论:

  • 实例对象能够有多个:每alloc一个对象,就会建立一个实例对象;
  • 类对象只能有一个:不一样实例对象的类对象为同一个;
  • 元类对象只能有一个:不一样实例对象(类对象)的元类对象只有一个;

那么测试代码中的几个方法,下面也简要说明下,部分截取源码项目-BFOCClass分类-01

1.1.1 [NSObject class]

NSObject.mm中:

+ (Class)class {
    return self;
}
复制代码

返回类对象(元类对象)自己。

因此,[[[NSObject class] class] class] 不管调用多少次class,其返回都是NSObject class对象。

1.1.2 [person1 class]

返回实例对象的类对象

- (Class)class {
    return object_getClass(self);
}
复制代码

由(2)中可知,

  • [person1 class]:返回person1中isa指向的对象,就是BFPerson class对象;

1.1.3 objc_getClass

该方法与object_getClass从方法名看极其类似。

  • 参数:字符串类名;
  • 返回:对应的类对象。
Class objc_getClass(const char *aClassName)
{
    if (!aClassName) return Nil;
    // NO unconnected, YES class handler
    return look_up_class(aClassName, NO, YES);
}
复制代码

1.1.4 object_getClass

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
复制代码

返回传入objisa所指向的对象,传入的obj多是instance对象、class对象、meta-class对象

其返回值对应为:

a) 若是是instance对象,返回class对象

b) 若是是class对象,返回meta-class对象

c) 若是是meta-class对象,返回NSObject(基类)的meta-class对象

因此上面测试中:

  • object_getClass(person1):返回即class对象;

  • object_getClass(personClass5):返回即meta-class对象;

1.1.5 class_isMetaClass

返回是否为meta-class对象。

1.2 一些从源码来的结构体

咱们会从源码摘抄一部分结构体,用来进行后面的论证。

1.2.1 NSObject

NSObject从源码中获得,其第一个成员变量为isa,该Class结构体第一个成员变量名称也是isa

isa里则包含了(元)类信息的存放地址。

image-20181122114646926

1.2.2 class_rw_t

class_rw_tclass_ro_t均是存放类中信息的结构体,包括成员变量、属性列表、协议列表及方法列表,其具体以下:

image-20181122114716755

目前,咱们class_ro_t当前类在编译期就已经肯定的属性、方法以及遵循的协议

class_rw_t则是在运行时,runtime从新加载布局的当前类的属性、方法和协议等。

1.2.3 method_t

  • method_t是 方法最底层的结构。
  • method_list_t
    • 方法列表,一维数组
    • 内部存放method_t
    • class_ro_t中的成员变量。
  • method_array_t
  • 一样是方法列表,可是是二维数组
  • 内部存放method_t
  • class_rw_t中的成员变量;

三者的关系以下图:

image-20181122114848636

与method相似,property、protocol的相似。

至于为何会有一维和二维数组,后续文章会详述该问题。

1.2.4 property_t

property_array_tproperty_list_t的结构与上面的method中的数组相似,分别是二维、一维数组。

struct property_t {
    const char *name;  		 //属性名
    const char *attributes;  //字符串,代表了该属性的特性
};
复制代码

假如属性名为name,类型为NSString,那么对应的attributes:T@"NSString",C,N,V_name

1.2.5 protocol_t

protocol_array_tprotocol_list_t分别是存放protocol_t的二维及一维数组。

相似的,咱们关注protocol_t结构体:

struct protocol_t : objc_object {
    const char *mangledName;
    struct protocol_list_t *protocols;
    method_list_t *instanceMethods;
    method_list_t *classMethods;
    method_list_t *optionalInstanceMethods;
    method_list_t *optionalClassMethods;
    property_list_t *instanceProperties;
    uint32_t size;   // sizeof(protocol_t)
    uint32_t flags;
    // Fields below this point are not always present on disk.
    const char **_extendedMethodTypes;
    const char *_demangledName;
    property_list_t *_classProperties;
};
复制代码

1.2.6 ivar_t

咱们发现,针对成员变量,只有ivar_list_t,而没有ivar_array_t这样的结构。

从这里咱们能够知道,就算是runtime,也没法在运行时往类中添加成员变量。

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    // alignment is sometimes -1; use alignment() instead
    uint32_t alignment_raw;
    uint32_t size;
};
复制代码

1.3 NSObject实例对象的内存

咱们已经对NSObject实例对象的内存结构至关熟悉了。咱们再阐述如下观点:

  • NSObject实例对象内存中有且只有一个变量为isa,isa指向NSObject的class对象;

下面,咱们针对编译期isa,结合源码,咱们获得以下:

image-20181122162823924

运行期的isa:

image-20181122162910315

2、对象的内存存储

上面,咱们作了一些准备工做,了解了NSObject如何存储各类各样信息的结构体及存储在什么地方。

下面咱们将会针对,三种类型作一个简单的描述。

2.1 instance对象

image-20181123041001470

其内存布局以下:

image-20181123041041229

instance对象在内存中存储的信息有:

  • isa指针
  • 其余成员变量

2.2 class对象

image-20181123063510205

class对象在内存中存储的信息主要包括

  • isa指针
  • superclass指针
  • 类的对象方法信息(instance method)
  • 类的属性信息(property)
  • 类的协议信息(protocol)
  • 类的成员变量信息(ivar)

2.3 meta-class对象

image-20181123071237895

meta-class对象和class对象的内存结构是同样的,可是用途不同,在内存中存储的信息主要包括

  • isa指针
  • superclass指针
  • 类的类方法信息(class method)
  • 其余包括属性、协议、成员变量均为NULL

3、isa和superclass

isa在平时开发中不多使用,可是相信不少iOS开发者并不陌生。

在前面了解了NSObject、各类结构体及对象三大分类以后,苹果为咱们提供了的Cocoa/Cocoa Touch中,类蔟体系的创建依赖两个指针:isasuperclass。那么又是如何串联的?

横向联系:isa的做用就是,将某一个类的instance对象、class对象、meta-class对象创建了横向联系,为查找对象存储各种信息提供指引。

纵向联系:superclass则是面向对象语言中最为重要的特性——继承的体现,它体现的是类与类之间的联系,即为类图中创建了纵向联系

请注意区分:一个类各类对象之间的联系,以及不一样类之间的联系!

3.1 isa

3.1.1 isa指针

image-20181123102401283

3.1.2 ISA_MASK

image-20181123102411439

咱们为了验证该结论,在真机设备上,运行项目-BFOCClass分类-01,尝试获取了BFPerson对象,并打印类相关地址:

BFPerson instance - 0x12fd1f570 0x12fd366e0
BFPerson class - 0x100091e28 0x100091e28 0x100091e28 0x100091e28 0x100091e28 0
BFPerson meta class - 0x100091e00 1 0x100091e28 0
(lldb) x/2xw 0x12fd1f570
0x12fd1f570: 0x00091e2d 0x000001a1
复制代码

从上面打印咱们得知:

  • person1实例对象的地址为0x12fd1f570
  • person1对象地址起始的8个字节,就是isa,此处为:0x1a100091e2d
  • BFPerson class 地址为0x100091e28不等于0x1a100091e2d
  • 0x1a100091e2d(isa) & 0x0000000ffffffff8(ISA_MASK) = 0x100091e28

3.2 superclass

3.2.1 class对象

image-20181123100901305

3.2.2 meta-class对象

image-20181123102215644

3.3 类图体系

据此,isasuperclass联手织起的下面这张类图:

class-superclas

3.3.1 联系

  • isa指向

    • instance的isa指向class
    • class的isa指向meta-class
    • meta-class的isa指向基类的meta-class
  • superclass指向

    • class的superclass指向父类的class 若是没有父类,superclass指针为nil
    • meta-class的superclass指向父类的meta-class 基类的meta-class的superclass指向基类的class

3.3.2 方法调用

  1. instance调用对象方法的轨迹 isa找到class,方法不存在,就经过superclass找父类
  2. class调用类方法的轨迹 isa找meta-class,方法不存在,就经过superclass找父类

代码参考BFOCClass分类-01

继承关系:BFStudent->BFPerson->NSObject

3.3.2.1 状况1

BFStudent *stu = [[BFStudent alloc] init];
[stu test];
复制代码
  • BFStudent实现-(void)test;,调用BFStudent的实现方法;

  • BFStudent未实现-(void)test;,若BFPerson实现,调用BFPerson实现;

  • BFStudent和BFPerson均未实现,NSObject若实现,调用NSObject实现;

    • 以上都未实现,代码是不能直接调用[stu test];,会编译报错,须要改调用方式:

      ((**void** (*)(**id**, **SEL**))objc_msgSend)(stu, **@selector**(test));
      复制代码
      • NSObject若实现+(void)test;,崩溃;
      • NSObject若实现-(void)test;,调用-(void)test;
  • 继承体现中的类,均未实现,崩溃—— -[BFStudent test]: unrecognized selector sent to instance

3.3.2.2 状况2

[BFStudent test];
复制代码
  • BFStudent实现+(void)test;,调用BFStudent的实现;

  • BFStudent未实现+(void)test;,若BFPerson实现,调用BFPerson实现;

  • BFStudent和BFPerson均未实现,调用NSObject实现;

    • 没法[BFStudent test];调用,调用方式需改成:

      Class stuClass = [BFStudent class];
      ((void (*)(id, SEL))objc_msgSend)(stuClass, @selector(test))
      复制代码
      • NSObject若实现+(void)test;,调用+(void)test;
      • NSObject若实现-(void)test;,调用-(void)test;
      • NSObject若实现+(void)test;-(void)test;,调用+(void)test;
  • 继承体现中的类,均未实现,崩溃—— +[BFStudent test]: unrecognized selector sent to instance

3.3.3 分析

针对状况1和2,大部分咱们都能理解,但有一种状况。是有差异的。

在BFStudent和BFPerson中均未实现对应的对象方法(类方法)时,都会去NSObject中寻找。

  • 对象方法:NSObject寻找对应的对象方法,未找到对象方法,崩溃,参考下面的蓝色箭头。
  • 类方法:NSObject先寻找对应的类方法,未找到类方法,而后继续寻找对应的实例方法,若二者未找到,才会崩溃,见下面的黄色箭头。

image-20181210131420601

4、窥探对象完整的内部结构

到此,咱们针对对象已经了解的差很少了,可是咱们仍是没有完整的看到运行时,对象的内部结构。

4.1 如何窥探

有两种方式:

  • 方式一:编译objc源码,直接经过objc4源码调试;
  • 方式二:抽象出[一些从源码来的结构体](#2. 一些从源码来的结构体)中提到的结构体,将正常对象转换为对应的结构体,进行调试;

咱们采用方式二。

4.2 一探究竟

  1. 抽象出的结构体从这里下载。

  2. 导入后,将编译项目调整为命令行项目,并将main.m改成main.mm,以支持C++编译。

对应的项目源码

4.2.1 类对象

image-20181123170300802

4.2.2 元类对象

image-20181123170320360

如今,你知道OC对象的本质了吗?它们的分类呢?

参考

连接

  1. objc4
  2. Runtime(一)Runtime 简介

示例代码

  1. 本文源码项目-BFOCClass分类-01
  2. 本文源码项目-BFOCClass分类-02
相关文章
相关标签/搜索