NDK-JNI开发笔记

Android 2016-04-14

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 文件同样是在\app\src\main路径下(jni下面),可以在Studio的工程结构中直接看到。 操作命令:

//-d jni 表示生成名为jni的目录

javah -d jni -classpath <SDK_android.jar>;<APP_classes> <class>

//<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

然后就看到<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++高级编程

本文由 GaHingZ 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

赏个馒头吧