FURYU Tech Blog - フリュー株式会社

フリュー株式会社の開発者が技術情報を発信するブログです。

Github Action による iOS アプリビルド時間を3分の1に改善しました!!

みなさん、こんにちは。

ピクトリンク事業部 商品技術開発部 の 足立 です。

今回は通例となってきた フリュー Advent Calendar 20243日目 の記事となります。

記事の内容としては、iOS アプリのビルドを Github Action で実行しているのですが、そのビルド時間を改善するためにやったことの紹介になります。

地道な対応でしたが効果としては絶大だったので、記事にさせていただきました。

Github Action とは

この記事を読まれている方はほとんどがエンジニアだと思うので、簡単に紹介させていただきます。

Github Action は Github が提供している CI/CD のプラットフォームです。

リポジトリに紐づいているので、PRを上げたタイミングでのテスト実行や、PRマージ後に最新のアプリを自動で作成するなど、さまざまなことができます 😍アリガトウッ!!

docs.github.com

改善前の状況

私が担当している ピクトリンクカレンダー でも、 Github Action を利用しており、手動ビルドに限った設定を行なっています。

自動にしていないのは「ビルドに時間がかかる = Github Action の実行時間の長さによる料金が爆上がり」するため、一旦必要なタイミングでのビルドに制限しています。

ビルドするタイミングは「企画メンバーへの動作確認依頼」や「App Store へのアップロード」する時に使っています。

 

さて、では実際にどれくらいの時間がかかっていたというと・・・

30分!!さんじゅっぷん!!です😂

iOS アプリの場合、どうしても時間がかかってしまいますが、さすがにこのまま運用を続けるには弊害が出てきます。

  • 確認用アプリを打ち合わせまでにつくりたい
  • スプリントレビューの直前に完了したPBIもアプリに入れて配布したい
  • Github Action の Workflow の改善後の動作確認をしたい

などなど、すぐにビルドを終わらせたい状況はたくさんあり、急いでいる時は待っているだけでもストレスを感じることは、皆さんも経験があるのではないでしょうか?

どのように改善したか

結論をひとことで言うと「ビルドに時間がかかっていたライブラリの利用をやめた」だけです。

原因

実装では NotificationCenter でのデータやりとりを type-safe に使えるようにするライブラリである typed-notifications を利用していました。

この typed-notifications 自体に問題があるわけではなく、依存関係にある swift-syntax が直接的な原因になっており、ビルド時のモジュールコピータイミングで、「10 〜 15分 かかる」状態になっていました 🫠

解決方法

解決方法は色々ありますが、今回は「対象のライブラリを使わない」方法を選択しました。

理由として以下が挙げられるため、修正による影響範囲をできるだけ少なくしつつ、 Notification Center の拡張機能として実装することを決めました。

  • Notification Center で代用が可能なことが明確
  • データのやり取りは Dictionary なので Codable が利用可能

実装方針として、以下の2つを使っています。

解決方法1: Codable を使い type-safe に値を受け渡す

NotificationCenter では、データの受け渡しでは Dictionary を使うため「通知を送る側/受け取る側」でパラメータを常に整合が取れている状態にしなければなりません。

パラメータ名やデータ型の間違いで、値の受け渡しができないだけでなく、クラッシュにつながる可能性も出てきます。

Notification Center を使うにあたって、基本的な情報や変換処理のみを共通部品として提供し、移行作業自体にもあまり時間をかけないようにしました。

実装のサンプルは以下になります。

/// Notification Center で受け渡すデータ
struct AdventCalendar: Codable {
    let year: Int
    let day: Int
    let blogTitle: String
    let blogURL: URL
}

/// Notiication Center に登録する情報の定義
struct NotificationDefinition<Payload: Decodable> {
    let name: Notification.Name
    let payloadKey: String

    init(name: String) {
        self.name = Notification.Name(rawValue: name)
        self.payloadKey = "\(name)_payload"
    }

    func payload(_ userInfo: [AnyHashable: Any]?) -> Payload? {
        guard let payloadJson = userInfo?[payloadKey] as? String else { return nil }
        let data = decode(payloadJson)
        return data
    }
    ......
}

/// 便利拡張として、受け渡すデータの定義を一括管理
extension NotificationDefinition {
    static var adventCalendarDay3: NotificationDefinition<AdventCalendar> {
        .init(name: "AdventCalendarDay3")
    }
}

ポイントは NotificationDefinition の中に、通知の受け渡しで必要となる情報を一括管理できるようにしているところです。 さらに adventCalendarDay3 として共通の定義することで、保守性を意識してみました。

payload(_:) についてはどこに定義するかは悩みましたが、 NotificationDefinition にすることで「受け渡すデータのEncode/Decode の間違いをなくす」ことを目的としてこのようにしました。

解決方法2: Combine で Wrap method を作成

次に、通知の発火・通知の購読する側です。

extension NotificationCenter {
    func post<T: Codable>(
        _ definition: NotificationDefinition<T>,
        payload: T? = nil,
        object: Any? = nil,
        userInfo: [AnyHashable: Any]?
    ) {
        var notifyInfo: [AnyHashable: Any] = [
            definition.payloadKey: encode(payload) as Any
        ]

        if let userInfo {
            notifyInfo = notifyInfo.merging(userInfo) { x, _ in x }
        }

        let notification = Notification(name: definition.name, object: object, userInfo: notifyInfo)
        self.post(notification)
    }

