ios 对象的本质与isa

325 阅读8分钟

1.0 对象的本质

我们经常new或者alloc一个对象,那么底层到底是怎么创建这个对象的,这个对象在底层到底是什么呢?

先了解一下Clang编译器,后面我们需要编译源文件

  • Clang是一个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器
  • Clang 主要用于把源文件编译成底层文件,比如把main.m 文件编译成main.cpp、main.o或者可执行文件。便于观察底层的逻辑结构,便于我们探究底。
  • 终端编译命令: 1、 clang -rewrite-objc main.m -o main.cpp 2、clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m //UIKit报错的话就指定路径 3、xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp // xcrun命令基于clang基础上进行了封装更好用,模拟器编译 4、xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp //真机编译

1.0.1 实例探索

写个实例demo,main.m,用clang编译成main.cpp

@interface LGWPerson:NSObject{
    NSString* sex;
}
@property(nonatomic,copy)NSString* name;
@property(nonatomic,strong)NSString* nickname;
@property(nonatomic,assign)int age;
@end
@implementation LGWPerson
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
    return 0;
}

main.cpp部分重要代码如下:

typedef struct objc_object LGWPerson;
typedef struct {} _objc_exc_LGWPerson;
#endif
extern "C" unsigned long OBJC_IVAR_$_LGWPerson$_name;
extern "C" unsigned long OBJC_IVAR_$_LGWPerson$_nickname;
extern "C" unsigned long OBJC_IVAR_$_LGWPerson$_age;
typedef struct objc_object *id;
typedef struct objc_selector *SEL;
typedef struct objc_class *Class;

struct NSObject_IMPL {
  Class isa;
};

struct LGWPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *sex;
	int _age;
	NSString *_name;
	NSString *_nickname;
};


// @implementation LGWPerson

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

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

static NSString * _I_LGWPerson_nickname(LGWPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGWPerson$_nickname)); }
static void _I_LGWPerson_setNickname_(LGWPerson * self, SEL _cmd, NSString *nickname) { (*(NSString **)((char *)self + OBJC_IVAR_$_LGWPerson$_nickname)) = nickname; }

static int _I_LGWPerson_age(LGWPerson * self, SEL _cmd) { return (*(int *)((char *)self + OBJC_IVAR_$_LGWPerson$_age)); }
static void _I_LGWPerson_setAge_(LGWPerson * self, SEL _cmd, int age) { (*(int *)((char *)self + OBJC_IVAR_$_LGWPerson$_age)) = age; }
// @end

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
    }
    return 0;
}

分析底层源码总结如下:

  • LGWPerson对象底层是struct objc_object结构体,对象的本质是结构体的由来
  • LGWPerson对象有5个变量,其中NSObject_IVARS是一个指向NSObject的isa,由系统自动创建的变量
  • Class isa的本质是一个指向struct objc_class *的结构体指针
  • id是一个指向objc_object的结构体指针,这也是为什么id修饰变量不加*的原因
  • SEL是一个指向objc_selector的结构体指针,SEL _cmd 这样的写法就解释通了
  • LGWPerson对象的属性age、name、nickname,底层编译会生成加下划线的变量,并且会生成get、set方法,然后对象的成员变量sex,底层编译是只生成了一个变量,没有get、set方法
  • 仔细观察name和nickname的set方法,发现name的set方法是通过objc_setProperty方法设置属性值,而nickname的set方法是通过self+OBJC_IVAR_LGWPerson_LGWPerson_nickname(起始首地址+偏移量)的方法设置属性值。why?这两者为什么会有这个区别,后面的博客我会详细阐述。

2.0 isa

isa这个词应该并不陌生,上面通过实例我们也知道了对象的创建必然会生成isa。那么为什么要创建isa,isa的作用到底是什么?

前面一篇博客“对象的创建过程”详细分析了对象是如何创建的。现在再简单概述一下大致流程: alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone-->initisa 重点分析下initisa到底干了什么?

2.1 联合体union和结构体struct

在分析initisa之前我们看一下联合体和结构体的区别,这有助于我们分析initisa的源码。

union GyPerson {
    int a;      //4
    short b;    //2
    char c;     //1
};

int main(int argc, char * argv[]) {
    @autoreleasepool {
        union GyPerson  person;
        person.a = 8;
        NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
        person.b = 2;
        NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
        person.c = 'd';
        NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
        
        NSLog(@"%lu---%lu",sizeof(person),sizeof(union GyPerson));
        
    }
    return 0;
}

上面的联合体输出如下:

 a=8---b=8---c=
 a=2---b=2---c=
 a=100---b=100---c=d
 4---4

再来看一下结构体:

struct GyPerson2 {
    int a;      //4
    short b;    //2
    char c;     //1
};

int main(int argc, char * argv[]) {
  
    @autoreleasepool {
        struct GyPerson2  person;
        person.a = 8;
        NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
        person.b = 2;
        NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
        person.c = 'd';
        NSLog(@"a=%d---b=%d---c=%c",person.a,person.b,person.c);
        
        NSLog(@"%lu---%lu",sizeof(person),sizeof(struct GyPerson2));
        
    }
    return 0;
}

结构体输出如下

a=8---b=32766---c=�
a=8---b=2---c=�
a=8---b=2---c=d
8---8

根据上面的输出日志可以总结:

  • 联合体的内存大小是由最大成员的大小决定
  • 联合体中修改其中某个变量会覆盖其他变量的值,也就是互斥,只能存在一个。
  • 结构体的内存大小由结构体中成员变量决定,具体规则见上一篇博客
  • 结构体的成员变量相互独立,互不影响

两者的优缺点:

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

2.2 位域

有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如一个布尔值, 用一位二进位0或1表示即可,目的是节省存储空间。

