特定の条件で rootViewController を差し替えるとメモリリークする件

UIApplication.shared.keyWindow?.rootViewController で画面を差し替えたい時がありますよね?

例えばどんな時があるかとかと言いますと

  • 認証画面があり signup/signin 後に画面を切り替え
  • sigup/signin 後にチュートリアルの画面を表示
  • signout 後に認証画面を表示

などがあるかなと思います。

タイトルの通り、とある条件を満たして rootViewController を切り替えると差し替える前の viewController が解放されずに残り続けてしまいます。

これは iOS8 頃から認識していた問題ではありますが、iOS10 現在になっても修正されていません。

rdar://21404408: Memory leak in iOS 8+ after setting window.rootViewController while another view controller is presented

これのタチが悪いところは今使用されているメモリのほぼ全てがリークしてしまうといったことです。 今あなたのアプリがメモリを100MB使用しているとすると、それがそのままメモリリークしてしまうのでいきなり 100MB の負債を担うことになります。

これを解放するためにはアプリを App Switcher からアプリを kill するしかありません。

なんどもやってしまうと芋づる式にメモリリークしていっちゃいます。 かなり辛いですね。

どうすればメモリリークするのか。 それは 「モーダルが表示されている状態で rootViewController を差し替える」です。

この太字のとこおがかなり重要です。 つまりは モーダルが表示されている場合 モーダルを閉じてから rootViewController を差し替えればいいといった話ですね。

簡単に試せるようにサンプルプロジェクトを作りました。

github.com

dissmissの部分コメントアウトして試してもらえると、メモリリークすることが確認できます。

メモリリークするとき f:id:dealforest:20161203042740p:plain

メモリリークしないとき f:id:dealforest:20161203042721p:plain

signout したり画面を切り替える度にメモリリークしていて悩んでいる方は、一度確認してみるといいのではないでしょうか。

追記

そもそもwindow.rootViewControllerを差し替えるとうのがあまり良くないので、空のビューコントローラでいいのでそれをRootにして、それはずっと変わらないように保って、それ以下のコントローラを付け替えるようにするのがいいです。

コメントで教えてもらったのですが window を差し替えるよりも、この方がアニメーションもつけやすくていいですね。 勉強になりました。

AnyObject を JSON として出力する LLDB Plugin を作ったお話

Array または Dictionary のインスタンスJSON 形式で print する LLDB Plugin ほしい

と言われたので作ってみました。いざ作ってみるとこれがとても便利!!

github.com

README に install 方法が書いてあります。

注意点としては Swift モードの時の LLDB でしか意図した挙動はしません。 どうしても使いたい場合は、手元で Objective-C に書き直してもらえればと... LLDB Plugin の作り方は以前に記事を書いたので、そちらが参考になると思います。

dealforest.hatenablog.com

タイトルの通りインスタンスJSON に変換するということなので json コマンドにしました。 DataSource や API のレスポンスなど、特にデータ量が多いもの(でかい class や struct)を表示するときが幸せな気持ちになれます。

このような class, struct 定義があった場合

class User {
    var identifier = "dealforest"
    var loginCount = 10
    var height = 165.0
    var settings = [ "notification", "...", "..." ]
    var blog = Blog()
}

struct Blog {
    var title = "NANAIRO"
    var url = "http://dealforest.hatenablog.com/"
}

po で出力しようとすると class の場合アドレスしか表示されません。つらいですね。

(lldb) po User()
<User: 0x7faa5ad1b7b0>

(lldb) po Blog()
▿ Blog
  - title : "NANAIRO"
  - url : "http://dealforest.hatenablog.com/"

これが今回作った json コマンドの場合だとこうなります。

(lldb) json User()
{
  "height" : "165.0",
  "blog" : {
    "title" : "NANAIRO",
    "url" : "http:\/\/dealforest.hatenablog.com\/"
  },
  "identifier" : "dealforest",
  "settings" : [
    "notification",
    "...",
    "..."
  ],
  "loginCount" : "10"
}
/var/folders/63/jdd3tr3950s_0wvf5fk3_5xr0000gn/T/tmpyZbRpr.json

(lldb) json Blog()
{
  "title" : "NANAIRO",
  "url" : "http:\/\/dealforest.hatenablog.com\/"
}
/var/folders/63/jdd3tr3950s_0wvf5fk3_5xr0000gn/T/tmpQJRgo5.json

json コマンドを実行すると、JSON をファイルにも出力し、クリップボードにファイルの保存先のURLをコピーします。

なので jq を使えば簡単に JSON の結果をフィルタリングできます。 (jq は Homebrew を使って install できます)

