Objc4-818底层探索(三):isa

建议先看下我之前的
Objc4-818底层探索(一):alloc探索(一)
Objc4-818底层探索(二):内存对齐

开始探索ISA前, 先看个例子,


例子
// 声明
@interface TestObj : NSObject

@property (nonatomic, copy) NSString *SAName;

@end


// 实现
@implementation TestObj

@end

创建一个只有main的项目, 里面添加一个对象继承NSObject (创建main项目可以参考我这一篇章: IOS创建个只有main.m工程) 。完成之后我们Clang转一下 。

接下来介绍一个重要的前端编辑器Clang

Clang

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

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

  • 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。

iOS架构

对于初学者来说光看Clang定义, 可能有些迷茫, 因为涉及一些ios架构内容, 这里简单介绍下。

编译器架构

这块简单介绍一些架构概念:

  • 前端编译器(Frontend): 编译器的前端任务是解析源代码, 会进行词法分析语法分析语义分析

  • 优化器(Optimizer): 负责各种优化, 改善代码的运行时间,如消除冗余计算等

  • 后端编译器(Backkend)/ 代码生成器(CodeGenerator): 将代码映射到目标指令集,生成机器语言,并进行机器相关的代码优化(目标指不同操作系统)。



Objective C / C / C++ 使用的编译器前端是ClangSwiftswift,后端都是LLVMLLVM可参考上一篇 Objc4-818底层探索(二):内存对齐 llvm部分

iOS编译器架构

总结

iOS编译器架构流程顺序依次是 OC→Clang→LLVM→机器代码, Clang为前端编译器, llvm为后端编译器




我们回到Clang命令转一下main, 通过命令将main.m转成C++文件main.cpp 方便我们更好的探究底层

  • 打开终端
cd 进入对应项目文件夹

clang -rewrite-objc main.m -o main.cpp //将 main.m 编译成 main.cpp 
clang命令
  • 打开 main.cpp
main.cpp

里面代码很多, 我们看想要找的就行, 因为我们要查看自定义类TestObj, 全篇搜索TestObj

自定义对象底层

  • TestObj_IMPL这句代码可看出: 对象在底层本质就是结构体, 即OC对象的本质其实就是结构体

  • typedef struct objc_object TestObj; 可看出: 虽然OC层面TestObj 是继承于NSObject的, 但实际在底层我们可看出, TestObj 是继承于objc_object

这里补充几个知识点:

补充知识点1
typedef struct objc_class *Class;

typedef struct objc_object *id;

①: 可看出 Class类实际上是 struct objc_class *结构体指针类型, Class其实就是个别名
②: 可看出 id实际上是 struct objc_object *结构体指针类型, 这就是为什么我们平常定义id类型不带*原因


补充知识点2
struct NSObject_IMPL {
    Class isa;
};
  • 上面代码可看出: struct NSObject_IMPL NSObject_IVARS;其实就是ISA


补充知识点3

    结构体是可以继承的, 在C++是可以继承的, 在C可以伪继承。这种继承的方式是直接将NSObject结构体定义为 TestObj 中的第一个属性, 意味着 TestObj 拥有 NSObject中的所有成员变量。所以 NSObject_IVARS 等效于 NSObject中的isa


补充知识点4
  • NSString *_SAName;就是我们定义的属性, 可看到属性在底层是以成员变量形式存放的

  • _I_TestObj_SAName, _I_TestObj_setSAName_就是属性的get, set方法


补充知识点5
static NSString * _I_TestObj_SAName(TestObj * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_TestObj$_SAName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_TestObj_setSAName_(TestObj * self, SEL _cmd, NSString *SAName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct TestObj, _SAName), (id)SAName, 0, 1); }
// 偏移量定义
extern "C" unsigned long int OBJC_IVAR_$_TestObj$_SAName __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct TestObj, _SAName);
  • 里面可看到实例法里面有TestObj *, SEL等, 这些属于隐藏参数, 只存在底层, 上层OC不需要

  • get方法取值return (*(NSString **)((char *)self + OBJC_IVAR_$_TestObj$_SAName));, 实际上也是一种平移取值方法, self(首地址) + OBJC_IVAR_$_TestObj$_SAName(偏移量) = 想要获取值的地址, 然后通过地址, 获得想查找的值。

  • set方法赋值也是同上, 关键点: 首地址 + 偏移量



联合体与联合体位域

我们先看下联合体相关知识点, 方便我们接下来探索ISA

1.联合体与结构体

联合体结构体都是基础的构造数据类型

结构体(struct)

结构体: 把不同的数据整合成一个整体, 其变量是共存的, 变量是否使用都会为其开辟内存空间。

  • 优点: 储存容量大, 包容性强, 并且成员与成员之间不会互相影响

  • 缺点: 因为结构体里面成员都会给分配内存(不管用没用到), 没有用到的会造成内存浪费

联合体(union)

联合体: 也叫共用体, 把不同的数据整合成一个整体,其变量是互斥的, 所有成员同一片内存。联合体采用内存覆盖技术, 同一时刻只能保存一个成员值, 即:

  • 优点: 所有成员共用一段内存节省内存空间

  • 缺点: 包容性弱, 成员互斥, 同一时刻只能保存一个成员的值

两者的区别
  • 内存占用

    • 结构体: 各个成员占用不同内存, 相互没有影响
    • 联合体: 各个内存占用同一内存, 修改一个成员影响其他所有成员
  • 内存分配

    • 结构体: 开辟的内存大于等于所有成员总和(对齐可能导致有空余)
    • 联合体: 内存占用为最大成员占用的内存

