1. 程式人生 > >iOS進階--block詳解

iOS進階--block詳解

ios的oc語法底層是基於c語言來實現的,為了更好的瞭解ios的一些底層的東西,首先我們將oc轉成c語言,具體方法如下。

開啟終端,輸入xcodebuild -showsdks

可以獲取到本地上所裝的SDK。


接下來cd到你要rewrite的檔案的目錄下,如果該檔案沒有依賴第三方庫或者framework的話,直接

xcrun -sdk iphonesimulator11.2 clang -rewrite-objc main.m 

如果依賴framework,則

xcrun -sdk iphonesimulator11.2 clang -rewrite-objc -F /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m

此時可以獲取到一個.cpp檔案。開啟後可以看到整個檔案的底層C語言實現。

先輸入一個block塊程式碼。(block函式的語法和使用比較簡單,不講)


將其轉化之後,得到


我們將viewdidload裡面的方法呼叫取出來

static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));

    int num1 = 30,num2 =50;
    int (*myBlock)(int a) = ((int (*)(int))&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, num1, num2));
    ((int (*)(__block_impl *, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 100);
}

首先我們注意到viewDidLoad的方法傳入了兩個引數,這兩個引數被稱作隱式引數。self不多介紹,_cmd的作為一個SEL指標型別,指向某一個函式的IMP指標。

這裡執行的操作就是:初始化一個block例項,交給我們這麼myBlock名字變數,也就是用myBlock這個指標指向這個block例項,執行的時候,直接找到這個block中的指向函式地址的指標。int (*myBlock)(int a)這個則是C語言經典的函式指標。

再看myBlock的定義。

struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  int num1;
  int num2;
  __ViewController__viewDidLoad_block_impl_0(void *fp,struct __ViewController__viewDidLoad_block_desc_0 *desc,int _num1, int _num2,int flags=0) : num1(_num1), num2(_num2) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static int __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself,int a) {
  int num1 = __cself->num1; // bound by copy
  int num2 = __cself->num2; // bound by copy

        return a+num1+num2+100;
    }

可以看到,這裡對應我們程式碼中的block中的實現,所以可以知道,block使用的匿名函式,實際上被當作一個函式來處理。不過傳入的是:一個_main_block_impl_0型別的結構體,裡面有一個block_impl的結構體,和一個_main_block_desc_0的結構體。跟著是他們的建構函式。

來簡單看一下這個_main_block_impl_0結構體吧:

isa指向這個block的型別。這裡說明這個block是NSConcreteStackBlock型別的。

(補充:OS中有三種block,下文會細說
NSConcreteGlobalBlock;//在全域性中定義的
NSConcreteStackBlock; //在區域性定義的
NSConcreteMallocBlock;//分配在堆中)

flag是標誌,可以看到,預設構造為0;
還有一個FuncPtr,也就是指向函式地址的指標。
還有一個__main_block_desc_0的結構體,
在下面可以看到這個結構體的初始化,
一個是reserverd預設為0,
一個是block_size。是這個impl的size。
所以,這個_main_block_impl_0,我們可以理解為就是一個block例項,裡面的成員變數有要執行的函式的指標,和isa(和所有的oc物件一樣),還有一個size。

此時再研究下外部引數,num1,num2。block中用到的變數被作為成員變數追加到了結構體中,而由於只是傳入了num1,num2的值,在結構體內部是無法直接作修改的。

當加上了__block字首之後,我們再來觀察num1和num2發生的變化。

struct __Block_byref_num1_0 {
  void *__isa;
__Block_byref_num1_0 *__forwarding;
 int __flags;
 int __size;
 int num1;
};
struct __Block_byref_num2_1 {
  void *__isa;
__Block_byref_num2_1 *__forwarding;
 int __flags;
 int __size;
 int num2;
};

struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  __Block_byref_num1_0 *num1; // by ref
  __Block_byref_num2_1 *num2; // by ref
  __ViewController__viewDidLoad_block_impl_0(void *fp,struct __ViewController__viewDidLoad_block_desc_0 *desc, __Block_byref_num1_0 *_num1, __Block_byref_num2_1 *_num2,int flags=0) : num1(_num1->__forwarding), num2(_num2->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static int __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself,int a) {
  __Block_byref_num1_0 *num1 = __cself->num1; // bound by ref
  __Block_byref_num2_1 *num2 = __cself->num2; // bound by ref

        return a+(num1->__forwarding->num1)+(num2->__forwarding->num2)+100;
    }

原來是把一個區域性變數,封裝成了一個結構體!!

賦值的時候直接給這個結構體中的這個值賦值。在訪問這些變數的時候,實質就是在訪問它的結構體。

static int __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself,int a) {
  __Block_byref_num1_0 *num1 = __cself->num1; // bound by ref
  __Block_byref_num2_1 *num2 = __cself->num2; // bound by ref

        (num1->__forwarding->num1) = 1000;
        return a+(num1->__forwarding->num1)+(num2->__forwarding->num2)+100;
    }
