Android Studio引入ndk编译的so库, 通过jni给Java程序使用

news2024/11/13 10:56:23

前言

工作要求将一个C++老项目的函数用ndk打包成库给安卓同事的java程序调用。

这个任务我debuff拉满:

  • 自己之前从来没接触过安卓开发,问了老板为什么不让安卓开发来干,老板说安卓开发不懂c++,公司就我一个是懂c++的。。。
  • 项目开发年限超过十年,只在32位系统编译过,一些32位可以通过的代码到了64位就不行了,很多的库多少有些兼容问题
  • 项目开发环境全程断网,给开发带来诸多不便

感谢智谱和GPT4,最后花了几天还是摸清了一条路出来,不然可能过了一周都搞不清android.mk要怎么写。

不废话,笔记如下:

知识点记录:
NDK:Native Development Kit,是 Android 的一个工具开发包。NDK 可以看做是 Android 中实现 JNI 的一种手段(另一种是CMake),通过 NDK,还可以打包 C/C++ 动态库,并自动打包进 APK/AAR 中。我们可以到安卓官网下载NDK,可以直接执行NDK命令,也可以集成NDK到AndroidStudio中编译C/C++文件。

JNI:Java Native Interface,即 Java 本地接口。使得 Java 与本地其他类型语言(如 C、C++)交互。也就是在 Java 中调用 C/C++ 代码,或者在 C/C++ 中调用 Java 代码。JNI 是 Java 的,和 Android 无关。
.SO文件:so文件是Linux下的程序函数库,即编译好的可以供其他程序使用的代码和数据。一般来说.so文件就是常说的动态链接库, 都是C或C++编译出来的。与Java比较就是:它通常是用的Class文件(字节码)。Linux下的.so文件时不能直接运行的,一般来讲,.so文件称为共享库。

Android.mk :文件位于项目 jni/ 目录的子目录中,用于向编译系统描述源文件和共享库。它实际上是编译系统解析一次或多次的微小 GNU makefile 片段。Android.mk 文件用于定义 Application.mk、编译系统和环境变量所未定义的项目范围设置。它还可替换特定模块的项目范围设置。Android.mk 的语法支持将源文件分组为模块。模块是静态库、共享库或独立的可执行文件。

开发环境

OS:Windows 11

Android Studio版本:2021.3.1.17 Download link

NDK: r26d download link

开始操作

编译安卓so库

首先得搞清楚ndk-build怎么使用,这东西本质是对clang的封装, android.mk就相当于ndk的makefile,这里用一个c++的四则运算来讲。

创建这么个项目:

PS D:\MyCodeBase\ndk_test> ls
目录: D:\MyCodeBase\ndk_test

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----         2024/7/13     17:34            594 android.mk
-a----         2024/7/13     17:29             71 application.mk
-a----         2024/7/13     17:27            838 mycal.cpp
-a----         2024/7/13     17:27            520 mycal.h

c++的四则运算类实现如下,我这里参考公司的实现方式,一个亮点是使用了工厂模式,头文件类只放基类,基类内全是纯虚函数(接口),实现在源文件里另有子类处理,头文件只提供creator创建实现类,返回基类指针,通过基类指针来操作接口方法。

// mycal.h

#ifndef MYCAL_H
#define MYCAL_H

#include <iostream>
#include <string>

// 运算接口基类
class Calculator {
public:
    virtual ~Calculator() = default;

    virtual double add(double a, double b) const = 0;
    virtual double sub(double a, double b) const = 0;
    virtual double mul(double a, double b) const = 0;
    virtual double div(double a, double b) const = 0;
};

// 非类成员工厂函数声明
Calculator* createCalculator();

#endif // MYCAL_H
// mycal.cpp

#include "mycal.h"
#include <stdexcept>

// 实现Calculator接口的具体类
class CalculatorImpl : public Calculator {
public:
    double add(double a, double b) const override {
        return a + b;
    }

    double sub(double a, double b) const override {
        return a - b;
    }

    double mul(double a, double b) const override {
        return a * b;
    }

