Metal で算術演算(MTLComputeCommandEncoder) を行った際のデバッグ方法
Metal といえばテクスチャなど GPU を使ったレンダリングの処理をおこなうものと思う方が大半かと思います。 そのような利用の時は「Metalのデバッグまとめ(随時更新)」に書かれているようなデバッグ方法でことたります。
今回は算術演算で GPU を使いたくて MTLComputeCommandEncoder
をつかったのですが、Instruments で表示しても上記のような情報がなくデバッグの方法が全くわからなかったので自分なりに試して、ある程度目処がたちましたので、その方法のメモとなります。
また、このエントリーでは特に Metal についてはふれませんので、さっと学びたい方は「iOSDC2017で「飛び道具ではないMetal」という話をしました #iOSDC 」を読んでもらえれば大枠は理解できるかと思います。
サンプルコードと問題
それでは早速 noppoMan/Shaders.metal のコードで試していきましょう。
Shaders.metal
の kernel は Metal で実装されたシグモイド関数です。何か計算しているんだなレベルで問題ありません。
これをプロジェクトに追加して試したときにデバッグできなくて困りました。 簡単に紹介するとkernel 内で breakpoint がはれない、print しようとしてもそんな関数がない、stdio を import しようとするとそんなものはない、となりどのようにデバッグすればいいんだと発狂しそうになりました。
これについてはある程度、挙動が確認できた状態のものを kernel に移植すれば、まだある程度動作は担保されます。
しかし実行時のスレッドの情報(thread_position_in_grid
など)は確認することができないのは困ります。
見かけたデバッグ
「【Swift Metal】thread_position_in_grid等の属性について解説」のエントリーを見て out buffer につめれば、実行時のスレッドの情報(thread_position_in_threadgroup
, thread_position_in_grid
など)を取得できるのでと思い試してしたが、取得できはするのですがデバック時に毎回これをやるのは正直億劫です。
MTLCaptureManager を使用
そんな中「Capturing GPU Command Data Programmatically」というドキュメントにたどりつきましたが、どうせ MTLComputeCommandEncoder
だとまともにキャプチャされないんだろうなと思いつつ試してみましたが、なんとこれで確認することができました!!
使い方は簡単で MTLCaptureManager.shared().startCapture(with: )
を実行するだけです。
sigmoid_on_gpu.swift
に反映すると下記のようになります。
func sigmoid_on_gpu(_ input: [Float]) throws -> [Float] { var input = input + let captureDescriptor = MTLCaptureDescriptor() + captureDescriptor.captureObject = device + try! MTLCaptureManager.shared().startCapture(with: captureDescriptor) ...(省略) + MTLCaptureManager.shared().stopCapture() return output }
実行すると gputrace が開かれ情報を見ることができます。
上記のような画面が表示され Buffer
をダブルクリックすると kernel への in, out を確認することができます。便利ですね。
あとは左側のナビゲーションエリアにある ComputeCommandEncoder
を選択するとプレビューが表示されサマリーが表示されます。
また、このプレビューからも out を表示することができます。
そして肝心の困っていた実行スレッド情報も取得することができました、これが一番でかい!
このてんとう虫のようなアイコンを選択すると、該当するスレッドの情報を確認できます。
スレッドの情報を指定すれば該当するスレッドの情報を Playground のように右側に値が表示されます。
他にもスレッドの情報を確認したいようでしたら追加して再実行するだけでいいので、これでしたら簡単にデバッグできそうですね。 適当にぽちぽちしていると他にも便利そうなのものありましたので、適当にさわってみるものもいいかもしれません。
自作したモデルを iOS で使う際に input の型がどのような影響を与えるのか調べた
1桁の手書きの数字(0〜9の数字に対応)を分類するモデルを Vision, CoreML から利用してみたいと思います。
Apple が提供しているモデルもありますが、今回は自作したモデルを使っていきます。
input が画像のため今回のモデルでは input の型が Image
, MLMultiArray
で作成することができます。
それらを Vision, CoreML から利用するのにどれくらい変わってくるのかを調べてみました。
mlmodel を用意
keras のサンプルをおこないモデルを作成しました。 今回利用するモデルは手書き数字の画像をもとに推論を行い0-9のどの数字かを判断するものです。
keras でモデルを作成したままでは CoreML から使えないため coremltools で変換します。
import coremltools mlmodel = coremltools.converters.keras.convert( model, # 自作したモデル input_names=['image'], output_names=['symbol'], class_labels=['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] ) mlmodel.save('Mnist.mlmodel')
さらにこのままでは input の型が MLMuitiArray
で扱いずらいので Image
に変換したモデルも用意します。
import coremltools import coremltools.proto.FeatureTypes_pb2 as ft spec = coremltools.utils.load_spec("Mnist.mlmodel") input = spec.description.input[0] input.type.imageType.width = 28 input.type.imageType.height = 28 input.type.imageType.colorSpace = ft.ImageFeatureType.GrayScale coremltools.utils.save_spec(spec, "MnistImageType.mlmodel")
モデルの生成や coremltools につきましては別記事でおいおいまとめます。 気になるかたは「Core MLモデルの入力の型をMLMultiArrayから画像(CVPixelBuffer)に変更する」をどうぞ。
mlmodel を使って推論を実行するパターン
- input の型が
Image
(MnistImageType.mlmodel) の場合に- Vision.framework を使って推論を行う
- CoreML.framework を使って推論を行う
- input の型が
MLMultiArray
(Mnist.mlmodel) の場合に- Vision.framework を使って推論を行う
- CoreML.framework を使って推論を行う
(これから先、冗長なため framework という単語は省略します)
mlmodel を Xcode プロジェクトに追加することでクラスが自動生成され利用しやすいようになっています。自動生成されたクラスはこちら。 多くの場合このクラスを利用するかと思います。 便利ではありますが残念ながら Swift Playgrounds では生成されませんので Playgrounds から利用する際は注意が必要ですね。
自動生成される内容をみていくと input, output の型定義、input, output の型を用いて分類を実行できるようにしているだけです。
最低限利用する際には input だけ MLFeatureProvider
に準拠して作成すると利用できます。
作成するのがめんどくさい場合は、一度 Xcode プロジェクトに追加し自動生成されたクラスを持ってくればいいかなと思います。
今回は最低限の推論が実行できているところを確認できればいいので、自動生成されたクラスは利用せず実行していきましょう。
input の型が Image
の場合に Vision を使う
下記のコードは Vision を利用した際に、よく見ますね。
mlmodel の output の定義によって request.results
の型はかわってきます。
import UIKit import Vision let image = UIImage(named: "4.png")! let modelURL = Bundle.main.url(forResource: "MnistImageType", withExtension: "mlmodelc")! let model = try! VNCoreMLModel(for: MLModel(contentsOf: modelURL)) let handler = VNImageRequestHandler(cgImage: image.cgImage!, options: [:]) let request = VNCoreMLRequest(model: model, completionHandler: { (request, error) in guard let results = request.results as? [VNClassificationObservation] else { fatalError() } if let result = results.first { print("☀️ idntifier: \(result.identifier) (\(result.confidence))") } else { print("🌧not found") } }) try! handler.perform([request]) /* [predict result] ☀️ idntifier: 4 (1.0) [verbose] identifier: 4 (1.0) identifier: 0 (1e-45) identifier: 1 (1e-45) identifier: 2 (1e-45) identifier: 3 (1e-45) identifier: 5 (1e-45) identifier: 6 (1e-45) identifier: 7 (1e-45) identifier: 8 (0.0) identifier: 9 (0.0) */
ここで見て分かるように Vision での推論は非同期処理でおこなわれます。
また Vision でも自動生成したクラスを利用することはできます。この場合 output の型推論がきくようになります。
let model = try! VNCoreMLModel(for: MnistImageType().model)
input の型が Image
の場合に CoreML を使う
input の型が Image
の場合は CVPixelBuffer
をわたします。
Vision を利用した際はチャンネル数や画像サイズの調整はよしなに行われますが、CoreML を利用した際は自分で行う必要があります。
その際に便利なのが CoreMLHelpers です。
今回は UIImage.pixelBufferGray(width:height:)
を利用し CVPixelBuffer
に変換します。
import UIKit import CoreML // input の型を定義 class MnistImageTyepInput: MLFeatureProvider { var image: CVPixelBuffer var featureNames: Set<String> = ["image"] // mlmodel の input とずれているとエラーになる func featureValue(for featureName: String) -> MLFeatureValue? { if (featureName == "image") { return MLFeatureValue(pixelBuffer: image) } return nil } init(image: CVPixelBuffer) { self.image = image } } let image = UIImage(named: "4.png")! let pixelBuffer = image.pixelBufferGray(width: 28, height: 28)! let modelURL = Bundle.main.url(forResource: "MnistImageType", withExtension: "mlmodelc")! let model = try! MLModel(contentsOf: modelURL) let input = MnistImageTyepInput(image: pixelBuffer) let output = try! model.prediction(from: input) print(output.featureValue(for: "symbol")) print(output.featureValue(for: "classLabel")) /* [symbol] Dictionary : { 0 = "1.401298464324817e-45"; 1 = "1.401298464324817e-45"; 2 = "1.401298464324817e-45"; 3 = "1.401298464324817e-45"; 4 = 1; 5 = "1.401298464324817e-45"; 6 = "1.401298464324817e-45"; 7 = "1.401298464324817e-45"; 8 = 0; 9 = 0; } [classLabel] String : 4 */
ここで見て分かるように CoreML での推論は同期処理でおこなわれます。
input の型が MLMultiArray
の場合に Vision を使う
残念ながらこのパターンは実行することができません。
Vision は input の型が Image
のときしか利用できません。
ただしコンパイルは通ってしまい下記のような実行時エラーになります。
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0) * frame #0: 0x000000010a38460d libswiftCore.dylib`function signature specialization <Arg[0] = Exploded, Arg[1] = Exploded> of Swift._assertionFailure(_: Swift.StaticString, _: Swift.String, file: Swift.StaticString, line: Swift.UInt, flags: Swift.UInt32) -> Swift.Never + 509 frame #1: 0x000000010a1bc2da libswiftCore.dylib`swift_unexpectedError + 314 frame #2: 0x000000010cabca07 $__lldb_expr4`main at Vision.xcplaygroundpage:7:18 frame #3: 0x000000010a09e560 predictNotUseAutoGenerateModel`linkResources + 304 frame #4: 0x00007fff23bd429c CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12 frame #5: 0x00007fff23bd3a08 CoreFoundation`__CFRunLoopDoBlocks + 312 frame #6: 0x00007fff23bce894 CoreFoundation`__CFRunLoopRun + 1284 frame #7: 0x00007fff23bce066 CoreFoundation`CFRunLoopRunSpecific + 438 frame #8: 0x00007fff384c0bb0 GraphicsServices`GSEventRunModal + 65 frame #9: 0x00007fff48092d4d UIKitCore`UIApplicationMain + 1621 frame #10: 0x000000010a09e62d predictNotUseAutoGenerateModel`main + 205 frame #11: 0x00007fff5227ec25 libdyld.dylib`start + 1
input の型が MLMultiArray
の場合に CoreML を使う
⚠️このパターンの利用方法は推奨しませんが、推論を行うと大変なことを示したかったので実装しました
Image
のときと同様に CoreMLHelpers を利用して MLMultiArray
を作成したいところですが、そのような機能はありません。
理由は明確で MLMultiArray
に変換するよりも input の型を Image
に変換した mlmodel を作成した方がいいためです。
「How to convert images to MLMultiArray」に詳細が書かれていますので興味がある方はご覧ください。
そのため他に比べてコードがどうしても長くなってしまっています。
import UIKit import CoreML // input の型を定義 class MnistInput : MLFeatureProvider { var image: MLMultiArray var featureNames: Set<String> = ["image"] func featureValue(for featureName: String) -> MLFeatureValue? { if (featureName == "image") { return MLFeatureValue(multiArray: image) } return nil } init(image: MLMultiArray) { self.image = image } } extension UIImage { func pixelDataForGrayscale() -> [UInt8]? { guard let cgImage = self.cgImage else { return nil } let size = self.size let dataSize = size.width * size.height var pixelData = [UInt8](repeating: 0, count: Int(dataSize)) let context = CGContext( data: &pixelData, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: Int(size.width), space: CGColorSpaceCreateDeviceGray(), bitmapInfo: CGImageAlphaInfo.none.rawValue) context?.draw(cgImage, in: CGRect(origin: CGPoint(x: 0, y: 0), size: size)) return pixelData } } func preprocess(image: UIImage) -> MLMultiArray? { guard let pixels = image.pixelDataForGrayscale() else { return nil } // [channel, width, height] guard let array = try? MLMultiArray(shape: [1, 28, 28], dataType: .double) else { return nil } for (index, element) in pixels.enumerated() { let value = Double(element) / 255.0 array[index] = NSNumber(value: value) } return array } let image = UIImage(named: "samples/4.png")! let modelURL = Bundle.main.url(forResource: "Mnist", withExtension: "mlmodelc")! let model = try! MLModel(contentsOf: modelURL) let multiArray = preprocess(image: image)! let input = MnistInput(image: multiArray) let output = try! model.prediction(from: input) print(output.featureValue(for: "symbol") ?? "none") print(output.featureValue(for: "classLabel") ?? "none") /* [symbol] Dictionary : { 0 = "0.006920351181179285"; 1 = "0.0009511407115496695"; 2 = "0.005366008728742599"; 3 = "8.467519364785403e-06"; 4 = "0.9653815627098083"; 5 = "0.003310209838673472"; 6 = "0.00117537344340235"; 7 = "0.004033106379210949"; 8 = "0.0103130042552948"; 9 = "0.002540647285059094"; } [classLabel] String : 4 */
まとめ
Vision | CoreML | |
---|---|---|
Image |
UIImage のまま利用可能画像情報は Vision が調整 |
UIImage から CVPixelBuffer に変換。画像情報は自分で調整 |
MLMultiArray |
利用不可 | 非推奨 |
こうやってみると input で画像を扱う場合は Vision か CoreML かは関係なく coremltools
で型を Image
にしておくべきですね。
今回は入力が画像でしたためどうしても MLMultiArray
の心理的な壁を感じたかもしれません。
入力が文字や数字のモデルを扱う場合ですとそうでもありませんので、また別途まとめれればなと思います。
また画像を推論するにあたり簡単に試せる方がいいので playground を作成しました。 live view で入力した数字を、そのまま推論できたりしますので興味ある方はみてもらえればと思います。 また本記事の中のコードもこちらにあります。
Xcodeのブレークポイントを通知センターに表示する
「Xcodeのブレークポイントで音を鳴らす」で耳デバッグを紹介されていますが、音を鳴らせない状況ではイヤホンをMacにつなぐと言われているように毎回気軽にデバッグをできないのが難点ではあります。
そこで最近、便利だなと思っているのが通知センターを使う方法です。
Xcodeから起動した際、該当箇所が実行されたかどうかを確認したいというニーズは満たしてくれます。
通知センターがゴミ通知で溢れるという懸念もありますが、簡単に消せるのであまり気にしていません。
やり方に触れる前に通知を送信する方法に触れておきます。 Mavericks以降では Apple Script から送信することができます。
$ osascript -e 'display notification "[メッセージ]"'
簡単ですね。
それでは実際にXcode経由で通知を送信してみましょう。 通知を送信するにはいくつかの方法があります。
ブレークポイントアクションでApple Scriptを実行
先ほどの通知を送信するコマンドをそのままブレークポイントアクションに設定することで送信することができます。
デメリットとしては display notification '[メッセージ]' を毎回タイピングしないといけません。 また複数箇所にブレークポイントを設定した場合、どこを通過したのか判別するためメッセージを都度変更する必要もあります。
LLDBコマンドを定義
先ほど述べたデメリットも辛いですが、ブレークポイントの設定箇所をメッセージに入れたくなるのが心情ではないでしょうか。
さらに、めんどくさいことが多いと使わなくなってしまうのは人の性です。 もっと手軽に実行できるようにするためLLDBコマンドにしましょう。
ブレークポイントアクションと比べると簡単に送信できていますね。
ワンライナーで定義
~/.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
が追加されました。
記憶にも新しいことでしょう。
これを使うと次のようなデバッグ用の画面を表示することができます。
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の特集記事を寄稿しました
タイトルのとおり本日発売のWEB+DB PRESS Vol.101に寄稿しました。 WEB+DB PRESS はエンジニアになりたての頃から読んでいた雑誌なので、 実際に自分の名前が載っているのを見た時に、なんとも言えない気持ちになり感動めいたものがありました。
今回の特集記事はサーバーサイドSwiftで有名な武井くん(@noppoMan722と共著しました。 僕はiOS 11とXcode、武井くんはSwift 4とサーバーサイドSwiftについてそれぞれ執筆しました。
特集について
特集の構成
特集の概要
2017年9月20日にiOS 11がリリースされ、Swift 4、Xcode 9が正式に利用できるようになりました。 特集では、それぞれの注目すべき機能などを具体例や実用例を交えながら紹介しています。
中でもサーバサイドSwiftを扱っているのが他ではないかなと思います。 現時点で日本語の本でまとまっているのは初めてではないでしょうか。
実際にサーバーサイドSwiftをプロダクトで運用した上で書かれているので必見です!! まだまだアーリーな状況ではありますが、実践的な内容となっています。
不定期ではありますが Tokyo Server Side Meetup を開催しているので、 興味がわいた方は参加してみてもいいかもしれません。
執筆を終えて
iOS 11の特集ということもありiOSエンジニアやそうでない方も、興味をもって読んでもらえるように執筆しました。 楽しんでもらえるかなと思うので、ぜひ見かけた際は手にとって読んでみてください。
執筆時の話を少しすると、iOS 11がリリースされてから脱稿までが10日ほどしかありませんでした。 なので beta のはやいタイミングから執筆に取りかかっていました。
そのため新しく beta がリリースされるとアップデート内容と原稿との相違を確認する作業が毎回発生していました。
この作業が心身ともにかなりつらく、ギリギリまで内容を調整することとなりました。
今回はなんと過去最多のbeta 10まで更新され、リリース間近には怒涛のように更新されていました。
これも今となってしまえば、いい思い出です。
さいごに
編集を担当していただいた技術評論社の稲尾さんをはじめ、 監修頂いたみなさま(@shu223, @myb, @_novi)、 共著者の武井くん、今回紹介してくださった石川くん(@_ishkawa)など各方面の関係者の方々にも様々な面で協力してもらい助けて頂きました。 ありがとうございました!!
最後になりますが今年のはじめに出版された「モバイルアプリ開発エキスパート養成読本」でもXcodeとテストについて執筆させていただいので、こちらもよろしければどうぞ。
モバイルアプリ開発エキスパート養成読本 (Software Design plus)
- 作者: 山戸茂樹,坂田晃一,黒川洋,藤田琢磨,山田航,田坂和暢,熊谷知子,森本利博,坂本和大,小形昌樹,鈴木大貴,志甫侑紀
- 出版社/メーカー: 技術評論社
- 発売日: 2017/04/11
- メディア: 大型本
- この商品を含むブログを見る
iOSDC 2017でLTベストトーク賞をいただきました
iOSDC Japan 2017にてiOSDC Japan 2016 の賞金を放置しておくと1年でどうなったか?!というタイトルでLTをさせていただきました。 めでたく今回もベストトーク賞をいただき投票していただいた方はありがとうございます!!
今回のLTのネタ元となった前回の発表「iOSDC でベストトーク賞(3位)をいただきました」です。
もらっておいてあれですが、ただのネタでいただくのは相当忍びなかったです。。。
いただいた商品(Qi受電器)は早速 iPhone 8 の充電に重宝しています。
タイトルから見てわかる通り出落ち感しかないタイトルで採択されたため、正直後悔がすごかったです。 資料はネタで公開するのは微妙なので、当日のタイムラインを見てもらえるとどういったものかがわかるかなと思います。
全てはこのツイートから始まりました。
…… きこえますか… きこえますか… 私は今… あなたの心に… 直接… 呼びかけています…飲みに行く前に… iOSDC Japan 2017のトークに… 応募… 応募するのです…
— Motoki Narita (@motokiee) July 21, 2017
さらに続く悪ノリ…
…… きこえますか… きこえますか… 私は今… あなたの心に… 直接… 呼びかけています…リリースは終わったみたいですがiOSDC Japan 2017の応募が終わってません…このままだとミッケラーのビールが美味しく飲めません…さぁ、応募するのです…
— huin (@huin) July 21, 2017
確かにこのままでは、せっかくミッケラーに行くのに美味しく飲めません。 というわけで、ちゃちゃっと書いたCfPがこれでしたが、めでたく採択されて話すこととなりました。
最初はなんとかなるかなと思っていましたが、いざ資料を作り出すと全くもって面白い話になりませんでした。 技術の話なら面白くなくてもいざ知らず、ネタのLTで面白くないのは話す方も聞く方も苦痛でしかないので。。。
当日までに資料はほぼ出来上がっていましたが、なかなかオチが決められるず迷走していました。 他のセッションも聞けずに発表のクオリティを上げていました。
そんな中、野生の@jpmartha_jpに絡まれ、余計にプレッシャーをかけれる始末。
大トリの @dealforest さん #iosdc pic.twitter.com/iuDAlwvkza
— pancake(Satoshi)🇮🇳 (@jpmartha_jp) September 17, 2017
大トリの @dealforest さん考えてるのか寝てるのか🤔 #iosdc pic.twitter.com/TlnLSTX7P1
— pancake(Satoshi)🇮🇳 (@jpmartha_jp) September 17, 2017
大トリということもあり前回の大トリを勤めた@yimajoがブース出展されていたので、そこで色々と話を聞かせてもらいました。
特に心に響いたのが、LTをする際に逃げて酒を飲むのがよくないということでした。 今後同じようなプレッシャーに遭遇した時、乗り越えられずお酒に逃げてしまうことの繰り返しになります。
去年は飲まれずに立ち向かったそうです。 若干プレッシャーで吐きそうだったのですが、さらに追い討ちをかけられたと感じました。 しかし、冷静に考えれば通常セッションでは酒を飲まずに話してるんだから、ネタLTもできるだろうと気持ちを切り替えることができ、結果的にとても助かりました。
そして、今回のLTは酒を飲まずにやりましたが、やはり終わった後の一杯は格別の味がした気がします。 来年はゆっくりと他の人の発表が見たいものですね。
発表でたかられたを連呼していたので、今年は奢ってもらえました!ごちそうさまです!!!!
その後、スタッフ打ち上げしているところを通りがかり合流して、そのまま夜のiOSDCに参加したりなどとても楽しかったです。
2年連続スピーカーをさせていただきましたが、控えめに言っても最高でした!
実行委委員長の長谷川さんはじめ、スタッフの皆さん、本当にお疲れさまでした!!!!
特定の条件で rootViewController を差し替えるとメモリリークする件
UIApplication.shared.keyWindow?.rootViewController
で画面を差し替えたい時がありますよね?
例えばどんな時があるかとかと言いますと
- 認証画面があり signup/signin 後に画面を切り替え
- sigup/signin 後にチュートリアルの画面を表示
- signout 後に認証画面を表示
などがあるかなと思います。
タイトルの通り、とある条件を満たして rootViewController
を切り替えると差し替える前の viewController
が解放されずに残り続けてしまいます。
これは iOS8 頃から認識していた問題ではありますが、iOS10 現在になっても修正されていません。
これのタチが悪いところは今使用されているメモリのほぼ全てがリークしてしまうといったことです。 今あなたのアプリがメモリを100MB使用しているとすると、それがそのままメモリリークしてしまうのでいきなり 100MB の負債を担うことになります。
これを解放するためにはアプリを App Switcher からアプリを kill するしかありません。
なんどもやってしまうと芋づる式にメモリリークしていっちゃいます。 かなり辛いですね。
どうすればメモリリークするのか。 それは 「モーダルが表示されている状態で rootViewController を差し替える」です。
この太字のとこおがかなり重要です。 つまりは モーダルが表示されている場合 モーダルを閉じてから rootViewController を差し替えればいいといった話ですね。
簡単に試せるようにサンプルプロジェクトを作りました。
dissmissの部分をコメントアウトして試してもらえると、メモリリークすることが確認できます。
メモリリークするとき
メモリリークしないとき
signout したり画面を切り替える度にメモリリークしていて悩んでいる方は、一度確認してみるといいのではないでしょうか。
追記
そもそも
window.rootViewController
を差し替えるとうのがあまり良くないので、空のビューコントローラでいいのでそれをRootにして、それはずっと変わらないように保って、それ以下のコントローラを付け替えるようにするのがいいです。
コメントで教えてもらったのですが window を差し替えるよりも、この方がアニメーションもつけやすくていいですね。 勉強になりました。