OC底层学习-对象的本质

710 阅读10分钟

引言

前面我们学习了对象alloc的流程,影响对象的内存大小的根本因素等内容,那我们有没有想过什么是对象呢?那接下来我们一起来探索一下对象的本质

结构体本质探究

我们先上代码: image.png 我们创建了一个继承与NSObjectLhkhPerso对象,但是你不觉得这个LhkhPerson很抽象吗?它到底是什么?怎么去认知它呢? 经过前面的学习,我们掌握了几种学习底层的方法,我们可以通过汇编,LLDB调试,底层源码调试等手段是探索底层,那么接下来我们探索对象的本质会通过一个更加好玩的东西,也就是Clang

Clang的认识

Clang及其文档Clang文档

Clang可以理解为基于LLVMCC++Objective-C语言的轻量级编辑器,是由Apple主导编写的;Clang支持其普通的lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。我们知道OC语言是一门高级语言,我们往往看不到其底层是怎么实现的,而Clang的作用就是将我们OC语言代码文件编译成底层语言文件,就例如可以将OC.m文件编译成.cpp文件,便于我们更好的看到其底层实现,那我们就来探究一下吧。

Clang命令

//直接将main.m oc文件编译为main.cpp c++文件
clang -rewrite-objc main.m -o main.cpp //mac工程可直接使用
//如果是iOS工程使用上面的命令会出现UIKit找不到报错情况,则用下面命令 
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
//我们安装xcode的时候其实就已经安装了xcrun命令 xcrun命令基于clang基础上进行了封装更好用
//模拟器编译
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 
//真机编译
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp 

我们定位到想要编译的文件目录,通过上面的Clang命令,我们这边直接编译main.m文件,得到了main.cpp文件,因为我们是研究LhkhPerson这个对象,直接在main.cpp 文件中搜索LhkhPerson,直接定位:

image.png image.png 我们通过直接定位源码过后发现:

  • LhkhPerson其实就是一个结构体(struct LhkhPerson_IMPL);
struct NSObject_IMPL NSObject_IVARS; //继承自NSObject的isa,下面会介绍
NSString *_lhkhName;//我们自定义的属性
  • NSObject实质也是一个结构体,内部只有一个成员变量Class isa;
Class 在之前探索中我们已经知道其是一个objc_class *类型,也就是一个结构体指针 我们这边也
验证一下看看,通过搜索objc_class

typedef struct objc_class *Class;
struct objc_object {
    Class _Nonnull isa __attribute__ ((deprecated));
};

这边我们眼睛大的可能会发现id,SEL,其实也是一个结构体指针,也就解释了我们的id定义的类型不用加`‘*’`
typedef struct objc_object *id;
typedef struct objc_selector *SEL;
  • typedef struct objc_object LhkhPersontypedef struct objc_object NSObject,其中都有objc_object这个类型;这边需要注意,在OC层面我们对象都是继承自NSObject,但是在OC下层对象就是objc_object

对象成员变量和属性的扩展

image.png 我们加了一个成员变量,再来看看c++文件 image.png

  1. 通过@property声明的属性底层会生成 _lhkhName 变量和对应的getter和setter方法,而在大括号里面的成员变量则只会生成对应的变量;这也验证了OC的点语法,通过@property声明的属性底层自动给其添加了带_的变量和gettersetter方法,所以我们可以通过对象加上.直接访问,而大括号里面的成员变量则只是生成了声明的变量并没有其gettersetter方法。
  2. 底层中的(*(NSString **)((char *)self + OBJC_IVAR_$_LhkhPerson$_lhkhName)) 这个是为了取到当前lhkhName的地址所对应的值,通过获取到当前对象的首地址加上属性的偏移量从而获取到属性的地址,再取到地址所存储的值,最后强转成NSString类型。

对象本质总结

通过上面的c++文件的探究,我们可以确定对象其实就是一个结构体,其第一个成员变量是继承自父类的isa

那我们知道了对象的本质是结构体且其第一个成员变量是继承自父类的isa,那么isa到底是什么呢?

isa探究

在探索isa之前,我们需要先了解一些基础知识。我们知道结构体的内存是其内部的元素的占用大小的总和且按照结构体内存原则来开辟的,那我们有没有想过,在某些情况下这种会很浪费内存,Apple这么细致你觉得他会不知道吗?所以避免这些情况,Apple又出现了位域联合体,那接下来我们一起来看一看什么是位域和联合体:

位域

image.png 我们看上面的例子,LhkhCar1是一个结构体,通过之前的学习,我们知道此时LhkhCar1的占用内存大小为4字节(BOOL占用一个字节),那么为什么LhkhCar2只占用1字节呢?其实这个里面就是位域在作怪。。。

注意LhkhCar2中的 :1 这个即指的位域,表示当前这个元素只占用1 bit位 1byte = 8 bit

我们来分析一下

LhkhCar1 占用4个字节 也就是 占用32bit,0000 0000 0000 0000 0000 0000 0000 0000
其实我们存这4个元素的状态的时候只需要4个bit即可 0000 1111,这样一来我们最多需要半个字节,
但是没有半个字节,所以1个字节足够,这也就导致了 LhkhCar1 浪费了3倍的内存;

我们再来看使用了位域的结构体LhkhCar2,占用1字节,为什么这样呢?我们想一下,一个车子在前进
的时候还能同时后退吗,显然是不行的,所以位域的作用就是使得这些变量是互斥关系,有你没我,从而
达到节省内存。

联合体

我们再来看一个例子结构体struct和联合体union image.png image.png 我们通过lldb调试发现结构体t1里面的变量最终都是我们赋予的值,而我们发现联合体t2在刚开始声明的时候就出现了值,可以看出是系统分配的脏数据,然后我们一步一步走的时候发现后面每赋值一个变量,上一个变量的值就没有了。

通过上面的例子我们可以总结一下结构体和联合体

  • 结构体struct里面所有的变量是共存关系,优点是‘有容乃大’、全面;缺点就是内存空间分配粗放,不管你用不用全部分配;
  • 联合体union里面所有变量是互斥关系,优点是内存的使用精细灵活,节省空间;缺点就是不够‘包容’。
注意点:联合体的内存大小取决于其内部最大的变量的大小,例如上面的t2里面最大的为double类型占用8字节,所以最终联合体的大小为8

isa内部结构

那了解了联合体后我们回过头来回顾一下alloc探究的时候,最终会有调用三个方法instanceSizecallocinitInstanceIsa,而内存开辟我们已经了解了,那么initInstanceIsa的过程是怎么样的呢,我们再来走一遍objc源码 我们通过alloc流程进入到initInstanceIsa方法中, image.png 然后进入到initIsa方法中,

image.png

需要注意点:nonpointer 非指针源,我们知道isa占用8个字节,也就是64位,根据我们上面探究的,
一个isa会单单纯纯占用这么多位?我们项目中一班会有很多的isa,那么这还怎么做到节省内存呢,
所以这边就会有nonpointer的存在,isa里面不单单是一个指针,内部结构中还可能包括 是否正在释放、
引用计数、weak、关联类、析构函数等

可以看到在initInstanceIsa方法中直接传入的是true,也就是非指针源,那这就给我们探究isa里面
到底存放了些什么铺开了路

通过源码我们可以得到isa的类型为isa_t,那么我们进入到isa_t image.png 我们发现isa_t是一个联合体,里面有两个变量 clsbits,还有一个宏定义ISA_BITFIELD

这个宏主要对应两个端 arm_64 的iOS端和 __x86_64 的macOS端,而里面使用的正是我们上面探索的位域,具体看下图:

image.png 名词解释:

nonpointer:表示是否对isa指针进行优化,0表示纯指针,1表示不止是类对象的地址,isa中
包含了类信息、对象、引用计数等
has_assoc:关联对象标志位,0表示未关联,1表示关联
has_cxx_dtor:该对象是否C ++ 或者Objc的析构器,如果有析构函数,则需要做析构逻辑,
没有,则释放对象
shiftcls:储存类指针的值,开启指针优化的情况下,在arm64架构中有33位用来存储类指针,
x86_64架构中占44magic:用于调试器判断当前对象是 真的对象 还是没有初始化的空间
weakly_referenced:指对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以
更快释放
deallocating:标志对象是否正在释放
has_sidetable_rc:当对象引用计数大于10时,则需要借用该变量存储进位
hextra_rc:表示该对象的引用计数值,实际上引用计数值减1,例如,如果对象的引用计数为10,
那`extra_rc为9,如果大于10,就需要用到上面的has_sidetable_rc

我们大概可以看一下arm_64 和 __x86_64 64位存储分布图 未命名文件(2).png

isa关联类

通过上面的64位存储分布图可以了解到真正与类相关的为shiftcls,这个里面存储着类的相关信息,我们不妨来看一看:

通过LLDB x/4gx 打印一下当前对象的地址信息,然后p/t一下isa,获取到整个isa的64存储信息

image.png image.png 这边的ISA_MASK是一个掩码,主要作用是通过掩码过滤掉其他信息只返回类信息,也就是shiftcls的内容。

我们继续走,通过断点进入到initIsa方法中

image.png 再往下走, image.png 可以发现isa与类关联的核心代码是在setClass这个方法内部,进入到setClass

image.png 这里为什么 shiftcls = (uintptr_t)newCls >> 3

知道了64位分布,那反着来推算一下 image.png 知道了在macOS端64位分布图,为了取到shiftcls的信息,先向右移3位,舍弃掉前三位信息,然后我们知道原始的shiftcls后面有17位,加上右移的3位,那么在左移20位,舍弃掉后17位信息,最后我们再右移17位,即还原shiftcls的位置,从而取到所对应的类的信息,看上面的打印,结果是一摸一样的,这也就是isa关联到类的关键步骤。

initnew探究

通过源码搜索到init image.png 对象调用init,然后进入到_objc_rootInit,直接返回objc

image.png

再来看看new 通过源码搜索new,实际上类调用new其实在源码中被转化为调用callAllocinit image.png