eaglesakuraの技術ブログ

技術的な話題とか、メモとか。

AndroidStudio 3.2-Canary17の所感

Moduleの依存グラフが表示されるようになっていた

  • いつからだろうか
  • Build Variants > i アイコンから見れる
  • 依存Moduleの依存は見れない
  • app > sdk > ktx のような依存グラフにはなってない
  • そのうち改善される?

Android API-28公開

  • これでライブラリ公開やGoogle Play Consoleへのアップロードが可能になった
  • APIがFIXされたので、互換性チェックも行えるだろう
  • Kotlinバージョン 1.2.41 にアップデート

ConstraintLayoutが破壊的変更

  • パッケージ名が変更された
    • NG androidx.constraintlayout.widget.ConstraintLayout
    • OK androidx.constraintlayout.ConstraintLayout

NavigationEditor対応

  • 相変わらずビルドに失敗する
  • Googleにクラッシュレポートをアップして終了

Unity学習の整理 Animatorの基本

UnityChanをアニメーションさせる

Animator

  • 複数Layerが配置できる
    • Layerは Baseが身体本体の歩きモーション 表情モーション のように組み合わせられる
    • UnityChan付属のAnimator UnityChanAnimationCheck が参考になる
  • 基本的にステートマシンとして動作する
    • おそらく同じ骨構成であれば別アバターにも適用できる構造のはず

アニメーションループ

  • アニメーションが終わったタイミングでイベントを発火というユースケースがある
    • イベント処理で使っていた
  • 現在のAnimationの位置が正規化されたTime値でわかる
    • 開始 => 終了で1.0
    • 周回を進められるので、小数点以下が正規化された現在のループ
    • 整数部がループ回数として得られる
  • アニメーションがループ設定になっていない場合でも時計は進み続ける
    • UnityChanの JUMP00 で確認できる
    • 時計は進むが、アニメーションは止まる
      • 適切な次のアクションに切り替えてあげなければならない
  • ポーズだけのアニメーションの場合、凄まじい速度で時計が進む
    • UnityChanの POSE で確認できる

UniRxでループタイミングを得る方法

  • normalizedTime をint変換することで、整数部の切り替わり = ループのタイミングとして検出できる。
  • 0は最初の開始時となる。
_animator = GetComponentInChildren<Animator>();
_animator.UpdateAsObservable()
    .Select(_ => (int) _animator.GetCurrentAnimatorStateInfo(0).normalizedTime)
    .DistinctUntilChanged()
    .Subscribe(loop => Debug.Log($"Animation Loop!! [{loop}]"));

RangeBarで始めるCustomViewとDataBindingとMVVM

どういうことがしたいのか

こんなUIをMVVMで簡潔に実現したい。

f:id:eaglesakura:20180608174849g:plain

  • ユーザーに心拍数(下限、上限)等の一定範囲データを設定させる
  • その際、実装コストを減らしたい
    • なるべく現代的に仕上げたい
  • UIは RangeBar を利用する
    • RangeBarを操作すると、対応する値が表示される
    • RangeBarでは最大値1000のようなインデックス値のみ設定でき、min-maxのような範囲は指定できない
  • ViewとModel分離の利点として、UIに設定可能な値と実際の値を分離することができる
    • Modelを単純値のシンプルなまま、UIをカスタムすることができる
    • 実際に設定されるViewの変数と保存される値の取次をViewModelが担当する

設計方針

  • Android Architecture ComponentsのMVVMを使用
  • Viewはほぼレイアウトファイルのみ
  • 入力値は範囲を絞る
    • 人間にとって心拍0はありえないので、入力値を限定させる
  • データは整数値で保存する

RangeBarをDataBinding対応にする

DataBindingを行うための拡張

  • @BindingAdapter でViewModel -> Viewの一方通行
  • @InverseBindingAdapter で View <-> ViewModel の双方向
    • ここではViewModelにRangeBarのインデックス値を設定させている
