入职后从iOS转向了ReactNative,也写了不少ReactNative需求,最近忽然和搭档聊到ReactNative 内存办理,发现自己对这块还不太了解,为此调研了ReactNative内存办理知识,与iOS 中Objective做为对比总结 文章中关于ObjectiveC的内存办理知识,来自苹果最新源码。关于JS相关知识,来自网络调研和JavaScript高档程序规划一书,若有不同看法或发现错误,欢迎拍砖指正

参考:

objc4-866源码 objc4历史版本 WWDC Advancements in the Objective-C runtime

总结

  1. ObjectiveC首要采样引证计数办理内存,引证技能存储在isa_t的extra_rc和散列表的引证计数表里
  2. ObjectiveC的TaggedPointer的值存储在指针中,存储在栈上,不需要经过引证计数办理内存
  3. JS首要经过符号的办法办理内存, 在效果域的变量会加符号。废物收回程序每次运转时会整理未运用的变量
  4. 引证技能的办法简单形成循环引证,符号清除的办法更简单形成内存走漏

1. 内存区

在iOS中,首要把内存分为五大区,从高到底分别为

  • 栈区 寄存函数变量和函数参数
  • 堆区 寄存动态分配的内存段
  • 大局静态区 寄存大局变量、静态变量
  • 常量区 寄存常量
  • 代码区 寄存程序代码
    ObjectiveC和JS的内存管理区别

2. ObjectiveC 内存办理

2.1 内存办理办法

ObjectiveC的内存办理首要分为两种办法,即MRC(手动办理)和ARC(主动),都是引证计数的办法办理内存,区别在于ARC形式下,编译器会主动的帮程序要增加引证计数+1和-1代码

2.2 两种内存办理计划

2.2.1 非引证计数办理(TaggedPointer目标)

总结:TaggedPointer目标的值存储在指针中,指针存储在栈上,无需引证计数办理, 开发者也无需办理其内存

苹果从32位转向64位时,**NSString****NSNumer****NSDate**这类型数据,假如用旧的办法办理,会形成资源和功率的糟蹋,毕竟一个简短的字符串假如定义为一个目标,存储isa。class等相关的信息会形成不必要的空间资源糟蹋,而为办理起引证计数、生命周期,也会形成时间功率上的糟蹋。因此苹果定义了一种新目标taggedPointer,为了对此做出改善 taggedpointer的改善在于,指针中存储了taggedpointer目标的值,除此之外,指针中部分空间存储符号,如:是否是taggedPoninter目标、是什么类型的**taggedPointer目标(NSString/NSNmer/NSDate) 他的数据结构如下:

ObjectiveC和JS的内存管理区别
objc4-866源码 源码中看,判别是否是taggedPointer目标,拿着 指针与_OBJC_TAG_MASK做了操作,假如结果仍然是_OBJC_TAG_MASK,则判别为taggedPointer目标

_objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

_OBJC_TAG_MASK定义可看出, 编译器在不同的64位上经过判别最高位或最低位是否是为1,来判别是否是taggedPointer目标

#if OBJC_SPLIT_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#elif OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#else
#   define _OBJC_TAG_MASK 1UL
#endif

TaggedPointer生命周期办理 TaggedPointer目标的值存储在指针中,指针又存储在栈上,所以不需要引证计数办理, 这儿retain办法在判别是isTaggedPointer时,直接return,什么都不做

objc_object::retain()
{
    ASSERT(!isTaggedPointer());
    if (fastpath(!ISA()->hasCustomRR())) {
        return sidetable_retain();
    }
    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
}

TaggedPointer开释 追relase源码调用,可看到最后调用到了objc_object::rootRelease()这个办法,办法内部先判别假如是TaggedPointer目标的话,return什么都不做

-(void) release
{
    _objc_rootRelease(self);
}
_objc_rootRelease(id obj)
{
    ASSERT(obj);
    obj->rootRelease();
}
objc_object::rootRelease()
{
    return rootRelease(true, RRVariant::Fast);
}
// Base release implementation, ignoring overrides.
// Does not call -dealloc.
// Returns true if the object should now be deallocated.
// This does not check isa.fast_rr; if there is an RR override then 
// it was already called and it chose to call [super release].
inline bool 
objc_object::rootRelease()
{
    if (isTaggedPointer()) return false;
    return sidetable_release();
}

2.2.2 引证计数办理(object目标)

谈到iOS,离不开面向目标这个概念。目标什么时候创立、什么时候该开释,都是用引证计数来办理的。

  1. 当目标创立时引证计数=0,被引证时调用-(void)retain办法,将其引证计数为+1。
  2. 免除引证时调用-(void)release对引证计数-1。
  3. 当引证计数减为0时,表明目标不再运用,此时会开释目标所占用的堆空间, 并调用- (void)dealloc析构办法

