进厂工-对象的本质和isa探究

101 阅读12分钟

首先看下基本数据类型在系统中的内存,如下图

image.png

可以看到,longunsigned long 在32位处理器中分配4字节,64位中分配8字节,其余类型的大小都一样

对象的结构

1.对象类型大小、成员变量大小、malloc分配大小

创建NSObjectStudent(有_age、_no两个int类型的成员变量),HighSchoolStudent(有grade一个int类型的属性变量)继承Student对象

//sizeof 判断数据类型或者表达式长度的运算符,返回一个变量或者类型的大小(以字节为单位)
//class_getInstanceSize 返回类实例的大小
//malloc_size 系统分配的内存大小
//两者区别 二者的区别相当于一个盒子有多大空间和实际用了多少空间
//获取成员变量的大小已经是结构体内存之后的大小了

NSObject *obj = [[NSObject alloc]init];
printf("obj 类型大小 %zu\n", sizeof(obj));
printf("obj 成员变量的实际大小 %zu\n", class_getInstanceSize([NSObject class]));
printf("obj 分配的大小 %zu\n", malloc_size((__bridge const void *)(obj)));

Student *stu = [[Student alloc]init];
printf("stu 类型大小 %zu\n", sizeof(stu));
printf("stu 成员变量实际大小%zu\n",class_getInstanceSize([stu class]));
printf("stu 分配的大小 %zu\n",malloc_size((__bridge const void *)(stu)));

HighSchoolStudent *highSchoolStu = [[HighSchoolStudent alloc]init];
printf("highSchoolStu 类型大小 %zu\n", sizeof(highSchoolStu));
printf("highSchoolStu 成员变量实际大小 %zu\n",class_getInstanceSize([HighSchoolStudent class]));
printf("highSchoolStu 分配的大小 %zu\n",malloc_size(CFBridgingRetain(highSchoolStu)));

打印结果:

obj 类型大小 8   obj 成员变量的实际大小 8  obj 分配的大小 16
stu 类型大小 8   stu 成员变量实际大小16    stu 分配的大小 16
highSchoolStu 类型大小 8
highSchoolStu 成员变量实际大小 24
highSchoolStu 分配的大小 32
Program ended with exit code: 0

堆 对象的内存 采用16字节对齐,结构体内部 成员变量 8字节对齐, 对象与对象16字节对齐,避免野指针访问,容错。

2.通过clang的方式把以上代码编译成c++文件的结果:

image.png

我们在看下NSObject、Student、HighSchoolStudent对应的结构:

image.png image.png image.png

由上可以看出oc对象的本质是结构体,oc中基类是NSObject, 结构体中都包含一个 Class isa, 是一个objc实例是指向一个结构体的指针。实例对象在内存中存储的内容是 isa指针 + 其他成员变量。

之后在通过查看源码得知,

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

NSObject的实例对象本质是一个isa 指向了它的类的地址,类本质是一个结构体,结构体中包含一个isa指针。

struct objc_class 在objc-runtime-new 中查看

struct objc_class : objc_object {
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    ...
}

类对象其实是 objc_class类型的结构体,存放了isa指针,super指针,属性列表,类的对象方法信息,缓存等

在 oc环境 NSObject 是基类,创建 Class 类对象, alloc之后实例化对象 在objc 源码环境 主要是 c、c++、汇编 objc_object vs NSObject对应, objc_class vs Class 对应

[alloc init] 和 new 扩展

看下源码

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}

// Calls [[cls alloc] init].
id objc_alloc_init(Class cls)
{
  return [callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/) init];
}

// Replaced by CF (throws an NSException)
+ (id)init {
    return (id)self;
}

由此可以看出, [[cls alloc] init] = [cls new]

两者的区别,init 提供了初始化方法,工厂方法,构造函数,提供了接口,便于扩展。new 方法相对固定。

结构体内存对齐

上面打印出了对象成员变量的大小,为什么是这个值

内存对齐的原则

  • 数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,⽐如说是数组,结构体等)的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存储。 min(当前开始的位置m n) m = 9 n = 4 9 10 11 12

  • 结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储.(struct a⾥存有struct b,b⾥有char,int ,double等元素,那b应该从8的整数倍开始存储.)

  • 结构体收尾补齐:结构体的总⼤⼩,也就是sizeof的结果,.必须是其内部最⼤成员的整数倍.不⾜的要补⻬。

根据上面的对其原则, 举个例子

