ios 底层原理Block

1,468 阅读16分钟

前言

写这篇文章挣扎了很久,感觉很难简短而又精辟的写好block这篇文章,初探时走了一遍源码也看了一些帖子,但是探索完就忘了没什么感觉,只知道当需要修改block捕获的外部变量时,变量需要加__block修饰,但是为什么呢?block作为属性时,一般都是用copy修饰,但是又为什么呢?block最头疼的问题莫过于循环引用,我们总是无脑的用weak解决这个问题,但是弱引用真的是万能的吗?

Block是什么?

随便写一个block,clang编译一下看下底层结构

int a=10;
void(^Myblock)(void)=^{
   NSLog("%d",a);
};
Myblock();

clang编译后的底层结构

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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
}
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)};

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        void(*Myblock)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
      ((__block_impl *)Myblock)->FuncPtr)((__block_impl *)Myblock);
    }
    return 0;

}

分析:

  • block代码块最后被编译成了__main_block_impl_0结构体,结构体本质上就是对象,所以block也是一个有引用计数的对象
  • impl.FuncPtr = fp,结构体的构造函数把函数的实现fp赋值给了imp,也就是说block结构体保存了函数的实现,通过Myblock->FuncPtr(Myblock)调起函数实现说明block光申明不调用是没有用的。
  • impl.isa = &_NSConcreteStackBlock,说明block是存在于栈中的,但其实这个block是堆block,但为什么编译的时候是栈block?其实想想也能大概明白,堆内存是动态添加的,编译的时候不可能分配堆内存,此时只是临时的分配成栈block

Block类型

既然block是对象,那么它在哪个内存区域呢?

全局block

void (^block)(void) = ^{
       
};
//或者
static int a=10;
void (^block)(void) = ^{
     NSLog(@"%ld",a);  
};

NSGlobalBlock全局block:在block内部不使用外部变量,或者只使用静态变量和全局变量

栈block

int b=10;
void (^__weak mblock)(void) = ^{
   NSLog(@"%ld",b);
};

NSStackBlock栈block:在栈区,栈内有效,出栈后销毁。在内部使用外部变量,调用函数的时候,会在栈上开辟一块内存,函数执行完后,这块内存就销毁了也就是出栈了。所以不能赋值给强引用或者copy修饰的变量,因为函数调用完就销毁了,强引用的话会造成野指针。ARC下不特别指定,一般不会存在栈block

堆block

int b=10;
void (^mblock)(void) = ^{
   NSLog(@"%ld",b);
};

NSMallocBlock堆block:在堆区,可以使用外部变量。堆block可以被强引用和copy,它的销毁取决于引用计数,跟我们创建对象一样,引用计数为0时就销毁。示例中mblock初始化后,其本身就是一次强引用。实际开发中百分之99的情况都是堆block,所以堆block是我们关注的重点

Block引用外部变量

写个demo控制台打印情况如下:

//新建一个LGTeacher对象
@interface LGTeacher : NSObject
@property (nonatomic, copy) NSString *hobby;
- (void)teacherSay;
@end

LGTeacher* lgter=[[LGTeacher alloc]init];
NSLog(@"%@-%p",lgter,&lgter);
void(^Myblock)(void)=^{
    lgter.hobby=@"test";
    NSLog(@"%@-%p",lgter,&lgter);
};
 Myblock();

输出:

<LGTeacher: 0x101218ad0>-0x7ffeefbff200
<LGTeacher: 0x101218ad0>-0x7ffeefbff1e8

分析:block外部lgter指针地址是0x7ffeefbff200block内部lgter指针地址是0x7ffeefbff1e8,虽然他们都是指向相同的对象,但是指针地址却不相同,说明block内部的lgter已经是一个全新的变量,如果在block内部让lgter=nil,此时编译器会报错,因为我们的本意是想改变外部lgter的指针地址,但是在block内部此时的lgter已经不是外部的lgter,此时编译器会混乱。虽然block外部的指针地址和block内部的指针地址不一样,但是他们指向的是同一块内存,所以依然可以改变这块内存的值,比如此处lgter.hobby=@"test",这就是常说的值传递,不能改变外部指针,但是可以改变指针所指向的对象的值。

再来看一下使用__block修饰的情况。

