实现应用的增量更新\升级

转载请注明出处: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语言实现的主要代码

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. /** 
  2.  * 生成差分包 
  3.  */  
  4. JNIEXPORT jint JNICALL Java_com_yyh_lib_bsdiff_DiffUtils_genDiff(JNIEnv *env,  
  5.         jclass cls, jstring old, jstring new, jstring patch) {  
  6.     int argc = 4;  
  7.     char * argv[argc];  
  8.     argv[0] = "bsdiff";  
  9.     argv[1] = (char*) ((*env)->GetStringUTFChars(env, old, 0));  
  10.     argv[2] = (char*) ((*env)->GetStringUTFChars(env, new, 0));  
  11.     argv[3] = (char*) ((*env)->GetStringUTFChars(env, patch, 0));  
  12.   
  13.     printf("old apk = %s \n", argv[1]);  
  14.     printf("new apk = %s \n", argv[2]);  
  15.     printf("patch = %s \n", argv[3]);  
  16.   
  17.     int ret = genpatch(argc, argv);  
  18.   
  19.     printf("genDiff result = %d ", ret);  
  20.   
  21.     (*env)->ReleaseStringUTFChars(env, old, argv[1]);  
  22.     (*env)->ReleaseStringUTFChars(env, new, argv[2]);  
  23.     (*env)->ReleaseStringUTFChars(env, patch, argv[3]);  
  24.   
  25.     return ret;  
  26. }  
  27. /** 
  28.  * 差分包合成新的APK 
  29.  */  
  30. JNIEXPORT jint JNICALL Java_com_yyh_lib_bsdiff_PatchUtils_patch  
  31.   (JNIEnv *env, jclass cls,  
  32.             jstring old, jstring new, jstring patch){  
  33.     int argc = 4;  
  34.     char * argv[argc];  
  35.     argv[0] = "bspatch";  
  36.     argv[1] = (char*) ((*env)->GetStringUTFChars(env, old, 0));  
  37.     argv[2] = (char*) ((*env)->GetStringUTFChars(env, new, 0));  
  38.     argv[3] = (char*) ((*env)->GetStringUTFChars(env, patch, 0));  
  39.   
  40.     printf("old apk = %s \n", argv[1]);  
  41.     printf("patch = %s \n", argv[3]);  
  42.     printf("new apk = %s \n", argv[2]);  
  43.   
  44.     int ret = applypatch(argc, argv);  
  45.   
  46.     printf("patch result = %d ", ret);  
  47.   
  48.     (*env)->ReleaseStringUTFChars(env, old, argv[1]);  
  49.     (*env)->ReleaseStringUTFChars(env, new, argv[2]);  
  50.     (*env)->ReleaseStringUTFChars(env, patch, argv[3]);  
  51.     return ret;  
  52. }  

这是在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

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. package com.yyh.lib.bsdiff;  
  2.   
  3. /** 
  4.  * APK Diff工具类 
  5.  *  
  6.  * @author yuyuhang 
  7.  * @date 2016-1-26 下午1:10:18 
  8.  */  
  9. public class DiffUtils {  
  10.   
  11.     static DiffUtils instance;  
  12.   
  13.     public static DiffUtils getInstance() {  
  14.         if (instance == null)  
  15.             instance = new DiffUtils();  
  16.         return instance;  
  17.     }  
  18.   
  19.     static {  
  20.         System.loadLibrary("ApkPatchLibrary");  
  21.     }  
  22.   
  23.     /** 
  24.      * native方法 比较路径为oldPath的apk与newPath的apk之间差异,并生成patch包,存储于patchPath 
  25.      *  
  26.      * 返回:0,说明操作成功 
  27.      *  
  28.      * @param oldApkPath 
  29.      *            示例:/sdcard/old.apk 
  30.      * @param newApkPath 
  31.      *            示例:/sdcard/new.apk 
  32.      * @param patchPath 
  33.      *            示例:/sdcard/xx.patch 
  34.      * @return 
  35.      */  
  36.     public native int genDiff(String oldApkPath, String newApkPath, String patchPath);  
  37. }  


