CI実行時だけ一定確率で失敗するというテストがあり, 長らく原因が分からず仕舞いだった.
たまたま実機で特定の動作手順で確実に再現するバグがあり, それを直したところ上記の問題が解消されるということがあった.
今回はそれについて紹介する.
TL; DR
NSPointerArray.compactを実行する前に, addPointer(nil)を実行しよう
NSPointerArrayとは
まず, NSPointerArrayについて軽く説明する.
このクラスは要素がweak
である配列が欲しい時に利用するクラスだ.
例えば上記のように実装すると, observerの参照カウンタを保持せず, notificate実行時に解放されているobserverには呼び出しを行わないことができる.
複数に対してdelegateを実行しない場合に有効な書き方である.
NSPointerArray.compact
NSPointerArrayはnilも保持できるようになっており, 解放された場合でも自動的にNSPointerArrayから削除される訳ではない.
つまり, すぐに解放されるようなポインタを大量に追加した場合, 内部配列の要素数が増え続けることになる.
これを解消する為に, compactというメソッドが用意されている.
Removes NULL values from the receiver.
とあるように, 内部配列内に含まれるnilを取り除き, 内部配列の要素数を必要なサイズにすることができる……はずだった.
このcompactにはアンドキュメントな仕様がある為, 必ずしもnilを取り除いてくれるとは限らない.
確実にcompactionを実行する方法
NSPointerArrayはneedsCompaction
という非公開のプロパティを持っている.
この値がtrue
になっている状態でcompact
を実行した時のみcompactionが行われる.
そして, 内部配列に追加されたインスタンスが解放されてもこの値はtrue
にならない.
実際に上記のコードを実行すると, compact
後にcount
が減っていないことが分かる.
これを以下のように修正することでcompactionを確実に実行することができる.
NSPointerArrayを使った正しい実装
上記を踏まえて, 先程のコードを正しいコードに修正する.
呼び出し頻度に応じて, どこでcompactを行うかは調整すると良い.
参考文献
この記事中のテストコードはここから参照できる.