【Android-功能】Android应用增量更新

很久没有更新博客了,真是堕落啊,几次想提起笔,却总是被各种琐事耽搁,以后会多写文章记录点滴。

背景

随着android应用体积的不断增大,以及应用版本发布的不断更迭,用户的升级成了一个问题,google也意识到不断更新应用对用户流量的损耗,在Google I/O 上提及的 Smart App update,即应用增量升级,或者叫做差分升级的做法,并在新版本的Google Play中得到支持,某天在和群友聊天是扯到这方面的话题,好奇就稍微研究了一下。

增量升级的原理

今天我们就来实现类似的应用的增量升级。其实增量升级的原理很简单,即首先将应用的旧版本Apk与新版本Apk做差分,得到更新的部分的补丁,例如旧版本的APK有5M,新版的有8M,更新的部分则可能只有3M左右(这里需要说明的是,得到的差分包大小并不是简单的相减,因为其实需要包含一些上下文相关的东西),使用差分升级的好处显而易见,那么你不需要下载完整的8M文件,只需要下载更新部分就可以,而更新部分可能只有3、4M,可以很大程度上减少流量的损失。

在用户下载了差分包之后,需要在手机端将他们组合起来。可以参考的做法是先将手机端的旧版本软件(多半在/data/下),复制到SD卡或者cache中,将它们和之前的差分patch进行组合,得到一个新版本的apk应用,如果不出意外的话,这个生成的apk和你之前做差分的apk是一致的。

增量升级的操作

在了解基本的原理之后,我们来逐步解决其中的各个难点。首先是差分包patch的生成。如果做过android手机OTA升级的同学应该注意到,在update.zip中的patch文件夹中有需要与系统文件同名但是以xxx.p 为后缀的文件,他们就是生成的差分patch文件。我们可以借鉴OTA系统升级的差分生成工具来生成我们单个应用apk的差分patch文件。

OTA系统差分包的制作,使用命令:

[html] view plaincopyprint?

  1. ./build/tools/releasetools/ota_from_target_files -n -i <旧包> <新包> <差分包名>

在查阅ota_from_target_files 的代码可知,是在函数WriteIncrementalOTAPackage里生成差分包的,在这个函数里边创建了common.Difference这个类,我们继续跟进,在common.py中的类   class Difference(object):里可以看到:

[html] view plaincopyprint?

  1. diff_program = DIFF_PROGRAM_BY_EXT.get(ext, "bsdiff")

至此我们就看到了android中提供我们用来制作差分增量升级包的工具,"bsdiff",这是一个很牛X开源的二进制差分工具.相关的介绍传送门

相关的代码地址 或者在android的代码目录下 \external\bsdiff

bsdiff是二进制差分工具,其对应的bspatch是相应的补丁合成工具

需要注意的是增量升级的补丁包,是需要在服务器端,即PC端完成,大致流程如,制作补丁时调用bsdiff函数,根据两个不同版本的二进制文件,生成补丁文件。

[html] view plaincopyprint?

  1. 命令:bsdiff oldfile newfile patchfile
  2. 例如: bsdiff xx_v1.0.apk xx_v2.0.apk xx.patch

将生成的补丁包 xx.patch放置在升级服务器上,供用户下载升级,对应多版本需要对不同的版本进行差分,对于版本跨度较大的,建议整包升级。

用户在下载了 xx.patch补丁包后,需要用到补丁所对应的apk,即原来系统安装的旧版本apk和补丁合成的bspatch工具。系统旧版本的apk可以通过copy系统data/app目录下的apk文件获取,而补丁合成的bspatch可以通过将bspatch源码稍作修改,封装成一个so库,供手机端调用。

[html] view plaincopyprint?

  1. bspatch的命令格式为:
  2. bspatch oldfile newfile patchfile

和差分时的参数一样。合成新的apk便可以用于安装。

