U
03 / 22 · 11 分
FAQ

APK がインストールできない:エラー文別に全原因を解説

短い答え

Unity でビルドした APK がスマホにインストールできないのはなぜ?

Android が Unity のビルドを断る理由は数種類に絞られ、それぞれ違う一文が出ます。ファイルをタップしたアプリにインストール権限が無い、別の鍵で署名された旧版が残っている、Minimum API Level か ARM64 が端末の性能を超えている、ファイルが途中で切れている。自分で adb install を実行して Failure の行を読み、端末の API level と abi を Player Settings と比べてください。

Unity は Build completed と報告するのに、スマホは応じません。これは 1 つではなく 6 つの問題で、それぞれが違う一文で姿を現します。以下が Android が Unity のビルドを断るすべてのパターンと、その文の本当の意味、そして解消に必要なただ一つの変更です。

インストール前に:提供元不明アプリの許可

Android 8 で Unknown sources の一括スイッチは無くなり、アプリ単位の権限に置き換わりました。すべてをまとめて許可する項目はもう存在しません——APK をタップしたアプリ側にインストール権限を与える形です。

  1. 1

    Settings▸Apps から、ファイルをタップしたアプリ(Chrome、Files by Google、Zalo など)を開きます。推測は禁物です。続いて Special app access▸Install unknown apps で Allow from this source をオンにします。

  2. 2

    MIUI と HyperOS ではこの上にさらに 2 画面目が来ます:Mi アカウントへのログインを要求し、スキャン中にカウントダウンを表示し、気に入らなければ Declined due to system restrictions の一文だけが残ります。Security Center アプリ内の Scan apps for viruses を切ればこの拒否は無くなります。

  3. 3

    Samsung の場合は Play Protect のスキャンが走ります:Google Play で自分のアイコン → Settings → Notifications から、Scan apps with Play Protect を切ってください。ただしどんな設定でもどうにもならないケースが一つあります——会社のプロファイルが入った端末は規約上サイドロード不可です。

  4. 4

    PC から USB 経由で入れるには、開発者オプション内の Install via USB という独立スイッチが必要です。さらに MIUI では、Mi アカウントでのログインと通信が揃って初めて解放されます。

アプリをインストールできませんでした

この一文の意味は一つだけです。すでに同じパッケージ名のアプリが入っていて、そちらが別の鍵で署名されていた、ということです。Android は署名が一致する場合にのみ上書きを許可するため、インストーラーはそれ以上何も言わずに諦めます。

  • 古い方をアンインストールしてからインストールします。端末側に保存されたデータも一緒に消えるので、テストしてもらっている人には事前に伝えておきましょう。

  • Development Build はユーザーフォルダ内の debug key で署名されます。そのファイルが消えると Unity は黙って作り直すため、古いビルドを持つ端末は新しい方を片端から拒否します。2 台目の PC、Unity の入れ直し、同僚のビルド、CI サーバーはすべてこの状態になります。

  • いたちごっこをやめるには、自分で保管する本物の keystore で署名します:Project Settings → Player → Publishing Settings → Custom Keystore。1 つのファイル、1 つの鍵、全マシン共通。そしてそれやパスワードを git にコミットしてはいけません。

terminal — the phone hides the reason, adb prints it
adb install Builds/game.apk
# Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Package com.yourstudio.yourgame
#   signatures do not match previously installed version; ignoring!]   -> a different key
# Failure [INSTALL_FAILED_VERSION_DOWNGRADE: Downgrade detected]        -> older versionCode
# Failure [INSTALL_FAILED_OLDER_SDK: ... is greater than the device ...] -> API too low
# Failure [INSTALL_FAILED_NO_MATCHING_ABIS: Failed to extract native libraries] -> wrong ABI

# what the phone actually is, and whether an old copy survived
adb shell getprop ro.build.version.sdk
adb shell getprop ro.product.cpu.abi
adb shell pm list packages | grep yourgame

adb uninstall com.yourstudio.yourgame
adb install Builds/game.apk

150 MB を超えるとき:AAB と APK

ここには 2 つのサイズ上限が登場しますが、どちらについてもスマホ側の話ではありません。だからこそ講座では両方のファイルを出力します。混同すると、自分は完璧に入ってもアップロードできない、あるいはアップロードできるのに端末が入れない、という状況になります。

  • 自分でスマホにコピーするファイルにサイズ制限は一切ありません。400 MB の APK でも普通に入ります。よく引用される数字はストア側の制約です。

  • Google Play は単一 APK を 100 MB、アプリバンドルの合計ダウンロードサイズを 150 MB までに抑えます(高度な圧縮最適化を有効にすれば約 1 GB)。つまり友達に渡すファイルは、Google に渡すものより大きくて構わないのです。

  • Build Profiles で Build App Bundle をオンにすると、Unity はどのスマホにも入れられない .aab を出力します。Google がサーバー側で端末別 APK に分割する前提の設計だからです。テスト用ファイルが欲しいならチェックを外して再ビルドしてください。

  • 大きなファイルは転送の段階でも壊れます。チャットアプリや不良ケーブル経由の APK は途中で切れやすく、Android はそれを無効なパッケージとして扱います。再ビルドの前に、両側のバイト数が完全に一致しているかを確認してください。

このバージョンはお使いのデバイスと互換性がありません

このメッセージは 3 つの設定のいずれかが生みますが、3 つともスマホ側ではなく Player Settings にあります。助かるのは、スマホが自分の数値を表示してくれること。つまりこれは推測ではなく比較で決まります。

  1. 1

    Minimum API Level が端末より高い場合。adb shell getprop ro.build.version.sdk を実行して比べます。Android 9 は 28、10 は 29、12 は 31、13 は 33、14 は 34。Player Settings 側が上回っていれば、これが原因です。24 にしておけば今も現役のスマホはほぼカバーできます。

  2. 2

    Target Architectures のチェックが逆。ARM64 のみのビルドを古い 32-bit 機に入れると INSTALL_FAILED_NO_MATCHING_ABIS になり、ARM 変換のない x86 エミュレーターでも同じです。特別な理由が無い限り ARMv7 と ARM64 の両方にチェックを——コストはビルド時間だけで、実行速度には響きません。

  3. 3

    Package Name が com.DefaultCompany.* のまま。帰結は 2 つ。端末側では、デフォルトのままの自分の全プロジェクトは Android にとって同一アプリなので、一つの席と一つの署名を奪い合います——つまり前出の不具合が少し早く訪ねてきたものです。ストア側では、この prefix は Unity そのものに属し何千人もがそのまま出荷しているため、自分が守れる ID になりません。

入ったのに、起動した瞬間に閉じる

この分類はインストール失敗ではありません。スマホは気持ちよくアプリを受け入れています。問題はゲームの側にあり、Android はそれをホーム画面への静かな復帰の裏に隠してしまいます。コマンド一つで表に出せます。

terminal — surface the reason
# clear last run, then watch only Unity's lines while you reproduce it
adb logcat -c
adb logcat -s Unity

# nothing under the Unity tag? then it is a native crash, not a C# one
adb logcat -d > full-log.txt
grep -iE "fatal|androidruntime|libc |SIGSEGV" full-log.txt

# what Android thinks you actually installed, in one shot
adb shell dumpsys package com.yourstudio.yourgame | grep -E "versionCode|minSdk|targetSdk"
  • ファイルと行番号を伴うスタックトレースは C# のエラーです。定番は NullReferenceException で、指摘された行はほぼ必ず、Inspector に何も代入していない参照です。

  • Unity の行が一切出ず libc や SIGSEGV のブロックだけなら、クラッシュはネイティブ側です。ローエンド端末でのメモリ不足か、ARM64 バイナリを同梱していないプラグインが典型です。

その次に、実機にしか存在しないエラーが来ます。外見は 3 つとも同じ——初フレームの NullReferenceException——なのに、中身はまったくの別物です。

  • Scene がビルドに追加されていないこと。エディタで開ける事に意味は無く、Build Profiles に載ったものだけが出荷されます。出荷されていない scene を LoadScene すると、イントロに立ったまま本来そこにあるはずだったものを参照しようとします。

  • エディタでしか動かない初期化。UNITY_EDITOR ブロックの中身は実機では空にコンパイルされ、Editor フォルダ内の script からのみ呼ばれる初期化も同様です。

  • StreamingAssets を System.IO.File で読むケース。スマホではそのフォルダはディスク上ではなく APK の内側にあるため、デスクトップでは完璧に動くのに File.ReadAllText は空を返します。

Device-safe way to read a file you shipped
using UnityEngine;
using UnityEngine.Networking;

public class LoadOnDevice : MonoBehaviour
{
    // File.ReadAllText works in the editor and returns nothing on Android:
    // StreamingAssets sits inside the APK there, not on the filesystem.
    IEnumerator Start()
    {
        string path = Application.streamingAssetsPath + "/levels.json";

        using var request = UnityWebRequest.Get(path);
        yield return request.SendWebRequest();

        if (request.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError($"Cannot read {path}: {request.error}");
            yield break;
        }

        var data = JsonUtility.FromJson<LevelList>(request.downloadHandler.text);
        Debug.Log($"Loaded {data.levels.Length} levels");
    }
}

4 つ目の実機専用原因は IL2CPP そのものです。Managed Stripping Level を Medium や High にすると、コンパイラから見て誰も呼んでいない型が削除されますが、それは reflection でしか辿れないもの —— JsonUtility.FromJson に渡す class、Activator.CreateInstance で作る型、Inspector の UnityEvent が文字列名で呼ぶメソッド —— にちょうど該当します。直す前に証明しましょう。Managed Stripping Level を Minimal にして再ビルドします。クラッシュが消えれば犯人は stripping です。link.xml を足してその型だけを名指しで守り、レベルは元に戻してください。Minimal のままでは、削れた容量と速度をそのまま返すことになります。

Assets/link.xml
<!-- Anywhere under Assets/ — Unity picks up every link.xml it finds. -->
<linker>
  <assembly fullname="Assembly-CSharp">
    <!-- keep what only reflection can reach: JSON classes, Activator targets -->
    <namespace fullname="Game.Data" preserve="all" />
    <type fullname="Game.Skills.Fireball" preserve="all" />
  </assembly>
  <assembly fullname="Newtonsoft.Json" preserve="all" />
</linker>

ビルドが最後まで終わらない

ファイルが一切出てこないなら、エラー別の一覧をたどるのはやめてください。インストールの問題ではありません。ほぼ全ケースが以下の 4 つで、講座の Android 準備レッスンがインストールそのものを順番に扱っています。

  • モジュール不足。Android Build Support だけでは足りません。その下位項目である Android SDK & NDK Tools と OpenJDK は Unity Hub の Installs、Add Modules 内の 2 項目で、既定ではチェックが入っていません。

  • パスが空。Unable to locate Android SDK は SDK 欠如を意味することはほとんど無く、Preferences の External Tools で Android のパスが空欄なだけです。3 つの Use Installed にチェックすれば勝手に正しい場所を指し直します。

  • Gradle の赤い文字の壁。上へスクロールして最初の What went wrong を探してください。それがエラー本体で、その下は Gradle が同じ話を繰り返しているだけです。大半は、要求する API level が食い違う 2 つのプラグイン、あるいは 2 回含まれたライブラリです。

  • 凍結ではなく IL2CPP のせいです。最初のビルドは C# をすべて C++ に変換してコンパイルするため、10〜20 分は普通です。Unity を落としてやり直す前に、ウィンドウ右下の進捗バーを確認してください。

60 秒で終わる確認順

  • ✓ファイルは本当に .apk で終わるか、バイト数は PC のものと完全に一致するか。チャットアプリは名前を変え、途中で切り捨てることもよくあります。
  • ✓Install unknown apps は、実際にファイルをタップしたアプリに対してオンか——使っていないファイルマネージャーではないか?
  • ✓adb shell getprop ro.build.version.sdk の値が Minimum API Level 以上か?
  • ✓adb shell getprop ro.product.cpu.abi は arm64-v8a か armeabi-v7a か——その一方は Target Architectures でチェックされているか?
  • ✓adb uninstall で該当パッケージを消してから adb install し、Failure の行を読む——原因がずばり書かれています。
  • ✓入るのに閉じるなら、まず adb logcat -s Unity。次に Managed Stripping Level を Minimal で再ビルドし、stripping の疑いを外す。
  • ✓それでもファイルが出ないなら、Preferences の External Tools で Android の 3 つのパスが空でないか確認する。

インストールできて起動したままになるなら、次に壊れるのはファイルではなくフレームレートです。その流れは プロジェクトから APK へのガイド が順番に、セットアップから扱っています。

これを作るレッスン

ビルド、インストール、実機でのデバッグ

この記事はそれだけで完結するレシピです。コースでは同じものを、5 つのタブを貫くひとつのプロジェクトの一部として作ります — モバイルへ書き出す のレッスン 05。