iOS底层学习 - 内存管理以内存管理方案

不一样的系统版本对 App 运行时占用内存的限制不一样,超过限制时,App就会被强制杀死,因此对于内存的要求也就愈来愈高,因此本章来探索一下iOS中的内存管理方案c++

移动端的内存管理技术,主要有 GC(Garbage Collection,垃圾回收)的标记清除算法和苹果公司使用的引用计数方法程序员

相比较于 GC 标记清除算法,引用计数法能够及时地回收引用计数为 0 的对象,减小查找次数。可是,引用计数会带来循环引用的问题,好比当外部的变量强引用 Block 时,Block 也会强引用外部的变量,就会出现循环引用。咱们须要经过弱引用,来解除循环引用的问题。面试

另外,在 ARC(自动引用计数)以前,一直都是经过 MRC(手动引用计数)这种手写大量内存管理代码的方式来管理内存,所以苹果公司开发了 ARC 技术,由编译器来完成这部分代码管理工做。可是,ARC 依然须要注意循环引用的问题。当 ARC 的内存管理代码交由编译器自动添加后,有些状况下会比手动管理内存效率低,因此对于一些内存要求较高的场景,咱们仍是要经过 MRC 的方式来管理、优化内存的使用。算法

内存布局

首先咱们再来回顾一下,OS/iOS系统的内存布局c#

在iOS程序的内存中,从底地址开始,到高地址一次分为:程序区域、数据区域、堆区、栈区。其中程序区域主要是代码段,数据区域包括数据段和BSS段。咱们具体分析一下各个区域所表明的含义数组

  • 栈区(stack):编译器自动分配,由系统管理,在不须要的时候自动清除。局部变量、函数参数存储在这里。栈区的内存地址通常是0x7开头,从高地址到底地址分配内存空间
  • 堆区 (heap): 那些由newallocblockcopy建立的对象存储在这里,是由开发者管理的,须要告诉系统何时释放内存。ARC下编译器会自动在合适的时候释放内存,而在MRC下须要开发者手动释放。堆区的内存地址通常是0x6开头,从底地址到高地址分配内存空间
  • _DATA区:主要分为BSS(静态区)和数据段(常量区)
    • BSS(静态区):BSS段又称静态区,未初始化的全局变量,静态变量存放在这里。一旦初始化就会被回收,而且将数据转存到数据段中。
    • 数据段(常量区):数据段又称常量区,专门存放常量,直到程序结束的时候才会被回收。
  • 代码段:用于存放程序运行时的代码,代码会被编译成二进制存进内存的程序代码区。程序结束时系统会自动回收存储在代码段中的数据。

小问题:static静态变量的做用域

首先先查看下面代码,age的打印结果是多少呢?bash

static int age = 10;

@interface Person : NSObject
-(void)add;
+(void)reduce;
@end

@implementation Person

- (void)add {
    age++;
    NSLog(@"Person内部:%@-%p--%d", self, &age, age);
}

+ (void)reduce {
    age--;
    NSLog(@"Person内部:%@-%p--%d", self, &age, age);
}
@end


@implementation Person (WY)

- (void)wy_add {
    age++;
    NSLog(@"Person (wy)内部:%@-%p--%d", self, &age, age);
}

@end

- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"vc:%p--%d", &age, age);
    age = 40;
    NSLog(@"vc:%p--%d", &age, age);
    [[Person new] add];
    NSLog(@"vc:%p--%d", &age, age);
    [Person reduce];
    NSLog(@"vc:%p--%d", &age, age);
    [[Person new] wy_add];
}
复制代码

经过打印结果,咱们能够得出这么一个结论:数据结构

静态变量的做用域与对象、类、分类不要紧,只与文件有关系。架构

内存优化方案

iOS除了使用ARC来进行自动引用计数之外,还有一些其余的内存优化方案,主要有Tagged Ponter,NONPOINTER_ISA,SideTable3种async

Tagged Ponter

概述

在 2013 年 9 月,苹果推出了 iPhone5s,与此同时,iPhone5s 配备了首个采用 64 位架构的 A7 双核处理器,为了节省内存和提升执行效率,苹果提出了Tagged Pointer的概念。

