1. 程式人生 > >【ios開發系列】block詳解

【ios開發系列】block詳解

block到底是什麼

我們使用clang的rewrite-objc命令來獲取轉碼後的程式碼。

1、block的底層實現

我們來看看最簡單的一個block:

這個block僅僅列印棧變數i和j的值,其被clang轉碼為:

首先是一個結構體__main_block_impl_0(從圖二中的最後一行可以看到,block是一個指向__main_block_impl_0的指標,初始化後被型別強轉為函式指標),其中包含的__block_impl是一個公共實現(學過c語言的同學都知道,__main_block_impl_0的這種寫法表示其可以被型別強轉為__block_impl型別):

  1. struct __block_impl { 
  2.   void *isa; 
  3.   int Flags; 
  4.   int Reserved; 
  5.   void *FuncPtr; 
  6. };

isa指標說明block可以成為一個objc物件。

__main_block_impl_0的意思是main函式中的第0個block的implementation,這就是這個block的主體了。

這個結構體的建構函式的引數:

block實際執行程式碼所在的函式的指標,當block真正被執行時,實際上是呼叫了這個函式,其命名也是類似的方式。

block的描述結構體,注意這個結構體宣告結束時就建立了一個唯一的desc,這個desc包含了block的大小,以及複製和析構block時需要額外呼叫的函式。

接下來是block所引用到的變數們

最後是一個標記值,內部實現需要用到的。(我用計算器看了一下,570425344這個值等於1<<29,即BLOCK_HAS_DESCRIPTOR這個列舉值)

所以,我們可以看到:

為什麼上一篇我們說j已經不是原來的j了,因為j是作為引數傳入了block的建構函式,進行了值複製。

帶有__block標記的變數會被取地址來傳入建構函式,為修改其值奠定了基礎

接下來是block執行函式__main_block_func_0:

其唯一的引數是__main_block_impl_0的指標,我們看到printf語句的資料來源都取自__cself這個指標,比較有意思的是i的取值方式(帶有__block標記的變數i被轉碼為一個結構體),先取__forward指標,再取i,這為將i複製到堆中奠定了基礎。

再下來是預定義好的兩個複製/釋放輔助函式,其作用後面會講到。 

最後是block的描述資訊結構體 __main_block_desc_0,其包含block的記憶體佔用長度,已經複製/釋放輔助函式的指標,其宣告結束時,就建立了一個名為__main_block_desc_0_DATA的結構體,我們看它構造時傳入的值,這個DATA結構體的作用就一目瞭然了:

長度用sizeof計算,輔助函式的指標分別為上面預定義的兩個輔助函式。

注意,如果這個block沒有使用到需要在block複製時進行copy/retian的變數,那麼desc中不會有輔助函式

至此,一個block所有的部件我們都看齊全了,一個主體,一個真正的執行程式碼函式,一個描述資訊(可能包含兩個輔助函式)。

2、構造一個block

我們進入main函式:

圖一中的第三行(block的宣告),在圖二中,轉化為一個函式指標的宣告,並且都沒有被賦予初始值。

而圖一中的最後一行(建立一個block),在圖二中,成為了對__main_block_impl_0的建構函式的呼叫,傳入的引數的意義上面我們已經講過了。

所以構造一個block就是建立了__main_block_impl_0 這個c++類的例項。

3、呼叫一個block

呼叫一個block的寫法很簡單,與呼叫c語言函式的語法一樣:

  1. blk(); 

其轉碼後的語句:

  1. ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); 

將blk這個函式指標型別強轉為__block_impl型別,然後取其執行函式指標,然後將此指標型別強轉為返回void*並接收一個__block_impl*的函式指標,最後呼叫這個函式,傳入強轉為__block_impl*型別的blk,

即呼叫了前述的函式__main_block_func_0

4、objective-c類成員函式中的block

原始碼如下:

  1. - (void)of1 
  2.     OBJ1* oj = self; 
  3.     void (^oblk)(void) = ^{ printf("%d\n", oj.oi);}; 
  4.     Block_copy(oblk); 

這裡我故意將self賦值給oj這個變數,是為了驗證前一章提出的一個結論:無法通過簡單的間接引用self來防止retain迴圈,要避免迴圈,我們需要__block標記(多謝樓下網友的提醒)

