しおメモ

雑多な技術系ブログです。

Xcodeのビルド時間をキレずに改善する

Xcode10でNew Build Systemになってから、全体的にビルド時間は短くなりました。

しかし、それでも諸々の原因によりビルドがめちゃくちゃ遅い場合があります。
そんな場合でも、キレずに冷静に改善する方法紹介します。

ビルド時間の計測方法

Xcodeの上部にビルド時間を表示するために、ターミナルで

defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES

を実行します。

f:id:scior:20190725231154p:plain

ステップごとのビルド時間を計測するには、Product > Perform Action > Build With Timing Summaryを実行します。

f:id:scior:20190725230902p:plain

xcodebuildの場合は、-showBuildTimingSummaryをつけます。

xcodebuild clean build -project ColorStock.xcodeproj -scheme ColorStock -destination="platform=iOS Simulator,name=iPhone 8" -showBuildTimingSummary

出力はこのようになります。

Build Timing Summary

CompileSwiftSources (1 task) | 5.000 seconds

PhaseScriptExecution (1 task) | 3.000 seconds

CompileStoryboard (2 tasks) | 2.000 seconds

...

** BUILD SUCCEEDED **

大まかにどこに時間がかかっているかがわかります。
まんべんなくかかってたら諦めるしか無い

ビルド設定の見直し

New Build Systemの利用

Xcode10から新しいビルドシステムが採用されているので、そちらを利用します。

f:id:scior:20190725231014p:plain

旧来のビルドシステムとは違い、並列ビルドをしてくれるので、IDEBuildOperationMaxNumberOfConcurrentCompileTasksの指定は不要になります。
こちらをすでに設定している場合、そちらが優先されてしまうので、無効にしておきます。

defaults delete com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks

CocoaPodsのライブラリも並列ビルドしてくれます。

最適化の設定変更

最適化はデバッグ時には必要ないので、Optimization Levelは-O0にします。

f:id:scior:20190725230943p:plain

ライブラリ管理の見直し

CocoaPodsからCarthageに移行する

CocoaPodsを使っている場合、Carthageに切り替えることで、ライブラリのfetchと同時にframeworkのビルドを行ってくれるため、その分の時間を節約できます。

特に、クリーンビルドの場合、CocoaPodsで導入したライブラリは、再度ビルドし直しになってしまうので、かなり時間がかかってしまいます。

CocoaPodsのプレビルド

ライブラリが多すぎて断念しましたが、Carthageが使えないライブラリの場合、Pods.xcodeprojを別にビルドして、framework化してリンクする方法もあります。

未使用コードやアセットの削除

未使用コードの削除

peripheryというOSSで、Swift限定ですが、使われていないコードを検知することが出来ます。

github.com

複雑な書き方をしている部分は誤検出が多々あるので、一つずつCall Hierarchyやシンボル検索でたどっていくことをおすすめします。

未使用アセットの削除

こちらもFengNiaoというOSSで検知することができます。

github.com

やはりこちらも誤検出があるので、確認が必要です。
バイナリサイズも小さくなります。

コードの書き方の改善

Swiftは言語として非常に高機能ですが、便利な機能とトレードオフでビルド時間が増加することがあります。
とくに、型推論は時間がかかることが多く、演算の途中では型が確定しないような書き方をすると遅くなることがあります。

CompileSwiftに時間が取られている場合、ここを改善することで多少ビルド時間が短くなることがあります。

メソッドごとのビルド時間の計測

メソッドごとのビルド時間を測るには、ビルドオプションのOther Swift Flagsに-Xfrontend -debug-time-function-bodiesを追加します。

f:id:scior:20190725232351p:plain

xcodebuildの場合、OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies"で計測することが出来ます。

計測した時間を集計するには、xcactivitylogから拾ってくるか、xcodebuildを使う必要があります。

👇測定用のPythonスクリプトの例です https://github.com/Scior/SBTProfiler/blob/master/src/profiler.py

👹シェル芸 scior.hatenablog.com

コンパイルが長くなるケース

細かいケースは割愛しますが、配列やString+結合や、nil coalescing operator、genericsなどはコンパイルが長くなる場合があります。

以下、遅くなるようなコードの例です。

Stringの+結合

let hoge: String? = "hoge"
let fuga: String? = "fuga"

let result = "Hello, " + (hoge ?? "") + "and " + (fuga ?? "")

特にnil coalescing operatorと組み合わせると遅くなるので、一気に結合しないほうがよいです。