使用Tagged Pointer以前,若是声明一个NSNumber *number = @10;变量,须要一个占8字节的指针变量number,和一个占16字节的NSNumber对象,指针变量number指向NSNumber对象的地址。这样须要耗费24个字节内存空间。

而使用Tagged Pointer以后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中。直接将数据10保存在指针变量number中,这样仅占用8个字节。

可是当指针不够存储数据时,就会使用动态分配内存的方式来存储数据。

经过下面面试题的打印结果能够很好的反映出来

//第1段代码
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            self.name = [NSString stringWithFormat:@"asdasdefafdfa"];
        });
    }
    NSLog(@"end");
复制代码
//第2段代码
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            self.name = [NSString stringWithFormat:@"abc"];
        });
    }
    NSLog(@"end");
复制代码

上面两段代码的打印结果过是: 第1段会发生崩溃,而第2段不会。

按道理讲,建立多个线程来对name进行操做时,name的值会不断的retainrelease,此时就会出现资源竞争而崩溃,可是第二段却不会崩溃,说明在Tagged Pointer下,较小的值,不会调用set和get等方法,因为其值是直接存储在指针变量中的,因此能够直接修改。

经过源码,咱们也能够比较直观的看出,Tagged Pointer类型对象是直接返回的。

总结一下使用Tagged Pointer的好处

  • Tagged Pointer是专⻔⽤来存储⼩的对象,例如NSNumberNSDate等。
  • Tagged Pointer指针的值再也不是地址了,⽽是真正的值。因此,实际上它再也不是⼀个对象了,它只是⼀个披着对象⽪的普通变量⽽已。因此,它的内存并不存储在堆中,也不须要mallocfree
  • 当指针不够存储数据时,就会使用动态分配内存的方式来存储数据。
  • 在内存读取上有着3倍的效率,建立时⽐之前快106倍。

源码

static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}

