阅读 287

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

欢迎阅读iOS底层原理探索系列篇章

isa底层原理

联合体位域

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

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

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

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

现在大家可能心存疑问,什么是联合体?什么是位域?位运算又是什么?不要着急,接下来一一为大家解答.

1.位域

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

2.位运算符

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

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关联对象与类

isaOC对象的第一个属性,因为这一属性是来自于继承,要早于对象的成员变量,属性列表,方法列表以及所遵循的协议列表. 在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

章后总结

大家在学习的路上,发现会很多的东西都是有关联的!对象的探索我们先告一个段落,因为我们通过对象的固有属性isa摸到了类!下面篇章我们针对类的结构详细分析. 在此附上一张isa初始化的流程图

关注下面的标签,发现更多相似文章
评论