iOS-- 内存管理

手动目录

  • 内存分布及存储
    静态变量安全
  • taggedPointer
    特点
    taggedPointer 演变
    taggedPointer 存储方式
    引用计数处理方式
    alloc 出来的对象引用计数
    dealloc 干了什么

内存分布及存储

内存分布

为什么堆区比栈区的访问速度慢?
栈区是寄存器直接读取。
堆区的访问,是寄存器先读取栈区的指针地址,然后通过这个地址去堆区找到相应的数据。

栈区内存地址:一般0x7开头
堆区内存地址:一般0x6开头
数据段、BSS段地址:一般0x1开头

// 全局变量、全局静态变量  初始化的在常量区(.data) ,未初始化的在静态区(.bss)
int clA;                                //静态区
int clB = 10;                                       //常量区
static int bssA;                        //静态区
static NSString *bssStr1;               //静态区
static int bssB = 10;                               //常量区
static NSString *bssStr2 = @"name";                 //常量区

- (void)testStack{
    int a = 10;                             // 栈区
    int b = 20;                             // 栈区
    NSObject *object = [NSObject new];      // *obj 栈区  ,  obj 堆区
    NSString *str = @"aaa";                 // *str 栈区  ,  str 常量区(.data)
    NSString *str1;                         // *str1 栈区 ,  str1  0x0
}

静态变量安全

如下代码

// Person 类  定义一个静态变量 personHei
static int personHeig = 180;
@interface Person : NSObject
- (void)growUp;
@end

@implementation Person
- (void)growUp {
    personAge =  30;
    NSLog(@"person age = %d %p",personAge,&personAge);          // person age = 30 0x10be0fe10
}
@end

// 在另外一个类中区访问并修改
- (void)task8 {
    NSLog(@"person age = %d %p",personAge,&personAge);          // person age = 18 0x10be0fc18
    personAge = 30;
    NSLog(@"person age = %d %p",personAge,&personAge);          // person age = 30 0x10be0fc18
    
    [[Person new] growUp];
    personAge ++;
    NSLog(@"person age = %d %p",personAge,&personAge);          // person age = 31 0x10be0fc18
}

//最后打印结果:
person age = 18 0x10be0fc18
person age = 30 0x10be0fc18
person age = 30 0x10be0fe10
person age = 31 0x10be0fc18

对于静态全局变量,对比内存地址和值,我们发现:
同一个文件中,访问的全局静态变量 地址相同,而且可以正常的修改
对于不同文件中的全局静态变量 地址是不同的,而且修改是相互不影响
personAge 在Person类中,地址和在另外一个类中的地址不同,而且修改之后相互不影响。

taggedPointer

taggedPointer是干嘛用的? 用于优化内存的

特点

  • Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate
  • Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free。不需要引用计数处理,与系统自动回收
  • 在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。

在iPhone5s之前,苹果系统是32位,而5s出来之后,是64位,
在32位系统中,小对象(比如NSNumber、NSData)用8位去存储值都能满足大部分情况,如果还用这8位去存储一个地址指针,就太浪费内存了。苹果为了优化这个问题,而引入taggedPointer的方式。

简单来讲可以理解为把指针指向的内容直接放在了指针变量的内存地址中,因为在 64 位环境下指针变量的大小达到了 8 位足以容纳一些长度较小的内容。于是使用了标签指针这种方式来优化数据的存储方式。

taggedPointer 演变

  • 早期
    taggedPointer 的地址 直接存储值 比如 Number = @(1)的对象,其存储形式为:
    __NSCFNumber, 0xb000000000000013 参考文章

    - (void)task {
         NSNumber *number1 = @1;
         NSNumber *bigNumber = @(0x7fffffffffffff + 1);      // 14位
    
         NSLog(@"number1 pointer is %p", number1);
         NSLog(@"bigNumber pointer is %p", bigNumber);
    }
    // 打印结果
    number1 pointer is 0xb000000000000012
    bigNumber pointer is 0x10921ecc0
    
  • 优化
    在10.14之后,又进行了一次优化:
    在 Objc源码中有这样一段:
    在程序启动的源码中:objc_init() -> map_images() -> read_image()

    initializeTaggedPointerObfuscator(void)
    {
      if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
          // Set the obfuscator to zero for apps linked against older SDKs,
          // in case they're relying on the tagged pointer representation.
          DisableTaggedPointerObfuscation) {
          objc_debug_taggedpointer_obfuscator = 0;
      } else {
          // Pull random data into the variable, then shift away all non-payload bits.
          arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                         sizeof(objc_debug_taggedpointer_obfuscator));
          objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
      }
    }
    
    // taggedPointer 对象进行编码解码的过程中,进行了异或运算
    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;
    }
    

    优化之后要打印出实际指针信息 ,采用以下方式:

    #import <objc/runtime.h>
    
    extern uintptr_t objc_debug_taggedpointer_obfuscator;
    uintptr_t
    _objc_decodeTaggedPointer_(id ptr)
    {
      return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
    }
    
    NSNumber *number1 = @(1);
    NSLog(@"%@-%p---%@ - 0x%lx",object_getClass(number1),number1,number1,_objc_decodeTaggedPointer_(number1));
    
    // 打印结果
    __NSCFNumber-0xcdfb8d4002f69cf0---1 - 0xb000000000000012
    