CGFloatの演算

let height = (hogeHeight - inset * 2) / 2

let height: CGFloatのようにあらかじめ型を明示することで改善することが多いです。

mapなどのgenericメソッド

let array = [1 ,3 ,7, 9]

let result = array.map { num in
    // 複雑な処理
}

num -> Hogeのように戻り値の型明示すると改善します。

そもそもビルドしない

最近良くやりますが、ちょっとの修正の場合、毎回ビルドしないで、LLDBで書き換える方法もあります。

scior.hatenablog.com

万策尽きたら

28コアのMac Proを買えばいいんじゃないかな🤔

www.apple.com

RxSwiftのSchedulerの実装を読んだ

少し前に、RxSwift5のSchedulerの実装を読んでみたので、初めての読んだ系記事です。

概要

RxSwiftでは、observeOnsubscribeOnで、流れてくるものに対する操作を行うスレッドやキューを指定できます。
それらの実行場所を指定するものがSchedulerクラス群になっていて、内部実装でDispatchQueueOperationQueueをラップしています。

observeOnとsubscribeOn

observeOnは、指定したschedulerで処理を実行するようなObservableを返すメソッドです。

Observable.just(Thread.isMainThread)
    .map { _ in print(Thread.isMainThread) }
    .observeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated))
    .subscribe(onNext: {
        print(Thread.isMainThread)
    })
    .disposed(by: disposeBag)

このようにすると、observeOn以降のprintDispatchQueue.globalで実行されるので、出力はtruefalseになります。

subscribeOnsubscribeとその解除を指定したスケジューラーで行うようなObservableを返すメソッドです。

Observable.just(Thread.isMainThread)
    .map { _ in print(Thread.isMainThread) }
    .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated))
    .subscribe(onNext: {
        print(Thread.isMainThread)
    })
    .disposed(by: disposeBag)

例えばこのように流すと、doもサブスレッドで行われるため、出力は両方ともfalseになります。

Serial/Concurrent Scheduler

ややこしいですが、Serial Scheduler(SerialDispatchQueueScheduler)は並列なDispatchQueueが渡された場合にも、実行順を保証するために、キューを直列に作り直します。

それに対して、Concurrent Schedulerは並列キューをそのまま実行してもに問題ない場合に用いられます。
こちらの場合、パフォーマンス的に有利になります。

主な組み込みのスケジューラー

MainScheduler

DispatchQueue.mainのラッパーになっているので、メインスレッドでasyncで実行されます。
しかし、これはSerialDispatchQueueSchedulerを実装しているので、直列スケジューラーです。

UIの更新などの、observeOn向けの実装になっています。

ConcurrentMainScheduler

こちらもDispatchQueue.mainのラッパーですが、こちらは並列スケジューラーで、subscribeOn用の実装になっています。

SerialDispatchQueueScheduler

DispatchQueue.globalのラッパーで直列スケジューラーです。

ConcurrentDispatchQueueScheduler

DispatchQueue.globalのラッパーで並列スケジューラーです。
グローバルキューなので、QoSを指定することが出来ます。

明示的にサブスレッドで動かしたい際は、通常こちらを利用することになります。

XcodeのLLDBデバッグでよく使う技

若干話題になって出尽くしてる感がありますが、XcodeのLLDBを絡めたデバッグでよく使う手法をまとめてみました。

特定の行をスキップ

行をスキップしたい際は、thread jumpthread returnを活用します。

# 1行スキップ
(lldb) th j --by 1
c

th jthread jumpの、ccontinueのように一意に決まれば先頭だけで短縮してよいので、こちらを利用すると便利です。

ある1行だけコメントアウトしたい際は、ブレークポイントを挟んで再度ビルドせずにXcodeのGUIを利用してこのようにすると、同様のことができます。

f:id:scior:20190703220643p:plain

thread returnは実行中の関数から即returnすることが出来ます。

(lldb) th r
c

特定の行を書き換える

書き換えたい行の手前にブレークポイントを挟んで、expressionコマンドを実行して、thread jumpすることで実現できます。

# Viewの背景色を赤に書き換える例
(lldb) e view.backgroundColor = UIColor.red
th j --by 1
c

eexpressionのエイリアスになっています。

こちらも毎回書き換えたい場合には、XcodeのGUIを利用するのが便利です。

f:id:scior:20190630233325p:plain

このようにすることで、上の3行のLLDBコマンドと同様のことを毎回実行してくれます。

