2019-10-22
關鍵字:APK 通過代碼安裝程序、APK 更新、打開APK安裝程序、APK的升級
這篇文章是基於筆者的另一篇 APK 升級相關的文章(如何實現APK的升級功能)的改進版本。
主要改進的點如下:
1、新增了兼容高低版本系統安裝APK功能
2、改進了定時檢測流程;
3、改進了提示流程;
4、改進了存儲邏輯,兼容多種類型手機,避免出現權限不足的問題。
5、區分了普通升級與強制升級功能。
1、概述
所謂熱更新就是 APK 自動升級的一種較好聽一點的說法而已。一款APK若能擺脫手機系統應用商店的依賴而僅依靠自己來實現升級,就稱為熱更新。
APK 的升級由於需要在程序運行期間,有些甚至在程序在后台運行時都要進行。因此通常都會把升級功能做在一個后台服務子線程上。
而我個人目前寫代碼的理念崇尚“專人專事”,用專業的話來說就是“高內聚”。因此我將整個升級相關的功能代碼都封裝在同一個 Service 代碼文件中。雖然它內部其實夾雜着不少不同類型的功能代碼,但所幸整個代碼文件也並不龐大,同時對於升級功能的管理也特別輕松。
對於APK的升級,我個人之前從未參考過其他人的實現方法。我自己對於升級的理解就是有一個后台線程在程序運行期間不間斷地定時向升級服務器檢索升級信息,當有新版本時,發出提示供用戶選擇下載與否。並且我認為升級應該至少設置兩種升級策略:一是普通升級。二是強制升級。普通升級不必多說,強制升級就是當檢測到有強制升級包時,立即在后台下載該更新包,下載完畢后即彈出軟件安裝界面,並縮短強制升級檢測時間間隔。若用戶取消安裝強制升級包,則在一個較短的時間內再次彈出安裝界面。通過這種“死皮賴臉”的方式來強迫用戶安裝新版本軟件。也許可以實現直接通過代碼來安裝軟件的功能,但我並沒有去研究,所以就姑且不討論這種方式的強制升級了。
2、升級服務
筆者這邊直接通過一個 UpgradeService 來實現升級的所有功能。

由於一個 Service 里要實現多種功能,比如:啟停檢測、發出提示、下載、安裝等,因此這個 Service 定義了多種類型,如下圖:

筆者的 Service 僅支持以 start 的方式啟動,因此要求在啟動 Intent 中設置類型以便於 Service 區分不同的動作,onStartCommand() 方法的實現如下圖所示:

整個 Service 的架構倒是簡單,就根據不同的類型參數干不同的活而已。下面逐一分析下這些類型的“活”。
3、升級檢測
在 Serivce 中關於啟動類型有兩種:1、TYPE_START_CHECK;2、TYPE_STOP_CHECK。筆者關於升級檢測的開啟策略是:當應用不處於前台時即停止升級檢測,回到前台運行又會重新開啟升級檢測。
首先,關於定時功能,這里使用的是 Android 自帶的 android.os.CountDownTimer 來實現。由於這個類是抽象類,因此筆者在 Service 內部封裝了一個 Timer 內部類來實現 CountDownTimer,並根據業務需要定制了一些功能,整個 Timer 內部類的代碼如下:
private static class Timer extends CountDownTimer { private int timing; private int interval; private OnTimeOutCallback callback; Timer(int timingSeconds, int intervalSeconds, OnTimeOutCallback callback) { super(timingSeconds * 1000, intervalSeconds * 1000); timing = timingSeconds; interval = intervalSeconds; this.callback = callback; } @Override public void onTick(long l) { Logger.d(TAG, "onTick:" + l + "," + this); } @Override public void onFinish() { boolean ret = false; if(callback != null) { ret = callback.onTimeOut(timing, interval); } if(!ret) { start(); // Restart timing... } } interface OnTimeOutCallback { /**@return true 表示已處理過Timer的生命周期,Timer不要再重啟計數線程。。*/ boolean onTimeOut(int timingSeconds, int countDownIntervalSeconds); } } // class Timer -- end.
在這個 Timer 類中,定時完成事件是通過回調的方式來通知的,並且在默認情況下定時結束回調過后就會重啟定時器進行二次定時計數。
然后是在 Service 類內部又封裝了一個內部類專門負責普通升級檢查工作。這個內部類僅提供一個 start() 方法與 stop() 方法,同時由於需要依賴於前面說到的 Timer 類來定時計數,因此為了接收定時結果,還得實現 Timer 類中的接口。源碼框架如下:
private class NormalUpgradeThread implements Timer.OnTimeOutCallback { Timer timer; private void start(){ // 根據實際業務需要開啟定時器。 } private void stop(){ // 根據實際業務需要停止定時器 } @Override public boolean onTimeOut(int timingSeconds, int countDownIntervalSeconds) { // 處理邏輯 // 連接服務器檢查升級 return false / true; } }
至此,Service 只需要持有這個 NormalUpgradeThread 類對象就可以控制后台升級子線程,或者說定時計數器的啟動與停止了。
至於前面提到的強制升級,它的框架也和這個基本一樣。
4、提示與觸發升級
在連接服務器檢測升級的功能中,筆者這邊就直接使用 OkHttp 開源框架來實現了。
至於通信傳參與結果解析及錯誤處理,就得各位自行處理了。
關於提示有新升級包的功能,筆者這邊使用的是傳統的在合適的地方以小紅點提示的方式來指示。
當用戶點擊對應的小紅點后會彈出提示對話框,經用戶確定后會觸發下載新升級包或直接打開系統安裝程序的功能。
至於到底是下載還是打開安裝界面取決於指定下載目錄中是否已成功下載好該升級包軟件。
5、下載與安裝
關於下載的新升級包文件的保存位置,筆者這邊采用的是 APK 應用安裝目錄下的 cache 目錄,即
context.getCacheDir();
2020-02-01 更新:
關於選擇下載應用的存儲目錄要慎重!Android 中應用私有目錄,如 /data/data/com.xxx.xxx/ 目錄真的是私有目錄來的。如果你把下載的APK保存在那個地方,到時想調用系統的安裝程序來安裝時就會出現問題了,可能會提示諸如“解析程序包失敗”等異常提示。其原因就是系統的應用安裝程序對於你自己編寫的應用來說也屬於“外部應用”,一個外部應用是沒有權限直接訪問你的私有目錄的,就相當於系統的應用安裝程序拿不到你的安裝包文件,所以當然要報錯了。
解決的辦法倒也簡單,最簡單的就是直接在 sdcard 里保存你下載的新版本APK即可。
在 Android 中,提供給 APK 存儲文件用的位置主要有以下五個:
1、程序安裝目錄下的 cache 目錄,即 /data 目錄對應包名下的 cache 目錄;
2、程序安裝目錄下的 files 目錄,同樣是 /data 目錄對應包名下的 files 目錄;
3、sdcard 目錄;
4、sdcard 目錄下對應包名的專屬目錄下的 cache 目錄;
5、sdcard 目錄下對應包名的專屬目錄下的 files 目錄;
筆者所選擇的,就是上述第 1 條所示的目錄。
之所以選擇這個目錄,是為了避免因手機 ROM 對 APK 訪問 sdcard 權限的默認策略的不同而引發的無法讀寫的問題。上述第 1、第 2 條默認都具有讀寫權限,使用它們是兼容性最好的。
當下載與校驗通過后即自動打開程序安裝應用以便安裝。該功能也因系統版本不同會有不同的實現代碼,核心代碼如下:
Intent intent = new Intent(Intent.ACTION_VIEW); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){ intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_ACTIVITY_NEW_TASK); Uri uri = FileProvider.getUriForFile(this, "com.unionman.locator.fileprovider", apk); intent.setDataAndType(uri, "application/vnd.android.package-archive"); }else{ intent.setDataAndType(Uri.fromFile(apk),"application/vnd.android.package-archive"); } try { startActivity(intent); }catch(Exception e){ e.printStackTrace(); }
對於 Android7.0 及以上的系統,它還需要一些額外的配置,關於這些配置請參閱筆者的另一篇博文:Android如何通過Java代碼安裝APK?
6、普通升級與強制升級
前面所述都是普通升級的流程。
但所謂強制升級它與普通升級也基本一致,唯一的不同就是檢測到有新版本應用時會在后台自動下載。當然你其實也可以根據用戶所使用的網絡來選擇是否自動下載,例如只當用戶通過 WIFI 上網時才自動下載以避免給用戶產生額外的流量費用。
當新版本應用下載好后即縮短檢測周期,每個檢測周期到來時,由於能檢測到當前已下載好安裝包,即會強制打開應用安裝程序以供用戶確認安裝。
縮短檢測周期的目的是為了避免用戶拒絕安裝的情況而准備的。當用戶拒絕安裝時,會回到應用繼續使用,若此時仍安裝原來的檢測周期,則這個強制升級可以認為是沒有一點“強制效果”的。只要用戶不想升級,那用戶只需要在每次彈出安裝界面時返回掉就可以繼續使用舊版軟件了。因此,我們必須要縮短檢測周期,並且縮的非常短。例如每 5 秒彈一次,這樣一來,用戶不升級就相當於無法使用了,就能一定程度上達到強制升級的目的。
至於強制升級的代碼就不貼了。原理知道了,實現起來就很簡單了。
