一、准备Android NDK开发环境
NDK:android原生开发工具包,这套工具集允许您为 Android 使用 C 和 C++ 代码,并提供众多平台库,让您可以管理原生 Activity 和访问物理设备组件,例如传感器和触摸输入。
CMake:一款外部构建工具,可与 Gradle 搭配使用来构建原生库。如果您只计划使用 ndk-build,则不需要此组件。
LLDB:一种调试程序,Android Studio 使用它来调试原生代码。
可以从 SDK 管理器中安装 LLDB、CMake 和 NDK
1.在打开的项目中,从菜单栏选择 Tools > Android > SDK Manager。
2.点击 SDK Tools 标签。
3.选中 LLDB、CMake 和 NDK 旁的复选框,如图所示。
4.点击 Apply,然后在弹出式对话框中点击 OK。
5.安装完成后,点击 Finish,然后点击 OK。
本片文章使用ndk-build来构建ndk项目,cmake方式本篇不做阐述。
二、要了解的知识点
Android.mk:
位于项目JNI目录的子目录中,用于向编译系统描述源文件和共享库。它实际上是编译系统解析一次或多次的微小 GNU makefile 片段。Android.mk文件用于定义Application.mk、编译系统和环境变量所未定义的项目范围设置。它还可替换特定模块的项目范围设置。
内容解析:
LOCAL_PATH := $(call my-dir)//编译系统提供宏函数 my-dir
返回当前目录(Android.mk
文件本身所在的目录)的路径。
include $(CLEAR_VARS)//CLEAR_VARS
变量指向一个特殊的 GNU Makefile,后者会清除许多 LOCAL_XXX
变量,例如 LOCAL_MODULE
、LOCAL_SRC_FILES
和 LOCAL_STATIC_LIBRARIES,但是不会清除不会清除 LOCAL_PATH。在描述每个模块之前,必须声明(重新声明)此变量
LOCAL_MODULE := hello-jni//每个模块名称必须唯一,且不含任何空格。编译系统在生成最终共享库文件时,会对您分配给 LOCAL_MODULE
的名称自动添加正确的前缀和后缀。例如,上述示例会生成名为 libhello-jni.so
的库
LOCAL_SRC_FILES := hello-jni.c//列举要编译到模块中的C、C++源文件,以空格分隔多个文件
LOCAL_CPP_EXTENSION := .cxx //使用此可选变量为 C++源文件指明.cpp
以外的文件扩展名。例如.cxx
LOCAL_SHARED_LIBRARIES := libz //此变量会列出此模块在运行时依赖的共享库模块,如libz.so。此信息是链接时必需的信息,用于将相应的信息嵌入到生成的文件中
LOCAL_STATIC_LIBRARIES := libz
//此变量用于存储当前模块依赖的静态库模块列表,如libz.a。此信息是链接时必需的信息,用于将相应的信息嵌入到生成的文件中(需要先预编译libz.a才能找到)
LOCAL_LDLIBS := -lz //此变量列出了在编译共享库或可执行文件时使用的额外链接器标记,可使用
-l
前缀传递特定系统库的名称,如
-lz表示
libz.so。
LOCAL_CFLAGS //只编译 C++ 源文件时将传递的一组可选编译器标记。它们将出现在编译器命令行中的LOCAL_CFLAGS
后面。使用LOCAL_CFLAGS
为 C 和 C++ 指定标记。比如:= -frtti -fexceptions --std=c++11 -DANDROID -DNDEBUG
LOCAL_C_INCLUDES := sources/foo //可以使用此可选变量指定相对于 NDK root
目录的路径列表,以便在编译所有源文件(C、C++ 和 Assembly)时添加到 include 搜索路径
include $(BUILD_SHARED_LIBRARY)//将所有内容连接到一起
include $(PREBUILT_SHARED_LIBRARY) //指向用于指定预编译共享库的编译脚本
include $(PREBUILT_STATIC_LIBRARY)//预编译静态库
LOCAL_ARM_MODE := arm //默认情况下,编译系统会在 thumb 模式下生成 ARM 目标二进制文件,其中每条指令都是 16 位宽,并与 thumb/
目录中的 STL 库关联。将此变量定义为 arm
会强制编译系统在 32 位 arm
模式下生成模块的对象文件
Application.mk
位于项目JNI目录的子目录中,用于指定项目范围设置。(在Android.mk中设置的模块选项优先于项目范围选项)
内容解析:
APP_OPTIM := release //将此可选变量定义为release
或debug
。默认情况下,将编译发布模式的二进制文件,发布模式会启用优化。
APP_ABI := armeabi-v7a arm64-v8a x86 //默认情况下,NDK 编译系统会为所有非弃用 ABI 生成代码。您可以使用 APP_ABI
设置为特定 ABI 生成代码
APP_PLATFORM := android-16 //声明编译此应用所面向的 Android API 级别,并对应于应用的minSdkVersion
APP_STL := c++_static //用于此应用的 C++ 标准库。默认情况下使用system
STL。其他选项包括c++_shared
、c++_static
和none
java与jni数据类型对照以及使用:
1.综述:
1)java中的返回值void与jni中的void是完全对应的。
2)java中的基本数据类型(byte,short,int,long,float,double,boolean,char)在jni中对应的数据类型在前面加上j (jbyte,jshort,jint,jlong,jfloat,jdouble,jboolean,jchar)。
3)java中的对象,包括类库中定义的类、接口,都对应jni中的jobject。
4)java中基本数据类型的数组对应与jni中的jarray类型(type就是上面说的8中基本数据类型
5)java中对象的数组对应于jni中jobjectArray类型
2.java数据类型与jni类型映射表
3.java类型和jni签名的关系
4.常用的函数:
jclass (*FindClass)(JNIEnv*, const char*);
jfieldID (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
jobject (*GetObjectField)(JNIEnv*, jobject, jfieldID);
jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
jsize (*GetStringLength)(JNIEnv*, jstring);
......
三、自定义Android Studio工具
1.在打开的项目中,从菜单栏选择 Tools > Android > SDK Manager。
2.点击 Tools > External Tools
3.点击OK保存。这时候就可以在菜单栏中看到javah选项,后续就可以直接右键生成java文件对应的C++头文件了。
四、简单的Demo
1.创建Android 工程,在java同级目录下创建jni目录,并创建Android.mk和Application.mk文件
2.在Android.mk中将原生库添加到 LOCAL_LDLIBS变量中,例如要链接liblog.so,添加代码如下:
LOCAL_LDLIBS := -llog; //编译系统会自动链接标准 C 库和 C++ 库,无需在这里添加
使用空格作为分隔符列出多个原生库。
3.在Java层创建接口文件HelloWorld.java
public class HelloWorld { public native String ToastHelloFromC(String javaStr); }
4.右键javah,生成对应的JNI头文件
JNIEXPORT jstring JNICALL Java_com_axu_ndkdemo_HelloWorld_ToastHelloFromC (JNIEnv *, jobject, jstring);
5.根据上一步生成的头文件生成C++源文件并编写逻辑
jstring JNICALL Java_com_axu_ndkdemo_HelloWorld_ToastHelloFromC(JNIEnv * env, jobject jobj, jstring javaStr) { const char * javaChar_ptr = env -> GetStringUTFChars(javaStr,NULL); char jniChar[] = "and 我来自JNI"; char* result_ptr = (char*)malloc(strlen(javaChar_ptr) + strlen(jniChar) + 1); strcpy(result_ptr, javaChar_ptr); strcat(result_ptr, jniChar); return env -> NewStringUTF(result_ptr); }
6.完善Android.mk和Application.mk
Android.mk:
LOCAL_PATH := $(call my-dir) ######## build hello.so start ######### include $(CLEAR_VARS) LOCAL_CPP_EXTENSION := .cpp LOCAL_MODULE := hello LOCAL_SRC_FILES := com_axu_ndkdemo_HelloWorld.cpp #LOCAL_C_INCLUDES += com_axu_ndkdemo_HelloWorld.h LOCAL_CPPFLAGS:= -frtti -fexceptions --std=c++11 -DANDROID -DNDEBUG include $(BUILD_SHARED_LIBRARY) ######### build hello.so end ##########
Application.mk:
#APP_OPTIM := release APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 APP_PLATFORM := android-16 #APP_STL := c++_static
7.在jni目录执行ndk-build命令,生成对应CPU架构的so文件
8.配置app gradle 将so加载进去,并编写调用程序
sourceSets { main { jniLibs.srcDirs 'src/main/libs' jni.srcDirs = [] } }
public class MainActivity extends Activity { private Button getJni_btn; private HelloWorld helloWorld; static { try{ System.loadLibrary("hello"); }catch (Exception ex){ ex.printStackTrace(); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); getJni_btn = (Button)findViewById(R.id.get_jni_btn); getJni_btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String result = helloWorld.ToastHelloFromC(" 我来自JAVA "); Log.e("MainActivity",result); } }); } private void init(){ helloWorld = new HelloWorld(); } }View Code
9.运行程序,Logcat输出:
四、熟练掌握JNI层与Java层交互
1、Java调用C代码
这种情况一般是正向调用,应用场景为java层应用需要使用C/C++层中的方法,常见于调用设备硬件驱动,优化算法,要求计算效率等接口中。
常见的形式有数值运算,字符串运算、数组运算以及复杂对象运算,下面分别举例说明
1)数值运算
2)字符串运算
3)数组运算
4)对象运算
2、C代码调用Java
这种情况可称为反向调用,最常见的应用场景为C/C++层需要将处理信息或者执行结果异步回调给java层。
1)保存全局JVM实例和回调函数的实例对象
2)从全局jvm中获取当前线程的JNIEnv实例
3)获取回调函数的实例对象字节码jclass
4)通过GetMethodId方法获得回调函数的方法id
5)通过CallVoidMethod调用java方法
需要注意的地方:
如果需要在jni层回调函数内处理java对象,分两种情况:
1.如果该对象在回调函数中使用到(传参或返回值),这可以直接通过FindClass找到该对象的字节码
2.如果该对象在回调函数中没有被使用到,那么就需要提前注册该类型的对象,然后在回调函数中取出对象进行使用
3、复杂对象的数据互通
复杂对象指对象列表,对象中嵌套对象。原理一样,当C解析java传过来的对象时,使用FindClass找到对象然后解析字段
当C组装java对象并传给java层时,需要先找到对象的构造方法id,之后通过构造方法进行对象赋值,从而创建对象,之后直接抛到java层,java层使用相应的对象接收即可。