OC alloc 底层探索

2021-06-07  本文已影响0人  HotPotCat

一、alloc对象的指针地址和内存

有如下代码:

//alloc后分配了内存,有了指针。
//init所指内存地址一样,init没有对指针进行操作。
HPObject *hp1 = [HPObject alloc];
HPObject *hp2 = [hp1 init];
HPObject *hp3 =  [hp1 init];
NSLog(@"%@-%p",hp1,hp1);
NSLog(@"%@-%p",hp2,hp2);
NSLog(@"%@-%p",hp3,hp3);

输出:

<HPObject: 0x600000f84330>-0x600000f84330
<HPObject: 0x600000f84330>-0x600000f84330
<HPObject: 0x600000f84330>-0x600000f84330

说明alloc后进行了内存分配有了指针,而init后所指内存地址一致,所以init没有对指针进行操作。
修改NSLog内容如下:

NSLog(@"%@-%p &p:%p",hp1,hp1,&hp1);
NSLog(@"%@-%p &p:%p",hp2,hp2,&hp2);
NSLog(@"%@-%p &p:%p",hp3,hp3,&hp3);

输出:

<HPObject: 0x600000e7c2c0>-0x600000e7c2c0 &p:0x7ffeefbf40d8
<HPObject: 0x600000e7c2c0>-0x600000e7c2c0 &p:0x7ffeefbf40d0
<HPObject: 0x600000e7c2c0>-0x600000e7c2c0 &p:0x7ffeefbf40c8

这就说明hp1hp2hp3都指向堆空间的一块区域。而3个指针本身是在栈中连续开辟的空间,从高地址->低地址。
那么alloc是怎么开辟的内存空间呢?

