寄付窓口はこちら

テストケースでMemory Leakを発見する | try! Swift Tokyo 2019 1-8

SwiftはARCでメモリを管理しています。従って下手な書き方をすればメモリリークがいとも簡単に発生します。 どのようにメモリリークを防ぎますか?コーディング規約、コードレビュー、最後の砦のQA… 勿論、我々は怠惰なので、努力や根性よりも、自動的に動作するものを好みます。第四の方法として、Mirrorを使ったテストケースの実装を紹介します。

f:id:niwatako:20190321143503j:plain

f:id:niwatako:20190321143545j:plain

最近のタスクはコードを書くなくなってリファクタリングやコードレビュー、ペアプロ、プロデューサーとのコミュニケーションなど。

スマブラスプラトゥーンやっていたのが1ヶ月前、2倍で大変だったが、FF14もやって3倍になっています

f:id:niwatako:20190321143632j:plain

アイコンが何に見えるか、みなさんに聞いてみました

ドラゴン?

f:id:niwatako:20190321143651j:plain

実はナメクジがヘッドホンで音楽を聞いています。

左側の解釈を聞いてから、僕自身、左に見えると思っていますが、右、ナメクジです。

f:id:niwatako:20190321143721j:plain

f:id:niwatako:20190321143725j:plain

一つの画面をマイクロサービスで再現する取り組みを去年のiOSDCで発表しました。

f:id:niwatako:20190321143746j:plain

これはメモリリークする。

f:id:niwatako:20190321143751j:plain

ガベージコレクションがない。

f:id:niwatako:20190321143804j:plain

たまにこの話をするとARCはガベージコレクションだという人も居るが、話がややこしくなるのでその話はやめましょう。

ガベージコレクションがない代わりにどうメモリを開放しているのか、リファレンスカウンティングですね

f:id:niwatako:20190321143842j:plain

WindowがRootViewControllerをもっていてRootViewControllerが次に出したい画面をもっていたりする。

リファレンスをもっている、それによってオブジェクトを生存させている。

リファレンスが消えると

f:id:niwatako:20190321143916j:plain

f:id:niwatako:20190321143922j:plain

消える

それによってまたリファレンスカウントがなくなったものがきえて

f:id:niwatako:20190321143931j:plain

きれいになる

さっきのコードを見てみましょう

f:id:niwatako:20190321143948j:plain

循環参照が出来てしまっってずっと残り続ける

さっきの図にこの参照を入れてみました

f:id:niwatako:20190321144013j:plain

f:id:niwatako:20190321144022j:plain

なのでWeakを入れましょう

f:id:niwatako:20190321144027j:plain

f:id:niwatako:20190321144045j:plain

コードレビューで防ぐ、コーディングルールにする、handlerでweakを必須とする、Lintルールを活用する、MemoryGraphDebuggerをつかって起きてしまったものに対処していくこともできる

でもすごく時間がかかって面倒

f:id:niwatako:20190321144136j:plain

ミラーとウィークの箱を使います

ミラーはSwiftにおけるRefrectingの機能。

画面を鏡に写すとニョロっと全プロパティが見えるように成る

f:id:niwatako:20190321144213j:plain

それらをweakリファレンスの箱にいれる

f:id:niwatako:20190321144219j:plain

それでもとがなくなれば

f:id:niwatako:20190321144226j:plain

全部消えるはず

出来上がったものがあります

f:id:niwatako:20190321144259j:plain

f:id:niwatako:20190321144317j:plain

XCTAssertNoLeakをimportします。

。。。デモは動かないというジンクスがありますが...早速Xcodeの補完がうまく働いていません。。

f:id:niwatako:20190321144423j:plain

なんでFailするの

f:id:niwatako:20190321144437j:plain

f:id:niwatako:20190321144612j:plain

こちらで確認してください、時間があればもう一度あとで。

f:id:niwatako:20190321144629j:plain

AssertNoLeak作りました。作るまでに乗り越えなければ行けない壁があって、Swiftのasの不思議な挙動に立ち向かう、というのがあります。

f:id:niwatako:20190321144707j:plain

weak box に値を詰める、値を詰めるためにclassを詰める必要がある。でもミラーで取得できるのはany。

AnyObjectにキャストすると、すべてのものがAnyObjectにキャストできるようになってしまった。

type(of:)を使うと

f:id:niwatako:20190321144817j:plain

isを使ってみる

f:id:niwatako:20190321144822j:plain

f:id:niwatako:20190321144837j:plain

引数に渡ってきたオブジェクトの型を確認する。

AnyObjectの型かと聞くと割と正直に答えてくれる。Structを渡すと、違います、となる

f:id:niwatako:20190321144914j:plain

次は、OptionalをUnwrapしたい

f:id:niwatako:20190321144934j:plain

Optionalはenum。Optionalの向う側にあるものがほしい。

なのでanyで渡ってきたargからOptionalの向う側にある値を取り出さなくてはいけない

キャストしてみた。全くうまくいかない

f:id:niwatako:20190321145017j:plain

Optionalが入っている

Optionalを取り巻くsubtypingルールは2つある

f:id:niwatako:20190321145041j:plain

中身の値にsubtyping関係があるときにString?はAny?にキャストできる

f:id:niwatako:20190321145117j:plain

これはコンパイラの気持ちがわかっていない

コンパイラの気持ちになると、実はString?からAny?へのキャストではなく、Anyから

f:id:niwatako:20190321145159j:plain

こういう動きをしているので、まだOptionalが入っているということに成る

f:id:niwatako:20190321145220j:plain

f:id:niwatako:20190321145225j:plain

Mirrorを使うとオプショナルであるということが分かる。

Optionalは一つしか値を持っていないので先頭を取り出せばいい

ダメなこともある

f:id:niwatako:20190321145257j:plain

IUOは今は滅びたが、昔のコードにはある、Optionalの中にIUOが入っている

OptionalKindというProtocolを作る

f:id:niwatako:20190321145338j:plain

これにキャストするとうまく言う

f:id:niwatako:20190321145405j:plain

XCTAssertNoLeakを作り、動かないデモをしました AnyObjectへのキャストとOptionalへのキャスト

サポートバージョン

f:id:niwatako:20190321145435j:plain

会社で作って運用していた。4.2にしたら全部動かないことが起きた。なぜか

f:id:niwatako:20190321145510j:plain

ミラーでweakなプロパティにアクセスするとretainされる

リファレンスカウントが1増えてしまう

ミラーを使ってメモリリークのテストをしようとしたら、関数を通すとメモリリークが起きる。

おあとがよろしいようで、ありがとうございました。