底層探索--內(nèi)存管理的本質(zhì)

定時(shí)器

1. CADisplayLink殉簸、NSTimer使用注意

  • CADisplayLink侧到、NSTimer會(huì)對(duì)target產(chǎn)生強(qiáng)引用奶浦,如果target又對(duì)它們產(chǎn)生強(qiáng)引用略就,那么就會(huì)引發(fā)循環(huán)引用捎迫,從而導(dǎo)致對(duì)象無(wú)法釋放。

  • 解決方案如下:

      //解決方式1:使用閉包(>= ios1 0才能使用) 
      [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
             // 所指針引用self 
      }];
      
      //解決方式2:使用NSProxy對(duì)象(推薦)
      
      /// 定時(shí)器-代理對(duì)象:專門解決定時(shí)器內(nèi)存問(wèn)題的
      @interface TimerProxy : NSProxy
      
      /** NSProxy:特殊的專門用來(lái)做代理對(duì)象的表牢,與NSObject不同的是窄绒,轉(zhuǎn)發(fā)時(shí)它的效率更高(消息轉(zhuǎn)發(fā)時(shí),只會(huì)從自身類的類對(duì)象尋找方法崔兴,不會(huì)再到父類去尋找方法彰导,且轉(zhuǎn)發(fā)階段,立馬會(huì)走慢速轉(zhuǎn)發(fā)方法:methodSignatureForSelector敲茄,不會(huì)經(jīng)歷方法解析和快速轉(zhuǎn)發(fā)階段)
       */
      
      /// 初始化
      /// @param target 代理對(duì)象(一般傳self)
      + (instancetype)proxyWithTarget:(id)target;
      
      @end
      
      @interface TimerProxy()
      
      @property (nonatomic, weak) id tatget; //目標(biāo)對(duì)象
      
      @end
      
      @implementation TimerProxy
      
      //初始化
      + (instancetype)proxyWithTarget:(id)target {
          //NSProxy:專門用于解決代理對(duì)象問(wèn)題位谋,效率比NSObject高,然沒(méi)有init方法
          TimerProxy *proxy = [TimerProxy alloc];
          proxy.tatget = target;
          return proxy;
      }
      
      //消息簽名
      - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
          if (self.tatget) {
              return [self.tatget methodSignatureForSelector:sel];
          }
          return [super methodSignatureForSelector:sel];
      }
      
      //消息轉(zhuǎn)發(fā)
      - (void)forwardInvocation:(NSInvocation *)invocation {
          if (self.tatget) {
              [invocation invokeWithTarget:self.tatget];
           }
      }
    
      @end
    

2. GCD自定義定時(shí)器

//創(chuàng)建線程
dispatch_queue_t queue =  dispatch_get_global_queue(0, 0);
//初始化定時(shí)器
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//設(shè)置時(shí)間(參數(shù)-1.資源本身堰燎,2.開(kāi)始時(shí)間掏父,3.間隔時(shí)間,4.偏差爽待,默認(rèn)為0)
//開(kāi)始時(shí)間:dispatch_time(DISPATCH_TIME_NOW, (2*NSEC_PER_SEC))從現(xiàn)在開(kāi)始损同,2秒后執(zhí)行
dispatch_source_set_timer(source, 0, (ti*NSEC_PER_SEC), 0);
//設(shè)置回調(diào)
dispatch_source_set_event_handler(source, ^{
    //處理邏輯。鸟款。膏燃。
});
//啟動(dòng)定時(shí)器
dispatch_resume(source);

iOS程序的內(nèi)存布局

多線程的iOS程序的內(nèi)存布局.png

Tagged Pointer詳解

參考鏈接

// objc-internal.h 
static inline bool 
// 判斷是否是TaggedPointer的指針
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

#if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __x86_64__
    // 64-bit Mac - tag bit is LSB
#   define OBJC_MSB_TAGGED_POINTERS 0  // MacOS
#else
    // Everything else - tag bit is MSB
#   define OBJC_MSB_TAGGED_POINTERS 1  // iOS
#endif

