引言

Block 本质上也是一个 Objective-C 对象,它内部也有个 isa指针。Block 是封装了函数调用以及函数调用环境的 Objective-C 对象。Block 的底层结构如下图所示:

Block 对于不同类型的值会有不同的捕获方式,本文将通过代码展示其对于各种场景下的外部值是如何进行捕获的。

自动变量

首先展示源代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSInteger value = 0;
        void(^block)(void) = ^{
            NSLog(@"%zd", value);
        };
        block();
    }
    return 0;
}

经过 clang -rewrite-objc 之后,得到的代码如下,可以看到,对于自动变量的捕获,是会在 Block 结构体中生成一个对应类型的成员变量来实现捕获的能力,这也解释了为什么在 Block 中修改捕获的值的内容,无法对 Block 外的值产生影响。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSInteger value; // 捕获的 NSInteger value
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger _value, int flags=0) : value(_value) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSInteger value = __cself->value; // bound by copy
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_zz_zyxvpxvq6csfxvn_n0000000000000_T_main_e3ca95_mi_0, value);
        }
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, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NSInteger value = 0;
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, value));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

静态变量、静态全局变量与全局变量

对于静态变量、静态全局变量与全局变量的捕获,会稍有不同,其中:

  • 全局变量与静态全局变量:直接使用,因为地址一直是可以直接获取的。
  • 静态变量:捕获地址使用,因为 block 有可能会传递出创建时的作用域。
NSInteger globalValue = 1;
static NSInteger staticGlobalValue = 2;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        static NSInteger staticValue = 3;
        void(^block)(void) = ^{
            globalValue  = 1;
            staticGlobalValue  = 2;
            staticValue  = 3;
        };
        block();
    }
    return 0;
}
NSInteger globalValue = 1;
static NSInteger staticGlobalValue = 2;
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSInteger *staticValue;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger *_staticValue, int flags=0) : staticValue(_staticValue) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSInteger *staticValue = __cself->staticValue; // bound by copy
            globalValue  = 1;
            staticGlobalValue  = 2;
            (*staticValue)  = 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, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        static NSInteger staticValue = 3;
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &staticValue));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

带 __block 的自动变量

__block 修饰的自动变量,可以在 Block 内部对其外部的值进行修改:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block NSInteger value = 0;
        void(^block)(void) = ^{
            value = 10;
        };
        block();
        NSLog(@"%zd", value);
    }
    return 0;
}

这次生成的代码复杂了一些,不过只关注 value 部分的话可以发现,Block 为了捕获 __block 类型的自动变量,会生成 __Block_byref_value_0 结构体,并通过该结构体来实现对外部 __block 自动变量的捕获。

struct __Block_byref_value_0 { // 为捕获 __block 的自动变量,生成的结构体。为的是方便多个 Block 同时捕获一个自动变量时使用。
  void *__isa; // isa 指针
__Block_byref_value_0 *__forwarding; // 在 Block 单纯在栈上是,指向的是自己,拷贝到堆上后,指向的是在堆上的 Block。之所以需要这样的指针是因为当 Block 拷贝到堆上时,调用方式是统一的。
 int __flags;
 int __size;
 NSInteger value; // 具体的值
};
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_value_0 *value; // 通过引用的方式捕获 value,其中变量类型为 __Block_byref_value_0
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_value_0 *_value, int flags=0) : value(_value->__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_value_0 *value = __cself->value; // bound by ref
            (value->__forwarding->value) = 10; // 赋值代码
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->value, (void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->value, 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, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        __attribute__((__blocks__(byref))) __Block_byref_value_0 value = {(void*)0,(__Block_byref_value_0 *)&value, 0, sizeof(__Block_byref_value_0), 0};
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_value_0 *)&value, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_zz_zyxvpxvq6csfxvn_n0000000000000_T_main_6bf1c6_mi_0, (value.__forwarding->value));
    }
    return 0;
}

__block 可以用于解决 block 内部无法修改 auto 变量值的问题,__block 不能修饰全局变量、静态变量(static),编译器会将 __block 变量包装成一个对象。

block 在栈上时,并不会对 __block 变量产生强引用。

