iOS 底层原理-OC对象的本质与isa

82 阅读5分钟

一、了解Clang

1.clang

Clang是⼀个C语⾔、C++、Objective-C语⾔的轻量级编译器。源代码发布于BSD协议下。Clang将⽀持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。

Clang是⼀个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器

2013年4⽉,Clang已经全⾯⽀持C++11标准,并开始实现C++1y特性(也就是C++14,这是C++的下⼀个⼩更新版本)。Clang将⽀持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。

Clang是⼀个C++编写、基于LLVM、发布于LLVMBSD许可证下的C/C++/Objective-C/ Objective-C++编译器。它与GNUC语⾔规范⼏乎完全兼容(当然,也有部分不兼容的内容,包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,⽐如C函数重载(通过__attribute__((overloadable))来修饰函数),其⽬标(之⼀)就是超越GCC。

2.clang作用

因为OC是C、C++的超集,通过clang可以将m文件编译成cpp文件,这样我们可以了解更多的关于底层的实现原理。

3.clang的使用方式

  1. clang -rewrite-objc main.m -o main.cpp —— 把⽬标⽂件编译成c++⽂件

  2. UIKit报错问题 clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / Applications/Xcode.app/Contents/Developer/Platforms/ iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk main.m

  3. xcrun命令

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 (⼿机)

二、对象的本质

1.使用clang编译生成cpp文件

直接上代码!引入一个案例,在main.m文件中添加JHSPerson类的声明和实现,如下图所示

WeChat6f647a2c8097eb1eb1eb1c16255cf8c7.png

打开系统终端,进入main.m文件所在的目录,运行命令clang -rewrite-objc main.m -o main.cpp

命令在终端执行完之后,就可以在其目录生成main.cpp文件

2.main.cpp解读

打开main.cpp文件,从我们要了解的对象JHSPerson开始!

1.JHSPerson对象

全局搜索JHSPerson,可以找到main文件在底层编译成C++语言代码:


#define _REWRITER_typedef_JHSPerson
// JHSPerson结构体声明
typedef struct objc_object JHSPerson;

typedef struct {} _objc_exc_JHSPerson;

#endif

extern "C" unsigned long OBJC_IVAR_$_JHSPerson$_JHSName;

// JHSPerson_IMPL结构体实现
struct JHSPerson_IMPL {

struct NSObject_IMPL NSObject_IVARS;

NSString *_JHSName;

};

代码理解:

  1. typedef struct objc_object JHSPerson C++ 语言定义的结构体相当于 JHSPerson == struct objc_object 结构体; 2.结构体JHSPerson_IMPL实现中,有一个成员变量NSObject_IVARS,来自所继承的结构体,也就是isa;另一个成员变量是_JHSName,也就是JHSPerson的属性,和OC层面定义是一致的。

2.NSObject对象

// NSObject结构体声明 
typedef struct objc_object NSObject; 
typedef struct {} _objc_exc_NSObject;
#endif

// NSObject实现-对象
struct NSObject_IMPL { 
Class isa; 
};

同上理解:

  1. 定义别名NSObject,同样指向struct objc_object类型
  2. NSObject结构体实现中,有一个Class类型的成员变量isa

3.底层结构关系

进一步搜索Class的定义和objc_object的定义,见下面代码:

// Class定义 - 指向objc_class的指针 
typedef struct objc_class *Class;

// objc_object定义 - 根类定义
struct objc_object {
    Class _Nonnull isa __attribute__((deprecated));
};

// id的定义指向objc_object的指针
typedef struct objc_object *id;

// SEL 方法编号,方法选择器指针
typedef struct objc_selector *SEL;

解读代码:

  1. OC层面NSObject,在底层对应objc_object结构体
  2. 子类的isa均继承自NSObject,也就是来自objc_object结构体;
  3. Objective-CNSObject是大多数类的根类,而objc_object可以理解为就是c\c++层面的根类
  4. isa的类型为Class,被定义为指向objc_class的指针
  5. 在开发中可以用id来表示任意对象,根本原因就是id被定义为指向objc_object的指针,也就指向NSObject的指针
  6. SEL方法选择器指针,方法编号。

通过上面分析,可以得到一下结构关系:

未命名文件-2.png

4.set/get方法

static NSString * _I_JHSPerson_JHSName(JHSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JHSPerson$_JHSName)); }

