Xcodeのブレークポイントを通知センターに表示する

Xcodeのブレークポイントで音を鳴らす」で耳デバッグを紹介されていますが、音を鳴らせない状況ではイヤホンをMacにつなぐと言われているように毎回気軽にデバッグをできないのが難点ではあります。

そこで最近、便利だなと思っているのが通知センターを使う方法です。

Xcodeから起動した際、該当箇所が実行されたかどうかを確認したいというニーズは満たしてくれます。

f:id:dealforest:20180112003713p:plain

通知センターがゴミ通知で溢れるという懸念もありますが、簡単に消せるのであまり気にしていません。

やり方に触れる前に通知を送信する方法に触れておきます。 Mavericks以降では Apple Script から送信することができます。

$ osascript -e 'display notification "[メッセージ]"'

簡単ですね。

それでは実際にXcode経由で通知を送信してみましょう。 通知を送信するにはいくつかの方法があります。

ブレークポイントアクションでApple Scriptを実行

先ほどの通知を送信するコマンドをそのままブレークポイントアクションに設定することで送信することができます。

f:id:dealforest:20180112004122p:plain

デメリットとしては display notification '[メッセージ]' を毎回タイピングしないといけません。 また複数箇所にブレークポイントを設定した場合、どこを通過したのか判別するためメッセージを都度変更する必要もあります。

LLDBコマンドを定義

先ほど述べたデメリットも辛いですが、ブレークポイントの設定箇所をメッセージに入れたくなるのが心情ではないでしょうか。

さらに、めんどくさいことが多いと使わなくなってしまうのは人の性です。 もっと手軽に実行できるようにするためLLDBコマンドにしましょう。

f:id:dealforest:20180112005208p:plain

ブレークポイントアクションと比べると簡単に送信できていますね。

ワンライナーで定義

~/.lldbinit に以下のコマンドを書いておけば d と実行すれば、ブレークポイントを設定しているファイル名と行数をテキストに入れて通知を送信します。

~/.lldbinit

command regex d "s/^[[:space:]]*$/script text='%s L:%d' % (lldb.frame.line_entry.file.basename, lldb.frame.line_entry.line); os.system(\"osascript -e 'display notification \\\"%s\\\"'\" % (text))/"

LLDBコマンドで定義

これくらいの処理でしたらワンライナーで定義しておいたほうが簡単でオススメですが、普段からLLDBコマンドを管理している人のためにLLDBコマンドにもしておきました。

pusher.py

#!/usr/bin/env python

import lldb
import commands
import os

def process(debugger, command, result, internal_dict):
    frame = lldb.debugger.GetSelectedTarget().GetProcess().GetSelectedThread().GetSelectedFrame()
    line_entry = frame.line_entry
    file_name = line_entry.file.basename
    line_number = line_entry.line
    text = '%s L:%d' % (file_name, line_number)
    cmd = 'display notification "%s"' % (text)
    os.system("osascript -e '%s'" % cmd)

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

~/.lldbinit

command script import path/to/pusher.py
command alias d pusher

これで AirPods を買わなくても済むのかもしれませんね

iOS 11からUIDebuggingInformationOverlayを使えるようにする

iOS 10から UIKit に UIDebuggingInformationOverlay が追加されました。 記憶にも新しいことでしょう。 これを使うと次のようなデバッグ用の画面を表示することができます。

f:id:dealforest:20171213194316p:plain

private APIではありますが最低限のデバッグできる機能が提供されていました。 どういうことができるかは「UIDebuggingInformationOverlay - Low Level」が参考になります。

これが残念ながら iOS 11 からは利用できなくなっていました。

iOS 10での表示方法

private APIなのでビルドすると動的に呼び出す必要がありますが、 今回は本質的な箇所を見てもらいたいのでLLDBで実行しています。

// Objective-C
(lldb) po [UIDebuggingInformationOverlay prepareDebuggingOverlay]
(lldb) po [[UIDebuggingInformationOverlay overlay] toggleVisibility]

この2行で表示することができます。

こういう場合、動的に実行できるObjective-Cは便利ですね。 Swift で書くと次のようになります。見辛いですね。。。

