2021-06-12

iOS 底层原理之alloc 探究(三)对象本质与神秘的isa

一,准备工作

1.了解Clang编译器

Clang是⼀个C、C++、Objective-C语⾔的轻量级编译器。源代码发布于BSD协议下。
Clang是⼀个C++编写由Apple主导,基于LLVM的C/C++/Objective-C编译器2013年4⽉,
Clang已经全⾯⽀持C++11标准,并开始实现C++1y特性(也就是C++14,这是C++的下⼀个⼩更新版本)。
Clang将⽀持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。
Objective-C++编译器。它与GNU C语⾔规范⼏乎完全兼容(当然,也有部分不兼容的内容,
包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,⽐如C函数重载(通过

2.将oc⽂件编译成c++⽂件
1)直接使用clang命令
clang -rewrite-objc main.m -o main.cpp
在使用上面的命令可能会报错需要修改 xcode版本,以及模拟器版本,和最后的文件名称
2021-06-12

2)使用xcrun命令(clang基础上的封装)
xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m -o main-x86_64.cpp (模拟器)
3)
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o mainarm64.cpp(真机)

二,分析编译后的c++文件

随便在.m文件中创建一个类

@interface Goods : NSObject
@property (nonatomic, strong) NSString *good;

@end

@implementation Goods

编译后全局搜索Goods

extern "C" unsigned long OBJC_IVAR_$_Goods$_good;
struct Goods_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *_good;
};
struct NSObject_IMPL {
	Class isa;
};

我们将会看到这样一个结构体,这里的NSObject_IVARSisa成员变量,因此可以得出一个结论:对象在底层的本质就是结构体

三,对象之神秘成员变量isa

1.isa 低层源码分析
在第一讲我们提到isa主要作用是将 对象开辟的内存和class进行绑定。

   if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

我们继续深入 initIsa 方法实现

inline void 
objc_object::initIsa(Class cls)
{
    initIsa(cls, false, false);
}
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 newisa(0) ----->isa = newisa进而确定isa类型 isa_t
这里的参数:nonpointer 表示不单单只是一个指针,为什么会有这样一个定义?
因为每一个对象都包含isa指针,isa指针会分配8字节,即8*8=64位。又因为isa的主要作用是将内存和类进行绑定,那么如果将对应的类(即指针)存储到isa中只需要8位,那么这里造成大量的内存空闲。因此苹果针对空余的空间做了相关优化。将一些关键的信息也放入到这里,如引用计数,是否正在释放,weak,关联对象等

#include "isa.h"
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
    };

};
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#   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_t 是一个联合体Class cls为什么会在 union isa_t联合体里面,这就利用了联合体的一个特性,互斥,独一无二。(后面会对联合体位域做补充)
nonpointer:表示是否对isa指针进行优化,0表示纯指针,1表示不止是类对象的地址,isa中包含了类信息、对象、引用计数等
has_assoc:关联对象标志位,0表示未关联,1表示关联
has_cxx_dtor:该对象是否C ++ 或者Objc的析构器,如果有析构函数,则需要做析构逻辑,没有,则释放对象
shiftcls:储存类指针的值,开启指针优化的情况下,在arm64架构中有33位用来存储类指针,x86_64架构中占44位
magic:用于调试器判断当前对象是真的对象还是没有初始化的空间
weakly_referenced:指对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放
deallocating:标志对象是否正在释放
has_sidetable_rc:当对象引用计数大于10时,则需要借用该变量存储进位
hextra_rc:表示该对象的引用计数值,实际上引用计数值减1,例如,如果对象的引用计数为10,那么extra_rc为9,如果大于10,就需要用到上面的has_sidetable_rc

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

四,联合体位域补充


struct LGTeacher1 {
    char        *name;
    int         age;
    double      height ;
};
   struct Goods   good;
   good.name = "shouji";
   good.num  = 2;
   good.wight  = 5;

(lldb) p good
(Goods) $0 = (name = "shouji", num = 0, wight = 0)
(lldb) p good
(Goods) $1 = (name = "shouji", num = 2, wight = 5)

结构体(struct)中所有变量是“共存”的——优点是“有容乃大”, 全面;缺点是struct内存空间的分配是粗放的,不管用不用,全分配。

// 4 * 8 = 32  0000 0000 0000 0000 0000 0000 0000 1111
// 4 位
// 1 字节 3倍浪费
struct Goodss1 {
    BOOL good1; // 0 1
    BOOL good2;
    BOOL good3;
    BOOL good4;
};

// 位域
struct Goodss2 {
    BOOL good1: 1;
    BOOL good2 : 1;
    BOOL good3 : 1;
    BOOL good4: 1;
};
struct Goodss3 {
    BOOL good1: 1;
    BOOL good2 : 2;
    BOOL good3 : 6;
    BOOL good4: 1;
};

2021-06-13 21:42:45.076498+0800 001-联合体位域[1931:92731] 4-1-2

通过代码测试可以得出结论:结构体可以动态调整成员变量的存储大小,即位域。 主要目的还是为了内存利用最大化。

// 联合体 : 互斥
union Goods2 {
    char        *name;
    int         age;
    double      height ;
};
(lldb) p good2
(Goods2) $0 = (name = "是", age = 15999, height = 2.1220036955215401E-314)
(lldb) p good2
(Goods2) $2 = (name = "", age = 100, height = 2.1219958403718369E-314)

联合体可以定义多个不同类型的成员,联合体的内存大小由其中最大的成员的大小决定。
联合体中修改其中的某个变量会覆盖其他变量的值。
联合体所有的变量公用一块内存,变量之间互斥。

五,读取 isa 中的存储信息

        Goods *p = [Goods alloc];
        NSLog(@"%@",p);
(lldb) x/4gx p
0x100621d60: 0x011d800100008275 0x0000000000000000
0x100621d70: 0x0000000000000000 0x0000000000000000
(lldb) p/x Goods.class
(Class) $2 = 0x0000000100008270 Goods
(lldb) p/x 0x011d800100008275 & 0x00007ffffffffff8ULL
(unsigned long long) $3 = 0x0000000100008270

1)通过打印可以发现 0x011d800100008275 即为isa的内存。可以发现并没有存满。
0x0000000100008270 位类的信息储存值。当我们通过 总的内存 & 上 # define ISA_MASK 0x00007ffffffffff8ULL时可以得到class 的存储信息。这里用到 ISA_MASK 是一个掩码 ,通过 & 操作读取对应内存中的信息。因为我们存储的时候也是通过这种方式存储的。

(lldb) p/x 0x011d800100008275 >>3
(long) $4 = 0x0023b0002000104e
(lldb) p/x 0x0023b0002000104e << 20
(long) $5 = 0x0002000104e00000
(lldb) p/x 0x0002000104e00000 >> 17
(long) $6 = 0x0000000100008270

这里为什么先向右移3位, 1+1+1=3
在向左移动20位  3+6+1+1+1+8=20
最后在向右移动17位 回到原来的位置。

      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

2)通过位运算得到存储的 class 信息这里
对象中的成员变量是通过平移也是通过对应的属性存储的大小通过位运算平移取值。

总结

1.对象在底层的本质就是结构体
2.对象中的成员变量isa主要是通过联合体+位域的方式存储信息,优点就是节省大量内存。
3.isa主要是通过shiftcls进行储存类指针的值,进而将类和对应申请的内存进行绑定。
未完待续。。。

上一篇:使用NWPU VHR-10数据集训练Faster R-CNN模型


下一篇:单例设计模式的理解和分析