第一节课 OC对象原理(上)
其实我们最开始学习iOS都应该是从创建对象开始的吧?还记得当初教我的老师开玩笑的说到,以后没对象就自己创建一个,要多少有多少~虽然我们一直在使用,但是很少去想要弄明白它的原理,接下来我们就一步步去深入了解这个要多少有多少的对象是怎么创建的!
alloc对象的指针地址和内存
先来看这么一个例子,这三个变量的内容、内存地址与指针地址有什么区别呢?
HZMPerson *p1 = [HZMPerson alloc];
HZMPerson *p2 = [p1 init];
HZMPerson *p3 = [p1 init];
NSLog(@"%@-%p-%p",p1,p1,&p1);
NSLog(@"%@-%p-%p",p2,p2,&p2);
NSLog(@"%@-%p-%p",p3,p3,&p3);
分别输出,看下结果
通过看输出我们可以得出几个结论:
- 首先
alloc
大家都知道是开辟内存空间
,p1、p2、p3我们也知道都指向了这片内存空间
,所以内容跟内存地址是相同的
。 - 我们也可以看到这三个
地址是连续开辟的
68+8=70、70+8=78
但是我们也好奇init的作用是什么?alloc到底是做了什么开辟的内存呢?这就是我们本文主要探索的地方啦~
底层探索的三种方法
1:符号断点:libobjc.A.dylib
objc_alloc`
在alloc这行添加一个断点
通过按住control + step into
进入
进入后如下图
再下一个objc_alloc符号断点,符号断点后显示了 objc_alloc所在的源码库(需要去Apple相应的开源网址下载objc源码进行更深入的探索)
2: 汇编跟流程
开启
Debug汇编
模式,再次运行进入汇编界面通过按住
control
,点击step into
键往下执行到callq
语句,callq就是调用
语句,可以看到调用的是objc_alloc符号
,底层中调用符号还原到上层中就是调用函数或方法
。继续step into
,又来到了我们熟悉的界面
这个时候我们就知道下一步该做什么了~还是通过符号断点,去定位到源码库
3:已知符号断点确定未知
我们什么也不知道,但是至少知道是一个alloc
函数,所以直接下一个alloc的符号断点
libobjc.A.dylib`+[NSObject alloc],又一次定位到了底层源码
汇编结合源码调试分析
定位到底层源码之后就是看源码啦~
源码下载地址根据自己电脑的版本找相应的源码下载,我的电脑是10.15的,就按照 :macOS --> 10.15 --> 选择10.15 --> 搜索 objc
如果想知道最新的源码合集就去这里
下载下来编译会有各种错误,详细的调整过程可以参照→月月博客每一步介绍的都很详细
配置好后我们搜索alloc函数
//alloc源码分析-第一步
+ (id)alloc {
return _objc_rootAlloc(self);
}
进入_objc_rootAlloc
//alloc源码分析-第二步
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
继续进
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)// alloc 源码 第三步
{
#if __OBJC2__ //有可用的编译器优化
// checkNil 为false,!cls 也为false ,所以slowpath 为 false,假值判断不会走到if里面,即不会返回nil
if (slowpath(checkNil && !cls)) return nil;
//判断一个类是否有自定义的 +allocWithZone 实现,没有则走到if里面的实现
// 第一次在这里 hasCustomAWZ 会返回 false, 没有默认的 allocWithZone 实现, 所以第一次进入会判断失败, 不会调用 _objc_rootAllocWithZone, 而是会走对 objc_msgSend 的调用; 第二次进入时, 才会调用 _objc_rootAllocWithZone
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
// No shortcuts available. // 没有可用的编译器优化
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
到这里我们不知道这个分支走的是哪个了,怎么办?在我们刚才的代码里面添加上这两个分支的符号断点就好啦~
这里是执行到_objc_rootAllocWithZone
我们可以看出这部分的流程大体就是:
alloc->_objc_rootAlloc->callAlloc->_objc_rootAllocWithZone
alloc的主线流程
通过上述分析我们已经大概能了解了一部分alloc的流程了,接下来我们继续探索下alloc到底做了什么
_objc_rootAllocWithZone继续进入,
return _class_createInstanceFromZone(cls, 0, nil, OBJECT_CONSTRUCT_CALL_BADALLOC);
跳转至_class_createInstanceFromZone的源码实现,这部分是alloc源码的核心操作
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
ASSERT(cls->isRealized());//检查是否已经实现
// Read class's info bits all at once for performance
//一次性读取类的位信息以提高性能
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
//计算需要开辟的内存大小,传入的extraBytes 为 0
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);//申请内存
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);//将 cls类 与 obj指针(即isa) 关联
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
通过对上面的源码分析,发现主要的部分有三部分:
cls->instanceSize:计算需要开辟的内存空间大小
calloc:申请内存,返回地址指针
obj->initInstanceIsa:将 类 与 isa 关联
小结:
通过上述两段我们可以分析出alloc的主线流程图为
补充:当我们第一次进行alloc的时候实际是先走的objc_alloc->callAlloc->objc_msgSend->alloc,然后才再次走的_objc_rootAlloc。
这一点我们可以通过在alloc处下断点后查看汇编发现,也可以在源码调试的过程中分别在alloc处与objc_alloc处分别下断点进行查看。
字节对齐
1、跳转至instanceSize的源码实现
inline size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
通过断点调试,会执行到cache.fastInstanceSize方法,快速计算内存大小
PS:其实当我们第一次启动的时候是不会执行fastInstanceSize而是走到下面去计算内存大小,当我们后面再次使用的时候才会去快速计算内存大小
2、跳转至fastInstanceSize的源码实现,通过断点调试,会执行到align16
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
3、跳转至align16的源码实现,这个方法是16字节对齐算法
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
整体流程图为
小结:
1:一开始就有8字节,是继承于NSObject
2:Size实际返回的大小最少16,所以是16字节对齐(isa结构体指针8字节,还有8字节是预留的)
3:字节对齐算法(x + WORD_MASK) & ~WORD_MASK
~(取反)
的规则是:1变为0,0变为1
&(与)
的规则是:都是1为1,反之为0
例子x=8 (8+7)&~7-> 15&~7
15 = 00001111
7 = 00000111
~7 = 11111000
15&~7 = 00001000 = 8
存取值8字节对齐 取8的整数
为什么是8的倍数 ?我们来看下对齐的原理
ps:内部的成员变量以字节对齐,真正的内存大小以内存对齐,也就是对象计算实际内存是8字节对齐,但是在系统分配的时候是按照16字节对齐分配的
字节对齐的原理
CPU在读取过程中如果按照对象实际大小读取,会导致频繁切换读取大小,造成CPU计算过程缓慢
,如果我们每次固定字节大小,以这个固定值读取,虽然会有资源浪费,但是CPU效率显著提升
也就是用空间换时间
。
为什么是8呢?
因为我们整个内存里面,8字节的是最多的
double b; //8字节
int c; //4字节
short d; //2字节
char a; //1字节
指针 //8字节
如果说我们一个int 一个char 这两个能不能放到一个8字节里呢?
我们先在源码的main.m中添加几个属性看下。
这是我们的输出结果,我们可以看到,每个单独的对象都占用了8字节的大小,那我们把Bool类型与Int类型放到一块试一下
我们发现这部分的数据变了,但是又跟之前的有点像,分别输出下,就是我们的Int与BOOL,已经合并成一个8字节存储,证明不足8字节的两个对象是可以存储在一个8字节中的。
下篇文章我们再来看看内存对齐