轉碼如下:

  1. struct __OBJ1__of1_block_impl_0 { 
  2.   struct __block_impl impl; 
  3.   struct __OBJ1__of1_block_desc_0* Desc; 
  4.   OBJ1 *oj; 
  5.   __OBJ1__of1_block_impl_0(void *fp, struct __OBJ1__of1_block_desc_0 *desc, OBJ1 *_oj, int flags=0) : oj(_oj) { 
  6.     impl.isa = &_NSConcreteStackBlock; 
  7.     impl.Flags = flags; 
  8.     impl.FuncPtr = fp; 
  9.     Desc = desc; 
  10.   } 
  11. }; 
  12. staticvoid __OBJ1__of1_block_func_0(struct __OBJ1__of1_block_impl_0 *__cself) { 
  13.   OBJ1 *oj = __cself->oj; // bound by copy
  14.  printf("%d\n", ((int (*)(id, SEL))(void *)objc_msgSend)((id)oj, sel_registerName("oi")));} 

objc方法中的block與c中的block並無太多差別,只是一些標記值可能不同,為了標記其是objc方法中的blcok。

注意其建構函式的引數:OBJ1 *_oj

這個_oj在block複製到heap時,會被retain,而_oj與self根本就是相等的,所以,最終retain的就是self,所以如果當前例項持有了這個block,retain迴圈就形成了。

而一旦為其增加了__block標記:

  1. - (void)of1 
  2.     __block OBJ1 *bSelf = self; 
  3.     ^{ printf("%d", bSelf.oi); }; 
  4. }其轉碼則變為: 
  5. //增加了如下行
  6. struct __Block_byref_bSelf_0 { 
  7.   void *__isa; 
  8. __Block_byref_bSelf_0 *__forwarding; 
  9.  int __flags; 
  10.  int __size; 
  11.  void (*__Block_byref_id_object_copy)(void*, void*); 
  12.  void (*__Block_byref_id_object_dispose)(void*); 
  13.  OBJ1 *bSelf; 
  14. }; 
  15. staticvoid __Block_byref_id_object_copy_131(void *dst, void *src) { 
  16.  _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131); 
  17. staticvoid __Block_byref_id_object_dispose_131(void *src) { 
  18.  _Block_object_dispose(*(void * *) ((char*)src + 40), 131); 
  19. //宣告處變為
  20.     __block __Block_byref_bSelf_0 bSelf = {(void*)0,(__Block_byref_bSelf_0 *)&bSelf, 33554432, sizeof(__Block_byref_bSelf_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, self}; 

clang為我們的bSelf結構體建立了自己的copy/dispose輔助函式,33554432(即1<<25 BLOCK_HAS_COPY_DISPOSE)這個值告訴系統,我們的bSelf結構體具有copy/dispose輔助函式。

而131這個引數(二進位制1000 0011,即BLOCK_FIELD_IS_OBJECT (3) |BLOCK_BYREF_CALLER(128))

中的BLOCK_BYREF_CALLER在內部實現中告訴系統不要進行retain或者copy,

也就是說,在 __block bSelf 被複制至heap上時,系統會發現有輔助函式,而輔助函式呼叫後,並不retain或者copy 其結構體內的bSelf。

這樣就避免了迴圈retain。

小結:

當我們建立一個block,並呼叫之,編譯器為我們做的事情如下:

1.建立block所有的部件程式碼:一個主體,一個真正的執行程式碼函式,一個描述資訊(可能包含兩個輔助函式)。

2.將我們的建立程式碼轉碼為block_impl的構造語句。

3.將我們的執行語句轉碼為對block的執行函式的呼叫。

下一篇我們將剖析runtime.c的原始碼,並理解block的堆疊轉換。

【4】

終於有空開始這系列最後一篇的編寫。這一篇,我們將看到block的記憶體管理的內部實現,通過剖析runtime庫原始碼,我們可以更深刻的理解block的記憶體運作體系。

看此篇時,請大家同時開啟兩個網址(或者下載它們到本地然後開啟):

記憶體管理的真面目

objc層面如何區分不同記憶體區的block

Block_private.h中有這樣一組值:

  1. /* the raw data space for runtime classes for blocks */
  2. /* class+meta used for stack, malloc, and collectable based blocks */
  3. BLOCK_EXPORT void * _NSConcreteStackBlock[32]; 
  4. BLOCK_EXPORT void * _NSConcreteMallocBlock[32]; 
  5. BLOCK_EXPORT void * _NSConcreteAutoBlock[32]; 
  6. BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32]; 
  7. BLOCK_EXPORT void * _NSConcreteGlobalBlock[32]; 
  8. BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32]; 

