转载请注明出处:http://blog.csdn.net/yyh352091626/article/details/50579859


GitHub更新:https://github.com/smuyyh/IncrementallyUpdate


增量升级的背景

虽然很多App的版本更新并不频繁,但是一个App基本上也有几兆到几十兆不等,在没有Wifi的条件下,更新App是非常耗流量的。说到这个就必须得吐槽一下三大网络运营商,4G网络是变快了,但是流量确没有多,流量仍然不够用,治标不治本,并没什么卵用。

随着各类App版本的不断更新和升级,App体积也逐渐变大,用户升级成了一个比较棘手的问题,Google很快就意识到了这一点,在IO大会上提出了增量升级,国内诸如小米应用商店也实现了应用的增量升级,减少用户流量的损耗。

增量更新原理

增量更新的原理也很简单,就是将手机上已安装的旧版本apk与服务器端新版本apk进行二进制对比,并得到差分包(patch),用户在升级更新应用时,只需要下载差分包,然后在本地使用差分包与旧版的apk合成新版apk,然后进行安装。这个原理就很想微软更新漏洞打补丁一样,其实都是一个道理。差分包文件的大小,那就远比APK小得多了,这样也便于用户进行应用升级。旧版的APK可以在/data/app/%packagename%底下找到。

差分包的生成和新的APK的合成,需要用到NDK环境,没接触过的那就先学一下,当然,我后面会提供编译好的so库,直接放倒libs/armeabi下调用也是可以的。制作差分包的工具为bsdiff,这是一个非常牛的二进制查分工具,bsdiff源代码在android的源码目录下 \external\bsdiff这边也可以找到。另外还需要bzlib来进行打包。在安全性方面,补丁和新旧版APK最好都要进行MD5验证,以免被篡改,对此我暂不进行叙述。

增量更新存在的不足

1、增量升级是以两个应用版本之间的差异来生成补丁的,但是我们无法保证用户每次的及时升级到最新,也就是在更新前,新版和旧版只差一个版本,所以必须对你所发布的每一个版本都和最新的版本作差分,以便使所有版本的用户都可以差分升级,这样相对就比较繁琐了。解决方法也有,可以通过Shell脚本来实现批量生成。

2.增量升级能成功的前提是,从手机端能够获得旧版APK,并且与服务端的APK签名是一样的,所以像那些破解的APP就无法实现更新。前面也提到了,为了安全性,防止补丁合成错误,最好在补丁合成前对旧版本的apk进行sha1或者MD5校验,保证基础包的一致性,这样才能顺利的实现增量升级。

C语言实现的主要代码

/** * 生成差分包 */JNIEXPORT jint JNICALL Java_com_yyh_lib_bsdiff_DiffUtils_genDiff(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;}/** * 差分包合成新的APK */JNIEXPORT jint JNICALL Java_com_yyh_lib_bsdiff_PatchUtils_patch  (JNIEnv *env, jclass cls,jstring old, jstring new, jstring patch){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]);int ret = applypatch(argc, argv);printf("patch result = %d ", ret);(*env)->ReleaseStringUTFChars(env, old, argv[1]);(*env)->ReleaseStringUTFChars(env, new, argv[2]);(*env)->ReleaseStringUTFChars(env, patch, argv[3]);return ret;}

这是在jni上实现差分包的生成与合并,当然,差分包一般是在服务端生成的,在服务端,那就需要把com_yyh_lib_bsdiff_DiffUtils.c以及bzip2库,编译成动态链接库,供Java调用。windows下生成的动态链接库为.dll文件,Unix-like下生成的为.so文件,因为Android是基于Linux内核的,所以也是.so文件,Mac OSX下生成的动态链接库为.dylib文件。

所以后面需要DaemonProcess-1.apk(旧版) DaemonProcess-2.apk(新版)这两个APK,我放在assets文件夹下,来生成差分包。这两个文件就自行拷贝到SD卡/yyh文件夹下,或者按需修改。

Java代码主要实现部分

DiffUtils.java

package com.yyh.lib.bsdiff;/** * APK Diff工具类 *  * @author yuyuhang * @date 2016-1-26 下午1:10:18 */public class DiffUtils {static DiffUtils instance;public static DiffUtils getInstance() {if (instance == null)instance = new DiffUtils();return instance;}static {System.loadLibrary("ApkPatchLibrary");}/** * native方法 比较路径为oldPath的apk与newPath的apk之间差异,并生成patch包,存储于patchPath *  * 返回:0,说明操作成功 *  * @param oldApkPath *            示例:/sdcard/old.apk * @param newApkPath *            示例:/sdcard/new.apk * @param patchPath *            示例:/sdcard/xx.patch * @return */public native int genDiff(String oldApkPath, String newApkPath, String patchPath);}


