Android APP更新升级完整实现Demo
市场上的Android应用都能够自动提示升级更新,这里就完整的来实现一下AndroidAPP,实现自动升级的功能。
具体如何实现,其实不难,先看看流程:
一、更新方式介绍
1、全量更新
(1) 全量更新的定义
全量更新是指用户下载应用程序或系统的完整新版本安装包来替换旧版本的过程。这通常涉及到下载一个包含所有应用组件和资源的全新文件(如 APK 文件对于 Android 应用),以确保用户获得最新的功能、改进和安全更新。
(2) 全量更新原理与流程
准备完整的新版安装包:
完成新版应用的开发后,会打包生成一个完整的安装文件,这个文件包含了所有的更新内容。
发布新版安装包:
新的安装包会被上传到应用商店或官方服务器,供用户下载。
用户下载完整的新版安装包:
用户通过应用商店或者直接从开发者提供的链接下载整个新的安装包。
安装新版安装包:
下载完成后,用户可以选择覆盖安装(如果系统支持),即在不删除原有数据的情况下直接替换旧版本;或者是先卸载旧版本再安装新版本。
安装后的处理:
安装完成后,可能需要重启应用或设备,以便所有更改生效。此外,可能还需要进行一些初始化设置或数据迁移工作,以确保用户的个人资料和其他数据能正确迁移到新版本中。
2、增量更新
(1) 增量更新的定义
增量更新是通过生成新版本与旧版本之间的差分补丁文件,让用户只下载补丁并合并到旧版本中以完成更新,而无需下载完整的 APK 文件。
(2) 增量更新原理与流程
生成差分补丁文件:
使用工具(如 bsdiff)对旧版和新版的 APK 进行二进制比较,生成差分补丁文件(.patch)。
客户端应用补丁:
使用对应工具(如 bspatch)在客户端对旧版 APK 应用差分补丁,生成新版 APK。
安装生成的 APK:
对新生成的 APK 进行完整性校验后安装。
二、安卓系统中文件变化流程
1、update.apk 的位置与清理
下载后的 update.apk存储在 getExternalFilesDir() 或 Download 目录下(根据应用逻辑)。
安装完成后,该文件不会自动删除,除非应用的代码中明确写入了删除逻辑。
如果不清理,会占用存储空间。常见做法是安装完成后清理下载的 update.apk,确保不浪费用户的存储资源。
整理如下:
增量更新和全量更新在流程上有所不同,特别是在处理文件和文件夹时。
文件/文件夹处理对比表
文件/文件夹 | 位置 | 全量更新变化 | 增量更新变化 |
原 APK 文件 | /data/app/<package_name>/ | 被新的完整 APK 替换。 | 应用差分补丁生成新 APK 后替换旧 APK。 |
私有数据文件夹 | /data/data/<package_name>/ | 一般不变,但可能因更新逻辑被迁移或部分清理。 | 一般不变,但在某些情况下可能会因为版本间的差异需要调整(如数据库结构改变)。 |
Dalvik/ART 缓存 | /data/dalvik-cache/ | 重新生成缓存以适配新 APK。 | 新生成的 APK 可能会触发缓存的重新生成,以适配新版本。 |
下载的 update.apk | getExternalFilesDir()或 Download 目录 | 默认不变,可能需要手动删除。 | 默认不变,但增量更新完成后,建议手动删除以节省存储空间。 |
注意事项
原 APK 文件:
在全量更新中,整个应用被全新的 APK 文件替代;而在增量更新中,只有旧 APK 和新 APK 之间的差异部分通过补丁形式下载并应用于旧 APK。
私有数据文件夹:
无论是全量还是增量更新,通常用户的私有数据不会受到影响,除非更新涉及到对用户数据格式的变更或者优化,此时可能需要进行数据迁移或清理。
Dalvik/ART 缓存:
由于 APK 文件内容发生了变化,无论哪种更新方式,都可能导致系统需要重新编译应用程序代码以创建适合新版本的缓存。
下载的 update.apk:
在完成更新后,不论是全量还是增量更新,下载的临时文件(如 update.apk)都可以选择性地删除,以释放设备上的存储空间。
三、环境搭建(APK实现流程)
1、全量更新客户端实现
(1) AndroidManifest.xml文件配置了应用的基本信息、权限、FileProvider 和主 Activity
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <!-- 权限声明,放在 <application> 标签之前 --> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/Theme.OTA"> <!-- FileProvider 配置,用于安装 APK --> <provider android:name="androidx.core.content.FileProvider" android:authorities="com.example.a1.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider> <!-- MainActivity 配置 --> <activity android:name=".MainActivity" android:exported="true" android:label="OTA Demo" android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
(2) MainActivity.java 文件定义了一个主活动类 `MainActivity`,其中包括以下功能:
① 初始化界面并设置按钮点击事件以检查更新。
② 应用启动时自动检查更新。
③ 显示更新对话框,提示用户是否进行更新。
④ 下载新版本的 APK 文件并安装。
package com.example.a1; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.widget.Button; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.FileProvider; import java.io.File; public class MainActivity extends AppCompatActivity { private static final String APK_URL = "https://qxhut.com/update/app-latest.apk"; // 更新 APK 的 URL @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button checkUpdateButton = findViewById(R.id.check_update_button); checkUpdateButton.setOnClickListener(v -> UpdateManager.checkForUpdates(this)); // 应用启动时自动检查更新 UpdateManager.checkForUpdates(this); } private void showUpdateDialog() { new AlertDialog.Builder(this) .setTitle("发现新版本") .setMessage("有新版本的应用程序,是否现在更新?") .setPositiveButton("是", (dialog, which) -> downloadAndInstallApk()) .setNegativeButton("否", null) .show(); } private void downloadAndInstallApk() { // 下载 APK 文件到设备 File apkFile = new File(getExternalFilesDir(null), "app-update.apk"); UpdateManager.downloadApk(this, APK_URL); // 传递 APK URL 给 UpdateManager // 等待下载完成后执行安装 apkFile.setReadable(true, false); // 设置文件可读权限 Uri apkUri = FileProvider.getUriForFile(this, "com.example.a1.fileprovider", apkFile); Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE); intent.setData(apkUri); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 授予 URI 读取权限 startActivity(intent); } }
(3) UpdateManager.java文件定义了一个用于管理应用更新的类 `UpdateManager`,主要功能包括:
① 检查更新:通过 `checkForUpdates` 方法从服务器获取最新版本信息。
② 异步任务:`FetchVersionInfoTask` 异步任务从服务器获取版本信息,并与当前应用版本进行比较。
③ 显示更新对话框:如果发现新版本,显示更新对话框提示用户。
④ 下载 APK:用户确认更新后,下载新版本的 APK 文件。
⑤ 安装 APK:下载完成后,安装新版本的 APK 文件。
package com.example.a1; import android.app.AlertDialog; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.AsyncTask; import android.app.DownloadManager; import android.net.Uri; import android.os.Environment; import org.json.JSONObject; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; public class UpdateManager { private static final String VERSION_URL = "https://qxhut.com/update/version.json"; public static void checkForUpdates(Context context) { new FetchVersionInfoTask(context).execute(VERSION_URL); } private static class FetchVersionInfoTask extends AsyncTask<String, Void, String> { private final Context context; FetchVersionInfoTask(Context context) { this.context = context; } @Override protected String doInBackground(String... urls) { try { URL url = new URL(urls[0]); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); InputStreamReader reader = new InputStreamReader(connection.getInputStream()); StringBuilder result = new StringBuilder(); int data; while ((data = reader.read()) != -1) { result.append((char) data); } reader.close(); return result.toString(); } catch (Exception e) { e.printStackTrace(); return null; } } @Override protected void onPostExecute(String jsonResponse) { if (jsonResponse != null) { try { JSONObject jsonObject = new JSONObject(jsonResponse); int serverVersionCode = jsonObject.getInt("versionCode"); String apkUrl = jsonObject.getString("apkUrl"); PackageManager packageManager = context.getPackageManager(); PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0); int currentVersionCode = packageInfo.versionCode; // 获取当前保存的 APK URL String currentApkUrl = getCurrentApkUrl(context); if (serverVersionCode > currentVersionCode) { if (!apkUrl.equals(currentApkUrl)) { // 如果服务器返回的 APK URL 不同,则弹出更新对话框并下载 showUpdateDialog(context, apkUrl); } } else { // 已是最新版本,弹出提示 new AlertDialog.Builder(context) .setTitle("提示") .setMessage("当前已是最新版本。") .setPositiveButton("确定", null) .show(); } } catch (Exception e) { e.printStackTrace(); } } } private void showUpdateDialog(Context context, String apkUrl) { new AlertDialog.Builder(context) .setTitle("更新提示") .setMessage("发现新版本,是否立即更新?") .setPositiveButton("确定", (dialog, which) -> { // 调用下载和安装逻辑 downloadApk(context, apkUrl); }) .setNegativeButton("取消", null) .show(); } // 获取当前保存的 APK URL private String getCurrentApkUrl(Context context) { SharedPreferences preferences = context.getSharedPreferences("UpdatePrefs", Context.MODE_PRIVATE); return preferences.getString("apkUrl", ""); // 获取已保存的 APK URL } } // 下载 APK public static void downloadApk(Context context, String apkUrl) { // 保存当前的 APK URL 到 SharedPreferences SharedPreferences preferences = context.getSharedPreferences("UpdatePrefs", Context.MODE_PRIVATE); SharedPreferences.Editor editor = preferences.edit(); editor.putString("apkUrl", apkUrl); // 存储 apkUrl editor.apply(); // 设置下载请求 DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkUrl)); request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, "app-update.apk"); request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); // 获取 DownloadManager 并启动下载 DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); if (manager != null) { manager.enqueue(request); } // 监听下载完成事件 context.registerReceiver(new android.content.BroadcastReceiver() { @Override public void onReceive(Context context, android.content.Intent intent) { long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); if (downloadId != -1) { Uri apkUri = Uri.fromFile(new java.io.File(context.getExternalFilesDir(null), "app-update.apk")); installApk(context, apkUri); } } }, new android.content.IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); } // 安装 APK private static void installApk(Context context, Uri apkUri) { android.content.Intent intent = new android.content.Intent(android.content.Intent.ACTION_VIEW); intent.setDataAndType(apkUri, "application/vnd.android.package-archive"); intent.addFlags(android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION); context.startActivity(intent); } }
2、增量更新客户端实现
(1) 检查更新
在 UpdateManager 类中,扩展 checkForUpdates 方法来支持差分更新:
public static void checkForUpdates(Context context) { new FetchVersionInfoTask(context).execute(VERSION_URL); } private static class FetchVersionInfoTask extends AsyncTask<String, Void, String> { private final Context context; FetchVersionInfoTask(Context context) { this.context = context; } @Override protected String doInBackground(String... urls) { // 获取服务器返回的 JSON 数据 try { URL url = new URL(urls[0]); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); InputStreamReader reader = new InputStreamReader(connection.getInputStream()); StringBuilder result = new StringBuilder(); int data; while ((data = reader.read()) != -1) { result.append((char) data); } reader.close(); return result.toString(); } catch (Exception e) { e.printStackTrace(); return null; } } @Override protected void onPostExecute(String jsonResponse) { if (jsonResponse != null) { try { JSONObject jsonObject = new JSONObject(jsonResponse); int serverVersionCode = jsonObject.getInt("versionCode"); String apkUrl = jsonObject.getString("apkUrl"); String patchUrl = jsonObject.optString("patchUrl", null); long patchSize = jsonObject.optLong("patchSize", 0); PackageManager packageManager = context.getPackageManager(); PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0); int currentVersionCode = packageInfo.versionCode; if (serverVersionCode > currentVersionCode) { // 如果存在增量更新,则显示增量更新提示 if (patchUrl != null && !patchUrl.isEmpty()) { showPatchUpdateDialog(context, patchUrl, patchSize); } else { // 如果没有增量更新,使用全量更新 showFullUpdateDialog(context, apkUrl); } } else { // 已是最新版本,提示用户 new AlertDialog.Builder(context) .setTitle("提示") .setMessage("当前已是最新版本。") .setPositiveButton("确定", null) .show(); } } catch (Exception e) { e.printStackTrace(); } } } private void showPatchUpdateDialog(Context context, String patchUrl, long patchSize) { new AlertDialog.Builder(context) .setTitle("更新提示") .setMessage("发现新版本,是否立即更新?") .setPositiveButton("确定", (dialog, which) -> { downloadPatch(context, patchUrl, patchSize); }) .setNegativeButton("取消", null) .show(); } private void showFullUpdateDialog(Context context, String apkUrl) { new AlertDialog.Builder(context) .setTitle("更新提示") .setMessage("发现新版本,是否立即更新?") .setPositiveButton("确定", (dialog, which) -> { downloadApk(context, apkUrl); }) .setNegativeButton("取消", null) .show(); } }
(2) 下载补丁
客户端下载差分补丁文件,并保存到本地:
public static void downloadPatch(Context context, String patchUrl, long patchSize) { // 设置下载请求 DownloadManager.Request request = new DownloadManager.Request(Uri.parse(patchUrl)); request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, "update.patch"); request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); // 启动下载 DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); if (manager != null) { manager.enqueue(request); } // 监听下载完成事件 context.registerReceiver(new android.content.BroadcastReceiver() { @Override public void onReceive(Context context, android.content.Intent intent) { long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); if (downloadId != -1) { Uri patchUri = Uri.fromFile(new File(context.getExternalFilesDir(null), "update.patch")); mergePatch(context, patchUri); } } }, new android.content.IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); }
(3) 合并补丁并生成新 APK
使用 bspatch 命令合并 APK 和补丁:
private void mergePatch(Context context, Uri patchUri) { File oldApk = new File(context.getExternalFilesDir(null), "old-app.apk"); // 旧 APK 文件 File patchFile = new File(context.getExternalFilesDir(null), "update.patch"); // 补丁文件 File newApk = new File(context.getExternalFilesDir(null), "new-app.apk"); // 新 APK 文件 try { // 调用 bspatch 合并补丁 String command = "bspatch " + oldApk.getAbsolutePath() + " " + newApk.getAbsolutePath() + " " + patchFile.getAbsolutePath(); Process process = Runtime.getRuntime().exec(command); process.waitFor(); // 校验 APK 完整性 if (checkApkIntegrity(newApk)) { installApk(context, Uri.fromFile(newApk)); } else { throw new Exception("APK Integrity check failed."); } } catch (Exception e) { e.printStackTrace(); } } (4)
安装 APK
安装新生成的 APK 文件:
private static void installApk(Context context, Uri apkUri) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(apkUri, "application/vnd.android.package-archive"); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); context.startActivity(intent); }
(5) 注意事项
1. 增量更新工具的选择:可以选择不同的差分工具(如 bsdiff)来生成和应用补丁,bspatch 是常用的选择。
2. 安全性:在处理补丁文件时,要确保文件来源可信,并进行完整性校验(如使用 SHA256 校验)。
3. 回退机制:在应用补丁时,若发现问题,可以提供回退机制,恢复到原始版本。
这样,增量更新的流程就完整了,您可以根据这个方案进行实现和测试。如果有任何问题或需要进一步的帮助,随时告知!
四、搭建本地Web服务器:
1、搭建一个http服务器
2、全量更新服务所需文件
(1) 放置两个文件,一个是1.2版本的apk文件
(2) 一个是ver.aspx文件用于获取服务器版本的json字符串,里面的内容为:
{ "versionCode": 3, "versionName": "1.2", "updateMessage": "1. 修复了已知问题\n2. 提升了性能和稳定性\n3. 添加了新功能", "forceUpdate": false, "apkUrl": "https://qxhut.com/update/app-latest.apk" }
3、增量更新服务所需文件
(1) 服务端流程
使用 bsdiff 工具生成差分补丁文件。
bsdiff old_version.apk new_version.apk update.patch
(2) 提供版本信息
在服务端提供一个 version.json 文件,文件内容包括新版本的信息、补丁文件的 URL 和大小:
{ "versionCode": 3, "versionName": "1.2", "updateMessage": "1. 修复了已知问题\n2. 提升了性能和稳定性", "forceUpdate": false, "apkUrl": "https://example.com/app-latest.apk", "patchUrl": "https://example.com/app-latest.patch", "patchSize": 2048576 }
上述操作结束后,服务器端就完全搭好了,可以开始测试升级流程。
目录 返回
首页