blog

メモリリーク解析のiOS開発シリーズ

この記事では、通知とKVOは、オブザーバ、ブロックの循環参照、NSThreadとRunLoopを削除しないことによって引き起こされるメモリリークに焦点を当てます。\n通知によるメモリリーク\n1.1....

Jul 15, 2020 · 10 min. read
シェア

webViewのメモリリーク解析を追加しました。

続き、この投稿ではnotificationとKVOがオブザーバーを削除しないことによるメモリー・リーク、ブロック循環参照、NSThreadとRunLoopの併用に焦点を当てます。

通知によるメモリリーク

1.1.iOS9以降、一般的な通知の場合、手動でオブザーバを削除する必要がなくなりました。iOS9以前は、手動で削除する必要がありました。

iOS9の以前のオブザーバ登録では、通知センターはオブザーバオブジェクトのretain操作を行わず、unsafe_unretained参照を行うため、オブザーバが再取得されたときに、通知を手動で削除しないと、再取得されたメモリ領域を指すポインタがワイルドポインタになり、通知を送信するとプログラムがクラッシュします。

iOS9から、Notification Centreはobserverを弱く参照するようになったので、手動で通知を削除しなくても、observerがreclaimされたときにポインタは自動的にnullになり、nullポインタにメッセージを送るような通知を送っても問題ありません。

1.2.このAPIを使用すると、オブザーバーがシステムによって保持されるため、リスニングのブロックメソッドを使用する通知はまだ処理する必要があります。

以下のコードをご覧ください:

[[NSNotificationCenter defaultCenter] addObserverForName:@"notiMemoryLeak" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { NSLog(@"11111"); }]; //通知を送信する [[NSNotificationCenter defaultCenter] postNotificationName:@"notiMemoryLeak" object:nil];

初回は1回、2回目は2回、3回目は3回印刷されます。デモで試すことができます。デモのアドレスは記事の一番下をご覧ください。

解決策は、通知の受信者を記録し、それをdeallocから削除することです:

@property(nonatomic, strong) id observer;
self.observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"notiMemoryLeak" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { NSLog(@"11111"); }]; //通知を送信する [[NSNotificationCenter defaultCenter] postNotificationName:@"notiMemoryLeak" object:nil];
- (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self.observer name:@"notiMemoryLeak" object:nil]; NSLog(@"hi,私はdealloc'dだ"); }

KVOによるメモリリーク

2.1、今KVOの一般的な使用は、オブザーバーを削除しなくても、問題はありません

以下のコードをご覧ください:

- (void)kvoMemoryLeak { MFMemoryLeakView *view = [[MFMemoryLeakView alloc] initWithFrame:self.view.bounds]; [self.view addSubview:view]; [view addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew context:nil]; //積極的にkvoを刺激するために、これらの2つの文章を呼び出す具体的な原則は、後のkvoの詳細な説明で説明される [view willChangeValueForKey:@"frame"]; [view didChangeValueForKey:@"frame"]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { if ([keyPath isEqualToString:@"frame"]) { NSLog(@"view = %@",object); } }

この場合、コントローラが破壊されるとビューも破壊されるので、ビューのフレームが変更されなくなり、オブザーバを削除しなくても問題がないからだと思いますが、オブザーバが破壊されないオブジェクトだったらどうなるのだろうと推測してみました。もし、オブザーバーが破壊されないオブジェクトだったらどうなるでしょうか? オブザーバーが破壊されても、見ているオブジェクトが変化している場合、問題はないでしょうか?

2.2.オブザーバーを取り除かなければ破壊されないオブジェクトをオブザーバーすると、不確定なクラッシュが発生します。

上記の推測をもとに、まず、プロパティ・タイトルを持つシングルトン・オブジェクト MFMemoryLeakObject を作成します:

@interface MFMemoryLeakObject : NSObject @property (nonatomic, copy) NSString *title; + (MFMemoryLeakObject *)sharedInstance; @end #import "MFMemoryLeakObject.h" @implementation MFMemoryLeakObject + (MFMemoryLeakObject *)sharedInstance { static MFMemoryLeakObject *sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; sharedInstance.title = @"1"; }); return sharedInstance; } @end
#import "MFMemoryLeakView.h" #import "MFMemoryLeakObject.h" @implementation MFMemoryLeakView - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.backgroundColor = [UIColor whiteColor]; [self viewKvoMemoryLeak]; } return self; } #pragma mark - 6.KVOメモリリークの原因 - (void)viewKvoMemoryLeak { [[MFMemoryLeakObject sharedInstance] addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:nil]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { if ([keyPath isEqualToString:@"title"]) { NSLog(@"[MFMemoryLeakObject sharedInstance].title = %@",[MFMemoryLeakObject sharedInstance].title); } }

最後に、ビューが破棄される前と後で、タイトルの値がコントローラで変更されます:

//6.MFMemoryLeakViewでシングルトンオブジェクトに耳を傾ける。
MFMemoryLeakView *view = [[MFMemoryLeakView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:view];
[MFMemoryLeakObject sharedInstance].title = @"2";
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
 [view removeFromSuperview];
 [MFMemoryLeakObject sharedInstance].title = @"3";
});

