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
打开 ,选中你点这个文件时所在的那个 app —— Chrome、Files by Google、Zalo,别靠猜。然后进 ,打开 Allow from this source。
- 2
MIUI 和 HyperOS 会在上面再加一屏:它要求你登录 Mi 账号,扫描时倒计时,看不上这个文件时就只给你一句 Declined due to system restrictions。在手机自带的“安全中心”里关掉 Scan apps for viruses 就不会再被这样拒。
- 3
三星的做法是跑一遍 Play Protect 扫描:打开 Google Play,点头像,进 Settings、Notifications,然后关掉 Scan apps with Play Protect。但有一种情况任何设置都赢不了:装了公司工作资料的手机,策略上就是禁止侧载。
- 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。
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
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
Target Architectures 勾错了。只带 ARM64 的构建放到老款 32 位手机上就会报 INSTALL_FAILED_NO_MATCHING_ABIS,没有 ARM 翻译层的 x86 模拟器也一样。除非有明确理由,否则 ARMv7 和 ARM64 都勾上 —— 代价是构建时间,不是运行速度。
- 3
Package Name 还留着 com.DefaultCompany.*。后果有两个。在手机上:你所有仍用默认值的项目,在 Android 眼里都是同一个 app,于是它们抢同一个位置、同一份签名 —— 这是上一节的毛病提前找上门。在商店里:这个前缀属于 Unity 自己,成千上万人都拿它出货,所以它并不是一个你守得住的身份。
装上了,但一打开就退回去
这一组里没有任何一条属于安装失败 —— 手机痛痛快快地收下了你的应用。问题出在游戏里面,而 Android 把它藏在一次静默退回桌面背后。一条命令就能把它揪出来。
# 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 只会给你空的,而它在桌面上却一直好使。
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 等于把省下来的体积和速度原样还回去。
<!-- 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 课。