IOS 메모리 누출 검사 방법 및 MLeakFinder 다시 쓰기

iOS 개발에 있어 메모리 유출 문제는 이미 흔히 말하는 화제이다.일상적인 면접에서 이런 문제들이 자주 언급된다.우리는 일상적인 개발 과정에서 메모리 유출 검사를 하는데 일반적으로 인스트루먼트 도구의 Leaks/Allocation을 사용하여 검사를 한다. 네트워크에도 비교적 효율적이고 사용하기 좋은 메모리 유출 검사 도구, MLeakFinder가 있다.

MLeakFinder - 원리


우선 UIVIew Controller를 보면 UIVIew Controller가 팝이나 디스미스에 걸렸을 때 이 VC는 이 VC에 포함된View나 하위 View가 빨리 방출됩니다.그래서 우리는 UIVIew Controller가 팝이나 디스미스에 의해 잠시 후에 이 VC에서의view,subView 등이 아직 존재하는지 확인해야 한다.
UIVIEW Controller + Memory Leak.h의load방법에서 볼 수 있듯이 아침+load방법에서runtime를 통해viewWill Appear,viewDid Appear,dismissViewController Animated:completion: 이 세 가지 방법을 교환했다.

1, 먼저 viewWill Appear 보기


- (void)swizzled_viewWillAppear:(BOOL)animated {
    [self swizzled_viewWillAppear:animated];
    objc_setAssociatedObject(self, kHasBeenPoppedKey, @(NO), OBJC_ASSOCIATION_RETAIN);
}
VC가 들어올 때 연관된 객체를 추가하고 NO로 표시합니다.

2, viewDid Appear 보기


- (void)swizzled_viewDidDisappear:(BOOL)animated {
    [self swizzled_viewDidDisappear:animated];
    if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {
        [self willDealloc];    
    }
}
코드를 통해 알 수 있듯이 현재 관련 대상의 태그를 가져와서 YES로 표시할 때 willDealloc를 호출합니다.

3, 우리는 언제 YES로 표시될지 봅시다.


UINavigationController + MemoryLeak.h의 popViewController Animated: 방법에서 볼 수 있습니다.

- (UIViewController *)swizzled_popViewControllerAnimated:(BOOL)animated {
    UIViewController *poppedViewController = [self swizzled_popViewControllerAnimated:animated];
    if (!poppedViewController) {
        return nil;
    }    
// Detail VC in UISplitViewController is not dealloced until another detail VC is shown
    if (self.splitViewController &&
        self.splitViewController.viewControllers.firstObject == self &&
        self.splitViewController == poppedViewController.splitViewController) {
        objc_setAssociatedObject(self, kPoppedDetailVCKey, poppedViewController, OBJC_ASSOCIATION_RETAIN)
        return poppedViewController;    }    // VC is not dealloced until disappear when popped using a left-edge swipe gesture
    extern const void *const kHasBeenPoppedKey;
    objc_setAssociatedObject(poppedViewController, kHasBeenPoppedKey, @(YES), OBJC_ASSOCIATION_RETAIN);
    return poppedViewController;
}
VC가 팝이나 왼쪽으로 미끄러져 돌아올 때 보기를 없애면 YES로 표시된다는 것을 알 수 있다.

4, 우리는 윌딜록을 중점적으로 본다


- (BOOL)willDealloc {
   // 
    NSString *className = NSStringFromClass([self class]);
    if ([[NSObject classNamesWhitelist] containsObject:className])
        return NO;

   // 
    NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey)
    if ([senderPtr isEqualToNumber:@((uintptr_t)self)])
        return NO;

   // 
    __weak id weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong id strongSelf = weakSelf;
        [strongSelf assertNotDealloc];
    });
    return YES;
}
1. 첫 번째 단계: 현재class가 화이트리스트에 있는지 아닌지를 먼저 판단할 수 있습니다. 그렇다면 리턴 NO, 즉 메모리 유출이 아닙니다.
동시에 우리는 화이트 리스트를 구축하는 원본 코드를 살펴본다. 하나의 예를 사용하여 실현된 것은 하나뿐이고 개인적인 방법이다.

