# mypatch
**Repository Path**: gzlpsdp/mypatch
## Basic Information
- **Project Name**: mypatch
- **Description**: App增量更新合并差分文件
- **Primary Language**: Android
- **License**: AGPL-3.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 0
- **Created**: 2024-06-27
- **Last Updated**: 2026-07-15
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# mypatch
[](https://jitpack.io/#com.gitee.gzlpsdp/mypatch)
## 介绍
该库是一个功能完整的 Android 应用更新模块,支持全量更新和差分更新,完全解耦 UI,需配合回调自行实现界面。下面分手动更新和自动更新两种场景,提供从配置到实现的完整代码示例。
# 如何使用
## 1. 添加依赖
在项目根目录的 build.gradle 中添加码云 Maven 仓库:
```gradle
dependencies {
implementation 'com.gitee.gzlpsdp:mypatch:1.6.0'
}
```
## 2. 必要配置
**2.1 添加权限**
在 AndroidManifest.xml 中添加网络权限:
```xml
```
如果希望将 APK 下载到外部存储公共目录(非必须,应用专属目录无需存储权限),则需添加:
```xml
```
**2.2 配置 FileProvider(Android 7.0+ 必需)在 AndroidManifest.xml 的 标签内添加:**
```xml
```
创建 res/xml/file_paths.xml 文件,内容如下(根据你设置的存储路径调整):
```xml
```
也可以直接使用
```xml
```
**在 gradle.properties 中添加:**
```properties
android.useAndroidX=true
android.enableJetifier=true
```
>Jetifier 会自动将旧 Support 库转换为 AndroidX,但如果依赖的是 support-v4:21.0.0(太旧),Jetifier 可能无法转换,建议直接排除。
**2.3 确保 Native 库存在**
库内部依赖 libmyPatch.so,该 so 文件已包含在 AAR 中,无需额外处理。
## 3. 初始化更新模块
建议在自定义 Application 类的 onCreate() 中完成初始化。
```java
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
// 构建配置
UpdateConfig config = new UpdateConfig.Builder(this)
.setCheckUrl("https://your-server.com/api/update") // 替换为你的检查接口
.setApkStorePath(getExternalFilesDir("apk").getAbsolutePath())
.addRequestHeader("User-Agent", "MyApp/1.0") // 可选
.enableLog(BuildConfig.DEBUG) // 调试时开启日志
.build();
// 初始化
AppUpdate.init(config);
}
}
```
别忘了在 AndroidManifest.xml 中注册该 Application:
```xml
...
```
## 4. 手动更新示例(用户主动触发)
**4.1 在 Activity 中实现手动检查**
```java
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_INSTALL_PERMISSION = 1001;
private UpdateInfo latestInfo;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_check_update).setOnClickListener(v -> checkUpdateManually());
}
private void checkUpdateManually() {
// 显示进度对话框
ProgressDialog pd = new ProgressDialog(this);
pd.setMessage("正在检查更新...");
pd.setCancelable(false);
pd.show();
AppUpdate.getInstance().checkUpdate(new UpdateCheckCallback() {
@Override
public void onSuccess(UpdateInfo info) {
pd.dismiss();
latestInfo = info;
String currentVer = AppUpdate.getInstance().getCurrentVersion();
if (needUpdate(currentVer, info.getNewVersion())) {
showUpdateDialog(info);
} else {
Toast.makeText(MainActivity.this, "已是最新版本", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(int code, String message) {
pd.dismiss();
Toast.makeText(MainActivity.this, "检查更新失败:" + message, Toast.LENGTH_SHORT).show();
}
});
}
// 简单版本比较(可根据实际版本格式优化)
private boolean needUpdate(String current, String latest) {
return !current.equals(latest);
}
private void showUpdateDialog(UpdateInfo info) {
AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setTitle("发现新版本 " + info.getNewVersion())
.setMessage(info.getDescription())
.setPositiveButton("立即更新", (dialog, which) -> startDownload(info));
if (info.isForceUpdate()) {
builder.setCancelable(false);
} else {
builder.setNegativeButton("以后再说", null);
}
builder.show();
}
private void startDownload(UpdateInfo info) {
ProgressDialog pd = new ProgressDialog(this);
pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
pd.setMessage("下载中...");
pd.setCancelable(false);
pd.show();
AppUpdate.getInstance().downloadAndInstall(info,
new DownloadCallback() {
@Override
public void onStart() { }
@Override
public void onProgress(int percent) {
pd.setProgress(percent);
}
@Override
public void onComplete(File file) {
pd.dismiss();
}
@Override
public void onError(String message) {
pd.dismiss();
Toast.makeText(MainActivity.this, "下载失败:" + message, Toast.LENGTH_SHORT).show();
}
},
new InstallCallback() {
@Override
public void onSuccess() { }
@Override
public void onFailure(String message) {
Toast.makeText(MainActivity.this, "安装失败:" + message, Toast.LENGTH_SHORT).show();
}
@Override
public void onNeedPermission(Intent intent) {
startActivityForResult(intent, REQUEST_INSTALL_PERMISSION);
}
}
);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_INSTALL_PERMISSION) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (getPackageManager().canRequestPackageInstalls()) {
// 权限已获取,重新下载安装(需保存 latestInfo)
startDownload(latestInfo);
} else {
Toast.makeText(this, "未获得安装权限,无法自动安装", Toast.LENGTH_SHORT).show();
}
}
}
}
}
```
## 5. 自动更新示例(后台静默检查)
自动更新通常在应用启动时或特定时机执行。我们建议封装一个 UpdateManager 来统一管理更新逻辑,避免代码分散。下面是一个简单的实现。
**5.1 创建 UpdateManager 单例类**
```java
public class UpdateManager {
private static volatile UpdateManager instance;
private Context appContext;
private UpdateInfo latestInfo;
private UpdateManager(Context context) {
this.appContext = context.getApplicationContext();
}
public static UpdateManager getInstance(Context context) {
if (instance == null) {
synchronized (UpdateManager.class) {
if (instance == null) {
instance = new UpdateManager(context);
}
}
}
return instance;
}
/**
* 自动检查更新(静默方式)
* @param foregroundActivity 当前前台的 Activity,用于强制更新时弹窗,若为 null 则只发送通知
*/
public void checkAutoUpdate(final Activity foregroundActivity) {
AppUpdate.getInstance().checkUpdate(new UpdateCheckCallback() {
@Override
public void onSuccess(UpdateInfo info) {
latestInfo = info;
String current = AppUpdate.getInstance().getCurrentVersion();
if (!current.equals(info.getNewVersion())) {
// 发现新版本
if (info.isForceUpdate()) {
// 强制更新:必须立即弹窗,需要前台 Activity
if (foregroundActivity != null) {
showForceUpdateDialog(foregroundActivity, info);
} else {
// 无前台 Activity,可发送通知,但强制更新建议直接退出
}
} else {
// 非强制更新:可发送通知栏提醒
showUpdateNotification(info);
}
}
}
@Override
public void onFailure(int code, String message) {
// 静默失败不处理
}
});
}
private void showForceUpdateDialog(Activity activity, UpdateInfo info) {
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle("强制更新")
.setMessage(info.getDescription())
.setPositiveButton("立即更新", (d, w) -> startDownload(activity, info))
.setCancelable(false)
.create();
dialog.setCanceledOnTouchOutside(false);
dialog.show();
}
private void showUpdateNotification(UpdateInfo info) {
// 构建通知(略,可使用 NotificationManager)
// 点击通知后跳转到某个 Activity 并传入 info 开始下载
}
private void startDownload(Activity activity, UpdateInfo info) {
// 同手动更新中的下载安装代码,可复用
ProgressDialog pd = new ProgressDialog(activity);
pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
pd.setMessage("下载中...");
pd.setCancelable(false);
pd.show();
AppUpdate.getInstance().downloadAndInstall(info,
new DownloadCallback() {
@Override
public void onStart() { }
@Override
public void onProgress(int percent) { pd.setProgress(percent); }
@Override
public void onComplete(File file) { pd.dismiss(); }
@Override
public void onError(String message) {
pd.dismiss();
Toast.makeText(activity, "下载失败", Toast.LENGTH_SHORT).show();
}
},
new InstallCallback() {
@Override
public void onSuccess() { }
@Override
public void onFailure(String message) {
Toast.makeText(activity, "安装失败", Toast.LENGTH_SHORT).show();
}
@Override
public void onNeedPermission(Intent intent) {
activity.startActivityForResult(intent, 1001);
}
}
);
}
}
```
**5.2 在 Application 中启动自动更新**
```java
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
// 初始化配置(同上)
UpdateConfig config = ...;
AppUpdate.init(config);
// 延迟5秒后自动检查(可调整)
new Handler().postDelayed(() -> {
// 注意:此时可能没有前台 Activity,因此传入 null
UpdateManager.getInstance(this).checkAutoUpdate(null);
}, 5000);
}
}
```
如果需要在前台时进行自动更新(例如用户回到应用时),可以在 BaseActivity 中注册监听,然后调用 UpdateManager 的检查方法并传入当前 Activity。
## 6. 关于版本号比较
上述示例使用简单的字符串 equals 比较,但在实际业务中版本号可能为 1.2.3 格式,建议使用 versionCode(整数)进行比较。库中 AppUpdate.getInstance().getCurrentVersion() 返回的是 versionName,你也可以自行通过 PackageManager 获取 versionCode:
```java
int currentCode = context.getPackageManager()
.getPackageInfo(context.getPackageName(), 0).versionCode;
```
如果服务器返回的 JSON 中包含 versionCode,则可以在 UpdateInfo 中提供 getNewVersionCode() 方法,然后在自定义解析器中实现。
## 7. 自定义 UpdateInfo 解析(可选)
如果服务器返回的 JSON 格式与默认不同,可以实现 UpdateParser 接口:
```java
public class CustomUpdateParser implements UpdateParser {
@Override
public UpdateInfo parse(String response) throws Exception {
JSONObject root = new JSONObject(response);
JSONObject data = root.getJSONObject("data");
return new UpdateInfo() {
@Override
public String getNewVersion() {
return data.optString("ver");
}
@Override
public String getDownloadUrl() {
return data.optString("full_url");
}
@Override
public String getPatchDownloadUrl() {
return data.optString("patch_url");
}
@Override
public String getOldVersion() {
return data.optString("old_ver");
}
@Override
public String getDescription() {
return data.optString("desc");
}
@Override
public boolean isForceUpdate() {
return data.optBoolean("force", false);
}
};
}
}
// 在初始化后设置
AppUpdate.getInstance().setUpdateParser(new CustomUpdateParser());
```
## 8. 常见问题
- FileProvider 冲突:如果应用中已使用其他 FileProvider,需合并 file_paths.xml,并确保 authorities 唯一。
- 安装时解析错误:可能是 APK 下载不完整或差分合成失败,请检查日志。
- 权限问题:Android 8.0+ 需要动态申请安装未知来源权限,已在回调中处理。
- Native 库找不到:检查 APK 是否包含 lib/armeabi-v7a/libmyPatch.so 等文件,可解压 APK 确认。
## 9. 完整项目示例结构
建议将更新相关代码统一放在一个包内,例如:
```text
com.example.app
├── update
│ ├── UpdateManager.java # 更新管理器
│ ├── CustomUpdateParser.java # 自定义解析器(可选)
│ └── activity
│ └── UpdateActivity.java # 处理通知点击后的下载界面
```
## 10. 期望后端接口返回的 JSON 数据格式要求
模块默认的更新信息解析器 DefaultUpdateParser 期望后端接口返回的 JSON 数据格式如下:
#### 基本响应结构
```json
{
"data": {
"version": "2.0.0", // 新版本号(字符串,必需)
"downurl": "https://.../app.apk", // 全量包下载地址(字符串,必需)
"patchPath": "https://.../patch.patch", // 差分包下载地址(字符串,可选)
"oldVersion": "1.0.0", // 差分所需的基础版本号(字符串,可选)
"description": "更新说明内容", // 更新说明(字符串,可选,可用"|"分隔换行)
"force": false // 是否强制更新(布尔值,可选,默认false)
}
}
```
#### 字段说明
|字段 |类型 |必需 |描述|
|------|------|------|------|
|version| String| 是| 新版本号,用于版本比较。|
|downurl| String| 是| 全量 APK 的下载地址。|
|patchPath| String| 否| 差分补丁文件的下载地址。若提供此字段,且设备当前版本与 oldVersion 匹配,则会使用差分更新以节省流量。|
|oldVersion| String| 否| 差分补丁对应的旧版本号。如果提供了 patchPath,建议也提供此字段,以便模块判断是否适用差分更新。|
|description| String| 否| 更新说明。如果包含 | 字符,会自动替换为换行符,方便在对话框中显示。|
|force| Boolean| 否| 是否为强制更新。若为 true,建议调用方显示不可取消的对话框。|
#### 示例
```json
{
"code": 200,
"message": "success",
"data": {
"version": "3.1.0",
"downurl": "https://cdn.example.com/app-v3.1.0.apk",
"patchPath": "https://cdn.example.com/patch-v3.0.0-to-v3.1.0.patch",
"oldVersion": "3.0.0",
"description": "新增暗黑模式|修复登录闪退|优化启动速度",
"force": false
}
}
```
#### 注意事项
- 差分更新条件:只有当 patchPath 不为空且当前应用的版本号与 oldVersion 完全一致时,模块才会自动下载差分包并合成新 APK。否则将回退到全量下载。
- 版本号比较:模块内部仅通过字符串 equals 比较版本号,若您的版本号格式复杂(如 1.2.3),建议在调用方自行实现版本比较逻辑,或通过自定义解析器返回更合适的数据(如增加 versionCode 字段)。
- 自定义解析:如果后端接口格式与上述不同,您可以通过实现 UpdateParser 接口自定义解析逻辑,然后在初始化后调用 AppUpdate.getInstance().setUpdateParser(parser) 替换默认解析器。
#### 自定义解析器示例
假设您的后端返回如下格式:
```json
{
"version": "2.0.0",
"apk_url": "https://.../app.apk",
"patch_url": "https://.../patch.patch",
"base_version": "1.0.0",
"changelog": "更新内容",
"force_update": true
}
```
您可以编写自定义解析器:
```java
public class MyUpdateParser implements UpdateParser {
@Override
public UpdateInfo parse(String response) throws Exception {
JSONObject root = new JSONObject(response);
return new UpdateInfo() {
@Override
public String getNewVersion() {
return root.optString("version");
}
@Override
public String getDownloadUrl() {
return root.optString("apk_url");
}
@Override
public String getPatchDownloadUrl() {
return root.optString("patch_url", null);
}
@Override
public String getOldVersion() {
return root.optString("base_version", null);
}
@Override
public String getDescription() {
return root.optString("changelog", "");
}
@Override
public boolean isForceUpdate() {
return root.optBoolean("force_update", false);
}
};
}
}
```
然后在初始化后设置:
```java
AppUpdate.getInstance().setUpdateParser(new MyUpdateParser());
```
## API
### 1. 入口类:AppUpdate
**静态方法**
|方法| 描述|
|----|----|
|void| init(UpdateConfig config)| 必须在使用前调用一次,初始化更新模块。传入配置对象。|
|AppUpdate| getInstance()| 获取单例实例。需先调用 init()。|
**实例方法**
|方法| 描述|
|----|----|
|void setUpdateParser(UpdateParser parser)| (可选)设置自定义的更新信息解析器,用于解析服务器响应。|
|String getCurrentVersion() |获取当前应用的版本名(versionName)。|
|void checkUpdate(UpdateCheckCallback callback)| 检查更新。结果通过回调返回。|
|void downloadAndInstall(UpdateInfo info, DownloadCallback downloadCallback, InstallCallback installCallback)| 下载并安装更新。自动判断使用全量包还是差分包。|
|void cancelDownload()| 取消当前正在进行的下载任务。|
### 2. 配置类:UpdateConfig
通过 UpdateConfig.Builder 构建,必须设置 checkUrl。
**Builder 方法**
|方法| 描述|
|----|----|
|Builder(Context context)| 构造函数,传入 Context(建议使用 ApplicationContext)。|
|Builder setPackageName(String packageName)| 设置目标应用包名,默认使用当前应用的包名。|
|Builder setApkStorePath(String path)| 设置 APK 和补丁文件的存储目录,默认使用 context.getExternalFilesDir("apk")。|
|Builder setCheckUrl(String url)| 必须设置检查更新的接口 URL。|
|Builder addRequestHeader(String key, String value)| 添加 HTTP 请求头,可多次调用。|
|Builder setExecutor(ExecutorService executor)| 自定义线程池,默认使用 Executors.newCachedThreadPool()。|
|Builder enableLog(boolean enable)| 是否打印内部日志,默认 false。|
|UpdateConfig build()| 构建配置对象。|
**配置对象方法(通常由模块内部使用,但也可公开)**
|方法| 描述
|----|----|
|Context getContext()| 获取 ApplicationContext。
|String getPackageName()| 获取目标包名。
|String getApkStorePath() |获取存储路径。
|String getCheckUrl()| 获取检查 URL。
|Map getRequestHeaders()| 获取请求头。
|ExecutorService getExecutor()| 获取线程池。
|boolean isLogEnabled() |是否启用日志。
### 3. 更新信息抽象类:UpdateInfo
需要调用方在解析服务器响应后,提供一个实现了该抽象类的对象。模块内部通过它获取更新所需数据。
**必须实现的抽象方法**
|方法| 描述
|----|----|
|String getNewVersion() |新版本号。
|String getDownloadUrl() |全量包下载地址。
|String getPatchDownloadUrl() |差分包下载地址(若无差分可返回 null)。
|String getOldVersion() |差分所需的基础版本号(当有差分包时有效)。
|String getDescription() |更新说明。
|boolean isForceUpdate() |是否强制更新。
### 4. 回调接口
**UpdateCheckCallback(检查更新回调)**
|方法| 描述
|----|----|
|void onSuccess(UpdateInfo info)| 检查成功,返回更新信息。
|void onFailure(int code, String message) |检查失败,code 为 HTTP 状态码或 -1,message 为错误描述。
**DownloadCallback(下载回调)**
|方法| 描述|
|----|----|
|void onStart() |下载开始。
|void onProgress(int percent) |下载进度(0-100)。
|void onComplete(File file) |下载完成,返回下载的文件。
|void onError(String message) |下载失败,返回错误信息。
**InstallCallback(安装回调)**
|方法| 描述|
|----|----|
|void onSuccess()| 安装启动成功(注意:安装过程由系统完成,此回调仅表示安装 Intent 已发出)。
|void onFailure(String message)| 安装失败,返回错误信息。
|void onNeedPermission(Intent intent)| Android 8.0+ 需要请求安装未知来源权限时调用,调用方应使用 startActivityForResult(intent, requestCode) 启动权限设置页面。
### 5. 解析器接口:UpdateParser
用于自定义服务器响应解析,实现该接口后通过 AppUpdate.getInstance().setUpdateParser(parser) 设置。
|方法| 描述|
|----|----|
|UpdateInfo parse(String response) throws Exception |解析服务器返回的 JSON 字符串,返回 UpdateInfo 对象。
**默认实现:DefaultUpdateParser**
默认解析器期望服务器返回如下 JSON 格式:
```json
{
"data": {
"version": "x.x.x",
"downurl": "https://...",
"patchPath": "https://...",
"oldVersion": "x.x.x",
"description": "...",
"force": false
}
}
```
通过以上步骤,你可以轻松地在项目中集成手动和自动更新功能。如果还有疑问,欢迎查阅库的源码或提交 issue。