16
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

iOS #2Advent Calendar 2019

Day 18

iOS でマルチモジュールを試したときにハマったこと

Posted at

Web ではマイクロサービスの話をよく聞くようになり、また Android でもたまにマルチモジュールの話を聞くようになりました。

iOS でも企業で採用している話をチラチラ聞くようになり、また今回のアドベントカレンダーにもマルチモジュールについての話が上がっています。

iOSでもマルチモジュール化したい!
https://qiita.com/hironytic/items/3fcd825cc1ef135f5b0f

ということで私もプライベートで作成している iOS アプリをモジュール分割してみたらハマりまくった話。

環境

  • Swift: 5.1
  • Xcode: 11.3
  • Deployment Target: iOS 13.1

今回はサンプルとして、

  • インターフェースなどを提供する Common モジュール
  • Common モジュールを実装する Core モジュール
  • 画面などの機能を提供する ModuleAModuleB モジュール
  • アプリのエントリポイントとなる MultiModuleSample モジュール

のモジュールに分割してみたいと思います。
依存関係は以下。

依存関係.png

この構成では MultiModuleSample が全てのモジュールを参照していて、逆に Common モジュールは全てのモジュールから参照されています。

また、 Core モジュールではみんな大好き Firebase を CocoaPods を使って参照してみました。

さらに、ModuleAModuleB について、

  • これから機能が増えてモジュール分割していったとき、このまま Dynamic Library のままだとアプリの起動時間が増えそう
  • ModuleAModuleB などの機能のモジュールは Common モジュールのように複数から参照される想定ではない
  • じゃあ全部 Static Library にしようと思ったけど今度はビルドのキャッシュが効きにくくなったりしてビルド時間が増えそう(多分)
  • じゃあ Release ビルド時は Static Library、 Debug ビルド時は Dynamic Libary ならちょうどいいのでは?(錯乱)

ということで、ModuleAModuleB

  • Mach-O Type
    • Debug: Dynamic Library
    • Release: Static Library

でいきます。

実際のプロジェクトは以下に置きました。

ただ、あくまでサンプルでいろいろ適当です :bow:

Firebase が見つからない

さっそくプロジェクトをマルチモジュール化したのですが、エラーが出ました。

Screen Shot 2019-12-15 at 18.45.25.png

import Core で Firebase が見つからないと言われてますね。

この解決策はいろいろあると思うのですが、Firebase を CocoaPods で導入したことにより毎回のビルド時間がかなり肥大化してしまったこともあり、
以下の記事を参考に CocoaPods の自動統合をやめて手動リンクする道を選びました。

CocoaPodsをWorkspaceに自動統合せずに利用する - 24/7 twenty-four seven

最初にプロジェクトの CocoaPods 参照を全て消します

次に CocoaPods をあらかじめビルドしておくためのスクリプトを用意しました

そして Core モジュールで参照するための .xcconfig を用意します

Firebase はリンカー設定がいろいろ増えて管理が大変なので CocoaPods が生成してくれるファイルを参照し、必要箇所だけ上書きする方針にしました。

#include "../Pods/Target Support Files/Pods-Core/Pods-Core.release.xcconfig"

PODS_ROOT = $(SRCROOT)/../Pods
PODS_BUILD_DIR = $(PODS_ROOT)/Build
PODS_CONFIGURATION_BUILD_DIR = $(PODS_BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)

最後に MultiModuleSample に参照を追加すれば終わりです

今回も Core モジュールの .xcconfig を参照し、 OTHER_LDFLAGS(Xcode の Build Settings > Other Linker Flags) がいらないので何も指定しないようにしました。

#include "../Core/Core/build.xcconfig"

OTHER_LDFLAGS = ""

これでとりあえずビルドは通るようになりました!

Framework が見つからない

上記の対応をすればビルドは通るようになるのですが、シミュレータで実行すると以下のようなエラーを吐いて死にます。

dyld: Library not loaded: @rpath/GTMSessionFetcher.framework/GTMSessionFetcher
  Referenced from: ~/Library/Developer/CoreSimulator/Devices/C9744A7D-E41D-4759-9741-8BDF4AFA516A/data/Containers/Bundle/Application/46366FAE-BA12-4645-858A-A93477C7BDF1/MultiModuleSample.app/MultiModuleSample
  Reason: image not found

Carthage を使ったことがある人は1度は見たことあるエラーですね(過言)

原因は、上で CocoaPods をやめたときに リンカーの設定をしたものの、 .framework をアプリに含めるようにしてないからです。
従来は CocoaPods がよしなに追加してくれていたのですが、 CocoaPods の自動統合をやめた結果、自分でアプリに含めるようにする必要があります。

なので MultiModuleSample プロジェクトに以下の Build Phase Script を用意しました

# ほぼ https://blog.kishikawakatsumi.com/entry/2019/06/17/090724 の「フレームワークのコピー」と同じです 🙇‍♂️
code_sign() {
  # Use the current code_sign_identitiy
  echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}"
  echo "/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --preserve-metadata=identifier,entitlements $1"
  /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --preserve-metadata=identifier,entitlements "$1"
}

