Objective-C 底层对象探究-下

894 阅读14分钟

“我正在参加中秋创意投稿大赛,详情请看:中秋创意投稿大赛

目录

1. 背景

学习不迷茫,无阻我飞扬!大家好我是Tommy!本篇是Objective-C 底层对象探究的最终篇,废话不说我们这就开始!

2.从编译后的文件理解OC对象

  • 通过xcrun编译成C++文件
    • 再上一篇内容中我们是通过clang命令来进行的编译的,其实还有一种方法就是通过xcrun命令也可以达到一样的效果。
        xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
    
  • 对苹果开发语言的层次结构的理解
    • 这里简要的把我个人对苹果开发语言的一些理解简单说明一下,我们大家都知道目前苹果提供2种开发语言Objective-CSwift,在2014年之前苹果都是选用OC来担任开发语言,直到2014年的WWDC才将Swift展现在广大开发者的面前,推出之后也是受到了广大开发人员的强烈关注;
    • 苹果之所以大费周章的推出一种新语言,我想是Objective-C已经无法达到苹果对于效率方面的满足了,如果你关注每年的WWDC的话,其实可以感觉出来苹果在效率的问题上一直是追求极致的,通过上两篇的学习我们也能通过底层代码感觉出由于Objective-C的一些语言特性导致苹果为其做出的效率上的牺牲,也就不难理解Objective-C在苹果追求效率的路上成为了最大的屏障,所以对其再重新创建一种语言也就合情合理了。
    • 其实对于我们开发者而言,无论是Objective-C还是Swift其实只是苹果给予开发者在上层结构的一种开发方式,举个例子:就相当于购买了一个电子产品,想要运行这个电子产品功能,就需要阅读说明指南,而Objective-C还是Swift就相当于一种操作指令,用户通过指令来控制这个产品的功能,上层指令不管方式如何改变(不管你是物理按钮,还是电子屏幕触控),都不会对影响到底层的功能。虽然底层功能不会发生改变,但是两种方式带来的效率就会产生差距了。 图片.png
  • 从编译后文件我们能知道什么?
    • 对象的本质就是结构体,我们通过查看编译成C++的文件就可以发现,一个叫做ZXPerson_IMPL的结构体,这个就是我们创建的ZXPerson对象 ps:如果把ZXPerson类的定义放到main.m中会看到更多内容 图片.png 图片.png
    • 如果想验证一下,我们可以通过在ZXPerson类中增加一个成员变量后,再编译成C++来观察变化。编译后我们就能看到在ZXPerson_IMPL内部新增了一个我们创建的成员name图片.png 图片.png
    • 除了我们新增的成员name外,我们发现还会有一个默认的成员结构体NSObject_IMPL NSObject_IVARS,这个是什么呢?通过搜索NSObject_IMPL我们就一目了然——其实就是isa图片.png
    • Isa是指向类的结构体的指针,我们搜索Class可以看到其实是objc_class结构体的指针。 图片.png
    • id类型是指向对象的指针,我们再往下看可以看到在OC中id类型其实是一个对象的指针。 图片.png
  • 属性取值的分析
    • 观察_I_ZXPerson_nikeName函数(在OC中其实就是getNikeName()方法)返回时的语句,我们看到在底层并不是直接将数值进行返回的,而是通过(char *)self(对象首地址)加上OBJC_IVAR_$_ZXPerson$_nikeName(变量的偏移量)来找到实际数值的地址,再进行类型转换最终返回。 图片.png
  • 节点小结:
    • 可以通过clangxcrun方式对.m文件进行编译,编译后可以帮助我们理解底层对象的实现,本小结内容到此结束。

