第二十篇:iOS内存管理

在iOS里有值类型和对象,值类型不需要我们内存管理。引用类型需要我们内存管理是为什么呢。因为值类型是放在栈里,受系统管理,栈是一块连续的内存空间,引用类型是放在堆里的。

内存5大区:
堆区 -- 通过alloc new malloc创建的对象存在堆区,需要程序员管理内存

栈区 -- 一般存储局部变量 方法参数 对象的指针,栈一般是以0x7开 头的

全局区(静态区)存全局变量\静态变量一般是 0x1开头,编译时候进行内存分配

常量区 -- 常量 一般是0x1开头,编译时候进行内存分配

代码区 -- 编译生成的二进制代码

在栈区一般主线程如果大于1M就会奔溃,会出现内存溢出,其他线程512k。栈是线程独有的空间,。

下面我们定义了一个double,这个是存在栈里的,因为达到1m所以奔溃了


WechatIMG2172.jpeg

什么是引用计数:管理对象生命周期的方式,通过retain,release,autorelease

iOS内存管理方案:
1.nonpointperISA (之前有讲到)
2.tagged point
3.sidetable(散列表)

newisa.extra_rc = 1这个是引用计数,isa其有8x8字节存内存地址,但是存内存地址也用不到这么多,所以其中一些是存引用计数

#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
#endif
        newisa.extra_rc = 1;
    }

下面是nonpointperISA介绍

WechatIMG2173 1.jpeg

下面是tagged point介绍

是针对小对象类型,其实其也是一个指针,只是这个指针被打上tagged,tagged point存着的不再是地址,而是真正的值,3倍的空间效率,106倍的访问速度。

下面可以看到我们创建字符串有多种方式,通过第一种和第三种创建的字符串是__NSCFConstantString,这个是存在常量区。通过stringWithFormat创建的,而且后面字符小于9的话就会存放在NSTaggedPointer里。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSString *firstString = @"helloworld";
    NSString *secondString = [NSString stringWithFormat:@"helloworld"];
    NSString *thirdString = @"hello";
    NSString *fourthSting = [NSString stringWithFormat:@"hello"];

    NSLog(@"%p %@",firstString,[firstString class]);
    NSLog(@"%p %@",secondString,[secondString class]);
    NSLog(@"%p %@",thirdString,[thirdString class]);
    NSLog(@"%p %@",fourthSting,[fourthSting class]);
        
}
WechatIMG2174.jpeg

Tagged Pointer

Tagged Pointer:在总是为0的位置打上了标记的指针,这个指针存放的是真正 的值。
Tagged Pointe会在程序启动时候进行随机值混淆,在intel最低位为1,arm最高位1

q

uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr)这个是进行代码混淆的。其混淆是在下面方法进行操作的。


WechatIMG2349.jpeg

下面的是对Tagged Pointer进行解混淆。

_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    uintptr_t value = _objc_decodeTaggedPointer_noPermute(ptr);
#if OBJC_SPLIT_TAGGED_POINTERS
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;

    value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
    value |= _objc_obfuscatedTagToBasicTag(basicTag) << _OBJC_TAG_INDEX_SHIFT;
#endif
    return value;
}
WechatIMG2350.jpeg

在源码里不同的OBJC_TAG代表不同的类型,如下所示。

enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
    // 60-bit payloads
    OBJC_TAG_NSAtom            = 0, 
    OBJC_TAG_1                 = 1, 
    OBJC_TAG_NSString          = 2, 
    OBJC_TAG_NSNumber          = 3, 
    OBJC_TAG_NSIndexPath       = 4, 
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6,

我们OBJC_TAG_NSNumber来验证下,Tagged Pointer里后三位存的是不同类型值,我们看到下面的是011,这里表示的就是3,也就是OBJC_TAG_NSNumber类型,上面我们确实也是定义的是NSNumber类型。


WechatIMG2351.jpeg

我们来看个关于Tagged Pointer的题目

下面这个代码在运行的时候会发生奔溃,这个是什么原因呢,当我们把标号3里代码注释掉是不会奔溃的,因为标号2里stringWithFormat创建的是一个Tagged Pointer,其存在指针里面,在栈的空间,系统会自己进行管理。标号3里是存的中文,因为其是异步的,所以当一个空间已经release掉了,再发起release就会奔溃了。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    self.queue = dispatch_queue_create("com.lg.cn", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i<100000; i++) {//标号2
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"HPW"];
            NSLog(@"%@",self.nameStr);
        });
    }
    
    for (int i = 0; i<100000; i++) {  //标号3
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"好好学习"];
            NSLog(@"%@",self.nameStr);
        });
    }
}
WechatIMG2353.jpeg

