【Android OTA】應用的更新升級

timg-2.jpeg

原文鏈接

Android應用經常會內置檢測版本更新的功能,在有版本更新的時候,通過下載更新文件進行本地的升級。本文通過實現一個簡單的Demo,來介紹App的更新升級方式。提供更新信息的服務器用到了上一篇文章實現的服務器Demo

App的更新方式主要有兩種:

  • 完全更新(Full updates)
  • 增量更新(Incremental updates,也叫差分包升級)

應用更新前

1.jpg

更新完成后

2.jpg

應用比較簡單,關鍵是更新的流程,看背景色既可知更新是否成功。

App實現

文末附Demo完整源碼,這里只大概介紹主要的步驟。

主要的界面就是一列表,列表有2個選項

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>

列表項初始化,點擊列表選項做對應的跳轉

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 為更新升級界面,進入該界面時會自動向服務器進行更新信息的請求,并將請求的結果顯示。

3.jpg

新建 AsyncTask<Void, Void, String> 子類 UpdateCheckTask,用來做后臺網絡請求。

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;
}

將獲取到的服務器返回數據做json解析,并返回給UpdateActivity

protected void onPostExecute(String result) {
    Log.i(TAG, "onPostExecute()");
    UpdateInfo info = parseJson(result);
    if (this.mListener != null) {
        this.mListener.onSuccess(info);
    }
}

parseJson 函數為UpdateCheckTask中實現的解析方法,具體見源碼。

UpdateCheckTaskonCreate中調用更新請求

new UpdateCheckTask(UpdateActivity.this, this).execute();

檢測到更新后,界面出現下載按鈕,點擊按鈕,下載指定url中的更新文件,顯示下載進度條

4.jpg

新建 DownloadService 用來處理下載流程,DownloadService 繼承自 IntentService

DownloadService 通過指定地址將文件下載到本地

@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();
        
        ...
}

下載過程中將進度以消息的方式通知UpdateActivity以更新進度條

...

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);
    // 如果進度與之前進度相等,則不更新,如果更新太頻繁,否則會造成界面卡頓
    if (progress != oldProgress) {
        sendIntent.putExtra(Constants.UPDATE_DOWNLOAD_PROGRESS, progress);
        getApplicationContext().sendBroadcast(sendIntent);
    }
    oldProgress = progress;
}

...

UpdateActivity 監聽進度消息

mReceiver = new ProgressReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(SERVICE_RECEIVER);
registerReceiver(mReceiver, intentFilter);

UpdateActivity 獲取到消息時更新進度條

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 的具體實現

