1. 程式人生 > >Android開發之增量更新

Android開發之增量更新

一、使用場景

apk升級,節省伺服器和使用者的流量

二、原理

自從 Android 4.1 開始, Google Play 引入了應用程式的增量更新功能,App使用該升級方式,可節省約2/3的流量。

這裡寫圖片描述

現在國內主流的應用市場也都支援應用的增量更新了,最常見的應用寶省流量更新。

增量更新的原理,就是將手機上已安裝apk與伺服器端最新apk進行二進位制對比,得到差分包(即兩個版本的差異檔案),使用者更新程式時,只需要下載差分包,並在本地使用差分包與已安裝apk,合成新版apk。

例如,當前手機中已安裝微博V1,大小為12.8MB,現在微博釋出了最新版V2,大小為15.4MB,我們對兩個版本的apk檔案差分比對之後,發現差異只有3M(可能更小,因為得到差異檔案後內部還會使用壓縮),那麼使用者就只需要要下載一個3M的差分包,使用舊版apk與這個差分包,合成得到一個新版本apk,提醒使用者安裝即可,不需要整包下載15.4M的微博V2版apk。

三、過程

此過程需要伺服器和客戶端寫作完成

伺服器:拿到最新版的apk,called new.apk,舊版本的apk,called old.apk,通過增量更新技術得到差分檔案,called patch.patch。

客戶端:通過網路操作去伺服器下載已準備好的差分檔案patch.patch,找到data目錄下當前版本的old.apk,通過增量更新技術合併這兩個檔案,得到new.apk。

四、例項講解

上述過程需要用到伺服器得到差分檔案,但是尷尬了,我不會,所以我使用投機取巧的方式,新建一個工程通過NDK去得到差分檔案,然後通過在另外一個專案中通過NDK去合併,當然無論是伺服器還是通過模擬我們最終是需要知道是怎麼的過程具體實現。

4.1 準備工作

1.NDK開發技術,還不會的,或者會的歡迎檢視我NDK相關文章:點選進入

2.我們自己需要準備兩個apk,一個old.apk,一個new.apk,用於模擬伺服器進行差分,old.apk只是用了TextView顯示當前為1.0的舊版本,在old.apk的基礎上new.apk在當前目錄下的res/drawable-hdpi目錄下增加了一些圖片以便模擬容量擴從,並將版本號修改為2,TextView顯示為2.0新版本,這樣就簡單的得到了2個新舊版本。當然,這個操作非常簡單,因此我們提供了資源下載:old.apk與new.apk下載。

3.用於差分new.apk與old.apk以及合併old.apk與patch.patch的bsdiff檔案,又因為bsdiff依賴bzip2,所以我們還需要用到 bzip2
bsdiff中,bsdiff.c 用於生成差分包,bspatch.c 用於合成檔案。這些檔案都是c語言寫的,所以需要使用NDK技術: 本地下載

4.2 模擬伺服器得到差分檔案patch.patch

注以下開發環境都是eclipse。

1.新建Android專案,隨後new一個source folder 命名為jni,並做好此專案的NDK配置,這裡不講解,

2.然後將準備工作下載的bsdiff中的bsdiff.c,bsdiff.h,bzip2資料夾拷貝到jni目錄下,

3.新建類,類名當然隨意,用於載入動態庫以及生成native方法,如下:

package com.example.serverpatch;

public class PatchAPK {
    public native static int patchAPK(String oldFile,String newFile,String patchFile);
    static{
        System.loadLibrary("server_patch");
    }
}

當然這裡的server_patch必須與後面Android.mk檔案中的so庫名字一致。並且patchAPK方法需要3個引數,分別為舊apk的路徑,新apk的路徑,差分檔案的路徑。

4.通過cmd定位到此專案的src目錄下,輸入javah +3步驟中建立的類包名,例如:

javah com.example.serverpatch.PatchAPK

然後重新整理專案,可以看到:
這裡寫圖片描述

將其也複製到jni目錄下

5.將com.example.serverpatch.PatchAPK.h中需要實現的方法複製到bsdiff.c中實現:
這裡寫圖片描述
當然這裡需要給這些JNIEnv,變數命名久不用說了吧。

然後我們看看具體怎麼實現:

JNIEXPORT jint JNICALL Java_com_example_serverpatch_PatchAPK_patchAPK
(JNIEnv *env, jclass cls, jstring oldFile, jstring newFile, jstring patchFile){
    int argc=4;
    char *argv[argc];
    argv[0] = "bsdiff";
    argv[1] = (char*) ((*env)->GetStringUTFChars(env, oldFile, 0));
    argv[2] = (char*) ((*env)->GetStringUTFChars(env, newFile, 0));
    argv[3] = (char*) ((*env)->GetStringUTFChars(env, patchFile, 0));

    printf("old apk = %s \n", argv[1]);
    printf("new apk = %s \n", argv[2]);
    printf("patch = %s \n", argv[3]);

    int ret = diff_main(argc, argv);

    printf("diff_main result = %d ", ret);

    (*env)->ReleaseStringUTFChars(env, oldFile, argv[1]);
    (*env)->ReleaseStringUTFChars(env, newFile, argv[2]);
    (*env)->ReleaseStringUTFChars(env, patchFile, argv[3]);

    return ret;
}

生成差分檔案我們需要用到是bsdiff.c中的main方法,因為我將其修改為diff_main,所以大家不必鬱悶哪裡的diff_main()函式,main函式需要兩個引數,第一個固定為4,第二個為 char* 資料,但是我們傳入的是Java中的string,所以我們首先通過NDK中的GetStringUTFChars將其轉為 char* ,因為java中有GC,但是C語言必須自己釋放通過ReleaseStringUTFChars釋放。

