首先我们先看一段代码:
#ifdef DEBUG
#define LGNSLog(format, ...) printf("%s\n", [[NSString stringWithFormat:format, ## __VA_ARGS__] UTF8String]);
#else
#define LGNSLog(format, ...);
#endif
LGPerson *p1 = [LGPerson alloc];
LGPerson *p2 = [p1 init];
LGPerson *p3 = [p1 init];
LGNSLog(@"%@ - %p - %p",p1,p1,&p1);
LGNSLog(@"%@ - %p - %p",p2,p2,&p2);
LGNSLog(@"%@ - %p - %p",p3,p3,&p3);
打印的三个数据分别为:对象
,指针地址
,对象地址
。
查看打印结果如下:
可以看到,第一、二个参数一样,第三个参数一样,我们用一张图来表示
LGPerson原理.png
原理
虽然创建了三个对象p1,p2,p3,但是他们的指针都指向同一个类,所以第一、第二个参数一致,但是创建的3个对象分别在不同的地址,所以第三个参数不一致。
Tips:我们拿到p1,p2,p3的地址,进行相减,可以看到,每个地址都是相差8个字节。这是因为栈内存是连续的,由于对象是一个结构体,结构体是一个指针,占8字节,所以每个对象之间差8个字节,可以节省内存空间。
alloc原理
当我们想查看alloc
方法的具体执行流程的时候,往往就会被苹果给阻挠掉,因为苹果完全给封装死了,根本看不到,那么我们通过符号断点可以大致查看流程的走向。
具体操作如下:
- 选择
Symbolic BreakPoint...
Symbolic BreakPoint.png - 输入
alloc
alloc.png - 运行程序(注意alloc在ViewDidLoad断点执行完成时再打开,否则其他的创建方法会一直调用这里),进入到
[NSObject alloc]
方法,LGPerson继承自NSObject方法
image.png - 点击step into,进入
_objc_rootAlloc
image.png - 点击step into,进入
_objc_rootAllocWithZone
image.png - 这就是alloc的大致执行流程
但是,这样还是看不到具体方法,那么我们就需要看objc的源码了,我们下载最新的代码objc4-781。
推荐大家去看Style_月月的iOS-底层原理 03:objc4-781 源码编译 & 调试这篇博客,里面对objc4-781的编译调试讲的非常仔细。
源码解读
alloc流程图如下,其中最关键的步骤在虚线框里:
alloc.png
-
alloc,我们点击
alloc
可以看到,跳到了
+ (id)alloc {
return _objc_rootAlloc(self);
}
- _objc_rootAlloc,第二步跳到_objc_rootAlloc
id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
- callAlloc,第三步跳到callAlloc
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
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,第四步跳到_objc_rootAllocWithZone
NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
- _class_createInstanceFromZone
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;
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
// alloc 开辟内存的地方
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);
} 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);
}
上面这个方法有3个方法比较重要
cls->instanceSize
cls->instanceSize,这个方法是分配对象的内存。
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(extraBytes)
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);
}
}
经过断点,我们可以看到extra
为0,所以走到
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
这一步,通过取与运算后size = 16
,接下来,
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
这一步是干嘛呢?我们继续断点调试,size_t x = 8
,通过
// 8 + 15 = 23
x + size_t(15)
得到23
,那么23用二进制表示为
// 23转换成2进制
0000 0000 0001 0111
而15
的二进制表示为
// 15转换成2进制
0000 0000 0000 1111
然后~size_t(15)
表示15的二进制取反
// 15二进制取反
1111 1111 1111 0000
最后(x + size_t(15)) & ~size_t(15)
表示23和15取反的结果去与,得到
//23转换成2进制
0000 0000 0001 0111
//与运算
&
// 15二进制取反
1111 1111 1111 0000
//&运算得到的结果
= 0000 0000 0001 0000
最终得到0000 0000 0001 0000
,最后结果转换为十进制为16
,把后4位都抹掉了,只剩下第5位开始,都为16的倍数。
所以,align16()方法就是16字节的对齐,这就是所谓的字节对齐。
字节对齐
-
字节对齐的解释
现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特 定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。 -
为什么要16字节对齐?
- 一个对象有
isa
指针,占8字节,如果是8字节对齐,会因为没有预留空间导致一个isa
紧挨着另一个isa
,造成访问混乱。所以需要进行内存预留。 - 一个对象最小为8字节,所以预留的空间最小为16的倍数,方便读写。
- 16字节对齐后,可以
加快CPU读取速度
,同时使访问更安全,不会产生访问混乱的情况
- 一个对象有
内存开辟的影响因素
现在我们给LGPerson
添加几个属性
//名称
@property (nonatomic,strong) NSString *name;
//昵称
@property (nonatomic,strong) NSString *nickName;
//爱好
@property (nonatomic) int hobby;
再看我们当前的size
以上可以验证,对于一个对象来说,
属性是影响内存的大小的因素
。同时也印证了字节对齐,32是16的2倍
obj = (id)calloc(1, size)
这个方法就是向内存中申请 大小 为 size的内存,并赋值给obj,因此 obj是指向内存地址的指针
obj = (id)calloc(1, size)
我们可以验证一下
如上图,在执行
calloc
方法前,obj
为nil,之后为内存地址,证明obj为指向内存地址的指针。
obj->initInstanceIsa(cls, hasCxxDtor)
这个方法就是将类和指针进行绑定,通过打印
可以看到obj方法是指向了
LGPerson
类。
init方法
从上面的alloc
方法可以看到,我们通过alloc
方法已经可以拿到得到一个指向LGPerson
类的指针。那么alloc
方法是干嘛的呢?
我们通过流程图查看。
- 点击
init
,进入_objc_rootInit
方法
- (id)init {
return _objc_rootInit(self);
}
- 点击
_objc_rootInit
,返回obj
自己
id
_objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.
// Many classes do not properly chain -init calls.
return obj;
}
可以看到,其实init方法就是返回了对象本身。所以init是一个构造方法
,是通过工厂方法
,主要是用于给用户提供构造方法入口。
这里能使用id强转的原因,主要还是因为内存字节
对齐后,可以使用类型强转为你所需的类型。
new方法
通过代码可以看到,new
方法会调用callAlloc
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
所以,new
等同于alloc init
方法。
但是,一般我们不建议直接使用new
方法,因为 init
方法可以自定义参数等操作,而new
方法不行。
网友评论