Block解析(iOS)

来源:http://www.cnblogs.com/bao-jie/archive/2016/12/19/6196765.html
-Advertisement-
Play Games

1. 操作系統中的棧和堆 我們先來看看一個由C/C++/OBJC編譯的程式占用記憶體分佈的結構: 棧區(stack):由系統自動分配,一般存放函數參數值、局部變數的值等。由編譯器自動創建與釋放。其操作方式類似於數據結構中的棧,即後進先出、先進後出的原則。 例如:在函數中申明一個局部變數int b;系統 ...


1. 操作系統中的棧和堆

我們先來看看一個由C/C++/OBJC編譯的程式占用記憶體分佈的結構: 

棧區(stack):由系統自動分配,一般存放函數參數值、局部變數的值等。由編譯器自動創建與釋放。其操作方式類似於數據結構中的棧,即後進先出、先進後出的原則。

例如:在函數中申明一個局部變數int b;系統自動在棧中為b開闢空間。

堆區(heap):一般由程式員申請並指明大小,最終也由程式員釋放。如果程式員不釋放,程式結束時可能會由OS回收。對於堆區的管理是採用鏈表式管理的,操作系統有一個記錄空閑記憶體地址的鏈表,當接收到程式分配記憶體的申請時,操作系統就會遍歷該鏈表,遍歷到一個記錄的記憶體地址大於申請記憶體的鏈表節點,並將該節點從該鏈表中刪除,然後將該節點記錄的記憶體地址分配給程式。

例如:在C中malloc函數 

   char p;

   p = (char)malloc(10);

但是p本身是在棧中的。

鏈表:是一種常見的基礎數據結構,一般分為單向鏈表、雙向鏈表、迴圈鏈表。以下為單向鏈表的結構圖:

linked_list.jpg

單向鏈表是鏈表中最簡單的一種,它包含兩個區域,一個信息域和一個指針域。信息域保存或顯示關於節點的信息,指針域儲存下一個節點的地址。

上述的空閑記憶體地址鏈表的信息域保存的就是空閑記憶體的地址。

全局區/靜態區:顧名思義,全局變數和靜態變數存儲在這個區域。只不過初始化的全局變數和靜態變數存儲在一塊,未初始化的全局變數和靜態變數存儲在一塊。程式結束後由系統釋放。

文字常量區:這個區域主要存儲字元串常量。程式結束後由系統釋放。

程式代碼區:這個區域主要存放函數體的二進位代碼。

例子:

  //main.cpp   int a = 0; // 全局初始化區   char *p1; // 全局未初始化區   main {       int b; // 棧       char s[] = "abc"// 棧       char *p2; // 棧       char *p3 = "123456"// 123456\0在常量區,p3在棧上         static int c =0; // 全局靜態初始化區       p1 = (char *)malloc(10);         p2 = (char *)malloc(20); // 分配得來的10和20位元組的區域就在堆區       strcpy(p1, "123456"); // 123456\0在常量區,這個函數的作用是將"123456" 這串字元串複製一份放在p1申請的10個位元組的堆區域中。       // p3指向的"123456"與這裡的"123456"可能會被編譯器優化成一個地址。   }
 

strcpy函數

原型聲明:extern char *strcpy(char* dest, const char *src);

功能:把從src地址開始且含有NULL結束符的字元串複製到以dest開始的地址空間。

2. 結構體(Struct)

在C語言中,結構體(struct)指的是一種數據結構。結構體可以被聲明為變數、指針或數組等,用以實現較複雜的數據結構。結構體同時也是一些元素的集合,這些元素稱為結構體的成員(member),且這些成員可以為不同的類型,成員一般用名字訪問。

我們來看看結構體的定義:struct tag { member-list } variable-list;

struct:結構體關鍵字。

tag:結構體標簽。

member-list:結構體成員列表。

variable-list:為結構體聲明的變數列表。

在一般情況下,tag,member-list,variable-list這三部分至少要出現兩個。以下為示例:

  // 該結構體擁有3個成員,整型的a,字元型的b,雙精度型的c   // 並且為該結構體聲明瞭一個變數s1   // 該結構體沒有標明其標簽   struct{       int a;       char b;       double c;   } s1;   // 該結構體擁有同樣的三個成員   // 並且該結構體標明瞭標簽EXAMPLE   // 該結構體沒有聲明變數   struct EXAMPLE{       int a;       char b;       double c;   };   //用EXAMPLE標簽的結構體,另外聲明瞭變數t1、t2、t3   struct EXAMPLE t1, t2[20], *t3;

以上就是簡單結構體的代碼示例。結構體的成員可以包含其他結構體,也可以包含指向自己結構體類型的指針。結構體的變數也可以是指針。

