Objective-C之Block

896 阅读6分钟

1. 前言

Blocks是C语言的扩充功能,可以用一句话来表示Blocks的扩充功能:带有自动自动变量的匿名函数。你可以用block来组合函数表达式,可以将block传递给API,可以在多线程环境使用block和存储block。block包括执行代码以及执行代码期间需要的数据,block可以作为回调函数。block也称为“闭包”。

2. Getting Started with Blocks

2.1 Declaring and Using a Block

你可以使用^操作符来声明一个block variable。block body是在{}内。下面一个例子是定义了block variable。

    int multiplier = 7;
    int (^myBlock)(int) = ^(int num){
        return num * multiplier;
    };

下面是关于上述代码的一些描述,注意到block能够使用同一作用域的变量multiplier。 未命名文件.png 当你定义了一个block变量,则可以把它当作函数一样使用。

printf("%d", myBlock(3));
// prints "21"

2.2 Using a Block Driectly

在多数情况下,你不需要定义block variables。你可以使用block的字面定义直接作为参数。如下面所示:可以看出qsort_b函数的第三个参数就是block的字面定义,而没有定义一个block variable。

char *myCharacters[3] = { "TomJohn", "George", "Charles Condomine" };
 
qsort_b(myCharacters, 3, sizeof(char *), ^(const void *l, const void *r) {
    char *left = *(char **)l;
    char *right = *(char **)r;
    return strncmp(left, right, 1);
});
 
// myCharacters is now { "Charles Condomine", "George", "TomJohn" }

当然你也可以这样做,定义一个block变量,然后将其作为qsort_b函数的第三个参数。

char *myCharacters[3] = { "TomJohn", "George", "Charles Condomine" };
 
(void) (^myBlock) (const void *, const void *) =  ^(const void *l, const void *r) {
    char *left = *(char **)l;
    char *right = *(char **)r;
    return strncmp(left, right, 1);
};

qsort_b(myCharacters, 3, sizeof(char *), myBlock);
 
// myCharacters is now { "Charles Condomine", "George", "TomJohn" }

3. Declaring and Creating Blocks

3.1 Declaring a block Reference

block variable引用一个block,就好像定义一个函数指针,只是用^代替*。下面是所有block variable声明。

void (^blockReturningVoidWithVoidArgument)(void);
int (^blockReturningIntWithIntAndCharArguments)(int, char);
void (^arrayOfTenBlocksReturningVoidWithIntArgument[10])(int);

Block支持可变参数,一个不需要参数的block必须指定void在参数列表,且返回值为void,也必须在代码中写出。下面的例子就是错误的。

int (^myBlock);   //参数为空,但没有明确指出void在参数列表。
(^myBlock1r) (void);   //返回值为空,但没有指出返回值为void

你也可以使用typedef来定义block variable。

typedef float (^MyBlockType)(float, float);
 
MyBlockType myFirstBlock = // ... ;
MyBlockType mySecondBlock = // ... ;

3.2 Creating a Block

用^开头开表示一个block对象的开头,它也许会跟着参数列表(用()表示),block body则用{}表示,最后body后面用;结尾。如下面所示,定义一个名为oneFrom的block variable。在将block字面表达式赋值给它,也可以说oneFrom引用了该block。如果你不显式的写出block的返回值类型,则会自动根据body中return语句的表达式计算结果的类型来作为返回值类型。如果return type是根据return 表达式推断的,且参数列表是void,则可以不写void作为参数列表。如果body中有多个return表达式,则每个表达式返回的类型必须相同(using casting if necessary)。

float (^oneFrom)(float);
 
oneFrom = ^(float aFloat) {
    float result = aFloat - 1.0;
    return result;
};

3.3 Global Blocks

在文件级别,你可以定义一个全局block variable。

#import <stdio.h>
 
int GlobalInt = 0;
int (^getGlobalInt)(void) = ^{ return GlobalInt; };

Blocks and Variables

这个小节主要介绍block和variable之间的交互,包括内存管理。

Types of Variable

在block body的代码,variable可以被分成五种。

  1. 全局变量,包括全局静态变量。
  2. 全局函数
  3. 在同一作用域的局部变量和参数
  4. __block variables,它们在block body内是可变的,并且如果引用了该变量的block被copy到堆上,这些变量会一直存在。
  5. const imports

下面是block body内使用变量的一些原则,

  • 全局变量(包含静态)是可以访问
  • 全局函数可以访问
  • 局部变量(包含静态),且声明在block之前。
  • block能够访问的变量,如果是在栈上分配的,则默认它的cv-qualifer是const。也就是不能够修改。
  • 在block body内的variable,可以看作函数内部的局部变量,每次调用block,这些变量都会被copy。
  • __block修饰的非static局部变量,能够被修改。
int GlobalVariable = 1;
static int staticGlovalVariable = 2;

void globalFunction(void){
    printf("myBlock can assess staticGlobalFuncion\n");
}

int main(int argc, const char * argv[]) {
    
    int outScopeLocalVariable = 3;
    static int staticOutScopeLocalVariable = 4;
    {
        static int staticLocalVariable = 5;
        int LocalVariable = 6;
        
        int __block blockLocalVariable = 7;   //block type
        
        void (^myBlock)(int) = ^(int parameter){
            
            printf("%d\n", GlobalVariable);
            printf("%d\n", staticGlovalVariable);
            printf("%d\n", outScopeLocalVariable);
            printf("%d\n", staticOutScopeLocalVariable);
            printf("%d\n", LocalVariable);
            printf("%d\n", staticLocalVariable);
            printf("%d\n", blockLocalVariable);
            printf("%d\n", parameter);
        
            globalFunction();
            
            GlobalVariable = 1;
            staticGlovalVariable = 1;
            //outScopeLocalVariable = 1;  //不能修改
            staticOutScopeLocalVariable = 1;
            //LocalVariable = 7;          //不能修改
            staticLocalVariable = 1;
            blockLocalVariable = 1;      //能够被修改
            parameter = 1;
            
            //修改之后的输出
            printf("%d\n", GlobalVariable);
            printf("%d\n", staticGlovalVariable);
            printf("%d\n", outScopeLocalVariable);
            printf("%d\n", staticOutScopeLocalVariable);
            printf("%d\n", LocalVariable);
            printf("%d\n", staticLocalVariable);
            printf("%d\n", blockLocalVariable);
            printf("%d\n", parameter);
        };
        
        myBlock(8);
    
    }
    
    return 0;
}

输出结果如下。

截屏2021-12-25 19.45.47.png

3.3 Object and Block Variables

3.3.1 Objective-C Objects

当一个block被copy后,当你在block中使用一个调用函数或方法时,使用一个OC Object时,它会创建一个强引用,根据情况不同,引用指向的对象不同。 有两种情况,一种是传值,另一种是传引用。 第一种情况,直接传递一个实例变量,那么直接引用self。第二种情况,传递实例变量通过值,则引用该variable。

dispatch_async(queue, ^{
    // instanceVariable NSString is used by reference, a strong reference is made to self
    doSomethingWithNSString([[NSString alloc] init]);
});
 
 
NSString *localVariable = [[NSString alloc] init];
dispatch_async(queue, ^{
    /*
      localVariable is used by value, a strong reference is made to localVariable
      (and not to self).
    */
    doSomethingWithObject(localVariable);
});

3.3.2 C++ Objects

如果你在block里使用C++对象,当block copy后,需要注意如下两点。

  1. 如果是__block 修饰的c++栈对象,这时候,c++对象的拷贝构造函数被调用。
  2. 如果不是__block 修饰的c++栈对象,则它必须有一个const拷贝构造函数。c++对象拷贝时,使用该拷贝构造函数。

3.3.3 Blocks

当你在block内部引用了其它blocks时,如果该block被copy,那么内部的blocks也会被copy。如果在block内部使用了block variable,则该block variable引用的block也会被copy。

4. Others

4.1 Copying Blocks

一般情况下,你不需要copy或者retain block。当你需要在声明block的作用域被销毁后使用该块,才需要复制,复制将block到堆内存上。可以分别使用两个C函数来进行copy和release block。为了防止内存泄露,你必须balance Block_copy和Block_release操作。

Block_copy();
Block_release();

4.2 Patterns to Aviod

一个block字面值是一个在栈上的局部数据结构地址,用来代表一个block。那么它的作用范围就是在最内层{}范围内。下面是两个错误的使用block的例子。

void dontDoThis() {
    void (^blockArray[3])(void);  // an array of 3 block references
 
    for (int i = 0; i < 3; ++i) {
        blockArray[i] = ^{ printf("hello, %d\n", i); };
        // WRONG: The block literal scope is the "for" loop.
    }
}
 
void dontDoThisEither() {
    void (^block)(void);
 
    int i = random():
    if (i > 1000) {
        block = ^{ printf("got i at: %d\n", i); };
        // WRONG: The block literal scope is the "then" clause.
    }
    // ...
}

5. 总结

  1. block的定义以及一些错误的写法
  2. block与变量的交互和内存管理。
  3. __block type的使用。
  4. block内使用OC,C++和block Objects。