深入理解 OC/C++ 闭包

深入理解 OC/C++ 闭包

布景

苹果的 Objective-C 编译器答应用户在同一个源文件里自由地混合运用 C++和 Objective-C,混编后的言语叫 Objective-C++。相对于其它言语(例如 Swift、Kotlin、Dart 等)和 C++的文件阻隔和架桥通信(例如 Kotlin 运用JNI,Dart 运用FFI),Objective-C 和 C++的同文件混编办法无疑是令人酣畅的。OC/C++混编尽管能够在一个文件中进行编写,可是有一些注意事项需求了解:Objective-C++没有为 OC 类添加 C++的功用,也没有为 C++添加 OC 的功用,例如:不能用 OC 语法调用 C++目标,也不能为 OC 目标添加结构函数和析构函数,也不能将thisself彼此替换运用。类的体系结构是独立的,C++类不能承继 OC 类,OC 类也不能承继 C++类。

本文首要就之前令人困惑的 OC 的Block和 C++的lambda混编问题做一些探究。

试验环境:C++版别为 C++14,OC 只局限于 ARC。

基本了解

在深入探究之前,先经过比照的办法了解下二者:

语法

^(intx,NSString*y){}//ObjC,takeintandNSString*
[](intx,std::stringy){}//C++,takeintandstd::string
^{return42;}//ObjC,returnsint
[]{return42;}//C++,returnsint
^int{if(something)return42;elsereturn43;}
[]()->int{if(something)return42;elsereturn43;}

原理

OC 的Block的底层能够参阅《深入研究 Block 捕获外部变量和 __block 完成原理》(halfrost.com/ios_block/ ),这儿不做深入探究,仅仅是要打开代码到达比照作用。

-(void)viewDidLoad{
[superviewDidLoad];
intx=3;
void(^block)(int)=^(inta){
NSLog(@"%d",x);
};
block(5);
}

经过clang -rewrite-objc重写,能够得到以下成果:

struct__ViewController__viewDidLoad_block_impl_0{
struct__block_implimpl;
struct__ViewController__viewDidLoad_block_desc_0*Desc;
intx;
__ViewController__viewDidLoad_block_impl_0(void*fp,struct__ViewController__viewDidLoad_block_desc_0*desc,int_x,intflags=0):x(_x){
impl.isa=&_NSConcreteStackBlock;
impl.Flags=flags;
impl.FuncPtr=fp;
Desc=desc;
}
};
staticvoid__ViewController__viewDidLoad_block_func_0(struct__ViewController__viewDidLoad_block_impl_0*__cself,inta){
intx=__cself->x;//boundbycopy
NSLog((NSString*)&__NSConstantStringImpl__var_folders_st_jhg68rvj7sj064ft0rznckfh0000gn_T_ViewController_d02516_mii_0,x);
}
staticstruct__ViewController__viewDidLoad_block_desc_0{
size_treserved;
size_tBlock_size;
}__ViewController__viewDidLoad_block_desc_0_DATA={0,sizeof(struct__ViewController__viewDidLoad_block_impl_0)};
staticvoid_I_ViewController_viewDidLoad(ViewController*self,SEL_cmd){
((void(*)(__rw_objc_super*,SEL))(void*)objc_msgSendSuper)((__rw_objc_super){(id)self,(id)class_getSuperclass(objc_getClass("ViewController"))},sel_registerName("viewDidLoad"));
intx=3;
void(*block)(int)=((void(*)(int))&__ViewController__viewDidLoad_block_impl_0((void*)__ViewController__viewDidLoad_block_func_0,&__ViewController__viewDidLoad_block_desc_0_DATA,x));
((void(*)(__block_impl*,int))((__block_impl*)block)->FuncPtr)((__block_impl*)block,5);
}

而 C++lambda采取了天壤之别的的完成机制,会把lambda表达式转换为一个匿名 C++类。这儿凭借cppinsights看下 C++lambda的完成。