例子

例子1


联合体, 结构体例子

例子2


联合体, 结构体例子

针对于上面例子也可看出, 结构体开辟的内存大于等于所有成员总和, 而 联合体: 内存占用为最大成员占用的内存



位域

还是先看个例子


位域例子

我们发现, 针对结构体1我们给每一项赋值得到结构体2, 当打印内存占用时候发现, 结构体占用内存大小变为1了, 这就是位域(:后面定义位数)

结构体1存放情况

0000 0000 0000 0000 0000 0000 0000 1111
可看到其实浪费了很多内存

结构体2存放情况

0000 1111

0000 dcba 存放情况(从后往前排)

结构体2只占4位, 留意是, 占0.5字节, 系统会自动分配1字节



联合体位域

有了联合体还有位域概念, 我们看下联合体位域, 还是先看例子

联合体位域例子

针对于上面例子, 我们每赋值一次, p一下, 可看到

  • 结构体成员是共存的, 而联合体成员是互斥的, 同一阶段只能有一个被使用, 这就是联合体位域
    (age = 16122, height = 2.1220037562916146E-314, 这种是脏内存脏数据, 不被使用的)


isa

alloc核心三个方法的最后一个: 指针关联 obj->initInstanceIsa(cls, hasCxxDtor);其中isa很关键, 我们先看下其定义

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;
    }
    isa = newisa;
    
}

initIsa可看到isa是一个isa_t类型

union isa_t {
    // isa构造方法
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    
    // 定义bits 位域
    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_tunion联合体类型, 而联合我们重点要看位域, 看下里面存储那些东西, 而isa_t的位域为ISA_BITFIELD; // defined in isa.h, 点进去看下有:

其中ISA_BITFIELD, 其中我们重点看下arm手机端的和x86_64模拟器的就行

# 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

# 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;                                         \
      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)

  • nonpoint: 占1位, 表示是否对isa开启了指针优化, 默认创建都是nonpointisa(不纯), 纯isa需配置

    • 0: 纯isa指针
    • 1: 不仅仅是类对象指针, isa中还包含类信息, 引用计数, 关联对象, 析构函数等
  • has_assoc: 占1位, 是否关联了对象

    • 0: 没有关联了对象
    • 1: 关联了对象
  • has_cxx_dtor: 占1位, 是否有C++或者Objc的析构器, 占1位。类似dealloc, 因为底层的C++释放, 才是真正的释放。可以理解成底层C++没有释放, OC层其实不是释放。

    • 0: 没有, 则可以更快的释放对象
    • 1: 有, 有析构函数,则需要做析构逻辑
  • shiftcls: 类相关信息, 其实存的是类的指针地址

    • arm64: 占33
    • x86_64: 占44
  • magic: 占6位, 用于调试器判断当前对象是真的对象 还是 没有初始化的空间

  • weakly_refrenced: 占1位, 指对象是否被指向 或者 曾经指向一个ARC的弱变量
    , 没有弱引用的对象可以更快释放

  • unused: 占1

  • has_sidetable_rc: 占1位, 散列表, 通常对象引用计数大于10时,需用到它, 借用该变量存储进位

  • extra_rc: 对象的引用计数表示该对象的引用计数值,实际上是引用计数值减1
    例如: 引用计数为10,那么extra_rc为9。对象引用计数大于10, 会用到has_sidetable_rc

    • arm64: 占19
    • x86_64: 占8

arm64下ISA位域:

ISA_BITFIELD arm64

x86_64下ISA位域:

ISA_BITFIELD x86_64

isa位域

当然我们也可以断点读一下isa信息, 也可以看位域情况



isa获取类

之前提到过isa中存的就是类信息, 那么已知isa怎么获取类呢?

方法1: isa通过掩码ISA_MASK得到类地址

OC对象中的isa并不是直接存放所指向对象的地址值,而是需要通过和ISA_MASK进行一次&运算才能得出真正的地址

看下例子


例子
  • x/4gx test: 以4片16进制读取test内存段, 其中0x011d8001000081ad为isa
  • p/x 0x011d8001000081ad & 0x00007ffffffffff8ULL: 取isa & ISA_MASK掩码(留意下模拟器取x86_64掩码)得到0x00000001000081a8
  • p或p/x test.class: 直接读取下test的类信息, 发现0x00000001000081a8, 两者相等


方法2: 字节平移得到类地址

#   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

由于类信息存在shiftcls, 即中间44位(X86_64下), 固定前面3位, 后面17位。那么我们可以通过字节平移得到类信息。

看下例子


字节平移
  • isa先向右平移3位, 低3位抹零
  • isa再向左平移20位, 高位抹17位零(20位原因是, 因为先向右移动了3位)
  • isa再向右平移17位, 即得到类信息
    字节平移

注意: arm64下shiftcls33, 固要按33进行相应平移得到类信息



总结

根据我们之前的探索可得出

struct objc_class : objc_object

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

类(Class), 对象(NSObject)底层都是继承于objc_object, 其本质为结构体, 结构体里面有个nonpointerisa其本质是联合体, 同时isa里面按位域储存相应信息。

对象
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,841评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,415评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,904评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,051评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,055评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,255评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,729评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,377评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,517评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,420评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,467评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,144评论 3 317
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,735评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,812评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,029评论 1 256
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,528评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,126评论 2 341

推荐阅读更多精彩内容