//结构体
struct LGStruct1 {
    double a;       // 8    [0 7]
    char b;         // 1    [8]
    int c;          // 4    (9 10 11 [12 13 14 15]
    short d;        // 2    [16 17] 向上补齐8的倍数->24
}struct1;

struct LGStruct2 {
    double a;       // 8    [0 7]
    int b;          // 4    [8 9 10 11]
    char c;         // 1    [12]
    short d;        // 2    (13 [14 15] 向上补齐8的倍数->16
}struct2;

struct LGStruct3 {
    double a; //8 [0, 7]
    int b; //4 [8, 9, 10, 11]
    char c; // 1 [12]
    short d; // 2 (13 [14, 15]
    int e; //4 [16, 17, 18, 19] //24
    struct LGStruct2 str; //16 24+16 = 40 向上补齐8的倍数->48
}struct3;

//sizeof 判断数据类型或者表达式长度的运算符,返回一个变量或者类型的大小(以字节为单位)
NSLog(@"struct1:sizeof--%zd", sizeof(struct1));
NSLog(@"struct2:sizeof--%zd", sizeof(struct2));
NSLog(@"struct3:sizeof--%zd", sizeof(struct3));
打印结果:
class-class-1[44571:9003986] struct1:sizeof--24
class-class-1[44571:9003986] struct2:sizeof--16
class-class-1[44571:9003986] struct3:sizeof--48

为什么要内存对齐

结构体成员为什么要从内部最大元素的整数倍地址开始存储?

我们知道计算机存储数据一般以字节(byte)形式进行存储,读取的时候却不是以一个字节方式进行读取,而是根据数据类型和处理器进行读取,一般以,1字节,2字节,4字节,8字节来存取内存,我们将上述这些存取单位称为 内存存取粒度。我们知道对于结构体成员变量来说,特殊组合,在底层做了内存优化,组合类型按照最大的元素去读。内存对齐是需要遵守的一套规则,通过适当增加内存大小,达到一次就能读取内存数据,以达到一点点空间换取时间,提高效率。

结构体位域

union LGTeacher1 t1;
t1.age = 18;
t1.name = "Andy";
printf("name = %s, age = %d\n", t1.name, t1.age);
printf("t1:sizeof--%zd\n", sizeof(t1));
struct LGTeacher2 t2;
t2.name = "Andy";
t2.age = 18;
printf("name = %s, age = %d\n", t2.name, t2.age);
printf("t2:sizeof--%zd\n", sizeof(t2));

通过结果打印可以看出,这里就不贴结果了,联合体union成员变量之间是互斥的,有我没他。

联合体和结构的区别:

  • struct内成员变量互不影响,优点就是可以存储所有成员变量,比较全面。缺点内存空间的分配是粗放的,不管是当否使用全部分配。
  • union 内成员变量互斥,优点内存使用比较灵活,节省内存空间。缺点就是不够包容。
布尔类型系统分配 1个字节(8byte)除了符号位其余都位都是空的,可以在空位置上存值
struct LGStruct4 {
    BOOL front: 1;
    BOOL back : 1;
    BOOL left : 1;
    BOOL right: 1;
}struct4;

nonpointer_isa

isa 在 arm64 开启指针优化的结构组成

__arm64__ 开启指针优化的情况,在 arm64架构中有33位用来存储指针地址

#     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; //表示是否对 isa 指针开启指针优,0代表是纯isa指针,1代表除了地址外,还包含了类的一些信息、对象的引用计数等化                                       \
        uintptr_t has_assoc         : 1;//关联对象标志位                                       \
        uintptr_t has_cxx_dtor      : 1;//该对象是否有C++或Objc的析构器,如果有析构函数,则需要做一些析构的逻辑处理,如果没有,则可以更快的释放对象                                       \
        uintptr_t shiftcls          : 33;//存在类指针的值,开启指针优化的情况下,arm64位中有33位来存储类的指针 /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
        uintptr_t magic             : 6;//判断当前对象是真的对象还是一段没有初始化的空间                                       \
        uintptr_t weakly_referenced : 1;//是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象释放的更快                                       \
        uintptr_t unused            : 1;//是否正在释放                                       \
        uintptr_t has_sidetable_rc  : 1;//当对象引用计数大于10时,则需要借用该变量依赖进位                                       \
        uintptr_t extra_rc          : 19//表示该对象的引用计数值,实际上是引用计数减一。例如:如果引用计数为10,那么extra_rc为9。如果引用计数大于10,则需要使用has_sidetable_rc
#     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
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

