U
03 / 22 · 11 分钟
常见问题

APK 装不上手机:每条报错逐个讲原因

简短回答

为什么 Unity 构建的 APK 装不到手机上?

手机拒绝 Unity 构建的原因就那么几种,而每一种都会印出不同的一句话:你点文件的那个 app 没拿到安装权限、机里还留着另一把密钥签名的旧版、Minimum API Level 或 ARM64 设得比设备还高、或者文件传过来时被截断了。自己跑一次 adb install,读 Failure 那行,再把设备的 API level 和 abi 跟 Player Settings 对一遍。

Unity 报告 Build completed,手机却直接拒绝。这不是一个问题而是六个,而且每个都会用不同的一句话来报信。下面是 Android 拒绝 Unity 构建的全部方式、每句话的真实含义,以及只需要改那一处的解法。

安装之前:先放行未知来源

Android 8 去掉了 Unknown sources 那个总开关,改成按 app 单独授权。现在没有任何一处能一次允许全部 —— 你要把安装权限授给你刚才点这个文件的那个 app。

  1. 1

    打开 Settings▸Apps,选中你点这个文件时所在的那个 app —— Chrome、Files by Google、Zalo,别靠猜。然后进 Special app access▸Install unknown apps,打开 Allow from this source。

  2. 2

    MIUI 和 HyperOS 会在上面再加一屏:它要求你登录 Mi 账号,扫描时倒计时,看不上这个文件时就只给你一句 Declined due to system restrictions。在手机自带的“安全中心”里关掉 Scan apps for viruses 就不会再被这样拒。

  3. 3

    三星的做法是跑一遍 Play Protect 扫描:打开 Google Play,点头像,进 Settings、Notifications,然后关掉 Scan apps with Play Protect。但有一种情况任何设置都赢不了:装了公司工作资料的手机,策略上就是禁止侧载。

  4. 4

    从电脑用 USB 安装还需要一个单独的开关 Install via USB,就在 Developer options 里 —— 而 MIUI 还会额外要求你登录 Mi 账号并联网,才肯放开这个开关。

应用未安装

这句话只有一个意思:手机里已经装了一个跟你同包名的应用,而且那份是用另一把密钥签名的。Android 只允许签名一致的应用互相覆盖,所以安装器直接放弃,不会再给你任何有用的提示。

  • 先卸载旧的再装。它存在手机里的数据也会一起没了,所以在动手前先跟帮你测试的人说一声。

  • Development Build 用的是你用户目录里那把 debug key。这个文件一旦没了,Unity 会不声不响地再生成一把,于是所有还装着旧版的手机从此拒绝新版。换第二台电脑、重装 Unity、同事帮你构建、或者走 CI,出来的全是这同一种毛病。

  • 想让它不再复发,就用一把你自己留着、大家共用的正式 keystore 来签名:Project Settings、Player、Publishing Settings、Custom Keystore。一个文件、一把密钥、所有机器共用 —— 而且绝对不要把它或密码提交进 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

这件事里有两个大小限制,而它们都跟你的手机无关 —— 这就是课程要把两种文件都构建一遍的原因。搞混的结果要么是装得好好的却传不上去,要么是传得上去却没有手机能装。

  • 你自己拷进手机的文件完全没有任何大小限制。400 MB 的 APK 照样装得上。人们常引用的那些数字,是商店的限制。

  • Google Play 把单个 APK 卡在 100 MB,把 app bundle 的总下载量卡在 150 MB,开启高级瘦身的话能放宽到大约 1 GB。也就是说,发给朋友的文件可以比交给 Google 的更大。

  • 在 Build Profiles 里勾上 Build App Bundle,Unity 就会输出一个没有任何手机能安装的 .aab —— 这是设计如此,因为 Google 会在自己的服务器上把它拆成各机型专属的 APK。想要能测的文件,就取消勾选再构建一次。

  • 大文件还会坏在传输环节。通过聊天软件或不稳定的数据线推过去的 APK 常常只到一半,Android 会把这样得到的东西报成无效的包。重建之前,先对一下两台机器上的字节数是不是一模一样。

您的设备与该版本不兼容

会触发这句话的设置共有三处,三处全在 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 位手机上就会报 INSTALL_FAILED_NO_MATCHING_ABIS,没有 ARM 翻译层的 x86 模拟器也一样。除非有明确理由,否则 ARMv7 和 ARM64 都勾上 —— 代价是构建时间,不是运行速度。

  3. 3

    Package Name 还留着 com.DefaultCompany.*。后果有两个。在手机上:你所有仍用默认值的项目,在 Android 眼里都是同一个 app,于是它们抢同一个位置、同一份签名 —— 这是上一节的毛病提前找上门。在商店里:这个前缀属于 Unity 自己,成千上万人都拿它出货,所以它并不是一个你守得住的身份。

装上了,但一打开就退回去

这一组里没有任何一条属于安装失败 —— 手机痛痛快快地收下了你的应用。问题出在游戏里面,而 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,说明崩溃在 native 层:通常是低端机内存耗尽,或者某个插件没带 ARM64 的二进制。

然后是那类只在真机上才存在的错误。其中三种外表完全一样 —— 第一帧就抛 NullReferenceException —— 底下却毫无关系。

  • 这个 scene 从没被加进构建里。能在编辑器里打开说明不了任何事;只有列在 Build Profiles 里的才会跟着走。对一个没进构建的 scene 调 LoadScene,你就会留在开场场景里,去引用一个本该在那儿却不存在的东西。

  • 只在编辑器里跑的初始化。写在 UNITY_EDITOR 块里的任何东西,到真机上会被编译成什么都没有;只在 Editor 文件夹里的脚本中调用一次的那个 init 也一样。

  • 用 System.IO.File 去读 StreamingAssets。在手机上那个文件夹是在 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");
    }
}

第四种只在真机上出现的原因是 IL2CPP 自己。Managed Stripping Level 设成 Medium 或 High 时,它会删掉编译器看不到有人调用的类型,而这恰恰就是走 reflection 的那批东西:你交给 JsonUtility.FromJson 的 class、用 Activator.CreateInstance 造出来的类型,以及 Inspector 里 UnityEvent 按字符串名字调用的每一个方法。先证明再修:把 Managed Stripping Level 改成 Minimal 再构建一次。闪退没了,就说明是裁剪干的 —— 然后加一个 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>

构建根本没跑完

如果压根没有产出文件,就别再按报错清单一条条查了 —— 这不是安装问题。几乎每种情况都落在下面四条里,课程里的 Android 准备课正是按顺序讲安装本身那部分的。

  • 模块没装全。只有 Android Build Support 还不够:Android SDK & NDK Tools 和 OpenJDK 是它在 Unity Hub 的 Installs、Add Modules 里的两个子项,默认都是没勾的。

  • 路径是空的。Unable to locate Android SDK 几乎从不真的意味着缺 SDK —— 而是 Preferences、External Tools 里的 Android 路径空着。勾上那三个 Use Installed,它们自己就会指回正确位置。

  • Gradle 的一片红字。往上滚到第一个 What went wrong —— 那才是错误本身,下面全是 Gradle 在重复自己。其中一大半是两个 plugin 要的 API level 不一致,或者同一个库被引了两次。

  • 它没卡死,是 IL2CPP。第一次构建要把你所有 C# 翻译成 C++ 再编译,10 到 20 分钟很正常。在你强行关掉 Unity 重来之前,先看一眼窗口右下角的进度条。

60 秒检查顺序

  • ✓文件真的以 .apk 结尾吗?字节数跟电脑上那份一模一样吗?聊天软件会改名、会截断。
  • ✓Install unknown apps 是不是开在你点这个文件的那个 app 上——而不是开在你压根没用到的文件管理器上?
  • ✓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 重建一次,排除掉裁剪。
  • ✓还是没有产出文件?去 Preferences 的 External Tools 里看,三个 Android 路径一个都不能空。

既然装得上、打开也不会秒退了,下一个出问题的就不是文件而是帧率 —— 从项目到 APK 的那篇指南 按顺序讲完整条流程,先从配置开始。

做出这个的那一课

构建、安装并在真机上调试

这篇指南单独成篇,是一份可以直接照着做的配方。在课程里,同样的东西会作为贯穿全部五个章的那个项目的一部分来搭建 —— 导出到手机,第 05 课。