增量更新實踐

定義

所謂的增量更新,區別於傳統的更新方式,將新的apk全部下載下來後安裝覆蓋掉舊的版本,增量更新只需要下載一個差分包即可,然後把下載的差分包和原來舊的apk進行合成,生成一個新的apk,這樣就可以極大的減少升級新版本所需要的流量,下面就通過一個簡單的demo來了解一下什麼時增量更新。

如果想更深層次的瞭解增量更新的原理,可以參考下面兩個博客

http://blog.csdn.net/hmg25/article/details/8100896
http://blog.csdn.net/tu_bingbing/article/details/8538592

  • 首先需要準備生成差分包的工具

http://download.csdn.net/detail/hmg25/4676737

下載後解壓如圖
這裏寫圖片描述

  • 下面就是具體的代碼了
    有兩個別人寫好的工具類ApkUtils,PatchUtils
package com.cundong.utils;

import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.text.TextUtils;

import java.util.Iterator;
import java.util.List;

/**
 * 類說明:  Apk工具類
 *
 * @author Cundong
 * @version 1.1
 * @date
 */
public class ApkUtils {

    /**
     * 獲取已安裝apk的PackageInfo
     *
     * @param context
     * @param packageName
     * @return
     */
    public static PackageInfo getInstalledApkPackageInfo(Context context, String packageName) {
        PackageManager pm = context.getPackageManager();
        List<PackageInfo> apps = pm.getInstalledPackages(PackageManager.GET_SIGNATURES);

        Iterator<PackageInfo> it = apps.iterator();
        while (it.hasNext()) {
            PackageInfo packageinfo = it.next();
            String thisName = packageinfo.packageName;
            if (thisName.equals(packageName)) {
                return packageinfo;
            }
        }

        return null;
    }

    /**
     * 判斷apk是否已安裝
     *
     * @param context
     * @param packageName
     * @return
     */
    public static boolean isInstalled(Context context, String packageName) {
        PackageManager pm = context.getPackageManager();
        boolean installed = false;
        try {
            pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES);
            installed = true;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return installed;
    }

    /**
     * 獲取已安裝Apk文件的源Apk文件
     *
     * @param context
     * @param packageName
     * @return
     */
    public static String getSourceApkPath(Context context, String packageName) {
        if (TextUtils.isEmpty(packageName))
            return null;

        try {
            ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(packageName, 0);
            return appInfo.sourceDir;
        } catch (NameNotFoundException e) {
            e.printStackTrace();
        }

        return null;
    }

    /**
     * 安裝Apk
     *
     * @param context
     * @param apkPath
     */
    public static void installApk(Context context, String apkPath) {

        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(Uri.parse("file://" + apkPath),
                "application/vnd.android.package-archive");

        context.startActivity(intent);
    }

