1、alloc底层原理
1.1 三种方式调试底层源码:
1: 符号断点 libobjc.A.dylib`objc_alloc:
2: 汇编 跟流程 - 符号断点: objc_alloc
3: 符号断点 确定未知 : libobjc.A.dylib`+[NSObject alloc]:
以上,都定位到libobjc这个库下面,所以可以通过苹果源码开源地址下载objc这个库,来一探alloc底层实现原理;
苹果源码下载地址:https://opensource.apple.com/releases/
通过上图可以分析得出,alloc具备开辟内存空间的能力,因为p1、p2、p3都指向同一片内存地址;&p1、&p2、&p3分别代表他们的指针地址;
接下来就通过源码看一下其内部具体实现,以下代码都摘抄自objc->NSObject.mm文件中
+ (id)alloc {
return _objc_rootAlloc(self);
}
// Calls [cls alloc].
id
objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.
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));
}
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);
}
/***********************************************************************
* class_createInstance
* fixme
* Locking: none
*
* Note: this function has been carefully written so that the fastpath
* takes no branch.
**********************************************************************/
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 {
// 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);
}
执行calloc之后就会分配一个内存地址
执行完initInstanceIsa之后会发现,此时obj已经成功绑定上Person对象了;
obj绑定上Person类
_class_createInstanceFromZone这一步是我们的核心方法,我们会发现,它起到的作用是开辟内存+绑定我们的对象类名;
alloc方法调用,汇编查看
通过查看汇编调用流程,因为在编译阶段,链接完objc之后再经过llvm的编译处理,我们会发现alloc会通过llvm重定向到objc_alloc;
sel跟imp的关系,其实就是一本书的目录中的一个章节,sel代表章节名称,imp代表页码,imp指向实际的内容,修改了imp,就会导致我们调用的实际方法进行改变; llvm判断是alloc执行重定向 llvm指定调用objc_alloc
llvm动态生成函数
之所以这么处理,也可以看出苹果对特殊函数(类似alloc开辟内存这种)处理的看重程度,防止被人编译源码,导致的一些问题,就把特殊函数的处理放在llvm中,防止别人随意篡改;
llvm尝试特殊函数的消息转发
处理完特殊函数之后再走一遍正常流程
这里也有解释为什么callAlloc会执行两次的原因,所以alloc的基本流程如下:
objc_alloc->callAlloc->alloc->_objc_rootAlloc->callAlloc->_objc_rootAllocWithZone->_class_createInstanceFromZone
2、内存分配
在_class_createInstanceFromZone中有这么一行代码
size = cls->instanceSize(extraBytes);
获取当前要开辟的内存大小,一步步点进去会发现,它其实是以16进制对齐的方式,x+15 & ~15,相当于加上15,再把后四位抹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;
}
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);
}
alloc核心方法_class_createInstanceFromZone
cls->instanceSize --》先计算出需要的内存空间大小
calloc --》 向系统申请开辟内存,返回地址指针
obj->initInstanceIsa --》关联到相应的类
影响对象开辟内存大小的因素
分析可能因素:属性、方法、实例变量、协议、分类、扩展
啥都没有时需要的内存大小 一个属性时需要的内存大小定义一个方法时需要的内存大小
定义一个成员变量时需要的内存大小
因为 属性-方法(set/get)=变量,所以真正影响内存大小的因素是变量;
其实根据我们的常识,方法是定义在方法区,而我们的对象是开辟在堆区,而指针的开辟是在栈区,所以我们的方法定义根本不影响我们类对象的大小,其他协议、分类、扩展其实也一样,不会影响对象内存大小;
用x/4gx打印p1的地址,会发现第一个内存地址,其实就是isa地址,通过Class强转或者与上0x00007ffffffffff8ULL掩码,都可以得到真实的class name;
3、内存对齐
先看下面两张图
同样的成员变量,只是因为书写顺序不一样,导致需要开辟的内存空间大小就不一样,why?
主要是因为类的本质其实也就是结构体,像下面俩结构体,LGStruct1就需要开辟24个字节的内存,而LGStruct2只需要16个字节的内存就可以;
因为在内存对齐的原则中:
- 一是要向当前结构体最大变量需要开辟的内存大小进行对齐,比如double是8,每次CPU读取都是以8个字节单位;
- 二是当前插入的位置一定要被当前变量需要的字节数整除,比如下面的LGStruct1,当b存了第8个位置之后,c只能从12开始存,导致9到11的内存都被浪费了;而LGStruct2中b从8开始存,c只要1个字节,所以12这个位置就不会被浪费了,d也可以从14开始存;
- 三是当最后存完之后,得到的最后的内存位置一定要是当前结构体需要的最大变量的整数倍,比如LGStruct1最后是存到17这个位置,那么要满足CPU的按8位读取一次的的条件,此时我们需要的内存空间就是24个;
-
四是如果结构体中包含结构体的话,不是以结构体为整数倍,而是以结构体中的最大变量为整数倍;比如这里LGStruct3需要的就是48字节的内存;
之所以这么设计,是为了保存CPU读取的一致性以及高效性,牺牲一点点空间换取时间;而我们的类中的成员变量就是结构体的属性一样,需要遵循字节对齐的原则;所以上文说到成员变量的书写顺序不一样会导致需要的内存大小也不一样就是这个原因;
但是对象之所以不会造成这种问题,是因为苹果开发人员在属性底层设计的时候对内存做了极致的优化,保存我们的内存能够最大化的被利用;所以在我们平时开发中,为了保证我们的内存能够被充分的利用,尽量的使用属性去定义我们的变量。
struct LGStruct1 {
double a; // 8 [0 7]
char b; // 1 [8]
int c; // 4 (9 10 11 [12 13 14 15]
short d; // 2 [16 17] 24
}struct1;
struct LGStruct2 {
double a; // 8 [0 7]
int b; // 4 [8 9 10 11]
char c; // 1 [12]
short d; // 2 (13 [14 15] 16
}struct2;
struct LGStruct3 {
double a;
int b;
char c;
short d;
int e;
struct LGStruct1 str;
}struct3;
前文有提到过,对象在开辟内存的时候是以16字节对齐的方式,那为什么这里的属性又说以8字节对齐呢?
上图可以看到成员变量是8字节对齐,对象和对象之间是16字节对齐;
那是因为一个对象至少都有一个属性isa,而在我们实际开发中,有属性的概率是90%以上,苹果为了保证读取的效率,也同样遵循空间换时间的原则,在新的版本中都以16字节方式对齐,这也可以说明当前的iPhone设备内存越来越大,之前16GB、32GB内存的手机越来越卡的原因;
第二个上文也有提到,对象其实是通过calloc进行内存开辟的,通过阅读calloc底层源码会发现,它其实也是16字节对齐,对当前的size + 15 之后先右移4位再左移4位;
calloc底层实现
附上一张牛逼plus的kc画的instanceSize流程图;
4、对象本质
我们可以使用下面命令行把目标文件编译成c++文件
clang -rewrite-objc main.m -o main.cpp
对象的本质其实就是个结构体,NSObject_IMPL表示继承NSObject的所有属性
4.1 结构体和联合体区别
// 4 * 8 = 32 0000 0000 0000 0000 0000 0000 0000 1111
// 4 位
// 1 字节 3倍浪费
struct LGCar1 {
BOOL front; // 0 1
BOOL back;
BOOL left;
BOOL right;
};
// 位域 使用1位内存就可以
// 互斥
union LGCar2 {
BOOL front;
BOOL back;
BOOL left;
BOOL right;
};
结构体(struct)中所有变量是“共存”的——优点是“有容乃大”, 全面;缺点是struct内存空间的分配是粗放的,不管用不用,全分配。
联合体(union)中是各变量是“互斥”的——缺点就是不够“包容”; 但优点是内存使用更为精细灵活,也节省了内存空间;
isa使用union,为了兼容不同的版本;
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private:
// Accessing the class requires custom ptrauth operations, so
// force clients to go through setClass/getClass by making this
// private.
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
bool isDeallocating() {
return extra_rc == 0 && has_sidetable_rc == 0;
}
void setDeallocating() {
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif
void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
Class getDecodedClass(bool authenticated);
}
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD
: 1;
uintptr_t nonpointer
uintptr_t has_assoc
uintptr_t has_cxx_dtor
uintptr_t shiftcls : 33; uintptr_t magic : 6; uintptr_t weakly_referenced : 1; uintptr_t deallocating : 1; uintptr_t has_sidetable_rc : 1; uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
isa里面包含的信息:
nonpointer:表示是否对 isa 指针开启指针优化 0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等
has_assoc:关联对象标志位,0没有,1存在
has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
shiftcls:
存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针,用isa地址与上 is_mask掩码就可以查看当前isa指向。
magic:用于调试器判断当前对象是真的对象还是没有初始化的空间 weakly_referenced:志对象是否被指向或者曾经指向一个 ARC 的弱变量,
没有弱引用的对象可以更快释放。 deallocating:标志对象是否正在释放内存
has_sidetable_rc:当对象引用技术大于 10 时,则需要借用该变量存储进位
extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。
4.2 isa走位
元类就是meta,它也是有继承链的;继承链这一块就不多做缀述了,层层继承,层层关联;
isa走位就比较牛逼了,
对象的isa指向类;
类的isa的指向元类;
元类的isa指向根类的元类;
根类的元类的isa指向本身;
即LGPerson对象->LGPerson.class->LGPerson.meta->NSObject.meta
探索isa走位 isa走位图
最后附上一张完整alloc的流程图,同样也是摘抄自666归零666;
666归零666
网友评论