其用於對block的isa指標賦值

1.棧

  1. struct __OBJ1__of2_block_impl_0 { 
  2.   struct __block_impl impl; 
  3.   struct __OBJ1__of2_block_desc_0* Desc; 
  4.   OBJ1 *self; 
  5.   __OBJ1__of2_block_impl_0(void *fp, struct __OBJ1__of2_block_desc_0 *desc, OBJ1 *_self, int flags=0) : self(_self) { 
  6.     impl.isa = &_NSConcreteStackBlock; 
  7.     impl.Flags = flags; 
  8.     impl.FuncPtr = fp; 
  9.     Desc = desc; 
  10.   } 
  11. }; 

在棧上建立的block,其isa指標是_NSConcreteStackBlock。

2.全域性區

在全域性區建立的block,其比較類似,其建構函式會將isa指標賦值為_NSConcreteGlobalBlock。

3.堆

我們無法直接建立堆上的block,堆上的block需要從stack block拷貝得來,在runtime.c中的_Block_copy_internal函式中,有這樣幾行:

  1. // Its a stack block.  Make a copy.
  2.     if (!isGC) { 
  3.         struct Block_layout *result = malloc(aBlock->descriptor->size); 
  4.         ... 
  5.         result->isa = _NSConcreteMallocBlock; 
  6.         ... 
  7.         return result; 
  8.     } 

可以看到,棧block複製得來的新block,其isa指標會被賦值為_NSConcreteMallocBlock

