iOS Block浅浅析

栏目: IOS · 发布时间: 7年前

内容简介:Block真的难,笔者静下心来读《Objective-C 高级编程 iOS与OS X多线程和内存管理》,读的时候顺便记录下来自己的心得,方便以后再翻回,也希望能带给大家一些帮助。本文将以一个菜dog的角度,从 Block 不截获变量、截获变量不修改、截获并修改变量 、 截获对象 四个层次 浅浅探究Block的实现。Block的语法就不回顾了,不好记Block语法可以翻这篇

Block真的难,笔者静下心来读《Objective-C 高级编程 iOS与OS X多线程和内存管理》,读的时候顺便记录下来自己的心得,方便以后再翻回,也希望能带给大家一些帮助。

本文将以一个菜dog的角度,从 Block 不截获变量、截获变量不修改、截获并修改变量 、 截获对象 四个层次 浅浅探究Block的实现。

Block的语法就不回顾了,不好记Block语法可以翻这篇 How Do I Declare A Block in Objective-C? 。

Block实现

转成C++ 的源代码学习,笔者加了适当的注释方便理解。

不截获自动变量值

int main()
{
    void (^blk)(void) = ^{printf("Block\n");};
    
    blk();
    
    retrun 0;
}
复制代码

将转为

// block中通用的成员变量 结构体
// 文章后面的代码不再给出,但都有用到
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

// 代表Block 的结构体
struct __main_block_impl_0 {
    struct __block_impl impl;// block通用的成员变量
    struct __main_block_desc_0* Desc;// block 的大小
    
    // 构造函数
    __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;
    }
};

// 原本的代码块 转到一个C函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
    printf("Block\n");
}

// 计算block大小的结构体
// 声明的同时,初始化一个变量__main_block_desc_0_DATA
static struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long Block_size;
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0)
};

int main()
{
    // 声明定义block
    // 用到了构造函数方法
    void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
    /*
        相当于以下
        struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
        
        struct __main_block_impl_0 *blk = &tmp;
        
        栈上生成的结构体实例的指针,赋值给变量blk。
    */
    
    // 调用block
    // 第一个参数为 blk_>FuncPtr,即C函数
    // 第二个参数为 blk本身
    ((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
    /*
        相当于以下
        普通的C函数调用
        (*blk->impl.FuncPtr)(blk);
    */
    
    return 0;
}
复制代码

即把原本的代码块,转到一个C函数中。并且创建一个 代表Block 的结构体,最后一个构造函数,Block对象把函数和成员绑定起来。

截获自动变量不修改的情况

和以上区别在于,Block结构体中的成员变量多了截获的自动变量,并且构造函数参数也是。

int main()
{
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    void (^blk)(void) = ^{printf(fmt, val);};
    
    val = 2;
    fmt = "These values were changed.val = %d\n";
    
    blk();
    
    return 0;
}
复制代码

将转为

// 跟上面一样
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

// 代表Block 的结构体
struct __main_block_impl_0 {
    struct __block_impl impl;// block通用的成员变量
    struct __main_block_desc_0* Desc;// block 的大小
    
    // 截获的自动变量
    // 结构体中有名字一样的成员变量
    const char *fmt;
    int val;
    
    // 构造函数
    // 参数多了截获的自动变量
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags = 0) : fmt(_fmt), val(_val) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

// 原本的代码块 转到一个C函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
    const char *fmt = __cself->fmt;
    int val = __cself->val;
    
    printf(fmt, val);
}

// 计算block大小的结构体
// 声明的同时,初始化一个变量__main_block_desc_0_DATA
static struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long Block_size;
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0)
};

int main()
{
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    void (*blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, fmt, val);
    /*
        结构体初始化如下:
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = 0;
        impl.FuncPtr = __main_block_func_0;
        Desc = &__main_block_desc_0_DATA;
        fmt = "val = %d\n";
        val = 10;
    */
    
    return 0;
}
复制代码

根据以上,我们知道截获变量后,实质上是Block结构体中有一个成员变量存了起来。调用Block时,是访问取结构体成员变量,而不是外面的局部变量。

iOS Block浅浅析

Block中修改值

Block不允许修改外部变量的值。Apple这样设计,应该是考虑到了block的特殊性,block也属于“函数”的范畴,变量进入block,实际就是已经改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。又比如我想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。于是栈区变成了红灯区,堆区变成了绿灯区。

iOS Block不能修改外部变量的值,指的是栈中指针的内存地址。下面举几个例子理解。

  • 非OC对象,修改会编译错误。
int val = 0;
void (^blk)(void) = ^{
    val = 1;
};
复制代码
  • OC对象,发送消息可以,但改指针内存地址不行。

以下没问题

id array = [[NSMutableArray alloc] init];
    
    void (^blk)(void) = ^{
        id obj = [[NSObject alloc] init];
        [array addObject:obj];
    };
复制代码

以下编译报错

id array = [[NSMutableArray alloc] init];
    
    void (^blk)(void) = ^{
        array = [[NSMutableArray alloc] init];
    };
复制代码
  • C 数组 截获自动变量的方法没有实现对 C语言 数组的截获。

以下编译错误

const char text[] = "hello";
    
    void (^blk)(void) = ^{
        printf("%c\n", text[2]);
    };
复制代码

需改成指针

const char *text = "hello";
    
    void (^blk)(void) = ^{
        printf("%c\n", text[2]);
    };
复制代码

那么Block 要怎么修改变量呢?

方法一:用到静态或全局变量

  • C 中有一个变量,允许Block改写值。

    • 静态变量
    • 静态全局变量
    • 全局变量
  • 例子

int global_val = 1;// 全局变量
static int static_global_val = 2;// 静态全局变量

int main()
{
    static int static_val = 3;// 静态变量
    
    void (^blk)(void) = ^{
        global_val *= 1;
        static_global_val *= 2;
        static_val *= 3;
    }
    
    return 0;
}
复制代码

转换后

int global_val = 1;
static int static_global_val = 2;

// 代表Block 的结构体
struct __main_block_impl_0 {
    struct __block_impl impl;// block通用的成员变量
    struct __main_block_desc_0* Desc;
    // 成员变量只多了静态变量,原因在后面分析
    int *static_val;
    
    // 构造函数
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_staitc_val, int flags=0) : static_val(_static_val) {
        impl.isa = &_NSConcreteStackblock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

// 原本的代码块 转到一个C函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int *static__val = __cself->static_val;
    
    global_val *= 1;
    static_global_val *= 2;
    (*static_val) *=3;
}

// 计算block大小的结构体
// 声明的同时,初始化一个变量__main_block_desc_0_DATA
static struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long Block_size;
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0)
};

int main()
{
    static int static_val = 3;
    blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &static_val);
    
    return 0;
}
复制代码

为什么成员变量只多了静态变量呢?

这就要先了解 iOS 内存区域。 iOS-MRC与ARC区别以及五大内存区

  • 栈:
    • 由系统管理分配和释放
    • 存放函数参数值,局部变量值
    • iPhone 的栈区只有512K
    • 局部变量在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用结束后,变量就被撤销,其所占用的内存也被收回。
  • 堆:
    • 由程序猿管理
    • 存放程序猿创建的对象
    • C用malloc/calloc/relloc分配的区域
  • 代码区:
    • 存放函数的二进制代码
  • 全局区(又称静态区):
    • 存放全局变量和静态变量
    • 程序运行时一直存在
    • 由编译器管理(分配释放),程序结束后由系统释放

全局区又分为 BSS段 和 数据段(data)。

BSS段:BSS段(bss segment)通常是指用来存放程序中未初始化的或者初始值为0的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。

数据段:数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。

但不同的是C++中,不区分有没有初始化,都放到一块去。

  • 文字常量区
    • 存放常量字符串
    • 为了节省内存,C/C++/OC把常量字符串放到单独的一个内存区域。当几个指针赋值给相同的常量字符串时,它们实际上会指向相同的内存地址。
iOS Block浅浅析

再回到刚刚的代码上,为什么block结构体中成员变量只多了静态变量呢?

int global_val = 1;// 全局变量
    static int static_global_val = 2;// 静态全局变量
    static int static_val = 3;// 静态变量
复制代码

关于它们的区别—— 全局变量/静态全局变量/局部变量/静态局部变量的异同点

静态局部变量虽然程序运行时一直存在,但只对定义自己的函数体始终可见。

编译后,调用block实质上是在 一个新定义的函数 中访问静态局部变量,不能直接访问,所以需要保存其指针。而全局变量可以访问到,所以没有加到成员变量中。

方法二:用到__block 说明符

int main()
{
    __block int val = 10;
    
    void (^blk)(void) = ^{val = 1;};
    
    return 0;
}
复制代码

转换后

// 变量将会变成的结构体
// 即val不是int类型,变成此结构体实例
struct __Block_byref_val_0 {
    void *__isa;// __block变量转化后所属的类对象
    __Block_byref_val_0 *__forwarding;//指向__block变量自身的指针,后面解释
    int __flags;// 版本号
    int __size;// 结构体大小
    int val;// 原本的int数值
};

// 代表Block 的结构体
struct __main_block_impl_0 {
    struct __block_impl impl;// block通用的成员变量
    struct __main_block_desc_0* Desc;// block 的大小
    __Block_byref_val_0 *val;// val转成成员变量,类型为结构体
    
    // 构造函数
    __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;
    }
};

// 原本的代码块 转到一个C函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
    __Block_byref_val_0 *val = __cself->val;
    
    // 这里通过__forwarding赋值?后面解释
    (val->__forwarding->val) = 1;
}

// 当Block从栈复制到堆时
// 通过此函数把截获的__block变量移到堆或者引用数+1
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src)
{
    _Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}

// 当Block从堆被废弃时
// 通过此函数把截获的__block变量引用数-1
// 相当于对象的delloc方法
static void __main_block_dispose_0(struct __main_block_impl_0*src)
{
    _Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}

// 计算block大小的结构体
// 该结构体有两个函数
// copy 和 dispose
// 声明的同时,初始化一个变量__main_block_desc_0_DATA
static struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long 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()
{
    /*
        val变成了__Block_byref_val_0结构体实例
    */
    __Block_byref_val_0 val = {
        0,// isa指针
        &val,//forwarding成员,指向自己
        0,// 版本号
        sizeof(__Block_byref_val_0),
        10 //原来int val的值
    };
    
    blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);
    
    return 0;
}
复制代码

从main函数中,我们可以发现,Block转换成Block的结构体类型**__main_block_impl_0的自动变量,__block变量val转换为block变量的结构体类型__Block_byref_val_0**的自动变量。它们都在栈上。所以Block的isa指针指向NSConcreteStackBlock。

除了NSConcreteStackBlock,还有两种类型 NSConcreteGlobalBlock 和 NSConcreteMallocBlock。

类 设置对象的存储域
NSConcreteStackBlock 栈
NSConcreteGlobalBlock 全局区
NSConcreteMallocBlock 堆
  • NSConcreteGlobalBlock 在全局变量的地方生成的Block为NSConcreteGlobalBlock,如下。在全局变量的地方不能使用自动变量,也就不存在截获的问题。
void (^blk)(void) = ^{printf("Global Block\n");};

int main
{
    return 0;
}
复制代码

另外只要没有截获自动变量,Block类型就是NSConcreteGlobalBlock。

  • NSConcreteMallocBlock 栈上的Block,在出了作用域后会被摧毁,__block变量也是。那么如果我们要在别的地方调用Block,就需要把它们移到堆中,手动管理它们的生命周期。这种Block类型就是NSConcreteMallocBlock。

先来理解为什么有个forwarding指向自己。

试想,Block如果截获了自动变量,然后移到堆上,在别的作用域调用(很常见)。如果__block变量在栈上已经释放了,Block访问__block变量会失败。所以系统需要在Block变成NSConcreteMallocBlock时,截获的__block变量也复制到堆上。

Block什么时候会复制到堆上呢?

  • 调用Block的copy方法
  • 将Block作为函数返回值时
  • 将Block赋值给__strong修饰的变量时
  • 向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时
__block变量的配置存储域 Block从栈赋值到堆时的影响
栈 从栈复制到堆并被Block持有
堆 被Block持有

当Block从栈复制到堆时,__block变量的 forwarding 会重新指向其在堆中的内存地址。

这样,无论是在Block语法中、Block语法外使用__block变量,还是__block变量配置在栈上或对上,都可以顺利地访问同一个__block变量。

笔者在书上刚看到这句话时,有点晕,后来想了一段时间应该是以下意思,如果有误,欢迎大神批斗。

如下代码,有注释

__block int val = 0;
    
    void (^blk)(void) = [^{++val;} copy];
    
    ++val;// 转换为++(val.__forwarding->val);即(栈上的val).__forwarding->val,最终指向堆上的val
    
    blk();// 转换为++(val.__forwarding->val);即(堆上的val).__forwarding->val,最终指向堆上的val
    
    NSLog(@"%d", val);
复制代码

截获对象

  • __strong 修饰的对象
blk_t blk;

{
    id array = [[NSMutablArray alloc] init];
    blk = [^(id obj) {
        [array addObject:obj];
        NSLog(@"array count = %ld", [array count]);
    } copy];
}

blk([NSObject alloc] init]);
blk([NSObject alloc] init]);
blk([NSObject alloc] init]);
复制代码

还记得上面提到的截获变量不修改,转为C++,Block结构体中的成员变量多了截获的自动变量。

这里,变量作用域结束时,理论上array被废弃,但执行输出结果为数组count123。

这意味着array超出作用域而存在。

会不会也是Block结构体中的成员变量多了截获的自动变量呢?

转换为C++后

struct __main_block_impl_0 {
    struct __block_impl impl;// Block通用的成员变量
    struct __main_block_desc_0* Desc;// Block的大小
    // 指向数组的成员变量
    id __strong array;
    
    // 构造函数
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,id __strong _array, int flags=0) : array(_array) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

// 原本的代码块 转到一个C函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
    id __strong array = __cself->array;
    
    [array addObject:obj];
    
    NSLog(@"array count = %ld", [array count]);
}

// 当Block从栈复制到堆时
// 通过此函数把截获的对象引用数+1
// 相当于retain
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src)
{
    _Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}

// 当Block从堆被废弃时
// 通过此函数把截获的对象release引用数-1
// 相当于对象的delloc方法
static void __main_block_dispose_0(struct __main_block_impl_0 *src)
{
    _Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);
}

// 计算block大小的结构体
// 该结构体有两个函数
// copy 和 dispose
// 声明的同时,初始化一个变量__main_block_desc_0_DATA
static struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long 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
};
复制代码

调用block转换如下。

blk_t blk;

{
    id __strong array = [[NSMutableArray alloc] init];
    
    // 构造函数
    blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 0x22000000);
    
    blk = [blk copy];
}

// 调用,第一个参数为blk本身,第二个参数为id类型对象
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
复制代码

可以看到,和猜测一样,Block结构体中确实多了一个 id __strong array ;

我们知道,我们写的 C语言结构体不能带有__strong修饰符的变量 。原因是编译器不知道何时进行C语言结构体的初始化和废弃操作。

但是 OC运行时库 能 把握 Block从栈复制到堆以及堆上的Block被 废弃的时机 ,因此Block用结构体中可以 管理好 。

那么同时用__block 和 __strong 修饰的对象呢?

上面提到过__block int val,val将变为一个结构体,对象也一样。

__block id obj = [[NSObject alloc] init];
// 相当于__block id __strong obj = [[NSObject alloc] init];
复制代码

转换为

// 对象将会变成的结构体
struct __Block_byref_obj_0 {
    void *__isa;// __block变量转化后所属的类对象
    __Block_byref_val_0 *__forwarding;//指向对象自身的指针,后面解释
    int __flags;// 版本号
    int __size;// 结构体大小
    void (*__Block_byref_id_object_copy)(void*, void*);// retain对象
    void (*__Block_byref_id_object_dispose)(void*);// release对象
    __strong id obj;//指向对象
};

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
    _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}

static void __Block_byref_id_object_dispose_131(void *src) {
    _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

// 对象变成了__Block_byref_obj_0结构体实例
__Block_byref_obj_0 obj = {
    0,
    &obj,
    0x2000000,
    sizeof(__Block_byref_obj_0),
    __Block_byref_id_object_copy_131,
    __Block_byref_id_object_dispose_131,
    [[NSObject alloc] init]
};
复制代码
  • __weak 修饰的对象
blk_t blk;

{
    id array = [[NSMutableArray alloc] init];
    __block id __weak array2 = array;
    
    blk =[^(id obj) {
        [array2 addObject:obj];
        NSLog(@"array count = %ld", [array2 count]);
    } copy];
}

blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
复制代码

输出结果为数组数目0。

这是由于array在作用域结束时被释放、废弃,nil被赋值在array2中。

结论:Block中持有weak声明的对象,对象引用数不会增加。

问题

  • Block中是否需要对弱引用的对象强引用?

到底什么时候才需要在ObjC的Block中使用weakSelf/strongSelf

  • Block属性中内存语义用copy 还是strong?

    在ARC下,这两种效果都会把Block 从栈上压到堆上。但事实上,copy更接近Block的本质。

block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”,他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。你也许会感觉我这种做法有些怪异,不需要写依然写。如果你这样想,其实是你“日用而不知”。

  • 在这篇文章iOS-Block本质,看到许多关于Block理解的问题,对照着实现看挺有帮助。

参考

  • [1] Kazuki Sakamoto,Tomohiko Furumoto.Objective-C高级编程 iOS与OS X多线程和内存管理[M].北京:人民邮电出版社,2013:79-136.

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。

Distributed Algorithms

Distributed Algorithms

Nancy A. Lynch / Morgan Kaufmann / 1996-3-15 / USD 155.00

In "Distributed Algorithms", Nancy Lynch provides a blueprint for designing, implementing, and analyzing distributed algorithms. She directs her book at a wide audience, including students, programmer......一起来看看 《Distributed Algorithms》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具