OC底层原理初探之对象的本质

206 阅读12分钟

对象的本质

探索方式:

Clang

Clang是一个C语言C++Objective-C语言的轻量级编译器源代码发布于BSD协议下,由Apple主导编写,基于 LLVMC/C++/Objective-C编译器

Clang编译:

clang -rewrite-objc main.m -o main.cpp

重写带有UIKit库的文件可能出现的问题:

image.png

解决方案:

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk  ViewController.m

xcrun编译

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

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

main.m文件 声明一个 XQPerson

image.png

打开终端,打开到 main.m所在文件夹运行上文中的指令,生成 main.cpp文件,打开此文件,搜索XQPerson,可以发现很多信息。

image.png

main.cpp文件里发现了XQPerson的内容,在.m文件中XQPerson继承自NSObject,但是在.cpp文件中却是objc_object。可以得出结论,对象的本质是objc_object结构体

观察XQPerson_IMPL结构体中嵌套了NSObject_IMPL结构体,这是对NSObject_IMPL结构体的伪继承,通过搜索NSObject_IMPL,发现如下图信息:

image.png

由此可知:NSObject_IVARS就是成员变量isa

继续搜索objc_object可以发现如下信息:

image.png

可以发现id在底层的定义为struct objc_object *类型,所以可以代表任意类型的对象。

成员变量和属性的区别

XQPerson_IMPL结构体中可以发现在XQPerson定义的属性,此时已经转换为成员变量_age_name_hobby,而属性的 setter,getter方法我们通过搜索相关属性名称可以找到如下代码:

image.png image.png

而搜索成员变量并不能找到相关的getter,setter方法,由此可以得出结论: 属性会自动生成成员变量同时实现了setter和getter方法

setter/getter解释 image.png

Type Encodings官方文档

截屏2022-01-27 下午7.14.26.png

getter/setter方法

image.png

我们可以发现对象的setter,getter方法是通过内存平移的方式找到成员变量的地址进行存值和取值的。

细心的我们可以发现,属性name和nickName的setter方法和hobby,age的是不一样的,调用的是objc_setProperty函数,而nickName的getter方法和其他属性都不一样,调用的是 objc_getProperty

通过 objc4-818.2源码里搜索objc_setProperty函数,发现objc_setProperty函数对copy修饰的属性进行了相关的copy处理。

image.png

image.png

通过 objc4-818.2源码里搜索此函数,发现objc_getProperty函数对copy修饰的属性进行了相关的加锁处理。

image.png

objc4-818.2源码里只有objc_setProperty函数的实现,却没有相关调用信息,通过llvm源码逆向查找确定了objc_setProperty函数的调用条件。

llvm源码逆向验证流程:objc_setProperty->getSetPropertyFn->GetPropertySetFunction->PropertyImplStrategy->IsCopy(判断)

objc_setProperty图解流程:

image.png image.png image.png image.png

objc_getProperty的逆向查找和objc_setProperty接近,这里就不在赘述了。

结论:对象属性的setter,getter方法通过地址偏移的方式找到成员变量进行存取值,当属性由copy修饰时,无论是nonatomic还是atomic,setter方法都会被重定向到objc_setProperty函数,而atomic修饰的属性,getter方法会被重定向到objc_getProperty函数。

结构体,联合体,位域

上文中提到,对象都有一个很关键的isa成员,在探索isa之前,需要补充一点关于,结构体,联合体,位域相关的知识。

案例:

此时XQCar1共占用4*8=32位,8字节
struct XQCar1 {
    BOOL front; // 1字节
    BOOL back;
    BOOL left;
    BOOL right;
};


// 指定位域后,XQCar2只需要使用1个字节,相比XQCar1节省了3个字节
struct XQCar2 {
    BOOL front: 1; //1位
    BOOL back : 1;
    BOOL left : 1;
    BOOL right: 1;
};
// 结构体:共存,占用内存为所有成员变量内存之和,然后以结构体内存对齐原则对齐
struct XQTeacher1 {
    char *name;
    int age;
};

