对象的本质

OC是一门面向对象的语言,那什么是对象呢,今天我们一起探索一下对象的本质

认识clang

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

Clang是一个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器。

Clang是一个C++编写、基于LLVM、发布于LLVM BSD许可证下的C/C++/Objective-C/Objective-C++编译器。它与GNU C语言规范几乎完全兼容(当然,也有部分不兼容的内容,包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,比如C函数重载(通过__attribute__((overloadable))来修饰函数),其目标(之一)就是超越GCC。
clang官方文档

查看源码,探索类的本质

生成cpp文件

创建一个Person

@interface Person : NSObject

@end


@implementation Person

@end
复制代码

通过
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

然后我们就得到了一个cpp文件

查看cpp文件

搜索我们创建的Person文件

查看创建的Person

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

struct Person_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
};
复制代码

我们发现Person在底层是一个结构体,并且有一个元素

struct NSObject_IMPL NSObject_IVARS;
复制代码

这个是结构体的一个伪继承
这个NSObject_IMPL是什么呢,在这个cpp文件中搜索一下发现

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

总结:
定义了一个别名Person,该别名指向struct objc_object类型;

在结构体实现Person_IMPL中,有一个成员变量NSObject_IVARS,来自所继承的结构体,也就是isa
我们的Person继承了NSObject,在底层就是objc_object

疑问:?️为什么isa的类型是Class?

  • OC底层探索-alloc底层原理 源码分析文章中,提及过alloc方法的核心之一的initInstanceIsa方法,通过查看这个方法的源码实现,我们发现,在初始化isa指针时,是通过isa_t类型初始化的,
  • 而在NSObject定义中isa的类型是Class,其根本原因是由于isa 对外反馈的是类信息,为了让开发人员更加清晰明确,需要在isa返回时做了一个类型强制转换,类似于swift中的 as 的强转。

底层探索

typedef struct objc_class *Class;


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


typedef struct objc_object *id;



typedef struct objc_selector *SEL;
复制代码
  1. OC层面的NSObject,在底层对应objc_object结构体;
  2. 子类的isa均继承自NSObject,也就是来自objc_object结构体;
  3. Objective-CNSObject是大多数类的根类,而objc_object可以理解为就是c\c++层面的根类。
  4. isa的类型为Class,被定义为指向objc_class的指针。
  5. Class也是一个结构体的指针
  6. 在开发中可以用id来表示任意对象,根本原因就是id被定义为指向objc_object的指针,也就指向NSObject的指针。
  7. SEL方法选择器指针,方法编号。

get/set方法

将上面的Person类写个name属性,生成cpp文件在查看一下

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

static void _I_Person_setName_(Person * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _name), (id)name, 0, 1); }
复制代码

通过以上代码可以发现,无论是get方法还是set方法,都会有两个隐藏参数,self_cmd,也就是方法接收者和方法编号。在获取属性时,采用指针平移的方式,获取成员变量所在地址,转换后返回对应的数值。

objc_setProperty,在对实例变量进行设置时,会自动调用objc_setProperty方法。该方法可以理解为set方法的底层适配器,通过统一的封装,实现set方法的统一入口。

runtime源码中,搜索objc_setProperty,可以找到最终实现方法,见下段代码:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);
    
    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue); // retain新值
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
    
    // 释放旧值
    objc_release(oldValue);
}
复制代码

本质是通过指针平移找到成员变量位置,然后进行新值的retain,旧值的release。

总结

通过对objc_setProperty的底层源码探索,有以下几点说明:

  • objc_setProperty方法的目的适用于关联上层的set方法以及 底层的set方法,其本质就是一个接口

  • 这么设计的原因是,上层的set方法有很多,如果直接调用底层的set方法中,会产生很多的临时变量,当你想查找一个sel时,会非常麻烦

  • 基于上述原因,苹果采用了适配器设计模式(即将底层接口适配为客户端需要的接口),对外提供一个接口,供上层的set方法使用,对内调用底层的set方法,使其相互不受影响,即无论上层怎么变,下层都是不变的,或者下层的变化也无法影响上层,主要是达到上下层接口隔离的目的

对象的大小

空对象

16241044182139.jpg
空对象,大小为8字节,从上面可以看出空对象有个isa

使用class_getInstanceSize()需要导入头文件#import <objc/runtime.h>

添加属性

16241061401238.jpg

如果根据结构体的大小计算,应该是48个,打印出大小是40。

发现有内存优化,在一个8字节中存了两个字段。

16241069959154.jpg

添加成员变量

16241092305421.jpg

所占内存增加了

添加方法

16241094073785.jpg

所占内存没有变化

结论:
1.对象的大小跟成员变量和属性有关,跟方法无关,也就是对象的大小跟成员变量有关
2.在添加成员变量的过程中,由于成员变量的数据类型是不一致的,向最大数据类型的成员变量对齐。继承自NSObject对象的类,默认字节对齐方式是8字节。
3.底层虽然是结构体,但它的大小进行了优化

附上x /nuf 的说明

x /nuf

x以十六字节打印
—————
n表示要显示的内存单元的个数
—————
u表示一个地址单元的长度:
b表示单字节
h表示双字节
w表示四字节
g表示八字节
—————
f表示显示方式,可取如下值:
x按十六进制格式显示变量
d按十进制格式显示变量
u按十进制格式显示无符号整型
o按八进制格式显示变量
t按二进制格式显示变量
a按十六进制格式显示变量
i指令地址格式
c按字符格式显示变量
f按浮点数格式显示变量
事例:x/4gx
以十六进制打印,4个单元,一个单元长度是8字节,以16进制显示

