Android NDK开发之旅17 NDK Apk增量更新
前言
有關APK更新的技術比較多,例如:增量更新、插件式開發(fā)、熱修復、RN、靜默安裝。 下面簡單介紹一下:
| 增量更新 | 舊版本Apk(v1.0)和新(v2.0)、舊版本Apk(v1.0)生成的差分包(apk.patch 質量小) 合并成為新版本Apk(v2.0)安裝。 |
| 插件式開發(fā) | 給宿主APK提供插件,擴展(需要的時候再下載),可以動態(tài)地替換。主要技術是動態(tài)代理的知識。 |
| 熱修復 | 通過NDK底層去修復,也是C/C++的技術。 |
| RN | 通過JS腳本去修復APK。 |
| 靜默安裝 | 需要root權限,適配不同手機ROM很麻煩。 |
什么是增量更新?
增量更新就是原有app的基礎上只更新發(fā)生變化的地方,其余保持原樣。 與原來每次更新都要下載完整apk包的做法相比,這樣做的好處顯而易見:每次變化的地方總是比較少,因此更新包的體積就會小很多。
增量更新的流程
1.APP檢測最新版本:把當前版本告訴服務端,服務端進行判斷。 如果有新版本,服務端需要對當前版本的APK與最新版本的APK進行一次差分,產生patch差分文件。(或者新版本的APK上傳到服務端的時候就已經差分好了) 2.APP在后臺下載差分文件,進行文件的MD5校驗,在本地進行合并(跟本地的data目錄下面的APK文件合并),合并出最新的APK之后,提示用戶安裝。 3.增量更新的最終目的:省流量地更新宿主APK。
差分的處理比較麻煩的地方就是要針對不同的應用市場渠道和眾多不同版本進行差分。
注意:新版本有可能比舊版本小,差分只是把變化的部分記錄下來。
服務器端行為(后臺工程師操作)
1.下載拆分和合并要用的第三方庫(bsdiff、bzip2)
我們使用到的第三方庫是:Binary diff,簡稱bsdiff,這個庫專門用來實現(xiàn)文件的差分和合并的,它的官網(wǎng)如下: www.daemonology.net/bsdiff/
在這里我們可以點擊文中的"here"下載源碼,這是Linux源碼。也可以下載Windows版本的源碼,點擊"Windows port"。
建議Windows 下用sbsdiff4.3-win32-src編譯
這個庫引用了bzip2這個庫,官網(wǎng)如下: www.bzip.org/
2.編譯第三方庫源碼生成dll動態(tài)庫
為了方便演示,我在Windows 10平臺下用VS2017編譯,實際情況服務器大都在Linux系統(tǒng)下運行,這個大家去測試吧。
Windows 下生成dll動態(tài)庫參考 Android NDK開發(fā)之旅10--JNI--JNI開發(fā)流程
注意:com_haocai_bsdiff_BsDiff.h 是根據(jù)Java文件聲明得到的,步驟省略。
編譯過程中會有以下錯誤提示
- 字符集問題
- 用了不安全和過時的函數(shù)
- SDL檢查不通過
以下是解決辦法:
另外,可能報頭文件找不到的錯誤,這有可能是編碼問題,因為外國人使用的蘋果電腦跟Windows電腦的編譯不一致產生的。可以通過Notepad++的轉碼功能進行轉碼,全部轉為UTF-8無BOM格式編碼即可,Windows、Linux通用的。
我們項目屬性里面的生成配置里面選擇DLL,并且修改解決方案為你的電腦的對應平臺,然后編譯,生成DLL動態(tài)庫文件。
3.Java代碼調用
創(chuàng)建Web項目,用來做APP的服務端。創(chuàng)建工具類專門用于產生差分包:
public class BsDiff {/*** 差分* @param oldfile* @param newfile* @param patchfile*/public native static void diff(String oldfile,String newfile,String patchfile);static {System.loadLibrary("bsdiff");} } 復制代碼其中JNI的實現(xiàn)如下(該實現(xiàn)寫在bsdiff.cpp中):
JNIEXPORT void JNICALL Java_com_haocai_bsdiff_BsDiff_diff (JNIEnv *env, jclass jcls, jstring oldfile_jstr, jstring newfile_jstr, jstring patchfile_jstr) {int argc = 4;char* oldfile = (char*)env->GetStringUTFChars(oldfile_jstr, NULL);char* newfile = (char*)env->GetStringUTFChars(newfile_jstr, NULL);char* patchfile = (char*)env->GetStringUTFChars(patchfile_jstr, NULL);//參數(shù)(第一個參數(shù)無效)char *argv[4];argv[0] = { "bsdiff" };argv[1] = oldfile;argv[2] = newfile;argv[3] = patchfile;bsdiff_main(argc, argv);env->ReleaseStringUTFChars(oldfile_jstr, oldfile);env->ReleaseStringUTFChars(newfile_jstr, newfile);env->ReleaseStringUTFChars(patchfile_jstr, patchfile); };復制代碼通過研究bsdiff的源碼,我們發(fā)現(xiàn)bsdiff.cpp里面的main函數(shù)就是入口函數(shù),避免歧義把函數(shù)名main改為bsdiff_main,然后通過JNI去調用。
根據(jù)bsdiff.cpp中bsdiff_main函數(shù)方法中有以下關鍵語句
if (argc != 4) errx(1, "usage: %s oldfile newfile patchfile\n", argv[0]); 復制代碼根據(jù)提示需要傳入4個參數(shù):
argv[0] = "bsdiff";//這個參數(shù)沒用argv[1] = oldPath;//舊APK文件路徑argv[2] = newPath;/新APK文件路徑argv[3] = patchPath;//APK差分文件路徑 復制代碼然后我們準備兩個APK文件,不同版本的,最好Java代碼、資源都不一樣。
寫一個Java測試類生成差分包:
package com.haocai.bsdiff;public class ConstantsWin {//路徑不能包含中文public static final String OLD_APK_PATH = "D:/android_apks/test_old.apk";public static final String NEW_APK_PATH = "D:/android_apks/test_new.apk";public static final String PATCH_PATH = "D:/android_apks/apk.patch"; }復制代碼package com.haocai.bsdiff;/*** Created by Administrator on 2017/11/14.*/ public class BsDiffTest {public static void main(String[] args){//得到差分包BsDiff.diff(ConstantsWin.OLD_APK_PATH,ConstantsWin.NEW_APK_PATH,ConstantsWin.PATCH_PATH);} } 復制代碼生成結果如下圖所示:
注意:
-
test_new.apk、test_old.apk 要先放在目標目錄
-
bsdiff.cpp中生成差分包的程序方法是異步的,所以生成完整的apk.patch可能要等一下。apk.patch體積大小停止增長,表示生成結束。
####4.簡單搭建后臺JavaWeb供Android前端下載apk.patch差分包
參考 Intellij idea創(chuàng)建javaWeb以及Servlet簡單實現(xiàn)
在瀏覽器中輸入
http://localhost:8080/App_Update_Web/patchfile/apk.patch
如圖,提示可以下載服務器搭建完畢。
#### Android客戶端行為
1.編譯合并要用的第三方庫(bsdiff、bzip2)
對應的Java代碼如下:
package com.haocai.app.update;/*** Created by Xionghu on 2017/11/14.* Desc:*/public class BsPatch {/*** 合并* @param oldfile* @param newfile* @param patchfile*/public native static void patch(String oldfile,String newfile,String patchfile);static {System.loadLibrary("bspatch");} } 復制代碼在Android端,我們需要把bzip2以及bsdiff的文件拷貝到jni目錄里面,同樣的,我們只需要編譯一個bspatch.c源文件即可。
由于Android手機本來就是Linux系統(tǒng),因此我們直接使用bsdiff的Linux版本的庫即可。 跟服務器端一樣,在這里我們把bspatch.c中的main函數(shù)改為bspatch_main,提供JNI調用:
//合并 JNIEXPORT void JNICALL Java_com_haocai_app_update_BsPatch_patch(JNIEnv *env, jclass jcls, jstring oldfile_jstr, jstring newfile_jstr, jstring patchfile_jstr){int argc = 4;char* oldfile = (char*)(*env)->GetStringUTFChars(env,oldfile_jstr, NULL);char* newfile = (char*)(*env)->GetStringUTFChars(env,newfile_jstr, NULL);char* patchfile = (char*)(*env)->GetStringUTFChars(env,patchfile_jstr, NULL);//參數(shù)(第一個參數(shù)無效)char *argv[4];argv[0] = "bspatch";argv[1] = oldfile;argv[2] = newfile;argv[3] = patchfile;bspatch_main(argc,argv);(*env)->ReleaseStringUTFChars(env,oldfile_jstr, oldfile);(*env)->ReleaseStringUTFChars(env,newfile_jstr, newfile);(*env)->ReleaseStringUTFChars(env,patchfile_jstr, patchfile);}復制代碼代碼v1.0差分包合并核心代碼如下:
package com.haocai.app.update;import android.Manifest; import android.content.pm.PackageManager; import android.os.Handler; import android.os.Message; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.ActivityCompat; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.text.format.Formatter; import android.widget.Toast; import com.lzy.okgo.OkGo; import com.lzy.okgo.callback.FileCallback; import com.lzy.okgo.model.Progress; import com.lzy.okgo.model.Response; import com.lzy.okgo.request.base.Request; import java.io.File; import java.text.NumberFormat;public class MainActivity extends AppCompatActivity {private static final int REQUEST_PERMISSION_STORAGE = 0x01;private Handler mHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);switch (msg.what) {case 0:Toast.makeText(MainActivity.this, "您正在進行省流量更新", Toast.LENGTH_SHORT).show();ApkUtils.installApk(MainActivity.this, Constants.NEW_APK_PATH);break;}}};private NumberFormat numberFormat;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);setTitle("簡單文件下載");numberFormat = NumberFormat.getPercentInstance();numberFormat.setMinimumFractionDigits(2);checkSDCardPermission();/*** 因為后臺沒有寫版本判斷語句* 在高版本下暫時先注釋fileDownload(); 否則一直下載安裝** 低版本下運行fileDownload();*/fileDownload();}/*** 檢查SD卡權限*/protected void checkSDCardPermission() {if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_PERMISSION_STORAGE);}}@Overridepublic void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {super.onRequestPermissionsResult(requestCode, permissions, grantResults);if (requestCode == REQUEST_PERMISSION_STORAGE) {if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {//獲取權限fileDownload();} else {Toast.makeText(getApplicationContext(), "權限被禁止,無法下載文件!", Toast.LENGTH_SHORT).show();}}}@Overrideprotected void onDestroy() {super.onDestroy();//Activity銷毀時,取消網(wǎng)絡請求OkGo.getInstance().cancelTag(this);}public void fileDownload() {OkGo.<File>get(Constants.URL_PATCH_DOWNLOAD)//.tag(this)//.execute(new FileCallback(Constants.SD_CARD, Constants.PATCH_FILE) {@Overridepublic void onStart(Request<File, ? extends Request> request) {}@Overridepublic void onSuccess(Response<File> response) {new Thread(new Runnable() {@Overridepublic void run() {try {// File patchFile = new File(Constants.SD_CARD, Constants.PATCH_FILE);String oldfile = ApkUtils.getSourceApkPath(MainActivity.this, getPackageName());String newfile = Constants.NEW_APK_PATH;String patchfile = Constants.SD_CARD + File.separator + Constants.PATCH_FILE;BsPatch.patch(oldfile, newfile, patchfile);mHandler.sendEmptyMessage(0);} catch (Exception e) {e.printStackTrace();}}}).start();}@Overridepublic void onError(Response<File> response) {}@Overridepublic void downloadProgress(Progress progress) {System.out.println(progress);String downloadLength = Formatter.formatFileSize(getApplicationContext(), progress.currentSize);String totalLength = Formatter.formatFileSize(getApplicationContext(), progress.totalSize);String speed = Formatter.formatFileSize(getApplicationContext(), progress.speed);System.out.println(downloadLength);}});}} 復制代碼主要的邏輯在fileDownload方法中,我們先下載差分包,然后在本地合成,最后提示用戶安裝。
為了達到明顯的效果,兩個版本可以增刪一些資源文件、修改Java代碼、布局文件等。
注意:這里7.0可能會有問題,把路徑暴露給別的app,需要FileProvider去實現(xiàn)(不難,這個留給大家去做吧)。
源碼下載:https://github.com/kpioneer123/DiffInstallApp
特別感謝: 動腦學院Jason
總結
以上是生活随笔為你收集整理的Android NDK开发之旅17 NDK Apk增量更新的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: TCOOP-M048-降压模块-78M0
- 下一篇: android sina oauth2.