PatchUtils.java

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. package com.yyh.lib.bsdiff;  
  2.   
  3. /** 
  4.  * APK Patch工具类 
  5.  *  
  6.  * @author yuyuhang 
  7.  * @date 2016-1-26 下午1:10:40 
  8.  */  
  9. public class PatchUtils {  
  10.   
  11.     static PatchUtils instance;  
  12.   
  13.     public static PatchUtils getInstance() {  
  14.         if (instance == null)  
  15.             instance = new PatchUtils();  
  16.         return instance;  
  17.     }  
  18.   
  19.     static {  
  20.         System.loadLibrary("ApkPatchLibrary");  
  21.     }  
  22.   
  23.     /** 
  24.      * native方法 使用路径为oldApkPath的apk与路径为patchPath的补丁包,合成新的apk,并存储于newApkPath 
  25.      *  
  26.      * 返回:0,说明操作成功 
  27.      *  
  28.      * @param oldApkPath 
  29.      *            示例:/sdcard/old.apk 
  30.      * @param newApkPath 
  31.      *            示例:/sdcard/new.apk 
  32.      * @param patchPath 
  33.      *            示例:/sdcard/xx.patch 
  34.      * @return 
  35.      */  
  36.     public native int patch(String oldApkPath, String newApkPath, String patchPath);  
  37. }  