關於__forwarding這個變數:

設定在棧上的block,myBlock這個名字變數”作用域結束時,block變數也會廢棄。
所以,iOS提供了將block結構體和_block變數,複製到堆上的方法。即使block的name變數結束,那麼堆上的block還可以繼續訪問。
而此時,_block變數結構體中的_forwarding變數可以實現,無論在堆上還是在棧上。都可以正確訪問_block變數。可以理解,當把_block變數複製到堆上的時候,_forwarding就指向堆裡中的自己。所以無論是訪問棧中自己,還是堆中的自己,最終訪問都是堆中的這個值。

一個Block對_block的記憶體管理方式與 ARC機制完全相同。
而_main_block_desc 中的copy和dispose就是這個 __block的retain和release操作。

那什麼block在時候會複製到堆呢?

  1. 掉用block的copy方法。

  2. block作為函式返回值返回時。

  3. block呼叫外面的_strong的id的類時,或用_block時。

  4. 方法中,用usingblock或者GCD中的API時。

這裡想講一下,在區域性函式裡,定義block時,打印出來還是NSConcreteGlobalBlock型別的,而且只要用了外部變數,不管是assign還是week還是strong型別的,打印出來都是NSConcreteMallocBlock型別的。所以我猜測這會不會是蘋果新版的改進,為了block在訪問無效的變數,直接把block拷貝到堆上,從而也拷貝一份變數。或許是我忽略了中間的某個步驟

其實到了這裡,不用再描述,也知道為什麼會發送死迴圈,又怎麼解決了。當在block中用self的時候,block拷貝到堆上,首先,在棧上的這個block有一個持有者,是name這個變數。當name這個變數作用域之外,棧上這個block就release了。,那麼當block拷貝到堆上的時候,block有一個持有者是self,那麼block在拷貝時,它的變數一個self指標,也會拷貝,而self又指向這個block,block持有self,self持有block,兩者都不會釋放。要打破這個迴圈,需要將self置為__week,就算拷貝一個week指標,那也不影響self的引用計數。

往下看程式碼會發現有一個OC的屬性列表和方法列表:

static struct/*_ivar_list_t*/ {
unsignedint entsize; // sizeof(struct _prop_t)
unsignedint count;
struct _ivar_t ivar_list[2];
} _OBJC_$_INSTANCE_VARIABLES_ViewController __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_ivar_t),
2,
{{(unsignedlong int *)&OBJC_IVAR_$_ViewController$_str1,"_str1", "@\"NSString\"",3, 8},
{(unsignedlong int *)&OBJC_IVAR_$_ViewController$_str2,"_str2", "@\"NSString\"",3, 8}}
};

static struct/*_method_list_t*/ {
unsignedint entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[6];
} _OBJC_$_INSTANCE_METHODS_ViewController __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
6,
{{(struct objc_selector *)"viewDidLoad","[email protected]:8", (void *)_I_ViewController_viewDidLoad},
{(struct objc_selector *)"touchesBegan:withEvent:","[email protected]:[email protected]@24", (void *)_I_ViewController_touchesBegan_withEvent_},
{(struct objc_selector *)"str1","@[email protected]:8", (void *)_I_ViewController_str1},
{(struct objc_selector *)"setStr1:","[email protected]:[email protected]", (void *)_I_ViewController_setStr1_},
{(struct objc_selector *)"str2","@[email protected]:8", (void *)_I_ViewController_str2},
{(struct objc_selector *)"setStr2:","[email protected]:[email protected]", (void *)_I_ViewController_setStr2_}}
};

這裡引入一個知識點,oc中方法的呼叫實質是訊息的傳送,在Objective-C中,message與方法的真正實現是在執行階段繫結的,而非編譯階段。編譯器會將訊息傳送轉換成對objc_msgSend方法的呼叫。objc_msgSend方法含兩個必要引數:receiver、方法名(即:selector),如:[receivermessage]; 將被轉換為:objc_msgSend(receiver,selector);objc_msgSend方法也能hold住message的引數,如:objc_msgSend(receiver,selector,para)。

當向一個物件傳送訊息時,objc_msgSend方法根據物件的isa指標找到物件的類,然後在類的排程表(dispatch table)中查詢selector。如果無法找到selector,objc_msgSend通過指向父類的指標找到父類,並在父類的排程表(dispatch table)中查詢selector,以此類推直到NSObject類。一旦查詢到selector,objc_msgSend方法根據排程表的記憶體地址呼叫該實現。通過這種方式,message與方法的真正實現在執行階段才繫結。

selector作為一個指標,本身無法直接獲取到對應的函式。

