
1. 项目概述为什么Unity需要调用安卓安装APK在Unity开发中尤其是涉及应用内更新、游戏分包下载、或者需要引导用户安装合作伙伴应用时我们经常会遇到一个核心需求如何从Unity构建的安卓应用内部直接拉起系统安装器去安装另一个APK文件这个需求听起来简单但Unity本身作为一个跨平台引擎并没有提供一个现成的、安全的API来直接执行APK安装。它只负责渲染你的游戏世界而安装应用这种高度依赖操作系统权限和流程的操作必须通过安卓原生Android侧来完成。这就引出了我们今天要深入探讨的核心技术Unity与Android原生代码的无缝对接。这不仅仅是调用一个方法那么简单它涉及到UnityC#与AndroidJava/Kotlin之间的通信桥梁AndroidJavaClass/AndroidJavaObject、安卓系统的文件权限管理FileProvider、以及安装意图Intent的构建与发送。掌握这套流程意味着你能够突破Unity引擎的边界深度集成安卓系统的原生能力从而为你的应用增添诸如热更新包安装、推广其他应用、甚至构建自己的应用商店模块等强大功能。对于任何一位希望在安卓平台深耕的Unity开发者来说这都是必须掌握的“利器”。2. 核心原理与架构设计2.1 Unity与Android通信机制解析Unity调用安卓功能其本质是C#脚本通过一个名为AndroidJavaClass和AndroidJavaObject的接口与运行在同一个进程内的Android RuntimeART中的Java/Kotlin类进行交互。你可以把这个过程想象成UnityC#世界和安卓系统Java世界之间的一座桥梁。AndroidJavaClass用于访问Java/Kotlin中的静态类static class及其静态方法static method。例如我们要使用安卓的Intent类它在C#侧就对应一个AndroidJavaClass对象。AndroidJavaObject用于实例化一个Java/Kotlin类的对象并调用该对象的实例方法。例如我们创建一个Intent对象后需要调用它的setDataAndType、addFlags等方法。这套机制被称为“Android Java Native Interface (JNI) Bridge”。Unity在构建APK时会将你的C#脚本编译成IL2CPP或Mono并与一个基础的安卓Activity默认是UnityPlayerActivity打包在一起。你的C#代码和Java代码最终运行在同一个应用进程内因此可以通过JNI进行高效通信。2.2 APK安装的系统流程与权限要求在安卓系统中安装一个APK文件不是一个简单的文件复制操作而是一个由系统包管理器PackageManager负责的安全流程。核心步骤是应用发起一个包含特定动作ACTION_VIEW或ACTION_INSTALL_PACKAGE和文件URI的Intent系统会捕获这个Intent弹出安装确认界面由用户决定是否安装。这里的关键挑战在于文件访问权限。从Android 7.0 (Nougat, API 24) 开始谷歌引入了“StrictMode”政策禁止应用通过file://URI直接暴露内部文件给其他应用包括系统安装器。取而代之的是必须使用FileProvider。FileProvider是ContentProvider的一个特殊子类它允许你安全地共享应用私有目录下的文件。你需要在AndroidManifest.xml中声明一个FileProvider。在res/xml目录下创建一个XML文件定义哪些路径的文件可以被共享。使用FileProvider.getUriForFile()方法生成一个content://格式的URI这个URI才是安全且系统认可的。注意如果你的minSdkVersion低于24理论上仍可使用file://URI但为了应用的兼容性和安全性强烈建议对所有API级别的设备都统一使用FileProvider方案。这是当前安卓开发的最佳实践。2.3 整体方案设计基于以上原理我们的技术方案可以拆解为以下步骤Unity侧C#负责逻辑触发如点击按钮、APK文件的管理下载、存储到持久化路径、以及通过AndroidJavaObject调用我们编写的原生插件方法。Android侧Java/Kotlin编写一个原生插件接收来自Unity的调用。该插件需要检查是否有安装未知应用的权限针对Android 8.0。使用FileProvider为指定的APK文件生成安全的Content URI。构建一个正确的Intent并设置动作、数据URI、类型application/vnd.android.package-archive和必要的Flags。使用当前Activity的上下文启动这个Intent。这个设计将平台相关的复杂逻辑封装在原生插件中Unity侧只需进行简单的调用保持了代码的清晰和可维护性。3. 实操步骤构建Unity与Android的桥梁3.1 环境准备与工具确认在开始编码前请确保你的开发环境已就绪Unity版本2019.4 LTS或更新版本推荐使用LTS版本以获得最佳稳定性。在File - Build Settings - Android中确认平台已切换。Android开发环境JDK安装Java Development Kit (JDK) 8或11。Unity Hub通常自带OpenJDK但也可以指向你自己安装的JDK路径Preferences - External Tools。Android SDK NDK通过Unity Hub安装或指定路径。确保SDK Platforms中包含了你的目标API级别如Android 11.0 (API 30)。GradleUnity默认使用其内置的Gradle进行构建。确保Build Settings - Player Settings - Publishing Settings - Build中Build System选择的是Gradle这是当前推荐且功能更完整的构建系统。代码编辑器用于编写Java代码如Android Studio或IntelliJ IDEA。虽然可以用文本编辑器但IDE能提供语法高亮、自动补全和更便捷的编译功能。3.2 创建Android原生插件Java层这是整个流程的核心。我们将在Unity项目中创建一个Android插件。步骤1在Unity项目中创建插件目录结构在你的Unity项目Assets文件夹下创建如下目录Assets/Plugins/Android。这是Unity识别Android插件的标准路径。步骤2编写Java插件类在Assets/Plugins/Android目录下创建一个新的Java文件例如ApkInstaller.java。package com.yourcompany.unityplugin; // 替换成你自己的包名 import android.app.Activity; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.provider.Settings; import androidx.core.content.FileProvider; import java.io.File; public class ApkInstaller { // 定义一个静态常量用于请求安装未知应用权限的请求码 private static final int REQUEST_CODE_INSTALL 10001; // 存储当前Activity的静态引用用于权限回调 private static Activity currentActivity null; // Unity调用的入口方法 public static void InstallApk(final Activity activity, final String apkFilePath) { currentActivity activity; final Context context activity.getApplicationContext(); final File apkFile new File(apkFilePath); // 检查文件是否存在 if (!apkFile.exists()) { UnityPlayer.UnitySendMessage(YourGameObjectName, OnInstallFailed, APK file does not exist: apkFilePath); return; } // 针对Android 8.0 (API 26) 及以上版本需要检查“安装未知应用”权限 if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { if (!context.getPackageManager().canRequestPackageInstalls()) { // 没有权限跳转到设置页面引导用户开启 Intent intent new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES); intent.setData(Uri.parse(package: context.getPackageName())); activity.startActivityForResult(intent, REQUEST_CODE_INSTALL); // 注意这里不能直接安装需要等待权限申请结果 return; } } // 拥有权限执行安装 performInstall(context, apkFile); } // 处理权限申请结果的回调需要在Unity的Activity中重写onActivityResult并调用此方法 public static void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode REQUEST_CODE_INSTALL currentActivity ! null) { Context context currentActivity.getApplicationContext(); // 重新检查权限 if (Build.VERSION.SDK_INT Build.VERSION_CODES.O || context.getPackageManager().canRequestPackageInstalls()) { // 用户已授权可以再次尝试安装这里需要重新获取文件路径实际项目中应缓存路径 // 例如performInstall(context, cachedApkFile); UnityPlayer.UnitySendMessage(YourGameObjectName, OnPermissionGranted, ); } else { UnityPlayer.UnitySendMessage(YourGameObjectName, OnInstallFailed, User denied the install permission.); } } } // 执行实际的安装操作 private static void performInstall(Context context, File apkFile) { try { // 使用FileProvider获取安全的URI Uri apkUri; if (Build.VERSION.SDK_INT Build.VERSION_CODES.N) { // 参数1: Context, 参数2: FileProvider在Manifest中定义的authorities, 参数3: File对象 apkUri FileProvider.getUriForFile(context, context.getPackageName() .fileprovider, apkFile); } else { // 低版本使用file:// URI (不推荐仅作兼容) apkUri Uri.fromFile(apkFile); } Intent intent new Intent(Intent.ACTION_VIEW); intent.setDataAndType(apkUri, application/vnd.android.package-archive); // 必须添加这些Flags否则在Android 7.0上会报错 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); // 通知Unity安装器已成功拉起 UnityPlayer.UnitySendMessage(YourGameObjectName, OnInstallStarted, ); } catch (Exception e) { e.printStackTrace(); UnityPlayer.UnitySendMessage(YourGameObjectName, OnInstallFailed, e.getMessage()); } } }步骤3创建FileProvider的配置文件在Assets/Plugins/Android目录下创建子文件夹res/xml如果不存在则创建。然后在xml文件夹内创建一个文件命名为file_paths.xml。?xml version1.0 encodingutf-8? paths xmlns:androidhttp://schemas.android.com/apk/res/android !-- 对应Unity的持久化数据路径/storage/emulated/0/Android/data/your.package.name/files -- external-files-path nameexternal_files path. / !-- 如果APK存放在其他位置可以添加更多路径 -- !-- cache-path namecache path. / -- !-- external-cache-path nameexternal_cache path. / -- /paths这个文件定义了FileProvider可以共享的文件路径。external-files-path对应的是Application.persistentDataPath在安卓上的物理路径。步骤4修改AndroidManifest.xmlUnity在构建时会合并所有插件中的AndroidManifest.xml。我们需要在Assets/Plugins/Android目录下创建一个AndroidManifest.xml文件如果已有则在其内部添加来声明FileProvider和必要的权限。?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.yourcompany.unityplugin !-- 声明安装未知应用的权限用于Android 8.0 -- uses-permission android:nameandroid.permission.REQUEST_INSTALL_PACKAGES / application !-- 声明FileProvider -- provider android:nameandroidx.core.content.FileProvider android:authorities${applicationId}.fileprovider android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/file_paths / /provider /application /manifest关键点android:authorities属性通常设置为${applicationId}.fileprovider。${applicationId}会在构建时被替换成你游戏的包名Bundle Identifier这确保了Provider的唯一性。3.3 Unity C#调用层实现现在我们需要在Unity中编写C#脚本来调用刚才创建的Java插件。创建一个C#脚本例如ApkInstallManager.csusing UnityEngine; using System.IO; using System.Runtime.InteropServices; public class ApkInstallManager : MonoBehaviour { // 定义接收Java回调的GameObject名称需与Java代码中UnitySendMessage的第一个参数一致 public string callbackTargetName “YourGameObjectName”; // 声明导入Java方法 #if UNITY_ANDROID !UNITY_EDITOR private static AndroidJavaClass _pluginClass; private static AndroidJavaObject _currentActivity; private static void InitAndroidPlugin() { if (_pluginClass null) { // 参数必须是Java类的完整包名 _pluginClass new AndroidJavaClass(“com.yourcompany.unityplugin.ApkInstaller”); } if (_currentActivity null) { AndroidJavaClass unityPlayer new AndroidJavaClass(“com.unity3d.player.UnityPlayer”); _currentActivity unityPlayer.GetStaticAndroidJavaObject(“currentActivity”); } } #endif // 供Unity调用的公开方法 public void InstallApk(string relativeApkPath) { #if UNITY_ANDROID !UNITY_EDITOR InitAndroidPlugin(); // 构建完整的APK文件路径。假设APK存放在PersistentDataPath下 string fullPath Path.Combine(Application.persistentDataPath, relativeApkPath); Debug.Log($“[ApkInstallManager] Attempting to install APK at: {fullPath}”); // 调用Java静态方法 _pluginClass.CallStatic(“InstallApk”, _currentActivity, fullPath); #else Debug.LogWarning(“[ApkInstallManager] APK installation is only supported on Android platform.”); #endif } // —————— 以下方法由Java层通过UnitySendMessage调用 —————— // 安装流程开始 void OnInstallStarted(string message) { Debug.Log(“[ApkInstallManager] System installer launched successfully.”); // 可以在这里更新UI提示用户正在跳转安装 } // 安装失败 void OnInstallFailed(string errorMessage) { Debug.LogError($“[ApkInstallManager] Installation failed: {errorMessage}”); // 可以在这里弹出错误提示框 } // 权限已获取针对Android 8.0用户从设置返回后 void OnPermissionGranted(string message) { Debug.Log(“[ApkInstallManager] Install permission granted. You may need to trigger install again.”); // 这里可以重新触发安装或者提示用户手动操作。 // 更优的做法是在Java层onActivityResult里拿到权限后直接使用之前缓存的路径再次调用performInstall。 } }将这个脚本挂载到一个场景中始终存在的GameObject上例如GameManager并确保callbackTargetName字段与该GameObject的名字一致。3.4 构建配置与打包测试Player Settings配置打开File - Build Settings - Player Settings。IdentificationPackage Name (Bundle Identifier)设置一个唯一的包名如com.yourcompany.yourgame。这将是安卓应用的唯一ID。ConfigurationScripting Backend推荐使用IL2CPP以获得更好的性能和兼容性。Target API Level设置为与你测试设备相匹配的级别如Android 11.0 (API 30)。Minimum API Level根据你的用户群体设定。Publishing Settings确保Custom Main Gradle Template和Custom Main Manifest等选项未被勾选除非你有高级定制需求。我们已通过插件方式提供了Manifest。在Build区域Build System选择Gradle。准备测试APK将一个用于测试的APK文件例如test_update.apk通过代码下载或手动放置到设备的Application.persistentDataPath目录下。在Unity编辑器中这个路径类似C:\Users\[User]\AppData\LocalLow\[CompanyName]\[ProductName]Windows或~/Library/Application Support/…macOS。在真机上路径是/storage/emulated/0/Android/data/[your.package.name]/files。构建与运行在Unity中点击File - Build Settings - Build And Run。将构建好的APK安装到已开启USB调试的安卓手机或模拟器上。在游戏中触发调用ApkInstallManager.InstallApk(“test_update.apk”)。如果一切配置正确你会看到系统弹出安装未知应用的权限请求Android 8.0或直接跳转到系统安装界面。4. 深度解析与高级技巧4.1 不同安卓版本的兼容性处理安卓版本的碎片化是开发者必须面对的挑战。我们的代码已经通过Build.VERSION.SDK_INT进行了条件判断但还有一些细节需要注意Android 11 (API 30) 及更高版本引入了Scoped Storage分区存储的进一步限制。虽然Application.persistentDataPath(/Android/data/包名/files) 下的文件不受影响但如果你尝试安装存储在公共目录如Downloads下的APK可能会遇到权限问题。最佳实践始终是将待安装的APK放在应用的私有目录下。Android 10 (API 29)同样受Scoped Storage影响。对于targetSdkVersion 29的应用访问外部存储需要特殊权限或使用MediaStore API。再次强调使用私有目录是最省事的方案。Android 6.0-7.1 (API 23-25)主要需要处理运行时权限如读写存储权限但安装APK本身不需要特殊权限。我们的FileProvider方案在此版本上工作正常。低版本兼容 (API 24)在performInstall方法中我们对低于Nougat的设备回退到Uri.fromFile()。但请注意即使使用file://URI在部分高度定制的系统上也可能失败。因此将minSdkVersion设定在24以上可以大幅简化兼容性工作。4.2 FileProvider的路径配置详解file_paths.xml中的paths元素支持多种子标签对应不同的根目录标签对应的根目录典型用途files-path/data/data/包名/files内部文件存储cache-path/data/data/包名/cache内部缓存external-files-path/storage/emulated/0/Android/data/包名/filesUnity的Application.persistentDataPathexternal-cache-path/storage/emulated/0/Android/data/包名/cache外部缓存external-media-path/storage/emulated/0/Android/media/包名媒体文件一个常见的坑如果你把APK文件放在了Application.temporaryCachePath对应/data/data/包名/cache或外部cache你需要使用cache-path或external-cache-path来声明路径。路径声明错误会导致FileProvider抛出IllegalArgumentException提示找不到文件。4.3 处理安装结果回调我们的当前实现只负责“拉起”系统安装界面。用户是点击了“安装”还是“取消”这个结果不会直接返回给我们的应用。系统安装器进程是独立的。如果你需要知道安装是否成功有几种思路轮询检查在安装界面弹出后可以启动一个协程每隔几秒检查目标包名即你要安装的APK的包名是否已出现在系统已安装应用列表中通过AndroidJavaObject调用PackageManager.getInstalledPackages。但这并不优雅且无法区分是用户取消了安装还是安装失败了。使用PackageInstallerAPI (Android 5.0)这是一个更底层的API允许你以编程方式安装APK并接收详细的安装状态回调。但这需要更复杂的权限和代码并且需要应用拥有INSTALL_PACKAGES权限该权限是系统级权限普通应用无法获取。业务逻辑设计对于大多数场景如游戏更新更合理的做法是当安装器被拉起后就认为本次更新任务已完成。应用可以正常退出。用户安装完成后需要手动点击新安装的应用图标启动。你可以在更新流程的最后一步提示用户“更新包已就绪点击确定将跳转安装安装完成后请重新启动应用”。4.4 在Unity编辑器中调试在Unity Editor中UNITY_ANDROID宏不会被定义我们的原生代码不会执行。为了便于调试可以创建一个模拟版本public void InstallApk(string relativeApkPath) { #if UNITY_ANDROID !UNITY_EDITOR // ... 安卓真机代码 ... #elif UNITY_EDITOR string fullPath Path.Combine(Application.persistentDataPath, relativeApkPath); Debug.Log($“[EDITOR SIMULATION] Would install APK at: {fullPath}”); // 可以在这里模拟成功或失败的回调测试UI逻辑 OnInstallStarted(“”); // OnInstallFailed(“Simulated failure in editor.”); #endif }5. 常见问题排查与实战心得5.1 问题排查清单问题现象可能原因解决方案调用后无任何反应1. Java插件类名、包名或方法名与C#调用不匹配。2. APK文件路径错误文件不存在。3.AndroidManifest.xml中FileProvider的authorities与Java代码中getUriForFile使用的字符串不匹配。1. 检查C#中AndroidJavaClass的参数是否为完整包名类名。检查方法名大小写。2. 在C#和Java中都打印文件路径确认文件存在且可读。3. 确保authorities为${applicationId}.fileprovider且Java代码中拼接的字符串一致。报错FileUriExposedException在Android 7.0设备上使用了file://URI。确保使用了FileProvider并且Build.VERSION.SDK_INT Build.VERSION_CODES.N的判断正确。检查file_paths.xml路径配置是否正确。报错IllegalArgumentException: Failed to find configured root…FileProvider在file_paths.xml中找不到APK文件所在的父目录。确认APK文件存储的目录如persistentDataPath与file_paths.xml中配置的路径标签匹配。例如文件在/Android/data/包名/files/下应使用external-files-path。安装界面提示“解析包时出现问题”1. APK文件在下载或拷贝过程中损坏。2. APK文件的架构与当前设备不兼容如x86的APK装在ARM设备上。3. 待安装APK的minSdkVersion高于当前设备系统版本。1. 计算文件的MD5或SHA1校验和确保完整性。2. 检查Unity构建APK时的Target Architectures设置通常勾选ARMv7和ARM64即可覆盖绝大多数设备。3. 检查待安装APK的版本要求。在Android 8.0上第一次调用直接返回没有弹出权限申请onActivityResult回调没有正确地从Unity的Activity传递到我们的Java插件。需要创建一个继承自UnityPlayerActivity的自定义Activity并重写其onActivityResult方法在其中调用我们的ApkInstaller.onActivityResult。这是处理权限回调的标准做法。5.2 创建自定义UnityPlayerActivity处理权限回调为了解决上述最后一个问题我们需要创建一个自定义的Activity。创建新的Java类在Assets/Plugins/Android下创建CustomUnityPlayerActivity.java。package com.yourcompany.unityplugin; import com.unity3d.player.UnityPlayerActivity; public class CustomUnityPlayerActivity extends UnityPlayerActivity { Override protected void onActivityResult(int requestCode, int resultCode, android.content.Intent data) { super.onActivityResult(requestCode, resultCode, data); // 将回调传递给我们的安装器 ApkInstaller.onActivityResult(requestCode, resultCode, data); } }修改AndroidManifest.xml指定应用使用这个自定义的Activity作为主入口。?xml version“1.0” encoding“utf-8”? manifest … application … !-- 替换Unity默认的Activity -- activity android:name“com.yourcompany.unityplugin.CustomUnityPlayerActivity” android:configChanges“fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen” android:hardwareAccelerated“true” android:launchMode“singleTask” android:theme“style/UnityThemeSelector” intent-filter action android:name“android.intent.action.MAIN” / category android:name“android.intent.category.LAUNCHER” / /intent-filter meta-data android:name“unityplayer.UnityActivity” android:value“true” / /activity !-- … 其他配置如之前的Provider … -- /application /manifest5.3 实战心得与优化建议路径管理是重中之重APK文件的存储路径建议统一使用Application.persistentDataPath。这个路径不需要任何运行时权限即可读写且与应用生命周期绑定卸载应用时会自动清除。避免使用Application.streamingAssetsPath只读或Application.temporaryCachePath可能被系统清理。下载与安装分离对于网络下载的更新包建议先完整下载到persistentDataPath并进行完整性校验如对比文件大小或MD5校验通过后再调用安装接口。避免下载中途或损坏的文件触发安装流程。用户引导在Android 8.0上跳转到“安装未知应用”设置页面对用户来说比较突兀。最好在调用InstallApk之前先检查权限(canRequestPackageInstalls)如果没有权限则先弹出一个友好的自定义对话框解释为什么需要这个权限用户确认后再跳转设置。这能极大提升用户体验。日志输出在Java插件的关键步骤如文件检查、URI生成、Intent构建添加Log.d(“ApkInstaller”, “message”)日志。在Unity C#侧也使用Debug.Log。通过adb logcat查看日志是定位问题最有效的手段。安全考虑确保你安装的APK来源可靠。如果是应用内更新应对下载的APK进行签名验证确保它是由你自己的证书签名的防止被中间人攻击替换成恶意应用。通过以上步骤你不仅能够实现Unity调用安卓安装APK的基本功能更能理解其背后的原理、应对各种兼容性问题并掌握优化体验的技巧。这套“利器”将成为你开发功能更丰富、体验更完善的安卓Unity应用的坚实基石。