PatchUtils.java

package com.yyh.lib.bsdiff;/** * APK Patch工具类 *  * @author yuyuhang * @date 2016-1-26 下午1:10:40 */public class PatchUtils {static PatchUtils instance;public static PatchUtils getInstance() {if (instance == null)instance = new PatchUtils();return instance;}static {System.loadLibrary("ApkPatchLibrary");}/** * native方法 使用路径为oldApkPath的apk与路径为patchPath的补丁包,合成新的apk,并存储于newApkPath *  * 返回:0,说明操作成功 *  * @param oldApkPath *            示例:/sdcard/old.apk * @param newApkPath *            示例:/sdcard/new.apk * @param patchPath *            示例:/sdcard/xx.patch * @return */public native int patch(String oldApkPath, String newApkPath, String patchPath);}

MainActivity.java
package com.yyh.activity;import java.util.ArrayList;import java.util.Collections;import java.util.Comparator;import android.app.Activity;import android.content.Intent;import android.content.pm.PackageInfo;import android.content.pm.PackageManager;import android.content.pm.ResolveInfo;import android.os.AsyncTask;import android.os.Bundle;import android.os.Environment;import android.os.Looper;import android.text.TextUtils;import android.util.Log;import android.view.View;import android.widget.Button;import android.widget.Toast;import com.example.bsdifflib.R;import com.yyh.lib.bsdiff.DiffUtils;import com.yyh.lib.bsdiff.PatchUtils;import com.yyh.utils.ApkUtils;import com.yyh.utils.SignUtils;@SuppressWarnings("unchecked")public class MainActivity extends Activity {Button btnstart;private ArrayList mApps;private PackageManager pm;// 成功private static final int WHAT_SUCCESS = 1;// 合成失败private static final int WHAT_FAIL_PATCH = 0;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);pm = getPackageManager();// initApp();}public void bsdiff(View view) {new DiffTask().execute();}public void bspatch(View view) {new PatchTask().execute();}/** * 生成差分包 *  * @author yuyuhang * @date 2016-1-25 下午12:24:34 */private class DiffTask extends AsyncTask {@Overrideprotected void onPreExecute() {super.onPreExecute();}@Overrideprotected Integer doInBackground(String... params) {String appDir, newDir, patchDir;try {appDir = Environment.getExternalStorageDirectory().toString() + "/yyh/DaemonProcess-1.apk";newDir = Environment.getExternalStorageDirectory().toString() + "/yyh/DaemonProcess-2.apk";patchDir = Environment.getExternalStorageDirectory() + "/yyh/YixinCall.patch";int result = DiffUtils.getInstance().genDiff(appDir, newDir, patchDir);if (result == 0) {runOnUiThread(new Runnable() {@Overridepublic void run() {Toast.makeText(getApplicationContext(), "差分包已生成", Toast.LENGTH_SHORT).show();}});return WHAT_SUCCESS;} else {runOnUiThread(new Runnable() {@Overridepublic void run() {Toast.makeText(getApplicationContext(), "差分包生成失败", Toast.LENGTH_SHORT).show();}});return WHAT_FAIL_PATCH;}} catch (Exception e) {e.printStackTrace();}return WHAT_FAIL_PATCH;}}/** * 差分包合成APK *  * @author yuyuhang * @date 2016-1-25 下午12:24:34 */private class PatchTask extends AsyncTask {@Overrideprotected void onPreExecute() {super.onPreExecute();}@Overrideprotected Integer doInBackground(String... params) {String appDir, newDir, patchDir;try {// 指定包名的程序源文件路径appDir = Environment.getExternalStorageDirectory().toString() + "/yyh/DaemonProcess-1.apk";newDir = Environment.getExternalStorageDirectory().toString() + "/yyh/DaemonProcess-3.apk";patchDir = Environment.getExternalStorageDirectory() + "/yyh/YixinCall.patch";int result = PatchUtils.getInstance().patch(appDir, newDir, patchDir);if (result == 0) {runOnUiThread(new Runnable() {@Overridepublic void run() {Toast.makeText(getApplicationContext(), "合成APK成功", Toast.LENGTH_SHORT).show();}});return WHAT_SUCCESS;} else {runOnUiThread(new Runnable() {@Overridepublic void run() {Toast.makeText(getApplicationContext(), "合成APK失败", Toast.LENGTH_SHORT).show();}});return WHAT_FAIL_PATCH;}} catch (Exception e) {e.printStackTrace();}return WHAT_FAIL_PATCH;}}/** * 初始化app列表 */private void initApp() {// 获取android设备的应用列表Intent intent = new Intent(Intent.ACTION_MAIN); // 动作匹配intent.addCategory(Intent.CATEGORY_LAUNCHER); // 类别匹配mApps = (ArrayList) pm.queryIntentActivities(intent, 0);// 排序Collections.sort(mApps, new Comparator() {@Overridepublic int compare(ResolveInfo a, ResolveInfo b) {// 排序规则PackageManager pm = getPackageManager();return String.CASE_INSENSITIVE_ORDER.compare(a.loadLabel(pm).toString(), b.loadLabel(pm).toString()); // 忽略大小写}});for (ResolveInfo ri : mApps) {Log.i("test", ri.activityInfo.packageName);}}}

这样就实现了差分包的生成与新的APK的合成,那么我们得到新的APK之后,就调用以下代码进行安装。

Intent intent = new Intent(Intent.ACTION_VIEW);  intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");  startActivity(intent);  
或者如果需要静默安装的话,可以参考我的另一篇博客: Android 无需root实现apk的静默安装

对于应用商店来说,App就不仅一个,想要得到所有旧版APK,就可以遍历所有的包名,通过context.getPackageManager().getApplicationInfo(packageName, 0).sourceDir获取。相关代码如下

package com.yyh.utils;import android.content.Context;import android.content.Intent;import android.content.pm.ApplicationInfo;import android.content.pm.PackageInfo;import android.content.pm.PackageManager;import android.content.pm.PackageManager.NameNotFoundException;import android.net.Uri;import android.text.TextUtils;import java.util.Iterator;import java.util.List;/** * Apk工具类 *  * @author yuyuhang * @date 2016-1-25 下午12:07:09 */public class ApkUtils {/** * 获取已安装apk的PackageInfo *  * @param context * @param packageName * @return */public static PackageInfo getInstalledApkPackageInfo(Context context, String packageName) {PackageManager pm = context.getPackageManager();List apps = pm.getInstalledPackages(PackageManager.GET_SIGNATURES);Iterator it = apps.iterator();while (it.hasNext()) {PackageInfo packageinfo = it.next();String thisName = packageinfo.packageName;if (thisName.equals(packageName)) {return packageinfo;}}return null;}/** * 判断apk是否已安装 *  * @param context * @param packageName * @return */public static boolean isInstalled(Context context, String packageName) {PackageManager pm = context.getPackageManager();boolean installed = false;try {pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES);installed = true;} catch (Exception e) {e.printStackTrace();}return installed;}/** * 获取已安装Apk文件的源Apk文件 *  * @param context * @param packageName * @return */public static String getSourceApkPath(Context context, String packageName) {if (TextUtils.isEmpty(packageName))return null;try {ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(packageName, 0);return appInfo.sourceDir;} catch (NameNotFoundException e) {e.printStackTrace();}return null;}/** * 安装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);}}

之前在实现的过程中,还碰到过一个问题,就是差分包可以生成,但是合成的时候就出错了,最后还是没搞懂是为什么。有解决过类似问题的,还希望多交流一下~~

以上主要就是进行差分包的生成与新的APK的合成,关键技术都实现了,调试了两天,终于把它搞定了。其他扩展的功能,大家自行实现。上效果~点击bsdiff进行差分包生成,然后点击bspatch进行合并。

GitHub源码地址:Android实现应用增量更新


     



更多相关文章

  1. android studio *.jar 与 *.aar 的生成与*.aar导入项目方法
  2. Android(安卓)生成长图并添加水印(一)
  3. Android(安卓)用KSOAP2 调用 webservice 传入参数服务器接受不到
  4. Android(安卓)NDK 开发:CMake 使用
  5. Android中CMake的使用之四调用第三方库的实战
  6. React Native Android(安卓)即时热更新bundle 以及增量更新bundl
  7. Android(安卓)studio JavaDoc的使用
  8. keytool 生成 Android(安卓)SSL 使用的 BKS
  9. Android技术周报_W2_2017年01月15日

随机推荐

  1. 解析SQLServer任意列之间的聚合
  2. 浅析SQLServer中的Scanf与Printf
  3. 浅析被遗忘的SQLServer比较运算符修饰词
  4. 解析SQLServer获取Excel中所有Sheet的方
  5. 解析SQLServer2005的Top功能
  6. 解析关于SQL语句Count的一点细节
  7. 解析sql中得到刚刚插入的数据的id
  8. 使用SQL Server 获取插入记录后的ID(自动
  9. 如何区分SQL数据库中的主键与外键
  10. sqlServer使用ROW_NUMBER时不排序的解决