static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
    if (tag <= OBJC_TAG_Last60BitPayload) {
        uintptr_t result =
            (_OBJC_TAG_MASK | 
             ((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) | 
             ((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
        return _objc_encodeTaggedPointer(result);
    } else {
        uintptr_t result =
            (_OBJC_TAG_EXT_MASK |
             ((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
             ((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
        return _objc_encodeTaggedPointer(result);
    }
}

复制代码

从上面的这代码能够看出来,系统调用了_objc_decodeTaggedPointer_objc_taggedPointersEnabled这两个方法对于taggedPointer对象的指针进行了编码和解编码,这两个方法都是将指针地址和objc_debug_taggedpointer_obfuscator进行异或操做

咱们都知道将a和b异或操做获得c再和a进行异或操做即可以从新获得a的值,一般可使用这个方式来实现不用中间变量实现两个值的交换。Tagged Pointer正是使用了这种原理。经过这种解码的方法,咱们能够获得对象真正的指针地址

下面是系统定义的各类Tagged Pointer标志位。

enum objc_tag_index_t : uint16_t
{
    // 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,

    // 60-bit reserved
    OBJC_TAG_RESERVED_7        = 7, 

    // 52-bit payloads
    OBJC_TAG_Photos_1          = 8,
    OBJC_TAG_Photos_2          = 9,
    OBJC_TAG_Photos_3          = 10,
    OBJC_TAG_Photos_4          = 11,
    OBJC_TAG_XPC_1             = 12,
    OBJC_TAG_XPC_2             = 13,
    OBJC_TAG_XPC_3             = 14,
    OBJC_TAG_XPC_4             = 15,

    OBJC_TAG_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 
    OBJC_TAG_Last52BitPayload  = 263, 

    OBJC_TAG_RESERVED_264      = 264
};

复制代码

NONPOINTER_ISA

关于NONPOINTER_ISA咱们在以前的博客中有讲过,这也是苹果的一种内存优化的方案。用 64 bit 存储一个内存地址显然是种浪费,毕竟不多有那么大内存的设备。因而能够优化存储方案,用一部分额外空间存储其余内容。isa 指针第一位为 1 即表示使用优化的 isa 指针。能够阅读☞iOS底层学习 - OC对象前世此生

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
      uintptr_t nonpointer        : 1;                                         
      uintptr_t has_assoc         : 1;                                         
      uintptr_t has_cxx_dtor      : 1;                                         
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ 
      uintptr_t magic             : 6;                                         
      uintptr_t weakly_referenced : 1;                                         
      uintptr_t deallocating      : 1;                                         
      uintptr_t has_sidetable_rc  : 1;                                         
      uintptr_t extra_rc          : 8
    };
#endif
};

复制代码

SideTable

NONPOINTER_ISA中有两个成员变量has_sidetable_rcextra_rc,当extra_rc的19位内存不够存储引用计数时has_sidetable_rc的值就会变为1,那么此时引用计数会存储在SideTable中。

SideTables能够理解为一个全局的hash数组,里面存储了SideTable类型的数据,其长度为64,就是里面有64个SideTable。

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
}
复制代码

查看SideTable的源码,其3个成员变量,表明的意义以下:

  • spinlock_t:自旋锁,用于上锁/解锁 SideTable。
  • RefcountMap:用来存储OC对象的引用计数的 hash表(仅在未开启isa优化或在isa优化状况下isa_t的引用计数溢出时才会用到)。
  • weak_table_t:存储对象弱引用指针的hash表。是OC中weak功能实现的核心数据结构。

有关于弱引用表weak_table_t,能够阅读☞iOS底层学习 - 内存管理之weak原理探究

// RefcountMap disguises its pointers because we 
// do not want the table to act as a root for `leaks`.
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,RefcountMapValuePurgeable> RefcountMap;
复制代码

实质上是模板类型objc::DenseMap。模板的三个类型参数DisguisedPtr<objc_object>size_ttrue 分别表示DenseMap的 key类型value类型、是否须要在value == 0 的时候自动释放掉响应的hash节点,这里是true。

MRC与ARC

MRC与ARC概念

MRC

在MRC时代,程序员须要手动的去管理内存,建立一个对象时,须要在set方法和get方法内部添加释放对象的代码。而且在对象的dealloc里面添加释放的代码。

@property (nonatomic, strong) Person *person;

- (void)setPerson:(Person *)person {
    if (_person != person) {
        [_person release];
        _person = [person retain];
    }
}

- (Person *) person {
    return _person;
}
复制代码

ARC

在ARC环境中,咱们再也不像之前同样本身手动管理内存,系统帮助咱们作了release或者autorelease等事情。 ARC是LLVM编译器RunTime协做的结果。其中LLVM编译器自动生成releasereatinautorelease的代码,像weak弱引用这些则靠RunTime在运行时释放。

引用计数

引用计数是一种内存管理技术,是指将资源(能够是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术能够实现自动资源管理的目的。同时引用计数还能够指使用引用计数技术回收未使用资源的垃圾回收算法。

在iOS中,对象执行reatin等操做后,该对象的引用计数就会+1,调用release等操做后,改对象的引用计数就会-1

引用计数规则

关于引用计数的规则,能够总结以下:

  1. 本身生成的对象,本身持有。(alloc,new,copy,mutableCopy等)
  2. 非本身生成的对象,本身也能持有。(retain 等)
  3. 再也不须要本身持有的对象时释放。(release,dealloc 等)
  4. 非本身持有的对象没法释放。

原理探索

alloc与retainCount原理

有关alloc的流程,能够阅读☞iOS底层学习 - OC对象前世此生来进行查看,就不作过多的赘述。

可是有一个细节须要注意,alloc自己是只申请内存空间,不增长引用计数的。此时isaextra_rc为0。

可是为何咱们打印retainCount时,显示的是1呢,咱们经过查看源码能够发现uintptr_t rc = 1 + bits.extra_rc;其自己前面是会加一个常量1的,用来标记本身生成的对象的引用计数。

inline uintptr_t 
objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}
复制代码

reatin原理

经过如下源码,咱们能够知道:

  • TaggedPointer对象是不参与引用计数的
  • 小几率若有hasCustomRR,会走消息发送
inline id objc_object::retain()
{
    assert(!isTaggedPointer());

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

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

复制代码

接着咱们查看rootRetain方法,其主要处理以下:

  1. 判断当前对象是否一个TaggedPointer,若是是则返回。
  2. 判断isa是否通过NONPOINTER_ISA优化,若是未通过优化,则将引用计数存储在SideTable中。64位的设备不会进入到这个分支。
  3. 判断当前的设备是否正在析构。
  4. 将isa的bits中的extra_rc进行加1操做。
  5. 若是在extra_rc中已经存储满了,则调用sidetable_addExtraRC_nolock方法将一半的引用计数移存到SideTable中。
ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    ✅//若是是TaggedPointer 直接返回
    if (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    do {
        transcribeToSideTable =false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        ✅// 若是isa未通过NONPOINTER_ISA优化
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();//引用计数存储于SideTable中
        }
        // donot check newisa.fast_rr; we already called any RR overrides
        ✅//检查对象是都正在析构
        if (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        uintptr_t carry;
        ✅//isa的bits中的extra_rc进行加1
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
        ✅//若是bits的extra_rc已经存满了,则将其中的一半存储到sidetable中
        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;//extra_rc置空一半的数值
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        ✅//将另外的一半引用计数存储到sidetable中
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}
复制代码

其余

关于release的相关原理,和retain的原理相反,你们能够本身去探究一下;

关于dealloc的相关原理,在iOS底层学习 - 内存管理之weak原理探究中也有解析

总结

  • 内存布局:
    • 内存地址由高到低分为栈区(自动处理内存)、堆区(开发者管理,ARC下会自动释放)、静态区(全局变量,静态变量)、数据段(常量)、代码段(编写的二进制代码区)
    • static静态变量的做用域与对象、类、分类不要紧,只与文件有关系。
  • 内存优化方案:
    • Tagged Pointer
      • Tagged Pointer是专⻔⽤来存储⼩的对象,例如NSNumber,NSDate等。
      • Tagged Pointer指针的值再也不是地址了,⽽是真正的值。因此,实际上它再也不是⼀个对象了,它只是⼀个披着对象⽪的普通变量⽽已。因此,它的内存并不存储在堆中,也不须要malloc和free。
      • 当指针不够存储数据时,就会使用动态分配内存的方式来存储数据。
      • 在内存读取上有着3倍的效率,建立时⽐之前快106倍。
    • NONPOINTER_ISA
      • 经过nonpointer标志是否开启指针优化
      • 经过extra_rc来存储引用计数,根据系统架构不一样,长度不一样
      • 经过has_sidetable_rc来判断超出extra_rc后,是否有全局SideTable存储引用计数
    • SideTable
      • SideTables是一个全局的哈希数组,里面存储了SideTable类型数据
      • SideTablespinlock_t(自旋锁,用于上/解锁)、RefcountMap(存储extra_rc溢出或者未开启优化的引用计数)、weak_table_t(存储弱引用表)
  • 引用计数规则:
    • 本身生成的对象,本身持有。(alloc,new,copy,mutableCopy等)
    • 非本身生成的对象,本身也能持有。(retain 等)
    • 再也不须要本身持有的对象时释放。(release,dealloc 等)
    • 非本身持有的对象没法释放。
  • alloc自己是只申请内存空间,不增长引用计数的。此时isa中extra_rc为0。只是调用rootRetainCount其自己前面是会加一个常量1的,用来标记本身生成的对象的引用计数。
  • reatin原理:
    • 判断当前对象是否一个TaggedPointer,若是是则返回。
    • 判断isa是否通过NONPOINTER_ISA优化,若是未通过优化,则将引用计数存储在SideTable中。64位的设备不会进入到这个分支。
    • 判断当前的设备是否正在析构。
    • 将isa的bits中的extra_rc进行加1操做。
    • 若是在extra_rc中已经存储满了,则调用sidetable_addExtraRC_nolock方法将一半的引用计数移存到SideTable中。

参考

内存管理(一)

iOS内存管理一:Tagged Pointer&引用计数

相关文章
相关标签/搜索