static void _I_JHSPerson_setJHSName_(JHSPerson * self, SEL _cmd, NSString *JHSName) { (*(NSString **)((char *)self + OBJC_IVAR_$_JHSPerson$_JHSName)) = JHSName; }

通过以上代码可以发现,无论get方法还是set方法,都会有两个隐藏参数self_cmd,也就是方法接收者方法编号。在获取属性时,采用指针平移的方式,获取成员变量所在地址,转换后返回对应的数值。

  • objc_setProperty,在对实例变量进行设置时,会自动调用objc_setProperty方法。该方法可以理解为set方法的底层适配器,通过统一的封装,实现set方法的统一入口。

runtime源码中,搜索objc_setProperty,可以找到最终实现方法,见下段代码:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);
    
    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue); // retain新值
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
    
    // 释放旧值
    objc_release(oldValue);
}

本质是通过指针平移找到成员变量位置,然后进行新值的retain,旧值的release。

5.cpp内容补充

除了我们最关心的对象的定义外,在cpp文件中,还可一看到rorw协议分类方法等内容的定义。

  • 分类的定义。包括分类名称关联的类实例方法列表类方法列表等信息
struct _category_t {
   const char *name; // 名称
   struct _class_t *cls; // 关联的类
   const struct _method_list_t *instance_methods; // 实例方法
   const struct _method_list_t *class_methods; // 类方法
   const struct _protocol_list_t *protocols; // 协议
   const struct _prop_list_t *properties; // 属性
};

  • 方法或函数的定义。Method是一个objc_method结构体,包括方法编号selectortype encoding方法实现地址
struct _objc_method {
   struct objc_selector * _cmd; // 方法编号
   const char *method_type; // type encodings
   void  *_imp; // 方法实现地址
};

其中_cmd_imp比较熟悉,方法编号和方法实现,那么method_type是什么呢?在苹果开发者官网,可以找到对应的type encodings对照表

1.webp

如本例中JHSPerson的方法列表:

static struct /*_method_list_t*/ {

unsigned int entsize;  // sizeof(struct _objc_method)

unsigned int method_count;

struct _objc_method method_list[4];

} _OBJC_$_INSTANCE_METHODS_JHSPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {

sizeof(_objc_method),

4,

{{(struct objc_selector *)"JHSName", "@16@0:8", (void *)_I_JHSPerson_JHSName},

{(struct objc_selector *)"setJHSName:", "v24@0:8@16", (void *)_I_JHSPerson_setJHSName_},

{(struct objc_selector *)"JHSName", "@16@0:8", (void *)_I_JHSPerson_JHSName},

{(struct objc_selector *)"setJHSName:", "v24@0:8@16", (void *)_I_JHSPerson_setJHSName_}}

};


name属性get方法为例:@16@0:8

  • @:表示返回值,一个object对象,这里是NSString *类型;

  • 16:函数的传入的所有参数的字节数之和为16,因为有两个隐藏参数GFPerson * selfSEL _cmd,所以一共16个字节

  • @:第一个参数类型为id型GFPerson * self

  • 0:前面的参数起始的字节位置(从0开始)

  • ::第二个参数类型为selSEL _cmd

  • 8:前面的参数起始的字节位置(从8开始)

3.对象本质总结

通过工具clang,编译生成的cpp文件,我们可以发现,对象实质是一个结构体。在OC层,NSObject是大多数类的根类,而objc_object可以理解为就是c\c++层面的根类NSObject仅有一个实例变量Class isaClass实质上是指向objc_class的指针objc_class的定义见下面的代码:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() const {
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
    
    …… 省略
}

objc_class继承自objc_object,所以万物皆对象!

三.了解联合体和位域

1.位域

引入一个案例,定义一个结构体Car1,体现车的运动方向,见下面代码:

WeChate5b70e13f6943be496ea42c0d60cd705.png

这样看上去已经满足业务需求,但是这里有个问题,这个结构体需求占用4个字节32位,使用4个字节去体现一个单一功能有些浪费空间。理论上,一个字节就可以体现车的运动状态,改进一下,见下面:

WeChatab8710d576a68505a8bb45d2a5686b9d.png

这里使用了位域,用来体现一个功能,比如有值就是1,没有值就是用0.BOOL front: 1;表示front占用一位,这样体现一个车辆的状态只需要4位即可,这样整体需要一个字节即可满足要求!

2.结构体特点

同样看一例子:

WeChat8afd03888550e481c48ba2e44656cc64.png 上面的例子中定义一个结构体Perpson1char *name 占用8个字节,int age占用4个字节, double height占用8个字节,结合8字节对齐,该结构体共占用24个字节。同时,运行代码,给结构体赋值过程中,结构体中各个属性之间并无冲突,处于共存的状态。

结构体(struct)特点总结如下:

  • 优点:共存,有容乃大,全面;
  • 缺点:struct内存空间的分配是粗放的,不管用不用,全分配的。

3.联合体特点(又名共用体)

引入一例子如下:

WeChateef8bf88802581c5f31c35ef6ca3be16.png

上面的例子中定义一个联合体Perpson2char *name 占用8个字节,int age占用4个字节, double height占用8个字节,而这三个属性是互斥的,该联合体实际占用空间是8个字节。同时,运行代码,给联合体赋值过程中,联合体各个属性之间处于互斥的状态,并且联合体实际大小与最大属性值大小相等

联合体(union)特点总结如下:

  • 优点:内存使用更为精细灵活,也节省了内存空间;
  • 缺点:不够包容,各变量是互斥的

四. isa了解

1.isa_t联合体

上面例子中,了解到了联合体和结构体的区别,同时了解到位域在节省内存方面的优势。而isa就是采用联合体结合位域,对数据进行了封装,如下:

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    uintptr_t bits;
    Class cls;

#if defined(ISA_BITFIELD)
    struct {

        ISA_BITFIELD;  // defined in isa.h

    };
#endif
};

isa_t是一个联合体,其有两个属性Class clsuintptr_t bits,这两个属性是互斥的,该联合体占用8个字节内存空间。

  • Class cls;非nonpointer isa,没有对指针进行优化,直接指向类,typedef struct objc_class *Class;

  • uintptr_t bits;nonpointer isa,使用了结构体位域,针对arm64架构x86架构提供了不同的位域设置规则

#if SUPPORT_PACKED_ISA

// ios真机环境
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   define ISA_BITFIELD                                                      \
     uintptr_t nonpointer        : 1;                                       \
     uintptr_t has_assoc         : 1;                                       \
     uintptr_t has_cxx_dtor      : 1;                                       \
     uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
     uintptr_t magic             : 6;                                       \
     uintptr_t weakly_referenced : 1;                                       \
     uintptr_t deallocating      : 1;                                       \
     uintptr_t has_sidetable_rc  : 1;                                       \
     uintptr_t extra_rc          : 19
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

// mac、模拟器环境
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_BITFIELD                                                        \
     uintptr_t nonpointer        : 1;                                         \
     uintptr_t has_assoc         : 1;                                         \
     uintptr_t has_cxx_dtor      : 1;                                         \
     uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
     uintptr_t magic             : 6;                                         \
     uintptr_t weakly_referenced : 1;                                         \
     uintptr_t deallocating      : 1;                                         \
     uintptr_t has_sidetable_rc  : 1;                                         \
     uintptr_t extra_rc          : 8
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

# else
#   error unknown architecture for packed isa
# endif

// SUPPORT_PACKED_ISA
#endif


