理解iOS中的仪器内存分配日志

9
我已经开发了一个几乎完成的iOS应用程序,但是最近我遇到了由于“内存压力”而崩溃的问题。所以我开始在Instruments中分析内存分配情况,确实,这个应用程序使用了相当多的内存,并且在使用过程中似乎只会增加。
然而,作为相对新手的Instruments内存分配者,我无法准确地解释52%的分配是在哪里进行的,如下面屏幕截图所示:
显然,这与核心动画有关,但是具体是什么我很难确定,因此我认为那里一定有聪明人可能知道答案。
导航:
我的应用程序在视图控制器之间移动时使用自定义转场,其中涉及大量动画。以下是一个示例:
@interface AreaToKeyFiguresSegue : UIStoryboardSegue

@end

...

@implementation AreaToKeyFiguresSegue

- (void)perform
{
    [self sourceControllerOut];
}

- (void)sourceControllerOut
{
    AreaChooserViewController *sourceViewController = (AreaChooserViewController *) [self sourceViewController];
    KeyFigureViewController *destinationController = (KeyFigureViewController *) [self destinationViewController];

    double ratio = 22.0/sourceViewController.titleLabel.font.pointSize;

    sourceViewController.titleLabel.adjustsFontSizeToFitWidth = YES;

    [UIView animateWithDuration:TRANSITION_DURATION delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{

        // Animate areaChooser
        sourceViewController.areaChooserScrollView.alpha = 0;
        sourceViewController.areaScrollViewVerticalSpaceConstraint.constant = -300;

        sourceViewController.backButtonVerticalConstraint.constant = 20;
        sourceViewController.backButton.transform = CGAffineTransformScale(sourceViewController.backButton.transform, ratio, ratio);
        sourceViewController.backButton.titleLabel.textColor = [UIColor redKombitColor];

        sourceViewController.backArrowPlaceholderVerticalConstraint.constant = 14;
        sourceViewController.backArrowPlaceholder.alpha = 1;

        sourceViewController.areaLabelVerticalConstraint.constant = 50;
        sourceViewController.areaLabel.alpha = 1;

        [sourceViewController.view layoutIfNeeded];

    } completion:^(BOOL finished) {
        [destinationController view]; // Make sure destionation view is initialized before animating it
        [sourceViewController.navigationController pushViewController:destinationController animated:NO]; // Push new viewController without animating it

        [self destinationControllerIn]; // Now animate destination controller
    }];
}

- (void)destinationControllerIn
{
    AreaChooserViewController *sourceViewController = (AreaChooserViewController *) [self sourceViewController];
    KeyFigureViewController *destinationController = (KeyFigureViewController *) [self destinationViewController];

    destinationController.keyFigureTableViewVerticalConstraint.constant = 600;
    destinationController.keyFigureTableView.alpha = 0.0;
    destinationController.allFavoritesSegmentedControl.alpha = 0.0;
    [destinationController.view layoutIfNeeded];
    [sourceViewController.segueProgress setHidden:YES];
} 

@end

每当一个视图控制器需要被弹出时,我只需执行相反的操作:
- (IBAction)goBack:(id)sender
{
    [UIView animateWithDuration:TRANSITION_DURATION delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{

        [self.keyFigureTableView setAlpha:0];
        self.keyFigureTableViewVerticalConstraint.constant = 700;
        [self.allFavoritesSegmentedControl setAlpha:0];
        [self.view layoutIfNeeded];
    } completion:^(BOOL finished) {
        [self.navigationController popViewControllerAnimated:NO]; // Pop viewController without animating it
    }];
}

编辑:

大部分内存分配发生在推送视图控制器时,即使它之前已经显示过。也就是说,从

A -> B -> C

B <- C

B -> C

其中“->” = 推送,“<-” = 弹出,每个“->”都会分配更多的内存,“<-”不会释放任何内存。

更多细节

根据Instruments的结果,我没有僵尸对象和内存泄漏,并且静态分析也没有发现问题。我的应用程序只是不断地分配内存,直到最终崩溃。

我大约有70%的内存分配发生在以下调用堆栈中,与我的代码无关(倒置的调用树):

enter image description here


我从中得到的唯一信息是一堆十六进制地址,而调用者总是“vm_allocate”。 - Zappel
扩展细节基本上提供了与上面调用树相同的概述。如果我注释掉layoutIfNeeded,基本上就没有动画了。 - Zappel
那你的布局代码里面都做了些什么呢?可以分享一下代码片段吗? - yurish
在哪个布局代码中?我的segue吗? - Zappel
哦,抱歉。不,我没有任何drawRect。我只是对所有东西使用自动布局。 - Zappel
显示剩余9条评论
4个回答

