Summary: Classic crash of multithreading in iOS

Posted by darksniperx on Mon, 22 Jun 2020 09:02:04 +0200

preface

iOS crash is a headache for iOS developers. App crashes, indicating that there is a problem in code writing. It is very important to quickly locate the crash site. It's easier to find the problem in the debugging phase, but it's more troublesome to analyze the crash report of the app that has been launched.

This article will summarize and introduce some classic crashes of multithreading in iOS. I won't say much about it. Let's take a look at the detailed introduction.

Crash of Block callback

In the MRC environment, use Block to set the downloaded pictures. When self is released, weakSelf becomes a wild pointer, and then tragedy happens

__block ViewController *weakSelf = self;
[self.imageView imageWithUrl:@"" completedBlock:^(UIImage *image, NSError *error) {
NSLog(@"%@",weakSelf.imageView.description);
}];

The breakdown of Setter under multithreading

Getter & setter write too much, in the case of single thread, there is no problem. But in the case of multithreading, it can crash. Because[_ imageView release]; this code may be executed twice, oops!

UIKit is not a thread, so it is possible to call UIKit in a place that is not the main thread, which is completely OK in the development stage, and is directly free of testing. But once online, the crash system may be your crash log. Holy shit!

The solution is to check whether the thread currently called is the main thread through hook setNeedsLayout, setNeedsDisplay and setNeedsDisplayInRect.

-(void)setImageView:(UIImageView *)imageView
{
if (![_imageView isEqual:imageView])
{
[_imageView release];
_imageView = [imageView retain];
}
}

More Setter type crashes

Property property, the most written property is nonatomic, and generally there is no problem!

@interface ViewController ()
@property (strong,nonatomic) NSMutableArray *array;
@end

Run the following code and you will see:

malloc: error for object 0x7913d6d0: pointer being freed was not allocated

for (int i = 0; i < 100; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
self.array = [[NSMutableArray alloc] init];
});
}

The reason is: the object is repeatedly relaese. Check it out runtime source code

Solution: the attribute is declared atomic

A more common example:

if(handler == nil)
{
hander = [[Handler alloc] init];
}
return handler;

If two threads a and B access the if statement at the same time, the handler == nil condition is met, and both threads go to the next initialization instance

At this time, thread a completes initialization and assignment (we call it a in this instance), and then goes back to other logic. At this time, thread B starts to initialize and assign (we call it B in this instance). The handler will point to the object initialized by thread B. and instance a, which is initialized by thread a, is released because the reference count is reduced by 1 (to 0). But in thread a, The code will also try to access the address where a is located. The content in this address is unpredictable due to being released, resulting in a wild pointer

There is also a key point in the problem. During the calling process of a method of an object, the reference count of the object does not increase, so that if it is released, the subsequent access to the object in the execution process may result in the wild pointer [1]

Exception Type: SIGSEGV
Exception Codes: SEGV_ACCERR at 0x12345678
Triggered by Thread: 1

Simply add a lock to solve the problem:

@synchronized(self){
if(handler == nil)
{
hander = [[Handler alloc] init];
}
}
return handler;

Access to variables under multithreading

if (self.xxx) {
[self.dict setObject:@"ah" forKey:self.xxx];
}

Do you think this code is correct at first sight? Because the key has been set in advance self.xxx If it is a non nil judgment, subsequent instructions will be executed only if it is not nil. However, the above code is correct only under the premise of single thread.

Let's assume that the current thread of the above code is Thread A. when we finish executing if( self.xxx After the statement, CPU switches the execution power to Thread B, and at this time Thread B invokes a sentence. self.xxx = nil. Using local variables can solve this problem

__strong id val = self.xxx;
if (val) {
[self.dict setObject:@"ah" forKey:val];
}

This way, no matter how many threads try to self.xxx By modifying, the val in essence will remain in the existing state and conform to the judgment of non nil.

As a developer, it is particularly important to have a learning atmosphere and a communication circle. This is my iOS communication group: 519832104 Whether you are Xiaobai or Daniel, welcome to join us, share experience, discuss technology, and let's learn and grow together!

In addition, a copy of interview questions collected by all friends is attached. If you need iOS development learning materials and real interview questions, you can add advanced iOS development communication group, which can be downloaded by yourself!

dispatch_group collapse

dispatch_group_enter and leave must be matched, otherwise they will crash. When downloading multiple resources, it is often necessary to use multiple threads to download concurrently, and notify the user after all downloads are completed. Start downloading, dispatch_group_enter, download and complete the dispatch_group_leave . Very simple process, but when the code is complex to a certain extent or some third-party libraries are used, it is likely to cause problems.

