JNI接着入门

上一篇博客:JNI入门基础主要简述了如何搭建jni环境以及一些基础的jni语法,这篇博客我们使用几个案例接着来夯实基础。

  • jni操纵数组
extern "C"
JNIEXPORT void JNICALL
Java_com_mvp_jnidemo_MainActivity_testArrayAction(JNIEnv *env, jobject thiz, jint count,
                                                  jstring text_info, jintArray ints,
                                                  jobjectArray strs) {
    int _count = count;
    LOGD("C++ _count:%d\n", _count);

    //NuLL就是0,在c/c++中非0就是true,0就是false。
    //记住jstring是jni专用的,现在我们写的是c/c++代码,必须转为char*
    const char *_text_info = env->GetStringUTFChars(text_info,
                                                    NULL);//false 在本地内部完成翻译转换,不需要开辟copy机制转换
    LOGD("C++ _text_info:%s\n", _text_info);

    //写c/c++代码必须时刻记住回收内存
    //释放_text_info,虽然这个函数执行结束后,会弹栈将所有的局部变量回收,但是我们还是养成随时回收内存的习惯
    env->ReleaseStringUTFChars(text_info, _text_info);

    jint *_ints = env->GetIntArrayElements(ints, NULL);
    int intsLen = env->GetArrayLength(ints);
    for (int i = 0; i < intsLen; ++i) {
        LOGI("修改前 C++ ints item:%d\n", *(_ints + i))
        *(_ints + i) = i + 10001;
        LOGI("修改后 C++ ints item:%d\n", *(_ints + i))
    }
    // JNI_OK     本次C++的修改的数组, 刷新给JVM Java层(即上面我们改了ints的值,java中获取的值是我们改变后的值), 并且释放C++数组
    // JNI_COMMIT 本次C++的修改的数组, 刷新给JVM Java层
    // JNI_ABORT  只释放C++数组
    env->ReleaseIntArrayElements(ints, _ints, JNI_OK);

    int strsLen = env->GetArrayLength(strs);
    for (int i = 0; i < strsLen; ++i) {
        jstring strItemS = (jstring) env->GetObjectArrayElement(strs, i);
        const char *strItemC = env->GetStringUTFChars(strItemS, NULL);
        LOGI("修改前 C++ strItemC:%s\n", strItemC)
        env->ReleaseStringUTFChars(strItemS, strItemC);

        jstring updateValue = env->NewStringUTF("Beyond");//现在我们是将Beyond这个字符串给jni使用,所以需要转为jstring
        env->SetObjectArrayElement(strs, i, updateValue);

        jstring strItemS2 = (jstring) env->GetObjectArrayElement(strs, i);
        const char *strItemC2 = env->GetStringUTFChars(strItemS2, NULL);
        LOGI("修改后 C++ strItemC2:%s\n", strItemC2)

        env->ReleaseStringUTFChars(strItemS2,strItemC2);
    }

}
  • jni操作对象
package com.mvp.jnidemo;

import android.util.Log;