@InverseBindingAdapter(attribute = "range")
fun RangeBar.getRange(): Range<Int> {
    return Range.create(leftIndex, rightIndex)
}

@BindingAdapter("range")
fun RangeBar.setRange(range: Range<Int>?) {
    @Suppress("NAME_SHADOWING")
    val range = range ?: return

    if (range.upper <= range.lower      // validation error.
            || range.lower == leftIndex && range.upper == rightIndex    // not modified.
    ) {
        return
    }
    setThumbIndices(range.lower, range.upper)
}

@BindingAdapter("tickCount")
fun RangeBar.bindTickCount(value: Int) {
    this.setTickCount(value)
}

ViewModel

  • val heartrateRange = MutableLiveData<Range<Int>>() にViewが持つIndex値を保存する
  • commitHeartrateRange() で保存されるときにIndex値を変更してあげる
  • このViewModelでは、実際に保存するときとViewに渡される値に HEARTRATE_RANGE_OFFSET ぶんの差が発生するので、ViewModelがソレを吸収する
class FitnessViewModel : AppViewModel() {
    private val fitnessRepository: FitnessRepository by lazy { FitnessRepository.getInstance() }

    private val serivce: FitnessService by lazy { FitnessService() }

    val heartrateRange = MutableLiveData<Range<Int>>()

    override fun attach(owner: ViewModelOwner) {
        super.attach(owner)

        heartrateRange.value = serivce.heartrateRange.let {
            Range.create(it.min - HEARTRATE_RANGE_OFFSET, it.max - HEARTRATE_RANGE_OFFSET)
        }
    }

    fun commitHeartrateRange() {
        val current = heartrateRange.value ?: return
        launch {
            // HeartrateRangeはRaw値のため、ここで値を変換する
            serivce.setHeartrateRange(HeartrateRange(
                    current.lower + HEARTRATE_RANGE_OFFSET,
                    current.upper + HEARTRATE_RANGE_OFFSET
            ))
        }
    }

    companion object {
        @JvmStatic
        val HEARTRATE_RANGE_OFFSET = 50

        @JvmStatic
        val HEARTRATE_RANGE_MAX = 200
    }
}

XML設定

  • app:tickCount 固定値だが、ViewModelの定数を計算で表示している
  • app:range は双方向なので、 @={ViewModelの変数名} を使用する
<com.edmodo.rangebar.RangeBar2
    app:tickCount="@{fitness.HEARTRATE_RANGE_MAX - fitness.HEARTRATE_RANGE_OFFSET}"
    app:range="@={fitness.heartrateRange}"
    android:id="@+id/HeartrateRange"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

RangeBarを継承せずにカスタムイベントを追加する

  • RangeBarの仕様上、値が変更されるたびにコールバックが飛んでくる
    • RangeBarの値を50から100に移動させると、50,51,52.....100のようにすべての値でコールバックが飛んでくる
  • そのたびにストレージに書き込むと、無駄な書き込みが発生する
  • ユーザーが値を決定した(指を離した)際のListenerは定義されていない
  • カスタムで作り、DataBindingにも反映させる
  • app:onRangeComplete というAttributeを作成した
  • OnCompleteListener には引数付きだが、DataBinding側では引数なしのラムダ式を書いても問題なくコード生成してくれる
  • これにより、 ViewModelの commitHeartrateRange() を確定タイミングでのみ行わせることができる
interface OnCompleteListener {
    fun onIndexChangeCompleted(view: RangeBar, leftThumbIndex: Int, rightThumbIndex: Int)
}

@BindingAdapter("onRangeComplete")
fun RangeBar.setOnCompleteListener(listener: OnCompleteListener?) {
    if (listener == null) {
        setOnTouchListener(null)
    } else {
        setOnTouchListener { _, event ->
            if ((event.action and MotionEvent.ACTION_UP) != 0) {
                listener.onIndexChangeCompleted(this, leftIndex, rightIndex)
            }
            return@setOnTouchListener false
        }
    }
}
<com.edmodo.rangebar.RangeBar2
        app:onRangeComplete="@{() -> fitness.commitHeartrateRange()}"
        />

