iOS 에서 다 중 스 레 드 의 고전 충돌 요약

머리말
iOS 붕 괴 는 iOS 개발 자 들 을 골 치 아 프 게 하 는 일이 다.앱 이 붕 괴 된 것 은 코드 에 문제 가 있다 는 것 을 설명 한다.이때 붕 괴 된 곳 을 어떻게 빨리 찾 느 냐 가 중요 하 다.디 버 깅 단 계 는 문제 가 발생 한 곳 을 쉽게 찾 을 수 있 지만 이미 출시 된 app 과 붕괴 보고 서 를 분석 하 는 것 은 비교적 번거롭다.
본 고 는 iOS 의 다 중 스 레 드 에 관 한 전형 적 인 붕 괴 를 정리 하고 자 합 니 다.다음은 더 이상 말 하지 않 겠 습 니 다.상세 한 소 개 를 해 보 겠 습 니 다.
0x0 Block 반전 의 붕괴
MRC 환경 에서 블록 을 사용 하여 다운로드 에 성공 한 그림 을 설정 합 니 다.self 가 풀 려 난 후,weak Self 는 야생 지침 이 되 었 고,이어서 비극 이 되 었 다.

 __block ViewController *weakSelf = self;
 [self.imageView imageWithUrl:@"" completedBlock:^(UIImage *image, NSError *error) { 
 NSLog(@"%@",weakSelf.imageView.description);
 }];
0x1 다 중 스 레 드 에서 Setter 의 충돌
Getter&Setter 를 많이 썼 습 니 다.단일 스 레 드 의 경우 문제 가 없습니다.하지만 다 중 스 레 드 의 경우 무 너 질 수 있다.왜냐하면imageView release]; 이 코드 는 두 번 실 행 될 수 있 습 니 다.oops!
UIKit 는 스 레 드 가 아니 므 로 메 인 스 레 드 가 아 닌 곳 에서 UIKit 를 호출 하 는 것 은 개발 단계 에서 전혀 문제 가 없 을 수 있 으 며 직접 테스트 를 면제 할 수 있 습 니 다.하지만 온라인 에 도착 하면 붕괴 시스템 은 모두 당신 의 붕괴 로그 일 수 있 습 니 다.Holy shit!
해결 방법:hook 을 통 해 setNeedsLayout,setNeedsDisplay,setNeedsDisplayInRect 를 통 해 현재 호출 된 스 레 드 가 주 스 레 드 인지 확인 합 니 다.

- (void)setImageView:(UIImageView *)imageView
{
 if (![_imageView isEqual:imageView])
 {
 [_imageView release];
 _imageView = [imageView retain];
 }
}
0x2 더 많은 Setter 형식의 충돌
property 의 속성,가장 많이 쓴 것 은 nonatomic 입 니 다.일반적인 상황 에서 도 문제 가 없습니다!

@interface ViewController ()
@property (strong,nonatomic) NSMutableArray *array;
@end
아래 코드 를 뛰 어보 면 보 실 수 있 습 니 다.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];
 });
 } 
대상 이 중복 되 었 기 때문이다.runtime 소스 코드 을 살 펴 보 겠 습 니 다.

해결 방법:속성 성명 은 atomic 입 니 다.
더 흔히 볼 수 있 는 예:

if(handler == nil)
{
 hander = [[Handler alloc] init];
}
return handler;
만약 에 A,B 두 스 레 드 가 if 문 구 를 동시에 방문 하면 handler == nil 조건 이 만족 하고 두 스 레 드 는 모두 다음 문장 으로 가서 인 스 턴 스 를 초기 화 합 니 다.
이때 A 스 레 드 는 초기 화 를 완료 하고 값 을 부여 합 니 다(이 인 스 턴 스 는 a 라 고 합 니 다).그리고 계속 뒤로 이동 합 니 다.이때 B 스 레 드 는 초기 화 되 고 값 을 부여 하기 시 작 했 습 니 다(이 인 스 턴 스 는 b 라 고 합 니 다).handler 는 B 스 레 드 가 초기 화 된 대상 을 가리 키 고 A 초기 화 된 인 스 턴 스 a 는 인용 계수 가 1(0 으로 감소)줄 어 들 었 기 때문에 방출 되 었 습 니 다.그러나 A 스 레 드 에 서 는....코드 는 a 가 있 는 주 소 를 방문 하려 고 시도 합 니 다.이 주소 의 내용 은 방출 되 어 예측 할 수 없 게 되 어 포인터 가 됩 니 다.
문 제 는 또 하나의 관건 적 인 점 이 있다.한 대상 의 특정한 방법 을 호출 하 는 과정 에서 이 대상 의 인용 수 는 증가 하지 않 고 풀 려 나 면 후속 적 인 실행 과정 에서 이 대상 에 대한 방문 은 야생 지침[1]을 초래 할 수 있다.

