iOS之武功秘籍④:类结构分析

1,068 阅读14分钟

iOS之武功秘籍 文章汇总

写在前面

通过前面篇章的探索,我们已成功的从对象过渡到类了.本文就来讲讲实例出实例对象的类以及类的结构.

本节可能用到的秘籍Demo

一、类的本质

① 类的本质

objc源码下准备代码 利用clangOC文件输出cpp文件.(xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp)

发现类在底层用Class接收,然后开始大海捞针(开天眼模式)找到了Class的定义

想要找到objc_class就搜不到了,但是总觉得它似曾相似,或许能在objc源码中找到灵感

在源码中搜索代码的经验 "objc_class :"、 "objc_class {"

我们发现objc_class继承于objc_object 结论:类的本质是objc_class类型的结构体,objc_class继承于objc_object,所以满足万物皆对象

② objc_class & objc_object 、objc_object和NSObject的关系

objc_object和NSObject

等等,为什么继承objc_object就满足万物皆对象了??? 看过NSObject的定义就知道了 仔细比较的话就能看出NSObjectobjc_object有着说不清道不明的关系

  • 其实NSObjectobjc_object的仿写,和objc_object的定义是一样的,在底层会编译成objc_object
  • 同理NSObject类OC版本的objc_class

objc_class & objc_object