#if OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)  // _OBJC_TAG_MASK -- iOS
#else
#   define _OBJC_TAG_MASK 1UL       // _OBJC_TAG_MASK -- MacOS
#endif
  • 字符串繼承鏈:
    __NSCFConstantString -> __NSCFString -> NSMutableString -> NSString -> NSObject

  • 特點(diǎn):在64位機(jī)器上

    • Tagged Pointer專門用來(lái)存儲(chǔ)小的對(duì)象,例如NSNumber, NSDate, NSString何什。這是一個(gè)特別的指針组哩,不指向任何一個(gè)地址,當(dāng)指針不夠存儲(chǔ)數(shù)據(jù)時(shí),才會(huì)使用動(dòng)態(tài)分配內(nèi)存的方式來(lái)存儲(chǔ)數(shù)據(jù),這個(gè)標(biāo)志位处渣,也是在最高4位來(lái)表示的伶贰。
    • Tagged Pointer指針的值不再是地址了,而是真正的值罐栈。所以黍衙,實(shí)際上它不再是一個(gè)對(duì)象了,它只是一個(gè)披著對(duì)象皮的普通變量而已荠诬。所以琅翻,它的內(nèi)存并不存儲(chǔ)在堆中位仁,也不需要malloc和free。
    • 在內(nèi)存讀取上有著3倍的效率方椎,創(chuàng)建時(shí)比以前快106倍聂抢。
    • 如何判斷一個(gè)指針是否為Tagged Pointer
      • iOS平臺(tái)棠众,最高有效位是1(第64bit)
      • Mac平臺(tái)琳疏,最低有效位是1。
    • 在字符串長(zhǎng)度在9個(gè)以內(nèi)時(shí)闸拿,iOS其實(shí)使用了Tagged pointer做了優(yōu)化的空盼, 直到字符串長(zhǎng)度大于9,字符串才真正成為了__NSCFString類型(即對(duì)象類型)
    • 位圖:
      • 1. NSNumber(1標(biāo)識(shí)位 2-4類型識(shí)位 后四位數(shù)據(jù)類型胸墙,其他存儲(chǔ)數(shù)據(jù))
      • 2. NSString(1標(biāo)識(shí)位 2-4類型識(shí)位 后四位字符串長(zhǎng)度我注,其他存儲(chǔ)數(shù)據(jù))
        內(nèi)存管理之taggedPinter-NSNumber.png

        內(nèi)存管理之taggedPinter-NSString.png
  • 什么時(shí)候使用的Tagged Pointer?
    • NSString:[動(dòng)態(tài)字符串調(diào)用 copy]且length<=9迟隅,[NSString stringWithFormat]且length<=9
    • NSNumber:存儲(chǔ)較小的值但骨,如果 8 字節(jié)承載不了時(shí),則又用以前的方式來(lái)生成普通的指針智袭。
  • 經(jīng)典題目:
    //經(jīng)典題目:加鎖奔缠、原子、同步吼野、串行解決
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            self.name = [NSString stringWithFormat:@"abcdefghij"]; //崩潰,因?yàn)橥瑫r(shí)訪問(wèn)校哎,name的setter方法先release后retain,可能遇到兩次都release造成過(guò)度釋放而訪問(wèn)野指針
            //self.name = [NSString stringWithFormat:@"abcdefghi"]; //為Tagged Pointer瞳步,相當(dāng)于直接賦值闷哆,不是一個(gè)真正的OC對(duì)象,不會(huì)調(diào)用setter方法進(jìn)行
        });
    }
    
    - (void)setName:(NSString *)name {
        if(_name != name) { 
            [_name release];
            _name = [name retain]; // or [name copy]
        }
    }

MRC的對(duì)象的setter方法

重點(diǎn):使用MRC单起,記住原則:“誰(shuí)創(chuàng)建誰(shuí)釋放”

// getter方法直接返回
- (NSString *)name {
    return _name;
}

// setter 方法
- (void)setName:(NSString *)name {
    if(_name != name) {  //保證同一個(gè)對(duì)象不用重復(fù)操作
        [_name release]; //保證替換的上一個(gè)對(duì)象計(jì)數(shù)器-1
        _name = [name retain]; // or [name copy] //計(jì)數(shù)器+1抱怔,保證此對(duì)象被當(dāng)前對(duì)象擁有,即使外面對(duì)象計(jì)數(shù)器減一嘀倒,此對(duì)象也不會(huì)被釋放
    }
}

