IOS内存管理

673 阅读8分钟

这篇文章将分析苹果的内存管理的相关知识点,本文章都是基于arm64架构下的输出。分为以下几点内容

1.内存的布局 2.引用计数 3.弱引用/强引用 4.自动释放池

内存布局

关于内存布局,这里放两张图,尊重原创,图片的出处来自于 作者对内存布局的不同区域都做了总结。

2679624-dbbff7795755d87d.png

2679624-e0d5f4f8c047a8d5.png 打印一下看看,栈区,堆区,常量区,静态区的地址有没有什么差异: x86_64架构下:发现栈的地址是0x7开始,堆的内存地址0x6开始,静态常量0x1 arm_64架构下:栈的地址 静态常量 是0x1开始,堆的地址从0x2开始

**2021-09-10 14:41:54.922932+0800 001---五大区Demo[3358:229699] obj栈的地址=0x7ffeeb3e04d8, 堆的内存地址=0x60000396c550**
**2021-09-10 14:41:54.923074+0800 001---五大区Demo[3358:229699] a == 0x7ffeeb3e04d4**
**2021-09-10 14:41:54.923140+0800 001---五大区Demo[3358:229699] b == 0x7ffeeb3e04d0**
**2021-09-10 14:41:54.923221+0800 001---五大区Demo[3358:229699] ************静态区**************
**2021-09-10 14:41:54.923317+0800 001---五大区Demo[3358:229699] clA == 0x10482045c**
**2021-09-10 14:41:54.923398+0800 001---五大区Demo[3358:229699] bssA == 0x104820460**
**2021-09-10 14:41:54.923470+0800 001---五大区Demo[3358:229699] bssStr1 == 0x104820468**
**2021-09-10 14:41:54.923570+0800 001---五大区Demo[3358:229699] ************常量区**************
**2021-09-10 14:41:54.923654+0800 001---五大区Demo[3358:229699] clB == 0x104820440**
**2021-09-10 14:41:54.923724+0800 001---五大区Demo[3358:229699] bssB == 0x104820444**
**2021-09-10 14:41:54.923792+0800 001---五大区Demo[3358:229699] bssStr2 == 0x104820448**
2021-09-10 14:47:46.683094+0800 001---五大区Demo[515:92250] obj栈的地址=0x16d5f8f48, 堆的内存地址=0x281d150e0
2021-09-10 14:47:46.683186+0800 001---五大区Demo[515:92250] a == 	0x16d5f8f44
2021-09-10 14:47:46.683226+0800 001---五大区Demo[515:92250] b == 	0x16d5f8f40
2021-09-10 14:47:46.683267+0800 001---五大区Demo[515:92250] ************静态区************
2021-09-10 14:47:46.683302+0800 001---五大区Demo[515:92250] clA == 	0x10280d45c
2021-09-10 14:47:46.683334+0800 001---五大区Demo[515:92250] bssA == 	0x10280d460
2021-09-10 14:47:46.683367+0800 001---五大区Demo[515:92250] bssStr1 == 	0x10280d468
2021-09-10 14:47:46.683407+0800 001---五大区Demo[515:92250] ************常量区************
2021-09-10 14:47:46.683589+0800 001---五大区Demo[515:92250] clB == 	0x10280d440
2021-09-10 14:47:46.683727+0800 001---五大区Demo[515:92250] bssB == 	0x10280d444
2021-09-10 14:47:46.683841+0800 001---五大区Demo[515:92250] bssStr2 == 	0x10280d448

知识点:静态变量只有文件拥有者才有权限修改,其它文件只可读取。

引用计数

引用计数存在ISA的extra_rc下,如果extra_rc存满了,大于10的时候就需要使用到has_sidetable_rc散列表

      define ISA_BITFIELD                                                        \
      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 unused            : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8

我们用strong修饰的属性,在赋值的时候set方法会触发objc_object::rootRetain进行应用计数处理

