日常记录

Android APP更新升级完整实现Demo

19 12月
作者:6912809|分类:学习心得


市场上的Android应用都能够自动提示升级更新,这里就完整的来实现一下AndroidAPP,实现自动升级的功能。

具体如何实现,其实不难,先看看流程:

 

 

image.png 

一、更新方式介绍

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
}

 

上述操作结束后,服务器端就完全搭好了,可以开始测试升级流程。

 


打赏
浏览63 评论0
返回
目录
返回
首页

除特别注明外,本站所有文章均为七原创,转载请注明出处来自https://qxhut.com/?id=42

本站提供的一切软件、教程和内容信息仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络收集整理,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。如果您喜欢该程序和内容,请支持正版,购买注册,得到更好的正版服务。我们非常重视版权问题,如有侵权请邮件与我们联系处理。敬请谅解!

MT8676安卓OTA升级原理与内部文件变化

发表评论