+ (NSMutableSet *)classNamesWhitelist {
    static NSMutableSet *whitelist = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        whitelist = [NSMutableSet setWithObjects:
                     @"UIFieldEditor", // UIAlertControllerTextField
                     @"UINavigationBar",
                     @"_UIAlertControllerActionView",
                     @"_UIVisualEffectBackdropView",
                     nil];
        
        // System's bug since iOS 10 and not fixed yet up to this ci.
        NSString *systemVersion = [UIDevice currentDevice].systemVersion;
        if ([systemVersion compare:@"10.0" options:NSNumericSearch] != NSOrderedAscending) {
            [whitelist addObject:@"UISwitch"];
        }
    });
    return whitelist;
}
또한 사용자 정의 화이트 리스트 추가 지원

+ (void)addClassNamesToWhitelist:(NSArray *)classNames {
    [[self classNamesWhitelist] addObjectsFromArray:classNames];
}
2. 두 번째 단계: 이 대상이 지난번에 action을 보낸 대상인지 판단하고, 그렇다면 메모리 검사를 하지 않는다

 // 
   
 NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey)  
  
if ([senderPtr isEqualToNumber:@((uintptr_t)self)])
        
return NO;
3, 3단계: 약한 바늘이 self를 가리키고 2s가 지연된 다음에 이 약한 바늘을 통해 - assert Not Dealloc를 호출합니다. 만약에 방출되면nil에게 메시지를 보내서 바로 되돌아옵니다. - assert Not Dealloc 방법을 터치하지 않고 방출되었다고 생각합니다.만약 그것이 방출되지 않았다면, - assert Not Dealloc이 호출될 것이다

    __weak id weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong id strongSelf = weakSelf;
        [strongSelf assertNotDealloc];
    });

5, 이제 우리는 돌아가자: 2의 코드 [self will Dealloc]


원본 좀 볼게요.

- (BOOL)willDealloc {
    // 
    if (![super willDealloc]) {
        return NO;
    }
    // 
    [self willReleaseChildren:self.childViewControllers];
    [self willReleaseChild:self.presentedViewController];
    if (self.isViewLoaded) {
        [self willReleaseChild:self.view];
    }
    return YES;
}
1, 첫 번째 단계: 상위 클래스의 willDealloc, 즉 위 디렉터리 4를 슈퍼를 통해 호출합니다.
2, 2단계: willReleaseChildren, willReleaseChildren을 호출하여 이 대상의 하위 대상을 두루 훑어보고 방출 여부를 봅니다

- (void)willReleaseChild:(id)child {
    if (!child) {
        return;
    }
    
    [self willReleaseChildren:@[ child ]];
}


- (void)willReleaseChildren:(NSArray *)children {
    NSArray *viewStack = [self viewStack];
    NSSet *parentPtrs = [self parentPtrs];
    for (id child in children) {
        NSString *className = NSStringFromClass([child class]);
        [child setViewStack:[viewStack arrayByAddingObject:className]];
        [child setParentPtrs:[parentPtrs setByAddingObject:@((uintptr_t)child)]];
        [child willDealloc];
    }
}
코드를 통해 알 수 있듯이 willReleaseChildren을 호출하는 방법을 통해 현재 대상viewStack,parentPtrs를 가져오고,children을 두루 돌아다니며 하위 대상마다viewStack,parentPtrs를 설정하고willDealloc를 호출합니다.
viewStask, parentPtrs의 구현을 원본 코드로 보십시오.

- (NSArray *)viewStack {
    NSArray *viewStack = objc_getAssociatedObject(self, kViewStackKey);
    if (viewStack) {
        return viewStack;
    }
    
    NSString *className = NSStringFromClass([self class]);
    return @[ className ];
}


- (void)setViewStack:(NSArray *)viewStack {
    objc_setAssociatedObject(self, kViewStackKey, viewStack, OBJC_ASSOCIATION_RETAIN);
}


- (NSSet *)parentPtrs {
    NSSet *parentPtrs = objc_getAssociatedObject(self, kParentPtrsKey);
    if (!parentPtrs) {
        parentPtrs = [[NSSet alloc] initWithObjects:@((uintptr_t)self), nil];
    }
    return parentPtrs;
}


