iOS-- 內存管理

手動目錄

  • 內存分布及存儲
    靜態(tài)變量安全
  • taggedPointer
    特點
    taggedPointer 演變
    taggedPointer 存儲方式
    引用計數處理方式
    alloc 出來的對象引用計數
    dealloc 干了什么

內存分布及存儲

內存分布

為什么堆區(qū)比棧區(qū)的訪問速度慢挠羔?
棧區(qū)是寄存器直接讀取腮恩。
堆區(qū)的訪問,是寄存器先讀取棧區(qū)的指針地址偏窝,然后通過這個地址去堆區(qū)找到相應的數據。

棧區(qū)內存地址:一般0x7開頭
堆區(qū)內存地址:一般0x6開頭
數據段武学、BSS段地址:一般0x1開頭

// 全局變量祭往、全局靜態(tài)變量  初始化的在常量區(qū)(.data) ,未初始化的在靜態(tài)區(qū)(.bss)
int clA;                                //靜態(tài)區(qū)
int clB = 10;                                       //常量區(qū)
static int bssA;                        //靜態(tài)區(qū)
static NSString *bssStr1;               //靜態(tài)區(qū)
static int bssB = 10;                               //常量區(qū)
static NSString *bssStr2 = @"name";                 //常量區(qū)

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

靜態(tài)變量安全

如下代碼

// Person 類  定義一個靜態(tài)變量 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

// 在另外一個類中區(qū)訪問并修改
- (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

對于靜態(tài)全局變量,對比內存地址和值火窒,我們發(fā)現:
同一個文件中硼补,訪問的全局靜態(tài)變量 地址相同,而且可以正常的修改熏矿,
對于不同文件中的全局靜態(tài)變量 地址是不同的括勺,而且修改是相互不影響
personAge 在Person類中曲掰,地址和在另外一個類中的地址不同疾捍,而且修改之后相互不影響。

taggedPointer

taggedPointer是干嘛用的栏妖? 用于優(yōu)化內存的乱豆。

特點

  • Tagged Pointer專門用來存儲小的對象,例如NSNumber和NSDate
  • Tagged Pointer指針的值不再是地址了吊趾,而是真正的值宛裕。所以瑟啃,實際上它不再是一個對象了,它只是一個披著對象皮的普通變量而已揩尸。所以蛹屿,它的內存并不存儲在堆中,也不需要 malloc 和 free岩榆。不需要引用計數處理错负,與系統(tǒng)自動回收
  • 在內存讀取上有著 3 倍的效率,創(chuàng)建時比以前快 106 倍勇边。

在iPhone5s之前犹撒,蘋果系統(tǒng)是32位,而5s出來之后粒褒,是64位识颊,
在32位系統(tǒng)中,小對象(比如NSNumber奕坟、NSData)用8位去存儲值都能滿足大部分情況祥款,如果還用這8位去存儲一個地址指針,就太浪費內存了月杉。蘋果為了優(yōu)化這個問題刃跛,而引入taggedPointer的方式。

簡單來講可以理解為把指針指向的內容直接放在了指針變量的內存地址中沙合,因為在 64 位環(huán)境下指針變量的大小達到了 8 位足以容納一些長度較小的內容。于是使用了標簽指針這種方式來優(yōu)化數據的存儲方式跌帐。

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
    
  • 優(yōu)化
    在10.14之后,又進行了一次優(yōu)化:
    在 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;
    }
    

    優(yōu)化之后要打印出實際指針信息 究履,采用以下方式:

    #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張)萤捆,每張散列表維護 三張表裙品,多表結構的目的是為了:安全、高效鳖轰。對表進行操作清酥,需要加鎖/解鎖,同時可能由多個任務需要加鎖/解鎖蕴侣。所以用多表可以提高查詢速度焰轻、提高執(zhí)行速度。

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閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異通铲,居然都是意外死亡瓦呼,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來央串,“玉大人磨澡,你說我怎么就攤上這事≈屎停” “怎么了稳摄?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長饲宿。 經常有香客問我厦酬,道長,這世上最難降的妖魔是什么瘫想? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任仗阅,我火速辦了婚禮,結果婚禮上国夜,老公的妹妹穿的比我還像新娘减噪。我一直安慰自己,他們只是感情好车吹,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布筹裕。 她就那樣靜靜地躺著,像睡著了一般窄驹。 火紅的嫁衣襯著肌膚如雪朝卒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天乐埠,我揣著相機與錄音抗斤,去河邊找鬼。 笑死丈咐,一個胖子當著我的面吹牛瑞眼,可吹牛的內容都是我干的。 我是一名探鬼主播扯罐,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼负拟,長吁一口氣:“原來是場噩夢啊……” “哼烦衣!你這毒婦竟也來了歹河?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤花吟,失蹤者是張志新(化名)和其女友劉穎秸歧,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體衅澈,經...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡键菱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了今布。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片经备。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡拭抬,死狀恐怖,靈堂內的尸體忽然破棺而出侵蒙,到底是詐尸還是另有隱情造虎,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布纷闺,位于F島的核電站算凿,受9級特大地震影響,放射性物質發(fā)生泄漏犁功。R本人自食惡果不足惜氓轰,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望浸卦。 院中可真熱鬧署鸡,春花似錦、人聲如沸镐躲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽萤皂。三九已至撒穷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間裆熙,已是汗流浹背端礼。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留入录,地道東北人蛤奥。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像僚稿,于是被迫代替她去往敵國和親凡桥。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348