試した後、最初の時間は問題ありませんが、クラッシュ時に2回目、レポートエラー野生のポインタは、特定のあなたがテストを行うデモを使用することができ、デモのアドレスは底を参照してください。

解決策は簡単で、ビューのdeallocメソッドからオブザーバーを削除することです:

- (void)dealloc { [[MFMemoryLeakObject sharedInstance] removeObserver:self forKeyPath:@"title"]; NSLog(@"hi,私はMFMemoryLeakViewが解除された"); }

、ブロックによるメモリリーク

ブロックによるメモリー・リークは通常、循環参照で、ブロックの所有者がブロックのスコープ内で自分自身を参照するため、ブロックの所有者はメモリーを解放することができません。

この記事では、ブロックに起因するメモリ・リーク・シナリオの解析と解決策のみを説明し、他のブロックの原理については、後でブロックに関する別の章で説明します。

3.1.ブロックの属性として、selfまたはメンバ変数と呼ばれ、循環参照を引き起こします。

以下のコードを見て、ブロック・プロパティを定義することから始めましょう:

typedef void (^BlockType)(void); @interface MFMemoryLeakViewController () @property (nonatomic, copy) BlockType block; @property (nonatomic, assign) NSInteger timerCount; @end

それから、電話をかけてください。

#pragma mark - 7.block メモリリークの原因 - (void)blockMemoryLeak { // 7.1 通常のブロック循環参照 self.block = ^(){ NSLog(@"MFMemoryLeakViewController = %@",self); NSLog(@"MFMemoryLeakViewController = %zd",_timerCount); }; self.block(); }

この結果、ブロックやコントローラへの循環参照が発生しますが、MRCでは__block、ARCでは__weakを使用してループを解除し、インスタンス変数に->アクセスすることで簡単に解決できます。

weakによってのみ変更されたオブジェクトは、解放された場合、ブロック実行中にnilになることに注意してください。

そのため、ブロック内で__weakによって変更されたオブジェクトへのstrong参照を作成することを推奨します。そうすることで、ブロック実行中にオブジェクトがnilに設定されることがなくなり、ブロック実行終了後にオブジェクトがARCの下で自動的に解放されるため、循環参照が発生しなくなります:

- (void)threadMemoryLeak { NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadRun) object:nil]; [thread start]; } - (void)threadRun { [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] run]; }

3.2.ブロックを使ってNSTimerを作成する場合は、循環参照に注意してください。

このコードをご覧ください:

- (void)threadMemoryLeak {
 NSThread *thread = [[NSThread alloc] initWithBlock:^{
 [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
 [[NSRunLoop currentRunLoop] run];
 }];
 [thread start];
}

ブロックの観点から、ここで循環参照はありません、実際には、このクラスの内部メソッドでは、自己の強力な参照にタイマーがあるので、また、__weakを使用して閉ループを切断する必要があり、さらに、タイマーを作成するこの方法は、REPEATS YES、また、INVALIDATE処理を実行する必要があり、そうでなければ、タイマーはまだ停止することはできません!そうしないと、タイマーは停止しません。

UIWebView *webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
webView.backgroundColor = [UIColor whiteColor];
NSURLRequest *requset = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://..com"]];
[webView loadRequest:requset];
[self.view addSubview:webView];
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
config.userContentController = [[WKUserContentController alloc] init];
[config.userContentController addScriptMessageHandler:self name:@"WKWebViewHandler"];
WKWebView *wkWebView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
wkWebView.backgroundColor = [UIColor whiteColor];
[self.view addSubview:wkWebView];
NSURLRequest *requset = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://..com"]];
[wkWebView loadRequest:requset];
@property (nonatomic, strong) WKWebView *wkWebView;

NSThreadによるメモリリーク

NSThreadをRunLoopと組み合わせて使用する場合は、循環参照に注意してください:

- (void)webviewMemoryLeak {
 // 9.2 WKWebView
 WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
 config.userContentController = [[WKUserContentController alloc] init];
 [config.userContentController addScriptMessageHandler:self name:@"WKWebViewHandler"];
 _wkWebView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
 _wkWebView.backgroundColor = [UIColor whiteColor];
 [self.view addSubview:_wkWebView];
 NSURLRequest *requset = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://..com"]];
 [_wkWebView loadRequest:requset];
}

この問題を引き起こしているのは、"[[NSRunLoop currentRunLoop] run]; "というコードです。この原因は、NSRunLoopのrunメソッドが停止不可能で、決して破壊されないスレッドを開始することに特化しており、スレッド作成も現在の現在のコントローラを強く参照するため、循環参照を引き起こすからです。

解決策は、作成時にブロックメソッドを使用することです:

- (void)viewDidDisappear:(BOOL)animated {
 [super viewDidDisappear:animated];
 [_wkWebView.configuration.userContentController removeScriptMessageHandlerForName:@"WKWebViewHandler"];
}

CFRunLoopStop(CFRunLoopGetCurrent());」を呼び出しても、スレッドを停止することはできません。次の実行ループはまだ続けることができます。この方法はRunLoopの別の章で説明します。

webViewによるメモリリーク

現在、iOSにはUIWebViewとWKWebViewの2種類のWebViewがあります。

5.1, UIWebView

UIWebViewのメモリ問題はよく知られているはずです、Appleも正式にメモリリークが存在することを認めたので、iOS8では、より強力な機能とパフォーマンスのWKWebViewを導入しました。

以下のコードを見てください:


このような単純なコードで、ウェブページを開くとメモリが200Mも高騰し、親ページに戻ってwebViewを破棄してもメモリは50Mほど増え、コントローラのdeallocで空のurlを読み込んでもやはりうまくいきません、デモで試してみてください。

5.2, WKWebView

一般的に、WKWebView はパフォーマンスと機能の面で UIWebView よりもはるかに強力であり、それ自体にメモリリークはありませんが、開発者が適切に使用しないとメモリリークを引き起こす可能性があります。以下のコードを参照してください:


これは問題ないように見えますが、実際には、"addScriptMessageHandler "アクションはwkWebViewにselfへの強い参照を作らせ、次に "addSubview"を実行すると、"addSubview "もselfをwkWebViewに強く参照させるため、循環参照になります。

解決策は、例えば、適切なタイミングで「MessageHandler」に対して削除操作を行うことです:




デモでは、" viewDidDisappear" メソッドで削除を行い、コントローラを解放できるようにしました。

このメモリリーク解析は、ここに書いて、私のレベルの限界のため、多くの場所はまだ十分に深く話していない、あなたが修正することを歓迎します。

Read next

Spring Boot実践講座(16): mysql8 IPリモートログインを制限する

全ユーザーをここで確認できます。

Jul 15, 2020 · 3 min read

HTTPプロトコルの歴史

Jul 15, 2020 · 4 min read

Js実行コンテキスト

Jul 14, 2020 · 2 min read

OpenGLベクトルと行列

Jul 14, 2020 · 4 min read

Python:データクラスを使う。

Jul 14, 2020 · 2 min read