iOSにおける静的ライブラリ作成技法

この記事は VOYAGE GROUP Advent Canlendar 20日目の記事です。

こんにちは。@daybysayです。

私事ですが最近SDKエンジニアに転向したので、iOSにおける静的ライブラリ作成技法について勉強してまとめてみました。

iOSにおけるライブラリ、特にベンダーが提供するSDKと呼ばれている子たちは静的ライブラリ、あるいはそれをラップしたFramework形式で配布されることが多いです。

そこで、今回は静的ライブラリの作成方法とFramework化をするところまでを実装しました。

静的ライブラリ作成とFramework化を実現したプロジェクトはこちらになります。

静的ライブラリの作成

静的ライブラリを作成するには、まずCocoa Touch Static Libraryテンプレートを用いてプロジェクトを作成します。

Xcode -> File -> New -> Project から iOS -> Framework & Library -> Cocoa Touch Static Library を選択します。 f:id:DayBySay:20151220032715p:plain

今回のプロジェクト(Framework)名はHOGEFugaにしました。

プロジェクト作成時にHOGEFuga.hHOGEFufa.mの2つのファイルが生成されると思います。 こちらは後ほど利用しますので、一旦無視してください。

利用者に提供する機能をもったクラスを実装する

今回はお試しなので、HOGEFugaServiceというクラス名で、ログを吐くだけのメソッドを実装しました。

#import <Foundation/Foundation.h>

@interface HOGEFugaService : NSObject

+ (void)show;

@end
#import "HOGEFugaService.h"

@implementation HOGEFugaService

+ (void)show {
    NSLog(@"hogehoge");
}

静的ライブラリをビルドする

あとはCmd + Bでビルドするだけで静的ライブラリが作成されます。とても簡単ですね。

出力先はここに書いてあります。 f:id:DayBySay:20151220033103p:plain

ちなみに静的ライブラリはlib{ターゲット名}.aで出力されます。

ビルドする際の注意点は、内包するアーキテクチャを正しく指定することです。

現在のデフォルトでは armv64, armv7 が設定されているので、何もしないと下記のような出力結果になります。

$ file /Users/t-sei/Library/Developer/Xcode/DerivedData/HOGEFuga-fsxeplpuhlcgeseuqdiocmyhpvtp/Build/Products/Release-iphoneos/libHOGEFuga.a
/Users/t-sei/Library/()/Release-iphoneos/libHOGEFuga.a: Mach-O universal binary with 2 architectures
/Users/t-sei/Library/()/Release-iphoneos/libHOGEFuga.a (for architecture armv7):  current ar archive random library
/Users/t-sei/Library/()/Release-iphoneos/libHOGEFuga.a (for architecture arm64):  current ar archive random library

Build Settingsで armv7s を追加することで、iPhone実機すべてのアーキテクチャに対応できるようになります。 f:id:DayBySay:20151220033149p:plain

設定後

file /Users/t-sei/Library/Developer/Xcode/DerivedData/HOGEFuga-fsxeplpuhlcgeseuqdiocmyhpvtp/Build/Products/Release-iphoneos/libHOGEFuga.a
/Users/t-sei/Library/()/Release-iphoneos/libHOGEFuga.a: Mach-O universal binary with 3 architectures
/Users/t-sei/Library/()/Release-iphoneos/libHOGEFuga.a (for architecture armv7):  current ar archive random library
/Users/t-sei/Library/()/Release-iphoneos/libHOGEFuga.a (for architecture arm64):  current ar archive random library
/Users/t-sei/Library/()/Release-iphoneos/libHOGEFuga.a (for architecture armv7s): current ar archive random library

また、iPhone Simulator用のアーキテクチャが内包された静的ライブラリはビルドターゲットをiPhone Simulatorにすることで出力可能です。 f:id:DayBySay:20151220043434p:plain

出力先は実機用静的ライブラリの隣のディレクトリに居ました。 f:id:DayBySay:20151220043546p:plain

実機用、SImulator用の2つのライブラリを作成後にlipoコマンドを用いて1つにまとめるのが一般的のようです。

