Xcode - iOS 内存泄漏让我发疯,我认为它是 NSNotificationCenter,但希望新的眼睛能看到我看不到的东西
这里的代码是从 RootViewController 启动的模态视图,用于显示视频,并在电影下方显示缩略图幻灯片,然后将定时指令绑定到电影。
这一切都有效,但是有一个内存泄漏/缺乏释放,我只是看不到它,花了三天时间试图修复它,是时候寻求帮助了......
如果我通过以下方式禁用 NSNotificationCenter注释掉(在 .m 中突出显示)我没有任何关于内存的问题并保留定时文本。但我也没有任何缩略图。我尝试在很多地方插入 [[NSNotificationCenter alloc] removeObserver:self];
,看看这是否能为我摆脱它。但可惜,并没有什么卵用。
我也尝试过发布“backgroundTimer”,但当我尝试编译和运行时,它并没有给人留下太深刻的印象。
本质上,我第一次加载模态视图时,没有任何问题,一切看起来都很棒 - 但是,如果我用 -(IBAction)close:(id)sender;
关闭它,看起来有些东西没有释放,因为下次我启动同一页面时,内存使用量增加了大约 30%(大约是缩略图生成所使用的量),并且每次重新启动模态视图时都会增加大约相同的量。
请记住,我是这方面的新手,对于那些了解情况的人来说,这个错误可能是一个非常愚蠢的错误。但为了完成这个项目,我很乐意接受你对我的任何辱骂。
代码如下:
.h
#import <UIKit/UIKit.h>
#import <MediaPlayer/MPMoviePlayerController.h>
#import "ImageViewWithTime.h"
#import "CommentView.h"
@interface SirloinVideoViewController_iPad : UIViewController {
UIView *landscapeView;
UIView *viewForMovie;
MPMoviePlayerController *player;
UILabel *onScreenDisplayLabel;
UIScrollView *myScrollView;
NSMutableArray *keyframeTimes;
NSArray *shoutOutTexts;
NSArray *shoutOutTimes;
NSTimer *backgroundTimer;
UIView *instructions;
}
-(IBAction)close:(id)sender;
-(IBAction)textInstructions:(id)sender;
@property (nonatomic, retain) IBOutlet UIView *instructions;
@property (nonatomic, retain) NSTimer *theTimer;
@property (nonatomic, retain) NSTimer *backgroundTimer;
@property (nonatomic, retain) IBOutlet UIView *viewForMovie;
@property (nonatomic, retain) MPMoviePlayerController *player;
@property (nonatomic, retain) IBOutlet UILabel *onScreenDisplayLabel;
@property (nonatomic, retain) IBOutlet UIScrollView *myScrollView;
@property (nonatomic, retain) NSMutableArray *keyframeTimes;
-(NSURL *)movieURL;
- (void) playerThumbnailImageRequestDidFinish:(NSNotification*)notification;
- (ImageViewWithTime *)makeThumbnailImageViewFromImage:(UIImage *)image andTimeCode:(NSNumber *)timecode;
- (void)handleTapFrom:(UITapGestureRecognizer *)recognizer;
@end
.m
#import "SirloinVideoViewController_iPad.h"
#import "SirloinTextViewController.h"
@implementation SirloinVideoViewController_iPad
@synthesize theTimer, backgroundTimer, viewForMovie, player,
onScreenDisplayLabel, myScrollView, keyframeTimes, instructions;
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
}
return self;
[nibNameOrNil release];
[nibBundleOrNil release];
}
- (IBAction)close:(id)sender{
[self.parentViewController dismissModalViewControllerAnimated:YES];
[player stop];
[player release];
[theTimer invalidate];
[theTimer release];
[backgroundTimer invalidate];
[SirloinVideoViewController_iPad release];
}
—
-(IBAction)textInstructions:(id)sender {
SirloinTextViewController *vController = [[SirloinTextViewController alloc] initWithNibName:nil bundle:nil];
[self presentModalViewController:vController animated:YES];
[vController release];
}
- (void)viewDidLoad {
[super viewDidLoad];
keyframeTimes = [[NSMutableArray alloc] init];
shoutOutTexts = [[NSArray
arrayWithObjects:
@"1. XXXXXXXXXXXX",
@"2. XXXXXXXXXXXX",
@"3. XXXXXXXXXXXX",
@"4. XXXXXXXXXXXX",
@"5. XXXXXXXXXXXX",
@"6. XXXXXXXXXXXX"
@"7. XXXXXXXXXXXX",
@"8. XXXXXXXXXXXX",
@"9. XXXXXXXXXXXX",
@"10. XXXXXXXXXXXX",
@"11. XXXXXXXXXXXX",
@"12. XXXXXXXXXXXX",
@"13. XXXXXXXXXXXX",
@"14. XXXXXXXXXXXX",
@"15. XXXXXXXXXXXX",
nil] retain];
shoutOutTimes = [[NSArray
arrayWithObjects:
[[NSNumber alloc] initWithInt: 1],
[[NSNumber alloc] initWithInt: 73],
[[NSNumber alloc] initWithInt: 109],
[[NSNumber alloc] initWithInt: 131],
[[NSNumber alloc] initWithInt: 205],
[[NSNumber alloc] initWithInt: 250],
[[NSNumber alloc] initWithInt: 337],
[[NSNumber alloc] initWithInt: 378],
[[NSNumber alloc] initWithInt: 402],
[[NSNumber alloc] initWithInt: 420],
[[NSNumber alloc] initWithInt: 448],
[[NSNumber alloc] initWithInt: 507],
[[NSNumber alloc] initWithInt: 531],
[[NSNumber alloc] initWithInt: 574],
nil] retain];
self.player = [[MPMoviePlayerController alloc] init];
self.player.contentURL = [self movieURL];
self.player.view.frame = self.viewForMovie.bounds;
self.player.view.autoresizingMask =
UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight;
[self.viewForMovie addSubview:player.view];
backgroundTimer = [NSTimer scheduledTimerWithTimeInterval:0.5f target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
[self.view addSubview:self.myScrollView];
//I am pretty sure that this is the culprit - Just not sure why...
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(movieDurationAvailable:)
name:MPMovieDurationAvailableNotification
object:theTimer];
//Could be wrong, but when commented out I don't have the memory issues
}
—
- (NSInteger)positionFromPlaybackTime:(NSTimeInterval)playbackTime
{
NSInteger position = 0;
for (NSNumber *startsAt in shoutOutTimes)
{
if (playbackTime > [startsAt floatValue])
{
++position;
}
}
return position;
}
-(NSURL *)movieURL
{
NSBundle *bundle = [NSBundle mainBundle];
NSString *moviePath =
[bundle
pathForResource:@"sirloin"
ofType:@"m4v"];
if (moviePath) {
return [NSURL fileURLWithPath:moviePath];
} else {
return nil;
}
}
NSTimeInterval lastCheckAt = 0.0;
- (void)timerAction: theTimer
{
int count = [shoutOutTimes count];
NSInteger position = [self positionFromPlaybackTime:self.player.currentPlaybackTime];
NSLog(@"position is at %d", position);
if (position > 0)
{
--position;
}
if (position < count)
{
NSNumber *timeObj = [shoutOutTimes objectAtIndex:position];
int time = [timeObj intValue];
NSLog(@"shout scheduled for %d", time);
NSLog(@"last check was at %g", lastCheckAt);
NSLog(@"current playback time is %g", self.player.currentPlaybackTime);
if (lastCheckAt < time && self.player.currentPlaybackTime >= time)
{
NSString *shoutString = [shoutOutTexts objectAtIndex:position];
NSLog(@"shouting: %@", shoutString);
CommentView *cview = [[CommentView alloc] initWithText:shoutString];
[self.instructions addSubview:cview];
[shoutString release];
}
}
lastCheckAt = self.player.currentPlaybackTime;
}
// Override to allow orientations other than the default portrait orientation.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return YES;
}
-(void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
[[NSNotificationCenter defaultCenter] removeObserver:MPMovieDurationAvailableNotification];
[[NSNotificationCenter defaultCenter] removeObserver:MPMoviePlayerThumbnailImageRequestDidFinishNotification];
[keyPath release];
}
- (void) movieDurationAvailable:(NSNotification*)notification {
float duration = [self.player duration];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(playerThumbnailImageRequestDidFinish:)
name:MPMoviePlayerThumbnailImageRequestDidFinishNotification
object:nil];
NSMutableArray *times = [[NSMutableArray alloc] init];
for(int i = 0; i < 20; i++) {
float playbackTime = i * duration/20;
[times addObject:[NSNumber numberWithInt:playbackTime]];
}
[self.player
requestThumbnailImagesAtTimes:times
timeOption: MPMovieTimeOptionExact];
}
- (void) playerThumbnailImageRequestDidFinish:(NSNotification*)notification {
NSDictionary *userInfo = [notification userInfo];
NSNumber *timecode =
[userInfo objectForKey: MPMoviePlayerThumbnailTimeKey];
UIImage *image =
[userInfo objectForKey: MPMoviePlayerThumbnailImageKey];
ImageViewWithTime *imageView =
[self makeThumbnailImageViewFromImage:image andTimeCode:timecode];
[myScrollView addSubview:imageView];
UITapGestureRecognizer *tapRecognizer =
[[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(handleTapFrom:)];
[tapRecognizer setNumberOfTapsRequired:1];
[imageView addGestureRecognizer:tapRecognizer];
[tapRecognizer release];
[image release];
[imageView release];
}
- (void)handleTapFrom:(UITapGestureRecognizer *)recognizer {
ImageViewWithTime *imageView = (ImageViewWithTime *) recognizer.view;
self.player.currentPlaybackTime = [imageView.time floatValue];
}
- (ImageViewWithTime *)makeThumbnailImageViewFromImage:(UIImage *)image andTimeCode:(NSNumber *)timecode {
float timeslice = self.player.duration / 3.0;
int pos = [timecode intValue] / (int)timeslice;
float width = 75 *
((float)image.size.width / (float)image.size.height);
self.myScrollView.contentSize =
CGSizeMake((width + 2) * 13, 75);
ImageViewWithTime *imageView =
[[ImageViewWithTime alloc] initWithImage:image];
[imageView setUserInteractionEnabled:YES];
[imageView setFrame:CGRectMake(pos * width + 2, 0, width, 75.0f)];
imageView.time = [[NSNumber alloc] initWithFloat:(pos * timeslice)];
return imageView;
[myScrollView release];
}
- (void)dealloc {
[player release];
[viewForMovie release];
[onScreenDisplayLabel release];
[keyframeTimes release];
[instructions release];
[shoutOutTexts release];
[shoutOutTimes release];
[super dealloc];
}
@end
这个应用程序已经大量使用 UIWebView (只是普通的 sux),所以我正在尝试正确的事情并正确执行。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
您确实遇到了比一个泄漏更多的问题。
第一个
位于您的
initWithNibName:bundle:
中,因为您在那里没有做任何有用的事情:完全摆脱它! (此外:不要释放传递给您的方法的参数!幸运的是,您已将这些释放放在无法访问的行中,即在 return 语句之后...)下一个方法,下一个问题
release
到类对象?不!这在很多层面上都是错误的。textInstructions:
看起来并不可疑,但是...viewDidLoad
还有一些问题@property
将保留它,因此您需要在此处平衡alloc
。backgroundTimer
有一个相应的@property
声明为保留:您在这里违反了此 API 约定,因为您只将计时器分配给 ivar。使用self.backgroundTimer = ...
代替。theTimer
作为调用中的最后一个参数传递给-[NSNotificationCenter addObserver:selector:name:object:]
是传入 nil 作为参数的奇特方式。这很好,因为通常NSTimer
不会发布太多MPMovieDurationAvailableNotification
。事实上,除了在close:
中之外,我看不到theTimer
被使用:这可能只是您引入backgroundTimer 之前的无用残留物吗?
ivar/@property? (嗯,还有另一个同名变量出现,但它应该伴随着一个巨大的编译器警告......)self.player = nil;
?[shoutOutTexts 发布],shoutOutTexts = nil;
?[shoutOutTimes 发布],shoutOutTimes = nil;
?self.keyframeTimes = nil;
?[[NSNotificationCenter defaultCenter]removeObserver:self 名称:MPMovieDurationAvailableNotification 对象:nil];
?self.backgroundTimer = nil;
? (假设,setBackgroundTimer:
释放并使旧值无效)shoutOutTimes
的设置中使用[NSNumber numberWithInt:]
而不是alloc/init。关于
movieURL
的一点小评论,您可以将其变成以下一行:然后
在全局范围内使用此
NSTimeInterval lastCheckAt = 0.0;
。从你的用法来看:ivar PLZ?!?one?更多问题稍后再说。我得先给自己弄点吃的。
第二部分
现在让我们进入 timerAction:
第一个问题并不是太严重,尤其是在这个特定的上下文中,但是您应该知道
-[NSArray count]
返回一个NSUInteger
并且 U 不是拼写错误,而是表明该值是无符号的。您当然不会在此应用程序中遇到签名问题,并且在其他情况下也很少遇到这种情况,但当您这样做时,它们会弥补非常奇怪的错误,您应该意识到其中的含义。 .然而,这种方法的真正问题是,您每次迭代都会泄漏一个
CommentView
,同时 - 过度释放一个NSString
接下来:
removeObserver:forKeyPath:
您真的应该改掉释放传递给方法的参数的坏习惯!
话虽这么说,摆脱整个这个方法!
首先也是最重要的
removeObserver:forKeyPath:
是NSKeyValueObserving
非正式协议中的一个方法,它所扮演的角色与您在这里使用它来完成的任务完全不同。其次,它是那些必须调用
super
的方法之一(无论以何种方式),如果您确实需要重写它。 (嗯,除了,当你覆盖addObserver:forKeyPath:options:context:
以及-it-should-go-without-saying-that-you-shouldn't-do-that-除非你真的知道你在做什么,如果你曾经计划使用 KVO。)movieDurationAvailable:
就像 Evan 所说,你正在泄漏
次
在这里。采纳他的建议,或者将其设为NSMutableArray *times = [NSMutableArray array];
,这样就完成了。playerThumbnailImageRequestDidFinish:
您不拥有
图像
,因此请勿释放它!就我个人而言,我会在将视图添加到视图层次结构之前完成视图的设置(即添加识别器并执行类似的操作),但这完全是一个品味问题...
makeThumbnailImageViewFromImage:andTimeCode:
...泄漏
NSNumber
(使用[NSNumber numberWithFloat:(pos * timeslice)]
而不是alloc/initWithFloat:
-dance)防止您因直接在其前面的无条件返回语句过度释放myScrollView
而崩溃(唷!)。当我们这样做时:将此方法重命名为
newThumbnailImageView...
,这样当您在一年左右重新访问此代码时,您会立即知道[imageView release];
确实是必要的,而不必查看该方法的实现。playerThumbnailImageRequestDidFinish:
底部的或者,您可以将其重命名为
thumbnailImageView...
并将 return 语句更改为return [imageView autorelease];
。奖励:playerThumbnailImageRequestDidFinish:
中少一行,因为[imageView release];
就变得过时了。dealloc
在最顶部添加
[[NSNotificationCenterdefaultCenter]removeObserver:self];
。其余部分看起来还不错。 (尽管我觉得很奇怪,除了它的声明之外,ivar
landscapeView
从未被提及过。)摘要
阅读这些部分内存管理规则和Autorelease 再次来自 Apple 的“内存管理编程指南”。它们是纯金的!
You do have a couple more issues than one leak.
The first one
is in your
initWithNibName:bundle:
as you aren't doing anything useful there: get rid of it, entirely! (Besides: don't release arguments, that are passed to your methods! Luckily, you've placed those releases in lines that are unreachable, i.e. after the return statement...)Next method, next problems
release
to a class object? Don't! That's wrong on many levels.setTheTimer:
andsetBackgroundTimer:
to handle the invalidation and release properly and simply doself.theTimer = nil; self.backgroundTimer = nil;
here. That would fix the asymmetry in handling those things, as well. (By the way: theTimer isn't such a great name for an ivar...especially when there is another ivar that is a timer!)textInstructions:
looks unsuspicious but...viewDidLoad
has some more issuesThe
@property
will retain it, so you need to balance thealloc
here.backgroundTimer
has a corresponding@property
that is declared to be retaining: You are violating this API contract here, since you only assign the timer to the ivar. Useself.backgroundTimer = ...
instead.theTimer
as the last argument in your call to-[NSNotificationCenter addObserver:selector:name:object:]
is a fancy way of passing innil
as that parameter. Which is kind of good, because usuallyNSTimer
doesn't post too manyMPMovieDurationAvailableNotification
s. In fact, as I can't seetheTimer
being used except inclose:
: Could it be that this is just a useless remnant from before you introduced thebackgroundTimer
ivar/@property? (Well there is another occurrence of a variable of that name, but it should be accompanied by a big fat compiler warning...)viewDidUnload
? If so, does it:self.player = nil;
?[shoutOutTexts release], shoutOutTexts = nil;
?[shoutOutTimes release], shoutOutTimes = nil;
?self.keyframeTimes = nil;
?[[NSNotificationCenter defaultCenter] removeObserver:self name: MPMovieDurationAvailableNotification object:nil];
?self.backgroundTimer = nil;
? (Assuming,setBackgroundTimer:
releases and invalidates the old value)NSNumber
s here. Use[NSNumber numberWithInt:]
instead of alloc/init in the setup ofshoutOutTimes
.A minor remark on
movieURL
, which you can turn into the following one-liner:And then this
NSTimeInterval lastCheckAt = 0.0;
within the global scope. From your usage of it: ivar PLZ?!?one?More issues later. I gotta get myself something to eat first.
Part Two
Now let's get into
timerAction:
The first issue is not too grave, — especially in this particular context — but you should be aware that
-[NSArray count]
returns anNSUInteger
and that the U is not a typo, but a designation that this value is unsigned. You certainly won't run into problems with the signedness in this App and rarely do on other occasions, but when you do, they make up for really funky bugs and you should be aware of the implications...The real issue with this method is, however, that you are leaking one
CommentView
per iteration while — at the same time — overreleasing oneNSString
. The fact, that you were using string literals (which will never be dealloced) in the first place, (i.e. when you were initializing shoutOutTimes) totally saves your butt, here.Next up:
removeObserver:forKeyPath:
You really should get rid of that really bad habit of releasing parameters, that are passed to your methods!
That being said, get rid of the entirety of this method!
First and foremost
removeObserver:forKeyPath:
is a method from theNSKeyValueObserving
informal protocol and plays a totally different role than what you are (ab-)using it to accomplish here.Secondly, it is one of those methods where it is essential to call through to
super
if — by any means — you really need to override it. (Well, except, when you were overridingaddObserver:forKeyPath:options:context:
as well and-it-should-go-without-saying-that-you-shouldn't-do-that-unless-you-really-know-what-you-are-doing-if-you-ever-planned-on-using-KVO.)movieDurationAvailable:
Like Evan said, you're leaking
times
here. Go for his suggestion or — instead — make itNSMutableArray *times = [NSMutableArray array];
and you are done here.playerThumbnailImageRequestDidFinish:
You don't own
image
, so don't release it!Personally, I would finish setting up the view (i.e. add the recognizer and do stuff like that) before adding it into the view-hierarchy, but that's completely a matter of taste...
makeThumbnailImageViewFromImage:andTimeCode:
...leaks an
NSNumber
(use[NSNumber numberWithFloat:(pos * timeslice)]
instead of thealloc/initWithFloat:
-dance) and prevents you from crashing due to overreleasingmyScrollView
by the unconditional return statement that directly precedes it (phew!).While we're at it: rename this method to either
newThumbnailImageView...
so that when you'll revisit this code in a year or so, you instantly know that[imageView release];
at the bottom ofplayerThumbnailImageRequestDidFinish:
really is necessary, without having to look at the implementation of this method.Alternatively, you could rename it to
thumbnailImageView...
and change the return statement toreturn [imageView autorelease];
. Bonus: one fewer line inplayerThumbnailImageRequestDidFinish:
as the[imageView release];
there becomes obsolete then.dealloc
Add
[[NSNotificationCenter defaultCenter] removeObserver:self];
at the very top.The rest of it looks okay. (Although I find it odd, that the ivar
landscapeView
is never/nowhere mentioned apart from its declaration.)Summary
Read the sections Memory Management Rules and Autorelease from Apple's "Memory Management Programming Guide" once again. They're pure gold!
您永远不会在
movieDurationAvailable:
中释放times
:将其传递给方法时应该使用
autorelease
:You never release
times
inmovieDurationAvailable:
:You should use
autorelease
when you pass it to the method: