iOS底层原理之OC对象本质

一、涉及知识点

1.共用体(联合体)

定义

在进行某些算法的C语言编程的时候,需要使几种不同类型的变量存放到同一段内存单元中,也就是使用覆盖技术,几个变量互相覆盖,以达到节省空间的目的,这种几个不同的变量共同占用一段内存的结构叫共用体,又称联合体。

特点:

共用体和结构体有下列区别:
a.结构体的成员之间是共存的:各个成员占用不同的内存,它们互相之间没有影响。

b.联合体的成员之间是互斥的:所有成员共用同一段内存,修改一个成员的值,会影响其余所有成员。

c.结构体占用的内存:大于等于所有成员占用内存的总和(需要内存对齐)

d.联合体占用的内存:等于最大的成员占用的内存,同一时刻只能保存一个成员的值

声明
#pragma mark 共用体
union WJUnion {
    int     a;
    float   b;
    char    c;
};

2.位域

定义

C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称位域( bit field) 。利用位段能够用较少的位数存储数据。

特点:

a.位段成员的类型必须指定为unsigned或int类型;
b.若某一位段要从另一个字开始存放,用:0长度为0的空位段,作用就是使下一个位段从下一个存储单位(视不同编译系统而异)开始存放;
c.一个位段必须存储在同一存储单元中,不能跨两个单元;
d.可以定义无名字段例如":2";
e.位段的长度不能大于存储单元的长度,也不能定义位段数组;
f.位段可以用整形格式符输出;
g.位段可以在数值表达式中引用,它会被系统自动地转换成整形数;

声明
#pragma mark 结构体
struct WJCarStruct {
    BOOL front;
    BOOL back;
    BOOL left;
    BOOL right;
};

#pragma mark 位域
struct WJCarStructBit {
    BOOL front  : 1;
    BOOL back   : 1;
    BOOL left   : 1;
    BOOL right  : 1;
};

分析:
结构体WJCarStruct占4个字节,因为每个BOOL各占1个字节;
位域WJCarStructBit占1个字节,对比WJCarStruct结构体节省3个字节。

二、OC对象底层分析辅助工具(clang编译源码成底层代码)

1.clang定义

clang简而言之是一个C语言、C++、Objective-C语言的轻量级编译器,源代码发布于BSD协议下。Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字

2.在终端使用clang相关命令编译WJPerson.m生成WJPerson.cpp底层文件

2.1 使用clang命令

# clang -rewrite-objc WJPerson.m -o WJPerson.cpp

2.2 使用Xcode中的xcrun命令

# xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc WJPerson.m -o WJPerson.cpp
3.打开WJPerson.cpp并找到WJPerson类
typedef struct objc_object WJPerson;
typedef struct {} _objc_exc_WJPerson;

extern "C" unsigned long OBJC_IVAR_$_WJPerson$_name;
extern "C" unsigned long OBJC_IVAR_$_WJPerson$_nickName;
struct WJPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;  //就是isa指针
    NSString * _Nonnull _name;        //属性name对应的成员变量
    NSString * _Nonnull _nickName;    //属性nickName对应的成员变量
};
// @property (nonatomic, copy)NSString *name;
// @property (nonatomic, copy)NSString *nickName;
/* @end */

从编译之后的文件找到WJPerson_IMPL,可以看出WJPerson类实际上是一个结构体;由此可见,在OC中类的本质就是结构体(struct)。

3.1 来看结构体中的成员变量_name和_nickName,其实就是WJPerson头文件中声明的两个属性name和nickName所对应的。
@interface WJPerson : NSObject

@property (nonatomic, copy)NSString *name;
@property (nonatomic, copy)NSString *nickName;

@end
3.2 再来看结构体NSObject_IMPL是什么呢?

在WJPerson.cpp中搜索NSObject_IMPL,发现其实就是isa指针,

struct NSObject_IMPL {
    Class isa;
};

在OC中基本上所有的对象都是继承NSObject,但是真正的底层实现是objc_object的结构体类型

3.3 对象的本质拓展

在WJPerson.cpp文件中全局搜索*Class时,发现有这样几行代码如下:

typedef struct objc_class *Class;

struct objc_object {
    Class _Nonnull isa __attribute__((deprecated));
};

typedef struct objc_object *id;

typedef struct objc_selector *SEL;

源码分析:
常用的id原来是也是一个objc_object结构体指针别称,这就解释了id修饰变量和作为返回值的时候为什么不加*了,此外还有Class、SEL等也是结构体指针。

三、nonPointerIsa分析

对象alloc流程核心三步曲

a.instanceSize,计算开辟内存需要的大小。
b.calloc,向系统申请开辟内存,返回地址指针。
c.initInstanceIsa,初始化指针和类关联起来。

这里主要介绍initInstanceIsa流程,见objc源码对应处如下:
4761623767547_.pic_hd.jpg
inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

进入initIsa方法流程

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); 
    isa_t newisa(0);
    if (!nonpointer) {
        newisa.setClass(cls, this);
    } else {
      ......
    }
    isa = newisa;
}

进入isa_t,发现isa_t就是一个共用体,源码如下:

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_BITFIELD分析

在表现一个类的地址时,会出现一个词:nonPointerIsa。就比如一个类,也就可以作为一个指针,类上面是可以有很多内容是能够被存储的。类的指针是8字节,8字节 * 8 bit = 64 bit(64位)。那么如果只是用来存储一个指针,就会造成大大的浪费,因为每个类都有一个isa指针。苹果就对这个isa做了优化,就把和类息息相关的一些内容存在里面,比如:是否正在释放、引用计数、weak、关联对象、析构函数等等(所以,OC在底层,就是C++,像OC的释放,并不是真正的释放,而是其下层的C++释放,才是真正的释放),这些都和类先关,所以,可以把这些内容存储到那64位里面去。那么就出现了nonPointerIsa。nonPointerIsa也不是一个简单的地址。我们可以通过查看isa_t的位域,来了解里面存的是什么。

在arm64中:

# 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

字段说明如下:
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_rc为9。如果引⽤计数⼤于10,则需要使⽤到下⾯的has_sidetable_rc。

以arm64为例,堆中,8字节对齐排列,8字节 * 8 bit = 64 bit(64位)。
那么,nonpointer占[1]号位置,has_assoc占[2]号位置,has_cxx_dtor占[3]号位置,shiftcls占[4 ~ 36]号位置,magic占[37~42]号位置,weakly_referenced占[43]号位置,deallocating占[44]号位置,has_sidetable_rc占[45]号位置,extra_rc占[46 ~ 64]号位置。

总结:
1.OC中类的本质就是结构体。
2.ISA是通过共用体(联合体)互斥的特性,确定ISA是纯的ISA还是NONPOINTER_ISA,并通过位域来实现节约空间。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容