// 联合体 : 互斥 所有成员变量共用一块内存空间
union XQTeacher2 {
    char *name;
    int  age;
};


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        struct XQCar1 car1;
        struct XQCar2 car2;
        NSLog(@"%ld-%ld",sizeof(car1),sizeof(car2));

        struct XQTeacher1   teacher1;
        teacher1.name = "NB";
        NSLog(@"name = %s , age = %d",teacher1.name,teacher1.age);
        teacher1.age  = 18;
        NSLog(@"name = %s , age = %d",teacher1.name,teacher1.age);
        
        union XQTeacher2    teacher2;
        teacher2.name = "NB";
        NSLog(@"name = %s , age = %d",teacher2.name,teacher2.age);
        teacher2.age  = 18;
        NSLog(@"name = %s , age = %d",teacher2.name,teacher2.age);
        
        NSLog(@"%ld-%ld",sizeof(teacher1),sizeof(teacher2));

    }

    return 0;

}


********************************输出结果********************************

2022-01-27 16:00:27.614838+0800 001-联合体位域[8631:278201] 4-1
2022-01-27 16:00:27.615297+0800 001-联合体位域[8631:278201] name = NB , age = 524304
2022-01-27 16:00:27.615349+0800 001-联合体位域[8631:278201] name = NB , age = 18
2022-01-27 16:00:27.615385+0800 001-联合体位域[8631:278201] name = NB , age = 18
2022-01-27 16:00:27.615465+0800 001-联合体位域[8631:278201] name = NB , age = 15990
2022-01-27 16:00:27.615513+0800 001-联合体位域[8631:278201] name =  , age = 18
2022-01-27 16:00:27.615549+0800 001-联合体位域[8631:278201] 16-8

结论:

  1. 结构体(struct)中所有变量是共存的,优点是“有容乃大”,全面;缺点是内存空间的分配是粗放的,不管是否使用,全部分配。
  2. 结构体(struct)可以通过为成员变量指定位域的方式来优化内存。
  3. 联合体(union)中各变量是互斥的,优点是内存使用更为精细灵活,节省了内存空间,缺点就是不够包容

接下来正式开始对 isa 的探索,在探索alloc方法时调用了initInstanceIsa函数初始化isa,继续前面的逻辑,跳转到isa的定义如下:

image.png

查看宏定义ISA_BITFIELD,探索isa指针内都存放了什么,分为两种架构模式。

# 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;  // 是否有C++析构函数                                      \
        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__
#   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;  // 是否有C++析构函数                                       \
      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

__has_feature(ptrauth_calls)介绍

  • __has_feature(ptrauth_calls)的作用是判断编译器是否支持指针身份验证功能。
  • __has_feature:此函数的功能是判断编译器是否支持某个功能。
  • ptrauth_calls:指针身份验证,针对arm64e架构;使用Apple A12或更高版本A系列处理器的设备(如iPhone XS、iPhone XS Max和iPhone XR或更新的设备)支持arm64e架构。
  • 参考链接developer.apple.com/documentati…

图解isa(以64位为例)

image.png image.png image.png image.png

isa还原类信息

以86_64为例

image.png

测试所用电脑为86_64架构,生成的对象的ISA_BITFIELD第3位至第46位为XQPerson的信息,还原方式说明:

  1. 通过x/2gx p1格式化输出p1对象的内存信息,首地址为isa指针0x011d8001000080e9
  2. isa的前3个bit位移除,即0x011d8001000080e9向右移3位,通过p/x得到新的指针0x0023b0002000101d
  3. isa指针的后17个bit位移除,由于刚刚向右移了3个bit位,那么现在就需要向左移31个bit位,即0x0023b0002000101d向左移17 + 3 = 20位,通过p/x得到新的指针0x0002000101d00000
  4. 最后将isa内代表XQPerson信息的17个bit位还原,即0x0002000101d00000向右移17个bit位,通过p/x得到新的指针0x00000001000080e8
  5. 最后po输出0x00000001000080e8,得到的结果为XQPerson

ISA_MASK还原:

image.png

