OC底层原理初探之对象的本质(三)alloc探索下

682 阅读7分钟

对象的本质

探索方式:

由于Objective-C语言是C语言和C++的一种超集,那么可以通过轻量编译器Clang来对Objective-C代码进行rewrite,还原Objective-C代码的底层实现来进行探索。Clang可以将.m文件编译成.cpp文件。

Clang和xcrun

Clang:

Clang是一个由Apple主导的,用C++编写的,基于LLVM的,发布于BSD许可证下的CC++Objective-C的轻量级编译器,其目标(之一)就是超越GCC

xcrun

xcrun是一个Xcode的脚本指令集,其在Clang的基础上进行了封装,更加方便使用。

准备阶段:

.m文件编译成.cpp文件

Clang编译:

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

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

In file included from ViewController.m:9:
./ViewController.h:9:9: fatal error: 'UIKit/UIKit.h' file not found
#import <UIKit/UIKit.h>
        ^~~~~~~~~~~~~~~
1 error generated.

解决方法:

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

xcrun编译:

// 模拟器
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

分析阶段:

main.m文件里声明一个XJPerson的类:

image.png

打开终端,进入main.m所在文件夹,运行上面命令生成main.cpp,打开main.cpp搜索XJPerson开始探索,会发现很多值得关注的东西:

关注点一:objc_object

main.cpp文件里发现了XJPerson对象的内容,在.m文件中XJPerson继承自NSObject,但是在.cpp文件中却是objc_object

image.png image.png

结论一:针对.m文件的声明和.cpp文件的源码对比,可以得出结论,对象在底层的本质就是objc_object结构体。

关注点二:isa

XJPerson_IMPL结构体内看到了struct NSObject_IMPL NSObject_IVARS结构体,转而在.cpp文件中搜索NSObject_IMPL结构体(XJPerson_IMPL结构体内嵌套NSObject_IMPL结构体,就是结构体的伪继承)。

image.png

结论二:NSObject_IVARS就是成员变量isa

关注点三:Class

搜索objc_object时在.cpp文件中发现了

typedef struct objc_class *Class;

结论三:Class在底层的类型是objc_class *这样的结构体指针。

关注点四:id

typedef struct objc_object *id;

结论四:id在底层的类型是objc_object *,所以可以表示任意类型的对象。

image.png

关注点五:属性的gettersetter方法

image.png

getter方法图解:

image.png

setter方法图解:

image.png

结论五:在gettersetter方法中看到了两个参数XJPerson * self, SEL _cmd,在.m文件中自定义gettersetter方法对比可知这是隐藏参数,Objective-C创建的方法通常默认携带这两个参数,这就是在每一个方法里都可以使用self的原因;gettersetter方法是通过地址偏移的方式找到成员变量的位置进行存值、取值的。

结构体,联合体,位域

在探索isa之前先补充点结构体、联合体和位域的知识。

案例:

#import <Foundation/Foundation.h>

// 不指定位域
// 4字节 * 每字节8位 = 32位  0000 0000 0000 0000 0000 0000 0000 0000
// 但BOOL值只有0和1,这4个BOOL值只需要用1个字节的后4位就可以表示了 0000 1111
// 只需要1个字节,却用了4个字节,浪费了3个字节
struct XJCar1 {
    BOOL front; // 1字节
    BOOL back;
    BOOL left;
    BOOL right;
};

// 指定位域后,每个BOOL值只占1位,整个结构体只需占用1字节
struct XJCar2 {
    BOOL front: 1; // 指定位域为1bit,不是1字节
    BOOL back : 1;
    BOOL left : 1;
    BOOL right: 1;
};

// 结构体:共存,占用内存为所有成员变量内存之和,然后以结构体内存对齐原则对齐
struct XJTeacher1 {
    char        *name;      // 字符串,指针,8字节
    int         age;        // 4字节
    double      height;     // 8字节
}; // 8 + 4 + 8 = 20,再以8字节对齐,一共24字节

// 联合体:互斥,所有成员变量共用一块内存空间
union XJTeacher2 {
    char        *name;      // 字符串,指针,8字节
    int         age;        // 4字节
    double      height;     // 8字节
}; // 8字节


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        struct XJCar1 car1;
        struct XJCar2 car2;
        
        NSLog(@"---- car1 sizeof = %ld ---- car2 sizeof = %ld ----",sizeof(car1), sizeof(car2));
        
        struct XJTeacher1   teacher1;
        teacher1.name = "Cooci";
        NSLog(@"---- struct XJTeacher1 (name = %s, age = %d, height = %f)-----sizeof = %ld----", teacher1.name, teacher1.age, teacher1.height, sizeof(teacher1));
        teacher1.age  = 18;
        NSLog(@"---- struct XJTeacher1 (name = %s, age = %d, height = %f)-----sizeof = %ld----", teacher1.name, teacher1.age, teacher1.height, sizeof(teacher1));

        union XJTeacher2    teacher2;
        teacher2.name = "Cooci";
        NSLog(@"---- union XJTeacher2 (name = %s, age = %d, height = %f)-----sizeof = %ld----", teacher2.name, teacher2.age, teacher2.height, sizeof(teacher2));
        teacher2.age  = 18;
        NSLog(@"---- union XJTeacher2 (name = %s, age = %d, height = %f)-----sizeof = %ld----", teacher2.name, teacher2.age, teacher2.height, sizeof(teacher2));

    }
    return 0;
}