lipo -output libHOGEFuga.a -create /Users/t-sei/Library/()/Release-iphoneos/libHOGEFuga.a /Users/t-sei/Library/()/Release-iphonesimulator/libHOGEFuga.a

統合後

$ file libHOGEFuga.a
libHOGEFuga.a: Mach-O universal binary with 5 architectures
libHOGEFuga.a (for architecture armv7): current ar archive random library
libHOGEFuga.a (for architecture armv7s):        current ar archive random library
libHOGEFuga.a (for architecture i386):  current ar archive random library
libHOGEFuga.a (for architecture x86_64):        current ar archive random library
libHOGEFuga.a (for architecture arm64): current ar archive random library

確認してみると、iPhone Simulator用の i386x86_64 が追加されているのがわかります。

これで1つのライブラリで実機でもiPhone Simulatorでも動くバイナリが出来上がりました。

調査した中では、XcodeRun Scriptを用いて上記の統合処理を行っているものが多かったのですが、個人的にRun Scriptが得意ではないので、今回はMakefileで実装しています。

XCODEBUILD?=$(shell which xcodebuild)
TARGET_NAME?=HOGEFuga
LIB_NAME:=lib${TARGET_NAME}.a
PROJECT_NAME:=${TARGET_NAME}.xcodeproj
BUILD_DIR:=./
CONFIGURATION?=Release

libHOGEFuga.a: libHOGEFugaPhoneos.a libHOGEFugaPhonesimulator.a
        lipo -output $@ -create $^
        rm -rf build/ Release-*/

libHOGEFugaPhoneos.a:
        ${XCODEBUILD} -target ${TARGET_NAME} -project ${PROJECT_NAME} -configuration ${CONFIGURATION} -sdk iphoneos -arch armv7 -arch armv7s -arch arm64 clean build ONLY_ACTIVE_ARCH=NO BUILD_DIR="${BUILD_DIR}"
        mv ${BUILD_DIR}/${CONFIGURATION}-iphoneos/${LIB_NAME} $@