RangeBarの変更を別なViewへ反映する

  • RangeBarが変更されたら、実際の設定値をユーザーに表示させなければならない
  • これもレイアウトXMLに記述できる
    • 変更通知はLiveDataが受け持っているため、複雑な設定は不要
  • app:valueText で、テキストを設定している
    • このとき、string.xmlのフォーマッタを適用している
      • @string/HogeFuga(値)で参照できる
      • safeUnbox() で囲わないとLintに怒られる場合がある
    • 計算もできるので、表示するときには fitness.HEARTRATE_RANGE_OFFSET を加算してもとの値にしている
  • 計算がダルければ、表示テキスト用のLiveDataをViewModel側に用意しても良いかもしれない
<org.andriders.ace.component.activity.widget.AppKeyValueView
        app:keyText="Heartrate"
        app:valueText="@{@string/BodyHeartrateFormat(safeUnbox(fitness.heartrateRange.lower) + fitness.HEARTRATE_RANGE_OFFSET, safeUnbox(fitness.heartrateRange.upper) + fitness.HEARTRATE_RANGE_OFFSET)}"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    <string name="BodyHeartrateFormat">%1$d bpm - %2$d bpm</string>

発生した問題

LiveDataの変更をしても値が反映されない場合

  • 生成されるLayoutBindingクラスにLifecycleOwnerを渡していないとそうなる
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        binding = ProfileBinding.inflate(inflater, container, false).also { binding ->
            binding.setLifecycleOwner(activity)
            binding.fitness = fitnessViewModel
        }
        return binding.root
    }

Unity学習の整理 UnityEditorでC#のUnitTest

C#でコードを書くにあたって、UnitTestができたほうが安心できるので調べた。

UnitTestの方法

  • UnityEditorに組み込まれたUnitTest Runnerが使用できる
  • NUnit相当の機能が使える

テストプロジェクトに癖がある

  • Create > Testing でTestsディレクトリを作成可能
  • 作成されたテストプロジェクトは、UnityProject(ゲーム)側のdllが参照されない
    • ゲームコードのUnitTestができないのでは?
  • 公式動画を見ると、アセット内に直接UnitTestコードを追加していた
    • 成果物バイナリにUnitTestが含まれてしまうのでは?
      • 最適化で外される?
      • 試しに new HogeTest() をゲームコードから呼び出したが、リリースビルドでも動作した
      • CIなりなんなりでどうにもできるとはいえ、この構成は正しいのか?と不安になる
    • C#の仕様とも比べなければならない

非同期のテストも記述できる

  • IEnumerator を返却することで非同期(Unity Coroutine)のテストが行える
  • その場合のAttributeは [UnityTest]

Runtime(Editor外)でもテストが実行できる

  • Windowsで確認
  • Monoに限定されるだろうが、実ビルドしてEditor外でUnitTestが行える
    • ある程度安心はできそう

Rider内でテストが失敗する

  • 原因が不明
  • Unity Editorでは成功しているが、Riderは実行直前に例外で落ちる

Unity学習の整理 連携する統合開発環境

開発環境は必要に応じて最適なものを使いたい。 Unityでアプリを開発するにあたって、選定を行った。

C#プロジェクトエディタの選定

Visual Studioの利点・欠点

  • Unityプロジェクト側にプラグインを追加せずに使える
    • Riderは専用プラグインが自動的に組み込まれる
    • 「どうしてもファイルを増やしたくない」ならこっち
  • 日本語環境がある
  • Visual Studioである
    • エディタ機能は超強力
    • Xamarin等、他の環境も使いやすい
      • 話がそれるけど、iOSのLayout Editor等の環境はVisual Studioのほうが整備されている
      • こっちに慣れておけば、同じC#のXamarinを使う際の抵抗が小さくなるかも
  • ReShaperがないとどうにもならない
    • 結局Jetbrainsのお世話になるのである
    • 有料である
  • 会社規模が大きい場合、有料となる
    • ライセンス料は結構高い

