Android實現應用的增量更新\升級
轉載請註明出處: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下呼叫也是可以的。製作差分包的工具為
增量更新存在的不足
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進行合併。
GitHub原始碼地址:Android實現應用增量更新