OC-对象的本质

489 阅读5分钟

前言

什么是对象? 对象在底层的本质是什么? 这篇文章带我们了解对象的本质。

对象的本质

  • 对象的探索

    • 之前我们已经了解了可以通过 汇编 LDB 源码调试 的方式进行探索。今天我们将以另外一种方式进行探索, 它叫做 clang

      • 什么是 clang

         clang 是一个 C 语言、C++、Objective-C 语言的轻量级编译器。源代码发布于 BSD 协议下。
         clang 将支持其普通 lambda 表达式、返回类型的简化处理以及更好的处理 constexpr 关键字。
        
         clang 是一个由 Apple 主导编写, 基于 LLVM 的 C/C++/Objective-C 编译器
        
         20134月,clang 已经全面支持 C++11 标准, 并且开始实现 C++1y 特性 (也就是 C++14,这是 C++的下一个小更新版本)。clang 将支持其普通 lambda 表达式、返回类型的简化处理以及更好的处理 constexpr 关键字。
         clang 是一个 C++编写、基于 LLVM 发布于 LLVM BSD 许可证下的 C/C++/Objective-C/Objective-C++编译器。它与 GNU C语言规范几乎完全兼容 (当然,也有部分不兼容的内容,包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,比如 C 函数重载(通过__attribute__((overloadable))来修饰函数), 其目的(之一) 就是超越 GCC。
        
      • 我们先准备如下代码:

            #import <UIKit/UIKit.h>
            #import "AppDelegate.h"
        
            @interface TPerson : NSObject
            @property (nonatomic, strong) NSString * tName;
            @end
            
            @implementation TPerson
        
            @end
        
            int main(int argc, char * argv[]) {
                NSString * appDelegateClassName;
                @autoreleasepool {
                // Setup code that might create autoreleased objects goes here.
                    appDelegateClassName = NSStringFromClass([AppDelegate class]);
                }
                return UIApplicationMain(argc, argv, nil, appDelegateClassName);
            }
        
        
      • 在终端中使用以下命令,将main.m编译成main.cpp文件

          clang -rewrite-objc main.m -o main.cpp
        
      • 出现 UIKit 报错问题
        image.png

      • 解决方法 1:

           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
        

        这里需要注意的是 ios-13.0.0 iPhoneSimulator13.0.sdk 的版本号,需要改为自己 Xcode 所对应的版本号

      • 解决方法 2:

          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
        

        我们就得到了main.cpp 文件

        image.png 然后我们打开 main.cpp 文件,command+F 搜索 TPerson

        image.png

        这里我们就得到了一个结论: 对象在底层的本质就是结构体. 解决我们在文章开头出提出的对象在底层的本质是什么的疑问.

        我们发现这个地方有一个这个 struct NSObject_IMPL NSObject_IVARS;结构体. 那么这个结构体是什么呢?通过搜索如图:

        image.png

        得到一个结论: NSObject_IVARS 就是成员遍历 isa

        TPerson 是继承 NSObjc ,在 .cpp 文件中为什么是继承于 objc_object 呢?

        image.png

        得到一个结论: objc_object 就是 NSObject 的底层对象。

        同理类推我们发现 Class 在底层的类型是 objc_class *, 意味着 Class 是一个结构体指针,Class 只是一个别名而已。id 的类型是 objc_object *, 意味着 id 可以定义任何类型的变量。

        image.png

        我们通过查看 .cpp 文件发现 TPersongettersetter 方法

        image.png

联合体,位域

我们通过以下几个例子来了解 联合体位域

  1. 首先我们先来看下这个结构体占用多少个内存. 由于 BOOL 占位1个内存,所以可以得知这个结构体占用4个内存

image.png 2. 再看下面这个结构体,发现这个结构体只占用1个内存, : 就代表 位域, : 1 代表只占一个位置.

image.png

  1. 我们再看下下面这两段代码

image.png

image.png

发现第二段代码中,当给 tp1.name 赋值后, 再给 tp1.age 赋值时, tp1.name 的值被设置成了空字符串. 可以得出如下结论:

结构体(struct)中所有变量是"共存"的 -- 优点:"有容乃大",全面; 缺点: struct中内存空间分配是粗放的,不管是否使用,均分配内存空间。
联合体(union)中各变量是"互斥"的 -- 优点:内存使用更加精细灵活, 节省内存空间; 缺点:不够"包容"

initIsa

我们通过对象的本质得知,TPerson 对象的第一个隐藏变量就是 isa

接下来我们就一起探究一下 isa 的构造。

通过 alloc 流程分析: alloc -> _objc_rootAlloc -> callAlloc -> _objc_rootAllocWithZone -> _class_createInstanceFromZone -> 断点 obj->initInstanceIsa 和 obj->initIsa -> 运行发现进入了 obj->initIsa -> 进入 initIsa .

obj->initIsa 代码块

objc_object::initIsa(Class cls)
{
    initIsa(cls, false, false);
}

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 {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());


#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
#endif
        newisa.extra_rc = 1;
    }

    // This write must be performed in a single store in some cases
    // (for example when realizing a class because other threads
    // may simultaneously try to use the class).
    // fixme use atomics here to guarantee single-store and to
    // guarantee memory order w.r.t. the class index table
    // ...but not too atomic because we don't want to hurt instantiation
    isa = newisa;
}

通过对代码块的分析我们发现 isa_t 就是 isa 的构造方法.查看 isa_t 源码.

union isa_t {
    isa_t() { }   // isa_t 的构造方法
    isa_t(uintptr_t value) : bits(value) { }  // isa_t 的构造方法

    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源码分析可以得知, isa_t 是一个联合体(共用体),它拥有两个变量 bitscls. 根据上述联合体 变量 互斥 的特点, 变量 bitscls 也是 互斥 的. isa_t 还有一个 位域 成员变量 ISA_BITFIELD , ISA_BITFIELD 分别对应两个端 __x86_64__(macOS)__arm64__(iOS). 代码如下:

# 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

// SUPPORT_PACKED_ISA
#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_rc9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc

根据以上结论,我们发现在 __x86_64__(macOS)中, isa 指针的第 4 - 48 存储的是类的地址. 接下来我们一起通过运算验证一下如图:

image.png 代码如下:

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

(lldb) x/4gx tp
0x100563a20: 0x011d800100008309 0x0000000000000000
0x100563a30: 0x6c6c6f43534e5b2d 0x69566e6f69746365
(lldb) p/x 0x011d800100008309 >> 3
(long) $1 = 0x0023b00020001061
(lldb) p/x 0x0023b00020001061 << 20
(long) $2 = 0x0002000106100000
(lldb) p/x 0x0002000106100000 >> 17
(long) $3 = 0x0000000100008308
(lldb) p/x TPerson.class
(Class) $4 = 0x0000000100008308 TPerson
(lldb) 

经过以上运算,验证了 shiftcls 存储的就是 TPerson 类的地址。

总结

通过本文我们了解了 clang ,知道了 对象在底层的本质就是结构体 , 结构体共存 的, 联合体互斥 的.还了解了 initIsa 的构造.

Tips: 如若有理解错误或者不对的地方欢迎一起探索和交流!!!