    double div(double a, double b) const override {
        if (b != 0.0) {
            return a / b;
        } else {
            throw std::runtime_error("Division by zero.");
        }
    }
};

// 工厂函数实现
Calculator* createCalculator() {
    return new CalculatorImpl();
}

ndk-build通过android.mk和application.mk来控制编译,android.mk已经提过,什么是application.mk? 其实是另一个脚本,专门用于控制编译平台相关的东西,比如android平台版本,ABI架构,我在这个例子中指定平台为android-24,输出x86, x86_64, arm64-v8a, armeabi-v7a四种架构的so库。

#application.mk
APP_PLATFORM := android-24
APP_ABI := x86 x86_64 arm64-v8a armeabi-v7a
# android.mk
# build: ndk-build NDK_PROJECT_PATH="./" APP_BUILD_SCRIPT="./android.mk" NDK_APPLICATION_MK="./application.mk" V=1
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := mycal 					#模块名
LOCAL_SRC_FILES := mycal.cpp   				#源文件路径
LOCAL_C_INCLUDES := $(LOCAL_PATH) 			#指定头文件搜索路径
LOCAL_CPPFLAGS := -fexceptions 				#启用throw错误,默认不启用会保存
LOCAL_LDLIBS    := -llog -lc++_shared 		#指定链接库, 其中c++_shared对于c++文件编译必须要加上,否则会报错

include $(BUILD_SHARED_LIBRARY)

ndk-build编译:

ndk-build NDK_PROJECT_PATH="./" APP_BUILD_SCRIPT="./android.mk" NDK_APPLICATION_MK="./application.mk" V=1

参数讲解:

NDK_PROJECT_PATH:指定ndk项目的根路径

APP_BUILD_SCRIPT:指定android.mk路径

NDK_APPLICATION_MK:指定application.mk路径

V=1:输出clang的编译日志,默认不输出

ndk-build的更多选项可见:ndk-build官方使用说明

命令编译成功后,应该会在目录下出现libs和objs两个文件夹,其中libs存在指定的各个架构下的so库:

PS D:\MyCodeBase\ndk_test> tree /F
卷 New Volume 的文件夹 PATH 列表
卷序列号为 6AC4-31C7
D:.
│  android.mk
│  application.mk
│  main.cpp
│  mycal.cpp
│  mycal.h
│
├─libs
│  ├─arm64-v8a
│  │      libmycal.so
│  │
│  ├─armeabi-v7a
│  │      libmycal.so
│  │
│  ├─x86
│  │      libmycal.so
│  │
│  └─x86_64
│          libmycal.so
│
└─obj
    ....

自此android程序需要的so库编译完成。

android studio的jni编程

打开android studio,新建一个native c++项目MyCalLib:

image-20240717111945879

在MyCalLib项目中,将ndk_test项目的libs文件夹拷贝到对应的libs文件夹(更老版本可能是jniLibs,不过我这个创建就有libs), 并将mycal.h拷贝到cpp文件夹,接着修改CMakeLists.txt将so库引入:

cmake_minimum_required(VERSION 3.18.1)

# Declares and names the project.

project("mycallib")


find_library(log-lib log)
find_library(c++_shared-lib c++_shared)

# 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.
set(my_lib_path ${CMAKE_SOURCE_DIR}/../../../libs)
add_library(mycal SHARED IMPORTED)
set_target_properties(mycal PROPERTIES IMPORTED_LOCATION ${my_lib_path}/${ANDROID_ABI}/libmycal.so)

add_library( # Sets the name of the library.
        mycallib

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        native-lib.cpp
        )


# 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.
        mycallib

        # Links the target library to the log library
        # included in the NDK.
        ${log-lib}
        ${c++_shared-lib}
        mycal
        )

image-20240717113422508

然后在MainActitivity.java声明jni接口:

    public native void calInit();
    public native double add(double a, double b);
    public native double sub(double a, double b);
    public native double mul(double a, double b);
    public native double div(double a, double b);

