Android实现应用的增量更新\升级---其一

来源:互联网 发布:php获取对象的属性值 编辑:程序博客网 时间:2024/06/05 17:16

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

[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实现应用增量更新 源码



0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 搬家与生肖相冲怎么办 颈椎生理曲度变直怎么办 整个背部长痘痘怎么办 卧室门对着厨房怎么办 卧室门正对厕所怎么办 进门正对厕所门怎么办 门口对着厕所门怎么办 厨房门比大门高怎么办 鼻子上山根横纹怎么办 墙与床的缝隙怎么办 床边与墙有间隙怎么办 抽了烟头晕恶心怎么办 9个月宝宝口臭怎么办 狗舔了人的伤口怎么办 狗舔了结痂伤口怎么办 狗狗指甲变黑了怎么办 狗狗不肯剪指甲怎么办 厕所门对厨房门怎么办 房间门对着镜子怎么办 门直对着楼梯口怎么办 厨房门对着客厅怎么办 卧室正对着马路怎么办 主卧厕所对着床怎么办 卧室门对着床头怎么办 主卧厕所门对床怎么办 老人晕车怎么办最有效方法 货车油刹不好用怎么办 7岁儿童喉咙有痰怎么办 3岁宝宝喉咙有痰怎么办 冰箱正对厨房门怎么办 买了连廊高层怎么办 想买电玩瑞文怎么办 财位旁边有窗户怎么办 入室门对卧室门怎么办 卧室门对着大门怎么办 床给别人睡过了怎么办 镜子对着书房门怎么办 次卧对着卫生间怎么办 一楼房间太潮湿怎么办 房间里潮湿很重怎么办 儿童眼屎多又黄怎么办