爲何須要增量更新:節省流量,節省流量,節省流量,重要的事情說三遍!
增量更新不只可讓用戶在客戶端實現省流量更新,更重要的是增量更新還能夠實現服務器端流量的節省,爲網站節省成本。html
增量更新的原理
服務端將應用的舊版本Apk與新版本Apk作差分處理,獲得新版APK更新部分的差分包,例如舊版本的APK有6M,新版的有10M,更新的部分則可能只有4M左右(這4M文件除了包含更新內容之外,還包含一些上下文相關的東西),對於服務端來說,若是是大型應用每次用戶下載數量在百萬以上,那麼數百萬*4M節省到的流量可想而知。這裏強調一點,新apk和舊apk生成的差分包,其體積大小必定要小於新apk的體積,不然增量更新無心義。
客戶端在用戶下載了差分更新包以後,將手機端存在的舊版本軟件APK(大多處在/data/data/app/下),複製到SD卡或者cache中,並與差分更新包進行合併,獲得一個新版本的apk升級安裝包,最終,這個生成的apk和你作差分以前的apk是一致的。
爲了獲得增量更新中的差分包,咱們使用網上一款很是有名的開源二進制查分工具bsdiff。其中bsdiff依賴bzip2,因此咱們還須要用到 bzip2工具。
關於bsdiff你們能夠去官網下載http://www.daemonology.net/bsdiff/
關於bzip2你們能夠去官網下載http://www.bzip.org/downloads.html
下載後的bsdiff目錄以下java
在bsdiff中,bsdiff.c 用於生成差分包,bspatch.c 用於合併文件。android
ok,準備就緒下面開始步入正題,我將分爲服務端和客戶端兩個方面給你們講解。講以前,仍是先畫個圖,給你們從全局上描繪一下。ruby
客戶端開發流程講解
第1步.下載服務端的差分包
下面是經過自定義的下載方法,獲得差分包文件patchFile 。服務器
String downUrl = "http://www.castiel.com/apk.patch"; File patchFile = DownloadUtils.download(downUrl); public static File download(String url){ File file = null; InputStream is = null; FileOutputStream os = null; try { file = new File(Environment.getExternalStorageDirectory(),Constants.PATCH_FILE); if (file.exists()) { file.delete(); } HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setDoInput(true); is = conn.getInputStream(); os = new FileOutputStream(file); byte[] buffer = new byte[1024]; int len = 0; while((len = is.read(buffer)) != -1){ os.write(buffer, 0, len); } } catch(Exception e){ e.printStackTrace(); }finally{ try { os.close(); } catch (IOException e) { e.printStackTrace(); } try { is.close(); } catch (IOException e) { e.printStackTrace(); } } return file; }
第2步.合併獲得最新版本的APK文件
String oldfile = ApkUtils.getSourceApkPath(MainActivity.this, getPackageName()); String newfile = Constants.NEW_APK_PATH; String patchfile = patchFile.getAbsolutePath(); BsPatch.patch(oldfile, newfile, patchfile);
這裏的BsPatch是咱們的定義的調用Native方法的類markdown
public class BsPatch { /** * 合併包 * @param oldfile 舊版本文件 * @param newfile 新版本文件 * @param patchfile 合併後的文件 */ public native static void patch(String oldfile, String newfile, String patchfile); static{ System.loadLibrary("bspatch"); } }
其中加載的baspatch.c文件app
#if 0 __FBSDID("$FreeBSD: src/usr.bin/bsdiff/bspatch/bspatch.c,v 1.1 2005/08/06 01:59:06 cperciva Exp $"); #endif #include <bzlib.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <err.h> #include <unistd.h> #include <fcntl.h> 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 main(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; }
爲了JNI開發方便,咱們能夠生成BsPatch的頭文件工具
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_castiel_inupdate_utils_BsPatch */ #ifndef _Included_com_castiel_inupdate_utils_BsPatch #define _Included_com_castiel_inupdate_utils_BsPatch #ifdef __cplusplus extern "C" { #endif /* * Class: com_castiel_inupdate_utils_BsPatch * Method: patch * Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_com_castiel_inupdate_utils_BsPatch_patch (JNIEnv *, jclass, jstring, jstring, jstring); #ifdef __cplusplus } #endif #endif
Android.mk文件配置測試
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE:= bspatch LOCAL_SRC_FILES:= bspatch.c include $(BUILD_SHARED_LIBRARY)
第3步.安裝合併後的新APK
String newApkPath = Environment.getExternalStorageDirectory() + File.separator + "castielNew.apk"; installApk(MainActivity.this, newApkPath); /** * 安裝Apk * @param context * @param apkPath */ 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); }
注意不要忘記添加權限網站
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
服務端開發流程講解
其實開發很是簡單,這裏我簡單的說明一下關鍵源碼
public class BsDiff { /** * 導入差分庫,調用差分方法 * @param oldfile * @param newfile * @param patchfile */ public native static void diff(String oldfile, String newfile, String patchfile); static{ System.loadLibrary("bsdiff.so"); } }
測試獲得差分包
public class Test { public static final String OLD_APK_PATH = "你服務器端的路徑/castiel_old.apk"; public static final String NEW_APK_PATH = "你服務器端的路徑/castiel_new.apk"; public static final String PATCH_PATH = "你服務器端的路徑/castiel/apk/apk.patch"; public static void main(String[] args) { //獲得差分包 BsDiff.diff(ConstantsWin.OLD_APK_PATH, ConstantsWin.NEW_APK_PATH, ConstantsWin.PATCH_PATH); } }
增量更新的問題
- 增量更新在實施中,咱們沒法保證用戶每次都可以及時升級到最新版本,因此必須對所發佈的每個版本都和最新的版本做差分處理,以便讓全部版本的用戶均可以進行差分升級,這樣的流程除非經過自動化的腳本批量生成不然比較繁瑣。
- 增量更新對於手機內存沒法提供足夠空間用做本地APK合成的用戶和本地APK損壞的用戶來講,是沒法實現的。