二、底层探索思路

  1. 断点结合Step into instruction进入调用堆栈找到关键函数:

    image.png
    这里以alloc为例:
    image.png
    找到了objc_alloc关键韩素,然后对它下符号断点:
    image.png
    找到了最中调用的是libobjc.A.dylibobjc_alloc:`。
  2. 下断点后通过汇编查看调用流程Debug->Debug workflow->Always Show Disassembly

    image.png
    可以直接跟进去看调用情况。
  3. 通过已知符号断点确定未知符号。
    直接alloc下符号断点跟踪:

    image.png

三、alloc源码分析

通过上面的分析已经能确定allocobjc框架中,正好苹果开源了这块代码,源码:objc源码地址:Source Browser
最好是自己能编译一份能跑通的源码(也可以直接github上找别人编译好的)。当然也可以根据源码下符号断点跟踪调试。由于objc4-824目前下载不了,这里以objc4-824.2为例进行调试。

HPObject定义如下:

@interface HPObject : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;

@end

3.1 alloc

直接搜索alloc函数的定义发现在NSObject.mm 2543,通过断点调试类。
调用alloc会首先调用objc_alloc:

id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

callAlloc会走到调用alloc分支。

+ (id)alloc {
    return _objc_rootAlloc(self);
}

alloc直接调用了_objc_rootAlloc

id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

这里没什么好说的只是方法的一些封装,具体实现要看callAlloc

⚠️:明明有alloc方法为什么先调用的是objc_alloc?文章后面会有分析

3.2 callAlloc

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    //表示值为假的可能性更大。即执行else里面语句的机会更大
    if (slowpath(checkNil && !cls)) return nil;
    //hasCustomAWZ方法判断是否实现自定义的allocWithZone方法,如果没有实现就调用系统默认的allocWithZone方法。
    //表示值为真的可能性更大;即执行if里面语句的机会更大
    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));
}

slowpath:表示值为假的可能性更大。即执行else里面语句的机会更大。
fastpath:表示值为真的可能性更大;即执行if里面语句的机会更大。
OBJC2:是因为有两个版本。Legacy版本(早期版本,对应Objective-C 1.0) 和 Modern版本(现行版本Objective-C 2.0)。

⚠️:自己实现一个类的allocWithZone alloc分支就每次都被调用了。

3.3 _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);
}

_objc_rootAllocWithZone直接调用了_class_createInstanceFromZone

3.4 allocWithZone

// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}

_objc_rootAllocWithZone直接调用了_objc_rootAllocWithZone,与上面的3.3中的逻辑汇合了。

3.5 _class_createInstanceFromZone

最终会调用_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
    //判断当前class或者superclass是否有.cxx_construct构造方法的实现
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    //判断当前class或者superclass是否有.cxx_destruct析构方法的实现
    bool hasCxxDtor = cls->hasCxxDtor();
    //标记类是否支持优化的isa
    bool fast = cls->canAllocNonpointer();
    size_t size;
    //通过内存对齐得到实例大小,extraBytes是由对象所拥有的实例变量决定的。
    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;
    }
    //初始化实例isa指针
    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.6 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.
    if (size < 16) size = 16;
    return size;
}

3.6.1 alignedInstanceSize

#ifdef __LP64__
#   define WORD_MASK 7UL
#else
#   define WORD_MASK 3UL

uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

uint32_t unalignedInstanceSize() const {
    ASSERT(isRealized());
    return data()->ro()->instanceSize;
}
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
 Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

那么为什么以8字节对齐,最后最小分配16呢?
分配16是为了做容错处理。以8字节对齐(选择8字节是因为8字节类型是最常用最多的)是以空间换取时间,提高CPU读取速度。当然这过程中会做一定的优化。

3.6.2 fastInstanceSize

bool hasFastInstanceSize(size_t extra) const
{
    if (__builtin_constant_p(extra) && extra == 0) {
        return _flags & FAST_CACHE_ALLOC_MASK16;
    }
    return _flags & FAST_CACHE_ALLOC_MASK;
}

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);
    }
}
static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

3.6.3 setInstanceSize

setInstanceSize代码如下:

void setInstanceSize(uint32_t newSize) {
    ASSERT(isRealized());
    ASSERT(data()->flags & RW_REALIZING);
    auto ro = data()->ro();
    if (newSize != ro->instanceSize) {
        ASSERT(data()->flags & RW_COPIED_RO);
        *const_cast<uint32_t *>(&ro->instanceSize) = newSize;
    }
    cache.setFastInstanceSize(newSize);
}

size变化只会走会更新在缓存中。那么调用setInstanceSize的地方如下:

instanceSize对于HPObject而言分配内存大小应该为8(isa) + 8(name)+4(age)= 20根据内存对齐应该分配24字节。

image.png
而根据调试instanceSize最终返回了32。为什么?参考iOS内存对齐

3.7 initInstanceIsa

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

initInstanceIsa最终会调用initIsainitIsa最后会对isa进行绑定:

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); 
    
    isa_t newisa(0);

    if (!nonpointer) {
        newisa.setClass(cls, this);
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());


#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
#endif
        newisa.extra_rc = 1;
    }

    // This write must be performed in a single store in some cases
    // (for example when realizing a class because other threads
    // may simultaneously try to use the class).
    // fixme use atomics here to guarantee single-store and to
    // guarantee memory order w.r.t. the class index table
    // ...but not too atomic because we don't want to hurt instantiation
    isa = newisa;
}

3.8 alloc调用流程图

alloc调用流程

四、llvm优化alloc

为什么调用alloc最终调用了objc_alloc

4.1 objc源码中探索分析

在源码中我们点击alloc会进入到+ (id)alloc方法,但是在实际调试中却是先调用的objc_alloc,系统是怎么做到的呢?

image.png
在源码中可以看到上图中的官方注释。所以这里难道是SELIMP进行了交换?
尝试去源码中找哪里进行了objc_alloc替换,最终在void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)方法中找到了如下代码:
fixupMessageRef(refs+i);

fixupMessageRef实现中有如下核心代码:

image.png
可以看到在这个方法中进行了imp的重新绑定将alloc绑定到了objc_alloc上面。当然retainrelease等都进行了同样的操作。
既然在_read_images中出现问题的时候尝试进行fixup,那么意味着正常情况下在_read_images之前llvm的编译阶段就完成了绑定。

4.2 llvm源码探索分析

那么直接在llvm中搜索objc_alloc,在ObjCRuntime.h中发现了如下注释:

  /// When this method returns true, Clang will turn non-super message sends of
  /// certain selectors into calls to the corresponding entrypoint:
  ///   alloc => objc_alloc
  ///   allocWithZone:nil => objc_allocWithZone

这说明方向没有错,最中在CGObjC.cpp中找到了如下代码:

  case OMF_alloc:
    if (isClassMessage &&
        Runtime.shouldUseRuntimeFunctionsForAlloc() &&
        ResultType->isObjCObjectPointerType()) {
        // [Foo alloc] -> objc_alloc(Foo) or
        // [self alloc] -> objc_alloc(self)
        if (Sel.isUnarySelector() && Sel.getNameForSlot(0) == "alloc")
          return CGF.EmitObjCAlloc(Receiver, CGF.ConvertType(ResultType));
        // [Foo allocWithZone:nil] -> objc_allocWithZone(Foo) or
        // [self allocWithZone:nil] -> objc_allocWithZone(self)
        if (Sel.isKeywordSelector() && Sel.getNumArgs() == 1 &&
            Args.size() == 1 && Args.front().getType()->isPointerType() &&
            Sel.getNameForSlot(0) == "allocWithZone") {
          const llvm::Value* arg = Args.front().getKnownRValue().getScalarVal();
          if (isa<llvm::ConstantPointerNull>(arg))
            return CGF.EmitObjCAllocWithZone(Receiver,
                                             CGF.ConvertType(ResultType));
          return None;
        }
    }
    break;

可以看出来alloc最后执行到了objc_alloc。那么具体的实现就要看CGF.EmitObjCAlloc方法:

llvm::Value *CodeGenFunction::EmitObjCAlloc(llvm::Value *value,
                                            llvm::Type *resultType) {
  return emitObjCValueOperation(*this, value, resultType,
                                CGM.getObjCEntrypoints().objc_alloc,
                                "objc_alloc");
}

llvm::Value *CodeGenFunction::EmitObjCAllocWithZone(llvm::Value *value,
                                                    llvm::Type *resultType) {
  return emitObjCValueOperation(*this, value, resultType,
                                CGM.getObjCEntrypoints().objc_allocWithZone,
                                "objc_allocWithZone");
}

llvm::Value *CodeGenFunction::EmitObjCAllocInit(llvm::Value *value,
                                                llvm::Type *resultType) {
  return emitObjCValueOperation(*this, value, resultType,
                                CGM.getObjCEntrypoints().objc_alloc_init,
                                "objc_alloc_init");
}

这里可以看到alloc以及objc_alloc_init相关的逻辑。这样就实现了绑定。那么系统是怎么走到OMF_alloc的逻辑的呢?
通过发送消息走到这块流程:

CodeGen::RValue CGObjCRuntime::GeneratePossiblySpecializedMessageSend(
    CodeGenFunction &CGF, ReturnValueSlot Return, QualType ResultType,
    Selector Sel, llvm::Value *Receiver, const CallArgList &Args,
    const ObjCInterfaceDecl *OID, const ObjCMethodDecl *Method,
    bool isClassMessage) {
    //尝试发送消息
  if (Optional<llvm::Value *> SpecializedResult =
          tryGenerateSpecializedMessageSend(CGF, ResultType, Receiver, Args,
                                            Sel, Method, isClassMessage)) {
    return RValue::get(SpecializedResult.getValue());
  }
  return GenerateMessageSend(CGF, Return, ResultType, Sel, Receiver, Args, OID,
                             Method);
}

五、内存分配优化

HPObject *hpObject = [HPObject alloc];
NSLog(@"%@:",hpObject);

对于hpObject我们查看它的内存数据如下:

(lldb) x hpObject
0x6000030cc2e0: c8 74 e6 0e 01 00 00 00 00 00 00 00 00 00 00 00  .t..............
0x6000030cc2f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
(lldb) p 0x000000010ee674c8
(long) $4 = 4544951496

可以打印的isa4544951496并不是HPObject。因为这里要&mask,在源码中有一个&mask结构。arm64`定义如下:

#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     define ISA_MASK        0x007ffffffffffff8ULL
#   else
#     define ISA_MASK        0x0000000ffffffff8ULL

这样计算后就得到isa了:

(lldb) po 0x000000010ee674c8 & 0x007ffffffffffff8
HPObject

HPObjetc添加属性并赋值,修改逻辑如下:

@interface HPObject : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) double height;
@property (nonatomic, assign) BOOL marry;

@end

调用:

    HPObject *hpObject = [HPObject alloc];
    hpObject.name = @"HotpotCat";
    hpObject.age = 18;
    hpObject.height = 180.0;
    hpObject.marry = YES;
image.png
这个时候发现agemarry存在了isa后面存在了一起。
那么多增加几个BOOL属性呢?
@interface HPObject : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) double height;
@property (nonatomic, assign) BOOL marry;
@property (nonatomic, assign) BOOL flag1;
@property (nonatomic, assign) BOOL flag2;
@property (nonatomic, assign) BOOL flag3;

@end

BOOL属性都设置为YES查看内存地址:

image.png

发现所有BOOL值都放在了一起。那么再增加一个呢?

image.png

可以看到int类型的age单独存放了,5bool值放在了一起。这也就是内存分配做的优化。

六、init源码探索

既然alloc已经完成了内存分配和isa与类的关联那么init中做了什么呢?

