阅读 515

iOS 底层探索03——iOS 对象的本质

这是我参与更文挑战的第5天,活动详情查看: 更文挑战

引言

  1. 我们已经知道iOS对象的+alloc方法底层进行了instanceSize(内存分配)initInstanceIsa(绑定类型)等操作,内存分配和计算相关信息可以通过iOS 底层探索01——alloc初探iOS 底层探索02——结构体对齐原则进行回顾;
  2. 本文将继续对alloc方法的核心方法initInstanceIsa(绑定类型)进行探索;

一、对象的本质

1.1 LLVM与GCC

  1. 我们都知道OC是C语言的超集,为了探索OC对象的本质,最好的办法是看到OC对象在底层语言的实现;早期的OC编译器使用的是GCC编译器,但GCC编译器编译器的前端和后端过度耦合,当需要扩展语言支持的CPU架构时不仅要新增编译器的后端功能,前端代码也需要增加,非常麻烦;
  2. 为了解决GCC编译器前后端耦合不利于扩展的问题,苹果引入了LLVM编译器,由于LLVM编译器的前端和后端是隔离开来的,当扩展语言支持的架构时只需要修改编译器后端,扩展性更高,性能更好,现阶段Xcode内置的编译器已经替换为LLVM;
  3. 这里我们使用LLVM编译器框架中的前端编译器clangOC代码还原成c++

1.2 Clang重写OC

有两种还原方式,分别是:

  • Clang直接重写
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.2.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk main.m
复制代码
  • 使用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 mainarm64.cpp (⼿机) 
复制代码

上述两种方式都可以将main.m重写为main.cpp,重写完成之后我们进一步查看底层C++文件;

重写前 1.1.jpg

重写后 1.3.png

1.3 阅读重写后的OC代码

  1. 通过关键字搜索发现OC对象GCPerson被转换成了GCPerson结构体
  2. GCPerson结构体内部持有一个NSObject_IMPL结构体类型的成员变量NSObject_IVARSNSString类型的gcName;
  3. 最终可以确定OC对象GCPerson底层主要内容是一个只有一个成员变量isaNSObject_IMPL结构体,并且所有继承于NSObjectOC对象底层实现都具有的结构体NSObject_IMPL
struct GCPerson_IMPL {//转换后的结构体GCPerson
	struct NSObject_IMPL NSObject_IVARS;
	NSString *__strong _gcName;
};

struct NSObject_IMPL {//所有对象底层实现都具有的结构体NSObject_IMPL
	__unsafe_unretained Class isa;
};
复制代码
  1. 同时我们还发现和idClass的一些底层实现信息
typedef struct objc_object GCPerson;//把objc_object 起别名为GCPerson

typedef struct objc_class *Class;//objc_class结构体的指针起别名为Class

typedef struct objc_object *id;// objc_object结构体指针起别名位id
复制代码

因为*Class*id的别名中已经包含了指针*所以我们上层在使用idClass类型的时候可以直接不用加*了;

  1. 另外可以发现编译器帮我们把OC对象GCPerson的属性gcName添加了set方法和get方法;方法中增加了GCPerson类型的selfSEL类型的cmd这两个参数;
//set方法
static void _I_GCPerson_setGcName_(GCPerson * self, SEL _cmd, NSString *gcName) { 
(*(NSString *__strong *)((char *)self + OBJC_IVAR_$_GCPerson$_gcName)) = gcName; 
}

//get方法
static NSString * _I_GCPerson_gcName(GCPerson * self, SEL _cmd) { 
return (*(NSString *__strong *)((char *)self + OBJC_IVAR_$_GCPerson$_gcName)); 
}
复制代码
  1. 通过gcNameget方法可以看出:获取gcName成员的方法是self的地址 + OBJC_IVAR的地址,然后强转成OCNSString即可;为什么会这样?self地址是当前对象的首地址,将ivar跳过之后的下一个位置就是成员变量gcName的地址;如下图所示;

1.4.jpg 7. 继续阅读源码我们发现底层中还存在其他信息,如_protocol_t,ivar,methodlist,,category_t,class_t等相关信息;

