• UID7342
  • 登录2016-03-23
  • 粉丝7
  • 关注0
  • 发帖17
  • 主页
  • 金币131枚
sina_KVlrs14557 发布于2016-02-22 11:16
5/2794

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

楼层直达
原文来自:http://blog.csdn.net/yyh352091626/article/details/50579859


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

增量升级的背景
虽然很多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<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();  
        // 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<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;  
        }  
    }  
            
    /** 
     * 差分包合成APK 
     *  
     * @author yuyuhang 
     * @date 2016-1-25 下午12:24:34 
     */  
    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;  
        }  
    }  
            
    /** 
     * 初始化app列表 
     */  
    private void initApp() {  
        // 获取android设备的应用列表  
        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;
     
/**
 * 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<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;
    }
     
    /**
     * 判断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进行合并。

Demo源码下载:
[sina_KVlrs14557于2016-02-26 08:38编辑了帖子]
        附件名称/大小 下载 更新 附加说明
BsdiffDemo.zip (1946KB)  5 2016-02-22 11:40

0人打赏
  • UID7342
  • 登录2016-03-23
  • 粉丝7
  • 关注0
  • 发帖17
  • 主页
  • 金币131枚
sina_KVlrs14557 发布于2016-02-22 11:19
沙发F
为什么发布了内容显示不出来...
  • UID328
  • 登录2016-12-14
  • 粉丝8
  • 关注4
  • 发帖53
  • 主页
  • 金币381枚
社区居民
a153013144 发布于2016-02-22 11:19
板凳F
sina_KVlrs14557:为什么发布了内容显示不出来...回到原帖
太帅。
不会爬 ,就不要想着去走!
  • UID7342
  • 登录2016-03-23
  • 粉丝7
  • 关注0
  • 发帖17
  • 主页
  • 金币131枚
sina_KVlrs14557 发布于2016-02-22 11:21
地板F
a153013144:太帅。回到原帖
说得好...
  • UID15
  • 登录2016-08-05
  • 粉丝48
  • 关注40
  • 发帖447
  • 主页
  • 金币1836枚
社区居民
喜欢达人
原创写手
janking 发布于2016-02-22 11:32
4楼F
sina_KVlrs14557:为什么发布了内容显示不出来...回到原帖
还不能显示吗?
  • UID80
  • 登录2017-01-12
  • 粉丝58
  • 关注21
  • 发帖1115
  • 主页
  • 金币6302枚
社区居民
忠实会员
原创写手
潇潇宇 发布于2016-02-22 11:43
5楼F
sina_KVlrs14557:为什么发布了内容显示不出来...回到原帖
已经写好的文章,建议用Markdown或者html格式发布,编辑器之间的兼容问题引起的
您需要登录后才可以回帖
发表回复
极贡献
技术问答
专题荟萃
程序人生
视觉设计
Android开发
iOS开发
编程语言
前端开发
后端开发
服务器架构
软件测试
运维方案
创业路上



最热文章墙

  • 57496/337   【精品推荐】200多种Android动画效果的强悍框架,太全了,不看这个,再有动画的问题,不理你了^@^

  • 30909/139   省时省力的Android组件群来了,非常棒的原型参考

  • 30559/187   情人节福利,程序员表白的正确姿势:改几行代码就变成自己的表白了

  • 27200/216   【精品推荐】Android版产品级的音乐播放器源码,功能太强大了,最好的产品原型有木有?

  • 23892/70   原创表白APP,以程序员的姿势备战新年后的7夕,持续完善中!

  • 22626/2   超全!整理常用的iOS第三方资源

  • 21893/0   Python爬虫:常用浏览器的useragent

  • 21575/137   2016抢红包软件及源码

  • 19577/29   麻省理工的一帮疯子,真的实现了随意操控万物!(绝对黑科技)

  • 19413/25   Android工程师面试题大全

  • 18692/27   2016程序员跳槽全攻略

  • 18588/9   GitHub上排名前50的iOS项目:总有一款你用得着

  • 18166/20   码魂:程序员的牛B漫画

  • 16621/3   吐槽那些程序员的搞笑牛逼注释

  • 15671/146   Android版类似UC浏览器:非常赞,产品级的源码

  • 15443/1   iOS 动画总结

  • 14801/41   一个绚丽的loading动效分析与实现!

  • 14392/10   女程序员的梦,众网友的神回复

  • 14215/11   年会上现场review代码是怎么样的体验!

  • 14182/83   Android小而全的博客源码:非常适合全面掌握开发技巧

  • 14166/73   【持续更新中】Android福利贴(二):资料源码大放送

  • 14102/44   惊艳的App引导页:背景图片切换加各个页面动画效果

  • 13793/5   新一代Android渠道打包工具:1000个渠道包只需要5秒

  • 13605/22   个人收集的Android 各类功能源代码

  • 13479/10   2016年最全的Android面试考题+答案 精编版

  • 12910/19   珍藏多年的素材,灵感搜寻网站

  • 12780/53   基于瀑布流的美女图片浏览App,有注释的源代码

  • 12428/15   基于Android支付宝支付设计和开发方案

  • 12405/17   用JavaScript 来开发iOS和Android 原生应用:React Native开源框架中文版来啦

  • 12114/74   仿京东商城客户端Android最新版,不错的原型和学习资料

  • 12103/20   Android福利第三波【Android电子书】

  • 12052/17   什么是真正的黑客:收获12200+Stars,人气远超微软开源VS

  • 11554/18   65条最常用正则表达式,你要的都在这里了

  • 11498/7   用程序员的姿势抢过年的火车票

  • 11464/94   Android带弹幕的视频播放器源码,来自大名鼎鼎的Bilibili弹幕网站

  • 11459/70   【精品推荐】类似360安全卫士安Android源码:非常赞的产品原型

  • 11391/7   一张图搞定iOS学习路线,非常全面

  • 11283/11   有木有这样一张酷图帮你集齐所有git命令超实用

  • 10984/0   iOS中文版资源库,非常全

  • 10919/10   成为Java顶尖程序员 ,看这11本书就够了

  • 10507/18   一张图搞定Android学习路线,非常全面

  • 10482/10   微信支付终于成功了(安卓,iOS),在此分享

  • 10060/3   基于Node.js的强大爬虫,能直接发布抓取的文章哦

  • 10026/29   【持续更新中】Android福利贴(一):资料源码

  • 10008/0   GitHub iOS 库和框架Top100 

  • 9997/44   在线音乐播放器完整版(商用级的源码):非常赞,可听免费高品质专辑

  • 9670/4   46 个非常有用的 PHP 代码片段

  • 9308/9   烧了5亿美金,这家神秘的公司即将颠覆人类未来!

  • 9279/3   即时通信第三方库

  • 9149/8   流媒体视频直播方案

  • 9082/61   【技巧一】搭配Android Studio,如何实现App远程真机debug?

  • 9013/9   B站建开源工作组:APP想支持炫酷弹幕的看过来

  • 8984/2   【精品推荐】高质量PHP代码的50个实用技巧:非常值得收藏

  • 8891/9   中国黑客的隐秘江湖:攻守对立,顶尖高手月入千万美元

  • 8848/18   八个最优秀的Android Studio插件

  • 8420/6   开箱即用!Android四款系统架构工具

  • 8337/3   一张图看清Linux 内核运行原理

  • 8218/3   10款GitHub上最火爆的国产开源项目——可以媲美西半球

  • 8205/10   十大技巧快速提升Android应用开发性能

  • 7868/1   Android性能优化视频,文档以及工具

  • 返回顶部