public class Student {
    public String name;
    public int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public static void showInfo(String info) {
        Log.d("Brett", "showInfo info: " + info);
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

extern "C"
JNIEXPORT void JNICALL
Java_com_mvp_jnidemo_MainActivity_putObject(JNIEnv *env, jobject thiz, jobject student) {

    jclass studentClass = env->FindClass("com/mvp/jnidemo/Student");
    jmethodID toStringMethod = env->GetMethodID(studentClass,"toString","()Ljava/lang/String;");
    jstring toStringValueS = (jstring) env->CallObjectMethod(student, toStringMethod);

    const char * toStringValueC = env->GetStringUTFChars(toStringValueS, NULL);
    LOGD("toStringValueC:%s\n", toStringValueC);
    env->ReleaseStringUTFChars(toStringValueS, toStringValueC);

    // setName
    jmethodID setNameMethod = env->GetMethodID(studentClass, "setName", "(Ljava/lang/String;)V");
    jstring value1 = env->NewStringUTF("李元霸");
    env->CallVoidMethod(student, setNameMethod, value1);

    // setAge
    jmethodID setAgeMethod = env->GetMethodID(studentClass, "setAge", "(I)V");
    env->CallVoidMethod(student, setAgeMethod, 99);

    jmethodID showInfo = env->GetStaticMethodID(studentClass, "showInfo", "(Ljava/lang/String;)V");
    jstring value2 = env->NewStringUTF("静态的函数 李元霸");
    env->CallStaticVoidMethod(studentClass, showInfo, value2);


    jclass studentClass2 = env->FindClass("com/mvp/jnidemo/Student");
    jmethodID toStringMethod2 = env->GetMethodID(studentClass2, "toString", "()Ljava/lang/String;");
    jstring toStringValueS2 = (jstring) env->CallObjectMethod(student, toStringMethod2);

    const char * toStringValueC2 = env->GetStringUTFChars(toStringValueS2, NULL);
    // 调用完GetStringUTFChars之后不要忘记安全检查,因为JVM需要为新诞生的字符串分配内存空    
   // 间,当内存空间不够分配的时候,会导致调用失败,失败后GetStringUTFChars会返回NULL,并抛出一个
  // OutOfMemoryError异常。JNI的异常和Java中的异常处理流程是不一样的,Java遇到异常如果没有捕获,程序会立即停止运行。而JNI遇到未决的异常不会改变程序的运行流程,也就是程序会继续往下走,这样后面针对这个字符串的所有操作都是非常危险的,因此,我们需要立即结束当前方法。
    if(toStringValueC2 !=NULL){
       LOGD("toStringValueC:%s\n", toStringValueC2);
    }
    
    // 记得随手释放内存
    env->ReleaseStringUTFChars(toStringValueS2, toStringValueC2);

    env->DeleteLocalRef(studentClass);//释放内存
}
  • c/c++创建Java对象
package com.mvp.jnidemo;

import android.util.Log;

public class Person {
    private Student student;

    public void setStudent(Student student) {
        this.student = student;
        Log.e("Brett", "call setStudent student :" + student.toString());
    }
}

extern "C"
JNIEXPORT void JNICALL
Java_com_mvp_jnidemo_MainActivity_insertobject(JNIEnv *env, jobject thiz) {
    // @Student对象
    jclass studentClass = env->FindClass("com/mvp/jnidemo/Student");
    jobject student = env->AllocObject(studentClass);

    // setName
    jmethodID setNameMethod = env->GetMethodID(studentClass, "setName", "(Ljava/lang/String;)V");
    jstring value1 = env->NewStringUTF("小明");
    env->CallVoidMethod(student, setNameMethod, value1);
    // setAge
    jmethodID setAgeMethod = env->GetMethodID(studentClass, "setAge", "(I)V");
    env->CallVoidMethod(student, setAgeMethod, 20);

    jclass personClass = env->FindClass("com/mvp/jnidemo/Person");

    //c++分配一个对象出来。但是不会调用此对象的构造函数
    jobject person = env->AllocObject(personClass);
//    env->NewObject(); //c++分配一个对象出来,会调用此对象的构造函数,相当于new XXX();

    jmethodID setStudent = env->GetMethodID(personClass, "setStudent",
                                            "(Lcom/mvp/jnidemo/Student;)V");
    env->CallVoidMethod(person, setStudent, student);
    env->DeleteLocalRef(person);
    env->DeleteLocalRef(student);
    env->DeleteLocalRef(studentClass);
    env->DeleteLocalRef(personClass);
}
  • c/c++中的全局变量
package com.mvp.jnidemo;

import android.util.Log;

public class Dog {

    // <init> 构造函数名
    public Dog() {
        Log.d("Brett", "Dog init...");
    }

    public Dog(int number1) {
        Log.d("Brett", "Dog init... number1:" + number1);
    }

    public Dog(int number1, int number2) {
        Log.d("Brett", "Dog init... number1:" + number1 + " number2:" + number2);
    }

    // ...

}

jclass dogClass = nullptr; // 在Java的思想中,这个就是全局变量 全局成员,注意了:现在是JNI,这句话 依然是 局部成员

extern "C"
JNIEXPORT void JNICALL
Java_com_mvp_jnidemo_MainActivity_testQuote(JNIEnv *env, jobject thiz) {
    if(!dogClass){
        LOGI("dogClass is null")
        jclass  temp = env->FindClass("com/mvp/jnidemo/Dog");
        dogClass = static_cast<jclass>(env->NewGlobalRef(temp));
        env->DeleteLocalRef(temp);
    }
    LOGI("dogClass is not null")
    // env->NewObject();  // C++ 实例化一个对象出来,会调用此对象的构造函数,相当于: new XXX();
    jmethodID dogInit = env->GetMethodID(dogClass, "<init>", "()V");
    jobject dog = env->NewObject(dogClass, dogInit); // Dog dog = new Dog();

    jmethodID dogInit1 = env->GetMethodID(dogClass, "<init>", "(I)V");
    jobject dog2 = env->NewObject(dogClass, dogInit1, 1); // Dog dog = new Dog(1);

    jmethodID dogInit3 = env->GetMethodID(dogClass, "<init>", "(II)V");
    jobject dog3 = env->NewObject(dogClass, dogInit3, 1, 2); // Dog dog = new Dog(1, 2);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_mvp_jnidemo_MainActivity_delQuote(JNIEnv *env, jobject thiz) {
    if (dogClass){
        env->DeleteGlobalRef(dogClass);
        //jni函数弹栈后会自动释放所有的局部变量,释放后,不会置为null
        dogClass = nullptr;
        LOGI("手动释放全局变量成功")
    }
}
package com.mvp.jnidemo;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.mvp.jnidemo.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "Brett";

    // Used to load the 'jnidemo' library on application startup.
    static {
        //我们在工程项目里面编写的c/c++代码最后会被编译成一个so,可以认为这个so库就是c/c++的源码,有点类似于java的jar包
        //这个jnidemo是apk包里面的lib目录下的libjnidemo.so,注意编译后会自动给so库加上lib前缀
        System.loadLibrary("jnidemo");
        // System.load("D://xxx/xxx/xxx/xx/xx.so"); // 加载绝对路径下的 库
    }

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // Example of a call to a native method
        Button btn = binding.btn1;
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int[] ints = new int[]{1, 2, 3, 4, 5, 6};
                String[] strs = new String[]{"李小龙", "李连杰", "李元霸"};
                testArrayAction(99, "你好", ints, strs);

                for (int anInt : ints) { // Java 输出 int 数组
                    Log.d(TAG, "Java test01: java ints:" + anInt);
                }

                for (String str : strs) { // 输出 String 数组
                    Log.e(TAG, "Java test01: java strs:" + str);
                }
            }
        });

        binding.btn2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Student student = new Student();
                student.name = "史泰龙";
                student.age = 88;
                putObject(student);
            }
        });
        binding.btn3.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                insertobject();
            }
        });
        binding.btn4.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                testQuote();
            }
        });
        binding.btn5.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                delQuote();
            }
        });
    }

    /**
     * A native method that is implemented by the 'jnidemo' native library,
     * which is packaged with this application.
     */

    public native void testArrayAction(int count, String textInfo, int[] ints, String[] strs);

    public native void putObject(Student student);

    public native void insertobject();
    
    public native void testQuote(); // 测试引用
    public native void delQuote(); // 释放全局引用
}

