美文网首页开发Android开发Android拓展
Android增量更新与CMake构建工具

Android增量更新与CMake构建工具

作者: Mr2eA | 来源:发表于2016-10-24 10:59 被阅读316次

    前些天鸿洋的公众号推送了一篇文章《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 手册详解(六)


    相关文章

      网友评论

        本文标题:Android增量更新与CMake构建工具

        本文链接:https://www.haomeiwen.com/subject/lgfluttx.html