Objective-C 底层对象探究-下

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!前端

目录

1. 背景

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

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 图片.pnggithub

    • 若是想验证一下,咱们能够经过在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
  • 理解说明:
    • 一、共用体未对任何成员进行赋值操做时成员都是nil
    • 二、当 zx4.name="zhaoxin"进行赋值后内存地址发生变化,因为是指针类型须要占用8个字节,这时其实已经将整个共用体的内存占用满了;第二个成员nikeName也是指针类型,因此共用了成员name的内存,所以值与name一致;第三个成员age占用4个字节,因为IOS是小端模式,因此age的值为0x3f4c,转换为10进制正好是16204;最后一个成员heightdouble类型比较特殊因此值是‘0’;
    • 三、 当zx4.nikeName="zhaoxin"进行赋值后内存地址发生变化,与成员name的值一致;第三个成员age值为0x3f54;转换为10进制是16212;最后一个成员height依旧是‘0’;
    • 四、当zx4.age=20进行赋值后内存地址发生变化,成员namenikeName值为58 07 00 00,转换为ASCII为‘X’(07的ASCII是BEL (bell)不会被显示);age2016进制0x0014就是20),height依旧是‘0’;
    • 五、当zx4.height=179.2进行赋值后内存地址发送变化,成员namenikeName值没法读取,age则超出了范围大小了直接显示了最大数;height经过p/f方式打印能够读取到数值。
赋值顺序 name nikeName age height
name赋值时 zhaoxin zhaoxin 16240 0
nikeName赋值时 Tommy Tommy 16212 0
age赋值时 X X 20 0
height赋值时 null null 越界了 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. 总结

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