// Swift
(lldb) po ((NSClassFromString("UIDebuggingInformationOverlay") as! NSObject.Type).perform(Selector("prepareDebuggingOverlay"))
(lldb) po ((NSClassFromString("UIDebuggingInformationOverlay") as! NSObject.Type).perform(Selector("overlay")).takeUnretainedValue() as! UIWindow).perform(NSSelectorFromString("toggleVisibility"))

iOS 11からの表示方法

さて、これからが本題です。 残念ながら iOS 11 からは利用できなくなりました。

// iOS 11
(lldb) po [UIDebuggingInformationOverlay prepareDebuggingOverlay]
(lldb) po [UIDebuggingInformationOverlay overlay]
nil
(lldb) po [UIDebuggingInformationOverlay new]
nil

iOS 11では UIDebuggingInformationOverlayインスタンス生成ができなくなっています。

しかし、世の中にはそれでも諦めずにどうにかしてしまう人がいます。 「Swizzling in iOS 11 with UIDebuggingInformationOverlay」で紹介されています。 下の方に実際に動作するサンプルコードのDLリンクがあります。

Method Swizzlingを使って動作するようにハックをしていますが、メモリ上で処理を書き換えています。 ただそれを調査するプロセスがとにかく凄いので必見です!!

NSObject+UIDebuggingInformationOverlayInjector.mをプロジェクトに同梱した上で、次のコマンドを実行することで表示されます。

(lldb) po @import UIKit
(lldb) po [UIDebuggingInformationOverlay prepareDebuggingOverlay]
(lldb) po UITapGestureRecognizer *$gesture = [UITapGestureRecognizer new]
(lldb) po [$gesture setValue:@(UIGestureRecognizerStateEnded) forKey:@"state"]
(lldb) po [[UIDebuggingInformationOverlayInvokeGestureHandler mainHandler] 

LLDB コマンド

毎回打つのはだるいのでLLDBコマンドにしてみましょう。

/path/to/debug_overlay.py

#!/usr/bin/env python

import lldb

def process(debugger, command, result, internal_dict):
    lldb.debugger.HandleCommand('expr -lobjc -O -- @import UIKit')
    lldb.debugger.HandleCommand('expr -lobjc -O -- [UIDebuggingInformationOverlay prepareDebuggingOverlay]')
    lldb.debugger.HandleCommand('expr -lobjc -O -- UITapGestureRecognizer *$gesture = [UITapGestureRecognizer new]')
    lldb.debugger.HandleCommand('expr -lobjc -O -- [$gesture setValue:@(UIGestureRecognizerStateEnded) forKey:@"state"]')
    lldb.debugger.HandleCommand('expr -lobjc -O -- [[UIDebuggingInformationOverlayInvokeGestureHandler mainHandler] _handleActivationGesture: $gesture]')

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

~/.lldbinit

...

command script import /path/to/debug_overlay.py

これで (lldb) debug_overlay とすると表示されるようになりました。 とはいえ NSObject+UIDebuggingInformationOverlayInjector.m が必要であったり、iOS 10 で動作するようにするためには分岐が必要になったりと微妙な点は多々あります。

まとめ

現時点ではここまでして使うかどうかは微妙なところがありますね。。。 いつの日か解放される日が来ることを心待ちにするのがいいかもしれません。

自分でも色々と試していたのですが動作させれずでしたが、ハックして使えるようにするまでのプロセスに感動したものがあったので紹介しました。

Enjoy バイナリハック

WEB+DB PRESS Vol.101にiOSの特集記事を寄稿しました

f:id:dealforest:20171023234554p:plain

タイトルのとおり本日発売のWEB+DB PRESS Vol.101に寄稿しました。 WEB+DB PRESS はエンジニアになりたての頃から読んでいた雑誌なので、 実際に自分の名前が載っているのを見た時に、なんとも言えない気持ちになり感動めいたものがありました。

今回の特集記事はサーバーサイドSwiftで有名な武井くん(@noppoMan722と共著しました。 僕はiOS 11とXcode、武井くんはSwift 4とサーバーサイドSwiftについてそれぞれ執筆しました。

特集について

特集の構成

  • 第1章 iOS 11の新機能
  • 第2章 iOS 11の新しいフレームワーク
  • 第3章 Swift 4の新機能
  • 第4章 サーバーサイドSwift入門
  • 第5章 Xcode 9の新機能

特集の概要

2017年9月20日にiOS 11がリリースされ、Swift 4、Xcode 9が正式に利用できるようになりました。 特集では、それぞれの注目すべき機能などを具体例や実用例を交えながら紹介しています。

中でもサーバサイドSwiftを扱っているのが他ではないかなと思います。 現時点で日本語の本でまとまっているのは初めてではないでしょうか。

実際にサーバーサイドSwiftをプロダクトで運用した上で書かれているので必見です!! まだまだアーリーな状況ではありますが、実践的な内容となっています。

tokyo-ss-swift.connpass.com

不定期ではありますが Tokyo Server Side Meetup を開催しているので、 興味がわいた方は参加してみてもいいかもしれません。

執筆を終えて

iOS 11の特集ということもありiOSエンジニアやそうでない方も、興味をもって読んでもらえるように執筆しました。 楽しんでもらえるかなと思うので、ぜひ見かけた際は手にとって読んでみてください。

執筆時の話を少しすると、iOS 11がリリースされてから脱稿までが10日ほどしかありませんでした。 なので beta のはやいタイミングから執筆に取りかかっていました。

そのため新しく beta がリリースされるとアップデート内容と原稿との相違を確認する作業が毎回発生していました。

この作業が心身ともにかなりつらく、ギリギリまで内容を調整することとなりました。

今回はなんと過去最多のbeta 10まで更新され、リリース間近には怒涛のように更新されていました。

これも今となってしまえば、いい思い出です。

さいごに

編集を担当していただいた技術評論社の稲尾さんをはじめ、 監修頂いたみなさま(@shu223, @myb, @_novi)、 共著者の武井くん、今回紹介してくださった石川くん(@_ishkawa)など各方面の関係者の方々にも様々な面で協力してもらい助けて頂きました。 ありがとうございました!!

最後になりますが今年のはじめに出版された「モバイルアプリ開発エキスパート養成読本」でもXcodeとテストについて執筆させていただいので、こちらもよろしければどうぞ。

モバイルアプリ開発エキスパート養成読本 (Software Design plus)

モバイルアプリ開発エキスパート養成読本 (Software Design plus)

iOSDC 2017でLTベストトーク賞をいただきました

iOSDC Japan 2017にてiOSDC Japan 2016 の賞金を放置しておくと1年でどうなったか?!というタイトルでLTをさせていただきました。 めでたく今回もベストトーク賞をいただき投票していただいた方はありがとうございます!!

今回のLTのネタ元となった前回の発表「iOSDC でベストトーク賞(3位)をいただきました」です。

もらっておいてあれですが、ただのネタでいただくのは相当忍びなかったです。。。

f:id:dealforest:20170922202509p:plain

いただいた商品(Qi受電器)は早速 iPhone 8 の充電に重宝しています。

タイトルから見てわかる通り出落ち感しかないタイトルで採択されたため、正直後悔がすごかったです。 資料はネタで公開するのは微妙なので、当日のタイムラインを見てもらえるとどういったものかがわかるかなと思います。

全てはこのツイートから始まりました。

さらに続く悪ノリ…

確かにこのままでは、せっかくミッケラーに行くのに美味しく飲めません。 というわけで、ちゃちゃっと書いたCfPがこれでしたが、めでたく採択されて話すこととなりました。

最初はなんとかなるかなと思っていましたが、いざ資料を作り出すと全くもって面白い話になりませんでした。 技術の話なら面白くなくてもいざ知らず、ネタのLTで面白くないのは話す方も聞く方も苦痛でしかないので。。。

当日までに資料はほぼ出来上がっていましたが、なかなかオチが決められるず迷走していました。 他のセッションも聞けずに発表のクオリティを上げていました。

そんな中、野生の@jpmartha_jpに絡まれ、余計にプレッシャーをかけれる始末。

大トリということもあり前回の大トリを勤めた@yimajoがブース出展されていたので、そこで色々と話を聞かせてもらいました。

特に心に響いたのが、LTをする際に逃げて酒を飲むのがよくないということでした。 今後同じようなプレッシャーに遭遇した時、乗り越えられずお酒に逃げてしまうことの繰り返しになります。

去年は飲まれずに立ち向かったそうです。 若干プレッシャーで吐きそうだったのですが、さらに追い討ちをかけられたと感じました。 しかし、冷静に考えれば通常セッションでは酒を飲まずに話してるんだから、ネタLTもできるだろうと気持ちを切り替えることができ、結果的にとても助かりました。

そして、今回のLTは酒を飲まずにやりましたが、やはり終わった後の一杯は格別の味がした気がします。 来年はゆっくりと他の人の発表が見たいものですね。

発表でたかられたを連呼していたので、今年は奢ってもらえました!ごちそうさまです!!!!

その後、スタッフ打ち上げしているところを通りがかり合流して、そのまま夜のiOSDCに参加したりなどとても楽しかったです。

2年連続スピーカーをさせていただきましたが、控えめに言っても最高でした!

実行委委員長の長谷川さんはじめ、スタッフの皆さん、本当にお疲れさまでした!!!!

特定の条件で 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 を連携させるやり方を書こうかなと思います。 ではでは。