小码哥底层原理笔记:内存管理
iOS程序的内存布局
55B534F2-D918-47A7-86EE-D03A79E39FB6.png注:只要是static修饰的变量就相当于是全局变量,整个项目就只有一份内存地址
Tagged Point技术
从64bit开始,iOS引入了Tagged Point技术,用于优化NSNumber、NSDate、NSString等小对象的存储。
在没有使用Tagged Pointer之前,NSNumber等对象需要动态分配内存、维护引用计数等,NSNumer指针存储的是堆中NSNumber对象的地址值。
当使用了Tagged Point之后,NSNumber指针里面存储的数据变成了:Tag + Data,直接把数据存储在指针里面,比如NSNumber里面存储10:number = 0xb000a1
最高位是标志位,如果为1表示为Tagged Point指针,如果是MAC平台则最低有效位为1
当指针不够存储数据的时候,才使用动态分配内存的方式来存储数据
objc_msgSend能识别Tagged Point,比如调用NSNumber的intValue方法,直接从指针提取数据,因为Tagged Point并不是一个OC对象,没有类对象,没有isa。所以如果按之前的方法调用流程的话会报找不到方法错误。
面试题:
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
// 加锁
self.name = [NSString stringWithFormat:@"abcdefghijk"];
// 解锁
});
}
以上代码运行会出现闪退。因为给self.name赋值相当于调用setName方法。
在MRC中setName方法:
- (void)setName:(NSString *)name
{
if (_name != name) {
[_name release];
_name = [name retain];
}
}
有可能会出现多个子线程同时访问[_name release];,从而导致报错。解决办法可以将name设置为atmoic或者在name赋值的前后加锁。保证只有一个线程在赋值
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abc"];
});
}
以上代码并不会闪退。因为abc是小数据,根据Tagged Point技术,数据是直接存储在name的地址里面的,并不会动态生成对象存储,也就不会调用set方法了。
引用计数
在iOS中,使用引用计数来管理OC对象的内存
一个新建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁。
当调用alloc、new、copy、mutableCopy方法返回一个对象,在不需要使用时需要进行release或者autorelease。
ARC是LLVM编译的时候帮我们处理内存管理方面的代码,然后在RunTime运行时帮我们处理弱引用相关的操作
引用计数的存储
在64bit中,引用计数可以直接存储在有划过的isa指针中,也可能存储在SideTable类中:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;//引用计数存储,散列表,当前最新的指针为key,value为引用计数
weak_table_t weak_table;//weak引用存储,散列表
}
在isa指针中有一个字段19位存储对象的引用计数,如果不够存储则会存储在SideTable表中的refcnts;
weak指针的原理
将weak修饰的对象存放到SideTable里面的weak_table里面
当对象释放自动调用dealloc函数时会调用clearDeallocating()将指向当前对象的弱指针置为nil
自动释放池
autoreleasePool的底层代码如下:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();//构造函数,在创建结构体的时候调用
}
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);//析构函数,在结构体销毁的时候调用
}
void * atautoreleasepoolobj;
};
接下来我们在使用的时候如下代码:
- (void)test{
@autoreleasepool {
NSObject *object = [[NSObject alloc] init];
}
}
以上代码本质是
- (void)test{
atautoreleasepoolobj = objc_autoreleasePoolPush();
MJPerson *person = [[[MJPerson alloc] init] autorelease];
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
即在autorelease前面调用了objc_autoreleasePoolPush方法,在销毁的时候调用了objc_autoreleasePoolPush方法。在这两个方法中都是通过AutoreleasePoolPage对象来管理的。
class AutoreleasePoolPage
{
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
}
每个AutoreleasePoolPage对象占用4096字节,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的内存地址。当我们一个对象调用了autorelease后会将这个对象的地址存放到AutoreleasePoolPage里面。
所有的AutoreleasePoolPage对象是通过双向链表的形式连接在一起。
AutoreleasePoolPage
autoreleasePool调用push方法:
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
当我们调用push方法会将一个POOL_BOUNDARY入栈,并且返回其内存地址,然后再把对象的内存地址一次入栈。当调用pop方法的时候会传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY。比如当我们有多个@autoreleasepool 的时候:
int main(int argc, const char * argv[]) {
@autoreleasepool {//r1 = push()
MJPerson *p1 = [[MJPerson alloc] init];
MJPerson *p2 = [[MJPerson alloc] init];
@autoreleasepool {//r2 = push()
MJPerson *p3 = [[MJPerson alloc] init];
@autoreleasepool {//r3 = push()
MJPerson *p4 = [[MJPerson alloc] init];
}//pop(r3)
}//pop(r2)
}//pop(r1)
return 0;
}
POOL_BOUNDARY相当于一个标志位,将多个autoreleasepool隔开r1、r2、r3分别是对应每个autoreleasepool的POOL_BOUNDARY的内存地址,释放的时候我们调用pop把这个POOL_BOUNDARY的内存地址传进去,然后一次释放直到遇到POOL_BOUNDARY的内存地址。
autoreleasepool释放流程
autorelease在什么时候释放?
如果是直接被autoreleasePool包住的话,那么释放的时机就是在autoreleasePool执行完之后就释放了。比如:object将在执行完@autoreleasepool的时候释放
- (void)test{
@autoreleasepool {
NSObject *object = [[NSObject alloc] init];
}
}
如果不是在autoreleasePool里面的话,那是由runloop控制的,iOS在主线程的RunLoop中注册了2个Observer,第一个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()。第二个Observer监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush(),还监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()。
注意:在ARC中编译时插入的代码是release,而不是autorelease,所以在执行完当前函数的时候局部变量就会立即释放。