iOS怎么捏泥人-alloc初探

iOS开发刚开始做的是什么?从初步、比较普遍的结果来看,可能是做一个稳定的好用的App给用户,这App可以是一个小世界,通过用户点击行为等产生一系列的响应,进而成为工具、游戏等。
写到这里,我们以捏泥巴类比,今天我也想探索下苹果生态系统下的App是怎么来的,苹果是依据什么原则去捏对象,进而形成App的呢?

一、 先来一个常见的现象

我们来看下一段代码:

//YPerson的.h声明文件
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN

@interface YPerson : NSObject

@property (nonatomic, assign) int age;

@property (nonatomic, assign) double height;

@property (nonatomic, copy) NSString *name;

@end

NS_ASSUME_NONNULL_END

第二段代码:

#import "ViewController.h"
#import "YPerson.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    NSMutableArray *array = [NSMutableArray array];
    
    YPerson *p = [[YPerson alloc] init];
    
    YPerson *p2 = [[YPerson alloc] init];

}
@end

二、方法调用底层初步思考(该用什么工具、从哪里入手):

1. 问题:@selector(array)和@selector(alloc)是否殊途同归?

以上体现了一个常见的YPerson类的创建方式,即Alloc、init方式,以及对象Array的表面上看非alloc的创建方式,这里先把结果,实际上@selector(array)这个方法最终也是调用的alloc,这是怎么知道的呢?

2.探究工具:符号断点与混编

这是我们通过这里方法似乎有很多,只讲一种我觉得方便的,先断点调试到目标代码,如下图所示:


右键调试附件1

红色标记断点后,因为OC的底层封装是以动态库的形式给到开发者的,点击查询调试,找不到@selector(array)的实现,进而无法找到array方法的调用链,即到这里就没了:


右键调试附件2

怎么办呢,这里讲一种比较习惯用的即符号断点和混编的混用:
走到断点后,插入symbol(方法名)符号断点如下:


符号断点添加图例

符号断点创建好后,点击继续往下走箭头,可以看到混编内容如下:


混编内容
3.结论:都是走了objc_alloc方法

可以看到,@selector(array)紧接着是走了objc_alloc方法的,同样的方法调试@selector(alloc),也可以发现是走了objc_alloc方法,的确是一样的链路。

PS:OC有一个非常特性-封装,很多我们只能看到声明,看不到实现,这好急啊,那就把墙砸了吧,依次点击查询调试是可以看到完整的调用链路的,如下提供源代码链接:
objc4-818.2源码下载

三、alloc干了什么,整体调用链路什么样的:

前面都是铺垫,这里来到了正主,即捏对象中的关键一步,alloc都干了些什么呢?先把结果公布,如下图所示:


[class alloc] 调用链路.png

下载源码对初始化行断点调试,接着对alloc符号断点调试,进而objc_alloc断点调试,进入系统库断点后,依次点击查看调试我们发现如下的调用链:

①.objc_alloc
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
②.callAlloc
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__    
    #warning 咱们只研究OBJC2
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

    // No shortcuts available.
    if (allocWithZone) {
    #warning 基本弃用
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
③._objc_rootAllocWithZone
_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);
}
④._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;

    #PersonalMark 核心方法1:instanceSize,计算需要多大的内存(捏个对象,估计身高体重,考量需要多少泥巴) 
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
    #PersonalMark 核心方法2:calloc,根据计算出的内存size来给对象划分内存,开辟内存空间(已经根据第一步捏出来大致的东西了,需要有放置的地方,有存才能有取)
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
    #PersonalMark 核心方法3:initInstanceIsa,将创建的对象分类,和对象的class绑定(确定捏的对象的类别,战士、法师啥的)
        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方法在实际的调用链路用了较清晰的认知,那么为什么是这样的呢?去掉继承、封装的干扰,那么整体的思路就是什么呢,我想捏个东西搞事情,我得去计算这个东西的用料及成本,并最终捏个雏形。这个工序我认为有很多思路,苹果的思路非常棒,将它封装工厂化,所有的对象创建都可以走这套流程,非常方便、规范;

前面也讲了,大部分是封装,核心的东西是计算并捏出来,这个该怎么做呢?

四、Alloc核心代码挖掘(泥人内存计算、开辟内存、绑定Class)

①.instanceSize(计算对象所需内存)
    inline size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
        #mark --- 快速计算
            return cache.fastInstanceSize(extraBytes);
        }
  
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        #mark --- 最小16字节
        if (size < 16) size = 16;
        return size;
    }

  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) {
    #mark 16字节对齐
    return (x + size_t(15)) & ~size_t(15);
}

以上是计算所需内存的代码,这保证了创建的size是16的整数倍,那么这里为什么要对齐,并且是16字节对齐呢? 首先从对齐的角度来说,要保证在cpu在按块读取的时候,每次读取一样大小的块,更方便快捷,所以要对齐; 那么为什么要16字节对齐呢不是8字节或是32字节对齐呢?可以这么理解,一个对象都含有一个isa至少8字节,如果8字节对齐,对象之间会更紧凑,访问错误发生的概率就会变大,而如果以32字节对齐,又会在一定程度上浪费内存,所以这里采取了16字节对齐。

②.calloc(开辟内存,用准备好的泥巴捏个雏形对象)
void *
calloc(size_t num_items, size_t size)
{
    return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}

calloc的封装不深挖了,看方法执行后的结果吧,附图:


开辟内存前后对象地址打印.png

id obj这句代码给予obj一个脏地址后(烂泥巴),通过calloc分配了具体的内存,obj有了最终的地址{这个地址和(YPerson*) $4 = 后跟的地址一样的},即我们最终打印对象的地址

③.initInstanceIsa(将对象和类绑定,isa指向明确,泥人的出厂值设定)
inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

这句代码执行后的效果,打印地址如下图所示:

指针和类绑定后打印结果.png

我们可以看到这个obj已经是我们可以正常调试的对象了,包含了类名、指向的内存地址。

讲到这里,已经为对象的创建在底层做了什么讲了该大概,那么这里还有些疑问,为何断点先执行objc_alloc没直接走alloc?

五、补充(llvm编译阶段对alloc的hook)

看一段非常特别的源码贴图:

更改alloc执行.png

有事、待补充

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容