对象的本质&NonPointer_isa

458 阅读7分钟

前言

对象的本质是什么? 我们经常听说对象的本质就是结构体,哪怎么去验证呢?我们经常听说类里面有isa ,哪 NONPOINTER_ISA 又是什么呢?带着疑问,我们用编译器编译OC代码看看,编译完成后的代码是否能找到一些线索。

编译器介绍

Clang 编译器

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

Clang 的使用

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

如果是命令行工程则没有问题,顺利编译成功!

如果是iOS工程则会报错:

image.png

解决方案,就要指定SDK路径(iPhoneSimulator14.2.sdk 要修改为自己机器的对应版本)

clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.2 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk main2.m -o main2.cpp

也可以用Xcode自带的命令xcrun编译:

#模拟器
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main2.m -o main.cpp

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

编译前的代码和编译后的代码对比

编译前的代码

@interface WJPerson : NSObject

@property (nonatomic, strong) NSString *sName;
@property (nonatomic, copy) NSString *cName;

@end

@implementation WJPerson

@end

编译后的代码

//省略上面配置代码
#ifndef _REWRITER_typedef_WJPerson
#define _REWRITER_typedef_WJPerson
typedef struct objc_object WJPerson;
typedef struct {} _objc_exc_WJPerson;
#endif

extern "C" unsigned long OBJC_IVAR_$_WJPerson$_sName;
extern "C" unsigned long OBJC_IVAR_$_WJPerson$_cName;
struct WJPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *_sName;
	NSString *_cName;
};

// @property (nonatomic, strong) NSString *sName;
// @property (nonatomic, copy) NSString *cName;

/* @end */


// @implementation WJPerson


static NSString * _I_WJPerson_sName(WJPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_WJPerson$_sName)); }
static void _I_WJPerson_setSName_(WJPerson * self, SEL _cmd, NSString *sName) { (*(NSString **)((char *)self + OBJC_IVAR_$_WJPerson$_sName)) = sName; }

static NSString * _I_WJPerson_cName(WJPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_WJPerson$_cName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_WJPerson_setCName_(WJPerson * self, SEL _cmd, NSString *cName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct WJPerson, _cName), (id)cName, 0, 1); }

// @end

//省略...

分析过程

1、原本 WJPerson 继承 NSObject 变成了继承 objc_object

typedef struct objc_object WJPerson;

2、WJPerson 类变成了 WJPerson_IMPL 结构体,所以类的本质就是结构体, 申明的属性也帮忙我们自动生成了带下划线的成员变量

struct WJPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *_sName;
	NSString *_cName;
};
// @property (nonatomic, strong) NSString *sName;
// @property (nonatomic, copy) NSString *cName;

3、WJPerson_IMPL 中的成员变量 struct NSObject_IMPL NSObject_IVARS; 我们并没申明,是继承下来的成员变量。在编译完成的代码中可以找到结构体 objc_objectNSObject_IMPL 里面有一个成员变量是 Class isa

typedef struct objc_class *Class;

struct objc_object {
    Class _Nonnull isa __attribute__((deprecated));
};

struct NSObject_IMPL {
	Class isa;
};

4、WJPerson.m 我没有申明任何方法,但是编译完成后的代码多出4个方法,分别是 sNamecNamesettergetter 函数实现。仔细看会发现 sNamecName实现方法不同,sName是通过内存平移去读写的,而cName 是通过 objc_setProperty 是去实现的。sNamecName都是属性,只是修饰的关键字不一样,sNamestrong 修饰的, cNamecopy修饰的,所以我们可以得出修饰关键字会改变底层实现方式。

// @implementation WJPerson

static NSString * _I_WJPerson_sName(WJPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_WJPerson$_sName)); }
static void _I_WJPerson_setSName_(WJPerson * self, SEL _cmd, NSString *sName) { (*(NSString **)((char *)self + OBJC_IVAR_$_WJPerson$_sName)) = sName; }

