Android開發 如何使用差分算法實現增量更新支持新建四大組件

在當代App應用大小不斷增大的情況下增量更新代替全量更新已是趨勢,可以節省許多用戶流量,還是老樣子先上效果圖,由於上傳圖片限制壓縮有點嚴重湊合看吧:

如果大家想直接使用,我這裏已經爲大家封裝成library庫了,大家直接依賴庫就可以直接使用了非常簡單:

allprojects {
    repositories {
        
        maven { url('https://oranges.bintray.com/DiffPatch') }
    }
}
    implementation 'com.xhiston.diffpatch:diffpatch:1.0.0'
DiffPatchUtil diffPatchUtil = new DiffPatchUtil(context);

diffPatchUtil.setOnDiffClickListener(new DiffPatchUtil.OnDiffClickListener() {
            @Override
            public void onStart() {

            }

            @Override
            public void onSuccess() {
                diffPatchUtil.showMessage("Success");
            }

            @Override
            public void onError(String msg) {
                diffPatchUtil.showMessage(msg);
            }
        });        

 diffPatchUtil.setOnPatchClickListener(new DiffPatchUtil.OnPatchClickListener() {
            @Override
            public void onStart() {

            }

            @Override
            public void onSuccess() {
                diffPatchUtil.showMessage("Success");
            }

            @Override
            public void onError(String msg) {
                diffPatchUtil.showMessage(msg);
            }
        });       


File file = new File(newApk);
//打出差分補丁包路徑爲patch
diffPatchUtil.diff(getApplicationInfo().sourceDir, newApk, patch);        

File file = new File(patch);       
//根據補丁包路徑patch合成新的apk路徑爲patchApk       
diffPatchUtil.patch(getApplicationInfo().sourceDir, patchApk, patch);                        
                

當然還有不能忘記加入權限

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

最後別忘了下載so庫導入項目,如果項目中發現有問題隨時歡迎告知我及時修正。

如果準備自己手寫一個增量更新注意你的Android Api Level 爲29以上的話請在manifest中加入以下一行代碼,否則就算打開了讀寫權限也會無法讀寫本地文件,如果依賴的是我的lib就不用寫了因爲我已經加入了本行代碼:

<application android:requestLegacyExternalStorage="true" />

如果大家想了解原理準備自己擼一個就往下看吧具體實現方式吧:

首先使用差分算法bsdiff計算出差分包,感興趣的可以自己點擊進去下載源碼,然後就是使用bzip2壓縮工具打包生成補丁差分包文件和合並補丁包文件;由於這裏提供的都是C語言程序所以我們需要藉助NDK/JNI實現增量更新了。

我們先去bsdiff地址下載bsdiff.c和bspatch.c這兩個文件,然後去bzip2下載源碼包解壓複製粘貼出我們需要的文件:

                        bzip2/blocksort.c\
                   		bzip2/bzip2.c\
                    	bzip2/bzip2recover.c\
                    	bzip2/bzlib.c\
                    	bzip2/bzlib.h\
                    	bzip2/bzlib_private.h\
                    	bzip2/compress.c\
                    	bzip2/crctable.c\
                        bzip2/decompress.c\
                        bzip2/huffman.c\
                        bzip2/randtable.c

好了準備工作我們都做好了就開始創建JNI文件吧:

1.創建一個java文件例如:DiffPatchUtil,然後使用命令進入該文件目錄下用命令編譯“javac DiffPatchUtil.java”生成DiffPatchUtil.class文件,再執行“javah com.xhiston.diffpatch

.DiffPatchUtil”生成com_xhiston_diffpatch_DiffPatchUtil.h文件這一步大家需要注意一下命令目錄回退一下到DiffPatchUtil的最外層包名下面不然命令提示找不包名下文件。DiffPatchUtil.java創建的時候可以簡單的只放JNI回調的相關方法,生成com_xhiston_diffpatch_DiffPatchUtil.h文件後再修改添加其他方法。

package com.xhiston.diffpatch;

import android.content.Context;
import android.os.Looper;
import android.widget.Toast;

/**
 * Created by xie on 2020/10/20.
 */
public class DiffPatchUtil {

    static {
        System.loadLibrary("diffpatch");
    }
   
    /**
     * 採用差分算法將當前包與新包打patch補丁包,生成xxx.patch文件
     **/
    public native int diff(String oldApk, String newApk, String patch);

    /**
     * 採用差分算法將patch補丁包與當前包合併生成新包,生成apk文件
     **/
    public native int patch(String oldApk, String newApk, String patch);

}

com_xhiston_diffpatch_DiffPatchUtil.h文件:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_xhiston_diffpatch_DiffPatchUtil */

#ifndef _Included_com_xhiston_diffpatch_DiffPatchUtil
#define _Included_com_xhiston_diffpatch_DiffPatchUtil
#ifdef __cplusplus
extern "C" {
#endif
    int mybspatch(JNIEnv *env, jobject clazz,int argc, char *argv[]);

    int mybsdiff(JNIEnv *env, jobject clazz, int argc,char *argv[]);

    JNIEXPORT jint JNICALL Java_com_xhiston_diffpatch_DiffPatchUtil_patch
      (JNIEnv *, jobject, jstring, jstring, jstring);

    JNIEXPORT jint JNICALL Java_com_xhiston_diffpatch_DiffPatchUtil_diff
      (JNIEnv *, jobject, jstring, jstring, jstring);

#ifdef __cplusplus
}
#endif
#endif

2.創建com_xhiston_diffpatch_DiffPatchUtil.c以及JNI配置文件Android.mk:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_LDLIBS += -L$(SYSROOT)/usr/lib -llog
APP_ABI := All
APP_PLATFORM := android-16
LOCAL_C_INCLUDES :=bzip2
LOCAL_MODULE    := diffpatch
LOCAL_SRC_FILES := com_xhiston_diffpatch_DiffPatchUtil.h\
				   	com_xhiston_diffpatch_DiffPatchUtil.c\
					bspatch.c\
					bsdiff.c\
					myerr.h\
                    myerr.c\
                    	bzip2/blocksort.c\
                   		bzip2/bzip2.c\
                    	bzip2/bzip2recover.c\
                    	bzip2/bzlib.c\
                    	bzip2/bzlib.h\
                    	bzip2/bzlib_private.h\
                    	bzip2/compress.c\
                    	bzip2/crctable.c\
                        bzip2/decompress.c\
                        bzip2/huffman.c\
                        bzip2/randtable.c\

include $(BUILD_SHARED_LIBRARY)

修改一下bspatch.c、bsdiff.c裏的main方法名然後com_xhiston_diffpatch_DiffPatchUtil中就可以重新調用了,可以參考我的源碼進行修改,確保代碼無誤後便可以ndk-buildd編譯生成so庫了,當然編譯的時候也會檢查代碼報錯的需要自行修改,不過這個就要求大家有一定的C語言基礎了,沒基礎的話可以現學一下不是多難。

com_xhiston_diffpatch_DiffPatchUtil.c:


#include <com_xhiston_diffpatch_DiffPatchUtil.h>

jint
Java_com_xhiston_diffpatch_DiffPatchUtil_patch (JNIEnv *env, jclass clazz, jstring old, jstring new, jstring patch) {
    int args=4;
    int resutlt = args;
    char *argv[args];
    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));
    resutlt = mybspatch(env,clazz,args,argv);
    (*env)->ReleaseStringUTFChars(env,old, argv[1]);
    (*env)->ReleaseStringUTFChars(env,new, argv[2]);
    (*env)->ReleaseStringUTFChars(env,patch,argv[3]);
    return resutlt;
}


