Android 實現增量更新

咱們知道隨着功能不斷增長,apk 的體積也會不斷增大。若是每次更新都須要用戶下載全新的 apk 覆蓋用戶手機老的版本的話,會浪費用戶的流量,也會增長服務器帶寬。要想實現此需求的話,就須要瞭解一下 bisdiff/bspatch 。顧名思義,diff 就是經過算法計算兩個文件獲得差別包,patch 就是補丁(通過 diff 後的差別包),能夠經過 patch 將源文件和補丁文件組合成新的文件,使得用戶無需下載全新的文件,而只下載補丁文件就可獲得新的文件啦!java

補丁文件應該放在服務器端使用,用戶端經過正常的更新方式去下載補丁文件。下面的實現方案都在本地,不模擬從服務器下載補丁文件的流程。筆者是 Mac OS,讀者可使用 linux 系統來測試,好比 Ubuntu、Centos 等等。linux

Mac 下載 bsdiff/bspatch

下載連接 bsdiff/bspatch,當前下載的版本是 4.3。下載到本地後解壓可看到以下文件:android

能夠看到其實就只有兩個 C 源文件,還提供了 Makefile 文件,既然提供了 Makefile 源文件,那麼咱們就能夠執行 make 命令。如圖所示:git

這是由於 Makefile 文件中,命令前面沒有使用 tab 鍵,這個是 makefile 的語法規則。如圖所示:github

修改後在執行就能夠看到生成了可執行文件 bspatch、bsdiff算法

固然若是是 Mac 系統,你可能還會遇到一個報錯,找不到 u_char。這個時候須要在 bspatch.c 中加入shell

#ifdef __APPLE__ 
#include <sys/types.h> 
#endif

複製代碼

到此環境就已經配置完畢,咱們接下來看 Android 如何實現。數組

Android 中實現增量更新

  1. 咱們新建一個 Android 工程,這裏我選擇的是默認的空白工程,固然你也可使用支持 C/C++ 的工程建立。

  1. 添加 bspatch 源碼, 不須要 bsdiff,由於 Android 不須要實現生成補丁,只須要根據補丁文件合成新的 apk 便可。 首先分析一下 bspatch.c
#include <bzlib.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <err.h>
#include <unistd.h>
#include <fcntl.h>

.... 省略...

複製代碼

咱們都知道通常狀況下尖括號<> 都是系統的頭文件,可是這裏有個特殊的地方, 就是 #include <bzlib.h>, 這個頭文件系統並不存在,而須要咱們引入它的源碼。它的源碼下載地址 bzip2, 直接搜索關鍵字 bzip2 而後選擇,如圖所示:bash

將其解壓後的文件下圖所示:服務器

能夠看到有不少文件,既有 C 源文件,也有一些其餘文件,固然咱們這裏只關心 C 源文件。但是也發現有不少文件,一個辦法就是將其所有拷入,另外一種方式就是查看 Makefile 文件,看看其如何構建的。OK, 那咱們就來看看 Makefile

SHELL=/bin/sh

# To assist in cross-compiling
# 交叉編譯相關的工具
CC=gcc
AR=ar
RANLIB=ranlib
LDFLAGS=

BIGFILES=-D_FILE_OFFSET_BITS=64
# 傳遞給編譯器的指令
CFLAGS=-Wall -Winline -O2 -g $(BIGFILES)

# Where you want it installed when you do 'make install'
# 將其安裝到 /url/local 下
PREFIX=/usr/local


# OBJS 變量,這裏是關鍵,能夠看到 ***.o 文件其實就是經過 .c 源文件編譯獲得,這裏他們就會看成一個個目標來用。
OBJS= blocksort.o  \
      huffman.o    \
      crctable.o   \
      randtable.o  \
      compress.o   \
      decompress.o \
      bzlib.o

# 通常狀況下,開源項目都會在 Makefile 中提供 all 目標,它告訴須要那些目標來構建最終的可執行文件
all: libbz2.a bzip2 bzip2recover test