#include<cstdio>
structA{
intx;
inty;
};
intmain()
{
Aa={1,2};
intm=3;
autoadd=[&a,m](intn)->int{
returnm+n+a.x+a.y;
};
m=30;
add(20);
}
#include<cstdio>
structA
{
intx;
inty;
};
intmain()
{
Aa={1,2};
intm=3;
class__lambda_12_15
{
public:
inlineintoperator()(intn)const
{
return((m+n)+a.x)+a.y;
}
private:
A&a;
intm;
public:
__lambda_12_15(A&_a,int&_m)
:a{_a}
,m{_m}
{}
};
__lambda_12_15add=__lambda_12_15{a,m};
m=30;
add.operator()(20);
return0;
}

能够看到:lambda表达式add被转换为类__lambda_12_15,且重载了操作符(),对add的调用也被转换为对add.operator()的调用。

捕获变量

OCBlock只或许经过一般办法和__block办法捕获变量:

intx=42;
void(^block)(void)=^{printf("%d\n",x);};
block();//prints42
__blockintx=42;
void(^block)(void)=^{x=43;};
block();//xisnow43

C++lambda带来了更多的灵活性,能够经过以下这些办法捕获变量:

[]Capturenothing
[&]Captureanyreferencedvariablebyreference
[=]Captureanyreferencedvariablebymakingacopy
[=,&foo]Captureanyreferencedvariablebymakingacopy,butcapturevariablefoobyreference
[bar]Capturebarbymakingacopy;don'tcopyanythingelse
[this]Capturethethispointeroftheenclosingclass
intx=42;
inty=99;
intz=1001;
autolambda=[=,&z]{
//can'tmodifyxoryhere,butwecanreadthem
z++;
printf("%d,%d,%d\n",x,y,z);
};
lambda();//prints42,99,1002
//zisnow1002

内存办理

OC 的Block和 C++lambda均起源于栈目标,然而二者的后续开展天壤之别。OC 的Block实质是 OC 目标,他们是经过引证办法存储,从来不会经过值办法存储。为了延伸生命周期,OCBlock有必要被仿制到堆上。OCBlock遵从 OC 的引证计数规则,copyrelease有必要平衡(Block_copyBlock_release同理)。初次仿制会把Block从栈上移动到堆上,再次仿制会添加其引证计数。当引证计数为 0 的时分,Block会被销毁,其捕获的目标会被release

C++lambda按值存储,而非按引证存储。所有捕获的变量都会作为匿名类目标的成员变量存储到匿名类目标中。当lambda表达式被仿制的时分,这些变量也都会被仿制,只需求触发恰当的结构函数和析构函数即可。这儿面有一个极其重要的点:经过引证捕获变量。这些变量是作为引证存储在匿名目标中的,他们并没有得到任何特殊待遇。这意味着这些变量的生命周期结束之后,lambda依然有或许会去拜访这些变量,从而造成未定义的行为或者溃散,例如:


 - (void)viewDidLoad {
    [super viewDidLoad];
    int x = 3;
    lambda = [&x]() -> void {
        NSLog(@"x = %d", x);
    };
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    lambda();
}
 // 从输出成果中能够看到x是一个随机值
2022-02-12 23:15:01.375925+0800 BlockTest[63517:1006998] x = 32767

相对来说,this指向的存储在堆上,它的生命周期有必定的确保,可是即使如此,也无法绝对确保生命周期安全,有些情况下需求凭借智能指针延伸生命周期。

autostrongThis=shared_from_this();
doSomethingAsynchronously([strongThis,this](){
someMember_=42;
});

闭包混合捕获问题

前面评论的内容都是彼此独立的,OC 的Block并未涉及 C++目标,C++的lambda也没有牵扯 OC 目标,这大概是我们最期望看到的,可是混编过程中会发现这仅仅自己的一厢情愿。二者往往会彼此把自己的魔杖伸向对方范畴,从而会引发一些比较隐晦的问题。

C++的lambda捕获OC目标

C++的lambda能够捕获 OC 变量吗?假如能够的话,会有循环引证的问题吗?假如有循环引证的问题,该怎样处理呢?