MainActivity.java
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. package com.yyh.activity;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.Collections;  
  5. import java.util.Comparator;  
  6.   
  7. import android.app.Activity;  
  8. import android.content.Intent;  
  9. import android.content.pm.PackageInfo;  
  10. import android.content.pm.PackageManager;  
  11. import android.content.pm.ResolveInfo;  
  12. import android.os.AsyncTask;  
  13. import android.os.Bundle;  
  14. import android.os.Environment;  
  15. import android.os.Looper;  
  16. import android.text.TextUtils;  
  17. import android.util.Log;  
  18. import android.view.View;  
  19. import android.widget.Button;  
  20. import android.widget.Toast;  
  21.   
  22. import com.example.bsdifflib.R;  
  23. import com.yyh.lib.bsdiff.DiffUtils;  
  24. import com.yyh.lib.bsdiff.PatchUtils;  
  25. import com.yyh.utils.ApkUtils;  
  26. import com.yyh.utils.SignUtils;  
  27.   
  28. @SuppressWarnings("unchecked")  
  29. public class MainActivity extends Activity {  
  30.   
  31.     Button btnstart;  
  32.     private ArrayList<ResolveInfo> mApps;  
  33.     private PackageManager pm;  
  34.   
  35.     // 成功  
  36.     private static final int WHAT_SUCCESS = 1;  
  37.     // 合成失败  
  38.     private static final int WHAT_FAIL_PATCH = 0;  
  39.   
  40.     @Override  
  41.     protected void onCreate(Bundle savedInstanceState) {  
  42.         super.onCreate(savedInstanceState);  
  43.         setContentView(R.layout.activity_main);  
  44.         pm = getPackageManager();  
  45.         // initApp();  
  46.     }  
  47.   
  48.     public void bsdiff(View view) {  
  49.         new DiffTask().execute();  
  50.     }  
  51.   
  52.     public void bspatch(View view) {  
  53.         new PatchTask().execute();  
  54.     }  
  55.   
  56.     /** 
  57.      * 生成差分包 
  58.      *  
  59.      * @author yuyuhang 
  60.      * @date 2016-1-25 下午12:24:34 
  61.      */  
  62.     private class DiffTask extends AsyncTask<String, Void, Integer> {  
  63.   
  64.         @Override  
  65.         protected void onPreExecute() {  
  66.             super.onPreExecute();  
  67.         }  
  68.   
  69.         @Override  
  70.         protected Integer doInBackground(String... params) {  
  71.             String appDir, newDir, patchDir;  
  72.   
  73.             try {  
  74.                 appDir = Environment.getExternalStorageDirectory().toString() + "/yyh/DaemonProcess-1.apk";  
  75.                 newDir = Environment.getExternalStorageDirectory().toString() + "/yyh/DaemonProcess-2.apk";  
  76.                 patchDir = Environment.getExternalStorageDirectory() + "/yyh/YixinCall.patch";  
  77.   
  78.                 int result = DiffUtils.getInstance().genDiff(appDir, newDir, patchDir);  
  79.                 if (result == 0) {  
  80.                     runOnUiThread(new Runnable() {  
  81.   
  82.                         @Override  
  83.                         public void run() {  
  84.                             Toast.makeText(getApplicationContext(), "差分包已生成", Toast.LENGTH_SHORT).show();  
  85.                         }  
  86.                     });  
  87.                     return WHAT_SUCCESS;  
  88.                 } else {  
  89.                     runOnUiThread(new Runnable() {  
  90.   
  91.                         @Override  
  92.                         public void run() {  
  93.                             Toast.makeText(getApplicationContext(), "差分包生成失败", Toast.LENGTH_SHORT).show();  
  94.                         }  
  95.                     });  
  96.                     return WHAT_FAIL_PATCH;  
  97.                 }  
  98.             } catch (Exception e) {  
  99.                 e.printStackTrace();  
  100.             }  
  101.             return WHAT_FAIL_PATCH;  
  102.         }  
  103.     }  
  104.   
  105.     /** 
  106.      * 差分包合成APK 
  107.      *  
  108.      * @author yuyuhang 
  109.      * @date 2016-1-25 下午12:24:34 
  110.      */  
  111.     private class PatchTask extends AsyncTask<String, Void, Integer> {  
  112.   
  113.         @Override  
  114.         protected void onPreExecute() {  
  115.             super.onPreExecute();  
  116.         }  
  117.   
  118.         @Override  
  119.         protected Integer doInBackground(String... params) {  
  120.             String appDir, newDir, patchDir;  
  121.   
  122.             try {  
  123.                 // 指定包名的程序源文件路径  
  124.                 appDir = Environment.getExternalStorageDirectory().toString() + "/yyh/DaemonProcess-1.apk";  
  125.                 newDir = Environment.getExternalStorageDirectory().toString() + "/yyh/DaemonProcess-3.apk";  
  126.                 patchDir = Environment.getExternalStorageDirectory() + "/yyh/YixinCall.patch";  
  127.   
  128.                 int result = PatchUtils.getInstance().patch(appDir, newDir, patchDir);  
  129.                 if (result == 0) {  
  130.                     runOnUiThread(new Runnable() {  
  131.   
  132.                         @Override  
  133.                         public void run() {  
  134.                             Toast.makeText(getApplicationContext(), "合成APK成功", Toast.LENGTH_SHORT).show();  
  135.                         }  
  136.                     });  
  137.                     return WHAT_SUCCESS;  
  138.                 } else {  
  139.                     runOnUiThread(new Runnable() {  
  140.   
  141.                         @Override  
  142.                         public void run() {  
  143.                             Toast.makeText(getApplicationContext(), "合成APK失败", Toast.LENGTH_SHORT).show();  
  144.                         }  
  145.                     });  
  146.                     return WHAT_FAIL_PATCH;  
  147.                 }  
  148.             } catch (Exception e) {  
  149.                 e.printStackTrace();  
  150.             }  
  151.             return WHAT_FAIL_PATCH;  
  152.         }  
  153.     }  
  154.   
  155.     /** 
  156.      * 初始化app列表 
  157.      */  
  158.     private void initApp() {  
  159.         // 获取android设备的应用列表  
  160.         Intent intent = new Intent(Intent.ACTION_MAIN); // 动作匹配  
  161.         intent.addCategory(Intent.CATEGORY_LAUNCHER); // 类别匹配  
  162.         mApps = (ArrayList<ResolveInfo>) pm.queryIntentActivities(intent, 0);  
  163.         // 排序  
  164.         Collections.sort(mApps, new Comparator<ResolveInfo>() {  
  165.   
  166.             @Override  
  167.             public int compare(ResolveInfo a, ResolveInfo b) {  
  168.                 // 排序规则  
  169.                 PackageManager pm = getPackageManager();  
  170.                 return String.CASE_INSENSITIVE_ORDER.compare(a.loadLabel(pm).toString(), b.loadLabel(pm).toString()); // 忽略大小写  
  171.             }  
  172.         });  
  173.         for (ResolveInfo ri : mApps) {  
  174.             Log.i("test", ri.activityInfo.packageName);  
  175.         }  
  176.     }  
  177. }  

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

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. Intent intent = new Intent(Intent.ACTION_VIEW);    
  2. intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");    
  3. startActivity(intent);    