在native-lib.cpp实现这些jni接口,jni的命名方式为Java_包名_类名_方法名,不过AS下应该能自动生成函数签名。

#include <jni.h>
#include <string>
#include "mycal.h"


extern "C" JNIEXPORT jstring JNICALL
Java_com_example_mycallib_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
Calculator* cal;
extern "C"
JNIEXPORT void JNICALL
Java_com_example_mycallib_MainActivity_calInit(JNIEnv *env, jobject thiz) {
    cal = createCalculator();
}
extern "C"
JNIEXPORT jdouble JNICALL
Java_com_example_mycallib_MainActivity_add(JNIEnv *env, jobject thiz, jdouble a, jdouble b) {
    return cal->add(a, b);
}
extern "C"
JNIEXPORT jdouble JNICALL
Java_com_example_mycallib_MainActivity_sub(JNIEnv *env, jobject thiz, jdouble a, jdouble b) {
    return cal->sub(a, b);
}
extern "C"
JNIEXPORT jdouble JNICALL
Java_com_example_mycallib_MainActivity_mul(JNIEnv *env, jobject thiz, jdouble a, jdouble b) {
    return cal->mul(a, b);
}
extern "C"
JNIEXPORT jdouble JNICALL
Java_com_example_mycallib_MainActivity_div(JNIEnv *env, jobject thiz, jdouble a, jdouble b) {
    return cal->div(a, b);
}

函数的生成结果需要在机子上测试,测试界面activity_main.xml准备四个文本框,分别显示四则运算的运算结果:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/sample_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_add"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.554" />

    <TextView
        android:id="@+id/tv_sub"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.639" />

    <TextView
        android:id="@+id/tv_mul"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.502"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.683" />

    <TextView
        android:id="@+id/tv_div"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.744" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.java测试代码:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        calInit();

        // Example of a call to a native method
        TextView tv_add = binding.tvAdd;
        TextView tv_sub = binding.tvSub;
        TextView tv_mul = binding.tvMul;
        TextView tv_div = binding.tvDiv;

        double val = add(5,6);
        tv_add.setText(String.valueOf(val));
        val = sub(11,10);
        tv_sub.setText(String.valueOf(val));
        val = mul(11, 10);
        tv_mul.setText(String.valueOf(val));
        val = div(12,7);
        tv_div.setText(String.valueOf(val));


    }

自此一切准备工作完毕,Device Manager>Create device创建一个安卓虚拟机,这里指定系统为x86,API 24。

image-20240717114626543

创建成功后build run, 如果运行成功,虚拟机应该会打开一个app,里面是activity_main.xml上测试代码的输出结果。

image-20240717115620697

坑点记录

  1. android studio不能用的太新,一开始直接官网下的最新的android studio koala 2024.1来弄,结果发现网上很多教程(包括AI回答)都停留在安卓的老版本,很多项目设置找不到,ndk路径设置找不到(后来才知道谷歌2021年出于多余理由把ndk选项从AS中移除了),这个版本用的gradle 8.0,build.gradle一些cmake和ndk的设置放到上面根本build不了,寒。。。最后请了安卓同事给我弄了这个老版本,gradle7.4,build.gradle的一些设置缺失果然就没了。

  2. android.mk编译的第三方so库如果有引入c++_shared.so库,AS中cmake版本太低是不会识别编译选项的,届时运行会在Logcat上报找不到库的错误,而升级cmake的提示,在有中间文件生成时是不会提醒的。就因为这个不会提示,我从下午4点一直被硬控到第二天下午3点。

image-20240717120039386

image-20240717120055924

解决方法:Tools>SDK Manager>SDK Tools, 找到CMake, 下载3.19以上的版本,可勾选Show Package Details看到版本信息。

image-20240717112751480

参考

https://developer.android.com/ndk/guides/android_mk?hl=zh-cn

ndk-build官方使用说明

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1937350.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【STM32嵌入式系统设计与开发---拓展】——1_10矩阵按键