retain原理

下面代码可以看出,当其为TaggedPointer的时候其不会进行objc_retain的插入,而是直接返回。

#if __OBJC2__

__attribute__((aligned(16), flatten, noinline))
id 
objc_retain(id obj)
{
    if (_objc_isTaggedPointerOrNil(obj)) return obj;
    return obj->retain();
}

isa指针存放引用计数方式

这个是Retain的核心代码,首先获取对象的isa指针。下面存储对象的引用计数分为三种情况,如果不是nonpointerisa就会存在sidetable里。当是nonpointerisa里,extrc_rc能够存的下,就会直接存在extrc_rc里。当是nonpointerisa里,extrc_rc存不下的时候,就会借sidetable来存储,但是这个存储在sidetable也只会存一半,这个提高效率。

ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    oldisa = LoadExclusive(&isa.bits);

    if (variant == RRVariant::FastOrMsgSend) {
        // These checks are only meaningful for objc_retain()
        // 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()) {
                return swiftRetain.load(memory_order_relaxed)((id)this);
            }
            return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
        }
    }

    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 (id)this;
        }
    }

Retain总结

WechatIMG2356.jpeg
WechatIMG2357.jpeg

release原理

inline void
objc_object::release()
{
    ASSERT(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        sidetable_release();
        return;
    }

    ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
}

release核心代码是下面的do while循环如下:release的过程也就是retain过程的逆向,

  do {
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            return sidetable_release(sideTableLocked, performDealloc);
        }
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            return false;
        }

        // 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;

Release总结

WechatIMG2358.jpeg

dealloc原理

当满足下面的&&条件时候就会调用free(this)进行释放,isa.has_assoc就是关联对象,isa.has_cxx_dtor就是是否有cxx的构造对象。

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer                     &&
                 !isa.weakly_referenced             &&
                 !isa.has_assoc                     &&
#if ISA_HAS_CXX_DTOR_BIT
                 !isa.has_cxx_dtor                  &&
#else
                 !isa.getClass(false)->hasCxxDtor() &&
#endif
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

object_dispose会调用如下的方法objc_destructInstance,下面的object_cxxDestruct是释放成员变量的。

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
        obj->clearDeallocating();
    }

    return obj;
}

dealloc总结

WechatIMG2359.jpeg

SideTable内存管理

首先其有一把spinlock_t锁,这个spinlock_t锁是保证线程安全的,其是一把互斥锁。RefcountMap这个是用用来存储引用计数的,weak_table就是一个弱引用表。这里有个问题,当每次都进行加锁和解锁的话这样的效率会很低,那苹果是怎样去处理的呢?苹果处理是其添加了多个的表,其中用到了StripedMap,提高了效率。

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }

    void lock() { slock.lock(); }
    void unlock() { slock.unlock(); }
    void forceReset() { slock.forceReset(); }

    // Address-ordered lock discipline for a pair of side tables.

    template<HaveOld, HaveNew>
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template<HaveOld, HaveNew>
    static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

StripedMap如下所示,其在真机环境里创建了64个表提高效率。其是通过indexForPointer这个函数里的算法快速均匀的找到对应的StripedMap。模拟器是8个表,真机上是64个表。每个StripedMap里有多个sideTable, StripedMap是存放sideTable的容器。

template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif

    struct PaddedT {
        T value alignas(CacheLineSize);
    };

    PaddedT array[StripeCount];

    static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }

 public:
    T& operator[] (const void *p) { 
        return array[indexForPointer(p)].value; 
    }
    const T& operator[] (const void *p) const { 
        return const_cast<StripedMap<T>>(this)[p]; 
    }

weak_table是一个弱引用表

我们通过下面方式进行打印其引用计数,第一个打印P的引用计数为1很好理解,第二个用__weak修饰weakP为什么是2呢,那我们探究下这个__weak在底层干了啥?

WechatIMG3.jpeg

我们在__weak处打一个断点,然后点击进去进入到如下图方法,其中工newObj也就是p对象,location代表的是前面的weak指针地址。

WechatIMG4.jpeg

再点击storeWeak进去,HaveOld 是指weak指针是否指向了一个弱引用,HaveNew 是指weak指针是否需要指向一个新的引用, CrashIfDellocating是指引用的对象是否析构

