- 還記得當初剛接觸
Block
的時候觅闽,第一感覺就是覺得語法怪異,只知道就這么寫就對了聋伦,然后稀里糊涂地用了一段時間夫偶,之后發(fā)現(xiàn)在iOS
里,Block
頻繁使用觉增,比如官方的API大量用到Block
來回調(diào)做事情兵拢。經(jīng)過一段漫長歲月的使用和研究才明白Block
這個東西遠遠沒有這么簡單。 - 所以在這里總結(jié)一下我所學(xué)的關(guān)于
Block
的所有知識點逾礁,畢竟好記性不如爛筆頭说铃,寫下來記憶會更加深刻而且寫的過程會有更多的思考。
我將會從以下方面來講解Block
-
Block
的定義 -
Block
的基本使用 -
Block
的底層數(shù)據(jù)結(jié)構(gòu) -
Block
的類型 -
Block
捕獲變量機制 -
__Block
修飾符究竟做了什么嘹履? -
Block
內(nèi)存管理 -
Block
循環(huán)引用 -
Block
交換實現(xiàn) -
Block
相關(guān)面試題 - ...
Block的定義
Blocks是C語言的擴充功能腻扇。可以用一句話來表示Blocks的擴充功能:帶有自動變量(局部變量)的匿名函數(shù)砾嫉。
顧名思義幼苛,所謂匿名函數(shù)就是不帶有名稱的函數(shù)。
—— 引用自《iOS與OS X多線程和內(nèi)存管理》
也就是說焕刮,Blocks
類似于某些語言中的閉包函數(shù)舶沿,以下是block
的語法聲明
返回值類型 (^變量名)(參數(shù)列表) = ^ 返回值類型 (參數(shù)列表) 表達式
用代碼來表示就是
void (^block)(void) = ^void (void){};
其中右邊的返回值類型和參數(shù)類型為空的時候可以省略不寫
void (^block)(void) = ^{};
當然,我們也可以利用typedef
的特性來定義一個Block
typedef void (^block)(void);
這樣使用起來更方便
比如第三方網(wǎng)絡(luò)框架AFNetworking
就通過這種定義方式大量使用Block
typedef void (^AFURLSessionDidBecomeInvalidBlock)(NSURLSession *session, NSError *error);
typedef NSURLSessionAuthChallengeDisposition (^AFURLSessionDidReceiveAuthenticationChallengeBlock)(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential * __autoreleasing *credential);
typedef NSURLRequest * (^AFURLSessionTaskWillPerformHTTPRedirectionBlock)(NSURLSession *session, NSURLSessionTask *task, NSURLResponse *response, NSURLRequest *request);
typedef NSURLSessionAuthChallengeDisposition (^AFURLSessionTaskDidReceiveAuthenticationChallengeBlock)(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential * __autoreleasing *credential);
typedef id (^AFURLSessionTaskAuthenticationChallengeBlock)(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, void (^completionHandler)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential));
以上摘自AFNetworking
中的AFURLSessionManager
Block的基本使用
block
可以作為屬性配并、參數(shù)括荡、返回值等形式使用
- 一、當
block
作為屬性時
@property(nonatomic, copy) void (^NormalBlock)(void);
或者
typedef void (^NormalBlock)(void);
@property(nonatomic, copy) NormalBlock block;
這種用法最常見的就是平時我們在cell
中的響應(yīng)事件的處理荐绝,有時使用block
來回調(diào)到VC
去處理會更加方便
@interface Cell : UITableViewCell
@property(nonatomic, copy) void (^clickBlock)(void);
@end
@implementation Cell
- (void)clickAction{
if(self. clickBlock){
self. clickBlock();
}
}
@end
@interface VC : UIViewController
@end
@implementation VC
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
Cell *cell = [ProGoldRiceRankCell makeCellWithTableView:tableView];
cell. clickBlock = ^{
//do anything
};
return cell;
}
@end
-
二一汽、當
block
作為參數(shù)時
有時候我們需要從一個方法中返回一個值時,但剛好需要經(jīng)過GCD
延時處理后賦值才返回低滩,這種場景用return
時不行的召夹,因為GCD
中的block
返回值類型為空,那么這時候可以用block
來回調(diào)返回值恕沫。
typedef void (^NormalBlock)(NSString *value);
- (void)test{
[self doSomeThingWithBlock:^(NSString *value) {
NSLog(@"%@",value);
}];
}
- (void)doSomeThingWithBlock:(NormalBlock)block{
NSString *value = @"1";
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
value = @"2";
block(value);
});
}
-
三监憎、當
block
作為返回值時
我們經(jīng)常使用的Masonry
框架內(nèi)部實現(xiàn)就大量用到block
返回值來實現(xiàn)鏈式調(diào)用的語法
[_iconImg mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.left.bottom.right.mas_equalTo(0);
}];
在這里簡單說一下Masonry
鏈式調(diào)用的實現(xiàn)原理(想要看完整源碼解析的可以看這篇iOS開發(fā)之Masonry框架源碼解析,個人覺得寫得非常不錯)
mas_makeConstraints
這個方法的實現(xiàn)如下婶溯,可以看到我們平時寫的約束代碼都是通過Block
傳參的方式來對MASConstraintMaker
進行所有的約束設(shè)置鲸阔,然后再調(diào)用install
方法安裝所有約束
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
make.top.left.bottom.right.mas_equalTo(0);
這一句鏈式調(diào)用內(nèi)部是這么操作的
- 通過封裝好各種約束方法的工廠類
MASConstraintMaker
偷霉,首先調(diào)用top
- (MASConstraint *)top {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}
- 然后在調(diào)用
top
后會返回約束抽象類MASConstraint
(實際上返回的是MASConstraint
的子類MASViewConstraint
或者MASCompositeConstraint
)
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
if ([constraint isKindOfClass:MASViewConstraint.class]) {
//replace with composite constraint
NSArray *children = @[constraint, newConstraint];
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self;//設(shè)為代理
[self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
return compositeConstraint;//這里返回MASCompositeConstraint類型
}
if (!constraint) {
newConstraint.delegate = self;//設(shè)為代理
[self.constraints addObject:newConstraint];
}
return newConstraint;//這里返回MASViewConstraint類型
}
- 接著再次調(diào)用
left
(這次是MASConstraint
里的方法)
- (MASConstraint *)left {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- 而
MASConstraint
通過把MASConstraintMaker
設(shè)為代理從而使調(diào)用MASConstraint
的left
方法傳遞到MASConstraintMaker
實現(xiàn)的代理方法里面,然后代理方法又返回約束類MASConstraint
本身褐筛,這樣就可以連續(xù)設(shè)置多個約束类少,而且最終都會調(diào)用到最上層工廠類MASConstraintMaker
里的方法
- (MASConstraint *)left {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");
//調(diào)用代理方法
return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
}
- 我們來看
mas_equalTo
和offset
- (MASConstraint * (^)(id))mas_equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
- (MASConstraint * (^)(CGFloat))offset {
return ^id(CGFloat offset){
self.offset = offset;
return self;
};
}
這兩個方法都是MASConstraint
里的方法,所以設(shè)置完約束后返回的MASConstraint
類可以直接調(diào)用渔扎。
可以看到這兩個方法都返回了一個(返回值為MASConstraint
類型的Block
)硫狞,所以mas_equalTo(0)
相當于(MASConstraint * (^)(id))(0)
,MASConstraint * (^)(id)
看作一個整體Block
的話就相當于Block(0)
晃痴,這不就是我們平時調(diào)用Block
的方法么残吩!然后調(diào)用Block
后返回MASConstraint
類型,從而可以繼續(xù)調(diào)用下一個方法倘核,這就是Block
作為返回值實現(xiàn)鏈式調(diào)用的用法所在泣侮。
正所謂光說(看)不練假功夫,那么現(xiàn)在我們親自實現(xiàn)一個鏈式調(diào)用的例子=舫活尊!
創(chuàng)建一個Student
類
.h
文件
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@class Student;
@interface Student : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger tall;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) CGSize size;
- (Student * (^)(NSString *))per_name;
- (Student * (^)(int))per_tall;
- (Student * (^)(int))per_age;
- (Student * (^)(CGSize))per_size;
- (Student * (^)(void))run;
@end
NS_ASSUME_NONNULL_END
.m
文件
#import "Student.h"
@interface Student ()
@end
@implementation Student
- (Student * (^)(NSString *))per_name{
return ^ Student * (NSString *name){
self.name = name;
return self;
};
}
- (Student * (^)(int))per_tall{
return ^ Student * (int tall){
self.tall = tall;
return self;
};
}
- (Student * (^)(int))per_age{
return ^ Student * (int age){
self.age = age;
return self;
};
}
- (Student * (^)(CGSize))per_size{
return ^ Student * (CGSize size){
self.size = size;
return self;
};
}
- (Student * (^)(void))run{
return ^ Student * (void){
NSLog(@"我在跑步");
return self;
};
}
@end
在TestVC
里使用
- (void)test{
Student *s = [Student new];
s.per_name(@"小強")
.per_tall(173)
.per_age(18)
.per_size(CGSizeMake(180, 80))
.run();
NSLog(@"我是一名學(xué)生,我的名字是%@琼蚯,身高%ld,年齡%ld,尺寸%@",s.name,s.tall,s.age,NSStringFromCGSize(s.size));
}
打印
2020-08-18 12:02:19.315271+0800 CJJFramework[3846:74527] 我在跑步
2020-08-18 12:02:21.422766+0800 CJJFramework[3846:74527] 我是一名學(xué)生酬凳,我的名字是小強,身高173,年齡18,尺寸{180, 80}
(lldb)
這就是一個簡單的鏈式語法調(diào)用的實現(xiàn)遭庶,簡單太優(yōu)美了有木有宁仔!比oc
那繁瑣的對象.調(diào)用簡潔太多了。
順便打個小廣告^-^
iOS-CJJTimer 高性能倒計時工具(短信峦睡、商品秒殺
Github地址
我封裝的一個倒計時工具翎苫,里面也用到了鏈式語法調(diào)用,有興趣的可以看看榨了。
Block的底層數(shù)據(jù)結(jié)構(gòu)
Block
本質(zhì)上是一個OC
對象煎谍,因為它繼承自NSBlock
,而NSBlock
又繼承自NSObject
龙屉,所以Block
內(nèi)部是有一個isa
指針的呐粘。
并且,Block
是一個封裝了函數(shù)調(diào)用以及函數(shù)調(diào)用環(huán)境的OC
對象转捕。
- 函數(shù)調(diào)用
void (^block)(void) = ^{
NSLog(@"%d",a);
};
通過窺探底層作岖,我們會發(fā)現(xiàn)
NSLog(@"%d",a);
這一句代碼會直接存在于
Block
中,在Block
的初始化方法中五芝,傳遞了一個參數(shù)*fp
(最后把函數(shù)的地址傳給了block->impl->FuncPtr
)痘儡,這就意味著直接把整段代碼塊傳遞進Block
里面存著了(封裝了函數(shù)的地址,屬于引用傳遞)
- 函數(shù)調(diào)用環(huán)境
Block
里面會封裝(存儲)外面?zhèn)鬟M來的自動變量
具體的實現(xiàn)流程接下來會講到:
通過翻看蘋果官方源碼或者直接把oc
代碼編譯成底層語言C++
代碼枢步,就可以找到以下源碼
-
block
的底層結(jié)構(gòu)如下圖所示
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5l_0xn052bn6dgb9z7pfk8bbg740000gn_T_main_88f00d_mi_0);
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
從來沒讀過源碼或者不熟悉C++
的可能會覺得一臉懵沉删,其實Block
可以簡化成以下結(jié)構(gòu)
struct __main_block_impl_0{
//struct __block_impl impl; //block的底層信息
void *isa;//說明block是一個oc對象
int Flags;
int Reserved;
void *FuncPtr;//所封裝的函數(shù)的地址
//struct __main_block_desc_0* Desc; //block的描述信息
size_t reserved;
size_t Block_size;//block的大小
};
可以看到渐尿,Block
的底層數(shù)據(jù)結(jié)構(gòu)就是一個結(jié)構(gòu)體,其簡化后所包含的成員變量如下
- void *isa //說明
block
是一個oc
對象 - int Flags // 某些標志矾瑰,蘋果用這個
flags
與上以下的枚舉值來判斷一些東西
// Values for Block_layout->flags to describe block objects
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};
比如通過判斷flags & BLOCK_HAS_COPY_DISPOSE
來確定是否存在copy
和dispose
函數(shù)砖茸,具體后面會講到
if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
desc += sizeof(struct Block_descriptor_2);
}
以上代碼來自蘋果官方源碼libclosure-74
- int Reserved //版本升級所需的區(qū)域大小
- void *FuncPtr //所封裝的函數(shù)的地址
- size_t reserved //版本升級所需的區(qū)域大小
- size_t Block_size //block的大小
以及初始化函數(shù)
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
在初始化block
時傳了2個參數(shù),一個是函數(shù)對象的地址impl.FuncPtr = fp
(fp
就是函數(shù)指針(void *)__main_block_func_0
)脯倚,另一個是描述對象的地址Desc = desc
(desc
就是描述信息的地址&__main_block_desc_0_DATA
)
Block的類型
Block
有3種類型渔彰,可以通過調(diào)用class方法查看其類型以及繼承鏈
- 1.全局Block(
_NSConcreteGlobalBlock
)
(__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject)
- 2.棧Block(
_NSConcreteStackBlock
)
(__NSStackBlock__ : __NSStackBlock : NSBlock : NSObject)
- 3.堆Block(
_NSConcreteMallocBlock
)
(__NSMallocBlock__ : __NSMallocBlock : NSBlock : NSObject)
為什么Block
會有三種類型的呢嵌屎?
這個是由存儲它的內(nèi)存位置決定的推正,下圖展示了在應(yīng)用程序的內(nèi)存中,三種Block
所存在的區(qū)域宝惰,也就是說要判斷一個Block
是什么類型植榕,就是看它存在于內(nèi)存的哪個區(qū)域。
那么如何區(qū)分三種
Block
尼夺,它們之間有什么異同點尊残?以下就是這三種
Block
的對比
NSGlobalBlock
存儲的位置:程序的數(shù)據(jù)區(qū)域(全局區(qū))
環(huán)境:沒有訪問auto變量
copy后的效果:什么也不做NSStackBlock
存儲的位置:棧
環(huán)境:訪問了auto變量
copy后的效果:從棧賦值到堆NSMallocBlock
存儲的位置:堆
環(huán)境:NSStackBlock調(diào)用了copy
copy后的效果:引用計數(shù)增加
舉例
typedef void (^block0)(void)
int val1 = 10;
- (void)test{
//NSGlobalBlock
block0 = ^{
};
//NSGlobalBlock
void (^block)(void) = ^{
};
//NSGlobalBlock
void (^block1)(void) = ^{
NSLog(@"%d",val1);
};
//MRC下為NSStackBlock,ARC下為NSMallocBlock(ARC下賦值給會把此Block從棧Copy到堆里)
int val2 = 20;
void (^block2)(void) = ^{
NSLog(@"%d",val2);
};
//NSMallocBlock
__block int val3 = 20;
void (^block3)(void) = ^{
NSLog(@"%d",val3);
};
}
Block捕獲變量機制
眾所周知淤堵,為了保證Block
內(nèi)部能夠正常訪問外部的變量寝衫,Block
有一個捕獲變量的機制。
Block
捕獲變量后相當于往Block
結(jié)構(gòu)體里增加一個成員變量拐邪。
首先變量可以分為兩種慰毅,局部變量和全局變量。
局部變量分為局部(自動)變量和局部靜態(tài)變量(static
)
全局變量分為全局變量和全局靜態(tài)變量(static
)
以下是它們的區(qū)別
- 局部變量
- 1.自動變量(意思是扎阶,離開作用范圍就會自動銷毀汹胃,所以叫做自動變量,被
Block
捕獲時是值傳遞(捕獲的是具體存儲的值))
{ auto int a = 0; }
- 2.局部靜態(tài)變量(會在內(nèi)存中一直存在东臀,被
Block
捕獲時是引用傳遞(捕獲的是變量的地址))
{ static int a = 0; }
- 1.自動變量(意思是扎阶,離開作用范圍就會自動銷毀汹胃,所以叫做自動變量,被
- 全局變量(會在內(nèi)存中一直存在着饥,不會被
Block
捕獲)- 全局變量
int a = 0;
- 全局靜態(tài)變量,只能在本文件訪問惰赋,不能在外部
extern
static int b = 0;
總結(jié):只有局部變量才會被Block
捕獲宰掉,全局變量不會被捕獲
為什么全局變量不用捕獲?
因為隨時可以訪問
為什么局部變量需要捕獲赁濒?
作用域的問題轨奄,在Block
里面使用Block
外聲明的局部變量,相當于跨函數(shù)使用這個局部變量流部,如果不存一份到Block
里面戚绕,是無法使用的,會造成訪問無效內(nèi)存枝冀,因為外面的局部變量有可能過了作用域就會自動被銷毀
例如
typedef void (^Block)(void);
@property(nonatomic, copy) Block block;
- (void)test{
int a = 0;
self.block = ^{
NSLog(@"%d",a);
};
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self test];
self.block();
}
以上這段代碼舞丛,當點擊self.view
時會響應(yīng)touchBegin
耘子,然后調(diào)用test
焕议,test
里面創(chuàng)建了一個局部自動變量a
佛猛,然后初始化了self.block
變量,里面使用了a
番舆,但是調(diào)用完test
后吨凑,a
就會銷毀捍歪,然后才調(diào)用Block
,這時候Block
里面再使用a
鸵钝,如果不事先捕獲(存一份)糙臼,就會崩潰,訪問無效內(nèi)存恩商,這就是為什么局部變量需要捕獲变逃,而全局變量不需要捕獲的根本原因。
還有一個特殊情況怠堪,self會被捕獲嗎揽乱?
- (void)test{
self.block = ^{
NSLog(@"%p",self);
};
}
會,因為self
也是局部變量粟矿,我們來回想一下凰棉,在OC
里調(diào)用方法實際上會傳遞self
指針的參數(shù),而且捕獲的是指針,所以屬于引用傳遞陌粹。
objc_msgSend(id self, SEL _cmd, ...)
所以我們之所以能在每一個方法中使用self
撒犀,就是因為默認傳入self
變量
另一個特殊情況,成員變量會被捕獲嗎申屹?
@property(nonatomic, copy) NSString *name;
- (void)test{
self.block = ^{
NSLog(@"%@",_name);
};
}
會绘证,因為這里訪問的成員變量也是局部變量,相當于
- (void)test{
self.block = ^{
NSLog(@"%@",self->_name);
};
}
__Block修飾符究竟做了什么哗讥?
我們來看下面這一段代碼
int val = 10;
void (^block)(void) = ^{
val = 20;//這個是錯誤的嚷那,不能通過編譯的,因為val是自動局部變量杆煞,過了作用域就銷毀
//而這里是在另一個椢嚎恚空間,不能訪問val
};
那么如何使得變量val
可以更改呢决乎?
有幾種辦法
可以把變量val
修飾為全局變量或者靜態(tài)變量队询,而更好的辦法是用__block
修飾符修飾
__block
修飾符
-
__block
可以用于解決Block
內(nèi)部無法修改auto
變量值的問題 -
__block
不能修飾全局變量、靜態(tài)變量(static
) - 編譯器會將
__block
變量包裝成一個對象(__Block_byref_age_0
類型)
比如說這一段
__block int val = 1;
int (^block)(CGFloat num) = ^ int (CGFloat num){
NSLog(@"這是一個Block");
val = 2;
return val;
};
編譯成C++代碼如下构诚,我整理了一下格式方便查看
__attribute__((__blocks__(byref))) __Block_byref_ val_0 val =
{
(void*)0,//void *__isa
(__Block_byref_ val_0 *)& val,//__Block_byref_val_0 *__forwarding
0,//int __flags
sizeof(__Block_byref_val_0),//int __size
1 //int val
};
int (*block)(CGFloat num) = (
(int (*)(CGFloat))
&__main_block_impl_0(
(void *)__main_block_func_0, //
&__main_block_desc_0_DATA,
(__Block_byref_val_0 *)& val,
570425344
)
);
自動變量val
被__block
修飾后會包裝成__Block_byref_val_0
對象蚌斩,也就是說Block
的__main_block_impl_0
結(jié)構(gòu)體實例持有指向__block
變量的__Block_byref_val_0
結(jié)構(gòu)體實例的指針。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __Block_byref_val_0{
void *__isa;
__Block_byref_age_0 *__forwarding;//這個指針指向該對象自身的地址
int __flags;
int __size;
int val;
};
會發(fā)現(xiàn)里面也有一個val
范嘱,其實這里的val
才是Block
捕獲進來的那個val
送膳。
還有一個成員變量__forwarding
而且__main_block_impl_0里的__Block_byref_val_0變量并不是存在于Block
結(jié)構(gòu)體里面员魏,Block
只是保存了一個引用了__Block_byref_val_0變量地址的指針,這樣就可以在多個不同的Block
里面訪問同一個__block
變量了叠聋。
看著這個圖可能會有疑問了撕阎。
為什么不能直接在Block
結(jié)構(gòu)體里面存儲val
,而要搞這么麻煩碌补,生成一個val
結(jié)構(gòu)體虏束,然后把val
變量存放到里面呢?
- 我的理解是厦章,因為直接在
Block
中存儲val
變量的話镇匀,是在棧上存儲的,等變量作用域過去之后變量就會銷毀闷袒,這樣就無法在Block
里訪問該變量了坑律;而通過把其包裝成一個__Block_byref_val_0
類型的對象,把該變量保存在對象里囊骤,當Block
從棧copy
到堆上的時候,相當于__block
變量也從棧copy
到堆里存了一份冀值,這樣作用域過了之后也物,Block
仍然可以訪問val
變量,而在copy
的過程中列疗,棧上的__block
變量中的__forwarding
指針會變?yōu)橹赶蚨焉系?code>__block變量的結(jié)構(gòu)體實例的地址滑蚯,而通過這種方式,無論是在Block
語法中抵栈、Block
語法外使用__block
變量告材,還是__block
變量配置在棧上或堆上,都可以順利地訪問同一個__block
變量古劲。
賦值__block變量
Block內(nèi)存管理
如果
Block
捕獲了對象類型的auto
變量會怎么樣斥赋?
實際上只是多了內(nèi)存管理方面的操作。
Block
經(jīng)過copy
之后會在desc
里生成的2個函數(shù)
-
copy
函數(shù)
調(diào)用時機 棧上的Block
復(fù)制到堆時 -
dispose
函數(shù)
調(diào)用時機 堆上的Block
被廢棄時
當Block
內(nèi)部訪問了帶有__block
修飾符的對象類型的auto
變量時
當
block
在棧上時产艾,并不會對__block
變量產(chǎn)生強引用-
當
block
被copy
到堆時- 會調(diào)用
block
內(nèi)部的copy
函數(shù) -
copy
函數(shù)內(nèi)部會調(diào)用_Block_object_assign
函數(shù) -
_Block_object_assign
函數(shù)會根據(jù)所指向?qū)ο蟮男揎椃?code>__strong,__weak
,__unsafe_unretained
)做出相應(yīng)的操作疤剑,形成強引用(retain
)或者弱引用(注意:這里僅限于ARC時會retain
,MRC時不會retain
)
__block持有對象
- 會調(diào)用
-
當
block
從堆中移除時- 會調(diào)用
block
內(nèi)部的dispose
函數(shù) -
dispose
函數(shù)內(nèi)部會調(diào)用_Block_object_dispose
函數(shù) -
_Block_object_dispose
函數(shù)會自動釋放引用的__block變量(release)
- 會調(diào)用
對象類型的auto變量闷堡、__block變量
//auto
{
(auto) Person *person = [Person new];
void (^block)(void) = ^{
NSLog(@"%@",person);
};
}
//__block
{
__block Person *person = [Person new];
void (^block)(void) = ^{
NSLog(@"%@",person);
};
}
- 當
block
在棧上時隘膘,對它們都不會產(chǎn)生強引用 - 當
block
拷貝到堆上時,都會通過copy
函數(shù)來處理它們
//傳8和3來區(qū)別這兩種變量
//__block變量(假設(shè)變量名叫做a)
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
//對象類型的auto變量(假設(shè)變量名叫做p)
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
- 當
block
從堆上移除時杠览,都會通過dispose
函數(shù)來釋放它們
//__block變量(假設(shè)變量名叫做a)
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
//對象類型的auto變量(假設(shè)變量名叫做p)
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
Block循環(huán)引用
有一個對象A
弯菊,一個Block
當A
強引用了Block
,Block
也強引用了A
踱阿,這種情況就是循環(huán)引用管钳,造成內(nèi)存泄漏吨悍。
用代碼表示就是
@interface A : NSObject
@property(nonatomic, copy) void (^block)(void);
@end
@implementation
- (void)viewDidLoad{
[super viewDidLoad];
self.block = ^{
NSLog(@"%@",self);
};
}
@end
如上,self
持有block
屬性蹋嵌,然后block
里持有self
育瓜,互相強引用,造成誰也釋放不了栽烂,這只是最簡單的一種情況躏仇,實際上平時遇到得有可能比這種復(fù)雜得多,有自引用循環(huán)(A->A
)腺办,雙向引用循環(huán)(A->B->A
)焰手,多引用循環(huán)(A->B->C->A
)等等,但是只要我們清楚了引用循環(huán)的本質(zhì)怀喉,這些情況其實都很容易發(fā)現(xiàn)并解決书妻,我們只要切斷引用鏈中隨意一方的強引用就可以解決引用循環(huán)的問題。
解決方案
在ARC
和MRC
下解決循環(huán)引用的方式各有不同躬拢。
在ARC
下躲履,可以使用__weak
、__unsafe_unretained
聊闯、__block
三種方式解決
//__weak
__weak typeof(self) weakSelf = self;
self.block = ^{
NSLog(@"%p",weakSelf);
};
//__unsafe_unretained
__unsafe_unretained id weakSelf = self;
self.block = ^{
NSLog(@"%p",weakSelf);
};
//__block
//因為ARC下__block會使得Block內(nèi)部強引用外部的變量
//所以需要調(diào)用Block并且手動把變量置空(nil)
__block id weakSelf = self;
self.block = ^{
NSLog(@"%p",weakSelf);
weakSelf = nil;
};
self.block();
在MRC
下工猜,可以使用__unsafe_unretained
、__block
解決
//__unsafe_unretained
__unsafe_unretained id weakSelf = self;
self.block = ^{
NSLog(@"%p",weakSelf);
};
//__block
__block id weakSelf = self;
self.block = ^{
NSLog(@"%p",weakSelf);
};
綜上菱蔬,最好的方法是ARC
下用__weak
篷帅,MRC
下用__unsafe_unretained
。
- 還有一種情況
如果要在block
里面訪問成員變量的話
@interface A : NSObject
{
NSString *name;
}
@property (nonatomic, copy) void (^block)(void);
@property (nonatomic, copy) NSString *address;
@end
@implementation A
- (void)testMethod {
name = @"名字";
__weak typeof(self) weakSelf = self;
self.block = ^{
//一定要加上這一句才能訪問name拴泌,不然weakSelf->name會報錯Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to strong variable first
__strong __typeof(weakSelf)strongSelf = weakSelf;
NSLog(@"%@-%@", strongSelf->name, weakSelf.address);
};
self.block();
}
@end
分析:
問題一:為什么成員變量name
要加上__strong
修飾一下才能訪問呢魏身?
答1:因為weakself
有可能在block
執(zhí)行過程中就釋放了,也就是weakself
指針置為nil
蚪腐,一旦釋放再使用nil
指針去訪問成員變量拿到的值也為nil
問題二:而address
卻可以直接用weakSelf
訪問weakSelf.address
?
答2:因為weakSelf.address
是調(diào)用address
的getter
方法箭昵,而不是直接訪問成員變量,即使weakself釋放了削茁,也不一定會影響使用宙枷,因為nil訪問getter方法無效,給空對象發(fā)消息是不會生效的茧跋。
另外
在平時開發(fā)中慰丛,我發(fā)現(xiàn)有一些同事看到只要有block
的地方就使用weakself
,即使是工作了三四年的瘾杭,也有這種問題诅病,實際上就是沒有搞懂引用循環(huán)的本質(zhì),下面舉幾個block
里使用self
不需要弱引用的例子
例子一:控制器沒有強引用block,block強引用self(不需要weakself)
@interface A : NSObject
@end
@implementation
- (void)viewDidLoad{
[super viewDidLoad];
id block = ^{
NSLog(@"%@",self);
};
}
@end
分析:self不持有block贤笆,block持有self蝇棉,不構(gòu)成雙向的循環(huán)引用,所以不需要weakself
例子二:類方法的block(不需要weakself)
@interface A : NSObject
@end
@implementation
- (void)viewDidLoad{
[super viewDidLoad];
[UIView animateWithDuration:duration animations:^{
NSLog(@"%@",self);
}];
}
@end
分析:同例子一
例子三:AFNetworking的請求方法的回調(diào)block(不需要weakself)
[[AFNetWorkManager sharedManager] requestWithUserMethod:POST Url:url parameters:paramsDic success:^(NetWorkResultModel * _Nonnull resultModel) {
NSLog(@"%@",self);
} failure:^(NSError * _Nonnull error) {
}];
分析:首先大多數(shù)封裝了AFN
的都是使用單例芥永,正常情況下篡殷,如果單例持有了self
,是會造成釋放不了self
的埋涧,因為除非人為釋放板辽,否則單例會在內(nèi)存中一直存在,那么這里的AFN
的block
引用了self
為什么不需要weakself
呢棘催,是因為AFN
內(nèi)部已經(jīng)做了處理劲弦,在請求結(jié)束之后移除了對block
的引用,所以在這種情況下是不需要使用weakself
的醇坝。
strongself
我們經(jīng)常會使用(weakself
+ strongself
)搭配使用
__weak __typeof(self) weakself = self;
self.block = ^{
__strong __typeof(self) strongSelf = weakself;
};
作用就是防止在block
執(zhí)行過程中使用了self
邑跪,但是self
已經(jīng)銷毀的情況
比如
@interface TestViewController ()
@property (nonatomic, copy) void (^testBlock)(void);
@end
@implementation TestViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.testBlock = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", weakSelf);
[weakSelf dataCollect];
});
};
}
- (void)dealloc {
NSLog(@"TestViewController銷毀");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.testBlock();
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)dataCollect {
NSLog(@"發(fā)送埋點");
}
@end
2022-04-13 11:13:55.398826+0800 Test[38751:1052715] TestViewController銷毀
2022-04-13 11:14:00.395086+0800 Test[38751:1052715] (null)
當我們點擊view
的時候,調(diào)用了block
的同時觸發(fā)了dismiss
呼猪,這時候由于沒有地方對self
有強引用画畅,所以就會走dealloc
方法,等5s之后再觸發(fā)GCD
里面的代碼時郑叠,weakself
已經(jīng)為nil
夜赵,所以無法調(diào)用dataCollect
。
但如果我們在block
里面使用strongself
乡革,重新強引用self
對象,那么就可以延長self
的生命周期
@interface TestViewController ()
@property (nonatomic, copy) void (^testBlock)(void);
@end
@implementation TestViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.testBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", strongSelf);
[strongSelf dataCollect];
});
};
}
- (void)dealloc {
NSLog(@"TestViewController銷毀");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.testBlock();
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)dataCollect {
NSLog(@"發(fā)送埋點");
}
@end
2022-04-13 11:16:13.391062+0800 Test[38821:1055142] <TestViewController: 0x14d627d70>
2022-04-13 11:16:13.391154+0800 Test[38821:1055142] 發(fā)送埋點
2022-04-13 11:16:13.391199+0800 Test[38821:1055142] TestViewController銷毀
當然摊腋,只要你結(jié)合上下文分析出不會出現(xiàn)以上這種情況沸版,只使用weakself
也沒問題。
Block交換實現(xiàn)
由于這一主題內(nèi)容太多兴蒸,所以另開一篇來談?wù)?br>
如何去hook一個block的實現(xiàn)视粮?
傳送門->iOS-玩轉(zhuǎn)Block(Hook Block 交換block的實現(xiàn))
Block相關(guān)面試題
一、
Block
的原理是怎樣的橙凳?本質(zhì)是什么蕾殴?
封裝了函數(shù)調(diào)用以及調(diào)用環(huán)境的OC
對象。(有待補充岛啸,結(jié)合實際面試情況自由發(fā)揮)二钓觉、
__block
的作用是什么?有什么使用注意點坚踩?
本質(zhì):把變量包裝成一個對象
作用:可以解決Block
內(nèi)部無法修改auto
變量值的問題
使用注意:內(nèi)存管理問題荡灾,在MRC
下__block
修飾內(nèi)部不會對對象產(chǎn)生強引用(retain
);ARC
下會,需要避免循環(huán)引用批幌。三础锐、
Block
的屬性修飾詞為什么是copy
?使用Block
有哪些使用注意荧缘?
原因:Block
一旦沒有進行copy
操作皆警,就不會在堆上,所以通過copy
到堆上我們可以對Block
進行內(nèi)存管理
使用注意:循環(huán)引用問題
另外:ARC
下用Strong
和Copy
是一樣的截粗,都會把Block
copy
到堆里面信姓,MRC
下只能用Copy
,所以結(jié)合兩種情況桐愉,用Copy
是最好的四财破、
Block
在修改NSMutableArray
,需不需要添加__block
?
不需要从诲,因為只是對數(shù)組操作內(nèi)容左痢,并不是修改他的內(nèi)存地址