Android應(yīng)用經(jīng)常會(huì)內(nèi)置檢測(cè)版本更新的功能供搀,在有版本更新的時(shí)候隅居,通過(guò)下載更新文件進(jìn)行本地的升級(jí)。本文通過(guò)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的Demo葛虐,來(lái)介紹App的更新升級(jí)方式胎源。提供更新信息的服務(wù)器用到了上一篇文章實(shí)現(xiàn)的服務(wù)器Demo。
App的更新方式主要有兩種:
- 完全更新(Full updates)
- 增量更新(Incremental updates屿脐,也叫差分包升級(jí))
應(yīng)用更新前
更新完成后
應(yīng)用比較簡(jiǎn)單涕蚤,關(guān)鍵是更新的流程,看背景色既可知更新是否成功的诵。
App實(shí)現(xiàn)
文末附Demo完整源碼万栅,這里只大概介紹主要的步驟。
主要的界面就是一列表西疤,列表有2個(gè)選項(xiàng)
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.example.hd.ota.Activity.MainActivity">
<ListView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</ListView>
</RelativeLayout>
列表項(xiàng)初始化烦粒,點(diǎn)擊列表選項(xiàng)做對(duì)應(yīng)的跳轉(zhuǎn)
MainActivity.java
protected void onCreate(Bundle savedInstanceState) {
...
mListView = (ListView)findViewById(R.id.list);
mList = new ArrayList<String>();
mList.add("版本信息");
mList.add("版本更新");
ArrayAdapter adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, mList);
mListView.setAdapter(adapter);
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, final View view, int position, long id) {
Intent intent = null;
switch (position) {
case 0:
intent = InfoActivity.getInfoIntent(getApplicationContext());
break;
case 1:
intent = UpdateActivity.getUpdateIntent(getApplicationContext());
break;
default:
}
if (intent != null) {
startActivity(intent);
}
}
});
}
UpdateActivity 為更新升級(jí)界面,進(jìn)入該界面時(shí)會(huì)自動(dòng)向服務(wù)器進(jìn)行更新信息的請(qǐng)求瘪阁,并將請(qǐng)求的結(jié)果顯示撒遣。
新建 AsyncTask<Void, Void, String> 子類 UpdateCheckTask,用來(lái)做后臺(tái)網(wǎng)絡(luò)請(qǐng)求管跺。
protected String doInBackground(Void... voids) {
HttpURLConnection uRLConnection = null;
InputStream is = null;
BufferedReader buffer = null;
String result = null;
String urlStr = Constants.UPDATE_REQUEST_URL + "?" + Constants.APK_VERSION_NAME + "=" + InfoUtils.getVersionName(this.mContext);
try {
URL url = new URL(urlStr);
uRLConnection = (HttpURLConnection) url.openConnection();
uRLConnection.setRequestMethod("GET");
is = uRLConnection.getInputStream();
buffer = new BufferedReader(new InputStreamReader(is));
StringBuilder strBuilder = new StringBuilder();
String line;
while ((line = buffer.readLine()) != null) {
strBuilder.append(line);
}
result = strBuilder.toString();
} catch (Exception e) {
Log.e(TAG, "http post error");
} finally {
...
}
return result;
}
將獲取到的服務(wù)器返回?cái)?shù)據(jù)做json解析义黎,并返回給UpdateActivity
protected void onPostExecute(String result) {
Log.i(TAG, "onPostExecute()");
UpdateInfo info = parseJson(result);
if (this.mListener != null) {
this.mListener.onSuccess(info);
}
}
parseJson 函數(shù)為UpdateCheckTask中實(shí)現(xiàn)的解析方法,具體見(jiàn)源碼豁跑。
UpdateCheckTask在onCreate中調(diào)用更新請(qǐng)求
new UpdateCheckTask(UpdateActivity.this, this).execute();
檢測(cè)到更新后廉涕,界面出現(xiàn)下載按鈕,點(diǎn)擊按鈕艇拍,下載指定url中的更新文件狐蜕,顯示下載進(jìn)度條
新建 DownloadService 用來(lái)處理下載流程,DownloadService 繼承自 IntentService 類
DownloadService 通過(guò)指定地址將文件下載到本地
@Override
protected void onHandleIntent(Intent intent) {
String urlStr = Constants.OTA_SERVER_IP + intent.getStringExtra(Constants.APK_DOWNLOAD_URL);
String md5 = intent.getStringExtra(Constants.APK_MD5);
boolean isDiff = intent.getBooleanExtra(Constants.APK_DIFF_UPDATE, false);
InputStream in = null;
FileOutputStream out = null;
try {
URL url = new URL(urlStr);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
...
}
下載過(guò)程中將進(jìn)度以消息的方式通知UpdateActivity以更新進(jìn)度條
...
int oldProgress = 0;
Intent sendIntent = new Intent(UpdateActivity.SERVICE_RECEIVER);
while ((byteread = in.read(buffer)) != -1) {
bytesum += byteread;
out.write(buffer, 0, byteread);
int progress = (int) (bytesum * 100L / bytetotal);
// 如果進(jìn)度與之前進(jìn)度相等卸夕,則不更新层释,如果更新太頻繁,否則會(huì)造成界面卡頓
if (progress != oldProgress) {
sendIntent.putExtra(Constants.UPDATE_DOWNLOAD_PROGRESS, progress);
getApplicationContext().sendBroadcast(sendIntent);
}
oldProgress = progress;
}
...
UpdateActivity 監(jiān)聽(tīng)進(jìn)度消息
mReceiver = new ProgressReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(SERVICE_RECEIVER);
registerReceiver(mReceiver, intentFilter);
UpdateActivity 獲取到消息時(shí)更新進(jìn)度條
public class ProgressReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
int progress = intent.getIntExtra(Constants.UPDATE_DOWNLOAD_PROGRESS, 0);
Log.i(TAG, "progress......" + progress);
mProgressBar.setProgress(progress);
if (mProgressBar.getVisibility() != View.VISIBLE) {
mProgressBar.setVisibility(View.VISIBLE);
mDownloadBtn.setVisibility(View.GONE);
}
}
}
下載完成后安裝apk
File apkFile = downloadFile;
installAPk(apkFile);
downloadFile 為下載完成保存到本地的apk文件快集,installAPk 的具體實(shí)現(xiàn)
private void installAPk(File apkFile) {
Intent intent = new Intent(Intent.ACTION_VIEW);
//如果沒(méi)有設(shè)置SDCard寫(xiě)權(quán)限贡羔,或者沒(méi)有sdcard,apk文件保存在內(nèi)存中,需要授予權(quán)限才能安裝
try {
String[] command = {"chmod", "777", apkFile.toString()};
ProcessBuilder builder = new ProcessBuilder(command);
builder.start();
} catch (IOException ignored) {
}
intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
Android本身自帶接口个初,當(dāng)打開(kāi).apk格式的文件時(shí)跳轉(zhuǎn)到安裝界面乖寒,主要實(shí)現(xiàn)為這行代碼
intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
安裝界面
若安裝文件有問(wèn)題,系統(tǒng)會(huì)提示解析錯(cuò)誤
完全更新
為了區(qū)分完全更新和增量更新院溺,服務(wù)器返回的json數(shù)據(jù)增加一個(gè)diffUpdate字段,用來(lái)區(qū)分文件是.apk文件還是差分包文件
修改服務(wù)器的info數(shù)據(jù)
var info = {
'url': '/ota_file/update.zip',
'updateMessage': 'Fix bugs.',
'versionName': 'v2',
'md5': '',
'diffUpdate': true
};
完全更新時(shí)逐虚,diffUpdate 值設(shè)為 true
url 設(shè)置為 /ota_file/update.zip
應(yīng)用做些修改痊班,修改主界面的背景色涤伐,增加一行代碼
android:background="@color/colorAccent"
對(duì)應(yīng)的項(xiàng)目版本信息修改
然后運(yùn)行Android Studio Build Apk,生成的Apk文件在項(xiàng)目目錄下
將apk文件拷貝到 ota服務(wù)器項(xiàng)目的 ota_file 文件夾下型雳,并修改文件名為 update.zip
然后將修改的代碼回退纠俭,運(yùn)行舊的版本冤荆,點(diǎn)擊版本更新钓简,下載更新文件完成后進(jìn)行安裝
安裝完成后外邓,重新打開(kāi)應(yīng)用古掏,便可以看到主界面的背景顏色改變了槽唾。
增量更新
增量更新的原理,就是將手機(jī)上已安裝apk與服務(wù)器端最新apk進(jìn)行二進(jìn)制對(duì)比,得到差分包挂绰,用戶更新程序時(shí)葵蒂,只需要下載差分包秦士,并在本地使用差分包與已安裝apk隧土,合成新版apk曹傀。
apk文件的差分皆愉、合成可以通過(guò)開(kāi)源的二進(jìn)制比較工具 bsdiff實(shí)現(xiàn)幕庐。
處理差分包的代碼實(shí)現(xiàn)
項(xiàng)目中引入第三方動(dòng)態(tài)庫(kù) libApkPatchLibrary.so
引入對(duì)應(yīng)的包 com.cundong.utils.PatchUtils
(差分包更新的實(shí)現(xiàn)參考了這篇文章,其中也用到了里面的文件)
DownloadService 下載完成時(shí)家淤,根據(jù)服務(wù)器提供的 diffUpdate 字段來(lái)判斷是否為差分包文件,若是媒鼓,則將差分包文件與當(dāng)前的apk合成新的apk,并安裝新的apk
安裝前合成代碼的實(shí)現(xiàn)
if (isDiff) {
// 增量式升級(jí)绿鸣,先將patch合成新apk
String oldApkPath = InfoUtils.getBaseApkPath(getApplicationContext());
String newApkName = "update.apk";
String newApkPath = dir.getPath() + "/" + newApkName;
String patchPath = downloadFile.getPath();
Log.i(TAG, "MD5:");
Log.i(TAG, "old apk md5: " + SignUtils.getMd5ByFile(new File(oldApkPath)));
Log.i(TAG, "new apk md5: " + SignUtils.getMd5ByFile(new File(newApkPath)));
Log.i(TAG, "patch md5: " + SignUtils.getMd5ByFile(new File(patchPath)));
Log.i(TAG, "Patch diff...");
int patchResult = PatchUtils.patch(oldApkPath, newApkPath, patchPath);
if (patchResult == 0) {
apkFile = new File(newApkPath);
}
}
installAPk(apkFile);
應(yīng)用當(dāng)前的apk會(huì)保存在系統(tǒng)特定的位置,通過(guò) getBaseApkPath 方法獲取路徑潮模,方法的具體實(shí)現(xiàn)
public static String getBaseApkPath(Context context) {
String pkName = context.getPackageName();
try {
ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(pkName, 0);
String path = appInfo.sourceDir;
return path;
} catch (PackageManager.NameNotFoundException e) {
}
return null;
}
PatchUtils.patch 方法將舊apk文件與差分包文件合成新apk亮蛔,并保存到newApkPath路徑下。
然后和完全更新的方式一樣究流,通過(guò) installAPk(apkFile); 安裝更新。
生成差分包文件
首先安裝差分包生成工具,下載地址為http://www.daemonology.net/bsdiff/
安裝完成后厘惦,試著在命令行敲 bsdiff 命令
bsdiff
bsdiff: usage: bsdiff oldfile newfile patchfile
可看到該命令接收3個(gè)參數(shù)节榜,依次為舊的文件、新的文件、生成的差分包文件
和之前Build Apk方式一樣讳窟,先將未修改前的應(yīng)用編一個(gè)apk文件,命名為old.apk
然后修改應(yīng)用的背景色和版本信息蛇数,重新編一個(gè)apk文件挪钓,命名為new.apk
將新舊兩個(gè)apk文件拷貝到同一目錄下,命令行cd到當(dāng)前目錄下耳舅,運(yùn)行命令
old.apk new.apk patch.zip
完成后可看到在當(dāng)前目錄下生成了新文件 patch.zip碌上,將patch.zip拷貝到服務(wù)器的ota_file文件夾下
服務(wù)器返回的信息做對(duì)應(yīng)修改
var info = {
'url': '/ota_file/patch.zip',
'updateMessage': 'Fix bugs.',
'versionName': 'v2',
'md5': '',
'diffUpdate': true
};
diffUpdate 改為 true,url 改為差分包文件
運(yùn)行
之前的運(yùn)行都是通過(guò)Android Studio直接連手機(jī)真機(jī)跑起來(lái)的浦徊,但是調(diào)試差分包升級(jí)時(shí)需要注意馏予,差分包必須保證本地應(yīng)用的apk文件與生成差分包時(shí)的old.apk文件完全一致,否則合成新的apk會(huì)失敗盔性。
而每次通過(guò)Android Studio連接手機(jī)編起來(lái)的應(yīng)用所生成的apk文件都是不一樣的霞丧,雖然代碼一模一樣。
為了測(cè)試差分包冕香,手機(jī)必須通過(guò)舊的apk來(lái)安裝并運(yùn)行應(yīng)用
用 adb 命令安裝
adb install -r old.apk
結(jié)果打印
[100%] /data/local/tmp/old.apk
pkg: /data/local/tmp/old.apk
Success
安裝完成后運(yùn)行蛹尝,和之前一樣操作的流程。
end悉尾。
其他
實(shí)際生產(chǎn)中的升級(jí)過(guò)程還會(huì)涉及到很多邏輯處理細(xì)節(jié)突那,比如下載完成之后的MD5校驗(yàn),下載前判斷本地是否存在更新文件等构眯,但基本的更新方式和流程如上所示愕难。
Demo附完整源碼,源碼里還附了一些工具類處理一些細(xì)節(jié)惫霸。
運(yùn)行時(shí)只需按照文章之前所示猫缭,修改服務(wù)器信息即可進(jìn)行相應(yīng)的升級(jí)方式。