jint
Java_com_xhiston_diffpatch_DiffPatchUtil_diff (JNIEnv *env, jobject clazz, jstring old, jstring new, jstring patch) {
       int args=4;
       int resutlt = args;
       char *argv[args];
       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));
       resutlt= mybsdiff(env,clazz,args,argv);
       (*env)->ReleaseStringUTFChars(env,old, argv[1]);
       (*env)->ReleaseStringUTFChars(env,new, argv[2]);
       (*env)->ReleaseStringUTFChars(env,patch,argv[3]);
 return resutlt;
}

bspatch.c:

int mybspatch(JNIEnv *env, jobject clazz,int argc,char * argv[])
{
    onPatchStart(env,clazz);
	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 ;
    errno =0;
	if(argc!=4) errx(1,"usage: %s oldfile newfile patchfile\n",argv[0]);

	/* Open patch file */
	if ((f = fopen(argv[3], "r")) == NULL){
		errx(1, "fopen(%s)", argv[3]);
	    LOG_E("errno is %d,strerror is %s\n",errno,strerror(errno));
		onPatchError(env,clazz,strerror(errno));
		return -1;
    }

	/* Read header */
	if (fread(header, 1, 32, f) < 32) {
		if (feof(f))
			err(1, "Corrupt patch\n");
		errx(1, "fread(%s)", argv[3]);
		LOG_E("errno is %d,strerror is %s\n",errno,strerror(errno));
        onPatchError(env,clazz,strerror(errno));
        return -1;
	}

	/* Check for appropriate magic */
	if (memcmp(header, "BSDIFF40", 8) != 0)
		err(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))
		err(1,"Corrupt patch\n");

	/* Close patch file and re-open it via libbzip2 at the right places */
	if (fclose(f))
		errx(1, "fclose(%s)", argv[3]);
	if ((cpf = fopen(argv[3], "r")) == NULL){
		errx(1, "fopen(%s)", argv[3]);
		LOG_E("errno is %d,strerror is %s\n",errno,strerror(errno));
		onPatchError(env,clazz,strerror(errno));
		return -1;
	}
	if (fseeko(cpf, 32, SEEK_SET))
		errx(1, "fseeko( %lld)",  (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){
		errx(1, "fopen(%s)", argv[3]);
		LOG_E("errno is %d,strerror is %s\n",errno,strerror(errno));
		onPatchError(env,clazz,strerror(errno));
		return -1;
	}
	if (fseeko(dpf, 32 + bzctrllen, SEEK_SET))
		errx(1, "fseeko( %lld)",  (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){
		errx(1, "fopen(%s)", argv[3]);
		LOG_E("errno is %d,strerror is %s\n",errno,strerror(errno));
        onPatchError(env,clazz,strerror(errno));
        return -1;
	}
	if (fseeko(epf, 32 + bzctrllen + bzdatalen, SEEK_SET))
		errx(1, "fseeko(%lld)",  (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)) errx(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)))
				err(1, "Corrupt patch\n");
			ctrl[i]=offtin(buf);
		};

		/* Sanity-check */
		if(newpos+ctrl[0]>newsize)
			err(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)))
			err(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)
			err(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)))
			err(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))
		errx(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))
		errx(1,"%s",argv[2]);

	free(new);
	free(old);
    LOG_E("errno is %d,strerror is %s\n",errno,strerror(errno));
    if(errno == 0){
        onPatchSuccess(env,clazz);
    }else{
        onPatchError(env,clazz,strerror(errno));
        return -1;
    }
	return 0;
}

bsdiff.c:

