天天看点

iOS-底层原理 07:isa与类关联的原理

所以

isa

中通过

初始化

后的

成员

值变化过程

,如下图所示

iOS-底层原理 07:isa与类关联的原理

>iOS 底层原理 文章汇总

本文的主要目的是理解类与isa是如何关联的

在介绍正文之前,首先需要理解一个概念:

OC对象

本质

是什么?

OC对象本质

在探索oc对象本质前,先了解一个编译器:

clang

Clang

  • clang

    是一个由

    Apple

    主导编写,基于

    LLVM

    C/C++/OC的编译器

  • 主要是用于

    底层编译

    ,将一些

    文件``输出

    c++文件

    ,例如

    main.m

    输出成

    main.cpp

    ,其目的是为了更好的观察

    底层

    的一些

    结构

    实现

    的逻辑,方便理解底层原理。

探索对象本质

  • main

    中自定义一个类

    LGPerson

    ,有一个属性name
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation LGPerson
@end
           
  • 通过终端,利用

    clang

    main.m

    编译成

    main.cpp

    ,有以下几种编译命令,这里使用的是第一种
//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp

//2、将 ViewController.m 编译成  ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m

//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 

//4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp 
           
  • 打开编译好的main.cpp,找到

    LGPerson

    的定义,发现

    LGPerson

    在底层会被编译成

    struct

    结构体
    • LGPerson_IMPL

      中的第一个属性 其实就是

      isa

      ,是继承自

      NSObject

      ,属于

      伪继承

      ,伪继承的

      方式

      是直接将

      NSObject

      结构体定义为

      LGPerson

      中的

      第一个属性

      ,意味着

      LGPerson

      拥有

      NSObject

      中的

      所有成员变量

    • LGPerson

      中的第一个属性

      NSObject_IVARS

      等效于

      NSObject

      中的

      isa

//NSObject的定义
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

//NSObject 的底层编译
struct NSObject_IMPL {
	Class isa;
};

//LGPerson的底层编译
struct LGPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS; // 等效于 Class isa;
	NSString *_name;
};
           

如下图所示

iOS-底层原理 07:isa与类关联的原理

通过上述分析,理解了OC对象的本质,但是看到

NSObject

的定义,会产生一个疑问:为什么

isa

的类型是

Class

?

  • 在iOS-底层原理 02:alloc & init & new 源码分析文章中,提及过

    alloc

    方法的核心之一的

    initInstanceIsa

    方法,通过查看这个方法的源码实现,我们发现,在初始化

    isa

    指针时,是通过

    isa_t

    类型初始化的,
  • 而在

    NSObject

    定义中isa的类型是

    Class

    ,其根本原因是由于

    isa

    对外反馈的是

    类信息

    ,为了让开发人员更加

    清晰明确

    ,需要在

    isa

    返回时做了一个

    类型强制转换

    ,类似于

    swift

    中的

    as

    的强转。源码中

    isa

    强转

    如下图所示
    iOS-底层原理 07:isa与类关联的原理

总结

所以从上述探索过程中可以得出:

  • OC对象的本质

    其实就是

    结构体

  • LGPerson

    中的

    isa

    是继承自

    NSObject

    中的

    isa

objc_setProperty 源码探索

除了

LGPersong

的底层定义,我们发现还有属性

name

对应的

set

get

方法,如下图所示,其中

set

方法的实现依赖于runtime中的

objc_setProperty

iOS-底层原理 07:isa与类关联的原理

可以通过以下步骤来一步步解开

objc_setProperty

的底层实现

  • 在objc4-781中全局搜索

    objc_setProperty

    ,找到

    objc_setProperty

    的源码实现
    iOS-底层原理 07:isa与类关联的原理
  • 进入

    reallySetProperty

    的源码实现,其方法的原理就是

    新值retain,旧值release

    iOS-底层原理 07:isa与类关联的原理

总结

通过对

objc_setProperty

的底层源码探索,有以下几点说明:

  • objc_setProperty

    方法的目的适用于

    关联 上层

    set

    方法 以及

    底层

    set

    方法,其本质就是一个

    接口

  • 这么设计的

    原因

    是,

    上层

    set

    方法有很多,如果

    直接调用底层set

    方法中,会产生很多的

    临时变量

    ,当你想

    查找

    一个sel时,会非常

    麻烦

  • 基于上述原因,苹果采用了

    适配器设计模式(即将底层接口适配为客户端需要的接口)

    对外

    提供一个

    接口

    ,供上层的set方法使用,

    对内

    调用底层的

    set方法

    ,使其相互不受影响,即

    无论上层怎么变,下层都是不变的

    ,或者

    下层的变化也无法影响上层

    ,主要是达到上下层接口隔离的目的

