Android增量更新與CMake構(gòu)建工具

前些天鴻洋的公眾號推送了一篇文章《Android 增量更新完全解析 是增量不是熱修復(fù)》,研究增量更新的熱情被激發(fā)了邻辉,通過幾天的資料查找和學(xué)習(xí)厦瓢,搞懂增量更新之余窖梁,也順便練習(xí)了下NDK開發(fā)瘤袖。(小小吐槽下鴻洋那篇文章,坑留得蠻多的玖喘,哈哈)

效果圖預(yù)覽

screenshot

開發(fā)環(huán)境

  • Android Studio 2.2.1 For Windows
  • CMake
  • Cygwin

一澎办、更新Android Studio 2.2.1,安裝NDK

最新的Android Studio 2.2集成了CMake構(gòu)建工具窖逗,并支持在C++打斷點(diǎn)址否,聽說在NDK開發(fā)上比以前更方便快捷,在創(chuàng)建工程時就可以選擇C++支持碎紊。


在Android Studio界面點(diǎn)擊Tools-->Android-->SDN Manager-->點(diǎn)擊SDK Tools標(biāo)簽-->勾選CMake佑附、LLDB、NDK-->確認(rèn)即可安裝NDK環(huán)境


二仗考、創(chuàng)建工程音同,下載bsdiff和bzip2

  • 創(chuàng)建一個工程,勾選Include C++ Support痴鳄,Android Studio會在main目錄創(chuàng)建cpp文件夾瘟斜,里邊有個native-lib.cpp的C++文件缸夹;在app目錄還有個CMakeLists.txt文件,這個文件類似過去的Android.mk螺句;在module的build.gradle中標(biāo)示了采用CMake構(gòu)建方式虽惭,并設(shè)置CMakeLists.txt路徑。
//定義工程名稱
PROJECT(bzip2)
  • 將app目錄下的CMakeLists.txt文件移動到cpp目錄谱邪,并將其修改為:
# Sets the minimum version of CMake required to build the native
# library. You should either keep the default value or only pass a
# value of 3.4.0 or lower.

#CMake版本信息
cmake_minimum_required(VERSION 3.4.1)

#支持-std=gnu++11
set(CMAKE_VERBOSE_MAKEFILE on)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11 -Wall -DGLM_FORCE_SIZE_T_LENGTH")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DGLM_FORCE_RADIANS")

#添加bzip2目錄炮捧,為構(gòu)建添加一個子路徑
set(bzip2_src_DIR ${CMAKE_SOURCE_DIR})
add_subdirectory(${bzip2_src_DIR}/bzip2)

#cpp目錄下待編譯的bspatch.c文件
add_library( # Sets the name of the library.
             bspatch

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             # Associated headers in the same location as their source
             # file are automatically included.
             bspatch.c )

# Searches for a specified prebuilt library and stores the path as a
# variable. Because system libraries are included in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in the
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
                       bspatch

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

  • 將module的build.gradle中的CMakeLists.txt路徑改為:
externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
        }
    }
  • 修改cpp/bspatch.c文件,加入bzip2的頭文件包含惦银,修改main函數(shù)名為patch_main咆课,添加JNI函數(shù):
…………

#include <sys/types.h>
#include <jni.h>

// bzip2
#include "bzip2/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"

…………

int bspatch_main(int argc,char * argv[])
{
    …………
}

JNIEXPORT jint JNICALL
               Java_com_whoisaa_apkpatchdemo_BsPatchJNI_patch(JNIEnv *env, jclass type, jstring oldApkPath_,
                                                              jstring newApkPath_, jstring patchPath_) {
    const char *oldApkPath = (*env)->GetStringUTFChars(env, oldApkPath_, 0);
    const char *newApkPath = (*env)->GetStringUTFChars(env, newApkPath_, 0);
    const char *patchPath = (*env)->GetStringUTFChars(env, patchPath_, 0);

    // TODO
    int argc = 4;
    char* argv[4];
    argv[0] = "bspatch";
    argv[1] = oldApkPath;
    argv[2] = newApkPath;
    argv[3] = patchPath;

    int ret = bspatch_main(argc, argv);

    (*env)->ReleaseStringUTFChars(env, oldApkPath_, oldApkPath);
    (*env)->ReleaseStringUTFChars(env, newApkPath_, newApkPath);
    (*env)->ReleaseStringUTFChars(env, patchPath_, patchPath);

    return ret;
}