1.5.jpg

二、nonPointerIsa

2.1 结构体、联合体、位域

在探索nonPointerIsa方法之前我们先了解下列几个知识点

2.1.1 结构体

结构体(struct)是一种存储结构,特点是所有成员变量是“共存”的,优点是“有容乃⼤”,信息更全⾯;缺点是struct内存空间的分配是粗放的,不管⽤不⽤,全部分配,不够精细;

2.1.2 联合体

联合体(union)也是一种存储结构,特点是各变量是“互斥”的,优点是内存使⽤更为精细灵活,也节省了内存空间;缺点就是不够“包容”,使用覆盖技术导致内部的成员变量互相覆盖,同时只能存储一个成员变量值;如下图所示age会覆盖掉name

设置联合体的成员变量name 2.1.jpg

设置联合体的成员变量age 2.2.jpg

2.1.3 位域

  • 位域是一种常用于结构体中的存储技术,一般情况下结构体信息的存取以字节为单位,实际上有时存储信息只用一个字节中的几个byte位即可,这种情况下可以使用位域存储技术;如图所示

2.4.jpg

  • 例如:一个BOOL占用一个字节8位,4个BOOL共占用32位,但实际上BOOL01表示只需要一个byte位即可存储,4个BOOL用4个byte位半个字节就够了;
  • 位域虽然可以节省存储空间,但由于需要读写时需要对特定byte位进行操作,不是很方便,在当前硬件条件下,日常开发中使用位域并不常见;
  • 位域的读写需要借助于掩码来进行,所谓掩码就是一串特定的byte位信息,通过将原数据掩码进行按位与&或者按位或|运算,即可对位域中的值进行读写;

2.1.4 位域和联合体结合使用

一般情况下位域联合体一起使用,可以实现节省存储空间的作用;

2.2 initInstanceIsa

2.2.1 isa_t

之前我们分析alloc过程中核心方法instanceSizecalloc,现在继续分析另一个核心方法initInstanceIsa,这里可以看到一个关键信息isa_t

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); //判断是否TaggedPointer
    
    isa_t newisa(0);

    if (!nonpointer) {
        newisa.setClass(cls, this);
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());


#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
#endif
        newisa.extra_rc = 1;
    }
    isa = newisa;
}
复制代码

isa_t的简略结构如下,为了不影响分析我删除了一些无关紧要的信息;

union isa_t {
    isa_t() { }//构造方法
    isa_t(uintptr_t value) : bits(value) { }//构造方法
    uintptr_t bits;
private:
    Class cls;
public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // 关键信息 defined in isa.h
    };
    ...无关内容
};
复制代码

isa_t结构中可以看到ISA_BITFIELD这个成员变量,进入ISA_BITFIELD定义界面发现了真相;

2.2.2 ISA_BITFIELD

ISA_BITFIELD是一个使用了位域存储技术的结构体,结构体内部不同的byte位存储的信息如下

//arm64真机真机下各个byte位的存储信息
# if __arm64__                                        
        uintptr_t nonpointer        : 1;                           
        uintptr_t has_assoc         : 1;                           
        uintptr_t has_cxx_dtor      : 1;                           
        uintptr_t shiftcls          : 33; 
        uintptr_t magic             : 6;                           
        uintptr_t weakly_referenced : 1;                           
        uintptr_t unused            : 1;                           
        uintptr_t has_sidetable_rc  : 1;                           
        uintptr_t extra_rc          : 19
#   endif

//_x86_64架构下各个byte位的存储信息
# elif __x86_64__
      uintptr_t nonpointer        : 1;                            
      uintptr_t has_assoc         : 1;                            
      uintptr_t has_cxx_dtor      : 1;                             
      uintptr_t shiftcls          : 44;  
      uintptr_t magic             : 6;                             
      uintptr_t weakly_referenced : 1;                             
      uintptr_t unused            : 1;                             
      uintptr_t has_sidetable_rc  : 1;                             
      uintptr_t extra_rc          : 8
复制代码

