iOS底层 - 对象的本质

236 阅读7分钟

前面我们探索过NSObjectalloc方法及内存对齐的规则,我们本文主要探索OC对象的本质。

查找对象的定义

提到对象的本质,我们最先想到的就是看源码,那怎么定位源码中对象的定义的内容呢。感觉无从下手,那我们就先定义一个对象初始化一下,利用clang命令,将OC代码转换成c++代码找一下线索。我们先定义一个类JSPerson,然后初始化对象:

@interface JSPerson : NSObject

@property (nonatomic, strong) NSString *firstName;

@property (nonatomic, strong) NSString *lastName;
@end
   
@implementation JSPerson
@end
   
#import "JSPerson.h"
int main(int argc, const char * argv[]) {
  @autoreleasepool {
      JSPerson *person = [[JSPerson alloc] init];
      person.firstName  = @"Jason";
      person.lastName    = @"Test";
      NSLog(@"%@",person.firstName);
      NSLog(@"%@",person.firstName);
      }
   return 0;
 }

cdmain.m文件的目录,我们使用clang命令将main.m文件转换成c++代码: clang -rewrite-objc main.m -o main.cpp在目录里生成了main.cpp文件。我们在main.cpp文件中搜索JSPerson,搜查查找到了下面这段代码:

typedef struct objc_object JSPerson;
///省略代码
struct JSPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *_firstName;
	NSString *_lastName;
};

这个结构体JSPerson_IMPL里包含了我们定义的两个属性_firstName_lastName,说明对象的本质是objc-object类型的结构体,我们发现`

typedef struct objc_object JSPerson;
///省略代码
struct JSPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_firstName;
    NSString *_lastName;
};

这个结构体JSPerson_IMPL里包含了我们定义的两个属性_firstName_lastName,说明对象的本质是objc-object类型的结构体,我们发现`

typedef struct objc_object JSPerson;
///省略代码
struct JSPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_firstName;
    NSString *_lastName;
};

这个结构体JSPerson_IMPL里包含了我们定义的两个属性_firstName_lastName,说明对象的本质是objc-object类型的结构体,我们发现JSPerson_IMPL第一个属性是NSObject_IVARS是什么呢,它并不是我们定义的属性,我们搜索它的类型NSObject_IMPL发现:

struct NSObject_IMPL {
	Class isa;
};

显而易见NSObject_IVARS就是isa指针。我们接下来开始探索isa,在探索isa指针之前,我们先看一个概念位域

位域

我们先定义两个结构体JSCar1JSCar2,我们在main方法里打印一下两个结构体的大小

struct JSCar1 {
    BOOL front;
    BOOL back;
    BOOL left;
    BOOL right;
};
struct JSCar2 {
    BOOL front: 1;
    BOOL back : 1;
    BOOL left : 1;
    BOOL right: 1;
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        struct JSCar1 car1;
        struct JSCar2 car2;
        NSLog(@"%ld-%ld",sizeof(car1),sizeof(car2));
    }

发现打印的结果是

4-1

为什么结构体的属性个数和类型都相同,而结构体占用的内存的大小不一样呢,这里就是位域的作用,在结构体属性后面加上:1表示这个属性只占用一位(注意这里不是字节,一个字节是8位),所以car2的四个属性只占用了4位,但是内存分配最少是一个字节(8位),所以car2的内存大小是1个字节。示例图如下:

位域.png

可以看出使用位域我们可以节省内存(4B->1B)。

联合体

除了在结构体里使用位域,我们使用联合体也能达到节省作用:

union JSCar3 {
    BOOL front;
    BOOL back;
    BOOL left;
    BOOL right;
};
struct JSCar1 car1;
struct JSCar2 car2;
union JSCar3 car3;
NSLog(@"%ld-%ld-%ld",sizeof(car1),sizeof(car2),sizeof(car3));

我们定义一个联合体JSCar3,然后打印其大小:

4-1-1

可以看到car3联合体的内存大小也是1,我们很容易猜想car2car3的内存大小相同,那内存存储结构是相同的吗?带着这个疑问我们继续探索,在图中箭头位置使用lldb打印相应结构体:

截图.jpg

下面是打印的结果:

(lldb) p car2
(JSCar2) $0 = (front = NO, back = NO, left = NO, right = NO)
(lldb) p car2
(JSCar2) $1 = (front = 255, back = NO, left = NO, right = NO)
(lldb) p car2
(JSCar2) $2 = (front = 255, back = 255, left = NO, right = NO)
(lldb) p car3
(JSCar3) $3 = (front = NO, back = NO, left = NO, right = NO)
(lldb) p car3
(JSCar3) $4 = (front = YES, back = YES, left = YES, right = YES)
(lldb) p car3
(JSCar3) $5 = (front = YES, back = YES, left = YES, right = YES)

从打印结果我们发现,car2一个属性的赋值并不会影响其他属性,而car3一个属性的值变化了,其他属性的值也会变化,说明car2car3的内存存储结构是不一样的,结构体各成员变量是共存的,联合体各成员变量是互斥的,一般联合体位域配合使用。

联合体的内存规则如下

  • 联合体中可以定义多个成员,联合体的大小由最大的成员大小决定。
  • 联合体的成员公用一个内存,一次只能使用一个成员。
  • 对某一个成员赋值,会覆盖其他成员的值。

isa指针

有了上面知识的铺垫,我们继续研究isa指针。我们打开objc的源码,找到isa指针的定义:

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是一个联合体,它有一个位域成员ISA_BITFIELD,我们继续看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_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
#   else
#     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
        //省略代码
# elif __x86_64__
#   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

这些位代表的含义分别是:

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

可以看到x86平台下,isa指针的第4-48存储的是类的地址,下面我们通过位运算验证一下我们的理解是否正确,我们在main方法初始化JSPerson的实例:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        JSPerson *p = [JSPerson alloc];//打断点
        NSLog(@"%@",p);
    }
    return 0;
}

我们在注释位置打断点,使用lldb命令,打印p的地址:

(lldb) x/4gx p
0x1005186b0: 0x001d800100008309 0x0000000000000000
0x1005186c0: 0x63756f54534e5b2d 0x746e6f4372614268
(lldb) p/x 0x001d800100008309 >> 3 //isa指针地址 右移3位
(long) $2 = 0x0003b00020001061
(lldb) p/x 0x0003b00020001061 << 20//左移 20位(因为要补上一步右移的三位)
(long) $3 = 0x0002000106100000
(lldb) p/x 0x0002000106100000 >> 17//右移 17位
(long) $4 = 0x0000000100008308 // 通过位运算 shiftcls的值
(lldb) p/x JSPerson.class
(Class) $5 = 0x0000000100008308 JSPerson // 直接打印 JSPerson类的地址

通过上面的位运算,我们可以确定shiftcls存储的就是JSPerson类的地址。

在我们实际开发中我们定义的对象的isa基本都是nonpointer,好处不言而喻可以增加内存的利用率,减少内存浪费。对象的本质就先探索到这里,后续文章我们继续探索。