Block底層分析

  • block簡介

    Block塊是封裝工做單元的對象,是能夠在任什麼時候間執行的代碼段。其本質上是可移植的匿名函數,能夠做爲方法和函數的參數傳入,能夠從方法和函數中返回。—(翻譯自官方文檔)markdown

    塊是對C語言的一種擴展,它並未做爲標準的ANSI C所定義的部分,而是有蘋果公司添加到語言中的。塊看起來更像是函數,能夠給塊傳遞參數,塊也能夠具備返回值。函數

  • block類型

    • __NSGlobalBlock__全局block,存儲在全局區
      先看以下代碼 image.png 發現就是簡單的聲明一個block,block沒有入參,沒有block內部代碼塊只是簡單的打印沒有引用外界變量,則此時的block類型是__NSGlobalBlock__
    • __NSStackBlock__棧區block
      image.png 在外界變量沒有處理以前此時的類型是__NSStackBlock__,外界變量處理以後底層會對block進行拷貝從棧區拷貝到堆區。在ARC下,編譯器作了不少的優化,每每看不到本質,上面的代碼輸出結果找不到,由於編譯器對__NSStackBlock__自動進行了copy操做。改成MRC就能夠看到不論是否對變量進行處理都會打印__NSStackBlock__

      改成MRC方法: Build Settings 裏面的Automatic Reference Counting改成NO。源碼分析

      固然也能夠用__weak修飾block,不作強引用同樣不會copy因此此時打印的block仍是棧區block image.png
    • __NSMallocBlock__堆區block
      image.png 棧區block底層拷貝以後變成堆區block
  • block本質

    • 編譯後的代碼探索
      首先寫一個簡單的block而後查看編譯後的代碼 image.png image.png 發現底層是將__main_block_impl_0函數地址賦值給了block此時咱們再看__main_block_impl_0的結構 image.png 從這裏能夠發現block本質其實就是一個結構體,結構體中又存在着isa,也能夠說block本質就是一個OC對象,一個封裝了函數調用以及函數調用環境的OC對象
      從結構體中也看到了函數的賦值,這也說明了爲何block須要調用後纔會執行代碼塊,應爲在底層只是函數指針的賦值,並無主動調用。 image.png編譯後的代碼咱們也能夠看出在調用的時候是調用的FuncPtr,並將block做爲入參傳遞
  • block如何捕獲變量

    上文分析本質的時候寫了一個簡單的block沒有任何入參,也沒有使用外界變量,再寫一個使用外界變量的block,看看底層block如何使用這些外界變量的,在外界定義一個變量a,而後在代碼塊中打印a,查看編譯後的代碼: image.png image.png image.png優化

    • 總結
      發現底層block結構體中多了一個同名的參數,初始化的時候將外界的變量賦值給這個同名變量,其中局部變量會生成一個變量進行值拷貝,全局變量不捕獲變量直接使用外界變量,靜態變量是是有一個同名變量指針,是指針拷貝
  • __block原理

    block代碼塊中直接修改外界沒有使用__block修飾的變量時回報以下錯誤 image.png此時咱們在使用__block修飾發現沒有報錯而且修改爲功 image.png經過clang查看編譯後的代碼 image.png 發現編譯後a此時取得是地址不僅僅是簡單的數值而且被轉換成了__Block_byref_a_0類型 image.png經過編譯後的代碼發現__Block_byref_a_0是一個結構體,而後block的結構體同時也多了一個__Block_byref_a_0類型的指針因此底層a是指針複製也就是和外界的a變量指向了同一片內存地址,因此此時使用__block修飾的變量可以修改爲功ui

  • block真正的類型Block_layout

    • 經過代碼調試找到block的真正類型
      經過彙編跟蹤發現首先聲明block的時候首先會走到objc_retainBlock方法中去如圖image.png此時咱們下一個符號斷點objc_retainBlock,發現objc_retainBlock方法裏面會跳轉到_Block_copy方法中去,此時斷點繼續往下跟發現第一次會走進libobjc.A.dylib庫中的_Block_copy方法,方法內部呢又跳轉了一個_Block_copy方法,一樣的繼續往下跟發現走進了libsystem_blocks.dylib庫中的_Block_copy方法,此時咱們再衝源碼中找block的真正類型image.png發現block的真正類型是Block_layout
    • Block_layout源碼探索
      經過源碼發現底層就是一個結構體如圖 image.png flags標識說明
      • 第1位 - BLOCK_DEALLOCATING,釋放標記,-般經常使用BLOCK_NEEDS_FREE作位與操做,一同傳入Flags,告知該block可釋放。
      • 低16位 - BLOCK_REFCOUNT_MASK,存儲引用計數的值;是一個可選用參數
      • 第24位 - BLOCK_NEEDS_FREE,低16是否有效的標誌,程序根據它來決定是否增長或是減小引用計數位的值;
      • 第25位 - BLOCK_HAS_COPY_DISPOSE,是否擁有拷貝輔助函數(a copy helper function);
      • 第26位 - BLOCK_IS_GC,是否擁有block析構函數;
      • 第27位,標誌是否有垃圾回收;//OS X
      • 第28位 - BLOCK_IS_GLOBAL,標誌是不是全局block;
      • 第30位 - BLOCK_HAS_SIGNATURE,與BLOCK_USE_STRET相對,判斷當前block是否擁有一個簽名。用於 runtime 時動態調用。
      descriptor說明:
      block的附加信息,好比保留變量數、block的大小、進行copy或dispose的輔助函數指針。有三類
      • Block_descriptor_1是必選的
      • Block_descriptor_2Block_descriptor_3都是可選的
      image.png 再看Block_descriptor_2Block_descriptor_3的構造函數 image.png 發現都是經過Block_descriptor_1的內存地址平移得來
  • block三層拷貝分析

    注意:能出發三層拷貝的狀況是,在block中捕獲用__block修飾的對象的時候出發spa

    • 第一層拷貝_Block_copy
      這一層拷貝主要是將block從棧區拷貝到堆區 image.png 源碼也比較簡單,主要分爲如下幾個步驟
      1. 首先判斷入參是否爲空是空則返回空
      2. 判斷block是否須要釋放,須要則不拷貝
      3. 判斷是否爲全局block是則不拷貝
      4. 最後則是棧區block,首先第一步就是開闢內存空間
      5. 而後就是內存拷貝,將aBlock拷貝到result
      6. 最後就是簡單的賦值操做而後返回result
    • 第二層拷貝_Block_byref_copy
      主要針對外界變量使用__block修飾的時候拷貝成Block_byref結構體 從下文中的_Block_object_assign源碼分析知道若是是__block修飾的對象會交給_Block_byref_copy去處理此時咱們再看_Block_byref_copy的源碼 image.png 從改源碼中就能夠看到爲何使用__block修飾的變量在block中可以直接修改對應的值,我看再看編譯後的代碼image.png發現用__block修飾的對象轉換成__Block_byref_person_0類型再看__Block_byref_person_0結構體 image.png發現多了兩個函數__Block_byref_id_object_copy__Block_byref_id_object_dispose,暫時不知道多的這兩個函數是怎麼用的,這時候咱們再回到源碼先看Block_byref結構體 image.png 發現一個普通的Block_byref結構中是沒有這兩個函數的,可是Block_byref_2恰好存在着兩個函數分別是copydispose,再看_Block_byref_copy函數的源碼發現若是存在copydispose方法的時候會有調用copy方法image.png,這裏再回到編譯後的代碼上看copy的實現 image.png發現又是調用的_Block_object_assign方法,此時傳入的就是普通的對象交給系統arc去處理,而後作一個指針拷貝,至此這一層拷貝就找到了
    • 第三層拷貝_Block_object_assign
      先看編譯後的源碼 image.png 發現有兩個方法__main_block_copy_0__main_block_dispose_0方法裏面的實現分別調用的是_Block_object_assign__main_block_dispose_0,衝源碼中也能夠知道這兩個方法分別對應着Block_descriptor_2中的copydispose此時咱們再看_Block_object_assign的源碼實現 image.png 經過源碼發現最主要的步驟有三種
      1. 判斷若是是簡單的普通對象類型則交給arc去處理
      2. 若是是block類型則交給_Block_copy去處理
      3. 若是是使用__block修飾的對象則交給_Block_byref_copy去處理該方法的源碼分析可看面內容
  • block循環引用

    • 形成循環引用的緣由
      形成循環引用的根本緣由就是相互持有都不能釋放,好比當前類本身有個block,而後還有個屬性name若是在block中使用name,那麼在block中就會只有當前這個對象(上文中也分析過了,此狀況,block內部結構體會添加一個一樣的對象作持有當前對象,底層傳入普通對象會進行指針拷貝而且引用計數加一)這就形成了相互持有循環引用。
    • 循環引用解決辦法
      • **__weak、__strong結合使用 **
        • 若是是簡單的block中須要使用self中的屬性直接使用__weak就能夠了,這樣兩個指針都指向同一片內存地址可是使用__weak修飾不會形成引用計數加一。
        • 若是是block中又嵌套block的狀況
          __weak typeof(self) weakSelf = self;
          self.tdBlock = ^(void){
              __strong typeof(weakSelf) strongSelf = weakSelf;
              dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                  NSLog(@"%@",strongSelf.name);
              });
          };
          self.tdBlock();
          複製代碼
          這個狀況外面同樣的使用__weak去修飾,可是在第一個block裏面在使用__strong去修飾。應爲__strong修飾的變量在代碼塊中是個局部變量,因此block代碼執行完成以後會自動釋放,因此不會形成循環引用,爲何要在block中在使用__strong修飾呢?主要是應爲裏面的block是一個延時操做若是不是用__strong修飾那麼執行完析構函數weakSelf就變成nil了,因此在嵌套的block,中拿到的weakSelf,就是nil,此時在使用__strong是爲了延長weakSelf聲明週期,讓其在嵌套的block執行完成以後再銷燬
      • __block修飾變量
        使用__block修飾是利用了在block內部能夠修改變量的值的屬性,一樣的在定義block的時候self的引用計數會加一,可是在使用完成以後能夠吧變量置爲nil,那麼self的引用計數又會減一,因此不會形成循環引用。(注意:這種方法block必需要調用若是不調用變量沒法置空同樣會形成循環引用
      • 對象self做爲參數
        當作參數傳入,此時的self就會被做爲臨時變量壓棧進來,因此就不會形成持有,也就不會形成循環引用(函數參數是存在棧區的,是由編譯器自動分配和釋放的)
      • NSProxy 虛擬類
        • OC是隻能單繼承的語言,可是它是基於運行時的機制,因此能夠經過NSProxy來實現 僞多繼承,填補了多繼承的空白
        • NSProxy NSObject是同級的一個類,也能夠說是一個虛擬類,只是實現了NSObject的協議
        • NSProxy實際上是一個消息重定向封裝的一個抽象類,相似一個代理人,中間件,能夠經過繼承它,並重寫下面兩個方法來實現消息轉發到另外一個實例
          - (void)forwardInvocation:(NSInvocation *)invocation;
          - (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
相關文章
相關標籤/搜索