深入理解iOS之 Block

image.png

一 Block基本语法

1.1 什么是block

带有自动变量(局部变量)的匿名函数;

1.2 Block语法

匿名函数Block,简写方式:

^(int event) {
    printf("buttonId:%d event=%d\n", i , event);
}

完整形式如下

^void (int event) {
    printf("buttonId:%d event=%d\n", i , event);
}
  • 完整的Block与C语言的函数定义相比,仅有两点不同:
    • 没有函数名, 因为它是匿名函数
    • 返回值类型前带有 “^”, 因为OS X,iOS 应用程序的源代码中将大量使用Block, 所以插入该记号便于查找

Block语法的BN范式:

^ 返回值类型 参数列表 表达式

^int (int count) {
      return count +1;
}

Block省略方式一

^ 参数列表 表达式

^ (int count) {
      return count +1;
}

Block 省略方式二

^ 表达式

不使用参数的完整Block语法

^void (void) {
      printf("Block");
}

省略方式如下

^ {
printf("Block");
}

1.3 截获的自动变量

  • 自动变量值的截获:block表达式会保存自动变量的值,所以在执行block语法后,及时改写Block中使用的自动变量的值也不会影响block执行时自动变量的值;

  • 截获的OC对象,调用变更对象的方法会产生编译错误么?

 NSMutableArray *arrM = [[NSMutableArray alloc] init];
 void (^blk)(void) = ^{
      id object = [[NSObject alloc] init];
      [arrM addObject:object];
 };

以上是没有问题的,源代码截获的变量值为NSMutableArray 类的对象,即截获 NSMutableArray 类用的结构体示例指针,虽然赋值给截获的自动变量arrM操作会产生编译错误,但使用截获的值却不会有任何问题;

1.4 __block 说明符

实际上,自动变量值的截获只能保存执行Block语法瞬间的值,保存后就不能改写该值,如果改写的话,则会进行报错;

typedef int (^blk_t) (int);

int main(int argc, char * argv[]) {
    int  val = 0;
    void (^blk)(void) = ^{
         val = 1;// 此处会报错
    }
}

使用附有__block说明符的自动变量可在Block中赋值,该变量称为__block变量;

二 Block实现

2.1 Block实质

clang(LLVM编译器)具有转换为我们可读源代码的功能,通过-rewrite-objc选项就能将含有Block语法的源代码变换为C++的源代码, 其实仅仅使用了struct结构,其本质是C语言源代码
终端执行以下代码即可

clang -rewrite-objc 源代码文件名
结合此图会更好理解.png

我们将以下代码转换为Block语法

int main(int argc, char * argv[]) {
    void (^blk)(void) = ^ {
        printf("Block");
    };
    blk();
    return 0;
}

以上代码仅有6行,但是通过clang转换后,得到的main.cpp文件,达到130+行代码,我们看最初源代码中的block语法。

  ^ {
  printf("block");
}

变换后的源代码中也有相同的表达式:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
   printf("block");
}

该函数的__cself其实就是C语言中的this,OC实例方法中指向对象自身的变量self,即参数__cself,为指向Block值的变量;

  • 该参数的声明如下:
struct __main_block_impl_0 *__cself

__cself 是 __main_block_impl_0 结构体的指针;
该结构体声明如下

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;
  }
};

第一个成员变量是 impl,看一下这个成员变量的结构体声明

struct __block_impl {
  void *isa; 
  int Flags;
  int Reserved;
  void *FuncPtr;
};

第二个成员变量是desc指针:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
}

那么我们再来看一下 初始化含有这些结构体的的main_block_impl_0函数的 构造函数

 __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;
  }

我们可以来看看该构造函数的调用:

    void (*blk)(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;

该源代码将 __main_block_impl_0结构体类型的变量,即栈上生成的 __main_block_impl_0结构体实例的指针,赋值给了 __main_block_impl_0结构体指针的变量blk,以下为这部分的最初源代码

 void (^blk)(void) = ^ {
        printf("block");
    };

以上代码它等同于将 __main_block_impl_0 结构体实例的指针赋值给变量blk, 该源代码中的Block就是 __main_block_impl_0 结构体类型的自动变量,即栈上生成的 __main_block_impl_0 的结构体示例

来看看__main_block_impl_0 结构体示例构造函数

__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA)
  • __main_block_func_0为C语言函数指针
  • &__main_block_desc_0_DATA 为静态全局变量初始化的 __main_block_desc_0的结构体示例指针;