$ cat /var/folders/63/jdd3tr3950s_0wvf5fk3_5xr0000gn/T/tmpyZbRpr.json | jq '.blog'
{
  "title": "NANAIRO",
  "url": "http://dealforest.hatenablog.com/"
}

class でも一応 p を使えば出力されはするのですが、データ量が多いときは出力される量も多くてつらかったのでなかなかに気に入っています。

ではでは。

iOSエンジニアのための LLDB Plugin 入門

f:id:dealforest:20160902102340p:plain

iOSDC」「AKIBA.swift×Swift愛好会」「iOSDC Reject Conference days2」で話させていただいたんですが、発表ではなるべく興味を持ってもらえるような内容になっていて、LLDB Plugin の作り方など詳細についてはふれていませんでした。

本来ならブログ書いてあるので見てください。と言いたかったんですが、なかなか手が回らず今になってしまいました。 できることも多いので、回数を分けて書いていければなと思います。

言語選択

LLDB の Plugin は Python で作られています。 ただし、Python が全くわからなくても作れているんで安心してください。

LLDB で実行する際に iOS エンジニアなら2つの言語から、どちらで実行するかを選択する必要があります。

正直どちらを選んでもいいと思っています。 どちらにせよ LLDB に寄り添わなければ意図した通りには動かないので。。。

以下に違いをまとめました。

Swift の場合

  • 実行する Xcode のバージョン (toolchain) に 依存してしまう
  • Objective-C の instance を扱うのが面倒くさい (頑張る必要がある)

Objective-C の場合

  • Swift の instance を扱おうとすると面倒くさい (頑張る必要がある)
  • Swift から使う場合でも頑張れば共通化して使うことができます
  • 関数の定義はできない

なので、自分のプロジェクトに合う、もしくは書きたい言語で書けばいいかなと思います。

facebook/chisel という facebook が作っている LLBD コマンド集があるのですが、これは Objectvie-C を採用しています。興味ある方は見てみると面白いかと思います。 ただし Swift から使う場合には少しクセがあるため慣れが必要となります。

chisel については「iOS開発のデバッグツールchiselの紹介」を見てもらえればどういうものか分かるかと思います。

テンプレート

これが一番シンプルなテンプレートです。 それぞれ Foundation 等 framework 内で完結するようなマクロでしたらこれで問題ありません。 例えば UIImage をファイルに出力したり、API レスポンスの NSData をデシリアライズして表示したりなどです。

#!/usr/bin/env python

import lldb

def process(debugger, command, result, internal_dict):
    lldb.debugger.HandleCommand("""
    expr -l swift --
    func $<functionName1>() {
        ...
    }
    """.strip())

    // 1命令1行の制約があるので、別の関数を定義しようと思うと2回実行する必要がある
    lldb.debugger.HandleCommand(""" 
    expr -l swift --
    func $<functionName2>() {
        ..
    }
    """.strip())

    lldb.debugger.HandleCommand('expr -l swift -- $<functionName>()')

def __lldb_init_module(debugger,internal_dict):
    debugger.HandleCommand("command script add -f <fileName>.process <commandName>")
    print"<commandName> command enabled."

Swift の場合は、処理を関数内に閉じ込めれば LLDB の事を意識せずに書けます。 なぜ 関数を定義して実行しているのかは、この資料を見てもらえればわかるかなと思います。

簡単にまとめますと

  • 1命令1行
  • 実行時に po をつける必要がある
  • 変数と関数の定義、参照をする場合に $ をつける必要がある(上書きできないという制約付き)

これらを回避するための工夫が関数にすることです。 なので Playground などで挙動を確認して最後に LLDB のコマンドにするといったフローで作るのがオススメです。

あと Optional は forced unwrapping するのでいいのじゃないかと思っています。 何かおかしい時に把握しやすいので僕はそうしてます。

#!/usr/bin/env python

import lldb

def process(debugger, command, result, internal_dict):
    lldb.debugger.HandleCommand("expr -l objc -O -- ...")

def __lldb_init_module(debugger,internal_dict):
    debugger.HandleCommand("command script add -f <fileName>.process <commandName>")
    print "<commandName> command enabled."

Objective-C の場合は LLDB で実行する時と同じことに注意しないといけません。 上で簡単にまとめた内容を全て意識する必要が出てきます。 LLDB で実行する命令のマクロみたいな認識がわかりやすいかもしれませんね。

いざコマンドを作る時に自分で決める必要があるものが3つあります。 (process と言う名前は変えても問題ありません)

  • fileName
  • commandName
  • functionName

実際に使う時に影響があるのは commandName だけなのでどういうコマンド名にしたいかさえ決めれば作れます。 悩むくらいなら全部同じ名前でもいいんじゃないですかね。

