IOS 内存管理

早前大家都知道IOS的内存管理方式是MRC的, 尽管现在我们是在ARC下开发,但是我们依然要保持着一颗好奇的人。IOS 到底是怎么进行内存管理的?

一,IOS的程序内存布局(由低地址 -> 高地址)

  • 保留区
  • 代码段:编译之后的代码
  • 数据段:字符串常量,已初始化/未初始化 的静态变量/全局变量
  • 堆:由低地址到高地址,通过malloc,alloc,new,calloc等动态分配的空间
  • 栈区:函数调用,局部变量。分配的内存空间地址由高到低

OC 的内存管理不管是ARC 还是MRC 采用的是引用计数器管理。

ARC 的内存管理规则可以简述为:

  • 每个对象都有一个『被引用计数』
  • 引用计数器:一个简单而有效的管理生命周期的方式。
  • 对象被持有,『被引用计数』+1 (alloc/new/copy/mutableCopy 这些方法在对象创建的时候引用计数器自动+1)
  • 对象被放弃持有,『被引用计数』-1
  • 『引用计数』=0,释放对象  

引用计数的存储

在runtime的源码NSobject.mm 中找到函数
id  objc_retain(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}
点击进去可以看到
objc_object::retain()
{
    assert(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
//在次点击以下函数
        return rootRetain();
    }

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}

从源码中可以看出,先判断是不是taggerPointer。

taggerPointer是64位开始引入的,用于优化NSNumberNSDateNSString等小对象的存储。

在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值;使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中;当指针不够存储数据时,才会使用动态分配内存的方式来存储数据objc_msgSend能识别Tagged Pointer,比如NSNumberintValue方法,直接从指针提取数据,节省了以前的调用开销;

在64位iOS平台中,通过最高有效位是否是1 来判断是不是taggerPointer;

 

在继续往下查看源码之前,大家都知道,64位之前isa就是个普通的指针,存储着类/元类对象的内存地址,64位之后isa 进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息;

IOS 内存管理_第1张图片

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;//是一个存放着对象引用计数的散列表
    weak_table_t weak_table;

}

id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];
    
    table.lock();
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    table.unlock();

    return (id)this;
}

 

 

说到内存管理,不得不提“僵尸对象+ 空指针+野指针” 

1.僵尸对象:一个OC对象引用计数器为0,对象内存已经被回收但是数值(对象)还存在的对象被成为僵尸对象,该对象不能被引用也不可以访问

2.空指针:空指针是有效指针,值为nil,NULL ,0,给空指针发消息,不会报错,只是不响应而已。他是一个没有指向任何东西的指针。

3.野指针:指针指向了一个已经被销毁的对象的内存地址,向野指针发消息会报EXC_BAD_ACCESS,而导致程序崩溃

AutoRelease什么时候调用release? 和 AutoReleasePool  

1.在控制器的viewdidload 中打印下,runloop的mainloop,你会发现主线程中注册了两个关于autolease的观察者。所以autorelease调用release是由runloop控制的,在某次runlop循环中,runloop休眠之前调用的。

2.autoreleasepool底层实现

用clang编译器吧main.m 生成.cpp 文件发现 @autoreleasepool 变成了 __AtAutoreleasePool __autoreleasepool;全局搜索查看 __AtAutoreleasePool如下:
struct __AtAutoreleasePool {

// C++的构造函数
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} 

// C++的析构函数;与构造函数不同的地方在于前面有个~
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};


由此可知
@autoreleasepool{ //大括号开头调用 objc_autoreleasePoolPush()函数。

} //大括号结束之前调用objc_autoreleasePoolPop()函数。

在runtime的源码中搜索objc_autoreleasePoolPush与objc_autoreleasePoolPop
 objc_autoreleasePoolPush
void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}
 objc_autoreleasePoolPop
void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

自动释放池的底层全部依赖类 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;
}

IOS 内存管理_第2张图片

 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址

调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY

id *next指向了下一个能存放autorelease对象地址的区域 

 

循环引用/Core Foundation 对象的内存管理 

循环引用:可以使用instrument 里的leaks 观察是否有循环引用问题,然后在想办法解决。一般都是吧其中的一个对象改为弱引用。另外就是主动断开链接

Core Foundation 的内存管理不受ARC控制。对于这些引用技术的修改使用CFRetain 与CFRelease。在ARC 有时需要吧Core Foundation 对象转换成OC对象,这就需要Bridge。

  • __bridge: 只做类型转换,不修改相关对象的引用计数,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。
  • __bridge_retained:类型转换后,将相关对象的引用计数加 1,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。
  • __bridge_transfer:类型转换后,将该对象的引用计数交给 ARC 管理,Core Foundation 对象在不用时,不再需要调用 CFRelease 方法。

 

 

你可能感兴趣的:(内存管理,IOS)