块类似于匿名函数或闭包,在许多其他编程语言中也存在类似的概念。
可以访问上下文,运行效率高
Block
以下是块的一些基本知识:
- 块的定义:块是由一对花括号
{}
包围的代码片段,可以包含一段可执行的代码。块的定义使用^
符号,并可以带有参数列表和返回类型。例如:
^{
// 代码块的内容
}
- 块的类型:块也是一种数据类型,与函数类似。它们可以具有参数和返回值类型。可以使用
typedef
来定义块的类型。例如:
typedef returnType (^BlockTypeName)(parameterTypes);
其中 returnType
是块的返回类型,BlockTypeName
是块的类型名称,parameterTypes
是块的参数类型。
3. 块的赋值和调用:块可以赋值给变量,并且可以像函数一样进行调用。可以使用 =
运算符将块赋值给变量,然后使用该变量调用块。例如:
ReturnType (^blockName)(ParameterTypes) = ^ReturnType (Parameters) {
// 块的内容
};
blockName(argumentValues); // 调用块
- 块的捕获变量:块可以捕获其定义范围内的变量,并在块内部访问这些变量。捕获的变量在块中形成了一个闭包,可以在块的[[生命周期]]内保持其状态。例如:
NSInteger outsideVariable = 10;
void (^block)(void) = ^{
NSLog(@"Outside variable: %ld", (long)outsideVariable);
};
block(); // 输出:Outside variable: 10
在这个例子中,块捕获了外部的 outsideVariable
变量,并在块内部访问它。
默认情况下,为块所捕获的变量是不可以在块里面修改的,如果修改了outsideVariale的值,就会报错,声明变量的时候可以加上__block修饰符,这样就可以在块内修改了
外部变量如果是数组那种的话是可以使用方法来给数组添加内容的,因为此举没有改变对象原本的地址
- 块作为参数:块可以作为方法或函数的参数进行传递,从而实现回调和异步操作等功能。可以将块作为参数声明,并在调用方法或函数时传递块。例如:
- (void)performOperationWithCompletion:(void (^)(void))completionBlock {
// 执行操作
// 操作完成后调用块
completionBlock();
}
// 调用方法,传递块作为参数
[self performOperationWithCompletion:^{
NSLog(@"Operation completed!");
}];
在这个例子中,performOperationWithCompletion:
方法接受一个块作为参数,并在操作完成后调用该块。
#pragma mark **--修改为块所捕获的变量**
NSArray *array = @[@0, @1, @2, @3, @4, @5];
**__block** NSInteger count = 0;
//块作为方法的参数
[array enumerateObjectsUsingBlock:^(NSNumber *number, NSUInteger idx, **BOOL** *stop) {
**if** ([number compare:@2] == NSOrderedAscending) {
count++;
}
NSLog(@"%ld====%@",(**unsigned** **long**)idx, number);
}];
NSLog(@"%ld", (**long**)count);
//内联块的用法,传给“numberateObjectsUsingBlock:"方法的块并未先赋给局部变量,而是直接在内联函数中调用了,如果块所捕获的变量类型是对象类型的话,那么就会自动保留它,系统在释放这个块的时候,也会将其一并释放。这就引出了一个与块有关的重要问题,块本身可以视为对象,在其他oc对象能响应的选择子中,很多块也可以响应,最重要的是,块本身也会像其他对象一样,有引用计数,为0时,块就回收了,同时也会释放块所捕获的变量,以便平衡捕获时所执行的保留操作
//如果块定义在oc类的实例方法中,那么除了可以访问类的所有实例变量之外,还可以使用self变量,块总能修改实例变量,那么除了声明时无需添加__block。不过,如果通过读取或写入操作捕获了实例变量,那么也会自动把self给捕获了,因为实例变量是与self所指代的实例关联在一起的。
// 需要注意的是(self也是一个对象,也会被保留),如果在块内部使用了实例变量,块会自动对self进行保留操作,以确保在块执行期间保持对象的有效性。但是,如果在块内部直接使用了self,并对其进行读取或写入操作,那么self也会被捕获,从而导致循环引用的问题。为了避免循环引用,可以在块内部使用__weak修饰符来避免对self进行保留操作。
如果某个实例在执行anInstanceMethod放法,那么self变量就会指向此实例。由于块里没有明确使用self变量,所以很容易就会忘记self变量其实也为块所捕获了。直接访问实例遍历和通过self来访问时等效的:
self->_anInstanceVariable = @“someThing”;
typedef void(^SomeBlock) (void);
@property (nonatomic, copy) BlockName someBlock;
`- (void)anInstanceMethod {
self.someBlock = ^ {
_anInstanceVariable = @"someThing";
}
}
self也是个对象,因而块在捕获它时也会将其保留。如果self所指代的那个对象同时也保留了块,那么这种情况就会导致“保留环”
修改为以下代码即可:
typedef void(^SomeBlock) (void);
@property (nonatomic, copy) BlockName someBlock;
__weak typeof(self)weakSelf = self;
`- (void)anInstanceMethod {
self.someBlock = ^ {
weakSelf.anInstanceVariable = @"someThing";
}
}
块的本质
带有自动变量(局部变量)的匿名函数。
- block本质上也是一个OC对象,它内部也有个
isa
指针 - block是封装了函数调用以及函数调用环境的OC对象
- block是封装函数及其上下文的OC对象
- !
block变量捕获
为了保证block内部能够正常访问外部的变量,block有个变量捕获机制
block变量与c语言变量完全相同,可以作为以下用途:
- 自动变量
- 函数参数
- 静态变量
- 静态全局变量
- 全局变量
BLOCK的三种类型
block的类型,取决于isa指针,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型
__ NSGlobalBlock __ ( _ NSConcreteGlobalBlock ) 对象存储在数据区
__ NSStackBlock __ ( _ NSConcreteStackBlock ) 对象存储在栈区
__ NSMallocBlock __ ( _ NSConcreteMallocBlock )对象存储在堆区
捕获了自动变量block就是栈类型
没有捕获就是数据区
不存在一创建就在堆区的,堆区的意义可以理解为和autorelease
一样:延长作用域 Stack
类型的Block进行了copy
操作之后变成了堆区
- 堆:动态分配内存,需要程序员自己申请,程序员自己管理
- 栈:自动分配内存,自动销毁,先入后出,栈上的内容存在自动销毁的情况
NSGlobalBlock&NSStackBlock&NSMallocBlock
如果一个block没有访问外部局部变量,或者访问的是全局变量,或者 静态局部变量,此时的blcok就是一个全局block,并且储存在全局区
- (**void**)NSGlobalBlock {
//block1没有引用到局部变量
**int** a = 10;
**void** (^block)(**void**) = ^{
NSLog(@"hello world");
};
NSLog(@"block:%@", block);
// block2中引入的是静态变量
**static** **int** a1 = 20;
**void** (^block1)(**void**) = ^{
NSLog(@"hello - %d",a1);
};
NSLog(@"block:%@", block);
}
- (**void**)NSStackBlock {
**__block** **int** a = 10;
**static** **int** a1 = 20;
**void** (^**__weak** block)(**void**) = ^{
NSLog(@"hello - %d",a);
NSLog(@"hello - %d",a1);
};
NSLog(@"block:%@", block);
}
- (**void**)NSMallocBlock {
**int** a = 10;
**void** (^block1)(**void**) = ^{
NSLog(@"%d",a);
};
NSLog(@"block1:%@", block1);
**__block** **int** b = 10;
**void** (^block2)(**void**) = ^{
NSLog(@"%d",b);
};
NSLog(@"block2:%@", block2);
}
//block继承与nsobject
- (**void**)blockFromNSObject {
**void** (^block1)(**void**) = ^{
NSLog(@"block1");
};
NSLog(@"%@",[block1 class]);
NSLog(@"%@",[[block1 class] superclass]);
NSLog(@"%@",[[[block1 class] superclass] superclass]);
NSLog(@"%@",[[[[block1 class] superclass] superclass] superclass]);
NSLog(@"%@",[[[[[block1 class] superclass] superclass] superclass] superclass]);
}
- 上述代码输出了
block1
的类型,也证实了block
是对象,最终继承NSObject
栈区block和堆区block的区别
- (**void**)diffStackAndHip {
**__block** **int** a = 10;
**__block** **int** b = 20;
NSLog(@"a:%p---b:%p", &a, &b);
**void** (^**__weak** block)(**void**) = ^{
NSLog(@"hello - %d---%p",a, &a);
a++;
};
**void** (^block1)(**void**) = ^{
NSLog(@"hello - %d---%p",b, &b);
b++;
};
block();
block1();
NSLog(@"block:%@---block1:%@", block, block1);
NSLog(@"a:%d---b:%d", a, b);
NSLog(@"a:%p---b:%p", &a, &b);
}
- 通过结果我们看到,首先
block
的地址是在栈区,而block1
的地址是在堆区,而栈block
引用的变量a
的地址并没有变化,而堆block1
引用的变量b
的地址也相应变成了堆区`0x6,并且后面使用的b的地址都是堆区上的。
#栈block存放在栈区,对局部变量引用只拷贝局部变量的地址,而堆block存放在堆区,并且直接将局部变量拷贝了一份到堆空间。
- (**void**)diffStackAndHip2 {
NSObject *objc = [NSObject new];
NSLog(@"%@---%ld",objc, CFGetRetainCount((**__bridge** CFTypeRef)(objc)));// 1
// block 底层源码
// 捕获 + 1
// 堆区block
// 栈 - 内存 -> 堆 + 1
**void**(^strongBlock)(**void**) = ^{ // 1 - block -> objc 捕获 + 1 = 2
NSLog(@"%@---%ld",objc, CFGetRetainCount((**__bridge** CFTypeRef)(objc)));
};
strongBlock();
**void**(^**__weak** weakBlock)(**void**) = ^{ // + 1
NSLog(@"%@---%ld",objc, CFGetRetainCount((**__bridge** CFTypeRef)(objc)));
};
weakBlock();
**void**(^mallocBlock)(**void**) = [weakBlock copy];
mallocBlock();
}
奇怪为什么堆区block
里面的对象引用计数加2呢?而后面的mallocBlock
只加1呢?
首先objc在strongBlock里面必然会拷贝一份到堆区,所以会加1,但是他是从当前函数的栈区拷贝吗?并不是,对于堆区Block一开始编译时是栈block这时候objc对象地址拷贝了一份引用计数加1,后面从栈block变成堆block,又拷贝了一份引用计数又加1,所以这时候是3,weakBlock是栈block仅拷贝了一份,所以引用计数加1,这时候是4,mallocBlock从weakblock拷贝了一份,所以引用计数再加1,这时候是5,相当于strongBlock = weakblock + void(^mallocBlock)(void) = [weakBlock copy];
- (**void**)diffStackAndHip3 {
NSObject *a = [NSObject alloc];
NSLog(@"1---%@--%p", a, &a);
**void**(^**__weak** weakBlock)(**void**) = **nil**;
{
// 栈区
**void**(^**__weak** strongBlock)(**void**) = ^{
NSLog(@"2---%@--%p", a, &a);
};
weakBlock = strongBlock;
strongBlock();
NSLog(@"3 - %@ - %@",weakBlock,strongBlock);
}
weakBlock();
NSLog(@"4---%@--%p", a, &a);
}
- 当前是栈区
strongBlock
的赋值给外面的栈区weakBlock
,因为都是存放在栈空间的,只有当前函数结束才会被销毁,随意这边weakBlock
调用并不会有什么问题。如果换成堆区block
就不一样了。 - 这边的a对象在weakBlock()调用时是nil,通过上面打印可以看出a对象在进入到strongblock里,&a拷贝了一份,拷贝的这一份地址指向的跟外面一样,但是当strongblock出了{}刚才复制的对象就要销毁了,(栈区的对象在函数结束的时候就会被销毁,函数本身会在作用域结束的时候被销毁)尽管strongblock对象不再了,但是其指向的内存空间还在,销毁之前给了外面的weakBlock,同理a也一样,对象(此时a指向的内容)不在了,但是内存空间却还在。
#这边建议打断点看一看运行流程,方便理解
- (**void**)diffStackAndHip4 {
NSObject *a = [NSObject alloc];
NSLog(@"1---%@--%p", a, &a);
**void**(^**__weak** weakBlock)(**void**) = **nil**;
{
// 栈区
**void**(^**__strong** strongBlock)(**void**) = ^{
NSLog(@"2---%@--%p", a, &a);
};
weakBlock = strongBlock;
strongBlock();
NSLog(@"3 - %@ - %@",weakBlock,strongBlock);
}
// weakBlock();//测试的时候取消注释
NSLog(@"4---%@--%p", a, &a);
}
- 为什么呢?因为在{}里面的堆区
strongBlock
出了大括号就会被销毁,此时你去调用这个block
就会崩溃 - 注意:这边
weakBlock
为什么也是__NSMallocBlock__
,其实weakBlock
相当于是指针,此时指向的是一个堆上的内存所以是__NSMallocBlock__