if [ "$ACTION" = "install" ]; then
  echo "Copy .bcsymbolmap files to .xcarchive"
  find . -name '*.bcsymbolmap' -type f -exec mv {} "${CONFIGURATION_BUILD_DIR}" \;
fi

echo 'Copying frameworks'

# ${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/ がないときにエラーが起きるので作るようにする
ls "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/" || mkdir "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/"

if [ $SCRIPT_INPUT_FILE_LIST_COUNT -ne 0 ]; then
  for i in $(seq 0 $(expr $SCRIPT_INPUT_FILE_LIST_COUNT - 1)); do
    inputFileListVar="SCRIPT_INPUT_FILE_LIST_${i}"
    inputFileList="${!inputFileListVar}"
    cat "${inputFileList}" | while read inputFile; do
      cp -rf "$inputFile" "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/"

      for file in $(find ${inputFile} -type f -perm +111); do
        # Skip non-dynamic libraries
        if ! [[ "$(file "$file")" == *"dynamically linked shared library"* ]]; then
          continue
        fi
        if [ "${CODE_SIGNING_REQUIRED}" = "YES" ]; then
          code_sign "${file}"
        fi
      done
    done
  done
fi

あとは Carthage と同じように Input File Lists の欄にコピーしたい Framework のパスを書いた .xcfilelist を用意すれば OK です。

$(PODS_CONFIGURATION_BUILD_DIR)/GTMSessionFetcher/GTMSessionFetcher.framework
$(PODS_CONFIGURATION_BUILD_DIR)/GoogleUtilities/GoogleUtilities.framework
$(PODS_CONFIGURATION_BUILD_DIR)/gRPC-Core/grpc.framework
$(PODS_CONFIGURATION_BUILD_DIR)/gRPC-C++/grpcpp.framework
$(PODS_CONFIGURATION_BUILD_DIR)/leveldb-library/leveldb.framework
$(PODS_CONFIGURATION_BUILD_DIR)/nanopb/nanopb.framework
$(PODS_CONFIGURATION_BUILD_DIR)/BoringSSL-GRPC/openssl_grpc.framework
$(PODS_CONFIGURATION_BUILD_DIR)/abseil/absl.framework

これで Debug できるようになりました!

ちなみに

本当は モジュールを使う MultiModuleSample ではなく、各 Framework に依存している Core モジュールにコピーする処理を書きたかったのですが、

dyld: Library not loaded: @rpath/GTMSessionFetcher.framework/GTMSessionFetcher
  Referenced from: /private/var/containers/Bundle/Application/7B9A24BA-DFC7-4EBB-A5ED-D098BBB3D7F5/MultiModuleSample.app/Frameworks/Core.framework/Core
  Reason: no suitable image found.  Did find:
    /private/var/containers/Bundle/Application/7B9A24BA-DFC7-4EBB-A5ED-D098BBB3D7F5/MultiModuleSample.app/Frameworks/Core.framework/Frameworks/GTMSessionFetcher.framework/GTMSessionFetcher: code signature in (/private/var/containers/Bundle/Application/7B9A24BA-DFC7-4EBB-A5ED-D098BBB3D7F5/MultiModuleSample.app/Frameworks/Core.framework/Frameworks/GTMSessionFetcher.framework/GTMSessionFetcher) not valid for use in process using Library Validation: mapped file has no cdhash, completely unsigned? Code has to be at least ad-hoc signed.

のようなエラーが発生して死にました。

code signature in (フレームワーク名) not valid for use in process using Library Validation: mapped file has no cdhash, completely unsigned? Code has to be at least ad-hoc signed.

とあるので、おそらく署名関連で落ちてると思うのですがどう解決したものか悩ましかったので諦めて MultiModuleSample でコピーする処理を書きました。

Archive できない

これで実行できるようになったので、試しに Archive をしてみると以下のようなエラーが発生します。

Screen Shot 2019-12-16 at 0.00.08.png

エラー画面に一切の情報がない上にログの中にある IDEDistribution.critical.log も空っぽです。

ただ、 IDEDistributionPipeline.log の中をよく見てみると、

Assertion failed: Duplicate symbols for 17B1BDAC-7347-380D-B067-67115D777DF5

という記述があり、 17B1BDAC-7347-380D-B067-67115D777DF5 を検索すると

bitcode-build-tool built /var/folders/sb/{省略}/MultiModuleSample.app/Frameworks/Core.framework/Frameworks/Common.framework/Common arm64:17B1BDAC-7347-380D-B067-67115D777DF5

というログが見つかります。

どうやら Common.framework が .app に複数含まれているせいで bitcode コンパイルに失敗しているみたいです。

この原因ですが、各モジュールでの Common.framework の参照方法は以下のようになっています。

Screen Shot 2019-12-16 at 0.10.17.png

