Android增量更新与CMake构建工具
来源:互联网 发布:sqlserver实例是什么 编辑:程序博客网 时间:2024/04/29 08:24
- 简书同步更新:http://www.jianshu.com/p/776612b5be8a
前些天鸿洋的公众号推送了一篇文章《Android 增量更新完全解析 是增量不是热修复》,研究增量更新的热情被激发了,通过几天的资料查找和学习,搞懂增量更新之余,也顺便练习了下NDK开发。(小小吐槽下鸿洋那篇文章,坑留得蛮多的,哈哈)
效果图预览
开发环境
- 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路径。
下载bsdiff工具,以及依赖的bzip2工具
bsdiff官网:http://www.daemonology.net/bsdiff/
bsdiff 4.3下载地址:http://www.daemonology.net/bsdiff/bsdiff-4.3.tar.gz
bzip2官网:http://www.bzip.org/downloads.html
bzip2-1.0.6下载地址:http://www.bzip.org/1.0.6/bzip2-1.0.6.tar.gz- 删除cpp下的native-lib.cpp,解压bsdiff和bzip2,将bsdiff-4.3目录下的bspatch.c复制到cpp目录,将bzip2-1.0.6目录复制到cpp目录并重命名为bzip2,在bzip2目录下创建CMakeLists.txt文件(需要确保每个目录都存在一个CMakeLists.txt),添加以下内容(参考:CMake语法学习笔记:http://blog.csdn.net/myatlantis/article/details/53073736):
//定义工程名称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++11set(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 手册详解(六)
5、CMake 手册详解(二十二)
- Android增量更新与CMake构建工具
- Android Studio NDK 构建工具 CMake初探
- linux Cmake构建工具
- Android中的增量更新与热修复
- 增量更新与HotFix
- android 增量更新应用
- Android 增量更新实例
- android 增量更新
- android增量更新demo
- android实现增量更新
- android 增量更新
- Android 增量更新实例
- Android增量更新
- Android 增量更新APK
- Android APP增量更新
- Android之增量更新
- android增量更新
- Android增量更新
- WampServer 3.0.6安装错误解决办法汇总
- 一:express框架学习之路由控制与中间件
- oracle 11g手工建库
- CUDA的内存结构,通过实例展示寄存器和共享内存的使用
- AndroidStudio 编辑面板中的各种文件快速在Project视图中选中(图片资源,java类,xml等)
- Android增量更新与CMake构建工具
- 错误1error C2248: “Point::count”: 无法访问 private 成员(在“Point”类中声明)
- Android下分析内存泄露
- 解决windows server 2012 的mstdc.exe程序占100%cpu问题
- C语言,不定参数的使用
- 线程同步
- 重建二叉树
- SQL Server中实现数据换行
- org.apache.hadoop.yarn.exceptions.YarnException: Unauthorized request to start container