iOS底层学习——OC对象的本质与isa

1,496 阅读6分钟

我们已经学习了对象的初始化内存对齐等内容。这篇文章将深入学习探究对象的本质对isa进行分析

学习对象本质之前,先引入一个工具clang

一.clang

1.什么是clang

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

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

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

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

2.clang的作用

那么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

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

如果代码有使用runtime的内容,如weak,可以使用一下指令:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

二.探索对象的本质

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

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

image.png

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

image.png

执行指令后,即可编译生成一个main.cpp文件。

2.cpp文件解读

打开运行指令后编译生成的cpp文件,文件很长,从我们自定义的类GFPerson开始寻找!

1.GFPerson对象

全局搜索GFPerson,获得以下核心代码:

// GFPerson结构体声明
typedef struct objc_object GFPerson;
typedef struct {} _objc_exc_GFPerson;
#endif

// GFPerson_IMPL结构体实现
struct GFPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
        NSString *_name;
};

/* @end */
// @implementation GFPerson
// @end

解读代码:

  1. 定义了一个别名GFPerson,该别名指向struct objc_object类型
  2. 在结构体实现GFPerson_IMPL中,有一个成员变量NSObject_IVARS,来自所继承的结构体,也就是isa;另一个成员变量是_name,也就是GFPerson的属性,和OC层面定义是一致的。
2.NSObjec对象

根据NSObject_IMPL进行搜索,获取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方法选择器指针,方法编号。

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

image.png

4.get\set方法

GFPerson类的属性,自动添加get\set方法。见下面代码:

static NSString * _I_GFPerson_name(GFPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_GFPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_GFPerson_setName_(GFPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct GFPerson, _name), (id)name, 0, 1); }

通过以上代码可以发现,无论是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对照表。

iShot2021-06-15 11.23.47.png

如本例中GFPerson的方法列表:

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_GFPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	4,
	{{(struct objc_selector *)"name", "@16@0:8", (void *)_I_GFPerson_name},
	{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_GFPerson_setName_},
	{(struct objc_selector *)"name", "@16@0:8", (void *)_I_GFPerson_name},
	{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_GFPerson_setName_}}
};

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,所以万物皆对象!

三.联合体位域简述

在学习isa之前,先了解一下联合体以及位域。

1.位域

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

image.png

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

image.png

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

2.结构体特点

同样引入一个案例!见下图:

image.png

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

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

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

3.联合体特点

同样引入一个案例,来区分结构体和联合体的区别!见下图:

image.png

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

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

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

四.isa探索

1.isa_t联合体

通过上面的案例,认识到了联合体与结构体的区别,同时了解到位域在节省内存方面的优势。而isa,就是采用联合体结合位域,对数据进行了封装。见下面源码:

// isa 联合体
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls; 
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

isa_t是一个联合体,有两属性Class cls;uintptr_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_rc9。如果引⽤计数⼤于 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类的初始化流程。见下图所示:

image.png

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

image.png

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

image.png

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

isa初始化流程结束,我们可以通过创建的gf对象反推isa指向的是否为GFPerson类。见下图:

image.png

获取gf对象的内存结构,右移3位左移20位,再右移17位,获取类地址,成功指向GFPerson类

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是什么呢?见下图:

image.png

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

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

image.png