# bzip2 目標依賴 libbz2.a bzip2.o
bzip2: libbz2.a bzip2.o
	# 這裏就調用了 CC 編譯器以及指定一些參數,還有連接庫
	$(CC) $(CFLAGS) $(LDFLAGS) -o bzip2 bzip2.o -L. -lbz2

bzip2recover: bzip2recover.o
	$(CC) $(CFLAGS) $(LDFLAGS) -o bzip2recover bzip2recover.o

# 根據 OBJS 生成 libbz2.a
libbz2.a: $(OBJS)
	rm -f libbz2.a
	$(AR) cq libbz2.a $(OBJS)
	@if ( test -f $(RANLIB) -o -f /usr/bin/ranlib -o \
		-f /bin/ranlib -o -f /usr/ccs/bin/ranlib ) ; then \
		echo $(RANLIB) libbz2.a ; \
		$(RANLIB) libbz2.a ; \
	fi

check: test
test: bzip2
	@cat words1
	./bzip2 -1  < sample1.ref > sample1.rb2
	./bzip2 -2  < sample2.ref > sample2.rb2
	./bzip2 -3  < sample3.ref > sample3.rb2
	./bzip2 -d  < sample1.bz2 > sample1.tst
	./bzip2 -d  < sample2.bz2 > sample2.tst
	./bzip2 -ds < sample3.bz2 > sample3.tst
	cmp sample1.bz2 sample1.rb2 
	cmp sample2.bz2 sample2.rb2
	cmp sample3.bz2 sample3.rb2
	cmp sample1.tst sample1.ref
	cmp sample2.tst sample2.ref
	cmp sample3.tst sample3.ref
	@cat words3

# 安裝到 /usr/local/bin
install: bzip2 bzip2recover
	if ( test ! -d $(PREFIX)/bin ) ; then mkdir -p $(PREFIX)/bin ; fi
	if ( test ! -d $(PREFIX)/lib ) ; then mkdir -p $(PREFIX)/lib ; fi
	if ( test ! -d $(PREFIX)/man ) ; then mkdir -p $(PREFIX)/man ; fi
	if ( test ! -d $(PREFIX)/man/man1 ) ; then mkdir -p $(PREFIX)/man/man1 ; fi
	if ( test ! -d $(PREFIX)/include ) ; then mkdir -p $(PREFIX)/include ; fi
	cp -f bzip2 $(PREFIX)/bin/bzip2
	cp -f bzip2 $(PREFIX)/bin/bunzip2
	cp -f bzip2 $(PREFIX)/bin/bzcat
	cp -f bzip2recover $(PREFIX)/bin/bzip2recover
	chmod a+x $(PREFIX)/bin/bzip2
	chmod a+x $(PREFIX)/bin/bunzip2
	chmod a+x $(PREFIX)/bin/bzcat
	chmod a+x $(PREFIX)/bin/bzip2recover
	cp -f bzip2.1 $(PREFIX)/man/man1
	chmod a+r $(PREFIX)/man/man1/bzip2.1
	cp -f bzlib.h $(PREFIX)/include
	chmod a+r $(PREFIX)/include/bzlib.h
	cp -f libbz2.a $(PREFIX)/lib
	chmod a+r $(PREFIX)/lib/libbz2.a
	cp -f bzgrep $(PREFIX)/bin/bzgrep
	ln -s -f $(PREFIX)/bin/bzgrep $(PREFIX)/bin/bzegrep
	ln -s -f $(PREFIX)/bin/bzgrep $(PREFIX)/bin/bzfgrep
	chmod a+x $(PREFIX)/bin/bzgrep
	cp -f bzmore $(PREFIX)/bin/bzmore
	ln -s -f $(PREFIX)/bin/bzmore $(PREFIX)/bin/bzless
	chmod a+x $(PREFIX)/bin/bzmore
	cp -f bzdiff $(PREFIX)/bin/bzdiff
	ln -s -f $(PREFIX)/bin/bzdiff $(PREFIX)/bin/bzcmp
	chmod a+x $(PREFIX)/bin/bzdiff
	cp -f bzgrep.1 bzmore.1 bzdiff.1 $(PREFIX)/man/man1
	chmod a+r $(PREFIX)/man/man1/bzgrep.1
	chmod a+r $(PREFIX)/man/man1/bzmore.1
	chmod a+r $(PREFIX)/man/man1/bzdiff.1
	echo ".so man1/bzgrep.1" > $(PREFIX)/man/man1/bzegrep.1
	echo ".so man1/bzgrep.1" > $(PREFIX)/man/man1/bzfgrep.1
	echo ".so man1/bzmore.1" > $(PREFIX)/man/man1/bzless.1
	echo ".so man1/bzdiff.1" > $(PREFIX)/man/man1/bzcmp.1