blockcopy 到堆时,会调用 block 内部的 copy 函数,copy 函数内部会调用 _Block_object_assign 函数,_Block_object_assign 函数会对 __block 变量形成强引用(retain)。

block 从堆中移除时,会调用 block 内部的 dispose 函数,dispose 函数内部会调用 _Block_object_dispose 函数,_Block_object_dispose 函数会自动释放引用的 __block 变量(release)。

捕获对象

在探究完对标量类型的捕获之后,让我们看一下对对象类型的捕获:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSArray *array = [NSArray array];
        void(^block)(void) = ^{
            NSLog(@"%@", array);
        };
        block();
    }
    return 0;
}

通过转译的代码可以看出,因为对象类型本身已经是存储在堆上的值了,所以直接获取其地址即可,同时其新增了两个函数 __main_block_copy_0__main_block_dispose_0,这两个函数是用来将对象拷贝到堆上和被从堆上移除时调用的,其内部又分别调用了 _Block_object_assign_Block_object_dispose 用来对捕获的对象进行引用计数的增加和减少。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSArray *array;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSArray *_array, int flags=0) : array(_array) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSArray *array = __cself->array; // bound by copy
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_zz_zyxvpxvq6csfxvn_n0000000000000_T_main_8ba4f7_mi_0, array);
        }
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*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
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, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NSArray *array = ((NSArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("array"));
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

Block 对象本身分为三种类型:

  • NSGlobalBlock:没有访问 auto 变量,调用 copy 方法之后不会发生变化。
  • NSStackBlock:访问了 auto 变量,调用 copy 方法之后存储位置从栈变为堆。
  • NSMallocBlock__NSStackBlock__ 调用了 copy 方法之后,引用计数增加。

ARC 环境下,编译器会根据情况自动将栈上的 block 复制到堆上,比如以下情况:

  • Block 作为函数返回值时
  • Block 赋值给 __strong 指针时
  • Block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时
  • Block 作为 GCD API 的方法参数时

所以,当 Block 内部访问了对象类型的 auto 变量时。如果 Block 是在栈上,将不会对 auto 变量产生强引用。

如果 Block 被拷贝到堆上,会调用 Block 内部的 copy 函数,copy 函数内部会调用 _Block_object_assign 函数,_Block_object_assign 函数会根据 auto 变量的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,形成强引用或者弱引用。

如果 Block 从堆上移除,会调用 Block 内部的 dispose 函数,dispose 函数内部会调用 _Block_object_dispose 函数。_Block_object_dispose 函数会自动释放引用的 auto 变量(release)。

__block 对象类型的捕获

如果想在 Block 中,对捕获的对象的指针指向进行修改,则需要添加 __block 关键字:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block NSArray *array = [NSArray array];
        void(^block)(void) = ^{
            array = [NSArray array];
            NSLog(@"%@", array);
        };
        block();
    }
    return 0;
}

通过转译我们可以看出,跟 __block 修饰的标量类型相似,同样会生成 __Block_byref_array_0 结构体来捕获对象类型。同时其内部生成了 __Block_byref_id_object_copy__Block_byref_id_object_dispose 两个函数指针,用于对被结构体包装的对象进行内存管理。

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst   40, *(void * *) ((char*)src   40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src   40), 131);
}
struct __Block_byref_array_0 {
  void *__isa;
__Block_byref_array_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSArray *array;
};
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_array_0 *array; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_array_0 *_array, int flags=0) : array(_array->__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_array_0 *array = __cself->array; // bound by ref
            (array->__forwarding->array) = ((NSArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("array"));
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_zz_zyxvpxvq6csfxvn_n0000000000000_T_main_3593f0_mi_0, (array->__forwarding->array));
        }
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, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 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, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        __attribute__((__blocks__(byref))) __Block_byref_array_0 array = {(void*)0,(__Block_byref_array_0 *)&array, 33554432, sizeof(__Block_byref_array_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("array"))};
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_array_0 *)&array, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

block 在栈上时,对它们都不会产生强引用。

block 拷贝到堆上时,都会通过 copy 函数来处理它们,__block 变量(假设变量名叫做 a):