注意:*Java_com_whoisaa_apkpatchdemo_BsPatchJNI_patch(JNIEnv env, jclass type, jstring oldApkPath_,jstring newApkPath_, jstring patchPath_)是下面我們要創(chuàng)建的BsPatchJNI類的JNI函數(shù)名,com_whoisaa_apkpatchdemo為包名請對應(yīng)地修改
(1)第一個參數(shù)表示JNI環(huán)境本身
(2)第二個參數(shù)扯俱,當(dāng)方法靜態(tài)時為jclass书蚪,否則為jobject類型

最后的cpp目錄是這樣子的:


三、創(chuàng)建Java方法

  • 創(chuàng)建BsPatchJNI.java迅栅,用來合成增量文件
public class BsPatchJNI {

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

    /**
     * 將增量文件合成為新的Apk
     * @param oldApkPath 當(dāng)前Apk路徑
     * @param newApkPath 合成后的Apk保存路徑
     * @param patchPath 增量文件路徑
     * @return
     */
    public static native int patch(String oldApkPath, String newApkPath, String patchPath);
}
  • 在MainActivity中使用:
public class MainActivity extends AppCompatActivity {

    public static final String SDCARD_PATH = Environment.getExternalStorageDirectory() + File.separator;
    public static final String PATCH_FILE = "old-to-new.patch";
    public static final String NEW_APK_FILE = "new.apk";

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

        findViewById(R.id.btn_main).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //并行任務(wù)
                new ApkUpdateTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
            }
        });
    }

    /**
     * 合并增量文件任務(wù)
     */
    private class ApkUpdateTask extends AsyncTask<Void, Void, Boolean> {

        @Override
        protected Boolean doInBackground(Void... params) {
            String oldApkPath = ApkUtils.getCurApkPath(MainActivity.this);
            File oldApkFile = new File(oldApkPath);
            File patchFile = new File(getPatchFilePath());
            if(oldApkFile.exists() && patchFile.exists()) {
                Log("正在合并增量文件...");
                String newApkPath = getNewApkFilePath();
                BsPatchJNI.patch(oldApkPath, newApkPath, getPatchFilePath());
//                //檢驗(yàn)文件MD5值
//                return Signtils.checkMd5(oldApkFile, MD5);

                Log("增量文件的MD5值為:" + SignUtils.getMd5ByFile(patchFile));
                Log("新文件的MD5值為:" + SignUtils.getMd5ByFile(new File(newApkPath)));

                return true;
            }
            return false;
        }

        @Override
        protected void onPostExecute(Boolean result) {
            super.onPostExecute(result);
            if(result) {
                Log("合并成功殊校,開始安裝");
                ApkUtils.installApk(MainActivity.this, getNewApkFilePath());
            } else {
                Log("合并失敗");
            }
        }
    }

    private String getPatchFilePath() {
        return SDCARD_PATH + PATCH_FILE;
    }

    private String getNewApkFilePath() {
        return SDCARD_PATH + NEW_APK_FILE;
    }

    /**
     * 打印日志
     * @param log
     */
    private void Log(String log) {
        Log.e("MainActivity", log);
    }

}
  • 創(chuàng)建ApkUtils.java,用來獲取當(dāng)前Apk路徑和安裝新的Apk文件
public class ApkUtils {

    /**
     * 獲取當(dāng)前應(yīng)用的Apk路徑
     * @param context 上下文
     * @return
     */
    public static String getCurApkPath(Context context) {
        context = context.getApplicationContext();
        ApplicationInfo applicationInfo = context.getApplicationInfo();
        String apkPath = applicationInfo.sourceDir;
        return apkPath;
    }

    /**
     * 安裝Apk
     * @param context 上下文
     * @param apkPath Apk路徑
     */
    public static void installApk(Context context, String apkPath) {
        File file = new File(apkPath);
        if(file.exists()) {
            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
            context.startActivity(intent);
        }
    }
}
  • 創(chuàng)建SignUtils.java读存,用來校驗(yàn)增量文件和合成的新Apk文件MD5值是否與服務(wù)器給的值相同
public class SignUtils {

    /**
     * 判斷文件的MD5值是否為指定值
     * @param file1
     * @param md5
     * @return
     */
    public static boolean checkMd5(File file1, String md5) {
        if(TextUtils.isEmpty(md5)) {
            throw new RuntimeException("md5 cannot be empty");
        }

        if(file1 != null && file1.exists()) {
            String file1Md5 = getMd5ByFile(file1);
            return file1Md5.equals(md5);
        }
        return false;
    }

    /**
     * 獲取文件的MD5值
     * @param file
     * @return
     */
    public static String getMd5ByFile(File file) {
        String value = null;
        FileInputStream in = null;
        try {
            in = new FileInputStream(file);

            MessageDigest digester = MessageDigest.getInstance("MD5");
            byte[] bytes = new byte[8192];
            int byteCount;
            while ((byteCount = in.read(bytes)) > 0) {
                digester.update(bytes, 0, byteCount);
            }
            value = bytes2Hex(digester.digest());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != in) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return value;
    }

