我们实例化一个NSObject
对象,最常见的代码就是
NXPerson *person = [[NXPerson alloc] init];
对于调用alloc
/init
等方法底层究竟发生了什么,你是否知道呢?今天我们就来一探究竟:
以下调试基于
Apple
开源的objc4-818.2
代码进行。
1.alloc的代码执行流程
1.1.首先在上述代码开始开始位置打第1个断点,然后运行代码使得代码断在这个断点。
1.2.然后点击alloc
,跳转到了[NSObject.mm alloc]
,并在这里设置第2个断点。
1.3.此时我们观察Xcode
左侧的调用栈
仔细看,我们会发现一个奇怪的问题:在main之后,[NSObject.mm alloc]
之前,还有两个方法的调用
1.3.1.调用objc_alloc
方法:
id objc_alloc(Class cls){
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
1.3.2. objc_alloc
的内部调用了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));
}
1.3.3.callAlloc
内部通过((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc))
调用[NSObject.mm alloc]
方法:
+ (id)alloc {
return _objc_rootAlloc(self);
}
1.3.4. alloc
内部调用了_objc_rootAlloc
方法:
id _objc_rootAlloc(Class cls){
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
1.3.5. _objc_rootAlloc
内部调用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));
}
1.3.6. callAlloc
内部调用了_objc_rootAllocWithZone
方法,这里明确了objc2忽略了allocWithZone,所以这里以及后续都传的是nil
.
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);
}
1.3.7. _objc_rootAllocWithZone
内部调用_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 {
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 {
obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
整个流程一共经历了7步,在第1.3.7步中返回了
obj
对象。也就是前面的一些步骤只是一些通用接口,根据环境变量等的不同来获得第七步所需要的一些参数,接下来我们探究obj
初始化初始化的流程
2.obj初始化流程
2.1.obj初始化流程中,我们看到对其进行了声明, 接下来执行的代码如下,根据上下问参数可以知道zone
参数为nil,所以就走到了else
分支的obj = (id)calloc(1, size)
中,calloc
函数是一个分配内存的底层函数,第二个参数是size
,表示需要开辟的内存空间的大小,第一个参数是1,表示申请一倍于size的大小。
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
}
else {
obj = (id)calloc(1, size);
}
2.2.size
的代码如下,我们根据上下参数可以看到outAllocatedSize
参数为nil,所以if条件就可以不看了。那么size = cls->instanceSize(extraBytes)
,这个就是获取初始化这个对象需要分配的内存大小了。具体怎么计算的,我们后面再继续探究,这里拿到size
之后就可以完成对象的初始化了。
size_t size;
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
打印下size
和obj
,我们发现打印的只有一个地址,没有描述信息呀,我们平时打印个对象,都是
这种格式的啊,在继续探索,我们传入的NXPerson类并没有派上用场,也就是他还没有跟类的信息绑定起来。
2.3.继续往下,断点走到了这里,具体怎么绑定的我们后面在再进行探究。我们接着再打印下obj
,到这里我们看到了已经有类名相关的信息了
obj->initInstanceIsa(cls, hasCxxDtor);
综上所属,类的实例初始化可以分为三步,第一步通过
cls->instanceSize
获得分配的内存大小;第二步通过calloc
分配内存空间;第三步通过obj->initInstanceIsa
绑定类信息到实例上。完整的流程可以查看下方的流程图:
3.重点探究
3.1内存大小的计算
调用如下objc_class
的方法instanceSize
,参数extraBytes = 0
,
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_t
的fastInstanceSize
方法,走到这一步size=40,然后再减去FAST_CACHE_ALLOC_DELTA16
=0x0008,得到32。值得注意的是align16
是表示16字节对齐,算法为(x + size_t(15)) & ~size_t(15)
.
16位对齐也就是在开辟内存空间的时候,只能开辟16的整数倍大小的内存空间,这样以16字节为基础单位开辟的内存空间,就可以保证在内存连续上的整齐度。采取16字节对齐,是因为OC的对象中,第一位isa指针,是必然存在的,而且它就占了8字节,就算你的对象中没有其他的属性了,那对象也至少要占用8位字节。如果以8位字节对齐的话,如果连续的两块内存都是没有属性的对象,那么它们的内存空间就会完全的挨在一起,是容易混乱的。采用16字节对齐,按块读取效率更高,同时还不容易混乱。
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.2.设置对象的isa,调用的函数如下
断点调试发现走到了图5打断点的位置,
newisa.setClass(cls, this);
就是用来设置对象的isa信息(isa中存储了类的信息),这样就把实例和对应的类关联起来了。
3.init流程
调试中,首先来到了[NsObject.mm init]
,紧接着调用了_objc_rootInit
方法,_objc_rootInit
方法中直接返回了obj对象自身。也就是NSObject提供的init方法,底层并没有做什么事情,直接返回了对象本身。其实这是为了给开发者提供一个初始化构造函数,方便开发者直接在init中初始化的对象的相关属性信息。
- (id)init {
return _objc_rootInit(self);
}
id _objc_rootInit(id obj){
return obj;
}
4. [[X alloc] init]
调试过程中你会发现一个很有趣的问题,就是如果你直接NXPerson *person = [[NXPerson alloc] init]
,这样调用,实际上会直接来到
id objc_alloc_init(Class cls){
return [callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/) init];
}
5.new
id
objc_opt_new(Class cls)
{
#if __OBJC2__
if (fastpath(cls && !cls->ISA()->hasCustomCore())) {
return [callAlloc(cls, false/*checkNil*/) init];
}
#endif
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(new));
}
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
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));
}
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);
}
6. 调试相关:
- 代码转cpp文件
// 简单方式
clang -rewrite-objc main.m
// 如果有引用的UIKit等框架则需要:
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
// 包含weak出错:
clang -x objective-c -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
// 第二种方式
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
// 包含weak
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
- 设置断点:
//c函数
void test(){
}
breakpoint set -n test
// oc方法, 可以连续-n
breakpoint set -n "[ViewController viewWillAppear:]"
//查看断点列表
breakpoint list
//断点禁用/启用(组/分支)
breakpoint disable/enable 1/1.1 ..
//删除断点(只能删除整个组,不带数字,删除全部)
breakpoint delete 1/2
//添加selector
breakpoint set --selector touchesBegan:withEvent:
breakpoint set --file ViewController.m --selector touchesBegan:withEvent:
//带Game的都打上断点
breakpoint set -r Game:
- p 及po
# 打印类型及地址(p是expression的简写)
p obj
# 打印对象的description方法
po obj //
- 查看堆栈
#查看当前堆栈
bt
#查看上一级/下一级
up/down
#查看指定堆栈
frame select 3
#查看指定堆栈下的参数列表
frame variable
#可以使用p指令修改参数
p str = @"123456"
# 堆栈回滚(不会继续往下执行)
thread return
- 内存断点
# 设置p1.name的断点给p1.name赋值时会断注
watchpoint set variable p1->_name
# 通过p指令获取指针的地址,然后通过地址来设置
p &p1->_name // (NSString **) $0 = 0x0000000.....30
watchpoint set expression 0x0000000.....30
- target stop-hook
//在每一个断点位置打印参数列表
target stop-hook add -o "frame variable"
//查看列表
target stop-hook list
//删除
target stop-hook delete
- image
//查看类信息
image lookup -t NXPerson
//查看image列表
image list