美文网首页
3.iOS底层学习之oc对象的本质

3.iOS底层学习之oc对象的本质

作者: 牛牛大王奥利给 | 来源:发表于2021-06-16 21:45 被阅读0次

如何探究对象的本质?

因为oc的底层是c和c++实现的,clang可以将oc还原为c或者c++的代码,所以通过clang可以看到一个对象的c或者c++的基层实现。

Clang的扩展,什么是Clang?

Clang: a C language family frontend for LLVM。Clang 项目为LLVM 项目的 C 语言家族(C、C++、Objective C/C++、OpenCL、CUDA 和 RenderScript)中的语言提供了语言前端和工具基础结构。提供了 GCC 兼容的编译器驱动程序和 MSVC 兼容的编译器驱动程序 。

Clang编译oc文件

模拟器版cpp文件生成命令:

xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
生成了main-arm64.cpp文件:
301623835223_.pic.jpg

查看文件的内容,我在main.m中声明和实现了NNPerson这个类,对应的main.cpp文件的代码如下:


WechatIMG33.jpeg

可以看到NNPerson生成了一个结构体:

struct NNPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _myAge;
};

其中,有一个NSObject_IMPL,还一个我在oc中定义的myAge。进一步查看NSObject_IMPL是什么,全局搜索可以看到如下代码块:

struct NSObject_IMPL {
    Class isa;
};

typedef struct objc_class *Class;
typedef struct objc_object *id;
typedef struct classref *classref_t;

由此可见,一个对象的本质是一个结构体,里面主要有一个结构体NSObject_IMPL,也就是isa,是一个objc_class指针。
在这里我们也看到了关于id的定义,就是一个objc_object指针,所以我们通常用id来声明对象的时,直接id声明就好,不用再加对象的星号了。

然后进一步查找objc_class可以看到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

通过这里可以看到objc_class继承自objc_object,我们点进去objc_object看到了objc_class的结构:

#if !OBJC_TYPES_DEFINED

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;

/// An opaque type that represents a category.
typedef struct objc_category *Category;

/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

#endif

看到这个,但是在现在的oc版本中这段代码已经废弃了,最前面有宏定义#if !OBJC_TYPES_DEFINED,然后查看这个OBJC_TYPES_DEFINED发现定义如下:

#ifdef _OBJC_OBJC_H_
#error include objc-private.h before other headers
#endif

#define OBJC_TYPES_DEFINED 1
#undef OBJC_OLD_DISPATCH_PROTOTYPES
#define OBJC_OLD_DISPATCH_PROTOTYPES 0

始终为1,取非的话就是0就不会走这个地方了。

再搜索看到可以看大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
  //此处省略下面的一堆方法等等等
}

objc_class继承自objc_object,所以我们来看objc2在使用的objc_object的结构,查找如下:

struct objc_object {
private:
    isa_t isa;
//此处省略下面的一堆定义 主要可以看到一个isa
}

isa为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_t是一个联合体,所以下面说下结构体和共用体的区别。

结构体和共用体

-结构体:结构体(Struct)是一种集合,它里面包含了多个变量或数组,它们的类型可以相同,也可以不同,每个这样的变量或数组都称为结构体的成员(Member)。定义格式如下:

struct 结构体名{
    结构体所包含的变量或数组
};

-共用体:共用体(Union)有时也被称为联合或者联合体。定义格式如下:

union 共用体名{
    成员列表
};

结构体和共用体的区别在于:结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。

举一个共用体的例子🌰:

union NNTestUnion{
    int a;
    char b;
    double c;
}testUnion;

 NSLog(@"占用的大小是:%lu",sizeof(testUnion));
 testUnion.a = 5;
 NSLog(@"a=%d,b=%c,c=%f",testUnion.a,testUnion.b,testUnion.c);
 testUnion.b = 'a';
 NSLog(@"a=%d,b=%c,c=%f",testUnion.a,testUnion.b,testUnion.c);
 testUnion.c = 8.0;
 NSLog(@"a=%d,b=%c,c=%f",testUnion.a,testUnion.b,testUnion.c);

打印如下:


351624008206_.pic.jpg

可以根据上面的结果看到,同一时刻只能保存一个成员的值。成员之间是互斥的

如果union的成员定义改成如下:

union NNTestUnion{
    int a;
    char b;
    float c;
}testUnion;

那么占用的大小第一个打印变成:
2021-06-18 17:23:57.350435+0800 KCObjcBuild[48834:10108220] 占用的大小是:4
所以共用体占用的内存等于最长的成员占用的内存。

位域

位域:有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可。例如开关只有通电和断电两种状态,用 0 和 1 表示足以,也就是用一个二进位。正是基于这种考虑,C语言又提供了一种叫做位域的数据结构

在结构体定义时,我们可以指定某个成员变量所占用的二进制位数(Bit),这就是位域。

