美文网首页
001-OC对象原理探究

001-OC对象原理探究

作者: BBLv | 来源:发表于2021-06-07 08:54 被阅读0次

    alloc探索

    通过这篇文章可以知道什么:

    • alloc方法是如何开辟内存的,开辟了多少内存?
    • 在alloc过程中内存、指针有什么关系?
    • alloc是如何开辟内存空间的?
    • 如何探索底层源码?
    • 底层源码怎么获取,例如(Objc4/)
    • alloc源码的详细分析
    • alloc加载流程图
    • 不同模式下的编译器优化,在汇编层面上是怎样的?
    • 什么是字节对齐?字节对齐的好处?
    从启动流程开始搞起:
    启动流程.jpg

    加载过程

    绿色部分为程序启动部分,由_dyld_start(dyld开始加载)开始到dyld::main再到dyld_initialzeMainExecutableImageLoader::*等等,代表着主程序由_dyld_start开始,到main等为启动做准备,包括加载动态库,共享内存,全局C++函数的析构,还有一系列的初始化,注册回调函数都在此步骤内完成。这里并不是此篇文章的详细说明,只做引入功能。
    红色部分为对象加载过程的开始,通过App启动一系列函数之后会进入到libSystem_initializer -> libdispatch_init -> GCD环境的准备 -> _objc_init

    OC对象的初始化

    1、oc对象是如何开辟的?
    2、alloc、init、new是如何操作的?
    3、在此过程中内存、指针有什么关系?


    p1与p2.jpg

    p1与p2的打印结果为什么一样?

    LGPerson *p1 = [LGPerson alloc];
    

    得出结论:

    • p1此刻拥有了内存
    • p1拥有了指针的指向
    LGPerson *p2 = [p1 init];
    LGPerson *p3 = [p1 init];
    

    由于打印对象p2=p3,得出结论:

    • p2、p3所指向的内存地址是一样的
    • init未对指针进行任何操作
    &p3.jpg

    通过alloc之后开辟了一块内存空间,*p1 *p2 *p3代表3个指针地址,并且同时指向了同一块内存空间,由上图内存地址0x7ffeede340a8 0x7ffeede340a0 0x7ffeede34098得出结论:

    • *p1*p2*p3属于栈上内存地址
    • *p1*p2*p3是连续的地址空间,每个相隔8字节(解释:0x98+0x8=0xa0、0xa0+0x8=0xa8)

    图形详解

    关键点:连续开辟,指向同一块空间

    对象内存开辟与指向.png

    alloc是如何做到的?
    init是真的什么都不做吗?

    如何探索源码:

    方式一:

    • 真机模式

    第一步:在工程的LGPerson *p1 = [LGPerson alloc];处设置断点

    真机+源码探索00.jpg

    第二步:将工程运行,停在断点处之后,按住control + Step into进入到汇编代码

    真机+源码探索01.jpg 真机+源码探索02.jpg

    这里发现了objc_alloc方法,看到了熟悉的代码,变得很兴奋,再次按住control + Step into

    真机+源码探索03.jpg

    结果是无法再看到有效的信息了,原因是真机模式下Apple做了限制

    • 模拟器模式

    第一步:在工程的LGPerson *p1 = [LGPerson alloc];处设置断点,
    第二步:将工程运行,停在断点处之后,按住control + Step into进入到汇编代码

    模拟器+源码探索00.jpg 模拟器+源码探索01.jpg

    第三步:将看到的objc_alloc添加符号断点,具体步骤如下:

    模拟器+源码探索02.jpg

    第四步:继续按住control + Step into向下走

    模拟器+源码探索03.jpg

    这里看到了libobjc.A.dylib objc_alloc,看到了接下来会调用的方法_objc_rootAllocWithZone,objc_msgSend,这里豁然开朗,终于找到了objc_alloc底层的源码,来自于哪个动态库,为向下探索提供了更多的线索!

    方式二:
    通过汇编流程的方式去查看:
    第一步,设置工程的模式,选择菜单栏Debug->Debug wrokflow->Always Show Disassembly,将工程运行

    汇编+源码探索00.jpg 汇编+源码探索01.jpg

    第二步:此时断点断在了LGPerson处,按住control + Step into,去找到objc_alloc

    汇编+源码探索02.jpg

    第三步:设置符号断点:


    模拟器+源码探索02.jpg

    第四步:再次按住control + Step into调试objc_alloc

    汇编+源码探索03.jpg

    这里看到了libobjc.A.dylib objc_alloc,看到了接下来会调用的方法_objc_rootAllocWithZone,objc_msgSend,这里豁然开朗,终于找到了objc_alloc底层的源码,来自于哪个动态库,为向下探索提供了更多的线索!

    第三种:
    直接通过已知符号断点设定,直接进入,通常配合第二种使用

    底层源码在哪里?

    Apple开元源码汇总:https://opensource.apple.com/

    Apple开源源码汇总.jpg

    [Source Browser:https://opensource.apple.com/tarballs/]

    Source Browser.jpg

    我这里查看的源码是objc4-818.2.tar.gz,来自于LGCocci老师,那个最靓的男人:https://github.com/LGCooci/objc4_debug,有需求的伙伴可以自行获取,素质三连

    Source Browser objc4:818.jpg

    alloc源码分析:

    首先打开源码项目objc4-818.2,搜索alloc,查看一下alloc源码执行的详细流程:

    源码alloc.jpg

    1、进入_objc_rootAlloc方法

    源码_objc_rootAlloc.jpg

    2、进入callAlloc方法

    源码callAlloc.jpg

    3、这里有#if __OBJC2__判断,如何验证走哪个方法进入_objc_rootAllocWithZone

    源码_objc_rootAllocWithZone.jpg

    4、进入_class_createInstanceFromZone方法

    源码_class_createInstanceFromZone.jpg

    alloc加载流程图

    alloc加载流程图.png

    编译器优化

    <span id="callalloc">进入到BuildSetting下,找到Optimization level(GCC_OPTIMIZATION_LEVEL),意思是指定生成的代码针对速度和二进制大小进行优化的程度</span>

    设置 参数
    None[-O0] 编译器不会优化代码。编译器的目标是蒋迪编译成本并使调试产生预期的结果,通常在Debug模式下使用。
    Fast[-O,O1] 快速,优化编译器需要编译的时间更久,对大型函数需要更多的内存。编译器会尝试减少代码大小和执行时间,而不执行任何需要大量编译时间的优化。
    Faster[-O2] 更快速,编译器执行几乎所有不涉及空间速度权衡的受支持优化。使用此设置,编译器不会执行循环展开或函数内联或寄存器命名,次设置会增加编译时间和生成代码的性能。
    Fastest[-O3] 设置指定的所有优化,并打开函数内联和寄存器重命名选项,此设置可能会产更大的二进制文件
    Fastest,Smallest[-Os] 最快、最小,此设置启用所有通常不会增加代码大小的更快的优化,它还会做减少代码大小的进一步优化

    尝试写一个小例子,设置不同的优化方案,用来验证编译器优化情况:

    #import <UIKit/UIKit.h>
    #import "AppDelegate.h"
    
    //MARK: - 测试函数
    int lgSum(int a, int b){
        return a+b;
    }
    
    int main(int argc, char * argv[]) {
        int a = 10;
        int b = 20;
        int c = lgSum(a, b);
        NSLog(@"查看编译器优化情况:%d",c);
        return 0;
    }
    
    • None[-O0]


      编译器优化.jpg
    执行结果:不优化的情况下所有信息在寄存器中显示完整,我分别打印了a、b、计算钱与计算后的x0寄存器,结果如下:
    
    编译器优化-None.jpg
    • Fastest,Smallest[-Os]


      编译器优化-Fastest,Smallest.jpg

    执行结果:优化掉了a、b两个变量,甚至连lgSum函数都被优化掉了,只剩下了一个结果0x1e存在w8寄存器中了。

    结论:由于选择了Fastest,Smallest[-Os]优化方案,导致lgSum函数没有了,同理callAlloc函数也是一样的。

    alloc做了什么?

    源码解析


    源码_class_createInstanceFromZone详解.jpg

    alloc内存是如何开辟的,开辟了多少内存

    开辟内存是由instanceSize这个函数决定的,进入到这个函数,首先判断是否有缓存,如果有执行cache.fastInstanceSize函数直接返回,内存开辟结束,获得该对象内存大小。如果没有缓存,会执行alignedInstanceSize函数,执行word_align函数,此函数的参数是函数unalignedInstanceSize,而这个函数通过data()->ro()->instanceSize获取到对象的实例大小,也就是说,最终开辟内存空间的大小是根据对象的成员变量大小决定的。

    默认情况下,不创建任何成员变量,类开辟的内存空间是8字节,因为继承NSObject造成的,NSObject内有成员变量isa,由于isa的类型是结构体指针,所以isa是8字节,所以创建一个新的对象,没有任何成员变量,默认内存大小是8字节

    //对象
    struct class_ro_t {
        uint32_t flags;
        uint32_t instanceStart;
        uint32_t instanceSize;
    #ifdef __LP64__
        uint32_t reserved;
    #endif
    
    //_class_createInstanceFromZone内开辟内存的大小
    size = cls->instanceSize(extraBytes);
    
    // May be unaligned depending on class's ivars.
    uint32_t unalignedInstanceStart() const {
        ASSERT(isRealized());
        return data()->ro()->instanceStart;
    }
    
    // Class's instance start rounded up to a pointer-size boundary.
    // This is used for ARC layout bitmaps.
    uint32_t alignedInstanceStart() const {
        return word_align(unalignedInstanceStart());
    }
    
    // 可能是不对齐的,取决于类的成员变量(ivars)
    uint32_t unalignedInstanceSize() const {
        ASSERT(isRealized());
        return data()->ro()->instanceSize;
    }
    
    // 类的 ivar 大小向上舍入到指针大小边界。
    uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
    }
    
    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;
    }
    

    字节对齐

    字节对齐的优势:以空间换取时间

    • 8字节来自于NSObject对象的isa结构体指针
    • 不满16等于16
    • 如果大于16会根据对象在内存分布中的特性来决定(根据传入的x,取x的整数倍),如果传入8,最后得到的是8的倍数
    
    #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
    
    //字节对齐算法
    //define WORD_MASK = 7
    static inline uint32_t word_align(uint32_t x) {
        return (x + WORD_MASK) & ~WORD_MASK;
    }
    

    工程调试

    1、验证代码是否执行#if __OBJC2__判断内函数

    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_rootAlloccallAlloc_objc_rootAllocWithZone等方法添加符号断点,并且将项目运行起来

    排查错误-断点调试00.jpg

    按照想象如期的停在了_objc_rootAlloc方法处,通过register read读取寄存器,但是问题是并没有发现LGPerson这个class,原因是LGPerson还没有初始化,解决方法先将断点放过去,让系统的方法执行完,等执行到LGPerson时候再调试

    排查错误-断点调试01.jpg

    执行的结果是_objc_rootAllocWithZone先会被执行,然后再执行objc_msgSend,这也就证明了#if __OBJC2__判断为true,执行了内部的代码。
    但是细心你会发现,当前正在被执行的这个函数是_objc_rootAlloc并不是源码中的callAlloc,这是为什么?

    问题:

    当前简书页面内跳转失效了上文中两个对应关系如下:

    • 如何验证走哪个方法 -> 工程调试部分
    • 并不是源码中的callAlloc,这是为什么?->编译器优化部分

    相关文章

      网友评论

          本文标题:001-OC对象原理探究

          本文链接:https://www.haomeiwen.com/subject/ddbceltx.html