02--对象本质02--isa本质

[TOC]

一、联合体 Union

1.1 联合体的特性

联合体 union 也成共用体,有以下特性:

  1. union中可以定义多个成员, union的大小由最大成员的大小决定.
  2. union成员共享同一块大小的内存,一次只能使用其中的一个成员.
  3. 对union某一个成员赋值, 会覆盖其他成员的值.
  1. union的存放顺序是所有成员都从低地址开始存放的.

使用联合体的好处

  1. 多个成员共用一块内存
  2. 可读性强
  3. 使用位运算提高数据存储的效率

使用联合体的坏处

  1. 也是共用同一块内存, 有可能会浪费一定的空间

1.2 联合体特性的验证

验证条件

  • 先创建一个对象 LGTank
  • 有一个 union 类型的属性 _direction
  • 结构体中有两个属性:bitsstruct(匿名结构体)
@interface LGTank(){
    // 联合体
    union {
        char bits;
        // 位域
        struct {
            char front  : 1;
            char back   : 8;
            char left   : 1;
            char right  : 1;
        };
        
    } _direction;
}

验证1:union 中可以定义多个成员, union 的大小由最大成员的大小决定.

  1. struct 中的值类型都为 char 时, 打印出来的大小为 1

    • bits 大小为 1
    • 结构体大小也为 1
    • 所以联合体大小为 1
    image
  2. 将 struct 其中一个成员变量类型改为 short, 打印出来的大小为 2

    • bits 大小为 1
    • 结构体大小为 2
    • 所以联合体大小为 2
    image
  3. 将 bits 类型改为 int, 打印出来的大小为 4

    • bits 大小为4
    • 结构体大小为2
    • 所以联合体大小为4
    image
  1. struct 其中一个成员变量类型改为 int, 打印出来的大小为 4

    • bits 大小为4
    • 结构体大小为4
    • 所以联合体大小为4
    image

验证2:union成员共享同一块大小的内存,一次只能使用其中的一个成员.

联合体结构的分析

  1. 变量 char bits

    这个使我们常见的定义变量的方式, 类型+变量名.

  2. 位域 struct 的分析

    struct {
        char front  : 1;
        char back   : 1;
        char left   : 1;
        char right  : 1;
    };
    

    假如我们不知道有联合体和位域这个称谓, 这里定义了一个结构体, 没有声明结构体类型, 也没有创建结构体变量, 那放在这里有什么用呢?

    • 打印联合体信息


      image

      可以看出来存在两个变量, 一个变量 bits, 一个匿名结构体变量, 值为(1, 0, 0, 0)

    • 注释这段代码, 发现程序还是能够正常运行, 也还是能输出bits的值, 跟有没有注释这段代码前的结果一模一样.


      image

      但是这段代码输出的联合体重的变量只有一个 bits

    • 联合体的赋值(见下面)

联合体的赋值

对结构体的赋值一般都是通过位运算进行赋值

#define LGDirectionFrontMask    (1 << 0)
#define LGDirectionBackMask     (1 << 1)
#define LGDirectionLeftMask     (1 << 2)
#define LGDirectionRightMask    (1 << 3)
- (void)setFront:(BOOL)isFront {
    if (isFront) {
        _direction.bits |= LGDirectionFrontMask;
    } else {
        _direction.bits &= ~LGDirectionFrontMask;
    }
}

因为联合体共用的是一块内存, 所以不能直接对bits赋一个数值, 这样会导致结果超出预期范围之外.

需要通过对面罩Mask做位运算来进行赋值
需要设置front为YES,
比如 LGDirectionFrontMask 的二进制为 0x00000001

  1. 最后一位一定是1, 所以需要跟最后一位或运算
  2. 现在需要改变 front位的值, 而不影响到其他位的值, 所以其他为要跟00使用或运算

需要设置front为NO,

  1. 最后一位是0, 必须执行与运算
  2. 而执行运算的时候, 不影响其他位域的值, 则需要跟1进行或运算. 从而可以推断出跟bits进行与运算的数是0x11111110, 刚好等于~LGDirectionFrontMask

共用空间

image

再来看这张图, 在设置属性的值的地方, 明明没有设置结构体中front的值, 可是输出的结果中却显示front=1, 就是我们设置的bits=1

猜想: 是否结构体中的值表示这个联合体的某一位的值?

#define LGDirectionFrontMask    0b0000011

尝试将面罩从0b1设置为0b11, 打印出来的结果如下


image

果然是表示这个联合体前两位的一个值

再次验证

#define LGDirectionFrontMask    0b0000101

猜测结果 应该是 front位和left位值为 1


image

输出的结果和我们预期是完全一模一样的.

所以, 我们可以知道这个结构体在联合体里面的作用相当于一个注释的功能, 就是告诉使用这个联合体的人, 这个联合体每一位对应存的是什么信息, 你要按照结构体中标注的位域空间来进行读取.

惊喜二, 上述验证除了解释联合体的特性之外, 还辅助我解释了小端序这一特性