@interface LGTeacher : NSObject
@property (nonatomic, copy) NSString *hobby;
- (void)teacherSay;
@end

__block LGTeacher* lgter=[[LGTeacher alloc]init];
NSLog(@"%@-%p",lgter,&lgter);
void(^Myblock)(void)=^{
    lgter.hobby=@"test";
    NSLog(@"%@-%p",lgter,&lgter);
    lgter=nil;
};
 Myblock();

输出:

<LGTeacher: 0x1007275d0>-0x7ffeefbff1f0
<LGTeacher: 0x1007275d0>-0x7ffeefbff1f0

block外部lgter指针地址是0x7ffeefbff1f0block内部lgter指针地址也是0x7ffeefbff1f0,它们指向同一个LGTeacher对象0x1007275d0。说明使用__block修饰的对象,在block内部变量的指针地址没有发生改变,所以可以使用lgter=nil改变指针,因为内外部变量的指针是同一个,这就是常说的地址传递。

why?下面通过clang编译一下看下底层使用__block修饰和不使用__block有什么区别 image.png 这是没有使用__block修饰的底层结构,block是一个结构体,结构体内部有一个属于结构体的对象lgter,外部的lgter对象作为参数传进block结构体的构造函数中,在构造函数中把外部的lgter赋值给block内部的lgter,注意这只是赋值block内部的lgter在内存分配时有自己的指针地址

再看一下使用__block修饰的对象 image.png

  • 使用__block修饰的对象LGTeacher已经被编译成了__block_byref_lgter_0结构体,在block构造函数中,把参数_block_byref_lgter_0结构体指针的__forwarding赋值给block中属性_block_byref_lgter_0,而__forwarding又是一个指向自己的__block_byref_lgter_0指针
  • 虽然有点拗口,但是好好理解一下,简单总结就是这是一个指针的传递,最终都指向了_block_byref_lgter_0结构体。_forwarding既可以指向自己,也可以指向复制后的自己,也就是说有了这个指针的存在,无论__block变量配置在堆上还是栈上都能够正确的访问__block变量。
  • 仔细看一下如果使用__block修饰的对象,对象被编译成结构体的同时会有一个__Block_byref_id_object_copy方法和___Block_byref_id_object_dispose方法,下面会分析到。

总结

  • __block修饰的对象是指针传递,无论__block变量配置在堆上还是栈上都能够正确的访问__block变量。
  • 没有用__block修饰的对象是值传递,block内部的变量和外部的变量不是同一个指针,只是值相同或者说指向的对象相同

block的循环引用

先想一下我们在申明一个block属性时,为什么总是用关键字copy修饰?

我们称block为代码块,他类似一个方法,方法是在内存的栈区,执行完就消失,它的内存管理是系统自动管理的。block不像OC中的类对象(在堆区),他也是在栈区的,我们没办法像控制对象一样控制他的消亡,所以我们常使用copy修饰block,系统会把该 block的实现拷贝一份到堆区,这样我们就可以像控制对象一样控制它的生命周期,在ARC,不使用copy修饰的block,系统也会拷贝block进堆区,所以说现在如果不特别制定block都是堆block

还记得一开始我们写的那个demo吗,明明是一个堆block,但是编译的时候是一个栈block,那么是栈block是怎么变成堆block的呢?

int a=10;
void(^Myblock)(void)=^{
  NSLog("%d",a);
};
Myblock();

汇编看一下调用了调用过程 image.png 汇编看流程,刚开始会调用objc_retainBlock,跟进objc_retainBlock里又会调用Block_copy,我们研究的不就是堆block的拷贝吗?源码看下这个函数

