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 が開かれ情報を見ることができます。

f:id:dealforest:20200602193333p:plain

上記のような画面が表示され Buffer をダブルクリックすると kernel への in, out を確認することができます。便利ですね。 あとは左側のナビゲーションエリアにある ComputeCommandEncoder を選択するとプレビューが表示されサマリーが表示されます。 また、このプレビューからも out を表示することができます。

f:id:dealforest:20200602194158p:plain

そして肝心の困っていた実行スレッド情報も取得することができました、これが一番でかい!

f:id:dealforest:20200602194338p:plain

このてんとう虫のようなアイコンを選択すると、該当するスレッドの情報を確認できます。

スレッドの情報を指定すれば該当するスレッドの情報を Playground のように右側に値が表示されます。

f:id:dealforest:20200602194743p:plain

他にもスレッドの情報を確認したいようでしたら追加して再実行するだけでいいので、これでしたら簡単にデバッグできそうですね。 適当にぽちぽちしていると他にも便利そうなのものありましたので、適当にさわってみるものもいいかもしれません。

自作したモデルを iOS で使う際に input の型がどのような影響を与えるのか調べた

f:id:dealforest:20200117013235p:plain

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')

f:id:dealforest:20200117010515p:plain

さらにこのままでは 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")

f:id:dealforest:20200117010518p:plain

モデルの生成や 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 で入力した数字を、そのまま推論できたりしますので興味ある方はみてもらえればと思います。 また本記事の中のコードもこちらにあります。

dealforest/CoreMLEasyTryPlayground

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 を差し替えるよりも、この方がアニメーションもつけやすくていいですね。 勉強になりました。