- (void)setParentPtrs:(NSSet *)parentPtrs {
    objc_setAssociatedObject(self, kParentPtrsKey, parentPtrs, OBJC_ASSOCIATION_RETAIN);
}
viewStack은 배열, parentPtrs는 집합 형식을 사용합니다.모두 실행할 때 연관된 객체로 속성을 추가합니다.
parentPtrs는 -assertNotDealloc에서 현재 대상이 부모 노드 집합과 교차하는지 판단합니다.다음은 자세히 살펴보겠습니다. - assert Not Dealloc 방법.

- (void)assertNotDealloc {    // 
    if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
        return;
    }    // 
    [MLeakedObjectProxy addLeakedObject:self];
    
    NSString *className = NSStringFromClass([self class]);
    NSLog(@"Possibly Memory Leak.
In case that %@ should not be dealloced, override -willDealloc in %@ by returning NO.
View-ViewController stack: %@", className, className, [self viewStack]); }
1, 첫 번째 단계에서 우리는 parentPtrs를 통해 교차 여부를 판단한다
소스 번호:

+ (BOOL)isAnyObjectLeakedAtPtrs:(NSSet *)ptrs {
    NSAssert([NSThread isMainThread], @"Must be in main thread.");
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        leakedObjectPtrs = [[NSMutableSet alloc] init];
    });
    if (!ptrs.count) {
        return NO
    }
    if ([leakedObjectPtrs intersectsSet:ptrs]) {
        return YES;
    } else {
        return NO;
    }}
예를 들어 대상을 만들고 집합의 형식을 통해 교집합이 있는지 판단하는 것을 볼 수 있습니다. 그렇다면return입니다.그렇지 않으면 두 번째 단계로 들어갑니다.
2, 2단계: addLeakedObject

+ (void)addLeakedObject:(id)object {
    NSAssert([NSThread isMainThread], @"Must be in main thread.");
    MLeakedObjectProxy *proxy = [[MLeakedObjectProxy alloc] init];
    proxy.object = object;
    proxy.objectPtr = @((uintptr_t)object);
    proxy.viewStack = [object viewStack];
    static const void *const kLeakedObjectProxyKey = &kLeakedObjectProxyKey;
    objc_setAssociatedObject(object, kLeakedObjectProxyKey, proxy, OBJC_ASSOCIATION_RETAIN);
    [leakedObjectPtrs addObject:proxy.objectPtr];
#if _INTERNAL_MLF_RC_ENABLED    [MLeaksMessenger alertWithTitle:@"Memory Leak"                            message:[NSString stringWithFormat:@"%@", proxy.viewStack]
                           delegate:proxy
              additionalButtonTitle:@"Retain Cycle"];
#else
    [MLeaksMessenger alertWithTitle:@"Memory Leak"
                            message:[NSString stringWithFormat:@"%@", proxy.viewStack]];#endif
}
첫 번째 단계: MLeakedObjectProxy 대상을 구성하여 전송된 누설 대상인object에 프록시인 프록시를 연결합니다
2단계:objc_를 통해setAssociatedObject(object,kLeakedObjectProxyKey,proxy,OBJC_ASSOCIATION_RETAIN) 방법,object는 proxy를 강하게 보유하고,proxy는 object를 보유하면,object가 방출하면proxy도 방출한다
3단계: proxy 저장.objectPtr (실제 개체 주소) 집합leakedObjectPtrs 안쪽으로
4단계: 탄창 AlertView약_INTERNAL_MLF_RC_ENABLED ==1, 탄창은 순환 참조를 감지하는 옵션을 추가합니다.만약_INTERNAL_MLF_RC_ENABLED = = 0, 스택 정보만 표시합니다.
LeakedObjectProxy 클래스의 경우 메모리 유출이 검출되어 발생한 것으로 유출 대상의 속성으로 존재하며, 유출된 대상이 방출되면 MLeakedObjectProxy도 방출되고 - dealloc 함수를 호출합니다
집합leakedObjectPtrs에서 이 대상의 주소를 제거하고 다시 창을 쳐서 이 대상이 방출되었음을 알립니다

6. 자신도 이 프레임워크를 다시 쓰려고 시도하고 있습니다. 여러분의 대화를 환영합니다.


이상은 IOS 메모리 유출 검사 방법 및 MLeakFinder의 상세한 내용을 다시 쓰는 것입니다. 더 많은 IOS 메모리 유출에 관한 자료는 저희 기타 관련 글을 주목해 주십시오!

좋은 웹페이지 즐겨찾기