taggedPointer 存储方式

最后一位表示的含义:

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,

    // 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_NSColor           = 16,
    OBJC_TAG_UIColor           = 17,
    OBJC_TAG_CGColor           = 18,
    OBJC_TAG_NSIndexSet        = 19,

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

    OBJC_TAG_RESERVED_264      = 264
};

引用计数处理方式

如何维护引用计数呢?

在之前的的 isa结构探究 中 提到 isa结构中,有引用计数的记录。

extra_rc :19
当表示该对象的引用计数值,实际上是引用计数值减1,例如,如果对象的引用计数为10,那么extra_rc为9.如果引用计数大于10,则需要使用上面提到的has_sidetable_rc。

那么在对象进行retain的时候, 那么具体是进行了什么操作呢?

id 
objc_retain(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;              // ⚠️ 如果是  taggedPointer ,就不进行retain
    return obj->retain();
}


ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    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;
        if (slowpath(!newisa.nonpointer)) {                    // 如果不是 nonpoint isa  直接存散列表
            ClearExclusive(&isa.bits);
            if (rawISA()->isMetaClass()) return (id)this;
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        // don't 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;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++            // 如果是nonpoint isa  就进行isa 的extra_rc++          操作

        if (slowpath(carry)) {          // 如果isa的  extra_rc  超过 容量,
            // 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;                  // isa 中的 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_addExtraRC_nolock(RC_HALF);            // 散列表中的引用计数表 存一半
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}

引用计数的处理 retain

  • 如果不是 nonpoint isa 直接加入散列表
  • 如果是 nonpoint isa 在isa的 extra_rc 进行++操作
    如果超过容量,isa 中的 extra_rc 保存一半 散列表中的引用计数表 存一半

引用计数的处理 release

  • 如果不是 nonpoint isa 直接对散列表进行操作
  • 如果是 nonpoint isa 在isa的 extra_rc 进行-- 操作
    如果减到最后没了,看有没有相应的散列表的引用计数表,如果有,把引用计数表的计数赋值给extra_rc。

补充:
散列表有多张(据说最多64张),每张散列表维护 三张表,多表结构的目的是为了:安全、高效。对表进行操作,需要加锁/解锁,同时可能由多个任务需要加锁/解锁。所以用多表可以提高查询速度、提高执行速度。

struct SideTable {
    spinlock_t slock;             // 自旋锁
    RefcountMap refcnts;  //引用计数表
    weak_table_t weak_table;    // 弱引用表1
}

alloc 出来的对象引用计数

这也是一个面试题:alloc出来的对象,其引用计数是多少?
答案是0。

在之前的iOS底层-alloc与init 中详细的说了alloc的过程,在此过程中,我们并没有看到任何与引用计数有关的内容,也就是说,并没有操作引用计数,所以为0 。

但是 为什么通过打印的方式得到的是1 呢?
printf("Retain Count = %ld\n",CFGetRetainCount((__bridge CFTypeRef)(obj)));

通过源码来看:

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

打印出来的引用计数值的又来: 1 + isa.extra_rc + sidetable_rc(散列表的引用计数)
既然打印结果为1,那么 其 isa.extra_rc 必然为0, 所以其真正的引用计数为0。

为什么默认要给个1呢,因为 它要被autorelease(自动释放池)所持有。

dealloc 干了什么

看源码:

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;
    object_dispose((id)this);
}

id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

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);       // 清空关联对象
        obj->clearDeallocating();
    }

    return obj;
}

inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();              //  非 nonpointer 散列表清空
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

objc_object::clearDeallocating_slow()
{
    ASSERT(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        weak_clear_no_lock(&table.weak_table, (id)this);        // 清空弱引用表中的弱引用对象
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);                  // 清空散列表中的引用计数表的引用计数
    }
    table.unlock();
}

dealloc 干了什么

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