此文章写于2015.02.03,发布于网易博客,于2020.03.25迁移至此。
前言:经多多次尝试与查阅资料,得出结论:Android环境下的JNI调用不能像Java环境下一样加载Windows下的dll文件,需要加载的是Linux下的so文件。
package com.example.helloworld; import android.os.Bundle; import android.app.Activity; import android.view.Menu; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); System.out.println("------>>" + printJNI("I am HelloWorld Activity")); } static { //加载库文件 System.loadLibrary("HelloWorldJni"); } //声明原生函数 参数为String类型 返回类型为String private native String printJNI(String inputStr); @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } }
eclipse会自动为我们编译此Java文件,后面要用到。
然后,生成共享库的头文件:
进入到Android 项目目录中 :D:\workspace\HelloWorld\bin\classes\com\example\helloworld,可以看到里面有很多后缀为.class的文件,就是eclipse为我们自动编译好了的java文件,找到MainActivity.class文件。退回到classes级目录:D:\workspace\HelloWorld\bin\classes\
执行如下命令:javah com.example.helloworld.MainActivity,生成文件:com_example_helloworld_MainActivity.h
打开如下:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_example_helloworld_MainActivity */ #ifndef _Included_com_example_helloworld_MainActivity #define _Included_com_example_helloworld_MainActivity #ifdef __cplusplus extern "C" { #endif /* * Class: com_example_helloworld_MainActivity * Method: printJNI * Signature: (Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_example_helloworld_MainActivity_printJNI (JNIEnv *, jobject, jstring); #ifdef __cplusplus } #endif #endif
可以看到自动生成对应的函数:Java_com_example_helloworld_MainActivity_printJNI。
java虚拟机就可以在调用printJNI接口的时候自动找到这个C实现的Native函数。(函数名太长可以在.c文件中通过函数名映射表来实现简化)
下一步,实现JNI原生函数源文件:
新建com_example_helloworld_MainActivity.c文件:
#include <jni.h> #include<android/log.h> #define LOG "ffmpegDemo-jni" // 这个是自定义的LOG的标识 #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG,__VA_ARGS__) // 定义LOGD类型 #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG,__VA_ARGS__) // 定义LOGI类型 #define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG,__VA_ARGS__) // 定义LOGW类型 #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG,__VA_ARGS__) // 定义LOGE类型 #define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,LOG,__VA_ARGS__) // 定义LOGF类型 /* Native interface, it will be call in java code */ JNIEXPORT jstring JNICALL Java_com_example_helloworld_MainActivity_printJNI(JNIEnv *env, jobject obj,jstring inputStr) { LOGD("LXK Hello World From libhelloworld.so!"); // 从instring字符串取得指向字符串UTF编码的指针 const char *str = (const char *)(*env)->GetStringUTFChars( env,inputStr, JNI_FALSE ); LOGD("LXK--->%s",(const char *)str); // 通知虚拟机本地代码不再需要通过str访问Java字符串。 (*env)->ReleaseStringUTFChars(env, inputStr, (const char *)str ); return (*env)->NewStringUTF(env, "Hello World! I am Native interface"); } /* This function will be call when the library first be load. * You can do some init in the libray. return which version jni it support. */ jint JNI_OnLoad(JavaVM* vm, void* reserved) { void *venv; LOGD("LXK----->JNI_OnLoad!"); if ((*vm)->GetEnv(vm, (void**)&venv, JNI_VERSION_1_4) != JNI_OK) { LOGE("LXK--->ERROR: GetEnv failed"); return -1; } return JNI_VERSION_1_4; }
JNI_OnLoad函数是JNI规范定义的,当共享库第一次被加载的时候会被回调,这个函数里面可以进行一些初始化工作,比如注册函数映射表,缓存一些变量等,最后返回当前环境所支持的JNI环境。本例只是简单的返回当前JNI环境。
下一步,编译生成so库(假设你已经搭建好了Android NDK开发环境):
在当前项目目录下建立jni文件夹:HelloWorld/jni/,并将com_example_helloworld_MainActivity.c和 com_example_helloworld_MainActivity.h 文件都拷贝进去,然后在此目录建立Android.mk 文件,编写用于编译so库的Android.mk文件:
LOCAL_PATH:= $(call my-dir)
# 一个完整模块编译
include $(CLEAR_VARS)
LOCAL_SRC_FILES:=com_example_helloworld_MainActivity.c
LOCAL_C_INCLUDES := $(JNI_H_INCLUDE)
LOCAL_MODULE := libHelloWorldJni
LOCAL_SHARED_LIBRARIES := libutils
LOCAL_LDLIBS := -llog
LOCAL_PRELINK_MODULE := false
LOCAL_MODULE_TAGS :=optional
include $(BUILD_SHARED_LIBRARY)
系统变量解析:
LOCAL_PATH - 编译时的目录
$(call 目录,目录….) 目录引入操作符,如该目录下有个文件夹名称 src,则可以这样写 $(call src),那么就会得到 src 目录的完整路径
include $(CLEAR_VARS) -清除之前的一些系统变量
LOCAL_MODULE - 编译生成的目标对象
LOCAL_SRC_FILES - 编译的源文件
LOCAL_C_INCLUDES - 需要包含的头文件目录
LOCAL_SHARED_LIBRARIES - 链接时需要的外部库
LOCAL_PRELINK_MODULE - 是否需要prelink处理
include$(BUILD_SHARED_LIBRARY) - 指明要编译成动态库
打开cygwin命令行,进入到刚才建立的jni目录里,输入命令:$NDK/ndk-build 即可,此时在你项目的HelloWorld/libs/armeabi目录下即可生成需要的.so文件。
将编译好的apk安装到手机上
使用adb push到手机上的需要自己去导入库文件libHelloWorldJni.so到data/data/com.example.helloworld/lib/目录下
使用adb install方式安装到手机上的则会自动导入。
另外需要注意的一点是,在.c文件中用printf进行的打印是无法在logcat中看到的,因为NDK根本就不支持。要想在.c文件中加打印进行调试,可以调用NDK 下的log.h 来打印Log 日志,具体方法如下:
1. 在你的Android.mk文件中加入:LOCAL_LDLIBS := -llog
2. 导入log头文件:在你使用的 .c/ .cpp 文件中,导入 log.h 头文件: #include<android/log.h>
3. 定义LOG 函数:先定义一个全局变量,再定义一些输出的LOG函数:
- #define LOG "HelloWorldDemo-jni" // 这个是自定义的LOG的标识
- #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG,__VA_ARGS__) // 定义LOGD类型
- #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG,__VA_ARGS__) // 定义LOGI类型
- #define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG,__VA_ARGS__) // 定义LOGW类型
- #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG,__VA_ARGS__) // 定义LOGE类型
- #define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,LOG,__VA_ARGS__) // 定义LOGF类型
上述代码中定义的函数,分别对应于Android 的Java代码中的 Log.d(), Log.i(), Log.w(),Log.e(), Log.f()等方法;其中:
ANDROID_LOG_DEBUG:是打印日志的级别;
LOG:是要过滤的标签,可以在LogCat视图中进行过滤。
__VA_ARGS__:是实际的打印内容。
4. 使用上述方法:在.c 代码中直接按照以下方式书写:
LOGD("这是Debug的信息"); LOGE("程序错误!!!");
除此之外,更可以给在打印时带上一些变量:
int width=10; int height=20; LOGI("长和宽分别为 %d ,%d“,width,height);
这种方式非常灵活,类似于 C语言中的 printf()函数。
注:在你编译.so文件的过程中,如果你的.c或.c++文件出现类似下面的错误,则原因是由于你从网页上复制下来粘贴而引起的空格与linux下的space键不同的问题,所以最好自己手动输入,这样可以避免格式上的错误。