下图是上层、隔离层、底层之间的关系

iOS-底层原理 07:isa与类关联的原理

cls 与 类 的关联原理

在在iOS-底层原理 02:alloc & init & new 源码分析与iOS-底层原理 05:内存对齐原理中分别分析了alloc中3核心的前两个,今天来探索

initInstanceIsa

是如何将

cls

isa

关联的

在此之前,需要先了解什么是

联合体

,为什么

isa

的类型

isa_t

是使用

联合体定义

联合体(union)

构造数据类型的方式有以下两种:

  • 结构体

    struct

  • 联合体

    union

    ,也称为

    共用体

结构体

结构体

是指把不

同的数据组合成一个整体

,其

变量

共存

的,变量不管是否使用,都会分配内存。

  • 缺点:所有属性都分配内存,比较

    浪费内存

    ,假设有4个int成员,一共分配了

    16

    字节的内存,但是在使用时,你只使用了

    4

    字节,剩余的

    12

    字节就是属于内存的浪费
  • 优点:存储

    容量较大

    包容性强

    ,且成员之间

    不会相互影响

联合体

联合体也是由

不同的数据类型组成

,但其变量是

互斥

的,所有的成员

共占一段内存

。而且

共用体

采用了

内存覆盖技术

同一时刻只能保存一个成员的值

,如果

对新的成员赋值

,就会将

原来成员的值覆盖掉

  • 缺点:,包容性弱
  • 优点:所有成员共用一段内存,使内存的使用更为精细灵活,同时也节省了内存空间

两者的区别

  • 内存占用情况
    • 结构体的

      各个成员会占用不同的内存

      ,互相之间

      没有影响

    • 共用体的

      所有成员占用同一段内存

      ,修改一个成员会

      影响

      其余所有成员
  • 内存分配大小
    • 结构体内存

      >=

      所有成员占用的

      内存总和

      (成员之间可能会有缝隙)
    • 共用体

      占用的

      内存

      等于

      最大的成员

      占用的

      内存

isa的类型 isa_t

以下是isa指针的类型

isa_t

的定义,从定义中可以看出是通过

联合体(union)

定义的。

union isa_t { //联合体
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    //提供了cls 和 bits ,两者是互斥关系
    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};
           

isa_t

类型使用

联合体

的原因也是基于

内存优化

的考虑,这里的内存优化是指在isa指针中通过

char + 位域

(即二进制中每一位均可表示不同的信息)的原理实现。通常来说,

isa指针

占用的内存大小是

8

字节,即

64

位,已经足够存储很多的信息了,这样可以极大的节省内存,以提高性能

isa_t

的定义中可以看出:

  • 提供了两个成员,

    cls

    bits

    ,由联合体的定义所知,这两个成员是

    互斥

    的,也就意味着,当初始化isa指针时,有两种初始化方式
    • 通过

      cls

      初始化,

      bits无默认值

    • 通过

      bits

      初始化,

      cls有默认值

  • 还提供了一个结构体定义的

    位域

    ,用于存储类信息及其他信息,结构体的成员

    ISA_BITFIELD

    ,这是一个

    定义,有两个版本

    __arm64__

    (对应ios 移动端) 和

    __x86_64__

    (对应macOS),以下是它们的一些宏定义,如下图所示
    iOS-底层原理 07:isa与类关联的原理
    • nonpointer

      有两个值,表示自定义的类等,占

      1

      • 纯isa指针

      • 1

        :不只是

        类对象地址

        ,isa中包含了

        类信息

        、对象的

        引用计数

    • has_assoc

      表示

      关联对象标志

      位,占

      1

      • 没有关联

        对象
      • 1

        存在关联

        对象
    • has_cxx_dtor

      表示该对象是否有C++/OC的

      析构器

      (类似于

      dealloc

      ),占

      1

      • 如果

        析构函数,则需要

        做析构

        逻辑
      • 如果

        没有

        ,则可以更快的

        释放

        对象
    • shiftclx

      表示

      存储类的指针的值

      (类的地址), 即类信息
      • arm64

        中占

        33

        位,开启指针优化的情况下,在arm64架构中有

        33

        位用来存储类指针
      • x86_64

        中占

        44

    • magic

      用于调试器判断当前对象是

      真的对象

      还是

      没有初始化的空间

      ,占

      6

    • weakly_refrenced

      是 指对象

      是否被指向

      或者

      曾经指向一个ARC的弱变量

      • 没有弱引用的对象可以更快释放
    • deallocating

      标志对象是

      是否正在释放

      内存
    • has_sidetable_rc

      表示 当对象

      引用计数大于10

      时,则需要

      借用该变量存储进位

    • extra_rc

      (额外的引用计数) — 导尿管表示该

      对象的引用计数值

      ,实际上是引用计数值减1
      • 如果对象的

        引用计数为10

        ,那么

        extra_rc

        为9

