iOS底层原理 03:OC对象原理探索(下)

这是我参与更文挑战的第4天,活动详情查看: 更文挑战

对象的本质及拓展

什么是Clang

Clang是一个C语言C++Objective-C语言的轻量级编译器。源代码发布于BSD协议下。Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。Clang是一个有Apple主导编写,基于LLVMC/C++/Objective-C/Objective-C++编译器。它与GNU C语言规范几乎完全兼容,并在此基础上增加了额外的语法特性,比如C函数重载(通过__attribute__((overloadable))来修饰函数),其目标之一就是超越GCC

Clang命令

接下来,我们来使用几个Clang命令

0、准备工作

新建一个类Person如下:

@interface Person : NSObject
@property (nonatomic, copy) NSString *nickName;
@end

@implementation Person

@end
复制代码

1、clang命令

Person.m文件所在目录下执行命令

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

执行完毕之后,将会在目录下生成一个Person.cpp文件

2、复杂的clang命令

我们再次使用上述命令,生成ViewController.m文件的cpp文件,结果如下:

image.png

错误显示,在ViewController.m文件中引入了UIKit库,那么单纯的Clang命令已经无法满足要求,此时我们需要如下命令

clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.5 -isysroot/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ ViewController.m

来生成ViewController.m文件的cpp文件

3、xcrun命令

Xcode安装的时候顺带安装了xcrun命令,xcrun命令在clang的基础上进行了封装,更为好用

模拟器的xcrun命令

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

真机的xcrun命令

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

用这两种xcrun命令都可生成.m文件对应的cpp文件

cpp文件

我们来研究一下Person.m生成的cpp文件

对象在底层的本质

我们在cpp文件中查找nickName属性的位置,我们会定位到代码

#ifndef _REWRITER_typedef_Person
#define _REWRITER_typedef_Person
typedef struct objc_object Person;
typedef struct {} _objc_exc_Person;
#endif

extern "C" unsigned long OBJC_IVAR_$_Person$_nickName;
struct Person_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString * _Nonnull _nickName;
};
复制代码

我们可以发现类Person的本质是一个结构体struct,此处是一个struct的伪继承,在C++中,结构体是可以继承的。

OC层面,Person是继承自NSObject,而在下层, 他是一个objc_object类型的struct

我们继续搜索一下NSObject_IMPL,发现此处代码:

struct NSObject_IMPL {
	Class isa;
};
复制代码

我们发现,NSObject_IMPL就是Person父类NSObject中的isa

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
复制代码

我们继续查看Class在底层的实现:

typedef struct objc_class *Class;
复制代码

Class在底层是一个objc_class类型的结构体指针

typedef struct objc_object *id;
typedef struct objc_selector *SEL;
复制代码

我们常用的id,SEL等都是结构体指针

那么,nickNamegettersetter方法,在底层是什么情况呢:

// @implementation Person


static NSString * _Nonnull _I_Person_nickName(Person * self, SEL _cmd) { return (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_Person$_nickName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_Person_setNickName_(Person * self, SEL _cmd, NSString * _Nonnull nickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _nickName), (id)nickName, 0, 1); }
// @end
复制代码

我们发现gettersetter方法都多了两个参数self_cmd,这是方法的隐藏参数

联合体位域

结构体

我们看一个简单的结构体:

struct Car {
    BOOL front;
    BOOL back;
    BOOL left;
    BOOL right;
};

struct Car car1;
NSLog(@"1--->%lu", sizeof(car1));
复制代码

通过运行打印,我们发现这个结构体占用了4个字节的大小(每个BOOL占用一个字节)

0000 0000 0000 0000 0000 0000 0000 0000

在这样一个4字节,32位的空间内,我们仅仅只需要存放front,back,left,right四个方向值,极大的造成了空间的浪费,那么我们有没有办法优化空间呢,对我们来说4位大小的空间就已经能够满足我们四个方向的判断了,但是由于内存至少都是1字节的分配,那么我们有没有办法,让这个struct内存占用优化为1个字节呢?

接下来我们引入位域的概念

位域

我们将上述结构体进行改造如下:

struct Car {
    BOOL front: 1; // 规定front只占用1位 
    BOOL back: 1; // 规定back只占用1位
    BOOL left: 1; // 规定left只占用1位
    BOOL right: 1; // 规定right只占用1位
};
复制代码

接下来,我们再来打印一下:

image.png

我们已经成功将原有的结构体4字节的占用内存,优化为了1个字节

0000 0000

这就是位域

