iOS底層原理探索-Block基本使用
block是什麼?
block其實是一段程式碼塊,其作用是儲存一段程式碼塊,在真正呼叫block的時候,才執行block裡面的程式碼。
在程式裡面輸入inlineBlock,就可以得到block的宣告與定義形式:
/** 等號前面是block的宣告; 等號後面是block的定義; returnType:block宣告的返回型別 blockName:block的名字 parameterTypes:block宣告的引數型別 parameters:block定義的引數型別及引數值 statements:block程式碼塊 */ returnType(^blockName)(parameterTypes) = ^(parameters) { statements };//末尾的;不能省略
block的定義常見的有四種形式:
- 無引數,無返回值
- 無引數,有返回值
- 有引數,無返回值
- 有引數,有返回值
我們具體看一下四種常見的形式以及各個形式的型別都長什麼樣:
- (void)viewDidLoad { [super viewDidLoad]; /** 1.無引數,無返回值 定義的時候,沒有返回值可以不寫()及裡面的內容 其block型別是:void(^)(void) */ void(^block1)(void) = ^{ NSLog(@"block1"); }; /** 2.1無引數,有返回值 其block型別是:int(^)(void) */ int(^block21)(void) = ^int{ NSLog(@"block21"); return 21; }; /** 2.2無引數,有返回值 定義的時候,返回值可以省略 其block型別是:int(^)(void) */ int(^block22)(void) = ^{ NSLog(@"block22"); return 22; }; /** 3.有引數,無返回值 有引數的情況下,宣告的引數型別必須寫 有引數的情況下,定義的引數型別和引數名必須寫 其block型別是:void(^)(int) */ void(^block3)(int) = ^(int a){ NSLog(@"block3---%d", a); }; /** 4.有引數,有返回值 定義的時候,返回值int可以省略 其block型別是:int(^)(int) */ int(^block4)(int) = ^int(int a){ NSLog(@"block4---%d", a); return 4; }; /**block的呼叫*/ block1(); int a = block21(); NSLog(@"%d", a); int b = block22(); NSLog(@"%d", b); block3(3); NSLog(@"%d", block4(4)); } 執行結果: 2020-03-06 15:23:18.860990+0800 test001[3456:921319] block1 2020-03-06 15:23:18.861075+0800 test001[3456:921319] block21 2020-03-06 15:23:18.861100+0800 test001[3456:921319] 21 2020-03-06 15:23:18.861122+0800 test001[3456:921319] block22 2020-03-06 15:23:18.861141+0800 test001[3456:921319] 22 2020-03-06 15:23:18.861161+0800 test001[3456:921319] block3---3 2020-03-06 15:23:18.861190+0800 test001[3456:921319] block4---4 2020-03-06 15:23:18.861211+0800 test001[3456:921319] 4
通過以上程式碼,我們可以得出一下結論:
block在定義的時候,引數為空的時候,可以將定義裡面的()以及內容都省略
block在定義的時候,引數不為空的時候,引數值和引數名都不可以省略
block在定義的時候,返回值不管有或者沒有都可以省略
block的宣告
在interface裡面宣告block的時候,有兩種方法:
typedef void(^BLOCK2)(void); @interface ViewController () /**在ARC下,strong和copy修飾block都可以*/ /**直接宣告,按照block的宣告樣式寫就可以*/ @property (strong, nonatomic) void(^block1)(void); /**將BLOCK2進行定義型別,然後在定義變數block2*/ @property (strong, nonatomic) BLOCK2 block2; @end
block使用場景-反向傳值
我們知道,兩個物件的傳值有兩種:正向傳值和反向傳值。
其中,正向傳值可以直接將值賦值過去完成傳值動作。
而反向傳值,一般有三種方法:代理、block和通知,然後,我們介紹一下block的反向傳值。
/**
ViewController
*/
#import "ViewController.h"
#import "TestViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
TestViewController *testVc = [[TestViewController alloc] init];
testVc.myBlock = ^(int value) {
NSLog(@"value = %d", value);
};
[self presentViewController:testVc animated:YES completion:nil];
}
/**TestViewController*/
@interface TestViewController : UIViewController
@property (strong, nonatomic) void(^myBlock)(int value);
@end
#import "TestViewController.h"
@interface TestViewController ()
@end
@implementation TestViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor redColor];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
if (self.myBlock) {
self.myBlock(100);
}
}
@end
兩次點選後,列印如下:
2020-03-06 17:49:31.981064+0800 test001[3497:936661] value = 100
第一次點選螢幕,會撥出TestViewController,
第二次點選螢幕,會呼叫myBlock(100),其實它是做了如下操作:
在TestViewController中myBlock(100);的呼叫等價於:
if (self.myBlock) {
value = 100;
^(int value) {
NSLog(@"value = %d", value);
}();
}
block在MRC下的記憶體儲存地址
首先我們要明確一點的是,block其實是一個物件,那麼,block這個物件,儲存在什麼區呢?
我們知道,記憶體分為五大區:
棧stack(系統) | 堆malloc、heap(手動) | 靜態區(全域性區) | 常量區 | 方法區(程式程式碼區)
block儲存在哪個區呢?
這個,需要根據專案是MRC或者ARC做具體的判斷。
在講MRC或者ARC前,我們先了解一些基本知識點:
ARC管理原則:
只要一個物件沒有被強指標引用,該物件就會被銷燬。
預設區域性變數物件都是使用強指標引用,並存放在堆裡面。
MRC管理原則:
MRC沒有strong、weak修飾指標,區域性變數物件做基本資料型別處理,基本資料型別統一放在棧區。
MRC常用知識點:
MRC給屬性賦值的時候,一定要用set方法,不能直接訪問下劃線成員屬性。因為,在MRC下的set方法會做一些其他事情,而直接用_成員屬性就不會做這些事情。
在MRC下,@property (retain, nonatomic) NSString *name;該句程式碼的set方法的實現轉換為下面程式碼:
- (void)setName:(NSString *)name
{
if (_name != name) {
[_name release];
_name = [name retain];
}
}
首先,我們看下在MRC環境下block的儲存位置
將Build Setting下的Objective-C Automatic Reference Counting設定為NO即是在MRC環境下
void(^block)(void) = ^{
NSLog(@"呼叫了block");
};
NSLog(@"%@", block);
結果:<__NSGlobalBlock__: 0x1006a0080>
int a = 3;//區域性變數
void(^block)(void) = ^{
NSLog(@"呼叫了block, a = %d", a);
};
NSLog(@"%@", block);
結果:<__NSStackBlock__: 0x16f971328>
__block int a = 3;
void(^block)(void) = ^{
NSLog(@"呼叫了block, a = %d", a);
};
NSLog(@"%@", block);
結果:<__NSStackBlock__: 0x16f935310>
static int a = 3;//static修飾區域性變數
void(^block)(void) = ^{
NSLog(@"呼叫了block, a = %d", a);
};
NSLog(@"%@", block);
結果:<__NSGlobalBlock__: 0x1008ec080>
int global = 5;
- (void)viewDidLoad {
[super viewDidLoad];
void(^block)(void) = ^{
NSLog(@"呼叫了block, global = %d", global);
};
NSLog(@"%@", block);
}
結果:<__NSGlobalBlock__: 0x100934080>
static int global = 5;
- (void)viewDidLoad {
[super viewDidLoad];
void(^block)(void) = ^{
NSLog(@"呼叫了block, global = %d", global);
};
NSLog(@"%@", block);
}
結果:<__NSGlobalBlock__: 0x100be8080>
__block int global = 5;//__block修飾全域性變數,報錯
通過程式碼可以看出,如果使用retain修飾block,則self.block存放在棧區。棧是由系統自動控制,則在程式碼塊{}執行完畢後,self.block將被回收,而在touchesBegan方法中還呼叫self.block,報野指標錯誤。
然後,我們看下使用copy修飾block:
@property (copy, nonatomic) void(^block)(void);
結果:<__NSMallocBlock__: 0x28237b2d0>
觸控點選列印:呼叫了block, a = 3
(其他程式碼與上面相同)
這是因為,使用copy修飾block,self.block儲存在堆區,而堆記憶體區域是由程式設計師自己控制的,因此,在viewDidLoad方法執行完畢後,self.block的記憶體地址並沒有被回收,因此在touchesBegan方法中呼叫self.block();沒有問題。
總結:
在MRC下
block屬性必須使用copy,而不能使用retain修飾
void(^block)(void) = ^{
NSLog(@"呼叫了block");
};
NSLog(@"%@", block);
結果:<__NSGlobalBlock__: 0x100b5c098>
int a = 3;//區域性變數
void(^block)(void) = ^{
NSLog(@"呼叫了block, a = %d", a);
};
NSLog(@"%@", block);
結果:<__NSMallocBlock__: 0x28343ea60>
__block int a = 3;
void(^block)(void) = ^{
NSLog(@"呼叫了block, a = %d", a);
};
NSLog(@"%@", block);
結果:<__NSMallocBlock__: 0x2805b11d0>
static int a = 3;//static修飾區域性變數
void(^block)(void) = ^{
NSLog(@"呼叫了block, a = %d", a);
};
NSLog(@"%@", block);
結果:<__NSGlobalBlock__: 0x100f38098>
int global = 5;
- (void)viewDidLoad {
[super viewDidLoad];
void(^block)(void) = ^{
NSLog(@"呼叫了block, global = %d", global);
};
NSLog(@"%@", block);
}
結果:<__NSGlobalBlock__: 0x100564098>
static int global = 5;
- (void)viewDidLoad {
[super viewDidLoad];
void(^block)(void) = ^{
NSLog(@"呼叫了block, global = %d", global);
};
NSLog(@"%@", block);
}
結果:<__NSGlobalBlock__: 0x100b54098>
__block int global = 5;//__block修飾全域性變數,報錯
總結:
在ARC下
block本身是儲存在全域性區;
如果block引用了外部區域性變數,或者引用了被__block修飾的外部區域性變數,則存放在堆區。
被__block修飾的block還是區域性變數;
被static修飾的區域性變數,改變區域性變數的宣告週期。
在ARC中使用copy修飾block
@interface ViewController ()
@property (copy, nonatomic) void(^block)(void);
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
int a = 3;
void(^block)(void) = ^{
NSLog(@"呼叫了block, a = %d", a);
};
self.block = block;
NSLog(@"%@", self.block);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.block();
}
結果:<__NSMallocBlock__: 0x28065c0f0>
觸控點選列印:呼叫了block, a = 3
在ARC中使用strong修飾block
@property (strong, nonatomic) void(^block)(void);
結果:<__NSMallocBlock__: 0x2802b5ef0>
觸控點選列印:呼叫了block, a = 3
在ARC中使用weak修飾block
@property (weak, nonatomic) void(^block)(void);
結果: <__NSMallocBlock__: 0x282cdb3c0>
觸控點選崩潰,報EXC_BAD_INSTRUCTION錯誤
在ARC中使用assign修飾block
在ARC下
block屬性可以使用copy或者strong修飾,不能使用weak或者assign修飾
通過ARC管理機制我們知道,如果沒有任何強指標引用,則物件會被清空。所以,用weak或者assign沒有對block進行強指標引用,因此,在viewDidLoad方法執行完畢後,block就被清空,再次使用self.block會造成野指標錯誤。
在ARC下,string和block用copy還是strong?
其實兩個都可以使用,但是還是建議使用strong。
string使用copy修飾,只是淺拷貝,並沒有建立新的物件,所以,strong就可以滿足。
block使用copy修飾,也沒有新的物件建立,所以,strong就可以滿足。
而,strong和copy不一樣的地方在於,使用copy修飾的屬性,在set方法中,會有一些列的copy操作,而strong並不需要,從效能上說,strong高一些。
Block的迴圈引用注意事項
block會對裡面所有的外部變數物件進行強引用。
int a = 3;//區域性變數
void(^block)(void) = ^{
NSLog(@"呼叫了block, a = %d", a);
};
a = 4;
block();
結果:呼叫了block, a = 3
static int a = 3;//static修飾區域性變數
結果:呼叫了block, a = 4
__block int a = 3;//__block修飾區域性變數
結果:呼叫了block, a = 4
int a = 3;//全域性變數
結果:呼叫了block, a = 4
static int a = 3;//static修飾全域性變數
結果:呼叫了block, a = 4
總結
block呼叫區域性變數是值傳遞;
使用static或者__block修飾的區域性變數是指標傳遞;
全域性變數和使用static修飾的全域性變數,block沒有捕獲全域性變數,因此,在block內部可以修改全域性變數
上面的總結第三點其實是不對的,為什麼呢?
為什麼是這樣的結果呢???
想了解更多的小夥伴,可以看這兩篇文章,為你答疑解惑。
iOS之Block本質(一)
iOS之Block本質(二)
block作為引數
block作為引數使用,在UIView的動畫裡面很常見,例如:
[UIView animateWithDuration:3.0 animations:^{
}];
其方法是:
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations
(void (^)(void))是一個引數為void,返回值為void的block型別,animations是block變數名。
可以看出,block作為引數時,使用 (block型別)block變數名 的形式定義。
我們什麼時候讓block作為引數使用呢?
這個可以想下,我們什麼時候使用動畫。
在3.0內,執行animations裡面的內容,什麼時候執行這個方法,是由UIView呼叫animateWithDuration方法決定的,至於裡面要執行的什麼動畫,則是一個block,是一個程式碼塊,是可以由程式設計師自己定義自己寫的。
換言之,block程式碼塊裡面的內容是程式設計師儲存的一端程式碼,寫完並沒有立馬執行。什麼時候執行呢?是由UIView呼叫animateWithDuration方法執行的。
舉一個例子:
@interface Calculator : NSObject
@property (assign, nonatomic) int result;
//定義一個方法,引數型別是int(^)(int),引數名是block
- (void)jisuan:(int(^)(int))block;
@end
@implementation Calculator
//方法的實現
- (void)jisuan:(int(^)(int result))block
{
if (block) {
//呼叫block,並將block呼叫的結果賦值給result
_result = block(_result);
}
}
@end
Calculator *calculator = [[Calculator alloc] init];
[calculator jisuan:^int(int result) {//block的引數名不可省略,block的返回值型別可以省略(第一個int)
result += 5;
result *= 2;
return result;
}];
NSLog(@"%d", calculator.result);
結果:10
這個例子中,jisuan:方法裡面的引數是一個block,當呼叫這個方法的時候,呼叫的block,執行block裡面的程式碼。
block作為返回值
block作為返回值的經典代表就是Masonry三方框架,基本上整個框架都是使用的Block作為返回值。
拿一個簡單的Masonry佈局程式碼進行分析:
[thirdView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(bottomView);
make.top.equalTo(_secondView.mas_bottom);
make.height.equalTo(@70);
}];
make.left.right.equalTo(bottomView);
程式碼中,從前往後執行,首先是make.left,該語法是一個get方法,自動尋找是否有left方法,並返回物件本身(型別MASConstraintMaker),make.left.right以及make.left.right.equalTo都是返回物件本身(型別MASConstraintMaker),則最後是(MASConstraintMaker型別)(bottomView)。其實iMASConstraintMaker型別是一個block型別,最後的呼叫是對該block進行了呼叫,引數是bottomView。
來個簡單的block作為返回值的例子:
- (void)viewDidLoad {
[super viewDidLoad];
self.test();
}
- (void(^)(void))test{
//方法一:定義一個變數名為block的blocke,並返回
void(^block)(void) = ^{
NSLog(@"呼叫test函式返回block");
};
return block;
//方法二:直接返回一個block
return ^{
NSLog(@"呼叫test函式返回block");
};
}
結果:呼叫test函式返回block
接下來,我們做一個簡單的連續進行加法計算的例子
@interface Calculator : NSObject
@property (assign, nonatomic) int result;
//定義一個方法,返回值是其本身
- (Calculator *)jisuanAdd:(int)value;
@end
@implementation Calculator
//方法的實現
- (Calculator *)jisuanAdd:(int)value
{
self.result += value;
return self;
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
Calculator *calculator = [[Calculator alloc] init];
[[[calculator jisuanAdd:2] jisuanAdd:5] jisuanAdd:3];
NSLog(@"%d", calculator.result);
}
結果:10
通過上面的例子,我們可以實現連續進行加法的計算,我們對例子進行改進,使其可以跟Masonry一樣,進行鏈式程式設計。
@interface Calculator : NSObject
@property (assign, nonatomic) int result;
//定義一個方法,返回值是一個block,該block是一個引數為int,返回值為Calculator的型別
- (Calculator *(^)(int))jisuanAdd;
@end
@implementation Calculator
//方法的實現
- (Calculator *(^)(int))jisuanAdd
{
//返回一個block
return ^(int value){
self.result += value;
return self;
};
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
Calculator *calculator = [[Calculator alloc] init];
calculator.jisuanAdd(2).jisuanAdd(5).jisuanAdd(3);
NSLog(@"%d", calculator.result);
}
結果:10