好了,以上几个案例供大家参考使用。。。

异常处理

// 异常方式一:【C++处理异常】
extern "C"
JNIEXPORT void JNICALL
Java_com_test_MainActivity3_exception(JNIEnv *env, jclass clazz) {
   jfieldID f_id = env->GetStaticFieldID(clazz, "name999", "Ljava/lang/String;");

    // 一共有两种方式  方式
    // 补救措施:name999拿不到报错的话, 那么我就拿 name1

    jthrowable throwable = env->ExceptionOccurred(); // 检查本次函数执行,有没有异常
    if (throwable) {
        // 补救措施,先把异常清除,先不要奔溃
        LOGD("检查到有异常 native层")
        // 清除异常
        env->ExceptionClear();//让app不会闪退

        // 重新获取 name1 属性
        jfieldID f_id = env->GetStaticFieldID(clazz, "name1", "Ljava/lang/String;");
    }
}

// 异常方式二:【C++处理异常】往Java层抛异常,这样可以在java层通过try-catch的方式捕获exception2的异常
extern "C"
JNIEXPORT void JNICALL
Java_com_test_exception2(JNIEnv *env, jclass clazz) {
    jfieldID f_id = env->GetStaticFieldID(clazz, "name666", "Ljava/lang/String;");

    jthrowable throwable = env->ExceptionOccurred(); // 检查本次函数执行,有没有异常
    if (throwable) {
        // 补救措施,先把异常清除,先不要奔溃
        LOGD("检查到有异常 native层")
        // 清除异常
        env->ExceptionClear();

        // Throw抛一个 Java 的 Throwable 对象
        jclass  no_such_clz = env->FindClass("java/lang/NoSuchFieldException");
        env->ThrowNew(no_such_clz, "NoSuchFieldException 实在是找不到 name666 啊,没办法,奔溃了!");
    }
}