值捕获 OC 目标

如代码所示,在OCClass类中有一个 C++字段cppObj,在OCClass的初始化办法中,对cppObj进行了初始化,并对其字段callback进行了赋值。能够看到,在lambda中对self进行了捕获,依照前面的规则,能够以为值捕获。

classCppClass{
public:
CppClass(){
}
~CppClass(){
}
public:
std::function<void()>callback;
};
@implementationOCClass{
std::shared_ptr<CppClass>cppObj;
}
-(void)dealloc{
NSLog(@"%s",__FUNCTION__);
}
-(instancetype)init{
if(self=[superinit]){
cppObj=std::make_shared<CppClass>();
cppObj->callback=[self]()->void{
[selfexecuteTask];
};
}
returnself;
}
-(void)executeTask{
NSLog(@"executetask");
}
OCClass *ocObj = [[OCClass alloc] init];

不幸的是,这样的捕获办法会产生循环引证:OCClass目标ocObj持有cppObjcppObj经过callback持有了ocObj

深入理解 OC/C++ 闭包

看下对应的汇编代码,能够发现捕获的时分,触发了ARC语义,自动对self进行了retain

深入理解 OC/C++ 闭包

这几行汇编代码对self添加引证计数。

0x10cab31ea<+170>:movq-0x8(%rbp),%rdi
0x10cab31ee<+174>:movq0x5e7b(%rip),%rax;(void*)0x00007fff2018fa80:objc_retain
0x10cab31f5<+181>:callq*%rax

最终来看一下匿名类的参数,能够发现selfOCClass *类型,是一个指针类型。

深入理解 OC/C++ 闭包

那么能够简略地以为捕获伪代码如下,在ARC语义下会产生retain行为:

__strong__typeof(self)capture_self=self;
//打开
__strongOCClass*capture_self=self;

为了解决循环引证的问题,能够运用__weak

cppObj=std::make_shared<CppClass>();
__weak__typeof(self)wself=self;
cppObj->callback=[wself]()->void{
[wselfexecuteTask];
};

深入理解 OC/C++ 闭包

再次观察汇编代码,发现前面的objc_retain逻辑现已消失,替代的逻辑为objc_copyWeak

引证捕获 OC 目标

那么是否能够经过引证捕获来捕获self呢?

cppObj=std::make_shared<CppClass>();
cppObj->callback=[&self]()->void{
[selfexecuteTask];
};

能够看到汇编代码中相同没有objc_retain逻辑。

深入理解 OC/C++ 闭包

最终来看一下匿名类的参数,能够发现selfOCClass *&类型,是一个指针引证类型。

深入理解 OC/C++ 闭包

能够看到引证捕获并不会对self进行retain,能够简略的以为捕获伪代码如下,在ARC语义下不会产生retain行为。

__unsafe_unretained__typeof(self)&capture_self=self;
//打开
__unsafe_unretainedOCClass*&capture_self=self;

被捕获的 OC 目标什么时分开释?

以这个代码片段为例:

autocppObj=std::make_shared<CppClass>();
OCClass2*oc2=[[OCClass2alloc]init];
cppObj->callback=[oc2]()->void{
[oc2class];
};

深入理解 OC/C++ 闭包

能够看到,在CppClass的析构函数中对std::function进行了析构,而std::function则对其捕获的 OC 变量 oc2 进行了开释。

定论

C++lambda的实质是创建一个匿名结构体类型,用来存储捕获的变量。ARC会确保包括 OC 目标字段的 C++结构体类型遵从ARC语义:

  1. C++结构体的结构函数会将 OC 目标字段初始化为nil
  2. 当该 OC 目标字段被赋值的时分,会release掉之前的值,并retain新值(假如是block,会进行copy);
  3. 当 C++结构体的析构函数被调用的时分,会release掉 OC 目标字段。

