阅读 785
iOS 底层原理探索 之 对象的本质  &  isa的底层实现

iOS 底层原理探索 之 对象的本质 & isa的底层实现

写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。
复制代码

目录如下:

  1. iOS 底层原理探索之 alloc
  2. iOS 底层原理探究 之 结构体内存对齐

以上内容的总结专栏


写在前面

之前我们分析了alloc底层流程和结构体的内存对齐原理。那么,今天我们来分析下对象的本质是什么。

我们都知道,OC语言是基于C和C++语言增加了一层面向对象,那么,我们就从OC的对象,在C和C++中底层的实现代码来开始今天的探索之路。

准备

  • 1、Clang

Clang是一个C语言、C++、Objective-C语言的轻量级编译器。源代码发布于BSD协议下。 Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。Clang是一个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器。 其常用命令如下:

把目标文件编译成c++文件 
clang -rewrite-objc main.m -o main.cpp 
复制代码

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 (手机)
复制代码
  • 2、union

联合体 我们通过一个例子来认识和了解 union 以及 unionstruct的区别

struct SMTeacher3 {
    char        *name;
    int         age;
    double      height ;
};

union SMTeacher4 {
    char        *name;
    int         age;
    double      height ;
};
复制代码

image.png

简单总结一下,

联合体 中各变量是“互斥的”,缺点是不够“包容”;优点是内存使用更为精细灵活,也节省来内存空间。 (通过打印的t4占用了8字节内存空间比结构体的24字节节省了3倍,随着第二次第三次打印 t4 的内容,可以看到每次赋值后,其他的属性就变成了脏数据,不再可读);

结构体 则是所有的变量都是“共存的”,优点是足够“包容”全面,缺点是内存的管理是粗放的,不管用不用全分配。

开始

首先,我们在新建的项目中 main.h 文件中,我们添加一个SMPerson对象 :

@interface SMPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) NSInteger height;
@property (nonatomic, copy) NSString *like;

@end

@implementation SMPerson

@end
复制代码

接着,我们通过命令行工具,将 main.h 文件编译为c++文件 main.cpp ,命令为:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

可以看到 在当前的项目路径下生成来一个 main.cpp 文件。直接找到我们的SMPerson 类在底层是一个结构体,实现如下 :

struct SMPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *_name;
	NSInteger _age;
	NSInteger _height;
	NSString *_like;
};
复制代码

多出来的这个 struct NSObject_IMPL NSObject_IVARS; 是什么呢? 没错他就是 isa,

typedef struct objc_class *Class;

struct NSObject_IMPL {
	Class isa;
};
复制代码

下面是SMPerson 属性的set、get方法的底层实现

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

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

static NSInteger _I_SMPerson_age(SMPerson * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_SMPerson$_age)); }
static void _I_SMPerson_setAge_(SMPerson * self, SEL _cmd, NSInteger age) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_SMPerson$_age)) = age; }

static NSInteger _I_SMPerson_height(SMPerson * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_SMPerson$_height)); }
static void _I_SMPerson_setHeight_(SMPerson * self, SEL _cmd, NSInteger height) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_SMPerson$_height)) = height; }

static NSString * _I_SMPerson_like(SMPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_SMPerson$_like)); }
static void _I_SMPerson_setLike_(SMPerson * self, SEL _cmd, NSString *like) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct SMPerson, _like), (id)like, 0, 1); }
复制代码

我们通过调试台将 p1 对象的内存信息打印出来,

WX20210614-085643@2x.png

由此可见,p1 对象在内存中,其属性是如下所示布局的:

image.png

所以,在set、get方法的底层实现中 OBJC_IVAR_$_SMPerson$_name 就是属性对应的内存地址的偏移量, 系统通过每个属性的偏移量,来实现对其赋值和取值。

对象的本质:

对象在底层就是一个结构体,其内部存储了类的实例变量。

对象的 NSObject_IMPL 继承自 NSObject 的 isa。

NSObject 只有一个成员变量 isa。

isa 的底层实现是什么呢?

iOS 底层原理探索之 alloc中我们探寻类 alloc 方法的底层流程。 其中在

    /// 将类和指针做绑定
    obj->initInstanceIsa(cls, hasCxxDtor);

复制代码

中,此过程包含了 isa 的初始过程,要探究 isa 的底层实现,我们就从 isa 的创建开始:

image.png

initIsa 内容如下:

image.png

我们可以看到 方法内创建了一个 isa_t 类型的 newisa 实例, 做了 赋值操作后,返回了 newisa。 那么,接着我们就来详细的看一下这个 isa_t 的底层实现。

isa_t 内容如下:

image.png

所以 isa_t 是一个联合体, 有两个成员变量一个是 bits, 还有一个 是 cls。我们知道 联合体 中各变量是互斥的, 它的优点是内存使用更为精细灵活。 所以,也就是说, isa_t 有两种初始化方式:

  • bits 被赋值, cls 没有值或者值被覆盖;
  • cls 被赋值, bits 没有值或者值被覆盖。

isa_t 中还有一个成员变量 是 结构体 ISA_BITFIELD, 这个宏定义对应 __arm64____x86_64__ 即 iOS 和 MacOS 两个端的实现。 ISA_BITFIELD 通过位域存储信息,具体存储信息如下:

# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     define ISA_MASK        0x007ffffffffffff8ULL
#     define ISA_MAGIC_MASK  0x0000000000000001ULL
#     define ISA_MAGIC_VALUE 0x0000000000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 0
#     define ISA_BITFIELD                                                      \
        uintptr_t nonpointer        : 1;                                       \
        uintptr_t has_assoc         : 1;                                       \
        uintptr_t weakly_referenced : 1;                                       \
        uintptr_t shiftcls_and_sig  : 52;                                      \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 8
#     define RC_ONE   (1ULL<<56)
#     define RC_HALF  (1ULL<<7)
#   else
#     define ISA_MASK        0x0000000ffffffff8ULL
#     define ISA_MAGIC_MASK  0x000003f000000001ULL
#     define ISA_MAGIC_VALUE 0x000001a000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 1
#     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 unused            : 1;                                       \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 19
#     define RC_ONE   (1ULL<<45)
#     define RC_HALF  (1ULL<<18)
#   endif

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_HAS_CXX_DTOR_BIT 1
#   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 unused            : 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
复制代码

为来更直观的理解上面 位域 ISA_BITFIELD存储的信息, 我们画个图来解析一下以上这段很长的代码。

arm64架构下的isa:

未命名文件.png

x86_64架构下的isa:

未命名文件 (1).png 各变量代表的意思是:

  • nonpointer:表示是否对 isa 指针开启指针优化 0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等。
  • has_assoc:关联对象标志位,0没有,1存在。
  • has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象。
  • shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
  • magic:用于调试器判断当前对象是真的对象还是没有初始化的空间。
  • weakly_referenced:对象是否被指向或者曾经指向一个 ARC 的弱变量,
  • 没有弱引用的对象可以更快释放。
  • deallocating:标志对象是否正在释放内存。
  • has_sidetable_rc:当对象引用技术大于 10 时,则需要借用该变量存储进位。
  • extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。

isa底层实现总结

  • isa 有两种类型 nonpointer类型和 非nonpointer类型,nonpointer类型包含来类信息、对象引用计数等数据; 非nonpointer类型只是一个纯指针。
  • isa使用 联合体位域 来实现。 采用这种方式 节省了大量的内存(由此可见,苹果在底层实现是做了很多的优化工作的);开发中,大量使用的对象都会有一个 isa 指针, 这么多的 isa 会占用大量的内存,联合体 中成员变量的互斥特性,节省了部分的内存空间。再加上 位域 的使用,更是在节省内存空间的同时,将对象类的信息等信息作了优化存储,这样,使得 isa 指针 所占用的内存空间得到最大化的使用。

扩展

# if __arm64__
#     define ISA_MASK        0x007ffffffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
复制代码

以上四行代码,是我取自 结构体 ISA_BITFIELD 中的内容。 这里的 ISA_MASK 是一个掩码。 我们知道 isa 的 8字节 - 64位存储空间中 在 arm64 也就是手机端 shiftcls 存储类指针的值 占用的是第4位开始33位长度的一段内存空间; 在 x86_64 也就是电脑端, shiftcls 存储类指针的值 占用的是第4位开始44位长度的一段内存空间。我们调试台打印出来一个对象的内存地址后,取第一位存储的isa地址&上对应的掩码后,就是对象的类指针的值。 下面,验证一下:

image.png

文章分类
iOS
文章标签