iOS内存管理浅析

垃圾回收

对于c语言内存需要手动去管理申请(malloc/calloc)/释放(free),容易导致忘记释放或者重复释放,进而引发”内存泄漏“或者”进程异常崩溃“等,且容易出现”内存碎片“。

像对于已经使用完成但未释放的内存对象(即不再存活的对象)如同”垃圾“一样存在内存中且占用内存,若”垃圾“内存不及时释放占用多,就会导致进程内存紧张,进而引发”OOM“;

常见的”垃圾回收“判定方式如下:

  • 引用计数

    对每一个创建的对象分配一个引用计数器,用来存储对象被引用的次数,若计数为0,则标记为”垃圾“对象;但这种存在”循环引用“问题,且对象已经不再被使用,导致内存泄露,需要手动管理该对象,如弱引用或者手动释放;objective-cc++11智能指针采用了此方案;

  • 可达性分析

    这种方案是目前主流语言里采用的对象存活性判断方案。基本思路是把所有引用的对象想象成一棵树,从树的根结点 GC Roots 出发,持续遍历找出所有连接的树枝对象,这些对象则被称为“可达”对象,或称“存活”对象。其余的对象则被视为“死亡”的“不可达”对象,或称“垃圾”。

    可达性

    java采用了此垃圾回收机制,在后台创建一个守护进程。该进程会在内存紧张的时候自动跳出来,把堆空间的垃圾全部进行回收,从而保证程序的正常运行。并通过内存区域分区(包含新生代和老生代)来解决内存碎片问题:

  • 新生代:存活对象少、垃圾多,采用复制回收机制;

  • 老年代:存活对象多、垃圾少,采用标记整理机制;

一文学会 Java 垃圾回收机制

自动引用计数

MRC时代,对于内存的管理需要通过retain/release/autorelease方式去手动调用管理,难免存在内存泄露及野指针导致的进程异常,因此,苹果引入了自动引用计数c++11引入智能指针内存回收机制;

c++11智能指针实现

每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,引用计数加1;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。

原文链接:C++11--智能指针详解及实现

自动引用计数ARC(Automatic Reference Counting)

xcode已默认开启ARC模式,苹果的LLVM编译器自动添加retain/release代码,无需手动添加(会编译报错),降低了开发工作量同时减少了内存泄露及进程异常崩溃的风险;

xcode配置工程ARC模式的选项为:Objective-C Automatic Reference Counting,若当文件不开启ARC,则需要添加fobjc-no-arc选项标志;

引用计数原理实现

alloc/retain/release/dealloc

源码来源于:objc4-779.1

alloc调用栈如下:

alloc
    _objc_rootAlloc
        callAlloc
            objc_msgSend->allocWithZone
                _objc_rootAllocWithZone
                    _class_createInstanceFromZone
                        calloc
                        initInstanceIsa

核心函数如下:

static ALWAYS_INLINE id
_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 {
        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);//初始化isa指针指向类对象
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

主要是获取类实例对象的大小并分配内存同时修改isa指针;

retain实现调用栈如下:

retain
    obj->rootRetain
        objc_object::rootRetain

核心代码如下:

inline id 
objc_object::rootRetain()
{
    //判断是否为taggerPointer,若是则返回实例对象自身
    if (isTaggedPointer()) return (id)this;
    //散列表引用计数
    return sidetable_retain();
}

具体关于taggerPointer指针可参考深入理解Tagged Pointer,特点如下:
对于sidetable_retain函数如下:

static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;

static StripedMap<SideTable>& SideTables() {
    return SideTablesMap.get();
}

id objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    ASSERT(!isa.nonpointer);
#endif
    //全局对象获取对应的table,其中内部使用
    SideTable& table = SideTables()[this];
    //内部锁加锁,其中使用了spinlock_t自旋锁,以保证原子性获取引用计数属性refcnts
    table.lock();
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    table.unlock();

    return (id)this;
}

实质就是通过静态全局SideTablesMap对象中的SideTable对象中的refcnts属性来记录引用计数,retain就是原子性的+1操作,具体的SideTable对象内部结构可参考分析iOS管理对象内存的数据结构以及操作算法--SideTables、RefcountMap、weak_table_t-
release核心调用如下:

objc_object::rootRelease()
{
    if (isTaggedPointer()) return false;
    return sidetable_release(true);
}

uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    ASSERT(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];

    bool do_dealloc = false;

    table.lock();
    auto it = table.refcnts.try_emplace(this, SIDE_TABLE_DEALLOCATING);
    auto &refcnt = it.first->second;
    if (it.second) {
        do_dealloc = true;
    } else if (refcnt < SIDE_TABLE_DEALLOCATING) {
        // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
        do_dealloc = true;
        refcnt |= SIDE_TABLE_DEALLOCATING;
    } else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
        refcnt -= SIDE_TABLE_RC_ONE;
    }
    table.unlock();
    if (do_dealloc  &&  performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return do_dealloc;
}

