OC block使用及底层原理、循环引用、__weak、__strong

本文主要总结了:

  1. block的基本语法
    有/无返回值和形参
    typedef定义block类型
  2. 变量访问,__block底层原理
  3. block的3种类型
  4. 什么时候会触发block的copy
  5. block的循环引用
    什么是循环引用
    block循环引用例子
    检测工具instruments-Leaks
  6. __weak
    为什么__weak可以打破循环引用?
    常见使用
  7. __strong
    使用场景,例子(延迟函数)

一、block基本语法

block用来保存一段代码,封装代码段,在需要的时候用。
block的标志:^

1. 没有返回值和形参时,可以省略后面的()

void (^myBlock) ()= ^ {
    //要保存的代码段
};

// 调用方式与函数一样:
myBlock();

2. 有返回值和形参

int (^sumBlock) (int , int) = ^(int a , int b){
    return a+b;
};
int c = sumBlock(10,12);

对比函数指针:

int sum(int a, int b){
    return a+b;
}

//调用:
int (*p)(int, int)=sum;
int d = p(10,12);
  • 再来一个例子:
void (^lineBlock)(int) = ^(int n){
      for(int i=0; i<n; i++){
        NSLog(@“_______”);
      }
};
lineBlock(5);

3. 使用typedef定义block类型

//定义MyBlock是一个传两个int类型的参数,返回一个int的代码块

typedef  int (^MyBlock)(int, int);

这时
int (^sumBlock) (int , int) = ^(int a , int b){
return a+b;
};
int c = sumBlock(10,12);
就可以写成:

MyBlock sumBlock=^(int a, int b){
    return a+b;
};
sumBlock(1, 2);

二、变量访问

  • 在block中可以使用和改变全局变量
  • block内部可以访问外面的变量
  • 局部变量,可以使用,不能改变
    本地变量,代码块会在定义的时候复制并保存他们的状态,作为常量获取到
typedef double (^MulBlock)(void);
double a=10,b=20;
MulBlock b=^(void){
    return a*b;
}
a=20;
b=50;
NSLog(@“%f”,b(a, b));//此时还是会输出200

*可以通过将变量标记为全局(static)解决

  • 默认情况下,block内部不能修改外面的局部变量
  • 给局部变量加上_blcok关键字,这个局部变量就可以在block内部捕捉到变量,并可以进行修改
__block int b=20;

【注意】__block修饰时,底层实现是将变量从栈拷贝一份到堆中,从而获得使用权,用代码打印一下:

        __block int a = 0;
        NSLog(@"a address %p",&a);
        void (^function)()=^(){
            NSLog(@"a address %p",&a);
            a = 100;
            NSLog(@"1 %d",a);
        };
        a = 20;
        function();
        NSLog(@"a address %p",&a);
        NSLog(@"2 %d",a);
//打印结果:
//  a address 0x7ffeefbff5d8,还在栈中
//  a address 0x10060fff8,拷贝到了堆中
//  1 100
//  a address 0x10060fff8
//  2 100,在block中对a修改成功
  • __block int a = 0;对a的内存地址进行修改。从栈copy到堆中,此时不会被随意销毁。
  • 栈的区间一般来说是2M,变化区间过大内存地址会发生变化,由高地址跑到低地址,进入堆中。

附上内存中堆栈的关系图(参考了_block关键字的实现原理):

无论访问的是栈里的__block还是堆里的__block都是以val->__forwarding的形式访问,访问的都是堆上的__block所以地址改变值也修改成功


三、block的3种类型:

1. NSGlobalBlock,全局block:静态

  • 位于全局区
  • 在block内部不使用外部变量,或者只使用静态变量和全局变量
//例子1
- (void)viewDidLoad {
        // main thread [block copy]
        void (^block)(void) = ^{
                NSLog(@"hello block");
        }; //匿名函数
        block();
        NSLog(@"%@",block);  
        //万物皆对象   <__NSGlobalBlock__: 0x108656088>
        //虽然赋值给了强引用对象“block”(默认__strong修饰),但是没有使用外部变量,所以是globalBlock
}
//例子2
- (void)viewDidLoad {
        NSLog(@"%@",^{
        
        });
        //<__NSGlobalBlock__: 0x100001028>
        //没有使用外部变量,所以还是globalBlock
}

