核心部分 bsdiff 和zip2
bsdiff 相关下载地址 :
官网http://www.daemonology.net/bsdiff/
镜像地址 https://packages.debian.org/zh-cn/sid/ppc64el/bsdiff/download
对应的版本链接http://ftp.de.debian.org/debian/pool/main/b/bsdiff/
zip2 相关下载地址 :
官方网站https://sourceforge.net/projects/bzip2/
对应的版本链接https://sourceware.org/pub/bzip2/
Android引入 新建module lib模块 如图所示引入相关bsdiff源码和zip源码相关文件
CMakeLists.txt 文件配置
相关代码
cmake_minimum_required(VERSION 3.4.1)
aux_source_directory(bzip2 BZIP_SOURCES)#bspatch.c合成文件所需依赖库
add_library(
bspatch_lib #库名
SHARED#动态库
native-lib.cpp
bspatch.c
${BZIP_SOURCES}#源文件
)
include_directories(bzip2)
target_link_libraries(
bspatch_lib
log)
native-lib.cpp相关配置,这里是jni交互部分 java 调用c c++相关方法,如图所示
相关代码
#include <jni.h>
#include <android/log.h>
extern "C" {
extern int executePatch(int argc, char *argv[]);
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_patchlib_PatchUtils_patch(JNIEnv *env, jobject clazz, jstring old_apk,
jstring new_apk, jstring patch_file) {
int args = 4;
char *argv[args];
argv[0] = "bspatch";
argv[1] = (char *) (env->GetStringUTFChars(old_apk, 0));
argv[2] = (char *) (env->GetStringUTFChars(new_apk, 0));
argv[3] = (char *) (env->GetStringUTFChars(patch_file, 0));
//此处executePathch()就是上面我们修改出的
int result = executePatch(args, argv);
env->ReleaseStringUTFChars(old_apk, argv[1]);
env->ReleaseStringUTFChars(new_apk, argv[2]);
env->ReleaseStringUTFChars(patch_file, argv[3]);
__android_log_print(ANDROID_LOG_ERROR,"diff","==%s==%s==%s==%d",argv[1] ,argv[2] ,argv[3],result );
return result;
}
executePatch 定位为patch.c 这个文件中定义的合成方法,下载的bsdiff的文件源码中原来的是mian函数方法,避免与其他函数冲突,可以另起方法名,如图所示部分
Java_com_example_patchlib_PatchUtils_patch 这里com_example_patchlib_PatchUtils为java部分定义的包名和方法名,如图所示
相关代码
package com.example.patchlib
import android.util.Log
object PatchUtils {
init {
try {
System.loadLibrary("bspatch_lib")
Log.e("patch", "增量合成cpp库加载成功")
} catch (e: java.lang.Exception) {
Log.e("patch", "Exception: ${e.message}")
}
}
external fun patch(oldApk: String, newApk: String, patchFile: String): Int
}
System.loadLibrary(“bspatch_lib”) 这里为加载的patch合成库即上面引入的bsdiff和zip2 部分,bspatch_lib为cmakeList.txt 定义的库名
gradle 部分配置,如图所示
相关代码
externalNativeBuild {
cmake {
abiFilters += listOf("armeabi-v7a", "arm64-v8a")
}
}
这里主要是编译 cpp目录下的 bsdiff的patch.c 和zip2 目录下的相关c文件
到这里基本合成的patch库添加完毕 ,下面是使用
首先是 打包一个基础包 old.apk 在打包一个修改包 new.apk
通过patch工具 生成差量的patch包
工具下载地址:https://obs-mips3-test.obs.cn-north-1.myhuaweicloud.com/bk_run_log/20200306/xa/bsdiff.zip
如图所示:
用bsdiff.exe 来打差分包 ,如:bsdiff 1.apk 2.apk patch
相关命令:
bsdiff与bspatch重新编译了一份,现在可用了,使用方式:
生成差分包: bsdiff oldfile newfile patchfile
合成:bspatch oldfile 合成后的输出文件 patchfile
如:bsdiff 1.apk 2.apk patch
对比版本1与版本2的apk,生成差分包patch
bspatch 1.apk 2.apk patch
使用差分包patch与1.apk合并,生成2.apk
合成的差分包 后缀为.patch 或者无后缀都可以,测试差分包是否可用 可以用 bspatch.exe 来进行合包
差分包打包完毕后,代码先进行下载patch文件 ,再通过BsPatchUtils 调用patch方法合成apk并打开安装
``
val newApkFile = File(savePath, "new2.apk")//新包本保存路径
val result = BsPatchUtils.patch(
applicationInfo.sourceDir,//app 原包old.apk获取方式,这里需要兼容13,获取原包方法不一样
newApkFile.absolutePath,//这里为合成新包的输出路径
"$saveFileName"//这里为 差分包 patch.patch 下载后保存的地址
)
整理下载合成代码如下:
package com.example.hotfix
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.text.Html
import android.text.Html.ImageGetter
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import com.bumptech.glide.Glide
import com.example.patchlib.BsPatchUtils
import dalvik.system.DexClassLoader
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
class MainActivity : AppCompatActivity() {
// 下载线程
private var downLoadThread: Thread? = null
private var progress = 0 // 当前进度
// 保存路径
private var savePath: String? = null
// 保存的文件名称
private var saveFileName: String? = null
// 取消下载的标志位
private var intercept = false
// 是否下载 默认 false 没有下载
private var isDownLoad = false
private var apkUrl = ""
companion object {
private const val DOWN_UPDATE = 1
private const val DOWN_OVER = 2
}
@SuppressLint("MissingInflatedId")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
savePath = "${getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString()}/"
Log.e("tag", "$savePath")
findViewById<Button>(R.id.btnClick).setOnClickListener {
apkUrl = findViewById<EditText>(R.id.et_input).text.toString()
downLoadThread = Thread(mdownApkRunnable)
downLoadThread?.start()
}
}
//下载安装包的进程,同步更新安装包的下载进度,以及下载完成后的通知安装
private val mdownApkRunnable = Runnable {
val url: URL
try {
//对sd卡进行状态的判断,如果相等的话表示当前的sdcard挂载在手机上并且是可用的
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
isDownLoad = true
url = URL(apkUrl)
val conn = url
.openConnection() as HttpURLConnection
conn.connect()
val length = conn.contentLength
val ins = conn.inputStream
//创建安装包所在的前置文件夹
val file = File(savePath)
if (!file.exists()) {
file.mkdir()
}
//创建安装包的文件,安装包将下载入这个文件夹中
val time = System.currentTimeMillis() //当前时间的毫秒数
saveFileName = "$savePath${time}_patch.patch"
val apkFile = File(saveFileName)
//创建该文件夹的输入流
val fos = FileOutputStream(apkFile)
var count = 0
val buf = ByteArray(1024)
while (!intercept) { //对取消下载的标志位进行判断,如果一直没有被打断即没有点击取消下载按钮则继续下载
val numread = ins.read(buf) //返回读入的字节个数并将读到的字节内容放入buf中
count += numread
progress = (count.toFloat() / length * 100).toInt() //当前进度,用来更新progressBar的进度
// 通知主线程更新下载进度
mHandler.sendEmptyMessage(DOWN_UPDATE)
if (numread <= 0) {
// 下载完成通知安装
mHandler.sendEmptyMessage(DOWN_OVER)
break
}
//已经全部读入,不需要再读入字节为-1的内容
fos.write(buf, 0, numread) //从fos中写入读出的字节个数到buf中
}
fos.close()
ins.close()
} else return@Runnable
} catch (e: Exception) {
Log.e("tag", "error====${e.message}")
isDownLoad = false
e.printStackTrace()
}
}
//消息通知部分
private val mHandler: Handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
when (msg.what) {
DOWN_UPDATE -> {
findViewById<TextView>(R.id.btnClick).text = "下载进度$progress%"
}
DOWN_OVER -> {
isDownLoad = false
//下载完成合成新包
patch()
}
else -> {}
}
}
}
//patch.patch 差量包下载完成 合成新包并安装
fun patch() {
val newApkFile = File(savePath, "new2.apk")//新版本保存路径
val result = BsPatchUtils.patch(
applicationInfo.sourceDir,
newApkFile.absolutePath,
"$saveFileName"
)
Log.e("tag", "组装result: $result")
Log.e("tag", "组装result: $newApkFile")
installAPK(newApkFile)
}
//安装apk
private fun installAPK(apkFile: File) {
val intent = Intent(Intent.ACTION_VIEW)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) //设置启动模式,四种之一
intent.addCategory(Intent.CATEGORY_DEFAULT)
val uri: Uri
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // 前面代表的是当前操作系统的版本号,后面是给定的版本号,Ctrl鼠标放置显示版本号
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
//记得修改com.xxx.fileprovider与androidmanifest相同
// 获取的是应用唯一区分的id即applicationId
uri = FileProvider.getUriForFile(
this,
this.packageName + ".fileProvider",
apkFile
)
intent.setDataAndType(uri, "application/vnd.android.package-archive") // 打开apk文件
} else {
uri = Uri.parse("file://$apkFile")
}
intent.setDataAndType(uri, "application/vnd.android.package-archive")
this.startActivity(intent)
}
}