// 此函数是 让 C++调用的native层 来 调用的函数
    public static void show() throws Exception {
        Log.d("Derry", "show: 111");
        Log.d("Derry", "show: 222");
        Log.d("Derry", "show: 333");
        Log.d("Derry", "show: 444");
        Log.d("Derry", "show: 555");

        // 模拟 Java在执行逻辑的时候,出现了异常
        // 故意抛一个异常 给 下面的C++往,看你怎么办?
        throw new NullPointerException("我是java中的抛出的异常,我的show方法里面发送了Java语法错误");
    }

// 异常方式三:【Java层出现了异常】
extern "C"
JNIEXPORT void JNICALL
Java_com_test_MainActivity3_exception3(JNIEnv *env, jclass clazz) {
   jmethodID showMid = env->GetStaticMethodID(clazz, "show", "()V");
   env->CallStaticVoidMethod(clazz, showMid);

    // JNIEnv的操作
    // env->GetStaticFieldID(clazz, "name1", "Ljava/lang/String;"); //该代码若写在ExceptionCheck前面会奔溃

    // 证明不是马上奔溃了,在这个区域还没有奔溃,赶快处理,把异常被 磨平
    if (env->ExceptionCheck()) {
        env->ExceptionDescribe(); // 输出描述
        env->ExceptionClear();   // 清除异常
    }

    // 按道理来说,上面的这句话:env->CallStaticVoidMethod(clazz, showMID);,就已经奔溃了,但是事实是否如此呢?
    // 非JNIEnv的操作
    LOGI("native层:>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>.1");
    // JNIEnv的操作
    env->GetStaticFieldID(clazz, "name1", "Ljava/lang/String;");
}

后记

什么是JNI

JNI就是Java调用本地方法的技术,最简单的来说,java运行一个程序需要和不同的系统平台打交道,在window系统就是和windows平台底层打交道,mac就是和mac底层打交道,jvm通过使用大量jni技术使得java能够在不同平台上运行。
使用了这技术的一个标志就是native,如果一个类里的一个方法被native修饰,那就说明这个方法是jni来实现的,他是通过本地系统api里的方法实现的。当然这个本地方法可能是c或者c++,当然也可能是别的语言。jni是java跨平台的基础,jvm通过在不同系统上调用不同的本地方法使得jvm可以在不同平台移植。
注意:jni是一个接口语言,简单来说它是连接java和c/c++的中间商。因此,会有一个中间的转型过程,在这个过程中,有一个非常重要的也是非常关键的类型对接方式,这个方式就是,数据类型的转变。