截屏2021-09-15 上午11.56.40.png 通过生成中间层代码后我们发现,set方法编译器增加了obj_storeStrong方法, clang -S -fobjc-arc -emit-llvm main.m -o main.ll。

; Function Attrs: noinline optnone ssp uwtable
define internal void @"\01-[LGTeacher setPerson:]"(%0* %0, i8* %1, %2* %2) #1 {
  %4 = alloca %0*, align 8
  %5 = alloca i8*, align 8
  %6 = alloca %2*, align 8
  store %0* %0, %0** %4, align 8
  store i8* %1, i8** %5, align 8
  store %2* %2, %2** %6, align 8
  %7 = load %2*, %2** %6, align 8
  %8 = load %0*, %0** %4, align 8
  %9 = bitcast %0* %8 to i8*
  %10 = getelementptr inbounds i8, i8* %9, i64 16
  %11 = bitcast i8* %10 to %2**
  %12 = bitcast %2** %11 to i8**
  %13 = bitcast %2* %7 to i8*
  call void @llvm.objc.storeStrong(i8** %12, i8* %13) #3
  ret void

断点走下去可以看到几个核心的函数,如果不是nopointisa会发现SideTable里面的refcntStorage做了一个+2的操作,直接操作散列表。

objc_object::sidetable_retain(bool locked)
{
#if SUPPORT_NONPOINTER_ISA
    ASSERT(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];
    
    if (!locked) table.lock();
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    table.unlock();

    return (id)this;
}

如果是nonpointerisa,先判断是否在执行析构函数。不是在析构会对newisa.bits的extra_rc+1,多了放到carry中。extra_rc在StoreExclusive方法中对引用计数进行了+1操作。

slowpath(newisa.isDeallocating())

newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

newisa.extra_rc = RC_HALF;//extra_rc存一半

sidetable_addExtraRC_nolock(RC_HALF);//散列表存一半 

StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)) // extra_rc++

下面看一下弱引用oc是怎么处理的。当我们用weak修饰一个变量的时候,可以看到调用了storeWeak函数

@property(nonatomic, weak) LGPerson *person;

; Function Attrs: noinline optnone ssp uwtable
define internal void @"\01-[LGTeacher setPerson:]"(%0* %0, i8* %1, %2* %2) #1 {
  %4 = alloca %0*, align 8
  %5 = alloca i8*, align 8
  %6 = alloca %2*, align 8
  store %0* %0, %0** %4, align 8
  store i8* %1, i8** %5, align 8
  store %2* %2, %2** %6, align 8
  %7 = load %2*, %2** %6, align 8
  %8 = load %0*, %0** %4, align 8
  %9 = bitcast %0* %8 to i8*
  %10 = getelementptr inbounds i8, i8* %9, i64 16
  %11 = bitcast i8* %10 to %2**
  %12 = bitcast %2** %11 to i8**
  %13 = bitcast %2* %7 to i8*
  %14 = call i8* @llvm.objc.storeWeak(i8** %12, i8* %13) #3
  ret void

进入到storeWeak中可以看到SideTabble结构体

