Android 使用ndk-build和cmake构建JNI程序

文末附源码

先简单看下效果,如图:

 可以看出来,效果非常的简单,跟网上大部分例子一样,选择实现两个数字的加法运算。

开发环境:

AndroidStudio版本:Android Studio Dolphin | 2021.3.1 Patch 1

ndk和cmake的版本均是从AndroidStudio中下载的最新的版本

接下来,本文将会分别使用ndk-build和cmake两种方式实现jni

(默认大家已经创建了一个新的项目)

共有部分:

1.布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="20dp"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/et_no1"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:hint="number 1" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:text="+" />

    <EditText
        android:id="@+id/et_no2"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:layout_marginTop="10dp"
        android:hint="number 2" />

    <Button
        android:id="@+id/btn_plus"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:layout_marginTop="10dp"
        android:text=" = " />

    <EditText
        android:id="@+id/et_result"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:layout_marginTop="10dp"
        android:enabled="false"
        android:hint="result" />
</LinearLayout>

2.MainActivity

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        binding.btnPlus.setOnClickListener(view -> {
            String number1 = !binding.etNo1.getText().toString().trim().isEmpty() ? binding.etNo1.getText().toString().trim() : "0";
            String number2 = !binding.etNo2.getText().toString().trim().isEmpty() ? binding.etNo2.getText().toString().trim() : "0";
            binding.etResult.setText(String.valueOf(JNIUtils.plusFromJNI(Integer.parseInt(number1), Integer.parseInt(number2))));
        });
    }
}

我这里使用了viewbind,具体配置方法:

android {
  //...其他
    viewBinding{
        enabled true
    }
}
3.JNIUtils
public class JNIUtil {
    //通过jni,调用c/c++ 函数
    public static native int doPlus(int a, int b);

    // Used to load the 'cmakejnidemo' library on application startup.
    static {
        System.loadLibrary("jnidemo");
    }
}

至此,共有部分完成,剩余部分将会分类展示

(1)ndk-build 方式

1.创建头文件,如图

说明: 编译获取头文件我这里使用的是javac -h ./ JNIUtils.java

(1)旧版本sdk使用的是javah ./ JNIUtils.java,但是高版本sdk并不支持javah,需要使用javac -h;

(2)命令中./表示的是当前文件,javac 命令需要指定编译完成后的文件存放到什么位置,如果这里不指定位置,直接写javac - h JNIUtils.java,那么就会报错:无源文件,如图

 (3)使用cd命令进入JNIUtils.java所在目录时,需要先看看当前在什么目录,而不是非得按照图示中那么敲,查看当前目录的命令是pwd,如图

 调用pwd命令,发现我现在就在JNIUtils目录下,那么此时我就可以直接执行javac -h命令

假如说,我现在在"E:\AndroidProject\NDKDemo\app\src\main\"这个目录下,

那么显然就需要执行"cd main/java/com/example/ndkdemo"

然后就可以进入目标目录下了

说明:

cd main/java/com/example/ndkdemo

cd .\main\java\com\example\ndkdemo\

效果是相同的,写法都支持

2.创建jni目录,将第一步生成的文件放入jni目录中

3.创建.c文件,实现native方法

  

com_example_ndkbuildjnidemo_JNIUtils.c

extern "C" JNIEXPORT jint JNICALL
Java_com_example_ndkbuildjnidemo_JNIUtils_plusFromJNI(JNIEnv *env,jobject thiz,jint number1,jint number2) {
    return no1+no2;
}

说明:

JNIEnv *env:表示指向JNI接口函数表的指针,通过这个指针可以调用JNI接口函数,实现Java和本地代码的交互。

jobject thiz:表示Java中的对象引用,即调用本地代码的Java对象。在本地代码中可以通过这个对象引用调用Java对象的成员方法和属性。

number1和number2则是与前面在JNIUtils中定义的native方法的两个形参

这里因为只是简单的计算,所以直接返回number1+number2即可。

当然这个.c代码中也可以根据自己实际需求编写C/C++代码区定义一些其他比较复杂的逻辑。

4.定义Android.mk和Application.mk

Android.mk文件是Android Native Development Kit (NDK) 中的一个重要文件,它是一个Makefile文件,用于编译Android应用程序项目中的本地代码,例如C/C++源文件、静态库、动态库等。

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := jnidemo
LOACA_SRC_FILES := com_example_ndkbuildjnidemo_JNIUtils.c
include $(BUILD_SHARED_LIBRARY)

这个文件,在使用的时候,需要根据自己的实际配置进行修改,其实主要修改

LOCAL_MODULE 和LOACA_SRC_FILES 这两项

分别填的是本地JNI库的名字(可以在JNIUtils文件中找到)和上一步创建的.c代码的名字