那么,引证技能存储在哪呢?堆、栈仍是其他地方? 又是怎样与目标关联的呢? 结论: nonapointer_isa目标,存储在extra_rc和SideTables中, pointer_isa,即纯指针类型的isa,存储在SideTables中 他俩的区别在于,isa是指针仍是isa_t联合体 怎么证明上边结论?从前边的release办法跟下去,最终源码会走到这个办法,这儿做的操作为

  1. 假如是pointer_isa目标,直接查找大局散列表,招到对应的引证计数表,再从引证计数表里,将当时目标的引证计数-1
  2. 假如是nonapointer_isa目标 a. 先将isa_t里的extra_rc-1 b. 当extra_rc=0,从散列表中取出一半的引证计数值,做-1操作后赋值给extra_rc 这儿有个问题, 那便是上边的ab流程, 苹果为什么这么规划,首要是考虑从extra_rc里操作引证计数,是直接对联合体isa_t的地址做与操作,比从散列表里查询、取值、操作功率更快。 这儿源码加了注释,直接看源码就可以了
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return false;
    bool sideTableLocked = false;
    isa_t newisa, oldisa;
    oldisa = LoadExclusive(&isa().bits);
    if (variant == RRVariant::FastOrMsgSend) {
        // These checks are only meaningful for objc_release()
        // They are here so that we avoid a re-load of the isa.
        if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
            ClearExclusive(&isa().bits);
            if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
                swiftRelease.load(memory_order_relaxed)((id)this);
                return true;
            }
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
            return true;
        }
    }
    if (slowpath(!oldisa.nonpointer)) {
        // a Class is a Class forever, so we can perform this check once
        // outside of the CAS loop
        if (oldisa.getDecodedClass(false)->isMetaClass()) {
            ClearExclusive(&isa().bits);
            return false;
        }
    }
retry:
    do {
        newisa = oldisa;
        // 判别假如是指针isa,则调用sidetable_release从大局散列表查询当时目标的引证计数并-1
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa().bits);
            return sidetable_release(sideTableLocked, performDealloc);
        }
        // 假如当时目标正在开释析构,则直接return
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa().bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            return false;
        }
        //走到这儿说明是nonpointer isa, 这儿的代码首要做的操作便是清楚isa里的extra_rc--
        //假如extra_rc--减为0,则跳转到underflow:
        // don't check newisa.fast_rr; we already called any RR overrides
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa().bits, &oldisa.bits, newisa.bits)));
    if (slowpath(newisa.isDeallocating()))
        goto deallocate;
    if (variant == RRVariant::Full) {
        if (slowpath(sideTableLocked)) sidetable_unlock();
    } else {
        ASSERT(!sideTableLocked);
    }
    return false;
    // underflow首要做的操作便是对散列表里当时目标的引证计数-1
 underflow:
    // newisa.extra_rc-- underflowed: borrow from side table or deallocate
    // abandon newisa to undo the decrement
    newisa = oldisa;
    // 假如引证计数表里存储了引证计数,则跳转到函数头部,从头履行
    if (slowpath(newisa.has_sidetable_rc)) {
        if (variant != RRVariant::Full) {
            ClearExclusive(&isa().bits);
            return rootRelease_underflow(performDealloc);
        }
        // Transfer retain count from side table to inline storage.
        if (!sideTableLocked) {
            ClearExclusive(&isa().bits);
            sidetable_lock();
            sideTableLocked = true;
            // Need to start over to avoid a race against 
            // the nonpointer -> raw pointer transition.
            oldisa = LoadExclusive(&isa().bits);
            goto retry;
        }
        // 这儿苹果注释很明白了,测验对引证计数表里的引证计数-1
        // Try to remove some retain counts from the side table.        
        auto borrow = sidetable_subExtraRC_nolock(RC_HALF);
        bool emptySideTable = borrow.remaining == 0; // we'll clear the side table if no refcounts remain there
        // 让后将引证计数表里的部分值,移到extra_rc中
        if (borrow.borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            bool didTransitionToDeallocating = false;
            newisa.extra_rc = borrow.borrowed - 1;  // redo the original decrement too
            newisa.has_sidetable_rc = !emptySideTable;
            bool stored = StoreReleaseExclusive(&isa().bits, &oldisa.bits, newisa.bits);
            if (!stored && oldisa.nonpointer) {
                // Inline update failed. 
                // Try it again right now. This prevents livelock on LL/SC 
                // architectures where the side table access itself may have 
                // dropped the reservation.
                uintptr_t overflow;
                newisa.bits =
                    addc(oldisa.bits, RC_ONE * (borrow.borrowed-1), 0, &overflow);
                newisa.has_sidetable_rc = !emptySideTable;
                if (!overflow) {
                    stored = StoreReleaseExclusive(&isa().bits, &oldisa.bits, newisa.bits);
                    if (stored) {
                        didTransitionToDeallocating = newisa.isDeallocating();
                    }
                }
            }
            if (!stored) {
                // Inline update failed.
                // Put the retains back in the side table.
                ClearExclusive(&isa().bits);
                sidetable_addExtraRC_nolock(borrow.borrowed);
                oldisa = LoadExclusive(&isa().bits);
                goto retry;
            }
            // Decrement successful after borrowing from side table.
            if (emptySideTable)
                sidetable_clearExtraRC_nolock();
            if (!didTransitionToDeallocating) {
                if (slowpath(sideTableLocked)) sidetable_unlock();
                return false;
            }
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }
    // 当extra_rc和引证计数表里的引证计数=0时,开释目标,履行析构函数
deallocate:
    // Really deallocate.
    ASSERT(newisa.isDeallocating());
    ASSERT(isa().isDeallocating());
    if (slowpath(sideTableLocked)) sidetable_unlock();
    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);
    if (performDealloc) {
        this->performDealloc();
    }
    return true;
}