但是,车子的四个方向不可能同时是都为真,他们中永远都只可能有一个方向是真,那么有没有方法,让他们中只有一个方向有值,其他方向都没有值呢?

接下来我们引入联合体的概念

联合体

我们来看一个联合体

union Person {
    char *name;
    int age;
    double height;
};
复制代码

解析来,我们创建一个联合体,然后依次给成员赋值:

image.png

name赋值:

image.png

ageheight此时的内存是不被使用的,是脏内存,值也是脏数据

image.png

image.png

通过以上赋值流程,我们可以发现每一次给成员赋值,都会影响其他成员的值,这是因为,联合体的成员地址相同,占用了同一块内存,同一时间只有一个成员可被使用。

案例

我们来看一个案例:

Car.h文件

@interface Car : NSObject
@property (nonatomic, assign) BOOL front;
@property (nonatomic, assign) BOOL back;
@property (nonatomic, assign) BOOL left;
@property (nonatomic, assign) BOOL right;
@end
复制代码

Car.m文件

#define LGDirectionFrontMask    (1 << 0)
#define LGDirectionBackMask     (1 << 1)
#define LGDirectionLeftMask     (1 << 2)
#define LGDirectionRightMask    (1 << 3)

@interface Car (){
    // 联合体
    union {
        char bits;
        // 位域
        struct {
            char front  : 1;
            char back   : 1;
            char left   : 1;
            char right  : 1;
        };
    } _direction;
}
@end

@implementation Car
- (instancetype)init {
    self = [super init];
    if (self) {
        _direction.bits = 0b0000000000; // 二进制默认初始化
    }
    return self;
}

- (void)setFront:(BOOL)isFront {
    if (isFront) {
        _direction.bits |= LGDirectionFrontMask;
    } else {
        _direction.bits |= ~LGDirectionFrontMask;
    }
    NSLog(@"%s",__func__);
}

- (BOOL)isFront{
    return _direction.front;
}

- (void)setBack:(BOOL)isBack {
    _direction.back = isBack;

    NSLog(@"%s",__func__);
}

- (BOOL)isBack{
    return _direction.back;
}
复制代码

可以针对运行结果更好的理解联合体:

image.png

结构体和联合体总结

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

nonPointerIsa的分析

什么是nonPointerIsa?

我们经常使用的,它的对象指针占用8字节8字节等于64位,如果在这64位大小的空间之存放指针的话,那其实是很大的浪费,为了更好的使用内存空间,苹果把isa根据需要进行了区分,苹果提出了TaggedPointer和NonpointerIsa。对于小对象采用TaggedPointet方式来存放其值。对于占用内存比较大的对象采用NonpointerIsa来把isa按位使用,一部分用来存放实际的对象地址,一部分存放附加的其他信息。

nonPointerIsa的定义

还记得我们在研究objc的源码是,内存中的cls和我们的Person类是如何绑定的么?

通过_class_createInstanceFromZone方法中的obj->initIsa(cls)把内存中生成的clsPerson进行了绑定

我们来看一下initIsa的具体实现

image.png

重要的是isa_t,我们来看一下它的定义

image.png

isa_t是一个联合体,在联合体中有一个ISA_BITFIELD,这就是我们要找的nonPointerIsa

我们看一下它的定义

# 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;                                       \
        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;                                         \
      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)
复制代码

这里我们以__x86_64__架构来讲解:

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 的弱变量,没有弱引⽤的对象可以更快释放
  • unused标志对象是否正在释放内存
  • has_sidetable_rc当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位
  • extra_rc当表示该对象的引⽤计数值,实际上是引⽤计数值减 1,例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,则需要使⽤到上⾯的has_sidetable_rc

既然如此,那么我们就能够根据isa来推导出类Class

isa推导class

我们以上位中使用的Car为例,来推导

image.png

其中0x01000001005c55d9就是isa

接下来我们用isaISA_MASK做一下操作

image.png

isaISA_MASK(掩码)运算结果就是Car.class

isa的位运算

根据nonPointerIsa的定义,我们可以知道,在其64位空间内,类的指针shiftcls占用44位,右边有3位的数据,左边有17位的数据(小端模式,数据从右往左读)如下图:

image.png

那么我们可以经过位运算,将左右数据全部清空,最后只留下shiftcls44位数据留在原位。

右移3

image.png

左移20

image.png

右移17

image.png

最终我们清空了其他数据,只留下了类的指针shiftcls

那么如何验证呢,我们通过代码来验证一下:

image.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享