下麵我們來看看結構體成員的訪問。結構體成員依據結構體變數類型的不同,一般有2種訪問方式,一種為直接訪問,一種為間接訪問。直接訪問應用於普通的結構體變數,間接訪問應用於指向結構體變數的指針。直接訪問使用結構體變數名.成員名,間接訪問使用(*結構體指針名).成員名或者使用結構體指針名->成員名。相同的成員名稱依靠不同的變數首碼區分。

  struct EXAMPLE{       int a;       char b;   };   //聲明結構體變數s1和指向結構體變數的指針s2   struct EXAMPLE s1, *s2;   //給變數s1和s2的成員賦值,註意s1.a和s2->a並不是同一成員   s1.a = 5;   s1.b = 6;   s2->a = 3;   s2->b = 4;

最後我們來看看結構體成員存儲。在記憶體中,編譯器按照成員列表順序分別為每個結構體成員分配記憶體。如果想確認結構體占多少存儲空間,則使用關鍵字sizeof,如果想得知結構體的某個特定成員在結構體的位置,則使用offsetof巨集(定義於stddef.h)。

  struct EXAMPLE{         int a;       char b;   };   //獲得EXAMPLE類型結構體所占記憶體大小   int size_example = sizeof( struct EXAMPLE );   //獲得成員b相對於EXAMPLE儲存地址的偏移量   int offset_b = offsetof( struct EXAMPLE, b );   3 閉包(Closure)

閉包就是一個函數,或者一個指向函數的指針,加上這個函數執行的非局部變數。

說的通俗一點,就是閉包允許一個函數訪問聲明該函數運行上下文中的變數,甚至可以訪問不同運行上文中的變數。

