Android APK 更新之路

iuds8979 8年前發布 | 11K 次閱讀 安卓開發 Android開發 移動開發

一、前言

提到 APK 更新,大家可能會想到友盟(umeng)更新,市場上已有數萬款應用在使用友盟自動更新的服務。但友盟于 2016 年 10 月 15 日起停止了更新服務。那么我們需要自己處理 APK 更新的業務。

本篇主要講解以下知識點:

  • 使用 DownloadManager 更新

  • 基于 RxJava 和 retrofit 擴展的 Android 線程安全 http 請求庫下載 APK 更新

  • 熱更新(AndFix)

我們來啾啾第一個知識點。

DownloadManager 更新

Android 2.3(API level 9)開始 Android 用系統服務(Service)的方式提供了DownloadManager 來優化處理長時間的下載操作。DownloadManager 對后臺下載,下載狀態回調,斷點續傳,下載環境設置,下載文件的操作等都有很好的支持。

本篇基于 Android 4.0 ~7.0 (SDK 14~24) 開發,眾所周知 Android 6.0 的 Runtime Permissions (運行時權限)。

下面具體來看看 DownloadManager 更新的具體流程。

AndroidManifest 清單文件配置權限

下載文件需要使用到網絡權限,文件讀寫權限:

<uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

獲取當前的版本號

getPackageManager().getPackageInfo(getPackageName(), 0).versionName;

后臺需要提供查詢最新版本號的接口,獲取接口數據與當前版本號對比,判定是否需要更新。

獲取 DownloadManager 實例

DownloadManager manager = (DownloadManager)
            appContext.getSystemService(Context.DOWNLOAD_SERVICE);

下面來看看 DownloadManager 提供哪些接口:

  • public long enqueue(Request request) 執行下載,返回 downloadId,downloadId 可用于后面查詢下載信息。若網絡不滿足條件、Sdcard 掛載中、超過最大并發數等異常則會等待下載,正常則直接下載。

  • int remove(long… ids) 刪除下載,若下載中取消下載。會同時刪除下載文件和記錄。參數 ids 為 enqueue 返回的 downloadId 集合。

  • Cursor query(Query query) 查詢下載信息。

  • getMaxBytesOverMobile(Context context) 返回移動網絡下載的最大值

  • rename(Context context, long id, String displayName) 重命名已下載項的名字

  • getRecommendedMaxBytesOverMobile(Context context) 獲取建議的移動網絡下載的大小

  • 其它:通過查看代碼我們可以發現還有個 CursorTranslator 私有靜態內部類。這個類主要對 Query 做了一層代理。將 DownloadProvider 和 DownloadManager之間做了個映射。

接著來看看 DownloadManager.Request 的請求參數。

組裝 DownloadManager.Request 請求參數

//獲取Request的實例對象 
    DownloadManager.Request request = new DownloadManager.Request(Uri.parse(appUrl));

顯示信息:

//設置一些基本顯示信息
    request.setTitle(name); //通知欄標題
    request.setDescription(description);//通知欄內容
    request.setMimeType("application/vnd.android.package-archive");//文件的類型

網絡類型:

//NETWORK_MOBILE移動網絡
//NETWORK_WIFI  wifi網絡
//NETWORK_BLUETOOTH 藍牙
req.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);

通知欄顯示類型:

request.setNotificationVisibility(DownloadManager.Request
            .VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
  • VISIBILITY_HIDDEN 下載UI不會顯示,也不會顯示在通知中,如果設置該值,
    需要聲明android.permission.DOWNLOAD_WITHOUT_NOTIFICATION
  • VISIBILITY_VISIBLE 當處于下載中狀態時,可以在通知欄中顯示;當下載完成后,通知欄中不顯示
  • VISIBILITY_VISIBLE_NOTIFY_COMPLETED 當處于下載中狀態和下載完成時狀態,均在通知欄中顯示
  • VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION 只在下載完成時顯示在通知欄中。

文件的保存位置:

  • 保存到外部環境的私有目錄:file:///storage/emulated/0/Android/data/your-package/files/Download/app.apk
request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, "app.apk");
  • 保存到外部環境的共有目錄: file:///storage/emulated/0/Download/app.apk
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "app.apk");
  • 自定義文件路徑
setDestinationUri(Uri uri)

添加請求下載的網絡鏈接的http頭,比如User-Agent,gzip壓縮等:

request.addRequestHeader(String header, String value)

漫游:

//true  允許
//false  不允許
request.setAllowedOverRoaming(false);

其他:

setAllowedOverMetered(boolean allow) //是否允許計量
setRequiresCharging(boolean requiresCharging)//是否在充電環境下
setVisibleInDownloadsUi(boolean isVisible)//是否顯示下載界面
...

下面是本文創建Request的示例代碼:

request.setTitle(name);
    request.setDescription(description);
    //在通知欄顯示下載進度
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        request.allowScanningByMediaScanner();
        request.setNotificationVisibility(DownloadManager.Request
                .VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
    }