あとは、どういう挙動をさせたいかを書けば出来上がりです。

簡単なサンプル

まず初めに簡単に引数を出力する関数を作ってみましょう Xcode7.3.1 で試しています。

コマンド名以降が全て command という引数に渡ってくるので

Swift

#!/usr/bin/env python

import lldb

def process(debugger, command, result, internal_dict):
    lldb.debugger.HandleCommand("""
    expr -l swift --
    func $process(text: String) {
        print(text)
    }
    """.strip())
    lldb.debugger.HandleCommand('expr -l swift -- $process(' + command + ')')

def __lldb_init_module(debugger,internal_dict):
    debugger.HandleCommand("command script add -f echo.process echo")
    print "echo command enabled."

Objective-C

#!/usr/bin/env python

import lldb

def process(debugger, command, result, internal_dict):
    lldb.debugger.HandleCommand("expr -l objc -O -- NSLog(@" + command + ")")

def __lldb_init_module(debugger,internal_dict):
    debugger.HandleCommand("command script add -f echo.process echo")
    print "echo command enabled."

LLDB

(lldb) command script import <path_to>/echo.py
echo command enabled.
(lldb) echo "dealforest"
dealforest

毎回読み込むのが面倒くさい場合は ~/.lldbinit に書いておけば LLDB 起動時に読み込んでくれます。

command script import <path_to>/echo.py --allow-reload

--allow-reload つけておけばデバッグの時に楽なのでつけておくことをオススメします。

他にも、ちょっとしたコマンドを dealforest/dotfile に置いてあるので参考までにどうぞ。

追記: 別リポジトリにしました。 github.com

  • show_image.py - 画像を保存し Preview で表示する
  • slack.py - ファイルを slack へ送信
  • ambigurous_layout.py - ambigurous layout の場合、一定間隔でレイアウトが変更される

その他にもあるのですが、少しトリッキーな感じで作っているので、そのうち綺麗にして追加していこうかなと思っています。

次回は Python, LLDB, Swift を連携させるやり方を書こうかなと思います。 ではでは。

iOSDC でベストトーク賞(3位)をいただきました

f:id:dealforest:20160822153003p:plain

8/19-20 にかけて開催された 400人規模 600人規模のiOSカンファレンス「iOS Developers Conference Japan 2016」にて「Xcode で快適なデバッグライフを追い求める」というタイトルで発表させていただきました。

以下、CFPより

僕は怠惰な人間です。プログラミングの大半はデバッグに時間を費やすと思っているので、なるべく早く原因に辿りついたり効率のよいデバッグライフを送りたいと常々思っています。 プリントデバッグもいいのですが Xcode には便利な機能が色々とあります。それらを使うことで簡単に原因を特定できるケースがあります。 visualize されるのは分かりやすいですよね。 それらを tips で紹介できたらなと思います。

発表資料

皆さん怠惰ですよね…? -Xcodeで快適なデバッグライフを送りましょう- #iosdc #a - Togetterまとめ

動画

(近々配信される予定ですので、後ほど埋め込みます)

ベストトーク賞(3位)

f:id:dealforest:20160824223842p:plain 投票してくださった皆様のおかげでいただくことができました、ありがとうございます!!!

発表直前まで、この話しおもしろいのか...そもそも時間内に終わるのか...と不安だらけでしたが、うまくいったようで良かったです。Diagnostics の発音が分からなくなった辺りから会場の空気が良くなったので、忘れて本当に良かったなとw

こういった賞レースとは縁遠かったので貰えるとは思っておらず、後ろでバドワイザーを飲んでLTの余韻に浸ってぼーっとしてたんで、しばらく気づかなかったです。。。

反響も多く興味も持ってもらえたようなので本当に嬉しいかぎりです。

補足

発表自体かなり駆け足気味で話してしまったので、少し補足しておきます

Xcode Plugin が動かなくなる

Xcodeバイナリから証明書を消せば一部の Plugin は使えるようになります。みんな大好き XVim は動きます

すべての Plugin が動くわけではなく一部なので何が原因か特定できず GM が出たタイミングでもう一度調べようと思っています。

詳しくは「Xcode Editor Extension と Xcode8 で Plugin が動かなくなったことについて発表してきました」をどうぞ

どの ViewController かわからない問題

symbolic breakpoint を使えば、特定のセレクタが実行されたタイミングでブレイクできるので、わざわざここかな?と推測してログを仕込んだりしなくても済むというお話でした。

LOG_CURRENT_METHOD すでに仕込まれていれば便利ですけど、人が仕込むものなので、忘れられている可能性もあったりしますしね。 あとログが出すぎでうざったいとかも Breakpoint だと、該当する箇所だけ無効にするのも簡単です。

