头像 黑化肥挥发发灰会花飞

3DTouch 之与 MLeaksFinder 反应

2018-05-09
iOS

leaks

在 iOS11.3 下,检测到了 3DTouch Pop 时的内存泄漏。

MLeaksFinder

MLeaksFinder 是腾讯系团队出的一款针对 iOS 平台的自动化内存泄漏检测工具,介绍及使用方式等详见链接。

背景

偶然发现在之前的项目中 MLeaksFinder 有报内存泄漏,而且只有在 3DTouch Pop 才会发生,很纳闷。刚适配 3DTouch 的时候,好像也没有发现这种情况,一度怀疑是新页面上后加的逻辑所出现的问题,找来找去无果,遂新建了工程,里面只有 3DTouch 的逻辑,看看会不会还有泄漏。
结果是 在 iOS9.3 下无内存泄漏, iOS10.3.1 和 iOS 11.3 下均有泄漏。(我只下了这几个版本,其他的未测) iPhone-simulator-os

UIKit Peek and Pop

3DTouch 是苹果在 iOS9 时加入的一项划时代的功能,具体就不表了,今天说的是 3DTouch 在 UIKit 内的 Peek 和 Pop 功能,介绍及使用等详见 开发者文档

我们这边集成主要就是 注册继承及实现 UIViewControllerPreviewingDelegate

show-me-code
  • 先检查可用后注册 3DTouch
    if ([self.traitCollection respondsToSelector:@selector(forceTouchCapability)] &&
        (self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable)) {
        [self registerForPreviewingWithDelegate:self sourceView:self.tableView];
    }
  • 然后实现相关代理方法
//peek 预览
- (nullable UIViewController *)previewingContext:(id <UIViewControllerPreviewing>)previewingContext viewControllerForLocation:(CGPoint)location {
    if ([self.presentedViewController isKindOfClass:[BViewController class]]) {
        return nil;
    }
    else {
        UITableView *tableView = (UITableView *)previewingContext.sourceView;
        NSIndexPath *indexPath = [tableView indexPathForRowAtPoint:location];
        UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
        if (!cell) {
            return nil;
        }
        //设置不被虚化的范围
        previewingContext.sourceRect = cell.frame;
        BViewController *previewingVC = [[BViewController alloc] init];
        previewingVC.parameter = [NSString stringWithFormat:@"BVC--->%ld",indexPath.row];
        //可调整预览视图的大小
        previewingVC.preferredContentSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height/(indexPath.row%2 + 1));
        return previewingVC;
    }
}
//Pop 进入
- (void)previewingContext:(id <UIViewControllerPreviewing>)previewingContext commitViewController:(nonnull UIViewController *)viewControllerToCommit {
    [self showViewController:viewControllerToCommit sender:self];
}
  • 若想上滑预览图出现选项,则在 Pop 的页面实现 previewActionItems 方法,例:
- (NSArray<id<UIPreviewActionItem>> *)previewActionItems {
    UIPreviewAction *action1 = [UIPreviewAction actionWithTitle:@"Selected" style:UIPreviewActionStyleSelected handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
        NSLog(@"点击了Selected");
    }];
    
    UIPreviewAction *action2 = [UIPreviewAction actionWithTitle:@"Destructive" style:UIPreviewActionStyleDestructive handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
        NSLog(@"点击了Destructive");
    }];
    
    UIPreviewAction *group1 = [UIPreviewAction actionWithTitle:@"选项一" style:UIPreviewActionStyleDefault handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
        NSLog(@"点击了选项一");
    }];
    UIPreviewAction *group2 = [UIPreviewAction actionWithTitle:@"选项二" style:UIPreviewActionStyleDefault handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) {
        NSLog(@"点击了选项二");
    }];
    UIPreviewActionGroup *group = [UIPreviewActionGroup actionGroupWithTitle:@"更多选项" style:UIPreviewActionStyleDefault actions:@[group1,group2]];
    
    return @[action1,action2,group];
}

问题

上面的写法看起来好像都没有什么问题,而且我也是下载了 开发者指南和样例代码 上的 swift 工程作为参考,但是不论是我自己写的 demo 还是下载的样例工程,在加入了 MLeaksFinder 后均能发现在 iOS10.3.1 和 iOS 11.3 下有泄漏,和开头一样。

分析

作为分析,我在 Pop 的页面 BViewController 里打印了其各个生命周期函数和内存地址

在重按 Pop 后,控制台输出如下: clonsole-log

初步分析下 Memory Leak 的输出其实应该是前5行走完周期后,最后可能走的 dealloc 函数,前5行是 Peek 预览时走的,后面是除了异常是 Pop 跳转后输出的。

因而猜想是不是 Pop 又引用了 Peek 的 BViewController ,同样的内存地址,因而是 Pop 持有了 Peek 的对象,Peek 要释放时,引用计数器不为空,走不了 dealloc ,然后 MLeaksFinder 就报异常了。

也就是说,在 Pop 的代理函数里,这么写可能有问题。同样样例代码里也是这么写的

[self showViewController:viewControllerToCommit sender:self];
//样例代码
show(viewControllerToCommit, sender: self)

然后也试了用 push 或 present 的方式跳转过去,依然有问题

[self.navigationController pushViewController:viewControllerToCommit animated:YES];
//或
[self presentViewController:viewControllerToCommit animated:YES completion:nil];

解决

既然 Pop 时还持有 Peek 的类,不让它持有 Peek 的 ViewController 不就完了。在 Pop 函数里创建一个新的 BViewController ,和 Peek 里的 BViewController 内存地址不一样,最后像这样:

BViewController *pushVC = [[BViewController alloc] init];
pushVC.parameter = [(BViewController *)viewControllerToCommit parameter];
[self.navigationController pushViewController:pushVC animated:YES];

no-leaks

可以看到第一次的 viewDidLoaddealloc 这一系列生命周期函数都是内存地址为 0x7fd6e19883f0BViewController Peek 产生的,后面的就是内存地址为 0x7fd6df503b30 的 ViewController Pop 产生的,这样就没有内存泄漏警告了。

最后

最后这个 demo 和官方样例代码在这里:https://github.com/1ilI/3DTouch


引申阅读

欢迎评论