iOS底层原理探索 — isa原理与对象的本质


iOS底层原理探索篇 主要是围绕底层进行源码分析-LLDB调试-源码断点-汇编调试,让本身之后回顾复习的😀😀html

目录以下:c++

iOS底层原理探索 — 开篇程序员

iOS底层原理探索 — alloc&init探索xcode

iOS底层原理探索 — 内存对齐&malloc源码分析bash

iOS底层原理探索 一 isa原理与对象的本质数据结构


isa底层原理

联合体位域

咱们在iOS底层原理探索 — 内存对齐&malloc源码分析一文中讲解到NObject的底层实现其实就是一个包含一个isa指针的结构体:架构

struct NSObject_IMPL {
    Class isa;
};
复制代码

arm64架构以前,isa仅是一个指针,保存着类对象(Class)或元类对象(Meta-Class)的内存地址,在arm64架构以后,苹果对isa进行了优化,变成了一个isa_t类型的联合体(union)结构,同时使用位域来存储更多的信息:app

也就是说,咱们以前熟知的OC对象的 isa指针并非直接指向类对象或者元类对象的内存地址,而是须要 & ISA_MASK经过位运算才能获取类对象或者元类对象的地址.

如今你们可能心存疑问,什么是联合体?什么是位域?位运算又是什么?不要着急,接下来一一为你们解答.

1.位域

位域是指信息在存储时,并不须要占用一个完整的字节, 而只需占一个或几个二进制位。例如生活中的电灯开关,它只有“开”、“关”两种状态,那咱们就能够用10来分别表明这两种状态,这样咱们就仅仅用了一个二进制位就保存了开关的状态。这样一来不只节省存储空间,还使处理更加简便。iphone

2.位运算符

在计算机语言中,除了加、减、乘、除等这样的算术运算符以外还有不少运算符,这里只为你们简单讲解一下位运算符。 位运算符用来对二进制位进行操做,固然,操做数只能为整型和字符型数据C语言中六种位运算符:&按位与、|按位或、^按位异或、~非、<<左移和>>右移。 咱们依旧引用上面的电灯开关论,只不过如今咱们有两个开关:开关A和开关B,1表明开,0表明关。ide

1)按位与&

有0出0,全1出1.

A B &
0 0 0
1 0 0
0 1 0
1 1 1

咱们能够理解为在按位与运算中,两个开关是串联的,若是咱们想要灯亮,须要两个开关都打开灯才会亮,因此是1 & 1 = 1. 若是任意一个开关没有打开,灯都不会亮,因此其余运算都是0.

2)按位或 |

有1出1,全0出0.

A B I
0 0 0
1 0 1
0 1 1
1 1 1

在按位或运算中,咱们能够理解为两个开关是并联的,即一个开关开,灯就会亮.只有当两个开关都是关的.灯才不会亮.

3)按位异或^

相同为0,不一样为1.

A B ^
0 0 0
1 0 1
0 1 1
1 1 0
4)非 ~

非运算即取反运算,在二进制中 1 变 0 ,0 变 1。例如110101进行非运算后为001010,即1010.

5)左移 <<

左移运算就是把<<左边的运算数的各二进位所有左移若干位,移动的位数即<<右边的数的数值,高位丢弃,低位补0。 左移n位就是乘以2的n次方。例如:a<<4是指把a的各二进位向左移动4位。如a=00000011(十进制3),左移4位后为00110000(十进制48)。

6)右移 >>

右移运算就是把>>左边的运算数的各二进位所有右移若干位,>>右边的数指定移动的位数。例如:设 a=15,a>>2 表示把00001111右移为00000011(十进制3)

位运算符的运用

1)取值

能够利用按位与 &运算取出指定位的值,具体操做是想取出哪一位的值就将那一位置为1,其它位都为0,而后同原数据进行按位与计算,便可取出特定的位.

例: 0000 0011取出倒数第三位的值

// 想取出倒数第三位的值,就将倒数第三位的值置为1,其它位为0,跟原数据按位与运算
  0000 0011
& 0000 0100
------------
  0000 0000  // 得出按位与运算后的结果,便可拿到原数据中倒数第三位的值为0
复制代码

上面的例子中,咱们从0000 0011中取值,则有0000 0011被称之为源码.进行按位与操做设定的0000 0100称之为掩码.

2)设值

