1. 程式人生 > >IOS 程式碼塊之block的宣告、建立、傳參的基本使用

IOS 程式碼塊之block的宣告、建立、傳參的基本使用

Block 是iOS在4.0之後新增的程式語法,在iOS SDK 4.0之後,block應用幾乎無處不在。

在其他語言中也有類似的概念稱做閉包(closure),比如object C的好兄弟Swift 中閉包(swift 閉包詳解)的使用跟 OC的block一樣重要。總的來說:

Block是C語言的 
Block是一個數據型別 
Block 是一個提前準備好的程式碼,在需要的時候執行

1. block作用:

Block用來封裝一段程式碼,可以在任何時候執行;

  • Block可以作為函式引數或者函式的返回值,而其本身又可以帶輸入引數或返回值。
  • 蘋果官方建議儘量多用block。在多執行緒、非同步任務 、集合遍歷、集合排序、動畫轉場用的很多

在新的iOS API中block被大量用來取代傳統的delegate和callback,而新的API會大量使用block主要是基於以下兩個原因:

A. 可以直接在block程式碼塊中寫等會要接著執行的程式碼,直接把block變成函式的引數傳入函式中,這是新API最常使用block的地方。

B. 可以存取區域性變數,在傳統的callback操作時,若想要存取區域性變數得將變數封裝成結構體才能使用,而block則是可以很方便地直接存取區域性變數。

2. Block的定義:

定義時,把block當成資料型別

特點: 
1. 型別比函式定義多了一個 ^ 
2. 設定數值,有一個 ^,內容是 {} 括起的一段程式碼

(1)基本定義方式

/*
*1.最簡單的定義方式:
*格式:void (^myBlock)() = ^ { // 程式碼實現; }
*/
void (^myBlock)() = ^ {
    NSLog(@"hello");
};

// 執行時,把block當成函式
myBlock();

/*
*2.定義帶引數的block:
*格式:void (^block名稱)(引數列表) = ^ (引數列表) { // 程式碼實現; }
*/
void (^sumBlock)(int, int) = ^ (int x, int y) {
    NSLog(@"%d", x + y);
};

sumBlock(10
, 20); /* *3.定義帶返回值的block *格式:返回型別 (^block名稱)(引數列表) = ^ 返回型別 (引數列表) { // 程式碼實現; } */ int (^sumBlock2)(int, int) = ^ int (int a, int b) { return a + b; }; NSLog(@"%d", sumBlock2(4, 8));
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

(2) block 指標

Block Pointer是這樣定義的:

回傳值 (^名字) (引數列);

//宣告一個名字為square的Block Pointer,其所指向的Block有一個int輸入和int輸出  
int (^square)(int);  

//block 指標square的內容
square = ^(int a){ return a*a ; };  

//呼叫方法,感覺是是不是很像function的用法?  
int result = square(5);  
NSLog(@"%d", result);  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

(3) 用typedef先宣告型別,再定義變數進行賦值