Riderの利点

  • Android StudioやGolandと同様の使いやすさ
    • ベースが同じなので、概ね同じように動作する
  • 微妙にショートカットや機能が違う
  • サンプルコードだとBreakPointにアタッチできない
    • Visual Studioならできる
    • Pluginとの兼ね合いかもしれない
    • 通常のプロジェクトなら問題ない
  • Windows, Ubuntuでは正常動作

Unityの初期設定

  • Edit > Preferences
    • External Tools > External Script Editor でRiderを選択
      • Android SDKとNDKも選択
      • SDKはUnity用にStableな環境を持っておいたほうが良いかも
      • NDKは13b限定だった
    • ndk-bundleではなく、バージョン固定で持っておいたほうが良い
  • Unity2018から正式に .Net 4.5 + C#6.0 が使用できるようになった
    • なのでそれを使う
    • Edit > Project Settings > Player から変更
  • 必要な参照は NuGet for UnityAsset Store から取得する

Reactive Extension for Unity -> UniRx

  • UnityでもRxを使いたいので導入する
  • NuGetで取得すると、古いC#バージョン向けのコードが降ってくる
    • MainThreadDispatcher等が無いので、注意する

MainThreadDispatcherの実装

  • UnityMainThreadへのアタッチ方法を確認するため、コードを読む
    • GlobalなDispatcherを登録していた
    • GameObjectが取得できるのがMainThread限定なので、その特性を利用している
    • C#世界ではなく、UnityEngine側も利用して実装されている
  • MainThreadDispatcherが登録されており、それがMainThreadのPostを担当している

リポジトリ管理

  • gitを利用する
  • Windowsを使っていると改行コードがズレるので、LFで統一
    • .gitattributeで統一しておくといい
    • Visual StudioはCRLF+BOMじゃないと正常に動作しなかった気がするけど、直ったかな?
    • このあたりはプロジェクトメンバーの構成に合わせて変化するだろう
  • Mergerを設定する
    • .gitconfig を編集する
    • UnityYAMLMerger というツールが付属しているので、PATHを通してConfigを設定する

Unity学習の整理 Unityについての所見

なぜゲームエンジンを覚えるのか

2018.06時点のスキル

UnrealEngineとの比較

  • UnrealEngineは個人的にすごく好き
    • 学生時代(2005年頃)から、たまに雑誌で紹介されていたね
  • C#による手軽なコーディングが行える
    • テストコードもある程度書きやすい
    • 言語仕様はC#だが、RuntimeはUnityのために少し特殊であることを覚える
    • 処理がコードベースなので、プログラマとして見ると理解しやすい
    • プログラマとしてのキャリアを考えると、コードとの親和性の高い環境を使いたい
    • コードを書くのが好きである
  • アプリを観察してみると、モバイル分野ではUnityのほうが盛んだと思う
    • どっかでデータを見た気がするけど、ソースが見つからなかった

学習環境

  • 2018.06時点では、基本的に独習である
    • 勉強会は、子育てもあるので出づらい
  • 情報はかなりの量が出ている
  • 調べてみると、痒いところに手が届かない感じの情報量
    • UnitTestの件とか
  • 調べ尽くしてわからなければ、 コミュニティ にも頼ろう
  • リソースは標準のものやAsset Storeから無料・低額な素材を探す
    • 学習用としては十分である

Unityのインストール

Linux(Ubuntu)版の不具合

  • Floating Windowのリサイズができない
  • フォントに独特の滲みがある
    • GeForceで確認
    • Intel GPUCore i7組み込みGPU)では再現しない
    • フォント用テクスチャ生成でトチってる?
  • 同スペックマシンの場合、Windows版に比べて重い
  • 不具合ではないが、Launcherが登録されないので自分で *.desktop ファイルを書く必要がある