Exception Type: SIGSEGV
Exception Codes: SEGV_ACCERR at 0x12345678
Triggered by Thread: 1
간단하게 자 물 쇠 를 넣 으 면 문 제 를 해결 할 수 있다.

 @synchronized(self){
 if(handler == nil)
 {
  hander = [[Handler alloc] init];
 }
 }
return handler;
0x3 다 중 스 레 드 에서 변수 에 대한 액세스

if (self.xxx) {
 [self.dict setObject:@"ah" forKey:self.xxx];
}
여러분 은 이런 코드 를 처음 보 았 는데,정확 하 다 고 생각 하 시 겠 습 니까?키 를 설정 할 때 self.xxx 가 비 nil 로 판단 되 었 기 때문에 비 nil 이 아 닌 경우 에 만 후속 명령 을 수행 할 수 있 습 니 다.그러나 상기 코드 는 단일 라인 의 전제 하에 서 만 정확 하 다.
만약 에 우리 가 상기 코드 가 현재 실행 중인 스 레 드 를 Thread A 라 고 가정 하면 if (self.xxx) 의 문 구 를 실행 한 후에 CPU 는 실행 권 을 Thread B 로 전환 시 켰 고 이때 Thread B 에서 self.xxx = nil 을 호출 했다.부분 변 수 를 사용 하면 이 문 제 를 해결 할 수 있다.

__strong id val = self.xxx;
if (val) {
 [self.dict setObject:@"ah" forKey:val];
}
이렇게 하면 아무리 많은 스 레 드 가 self.xxx 를 수정 하려 고 시도 하 더 라 도 본질 적 인 val 은 기 존의 상 태 를 유지 하고 비 nil 의 판단 에 부합된다.
0x4 dispatch_그룹 붕괴
dispatch_group_enter 와 leave 는 일치 해 야 합 니 다.그렇지 않 으 면 crash 가 됩 니 다.다 중 자원 을 다운로드 할 때 다 중 스 레 드 를 사용 하여 다운로드 하고 모두 다운로드 한 후에 사용자 에 게 알려 야 합 니 다.다운로드 시작,dispatchgroup_enter,다운로드 완료 dispatchgroup_leave 。 매우 간단 한 절차 이지 만 코드 가 어느 정도 복잡 하거나 제3자 라 이브 러 리 를 사 용 했 을 때 문제 가 생 길 수 있 습 니 다.

dispatch_group_t serviceGroup = dispatch_group_create();
dispatch_group_notify(serviceGroup, dispatch_get_main_queue(), ^{
 NSLog(@"Finish downloading :%@", downloadUrls);
});
// t               
[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];
}];
다 중 스 레 드 를 사용 하여 동시 다운 로드 를 진행 합 니 다.모든 그림 을 다운로드 할 때 까지(실패 할 수 있 습 니 다)리 셋 을 진행 합 니 다.그 중에서 그림 다운 로드 는 SDWebImage 를 사용 합 니 다.충돌 하 는 장면 은 10 장의 그림 이 있 고 두 번 나 누 어 다운로드(A&B)합 니 다.그 중 B 조 안에 한 장의 그림 과 A 조 가 다운로드 한 그림 이 중복 되 었 다.A 조 가 대응 하 는 그룹 A,B 조 그룹 B 를 다운로드 한다 고 가정 합 니 다.
다음은 SDWebImage 원본 코드 를 캡 처 합 니 다.

dispatch_barrier_sync(self.barrierQueue, ^{
 SDWebImageDownloaderOperation *operation = self.URLOperations[url];
 if (!operation) {
  operation = createCallback();
  // ****    ****
  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];
   };
  };
 }