isa推导class类地址

//LGPerson *person = [[LGPerson alloc] init];

//断点 lldb 打印:
(lldb) x/6gx person      --> x/6gx 或者 p      打印指针地址
0x10160cbd0: 0x011d800100008199 0x0000000000000000
0x10160cbe0: 0x72726f43534e5b2d 0x65546e6f69746365
0x10160cbf0: 0x6d20776569567478 0x6e776f446573756f
(lldb) p/x 0x011d800100008199 & 0x0000000ffffffff8ULL  -->这里 isa & 掩码 得到 类对象的地址
(unsigned long long) $2 = 0x0000000100008198
(lldb) po 0x0000000100008198
LGPerson
(lldb) p/x LGPerson.class   --> --> p/x 类对象      打印类对象的地址
(Class) $4 = 0x0000000100008198 LGPerson
(lldb)

通过lldb打印我们可以分析,首先通过 [LGPerson class] 可以拿到 LGPerson 的类对象,每个实例对象都有一个 isa, 这个 isa 指向 类对象。其中我们看到通过 lldb 打印 LGPerson 类对象的地址 和 isa 的地址是不一样的?因为苹果做了一个掩码的操作,在源码 isa.h 文件中有一个 ISA_MASK 的宏定义(系统架构不一样,值也不一样,调试的时候根据运行环境选取不同的值),用( isa 的地址 &  ISA_MASK ),就可以得出 class 对象的内存地址。

其次还可以通过  isa 的位运算获取 calss 对象的内存地址。分析 isa_t 得知 ISA_BITFIELDshiftcls 是用来存储类指针的值,所以只要拿到 shiftcls,看看里面存放的值就可以知道类对象的内存地址。

