Android NDK开发之旅(1):Eclipse中NDK环境搭建与JNI开发流程
(码字不易,转载请表明出处:http://blog.csdn.net/andrexpert/article/details/72626830)
前言
看着本篇文章的标题,或许你会问现在AndroidStudio版本都更新到3.0了,从2.2开始就可以直接使用Cmake来构建NDK项目,根本没有必要再去研究NDK在Eclipse中的开发。嗯,在我使用过AndroidStudio构建NDK项目后,我也是这种想法,通过cmake编译C/C++代码来构建NDK开发框架确实是非常智能、步骤也很简单,完全可以秒杀Eclipse。但是,对于从来没有开发NDK项目的人来说,直接上AS开发可能一是无法明白这其中的原理,比如Android项目是如何构建C/C++开发环境的、Java层和C/C++层是如何映射的等等,这就是我打算写这篇文件的原因。
1. NDK/JNI简介
AndroidFramework由基于Java语言的Java层与基于C/C++语言的C/C++层组成,在某些情况下,为了将Java(上层)与C/C++(底层)有机地联系起来,使得他们相互协调,共同完成某些任务,Android引入了Java本地接口(JNI,JavaNative interface),它允许Java代码与基于C/C++编写的应用程序、模块和库进行交互操作。在AndroidFramework中,借助JNI综合了Java语言与C/C++等本地语言的优点,使得开发者既可以利用Java语言跨平台、类库丰富、开发便捷等特点,又可以利用本地语言开发运行效率更高、更健壮、更安全的程序。使用JNI几种情况:
(1)注重处理速度(栈、堆速度区别)
与本地代码(C/C++等)相比,Java代码的执行速度要慢一些。如果对某段程序的执行速度有较高的要求,建议使用C/C++编写代码。而后在Java中通过JNI调用基于C/C++编写的部分,常常能够获得很快的运行速度。
(2)硬件控制
为了更好地控制硬件,硬件控制代码通常使用C语言编写。而后借助JNI将其与Java层连接起来,从而实现对硬件的控制。另外,假如搭载Android的设备上安装AndroidFramework不支持的硬件时,可以使用C语言实现设备的驱动程序,以便对设备进行控制。
(3)既有C/C++代码的复用
在程序编写过程中,常常会使用一些已经编写好的C/C++代码,既提高了编程效率,又确保了程序的安全性与健壮性,在复用这些C/C++代码时,就要通过JNI来实现。
(4)防止被反编译,提高app安全性
2. NDK开发环境配置
* NDK:android-ndk-r10c(32位)
* JDK:jdk1.8.0_20(32位)
* NDK插件:com.android.ide.eclipse.ndk_23.0.7.2120684.jar
com.android.ide.eclipse.ndk.feature_23.0.7.2120684(目录)
(1) 配置环境变量
将JDK和NDK相关路径配置到环境变量中,需要注意的是,JDK的版本要与NDK版本一致,否则会报错,比如系统是64位那么JDK和NDK应该都是64位的版本。path变量添加:;%NDK_HOME%;%JAVA_HOME%;%JAVA_HOME%\include;%JAVA_HOME%\bin;%JAVA_JRE%; ,其中,NDK_HOME 、JAVA_HOME分别为android-ndk-r10c、jdk1.8.0_20的根目录路径。然后,在cmd窗口中敲入”ndk-build”命令,如果出现以下信息,即可说明NDK环境配置成功。
(2) 配置NDK选项
“Window->Preferences->NDK选项”中配置AndroidNDK开发环境,有可能你的Elipse没有这个NDK选项,那是因为Eclipse没有安装NDK插件,我们可以手动来进行安装,即分别将com.android.ide.eclipse.ddms_23.0.7.2120684.jar、com.android.ide.eclipse.ndk.feature_23.0.7.2120684(目录)拷贝到Eclipse安装目录下的plugins和features目录,重启Eclipse。
(3) AddAndroid Native Support,创建JNI目录
在Eclipse中进行NDK开发,最核心的地方就是Android工程中的JNI目录,所有一切与C/C++有关的开发与配置均是在该目录下实现的。
(4) 配置javah.exe命令到Eclipse,方便生成头文件
javah.exe命令是JDK提供的工具,位于C:\ProgramFiles\Java\jdk1.8.0_20\bin目录下,主要用于生成JNI开发所需的且与Native方法对应的头文件。通常,我们主要是通过CMD命令窗口执行javah命令行得到所需的头文件,但CMD生成头文件操作有点麻烦。这里为了方便,只需将javah配置到Eclipse即可直接在JNI目录下生成目标头文件,其中javah命令行执行相关参数为:
-v-classpath "${project_loc}/bin/classes" -d"${project_loc}/jni" -jni ${java_type_name}
说明:
-v:启用详细输出;
-classpath
-d
-jni:指定需要编译的类名,即包含native方法的Java类的类名;
javah命令使用方法:
先选中包含native方法的java类,再执行javah命令。 (注意:需要refresh下JNI目录才能显示头文件)
(5) 为目标工程配置ndk-build命令,方便编译生成.so文件
HelloJni命令使用方法:
选中Android工程,执行HelloJni命令,会自动在libs目录下生成相应平台的.so文件。
2.JNI开发流程解析
(1) JNI工程目录结构
根据HelloJni工程结构图可知,JNI目录主要包含三个文件:HelloJni.cpp、Android.mkcom_example_hellojni_CalculateUtils.h。其中,HelloJni.cpp是C++函数实现;Android.mk是C/C++配置文件,主要用于配置动态库的名称、有哪些C/C++文件等信息;com_example_hellojni_CalculateUtils.h是使用javah命令自动生成且与Java层native方法相对应的头文件。在Eclipse中,JNI开发流程如下:
a) Java层编写native方法,即CalculateUtils.java;
b) 使用javah工具生成C语言头文件,即com_example_hellojni_CalculateUtils.h。需要注意的是每次修改或增加了native方法,都要重新生成头文件;
c) 拷贝头文件声明的函数到HelloJni.cpp,编写C/C++代码实现功能逻辑;如果使用的c开发(xxx.c),可能会报"Method can not resolved"异常,且使用(*env)->无法补全方法,这种情况是可以正常编译的,可以通过右键工程 property->C/C++ General->Code Analysis—>配置当前工程(或者workspace)->使 method cannot be resolved 不选中(即此项不进行报错 ),C++开发就不会出现上述情况;
d) 使用ndk工具生成共享库.so,即libHelloJni.so。需要注意的是,每次修改了C/C++层函数的相关代码,都要重新生成共享库;
e) 运行Android工程;
(2) Native本地方法创建
/** 本地方法
* Created by jiangdg on 2017/5/17.
*/
public classCalculateUtils {
// 加载动态库,其中JNITest为Lib名称
// 通过Android.mk中的LOCAL_MODULE字段值可知
static {
System.loadLibrary("JNITest");
}
// 使用native关键字声明该方法的具体实现是在本地层,使用C/C++开发
public static native int getAddResult(inta,int b);
public static native StringgetEncryptString(String str);
}
(3) C/C++本地方法实现与日志打印
#include
#include
#include
#include "com_example_hellojni_CalculateUtils.h"
#define LOG_TAG "laojiang"
// 不带格式log
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,"%s",__VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,"%s",__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,"%s",__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,"%s",__VA_ARGS__)
// 带格式log
#define LOG_I(format,...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,format,__VA_ARGS__)
#define LOG_D(format,...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,format,__VA_ARGS__)
#define LOG_W(format,...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,format,__VA_ARGS__)
#define LOG_E(format,...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,format, __VA_ARGS__)
JNIEXPORT jintJNICALL Java_com_example_hellojni_CalculateUtils_getAddResult
(JNIEnv *env, jclass jobj, jint numA, jintnumB){
int i,result = 0;
for (i = numA; i <= numB ; ++i) {
result += i;
}
LOGI("计算和...");
LOG_D("result = %d",result);
return result;
}
JNIEXPORTjstring JNICALL Java_com_example_hellojni_CalculateUtils_getEncryptString
(JNIEnv *env, jclass jobj, jstring str){
// jstring转const char*
const char* temp =env->GetStringUTFChars(str,JNI_FALSE);
LOGI("jstring转constchar*...");
LOG_D("temp =%s",temp);
// 拼接字符串
strcat((char *)temp,"jiangdongguo");
// 释放资源,防止内存溢出
jstring result = env->NewStringUTF(temp);
env->ReleaseStringUTFChars(str,temp);
return result;
}
JNIEXPORT jint JNICALL Java_com_example_hellojni_CalculateUtils_getAddResult(JNIEnv *env, jclass jobj, jint numA, jintnumB)
JNIEXPORT jint JNICALL Java_com_example_hellojni_CalculateUtils_getAddResult(JNIEnv *env, jclass jobj, jint numA, jintnumB)
解析:
上面两个函数即为Java层native方法在C/C++层的函数映射,当在Java层调用getAddResult(inta,int b)方法时,实际最终调用的是JNIEXPORT jint JNICALL Java_com_example_hellojni_CalculateUtils_getAddResult(JNIEnv *env, jclass jobj, jint numA, jintnumB)。其实,从该函数的结构来看,它与getAddResult(inta,int b)方法都有着千丝万缕的关系:JNIEXPORT 返回值 JNICALL Java_包名_类名_方法名(参数列表),其中,env参数表示的是JNI环境变量,我们可以通过它调用JNI相关的API实现Java与C/C++之间的转换开发,jobj参数表示Java中getAddResult方法属于的Java类对象,通过它我们可以在C/C++层轻松调用Java层该对象的其他属性和方法。
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,"%s",__VA_ARGS__)
#define LOG_I(format,...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,format,__VA_ARGS__)
解析:
#define是宏定义,表明使用LOGI(...)用来替换后面的一串;__android_log_print函数来源于android/log.h(源码中声明为:int __android_log_print(int prio, const char *tag, const char *fmt, ...)),C/C++层打印日志就靠它来实现,它的第一个参数是日志的等级,类似如java层中的info、debug、warn、error,第二个参数为日志打印tag,第三个参数表示输出格式,第四个参数表示若干个变量参数。
(4) Android.mk配置文件分析
LOCAL_PATH :=$(call my-dir)
include $(CLEAR_VARS)
# 指定共享库名称
LOCAL_MODULE := HelloJni
# 使C/C++支持android/log.h,日志打印
LOCAL_LDLIBS:= -L$(call host-path, $(LOCAL_PATH))-lm -llog -lc
# 添加实现的C/C++文件,比如我们又在jni目录下添加了一个test.c文件,那么
#LOCAL_SRC_FILES:= HelloJni.cpp test.c
LOCAL_SRC_FILES:= HelloJni.cpp
include$(BUILD_SHARED_LIBRARY)
3.C/C++层日志打印与最终效果
(1)日志打印
(2)效果演示