dispatch_group_t serviceGroup = dispatch_group_create();
dispatch_group_notify(serviceGroup, dispatch_get_main_queue(), ^{
NSLog(@"Finish downloading :%@", downloadUrls);
});
// t is an array of strings
[downloadUrls enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
dispatch_group_enter(serviceGroup);
SDWebImageCompletionWithFinishedBlock completion =
^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
dispatch_group_leave(serviceGroup);
NSLog(@"idx:%zd",idx);
};
[[SDWebImageManager sharedManager] downloadImageWithURL:[NSURL URLWithString: downloadUrls[idx]] options:SDWebImageLowPriority progress:nil completed:completion];
}];

Use multithreading for concurrent download until all pictures are downloaded (can fail) for callback, and SDWebImage is used for picture download. The scene of crash is: there are 10 pictures, which are downloaded twice separately (A & B). One of the pictures in group B is the same as the one downloaded in group A. Suppose group a downloads the corresponding GroupA, group B

Here is the SDWebImage source code:

dispatch_barrier_sync(self.barrierQueue, ^{
SDWebImageDownloaderOperation *operation = self.URLOperations[url];
if (!operation) {
operation = createCallback();
// Pay attention to this line
self.URLOperations[url] = operation;
__weak SDWebImageDownloaderOperation *woperation = operation;
operation.completionBlock = ^{
SDWebImageDownloaderOperation *soperation = woperation;
if (!soperation) return;
if (self.URLOperations[url] == soperation) {
[self.URLOperations removeObjectForKey:url];
};
};
}
// Pay attention to this line
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
}

The downloader of SDWebImage will map the NSOperation corresponding to the download task according to the url, and the same url will map to the same unimplemented NSOperation. When Group A pictures are downloaded, the same url callback is GroupB instead of Group A. The Group B count is now 1. When all pictures in Group B are downloaded, the end count is 5 + 1. Because the number of enter is 5 and the number of leave is 6, it will crash!

Crash after the last holder is released

The object A is held by manager and called [Manager removeObjectA] in A. retainCount -1 of object A. when retainCount equals zero, object a has started to release. When removeobjecta is called, and then [self doSomething] is called, it crashes.

-(void)finishEditing
{
[Manager removeObject:self];
[self doSomething];
}

This happens when an array or dictionary contains an object and is the last holder of the object. When the object processing is not good, there will be the above crash. Another case is that when the objects in the array or dictionary have been released, when traversing the array or fetching the values in the dictionary, it will crash. In this case, it's very crashing, because sometimes the stack is like this:

Thread 0 Crashed:
0 libobjc.A.dylib 0x00000001816ec160 _objc_release :16 (in libobjc.A.dylib)
1 libobjc.A.dylib 0x00000001816edae8 __ZN12_GLOBAL__N_119AutoreleasePoolPage3popEPv :508 (in libobjc.A.dylib)
2 CoreFoundation 0x0000000181f4c9fc __CFAutoreleasePoolPop :28 (in CoreFoundation)
3 CoreFoundation 0x0000000182022bc0 ___CFRunLoopRun :1636 (in CoreFoundation)
4 CoreFoundation 0x0000000181f4cc50 _CFRunLoopRunSpecific :384 (in CoreFoundation)
5 GraphicsServices 0x0000000183834088 _GSEventRunModal :180 (in GraphicsServices)
6 UIKit 0x0000000187236088 _UIApplicationMain :204 (in UIKit)
7 Tmall4iPhone 0x00000001000b7ae4 main main.m:50 (in Tmall4iPhone)
8 libdyld.dylib 0x0000000181aea8b8 _start :4 (in libdyld.dylib)

The possible scenarios for this kind of stack are:

When a Dictionary is released, a value is released by other codes in advance and becomes a wild pointer. At this time, it is released again to trigger crash. If you can type all the key / values when each Dictionary is released, if a key/value is just typed out and crash occurs, then hang it on the newly typed key/value

The release thread of an object should be the same as the thread it handles things

Object A listens for notification events in the main thread if the object is released by another thread. At this moment, if object A is performing notification related operations, then accessing the object related resources will be A wild pointer, and crash will occur

performSelector:withObject:afterDelay:

Call this method. If it is not in the main thread, you must ensure that the ruuloop of the current thread exists. performSelector_xxx_afterDelay depends on runlopp to execute. Also use perfor mSelector:withObject : afterdelay: be careful when combining with cancelPreviousPerformRequestsWithTarget.

  • afterDelay will increase the reference count of the receiver, and cancel will decrease by one
  • If the reference count of the receiver is only 1 (delay only), the receiver will be destroyed immediately after calling cancel, and then the method of the receiver will crash

__weak typeof(self) weakSelf = self;
[NSObject cancelPreviousPerformRequestsWithTarget:self];
if (!weakSelf)
{
//NSLog(@"self destroyed");
return;
}
[self doOther];

summary

The above is the whole content of this article. I hope that the content of this article has a certain reference learning value for everyone's study or work. If you have any questions, you can leave a message for communication. Thank you for your support.

Click here to exchange and learn with iOS Daniel now

Topics: iOS Attribute