STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

iOSアプリだってコンパイル時間を短くしようと頑張った話(ビルド時間もね!)

はじめまして、こんにちは。 iOSエンジニアの とと です。 STORES 決済 アプリ / SDK の開発を担当しています。

少し前になりますが、決済 iOSチームでCI上でのビルド時間削減の取り組みをおこなっておりました。

Bitriseがクレジット制になるということで、ビルド時間を短くしてクレジットの消化(ひいてはコスト)を抑えようというのが目的でした。

そもそも、エンジニアとして日々の業務でビルド待ち時間が短くなるのは嬉しいですよね。

今回は、その時の取り組みの中から、主にコンパイル時間削減のためのSwiftでのコードの書き方を修正したお話をします。

環境

  • Xocde:13.3.1
  • Swift:5.6
  • ローカルで計測時に使用したMacのスペック

コンパイル時間の計算

XCLogParser を使って、ローカルで計測していました。

型チェックや、関数単位でコンパイルの時間を計測して、時間がかかっている順を視覚的に表示してくれます。

ここで計測した時間に閾値を決めて、それを超えている箇所を改修対象としました。

大体上位20くらい改修した気がします。

やったこと

修正前と比較して、その箇所のコンパイルがどれくらい速くなったかを記載しております。

(ないやつは、計測記録をどっか飛ばしてしまってございません。すみません……)

XcodeやSwiftのバージョンによって、またはコードの内容によって大きく差はでるとは思いますので、ご参考まで。

一度変数にとる

ネストが深ければ深いほど、一度変数に取った時の効果がある傾向に見えました。

コンパイル時間:70.1%減

// Before
if viewController == viewController?.navigationController?.viewControllers.first {

// After
let firstVC = viewController?.navigationController?.viewControllers.first
if viewController == firstVC {

型指定

特に計算箇所で指定すると効果が大きい傾向にありました。

コンパイル時間:44.1%減

// Before
view.layer.cornerRadius = view.frame.height / 2

// After
view.layer.cornerRadius = view.frame.height / CGFloat(2)

クロージャーの型チェック

コンパイル時間:1.9%減

// Before
let task = dataTask() { data, response, error in
    ⋮
}

// After
let completion: (Data?, URLResponse?, Error?) -> Void = { (data: Data?, response: URLResponse?, error: Error?) in
    ⋮
}
let task = dataTask(completionHandler: completion)

forEachの中で行う処理は最低限に

// Before
list.forEach {
    $0.isEnabled = status == .notDetermined || status == .denied
}

// After
let isEnabled = status == .notDetermined || status == .denied
list.forEach {
    $0.isEnabled = isEnabled
}

演算子ではなくswitch構文を使う

コンパイル時間:23%減

// Before
let isEnabled = status == .notDetermined || status == .denied
list.forEach {
    $0.isEnabled = isEnabled
}

// After
let isEnabled: Bool
switch status {
case .notDetermined, .denied:
    isEnabled = true
default:
    isEnabled = false
}
list.forEach {
    $0.isEnabled = isEnabled
}

空の配列から要素を足すのではなく最初から突っ込む

コンパイル時間:67.3%減

// Before
var items: [Fruit] = []
items.append(.apple)
items.append(.durian)
items.append(.avocado)

// After
let items: [Fruit] = [
            .apple,
            .durian,
            .avocado
]

変数はなるべくシンプルにする

コンパイル時間:6%減

// Before
let status = permission.status
if status == .allow {
    ⋮
}

// After
let isEnable = permission.status == .allow
if isEnable {
    ⋮
}

Stringで演算子で足すのは不要なため削除

// Before
message = "\(title)" + "\(version)\n" + "\(message)"

// After
message = "\(title)\(version)\n\(message) "

式のなかで ?? (Nil-Coalescing Operator)を使わない

コンパイル時間:33.6%減

// Before
let name: String = (lastName ?? "") + (firstName ?? "")

// After
let lastName = lastName ?? ""
let firstName = firstName ?? ""
let name = lastName + firstName

余計な型変換を削除

CGFloat -> Float -> CGFloatと変換しているのをCGFloatで完結させました。

コンパイル時間:13%減

// Before
let width = CGFloat(ceilf(Float(lineWidth / 2.0)))

// After
let width: CGFloat = (lineWidth / CGFloat(2)).rounded(.up)

計算は1度にまとめる

コンパイル時間:21.6%減

// Before
path.addLine(to: CGPoint(x: 0, y: - width))
path.addLine(to: CGPoint(x: 10, y: - width))

// After
let minusWidth = - width
path.addLine(to: CGPoint(x: 0, y: minusWidth))
path.addLine(to: CGPoint(x: 10, y: minusWidth))

大きい関数を分割

関数ごとのビルド時間は短くなりますがが、分けた関数トータルでのビルド時間は変わらないことがほとんどでした。

ビルド時間というよりは、可読性のため分割対応となりました。

他にやったこと

コンパイル時間だけでなく、ビルド時間も短くしようと、 上記以外にも、いろいろと対応をおこないました。

periphery を利用して、未使用コードの削除したり、

FengNiaoを利用して、未使用アセットの削除をしたり。

また、以下のサイトを参考にさせていただき、Xcodeの設定周りの見直しもおこないました。

そして、Swiftlintの実行にCI上で20秒ほどかかっていたのですが、それを変更したファイルのみ実行するようにスクリプトを書き換えることで5秒ほどまで短くできました。

できなかったこと

コンパイルに、決めた閾値を超えていても、どうしてもリファクタ案が浮かばない/色々触ってみたが改善が見られなかったものについては現状ままの箇所もあります。

特にUI系の処理はどうしても時間がかかるものの、他の書き方はなく対応出来ませんでした。

まとめ

コードの修正だけの効果にとどまりませんが、最終的にコード全体のコンパイル時間への効果は以下のとおりです。

対応前 対応後 効果
アプリ 120~129s 113~124s 4.4%減!
SDK 47~52s 39~41s 19%減!

また、設定やコードを見直すきっかけになり、ビルド時間以外の恩恵が受けられた部分もありました!

他の取り組みについては、以前同僚の@k_koheyi さんが以前発表した時の資料をご覧ください。

BitriseのCredits-Basedな 新プランの利用と改善 - Speaker Deck


AndroidのCIビルド時間改善の話はこちら!

なんと、Androidチームは50%の削減(ちょっと悔しい)

AndroidアプリのCIビルド時間が半分以下になった話 - STORES Product Blog


モバイルエンジニア募集中です!ご興味があればお気軽にご連絡ください。

私のTwitterのDMでも大丈夫ですよ

jobs.st.inc