void *_Block_copy(const void *arg) {
    //block底层正在的结构是一个Block_layout
    struct Block_layout *aBlock;
    if (!arg) return NULL;
    aBlock = (struct Block_layout *)arg;
    //block是否需要释放
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    //全局block直接返回
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // 栈 - 堆 (编译期),栈block的拷贝,堆block都是从栈拷贝过来的
        size_t size = Block_size(aBlock);
        //开辟堆block内存
        struct Block_layout *result = (struct Block_layout *)malloc(size);
        if (!result) return NULL;
        //把栈block拷贝进堆block中
        memmove(result, aBlock, size); 
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        result->invoke = aBlock->invoke;
        //省略...
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 2// logical refcount 1
        //获取copy函数,其实是对对象的拷贝
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

分析:

  • 我们上面通过clang分析了block底层结构是一个结构体,这个结构体具体的结构其实是Block_layout结构体。
  • 如果aBlock是全局blockcopy是直接返回
  • 如果aBlock是栈block,那么就会在堆区malloc开辟内存,并且把栈区block拷贝进新开辟的堆区Block_call_copy_helper获取copy函数,copy里调用了_Block_object_assign方法改变外部对象的引用计数或内存拷贝。

_Block_object_assign

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      //对象 引用计数加1
      case BLOCK_FIELD_IS_OBJECT:
        _Block_retain_object(object);
        *dest = object;
        break;
        //block类型,进行_Block_copy
      case BLOCK_FIELD_IS_BLOCK:
        *dest = _Block_copy(object);
        break;
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      //__block修饰的对象 会拷贝整个结构体进堆区
      case BLOCK_FIELD_IS_BYREF:
        *dest = _Block_byref_copy(object);
        break;
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        *dest = object;
        break;
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        *dest = object;
        break;
      default:
        break;
    }
}
  • 如果外部变量是对象_Block_retain_object使外部对象引用计数加1,所以在block内使用外部变量,比如self就需要使用弱引用,否则block内部会强引用self使得引用计数加1
  • 如果外部变量是Block类型的,执行_Block_copy拷贝block,这里一定要注意,这里有坑点,下面会有例子。
  • 如果外部变量是__block修饰的,__block修饰的对象会被编译成一个block_byref结构体,那么会拷贝整个结构体进堆区

示例分析

经过上面对block的分析,我们已经有一个大概的认知与理解,其最终目的还是要在开发中使用和避免犯错,下面看几个示例巩固和理解。

对象的引用计数理解

    NSObject *objc = [NSObject new];
    NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(objc))); // 1
    void(^strongBlock)(void) = ^{ 
        NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
    };
    strongBlock();
    void(^__weak weakBlock)(void) = ^{ // + 1
        NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
    };
    weakBlock();
    void(^mallocBlock)(void) = [weakBlock copy];
    mallocBlock();

输出:

1
3
4
5

分析:上面输出objc对象的引用计数,第一次输出1能理解,第二次输出为什么是3呢?上面我们分析得出如果外部变量不使用__block修饰,那么在block内部是值传递,会生成一个新的指针变量指向这个对象,所以对象的引用计数会加1,strongBlock编译底层其实是栈block,运行时栈block拷贝进堆区调用_Block_retain_object函数使得对象的引用计数又加1,所以是3。至于第三次输出,特别指定这是一个栈block,其内部又会有一个新的指针变量指向objc,所以加1。第四次,将一个栈block拷贝进堆区,对象引用计数加1。

如果把上面的对象使用__block修饰呢?

   __block NSObject *objc = [NSObject new];
    NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(objc))); // 1
    void(^strongBlock)(void) = ^{ 
        NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
    };
    strongBlock();

输出:

1

分析:可以看到引用计数永远为1,并不会使对象的引用计数增加,因为这是指针传递。还记得上一篇关于弱引用的文章吗,weak可以解决block循环引用的本质就是不会使对象的引用计数增加,而且弱引用对象的引用计数是系统的弱引用表管理的。这里是不是感觉__block也有相同的作用,也可以解决循环引用。

内存拷贝的理解

    int a = 0;
    void(^ __weak weakBlock)(void) = ^{
        NSLog(@"-----%d", a);
    };
    struct _LGBlock *blc = (__bridge struct _LGBlock *)weakBlock;
    // 深浅拷贝
    id __strong strongBlock = [weakBlock copy];
    blc->invoke = nil;
    void(^strongBlock1)(void) = strongBlock;
    strongBlock1();

输出

0

分析:weakBlock栈block,blc通过类型强转也是一个跟weakBlock指向同一片内存空间的栈block,strongBlock是weakBlock的内存拷贝是堆block,调用blc->invoke=nil把栈block内存清空,但是这不影响堆block的内存,所以能够输出

堆栈释放差异

