在日常的开发中Block
也算是我们经常使用的,那么它的底层原理是什么?它的使用又会引起什么问题?解决的方法是什么?笔者将会逐步探索并把结果记录到本篇文章中。
Block的种类
我们平常经常用的Block一般是三种,但其实Block一共有六种,这个可以在闭包源码libclosure
中查看到:
/********************
NSBlock support
We allocate space and export a symbol to be used as the Class for the on-stack and malloc’ed copies until ObjC arrives on the scene. These data areas are set up by Foundation to link in as real classes post facto.
We keep these in a separate file so that we can include the runtime code in test subprojects but not include the data so that compiled code that sees the data in libSystem doesn't get confused by a second copy. Somehow these don't get unified in a common block.
**********************/
void * _NSConcreteStackBlock[32] = { 0 };
void * _NSConcreteMallocBlock[32] = { 0 };
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 };
void * _NSConcreteWeakBlockVariable[32] = { 0 };
我们经常用到是_NSConcreteStackBlock
、_NSConcreteMallocBlock
、_NSConcreteGlobalBlock
这三种,而剩下的三种是系统级别使用的,iOS开发中不太能用的到。那我们经常使用的三种表现形式例如以下代码:
void (^block1)(void) = ^{};
NSLog(@"%@",block1);
int a = 10;
void (^block2)(void) = ^{
NSLog(@"123 - %d",a);
};
NSLog(@"%@",block2);
NSLog(@"%@",^{
NSLog(@"123 - %d",a);
});
在控制台会看到输出:
<__NSGlobalBlock__: 0x1080c2090>
<__NSMallocBlock__: 0x600002e9f990>
<__NSStackBlock__: 0x7ffee7b3c458>
Block的本质
Block的本质以及调用
先创建一个block1.c
文件,文件内代码如下:
#include "stdio.h"
int main(){
void(^block)(void) = ^{
printf("block1");
};
block();
return 0;
}
然后用clang
指令clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk block1.c
去编译会等到一个block1.cpp
文件,在cpp文件中会看到代码:
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("block1");
}
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(){
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
我们先看void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
这段代码,=前是block的声明,=后简化一下__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
,这是一个构造函数,而我们可以看到__main_block_impl_0
其实是一个结构体,由此可知block
的声明其实是一个结构体类型,而我们知道block
是可以用%@
打印的,所以它的本质是一个对象,而对象的本质其实就是结构体。
我们也可以看到__main_block_impl_0()
的第一个参数__main_block_func_0
是一个函数,内容就是在block
代码块内写的东西,而__main_block_func_0
保存在了结构体__main_block_impl_0
的函数__main_block_impl_0
的impl.FuncPtr
中,而在调用时我们可以看到代码((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
其实就等同于block->FuncPtr
,所以这也就解释了block
为什么需要调用,因为block
声明的时候只是函数声明,具体的函数实现是需要调用的。
Block捕获外界变量
为了研究这个问题,我们先把代码修改一下:
int main(){
int a = 10;
void(^block)(void) = ^{
printf("block1 - %d",a);
};
block();
return 0;
}
同样用clang
指令编译得到cpp文件查看:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
printf("block1 - %d",a);
}
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 a = 10;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
可以看到,我在源码中加了一行int a = 10;
,而编译后可以看到__main_block_impl_0
函数多了一个参数a
,而在结构体__main_block_impl_0
中也多了一个属性int a;
,通过传值把10
这个值赋值给了a
。由此我们可知,block
在捕获外界变量的时候会自动生成一个属性来保存外界传进来的值。
__block
我们都知道block
要想在内部改变外界的变量,需要在变量声明前加一个__block
,那么它的原理是什么呢?这里我们再次修改代码:
int main(){
__block int a = 10;
void(^block)(void) = ^{
printf("block1 - %d",a);
};
block();
return 0;
}
可以看到只是在int a
前面加了一个__block
,然后同样用clang
编译,得到结果:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref
printf("block1 - %d",(a->__forwarding->a));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 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(){
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
我们可以看到代码中生成了一个新的结构体__Block_byref_a_0
并把a
的地址指针和值分别传到了结构体的__forwarding
和a
中,而__main_block_impl_0
函数的第三个参数也变为(__Block_byref_a_0 *)&a
,当然对应的结构体__main_block_impl_0
中也生成了一个__Block_byref_a_0 *a
属性来接收,__main_block_func_0
函数内的代码也变成了__Block_byref_a_0 *a = __cself->a;
,这相当于指针拷贝,所以在调用的时候在block
内部也能修改外界的变量。
我们还注意到cpp文件中还生成了函数__main_block_copy_0
、__main_block_dispose_0
和结构体__main_block_desc_0
,这些也都需要我们继续进行探索。
Block源码分析
Block的结构
接下来我们就进入源码分析阶段,同样在libclosure
中,打开Block_private.h
文件,我们可以看到这样一个结构:
struct Block_layout {
void *isa; // isa指针
volatile int32_t flags; // 用于记录block的状态
int32_t reserved; // 保留变量
BlockInvokeFunction invoke; // 函数指针,指向具体的函数实现
struct Block_descriptor_1 *descriptor; //
// imported variables
};
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
// 可选
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
BlockCopyFunction copy;
BlockDisposeFunction dispose;
};
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
这个其实就是block
的数据结构。这里我们关注一下flags
和descriptor
,先看flags
:
// Values for Block_layout->flags to describe block objects
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime 释放标记,一般常用BLOCK_NEEDS_FREE做位与操作,一同传入Flags,告知该block可释放
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime 存储引用计数的值,是一个可选用参数
BLOCK_NEEDS_FREE = (1 << 24), // runtime 第16位是否有效的标志,程序根据它来决定是否增加或是减少引用计数位的值
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler 是否拥有拷贝辅助函数
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code 是否拥有block析构函数
BLOCK_IS_GC = (1 << 27), // runtime 标志是否有垃圾回收
BLOCK_IS_GLOBAL = (1 << 28), // compiler 标志是否是全局block
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler 与BLOCK_USE_STRET相对,判断是否当前block拥有一个签名。用于runtime时动态调用
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};
可以看出flags
决定了很多block
的信息状态,例如BLOCK_HAS_COPY_DISPOSE
就决定了block
中是否含有Block_descriptor_2
,BLOCK_HAS_SIGNATURE
决定了block
中是否含有Block_descriptor_3
,这也说明Block_descriptor_2
和Block_descriptor_3
是可选的,那么它们是如何获取到的呢,我们在源码runtime.cpp
文件中搜索可以看到:
static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
uint8_t *desc = (uint8_t *)aBlock->descriptor;
desc += sizeof(struct Block_descriptor_1);
return (struct Block_descriptor_2 *)desc;
}
static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock)
{
if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
uint8_t *desc = (uint8_t *)aBlock->descriptor;
desc += sizeof(struct Block_descriptor_1);
if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
desc += sizeof(struct Block_descriptor_2);
}
return (struct Block_descriptor_3 *)desc;
}
可以看到,不管是要获取Block_descriptor_2
还是Block_descriptor_3
都需要先进行判断,其实也就是判断flags
,如果符合条件的话,就通过指针地址偏移的方式得到对应的结构,这其实也说明了Block_descriptor_1、2、3的内存地址是连续的。
Block的拷贝
接下来我们来研究以下block
是如何进行拷贝的,我们在代码中可以找到_Block_copy
这个函数:
// Copy, or bump refcount, of a block. If really copying, call the copy helper if present.
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
if (!arg) return NULL;
// The following would be better done as a switch statement
aBlock = (struct Block_layout *)arg;
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) { // 全局block
return aBlock;
}
else {
// Its a stack block. Make a copy.
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
result->invoke = aBlock->invoke;
#endif
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
result->isa = _NSConcreteMallocBlock;
return result;
}
}
这里的代码其实逻辑很简单:先同block->flags
来判断block包含的信。
- 如果需要对引用计数处理,那就调用函数去处理。
- 如果是一个全局block的话就不做处理直接返回。
- 不是全局block的话那就是一个栈block,会通过
malloc
函数在堆区申请一份新的同样大小的内存空间,然后把栈block中的信息全部拷贝到新的堆区中,同时将isa
置为_NSConcreteMallocBlock
,这就是block
从栈拷贝到堆的过程。
__block的拷贝
在上面我们看到了block
对于自身的拷贝,那么对于__block
修饰的变量究竟又做了什么呢?首先我们需要改一下代码:
__block NSString *name = [NSString stringWithFormat:@"luode"];
void(^block)(void) = ^{
name = @"luode123";
NSLog(@"block - %@",name);
};
block();
然后继续通过clang
编译:
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_name_0 name = {
(void*)0,
(__Block_byref_name_0 *)&name,
33554432,
sizeof(__Block_byref_name_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"), (NSString *)&__NSConstantStringImpl__var_folders__l_qrwh0b994bv2231f8v82qgdr0000gn_T_main_8fee07_mi_0)};
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_name_0 *)&name, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));
}
}
我们可以看到__block
修饰的name
转化成了一个__Block_byref_name_0
的结构体,它在源码中对应的结构是:
struct Block_byref {
void *isa;
struct Block_byref *forwarding;
volatile int32_t flags; // contains ref count
uint32_t size;
};
struct Block_byref_2 {
// requires BLOCK_BYREF_HAS_COPY_DISPOSE
BlockByrefKeepFunction byref_keep;
BlockByrefDestroyFunction byref_destroy;
};
struct Block_byref_3 {
// requires BLOCK_BYREF_LAYOUT_EXTENDED
const char *layout;
};
在编译后的文件中我们可以发现__main_block_copy_0
中调用了_Block_object_assign
函数,于是我们去源码中查找可以找到对应的函数:
// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment.
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT:
/*******
id object = ...;
[^{ object; } copy];
********/
_Block_retain_object(object);
*dest = object;
break;
case BLOCK_FIELD_IS_BLOCK:
/*******
void (^object)(void) = ...;
[^{ object; } copy];
********/
*dest = _Block_copy(object);
break;
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
/*******
// copy the onstack __block container to the heap
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__block ... x;
__weak __block ... x;
[^{ x; } copy];
********/
*dest = _Block_byref_copy(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
/*******
// copy the actual field held in the __block container
// Note this is MRC unretained __block only.
// ARC retained __block is handled by the copy helper directly.
__block id object;
__block void (^object)(void);
[^{ object; } copy];
********/
*dest = object;
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
/*******
// copy the actual field held in the __block container
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__weak __block id object;
__weak __block void (^object)(void);
[^{ object; } copy];
********/
*dest = object;
break;
default:
break;
}
}
再把switch
判断的标识展示出来:
// Values for _Block_object_assign() and _Block_object_dispose() parameters
enum {
// see function implementation for a more complete description of these fields and combinations
BLOCK_FIELD_IS_OBJECT = 3, // id, NSObject, __attribute__((NSObject)), block, ... 对象
BLOCK_FIELD_IS_BLOCK = 7, // a block variable block变量
BLOCK_FIELD_IS_BYREF = 8, // the on stack structure holding the __block variable __block修饰的结构体
BLOCK_FIELD_IS_WEAK = 16, // declared __weak, only used in byref copy helpers __weak修饰的变量
BLOCK_BYREF_CALLER = 128, // called from __block (byref) copy/dispose support routines. 处理block_bref内部对象内存的时候会加一个额外的标记,配合上面的枚举一起使用
};
从源码中可以看到:
- 如果传入的是一个对象的话,就会调用
_Block_retain_object
,而_Block_retain_object
内部其实是一个空实现,推测这么做的原因在于block
并不需要对这个对象进行持有,因为该对象的引用计数已经交给ARC
进行管理了,只需要通过*dest = object;
进行指针赋值即可。 - 如果传入的是一个
block
的话,就会调用_Block_copy
函数,也就是我们上面研究过的block
的拷贝。 - 如果传入的是
BLOCK_FIELD_IS_BYREF
类型的,也就是由__block
修饰的结构体,那么就会调用_Block_byref_copy
函数,来看一下这个函数的源码:
static struct Block_byref *_Block_byref_copy(const void *arg) {
struct Block_byref *src = (struct Block_byref *)arg;
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
// __block 修饰变量 block具有修改能力
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
// Trust copy helper to copy everything of interest
// If more than one field shows up in a byref block this is wrong XXX
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;
if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
copy3->layout = src3->layout;
}
(*src2->byref_keep)(copy, src);
}
else {
// Bitwise copy.
// This copy includes Block_byref_3, if any.
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
// already copied to heap
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}
首先通过malloc
函数在堆区拷贝了一份,然后进行赋值,这里需要注意的是:
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy
这两行代码让原来的__block
修饰的结构体和新拷贝的结构体都指向了copy
,这也就是为什么__block
修饰的变量在block
内部具有修改的能力,在前面我们通过clang
看到是指针拷贝,那这里就是对应的源码实现。
接着往下看我们会看到(*src2->byref_keep)(copy, src);
这样一句代码,src2
其实就是src+1
通过内存地址偏移得到的,这里src2
调用了byref_keep
,我们跟踪一下这个byref_keep
会发现它是上面提到的结构体Block_byref
中的第五个属性,我们在编译后的cpp文件中已经得知了被__block
修饰的变量会转化成一个Block_byref
的结构体,我们查看这个结构体name
会发现它的第五个属性是__Block_byref_id_object_copy_131
,我们在cpp文件中搜索可以找到:
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
可以看到该函数也调用了上面探索过的_Block_object_assign
函数,但是传的参数多了个+40
,这个是地址偏移应该不难理解,那么为什么是偏移40字节呢,这里我们就需要看一下它的参数也就是__Block_byref_name_0
的结构:
struct __Block_byref_name_0 {
void *__isa;
__Block_byref_name_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSString *name;
};
不难看出,偏移40个字节其实正好取到name
的内存地址,这里其实是对name
这个对象又做了一次拷贝。
所以__block
修饰的外部变量,在block
内部会出现三层拷贝:
- 第一次是
block
自身的拷贝,从栈区拷贝到堆区。 - 第二次是对新生成的结构体的拷贝,
__block
修饰的变量会生成一个__Block_byref_XXX_0
的结构体,通过调用_Block_object_assign
函数中的_Block_byref_copy
函数拷贝到堆区中。 - 第三次就是刚讲过的对
__block
修饰的变量本身的拷贝。
以上就是本次对Block
的探索,笔者能力有限,如有错误还请指正。