拷貝Copy和mutableCopy

  • 重要規(guī)則:

    • 目的:類似文件夾屈留,拷貝備份之后,源文件修改不影響copy文件测蘑,同時(shí)灌危,copy文件修改也不影響源文件√几欤總結(jié):拷貝之后勇蝙,互不影響
      • 不可變對(duì)象:copy直接引用+1,互相修改也不會(huì)影響挨约;mutableCopy的副本能修改浅蚪,則需生成新的對(duì)象
      • 可變對(duì)象:copy和mutableCopy的源本能修改藕帜,則需生成新的對(duì)象
    • 拷貝:Copy拷貝后都是不可變對(duì)象烫罩,mutableCopy拷貝后都是可變對(duì)象惜傲。(不包括自己實(shí)現(xiàn)的)
  • 定義解釋

    • 淺拷貝:指針拷貝,相當(dāng)于retain贝攒,引用計(jì)數(shù)+1
    • 深拷貝:內(nèi)容拷貝盗誊,生成新的相同內(nèi)容的對(duì)象,同時(shí)指針指向此對(duì)象隘弊。
  • 注意:

    • 申明屬性時(shí)哈踱,如果用關(guān)鍵字copy,則最好不要用可變對(duì)象梨熙,因?yàn)榻?jīng)copy之后就變成不可變對(duì)象开镣,所以在使用的時(shí)候就會(huì)出現(xiàn)無(wú)法增刪改的錯(cuò)誤。
    • 為什么NSString咽扇,系統(tǒng)常用copy邪财?為了避免外部賦值的改變從而影響控件的UI顯示的值。
  • 總結(jié)

    對(duì)象類型 copy mutableCopy
    不可變對(duì)象 淺copy质欲,指針復(fù)制树埠,返回值不可變 深copy,內(nèi)容復(fù)制嘶伟,返回值可變
    可變對(duì)象 深copy怎憋,內(nèi)容復(fù)制,返回值不可變 深copy九昧,內(nèi)容復(fù)制绊袋,返回值可變

引用計(jì)數(shù)器

  • 在64bit中,引用計(jì)數(shù)可以直接存儲(chǔ)在優(yōu)化過(guò)的isa指針(詳情見(jiàn)對(duì)象的本質(zhì))中铸鹰,也可能存儲(chǔ)在SideTable類中

      // SideTable的定義
      struct SideTable {
          spinlock_t slock;
          RefcountMap refcnts; //存放著對(duì)象引用計(jì)數(shù)的散列表
          weak_table_t weak_table; //存放著所有弱引用的對(duì)象指針的散列表
      }
    
  • dealloc釋放過(guò)程:dealloc->_objc_rootDealloc->rootDealloc->object_dispose->objc_destructInstance/free

    void *objc_destructInstance(id obj) 
    {
        if (obj) {
            // Read all of the flags at once for performance.
            bool cxx = obj->hasCxxDtor(); //是否有C++的析構(gòu)函數(shù)
            bool assoc = obj->hasAssociatedObjects(); //是否有設(shè)置關(guān)聯(lián)對(duì)象
    
            // This order is important.
            if (cxx) object_cxxDestruct(obj); //清楚成員變量
            if (assoc) _object_remove_assocations(obj); //移除關(guān)聯(lián)對(duì)象
            obj->clearDeallocating(); //將指向當(dāng)前對(duì)象的弱指針置為nil
        }
    
        return obj;
    }

自動(dòng)釋放池

主線程的自動(dòng)釋放池