private void installAPk(File apkFile) {
    Intent intent = new Intent(Intent.ACTION_VIEW);
    //如果沒有設置SDCard寫權限,或者沒有sdcard,apk文件保存在內存中,需要授予權限才能安裝
    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本身自帶接口,當打開.apk格式的文件時跳轉到安裝界面,主要實現為這行代碼

intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");

安裝界面

5.jpg

若安裝文件有問題,系統會提示解析錯誤

6.jpg

完全更新

為了區分完全更新和增量更新,服務器返回的json數據增加一個diffUpdate字段,用來區分文件是.apk文件還是差分包文件

修改服務器的info數據

var info = {
    'url': '/ota_file/update.zip',
    'updateMessage': 'Fix bugs.',
    'versionName': 'v2',
    'md5': '',
    'diffUpdate': true                        
};

完全更新時,diffUpdate 值設為 true
url 設置為 /ota_file/update.zip

應用做些修改,修改主界面的背景色,增加一行代碼

android:background="@color/colorAccent"
7.jpg

對應的項目版本信息修改

8.jpg

然后運行Android Studio Build Apk,生成的Apk文件在項目目錄下

9.jpg

將apk文件拷貝到 ota服務器項目的 ota_file 文件夾下,并修改文件名為 update.zip

然后將修改的代碼回退,運行舊的版本,點擊版本更新,下載更新文件完成后進行安裝

10.jpg

安裝完成后,重新打開應用,便可以看到主界面的背景顏色改變了。

增量更新

增量更新的原理,就是將手機上已安裝apk與服務器端最新apk進行二進制對比,得到差分包,用戶更新程序時,只需要下載差分包,并在本地使用差分包與已安裝apk,合成新版apk。

apk文件的差分、合成可以通過開源的二進制比較工具 bsdiff實現。

處理差分包的代碼實現

項目中引入第三方動態庫 libApkPatchLibrary.so

引入對應的包 com.cundong.utils.PatchUtils

(差分包更新的實現參考了這篇文章,其中也用到了里面的文件)

DownloadService 下載完成時,根據服務器提供的 diffUpdate 字段來判斷是否為差分包文件,若是,則將差分包文件與當前的apk合成新的apk,并安裝新的apk

安裝前合成代碼的實現

if (isDiff) {
     // 增量式升級,先將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);

應用當前的apk會保存在系統特定的位置,通過 getBaseApkPath 方法獲取路徑,方法的具體實現

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路徑下。

然后和完全更新的方式一樣,通過 installAPk(apkFile); 安裝更新。

生成差分包文件

首先安裝差分包生成工具,下載地址為http://www.daemonology.net/bsdiff/

安裝完成后,試著在命令行敲 bsdiff 命令

bsdiff
bsdiff: usage: bsdiff oldfile newfile patchfile

可看到該命令接收3個參數,依次為舊的文件新的文件生成的差分包文件

和之前Build Apk方式一樣,先將未修改前的應用編一個apk文件,命名為old.apk

然后修改應用的背景色和版本信息,重新編一個apk文件,命名為new.apk

將新舊兩個apk文件拷貝到同一目錄下,命令行cd到當前目錄下,運行命令

old.apk new.apk patch.zip

完成后可看到在當前目錄下生成了新文件 patch.zip,將patch.zip拷貝到服務器的ota_file文件夾下

服務器返回的信息做對應修改

var info = {
    'url': '/ota_file/patch.zip',
    'updateMessage': 'Fix bugs.',
    'versionName': 'v2',
    'md5': '',
    'diffUpdate': true                        
};

diffUpdate 改為 trueurl 改為差分包文件

運行

之前的運行都是通過Android Studio直接連手機真機跑起來的,但是調試差分包升級時需要注意,差分包必須保證本地應用的apk文件與生成差分包時的old.apk文件完全一致,否則合成新的apk會失敗。

而每次通過Android Studio連接手機編起來的應用所生成的apk文件都是不一樣的,雖然代碼一模一樣。

為了測試差分包,手機必須通過舊的apk來安裝并運行應用

adb 命令安裝

adb install -r old.apk 

結果打印

[100%] /data/local/tmp/old.apk
pkg: /data/local/tmp/old.apk
Success

安裝完成后運行,和之前一樣操作的流程。

end。

其他

實際生產中的升級過程還會涉及到很多邏輯處理細節,比如下載完成之后的MD5校驗,下載前判斷本地是否存在更新文件等,但基本的更新方式和流程如上所示。

Demo附完整源碼,源碼里還附了一些工具類處理一些細節。

運行時只需按照文章之前所示,修改服務器信息即可進行相應的升級方式。

Demo源碼地址

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,813評論 25 708
  • 1.概述 1.1.什么是應用增量更新 當我們要更新一個應用的時候,以前很多更新的做法是下載一個新版本去覆蓋一個舊版...
    揚靈閱讀 3,219評論 8 19
  • 在前幾年,整體移動網絡環境相比現在差很多,加之流量費用又相對較高,因此每當我們發布新版本的時候,一些用戶升級并不是...
    涅槃1992閱讀 5,513評論 2 39
  • 轉自《人人都是產品經理》,原文鏈接:寫給產品經理技術書 產品經理有三大領域的技術是需要去攻克的,分別是:客戶端相關...
    游社長閱讀 4,164評論 4 79
  • 進行中的工作; 流程中的工作. 利特爾法則顯示了在制品和前置時間之間的關系. 軟件開發中的在制品可以是: -尚未實...
    觀止_上海閱讀 601評論 0 1