1.3 意外收获——小端序的理解

前面的笔记中说过小端模式的特点是: 是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。

当时一直不明白什么是数据的高字节/低字节, 什么又是内存的高地址/低地址

让联合体带着我们来看

#define LGDirectionFrontMask    0b00000101

LGDirectionFrontMask这个常量, 左边的是高字节, 这个很好理解, 不会理解的就当做十进制数的个十百千万的顺序来理解就行.

而内存的低地址/高地址怎么理解呢? 我们平时通过x/4gx obj来打印一个对象的属性内存段地址时, 可以看到属性的地址是依次增加的, 也就是说在前面的属性的地址属于低地址.

通过上面实例来分析: LGDirectionFrontMask最右边的1, 表示这个字节的最低字节了, 而对应的联合体union的内存地址中的 front 位--第一位, 也就是内存的最低地址, 从而验证了数据的低字节保存的内存的低地址中

nice, 学习总是环环相扣, 当你对某个知识点理不清楚的时候, 不要钻牛角尖, 去了解下一个知识点, 也许会柳暗花明又一村

1.4 联合体和结构体的对比

  • 分别定义一个相同结构的联合体和结构体
// 联合体
union {
    char bits;
    // 位域
    struct {
        char front  : 1;
        char back   : 1;
        char left   : 1;
        char right  : 1;
    };

} _direction;

//结构体
struct {
    char bits;
    // 匿名结构体
    struct {
        char front  : 1;
        char back   : 1;
        char left   : 1;
        char right  : 1;
    };
} _word;
  • 使用相同的面罩给他们赋值
#define LGDirectionFrontMask    0b0000101
- (void)setFront:(BOOL)isFront {
    if (isFront) {
        _direction.bits |= LGDirectionFrontMask;
        _word.bits      |= LGDirectionFrontMask;
    } else {
        _direction.bits &= ~LGDirectionFrontMask;
        _word.bits      &= ~LGDirectionFrontMask;
    }
}
  • 查看_direction和_word
(lldb) p tank->_direction
((anonymous union)) $0 = {
  bits = '\x05'
   = (front = '\x01', back = '\0', left = '\x01', right = '\0')
}
(lldb) p tank->_word
((anonymous struct)) $1 = {
  bits = '\x05'
   = (front = '\0', back = '\0', left = '\0', right = '\0')
}

在联合体中, 可以看到我们定义的那个匿名结构体中有值, 从右往左读分别是0x0101, 而这个顺序恰好是bits值(5)的二进制值, 这便解释了位域这个词的含义, 结构体所在内存的每一位的值.

而在结构体中, 可以看到属性bits有值, 但是另一个属性匿名结构体中的值并没有任何变化, 说明这两个属性之间没有任何关联, 分别存在两个不同的地方.

二、内存对齐

2.1 内存对齐的原则

  • 原则一: 第一个数据成员放在offset为0的地方
  • 原则二: 以后每个成员的起始位置为该成员大小的整数倍
  • 原则三: 如果成员是结构体, 则这个成员的起始位置为结构体内部的最大成员的整数倍
  • 原则四: 结构体总大小, 也就是sizeof的结果, 必须为内部成员(包括成员为结构体的内部成员)最大成员大小的整数倍, 不足要补齐.

2.2 内存对齐的思考

  1. 空间换时间

    我们的内存按照8字节对齐, 计算机每次在读数据的时候, 每次按照8字节的刻度读取, 远比逐个字节读取的效率要高得多.

  2. 如何定义属性保证结构体所占字节最小

    这个系统会自动编排, 不需要我们关心成员的排列方式. 系统是如何自动编排的, 有兴趣的时候可以研究下.

  3. 内存优化

    内存对齐可以节约内存空间, 优化内存.
    isa指针的结构也是优化内存的一种设计, 将不同的信息存在isa的不同位域, 避免使用过多的属性

  4. 二进制重排

    先将有意义的内存排列在一起, 优先进行加载, 对没有用到的内存排列在其他地方等待加载, 以提高启动速度.


    image

2.3 内存对齐的示例

// char     1
// short    2
// int      4
// double   8
// 原则一: 第一个数据成员放在offset为0的地方
// 原则二: 以后每个成员的起始位置为该成员大小的整数倍
// 原则三: 如果成员是结构体, 则这个成员的起始位置为结构体内部的最大成员的整数倍
// 原则四: 结构体总大小, 也就是sizeof的结果, 必须为内部成员(包括成员为结构体的内部成员)最大成员大小的整数倍, 不足要补齐.
struct LGStruct1 {
    char a;     // 1 -- 原则一 ==> a 所在下标为 0, 排位(0)
    double b;   // 8 -- 原则二 ==> 当前offset=1, 所以要 +7=8, b要从offset=8的地方开始排, 排位(8 9 10 11 12 13 14 15)
    int c;      // 4 -- 原则二 ==> 当前offset=16, 满足原则二, 直接排, 排位(16 17 18 19)
    short d;    // 2 -- 原则二 ==> 当前offset=20, 满足原则二, 直接排, 排位(20, 21)
                //      原则四 ==> 当前size=22, 最大成员size=8, 不满足, 则需要补齐, 总大小为22+2=24
} myStruct1;