C++lambda会经过值或者引证的办法捕获 OC 目标。

  1. 引证捕获 OC 目标相当于运用__unsafe_unretained,存在生命周期问题,本身比较危险,不太引荐;
  2. 而值捕获的办法相当于运用__strong,或许会引起循环引证,必要的时分能够运用__weak

OC 的 Block 怎样捕获 C++目标?

反过来看看 OC 的Block是怎样捕获 C++目标的。

代码中的HMRequestMonitor是一个 C++结构体,其中的WaitForDoneSignalDone办法首要是为了完成同步。

structHMRequestMonitor{
public:
boolWaitForDone(){returnis_done_.get();}
voidSignalDone(boolsuccess){done_with_success_.set_value(success);}
ResponseStruct&GetResponse(){returnresponse_;}
private:
.....
};

upload办法运用HMRequestMonitor目标,到达同步等候网络请求成果的目的(为了排版,代码有所调整)。

hermas::ResponseStructHMUploader::upload(
constchar*url,
constchar*request_data,
int64_tlen,
constchar*header_content_type,
constchar*header_content_encoding){
HMRequestModel*model=[[HMRequestModelalloc]init];
......
automonitor=std::make_shared<hermas::HMRequestMonitor>();
std::weak_ptr<hermas::HMRequestMonitor>weakMonitor(monitor);
DataResponseBlockblock=^(NSError*error,iddata,NSURLResponse*response){
weakMonitor.lock()->SignalDone(true);
};
[m_session_managerrequestWithModel:modelcallBackWithResponse:block];
monitor->WaitForDone();
returnmonitor->GetResponse();
}

这儿直接运用std::weak_ptr

不运用__block

深入理解 OC/C++ 闭包

深入理解 OC/C++ 闭包

经过试验能够得到以下定论:

  1. C++的目标会被 OC 的Block捕获,且经过值传递办法。经过断点能够发现调用的是std::weak_ptr的仿制结构函数。
template<class_Tp>
inline
weak_ptr<_Tp>::weak_ptr(weak_ptrconst&__r)_NOEXCEPT
:__ptr_(__r.__ptr_),
__cntrl_(__r.__cntrl_)
{
if(__cntrl_)
__cntrl_->__add_weak();
}
  1. monitor的弱引证计数改变如下:
  • 初始化monitor时,weak_count = 1;
  • 初始化weakMonitor时,weak_count = 2,添加 1;
  • OC Block 捕获后,weak_count = 4,添加了 2。经过观察汇编代码,有 2 处:
    • 初次捕获的时分,对weakMinotor进行了仿制,在汇编代码 142 行;
    • Block从栈上仿制到堆上的时分,再次对weakMinotor进行了仿制,在汇编 144 行;

这儿需求注意的是:C++的weak_count比较古怪,它的值 = 弱引证个数 + 1,这么设计的原因比较复杂,具体能够参阅:stackoverflow.com/questions/5…

假如此处不运用std::weak_ptr,而是直接捕获std::shared_ptr,被捕获后其强引证计数为 3,逻辑和上述的std::weak_ptr是一致的。(就实质上来说,std::shared_ptrstd::weak_ptr都是 C++类)

std::shared_ptr<hermas::HMRequestMonitor>monitor=std::make_shared<hermas::HMRequestMonitor>();
DataResponseBlockblock=^(NSError*_Nonnullerror,id_Nonnulldata,NSURLResponse*_Nonnullresponse){
monitor->SignalDone(true);
};
(lldb)pomonitor
std::__1::shared_ptr<hermas::HMRequestMonitor>::element_type@0x00006000010dda58strong=3weak=1

运用__block

那么是否能够运用__block修改被捕获的 C++变量呢?经过试验发现是可行的。

深入理解 OC/C++ 闭包

深入理解 OC/C++ 闭包

能够得到以下定论:

  1. OC 的Block能够经过引证传递办法捕获 C++目标;
  2. monitorweak引证计数如下:
  • 初始化monitor时,weak_count = 1;
  • 初始化weakMonitor时,weak_count = 2,添加 1;
  • OCBlock捕获后,weak_count = 2,首要是由于移动结构函数被触发,仅仅所有权的搬运,不会改变引证计数;

