安卓逆向_15( 一 ) --- JNI 和 NDK

 

From:较详细的介绍JNI:https://blog.csdn.net/lizhifa2011/article/details/21021177

From:https://www.jb51.net/article/126111.htm

 

NDK 官方文档:https://developer.android.google.cn/training/articles/perf-jni
JNI / NDK 开发指南:https://wiki.jikexueyuan.com/project/jni-ndk-developer-guide/overview.html
Java Native Interface Specification—Contents:https://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/jniTOC.html
菜鸟教程  之 JNI 入门教程:https://www.runoob.com/w3cnote/jni-getting-started-tutorials.html
Android JNI学习(1、2、3、4、5 ):https://www.jianshu.com/p/b4431ac22ec2


JNI官方中文资料:https://blog.csdn.net/yishifu/article/details/52180448
JNI 入门教程( 菜鸟教程 ):https://www.runoob.com/w3cnote/jni-getting-started-tutorials.html
JNI 实战全面解析:https://blog.csdn.net/yuzhou_zang/article/details/78410632
Java 与 c++ 通过 JNI 的完美结合:https://blog.csdn.net/xiaoxiaoyusheng2012/article/details/56672173
使用 jni 调用 C++ 的过程:https://www.cnblogs.com/mssyj/p/12148739.html
VS2019 C++的跨平台开发——Android .so 开发:https://blog.csdn.net/luoyu510183/article/details/94590497
Android Studio 开发 JNI 示例:https://blog.csdn.net/wzhseu/article/details/79683045
JNI开发总结:https://cloud.tencent.com/developer/article/1356493
Android JNI原理分析:http://gityuan.com/2016/05/28/android-jni/

 

 

 

较详细的 JNI 简介

 

 

JNI 是本地语言编程接口。它允许运行在 JVM 中的 Java 代码和用C、C++ 或 汇编 写的本地代码相互操作。

在 Java中,有时候我们不得不要去使用其他语言的代码,比如说:

  • 1、你的应用需要访问系统的各个特性和设备,这些特性和设备通过java平台是无法访问的。
  • 2、你已经有了大量的测试过和调试过的用另一种语言编写的代码,并且知道如何将其导出到所有的目标平台上。
  • 3、通过基础测试,你已经发现所编写的 Java 代码比用其他语言编写的等价代码要慢得多。

Java 平台有一个用于和本地C代码进行互操作的 API,称为 Java本地接口JNI)。

 

JNI 有什么用?

        JNI 是 Java Native Interface 的缩写,它提供了若干的 API 实现了Java 和 其他语言的通信(主要是 C/C++)。通俗来说,就是 JAVA 调用 C/C++ 函数的接口。如果你要想调用 C系列的函数,你就必须遵守这样的约定。

        JNI 最常见的两个应用:从 Java 程序调用 C/C++,以及从 C/C++ 程序调用Java代码。JNI 是一个双向的接口:开发者不仅可以通过 JNI 在 Java 代码中访问 Native 模块,还可以在 Native 代码中嵌入一个 JVM,并通过 JNI 访问运行于其中的 Java 模块。可见 JNI 担任了一个桥梁的角色,它将 JVM 与 Native 模块联系起来,从而实现了 Java 代码与 Native 代码的互访

 

 

1、JNI 组织结构

JNI 函数表的组成就像 C++的虚函数表,虚拟机可以运行多张函数表。

JNI 接口指针仅在当前线程中起作用,指针不能从一个线程进入另一个线程,但可以在不同的线程中调用本地方法。

 

 

2、原始数据

Jobject 对象 和 引用类型

1、基本数据类型

以下是 Java的基本数据类型 jni中的基本数据类型 的 比较,及各类型所占的字节。

图表:

Java类型本地类型(JNI)描述
boolean(布尔型)jboolean无符号8个比特
byte(字节型)jbyte有符号8个比特
char(字符型)jchar无符号16个比特
short(短整型)jshort有符号16个比特
int(整型)jint有符号32个比特
long(长整型)jlong有符号64个比特
float(浮点型)jfloat32个比特
double(双精度浮点型)jdouble64个比特
void(空型)voidN/A

2、引用类型

Java不同的引用类型JNI当中也有对应的引用类型,如下树形表示结果:

当在 C 语言中使用时,所有的 JNI 引用类型都被定义为 jobject 类型。typedef jobject jclass;

jvalue 类型

jvalue 类型是一个 基本数据类型引用类型 的集合,定义方式如下:

 typedef union jvalue {jboolean z;jbyte    b;jchar    c;jshort   s;jint     i;jlong    j;jfloat   f;jdouble  d;jobject  l;} jvalue;

class 的说明

  • (1):类和接口的描述符在 java 当中使用 ".",如:java.lang.String。而在 JNI 当中是用 "/",如:java/lang/String
  • (2):数组类型的引用类型用 "[" 表示。如  int[] ( java中的表示法 )    [I ( [ 大写的 i 是 JNI 中的表示法,[ 的个数表示数组的维数  二维则是  [[ I )
  • (3):域的说明,和 java 比较如下表:

    注意:引用类型的域 用L开头,并且以”;”作为结尾。数组类型和class说明的一样。

  • (4):Method 说明JNI 中的方法的声明规则:先写参数列表,再写返回类型,以下是例子。

域描述符

Java 语言
Zboolean
Bbyte
Cchar
Sshort
Iint
Jlong
Ffloat
Ddouble

引用类型则为 L + 该类型类描述符 + 。

数组,其为 :  [ + 其类型的域描述符 + 。

int[ ]         描述符为  [I  
float[ ]       描述符为  [F  String[ ]      描述符为  [Ljava/lang/String;  
String         描述符为  Ljava/lang/String;    Object[ ]      描述符为  [Ljava/lang/Object;  
int  [ ][ ]    描述符为  [[I  
float[ ][ ]    描述符为  [[F  

将参数类型的域描述符按照申明顺序放入一对括号中后跟返回值类型的域描述符,规则如下: (参数的域描述符的叠加)返回类型描述符。对于,没有返回值的,用V(表示void型)表示。

举例如下:( 函数签名 就是 " 参数 + 返回值 " )

Java层方法                               JNI函数签名  String test ( )                         Ljava/lang/String;  int f (int i, Object object)            (ILjava/lang/Object;)I  void set (byte[ ] bytes)                ([B)V  

 

JavaVM  与 JNIEnv 这两个概念的区分:

  • JavaVM 是虚拟机在 JNI 层的代表,一个进程只有一个 JavaVM,所有的线程共用一个 JavaVM。JavaVM 是一个全局变量,一个进程只有一个 JavaVM 对象。
  • JNIEnv 是一个线程相关的结构体,该结构体代表了 Java 在本线程的运行环境 。JNIEnv 是一个线程拥有一个,不同线程的 JNIEnv 彼此独立。
    JNIEnv 作用 : 
            ----- 调用 Java 函数: JNIEnv 代表 Java 运行环境,可以使用 JNIEnv 调用 Java 中的代码。
            ----- 操作 Java 对象:Java 对象传入 JNI 层就是 Jobject 对象, 需要使用 JNIEnv 来操作这个 Java 对象。

 

JNIEnv 体系结构 

  • JNIEnv 是线程相关 : JNIEnv 是线程相关的,即在每个线程中都有一个 JNIEnv 指针,每个 JNIEnv 都是线程专有的, 其它线程不能使用本线程中的 JNIEnv, 线程 A 不能调用 线程 B 的 JNIEnv。
  • JNIEnv 不能跨线程
            --- 当前线程有效 : JNIEnv 只在当前线程有效, JNIEnv 不能在 线程之间进行传递, 在同一个线程中, 多次调用 JNI层方法, 传入的 JNIEnv 是相同的;
            --- 本地方法匹配多 JNIEnv : 在 Java 层定义的本地方法, 可以在不同的线程调用, 因此 可以接受不同的 JNIEnv;
  • JNIEnv 结构 : 由上面的代码可以得出,,JNIEnv 是一个指针,  指向一个线程相关的结构, 线程相关结构指向 JNI 函数指针数组, 这个数组中存放了大量的 JNI 函数指针,这些指针指向了具体的 JNI 函数; 
  • 注意:JNIEnv 只在当前线程中有效。本地方法不能将 JNIEnv 从一个线程传递到另一个线程中。相同的 Java 线程中对本地方法多次调用时,传递给该本地方法的 JNIEnv 是相同的。但是,一个本地方法可被不同的 Java 线程所调用,因此可以接受不同的 JNIEnv。

关于 UTF-8 编码:JNI 使用改进的 UTF-8 字符串来表示不同的字符类型。Java 使用 UTF-16 编码。UTF-8 编码主要使用于 C 语言,因为它的编码用 \u000 表示为 0xc0,而不是通常的 0×00。非空 ASCII 字符改进后的字符串编码中可以用一个字节表示。

关于错误:JNI不会检查 NullPointerException、IllegalArgumentException 这样的错误,原因是:导致性能下降。在绝大多数 C 的库函数中,很难避免错误发生。JNI 允许用户使用 Java 异常处理。大部分 JNI 方法会返回错误代码但本身并不会报出异常。因此,很有必要在代码本身进行处理,将异常抛给 Java。在 JNI 内部,首先会检查调用函数返回的错误代码,之后会调用 ExpectOccurred() 返回一个错误对象。

jthrowable ExceptionOccurred(JNIEnv *env);  

例如:一些操作数组的 JNI 函数不会报错,因此可以调用 ArrayIndexOutofBoundsException 或 ArrayStoreExpection 方法报告异常。  

 

 

3、JNI 函数 实战

 

1、*.so 的入口函数

JNI_OnLoad()JNI_OnUnload()

当 Android 的 VM(Virtual Machine) 执行到 System.loadLibrary() 函数时,首先会去执行 C 组件里的 JNI_OnLoad() 函数。它的用途有二:

  • (1)  告诉 VM 此 C 组件使用那一个 JNI 版本。如果你的 *.so 没有提供 JNI_OnLoad() 函数,VM 会默认该 *.so 是使用最老的JNI 1.1 版本。由于新版的 JNI 做了许多扩充,如果需要使用 JNI 的新版功能,例如 JNI 1.4 的 java.nio.ByteBuffer,就必须藉由 JNI_OnLoad() 函数来告知 VM 。
  • (2)  由于 VM 执行到 System.loadLibrary() 函数时,就会立即先呼叫 JNI_OnLoad(),所以 C 组件的开发者可以藉由JNI_OnLoad() 来进行 C 组件内的初期值之设定 (Initialization) 。

 

2、返回值

jstring str = env->newStringUTF("HelloJNI");  //直接使用该JNI构造一个jstring对象返回    
return str ;    

示例:

jobjectArray ret = 0;  
jsize len = 5;  
jstring str;  
string value("hello");  ret = (jobjectArray)(env->NewObjectArray(len, env->FindClass("java/lang/String"), 0));  
for(int i = 0; i < len; i++)  
{  str = env->NewStringUTF(value..c_str());  env->SetObjectArrayElement(ret, i, str);  
}  
return ret; 返回数组  

示例:

jclass    m_cls   = env->FindClass("com/ldq/ScanResult");    jmethodID m_mid   = env->GetMethodID(m_cls,"<init>","()V");    jfieldID  m_fid_1 = env->GetFieldID(m_cls,"ssid","Ljava/lang/String;");    jfieldID  m_fid_2 = env->GetFieldID(m_cls,"mac","Ljava/lang/String;");    jfieldID  m_fid_3 = env->GetFieldID(m_cls,"level","I");    jobject   m_obj   = env->NewObject(m_cls,m_mid);    env->SetObjectField(m_obj,m_fid_1,env->NewStringUTF("AP1"));    env->SetObjectField(m_obj,m_fid_2,env->NewStringUTF("00-11-22-33-44-55"));    env->SetIntField(m_obj,m_fid_3,-50);    return m_obj;  返回自定义对象  

示例:

jclass list_cls = env->FindClass("Ljava/util/ArrayList;");//获得ArrayList类引用    if(listcls == NULL)    {    cout << "listcls is null \n" ;    }    jmethodID list_costruct = env->GetMethodID(list_cls , "<init>","()V"); //获得得构造函数Id    jobject list_obj = env->NewObject(list_cls , list_costruct); //创建一个Arraylist集合对象    //或得Arraylist类中的 add()方法ID,其方法原型为: boolean add(Object object) ;    jmethodID list_add  = env->GetMethodID(list_cls,"add","(Ljava/lang/Object;)Z");     jclass stu_cls = env->FindClass("Lcom/feixun/jni/Student;");//获得Student类引用    //获得该类型的构造函数  函数名为 <init> 返回类型必须为 void 即 V    jmethodID stu_costruct = env->GetMethodID(stu_cls , "<init>", "(ILjava/lang/String;)V");    for(int i = 0 ; i < 3 ; i++)    {    jstring str = env->NewStringUTF("Native");    //通过调用该对象的构造函数来new 一个 Student实例    jobject stu_obj = env->NewObject(stucls , stu_costruct , 10,str);  //构造一个对象    env->CallBooleanMethod(list_obj , list_add , stu_obj); //执行Arraylist类实例的add方法,添加一个stu对象    }    return list_obj ;   返回对象集合  

 

3、操作 Java层 的 类

//获得jfieldID 以及 该字段的初始值    jfieldID  nameFieldId ;    jclass cls = env->GetObjectClass(obj);  //获得Java层该对象实例的类引用,即HelloJNI类引用    nameFieldId = env->GetFieldID(cls , "name" , "Ljava/lang/String;"); //获得属性句柄    if(nameFieldId == NULL)    {    cout << " 没有得到name 的句柄Id \n;" ;    }    jstring javaNameStr = (jstring)env->GetObjectField(obj ,nameFieldId);  // 获得该属性的值    const char * c_javaName = env->GetStringUTFChars(javaNameStr , NULL);  //转换为 char *类型    string str_name = c_javaName ;      cout << "the name from java is " << str_name << endl ; //输出显示    env->ReleaseStringUTFChars(javaNameStr , c_javaName);  //释放局部引用    //构造一个jString对象    char * c_ptr_name = "I come from Native" ;    jstring cName = env->NewStringUTF(c_ptr_name); //构造一个jstring对象    env->SetObjectField(obj , nameFieldId , cName); // 设置该字段的值   

 

4、回调 Java层 方法

jstring str = NULL;    jclass clz = env->FindClass("cc/androidos/jni/JniTest");    //获取clz的构造函数并生成一个对象    jmethodID ctor = env->GetMethodID(clz, "<init>", "()V");    jobject obj = env->NewObject(clz, ctor);    // 如果是数组类型,则在类型前加[, 如整形数组int[] intArray, 则对应类型为[I, 即整形数组。// String[] strArray 对应为 [Ljava/lang/String;    jmethodID mid = env->GetMethodID(clz, "sayHelloFromJava", "(Ljava/lang/String;II[I)I");    if (mid)    {    LOGI("mid is get");    jstring str1 = env->NewStringUTF("I am Native");    jint index1 = 10;    jint index2 = 12;    //env->CallVoidMethod(obj, mid, str1, index1, index2);    // 数组类型转换 testIntArray能不能不申请内存空间    jintArray testIntArray = env->NewIntArray(10);    jint *test = new jint[10];    for(int i = 0; i < 10; ++i)    {    *(test+i) = i + 100;    }    env->SetIntArrayRegion(testIntArray, 0, 10, test);    jint javaIndex = env->CallIntMethod(obj, mid, str1, index1, index2, testIntArray);    LOGI("javaIndex = %d", javaIndex);    delete[] test;    test = NULL;    }    

示例代码:

static void event_callback(int eventId,const char* description) {  //主进程回调可以,线程中回调失败。  if (gEventHandle == NULL)  return;  JNIEnv *env;  bool isAttached = false;  if (myVm->GetEnv((void**) &env, JNI_VERSION_1_2) < 0) { //获取当前的JNIEnv  if (myVm->AttachCurrentThread(&env, NULL) < 0)  return;  isAttached = true;  }  jclass cls = env->GetObjectClass(gEventHandle); //获取类对象  if (!cls) {  LOGE("EventHandler: failed to get class reference");  return;  }  jmethodID methodID = env->GetStaticMethodID(cls, "callbackStatic",  "(ILjava/lang/String;)V");  //静态方法或成员方法  if (methodID) {  jstring content = env->NewStringUTF(description);  env->CallVoidMethod(gEventHandle, methodID,eventId,  content);  env->ReleaseStringUTFChars(content,description);  } else {  LOGE("EventHandler: failed to get the callback method");  }  if (isAttached)  myVm->DetachCurrentThread();  
}  

 

线程中回调
把 c/c++ 中所有线程的创建,由 pthread_create 函数替换为由 Java 层的创建线程的函数 AndroidRuntime::createJavaThread。

static pthread_t create_thread_callback(const char* name, void (*start)(void *), void* arg)    
{    return (pthread_t)AndroidRuntime::createJavaThread(name, start, arg);    
}   static void checkAndClearExceptionFromCallback(JNIEnv* env, const char* methodName) {  //异常检测和排除  if (env->ExceptionCheck()) {    LOGE("An exception was thrown by callback '%s'.", methodName);    LOGE_EX(env);    env->ExceptionClear();    }    
}    static void receive_callback(unsigned char *buf, int len)  //回调  
{    int i;    JNIEnv* env = AndroidRuntime::getJNIEnv();    jcharArray array = env->NewCharArray(len);    jchar *pArray ;    if(array == NULL){    LOGE("receive_callback: NewCharArray error.");    return;     }    pArray = (jchar*)calloc(len, sizeof(jchar));    if(pArray == NULL){    LOGE("receive_callback: calloc error.");    return;     }    //copy buffer to jchar array    for(i = 0; i < len; i++)    {    *(pArray + i) = *(buf + i);    }    //copy buffer to jcharArray    env->SetCharArrayRegion(array,0,len,pArray);    //invoke java callback method    env->CallVoidMethod(mCallbacksObj, method_receive,array,len);    //release resource    env->DeleteLocalRef(array);    free(pArray);    pArray = NULL;    checkAndClearExceptionFromCallback(env, __FUNCTION__);    
}  public void Receive(char buffer[],int length){  //java层函数  String msg = new String(buffer);    msg = "received from jni callback" + msg;    Log.d("Test", msg);    }  

示例代码:

jclass cls = env->GetObjectClass(obj);//获得Java类实例    
jmethodID callbackID = env->GetMethodID(cls , "callback" , "(Ljava/lang/String;)V") ;//或得该回调方法句柄    if(callbackID == NULL)    
{    cout << "getMethodId is failed \n" << endl ;    
}    jstring native_desc = env->NewStringUTF(" I am Native");    env->CallVoidMethod(obj , callbackID , native_desc); //回调该方法,并且  

 

5、传对象到 JNI 调用

jclass stu_cls = env->GetObjectClass(obj_stu); //或得Student类引用    if(stu_cls == NULL)    {    cout << "GetObjectClass failed \n" ;    }    //下面这些函数操作,我们都见过的。O(∩_∩)O~    jfieldID ageFieldID = env->GetFieldID(stucls,"age","I"); //获得得Student类的属性id     jfieldID nameFieldID = env->GetFieldID(stucls,"name","Ljava/lang/String;"); // 获得属性ID    jint age = env->GetIntField(objstu , ageFieldID);  //获得属性值    jstring name = (jstring)env->GetObjectField(objstu , nameFieldID);//获得属性值    const char * c_name = env->GetStringUTFChars(name ,NULL);//转换成 char *    string str_name = c_name ;     env->ReleaseStringUTFChars(name,c_name); //释放引用    cout << " at Native age is :" << age << " # name is " << str_name << endl ;     

 

6、与 C++ 互转

jbytearray 转 c++byte 数组

jbyte * arrayBody = env->GetByteArrayElements(data,0);     
jsize theArrayLengthJ = env->GetArrayLength(data);     
BYTE * starter = (BYTE *)arrayBody;     

jbyteArray 转 c++ 中的 BYTE[] 

jbyte * olddata = (jbyte*)env->GetByteArrayElements(strIn, 0);    
jsize  oldsize = env->GetArrayLength(strIn);    
BYTE* bytearr = (BYTE*)olddata;    
int len = (int)oldsize;    

C++ 中的 BYTE[] 转 jbyteArray 

jbyte *by = (jbyte*)pData;    
jbyteArray jarray = env->NewByteArray(nOutSize);    
env->SetByteArrayRegin(jarray, 0, nOutSize, by);    

jbyteArray 转 char * 

char* data = (char*)env->GetByteArrayElements(strIn, 0);    

char* 转 jstring

jstring WindowsTojstring(JNIEnv* env, char* str_tmp)    
{    jstring rtn=0;    int slen = (int)strlen(str_tmp);    unsigned short* buffer=0;    if(slen == 0)    {    rtn = env->NewStringUTF(str_tmp);    }    else    {    int length = MultiByteToWideChar(CP_ACP, 0, (LPCSTR)str_tmp, slen, NULL, 0);    buffer = (unsigned short*)malloc(length*2+1);    if(MultiByteToWideChar(CP_ACP, 0, (LPCSTR)str_tmp, slen, (LPWSTR)buffer, length) > 0)    {    rtn = env->NewString((jchar*)buffer, length);    }    }    if(buffer)    {    free(buffer);    }    return rtn;    
}    

char* jstring 互转

JNIEXPORT jstring JNICALL Java_com_explorer_jni_SambaTreeNative_getDetailsBy    (JNIEnv *env, jobject jobj, jstring pc_server, jstring server_user, jstring server_passwd)    
{    const char *pc = env->GetStringUTFChars(pc_server, NULL);    const char *user = env->GetStringUTFChars(server_user, NULL);    const char *passwd = env->GetStringUTFChars(server_passwd, NULL);    const char *details = smbtree::getPara(pc, user, passwd);    jstring jDetails = env->NewStringUTF(details);    return jDetails;    
}    

 

4、Android.mk、Application.mk

1、Android.mk

Android.mk 文件是 GNU Makefile 的一小部分,它用来对 Android 程序进行编译,Android.mk 中的变量都是全局的,解析过程会被定义。一个 Android.mk 文件可以编译多个模块,模块包括:APK程序、JAVA库、C\C++应用程序、C\C++静态库、C\C++共享库。

简单实例如下:

LOCAL_PATH := $(call my-dir)  #表示是当前文件的路径  
include $(CLEAR_VARS)       #指定让GNU MAKEFILE该脚本为你清除许多 LOCAL_XXX 变量  
LOCAL_MODULE:= helloworld   #标识你在 Android.mk 文件中描述的每个模块  
MY_SOURCES := foo.c         #自定义变量  
ifneq ($(MY_CONFIG_BAR),)  MY_SOURCES += bar.c  
endif  
LOCAL_SRC_FILES += $(MY_SOURCES)    #包含将要编译打包进模块中的 C 或 C++源代码文件  
include $(BUILD_SHARED_LIBRARY) #根据LOCAL_XXX系列变量中的值,来编译生成共享库(动态链接库)  

 

GNU Make 系统变量

变量描述
CLEAR_VARS指向一个编译脚本,几乎所有未定义的 LOCAL_XXX 变量都在"Module-description"节中列出。必须在开始一个新模块之前包含这个脚本:include$(CLEAR_VARS),用于重置除LOCAL_PATH变量外的,所有LOCAL_XXX系列变量。
BUILD_SHARED_LIBRARY指向编译脚本,根据所有的在 LOCAL_XXX 变量把列出的源代码文件编译成一个共享库。
BUILD_STATIC_LIBRARY一个 BUILD_SHARED_LIBRARY 变量用于编译一个静态库。静态库不会复制到的APK包中,但是能够用于编译共享库。
TARGET_ARCH目标 CPU平台的名字,  和 android 开放源码中指定的那样。如果是arm,表示要生成 ARM 兼容的指令,与 CPU架构的修订版无关。
TARGET_PLATFORMAndroid.mk 解析的时候,目标 Android 平台的名字.详情可参考/development/ndk/docs/stable- apis.txt.
TARGET_ARCH_ABI支持目标平台
TARGET_ABI目标平台和 ABI 的组合,它事实上被定义成$(TARGET_PLATFORM)-$(TARGET_ARCH_ABI)  ,在想要在真实的设备中针对一个特别的目标系统进行测试时,会有用。在默认的情况下,它会是'android-3-arm'。

模块描述变量

变量描述
LOCAL_PATH这个变量用于给出当前文件的路径。必须在 Android.mk 的开头定义,可以这样使用:LOCAL_PATH := $(call my-dir)  这个变量不会被$(CLEAR_VARS)清除,因此每
个 Android.mk 只需要定义一次(即使在一个文件中定义了几个模块的情况下)。
LOCAL_MODULE这是模块的名字,它必须是唯一的,而且不能包含空格。必须在包含任一的$(BUILD_XXXX)脚本之前定义它。模块的名字决定了生成文件的名字。例如,如果一个一个共享库模块的名字是,那么生成文件的名字就是 lib.so。但是,在的 NDK 生成文件中(或者 Android.mk 或者 Application.mk),应该只涉及(引用)有正常名字的其他模块。
LOCAL_SRC_FILES这是要编译的源代码文件列表。只要列出要传递给编译器的文件,因为编译系统自动计算依赖。注意源代码文件名称都是相对于 LOCAL_PATH的,你可以使用路径部分。
LOCAL_CPP_EXTENSION这是一个可选变量, 用来指定C++代码文件的扩展名,默认是'.cpp',但是可以改变它。
LOCAL_C_INCLUDES可选变量,表示头文件的搜索路径。
LOCAL_CFLAGS可选的编译器选项,在编译 C 代码文件的时候使用。
LOCAL_CXXFLAGS与 LOCAL_CFLAGS同理,针对 C++源文件。
LOCAL_CPPFLAGS与 LOCAL_CFLAGS同理,但是对 C 和 C++ source files都适用。
LOCAL_STATIC_LIBRARIES表示该模块需要使用哪些静态库,以便在编译时进行链接。
LOCAL_SHARED_LIBRARIES表示模块在运行时要依赖的共享库(动态库),在链接时就需要,以便在生成文件时嵌入其相应的信息。注意:它不会附加列出的模块到编译图,也就是仍然需要在Application.mk 中把它们添加到程序要求的模块中。
LOCAL_LDLIBS编译模块时要使用的附加的链接器选项。这对于使用‘-l’前缀传递指定库的名字是有用的。
LOCAL_ALLOW_UNDEFINED_SYMBOLS默认情况下, 在试图编译一个共享库时,任何未定义的引用将导致一个“未定义的符号”错误。
LOCAL_ARM_MODE默认情况下, arm目标二进制会以 thumb 的形式生成(16 位),你可以通过设置这个变量为 arm如果你希望你的 module 是以 32 位指令的形式。
LOCAL_MODULE_PATH 和 LOCAL_UNSTRIPPED_PATH在 Android.mk 文件中, 还可以用LOCAL_MODULE_PATH 和LOCAL_UNSTRIPPED_PATH指定最后的目标安装路径.
不同的文件系统路径用以下的宏进行选择:
  TARGET_ROOT_OUT:表示根文件系统。
   TARGET_OUT:表示 system文件系统。
   TARGET_OUT_DATA:表示 data文件系统。
用法如:LOCAL_MODULE_PATH :=$(TARGET_ROOT_OUT) 
至于LOCAL_MODULE_PATH 和LOCAL_UNSTRIPPED_PATH的区别,暂时还不清楚。

GNU Make 功能宏

变量描述
my-dir返回当前 Android.mk 所在的目录的路径,相对于 NDK 编译系统的顶层。
all-subdir-makefiles返回一个位于当前'my-dir'路径的子目录中的所有Android.mk的列表。
this-makefile返回当前Makefile 的路径(即这个函数调用的地方)
parent-makefile返回调用树中父 Makefile 路径。即包含当前Makefile的Makefile 路径。
grand-parent-makefile返回调用树中父Makefile的父Makefile的路径

范例:编译一个简单的 APK

LOCAL_PATH := $(call my-dir)  
include $(CLEAR_VARS)  
# Build all java files in the java subdirectory  
LOCAL_SRC_FILES := $(call all-subdir-java-files)  
# Name of the APK to build  
LOCAL_PACKAGE_NAME := LocalPackage  
# Tell it to build an APK  
include $(BUILD_PACKAGE)  

编译一个依赖静态 .jar 文件的 APK 

LOCAL_PATH := $(call my-dir)  include $(CLEAR_VARS)  # List of static libraries to include in the package  LOCAL_STATIC_JAVA_LIBRARIES := static-library  # Build all java files in the java subdirectory  LOCAL_SRC_FILES := $(call all-subdir-java-files)  # Name of the APK to build  LOCAL_PACKAGE_NAME := LocalPackage  # Tell it to build an APK  include $(BUILD_PACKAGE)  注:LOCAL_STATIC_JAVA_LIBRARIES 后面应是你的APK程序所需要的JAVA库的JAR文件名。  

编译一个需要 platform key 签名的 APK

LOCAL_PATH := $(call my-dir)  include $(CLEAR_VARS)  # Build all java files in the java subdirectory  LOCAL_SRC_FILES := $(call all-subdir-java-files)  # Name of the APK to build  LOCAL_PACKAGE_NAME := LocalPackage  LOCAL_CERTIFICATE := platform  # Tell it to build an APK  include $(BUILD_PACKAGE)  注:LOCAL_CERTIFICATE 后面应该是签名文件的文件名  

编译一个需要特殊 vendor key 签名的 APK 

LOCAL_PATH := $(call my-dir)  include $(CLEAR_VARS)  # Build all java files in the java subdirectory  LOCAL_SRC_FILES := $(call all-subdir-java-files)  # Name of the APK to build  LOCAL_PACKAGE_NAME := LocalPackage  LOCAL_CERTIFICATE := vendor/example/certs/app  # Tell it to build an APK  include $(BUILD_PACKAGE)  

装载一个普通的第三方 APK

LOCAL_PATH := $(call my-dir)  include $(CLEAR_VARS)  # Module name should match apk name to be installed.  LOCAL_MODULE := LocalModuleName  LOCAL_SRC_FILES := $(LOCAL_MODULE).apk  LOCAL_MODULE_CLASS := APPS  LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)  LOCAL_CERTIFICATE := platform  include $(BUILD_PREBUILT)   

装载需要 .so(动态库)的第三方 apk

LOCAL_PATH := $(my-dir)  
include $(CLEAR_VARS)  
LOCAL_MODULE := baiduinput_android_v1.1_1000e  
LOCAL_SRC_FILES := $(LOCAL_MODULE).apk  
LOCAL_MODULE_CLASS := APPS  
LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)  
LOCAL_CERTIFICATE := platform  
include $(BUILD_PREBUILT)  #################################################################  
####### copy the library to /system/lib #########################  
#################################################################  
include $(CLEAR_VARS)  
LOCAL_MODULE := libinputcore.so  
LOCAL_MODULE_CLASS := SHARED_LIBRARIES  
LOCAL_MODULE_PATH := $(TARGET_OUT_SHARED_LIBRARIES)  
LOCAL_SRC_FILES := lib/$(LOCAL_MODULE)  
OVERRIDE_BUILD_MODULE_PATH := $(TARGET_OUT_INTERMEDIATE_LIBRARIES)  
include $(BUILD_PREBUILT)  

编译一个静态 java 库 

  LOCAL_PATH := $(call my-dir)  include $(CLEAR_VARS)  # Build all java files in the java subdirectory  LOCAL_SRC_FILES := $(call all-subdir-java-files)  # Any libraries that this library depends on  LOCAL_JAVA_LIBRARIES := android.test.runner  # The name of the jar file to create  LOCAL_MODULE := sample  # Build a static jar file.  include $(BUILD_STATIC_JAVA_LIBRARY)  
注:LOCAL_JAVA_LIBRARIES表示生成的java库的jar文件名。  

编译 C/C++ 应用程序模板

LOCAL_PATH := $(call my-dir)  
#include $(CLEAR_VARS)  
LOCAL_SRC_FILES := main.c  
LOCAL_MODULE := test_exe  
#LOCAL_C_INCLUDES :=  
#LOCAL_STATIC_LIBRARIES :=  
#LOCAL_SHARED_LIBRARIES :=  
include $(BUILD_EXECUTABLE)  
注:‘:=’是赋值的意思,'+='是追加的意思,‘$’表示引用某变量的值  
LOCAL_SRC_FILES中加入源文件路径,LOCAL_C_INCLUDES中加入需要的头文件搜索路径  
LOCAL_STATIC_LIBRARIES 加入所需要链接的静态库(*.a)的名称,  
LOCAL_SHARED_LIBRARIES 中加入所需要链接的动态库(*.so)的名称,  
LOCAL_MODULE表示模块最终的名称,BUILD_EXECUTABLE 表示以一个可执行程序的方式进行编译。  
(4)编译C\C++静态库  
LOCAL_PATH := $(call my-dir)  
include $(CLEAR_VARS)  
LOCAL_SRC_FILES := \  helloworld.c  
LOCAL_MODULE:= libtest_static  #LOCAL_C_INCLUDES :=  
#LOCAL_STATIC_LIBRARIES :=  
#LOCAL_SHARED_LIBRARIES :=  
include $(BUILD_STATIC_LIBRARY)  
和上面相似,BUILD_STATIC_LIBRARY 表示编译一个静态库。  

编译 C\C++ 动态库 的 模板

LOCAL_PATH := $(call my-dir)  
include $(CLEAR_VARS)  
LOCAL_SRC_FILES := helloworld.c  
LOCAL_MODULE := libtest_shared  
TARGET_PRELINK_MODULES := false  
#LOCAL_C_INCLUDES :=  
#LOCAL_STATIC_LIBRARIES :=  
#LOCAL_SHARED_LIBRARIES :=  
include $(BUILD_SHARED_LIBRARY)  
和上面相似,BUILD_SHARED_LIBRARY 表示编译一个共享库。  
以上三者的生成结果分别在如下目录中,generic 依具体 target 会变:  
out/target/product/generic/obj/APPS  
out/target/product/generic/obj/JAVA_LIBRARIES  
out/target/product/generic/obj/EXECUTABLE  
out/target/product/generic/obj/STATIC_LIBRARY  
out/target/product/generic/obj/SHARED_LIBRARY  
每个模块的目标文件夹分别为:  
1)APK程序:XXX_intermediates  
2)JAVA库程序:XXX_intermediates  
这里的XXX  3)C\C++可执行程序:XXX_intermediates  4)C\C++静态库: XXX_static_intermediates  5)C\C++动态库: XXX_shared_intermediates  

实例:

LOCAL_PATH := $(call my-dir)  #项目地址  
include $(CLEAR_VARS)       #清除变量  LOCAL_MODULE    := libvlcjni    #库  #源文件  
LOCAL_SRC_FILES := libvlcjni.c libvlcjni-util.c libvlcjni-track.c libvlcjni-medialist.c aout.c vout.c libvlcjni-equalizer.c native_crash_handler.c  
LOCAL_SRC_FILES += thumbnailer.c pthread-condattr.c pthread-rwlocks.c pthread-once.c eventfd.c sem.c  
LOCAL_SRC_FILES += pipe2.c  
LOCAL_SRC_FILES += wchar/wcpcpy.c  
LOCAL_SRC_FILES += wchar/wcpncpy.c  
LOCAL_SRC_FILES += wchar/wcscasecmp.c  
LOCAL_SRC_FILES += wchar/wcscat.c  
LOCAL_SRC_FILES += wchar/wcschr.c  
LOCAL_SRC_FILES += wchar/wcscmp.c  
LOCAL_SRC_FILES += wchar/wcscoll.c  
LOCAL_SRC_FILES += wchar/wcscpy.c  
LOCAL_SRC_FILES += wchar/wcscspn.c  
LOCAL_SRC_FILES += wchar/wcsdup.c  
LOCAL_SRC_FILES += wchar/wcslcat.c  
LOCAL_SRC_FILES += wchar/wcslcpy.c  
LOCAL_SRC_FILES += wchar/wcslen.c  
LOCAL_SRC_FILES += wchar/wcsncasecmp.c  
LOCAL_SRC_FILES += wchar/wcsncat.c  
LOCAL_SRC_FILES += wchar/wcsncmp.c  
LOCAL_SRC_FILES += wchar/wcsncpy.c  
LOCAL_SRC_FILES += wchar/wcsnlen.c  
LOCAL_SRC_FILES += wchar/wcspbrk.c  
LOCAL_SRC_FILES += wchar/wcsrchr.c  
LOCAL_SRC_FILES += wchar/wcsspn.c  
LOCAL_SRC_FILES += wchar/wcsstr.c  
LOCAL_SRC_FILES += wchar/wcstok.c  
LOCAL_SRC_FILES += wchar/wcswidth.c  
LOCAL_SRC_FILES += wchar/wcsxfrm.c  
LOCAL_SRC_FILES += wchar/wmemchr.c  
LOCAL_SRC_FILES += wchar/wmemcmp.c  
LOCAL_SRC_FILES += wchar/wmemcpy.c  
LOCAL_SRC_FILES += wchar/wmemmove.c  
LOCAL_SRC_FILES += wchar/wmemset.c  LOCAL_C_INCLUDES := $(VLC_SRC_DIR)/include  #包含头  ARCH=$(ANDROID_ABI) #变量 平台  CPP_STATIC=$(ANDROID_NDK)/sources/cxx-stl/gnu-libstdc++$(CXXSTL)/libs/$(ARCH)/libgnustl_static.a #应用静态库  LOCAL_CFLAGS := -std=gnu99  #编译器标识  
ifeq ($(ARCH), armeabi)  LOCAL_CFLAGS += -DHAVE_ARMEABI  # Needed by ARMv6 Thumb1 (the System Control coprocessor/CP15 is mandatory on ARMv6)  # On newer ARM architectures we can use Thumb2  LOCAL_ARM_MODE := arm  
endif  
ifeq ($(ARCH), armeabi-v7a)  LOCAL_CFLAGS += -DHAVE_ARMEABI_V7A  
endif  
LOCAL_LDLIBS := -L$(VLC_CONTRIB)/lib \  #使用本地库  $(VLC_MODULES) \  $(VLC_BUILD_DIR)/lib/.libs/libvlc.a \  $(VLC_BUILD_DIR)/src/.libs/libvlccore.a \  $(VLC_BUILD_DIR)/compat/.libs/libcompat.a \  -ldl -lz -lm -llog \  -ldvbpsi -lebml -lmatroska -ltag \  -logg -lFLAC -ltheora -lvorbis \  -lmpeg2 -la52 \  -lavformat -lavcodec -lswscale -lavutil -lpostproc -lgsm -lopenjpeg \  -lliveMedia -lUsageEnvironment -lBasicUsageEnvironment -lgroupsock \  -lspeex -lspeexdsp \  -lxml2 -lpng -lgnutls -lgcrypt -lgpg-error \  -lnettle -lhogweed -lgmp \  -lfreetype -liconv -lass -lfribidi -lopus \  -lEGL -lGLESv2 -ljpeg \  -ldvdnav -ldvdread -ldvdcss \  $(CPP_STATIC)  include $(BUILD_SHARED_LIBRARY) #编译成动态库  include $(CLEAR_VARS)   #清除变量  LOCAL_MODULE     := libiomx-gingerbread    
LOCAL_SRC_FILES  := ../$(VLC_SRC_DIR)/modules/codec/omxil/iomx.cpp  
LOCAL_C_INCLUDES := $(VLC_SRC_DIR)/modules/codec/omxil $(ANDROID_SYS_HEADERS_GINGERBREAD)/frameworks/base/include $(ANDROID_SYS_HEADERS_GINGERBREAD)/system/core/include  
LOCAL_CFLAGS     := -Wno-psabi  
LOCAL_LDLIBS     := -L$(ANDROID_LIBS) -lgcc -lstagefright -lmedia -lutils -lbinder  include $(BUILD_SHARED_LIBRARY)  include $(CLEAR_VARS)  LOCAL_MODULE     := libiomx-hc  
LOCAL_SRC_FILES  := ../$(VLC_SRC_DIR)/modules/codec/omxil/iomx.cpp  
LOCAL_C_INCLUDES := $(VLC_SRC_DIR)/modules/codec/omxil $(ANDROID_SYS_HEADERS_HC)/frameworks/base/include $(ANDROID_SYS_HEADERS_HC)/frameworks/base/native/include $(ANDROID_SYS_HEADERS_HC)/system/core/include $(ANDROID_SYS_HEADERS_HC)/hardware/libhardware/include  
LOCAL_CFLAGS     := -Wno-psabi  
LOCAL_LDLIBS     := -L$(ANDROID_LIBS) -lgcc -lstagefright -lmedia -lutils -lbinder  include $(BUILD_SHARED_LIBRARY)  include $(CLEAR_VARS)  LOCAL_MODULE     := libiomx-ics  
LOCAL_SRC_FILES  := ../$(VLC_SRC_DIR)/modules/codec/omxil/iomx.cpp  
LOCAL_C_INCLUDES := $(VLC_SRC_DIR)/modules/codec/omxil $(ANDROID_SYS_HEADERS_ICS)/frameworks/base/include $(ANDROID_SYS_HEADERS_ICS)/frameworks/base/native/include $(ANDROID_SYS_HEADERS_ICS)/system/core/include $(ANDROID_SYS_HEADERS_ICS)/hardware/libhardware/include  
LOCAL_CFLAGS     := -Wno-psabi  
LOCAL_LDLIBS     := -L$(ANDROID_LIBS) -lgcc -lstagefright -lmedia -lutils -lbinder  include $(BUILD_SHARED_LIBRARY)  

 

2、Application.mk

Application.mk 目的是描述在你的应用程序中所需要的模块 (即 静态库 或 动态库)。

变量描述
APP_PROJECT_PATH这个变量是强制性的,并且会给出应用程序工程的根目录的一个绝对路径。
APP_MODULES这个变量是可选的,如果没有定义,NDK将由在Android.mk中声明的默认的模块编译,并且包含所有的子文件(makefile文件)如果APP_MODULES定义了,它不许是一个空格分隔的模块列表,这个模块名字被定义在Android.mk文件中的LOCAL_MODULE中。
APP_OPTIM这个变量是可选的,用来义“release”或"debug"。在编译您的应用程序模块的时候,可以用来改变优先级。
APP_CFLAGS当编译模块中有任何C文件或者C++文件的时候,C编译器的信号就会被发出。
APP_CXXFLAGSAPP_CPPFLAGS的别名,已经考虑在将在未来的版本中废除了
APP_CPPFLAGS当编译的只有C++源文件的时候,可以通过这个C++编译器来设置
APP_BUILD_SCRIPT默认情况下,NDK编译系统会在$(APP_PROJECT_PATH)/jni目录下寻找名为Android.mk文件:
$(APP_PROJECT_PATH)/jni/Android.mk
APP_ABI默认情况下,NDK的编译系统回味"armeabi"ABI生成机器代码。
APP_STL默认情况下,NDK的编译系统为最小的C++运行时库(/system/lib/libstdc++.so)提供C++头文件。然而,NDK的C++的实现,可以让你使用或着链接在自己的应用程序中。
例如:
APP_STL := stlport_static    --> static STLport library
APP_STL := stlport_shared    --> shared STLport library
APP_STL := system            --> default C++ runtime library

实例:

APP_OPTIM := release   //调试版还是发行版  
APP_PLATFORM := android-8  //平台  
APP_STL := gnustl_static  //C++运行时库  
APP_CPPFLAGS += -frtti      //编译标识  
APP_CPPFLAGS += -fexceptions  //编译标识 异常  
APP_CPPFLAGS += -DANDROID   //编译标识  
APP_MODULES := test     //静态模块  

 

JNI 内存泄漏

JAVA 编程中的内存泄漏,从泄漏的内存位置角度可以分为两种:

  • JVM 中 Java Heap 的内存泄漏。

    Java 对象存储在 JVM 进程空间中的 Java Heap 中,Java Heap 可以在 JVM 运行过程中动态变化。如果 Java 对象越来越多,占据 Java Heap 的空间也越来越大,JVM 会在运行时扩充 Java Heap 的容量。如果 Java Heap 容量扩充到上限,并且在 GC 后仍然没有足够空间分配新的 Java 对象,便会抛出 out of memory 异常,导致 JVM 进程崩溃。
    Java Heap 中 out of memory 异常的出现有两种原因①程序过于庞大,致使过多 Java 对象的同时存在;②程序编写的错误导致 Java Heap 内存泄漏。

  • JVM 内存中 native memory 的内存泄漏。

             从操作系统角度看,JVM 在运行时和其它进程没有本质区别。在系统级别上,它们具有同样的调度机制,同样的内存分配方式,同样的内存格局。
            JVM 进程空间中,Java Heap 以外的内存空间称为 JVM 的 native memory。进程的很多资源都是存储在 JVM 的 native memory 中,例如载入的代码映像,线程的堆栈,线程的管理控制块,JVM 的静态数据、全局数据等等。也包括 JNI 程序中 native code 分配到的资源。
            在 JVM 运行中,多数进程资源从 native memory 中动态分配。当越来越多的资源在 native memory 中分配,占据越来越多 native memory 空间并且达到 native memory 上限时,JVM 会抛出异常,使 JVM 进程异常退出。而此时 Java Heap 往往还没有达到上限。
            多种原因可能导致 JVM 的 native memory 内存泄漏。
                例如:
                        JVM 在运行中过多的线程被创建,并且在同时运行。
                        JVM 为线程分配的资源就可能耗尽 native memory 的容量。
                        JNI 编程错误也可能导致 native memory 的内存泄漏。

Native Code 本身的内存泄漏

JNI 编程首先是一门具体的编程语言,或者 C 语言,或者 C++,或者汇编,或者其它 native 的编程语言。每门编程语言环境都实现了自身的内存管理机制。因此,JNI 程序开发者要遵循 native 语言本身的内存管理机制,避免造成内存泄漏。以 C 语言为例,当用 malloc() 在进程堆中动态分配内存时,JNI 程序在使用完后,应当调用 free() 将内存释放。总之,所有在 native 语言编程中应当注意的内存泄漏规则,在 JNI 编程中依然适应。
Native 语言本身引入的内存泄漏会造成 native memory 的内存,严重情况下会造成 native memory 的 out of memory。

 

Global Reference 引入的内存泄漏

JNI 编程还要同时遵循 JNI 的规范标准,JVM 附加了 JNI 编程特有的内存管理机制。
JNI 中的 Local Reference 只在 native method 执行时存在,当 native method 执行完后自动失效。这种自动失效,使得对 Local Reference 的使用相对简单,native method 执行完后,它们所引用的 Java 对象的 reference count 会相应减 1。不会造成 Java Heap 中 Java 对象的内存泄漏。而 Global Reference 对 Java 对象的引用一直有效,因此它们引用的 Java 对象会一直存在 Java Heap 中。程序员在使用 Global Reference 时,需要仔细维护对 Global Reference 的使用。如果一定要使用 Global Reference,务必确保在不用的时候删除。就像在 C 语言中,调用 malloc() 动态分配一块内存之后,调用 free() 释放一样。否则,Global Reference 引用的 Java 对象将永远停留在 Java Heap 中,造成 Java Heap 的内存泄漏。

 

LocalReference 的深入理解

Local Reference 在 native method 执行完成后,会自动被释放,似乎不会造成任何的内存泄漏。但这是错误的。



泄漏实例1:创建大量的 JNI Local Reference

Java 代码部分  class TestLocalReference {   private native void nativeMethod(int i);   public static void main(String args[]) {   TestLocalReference c = new TestLocalReference();   //call the jni native method   c.nativeMethod(1000000);   }    static {   //load the jni library   System.loadLibrary("StaticMethodCall");   }   }   JNI 代码,nativeMethod(int i) 的 C 语言实现  #include<stdio.h>   #include<jni.h>   #include"TestLocalReference.h"  JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod   (JNIEnv * env, jobject obj, jint count)   {   jint i = 0;   jstring str;   for(; i<count; i++)   str = (*env)->NewStringUTF(env, "0");   }   
运行结果  JVMCI161: FATAL ERROR in native method: Out of memory when expanding   local ref table beyond capacity   at TestLocalReference.nativeMethod(Native Method)   at TestLocalReference.main(TestLocalReference.java:9)  

泄漏实例2:建立一个 String 对象,返回给调用函数。

JNI 代码,nativeMethod(int i) 的 C 语言实现  #include<stdio.h>   #include<jni.h>   #include"TestLocalReference.h"  jstring CreateStringUTF(JNIEnv * env)   {   return (*env)->NewStringUTF(env, "0");   }   JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod   (JNIEnv * env, jobject obj, jint count)   {   jint i = 0;   for(; i<count; i++)   {   str = CreateStringUTF(env);   }   }   
运行结果  JVMCI161: FATAL ERROR in native method: Out of memory when expanding local ref   table beyond  capacity   at TestLocalReference.nativeMethod(Native Method)   at TestLocalReference.main(TestLocalReference.java:9)  

编译问题:SLES/OpenSLES.h: No such file or directory
解决方法:ndk-build TARGET_PLATFORM=android-9

编译断点问题:有没有好用的断点工具

解决方法:visualGDB 神器

 

 

 

Windows 下 JNI 的使用教程

 

参考:IntelliJ idea 2018 平台下JNI编程调用 C++ 算法(一):https://www.cnblogs.com/lucychen/p/9771236.html

JNI 的使用大致有以下4个步骤:

  • 一、在 Java 中写 native 方法
  • 二、用 javah 命令生成 C/C++ 头文件。( 注意:windows 系统生成的动态链接库是 .dll 文件,Linux 是 .so 文件。JDK10 中将 javah 工具取消了,需要使用 javac -h 替代,这是与 jdk8 不同的地方。 )
  • 三、写对应的 C/C++ 程序,实现头文件中声明的方法,并编译成库文件
  • 四、在 Java 中加载这个库文件并使用

注意:Windows 平台需要注意操作系统位数,32 位 dll 无法在 64位 上被调用。

 

 

一、在 Java 中写 native 方法

 

主要步骤

  1. 创建一个 java 项目,在其中编写一个带有 native 方法的类
  2. 利用 idea 生成 .h 头文件。  
  3. 在 vs 中创建一个动态链接库应用程序的解决方案
  4. 在解决方案中创建 C++ 文件,实现头文件中的方法
  5. 生成 动态 链接库
  6. 回到 idea,运行 java 项目,排错重复以上步骤直到运行成功

 

1. 在 idea 创建 java 项目

实现一个简单的 testHello_1() 函数 和 静态的 testHell0_2() 函数,在 C++ 中实现  testHello_1() 和 testHell0_2()。

注意:java 代码都不要放到默认包下(就是不写 package 语句就会放到默认包),默认包下的方法在其他地方都不能调用!!

步骤如下:

  1. 在 idea 创建 java 项目(例如:jni_demo),在 src 目录下新建一个 package(示例 包名  com.jni.test )。
  2. 在包下创建一个类,用来编写 native 方法和 main 函数。示例 类名 JNIDemo
  3. 声明  native 方法,native 方法就是声明一个非 java 实现的方法,比如用 C/C++ 实现。本地方法可以是静态的,也可以不声明为静态的。

图示:

示例代码:

package com.jni.test;public class JNIDemo {public native void testHello_1();public static native int testHello_2();public static void main(String[] args) {try {// System.loadLibrary("JNIPROJECT.dll");System.load("D:\\jni_demo\\src\\com\\jni\\test\\JNIPROJECT.dll");JNIDemo jniDemo =new JNIDemo();jniDemo.testHello_1();int retVal = testHello_2();System.out.println("retVal : " + retVal);}catch (Exception ex) {ex.printStackTrace();}}
}

其中 testHello_1 是一个类方法,testHello_2 是一个静态方法,前面都有 native 代表是一个本地函数。

main 函数中,调用 testHello_1 函数 和 testHello_2  函数。下面的 static 代码块暂且不谈。

代码写好后,build 一下项目,生成 class文件,build 后,可在左侧目录看到 out/production 目录下生成了对应 class 文件。

 

注:

load 和 loadLibrary 区别

  1. 它们都可以用来装载库文件,不论是 JNI 库文件还是非 JNI 库文件。在任何本地方法被调用之前必须先用这个两个方法之一把相应的 JNI 库文件装载。
  2. System.load 参数为库文件的绝对路径,可以是任意路径。例如,你可以这样载入一个 windows 平台下 JNI 库 文件:System.load("C:\\Documents and Settings\\TestJNI.dll");
  3. System.loadLibrary 参数为库文件名,不包含库文件的扩展名。例如,你可以这样载入一个 windows 平台下 JNI 库 文件System. loadLibrary ("TestJNI"); 这里,TestJNI.dll 必须是在 java.library.path 这一 jvm 变量所指向的路径中。
    可以通过如下方法来获得该变量的值:System.getProperty("java.library.path");
     默认情况下,在 Windows 平台下,该值包含如下位置:
            1)和 jre 相关的一些目录
            2)程序当前目录
            3)Windows 目录
            4)系统目录(system32)
            5)系统环境变量 path 指定目录。

classpath 与 java.library.path 区别

classpath 路径下,只能是 jar 或者 class 文件,否者会报错,因为他们会被 load 到 JVM 

build ---> build project,

 

 

2.生成 头文件

 

静态注册动态注册

为什么需要注册?其实就是给 Java 的 native 函数找到底层 C/C++ 实现的函数指针。

  • 静态注册:通过包名、类名一致来确认,Java 有一个命令 javah,专门生成某一个 JAVA 文件所有的 native 函数的头文件(h文件), 静态方法注册 JNI 有哪些缺点?1:必须遵循某些规则。 2:名字过长。 3:多个 class 需 Javah 多遍。 4:运行时去找效率不高
  • 动态注册 :在 JNI 层实现的,JAVA 层不需要关心,因为在 system.load 时就会去掉 JNI_OnLoad,有就注册,没就不注册。
  • 区别:静态注册是用到时加载,动态注册一开始就加载好了,这个可以从 DVM 的源代码看出来。

 

生成 JNI 头文件。(此处有两种方法:2.1手动输入 javah 命令生成头文件、2.2 一键生成头文件)

 

2.1 手动输入 javah 命令生成头文件

打开 cmd,进入 src 目录,运行 javah 命令,生成 C/C++ 头文件,注意:要带上 java 包名

命令格式:javah -classpath 要加载的类的路径 -jni 包名.类名

执行完命令之后,会在 src  目录生成一个 .h 文件:

在 IntelliJ IDEA 图示:

头文件完整代码:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_jni_test_JNIDemo */#ifndef _Included_com_jni_test_JNIDemo
#define _Included_com_jni_test_JNIDemo
#ifdef __cplusplus
extern "C" {
#endif
/** Class:     com_jni_test_JNIDemo* Method:    testHello_1* Signature: ()V*/
JNIEXPORT void JNICALL Java_com_jni_test_JNIDemo_testHello_11(JNIEnv *, jobject);/** Class:     com_jni_test_JNIDemo* Method:    testHello_2* Signature: ()I*/
JNIEXPORT jint JNICALL Java_com_jni_test_JNIDemo_testHello_12(JNIEnv *, jclass);#ifdef __cplusplus
}
#endif
#endif

头文件 说明:

  1. 包含了 jni.h 头文件。
  2. 在类中 声明 的常量(static final)类型会在头文件中以宏的形式出现,这一点还是很方便的。

  3. 函数的注释还是比较全的,包括了:
    1. 对应的 class
    2. 对应的 java 方法名
    3. 对应 java 方法 的 签名
  4. 方法的声明显得有点奇怪,由以下及部分组成:
    1. JNIEXPORT 这是函数的导出方式
    2. jint 返回值类型( jint 由 jni.h定义,对应 int
    3. JNICALL 函数的调用方式也就是汇编级别参数的传入方式
    4.  Java_com_jni_test_JNIDemo_testHello_11  超级长的函数名!!!
      格式是 :Java_ + 类全名 + _ + JAVA中声明的 native 方法名。其中会把包名中的点(.)替换成下划线(_),同时为了避免冲突把 下划线 替换成 _1
    5. 方法的参数,上面的这个方法在 JAVA 的声明中实际上是没有参数的,其中的 JNIENV 顾名思义是 JNI 环境,和具体的线程绑定。而第二个参数 jclass 其实是 java 中的 Class 因为上面是一个 static 方法,因此第二个参数是jclass。如果是一个实例方法则对应第二个参数是 jobject,相当于 java 中的 this 

 

2.2 一键生成头文件

头文件可以使用命令行生成(见参考文献),或者熟悉格式后自己手写。但是如果希望能够随便点一下就生成头文件,于是,找到了一种 用idea工具生成头文件的方法,那就是 External Tools。External Tools 其实就是将手动输入的命令存下来,本质也是运行 javah,后面跟着配置参数,这些参数存在 External Tools,避免每次手动输入。

  • 添加 External Tools。File -> Settings -> Tools -> ExternalTools,点击添加
  • 编辑 Tools
              Name: Generate Header File   
              Program: $JDKPath$/bin/javah 
              Arguments: -jni -classpath $OutputPath$ -d ./jni $FileClass$
              Working directory: $ProjectFileDir$

        Name:External Tools 的名称,喜欢什么起什么,只要自己明白
        Program是javah工具所在地址,即jdk所在路径下的bin,该参数是指tool采用的运行工具是javah
        Arguments设置的是javah的参数,具体可在命令行中查看javah的帮助,查看每个函数含义
        Working directory:项目名称

  • 生成头文件
    保存工具后,右击需要生成头文件的类,即我们的SimpleHello,选择External Tool,点击我们刚刚创建的tool。
    然后你就会发现我们的目录中多了一个jni文件夹,jni文件夹里面有一个名字长长的.h文件,成功!

提示:该方法适用于 jdk8,jdk10 中取消了 javah,的使用 javac -h。

 

jni.h 是什么 ?

  • jni.h 头文件一般位于 $JAVA_HOME/jd{jdk-version}/include 目录内下面的一个文件,jni.h 里面存储了大量的函数和对象,这是 JNI 中所有的 类型、函数、宏 等定义的地方。C/C++ 世界的 JNI 规则就是由他制定的。它有个很好的方法就是通过 native 接口名来获取 C/C++ 函数。
  • 另外还有个 %JAVA_HOME%\bin\include\win32 下的 jni_md.h 

打个比方类似如下:public static String getCMethod(String javaMethodName);

它可以根据你的 java接口,找到 C函数并调用。但这就意味着你不能在 C 里随意写函数名,因为如果你写的 java 方法叫 native aaa(); C函数也叫 aaa(); 但 jni.h 通过 getCMethod(String javaMethodName) 去找的结果是 xxx(); 那这样就无法调用了。

既然不能随意写,怎么办?

没事,jdk 提供了一个通过 java 方法生成 C/C++ 函数接口名的工具 javah。

 

javah 是什么?

javah 就是提供具有 native method 的 java 对象的 C/C++ 函数接口。javah  命令可以提供一个 C/C++ 函数的接口。

 

然后就是在 C/C++ 中实现这个方法就可以了。

但是在动手前现大致了解以下 jni.h 制定的游戏规则。javah 生成的头文件里面使用的类型都是 jni.h 定义的,目的是做到 平台无关,比如保证在所有平台上 jint 都是 32位 的有符号整型。

基本对应关系如下:

jni 类型JAVA 类型对应 本地类型类型签名
jbooleanbooleanuint8_tZ
jbytebytecharB
jcahrcharuint16_tC
jshortshortint16_tS
jintintint32_tI
jlonglongint64_tJ
jfloatfloatfloatF
jdoubledoubledoubleD
voidvoidvoidV

引用类型对应关系:

java 类型JNI 类型java 类型JNI 类型
所有的实例引用jobjectjava.lang.Classjclass
java.lang.StringjstringOcject[]jobjectArray
java.lang.Throwablejthrowable基本类型[]jxxxArray

通过表格发现,除了上面定义的 StringClassThrowable,其他的类(除了数组)都是以 jobject 的形式出现的!事实上jstring, jclass 也都是 object 的子类。所以这里还是和 java 层一样,一切皆 jobject。(当然,如果 jni 在 C 语言中编译的话是没有继承的概念的,此时 jstring,jclass 等其实就是 jobject !用了 typedef 转换而已!!)

接下来是 JNIEnv * 这个指针,他提供了 JNI 中的一系列操作的接口函数。

JNI 中操作 jobject

其实也就是在 native 层操作 java 层的实例。 要操作一个实例无疑是:

  1. 获取/设置 (即 get/set )成员变量(field)的值

  2. 调用成员方法(method)

怎么得到 field 和 method?

通过使用 jfieldID jmethodID: 在 JNI 中使用类似于放射的方式来进行 field 和 method 的操作。JNI 中使用 jfieldID 和jmethodID 来表示成员变量和成员方法,获取方式是:

jfieldID GetFieldID(jclass clazz, const char *name, const char *sig);
jfieldID GetStaticFieldID(jclass clazz, const char *name, const char *sig);
jmethodID GetMethodID(jclass clazz, const char *name, const char *sig);
jmethodID GetStaticMethodID(jclass clazz, const char *name, const char *sig) ;

其中最后一个参数是签名。 获取 jclass 的方法 除了实用上面静态方法的第二个参数外,还可以手动获取。 jclass FindClass(const char *name) 需要注意的是 name 参数,他是一个类包括包名的全称,但是需要把包名中的点.替换成斜杠/

有了 jfieldID 和 jmethodID 就知道狗蛋住哪了,现在去狗蛋家找他玩 ♪(^∇^*)

 

成员变量:

  1. get:

    1. <type> Get<type>Field(jobject , jfieldID);即可获得对应的field,其中field的类型是type,可以是上面类型所叙述的任何一种。

    2. <type> GetStatic<type>Field(jobject , jfieldID);同1,唯一的区别是用来获取静态成员。

  2. set:

    1. void Set<type>Field(jobject obj, jfieldID fieldID, <type> val)

    2. void SetStatic<type>Field(jclass clazz, jfieldID fieldID, <type> value);

成员方法:

调用方法自然要把方法的参数传递进去,JNI中实现了三种参数的传递方式:

  1. Call<type>Method(jobject obj, jmethod jmethodID, ...)其中...是C中的可变长参数,类似于printf那样,可以传递不定长个参数。于是你可以把java方法需要的参数在这里面传递进去。

  2. Call<type>MethodV(jobject obj, jmethodID methodID, va_list args)其中的va_list也是C中可变长参数相关的内容(我不了解,不敢瞎说。。。偷懒粘一下Oracle的文档)Programmers place all arguments to the method in an args argument of type va_list that immediately follows the methodID argument. The CallMethodV routine accepts the arguments, and, in turn, passes them to the Java method that the programmer wishes to invoke.

  3. Call<type>MethodA(jobject obj, jmethodID methodID, const jvalue * args)哎!这个我知道可以说两句LOL~~这里的jvalue通过查代码发现就是JNI中各个数据类型的union,所以可以使用任何类型复制!所以参数的传入方式是通过一个jvalue的数组,数组内的元素可以是任何jni类型。

然后问题又来了:(挖掘机技术到底哪家强?!o(*≧▽≦)ツ┏━┓) 如果传进来的参数和java声明的参数的不一致会怎么样!(即不符合方法签名)这里文档中没用明确解释,但是说道: > Exceptions raised during the execution of the Java method.

typedef union jvalue {jboolean z;jbyte    b;jchar    c;jshort   s;jint     i;jlong    j;jfloat   f;jdouble  d;jobject  l;
} jvalue;
  1. 调用实例方法(instance method):
    1. <type> Call<type>Method(jobject obj, jmethodID methodID, ...);调用一个具有<type>类型返回值的方法。
    2. <type> Call<type>MethodV(jobject obj, jmethodID methodID, va_list args);
    3. Call<type>MethodA(jobject obj, jmethodID methodID, const jvalue * args)
  2. 调用静态方法(static method):
    1. <type> CallStatic<type>Method(jobject obj, jmethodID methodID, ...);
    2. <type> CallStatic<type>MethodV(jobject obj, jmethodID methodID, va_list args);
    3. CallStatic<type>MethodA(jobject obj, jmethodID methodID, const jvalue * args)
  3. 调用父类方法(super.method),这个就有点不一样了。多了一个jclass参数,jclass可以使obj的父类,也可以是obj自己的class,但是methodID必须是从jclass获取到的,这样就可以调用到父类的方法。
    1. <type> CallNonvirtual<type>Method(jobject obj, jclass clazz, jmethodID methodID, ...)
    2. <type> CallNonvirtual<type>MethodV(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args);
    3. <type> CallNonvirtual<type>MethodA(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, const jvalue *args);

#### 数组的操作

数组是一个很常用的数据类型,在但是在JNI中并不能直接操作jni数组(比如jshortArray,jfloatArray)。使用方法是:

  1. 获取数组长度:jsize GetArrayLength(jarray array)
  2. 创建新数组: ArrayType New<PrimitiveType>Array(jsize length);
  3. 通过JNI数组获取一个C/C++数组:<type>* Get<type>ArrayElements(jshortArray array, jboolean *isCopy)
  4. 指定原数组的范围获取一个C/C++数组(该方法只针对于原始数据数组,不包括Object数组):void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize len, NativeType *buf);
  5. 设置数组元素:void Set<type>ArrayRegion(jshortArray array, jsize start, jsize len,const <type> *buf)。again,如果是Object数组需要使用:void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);
  6. 使用完之后,释放数组:void Release<type>ArrayElements(jshortArray array, jshort *elems, jint mode)

有点要说明的:

  1. 面的 3中的 isCopy:当你调用getArrayElements时JVM(Runtime)可以直接返回数组的原始指针,或者是copy一份,返回给你,这是由JVM决定的。所以isCopy就是用来记录这个的。他的值是JNI_TURE或者JNI_FALSE

  2. 6释放数组。一定要释放你所获得数组。其中有一个mode参数,其有三个可选值,分别表示:

  • 0

    • 原始数组:允许原数组被垃圾回收。

    • copy: 数据会从get返回的buffer copy回去,同时buffer也会被释放。

  • JNI_COMMIT

    • 原始数组:什么也不做

    • copy: 数据会从get返回的buffer copy回去,同时buffer不会被释放。

  • JNI_ABORT

    • 原始数组:允许原数组被垃圾回收。之前由JNI_COMMIT提交的对数组的修改将得以保留。

    • copy: buffer会被释放,同时buffer中的修改将不会copy回数组!

####关于引用与垃圾回收 比如上面有个方法传了一个jobject进来,然后我把她保存下来,方便以后使用。这样做是不行哒!因为他是一个LocalReference,所以不能保证jobject指向的真正的实例不被回收。也就是说有可能你用的时候那个指针已经是个野指针的。然后你的程序就直接Segment Fault了,呵呵。。。

在 JNI 中提供了三种类型的引用:

  1. Local Reference:即本地引用。在JNI层的函数,所有非全局引用对象都是Local Reference, 它包括函数调用是传入的jobject和JNI成函数创建的jobject。Local Reference的特点是一旦JNI层的函数返回,这些jobject就可能被垃圾回收。
  2. Glocal Reference:全局引用,这些对象不会主动释放,永远不会被垃圾回收。
  3. Weak Glocal Reference:弱全局引用,一种特殊的Global Reference,在运行过程中有可能被垃圾回收。所以使用之前需要使用jboolean IsSameObject(jobject obj1, jobject obj2)判断它是否已被回收。

Glocal Reference:
1. 创建:jobject NewGlobalRef(jobject lobj);
2. 释放:void DeleteGlobalRef(jobject gref);

Local Reference:
LocalReference也有一个释放的函数:void DeleteLocalRef(jobject obj),他会立即释放Local Reference。 这个方法可能略显多余,其实也是有它的用处的。刚才说Local Reference会再函数返回后释放掉,但是假如函数返回前就有很多引用占了很多内存,最好函数内就尽早释放不必要的内存。

####关于JNI_OnLoad 开头提到JNI_OnLoad是java1.2中新增加的方法,对应的还有一个JNI_OnUnload,分别是动态库被JVM加载、卸载的时候调用的函数。有点类似于WIndows里的DllMain。
前面提到的实现对应native的方法是实现javah生成的头文件中定义的方法,这样有几个弊端:

  1. 函数名太长。很长。。相当长。。。
  2. 函数会被导出,也就谁说可以在动态库的导出函数表里面找到这些函数。这将有利于别人对动态库的逆向工程,因此带来安全问题。

现在有了JNI_OnLoad,情况好多了。你不光能在其中完成动态注册native函数的工作还可以完成一些初始化工作。java对应的有了jint RegisterNatives(jclass clazz, const JNINativeMethod *methods,jint nMethods)函数。参数分别是:

  1. jclass clazz,于native层对应的java class

  2. const JNINativeMethod *methods这是一个数组,数组的元素是JNI定义的一个结构体JNINativeMethod

  3. 上面的数组的长度

JNINativeMethod:代码中的定义如下:

/** used in RegisterNatives to describe native method name, signature,* and function pointer.*/typedef struct {char *name;char *signature;void *fnPtr;
} JNINativeMethod;

所以他有三个字段,分别是

字段含义
char *namejava class中的native方法名,只需要方法名即可
char *signature方法签名
void *fnPtr对应native方法的函数指针

于是现在你可以不用导出native函数了,而且可以随意给函数命名,唯一要保证的是参数及返回值的统一。然后需要一个const JNINativeMethod *methods数组来完成映射工作。

看起来大概是这样的:

//只需导出JNI_OnLoad和JNI_OnUnload(这个函数不实现也行)
/*** These are the exported function in this library.
*/
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved);
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved);//为了在动态库中不用导出函数,全部声明为static
//native methods registered by JNI_OnLoad
static jint native_newInstance (JNIEnv *env, jclass);//实现native方法
/*
* Class:     com_young_soundtouch_SoundTouch
* Method:    native_newInstance
* Signature: ()I
*/
static jint native_newInstance
(JNIEnv *env, jclass ) {int instanceID = ++sInstanceIdentifer;SoundTouchWrapper *instance = new SoundTouchWrapper();if (instance != NULL) {sInstancePool[instanceID] = instance;++sInstanceCount;}LOGDBG("create new SouncTouch instance:%d", instanceID);return instanceID;
}//构造JNINativeMethod数组
static JNINativeMethod gsNativeMethods[] = {{"native_newInstance","()I",reinterpret_cast<void *> (native_newInstance)}
};
//计算数组大小
static const int gsMethodCount = sizeof(gsNativeMethods) / sizeof(JNINativeMethod);//JNI_OnLoad,注册native方法。
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {JNIEnv* env;jclass clazz;LOGD("JNI_OnLoad called");if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {return -1;}//FULL_CLASS_NAME是个宏定义,定义了对应java类的全名(要把包名中的点(.)_替换成斜杠(/))clazz = env->FindClass(FULL_CLASS_NAME);LOGDBG("register method, method count:%d", gsMethodCount);//注册JNI函数env->RegisterNatives(clazz, gsNativeMethods,gsMethodCount);//必须返回一个JNI_VERSION_1_1以上(不含)的版本号,否则直接加载失败return JNI_VERSION_1_6;
}

###实战技巧篇

这里主要是巧用C中的宏来减少重复工作:

####迅速生成全名

//修改包名时只需要改以下的宏定义即可
#define FULL_CLASS_NAME "com/young/soundtouch/SoundTouch"
#define func(name) Java_ ## com_young_soundtouch_SoundTouch_ ## name
#define constance(cons) com_young_soundtouch_SoundTouch_ ## cons

比如func(native_1newInstance)展开成:Java_com_young_soundtouch_SoundTouch_native_1newInstance即JNI中需要导出的函数名(不过用动态注册方式没太大用了)

constance(AUDIO_FORMAT_PCM16)展开成com_young_soundtouch_SoundTouch_AUDIO_FORMAT_PCM16这个着实有用。

而且如果包名改了也可以很方便的适应之。

###安卓的log

//define __USE_ANDROID_LOG__ in makefile to enable android log
#if defined(__ANDROID__) && defined(__USE_ANDROID_LOG__)
#include <android/log.h>
#define LOGV(...)   __android_log_print((int)ANDROID_LOG_VERBOSE, "ST_jni", __VA_ARGS__)
#define LOGD(msg)  __android_log_print((int)ANDROID_LOG_DEBUG, "ST_jni_dbg", "line:%3d %s", __LINE__, msg)
#define LOGDBG(fmt, ...) __android_log_print((int)ANDROID_LOG_DEBUG, "ST_jni_dbg", "line:%3d " fmt, __LINE__, __VA_ARGS__)
#else
#define LOGV(...) 
#define LOGD(fmt) 
#define LOGDBG(fmt, ...) 
#endif

通过这样的宏定义在打LOGD或者LOGDBG的时候还能自动加上行号!调试起来爽多了!

####C++中清理内存的方式

由于C++里面需要手动清楚内存,因此我的解决方案是定义一个map,给每个实例一个id,用id把java中的对象和native中的对象绑定起来。在java层定义一个release方法,用来释放本地的对象。 本地的 KEY-对象 映射 static std::map<int, SoundTouchWrapper*> sInstancePool;

####关于NDK 因为安卓的约定是把本地代码放到jni目录下面,但是假如有多个jni lib的时候会比较混乱,所以方案是每一个lib都在jni里面建一个子目录,然后jni里面的Android.mk就可以去构建子目录中的lib了。

jni/Android.mk如下(超级简单):

LOCAL_PATH := $(call my-dir)
include $(call all-subdir-makefiles)

然后在子目录soundtouch_module中的Android.mk就可以像一般的Android.mk一样书写规则了。

同时记录一下在Andoroid.mk中使用makefile内建函数wildcard的方法。 有时候源文件是一个目录下的所有.cpp/.c文件,这时候wildcard来统配会很方便。但是Android.mk与普通的Makefile的不同在于:

  1. 调用Android.mkmingling的${CWD}并不是Android.ml所在的目录。所以Android.mk中有一个变量LOCAL_PATH := $(call my-dir)来记录当前 Android.mk所在的目录。
  2. 同时还会把所有的LOCAL_SRC_FILES 前面加上$(LOCAL_PATH)这样写makefile的时候就可以用相对路径了,提供了方便。但是这也导致了坑!

因为1,直接使用相对路径会导致wildcard匹配不到源文件。所以最好这么写FILE_LIST := $(wildcard $(LOCAL_PATH)/soundtouch_source/source/SoundTouch/*.cpp)。然而又因为2,这样还是不行的。所以还需要匹配之后把$(LOCAL_PATH)的部分去掉,因此还得这样$(FILE_LIST:$(LOCAL_PATH)/%=%).

还有个小tip:LOCAL_CFLAGS中最好加上这个定义-fvisibility=hidden这样就不会在动态库中导出不必要的函数了。

###附录签名

JAVA中的函数签名包括了函数的参数类型,返回值类型。因此即使是重载了的函数,其函数签名也不一样。java编译器就会根据函数签名来判断你调用的到地址哪个方法。 签名中表示类型是这样的

1.基本类型都对应一个大写字母,如下:

JAVA类型类型签名
booleanZ
byteB
charC
shortS
intI
longJ
floatF
doubleD
voidV

2.如果是类则是: L + 类全名(报名中的点(.)用(/)代替)+ ; 比如java.lang.String 对应的是 Ljava/lang/String;

3.如果是数组,则在前面加[然后加类型签名,几位数组就加几个[ 比如int[]对应[I,boolean[][] 对应 [[Z,java.lang.Class[]对应[Ljava/lang/Class;

可以通过javap命令来获取签名(javah生成的头文件注释中也有签名):javap -x -p <类全名> 坑爹的是java中并不能通过反射来获取方法签名,需要自己写一个帮助类。 (其实我还写了个小程序可以自动生成签名,和JNI_OnLoad中注册要用到的JNINativeMethod数组,从此再也不用糟心的去写那该死的数组了。LOL~~~)

 

 

 

3. 在 VS 中创建解决方案

接下来打开 Visual studio 2019,新建动态链接库: JniProject

填写 项目名,项目所在目录:

创建完成后再添加类:

 

设置项目包含目录

  • 本来我是按照这篇文章复制jni.h等文件的,但是一直报错“找不到 源 文件 jni.h”。搞来搞去总是不成,后来才发现,我在vs2017直接复制,jni.h并没有到C++项目目录下,而是仍然在原来的目录里,这与java的ide很不同啊。虽然被这个问题搞到差点摔桌子,但我转念一想,在原来的目录下就还不错啊,省得我复制来复制去。于是刷刷刷设置了包含路径

  • 点击项目,我的项目叫 jniCppDemo,在菜单栏选择:项目 -> 属性 -> 配置属性 -> VC++目录 -> 包含目录
  • 设置包含路径:          
              设置 jni.h 所在路径: C:\Program Files\Java\jdk1.8.0_181\include
              设置 jni_md.h 所在路径: C:\Program Files\Java\jdk1.8.0_181\include\win32
              设置刚刚生成头文件所在路径: D:\javaWorkspace\jniJavaDemo\jni

 

如果不想设置 包含目录,可以直接把文件( jni.h、com_jni_test_JNIDemo.h、jni_md.h )复制到工程目录下.

JDK 安装目录的 include 目录下有一个 jni.h 的文件,include 的 win32 目录下有个 jni_md.h 文件,还有 java 工程的 src 目录下的C 头文件,一起拷贝到 C工程的 JniProject 目录下:( JniProject ---> jni.h   com_jni_test_JNIdemo.h    jni_md.h )如下图:

在 C项目的头文件文件夹上面:右键 --- > 添加 ---> 现有项

选择 jni.h、com_jni_test_JNIDemo.h、jni_md.h

添加完可以在 头文件 目录中看到

打开 com_jni_test_JNIDemo.h 文件

#include <jni.h> 修改为 #include "jni.h" 错误提示消失。

 

 

4. 编写 cpp 文件

然后在 TestJNI.cpp 文件中写入如下代码:

#include "pch.h"
#include "TestJNI.h"
#include "com_jni_test_JNIDemo.h"JNIEXPORT void JNICALL Java_com_jni_test_JNIDemo_testHello_11
(JNIEnv*, jobject) {printf("this is C++ print : Java_com_jni_test_JNIDemo_testHello_11\n");
}JNIEXPORT jint JNICALL Java_com_jni_test_JNIDemo_testHello_12
(JNIEnv*, jclass) {printf("this is C++ print : Java_com_jni_test_JNIDemo_testHello_12\n");return 100;
}

 

 

5. 生成 dll 文件

使用 C/C++ 实现本地方法生成动态库文件(windows下扩展名为 DDL,linux 下扩展名为 so):

写好了 cpp,就可以生成 dll。右击项目生成/重新生成,就生成了 dll 文件。从控制台输出可看到 dll 的地址

注意:设置为 64位

保存,运行,编译生成 DLL 文件,在工程项目的 release 目录中可以找到。

 

 

6. 运行 Java

示例代码 1:

package com.jni.test;public class JNIDemo {public native void testHello_1();public static native int testHello_2();public static void main(String[] args) {try {// System.loadLibrary("JNIPROJECT.dll");System.load("D:\\jni_demo\\src\\com\\jni\\test\\JNIPROJECT.dll");JNIDemo jniDemo =new JNIDemo();jniDemo.testHello_1();int retVal = testHello_2();System.out.println("retVal : " + retVal);}catch (Exception ex) {ex.printStackTrace();}}
}

运行截图:

示例代码 2:

package com.jni.test;public class JNIDemo {public native void testHello_1();public static native int testHello_2();static {// System.loadLibrary("JNIPROJECT.dll");System.load("D:\\jni_demo\\src\\com\\jni\\test\\JNIPROJECT.dll");}public static void main(String[] args) {try {JNIDemo jniDemo =new JNIDemo();jniDemo.testHello_1();int retVal = testHello_2();System.out.println("retVal : " + retVal);}catch (Exception ex) {ex.printStackTrace();}}
}

运行截图:

 

注意:

  • 1、一般在 static 代码块中加载动态链接库
  • 2、如果将 DLL 路径加入 PATH 环境变量的时候,eclipse是开着的,那么要关闭 eclipse 再开,让 eclipse 重新读取环境变量
  • 3、必须在本类中使用native方法

 

 

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/495415.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Nature:科学家成功绘制出大脑神经细胞“地图”

图片来源&#xff1a;Thomas Hainmller, Marlene Bartos来源&#xff1a;生物谷摘要&#xff1a;最近&#xff0c;一项刊登在国际杂志Nature上的研究报告中&#xff0c;来自弗莱堡大学的科学家们通过研究开发出了一种新型模型来解释大脑如何储存一些“有形事件”&#xff08;ta…

互联网的大脑模型与原子的太阳系模型,科学史上的巨系统对比

作者&#xff1a;刘锋 计算机博士&#xff0c;互联网进化论作者科学探索中&#xff0c;有两种重要的促进力量&#xff0c;第一种是认同&#xff0c;会帮助研究者增强对探索方向的信心和勇气&#xff0c;第二种是批判&#xff0c;会帮助研究者获知探索路上的障碍和陷阱。10年前…

安卓逆向_15( 二 ) --- Android Studio 3.6.3 JNI 环境配置 和 so 生成开发 demo

From&#xff1a;Android Studio 3.0 JNI 的实现&#xff1a;https://blog.csdn.net/ziyoutiankoong/article/details/79696279 Android Studio 生成so包和.H文件给jni调用产生新so包。(即so包调so包)&#xff1a;https://blog.csdn.net/sxh_android/article/details/80694291…

卡内基梅隆大学机器学习系副主任邢波:AI落地现在最缺的是思维方式

来源&#xff1a;亿欧摘要&#xff1a;邢波认为&#xff1a;人工智能现在最缺的不是算法和知识&#xff0c;而是落地应用的思维方式&#xff1b;数据如何被处理、系统如何被调试、资源如何配置&#xff0c;目前阶段还处于黑箱&#xff0c;很混沌的状态&#xff1b;人工智能未来…

安卓逆向_15( 三 ) --- Android NDK 开发【 jni 静态注册、JNI_OnLoad 动态注册】

Android Studio开发JNI示例&#xff1a;https://blog.csdn.net/wzhseu/article/details/79683045 JNI_动态注册_静态注册.zip : https://pan.baidu.com/s/1wpTYA9euSdPqE1Z2bA_BHA 提取码: 7h97 错误: 编码GBK的不可映射字符 ( https://blog.csdn.net/talenter111/article/de…

学界 | DeepMind等机构提出「图网络」:面向关系推理

来源&#xff1a;机器之心摘要&#xff1a;近日&#xff0c;由 DeepMind、谷歌大脑、MIT 和爱丁堡大学等公司和机构的 27 位科学家共同提交的论文《Relational inductive biases, deep learning, and graph networks》引起了人们的关注。深度学习虽然精于分类&#xff0c;但一直…

ARM 汇编基础教程番外篇 ——配置实验环境

From&#xff1a;https://zhuanlan.zhihu.com/p/29145513 win10 arm 汇编环境 Windows 平台下搭建 ARM 汇编集成环境&#xff1a;https://jingyan.baidu.com/article/4b52d70288bfcdfc5c774ba5.html 要调试 ARM 程序&#xff0c;我们需要&#xff1a; 能运行 ARM 程序的运行环…

asp.net调试方法

1、先将网站设为启动项目。 2、选择“启动选项”。 3、进行设置&#xff1a; 然后调试&#xff0c;在浏览器输入网址&#xff0c;此时如果遇到“断点”程序将自动停止运行&#xff0c;即可进行调试&#xff0c;查看运行中的变量的值。 转载于:https://www.cnblogs.com/gwjtssy/…

基因对智力的预测能力不到7%,别迷信它

图片来源&#xff1a;The Conversation撰文 Carl Zimmer翻译 李杨审校 贾晓璇编辑 魏潇2016 年我在写一本关于遗传的书时&#xff0c;曾对自己的基因组进行了测序。一些科学家还好心地指出了我基因组图谱的一些有趣特征&#xff0c;教我如何自己读取数据。从那以后&#xff0c;…

ARM 汇编语言入门

[翻译]二进制漏洞利用&#xff08;二&#xff09;ARM32位汇编下的TCP Bind shell&#xff1a;https://bbs.pediy.com/thread-253511.htm ARM汇编语言入门 From&#xff1a;ARM汇编语言入门&#xff08;一&#xff09;&#xff1a;https://zhuanlan.zhihu.com/p/109057983 原文…

图灵奖演讲2018,59页PPT迎接芯片体系结构的新黄金时代

来源&#xff1a; 计算所控制计算实验室6月4日&#xff0c;在今年的国际计算机体系结构大会ISCA2018的图灵奖演讲会上&#xff0c;ACM/IEEE邀请了2017年图灵奖获得者John Hennessy与David Patterson联合进行了一场关于未来计算机体系结构发展道路探索的精彩演讲。二位图灵奖得主…

ARM 指令集 和 Thumb 指令集

From&#xff1a;https://gitee.com/lsliushuang/ASM/blob/master/arm汇编.txt ARM 汇编指令集汇总&#xff1a;https://blog.csdn.net/qq_40531974/article/details/83897559 ARM 指令集详解(超详细&#xff01;带实例!&#xff09;&#xff1a;https://blog.csdn.net/micke…

花旗银行将因人工智能裁员50%,失业风波究竟要持续多久

来源&#xff1a;ofweek摘要&#xff1a; 在这个人工智能随时可能会取代人类工作的年代里&#xff0c;人们对自己的工作的未来应该去了解和重视&#xff0c;尤其是数字类别的行业&#xff0c;像是会计、税务、收费站是最早被人工智能替代的一批。在这个人工智能随时可能会取代人…

Python 程序的抽样分析器 - Py-Spy

From&#xff1a;https://python.freelycode.com/contribution/detail/1320 GitHub 地址&#xff1a;https://github.com/benfred/py-spy Py-Spy 是 Python 程序的抽样分析器。 它允许您可视化 Python 程序正花费时间在哪部分&#xff0c;而无需重新启动程序或以任何方式修改代…

微观世界探索者:15家值得关注的纳米技术公司

来源&#xff1a;资本实验室摘要&#xff1a;纳米技术就像微观世界的魔术&#xff0c;让人类得以前所未有地深入到物质的分子与原子层面&#xff0c;探索生物、化学、物理等各领域的融合。资本实验室今日投资关注聚焦前沿科技创新与传统产业升级纳米技术就像微观世界的魔术&…

DeepMind新论文:给侧面照片,AI给你脑补出正面

来源&#xff1a;澎湃新闻摘要&#xff1a;大家在学生时代可能都面对过这些涉及空间想象的几何题。从根本上&#xff0c;它们考验的是2D图像和3D场景间的转换能力。如今&#xff0c;人工智能也成功打破了这种“次元壁”。用小立方体搭一个几何体&#xff0c;使它的主视图和俯视…

安卓逆向 和 手游辅助 学习 路线

From&#xff1a;https://zhuanlan.zhihu.com/p/95915254 知乎&#xff1a;Android 逆向分析学习路线&#xff1f;&#xff1a;https://www.zhihu.com/question/25626303 入门篇 --- 学习Android安全和逆向开发的路线总结&#xff1a;https://www.52pojie.cn/thread-1065039-…

可交互的对抗网络如何增强人类创造力?

编译&#xff1a;集智翻译组来源&#xff1a;distill.pub作者&#xff1a;Shan Carter&#xff0c;Michael Nielsen原题&#xff1a;Using Artificial Intelligence to Augment Human Intelligence摘要&#xff1a;计算机不仅可以是解决数学问题的工具&#xff0c;还可以是拥有…

ARM 寄存器 详解

From&#xff08; ARM 寄存器详解 &#xff09;&#xff1a;https://blog.csdn.net/sandeldeng/article/details/52954781 ARM 汇编基础教程&#xff1a;2.数据类型和寄存器&#xff1a;https://www.52pojie.cn/thread-797306-1-1.html ARM 的 寄存器 ARM 工作状态 和 工作模式…

让AI个性化而且功耗更低 IBM研发新型神经网络芯片

选自&#xff1a;Singularity Hub编译&#xff1a;网易智能参与&#xff1a;李擎摘要&#xff1a;在GPU上运行的神经网络已经在人工智能领域取得了一些惊人的进步&#xff0c;但这两者的合作还并不完美。IBM的研究人员希望能设计出一种专门用于运行神经网络的新的芯片&#xff…