int mybsdiff(JNIEnv *env,jobject clazz,int argc,char *argv[])
{
    onDiffStart(env,clazz);
	int fd;
	u_char *old,*new;
	off_t oldsize,newsize;
	off_t *I,*V;
	off_t scan,pos,len;
	off_t lastscan,lastpos,lastoffset;
	off_t oldscore,scsc;
	off_t s,Sf,lenf,Sb,lenb;
	off_t overlap,Ss,lens;
	off_t i;
	off_t dblen,eblen;
	u_char *db,*eb;
	u_char buf[8];
	u_char header[32];
	FILE * pf;
	BZFILE * pfbz2;
	int bz2err;
    errno =0;
	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)) errx(1,"%s 非設備文件",argv[1]);
    LOG_E("設備文件 %s",argv[1]);
	if(((I=malloc((oldsize+1)*sizeof(off_t)))==NULL) ||
		((V=malloc((oldsize+1)*sizeof(off_t)))==NULL)) err(1,"設備文件長度爲0");
	qsufsort(I,V,old,oldsize);
    free(V);

 	if(((fd=open(argv[2],O_RDONLY,0))<0) ||
 		((newsize=lseek(fd,0,SEEK_END))==-1) ||
 		((new=malloc(newsize+1))==NULL) ||
 		(lseek(fd,0,SEEK_SET)!=0) ||
 		(read(fd,new,newsize)!=newsize) ||
 		(close(fd)==-1)) errx(1,"%s 非存儲卡文件",argv[2]);
 	if(((db=malloc(newsize+1))==NULL) ||
 		((eb=malloc(newsize+1))==NULL)) err(1,"存儲卡文件長度爲0");
 	LOG_E("存儲卡文件 %s",argv[2]);
 	dblen=0;
 	eblen=0;
    if ((pf = fopen(argv[3], "w")) == NULL){
    	errx(1, "創建 %s 失敗", argv[3]);
        LOG_E("errno is %d,strerror is %s\n",errno,strerror(errno));
    	onPatchError(env,clazz,strerror(errno));
    	return -1;
    }
    memcpy(header,"BSDIFF40",8);
	offtout(0, header + 8);
	offtout(0, header + 16);
	offtout(newsize, header + 24);
    if (fwrite(header, 32, 1, pf) != 1)
		errx(1, "fwrite(%s) 寫入失敗", argv[3]);
    if ((pfbz2 = BZ2_bzWriteOpen(&bz2err, pf, 9, 0, 0)) == NULL)
		errx(1, "BZ2_bzWriteOpen, bz2err = %d", bz2err);
	scan=0;len=0;
	lastscan=0;lastpos=0;lastoffset=0;
    while(scan<newsize) {
		oldscore=0;
		for(scsc=scan+=len;scan<newsize;scan++) {
			len=search(I,old,oldsize,new+scan,newsize-scan,
					0,oldsize,&pos);

			for(;scsc<scan+len;scsc++)
			if((scsc+lastoffset<oldsize) &&
				(old[scsc+lastoffset] == new[scsc]))
				oldscore++;

			if(((len==oldscore) && (len!=0)) ||
				(len>oldscore+8)) break;

			if((scan+lastoffset<oldsize) &&
				(old[scan+lastoffset] == new[scan]))
				oldscore--;
		};

		if((len!=oldscore) || (scan==newsize)) {
			s=0;Sf=0;lenf=0;
			for(i=0;(lastscan+i<scan)&&(lastpos+i<oldsize);) {
				if(old[lastpos+i]==new[lastscan+i]) s++;
				i++;
				if(s*2-i>Sf*2-lenf) { Sf=s; lenf=i; };
			};

			lenb=0;
			if(scan<newsize) {
				s=0;Sb=0;
				for(i=1;(scan>=lastscan+i)&&(pos>=i);i++) {
					if(old[pos-i]==new[scan-i]) s++;
					if(s*2-i>Sb*2-lenb) { Sb=s; lenb=i; };
				};
			};

			if(lastscan+lenf>scan-lenb) {
				overlap=(lastscan+lenf)-(scan-lenb);
				s=0;Ss=0;lens=0;
				for(i=0;i<overlap;i++) {
					if(new[lastscan+lenf-overlap+i]==
					   old[lastpos+lenf-overlap+i]) s++;
					if(new[scan-lenb+i]==
					   old[pos-lenb+i]) s--;
					if(s>Ss) { Ss=s; lens=i+1; };
				};

				lenf+=lens-overlap;
				lenb-=lens;
			};

			for(i=0;i<lenf;i++)
				db[dblen+i]=new[lastscan+i]-old[lastpos+i];
			for(i=0;i<(scan-lenb)-(lastscan+lenf);i++)
				eb[eblen+i]=new[lastscan+lenf+i];

			dblen+=lenf;
			eblen+=(scan-lenb)-(lastscan+lenf);

			offtout(lenf,buf);
			BZ2_bzWrite(&bz2err, pfbz2, buf, 8);
			if (bz2err != BZ_OK)
				errx(1, "BZ2_bzWrite, bz2err = %d", bz2err);

			offtout((scan-lenb)-(lastscan+lenf),buf);
			BZ2_bzWrite(&bz2err, pfbz2, buf, 8);
			if (bz2err != BZ_OK)
				errx(1, "BZ2_bzWrite, bz2err = %d", bz2err);

			offtout((pos-lenb)-(lastpos+lenf),buf);
			BZ2_bzWrite(&bz2err, pfbz2, buf, 8);
			if (bz2err != BZ_OK)
				errx(1, "BZ2_bzWrite, bz2err = %d", bz2err);

			lastscan=scan-lenb;
			lastpos=pos-lenb;
			lastoffset=pos-scan;
		};
	};

	BZ2_bzWriteClose(&bz2err, pfbz2, 0, NULL, NULL);
	if (bz2err != BZ_OK)
    		errx(1, "BZ2_bzWriteClose, bz2err = %d", bz2err);

    /* Compute size of compressed ctrl data */
    if ((len = ftello(pf)) == -1)
    		err(1, "ftello");
    offtout(len-32, header + 8);

    /* Write compressed diff data */
    if ((pfbz2 = BZ2_bzWriteOpen(&bz2err, pf, 9, 0, 0)) == NULL)
    		errx(1, "BZ2_bzWriteOpen, bz2err = %d", bz2err);
    BZ2_bzWrite(&bz2err, pfbz2, db, dblen);
    if (bz2err != BZ_OK)
    		errx(1, "BZ2_bzWrite, bz2err = %d", bz2err);
    	BZ2_bzWriteClose(&bz2err, pfbz2, 0, NULL, NULL);
    if (bz2err != BZ_OK)
    		errx(1, "BZ2_bzWriteClose, bz2err = %d", bz2err);

    /* Compute size of compressed diff data */
   if ((newsize = ftello(pf)) == -1)
    		err(1, "ftello");
   offtout(newsize - len, header + 16);

   /* Write compressed extra data */
   if ((pfbz2 = BZ2_bzWriteOpen(&bz2err, pf, 9, 0, 0)) == NULL)
    		errx(1, "BZ2_bzWriteOpen, bz2err = %d", bz2err);
   BZ2_bzWrite(&bz2err, pfbz2, eb, eblen);
    if (bz2err != BZ_OK)
    		errx(1, "BZ2_bzWrite, bz2err = %d", bz2err);
    BZ2_bzWriteClose(&bz2err, pfbz2, 0, NULL, NULL);
    if (bz2err != BZ_OK)
    		errx(1, "BZ2_bzWriteClose, bz2err = %d", bz2err);

    /* Seek to the beginning, write the header, and close the file */
    if (fseeko(pf, 0, SEEK_SET))
    		err(1, "fseeko");
    if (fwrite(header, 32, 1, pf) != 1)
    		errx(1, "fwrite(%s)", argv[3]);
    if (fclose(pf))
    		err(1, "fclose");

    /* Free the memory we used */
    free(db);
    free(eb);
    free(I);
    free(old);
    free(new);
    LOG_E("errno is %d,strerror is %s\n",errno,strerror(errno));
    if(errno == 0){
        onPatchSuccess(env,clazz);
    }else{
        onPatchError(env,clazz,strerror(errno));
        return -1;
    }
	return 0;
}

如果我的文章對你有幫助,"一鍵三連"給個鼓勵,讓我更新文章更有動力,“關注”我會有更多幹貨不定時的更新哦!

源碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章