深入理解 OC/C++ 闭包

__block 的疑问

了解 C++的同学或许会疑问,这儿既然是移动结构函数被触发,仅仅所有权产生了搬运,意味着monitor作为右值被传递进来,现已变为nullptr被消亡,那么为什么示例中的monitor还能够持续拜访?能够来验证一下:

  1. 当初次执行完如下代码的时分

深入理解 OC/C++ 闭包

会发现monitor变量的地址为:

(lldb)po&monitor
0x0000700001d959e8
  1. 当执行block赋值的时分,会调用到std::shared_ptr的移动结构函数中:

深入理解 OC/C++ 闭包

深入理解 OC/C++ 闭包

  • 移动结构函数中的this地址为0x0000600003b0c830;
  • __r的地址也是0x0000700001d959e8,和monitor的地址一致。
  1. 当执行完block的时分,再次打印monitor的地址,会发现monitor的地址现已产生了改变,和第二步中的this保持了一致,这说明monitor现已变为第二步中的this
(lldb)po&monitor
0x0000600003b0c830

整个过程中,monitor前后地址产生了改变,分别是 2 个不同的std::shared_ptr目标。所以monitor还能够持续被拜访。

被捕获的 C++目标何时开释?

深入理解 OC/C++ 闭包

相同在 OC 的Block开释的时分,会对其捕获的 C++目标进行开释。

捕获 shared_from_this

C++的this是一个指针,实质就是一个整数,OC 的Block捕获this和捕获一个整数并没有实质上的差异,所以这儿不再具体评论。这儿要点看下 C++的shared_from_this类,它是 this 的智能指针版别。

一个 C++类假如想拜访shared_from_this,有必要承继自类enable_shared_from_this,并把自己的类名作为模板参数传入。

classCppClass:publicstd::enable_shared_from_this<CppClass>{
public:
CppClass(){}
~CppClass(){}
voidattachOCBlock();
public:
OCClass2*ocObj2;
voiddosomething(){}
};
voidCppClass::attachOCBlock(){
ocObj2=[[OCClass2alloc]init];
autoshared_this=shared_from_this();
ocObj2.ocBlock=^{
shared_this->dosomething();
};
}
@interfaceOCClass2:NSObject
@propertyvoid(^ocBlock)();
@end
autocppObj=std::make_shared<CppClass>();
cppObj->attachOCBlock();

依据前面的定论,在CppClass成员函数attachOCBlock中,ocBlock直接捕获shared_from_this相同会引发循环引证,相同采取std::weak_ptr来解决。

voidCppClass::attachOCBlock(){
ocObj2=[[OCClass2alloc]init];
std::weak_ptr<CppClass>weak_this=shared_from_this();
ocObj2.ocBlock=^{
weak_this.lock()->dosomething();
};
}

定论

OC 的Block能够捕获 C++目标。

  1. 假如运用一般办法捕获栈上的 C++目标,会调用仿制结构函数;
  2. 假如运用__block办法捕获栈上的 C++目标,会调用移动结构函数,而且__block修饰的 C++目标在被捕获的时分,会进行重定向。

总结

本文一开始分别从语法、原理、变量捕获和内存办理 4 个维度,对 OC 的Block和 C++的lambda进行了简略的比照,然后花了较多的篇幅要点评论OC/C++的闭包混合捕获问题。之所以如此大费周章,是因为不想稀里糊涂地「猜测」和「试错」,只要深入了解背后机制,才干写出较好的OC/C++混编代码,同时也期望能给有相同困惑的读者带来一些帮助。然而对于OC/C++整个混编范畴来说,这仅仅是冰山一角,疑难问题依然重重,期待未来能带来更多的探究。

参阅文档

  1. isocpp.org/wiki/faq/ob…
  2. www.philjordan.eu/article/mix…
  3. releases.llvm.org/12.0.0/tool…
  4. releases.llvm.org/12.0.0/tool…
  5. mikeash.com/pyblog/frid…