- (void)blocktest{
    NSObject *a = [NSObject alloc];
    //仔细看此处的block不是一个块
    void(^weakBlock)(void) = nil;
    {//临时作用域
        // 栈block
        void(^__weak strongBlock)(void) = ^{
            NSLog(@"---%@", a);
        };
        weakBlock = strongBlock;
        NSLog(@"1 - %@ - %@",weakBlock,strongBlock);

    }
    weakBlock();
  }

输出:

 1 - <__NSMallocBlock__: 0x281e830c0> - <__NSStackBlock__: 0x16b80cf10>
 
---(null)

分析:weakBlock不做特别指定是一个堆block临时作用域strongBlock是一个栈block,堆block被赋值成栈block,注意这里不是copy,只是指向同一片内存空间的赋值。分别打印__NSMallocBlock__和__NSStackBlock__类型能理解,为什么打印a是null呢?lldb断点看下 image.png 在81行打断点,此时weakBlockstrongBlock指向同一片栈内存空间,但是断点过了82行后,此时strongBlock栈内存被释放了,意思strongBlock生命周期在临时作用域内,出了作用域就释放了,既然栈内存释放了,堆block又指向这片内存,为什么会打印null呢?不应该是野指针吗,这点目前还不清楚,希望知道的指导一下。要想不打印为null,可以这样weakBlock = 【strongBlock copy】把栈block拷贝进堆,这里一定要注意copy和“=”的本质区别,"="指向的是同一块内存空间

避免循环引用

造成循环引用的本质是对象的引用计数始终不为0,导致对象该释放的时候释放不掉,从而导致内存泄漏。所以我们可以使用weak或者__block修饰对象,让对象的引用计数不增加就可以避免循环引用。如下

//方式1
__weak typeof(self) weakself=self;
void(^astrongBlock)(void)=^(void){
__strong typeof(weakself) strongself=weakself;
       NSLog(@"count=%ld",(long)CFGetRetainCount((__bridge CFTypeRef)strongself));
    };
astrongBlock();
//方式2
__block ViewController* vc=self;
void(^bstrongBlock)(void)=^(void){
       NSLog(@"count=%ld",(long**)CFGetRetainCount((__bridge CFTypeRef)vc));
        vc=nil;
    };
 bstrongBlock();
//方式3
 void(^cstrongBlock)(UIViewController*  vc)=^(UIViewController*  vc){
       NSLog(@"count=%ld",(long)CFGetRetainCount((__bridge CFTypeRef)vc));
    };
 cstrongBlock(self);

分析:

  • 方式1使用weak修饰对象,不会使self的引用计数增加,同时弱引用对象weakself是系统弱引用表管理的,注意我们一般会在block内部使用__strong再强引用一次self,此时self引用计数会加1,如果我们在block中操作了一些耗时的工作,但是self被提前释放了,那么弱引用对象weakself也就释放了,那么就会造成奔溃,而在block内部__strong修饰的是一个零时变量,出了作用域就自动销毁了,self引用计数又会减1。
  • 方式2使用__block修饰一个对象,不会增加对象的引用计数,注意一定要在block内部把这个对象释放掉,weak弱引用对象的释放是系统完成的。
  • 方式3通过传递一个临时变量,我们知道临时变量是在栈区的,是系统自动释放了

强弱共舞是万能的吗?

我们经常使用__weak__strong组合解决block循环引用,但这真的是万能的吗?看下面这个例子会闷逼,定义两个block作为控制器self的属性,即doWorkblockdoStudentblock,分别打印selfstrongself的引用计数

__weak typeof(self)weakSelf =self;
NSLog(@"self引用计数:%ld",CFGetRetainCount((__bridge CFTypeRef)self));
self.doWorkblock = ^{
    __strong typeof(self) strongSelf = weakSelf;
    NSLog(@"strongSelf引用计数:%ld",CFGetRetainCount((__bridge CFTypeRef)strongSelf));
    weakSelf.doStudentblock = ^{
       NSLog(@"strongSelf引用计数:%ld",CFGetRetainCount((__bridge CFTypeRef)strongSelf));
    };
   weakSelf.doStudentblock();
 };
self.doWorkblock()
NSLog(@"self引用计数:%ld",CFGetRetainCount((__bridgeCFTypeRef)self));

控制台输出:

self引用计数:21
strong引用计数:22
strong引用计数:25
self引用计数:22

分析:控制台可以看到一开始self应用计数为21,最后self引用计数为22,很明显造成了循环引用,why?

  • strongSelf是一个临时变量,强持有self,所以引用计数+1,为22。进入doStudentblock后,在block内部引用外部变量,根据上面的分析引用计数应该+2后是24,但是为什么是25呢?特别注意doWorkblock捕获了外部变量doStudentblock,当外部变量是block类型时,会拷贝这个block进堆区,doStudentblock里对strongself强引用即对self强引用,所以引用计数又会加1,所以为25。
  • 如果doStudentblock不作为属性呢?在doWorkblock内部自定义一个doStudentblock就不会造成循环引用,因为不存在block拷贝
  • 如果在doStudentblock里面不引用Strongself会造成循环引用吗?如果不强引用strongself,在block拷贝时就不会拷贝strongself,自然就不会使引用计数增加

NSTimer循环引用

有必要把NSTimer造成循环引用单独拎出来说一下的,一个是比较特殊一个是使用的时候会不注意。首先NSTimer是基于RunLoop的,以scheduledTimerWithTimeInterval:开头的方法会将NSTimer加到当前runloopdefault mode上,而以timerWithTimeInterval:开头的方法,则需要使用runloop的addTimer:方法,将其手动加到runloop上。因此,这里通常有一个注意的点:即runloop的UITrackingMode下,定时器会失效。解决办法即将定时器加到runloopcommonModes上即可。

写个会造成循环引用的demo如下:

image.png 控制器A push进控制器testvc中,在testvc中创建一个mytimer定时器,当控制器testvc pop返回上一个控制器时,理论上会调用dealloc方法,但实际情况并不会调用dealloc方法,因为mytimer强引用self,self又强引用mytimer,如果此时repeats=NO,并不会造成循环引用,但是一旦repeats=YES,那么mytimer对象就要一直持有self,从而造成循环引用,循环引用下self不会释放,只有self释放了才会调用dealloc方法,所以dealloc方法可以检测self是否正常释放。那么如何才能让dealloc正常调用呢?

中间对象解循环

image.png 分析:可以定义个中间对象,注意这个中间对象必须强引用,不能是一个临时对象,否则执行完就销毁了,runtime动态给中间对象添加timego方法,timer的target设置为这个中间对象,这样就可以解除self循环引用,dealloc也能够正常调用。

消息转发解循环

@interface TimeCenterObj()
@property(nonatomic,weak)id weakobj;
@end

@implementation TimeCenterObj
+(instancetype)initWithObjc:(id)weakobj{
    TimeCenterObj* myobj= [[TimeCenterObj alloc]init];
    myobj.weakobj=weakobj;
    return myobj;
}
-(id)forwardingTargetForSelector:(**SEL**)aSelector{
    return self.weakobj;
}

分析:创建一个中间对象TimeCenterObj,定义一个弱引用weakobj去持有self,利用弱引用的特性解除循环,最最主要的是要定义一个消息快速转发的方法forwardingTargetForSelector(原理见文章消息转发),在这个方法里指定处理这个方法的实体为weakobj,也就是self本身。当然也可以添加慢速转发方法

使用如图: image.png

block弱引用解循环

ios10.0之后,苹果应该是意识到NSTimmer会造成循环引用,所以添加了一个新的API,可以在这个新API里弱引用self。

 __weak typeof(self) weakself=self;
 self.mytimer=[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakself  timego];
    }];
 [self.mytimer fire];

但是这个API是ios10.0之后才有的,所以要兼容ios10.0以下的话需要使用分类动态创建一个方法,这里也推荐使用这种方法来解决NSTimer的循环引用

@implementation NSTimer (weakTimer)

+(NSTimer*)wgy_scheduledTimerWithTimeInterval:(NSTimeInterval)time repeats:(BOOL)repeat block:(void(^)(NSTimer * _Nonnull))block{
   return  [self scheduledTimerWithTimeInterval:time target:self selector: @selector(wgy_time:) userInfo:[block copy] repeats:repeat];
}

+(void)wgy_time:(NSTimer*)time{
    void(^timeblock)(NSTimer* time)=time.userInfo;
    if(timeblock){
        timeblock(time);
    }
}

注意:这里把target设置为NSTimer自己,SEL为添加的类方法wgy_time,在类方法里调用传进来的block