WechatIMG5.jpeg

下面的就是判断是否有haveOld,有的话就把weak的指针地址赋值给oldobj,然后创建sidetables表,如果有haveNew同理。 weak_unregister_no_lock(&oldTable->weak_table, oldObj, location)这个是当weak指向新的表时候,就会把之前的老表移除掉通过这个语句。通过 weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating)进行创建新表并存储。

weak_register_no_lock

weak_register_no_lock是添加弱引用,判断对象是否为nil或者是否是tagged point,如果是返回,如果不是,判断对象是否可用,不可用抛出异常。如果可用,通过weak_entry_for_referent从弱引用表里面找到weak_entry_t,插入weak指针地址,如果没有找到,新建一个weak_entry_t

weak_unregister_no_lock

weak_unregister_no_lock是删除弱引用,在弱引用表里面找到weak_entry_t,从weak_entry_t移除引用,判断weak_entry_t是否为空,如果为空,把weak_entry_t从弱引用表里面移除。

storeWeak(id *location, objc_object *newObj)
{
    ASSERT(haveOld  ||  haveNew);
    if (!haveNew) ASSERT(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;

    // Acquire locks for old and new values.
    // Order by lock address to prevent lock ordering problems. 
    // Retry if the old value changes underneath us.
 retry:
    if (haveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (haveNew) {
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }

    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);

    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
        goto retry;
    }
 // Clean up old value, if any.
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    // Assign new value, if any.
    if (haveNew) {
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
        // weak_register_no_lock returns nil if weak store should be rejected

        // Set is-weakly-referenced bit in refcount table.
        if (!_objc_isTaggedPointerOrNil(newObj)) {
            newObj->setWeaklyReferenced_nolock();
        }

        // Do not set *location anywhere else. That would introduce a race.
        *location = (id)newObj;
    }
    else {
        // No new value. The storage is not changed.
    }
    
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

    // This must be called without the locks held, as it can invoke
    // arbitrary code. In particular, even if _setWeaklyReferenced
    // is not implemented, resolveInstanceMethod: may be, and may
    // call back into the weak reference machinery.
    callSetWeaklyReferenced((id)newObj);

    return (id)newObj;
}

上面storeWeak的方法作用:
  如果weak指针之前指向了一个弱引用,它就会调用这个weak_unregister_no_lock将weak指针地址进行移除。如果这个weak指针指向了一个新的引用,它就会调用weak_register_no_lock将weak指针地址添加到对应对象的弱引用表,然后调用setWeaklyReferenced_nolock,把修饰isa的weakly_referenced置为YES。

接着我们看下弱引用表, weak_entry_t *weak_entries 这里是一个数组 ,我们弱应用对象就是存在这里面的。

struct weak_table_t {
    weak_entry_t *weak_entries;这里是一个数组 
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};

weak_entry_t的源代码如下,当对象的弱引用个数小于等于4的时候就会使用标记2里struct 结构体进行存储,大于4就会使用标记1里struct 结构体进行存储。out_of_line是判断用的哪种方式存储。

struct weak_entry_t {
    DisguisedPtr<objc_object> referent;
    union {
        struct {//标记1
            weak_referrer_t *referrers;
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };//标记1
        struct {//标记2
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };//标记2
    };

    bool out_of_line() {
        return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
    }

    weak_entry_t& operator=(const weak_entry_t& other) {
        memcpy(this, &other, sizeof(other));
        return *this;
    }

    weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
        : referent(newReferent)
    {
        inline_referrers[0] = newReferrer;
        for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
            inline_referrers[i] = nil;
        }
    }
};

sideTable与weakTable与entrt_t以及weak的关系

每个对象对应一个sideTable,sideTable里有个weakTable,weakTable里有个entrt_t的数组,其中弱引用weak就是存放在entrt_t里的。


WechatIMG6.jpeg

上面讲清了sideTable,weakTable的原理,下面我们继续上面那个问题进行探讨,第二个用__weak修饰weakP为什么是2呢?,打断点调试后,然后用汇编指令操作,发现其会走到objc_loadWeakRetained这个地方,我们在源码里进行搜索,然后进行定位,其最终会走到rootretain这个地方,所以引用计数会有所增加,但是这个是一个假象,因为其在rootretain对应函数里的作用域里,但是出了这个作用域就会变为原来的值,那我们再打印一下就知道了,还是1,所以说之前的是假像。如下图:

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

推荐阅读更多精彩内容