首先先看个例子:
TestObj *obj1 = [TestObj alloc];
TestObj *obj2 = [obj1 init];
TestObj *obj3 = [obj1 init];
NSLog(@"%@ - %p - %p", obj1, obj1, &obj1);
NSLog(@"%@ - %p - %p", obj2, obj2, &obj2);
NSLog(@"%@ - %p - %p", obj3, obj3, &obj3);
他们的打印情况为
问题8-答案首先了解下, 三个打印依次是打印 对象内容
, 对象指针指向的内存地址
, 指针地址
① %@
: 打印的是对象的内容
② %p → &p1
: 打印的是指向对象内存的指针地址
③ %p → p1
: 打印的是指针地址
那么
-
第一列: obj1, ob2, obj3 相等, 打印的是对象内容都是
TestObj
开辟的内存空间 -
第二列: obj1, obj2, obj3 相等, 留意下
init
是不会对指针进行操作的, 而%p打印的是对象指针
, obj1、obj2、 obj3, 三个都是指向同一片内存区域( [TestObj alloc]开辟的), 所以不变 -
第三列: &obj1, &ob2, &obj3 不相等, 打印的是指针地址, obj1、obj2、 obj3三个不同指针, 地址不同
(例子详细内容可看 IOS面试题 --- 类相关中的问题8
)
其实这里面就引出一个问题? alloc
究竟做了什么?
alloc探索
通常我们对alloc
了解是, 它做了初始化
, 开辟了一块内存空间, 那它底层究竟怎么做的呢? 首先我们需要一份objc-818
源码:
编译好的objc4-818源码下载: https://github.com/Lv100-ShawnAlex/objc4-818.git
源码探索常用方法: https://www.jianshu.com/p/63988f940c90
源码有了, 先建立一个SATest
类继承NSObject
, 并做alloc
初始化
cmd + 点击
查看alloc
源码
依次点击进入
+ (id)alloc {
return _objc_rootAlloc(self);
}
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
// OBJC有1.0版本2.0版本, 现在我们大部门都用2.0版本
#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);
}
// 发送 alloc 消息
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
走到这里是时候, 其实就会很迷茫, 因为我们不知道究竟走了哪个判断?那么可以加个断点运行一下
源码加断点运行这块留意下因为系统很多对象alloc方法, 都会走这里, 所以要确保是我们建的类SATest
进入
错误的例子
错误的例子正确的例子
最好main中的对象先加断点, 等main中断点确定走到之后再加/重新打开对应方法的断点
main先加断点 正确的例子
po
: lldb命令读出/输出值, 打印对象
当我们在这边打断点跟流程的时候, 可看到, 流程先走了return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
, 然后又来一遍走了
if (fastpath(!cls->ISA()->hasCustomAWZ()))
这个判断里面的return _objc_rootAllocWithZone(cls, nil);
为什么会走2次呢? 我们这里可以打开汇编(Debug→Debug Workflow → Always Show Disassembly
)看一下
可看到, 他并没有走alloc
, 而走了objc_alloc
, 这个objc_alloc是什么呢? 我们先看下源码, 在NSObjec.mm
中可以找到调用了callAlloc
方法
id
objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
那么我们 cmd + 点击
进入的方法走没走呢?
+ (id)alloc {
return _objc_rootAlloc(self);
}
我们依次(id)objc_alloc(Class cls)
, + (id)alloc
, callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
分别加个断点, 判断下是这里代码走入情况
可以看到依次走入顺序
objc_alloc
→ callAlloc
→ objc_msgSend
→ alloc
→ _objc_rootAlloc
→ callAlloc
→ _objc_rootAllocWithZone
接下来介绍下slowpath
和fastpath
(后面也会用到)
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
其中__builtin_expect
指令是由编译器gcc
引入的。 __builtin_expect(EXP, N)
, 表示 EXP==N
的概率很大。
目的 : 编译器可以对代码进行优化, 以减少指令跳转带来的性能下降。(性能优化)
作用 : 允许程序员将最有可能执行的分支告诉编译器
写法 : __builtin_expect(EXP, N)
。表示 EXP==N
的概率很大。
-
fastpath
:__builtin_expect(bool(x), 1)
。表示x值为真的概率很大如果方法放在if判断中, 执行真的if的几率很大。 -
slowpath
:__builtin_expect(bool(x), 0)
。表示x值为假的概率很大如果方法放在if判断中, 执行假的else的几率很大
在日常的开发中, 也可以通过设置来优化编译器 , 达到性能优化的目的, 设置位置 Build Settings
→Optimization Level
看下这里走2次原因, 涉及知识点比较多, 此处仅仅是探索猜测, 后续补完 (这块我也是在探索, 可直接跳到下面alloc 三大核心
)
fastpath(!cls->ISA()->hasCustomAWZ()
bool hasCustomAWZ() const {
return !cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ);
}
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
// 类或父类有默认 alloc/allocWithZone 实现
// 注意,它存储在元类中。
#define FAST_CACHE_HAS_DEFAULT_AWZ (1<<14)
SATest
继承NSObject
, NSObject
有个默认的isa
, 但此时isa
并没有做任何初始化(objc_object::initIsa
), 也可以理解成空白isa
。
-
系统默认走了(这块需要看下
llmv
)objc_alloc
→callAlloc
-
第一次进入:
-
cls
有值if (slowpath(checkNil && !cls)) return nil;
这个判断不会走 -
fastpath(!cls->ISA()->hasCustomAWZ()
, 因为这个值FAST_CACHE_HAS_DEFAULT_AWZ
存储在元类
中, 此时父类链
,元类
都没关联确认, 所以cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ)
为false
,!
一下为true
, 外层又!
了一下还是为false
, 所以首次不走这里。 -
allocWithZone
传入的也是false, 所以走消息转发((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc))
-
- 消息转发:
-
快速查找
: cache中并没有传入类的alloc方法, 所以直接进慢速查找流程 -
慢速查找
: 快速查找没有找到走lookUpImpOrForward
慢速查找流程- 跟流程走这里
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
- 跟流程走这里
-
这块跟一下 realizeAndInitializeIfNeeded_locked
源码
/***********************************************************************
* realizeAndInitializeIfNeeded_locked
* Realize the given class if not already realized, and initialize it if
* not already initialized.
* inst is an instance of cls or a subclass, or nil if none is known.
* cls is the class to initialize and realize.
* initializer is true to initialize the class, false to skip initialization.
**********************************************************************/
static Class
realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize)
{
runtimeLock.assertLocked();
// 判断当前类是否实现
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
// runtimeLock may have been dropped but is now locked again
}
// 判断当前类是否初始化
if (slowpath(initialize && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
// runtimeLock may have been dropped but is now locked again
// If sel == initialize, class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
return cls;
}
// Locking: caller must hold runtimeLock; this may drop and re-acquire it
static Class initializeAndLeaveLocked(Class cls, id obj, mutex_t& lock)
{
return initializeAndMaybeRelock(cls, obj, lock, true);
}
/***********************************************************************
* class_initialize. Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first.
* inst is an instance of cls, or nil. Non-nil is better for performance.
* Returns the class pointer. If the class was unrealized then
* it may be reallocated.
* Locking:
* runtimeLock must be held by the caller
* This function may drop the lock.
* On exit the lock is re-acquired or dropped as requested by leaveLocked.
**********************************************************************/
static Class initializeAndMaybeRelock(Class cls, id inst,
mutex_t& lock, bool leaveLocked)
{
lock.assertLocked();
ASSERT(cls->isRealized());
if (cls->isInitialized()) {
if (!leaveLocked) lock.unlock();
return cls;
}
// Find the non-meta class for cls, if it is not already one.
// The +initialize message is sent to the non-meta class object.
Class nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// Realize the non-meta class if necessary.
if (nonmeta->isRealized()) {
// nonmeta is cls, which was already realized
// OR nonmeta is distinct, but is already realized
// - nothing else to do
lock.unlock();
} else {
nonmeta = realizeClassMaybeSwiftAndUnlock(nonmeta, lock);
// runtimeLock is now unlocked
// fixme Swift can't relocate the class today,
// but someday it will:
cls = object_getClass(nonmeta);
}
// runtimeLock is now unlocked, for +initialize dispatch
ASSERT(nonmeta->isRealized());
initializeNonMetaClass(nonmeta);
if (leaveLocked) runtimeLock.lock();
return cls;
}
继续跟源码我们看到走了这里
realizeClassMaybeSwiftAndUnlock
static Class
realizeClassMaybeSwiftAndUnlock(Class cls, mutex_t& lock)
{
return realizeClassMaybeSwiftMaybeRelock(cls, lock, false);
}
/***********************************************************************
* realizeClassMaybeSwift (MaybeRelock / AndUnlock / AndLeaveLocked)
* Realize a class that might be a Swift class.
* Returns the real class structure for the class.
* Locking:
* runtimeLock must be held on entry
* runtimeLock may be dropped during execution
* ...AndUnlock function leaves runtimeLock unlocked on exit
* ...AndLeaveLocked re-acquires runtimeLock if it was dropped
* This complication avoids repeated lock transitions in some cases.
**********************************************************************/
static Class
realizeClassMaybeSwiftMaybeRelock(Class cls, mutex_t& lock, bool leaveLocked)
{
lock.assertLocked();
if (!cls->isSwiftStable_ButAllowLegacyForNow()) {
// Non-Swift class. Realize it now with the lock still held.
// fixme wrong in the future for objc subclasses of swift classes
realizeClassWithoutSwift(cls, nil);
if (!leaveLocked) lock.unlock();
} else {
// Swift class. We need to drop locks and call the Swift
// runtime to initialize it.
lock.unlock();
cls = realizeSwiftClass(cls);
ASSERT(cls->isRealized()); // callback must have provoked realization
if (leaveLocked) lock.lock();
}
return cls;
}
这里走了 if (!cls->isSwiftStable_ButAllowLegacyForNow())
判断进入realizeClassWithoutSwift(cls, nil);
这里比较长, 我就截取部分图片, 可看到处理了一些rw, ro信息
处理rw, ro信息
往下可看到进入了这里针对于父类链
, 元类
做一些判断处理
继续往下, 可看到进入这里initClassIsa
inline void
objc_object::initClassIsa(Class cls)
{
if (DisableNonpointerIsa || cls->instancesRequireRawIsa()) {
initIsa(cls, false/*not nonpointer*/, false);
} else {
initIsa(cls, true/*nonpointer*/, false);
}
}
initisa
之后进入isa
的核心代码initisa
, 这里主要是对isa
做一些操作处理
可看到这里的确对isa
做了一些字段赋值
之后进入的object_getClass
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
inline Class
objc_object::getIsa()
{
if (fastpath(!isTaggedPointer())) return ISA();
extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
uintptr_t slot, ptr = (uintptr_t)this;
Class cls;
slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
cls = objc_tag_classes[slot];
if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
cls = objc_tag_ext_classes[slot];
}
return cls;
}
因为isTaggedPointer
此时为0
, 所以!
为真, 直接走return ISA()
, 后续把一些剩余代码走完, 然后走第二次进入cAlloc
- 第二次进入:
第二次的calloc
来自于alloc
而不是objc_alloc
, 由于消息发送的是alloc
方法
-
cls
有值if (slowpath(checkNil && !cls)) return nil;
这个判断不会走 -
fastpath(!cls->ISA()->hasCustomAWZ()
,元类
,父类链
确认完成,ISA
中信息已更改所以FAST_CACHE_HAS_DEFAULT_AWZ
cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ)
为true
,!
一下为false
, 外层又!
了一下还是为true
, 第二次走了这里
两次打印isa bits也会发现bits不一样
两次打印isa
(此处仅仅是探索猜测, 后续补完)
alloc 三大核心
我们接着跟下源码
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);
}
alloc核心代码_class_createInstanceFromZone
/***********************************************************************
* class_createInstance
* fixme
* Locking: none
*
* Note: this function has been carefully written so that the fastpath
* takes no branch.
**********************************************************************/
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
// hasCxxCtor: C++/OC的析构器(类似于dealloc)占1位
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) {
// 核心代码之一, 指针与类关联
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核心代码: instanceSize:
计算开辟内存大小
我们也是_class_createInstanceFromZone
里打断点, 看一下源码流程
- 第一步肯定是留意一下是否是我们定义的类进入
2.开始定义一些是否包含析构的 bool
hasCxxCtor
: C++/OC的析构器(类似于dealloc)占1位
-
第一个bool
hasCxxCtor
为false
:cxxConstruct
为传入进来的为true,cls->hasCxxCtor()
是否有析构器false, 然后&
一下为false
-
第二个bool
hasCxxDtor
为false
:cls->hasCxxCtor()
同上为false
-
第三个bool
fast
:false
为cls->canAllocNonpointer()
判断当前类是否已经关联isa
, 为此并没有false
3.定义size
主要用于储存后面的开辟空间大小值
- 核心代码之一:
size = cls->instanceSize(extraBytes);
, 计算开辟内存空间大小并赋给size
(extraBytes
: 是否需要额外空间大小, 为0)
首先我们了解下内存大小是由属性/成员变量
决定, 这里可由data()->ro()->instanceSize
源码知道
// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
// 其中 instanceSize 是实例变量大小, 并且是编译完的干净内存大小
return data()->ro()->instanceSize;
}
接下来, 我们跟下源码进入 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.
// CF要求所有对象至少为16字节。不足16补齐16
if (size < 16) size = 16;
return size;
}
依次跟断点进入走了align16
方法
传入参数 size = 16
, extra == 0
, #define FAST_CACHE_ALLOC_DELTA16 0x0008 = 8
,
其中__builtin_constant_p
是 gcc
的内建函数 用于判断一个值是否为编译时常数,如果参数的值是常数,函数返回 1,否则返回 0, 这里很显然传入的是个变量所以走了下面的判断。(如果会走这里的话, 只能是 extra != 0
)
#define FAST_CACHE_ALLOC_MASK16 0x1ff0 = 8176 = 0001 1111 1111 0000
_flags
系统会计算多次, 最后为32784 = 1000 0000 0001 0000
两种做下 &
运算
0001 1111 1111 0000
&
1000 0000 0001 0000
=
0000 0000 0001 0000 = 16
cache.fastInstanceSize(extraBytes)
往下走, 这里align16
是一个向上取整的16字节对齐
方法
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
~
: 非操作
, 0变1, 1变0
&
: 与操作
, 1 & 0 = 0; 1&1 = 1; 0&0 = 0
这个方法核心在于转成二进制按位与
例子1: x = 16 , 16二进制为 0001 0000, 15 二进制为0000 1111, ~15为1111 0000
16 + 15 = 31 = 0001 1111
那么有
0001 1111
&
1111 0000
=
0001 0000 = 16
例子2: x = 7 , 二进制为 0000 0111, 15 二进制为0000 1111, ~15为1111 0000
7 + 15 = 22 = 0001 0110
那么有
0001 0110
&
1111 0000
=
0001 0000 = 16, 不足16倍数
的向上取整取到16
倍数为止, 当然size_t(7)为8字节对齐。
所以上面传入16 + 0 - 8 = 8 计算完size = 16
返回, 继续往下走可看到走了第二个核心代码calloc
。
当然我们也看一下如果计算空间大小没走, 后面方法怎么走的
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;
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
# define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
这里可看出是个8
字节对齐。这块我的理解是, obj对象实际占用是按8字节对齐,但之后系统还是会以16对齐分配大小,而快速创建一步到位, 直接按16字节对齐分配, 减少后面系统判断处理。
calloc
我们放在下一篇详细讲, 总结下instanceSize
流程图
为什么需要8字节/16字节存储, 直接按本身字节数存不就好了吗? 例如内存条这样存放
存完之后读取, 假如第一个是4字节, 第二个是8字节, 第三个是8字节, 第四个是4字节...
当CPU开始读数据时候读完第一个4字节, 立马要变换读取长度8字节, 当读到第四个时候又要变化读取长度4字节读取, 这样每次都要做判断改变读取长度, 繁琐且会影响CPU速度。CPU意思: 大哥你别这样存了, 我难受!!!
因为通常存储指针特别多, 即8字节比较多, 就规定存储时候都是8字节为一段进行存储, 不足也为8, 以空间换取时间
。
接下来, 我们验证下是否真的是按我们想象的一样8字节对齐
不同类型字节占用建立一个SATest
类继承于NSObject
, 并建立一些属性
@interface SATest : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic, strong) NSString *hobby;
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
#import "SATest.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
SATest *test = [[SATest alloc] init];
test.name = @"ShawnAlex";
test.age = 18;
test.height = 180;
test.hobby = @"女";
NSLog(@"%p", test);
}
return 0;
}
先介绍下我下面用到的一些lldb
命令
-
x
: 读取内存段 -
po
: 读取一下内容 -
x/4gx
: 以4个片段打印内存段 -
x/5gx
: 以5个片段打印内存段
可看到依次读出相应数据, 那么为什么没有hobby
呢?
因为4gx
只会按4个片段读取, 第五个需要5gx
(大于4都可)
我们再增加一个bool b
, 重新读取一下, 可看到还是5个片段
那么bool去哪了呢?
B100BFC2-F629-4CE0-8D2F-B61E7084BB9D.png其实这里, 系统帮我们优化了一下, 将int
和bool
放在一起, 减少浪费
总结:
alloc流程图alloc
开辟内存方法
-
底层顺序
objc_alloc
→callAlloc
→objc_msgSend
→alloc
→_objc_rootAlloc
→callAlloc
→_objc_rootAllocWithZone
→_class_createInstanceFromZone
-
_class_createInstanceFromZone
是alloc
的核心代码-
instanceSize
: 计算开辟空间大小 -
calloc
: 开辟内存 -
initInstanceIsa
: 对象与指针isa绑定
-
网友评论