
1. 项目概述为什么Unity需要调用安卓安装APK在移动游戏和应用开发中我们经常会遇到一个看似简单却至关重要的需求从Unity引擎内部直接拉起安卓系统的应用安装界面让用户安装一个下载好的APK文件。这个需求在游戏热更新、分包资源下载、内置应用商店或者推广自家其他产品时非常普遍。你可能会想Unity不是跨平台引擎吗直接打包成APK不就行了没错但那是最终产品。我们今天聊的是在运行时Runtime的动态安装能力。想象一个场景你的游戏上线后发现了一个紧急Bug需要更新一个核心模块。重新打包整个APK上架审核用户重新下载这个流程太慢了。更优雅的做法是游戏启动时从服务器下载一个包含修复代码的“补丁APK”然后在游戏内引导用户一键安装。这个“一键安装”的背后就是Unity与安卓原生系统之间的一次“握手”。这个过程涉及到Unity的C#脚本如何跨越边界调用安卓Java代码并最终触发系统级的安装意图Intent。对于不熟悉安卓原生开发的Unity程序员来说这里有几个坑权限声明、文件路径处理、Android API版本适配以及最重要的——用户隐私政策和系统安全限制。接下来我会结合我实际项目中的经验把这套流程掰开揉碎讲清楚。2. 核心原理与前置知识拆解在动手写代码之前我们必须理解Unity与安卓交互的基本原理。Unity使用C#进行逻辑开发而安卓系统的底层接口特别是涉及系统安全如安装应用的功能都是由Java/Kotlin语言通过Android SDK提供的。因此Unity调用安装APK本质上是一次C# → Java的跨语言通信。2.1 Unity与安卓通信的桥梁AndroidJavaClass与AndroidJavaObjectUnity提供了AndroidJavaClass和AndroidJavaObject这两个类作为调用Java代码的入口。你可以把它们理解为C#端的“代理”或“中介”。AndroidJavaClass用于调用Java中的静态static方法或访问静态字段。它的构造函数参数是Java类的完整包名。// 例如获取安卓系统当前的版本号一个静态字段 AndroidJavaClass versionClass new AndroidJavaClass(android.os.Build$VERSION); int sdkInt versionClass.GetStaticint(SDK_INT);AndroidJavaObject用于实例化一个Java对象并调用该对象的实例方法。它的构造函数第一个参数是类名后续参数是该类构造函数的参数。// 例如创建一个Uri对象对应Java中的android.net.Uri AndroidJavaObject uriObject new AndroidJavaObject(android.net.Uri, file:///path/to/file);调用安装APK的核心就是使用这些类在C#中构造出安卓Java端的Intent对象并启动它。2.2 安卓安装APK的系统机制Intent与FileProvider在纯安卓开发中安装一个APK文件的代码通常如下Java示例File apkFile new File(filePath); Intent intent new Intent(Intent.ACTION_VIEW); intent.setDataAndType(Uri.fromFile(apkFile), application/vnd.android.package-archive); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent);这段代码做了几件事创建Intent设置动作为ACTION_VIEW查看。设置数据与类型将APK文件的Uri和MIME类型application/vnd.android.package-archive绑定到Intent。添加标志FLAG_ACTIVITY_NEW_TASK确保在一个新的任务栈中启动安装界面。启动Activity调用startActivity系统会弹出熟悉的安装确认界面。关键转折点Android 7.0 (API 24) 与 FileProvider在Android 7.0之前我们使用Uri.fromFile(file)来生成一个file://协议的Uri。但从7.0开始谷歌引入了更严格的文件共享限制StrictMode禁止应用间直接通过file://Uri共享文件否则会抛出FileUriExposedException。解决方案是使用FileProvider。FileProvider是ContentProvider的一个特殊子类它可以通过生成content://协议的Uri来安全地共享文件。这意味着我们的Unity项目必须配置好FileProvider。注意这是第一个大坑。很多教程只给了代码没讲清楚为什么以及如何配置FileProvider导致开发者在Android 8.0的设备上测试时直接崩溃。2.3 必要的安卓权限除了代码还需要在安卓清单文件AndroidManifest.xml中声明权限。安装APK本身不需要特殊权限但访问APK文件所在的存储位置需要。访问外部存储传统方式如果你的APK放在SD卡或公共下载目录需要uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / !-- 如果针对Android 13 (API 33) 及以上需要请求 READ_MEDIA_IMAGES/VIDEO/AUDIO但APK文件通常用MANAGE_EXTERNAL_STORAGE上架有限制或更推荐使用应用私有目录 --使用应用私有目录推荐将APK下载到Unity应用的私有文件目录Application.persistentDataPath。访问此目录不需要任何运行时权限是最安全、最推荐的方式。我们后续的实践也将基于此方案。3. 完整实操流程从Unity工程配置到代码实现理论清楚了我们开始动手。我将以一个完整的、可复现的流程来讲解涵盖Unity工程设置、安卓侧配置和C#脚本编写。3.1 步骤一Unity工程基础设置切换构建平台在Unity Editor中打开File - Build Settings选择Android平台点击Switch Platform。安装必要的SDK/NDK/JDK确保Unity Hub或Preferences中的External Tools里Android SDK、NDK和JDK路径已正确设置。这是编译安卓项目的基础。修改Player Settings在Player Settings中找到Other Settings部分。Package Name填写你的应用包名如com.YourCompany.YourGame。这将是最终APK的标识。Minimum API Level根据你的目标用户设置建议至少设为Android 8.0 (API 26)但为了兼容FileProvider需要API 24。Target API Level设置为你要适配的最高版本通常建议设为最新的稳定版。3.2 步骤二配置AndroidManifest.xml与FileProvider这是核心配置环节我们需要自定义安卓清单文件和资源文件。创建或修改Plugins/Android目录结构 在你的Unity项目Assets目录下创建文件夹Assets/Plugins/Android。所有安卓特定的配置都将放在这里。准备AndroidManifest.xml 你可以从Unity安装目录下的{UnityInstallPath}/Editor/Data/PlaybackEngines/AndroidPlayer/Apk中找到默认的AndroidManifest.xml复制到Assets/Plugins/Android下进行修改。或者直接新建一个。 关键修改内容如下?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.YourCompany.YourGame !-- 这里会自动替换为Player Settings中的包名 -- xmlns:toolshttp://schemas.android.com/tools !-- 如果APK文件放在外部存储需要此权限不推荐 -- !-- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / -- application android:iconmipmap/app_icon android:labelstring/app_name android:themestyle/UnityThemeSelector !-- 关键定义你的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 !-- Unity Player Activity -- activity android:namecom.unity3d.player.UnityPlayerActivity android:configChanges... !-- ... 其他原有配置 ... -- /activity /application /manifestandroid:authorities这是FileProvider的唯一标识符。${applicationId}是一个占位符在构建时会被替换成你的应用包名如com.YourCompany.YourGame。后面生成Uri时会用到。android:resource指向一个XML资源文件定义了FileProvider可以共享哪些路径下的文件。创建file_paths.xml资源文件 在Assets/Plugins/Android下新建一个res/xml文件夹即完整路径为Assets/Plugins/Android/res/xml然后在xml文件夹内创建file_paths.xml文件。 文件内容如下?xml version1.0 encodingutf-8? paths xmlns:androidhttp://schemas.android.com/apk/res/android !-- 共享应用内部私有文件目录 -- files-path nameinternal_files path. / !-- 共享外部存储中本应用私有目录 -- external-files-path nameexternal_files path. / !-- 共享缓存目录 -- cache-path namecache path. / !-- 注意我们通常将下载的APK放在persistentDataPath对应的是‘files-path’ -- /pathsfiles-path对应Context.getFilesDir()也就是Unity中的Application.persistentDataPath在安卓上是/data/data/你的包名/files。这是我们存放下载APK最安全的位置。path.表示共享该根目录下的所有文件。你也可以指定子目录如pathdownload/。实操心得file_paths.xml的配置是第二个大坑。paths节点下的标签必须与你要共享的文件实际路径严格对应。如果你把APK放在了Application.persistentDataPath就必须使用files-path。用错了标签FileProvider会找不到文件导致安装失败。3.3 步骤三编写C#调用脚本现在我们来编写核心的C#脚本。创建一个名为AndroidAppInstaller.cs的脚本。using UnityEngine; using System.IO; using System; public class AndroidAppInstaller : MonoBehaviour { /// summary /// 调用安卓系统安装APK /// /summary /// param nameapkFilePathAPK文件的完整路径在设备上的路径/param public void InstallApk(string apkFilePath) { // 0. 基础检查 if (string.IsNullOrEmpty(apkFilePath) || !File.Exists(apkFilePath)) { Debug.LogError([AndroidAppInstaller] APK文件不存在或路径为空: apkFilePath); return; } // 判断当前平台 if (Application.platform ! RuntimePlatform.Android) { Debug.LogWarning([AndroidAppInstaller] 非Android平台调用无效。); return; } Debug.Log([AndroidAppInstaller] 开始安装APK: apkFilePath); // 1. 获取当前UnityPlayer的Activity上下文 AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer); AndroidJavaObject currentActivity unityPlayer.GetStaticAndroidJavaObject(currentActivity); // 2. 根据安卓版本选择不同的Uri构建方式 AndroidJavaClass versionClass new AndroidJavaClass(android.os.Build$VERSION); int sdkInt versionClass.GetStaticint(SDK_INT); AndroidJavaObject uri; string authority currentActivity.Callstring(getPackageName) .fileprovider; // 拼接authorities if (sdkInt 24) // Android 7.0 (N) 及以上 { // 使用FileProvider获取Content Uri AndroidJavaClass fileProvider new AndroidJavaClass(androidx.core.content.FileProvider); AndroidJavaObject fileObj new AndroidJavaObject(java.io.File, apkFilePath); // 调用FileProvider.getUriForFile uri fileProvider.CallStaticAndroidJavaObject(getUriForFile, currentActivity, authority, fileObj); Debug.Log([AndroidAppInstaller] 使用FileProviderAuthority: authority); } else { // Android 7.0 以下使用旧式File Uri (不推荐但需兼容) AndroidJavaClass uriClass new AndroidJavaClass(android.net.Uri); AndroidJavaObject fileObj new AndroidJavaObject(java.io.File, apkFilePath); uri uriClass.CallStaticAndroidJavaObject(fromFile, fileObj); Debug.Log([AndroidAppInstaller] 使用旧式File Uri); } // 3. 创建Intent并设置数据和类型 AndroidJavaObject intent new AndroidJavaObject(android.content.Intent, android.intent.action.VIEW); // 设置Data和Type intent.CallAndroidJavaObject(setDataAndType, uri, application/vnd.android.package-archive); // 添加Flags intent.CallAndroidJavaObject(addFlags, intent.GetStaticint(FLAG_ACTIVITY_NEW_TASK) | intent.GetStaticint(FLAG_GRANT_READ_URI_PERMISSION)); // 4. 启动安装Activity currentActivity.Call(startActivity, intent); Debug.Log([AndroidAppInstaller] 安装Intent已启动。); } /// summary /// 一个示例方法假设APK已下载到持久化数据路径下的Download文件夹 /// /summary public void InstallDownloadedApk(string apkFileName) { // 构建APK在设备上的完整路径 string apkPath Path.Combine(Application.persistentDataPath, Download, apkFileName); InstallApk(apkPath); } }代码关键点解析平台判断首先检查是否在Android平台运行避免在Editor或iOS上出错。获取Activitycom.unity3d.player.UnityPlayer.currentActivity是Unity游戏在安卓上的主Activity上下文是我们所有安卓调用的起点。版本适配通过Build.VERSION.SDK_INT判断安卓版本决定使用FileProviderAPI 24还是旧式File Uri。构建Authorityauthority必须与AndroidManifest.xml中provider标签定义的android:authorities完全一致。我们通过getPackageName()动态获取包名再拼接.fileprovider确保万无一失。生成UriAPI 24: 调用FileProvider.getUriForFile(Context, authority, File)生成安全的content://Uri。API 24以下: 调用Uri.fromFile(File)生成file://Uri。配置IntentACTION_VIEW标准查看动作。setDataAndType同时设置Uri和MIME类型。FLAG_ACTIVITY_NEW_TASK在新任务中启动安装界面。FLAG_GRANT_READ_URI_PERMISSION至关重要这个标志授予安装程序Package Installer临时读取我们通过FileProvider共享的Uri的权限。没有它安装程序会因权限不足而无法读取APK文件。启动调用当前Activity的startActivity方法。3.4 步骤四模拟测试与真机部署在Unity Editor中由于没有安卓环境直接调用会走Debug.LogWarning分支。为了测试逻辑你可以创建一个简单的UI按钮绑定InstallDownloadedApk方法。真机测试流程构建APK使用配置好的工程Build出一个母包APK我们称之为AppA安装到测试手机上。准备测试APK准备另一个简单的APK我们称之为AppB作为要被安装的对象。你可以用Unity新建一个空工程打包。放置测试APK将AppB.apk通过ADB命令推送到AppA的私有目录模拟下载完成。adb push AppB.apk /storage/emulated/0/Android/data/com.YourCompany.YourGame/files/Download/注意实际路径取决于Application.persistentDataPath和你的组织方式。运行测试在手机上运行AppA点击触发安装的按钮。你应该能看到系统弹出安装AppB的界面。4. 进阶议题、常见问题与深度避坑指南即使按照上述步骤操作在实际项目中你依然会遇到各种问题。下面是我踩过坑后总结的经验。4.1 适配Android 8.0 (API 26) 的“未知来源应用”权限从Android 8.0开始android.permission.REQUEST_INSTALL_PACKAGES权限被引入。用户不仅需要在系统设置中全局允许“未知来源应用”还需要针对你的应用单独授权。我们的安装代码需要检查并请求此权限。修改C#脚本添加权限请求逻辑public bool CheckAndRequestInstallPermission() { if (Application.platform ! RuntimePlatform.Android) return true; AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer); AndroidJavaObject currentActivity unityPlayer.GetStaticAndroidJavaObject(currentActivity); AndroidJavaObject packageManager currentActivity.CallAndroidJavaObject(getPackageManager); // 检查是否已有权限 bool canRequest packageManager.Callbool(canRequestPackageInstalls); Debug.Log($[AndroidAppInstaller] 当前安装权限状态: {canRequest}); if (!canRequest Build.VERSION.SDK_INT 26) { // 跳转到设置页面让用户手动开启 AndroidJavaObject intent new AndroidJavaObject(android.content.Intent, android.settings.MANAGE_UNKNOWN_APP_SOURCES, AndroidJavaObject.CallStaticAndroidJavaObject(parse, package: currentActivity.Callstring(getPackageName)) ); currentActivity.Call(startActivity, intent); return false; } return canRequest; } // 在调用InstallApk前先检查权限 public void TryInstallApk(string apkPath) { if (CheckAndRequestInstallPermission()) { InstallApk(apkPath); } else { Debug.Log(需要用户去设置页面开启‘允许安装未知应用’权限。); // 这里可以弹出UI提示引导用户操作 } }注意事项MANAGE_UNKNOWN_APP_SOURCES这个Action在部分厂商定制的系统上可能被修改或屏蔽。最稳妥的方式是在尝试安装前通过canRequestPackageInstalls()检查如果返回false则用清晰的UI文字提示用户“请在系统设置中为本应用开启‘安装未知应用’权限”并说明如何进入设置通常是在“应用管理”-找到你的应用-“特殊权限”或“高级”里。4.2 文件路径与权限的终极处理方案问题Application.persistentDataPath在安卓上指向/data/data/包名/files这个路径是应用私有的其他应用包括系统安装程序默认无法访问。即使我们通过FileProvider共享如果APK文件本身没有读取权限也会失败。解决方案确保下载的APK文件具有正确的读写权限。在Unity中使用System.IO创建或写入文件时其权限通常是正确的-rw-rw----即用户和组可读写。但如果你通过其他方式如WWW/UnityWebRequest的旧版本下载文件可能需要手动设置。一个更健壮的下载与准备函数示例using UnityEngine.Networking; using System.Threading.Tasks; public async Taskstring DownloadAndPrepareApk(string url, string localFileName) { string downloadDir Path.Combine(Application.persistentDataPath, Download); if (!Directory.Exists(downloadDir)) Directory.CreateDirectory(downloadDir); string localPath Path.Combine(downloadDir, localFileName); using (UnityWebRequest www UnityWebRequest.Get(url)) { www.downloadHandler new DownloadHandlerFile(localPath); var operation www.SendWebRequest(); while (!operation.isDone) await Task.Yield(); if (www.result ! UnityWebRequest.Result.Success) { Debug.LogError($下载失败: {www.error}); return null; } } // 下载完成后确保文件可读在安卓上UnityWebRequest下载的文件通常权限正确 // 可以在这里添加文件完整性校验例如检查MD5 Debug.Log($APK已下载至: {localPath}); return localPath; }4.3 常见错误排查表错误现象可能原因解决方案安装界面不弹出无反应或Log报错1. APK文件路径错误或不存在。2. FileProvider的authorities不匹配。3.file_paths.xml配置错误路径不包含目标文件。4. 缺少FLAG_GRANT_READ_URI_PERMISSION标志。1. 打印apkFilePath并确认文件存在且可读。2. 检查C#代码中拼接的authority与Manifest中的定义是否完全一致。3. 确认APK文件所在目录被file_paths.xml中正确的path标签覆盖。4. 在Intent创建后务必添加该Flag。安装界面弹出但提示“解析包时出现问题”1. APK文件下载不完整或已损坏。2. APK与当前设备CPU架构不兼容如arm64-v8a设备安装了纯armeabi-v7a的包。3. 目标设备的安卓版本低于APK要求的minSdkVersion。1. 校验文件MD5或重新下载。2. 检查被安装APK的构建配置支持多ABI或通用。3. 检查被安装APK的AndroidManifest.xml中的uses-sdk。在Android 11上无法访问Download文件夹Android 11引入了分区存储Scoped Storage对公共目录访问有更严格限制。强烈建议将文件下载到应用私有目录Application.persistentDataPath完全避免此问题。Logcat报错FileUriExposedException在Android 7.0设备上使用了file://Uri。确保代码中进行了版本判断sdkInt 24并对高版本使用FileProvider。安装时提示“禁止安装”或跳转到权限设置未处理Android 8.0的“未知来源”权限。集成CheckAndRequestInstallPermission方法在安装前检查和引导用户授权。4.4 性能与体验优化建议异步与UI反馈下载APK和安装调用可能耗时务必放在异步操作中如async/await、协程避免阻塞主线程导致游戏卡顿。同时提供清晰的进度条和状态提示如“下载中...”、“准备安装”。安装结果回调系统安装界面是一个独立的Activity用户可以选择“安装”或“取消”。Unity默认无法直接获取这个结果。一个变通的方法是在安装完成后让被安装的应用如果也是你开发的启动时通过某种方式如发送广播、写入共享文件通知母应用。但这通常不是强需求。清理旧文件定期清理Download目录下的历史APK文件避免占用用户不必要的存储空间。网络状态与重试实现健壮的下载逻辑包括断点续传、网络状态监听和失败重试机制。5. 扩展思考与其他方案的对比在实现应用内安装时除了直接调用系统安装器还有其他一些思路了解它们有助于你在不同场景下做出选择。使用第三方库如NatShare, Mobile Native Popup优点封装好了原生代码可能提供更统一的API和额外的功能如分享到其他应用。缺点引入额外依赖可能增加包体灵活性受限遇到深度定制需求或特定机型问题时排查困难。适用场景追求快速开发且功能需求简单标准。通过浏览器下载将APK链接通过Application.OpenURL在默认浏览器中打开。浏览器会处理下载并提示安装。优点实现极其简单无需处理任何权限和兼容性问题。缺点用户体验割裂会离开你的应用无法在应用内管理下载过程和状态。适用场景简单的网页推广链接对体验要求不高的场景。静默安装需要系统权限通过pm install命令实现无界面安装。这需要应用具有android.permission.INSTALL_PACKAGES权限该权限只授予系统应用或拥有root权限的设备。对于普通商业应用此路不通。谷歌严格禁止非系统应用请求此权限。结论对于大多数Unity游戏和应用内更新、交叉推广的需求本文详述的“调用系统安装器”方案是唯一可行且平衡了功能、用户体验和合规性的标准做法。它尊重了系统的安全规范将最终的安装确认权交给了用户同时也保证了流程的顺畅。整个流程走下来从配置到代码再到问题排查其核心在于理解安卓系统的安全机制演变FileProvider, Scoped Storage, 未知来源权限并妥善适配。把这些点都处理好你的Unity应用就能在各种安卓机型上稳定可靠地调用安装功能了。在实际项目中建议将安装模块封装成一个独立的、可复用的服务类并做好详细的日志记录这样在后续维护和问题定位时会轻松很多。