出处:http://blog.csdn.net/yyh352091626/article/details/50579859
增量升级的背景
虽然很多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;
- }
- 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;
-
- public class DiffUtils {
-
- static DiffUtils instance;
-
- public static DiffUtils getInstance() {
- if (instance == null)
- instance = new DiffUtils();
- return instance;
- }
-
- static {
- System.loadLibrary("ApkPatchLibrary");
- }
-
-
- public native int genDiff(String oldApkPath, String newApkPath, String patchPath);
- }
PatchUtils.java
- package com.yyh.lib.bsdiff;
-
- public class PatchUtils {
-
- static PatchUtils instance;
-
- public static PatchUtils getInstance() {
- if (instance == null)
- instance = new PatchUtils();
- return instance;
- }
-
- static {
- System.loadLibrary("ApkPatchLibrary");
- }
-
-
- 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<ResolveInfo> mApps;
- private PackageManager pm;
-
-
- private static final int WHAT_SUCCESS = 1;
-
- private static final int WHAT_FAIL_PATCH = 0;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- pm = getPackageManager();
-
- }
-
- public void bsdiff(View view) {
- new DiffTask().execute();
- }
-
- public void bspatch(View view) {
- new PatchTask().execute();
- }
-
-
- private class DiffTask extends AsyncTask<String, Void, Integer> {
-
- @Override
- protected void onPreExecute() {
- super.onPreExecute();
- }
-
- @Override
- protected 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() {
-
- @Override
- public void run() {
- Toast.makeText(getApplicationContext(), "差分包已生成", Toast.LENGTH_SHORT).show();
- }
- });
- return WHAT_SUCCESS;
- } else {
- runOnUiThread(new Runnable() {
-
- @Override
- public void run() {
- Toast.makeText(getApplicationContext(), "差分包生成失败", Toast.LENGTH_SHORT).show();
- }
- });
- return WHAT_FAIL_PATCH;
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- return WHAT_FAIL_PATCH;
- }
- }
-
-
- private class PatchTask extends AsyncTask<String, Void, Integer> {
-
- @Override
- protected void onPreExecute() {
- super.onPreExecute();
- }
-
- @Override
- protected 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() {
-
- @Override
- public void run() {
- Toast.makeText(getApplicationContext(), "合成APK成功", Toast.LENGTH_SHORT).show();
- }
- });
- return WHAT_SUCCESS;
- } else {
- runOnUiThread(new Runnable() {
-
- @Override
- public void run() {
- Toast.makeText(getApplicationContext(), "合成APK失败", Toast.LENGTH_SHORT).show();
- }
- });
- return WHAT_FAIL_PATCH;
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- return WHAT_FAIL_PATCH;
- }
- }
-
-
- private void initApp() {
-
- Intent intent = new Intent(Intent.ACTION_MAIN);
- intent.addCategory(Intent.CATEGORY_LAUNCHER);
- mApps = (ArrayList<ResolveInfo>) pm.queryIntentActivities(intent, 0);
-
- Collections.sort(mApps, new Comparator<ResolveInfo>() {
-
- @Override
- public 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;
-
- public class ApkUtils {
-
-
- public static PackageInfo getInstalledApkPackageInfo(Context context, String packageName) {
- PackageManager pm = context.getPackageManager();
- List<PackageInfo> apps = pm.getInstalledPackages(PackageManager.GET_SIGNATURES);
-
- Iterator<PackageInfo> it = apps.iterator();
- while (it.hasNext()) {
- PackageInfo packageinfo = it.next();
- String thisName = packageinfo.packageName;
- if (thisName.equals(packageName)) {
- return packageinfo;
- }
- }
-
- return null;
- }
-
-
- 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;
- }
-
-
- 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;
- }
-
-
- 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进行合并。
Demo源码下载地址:Android实现应用增量更新 源码