typedef int (^MySum)(int,int);
MySum sum = ^(int a,int b)
 { 
      return a + b;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

(4) block 訪問外部變數

但是block使用有個特點,Block可以訪問區域性變數,但是不能修改:

int sum = 10;

int (^MyBlock)(int) = ^(int num) 
{ 
     sum++;//編譯報錯 
     return num * sum; 
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

如果要修改就要加關鍵字 __block (下面詳細說明):

__block int sum =10;

(5) block 與函式指標

下面比較下函式指標與block異同:

  • 定義函式指標 int (*myFn)(); 
    呼叫函式指標 (*myFn)(10, 20);

  • 定義Block int (^MyBlocks)(int,int); 
    呼叫Blocks MyBlocks(10, 20);

3. block訪問外部變數

block 訪問外部變數有幾個特點必須知道:

  • block內部可以訪問外部變數;
  • 預設情況下block內部不能修改外面的區域性變數;
  • 給區域性變數加上關鍵字_block,這個區域性變數就可以在block內部修改;

block中可以訪問外部變數。但是不能修改它,否則編譯錯誤。但是可以改變全域性變數、靜態變數(static)、全域性靜態變數。

上面的特點是有原因滴:

A. 為何不讓修改變數:這個是編譯器決定的。理論上當然可以修改變量了,只不過block捕獲的是外部變數的副本,名字一樣。為了不給開發者迷惑,乾脆不讓賦值。道理有點像:函式引數,要用指標,不然傳遞的是副本(大家想起那個經典的兩個數調換值的問題了吧)。

B. 可以修改靜態變數的值。靜態變數屬於類的,不是某一個變數。所以block內部不用呼叫cself指標。所以block可以呼叫。

(1) __block儲存型別

通過__block儲存型別修飾符, 變數在block中可被修改。__block儲存跟register、auto和static儲存型別相似(但是之間互斥),用於區域性變數。__block變數儲存在堆區,因此,這個block使用的外部變數,將會在棧結束被留下來。

從優化角度考慮,block儲存在棧上,如果block被拷貝(通過Block_copy或者copy),變數被拷貝到堆。因此__block變數的地址就會改變。

__block變數還有兩個限制,他們不能是可變陣列(NSMutableArray),不能是結構體(structure)。

__block 變數的內部實現要複雜許多,__block 變數其實是一個結構體物件,拷貝的是指向該結構體物件的指標

(2) block訪問外部變數

上面已經說過,預設block 訪問的外部變數是隻讀屬性的,若要對外部變數進行讀寫,需要在定義外部變數時加一個 __block, 示例如下:

//示例1:block訪問外部變數
void demoBlock1() 
{
    int x = 10;
    NSLog(@"定義前 %p", &x);// 區域性變數在棧區

    // 在定義block的時候,如果引用了外部變數,預設是把外部變數當做是常量編碼到block當中,並且把外部變數copy到堆中,外部變數值為定義block時變數的數值
    // 如果後續再修改x的值,預設不會影響block內部的數值變化!
    // 在預設情況下,不允許block內部修改外部變數的數值!因為會破壞程式碼的可讀性,不易於維護!
    void(^myBlock)() = ^ {

        NSLog(@"%d", x);
        NSLog(@"in block %p", &x); // 堆中的地址
    };
    //輸出是10,因為block copy了一份x到堆中

    NSLog(@"定義後 %p", &x);  // 棧區
    x = 20;

    myBlock();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
//示例2:在block中修改外部變數
void demoBlock2() 
{
    // 使用 __block,說明不在關心x數值的具體變化
    __block int x = 10;
    NSLog(@"定義前 %p", &x);                 // 棧區

    // !定義block時,如果引用了外部使用__block的變數,在block定義之後, block外部的x和block內部的x指向了同一個值,記憶體地址相同
    void (^myBlock)() = ^ {
        x = 80;
        NSLog(@"in block %p", &x);          // 堆區
    };
    NSLog(@"定義後 %p", &x);                 // 堆區

    myBlock();
    NSLog(@"%d", x);
    //列印x的值為8,且地址在堆區中
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

下面的例子就有點難度了,讓我們看下block對指標變數的訪問

//例子3:block對指標變數的訪問
void demoBlock3() 
{    
    // !指標記錄的是地址
    NSMutableString *strM = [NSMutableString stringWithString:@"zhangsan"];
    //strM是指標,其在堆中儲存的是zhangsan這個string在記憶體中的的地址值
    //&strM是指標strM在堆中的地址
    NSLog(@"定義前 %p %p", strM, &strM); 

    void (^myBlock)() = ^ {
        /*首先呼叫block會對strM(指標)進行一份copy,這份copy會在堆中建立
        另一個指標,這個指標儲存的值同strM,都是zhangsan的地址,
        即新copy的指標指向的內容沒有變
        */

        // 注意下面的操作是修改strM指標指向的內容
        [strM setString:@"lisi"];
        NSLog(@"inblock %p %p", strM, &strM);
        //輸出:strM沒有變,因為儲存的都是zhangsan的地址,&strM為堆中新地址

        /*
        *這句程式碼是修改指標strM,因為strM copy過來後是隻讀的,所以同例子2編譯會報錯,需要在定義strM時加__block
        strM = [NSMutableString stringWithString:@"wangwu"];
        NSLog(@"inblock %p %p", strM, &strM);
        */
    };

    //大家想想使用__block輸出會是什麼呢
    NSLog(@"定義後 %p %p", strM, &strM);

    myBlock();
    NSLog(@"%@", strM);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

上面的例子搞定了,來讓我們看下各種型別的變數與block之間的互動:

//示例4:各種型別的變數和block之間的互動
  extern NSInteger CounterGlobal;
  static NSInteger CounterStatic;

  NSInteger localCounter = 42 ;
  __block char localCharacter;
  void (^aBlock)( void ) = ^( void )
  {
      ++ CounterGlobal ; //可以存取。
      ++ CounterStatic ; //可以存取。 

      CounterGlobal = localCounter; //localCounter在block 建立時就不可變了。
      localCharacter = 'a' ; //設定外面定義的localCharacter 變數。
  };

  ++localCounter; //不會影響的block 中的值。
  localCharacter = 'b' ;
  aBlock(); //執行block 的內容。

  //執行完後,localCharachter 會變成'a'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

(3) block 引用成員變數

OC物件,不同於基本型別,Block會引起物件的引用計數變化。若我們在block中引用到oc的物件,則物件的引用計數器會加1, 不過在物件前 加__block修飾,則參考計數不變。

- 若直接存取例項變數(instance variable),self的參考計數將被加1。
- 若透過變數存取例項變數的值,則變數的參考計數將被加1。
- 在物件前加 __block 則參考計數不會自動加1。
//例子1:定義一個變數間接給block呼叫,成員變數引用計數不變
dispatch_async (queue, ^{
   // 因為直接存取例項變數instanceVariable ,所以self 的retain count 會加1
   doSomethingWithObject (instanceVariable);
});

//通過
id localVaribale = instanceVariable;
dispatch_async (queue, ^{
   //localVariable 是存取值,所以這時只有localVariable 的retain count 加1
   //self 的 return count  並不會增加。
  doSomethingWithObject (localVaribale);
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

上面只是簡單演示下block引用成員變數,下面我們研究下block引用成員變數時出現的一個經典問題:迴圈引用

在block內部使用成員變數,如下:

@interface ViewController : UIViewController 
{ 
    NSString *_string; 
} 
@end