request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
request.setDestinationInExternalPublicDir(SAVE_APP_LOCATION, SAVE_APP_NAME);</code></pre> 

加入下載隊列

DownloadManager manager = (DownloadManager)                appContext.getSystemService(Context.DOWNLOAD_SERVICE);

manager.enqueue(request);

下載信息查詢

DownloadManager 下載工具并沒有提供相應的回調接口用于返回實時的下載進度狀態。可以通過 DownloadManager.query 方法進行查詢,該方法返回一個 Cursor 對象,具體看以下代碼:

private void queryDownloadManager(long id) {
        DownloadManager mDownloadManager = (DownloadManager)
                this.getSystemService(Context.DOWNLOAD_SERVICE);
        DownloadManager.Query query = new DownloadManager.Query().setFilterById(id);
        //可以對query設置一些過濾條件
        //setFilterById(long… ids)根據下載id進行過濾
        //setFilterByStatus(int flags)根據下載狀態進行過濾
        Cursor cursor = mDownloadManager.query(query);

        if (cursor != null) {

            while (cursor.moveToNext()) {

                String bytesDownload = cursor.getString(cursor.getColumnIndex(DownloadManager
                        .COLUMN_BYTES_DOWNLOADED_SO_FAR));
                String description = cursor.getString(cursor.getColumnIndex(DownloadManager
                        .COLUMN_DESCRIPTION));
                String cid = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_ID));
                String localUri = cursor.getString(cursor.getColumnIndex(DownloadManager
                        .COLUMN_LOCAL_URI));
                String mimeType = cursor.getString(cursor.getColumnIndex(DownloadManager
                        .COLUMN_MEDIA_TYPE));
                String title = cursor.getString(cursor.getColumnIndex(DownloadManager
                        .COLUMN_TITLE));
                String status = cursor.getString(cursor.getColumnIndex(DownloadManager
                        .COLUMN_STATUS));
                String totalSize = cursor.getString(cursor.getColumnIndex(DownloadManager
                        .COLUMN_TOTAL_SIZE_BYTES));

                Log.i("MainActivity", "bytesDownload:" + bytesDownload);
                Log.i("MainActivity", "description:" + description);
                Log.i("MainActivity", "cid:" + cid);
                Log.i("MainActivity", "localUri:" + localUri);
                Log.i("MainActivity", "mimeType:" + mimeType);
                Log.i("MainActivity", "title:" + title);
                Log.i("MainActivity", "status:" + status);
                Log.i("MainActivity", "totalSize:" + totalSize);
            }

        }
    }

本篇示例的打印結果如下:

man

注冊廣播監聽通知欄點擊事件和下載完成事件

當用戶點擊通知欄中的下載列表時,系統會發出 ACTION_NOTIFICATION_CLICKED 事件廣播;下載完成時會發出 ACTION_DOWNLOAD_COMPLETE 事件廣播,那么我們就可以實現一個廣播接收器處理點擊和完成時的狀態。請看下面代碼:

public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) {

            installApk(context);

        } else if (intent.getAction().equals(DownloadManager.ACTION_NOTIFICATION_CLICKED)) {
            //Toast.makeText(context, "Clicked", Toast.LENGTH_SHORT).show();

        }
    }

如文本下載 apk 文件,下載完成時就自動安裝,使用意圖進行 apk 安裝:

// 安裝Apk
    private void installApk(Context context) {
        try {
            Intent i = new Intent(Intent.ACTION_VIEW);
            String filePath = DownloadManagerUtils.APP_FILE_NAME;
            i.setDataAndType(Uri.parse("file://" + filePath), "application/vnd.android" +
                    ".package-archive");
            i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(i);
        } catch (Exception e) {
            Log.e(TAG, "安裝失敗");
            e.printStackTrace();
        }
    }

DownloadManager 更新就講到這里了,源碼在文章的后面會附上。

基于 RxJava 和 retrofit 擴展的 Android 線程安全 http 請求庫下載 APK 更新

針對 DownloadManager 更新,我們還可以通過 http 請求庫下載 apk 文件進行更新。

提到 http 請求庫,就不得不提到 Novate 庫,功能非常強大,使用便利,看看它有哪些功能:

  • 加入基礎API,減少Api冗余
  • 支持離線緩存
  • 支持多種方式訪問網絡(get,put,post ,delete)
  • 支持Json字符串,表單提交
  • 支持文件下載和上傳
  • 支持請求頭統一加入
  • 支持對返回結果的統一處理
  • 支持自定義的擴展API
  • 支持統一請求訪問網絡的流程控制

我下載了源碼,并修改了進度條的接口。下載文件相信大家都比較熟悉了,我這里就不再細講了。如果有什么疑問請鏈接上面地址查看。

新建通知

以下給出本篇用到的消息代碼:

private NotificationCompat.Builder buildNotification() {
        final Resources res = mContext.getResources();

        // This image is used as the notification's large icon (thumbnail).
        // TODO: Remove this if your notification has no relevant thumbnail.
        final Bitmap picture = BitmapFactory.decodeResource(res, R.mipmap.ic_launcher);

        return new NotificationCompat.Builder(mContext).
                setContentTitle("更新包下載中...")
                .setTicker("準備下載...")
                .setProgress(100, 0, false)
                .setContentText(String.format(mContext.getResources()
                        .getString(R.string.apk_progress), 0) + "%")
                .setLargeIcon(picture)
                .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                .setWhen(System.currentTimeMillis())
                .setSmallIcon(R.mipmap.ic_launcher)
                .setAutoCancel(false);
    }

    //更新消息進度
    public void showProgressNotification(int progress) {
        if (mBuilder == null) {
            mBuilder = buildNotification();
        }
        Notification notification = mBuilder.setProgress(100, progress, false)
                .setContentText(String.format(mContext.getResources().getString(R.string
                        .apk_progress), progress) + "%")
                .build();
        notify(mContext, notification);
    }

apk下載

private void downloadApk() {

        RetrofitClient.getInstance(this).createBaseApi()
                .download(DOWN_URL, new CallBack() {
                    @Override
                    public void onError(Throwable e) {
                        Log.e("HttpActivity", "onError--------2222" + e.getMessage());
                        mHttpNotification.removeProgressNotification();
                    }

                    @Override
                    public void onStart() {
                        super.onStart();
                        mHttpNotification.showProgressNotification(0);
                    }

                    @Override
                    public void onSucess(String path, String name, long fileSize) {
                        mHttpNotification.removeProgressNotification();
                        installApk(HttpActivity.this);
                    }

                    @Override
                    public void onProgress(int progress) {
                        super.onProgress(progress);
                        mCircleProgressView.setProgress(progress);
                        mHttpNotification.showProgressNotification(progress);
                    }
                });

    }

如果你還有疑問,在文章結尾處下載源碼進行查看。

更新全過程效果圖:

 

熱更新(AndFix)

熱更新技術近段時間非常火爆,各個大公司都相繼開發自己的熱更新框架。由于公司主要項目基于電商商城,所以我選擇了阿里巴巴的 AndFix 熱更新的實現,使用起來也比較簡單。至少在我的測試下修改一些小的 BUG 是沒有問題的。

我的開發工具是 Android Studio ,第一步導包:

app 的 dependencies 的節點下:

compile 'com.alipay.euler:andfix:0.3.1@aar'

第二步配置 MyApplication 類:

@TargetApi(Build.VERSION_CODES.KITKAT)
    @Override
    public void onCreate() {
        super.onCreate();

        String version = "";
        try {
            version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }

        mPatchManager = new PatchManager(getApplicationContext());
        mPatchManager.init(version);
        mPatchManager.loadPatch();
        try {
            String patchFileString = "/sdcard" + APATCH_PATH;
            mPatchManager.addPatch(patchFileString);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

首先獲取到版本號,系統會判斷版本號,只有相同的版本號的時候會執行熱更新。其中 String patchFileString = "/sdcard" + APATCH_PATH; 是我測試的補丁存放路徑。你需要替換成你自己的存放路徑。

注意:文件的權限。

然后在 MainActivity 中寫一個打印吐司的方法:

private void showToast() {
        Toast.makeText(this, "你好啊", Toast.LENGTH_LONG).show();
    }

然后打包,重命名為 old.apk

接著修改吐司的內容:

private void showToast() {
        Toast.makeText(this, "你好啊,世界", Toast.LENGTH_LONG).show();
    }

重新打包,命名為 new.apk

下載apkpatch工具

下面是我的目錄結構:

用紅線框框住的是簽名文件,補丁包,舊包。

打開 cmd ->cd 到 apkpatch 的目錄,如我 F:\AndroidTools\apkpatch 目錄下,下圖我已用紅框圈住:

然后輸入:

apkpatch.bat -f new.apk -t old.apk -o output -k demo.jks -p 123456 -a boby -e 123456

其中:

  • -f 是新apk的名字

  • -t 是舊apk的名字

  • -o 是輸出補丁的文件夾位置

  • -k 是 keystore(jks)文件的名稱

  • -p 是keystore文件的密碼

  • -a 是項目的別名

  • -e 別名的密碼

回車,不出現錯誤,補丁打包成功。

打開 output 目錄,則可以看到 out.apatch 文件。

補丁文件上傳到后臺,然后通過接口下載到 /sdcard/out.apatch 目錄下。

注意 /sdcard/out.apatch 路徑,跟 MyApplication 中的一致。

看看效果:

安裝 old.apk 包:

安裝補丁,接著運行:

 

 

來自:http://www.jianshu.com/p/61336c6f750a

 

 本文由用戶 iuds8979 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!