天天看點

【Android】Android程式自動更新

App自動更新的步驟可分為三步:

  1. 檢查更新(如果有更新進行第2步,否則傳回)
  2. 下載下傳新版的APK安裝包
  3. 安裝APK

下面對這三步進行解釋,其中會穿插相應代碼,App自動更新的這三步全部被封裝到了一個單獨的Updater類中,可以直接拿來使用,我會在文章最後貼出源碼github位址。

Updater 使用示例

通過單一的類

Updater

可以友善的實作自動檢查更新、下載下傳安裝包和自動安裝,可以監聽下載下傳進度,可以自定義更新提示等。儲存路徑可以自由書寫,如果路徑中某個目錄不存在會自動建立,流式API接口易于使用。下面是使用示例,一行代碼搞定自動更新:

String savePath = Environment.getExternalStorageDirectory() 
                    + "/whinc/download/whinc.apk";
String updateUrl = "http://192.168.1.168:8000/update.xml";
Updater.with(mContext)
        .downloadListener(mListener)
        .update(updateUrl)
        .save(savePath)
        .create()
        .checkUpdate();
           
【Android】Android程式自動更新

第一步:檢查更新

這一步需要服務端的配合,服務端存放一個XML格式的配置檔案(也可以用JSON或其他格式)提供給用戶端檢查更新,update.xml 格式如下:

<?xml version="1.0" encoding="utf-8"?>
<info>
    <version>
        <code>4</code>
        <name>1.0.4</name>
    </version>
    <url>http://192.168.1.168:8000/test.apk</url>
    <description>更新 - 吧啦吧啦;修複 - 吧啦吧啦;增加 - 巴拉巴拉巴</description>
</info>
           
  • <version>

    标簽指定服務端的版本号和版本名稱,該版本号和版本名稱對應Android項目配置裡的

    versionCode

    versionName

    (Eclipse ADT項目可在 AndroidManifest.xml中的标簽中找到,Android Studio項目在module的build.gradle中的defaultConfig中找到)。
  • <url>

    标簽指定APK的下載下傳位址,
  • <description>

    标簽指定更新内容。

用戶端通過 HTTP 請求服務端的 update.xml檔案,然後解析 update.xml,比較服務端的版本号與本地版本号,如果服務端版本号大于本地版本号說明有更新,則根據 update.xml中指定的APK下載下傳位址下載下傳最新的APK,下面将會詳細說明。

下面是檢查更新的代碼:

