Android JNI及NDK入门教程

2 篇文章 0 订阅
1 篇文章 0 订阅

Android JNI及NDK入门教程

为什么要使用JNI

  • 一、native语言性能更优。
  • 二、在Java诞生前很多库都是native语言写的,没必要再用Java实现一遍。
  • 三、安全性更好:不容易被反编译。

JNI是什么

  • 全称Java Native Interface。字面上理解就是Java和本地语言的接口。定义了Java和native语言之间互相通信的一套规范。(这是Java定义的,和安卓无关。)
  • NDK是将native语言编译成特定平台的可执行文件的一套开发组件。(将C/C++编译成so库,由Android定义的,和Java无关。)

所以jni和ndk是两个东西,ndk生成可执行so文件,jni通过加载so供java调用。如果有so文件,可以直接使用。

3、Native项目的创建

3.1、创建Native项目

cmake脚本文件定义编译和链接方式。

3.2、Native项目结构

在这里插入图片描述

之前的AS版本需要在local.properties文件指定ndk路径:ndk.dir=xxx。新版AS会自动合适的ndk版本,如果本地没有会推荐一个版本让你下载。

3.3、在Java中声明本地方法。

  • 一般我们有一个专门的访问native方法的Java类。比如本项目的NativeBridge.java。
public class NativeBridge {
    static {
        System.loadLibrary("native-lib");
    }

    // 本地方法1 静态注册。
    public static native String dataFromNative(int param);

    // 本地方法2 动态注册。
    public static native String nativeMethod1(int param);

    /// 本地方法3 动态注册。
    public static native String nativeMethod2(int param);

}

声明后需要在jni层进行注册和实现。注册native方法分为静态注册和动态注册。

4、Native方法的注册和实现。

4.1、Native方法的静态注册和动态注册的对比。

  • 静态注册写起来简单,可以AS快捷键一键生成。但运行效率低(第一次调用native方法时需要搜索一遍jni层的native方法,建立对应关系。)
  • 动态注册运行效率高(因为方法的映射关系我们已建立好)、写起来相对麻烦一点点。

4.2、静态注册

extern "C" JNIEXPORT jstring JNICALL
Java_com_hongenit_jnindkdemo_NativeBridge_dataFromNative(JNIEnv *env, jclass clazz, jint param) {
    std::string hello = "静态方法返回的Native字符串";
    return env->NewStringUTF(hello.c_str());
}