// ****    ****
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
}
SDWebImage 의 다운로드 기 는 URL 에 따라 NSOperation 맵 에 대응 하고 같은 URL 은 실행 되 지 않 은 NSOperation 에 매 핑 됩 니 다.A 조 그림 다운로드 가 완료 되면 같은 url 리 셋 은 Group A 가 아 닌 Group B 입 니 다.이때 그룹 B 의 계 수 는 1 이다.B 조 그림 을 모두 다운로드 한 후 종료 수 는 5+1 입 니 다.엔 터 의 횟수 는 5,leave 의 횟수 는 6 이기 때문에 붕 괴 됩 니 다!
0x5 마지막 소지 자 방출 후 붕괴
대상 A 는 관리자 가 보유 하고 있 으 며 A 에서 [Manager removeObjectA] 을 호출 합 니 다.A 대상자 의 retainCount -1,retainCount 가 0 일 때 대상자 A 는 이미 석방 되 기 시작 했다.removeObjectA 를 호출 한 뒤 곧바로 [self doSomething] 을 호출 하면 붕괴 된다.

- (void)finishEditing
{
 [Manager removeObject:self];
 [self doSomething];
}
이 경우 배열 이나 사전 에 대상 을 포함 하고 대상 의 마지막 소지 자 에서 발생 한다.대상 이 잘 처리 되 지 않 으 면 위의 붕괴 가 있 을 것 이다.또 하나의 경 우 는 배열 이나 사전 의 대상 이 풀 려 났 을 때 배열 을 옮 겨 다 니 거나 사전 의 값 을 가 져 오 면 무 너 지 는 것 이다.이런 상황 은 사람 을 매우 붕괴 시 킬 수 있다.왜냐하면 가끔 스 택 이 이 렇 기 때문이다.

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)
이런 스 택 이 생 길 수 있 는 장면 은:
Dictionary 를 풀 때,어떤 값(value)은 다른 코드 에 의 해 미리 풀 려 서 야 지침 이 되 었 습 니 다.이때 다시 풀 려 서 Crash 를 촉발 합 니 다.모든 Dictionary 가 풀 릴 때 모든 key/value 를 걸 수 있다 면,어떤 key/value 가 딱 맞 으 면 crash 가 발생 합 니 다.그러면 방금 걸 린 key/value 에 걸 립 니 다.
0x6 대상 의 방출 스 레 드 는 일 을 처리 하 는 스 레 드 와 일치 해 야 합 니 다.
대상 A 는 메 인 스 레 드 에서 Notification 사건 을 감청 합 니 다.만약 이 대상 이 다른 스 레 드 에 의 해 방출 된다 면.이때 대상 A 가 notification 과 관련 된 작업 을 수행 하고 있 으 면 대상 관련 자원 에 접근 하면 야 지침 이 되 고 crash 가 발생 합 니 다.
0x7 performSelector:withObject:afterDelay:
이 방법 을 사용 합 니 다.주 스 레 드 가 아니라면 현재 스 레 드 의 ruuloop 이 존재 하 는 지 확인 해 야 합 니 다.performSelectorxxx_after Delay 는 runlopp 에 의존 해 야 실행 할 수 있 습 니 다.또한 performSelector:withObject:afterDelay:cancelPreviousPerformRequestsWithTarget 을 조합 할 때 조심해 야 한다.
  • after Delay 는 receiver 의 인용 수 를 증가 시 키 고 cancel 은
  • 감소 에 대응 합 니 다.
  • receiver 의 인용 계수 가 1(delay 만 남 았 을 때 cancel 을 호출 한 후 receiver 를 즉시 소각 하고 나중에 receiver 를 호출 하 는 방법 은 crash
  • 입 니 다.
    
    __weak typeof(self) weakSelf = self;
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
    if (!weakSelf)
    {
    //NSLog(@"self   ");
     return;
    }
    [self doOther];
    총결산
    이상 은 이 글 의 전체 내용 입 니 다.본 논문 의 내용 이 여러분 의 학습 이나 업무 에 어느 정도 참고 학습 가치 가 있 기 를 바 랍 니 다.궁금 한 점 이 있 으 시 면 댓 글 을 남 겨 주 셔 서 저희 에 대한 지지 에 감 사 드 립 니 다.

    좋은 웹페이지 즐겨찾기