以下为 __main_block_desc_0 初始化部分

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)
};

栈上的__main_block_impl_0 结构体实例(即Block)是按照如下方式进行初始化的

struct __main_block_impl_0 {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
  struct __main_block_desc_0  *Desc;
}

这个结构体根基构造函数想下面这样进行初始化

isa = &_NSConcreteStackBlock;
Flags = 0;
Reserved = 0
FuncPtr = __main_block_impl_0;
Desc = &__main_block_desc_0_DATA;

blk() 转换为了以下代码

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

去掉转换部分,就是函数指针调用函数,

(*blk->impl.FuncPtr)(blk);

这就是简单的使用函数指针调用函数
正如我们刚确认的,有Block语法转换的 main_block_func_0函数的指针被赋值成员变量FuncPtr中; 另外也说明了main_block_func_0函数的参数__cself指向Block值,在调用该函数的源代码中可以看出Block正是作为参数进行了传递;

接下来看 & _NSConcreteStackBlock到底是什么;

isa = &_NSConcreteStackBlock;

其实Block指针赋值给Block的结构体成员变量isa,为了理解他,首先要理解OC类和对象的本质,其实block就是OC对象;
即有该类生成的对象的各个结构体示例,通过成员变量isa保持该类的结构体实例指针,如下OC类与对象的实质


image.png

_NSConcreteStackBlock 相当于Class结构体实例,在将Block作为OC的对象处理时,关于该类的的信息放置于 _NSConcreteStackBlock 中;

2.2 截获自动变量值

结合此流程图更容易理解.png

将截获的自动变量值的源代码通过clang转换为为以下代码

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  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;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt; // bound by copy
  int val = __cself->val; // bound by copy

        printf(fmt, val);
    }

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, char * argv[]) {
    int val = 10;
    const char *fmt = "val = %d";
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
 
    return 0;
}

Block语法表达式中使用的自动变量被追加到了 __main_block_impl_0 结构体中

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt;
  int val;
};

初始化构该结构体实例的构造函数时,根据传递给构造函数的参数进行初始化

impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;    
impl.FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
fmt = "val = %d";
val = 10

2.3 __Block说明符

我们先来看下OC中静态变量、静态全局变量、全局变量的源代码实现

结合此图更容易理解.png

源代码

// 全局变量
int global_val = 1;
    // 静态全局变量
static int static_global_val = 2;
int main(int argc, char * argv[]) {
    // 静态变量
    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;
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *static_val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_val = __cself->static_val; // bound by copy
        (*static_val) *= 3;
    }

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, char * argv[]) {

    static int static_val = 3;

    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
    return 0;
}

结论:静态全局变量static_global_val 和全局变量 global_val 的访问和转换前完全相同,来看下静态变量的转换

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_val = __cself->static_val; // bound by copy
        (*static_val) *= 3;
    }

将静态变量static_val 的指针传递给了__main_block_impl_0 结构体的构造函数进行保存,这是超出作用域使用变量的最简单的方式;

使用__block说明符

image.png
__block int val = 10;
    void (^blk)(void) = ^ {
        val = 1;
    };
struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  __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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref

        (val->__forwarding->val) = 1;
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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, char * argv[]) {

    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};

    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
    return 0;
}

只是在自动变量上附加了__block说明符,源代码就急剧增加;

__block int val = 10;

转换成了如下

 __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {
        (void*)0,
        (__Block_byref_val_0 *)&val,
        0,
        sizeof(__Block_byref_val_0),
        10        
    };

变成了一个结构体;__block变量也同Block一样变成了 __Block_byref_val_0 结构体类型的自动变量,即栈上生成了 __Block_byref_val_0 结构体实例,初始化为10;

该结构体声明如下:

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

而赋值的代码又转换成了什么呢?

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref
        (val->__forwarding->val) = 1;
  }

block向静态变量赋值时,使用了指向该静态变量的指针;

block变量赋值则是,Block的 __main_block_impl_0 结构体实例持有指向__Block变量的 __Block_byref_val_0结构体实例的指针;

__Block_byref_val_0 结构体实例的成员变量 __forwarding 持有指向该实例自身的指针。通过成员变量__forwarding 访问成员变量val;

image.png

为什么会有成员变量 __forwarding 指向自己呢? 这个放在后边讲

__Block生成的结构体 __Block_byref_val_0 结构体不在Block __main_block_impl_0中,是为了在多个Block中使用__Block变量;

2.4 Block存储域

Block与__Block变量的实质

名称 实质
Block 栈上Block的结构体实例
__block 变量 栈上__block变量的结构体实例

Block当做OC对象看,该Block的类为 _NSConcreteStackBlock,还有很多与之类似的类有

设置对象的存储域
_NSConcreteStackBlock
_NSConcreteGlobalBlock 程序的数据区域(.data区)
_NSConcreteMallocBlock

在记录全局变量的地方使用Block语法时,则Block_NSConcreteGlobalBlock类对象

  • 以下BlockNSConcreteGlobalBlock类对象,

    • 记述全局变量的地方有Block语法时
    • Block语法的表达式中不使用应截获的自动变量时;
  • 配置在栈上的Block,其变量作用域销毁如下

image.png

所以Blocks提供了将栈上的Block复制到堆上的方法来解决这个问题,如下图


image.png

此时Block的结构体实例的成员变量isa变化如下

impl.isa = &_NSConcreteMallocBlock;

__block变量用结构体成员变量__forwarding可以实现无论是__block变量配置在栈上,还是在堆上,都能正确的访问__Block变量;

实际上当ARC有效的时候,大多数情形下,编译器会恰当的进行判断,自动生成将Block从栈上复制到堆上的代码。
可以自动生成Block从栈上赋值到堆上的代码

查看此源代码

typedef int (^blk_t)(int);
blk_t func(int rate) {
  return ^(int count)(return rate * count);
}

该源代码返回配置在栈上的Block函数,即程序执行中从该函数返回函数函数调用方时变量作用域结束,因此栈上的Block也被废弃。但是该源代码通过对应的ARC编译器可转换如下

blk_t fun(int rate) {

// 通过block语法生成的Block,即配置在栈上的Block用结构体实例赋值给相当于Block类型的变量tmp中
  blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);

// 将栈上的Block复制到堆上。复制后,将堆上的地址作为指针赋值给变量tmp中
tmp = objc_retainBlock(tmp);
// 将堆上的Block作为Objectice-C对象 注册到autoreleasepool中,然后返回该对象
return objc_autoreleaseReturnValue(tmp);
}

通过Objc4运行时库的 runtime/objc-arr.mm 可知, objc_retainBlock函数实际上就是Block_copy函数,即

tmp = _Block_copy(tmp);

那么编译器不能进行判断是否从将Block从栈上复制到堆上有哪些情况呢?

  • 向方法或函数的参数中传递Block时,此种情况就需要手动复制 否则就会出现野指针Crash
typedef void (^blk_t)(void);
- (id)getBlockArrar {
    int val = 10;
// 此种写法会crash
//   return [[NSArray alloc] initWithObjects:^{NSLog(@"blk0:%d",val);}, ^{NSLog(@"blk1:%d",val);}, nil];

// 正确的写法
return [[NSArray alloc] initWithObjects:[^{NSLog(@"blk0:%d",val);} copy], [^{NSLog(@"blk1:%d",val);} copy], nil];

}
- (void)test {
    NSArray * obj = [self getBlockArrar];
    blk_t blk = (blk_t)[obj objectAtIndex:0];
    blk();    
}
  • 如果在方法或函数中适当地复制了传递过来的参数,那么就不必在调用该方法或函数前手动复制了,以下方法就不用手动复制
    • Cocoa框架的方法且方法中含有usingBlock时
    • Grand Central Dispatch的API

2.5 __Block变量存储域

使用__block变量的Block 从栈复制到堆上,__block变量同样跟着受影响

__Block变量的配置存储域 Block 从栈上复制到堆上的影响
从栈复制到堆并被Block持有
被Block持有

使用__block变量的Block持有__block变量,若Block被废弃,他所持有的__block变量也就被释放

image.png

2.6 截获对象

示例代码

    typedef void (^blk_t)(id);    
    blk_t blk;    
    {
        NSMutableArray * array = [[NSMutableArray alloc] init];        
        blk = [^(id obj) {
            [array addObject:obj];
            NSLog(@" count = %ld", [array count]);
        } copy];
    }
    
    blk([NSObject new]);
    blk([NSObject new]);
  // 输出为 
  count = 1
  count = 2

上述的代码能够正常运行,意味着赋值给变量Array的NSMutableArray类的对象在该源代码最后Block的执行部分超出其变量作用域而存在,通过编译器转换后的核心源代码如下

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  id array;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

但是在OC中,C语言结构体里不能含有附有__strong修饰的变量,因为编译器不知道应该何时初始化和废弃C结构体,不能很好的管理内存;
但是OC的运行时库能够准确的把Block从栈复制到堆上以及堆上的Block被废弃的时机,是在__main_block_desc_0结构体中增加的成员变量copydispose,以及作为指针赋值给改成员变量的__main_block_copy_0函数和__main_block_dispose_0函数实现的;

// _Block_object_assign 相当于retaain实例方法函数,将对象赋值在对象类型的结构体变量中
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
// __main_block_dispose_0 相当于release函数
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

调用Copy函数和Dispose函数的时机

函数 调用时机
Copy函数 栈上的Block复制到堆时
Dispose函数 堆上的Block被废弃时
  • 什么时候栈上的Block会被复制堆上呢?

    • 调用Block的copy实例方法时
    • Block 作为函数返回值返回时
    • 将Block 赋值给附有__strong修饰符id类型的类或Block类型成员变量时
    • 在方法名中含有usingBlock的Cocoa框架方法 或Grand Central Dispatch的API传递Block时
  • 什么时候堆上的Block被废弃呢?

    • 在释放复制到堆上的block后,谁都不持有Block而使其被废弃时调用dispose函数。相当于对象的dealloc实例方法

2.7 __Block 变量和对象

__block说明符可指定任何类型的自动变量。

我们上边讲述的是只有附有__strong修饰符的id类型或对象类型自动变量。如果使用__weak修饰符会如何呢?

    typedef void (^blk_t)(id);  
    blk_t blk;    
    {
        NSMutableArray * array = [[NSMutableArray alloc] init];
        id __weak weakArray = array;
        blk = [^(id obj) {
            [weakArray addObject:obj];
            NSLog(@" count = %ld", [weakArray count]);
        } copy];
    }
    
    blk([NSObject new]);
    blk([NSObject new]);
//输出为 
count = 0
count = 0

【结论】:结果与上边的执行结果不同,但是该代码能够正常执行;这是因为array 在变量作用域结束的时候,被释放和废弃,nil被赋值给了weakArray;

如果使用__block和__weak修饰符修饰的呢?

    typedef void (^blk_t)(id);  
    blk_t blk;    
    {
        NSMutableArray * array = [[NSMutableArray alloc] init];
        __block __weak NSMutableArray  *weakArray = array;
        blk = [^(id obj) {
            [weakArray addObject:obj];
            NSLog(@" count = %ld", [weakArray count]);
        } copy];
    }
    
    blk([NSObject new]);
    blk([NSObject new]);
//输出为 
count = 0
count = 0

【结论】即使加了__block ,array 在变量作用域结束的时候,被释放和废弃,nil被赋值给了weakArray;

参考:
《Objective-C高级编程iOS与OS X多线程和内存管理》

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

推荐阅读更多精彩内容

  • 0.前言 日常开发中经常会用到 Block,但如果对它的底层实现没有深入地挖掘过,就不能算是真正掌握,本篇就来探究...
    RiverSea阅读 718评论 0 13
  • Blocks是C语言的扩充功能。可以用一句话表示Blocks的扩充功能:带有自动变量/局部变量的匿名函数,也被称为...
    蜗牛非牛阅读 945评论 1 10
  • Block源码解析和深入理解 Block的本质 Block是"带有自动变量值的匿名函数". 我们通过Clang(L...
    十三亿少女梦丶阅读 3,131评论 1 18
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,513评论 16 22
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,559评论 0 11