iOS底层原理篇 主要是围绕底层进行
源码分析
-LLDB调试
-源码断点
-汇编调试
,让自己以后回顾复习Runtime底层之美的😀😀
目录如下:
OC对象原理(一) alloc&init探索
OC对象原理(二) 内存对齐探索&malloc源码分析
OC对象原理(三) isa原理与对象的本质
持续更新中....更新有点慢请谅解....
文中有误的地方请指正,大家一起学习
isa底层原理
联合体位域
我们在OC对象原理(二) 内存对齐探索&malloc源码分析一文中讲解到NObject的底层实现其实就是一个包含一个isa
指针的结构体:
struct NSObject_IMPL {
Class isa;
};
在arm64
架构之前,isa
仅是一个指针,保存着类对象(Class)或元类对象(Meta-Class)的内存地址,在arm64
架构之后,苹果对isa
进行了优化,变成了一个isa_t
类型的联合体(union)结构,同时使用位域来存储更多的信息:

isa
指针并不是直接指向类对象或者元类对象的内存地址,而是需要& ISA_MASK
通过位运算才能获取类对象或者元类对象的地址.

1.位域
位域是指信息在存储时,并不需要占用一个完整的字节, 而只需占一个或几个二进制位。例如生活中的电灯开关,它只有“开”、“关”两种状态,那我们就可以用1
和0
来分别代表这两种状态,这样我们就仅仅用了一个二进制位就保存了开关的状态。这样一来不仅节省存储空间,还使处理更加简便。
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
类型的属性,分别为front
、back
、left
、right
,通过这四个属性来判断这辆小车的行驶方向.

TCJCar
类对象所占据的内存大小:

TCJCar
类的对象占据16个字节.其中包括一个isa
指针和四个BOOL
类型的属性,8+1+1+1+1=12,根据内存对齐原则,所以一个TCJCar
类的对象占16个字节.
我们知道,BOOL
值只有两种情况:0
或1
,占据一个字节的内存空间.而一个字节的内存空间中又有8个二进制位,并且二进制同样只有0
或1
,那么我们完全可以使用1个二进制位来表示一个BOOL
值。也就是说我们上面声明的四个BOOL
值最终只使用4个二进制位就可以,这样就节省了内存空间。那我们如何实现呢?
想要实现四个BOOL
值存放在一个字节中,我们可以通过char
类型的成员变量来实现.char
类型占一个字节内存空间,也就是8个二进制位.可以使用其中最后四个二进制位来存储4个BOOL
值.
当然我们不能把char
类型写成属性,因为一旦写成属性,系统会自动帮我们添加成员变量,自动实现set
和get
方法.
@interface TCJCar(){
char _frontBackLeftRight;
}
如果我们赋值_frontBackLeftRight
为1
,即0b 0000 0001
,只使用8个二进制位中的最后4个分别用0
或者1
来代表front
、back
、left
、right
的值.那么此时front
、back
、left
、right
的状态为:

front
、back
、left
、right
的掩码,来方便我们进行下一步的位运算取值和赋值:
#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
代码执行后,返回的肯定是一个整型数,如当front
为YES
时,说明二进制数为0b 0000 1000
,对应的十进制数为8,那么进行一次逻辑非运算后,!(8)
的值为0
,对0
再进行一次逻辑非运算!(0)
,结果就成了1
,那么正好跟front
为YES
对应.所以此处进行两次逻辑非运算,!!
.
当然,还要实现初始化方法:
- (instancetype)init
{
self = [super init];
if (self) {
_frontBackLeftRight = 0b00001000;
}
return self;
}
通过测试验证,我们完成了取值和赋值:

使用结构体位域优化代码
我们在上文讲到了位域
的概率,那么我们就可以使用结构体位域
来优化一下我们的代码.这样就不用再额外声明上面代码中的掩码部分了.位域声明格式是位域名: 位域长度
.
在使用位域
的过程中需要注意以下几点:
- 如果一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域.
- 位域的长度不能大于数据类型本身的长度,比如
int
类型就不能超过32位二进位. - 位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的.
使用位域优化后的代码:


front
设为YES
、back
设为NO
、left
设为NO
、right
设为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
设为YES
、back
设为NO
、left
设为NO
、right
设为YES
:

