Android apk增量升级
前言
别看本文看上去很简单,实际在实验过程中遇到了很多问题,比如andorid studio下ndk编译报错,而本文呈现给大家的都是最终可行的方法.
所需资源
- bzip2
- bsdiff
- ndk
- 两个不同版本的测试apk
原理
- 在服务器端,生成最新版与之前所有版本的差分包,为什么是所有版本,因为我们无法知道用户当前版本是什么版本
- 在手机客户端更新程序时,下载差分包,使用已安装的旧版apk与这个差分包,合成为一个新版apk
- 校验新合成的apk文件是否完整,检验合成版本的签名是否和已安装客户端一致,如一致则提示用户安装
生成差分包
下载生成差分包所需依赖文件
新建项目并调用native代码
新建java层类
package cn.edu.zafu.util;
public class DiffAndPatch {
static {
System.loadLibrary("DiffAndPatch");
}
public static native int generateDiff(String oldApkPath, String newApkPath,
String patchPath);
public static native int applyPatch(String oldApkPath, String newApkPath,
String patchPath);
}
进入项目bin目录,使用javah命令创建头文件
javah cn.edu.zafu.util.DiffAndPatch
将头文件复制到jni文件夹下(自己创建),复制一份修改后缀名为.c
将里面的内容清除,复制下面的代码进去
#include "cn_edu_zafu_util_DiffAndPatch.h"
#include <time.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: cn_edu_zafu_util_DiffAndPatch
* Method: generateDiff
* Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I
*/
JNIEXPORT jint JNICALL Java_cn_edu_zafu_util_DiffAndPatch_generateDiff
(JNIEnv *env, jclass arg, jstring old, jstring new, jstring patch){
printf("-----generateDiff start-----\n");
clock_t start, finish;
double duration;
int argc = 4;
char * argv[argc];
argv[0] = "bsdiff";
argv[1] = (char*) ((*env)->GetStringUTFChars(env, old, 0));
argv[2] = (char*) ((*env)->GetStringUTFChars(env, new, 0));
argv[3] = (char*) ((*env)->GetStringUTFChars(env, patch, 0));
printf("old apk = %s \n", argv[1]);
printf("new apk = %s \n", argv[2]);
printf("patch = %s \n", argv[3]);
printf("start to generate patch file\n");
start = clock();
int ret = generatePatch(argc, argv);
finish = clock();
printf("finish to generate patch file\n");
duration = (double)(finish - start) / CLOCKS_PER_SEC;
printf("generateDiff result = %d \n", ret);
printf("time spended %f s \n",duration);
(*env)->ReleaseStringUTFChars(env, old, argv[1]);
(*env)->ReleaseStringUTFChars(env, new, argv[2]);
(*env)->ReleaseStringUTFChars(env, patch, argv[3]);
printf("-----generateDiff end-----\n");
return ret;
}
/*
* Class: cn_edu_zafu_util_PatchUtil
* Method: applyPatch
* Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I
*/
JNIEXPORT jint JNICALL Java_cn_edu_zafu_util_DiffAndPatch_applyPatch
(JNIEnv *env, jclass arg, jstring old, jstring new, jstring patch){
printf("-----applyPatch start-----\n");
clock_t start, finish;
double duration;
int argc = 4;
char * argv[argc];
argv[0] = "bspatch";
argv[1] = (char*) ((*env)->GetStringUTFChars(env, old, 0));
argv[2] = (char*) ((*env)->GetStringUTFChars(env, new, 0));
argv[3] = (char*) ((*env)->GetStringUTFChars(env, patch, 0));
printf("old apk = %s \n", argv[1]);
printf("patch = %s \n", argv[3]);
printf("new apk = %s \n", argv[2]);
printf("start to apply patch file\n");
start = clock();
int ret = applyPatch(argc, argv);
finish = clock();
printf("finish to apply patch file\n");
duration = (double)(finish - start) / CLOCKS_PER_SEC;
printf("applyPatch result = %d \n", ret);
printf("time spended %f s \n",duration);
(*env)->ReleaseStringUTFChars(env, old, argv[1]);
(*env)->ReleaseStringUTFChars(env, new, argv[2]);
(*env)->ReleaseStringUTFChars(env, patch, argv[3]);
printf("-----applyPatch end-----\n");
return ret;
}
#ifdef __cplusplus
}
#endif
将相关依赖文件复制到jni目录下,对应的文件从下载下来的bsdiff和bzip2中寻找,文件清单如图
并在jni文件下新建Makefile文件,内容如下,对应的jdk目录修改为自己电脑的目录.
CC := gcc
CFLAGS := -I /opt/java/jdk1.8.0_25/include -I /opt/java/jdk1.8.0_25/include/linux -I /usr/include
LDFLAGS := -fPIC -shared
SOURCE := $(wildcard *.c *.h)
libDiffAndPatch.so: $(SOURCE)
$(CC) $(LDFLAGS) $(CFLAGS) $^ -o libDiffAndPatch.so
clean:
rm libDiffAndPatch.so
增加native library location.项目文件右键Propreties->Java Build Path->Libraryies->JRE System Library->Native Library Location双击后指向jni目录
命令行进入jni目录进行编译
编写测试代码,将apk文件置于对应文件夹下,这里是/home/lizhangqu/apk/这个目录下
package cn.edu.zafu;
import cn.edu.zafu.util.DiffAndPatch;
public class Test {
// 旧版本
public static final String OLD_APK = "/home/lizhangqu/apk/ApkPatchTestVersion_1.apk";
// 新版本
public static final String NEW_APK = "/home/lizhangqu/apk/ApkPatchTestVersion_2.apk";
// patch存储路径
public static final String PATCH_FILE = "/home/lizhangqu/apk/version_1_to_version_2_.patch";
// 使用旧版本+patch包,合成的新版本
public static final String OLD_TO_NEW_APK = "/home/lizhangqu/apk/generated.apk";
public static void main(String[] args) {
System.out.println("正在生成文件,可能需要一段时间,请稍后...");
DiffAndPatch.generateDiff(OLD_APK, NEW_APK, PATCH_FILE);
}
}
最终会输出以下信息
正在生成文件,可能需要一段时间,请稍后...
-----generateDiff start-----
old apk = /home/lizhangqu/apk/ApkPatchTestVersion_1.apk
new apk = /home/lizhangqu/apk/ApkPatchTestVersion_2.apk
patch = /home/lizhangqu/apk/version_1_to_version_2_.patch
start to generate patch file
finish to generate patch file
generateDiff result = 0
time spended 2.595576 s
-----generateDiff end-----
将旧版本安装在手机上,将生成的patch拷到手机上.这里拷到手机内存卡根路径下apk文件夹里
手机端合成apk并检验
使用android studio新建项目,创建java层调用native层代码
package cn.edu.zafu.util;
/**
* 类说明: apk 合成工具类
*
* @author lizhangqu
* @date 2015-5-29
* @version 1.0
*/
public class PatchUtil {
static {
System.loadLibrary("Patch");
}
/**
* @param oldApkPath 旧包路径
* @param newApkPath 生成的包路径
* @param patchPath patch路径
* @return
*/
public static native int applyPatch(String oldApkPath, String newApkPath,
String patchPath);
}
build后打开android studio里的终端使用javah命令生成头文件
cd app/src/main
javah -d jni -classpath "/media/lizhangqu/windows/sdk/platforms/android-22/android.jar:../../build/intermediates/classes/debug/" cn.edu.zafu.util.PatchUtil
之后再jni目录里会有头文件生成,复制一份改后缀为.c,里面实现的内容如下
#include <time.h>
#include <stdio.h>
JNIEXPORT jint JNICALL Java_cn_edu_zafu_util_PatchUtil_applyPatch
(JNIEnv *env, jclass arg, jstring oldApk, jstring newApk, jstring patch){
printf("-----applyPatch start-----\n");
clock_t start, finish;
double duration;
int argc = 4;
char * argv[argc];
argv[0] = "bspatch";
argv[1] = (char*) ((*env)->GetStringUTFChars(env, oldApk, 0));
argv[2] = (char*) ((*env)->GetStringUTFChars(env, newApk, 0));
argv[3] = (char*) ((*env)->GetStringUTFChars(env, patch, 0));
printf("old apk = %s \n", argv[1]);
printf("patch = %s \n", argv[3]);
printf("new apk = %s \n", argv[2]);
printf("start to apply patch file\n");
start = clock();
int ret = applyPatch(argc, argv);
finish = clock();
printf("finish to apply patch file\n");
duration = (double)(finish - start) / CLOCKS_PER_SEC;
printf("applyPatch result = %d \n", ret);
printf("time spended %f s \n",duration);
(*env)->ReleaseStringUTFChars(env, oldApk, argv[1]);
(*env)->ReleaseStringUTFChars(env, newApk, argv[2]);
(*env)->ReleaseStringUTFChars(env, patch, argv[3]);
printf("-----applyPatch end-----\n");
return ret;
}
将之前的生成差分包的项目jni目录下除了Makefile文件和自己生成的两个文件外,其余文件都拷到android studio 的jni目录下.
在jni文件下创建Android.mk,内容如下
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := Patch
LOCAL_SRC_FILES := blocksort.c bspatch.c bzip2.c bzlib.c bzlib.h bzlib_private.h cn_edu_zafu_util_PatchUtil.c cn_edu_zafu_util_PatchUtil.h compress.c crctable.c decompress.c huffman.c randtable.c
include $(BUILD_SHARED_LIBRARY)
在jni目录下创建Application.mk,内容如下
APP_ABI := armeabi
APP_PLATFORM :=android-15
APP_PLATFORM一定要,否则编译的时候会报错
禁用android studio自带的ndk build并使用自己的Android.mk和Application.mk文件
在local.properties里加入ndk目录
ndk.dir=/media/lizhangqu/windows/sdk/android-ndk-r10e
修改app目录下的build.gradle文件,最终内容如下
import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: ‘com.android.application‘
android {
compileSdkVersion 22
buildToolsVersion "22.0.1"
defaultConfig {
applicationId "cn.edu.zafu.patch"
minSdkVersion 15
targetSdkVersion 22
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile(‘proguard-android.txt‘), ‘proguard-rules.pro‘
}
}
sourceSets.main.jni.srcDirs = [] // disable automatic ndk-build call, which ignore our Android.mk
sourceSets.main.jniLibs.srcDir ‘src/main/libs‘
// call regular ndk-build(.cmd) script from app directory
task ndkBuild(type: Exec) {
workingDir file(‘src/main‘)
commandLine getNdkBuildCmd()
}
tasks.withType(JavaCompile) {
compileTask -> compileTask.dependsOn ndkBuild
}
task cleanNative(type: Exec) {
workingDir file(‘src/main‘)
commandLine getNdkBuildCmd(), ‘clean‘
}
clean.dependsOn cleanNative
}
dependencies {
compile fileTree(dir: ‘libs‘, include: [‘*.jar‘])
compile ‘com.android.support:appcompat-v7:22.1.1‘
}
def getNdkDir() {
if (System.env.ANDROID_NDK_ROOT != null)
return System.env.ANDROID_NDK_ROOT
Properties properties = new Properties()
properties.load(project.rootProject.file(‘local.properties‘).newDataInputStream())
def ndkdir = properties.getProperty(‘ndk.dir‘, null)
if (ndkdir == null)
throw new GradleException("NDK location not found. Define location with ndk.dir in the local.properties file or with an ANDROID_NDK_ROOT environment variable.")
return ndkdir
}
def getNdkBuildCmd() {
def ndkbuild = getNdkDir() + "/ndk-build"
if (Os.isFamily(Os.FAMILY_WINDOWS))
ndkbuild += ".cmd"
return ndkbuild
}
编写更新代码,假设已经将patch下载到本地
package cn.edu.zafu;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import cn.edu.zafu.util.ApkUtil;
import cn.edu.zafu.util.PatchUtil;
import cn.edu.zafu.util.SignUtil;
public class MainActivity extends AppCompatActivity {
private static final String ROOT_PATH= Environment.getExternalStorageDirectory().getAbsolutePath();
private static final String PACKAGE_NAME="com.sina.weibo";
// patch存储路径
public static final String PATCH_FILE = "/apk/weibo.patch";
// 使用旧版本+patch包,合成的新版本
public static final String OLD_TO_NEW_APK = "/apk/generated.apk";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
boolean isInstalled=ApkUtil.isInstalled(getApplicationContext(), PACKAGE_NAME);
if(isInstalled){
String source=ApkUtil.getSourceApkPath(getApplicationContext(),PACKAGE_NAME);
Log.d("TAG",source);
Log.d("TAG",ROOT_PATH+OLD_TO_NEW_APK);
Log.d("TAG",ROOT_PATH+PATCH_FILE);
applyPatch(source,ROOT_PATH+OLD_TO_NEW_APK,ROOT_PATH+PATCH_FILE);
}
}
private void applyPatch(final String oldApk,final String newApk,final String patch) {
new Thread(new Runnable() {
@Override
public void run() {
Log.d("TAG", "正在合成文件,可能需要一段时间,请稍后...");
PatchUtil.applyPatch(oldApk, newApk, patch);
String oldSign=SignUtil.getInstalledApkSignature(getApplicationContext(), PACKAGE_NAME);
String newSign=SignUtil.getUnInstalledApkSignature(newApk);
if(oldSign.equals(newSign)){
Log.d("TAG", "签名相同");
ApkUtil.installApk(MainActivity.this, newApk);
}else{
Log.d("TAG","签名错误");
}
}
}).start();
}
}
工具类代码就不贴了,其作用就跟名字一样.还有注意的就是记得加读取内存卡的权限.
这时候build一下,再运行程序,记得把patch文件放在对应的文件目录下,不出意外,几秒钟之后就会合成新包并进行安装.
参考连接
源代码下载
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。