3. 位域与公用体

  • 位域:
    • 所谓的位域其实是在struct结构体中的一种表达语法,他的含义是为结构体中的成员明确定义其占用的二进制位数,听起来有点绕哈,其实一点也不难理解,请看下面的例子: 图片.png
    • 例子中结构体ZXStruct1包含4个成员,每个成员的类型是BOOL型(占用1个字节),打印占用的大小结果为4字节;
    • 例子中结构体ZXStruct2同样包含4个成员,每个成员的类型是BOOL型(占用1个字节),但是由于定位了位域所以成员声明后面增加了“:1”,最后打印占用的大小结果为1个字节;请看下面的说明图更便于理解。 图片.png
    • ZXStruct1结构体共占用4字节,32个二进制位,但是BOOL类型的话只需要一个进制位就可以表达了,其他进制位都是补零,所以空间方面有所浪费。
    • ZXStruct2结构体共占用1字节,由于定义了位域,使每个成员BOOL只占用1个二进制位故需要4个二进制位,又因8个二进制位为1个字节,所以只需1个字节就可以满足占用需求,大大节省了空间。
  • 公用体:
    • 我们知道结构体(Struct)是一种构造类型或复杂类型,它可以包含多个类型不同的成员。在C语言中,还有另外一种和结构体非常类似的语法,叫做共用体(Union),它的定义格式为:
    union 共用体名{
        成员列表
    };
    
    • 共用体有时也被称为联合或者联合体,这也是 Union 这个单词的本意。
    • 结构体与共用体在内存大小上也存在差异,结构体是各个成员会占用不同的内存,互相之间没有影响;而共用体是所有成员占用同一段内存,修改一个成员会影响其余所有成员,而内存的总大小已成员中最大占用的那个为准。
    • 主要区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。请看下面的例子: 图片.png 图片.png
    • 例子中结构体 ZXStruct3 包含 4 个成员,打印占用的大小结果为 32 字节;并且每个成员的值都是独立存放的,不会因为给其他成员赋值而改变。
    • 例子中共用体ZXStruct4包含4个成员,打印占用的大小结果为8字节,是因为成员中含有指针类型namenikeName,所以按照最大成员的大小进行分配。此外当我们分步骤进行成员变量赋值时,会发生改变其他成员变量值的现象。请看下面的说明图更便于理解。 图片.png 图片.png
  • 理解说明:
    • 1、共用体未对任何成员进行赋值操作时成员都是nil
    • 2、当 zx4.name="zhaoxin"进行赋值后内存地址发生变化,由于是指针类型需要占用8个字节,这时其实已经将整个共用体的内存占用满了;第二个成员nikeName也是指针类型,所以共用了成员name的内存,因此值与name一致;第三个成员age占用4个字节,由于IOS是小端模式,所以age的值为0x3f4c,转换为10进制正好是16204;最后一个成员heightdouble类型比较特殊所以值是‘0’;
    • 3、 当zx4.nikeName="zhaoxin"进行赋值后内存地址发生变化,与成员name的值一致;第三个成员age值为0x3f54;转换为10进制是16212;最后一个成员height依旧是‘0’;
    • 4、当zx4.age=20进行赋值后内存地址发生变化,成员namenikeName值为58 07 00 00,转换为ASCII为‘X’(07的ASCII是BEL (bell)不会被显示);age2016进制0x0014就是20),height依旧是‘0’;
    • 5、当zx4.height=179.2进行赋值后内存地址发送变化,成员namenikeName值无法读取,age则超出了范围大小了直接显示了最大数;height通过p/f方式打印可以读取到数值。
赋值顺序namenikeNameageheight
name赋值时zhaoxinzhaoxin162400
nikeName赋值时TommyTommy162120
age赋值时XX200
height赋值时nullnull越界了179.2
  • 节点小结:
    • 通过设置位域可以定义成员变量占用的二进制位的大小;
    • 普通结构体:结构体中的所有成员都会分配独立的内存空间且相互不会干扰,优点:不会互相影响;缺点:没有使用到的成员的空间会被浪费掉;
    • 共同体(联合体):共同体大小以成员中最大的那个为准,其中所有成员公用内存区域,优点:节省空间;缺点:成员的取值会发生变化;
    • 本小结内容到此结束。

4. nonPointerIsa的分析

  • 什么是nonPointerIsa:
    • 我们都知道Isa是指向类的一个指针,但是Isa也有包含一个特殊的种类,除了包括类信息之外还包含其他的信息例如:bitshas_cxx_dtorindexcls等信息的Isa,我们称作nonPointerIsa。(非单纯指针的Isa) (ps:在不设置环境变量OBJC_DISABLE_NONPOINTER_ISA =1的情况下,我们所用的Isa都是nonPointerIsa,后文有说明如何设置这个变量)
    • 可以通过objc源码来查看,从 _class_createInstanceFromZone() 开辟实例对象方法中对 obj->initIsa(cls) 代码进行追查。 图片.png 图片.png
  • Isa里面存放了什么信息:
    • 通过上面的源码分析,我们得知了nonPointerIsa除了类信息之外还会存放其他数据,源码中是将数据存放到了名叫newisa的对象里,newisa是一个叫做isa_t结构体类型,我们可以继续追踪这个isa_t结构体。 图片.png 图片.png
    • isa_t的结构体比较简单,包括2个构造方法、一个私有的成员cls、以及对cls操作的相关对外方法、最后就是最关键的成员结构体ISA_BItFIELD,这个就是isa真正存放数据的关键。此外我们在上一小结 位域与联合体 的知识就可以用到了。 图片.png
  • isa_t的特点分析:
    • 首先isa_t是一个共用体,并占有8个字节大小;
    • isa_t中有2个成员,一个是私有的cls、另一个就是内部结构体成员 ISA_BItFIELD,它俩共享8字节的大小空间;
    • 如果是非nonPointerIsa8字节大小只存放cls成员的信息,否则存放ISA_BItFIELD的信息;
    • 1个字节占用8个二进制位,isa_t占用8个字节即64个二进制位,ISA_BItFIELD通过定义了位域共占用64位,所以如果是nonPointerIsa则直接会占满。
    • ISA_BItFIELD的位域会根据当前系统进行调整,但是整体的大小不变,只是内部各个成员大小会发生细微变化。 图片.png
  • ISA_BItFIELD中成员的含义:
