一、前言
- 闭包 = 一个函数「或指向函数的指针」+ 该函数执行的外部的上下文变量「也就是自由变量」; Block是Objective-C对于闭包的实现。
- 其中,Block:
- 可以嵌套定义,定义Block方法和定义函数方法相似;
- Block 可以定义在方法内部或外部;
- 只有调用Block时候,才会执行其{}体内的代码;
- 本质是对象,使代码高聚合。
- 使用 clang 将 OC 代码转换为 C++ 文件查看block的方法:
- 在命令行输入代码 clang -rewrite-objc 需要编译的OC文件.m;
- 这时查看当前的文件夹里多了一个相同的名称的 .cpp 文件,在命令行输入 open main.cpp 查看文件。
二、Block的定义与使用
① 无参数无返回值
// 无参数,无返回值,声明和定义
void( ^MyBlockOne)(void) = ^(void){
NSLog(@"无参数,无返回值");
};
// block的调用
MyBlock0ne();
② 有参数无返回值
// 有参数,无返回值,声明和定义
void(^MyblockTwo)(int a) = ^(int a){
NSLog(@"@ = %d我就是block,有参数,无返回值", a);
;
MyblockTwo(1);
③ 有参数有返回值
// 有参数,有返回值
int (^MyBlockThree)(int,int) = ^(int a,int b){
NSLog (@"%d我就是block,有参数,有返回值", a + b);
return a + b;
};
MyBlockThree(1,2);
④ 无参数,有返回值
int(^MyblockFour)(void) = ^{
NSLog(@"无参数,有返回值");
return45;
}
MyblockFour();
⑤ 实际开发中常用typedef定义Block
- 用typedef定义 一个block:
typedef int (^MyBlock)(int ,int);
- 这时,MyBlock 就成为一种Block类型;
- 在定义类的属性时可以这样:
@property (nonatomic, copy) MyBlock myBlockOne;
- 使用时:
self.myBlockOne = ^int(int, int){
}
⑥ 为什么Block语法中不能使用数组?
因为结构体中的成员变量与自动变量类型完全相同,所以结构体中使用数组截取数组值,而后调用时再赋值给另一个数组。也就是数组赋值给数组,这在C语言中是不被允许的。
三、Block与外界变量
① 捕获自动变量(局部变量)值(auto变量)
- (1) 默认情况
- 所谓捕获外部变量,意思就是在block内部,创建一个变量来存放外部变量,这就叫做捕获。
- auto变量:自动变量,离开作用域就会销毁,一般我们创建的局部变量都是auto变量,比如 int age = 10,系统会在默认在前面加上auto int age = 10。
- 对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的。也就是说 block 的自动变量截获只针对 block 内部使用的自动变量,不使用则不截获,因为截获的自动变量会存储于block的结构体内部,会导致block体积变大。
- 特别要注意的是默认情况下block只能访问不能修改局部变量的值。
- 如下示例:
int age = 10;
myBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();
输出结果:
age = 10
- (2) _ _block 修饰的外部变量
- 对于用_ _block 修饰的外部变量引用,block 是复制其引用地址来实现访问的。
- block可以修改_ _block 修饰的外部变量的值。
- 如下示例:
_block int age = 10;
myBlock block = ^{
NSLog(@"age = %d",age);
age = 18;
block();
输出为:
age = 18
- (3)为什么使用_ block 修饰的外部变量的值就可以被block修改呢?
- 使用clang将OC代码转换为C++文件:clang -rewrite- -objc源代码文件名,便可揭开其真正面纱;
- 会发现一个局部变量加上_ block修 饰符后竟然跟block-样变成了一个_ Block_ byref. val 0结构体类型的自动变量实例。如下所示:
_block int val = 10;
转换成
__Block_ byref_ _val_ _0 val = {
),
&val,
sizeof(_ Block_ byref. val_ 0),
10
- 此时我们在block内部访问val变量则需要通过一个叫_ _forwarding的成员变量来间接访问val变量。
- 带__block的自动变量和静态变量就是直接地址访问,所以在Block里面可以直接改变变量的值。
② 捕获static变量
- 如下示例代码:
auto int age = 10;
static int height = 20;
void (^block)(void) = ^{
NSLog(@"age is %d, height is %d",age,height);
};
age = 20;
height = 20;
block();
- 打印的结果是
age is 10, height is 20
- 使用clang将OC代码同样转换 C++ 代码,查看底层:
{
auto int age = 10;
static int height = 20;
void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
age,
&height
);
age = 20;
height = 20;
(block->FuncPtr(block);
}
- 可以看到:在定义block时,调用block的构造函数,传递参数时,age传递的是值,而height传递的是指针,看看构造函数内部:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;//定义 age 变量
int *height;//定义一个指针变量,存放外部变量的指针
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
- 在block内部,定义了两个变量age、height,不同的是,height是一个指针指针变量,用于存放外部变量的指针。再来看看执行block代码块的内部:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
int *height = __cself->height; // bound by copy
// *height : 取出指针变量所指向的内存的值
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5t_pxd6sp5x6rl9gnk21q2q934h0000gn_T_main_bf6cae_mi_0,age,(*height));
- 对于age是捕获到内部,把外部age的值存起来,而对于height,是把外部变量的指针保存起来,所以,在修改height时,会影响到block内部的值。
- 思考:为什么会出现这两种情况?原因很简单,因为auto是自动变量,出了作用域后会自动销毁的,如果我们保留他的指针,就会存在访问野指针的情况。
// 定义block类型
void(^block)(void);
void test() {
int age = 10;
static int height = 20;
// 在block内部访问 age , height
block = ^{
NSLog(@"age is %d, height is %d",age,height);
};
age = 20;
height = 20;
}
// 在main函数中调用
int main(int argc, const char * argv[]) {
test();
// test调用后,age变量就会自动销毁,如果block内部是保留age变量的指针,那么我们在调用block()时,就出现访问野指针
block();
}
③ 全局变量
- 全局变量哪里都可以访问,所以block内部是不会捕获全局变量的,直接访问,这个很好理解,我们直接看代码:
- 为什么全局变量不需要捕获?因为全局变量无论哪个函数都可以访问,block内部当然也可以正常访问,所以根本无需捕获。
- 为什么局部变量就需要捕获呢?因为作用域的问题,我们在一个函数中定义变量,在block内部访问,本质上跨函数访问,所以需要捕获起来。
- 在Person类中写一个test()方法,在test()方法中定义一个block并访问self,请问block会不会捕获self:
@implementation Person
- (void)test {
void(^block)(void) = ^{
NSLog(@"会不会捕获self--%@",self);
};
block();
}
@end
- 结果是会捕获self,我们看看底层代码:
struct __Person__test_block_impl_0 {
struct __block_impl impl;
struct __Person__test_block_desc_0* Desc;
Person *self;
__Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
- 很显然block内部的确声明了一个Person *self用于保存self,既然block内部捕获了self,那就说明self肯定是一个局部变量。那问题就来了,为什么self会是一个局部变量?它应该是一个全局变量呀?我们看一下转换后的test()方法:
static void _I_Person_test(Person * self, SEL _cmd) {
void(*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
- OC 中的test()方法时没有参数的,但是转换成 C++ 后就多了两个参数self,_cmd,其实我们每个 OC 方法都会默认有这两个参数,这也是为什么我们在每个方法中都能访问self和_cmd,而参数就是局部变量,所以block就自然而然的捕获了self。
四、Block的copy操作
① Block的存储域及copy操作
- 在开始研究Block的copy操作之前,先来思考一下: Block是存储在栈上还是堆上呢?
- 我们先来看看一个由C/C+ +/OBJC编译的程序占用内存分布的结构:
- 其实,block有 三种类型:
- 全局块( _NSConcreteGlobalBlock)
- 栈块(_ NSConcreteStackBlock)
- 堆块( NSConcreteMallocBlock)
- 这三种block各自的存储域如下图:
- 说明:
- 全局块存在于全局内存中,相当于单例。
- 栈块存在于栈内存中,超出其作用域则马上被销毁。
- 堆块存在于堆内存中,是一个带引用计数的对象,需要自行管理其内存。
- 简而言之,存储在栈中的Block就是栈块、存储在堆中的就是堆块、既不在栈中也不在堆中的块就是全局块,即:
- block直接存储在全局区;
- 如果block访问外界变量,并进行block相应拷贝,即copy;
- 如果此时的block是强引用,则block存储在堆区,即堆区block;
- 如果此时的block通过__weak变成了弱引用,则block存储在栈区,即栈区block;
- 那么,遇到一个Block,我们该怎么访问这个Block的存储位置呢?
- (1) Block不访问外界变量(包括栈中和堆中的变量)
Block既不在栈又不在堆中,在代码段中,ARC和MRC下都是如此。此时为全局块。 - (2) Block访问外界变量
- MRC环境下:访问外界变量的Block默认存储栈中。
- ARC环境下:访问外界变量的Block默认存储在堆中(实际是放在栈区,然后ARC情况下自动又拷贝到堆区),自动释放。
- (1) Block不访问外界变量(包括栈中和堆中的变量)
- ARC下,访问外界变量的Block为什么要自动从栈区拷贝到堆区呢?栈上的Block,如果其所属的变量作用域结束,该Block就被废弃, 如同一般的自动变量。当然,Block中的_ _block变量也同时被废弃。 如下图:
- 为了解决栈块在其变量作用域结束之后被废弃(释放)的问题,我们需要把Block复制到堆中,延长其生命周期。开启ARC时,大多数情况下编译器会恰当地进行判断是否有需要将Block从栈复制到堆,如果有,自动生成将Block从栈上复制到堆上的代码。Block的复制操作执行的是copy实例方法。Block只要调用了copy方法,栈块就会变成堆块。如下图:
- 如下面一个返回值为Block类型的函数:
typedef int (^blk_t)(int);
blk_t func(int rate) {
return ^(int count) {
return rate * count;
};
}
- 分析可知:
- 上面的函数返回的Block是配置在栈上的,所以返回函数调用方时,Block变 量作用域就结束了,Block会被废弃。但在ARC有效,这种情况编译器会自动完成复制。
- 在非ARC情况下则需要开发者调用copy方法手动复制,由于开发中几乎都是ARC模式,所以手动复制内容不再过多研究。
- 将Block从栈上复制到堆上相当消耗CPU,所以当Block设置在栈上也能够使用时,就不要复制了,因为此时的复制只是在浪费CPU资源。
- Block的复制操作执行的是copy实例方法。不同类型的Block使用copy方法的效果如下表:
Block类 | 副本源的配置存储域 | 复制效果 |
---|---|---|
__NSConcreteStackBlock | 栈 | 从栈复制到堆 |
__NSConcreteGlobalBlock | 程序的数据区域 | 什么也不做 |
__NSConcreteMallocBlock | 堆 | 引用计数增加 |
- 根据表得知,Block在 堆中copy会造成引用计数增加,这与其他Objective-C对象是一样的。虽然Block在栈中也是以对象的身份存在,但是栈块没有引用计数,因为不需要,我们都知道栈区的内存由编译器自动分配释放。
- 不管Block存诸域在何处,用copy方 法复制都不会引起任何问题。在不确定时调用copy方法即可。
- 在ARC有效时,多次调用copy方法完全没有问题:
// 经过多次复制,变量blk仍然持有Block的强引用,该Block不会被废弃。
blk = [[[[blk copy] copy] copy] copy];
② _ block变量与_ forwarding
- 在copy操作之后,既然_ block变量也被copy到堆上去了,那么访问该变量是访问栈上的还是堆上的呢?_ forwarding 终于要闪亮登场了,如下图:
- 通过_ forwarding,无论是在block中还是block外访问_ block变量,也不管该变量在栈上或堆上,都能顺利地访问同一个_block变量。
五、防止Block循环引用
① 解决循环引用
Block循环引用的情况:某个类将block作为自己的属性变量,然后该类在block的方法体里面又使用了该类本身,如下:
self.myBlock = ^(void){
[self doSomething];
};
解决办法:
- (1) ARC下:使用__weak,此时的 weakSelf 和 self 指向同一片内存空间,且使用__weak 不会导致 self 的引用计数发生变化。
typedef void(^YDWBlock)(void);
@property (nonatomic, copy) YDWBlock myBlock;
__weak typeof(self) weakSelf = self;
self.myBlock = ^(void){
[weakSelf doSomething];
};
- (2)如果 block 内部嵌套 block,需要同时使用__weak 和 __strong:其中strongSelf 是一个临时变量,在 myBlock 的作用域内,即内部 block 执行完就释放strongSelf,这种方式属于打破 self 对 block 的强引用,依赖于中介者模式,属于自动置为nil,即自动释放。
__weak typeof(self) weakSelf = self;
self.myBlock = ^(void){
__strong typeof(weakSelf) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", strongSelf.name);
});
};
self.myBlock();
- (3) MRC下:使用__block
__block typeof(self) blockSelf = self;
self.myBlock = ^(void) {
[blockSelf doSomething];
};
- (4)值得注意的是,在ARC下,使用_ block 也有可能带来循环引用,如下:
// 循环引用 self ->_ attributBlock -> tmp -> self
typedef void (^Block)();
@interface TestObj : NS0bject {
Block_ attributBlock;
}
@end
@implementation TestObj
- (id)init {
self = [super init];
__block id tmp = self;
self.attributBlock = ^{
NSLog(@"Self = %@", tmp);
tmp = nil;
};
}
- (void)execBlock {
self.attributBlock();
}
@end
// 使用类
id obj = [[Test0bj alloc] init];
[obj execBlock]; // 如果不调用此方法,tmp 永远不会置 nil, 内存泄露会一直在
- (5)__block修饰变量:这种方式同样依赖于中介者模式,属于手动释放,是通过__block修饰对象,主要是因为__block修饰的对象是可以改变的,需要注意的是这里的 block 必须调用,如果不调用 block,vc 就不会置空,那么依旧是循环引用,self 和 block 都不会被释放。
__block ViewController *vc = self;
self.myBlock = ^(void){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
// 手动释放
vc = nil;
});
};
self.myBlock();
- (6)对象 self 作为参数:将对象 self 作为参数,提供给 block 内部使用,不会有引用计数问题。
typedef void(^YDWBlock)(ViewController *);
@property(nonatomic, copy) YDWBlock myBlock;
self.myBlock = ^(ViewController *vc){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
});
};
self.myBlock(self);
② NSProxy 虚拟类
- OC 是单继承的语言,但是它是基于运行时的机制,所以可以通过 NSProxy 来实现 伪多继承,填补了多继承的空白;
- NSProxy 和 NSObject 是同级的一个类,也可以说是一个虚拟类,只是实现了NSObject 的协议;
- NSProxy 其实是一个消息重定向封装的一个抽象类,类似一个代理人,中间件,可以通过继承它,并重写下面两个方法来实现消息转发到另一个实例,如下:
- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
- NSProxy 的使用场景主要有两种:
- 实现多继承功能;
- 解决了NSTimer&CADisplayLink创建时对self强引用问题,参考YYKit的YYWeakProxy。
- 循环引用解决原理:主要是通过自定义的 NSProxy 类的对象来代替 self,并使用方法实现消息转发。
- 自定义一个 YDWProxy 继承于 NSProxy,如下:
@interface YDWProxy()
@property(nonatomic, weak, readonly) NSObject *objc;
- (id)transformObjc:(NSObject *)objc;
+ (instancetype)proxyWithObjc:(id)objc;
@end
@implementation YDWProxy
- (id)transformObjc:(NSObject *)objc{
_objc = objc;
return self;
}
+ (instancetype)proxyWithObjc:(id)objc{
return [[self alloc] transformObjc:objc];
}
// 查询该方法的方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
NSMethodSignature *signature;
if (self.objc) {
signature = [self.objc methodSignatureForSelector:sel];
} else{
signature = [super methodSignatureForSelector:sel];
}
return signature;
}
// 有了方法签名之后就会调用方法实现
- (void)forwardInvocation:(NSInvocation *)invocation{
SEL sel = [invocation selector];
if ([self.objc respondsToSelector:sel]) {
[invocation invokeWithTarget:self.objc];
}
}
- (BOOL)respondsToSelector:(SEL)aSelector{
return [self.objc respondsToSelector:aSelector];
}
@end
- 自定义如下两个类:
// ********Cat类********
@interface Cat : NSObject
@end
@implementation Cat
- (void)eat {
NSLog(@"猫吃鱼");
}
@end
// ********Dog类********
@interface Dog : NSObject
@end
@implementation Dog
- (void)shut {
NSLog(@"狗叫");
}
@end
- 通过 YDWProxy 实现多继承功能:
- (void)proxyTest {
Dog *dog = [[Dog alloc] init];
Cat *cat = [[Cat alloc] init];
YDWProxy *proxy = [YDWProxy alloc];
[proxy transformObjc:cat];
[proxy performSelector:@selector(eat)];
[proxy transformObjc:dog];
[proxy performSelector:@selector(shut)];
}
- 通过 YDWProxy 解决定时器中 self 的强引用问题:
self.timer = [NSTimer timerWithTimeInterval:1 target:[YDWProxy proxyWithObjc:self] selector:@selector(print) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
六、Block的使用示例
① Block作为变量(Xcode快捷键: inlineBlock)
int (^sum) (int, int); // 定义一个 block 变量 sum
// 给 Block 变量赋值
// 一般返回值省略: sum = ^(int a,int b)..
sum = ^int (int a,int b) {
return a+b;
}; // 赋值语句最后有分号
int a = sum(10,20); // 调用 block 变量
② Block作为属性(Xcode 快捷键: typedefBlock)
// 1.给Calculate 类型sum变量赋值「下定义」
typedef int (^Calculate)(int, int); // calculatet 就是类型名
Calculate sum = ^(int a,int b)[
return a+b;
;
int a = sum(10,20); // 调用 sum 变量
// 2.作为对象的属性声明,copy 后 block 会转移到堆中和对象一起
@property (nonatomic, сору) Calculate sum; // 使用 typedef
@property (nonatomic, сору) int (~sum)(int, int); // 使用 typedef
// 声明,类外
self.sum = ^(int a,int b) {
return a+b;
}
// 调用,类内
int a = self.sum(10,20);
③ 作为OC的方法参数
// 无参数传递的Block
// 实现
- (CGFloat )testTimeConsume: (void(^)())middleBlock {
// 执行前记录下当前的时间
CFTimeInterval startTime = CACurrentMediaTime();
middleBlock();
// 执行后记录下当前的时间
CFTimeInterval endTime = CACurrentMediaTime();
return endTime - startTime;
}
// 调用
[self testTimeConsume:^{
};
// 有参数传递的Block
// 实现
- (CGFloat)testTimeConsume: (void(^) (NSString * name) )middleBlock {
// 执行前记录下当前的时间
CFTimeInterval startTime = CACurrentMediaTime();
NSString *name = @"有参数";
middleBlock(name);
// 执行后记录下当前的时间
CFTimeInterval endTime = CACurrentMediaTime();
return endTime - startTime;
}
// 调用
[self testT imeConsume :^(NSString *name) {
// 放入block中的代码,可以使用参数name
// 参数name是实现代码中传入的,在调用时只能使用,不能传值
}];
④ Block回调
- Block回调是关于Block最常用的内容,比如网络下载,我们可以用Block实现下载成功与失败的反馈。开发者在block没发布前,实现回调基本都是通过代理的方式进行的,比如负责网络请求的原生类NSURLConnection类,通过多个协议方法实现请求中的事件处理。
- 而在最新的环境下,使用的 NSURLSession 已经采用block的方式处理任务请求了。各种第三方网络请求框架也都在使用block进行回调处理。这种转变很大一部分原因在于block使用简单,逻辑清晰,灵活等原因。如下:
// DownloadManager.h
#import <Foundat ion/ Foundat ion. h>
@interface DownloadManager : NS0bject <NSURL .Sess ionDownloadDelegate>
// block 重命名
typedef void (^DownloadHandler) (NSData * receiveData, NSError * error);
- (void)downloadWithURL: (NSString * )URL parameters: (NSDictionary *)parameters handler: (DownloadHandler hanlder);
@end
// DownloadManager.m
#import "Down loadManager.h"
@implementation DownloadManager
- (void)downloadWithURL: (NSString * )URL parameters : (NSDictionary *)parameters handler: (DownloadHandler hanlder) {
NSURLRequest * request = [NSURLRequest reques tWithURL: [NSURL URLWithString:URL]];
NSURLSession * session = [NSURLSession sharedSession] ;
// 执行请求任务
NSURLSess ionDataTask * task = [session dataTaskWithRequest: request complet ionHandler:^(NSData *data) {
if (handler) (
dispatch_ async(dispatch_ get_ _main_ _queue(), ^{
handler(data,error);
));
}
)};
[task resume];
- 上面通过封装 NSURLSession 的请求,传入一个处理请求结果的 block 对象,就会自动将请求任务放到工作线程中执行实现,我们在网络请求逻辑的代码中调用如下:
- (IBAction)buttonClicked: (id)sender {
#define DOWNLOADURL @"https://codeload. github. com/ AFNetworking/ AFNetworking/zip/master"
// 下载类
DownloadManager *downloadManager = [[DownloadManager alloc] init];
[downloadManager downloadWithURL: DOWNLOADURL parameters:nil handler:^(NSData *receiveData, NSError *error) {
if (error) {
NSLog(@"下载失败:%@", error);
} else {
NSLog(@"下载成功:%@", receiveData);
}];
}
为了加深理解,再来一个简单的小例子:
- A、B两个界面,A界面中有一个label,一个buttonA。点击buttonA进入B界面,B界面中有一个UlTextfield和一个buttonB, 点击buttonB退出B界面并将B界面中UlTextfield的值传到A界面中的label。
- A界面中,也就是ViewController类中:
// 关键demo:
- (IBAction)buttonAction {
MyFirstViewController *myVC = [[MyFirstViewController alloc] init];
[self presentViewController:myVC animated:YES completion:^{
]];
// 防止循环引用
__weak typeof(self) weakSelf = self;
// 用属性定义的注意:这里属性是不会自动补全的,方法就会自动补全
[myVC setBlock: (NSString *string) ^{
weakSelf.labelA.text = string;
)};
}
- B界面中,也就是MyFirstViewController类中.m文件:
- (IBAction)buttonAction {
[self dismissViewControllerAnimated: YES completion:^{
)];
self.block(_myTextfielf.text);
}
- .h文件:
#import <UIKit/UIKit.h>
// typedef定义一下block,为了更好用
typedef void(^MyBlock) (NSString *string);
@interface MyFirstViewController : UIViewController
@property (nonatomic, сору) MyBlock block;
@end
- 看到以上的两个block回调,是不是感觉比delegate清爽好多?