NDK-JNI开发笔记草稿
# 2016/04/13
# hello-jni项目(基于android studio 2.0)
(as2.2+ 可以用cmake 其实现,非常简单 不用.h文件 一开始的配置见 http://tools.android.com/tech-docs/external-c-builds 后面只要写个native方法 c文件中写对应的函数即可 )
- 配置NDK 如果没下载NDK的话
File->Settings Appearance & Behavior->System Settings ->Android SDK 右侧选择SDK Tools 勾选NDK更新 勾上更新重启项目。
local.properties
里面如果没自动配置的话就这样配置(按上述步骤安装ndk 其目录就在sdk下)
## This file is automatically generated by Android Studio.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Wed Apr 13 00:40:11 CST 2016
ndk.dir=E\:\\BaiduYunDownload\\adt-bundle-windows-x86_64-20140702\\sdk\\ndk-bundle
sdk.dir=E\:\\BaiduYunDownload\\adt-bundle-windows-x86_64-20140702\\sdk
新建Project,一个
Activity(xml中带一个TextView)
新建一个
NdkJniUtils
类 声明原生方法getCString();
public class NdkJniUtil { public native String getCString(); }
生成C/C++ 头文件
法1: IDE:Build->MakeProject 得到class 编译之后的class在目录下
E:\AndroidTemp\JNIProject\app\build\intermediates\classes\debug
然后我们cd到这边
E:\AndroidTemp\JNIProject\app\build\intermediates\classes\debug> javah -jni com.france.jniproject.NdkJniUtil
执行完后可以看到在debug下生成的com_france_jniproject_NdkJniUtil.h
法2(推荐,复制命令操作速度快): 法1生成class后 可以不用cmd,而是采用as的,View->Tool Windows->Terminal",即在Studio中进行终端命令行工具 然后进入
E:\AndroidTemp\JNIProject\app\src\main>
执行javah命令,为的是生成的 .h 文件同样是在 <Project>\app\src\main路径下(jni下面)
,可以在Studio的工程结构中直接看到。
操作命令:
//-d jni 表示生成名为jni的目录
javah -d jni -classpath <SDK_android.jar>;<APP_classes> <class>
2
3
即
//<SDK_android.jar>看个人存放在哪
javah -d jni -classpath E:\BaiduYunDownload\adt-bundle-windows-x86_64-20140702\sdk\platforms\android-23\android.jar;..\..\build\intermediates\classes\debug com.france.jniproject.NdkJniUtil
2
然后就看到<project>\app\src\main\jni\com_france_jniproject_NdkJniUtil.h
出现了.
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_france_jniproject_NdkJniUtil */
#ifndef _Included_com_france_jniproject_NdkJniUtil
#define _Included_com_france_jniproject_NdkJniUtil
#ifdef __cplusplus
extern "C" {
#endif
// Class: com_france_jniproject_NdkJniUtil
// Method: getCString
// Signature: ()Ljava/lang/String;
// JniEnv: 指向可用JNI函数表的接口指针
// jobject: NdkJniUtil类实例的Java对象引用
JNIEXPORT jstring JNICALL Java_com_france_jniproject_NdkJniUtil_getCString
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
在main目录下新建jni目录放入.h文件 并新建c文件(名字随意)
#include "com_france_jniproject_NdkJniUtil.h" JNIEXPORT jstring JNICALL Java_com_france_jniproject_NdkJniUtil_getCString (JNIEnv *env, jobject obj){ return (*env)->NewStringUTF(env,"This just a test for Android Studio NDK JNI developer!"); }
方法名复制.h的
配置gradle(app) 在
defaultConfig {}
加入ndk{ moduleName "gahing"//生成的so名字 abiFilters "armeabi", "armeabi-v7a", "x86" //输出指定三种abi体系结构下的so库。目前可有可无。 }
Build -> make Module 'app'生成so库
我们AS是不需要添加Application.mk和Android.mk到jni目录下 如果是Eclipse的话就下面这样配置
Application.mk
APP_PROJECT_PATH := $(call my-dir)/project
APP_MODULES := nativebt
Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := gahing
LOCAL_SRC_FILES := jnitest.c
LOCAL_DEFAULT_CPP_EXTENSION := cpp
#include $(BUILD_EXECUTABLE)
include $(BUILD_SHARED_LIBRARY)
生成的so库我们可以在
E:\AndroidTemp\JNIProject\app\build\intermediates\ndk\debug\lib
找到,我们发现有7种版本,如果gradle里面配置abiFilters "armeabi", "armeabi-v7a", "x86" 就只剩这3种
参考:http://blog.csdn.net/sodino/article/details/41946607
现在有了so库,我们直接引用
public class NdkJniUtil {
public native String getCString();
static {
System.loadLibrary("gahing");
}
}
然后编译就可以运行了。 MainActivity
public class MainActivity extends AppCompatActivity {
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.text);
NdkJniUtil ndkJniUtil = new NdkJniUtil();
textView.setText(ndkJniUtil.getCString());
}
}
//参考:http://yanbober.github.io/2015/02/14/android_studio_jni_1/ //Android C++高级编程
# 内存泄漏
通过JNI GetStringChars(env,javaString,&isCopy)
函数和GetStringUTFChars
获得的C字符串在原生代码中使用完后需要正确释放
通过:(*env)->ReleaseString(UTF)Chars(env,javaString,str)
释放
同理,数组操作也需要释放指针
# NIO
创建直接字节缓冲区
unsigned char* buffer = (unsigned char*) malloc(1024);
...
jobject directBuffer;
directBuffer = (*env)->NewDirectByteBuffer(env,buffer,1024);
//注意释放内存
通过Java字节缓冲区获取原生字节数组
unsigned char* buffer;
buffer = (unsigned char*) (*env)->GetDirectByteBuffer(env,directBuffer);
(即 原生方法的内存分配超出虚拟机的管理范围)
# 访问域
原生方法获取Java对象属性及方法的值
获取域值需要调用2到3个的JNI函数: 用对象引用instance获得类
jclass clazz=(*env)->GetObjectClass(env,instance)
获取实例域(静态域)的域ID
jfieldID=(*env)->Get(Static)FieldID(env,clazz,变量名,"L/java/lang/String" 类型签名映射 );
获取实例域/静态域
jstring(返回值类型)=(*env)->GetObjectField(env,insrance(java引用对象),对应id)
原生方法获取Java对象方法
jmethodID methodID = (*env)->Get(Static)MethodID(env,clazz,方法名," 类型签名映射 ");
jstring(返回值类型)=(*env)->Call(Static)String(与返回值类型对应)Method(env,insrance(java引用对象),对应id);
就为了回到Java里面拿值,要进行这么多操作 增加额外负担 一般所有需要的参数都直接传递给原生方法,不用在回到Java中 如果真的要这么做,需要缓存最频繁使用的ID
Java类型签名映射
JNI使用Java虚拟机的类型签名表述。下表列出了这些类型签名:
类型签名 Java 类型 Z boolean B byte C char S short I int J long F float D double L fully-qualified-class ; 全限定的类 [ type type[] ( arg-types ) ret-type 方法类型
手工映射略麻烦,采用javap工具 命令行模式:
E:\AndroidTemp\JNIProject\app\build\intermediates\classes\debug>javap -p -s com.france.jniproject.NdkJniUtil
Compiled from "NdkJniUtil.java"
public class com.france.jniproject.NdkJniUtil {
public com.france.jniproject.NdkJniUtil();
Signature: ()V
public native java.lang.String getCString();
Signature: ()Ljava/lang/String;
static {};
Signature: ()V
}
# 2016/04/14
# 异常处理
Java中,抛出异常时,JVM停止运行代码并尝试捕获异常,进去异常处理程序 JNI需要开发人员在异常发生后显式实现异常处理流
# 捕获异常
如下NdkJniUtil定义一个抛出异常的方法
public class NdkJniUtil {
private void throwingMethod() throws NullPointerException{
throw new NullPointerException("this is NullPointerException");
}
}
获取到jmethodID后, 执行该函数使之产生异常
(*env)->CallVoidMethod(env,instance,jmethodID);
jthrowable ex=(*env)->ExceptionOccurred(env);
//通过调用函数ExceptionOccurred()来获得异常对象,它含有对错误情况的更详细说明
if(0!=ex){
//说明产生异常开始处理
//处理方式
//
}
可用两种方法来处理平台相关代码中的异常:
- 本地方法可选择立即返回,使异常在启动该本地方法调用的Java代码中抛出。
- 平台相关代码可通过调用ExceptionClear() 来清除异常,然后执行自己的异常处理代码。
(*env)->ExceptionClear(env);
抛出了某个异常之后,平台相关代码必须先清除异常,然后才能进行其它的JNI调用。
当有待定异常时,只有以下这些JNI函数可被 安全地调用:ExceptionOccurred()、ExceptionDescribe()和ExceptionClear()。
ExceptionDescribe()函数将打印有关待定异常的 调试消息。
# 抛出异常
JNI允许原生代码抛出异常
//获得异常类
jclass clazz=(*env)->FindClass(env,"java/lang/NullPointerException");
//ThrowNew来初始化且抛出新异常
if(0!=clazz){
(*env)->ThrowNew(env,clazz,"this is NullPointerException");
}
原生代码不受VM控制,抛出的异常不会停止原生函数的执行
此时应该释放所有已分配的原生资源(内存泄漏),例如内存及合适的返回值
而通过JNIEnv接口获取的引用,如果是局部引用当原生函数返回,他们自动被VM释放(具体参考下一节)
# 局部和全局引用
# 局部引用
对象是被作为局部引用传递给本地方法的 大部分JNI函数返回的都是局部引用,由JNI函数返回的所有Java对象都是局部引用 局部引用不能在后续的调用中被缓存及重用(使用期限仅在原生方法) 局部引用仅在创建它们的线程中有效。本地方法不能将局部引用从一个线程 传递到另一个线程中 例如上一节我们通过FindClass得到一个局部引用(异常类clazz) 当原生方法返回,它自动被释放
也可以用
(*env)->DeleteLocalRef(env,clazz);
显式释放
注:一般让VM去自动释放局部引用,以下情景考虑手动释放
- 本地方法要访问一个大型Java对象,于是创建了对该Java对象的局部引用。然后,本地方法要在返回调用程序之前执行其它计算。 对这个大型Java对象的局部引用将防止该对象被当作垃圾收集,即使在剩余的运算中并不再需要该对象。
- 本地方法创建了大量的局部引用,但这些局部引用并不是要同时使用。由于虚拟机需要一定的空间来跟踪每个局部引用, 创建太多的局部引用将可能使系统耗尽内存。 例如,本地方法要在一个大型对象数组中循环,把取回的元素作为局部引用, 并在每次迭代时对一个元素进行操作。每次迭代后,程序员不再需要对该数组元素的局部引用。
so,当内存密集型操作时最好删除无用的局部引用,当然也可以用EnsureLocalCapacity去申请更多的局部引用槽(如果内存足够的话)
# 局部引用的实现原理
为了实现局部引用,Java虚拟机为每个从Java到本地方法的控制转换都创建了注册服务程序。注册服务程序将不可移动的局部引用 映射为Java对象,并防止这些对象被当作垃圾收集。所有传给本地方法的Java对象(包括那些作为JNI函数调用结果返回的对象)将 被自动添加到注册服务程序中。本地方法返回后,注册服务程序将被删除,其中的所有项都可以被当作垃圾来收集。可用各种不同的 方法来实现注册服务程序,例如,使用表、链接列表或hash表来实现。虽然引用计数可用来避免注册服务程序中有重复的项,但JNI 实现不是必须检测和消除重复的项。注意,以保守方式扫描本地堆栈并不能如实地实现局部引用。平台相关代码可将局部引用储存在 全局或堆数据结构中。
# 全局引用
全局引用将一直有效,直到被显式释放(强引用,不注意释放可能导致内存溢出) 可以被其他原生函数及原生线程使用
jclass globalClazz;//设为全局变量 其他方法也可以调用
...原生方法中
//将局部引用初始化为全局引用
globalClazz = (*env)->NewGlobalRef(env,localClazz);
...
//方法返回后
//localClazz 被回收,globalClazz不被回收
//可以通过(*env)->DeleteGlobalRef(env,globalClazz)去释放它
# 弱全局引用
弱引用,原生方法返回后不被回收,但是当内存不足时会被回收(与Java一样)
//用给定的局部引用创建全局引用
jclazz weakGlobalClazz =(*env)->NewWeakGlobalRef(env,localClazz)
//用IsSameObject检测是否被回收(NULL)
if(JNI_FALSE == (*env)->IsSameObject(env,weakGlobalClazz,NULL)){/*未被回收,扔可使用*/}
else{/*被回收*/}
//删除弱全局引用
(*env)->DeleteWeakGlobalRef(env,weakGlobalClazz);
# 线程
传递给每个原生方法的JNIEnv 接口指针只在方法调用相关线程有效,其他线程不能被缓存or使用
# 同步
# 原生线程
> 参考: http://yanbober.github.io/2015/02/16/android_studio_jni_2/
> Android C++高级编程