clean: 
	rm -f *.o libbz2.a bzip2 bzip2recover \
	sample1.rb2 sample2.rb2 sample3.rb2 \
	sample1.tst sample2.tst sample3.tst

# 目標依賴,經過 CC 命令生成, 也就是主要使用了以下的幾個源文件
blocksort.o: blocksort.c
	@cat words0
	$(CC) $(CFLAGS) -c blocksort.c
huffman.o: huffman.c
	$(CC) $(CFLAGS) -c huffman.c
crctable.o: crctable.c
	$(CC) $(CFLAGS) -c crctable.c
randtable.o: randtable.c
	$(CC) $(CFLAGS) -c randtable.c
compress.o: compress.c
	$(CC) $(CFLAGS) -c compress.c
decompress.o: decompress.c
	$(CC) $(CFLAGS) -c decompress.c
bzlib.o: bzlib.c
	$(CC) $(CFLAGS) -c bzlib.c
bzip2.o: bzip2.c
	$(CC) $(CFLAGS) -c bzip2.c
bzip2recover.o: bzip2recover.c
	$(CC) $(CFLAGS) -c bzip2recover.c

.... 省略 ....


複製代碼

簡單分析了下 Makefile,咱們能夠知道須要的源文件有以下的幾個, 如圖所示:

可是咱們不須要 bzip2.c, 由於不須要調用 bzip2 來壓縮文件。只須要將以下的導入到 Android Studio 中便可,如圖所示:

筆者這裏用的是 ndk-build 的方式來進行構建,固然你也可使用 CMamke 的方式。Android.mk 配置以下:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

# 導入 bzlib 下全部的頭文件
LOCAL_C_INCLUDES := bzlib

# 模塊名稱
LOCAL_MODULE := bspatch

# 若是換行,須要用換行符\, 而後前面必需要有一個 tab 鍵
LOCAL_SRC_FILES := bspatch_native.cpp bzlib/bspatch.c bzlib/blocksort.c bzlib/huffman.c bzlib/crctable.c bzlib/randtable.c bzlib/compress.c bzlib/decompress.c bzlib/bzlib.c

# 連接系統的 log 日誌庫
LOCAL_LDLIBS := -llog

# 生成動態庫
include $(BUILD_SHARED_LIBRARY)

複製代碼

Application.mk 文件只有一行配置,也能夠刪掉這個文件,在app 下的 build.gradle 中配置過濾。

# 生成 armeabi-v7a 平臺
APP_ABI := armeabi-v7a

複製代碼

配置 app 下的 build.gradle 文件

android {
    .....

    externalNativeBuild {
        ndkBuild {
            // 必需要加入這行,指定 ndk-build 查找到 Andorid.mk 路徑
            path 'src/main/jni/Android.mk'
        }
    }
}

複製代碼

點擊 Build 下 Refresh Linked C++ Projects, 如圖:

若是看到頭文件都不報紅,也能夠正常運行起來就能夠接着下一步。

  1. 編寫本地方法,須要經過補丁生成新的 apk 的命令格式爲 bspatch oldapk newapk patch

那咱們本地方法的的參數也就能夠肯定了,方法聲明以下:

public native void generateNewApkByPatch(String oldApkFile, String newApkFile, String patchFile);

複製代碼

本地方法如何生成呢? 首先讀者須要瞭解如下 JNI 頭文件規則,不瞭解的能夠看個人其餘文章。這裏直接給出頭文件定義。

#include <jni.h>