2.nonpointer isa各位含义

  • nonpointer:1位,表示是否对 isa 指针开启指针优化,0:纯isa指针1:不⽌是类对象地址isa中包含了类信息、对象的引⽤计数等。

  • has_assoc:1位,关联对象标志位,0没有1存在

  • has_cxx_dtor:1位,该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑;如果没有,则可以更快的释放对象。

  • shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤来存储类指针,在 x86 架构中有 44 位⽤来存储类指针。

  • magic:6位,⽤于调试器判断当前对象是真的对象还是没有初始化的空间。

  • weakly_referenced:1位,指对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放。

  • deallocating:1位,标志对象是否正在释放内存。

  • has_sidetable_rc:1位,当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位。

  • extra_rc:表示该对象的引⽤计数值,实际上是引⽤计数值减 1,例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,则需要使⽤到下⾯的 has_sidetable_rc

3.nonpointer isa初始化

在对象进行初始化过程中,_class_createInstanceFromZone中三个重要的初始化流程:

  1. cls->instanceSize,计算要开辟的内存大小,16字节对齐原则
  2. obj = (id)calloc(1, size);,内存空间开辟;
  3. obj->initInstanceIsaisa初始化过程。
  • 本篇重点学习nonpointer isa的初始化流程!

设置断点,运行程序,过滤出我们所需要研究的GFPerson类的初始化流程。见下图所示:

WeChat3093da8f896c76b3e7da88d8cf5d24bc.png

isa_t为联合体,初始化nonpointer isa,则cls属性为空,bits结构体会被初始化(互斥)8字节共64位,默认都为0。继续运行代码,bits赋值ISA_MAGIC_VALUE,赋值后,各位域的值见下图:

WeChat4e837d0a8937c4abee36fa719062a971.png

arm架构下: 第一位值为1,即对 isa 指针开启指针优化。从3743位,也就是magic赋值为26,非0,表示当前对象已被初始化。通过计算器可以验证,26的二进制就是0001 1010,如下图所示:

WeChat9e1df98367a0871cb0b624818e16f1bd.png

继续运行代码,将类的地址右移3位,赋值给shiftcls,见下图。为何要右移三位呢?因为shiftcls前面还有3位存储着nonpointerhas_assochas_cxx_dtor

isa初始化流程基本结束,我们可以通过创建jhs对象的isa是否指向当前对象的类JHSPerson

WeChat31cefa50de506073af844dc2c3b6c1e5.png

如图可知,jhs对象的isa右移三位(内存平移),左移20,最后右移17之后回到原来位置,成功指向当前对象的类JHSPerson

4.对象获取类

我们平时对象的类会直接调用class方法,那么class方法内部实现是怎样的?见源码如下:

- (Class)class {
   return object_getClass(self);
}

// getIsa()方法;
Class object_getClass(id obj)
{
   if (obj) return obj->getIsa();
   else return Nil;
}

// 当前不是taggedPointer,而是nonpointor isa, 直接返回ISA()
inline Class 
objc_object::getIsa() 
{
   if (fastpath(!isTaggedPointer())) return ISA();

   extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
   uintptr_t slot, ptr = (uintptr_t)this;
   Class cls;

   slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
   cls = objc_tag_classes[slot];
   if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
       slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
       cls = objc_tag_ext_classes[slot];
   }
   return cls;
}

// ISA——返回:return (Class)(isa.bits & ISA_MASK);
inline Class 
objc_object::ISA() 
{
   ASSERT(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
   if (isa.nonpointer) {
       uintptr_t slot = isa.indexcls;
       return classForIndex((unsigned)slot);
   }
   return (Class)isa.bits;
#else
   return (Class)(isa.bits & ISA_MASK);
#endif
}

通过解读上面的代码,发现获取对象的类,最终是实现代码是(Class)(isa.bits & ISA_MASK);,是通过对象isa & ISA_MASKISA_MASK是什么呢?见下图:

WeChat15d838979fd87868d9feafd049685591.png

在计算器中可以发现,该掩码低三位高17位全部是0,通过对象isa & ISA_MASK运算,会将对象isa低三位高17位全部抹零,等价于上面的右移3位左移20位,再右移17位操作流程。

ISA_MASK 也即是ISA的一个面具!验证一下:

WeChat1c236c955b0b32e3974dd490d2e61b35.png