_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

对象类型的 auto 变量(假设变量名叫做 p):

_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

block 从堆上移除时,都会通过 dispose 函数来释放它们,__block 变量(假设变量名叫做 a):

_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

对象类型的 auto 变量(假设变量名叫做 p):

_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

以上就是详解Objective C 中Block如何捕获外部值的详细内容,更多关于Objective C Block捕获外部值的资料请关注Devmax其它相关文章!

详解Objective C 中Block如何捕获外部值的更多相关文章

  1. ios – NSGenericException reason Collection

    这是我在出现SKScene时看到的错误,此错误随机发生,无法复制*由于未捕获的异常’NSGenericException’而终止应用程序,原因:’*Collection在被列举时发生了变异.发生了什么?

  2. ios – 如何在Xcode堆栈跟踪中查找NSInternalInconsistencyException的来源

    我没有在此堆栈跟踪中的任何位置看到任何可识别的文件名,方法调用或行号.或者,如果不是简单地读取堆栈跟踪并且涉及其他技术,那么开发人员应该采取哪些适当的下一步来追踪此类错误的来源?

  3. ios – NSURLConnectionLoader线程中的奇怪崩溃

    我们开始看到我们的应用启动时发生的崩溃.我无法重现它,它只发生在少数用户身上.例外情况是:异常类型:EXC_BAD_ACCESS代码:KERN_INVALID_ADDRESS位于0x3250974659崩溃发生在名为com.apple.NSURLConnectionLoader的线程中在调用时–[NSBlockOperationmain]这是该线程的堆栈跟踪:非常感谢任何帮助,以了解可能导致这种崩

  4. ios – 为什么不保留__block变量(在非ARC环境中)?

    我正在阅读__blockvariables上的文档,并考虑我使用__block的情况.对我来说,似乎我需要两种情况:>在块中使用时将变量标记为读写>在块内引用self时避免保留周期从表面上看,这两件事似乎并不相关.我认为__block变量没有被保留为更多的技巧我需要记住避免保留周期的特定用例.我想知道,为什么不能保留它们是否有更重要的建筑理由?

  5. ios – 崩溃错误代码:-1001错误:NSURLErrorTimedOut

    我遇到以下事故:崩溃只出现在iOS9上,没有在iOS8上,但这可能只是巧合.我也通过Crittercism面包屑看到这个:我也使用了后台提取,它打到了网络层.任何想法为什么这是打嗝?

  6. ios – CGContextSaveGState:应用启动时的无效上下文0x0

    FWW我的应用程序大多是用Swift2.0编写的,在iOS9上使用Xcode7b6.解决方法我遇到同样的问题,其原因是,我正在为不同的屏幕设置不同的状态栏样式.因此,我需要在plist中添加“查看基于控制器的状态栏外观”键.如果我删除此键,警告消失.

  7. 使用ShareKit ios的链接器错误

    我花了3天时间来搜索解决方案.我不知道该怎么做…

  8. 当Xcode 4.6打开故事板时,ios – Xcode 5崩溃

    我从2个月前开始使用Xcode4.6的项目,现在我想发送给我的应用程序一切都可以,但是当我尝试在iOS7上运行应用程序时,我看到一些错误;所以我已经下载了新的Xcode5以查看错误在哪里,但是发生了什么事情是当我尝试打开与IBXcode崩溃的故事板.我试图在互联网上看到如何解决这个问题,但我没有找到任何关于这一点.此外,当我尝试使用iOS7在设备上运行应用程序时,Xcode给我的错误是:解决方法问

  9. ios – __copy_helper_block_在AVFoundation中崩溃

    我的视频处理应用程序中发生了一次奇怪的崩溃.它使用AVFoundation处理视频和视频.用于过滤的音频和GPUImage.我自己从未遇到过这个问题,但在将它发布到AppStore之后,它经常出现在Crashlytics中.这是崩溃的日志:这就是我将AVFoundation连接到GPUImage的方法:任何想法我的代码有什么问题?

  10. ios – Xcode 10 Swift构建错误:“将非转义值转换为’T’可能允许它逃脱”

    ,block:@escaping@convention–>Void){@noescape现在默认为

随机推荐

  1. iOS实现拖拽View跟随手指浮动效果

    这篇文章主要为大家详细介绍了iOS实现拖拽View跟随手指浮动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  2. iOS – genstrings:无法连接到输出目录en.lproj

    使用我桌面上的项目文件夹,我启动终端输入:cd然后将我的项目文件夹拖到终端,它给了我路径.然后我将这行代码粘贴到终端中找.-name*.m|xargsgenstrings-oen.lproj我在终端中收到此错误消息:genstrings:无法连接到输出目录en.lproj它多次打印这行,然后说我的项目是一个目录的路径?没有.strings文件.对我做错了什么的想法?

  3. iOS 7 UIButtonBarItem图像没有色调

    如何确保按钮图标采用全局色调?解决方法只是想将其转换为根注释,以便为“回答”复选标记提供更好的上下文,并提供更好的格式.我能想出这个!

  4. ios – 在自定义相机层的AVFoundation中自动对焦和自动曝光

    为AVFoundation定制图层相机创建精确的自动对焦和曝光的最佳方法是什么?

  5. ios – Xcode找不到Alamofire,错误:没有这样的模块’Alamofire’

    我正在尝试按照github(https://github.com/Alamofire/Alamofire#cocoapods)指令将Alamofire包含在我的Swift项目中.我创建了一个新项目,导航到项目目录并运行此命令sudogeminstallcocoapods.然后我面临以下错误:搜索后我设法通过运行此命令安装cocoapodssudogeminstall-n/usr/local/bin

  6. ios – 在没有iPhone6s或更新的情况下测试ARKit

    我在决定下载Xcode9之前.我想玩新的框架–ARKit.我知道要用ARKit运行app我需要一个带有A9芯片或更新版本的设备.不幸的是我有一个较旧的.我的问题是已经下载了新Xcode的人.在我的情况下有可能运行ARKit应用程序吗?那个或其他任何模拟器?任何想法或我将不得不购买新设备?解决方法任何iOS11设备都可以使用ARKit,但是具有高质量AR体验的全球跟踪功能需要使用A9或更高版本处理器的设备.使用iOS11测试版更新您的设备是必要的.

  7. 将iOS应用移植到Android

    我们制作了一个具有2000个目标c类的退出大型iOS应用程序.我想知道有一个最佳实践指南将其移植到Android?此外,由于我们的应用程序大量使用UINavigation和UIView控制器,我想知道在Android上有类似的模型和实现.谢谢到目前为止,guenter解决方法老实说,我认为你正在计划的只是制作难以维护的糟糕代码.我意识到这听起来像很多工作,但从长远来看它会更容易,我只是将应用程序的概念“移植”到android并从头开始编写.

  8. ios – 在Swift中覆盖Objective C类方法

    我是Swift的初学者,我正在尝试在Swift项目中使用JSONModel.我想从JSONModel覆盖方法keyMapper,但我没有找到如何覆盖模型类中的Objective-C类方法.该方法的签名是:我怎样才能做到这一点?解决方法您可以像覆盖实例方法一样执行此操作,但使用class关键字除外:

  9. ios – 在WKWebView中获取链接URL

    我想在WKWebView中获取tapped链接的url.链接采用自定义格式,可触发应用中的某些操作.例如HTTP://我的网站/帮助#深层链接对讲.我这样使用KVO:这在第一次点击链接时效果很好.但是,如果我连续两次点击相同的链接,它将不报告链接点击.是否有解决方法来解决这个问题,以便我可以检测每个点击并获取链接?任何关于这个的指针都会很棒!解决方法像这样更改addobserver在observeValue函数中,您可以获得两个值

  10. ios – 在Swift的UIView中找到UILabel

    我正在尝试在我的UIViewControllers的超级视图中找到我的UILabels.这是我的代码:这是在Objective-C中推荐的方式,但是在Swift中我只得到UIViews和CALayer.我肯定在提供给这个方法的视图中有UILabel.我错过了什么?我的UIViewController中的调用:解决方法使用函数式编程概念可以更轻松地实现这一目标.

返回
顶部