extern "C"
JNIEXPORT void JNICALL
Java_com_hxj_bsdiffdemo_MainActivity_generateNewApkByPatch(JNIEnv *env, jobject jobj,
        jstring old_apk_file_, jstring new_apk_file_, jstring patch_file_);



void Java_com_hxj_bsdiffdemo_MainActivity_generateNewApkByPatch(JNIEnv *env, jobject jobj,
        jstring old_apk_file_, jstring new_apk_file_, jstring patch_file_) {
    const char *old_apk_file = env->GetStringUTFChars(old_apk_file_, NULL);
    const char *new_apk_file = env->GetStringUTFChars(new_apk_file_, NULL);
    const char *patch_file = env->GetStringUTFChars(patch_file_, NULL);

    // 執行 dspatch 操做
    //TODO 

    env->ReleaseStringUTFChars(old_apk_file_, old_apk_file);
    env->ReleaseStringUTFChars(new_apk_file_, new_apk_file);
    env->ReleaseStringUTFChars(patch_file_, patch_file);
}

複製代碼

請務必記得 GetStringUTFChars 和 ReleaseStringUTFChars 成對出現, 防止內存泄漏。

接下來咱們只要調用 bspatch 提供的函數便可,咱們能夠知道它有入口函數 main, 而且提供了兩個參數

int main(int argc,char * argv[])
{
	FILE * f, * cpf, * dpf, * epf;
	BZFILE * cpfbz2, * dpfbz2, * epfbz2;
	int cbz2err, dbz2err, ebz2err;
	int fd;
	ssize_t oldsize,newsize;
	ssize_t bzctrllen,bzdatalen;
	u_char header[32],buf[8];
	u_char *old, *new;
	off_t oldpos,newpos;
	off_t ctrl[3];
	off_t lenread;
	off_t i;

    // 告訴咱們數組指針長度必須爲 4
	if(argc!=4) errx(1,"usage: %s oldfile newfile patchfile\n",argv[0]);

	/* Open patch file */
	if ((f = fopen(argv[3], "r")) == NULL)
		err(1, "fopen(%s)", argv[3]);

    .....

	return 0;
}

複製代碼

知道了須要傳入的參數,那麼咱們就將 JNI 本地函數修改,以下所示:

#include <jni.h>

extern int main(int argc,char * argv[]);

extern "C"
JNIEXPORT void JNICALL
Java_com_hxj_bsdiffdemo_MainActivity_generateNewApkByPatch(JNIEnv *env, jobject jobj,
        jstring old_apk_file_, jstring new_apk_file_, jstring patch_file_);



void Java_com_hxj_bsdiffdemo_MainActivity_generateNewApkByPatch(JNIEnv *env, jobject jobj,
        jstring old_apk_file_, jstring new_apk_file_, jstring patch_file_) {
    const char *old_apk_file = env->GetStringUTFChars(old_apk_file_, NULL);
    const char *new_apk_file = env->GetStringUTFChars(new_apk_file_, NULL);
    const char *patch_file = env->GetStringUTFChars(patch_file_, NULL);

    // 定一個數組指針,裏面存放都是 char * 指針
    char *args[4];

    // 拼接命令, 格式爲: bspatch old_apk_file new_apk_file patch_file
    args[0] = (char *)"bspatch";
    args[1] = (char *) old_apk_file;
    args[2] = (char *) new_apk_file;
    args[3] = (char *) patch_file;

    main(4, args);

    env->ReleaseStringUTFChars(old_apk_file_, old_apk_file);
    env->ReleaseStringUTFChars(new_apk_file_, new_apk_file);
    env->ReleaseStringUTFChars(patch_file_, patch_file);
}

複製代碼
  1. java 層函數實現, 前面完成了 JNI 本地函數編寫,接下來就就是 java 層業務邏輯處理啦!這一層邏輯就不贅述,詳情請結合註釋看代碼:
public class MainActivity extends AppCompatActivity {

    private Button mBtnUpdate;

    private TextView mTvVersion;

    static {
        System.loadLibrary("bspatch");
    }

    public native void generateNewApkByPatch(String oldApkFile, String newApkFile, String patchFile);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mBtnUpdate = findViewById(R.id.btn_update);
        mTvVersion = findViewById(R.id.tv_version);