6

以下是我调试的步骤:

  1. 在Instruments中,使用Allocations工具并打开“记录引用计数”

enter image description here

  1. 运行你的应用程序到“稳态”,包括执行你认为可能存在泄漏的操作几次。

  2. 在Instruments中,使用in/out标记设置你的基线内存水平。

  3. 执行你认为可能存在泄漏的操作几次。(比如7次)

  4. 在Instruments中,切换到显示所有分配内容的视图,并查找已分配但未释放相同数量次数的对象(再次,也许是7次)。首先尝试查找与你的程序特定相关的对象…因此,优先考虑使用MyNetworkOperation实例而不是像NSData这样的通用foundation类。 click arrow next to class you are interested in click arrow next to an allocated object

  5. 选择一个没有被释放的对象,并查看它的分配历史记录。你将能够看到与该对象相关的每个alloc/retain/release/autorelease的调用堆栈。也许其中一个调用看起来对你来说很可疑。 history of retain/release for selected object

我想这些步骤更适用于非ARC环境。在ARC下,你可能正在寻找一个保留周期。

一般来说,您可以通过确保您的强引用只沿着一个方向进行来避免retain cycles...例如,一个视图具有其子视图的强引用,并且每个子视图必须始终使用弱引用来引用任何父视图。或者你的视图控制器对应一个强引用,你的视图必须只有一个弱引用来引用它的视图控制器。另一种说法是在每个关系中决定哪个对象“拥有”另一个对象。


2
Instruments指出你的UIViews存在泄漏(或者准确地说是没有被释放)。每次推送都会创建一个新的destinationController,而destinationController的视图将由CALayer支持,而CALayer会消耗内存。为了证明这一点,你可以为destinationController实现dealloc,并在那里设置断点以查看是否已调用。如果已调用dealloc,则可以对其他对象执行此操作以找出哪个对象存在问题(例如destinationController.view)。为什么会出现泄漏呢?1.可能存在捕获destinationController的保留循环。这很难调试,你可能需要检查与代码相关的所有内容。2.每个destinationController可能被某些长寿对象保留。(未失效的重复NSTimer,Singleton,RootViewController,CADisplayLink等)3.假缓存。你缓存了一些东西并尝试重用它们。然而,缓存逻辑存在错误,这些对象永远不会被重用,并且新对象不断插入其中。

我已经在所有视图控制器和自定义segue的dealloc方法中实现了日志语句。Segue在推送后立即被释放,但是视图控制器却没有。如果我按照A->B->C->B->A的顺序(当然,返回时我会弹出),视图控制器B只有在等待很长时间后才被释放。C从未被释放。 - Zappel
这种内存泄漏是最难调试的。我可能会采取的解决方案是注释掉绝对安全的代码/文件,直到找到导致泄漏的那一行。有时,即使是iOS也可能存在导致视图控制器永远不释放的错误。 - Jay Zhao

0

你的动画大部分包含改变透明度或颜色。为了减少自定义segue中的动画代码,我建议将目标视图控制器的动画代码(即destinationControllerIn方法)移动到目标视图控制器的viewDidLoad:中。


-2

首先检查一下(scheme诊断)是否已经开启了僵尸进程。僵尸进程意味着没有任何东西被删除。考虑到您的内存图表从未下降,这应该是我首先要检查的。


只有在向已删除的对象发送消息时才会出现这些错误。问题不在于删除,而在于没有删除任何东西,导致内存不断增长。僵尸对象通过不丢弃任何内容并将对象转换为特殊的NSZombie类来工作。 - ahwulf
是的,但除非我发送了一个 Obj-C 消息,否则我永远不会意识到这些僵尸对象,而显然我没有这样做。 - Zappel
3
因为这个词的意思不正确,所以被踩了。"Zombie"指的是已经被释放过度的东西。给一个"Zombie"发送消息会导致访问错误。你提到的是内存泄漏,即某些东西从未完全释放,导致内存压力。 - Patrick Goley
在使用内存工具进行测试时,开启Zombies是我已经回答过许多次的问题。它很容易被意外打开并且会导致内存使用量出现相同的模式。无论你是否给它发送消息,它仍然是一个Zombie。在这种情况下,它不是问题所在,但仍值得检查。 - ahwulf
开启僵尸对象确实会导致看起来像是内存泄漏的情况。(因为僵尸对象永远不会被释放。)然而,当寻找内存泄漏时,我不会首先考虑这个问题。 - nielsbot
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接