iOS底层探索之对象的本质和isa的分析

247 阅读5分钟

因为OC的底层是C与C++编写的超集,那么学习OC底层对象本质就得知晓相关代码,所以先引入一个工具clang。通过clang能够将m文件编译成cpp文件,这样我们可以了解更多的关于底层的实现原理。

一.编译器Clang

1.什么是Clang

Clang是一个C语言、C++、Objective-C语言的轻量级编译器。源代码发布于BSD协议下。 Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。 2013年4月,Clang已经全面支持C++11标准,并开始实现C++1y特性(也就是C++14,这是 C++的下一个小更新版本)。 Clang是一个C++编写、基于LLVM、发布于LLVM BSD许可证下的C/C++/Objective-C/ Objective-C++编译器。它与GNU C语言规范几乎完全兼容(当然,也有部分不兼容的内容, 包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,比如C函数重载 (通过__attribute__((overloadable))来修饰函数),其目标(之一)就是超越GCC。

2.使用Clang

首先 clang -rewrite-objc main.m -o main.cpp 把目标文件编译成c++文件 让后这里会出现一个UIkit报错问题,这里我们需要通过Xcrun解决。

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

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

二.对象的本质

通过clang命令 获得编译后的main.m文件,main.m文件内容如下

#import <objc/runtime.h>

@interface XJPerson : NSObject
@property (nonatomic,strong) NSString *name;
@end

@implementation XJPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        NSLog(@"Hello, World!");
    }
    return 0;
}

编译后得到的mian.cpp文件,由于cpp文件又长又粗,我们只看其中需要的部分。

1.XJPerson对象

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

typedef struct objc_object XJPerson;
typedef struct {} _objc_exc_XJPerson;
#endif

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

/* @end */
// @implementation XJPerson
// @end

代码解读:

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

2.NSObjec对象

通过NSObject_IMPL进行全局搜索,得到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.对象属性底层分析

static NSString * _I_XJPerson_name(XJPerson * self, SEL _cmd)
{
    return (*(NSString **)((char *)self + OBJC_IVAR_$_XJPerson$_name));
}
//setter方法
static void _I_XJPerson_setName_(XJPerson * self, SEL _cmd, NSString *name)
{
    (*(NSString **)((char *)self + OBJC_IVAR_$_XJPerson$_name)) = name;
}

成员变量的内存访问本质上就是通过指针平移,先拿到对象首地址通过偏移,再计算出的地址来访问内存。最后进行新值的retain,旧值的release。

(char *)self:对象的首地址

OBJC_IVAR_$_XJPerson$_name:成员变量name的偏移地址

(*(NSString **)((char *)self + OBJC_IVAR_$_XJPerson$_name))成员变量name的内存

三.位域和联合体(共用体)

在分析isa之前,先得了解一下位域和联合体

1.位域

截屏2021-06-19 上午10.48.51.png

如上图两结构体:XJCar1结构体有4个BOOL成员,每个都需要1个字节,因此XJCar1占用4个字节。而XJCar2标明了每个成员所占bit位,4个bit位只要1个字节;XJCar1相对XJCar2产生了3倍的空间浪费。

截屏2021-06-19 上午11.10.21.png

2.联合体(共用体)

我们先通过代码案例来看看结构体和联合体的不同

截屏2021-06-19 上午11.24.44.png

截屏2021-06-19 上午11.23.45.png

可以通过代码看出;结构体共存输的三个成员都是有值的,而联合体输出的值只有age是正确的,之前被赋值的name却为空了,可以得出其互斥的特点,也就是最后一个值赋值之前的值清空,只能拥有一个成员被赋新值。而XJPlay2的height虽然没有赋值,却有一堆奇奇怪怪的数字,可以看出这是一段脏内存的数据。

结构体(struct)中所有变量是“共存”的——优点是“有容乃大”, 全面;缺点是struct内存空间的分配是粗放的,不管用不用,全分配。 联合体(union)中是各变量是“互斥”的——缺点就是不够“包容”; 但优点是内存使用更为精细灵活,也节省了内存空间

四.ISA探索

1.isa_t联合体

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

经过上面的分析,我们认识了位域在节省内存方面的优势,也了解了联合体的特性。下面探索isa会发现,其实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:表示是否对 isa 指针开启指针优化,0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等
  • has_assoc:关联对象标志位,0没有,1存在
  • has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
  • shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针
  • -magic:用于调试器判断当前对象是真的对象还是没有初始化的空间
  • weakly_referenced:志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放
  • unused:是否未被使用
  • has_sidetable_rc:散列表,当对象引用技术大于 10 时,则需要借用该变量存储进位
  • extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc

3.ISA的位运算

通过ISA的位运算拿到shiftcls。以macOS为例,shiftcls在64位结构中的位置:右边有3位,左边有17位。自己包含44位。 截屏2021-06-19 下午12.20.10.png 代码验证

WechatIMG219.jpeg 位运算得出来的值与框对象值相等,说明位运算成功拿到了shiftcls。

总结

对象在底层的本质就是结构体 ISA是通过共用体(联合体)互斥的特性,确定ISA是纯的ISA还是NONPOINTER_ISA,并通过位域来实现节约空间。