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 :指定加载类的路径(C/C++头文件是通过.class文件生成的);

       -d

:头文件输出目录,即选中工程下的jni目录;

       -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.cLOCAL_SRC_FILES:= HelloJni.cppinclude$(BUILD_SHARED_LIBRARY)

3.C/C++层日志打印与最终效果

(1)日志打印


(2)效果演示


更多相关文章

  1. Android(安卓)防止手机休眠
  2. Android基础入门教程——7.1.3 Android(安卓)HTTP请求方式-HttpU
  3. Android(安卓)破解apk文件
  4. Android(安卓)Service学习之本地服务
  5. 给 Android(安卓)开发者的 Flutter 指南(上)
  6. Android(安卓)CheckBox
  7. Android(安卓)10适配注意的问题
  8. 一步步教你用Android(安卓)Google Map(一)
  9. Android系统名词解释汇总

随机推荐

  1. Android 系统服务 - AMS 的启动过程
  2. Android中设定EditText的输入长度
  3. android lint 是什么
  4. android开发技巧精髓八
  5. 使用 kotlin 来进行 Android(安卓)开发(一
  6. Android应用程序组件Content Provider在
  7. Android UI 教程汇总
  8. Visual Studio 跨平台开发实战(5) - Xama
  9. android 横向滚动屏幕实现(3)
  10. Android软键盘调用