能够经过按位或 |运算符将某一位的值设为1或0.具体操做是: 想将某一位的值置为1的话,那么就将掩码中对应位的值设为1,掩码其它位为0,将源码与掩码进行按位或操做便可.

例: 将0000 0011倒数第三位的值改成1

// 改变倒数第三位的值,就将掩码倒数第三位的值置为1,其它位为0,跟源码按位或运算
  0000 0011
| 0000 0100
------------
  0000 0111  // 便可将源码中倒数第三位的值改成1
复制代码

想将某一位的值置为0的话,那么就将掩码中对应位的值设为0,掩码其它位为1,将源码与掩码进行按位或操做便可.

例: 将0000 0011倒数第二位的值改成0

// 改变倒数第二位的值,就将掩码倒数第二位的值置为0,其它位为1,跟源码按位或运算
  0000 0011
| 1111 1101
------------
  0000 0001  // 便可将源码中倒数第二位的值改成0
复制代码

到这里相信你们对位运算符有了必定的了解,下面咱们经过OC代码的一个例子,来将位运算符运用到实际代码开发中. 咱们声明一个TCJCar类,类中有四个BOOL类型的属性,分别为frontbackleftright,经过这四个属性来判断这辆小车的行驶方向.

而后咱们来查看一下这个 TCJCar类对象所占据的内存大小:
咱们看到,一个 TCJCar类的对象占据16个字节.其中包括一个 isa指针和四个 BOOL类型的属性,8+1+1+1+1=12,根据 内存对齐原则,因此一个 TCJCar类的对象占16个字节.

咱们知道,BOOL值只有两种状况:01,占据一个字节的内存空间.而一个字节的内存空间中又有8个二进制位,而且二进制一样只有01,那么咱们彻底可使用1个二进制位来表示一个BOOL值。也就是说咱们上面声明的四个BOOL值最终只使用4个二进制位就能够,这样就节省了内存空间。那咱们如何实现呢? 想要实现四个BOOL值存放在一个字节中,咱们能够经过char类型的成员变量来实现.char类型占一个字节内存空间,也就是8个二进制位.可使用其中最后四个二进制位来存储4个BOOL值. 固然咱们不能把char类型写成属性,由于一旦写成属性,系统会自动帮咱们添加成员变量,自动实现setget方法.

@interface TCJCar(){
    char _frontBackLeftRight;
}
复制代码

若是咱们赋值_frontBackLeftRight1,即0b 0000 0001,只使用8个二进制位中的最后4个分别用0或者1来表明frontbackleftright的值.那么此时frontbackleftright的状态为:

结合咱们上文讲的6种位运算符以及使用场景,咱们能够分别声明 frontbackleftright的掩码,来方便咱们进行下一步的位运算取值和赋值:

#define TCJDirectionFrontMask 0b00001000 //此二进制数对应十进制数为 8
#define TCJDirectionBackMask 0b00000100 //此二进制数对应十进制数为 4
#define TCJDirectionLeftMask 0b00000010 //此二进制数对应十进制数为 2
#define TCJDirectionRightMask 0b00000001 //此二进制数对应十进制数为 1
复制代码

经过对位运算符的左移<<和右移>>的了解,咱们能够将上面的代码优化成:

#define TCJDirectionFrontMask (1 << 3)
#define TCJDirectionBackMask (1 << 2)
#define TCJDirectionLeftMask (1 << 1)
#define TCJDirectionRightMask (1 << 0)
复制代码

自定义的set方法以下:

- (void)setFront:(BOOL)front
{
    if (front) {// 若是须要将值置为1,将源码和掩码进行按位或运算
        _frontBackLeftRight |= TCJDirectionFrontMask;
    } else {// 若是须要将值置为0 // 将源码和按位取反后的掩码进行按位与运算
        _frontBackLeftRight &= ~TCJDirectionFrontMask;
    }
}
- (void)setBack:(BOOL)back
{
    if (back) {
        _frontBackLeftRight |= TCJDirectionBackMask;
    } else {
        _frontBackLeftRight &= ~TCJDirectionBackMask;
    }
}
- (void)setLeft:(BOOL)left
{
    if (left) {
        _frontBackLeftRight |= TCJDirectionLeftMask;
    } else {
        _frontBackLeftRight &= ~TCJDirectionLeftMask;
    }
}
- (void)setRight:(BOOL)right
{
    if (right) {
        _frontBackLeftRight |= TCJDirectionRightMask;
    } else {
        _frontBackLeftRight &= ~TCJDirectionRightMask;
    }
}
复制代码

