赞
踩
Android Jni机制让开发者可以在Java端调用到C/C++,也是Android应用开发需要掌握的一项重要的基础技能。
计划分两篇博文讲述Jni实战开发。
本篇主要从项目架构上剖析一个Android App如何通过Jni机制加载三方库和C/C++文件。
Android Studio可以直接创建一个可运行的、最简单的Jni Demo App。
字符串"Hello from C++"从Jni传到Java再在TextView上显示。
运行:
文件目录结构:
这个默认创建的Jni Demo App的代码就不一一展示了。
接下来会详细讲解自定义Jni App架构和代码改造过程
默认创建的Jni Demo App只是简单实现了一个字符串在Jni和Java之间的传递,并没有涉及到加载三方库和.c/.cpp,所以接下来要做的就是,在默认Jni Demo App基础上进行升级改造,实现一个便于扩展、能够加载三方库和.c/.cpp的Demo。
先看看改造后的目录结构:
相对于AndroidStudio默认创建的Jni Demo App,主要的修改点有如下:
- 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());
- }
- }
- 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();
- }
- #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
此次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
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"
- }
- }
- }
-
- ...
-
- }
- # 设置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库上。
- # 设置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开发详细语法。
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。