美文网首页
ObjectiveC和JS的内存管理区别

ObjectiveC和JS的内存管理区别

作者: 码代码的小马 | 来源:发表于2023-04-12 11:02 被阅读0次

    入职后从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中,主要把内存分为五大区,从高到底分别为

    • 栈区 存放函数变量和函数参数
    • 堆区 存放动态分配的内存段
    • 全局静态区 存放全局变量、静态变量
    • 常量区 存放常量
    • 代码区 存放程序代码
      35edb41f49616b06502653de4cd9713a (1).png

    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)
    他的数据结构如下:

    d6f280d657940f12c4bc88014d32be3b.png

    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就会一致占用内存

    1. 使用JS闭包造成内存泄露
    let outer = function() {
        let name = '本地生活';
        return function() {
            return name;
            };
    };
    

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

    相关文章

      网友评论

          本文标题:ObjectiveC和JS的内存管理区别

          本文链接:https://www.haomeiwen.com/subject/vvanddtx.html