実機のログやファイルを取得したい問題

ここで特に触れていなかったので LLDB にslack コマンドがあると思ってしまったかもしれません。すみませんでした。

LLDB のコマンド自体に slack コマンドはなく拡張しました。 gist から DL して、~/.lldbinit から import しておくと、slack コマンドが使えるようになります。

このようにデバッグ用のコードを LLDB のコマンドに寄せると、どのプロジェクトでも使えるし #ifdef DEBUG のことも気にしなくていいようになるので、ライブラリにするより LLDB のコマンドで実装するようにしています。

おわりに

iOSDC はあの規模の人数を滞りなく終えていたので、とても初回のイベントとは思えませんでした。 さらに懇親会のビールの充実感がすごくて、COEDOの鞠花がビールサーバーであったり BREWDOG やよなよなエールがあったりなど、お酒好きの自分からすれば最高の懇親会でした。

iOSDCが最高だったので写真で紹介する #iosdc

良いカンファレンスは良いビールから。

これだ!!!と思い、これからこのフレーズは使っていきたいなと思いました。

本当にこのとおりで、実行委委員長の長谷川さんはじめ、スタッフの皆さん、本当にお疲れさまでした!!!! 最高な2日間を過ごせました、ありがとございます!!!

おしらせ

iOSDC では話せなかった、もっと LLDB にフォーカスを当てた内容を発表させていただく予定です。

定員は超えていますがおそらく当日キャンセルも出ると思うので、今申し込んでも参加できる可能性が高いかなと思います。

「Intrducing debug in WWDC2016」という内容で発表してきました

f:id:dealforest:20160701210529p:plain

WWDC.next」で発表させていただきました。

WWDCデバッグセッションは今すぐにでも実践できる tips が意外に多いので普段のデバッグの手助けになると思います。 ただしボリュームが結構でかいので心が折れないように進めていくのをオススメします。 (1日で全部見て気持ち悪くなりました...)

Xcode で快適なデバッグライフを追い求める | iOSDC Japan 201」にトークを応募したのでもしかしたら話せるかもしれません。

ではでは。

Tokyo Server-Side Swift Meetup で「swift build と Xcode での build の違い」について発表してきました

Tokyo Server-Side Swift Meetup #4」で発表させていただきました。

このイベントがきっかけで server side swift に初めてふれてみたのですが、久しぶりに web の開発をした気がしてとても楽しかったです。 Xcode で使うことを気にしなければ簡単に試せたのでイメージはすごい変わりました。

資料の中(P.43)に書いていたのですが SPM で dylib を Build するものに関しては、どうしても Xcode 側でうまく Build することができませんでした。

framework と dylib が絡む Xcode の設定がおかしいのかなと。 とはいえ clang-framework で compile しようとしているのに、なぜ -l しようとしてるのかもよくわかりません。

仕方ないので力技で swift build で作成した dylib を参照するようにして解決しました。 この辺りがなぜなのか分かる人がいれば教えてもらえるとありがたいです。

noppoMan/Slimane は現時点で気軽に試せるのでオススメです。(swift build で使う分には。)

ではでは

Xcode Editor Extension と Xcode8 で Plugin が動かなくなったことについて発表してきました

FiNC WWDC振り返り勉強会」と「potatotips #30 (iOS/Android開発Tips共有会)」で発表させていただきました。

内容としては Xcode Editor Extension についてと、既存の Xcode Plugin が Xcode8 では動かなくなるのを回避する方法があるよ。 という内容を話しました。

ngs さんと同じ内容を話していたので該当箇所は結構削ったので Xcode Editor Extension について、そちらの資料を見るのがいいかなと思います

ja.ngs.io

Xcode の非証明バイナリを作る方法を gist にスクリプトでまとめておきましたので自己責任でお願いします。

非証明バイナリにするというのはどうなのかといった話もあると思いますが、それでも Plugin を使いたい人もいると思うのでそういう人にはいいんじゃないでしょうか。 個人的には DL してきて正しい証明されてるものと確認した後に抜くのでいいんじゃないかなと思ってます。

ただ非証明 Xcode でも動かない Plugin もあって、それの原因がよくわかってません。 もうすこし調べれば分かるかなと思ってます。

Xcode Source Editor Extension はいい仕組みだと思うので、しばらくの間の移行期はこれで耐え忍ぶしかないかなと個人的には思っている次第です。

github.com

このようにフィードバックを送って API を増やしていってもらえれば、1年もすればすんなり移行できてたりするかもしれませんね。 まぁ Plugin をつかわないように調教するのもひとつの手段だとは思いますが。