以上只是简单的操作原理,增量升级还涉及很多其他方面,例如,升级补丁校验等问题,可以参考android源码中bootable\recovery\applypatch的相关操作,本文只是浅析,在此不表。

不足

增量升级并非完美无缺的升级方式,至少存在以下两点不足:

1.增量升级是以两个应用版本之间的差异来生成补丁的,你无法保证用户每次的及时升级到最新,所以你必须对你所发布的每一个版本都和最新的版本作差分,以便使所有版本的用户都可以差分升级,这样操作相对于原来的整包升级较为繁琐,不过可以通过自动化的脚本批量生成。

2.增量升级成功的前提是,用户手机端必须有能够让你拷贝出来且与你服务器用于差分的版本一致的apk,这样就存在,例如,系统内置的apk无法获取到,无法进行增量升级;对于某些与你差分版本一致,但是内容有过修改的(比如破解版apk),这样也是无法进行增量升级的,为了防止合成补丁错误,最好在补丁合成前对旧版本的apk进行sha1sum校验,保证基础包的一致性。

小实验

多说无益,实践才是王道。下面就来简单实践一下,检测之前理论的正确性。下载实验包 (http://download.csdn.net/detail/hmg25/4676737),解压后文件如下

[html] view plaincopyprint?

  1. ├── bsdiff-4.3        //bsdiff的源码路径,官网获取
  2. │   ├── bsdiff.1
  3. │   ├── bsdiff.c
  4. │   ├── bspatch.1
  5. │   ├── bspatch.c
  6. │   └── Makefile
  7. ├── bsdiff-4.3.tar.gz
  8. ├── bsdiff4.3-win32     //windows PC端的测试工具
  9. │   ├── Binary diff.txt
  10. │   ├── bsdiff.exe
  11. │   ├── bspatch.exe
  12. │   └── LICENSE
  13. ├── bspatch             //手机端的测试工具
  14. ├── iReader1.6.2.0(v35).apk      // 旧版本的apk
  15. └── iReader1.8.0.1(v40).apk    //新版本的apk

以附带的iReader来做测试,在shell进入test\bsdiff4.3-win32文件夹,并下运行命令:

[html] view plaincopyprint?

  1. bsdiff.exe   ../iReader1.6.2.0(v35).apk   ../iReader1.8.0.1(v40).apk   ../ireader.patch

原来的apk(2.94M),新版本的(3.24M),得到的patch文件为1.77M,用户需要下载的就只是1.77M,流量节省了很多。
     下面先在电脑端将他们合并。

[html] view plaincopyprint?

  1. bspatch.exe  ../iReader1.6.2.0(v35).apk   ../new.apk    ../ireader.patch

执行后得到名为new.apk 的合成版本应用,我在ubuntu下进行校验可以看出他们是一样的。

下面我们在手机端合成看看,将根目录下的bspatch(此为手机端运行的)、iReader1.6.2.0(v35).apk 和ireader.patch ,通过adb push到你有权限操作的目录,最好是在/sdcard/下,然后设置bspatch的执行权限,重复操作上述命令,可以合成新版本的apk,稍后安装查看验证版本即可,不详述。

扩展阅读

使用bsdiff 进行差分升级,还并不是最优的方式,google在它的Chromium项目中,对这个差分算法进行了优化,优化后的版本叫做小胡瓜Courgette,据说性能优化了很多不是一个数量级了,官方的一个例子:
Full update       10,385,920

bsdiff update     704,512

Courgette update      78,848
       大牛们可以去研究下。
      最近有些小忙,稍后有时间会对增量升级进行封装下,将合成的代码弄成一个lib库,供java调用。有兴趣的童鞋可以自己操作一下~~~

By 何明桂(http://blog.csdn.net/hmg25) 转载请注明出处   原装正版,盗版必究 ^_^

补充:

很多人不知道怎么进行封装为lib,其实这个和一般的android的C库是一样的,不明白的看看jni相关的知识,原来的测试工程已经找不到了,下边我给出个简单的例子,抛砖引玉,给大家参考下。

[java] view plaincopyprint?

  1. #include <stdio.h>
  2. #include "com_hmg25_newstart_BSpatch.h"
  3. #include <bzlib.h>
  4. #include <stdlib.h>
  5. #include <stdio.h>
  6. #include <string.h>
  7. #include <err.h>
  8. #include <unistd.h>
  9. #include <fcntl.h>
  10. #include <android/log.h>
  11. static off_t offtin(u_char *buf)
  12. {
  13. off_t y;
  14. y=buf[7]&0x7F;
  15. y=y*256;y+=buf[6];
  16. y=y*256;y+=buf[5];
  17. y=y*256;y+=buf[4];
  18. y=y*256;y+=buf[3];
  19. y=y*256;y+=buf[2];
  20. y=y*256;y+=buf[1];
  21. y=y*256;y+=buf[0];
  22. if(buf[7]&0x80) y=-y;
  23. return y;
  24. }
  25. int applypatch(int argc,char * argv[])
  26. {
  27. FILE * f, * cpf, * dpf, * epf;
  28. BZFILE * cpfbz2, * dpfbz2, * epfbz2;
  29. int cbz2err, dbz2err, ebz2err;
  30. int fd;
  31. ssize_t oldsize,newsize;
  32. ssize_t bzctrllen,bzdatalen;
  33. u_char header[32],buf[8];
  34. u_char *old, *new;
  35. off_t oldpos,newpos;
  36. off_t ctrl[3];
  37. off_t lenread;
  38. off_t i;
  39. if(argc!=4) errx(1,"usage: %s oldfile newfile patchfile\n",argv[0]);
  40. /* Open patch file */
  41. if ((f = fopen(argv[3], "r")) == NULL)
  42. err(1, "fopen(%s)", argv[3]);
  43. /*
  44. File format:
  45. 0   8   "BSDIFF40"
  46. 8   8   X
  47. 16  8   Y
  48. 24  8   sizeof(newfile)
  49. 32  X   bzip2(control block)
  50. 32+X    Y   bzip2(diff block)
  51. 32+X+Y  ??? bzip2(extra block)
  52. with control block a set of triples (x,y,z) meaning "add x bytes
  53. from oldfile to x bytes from the diff block; copy y bytes from the
  54. extra block; seek forwards in oldfile by z bytes".
  55. */
  56. /* Read header */
  57. if (fread(header, 1, 32, f) < 32) {
  58. if (feof(f))
  59. errx(1, "Corrupt patch\n");
  60. err(1, "fread(%s)", argv[3]);
  61. }
  62. /* Check for appropriate magic */
  63. if (memcmp(header, "BSDIFF40", 8) != 0)
  64. errx(1, "Corrupt patch\n");
  65. /* Read lengths from header */
  66. bzctrllen=offtin(header+8);
  67. bzdatalen=offtin(header+16);
  68. newsize=offtin(header+24);
  69. if((bzctrllen<0) || (bzdatalen<0) || (newsize<0))
  70. errx(1,"Corrupt patch\n");
  71. /* Close patch file and re-open it via libbzip2 at the right places */
  72. if (fclose(f))
  73. err(1, "fclose(%s)", argv[3]);
  74. if ((cpf = fopen(argv[3], "r")) == NULL)
  75. err(1, "fopen(%s)", argv[3]);
  76. if (fseeko(cpf, 32, SEEK_SET))
  77. err(1, "fseeko(%s, %lld)", argv[3],
  78. (long long)32);
  79. if ((cpfbz2 = BZ2_bzReadOpen(&cbz2err, cpf, 0, 0, NULL, 0)) == NULL)
  80. errx(1, "BZ2_bzReadOpen, bz2err = %d", cbz2err);
  81. if ((dpf = fopen(argv[3], "r")) == NULL)
  82. err(1, "fopen(%s)", argv[3]);
  83. if (fseeko(dpf, 32 + bzctrllen, SEEK_SET))
  84. err(1, "fseeko(%s, %lld)", argv[3],
  85. (long long)(32 + bzctrllen));
  86. if ((dpfbz2 = BZ2_bzReadOpen(&dbz2err, dpf, 0, 0, NULL, 0)) == NULL)
  87. errx(1, "BZ2_bzReadOpen, bz2err = %d", dbz2err);
  88. if ((epf = fopen(argv[3], "r")) == NULL)
  89. err(1, "fopen(%s)", argv[3]);
  90. if (fseeko(epf, 32 + bzctrllen + bzdatalen, SEEK_SET))
  91. err(1, "fseeko(%s, %lld)", argv[3],
  92. (long long)(32 + bzctrllen + bzdatalen));
  93. if ((epfbz2 = BZ2_bzReadOpen(&ebz2err, epf, 0, 0, NULL, 0)) == NULL)
  94. errx(1, "BZ2_bzReadOpen, bz2err = %d", ebz2err);
  95. if(((fd=open(argv[1],O_RDONLY,0))<0) ||
  96. ((oldsize=lseek(fd,0,SEEK_END))==-1) ||
  97. ((old=malloc(oldsize+1))==NULL) ||
  98. (lseek(fd,0,SEEK_SET)!=0) ||
  99. (read(fd,old,oldsize)!=oldsize) ||
  100. (close(fd)==-1)) err(1,"%s",argv[1]);
  101. if((new=malloc(newsize+1))==NULL) err(1,NULL);
  102. oldpos=0;newpos=0;
  103. while(newpos<newsize) {
  104. /* Read control data */
  105. for(i=0;i<=2;i++) {
  106. lenread = BZ2_bzRead(&cbz2err, cpfbz2, buf, 8);
  107. if ((lenread < 8) || ((cbz2err != BZ_OK) &&
  108. (cbz2err != BZ_STREAM_END)))
  109. errx(1, "Corrupt patch\n");
  110. ctrl[i]=offtin(buf);
  111. };
  112. /* Sanity-check */
  113. if(newpos+ctrl[0]>newsize)
  114. errx(1,"Corrupt patch\n");
  115. /* Read diff string */
  116. lenread = BZ2_bzRead(&dbz2err, dpfbz2, new + newpos, ctrl[0]);
  117. if ((lenread < ctrl[0]) ||
  118. ((dbz2err != BZ_OK) && (dbz2err != BZ_STREAM_END)))
  119. errx(1, "Corrupt patch\n");
  120. /* Add old data to diff string */
  121. for(i=0;i<ctrl[0];i++)
  122. if((oldpos+i>=0) && (oldpos+i<oldsize))
  123. new[newpos+i]+=old[oldpos+i];
  124. /* Adjust pointers */
  125. newpos+=ctrl[0];
  126. oldpos+=ctrl[0];
  127. /* Sanity-check */
  128. if(newpos+ctrl[1]>newsize)
  129. errx(1,"Corrupt patch\n");
  130. /* Read extra string */
  131. lenread = BZ2_bzRead(&ebz2err, epfbz2, new + newpos, ctrl[1]);
  132. if ((lenread < ctrl[1]) ||
  133. ((ebz2err != BZ_OK) && (ebz2err != BZ_STREAM_END)))
  134. errx(1, "Corrupt patch\n");
  135. /* Adjust pointers */
  136. newpos+=ctrl[1];
  137. oldpos+=ctrl[2];
  138. };
  139. /* Clean up the bzip2 reads */
  140. BZ2_bzReadClose(&cbz2err, cpfbz2);
  141. BZ2_bzReadClose(&dbz2err, dpfbz2);
  142. BZ2_bzReadClose(&ebz2err, epfbz2);
  143. if (fclose(cpf) || fclose(dpf) || fclose(epf))
  144. err(1, "fclose(%s)", argv[3]);
  145. /* Write the new file */
  146. if(((fd=open(argv[2],O_CREAT|O_TRUNC|O_WRONLY,0666))<0) ||
  147. (write(fd,new,newsize)!=newsize) || (close(fd)==-1))
  148. err(1,"%s",argv[2]);
  149. free(new);
  150. free(old);
  151. return 0;
  152. }
  153. JNIEXPORT jint JNICALL Java_com_hmg25_newstart_BSpatch_applyPatch(JNIEnv *env,
  154. jobject obj, jstring old, jstring new , jstring patch){
  155. int argc=4;
  156. char * argv[argc];
  157. argv[0]="bspatch";
  158. argv[1]=(*env)->GetStringUTFChars(env,old, 0);
  159. argv[2]=(*env)->GetStringUTFChars(env,new, 0);
  160. argv[3]=(*env)->GetStringUTFChars(env,patch, 0);
  161. int ret=applypatch(argc, argv);
  162. (*env)->ReleaseStringUTFChars(env,old,argv[1]);
  163. (*env)->ReleaseStringUTFChars(env,new,argv[2]);
  164. (*env)->ReleaseStringUTFChars(env,patch,argv[3]);
  165. return ret;
  166. }

Android.mk:

[java] view plaincopyprint?

  1. LOCAL_PATH:= $(call my-dir)
  2. include $(CLEAR_VARS)
  3. # This is the target being built.
  4. LOCAL_MODULE:= libbspatch
  5. # All of the source files that we will compile.
  6. LOCAL_SRC_FILES:= \
  7. com_hmg25_newstart_BSpatch.c
  8. # No static libraries.
  9. LOCAL_STATIC_LIBRARIES := \
  10. libbz
  11. # Also need the JNI headers.
  12. LOCAL_C_INCLUDES += \
  13. $(JNI_H_INCLUDE) external/bzip2
  14. # No special compiler flags.
  15. LOCAL_CFLAGS +=
  16. include $(BUILD_SHARED_LIBRARY)

----摘自何明桂(http://blog.csdn.net/hmg25

时间: 2024-10-07 06:00:10

【Android-功能】Android应用增量更新的相关文章

Android应用市场省流量更新(增量升级)原理解析

一.前言 最近在看热修复相关的框架,之前我们已经看过了阿里的Dexposed和AndFix这两个框架了,不了解的同学可以点击这里进行查看:Dexposed框架原理解析 和 AndFix热修复框架原理解析,然后还有最近很火的一个是腾讯的Tinker热修复框架,再看他的原理实现的时候,发现了他使用到了开源的文件差分工具bsdiff/bspatch,所以就单独用这篇文章来详细介绍一下这个工具,因为这个工具有一个很大的用途就是增量更新,也就是我们看到现在大部分的应用市场推出的省流量更新应用的效果: 看到

转Android开发之增量更新

一.使用场景 apk升级,节省服务器和用户的流量 二.原理 自从 Android 4.1 开始, Google Play 引入了应用程序的增量更新功能,App使用该升级方式,可节省约2/3的流量. 现在国内主流的应用市场也都支持应用的增量更新了,最常见的应用宝省流量更新. 增量更新的原理,就是将手机上已安装apk与服务器端最新apk进行二进制对比,得到差分包(即两个版本的差异文件),用户更新程序时,只需要下载差分包,并在本地使用差分包与已安装apk,合成新版apk. 例如,当前手机中已安装微博V

android 客户端增量更新

首先我们需要了解什么是增量更新,增量更新通俗点说就是客户端只需要下载新版本与旧版本的差分包,客户端再把差分包与旧版本进行合成得到一个新apk,在安装这个新的apk,这个新的apk其实新版本,实现更新,注意增量更新与热修复的区别. 原理大家可以查看:详情请见这篇文章Android 增量更新完全解析 是增量不是热修复 我们需要了解ndk,jni,以及.so如何生成,下面是我们windows的准备过程 1.下载bsdiff 下载地址:http://www.daemonology.net/bsdiff/

android友盟增量更新

1.增量升级的原理 增量更新的原理就是将本地apk与服务器端最新版本比对,并得到差异包.比如现在的版本是1.1.4,大小是7.2M,新版本是1.1.5.大小是7.3M.我们发现两个版本只有0.1M的差异,这样我们如果采用增量升级生成0.1M左右的差异包,这样用户只需要下载0.1M的差异包进行升级而不需要重新下载7.3M的新版本了. 2.以往增量升级的实现 首先要有服务端来生成差异包,这一步使用bsdiff(二进制差分工具)来生成老版本和新版本的差异包,再提供给应用下载差异包.应用端则是封装bsp

Android Apk增量更新

前言 有关APK更新的技术比较多,例如:增量更新.插件式开发.热修复.RN.静默安装. 下面简单介绍一下: 什么是增量更新?   增量更新就是原有app的基础上只更新发生变化的地方,其余保持原样. 与原来每次更新都要下载完整apk包的做法相比,这样做的好处显而易见:每次变化的地方总是比较少,因此更新包的体积就会小很多. 增量更新的流程   1.APP检测最新版本:把当前版本告诉服务端,服务端进行判断. 如果有新版本,服务端需要对当前版本的APK与最新版本的APK进行一次差分,产生patch差分文

一个简单的数据增量更新策略(Android / MongoDB / Django)

我在做个人APP - CayKANJI - 的时候遇到一个问题: 怎样增量式地把日语汉字数据地从服务器更新到APP端,即每次用户执行更新操作时,只获取版本高于本地缓存的内容. 数据格式 为了能够与mongoDB无缝结合,并省去编写后台代码的麻烦,索性就把汉字数据保存成json文件,上传到服务器后,交给web应用去读取并写入数据库. 汉字文件就是普通的json格式. { "category": "行為ー2", "contents": [ { &qu

开源 Android App 增量更新库 版本升级

开源 Android App 增量更新库 版本升级 经过几天的重构,我将之前写的一个Android 应用增量更新的示例程序重构为了一个开源库,现在已经push 到 GitHub 上,欢迎大家Watch.Star.Fork. 包含以下内容: 服务器端生成差异包的工程:AppUpdate 客户端使用的开源apk合并库:ApkPatchLibrary 引用ApkPatchLibrary,实现增量更新的ApkPatchLibraryDemo 旧版本的微博Android客户端,以及服务端生成的新旧微博差分

最全的增量更新入门 包含linux端和Android

简介 增量更新大量用于 Android各大应用市场.本文想做网络上从服务器到app客户端完整讲解.app用eclipse和android studio 最新版cmark开发ndk 如下图: 以前一直好奇怎么做的直到知道了bsdiff库. 地址附上: bsdiff源码地址和简介 大家可以从简介看到bsdiff是基于bzip2源码(bsdiff和bspatch一个用于生成差异文件补丁,另一个用于差异文件和旧文件合成新文件) 下载地址说明 应用市场原理说明 假设你用的是"XXX市场"点击更新

Android 增量更新实例(Smart App Updates)

目录[-] 官方说明 实现原理 实现 (1)生成差异包 (2)使用旧apk+差异包,在客户端合成新apk 注意事项 demo 自从 Android 4.1 开始,Google引入了应用程序的增量更新. 官方说明 Smart app updates is a new feature of Google Play that introduces a better way of delivering app updates to devices. When developers publish an

android增量更新(打补丁方式)

自从 Android 4.1 开始,Google引入了应用程序的增量更新. 官方说明 Smart app updates is a new feature of Google Play that introduces a better way of delivering app updates to devices. When developers publish an update, Google Play now delivers only the bits that have change