布景
苹果的 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 目标添加结构函数和析构函数,也不能将this
和self
彼此替换运用。类的体系结构是独立的,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 的引证计数规则,copy
和release
有必要平衡(Block_copy
和Block_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
持有cppObj
,cppObj
经过callback
持有了ocObj
。
看下对应的汇编代码,能够发现捕获的时分,触发了ARC
语义,自动对self
进行了retain
。
这几行汇编代码对self
添加引证计数。
0x10cab31ea<+170>:movq-0x8(%rbp),%rdi
0x10cab31ee<+174>:movq0x5e7b(%rip),%rax;(void*)0x00007fff2018fa80:objc_retain
0x10cab31f5<+181>:callq*%rax
最终来看一下匿名类的参数,能够发现self
是OCClass *
类型,是一个指针类型。
那么能够简略地以为捕获伪代码如下,在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];
};
再次观察汇编代码,发现前面的objc_retain
逻辑现已消失,替代的逻辑为objc_copyWeak
。
引证捕获 OC 目标
那么是否能够经过引证捕获来捕获self
呢?
cppObj=std::make_shared<CppClass>();
cppObj->callback=[&self]()->void{
[selfexecuteTask];
};
能够看到汇编代码中相同没有objc_retain
逻辑。
最终来看一下匿名类的参数,能够发现self
是OCClass *&
类型,是一个指针引证类型。
能够看到引证捕获并不会对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];
};
能够看到,在CppClass
的析构函数中对std::function
进行了析构,而std::function
则对其捕获的 OC 变量 oc2 进行了开释。
定论
C++lambda
的实质是创建一个匿名结构体类型,用来存储捕获的变量。ARC
会确保包括 OC 目标字段的 C++结构体类型遵从ARC
语义:
- C++结构体的结构函数会将 OC 目标字段初始化为
nil
; - 当该 OC 目标字段被赋值的时分,会
release
掉之前的值,并retain
新值(假如是block
,会进行copy
); - 当 C++结构体的析构函数被调用的时分,会
release
掉 OC 目标字段。
C++lambda
会经过值或者引证的办法捕获 OC 目标。
- 引证捕获 OC 目标相当于运用
__unsafe_unretained
,存在生命周期问题,本身比较危险,不太引荐; - 而值捕获的办法相当于运用
__strong
,或许会引起循环引证,必要的时分能够运用__weak
。
OC 的 Block 怎样捕获 C++目标?
反过来看看 OC 的Block
是怎样捕获 C++目标的。
代码中的HMRequestMonitor
是一个 C++结构体,其中的WaitForDone
和SignalDone
办法首要是为了完成同步。
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
经过试验能够得到以下定论:
- 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();
}
-
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_ptr
和std::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 的
Block
能够经过引证传递办法捕获 C++目标; -
monitor
的weak
引证计数如下:
- 初始化
monitor
时,weak_count = 1
; - 初始化
weakMonitor
时,weak_count = 2
,添加 1; - OC
Block
捕获后,weak_count = 2
,首要是由于移动结构函数被触发,仅仅所有权的搬运,不会改变引证计数;
__block 的疑问
了解 C++的同学或许会疑问,这儿既然是移动结构函数被触发,仅仅所有权产生了搬运,意味着monitor
作为右值被传递进来,现已变为nullptr
被消亡,那么为什么示例中的monitor
还能够持续拜访?能够来验证一下:
- 当初次执行完如下代码的时分
会发现monitor
变量的地址为:
(lldb)po&monitor
0x0000700001d959e8
- 当执行
block
赋值的时分,会调用到std::shared_ptr
的移动结构函数中:
- 移动结构函数中的
this
地址为0x0000600003b0c830
; -
__r
的地址也是0x0000700001d959e8
,和monitor
的地址一致。
- 当执行完
block
的时分,再次打印monitor
的地址,会发现monitor
的地址现已产生了改变,和第二步中的this
保持了一致,这说明monitor
现已变为第二步中的this
。
(lldb)po&monitor
0x0000600003b0c830
整个过程中,monitor
前后地址产生了改变,分别是 2 个不同的std::shared_ptr
目标。所以monitor
还能够持续被拜访。
被捕获的 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++目标。
- 假如运用一般办法捕获栈上的 C++目标,会调用仿制结构函数;
- 假如运用
__block
办法捕获栈上的 C++目标,会调用移动结构函数,而且__block
修饰的 C++目标在被捕获的时分,会进行重定向。
总结
本文一开始分别从语法、原理、变量捕获和内存办理 4 个维度,对 OC 的Block
和 C++的lambda
进行了简略的比照,然后花了较多的篇幅要点评论OC/C++
的闭包混合捕获问题。之所以如此大费周章,是因为不想稀里糊涂地「猜测」和「试错」,只要深入了解背后机制,才干写出较好的OC/C++
混编代码,同时也期望能给有相同困惑的读者带来一些帮助。然而对于OC/C++
整个混编范畴来说,这仅仅是冰山一角,疑难问题依然重重,期待未来能带来更多的探究。
参阅文档
- isocpp.org/wiki/faq/ob…
- www.philjordan.eu/article/mix…
- releases.llvm.org/12.0.0/tool…
- releases.llvm.org/12.0.0/tool…
- mikeash.com/pyblog/frid…