定义
所谓的增量更新,区别于传统的更新方式,将新的apk全部下载下来后安装覆盖掉旧的版本,增量更新只需要下载一个差分包即可,然后把下载的差分包和原来旧的apk进行合成,生成一个新的apk,这样就可以极大的减少升级新版本所需要的流量,下面就通过一个简单的demo来了解一下什么时增量更新。
如果想更深层次的了解增量更新的原理,可以参考下面两个博客
http://blog.csdn.net/hmg25/article/details/8100896
http://blog.csdn.net/tu_bingbing/article/details/8538592
- 首先需要准备生成差分包的工具
下载后解压如图
- 下面就是具体的代码了
有两个别人写好的工具类ApkUtils,PatchUtils
package com.cundong.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 Cundong
* @version 1.1
* @date
*/
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);
}
/**
* 获取版本名称
*
* @param context 上下文
* @return 版本名称
*/
public static String getVersionName(Context context) {
PackageManager pm = context.getPackageManager();
try {
PackageInfo packageInfo = pm.getPackageInfo(context.getPackageName(), 0);
return packageInfo.versionName;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return null;
}
/* 获取版本号
* @param context 上下文
* @return 版本号
*/
public static int getVersionCode(Context context) {
PackageManager pm = context.getPackageManager();
try {
PackageInfo packageInfo = pm.getPackageInfo(context.getPackageName(), 0);
return packageInfo.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 0;
}
}
package com.cundong.utils;
/**
* 类说明: APK Patch工具类
*
* @author Cundong
* @date 2013-9-6
* @version 1.0
*/
public class PatchUtils {
/**
* native方法 使用路径为oldApkPath的apk与路径为patchPath的补丁包,合成新的apk,并存储于newApkPath
*
* 返回:0,说明操作成功
*
* @param oldApkPath 示例:/sdcard/old.apk
* @param newApkPath 示例:/sdcard/new.apk
* @param patchPath 示例:/sdcard/xx.patch
* @return
*/
public static native int patch(String oldApkPath, String newApkPath, String patchPath);
}
注意这两个类一定要放在com.cundong.utils的包中,以为这里用到了一个native方法,使用了一个so库实现合成新的apk
然后将so库拷贝到项目中
- MainActivity的代码,首先是旧的版本
package com.demo.patch;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.cundong.utils.ApkUtils;
import com.cundong.utils.PatchUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("ApkPatchLibrary");//加载so库
}
private final String NEW_APK_PATH = Environment.getExternalStorageDirectory() + File.separator + "patchdemo.apk";
private final String PATCH_PATH = Environment.getExternalStorageDirectory() + File.separator + "patchdemo.patch";
private final String URL = "http://192.168.126.1:8080/patchdemo.patch";
private final int SUCCESS = 5;
private final int FAIL = 6;
FileOutputStream fos = null;
HttpURLConnection conn = null;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case SUCCESS:
//弹出Toast,提示合并成功,安装apk
Toast.makeText(MainActivity.this, "合并成功", Toast.LENGTH_SHORT).show();
ApkUtils.installApk(MainActivity.this, NEW_APK_PATH);
break;
default:
Toast.makeText(MainActivity.this, "合并失败", Toast.LENGTH_SHORT).show();
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = (TextView) findViewById(R.id.tv);
textView.setText("旧的版本" + ApkUtils.getVersionName(this) + " " + ApkUtils.getVersionCode(this));
new Thread(new Runnable() {
@Override
public void run() {
loadApk();
}
}).start();
findViewById(R.id.bt).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
updateApk();
}
});
}
private void updateApk() {
new Thread(new Runnable() {
@Override
public void run() {
//获取已经安装过的旧的apk的路径
String oldApkSource = ApkUtils.getSourceApkPath(MainActivity.this, getPackageName());
//合并旧的apk和补丁成为新的apk
int patchResult = PatchUtils.patch(oldApkSource, NEW_APK_PATH, PATCH_PATH);
int what;
if (patchResult == 0) {
what = SUCCESS;
} else {
what = FAIL;
}
handler.obtainMessage(what).sendToTarget();
}
}).start();
}
private void loadApk() {
try {
File file = new File(PATCH_PATH);
if (file.exists()) {
file.delete();
}
URL url = new URL(URL);
conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(5000);
if(conn.getResponseCode()==HttpURLConnection.HTTP_OK){
InputStream is=conn.getInputStream();
fos=new FileOutputStream(file);
byte[] b=new byte[1024];
int len=0;
while((len=is.read(b))!=-1){ //先读到内存
fos.write(b, 0, len);
}
fos.flush();
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this,"下载成功了",Toast.LENGTH_LONG).show();
}
});
}
} catch (final Exception e) {
e.printStackTrace();
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this,e.toString(),Toast.LENGTH_LONG).show();
}
});
}
}
}
界面就很简单了,一个TextView,一个Button。运行这个工程,然后去build/outputs/apk/文件夹下把生成的apk文件拷贝出来放到之前下载的工具的bsdiff4.3-win32文件夹中,并改名为patchdemo-old.apk
效果图:
- 更改textview的显示内容为
TextView textView = (TextView) findViewById(R.id.tv);
textView.setText("已经更新到最新版本了" + ApkUtils.getVersionName(this) + " " + ApkUtils.getVersionCode(this));
然后去build.gradle中把versioncode和versionname都做相应的更改,运行项目,生成新的apk文件,拷贝到工具bsdiff4.3-win32文件夹中,改名为patchdemo-new.apk,效果图:
打开dos窗口,进入bsdiff4.3-win32路径下,执行命令 bsdiff.exe patchdemo-old.apk patchdemo-new.apk patchdemo.patch 就会生成一个以.patch结尾的文件,如图:
将这个.patch文件放在自己的的服务器上,我这里用的是tomcat把新的apk从手机上卸载,把旧的apk安装上,当提示下载成功后,点击安装,就会执行合成新apk的代码,跳转到安装页面,如图;
点击安装,就会把和合成后新的版本覆盖掉旧的版本,之后打开应用就是最新的版本了,效果图:
一个简单的增量更新的demo就完成了,这里主要的难点在于通过旧的版本和差分包合成新的版本,这一部风代码都是通过调用so库中的函数实现的。这里没有进行版本的判断,以及多个差分包的情况。
github 代码仓库:https://github.com/githubyzd/PathDemo