    func publisher<T: Codable>(
        _ definition: NotificationDefinition<T>,
        object: AnyObject? = nil
    ) -> AnyPublisher<NotificationData<T>, Never> {
        return self.publisher(for: definition.name, object: object)
            .compactMap { notification in
                let payload = definition.payload(notification.userInfo)
                return NotificationData(payload: payload, center: self, object: object)
            }
            .eraseToAnyPublisher()
    }
}

/// Combine 利用時の返却データ
struct NotificationData {
    let payload: T?
    let center: NotificationCenter
    let object: AnyObject?
}

post(name:object:userInfo:)publisher(for:object:) のラッパーメソッドを定義し、NotificationDefinition を扱えるようにしました。

ポイントは publisher(_:object:) の返却値を NotificationData としているところです。

本来受け渡したいデータだけでなく、NotificationCenter 自体の情報も返却することで、購読する側の柔軟性をあげています。

また post(_:payload:object:userInfo:) の方では、実際に受け渡したいデータ以外に userInfo もあえてパラメータとして受け取るようにしています。

こちらも柔軟性を考慮したものですが、同じ Dictionary の Key が定義されると「パラメータの方がかき消される」ことになるところは注意でしょうか。

※ より安全に使うならば userInfo 自体をなくすか、専用のキー情報を定義し、そちらに格納する方法もありそうです。

使い方

最後に使い方の紹介ですが、そこまで難しいものではないので、さらっと見ていただければと。

private var cancellable = Set<AnyCancellable>()

// 通知の購読
NotificationCenter.default
    .publisher(.adventCalendarDay3)
    .map { $0.payload }
    .sink { [weak self] adventCalendar in
        print("""
        \(adventCalendar.year)年のフリューアドベントカレンダー \(adventCalendar.day)日目の記事は「\(adventCalendar.blogTitle)」です。
        URL: \(adventCalendar.blogURL.absoluteString)
        """)
    }
    .store(in: &cancellable)

// MARK: - 

let day3 = AdventCalendar(
    year: 2024,
    day: 3,
    blogTitle: "ピクトリンクカレンダーで改善した話でなにか書きます",
    blogURL: URL(string: "https://www.furyu.jp/news/2024/11/pictlink_calendar/")
)

// 通知の発火
NotificationCenter.default
    .post( .adventCalendarDay3,
            payload: day3,
            userInfo: nil)

実行結果

2024年のフリューアドベントカレンダー 3日目の記事は「ピクトリンクカレンダーで改善した話でなにか書きます」です。
URL: https://www.furyu.jp/news/2024/11/pictlink_calendar/

解決方法3: typed-notifications をプロジェクトから除外

忘れてはいけないのが、typed-notifications を利用している箇所全てを、上記 Notification Center の拡張機能に置き換え、対象のライブラリはプロジェクトから除外します。

改善後の状況

上記改善を行い、ビルドした結果がこちらになります。

驚きの結果!!10分 です!!💗

この結果には正直自分も驚きました。最初は、モジュールコピーの時間がなくなるので、15分前後になると思っていました。

ただ、冷静になると当たり前で typed-notificationsswift-syntax の Checkout など、関連するすべての処理がなくなるのでこれくらい早くなって当然ですね😆

今後について

今回は typed-notifictoins 相当の機能が、自前の実装で置き換えられる規模感だったので問題なく対応できましたが、他のライブラリも同様の対応ができるとは限りません。

では、どうするかというと XCFrameworks の導入を積極的にやっていきたいと考えています。

導入するライブラリがすでに XCFrameworks に対応したものであれば良いのですが、そうでないものもたたあります。

その場合にどうするのかを今後は調べつつ、さらに改善していきたいと思っています。

また、キャッシュの扱い方を変えるだけでもかわるといいますが、こちらは色々試していますが、どうもうまくいかないことも多く、もう少し色々と調べていきたいと思います。

おまけ: Github Action 実行ログチェッカー

今回、この原因に気づくきっかけは「ビルドが遅いので実行ログを眺めていたら、長い間処理が止まっている」状態に気づいたことでした。

今後の運用や他のプロジェクトの改善をする場合、全てのログを追いかけたり、処理時間の計算をすることは大変なので、今後の活用も踏まえて「Github Action 実行ログチェッカー」なるモノを作成しました。

ただの表計算を使っているので、大したモノではありませんが、こちらもついでに紹介させていただきます。

利用方法は簡単で「Github Action の実行ログを貼り付ける」だけで「閾値に沿った対応優先度のチェックマークがつく」だけです。

他のプロジェクトでも時間がかかっているものがあるので、チェックしてみようと思います。

さいごに

どのプロジェクトでも、ビルド改善は積極的に行われていると思います。

専任チームによる改善を進めるプロジェクトもあると思いますが、多くが私たちと同じ「施策をすすめつつ、開発チーム内で改善を行う」状況だと思います。

私、個人としてはこの状況自体は悪いとは思いませんが「改善することの優先度をさげ続ける」状態になってしまうのは避けるべきと考え、積極的に開発チーム以外のメンバーに「開発改善の重要性、必要性を認識してもらう」ことを行なっています。

これからもプロダクト品質はもちろんですが、開発の品質、プロジェクト進行の効率化などを進められるように、日々考え動けていけたらと思います。

何か新たな改善ができたら紹介させていただきますので、よろしくお願いします。

 

それでは、明日以降もアドベントカレンダーをお楽しみください!