ISA_BITFIELDbyte位上的分布如下图,每一种颜色代表存储某个成员变量所需要占用的byte位个数 2.5.jpg 成员变量占用byte位的位置和成员变量的用途如下表所示

成员变量arm下位置x86_64下位置用途
nonpointer6363表示是否对 isa指针开启指针优化0:纯isa指针,1:不⽌是类对象地址,isa中包含了类信息、对象的引⽤计数等
has_assoc6262关联对象标志位,0没有,1存在
has_cxx_dtor6161该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
shiftcls28-6017-60存储类指针的值。开启指针优化的情况下,在 arm64架构中有 33 位⽤来存储类指针;在X86_64架构下有44位用来存储类指针
magic22-2711-16⽤于调试器判断当前对象是真的对象还是没有初始化的空间
weakly_referenced2110标志对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放
unused209标志对象是否正在释放内存
has_sidetable_rc198当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位
extra_rc0-180-7当表示该对象的引⽤计数值,实际上是引⽤计数值减 1,例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,则需要使⽤到下⾯的 has_sidetable_rc

2.3 shiftcls的读取方式1

  1. 我们已经知道ISA_BITFIELD为位域存储的,shiftcls作为位域结构体中的成员变量,读取可以使用ISA_BITFIELD&ISA_MASK的方式;
  2. person对象初始化完毕后,通过x/4gx person查看person对象的ISA_BITFIELD信息;
  3. 根据当前的设备架构,选择合适的ISA_MASKISA_BITFIELD进行&(按位与)操作获取当前对象的Class对象指向的地址:p/x 0x000001a10405d685 & 0x0000000ffffffff8ULL;
  4. 通过p/x person.class操作查看person对象指向的Class对象地址;
  5. 发现步骤3和步骤4获取的Class对象地址一致,证明读取方式1是没问题的;

2.8.jpg

2.4 shiftcls的读取方式2

我们分析系统的initIsa方法时发现绑定Class的实际方法是newisa.setClass(cls, this),具体实现如下

inline void
isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{
#if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#   if ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_NONE
    uintptr_t signedCls = (uintptr_t)newCls;

#   elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ONLY_SWIFT
    uintptr_t signedCls = (uintptr_t)newCls;
    if (newCls->isSwiftStable())
        signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));
#   elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ALL
    uintptr_t signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));
#   else
#       error Unknown isa signing mode.
#   endif
    shiftcls_and_sig = signedCls >> 3;
#elif SUPPORT_INDEXED_ISA
    cls = newCls;
#else 
    shiftcls = (uintptr_t)newCls >> 3;
#endif
}
复制代码

这段方法本质上是对ISA_BITFIELD进行移位操作;下面我们就以arm64架构下的真机来演示上述代码所进行的移位操作;

  1. 因为ISA_BITFIELD中的固定位是shiftcls,所以我们需要通过位移操作,把不相关的byte位信息给移到64位以外,这样留下来的就是shiftcls位的信息了;
  2. 首先把shiftcls右移3位将shiftcls右边所有byte位清空;
  3. 再把shiftcls左移3+28位将shiftcls做边所有byte位清空;
  4. 经历过步骤3和步骤4的操作后此时ISA_BITFIELD中已经只剩下shiftcls信息了,但是经过移位后shiftcls的位置发生了改变需要右移28位还原到原来的位置;
  5. 发现步骤3和步骤4获取的Class对象地址一致,证明读取方式2是没问题的;

2.9.jpg

三、init、new分析

  1. 对象的init方法主要作用:提供一个工厂设计模式的构造函数,用来给子类便于重写初始化进行扩展;
  2. new方法内部也会调用init方法,new 本质上就是alloc 和 new 结合

3.0.png 3. 汇编查看alloc会调用objc_alloc_init,但是new调用的是 objc_opt_new, 经过查看objc源码发现这两个方法的实现是一样的;所以alloc、init的初始化方式和new的初始化方法可以看作是一致的;

4.1.jpg

4.2.jpg

4.3.jpg

文章分类
iOS
文章标签