ISA_MASK还原说明:

  1. 通过x/4gx p1格式化输出p1对象的内存信息,首地址为isa指针0x011d8001000080e9
  2. isa指针 &ISA_MASK,即0x011d8001000080e9 & 0x00007ffffffffff8,通过p/x得到新的指针0x00000001000080e8
  3. po输出0x00000001000080e8,得到的结果为XQPerson

isa掩码(ISA_MASK)介绍

现行源码版本一共分为三种架构:

x86_64: define ISA_MASK 0x00007ffffffffff8ULL // ULL: unsigned long long 无符号长整形
arm64: define ISA_MASK 0x0000000ffffffff8ULL 
arm64(simulators): define ISA_MASK 0x007ffffffffffff8ULL

isa指向分析

#import <Foundation/Foundation.h>

#import <objc/runtime.h>

@interface XQPerson : NSObject

@end

@implementation XQPerson

@end
//0x00007ffffffffff8ULL

void testClassNum(void){...}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        XQPerson* xq = [[XQPerson alloc]init];
        testClassNum();
    }
    return 0;
 }
********************在xq初始化后打断点lldb调试输出结果**********************

(lldb) x/4gx xq
0x10150cc40: 0x011d800100008101 0x0000000000000000
0x10150cc50: 0x0000000000000000 0x0000000000000000
(lldb) p/x 0x011d800100008101 & 0x00007ffffffffff8ULL
(unsigned long long) $1 = 0x0000000100008100
(lldb) po $1
XQPerson

(lldb) x/4gx $1
0x100008100: 0x00000001000080d8 0x00007ff85e149b88
0x100008110: 0x000000010150cc50 0x0001801000000003
(lldb) p/x 0x00000001000080d8 & 0x00007ffffffffff8ULL
(unsigned long long) $2 = 0x00000001000080d8
(lldb) po $2
XQPerson

由上面的输出可以看到,11和2地址不同,但是都输出了XQPerson,这是什么原因呢?难道类对象在运行时会和对象创建多份吗?

接下来,带着这个疑问,来验证一下:

void testClassNum(void){
    Class class1 = [XQPerson class];
    Class class2 = [XQPerson alloc].class;
    Class class3 = object_getClass([XQPerson alloc]);
    Class class4 = [XQPerson alloc].class;
    NSLog(@"\n%p-\n%p-\n%p-\n%p",class1,class2,class3,class4);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        XQPerson* xq = [[XQPerson alloc]init];
        testClassNum();
    }
    return 0;
 }

***********************输出结果*****************
0x100008100
0x100008100
0x100008100
0x100008100

可以看到,无论创建多少个对象,类始终只有一个,那么$2 = 0x00000001000080d8输出XQPerson是怎么回事呢?

下面用MachOView看看究竟

image.png

在MachOView里,我们可以看到除了_OBJC_CLASS_&_XQPerson外还有一个_OBJC_METACLASS_&_XQPerson,这就是 元类

到这里已经知道对象的isa->类对象,类对象的isa->元类(MetaClass),那么元类的isa指向哪里呢?接下来继续探索:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface XQPerson : NSObject

@end

@implementation XQPerson

@end

@interface XQStudent : XQPerson

@end

@implementation XQStudent

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        XQStudent* student = [[XQStudent alloc]init];
        NSLog(@"%@",student);
    }
    return 0;
}

*****************************lldb调试结果**************************

(lldb) x/4gx XQPerson.class                                //输出XQPerson类的内存信息
0x100008178: 0x0000000100008150 0x00007ff85e149b88
0x100008188: 0x00007ff81c7fc800 0x0000801000000000
(lldb) p/x 0x0000000100008150 & 0x00007ffffffffff8         //isa & ISA_MASK
(long) $1 = 0x0000000100008150
(lldb) po $1
XQPerson                                                   //po输出得到XQPerson元类

(lldb) x/4gx 0x0000000100008150                            //输出XQPerson元类的内存信息
0x100008150: 0x00007ff85e149b60 0x00007ff85e149b60
0x100008160: 0x0000000101416920 0x0002e03100000003
(lldb) p/x 0x00007ff85e149b60 & 0x00007ffffffffff8         //isa & ISA_MASK
(long) $2 = 0x00007ff85e149b60
(lldb) po $2
NSObject                                                  //此时输出的NSObject是类还是元类

