NSCacheとUINibの大群

数か月前に見つけた別のクラッシュを共有したいと思います。 現在、このタイプのクラッシュレポートはHockeyAppで見られなくなりましたが、以前は最も人気のあるレポートの1つでした。 実際、この問題はかなり以前から観察されていますが、その後、アプリケーションはまだTestFlightを使用しており、分析のための情報はありませんでした。 クラッシュは、そのようなスタックによってほぼ特徴付けられました。



Thread 0 Crashed: 0 libobjc.A.dylib 0x39abcf42 objc_msgSend + 2 1 CoreFoundation 0x2bfe0c61 __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 10 2 CoreFoundation 0x2bf3c6d5 _CFXNotificationPost + 1782 3 Foundation 0x2cc6e129 -[NSNotificationCenter postNotificationName:object:userInfo:] + 70 4 Foundation 0x2cc72c8f -[NSNotificationCenter postNotificationName:object:] + 28 5 UIKit 0x2f750883 -[UIApplication _performMemoryWarning] + 132 6 libdispatch.dylib 0x3a0107a7 _dispatch_client_callout + 20 7 libdispatch.dylib 0x3a021253 _dispatch_source_latch_and_call + 624 8 libdispatch.dylib 0x3a0122ed _dispatch_source_invoke + 210 9 libdispatch.dylib 0x3a013e1f _dispatch_main_queue_callback_4CF + 328 10 CoreFoundation 0x2bfee3b1 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 6 11 CoreFoundation 0x2bfecab1 __CFRunLoopRun + 1510 12 CoreFoundation 0x2bf3a3c1 CFRunLoopRunSpecific + 474 13 CoreFoundation 0x2bf3a1d3 CFRunLoopRunInMode + 104 14 GraphicsServices 0x332cf0a9 GSEventRunModal + 134 15 UIKit 0x2f5487b1 UIApplicationMain + 1438 16 xxx 0x0015bb81 main (main.m:18) 17 libdyld.dylib 0x3a030aaf start + 0
      
      





-[UIApplication _performMemoryWarning]を呼び出すことにより、メモリ警告の処理中に問題が発生したことは明らかです。 どうやら、いくつかのオブジェクトはUIApplicationDidReceiveMemoryWarningNotificationにサブスクライブし、破棄する前にサブスクライブを解除するのを忘れていました。 しかし、プロジェクトコードをチェックしても、疑わしい状況は明らかになりませんでした。この通知を使用した人は全員、シングルトンか、多少なりとも正しく登録解除されました。 当時、これはこれに限定されていました。これまで修正のアイデアはありませんでした。



次に、AppleがTestFlightを購入したときに、HockeyAppに切り替えました。 彼らはクールなクラッカー(PLCrashReporter)を使用し、一般に、クラッシュの処理はずっと優れていました(デバイスからレポートを送信するときにログ/情報を添付することもできます)。 しかし、上記のスタックに加えて、問題に戻ると、次の行も表示されます。



 Application Specific Information: objc_msgSend() selector name: setArchiveData:
      
      





これで、死亡したオブジェクトに送信されたセレクターがわかりました。 コードにはそのようなメソッド/プロパティはなく、以前の分析を確認しました。 したがって、タスクは、そのようなセレクターを持つクラスを見つけることです。 ランタイムのobj-c関数objc_getClassList(登録済みクラスのリストを提供)およびclass_copyMethodList(インスタンスメソッドとクラス自体を取得可能)がこれに役立ちます。 すべてのクラスを調べて、すべてのセレクターをチェックした後、唯一のオプション-UINibStorageがありました。 これはプライベートクラスであり、そのメソッドのスウィズリングを使用して、UINibsによって作成および保持されていることがわかります。 次に、スウィズルと逆アセンブルを使用して、UINibがUIApplicationDidReceiveMemoryWarningNotificationをサブスクライブし、それが受信されると、UINibStorageの内容(setArchiveDataへの呼び出しを含む)をクリアすることを確認します。この呼び出しはクラッシュログに記録されます。 通知のサブスクライブ解除は、UINib deallocで行われます。 UINibが死亡したが、通知を受け取ったのはどうしてですか?



問題は、NSCacheを使用してanyをキャッシュしたためであると思われました。 十分なメモリがない場合、NSCacheはバックグラウンドスレッドのコンテンツをクリアします。 基本的にメインスレッドのメモリ警告と非同期です。 T.O. [UINib dealloc]は、通知からサブスクライブされていないバックグラウンドスレッドで呼び出され、メインで処理されます。 これは、NSNotificationCenterを使用するための間違った危険なアプローチです。 一般的に言えば、プロジェクトの作業中に、非同期に関連する多くのバグを修正することができました。 多くの非同期操作がそこで実行されます。 私が遭遇したよくある間違いの1つは、deallokにあるもののキャンセルまたは登録解除です。 これは遅すぎます。 オブジェクトは実際に死にかけているので、非同期操作が同時にそれを処理しようとすると、それはひどく終了します。 残念ながら、厳しい現実は、退会できる良い場所が常にあるとは限らないということです。 UINibの場合、そのような便利な場所がないことは明らかであるため、非難するのは困難です(むしろ、インフラストラクチャまたはNSNotificationCenterを非難する方が良いです)。



この問題の解決策として、ペン先を保存するための簡単なキャッシュを作成しました。 一般に、これはNSCacheの最初のクラッシュではありません。 以前は、NSCacheにNSCacheを保存することに関連するクラッシュをすでに修正していました。これも実行する価値はありません。 ただし、NSCacheを明示的に有罪にすることもできません。 このリリースが最後であり、deallocは何もしないので、バックグラウンドスレッドでオブジェクトにリリースを送信できないと考えてはいけません。 おそらく、この状況は、明確で正当な解決策が否定的な結果をもたらす場合の1つです。



All Articles