定義
所謂的增量更新,區別於傳統的更新方式,將新的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