Akatsuki Hackers Lab | 株式会社アカツキ(Akatsuki Inc.)

Akatsuki Hackers Labは株式会社アカツキが運営しています。

UnityでAPK拡張ファイルを利用するときのあれこれ

こんにちは。このブログでははじめまして。クライアントエンジニアの下村です。

この記事は Akatsuki Advent Calendar 2018 の19日目の記事です。 
前回は kidach1 さんの ニューラルネットワークでStyle Transferを行う論文の紹介 でした。

背景

この記事を開いた方にはご存知の事かと思いますが、Google Play Storeではアプリケーションパッケージのサイズに制限があり、100MBを超えるapkファイルをアップロードすることができません。 100MBを超える大きなアプリを提供したい場合は、APK拡張ファイルと呼ばれる別のパッケージにデータを分割する必要があります。

インターネット接続を前提とするソーシャルゲームでこの制限に引っかかることはそう多くありませんが、最近たまたまこれを利用する機会があったので備忘録を兼ねて知見を共有したいと思います。

UnityでAPK拡張ファイルを出力する

UnityでAPK拡張ファイルを出力する方法は非常に簡単です。 PlayerSettingsの Split Application Binary オプションにチェックを入れてビルドすると、apkファイルと一緒にobbファイルが生成されます。 Unityは分割されたファイルから自動的にアセットを読み分けるため、Unityの実装上で分割を意識する必要はほとんどありません。

分割したアプリを手動でインストールする

Build And Run を実行するとUnityは自動的にobbファイルをインストールしてくれます。 一方手動でアプリをインストールする場合は、 apkファイルに加えてobbファイルを特定の場所に配置する必要があります。

obbファイルの名前や置き場所は決まっていて、以下のようなパスに配置する必要があります。
外部ストレージのAndroidディレクトリは端末によってパスが異なる事がある点に注意してください。

/storage/emulated/0/Android/obb/<パッケージ名>/main.<ビルド番号>.<パッケージ名>.obb

ファイルの転送には adb push コマンドを使うのが便利です。

$ adb install App.apk
$ adb push App.main.obb /storage/emulated/0/Android/obb/jp.aktsk.testapp/main.1.jp.aktsk.testapp.obb

これで分割されたアプリをインストールすることができました。

APK拡張ファイルのバリデーション

obbファイルは通常apkファイルと一緒にインストールされますが、実態は外部ストレージに配置されるいちファイルに過ぎません。 アプリ起動時にファイルが存在しないという状況が起こりえることから、Googleはアプリ起動時にAPK拡張ファイルのチェックや再ダウンロードを行うよう喚起しています。

Googleからはobbファイルをバックグラウンドでダウンロードするライブラリが公開されていますが、今回は急ぎで対応が必要だったことからこの高機能なダウンローダーを実装して詳しく検証している時間がありませんでした。 幸い分割されたデータはそう大きくなく、最近ではダウンロード漏れもかなり稀な発生率であるらしいことから、起動時に簡単なファイルチェックのみを行うことにしました。

APK拡張ファイルをチェックするアクティビティを挟む

今回のアプリではUnityの起動直後にいくつものアセットを読み込んでいたため、UnityPlayerActivityが起動する前にobbファイルの存在チェックをする必要がありました。 そこでonStartでobbファイルの存在をチェックをするActivityを新しく作成します。

public class ExpansionFileCheckActivity extends Activity {
    @Override
    protected void onStart() {
        super.onStart();

        if (getMainObbFile().canRead()) {
            // UnityPlayerActivityを起動する
            return;
        }

        // エラーを表示してアプリを終了する
    }

    private File getMainObbFile() {
        String fileName = "main." + getVersionCode() + "." + getPackageName() + ".obb";
        return new File(getObbDir(), fileName);
    }

    private long getVersionCode() {
        try {
            PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
            return PackageInfoCompat.getLongVersionCode(packageInfo);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
            return 0;
        }
    }

    // (省略)
}

getObbDir() でobbファイルの配置されるディレクトリを取得できますが、ファイル名は自分で生成する必要があります。 Activityを追加したら、AndroidManifestを修正して最初に起動するActivityを置き換えます。

<activity android:name="jp.aktsk.testapp.ExpansionFileCheckActivity" android:label="@string/app_name" android:launchMode="singleTask" android:theme="@style/Theme.AppCompat">
  <intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
  </intent-filter>
</activity>
<activity android:name="com.unity3d.player.UnityPlayerActivity" android:label="@string/app_name" android:launchMode="singleTask">
  <meta-data android:name="unityplayer.UnityActivity" android:value="true" />
</activity>

これで起動時に問題があれば適切なエラーを表示することができるようになりました。

必要なときだけアクセス権限を要求する

さらに一部の端末ではインストールされたobbファイルがユーザ所有にならず、アプリからアクセスできなくなることがあるようです。 Unityはこの問題を回避するため、 Split Application Binary オプションを有効にしてビルドすると READ_EXTERNAL_STORAGE 権限要求をAndroidManifestに自動で付与します。

しかしながら、ごく一部の端末のためにすべてのユーザーにこの強力な権限を要求するのはナンセンスです。ここはobbファイルが読み込めなかった場合にのみ権限を要求するようActivityを拡張しましょう。

public class ExpansionFileCheckActivity extends Activity {
    private static final int RC_PERMISSION_REQUEST = 1;

    @Override
    protected void onStart() {
        super.onStart();

        checkApkExtension();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode != RC_PERMISSION_REQUEST) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
            return;
        }

        if (grantResults.length == 0 && !ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
            // アプリの設定画面を開く
            return;
        }

        checkApkExtension();
    }

    private void checkApkExtension()
    {
        if (getMainObbFile().canRead()) {
            // UnityPlayerActivityを起動する
            return;
        }

        if (PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{
                    Manifest.permission.READ_EXTERNAL_STORAGE
            }, RC_PERMISSION_REQUEST);
            return;
        }

        // エラーを表示してアプリを終了する
    }

    // (省略)
}

ファイルチェックに失敗した場合は READ_EXTERNAL_STORAGE 権限をチェックし、なければリクエストしてから再度チェックを行います。 このときユーザーが「今後表示しない」をチェックしていると shouldShowRequestPermissionRationalefalse を返すので、一言添えてアプリ設定画面に誘導するとよいでしょう。

最後にUnity側の権限リクエストを無効化する設定をAndroidManifestに追加すれば完了です。

<meta-data android:name="unityplayer.SkipPermissionsDialog" android:value="true" />

これでなんとかAPK拡張ファイルを利用したアプリをリリースする最低限の準備ができました。 あとはお好みでユーザーへの説明や操作指示などを肉付けしましょう。

その他の注意点

バイナリの配布方法

利用しているアプリケーション配信サービスによってはobbファイルを取り扱えないことがあります。ストアの内部テストだけでは大抵不足なので、ビルドの用途に応じてAPK拡張ファイルを利用するかを切り替えられるような仕組みを準備しておくべきでしょう。

StreamingAssetsへのアクセス

UnityでAPK拡張ファイルを利用するとStreamingAssetsはobbファイルの側に格納されます。AndroidにおいてStreamingAssets以下のファイルへのアクセスはどちらにあってもそう変わりません。
しかし、一部のサードパーティライブラリではobbファイル内のStreamingAssetsに正常にアクセスできないものがありました。

StreamingAssets以下にあるアセットが正しく読み込まれているか検証を忘れないようにしましょう。

ビルド番号

Google Play StoreではAPK拡張ファイルのビルド番号はアプリ本体のものと別に扱われるようですが、Unityは必ずアプリのビルド番号を用いてobbファイルを開こうとするようです。 Google Play Storeで新しいリリースを生成するとき、新しいobbファイルをアップロードせずに過去のファイルを選択すると、Unityからアクセスできなくなってしまいます。 

まとめ

UnityでAPK拡張ファイルを扱うのは簡単ですが、その仕組みには落とし穴がいくつもありました。 アプリサイズが逼迫したときはまずより簡単な手順(例えば、アーキテクチャ毎にapkをビルドするなど)を講じて、なお足りない場合に採用を検討すべきでしょう。

その上でどうしても必要になったときは、Unityによるサポートがあるからと軽視せず早めに実装と検証を行うことが大事ですね。