一.概述
Android Jni机制让开发者可以在Java端调用到C/C++,也是Android应用开发需要掌握的一项重要的基础技能。
计划分两篇博文讲述Jni实战开发。
本篇主要从项目架构上剖析一个Android App如何通过Jni机制加载三方库和C/C++文件。
二.Native C++
Android Studio可以直接创建一个可运行的、最简单的Jni Demo App。
字符串"Hello from C++"从Jni传到Java再在TextView上显示。
运行:
文件目录结构:
这个默认创建的Jni Demo App的代码就不一一展示了。
接下来会详细讲解自定义Jni App架构和代码改造过程
三.自定义JNI App
3.1 目录架构
默认创建的Jni Demo App只是简单实现了一个字符串在Jni和Java之间的传递,并没有涉及到加载三方库和.c/.cpp,所以接下来要做的就是,在默认Jni Demo App基础上进行升级改造,实现一个便于扩展、能够加载三方库和.c/.cpp的Demo。
先看看改造后的目录结构:
相对于AndroidStudio默认创建的Jni Demo App,主要的修改点有如下:
- MainActivity.java中的Load Jni so以及native函数声明部分单独抽离出来,写成一个专门的JNIDEMO.java文件,便于对Jni的调用
- 与 java 目录平级新建 jnicpp 目录放置C/C++源码文件
- 与 java 目录平级新建 jnilibs 目录放置需要加载的三方库
- 变更CMakeLists.txt位置,放置在jnilibs根目录
3.2 源码解析
3.2.1 JniActivity.java
package com.android.demo.activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.android.demo.databinding.ActivityJniBinding;
import com.android.demo.jni.JNIDEMO;
public class JniActivity extends AppCompatActivity {
private final String TAG = "JniActivity";
private ActivityJniBinding binding;
//实现一个JNIDEMO实例对象
private JNIDEMO mJniDemo = new JNIDEMO();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityJniBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Example of a call to a native method
TextView tv = binding.sampleText;
//通过JNIDEMO实例调用java native方法,从而调用到Jni方法,实现对Jni字符串的获取
tv.setText(mJniDemo.JavaGetStringFromJNI());
}
}
3.2.2 JNIDEMO.java
package com.android.demo.jni;
public class JNIDEMO {
private static final String TAG = "JNIDEMO";
// 应用启动时,load编译Jni生成的so
static {
System.loadLibrary("jnidemo");
}
//Java从Jni获取String
public native String JavaGetStringFromJNI();
}
3.2.3 jnidemo.cpp
#include <jni.h>
#include <string>
#include <unistd.h>
#include <android/log.h> //引用android log
#include <stdlib.h>
#include <stdio.h>
#include "jnidemo.h"
//定义日志打印的方法
#define TAG "JNITEST" // 这个是自定义的LOG的标识
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) // 定义LOGD类型
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__) // 定义LOGI类型
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__) // 定义LOGW类型
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__) // 定义LOGE类型
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__) // 定义LOGF类型
using namespace std;
#ifdef __cplusplus //兼容C++代码
extern "C" { //兼容C代码
JNIEXPORT jstring JNICALL
Java_com_android_demo_jni_JNIDEMO_JavaGetStringFromJNI(
JNIEnv *env, //Java虚拟机内存地址指针
jobject instance //Java对象实例
) {
string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
}
#endif
3.2.4 jnidemo.h
此次Demo实现重点在工程架构,功能并不复杂,与默认创建的Jni Demo一样,只是传递一个string,所以.h文件中暂未有变量和函数声明。
/*
* Created by Shawn.xiao on 2023/12/31.
*/
#ifndef MYDEMO_JNIDEMO_H
#define MYDEMO_JNIDEMO_H
#endif
#ifdef __cplusplus
extern "C" {
}
#endif
3.2.5 build.gradle
build.gradle的android{ }中需要指定 jnilibs 和 CMakeList.txt 两个路径
android {
...
sourceSets {
main{
jniLibs.srcDirs = ['src/main/jnilibs']
}
}
externalNativeBuild {
cmake {
path file('src/main/jnicpp/CMakeLists.txt')
version '3.18.1'
}
}
...
}
如果要导入第三方库,CMake在编译时,会默认在jniLibs.srcDirs目录下查找和编译如下4个主流平台的库,如果这4个平台的库没有配置全,编译就会报错。
但是通常我们只会配置移动端64位的库,也就是arm64-v8a
所以需要在build.gradle :: android{} :: defaultConfig{}里加上如下代码进行过滤
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
abiFilters "arm64-v8a"
arguments "-DANDROID_STL=c++_shared", "-DANDROID_ARM_NEON=TRUE"
//arguments "-DANDROID_STL=c++_static", "-DANDROID_ARM_NEON=TRUE"
}
}
}
...
}
3.2.6 CMakeLists.txt
(1).最基础的CMakeList.txt
# 设置CMake版本
cmake_minimum_required(VERSION 3.18.1)
# 设置变量
set(jnicpp_src "${CMAKE_SOURCE_DIR}/src") #src源码路径
set(jnicpp_inc "${CMAKE_SOURCE_DIR}/inc") #inc头文件路径
set(jnilibs_dir "${CMAKE_SOURCE_DIR}/src/main/jnilibs") #so/.a
# 1.创建和命名库,本demo里是要生成的库jnidemo.so
# 2.设置要生成的库的属性:STATIC(.a) 或 SHARED(.so)
# 3.设置生成库的源码路径
# 4.可以定义多个库,CMake都会进行编译,Gradle再自动将库打包到Apk
add_library(
jnidemo #设置so文件名称
SHARED #设置这个so文件为共享
${jnicpp_src}/jnidemo.cpp) #源码路径
#指定需要使用的公共NDK库
find_library(
log-lib # 设置路径变量名称
log) # 指定需要CMake去搜寻定位的公共NDK库
#链接头文件
target_include_directories(
jnidemo #Jni库
PRIVATE #对外引用属性
${jnicpp_inc}) #头文件路径
#包含头文件
#这个方法与target_include_directories()不同
#设置后,当前目录的所有子目录中的CMakeLists.txt头文件包含都会引用该方法中的变量定义
#include_directories(${jnicpp_inc})
# 指定需要用CMake链接到目标库的库。
# 可以链接多个库,例如在本脚本中定义的库、预构建的第三方库或系统库。
target_link_libraries(
jnidemo #指定目标库
${log-lib} # 链接NDK中的log-lib库到目标库
)
一个最基本的CMakeList.txt就写成了
编译工程生成apk,将后缀.apk改为.zip后解压,就会发现lib文件夹
看看对应64位移动端的arm64-v8a目录:
到这里,一个能够加载第三方库和C/C++文件的Jni App基本成型了
但是,对于要导入的第三方库,这个最基础的CMakeLists.txt能做到的仅仅只是把它们加载到了Apk中,如果Jni代码中需要引用到这些三方库里的方法,那么在CMakeLists.txt里还需要对三方库进行设置和链接,将它们链接到最终会编译生成的Jni库上。
(2).链接三方库的CMakeLists.txt
# 设置CMake版本
cmake_minimum_required(VERSION 3.18.1)
# 设置变量
set(jnicpp_inc "${CMAKE_SOURCE_DIR}/inc") # jnicpp/inc头文件目录路径
set(jnicpp_src "${CMAKE_SOURCE_DIR}/src") # jnicpp/src源文件目录路径
# CMake所有内置变量都只能到CMakeLists.txt所在目录路径,下面方式可以获取CMakeLists.txt所在目录的上一级目录路径
string(REGEX REPLACE "(.*)/(.*)" "\\1" CMAKE_UP_PATH ${PROJECT_SOURCE_DIR})
set(jnilibs_dir "${CMAKE_UP_PATH}/jnilibs") ##jnilibs目录路径 so/.a
# 1.使用add_library命令创建和命名要生成的jni库,本demo里要生成的是jnidemo.so
# 2.设置要生成的库的属性:STATIC(.a) 或 SHARED(.so)
# 3.设置生成库的源码路径
add_library(
jnidemo #设置so文件名称
SHARED #设置这个so文件为共享
${jnicpp_src}/jnidemo.cpp) #源码路径
#查找指定需要使用的公共NDK库
find_library(
log-lib # 设置路径变量名称
log) # 指定需要CMake去搜寻定位的公共NDK库
# 使用add_library命令,通过指定IMPORTED选项表明这是一个要导入的库文件
# 相当于告知CMake,我要链接三个SHARED动态库:aaa、bbb、ccc
# 这三句必须在前面,要不然后面的语句不知道aaa、bbb、ccc是什么
add_library(aaa SHARED IMPORTED)
add_library(bbb SHARED IMPORTED)
add_library(ccc SHARED IMPORTED)
# CMake属性设置函数,IMPORTED_LOCATION 表示设置目标aaa、bbb、ccc的文件路径属性
# ${CMAKE_SOURCE_DIR}:表示CMakeLists.txt的当前文件夹路径
# ${ANDROID_ABI}:编译时会自动根据CPU架构去选择相应的库
set_target_properties(aaa
PROPERTIES
IMPORTED_LOCATION
"${jnilibs_dir}/${ANDROID_ABI}/libqxrcamclient.so")
set_target_properties(bbb
PROPERTIES
IMPORTED_LOCATION
"${jnilibs_dir}/${ANDROID_ABI}/libqxrcoreclient.so")
set_target_properties(ccc
PROPERTIES
IMPORTED_LOCATION
"${jnilibs_dir}/${ANDROID_ABI}/libqxrsplitclient.so")
#链接头文件目录路径
target_include_directories(
jnidemo #Jni库
PRIVATE #对外引用属性
${jnicpp_inc}) #头文件路径
#包含头文件
#这个方法与target_include_directories()不同
#设置后,当前目录的所有子目录中的CMakeLists.txt头文件包含都会引用该方法中的变量定义
#include_directories(${jnicpp_inc})
# 指定需要用CMake链接到目标库的库。
# 可以链接多个库,例如在本脚本中定义的库、导入的第三方库或系统库。
target_link_libraries(
jnidemo #指定目标库
${log-lib} # 链接NDK中的log-lib库到目标库
aaa
bbb
ccc
)
四.结束语
到此,一个可以加载三方库、编译C/C++的Jni App就搭建完成了
这一篇博文主要介绍Jni App的项目架构,构建文件编写等, 并没有涉及Jni代码语法
下一篇会在此篇基础上讲解Jni开发详细语法。