libHOGEFugaPhonesimulator.a:
        ${XCODEBUILD} -target ${TARGET_NAME} -project ${PROJECT_NAME} -configuration ${CONFIGURATION} -sdk iphonesimulator -arch i386 -arch x86_64 clean build ONLY_ACTIVE_ARCH=NO BUILD_DIR="${BUILD_DIR}"
        mv ${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${LIB_NAME} $@

make libHOGEFuga.aを叩くことでlibHOGEFugaPhoneos.alibHOGEFugaPhonesimulator.aがプロジェクトのルートに作成され、その2つのライブラリをlipoで1つにまとめています。

Framework化する

静的ライブラリの形式でも利用は可能ですが、Framework化することで、画像や文字列ファイルやドキュメントなどのリソースをセットで管理できたり、複数のバージョンを内包できたりなど幾つかの利点があります。

詳しくはドキュメントに書いてあります。

ドキュメントにある通り、Framework自体はただのディレクトリ構成なのでここではディレクトリ作成が中心になります。

公式推奨のディレクトリ構成は下記になります。

MyFramework.framework/
    MyFramework  -> Versions/Current/MyFramework
    Resources    -> Versions/Current/Resources
    Versions/
        A/
            MyFramework
            Resources/
                English.lproj/
                    InfoPlist.strings
                Info.plist
        Current  -> A

バージョニングですが、今回は必要性がなかったのでバージョニング用のディレクトリは作らず下記の構成にしました。

MyFramework.framework/
    MyFramework
    Headers/
    Resources/
        Info.plist

Info.plistを作成する

必要性がいまいちわかっていないですが、必要らしいので作成しています。

くわしいことはこちらにあるっぽいので時間ができたら読んでみようと思います。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>English</string>
    <key>CFBundleExecutable</key>
    <string>HOGEFuga</string>
    <key>CFBundleGetInfoString</key>
    <string>HOGEFuga</string>
    <key>CFBundleIdentifier</key>
    <string>jp.co.hogehoge.HOGEFuga-framework</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>HOGEFuga</string>
    <key>CFBundlePackageType</key>
    <string>FMWK</string>
    <key>CFBundleShortVersionString</key>
    <string>1</string>
    <key>CFBundleSignature</key>
    <string>????</string>
    <key>CFBundleVersion</key>
    <string>1</string>
    <key>NSHumanReadableCopyright</key>
    <string>(c) 2015 Hoge All rights reserved.</string>
</dict>
</plist>

アンブレラヘッダを作成する

アンブレラヘッダは、公開すべきヘッダファイルをすべてインポートしたヘッダファイルであり、Framework名.hとして作るのが一般的です。

こちらの作成は必須ではないですが、利用側にとってメリットがあるので作っています。

今回は下記のようになります。

#import <Foundation/Foundation.h>

#import <HOGEFuga/HOGEFugaService.h>

Frameworkにまとめる

最終的なプロジェクト構成はこんな感じになりました。 f:id:DayBySay:20151220041646p:plain

libHOGEFuga.aやヘッダファイルなどをFramework形式に沿ったディレクトリの中に組み替える処理はまたMakefileで実装しています。

TARGET_NAME?=HOGEFuga
FW_DIR:=${TARGET_NAME}.framework
FW_DIR_RES:=${FW_DIR}/Resoursec
FW_DIR_HEAD:=${FW_DIR}/Headers
FW_DIRS:=${FW_DIR_RES} ${FW_DIR_HEAD}

HOGEFuga.framework: libHOGEFuga.a
        mkdir -p ${FW_DIRS}
        cp ${TARGET_NAME}/*.h ${FW_DIR_HEAD}/.
        cp ${TARGET_NAME}/Info.plist ${FW_DIR_RES}/.
        cp $^ ${FW_DIR}/${TARGET_NAME}

libHOGEFuga.a: libHOGEFugaPhoneos.a libHOGEFugaPhonesimulator.a
 (略)

make HOGEFuga.frameworkを叩くとHOGEFuga.frameworkが作成されます。

f:id:DayBySay:20151220034459p:plain

Frameworkを利用する

利用している様子です。

f:id:DayBySay:20151220034640p:plain

利用する側では、アンブレラヘッダ(HOGEFuga.h)をインポートしてHOGEFugaServiceクラスのstaticメソッドをコールしています。 正しくログが吐かれているのが確認できました。

簡単でしたが静的ライブラリの作成からFramework化まで、以上となります。

最後に静的ライブラリ作成時に気をつけたほうが良いと思ったことを書いておきます。

静的ライブラリを作る際に注意すべきこと

静的ライブラリ作成時に限ったことではないですが、特に下記に注意すべきだと考えられます。

  1. 名前空間に気をつける
  2. privateAPIの利用を避ける
  3. ビルド時のパスに気をつける

名前空間に気をつける

Objective-Cは言語仕様として名前空間を持っていないため、クラス名にプレフィクスをつけることでシンボルの衝突を避ける必要があります。

滅多にないことですが、実際私も過去に2つのSDK内のシンボルが衝突してコンパイルできなくなった事があり、何もできずに頭を抱えたことがあります。

悲しみを生まないためにもシンボルの衝突を起こさない努力をしましょう。(あるいはSwiftを使う)

非公開APIの利用を避ける

非公開APIの利用は避けましょう。静的ライブラリ内で非公開API等を利用されてしまうと、利用者側のアプリが審査で落とされたり、そもそもSubmitできなくなったりします。 また過去に非公開APIの誤用によって、導入したアプリに脆弱性が埋め込まれるSDKが配布された事例がありました。このような事態を避けるためにも、非公開APIの利用は控えましょう。

ビルド時のパスに気をつける

stringsコマンドを用いることで、バイナリ内の文字列を取得する事が可能です。 静的ライブラリのビルド時にはビルドしたファイルのパスが残ってしまい恥ずかしい場合があるので、ビルドの際はパスに気をつけましょう。笑

strings HOGEFuga.framework/HOGEFuga | grep /User
/Users/t-sei/Development/misc/HOGEFuga/HOGEFuga
/Users/t-sei/Development/misc/HOGEFuga/HOGEFuga/HOGEFugaService.m
/Users/t-sei/Development/misc/HOGEFuga

こちらからは以上です。

明日は最近ツイートが減り気味の @co3k です。お楽しみに。