iOS 内存管理
2022-02-08 本文已影响0人
MrQun
内存布局-五大区

- 栈区 0x7
创建临时变量时由编译器自动分配,在不需要的时候自动清除的变量的存储区。
里面的变量通常是局部变量、函数参数等。在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。
- 堆区 0x6
那些由 new alloc 创建的对象所分配的内存块,它们的释放系统不会主动去管,由我们的开发者去告诉系统什么时候释放这块内存(一个对象引用计数为0是系统就会回销毁该内存区域对象)。一般一个 new 就要对应一个 release。在ARC下编译器会自动在合适位置为OC对象添加release操作。会在当前线程Runloop退出或休眠时销毁这些对象,MRC则需程序员手动释放。
堆可以动态地扩展和收缩。 - 静态区(未初始化数据).bss
程序运行过程内存的数据一直存在,程序结束后由系统释放 - 常量区(已初始化数据).data
专门用于存放常量,程序结束后由系统释放 - 代码区
用于存放程序运行时的代码,代码会被编译成二进制存进内存的程序代码区

int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString *a = @"a";
NSMutableString *b = [a mutableCopy];
NSString *c = [a copy];
NSString *d = [[a mutableCopy] copy];
NSString *e = [NSString stringWithString:a];
NSString *f = [NSString stringWithFormat:@"f"];
NSString *string1 = [NSString stringWithFormat:@"abcdefg"];
NSString *string2 = [NSString stringWithFormat:@"abcdefghi"];
NSString *string3 = [NSString stringWithFormat:@"abcdefghij"];
}
return 0;
}


Tagged Pointer并不是真正的对象,它没有isa指针,
// - 会 Crash;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abcdefghij"];
});
}
// - 不会Crash;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abcdefghi"];
});
}
// - 分析:
// - 第一个的字符串为__NSCFString, 存储在堆上,它是个正常对象,需要维护引用计数的。self.name通过setter方法为其赋值, 我们异步并发执行setter方法,可能就会有多条线程同时执行[_name release],连续release两次就会造成对象的过度释放,导致Crash。
- (void)setName:(NSString *)name {
if(_name != name) {
[_name release];
_name = [name retain]; // or [name copy]
}
}
// - 第二段代码中的NSString为NSTaggedPointerString类型,在objc_release函数中会判断指针是不是TaggedPointer类型,是的话就不对对象进行release操作,也就避免了因过度释放对象而导致的Crash
引用计数
struct objc_object {
private:
isa_t isa; // 在 arm64 架构开始
};
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if SUPPORT_PACKED_ISA
# if __arm64__ // 在 __arm64__ 架构下
# define ISA_MASK 0x0000000ffffffff8ULL // 用来取出 Class、Meta-Class 对象的内存地址
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1; // 0:代表普通的指针,存储着 Class、Meta-Class 对象的内存地址
// 1:代表优化过,使用位域存储更多的信息
uintptr_t has_assoc : 1; // 是否有设置过关联对象,如果没有,释放时会更快
uintptr_t has_cxx_dtor : 1; // 是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
uintptr_t shiftcls : 33; // 存储着 Class、Meta-Class 对象的内存地址信息
uintptr_t magic : 6; // 用于在调试时分辨对象是否未完成初始化
uintptr_t weakly_referenced : 1; // 是否有被弱引用指向过,如果没有,释放时会更快
uintptr_t deallocating : 1; // 对象是否正在释放
uintptr_t has_sidetable_rc : 1; // 如果为1,代表引用计数过大无法存储在 isa 中,那么超出的引用计数会存储在一个叫 SideTable 结构体的 RefCountMap(引用计数表)散列表中
uintptr_t extra_rc : 19; // 里面存储的值是对象本身之外的引用计数的数量,retainCount - 1
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
};
-
nonpointer也就是之前说过的TaggedPointer技术
如果isa非nonpointer,即 arm64 架构之前的isa指针。由于它只是一个普通的指针,存储着Class、Meta-Class对象的内存地址,所以它本身不能存储引用计数,所以以前对象的引用计数都存储在一个叫SideTable结构体的RefCountMap(引用计数表)散列表中。
如果isa是nonpointer,则它本身可以存储一些引用计数。从以上union isa_t的定义中我们可以得知,isa_t中存储了两个引用计数相关的东西:extra_rc和has_sidetable_rc。 -
extra_rc:里面存储的值是对象本身之外的引用计数的数量,这 19 位如果不够存储,has_sidetable_rc的值就会变为 1;
has_sidetable_rc:如果为 1,代表引用计数过大无法存储在isa中,那么超出的引用计数会存储SideTable的RefCountMap中。 -
所以,如果isa是nonpointer,则对象的引用计数存储在它的isa_t的extra_rc中以及SideTable的RefCountMap中。