美文网首页iOS基本功
iOS内存管理:析构检测原理

iOS内存管理:析构检测原理

作者: YYYYYY25 | 来源:发表于2018-11-03 15:37 被阅读30次
    一、前言

    在iOS日常开发中,很多不经意的小错误都会导致内存泄漏,从而影响项目的稳定性。
    除了最常见的循环引用外,还有下面几种常见错误会造成内存泄漏。
    1 使用c语言方法,需要手动释放
    2 对文件进行操作,要手动关闭
    3 调用block时,给block传指nil
    4 数组中插入了nil元素

    二、检测

    日常项目中常用的内存泄漏检测:
    1 静态检测,使用Xcode,Product->Analyze静态分析
    2 动态检测,使用Intruments->Leaks动态分析
    3 析构检测,通过Runtime(AOP)监听系统dealloc方法
    当然,还有像MLeaksFinder这类封装好的内存泄漏检测工具,是基于析构检测的一款非常好用且全面的工具。

    本文介绍在视图控制器Push/Pop操作中如何监听dealloc方法达到内存泄漏检测的目的,也是MLeaksFinder实现的基础。

    三、原理

    我们都知道在控制器正常的生命周期中会在viewDidDisappear:之后执行dealloc方法,从而进行内存的回收和释放。但是如果控制器中存在循环引用之类的问题,会导致控制器的dealloc方法无法执行,造成内存泄漏。
    析构检测的原理就是监听控制器的生命周期,在viewDidDisappear:时手动模拟dealloc方法,延迟检测控制器有没有被回收。

    结构图
    四、关键代码

    使用Runtime Method-Swizzling避免对项目代码的侵入:

    + (void)swizzleSelector:(SEL)orignSelector with:(SEL)swizzleSelector {
        Class class = [self class];
    
        Method orignalMethod = class_getInstanceMethod(class, orignSelector);
        Method swizzleMethod = class_getInstanceMethod(class, swizzleSelector);
    
        BOOL didAddMethod = class_addMethod(class, orignSelector, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
        if (didAddMethod) {
            class_replaceMethod(class, swizzleSelector, method_getImplementation(orignalMethod), method_getTypeEncoding(orignalMethod));
        }else {
            method_exchangeImplementations(orignalMethod, swizzleMethod);
        }
    }
    

    下面是模拟的dealloc方法,通过一个弱引用持有self,然后进行一个系统延迟执行的操作,在这个延迟的过程中,如果没有发生内存泄漏,系统应该会执行本身的dealloc方法将self回收,此后的打印应该为nil,如果控制器发生内存泄漏,导致dealloc方法没有执行,此时就会打印出相应发生内存泄漏的控制器。
    在MLeaksFinder中,延迟后执行的是断言操作。

    - (void)willDealloc {
        __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;
            NSLog(@"leak : %@", NSStringFromClass([strongSelf class]));
        });
    }
    
    五、监控生命周期

    UIViewController的分类中:

    const void *const kHasBeenPoppedKey = &kHasBeenPoppedKey;
    
    @implementation UIViewController (Leaks)
    
    +(void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [self swizzleSelector:@selector(viewWillAppear:) with:@selector(yy_viewWillAppear:)];
            [self swizzleSelector:@selector(viewDidDisappear:) with:@selector(yy_viewDidDisappear:)];
        });
    }
    
    // 标记入栈
    - (void)yy_viewWillAppear:(BOOL)animated {
        [self yy_viewWillAppear:animated];
        objc_setAssociatedObject(self, kHasBeenPoppedKey, @(NO), OBJC_ASSOCIATION_RETAIN);
    }
    
    // 查看标记情况,模拟dealloc方法
    - (void)yy_viewDidDisappear:(BOOL)animated {
        [self yy_viewDidDisappear:animated];
        if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {
            [self willDealloc];
        }
    }
    
    @end
    

    UINavigationController的分类中:

    @implementation UINavigationController (Leaks)
    +(void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [self swizzleSelector:@selector(popViewControllerAnimated:) with:@selector(yy_popViewControllerAnimated:)];
        });
    }
    
    // 标记出栈
    - (UIViewController *)yy_popViewControllerAnimated:(BOOL)animated {
        UIViewController *targetViewController = [self yy_popViewControllerAnimated:animated];
        extern const void *const kHasBeenPoppedKey;
        objc_setAssociatedObject(targetViewController, kHasBeenPoppedKey, @(YES), OBJC_ASSOCIATION_RETAIN);
        return targetViewController;
    }
    @end
    

    至此,一个简单的dealloc检测就基本实现了,你可以在控制器中模拟一个循环引用测试内存泄漏。

    六、测试

    我们都知道在使用NSTimer时,因为self.timer被控制器强引用,与此同时self.timer又持有self。这就是一个简单的循环引用。导致控制器退出时系统并不会调用dealloc方法释放self.timer。(解决方法这里不展开,有很多处理方式,比如:proxy)

    // TargetViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(runloop) userInfo:nil repeats:YES];
    }
    - (void)runloop{
        NSLog(@"run");
    }
    

    最终,我们在退出TargetViewController时,会打印:

    2018-11-03 15:32:47.164322+0800 HookDealloc[66621:3093836] leak : TargetViewController
    

    我们就可以去相应的控制器去排查问题了,假如实际开发中,控制器的逻辑比较复杂,这时就可以配合静态分析或者动态分析一起排查。

    DEMO

    相关文章

      网友评论

        本文标题:iOS内存管理:析构检测原理

        本文链接:https://www.haomeiwen.com/subject/rlykxqtx.html