struct Person1 {
    BOOL isTall;
    BOOL isHandsome;
    BOOL isRich;
};

struct Person2 {
    BOOL isTall: 1;
    BOOL isHandsome : 1;
    BOOL isRich : 1;
};

int main(int argc, char * argv[]) {
  
    @autoreleasepool {
    struct Person1 person1;
    struct Person2 person2;
    NSLog(@"内存大小:%lu----%lu",sizeof(person1),sizeof(person2));
    }
    return 0;
}

输出如下:

内存大小:3----1

总结:Person1占3个字节,变量isTall、isHandsome、isRich分别占一个字节。Person2占1个字节,isTall、isHandsome、isRich分别占一个二进制位,1个字节有8个二进制位。结构体位域好处就体现了出来,极大的节约了内存空间

2.3 initInstanceIsa

分析一下initInstanceIsa源码:

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); 
  
    isa_t newisa(0); // isa初始化

    if (!nonpointer) {
        newisa.setClass(cls, this);//如果是纯指针 isa被直接cls赋值
    } else { 
       ASSERT(!DisableNonpointerIsa);
       ASSERT(!cls->instancesRequireRawIsa());
#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else   
        newisa.bits = ISA_MAGIC_VALUE;
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
#endif
        newisa.extra_rc = 1;
    }
    isa = newisa;
}

分析:

  • 在arm64架构之前,isa就是一个纯指针,存储着class对象或meta-class对象的地址。在arm64架构之后,苹果用union结构优化了isa指针,让isa能够存储更多的信息,即联合体isa_t。
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_t:

  • 联合体isa_t有两个变量bitscls,联合体是互斥的,也就意味着要么bits被赋值,cls没有值或者被覆盖,要么cls被赋值,bits没有值或者被覆盖
  • 联合体位域ISA_BITFIELD,这是一个宏定义。定义了bits 64位各位域代表的含义,宏定义如下,我们只分析arm64以及x86这两个情况。
#arm_64
   define ISA_MASK        0x0000000ffffffff8ULL       //isa面具
#     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)

#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

各位域代表的含义 占位以x86_64为例:

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

总结:

  • isa_t 是联合体和位域的方式存储信息。联合体共用一块内存空间,目的是节省内存。
  • isa 分为nonpointer非nonpointer,非nonpointer只是一个纯指针,而nonpointer还包含了类信息、对象、引用计数等。
  • shiftcls位域段存储了isa的关联类信息

那么如何获取isa关联类的信息呢?也就是如何获取isa_t中shiftcls的值

2.4 isa关联类的获取

2.4.1 按位运算获取shiftcls

lldb断点调试LGPerson* p=[LGPerson alloc],在macos x86_64中

(lldb) x/4gx p  //注释:读取p对象内存
0x10070f660: 0x011d800100008519 0x0000000000000000
0x10070f670: 0x626d6f43534e5b2d 0x646e6957786f426f
(lldb) p/x 0x011d800100008519 >> 3
(long) $2 = 0x0023b000200010a3
(lldb) p/x 0x0023b000200010a3 << 20
(long) $3 = 0x000200010a300000
(lldb) p/x 0x000200010a300000 >> 17
(long) $4 = 0x0000000100008518
(lldb) po 0x0000000100008518
LGPerson

(lldb) p/x LGPerson.class
(Class) $6 = 0x0000000100008518 LGPerson
(lldb) 

分析:ios是小端模式,从右往左读。0x011d800100008519是isa,64位,我们要获取isa中第3位到第47位也就是shiftcls的值,就是把前3位抹0,后17位也抹0。0x011d800100008519右移3位清空前3位,再左移20位清空后17位,最后右移17位还原到原来的位置。

2.4.2 isa&ISA_MASK获取shiftcls

(lldb) x/4gx p  //注释:读取p对象内存
0x10070f660: 0x011d800100008519 0x0000000000000000
0x10070f670: 0x626d6f43534e5b2d 0x646e6957786f426f
(lldb) p/x 0x011d800100008519 & 0x00007ffffffffff8ULL
(unsigned long long) $8 = 0x0000000100008518
(lldb) po 0x0000000100008518
LGPerson   //注释:类对象

(lldb) x/4gx 0x0000000100008518
0x100008518: 0x00000001000084f0 0x000000010036a140
0x100008528: 0x0000000100362390 0x0000801000000000
(lldb) p/x 0x00000001000084f0 & 0x00007ffffffffff8ULL
(unsigned long long) $10 = 0x00000001000084f0
(lldb) po  0x00000001000084f0
LGPerson  //注释:元类

(lldb) x/4gx 0x00000001000084f0
0x1000084f0: 0x000000010036a0f0 0x000000010036a0f0
0x100008500: 0x0000000101353650 0x0004e03100000007
(lldb) p/x 0x000000010036a0f0 & 0x00007ffffffffff8ULL
(unsigned long long) $12 = 0x000000010036a0f0
(lldb) po 0x000000010036a0f0
NSObject   //注释:根元类

(lldb) 

分析:

  • ISA_MASK是一个宏。x86_64 的值等于 0x00007ffffffffff8ULL,arm64 的值等于0x0000000ffffffff8ULL,
  • 对象的isa&ISA_MASK得到关联类的信息。LGPerson实例对象isa关联类对象LGPerson,类对象LGPerson的isa关联元类对象LGPerson,元类对象isa关联根元类NSObject。
  • 对象isa的值0x011d800100008519不等于类的值0x0000000100008518,说明对象isa不仅存储关联类的信息,还存储着对象的引用计数、是否弱引用等信息。而类的isa0x00000001000084f0等于元类的值0x00000001000084f0,说明类的isa只存储着关联类的信息。 通过上面的分析我们可以理解苹果官方isa的关联图;

截屏2021-06-24 下午9.14.37.png