zoukankan      html  css  js  c++  java
  • JNI笔记

    这篇笔记是我半年前写的,当时因为某些原因经常需要写jni方面的代码,所以就深入学习了下jni方面的知识,现在再来看之前写的东西,一句话概括就是深度不够,废话太多。因为这是一个不全的笔记(还有一部分想写的内容未能写上),所以当初想分享给其他同事的也不好意思分享。

    #-------------Add Now-------------#

    jni是java native interface的简写,是java和c/c++通信的桥梁。

    jni.h是Android提供的jni调用接口头文件,我认为学习jni最好的办法是熟悉它里面的每一个接口、然后看framework中是如何去使用这些接口的,相信官方考虑的总是比较周到。在了解基础知识后,遇到问题就直接查看dalvik或者art里面是哪里打印出来的,从最根本去了解它的错误。

    基础知识:

    1. jni中严格区分c和c++调用方式,在native方法中提供的env是区分c、c++的,这个env对应的struct是不一样的(细微的差别而已)。

    2. jni中严格区分static与非static方法、参数、变量,不管是java调c/c++还是反着调都需要注意,它们对应的jni接口(c/c++调java)和参数(java native方法)是不一样的。

    3. jni的基本数据结构是和java的基本数据结构对应起来的,并不是和c/c++的基本数据结构对应。

    4. 调用的时候注意不要写错名字吧,区分好static和非static、区分好class和object类型,熟悉jni的每一个接口的含义,用时能够找到。

    5. native方法静态注册(Java_PackageName_ClassName_MethodName)时只能识别c类型的方法名,在c++中记得添加extern “C”,动态注册(RegisterNatives)时不做这样的限制,JNINativeMethod的第二个参数signature,如果是class则通过java获取;第三个参数fnPtr的返回值记得强制转换为void *类型。

    比较隐蔽的错误:

    1. 引用类型,引用类型分为local、weak、global。local类型只在函数调用返回之前有效,global相当于全局变量,不主动release会一直存在。

    2. 引用计数,每一种引用类型都有次数限制,这个是由dalvik或者art决定的,不需要的得手动release掉,要不crash信息也不容易看出问题所在。

    3. 在非UI线程下使用env需要AttacbCurrentThread,使用后记得释放。

    4. jclass、jmethod、jfield等需要在UI线程或者JNI_OnLoad中获取。

    调试:

    jni方面也接触一段时间了,真正让我去了解jni是因为在webrtc中存在不少jni的调用,而且需要写一些jni方法,不得不深入了解。jni方面的crash信息不是很容易看出问题所在,不能像其他crash一样给出堆栈信息,所以对于初学者来说比较难找出问题所在。不过如果能把该注意的地方注意了,再仔细一些问题应该不大,毕竟我相信我们平时的项目中真正使用到jni的地方不会太多。实在不行就只能gdb了,官网中给出了Android gdb的使用方法。

    为避免忘记释放,可以参考智能指针写一些实用性的东西。还可以写很多其他的,例如StringcharsScoped、LocalRefScoped等。

    1. // Attach thread to JVM if necessary and detach at scope end if originally

    2. // attached.

    3. // interface. !come from android source!

    4. #include <jni.h>

    5. class AttachThreadScoped {

    6. public:

    7.  explicit AttachThreadScoped(JavaVM* jvm);

    8.  ~AttachThreadScoped();

    9.  JNIEnv* env();

    10. private:

    11.  bool attached_;

    12.  JavaVM* jvm_;

    13.  JNIEnv* env_;

    14. };

    15. // implement

    16. #include <assert.h>

    17. #include <stddef.h>

    18. AttachThreadScoped::AttachThreadScoped(JavaVM* jvm) : attached_(false), jvm_(jvm), env_(NULL) {

    19.  jint ret_val = jvm->GetEnv(reinterpret_cast<void**>(&env_), JNI_VERSION_1_4);

    20.  if (ret_val == JNI_EDETACHED) {

    21.    // Attach the thread to the Java VM.

    22.    ret_val = jvm_->AttachCurrentThread(&env_, NULL);

    23.    attached_ = ret_val == JNI_OK;

    24.    assert(attached_);

    25.  }

    26. }

    27. AttachThreadScoped::~AttachThreadScoped() {

    28.  if (attached_ && (jvm_->DetachCurrentThread() < 0)) {

    29.    assert(false);

    30.  }

    31. }

    32. JNIEnv* AttachThreadScoped::env() { return env_; }

    #-------------End Add-------------#

     

    1. 基本数据类型的定义

    在jni的方法中我们是看不到char、short、int、unsigned int这样的基本数据类型的,都是看到jchar、jshort、jint等这样的基本数据类型。为什么不直接使用char、short等这样的基本数据类型呢?是因为要和Java的基本数据类型对应起来(Java的基本数据有boolean、byte、char、short、int、long、float和double),Java的这些基本数据的值正是jni里面定义的这些jboolean、jchar等。而且这里还考虑到了编译器支不支持C99标准。下面我们直接看看jni.h中是怎么考虑到这两种情况的吧:

    通过宏HAVE_INTTYPES_H来判断当前编译器时候存在inttypes.h这个头文件,如果存在则直接使用C99标准的定义,如果不存在则定义和C99一样的数据类型。

    注意事项:

    1. jboolean是unsigned char类型

    2. jchar是unsinged char类型

    3. NDK中的交叉编译器是支持并使用C99标准的

    1. #ifdef HAVE_INTTYPES_H

    2. # include <inttypes.h>      /* C99 */

    3. typedef uint8_t         jboolean;       /* unsigned 8 bits */

    4. typedef int8_t          jbyte;          /* signed 8 bits */

    5. typedef uint16_t        jchar;          /* unsigned 16 bits */

    6. typedef int16_t         jshort;         /* signed 16 bits */

    7. typedef int32_t         jint;           /* signed 32 bits */

    8. typedef int64_t         jlong;          /* signed 64 bits */

    9. typedef float           jfloat;         /* 32-bit IEEE 754 */

    10. typedef double          jdouble;        /* 64-bit IEEE 754 */

    11. #else

    12. typedef unsigned char   jboolean;       /* unsigned 8 bits */

    13. typedef signed char     jbyte;          /* signed 8 bits */

    14. typedef unsigned short  jchar;          /* unsigned 16 bits */

    15. typedef short           jshort;         /* signed 16 bits */

    16. typedef int             jint;           /* signed 32 bits */

    17. typedef long long       jlong;          /* signed 64 bits */

    18. typedef float           jfloat;         /* 32-bit IEEE 754 */

    19. typedef double          jdouble;        /* 64-bit IEEE 754 */

    20. #endif

    21. /* "cardinal indices and sizes" */

    22. typedef jint            jsize;

    2. 数组类型的定义

    在c语言中数组都可以用void *来表示,当然在c++中也可以用void *来表示,毕竟c++是兼容c的。但在jni.h中c++把字符串和数组与class一样对待,当做一个类来处理,这个可能是要和c区分开来。直接看源码:

    1. #ifdef __cplusplus

    2. /*

    3. * Reference types, in C++

    4. */

    5. class _jobject {};

    6. class _jclass : public _jobject {};

    7. class _jstring : public _jobject {};

    8. class _jarray : public _jobject {};

    9. class _jobjectArray : public _jarray {};

    10. class _jbooleanArray : public _jarray {};

    11. class _jbyteArray : public _jarray {};

    12. class _jcharArray : public _jarray {};

    13. class _jshortArray : public _jarray {};

    14. class _jintArray : public _jarray {};

    15. class _jlongArray : public _jarray {};

    16. class _jfloatArray : public _jarray {};

    17. class _jdoubleArray : public _jarray {};

    18. class _jthrowable : public _jobject {};

    19. typedef _jobject*       jobject;

    20. typedef _jclass*        jclass;

    21. typedef _jstring*       jstring;

    22. typedef _jarray*        jarray;

    23. typedef _jobjectArray*  jobjectArray;

    24. typedef _jbooleanArray* jbooleanArray;

    25. typedef _jbyteArray*    jbyteArray;

    26. typedef _jcharArray*    jcharArray;

    27. typedef _jshortArray*   jshortArray;

    28. typedef _jintArray*     jintArray;

    29. typedef _jlongArray*    jlongArray;

    30. typedef _jfloatArray*   jfloatArray;

    31. typedef _jdoubleArray*  jdoubleArray;

    32. typedef _jthrowable*    jthrowable;

    33. typedef _jobject*       jweak;

    34. #else /* not __cplusplus */

    35. /*

    36. * Reference types, in C.

    37. */

    38. typedef void*           jobject;

    39. typedef jobject         jclass;

    40. typedef jobject         jstring;

    41. typedef jobject         jarray;

    42. typedef jarray          jobjectArray;

    43. typedef jarray          jbooleanArray;

    44. typedef jarray          jbyteArray;

    45. typedef jarray          jcharArray;

    46. typedef jarray          jshortArray;

    47. typedef jarray          jintArray;

    48. typedef jarray          jlongArray;

    49. typedef jarray          jfloatArray;

    50. typedef jarray          jdoubleArray;

    51. typedef jobject         jthrowable;

    52. typedef jobject         jweak;

    53. #endif /* not __cplusplus */

    3. 引用类型

    在jni 1.6中新增了一个方法用于获取对象的引用类型,引用类型可以分为无效引用、本地引用、全局引用和弱全局引用。

    无效引用:此对象不是一个引用类型,一般情况下为空、野指针、一个值的地址或者不是jobject类型等类型,一句话概括就是:不是一个有效的地址

    本地引用:此对象的作用范围仅限于此对象所在的方法,当函数返回后此引用无效

    全局引用:此对象在整个程序中都可以使用,如果不显示删除,则生命周期和JavaVM的生命周期一致

    弱全局引用:在全局引用能用的地方都可以使用弱引用,但是这个引用随时都有可能会被GC回收,用的时候需要判断是否为空,如果被GC回收后此值为空,可以这样判断(env->IsSameObject(weakGlobal, NULL))

    引用计数:

    在jni中你是不能任意的new出无限多个引用的,这些引用都是需要占用资源的,特别是全局引用如果不删除不仅会造成内存泄漏还会发生不可预知的错误,在jni的实现文件中可以知道每一种引用类型的个数都是有限制的,所以在开发过程中需要注重引用类型的删除,以免发生不可预知的错误。

    1. //jni_internal.cc

    2. static const size_t kLocalsInitial = 64;  // Arbitrary.

    3. static const size_t kLocalsMax = 512;  // Arbitrary sanity check.

    4. static size_t gGlobalsInitial = 512;  // Arbitrary.

    5. static size_t gGlobalsMax = 51200;  // Arbitrary sanity check. (Must fit in 16 bits.)

    6. static const size_t kWeakGlobalsInitial = 16;  // Arbitrary.

    7. static const size_t kWeakGlobalsMax = 51200;  // Arbitrary sanity check. (Must fit in 16 bits.)

    jobjectRefType的定义:

    1. typedef enum jobjectRefType {

    2.    JNIInvalidRefType = 0,

    3.    JNILocalRefType = 1,

    4.    JNIGlobalRefType = 2,

    5.    JNIWeakGlobalRefType = 3

    6. } jobjectRefType;

    获取jobjectRefType的方法:

    可以分为c和c++两种方式调用,不过最终c++方法也是调用c方法。

    1. /*C方法*/

    2. /* added in JNI 1.6 */

    3. jobjectRefType (*GetObjectRefType)(JNIEnv*, jobject);

    4. /*C++方法*/

    5. /* added in JNI 1.6 */

    6. jobjectRefType GetObjectRefType(jobject obj)

    7. { return functions->GetObjectRefType(this, obj); }

    下面通过一个例子来认识这些引用:

    这个例子我是拿NDK里面的hello-jni来改写的,通过这个例子可以认识这些引用是如何创建和删除的。

    Java代码:

    1. //修改前

    2. public native String  stringFromJNI();

    3. //修改后

    4. public native String  stringFromJNI(Object context);

    Jni代码:

    1. #include <string.h>

    2. #include <jni.h>

    3. #include <android/log.h>

    4. #define ALOGI(...) __android_log_print(ANDROID_LOG_INFO,  "JNITest", __VA_ARGS__)

    5. #define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, "JNITest", __VA_ARGS__)

    6. extern "C" jstring

    7. Java_com_example_hellojni_HelloJni_stringFromJNI(JNIEnv* env, jobject thiz, jobject context)

    8. {

    9. jobjectRefType type = env->GetObjectRefType(context);

    10. if (JNILocalRefType == type) {

    11. ALOGI("%s:%d JNILocalRefType", __FUNCTION__, __LINE__);

    12. }

    13. type = env->GetObjectRefType(NULL);

    14. if (JNIInvalidRefType == type) {

    15. ALOGI("%s:%d JNIInvalidRefType", __FUNCTION__, __LINE__);

    16. }

    17. jobject localContext = env->NewLocalRef(context);

    18. type = env->GetObjectRefType(localContext);

    19. if (JNILocalRefType == type) {

    20. ALOGI("%s:%d JNILocalRefType", __FUNCTION__, __LINE__);

    21. env->DeleteLocalRef(localContext);

    22. }

    23. jobject globalContext = env->NewGlobalRef(context);

    24. type = env->GetObjectRefType(globalContext);

    25. if (JNIGlobalRefType == type) {

    26. ALOGI("%s:%d JNIGlobalRefType", __FUNCTION__, __LINE__);

    27. env->DeleteGlobalRef(globalContext);

    28. }

    29. jobject weakGlobalContext = env->NewWeakGlobalRef(context);

    30. type = env->GetObjectRefType(weakGlobalContext);

    31. if (JNIWeakGlobalRefType == type) {

    32. ALOGI("%s:%d JNIWeakGlobalRefType", __FUNCTION__, __LINE__);

    33. env->DeleteWeakGlobalRef(weakGlobalContext);

    34. }

    35. return env->NewStringUTF("hello jni");

    36. }

    Android.mk

    修改hello-jni.c为hello-jni.cpp,添加了引用log库,链接方式修改为c++方式,因为习惯写c++代码了

    1. LOCAL_PATH := $(call my-dir)

    2. include $(CLEAR_VARS)

    3. LOCAL_ARM_MODE := arm

    4. LOCAL_LINK_MODE := c++

    5. LOCAL_MODULE    := hello-jni

    6. LOCAL_SRC_FILES := hello-jni.cpp

    7. LOCAL_LDLIBS := -llog

    8. include $(BUILD_SHARED_LIBRARY)

    Application.mk

    因为我仅仅需要arm平台的

    1. APP_ABI := armeabi

    最后的打印为:

    我实际代码的行号和以上我给出的代码不一样,所以打印的行号是我实际代码存在的行号,不过实际输出结果是符合我们所想的。

    1. 12-22 12:34:38.381: I/JNITest(6406): Java_com_example_hellojni_HelloJni_stringFromJNI:36 JNILocalRefType

    2. 12-22 12:34:38.381: I/JNITest(6406): Java_com_example_hellojni_HelloJni_stringFromJNI:41 JNIInvalidRefType

    3. 12-22 12:34:38.381: I/JNITest(6406): Java_com_example_hellojni_HelloJni_stringFromJNI:47 JNILocalRefType

    4. 12-22 12:34:38.381: I/JNITest(6406): Java_com_example_hellojni_HelloJni_stringFromJNI:54 JNIGlobalRefType

    5. 12-22 12:34:38.381: I/JNITest(6406): Java_com_example_hellojni_HelloJni_stringFromJNI:61 JNIWeakGlobalRefType

    4. jni方法

    通过查看Android 4.4.2_r1源码发现jni.h提供出来的这些jni方法最终都是用c++实现的,而且还分为debug版本和release版本。

    4.1 调用方式

    按照调用方式可以分为两类c和c++两种调用方式,这两种方式除了调用方式上没有任何不同的地方,而且c++的调用方式本质上也是调用c方法。jni的这种划分方式是通过后缀名来划分的,编译时会根据后缀名来选择编译器,能够识别我们常见的后缀名,例如.cc .cp .cxx .cpp .CPP .c++ .C。如果需要扩展其他后缀可以使用LOCAL_CPP_EXTENSION,扩展的后缀名需要加上‘.’。

    1. LOCAL_CPP_EXTENSION := .cc .cxx

    这两种调用方式在编译阶段就已经确定了,在jni.h中JNIEnv中的方法分别使用两个结构体_JNIEnv和JNINativeInterface来表示,如果是c++使用_JNIEnv结构体中的方法,如果是c则使用JNINativeInterface中的方法,为什么要这样做呢?这样是为了充分使用c++的特性,在c++中对struct进行了扩展,里面能够直接定义方法,且默认是public类型的,定义了这样一个结构体则结构体对象可以直接去访问这些方法,让开发者写起来轻松些,但是c就没那么幸运了,c的struct虽然也可以定义方法但是也只能是函数指针这样的形式。同理jni.h中JavaVM也分别使用两个结构体_JavaVM和JNIInvokeInterface来表示。我们直接看代码jni.h中是如何定义JNIEnv和JavaVM的。

    使用编译器内置宏’__cplusplus‘来区分c和c++代码。

    1. #if defined(__cplusplus)

    2. typedef _JNIEnv JNIEnv;

    3. typedef _JavaVM JavaVM;

    4. #else

    5. typedef const struct JNINativeInterface* JNIEnv;

    6. typedef const struct JNIInvokeInterface* JavaVM;

    7. #endif

    看看_JNIEnv和JNINativeInterface两个结构体:

    从如下代码中我们可以看出最终都是调用同一个方法,在我们的代码中也可以利用这种方式把我们写的c接口以c++的接口提供出去。同样的道理我们对以下代码稍微修改,我们同样可以把我们写的c++接口以c接口的形式提供出去。

    1. /*

    2. c方法使用JNINativeInterface这个结构体

    3. */

    4. struct JNINativeInterface {

    5.    //...

    6.    jint        (*GetVersion)(JNIEnv *);

    7.    jclass      (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*,

    8.                        jsize);

    9.    jclass      (*FindClass)(JNIEnv*, const char*);

    10.    //...

    11. };

    12. /*

    13. c++方法使用_JNIEnv这个结构体

    14. */

    15. struct _JNIEnv {

    16. const struct JNIInvokeInterface* functions;

    17. #if defined(__cplusplus)

    18.    //...

    19.    jint GetVersion()

    20.    { return functions->GetVersion(this); }

    21.    jclass DefineClass(const char *name, jobject loader, const jbyte* buf,

    22.        jsize bufLen)

    23.    { return functions->DefineClass(this, name, loader, buf, bufLen); }

    24.    jclass FindClass(const char* name)

    25.    { return functions->FindClass(this, name); }

    26.    //...

    27. #endif

    28. };

    所以c和c++的JNIEnv和JavaVM是不一样的,如果我们的代码中同时存在c和c++代码,并且都需要用到JNIEnv和JavaVM时需要注意,每一个jni方法都有一个自己的JNIEnv,这个JNIEnv不能传给c++代码去调用c++方式的jni方法,同理c++方式的JNIEnv不能传给c代码去调用c方式的jni方法。c代码中的JavaVM需要用到c代码的JNIEnv方法获得,同理c++代码中的JavaVM需要用到c++代码的JNIEnv方法获得。通过JNIEnv获取JavaVM的方法如下:

    1. /*c方法*/

    2. JavaVM *jvm = NULL;

    3. (*env)->GetJavaVM(env, &jvm);

    4. /*c++方法*/

    5. JavaVM *jvm = NULL;

    6. env->GetJavaVM(&jvm);

    如果理解了我上面的说的知识就知道我们为什么是这样调用了,以下是我们常用的调用jni的方式,这里以GetObjectRefType为例:

    1. /*C方法*/

    2. jobjectRefType type = (*env)->GetObjectRefType(env, obj);

    3. /*C++方法*/

    4. jobjectRefType type = env->GetObjectRefType(obj);

    4.2 调用Java方法

    c、c++调用Java方法又可以分为static和非static两种方式,c、c++并不关心调用的Java方法是private还是public的。在使用的时候需要格外注意,在jni的编程中如果你使用了不当的方式它并不会提示你使用了错误的方式,而是直接给你一个crash,crash信息并不指明是什么错误,所以在jni编程中我们尽可能的注意这些细节要不我们会花费过多的时间在调试上。前面说了调用方式分为c和c++两种方式,在我以下所有的例子我都会使用c++的调用方式,所有的例子都是用c++写的。如果希望使用c方式调用可以参考我上面写的那个例子就好了,仅仅存在一些细微的变化。

    4.2.1 下面我们分析调用Java非static方法

    调用Java方法可以按照类型来划分分为十种,每一种类型按照参数划分又分别有三种。所以我们看到的调用Java方法的jni接口是这样的

    1. /*

    2. 这种调用方式把需要传的参数依次写在第二个参数之后就好,支持的类型为jni中的基本数据类型和数组类型

    3. 这种方式也是Android源码中常用的方式,因为灵活性较大,使用方便。

    4. */

    5. _jtype Call<_jname>Method(jobject  obj, jmethodID methodID, ...)

    6. /*

    7. 这种调用方式把需要传的参数依次写在变参数组中,在Android源码中很少看到这样的使用方式

    8. */

    9. _jtype Call<_jname>MethodV(jobject obj, jmethodID methodID, va_list args)

    10. /*

    11. 这种调用方式把需要传的参数依次写在jvalue数组中,在Android源码中更少看到这样的使用方式

    12. */

    13. _jtype Call<_jname>MethodA(jobject obj, jmethodID methodID, jvalue* args)

    14. typedef union jvalue {

    15.    jboolean    z;

    16.    jbyte       b;

    17.    jchar       c;

    18.    jshort      s;

    19.    jint        i;

    20.    jlong       j;

    21.    jfloat      f;

    22.    jdouble     d;

    23.    jobject     l;

    24. } jvalue;

    以上中_jtype和_jname对应的十种类型分别为:

    1. (jobject,  Object):   CallObjectMethod  CallObjectMethodV  CallObjectMethodA

    2. (jboolean, Boolean):  CallBooleanMethod CallBooleanMethodV CallBooleanMethodA

    3. (jbyte,    Byte):     CallByteMethod    CallByteMethodV    CallByteMethodA

    4. (jchar,    Char):     CallCharMethod    CallCharMethodV    CallCharMethodA

    5. (jshort,   Short):    CallShortMethod   CallShortMethodV   CallShortMethodA

    6. (jint,     Int):      CallIntMethod     CallIntMethodV     CallIntMethodA

    7. (jlong,    Long):     CallLongMethod    CallLongMethodV    CallLongMethodA

    8. (jfloat,   Float):    CallFloatMethod   CallFloatMethodV   CallFloatMethodA

    9. (jdouble,  Double):   CallDoubleMethod  CallDoubleMethodV  CallDoubleMethodA

    10. (void,     Void):     CallVoidMethod    CallVoidMethodV    CallVoidMethodA

    但是在jni.h的_JNIEnv结构体中我们能看到的Call<_jname>Method仅仅为CallVoidMethod,其他方法都用宏来写了,这种写法在实际开发中也是经常用到的,特别是类似这种仅仅是方法名和返回值不一样,把这几个宏贴出来。

    1. #define CALL_TYPE_METHOD(_jtype, _jname)                                    

    2.    __NDK_FPABI__                                                          

    3.    _jtype Call##_jname##Method(jobject obj, jmethodID methodID, ...)      

    4.    {                                                                      

    5.        _jtype result;                                                      

    6.        va_list args;                                                      

    7.        va_start(args, methodID);                                          

    8.        result = functions->Call##_jname##MethodV(this, obj, methodID,      

    9.                    args);                                                  

    10.        va_end(args);                                                      

    11.        return result;                                                      

    12.    }

    13. #define CALL_TYPE_METHODV(_jtype, _jname)                                  

    14.    __NDK_FPABI__                                                          

    15.    _jtype Call##_jname##MethodV(jobject obj, jmethodID methodID,          

    16.        va_list args)                                                      

    17.    { return functions->Call##_jname##MethodV(this, obj, methodID, args); }

    18. #define CALL_TYPE_METHODA(_jtype, _jname)                                  

    19.    __NDK_FPABI__                                                          

    20.    _jtype Call##_jname##MethodA(jobject obj, jmethodID methodID,          

    21.        jvalue* args)                                                      

    22.    { return functions->Call##_jname##MethodA(this, obj, methodID, args); }

    23. #define CALL_TYPE(_jtype, _jname)                                          

    24.    CALL_TYPE_METHOD(_jtype, _jname)                                        

    25.    CALL_TYPE_METHODV(_jtype, _jname)                                      

    26.    CALL_TYPE_METHODA(_jtype, _jname)

    27.    CALL_TYPE(jobject, Object)

    28.    CALL_TYPE(jboolean, Boolean)

    29.    CALL_TYPE(jbyte, Byte)

    30.    CALL_TYPE(jchar, Char)

    31.    CALL_TYPE(jshort, Short)

    32.    CALL_TYPE(jint, Int)

    33.    CALL_TYPE(jlong, Long)

    34.    CALL_TYPE(jfloat, Float)

    35.    CALL_TYPE(jdouble, Double)

    4.2.2 下面我们分析调用Java static方法

    static方法的调用和非static方法的调用时类似的,仅仅在调用方法不一样,其他都和非static调用方式一样了。具体的可以看非static方法的调用。

    1. _jtype CallStatic<_jname>Method(jclass  clazz, jmethodID methodID, ...)

    2. _jtype CallStatic<_jname>MethodV(jclass clazz, jmethodID methodID, va_list args)

    3. _jtype CallStatic<_jname>MethodA(jclass clazz, jmethodID methodID, jvalue* args)

    4.2.3 如何使用以上方法

    这里还是需要区分static方法和非static方法的,以CallVoidMethod和CallStaticVoidMethod为例,他们的第一个参数是不一样的,非static方法第一个参数是jobject,可以看出这个是一个类的对象,调用非static方法是通过对象来访问的。static方法是jclass,可以看出可以直接通过class来访问这个Java方法。除了第一个参数以外第二个参数也是不一样的,虽然都是jmethodID但是这个jmethodID用的方法是不一样的,一个是用static方法去获取static的jmethodID,另一个是用非static的方法去获取非static的jmethodID,这样的调用方式是和Java对应的。

    1. 非static方法通过调用类的对象来调用,jmethodID通过GetMethodID获得。

    2. static方法通过类来调用,jmethodID通过GetStaticMethodID来获得。

    我觉得我们有必要先了解下jmethodID和jfieldID,这两个分别表示方法id和字段id,我们可以通过方法id调用方法并获取返回值,通过变量id获取字段的值和设置字段的值。

    他们的定义如下,都是struct指针类型。

    1. struct _jfieldID;                       /* opaque structure */

    2. typedef struct _jfieldID* jfieldID;     /* field IDs */

    3. struct _jmethodID;                      /* opaque structure */

    4. typedef struct _jmethodID* jmethodID;   /* method IDs */

    为了方便获取这两个值通常定义几组宏来获取这两个值,这里需要注意的是在使用宏的方法里需要有一个c++类型的JNIEnv指针对象,并且名字是env,或许你可以修改下宏的名字以及调用方法。

    1. #define FIND_CLASS(var, className)

    2.    var = env->FindClass(className);

    3.    LOG_FATAL_IF(! var, "Unable to find class " className);

    4. #define GET_FIELD_ID(var, clazz, fieldName, fieldDescriptor)

    5.    var = env->GetFieldID(clazz, fieldName, fieldDescriptor);

    6.    LOG_FATAL_IF(! var, "Unable to find field " fieldName);

    7. #define GET_METHOD_ID(var, clazz, fieldName, fieldDescriptor)

    8.    var = env->GetMethodID(clazz, fieldName, fieldDescriptor);

    9.    LOG_FATAL_IF(! var, "Unable to find method " fieldName);

    10. #define GET_STATIC_FIELD_ID(var, clazz, fieldName, fieldDescriptor)

    11.    var = env->GetStaticFieldID(clazz, fieldName, fieldDescriptor);

    12.    LOG_FATAL_IF(! var, "Unable to find field " fieldName);

    13. #define GET_STATIC_METHOD_ID(var, clazz, fieldName, fieldDescriptor)

    14.    var = env->GetStaticMethodID(clazz, fieldName, fieldDescriptor);

    15.    LOG_FATAL_IF(! var, "Unable to find static method " fieldName);

    定义了宏方便我们使用,但是怎么使用呢,现在我们来分析GetMethodID和GetFieldID的参数。先来看看这两个方法的原型吧。

    1. jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)

    2. jfieldID GetFieldID(jclass   clazz, const char* name, const char* sig)

    可以看出这两个参数都是一样的,我们可以一起分析。

    clazz: MethodID或者FieldID所在的class,这个可以通过FindClass获得

    name:我们需要获取Java类的方法名或者字段名

    sig  :    sig描述的是对应于Java的类型,MethodID(描述了方法的参数和返回值),FieldID(描述了字段的类型)

    关于sig字段可以参考oracle的官方文档,http://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/types.html#wp16432,或者可以看我从文档提取出来的(如下),最后面还举例说明了用法,这个的命名方式和jvalue一样的,基本类型的名字只不过变成了大写而已,然后扩展了数组类型和类。

    注意事项:

    1. signature之间不能有空格

    2. 类变量需要以"L"打头,后面加有效的类名路径(包名+类名,内部类使用"$"隔开),以";"结尾

    3. 数组以"["打头,后面加类型,这里的类型还是signature的类型,不仅仅指基本类型,还包括类(Z B C S I J F D Lfully-qualified-class;)

    4. 参数不是必须的,返回值是必须有的,如果返回值为空则用"V"表示,参数为空可以不写("()V" "(I)V" "(ILjava/util/HashMap;)Ljava/util/HashMap;")

    5. 参数和返回值的顺序不能写反,必须遵循这样的规则(参数)返回值,记得用引号引起来,需要的是字符串的形式("(arg-types)ret-type")

    1. Type Signature                Java Type

    2. Z                             boolean

    3. B                             byte

    4. C                             char

    5. S                             short

    6. I                             int

    7. J                             long

    8. F                             float

    9. D                             double

    10. Lfully-qualified-class;       fully-qualified-class

    11. [type                         type[]

    12. (arg-types)ret-type           method type

    13. For example, the Java method:

    14. long f(int n, String s, int[] arr);

    15. has the following type signature:

    16. (ILjava/lang/String;[I)J

    上面已经为我们正式写例子做了大量的铺垫,下面写一个例子看看Android源码中是如何使用这些方法的,代码提取自Android 4.4.2_r1源码,并进行了一些细微的修改以方便大家以及自己今后参考,仅作参考不能直接编译使用。

    1. JNI调用Java非static方法的例子

    以下用到的宏在上面已经给出,往上拖动即可看到。

    1. /*

    2. 以调用Java的HashMap和ArrayList为例,从中了解JNI如何调用非static的方法。以下代码从Android 4.4.2_r1源码中提取,经过小小的改动方便阅读。

    3. */

    4. #include <jni.h>

    5. #include <vector>

    6. #include <list>

    7. #include <String8.h>

    8. struct HashmapFields {

    9.    jmethodID init;

    10.    jmethodID get;

    11.    jmethodID put;

    12.    jmethodID entrySet;

    13. };

    14. static jobject KeyedVectorToHashMap (JNIEnv *env, KeyedVector<String8, String8> const &map) {

    15.    jclass clazz;

    16. HashmapFields hashmap;

    17.    FIND_CLASS(clazz, "java/util/HashMap");

    18. GET_METHOD_ID(hashmap.init, clazz, "<init>", "()V");

    19. GET_METHOD_ID(hashmap.get, clazz, "get", "(Ljava/lang/Object;)Ljava/lang/Object;");

    20. GET_METHOD_ID(hashmap.put, clazz, "put",

    21.  "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");

    22. GET_METHOD_ID(hashmap.entrySet, clazz, "entrySet", "()Ljava/util/Set;");

    23.    jobject hashMap = env->NewObject(clazz, hashmap.init);

    24.    for (size_t i = 0; i < map.size(); ++i) {

    25.        jstring jkey = env->NewStringUTF(map.keyAt(i).string());

    26.        jstring jvalue = env->NewStringUTF(map.valueAt(i).string());

    27.        env->CallObjectMethod(hashMap, hashmap.put, jkey, jvalue);

    28.        env->DeleteLocalRef(jkey);

    29.        env->DeleteLocalRef(jvalue);

    30.    }

    31.    return hashMap;

    32. }

    33. struct ArrayListFields {

    34.    jmethodID init;

    35.    jmethodID add;

    36. };

    37. static jobject ListOfVectorsToArrayListOfByteArray(JNIEnv *env,

    38.                                                   List<Vector<uint8_t> > list) {

    39.    jclass clazz;

    40. ArrayListFields arraylist;

    41.    FIND_CLASS(clazz, "java/util/ArrayList");

    42. GET_METHOD_ID(arraylist.init, clazz, "<init>", "()V");

    43. GET_METHOD_ID(arraylist.add, clazz, "add", "(Ljava/lang/Object;)Z");

    44.    jobject arrayList = env->NewObject(clazz, arraylist.init);

    45.    List<Vector<uint8_t> >::iterator iter = list.begin();

    46.    while (iter != list.end()) {

    47.        jbyteArray byteArray = VectorToJByteArray(env, *iter);

    48.        env->CallBooleanMethod(arrayList, arraylist.add, byteArray);

    49.        env->DeleteLocalRef(byteArray);

    50.        iter++;

    51.    }

    52.    return arrayList;

    53. }

    2. JNI调用Java static方法的例子

    1. /*

    2. 以下这个例子是从android_opengl_GLES30.cpp拿出来的,这个是JNI调用Java static方法的例子,直接看代码就能明白了。

    3. 感觉需要说明的地方是nativeClassInit这个方法本身又是一个native方法,但是并不影响我们去理解JNI调用static方法的知识点。

    4. GetStaticMethodID CallStaticLongMethod CallStaticObjectMethod CallStaticIntMethod

    5. */

    6. static jclass nioAccessClass;

    7. static jclass bufferClass;

    8. static jmethodID getBasePointerID;

    9. static jmethodID getBaseArrayID;

    10. static jmethodID getBaseArrayOffsetID;

    11. static jfieldID positionID;

    12. static jfieldID limitID;

    13. static jfieldID elementSizeShiftID;

    14. static void

    15. nativeClassInit(JNIEnv *_env, jclass glImplClass)

    16. {

    17.    jclass nioAccessClassLocal = _env->FindClass("java/nio/NIOAccess");

    18.    nioAccessClass = (jclass) _env->NewGlobalRef(nioAccessClassLocal);

    19.    jclass bufferClassLocal = _env->FindClass("java/nio/Buffer");

    20.    bufferClass = (jclass) _env->NewGlobalRef(bufferClassLocal);

    21.    getBasePointerID = _env->GetStaticMethodID(nioAccessClass,

    22.            "getBasePointer", "(Ljava/nio/Buffer;)J");

    23.    getBaseArrayID = _env->GetStaticMethodID(nioAccessClass,

    24.            "getBaseArray", "(Ljava/nio/Buffer;)Ljava/lang/Object;");

    25.    getBaseArrayOffsetID = _env->GetStaticMethodID(nioAccessClass,

    26.            "getBaseArrayOffset", "(Ljava/nio/Buffer;)I");

    27.    positionID = _env->GetFieldID(bufferClass, "position", "I");

    28.    limitID = _env->GetFieldID(bufferClass, "limit", "I");

    29.    elementSizeShiftID =

    30.        _env->GetFieldID(bufferClass, "_elementSizeShift", "I");

    31. }

    32. static void *

    33. getPointer(JNIEnv *_env, jobject buffer, jarray *array, jint *remaining, jint *offset)

    34. {

    35.    jint position;

    36.    jint limit;

    37.    jint elementSizeShift;

    38.    jlong pointer;

    39.    position = _env->GetIntField(buffer, positionID);

    40.    limit = _env->GetIntField(buffer, limitID);

    41.    elementSizeShift = _env->GetIntField(buffer, elementSizeShiftID);

    42.    *remaining = (limit - position) << elementSizeShift;

    43.    pointer = _env->CallStaticLongMethod(nioAccessClass,

    44.            getBasePointerID, buffer);

    45.    if (pointer != 0L) {

    46.        *array = NULL;

    47.        return (void *) (jint) pointer;

    48.    }

    49.    *array = (jarray) _env->CallStaticObjectMethod(nioAccessClass,

    50.            getBaseArrayID, buffer);

    51.    *offset = _env->CallStaticIntMethod(nioAccessClass,

    52.            getBaseArrayOffsetID, buffer);

    53.    return NULL;

    54. }

    3. 字段(FieldID)方面的例子,这里就不去细分static和非static了,区别就在于调用的jni方法不一样,需要自己注意。

    测试例子中写了一个方法去获取类的字段,里面涉及到了普通的类和内部类,例子中的获取类的字段这个方法不错,在需要获取类的很多字段时这样写的效率很高。

    1. /*

    2. 代码还是提取自Android 4.4.2_r1 但是还是不能直接编译通过的,也是稍微修改过的。

    3. */

    4. struct fields_t {

    5.    jfieldID    context;

    6.    jfieldID    facing;

    7.    jfieldID    orientation;

    8.    jfieldID    canDisableShutterSound;

    9.    jfieldID    face_rect;

    10.    jfieldID    face_score;

    11.    jfieldID    rect_left;

    12.    jfieldID    rect_top;

    13.    jfieldID    rect_right;

    14.    jfieldID    rect_bottom;

    15.    jmethodID   post_event;

    16.    jmethodID   rect_constructor;

    17.    jmethodID   face_constructor;

    18. };

    19. static int find_fields(JNIEnv *env, field *fields, int count)

    20. {

    21.    for (int i = 0; i < count; i++) {

    22.        field *f = &fields[i];

    23.        jclass clazz = env->FindClass(f->class_name);

    24.        if (clazz == NULL) {

    25.            ALOGE("Can't find %s", f->class_name);

    26.            return -1;

    27.        }

    28.        jfieldID field = env->GetFieldID(clazz, f->field_name, f->field_type);

    29.        if (field == NULL) {

    30.            ALOGE("Can't find %s.%s", f->class_name, f->field_name);

    31.            return -1;

    32.        }

    33.        *(f->jfield) = field;

    34.    }

    35.    return 0;

    36. }

    37. int register_android_hardware_Camera(JNIEnv *env)

    38. {

    39. fields_t fields;

    40.    field fields_to_find[] = {

    41.        { "android/hardware/Camera", "mNativeContext",   "I", &fields.context },

    42.        { "android/hardware/Camera$CameraInfo", "facing",   "I", &fields.facing },

    43.        { "android/hardware/Camera$CameraInfo", "orientation",   "I", &fields.orientation },

    44.        { "android/hardware/Camera$CameraInfo", "canDisableShutterSound",   "Z",

    45.          &fields.canDisableShutterSound },

    46.        { "android/hardware/Camera$Face", "rect", "Landroid/graphics/Rect;", &fields.face_rect },

    47.        { "android/hardware/Camera$Face", "score", "I", &fields.face_score },

    48.        { "android/graphics/Rect", "left", "I", &fields.rect_left },

    49.        { "android/graphics/Rect", "top", "I", &fields.rect_top },

    50.        { "android/graphics/Rect", "right", "I", &fields.rect_right },

    51.        { "android/graphics/Rect", "bottom", "I", &fields.rect_bottom },

    52.    };

    53.    if (find_fields(env, fields_to_find, NELEM(fields_to_find)) < 0)

    54.        return -1;

    55. return 0;

    56. }

    4.2.3 注意事项:

    1. 内部类的用法不是斜杠("/"),是("$"),例如("android/hardware/Camera$CameraInfo")

    2. 获取一个类的构造函数GetMethodID或者GetStaticMethodID方法,不过中间的名字必须是("<init>"),sig是根据构造函数的实际参数和返回值写的,sig的写法往上拖动即可看到

    3. 调用Java方法的jni接口注意区分static和非static接口

    4.3. 调用和获取Java类的字段

    这里不会花很大篇章去详细描述了,因为GetFieldID和GetStaticFieldID这两个很重要的方法已经在4.2.3描述得和清楚了,而且字段的调用方式和调用Java的方法是很相识的,使用上难度并不大。

    字段的获取和调用也分为static和非static两种方式,下面先来看看有那些方法。

    1. 非static方法

    1. /*

    2.  获取字段类型的方法

    3. */

    4.          _jtype Get<_jname>Field

    5. GetObjectField  GetBooleanField GetByteField

    6. GetCharField    GetShortField   GetIntField

    7. GetLongField    GetFloatField   GetDoubleField

    8. /*

    9.  设置字段类型的方法

    10. */

    11.          void Set<_jname>Field

    12. SetObjectField  SetBooleanField SetByteField

    13. SetCharField    SetShortField   SetIntField

    14. SetLongField    SetFloatField   SetDoubleField

    2. static方法

    1. /*

    2.  获取字段类型的方法

    3. */

    4.            _jtype GetStatic<_jname>Field

    5. GetStaticObjectField  GetStaticBooleanField GetStaticByteField

    6. GetStaticCharField    GetStaticShortField   GetStaticIntField

    7. GetStaticLongField    GetStaticFloatField   GetStaticDoubleField

    8. /*

    9.  设置字段类型的方法

    10. */

    11.           void SetStatic<_jname>Field

    12. SetStaticObjectField  SetStaticBooleanField SetStaticByteField

    13. SetStaticCharField    SetStaticShortField   SetStaticIntField

    14. SetStaticLongField    SetStaticFloatField   SetStaticDoubleField

    4.3.1 如何使用以上方法

    这里以GetObjectField、SetObjectField、GetStaticObjectField和SetStaticObjectField为例:

    先看看他们的原型:

    1. jobject GetObjectField(jobject obj, jfieldID fieldID)

    2. void SetObjectField(jobject    obj, jfieldID fieldID, jobject value)

    3. jobject GetStaticObjectField(jclass clazz, jfieldID fieldID)

    4. void SetStaticObjectField(jclass    clazz, jfieldID fieldID, jobject value)

    1. 非static的类型get和set返回值不一样,get方法返回值的类型正是我们需要的类型,关于这个类型在最前面基本数据类型介绍过;第一个参数是类的对象,这个对象可以自己构造(4.2中有具体的描述),也可以通过native方法的第二个参数获得,非static的native方法第二个参数为类的对象;第二个参数是字段ID通过GetFieldID获得,关于这个我在4.2.3中有具体的描述,在这里就不说过多的描述了

    2. static方法和非static方法的差异在于第一个参数和第二个参数,第一个参数是类,可以直接通过类来获得字段,这个类可以通过FindClass获得,也可以通过static native方法的第二个参数获得,static的native方法第二个参数就是类本身;第二个参数是字段ID通过GetStaticFieldID获得,关于这个我在4.2.3中有具体的描述,在这里就不过多的描述了

    这里就不举例了,使用方式和调用Java方法类似的。

    4.4 数组类型的使用

    对于数组的操作可以分为:

    1. 创建新数组(_jtype##Array New<_jname>Array)

    2. 转换数组数据类型为基本数据类型指针(_jtype* Get<_jname>ArrayElements)

    3. 释放转换的基本数据类型指针(void Release<_jname>ArrayElements)

    4. 获取数组单个元素的值(_jtype GetObjectArrayElement(获取的类型只能为jobject,需要强制为自己需要的类型))

    5. 设置数组单个元素的值(void SetObjectArrayElement)

    简单介绍:

    1. 前面已经介绍过基本数据类型和数组数据类型,这些类型都是和Java的类型对应起来的

    2. 在C/C++中数组和指针在很多情况下是可以划等号的,但是在Java中是没有指针的概念的,jni夹在中间,所以就有了一个转换数组为指针的一组方法,那么相应的也有一组释放的方法。用得较多的情况应该就是把Java传过来的数组类型转换为jni的基本数据类型指针,然后根据需要强转为普通数据类型指针

    3. 在C/C++中如果调用的Java方法参数为数组类型,那么我们就需要在C/C++中使用创建新数组的接口了。这里有一个方法比较特殊(NewObjectArray),其他的接口都只能创建基本数据类型的数组,用它可以创建类类型的数组,类的获取还是通过(FindClass)获得

    4. 获取和设置数组的单个元素的值,一般需要一个一个获取或设置的数组元素的值一般都使用这方法,当然你也可以自己写个循环自己获取和设置

    数组类型的方法汇总:

    1. /*创建‘类’数组类型*/

    2. jobjectArray  NewObjectArray(jsize length, jclass elementClass, jobject initialElement)

    3. /*创建基本类型数组,这里的类型是和Java的数组类型一一对应的*/

    4. jbooleanArray NewBooleanArray(jsize length)

    5. jbyteArray    NewByteArray(jsize length)

    6. jcharArray    NewCharArray(jsize length)

    7. jshortArray   NewShortArray(jsize length)

    8. jintArray     NewIntArray(jsize length)

    9. jlongArray    NewLongArray(jsize length)

    10. jfloatArray   NewFloatArray(jsize length)

    11. jdoubleArray  NewDoubleArray(jsize length)

    12. /*转换数组类型为对应的指针类型*/

    13. jboolean* GetBooleanArrayElements(jbooleanArray array, jboolean* isCopy)

    14. jbyte*    GetByteArrayElements(jbyteArray array, jboolean* isCopy)

    15. jchar*    GetCharArrayElements(jcharArray array, jboolean* isCopy)

    16. jshort*   GetShortArrayElements(jshortArray array, jboolean* isCopy)

    17. jint*     GetIntArrayElements(jintArray array, jboolean* isCopy)

    18. jlong*    GetLongArrayElements(jlongArray array, jboolean* isCopy)

    19. jfloat*   GetFloatArrayElements(jfloatArray array, jboolean* isCopy)

    20. jdouble*  GetDoubleArrayElements(jdoubleArray array, jboolean* isCopy)

    21. /*释放*/

    22. void ReleaseBooleanArrayElements(jbooleanArray array, jboolean* elems, jint mode)

    23. void ReleaseByteArrayElements(jbyteArray array, jbyte* elems,jint mode)

    24. void ReleaseCharArrayElements(jcharArray array, jchar* elems,jint mode)

    25. void ReleaseShortArrayElements(jshortArray array, jshort* elems,jint mode)

    26. void ReleaseIntArrayElements(jintArray array, jint* elems,jint mode)

    27. void ReleaseLongArrayElements(jlongArray array, jlong* elems,jint mode)

    28. void ReleaseFloatArrayElements(jfloatArray array, jfloat* elems,jint mode)

    29. void ReleaseDoubleArrayElements(jdoubleArray array, jdouble* elems,jint mode)

    30. /*获取和设置单个数组元素的值*/

    31. jobject GetObjectArrayElement(jobjectArray array, jsize index)

    32. void    SetObjectArrayElement(jobjectArray array, jsize index, jobject value)

    例子:

    这个demo可以跑起来,整个流程也很简单Java上传传一个byte数组到jni,jni再调用Java的一个方法把传下来的byte数组转换为字符串显示出来。

    Java代码:

    1. package com.example.JniArrayTest;

    2. import android.app.Activity;

    3. import android.widget.TextView;

    4. import android.widget.Toast;

    5. import android.os.Bundle;

    6. public class JniArrayTest extends Activity

    7. {

    8. private final static String TAG = "Java Array Test";

    9.    @Override

    10.    public void onCreate(Bundle savedInstanceState)

    11.    {

    12.        super.onCreate(savedInstanceState);

    13.        TextView  tv = new TextView(this);

    14.        byte tojniString[] = new byte[] {'s', 't', 'r', 'i', 'n', 'g'};

    15.        tv.setText(stringToJNI(tojniString));

    16.        setContentView(tv);

    17.    }

    18.    

    19.    private void ComeFromeJni(String jniString)

    20.    {

    21.     Toast.makeText(getApplicationContext(), jniString, Toast.LENGTH_LONG).show();

    22.    }

    23.    private native String stringToJNI(byte javaString[]);

    24.    static {

    25.        System.loadLibrary("JniArrayTest");

    26.    }

    27. }

    Jni代码:

    1. #include <string.h>

    2. #include <jni.h>

    3. #include <android/log.h>

    4. #define ALOGI(...) __android_log_print(ANDROID_LOG_INFO,  "JniArrayTest", __VA_ARGS__)

    5. extern "C" jstring

    6. Java_com_example_JniArrayTest_JniArrayTest_stringToJNI(JNIEnv* env, jobject thiz, jcharArray javaString)

    7. {

    8. jchar *javaStr = env->GetCharArrayElements(javaString, NULL);

    9. int charlenght = env->GetArrayLength(javaString);

    10. ALOGI("string come from java %d:%s", charlenght, (const char *)javaStr);

    11. jmethodID mid = env->GetMethodID(env->GetObjectClass(thiz), "ComeFromeJni", "(Ljava/lang/String;)V");

    12. env->CallVoidMethod(thiz, mid, env->NewStringUTF((const char *)javaStr));

    13. return env->NewStringUTF((const char *)javaStr);

    14. }

    Android.mk

      1. LOCAL_PATH := $(call my-dir)

      2. include $(CLEAR_VARS)

      3. LOCAL_ARM_MODE := arm

      4. LOCAL_LINK_MODE := c++

      5. LOCAL_MODULE    := JniArrayTest

      6. LOCAL_SRC_FILES := JniArrayTest.cpp

      7. LOCAL_LDLIBS := -llog

      8. include $(BUILD_SHARED_LIBRARY)

  • 相关阅读:
    1.4 Arduino IDE
    1.3 选择适合的Arduino
    1.2为什么选择Arduino
    1.1什么是Arduino
    博文《arduino入门》预告
    Delphi实现HTMLWebBrowser实现HTML界面
    Delphi判断MDI子窗体是否被创建
    Delphi给窗体镶边-为控件加边框,描边,改变边框颜色
    Delphi 查找标题已知的窗口句柄,遍历窗口控件句柄
    DELPHI实现类似仿360桌面的程序界面
  • 原文地址:https://www.cnblogs.com/xiaorenwu702/p/5801877.html
Copyright © 2011-2022 走看看