將selector看作是一種方法編號,通過selector能夠快速找到對應的imp指標,imp指標即是函式指標,能夠指向某一個函式的實現。(implementation)

selector和imp會有一個對應的對映表,如這張圖:

static struct /*_method_list_t*/ {
unsigned int entsize;  // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[6];
} _OBJC_$_INSTANCE_METHODS_ViewController __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
6,
{{(struct objc_selector *)"viewDidLoad", "[email protected]:8", (void *)_I_ViewController_viewDidLoad},
{(struct objc_selector *)"touchesBegan:withEvent:", "[email protected]:[email protected]@24", (void*)_I_ViewController_touchesBegan_withEvent_},
{(struct objc_selector *)"str1", "@[email protected]:8", (void *)_I_ViewController_str1},
{(struct objc_selector *)"setStr1:", "[email protected]:[email protected]", (void *)_I_ViewController_setStr1_},
{(struct objc_selector *)"str2", "@[email protected]:8", (void *)_I_ViewController_str2},
{(struct objc_selector *)"setStr2:", "[email protected]:[email protected]", (void *)_I_ViewController_setStr2_}}
};

例如(struct objc_selector *)"setStr2:",即是sel指標,指向(void *)_I_ViewController_setStr2_}

我們再看下(void *)_I_ViewController_setStr2_的實現。

static void _I_ViewController_setStr2_(ViewController * self, SEL _cmd, NSString *str2) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct ViewController, _str2), (id)str2, 0, 1); }
前面說過,c函式中會包含兩個隱藏的引數,在這裡就能很好的體現出來。從第三個引數開始,即是我們手動傳進去的引數。這就是str2的set方法的實現。

為了保證訊息傳送與執行的效率,系統會將全部selector和使用過的方法的記憶體地址快取起來。每個類都有一個獨立的快取,快取包含有當前類自己的selector以及繼承自父類的selector。查詢排程表(dispatch table)前,訊息傳送系統首先檢查receiver物件的快取。

快取命中的情況下,訊息傳送(messaging)比直接呼叫方法(function call)只慢一點點點點。

這裡最後補充一點,當一個msgSend執行了之後,順著isa指標找到了對應的方法就會發送訊息(一層一層找,如果子類找不到就會在父類找)。但是如果找不到對應方法的實現,是否就會crash,報unrecognized selector sent to instance 0x87?這裡有一個訊息轉發的機制。

訊息轉發分為兩大階段。
第一階段先徵詢接收者,所屬的類,看其是否能動態新增方法,以處理當前這個“未知選擇子”,成為“動態方法解析”。
第二階段涉及“完整訊息轉發”。如果第一階段執行完,還是沒有找到相應的選擇子對應的方法,此時執行期系統會請求接收者以其他手段來處理與訊息有關的方法呼叫。這裡又分為兩小步:
首先,請接收者看看有沒有其他物件能夠處理這條訊息。若有,則執行期系統會把訊息轉給那個物件,這個過程稱為“備援接收者”。
其次,如果沒有,則進入第二小步“完整的訊息轉發”,執行期系統會把與訊息有關的全部細節都封裝到NSInvocation物件中,再給接收者最後一次機會,令其設法解決當前還未處理的這條訊息。

參考資料:http://www.cocoachina.com/ios/20160307/15441.html

http://www.cocoachina.com/ios/20160830/17424.html

如果有覺得哪些地方筆者理解得有偏差,請聯絡我~

相關推薦

iOS--block

ios的oc語法底層是基於c語言來實現的,為了更好的瞭解ios的一些底層的東西,首先我們將oc轉成c語言,具體方法如下。開啟終端,輸入xcodebuild -showsdks可以獲取到本地上所裝的SDK。接下來cd到你要rewrite的檔案的目錄下,如果該檔案沒有依賴第三方庫

python------面向對象反射(重點)

