Preface to 0.
Recently, I saw a Block interview question, which is quite interesting. Let me share it with you.
In this paper, starting from a Block interview question, layer by layer in-depth to explain the Block principle, the interview question to eat thoroughly.
An aside:
Many people find the definition of Block weird and hard to remember. But you can easily remember it by comparing it to the C definition of a function pointer.
// Block
returnType (^blockName)(parameterTypes)
// Function pointer
returnType (*c_func)(parameterTypes)
Copy the code
For example, both the input and return arguments are strings:
(char *) (*c_func)(const char *);
(NSString *) (^block)(NSString *);
Copy the code
Ok, let’s begin
1. The interview questions
1.1 problem 1
Does the following code leak memory?
- There is no
- There are
- (void)viewDidLoad {
[super viewDidLoad];
NSNotificationCenter* __weak center = [NSNotificationCenter defaultCenter];
id token = [center addObserverForName:UIApplicationDidEnterBackgroundNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification * _Nonnull note) {
[self doSomething];
[center removeObserver:token];
}];
}
- (void)doSomething {
}
Copy the code
The answer is yes!
1.1.1 analysis
-
In block, the external variables we use are self and center, and center has an __weak specifier, which is fine.
-
The center holds the token, the token holds the block(as you’ll see in the next screenshot), and the block holds self, which means that self can’t be released until the token is released.
If you have any doubt about the token held by Center, you can post wkCenter in LLDB.
-
We note that [Center removeObserver:token]; This step removes the token from the center. Should Center and Self be freed?
Let’s see what the compiler says:
The compiler tells us that the token was not initialized before being captured by the block! [center removeObserver:token]; The token cannot be removed correctly, so self cannot be freed!
Why is it not initialized?
The token will not be returned until the subsequent method has executed. Method is executed without the token being returned, so an uninitialized value is captured!
1.2 question 2
Does the following code leak memory?
- There is no
- There are
- (void)viewDidLoad {
[super viewDidLoad];
NSNotificationCenter* __weak center = [NSNotificationCenter defaultCenter];
id __block token = [center addObserverForName:UIApplicationDidEnterBackgroundNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification * _Nonnull note) {
[self doSomething];
[center removeObserver:token];
}];
}
- (void)doSomething {
}
Copy the code
This time the code adds a __block specifier before the token.
Tip: This time the compiler does not warn that the token is not initialized.
The answer is yes!
1.2.1 analysis
First, verify that the token value is correct, and you can also see that the token actually holds the block.
So why did it leak?
Because, although the Center no longer holds the token, the token is now held by the Block.
Some students might ask:
Add the __block specifier to the token object. The token object can be retrieved only after center returns.
The reason for this is explained in the Block Principle section.
1.3 question 3
Does the following code leak memory?
- There is no
- There are
- (void)viewDidLoad {
[super viewDidLoad];
NSNotificationCenter* __weak center = [NSNotificationCenter defaultCenter];
id __block token = [center addObserverForName:UIApplicationDidEnterBackgroundNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification * _Nonnull note) {
[self doSomething];
[center removeObserver:token];
token = nil;
}];
}
- (void)doSomething {
}
- (void)dealloc {
NSLog(@"%s", __FUNCTION__);
}
Copy the code
The answer is no!
1.3.1 analysis
We can verify this:
As you can see, we add token = nil; After that, the ViewController is released correctly. This step breaks the circular reference between the token and the block, so it is released correctly.
One might say:
Use __weak Typeof (self) wkSelf = self; I can solve the problem of self not releasing.
Sure, this solves the problem of self not releasing, but there is still a memory leak!
2. Principle of Block
Although the interview questions were solved, there were still a few questions that were not clear:
- Why not?
__block
specifiertoken
It’s not initialized, but it’s fine with this specifier? token
andblock
Why circular references?
2.1 Block captures automatic variables
Let’s start with a simple question:
After a Block is converted to C, the automatic variables used in the Block are appended as member variables to the __X_block_impl_Y structure, where X is the function name and Y is the Block number, such as the 0th structure in main: __main_block_impl_0.
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[])
{
@autoreleasepool
{
int age = 10;
MyBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();
}
return 0;
}
Copy the code
By the way, this output: age = 10
On the command line, do the following for this file:
clang -w -rewrite-objc main.m
Copy the code
or
xcrun -sdk iphoneos clang -arch arm64 -w -rewrite-objc main.m
Copy the code
The difference is that there is less SDK and architecture code specified below.
After processing, a main. CPP file will be generated. When you open it, you will find a lot of code. Search int Main to see the familiar code.
int main(int argc, const char * argv[])
{
/* @autoreleasepool */
{ __AtAutoreleasePool __autoreleasepool;
int age = 10;
MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
age = 18;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
Copy the code
Here are some structures involved in the main function:
struct __main_block_impl_0 {
struct __block_impl impl; // Block the imp structure of the function
struct __main_block_desc_0* Desc; // Block information
int age; // The age value referenced by the value
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock; // Stack block
impl.Flags = flags;
impl.FuncPtr = fp; // The imp pointer to the function is passedDesc = desc; }};struct __block_impl {
void *isa; // Block types: global, stack, heap
int Flags;
int Reserved;
void *FuncPtr; // Function pointer! That's how you call a block!
};
static struct __main_block_desc_0 { // Block information
size_t reserved;
size_t Block_size; // Block size
} __main_block_desc_0_DATA = { 0.sizeof(struct __main_block_impl_0)};
Copy the code
So with that information, let’s take a look
MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
Copy the code
As you can see, age is passed when the block is initialized, so age=10 in the block structure, so it prints age=10.
2.2 __block specifier
There are two ways to modify a captured automatic variable in a Block:
-
Use static variables, static global variables, global variables
From Block syntax to C language function access static global variables, global variables, no difference, can be directly accessed. Static variables are accessed using Pointers to static variables.
Automatic variables cannot be accessed as static variables. The reason is that automatic variables are stored on the stack and are released by the stack when they are out of scope. Static variables, on the other hand, are stored on the heap and are not released when out of scope, so they are still accessible.
-
Add the __block modifier
__block Storage domain class specifier. The storage domain specifier specifies the domain in which variables are stored, such as stack Auto, heap static, global extern, and register.
For example, add a __block specifier to the code we just wrote:
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[])
{
@autoreleasepool
{
int __block age = 10;
MyBlock block = ^{
age = 18;
};
block();
}
return 0;
}
Copy the code
On the command line, do the following for this file:
xcrun -sdk iphoneos clang -arch arm64 -w -rewrite-objc main.m
Copy the code
We see that the main function has changed:
-
Int age = 10;
-
__Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10}; .
int main(int argc, const char * argv[])
{
/* @autoreleasepool */
{ __AtAutoreleasePool __autoreleasepool;
__Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0.sizeof(__Block_byref_age_0), 10};
MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
Copy the code
It turns out that by adding a __block specifier, we can modify automatic variables within blocks.
Congratulations, now you have reached the second level! The __block specifier actually contains automatic variables in a structure.
This explains why the token gets the correct value in question 1 by adding a __block specifier.
MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
Copy the code
During block initialization, we passed the age structure into the block structure, which is now a pointer reference.
struct __Block_byref_age_0 {
void *__isa; / / isa pointer
__Block_byref_age_0 *__forwarding; // A pointer to oneself
int __flags; / / tag
int __size; // Structure size
int age; // A member variable that stores the age value
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // Struct pointer reference
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }};Copy the code
Let’s look at how the block changes the age value:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // Get the pointer to the age structure through the self pointer
(age->__forwarding->age) = 18; // Use the age structure pointer to change the age value
}
Copy the code
If you don’t understand what __forwarding does, we’ll get to that later. We now know that age is the pointer reference that was successfully modified.
2.3 Block Storage Domains
From C code we can see that Block refers to the Block struct instance, __block variable is the stack __block variable struct instance. Isa = &_nsConcretestackBlock; Before, we used stack blocks.
There are three types of blocks:
- The _NSConcreteGlobalBlock class object is stored in the program’s.data section.
- The _NSConcreteStackBlock class object is stored on the stack.
- The _NSConcreteMallocBlock class object is stored on the heap.
void (^blk)(void) = ^{ NSLog(@"Global Block"); }; int main() { blk(); NSLog(@"%@",[blk class]); // Print: __NSGlobalBlock__}Copy the code
Global blocks are definitely stored in the global data area, but blocks created on the function stack, if no automatic variables are captured, are structed as _NSConcreteGlobalBlock, not _NSConcreteStackBlock:
void (^blk0)(void) = ^ {// No blocks of automatic variables are intercepted
NSLog(@"Stack Block");
};
blk0();
NSLog(@ "% @",[blk0 class]); / / print: __NSGlobalBlock__
int i = 1;
void (^blk1)(void) = ^ {// Intercept the Block of automatic variable I
NSLog(@"Capture:%d", i);
};
blk1();
NSLog(@ "% @",[blk1 class]); // Print: __NSMallocBlock__
Copy the code
You can see that the class printed with blocks that do not capture automatic variables is NSGlobalBlock, which means stored in the global data area. But why does the Block that captures the automatic variable print the NSMallocBlock on the heap instead of the NSStackBlock on the stack? This will be explained later.
Blocks that are set on the stack are freed if they go out of scope. If the __block variable is also configured on the stack, it can also be freed. So, when the copy method is called, the __block variable is also copied to the heap, and impl. Isa = &_NSConcretemalLocblock; . After copying, the __forwarding pointer to the __block variable on the stack points to objects on the heap. Because of this, __block variables can be accessed correctly whether they are allocated on the stack or heap.
How does the compiler determine when it needs to copy?
When ARC is enabled, automatically judge to copy:
- Call copy manually.
- When a Block is returned as a function parameter, the compiler automatically copies it.
- Assign a Block tocopyModifies an ID class or Block type member variable, or
__strong
Modify the automatic variable. - The method name contains
usingBlock
theCocoa
Framework approach orGCD
The relevant API passes blocks.
If automatic copy is not available, we need to manually call the copy method to copy it to the heap. Such as passing blocks to arguments that do not include the methods or functions mentioned above.
In an ARC environment, when an object is returned, the object is copied to a temporary instance pointer, the retain operation is performed, and the object pointer is returned. Runtime /objc-arr.mm mentions that the retain operation on Block objc_retainBlock is actually Block_copy. After the retain operation objc_retainBlock, the Block on the stack is copied to the heap and the address on the heap is returned as a pointer to the temporary variable.
2.4 __block variable storage domain
When a Block is copied from the stack to the heap, the __block variable is also copied to the heap and held by the Block.
- If the __block variable is already on the heap, it is held by the Block.
- When a Block configured on the heap is freed, the __block variable it holds is also freed.
__block int val = 0;
void (^block)(void) = [^{ ++val; } copy];
++val;
block();
Copy the code
With copy, both Block and __block variables are copied from the stack to the heap. Either {++val; } or + + val; ++(val->__forwarding->val); .
The variable val in the Block is an instance of the __block variable structure copied to the heap, and the variable val outside the Block is an instance of the __block variable structure copied to the stack, but the __forwarding member of this structure refers to the __block variable structure instance on the heap. As a result, the same __block variable can be accessed without any problems whether it is used inside or outside a Block.
C
Let’s look at the C code for the interview questions.
@interface Test : NSObject
@end
@implementation Test
- (void)test_notification {
NSNotificationCenter* __weak center = [NSNotificationCenter defaultCenter];
id __block token = [center addObserverForName:@"com.demo.perform.once"
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification * _Nonnull note) {
[self doSomething];
[center removeObserver:token];
token = nil;
}];
}
- (void)doSomething {
}
@end
Copy the code
3.1 rewrite
Do something about this file on the command line, since the __weak specifier is used, and some additional arguments need to be specified:
xcrun -sdk iphoneos clang -arch arm64 -w -rewrite-objc -fobjc-arc -mios-version-min=8.0. 0 -fobjc-runtime=ios8.0. 0 main.m
Copy the code
This is a little more complicated, but let’s just look at the important parts:
struct __Block_byref_token_0 {
void *__isa;
__Block_byref_token_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
__strong id token; // Token variable of type ID (strong)
};
struct __Test__test_notification_block_impl_0 {
struct __block_impl impl;
struct __Test__test_notification_block_desc_0* Desc;
Test *const __strong self; // Capture self (strong)
NSNotificationCenter* __weak center; // Center object (weak)
__Block_byref_token_0 *token; // A pointer to the token structure
__Test__test_notification_block_impl_0(void *fp, struct __Test__test_notification_block_desc_0 *desc, Test *const __strong _self.NSNotificationCenter* __weak _center, __Block_byref_token_0 *_token, int flags=0) : self(_self), center(_center), token(_token->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }};Copy the code
Now we see that the block structure __Test__test_notification_block_impl_0 holds the token, and we saw earlier that the token also holds the block, so we created a circular reference.
So that’s question number two.
Let’s see how block IMP resolves the circular reference problem:
static void __Test__test_notification_block_func_0(struct __Test__test_notification_block_impl_0 *__cself, NSNotification * _Nonnull __strong note) {
__Block_byref_token_0 *token = __cself->token; // bound by ref
Test *const __strong self = __cself->self; // bound by copy
NSNotificationCenter* __weak center = __cself->center; // bound by copy
((void(*) (id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("doSomething"));
((void(*) (id, SEL, id _Nonnull __strong(a))void *)objc_msgSend)((id)center, sel_registerName("removeObserver:"), (id)(token->__forwarding->token));
(token->__forwarding->token) = __null;
}
Copy the code
As you can see, token = nil; Is converted to (token->__forwarding->token) = __null; The token is removed from the block object. If you’re confused, let me switch it over:
(__cself->token->__forwarding->token) = __null; // __cself is a pointer to the block structure
Copy the code
3.2 Block types
Careful students may find that:
impl.isa = &_NSConcreteStackBlock;
Copy the code
This is a stack block, which should be reclaimed by the system at the end of the period. We’re using ARC and we’re calling a method that has usingBlock in its name, which will actively trigger copy, copy it to the heap.
4. To summarize
Blocks most often ask about circular references and memory leaks.
Points to note:
- The use of the __weak specifier
- The use of the __block specifier
- Whoever holds
- How do I break circular references
In addition, it needs to be emphasized again:
-
Block code in the interview question will leak memory if it is not executed once!
-
One might say use __weak Typeof (self) wkSelf = self; I can solve the problem of self not releasing.
Sure, this solves the problem of self not releasing, but there is still a memory leak! We still need to address the root of the problem.
Supplement:
The previous section focused on the circular reference of tokens and blocks. The ViewController problem was briefly mentioned, which you may not have noticed when you read it.
Here I singled it out to say:
Token and block loop references, while block holds self(ViewController), resulting in ViewController cannot be freed.
If you want to release the ViewController first (regardless of whether the block is executed or not), it is best to give the ViewController an __weak descriptor.
In addition, there are actually two ways to break circular references to tokens and blocks:
- manually
token = nil;
. token
Also use__weak
specifierid __block __weak token
.
Note:
The following statements are lax and potentially problematic:
The simplest and most crude solution: everyone is __weak.
NSNotificationCenter* __weak wkCenter = [NSNotificationCenter >defaultCenter]; __weak typeof(self) wkSelf = self; id __block __weak wkToken = [wkCenter addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { [wkSelf doSomething]; [wkCenter removeObserver:wkToken]; }]; Copy the code
So that depends on how NSNotificationCenter is implemented. Tokens use an __weak specifier, but if the NSNotificationCenter does not hold the token, the token is destroyed at the end of the function scope. There is no problem with circular references, but it may cause the observer to be unable to be removed.
If you found this article helpful, give me a thumbs up