iOS 内存布局&内存管理方案

内存布局-五大区

image.png
  1. 栈区 0x7
    创建临时变量时由编译器自动分配,在不需要的时候自动清除的变量的存储区。
    里面的变量通常是局部变量、函数参数等。在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。
  1. 堆区 0x6
    那些由 new alloc 创建的对象所分配的内存块,它们的释放系统不会主动去管,由我们的开发者去告诉系统什么时候释放这块内存(一个对象引用计数为0是系统就会回销毁该内存区域对象)。一般一个 new 就要对应一个 release。在ARC下编译器会自动在合适位置为OC对象添加release操作。会在当前线程Runloop退出或休眠时销毁这些对象,MRC则需程序员手动释放。
    堆可以动态地扩展和收缩。

  2. 静态区(未初始化数据).bss
    程序运行过程内存的数据一直存在,程序结束后由系统释放

  3. 常量区(已初始化数据).data
    专门用于存放常量,程序结束后由系统释放

  4. 代码区
    用于存放程序运行时的代码,代码会被编译成二进制存进内存的程序代码区

内存管理方案

TaggedPointer

通常我们创建对象,对象存储在堆中,对象的指针存储在栈中,如果我们要找到这个对象,就需要先在栈中,找到指针地址,然后根据指针地址找到在堆中的对象。
这个过程比较繁琐,当存储的对象只是一个很小的东西,比如一个字符串,一个数字。去走这么一个繁琐的过程,无非是耗费性能的,所以苹果就搞出了TaggedPointer这么一个东西。

  1. TaggedPointer是苹果为了解决32位CPU到64位CPU的转变带来的内存占用和效率问题,针对NSNumber、NSDate以及部分NSString的内存优化方案。

  2. Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。

  3. Tagged Pointer指针中包含了当前对象的地址、类型、具体数值。因此Tagged Pointer指针在内存读取上有着3倍的效率,创建时比普通需要malloc跟free的类型快106倍。

这里有对TaggedPointer进行详细介绍

面试题

为什么第二个for会崩溃?

    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        for (int i = 0 ; i<1000000; i++) {
            self.str = @"abcd";
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0 ; i<1000000; i++) {
            self.str = [NSString stringWithFormat:@"adfalkdjfldkasjflakjsdkflasf-- %d",I];
        }
    });

答:taggedpointer。
在setproperty函数中,执行了objc_release(id obj)
由于大量的循环,导致了线程问题,使引用计数<=-1
但是由于第一个循环中的obj是taggedpointer类型的string,会直接return obj,并不会release。
但是这里release,retain的时候咋办呢,引用计数是一直往上增吗?并不是,在objc_retain(id obj)中,同样判断了obj->isTaggedPointer,如果是true,就直接return obj。

NONPOINTER_ISA

要说isa,得先从对象开始。

NSObject继承关系:

NSObject -> Class -> objc_class -> objc_object -> isa_t

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

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

isa_t是联合体,然后重点看ISA_BITFIELD,


image.png

参数解释:

nonpointer:表示是否对 isa 指针开启指针优化 ,
0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等。

has_assoc:关联对象标志位,0没有,1存在。

has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更更快的释放对象。

shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤来存储类指针的值。

magic:用于调试器判断当前对象是真的对象还是没有初始化的空间 。

weakly_referenced:标志对象是否被指向或者曾经指向⼀一个 ARC 的弱变量,没有弱引用的对象可以更快释放。

deallocating:标志对象是否正在释放内存。

has_sidetable_rc:是否有用到散列表,当对象引⽤计数大于 10 时,则需要借⽤该变量存储进位。

extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1。例如,如果对象的引用计数为 10,那么 extra_rc 为 9。
例:在x86_64(mac)的架构下,如果引用计数大于 255,引用计数将会发生溢出。 溢出时,则需要将has_sidetable_rc标记为1,将会将拿出2的7次方(128,就是上面的RC_HALF)放入散列表(sidetable)

那么has_sidetable_rc是怎么操作的呢?

SideTables 散列表

散列表.png
SideTables

SideTables是一个数组,里面存着很多SideTable。(注意看有没有s)
这是对象引用计数溢出时,会调用这个方法,将一半的引用计数存入sideTable。

image.png

在这方法里,我们可以看到这个方法,通过SideTables获取一个SideTable

SideTable& table = SideTables()[this];

那么这里要看的就应该是SideTables()

SideTables.png

这里我觉得可以理解成SideTables()就是一个StripedMap,继续看StripedMap

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) {
        //reinterpret_cast是C++里的强制类型转换符。
        //这里是将16进制转成10进制
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        // 这里StripeCount是64,看上面第755行
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }

 public:
    //重载中括号 , c++特有
    //要让”[]”内的操作数支持const void类型
    T& operator[] (const void *p) {
        // 调用indexForPointer(),获取sidetable
        return array[indexForPointer(p)].value; 
    }
    const T& operator[] (const void *p) const { 
        return const_cast<StripedMap<T>>(this)[p]; 
    }
    ...
}

看完上面的代码+注释,我们走一波lldb调试,分别打印各个参数


image.png

这里就是一个获取SideTable的过程

<1> SideTable& table = SideTables()[this];传入一个this指针对象
<2> 通过indexForPointer获取当前指针对象所对应的下标
<3> 通过array[indexForPointer(p)].value 返回一个SideTable

初探SideTable

spinlock_t:自旋锁、
RefcountMap:引用计数Map,是个C++的Map

weak_table_t:全局弱引用表


image.png
SideTable操作

这里举个例子
sidetable_addExtraRC_nolock在sideTable中添加RetainCount(RC)

bool 
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
    assert(isa.nonpointer);
    // 通过SideTables() 获取SideTable
    SideTable& table = SideTables()[this];

    //获取引用计数的size
    size_t& refcntStorage = table.refcnts[this];
    // 赋值给oldRefcnt
    size_t oldRefcnt = refcntStorage;
    // isa-side bits should not be set here
    assert((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
    assert((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);

    // 如果oldRefcnt & SIDE_TABLE_RC_PINNED = 1
    // 就是 oldRefcnt = 2147483648 (32位情况)
    if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;
    
    //引用计数也溢出判断参数
    uintptr_t carry;
    
    // 引用计数 add
    //delta_rc左移两位,右边的两位分别是DEALLOCATING(销毁ing) 跟WEAKLY_REFERENCED(弱引用计数)
    size_t newRefcnt = 
        addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
    //如果sidetable也溢出了。
    //这里我for了几百万次,也没有溢出,可见sidetable能容纳很多的引用计数
    if (carry) {
        // 如果是32位的情况 SIDE_TABLE_RC_PINNED = 1<< (32-1)
        // int的最大值 SIDE_TABLE_RC_PINNED = 2147483648
        //  SIDE_TABLE_FLAG_MASK = 3
        // refcntStorage = 2147483648 | (oldRefcnt & 3)
        // 如果溢出,直接把refcntStorage 设置成最大值
        refcntStorage =
            SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
        return true;
    }
    else {
        refcntStorage = newRefcnt;
        return false;
    }
}

以上,to be continue~

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

推荐阅读更多精彩内容