自定义的get方法以下:

- (BOOL)isFront
{
    return !!(_frontBackLeftRight & TCJDirectionFrontMask);
}
- (BOOL)isBack
{
    return !!(_frontBackLeftRight & TCJDirectionBackMask);
}
- (BOOL)isLeft
{
    return !!(_frontBackLeftRight & TCJDirectionLeftMask);
}
- (BOOL)isRight
{
    return !!(_frontBackLeftRight & TCJDirectionRightMask);
}
复制代码

此处须要注意的是,代码中!为逻辑运算符非,由于_frontBackLeftRight & TCJDirectionFrontMask代码执行后,返回的确定是一个整型数,如当frontYES时,说明二进制数为0b 0000 1000,对应的十进制数为8,那么进行一次逻辑非运算后,!(8)的值为0,对0再进行一次逻辑非运算!(0),结果就成了1,那么正好跟frontYES对应.因此此处进行两次逻辑非运算,!!. 固然,还要实现初始化方法:

- (instancetype)init
{
    self = [super init];
    if (self) {
        _frontBackLeftRight = 0b00001000;
    }
    return self;
}
复制代码

经过测试验证,咱们完成了取值和赋值:

使用结构体位域优化代码

咱们在上文讲到了位域的几率,那么咱们就可使用结构体位域来优化一下咱们的代码.这样就不用再额外声明上面代码中的掩码部分了.位域声明格式是位域名: 位域长度. 在使用位域的过程当中须要注意如下几点:

  1. 若是一个字节所剩空间不够存放另外一位域时,应从下一单元起存放该位域.
  2. 位域的长度不能大于数据类型自己的长度,好比int类型就不能超过32位二进位.
  3. 位域能够无位域名,这时它只用来做填充或调整位置。无名的位域是不能使用的.

使用位域优化后的代码:

来测试看一下是否正确,此次咱们将 front设为 YESback设为 NOleft设为 NOright设为 YES:
依旧能完成赋值和取值. 可是代码这样优化后咱们去掉了掩码和初始化的代码,可读性不好,咱们继续使用联合体进行优化:

使用联合体优化代码

咱们可使用比较高效的位运算来进行赋值和取值,使用union联合体来对数据进行存储。这样不只能够增长读取效率,还能够加强代码可读性.

#import "TCJCar.h"

//#define TCJDirectionFrontMask 0b00001000 //此二进制数对应十进制数为 8
//#define TCJDirectionBackMask 0b00000100 //此二进制数对应十进制数为 4
//#define TCJDirectionLeftMask 0b00000010 //此二进制数对应十进制数为 2
//#define TCJDirectionRightMask 0b00000001 //此二进制数对应十进制数为 1

#define TCJDirectionFrontMask (1 << 3)
#define TCJDirectionBackMask (1 << 2)
#define TCJDirectionLeftMask (1 << 1)
#define TCJDirectionRightMask (1 << 0)

@interface TCJCar()
{
    union{
        char bits;
        // 结构体仅仅是为了加强代码可读性
        struct {
            char front  : 1;
            char back   : 1;
            char left   : 1;
            char right  : 1;
        };
    }_frontBackLeftRight;
}
@end

@implementation TCJCar
- (instancetype)init
{
    self = [super init];
    if (self) {
        _frontBackLeftRight.bits = 0b00001000;
    }
    return self;
}
- (void)setFront:(BOOL)front
{
    if (front) {
        _frontBackLeftRight.bits |= TCJDirectionFrontMask;
    } else {
        _frontBackLeftRight.bits &= ~TCJDirectionFrontMask;
    }
}
- (BOOL)isFront
{
    return !!(_frontBackLeftRight.bits & TCJDirectionFrontMask);
}
- (void)setBack:(BOOL)back
{
    if (back) {
        _frontBackLeftRight.bits |= TCJDirectionBackMask;
    } else {
        _frontBackLeftRight.bits &= ~TCJDirectionBackMask;
    }
}
- (BOOL)isBack
{
    return !!(_frontBackLeftRight.bits & TCJDirectionBackMask);
}
- (void)setLeft:(BOOL)left
{
    if (left) {
        _frontBackLeftRight.bits |= TCJDirectionLeftMask;
    } else {
        _frontBackLeftRight.bits &= ~TCJDirectionLeftMask;
    }
}
- (BOOL)isLeft
{
    return !!(_frontBackLeftRight.bits & TCJDirectionLeftMask);
}
- (void)setRight:(BOOL)right
{
    if (right) {
        _frontBackLeftRight.bits |= TCJDirectionRightMask;
    } else {
        _frontBackLeftRight.bits &= ~TCJDirectionRightMask;
    }
}
- (BOOL)isRight
{
    return !!(_frontBackLeftRight.bits & TCJDirectionRightMask);
}
@end
复制代码