static NSString * _I_WJPerson_cName(WJPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_WJPerson$_cName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_WJPerson_setCName_(WJPerson * self, SEL _cmd, NSString *cName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct WJPerson, _cName), (id)cName, 0, 1); }
// @end

NONPOINTER_ISA

探索 NONPOINTER_ISA 之前我们先来了解一下 结构体联合体位域

结构体、位域、联合体之间的比较

结构体

struct Car1 {
    BOOL front; 
    BOOL back;
    BOOL left;
    BOOL right;
};
struct Car1 car1;
NSLog(@"%ld",sizeof(car1));

上面输出car1的内存大小是 4个字节,如果有不清楚结构体的内存大小怎么算,可以看我之前写的探索结构体内存对齐

位域

// 互斥
struct Car2 {
    BOOL front: 1; //后面的 1 代表用一个位
    BOOL back : 1;
    BOOL left : 1;
    BOOL right: 1;
};
struct Car2 car2;
NSLog(@"%ld",sizeof(car2));

BOOL类型 不是 1 就是 0,所以用一个位就能满足需求,car2一共用了4个位,只就是半个字节,而最小单位是一个字节,所以上面输出car2的内存大小是 1个字节。比结构体省了3个字节

联合体

// 结构体
struct Person1 {
    char        *name;
    int         age;
    double      height ;
};

// 联合体 
union Person2 {
    char        *name;
    int         age;
    double      height ;
};
struct Person1  person1;
person1.name    = "person1";
person1.age     = 18;
person1.height  = 180;

union Person2   person2;
person2.name = "person2";
person2.age  = 18;
person2.height  = 180;
NSLog(@"%ld-%ld",sizeof(person1),sizeof(person2));
//打印了:24-8 

每次单步,person1 的属性都赋值成功了,且前面赋值的属性也没有变化

(lldb) p person1
(Person1) $0 = (name = 0x0000000000000000, age = 0, height = 0)
(lldb) p person1
(Person1) $1 = (name = "person1", age = 0, height = 0)
(lldb) p person1
(Person1) $2 = (name = "person1", age = 18, height = 0)
(lldb) p person1
(Person1) $3 = (name = "person1", age = 18, height = 180)
(lldb) 

每次单步,person2 的属性都赋值成功了,但是其他属性要么是脏数据、要么是空,只有当前赋值的属性是正常的值。

(lldb) p person2
(Person2) $4 = (name = 0x0000000000000000, age = 0, height = 0)
(lldb) p person2
(Person2) $5 = (name = "person2", age = 15952, height = 2.1220036723004548E-314)
(lldb) p person2
(Person2) $6 = (name = "", age = 18, height = 2.1219957998584539E-314)
(lldb) p person2
(Person2) $7 = (name = "", age = 0, height = 180)
(lldb) 

所以我们可以得出一个结论,

  • 联合体是互斥的,共用了一个内存地址,会影响到其他成员变量
  • 结构体是共存的,成员变量是各占各的内存,互补影响
  • 位域(bit field),C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,利用位域(bit field)能够用较少的位数存储数据,苹果就是利用位域和联合体去优化类的内存

NONPOINTER_ISA

ISA分为纯的ISANONPOINTER_ISANONPOINTER_ISA除了包含一个纯指针外,还包含了类的一些信息。

在前面iOS源码探索之alloc一文中,最终会通过obj->initIsa(cls)将从堆申请的结构体指针和当前的class绑定在一起。

inline void 
objc_object::initIsa(Class cls)
{
    initIsa(cls, false, false);
}
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); 
    
    isa_t newisa(0);
    //省略部分代码
 }


//共用体
union isa_t {
   //构造方法
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    //变量
    uintptr_t bits;

private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};

ISA_BITFIELDarm64x86_64架构下的定义

# 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__
//macOS
#   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
  • 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_rc9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc