Build game Unity ra APK không cài được: lỗi nào nguyên nhân nấy
Vì sao build game Unity ra APK không cài được trên điện thoại?
Điện thoại từ chối bản build Unity vì một trong vài lý do, và mỗi lý do in ra một câu khác nhau: app bạn bấm vào file chưa được cấp quyền cài, máy còn bản cũ ký bằng khoá khác, Minimum API Level hoặc ARM64 đặt cao hơn khả năng của máy, hoặc file về bị cắt cụt. Tự chạy adb install, đọc dòng Failure, rồi so API level và abi của máy với Player Settings.
Unity báo Build completed, còn điện thoại thì từ chối. Đó không phải một vấn đề mà là sáu, và mỗi cái tự thông báo bằng một câu khác nhau. Dưới đây là mọi cách Android có thể nói không với bản build Unity, ý nghĩa thật của từng câu, và đúng một thay đổi để qua.
Trước khi cài: bật Unknown sources
Android 8 đã bỏ công tắc Unknown sources tổng và thay bằng quyền cấp riêng cho từng app. Không còn chỗ nào cho phép tất cả nữa — bạn phải cấp quyền cài cho đúng cái app bạn vừa bấm vào file.
- 1
Mở và chọn đúng cái app bạn bấm vào file — Chrome, Files by Google, Zalo — đừng đoán. Rồi và bật Cho phép từ nguồn này.
- 2
MIUI và HyperOS đặt thêm một màn hình nữa: nó bắt bạn đăng nhập tài khoản Mi, đếm ngược trong lúc quét, và khi không vừa ý thì câu duy nhất hiện ra là Declined due to system restrictions. Tắt Scan apps for viruses trong app Bảo mật là hết cảnh bị từ chối đó.
- 3
Samsung thì chạy một lượt quét của Play Protect: Google Play, bấm ảnh đại diện, Settings, Notifications, rồi tắt Scan apps with Play Protect. Có một thứ mà không thiết lập nào thắng nổi: máy đang gắn hồ sơ công ty thì chính sách chặn cài ngoài store.
- 4
Cài từ máy tính qua USB cần công tắc riêng là Install via USB trong Tuỳ chọn nhà phát triển — MIUI còn đòi bạn đăng nhập tài khoản Mi và có mạng thì mới mở khoá được công tắc này.
Ứng dụng chưa được cài đặt
Câu này chỉ có một nghĩa: điện thoại đã có app cùng package name với bạn, và bản đó ký bằng khoá khác. Android không cho app này đè lên app khác nếu hai bên khác chữ ký, nên bộ cài bỏ cuộc và không nói thêm gì hữu ích.
Gỡ bản cũ rồi hãy cài. Mọi dữ liệu nó giữ trên máy cũng đi theo, nên báo trước cho người đang thử giúp bạn.
Bản Development Build ký bằng debug key nằm trong thư mục người dùng của bạn. Mất file đó thì Unity lặng lẽ tạo cái mới, và mọi điện thoại đang giữ bản cũ sẽ từ chối bản mới. Máy tính thứ hai, Unity cài lại, build của đồng nghiệp hay của CI đều cho ra đúng lỗi này.
Để nó không quay lại, hãy ký bằng keystore thật do bạn giữ: Project Settings, Player, Publishing Settings, Custom Keystore. Một file, một khoá, mọi máy — và đừng bao giờ commit file đó hay mật khẩu lên git.
adb install Builds/game.apk
# Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: ...] -> khác chữ ký
# Failure [INSTALL_FAILED_VERSION_DOWNGRADE: ...] -> versionCode thấp hơn bản đang có
# Failure [INSTALL_FAILED_OLDER_SDK: ...] -> Android của máy thấp hơn Minimum API Level
# Failure [INSTALL_FAILED_NO_MATCHING_ABIS: ...] -> sai kiến trúc CPU
# máy này thật ra là gì, và bản cũ còn sót lại không
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.apkTrên 150 MB: AAB hay APK
Có hai giới hạn kích thước trong chuyện này, và không cái nào liên quan tới điện thoại của bạn — đó chính là lý do khoá học phát hành cả hai loại file. Nhầm chúng với nhau dẫn tới bản build cài ngon nhưng không tải lên được, hoặc bản tải lên được thì không máy nào cài được.
File bạn tự chép vào điện thoại không có giới hạn kích thước nào cả. APK 400 MB vẫn cài bình thường. Những con số người ta hay trích dẫn là giới hạn của store.
Google Play chặn một APK ở 100 MB và tổng kích thước tải về của app bundle ở 150 MB, nới lên cỡ 1 GB nếu bật chế độ nén nâng cao. Nghĩa là file đưa cho bạn bè được phép to hơn file đưa cho Google.
Tick Build App Bundle trong Build Profiles thì Unity xuất file .aab mà không điện thoại nào cài được — đúng như thiết kế, vì Google tự cắt nó thành APK riêng cho từng máy trên server của họ. Còn muốn có file để thử thì bỏ tick và build lại.
File lớn cũng hỏng ở khâu vận chuyển. APK đẩy qua app chat hoặc cáp kém hay bị cắt cụt, và Android báo kết quả là package không hợp lệ. So đúng số byte ở hai máy trước khi build lại mọi thứ.
Thiết bị của bạn không tương thích với phiên bản này
Ba thiết lập tạo ra câu này, cả ba đều nằm trong Player Settings chứ không phải trên điện thoại. Phần dễ chịu là điện thoại sẽ tự in ra số của nó, nên đây là phép so sánh chứ không phải đoán mò.
- 1
Minimum API Level cao hơn máy. Chạy adb shell getprop ro.build.version.sdk rồi so sánh: Android 9 là 28, 10 là 29, 12 là 31, 13 là 33, 14 là 34. Nếu con số trong Player Settings lớn hơn thì đúng là lỗi này. Mức 24 phủ gần như mọi điện thoại còn đang dùng.
- 2
Target Architectures tick sai. Bản chỉ có ARM64 đưa lên điện thoại 32-bit đời cũ sẽ ra INSTALL_FAILED_NO_MATCHING_ABIS, và emulator x86 không có lớp dịch ARM cũng vậy. Tick cả ARMv7 và ARM64 trừ khi có lý do riêng — tốn thời gian build chứ không tốn tốc độ lúc chạy.
- 3
Package name còn để com.DefaultCompany.*. Hai hệ quả. Trên điện thoại: mọi dự án của bạn vẫn để mặc định đều là cùng một app với Android, nên chúng giành nhau một chỗ và một chữ ký — đó là mục trước đến sớm hơn. Trên store: prefix đó thuộc về Unity và hàng nghìn người đang dùng nó, nên đó không phải định danh bạn bảo vệ được.
Cài được, nhưng vừa mở là đóng
Không có gì trong nhóm này là lỗi cài đặt — điện thoại đã nhận app của bạn, rất sẵn lòng. Vấn đề nằm ở game, còn Android giấu nó sau một cú quay về màn hình lặng lẽ. Một lệnh là kéo nó ra lại.
# xoá log lần trước, rồi chỉ nhìn dòng của Unity trong lúc bị lại
adb logcat -c
adb logcat -s Unity
# không có dòng Unity nào? vậy là crash ở tầng native, không phải C#
adb logcat -d > full-log.txt
grep -iE "fatal|androidruntime|libc |SIGSEGV" full-log.txt
# Android nghĩ bạn đã cài cái gì, xem một lần là xong
adb shell dumpsys package com.yourstudio.yourgame | grep -E "versionCode|minSdk|targetSdk"Dấu vết nêu tên file và số dòng là lỗi C#. Thường gặp nhất là NullReferenceException, và dòng nó chỉ ra gần như luôn là một reference bạn chưa kéo gì vào trong Inspector.
Hoàn toàn không có dòng Unity nào, chỉ có khối libc hoặc SIGSEGV, nghĩa là crash ở tầng native: thường là hết RAM trên máy yếu, hoặc plugin không có bản ARM64.
Rồi đến những lỗi chỉ tồn tại trên máy. Ba trong số đó nhìn ngoài giống hệt nhau — một NullReferenceException ngay khung hình đầu tiên — mà bên dưới thì chẳng liên quan gì nhau.
Scene chưa từng được thêm vào build. Mở được scene trong editor không có nghĩa gì cả; chỉ những gì nằm trong Build Profiles mới đi theo. Gọi LoadScene cho scene không có trong build khiến bạn đứng lại ở scene đầu và trỏ vào thứ lẽ ra phải ở đó.
Phần khởi động chỉ chạy trong editor. Mọi thứ trong khối UNITY_EDITOR biên dịch thành không trên điện thoại, và một lệnh init chỉ được gọi từ script nằm trong folder Editor cũng vậy.
Đọc StreamingAssets bằng System.IO.File. Trên điện thoại thư mục đó nằm trong APK chứ không nằm trên đĩa, nên File.ReadAllText trả về rỗng trong khi chạy hoàn hảo trên máy tính.
using UnityEngine;
using UnityEngine.Networking;
public class LoadOnDevice : MonoBehaviour
{
// File.ReadAllText chạy được trong editor nhưng trả về rỗng trên Android:
// ở đó StreamingAssets nằm bên trong APK, không nằm trên đĩa.
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($"Không đọc được {path}: {request.error}");
yield break;
}
var data = JsonUtility.FromJson<LevelList>(request.downloadHandler.text);
Debug.Log($"Đã nạp {data.levels.Length} màn");
}
}Nguyên nhân thứ tư chỉ có trên máy lại chính là IL2CPP. Managed Stripping Level Medium hoặc High xoá những kiểu mà trình biên dịch không thấy ai gọi, mà đó lại đúng là hoàn cảnh của mọi thứ đi qua reflection: một class bạn đưa vào JsonUtility.FromJson, thứ được dựng bằng Activator.CreateInstance, và mọi hàm mà UnityEvent trong Inspector gọi theo tên bằng chuỗi. Hãy chứng minh trước khi sửa: đặt Managed Stripping Level về Minimal rồi build lại. Nếu hết văng thì đúng là do stripping — khi đó thêm file link.xml nêu đích danh mấy kiểu đó rồi nâng mức lên lại, vì cứ để Minimal là trả lại nguyên phần dung lượng và tốc độ đã tiết kiệm.
<!-- Đặt ở bất kỳ đâu dưới Assets/ — Unity gom mọi file link.xml nó tìm thấy. -->
<linker>
<assembly fullname="Assembly-CSharp">
<!-- giữ lại những thứ chỉ reflection mới tới được: class JSON, đích của Activator -->
<namespace fullname="Game.Data" preserve="all" />
<type fullname="Game.Skills.Fireball" preserve="all" />
</assembly>
<assembly fullname="Newtonsoft.Json" preserve="all" />
</linker>Bản build không chạy xong
Nếu hoàn toàn không có file nào, đừng lần danh sách theo lỗi nữa — đây không phải vấn đề khi cài. Gần như mọi trường hợp nằm trong bốn cái dưới đây, và bài học chuẩn bị Android trong khoá đi qua phần cài đặt theo đúng thứ tự.
Thiếu module. Riêng Android Build Support không đủ: Android SDK & NDK Tools và OpenJDK là hai mục con của nó trong Unity Hub, Installs, Add Modules, và mặc định chúng không được tick.
Đường dẫn trống. Unable to locate Android SDK gần như không bao giờ nghĩa là thiếu SDK — mà là Preferences, External Tools đang để đường dẫn Android trống. Tick ba ô Use Installed là chúng tự trỏ về đúng chỗ.
Tường chữ đỏ của Gradle. Kéo lên tìm dòng What went wrong đầu tiên — đó mới là lỗi, mọi thứ phía dưới chỉ là Gradle lặp lại chính nó. Phần lớn là hai plugin đòi hai mức API khác nhau, hoặc một thư viện bị đưa vào hai lần.
Không phải bị đơ, mà là IL2CPP. Lần build đầu dịch toàn bộ C# của bạn sang C++ rồi biên dịch, nên 10–20 phút là bình thường. Nhìn thanh progress ở góc dưới bên phải cửa sổ trước khi tắt Unity và làm lại.
Thứ tự kiểm tra 60 giây
- ✓File có thật sự kết thúc bằng .apk không, và số byte có đúng bằng bản trên máy tính không? App chat đổi tên và cắt file đấy.
- ✓Install unknown apps đã bật cho đúng cái app bạn bấm vào file — không phải cho app quản lý file bạn không dùng?
- ✓adb shell getprop ro.build.version.sdk — lớn hơn hoặc bằng Minimum API Level của bạn chứ?
- ✓adb shell getprop ro.product.cpu.abi — ra arm64-v8a hay armeabi-v7a, và kiến trúc đó có được tick trong Target Architectures không?
- ✓adb uninstall gói đó rồi adb install, và đọc dòng Failure — nó nêu thẳng nguyên nhân.
- ✓Cài được nhưng đóng ngay: chạy adb logcat -s Unity trước, rồi build lại với Managed Stripping Level ở Minimal để loại trừ stripping.
- ✓Vẫn không có file nào? Preferences, External Tools: cả ba đường dẫn Android đều không được trống.
Cài được và mở lên không đóng rồi thì thứ hỏng kế tiếp là tốc độ khung hình chứ không phải file — và bài từ dự án tới file APK đi qua toàn bộ luồng đó theo thứ tự, bắt đầu từ cấu hình.
bài học làm ra thứ này
Build, cài và gỡ lỗi trên máy thật
Bài này là một công thức đứng riêng. Trong khoá học, cùng thứ đó được dựng như một phần của dự án xuyên suốt năm tab — Xuất lên điện thoại, bài 05.