这其中
_frontBackLeftRight
联合体只占用一个字节,因为结构体中front
、back
、left
、right
都只占一位二进制空间,所以结构体只占一个字节,而char
类型的bits
也只占一个字节.他们都在联合体中,因此共用一个字节的内存即可.而且我们在
set
、get
方法中的赋值和取值通过使用掩码进行位运算来增加效率,整体逻辑也就很清晰了.但是如果我们在日常开发中这样写代码的话,很可能会被同事打死.虽然代码已经很清晰了,但是整体阅读起来还是很吃力的.我们在这里学习了位运算以及联合体这些知识,更多的是为了方便我们阅读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对象的第一个属性,因为这一属性是来自于继承,要早于对象的成员变量,属性列表,方法列表以及所遵循的协议列表.
在OC对象原理(一) 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
了)- 如果是非
nonpointer
,代表普通的指针,存储着Class
、Meta-Class
对象的内存地址信息 - 如果是
nonpointer
,则会进行一系列的初始化操作.其中的newisa.shiftcls = (uintptr_t)cls >> 3;
中的shiftcls
存储着Class
、Meta-Class
对象的内存地址信息,我们来验证一下:
来我们来对上面
LLDB
相关的指令进行一波解析:-
x/4gx obj
:代表打印obj
的4段内存信息 -
p/t
:代表打印二进制信息(还有p/o
、p/d
、p/x
分别代表八进制、十进制和十六进制打印) -
p/t (uintptr_t)obj.class
将类信息进行二进制打印得到:$3
- 对第一个属性
isa
进行二进制打印p/t 0x001d8001000013f1
得到:$1
- 因为此时我们是在
x86_64
环境下进行打印的,通过上文我们知道在x86_64
环境下isa
的ISA_BITFIELD
位域结构中:前3
位是nonpointer
,has_assoc
,has_cxx_dtor
,中间44
位是shiftcls
,后面17
位是剩余的内容,同时因为iOS
是小端模式,那么我们就需要去掉右边的3
位和左边的17
位,所以就会采用$1>>3<<3
然后$4<<17>>17
的操作了.
通过这个测试,我们就知道了
isa
实现了对象与类之间的关联.
在上文中我们提得到OC对象的isa
指针并不是直接指向类对象或者元类对象的内存地址,而是需要& ISA_MASK
通过位运算才能获取类对象或者元类对象的地址.来我们也来验证一波:
来我们来对上面
LLDB
相关的指令进行一波解析:- 打印对象的内存信息:
x/4gx obj
- 打印类的信息:
p/x obj.class
得到$7
- 通过对象的
isa & ISA_MASK
操作:p/x 0x001d8001000013f1 & 0x00007ffffffffff8ULL
得到$8
- 对比
$7
和$8
他们是一模模一样样的
在此也验证了
isa
实现了对象与类之间的关联. - 如果是非
-
一个
8字节指针
在64位
下 其实可以存储很多内容,我们可以优化内存,在不同的位上,放不同的东西!
在这我们还需要补充一下Struct
与Union
的区别:-
struct
和union
都是由多个不同的数据类型成员组成,但在任何同一时刻,union
中只存放了一个被选中的成员, 而struct
的所有成员都存在。在struct
中,各成员都占有自己的内存空间,它们是同时存在的。一个struct
变量的总长度等于所有成员长度之和。在Union
中,所有成员不能同时占用它的内存空间,它们不能同时存在。Union
变量的长度等于最长的成员的长度 - 对于
union
的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于struct
的不同成员赋值是互不影响的.
-
isa的指向走位分析
我们都知道对象可以创建多个,那么类是否也可以创建多个呢?
答案是一个.怎么验证它呢?
来我们看下面代码及打印结果:


0x1000013f0-TCJPerson
他指向元类,是由系统创建的.我们来看一下对象-类-元类他们之间的关系:
- 对象是由程序员根据类进行实例化来的
- 类代码写出来的,内存只有一份,不是我们创建的,是由系统创建的
- 元类是根据系统在编译的时候发现有这么一个类,也是由系统创建的,我们是实例化不出来的.在编译阶段就会产生的.
到此我们知道对象的isa
指向类,类的isa
指向元类,那么元类的isa
指向哪呢?

isa
的走位:

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

到此我们用苹果官方提供的一张图看瞅一瞅:

1.
Root class (class)
其实就是NSObject
,NSObject
是没有超类的,所以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 class
的isa
指针都指向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++
代码
在OC对象原理(二) 内存对齐探索&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_IMPL
(IMPL
即 implementation
的缩写,代表实现).


-
NSObject
的底层实现就是一个结构体. -
Class
其实就是一个指针,指向了objc_class
类型的结构体. -
TCJPerson_IMPL
结构体内有三个成员变量:-
isa
继承自父类``NSObject` helloName
_name
-
- 对于属性
name
:底层编译会生成相应的setter
、getter
方法,且帮我们转化为_name
- 对于成员变量
helloName
:底层编译不会生成相应的setter
、getter
方法,且没有转化为_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-73.
:
参数二类型: sel
8-15
我们来打印一下@
、 :
具体代表啥:

当然我们也可以去苹果官方文档中查看TypeEncode
网友评论