IOS底层(三): alloc相关: 初探 alloc, ini
首先我们先看下, 这三个打印
exp依次输出的是 内容
、内存地址
、指针地址
打印结果
result因为3个对象的指针指向同一内存空间, 所以内容
, 内存地址
是相同的, 但是对象的指针是不同的
这里留意下
%p -> &p1: 是对象的指针地址
%p -> p1: 是对象指针指向的的内存地址
由上面例子我们也可以看出, alloc
是申请内存空间, 而init
根本不会对内存空间做任何修改, p1, p2, p3 的指针地址是不同的, 但是他们都指向同一内存空间
接下来进入我们的主题看一下 alloc, init都做了些什么
我这边准备的是 objc4-781 源码项目
下载或者自己编译相可参考我之前写的 IOS底层(一): objc4-781 官方源码编译以及调试
alloc源码探索
首先先看一张图, 希望牢牢记住, 图片内容很重要
alloc+init整体源码探索流程
接下来我们拿objc4-781 源码走下流程
首先我们见一个 NSObject 类, 我这里名字叫 SAPerson
-
第一步
跳转alloc
源码实现
// alloc源码分析---第一步
+ (id)alloc {
return _objc_rootAlloc(self);
}
-
第二步
跳转_objc_rootAlloc
源码实现
// alloc源码分析---第二步
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
-
第三步
跳转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
关于上面方法解释下
//进来之后按顺序执行, 先执行 slowpath(checkNil && !cls)
这个判断
关于slowpath
和fastpath
我们点进去看下定义
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
其中的__builtin_expect
指令是由编译器gcc
引入的
1. 目的: 编译器可以对代码进行优化, 以减少指令跳转带来的性能下降。(性能优化)
2. 作用: 允许程序员将最有可能执行的分支告诉编译器
3. 写法: __builtin_expect(EXP, N)
。表示 EXP==N的概率很大。
4. fastpath: __builtin_expect(bool(x), 1)
。表示x值为真的概率很大
如果方法放在if
判断中, 执行真的if
的几率很大。
5. slowpath: __builtin_expect(bool(x), 0)
。表示x值为假的概率很大
如果方法放在if
判断中, 执行假的else
的几率很大。
6. 在日常的开发中, 也可以通过设置来优化编译器
, 达到性能优化
的目的, 设置位置 Build Settings
→Optimization Level
我们接着回头接着看callAlloc
中判断
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
因为进来的checkNil
为false, cls
存在为true
, 但!cls
为false
则slowpath
这个方法为false
不执行return nil;
继续往下走
cls->ISA()->hasCustomAWZ()
是用来判断一个类是否自定义了 +allocWithZone
方法, 我们是没有的, 所以这里走if里面的代码, 即走到_objc_rootAllocWithZone
-
第四步
跳转_objc_rootAllocWithZone
源码实现
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);
}
我们可以看到zone参数不再使用, 接下来走类创建实例对象内存空间
方法
-
第五步
跳转_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;
// 计算内存空间大小
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) {
// 将cls类与obj的指针(即: isa)关联
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);
}
重点! 重点!! 重点!!!由上面源码我们可以看到, alloc核心操作
1.cls->instanceSize
计算开辟内存空间大小
-
calloc
开辟内存, 返回地址指针(obj)
3.obj->initInstanceIsa
类与isa指针关联 (其中isa是指示关系)
下面普及一个小知识点, 针对于下面例子, 点击进入NSObject, 可看到
NSObject *objc = [NSObject alloc];
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
OBJC_ROOT_CLASS
OBJC_EXPORT
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
可看出, 一旦创建NSObject 就会占用一部分内存空间, 为什么呢?
NSObject 默认有个isa对象, 对象就是个指针, isa需要开辟8字节(arm64)
我们看个流程图便于理解下
alloc-_class_createInstanceFromZone流程
1. cls->instanceSize:计算所需内存大小
首先通过断点可看到extraBytes为0
extraBytes进入instanceSize
源码,
size_t instanceSize(size_t extraBytes) const {
// 编译器快速计算内存空间大小方法
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
// alignedInstanceSize 对齐我们当前的实例size
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
// 最小16
if (size < 16) size = 16;
return size;
}
我们先看下alignedInstanceSize
(虽然打断点, 会发现走的时候fastInstanceSize快速计算内存空间方法)
uint32_t alignedInstanceSize() const {
// word_align 字节对齐
//这个方法主要做了, 将没有字节对齐的一些东西, 调整为字节对齐返还回去
return word_align(unalignedInstanceSize());
}
那没有字节对齐的是什么东西呢?我们再点击进去
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
data是当前类的信息(类是结构体), data()->ro->instanceSize就是获取这个类所有属性内存的大小, 这里只有继承NSObject的一个属性isa——返回8字节
再看下word_align 方法
#ifdef __LP64__
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL
# define WORD_BITS 64
#else
# define WORD_SHIFT 2UL
# define WORD_MASK 3UL
# define WORD_BITS 32
#endif
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
例如: 传进来是一个8字节的isa指针, x = 8, 因为WORD_MASK = 8
则有8 + 7 & ~7, 15 & ~7
0000 1111 & ~7, 7为 0000 0111, ~7为 1111 1000
0000 1111 &
1111 1000 =
0000 1000 = 8 即返回8字节
我们看下流程图便于理解下
alloc-instanceSize不过通过断点调试,会走cache.fastInstanceSize快速计算内存空间大小方法
点击进入看下源码
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);
}
}
这里留意下__builtin_constant_p
也是编译器gcc
的内建函数 用于判断一个值是否为编译时常数,如果传入参数的值是常数,函数返回 1,否则返回 0
通过断点调试,会执行到align16
其中
#define FAST_CACHE_ALLOC_MASK 0x1ff8
#define FAST_CACHE_ALLOC_MASK16 0x1ff0
#define FAST_CACHE_ALLOC_DELTA16 0x0008
#if __LP64__
uint16_t _flags;
#endif
_flags
是个16位无符号整数 与 FAST_CACHE_ALLOC_MASK
进行按位与运算
其中 FAST_CACHE_ALLOC_MASK
= 0x1ff8 = 8184 = 1111111111000
按位与运算
1111111111000 &
0000000010000 =
0000000010000 = 16
所以size = 16
FAST_CACHE_ALLOC_DELTA16
= 0x0008
= 8
extra
= 0
size + extra - FAST_CACHE_ALLOC_DELTA16
= 16 + 0 - 8
= 8
故 align16
方法传入 8
跳转align16
源码实现
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
我们可以看到, 这是个16字节对齐方法, 跟刚才类似虽然传入的是8
x + 15 & ~15
23 & ~15, 15为 0000 1111, ~ 15为 1111 0000
0001 0111 &
1111 0000 =
00010000 = 16 可看到返回16
最后的结果为 16
,即内存的大小是以16
的倍数增加的
这里先介绍下几个知识点
结构体的内存布局依赖于CPU
、操作系统
、编译器
及编译时的对齐选项。结构体内部成员的对齐要求,结构体本身的对齐要求。最重要的有三点
(一)成员对齐对于结构体内部成员,通常会有这样的规定:各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。但是也可以看到,有时候某些字段如果严格按照大小紧密排列,根本无法达到这样的目的,因此有时候必须进行padding。各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节编译器会自动填充也就是padding。
(二)然后,还要考虑整个结构体的对齐需求。ANSI C标准规定结构体类型的对齐要求不能比它所有字段中要求最严格的那个宽松,可以更严格。实际上要求结构体至少是其中的那个最大的元素大小的整数倍。因为有时候我们使用的是结构体数组,所以结构体的大小还得保证结构体数组中各个结构体满足对齐要求,同时独立的结构体与结构体数组中单个结构体的大小应当是一致的。
(三)编译器的对齐指令。VC 中提供了#pragma pack(n)来设定变量以n字节对齐方式。n字节对齐就是说变量存放的起始地址的偏移量有两种情况:第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式,第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。结构的总大小也有个约束条件,分下面两种情况:如果n大于所有成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数。
整理一下
内存字节对齐原则
-
数据成员对齐规则
: 结构体(struct)或者联合(union)的数据成员, 第一个数据成员放在offset为0的地方, 以后每个数据成员储存的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如数据、结构体等)的整数倍开始(例如int在32位机中是4字节,则要从4的整数倍地址开始存储) -
数据成员为结构体
:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(例如:struct a里面存有struct b,b里面有char、int、double等元素,则b应该从8的整数倍开始存储) -
结构体的整体对齐规则
:结构体的总大小,即sizeof的结果,必须是其内部做大成员的整数倍,不足的要补齐
那么OC为什么需要16字节对齐
-
通常内存是由一个个字节组成的,cpu在存取数据时,并不是以字节为单位存储,而是以块为单位存取,块的大小为内存存取力度。频繁存取字节未对齐的数据,会极大降低cpu的性能,所以可以通过减少存取次数来降低cpu的开销
-
16字节对齐,是由于在一个对象中,第一个属性isa占8字节,当然一个对象肯定还有其他属性,当无属性时,会预留8字节,即16字节对齐,如果不预留,相当于这个对象的isa和其他对象的isa紧挨着,容易造成访问混乱
-
16字节对齐后,可以加快CPU读取速度,同时使访问更安全,不会产生访问混乱的情况
2. calloc:申请内存,返回地址指针
我们之前已经通过instanceSize计算了内存大小
申请size大小, 并赋值给obj, obj是指向内存地址的指针 指针是obj
obj = (id)calloc(1, size);
这里我们可以通过断点来验证calloc
在平常的开发中,一般一个对象的打印的格式都是类似于这样的<NSObject: 0x01111111f>(是一个指针)。为什么这里不是呢?
- 主要是因为
objc地址
还没有与传入 的cls
进行关联, - 同时印证了
calloc
的根本作用就是开辟内存
3. obj->initInstanceIsa:类与isa关联
经过calloc,内存已经申请好了,类也已经传入进来了,接下来就需要将 类与 地址指针 即isa指针进行关联,其关联的流程图如下所示
obj->initInstanceIsa
主要过程就是初始化一个isa指针,并将isa指针指向申请的内存地址,在将指针与cls类进行关联
可以通过断点调试来印证上面的说法,在执行完initInstanceIsa后,在通过po obj可以得出一个对象指针
initInstanceIsa
总结
通过对alloc
源码的分析,可以得知alloc的主要目的就是开辟内存,而且开辟的内存需要使用16字节对齐算法,现在开辟的内存的大小基本上都是16的整数倍, 核心步骤有3步:计算 -- 申请 -- 关联
init源码探索
先看下init
源码, 有两种类方法init
, 实例方法init
类方法init
+ (id)init {
return (id)self;
}
实例方法init
- (id)init {
return _objc_rootInit(self);
}
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;
}
也可以看到返回的是传入的self 本身
new源码探索
先看底层, 可看到本身调用了callAlloc, init函数, 可以得出new等价于 [XXX alloc ] init]
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
但是针对于重写初始化方法
- (instancetype)init {
self = [super init];
if (self != nil) {
NSLog(@"1");
}
return self;
}
- (instancetype)initWithText {
self = [super init];
if (self != nil) {
NSLog(@"2");
}
return self;
}
TestObj *obj1 = [[TestObj alloc] initWithText]; 走2
TestObj *obj2 = [TestObj new]; 走1
new调用只会走1, 不难看出如果
- 子类没有重写 init方法, new调用父类 init 方法
- 子类重写 init方法, new调用子类 init 方法
- 子类重写 init方法并自定义init方法, new的话不会调用, 所以alloc + init更为灵活一些, 扩展性好