现在国内主流的应用市场也都支持应用的增量更新了。
增量更新的原理,就是将手机上已安装apk与服务器端最新apk进行二进制对比,得到差分包,用户更新程序时,只需要下载差分包,并在本地使用差分包与已安装apk,合成新版apk。
apk文件的差分、合成,可以通过 开源的二进制比较工具bsdiff
来实现,bsdiff依赖bzip2,所以我们还需要用到 bzip2
bsdiff中,bsdiff.c
用于生成差分包,bspatch.c
用于合成文件。
一、编写java层代码,生成头文件
DiffUtils
public class DiffUtils {
/**
* native方法 使用路径为oldApkPath的apk与路径为patchPath的补丁包,合成新的apk,并存储于newApkPath
* 返回:0,说明操作成功
*
* @param oldApkPath 示例:/sdcard/old.apk
* @param newApkPath 示例:/sdcard/new.apk
* @param patchPath 示例:/sdcard/xx.patch
* @return
*/
public static native int patch(String oldApkPath, String newApkPath,
String patchPath);
}
PatchUtils
public class PatchUtils {
/**
* native方法 使用路径为oldApkPath的apk与路径为patchPath的补丁包,合成新的apk,并存储于newApkPath
* 返回:0,说明操作成功
*
* @param oldApkPath 示例:/sdcard/old.apk
* @param newApkPath 示例:/sdcard/new.apk
* @param patchPath 示例:/sdcard/xx.patch
* @return
*/
public static native int patch(String oldApkPath, String newApkPath,
String patchPath);
}
二、引入bsdiff和bzip2源码
- bzip2目录中的文件,全部来自bzip2项目。
- bsdiff.c、bspatch.c为bsdiff项目中的代码,需要修改一下两个文件中的main函数,修改成其他函数名,bsdiff.c 用于生成差分包,bspatch.c 用于合成文件
- com_binzi_jni_DiffUtils.cpp
com_binzi_jni_DiffUtils.h
com_binzi_jni_PatchUtils.cpp
com_binzi_jni_PatchUtils.h
java调用的jni接口和实现
三、实现JNI接口函数
com_binzi_jni_PatchUtils.c
JNIEXPORT jint JNICALL Java_com_binzi_jni_PatchUtils_patch
(JNIEnv *env, jclass jcls, jstring oldApkPath, jstring newApkPath, jstring patchPath) {
int argc = 4;
char *argv[argc];
//第一个参数没有用到
argv[0] = "bspatch";
argv[1] = (char *) env->GetStringUTFChars(oldApkPath, NULL);
argv[2] = (char *) env->GetStringUTFChars(newApkPath, NULL);
argv[3] = (char *) env->GetStringUTFChars( patchPath, NULL);
int ret = bspatch_main(argc, argv);
env->ReleaseStringUTFChars(oldApkPath, argv[1]);
env->ReleaseStringUTFChars(newApkPath, argv[2]);
env->ReleaseStringUTFChars(patchPath, argv[3]);
return ret;
}
com_binzi_jni_DiffUtils.cpp
JNIEXPORT jint JNICALL Java_com_binzi_jni_DiffUtils_patch
(JNIEnv *env, jclass jcls, jstring oldApkPath, jstring newApkPath, jstring patchPath) {
int argc = 4;
char *argv[argc];
argv[0] = "bsdiff";
argv[1] = (char *) env->GetStringUTFChars(oldApkPath, 0));
argv[2] = (char *) env->GetStringUTFChars(newApkPath, 0));
argv[3] = (char *) env->GetStringUTFChars(patchPath, 0));
int ret = bsdiff_main(argc, argv);
env->ReleaseStringUTFChars(oldApkPath, argv[1]);
env->ReleaseStringUTFChars(newApkPath, argv[2]);
env->ReleaseStringUTFChars(patchPath, argv[3]);
return ret;
}
四、Java部分核心代码
- 获取已安装Apk文件的源Apk文件
public static String getSourceApkPath(Context context, String packageName) {
if (TextUtils.isEmpty(packageName))
return null;
try {
ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(packageName, 0);
return appInfo.sourceDir;
} catch (NameNotFoundException e) {
e.printStackTrace();
}
return null;
}
- 安装Apk
public static void installApk(Context context, String apkPath) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse("file://" + apkPath),
"application/vnd.android.package-archive");
context.startActivity(intent);
}
- apk 签名信息获取
public class SignUtils {
private static final boolean DEBUG = Constants.DEBUG;
private static final String TAG = DEBUG ? "SignUtils" : SignUtils.class.getSimpleName();
private static String bytes2Hex(byte[] src) {
char[] res = new char[src.length * 2];
final char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
for (int i = 0, j = 0; i < src.length; i++) {
res[j++] = hexDigits[src[i] >>> 4 & 0x0f];
res[j++] = hexDigits[src[i] & 0x0f];
}
return new String(res);
}
private static String getMd5ByFile(File file) {
String value = null;
FileInputStream in = null;
try {
in = new FileInputStream(file);
MessageDigest digester = MessageDigest.getInstance("MD5");
byte[] bytes = new byte[8192];
int byteCount;
while ((byteCount = in.read(bytes)) > 0) {
digester.update(bytes, 0, byteCount);
}
value = bytes2Hex(digester.digest());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != in) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return value;
}
/**
* 判断文件的MD5是否为指定值
*
* @param file
* @param md5
* @return
*/
public static boolean checkMd5(File file, String md5) {
if (TextUtils.isEmpty(md5)) {
throw new RuntimeException("md5 cannot be empty");
}
String fileMd5 = getMd5ByFile(file);
if (DEBUG) {
Log.d(TAG, String.format("file's md5=%s, real md5=%s", fileMd5, md5));
}
if (md5.equals(fileMd5)) {
return true;
} else {
return false;
}
}
/**
* 判断文件的MD5是否为指定值
*
* @param filePath
* @param md5
* @return
*/
public static boolean checkMd5(String filePath, String md5) {
return checkMd5(new File(filePath), md5);
}
}
- 增量更新
String oldApkSource = ApkUtils.getSourceApkPath(mContext, Constants.TEST_PACKAGENAME);
if (!TextUtils.isEmpty(oldApkSource)) {
// 校验一下本地安装APK的MD5是不是和真实的MD5一致
if (SignUtils.checkMd5(oldApkSource, mCurentRealMD5)) {
int patchResult = PatchUtils.patch(oldApkSource, Constants.NEW_APK_PATH, Constants.PATCH_PATH);
if (patchResult == 0) {
if (SignUtils.checkMd5(Constants.NEW_APK_PATH, mNewRealMD5)) {
ApkUtils.installApk(context, Constants.NEW_APK_PATH);
}
}
}