Cocos Creator 3.x APK热更新全流程:版本检测、下载与安卓原生安装实战

发布时间:2026/7/26 4:00:36

Cocos Creator 3.x APK热更新全流程:版本检测、下载与安卓原生安装实战 1. 项目概述与核心价值最近在社区里看到不少Cocos Creator的开发者特别是刚入坑3.x版本的朋友都在头疼同一个问题游戏上线后怎么让玩家手里的老版本APK能自动检测到新版本并完成更新这可不是个简单的“提示一下”就完事了它涉及到版本号的比对、更新包的获取、下载进度的展示以及在安卓系统权限越来越严格的今天如何安全地把一个APK文件安装到用户设备上。我自己在迭代项目时也在这个环节踩过不少坑从简单的弹窗提示到构建一套相对鲁棒的更新流程中间经历了多次方案重构。今天我就结合Cocos Creator 3.8.0这个当前比较主流的稳定版本把手头上经过多个项目验证的这套“APK版本检测及下载更新”流程拆解开来从头到尾捋一遍。我们的目标不仅仅是实现功能更要追求用户体验的流畅和开发维护的简便。你会看到我们将从最基础的版本号定义讲起一步步搭建起一个包含检测、提示、下载、安装的完整闭环。这个过程会涉及到Cocos Creator的前端逻辑、与后端服务器的简单交互、以及调用安卓原生能力等关键环节。无论你是独立开发者还是团队中的一员这套流程都能为你节省大量摸索时间直接应用到你的项目中去。2. 整体架构设计与思路拆解在动手写代码之前我们先得把整个流程的骨架搭好想清楚数据从哪里来到哪里去每个环节可能遇到什么“槛”。一个完整的APK更新流程可以抽象为四个核心阶段检测 - 比对 - 获取 - 安装。2.1 核心流程四阶段解析检测阶段目标是获取两个关键版本号一是当前安装在用户设备上的APK版本本地版本二是发布在服务器上的最新APK版本远程版本。本地版本号相对容易我们可以从App的配置信息中读取。而远程版本号的获取通常需要一个轻量级的接口比如一个简单的JSON文件我们常称之为version.json放在你的更新服务器上里面包含了最新版本的号、下载地址、更新日志等信息。这样做的好处是每次检查更新只需要请求这个很小的JSON文件而不用去请求完整的APK包速度快、流量省。比对阶段就是一场简单的“数字游戏”。但这里有个关键版本号不能是随便写的字符串必须有统一的规则以便于程序比较。最常见的是采用“主版本号.次版本号.修订号”如1.2.3的格式。在代码里我们需要一个函数来解析和比较这两个版本字符串判断远程版本是否大于本地版本。这里要特别注意处理各种边界情况比如版本号位数不同1.2vs1.2.0或者带有“v”前缀等。获取阶段当确定需要更新后就要开始下载新的APK文件。这个阶段的核心挑战在于网络稳定性和用户体验。我们不能让用户干等着必须提供一个清晰的下载进度条。在Cocos Creator中我们可以使用assetManager或更底层的XMLHttpRequest来下载文件并监听其进度事件。下载下来的APK文件需要保存到设备的某个安全目录比如Android的外部存储空间/Download/目录下并确保我们应用有权限写入这个位置。安装阶段这是整个流程中最“原生”的一步也是安卓版本迭代中变化最大、最需要注意兼容性的地方。从Android 7.0 (Nougat) 开始直接通过file://URI安装APK的方式被禁止必须使用FileProvider来生成一个content://URI。从Android 8.0 (Oreo) 开始又增加了“未知来源应用安装”的运行时权限。从Android 10 (Q) 开始对外部存储的访问权限Scoped Storage又有了新规。我们的代码必须妥善处理这些不同安卓版本的差异。2.2 技术方案选型与工具准备基于以上分析我们的技术栈如下游戏引擎Cocos Creator 3.8.0。我们将主要使用TypeScript编写游戏内的逻辑。原生交互为了调用安装APK的安卓API我们必须通过Cocos Creator的**原生反射Native Reflection**机制在TypeScript中调用Java代码。这需要我们对Android项目结构有一定了解。网络请求使用Cocos Creator内置的assetManager或jsb.HttpRequest来下载版本配置文件和APK文件。assetManager更友好自带缓存和重试机制jsb.HttpRequest则更底层可控性更强。文件存储使用jsb.fileUtils这个原生桥接模块来获取设备上的可写路径并进行文件操作。后端准备你需要准备一台Web服务器任何能通过HTTP访问的静态文件服务器都可以比如Nginx、Apache甚至对象存储服务用于存放version.json和最新的APK文件。注意在开始编码前请确保你的Cocos Creator 3.8.0项目已经成功构建并发布过Android平台并且能够正常运行在真机上。这保证了你的开发环境包括SDK、NDK、构建工具是正确配置的。3. 核心模块实现与代码详解理论说得再多不如一行代码。接下来我们进入实战环节我会把核心代码模块逐一实现并解释每一行代码的意图和注意事项。3.1 定义版本信息与比对逻辑首先我们需要定义版本信息的数据结构并实现版本号比较算法。创建一个TypeScript脚本例如UpdateManager.ts。// UpdateManager.ts 部分代码 export interface VersionInfo { version: string; // 版本号如 1.2.3 downloadUrl: string; // 新APK的完整下载地址 forceUpdate: boolean; // 是否强制更新 updateLog: string; // 更新日志用于展示给用户 apkSize: number; // APK文件大小单位字节用于计算下载进度和提示用户 } export class UpdateManager { // 本地版本号可以从引擎或配置中读取 private localVersion: string ; // 远程版本信息 private remoteVersionInfo: VersionInfo | null null; constructor() { // 初始化本地版本号这里假设我们打包时将一个版本号写入了某个配置或常量 // 一种简单做法在项目根目录放一个version.json构建时自动更新运行时读取 // 另一种做法直接定义一个常量。这里为了演示我们手动设置。 this.localVersion 1.0.0; // 实际项目中应从配置读取 } /** * 比较两个版本号字符串的大小。 * param v1 版本号1如 1.2.3 * param v2 版本号2 * returns 如果v1 v2返回1v1 v2返回-1相等返回0 */ private compareVersion(v1: string, v2: string): number { // 去除可能的v前缀 v1 v1.replace(/^v/i, ); v2 v2.replace(/^v/i, ); const parts1 v1.split(.).map(Number); const parts2 v2.split(.).map(Number); const maxLength Math.max(parts1.length, parts2.length); for (let i 0; i maxLength; i) { const num1 parts1[i] || 0; const num2 parts2[i] || 0; if (num1 num2) return 1; if (num1 num2) return -1; } return 0; } /** * 检查是否需要更新 * returns true 需要更新 false 已经是最新 */ public checkIfNeedUpdate(): boolean { if (!this.remoteVersionInfo) { console.warn(远程版本信息未获取请先调用 fetchRemoteVersion); return false; } const result this.compareVersion(this.remoteVersionInfo.version, this.localVersion); return result 0; // 远程版本 本地版本才需要更新 } }代码解读与注意事项compareVersion函数是核心它先将版本字符串按点分割成数字数组然后逐位比较。这种方法能正确处理1.2和1.2.0会被视为相等。在实际项目中localVersion不应硬编码。推荐的做法是在构建打包时由CI/CD流程自动生成一个包含版本号的配置文件如project_version.json并打包进APK的assets目录。游戏启动时读取这个文件获取版本号。这样可以保证构建版本和代码中判断的版本绝对一致避免人为错误。VersionInfo接口中的forceUpdate字段非常有用。对于修复重大BUG或涉及安全问题的更新你可以将其设为true然后在游戏内强制弹窗不给用户“忽略”的选项直到更新完成。3.2 获取远程版本信息与更新提示接下来我们要从服务器获取version.json并提示用户。// UpdateManager.ts 续 export class UpdateManager { // ... 之前的代码 ... // 服务器上version.json的地址请替换为你自己的地址 private readonly VERSION_CONFIG_URL https://your-update-server.com/version.json; /** * 从服务器获取远程版本信息 */ public async fetchRemoteVersion(): Promiseboolean { return new Promiseboolean((resolve, reject) { // 使用assetManager下载它支持更友好的进度和重试 assetManager.downloader.download(this.VERSION_CONFIG_URL, (err, text) { if (err) { console.error(获取远程版本信息失败:, err); // 这里可以加入重试逻辑或者给用户一个网络错误的提示 reject(err); return; } try { const config JSON.parse(text); this.remoteVersionInfo { version: config.version, downloadUrl: config.downloadUrl, forceUpdate: config.forceUpdate || false, updateLog: config.updateLog || , apkSize: config.apkSize || 0 }; console.log(获取到远程版本信息:, this.remoteVersionInfo); resolve(true); } catch (e) { console.error(解析版本配置文件失败:, e); reject(e); } }); }); } /** * 执行完整的更新检查流程 */ public async performUpdateCheck(): Promisevoid { // 1. 获取远程信息 try { await this.fetchRemoteVersion(); } catch (e) { // 网络错误处理可以提示用户“检查网络设置” this.showToast(网络连接失败请检查网络后重试); return; } // 2. 检查是否需要更新 if (this.checkIfNeedUpdate()) { // 3. 弹出更新提示框 this.showUpdateDialog(); } else { console.log(当前已是最新版本); this.showToast(当前已是最新版本); // 可以继续进入游戏主逻辑 } } private showUpdateDialog(): void { // 这里应该调用你的UI系统弹出一个模态对话框 // 对话框内容应包括 // - 显示 remoteVersionInfo.updateLog // - 显示APK大小 (格式化为 MB) // - 两个按钮“立即更新”和“稍后再说”如果forceUpdate为true则只显示“立即更新” // 示例 const apkSizeMB (this.remoteVersionInfo!.apkSize / (1024 * 1024)).toFixed(2); const message 发现新版本 ${this.remoteVersionInfo!.version}\n\n更新内容\n${this.remoteVersionInfo!.updateLog}\n\n文件大小${apkSizeMB} MB; // 假设你有一个全局的UI管理器这里调用它的弹窗方法 // UIManager.instance.showConfirmDialog(message, this.onUserConfirmUpdate.bind(this), this.remoteVersionInfo!.forceUpdate); console.log(弹出更新对话框:, message); // 为了演示我们直接模拟用户点击了更新 this.onUserConfirmUpdate(true); } private onUserConfirmUpdate(confirmed: boolean): void { if (confirmed) { this.startDownloadAPK(); } else { if (this.remoteVersionInfo!.forceUpdate) { // 如果是强制更新用户不同意则可能直接退出游戏 this.showToast(必须更新后才能继续使用); // 可以在这里延迟几秒后再次弹出对话框或者直接退出 // jsb.close(); // 谨慎使用 } else { // 非强制更新用户选择忽略可以记录一次下次启动再提示 console.log(用户选择忽略本次更新); // 继续进入游戏... } } } private showToast(msg: string): void { // 简单提示实际项目中应接入你的提示系统 console.log([Toast], msg); } }实操心得assetManager.downloader.download在Cocos Creator中是一个很好的选择它在原生平台上有更好的兼容性。你也可以使用fetch或XMLHttpRequest但在某些WebView或平台环境下可能需要额外处理跨域等问题。错误处理至关重要。网络请求可能失败JSON可能格式错误必须用try...catch包裹并给用户明确的反馈而不是让游戏卡住或崩溃。更新对话框的UI体验直接影响转化率。清晰的更新日志、准确的文件大小、美观的进度条都能增加用户立即更新的意愿。对于强制更新要设计得让用户无法绕过但提示语要友好。3.3 实现APK下载与进度展示当用户确认更新后就开始下载APK文件。我们需要实时展示下载进度。// UpdateManager.ts 续 export class UpdateManager { // ... 之前的代码 ... private downloadTask: any null; // 用于保存下载任务以便取消 private apkSavePath: string ; // 下载的APK保存路径 private startDownloadAPK(): void { if (!this.remoteVersionInfo) { return; } const url this.remoteVersionInfo.downloadUrl; console.log(开始下载APK:, url); // 确定APK保存路径 // 在Android上我们通常保存到外部存储的Download目录用户可见且应用通常有写入权限 let saveDir ; if (sys.isNative sys.os sys.OS.ANDROID) { // jsb.fileUtils.getWritablePath() 获取的是应用内部存储路径外部应用无法直接访问。 // 我们需要获取外部存储的公共目录。 // 注意Android 10 作用域存储下直接写入Download目录可能需要MANAGE_EXTERNAL_STORAGE权限通常我们使用MediaStore API。 // 为简化这里先演示写入应用内部目录安装时再通过FileProvider分享。 saveDir jsb.fileUtils.getWritablePath() update/; } else { // 非Android平台或编辑器环境保存到临时位置 saveDir jsb.fileUtils.getWritablePath() update/; } // 确保目录存在 if (!jsb.fileUtils.isDirectoryExist(saveDir)) { jsb.fileUtils.createDirectory(saveDir); } // 生成文件名可以用版本号命名以避免重复 const fileName game_update_${this.remoteVersionInfo.version}.apk; this.apkSavePath saveDir fileName; // 先检查文件是否已存在比如上次下载中断 if (jsb.fileUtils.isFileExist(this.apkSavePath)) { const fileSize jsb.fileUtils.getFileSize(this.apkSavePath); if (fileSize this.remoteVersionInfo.apkSize) { console.log(APK已存在且完整直接安装); this.installAPK(); return; } else { console.log(APK已存在但不完整删除重新下载); jsb.fileUtils.removeFile(this.apkSavePath); } } // 使用XMLHttpRequest进行下载以便更好地控制进度 const xhr new XMLHttpRequest(); xhr.open(GET, url, true); xhr.responseType arraybuffer; // 重要用于下载二进制文件 // 监听进度事件 xhr.onprogress (event) { if (event.lengthComputable) { const percent (event.loaded / event.total * 100).toFixed(1); // 更新UI进度条 this.updateDownloadProgress(parseFloat(percent), event.loaded, event.total); console.log(下载进度: ${percent}%); } }; xhr.onload () { if (xhr.status 200) { // 下载完成保存文件 const arrayBuffer xhr.response; if (arrayBuffer) { const uint8Array new Uint8Array(arrayBuffer); // 使用jsb.fileUtils写入文件 const success jsb.fileUtils.writeDataToFile(uint8Array, this.apkSavePath); if (success) { console.log(APK下载并保存成功:, this.apkSavePath); this.updateDownloadProgress(100, event.total, event.total); // 下载完成开始安装 setTimeout(() { this.installAPK(); }, 500); // 稍作延迟让进度条走完 } else { console.error(保存APK文件失败); this.showToast(保存更新文件失败请检查存储空间); } } } else { console.error(下载失败HTTP状态码:, xhr.status); this.showToast(下载失败请检查网络); } }; xhr.onerror (err) { console.error(下载请求发生错误:, err); this.showToast(网络错误下载失败); }; this.downloadTask xhr; xhr.send(); } private updateDownloadProgress(percent: number, loaded: number, total: number): void { // 这里应该更新你的UI进度条组件 // 例如this.progressBar.progress percent / 100; // 同时可以更新文本${(loaded/(1024*1024)).toFixed(2)}MB / ${(total/(1024*1024)).toFixed(2)}MB console.log(更新UI进度: ${percent}%); } public cancelDownload(): void { if (this.downloadTask) { this.downloadTask.abort(); this.downloadTask null; console.log(下载已取消); } } }避坑指南文件保存路径这是安卓开发的一个大坑。在旧版本安卓上写入外部存储/Download/目录相对简单。但在Android 10及以上版本由于作用域存储应用默认只能访问自己专属的外部存储目录和特定的媒体文件。对于APK这种非媒体文件写入公共目录需要申请MANAGE_EXTERNAL_STORAGE权限这个权限很敏感上架Google Play需要特殊声明且容易被拒。因此更推荐的做法是将APK下载到应用的内部私有目录getWritablePath()获取的路径然后通过FileProvider安全地分享给系统安装器。我们的代码正是采用了这种更稳妥的方式。断点续传上述代码做了简单的“文件存在且大小匹配则跳过”的逻辑。对于大型APK更专业的做法是实现断点续传这需要服务器支持Range请求头并且客户端记录已下载的字节位置。对于大多数手游更新APK通常小于200MB简单的重新下载通常可以接受。进度计算xhr.onprogress中的event.lengthComputable属性很重要只有当服务器返回了正确的Content-Length响应头时它才为true我们才能计算出准确的百分比。确保你的更新服务器配置正确。3.4 调用安卓原生接口安装APK最后也是最关键的一步引导系统安装我们下载好的APK。由于涉及原生API我们需要编写Java代码并通过Cocos的反射机制调用。第一步在Cocos Creator项目的native/engine/android/java目录下如果没有则创建创建我们的Java工具类。// 文件路径native/engine/android/java/com/yourcompany/gameutils/APKInstaller.java package com.yourcompany.gameutils; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build; import androidx.core.content.FileProvider; import java.io.File; public class APKInstaller { // 这个Authority需要和AndroidManifest.xml中配置的FileProvider的authorities属性完全一致。 // 通常格式是你的应用包名.fileprovider private static final String FILE_PROVIDER_AUTHORITY com.yourcompany.yourgame.fileprovider; public static void installAPK(Context context, String apkFilePath) { if (context null || apkFilePath null || apkFilePath.isEmpty()) { return; } File apkFile new File(apkFilePath); if (!apkFile.exists()) { return; } Intent intent new Intent(Intent.ACTION_VIEW); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); Uri apkUri; // 判断Android版本 if (Build.VERSION.SDK_INT Build.VERSION_CODES.N) { // Android 7.0及以上使用FileProvider // 注意FILE_PROVIDER_AUTHORITY 必须替换为你自己的 apkUri FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, apkFile); // 授予临时读写权限给安装程序 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 如果需要也可以添加FLAG_GRANT_WRITE_URI_PERMISSION但安装通常只需要读 // intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); } else { // Android 7.0以下使用旧方式 apkUri Uri.fromFile(apkFile); } intent.setDataAndType(apkUri, application/vnd.android.package-archive); // 处理Android 8.0的未知来源应用安装权限 if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { boolean hasInstallPermission context.getPackageManager().canRequestPackageInstalls(); if (!hasInstallPermission) { // 如果没有权限需要引导用户去设置页面开启 // 这里我们只是简单处理实际应该弹窗提示并跳转设置 // intent.setAction(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES); // intent.setData(Uri.parse(package: context.getPackageName())); // 注意直接跳转设置会中断安装流程更好的做法是检测到无权限时 // 给用户一个提示对话框用户确认后跳转设置返回后重新调用installAPK。 // 这部分逻辑较复杂此处省略。我们可以先尝试直接安装系统会弹出权限申请。 // 对于上架的应用通常会在商店描述中要求用户提前开启此权限。 } } try { context.startActivity(intent); } catch (Exception e) { e.printStackTrace(); // 可能没有找到处理该Intent的Activity即没有安装程序或者权限问题 } } }第二步配置AndroidManifest.xml添加FileProvider。你需要修改Cocos Creator构建生成的Android项目中的AndroidManifest.xml文件。通常我们通过Cocos Creator的构建模板功能来实现避免每次构建都被覆盖。在Cocos Creator项目根目录下创建build-templates文件夹。在build-templates下创建路径android/app/AndroidManifest.xml。将原生工程中的AndroidManifest.xml内容复制过来并添加application标签内的provider配置。!-- 文件路径build-templates/android/app/AndroidManifest.xml -- ?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.yourcompany.yourgame !-- 记得申请必要的权限如网络权限已经在Cocos默认模板中 -- uses-permission android:nameandroid.permission.INTERNET / !-- 如果需要写入外部存储如果采用下载到公共目录的方案则需要此权限 -- !-- uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE / -- !-- Android 11 (API 30) 及以上如果要管理所有文件需要此特殊权限但慎用 -- !-- uses-permission android:nameandroid.permission.MANAGE_EXTERNAL_STORAGE / -- application android:allowBackuptrue android:icondrawable/icon android:labelstring/app_name android:themestyle/AppTheme !-- 你的其他Activity包括Cocos2dxActivity -- !-- 关键配置FileProvider -- provider android:nameandroidx.core.content.FileProvider android:authoritiescom.yourcompany.yourgame.fileprovider !-- 必须和Java代码中的AUTHORITY一致 -- android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/file_paths / !-- 指向一个XML资源定义共享路径 -- /provider !-- ... 其他配置 ... -- /application /manifest第三步创建FileProvider的路径配置文件。在build-templates/android/app/res/xml/目录下需手动创建xml文件夹创建file_paths.xml文件。!-- 文件路径build-templates/android/app/res/xml/file_paths.xml -- ?xml version1.0 encodingutf-8? paths xmlns:androidhttp://schemas.android.com/apk/res/android !-- root-path: 设备根目录危险一般不使用 files-path: Context.getFilesDir() 指向的内部存储文件目录 cache-path: Context.getCacheDir() 指向的内部存储缓存目录 external-path: Environment.getExternalStorageDirectory() 指向的外部存储根目录 external-files-path: Context.getExternalFilesDir(String) 指向的应用专属外部存储目录 external-cache-path: Context.getExternalCacheDir() 指向的应用专属外部缓存目录 -- !-- 我们选择应用内部文件目录这是最安全且不需要额外权限的 -- files-path nameinternal_update pathupdate/ / !-- 如果你选择将APK下载到外部缓存可以添加这个 -- !-- external-cache-path nameexternal_cache_update path. / -- /paths这个配置告诉FileProvider我们愿意分享内部存储/files/update/目录下的文件。注意pathupdate/它对应我们TypeScript代码中保存APK的.../update/子目录。第四步在TypeScript中调用Java方法。回到我们的UpdateManager.ts添加安装方法。// UpdateManager.ts 续 export class UpdateManager { // ... 之前的代码 ... private installAPK(): void { if (!sys.isNative || sys.os ! sys.OS.ANDROID) { console.warn(非Android原生平台无法安装APK); return; } if (!this.apkSavePath || !jsb.fileUtils.isFileExist(this.apkSavePath)) { console.error(APK文件不存在无法安装:, this.apkSavePath); this.showToast(安装文件丢失请重新下载); return; } console.log(准备安装APK:, this.apkSavePath); // 通过反射调用我们写的Java方法 if (jsb.reflection) { // 注意Java类的全路径必须完全正确 const className com/yourcompany/gameutils/APKInstaller; const methodName installAPK; const methodSignature (Landroid/content/Context;Ljava/lang/String;)V; // 获取Android的Context对象即Activity const context jsb.reflection.getStaticMethod(org/cocos2dx/lib/Cocos2dxHelper, getActivity, ()Landroid/app/Activity;)(); // 调用静态方法 jsb.reflection.callStaticMethod(className, methodName, methodSignature, context, this.apkSavePath); } else { console.error(反射API不可用); } } }关键点梳理包名与Authority请务必将代码和配置中的所有com.yourcompany.yourgame替换成你自己应用的包名。这是整个流程能跑通的核心不一致会导致FileProvider权限错误安装失败。构建模板使用build-templates目录是Cocos Creator官方推荐的定制原生工程的方式。这样每次构建发布Android项目时你的AndroidManifest.xml和file_paths.xml都会被自动合并进去无需手动修改构建后的工程。权限处理对于Android 8.0的“未知来源”安装权限我们的Java代码中给出了注释。一个更友好的实现是在调用安装前先检查canRequestPackageInstalls()如果为false则先跳转到系统设置页面Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES等用户返回后再重试安装。这部分交互逻辑需要结合你的游戏UI来设计。4. 流程整合与优化实践现在我们已经有了所有零件接下来就是把它们组装起来形成一个稳定、用户友好的更新流程并考虑一些优化和边界情况。4.1 构建完整的更新管理器我们将UpdateManager类完善提供一个清晰的入口。// UpdateManager.ts 最终整合 export class UpdateManager { private static _instance: UpdateManager | null null; public static get instance(): UpdateManager { if (!this._instance) { this._instance new UpdateManager(); } return this._instance; } // ... 所有之前定义的属性和方法 ... /** * 游戏启动后在合适的时机如闪屏页之后调用此方法 */ public async launchUpdateFlow(): Promiseboolean { console.log(开始更新检查流程...); try { await this.performUpdateCheck(); // 如果不需要更新或者用户跳过了非强制更新这里返回true表示可以进入游戏 // 如果需要更新且正在下载流程会卡在下载和安装环节直到安装完成或用户取消。 // 实际项目中你可能需要更精细的状态管理如使用状态机。 return true; } catch (error) { console.error(更新流程出现未预期错误:, error); // 即使更新流程出错也不应该阻塞游戏进入除非是强制更新且网络绝对必需 // 这里可以根据错误类型决定是否让用户进入游戏 return true; } } } // 在游戏启动脚本如GameLauncher.ts中使用 // const updateManager UpdateManager.instance; // updateManager.launchUpdateFlow().then(canEnterGame { // if (canEnterGame) { // // 跳转到游戏主场景 // director.loadScene(MainScene); // } // });4.2 服务器端version.json配置示例你的更新服务器上version.json文件内容应该类似这样{ version: 1.2.0, downloadUrl: https://your-update-server.com/releases/game_v1.2.0.apk, forceUpdate: false, updateLog: 1. 修复了关卡3可能卡住的BUG。\n2. 优化了角色移动的手感。\n3. 新增了春节限定皮肤。, apkSize: 102400000 }确保这个文件可以被公开访问并且downloadUrl指向的APK文件也能被正确下载。4.3 常见问题与排查技巧实录在实际开发和测试中你几乎一定会遇到下面这些问题。我把它们和解决方案整理成了表格方便你快速排查。问题现象可能原因排查步骤与解决方案检测更新时网络请求失败1. 服务器地址错误或不可用。2. 客户端网络权限未开启。3. 服务器跨域CORS问题主要在浏览器或模拟器环境。1. 检查VERSION_CONFIG_URL是否正确用浏览器直接访问试试。2. 确认AndroidManifest.xml已添加uses-permission android:nameandroid.permission.INTERNET /。3. 对于Web调试在服务器配置CORS头。对于原生一般无此问题。版本号对比永远返回“已是最新”1. 本地版本号localVersion设置错误比服务器版本还高。2.version.json中的版本号格式与比较函数不兼容如带了“v”或日期格式。3. 网络请求成功但解析version.json失败remoteVersionInfo为null。1. 打印出本地和远程版本号进行对比。确保本地版本号是从正确的配置中读取的。2. 确保版本号使用x.y.z数字格式。我们的compareVersion函数已处理了“v”前缀。3. 在fetchRemoteVersion中打印出收到的text检查JSON格式是否正确。下载进度条不更新或卡住1. 服务器没有返回正确的Content-Length头导致lengthComputable为false。2. 网络缓慢或不稳定进度事件触发不频繁。3. UI更新代码有误未在主线程更新进度条。1. 检查你的Web服务器如Nginx配置确保对APK文件的响应包含Content-Length。2. 可以增加一个基于已下载字节数的粗略进度计算作为备选。3. Cocos Creator的UI操作必须在主线程确保进度回调中更新UI的代码是安全的。下载完成保存文件失败1. 存储路径不可写权限不足。2. 磁盘空间不足。3. 路径中包含非法字符或目录不存在。1. 确保使用的是jsb.fileUtils.getWritablePath()这是应用私有目录一定有写权限。2. 检查设备存储空间。3. 使用jsb.fileUtils.createDirectory创建目录并对文件名进行过滤移除非法字符。点击安装没反应或提示“解析包错误”1.最常见FileProvider的authorities配置与Java代码中不匹配。2.file_paths.xml中配置的路径与APK实际保存路径不匹配。3. 下载的APK文件损坏或不完整。4. Android 8.0未知来源安装权限未开启。1.仔细核对AndroidManifest.xml中的android:authorities、Java代码中的FILE_PROVIDER_AUTHORITY必须完全一致且包含你的正确包名。2. 检查file_paths.xml中files-path的path属性。我们配置的是pathupdate/那么APK必须保存在内部存储/files/update/下。打印出this.apkSavePath确认。3. 对比下载文件的MD5与服务器上的原始文件是否一致。4. 在应用内弹窗引导用户去设置中开启“允许来自此来源的应用”。安装时提示“禁止安装”或“存在安全风险”1. 新APK的签名与已安装APP的签名不一致。2. 系统安全软件或设置拦截。1.绝对确保用于打包新版本APK的签名文件keystore与线上版本使用的完全一致。丢失签名文件将无法覆盖安装必须卸载重装。2. 这是系统行为可以提示用户“本次更新经过安全检测请放心安装”。在Android 11设备上无法找到下载的APK文件使用了需要MANAGE_EXTERNAL_STORAGE权限的路径但未申请或用户未授权。坚持使用应用私有目录getWritablePath()。这是谷歌推荐的做法无需敏感权限。我们的方案正是如此所以通常不会遇到此问题。如果因历史原因用了外部路径需要考虑适配作用域存储。独家避坑技巧调试FileProvider这是最难调试的部分。当安装没反应时打开Android Studio的Logcat过滤FileProvider或Install关键字通常会有详细的错误日志比如FileNotFoundException或Permission Denial这些日志能直接指出是路径问题还是权限问题。版本号管理自动化将本地版本号写入一个project_version.json文件并在构建打包时通过脚本如Node.js自动从package.json或CI/CD的环境变量中读取并更新这个文件。这样彻底杜绝了手误。降级与回滚我们的compareVersion函数只判断远程版本是否更高。如果你想支持灰度发布或强制回滚到某个旧版本可以修改逻辑让服务器下发的version.json里包含一个minSupportedVersion字段如果本地版本低于这个最低支持版本则强制更新。增量更新对于资源热更新Cocos Creator有成熟的assetManager方案。但对于APK本身增量更新即下载补丁合并成全量APK技术复杂且容易出问题。对于中小型项目全量更新APK是更简单可靠的选择。确保你的APK经过优化如使用Android App Bundle生成更小的APK以节省用户流量。整个流程集成后你应该在游戏启动的早期比如在加载界面调用UpdateManager.instance.launchUpdateFlow()。对于强制更新流程会阻塞直到安装完成对于非强制更新可以允许用户跳过并进入游戏。记得在整个过程中提供清晰、友好的用户提示让用户知道当前在检查、下载还是安装以及为什么要这么做。一个流畅、透明的更新体验能显著提升用户的满意度和更新意愿。

相关新闻