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.探究工具:符号断点与混编
这是我们通过这里方法似乎有很多,只讲一种我觉得方便的,先断点调试到目标代码,如下图所示:
红色标记断点后,因为OC的底层封装是以动态库的形式给到开发者的,点击查询调试,找不到@selector(array)的实现,进而无法找到array方法的调用链,即到这里就没了:
怎么办呢,这里讲一种比较习惯用的即符号断点和混编的混用:
走到断点后,插入symbol(方法名)符号断点如下:
符号断点创建好后,点击继续往下走箭头,可以看到混编内容如下:
3.结论:都是走了objc_alloc方法
可以看到,@selector(array)紧接着是走了objc_alloc方法的,同样的方法调试@selector(alloc),也可以发现是走了objc_alloc方法,的确是一样的链路。
PS:OC有一个非常特性-封装,很多我们只能看到声明,看不到实现,这好急啊,那就把墙砸了吧,依次点击查询调试是可以看到完整的调用链路的,如下提供源代码链接:
objc4-818.2源码下载
三、alloc干了什么,整体调用链路什么样的:
前面都是铺垫,这里来到了正主,即捏对象中的关键一步,alloc都干了些什么呢?先把结果公布,如下图所示:
下载源码对初始化行断点调试,接着对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的封装不深挖了,看方法执行后的结果吧,附图:
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);
}
这句代码执行后的效果,打印地址如下图所示:
我们可以看到这个obj已经是我们可以正常调试的对象了,包含了类名、指向的内存地址。
讲到这里,已经为对象的创建在底层做了什么讲了该大概,那么这里还有些疑问,为何断点先执行objc_alloc没直接走alloc?
五、补充(llvm编译阶段对alloc的hook)
看一段非常特别的源码贴图:
有事、待补充