2024-12-23
Android/Java
00

目录

JNI 简介
NDK简介
NDK两种开发模式
1. ndk-build 模式
2. CMake 模式
为何要用到NDK
一、 开发环境的搭建
二、代码的编写
1.首先是写java代码
2. 编写相应的C/C++代码
2.1 生成相应.h文件:
2.2 编写相应的.c文件:
3. 编译hello-jni.c 生成相应的库
3.1 编写Android.mk文件
3.2 生成.so共享库文件
4. 在eclipse重新编译HelloJni工程,生成apk
附:使用 android 脚本生成android 工程
Reference

JNI 简介

JNI(Java Native Interface)是“Java 本地接口”的缩写,用于指使用 C 和 C++ 语言开发的接口。由于 JNI 是 JVM 规范的一部分,所以我们编写的 JNI 程序可以在任何实现了 JNI 规范的 Java 虚拟机上运行。这一特性允许我们复用大量用 C/C++ 编写的代码。

然而,开发 JNI 程序会受到系统环境的限制,因为用 C/C++ 编写的代码在编译时依赖于操作系统提供的库函数,并需要链接到本地库上。因此,编译后的二进制代码只能在特定操作系统环境下运行。由于不同操作系统有各自的本地库和 CPU 指令集,各平台对标准 C/C++ 规范和库函数的实现方式也有所不同。这使得使用了 JNI 接口的 Java 程序不再具备完全的跨平台能力。如果要实现跨平台,需要在不同操作系统下编译出相应的动态库。

JNI 开发流程主要包括以下六步:

  1. 编写声明了 native 方法的 Java 类。
  2. 将 Java 源代码编译成 .class 字节码文件。
  3. 使用 javah -jni 命令生成 .h 头文件(javah 是 JDK 自带的命令,-jni 参数用于生成符合 JNI 规范的函数声明)。
  4. 用本地代码实现 .h 头文件中的函数。
  5. 将本地代码编译成动态库(Windows: .dll, Linux/Unix: .so, Mac OS X: .jnilib)。
  6. 将动态库拷贝到 java.library.path 本地库搜索目录下,并运行 Java 程序。

通过以上步骤,可以成功创建并运行 JNI 程序,实现 Java 与本地代码的交互。

image.png

NDK简介

(英语: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个字节的空间。

NDK两种开发模式

在 Android 开发中,NDK(Native Development Kit)提供了两种主要的开发模式:ndk-buildCMake。本文将详细介绍这两种模式的特点和使用场景。

1. ndk-build 模式

ndk-build 是 Android Studio 2.2 之前主流的 NDK 开发模式,主要特点包括:

  • 脚本化构建:使用 Android.mkApplication.mk 文件定义构建过程。
  • 成熟稳定:长时间使用,有较多的社区资源和支持。
  • 直接集成:无需额外配置,适合已有项目及偏好命令行工具的开发者。

2. CMake 模式

自 Android Studio 2.2 起,NDK 开发开始原生支持 CMake。主要特点包括:

  • 跨平台支持:CMake 是一种跨平台构建工具,能简化跨平台项目配置。
  • CLion 支持:与 JetBrains 的 CLion 编辑器无缝整合,提升 C/C++ 开发体验。
  • 现代构建系统:利用 CMake 的现代功能,如自动依赖管理和更简洁的配置语法。

Android Studio 在 2.2 版本整合了 CLion 的代码,使其全面支持通过 CMake 进行 NDK 开发,提升了项目配置和管理的灵活性和便捷性。

为何要用到NDK

概括来说主要分为以下几种情况:

  1. 代码的保护

    • APK的Java层代码很容易被反编译。
    • C/C++库的反汇编难度较大。
  2. 调用第三方C/C++库

    • 大部分开源库是用C/C++代码编写的。
  3. 便于移植

    • 用C/C++编写的库可以方便地在其他嵌入式平台上再次使用。

下面介绍一下Android NDK的入门学习过程:

入门的最好办法就是学习Android自带的例子。这里通过学习Android NDK自带的demo程序:hello-jni 来达到这个目的。

一、 开发环境的搭建

我用的windwos的AS软件,安装即可。

二、代码的编写

1.首先是写java代码

建立一个Android应用工程HelloJni创建HelloJni.java文件:

image.png

image.png

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"); } }

这段代码逻辑简单,注释也十分清晰,这里只需指出两点:

java
static { System.loadLibrary("hello-jni"); }

这段代码会在程序启动时加载 hello-jni 库。由于它在 static 块中,代码会在任何类的 onCreate 方法执行之前运行。如果程序中存在多个类,且 HelloJni 不是应用程序的入口类,那么库 libhello-jni.so 会在首次使用 HelloJni 类时加载。

java
public native String stringFromJNI(); public native String unimplementedStringFromJNI();

这两段代码中的 native 关键字表明这些方法是本地方法,即它们由本地代码(C/C++)实现,Java代码中仅仅是声明。

在使用 Eclipse 编译工程时,生成相应的 .class 文件。这一步必须在下一步生成 .h 文件之前完成,因为生成 .h 文件需要对应的 .class 文件。

2. 编写相应的C/C++代码

刚开始学的时候,有个问题会让人很困惑,相应的C/C++代码如何编写,函数名如何定义? 这里讲一个方法,利用javah这个工具生成相应的.h文件,然后根据这个.h文件编写相应的C/C++代码。

2.1 生成相应.h文件:

就拿我这的环境来说,首先在终端下进入刚刚建立的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对象。

2.2 编写相应的.c文件:

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文件给删了,当然留着也行,只是我还是习惯把不需要的文件给清理干净了。

3. 编译hello-jni.c 生成相应的库

3.1 编写Android.mk文件

在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 直接执行它。

3.2 生成.so共享库文件

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

4. 在eclipse重新编译HelloJni工程,生成apk

eclipse中刷新下HelloJni工程,重新编译生成apk,libhello-jni.so共享库会一起打包在apk文件内。

在模拟器中看看运行结果:

hello-jni

附:使用 android 脚本生成android 工程

在使用 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。

Reference

http://www.cnblogs.com/hibraincol/archive/2011/05/30/2063847.html

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:Dong

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC。本作品采用《知识共享署名-非商业性使用 4.0 国际许可协议》进行许可。您可以在非商业用途下自由转载和修改,但必须注明出处并提供原作者链接。 许可协议。转载请注明出处!