//重新編譯為C/C++ 文件后的源碼
struct __AtAutoreleasePool {
    __AtAutoreleasePool() { //構(gòu)造函數(shù)
        atautoreleasepoolobj = objc_autoreleasePoolPush(); //運(yùn)行時(shí)的放入函數(shù)
    }
    ~__AtAutoreleasePool() { //析構(gòu)函數(shù)
        objc_autoreleasePoolPop(atautoreleasepoolobj); //運(yùn)行時(shí)的釋放函數(shù)
    }
    void * atautoreleasepoolobj;
};

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    // @autoreleasepool
    { __AtAutoreleasePool __autoreleasepool;

        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
    }
    return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}
  • 每個(gè)AutoreleasePoolPage對(duì)象占用4096字節(jié)內(nèi)存癌别,除了用來(lái)存放它內(nèi)部的成員變量,剩下的空間用來(lái)存放所有的Autoreleautorelease對(duì)象的地址asePoolPage對(duì)象通過(guò)雙向鏈表的形式連接在一起掉奄;
  • 調(diào)用push方法會(huì)將一個(gè)POOL_BOUNDARY入棧规个,并且返回其存放的內(nèi)存地址;
  • 調(diào)用pop方法時(shí)傳入一個(gè)POOL_BOUNDARY的內(nèi)存地址姓建,會(huì)從最后一個(gè)入棧的對(duì)象開(kāi)始發(fā)送release消息诞仓,直到遇到這個(gè)POOL_BOUNDARY
  • id *next指向了下一個(gè)能存放autorelease對(duì)象地址的區(qū)域速兔。
    class AutoreleasePoolPage;
    struct AutoreleasePoolPageData
    {
        magic_t const magic;
        __unsafe_unretained id *next;
        pthread_t const thread;
        AutoreleasePoolPage * const parent;
        AutoreleasePoolPage *child;
        uint32_t const depth;
        uint32_t hiwat;
    
        AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
            : magic(), next(_next), thread(_thread),
              parent(_parent), child(nil),
              depth(_depth), hiwat(_hiwat)
        {
        }
    };
    
    //底層push函數(shù)-向RunloopPage里加入對(duì)象
    static inline void *push() 
    {
        id *dest;
        if (slowpath(DebugPoolAllocation)) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }
    
    //底層pop函數(shù)-釋放RunloopPage里的對(duì)象
    static inline void pop(void *token)
    {
        AutoreleasePoolPage *page;
        id *stop;
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            // Popping the top-level placeholder pool.
            page = hotPage();
            if (!page) {
                // Pool was never used. Clear the placeholder.
                return setHotPage(nil);
            }
            // Pool was used. Pop its contents normally.
            // Pool pages remain allocated for re-use as usual.
            page = coldPage();
            token = page->begin();
        } else {
            page = pageForPointer(token);
        }
    
        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
            if (stop == page->begin()  &&  !page->parent) {
                // Start of coldest page may correctly not be POOL_BOUNDARY:
                // 1. top-level pool is popped, leaving the cold page in place
                // 2. an object is autoreleased with no pool
            } else {
                // Error. For bincompat purposes this is not 
                // fatal in executables built with old SDKs.
                return badPop(token);
            }
        }
    
        if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
            return popPageDebug(token, page, stop);
        }
    
        return popPage<false>(token, page, stop);
    }

ARC下的自動(dòng)內(nèi)存管理機(jī)制(結(jié)合上面的進(jìn)行理解)

  • LLVM + Runtime
  • iOS在主線程的Runloop中注冊(cè)了2個(gè)Observer
    • 第1個(gè)Observer監(jiān)聽(tīng)了kCFRunLoopEntry事件墅拭,會(huì)調(diào)用objc_autoreleasePoolPush()
    • 第2個(gè)Observer
      • 監(jiān)聽(tīng)了kCFRunLoopBeforeWaiting事件,會(huì)調(diào)用objc_autoreleasePoolPop()涣狗、objc_autoreleasePoolPush()
      • 監(jiān)聽(tīng)了kCFRunLoopBeforeExit事件谍婉,會(huì)調(diào)用objc_autoreleasePoolPop()
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末舒憾,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子穗熬,更是在濱河造成了極大的恐慌镀迂,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,376評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件唤蔗,死亡現(xiàn)場(chǎng)離奇詭異探遵,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)妓柜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門箱季,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人棍掐,你說(shuō)我怎么就攤上這事藏雏。” “怎么了作煌?”我有些...
    開(kāi)封第一講書人閱讀 156,966評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵掘殴,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我最疆,道長(zhǎng)杯巨,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 56,432評(píng)論 1 283
  • 正文 為了忘掉前任努酸,我火速辦了婚禮服爷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘获诈。我一直安慰自己仍源,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,519評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布舔涎。 她就那樣靜靜地躺著笼踩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪亡嫌。 梳的紋絲不亂的頭發(fā)上嚎于,一...
    開(kāi)封第一講書人閱讀 49,792評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音挟冠,去河邊找鬼于购。 笑死,一個(gè)胖子當(dāng)著我的面吹牛知染,可吹牛的內(nèi)容都是我干的肋僧。 我是一名探鬼主播,決...
    沈念sama閱讀 38,933評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼嫌吠!你這毒婦竟也來(lái)了止潘?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 37,701評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤辫诅,失蹤者是張志新(化名)和其女友劉穎凭戴,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體泥栖,經(jīng)...
    沈念sama閱讀 44,143評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡簇宽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,488評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吧享。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,626評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡譬嚣,死狀恐怖钢颂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情拜银,我是刑警寧澤殊鞭,帶...
    沈念sama閱讀 34,292評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站尼桶,受9級(jí)特大地震影響操灿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜泵督,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,896評(píng)論 3 313
  • 文/蒙蒙 一趾盐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧小腊,春花似錦救鲤、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,742評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至入问,卻和暖如春丹锹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背芬失。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工楣黍, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人麸折。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓锡凝,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親垢啼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子窜锯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,494評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容