本章教程主要對代碼塊回調模式進行講解,已經分析其他回調的各種優缺點和適合的使用場景。
蘋果公司在iOS4 SDK中首次支持代碼塊機制,隨後代碼塊機制被廣泛應用於各種編碼場景,最常見的為回調機制,也成為Block回調。
代碼塊也稱Block。是封裝代碼的一種機制,也可以稱為匿名函數。
使用這種機制可以將一段代碼放入一個Block變量中進行存儲,該變量可以作為參數進行傳遞,也可以通過該變量調用其存儲的代碼。
在OC語法中,創建一個變量首先要明確其類型。Block作為一個可以儲存代碼的變量,其類型相對特殊。
確定block變量的類型有兩個因素:
只要這兩個因素一樣,我們就可以說是相同的block類型。
現在我們舉一個簡單的例子,創建一個儲存沒有返回值,沒有輸入參數的代碼的block類型。
void (^ varBlock)(void);
上面的代碼聲明了一個block變量,變量名為varBlock
,其儲存代碼類型為沒有返回值,沒有輸入參數。
如果想要儲存有返回值,有輸入參數的代碼,同樣可以聲明響應的block變量進行使用。
int (^ varBlock1)(int a,int b);
上面的代碼聲明了一個block變量,變量名為varBlock1
,其儲存代碼類型為int型返回值,有兩個int型參數。
Block變量類型較為復雜,如果直接用這種方式進行聲明變量十分容易儲存。通常我們用typedef
關鍵字將Block類型重命名,然後用相對簡單的類型名進行聲明變量的工作。
typedef void (^ BlockType1)(void);
BlockType1 var1;//var1與varBlock1為同一類型
有了Block變量,下面我們就要給變量賦值。
typedef void (^ BlockType1)(void);
BlockType1 var1;
var1 = ^(){NSLog(@"test")};
通過上述語法格式將代碼封裝在大括號內,並用var1
變量進行儲存。封裝代碼的過程中要注意一下幾點:
^
符號開始為Block封裝過程。^
後面的小括號中寫這段代碼需要的參數。該參數有調用者進行賦值。下面舉一個求兩個數的和的代碼封裝過程。
typedef int (^BlockType)(int a,int b);
BlockType varBlock;
varBlock = ^(int a,int b){return a+b;};
將代碼存入varBlock
變量中後,便可以使用該變量調用代碼。
int a = 4;
int b = 6;
int sum = varBlock(a,b);
NSLog(@"sum = %d",sum);//輸出結果為10
Block變量也可以給同類型的變量賦值
BlockType varBlockTemp;
varBlockTemp = varBlock;
int sum = varBlockTemp(1,2);
NSLog(@"sum = %d",sum);//輸出結果為3
將一段代碼當做一個變量進行傳遞,Block這樣的特性極大的方便了我們之後的編碼工作
通過Block對象將代碼進行封裝的同時,有一個非常關鍵的問題我們需要明確討論,即Block變量對普通變量作用域的影響。
通過一個簡單案例來因此這個問題。見如下代碼:
main()
{
{
int a = 1;
{
a = 2;
}
//此處輸出a的值為2
}
//此處已經超出變量a的作用域,討論a的值無意義。
}
這段代碼很簡單,通過大擴展來表示變量的作用域范圍。再看下面代碼:
typedef void (^ BlockType)(void);
BlockType var;
void fun1()
{
int a = 10;
var = ^(){NSLog(@"a = %d",a)};
}
void fun2()
{
var();
}
main()
{
fun1();
fun2();
}
在fun2函數中調用var
變量時,運行的是fun1中存入var
變量的代碼,且代碼中的使用的變量a
也是fun1中的局部變量。
正常狀態下,變量a
的作用域在fun1函數體大括號內。在函數體大括號外面使用a
是沒有意義的。
但此處情況特殊,變量a
被block變量var
所使用,所以var
變量將a進行了一個復制操作,也就是我們在var的代碼裡面使用的a其實是a的副本。
我們看下面的代碼:
typedef void (^ BlockType)(void);
BlockType var;
void fun1()
{
int a = 10;
var = ^(){NSLog(@"a = %d",a)};
a = 20;
}
void fun2()
{
var();
}
main()
{
fun1();
fun2();
}
這段代碼的輸出和上一段代碼一樣,不會因為fun1函數中a的值發生變化而導致block裡面的a的值發生變化。原因是Block變量在使用局部變量是,會對局部變量進行一個復制操作,block變量中儲存的代碼使用的時局部變量的副本。
但是在某些特殊場合,我們需要改變局部變量可以引起block變量中代碼的變化。這時候我們需要使用block全景變量。
block全景變量通過:__block
來聲明。
typedef void (^ BlockType)(void);
BlockType var;
void fun1()
{
__block int a = 10;
var = ^(){NSLog(@"a = %d",a)};
a = 20;
}
void fun2()
{
var();
}
main()
{
fun1();
fun2();
}
上文代碼中,fun1中的變量a被block全景變量標識符所修飾,即變量a成為一個block全景變量。
其作用是,此時block封裝代碼時使用a變量,不會進行復制操作,也就表示,局部變量a與block代碼中的a為同一個變量。所以,當前代碼的運行結果為 a = 20。
回調的本質為控件反饋自身信息,在面向對象編程中,控件需要調用方法反饋自身信息,而方法必須從屬某個對象。所以之前的回調接口必須設置兩個參數,一個反饋對象,一個反饋方法。
在Block回調中,因Block機制可以直接將代碼封裝如一個變量中,而且這個變量可以當做參數進行傳遞。利用這個機制,組件可以保存這段代碼,在觸發事件的時候直接調用此段代碼,不需要設置反饋對象和反饋方法。
這裡仍然用之前的開關最為例子:
typedef enum : NSUInteger {
SwitchStateOff,//default
SwitchStateOn,
} SwitchState;
typedef void(^SBlockType)(SwitchState state);
@interface SwitchB : NSObject
@property(nonatomic,assign,readonly)SwitchState currentState;
@property(nonatomic,strong)SBlockType changeStateBlockHandle;
@end
聲明中的changeStateBlockHandle
屬性就是保存回調代碼。設置回調,只需要將此屬性賦值就可。
@interface Room : NSObject
@property (strong, nonatomic) Light *lightA;
@property (strong, nonatomic) SwitchB *s;
@end
@implementation Room
- (instancetype)init
{
self = [super init];
if (self) {
self.lightA = [[Light alloc] init];
self.s = [[SwitchB alloc] init];
__weak __block Room * copy_self = self;//打破強引用循環,後續章節會展開講解
self.s.changeStateBlockHandle = ^(SwitchState state)
{
if (state == SwitchStateOff)
{
[self.lightA turnOff];
}
else
{
[self.lightA turnOn];
}
};
}
return self;
}
@end
當開關的狀態發生改變時,開關需要將自身狀態反饋給使用者。當使用Block回調接口的組件時,需要將回調代碼直接封裝,賦值給組件響應的Block類型的屬性即可。當狀態改變時,封裝的代碼便被組件調用。