• iOS之Block总结以及内存管理


    block定义

    struct Block_descriptor {
        unsigned long int reserved;
        unsigned long int size;
        void (*copy)(void *dst, void *src);
        void (*dispose)(void *);
    };
    struct Block_layout {
        void *isa;
        int flags;
        int reserved; 
        void (*invoke)(void *, ...);
        struct Block_descriptor *descriptor;
        /* Imported variables. */
    };

    从上面代码看出,Block_layout就是对block结构体的定义:

    isa指针:指向表明该block类型的类。

    flags:按bit位表示一些block的附加信息,比如判断block类型、判断block引用计数、判断block是否需要执行辅助函数等。

    reserved:保留变量,我的理解是表示block内部的变量数。

    invoke:函数指针,指向具体的block实现的函数调用地址。

    descriptor:block的附加描述信息,比如保留变量数、block的大小、进行copy或dispose的辅助函数指针。

    variables:因为block有闭包性,所以可以访问block外部的局部变量。这些variables就是复制到结构体中的外部局部变量或变量的地址。

    举例,定义一个最简单block 打印hello world:

    int main(int argc, const char * argv[]) {
    
        void (^block)()=^{printf("hello world");};
        block();
        return 0;
    }

    使用clang指令

    clang -rewrite-objc main.m

    得到一个cpp文件,编译后,你就会看到什么是block了

    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) {
    
            printf("hello world");
    }
    
    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)};
    int main(int argc, const char * argv[]) {
    
        void (*block)()=((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;
    }
    static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
    View Code

    你定义完block之后,其实是创建了一个函数,在创建结构体的时候把函数的指针一起传给了block,所以之后可以拿出来调用。

    再看看值捕获的问题

    int main(int argc, const char * argv[]) {
    
        int a=10;
         //__block int a=10; //__block前缀
        void (^block)()=^{printf("打印a=%d",a);};
        block();
        
        return 0;
    }

    定义block的时候,变量a的值就传递到了block结构体中,仅仅是值传递,所以在block中修改a是不会影响到外面的a变量的。

    而加了__block前缀,编译后:

    struct __Block_byref_a_0 {
      void *__isa;
    __Block_byref_a_0 *__forwarding;
     int __flags;
     int __size;
     int a;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_a_0 *a; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_a_0 *a = __cself->a; // bound by ref
    printf("打印a=%d",(a->__forwarding->a));}
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
      void (*dispose)(struct __main_block_impl_0*);
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
    int main(int argc, const char * argv[]) {
    
        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
    
        void (*block)()=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    
        return 0;
    }
    static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
    View Code

    并不是直接传递a的值了,而是把a的地址(&a)传过去了,所以在block内部便可以修改到外面的变量了。

    isa:isa指针,在Objective-C中,任何对象都有isa指针。block 有三种类型:
    _NSConcreteGlobalBlock 全局静态,不会访问任何外部变量,不会涉及到任何拷贝,比如一个空的block。例如:

    #include int main()
    {
        ^{ printf("Hello, World!
    "); } ();
        return 0;
    }

    _NSConcreteStackBlock 保存在栈中,出函数作用域就销毁,例如:

    #include int main()
    {
        char a = 'A';
        ^{ printf("%c
    ",a); } ();
        return 0;
    }

    _NSConcreteMallocBlock 保存在堆中,retainCount == 0销毁

    该类型的block都是由_NSConcreteStackBlock类型的block从栈中复制到堆中形成的。例如下面代码中,在exampleB_addBlockToArray方法中的block还是_NSConcreteStackBlock类型的,在exampleB方法中就被复制到了堆中,成为_NSConcreteMallocBlock类型的block:

    void exampleB_addBlockToArray(NSMutableArray *array) {
        char b = 'B';
        [array addObject:^{
                printf("%c
    ", b);
        }];
    }
    void exampleB() {
        NSMutableArray *array = [NSMutableArray array];
        exampleB_addBlockToArray(array);
        void (^block)() = [array objectAtIndex:0];
        block();
    }

    总结一下:

    _NSConcreteGlobalBlock类型的block要么是空block,要么是不访问任何外部变量的block。它既不在栈中,也不在堆中,我理解为它可能在内存的全局区。

    _NSConcreteStackBlock类型的block有闭包行为,也就是有访问外部变量,并且该block只且只有有一次执行,因为栈中的空间是可重复使用的,所以当栈中的block执行一次之后就被清除出栈了,所以无法多次使用。

    _NSConcreteMallocBlock类型的block有闭包行为,并且该block需要被多次执行。当需要多次执行时,就会把该block从栈中复制到堆中,供以多次执行。

    而ARC和MRC中,还略有不同

    题目:下面代码在按钮点击后,在ARC下会发生什么,MRC下呢?为什么?

    @property(nonatomic, assign) void(^block)();
    
    - (void)viewDidLoad {
        [superviewDidLoad];
        int value = 10;
        void(^blockC)() = ^{
            NSLog(@"just a block === %d", value);
        };
    
        NSLog(@"%@", blockC);
        _block = blockC;
    
    }
    
    - (IBAction)action:(id)sender {
        NSLog(@"%@", _block);
    }

    在ARC 打印:

    mytest[25284:7473527] test:<__NSMallocBlock__: 0x60000005f3e0>
    mytest[25284:7473527] NSShadow {0, -1} color = {(null)}

    虽然不会crash,第二个是野指针

    MRC 会打印:test:<__NSStackBlock__: 0x7fff54941a38> 然后crash

    例如:

     NSArray *testArr = @[@"1", @"2"];
        NSLog(@"block is %@", ^{
            NSLog(@"test Arr :%@", testArr);
            
        });//结果:block is <__NSStackBlock__: 0x7fff54f3c808>
        
        void (^TestBlock)(void) = ^{
            NSLog(@"testArr :%@", testArr);
        };
        NSLog(@"block2 is %@", TestBlock);//block2 is <__NSMallocBlock__: 0x600000045e80>
    //其实上面这句在非arc中打印是 NSStackBlock, 但是在arc中就是NSMallocBlock
    //即在arc中默认会将block从栈复制到堆上,而在非arc中,则需要手动copy.

    循环引用

      Block的循环引用是比较容易被忽视,原本也是相对比较难检查出来的问题。当然现在苹果在XCode编译的层级就已经做了循环引用的检查,所以这个问题的检查就突然变的没有难度了。

      简单说一下循环引用出现的原理:Block的拥有者在Block作用域内部又引用了自己,因此导致了Block的拥有者永远无法释放内存,就出现了循环引用的内存泄漏。下面举个例子说明一下:

    @interface ObjTest () {
        NSInteger testValue;
    }
    @property (copy, nonatomic) void (^block)();
    @end
    
    @implement ObjTest
    - (void)function {
        self.block = ^() {
            self.testValue = 100;
        };
    }
    @end

    在这个例子中,ObjTest拥有了一个名字叫block的Block对象;然后在这个Block中,又对ObjTest的一个成员变量testValue进行了赋值。于是就产生了循环引用:ObjTest->block->ObjTest。

      要避免循环引用的关键就在于破坏这个闭合的环。在目前只考虑ARC环境的情况下,笔者所知的只有一种方法可以破坏这个环:在Block内部对拥有者使用弱引用。

    @interface ObjTest () {
        NSInteger testValue;
    }
    @property (copy, nonatomic) void (^block)();
    @end
    
    @implement ObjTest
    - (void)function {
        __weak ObjTest* weakSelf = self;
        self.block = ^() {
            weakSelf.testValue = 100;
        };
    }
    @end

    在单例模式下 Block避免循环引用,如下:

    @interface Singleton : NSObject
    @property (nonatomic, copy) void(^block)();
    + (instancetype)share;
    @end
    
    @implementation Singleton
    + (instancetype)share {
        static Singleton *singleton;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            singleton = [[Singleton alloc] init];
        });
        return singleton;
    }
    @end
    
    //============分割线=================
    //控制器中代码的实现
    
    @implementation NextViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        __weak typeof(self) weakSelf=self;
        
        void (^blockTest)()=^(){
    //        NSLog(@"print %@", self);//会内存泄漏
            NSLog(@"print %@", weakSelf);
        };
        
        Singleton *singleton = [Singleton share];
        singleton.block = blockTest;
    }
    - (IBAction)btnClick:(UIButton *)sender {
        
        [Singleton share].block();
    }
    
    - (void)dealloc {
        NSLog(@"%s", __FUNCTION__);
    }
    @end

     为什么iOS中系统的block方法可以使用self

    因为:首先循环引用发生的条件就是持有这个block的对象,被block里边加入的对象持有。当然是强引用。
    所以UIView的动画block不会造成循环引用的原因就是,这是个类方法,当前控制器不可能强引用一个类,所以循环无法形成。

    ARC情况下:
    1、如果用copy修饰Block,该Block就会存储在堆空间。则会对Block的内部对象进行强引用,导致循环引用。内存无法释放。

    解决方法:新建一个指针(__weak typeof(Target) weakTarget = Target )指向Block代码块里的对象,然后用weakTarget进行操作。就可以解决循环引用问题。
    2、如果用weak修饰Block,该Block就会存放在栈空间。不会出现循环引用问题。MRC情况下用copy修饰后,如果要在Block内部使用对象,则需要进行(__block typeof(Target) blockTarget = Target )处理。在Block里面用blockTarget进行操作。
    返回值类型(^block变量名)(形参列表) = ^(形参列表) {};调用Block保存的代码block变量名(实参);默认情况下,,Block内部不能修改外面的局部变量Block内部可以修改使用__block修饰的局部变量

    参考 收藏:https://www.zhihu.com/question/30779258/answer/49492783

    Objective-C中的Block原理

    云端之巅 Objective-C中block的底层原理

    iOS学习之block总结及block内存管理(必看)

    Block总结以及内存管理

  • 相关阅读:
    Matlab---绘制柱状图
    认识Caffe与Caffe2
    Matlab---绘图及其位置摆放
    Matlab---三维视图的自动旋转
    Matlab---读取 .txt文件
    Matlab---画图线型、符号及颜色
    day 28 黏包及黏包解决方案
    day 27
    day 26 网络知识 01
    day 25 模块与包
  • 原文地址:https://www.cnblogs.com/dhui69/p/6541324.html
Copyright © 2020-2023  润新知