(lldb) x/4gx 0x00007ff85e149b60
0x7ff85e149b60: 0x00007ff85e149b60 0x00007ff85e149b88
0x7ff85e149b70: 0x0000000101105e60 0x0003e03100000007
(lldb) p/x 0x00007ff85e149b60 & 0x00007ffffffffff8
(long) $3 = 0x00007ff85e149b60
(lldb) po $3                                              //$3与$2地址相同
NSObject

(lldb) x/4gx student                                      //输出student内存信息
0x101519a90: 0x011d8001000081c9 0x0000000000000000
0x101519aa0: 0x0000000000000000 0x0000000000000000
(lldb) p/x 0x011d8001000081c9 & 0x00007ffffffffff8         //isa & ISA_MASK
(long) $5 = 0x00000001000081c8
(lldb) po 0x00000001000081c8
XQStudent                                                  //po输出得到XQStudent类

(lldb) x/4gx $5                                            //输出XQStudent类的内存信息
0x1000081c8: 0x00000001000081a0 0x0000000100008178
0x1000081d8: 0x0000000101519aa0 0x0001801000000003
(lldb) p/x 0x00000001000081a0 & 0x00007ffffffffff8         //isa & ISA_MASK
(long) $7 = 0x00000001000081a0
(lldb) po 0x00000001000081a0
XQStudent                                                  //po输出得到XQStudent元类

(lldb) x/4gx $7                                            //输出XQStudent元类的内存信息
0x1000081a0: 0x00007ff85e149b60 0x0000000100008150
0x1000081b0: 0x0000000101035ec0 0x0002e03100000003

(lldb) p/x 0x00007ff85e149b60 & 0x00007ffffffffff8         //isa & ISA_MASK
(long) $9 = 0x00007ff85e149b60
(lldb) po $9                                              //$9,$3与$2地址相同
NSObject

(lldb) p/x NSObject.class                                 //16进制输出NSObject的类的
(Class) $10 = 0x00007ff85e149b88 NSObject
(lldb) x/4gx $10                                          // 得到NSObject类的内存信息
0x7ff85e149b88: 0x00007ff85e149b60 0x0000000000000000
0x7ff85e149b98: 0x000000010110ad70 0x0001801000000003
(lldb) p/x 0x00007ff85e149b60 & 0x00007ffffffffff8         //isa & ISA_MASK
(long) $12 = 0x00007ff85e149b60
(lldb) po $12                                              //¥12,$9,$3,$2地址相同,与¥10地址不同
NSObject


结论:

  1. 非根类NSObject类的实例对象的isa指向类(Class),类的isa指向元类(MetaClass),元类的isa指向根元类(RootMetaClass),而不是父类的元类。
  2. 根类NSObject类的实例对象的isa指向根类(RootClass),根类的isa指向根元类(RootMetaClass)。
  3. 根元类的isa还是指向根元类。

superclass指向分析

void superClass(void){

    // NSObject实例对象
    NSObject *object1 = [NSObject alloc];
    // NSObject类
    Class class = object_getClass(object1);
    // NSObject元类即为根元类
    Class metaClass = object_getClass(class);
    // NSObject根元类
    Class rootMetaClass = object_getClass(metaClass);

    NSLog(@"\n 实例对象%p\n类%p \n元类%p \n根元类%p",object1,class,metaClass,rootMetaClass);

    Class sMetaClass = object_getClass(XQStudent.class);
    Class sSuperClass = class_getSuperclass(tMetaClass);
    NSLog(@"XQStudent元类的父类%@ - %p",tSuperClass,sSuperClass);

    Class pMetaClass = object_getClass(XQPerson.class);
    Class psuperClass = class_getSuperclass(pMetaClass);
    NSLog(@"XQPerson元类的父类%@ - %p",psuperClass,psuperClass);

    Class nsuperClass = class_getSuperclass(NSObject.class);
    NSLog(@"NSObject的父类%@ - %p",nsuperClass,nsuperClass);
    
    Class rnsuperClass = class_getSuperclass(metaClass);
    NSLog(@"NSObject根元类的父类%@ - %p",rnsuperClass,rnsuperClass);
}