java类型jni类型
booleanjboolean
bytejbyte
charjchar
shortjshort
intjint
longjlong
floatjfloat
doublejdouble
Classjclass
Stringjstring
Objectjobject
byte[]jbyteArray

动态库和静态库

Android NDK中的动态库和静态库就是linux下的动态库和静态库,因为NDK的开发可以理解从基于Linux的开发。c/c++中我们封装的功能或者函数可以通过静态库或者动态库的方式提供给别人使用。
Linux平台静态库以.a结尾,而动态库以.so结尾。
静态库编译成的文件比较大,因为整个函数库的所有数据都会被整合进目标代码中,它的优点就显而易见了,即编译后的执行程序不需要外部的函数库支持,因为所有使用的函数都已经被编译进去了。缺点就是,如果静态库改变了,那么程序必须重新编译。
动态库在编译的时候并没有被编译进目标代码中,程序执行到相关函数时才调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。由于函数库没有被整合进程序,而是程序运行时动态的申请并调用,所以程序的运行环境中必须提供相应的库。

静态库的代码在编译过程中已经被载入可执行程序,因此体积比较大;动态库的代码在可执行程序运行时才载入内存,在编译过程中仅简单的引用,因此代码体积比较小。

Android如何通过cmakeList.txt配置编译动态库和静态库

add_library(jinInterface SHARED library.c library.h)// SHARED 表示是动态库
add_library(jinInterface STATIC library.c library.h)// STATIC 表示是静态库
ADD_LIBRARY(...)
语法:ADD_LIBRARY(libname [SHARED|STATIC] )
上面的表达式等同于:
set(LIB_SRC library.c library.h)
add_library(jinInterface SHARED ${LIB_SRC})

JNI动态注册和静态注册

具体读者可以参考这篇博客【Android】JNI静态与动态注册介绍

system.load()/system.loadLibrary()区别

  • System.load
    System.load 参数必须为库文件的绝对路径,可以是任意路径,例如: System.load(“C:\Documents and
    Settings\TestJNI.dll”); //Windows
    System.load(“/usr/lib/TestJNI.so”); //Linux
  • System.loadLibrary
    System.loadLibrary 参数为库文件名,不包含库文件的扩展名。
    System.loadLibrary (“TestJNI”); //加载Windows下的TestJNI.dll本地库
    System.loadLibrary (“TestJNI”); //加载Linux下的libTestJNI.so本地库

注意:TestJNI.dll或者TestJNI.so必须是在jvm属性java.library.path所指向的路径中。

JNI引用

局部引用

通过NewLocalRef和各种JNI接口创建(FindClass、NewObject、GetObjectClass和NewCharArray等)。会阻止GC
回收所引用的对象,不能本地函数中跨函数使用,不能跨线前使用。函数返回后局部引用所引用的对象会被JVM自动
释放,或调用DeleteLocalRef释放。 (env)->DeleteLocalRef(env,local_ref)。

jclass cls_string = (env)->FindClass(env, "java/lang/String");
jcharArray charArr = (env)->NewCharArray(env, len);
jstring str_obj = (env)->NewObject(env, cls_string, cid_string, elemArray);
jstring str_obj_local_ref = (env)->NewLocalRef(env,str_obj); // 通过NewLocalRef函数创建
全局引用

调用NewGlobalRef基于局部引用创建,会阻GC回收所引用的对象。可以跨方法、跨线程使用。JVM不会自动释放,
必须调用DeleteGlobalRef手动释放 (env)->DeleteGlobalRef(env,g_cls_string);

static jclass g_cls_string;
void TestFunc(JNIEnv* env, jobject obj) {
   jclass cls_string = (env)->FindClass(env, "java/lang/String");
   g_cls_string = (env)->NewGlobalRef(env,cls_string);
}
弱全局引用

调用NewWeakGlobalRef基于局部引用或全局引用创建,不会阻止GC回收所引用的对象,可以跨方法、跨线程使
用。引用不会自动释放,在JVM认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放。或调用
DeleteWeakGlobalRef手动释放。(env)->DeleteWeakGlobalRef(env,g_cls_string)