Windows版の所感

  • 開発環境としては最も良さそう
    • スペックが自由に選べる
    • 動作が安定している
  • ノートPC + Quadro環境ではカツカツな状態だった
    • DirectXを使っているはずなので、Quadroマシンにはあんまり期待してない
    • そもそも2015年のPCだし
    • 会社マシンがコレなので、スペック的にキツイ
    • 買い替えたいが、買い替え先の検討をしなければならない

Android NDKでprotocol buffersを利用する

Protocol Buffersとは

Protocol Buffersは2008年の7月7日=鑑純夏さんの誕生日にGoogleが公開したデータシリアライズ・デシリアライズライブラリです。なお、七夕は雨です。

IDLという独自のテキストフォーマットでデータを記述し、複数プラットフォーム間でデータのシリアライズとデシリアライズが行えます。

利用したいと思ったプラットフォームがAndroidiOSであり、Java/C++が公式に両対応され、かつ2008年公開でネット上に十分な情報が既に公式・非公式問わず大量に公開されているため利用しました。

JSONよりも優れている点

Javaだけで利用する場合はJSONXMLで十分でしたが、C++ではデコードや実際に利用するオブジェクトへのデータバインドがメンドウという欠点が有りました。

Protocol BuffersはIDLで記述したデータフォーマットからJava/C++のクラスを吐き出してくれるため、その点で便利です。

速度的に遅いという情報が多々見受けられますが、データサイズが100kb程度であれば速度やメモリ効率よりもデータの扱いやすさを重視したほうが開発側のコストが減るでしょう。2011年発売のMotorola XoomでもRAMのメモリ帯域は2.6GB/s程度あるようですので、その程度の(かつ読み込み時の一時的な)流量を心配するほど現代のモバイル端末のメモリは遅くないようです。

※Xoomのメモリ帯域はOpenGL Insights参照

protocのインストール

IDLからJava/C++のクラスを出力するには、protocコマンドが必要です。protocコマンドはGoogleから最新版のprotocol buffers一式を落としてきてビルドする必要があります。

cd Protocol Buffersのtarを解凍したディレクトリ
./configure
make
sudo make install

正常に完了すると、protocコマンドとincludeファイル一覧がMacにインストールされます。

データの用意

Protocol Buffersでデータをシリアライズ・デシリアライズするにはまずIDLでファイルを記述しなければなりません。

EclipseにはIDLを記述するためのプラグインが用意されているので、Marketで"Protocol Buffers"あたりで検索をかけるとインストール出来ます。

今回はTexture Packerのライセンスを貰うことが出来たので、JSONで出力されたテクスチャアトラスデータのIDLを記述しました。

IDLファイルの拡張子は"*.proto"を利用するのが通例のようです。

// Primitive.proto

package jc_res;

option java_package = "com.eaglesakura.resource.primitive";
// 四角形
message Rectangle {
    required int32 x = 1000;
    required int32 y = 2000;
    required int32 w = 3000;
    required int32 h = 4000;
}
// アトラス化された画像
message Size {
    required int32 w = 1000;
    required int32 h = 2000;
}

// Texture.proto
package jc_res_texture;

option java_package = "com.eaglesakura.resource.texture";

// テクスチャフォーマット
enum PixelFormat {
    RGBA8888 = 0; //
    RGB888 = 1; //
    RGB565 = 2; //
    RGBA4444 = 3; //
    RGB5551 = 4; //
}

// テクスチャの画像フォーマットを定義する
enum TextureFormat {
    PNG = 0;
    JPEG = 1;
    KTX = 2;
}
// TextureAtlasData.proto
package jc_res_atlas;

import "Primitive.proto";
import "Texture.proto";

option java_package = "com.eaglesakura.resource.texture.atlas";


// ATLAS化された各画像を示す
message AtlasTexture {
    // Origin Filename.
    // 拡張子は取り除かれる
    required string filename = 2000;
    // アトラスグループ内のテクスチャ位置
    required jc_res.Rectangle frame = 3000;
    // テクスチャが縦回転させられていたらtrue
    required bool rotated = 4000;
    // トリミング済みであればtrue
    required bool trimmed = 5000;
    //
    optional jc_res.Rectangle spriteSourceSize = 6000;
    // 元画像サイズ
    optional jc_res.Size sourceSize = 7000;
}

// アトラス化した画像の親グループ
message AtlasGroup {
    // 識別子
    required string uniqueId = 1000;
    // 画像配列
    repeated AtlasTexture images = 2000;
    // 画像ファイル名
    // 拡張子は取り除かれる
    required string filename = 3000;
    // 画像の拡大率
    optional float scale = 4000;
    // 画像読み込み時のピクセルフォーマット
    optional jc_res_texture.PixelFormat importPixelFormat = 5000;
}

IDLには外部ファイルをimport、コメント等の機能があるため、かなり簡単に記述できます。

Messageのデータには「必至/オプション」の設定や配列等の通常必要と思われる要素はひと通り使えます。

GradleでJava/C++用クラスを生成する

ここからが本番で、シリアライズ・デシリアライズ用のクラスを出力します。今回はオフラインツール(java)でデータを用意して、実機(NDK/C++)でデコードするという用途のためJavaC++の両方で生成を行わせます。

この頃Gradleにも興味が出てきたので、せっかくだからGradleにてビルドを行わせています。

// build.gradle

task buildIDL {
    doLast {
        // IDL出力先生成
        File cppOutDir = new File("./gen-cpp").getAbsoluteFile();
        cppOutDir.mkdirs();

        File javaOutDir = new File("./gen-java").getAbsoluteFile();
        javaOutDir.mkdirs();

        File srcDir = new File("./idl/").getAbsoluteFile();

        for(File proto : srcDir.listFiles()) {
            if(proto.getName().endsWith(".proto")) {
                // IDL File
                // build
                println("compile :: " + proto.getName());
                ant.exec(executable : 'protoc', dir : ".", output : "protoc.log") {
                    arg(value:String.format("--proto_path=%s", proto.parentFile.absolutePath));
                    arg(value:"--cpp_out");
                    arg(value:cppOutDir.absolutePath);
                    arg(value:"--java_out");
                    arg(value:javaOutDir.absolutePath);
                    arg(value:proto.absolutePath);
                }
            }
        }
    }
}

これで ./idl ディレクトリ配下の*.protoファイルを全てビルドし、gen-cppとgen-java配下にクラスを生成させています。

gradle buildIDL

Javaで利用する

JavaのプロジェクトでProtocol buffersで生成したクラスを利用するにはProtocol Buffersの開発者向けJarを生成しなければなりません。

Jarファイルを生成するにはMaven2が必要です(maven3では動きませんでした)。ただし、私の環境ではメモリが不足してビルドが失敗することがあったので、mvn2 install前にVMの使用メモリを変更しています。

ビルドには"..src/protoc"が必要(おそらくtestの実行に)ですので、最低でもprotocコマンドのmakeまでは完了させなければならないようです。

cd Protocol Buffersのtarを解凍したディレクトリ
cd java

# メモリ利用量を上げる
export MAVEN_OPTS=-Xmx2048M

# Maven2を利用してビルド
mvn2 install

実行すると、protobuf-java-2.5.0.jar(私の場合)が生成されるので、あとはそれをJavaのプロジェクトに加えれば利用が行えます。

arm向けの静的ライブラリを生成する

今回最も頭を悩ませたのが、NDK向けのprotocl buffersのビルドです。ソースレベルで配布されていますが、configure等を通さないとビルドが行えないため以前導入しようと思った時はここでコケてメンドウになって導入を見送っています。