6.將bsdiff.c中的 < bzlib.h >修改為如下:

#include "bzip2/bzlib.c"
#include "bzip2/crctable.c"
#include "bzip2/compress.c"
#include "bzip2/decompress.c"
#include "bzip2/randtable.c"
#include "bzip2/blocksort.c"
#include "bzip2/huffman.c"

因為此時的bzlib在本地,所以使用雙引號。

6.編寫Android.mk以及Application.mk。這裡不貼出。只要記住生成的.so和載入的.so名稱一致。

最終的專案樣式如下:
這裡寫圖片描述

首先在MainActivity不做任何事情,通過模擬器執行一把專案,我是通過模擬器。真機可以不用執行,因為我只是想將old.apk和new.apk放到模擬器的根目錄下,不行拿不到它的根目錄了啊,執行完成後將準備好的old.apk,與new.apk放到SDcard目錄下,如我的模擬器:
這裡寫圖片描述

可以看到new.apk有5M,old.apk有1M,如果不使用增量更新,現在我們需要使用5M的流量更新。

好的,現在在MainActivity中呼叫PatchAPK 類中的navtive方法:

package com.example.serverpatch;

import java.io.File;
import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getPatchAPK();
    }

    private void getPatchAPK() {
         if (Environment.getExternalStorageState() != null) {
             File sdFile=Environment.getExternalStorageDirectory();
             String sdString=sdFile.getAbsolutePath();
             PatchAPK.patchAPK(sdString+"/old.apk", sdString+"/new.apk", sdString+"/patch.patch");
         }

    }
}

我們找到剛才 放置於SD根目錄下的new.apk與old.apk的目錄,並新增差分檔案目錄,當然呼叫
PatchAPK.patchAPK(sdString+”/old.apk”, sdString+”/new.apk”, sdString+”/patch.patch”);
需要在子執行緒中,這裡沒有演示,主要理解原理。最後執行專案。

可以看到SD的根目錄下多了一個patch.patch檔案:
這裡寫圖片描述

可以看到只有3M多點,到了這裡我們模擬伺服器拿到差分檔案已經完成。

4.3 客戶端合差分檔案

到了這裡離成功已經特別近了,所以我們不能氣餒!

1.新建客戶端專案,隨之後面的操作和剛才模擬模擬伺服器製作差分檔案的步驟幾乎是一致,所以我們不用擔心程式碼量,可以一直複製,畢竟這是程式設計師必備技能。與模擬伺服器中不同的是:

  • 將bspatch.c,bspatch.h放到jni目錄下,而不是bsdiff系列,當然bzip2檔案
    夾仍保留
  • 新建類hebingAPK,當然我這裡命名不規範,別介意,關鍵看原理:
package com.example.patch;

public class hebingAPK {
    public native static int hbAPK(String oldFile,String newFile,String patchFile);
    static{
        System.loadLibrary("client_patch");
    }
}

通過javah命令生成標頭檔案,在bspatch.c中實現具體合併,

JNIEXPORT jint JNICALL Java_com_example_patch_hebingAPK_hbAPK
(JNIEnv *env, jclass cls,jstring old, jstring new, jstring patch){
    int argc = 4;
        char * argv[argc];
        argv[0] = "bspatch";
        argv[1] = (char*) ((*env)->GetStringUTFChars(env, old, 0));
        argv[2] = (char*) ((*env)->GetStringUTFChars(env, new, 0));
        argv[3] = (char*) ((*env)->GetStringUTFChars(env, patch, 0));

        printf("old apk = %s \n", argv[1]);
        printf("patch = %s \n", argv[3]);
        printf("new apk = %s \n", argv[2]);

        int ret = hb_main(argc, argv);

        printf("patch result = %d ", ret);

        (*env)->ReleaseStringUTFChars(env, old, argv[1]);
        (*env)->ReleaseStringUTFChars(env, new, argv[2]);
        (*env)->ReleaseStringUTFChars(env, patch, argv[3]);
        return ret;
}

可以看到合併和差分的程式碼NDK程式碼幾乎一致,只是這裡使用的main函式為bspatch.c中的main,將其修改為hb_main函式,最後當然也需要將將bsdiff.c中的 include < bzlib.h >導包修改。

最終專案目錄樣式:
這裡寫圖片描述

最後在MainActivity中呼叫native程式碼:

public class MainActivity extends Activity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_layout);
        getPatchAPK();
    }

    private void getPatchAPK() {
         if (Environment.getExternalStorageState() != null) {
             File sdFile=Environment.getExternalStorageDirectory();
             String sdString=sdFile.getAbsolutePath();
             hebingAPK.hbAPK(sdString+"/old.apk", sdString+"/new2.apk", sdString+"/patch.patch");
         }

    }
}

當然,這裡合併和差分一樣也是需要在子執行緒中進行,這裡也不演示了,只要看原理。

等會我們就需要看生成的new2.apk是否可以安裝並且大小是否和new.apk是否一樣,如果有必要可以通過校驗新合成的apk的MD5或SHA1是否正確,如正確,則引導使用者安裝,當然我們這裡不演示這個。

最後執行,模擬器根目錄如下:
這裡寫圖片描述

可以看到new2.apk成功生成了,好的到此我們的差分和合並全部完成,覺得可以的點個贊好嗎,謝謝。

4.3 安裝new2.apk

當然這一部是最簡單的,通過adb命令去安裝。

首先將new2.apk pull到電腦中,開啟CMD,定位到new2.apk目錄下,通過adb install new2.apk將其安裝:
這裡寫圖片描述

可以看到安裝成功,開啟模擬器也是可以使用,說明這一套的差分和合並是成功的。

最後提供一下兩個過程的原始碼:gitHub下載