code one -- ... set bject pan strip() asa 一.反射 通過字符串映射或者修改程序運行時的狀態,屬性,或者方法。 1.getattr(object,name,default=None) 2.hasattr(object,name

區塊鏈技術-深入以太坊智慧合約語言 solidity(含原始碼)-熊麗兵-專題視訊課程...

區塊鏈技術進階-深入詳解以太坊智慧合約語言 solidity(含原始碼)—103人已學習 課程介紹         區塊鏈開發技術進階-深入詳解以太坊智慧合約語言 solidity視訊培訓教程:本課

安卓自定義View-MotionEvent

Android MotionEvent 詳解,之前用了兩篇文章 事件分發機制原理 和 事件分發機制詳解 來講解事件分發,而作為事件分發主角之一的 MotionEvent 並沒有過多的說明,本文就帶大家瞭解 MotionEvent 的相關內容,簡要介紹觸控事件,主要包括 單點觸控、多點

安卓自定義View-Matrix

這應該是目前最詳細的一篇講解Matrix的中文文章了,在上一篇文章Matrix原理中,我們對Matrix做了一個簡單的瞭解,偏向理論,在本文中則會詳細的講解Matrix的具體用法,以及與Matrix相關的一些實用技巧。 ⚠️ 警告:測試本文章示例之前請關閉硬體加速。

IOS開發之Block

從Mac OS X 10.6以及iOS4開始,蘋果在GCC和Clang編譯器中為C語言引入了一個新擴充套件:Blocks,使得程式設計師可以在C、Objective-C、C++和Objective-C中使用閉包。Blocks有點像函式,但是它可以在其它函式或方法中進行宣告和定義,同時它還是匿名的(匿名函式)

阿里P7講解Java匿名內部類

在java提高篇—–詳解內部類中對匿名內部類做了一個簡單的介紹,但是內部類還存在很多其他細節問題,所以就衍生出這篇部落格。在這篇部落格中你可以瞭解到匿名內部類的使用、匿名內部類要注意的事項、如何初始化匿名內部類、匿名內部類使用的形參為何要為final。 一、使用匿名內部類內部類 匿名

Mybatis用法

一.mybatis主配置檔案SqlMapConfig.xml進階配置 SqlMapConfig.xml中配置檔案的內容和順序如下:(數字代表層級) properties(屬性) settings(全域性配置引數)typeAliases(類型別名) typeHandlers(

Android——Fragment之操作原理(三)

引言 前一篇文章總結了Fragment 的基本概念和基本的用法,相信大家也能夠掌握一些知識了,但是對於一些操作可能還是不知其所以然,說實話曾經很長一段時間為也是暈乎乎的,後來才慢慢重視去學習瞭解,才略知一二,遂分享之。 一、管理Fragement所涉及到

【Hibernate框架學習】:HibernateHibernate配置檔案和物件關係對映配置檔案

       Hibernate核心配置檔案               我們先來看一個比較常見的hibernate.cfg.xml配置檔案: <!DOCTYPE hibernate-confi

Django之models高階技術

目錄 一、常用欄位 1.AutoField 2.IntegerField 3.CharField 4.自定義及使用char 5.DateField 6.Date

[iOS]iOS快取機制

1、為什麼需要快取 應用需要離線工作的主要原因就是改善應用所表現出的效能。將應用內容快取起來就可以支援離線。我們可以用兩種不同的快取來使應用離線工作。 第一種是按需快取,這種情況下應用快取起請求應答,就和Web瀏覽器的工作原理一樣;第二種是預快

iOS —— iOS 記憶體管理 & Block

第一篇 iOS 記憶體管理 1 似乎每個人在學習 iOS 過程中都考慮過的問題 alloc retain release delloc 做了什麼? autoreleasepool 是怎樣實現的? __unsafe_unretained 是什麼? Block 是怎樣實現的 什麼

ios開發系列】block

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

OC學習小結之ios運行過程

for cat 用戶 with res nbsp c學習 launch cati 1)ios核心類 UIView 視圖,屏幕上能看得見的東西都是視圖,例如:按鈕、文本標簽、和表格等 UIViewController:內部默認有個視圖(UIView),負責管理UIView的

#21 在Linux裏程管理,與pstree、ps、pgrep、pkill、pidof、top命令的應用

在linux裏進程管理詳解 與pstree、ps、pgrep、pkill、pidof、top命令的應用 進程管理: 所謂進程:process,一個活動的程序的實體的副本; 生命周期; 可能包含一個或多個執行流; 創建進程: 每個進程的組織結構是一致的: 內核在正常啟動並且全

iOS 運行時

序列 get not oci protocol caption 聲明 實現 att 註:本篇文章轉自:http://www.jianshu.com/p/adf0d566c887 一、運行時簡介 Objective-C語言是一門動態語言,它將很多靜態語言在編譯和鏈接時期做的事

Linux基礎程命令

linux運維學習進程有關基礎命令一、進程定義1、進程就是CPU未完成的工作,而且它是其中運行著一個或多個線程的地址空間和這些線程所需要的系統資源。二、Linux系統進程和一些有關進程的命令1、ps命令 ps [options] 1》ps - report a snapshot of the c

iOS Code Signing: 解惑

唯一標識 解惑 並且 iphone 條件 等於 個人開發 視覺 電腦 原文鏈接地址:http://www.cnblogs.com/andyque/archive/2011/08/30/2159086.html iPhone開發的代碼簽名 代碼簽名確保代碼的真實以及明確識

iOS APP上架流程

復制 存儲 iphone6 調試 5.1 編輯 gre 9.png 待審核 iOS APP上架流程詳解 青蔥烈馬 2016.04.28 前言:作為一名 iOS 開發工程師, APP 的上架是必備技能. iOS 上架的流程主要可以簡單總結為: 一個包,兩個