这里写目录标题 1、矩阵按键2、代码片段分析 1、矩阵按键 通过将4x4矩阵按键的每一行依次设为低电平&#xff0c;同时保持其它行为高电平&#xff0c;然后读取所有列的电平状态&#xff0c;可以检测到哪个按键被按下。如果某列变为低电平&#xff0c;说明对应行和列的按键被按下…

day2 单机并发缓存

文章目录 1 sync.Mutex2 支持并发读写3 主体结构 Group3.1 回调 Getter3.2 Group 的定义3.3 Group 的 Get 方法 4 测试 本文代码地址&#xff1a; https://gitee.com/lymgoforIT/gee-cache/tree/master/day2-single-node 本文是7天用Go从零实现分布式缓存GeeCache的第二篇。 …

go 实现websocket以及详细设计流程过程,确保通俗易懂

websocket简介&#xff1a; WebSocket 是一种网络传输协议&#xff0c;可在单个 TCP 连接上进行全双工通信&#xff0c;位于 OSI 模型的应用层。WebSocket 协议在 2011 年由 IETF 标准化为 RFC 6455&#xff0c;后由 RFC 7936 补充规范。 WebSocket 使得客户端和服务器之间的数…

昇思学习打卡-21-生成式/Diffusion扩散模型

文章目录 Diffusion扩散模型介绍模型推理结果 Diffusion扩散模型介绍 关于扩散模型&#xff08;Diffusion Models&#xff09;有很多种理解&#xff0c;除了本文介绍的离散时间视角外&#xff0c;还有连续时间视角、概率分布转换视角、马尔可夫链视角、能量函数视角、数据增强…

《样式设计003:布局-自定义view模块》

描述&#xff1a;在开发小程序过程中&#xff0c;发现一些不错的案例&#xff0c;平时使用也比较多&#xff0c;稍微总结了下经验&#xff0c;以下内容可以直接复制使用&#xff0c;希望对大家有所帮助&#xff0c;废话不多说直接上干货&#xff01; 一、布局-自定义view模块 …

el-popover嵌套select弹窗点击实现自定义关闭

需求 el-popover弹窗内嵌套下拉选择框&#xff0c;点击el-popover弹出外部区域需关闭弹窗&#xff0c;点击查询、重置需关闭弹窗&#xff0c; 实现 根据需求要自定义弹窗的关闭和显示&#xff0c;首先想到的是visible属性&#xff0c;在实现过程中经过反复的测验&#xff0…

服务级别协议SLA与运营水平协议OLA

使用美团或饿了么在线订餐时&#xff0c;您将体验到即时的送餐提醒服务。首先&#xff0c;选择您想要的食品。系统会根据餐厅与您的位置、所选食品的种类&#xff0c;以及下单的具体时间&#xff0c;计算预计的等待时间和送餐费用&#xff0c;并将这些信息与您共享。这种信息的…

剖析SGI-STL二级空间配置器

概述 SGI-STL与C标准库提供的STL一样&#xff0c;都通过空间配置器allocator来申请或释放容器的空间。空间配置器的作用可以参考&#xff1a;浅谈C空间配置器allocator及其重要性 // C标准库的vector template < class T, class Alloc allocator<T> > class vec…

混淆专题一——简单AA,JJ,JSFuck混淆处理办法

以AA混淆为例 网址&#xff1a;Scrape | NBA 想要获取球员的信息&#xff0c;但找不到包。 刷新页面&#xff0c;main.js中找到混淆的代码&#xff0c;这串混淆代码就是球员信息。 如何处理&#xff1a; 复制下来&#xff0c;去除最后的笑脸 (_)&#xff0c;然后在控制台打…

启智集装箱箱号识别技术,更高效快捷

在当今这个信息技术高速发展的时代&#xff0c;集装箱箱号识别技术在全球物流领域扮演着至关重要的角色。随着物流行业的不断壮大和复杂化&#xff0c;对集装箱箱号识别的准确性、效率性和便捷性提出了更高的要求。启智集装箱箱号识别技术应运而生&#xff0c;以其高效快捷的特…

python-快速上手爬虫

