流程
step1 使用bsdiff生成差异包PATCH.patch
step2 在手机上合并base包和差异包,生成新版本的安装包
step3 安装新的安装包
准备
bsdiff-4.3 (用于生成差异包,合并新包)
bzip2 (bsdiff要使用到)
试验
step1 解压bsdiff4.3的压缩包
step2 修改Makefile文件,将.ifndef和.endif缩进,要么无法进行后面的操作
CFLAGS += -O3 -lbz2 PREFIX ?= /usr/localINSTALL_PROGRAM ?= ${INSTALL} -c -s -m 555 INSTALL_MAN ?= ${INSTALL} -c -m 444 all: bsdiff bspatch bsdiff: bsdiff.c bspatch: bspatch.c install: ${INSTALL_PROGRAM} bsdiff bspatch ${PREFIX}/bin .ifndef WITHOUT_MAN ${INSTALL_MAN} bsdiff.1 bspatch.1 ${PREFIX}/man/man1 .endif
step3 在该文件夹的命令行里执行make命令,会生成bsdiff和bspatch两个可执行的文件。如果是从上面的地址下载的bsdiff的话,会爆出bspatch.c:39:21: error: unknown type name 'u_char'; did you mean 'char'?这样的错误。这是由于bspatch.c中缺少了#include <sys/types.h>,加上即可。然后执行make命令。
bogon:bsdiff-4.3 Tony$ make cc -O3 -lbz2 bsdiff.c -o bsdiff cc -O3 -lbz2 bspatch.c -o bspatch
step4 准备两个同一签名的apk,old.apk和.apk,放在bsdiff-4.3解压后的文件夹里。
step5 使用bsdiff生成差异文件PATCH.patch,命令格式:bsdiff oldfile newfile patchfile(这一步操作可以放在服务端来执行)
bogon:bsdiff-4.3 Tony$ ./bsdiff old.apk new.apk PATCH.patch
-step6 使用bspatch合成新的apk,dest.apk。命令格式:bspatch oldfile newfile patchfile
bogon:bsdiff-4.3 Tony$ ./bspatch old.apk dest.apk PATCH.patch
step7 验证生成的dest.apk和之前的new.apk使用一样,验证两个apk的md5值,即可。
bogon:bsdiff-4.3 Tony$ md5 new.apk MD5 (new.apk) = 55005cec2de8ad3668a0fd5bd8746f43 bogon:bsdiff-4.3 Tony$ md5 dest.apk MD5 (dest.apk) = 55005cec2de8ad3668a0fd5bd8746f43 bogon:bsdiff-4.3 Tony$ md5 old.apk MD5 (old.apk) = 822cc0938694008089ea5523f86585d7 bogon:bsdiff-4.3 Tony$
在Android中使用增量更新
ndk部分
step1 新建一个PatchUtils的类,用于调用native的方法
public class PatchUtils { static PatchUtils instance; public static PatchUtils getInstance() { if (instance == null) { instance = new PatchUtils(); } return instance; } static { System.loadLibrary("patchutils"); } /** * native方法 使用路径为oldApkPath的apk与路径为patchPath的补丁包,合成新的apk,并存储于newApkPath * <p> * 返回:0,说明操作成功 * * @param oldApkPath 示例:/sdcard/old.apk * @param newApkPath 示例:/sdcard/new.apk * @param patchPath 示例:/sdcard/xx.patch * @return */ public static native int bspatch(String oldApkPath, String newApkPath, String patchPath); }
step2 使用javah命令生成PatchUtils的头文件,或者使用ndk-build里的javah生成头文件。会在jni文件夹下生成一个com_bigademo_updatedemo_PatchUtils.h的文件,其中com_bigademo_updatedemo是我的包名。
step3 将bsdiff4.3文件夹中的bspatch.c文件导入到jni文件夹下。
step4 将bzip2文件夹导入到jni文件夹下
step5 删除bzip2中的除了以c和h结尾的其他文件。
如图:jni文件夹目录
step6 修改bspatch.c。引入bzip的文件,引入com_bigademo_updatedemo_PatchUtils.h,在bspatch.c中重写JNIEXPORT jint JNICALL Java_com_bigademo_updatedemo_PatchUtils_bspatch方法。代码如下
#if 0__FBSDID("$FreeBSD: src/usr.bin/bsdiff/bspatch/bspatch.c,v 1.1 2005/08/06 01:59:06 cperciva Exp $");#endif#include <stdlib.h>#include <stdio.h>#include <string.h>#include <err.h>#include <unistd.h>#include <fcntl.h>#include "bzip2/bzlib.c"#include "bzip2/crctable.c"#include "bzip2/compress.c"#include "bzip2/decompress.c"#include "bzip2/randtable.c"#include "bzip2/blocksort.c"#include "bzip2/huffman.c"#include <com_bigademo_updatedemo_PatchUtils.h>JNIEXPORT jint JNICALL Java_com_bigademo_updatedemo_PatchUtils_bspatch(JNIEnv *env, jclass cls, jstring old, jstring new, jstring patch) { 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]); int ret = genpatch(argc, argv); printf("genDiff result = %d ", ret); (*env)->ReleaseStringUTFChars(env, old, argv[1]); (*env)->ReleaseStringUTFChars(env, new, argv[2]); (*env)->ReleaseStringUTFChars(env, patch, argv[3]); return ret; }static off_t offtin(u_char *buf){ off_t y; y=buf[7]&0x7F; y=y*256;y+=buf[6]; y=y*256;y+=buf[5]; y=y*256;y+=buf[4]; y=y*256;y+=buf[3]; y=y*256;y+=buf[2]; y=y*256;y+=buf[1]; y=y*256;y+=buf[0]; if(buf[7]&0x80) y=-y; return y; }int genpatch(int argc,char * argv[]){ FILE * f, * cpf, * dpf, * epf; BZFILE * cpfbz2, * dpfbz2, * epfbz2; int cbz2err, dbz2err, ebz2err; int fd; ssize_t oldsize,newsize; ssize_t bzctrllen,bzdatalen; u_char header[32],buf[8]; u_char *old, *new; off_t oldpos,newpos; off_t ctrl[3]; off_t lenread; off_t i; if(argc!=4) errx(1,"usage: %s oldfile newfile patchfile\n",argv[0]); /* Open patch file */ if ((f = fopen(argv[3], "r")) == NULL) err(1, "fopen(%s)", argv[3]); /* File format: 0 8 "BSDIFF40" 8 8 X 16 8 Y 24 8 sizeof(newfile) 32 X bzip2(control block) 32+X Y bzip2(diff block) 32+X+Y ??? bzip2(extra block) with control block a set of triples (x,y,z) meaning "add x bytes from oldfile to x bytes from the diff block; copy y bytes from the extra block; seek forwards in oldfile by z bytes". */ /* Read header */ if (fread(header, 1, 32, f) < 32) { if (feof(f)) errx(1, "Corrupt patch\n"); err(1, "fread(%s)", argv[3]); } /* Check for appropriate magic */ if (memcmp(header, "BSDIFF40", 8) != 0) errx(1, "Corrupt patch\n"); /* Read lengths from header */ bzctrllen=offtin(header+8); bzdatalen=offtin(header+16); newsize=offtin(header+24); if((bzctrllen<0) || (bzdatalen<0) || (newsize<0)) errx(1,"Corrupt patch\n"); /* Close patch file and re-open it via libbzip2 at the right places */ if (fclose(f)) err(1, "fclose(%s)", argv[3]); if ((cpf = fopen(argv[3], "r")) == NULL) err(1, "fopen(%s)", argv[3]); if (fseeko(cpf, 32, SEEK_SET)) err(1, "fseeko(%s, %lld)", argv[3], (long long)32); if ((cpfbz2 = BZ2_bzReadOpen(&cbz2err, cpf, 0, 0, NULL, 0)) == NULL) errx(1, "BZ2_bzReadOpen, bz2err = %d", cbz2err); if ((dpf = fopen(argv[3], "r")) == NULL) err(1, "fopen(%s)", argv[3]); if (fseeko(dpf, 32 + bzctrllen, SEEK_SET)) err(1, "fseeko(%s, %lld)", argv[3], (long long)(32 + bzctrllen)); if ((dpfbz2 = BZ2_bzReadOpen(&dbz2err, dpf, 0, 0, NULL, 0)) == NULL) errx(1, "BZ2_bzReadOpen, bz2err = %d", dbz2err); if ((epf = fopen(argv[3], "r")) == NULL) err(1, "fopen(%s)", argv[3]); if (fseeko(epf, 32 + bzctrllen + bzdatalen, SEEK_SET)) err(1, "fseeko(%s, %lld)", argv[3], (long long)(32 + bzctrllen + bzdatalen)); if ((epfbz2 = BZ2_bzReadOpen(&ebz2err, epf, 0, 0, NULL, 0)) == NULL) errx(1, "BZ2_bzReadOpen, bz2err = %d", ebz2err); if(((fd=open(argv[1],O_RDONLY,0))<0) || ((oldsize=lseek(fd,0,SEEK_END))==-1) || ((old=malloc(oldsize+1))==NULL) || (lseek(fd,0,SEEK_SET)!=0) || (read(fd,old,oldsize)!=oldsize) || (close(fd)==-1)) err(1,"%s",argv[1]); if((new=malloc(newsize+1))==NULL) err(1,NULL); oldpos=0;newpos=0; while(newpos<newsize) { /* Read control data */ for(i=0;i<=2;i++) { lenread = BZ2_bzRead(&cbz2err, cpfbz2, buf, 8); if ((lenread < 8) || ((cbz2err != BZ_OK) && (cbz2err != BZ_STREAM_END))) errx(1, "Corrupt patch\n"); ctrl[i]=offtin(buf); }; /* Sanity-check */ if(newpos+ctrl[0]>newsize) errx(1,"Corrupt patch\n"); /* Read diff string */ lenread = BZ2_bzRead(&dbz2err, dpfbz2, new + newpos, ctrl[0]); if ((lenread < ctrl[0]) || ((dbz2err != BZ_OK) && (dbz2err != BZ_STREAM_END))) errx(1, "Corrupt patch\n"); /* Add old data to diff string */ for(i=0;i<ctrl[0];i++) if((oldpos+i>=0) && (oldpos+i<oldsize)) new[newpos+i]+=old[oldpos+i]; /* Adjust pointers */ newpos+=ctrl[0]; oldpos+=ctrl[0]; /* Sanity-check */ if(newpos+ctrl[1]>newsize) errx(1,"Corrupt patch\n"); /* Read extra string */ lenread = BZ2_bzRead(&ebz2err, epfbz2, new + newpos, ctrl[1]); if ((lenread < ctrl[1]) || ((ebz2err != BZ_OK) && (ebz2err != BZ_STREAM_END))) errx(1, "Corrupt patch\n"); /* Adjust pointers */ newpos+=ctrl[1]; oldpos+=ctrl[2]; }; /* Clean up the bzip2 reads */ BZ2_bzReadClose(&cbz2err, cpfbz2); BZ2_bzReadClose(&dbz2err, dpfbz2); BZ2_bzReadClose(&ebz2err, epfbz2); if (fclose(cpf) || fclose(dpf) || fclose(epf)) err(1, "fclose(%s)", argv[3]); /* Write the new file */ if(((fd=open(argv[2],O_CREAT|O_TRUNC|O_WRONLY,0666))<0) || (write(fd,new,newsize)!=newsize) || (close(fd)==-1)) err(1,"%s",argv[2]); free(new); free(old); return 0; }
step7 在jni下创建Android.mk和Application.mk两个文件
Android.mk
# 构建系统提供的宏函数 my-dir 将返回当前目录(包含 Android.mk 文件本身的目录)的路径,基本上是固定的,不需要去动LOCAL_PATH := $(call my-dir)# 会清除很多 LOCAL_XXX 变量,不会清除 LOCAL_PATH,基本上是固定的,不需要去动include $(CLEAR_VARS)# 需要构建模块的名称,会自动生成相应的 libNDKSample.so 文件,每个模块名称必须唯一,且不含任何空格LOCAL_MODULE := patchutils# 包含要构建到模块中的 C 或 C++ 源文件列表LOCAL_SRC_FILES := bspatch.c LOCAL_C_INCLUDES := /Users/Tony/Code/android/AndroidStudioProjects/demo/updatedemo/app/src/main/jni/bzip2# 帮助系统将所有内容连接到一起,固定的,不需要去动include $(BUILD_SHARED_LIBRARY)
Application.mk
APP_PLATFORM := android-16# 选择不同的 ABI,多个使用空格作为分隔符,全部是allAPP_ABI := all
step8 在module中的build.gradle中配置ndk。
android{ ... defaultConfig{ ndk{ //这个名字就是在PatchUtils中loadLibrary的名字,同时和Android.mk中的LOCAL_MODULE的名字一样,也和生成的so的文件的名字类似。 moduleName "patchutils" } } externalNativeBuild { ndkBuild { path "src/main/jni/Android.mk" } } }
java部分
创建一个ApkExtract类,用于获取当前app的apk和安装新的apk。因为涉及到Android 7.0的,需要处理Provider的问题,同时在AndroidManifest.xml增加一些代码
public class ApkExtract { public static String extract(Context context) { context = context.getApplicationContext(); ApplicationInfo applicationInfo = context.getApplicationInfo(); String apkPath = applicationInfo.sourceDir; Log.d("info", apkPath); return apkPath; } public static void install(Context context, String apkPath) { Intent intent = new Intent(Intent.ACTION_VIEW); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); Uri contentUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", new File(apkPath)); intent.setDataAndType(contentUri, "application/vnd.android.package-archive"); } else { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setDataAndType(Uri.fromFile(new File(apkPath)), "application/vnd.android.package-archive"); } context.startActivity(intent); } }
androidmanifest.xml
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <provider android:name="android.support.v4.content.FileProvider" android:authorities="${applicationId}.fileProvider" android:grantUriPermissions="true" android:exported="false"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> ... </application>
最后在需要更新的时候调用下面这个方法,当然先做一下动态调用读写权限的操作。
if (ContextCompat.checkSelfPermission(MainActivity.this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(MainActivity.this, new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, 2); } else { doBspatch();// startActivity(new Intent(MainActivity.this, SecondActivity.class)); } private void doBspatch() { final File destApk = new File(Environment.getExternalStorageDirectory(), "dest.apk"); final File patch = new File(Environment.getExternalStorageDirectory(), "PATCH.patch"); //一定要检查文件都存在 int result= PatchUtils.bspatch(ApkExtract.extract(this), destApk.getAbsolutePath(), patch.getAbsolutePath()); Log.e("info","patch result "+result); if (destApk.exists()) { ApkExtract.install(this, destApk.getAbsolutePath()); } }
作者:Coding小学生
链接:https://www.jianshu.com/p/472d0453656a