这儿是对散列表里引证计数的操作: 从散列表里取引证计数的一般,-1后赋值给extra_rc。

// Move some retain counts from the side table to the isa field.
// Returns the actual count subtracted, which may be less than the request.
objc_object::SidetableBorrow
objc_object::sidetable_subExtraRC_nolock(size_t delta_rc)
{
    ASSERT(isa().nonpointer);
    SideTable& table = SideTables()[this];
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()  ||  it->second == 0) {
        // Side table retain count is zero. Can't borrow.
        return { 0, 0 };
    }
    size_t oldRefcnt = it->second;
    // isa-side bits should not be set here
    ASSERT((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
    ASSERT((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);
    size_t newRefcnt = oldRefcnt - (delta_rc << SIDE_TABLE_RC_SHIFT);
    ASSERT(oldRefcnt > newRefcnt);  // shouldn't underflow
    it->second = newRefcnt;
    return { delta_rc, newRefcnt >> SIDE_TABLE_RC_SHIFT };
}

2.3. iOS开发 内存留意事项

3. JS内存办理

总结 JavaScript是运用废物收回的编程语言,开发者不需要操心内存分配和收回。JavaScript的废物收回规则为: 1. 离开效果域的值会被主动符号为可收回,然后在废物收回期间被删去 2. 干流的废物收回算法是符号整理,即先给当时不运用的值加上符号,再回来收回他们的内存 3. 引证计数是另一种废物收回战略,需要记录值被引证了多少次。JavaScript引擎不再运用这种算法。在某些旧版本的IE仍然会受这种算法的影响,是因为JavaScript会拜访非原生JavaScript目标(如DOM元素) 4. 引证计数在代码中存在循环引证会出现内存走漏 5. 免除变量的引证可以消除循环引证,而且对废物收回也有协助。为促进废物收回,大局目标、大局目标的特点和循环引证都应该在不需要时触摸引证

3.1 引证计数办理

在前期的JS中,会运用引证计数办理内存,和iOS类似,每个值都会记录它被引证的次数。被引证时,引证数+1,引证免除时,引证数-1。废物收回程序会在每次运转的时候开释引证数=0的内存

3.2 符号整理

目前JS首要用这种办法办理内存。 当变量进入上下文,比如在函数内部声明一个变量时,这个变量会加上存在与上下文中的符号。当变量离开上下文时,也会被加上离开上下文的符号。 废物收回程序运转的时候 1. 会符号内存中存储的所有变量 2. 将所有在上下文中的变量、被在上下文中的变量引证的变量的符号去掉 3. 在此之后再被加上符号的变量便是待删去的变量。是因为没有上下文或变量拜访这些被符号的变量。此时废物收回程序会做一次内存整理 2008年后,干流浏览器都在自己的JavaScript完成中选用符号整理

原始值和引证值

JS变量可以坚持两种类型的值:原始值和引证值。原始值有:UndefinedNullBoolenNumberStringSymbol。区别如下

  1. 原始值巨细固定,保存在栈上
  2. 引证值是目标,存储在堆上
  3. 将一个变量的原始值赋值给另一个变量的原始值,会履行深拷贝
  4. 包含引证值的变量实际上包含的是呼应目标的指针,并不是目标自身
  5. typeof 用于确定值的原始类型,instanceof用于确定值的引证类型

3.1 JS开发,内存留意事项

  1. 经过const和let声明来提高性能。是因为const和let都以块为效果域,因此相较于var,前两者更简单被废物收回程序收回开释内存
  2. 留意内存走漏。意外声明大局变量可能导致内存走漏,如
		function setName() {
		name = '本地日子666';
		}

此时,编译器会把变量作为window的特点来创立(相当于window.name='本地日子666')。在window上创立的特点,只需window存在,name就不会消失。解决计划也很简单,便是在声明变量name的时候加上varletconst关键字

  1. 定时器可能会导致内存走漏
let name = '本地日子'
setInteral(() => {
	console.log(name);
}, 100)

只需定时器一向运转,回调函数中引证的name就会一致占用内存 4. 运用JS闭包形成内存走漏

let outer = function() {
	let name = '本地日子';
	return function() {
		return name;
		};
};

调用outer()会导致分配给name的内存被走漏