今回はまじめにいろいろググりながら利用できるようにしました。

configureを通すためには環境変数にいろいろ準備をしないといけないようなので、次のようなスクリプトを書いて実行しています。Android 4.0以上で使えればよかったのでandroid-14で通していますが、パスを変更すれば多分他のバージョンでもいけます(未検証)。

#! /bin/sh
# ndk-configure.sh

PREBUILT=${ANDROID_NDK_HOME}/toolchains//arm-linux-androideabi-4.8
PLATFORM=${ANDROID_NDK_HOME}/platforms/android-14/arch-arm

export CC=${PREBUILT}/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-gcc
export CFLAGS=-'fPIC -DANDROID -nostdlib'
export LDFLAGS="-Wl,-rpath-link=${PLATFORM}/usr/lib/ -L${PLATFORM}/usr/lib/"

export CPPFLAGS=-I${PLATFORM}/usr/include

export LIBS=-lc 
./configure --host=arm-eabi

ndk-configureを実行すると、Android NDK用のconfig.hファイルが生成されます。

次にprotocol buffersを解答したディレクトリをjniにリネームして、次のAndroid.mkとApplication.mkを配置します。無駄なものが含まれているかもしれませんが、おそらくリンク時に排除してくれるので多分問題ありません。

# Application.mk
APP_STL := gnustl_static
APP_ABI := armeabi-v7a armeabi
APP_MODULE := libprotobuf
# Copyright (C) 2009 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

COMPILER_SRC_FILES :=  \
src/google/protobuf/descriptor.cc \
src/google/protobuf/descriptor.pb.cc \
src/google/protobuf/descriptor_database.cc \
src/google/protobuf/dynamic_message.cc \
src/google/protobuf/extension_set.cc \
src/google/protobuf/extension_set_heavy.cc \
src/google/protobuf/generated_message_reflection.cc \
src/google/protobuf/generated_message_util.cc \
src/google/protobuf/message.cc \
src/google/protobuf/message_lite.cc \
src/google/protobuf/reflection_ops.cc \
src/google/protobuf/repeated_field.cc \
src/google/protobuf/service.cc \
src/google/protobuf/text_format.cc \
src/google/protobuf/unknown_field_set.cc \
src/google/protobuf/wire_format.cc \
src/google/protobuf/wire_format_lite.cc \
src/google/protobuf/io/coded_stream.cc \
src/google/protobuf/io/gzip_stream.cc \
src/google/protobuf/io/printer.cc \
src/google/protobuf/io/tokenizer.cc \
src/google/protobuf/io/zero_copy_stream.cc \
src/google/protobuf/io/zero_copy_stream_impl.cc \
src/google/protobuf/io/zero_copy_stream_impl_lite.cc \
src/google/protobuf/stubs/common.cc \
src/google/protobuf/stubs/once.cc \
src/google/protobuf/stubs/structurally_valid.cc \
src/google/protobuf/stubs/strutil.cc \
src/google/protobuf/stubs/stringprintf.cc \
src/google/protobuf/stubs/substitute.cc

# C++ full library
# =======================================================
#include $(CLEAR_VARS)

LOCAL_MODULE := libprotobuf
LOCAL_MODULE_TAGS := optional

LOCAL_CPP_EXTENSION := .cc

LOCAL_SRC_FILES := $(COMPILER_SRC_FILES) 

LOCAL_C_INCLUDES := $(LOCAL_PATH)/src

LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/android \
bionic \
$(LOCAL_PATH)/src \
$(JNI_H_INCLUDE)

# LOCAL_SHARED_LIBRARIES := libz libcutils libutils
# LOCAL_LDLIBS := -lz

# stlport conflicts with the host stl library
#ifneq ($(TARGET_SIMULATOR),true)
#LOCAL_C_INCLUDES += external/stlport/stlport
#LOCAL_SHARED_LIBRARIES += libstlport
#endif

