iOS底层原理探索篇 主要是围绕底层进行
源码分析
-LLDB调试
-源码断点
-汇编调试
,让本身之后回顾复习的😀😀数组目录以下:缓存
iOS底层原理探索 — 开篇bash
iOS底层原理探索 — alloc&init探索iphone
经过前面篇章的探索,咱们已成功的从对象过渡到类了.但在探索类以前,还须要补充一下咱们在前面篇章中没有细讲的一些小细节.post
咱们在iOS底层原理探索 — alloc&init探索一文中留下了一个细节没有细说,就是在分析alloc
源码分析流程的时候,在调用callAlloc
方法时,咱们只是简单的说了:此方法内部有一系列的判断条件,其中因为方法canAllocFast()
的内部调用了bits.canAllocFast()
,其返回值为固定值false
,因此能够肯定以后建立对象只会走class_createInstance
方法.即:callAlloc
中if (fastpath(cls->canAllocFast()))
方法不走直接走的else
后面的代码.那么为何会这样呢?来看源码:测试
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
if (slowpath(checkNil && !cls)) return nil;
#if __OBJC2__ //这个表示object-c 2.0 版本才有的功能
/*
这里的hasDefaultAWZ()方法是用来判断当前class是否有默认的allocWithZone。
if (fastpath(!cls->ISA()->hasCustomAWZ())):
意思就是若是该类实现了allocWithZone方法,那么就不会走if里的逻辑,直接走如下逻辑
if (allocWithZone) return [cls allocWithZone:nil];
*/
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);//initInstanceIsa 里面是初始化 isa 指针的操做。
return obj;
}
else {
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
复制代码
fastpath(!cls->ISA()->hasCustomAWZ())
的决定条件就是你是否有重写allocWithZone
的方法:即if (fastpath(!cls->ISA()->hasCustomAWZ())):
意思就是若是该类实现了allocWithZone方法,那么就不会走if里的逻辑,直接走if (allocWithZone) return [cls allocWithZone:nil]
;fastpath(cls->canAllocFast())
就是关于宏定义的设置:咱们沿着源码点进去能够看到:bool canAllocFast() {
assert(!isFuture());
return bits.canAllocFast();
}
复制代码
顺着bits.canAllocFast();
点进去能够看到:#if FAST_ALLOC
size_t fastInstanceSize()
{
assert(bits & FAST_ALLOC);
return (bits >> FAST_SHIFTED_SIZE_SHIFT) * 16;
}
void setFastInstanceSize(size_t newSize)
{
// Set during realization or construction only. No locking needed.
assert(data()->flags & RW_REALIZING);
// Round up to 16-byte boundary, then divide to get 16-byte units
newSize = ((newSize + 15) & ~15) / 16;
uintptr_t newBits = newSize << FAST_SHIFTED_SIZE_SHIFT;
if ((newBits >> FAST_SHIFTED_SIZE_SHIFT) == newSize) {
int shift = WORD_BITS - FAST_SHIFTED_SIZE_SHIFT;
uintptr_t oldBits = (bits << shift) >> shift;
if ((oldBits & FAST_ALLOC_MASK) == FAST_ALLOC_VALUE) {
newBits |= FAST_ALLOC;
}
bits = oldBits | newBits;
}
}
bool canAllocFast() {
return bits & FAST_ALLOC;
}
#else // 通常都会走这里
size_t fastInstanceSize() {
abort();
}
void setFastInstanceSize(size_t) {
// nothing
}
// 通常流程都会走这个false的返回
bool canAllocFast() {
return false;
}
#endif
复制代码
通常都会走#else
后面的代码,也就是bool canAllocFast(){return false}
.为何会这样呢?,这就要去看条件控制:#if FAST_ALLOC
这个宏定义的走向了. 在全局搜索宏定义FAST_ALLOC
,发现#define FAST_ALLOC (1UL<<2)
而这个宏定义外面还加了一层条件判断:#if !__LP64__
...
#elif 1
...
#else
...
#define FAST_ALLOC (1UL<<2)
#endif
复制代码
由于咱们的环境都是在64
位环境下,因此能够判断上面的判断只会走#elif 1
里面的代码,而#define FAST_ALLOC
的定义是在#else
里面,即FAST_ALLOC
永远都不会define
了.即只会走bool canAllocFast(){return false}
,进而就有callAlloc
中if (fastpath(cls->canAllocFast()))
方法不走,直接走的else{}
里面的代码.即走的下面红框里面的代码 优化
咱们在iOS底层原理探索 一 isa原理与对象的本质一文中有分析到,isa
的结构实际上是一个联合体,而联合体有一大特性,就是其内部属性是共享同一片内存的,也就是说属性之间都是互斥的.
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
复制代码
所以也就能解释初始化isa
的时候,一个分支是赋值cls
属性,一个分支是赋值bits
属性了.
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;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
isa = newisa;
}
}
复制代码
咱们在探索类和元类的时候,对于其建立时机还不是很清楚,这里咱们先抛出结论:类和元类是在编译期建立的,即在进行alloc
操做以前,类和元类就已经被编译器建立出来了. 那么如何来证实呢,咱们有两种方式来证实:
LLDB
指令打印类和元类指针main
函数开始以前打上断点,也就没有来到
TCJPerson *obj = [TCJPerson alloc];
,可是咱们经过
LLDB
能打印出
TCJPerson
的类和元类.这就证实了,类和元类的建立时机是在编译期.
MachoView
软件辅助证实:MachoView 密码:kx8c 编译项目后,使用MachoView
打开程序二进制可执行文件查看:
经过上面两种方式证实了:类和元类的建立时机是在编译期.
a
和
b
都是被赋值为10,可是
a
和
b
内存地址是不同的,这种方式被称为
值拷贝
.
obj1
和
obj2
对象不光自身内存地址不同,连指向的对象的内存地址也不同,这种方式被称为
指针拷贝
或
引用拷贝
.
咱们能够用一幅图来总结上面的两个例子:
&a
和&a[0]
的地址是相同的.即首地址就表明数组的第一个元素的地址.0x7ffeefbff400
和第二个元素地址0x7ffeefbff404
相差4个字节,也就是int
的所占的4字节.d
、d+1
、d+2
这个地方的指针相加就是偏移地址.地址加1就是偏移,偏移一个位数所在元素的大小.OC
中的类其实也是一种对象,怎么来证实呢,很简单,咱们只须要用clang
命令重写咱们的OC代码将其转化为C++
代码看其底层便可.
TCJPerson
对象,并获取到TCJPerson
的类,而后利用LLDB
指令查看NSObject
,继续输出第三个发现输出不了.Class superclass
,它表明的是继承关系,也即证实了TCJPerson
是继承自NSObject
的.OC
代码转化为C++
代码帮助分析原文件main.c
:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface TCJPerson : NSObject
@end
@implementation TCJPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
TCJPerson *obj = [TCJPerson alloc];
Class objClass = object_getClass(obj);
NSLog(@"%@ - %p", obj, objClass); //0x00007ffffffffff8ULL
}
return 0;
}
复制代码
在终端执行clang
指令:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
复制代码
便可将OC
原文件main.c
转化为C++
文件mian.cpp
文件后可看到:
至此,咱们能够得出一个结论,Class
类型在底层是一个结构体类型的指针,这个结构体类型为objc_classs
. 咱们再在libObjc
的源码中能够找到objc_classs
的详细定义:
objc_classs
的定义,咱们能够知道,
objc_classs
是继承于
objc_object
的.这就证实了
万物皆对象,也从本质上说明
类是一种对象,而且第一个属性是从
objc_object
上继承而来的
isa
. 除了
isa
,类还包含了
superclass
父类:表达继承关系;
cache
:方法缓存重要结构体;
bits
:存储数据的结构体.
至此咱们能够总结得出:
类是一种对象,而且帮咱们定义了一些属性和方法.
又OC
是对C
的底层封装,进而有下面的关系:
C | OC |
---|---|
objc_object | NSObject |
objc_class | NSObject(Class) |
最后咱们知道Class
的基本结构类型为:
isa
是
Class
?
isa
是能够由Class
接收的.isa
是用来返回类的,后面是经过nonpointer
区分纯净isa
和优化的isa
.return (Class)(isa.bits & ISA_MASK)
,进行了Class类型强转.
OC
中的类都会有属性及成员变量,那么它们到底是怎么存在于类里面的呢?
这里咱们须要对类的内存结构有一个比较清晰的认识:
类的内存结构 | 大小(字节) |
---|---|
isa | 8 |
superclass | 8 |
cache | 16 |
前面两个的大小很好理解,由于isa
和superclass
都是结构体指针,而在arm64
环境下,一个结构体指针的内存占用大小为8字节.而第三个属性cache
则须要咱们进行抽丝剥茧了. 来看源码:
cache
属性实际上是
cache_t
类型的结构体,其内部有一个
8
字节的结构体指针,有2个各为4字节的
mask_t
.因此加起来就是16个字节,也就是说前三个属性总共的内存偏移量为 8 + 8 + 16 = 32 个字节,32 是 10 进制的表示,在 16 进制下就是 20.
利用LLDB
命令来探索类结构的第四个属性bits
.
bits
的指针地址,就须要进行指针偏移,这里进行一下16进制下的地址偏移计算:
0x100001200 + 0x20 = 0x100001220
复制代码
咱们继续打印这个地址有:
bits
并非一个对象,而是一个结构体,这里须要进行强转一下:
objc_class
源码可知,其内部有
data()
方法:
data()
方法拿到
class_rw_t
:
libObjc
中关于
class_rw_t
的源码:得知
class_rw_t
也是一个结构体.
properties
里面,咱们在打印一下:
properties
:
const class_ro_t *ro;
.咱们来到其源码:
ro
的类型是
class_ro_t
结构体,它包含了
baseMethodList
、
baseProtocols
、
ivars
、
baseProperties
等属性.咱们刚才在
class_rw_t
中没有找到咱们声明在
TCJPerson
类中的实例变量
titleStr
和属性
helloName
,那么但愿就在
class_ro_t
身上了,咱们接着打印看看它的内容:
baseProperties
里面,咱们接着打印看看:
helloName
被找到了,就存放在
class_ro_t
的
baseProperites
里面.咦,怎么没有看到咱们的实例变量
titleStr
?咱们从
$10
的
count
为 1 能够得知确定不在
baseProperites
里面根.据名称咱们猜想应该是在
$8
的
ivars
里面.那咱们接着打印:
titleStr
也找到了,那为何这里的
count
是2呢?咱们接着打印第二个元素看看:
_helloName
.这一结果证明了编译器会帮助咱们给属性
helloName
生成一个带下划线前缀的实例变量
_helloName
. 至此,咱们能够处处一下结论:
class_rw_t
是能够在运行时来拓展类的一些属性、方法和协议等内容.class_ro_t
是在编译时就已经肯定了的,存储的是类的成员变量、属性、方法和协议等内容.研究完了类的属性是怎么存储的,咱们再来看看类的方法又是怎么存储的. 在TCJPerson
类里面增长一个readBook
的实例方法和一个writeBook
的类方法.
class_ro_t
中的
baseMethodList
的内容:
readBook
方法被找出来了,这说明
baseMethodList
就是存储实例方法的地方.咱们接着打印剩下的内容:
baseMethodList
中除了咱们的实例方法
readBook
外,还有属性
helloName
的
getter
和
setter
方法以及一个
C++
析构方法.而咱们的类方法
writeBook
并无被打印出来.那么类方法存储在哪呢?
咱们上面已经获得了属性,实例方法是怎么样存储的了,可是还留下了一个疑问点,就是类方法是怎么存储的,接下来咱们用 Runtime
的 API
来实际测试一下.
testInstanceMethod_classToMetaclass
方法测试的是分别从类和元类去获取实例方法、类方法的结果.由打印结果咱们能够知道:
readBook
是实例方法,存储于类对象的内存中,不存在于元类对象中.而 writeBook
是类方法,存储于元类对象的内存中,不存在于类对象中.readBook
是类对象的实例方法,跟元类不要紧;writeBook
是元类对象的实例方法,因此存在元类中. 咱们再测试另外的一个方法:
class_getClassMethod
获取writeBook
是有值的,而获取 readBook
是没有值的;对于元类对象来讲,经过 class_getClassMethod
获取writeBook
也是有值的,而获取 readBook
是没有值的.这里第一点很好理解,可是第二点会有点让人糊涂,不是说类方法在元类中是体现为对象方法的吗?怎么经过 class_getClassMethod
从元类中也能拿到 writeBook
,咱们进入到 class_getClassMethod
方法内部能够解开这个疑惑:
class_getClassMethod
方法底层其实调用的是
class_getInstanceMethod
,而
cls->getMeta()
方法底层的判断逻辑是若是已是元类就返回,若是不是就返回类的
isa
.这也就解释了上面的
writeBook
为何会出如今最后的打印中了. 除了上面的这种方式,咱们还能够经过
isa
的方式来验证类方法存放在元类中.
经过 isa
在类对象中找到元类.
打印元类的 baseMethodsList
. 咱们也来验证一下: 首先咱们获取objClass
的内存段:
& ISA_MASK
拿到其元类,而且打印其内存段:
bits
属性:这里进行一下16进制下的地址偏移计算: 0x100001280 + 0x20 = 0x1000012a0
复制代码
LLDB
来打印类和元类的指针,或者用 MachOView
软件查看二进制可执行文件class_ro_t
结构中存储了编译时肯定的属性、成员变量、方法和协议等内容,而且对于属性helloName
:底层编译会生成相应的setter
、getter
方法,且帮咱们转化为_helloName
,对于成员变量titleStr
:底层编译不会生成相应的setter
、getter
方法,且没有转化为_titleStr
在这一章中咱们完成了对 iOS
中类的结构的探索,下一章咱们将对类的缓存进行探索,敬请期待~