文末附源码
先简单看下效果,如图:
可以看出来,效果非常的简单,跟网上大部分例子一样,选择实现两个数字的加法运算。
开发环境:
AndroidStudio版本:Android Studio Dolphin | 2021.3.1 Patch 1
ndk和cmake的版本均是从AndroidStudio中下载的最新的版本
接下来,本文将会分别使用ndk-build和cmake两种方式实现jni
(默认大家已经创建了一个新的项目)
共有部分:
1.布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20dp"
tools:context=".MainActivity">
<EditText
android:id="@+id/et_no1"
android:layout_width="match_parent"
android:layout_height="60dp"
android:hint="number 1" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="+" />
<EditText
android:id="@+id/et_no2"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="10dp"
android:hint="number 2" />
<Button
android:id="@+id/btn_plus"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="10dp"
android:text=" = " />
<EditText
android:id="@+id/et_result"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="10dp"
android:enabled="false"
android:hint="result" />
</LinearLayout>
2.MainActivity
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.btnPlus.setOnClickListener(view -> {
String number1 = !binding.etNo1.getText().toString().trim().isEmpty() ? binding.etNo1.getText().toString().trim() : "0";
String number2 = !binding.etNo2.getText().toString().trim().isEmpty() ? binding.etNo2.getText().toString().trim() : "0";
binding.etResult.setText(String.valueOf(JNIUtils.plusFromJNI(Integer.parseInt(number1), Integer.parseInt(number2))));
});
}
}
我这里使用了viewbind,具体配置方法:
android {
//...其他
viewBinding{
enabled true
}
}
3.JNIUtils
public class JNIUtil {
//通过jni,调用c/c++ 函数
public static native int doPlus(int a, int b);
// Used to load the 'cmakejnidemo' library on application startup.
static {
System.loadLibrary("jnidemo");
}
}
至此,共有部分完成,剩余部分将会分类展示
(1)ndk-build 方式
1.创建头文件,如图
说明: 编译获取头文件我这里使用的是javac -h ./ JNIUtils.java
(1)旧版本sdk使用的是javah ./ JNIUtils.java,但是高版本sdk并不支持javah,需要使用javac -h;
(2)命令中./表示的是当前文件,javac 命令需要指定编译完成后的文件存放到什么位置,如果这里不指定位置,直接写javac - h JNIUtils.java,那么就会报错:无源文件,如图
(3)使用cd命令进入JNIUtils.java所在目录时,需要先看看当前在什么目录,而不是非得按照图示中那么敲,查看当前目录的命令是pwd,如图
调用pwd命令,发现我现在就在JNIUtils目录下,那么此时我就可以直接执行javac -h命令
假如说,我现在在"E:\AndroidProject\NDKDemo\app\src\main\"这个目录下,
那么显然就需要执行"cd main/java/com/example/ndkdemo"
然后就可以进入目标目录下了
说明:
cd main/java/com/example/ndkdemo
和
cd .\main\java\com\example\ndkdemo\
效果是相同的,写法都支持
2.创建jni目录,将第一步生成的文件放入jni目录中
3.创建.c文件,实现native方法
com_example_ndkbuildjnidemo_JNIUtils.c
extern "C" JNIEXPORT jint JNICALL
Java_com_example_ndkbuildjnidemo_JNIUtils_plusFromJNI(JNIEnv *env,jobject thiz,jint number1,jint number2) {
return no1+no2;
}
说明:
JNIEnv *env:表示指向JNI接口函数表的指针,通过这个指针可以调用JNI接口函数,实现Java和本地代码的交互。
jobject thiz:表示Java中的对象引用,即调用本地代码的Java对象。在本地代码中可以通过这个对象引用调用Java对象的成员方法和属性。
number1和number2则是与前面在JNIUtils中定义的native方法的两个形参
这里因为只是简单的计算,所以直接返回number1+number2即可。
当然这个.c代码中也可以根据自己实际需求编写C/C++代码区定义一些其他比较复杂的逻辑。
4.定义Android.mk和Application.mk
Android.mk文件是Android Native Development Kit (NDK) 中的一个重要文件,它是一个Makefile文件,用于编译Android应用程序项目中的本地代码,例如C/C++源文件、静态库、动态库等。
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := jnidemo
LOACA_SRC_FILES := com_example_ndkbuildjnidemo_JNIUtils.c
include $(BUILD_SHARED_LIBRARY)
这个文件,在使用的时候,需要根据自己的实际配置进行修改,其实主要修改
LOCAL_MODULE 和LOACA_SRC_FILES 这两项
分别填的是本地JNI库的名字(可以在JNIUtils文件中找到)和上一步创建的.c代码的名字
Application.mk
APP_ABI := all
5.build.gradle文件中,指定Android.mk文件的位置
android {
//其他
externalNativeBuild {
ndkBuild {
path "src/main/jni/Android.mk"
}
}
}
至此,rebuild一下项目,即实现了使用ndk-build的jni调用了。
说明:使用ndk-build生成的so库在这里:
(2)cmake 方式
1.新建CMakeDemo项目,复制共有部分代码,main目录下,新建cpp目录
2.在cpp目录下,创建CMakeLists.txt
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.18.1)
# Declares and names the project.
project("jnidemo")
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
jnidemo
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
native-lib.cpp)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries 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 this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
jnidemo
# Links the target library to the log library
# included in the NDK.
${log-lib})
说明:
1.cmakelists.txt文件可以直接复制上面的内容,但是要修改成自己的配置;
2. add_library(的第一个字段与target_link_libraries(的第一个字段都是需要加载的本地JNI库的名字,我这里命名与ndk-build方式的命名相同,就是为了比较二者的配置;
3.add_library(的第三个字段为本地实现native方法的cpp文件,文件名可以更改,但是要注意同时修改本处配置,保持一致,否则就会调用不到方法导致报错
4.Android默认支持的是cmake方法,并且在创建项目的时候,即可以选择支持C++,如下:
即在构建项目的时候,选择最下面的“Native C++”,然后后面的按照默认的走就可以用了
当然,可能不同的AndroidStudio版本,创建JNI项目的流程是不一样的,但是都是在创建项目的时候勾选选项,这里只是给一点思路,如果发现你的AndroidStudio没有我上面的那些,可以网上搜索一下对应的方法,或者是在页面中仔细找相关的勾选项,一般是很容易找到的
3.在cpp目录下,新建native-lib.cpp文件
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jint JNICALL
Java_com_example_cmakedemo_JNIUtil_doPlus(
JNIEnv *env,
jclass thiz,
jint number1,
jint number2) {
return number1+number2;
}
4.build.gradle
android {
//其他
defaultConfig {
//其他
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.18.1'
}
}
}
至此,rebuild一下项目,即实现了使用cmake的jni调用了。
说明:使用cmake方式生成的so库在这里
说明:
关于ndk-build和cmake两种方式,其实弄下来,发现虽然具体的配置不一样,但是基本上就是实现了那么点东西:定义一个方法或者是放在activity中创建一个native方法,并且指定app启动时加载的本地JNI库,然后把这个方法或者是activity生成.h头文件,然后在头文件的基础上,写.c文件去实现native方法,然后就是定义打包编译文件,这一块二者有区别,但是归根到底还是要指定本地JNI库的名字以及本地.c/cpp文件的路径,打包编译完成,生成so库,so库使用默认的路径即可,也可以放在lib文件下,或者是生成一个jniLibs文件夹放进去,然后使用起来。整个过程就算是结束了,但是要注意,一个module中只能使用一种方式构建jni,要么cmake,要么ndk-build。
写在后面:
我想说一下整理这篇博客的原因。前两天我开始研究Android JNI的内容,算是开始了系统的学习。之前其实也做过支持JNI的项目,但是那都是接手别人的,对于JNI模块一般创建好了,轻易不会需要改动的,所以一直对于这块也没怎么细究。
前天,我在网上到处找相关的资料、博客、书籍,想着能够找一个简单点的,让我了解流程的就行,很遗憾,网上的资源新的新,旧的旧,质量也是参差不齐,跟着别人的弄了一天,到最后啥都没弄成,要么是环境不对了,要么是编译不出来so文件,反正就是各种出问题,但是网上去找问题的解决方案吧,还找不到,要不然就是引入一个新坑,一天折腾下来,差点日志都没法写。
越是这样,我越觉得我必须得整理出来一份能用的关于构建jni项目的demo出来,我写博客的原则是,我希望别人按照我写出来的东西一步步弄下来,能用,起码不会说让别人觉得看我写的东西是在浪费时间,这就是我最低的要求了。所以这篇博客里,我不仅仅是写了流程,还尽可能地把我自己在前期摸索时,遇到的问题以及我找到的解决方案也列举出来。
最后希望这篇从找资料、各种测demo,到整理文字花了两天的博客可以帮到诸位。有什么问题,评论区探讨,如果时间充足,可以一起协助。