Android增量更新与CMake构建工具

前些天鸿洋的公众号推送了一篇文章《Android 增量更新完全解析 是增量不是热修复》,研究增量更新的热情被激发了,通过几天的资料查找和学习,搞懂增量更新之余,也顺便练习了下NDK开发。(小小吐槽下鸿洋那篇文章,坑留得蛮多的,哈哈)

效果图预览

screenshot

开发环境

  • Android Studio 2.2.1 For Windows
  • CMake
  • Cygwin

一、更新Android Studio 2.2.1,安装NDK

最新的Android Studio 2.2集成了CMake构建工具,并支持在C++打断点,听说在NDK开发上比以前更方便快捷,在创建工程时就可以选择C++支持。


在Android Studio界面点击Tools-->Android-->SDN Manager-->点击SDK Tools标签-->勾选CMake、LLDB、NDK-->确认即可安装NDK环境


二、创建工程,下载bsdiff和bzip2

  • 创建一个工程,勾选Include C++ Support,Android Studio会在main目录创建cpp文件夹,里边有个native-lib.cpp的C++文件;在app目录还有个CMakeLists.txt文件,这个文件类似过去的Android.mk;在module的build.gradle中标示了采用CMake构建方式,并设置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目录,为构建添加一个子路径
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函数名为patch_main,添加JNI函数:
…………

#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_)是下面我们要创建的BsPatchJNI类的JNI函数名,com_whoisaa_apkpatchdemo为包名请对应地修改
(1)第一个参数表示JNI环境本身
(2)第二个参数,当方法静态时为jclass,否则为jobject类型

最后的cpp目录是这样子的:


三、创建Java方法

  • 创建BsPatchJNI.java,用来合成增量文件
public class BsPatchJNI {

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

    /**
     * 将增量文件合成为新的Apk
     * @param oldApkPath 当前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) {
                //并行任务
                new ApkUpdateTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
            }
        });
    }

    /**
     * 合并增量文件任务
     */
    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());
//                //检验文件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);
    }

}
  • 创建ApkUtils.java,用来获取当前Apk路径和安装新的Apk文件
public class ApkUtils {

    /**
     * 获取当前应用的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);
        }
    }
}
  • 创建SignUtils.java,用来校验增量文件和合成的新Apk文件MD5值是否与服务器给的值相同
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卡操作权限和网络权限
<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文件中的缩进也还是报错。最后我在Cygwin中下载了bsdiff组件,顺利运行bsdiff命令。
    在这里使用的Cygwin下载源是:http://mirrors.163.com/cygwin/x86_64/
  • 然后使用命令生成增量文件:
bsdiff old.apk new.apk old-to-new.patch
  • 把这个增量文件放在服务器或SD卡中(测试),我们可以在Cygwin中查看patch文件和新Apk包的MD5值,然后运行App合成新Apk,对比下两个MD5是一致的,表示这次合成增量文件是OK的!


五、总结###

为了搞定这个增量更新,花了好几天时间,现在终于把很多东西都理清楚了,原先不太熟悉的NDK也有了小进步,一切都是值得的。

  • 之前失败过很多次,都是因为CMake语法的不熟悉,这里有一个很赞很赞的CMake文档(中文):http://pan.baidu.com/s/1jI2RWqE,写这篇文章时我也还没看完,接下来会花时间好好研究。
  • 曾经试过直接loadLibrary别人Demo中的so文件,最后失败了。就是因为JNI函数包名与当前工程包名不同,找不到对应JNI函数导致的。很想知道百度地图这些so文件如何让别人调用的,知道的朋友可以说下,谢谢!
  • 在一个悠闲的公司有利有弊,只希望自己在技术上不止步,继续向前!

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


参考链接:
1、NDK开发基础④增量更新之客户端合并差分包
2、在 Android Studio 2.2 中愉快地使用 C/C++
3、AndroidStudio2.2下利用CMake编译方式的NDK opencv开发
4、CMake 手册详解(六)


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 增量更新在Android开发中是一种很常见的技术。 增量更新的原理 增量更新的原理非常简单,就是将本地apk与服务...
    re冷星阅读 1,525评论 3 3
  • Android游戏开发实践(1)之NDK与JNI开发02 承接上篇Android游戏开发实践(1)之NDK与JNI...
    AlphaGL阅读 3,686评论 0 24
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 思念如果有重量 我想把它 称一称 然后像发布空气质量 一样发布出来 月底的时候 再汇制出一份曲线图来 寄给远方的她...
    成都老王阅读 257评论 0 0
  • 失眠了!
    浸水的烟阅读 160评论 0 0