內存管理上篇(TaggedPointer、retain、release、dealloc、retainCount 底層源碼分析)

  • 什麼是內存管理

    不一樣系統版本對App運行時佔用的內存限制不一樣。當程序所佔用的內存較多時,系統就會發出內存警告,這時就得回收一些不須要再使用的內存空間。好比回收一些不須要使用的對象、變量等。若是程序佔用內存過大,系統可能會強制關閉程序,形成程序崩潰、閃退現象,影響用戶體驗。因此,咱們須要對內存進行合理的分配內存、清除內存,回收那些不須要再使用的對象。從而保證程序的穩定性。
  • 內存佈局

    知道如何內存管理以前先要知道內存的佈局。文章內存五大區中介紹了內存的五大區依次是棧、堆、常量區、全局區、代碼區,其實除了這五大區還有保留區和內核區,內核區主要是系統進行內核操做的(例如:系統分給程序的內存是4GB,其中3GB是用於五大區和保留區,剩下1GB是用於內核區),而保留區主要是預留給系統處理nil等。內存佈局圖以下: 未命名.jpg
  • 內存管理方案

    • TaggedPointer
      先看一段代碼:
      - (void)taggedPointerDemo {
           self.queue = dispatch_queue_create("com.tudou.cn", DISPATCH_QUEUE_CONCURRENT);
      
           for (int i = 0; i<10000; i++) {
               dispatch_async(self.queue, ^{
                   self.nameStr = [NSString stringWithFormat:@"tudou"];  //
                    NSLog(@"%@",self.nameStr);
               });
           }
       }
      複製代碼
      調用這個方法運行發現能夠正常打印以下圖: image.png 此時再添加一個方法
      - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
           for (int i = 0; i<10000; i++) {
               dispatch_async(self.queue, ^{
                   self.nameStr = [NSString stringWithFormat:@"tudou_好好學習每天向上"];
                   NSLog(@"%@",self.nameStr);
               });
           }
       }
      複製代碼
      點擊屏幕發現崩潰了 image.png其實崩潰反而更好理解,咱們知道在賦值的過程當中調用set方法是須要對新值的retain和舊值的release,可是此時是多線程的而且也沒有加鎖作線程安全,因此就會出現多個線程同時訪問這個變量的狀況,同時賦值同時release舊值就形成了過分釋放的問題因此崩潰,可是第一種狀況就奇怪了沒有崩潰,此時分別下斷點查看兩種賦值狀況下nameStr的類型 image.png image.png 發現第一種狀況nameStr的類型是NSTaggedPointerString也就是小對象類型,而第二種狀況是NSCFString也就是字符串類型。
      這裏就引伸出來TaggedPointer小對象類型。那麼一樣是經過stringWithFormat方法建立字符串爲何第一種狀況是小對象類型而第二種狀況不是呢,又爲何第一種狀況不會崩潰,第二種狀況會崩潰呢?帶着問題咱們能夠先來了解一下什麼是小對象類型。
      1. 源碼探索小對象類型 首先建立一個小對象類型打印它的地址以下圖: image.png發現這個小對象的地址是0x8b168fada8723f5a,按照內存五大區文章中的內存分段發現不知道是歸屬於那一塊
        在看_read_images函數中有個initializeTaggedPointerObfuscator函數,查看該函數的源碼發現是初始化小對象類型混淆器,這裏的作法就是先與上_OBJC_TAG_MASK(ios12之後) image.png image.png 而後全局搜索objc_debug_taggedpointer_obfuscator發現以下代碼 image.png發現了小對象類型的編解碼函數,得知底層混淆小對象是進行了異或操做,編碼是使用objc_debug_taggedpointer_obfuscator異或小對象,解碼時是用小對象異或解碼時objc_debug_taggedpointer_obfuscator因此上文中打印的小對象的地址是編碼後的地址,獲得真實小對象的地址則須要解碼以下圖獲得解碼後的地址: image.png發現是0xa000000000000611發現61恰好就是aASCII碼,不知道是否是應爲湊巧咱們能夠多試幾個小對象類型: image.png發現地址中就存在着值那麼0xa0xb又是什麼呢,此時咱們查看一下判斷小對象類型的源碼也就是查看_objc_isTaggedPointer函數的源碼發現 image.png發現是經過最高位是不是1來判斷是不是小對象類型0xa0xb轉爲二進制分別是10十、1011,都是1因此都是小對象類型,後三位主要是用來標記tagType0xa的後三位轉成二進制是2,0xb的後三位轉二進制是3,此時咱們再看_objc_makeTaggedPointer的源碼 image.png發現入參就有一個tag,查看這個tag的枚舉類型 image.png發現2就是字符串的小對象類型,3就是NSNumber的小對象類型
      2. 小對象類型不會出現過分釋放崩潰的緣由 上文中發現小對象類型的值其實就在地址中,並非存在堆區而是常量區,因此小對象類型的釋放是系統處理的,也能夠查看retainrelease源碼發現 image.pngimage.png 若是是小對象類型直接返回對象了,因此set方法中不存在舊值的釋放也就不會存在過分釋放崩潰
      3. 狀況1是小對象的緣由 應爲狀況一複製的是a內存暫用太小,oc優化處理變成小對象類型,佔用多大內存是小對象類型,多大又是oc對象了呢以下表: image.png
      4. 小對象總結
        1. 小對象並非個真正的對象不存在堆區,是存在常量區的
        2. 小對象類型不會進入retainrelease方法中
        3. 小對象類型的64位地址中,前4位表明類型,後4位主要適用於系統作一些處理,中間56位用於存儲值
        4. 小對象的有點:應爲不存儲在堆區因此節省了空間,能夠直接進行讀取,在內存讀取上有着3倍的效率,建立時⽐之前快106倍。
    • 散列表sideTable
      在文章經過源碼分析isa知道了extra_rc使用來存儲引用計數的,可是也是有大小限制的若是extra_rc存滿了此時就會分出一半存到SideTables中,此時咱們就能夠經過retainrelease的源碼探索來驗證。
      • retain源碼分析
        首先過濾掉小對象類型 image.png image.png retain步驟:
        1. 先判斷若是是小對象類型直接返回對象
        2. 判斷Nonpointer_isa若是沒有開啓指針優化則引用計數直接存到散列表中
        3. 若是當前類正在執行Dealloc方法也就是則直接返回當前isa
        4. 這一步就能夠對引用計數進行加一操做了,先對extra_rc進行加一操做
        5. 判斷extra_rc是否已經存滿了
        6. 若是沒有存滿則直接返回isa
        7. 若是存滿了則分出一半存到extra_rc,另外一半存到散列表
        使用extra_rc的緣由是節省性能,若是都存在散列表每次讀寫散列表都要進行解鎖和加鎖的操做因此耗費性能。
      • release源碼分析
        image.png image.png release步驟:
        1. 先判斷若是是小對象類型直接返回對象
        2. 判斷nonpointer_isa若是沒有開啓指針優化則散列表中的引用計數減一,若是散列表中的引用計數減到0則調用dealloc方法
        3. 若是正在執行dealloc方法則直接返回false
        4. extra_rc進行減一操做。
        5. 若是extra_rc當前的值等於0,則判斷has_sidetable_rc是否爲true若是不是則執行dealloc方法不然跳轉到第六步。
        6. 取出散列表中的一半的引用計數減一後存儲到extra_rc參數。若是此時散列表的引用計數爲0則清空散列表中的引用計數has_sidetable_rc參數置爲false
      • 散列表相關問題
        1. 散列表多張仍是一張 答案是多張(8張),若是僅僅是一張表會不安全只要解鎖全部數據都能看到,可是個對象一張表又耗性能衝下面源碼能夠看出具體散列表個數
          第一步先找到散列表的get方法,看get方法的源碼 image.png image.png 衝這裏發現是8張散列表
    • dealloc源碼分析
      dealloc底層源碼調用流程dealloc->_objc_rootDealloc->rootDealloc image.png 再看object_dispose函數源碼 image.png image.png image.pngimage.png dealloc具體步驟:
      1. 先判斷若是是tagged pointer直接返回
      2. 若是開啓指針優化、沒有關聯對象、沒有弱引用表、沒有c++析構函數、散列表中沒有存儲相應的引用計數直接free。不然走下一步
      3. 若是存在C++析構函數則調用析構函數
      4. 若是存在關聯對象則刪除關聯對象
      5. 若是沒有開啓指針優化則直接清空散列表中的引用計數而後free.不然走到下一步
      6. 判斷若是存在弱引用或者散列表中有引用計數則清空弱引用表和散列表中的引用計數
      7. 最後再free
    • retainCount源碼分析
      先看一段代碼
      NSObject *objc = [NSObject alloc];
      NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc));
      複製代碼
      兩個問題alloc以後的引用計數是多少,NSLog打印的值是多少
      答案是多是1多是0,目前最新的源碼顯示alloc方法是會給extra_rc賦值爲1以下圖 image.png 可是老的源碼是沒有賦值的,我記得781的源碼是沒有賦值的,因此打印的是0,(具體alloc源碼探索能夠參考下面這個文章alloc & init & new 源碼分析);
      第二個問題:
      答案確定是1了。先看retainCount源碼 源碼調用路徑爲:retainCount->_objc_rootRetainCount->rootRetainCount image.png 源碼仍是挺簡單的主要是下面幾個步驟:
      1. 判斷是否是小對象類型,若是是直接返回,不是則走下一步
      2. 判斷是否作了指針優化,若是沒作則直接返回散列表中的引用計數,不然走下一步
      3. 此時判斷散列表中是否存儲了引用計數,沒有存直接返回extra_rc不然返回二者之和
      注意:781的源碼再此基礎上進行了加一操做因此返回的也是1
相關文章
相關標籤/搜索