4.3、动态注册

  1. 准备好要注册的方法的JNINativeMethod结构体数组。
    // JNI本地方法的数组。
    static JNINativeMethod gMethods[] = {
            {"nativeMethod1", "(I)Ljava/lang/String;", (void*)nativeMethod1},
            {"nativeMethod2", "(I)Ljava/lang/String;", (void*)nativeMethod1},
    };
    

    JNINativeMethod结构体的

    java数据类型native 类型域描述符
    基本数据类型
    booleanjbooleanZ
    bytejbyteB
    charjcharC
    shortjshortS
    intjintI
    longjlongJ
    floatjfloatF
    doublejdoubleD
    voidvoidV
    对象引用类型以”L”开头,以”;”结尾,用”/” 隔开的包及类名,如果内部类则使用$连接内部类;
    SurfacejobjectLandroid/view/Surface;
    StringjstringLjava/lang/String;
    数组数据类型对应的基本数据类型前面加个中括号[
    int[]jintArray[I
    float[]jfloatArray[f
    Surface[]jobjectArray[Landroid/view/Surface;
  2. 覆写JNI_OnLoad方法。
    extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
    {
        JNIEnv* env = NULL;
        jint result = -1;
        if (vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK) {
            return result;
        }
        // 动态注册native方法。
        register_gmethods(vm, env);
        return JNI_VERSION_1_6;
    }
    
  3. 在JNI_OnLoad中调用RegisterNatives方法注册native方法。
    void register_gmethods(JavaVM *pVm, JNIEnv *pEnv) {
        static const char* const jclassName = "com/hongenit/jnindkdemo/NativeBridge";
        jclass jclazz = pEnv->FindClass(jclassName);
        pEnv->RegisterNatives(jclazz,gMethods,int(sizeof(gMethods)/sizeof(gMethods[0])));
    }
    

    生成的so在build目录下。

4.4、Native方法的实现。

5、cmake编译脚本

# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.10.2)

project("jnindkdemo")

# 命名并创建一个动态库(与静态库的区别是不被编译进目标内部),并指定其源码文件。可以定义多个库,CMake会为你编译好。gradle会自动将so库打包到apk中。
add_library(native-lib SHARED native-lib.cpp)


# 从NDK的系统库中搜索某个库并取个别名,通过这个别名可以定位到这个系统库的路径。
find_library(log-lib log)

# 指定哪些库需要Cmake链接到你的目标库。可以链接多个库,比如你在脚本中定义了的库、预编译好的三方库、系统库。
target_link_libraries(
        native-lib
        play gnustl_shared
        ${log-lib}
)

5.1、使用三方库

  • 假设需要用三方so库,在链接到目标库之前先添加定义的库。
#---------------------------三方so库----------------
#添加头文件路径(相对于本文件路径)
include_directories(include)

#设置so库所在路径的变量
set(SO_PATH ${CMAKE_CURRENT_SOURCE_DIR}/libs/${ANDROID_ABI})  #CMAKE_CURRENT_SOURCE_DIR变量为当前cmake文件所在的目录

add_library(gnustl_shared SHARED IMPORTED)
set_target_properties(gnustl_shared PROPERTIES IMPORTED_LOCATION ${SO_PATH}/libgnustl_shared.so)

add_library(play SHARED IMPORTED)
set_target_properties(play PROPERTIES IMPORTED_LOCATION ${SO_PATH}/libplay.so)

如果三方库A(play)需要依赖另一个三方库B,也必须将B(gnustl_shared)链接到目标库。否则会报错如下:

java.lang.UnsatisfiedLinkError: dlopen failed: library "libgnustl_shared.so" not found: needed by /data/app//lib/arm64/libplay.so in namespace。。。
  • 如下表示头文件在include目录下,所以应将so库的头文件放入include目录,然后可以在目标程序中引入头文件并调用方法。
#添加头文件路径(相对于本文件路径)
include_directories(include)

eg. 调用该so库的获取版本号方法

#include <play.h>
...
jstring nativeMethod1(JNIEnv *env, jclass clazz, jint param) {
	...
	unsigned version = PLAY_GetSdkVersion();
	...
}

6、调用Native方法

一般在一个专门访问native方法的类中,在类的静态代码块中用System.loadLibrary加载so库。调用这些声明在Java层Native方法即执行Native对应的实现。

public class NativeBridge {
    static {
        System.loadLibrary("native-lib");
    }

    // 静态注册本地方法。
    public static native String dataFromNative(int param);

    // 动态注册本地方法1 。
    public static native String nativeMethod1(int param);

    /// 动态注册本地方法2 。
    public static native String nativeMethod2(int param);

}

7、Native调用Java方法。

7.1、定义被调用的Java方法。

public class NativeBridge {
	...
	
    // 供Native调用的方法。
    public void printResult(String result) {
        System.out.println(" print result = " + result);
    }
}

7.2、Native调用流程。

  1. 获取Java方法所在类的字节码

    static const char *const jclassName = "com/hongenit/jnindkdemo/NativeBridge";
    // 获取NativeBridge的字节码对象
    jclass jclazz = env->FindClass(jclassName);
    

    还可以通过以下两个方法获取

    // 通过对象实例来获取jclass,相当于Java中的getClass()函数
    jclass GetObjectClass(jobject obj):
    
    // 通过jclass可以获取其父类的jclass对象
    jclass getSuperClass(jclass obj):
    
  2. 获取对象

    构造方法的methodId固定为“” ,根据签名不同调用不同的构造方法。

    jobject obj= env->NewObject(jclazz,env->GetMethodID(jclazz, "<init>", "()V"));
    
  3. 调用方法

    // 获取要调用的Java方法的methodID
    jmethodID methodId = env->GetMethodID(jclazz, "printResult", "(Ljava/lang/String;)V");
    // 调用Java方法。
    env->CallVoidMethod(obj,methodId,env->NewStringUTF(("call from native " + std::to_string(param)).c_str()));
    
void nativeMethod2(JNIEnv *env, jclass clazz, jint param) {
    
    static const char *const jclassName = "com/hongenit/jnindkdemo/NativeBridge";
    // 获取NativeBridge的字节码对象
    jclass jclazz = env->FindClass(jclassName);
    // 获取NativeBridge对象
    jobject obj= env->NewObject(jclazz,env->GetMethodID(jclazz, "<init>", "()V"));
    // 获取要调用的Java方法的methodID
    jmethodID methodId = env->GetMethodID(jclazz, "printResult", "(Ljava/lang/String;)V");
    // 调用Java方法。
    env->CallVoidMethod(obj,methodId,env->NewStringUTF(("call from native " + std::to_string(param)).c_str()));

}

注意事项

1、全局引用对象的内存泄露问题

参考文献

https://www.jianshu.com/p/d8be99605c65

https://blog.csdn.net/carson_ho/article/details/73250163

https://blog.csdn.net/q610098308/article/details/79395232

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JNIJava Native Interface)是 Java 虚拟机提供的一种机制,它允许 Java 程序调用 Native 方法,即使用 C/C++ 编写的函数。Android 中也提供了 JNI 接口,允许开发者使用 C/C++ 编写 Native 代码,与 Java 代码进行交互。 下面是一个简单的 Android JNI 教程: 1. 创建一个 Java 类,声明一个 native 方法 ``` package com.example.myapplication; public class JNIExample { static { System.loadLibrary("native-lib"); } public native String getStringFromNative(); } ``` 2. 在 C/C++ 文件中实现该方法 ``` #include <jni.h> #include <string> extern "C" JNIEXPORT jstring JNICALL Java_com_example_myapplication_JNIExample_getStringFromNative(JNIEnv* env, jobject /* this */) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); } ``` 3. 在 Android.mk 中添加编译规则 ``` LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := native-lib LOCAL_SRC_FILES := native-lib.cpp include $(BUILD_SHARED_LIBRARY) ``` 4. 编译生成 .so 库 ``` ndk-build ``` 5. 在 Activity 中调用该 native 方法 ``` public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); JNIExample jniExample = new JNIExample(); String stringFromNative = jniExample.getStringFromNative(); Log.d("MainActivity", "stringFromNative: " + stringFromNative); } } ``` 以上就是一个简单的 Android JNI 教程。需要注意的是,在使用 JNI 接口时,需要注意内存管理和类型转换等问题。另外,建议使用 Android Studio 中的 CMake 工具链来编译 Native 代码,而不是使用过时的 ndk-build 工具链。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值