4.其餘的isa型別

  1. BLOCK_EXPORT 

    相關推薦

    ios開發系列block

    block到底是什麼 我們使用clang的rewrite-objc命令來獲取轉碼後的程式碼。 1、block的底層實現 我們來看看最簡單的一個block: 這個block僅僅列印棧變數i和j的值,其被clang轉碼為: 首先是一個結構體__main

    iOS開發系列九宮格布局

    使用 objc with div self. orm i++ back hab /** * 這個盡管非常easy,算是一個小技巧,可是碰到了就記錄下來吧.積跬步,致千裏嘛. */ - (void)scratchableLatex { for (int i=

    iOS開發系列NSObject方法介紹

    ati ber oid ring cto rgb dst -s 推斷 NSObject是OC中的基類,全部類都繼承於此,這裏面也給我們提供了非常多與“類”和“方法”相關的方法,本文將解說幾個非常有用的方法。 正文: Per

    iOS開發系列將阿拉伯數字轉換為中文數字

    /** * 將阿拉伯數字轉換為中文數字 */ +(NSString *)translationArabicNum:(NSInteger)arabicNum { NSString *arabicNumStr = [NSString stringWithForma

    3天搞定的小型B/S內部管理類軟體定製開發專案軟體開發實戰10步驟

         十一休假,杭州西湖邊逛了一圈只能用人山人海來形容,浙大紫金港校區也逛了一圈風景如畫,建設得真不錯很棒,假期就去了這2個地方,然後在家裡陪老婆、看孩子、洗尿布、打了幾局星際爭霸,在網上接了一個B/S架構的內部管理類定製軟體、淘寶上收了600元辛苦費後就開始行動了、現在把整個開發過程講解分享如下文

    iOS開發-79利用Modal方式實現控制器之間的跳轉

    article 運用 mis cli 控制 present 沒有 dismiss 導航控制器 利用Modal方法。事實上就是以下兩個方法的運用。Modal方式的切換效果是從底部呈現。 -(void)clickModal{ WPViewController *wp

    iOS開發-51案例學習:動畫新寫法、刪除子視圖、視圖順序、延遲方法、button多功能使用方法及icon圖標和啟動頁設置

    無法查看 font targe 技術 value lstat tostring sta dict 案例效果: (1)導入所需的素材,然後用storyboard把上半截位置和大小相對固定的東西布局起來。當然,這些控件也要定義成對應地IBOutlet和IBActio

    iOS開發-32iOS程序真機調試須要購買調試證書怎麽辦?

    pos 上進 ack tracking popu 怎麽辦 ont 調試 開發程序 一、情況 我們在開發iOS程序的時候,一般都是在模擬器上執行查看效果的。可是,當開完完畢。須要在真機上調試怎麽辦? 二、官方解決的方法 蘋果有為個人和企業開發人員提供調試證書和

    iOS開發-47怎樣下載iOS 7.1 Simulator 以及iOS 8離線的Documentation這些文件?

    hang http 下載 資料 zhang 目錄 ios 8 log targe (1)最官方的解決的方法 在Xcode6裏面提供下載。依照下圖找到下載就可以。一般建議把以下的自己主動檢查更新和下載的框框勾起來,這樣它會幫我們自己主動下載。 問題

    黑馬PHP教程錯誤

    error_log 報告 color 顯示 自定義 borde 技術 誤報 處理 一,錯誤通常分3種 二,錯誤的分級 三,錯誤的觸發 四,顯示錯誤報告 問題一:設置顯示錯誤報告 問題二:顯示哪些級別的錯誤報告 五,錯誤日誌的記錄問題 問題一

    iOS開發多執行緒

    在iOS開發中,多執行緒開發是非常重要的核心之一,這篇文章和大家分享一下多執行緒的進階-死鎖. iOS有三種多執行緒程式設計的技術,分別是:(一)NSThread(二)Cocoa NSOperation(三)GCD(全稱:Grand Central Dispatch) 如果你對多執行緒

    iOS開發之AddressBookUI框架

    iOS開發之AddressBookUI框架詳解 一、關於AddressBookUI框架     AddressbookUI是iOS開發框架中提供的一套通訊錄介面元件。其中封裝好了一套選擇聯絡人,檢視聯絡人的介面,在需要時開發者可以直接呼叫。當然對於聯絡人介面,

    iOS開發之AddressBook框架

    iOS開發之AddressBook框架詳解 一、寫在前面     首先,AddressBook框架是一個已經過時的框架,iOS9之後官方提供了Contacts框架來進行使用者通訊錄相關操作。儘管如此,AddressBook框架依然是一個非常優雅並且使用方便的通

    iOS開發之Accounts框架

    iOS開發之Accounts框架詳解     Accounts框架是iOS原生提供的一套賬戶管理框架,其支援Facebook,新浪微博,騰訊微博,Twitter和領英賬戶管理的功能。需要注意,在iOS 11及以上系統中,將此功能已經刪除,因此Accounts.frame

    java專案實戰Servlet以及Servlet編寫登陸頁面(二)

           Servlet是Sun公司提供的一門用於開發動態web網頁的技術。Sun公司在API中提供了一個servlet介面,我們如果想使用java程式開發一個動態的web網頁,只需要實現servelet介面,並把類部署到web伺服器上就可以運行了。 到底什麼是Ser

    iOS開發-29解決方案:TabBar的圖片不顯示,只顯示灰色的正方形

    (1)現象 tabbar上的圖片變成一塊正方形的灰色塊塊,原先的圖片沒有了。 (2)原因 tabbar上的圖片本質上不是一個圖片,而是一個形狀圖片。系統對我們使用的圖片也只是把其中的形狀“扣”出來,其餘的背景什麼的都不要。因為我們可能給背景加了顏色,所以系統扣的時候只是把

    kubernetes/k8s概念CNI

    1、為什麼CNI        CNI是Container Network Interface的是一個標準的,通用的介面。現在容器平臺:docker,kubernetes,mesos,容器網路解決方案:flannel,calico,weave。只要提供一個標準的介面,就能為

    IOS開發基礎之判斷NSString為純數字

    //判斷是否為整形: - (BOOL)isPureInt:(NSString*)string{     NSScanner* scan = [NSScanner scannerWithString:string];     int val;     return

    iOS開發-底層篇-Classios底層-class

    前言:iOS的開發語言objective-c,它的真實面目是它不是真正的面嚮物件語言,而抽象理解為此而已。其實它就是C+,有個公式可以很好地詮釋那就是 OC = C + Runtime; 接下來我們就好好講講在Runtime下的objc-class。準備資料,objc

    多執行緒BlockingQueue

    前言:      在新增的Concurrent包中,BlockingQueue很好的解決了多執行緒中,如何高效安全“傳輸”資料的問題。通過這些高效並且執行緒安全的佇列類,為我們快速搭建高質量的多執行緒程式帶來極大的便利。本文詳細介紹了BlockingQueue家庭中的所有成員