JNI(Java Native Interface)是“Java 本地接口”的缩写,用于指使用 C 和 C++ 语言开发的接口。由于 JNI 是 JVM 规范的一部分,所以我们编写的 JNI 程序可以在任何实现了 JNI 规范的 Java 虚拟机上运行。这一特性允许我们复用大量用 C/C++ 编写的代码。
然而,开发 JNI 程序会受到系统环境的限制,因为用 C/C++ 编写的代码在编译时依赖于操作系统提供的库函数,并需要链接到本地库上。因此,编译后的二进制代码只能在特定操作系统环境下运行。由于不同操作系统有各自的本地库和 CPU 指令集,各平台对标准 C/C++ 规范和库函数的实现方式也有所不同。这使得使用了 JNI 接口的 Java 程序不再具备完全的跨平台能力。如果要实现跨平台,需要在不同操作系统下编译出相应的动态库。
JNI 开发流程主要包括以下六步:
native
方法的 Java 类。.class
字节码文件。javah -jni
命令生成 .h
头文件(javah
是 JDK 自带的命令,-jni
参数用于生成符合 JNI 规范的函数声明)。.h
头文件中的函数。.dll
, Linux/Unix: .so
, Mac OS X: .jnilib
)。java.library.path
本地库搜索目录下,并运行 Java 程序。通过以上步骤,可以成功创建并运行 JNI 程序,实现 Java 与本地代码的交互。
(英语:Native Development Kit,简称NDK)是一种基于原生程序接口的软件开发工具。通过此工具开发的程序直接以本地语言运行,而非依赖虚拟机。因此,只有像Java这样基于虚拟机运行的语言才需要使用原生开发工具包。
NDK提供了一系列工具,帮助开发者快速开发C或C++的动态库,并能自动将.so文件和Java应用一起打包成APK。这些工具极大地简化了开发过程。NDK集成了交叉编译器,并提供了相应的.mk文件,用于处理CPU、平台和ABI等差异。开发人员只需简单修改.mk文件(例如指定需要编译的文件和编译特性要求),就可以创建出.so文件。
NDK可以自动将.so文件和Java应用一起打包,显著减轻了开发人员的工作量。
在Android NDK的platforms\<android版本>\arch-arm\usr\include\jni.h
头文件中,声明了所有可用的JNI接口函数。该文件包含两个重要的结构体:JNINativeInterface和JNIInvokeInterface。JNINativeInterface是JNI本地接口,实际上是一个接口函数指针表,包含所有JNI接口函数的指针,所有原生代码都可以调用这些接口函数。而JNIInvokeInterface是JNI调用接口,包含3个保留项和5个函数指针,这些函数用于访问全局的JNI接口,常用于原生多线程程序开发。
c++#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
如果使用C++代码调用JNI接口函数,JNIEnv被定义为 _JNIEnv
结构体,该结构体的第一个字段是一个JNINativeInterface结构体的指针。如果是C代码调用JNI接口函数,JNIEnv直接定义为JNINativeInterface结构体的指针。因此,可以将JNIEnv的首地址解释为JNINativeInterface的首地址使用,通过首地址和索引值即可找到需要调用的JNI接口函数。每个函数地址占用4个字节的空间。
在 Android 开发中,NDK(Native Development Kit)提供了两种主要的开发模式:ndk-build
和 CMake
。本文将详细介绍这两种模式的特点和使用场景。
ndk-build
是 Android Studio 2.2 之前主流的 NDK 开发模式,主要特点包括:
Android.mk
和 Application.mk
文件定义构建过程。自 Android Studio 2.2 起,NDK 开发开始原生支持 CMake
。主要特点包括:
Android Studio 在 2.2 版本整合了 CLion 的代码,使其全面支持通过 CMake 进行 NDK 开发,提升了项目配置和管理的灵活性和便捷性。
概括来说主要分为以下几种情况:
代码的保护:
调用第三方C/C++库:
便于移植:
下面介绍一下Android NDK的入门学习过程:
入门的最好办法就是学习Android自带的例子。这里通过学习Android NDK自带的demo程序:hello-jni
来达到这个目的。
我用的windwos的AS软件,安装即可。
建立一个Android应用工程HelloJni
创建HelloJni.java文件:
HelloJni.java :
java/*
* 版权所有 (C) 2009 The Android Open Source Project
*
* 根据 Apache License 2.0 版(“许可证”)授权;
* 除非遵守此许可证,否则您不得使用此文件。
* 您可以在以下网址获得许可证副本:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 除非适用法律要求或书面同意,按“原样”分发的许可软件
* 不得附带任何明示或暗示的保证或条件。
* 有关许可证下特定语言管理权限的详细信息,请参阅许可证。
*/
package com.example.hellojni;
import android.app.Activity;
import android.widget.TextView;
import android.os.Bundle;
public class HelloJni extends Activity {
/**
* 活动首次创建时调用。
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/* 创建一个TextView并设置其内容。
* 文本是通过调用本地函数检索的。
*/
TextView tv = new TextView(this);
tv.setText(stringFromJNI());
setContentView(tv);
}
/* 本地方法,由 'hello-jni' 本地库实现,
* 此库与此应用一起打包。
*/
public native String stringFromJNI();
/* 这是另一个本地方法声明,*未*由 'hello-jni' 实现。
* 这只是为了显示您可以在Java代码中声明任意多的本地方法,
* 它们的实现仅在首次调用它们时在当前加载的本地库中搜索。
*
* 尝试调用此函数将导致 java.lang.UnsatisfiedLinkError 异常!
*/
public native String unimplementedStringFromJNI();
/* 在应用启动时加载 'hello-jni' 库。
* 库已在安装时由包管理器解压到
* /data/data/com.example.HelloJni/lib/libhello-jni.so。
*/
static {
System.loadLibrary("hello-jni");
}
}
这段代码逻辑简单,注释也十分清晰,这里只需指出两点:
javastatic {
System.loadLibrary("hello-jni");
}
这段代码会在程序启动时加载 hello-jni
库。由于它在 static
块中,代码会在任何类的 onCreate
方法执行之前运行。如果程序中存在多个类,且 HelloJni
不是应用程序的入口类,那么库 libhello-jni.so
会在首次使用 HelloJni
类时加载。
javapublic native String stringFromJNI();
public native String unimplementedStringFromJNI();
这两段代码中的 native
关键字表明这些方法是本地方法,即它们由本地代码(C/C++)实现,Java代码中仅仅是声明。
在使用 Eclipse 编译工程时,生成相应的 .class
文件。这一步必须在下一步生成 .h
文件之前完成,因为生成 .h
文件需要对应的 .class
文件。
刚开始学的时候,有个问题会让人很困惑,相应的C/C++代码如何编写,函数名如何定义? 这里讲一个方法,利用javah这个工具生成相应的.h文件,然后根据这个.h文件编写相应的C/C++代码。
就拿我这的环境来说,首先在终端下进入刚刚建立的HelloJni工程的目录:
braincol@ubuntu:~$ cd workspace/android/NDK/hello-jni/
ls查看工程文件
braincol@ubuntu:~/workspace/android/NDK/hello-jni$ ls AndroidManifest.xml assets bin default.properties gen res src
可以看到目前仅仅有几个标准的android应用程序的文件(夹)。
首先我们在工程目录下建立一个jni文件夹:
braincol@ubuntu:~/workspace/android/NDK/hello-jni$ mkdir jni braincol@ubuntu:~/workspace/android/NDK/hello-jni$ ls AndroidManifest.xml assets bin default.properties gen jni res src
下面就可以生成相应的.h文件了:
braincol@ubuntu:~/workspace/android/NDK/hello-jni$ javah -classpath bin -d jni com.example.hellojni.HelloJni
-classpath bin:表示类的路径
-d jni: 表示生成的头文件存放的目录
com.example.hellojni.HelloJni
则是完整类名
这一步的成功要建立在已经在 bin/com/example/hellojni/ 目录下生成了 HelloJni.class的基础之上。
现在可以看到jni目录下多了个.h文件:
braincol@ubuntu:~/workspace/android/NDK/hello-jni$ cd jni/ braincol@ubuntu:~/workspace/android/NDK/hello-jni/jni$ ls com_example_hellojni_HelloJni.h
我们来看看com_example_hellojni_HelloJni.h
的内容:
com_example_hellojni_HelloJni.h :
c /* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_hellojni_HelloJni */
#ifndef _Included_com_example_hellojni_HelloJni
#define _Included_com_example_hellojni_HelloJni
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_hellojni_HelloJni
* Method: stringFromJNI
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_hellojni_HelloJni_stringFromJNI
(JNIEnv *, jobject);
/*
* Class: com_example_hellojni_HelloJni
* Method: unimplementedStringFromJNI
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_hellojni_HelloJni_unimplementedStringFromJNI
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
上面代码中的JNIEXPORT 和 JNICALL 是jni的宏,在android的jni中不需要,当然写上去也不会有错。
从上面的源码中可以看出这个函数名那是相当的长,不过还是很有规律的, 完全按照:java_pacakege_class_mathod
形式来命名。
也就是说:
Hello.java中 stringFromJNI() 方法对应于 C/C++中的 Java_com_example_hellojni_HelloJni_stringFromJNI() 方法 HelloJni.java中的 unimplementedStringFromJNI() 方法对应于 C/C++中的 Java_com_example_hellojni_HelloJni_unimplementedStringFromJNI() 方法
注意下其中的注释:
Signature: ()Ljava/lang/String; ()Ljava/lang/String;
()表示函数的参数为空(这里为空是指除了JNIEnv *, jobject
这两个参数之外没有其他参数,JNIEnv*, jobject
是所有jni函数必有的两个参数,分别表示jni 环境和对应的java类(或对象)本身),
Ljava/lang/String;
表示函数的返回值是java的String对象。
hello-jni.c :
c /*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
#include <string.h>
#include <jni.h>
/* This is a trivial JNI example where we use a native method
* to return a new VM String. See the corresponding Java source
* file located at:
*
* apps/samples/hello-jni/project/src/com/example/HelloJni/HelloJni.java
*/
jstring
Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
jobject thiz )
{
return (*env)->NewStringUTF(env, "Hello from JNI !");
}
这里只是实现了Java_com_example_hellojni_HelloJni_stringFromJNI
方法,而 Java_com_example_hellojni_HelloJni_unimplementedStringFromJNI
方法并没有实现,因为在HelloJni.java中只调用了stringFromJNI()
方法,所以unimplementedStringFromJNI()
方法没有实现也没关系,不过建议最好还是把所有java中定义的本地方法都实现了,写个空函数也行,有总比没有好。
Java_com_example_hellojni_HelloJni_stringFromJNI()
函数只是简单的返回了一个内容为 "Hello from JNI !"
的jstring对象(对应于java中的String对象)。
hello-jni.c文件已经编写好了,现在可以把com_example_hellojni_HelloJni.h
文件给删了,当然留着也行,只是我还是习惯把不需要的文件给清理干净了。
在jni目录下(即hello-jni.c 同级目录下)新建一个Android.mk文件,Android.mk 文件是Android 的 makefile文件,内容如下:
makefile# Copyright (C) 2009 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello-jni
LOCAL_SRC_FILES := hello-jni.c
include $(BUILD_SHARED_LIBRARY)
这个Androd.mk文件很短,下面我们来逐行解释下:
LOCAL_PATH := $(call my-dir)
一个Android.mk 文件首先必须定义好LOCAL_PATH变量。它用于在开发树中查找源文件。在这个例子中,宏函数’my-dir’, 由编译系统提供,用于返回当前路径(即包含Android.mk file文件的目录)。
include $( CLEAR_VARS)
CLEAR_VARS由编译系统提供,指定让GNU MAKEFILE为你清除许多LOCAL_XXX变量(例如 LOCAL_MODULE, LOCAL_SRC_FILES, LOCAL_STATIC_LIBRARIES, 等等...), 除了LOCAL_PATH 。这是必要的,因为所有的编译控制文件都在同一个GNU MAKE执行环境中,所有的变量都是全局的。
LOCAL_MODULE := hello-jni
编译的目标对象,LOCAL_MODULE变量必须定义,以标识你在Android.mk文件中描述的每个模块。名称必须是唯一的,而且不包含任何空格。
注意:编译系统会自动产生合适的前缀和后缀,换句话说,一个被命名为'hello-jni'的共享库模块,将会生成'libhello-jni.so'文件。
重要注意事项:
如果你把库命名为‘libhello-jni’,编译系统将不会添加任何的lib前缀,也会生成 'libhello-jni.so',这是为了支持来源于Android平台的源代码的Android.mk文件,如果你确实需要这么做的话。
LOCAL_SRC_FILES := hello-jni.c
LOCAL_SRC_FILES变量必须包含将要编译打包进模块中的C或C++源代码文件。注意,你不用在这里列出头文件和包含文件,因为编译系统将会自动为你找出依赖型的文件;仅仅列出直接传递给编译器的源代码文件就好。
注意,默认的C++源码文件的扩展名是’.cpp’. 指定一个不同的扩展名也是可能的,只要定义LOCAL_DEFAULT_CPP_EXTENSION变量,不要忘记开始的小圆点(也就是’.cxx’,而不是’cxx’)
include $(BUILD_SHARED_LIBRARY)
BUILD_SHARED_LIBRARY表示编译生成共享库,是编译系统提供的变量,指向一个GNU Makefile脚本,负责收集自从上次调用include $(CLEAR_VARS)
以来,定义在LOCAL_XXX变量中的所有信息,并且决定编译什么,如何正确地去做。还有 BUILD_STATIC_LIBRARY变量表示生成静态库:lib$(LOCAL_MODULE).a;BUILD_EXECUTABLE 表示生成可执行文件,可以直接把可执行文件 adb push 到某目录并chmod 给予执行权限,然后 adb shell 直接执行它。
Andro文件已经编写好了,现在可以用android NDK开发包中的 ndk-build脚本生成对应的.so共享库了,方法如下:
braincol@ubuntu:~/workspace/android/NDK/hello-jni/jni$ cd .. braincol@ubuntu:~/workspace/android/NDK/hello-jni$ ls AndroidManifest.xml assets bin default.properties gen jni libs obj res src braincol@ubuntu:~/workspace/android/NDK/hello-jni$ ndk-build Gdbserver : [arm-linux-androideabi-4.4.3] libs/armeabi/gdbserver Gdbsetup : libs/armeabi/gdb.setup Install : libhello-jni.so => libs/armeabi/libhello-jni.so
可以看到已经正确的生成了libhello-jni.so共享库了, 我们去 libs/armeabi/ 目录下看看:
braincol@ubuntu:~/workspace/android/NDK/hello-jni$ cd libs/ braincol@ubuntu:~/workspace/android/NDK/hello-jni/libs$ ls armeabi braincol@ubuntu:~/workspace/android/NDK/hello-jni/libs$ cd armeabi/ braincol@ubuntu:~/workspace/android/NDK/hello-jni/libs/armeabi$ ls gdbserver gdb.setup libhello-jni.so
eclipse中刷新下HelloJni工程,重新编译生成apk,libhello-jni.so共享库会一起打包在apk文件内。
在模拟器中看看运行结果:
hello-jni
在使用 ndk-build 工具前,需要先有一个Android 工程,这个工程可以从Android NDK 的 samples 目录下随便复制一份,也可以使用Android SDK 开发包 tools 目录下的 android 脚本生成。
android 脚本可以用来管理 AVD、android 工程,完整命令可以 “android --help” 查看。
android create project -n hellojni -p hellojni -t android-19 -k com.example.hellojni -a HelloJni
命令行解释如下:
"-n" 指定android 工程的名称;"-t" 指定生成android 工程的平台版本,也就是android list
列出的版本之一;"-p" 指定生成工程的目录名;"-k" 指定 android 工程的包名;"-a" 指定默认activity的名称;android create project
会根据默认activity 文件名自动生成相应的java 文件,并生成AndroidManifest.xml。
http://www.cnblogs.com/hibraincol/archive/2011/05/30/2063847.html
本文作者:Dong
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC。本作品采用《知识共享署名-非商业性使用 4.0 国际许可协议》进行许可。您可以在非商业用途下自由转载和修改,但必须注明出处并提供原作者链接。 许可协议。转载请注明出处!