目的
AndroidとiOSでモデル部分のコードを無償ツールで共通化したい。
AndroidとiOSでコードを共通化したいという要望はよくあると思います。ゲーム用やHTML5+JavaScriptを使ったものを除くと、
- Xamarin (C#, F#など.NET系言語)
- RoboVM (Java, KotlinなどJVM系言語)
- Delphi (Object Pascal, C++)
- Qt mobile (C++)
- RubyMotion (Ruby)
- Codename One (Java)
などなどiOS上でネイティブコードにコンパイルされる仕組みの商用ツールは多くあります。
が、どれも有償です。無償で使えて実用レベルに達しているものはほとんどなく、J2ObjCはGoogleでInboxの開発に使われた実績もあり、有力な候補になります1。
とりあえず少し試してみました。
環境
- Xcode7.2
- J2ObjC 0.9.8.2.1
- J2ObjC配置先: ~/Library/j2objc
アプリのJava部分をstatic library化
理由
J2ObjCホームページの説明どおり2にSwiftプロジェクト内にJavaファイルを共存させると、Objective-Cへの変換前にSwiftファイルのコンパイルが始まって、ヘッダファイルが見つからずに失敗します。またJavaファイルを変更してからビルドしても変換が走らない問題も発生しました。これらはObjective-Cプロジェクトなら発生しないようです。
Java部分は別ターゲットにしてライブラリ化しましょう。ただこれもdynamic framework化しようとすると、うまくいきません。ここによるとJ2ObjC自体をdynamic framework化する必要があるようです。static libraryの方が簡単だったので、その方法を紹介します。
別ターゲットを作成する
まずstatic libraryのターゲットを追加します。
今回はModelという名前のターゲットにしました。新しくModelターゲット用のソースファイルを置くModelフォルダができるので、ここにJavaファイルを置くことにします。Javaのルールに従って名前空間と同じフォルダ階層にします。
名前空間とプリフィクス
Objective-Cには名前空間がないため、クラス名にプリフィクスをつけます。デフォルトでは名前空間をキャメルケースに変換したものがプリフィクスになります。例えばapp.model.Configクラスの場合、Objective-CではAppModelConfigというクラス名になります。
これは設定ファイルを用意することで変更できます。ここではModel/prefixes.propertiesという名前で用意しました。
app.model: AM
これをJ2ObjCの変換コマンドに渡すことで、AMConfigというクラス名で出力されるようになります。空欄にすればプリフィクスはつかずにConfigというクラス名になります。アプリ内のクラスならプリフィクスはなくてもいいと思います。
しかし出力ファイル名にはプリフィクス付かないんですが・・・ファイル名衝突しちゃうじゃないですか・・・。説明ではヘッダファイルが階層化されるっぽいんですが、言われた通りに設定しただけではそうなりませんでした。まぁアプリ内で使うクラスは気にしませんが、外部ライブラリを変換する場合はこのままでは困りそうです。
変換設定
さてJavaファイルをObjective-Cファイルに変換する設定をModelターゲットに対して行います。J2ObjCのXcode Build Rulesを参考にしてください。
Build Rulesにプラスボタンでルールを追加します。設定結果は以下です。
スクリプト部分は以下です。
${HOME}/Library/j2objc/j2objc -d ${DERIVED_FILES_DIR} \
--prefixes ${PROJECT_DIR}/Model/prefixes.properties \
--no-package-directories ${INPUT_FILE_PATH};
J2ObjCホームページの説明にある-sourcepathの設定はなくても構いません。また先ほど用意したプリフィクス設定ファイルを指定しています。
これでプロジェクトに追加したJavaファイルがObjective-Cに変換されて中間ファイルとして出力されるようになります。
この記事では後で全体をARC無効に設定しますが、ここで.mファイルのCompiler Flagsに"-fno-objc-arc"を指定することで、変換ファイルのみARC無効にすることもできるはずです。
Objective-Cファイルの利用設定
User Header Search Pathsの設定を行います。変換されたヘッダファイルを参照できるように"$(DERIVED_FILES_DIR)"を、J2ObjCのヘッダを参照できるように"$(HOME)/Library/j2objc/include"を指定します。
またCLANG_ENABLE_OBJC_ARCはNOにしてARCを無効にしておきます3。J2ObjCライブラリとのリンクはアプリ側で行うので、Modelターゲットでは必要ありません。
アプリ側に中間ヘッダファイルを公開
生成されるライブラリをアプリ側から使うには、変換されたObjective-Cのヘッダファイルが必要です。これをアプリ側から見えるようにします。
ModelターゲットのBuild Phasesで+ボタンを押し、New Run Script Phaseを追加します。
以下のようにスクリプトを記述します。
cp ${DERIVED_FILES_DIR}/*.h ${BUILT_PRODUCTS_DIR}/include/
アプリ側で利用しやすいように、公開するヘッダファイルはまとめてModel/Model.hで#importするようにしましょう。
アプリ側の設定
アプリ側にModelライブラリをリンクするように設定します。アプリターゲットのGeneralを選択し、Linked Frameworks and LibrariesにプラスボタンでlibModel.aを追加してください。
またlibicucore.tbdも追加します。J2ObjCホームページによるとJava側で利用しているライブラリによっては他にも追加する必要があるようです。
Build Settingsで
- OTHER_LDFLAGSに"-ljre_emul"を追加
- LIBRARY_SEARCH_PATHSに"$(HOME)/LIBRARY/j2objc/lib"を追加
- USER_HEADER_SEARCH_PATHSに"${HOME}/Library/j2objc/include"を追加
- SWIFT_OBJC_BRIDGING_HEADERにBridging Headerを設定
Bridging Headerについてはここを参考にしてください。Bridging Headerには"#import Model.h"を書いて、ライブラリ化したJava側を利用できるようにしておきます。
使ってみる
Javaファイルを追加する
app.model.Config.javaを作成します。
$ mkdir -p Model/app/model
$ vi Model/app/model/Config.java
package app.model;
class Config {
private static final int DEFAULT_SPEED = 20;
private int _speed;
Config() {
reset();
}
public void reset() {
_speed = DEFAULT_SPEED;
}
public int getSpeed() {
return _speed;
}
public void setSpeed(int speed) {
_speed = speed;
}
}
これをプロジェクトのModelターゲットに追加します4。
ヘッダファイルをまとめて公開する
ターゲット作成時に自動生成されるModel/Model.mは必要ないので削除してください。一方Model.hには外部に公開するヘッダファイルを#importします。クラス宣言は削除してください。
#import "Config.h"
公開するヘッダファイルはここに足していきます。これでアプリ側からはModel.hを#importするだけで利用できます。
アプリ側から使ってみる
ViewControllerでConfigを生成してログにgetSpeed()の結果を出力してみます。
import UIKit
class ViewController: UIViewController {
private let config = Config()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
print(String(config.getSpeed()))
}
}
実行するとXcodeのログ出力に "20"と表示されるはずです。
変換後のファイルを見る方法
Java側のメソッドがObjective-Cでどのように変換されているか見たい場合があります。変換ファイルはDERIVED_FILES_DIRに出力されているわけですが、これがどこを指すか調べるには以下のコマンドが使えます。プロジェクトルートで実行してください。
$ xcodebuild -showBuildSettings -target Model | grep DERIVED_FILES_DIR
これでModelターゲットのDERIVED_FILES_DIRが出力されます。ビルド時のスキームに合わせてRelease-iphoneosの部分はDebug-iphonesimulatorなどに置き換えてください。
ここがイマイチ
変換が遅い!
しかもこの仕組みだとDebugビルド、Releaseビルド、実機とシミュレータを切り替えるたびに新たに変換が走ります。JavaからObjective-Cへの変換はそれら全てで1回やればいいはずですが、中間ファイルの出力先が違うためです。
中間ファイルの出力先を共通化できないかと考えています5。
既存Javaライブラリを使うには
J2ObjCでRxJavaをiOSライブラリ化するを参考にしてください。
-
十分実用になりますがまだβ版扱いです。Javaを使いたいなら企業では年間$199で使えるRoboVMをオススメします。 ↩
-
libicucore.tbdをLink Binary With Librariesに追加する必要がありました。 ↩
-
変換時にARC利用のコードを生成することもできますが、J2ObjCチームのオススメはARC無効です。ARCは人間のためのもので機械的に変換するなら必要なく、ARC無効の方がパフォーマンスが高いためです。パフォーマンス差についてはMobile App Performance参照。 ↩
-
別にこの方法でなく全部Xcode上で作成しても構いません。 ↩
-
それを考える前に結局RoboVMを使うことになりそうです。 ↩