Block解析(iOS)

1. 操做系統中的棧和堆程序員

咱們先來看看一個由C/C++/OBJC編譯的程序佔用內存分佈的結構: 數組

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

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

堆區(heap):通常由程序員申請並指明大小,最終也由程序員釋放。若是程序員不釋放,程序結束時可能會由OS回收。對於堆區的管理是採用鏈表式管理的,操做系統有一個記錄空閒內存地址的鏈表,當接收到程序分配內存的申請時,操做系統就會遍歷該鏈表,遍歷到一個記錄的內存地址大於申請內存的鏈表節點,並將該節點從該鏈表中刪除,而後將該節點記錄的內存地址分配給程序。函數

例如:在C中malloc函數 優化

   char p;spa

   p = (char)malloc(10);操作系統

可是p自己是在棧中的。指針

鏈表:是一種常見的基礎數據結構,通常分爲單向鏈表、雙向鏈表、循環鏈表。如下爲單向鏈表的結構圖:code

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標籤的結構體,另外聲明瞭變量t一、t二、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中拷貝的變量等。

相關文章
相關標籤/搜索