本文將講解app的升級(jí)與更新。一般而言用戶(hù)使用App的時(shí)候升級(jí)提醒有兩種方式獲得:
一種是通過(guò)App Store獲取
一種是打開(kāi)應(yīng)用之后提醒用戶(hù)更新升級(jí)
而更新操作一般是在用戶(hù)點(diǎn)擊了升級(jí)按鈕之后開(kāi)始執(zhí)行的,這里的升級(jí)操作也分為兩種形式:
一般升級(jí)
強(qiáng)制升級(jí)
app升級(jí)操作:
App Store升級(jí) 在App Store中升級(jí)需要為App Store上傳新版App,我們?cè)谛掳姹就瓿芍蠖紩?huì)上傳到App Store中,不同的應(yīng)用市場(chǎng)審核的時(shí)間不同,一般除了第一次上傳時(shí)間較長(zhǎng)之外,其余的審核都是挺快的,一般不會(huì)超過(guò)半天(不排除例外情況奧),在審核完成之后就相當(dāng)于完成了這個(gè)應(yīng)用市場(chǎng)的發(fā)布了,也就是發(fā)布上線了。這時(shí)候如果用戶(hù)安裝了這個(gè)應(yīng)用市場(chǎng),那么就能看到我們的App有新版本的升級(jí)提醒了。
應(yīng)用內(nèi)升級(jí) 除了可以在應(yīng)用市場(chǎng)升級(jí),我們還可以在應(yīng)用內(nèi)升級(jí),在應(yīng)用內(nèi)升級(jí)主要是通過(guò)調(diào)用服務(wù)器端接口獲取應(yīng)用的升級(jí)信息,然后通過(guò)獲取的服務(wù)器升級(jí)應(yīng)用信息與本地的App版本比對(duì),若服務(wù)器下發(fā)的最新的App版本高于本地的版本號(hào),則說(shuō)明有新版本發(fā)布,那么我們就可以執(zhí)行更新操作了,否則忽略掉即可。
應(yīng)用內(nèi)升級(jí)其實(shí)已經(jīng)有好多第三方的SDK了,常見(jiàn)的友盟,百度App開(kāi)發(fā)工具包都已經(jīng)集成了升級(jí)的功能,部分SDK廠商還提供增量更新的功能。增量更新的內(nèi)容不是我們這里的討論重點(diǎn),想了解更多增量更新的內(nèi)容可參考:淺談Android增量升級(jí)
這里我們先簡(jiǎn)單介紹一下友盟的App升級(jí)功能,友盟其實(shí)已經(jīng)有了App升級(jí)的API,我們只需要簡(jiǎn)單的調(diào)用即可。
友盟更新接口API
/** * 請(qǐng)求友盟更新API,判斷是否彈出更新彈窗 */public static void updateVersion(final Activity mContext, final MainActivity.UpdateCallback updateCallback, final boolean isShow) { UmengUpdateAgent.setUpdateListener(new UmengUpdateListener() { @Override public void onUpdateReturned(int updateStatus, UpdateResponse updateInfo) { switch (updateStatus) { //判斷是否有新版本需要更新 case UpdateStatus.Yes: // has update try { //在線讀取更新參數(shù) String value = MobclickAgent.getConfigParams(mContext, "FORCE_UPDATE_MIXVERSION"); if (value != null && !value.trim().equals("")) { int versionCode = Config.changeVersionNameToCode(value); if (versionCode != 0) { String localVersionName = getVersionName(mContext); int localVersionCode = Config.changeVersionNameToCode(localVersionName); //判斷當(dāng)前版本號(hào)于友盟中的最低版本號(hào),若當(dāng)前版本號(hào)小于最低版本號(hào),則強(qiáng)制更新,否則非強(qiáng)制更新 if (localVersionCode <= versionCode) { // 彈窗更新彈窗 updateCallback.onUpdateSuccess(updateInfo); } else { UmengUpdateAgent.setUpdateAutoPopup(true); UmengUpdateAgent.showUpdateDialog(mContext, updateInfo); } } else { UmengUpdateAgent.setUpdateAutoPopup(true); UmengUpdateAgent.showUpdateDialog(mContext, updateInfo); } } else { UmengUpdateAgent.setUpdateAutoPopup(true); UmengUpdateAgent.showUpdateDialog(mContext, updateInfo); } } catch (Exception e) { e.PRintStackTrace(); } break; case UpdateStatus.No: // has no update if (isShow) { Config.showToast(mContext, "您當(dāng)前使用的友友用車(chē)已是最新版本"); } break; } } }); UmengUpdateAgent.setUpdateAutoPopup(false); UmengUpdateAgent.forceUpdate(mContext); UmengUpdateAgent.setChannel(ChannelUtil.getChannel(mContext)); }以上是友盟的升級(jí)API,在調(diào)用之前需要先繼承友盟的SDK,這樣經(jīng)過(guò)調(diào)用之后我們就可以通過(guò)友盟實(shí)現(xiàn)更新接口的提示功能了,默認(rèn)的友盟提供了靜默安裝,更新提示彈窗,強(qiáng)制更新等幾種,可以根據(jù)自身App的需求來(lái)確定更新的方式。
如果不喜歡使用第三方的更新方式,我們也可以通過(guò)調(diào)用服務(wù)器接口的方式實(shí)現(xiàn)自己的更新彈窗提示,主要的邏輯也是通過(guò)判斷服務(wù)器下發(fā)的最新App版本號(hào)與本地版本號(hào)對(duì)比,若服務(wù)器端的App版本號(hào)大于本地的App版本號(hào),則說(shuō)明當(dāng)前App不是最新的版本,需要升級(jí),這里我們簡(jiǎn)單看一下友友用車(chē)中自定義的更新接口實(shí)現(xiàn):
/** * 檢測(cè)App是否需要更新 * * @param mContext * @param isShow 若不需要更新是否需要彈出文案 */ public static void queryAppBaseVersionInfo(final Activity mContext, final boolean isOneUpdate, final boolean isShow) { try { // 若當(dāng)前網(wǎng)絡(luò)異常,則直接return if (!Config.isNetworkConnected(mContext)) { // 關(guān)閉進(jìn)度條 dismissProgress(isShow); return; } // 控制變量,App更新接口進(jìn)程生命周期中只會(huì)調(diào)用一次 if (isQueryAppUpdated && isOneUpdate) { return; } L.i("開(kāi)始調(diào)用請(qǐng)求是否需要版本更新的接口...."); ExtInterface.QueryAppBaseVersionInfoNL.Request.Builder request = ExtInterface.QueryAppBaseVersionInfoNL.Request.newBuilder(); request.setClientChannel(CHANNEL_Android); // 查詢(xún)最新的版本信息,不需要傳入版本號(hào) // request.setVersionCode(VersionUtils.getVersionName(mContext)); NetworkTask task = new NetworkTask(Cmd.CmdCode.QueryAppBaseVersionInfo_VALUE); task.setBusiData(request.build().toByteArray()); NetworkUtils.executeNetwork(task, new HttpResponse.NetWorkResponse<UUResponseData>() { @Override public void onSuccessResponse(UUResponseData responseData) { if (responseData.getRet() == 0) { try { isQueryAppUpdated = true; ExtInterface.QueryAppBaseVersionInfoNL.Response response = ExtInterface.QueryAppBaseVersionInfoNL.Response.parseFrom(responseData.getBusiData()); if (response.getRet() == 0) { L.i("請(qǐng)求檢測(cè)App是否更新接口成功,開(kāi)始解析返回結(jié)果"); // 解析檢測(cè)結(jié)果 parserUpdateResule(mContext, response, isShow); } else { if (isShow) { showDefaultNetworkSnackBar(mContext); } } } catch (InvalidProtocolBufferException e) { e.printStackTrace(); if (isShow) { showDefaultNetworkSnackBar(mContext); } } } } @Override public void onError(VolleyError errorResponse) { L.e("請(qǐng)求檢測(cè)更新接口失敗...."); if (isShow) { showDefaultNetworkSnackBar(mContext); } } @Override public void networkFinish() { L.i("請(qǐng)求檢測(cè)更新接口完成...."); // 關(guān)閉進(jìn)度條 dismissProgress(isShow); } }); } catch (Exception e) { e.printStackTrace(); } }該接口只會(huì)在App打開(kāi)時(shí)調(diào)用一次,判斷App是否需要更新,然后在請(qǐng)求服務(wù)器成功之后,會(huì)解析請(qǐng)求結(jié)果,我們繼續(xù)看一下我們的解析邏輯:
/** * 解析更新檢查結(jié)果 * * @param response */ private static void parserUpdateResule(Activity mContext, ExtInterface.QueryAppBaseVersionInfoNL.Response response, boolean isShow) { if (mContext == null) { return; } // 判斷是否需要更新 ExtInterface.AppBaseVersionInfo appBaseVersionInfo = response.getAppBaseVersionInfo(); // 若當(dāng)前更新是否有效 if (appBaseVersionInfo.getIsDel() == ENEFFECT) { return; } String updateVersionCode = appBaseVersionInfo.getVersionCode(); int updateCode = changeVersionNameToCode(updateVersionCode); int localCode = changeVersionNameToCode(VersionUtils.getVersionName(mContext)); // 本地應(yīng)用版本號(hào)小于更新的應(yīng)用版本號(hào),則需要更新 L.i("本地版本號(hào):" + localCode + " " + VersionUtils.getVersionName(mContext) + " 遠(yuǎn)程版本號(hào):" + updateCode + " " + updateVersionCode); if (localCode < updateCode) { // 顯示更新文案 L.i("開(kāi)始顯示更新彈窗..."); showUpdateDialog(mContext, appBaseVersionInfo); } // 不需要更新 else { if (isShow) { Config.showToast(mContext, mContext.getResources().getString(R.string.about_new)); } } }app更新操作:
app的更新操作就是下載App并安裝了,下面我們還是分兩部分看,應(yīng)用市場(chǎng)的更新與應(yīng)用內(nèi)更新
App store更新App 在應(yīng)用市場(chǎng)中更新App很簡(jiǎn)單就是執(zhí)行簡(jiǎn)單的下載操作,然后順著App的提醒,一步步安裝即可,這里沒(méi)有什么需要注意的地方。
應(yīng)用內(nèi)更新 應(yīng)用內(nèi)更新操作主要是當(dāng)用戶(hù)點(diǎn)擊了更新按鈕之后執(zhí)行的,下載,安裝等邏輯,下面我們看一下友友用車(chē)應(yīng)用內(nèi)更新的實(shí)踐。
應(yīng)用內(nèi)更新主要包含了:普通更新和強(qiáng)制更新兩種,其中普通更新彈窗可以選擇更新也可以選擇忽略,而強(qiáng)制更新只能選擇更新,并且更新彈窗不可取消。
下面的代碼是執(zhí)行下載操作的核心邏輯:
/** * 開(kāi)始執(zhí)行下載動(dòng)作 */ private static void doDownLoad(final Activity mContext, String downloadUrl, final String actionButtonMsg, final boolean isFocusUpdate) { // 強(qiáng)制更新 if (isFocusUpdate) { DownLoadDialog.updateRela.setVisibility(View.VISIBLE); DownLoadDialog.progressBar.setProgress(0); DownLoadDialog.progressBar.start(); DownLoadDialog.updatePercent.setText("0%"); DownLoadDialog.materialDialog.getPositiveButton().setEnabled(false); DownLoadDialog.materialDialog.getPositiveButton().setText("下載中"); } Config.showToast(mContext, "開(kāi)始下載安裝包......."); // 刪除下載的apk文件 doDeleteDownApk(mContext); L.i("安裝包下載地址:" + downloadUrl); DownloadManager.getInstance().cancelAll(); DownloadManager.downloadId = DownloadManager.getInstance().add(DownloadManager.getDownLoadRequest(mContext, downloadUrl, new DownloadStatusListenerV1() { @Override public void onDownloadComplete(DownloadRequest downloadRequest) { L.i("onDownloadComplete_____..."); // 設(shè)置按鈕是否可點(diǎn)擊 showPositiveText(false, actionButtonMsg); if (isFocusUpdate) { // 更新進(jìn)度條顯示 DownLoadDialog.updatePercent.setText("100%"); DownLoadDialog.progressBar.stop(); } else { String title = "正在下載友友用車(chē)..."; String content = "下載成功"; DownloadNotification.showNotification(mContext, title, content, DownloadNotification.notofyId); // 關(guān)閉通知欄消息 UUApp.notificationManager.cancel(DownloadNotification.notofyId); } // 下載完成,執(zhí)行安裝邏輯 doInstallApk(mContext); // 退出App UUApp.getInstance().exit(); } @Override public void onDownloadFailed(DownloadRequest downloadRequest, int errorCode, String errorMessage) { L.i("onDownloadFiled______..."); L.i("errorMessage:" + errorMessage); // 設(shè)置按鈕是否可點(diǎn)擊 showPositiveText(false, actionButtonMsg); if (isFocusUpdate) { // DownLoadDialog.progressBar.stop(); DownLoadDialog.updatePercent.setText("更新失敗"); } else { String title = "正在下載友友用車(chē)..."; String content = "下載失敗"; DownloadNotification.showNotification(mContext, title, content, DownloadNotification.notofyId); } } @Override public void onProgress(DownloadRequest downloadRequest, long totalBytes, long downloadedBytes, int progress) { if (lastProgress != progress) { lastProgress = progress; L.i("onProgress_____progress:" + progress + " totalBytes:" + totalBytes + " downloadedBytes:" + downloadedBytes); // 設(shè)置按鈕是否可點(diǎn)擊 showPositiveText(true, actionButtonMsg); // 強(qiáng)制更新則更新進(jìn)度條 if (isFocusUpdate) { String content = downloadedBytes * 100 / totalBytes + "%"; float result = progress / (float)100.00; DownLoadDialog.progressBar.setProgress(result); DownLoadDialog.updatePercent.setText(content); } else { String title = "正在下載友友用車(chē)..."; String content = downloadedBytes * 100 / totalBytes + "%"; DownloadNotification.showNotification(mContext, title, content, DownloadNotification.notofyId); } } } })); }這里的下載操作包含了三個(gè)回調(diào)方法:
onDownloadComplete()
onDownloadFailed()
onProgress()
其中onDownlaodComplete方法在下載完成時(shí)回調(diào),onDownloadFailed方法在下載失敗是回調(diào),而onProgress方法則用于刷新下載進(jìn)程,我們?cè)趏nProcess方法中更新通知欄下載進(jìn)度,具體我們可以看一下更新通知欄消息的方法:
/** * 更新通知欄顯示 * @param title * @param content * @param notifyId */ public static void showNotification(Activity mContext, String title, String content, int notifyId) { NotificationCompat.Builder mNotifyBuilder = new NotificationCompat.Builder(mContext) .setSmallIcon(R.mipmap.icon) .setContentTitle(title) .setContentText(content) .setSmallIcon(Android.R.drawable.stat_sys_download); Notification notification = mNotifyBuilder.build(); // notification.flags = Notification.FLAG_NO_CLEAR; UUApp.notificationManager.notify(notifyId, notification); }而在onDownloadFailed方法中,執(zhí)行的代碼邏輯是提示用戶(hù)下載失敗, 而在onDownloadComplete方法中,執(zhí)行安裝下載apk文件的操作,我們可以繼續(xù)看一下我們是如何執(zhí)行安裝邏輯的。
/** * 執(zhí)行安裝apk文件 */ private static void doInstallApk(Activity mContext) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setDataAndType(Uri.fromFile(new File(DownloadManager.getApkPath(mContext))), "application/vnd.Android.package-archive"); mContext.startActivity(intent); }這段代碼會(huì)調(diào)用Android的安裝apk程序,這樣我們就執(zhí)行了下載文件的安裝操作,不同的手機(jī)安裝程序及展示界面略有不同。
總結(jié):
App升級(jí)操作分為兩種,在應(yīng)用市場(chǎng)提示升級(jí)和在應(yīng)用內(nèi)提示升級(jí),而在應(yīng)用內(nèi)提示升級(jí)可以繼承第三方升級(jí)API(如:友盟),也可以自己實(shí)現(xiàn);
應(yīng)用升級(jí)的提示主要邏輯是根據(jù)服務(wù)器端的APK版本號(hào)與本地的應(yīng)用版本號(hào)對(duì)比,若服務(wù)器端的應(yīng)用版本號(hào)高于本地版本號(hào),則說(shuō)明應(yīng)用需要升級(jí);
應(yīng)用升級(jí)可以分為普通升級(jí)和強(qiáng)制升級(jí)兩種,一般不太建議使用強(qiáng)制升級(jí)(用戶(hù)體驗(yàn)很差),除非是一些嚴(yán)重的線上bug;
App的更新操作包含下載與安裝兩部分,下載操作時(shí)可以選擇繼承第三方服務(wù),也可以自己實(shí)現(xiàn)。
背景 隨著android應(yīng)用體積的不斷增大,以及應(yīng)用版本發(fā)布的不斷更迭,用戶(hù)的升級(jí)成了一個(gè)問(wèn)題,google也意識(shí)到不斷更新應(yīng)用對(duì)用戶(hù)流量的損耗,在Google I/O 上提及的 Smart App update,即應(yīng)用增量升級(jí),或者叫做差分升級(jí)的做法,并在新版本的Google Play中得到支持,某天在和群友聊天是扯到這方面的話題,好奇就稍微研究了一下。 增量升級(jí)的原理 今天我們就來(lái)實(shí)現(xiàn)類(lèi)似的應(yīng)用的增量升級(jí)。其實(shí)增量升級(jí)的原理很簡(jiǎn)單,即首先將應(yīng)用的舊版本Apk與新版本Apk做差分,得到更新的部分的補(bǔ)丁,例如舊版本的APK有5M,新版的有8M,更新的部分則可能只有3M左右(這里需要說(shuō)明的是,得到的差分包大小并不是簡(jiǎn)單的相減,因?yàn)槠鋵?shí)需要包含一些上下文相關(guān)的東西),使用差分升級(jí)的好處顯而易見(jiàn),那么你不需要下載完整的8M文件,只需要下載更新部分就可以,而更新部分可能只有3、4M,可以很大程度上減少流量的損失。
在用戶(hù)下載了差分包之后,需要在手機(jī)端將他們組合起來(lái)??梢詤⒖嫉淖龇ㄊ窍葘⑹謾C(jī)端的舊版本軟件(多半在/data/下),復(fù)制到SD卡或者cache中,將它們和之前的差分patch進(jìn)行組合,得到一個(gè)新版本的apk應(yīng)用,如果不出意外的話,這個(gè)生成的apk和你之前做差分的apk是一致的。增量升級(jí)的操作 在了解基本的原理之后,我們來(lái)逐步解決其中的各個(gè)難點(diǎn)。首先是差分包patch的生成。如果做過(guò)android手機(jī)OTA升級(jí)的同學(xué)應(yīng)該注意到,在update.zip中的patch文件夾中有需要與系統(tǒng)文件同名但是以xxx.p 為后綴的文件,他們就是生成的差分patch文件。我們可以借鑒OTA系統(tǒng)升級(jí)的差分生成工具來(lái)生成我們單個(gè)應(yīng)用apk的差分patch文件。 OTA系統(tǒng)差分包的制作,使用命令:
./build/tools/releasetools/ota_from_target_files -n -i <舊包> <新包> <差分包名>在查閱ota_from_target_files 的代碼可知,是在函數(shù)WriteIncrementalOTAPackage里生成差分包的,在這個(gè)函數(shù)里邊創(chuàng)建了common.Difference這個(gè)類(lèi),我們繼續(xù)跟進(jìn),在common.py中的類(lèi) class Difference(object):里可以看到:
diff_program = DIFF_PROGRAM_BY_EXT.get(ext, "bsdiff") 至此我們就看到了android中提供我們用來(lái)制作差分增量升級(jí)包的工具,"bsdiff",這是一個(gè)很牛X開(kāi)源的二進(jìn)制差分工具.相關(guān)的介紹傳送門(mén)相關(guān)的代碼地址 或者在android的代碼目錄下 /external/bsdiff
bsdiff是二進(jìn)制差分工具,其對(duì)應(yīng)的bspatch是相應(yīng)的補(bǔ)丁合成工具 需要注意的是增量升級(jí)的補(bǔ)丁包,是需要在服務(wù)器端,即PC端完成,大致流程如,制作補(bǔ)丁時(shí)調(diào)用bsdiff函數(shù),根據(jù)兩個(gè)不同版本的二進(jìn)制文件,生成補(bǔ)丁文件。 命令:bsdiff oldfile newfile patchfile 例如: bsdiff xx_v1.0.apk xx_v2.0.apk xx.patch將生成的補(bǔ)丁包 xx.patch放置在升級(jí)服務(wù)器上,供用戶(hù)下載升級(jí),對(duì)應(yīng)多版本需要對(duì)不同的版本進(jìn)行差分,對(duì)于版本跨度較大的,建議整包升級(jí)。 用戶(hù)在下載了 xx.patch補(bǔ)丁包后,需要用到補(bǔ)丁所對(duì)應(yīng)的apk,即原來(lái)系統(tǒng)安裝的舊版本apk和補(bǔ)丁合成的bspatch工具。系統(tǒng)舊版本的apk可以通過(guò)copy系統(tǒng)data/app目錄下的apk文件獲取,而補(bǔ)丁合成的bspatch可以通過(guò)將bspatch源碼稍作修改,封裝成一個(gè)so庫(kù),供手機(jī)端調(diào)用。
bspatch的命令格式為: bspatch oldfile newfile patchfile和差分時(shí)的參數(shù)一樣。合成新的apk便可以用于安裝。 以上只是簡(jiǎn)單的操作原理,增量升級(jí)還涉及很多其他方面,例如,升級(jí)補(bǔ)丁校驗(yàn)等問(wèn)題,可以參考android源碼中bootable/recovery/applypatch的相關(guān)操作,本文只是淺析,在此不表。 不足 增量升級(jí)并非完美無(wú)缺的升級(jí)方式,至少存在以下兩點(diǎn)不足: 1.增量升級(jí)是以?xún)蓚€(gè)應(yīng)用版本之間的差異來(lái)生成補(bǔ)丁的,你無(wú)法保證用戶(hù)每次的及時(shí)升級(jí)到最新,所以你必須對(duì)你所發(fā)布的每一個(gè)版本都和最新的版本作差分,以便使所有版本的用戶(hù)都可以差分升級(jí),這樣操作相對(duì)于原來(lái)的整包升級(jí)較為繁瑣,不過(guò)可以通過(guò)自動(dòng)化的腳本批量生成。 2.增量升級(jí)成功的前提是,用戶(hù)手機(jī)端必須有能夠讓你拷貝出來(lái)且與你服務(wù)器用于差分的版本一致的apk,這樣就存在,例如,系統(tǒng)內(nèi)置的apk無(wú)法獲取到,無(wú)法進(jìn)行增量升級(jí);對(duì)于某些與你差分版本一致,但是內(nèi)容有過(guò)修改的(比如破解版apk),這樣也是無(wú)法進(jìn)行增量升級(jí)的,為了防止合成補(bǔ)丁錯(cuò)誤,最好在補(bǔ)丁合成前對(duì)舊版本的apk進(jìn)行sha1sum校驗(yàn),保證基礎(chǔ)包的一致性。
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注