【Android OTA】應(yīng)用的更新升級(jí)

timg-2.jpeg

原文鏈接

Android應(yīng)用經(jīng)常會(huì)內(nèi)置檢測(cè)版本更新的功能供搀,在有版本更新的時(shí)候隅居,通過(guò)下載更新文件進(jìn)行本地的升級(jí)。本文通過(guò)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的Demo葛虐,來(lái)介紹App的更新升級(jí)方式胎源。提供更新信息的服務(wù)器用到了上一篇文章實(shí)現(xiàn)的服務(wù)器Demo

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

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

應(yīng)用更新前

1.jpg

更新完成后

2.jpg

應(yīng)用比較簡(jiǎn)單涕蚤,關(guān)鍵是更新的流程,看背景色既可知更新是否成功的诵。

App實(shí)現(xiàn)

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

主要的界面就是一列表西疤,列表有2個(gè)選項(xiàng)

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>

列表項(xiàng)初始化烦粒,點(diǎn)擊列表選項(xiàng)做對(duì)應(yīng)的跳轉(zhuǎn)

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 為更新升級(jí)界面,進(jìn)入該界面時(shí)會(huì)自動(dòng)向服務(wù)器進(jìn)行更新信息的請(qǐng)求瘪阁,并將請(qǐng)求的結(jié)果顯示撒遣。

3.jpg

新建 AsyncTask<Void, Void, String> 子類 UpdateCheckTask,用來(lái)做后臺(tái)網(wǎng)絡(luò)請(qǐng)求管跺。

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

將獲取到的服務(wù)器返回?cái)?shù)據(jù)做json解析义黎,并返回給UpdateActivity

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

parseJson 函數(shù)為UpdateCheckTask中實(shí)現(xiàn)的解析方法,具體見(jiàn)源碼豁跑。

UpdateCheckTaskonCreate中調(diào)用更新請(qǐng)求

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

檢測(cè)到更新后廉涕,界面出現(xiàn)下載按鈕,點(diǎn)擊按鈕艇拍,下載指定url中的更新文件狐蜕,顯示下載進(jìn)度條

4.jpg

新建 DownloadService 用來(lái)處理下載流程,DownloadService 繼承自 IntentService

DownloadService 通過(guò)指定地址將文件下載到本地

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

下載過(guò)程中將進(jìn)度以消息的方式通知UpdateActivity以更新進(jìn)度條

...

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

...

UpdateActivity 監(jiān)聽(tīng)進(jìn)度消息

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

UpdateActivity 獲取到消息時(shí)更新進(jìn)度條

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 的具體實(shí)現(xiàn)