目录 前言 爬虫需谨慎&#xff0c;切勿从入门到入狱&#xff01; 一点小小的准备工作 直接上手爬取网页 1.获取UA伪装 2.获取url 3.发送请求 4.获取数据并保存 总结 前言 爬虫需谨慎&#xff0c;切勿从入门到入狱&#xff01; 一点小小的准备工作 对pip进行换源&#xf…

【EI检索】第二届机器视觉、图像处理与影像技术国际会议(MVIPIT 2024)

一、会议信息 大会官网&#xff1a;www.mvipit.org 官方邮箱&#xff1a;mvipit163.com 会议出版&#xff1a;IEEE CPS 出版 会议检索&#xff1a;EI & Scopus 检索 会议地点&#xff1a;河北张家口 会议时间&#xff1a;2024 年 9 月 13 日-9 月 15 日 二、征稿主题…

vue3前端开发-小兔鲜项目-面包屑导航的渲染

vue3前端开发-小兔鲜项目-面包屑导航的渲染&#xff01;今天来完成&#xff0c;一级分类页面顶部&#xff0c;面包屑导航的渲染。 1&#xff1a;完善好一级页面内的基础模块代码。 <script setup> import {getCategoryAPI} from /apis/category import {ref,onMounted} …

【知识蒸馏】YOLO object detection 逻辑蒸馏

YOLO检测蒸馏 和分类和分割蒸馏的差异&#xff1a; 由于YOLOv3检测框的位置输出为正无穷到负无穷的连续值&#xff0c;和上面将的分类离散kdloss不同&#xff0c;而且由于yolo是基于anchor的one stage模型&#xff0c;head out中99%都是背景预测。 Object detection at 200 F…

【论文阅读笔记】Hierarchical Neural Coding for Controllable CAD Model Generation

摘要 作者提出了一种CAD的创新生成模型&#xff0c;该模型将CAD模型的高级设计概念表示为从全局部件排列到局部曲线几何的三层神经代码的层级树&#xff0c;并且通过指定目标设计的代码树来控制CAD模型的生成或完成。具体而言&#xff0c;一种带有“掩码跳过连接”的向量量化变…

【BUG】已解决:To update, run: python.exe -m pip install --upgrade pip

To update, run: python.exe -m pip install --upgrade pip 目录 To update, run: python.exe -m pip install --upgrade pip 【常见模块错误】 解决办法&#xff1a; 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 欢迎来到我的主页&#xff0c;我是博主英杰&…

「MQTT over QUIC」与「MQTT over TCP」与 「TCP 」通信测试报告

一、结论 在实车5G测试中「MQTT Over QUIC」整体表现优于「TCP」&#xff0c;可在系统架构升级时采用MQTT Over QUIC替换原有的TCP通讯&#xff1b;从实现原理上基于QUIC比基于TCP在弱网、网络抖动导致频繁重连场景延迟更低。 二、测试方案 网络类型&#xff1a;实车5G、实车…

FPGA-计数器

前言 之前一直说整理点FPGA控制器应用的内容&#xff0c;今天就从计数器这个在时序逻辑中比较重要的内容开始总结一下&#xff0c;主要通过还是通过让一个LED闪烁这个简单例子来理解。 寄存器 了解计数器之前先来认识一下寄存器。寄存器是时序逻辑设计的基础。时序逻辑能够避…

Android C++系列:Linux信号(三)

可重入函数 不含全局变量和静态变量是可重入函数的一个要素可重入函数见man 7 signal在信号捕捉函数里应使用可重入函数在信号捕捉函数里禁止调用不可重入函数例如:strtok就是一个不可重入函数,因为strtok内部维护了一个内部静态指针,保存上一 次切割到的位置,如果信号的捕捉…

android Invalid keystore format

签名的时候提示:Invalid keystore format. 点击info查看更多日志 再点击一次 stactrace 查看更多提示 提示&#xff1a;javaio异常 基本是jdk版本的问题&#xff0c;高jdk版本打的key&#xff0c;在低版本jdk开发环境上无法使用。 查看自己的key信息 keytool -list -v -keys…