struct LGStruct2 {
    double b;   // 8 -- 原则一 ==> 当前offset=0, 排位(0 1 2 3 4 5 6 7)
    int c;      // 4 -- 原则二 ==> 当前offset=8, 满足原则二, 直接排, 排位(8 9 10 11)
    char a;     // 1 -- 原则二 ==> 当前offset=12, 满足原则二, 直接排, 排位(12 13)
    short d;    // 2 -- 原则二 ==> 当前offset=14, 满足原则二, 直接排, 排位(14 15)
                //      原则四 ==> 当前size=16, 最大成员size=8, 满足原则三, 则总大小为16
} myStruct2;

struct LGStruct3 {
    double b;   // 8 -- 原则一 ==> 当前offset=0, 排位(0 1 2 3 4 5 6 7)
    char a;     // 1 -- 原则二 ==> 当前offset=8, 满足原则二, 直接排, 排位(8)
    int c;      // 4 -- 原则二 ==> 当前offset=9, 不满足原则二, 需补齐(9+3=12), 当前offset=12, 排位(12 13 14 15)
    short d;    // 2 -- 原则二 ==> 当前offset=16, 满足原则二, 直接排, 排位(16, 17)
    struct LGStruct2 s;
                // 16-- 原则三 ==> 当前offset=18, 不满足原则四, 结构体s里面最大成员b的倍数, 需补齐(18+6=24), 排位(24-39)
                //      原则四 ==> 当前size=40, 最大成员size=8, 满足原则三, 则总大小为40
} myStruct3;

isa 本质

定位到isa指针

分析, 对于isa指针, 我们都知道它是用来表示一个对象是什么类型的, 可以猜到它和创建对象想关联.

alloc方法查找

  1. alloc


    image

    image
  2. 通过上一篇关于alloc的探索知道我们最后都会调用callAlloc方法. 在这个方法中我们看到了两个关于isa的代码, 一个是调用一个isa方法, 一个是创建一个isa实例, 不管是哪一个, 我们只要找到isa这个指针的结构即可, 所以随便找个进入. 不妨使用根据initInstanceIsa方法探寻

    image

  3. 通过initInstanceIsa方法找到了isa的初始化方法

    image

  4. initIsa方法中定位到isa_t newisa(0);这行代码, 类型名是isa_t, 变量名是newisa, 那这段代码含义也显然意见了, 创建一个新的isa指针, 那么我们想要找的isa指针结构只差最后一步了

    image

  5. 找到isa_t的定义, 是一个联合体, 联合体有什么特征了, 可以查看另一篇博客, 我们这里主要讲解isa_t的类型

    image

    前面两行代码

    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    

    分别表示isa_t的两个构造函数, 没多大考察意义--过

    第四行代码

    Class cls;
    

    一个class属性, 看到这个, 我们应该要有一种豁然开朗的感觉, 为什么都说isa指针表示对象类型, 因为在这里.
    后面的代码, 表示另一个重点, 为什么isa指针能表示那么多信息

    uintptr_t bits;
    #if defined(ISA_BITFIELD)
        struct {
             ISA_BITFIELD;  // defined in isa.h
        };
    #endif
    
  6. 位域宏定义ISA_BITFIELD

  • arm64架构

    # 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
    
  • x86架构

    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
    
  1. 因为我们真机都是arm64的架构, 所以下面对arm64结构的isa指针位域排列方式进行一个说明.
image

isa走向分析

我们都是站在巨人的肩上学习, 先来看一张巨人绘制的图


image

图中虚线部分, 非常清楚的描述出了 isa 指针的走向图, 但是我们可能还不太理解, 所以需要联合源码来一步一步证实.

验证思路

  • 输出p中isa指针地址 x/g p=>ptr
  • 输出 ptr&ISA_MASK 的值=>isa指向的对象obj的地址
  • 输出这个地址所指向的对象 po $?
  1. alloc一个LGPerson对象实例

    LGPerson *p = [LGPerson alloc];
    
  2. 输出对象p的内存段地址: 我们知道第一个地址肯定表示isa

    x/g p
    0x1007071b0: 0x001d800100002421
    

    打印对象的起始地址: 输出的是一个LGPerson对象

    po 0x1007071b0
    <LGPerson: 0x1007071b0>
    

    而po isa指针, 输出的是这个0x001d800100002421的十进制值, 说明这里存的是一个值. 从上面isa_t的结构来看, 里面是有一个uintptr_t bits;变量, 说明这个值就是isa.bits()的值, 这个后面来证明.

    po 0x001d800100002421
    8303516107940897
    
  3. 按照上述方式逐个打印isa指针所指向的类型

    image

    从这张图中可以看出来isa指针的走位图就是按照上述结构图中的位置

    image

    这张图再次验证了类对象在项目中只会存在一个

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