ブレークポイントをon/offする

アクティブなブレークポイントは、breakpoint listで確認します。

(lldb) br l
Current breakpoints:
1: file = '/Users/hoge/iOS/ColorStock/ColorStock/Domain/Model/EditingThemeDataStore.swift', line = 18, exact_match = 0, locations = 0 (pending)


2: file = '/Users/hoge/iOS/ColorStock/ColorStock/Presentation/ViewController/ColorSelectorViewController.swift', line = 88, exact_match = 0, locations = 1, resolved = 1, hit count = 0

  2.1: where = ColorStock`ColorStock.ColorSelectorViewController.toastViewRecoveryButtonTapped(Any) -> () + 1295 at ColorSelectorViewController.swift:88:5, address = 0x00000001029bcf0f, resolved, hit count = 0

ブレークポイントを無効にしたい場合は、breakpoint disableを使います。削除の場合はbreakpoint deleteです。複数指定も可能です。

# リストの2番目と4番目のブレークポイントを無効にする
(lldb) br di 2 4
1 breakpoints disabled.
# リストの1番目のブレークポイントを削除する
(lldb) br de 1
1 breakpoints deleted; 0 breakpoint locations disabled.

disableしたブレークポイントはenableで有効にできます。

# リストの1番目のブレークポイントを有効にする
(lldb) br e 1
1 breakpoints enabled.

アドレスからオブジェクトに戻して変数に格納する

デバッグ時の出力が不足している場合、アドレスから元のオブジェクトを取り出したいケースがあります。
その場合、Swiftの文としてexpressionunsafeBitCastを利用するのが、一つの手法としてあります。

# Viewの親VCを取得する
(lldb) e -l swift -- let $view = unsafeBitCast(0x01234567, UIView.self)
e -l swift -- print(view.parentVIewController)

letを使って変数を定義したい場合は、e -l swiftのように言語を明示する必要があります。--はセパレータになります。
その上、変数名は$viewのように$で始まる必要があります。

~/.lldbinit~/.lldbinit-Xcodeなどに以下のように記載すると、LLDBのエイリアスを定義することが出来るため、

swi let $view = unsafeBitCast(0x012345678900, UIView.self)

のように書くことが出来て、記述が楽になります。

command alias swi expr -l swift --

単純に変数に格納せずにprintしたい場合は、poコマンドを利用します。

po unsafeBitCast(0x012345678900, UIView.self).parentViewController

組み込みメソッドに対してブレークポイントを仕込む

Symbolic Breakpointを使うことで、組み込みのメソッドに対してもブレークポイントを追加することが出来ます。

viewDidLoadのように、UIKitのメソッドを対象にする場合は、Obj-C記法で-[UIViewController viewDidLoad]のように書きます。

f:id:scior:20190703220948p:plain

$arg1にオブジェクトが入るので、po $arg1とすることで、どのオブジェクトの該当シンボルが呼ばれているか確認することが出来ます。

変数の変更を監視する

watchpointを使うことで、指定した変数の変更を監視ができます。
設定はwatchpoint set varで行い、ブレークポイントのようにlistで一覧表示してdisableで無効にできます。

# self.redを監視
(lldb) w s v self.red
Watchpoint created: Watchpoint 3: addr = 0x7ffee3515a28 size = 1 state = enabled type = w
    declare @ '/Users/hoge/iOS/ColorStock/ColorStock/Data/Entity/Color.swift:21'
    watchpoint spec = 'self.red'
(lldb) w l
Number of supported hardware watchpoints: 4
Current watchpoints:
Watchpoint 2: addr = 0x7ffee3515c58 size = 8 state = disabled type = w
    declare @ '/Users/hoge/iOS/ColorStock/ColorStock/Presentation/View/ColorDetailView.swift:31'
    watchpoint spec = 'self'
    old value: 0x000000010c709300
    new value: 0x000000010c709300
Watchpoint 3: addr = 0x7ffee3515a28 size = 1 state = enabled type = w
    declare @ '/Users/hoge/iOS/ColorStock/ColorStock/Data/Entity/Color.swift:21'
    watchpoint spec = 'self.red'
(lldb) w di 2
1 watchpoints disabled.

その他のよく使うコマンド

Xcodeのボタンでも操作できますが、以下の操作はLLDBでも出来ます。

コマンド 操作
bt スタックトレース
step ステップ実行
stepi ステップイン
continue 実行の継続