在我们最早学习iOS开发时,会听到alloc的方法是用来开辟内存空间的说法。
但是在我们研究alloc底层时会发现,其实alloc不仅仅开辟了类的内存空间,同时他还关联了类的信息。
那么现在就来研究 alloc 过程中是如何关联类的信息的。
-
isa的关联时间
通过我们对于 alloc 源码的跟踪
当代码进入_class_createInstanceFromZone
方法时生成了对象
_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 {
// alloc 开辟内存的地方
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);
}
通过返回值 obj 我们知道关键方法必然是操作了obj 的方法
于是,我们如图设置断点
在对应断点打印obj的内容后
我们可以知道类信息的关联操作必然是通过
obj->initInstanceIsa(cls, hasCxxDtor)
或者obj->initIsa(cls)
方法进行的。
-
isa的结构
通过对obj->initInstanceIsa(cls, hasCxxDtor)
的源码追踪,发现其主要功能是从初始化了一个isa_t的东西,那么这个isa_t是什么?作用是什么?
首先,我们追踪断点进入方法,打印这个isa_t
可以看到isa_t的结构如图所示
同时,我们也可以通过代码发现,isa_t 其实是一个联合体union
结构体(struct)中所有变量是“共存”的——优点是“有容乃大”, 全面;缺点是struct内存空间的分配是粗放的,不管用不用,全分配。
联合体(union)中是各变量是“互斥”的——缺点就是不够“包容”; 但优点是内存使用更为精细灵活,也节省了内存空间
苹果通过联合体而非结构体的方式用来节省内存
-
重点 - isa是如何存储信息的
从上方打印的isa_t能看出cls里的信息为 类信息
那 bits 中储存了什么信息?
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
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)
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
这是isa的结构,分别为手机端 mac端 (不同处理器的区别)
而每一位代表的含义则如图
我们来尝试研究一下我们比较关心,也比较容易验证的shiftcls是否真的保存了类指针
我们在alloc流程中的_class_createInstanceFromZone
方法中设置断点,断点时间再isa赋值后返回前
打印当前的obj 即我们alloc初始化的FQModel类的对象
以八字节分段打印地址内容
其中第一段的8个字节应当就是我们要的isa
我们当前需要取的类指针shiftcls是在[3 46]位之间
那我们将其他位运用位运算进行抹零操作
先右移3位
再左移 20(17+3)位
最后让内容回到原本的[3 46]位 右移17位
最终结果,我们与当前的cls比较
确实一致,可见shiftcls确实存储了类指针
当然,并不一定每次计算都需要这么复杂,我们可以直接通过位运算与上掩码来取类指针
其原理 与运算中如果有都为1则保留1,否则保留0
1101 1000
如果我们想保留4-5位上的数字
我们可以直接用这个数与上
0001 1000
1101 1000
0001 1000
这样就可以起到将前三位和后三位的数字都置为0,保留原本4-5位上的数字
-
关于联合体位域的应用
这个话题其实和当前isa的讨论关系不大,但是在开发过程中也是可以用到这样的思路。
比如系统权限的控制
如果一个系统有八大权限,相互不依赖和影响,那我们就可以用一个8位的2进制数,每一位代表一个权限是否拥有,这样直接比用8个属性或者枚举所有可能性来的轻便,好用。