release实质就是引用计数减1且调用dealloc

autorelease核心调用如下:

inline id 
objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}
id 
objc_object::rootAutorelease2()
{
    ASSERT(!isTaggedPointer());
    //由AutoreleasePoolPage类来管理释放
    return AutoreleasePoolPage::autorelease((id)this);
}

实质就是就是将这个对象加入到当前AutoreleasePoolPage类实例对象的栈顶next指针指向的位置,等到下一次runloop触发调用,具体原理实现可参见黑幕背后的Autorelease

dealloc核心代码如下:

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;
    object_dispose((id)this);
}

id 
object_dispose(id obj)
{
    if (!obj) return nil;
        /***********************************************************************
    * objc_destructInstance
    * Destroys an instance without freeing memory. 
    * Calls C++ destructors.
    * Calls ARC ivar cleanup.
    * Removes associative references.
    * Returns `obj`. Does nothing if `obj` is nil.
    **********************************************************************/
    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

实质就是对象析构并释放内存,不要显式调用该函数且ARC模式下dealloc中隐含调用[super dealloc]

ARC

ARC属性

属性修饰符对应的所有权修饰符关系如下:

属性与所有权修饰符对应关系

其中id及对象类型默认为__strong类型;

  • assign,针对数值类型,如CFFloat NSIngter等;
  • strong,表示”拥有关系“,为这种属性设置新值时,会先保留新值,然后释放旧值,最后将新值设置上去;
  • retain,作用是一样的,只是写法上的区别。在非ARC机制时,是用retain关键字修饰;在ARC机制后,一般都用strong关键字来代替retain了;
  • weak,表示”非拥有关系“,为这种属性设置新值时,不会保留新值,也不会释放旧值;不过属性对象释放时,会被置为nil,一般用于解决循环引用问题;
  • unsafe_unretained,语义同assign相同,只是简单地赋值操作,但其用于对象类型,且对象被释放时,与weak不同的是,不会自动清空(因此称为unsafe);
  • copy,语义与strong类似,但设置方法时不会保留新值,而是将其拷贝;

手动释放对象

ARC解决大部分内存管理问题,但对于Core Foundation对象(使用c编写的对象)无法自动管理,需要通过CFRetain/CFRelease来增加/减小引用计数;另外,Core Foundation对象与objective-c对象需要桥接转换处理,称为"Toll-Free Bridge",转换处理需要告诉编译器如何处理引用计数,关键词如下;

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

dealloc内部需要释放对象内部持有的ARC不能自动管理的对象,如:

  • 移除通知中心的监听
  • 移除KVO监听
  • 取消定时器,并将定时器置空(nil),NSTimerGCDTimer
  • 释放非Objective-C对象的内存,如CFRelease(...), free(...)
  • 释放GCD队列:dispatch_release(_ioQueue);

内存泄露分析

内存泄露分析常用的工具,如Instrument->Leak及僵尸对象;

mem instruments

Leak
leak

leak

leak提供了直观的分析内存泄露的循环引用关系、调用树等分析手段;

僵尸对象

对已回收的内存对象再次访问有时会出现非法访问的情况导致进程异常崩溃,若同时导致栈紊乱,就难以通过崩溃报告查看具体的崩溃位置及相应的非法访问代码处,所幸苹果提供了”僵尸对象“工具来分析对已回收对象的非法访问。

具体的原理如下:

运行期系统会把所有已经回收的实例转化为”僵尸对象“且不回收它们,若僵尸对象收到消息后,会抛出异常,其中包含了准确的发送过来的消息,并描述了回收之前的对象;

实现原理是通过方法调配dealloc方法执行一段自定义代码,其中代码执行:

  1. 获取发送消息类对象名称clsName
  2. 拷贝_NSZombie_类并赋予其新的名称_NSZombie_clsName,该类如同NSobject类同属根类,同时存在唯一的isa实例变量,且没有实例方法,因此向该类发送消息都会走”完整的消息转发流程“;
  3. 消息转发流程中__forwarding__方法中截获类名前缀为__NSZombie_的类,则表明消息接收者为僵尸对象;

xcode中的Zombies工具检测出僵尸对象就会弹出此提示:

Zombies

Reference

理解 iOS 的内存管理

《objective-C 高级编程 iOS与OSX多线程和内存管理》

《Effective Objective 2.0 编写高质量iOS与OS X代码的52个有效方法》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358

推荐阅读更多精彩内容