上节讲解了NDK 开发环境搭建的方法,这节讲解下NDK编程的相关知识。
如何将.so文件打包到.APK
1、在你的项目根目录下建立libs/armeabi目录;
2、将libxxx.so文件copy到 libs/armeabi/下;
3、此时ADT插件自动编译输出的.apk文件中已经包括.so文件了;
4、安装APK文件,即可直接使用JNI中的方法;
我想还需要简单说明一下libxxx.so的命名规则,沿袭Linux传统,lib<something>.so是类库文件名称的格式,但在Java的System.loadLibrary(" something ")方法中指定库名称时,不能包括 前缀—— lib,以及后缀——.so。
当然,如果直接在模拟器上开发的话,将生成的.so 文件直接 adb remount, adb push *.so /system/lib 即可.但是这个需要ROOT权限,感觉不实用,也不方便。
编写自己的NDK 应用
1、首先创建含有native方法的Java类:
public class MainActivity extends Activity {
static {
System.loadLibrary("myjni");
}
public native String stringFromJni();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView textView = new TextView(this);
String tranString = stringFromJni();
textView.setText(tranString);
setContentView(textView);
}
}
2、在C文件中直接实现Native 方法
当然,也可以采用javah 生成头文件,然后再根据相关函数名写C源代码文件,但是JNI的函数命名规则我们已经清楚了,直接自己写就OK!
具体做法:
在java 工程下建立 jni 文件夹, 下面加建立 test.c 和 Android.mk 文件即可。
test.c:
#include <string.h>
#include <jni.h>
jstring Java_com_sj_ndktest_MainActivity_stringFromJni(JNIEnv * env,
jobject this) {
return (*env)->NewStringUTF(env, "Hello from My-JNI !");
}
Android.mk:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := myjni
LOCAL_SRC_FILES := myjni.c
include $(BUILD_SHARED_LIBRARY)
编译——两种不同的编译环境
2、完整源码编译环境 :Android平台提供有基于make的编译系统,为App编写正确的Android.mk文件就可使用该编译系统。该环境需要通过git从官方网站获取完整源码副本并成功编译,更多细节请参考:http://source.android.com/index.html
不管你选择以上两种方法的哪一个,都必须编写自己的Android.mk文件,有关该文件的编写请参考相关文档。
这里我们采用 NDK 环境编译,Cygwin 下 $NDK/ndk-build 即可。完成后会发现 文件夹下多了 libs 和 obj 文件夹,这样就对了。Eclipse 下直接Build RUN工程,构建APK,其会将so 文件自动打包,安装,完成。
NDK命令介绍:
ndk-build
ndk-build clean --> 清空所编译出的二进制文件们。
ndk-build -B V=1 --> 强制完全重新编译,并显示命令
JNI组件的常见函数——JNI_OnLoad()、JNI_OnUnload()
JNI组件被成功加载和卸载时,会进行函数回调,当VM执行到System.loadLibrary(xxx)函数时,首先会去执行JNI组件中的JNI_OnLoad()函数,而当VM释放该组件时会呼叫JNI_OnUnload()函数。
JNI_OnLoad()有两个重要的作用:
(1)指定JNI版本:告诉VM该组件使用那一个JNI版本(若未提供JNI_OnLoad()函数,VM会默认该使用最老的JNI 1.1版),如果要使用新版本的JNI,例如JNI 1.4版,则必须由JNI_OnLoad()函数返回常量JNI_VERSION_1_4(该常量定义在jni.h中) 来告知VM。
(2)初始化设定,当VM执行到System.loadLibrary()函数时,会立即先调用JNI_OnLoad()方法,因此在该方法中进行各种资源的初始化操作最为恰当。
JNI_OnUnload()作用:
JNI_OnUnload()的作用与JNI_OnLoad()对应,当VM释放JNI组件时会调用它,因此在该方法中进行善后清理,资源释放的动作最为合适。
本地方法注册——RegisterNatives方法
本地方法注册的作用:本地方法名不必固定按照 Java_包名_类名_方法名 的格式,我们完全可以自定义方法名,然后注册到虚拟机就可以。
当Java类别透过VM呼叫到本地函数时,通常是依靠VM去动态寻找.so中的本地函数(因此它们才需要特定规则的命名格式),如果某方法需要连续呼叫很多次,则每次都要寻找一遍,所以使用RegisterNatives将本地函数向VM进行登记,可以让其更有效率的找到函数。RegisterNatives方法的另一个重要用途是,运行时动态调整本地函数与Java函数值之间的映射关系,只需要多次调用RegisterNatives()方法,并传入不同的映射表参数即可。
本地方法注册实质是要建立本地方法和JAVA方法之间的映射关系,而建立c/c++方法和Java方法之间映射关系的关键是 JNINativeMethod 结构,该结构定义在jni.h中,具体定义如下:
/*
* used in RegisterNatives to describe native method name, signature,
* and function pointer.
*/
typedef struct {
char *name;
char *signature;
void *fnPtr;
} JNINativeMethod;
定义映射关系数组:
static JNINativeMethod methods[] = { { "produceString",
"(Ljava/lang/String;)Ljava/lang/String;", (void*) C_produceString } };
其中第一项即 name 项为java 方法名(不需要包含包名,直接写方法名即可);第三项为本地方法名;第二项看着比较复杂,其代表的是函数签名信息,括号内部是参数类型,后面是返回值类型,具体规则如下:
1)基本类型对应关系:
标识符
2)基本类型数组:(则以 [ 开始,用两个字符表示)
标识串
3)类(class):(则以 L 开头,以 ; 结尾,中间是用 / 隔开的 包 及 类名)
标识串 Java 类型 Jni 类型
L包1/包n/类名;
例子:
Ljava/net/Socket; Socket
4)例外(String 类):
标识串
Ljava/lang/String;
5)嵌套类(类位于另一个类之中,则用$作为类名间的分隔符)
标识串
L包1/包n/类名$嵌套类名;
例子:
Landroid/os/FileUtils$FileStatus;
具体我觉得没比较死记硬背,碰到了去查就可以,看多了就记住了。也可以用 java提供的一个javap的工具帮助生成函数或变量的签名信息,用法如下:
javap -s -p xxx
xxx 为class文件,有了它,就不用记上面的类型表示了。
生成文件如下 eg:
public com.sj.ndktest.MainActivity();
Signature: ()V
public native java.lang.String stringFromJni();
Signature: ()Ljava/lang/String;
public native java.lang.String produceString(java.lang.String);
Signature: (Ljava/lang/String;)Ljava/lang/String;
public void onCreate(android.os.Bundle);
Signature: (Landroid/os/Bundle;)V
RegisterNatives具体用法代码示例:
//定义目标类名称
static const char *className = "com/sj/ndktest/MainActivity";
//定义方法隐射关系
static JNINativeMethod methods[] = { { "produceString",
"(Ljava/lang/String;)Ljava/lang/String;", (void*) C_produceString } };
//onLoad方法,在System.loadLibrary()执行时被调用
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
LOGI("JNI_OnLoad startup~~!");
//声明变量
jint result = JNI_ERR;
JNIEnv* env = NULL;
jclass clazz;
int methodsLenght;
//获取JNI环境对象
if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
LOGE("ERROR: GetEnv failed\n");
return JNI_ERR;
}
assert(env != NULL);
//注册本地方法.Load 目标类
clazz = (*env)->FindClass(env, className);
if (clazz == NULL) {
LOGE("Native registration unable to find class '%s'", className);
return JNI_ERR;
}
//建立方法隐射关系
//取得方法长度
methodsLenght = sizeof(methods) / sizeof(methods[0]);
if ((*env)->RegisterNatives(env, clazz, methods, methodsLenght) < 0) {
LOGE("RegisterNatives failed for '%s'", className);
return JNI_ERR;
}
result = JNI_VERSION_1_4;
return result;
}
NDK 日志和调试
上面的代码中看到LOGE 符号,这个是NDK 的日志输出,类似于 android 中的 log.e(...) ,其实质也是调用了 android 的日志输出。
NDK 使用日志输出必须引入头文件 #include <android/log.h>,如果是在完整源码环境下编译,引入 include <utils/Log.h> 即可。
具体使用方法:
#ifndef __JNILOGGER_H_
#define __JNILOGGER_H_
#include <android/log.h>
#ifdef _cplusplus
extern "C" {
#endif
#ifndef LOG_TAG
#define LOG_TAG "MY_LOG_TAG"
#endif
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,LOG_TAG,__VA_ARGS__)
#ifdef __cplusplus
}
#endif
#endif
/* __JNILOGGER_H_ */
调试的话,方法有两种:一种就是上面的LOG日志输出,另一种利用 ndk-gdb 工具调试,使用方法与 gdb 工具一致。
Android MK 文件写法
示例文件:
# 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 := myjni
LOCAL_SRC_FILES := myjni.c
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
LOCAL_LDLIBS 指的是需要链接的库文件
#宏函数'my-dir',编译系统提供,用于返回当前路径(即包含Android.mk文件的路径) LOCAL_PATH -编译时的目录
$(call目录,目录….)目录引入操作符
如该目录下有个文件夹名称src,则可以这样写$(call src),那么就会得到src目录的完整路径
include $(CLEAR_VARS) -清除之前的一些系统变量
# 当前模块的名称/编译的目标对象。编译系统会自动产生合适的前缀和后缀
LOCAL_MODULE -编译生成的目标对象
# 包含将要编译打包进模块中的C或者C++源代码文件(无需列出头文件和包含文件)
LOCAL_SRC_FILES-编译的源文件
LOCAL_C_INCLUDES-需要包含的头文件目录
LOCAL_SHARED_LIBRARIES-链接时需要的外部库
LOCAL_PRELINK_MODULE-是否需要prelink处理
# BUILD_SHARED_LIBRARY表示编译生成共享库,是编译系统提供的变量,指向一个GNU Makefile脚本,
# 负责收集自从上次调用'include ($CLEAR_VARS)'以来,定义在LOCA_*变量中的所有信息,并且决定编译什么,如果正确去编译。
# 另: BUILD_STATIC_LIBRARY表示生成静态库: lib$(LOCAL_MODULE).a; BUILD_EXECUTABLE表示生成可执行文件。
include $(BUILD_SHARED_LIBRARY)-指明要编译成动态库
android.mk编译模块添加具体方法参考:http://blog.csdn.net/yili_xie/article/details/4906865
以上就是我的总结,附上完整源代码:
MainActivity.java:
package com.sj.ndktest;
import android.app.Activity;
import android.os.Bundle;
import android.os.Process;
import android.util.Log;
import android.view.Menu;
import android.widget.TextView;
public class MainActivity extends Activity {
static {
System.loadLibrary("myjni");
}
public native String stringFromJni();
public native String produceString(String string);
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView textView = new TextView(this);
String tranString = stringFromJni();
String produString = produceString("hello ");
textView.setText(tranString + "\n" + produString);
setContentView(textView);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
@Override
public void onBackPressed() {
super.onBackPressed();
finish();
Process.killProcess(Process.myPid());
}
@Override
public void onDestroy() {
super.onDestroy();
}
}
myjni.c :
#include <string.h>
#include <jni.h>
#include <stdlib.h>
#include <assert.h>
#ifndef __JNILOGGER_H_
#define __JNILOGGER_H_
#include <android/log.h>
#ifdef _cplusplus
extern "C" {
#endif
#ifndef LOG_TAG
#define LOG_TAG "MY_LOG_TAG"
#endif
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,LOG_TAG,__VA_ARGS__)
#ifdef __cplusplus
}
#endif
#endif
/* __JNILOGGER_H_ */
jstring Java_com_sj_ndktest_MainActivity_stringFromJni(JNIEnv * env,
jobject this) {
LOGE("123");
return (*env)->NewStringUTF(env, "Hello from My-JNI !");
}
jstring C_produceString(JNIEnv * env,
jobject this, jstring str) {
//从jstring类型取得c语言环境下的char*类型
const char* name = (*env)->GetStringUTFChars(env, str, 0);
LOGE(name);
//output:hello
//本地常量字符串
char* hello = "YEAH";
//动态分配目标字符串空间
char* result = malloc((strlen(name) + strlen(hello) + 1) * sizeof(char));
memset(result, 0, sizeof(result));
//字符串链接
strcat(result, hello);
strcat(result, name);
//释放jni分配的内存
(*env)->ReleaseStringUTFChars(env, str, name);
//生成返回值对象
str = (*env)->NewStringUTF(env, " JNI~!");
//strcat(str, result);
//释放动态分配的内存
LOGE(result);
//output:YEAHhello
free(result);
return str;
}
//定义目标类名称
static const char *className = "com/sj/ndktest/MainActivity";
//定义方法隐射关系
static JNINativeMethod methods[] = { { "produceString",
"(Ljava/lang/String;)Ljava/lang/String;", (void*) C_produceString } };
//onLoad方法,在System.loadLibrary()执行时被调用
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
LOGI("JNI_OnLoad startup~~!");
//声明变量
jint result = JNI_ERR;
JNIEnv* env = NULL;
jclass clazz;
int methodsLenght;
//获取JNI环境对象
if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
LOGE("ERROR: GetEnv failed\n");
return JNI_ERR;
}
assert(env != NULL);
//注册本地方法.Load 目标类
clazz = (*env)->FindClass(env, className);
if (clazz == NULL) {
LOGE("Native registration unable to find class '%s'", className);
return JNI_ERR;
}
//建立方法隐射关系
//取得方法长度
methodsLenght = sizeof(methods) / sizeof(methods[0]);
if ((*env)->RegisterNatives(env, clazz, methods, methodsLenght) < 0) {
LOGE("RegisterNatives failed for '%s'", className);
return JNI_ERR;
}
result = JNI_VERSION_1_4;
return result;
}
//onUnLoad方法,在JNI组件被释放时调用
void JNI_OnUnload(JavaVM* vm, void* reserved) {
LOGE("call JNI_OnUnload ~~!!");
}
Android.mk :
# 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 := myjni
LOCAL_SRC_FILES := myjni.c
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
参考地址,在此表示感谢:
http://mobile.51cto.com/android-267538_2.htm
http://blog.sina.com.cn/s/blog_4c451e0e0101339i.html
http://www.cnblogs.com/bastard/archive/2012/05/19/2508913.html
http://jykenan.iteye.com/blog/1140965
http://blog.csdn.net/gongyangyang100/article/details/7418436