struct SideTable {
    spinlock_t slock; //锁
    RefcountMap refcnts;//散列表 存储引用计数
    weak_table_t weak_table;//散列表 存储弱引用计数
    weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);//操作弱引用计数表
    
    if ((entry = weak_entry_for_referent(weak_table, referent))) { //如果散列表存在就追加
        append_referrer(entry, referrer);
    } 
    else {
        weak_entry_t new_entry(referent, referrer);//创建一个new_entry实体
        weak_grow_maybe(weak_table);
        weak_entry_insert(weak_table, &new_entry);
    }
    
    struct weak_entry_t {//结构体
    DisguisedPtr<objc_object> referent;
    union {
        struct {
            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;
        };
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };
    
    entry->inline_referrers[i] = new_referrer;

weak_table弱引用表里面存对象的弱引用,每个对象的属性也存在弱引用表。

截屏2021-09-15 下午8.41.17.png 如下图的弱引用处理办法,就算object实际被销毁了,weakobj3不会收到影响,weakobj3并不管理object对象。弱引用表和对象相对独立,各自管理。 截屏2021-09-16 上午11.08.19.png

自动释放池

在main函数入口,会用一个@autoreleasepool自动释放池去管理UIApplicationMain应用的运行。我们通过clang看看汇编内容是什么。再通过汇编方式看看调用的什么函数。libobjc.A.dylib`objc_autoreleasePoolPush:

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ 
    { 
        __AtAutoreleasePool __autoreleasepool; 
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_1y_3mygr0nx0c5dnvbk_r66nhtc0000gn_T_main_677664_mi_0);
    }
}

截屏2021-09-16 上午11.52.07.png

objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

看到autoreleasepoolpush的说明,首先是1.有一个POOL_BOUNDARY边界管理,2有区分hot page和cool page,对象使用的情况,3.自动释放池就是一个双向列表。找到了自动释放池的夫类,主要包含以下属性

magic_t const magic; // 16 校验page是否完整
__unsafe_unretained id *next; // 8 最新添加的autorelease对象的下一个位置
pthread_t const thread; // 8 当前线程
AutoreleasePoolPage * const parent; // 8 第一个节点的parent nil
AutoreleasePoolPage *child; // 8 最后一个节点的child nil
uint32_t const depth; // 4
uint32_t hiwat;  // 4 入栈最大数量标记

通过一个简单例子,我们看一下自动释放池管理的对象的情况

extern void  _objc_autoreleasePoolPrint(void);

int main(int argc, const char *argv[]){
    @autoreleasepool {
        NSObject *obj = [[[NSObject alloc] init] autorelease];
        _objc_autoreleasePoolPrint();
    }
    return 0;
}

objc[3237]: ##############
objc[3237]: AUTORELEASE POOLS for thread 0x1000ebe00
objc[3237]: 2 releases pending.
objc[3237]: [0x10880a000]  ................  PAGE  (hot) (cold)
objc[3237]: [0x10880a038]  ################  POOL 0x10880a038 //哨兵对象
objc[3237]: [0x10880a040]       0x101505760  NSObject //管理对象
objc[3237]: ##############

接下来进入到push函数里面,定位到autoreleaseFast,判断当前hotpage是否满了,满了就创建autoreleaseNoPage,不满就add

    static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
            return page->add(obj);
        } else if (page) {
            return autoreleaseFullPage(obj, page);
        } else {
            return autoreleaseNoPage(obj);
        }
    }

autoreleaseNoPage函数的部分逻辑

// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); //创建page
setHotPage(page);//设置为hotpage

if (pushExtraBoundary) {
    page->add(POOL_BOUNDARY); //增加边界
}
//AutoreleasePoolPage的构造函数
AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
    AutoreleasePoolPageData(begin(),
                            objc_thread_self(),
                            newParent,
                            newParent ? 1+newParent->depth : 0,
                            newParent ? newParent->hiwat : 0)
{
    if (objc::PageCountWarning != -1) {
        checkTooMuchAutorelease();
    }

    if (parent) {
        parent->check();
        ASSERT(!parent->child);
        parent->unprotect();
        parent->child = this;
        parent->protect();
    }
    protect();
}
    

思考每一个AutoreleasePoolPage对象到底存多少数据,到达多少需要开辟新的page,我们用for循环的方式增加对象,看一下输出结果

截屏2021-09-17 上午9.50.24.png

截屏2021-09-17 上午9.53.56.png

计算一页的空间504*8 + 56(自身) + 8(哨兵对象) = 4096 4k。 一个自动释放池中只需要一个哨兵对象。

一个知识点 alloc new copy mutablecopy都不会加入自动释放池。