    private static String bytes2Hex(byte[] src) {
        char[] res = new char[src.length * 2];
        final char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
        for (int i = 0, j = 0; i < src.length; i++) {
            res[j++] = hexDigits[src[i] >>> 4 & 0x0f];
            res[j++] = hexDigits[src[i] & 0x0f];
        }

        return new String(res);
    }
}
  • 最后在AndroidManifest.xml中加入SD卡操作權(quán)限和網(wǎng)絡(luò)權(quán)限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.INTERNET"/>

四为流、生成增量文件###

  • 一開始我用的是鴻洋文章說的方法,在Cygwin中使用make生成bsdiff和bspatch文件让簿,可惜失敗了艺谆,修改Makefile文件中的縮進(jìn)也還是報(bào)錯。最后我在Cygwin中下載了bsdiff組件拜英,順利運(yùn)行bsdiff命令。
    在這里使用的Cygwin下載源是:http://mirrors.163.com/cygwin/x86_64/
  • 然后使用命令生成增量文件:
bsdiff old.apk new.apk old-to-new.patch
  • 把這個增量文件放在服務(wù)器或SD卡中(測試)琅催,我們可以在Cygwin中查看patch文件和新Apk包的MD5值居凶,然后運(yùn)行App合成新Apk,對比下兩個MD5是一致的藤抡,表示這次合成增量文件是OK的侠碧!


五、總結(jié)###

為了搞定這個增量更新缠黍,花了好幾天時間弄兜,現(xiàn)在終于把很多東西都理清楚了,原先不太熟悉的NDK也有了小進(jìn)步,一切都是值得的替饿。

  • 之前失敗過很多次语泽,都是因?yàn)镃Make語法的不熟悉,這里有一個很贊很贊的CMake文檔(中文):http://pan.baidu.com/s/1jI2RWqE视卢,寫這篇文章時我也還沒看完踱卵,接下來會花時間好好研究。
  • 曾經(jīng)試過直接loadLibrary別人Demo中的so文件据过,最后失敗了惋砂。就是因?yàn)镴NI函數(shù)包名與當(dāng)前工程包名不同,找不到對應(yīng)JNI函數(shù)導(dǎo)致的绳锅。很想知道百度地圖這些so文件如何讓別人調(diào)用的西饵,知道的朋友可以說下,謝謝鳞芙!
  • 在一個悠閑的公司有利有弊眷柔,只希望自己在技術(shù)上不止步,繼續(xù)向前积蜻!

Github源碼:https://github.com/WhoIsAA/ApkPatchDemo


參考鏈接:
1闯割、NDK開發(fā)基礎(chǔ)④增量更新之客戶端合并差分包
2、在 Android Studio 2.2 中愉快地使用 C/C++
3竿拆、AndroidStudio2.2下利用CMake編譯方式的NDK opencv開發(fā)
4宙拉、CMake 手冊詳解(六)


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市丙笋,隨后出現(xiàn)的幾起案子谢澈,更是在濱河造成了極大的恐慌,老刑警劉巖御板,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件锥忿,死亡現(xiàn)場離奇詭異,居然都是意外死亡怠肋,警方通過查閱死者的電腦和手機(jī)敬鬓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來笙各,“玉大人钉答,你說我怎么就攤上這事¤厩溃” “怎么了数尿?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長惶楼。 經(jīng)常有香客問我右蹦,道長诊杆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任何陆,我火速辦了婚禮晨汹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘甲献。我一直安慰自己宰缤,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布晃洒。 她就那樣靜靜地躺著慨灭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪球及。 梳的紋絲不亂的頭發(fā)上氧骤,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天,我揣著相機(jī)與錄音吃引,去河邊找鬼筹陵。 笑死,一個胖子當(dāng)著我的面吹牛镊尺,可吹牛的內(nèi)容都是我干的朦佩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼庐氮,長吁一口氣:“原來是場噩夢啊……” “哼语稠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起弄砍,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤仙畦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后音婶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體慨畸,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年衣式,在試婚紗的時候發(fā)現(xiàn)自己被綠了寸士。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡碴卧,死狀恐怖碉京,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情螟深,我是刑警寧澤,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布烫葬,位于F島的核電站界弧,受9級特大地震影響凡蜻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜垢箕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一划栓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧条获,春花似錦忠荞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至修档,卻和暖如春碧绞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背吱窝。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工讥邻, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人院峡。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓兴使,卻偏偏與公主長得像,于是被迫代替她去往敵國和親照激。 傳聞我的和親對象是個殘疾皇子发魄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評論 2 355

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