成员代表的含义
nonpointer表示是否对 isa 指针开启指针优化;为0时: 纯isa指针;为1时:不止是类对象地址,还包含了类信息、对象的引用计数等。
has_assoc关联对象标志位,0:没有,1:存在 。
has_cxx_dtor该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象 。
shiftcls储类指针的值。开启指针优化的情况下,在arm64 架构中有33 位用来存储类指针。
magic用于调试器判断当前对象是真的对象还是没有初始化的空间 。
weakly_referenced志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。
unused标志对象是否被使用。(源码版本objc-723这里是deallocating表示对象是否正在释放内存,我这里源码版本是objc-818.2使用unused来代替原deallocating;本身的意义应该是一致的)
has_sidetable_rc当对象引用技术大于 10 时,则需要借用该变量存储进位 。
extra_rc当表示该对象的引用计数值,实际上是引用计数值减1,例如:如果对象的引用计数为10,那么extra_rc 为9。如果引用计数大于 10,则需要使用到上面的has_sidetable_rc。
  • 通过LLDB打印isa的二进制位
    • 之前我们可以通过x/4gx来打印isa的地址,现在我们已经了解了isa是占用了64个二进制位,如果想验证一下我们可以通过p/t (打印二进制),输出之后就得到了完整的二进制了(起始是从右往左)图片.png
  • ISA_MASK是作用
    • ISA_MASK是一个掩码,他的主要作用是通过掩码将不想得到的数据过滤掉,只留下想要的数据。具体实际情况就是在isa中,最重要是就是类有关的信息也就是shiftcls的内容,所以这个ISA_MASK的作用就是过滤掉其他信息只返回类信息。

    图片.png 图片.png

    • 依旧通过x/4gx来打印isa的地址,然后与上ISA_MASK的值,得到的就是类信息;在通过p/x ZXPerson.class来进行验证。结果是两个值都是一致的。
  • 设置环境变量 OBJC_DISABLE_NONPOINTER_ISA
    • 上文中提到过可以通过设置环境变量OBJC_DISABLE_NONPOINTER_ISA来改变isa的类型,OBJC_DISABLE_NONPOINTER_ISA的含义是:当设置值为1时,当前创建的所以isa均为普通isa。
    • 在Edit_Scheme中添加变量: 图片.png
    • 进行对比后我们发现isa二进制的首位发送了变化;普通的isa首位是‘0’,并且整体只保留了shiftcls的信息。

    图片.png 图片.png

  • 我是如何知道这些环境变量的?(2021-7-21补充)
    • 其实类似这种的环境变量还有很多,我是怎么知道有OBJC_DISABLE_NONPOINTER_ISA这个的呢?
    • 其实很简单,我们只需在终端输入 export OBJC_HELP=1 即可将所有环境变量打印出来,并且每个环境变量后面还有对应的用途与解释。大家不妨可以自己耍一耍。 图片.png

5. isa的位运算

  • 通过对isa地址进行位运算得到类信息
    • 上一节我们通过ISA_MASK来获取了类信息,本节我在介绍一种方式:采用对isa地址进行位运算来获取类信息。
    • 我们知道shiftcls的位置就存放类信息的地方,他在结构体中占用33位。我看先按右移3位、左移28+3位;、右移28位;三步骤就可以将shiftcls前后的数据进行清空,这时isa中剩下的数据就只有类信息了。请看如下示意图: 图片.png
    • 下面是验证结果,最终结果与我们料想的一致。 图片.png
  • 节点小结:
    • 通过对isa占位的理解,通过对isa地址进行位运算的方式,同样可以获取到类信息。本小结内容到此结束。

6. init与new的区别

  • init源码:
    • 我们通过command + shift + O 来搜索init,找到后点击进入; 图片.png
    • 找到实例对象会调用的入口,但是里面没有任何处理直接将obj对象返回了。 图片.png 图片.png
  • new源码:
    • 通过command + shift + O 来搜索new,找到后点击进入; 图片.png 图片.png
    • 找到入口后发现此方法就是再调用了callAlloc后再进行init的调用操作,所以验证了 [[alloc]init] new 是效果是相等的。
  • 节点小结:
    • 通过对源码的分析,我们得到的结论就是 init只是单纯的初始化,而new则是 alloc + init 。本小结内容到此结束。

7. 总结

  • 1、可以通过clang、xcrun等命令对OC源码进行编译,编译后的代码可以让我们更明确的分析底层实现。
  • 2、在结构体中可以通过设定位域来对内部成员进行独特的设置。
  • 3、共用体的特性:所含成员中最大的占位就是共用体的大小;内部占用空间是共享的,给不同成员赋值时会改变其他成员的值;
  • 4、nonPointerIsa是一种特殊的isa,里面除了包含class信息之外,还有其他额外的数据;
  • 5、通过isa_t可以对其进行位运算来获取想要的数据;
  • 6、init只是单纯的初始化方法,苹果没有对齐进行特殊处理;newalloc + new的简便方式。
写到最后
导航: