Android调用jni方法--增量更新apk
介绍
这里是测试Android调用jni方法,通过一个增量更新的例子来测试。
这里简略说明一下什么是增量更新,我们发布产品的时候通常是先发布一个1.0的版本,然后后面需要修复bug或是增加新的功能等等,通常是发布2.0版本然后需要用户去下载2.0版本安装,每次都需要用户下载更新,这样导致用户体验不好,这个时候我们可以使用增量更新,先通过增量来比对1.0与2.0的区别,然后生成一个1.0_2.0.patch差分文件,然后用户的1.0的应用查看服务器有没有更新,有的话就下载这个差分文件,然后通过这个差分文件来比对用户手中的1.0apk文件(即使安装应用后删除apk也能获取到这个apk),然后就能生成一个2.0版本的apk,然后通知用户安装更新,进而完成增量更新,当然其中还有更多细节这里就不详述了,有兴趣的可以自己看一下。
所以综上所述:这里有两步操作,一个是生成.patch差分文件,一个是通过差分文件生成新版本apk。
这里先说Android端的通过差分文件生成apk,这里需要通过ndk来生成一个so库使用,也就是说具体实现是在c++层的,将其打包成so库,然后给java层通过native来使用。
第二个生成差分文件,这个是在服务器中实现的,这里我将提供一个例子,具体就是生成一个dll库,然后提供给java服务器,java服务器就是通过这个dll文件来比对两个版本的apk来生成差分文件。
参考资料
jni常见问题:https://blog.csdn.net/qq_27706119/article/details/111614310
Android打包成so库的Patch源码:https://github.com/JackCho/AndroidPatchUpdate
供windows打包成dll库源码:https://github.com/curasystems/bsdiff-win/tree/master
android NDK-编译c生成so库:https://blog.csdn.net/cheng2290/article/details/77717164
准备工作
编译环境:Android studio 4.0.1
NDK版本:21.3.6528147
测试环境:真机 OPPO ACE (Androd 9.0)
准备的工作:详细参考android NDK-编译c生成so库这一篇文章(可能会在NDK生成so库时还有问题,那么就将ndk中的ndk-build.cmd配置在系统的path路径中)
然后在文章中提及到了需要安装CMake,LLDB,NDK,这里有部分Android studio中是没有LLDB,我的就没有,没有也不要紧,通常是整合到NDK里面。所以这种情况只需要下载CMake和NDK。
另外配置一下Android Studio的NDK路径
class文件生成头文件
准备工作
首先写下一个java文件,然后通过rebuild来生成对应的class文件
注意一下,class文件所在位置是在app\build\intermediates\javac\debug\classes文件下
public class PatchUtils {
/**
*
* @param oldApkPath 原来的apk 1.0
* @param newApkPath 合并需要的apk路径 需要生成2.0路径
* @param patchPath 差分包路径 从服务器上下载下来
*
* 注意通过rebuild生成头文件
*/
public static native void combine(String oldApkPath,String newApkPath,String patchPath);
}
注意到native了吗,这表示这是一个native方法,调用底层c代码。
生成头文件
在android studio terminal终端或是cmd中跳转到你需要生成头文件的目录下,
在我这里的目录是
然后输入javah -classpath **\app\build\intermediates\javac\debug\classes\ com.example.iocprojecttest.utils.PatchUtils
(**指的是项目所在位置,然后就会在当前目录下生成头文件)
**注意:**很多人会出现文件找不到的错误,原因是网上很多资料都是在-classpath 后面写的是java文件目录,又或是class文件所在目录。
正确的是Android studio生成class存放的目录,也就是javac\debug\classes\这个目录。
举个例子
我这个项目中生成class文件详细路径是**\app\build\intermediates\javac\debug\classes\com\example\iocprojecttest\utils这个目录下,然后很多人就会这么写
javah -classpath **\app\build\intermediates\javac\debug\classes\com\example\iocprojecttest\utils com.example.iocprojecttest.utils.PatchUtils
这个时候就会出现错误,一开始我也是这么错的。然后我猜想既然class文件都带有包名了,路径是不是可以不带包的路径
于是就写了javah -classpath **\app\build\intermediates\javac\debug\classes\ com.example.iocprojecttest.utils.PatchUtils
果然通过。
这个时候你就能得到一个头文件com_example_iocprojecttest_utils_PatchUtils.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_iocprojecttest_utils_PatchUtils */
#ifndef _Included_com_example_iocprojecttest_utils_PatchUtils
#define _Included_com_example_iocprojecttest_utils_PatchUtils
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_iocprojecttest_utils_PatchUtils
* Method: combine
* Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_example_iocprojecttest_utils_PatchUtils_combine
(JNIEnv *, jclass, jstring, jstring, jstring);
#ifdef __cplusplus
}
#endif
#endif
然后将其放到jni文件夹中,jni文件夹的创建是右键项目->new -> folder -> JNI folder。这个是后就会在你的项目中创建了一个JNI文件夹,你将这个头文件放入到jni文件夹中。
通过NDK创建so库
这个时候先去https://github.com/JackCho/AndroidPatchUpdate下载源文件,将jni里面的文件全部拉到你项目中的jni文件夹中。
like this
修改一下bspatch.c这个文件
首先是添加一下头文件
#include "bzlib.c"
#include "crctable.c"
#include "compress.c"
#include "decompress.c"
#include "randtable.c"
#include "blocksort.c"
#include "huffman.c"
#include <bzlib.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <err.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h> // android
#include "bspatch.h"
#include <android/log.h> // To be able to print to logcat
//引入我们的方法
#include "com_example_iocprojecttest_utils_PatchUtils.h"
然后增加我们我们调用方法,供java层的调用,将com_example_iocprojecttest_utils_PatchUtils.h里面的方法复制过去,然后这样改动。
//合并方法的实现
JNIEXPORT void JNICALL Java_com_example_iocprojecttest_utils_PatchUtils_combine
(JNIEnv *env, jclass jclz, jstring old_apk_path, jstring new_apk_path, jstring patch_path){
//封装参数
char * argv[4];
//转换 jstring -> char*
char* old_apk_cstr = (char*)(*env)->GetStringUTFChars(env,old_apk_path,NULL);
char* new_apk_cstr = (*env)->GetStringUTFChars(env,new_apk_path,NULL);
char* patch_cstr = (*env)->GetStringUTFChars(env,patch_path,NULL);
// 第0的位置随便给
argv[0] = "combine";
argv[1] = old_apk_cstr;
argv[2] = new_apk_cstr;
argv[3] = patch_cstr;
//调用上面的方法,这个方法就是通过差分文件生成apk的具体实现
bspatch(argv);
//释放资源
(*env)->ReleaseStringUTFChars(env,old_apk_path,old_apk_cstr);
(*env)->ReleaseStringUTFChars(env,new_apk_path,new_apk_cstr);
(*env)->ReleaseStringUTFChars(env,patch_path,patch_cstr);
}
然后我们就要着手so库的生成
首先在gradle.properties中添加下面这行代码
android.useDeprecatedNdk = true
然后在Android.mk文件这样 改动
#bspath模块
include $(CLEAR_VARS)
#生成的so库名称
LOCAL_MODULE := bspatch
#你要编译的文件
LOCAL_SRC_FILES := bspatch.c
#加入log
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
这里想了解详细的可以到google的NDK开发文档中可以查看。
然后在Application.mk文件这样写
APP_ABI := armeabi-v7a armeabi x86
APP_PLATFORM := android-9
ABI打包出来的so库可以使用的平台,这里是v7a 和x86 armeabi,google官网也有详细介绍
APP_PLATFORM表示的是最低可以使用的版本。
然后重中之重来了,build.gradle的配置。
这里有两种情况,我一种一种来介绍,一种是旧的,一种是新的
我先来说一下旧的
/*配置ndk的设置*/
sourceSets{
main{
jni.srcDirs = []//设置禁止gradle生成Android.mk
jniLibs.srcDirs = ['libs']
}
}
task ndkBuild(type: Exec){
//设置新的so生成目录
commandLine "E:\\android-sdk-windows\\ndk\\21.3.6528147\\ndk-build.cmd",
'NDK_PROJECT_PATH=build/intermediates/ndk',
'NDK_LIBS_OUT=libs',
'APP_BUILD_SCRIPT=jni/Android.mk',
'NDK_APPLICATION_MK=jni/Application.mk'
}
tasks.withType(JavaCompile){
compileTask -> compileTask.dependsOn ndkBuild
}
一种是这样的,这种是旧的方式,然后rebuild-project,通常就能生成so库了,但是在我这里是不成功的。当我rebuild-project的时候就会发现出一个问题Process 'command 'E:\AndroidSDK\ndk-bundle/ndk-build.cmd'' finished with non-zero exit value 2
这个问题搞得我想死,经过一顿google后发现了一个新的方法,可能是Android Studio比较新的版本,所以用新的方法,即是下面这种方法。
defaultConfig{
...
ndk{
//这个是so库的名字
moduleName "bspatch"
//这个是打包的abi文件平台
abiFilters "armeabi-v7a","x86"
}
}
sourceSets{
main{
jni.srcDirs = []//设置禁止gradle生成Android.mk
jniLibs.srcDirs = ['libs']
}
}
externalNativeBuild {
ndkBuild {
path "src/main/jni/Android.mk"
}
}
使用这个方法在rebuild-project就能生成so库,
这里你可以看到v7a和x86平台的,你在使用很多第三方SDK的时候都会有这个so库,这里你也可以将so库拿出来就能到其他项目中使用了。
这个时候我们就完成了android端的差分合并方法了,就差服务端生成差分文件了。
生成dll动态库
我们需要生成一个dll动态库给服务端(java端)使用。这里我是使用VS2015来生成dll动态库,这里先下载https://github.com/curasystems/bsdiff-win/tree/master windos版本的bsdiff ,用来生成dll库。
首先创建一个c++的空项目,然后将下载下来的.h文件、.c文件、cpp文件,还有你通过javah生成的头文件
com_example_iocprojecttest_utils_PatchUtils.h放入到这个项目中的文件夹中,然右击资源管理器项目的头文件,添加现有项,将.h文件添加进来,然后在源文件中添加现有项将所有的.c文件,cpp文件添加进来。
这个时候会报红,因为com_example_iocprojecttest_utils_PatchUtils.h是需要用到jni.h这个头文件,而c本身是没有这个库,这个库是在JDK中,我们可以到JDK搜索到这个文件jni.h和jni_md.h这两个文件,也是同样放入到项目中,添加进来。
修改bsdiff.cpp
然后我们需要修改一下bsdiff.cpp,因为我们就是从这里进入来的,我们需要将java调用c文件提供一个入口,这个入口就是我们生成的头文件com_example_iocprojecttest_utils_PatchUtils.h
然后我们需要添加#include "com_example_iocprojecttest_utils_PatchUtils.h",然后里面有个main()方法,这个是库的作者测试入口,我将这个方法改成diff,后面我们就是要调用这个diff()方法
添加的代码就是添加一个方法,可以被从外面java调用进来。
JNIEXPORT void JNICALL Java_com_example_iocprojecttest_utils_PatchUtils_combine
(JNIEnv *env, jclass jclz, jstring old_apk_path, jstring new_apk_path, jstring patch_path) {
//封装参数
char* old_apk_cstr = (char*)env->GetStringUTFChars(old_apk_path, NULL);
char* new_apk_cstr = (char*)env->GetStringUTFChars(new_apk_path, NULL);
char* patch_cstr = (char*)env->GetStringUTFChars(patch_path, NULL);
int argc = 4;
char *argv[4];
argv[0] = "diff";
argv[1] = old_apk_cstr;
argv[2] = new_apk_cstr;
argv[3] = patch_cstr;
//调用cpp方法
diff(argc, argv);
//释放资源
env->ReleaseStringUTFChars(old_apk_path, old_apk_cstr);
env->ReleaseStringUTFChars(new_apk_path, new_apk_cstr);
env->ReleaseStringUTFChars(patch_path, patch_cstr);
}
是不是和上面很相像,其实我们的class文件->.h文件,调用里面的方法就是通过这个方法名来找到的,这里为了能更加理解这过程我介绍一下这个方法名void JNICALL Java_com_example_iocprojecttest_utils_PatchUtils_combine
Java_com_example_iocprojecttest_utils_PatchUtils_combine
前面是java,然后后面的com_example_iocprojecttest_utils_PatchUtils其实就是包名+上类名。
_combine就是方法名,就是通过这个来做到java调用c的代码的。
这个时候改好了我们可以运行一下,看一下有没有问题。
通常是出问题的:就是有一些过时的方法,还有一些方法是警告,这些都可以通过宏定义来解决,然后有些方法是需要被检查的。
然后有很多源文件都需要添加宏定义,这很麻烦。
我们可以在 调试 -> **属性页(**是你的项目名称) -> c/c++ -> 命令行中添加
-D _CRT_SECURE_NO_WARNINGS -D _CRT_NONSTDC_NO_DEPRECATE
来让编译通过这些警告。
然后有些地方需要被检查的,我们可以将c/c++ -> 常规 中将SDL检查关闭
这个时候我们在运行一下代码,发现可以通过了(如果有问题就按照错误提示来解决)。通过后我们就可以来生成dll文件
生成dll文件
这个时候我么先配置一下资源管理器。将活动解决方案平台改成x64。
然后配置一下生成文件,因为我们运行代码一般是exe文件,但是这里我们需要的是dll文件。
修改一下,将其改成动态库dll。
然后直接运行,你会发现运行不了,需要在 生成 -> 重新生成解决方法才可以。
这个时候就能生成成功。文件就在E:\Visual_Studio_WorkSpace\BSDiffDemo\x64\Debug这个文件夹下。
好这个时候我们终于生成了dll文件,那就将这个dll文件给后端(java服务器),由其来生成差分文件。
服务端生成差分文件
这里我是使用eclipse,别问为什么不是IDEA,问就是又过期了。。。
在这里我们已经获得了dll动态库了,那么我们现在需要的就是如何调用这个dll库来产生差分文件。现在eclipse中新建一个工程,
将com_example_iocprojecttest_utils_PatchUtils.h文件放入到src文件夹下,然后创建一个包(注意一下这个包名和包里面的PatchUtils类,下面我会解释为什么要这个包名和一定要这个类名)
public class BSDiffTest {
public static void main(String[] args) {
PatchUtils.combine("version_1.apk", "version_2.apk"
,"version_1.0_2.0.patch");
System.out.println("生成完毕~");
}
}
这个是入口函数,调用PatchUtils.combine()方法。
PatchUtils类
public class PatchUtils {
//调用dll动态库
static {
System.load ("E:/Visual_Studio_WorkSpace/BSDiffDemo/x64/Debug/BSDiffDemo.dll");
//System.loadLibrary("BSDiffDemo");
System.out.println("加载成功");
}
public static native void combine(String oldApkPath,
String newApkPath,String patchPath);
}
这个类首先加载dll动态库,然后写一个native方法,注意方法名一定是combine,且类名一定是PatchUtils,一定要在com_example.iocprojecttest.utils.PatchUtils这个包下面,为什么要这样做?我来说明一下,当有一个不对的话就会出现下面这个问题
java.lang.UnsatisfiedLinkError: com_example.iocprojecttest.utils.PatchTest.combine(Ljava/string/string;IIII)V
这个问题也是搞了我很久,经过一番查找,终于明白了这一个问题的原因。
我们先看会com_example_iocprojecttest_utils_PatchUtils.h这个头文件,
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_iocprojecttest_utils_PatchUtils */
#ifndef _Included_com_example_iocprojecttest_utils_PatchUtils
#define _Included_com_example_iocprojecttest_utils_PatchUtils
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_iocprojecttest_utils_PatchUtils
* Method: combine
* Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_example_iocprojecttest_utils_PatchUtils_combine
(JNIEnv *, jclass, jstring, jstring, jstring);
#ifdef __cplusplus
}
#endif
#endif
这里的方法名是Java_com_example_iocprojecttest_utils_PatchUtils_combine,就是com.example.iocprojecttest.utils.PatchUtils.combine()方法,在dll调用方法的时候就会看是哪一个类调用哪一个方法,
看一下错误信息
java.lang.UnsatisfiedLinkError: com_example.iocprojecttest.utils.PatchTest.combine(Ljava/string/string;IIII)V
就是说没有找到com_example.iocprojecttest.utils.PatchTest.combine()这个方法,注意到有什么不同了吗和上面。
就是错误的是PatchTest.combine,而我们的是PatchUtils.combine()。
其实一开是我的类名就是PatchTest,dll库是根据调用类的包名、类名、方法名结合在一起来查找,而我有这个错误,所以导致没有成功运行。所以我们到包名一定时这个com.example.iocprojecttest.utils,然后类名是PatchUtils,方法名是combine
好了,这样就能成功运行了。
Android 应用升级代码
在这里我们是使用bsdiff 来进行增量更新,所以这是一次应用的升级。这里我是从1.0 -> 2.0
先看一下Android升级有关的代码。
注意一下这里的系统是Android 9.0 在 Android N 和 Android O在应用升级上都有所改变,想了解更多细节可以自己百度一下、
首先是AndroidManifest中的权限
添加应用升级权限
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
添加下面的代码
<!--安装apk的权限设置-->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.iocprojecttest.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
在res - xml -添加以一个file_paths文件
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path path="Android/data/com.example.iocprojecttest.fileprovider/" name="files_root"/>
<external-path path="." name="external_storage_root"/>
</paths>
主要代码
public class TestNdkActivity extends BaseActivity {
private String mPatchPath = Environment.getExternalStorageDirectory().getAbsolutePath()
+ File.separator+"version_1.0_2.0.patch";
private String mNewApkPath = Environment.getExternalStorageDirectory().getAbsolutePath()
+ File.separator+"version2.apk";
@Override
protected void initData() {
//访问后台接口,需不需要更新版本
//需要更新版本,那么提示用户需要更新
//直接下载,然后提示用户更新
//下载差分包后,调用我们的native方法去合并生成新的apk
//获取本地的getPackageResourcePath() aok路径
if(!new File(mPatchPath).exists()){
return;
}
PatchUtils.combine(getApplicationContext().getPackageResourcePath(),mNewApkPath,mPatchPath);
//安装最新版本
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//判断是否是O版本
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
startInstallO();
} else if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
startInstallN();
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
public void startInstallO(){
boolean isGranted = getPackageManager().canRequestPackageInstalls();
if (isGranted){
startInstallN();
}else {
new AlertDialog.Builder(this)
.setCancelable(false)
.setTitle("安装应用需要打开未知资源,需要去权限中心开启权限")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Uri packageUri = Uri.parse("package:" + TestNdkActivity.this.getPackageName());
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,packageUri);
TestNdkActivity.this.startActivity(intent);
}
}).show();
}
}
public void startInstallN(){
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri contentUri = FileProvider.getUriForFile(this,"com.example.iocprojecttest.fileprovider",new File(mNewApkPath));
intent.setDataAndType(contentUri,"application/vnd.android.package-archive");
startActivity(intent);
Log.d("NDK","跳转了");
}else {
intent.setDataAndType(Uri.fromFile(new File(mNewApkPath)),
"application/vnd.android.package-archive");
startActivity(intent);
}
}
@Override
protected void initView() {
}
@Override
protected void initTitle() {
}
@Override
protected void setContentView() {
setContentView(R.layout.activity_ndk);
}
}
基本上就是这样了,注意下Android不同版本的升级权限问题就可以了。注意这里安装后需要你自己到设置中赋予权限,我没有写动态申请权限,需要你自己手动开启权限。
这里为了测试1.0的版本,这里我就只改动一下画面ui就行了。然后导出apk,把apk改成version_1.apk然后放入到Eclipse中
,然后将ui改了后,将build-gradle中的 versionCode 改为2 versionName “2.0”,再把apk导出来,也是放到Eclipse中
然后运行就能导出version_1.0_2.0.patch 差分文件。
然后把手机中的1.0apk删掉,将version1.0 和version_1.0_2.0.patch文件放入手机中,安装1.0apk,这个时候提示你开启升级权限,然后安装2.0apk包。