init
init源码定义如下:

- (id)init {
    return _objc_rootInit(self);
}

_objc_rootInit

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中调用了_objc_rootInit,而_objc_rootInit直接返回obj没有做任何事情。就是给子类用来重写的,提供接口便于扩展。所以如果没有重写init方法,那么在创建对象的时候可以不调用init方法。

有了alloc底层骚操作的经验后,打个断点调试下:

NSObject *obj = [NSObject alloc];
[obj init];

这里allocinit分开写是为了避免被优化。这时候调用流程和源码看到的相同。

那么修改下调用逻辑:

NSObject *obj = [[NSObject alloc] init];

alloc init一起调用后会先进入objc_alloc_init方法。

objc_alloc_init

id
objc_alloc_init(Class cls)
{
    return [callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/) init];
}

objc_alloc_init调用了callAllocinit

⚠️不同版本的系统下调用逻辑会有不同。

七、new源码探索

既然alloc initnew都能创建对象,那么它们之间有什么区别呢?
new

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}

alloc init一起调用的不同点是checkNil传递的是fasle
源码调试发现new调用的是objc_opt_new

// Calls [cls 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));
}

objc2下也是callAllocinit

objc_alloc_initobjc_opt_new的绑定与objc_alloc的实现相同。同样的实现绑定的还有:

const char *AppleObjCTrampolineHandler::g_opt_dispatch_names[] = {
   "objc_alloc",//alloc
   "objc_autorelease",//autorelease
   "objc_release",//release
   "objc_retain",//retain
   "objc_alloc_init",// alloc init
   "objc_allocWithZone",//allocWithZone
   "objc_opt_class",//class
   "objc_opt_isKindOfClass",//isKindOfClass
   "objc_opt_new",//new
   "objc_opt_respondsToSelector",//respondsToSelector
   "objc_opt_self",//self
};

总结

alloc调用过程:

上一篇 下一篇

猜你喜欢

热点阅读