前面使用clang编译过main.m文件,从编译后的c++文件中可以看到如下c++源码

  • NSObject的底层编译是NSObject_IMPL结构体

    • 其中 Classisa指针的类型,是由objc_class定义的类型
    • objc_class是一个结构体.在iOS中,所有的Class都是以 objc_class 为模板创建的
  • objc4源码中搜索objc_class的定义,源码中对其的定义有两个版本

    • 旧版 位于 runtime.h中,已经被废除
    • 新版位于objc-runtime-new.h,这个是objc4-818.2最新优化的,我们后面的类的结构分析也是基于新版来分析的从新版的定义中,可以看到 objc_class 结构体类型是继承自 objc_object的.
  • objc4源码中搜索objc_object 或者 objc_object {,这个类型也有两个版本

    • 一个位于 objc.h,没有被废除,从编译的main-arm64.cpp中可以看到,使用的这个版本的objc_object
    • 另一个位于 objc-privat.h

那么objc_classobjc_object 到底有什么关系? 通过上述的源码查找以及main-arm64.cpp中底层编译源码,有以下几点说明:

  • 结构体类型objc_class 继承自objc_object类型,其中objc_object也是一个结构体,且有一个isa属性,所以objc_class也拥有了isa属性
  • mian-arm64.cpp底层编译文件中,NSObject中的isa在底层是由Class 定义的,其中class的底层编码来自 objc_class类型,所以NSObject也拥有了isa属性
  • NSObject 是一个类,用它初始化一个实例对象objcobjc 满足 objc_object 的特性(即有isa属性),主要是因为isa 是由 NSObjectobjc_class继承过来的,而objc_class继承自objc_objectobjc_objectisa属性.所以对象都有一个isaisa表示指向,来自于当前的objc_object
  • objc_object(结构体) 是 当前的 根对象,所有的对象都有这样一个特性 objc_object,即拥有isa属性

引申问题:objc_object 与 对象的关系

  • 所有的对象都是以 objc_object 为模板继承过来的
  • 所有的对象是来自 NSObject(OC),但是真正到底层的是一个objc_object(C/C++)的结构体类型
  • objc_object对象的关系 是 继承关系

③ Class isa

isa明明是isa_t类型的,为什么注释了一句Class ISA

  • 万物皆对象,用继承于objc_objectClass接收是没问题的
  • 强转,方便isa走位时返回类的类型

④总结

  • 所有的对象 + 类 + 元类 都有isa属性
  • 所有的对象都是由objc_object继承来的
  • 简单概括就是万物皆对象,万物皆来源于objc_object,有以下两点结论:
    • 所有以 objc_object为模板创建的对象,都有isa属性
    • 所有以 objc_class为模板创建的,都有isa属性
  • 在结构层面可以通俗的理解为上层OC底层的对接:
    • 下层是通过 结构体 定义的 模板,例如objc_class、objc_object
    • 上层是通过底层的模板创建的一些类型,例如TCJPerson

objc_class、objc_object、isa、object、NSObject等的整体的关系,如下图所示

二、指针内存偏移

在分析类结构之前,需要先了解内存偏移,因为类信息中访问时,需要使用内存偏移

① 普通指针 - 值拷贝

我们观察上面的代码,虽然整型变量ab都是被赋值为10,但是ab内存地址是不一样的,这种方式被称为值拷贝.

② 对象 - 指针拷贝或引用拷贝

通过运行结果,可以知道obj1obj2对象不光自身内存地址不一样,连指向的对象的内存地址也不一样,这种方式被称为指针拷贝引用拷贝.

我们可以用一幅图来总结上面的两个例子:

③ 用数组指针引出 - 内存偏移

通过运行结果可以看到:

  • &a&a[0]的地址是相同的.即首地址就代表数组的第一个元素的地址.
  • 第一个元素地址0x7ffeefbff400和第二个元素地址0x7ffeefbff404相差4个字节,也就是int的所占的4字节,因为他们的数据类型相同.
  • dd+1d+2这个地方的指针相加就是偏移地址.地址加1就是偏移,偏移一个位数所在元素的大小.
  • 可以通过地址,取出对应地址的值.

三、类的结构

objc_class的定义可以得出,类有4个属性:isa、superclass、cache、bits

① Class ISA

不但实例对象中有isa指针类对象中也有isa指针关联着元类

Class本身就是一个指针,占用8字节

② Class superclass

顾名思义就是类的父类(一般为NSObjectsuperclassClass类型,所以占用8字节

③ cache_t cache

虽然对这个属性比较陌生(后面章节会详细介绍),但是cache在英文中的意思是缓存 cache_t是一个结构体,内存长度由所有元素决定:_bucketsAndMaybeMasklong类型,它是一个指针,占用8字节mask_t是个uint32_t类型,_mask占用4字节;因_occupied_flags都是uint16_t类型,uint16_tunsigned short 的别名,所以_occupied占用2字节;_flags占用2字节 =>cache_t占用16字节

④ class_data_bits_t bits

又是一个陌生的属性,但是苹果工程师还是蛮友好的,这一看就是存数据的地方

那么问题来了,类的属性方法都去哪儿了?是在cache还是在bits? 其实前文中有提到一丢丢——objc_class中有个class_rw_t *data()方法

通过前面的分析可知,想要获取bits的中的内容,只需通过类的首地址平移32字节即可.

class_rw_t是在运行时生成的,它在realizeClass中生成,它包含了class_ro_t.它在_objc_init方法中关于dyld的回调的map_images中最终将分类的方法协议都插入到自己的方法列表协议列表中.它不包含成员变量列表,因为成员变量列表是在编译期就确定好的,它只保存在class_ro_t中.不过,class_rw_t中包含了一个指向class_ro_t的指针.

四、类的属性方法

TCJPerson添加hobby成员属性、tcj_name属性变量、sayNB类方法、sayHello实例方法

① 类的属性

x/4gx cls打印当前类结构

bits刚好是类的内存首地址+isa、superclass、cache的内存长度 => 0x1000082b0+32字节 = 0x1000082d0

po打印不出来,那就类型强转打印输出bits的内存地址

根据class_rw_t *data() { return bits.data(); }打印bits.data()

$4指针的打印结果中可以看出bits中存储的信息,其类型是class_rw_t,也是一个结构体类型.但我们还是没有看到属性列表、方法列表等,需要继续往下探索.

通过查看class_rw_t定义的源码发现,结构体中有提供相应的方法去获取属性列表、方法列表等,如下所示 接下来继续探索 bits中的属性列表,以下是 LLDB 探索的过程

那么我们的hobby去哪了呢?难道我TCJPerson类只配拥有“国籍”不配拥有“姓名”?让我静静...

由此我们知道class_rw_t中的property_list只有属性,没有成员变量,属性与成员变量的区别就是有没有set、get方法,如果有,则是属性,如果没有,则是成员变量.

在一顿猛如虎的操作(开天眼)之后,发现了class_rw_t有个属性class_ro_t.

class_ro_t编译期生成的,它存储了当前类在编译期就已经确定的属性方法以及协议,它里面是没有分类中定义的方法协议的.

在控制台输出ro,跟class_ro_t的结构类型一摸一样.

而我们的成员变量就在roivars里面,好期待啊

如愿拿到了hobbytcj_name,但是这个tcj_name长得有点不一样. 仔细想想这不就是编译器会在底层自动将属性变量生成一个成员变量 _tcj_name(_前缀+属性变量),嗦嘎撕裂,还有谁...

小总结:

  • 通过{}定义的成员变量,会存储在类的bits属性中,通过bits --> data() -->ro() --> ivars获取成员变量列表,除了包括成员变量,还包括属性定义的成员变量
  • 通过@property定义的属性,也会存储在bits属性中,通过bits --> data() --> properties() --> list获取属性列表,其中只包含属性

② 类的方法

  • 实例方法存储在类的bits属性中,通过bits --> methods() --> list获取实例方法列表,例如TCJPersong类的实例方法sayHello 就存储在 TCJPerson类的bits属性中,类中的方法列表除了包括实例方法,还包括属性的set方法get方法,还有系统在底层添加了一个c++的.cxx_destruct方法.我们来打印验证一方

  • 类方法存储在元类的bits属性中,通过元类bits --> methods() --> list获取类方法列表,例如TCJPerson中的类方法sayNB 就存储在TCJPerson类元类(名称也是TCJPerson)的bits属性中.打印验证一下

  • 类的类方法可以理解成元类对象的实例方法,因此存在元类中.

③ 结论

  • 成员变量存放在ivar
  • 属性存放在property,同时也会存一份在ivar,并生成settergetter方法
  • 对象方法存放在里面
  • 类方法存放在元类里面

④ API验证

利用底层开放的API可以验证以上结论

五、提出疑问

① 类存在几份?

由于类的信息内存永远只存在一份,所以 类对象只有一份.

② objc_object 与 对象的关系
  • 所有的 对象 都是以 objc_object为模板继承过来的
  • 所有的对象 是 来自 NSObject(OC) ,但是真正到底层的 是一个objc_object(C/C++)的结构体类型
  • objc_object对象的关系是继承关系
③ 什么是 属性 & 成员变量 & 实例变量 ?
  • 属性(property):在OC中是通过@property开头定义,且是带下划线成员变量 + setter + getter方法的变量
  • 成员变量(ivar):在OC的类中{}中定义的,且没有下划线的变量
  • 实例变量:通过当前对象类型,具备实例化的变量,是一种特殊的成员变量,例如 NSObject、UILabel、UIButton
④ 成员变量 和 实例变量什么区别?
  • 实例变量(即成员变量中的对象变量 就是 实例变量):以实例对象实例化来的,是一种特殊的成员变量
  • NSString常量类型, 因为不能添加属性,如果定义在类中的{}中,是成员变量
  • 成员变量中除去基本数据类型、NSString,其他都是 实例变量(即可以添加属性成员变量),实例变量主要是判断是不是对象
⑤ isKindOfClass 和 isMemberOfClass 的理解

这是一道涉及isa走位图的面试题,大胆猜测下结果 先来探索一下isKindOfClassisMemberOfClass的实现 上图中我已做了详细的解析.那么实现情况是怎样的呢?

通过断点调试,isMemberOfClass类方法实例方法的流程是正常的,会走到上面分析的源码中,而isKindOfClass根本不会走到上面分析的源码中!!!注意这里,这是一个坑点),而是会走到下面这个源码中,其类方法实例方法都是走到objc_opt_isKindOfClass方法源码中

  • 汇编调用如下

  • objc_opt_isKindOfClass方法源码如下

为什么会这样呢?主要是因为在LLVM中编译时对其进行了优化处理. 所以调用objc_opt_isKindOfClass实际走的逻辑如图所示

接下来在结合isa走位图(实线为父类走向)可以得出前面四个打印结果:

  • NSObject元类与NSObject类不相等,NSObject元类的父类(指向NSObject类)与NSObject类相等——YES
  • NSObject元类与NSObject类不相等——NO
  • TCJPerson元类与TCJPerson类不相等,TCJPerson元类的父类与TCJPerson类不相等——NO
  • TCJPerson元类与TCJPerson类不相等——NO

后面四个结果分析如下:

  • NSObject类与NSObject类相等——YES
  • NSObject类与NSObject类相等——YES
  • TCJPerson类与TCJPerson类相等——YES
  • TCJPerson类与TCJPerson类相等——YES

六、补充知识

strong & copy & weak 底层分析

clang编译的cpp文件中可以发现 strong & copy & weak 修饰的属性在编译的底层代码中是有区别的

  • TCJPerson中我们定义了两个两个属性,分别用copystrong修饰
  • clangmain.m文件编译成main-arm64.cpp,然后发现 copystrong修饰的属性的set方法是有区别的

这里就有疑问了,为什么copy修饰的属性使用了objc_setProperty,而strong修饰的没有?

  • LLVM中搜索objc_setProperty,找到如下所示的getOptimizedSetPropertyFn方法 从这里即可看出,针对不同的修饰符,返回的是不同的
  • 如果是atomic & copy修饰,nameobjc_setProperty_atomic_copy
  • 如果是atomic没有copy修饰,nameobjc_setProperty_atomic
  • 如果是nonatomic & copy 修饰,nameobjc_setProperty_nonatomic_copy
  • 其他剩余的组合,即nonatomic、nonatomic & strong、nonatomic & weak等,nameobjc_setProperty_nonatomic

上述的几个name分别对应objc4-818.2源码中的如下方法

然后通过汇编调试发现,最终都会走到objc_storeStrong

  • copy修饰的属性汇编调试结果

  • strong修饰的属性汇编调试结果

  • 源码中搜索objc_storeStrong,有如下源码,主要也是retain新值,release旧值

  • llvm编译源码中搜索objc_storeStrong,找到EmitARCStoreStrongCall方法,如下图所示,发现copystrong修饰的属性执行的策略是不一致的

  • llvm中搜索EmitARCStoreStrongCall方法,在GenerateCopyHelperFunction方法有调用,然后在这里发现了strongweak的不同处理 其中BlockCaptureEntityKind有如下的枚举值以及表示的含义

  • 如果是weak修饰,执行EmitARCCopyWeak方法,如下所示,weak在底层的调用是 objc_initWeak

  • 如果是strong修饰,执行EmitARCStoreStrongCall方法

结论

  • copystrong修饰的属性在底层编译的不一致,主要还是llvm中对其进行了不同的处理的结果.copy的赋值是通过objc_setProperty,而strong的赋值时通过self + 内存平移(即将指针通过平移移至name所在的位置,然后赋值),然后还原成 strong类型
  • strong & copy 在底层调用objc_storeStrong,本质是新值retain,旧值release
  • weak 在底层调用objc_initWeak

② Type Encoding & Property Type String

Type Encoding-官方文档 Property Type String-官方文档

clang中的方法签名

Type encoding -- clang编译后,方法列表的这些字符的含义是什么?

@16@0:8为例

  • @16表示返回字符串占用16个字节 -- 第二个@8字节sel8字节
    • 第一个@ 表示 返回值
    • 16 表示总共占用的字节数16字节
    • 第二个@:第一个参数
      • id -- @ 统配类型
      • typedef struct objc_object *id
    • 0 -- 从0开始 0-88字节
    • -- 代表sel,方法编号
    • 8 -- 从8开始 8-168字节
  • v24@0:8@16中的 v -- void 无返回值

更多的可以查看官网的以下列表

clang编译后的属性的attribute

clang编译输出了属性的attribute,同样也可以通过property_getAttributes方法获取

  • T 表示 type
  • @ 表示 变量类型
  • C 表示 copy
  • N 表示 nonatomic
  • V 表示 variable 变量,即下划线变量 _tcj_name

更多的可以查看官网的以下列表

写在后面

和谐学习,不急不躁.我还是我,颜色不一样的烟火.