举一个位域的例子🌰:

struct direction{
    bool up:1;
    bool down:1;
    bool left:1;
    bool right:1;
}direction1;

struct Direction{
    bool up;
    bool down;
    bool left;
    bool right;
}Direction1;

  NSLog(@"direction1占用的大小是:%lu",sizeof(direction1));
  NSLog(@"Direction1占用的大小是:%lu",sizeof(Direction1));

打印结果如下:


361624009262_.pic.jpg

就很清楚的可以看到结果,指定完毕之后就节省了很多内存

ISA_BITFIELD

在union isa_t 中有一个ISA_BITFIELD这个,点进去看一下这个的定义:

typedef unsigned long          uintptr_t;
# if __arm64__
#     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

# elif __x86_64__
#   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

可以看到ISA_BITFIELD是一个宏定义,所以那个isa_t中:

#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

可以替换为:

typedef unsigned long          uintptr_t;
#if defined(ISA_BITFIELD)
    struct {
          // defined in isa.h
      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;
    };

可以了解到,isa的定义,它里面定义了一个位域。并且可以看到,x86_64和arm64下的位域定义是不一样的,不过都是占满了所有的64位(1+1+1+33+6+1+1+1+19 = 64,x86_64同理),下面来说明一下每一个位域参数的含义:

-nonpointer:表示是否对isa开启指针优化 。0代表是纯isa指针,1代表除了地址外,还包含了类的一些信息、对象的引用计数等。
-has_assoc:关联对象标志位。
-has_cxx_dtor:该对象是否有C++或Objc的析构器,如果有析构函数,则需要做一些析构的逻辑处理,如果没有,则可以更快的释放对象。
-shiftcls:存在类指针的值,开启指针优化的情况下,arm64位中有33位来存储类的指针。
-magic:判断当前对象是真的对象还是一段没有初始化的空间。
-weakly_referenced:是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象释放的更快。
-unused:标志是否未被使用过。
-has_sidetable_rc:当对象引用计数大于10时,则需要进位。
-extra_rc:表示该对象的引用计数值,实际上是引用计数减一。例如:如果引用计数为10,那么extra_rc为9。如果引用计数大于10,则需要使用has_sidetable_rc。

( 说明:以上参数含义摘抄自网络资料,不是官方文档解释,我自己想查官方的解释没找到哇ε=(´ο`))) )*

类和isa是如何绑定的?

之前学习alloc的流程时,最后在方法_class_createInstanceFromZone中,做了三件事情,一个是计算所需要的空间大小,一个是开辟内存空间,另外一个是isa的的初始化,和相关类进行绑定。方法是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_BITFIELD各个位域的意义,可以了解到shiftcls是用来存放类指针的值,magic是用来判断这个类有没有被初始化完毕。
那么上面的代码可以看到,会先初始化一个isa_t,根据nonpointer,也就是是不是纯的ISA指针进行不同分支的初始化后续操作。
我这个走的是纯指针的流程:


371624250036_.pic.jpg

然后会来到setClass方法:

isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{
    // Match the conditional in isa.h.
#if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#   if ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_NONE
    // No signing, just use the raw pointer.
    uintptr_t signedCls = (uintptr_t)newCls;

#   elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ONLY_SWIFT
    // We're only signing Swift classes. Non-Swift classes just use
    // the raw pointer
    uintptr_t signedCls = (uintptr_t)newCls;
    if (newCls->isSwiftStable())
        signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));

#   elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ALL
    // We're signing everything
    uintptr_t signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));

#   else
#       error Unknown isa signing mode.
#   endif

    shiftcls_and_sig = signedCls >> 3;

#elif SUPPORT_INDEXED_ISA
    // Indexed isa only uses this method to set a raw pointer class.
    // Setting an indexed class is handled separately.
    cls = newCls;

#else // Nonpointer isa, no ptrauth
    shiftcls = (uintptr_t)newCls >> 3;
#endif
}

我这个进到setClass方法以后直接走的这句

 shiftcls = (uintptr_t)newCls >> 3;

shiftcls这个的值是newCls右移三位得到,我查看相关资料说这里是为了内存对齐,指针占8字节所以是八字节对齐。
此时我打印右移之前的和右移之后的内存地址如下:


381624250375_.pic.jpg

过掉断点走到这个地方:


391624250523_.pic_hd.jpg

此时打印下newisa为:


401624250583_.pic.jpg

根据显示的地址,此时isa中的cls已经关联上了类,shiftcls也保存上了相关的类信息。

(虽然我还是有点迷糊,再有新的理解会更新的,太难了哇😭)

相关文章

网友评论

      本文标题:3.iOS底层学习之oc对象的本质

      本文链接:https://www.haomeiwen.com/subject/oavzeltx.html