2. NSMallocBlock,堆block

  • 位于堆区
  • 在block内部使用局部变量或者OC属性,并且赋值给强引用或者Copy修饰的变量
- (void)viewDidLoad {
        int a = 10;  //捕获外部变量
        //下面括号里的block,默认是用__strong修饰的强引用
        void (^block)(void) = ^{
                //访问内存空间访问、捕获外部变量
                NSLog(@"hello block %d",a); 
        }; //匿名函数
        block();
        NSLog(@"%@",block);  //<__NSMallocBlock__: 0x6000025c4ba0>
}

3. NSStackBlock,栈block

  • 位于栈区
  • 与mallocBlock一样,可以在内部使用局部变量或者OC属性。但是!不能赋值给强引用或Copy修饰的变量。
//例子1
- (void)viewDidLoad {
        int a = 10;
        NSLog(@"%@",^{
                NSLog(@"%d",a);  //<__NSStackBlock__: 0x7ffee71e2930>
        });  
}
//例子2
- (void)viewDidLoad {
        int a  = 10;
        //不写"__weak"时默认是__strong修饰
        void(^__weak block)(void) = ^{
            NSLog(@"%d",a);
        };
        NSLog(@"%@",block);  //<__NSStackBlock__: 0x7ffeefbff4c8>
}

四、什么情况下会触发block的Copy

  1. 手动copy✅
- (void)viewDidLoad {
        int a = 10;
        void (^__weak myBlock)(void) = ^{
                NSLog(@"%d",a);
        }; 
        NSLog(@"%@",myBlock); //<__NSStackBlock__: 0x7ffeefbff4c8>
        NSLog(@"%@",[myBlock copy]);  //<__NSMallocBlock__: 0x1007003f0>
}
  1. Block作为返回值❌ 不一定会触发copy
- (void)viewDidLoad {
        NSLog(@"%@", [self returnBlock]);
        //因为下面方法中的block,并没有使用局部变量,仍然是globalBlock
}

- (void(^)(void))returnBlock {
        return ^{

        };
}
  1. 被强引用或者Copy修饰❌也要符合“在block内部使用了局部变量”
  2. 系统API包含usingBlock
  • NSArray

五、block的循环引用

1. 什么是循环引用?

在堆上的对象与堆上的对象互相强引用造成的环。
可能是两个对象,可能是N个对象

2. block循环引用例子
  • 例子1
    person->block, block->person
@interface Person : NSObject
@property (nonatomic, copy) void(^block)(void);
@end

int main(int argh, const char * argue[]) {
        Person *person = [Person new];
        person.block = ^{  //赋值时person持有了block
                NSLog(@"%@", person); //block内部又捕获person,引用+1
        };
        return 0;
}
  • 例子2
    在最外层ViewController中,创建BlockCoat类,BlockCoat中有有一个属性是BlockTest类,BlockTest类中有一个属性是block。
    在ViewController中,调用BlockCoat的方法,通过方法调用完之后,BlockTest有无执行dealloc方法,来判断是否存在循环引用。
//  ViewController.m
#import "BlockCoat.h"

@implementation ViewController
- (void)viewDidLoad {
    NSLog(@"-----lychee1----");
    BlockCoat *bc = [BlockCoat new];
    [bc test];  //在最外层的vc中测试,blockTest是否能正常dealloc
    NSLog(@"-----lychee2----");
}
//  BlockCoat.h
#import <Foundation/Foundation.h>
@class BlockTest;

@interface BlockCoat : NSObject
@property(nonatomic, strong) BlockTest *bt;

- (void)test;
@end
//  BlockCoat.m
#import "BlockCoat.h"

@implementation BlockCoat

- (void)test {
    int a = 10;
    self.bt = [BlockTest new]; //self(blockCoat)持有了blockTest
    __weak typeof (self) weakSelf = self;
    [self.bt testBlock:^BOOL{  //参数block中,捕获了self
        NSLog(@"%@",self);
 //    NSLog(@"%@", weakSelf);
        NSLog(@"%d",a);
        return YES;
    }];
}

