iOS 底层探索系列objective-c
上一篇文章主要咱们探索了 iOS
对象的 alloc
和 init
以及对象是怎么开辟内存以及初始化的,若是在对象身上增长一些属性,是否会影响内存开辟呢?还有一个遗留问题就是经过 calloc
,咱们的对象有了内存地址,可是对象结构里面的 isa
是怎么关联到咱们的对象的内存地址的呢。算法
calloc
底层探索在探索 calloc
底层前,咱们先补充一下内存对齐相关的知识点。bash
在 iOS
中,对象的属性须要进行内存对齐,而对象自己也须要进行内存对齐。
内存对齐有三原则架构
struct
)(或联合( union
))的数据成员,第一个数据成员放在 offset 为 0 的地方,之后每一个数据成员存储的起始位置要
从该成员大小或者成员的子成员大小ide
其内部最大元素大小的整数倍地址开始存储函数
sizeof
的结果,.必须是其内部最大成员的整数倍.不足的要补⻬。
翻译一下就是:post
**Struct**
的地址必须是最大字节的整数倍 咱们经过打印下面的代码:测试
NSLog(@"%lu - %lu",class_getInstanceSize([p class]),malloc_size((__bridge const void *)(p)));
能够发现对象本身申请的内存大小与系统实际给咱们开辟的大小时不同的,这里对象申请的内存大小是 40 个字节,而系统开辟的是 48 个字节。优化
40 个字节不难理解,是由于当前对象有 4 个属性,有三个属性为 8 个字节,有一个属性为 4个字节,再加上 isa 的 8 个字节,就是 32 + 4 = 36 个字节,而后根据内存对齐原则,36 不能被 8 整除,36 日后移动恰好到了 40 就是 8 的倍数,因此内存大小为 40。ui
48 个字节的话须要咱们探索 calloc
的底层原理。
这里还有一个注意点,就是 class_getInstanceSize
和 malloc_size
对同一个对象返回的结果不同的,缘由是 malloc_size
是直接返回的 calloc
以后的指针的大小,回忆上一节课,这里有一步在调用 calloc
以前的操做以下:
size_t instanceSize(size_t extraBytes) { size_t size = alignedInstanceSize() + extraBytes; // CF requires all objects be at least 16 bytes. if (size < 16) size = 16; return size; }
而 class_getInstanceSize
内部实现是:
size_t class_getInstanceSize(Class cls) { if (!cls) return 0; return cls->alignedInstanceSize(); }
也就是说 class_getInstanceSize
会输出 8 个字节,malloc_size
会输出 16 个字节,固然前提是该对象没有任何属性。
咱们从 calloc
函数出发,可是咱们直接在 libObjc
的源码中是找不到其对应实现的,经过观察 Xcode 咱们知道其实应该找 libMalloc
源码才对:
这里有个小技巧,其实咱们研究的是 calloc
的底层原理,而 libObjc
和 libMalloc
是相互独立的,因此在 libMalloc
源码里面,咱们不必去走 calloc
前面的流程了。咱们经过断点调试 libObjc
源码能够知道第二个参数是 40: (这是由于当前发送 alloc
消息的对象有 4 个属性,每一个属性 8 个字节,再加上 isa 的 8 个字节,因此就是 40 个字节)
接下来咱们打开 libMalloc
的源码,在新建的 target 中直接手动声明以下的代码:
void *p = calloc(1, 40); NSLog(@"%lu",malloc_size(p));
但 Command + Run
以后咱们会看到报错信息:
这个时候咱们会使用搜索大法,直接 Command + Shift + F
进行全局搜索对应的符号,可是会发现找不到,咱们再仔细观察,这些符号都是位于 .o
文件里面的,因此咱们能够去掉符号前面的下划线再进行搜索,这个时候就能够把对应的代码注释而后从新运行了。
运行以后咱们一直沿着源码断点下去,会来到这么一段代码
ptr = zone->calloc(zone, num_items, size);
咱们若是直接去找 calloc
,就会递归了,因此咱们须要点进去,而后咱们会发现一个很复杂的东西出现了:
这里咱们能够直接在断点处使用 LLDB
命令打印这行代码来看具体实现是位于哪一个文件中
p zone->calloc 输出: (void *(*)(_malloc_zone_t *, size_t, size_t)) $1 = 0x00000001003839c7 (.dylib`default_zone_calloc at malloc.c:249)
也就是说 zone->alloc
的真正实现是在 malloc.c
源文件的249行处。
static void * default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size) { zone = runtime_default_zone(); return zone->calloc(zone, num_items, size); }
可是咱们发现这里又是一次 zone->calloc
,咱们接着再次使用 LLDB
打印内存地址:
p zone->calloc 输出: (void *(*)(_malloc_zone_t *, size_t, size_t)) $0 = 0x0000000100384faa (.dylib`nano_calloc at nano_malloc.c:884)
咱们再次来到 nano_calloc
方法
static void * nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size) { size_t total_bytes; if (calloc_get_size(num_items, size, 0, &total_bytes)) { return NULL; } if (total_bytes <= NANO_MAX_SIZE) { void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1); if (p) { return p; } else { /* FALLTHROUGH to helper zone */ } } malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone); return zone->calloc(zone, 1, total_bytes); }
咱们简单分析一下,应该往 _nano_malloc_check_clear
里面继续走,而后咱们发现 _nano_malloc_check_clear
里面内容很是多,这个时候咱们要明确一点,咱们的目的是找出 48 是怎么算出来的,通过分析以后,咱们来到 segregated_size_to_fit
static MALLOC_INLINE size_t segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey) { // size = 40 size_t k, slot_bytes; if (0 == size) { size = NANO_REGIME_QUANTA_SIZE; // Historical behavior } // 40 + 16-1 >> 4 << 4 // 40 - 16*3 = 48 // // 16 k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size *pKey = k - 1; // Zero-based! return slot_bytes; }
这里能够看出进行的是 16 字节对齐,那么也就是说咱们传入的 size
是 40,在通过 (40 + 16 - 1) >> 4 << 4 操做后,结果为48,也就是16的整数倍。
总结:
对象本身进行的是 16 字节对齐
isa
底层探索union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; #if defined(ISA_BITFIELD) struct { ISA_BITFIELD; // defined in isa.h }; #endif };
咱们探索 isa
的时候,会发现 isa
实际上是一个联合体,而这实际上是从内存管理层面来设计的,由于联合体是全部成员共享一个内存,联合体内存的大小取决于内部成员内存大小最大的那个元素,对于 isa
指针来讲,就不用额外声明不少的属性,直接在内部的 ISA_BITFIELD
保存信息。同时因为联合体属性间是互斥的,因此 cls
和 bits
在 isa
初始化流程时是在两个分支中被赋值的。
isa
做为一个联合体,有一个结构体属性为 ISA_BITFIELD
,其大小为 8 个字节,也就是 64 位。
下面的代码是基于 arm64
架构的:
# 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
nonpointer
: 表示是否对 isa
指针开启指针优化
isa
指针isa
中包含了类信息、对象的引用计数等没有弱引用的对象能够更快释放。
isa
是对象中的第一个属性,由于这一步是在继承的时候发生的,要早于对象的成员变量,属性列表,方法列表以及所遵循的协议列表。
咱们在探索 alloc
底层原理的时候,有一个方法叫作 initIsa
。
这个方法的做用就是初始化 isa
联合体位域。其中有这么一行代码:
newisa.shiftcls = (uintptr_t)cls >> 3;
经过这行代码,咱们知道 shiftcls
这个位域其实存储的是类的信息。这个类就是实例化对象所指向的那个类。
经过 LLDB
进行调试打印,咱们能够知道一个对象的 isa
会关联到这个对象所属的类。
这里的左移右移操做其实很好理解,首先咱们先观察 isa
的 ISA_BITFIELD
位域的结构:
// 注:这里是x64架构 # 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
咱们能够看到,ISA_BITFIELD
的前 3 位是 nonpointer
,has_assoc
,has_cxx_dtor
,中间 44 位是 shiftcls
,后面 17 位是剩余的内容,同时由于 iOS 是小端模式,那么咱们就须要去掉右边的 3 位和左边的 17位,因此就会采用 >>3<<3 而后 <<17>>17 的操做了。
经过这个测试,咱们就知道了 isa
实现了对象与类之间的关联。
咱们还能够探索 object_getClass
底层,能够发现有这样一行代码:
return (Class)(isa.bits & ISA_MASK);
这行代码就是将 isa
中的联合体位域与上一个蒙版,这个蒙版定义是怎么样的呢?
# define ISA_MASK 0x00007ffffffffff8ULL
0x00007ffffffffff8ULL
这个值咱们转成二进制表示:
0000 0000 0000 0000 0111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000
结果一目了然,这个蒙版就是帮咱们去过滤掉除 shiftcls
以外的内容。
咱们直接将对象的 isa
地址与上这个mask以后,就会获得 object.class
同样的内存地址。
咱们都知道对象能够建立多个,可是类是否能够建立多个呢?
答案很简单,一个。那么若是来验证呢?
//MARK: - 分析类对象内存存在个数 void lgTestClassNum(){ Class class1 = [LGPerson class]; Class class2 = [LGPerson alloc].class; Class class3 = object_getClass([LGPerson alloc]); Class class4 = [LGPerson alloc].class; NSLog(@"\n%p-\n%p-\n%p-\n%p",class1,class2,class3,class4); } // 打印输出以下: 0x100002108- 0x100002108- 0x100002108- 0x100002108
因此咱们就知道了类在内存中只会存在一份。
(lldb) x/4gx LGTeacher.class 0x100001420: 0x001d8001000013f9 0x0000000100b38140 0x100001430: 0x00000001003db270 0x0000000000000000 (lldb) po 0x001d8001000013f9 17082823967917874 (lldb) p 0x001d8001000013f9 (long) $2 = 8303516107936761 (lldb) po 0x100001420 LGTeacher
咱们经过上面的打印,就发现 类的内存结构里面的第一个结构打印出来仍是 LGTeacher
,那么是否是就意味着 对象->类->类 这样的死循环呢?这里的第二个类实际上是 元类
。是由系统帮咱们建立的。这个元类也没法被咱们实例化。
也就是下面的这种关系:
(lldb) p/x 0x001d8001000013f9 & 0x00007ffffffffff8 (long) $4 = 0x00000001000013f8 (lldb) po 0x00000001000013f8 LGTeacher (lldb) x/4gx 0x00000001000013f8 0x1000013f8: 0x001d800100b380f1 0x0000000100b380f0 0x100001408: 0x0000000101c30230 0x0000000100000007 (lldb) p/x 0x001d800100b380f1 & 0x00007ffffffffff8 (long) $6 = 0x0000000100b380f0 (lldb) po 0x0000000100b380f0 NSObject
咱们在 Xcode 中测试有如下结果:
由此能够给出官方的经典 isa
走位图
在咱们认知里面,OC
对象的本质就是一个结构体,这个结论在 libObjc
源码的 objc-private.h
源文件中能够获得证明。
struct objc_object { private: isa_t isa; public: // ISA() assumes this is NOT a tagged pointer object Class ISA(); // getIsa() allows this to be a tagged pointer object Class getIsa(); ...省略其余的内容... }
而对于对象所属的类来讲,咱们也能够在 objc-runtime-new.h
源文件中找到
struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags ...省略其余的内容... }
也就是说 objc_class
内存中第一个位置是 isa
,第二个位置是 superclass
。
不过咱们本着求真的态度能够用 clang
来重写咱们的 OC
源文件来查看是否是这么回事。
clang -rewrite-objc main.m -o main.cpp
这行命令会把咱们的 main.m
文件编译成 C++
格式,输出为 main.cpp
。
咱们能够看到 LGPerson
对象在底层实际上是一个结构体 objc_object
。
而咱们的 Class
在底层也是一个结构体 objc_class
。
至此, iOS
底层探索之对象篇更新完毕,如今来回顾一下咱们所探索的内容。
下一篇章咱们要探索篇章的是类,敬请期待~