针对两种不同平台,其

isa

的存储情况如图所示

iOS-底层原理 07:isa与类关联的原理

原理探索

  • 通过

    alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone

    方法路径,查找到

    initInstanceIsa

    ,并进入其原理实现
inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());
    //初始化isa
    initIsa(cls, true, hasCxxDtor); 
}
           
  • 进入

    initIsa

    方法的源码实现,主要是初始化isa指针
    iOS-底层原理 07:isa与类关联的原理
    该方法的逻辑主要分为两部分
    • 通过

      cls

      初始化

      isa

    • 通过

      bits

      初始化

      isa

验证 isa指针 位域(0-64)

根据前文提及的

0-64

位域,可以在这里通过

initIsa

方法中证明有isa指针中有这些位域(目前是处于

macOS

,所以使用的是

x86_64

  • 首先通过main中的

    LGPerson

    断点 -->

    initInstanceIsa

    -->

    initIsa

    --> 走到

    else

    中的

    isa

    初始化
    iOS-底层原理 07:isa与类关联的原理
  • 执行lldb命令:

    p newisa

    ,得到

    newisa

    的详细信息
    iOS-底层原理 07:isa与类关联的原理
  • 继续往下执行,走到

    newisa.bits = ISA_MAGIC_VALUE;

    下一行,表示为

    isa

    bits

    成员赋值,重新执行lldb命令

    p newisa

    ,得到的结果如下
    iOS-底层原理 07:isa与类关联的原理
    通过与前一个newsize的信息对比,发现isa指针中有一些变化,如下图所示
    iOS-底层原理 07:isa与类关联的原理
    • 其中

      magic

      59

      是由于将

      isa

      指针地址转换为

      二进制

      ,从

      47

      (因为前面有4个位域,共占用47位,地址是从0开始)位开始读取

      6

      位,再转换为

      十进制

      ,如下图所示
      iOS-底层原理 07:isa与类关联的原理

isa

的关联

cls

isa

关联

原理

就是

isa

指针中的

shiftcls

位域中存储了

类信息

,其中

initInstanceIsa

的过程是将

calloc

指针 和当前的

类cls

关联起来,有以下几种验证方式:

  • 【方式一】通过

    initIsa

    方法中的

    newisa.shiftcls = (uintptr_t)cls >> 3;

    验证
  • 【方式二】通过

    isa指针地址

    ISA_MSAK

    的值

    &

    来验证
  • 【方式三】通过runtime的方法

    object_getClass

    验证
  • 【方式四】通过

    位运算

    验证

方式一:通过 initIsa 方法

  • 运行至

    newisa.shiftcls = (uintptr_t)cls >> 3;

    前一步,其中

    shiftcls

    存储当前

    类的值信息

    • 此时查看

      cls

      ,是

      LGPerson

    • shiftcls

      赋值的逻辑是将

      LGPerson

      进行编码后,

      右移3

      iOS-底层原理 07:isa与类关联的原理
  • 执行lldb命令

    p (uintptr_t)cls

    ,结果为

    (uintptr_t) $2 = 4294975720

    ,再右移三位,有以下两种方式(任选其一),将得到

    536871965

    存储到

    newisa

    shiftcls

    • p (uintptr_t)cls >> 3

    • 通过上一步的结果

      $2

      ,执行lldb命令

      p $2 >> 3

      iOS-底层原理 07:isa与类关联的原理
  • 继续执行程序到

    isa = newisa;

    部分,此时执行

    p newisa

    iOS-底层原理 07:isa与类关联的原理

    bits赋值

    结果的

    对比

    ,bits的

    位域

    中有两处变化
    • cls

      默认值

      ,变成了

      LGPerson

      ,将

      isa与cls完美关联

    • shiftcls

      由 变成了

      536871965

      iOS-底层原理 07:isa与类关联的原理
      所以

      isa

      中通过

      初始化

      后的

      成员

      值变化过程

      ,如下图所示
      iOS-底层原理 07:isa与类关联的原理

为什么在shiftcls赋值时需要类型强转?

因为

内存

的存储

不能存储字符串

机器码

只能识别

0 、1

这两种数字,所以需要将其转换为

uintptr_t

数据类型,这样

shiftcls

中存储的

类信息

才能

被机器码理解

, 其中

uintptr_t

long

为什么需要右移3位?

主要是由于

shiftcls

处于

isa

指针地址的

中间

部分,前面还有

3

个位域,为了不

影响前面的3个位域

的数据,需要

右移

将其

抹零

方式二:通过 isa & ISA_MSAK

  • 在方式一后,继续执行,回到

    _class_createInstanceFromZone

    方法,此时

    cls 与 isa已经关联完成

    ,执行

    po objc

  • 执行

    x/4gx obj

    ,得到

    isa

    指针的地址

    0x001d8001000020e9

  • isa

    指针地址 &

    ISA_MASK

    (处于

    macOS

    ,使用

    x86_64

    中的

    定义),即

    po 0x001d8001000020e9 & 0x00007ffffffffff8

    ,得出

    LGPerson

    • arm64

      中,ISA_MASK 宏定义的值为

      0x0000000ffffffff8ULL

    • x86_64

      中,ISA_MASK 宏定义的值为

      0x00007ffffffffff8ULL

      iOS-底层原理 07:isa与类关联的原理

方式三:通过 object_getClass

通过查看

object_getClass

的源码实现,同样可以验证isa与类关联的原理,有以下几步:

  • main中导入#import <objc/runtime.h>
  • 通过

    runtime

    的api,即

    object_getClass

    函数获取类信息
object_getClass(<#id  _Nullable obj#>)
           
  • 查看

    object_getClass

    函数 源码的实现
    iOS-底层原理 07:isa与类关联的原理
  • 点击进入

    object_getClass

    底层实现
    iOS-底层原理 07:isa与类关联的原理
  • 进入

    getIsa

    的源码实现
    iOS-底层原理 07:isa与类关联的原理
  • 点击

    ISA()

    ,进入源码,可以看到如果是

    indexed

    类型,执行

    if

    流程,反之 执行的是

    else

    流程
    iOS-底层原理 07:isa与类关联的原理
    • else

      流程中,拿到

      isa

      bits

      这个位,再

      & ISA_MASK

      ,这与方式二中的原理是一致的,

      获得当前的类信息

    • 从这里也可以

      得出 cls 与 isa 已经完美关联

方式四:通过位运算

  • 回到

    _class_createInstanceFromZone

    方法。通过

    x/4gx obj

    得到

    obj

    的存储信息,当前类的信息存储在

    isa

    指针中,且isa中的

    shiftcls

    此时占

    44

    位(因为处于

    macOS

    环境)
    iOS-底层原理 07:isa与类关联的原理
  • 想要

    读取

    中间的

    44

    类信息

    ,就需要经过

    位运算

    ,将右边

    3

    位,和左边除去

    44

    位以外的部分都

    抹零

    ,其

    相对位置是不变

    的。其位运算过程如图所示,其中

    shiftcls

    即为需要

    读取

    类信息

    iOS-底层原理 07:isa与类关联的原理
    • isa

      地址

      右移3

      位:

      p/x 0x001d8001000020e9 >> 3

      ,得到

      0x0003b0002000041d

    • 在将得到的

      0x0003b0002000041d``左移20

      位:

      p/x 0x0003b0002000041d << 20

      ,得到

      0x0002000041d00000

      • 为什么是

        左移20

        位?因为先

        移了

        3

        位,相当于向右

        偏移了3位

        ,而

        左边

        需要

        抹零

        的位数有

        17

        位,所以一共需要移动

        20

    • 将得到的

      0x0002000041d00000

      右移17

      位:

      p/x 0x0002000041d00000 >> 17

      得到新的

      0x00000001000020e8

  • 获取cls的地址 与 上面的进行验证 :

    p/x cls

    也得出

    0x00000001000020e8

    ,所以由此可以证明 cls 与 isa 是关联的
    iOS-底层原理 07:isa与类关联的原理