Android:JNI实战,加载三方库、编译C/C++

一.概述

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 只是简单实现了一个字符串在JniJava 之间的传递,并没有涉及到加载三方库 和**.c/.cpp** ,所以接下来要做的就是,在默认Jni Demo App 基础上进行升级改造,实现一个便于扩展、能够加载三方库和**.c/.cpp** 的Demo。

先看看改造后的目录结构:

相对于AndroidStudio 默认创建的Jni Demo App,主要的修改点有如下:

  1. MainActivity.java 中的Load Jni so 以及native 函数声明部分单独抽离出来,写成一个专门的JNIDEMO.java 文件,便于对Jni的调用
  2. java 目录平级新建 jnicpp 目录放置**C/C++**源码文件
  3. java 目录平级新建 jnilibs 目录放置需要加载的三方库
  4. 变更CMakeLists.txt 位置,放置在jnilibs根目录

3.2 源码解析

3.2.1 JniActivity.java
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
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
java 复制代码
#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**文件中暂未有变量和函数声明。

javascript 复制代码
/*
* 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.gradleandroid{ } 中需要指定 jnilibsCMakeList.txt两个路径

java 复制代码
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{}**里加上如下代码进行过滤

bash 复制代码
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
bash 复制代码
# 设置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
bash 复制代码
# 设置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开发详细语法。

相关推荐
像污秽一样13 分钟前
Spring MVC初探
java·spring·mvc
计算机-秋大田14 分钟前
基于微信小程序的乡村研学游平台设计与实现,LW+源码+讲解
java·spring boot·微信小程序·小程序·vue
LuckyLay17 分钟前
Spring学习笔记_36——@RequestMapping
java·spring boot·笔记·spring·mapping
醉颜凉1 小时前
【NOIP提高组】潜伏者
java·c语言·开发语言·c++·算法
阿维的博客日记1 小时前
java八股-jvm入门-程序计数器,堆,元空间,虚拟机栈,本地方法栈,类加载器,双亲委派,类加载执行过程
java·jvm
qiyi.sky1 小时前
JavaWeb——Web入门(8/9)- Tomcat:基本使用(下载与安装、目录结构介绍、启动与关闭、可能出现的问题及解决方案、总结)
java·前端·笔记·学习·tomcat
hunandede1 小时前
FFmpeg 4.3 音视频-多路H265监控录放C++开发十三.2:avpacket中包含多个 NALU如何解析头部分析
c++·ffmpeg·音视频
lapiii3581 小时前
图论-代码随想录刷题记录[JAVA]
java·数据结构·算法·图论
RainbowSea1 小时前
4. Spring Cloud Ribbon 实现“负载均衡”的详细配置说明
java·spring·spring cloud
程序员小明z1 小时前
基于Java的药店管理系统
java·开发语言·spring boot·毕业设计·毕设