        PackageManager packageManager = getPackageManager();
        try {
            PackageInfo packageInfo = packageManager.getPackageInfo(getPackageName(), 0);
            String versionName = packageInfo.versionName;
            Log.i("James", "onCreate versionName: " + versionName);
            mTvVersion.setText("當前的版本:" + versionName);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }

        mBtnUpdate.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                // 檢查是否有寫 sdcard 權限
                if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                        != PackageManager.PERMISSION_GRANTED) {
                    if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
                            Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                        // 自定義提示框.
                    } else {
                        ActivityCompat.requestPermissions(MainActivity.this,
                                new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
                                100);
                    }
                } else {
                    update();
                }
            }
        });
    }

    private void update() {

        new UpdateApkAsyckTask().execute();
    }

    class UpdateApkAsyckTask extends AsyncTask<Void, Void, File> {

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
        }

        @Override
        protected File doInBackground(Void... voids) {
            // 獲取當前 apk 安裝的路徑.
            String oldApkFilePath = getApplicationInfo().sourceDir;

            // 新 apk 所在目錄.
            File newApkFile = new File(Environment.getExternalStorageDirectory(), "test_new.apk");
            boolean newApkFileExist = false;
            if (!newApkFile.exists()) {
                try {
                    newApkFileExist = newApkFile.createNewFile();
                } catch (IOException e) {
                    newApkFileExist = false;
                    e.printStackTrace();
                }
            } else {
                newApkFileExist = true;
            }

            if (!newApkFileExist) {
                Log.e("James", "doInBackground new apk file 文件不存在.");
                return null;
            }

            // 服務器下載後的補丁文件
            File patchFile = new File(Environment.getExternalStorageDirectory(), "test.patch");
            if (!patchFile.exists()) {
                Log.e("James", "doInBackground 暫未發現新版本.");
                return null;
            }

            // 獲取新的 apk 安裝路徑.
            String newApkFilePath = newApkFile.getAbsolutePath();

            // 獲取補丁文件路徑
            String patchFilePath = patchFile.getAbsolutePath();

            // 調用 JNI 函數生成新的 apk 
            generateNewApkByPatch(oldApkFilePath, newApkFilePath, patchFilePath);

            return newApkFile;
        }

        @Override
        protected void onPostExecute(File file) {

            if (file == null || !file.exists()) return;

            Log.i("James", "onPostExecute: " + file.getTotalSpace());
            // 安裝 apk, 請注意 7.0 以上須要 authorities 
            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            if (Build.VERSION.SDK_INT >= 24) {
                //Android 7.0及以上
                // 參數2 清單文件中provider節點裏面的authorities ; 參數3  共享的文件,即apk包的file類
                Uri apkUri = FileProvider.getUriForFile(MainActivity.this,
                        getApplicationInfo().packageName + ".provider", file);
                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
            } else {
                intent.setDataAndType(Uri.fromFile(file),
                        "application/vnd.android.package-archive");
            }
            startActivity(intent);
        }
    }
}


複製代碼

下面來看測試,首先咱們看到版本爲 1.0 而且界面顏色爲白色。

生成補丁包

經過 adb 工具上傳到 sdcard 目錄,經常使用命令以下:

adb devices  查看當前鏈接設備

adb shell 進入到手機 shell 環境, 若是有若是手機鏈接,可使用 adb -s 設備名 shell 進入

adb install -r xxx.apk 強制安裝,去掉 r 普通安裝

adb push  abc.txt  /sdcard/   將 abc.txt 上傳到 /sdcard/目錄下

複製代碼

上傳到 sdcard 下,如圖所示:

最後一步,點擊更新按鈕,能夠看到開始安裝

點擊打開後,能夠看到版本爲 2.0, 界面背景也變了顏色。

最主要的是,test.patch 很小,只有幾百k。

好了,到此就已經所有介紹完畢。其實還算簡單的,須要一些 NKD 開發基礎,ndk-build 工具的使用,以及 Makefile 的語法。

須要代碼的請 點擊

相關文章
相關標籤/搜索