Xcode - Perdita di memoria iOS che mi sta facendo impazzire, penso che sia NSnotificationCenter, ma sperando che gli occhi freschi possano vedere cosa non posso vedere cosa non posso
-
27-10-2019 - |
Domanda
Il codice qui è una vista modale lanciata da RootViewController ed è quella di visualizzare un video con una pellicola in miniatura sotto il film e quindi le istruzioni a tempo limitate al film.
Funziona tutto, ma c'è una perdita di memoria / mancanza di rilascio che non riesco a vedere per aver guardato e aver trascorso tre giorni cercando di risolverlo, è giunto il momento di chiedere aiuto ...
Se disabilito NSNotificationCenter commentandolo (evidenziato nel .m) non ho problemi relativi alla memoria e mantengo il testo a tempo. Ma non ho nemmeno miniature. Ho provato a inserire [[NSNotificationCenter alloc] removeObserver:self];
In numerosi punti per vedere se questo si sbarazzerà per me. Ma ahimè, senza risultati.
Ho anche provato a rilasciare il "sfondo", ma non è eccessivamente colpito quando provo a compilare e correre.
In sostanza, la prima volta che carico la vista modale, non ci sono problemi e sembrano grandi - tuttavia, se lo chiudo con il -(IBAction)close:(id)sender;
Sembra che qualcosa non stia rilasciando poiché la prossima volta che lancio la stessa pagina l'utilizzo della memoria aumenta di circa il 30% (all'incirca l'importo utilizzato dalla generazione della miniatura) e aumenta di circa lo stesso importo ogni volta Vista modale.
Per favore, mi sente a mente che sono un principiante per questo e l'errore è probabilmente un sanguinoso stupido per quelli di voi a conoscenza. Ma nell'interesse di fare questo progetto, prenderò volentieri qualsiasi abuso che desideri lanciare contro di me.
Ecco il codice:
.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
Questa app è già là fuori usando UiWebView (che semplicemente semplicemente sux), quindi sto cercando di fare le cose bene e farlo correttamente.
Soluzione
Hai un paio di problemi in più di uno perdere.
Il primo
è nel tuo initWithNibName:bundle:
Dato che non stai facendo nulla di utile lì: liberartene, del tutto! (Inoltre: non rilasciare argomenti, che vengono passati ai tuoi metodi! Per fortuna, hai messo quelle versioni in linee irraggiungibili, cioè dopo la dichiarazione di ritorno ...)
Next Method, Next Problems
- Perché stai inviando
release
a un oggetto di classe? Non! È sbagliato molti livelli. - Hai deciso di creare proprietà per i tuoi timer. Non è niente di male di per sé. Ma perché allora, stai usando gli Ivars direttamente qui? Ti incoraggio fortemente ad implementare
setTheTimer:
esetBackgroundTimer:
per gestire l'invalidazione e rilasciare correttamente e semplicemente farloself.theTimer = nil; self.backgroundTimer = nil;
qui. Ciò correggerebbe anche l'asimmetria nella gestione di quelle cose. (A proposito: Thetimer non lo è tale un grande nome per un ivar ... specialmente quando c'è altro Ivar che è un timer!)
textInstructions:
Sembra onorevole ma ...
viewDidLoad
ha altri problemi
- Flega un mpmovieplayercontroller:
Il@property
lo manterrà, quindi devi bilanciare ilalloc
qui. backgroundTimer
ha un corrispondente@property
Ciò è dichiarato che stia trattenendo: stai violando questo contratto API qui, poiché assegni solo il timer all'IVAR. Usoself.backgroundTimer = ...
invece.- Da tutto il codice che hai pubblicato, mi sembra che passino
theTimer
Come ultimo argomento nella tua chiamata a-[NSNotificationCenter addObserver:selector:name:object:]
è un modo elegante di trasmetterenil
come quel parametro. Che è un po 'buono, perché di solitoNSTimer
non ne pubblica troppiMPMovieDurationAvailableNotification
S. In effetti, come non riesco a vederetheTimer
essere usato tranne inclose:
: Potrebbe essere che questo sia solo un reMint inutile da prima di introdurre ilbackgroundTimer
Ivar/@proprietà? (Beh, c'è un'altra occorrenza di una variabile di quel nome, ma dovrebbe essere accompagnato da un grande avvertimento del compilatore a grasso ...) - In ogni modo, implementi
viewDidUnload
? In tal caso, lo fa: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;
? (Supponendo,setBackgroundTimer:
rilasci e invalida il vecchio valore)
- Aggiornare Mi sono perso questo al primo giro: stai perdendo 15
NSNumber
s qui. Uso[NSNumber numberWithInt:]
invece di alloc/init nella configurazione dishoutOutTimes
.
Una piccola osservazione su movieURL
, che puoi trasformare nel seguente one-liner:
-(NSURL*)movieURL {
return [[NSBundle mainBundle] URLForResource:@"sirloin" withExtension:@"m4v"];
}
E poi questo
NSTimeInterval lastCheckAt = 0.0;
All'interno dell'ambito globale. Dal tuo utilizzo: Ivar Plz?!? Uno?
Più problemi più tardi. Devo prendermi qualcosa da mangiare per primo.
Seconda parte
Ora entriamo in timerAction:
Il primo problema non è troppo grave, - specialmente in questo particolare contesto - ma dovresti esserne consapevole -[NSArray count]
Restituisce un NSUInteger
e quello il U non è un errore di battitura, ma una designazione che questo valore non è firmato. Certamente non ti imbatterai in problemi con la firma in questa app e raramente lo faccerai in altre occasioni, ma quando Lo fai, compensano bug davvero funky e dovresti essere consapevole delle implicazioni ...
Il vero problema con questo metodo è, tuttavia, che lo sei che perde uno CommentView
per iterazione mentre - allo stesso tempo - in modo eccessivo NSString
. Il fatto che stavi usando i letterali di stringa (che non verranno mai trattati) in primo luogo, (cioè quando stavi inizializzando ShoutoutTimes) salva totalmente il culo, qui.
Prossimo: removeObserver:forKeyPath:
Voi veramente Dovrebbe sbarazzarsi di quella cattiva abitudine di rilasciare parametri, che vengono passati ai tuoi metodi!
Detto questo, sbarazzati dell'intero metodo!
Innanzitutto removeObserver:forKeyPath:
è un metodo del NSKeyValueObserving
Protocollo informale e svolge un ruolo totalmente diverso da quello che stai (ab-) che lo usa per realizzare qui. In secondo luogo, è uno di quei metodi in cui è essenziale chiamare super
if - in ogni caso - tu veramente bisogno di prevalerlo. (Beh, tranne, quando eri prevalente addObserver:forKeyPath:options:context:
e non vorrebbe e-senza che-senza dire che non sia da fare per non essere senza che-sei-sai-cosa-cosa-sei-in-fare-if-you-ever-planned-- on-use-kvo.)
movieDurationAvailable:
Come ha detto Evan, stai perdendo times
qui. Scegli il suo suggerimento o - invece - fallo NSMutableArray *times = [NSMutableArray array];
E hai finito qui.
playerThumbnailImageRequestDidFinish:
Non possiedi image
, quindi non rilasciarlo!
Personalmente, finirei di impostare la vista (cioè aggiungere il riconoscimento e fare cose del genere) prima di aggiungerlo alla gerarchia della vista, ma è completamente una questione di gusti ...
makeThumbnailImageViewFromImage:andTimeCode:
... perdite an NSNumber
(uso [NSNumber numberWithFloat:(pos * timeslice)]
invece di alloc/initWithFloat:
-dance) e ti impedisce di crash a causa del prevalente myScrollView
dalla dichiarazione di ritorno incondizionata che lo precede direttamente (hew!). Mentre ci siamo: rinominare questo metodo a entrambi newThumbnailImageView...
in modo che quando rivisiterai questo codice tra circa un anno, lo sai immediatamente [imageView release];
in fondo a playerThumbnailImageRequestDidFinish:
È davvero necessario, senza dover esaminare l'implementazione di questo metodo.
In alternativa, potresti rinominarlo a thumbnailImageView...
e modificare la dichiarazione di ritorno in return [imageView autorelease];
. Bonus: una linea in meno in playerThumbnailImageRequestDidFinish:
come la [imageView release];
allora diventa obsoleto.
dealloc
Aggiungere [[NSNotificationCenter defaultCenter] removeObserver:self];
in cima.
Il resto sembra a posto. (Anche se lo trovo strano, che l'Ivar landscapeView
non è mai/nessun posto menzionato a parte la sua dichiarazione.)
Riepilogo
Leggi le sezioni Regole di gestione della memoria e Autorelease Dalla "Guida alla programmazione della gestione della memoria di Apple" ancora una volta. Sono oro puro!
Altri suggerimenti
Non rilasci mai times
in movieDurationAvailable:
:
NSMutableArray *times = [[NSMutableArray alloc] init];
Dovresti usare autorelease
Quando lo passi al metodo:
[self.player requestThumbnailImagesAtTimes:[times autorelease] timeOption: MPMovieTimeOptionExact];