@end
//  BlockTest.h
#import <Foundation/Foundation.h>

typedef BOOL(^myBlock)(void);

@interface BlockTest : NSObject
@property(nonatomic, copy) myBlock b;

- (BOOL)testBlock:(myBlock)block;

@end
//  BlockTest.m
#import "BlockTest.h"

@implementation BlockTest

- (BOOL)testBlock:(myBlock)block {
    //还未进行赋值copy,传进来的block有捕获外部变量(self:BlockCoat),打印出来是stackBlock (不捕获外部变量时是globalBlock)
    NSLog(@"33---%@",block); 
    self.b = block;
    //赋值给了copy修饰的b属性,此时BlockTest持有了block,有捕获外部变量,打印的是mallocBlock
    NSLog(@"44---%@",self.b); 
    return block();
}

- (void)dealloc {
    NSLog(@"dealloc~~");
}

@end

由于在BlockCoat.m中,持有了BlockTest,同时block捕获了self(即BlockCoat);
下面的BlockTest.m中,持有了block,变为了mallocBlock;
此时就造成了循环引用:BlockCoat->BlockTest->block->BlockCoat

打破循环引用的方法?
在BlockCoat.m中,调用方法的block中,self换成weakSelf

什么时候不存在循环引用?(打破循环链中的任一环节)
(1)BlockCoat中,不把BlockTest作为属性赋值,用到的时候直接创建
(2)或者,BlockTest中,直接调用传来的block,不要赋值给自己的属性(持有)
(3)或者,block中,不捕获self(BlockCoat)
当不存在循环引用时,就不需要写__weak,直接用self

从上面这个例子中联想,如果BlockTest是系统框架中的类,那么在我们自定义的类中,如果持有了系统类对象,并且传参的block中捕获了self,就要特别注意测试是否存在循环引用。
如果能够确定系统库/三方库的类里,没有对block进行持有,例如masonry,只执行了block方法,那么就不会产生循环引用。

  • 例子3
    self -> _source -> block -> self
@interface ViewController()
@property (nonatomic, strong) dispatch_source_t source;
@end

@implementation ViewController
- (void)viewDidLoad {
        [self dispatch_source_block];
}

- (void)dispatch_source_block {
    _source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    dispatch_source_set_timer(_source, dispatch_time(DISPATCH_TIME_NOW, 0), 1, 3);
    dispatch_block_t block = ^{
        NSLog(@"%@",self);
        NSLog(@"12321");
    };
    dispatch_source_set_event_handler(_source, block); //_source持有block
    dispatch_activate(_source);
}

- (void)dealloc{
    NSLog(@"dealloc----");
}
3. 循环引用检测工具

instruments:Leaks

  • 菜单栏 Xcode->Open developer tool->instruments
  • 选择Blank
  • 点右上角“+”号,下拉框中选Leaks
  • 左上角选择要运行的应用程序,点击红点开始

有红色❌说明检测到内存泄露,根据下面显示的信息,具体排查问题。
(这里只是简单使用,具体还待进一步对工具进行学习、总结。)


instruments工具界面

六、 __weak 打破循环引用

为什么__weak可以打破循环引用?
(1)weak的作用:把self加入sideTables的弱引用表中,不对它产生强引用,在使用的时候取出来。当self的强引用计数变为0时,将其从表中移除,并自动置为nil
(2)mallocBlock捕获__weak修饰的变量时,捕获进内部的也是用__weak修饰。实际是在block内部另外定义了一个__weak修饰的局部变量,指向外部的变量。

//例如这么写
 __weak typeof (self) weakSelf = self;
void(^block)(void) = ^{
        weakSelf;
};

//在底层,实际上,block内部新建了__weak修饰的局部变量,来指向weakSelf
 __weak typeof (self) weakSelf = self;
void(^block)(void) = ^{
        //即 __weak NSObject *a = weakSelf;
        __weak typeof(weakSelf) a = weakSelf;
        a;
};

【tips】经常用到的宏定义弱引用:

//用weakState(weakVar, strongVar)来指代后面的__weak typeof(strongVar) weakVar = strongVar
#define weakState(weakVar, strongVar) __weak typeof(strongVar) weakVar = strongVar;
//__weak表示修饰词,typeof()获取原始变量的类型,定义同类型的新变量weakVar
//将原始变量strongVar,赋值给weakVar
__weak typeof(strongVar) weakVar = strongVar;

此时,在需要弱引用的地方只需要写:
weakState(weakSelf, self);
后文中,就可以拿weakSelf来用

七、 __strong

  • 应用场景:希望对象存活的时间久一点(持续到block结束),一般用在异步回调block中,防止执行block时,引用的对象已经被释放。OC中给nil发送消息是不响应的。
  • 以延时函数为例
    (1)不存在循环引用的情况:
    block中的self,延迟函数延迟2秒后才释放
    换成weakSelf,self将立即释放,不会延迟2秒
- (void)viewDidLoad {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self tp]; //这里没有循环引用,直接使用self即可
        //如果使用了weakSelf,由于没有强引用self,self释放掉之后,weakSelf就指向了nil,不会执行tp方法。
        //[weakSelf tp];
        NSLog(@"---延迟2秒");
    });
}

- (void)tp{
    NSLog(@"hahaha-----%@",self);
}

(2)存在循环引用,且block嵌套的情况

  • 因为self->myBlock->self,所以myBlock中用到self的地方要用weakSelf替代
  • 由于延迟函数,要执行self的tp方法,就要求self要能够延迟2秒后再释放,这时就需要用到__strong。但是__strong是写在myBlock中呢?还是dispatch_after中呢?

先写答案,要写在myBlock中,然后在dispatch_after中捕获外层的strongSelf。

- (void)viewDidLoad {
    __weak typeof (self) weakSelf = self;
    self.myBlock = ^(int a) {
        __strong typeof(weakSelf) strongSelf = weakSelf; //正确写法
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//            __strong typeof(weakSelf) strongSelf = weakSelf; //错误写法
            [strongSelf tp];
            NSLog(@"---延迟2秒");
        });
        [weakSelf tp];
    };
    self.myBlock(0);
}

原因是:变量在block中是层层传递的。

结合上个小节中block捕获__weak变量的原理,可以画出下图:
当self生命周期结束释放时,self、weakSelf变为nil,但myBlock中的a由于有被强引用,暂时不会释放,dispatch_after中捕获的strongSelf也还存活,直到延时函数执行完,再释放。

而如果__strong写在dispatch_after的block中,则会变成:

当self释放时,self、weakSelf、a由于没有被强引用都将被释放变为nil,此时di spatch_after捕获的变量a2、strongSelf也是nil,无法执行对应方法

【延伸】
后面这种写法,虽然无法“延长变量存活时间”,但反其道行之,既然正常不存在循环引用的情况下,不会执行对应方法。那么如果方法有执行,就说明存在循环引用?
FBRetainCycleDetector + MLeaksFinder 阅读中,指出MLeaksFinder关键源码中,就有这样的写法:

- (BOOL)willDealloc {
     NSString *className = NSStringFromClass([self class]);
     if ([[NSObject classNamesWhitelist] containsObject:className]) return NO; 
     NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey);
     if ([senderPtr isEqualToNumber:@((uintptr_t)self)]) return NO; 
      __weak id weakSelf = self;
     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong id strongSelf = weakSelf;
       [strongSelf assertNotDealloc]; 
        //此处,如果方法有响应,说明对象没有正常释放,存在内存泄露情况!!
     });
     return YES;
}

(flag: MLeaksFinder源码之后抽空学习总结!)

以上~如有错误还望不吝指出。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容

  • Block使用场景,可以在两个界面的传值,也可以对代码封装作为参数的传递等。用过GCD就知道Block的精妙之处。...
    Coder_JMicheal阅读 715评论 2 1
  • 前言 Blocks是C语言的扩充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了这...
    小人不才阅读 3,751评论 0 23
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,537评论 18 399
  • iOS代码块Block 概述 代码块Block是苹果在iOS4开始引入的对C语言的扩展,用来实现匿名函数的特性,B...
    smile刺客阅读 2,327评论 2 26
  • 摘要 Blocks是C语言的扩充功能, iOS 4中引入了这个新功能“Blocks”,那么block到底是什么东西...
    CholMay阅读 1,141评论 2 10