iOS底层原理探索篇 主要是围绕底层进行
源码分析
-LLDB调试
-源码断点
-汇编调试
,让本身之后回顾复习的😀😀html目录以下:c++
咱们在iOS底层原理探索 — 内存对齐&malloc源码分析一文中讲解到NObject的底层实现其实就是一个包含一个isa
指针的结构体:架构
struct NSObject_IMPL {
Class isa;
};
复制代码
在arm64
架构以前,isa
仅是一个指针,保存着类对象(Class)或元类对象(Meta-Class)的内存地址,在arm64
架构以后,苹果对isa
进行了优化,变成了一个isa_t
类型的联合体(union)结构,同时使用位域来存储更多的信息:app
isa
指针并非直接指向类对象或者元类对象的内存地址,而是须要
& ISA_MASK
经过位运算才能获取类对象或者元类对象的地址.
位域是指信息在存储时,并不须要占用一个完整的字节, 而只需占一个或几个二进制位。例如生活中的电灯开关,它只有“开”、“关”两种状态,那咱们就能够用1
和0
来分别表明这两种状态,这样咱们就仅仅用了一个二进制位就保存了开关的状态。这样一来不只节省存储空间,还使处理更加简便。iphone
在计算机语言中,除了加、减、乘、除等这样的算术运算符以外还有不少运算符,这里只为你们简单讲解一下位运算符。 位运算符用来对二进制位进行操做,固然,操做数只能为整型和字符型数据。C
语言中六种位运算符:&
按位与、|
按位或、^
按位异或、~
非、<<
左移和>>
右移。 咱们依旧引用上面的电灯开关论,只不过如今咱们有两个开关:开关A和开关B,1
表明开,0
表明关。ide
有0出0,全1出1.
A | B | & |
---|---|---|
0 | 0 | 0 |
1 | 0 | 0 |
0 | 1 | 0 |
1 | 1 | 1 |
咱们能够理解为在按位与运算中,两个开关是串联的,若是咱们想要灯亮,须要两个开关都打开灯才会亮,因此是1 & 1 = 1. 若是任意一个开关没有打开,灯都不会亮,因此其余运算都是0.
有1出1,全0出0.
A | B | I |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
1 | 1 | 1 |
在按位或运算中,咱们能够理解为两个开关是并联的,即一个开关开,灯就会亮.只有当两个开关都是关的.灯才不会亮.
相同为0,不一样为1.
A | B | ^ |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
1 | 1 | 0 |
非运算即取反运算,在二进制中 1 变 0 ,0 变 1。例如110101
进行非运算后为001010
,即1010
.
左移运算就是把<<
左边的运算数的各二进位所有左移若干位,移动的位数即<<
右边的数的数值,高位丢弃,低位补0。 左移n位就是乘以2的n次方。例如:a<<4
是指把a的各二进位向左移动4位。如a=00000011(十进制3),左移4位后为00110000(十进制48)。
右移运算就是把>>
左边的运算数的各二进位所有右移若干位,>>
右边的数指定移动的位数。例如:设 a=15,a>>2 表示把00001111右移为00000011(十进制3)
能够利用按位与 &
运算取出指定位的值,具体操做是想取出哪一位的值就将那一位置为1,其它位都为0,而后同原数据进行按位与计算,便可取出特定的位.
例:
0000 0011
取出倒数第三位的值
// 想取出倒数第三位的值,就将倒数第三位的值置为1,其它位为0,跟原数据按位与运算
0000 0011
& 0000 0100
------------
0000 0000 // 得出按位与运算后的结果,便可拿到原数据中倒数第三位的值为0
复制代码
上面的例子中,咱们从0000 0011
中取值,则有0000 0011
被称之为源码.进行按位与操做设定的0000 0100
称之为掩码.
能够经过按位或 |
运算符将某一位的值设为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
它是一个联合体,联合体是一个结构占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
是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
了)
nonpointer
,表明普通的指针,存储着Class
、Meta-Class
对象的内存地址信息nonpointer
,则会进行一系列的初始化操做.其中的newisa.shiftcls = (uintptr_t)cls >> 3;
中的shiftcls
存储着Class
、Meta-Class
对象的内存地址信息,咱们来验证一下:
来咱们来对上面LLDB
相关的指令进行一波解析: 3. x/4gx obj
:表明打印obj
的4段内存信息 4. p/t
:表明打印二进制信息(还有p/o
、p/d
、p/x
分别表明八进制、十进制和十六进制打印) 5. p/t (uintptr_t)obj.class
将类信息进行二进制打印获得:$3
6. 对第一个属性isa
进行二进制打印p/t 0x001d8001000013f1
获得:$1
7. 由于此时咱们是在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`相关的指令进行一波解析:
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位
下 其实能够存储不少内容,咱们能够优化内存,在不一样的位上,放不一样的东西! 在这咱们还须要补充一下Struct
与Union
的区别:
struct
和union
都是由多个不一样的数据类型成员组成,但在任何同一时刻,union
中只存放了一个被选中的成员, 而struct
的全部成员都存在。在struct
中,各成员都占有本身的内存空间,它们是同时存在的。一个struct
变量的总长度等于全部成员长度之和。在Union
中,全部成员不能同时占用它的内存空间,它们不能同时存在。Union
变量的长度等于最长的成员的长度union
的不一样成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于struct
的不一样成员赋值是互不影响的.咱们都知道对象能够建立多个,那么类是否也能够建立多个呢? 答案是一个.怎么验证它呢? 来咱们看下面代码及打印结果:
0x1000013f0-TCJPerson
他指向元类,是由系统建立的. 咱们来看一下对象-类-元类他们之间的关系:
到此咱们知道对象的isa
指向类,类的isa
指向元类,那么元类的isa
指向哪呢?
接下来,咱们一块儿来看一下 isa
的走位:
咱们在来看下面代码打印:
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++
代码在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_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-7 3.
:
参数二类型:
sel
8-15
咱们来打印一下@
、 :
具体表明啥: