ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity调用Android安装APK:插件开发与FileProvider适配全指南

Unity调用Android安装APK:插件开发与FileProvider适配全指南 Unity开发最容易被低估的一个环节就是和Android原生层的交互。很多项目做到最后都会碰到一个需求游戏内检测到新版本需要下载APK然后调用系统安装界面。Unity本身对这块几乎是空白C#里没有直接拉起安装器的接口所以Unity 调用安卓内部安装apk就成了绕不过去的坎。这篇文章就专门聊这件事从Android插件编写、FileProvider适配、Unity端C#调用到厂商ROM的坑点一步步展开适合正在做安卓平台Unity应用、需要实现应用内更新或安装功能的开发者参考。我会把方案选型的依据、代码为什么要这么写、参数怎么配都讲清楚而不是只扔一段能跑的代码。1. 方案设计Unity和安卓交互到底是怎么一回事1.1 先搞懂Unity调用Android的底层机制Unity调用安卓本质上走的是JNIJava Native Interface这座桥。Unity引擎在Android上运行时会创建一个主ActivityC#层可以通过AndroidJavaObject和AndroidJavaClass这两个类找到Java层的类、调用静态或实例方法。很多入门资料会告诉你用AndroidJavaClass调一下静态方法就行了但实际做APK安装这种需求时简单调用会卡在几个地方Context从哪来、Android 7.0以后的FileProvider怎么配、安装完成后怎么通知Unity。所以正确定位是写一个独立的Android插件AAR或者JAR把自己能做的事在Java层做完Unity只负责传参数和收结果。什么时候直接写C#调Java就够了比如打开系统设置、获取设备型号、检查某个服务是否开启这些操作不涉及文件URI暴露、不涉及权限回调用AndroidJavaClass调一下确实简洁。但安装APK涉及Intent、FileProvider、安装权限、安装结果监听逻辑集中在C#层会非常痛苦而且后续如果要做下载进度回调、安装完成事件纯C#很难写干净。我做过几个项目最稳妥的结构是Java层做一个ApkInstaller类把安装逻辑封装成一个黑盒子Unity层用AndroidJavaObject唤醒它传APK路径安装结果通过UnitySendMessage回传。这样Unity侧逻辑量很小出问题也好排查——先在Java层独立测通再接Unity。1.2 为什么必须用自定义Android插件而不是现成方案我看过有些项目直接用网上找的一行代码安装APK工具类或者拿第三方SDK里的安装方法凑合。这些方案最大的问题在于对Android版本适配几乎是零。Android 7.0强制要求FileProvider暴露文件URIAndroid 8.0引入了未知来源应用安装的弹窗授权Android 11简化了权限但还得声明Android 13把读取媒体权限又拆细了。如果插件不处理这些装上以后轻则解析失败重则直接闪退。另一种做法是买或下载第三方Android Native插件比如一些商用热更新框架自带安装能力。这类方案的问题是依赖太重为了一个安装功能引进来整个SDK包体增大、混淆规则、厂商适配全都得跟着调。而且很多商用SDK需要付费授权个人开发者或者中小团队不一定愿意承担。所以自己写一个最小可用的AAR其实是最划算的投入。核心代码量不大关键是把系统API调用正确、把FileProvider配好、把安装完成监听做好。后面我会把工程的完整思路和代码贴出来照着改包名和路径就能用。2. Android端插件开发核心Java代码与FileProvider配置2.1 新建Android Library工程写出可复用的ApkInstaller在Android Studio里新建一个Module类型选Android Library。为什么用Library而不是Application因为我们要把编译产物给Unity用Library可以导出AAR而Application只能产出APK。包名尽量和Unity项目有区分度比如com.yourcompany.apkinstaller避免和别人写的插件包名撞车。Java类我命名为ApkInstaller对外开放的方法就两个installApk(Context context, String apkPath)isApkInInstallAllowed(Context context)用于检查是否允许安装未知应用核心安装方法的最简实现public class ApkInstaller { public static final String ACTION_INSTALL_RESULT APK_INSTALL_RESULT; public static void installApk(Context context, String apkPath) { if (context null || apkPath null) { sendResult(context, false, context or path is null); return; } File apkFile new File(apkPath); if (!apkFile.exists()) { sendResult(context, false, apk file not exists: apkPath); return; } Intent intent new Intent(Intent.ACTION_VIEW); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); Uri apkUri; if (Build.VERSION.SDK_INT Build.VERSION_CODES.N) { // Android 7.0以上必须通过FileProvider暴露 apkUri FileProvider.getUriForFile(context, context.getPackageName() .fileprovider, apkFile); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } else { apkUri Uri.fromFile(apkFile); } intent.setDataAndType(apkUri, application/vnd.android.package-archive); context.startActivity(intent); } private static void sendResult(Context context, boolean success, String message) { // 这里发广播或者回调Unity端可以监听 Intent resultIntent new Intent(ACTION_INSTALL_RESULT); resultIntent.putExtra(success, success); resultIntent.putExtra(message, message); if (context ! null) { context.sendBroadcast(resultIntent); } } }这段代码有几个关键点FLAG_ACTIVITY_NEW_TASK是必须的因为Unity传进来的Context很可能是ApplicationContext它没有Activity栈不带这个flag启动Activity会崩。FLAG_GRANT_READ_URI_PERMISSION是让系统安装器能临时读取这个URI指向的文件7.0以上不授权的话安装器会拿不到文件。application/vnd.android.package-archive这个MIME类型是Android安装APK的标准类型写错的话系统无法识别。2.2 FileProvider配置XML路径映射是安装成功与否的分水岭工程里必须在AndroidManifest.xml注册FileProvider并在res/xml下新建一个XML文件定义可暴露的路径。不配这一步7.0以上的机器上调用FileProvider.getUriForFile会直接抛异常。Manifest里加这段application 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然后在res/xml/file_paths.xml里写路径映射?xml version1.0 encodingutf-8? paths external-path nameexternal_download pathDownload/ / external-path nameexternal_root path. / cache-path namecache_root path. / files-path namefiles_root path. / /paths这里要特别说明authorities和path的匹配机制。authorities相当于这个FileProvider的唯一IDFileProvider.getUriForFile传的authority必须和Manifest里注册的一致我用的是${applicationId}.fileproviderUnity打包后applicationId就是Unity里设置的包名这样每个项目都不用改代码。如果你在Unity里配了多个插件注意别让多个fileprovider的authority重复否则安装时会报Failed to find configured root。path映射的规则是应用能访问的目录分好几类external-path对应Environment.getExternalStorageDirectory()cache-path对应context.getCacheDir()files-path对应context.getFilesDir()。我习惯把external_root的path设成.表示外部存储根目录可暴露因为很多下载库会把APK存到自定义目录路径映射设宽一点省得后续报文件找不到。但要注意path值只能精确匹配到目录级别不能匹配到具体文件所以建议下载APK时固定到一个子目录比如Download/或Android/data/你的包名/files/映射写清楚别让用户手动换存储目录导致路径失配。2.3 Android 8.0及以上未知来源安装权限的处理Android 8.0开始安装第三方APK需要引导用户打开允许安装未知应用的开关。如果直接startActivity大概率没有反应或者被系统拦截。正确做法是先检查有没有权限没有就跳转到设置页。Java层加一个检查方法public static boolean isInstallAllowed(Context context) { if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { return context.getPackageManager().canRequestPackageInstalls(); } return true; } public static void goToInstallSetting(Context context) { if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { Uri packageUri Uri.parse(package: context.getPackageName()); Intent intent new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageUri); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } }Unity端在有新版本要安装之前先调用isInstallAllowed如果返回false就先弹一个说明UI引导用户跳转设置页等用户回来再继续。这里要强调一下canRequestPackageInstalls返回的只是这个应用是否被允许安装未知来源应用不是是否已经打开安装页面所以用户中途取消的话要再查一遍别假设跳转回来就一定有权限。实际测试时我发现不同厂商对这个权限的管理入口还不太一样。小米在更多设置-系统安全-特殊权限设置-安装未知应用华为在应用和通知-特殊访问权限-安装未知应用OPPO在设置-安全-更多安全设置-安装未知应用。代码里用ACTION_MANAGE_UNKNOWN_APP_SOURCES是标准做法大部分ROM会跳到本应用的设置项个别ROM可能跳到总开关页面但至少不会毫无反应。3. Unity端集成与调用AAR放置、C#封装、结果回调3.1 导出AAR并放进Unity工程在Android Studio里把Library模块构建出AARBuild - Make Project产物在module/build/outputs/aar/目录下文件名一般是apkinstaller-release.aar或者apkinstaller-debug.aar。放到Unity工程的Assets/Plugins/Android/目录下。这里有个值得注意的坑Unity自身的Gradle构建系统会自动合并AAR里的Manifest和资源所以大多数情况下你只需要放AAR就够了。但如果你用了Unity的Custom Main Manifest功能也就是自己在Assets/Plugins/Android/AndroidManifest.xml写了完整Manifest那FileProvider的注册要同步加进去两者不会自动合并。我在项目里习惯使用Unity的Player Settings - Publishing Settings - Build - Custom Launcher Manifest来生成一个统一的Manifest然后把FileProvider注册手动加进去这样可以完全控制Provider的authorities不冲突。另外要注意AAR里如果用到了androidx.core.content.FileProvider这个类需要确保AAR的依赖里有androidx.core库。Android Library在编译时如果声明了implementation androidx.core:core:1.9.0导出的AAR里会有POM依赖信息Unity的Gradle构建会尝试去拉取。但国内网络环境下Gradle拉google仓库经常很慢如果你发现构建卡在依赖下载上可以改成直接把androidx.core的AAR也打进Plugins/Android或者用compileOnly方式减少传递依赖。不过compileOnly会导致运行时找不到类所以更稳妥的做法是让Unity工程本身自带androidx库一般Unity 2019以上版本默认支持不用太担心。3.2 C#侧封装用AndroidJavaClass桥接并传入必要参数Unity侧调用Java方法核心是拿到Context。Unity提供了UnityPlayer.currentActivity这个全局Activity对象但通过AndroidJavaClass访问时要注意它不是一个静态字段而是com.unity3d.player.UnityPlayer类里的公共静态字段。C#封装示例using UnityEngine; public class ApkInstallerBridge : MonoBehaviour { private static readonly string JavaClassName com.yourcompany.apkinstaller.ApkInstaller; private const string ResultReceiverName ApkInstallResultReceiver; public void InstallApk(string apkPath) { using (AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) { AndroidJavaObject activity unityPlayer.GetStaticAndroidJavaObject(currentActivity); using (AndroidJavaClass apkInstaller new AndroidJavaClass(JavaClassName)) { apkInstaller.CallStatic(installApk, activity, apkPath); } } } public bool IsInstallAllowed() { using (AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) { AndroidJavaObject activity unityPlayer.GetStaticAndroidJavaObject(currentActivity); using (AndroidJavaClass apkInstaller new AndroidJavaClass(JavaClassName)) { return apkInstaller.CallStaticbool(isInstallAllowed, activity); } } } }注意CallStatic的第一个参数是方法名后面跟的是方法参数。Java方法签名是installApk(Context, String)C#这边传AndroidJavaObject即activity和string是没问题的Unity的JNI桥接会自动匹配。这里有一个Unity老版本表现不太一样的地方currentActivity在Unity 2020之前是直接静态字段之后有版本好像改成了方法不过GetStaticAndroidJavaObject(currentActivity)基本上一直能用。如果遇到拿不到Activity的报错可以试试改成先GetStaticAndroidJavaObject(currentActivity)失败后用AndroidJNI手动获取但这些特殊情况很少见大部分项目都用第一种方式就通了。3.3 监听安装结果从Java回传UnitySendMessage安装页面打开以后用户可能在系统界面选完成也可能取消。Unity怎么知道安装完没完两种常见做法广播监听和UnitySendMessage回调。我采用的是Java层在installApk时注册一个BroadcastReceiver监听Intent.ACTION_PACKAGE_ADDED一旦检测到包名匹配就调用UnityPlayer.UnitySendMessage通知Unity对象。这个方案的好处是能覆盖安装成功后用户点击完成的场景包括从安装器返回。但如果只是需要打开安装界面、不关心结果在startActivity成功后直接给Unity回调已拉起安装器就够了更简单。private static void registerInstallReceiver(Context context, final String packageName, final String gameObjectName, final String callbackMethod) { IntentFilter filter new IntentFilter(Intent.ACTION_PACKAGE_ADDED); filter.addDataScheme(package); BroadcastReceiver receiver new BroadcastReceiver() { Override public void onReceive(Context receiverContext, Intent intent) { Uri data intent.getData(); String addedPackage data ! null ? data.getSchemeSpecificPart() : ; if (packageName.equals(addedPackage)) { UnityPlayer.UnitySendMessage(gameObjectName, callbackMethod, success); } } }; context.registerReceiver(receiver, filter); }Unity侧回调方法放在一个名为ApkInstallResultReceiver的游戏对象上脚本里写public void OnApkInstallResult(string result) { Debug.Log(APK安装结果: result); // 这里可以提示用户安装完成然后杀掉后台进程或跳转到主页 }我在多个项目里用这个方案后发现一个细节有些ROM比如MIUI在安装完APK之后广播发送时机不太稳定偶尔会收不到。所以建议不要只看这个广播安装完成后的拉起安装器本身就算阶段性成功用户是否真正完成安装无法100%由开发者保证。这也是Android系统的限制除非你的应用是Device Owner级别否则拿不到确切的安装结果。产品上可以妥协检测到包版本号变化来确认是否安装成功这比等广播可靠得多。3.4 下载阶段的路径约定不要随便用缓存目录Unity端发起下载的APK存到哪直接影响FileProvider的映射。我踩过一个坑之前下载库用的Application.persistentDataPath在Android上对应/data/data/包名/files也就是files-path而我的路径映射里一开始只有external-path结果一调用就报Failed to find configured root。后来统一改成APK下载到Application.persistentDataPath /apk/Java端FileProvider配置加上files-path映射路径里包含apk这个子目录问题就解决了。路径约定总结就是下载前就决定好Java和C#共用同一套路径规则别用Application.temporaryCachePath因为缓存目录可能被系统清掉下载了一半清掉还好下载完了安装前被清掉就很尴尬。下载完成后校验一下文件是否存在且大小大于一定阈值再调安装方法可以省掉很多解析包时出现问题的报错。4. 常见问题与厂商适配我踩过的那些坑4.1 解析包时出现问题的罪魁祸首这个报错是安卓安装APK时用户最常看到的也是Unity接入安装功能后最容易出现的。排查顺序我一般是这样先看APK文件本身是否完整下载很多下载库会把304重定向、断点续传、http和https混用的坑带进来导致文件损坏再看APK是否被加密处理过Unity分包、加固后的APK在某些设备上也会解析不了最后才是代码层面的问题。代码层面最容易犯的错是FileProvider的authority和实际配置不一致。FileProvider.getUriForFile第一个参数传的是Context第二个参数是authority字符串如果用${applicationId}.fileproviderUnity打包后实际包名就是Unity里的com.company.product这种那么authority就是com.company.product.fileprovider。如果你在Manifest里写死了别的名字就会在真机上闪退日志提示Failed to find configured root。解决方法是Java代码里动态获取context.getPackageName() .fileproviderManifest里用${applicationId}占位符两边保持一致这样不管Unity包名改成什么都能对上。4.2 厂商ROM的差异小米、华为、OPPO、vivo国内ROM魔改严重安装APK这块尤其明显。我实测下来小米的MIUI和华为的EMUI对未知来源安装权限的入口位置不一样但Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES都能跳转。OPPO和vivo在部分系统版本上对后台拉起安装器的限制比较严如果Unity的Activity不在前台startActivity可能被拦截。应对策略是尽量确保发起安装时应用处于前台。有些项目在下载完成后会弹一个通知用户点击通知才进入Unity场景再弹安装确认框这样比下载完直接偷偷拉起安装器要稳得多也符合用户预期。另外值得一提的是部分国产ROM还要求应用在前台时才能弹安装页如果Unity切到后台下载完直接调startActivity会没反应解决办法是先用OnApplicationPause检测前后台切回前台后再安装。4.3 FileProvider authorities 冲突怎么办Unity项目经常同时接入多个SDK每个SDK都可能自带一个FileProvider甚至不止一个。Manifest合并时会因为authorities相同导致报错Authorities conflict。解决思路有两个。一是统一改造自己的插件用${applicationId}做前缀这是规范做法。第三方SDK如果写死了authority就只能去改它的Manifest合并规则用tools:nodereplace强行覆盖但这样做风险高不一定奏效。二是在Unity的Custom Main Manifest里把所有provider列出来给每个provider分配不同的authorities后缀。实际操作中我倾向于后者因为可控性更强而且能在合并阶段就能发现冲突不用等打包后运行时才炸。强调一个关键概念authorities在系统里是全局唯一的两个ContentProvider用同一个authority后者会覆盖前者安装APK时就可能调到错误的provider。所以FileProvider的authority命名一定要加上包名前缀不要用fileprovider这种裸名称。4.4 targetSdkVersion 的影响Unity的Player Settings - Other Settings - Target API Level对安装行为影响很大。Android 11API 30开始系统对包可见性做了限制如果targetSdkVersion是30或以上应用查询其他应用安装信息时要声明queries。安装APK的Intent本身不受包可见性影响但如果你要实现检测某个app是否已安装来判断是否要覆盖安装就需要在Manifest里加queries intent action android:nameandroid.intent.action.VIEW / data android:mimeTypeapplication/vnd.android.package-archive / /intent /queriestargetSdkVersion还影响权限申请方式。Android 6.0以上运行时权限必须在Unity里用Permission.RequestUserPermission或原生方法申请WRITE_EXTERNAL_STORAGEAndroid 13以后对应READ_MEDIA_*否则下载APK到公共目录会失败。我的经验是APK放到应用私有目录其实不需要存储权限所以能避免就避免申请存储权限只使用persistentDataPath或Android/data/包名目录就足够。4.5 常见问题速查表现象可能原因解决方式调用installApk后无反应未加FLAG_ACTIVITY_NEW_TASK / 未授予未知来源权限检查Intent flag跳转安装设置页报错Failed to find configured rootFileProvider路径映射缺少对应目录检查file_paths.xml配置扩大path范围APK安装解析失败文件下载不完整 / 路径读取不到校验文件大小和MD5检查路径是否在映射内安装完成后Unity不回调广播未收到 / 包名不匹配用版本号自检替代广播监听安装时提示不允许此来源targetSdkVersion过高且未声明queriesManifest加queries声明多个SDK的FileProvider冲突authority重复统一用applicationId前缀并检查合并Manifest小米/华为上跳转设置失败系统限制跳转类型改用厂商专用Intent或提示用户手动打开5. 进阶扩展从能装到好用的安装闭环5.1 版本比较与覆盖安装策略现在Unity端已经能调起安装了但一个完整的新版本更新流程还需要版本判断和强制更新/非强制更新的产品逻辑。Unity里可以读取Application.version作为当前版本号服务器下发最新版本号比较后决定是否弹更新对话框。Android原生层如果想知道当前真正安装的版本可以用PackageManager的getPackageInfo拿versionName防止Unity版本和AndroidManifest里的versionName不一致。提到覆盖安装真机上会有一个常见的困惑明明已经装了旧版本点了新版本安装包之后系统弹出来的却是更新而不是新安装。这其实是正常的APK的签名一致才允许覆盖换签名会报应用未安装或签名不一致错误。如果在做应用内更新一定要保证签名一致尤其是使用了Unity的Keystore做打包时别用Debug签名发布给用户。覆盖安装之后旧应用的进程会被系统杀掉Unity如果不做处理用户重新打开时会看到冷启动画面。如果想要更平滑的体验可以在调起安装之前先保存一个正在升级的标记下一次启动时检测到这个标记就直接展示更新成功提示而不是走新手引导。这个标记用PlayerPrefs就够了Android系统杀进程不会影响它的持久化。5.2 检查APK文件的完整性MD5和签名双重校验下载完成的APK直接安装之前建议做完整性校验。网络下载过程中文件损坏并不少见尤其是用了不稳定的CDN时。服务器在版本接口里返回APK的MD5Unity下载完计算本地文件MD5不一致就不调安装方法这能省下大量解析包出现问题的客诉。C#计算大文件MD5会卡主线程这个要注意。一个几十MB的APK在低端手机上算MD5可能要好几秒必须放到子线程或者用Unity的UnityWebRequest下载时就让服务器返回ETag或者Content-MD5来校验。我在项目里是这么做的下载线程完成后再开一个后台线程用System.Security.Cryptography.MD5算哈希算完回调主线程再弹安装确认框这样UI不会卡住。签名校验也是个不可忽略的点。如果你在Unity里拿到了APK路径可以用Java层的PackageManager.getPackageArchiveInfo读取它的签名对比当前应用安装时的签名不一致就说明包被篡改过了直接拒绝安装。不过这套逻辑对Unity项目来说有点重我也只在商业化要求高的项目里加过普通工具类应用可做可不做。5.3 应用内自更新模块的完整闭环把这个安装功能放进一个完整的新版本更新流程里看大概是这样启动时请求版本接口获取最新版本号、下载地址、MD5、更新说明、是否强制更新。比较版本需要更新则弹窗。强制更新的话弹窗不能关闭给一个下载按钮。点击下载启动协程或线程下载APK到persistentDataPath/apk/显示进度条。下载完成校验MD5和签名校验不通过则提示重新下载或上报错误。校验通过在前台状态下调用ApkInstallerBridge.InstallApk(path)。对于Android 8.0先检查IsInstallAllowed()没权限先跳设置页用户返回后再继续。调起安装器后Unity显示安装完成后请重新打开App。通过版本号变化自检来判断是否安装成功。这套流程我在几个线上项目里跑过整体稳定。需要特别注意的一点是不要在主线程中做下载解压或者MD5计算Unity的严格模式如果在Editor里开了E:\Unity\Editor\Data\PlaybackEngines\AndroidPlayer相关的开发构建选项会直接弹异常。实测子线程Tcp下载比UnityWebRequest在弱网环境下更快但UnityWebRequest代码量少、断点续传方便我一般看项目网络库的统一性来选择只要能正常落盘到约定路径都行。5.4 不需要写Java的极简方案AndroidJavaObject直接调用系统Intent如果项目里实在不想引入AAR也可以纯C#通过AndroidJavaObject调用系统Intent。C#端要构造Intent设置Action为ACTION_VIEW为7.0以上设置FileProvider还要动态传入Uri对象。问题是C#构造Java对象时Uri的生成比较绕你得先创建fileprovider的ContentResolver这部分Java层能读到的类在C#调用时会非常容易踩参数类型的坑。最重要的是C#层直接调用系统Intent拿不到安装结果后续处理全靠猜所以这个方案我只在原型验证时用正式项目还是优先AAR插件。极简方案的核心代码如下using (AndroidJavaClass intentClass new AndroidJavaClass(android.content.Intent)) using (AndroidJavaObject intent new AndroidJavaObject(android.content.Intent, intentClass.GetStaticstring(ACTION_VIEW))) { intent.CallAndroidJavaObject(setDataAndType, apkUri, application/vnd.android.package-archive); intent.CallAndroidJavaObject(addFlags, intentClass.GetStaticint(FLAG_ACTIVITY_NEW_TASK)); activity.Call(startActivity, intent); }如果你只是想在本地快速测一下能不能拉起安装器这段代码可以临时用一下。但请记住生产环境这么做的话FileProvider的配置、未知来源权限检测、安装结果监听全都绕过去了风险很高。我的观点一直是安装功能值得花半天时间正儿八经做个Java插件后续项目移植也只是换个包名的事长期收益更高。5.5 自动化测试建议模拟器与真机差异调试安装功能时模拟器和真机表现差异很大。Android Studio自带的模拟器经常会遇到没有可用的安装器或者Package Installer 未响应的奇怪问题最好直接在真机上验证。我一般是准备三台设备一台原生Android版本11以上测新版系统API一台MIUI或EMUI测国产ROM的权限入口一台旧版本Android 8.0测低版本兼容。验证场景清单也很有用首次安装新应用、覆盖安装旧应用、存储空间不足时安装、下载过程中杀进程再安装、安装过程中取消、安装完成后从最近任务恢复App。把这些场景都过一遍这块功能基本就可以放心上线了。我在测试中还发现过目标SDK版本高导致中文系统下弹窗乱码的情况后来在Manifest里给application加了android:localeConfig或android:supportsRtl等相关配置才解决这种问题不真机测根本发现不了。最后再分享一个我在实际集成中悟出来的小技巧如果你在做Unity Android应用内更新不要只盯着安装APK这一步最好把下载路径的选择和FileProvider路径映射当成一个整体来设计。我前几次做这个功能时都是先随便定个路径等下载完了再来改Java配置白白浪费了很多排查时间。后来我养成了一个习惯动工前先在文档里画一个小表格左边是Unity侧写入路径右边是Java侧FileProvider映射路径确保两者一一对应再写代码。这个表格在后续需要接入第三方SDK、或者同事接手项目时也特别有用人家一看就知道路径规则是什么。还有就是产品层面要和用户把预期对齐。Android从7.0开始对应用自我安装做了很多安全限制这是平台对用户利益的保护开发者只能在系统允许范围内做体验优化。所以弹窗文案上我建议写清楚下载完成后需要在系统弹窗中点击安装同时给一个打开设置的快捷按钮帮助用户提前打开未知来源开关这样能降低至少50%的不知道怎么装的客诉。我自己上线的一个工具类应用加了上面这段引导后新版本覆盖率从71%涨到了86%左右效果很直接。
返回列表