objc_class : objc_object { 
     struct objc_class : objc_object {
     // Class ISA;    // 8字节
     Class superclass; // 8字节
     cache_t cache; // 16字节
     class_data_bits_t bits;
         }

通过objc_class结构分析,cache_t 只有两个变量,一个是联合共用体,一个是 explicit_atomic<uintptr_t>explicit_atomic<uintptr_t>占8字节,另外一个联合共用体也只占8个字节,加起来16字节,所以cache占16个字节。

求出 bits 的内存地址

isa的内存大小 + superclass的内存大小 + cache的内存大小
8 + 8 + 16 = 32  16字节打印 即0x20
p(class_data_bits_t *) LGPerson.class首地址 + 32个字节(0x20)

类对象、元类对象、根元类对象 的  isa走位流程 和 继承链

这里先通过一个例子来说明:

// 类对象、元类对象、根元类对象 的  isa走位流程 和 继承链
void InstanceIsaAndSuperclassDemo(void) {
    //类对象、元类对象、根元类对象 继承连

    //object_getClass :传入一个对象,返回这个对象的类对象
    //class_getSuperclass :传入一个类,返回这个类的父类
    //objc_getClass :传入一个类名称,返回对应的类对象
    //objc_getMetaClass :传入一个类名称,返回对应的元类对象

  
    //object_getClass 可以得到 类对象、元类对象、根元类对象
    //class_getSuperclass 可以得到 类的父类、元类的父类、根元类的父类
    //1、子类实例对象 stu  isa流程 和 继承链
    NSLog(@"子类(Student)打印");
    LGStudent *stu = [[LGStudent alloc] init];
    NSLog(@"传入一个对象,得到这个对象的类对象");
    //class对象
    Class class_stu = object_getClass(stu);
    //metaclass 对象
    Class metaclass_stu  = object_getClass(class_stu);
    //rootMetaclass 对象
    Class rootMetaclass_stu = object_getClass(metaclass_stu);
    NSLog(@"class_stu                 : %@ - %p",class_stu, class_stu);
    NSLog(@"metaclass_stu        : %@ - %p",metaclass_stu, metaclass_stu);
    NSLog(@"rootMetaclass_stu: %@ - %p",rootMetaclass_stu, rootMetaclass_stu);
    NSLog(@"传入一个类,得到这个类的父类");
    //class对象 的 superclass 对象
    Class superclass_stu = class_getSuperclass(class_stu);
    //metaclass 对象 的 superclass 对象
    Class superMetaclass_stu  = class_getSuperclass(metaclass_stu);
    //rootMetaclass 对象 的 superclass 对象
    Class superRootMetaclass_stu = class_getSuperclass(rootMetaclass_stu);
    NSLog(@"superclass_stu                   : %@ - %p", superclass_stu, superclass_stu);
    NSLog(@"superMetaclass_stu          : %@ - %p", superMetaclass_stu, superMetaclass_stu);
    NSLog(@"superRootMetaclass_stu : %@ - %p", superRootMetaclass_stu, superRootMetaclass_stu);
    NSLog(@"------------------");


    //2. 父类的instance对象isa流程和继承链。
    NSLog(@"父类(LGPerson)打印");
    LGPerson *person = [[LGPerson alloc] init];
    NSLog(@"传入一个对象,得到这个对象的类对象");
    // class对象
    Class class_person = object_getClass(person);
    // metaclass对象
    Class metaclass_person = object_getClass(class_person);
    // rootMetaclass对象
    Class rootMetaclass_person = object_getClass(metaclass_person);
    NSLog(@"class_person        : %@ - %p",class_person, class_person);
    NSLog(@"metaclass_person    : %@ - %p",metaclass_person, metaclass_person);
    NSLog(@"rootMetaclass_person: %@ - %p",rootMetaclass_person, rootMetaclass_person);
    NSLog(@"传入一个类,得到这个类的父类");
    // class对象的superclass对象
    Class superclass_person = class_getSuperclass(class_person);
    // metaclass对象的superclass对象
    Class superMetaclass_person = class_getSuperclass(metaclass_person);
    // rootMetaclass对象的superclass对象
    Class superRootMetaclass_person = class_getSuperclass(rootMetaclass_person);
    NSLog(@"superclass_person        : %@ - %p",superclass_person, superclass_person);
    NSLog(@"superMetaclass_person    : %@ - %p",superMetaclass_person, superMetaclass_person);
    NSLog(@"superRootMetaclass_person: %@ - %p",superRootMetaclass_person, superRootMetaclass_person);
    NSLog(@"------------------");


    //3. 根类的instance对象isa流程和继承链。
    NSLog(@"根类(NSObject)打印");
    NSObject *object = [[NSObject alloc] init];
    NSLog(@"传入一个对象,得到这个对象的类对象");
    // class对象
    Class class_object = object_getClass(object);
    // metaclass对象
    Class metaclass_object = object_getClass(class_object);

    // rootMetaclass对象
    Class rootMetaclass_object = object_getClass(metaclass_object);
    NSLog(@"class_object        : %@ - %p",class_object, class_object);
    NSLog(@"metaclass_object    : %@ - %p",metaclass_object, metaclass_object);
    NSLog(@"rootMetaclass_object: %@ - %p",rootMetaclass_object, rootMetaclass_object);
    NSLog(@"传入一个类,得到这个类的父类");

    // class对象的superclass对象
    Class superclass_object = class_getSuperclass(class_object);
    // metaclass对象的superclass对象
    Class superMetaclass_object = class_getSuperclass(metaclass_object);
    // rootMetaclass对象的superclass对象
    Class superRootMetaclass_object = class_getSuperclass(rootMetaclass_object);
    NSLog(@"superclass_object        : %@ - %p",superclass_object, superclass_object);
    NSLog(@"superMetaclass_object    : %@ - %p",superMetaclass_object, superMetaclass_object);
    NSLog(@"superRootMetaclass_object: %@ - %p",superRootMetaclass_object, superRootMetaclass_object);
    

由于结果太长这里就不贴具体的打印结果,大家有需要可以自己运行一下。

image.png

上面这张图是苹果官方给出的有关isa 和 superclass走位的图,结合上面InstanceIsaAndSuperclassDemo例子具体结果可知:

实例对象的 isa 指向类对象,类对象 isa 指向元类对象, 元类对象 isa 指向 根元类对象, 根元类对象指向自己本身。

子类的类对象的父类,是父类的类对象,父类的类对象的父类,是根类的类对象,根类的类对象为nil。

子类的元类对象的父类,是父类的元类对象,父类的元类对象的父类,是根类的元类对象,根类的元类对象的父类,是根类的类对象。

clang 用法扩展

Clang是⼀个C语⾔、C++、Objective-C语⾔的轻量级编译器。源代码发布于BSD协议下。

Clang是⼀个由Apple主导编写,基于LLVMC/C++/Objective-C编译器

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

UIKit报错问题

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

使用xcode打开,得到了编译好的代码,我们根据int main全局搜索得到main.m 编译成c++文件main.cpp,如下

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

        NSObject *object = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_vh_px38rsqn3tsdd6jwr43k073h0000gp_T_main_5eb878_mi_0, object);

    }
    return 0;
}


~