    /**
     * 獲取版本名稱
     *
     * @param context 上下文
     * @return 版本名稱
     */
    public static String getVersionName(Context context) {
        PackageManager pm = context.getPackageManager();
        try {
            PackageInfo packageInfo = pm.getPackageInfo(context.getPackageName(), 0);
            return packageInfo.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }

    /* 獲取版本號
    * @param context 上下文
    * @return 版本號
    */
    public static int getVersionCode(Context context) {
        PackageManager pm = context.getPackageManager();
        try {
            PackageInfo packageInfo = pm.getPackageInfo(context.getPackageName(), 0);
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 0;
    }
}
package com.cundong.utils;

/**
 * 類說明:     APK Patch工具類
 * 
 * @author  Cundong
 * @date    2013-9-6
 * @version 1.0
 */
public class PatchUtils {

    /**
     * native方法 使用路徑爲oldApkPath的apk與路徑爲patchPath的補丁包,合成新的apk,並存儲於newApkPath
     * 
     * 返回:0,說明操作成功
     * 
     * @param oldApkPath 示例:/sdcard/old.apk
     * @param newApkPath 示例:/sdcard/new.apk
     * @param patchPath  示例:/sdcard/xx.patch
     * @return
     */
    public static native int patch(String oldApkPath, String newApkPath, String patchPath);
}

注意這兩個類一定要放在com.cundong.utils的包中,以爲這裏用到了一個native方法,使用了一個so庫實現合成新的apk
然後將so庫拷貝到項目中

  • MainActivity的代碼,首先是舊的版本
package com.demo.patch;

import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

import com.cundong.utils.ApkUtils;
import com.cundong.utils.PatchUtils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

public class MainActivity extends AppCompatActivity {
    static {
        System.loadLibrary("ApkPatchLibrary");//加載so庫
    }

    private final String NEW_APK_PATH = Environment.getExternalStorageDirectory() + File.separator + "patchdemo.apk";
    private final String PATCH_PATH = Environment.getExternalStorageDirectory() + File.separator + "patchdemo.patch";
    private final String URL = "http://192.168.126.1:8080/patchdemo.patch";
    private final int SUCCESS = 5;
    private final int FAIL = 6;
    FileOutputStream fos = null;
    HttpURLConnection conn = null;
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case SUCCESS:
                    //彈出Toast,提示合併成功,安裝apk
                    Toast.makeText(MainActivity.this, "合併成功", Toast.LENGTH_SHORT).show();
                    ApkUtils.installApk(MainActivity.this, NEW_APK_PATH);
                    break;
                default:
                    Toast.makeText(MainActivity.this, "合併失敗", Toast.LENGTH_SHORT).show();
                    break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView textView = (TextView) findViewById(R.id.tv);
        textView.setText("舊的版本" + ApkUtils.getVersionName(this) + "    " + ApkUtils.getVersionCode(this));
        new Thread(new Runnable() {
            @Override
            public void run() {
                loadApk();
            }
        }).start();
        findViewById(R.id.bt).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                updateApk();
            }
        });
    }

    private void updateApk() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                //獲取已經安裝過的舊的apk的路徑
                String oldApkSource = ApkUtils.getSourceApkPath(MainActivity.this, getPackageName());
                //合併舊的apk和補丁成爲新的apk
                int patchResult = PatchUtils.patch(oldApkSource, NEW_APK_PATH, PATCH_PATH);
                int what;
                if (patchResult == 0) {
                    what = SUCCESS;
                } else {
                    what = FAIL;
                }
                handler.obtainMessage(what).sendToTarget();
            }
        }).start();
    }

    private void loadApk() {
        try {
            File file = new File(PATCH_PATH);
            if (file.exists()) {
                file.delete();
            }
            URL url = new URL(URL);
            conn = (HttpURLConnection) url.openConnection();
            conn.setReadTimeout(5000);
            if(conn.getResponseCode()==HttpURLConnection.HTTP_OK){
                InputStream is=conn.getInputStream();
                fos=new FileOutputStream(file);
                byte[] b=new byte[1024];
                int len=0;
                while((len=is.read(b))!=-1){  //先讀到內存
                    fos.write(b, 0, len);
                }
                fos.flush();
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(MainActivity.this,"下載成功了",Toast.LENGTH_LONG).show();
                    }
                });
            }
        } catch (final Exception e) {
            e.printStackTrace();
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(MainActivity.this,e.toString(),Toast.LENGTH_LONG).show();
                }
            });
        }
    }
}

界面就很簡單了,一個TextView,一個Button。運行這個工程,然後去build/outputs/apk/文件夾下把生成的apk文件拷貝出來放到之前下載的工具的bsdiff4.3-win32文件夾中,並改名爲patchdemo-old.apk
效果圖:
這裏寫圖片描述

  • 更改textview的顯示內容爲
TextView textView = (TextView) findViewById(R.id.tv);
        textView.setText("已經更新到最新版本了" + ApkUtils.getVersionName(this) + "    " + ApkUtils.getVersionCode(this));

然後去build.gradle中把versioncode和versionname都做相應的更改,運行項目,生成新的apk文件,拷貝到工具bsdiff4.3-win32文件夾中,改名爲patchdemo-new.apk,效果圖:
這裏寫圖片描述

  • 打開dos窗口,進入bsdiff4.3-win32路徑下,執行命令 bsdiff.exe patchdemo-old.apk patchdemo-new.apk patchdemo.patch 就會生成一個以.patch結尾的文件,如圖:
    這裏寫圖片描述
    將這個.patch文件放在自己的的服務器上,我這裏用的是tomcat

  • 把新的apk從手機上卸載,把舊的apk安裝上,當提示下載成功後,點擊安裝,就會執行合成新apk的代碼,跳轉到安裝頁面,如圖;這裏寫圖片描述

  • 點擊安裝,就會把和合成後新的版本覆蓋掉舊的版本,之後打開應用就是最新的版本了,效果圖:
    這裏寫圖片描述

一個簡單的增量更新的demo就完成了,這裏主要的難點在於通過舊的版本和差分包合成新的版本,這一部風代碼都是通過調用so庫中的函數實現的。這裏沒有進行版本的判斷,以及多個差分包的情況。

github 代碼倉庫:https://github.com/githubyzd/PathDemo

發佈了28 篇原創文章 · 獲贊 27 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章