我們用腳本語言來看一下:

  function funA(callback){       alert(callback());   }   function funB(){       var str = "Hello World"// 函數funB的局部變數,函數funA的非局部變數       funA(           function(){               return str;           }       );   }

通過上面的代碼我們可以看出,按常規思維來說,變數str是函數funB的局部變數,作用域只在函數funB中,函數funA是無法訪問到str的。但是上述代碼示例中函數funA中的callback可以訪問到str,這是為什麼呢,因為閉包性。

2.blcok基礎知識

block實際上就是Objective-C語言對閉包的實現。

2.1 block的原型及定義

我們來看看block的原型:

  NSString * ( ^ myBlock )( int );

上面的代碼聲明瞭一個block(^)原型,名字叫做myBlock,包含一個int型的參數,返回值為NSString類型的指針。

下麵來看看block的定義:

  myBlock = ^( int paramA )   {       return [ NSString stringWithFormat: @"Passed number: %i", paramA ];   };

上面的代碼中,將一個函數體賦值給了myBlock變數,其接收一個名為paramA的參數,返回一個NSString對象。

註意:一定不要忘記block後面的分號。

定義好block後,就可以像使用標準函數一樣使用它了:

  myBlock(1);

由於block數據類型的語法會降低整個代碼的閱讀性,所以常使用typedef來定義block類型。例如,下麵的代碼創建了GetPersonEducationInfo和GetPersonFamilyInfo兩個新類型,這樣我們就可以在下麵的方法中使用更加有語義的數據類型。

  // Person.h   #import // Define a new type for the block   typedef NSString * (^GetPersonEducationInfo)(NSString *);   typedef NSString * (^GetPersonFamilyInfo)(NSString *);   @interface Person : NSObject   - (NSString *)getPersonInfoWithEducation:(GetPersonEducationInfo)educationInfo       andFamily:(GetPersonFamilyInfo)familyInfo;   @end

我們用一張大師文章里的圖來總結一下block的結構:

block.png

2.2 將block作為參數傳遞

  // .h   -(void) testBlock:( NSString * ( ^ )( int ) )myBlock;   // .m   -(void) testBlock:( NSString * ( ^ )( int ) )myBlock   {       NSLog(@"Block returned: %@", myBlock(7) );   }

由於Objective-C是強制類型語言,所以作為函數參數的block也必須要指定返回值的類型,以及相關參數類型。

2.3 閉包性

上文說過,block實際是Objc對閉包的實現。

我們來看看下麵代碼:

  #import void logBlock( int ( ^ theBlock )( void ) )   {       NSLog( @"Closure var X: %i", theBlock() );   }   int main( void )   {       NSAutoreleasePool * pool;       int ( ^ myBlock )( void );       int x;       pool = [ [ NSAutoreleasePool alloc ] init ];       x = 42;       myBlock = ^( void )       {           return x;       };       logBlock( myBlock );       [ pool release ];         return EXIT_SUCCESS;   }

上面的代碼在main函數中聲明瞭一個整型,並賦值42,另外還聲明瞭一個block,該block會將42返回。然後將block傳遞給logBlock函數,該函數會顯示出返回的值42。即使是在函數logBlock中執行block,而block又聲明在main函數中,但是block仍然可以訪問到x變數,並將這個值返回。

註意:block同樣可以訪問全局變數,即使是static。

2.4 block中變數的複製與修改

對於block外的變數引用,block預設是將其複製到其數據結構中來實現訪問的.

通過block進行閉包的變數是const的。也就是說不能在block中直接修改這些變數。來看看當block試著增加x的值時,會發生什麼:

  myBlock = ^( void )   {       x++;       return x;   };

編譯器會報錯,表明在block中變數x是只讀的。

有時候確實需要在block中處理變數,怎麼辦?彆著急,我們可以用__block關鍵字來聲明變數,這樣就可以在block中修改變數了。

基於之前的代碼,給x變數添加__block關鍵字,如下:

  __block int x;

對於用__block修飾的外部變數引用,block是複製其引用地址來實現訪問的.

3.編譯器中的block

3.1 block的數據結構定義

block-struct.jpg

上圖這個結構是在棧中的結構,我們來看看對應的結構體定義:

  struct Block_descriptor {       unsigned long int reserved;       unsigned long int size;       void (*copy)(void *dst, void *src);       void (*dispose)(void *);   };   struct Block_layout {       void *isa;       int flags;       int reserved;       void (*invoke)(void *, ...);       struct Block_descriptor *descriptor;       /* Imported variables. */     };

從上面代碼看出,Block_layout就是對block結構體的定義:

isa指針:指向表明該block類型的類。

flags:按bit位表示一些block的附加信息,比如判斷block類型、判斷block引用計數、判斷block是否需要執行輔助函數等。

reserved:保留變數,我的理解是表示block內部的變數數。

invoke:函數指針,指向具體的block實現的函數調用地址。

descriptor:block的附加描述信息,比如保留變數數、block的大小、進行copy或dispose的輔助函數指針。

variables:因為block有閉包性,所以可以訪問block外部的局部變數。這些variables就是複製到結構體中的外部局部變數或變數的地址。

3.2 block的類型

block有幾種不同的類型,每種類型都有對應的類,上述中isa指針就是指向這個類。這裡列出常見的三種類型:

_NSConcreteGlobalBlock:全局的靜態block,不會訪問任何外部變數,不會涉及到任何拷貝,比如一個空的block。例如:

  #include int main()   {       ^{ printf("Hello, World!\n"); } ();       return 0;   }

_NSConcreteStackBlock:保存在棧中的block,當函數返回時被銷毀。例如:

  #include int main()   {       char a = 'A';       ^{ printf("%c\n",a); } ();       return 0;   }

_NSConcreteMallocBlock:保存在堆中的block,當引用計數為0時被銷毀。該類型的block都是由_NSConcreteStackBlock類型的block從棧中複製到堆中形成的。例如下麵代碼中,在exampleB_addBlockToArray方法中的block還是_NSConcreteStackBlock類型的,在exampleB方法中就被覆制到了堆中,成為_NSConcreteMallocBlock類型的block:

  void exampleB_addBlockToArray(NSMutableArray *array) {       char b = 'B';         [array addObject:^{               printf("%c\n", b);         }];     }   void exampleB() {       NSMutableArray *array = [NSMutableArray array];       exampleB_addBlockToArray(array);       void (^block)() = [array objectAtIndex:0];       block();   }

總結:

_NSConcreteGlobalBlock類型的block要麼是空block,要麼是不訪問任何外部變數的block。它既不在棧中,也不在堆中,我理解為它可能在記憶體的全局區。

_NSConcreteStackBlock類型的block有閉包行為,也就是有訪問外部變數,並且該block只且只有有一次執行,因為棧中的空間是可重覆使用的,所以當棧中的block執行一次之後就被清除出棧了,所以無法多次使用。

_NSConcreteMallocBlock類型的block有閉包行為,並且該block需要被多次執行。當需要多次執行時,就會把該block從棧中複製到堆中,供以多次執行。

3.3 編譯器如何編譯

  #import typedef void(^BlockA)(void);   __attribute__((noinline))     void runBlockA(BlockA block) {       block();   }   void doBlockA() {       BlockA block = ^{           // Empty block       };       runBlockA(block);   }

上面的代碼定義了一個名為BlockA的block類型,該block在函數doBlockA中實現,並將其作為函數runBlockA的參數,最後在函數doBlockA中調用函數runBloackA。

註意:如果block的創建和調用都在一個函數裡面,那麼優化器(optimiser)可能會對代碼做優化處理,從而導致我們看不到編譯器中的一些操作,所以用__attribute__((noinline))給函數runBlockA添加noinline,這樣優化器就不會在doBlockA函數中對runBlockA的調用做內聯優化處理。

我們來看看編譯器做的工作內容:

  #import __attribute__((noinline))   void runBlockA(struct Block_layout *block) {       block->invoke();   }     void block_invoke(struct Block_layout *block) {       // Empty block function   }   void doBlockA() {       struct Block_descriptor descriptor;       descriptor->reserved = 0;       descriptor->size = 20;       descriptor->copy = NULL;       descriptor->dispose = NULL;       struct Block_layout block;       block->isa = _NSConcreteGlobalBlock;       block->flags = 12345678;       block->reserved = 0;       block->invoke = block_invoke;       block->descriptor = descriptor;       runBlockA(&block);   }

上面的代碼結合block的數據結構定義,我們能很容易得理解編譯器內部對block的工作內容。

3.4 copy()和dispose()

上文中提到,如果我們想要在以後繼續使用某個block,就必須要對該block進行拷貝操作,即從棧空間複製到堆空間。所以拷貝操作就需要調用Block_copy()函數,block的descriptor中有一個copy()輔助函數,該函數在Block_copy()中執行,用於當block需要拷貝對象的時候,拷貝輔助函數會retain住已經拷貝的對象。

既然有有copy那麼就應該有release,與Block_copy()對應的函數是Block_release(),它的作用不言而喻,就是釋放我們不需要再使用的block,block的descriptor中有一個dispose()輔助函數,該函數在Block_release()中執行,負責做和copy()輔助函數相反的操作,例如釋放掉所有在block中拷貝的變數等。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • CocoaPods是什麼? 當你開發iOS應用時,會經常使用到很多第三方開源類庫,比如JSONKit,AFNetWorking等等。可能某個類庫又用到其他類庫,所以要使用它,必須得另外下載其他類庫,而其他類庫又用到其他類庫,“子子孫孫無窮盡也”,這也許是比較特殊的情況。總之小編的意思就是,手動一個個 ...
  • 多線程下載就是將同一個網路上的原始文件根據線程個數分成均等份,然後每個單獨的線程下載對應的一部分 ...
  • 概覽屏幕 概覽屏幕 概覽屏幕(也稱為最新動態屏幕、最近任務列表或最近使用的應用)是一個系統級別 UI,其中列出了最近訪問過的 Activity 和任務。 用戶可以瀏覽該列表並選擇要恢復的任務,也可以通過滑動清除任務將其從列表中移除。 對於 Android 5.0 版本(API 級別 21),包含不同 ...
  • 廣度優先搜索 在給定圖G=(V,E)和一個特定的源頂點s的情況下,廣度優先搜索系統地探索G中的邊,以期“發現”可從s 到達的所有頂點,並計算s 到所有這些可達頂點之間的距離(即最少的邊數)。該搜索演算法同時還能生成一棵根為s、且包括所有s 的可達頂點的廣度優先樹。對從s 可達的任意頂點v,廣度優先樹中 ...
  • 一、SQLite保存數據介紹 將資料庫保存在資料庫對於重覆或者結構化數據(比如契約信息)而言是理想之選。SQL資料庫的主要原則之一是架構:資料庫如何組織正式聲明。架構體現於用於創建資料庫的SQL語句。它有助於創建伴隨類,即契約類,其以一種系統性、自記錄的方式明確指定架構佈局。 契約類是用於定義URL ...
  • res/layout中的佈局文件太雜,沒有層次感,受不了的我治好想辦法解決這個問題。 前幾天看博客說可以使用插件分組,可惜我沒找到。知道看到另一篇博客時,才知道這個方法不能用了。 不能用插件,那就手動來吧。(http://blog.csdn.net/u011156012/article/detail ...
  • 使用Android Studio 一、在build.gradle(Module:app)添加代碼 下載,調用插件 1 apply plugin: 'com.android.application' 2 3 android { 4 compileSdkVersion 24 5 buildToolsVe ...
  • 自從Android6.0發佈以來,在許可權上做出了很大的變動,不再是之前的只要在manifest設置就可以任意獲取許可權,而是更加的註重用戶的隱私和體驗,不會再強迫用戶因拒絕不該擁有的許可權而導致的無法安裝的事情,也不會再不征求用戶授權的情況下,就可以任意的訪問用戶隱私,而且即使在授權之後也可以及時的更改 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...