**********************   运行结果   ************************

2021-06-17 19:01:57.983162+0800 001-联合体位域[3350:202587] ---- car1 sizeof = 4 ---- car2 sizeof = 1 ----
2021-06-17 19:01:57.983463+0800 001-联合体位域[3350:202587] ---- struct XJTeacher1 (name = Cooci, age = 0, height = 0.000000)-----sizeof = 24----
2021-06-17 19:01:57.983489+0800 001-联合体位域[3350:202587] ---- struct XJTeacher1 (name = Cooci, age = 18, height = 0.000000)-----sizeof = 24----
2021-06-17 19:01:57.983502+0800 001-联合体位域[3350:202587] ---- union XJTeacher2 (name = Cooci, age = 15791, height = 0.000000)-----sizeof = 8----
2021-06-17 19:01:57.983535+0800 001-联合体位域[3350:202587] ---- union XJTeacher2 (name = , age = 18, height = 0.000000)-----sizeof = 8----

图解:

image.png

结论:

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

nonPointerIsa

前文OC底层原理初探之对象的本质(一)alloc探索上探索alloc底层实现时发现进入objc_object::initIsa函数后对象和类就绑定起来了,现在就来分析下isa

isa

image.png

从图中可以看出isa的类型是isa_tcontrol + command + 鼠标左键点击isa_t,观看其定义,发现isa_t是一个联合体(union)。

image.png

ISA_BITFIELD

查看宏定义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…

图解(arm64):

image.png

image.png

image.png

image.png

isa还原类信息

位运算还原:

上面已经分析了isa指针内存放的数据内容,现在就通过位运算来验证一下(M1芯片iMacarm64架构)。

案例:


#import <Foundation/Foundation.h>
#import "XJPerson.h"

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

**********************   位运算测试结果   ************************

(lldb) x/4gx p1
0x1009c2080: 0x000021a1000082c1 0x0000000000000000 // 格式化输出p1,拿到isa地址0x000021a1000082c1
0x1009c2090: 0x0000000000000000 0x0000000000000000
(lldb) p/x 0x000021a1000082c1 >> 3                 // 右移三位,得到新的地址
(long) $1 = 0x0000043420001058
(lldb) p/x 0x0000043420001058 << 31                // 将新的地址左移31位,得到新的地址
(long) $2 = 0x1000082c00000000
(lldb) p/x 0x1000082c00000000 >> 28                // 将新的地址右移28位,得到新的地址
(long) $3 = 0x00000001000082c0
(lldb) po 0x00000001000082c0                       // 输出新地址,得到XJPerson
XJPerson

图解:

image.png

测试所用电脑为M1芯片iMacarm64架构,生成的对象的ISA_BITFIELD第3位至第35位为XJPerson的信息,还原方式说明:

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

ISA_MASK还原:


#import <Foundation/Foundation.h>
#import "XJPerson.h"

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

**********************   位运算测试结果   ************************

(lldb) x/4gx p1
0x1009c2080: 0x000021a1000082c1 0x0000000000000000    // 格式化输出p1,拿到isa地址0x000021a1000082c1
0x1009c2090: 0x0000000000000000 0x0000000000000000
(lldb) p/x 0x000021a1000082c1 & 0x0000000ffffffff8ULL // isa & ISA_MASK(0x0000000ffffffff8ULL),得到新的地址
(long) $1 = 0x00000001000082c0
(lldb) po 0x00000001000082c0                          // 输出新地址,得到XJPerson
XJPerson

ISA_MASK还原说明:

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

备注:iOS属于小端模式,数据读写从右往左。

init

image.png

源码分析:

- (id)init {
    return _objc_rootInit(self);
}

id
_objc_rootInit(id obj)
{
    // In practice, it will be hard to rely on this function.
    // Many classes do not properly chain -init calls.
    return obj;
}

结论:通过源码可以看出来,init只是直接返回了对象本身而已,这是工厂设计模式的构造方法,用于提供接口给子类重写,便于扩展。

new

image.png

源码分析:

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

结论:通过源码可以看出,new方法就等于alloc + init。不建议直接使用new方法来初始化对象,因为没法使用initWithXXX这种构造方法。