# Define the header files to be copied
#LOCAL_COPY_HEADERS := \
#    src/google/protobuf/stubs/once.h \
#    src/google/protobuf/stubs/common.h \
#    src/google/protobuf/io/coded_stream.h \
#    src/google/protobuf/generated_message_util.h \
#    src/google/protobuf/repeated_field.h \
#    src/google/protobuf/extension_set.h \
#    src/google/protobuf/wire_format_lite_inl.h
#
#LOCAL_COPY_HEADERS_TO := $(LOCAL_MODULE)
# RTTIを有効
LOCAL_CPPFLAGS   += -frtti
LOCAL_CFLAGS   += -frtti
# LOCAL_CFLAGS := -DGOOGLE_PROTOBUF_NO_RTTI

include $(BUILD_STATIC_LIBRARY)

私の場合はdynamic_cast利用のため実行時型情報が欲しかったので、-frttiを有効にしています。ビルドはNDKのためndk-buildコマンドを利用します。

ビルドするにはAndroidプロジェクトと同等の構成にしないとエラーで弾かれるので、protobuf-2.5.0(最新版をDLした場合)をjniにリネームし、適当なAndroidManifest.xmlを配置すればndk-buildが行えます。

ビルドを行うとobj/local/armeabiにlibprotobuf.aが生成されています。

アプリ用のAndroid.mkで利用する

事前に生成した静的ライブラリは最終的にはアプリ用の*.soにリンクしなければなりません。アプリ側のAndroid.mkで次のように記述し、リンクを行っています。

## Import Protocolbuffer
include $(CLEAR_VARS)
LOCAL_MODULE := protobuf
LOCAL_SRC_FILES := 相対パス/libprotobuf.a
include $(PREBUILT_STATIC_LIBRARY)

#################################################################################
include $(CLEAR_VARS)

## アプリのビルド設定...

## libprotobuf.aをリンクする
LOCAL_STATIC_LIBRARIES += protobuf

## *.soをビルドする
include $(BUILD_SHARED_LIBRARY)

アプリ内で*.pb形式のデシリアライズを行う

オフラインツールで出力したProtocol Buffers形式のファイル(*.pbファイル)はNDKだと次のように記述することでデシリアライズを行えます。

今回は"TextureAtlasData.proto"から生成したAtlasGroupクラスを利用しています。

newでクラスを生成して、生成されるAtlasGroup::MergeFromCodedStream()メソッドを呼び出すことでbyte配列からデシリアライズしています。

#include    "TextureAtlasData.pb.h"

中略...


void Hoge::onAppInitialize() {

    // deserialize
    {
        MFileMapper file = Platform::getFileSystem()->loadFile(Uri::fromAssets("atlas.pb"), NULL);
        jc_sp group(new jc_res_atlas::AtlasGroup());


        google::protobuf::io::CodedInputStream is( (uint8_t*)file->getHead(), (int)file->length() );
        
        jclogf("created image size(%d)", group->images_size());

        group->MergeFromCodedStream(&is);

        jclogf("loaded image size(%d)", group->images_size());
        jclogf("file name(%s)", group->filename().c_str());
    }
}

まとめ

前準備が非常にメンドウですが、一度ビルドしてしまえば後は*.protoを書いてクラス生成 -> includeするだけでJava/C++両対応のファイルが生成できるため、今後は非常にデータ形式の生成が楽になりそうです。

makefile等はgithubでも公開しています。 https://github.com/eaglesakura/jointcoding/tree/develop/apps/protocolbuffers

大量の情報を書き留めてくれていたネット上の先人たちに感謝します。

OpenGL Insights 日本語版 (54名のエンジニアが明かす最先端グラフィックス プログラミング)

OpenGL Insights 日本語版 (54名のエンジニアが明かす最先端グラフィックス プログラミング)

マブラヴ ツインパック figma 鑑純夏 1体 同梱 - Xbox360

マブラヴ ツインパック figma 鑑純夏 1体 同梱 - Xbox360