来咱们测试看一下是否正确,此次咱们依旧将front设为YESback设为NOleft设为NOright设为YES:

经过结果咱们看一看到依旧能完成赋值和取值. 这其中 _frontBackLeftRight联合体只占用一个字节,由于结构体中 frontbackleftright都只占一位二进制空间,因此结构体只占一个字节,而 char类型的 bits也只占一个字节.他们都在联合体中,所以共用一个字节的内存便可. 并且咱们在 setget方法中的赋值和取值经过使用掩码进行位运算来增长效率,总体逻辑也就很清晰了.可是若是咱们在平常开发中这样写代码的话,极可能会被同事打死.虽然代码已经很清晰了,可是总体阅读起来仍是很吃力的.咱们在这里学习了位运算以及联合体这些知识,更多的是为了方便咱们阅读OC底层的代码.下面咱们来回到本文主题,查看一下 isa_t联合体的源码.

isa_t联合体

经过源码咱们发现 isa它是一个联合体,联合体是一个结构占8个字节,它的特性就是共用内存,或者说是 互斥,好比说若是 cls赋值了就不在对 bits进行赋值.在 isa_t联合体内使用宏 ISA_BITFIELD定义了位域,咱们进入位域内查看源码:
咱们看到,在内部分别定义了 arm64位架构和 x86_64架构的掩码和位域.咱们只分析 arm64为架构下的部份内容(真机环境下). 能够清楚的看到 ISA_BITFIELD位域的内容以及掩码 ISA_MASK的值: 0x0000000ffffffff8ULL.咱们重点看一下 uintptr_t shiftcls : 33;,在 shiftcls中存储着类对象和元类对象的内存地址信息,咱们上文讲到,对象的 isa指针须要同 ISA_MASK通过一次按位与运算才能得出真正的类对象地址.那么咱们将 ISA_MASK的值 0x0000000ffffffff8ULL转化为二进制数分析一下:
从图中能够看到 ISA_MASK的值转化为二进制中有33位都为1,上文讲到按位与运算是能够取出这33位中的值.那么就说明同 ISA_MASK进行按位与运算就能够取出类对象和元类对象的内存地址信息. 咱们继续分析一下结构体位域中其余的内容表明的含义:
至此咱们已经对 isa指针有了新的认识, arm64架构以后, isa指针不仅仅只存储了类对象和元类对象的内存地址,而是使用联合体的方式存储了更多信息,其中 shiftcls存储了类对象和元类对象的内存地址,须要同 ISA_MASK进行 按位与 &运算才能够取出其内存地址值.

isa关联对象与类

isa是OC对象的第一个属性,由于这一属性是来自于继承,要早于对象的成员变量,属性列表,方法列表以及所遵循的协议列表. 在iOS底层原理探索 — alloc&init探索这篇文章中,当时咱们在探索对象的初始化的时候还有一个很是重要的点没有细说就是:通过calloc申请内存的时候,这个指针是怎么和TCJPerson这个类所关联的呢? 下面咱们就能够直接定位到:obj->initInstanceIsa(cls, hasCxxDtor)

  • 经过前面两篇文章的学习,咱们知道了obj里面只有一个指针
  • 下面的代码就能够分析对象与类直接的联系
  • initIsa(cls, true, hasCxxDtor)
inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    assert(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa.cls = cls;
    } else {
        assert(!DisableNonpointerIsa);
        assert(!cls->instancesRequireRawIsa());

        isa_t newisa(0);

#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
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
#endif

        // 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; } } 复制代码
  • 上面第一层判断是isTaggedPointer的断言,这会在后续文章中重点分析

  • 接下来是nonpointer的判断,由于nonpointer优化,它是和普通结构不同的!经过上文咱们知道内存优化的isa_t结构:它采用的是联合体和位域的搭配.(目前咱们的类都是nonpointer了)

    1. 若是是非nonpointer,表明普通的指针,存储着ClassMeta-Class对象的内存地址信息
    2. 若是是nonpointer,则会进行一系列的初始化操做.其中的newisa.shiftcls = (uintptr_t)cls >> 3;中的shiftcls存储着ClassMeta-Class对象的内存地址信息,咱们来验证一下:

    来咱们来对上面LLDB相关的指令进行一波解析: 3. x/4gx obj:表明打印obj的4段内存信息 4. p/t:表明打印二进制信息(还有p/op/dp/x分别表明八进制、十进制和十六进制打印) 5. p/t (uintptr_t)obj.class将类信息进行二进制打印获得:$3 6. 对第一个属性isa进行二进制打印p/t 0x001d8001000013f1获得:$1 7. 由于此时咱们是在x86_64环境下进行打印的,经过上文咱们知道在x86_64环境下isaISA_BITFIELD位域结构中:前3位是nonpointerhas_assochas_cxx_dtor,中间44位是shiftcls,后面17位是剩余的内容,同时由于iOS是小端模式,那么咱们就须要去掉右边的3位和左边的17位,因此就会采用$1>>3<<3而后$4<<17>>17的操做了.

    经过这个测试,咱们就知道了isa实现了对象与类之间的关联. 在上文中咱们提获得OC对象的isa指针并非直接指向类对象或者元类对象的内存地址,而是须要& ISA_MASK经过位运算才能获取类对象或者元类对象的地址.来咱们也来验证一波:

来咱们来对上面`LLDB`相关的指令进行一波解析:
1. 打印对象的内存信息:`x/4gx obj`
2. 打印类的信息:`p/x obj.class`获得`$7`
3. 经过对象的`isa & ISA_MASK`操做:`p/x 0x001d8001000013f1 & 0x00007ffffffffff8ULL`获得`$8`
4. 对比`$7`和`$8`他们是一模模同样样的

在此也验证了`isa`实现了对象与类之间的关联.
复制代码
  • 一个8字节指针64位下 其实能够存储不少内容,咱们能够优化内存,在不一样的位上,放不一样的东西! 在这咱们还须要补充一下StructUnion的区别:
    1. structunion都是由多个不一样的数据类型成员组成,但在任何同一时刻,union 中只存放了一个被选中的成员, 而struct的全部成员都存在。在struct中,各成员都占有本身的内存空间,它们是同时存在的。一个struct变量的总长度等于全部成员长度之和。在Union中,全部成员不能同时占用它的内存空间,它们不能同时存在。Union变量的长度等于最长的成员的长度
    2. 对于union的不一样成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于struct的不一样成员赋值是互不影响的.

isa的指向走位分析

咱们都知道对象能够建立多个,那么类是否也能够建立多个呢? 答案是一个.怎么验证它呢? 来咱们看下面代码及打印结果:

经过运行结果证实了 类在内存中只会存在一份.

经过上面的打印,咱们发现类的内存结构里面的第一个位置是: 0x1000013f0-TCJPerson他指向元类,是由系统建立的. 咱们来看一下对象-类-元类他们之间的关系:

  1. 对象是由程序员根据类进行实例化来的
  2. 类代码写出来的,内存只有一份,不是咱们建立的,是由系统建立的
  3. 元类是根据系统在编译的时候发现有这么一个类,也是由系统建立的,咱们是实例化不出来的.在编译阶段就会产生的.

到此咱们知道对象的isa指向类,类的isa指向元类,那么元类的isa指向哪呢?

接下来,咱们一块儿来看一下 isa的走位:

至此咱们获得的结论是:

咱们在来看下面代码打印:

经过运行结果咱们知道:元类-根元类-根根元类是一模模同样样的. 到此咱们用苹果官方提供的一张图看瞅一瞅:
咱们对上图进行总结一波:图中实线是 super_class指针,它表明着继承链的关系.虚线是isa指针. 1. Root class (class)其实就是 NSObjectNSObject是没有超类的,因此 Root class(class)superclass指向 nil( NSObject父类是 nil). 2.每一个 Class都有一个isa指针指向惟一的 Meta class. 3. Root class(meta)superclass指向 Root class(class),也就是 NSObject,造成一个回路.这说明 Root class(meta)是继承至 Root class(class)(根元类的父类是 NSObject). 4.每一个 Meta classisa指针都指向 Root class (meta)

  • instance对象的isa指向class对象
  • class对象的isa指向meta-class对象
  • meta-class对象的isa指向基类的meta-class对象

对象的本质

OC中,类对象(class对象)和元类对象(meta-class对象)的本质结构都是struct objc_class指针,即在内存中就是结构体

Class clas = [NSObject class];
复制代码

来到class底层源码,咱们能够看到:

typedef struct objc_class *Class;
复制代码

class对象实际上是一个objc_class结构体的指针.所以咱们能够说类对象或元类对象在内存中其实就是objc_class结构体. 来咱们来看一下源码:

咱们发现 objc_class结构体继承 objc_object而且结构体内有一些函数,由于这是 c++结构体,在 C的基础之上作了扩展.所以结构体中能够包含函数.注意观察注释掉的 Class ISA这一行代码. 咱们来到 objc_object内,继续截取部分代码:

咱们发现 objc_object中有一个 isa指针,那么 objc_class继承 objc_object,也就一样拥有一个 isa指针。继承来了 isa指针,因此上文咱们提到了 Class ISA也就被注释掉了. 再来看第二行代码 Class superclass:咱们来打印一下来看结果:
也就是说 objc_class内存中第一个位置是 isa,第二个位置是 superclass,其余位置咱们后续文章在分析.到此,咱们要怎样继续进行分析呢? 咱们都知道咱们平时编写的 Objective-C代码,其底层的实现都是 C/C++代码.

因此 Objective-C的面向对象都是基于 C/C++的数据结构实现的.那么 Objective-C的对象、类主要是基于 C/C++的什么数据结构实现的呢?--结构体. 所以,咱们能够经过将建立好的 OC文件,转化为 C++文件来看一下 OC对象的底层结构.

OC代码转换为C/C++代码

iOS底层原理探索 — 内存对齐&malloc源码分析一文中,咱们提到过若是将OC代码转化为C/C++了,这里咱们在复习一下: 经过命令行将OC的main.m文件转换成C++文件,生成main.cpp.

clang -rewrite-objc main.m -o main.cpp 
/***rewrite表明 重写
   *-o表明 输出
   *cpp表明 c++(c plus plus)
**/
复制代码

须要注意这种方式没有指定运行平台和架构模式,咱们能够经过命令行设置参数,来指定运行平台和架构模式

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp 
/***xcrun表明 xcode
   * iphoneos表明 运行在iPhone上
   *-arch表明 架构模式,arm64参数表明64位的架构模式
**/
复制代码

生成的main.cpp 文件就是main.m转换c++后的文件,直接拖拽到工程中,就能够查看底层实现了. 咱们的OC文件为main.m:

将其转化为 C++文件 main.cpp后,咱们在 main.cpp 文件中搜索 TCJPerson,能够找到 TCJPerson_IMPLIMPLimplementation 的缩写,表明实现).

经过上文咱们能够看到:

  • NSObject的底层实现就是一个结构体.
  • Class其实就是一个指针,指向了objc_class类型的结构体.
  • TCJPerson_IMPL结构体内有三个成员变量:
    1. isa继承自父类NSObject
    2. helloName
    3. _name
  • 对于属性name:底层编译会生成相应的settergetter方法,且帮咱们转化为_name
  • 对于成员变量helloName:底层编译不会生成相应的settergetter方法,且没有转化为_helloName

接下来咱们来看看main.cpp文件中的method_list_t:

其中 (struct objc_selector *)"name"对应 SEL, "@16@0:8"对应的就是方法签名, (void *)_I_TCJPerson_name对应的方法实现(即 IMP). 其中的 "@16@0:8"方法签名中对应一个返回值和两个参数: 1. @ 返回值类型: id 返回16,表明总共的量 2. @ 参数一类型: id 0-7 3. : 参数二类型: sel 8-15

咱们来打印一下@:具体表明啥:

查看打印结果能够看到对应的typeEncode. 固然咱们也能够去苹果官方文档中查看 TypeEncode
相关文章
相关标签/搜索