private void installAPk(File apkFile) {
    Intent intent = new Intent(Intent.ACTION_VIEW);
    //如果沒(méi)有設(shè)置SDCard寫(xiě)權(quán)限贡羔,或者沒(méi)有sdcard,apk文件保存在內(nèi)存中,需要授予權(quán)限才能安裝
    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本身自帶接口个初,當(dāng)打開(kāi).apk格式的文件時(shí)跳轉(zhuǎn)到安裝界面乖寒,主要實(shí)現(xiàn)為這行代碼

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

安裝界面

5.jpg

若安裝文件有問(wèn)題,系統(tǒng)會(huì)提示解析錯(cuò)誤

6.jpg

完全更新

為了區(qū)分完全更新和增量更新院溺,服務(wù)器返回的json數(shù)據(jù)增加一個(gè)diffUpdate字段,用來(lái)區(qū)分文件是.apk文件還是差分包文件

修改服務(wù)器的info數(shù)據(jù)

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

完全更新時(shí)逐虚,diffUpdate 值設(shè)為 true
url 設(shè)置為 /ota_file/update.zip

應(yīng)用做些修改痊班,修改主界面的背景色涤伐,增加一行代碼

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

對(duì)應(yīng)的項(xiàng)目版本信息修改

8.jpg

然后運(yùn)行Android Studio Build Apk,生成的Apk文件在項(xiàng)目目錄下

9.jpg

將apk文件拷貝到 ota服務(wù)器項(xiàng)目的 ota_file 文件夾下型雳,并修改文件名為 update.zip

然后將修改的代碼回退纠俭,運(yùn)行舊的版本冤荆,點(diǎn)擊版本更新钓简,下載更新文件完成后進(jìn)行安裝

10.jpg

安裝完成后外邓,重新打開(kāi)應(yīng)用古掏,便可以看到主界面的背景顏色改變了槽唾。

增量更新

增量更新的原理,就是將手機(jī)上已安裝apk與服務(wù)器端最新apk進(jìn)行二進(jìn)制對(duì)比,得到差分包挂绰,用戶更新程序時(shí)葵蒂,只需要下載差分包秦士,并在本地使用差分包與已安裝apk隧土,合成新版apk曹傀。

apk文件的差分皆愉、合成可以通過(guò)開(kāi)源的二進(jìn)制比較工具 bsdiff實(shí)現(xiàn)幕庐。

處理差分包的代碼實(shí)現(xiàn)

項(xiàng)目中引入第三方動(dòng)態(tài)庫(kù) libApkPatchLibrary.so

引入對(duì)應(yīng)的包 com.cundong.utils.PatchUtils

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

DownloadService 下載完成時(shí)家淤,根據(jù)服務(wù)器提供的 diffUpdate 字段來(lái)判斷是否為差分包文件,若是媒鼓,則將差分包文件與當(dāng)前的apk合成新的apk,并安裝新的apk

安裝前合成代碼的實(shí)現(xiàn)

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

應(yīng)用當(dāng)前的apk會(huì)保存在系統(tǒng)特定的位置,通過(guò) getBaseApkPath 方法獲取路徑潮模,方法的具體實(shí)現(xiàn)

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

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

生成差分包文件

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

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

bsdiff
bsdiff: usage: bsdiff oldfile newfile patchfile

可看到該命令接收3個(gè)參數(shù)节榜,依次為舊的文件新的文件生成的差分包文件

和之前Build Apk方式一樣讳窟,先將未修改前的應(yīng)用編一個(gè)apk文件,命名為old.apk

然后修改應(yīng)用的背景色和版本信息蛇数,重新編一個(gè)apk文件挪钓,命名為new.apk

將新舊兩個(gè)apk文件拷貝到同一目錄下,命令行cd到當(dāng)前目錄下耳舅,運(yùn)行命令

old.apk new.apk patch.zip

完成后可看到在當(dāng)前目錄下生成了新文件 patch.zip碌上,將patch.zip拷貝到服務(wù)器的ota_file文件夾下

服務(wù)器返回的信息做對(duì)應(yīng)修改

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

diffUpdate 改為 trueurl 改為差分包文件

運(yùn)行

之前的運(yùn)行都是通過(guò)Android Studio直接連手機(jī)真機(jī)跑起來(lái)的浦徊,但是調(diào)試差分包升級(jí)時(shí)需要注意馏予,差分包必須保證本地應(yīng)用的apk文件與生成差分包時(shí)的old.apk文件完全一致,否則合成新的apk會(huì)失敗盔性。

而每次通過(guò)Android Studio連接手機(jī)編起來(lái)的應(yīng)用所生成的apk文件都是不一樣的霞丧,雖然代碼一模一樣。

為了測(cè)試差分包冕香,手機(jī)必須通過(guò)舊的apk來(lái)安裝并運(yùn)行應(yīng)用

adb 命令安裝

adb install -r old.apk 

結(jié)果打印

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

安裝完成后運(yùn)行蛹尝,和之前一樣操作的流程。

end悉尾。

其他

實(shí)際生產(chǎn)中的升級(jí)過(guò)程還會(huì)涉及到很多邏輯處理細(xì)節(jié)突那,比如下載完成之后的MD5校驗(yàn),下載前判斷本地是否存在更新文件等构眯,但基本的更新方式和流程如上所示愕难。

Demo附完整源碼,源碼里還附了一些工具類處理一些細(xì)節(jié)惫霸。

運(yùn)行時(shí)只需按照文章之前所示猫缭,修改服務(wù)器信息即可進(jìn)行相應(yīng)的升級(jí)方式。

Demo源碼地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末壹店,一起剝皮案震驚了整個(gè)濱河市猜丹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌硅卢,老刑警劉巖射窒,帶你破解...
    沈念sama閱讀 216,651評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件妖混,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡轮洋,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門抬旺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)弊予,“玉大人,你說(shuō)我怎么就攤上這事开财『浩猓” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,931評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵责鳍,是天一觀的道長(zhǎng)碾褂。 經(jīng)常有香客問(wèn)我,道長(zhǎng)历葛,這世上最難降的妖魔是什么正塌? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,218評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮恤溶,結(jié)果婚禮上乓诽,老公的妹妹穿的比我還像新娘。我一直安慰自己咒程,他們只是感情好鸠天,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著帐姻,像睡著了一般稠集。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上饥瓷,一...
    開(kāi)封第一講書(shū)人閱讀 51,198評(píng)論 1 299
  • 那天剥纷,我揣著相機(jī)與錄音,去河邊找鬼扛伍。 笑死筷畦,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的刺洒。 我是一名探鬼主播鳖宾,決...
    沈念sama閱讀 40,084評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼逆航!你這毒婦竟也來(lái)了鼎文?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,926評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤因俐,失蹤者是張志新(化名)和其女友劉穎拇惋,沒(méi)想到半個(gè)月后周偎,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,341評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡撑帖,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評(píng)論 2 333
  • 正文 我和宋清朗相戀三年蓉坎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胡嘿。...
    茶點(diǎn)故事閱讀 39,731評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蛉艾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出衷敌,到底是詐尸還是另有隱情勿侯,我是刑警寧澤,帶...
    沈念sama閱讀 35,430評(píng)論 5 343
  • 正文 年R本政府宣布缴罗,位于F島的核電站助琐,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏面氓。R本人自食惡果不足惜兵钮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望侧但。 院中可真熱鬧矢空,春花似錦、人聲如沸禀横。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,676評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)柏锄。三九已至酿箭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間趾娃,已是汗流浹背缭嫡。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,829評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留抬闷,地道東北人妇蛀。 一個(gè)月前我還...
    沈念sama閱讀 47,743評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像笤成,于是被迫代替她去往敵國(guó)和親评架。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容