********************************lldb输出********************************

2022-01-28 14:30:36.468562+0800 isa还原类信息[2659:114708]
实例对象0x101212f10
类0x7ff85e149b88
元类0x7ff85e149b60
根元类0x7ff85e149b60
2022-01-28 14:30:36.469073+0800 isa还原类信息[2659:114708] XQStudent元类的父类XQPerson - 0x100008178

2022-01-28 14:30:36.469148+0800 isa还原类信息[2659:114708] XQPerson元类的父类NSObject - 0x7ff85e149b60
2022-01-28 14:30:36.469186+0800 isa还原类信息[2659:114708] NSObject的父类(null) - 0x0

2022-01-28 14:30:36.469216+0800 isa还原类信息[2659:114708] NSObject根元类的父类NSObject - 0x7ff85e149b88

结论:

  1. 元类之间也存在继承链,与类一样。
  2. 根元类的父类是根类。
  3. 根类的父类是nil。

isa走位图与类的继承链图解

isa流程图.png

接下来看一个比较经典的面试题isKindOfClassisMemberOfClass

示例:

void kindofDemo(void){

    BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];   
    BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
    BOOL re3 = [(id)[XQPerson class] isKindOfClass:[XQPerson class]];
    BOOL re4 = [(id)[XQPerson class] isMemberOfClass:[XQPerson class]];
    NSLog(@"\n re1 :%d\n re2 :%d\n re3 :%d\n re4 :%d\n",re1,re2,re3,re4);

    BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
    BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
    BOOL re7 = [(id)[XQPerson alloc] isKindOfClass:[XQPerson class]];
    BOOL re8 = [(id)[XQPerson alloc] isMemberOfClass:[XQPerson class]]; 
    NSLog(@"\n re5 :%d\n re6 :%d\n re7 :%d\n re8 :%d\n",re5,re6,re7,re8);
}

****************************输出结果********************************

2022-01-28 19:14:54.529647+0800 isa还原类信息[4054:198852]
re1 :1
re2 :0
re3 :0
re4 :0
2022-01-28 19:14:54.530123+0800 isa还原类信息[4054:198852]
re5 :1
re6 :1
re7 :1
re8 :1

分析: 在isKindOfClass源码处打上断点,发现并不会执行到,此时我们打开汇编调试,会断到如下图代码:

image.png

由此可得出,isKindOfClass方法被重定向到了objc_opt_isKindOfClass函数。 找到objc_opt_isKindOfClass的源码如下:

image.png

可以看到,在OBJC2优先执行上面的代码。

接下来看看isMemberOfClass的源码:

image.png

接下来开始分析:

  1. 使用NSObject(Class)的元类即根元类NSObject(Root Meta Class)NSObject(Class)进行比较,不相等,第二次取根元类的父类即NSObject(Class)NSObject(Class)比较,相等,返回YES。

  2. 使用NSObject(Class)的元类即根元类NSObject(Root Meta Class)与NSObject类进行比较,不相等。

  3. 使用XQPerson类的元类XQPerson(Meta Class),元类的父类NSObject(Root Meta Class),NSObject(Root Meta Class的父类NSObject(Class)依次与XQPerson(CLASS)进行比较均不相等,第四次取NSObject(Class)的父类位nil退出循环。

  4. 使用XQPerson(Class)的元类XQPerson(Meta Class)XQPerson(Class)进行比较,不相等。

  5. 使用NSObject的对象的isaNSObject(Class)NSObject(Class)进行比较,相等,返回YES.

  6. 使用使用NSObject的对象的 classNSObject(Class)NSObject(Class)进行比较,相等,返回YES.

  7. 使用XQPerson的对象的isaXQPerson(Class)XQPerson(Class)进行比较,相等,返回YES.

  8. 使用使用XQPerson的对象的 classXQPerson(Class)XQPerson(Class)进行比较,相等,返回YES.

这个面试题很好的考察了对isa走向和继承链的关系。