对象本质

通过工具clang,编译生成的cpp文件,我们可以发现,对象实质是一个结构体。在OC层,NSObject是大多数类的根类,而objc_object可以理解为就是c\c++层面的根类。NSObject仅有一个实例变量Class isaClass实质上是指向objc_class的指针。

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

    Class getSuperclass() const {
#if __has_feature(ptrauth_calls)
#   if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
        if (superclass == Nil)
            return Nil;

#if SUPERCLASS_SIGNING_TREAT_UNSIGNED_AS_NIL
        void *stripped = ptrauth_strip((void *)superclass, ISA_SIGNING_KEY);
        if ((void *)superclass == stripped) {
            void *resigned = ptrauth_sign_unauthenticated(stripped, ISA_SIGNING_KEY, ptrauth_blend_discriminator(&superclass, ISA_SIGNING_DISCRIMINATOR_CLASS_SUPERCLASS));
            if ((void *)superclass != resigned)
                return Nil;
        }
#endif
            
        void *result = ptrauth_auth_data((void *)superclass, ISA_SIGNING_KEY, ptrauth_blend_discriminator(&superclass, ISA_SIGNING_DISCRIMINATOR_CLASS_SUPERCLASS));
        return (Class)result;

#   else
        return (Class)ptrauth_strip((void *)superclass, ISA_SIGNING_KEY);
#   endif
#else
        return superclass;
#endif
    }
复制代码

探索isa

位域

先看一个结构体

16241134816887.jpg

输出car size 4
结构体car里面的元素是BOOL类型的,只需要4位,现在却占了4个字节32位浪费了很多空间。

使用位域优化

16241135277299.jpg

输出:

car size 4 
car1 size 1
复制代码

BOOL front:1; 1表示占1位,0000 0000从后向前依次是frontbackleftright

联合体

先看一个结构体

struct Teacher {
    char name;
    int age;
    double height;
};
复制代码

给结构体赋值

1.声明

16241144609598.jpg
2.name赋值

16241145521468.jpg
3.age赋值

16241146647574.jpg
证明当前的结构体里的元素能够同时赋值

联合体(共用体)

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

联合体里面的内容与上面的结构体一模一样

给联合体赋值

  1. 声明
    16241150480610.jpg
  2. name赋值
    16241151446150.jpg
    在给name赋值以后,ageheight都有值,但是这是脏数据
  3. age赋值
    16241152259311.jpg
    在给age赋值以后,name的数据没有了,联合体是互斥的

isa就是联合体

我们看源码中,alloc一个对象的时候会将isa与类绑定,会使用initIsa()initIsa()里有一个对象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_t是一个联合体,有两属性Class clsuintptr_t bits,这两个属性时互斥的,该联合体占用8个字节内存空间。

Class cls,非nonpointer isa,没有对指针进行优化,直接指向类,

typedef struct objc_class *Class;
uintptr_t bits;
复制代码

nonpointer isa,使用了结构体位域,针对arm64架构和x86架构提供了不同的位域设置规则。

#if SUPPORT_PACKED_ISA

// ios真机环境
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   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 deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

// mac、模拟器环境
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   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 deallocating      : 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

// SUPPORT_PACKED_ISA
#endif
复制代码

针对两种不同平台,其isa的存储情况如图所示

16246787340500.jpg

isa各位含义

  • nonpointer:1位,表示是否对 isa 指针开启指针优化,
    • 0:纯isa指针,
    • 1:不⽌是类对象地址,isa中包含了类信息对象的引⽤计数等。
  • has_assoc:1位,关联对象标志位,
    • 0没有,
    • 1存在。
  • has_cxx_dtor:1位,该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑;如果没有,则可以更快的释放对象。
  • shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤来存储类指针,在 x86 架构中有 44 位⽤来存储类指针。
  • magic:6位,⽤于调试器判断当前对象是真的对象还是没有初始化的空间。
  • weakly_referenced:1位,指对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放。
  • deallocating:1位,标志对象是否正在释放内存。
  • has_sidetable_rc:1位,当对象引⽤计数⼤于 10 时,则需要借⽤该变量存储进位。
  • extra_rc:表示该对象的引⽤计数值,实际上是引⽤计数值减 1,例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,则需要使⽤到下⾯的 has_sidetable_rc

探索isa

通过alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone方法路径,查找到initInstanceIsa,并进入其原理实现
进入initIsa方法的源码实现,主要是初始化isa指针

16246792112960.jpg
该方法的逻辑主要分为两部分

  • 通过 cls初始化 isa
  • 通过 bits 初始化 isa

isa 与 类 的关联

clsisa 关联原理就是isa指针中的shiftcls位域中存储了类信息,其中initInstanceIsa的过程是将 calloc 指针 和当前的 类cls 关联起来

通过isa指针地址与ISA_MSAK 的值 & 来验证

创建一个macOS下的Command Line Tool项目

16241516737588.jpg
这里看到isa``64位存储情况

16241517336552.jpg
这里的isa怎么到Person.class这个地址呢
源码中

16241519579435.jpg
isa掩码

16241522567549.jpg
personisa & 掩码,得到的就是Person.class
我们从上面isa结构中可以看到x86_64环境下shiftcls44位从后往前放的,所以它是在[17 60]

通过位运算验证

16241561247469.jpg

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