Application.mk

APP_ABI := all

5.build.gradle文件中,指定Android.mk文件的位置

android {
    //其他
    externalNativeBuild {
        ndkBuild {
            path "src/main/jni/Android.mk"
        }
    }
}

至此,rebuild一下项目,即实现了使用ndk-build的jni调用了。

说明:使用ndk-build生成的so库在这里:

(2)cmake 方式

1.新建CMakeDemo项目,复制共有部分代码,main目录下,新建cpp目录

2.在cpp目录下,创建CMakeLists.txt 

 

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.18.1)

# Declares and names the project.

project("jnidemo")

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

add_library( # Sets the name of the library.
        jnidemo

        # Sets the library as a shared library.
        SHARED

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

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log)

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

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

说明:

        1.cmakelists.txt文件可以直接复制上面的内容,但是要修改成自己的配置;

        2. add_library(的第一个字段与target_link_libraries(的第一个字段都是需要加载的本地JNI库的名字,我这里命名与ndk-build方式的命名相同,就是为了比较二者的配置;

        3.add_library(的第三个字段为本地实现native方法的cpp文件,文件名可以更改,但是要注意同时修改本处配置,保持一致,否则就会调用不到方法导致报错

        4.Android默认支持的是cmake方法,并且在创建项目的时候,即可以选择支持C++,如下:

        

          即在构建项目的时候,选择最下面的“Native C++”,然后后面的按照默认的走就可以用了

        当然,可能不同的AndroidStudio版本,创建JNI项目的流程是不一样的,但是都是在创建项目的时候勾选选项,这里只是给一点思路,如果发现你的AndroidStudio没有我上面的那些,可以网上搜索一下对应的方法,或者是在页面中仔细找相关的勾选项,一般是很容易找到的

3.在cpp目录下,新建native-lib.cpp文件

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

extern "C" JNIEXPORT jint JNICALL
Java_com_example_cmakedemo_JNIUtil_doPlus(
        JNIEnv *env,
        jclass thiz,
        jint number1,
        jint number2) {
    return number1+number2;
}

4.build.gradle

android {
   

    //其他
    defaultConfig {


        //其他
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }
       
    }

  
    externalNativeBuild {
        cmake {
            path file('src/main/cpp/CMakeLists.txt')
            version '3.18.1'
        }
    }
}

至此,rebuild一下项目,即实现了使用cmake的jni调用了。

说明:使用cmake方式生成的so库在这里

说明:

关于ndk-build和cmake两种方式,其实弄下来,发现虽然具体的配置不一样,但是基本上就是实现了那么点东西:定义一个方法或者是放在activity中创建一个native方法,并且指定app启动时加载的本地JNI库,然后把这个方法或者是activity生成.h头文件,然后在头文件的基础上,写.c文件去实现native方法,然后就是定义打包编译文件,这一块二者有区别,但是归根到底还是要指定本地JNI库的名字以及本地.c/cpp文件的路径,打包编译完成,生成so库,so库使用默认的路径即可,也可以放在lib文件下,或者是生成一个jniLibs文件夹放进去,然后使用起来。整个过程就算是结束了,但是要注意,一个module中只能使用一种方式构建jni,要么cmake,要么ndk-build。 

写在后面:

        我想说一下整理这篇博客的原因。前两天我开始研究Android JNI的内容,算是开始了系统的学习。之前其实也做过支持JNI的项目,但是那都是接手别人的,对于JNI模块一般创建好了,轻易不会需要改动的,所以一直对于这块也没怎么细究。

        前天,我在网上到处找相关的资料、博客、书籍,想着能够找一个简单点的,让我了解流程的就行,很遗憾,网上的资源新的新,旧的旧,质量也是参差不齐,跟着别人的弄了一天,到最后啥都没弄成,要么是环境不对了,要么是编译不出来so文件,反正就是各种出问题,但是网上去找问题的解决方案吧,还找不到,要不然就是引入一个新坑,一天折腾下来,差点日志都没法写。

        越是这样,我越觉得我必须得整理出来一份能用的关于构建jni项目的demo出来,我写博客的原则是,我希望别人按照我写出来的东西一步步弄下来,能用,起码不会说让别人觉得看我写的东西是在浪费时间,这就是我最低的要求了。所以这篇博客里,我不仅仅是写了流程,还尽可能地把我自己在前期摸索时,遇到的问题以及我找到的解决方案也列举出来。

        最后希望这篇从找资料、各种测demo,到整理文字花了两天的博客可以帮到诸位。有什么问题,评论区探讨,如果时间充足,可以一起协助。

 demo源码

猜你喜欢

转载自blog.csdn.net/weixin_53324308/article/details/131207665