/**
     * 檢查 App 版本号
     *
     * @return 如果有新版本傳回true,否則傳回false
     */
    private boolean checkVersion() {
        URL url;
        HttpURLConnection httpConn = null;
        try {
            url = new URL(mCheckUpdateUrl);
            httpConn = (HttpURLConnection) url.openConnection();
            httpConn.setConnectTimeout();
            httpConn.setReadTimeout();
            httpConn.setUseCaches(false);       // disable cache for current http connection
            httpConn.connect();
            if (httpConn.getResponseCode() == HttpURLConnection.HTTP_OK) {
                InputStream inputStream = httpConn.getInputStream();
                // 解析 XML 資料
                if (!parseXml(inputStream)) {
                    return false;
                }
                // 比較本地版本号與伺服器版本号
                PackageInfo packageInfo = mContext.getPackageManager()
                        .getPackageInfo(mContext.getPackageName(), );
                if (packageInfo.versionCode < mRemoteVersionCode) {
                    return true;
                }
            } else {
                return false;
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        } finally {
            httpConn.disconnect();
        }

        return false;
    }
           

首先建立HTTPURLConnection通路服務端update.xml檔案,然後解析服務端傳回的update.xml檔案,并儲存版本資訊、APK下載下傳位址和更新日志,解析完後通過擷取目前用戶端的版本号與服務端版本号比較,如果服務端版本号更大,說明服務端有更新的版本,checkVersion() 方法傳回true,否則傳回false。

下面時檢查更新的代碼,需要注意的是,Android中不允許在主線程(UI線程)中發起網絡請求,是以

checkVersion()

的調用需要放在非主線程中。實作異步請求的方式有多種,這裡我使用 AsyncTask。

public void checkUpdate() {
        new AsyncTask<Void, Void, Boolean>() {

            @Override
            protected Boolean doInBackground(Void... params) {
                boolean hasNewVersion = checkVersion();
                return hasNewVersion;
            }

            @Override
            protected void onPostExecute(Boolean hasNewVersion) {
                super.onPostExecute(hasNewVersion);

                if (mCheckUpdateListener == null
                        || !mCheckUpdateListener.onCompleted(hasNewVersion, mRemoteVersionCode,
                        mRemoteVersionName, mUpdateLog, mApkDownloadUrl)) {
                    if (hasNewVersion) {
                        showUpdateDialog();
                    }
                }
            }
        }.execute();
    }
           

下載下傳新版的APK安裝包

showUpdateDialog()

調用後顯示更新提示對話框,在對話框确認按鈕點選事件中,首先建立DownloadManager.Request對象,然後設定該對象的各種屬性如下載下傳儲存路徑、通知欄标題等,最後将該下載下傳請求放到系統服務DownloadManager的下載下傳隊列中,交給系統去處理下載下傳邏輯。 為了監聽下載下傳完成事件,代碼裡注冊了廣播

DownloadManager.ACTION_DOWNLOAD_COMPLETE

。下載下傳進度通過注冊ContentObserver來監聽。

/**
     * 顯示更新對話框
     */
    private void showUpdateDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
        builder.setTitle(mTitle);
        builder.setMessage(mUpdateLog);
        builder.setPositiveButton(mDialogOkBtnTxt, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();

                // 背景下載下傳
                mDownloadMgr = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
                DownloadManager.Request request = new DownloadManager.Request(Uri.parse(mApkDownloadUrl));
                if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                    // 如果儲存路徑包含子目錄,需要先遞歸建立目錄
                    if (!createDirIfAbsent(mSavePath)) {
                        Log.e("TAG", "apk save path can not be created:" + mSavePath);
                        return;
                    }

                    request.setDestinationUri(Uri.fromFile(new File(mSavePath)));
                    request.setTitle(mNotificationTitle);
                    request.setTitle(mNotificationMessage);
                    // 注冊廣播,監聽下載下傳完成事件
                    mContext.registerReceiver(mCompleteReceiver,
                            new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
                    // 注冊監聽下載下傳進度
                    mContext.getContentResolver().registerContentObserver(Uri.parse("content://downloads/my_downloads"),
                            true, mContentObserver);
                    mDownloadId = mDownloadMgr.enqueue(request);
                } else {
                    Log.e("TAG", "can not access external storage!");
                    return;
                }
                Toast.makeText(mContext, "正在背景下載下傳...", Toast.LENGTH_SHORT).show();
            }
        });
        builder.setNegativeButton(mDialogCancelBtnTxt, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.cancel();
            }
        });
        builder.create().show();
    }

    /**
     * 如果參數 path 指定的路徑中的目錄不存在就建立指定目錄
     *
     * @param path 絕對路徑(包含檔案名,例如 '/sdcard/storage/download/test.apk')
     * @return 如果成功建立目錄傳回true,否則傳回false
     */
    private boolean createDirIfAbsent(String path) {
        String[] array = path.trim().split(File.separator);
        List<String> dirNames = Arrays.asList(array).subList(, array.length - );
        StringBuilder pathBuilder = new StringBuilder(File.separator);
        for (String d : dirNames) {
            pathBuilder.append(d);
            File f = new File(pathBuilder.toString());
            if (!f.exists() && !f.mkdir()) {
                return false;
            }
            pathBuilder.append(File.separator);
        }
        return true;
    }
           

安裝APK

一旦Apk下載下傳完成就會收到廣播消息,此時可以執行安裝APK的動作,不過要先通過下載下傳Id判斷該廣播事件是否是由于我們的APK下載下傳完成發出的,因為系統可能同時有多個下載下傳任務,通過下載下傳id區分。

mCompleteReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -);
                if (downloadId == mDownloadId) {
                    installApk();
                    release();
                }
            }
        };
           

下面是 installApk() 方法,首先通過下載下傳Id從DownloadManager中檢索到下載下傳的APK存儲路徑,然後通過Intent安裝下載下傳的APK,代碼非常簡單。注意,Intent設定辨別為

Intent.FLAG_ACTIVITY_NEW_TASK

,否則不能正常啟動安裝程式。

/**
     * 替換安裝目前App,注意:簽名一緻
     */
    private void installApk() {
        // 擷取下載下傳的 APK 位址
        Uri apkUri = mDownloadMgr.getUriForDownloadedFile(mDownloadId);
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
        mContext.startActivity(intent);
    }
           

github 源碼

whinc/Android-UpdateManager

比較好的參考資料:

DownloadManager | Android Developers

Android系統下載下傳管理DownloadManager功能介紹及使用示例