Author: Cui Xiaobing backgroundApple's Objective-C compiler allows users to freely mix C++ and Objective-C in the same source file. The mixed language is called Objective-C++. Compared with the file isolation and bridge communication between other languages (such as Swift, Kotlin, Dart, etc.) and C++ (for example, Kotlin uses JNI, Dart uses FFI), the same-file mixed mode of Objective-C and C++ is undoubtedly comfortable. Although OC/C++ mixed can be written in one file, there are some precautions to be aware of: Objective-C++ does not add C++ functions to OC classes, nor does it add OC functions to C++. For example, you cannot call C++ objects with OC syntax, you cannot add constructors and destructors to OC objects, and you cannot use this and self interchangeably. The class architecture is independent. C++ classes cannot inherit OC classes, and OC classes cannot inherit C++ classes. This article mainly explores the previously confusing mixing of OC's Block and C++'s lambda.
Basic understandingBefore we go into more detail, let’s first compare the two: grammar^ ( int x , NSString * y ){} // ObjC, take int and NSString* principleFor the underlying structure of OC's Block, please refer to "In-depth Study of Block Capture of External Variables and __block Implementation Principles" (https://halfrost.com/ios_block/). We will not delve into it here, but just expand the code to achieve a comparison effect. - ( void ) viewDidLoad { Rewriting with clang -rewrite-objc gives the following results: struct __ViewController__viewDidLoad_block_impl_0 { C++ lambda uses a completely different implementation mechanism, which converts the lambda expression into an anonymous C++ class. Here we use cppinsights to look at the implementation of C++ lambda. #include < cstdio > #include < cstdio > As you can see, the lambda expression add is converted to the class __lambda_12_15, and the operator() is overloaded. The call to add is also converted to a call to add.operator(). Capturing variablesOC Block can only capture variables through normal methods and __block methods: int x = 42 ; __block int x = 42 ; C++ lambda brings more flexibility and can capture variables in the following ways: [] Capture nothing int x = 42 ; Memory ManagementOC Block and C++ lambda both originated from stack objects, but their subsequent development is completely different. OC Block is essentially an OC object. They are stored by reference and never by value. In order to extend the life cycle, OC Block must be copied to the heap. OC Block follows OC's reference counting rules, and copy and release must be balanced (the same applies to Block_copy and Block_release). The first copy will move the Block from the stack to the heap, and the second copy will increase its reference count. When the reference count is 0, the Block will be destroyed and the object it captures will be released. C++ lambda is stored by value, not by reference. All captured variables are stored in anonymous class objects as member variables of the anonymous class object. When the lambda expression is copied, these variables are also copied, and only the appropriate constructors and destructors need to be triggered. There is an extremely important point here: capturing variables by reference. These variables are stored as references in anonymous objects, and they do not receive any special treatment. This means that after the life cycle of these variables ends, lambda may still access these variables, resulting in undefined behavior or crashes, for example: - ( void ) viewDidLoad { Relatively speaking, the storage pointed to by this is on the heap, and its life cycle is guaranteed to a certain extent. However, even so, the life cycle safety cannot be absolutely guaranteed. In some cases, it is necessary to use smart pointers to extend the life cycle. auto strongThis = shared_from_this (); Closure mixed capture problemThe contents discussed above are all independent of each other. OC's Block does not involve C++ objects, and C++'s lambda does not involve OC objects. This is probably what we want to see most, but in the process of mixing, we will find that this is just wishful thinking. The two often extend their magic wands to each other's fields, which will cause some more puzzling problems. C++ lambda captures OC objectsCan C++ lambda capture Objective-C variables? If so, will there be a circular reference problem? If there is a circular reference problem, how to deal with it? Value capture OC objectAs shown in the code, there is a C++ field cppObj in the OCClass class. In the initialization method of OCClass, cppObj is initialized and its field callback is assigned a value. It can be seen that self is captured in lambda, which can be considered value capture according to the previous rules. class CppClass { @implementation OCClass { OCClass * ocObj = [[ OCClass alloc ] init ]; Unfortunately, this capture method will cause a circular reference: the OCClass object ocObj holds cppObj, and cppObj holds ocObj through callback. Looking at the corresponding assembly code, we can find that when capturing, ARC semantics is triggered and self is automatically retained. These lines of assembly code increase the reference count of self. 0x10cab31ea < + 170 > : movq - 0x8 ( % rbp ), % rdi Finally, let’s look at the parameters of the anonymous class. We can see that self is of type OCClass *, which is a pointer type. Then we can simply think of the capture pseudo code as follows, and the retain behavior will occur under ARC semantics: __strong __typeof ( self ) capture_self = self ; To solve the problem of circular references, you can use __weak. cppObj = std :: make_shared < CppClass > (); Looking at the assembly code again, we find that the previous objc_retain logic has disappeared and is replaced by objc_copyWeak. Reference capture OC objectSo is it possible to capture self by reference? cppObj = std::make_shared();cppObj->callback = [&self]() -> void { [self executeTask];}; You can see that there is no objc_retain logic in the assembly code either. Finally, let’s look at the parameters of the anonymous class. We can see that self is of type OCClass *&, which is a pointer reference type. It can be seen that reference capture does not retain self. You can simply think of the capture pseudo code as follows, and no retain behavior will occur under ARC semantics. __unsafe_unretained __typeof ( self ) & capture_self = self ; When is the captured OC object released?Take this code snippet as an example: auto cppObj = std :: make_shared < CppClass > (); As you can see, std::function is destructed in the destructor of CppClass, and std::function The captured OC variable oc2 is released. in conclusionThe essence of C++ lambda is to create an anonymous structure type to store captured variables. ARC will ensure that the C++ structure type containing OC object fields follows ARC semantics:
C++ lambda captures OC objects by value or reference.
How does OC's Block capture C++ objects?Let's look at how OC's Block captures C++ objects. The HMRequestMonitor in the code is a C++ structure, in which the WaitForDone and SignalDone methods are mainly used to achieve synchronization. struct HMRequestMonitor { The upload method uses the HMRequestMonitor object to synchronously wait for the result of the network request (the code has been adjusted for typesetting). hermas :: ResponseStruct HMUploader :: upload ( std::weak_ptr is used directly here. Not using __blockThe following conclusions can be drawn from the experiment: 1. The C++ object will be captured by the OC block and passed by value. Through the breakpoint, it can be found that the copy constructor of std::weak_ptr is called. template < class _Tp > 2. The weak reference count of monitor changes as follows:
It should be noted here that: C++'s weak_count is rather strange, its value = number of weak references + 1, the reason for this design is quite complicated, for details, please refer to: https://stackoverflow.com/questions/5671241/how-does-weak-ptr-work If std::weak_ptr is not used here, but std::shared_ptr is captured directly, its strong reference count is 3 after being captured, and the logic is the same as the above std::weak_ptr. (In essence, std::shared_ptr and std::weak_ptr are both C++ classes) std :: shared_ptr < hermas :: HMRequestMonitor > monitor = std :: make_shared < hermas :: HMRequestMonitor > (); ( lldb ) po monitor Using __blockSo is it possible to use __block to modify captured C++ variables? Through experiments, we found that it is feasible. The following conclusions can be drawn:
Questions about __blockStudents who know C++ may wonder, since the move constructor is triggered here, only the ownership is transferred, which means that the monitor is passed in as an rvalue and has become nullptr and is destroyed, then why can the monitor in the example still be accessed? Let's verify it: 1. When the following code is executed for the first time You will find that the address of the monitor variable is: ( lldb ) po & monitor 2. When the block assignment is executed, the move constructor of std::shared_ptr is called:
3. When the block is executed, print the address of monitor again, and you will find that the address of monitor has changed and is consistent with this in the second step, which means that monitor has become this in the second step. ( lldb ) po & monitor During the whole process, the address of monitor changes, and they are two different std::shared_ptr objects. Therefore, monitor can still be accessed. When are captured C++ objects released?Similarly, when the OC Block is released, the captured C++ object will be released. Capturing shared_from_thisThis in C++ is a pointer, which is essentially an integer. There is no essential difference between capturing this in an OC Block and capturing an integer, so we will not discuss it in detail here. Here we will focus on the shared_from_this class in C++, which is the smart pointer version of this. If a C++ class wants to access shared_from_this, it must inherit from the enable_shared_from_this class and pass its own class name as a template parameter. class CppClass : public std :: enable_shared_from_this < CppClass > { @interface OCClass2 : NSObject auto cppObj = std :: make_shared < CppClass > (); According to the previous conclusion, in the CppClass member function attachOCBlock, ocBlock directly capturing shared_from_this will also cause a circular reference, which can also be solved by using std::weak_ptr. void CppClass :: attachOCBlock () { in conclusionOC's Block can capture C++ objects.
SummarizeThis article begins with a simple comparison of OC's Block and C++'s Lambda from four dimensions: syntax, principle, variable capture, and memory management. It then spends a lot of space to focus on the mixed capture of closures in OC/C++. The reason for this is that I don't want to "guess" and "trial and error" in a muddled way. Only by deeply understanding the mechanism behind it can we write better OC/C++ mixed code. At the same time, I also hope to bring some help to readers who have the same confusion. However, for the entire OC/C++ mixed field, this is just the tip of the iceberg. There are still many difficult problems, and I look forward to more exploration in the future. Reference Documentation
|
Q: Can the mini program adopt a dividend model? A...
Produced by: Science Popularization China Author:...
1. Bugs and Debugging Speaking of "Debug&quo...
In the desert grasslands of Namibia in southern A...
The highest mountain on earth is the main peak of...
The Year of the Tiger is here, Fat Tiger wishes y...
8 sets of special templates for creating Tik Tok ...
It is said that sunbathing helps to grow taller, ...
Whether offline or online, where there are transa...
At around 23:00 on April 7, 2021, my country'...
As digital marketing enters the stock market, ref...
This image of the temporary "mini-moon"...
Recently, according to AutoBlog, as early as Sept...
The pace of life of modern people is indeed getti...
The emergence of the new consumption wave has led...