static jclass g_cls_string;
void TestFunc(JNIEnv* env, jobject obj) {
    jclass cls_string = (*env)->FindClass(env, "java/lang/String");
    g_cls_string = (*env)->NewWeakGlobalRef(env,cls_string);
}
野指针

野指针指向一个已删除的对象或未申请访问受限内存区域的指针。通俗的讲,就是该指针就像野孩子一样,不受程序
控制,不知道该指针指向了什么地址。 与空指针不同,野指针无法通过简单地判断是否为NULL避免,而只能通过养
成良好的编程习惯来尽力减少。对野指针进行操作很容易造成程序错误。
野指针的问题在于,指针指向的内存已经无效了,而指针没有被置空,此时指针随机指向某个地址。引用一个非空的
无效指针是一个未被定义的行为,也就是说不一定导致段错误,野指针很难定位到是哪里出现的问题,在哪里这个指
针就失效了,不好查找出错的原因。所以调试起来会很麻烦,有时候会需要很长的时间。
因此,要想彻底地避免野指针,最好的办法就是养成一个良好的编程习惯。
1)初始化指针时将其置为NULL,之后再对其进行操作。
2)释放指针时将其置为NULL,最好在编写代码时将free()函数封装一下,在调用free()后就将指针置为NULL。

  • 如下引用就带来了野指针:
JNIEXPORT jstring JNICALL Java_Refrence_newString(JNIEnv * env, jobject jobj,jint len){
   jcharArray elemArray;
   jchar *chars = NULL;
   jstring j_str = NULL;
   // 定义静态的局部变量
   static jclass cls_string = NULL;
   static jmethodID cid_string = NULL;
   if (cls_string == NULL) {
      printf("cls_string is null \n");
      cls_string = (*env)->FindClass(env, "java/lang/String");
      if (cls_string == NULL) {
         return NULL;
      }
   }
   if(cid_string == NULL) {
      printf("cid_string is null \n");
      cid_string = (*env)->GetMethodID(env, cls_string, "<init>", "([C)V");
      if (cid_string == NULL) {
         return NULL;
      }
   }
   printf("this is a line -------------\n");
   elemArray = (*env)->NewCharArray(env, len);
   j_str = (*env)->NewObject(env, cls_string, cid_string, elemArray);
   (env)->DeleteLocalRef(env, elemArray);
   printf("end of function \n");
   return j_str;
}
static jclass cls_string = NULL;
static jmethodID cid_string = NULL;
以上两个变量都是静态的局部变量。
静态局部变量的作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量
离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不
变。换句话说,静态局部变量在函数内有效,由于是静态变量,所以当再次调用这个函数的时候,静态变量的值继续存在着。

cls_string/cid_string 这两个静态局部变量的值是 局部引用,局部引用的特点是:函数返回后局部引用所引用的对象
会被JVM自动释放。这样一来,给我们的结果就是:静态局部变量的值 所指向的内容被释放,出现野指针异常。

  • 解决方法
env -> DeleteLocalRef(env, cls_string);
cls_string = NULL;
// 此处的 delete不能存在,因为 cid_string不是jobject,应用只需要对object类型的引用而言的,
// (*env)->DeleteLocalRef(env, cid_string);
cid_string = NULL;
总结
  1. javaVm是全局的(能够跨越线程、函数),它是绑定当前进程,在jni中无论是子线程调用jni函数还是主线程调用jni函数都是只有一个相同的地址。
  2. JNIEnv是线程绑定的(不能跨线程,否则会奔溃,可以跨函数【解决方案:调用jvm的AttachCurrentThread方法将env附加在当前的异步线程中】),即不同的线程都拥有各自的env,不同的类只要是运行在主线程中env都是一样的,子线程也是如此。
  3. jobject:谁调用jni函数(不能跨越线程、函数,否则会奔溃,【解决方式:调用env的NewGlobalReft提升全局引用即可】),谁的实例就会给到jobject。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值