或者如果需要静默安装的话,可以参考我的另一篇博客:Android 无需root实现apk的静默安装

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

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. package com.yyh.utils;  
  2.   
  3. import android.content.Context;  
  4. import android.content.Intent;  
  5. import android.content.pm.ApplicationInfo;  
  6. import android.content.pm.PackageInfo;  
  7. import android.content.pm.PackageManager;  
  8. import android.content.pm.PackageManager.NameNotFoundException;  
  9. import android.net.Uri;  
  10. import android.text.TextUtils;  
  11.   
  12. import java.util.Iterator;  
  13. import java.util.List;  
  14.   
  15. /** 
  16.  * Apk工具类 
  17.  *  
  18.  * @author yuyuhang 
  19.  * @date 2016-1-25 下午12:07:09 
  20.  */  
  21. public class ApkUtils {  
  22.   
  23.     /** 
  24.      * 获取已安装apk的PackageInfo 
  25.      *  
  26.      * @param context 
  27.      * @param packageName 
  28.      * @return 
  29.      */  
  30.     public static PackageInfo getInstalledApkPackageInfo(Context context, String packageName) {  
  31.         PackageManager pm = context.getPackageManager();  
  32.         List<PackageInfo> apps = pm.getInstalledPackages(PackageManager.GET_SIGNATURES);  
  33.   
  34.         Iterator<PackageInfo> it = apps.iterator();  
  35.         while (it.hasNext()) {  
  36.             PackageInfo packageinfo = it.next();  
  37.             String thisName = packageinfo.packageName;  
  38.             if (thisName.equals(packageName)) {  
  39.                 return packageinfo;  
  40.             }  
  41.         }  
  42.   
  43.         return null;  
  44.     }  
  45.   
  46.     /** 
  47.      * 判断apk是否已安装 
  48.      *  
  49.      * @param context 
  50.      * @param packageName 
  51.      * @return 
  52.      */  
  53.     public static boolean isInstalled(Context context, String packageName) {  
  54.         PackageManager pm = context.getPackageManager();  
  55.         boolean installed = false;  
  56.         try {  
  57.             pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES);  
  58.             installed = true;  
  59.         } catch (Exception e) {  
  60.             e.printStackTrace();  
  61.         }  
  62.   
  63.         return installed;  
  64.     }  
  65.   
  66.     /** 
  67.      * 获取已安装Apk文件的源Apk文件 
  68.      *  
  69.      * @param context 
  70.      * @param packageName 
  71.      * @return 
  72.      */  
  73.     public static String getSourceApkPath(Context context, String packageName) {  
  74.         if (TextUtils.isEmpty(packageName))  
  75.             return null;  
  76.   
  77.         try {  
  78.             ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(packageName, 0);  
  79.             return appInfo.sourceDir;  
  80.         } catch (NameNotFoundException e) {  
  81.             e.printStackTrace();  
  82.         }  
  83.   
  84.         return null;  
  85.     }  
  86.   
  87.     /** 
  88.      * 安装Apk 
  89.      *  
  90.      * @param context 
  91.      * @param apkPath 
  92.      */  
  93.     public static void installApk(Context context, String apkPath) {  
  94.   
  95.         Intent intent = new Intent(Intent.ACTION_VIEW);  
  96.         intent.setDataAndType(Uri.parse("file://" + apkPath), "application/vnd.android.package-archive");  
  97.   
  98.         context.startActivity(intent);  
  99.     }  
  100. }  

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

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

Demo源码下载地址:Android实现应用增量更新 源码

没有更多推荐了,返回首页