はい、 Embed になっていますね。
この場合、各モジュールはそれぞれ モジュール名.framework/Frameworks/ 内に Common.framework をコピーします。
なので Common.framework が .app に複数含まれてエラーになっていたっぽいです。

この解消法は単純に、 エントリポイントである MultiModuleSample プロジェクト以外の全ての Common モジュールを参照するプロジェクトの Embed を 「Do Not Embed」 にするだけです

結果・・・

Screen Shot 2019-12-16 at 0.22.10.png

Archive に成功しました!

ちなみに

Xcode 10 までは Embed と Link Framework の欄が別々で、見るところだけ気をつければいいだけでした。

Xcode 11 で Embed と Link Framework が合体してデフォルトが Embed になったので、ついポンポン Link させると罠にハマります(ハマりました)。

余計なモジュールが含まれている

上の Archive を見ると必要そうなモジュールのみ含まれているように見えますが、実際にできた .ipa をバラしてみると ModuleA.frameworkModuleB.framework が含まれています。

Screen Shot 2019-12-16 at 0.50.40.png

上記2つは最初に書いた通り、 Release ビルド時は Static Library としてビルドしているはずなので .framework が含まれる必要はありません。

なので ModuleA ・ ModuleB についても上記のように Embed をやめます

ただ、このままでは Debug ビルド時に逆に .framework が含まれなくなってしまい、また image not found が発生してしまいます。
なので、今回も前回のように手動コピーさせます。

https://blog.kishikawakatsumi.com/entry/2019/06/17/090724 の「フレームワークのコピー」と同じです 🙇‍♂️
if [ ${CONFIGURATION} != 'Debug' ];then
    exit 0
fi

# あとは上と同じなので略
$(BUILT_PRODUCTS_DIR)/ModuleA.framework
$(BUILT_PRODUCTS_DIR)/ModuleB.framework

ただ、このまま実機でデバッグを実行した際、以下のようなメッセージが出てインストールに失敗します。

Screen Shot 2019-12-16 at 0.54.37.png

またもや優しさのかけらもないメッセージですね。
ちなみに Details をクリックすると以下のように詳細を教えてもらえました。

Screen Shot 2019-12-16 at 0.59.08.png

なるほど、わからん。

ただ、 Console.app でインストールしたときのログを見たところ、以下のようなものがありました。

0x16ef17000 +[MICodeSigningVerifier _validateSignatureAndCopyInfoForURL:withOptions:error:]: 183: Failed to verify code signature of /private/var/installd/Library/Caches/com.apple.mobile.installd.staging/temp.5JSbAi/extracted/MultiModuleSample.app/Frameworks/ModuleB.framework : 0xe800801c (No code signature found.)

まあなんのことやらさっぱりわかりませんが、なんとなく ModuleB で問題が発生してそうです。
また、 code signature で問題が起きているので署名周りで文句を言われてるのかなと思いました。

とりあえず、 ModuleA ・ ModuleB の Signing が 「Automatic」 になっていたので 「Manual」 に変更してみました

うまくいきました。

なるほど、わからん。

リソースをコピーしたい

ここまでで実行できるようになりましたが、 Release ビルドを Static Library にしたせいでもし ModuleA で .xib などリソースファイルを使った場合、 .app に含まれません。
なのでこれも手動でコピーしてやる必要があります

これも上に書いた .framework をコピーするのと同じ要領でいけます。

if [ $CONFIGURATION != "Release" ];then
    exit 0
fi

echo 'Copying Bundle Resources'

if [ $SCRIPT_INPUT_FILE_LIST_COUNT -ne 0 ]; then
  for i in $(seq 0 $(expr $SCRIPT_INPUT_FILE_LIST_COUNT - 1)); do
    inputFileListVar="SCRIPT_INPUT_FILE_LIST_${i}"
    inputFileList="${!inputFileListVar}"
    cat "${inputFileList}" | while read inputFile; do
      echo "copy $inputFile"
      cp -rf "$inputFile" "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_FOLDER_PATH}/"
    done
  done
fi

あとは Input File List に

$(BUILT_PRODUCTS_DIR)/ModuleA.framework/ModuleAXib.nib

のようにコピーしたいファイルを書いた .xcfilelist を指定すれば OK です。

ここまでで

これでようやく開発をしていけるくらいには体裁が整ったのかなと思います。

ただ、露見してないだけでまだ問題が潜んでいるかもしれませんし、上に書いたようにこの手の問題は非常にわかりにくいです。

しかし、あくまで個人の見解ですが、大きなプロジェクト、特に大人数で長期間メンテするようなプロジェクトはマルチモジュールの恩恵が大きいと思います。

個人的にもマルチモジュールな構成は好きなのでいろいろ試していきたいです!

学んだこと

  • ログをちゃんと見る
    • 大抵の問題はログで吐き出されている。 Console.app だったり Xcode の Report Navigator(⌘9)のビルドログに結構ヒントが転がっている
  • Diff 大事
    • git commit をこまめにしていこう(反省)
  • 最後に頼りになるのはやっぱり勘
    • なんやかんや頼りになる
16
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?