Android JNI學(xué)習(xí)手冊

一、JNI基礎(chǔ)學(xué)習(xí)-JNI調(diào)用java原生方法


class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        sample_text.setOnClickListener {

            callMethod("lilei", 18)
        }
    }

    external fun callMethod(name: String, age: Int)

    companion object {

        // Used to load the 'native-lib' library on application startup.
        init {
            System.loadLibrary("native-lib")
        }
    }
}

package com.microtechmd.jnidemo;

public class Student {

    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = 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;
    }

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

package com.microtechmd.jnidemo;

public class Person {

    private void setStudent(Student student){
        Log.d("dsh", "setStudent: "+student.toString());
    }

    public static String logcat(){

        Log.d("dsh", "log : ");
    }
}


extern "C" JNIEXPORT void JNICALL
Java_com_microtechmd_jnidemo_MainActivity_callMethod(
        JNIEnv *env,
        jobject jo /* this */, jstring name, jint age) {

    //創(chuàng)建Student對象
    const char *student_class_str = "com/microtechmd/jnidemo/Student";

    //獲取student class
    jclass student_class = env->FindClass(student_class_str); 
    //根據(jù)class獲取student對象
    jobject student_obj = env->AllocObject(student_class);
    //獲取setName 方法iD
    jmethodID setName_ID = env->GetMethodID(student_class, "setName", "(Ljava/lang/String;)V");
    //執(zhí)行setName方法
    env->CallVoidMethod(student_obj, setName_ID, name);

    jmethodID setAge_ID = env->GetMethodID(student_class, "setAge", "(I)V");
    env->CallVoidMethod(student_obj, setAge_ID, age);

    const char *person_class_str = "com/microtechmd/jnidemo/Person";
    jclass person_class = env->FindClass(person_class_str);
    jobject person_object = env->AllocObject(person_class);
    jmethodID setStudent_ID = env->GetMethodID(person_class, "setStudent",
                                               ("(Lcom/microtechmd/jnidemo/Student;)V"));

      /執(zhí)行普通方法 需要對象和方法id、參數(shù)??偨Y(jié)類比java靜態(tài)方法和普通方法的調(diào)用
    env->CallVoidMethod(person_object, setStudent_ID, student_obj);

   //獲取靜態(tài)方法 ,不需要person對象
    jmethodID log_ID = env->GetStaticMethodID(person_class, "logcat",
                                               ("()V"));
    //執(zhí)行靜態(tài)方法                                            
    jstring string_obj = static_cast<jstring> ( env->CallStaticVoidMethod(person_class,log_ID));
    const char *stringChar = env->GetStringUTFChars(string_obj,0);
    env->ReleaseStringUTFChars(string_obj,stringChar); //回收對象

    //JIN調(diào)用接口,有點(diǎn)類比Java發(fā)射
}

JNI調(diào)用java原生方法有四個重要的東西

一、class 類信息

二、method 方法信息

三、sign 方法簽名 ,里面包括了方法的參數(shù)類型信息 和返回信息,如(Ljava/lang/String;)V 代表的就是 void xxx(String)方法;其中構(gòu)造方法用 ,多個參數(shù)的方法這樣表示 (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; 代表 String xxxx(String , String )

四、實(shí)例對象

二、JNI基礎(chǔ)學(xué)習(xí)-String的處理

java傳給c一個string,javah生成了方法名后,

發(fā)現(xiàn)傳遞來的是一個jstring(因?yàn)樵赾里,是沒有string的),

public class Jni {
    static {
        System.loadLibrary("native-lib");
    }
    public native String study_string(String str);
}

//生成的頭文件
JNIEXPORT jstring JNICALL Java_jni_study_com_cvmars_Jni_study_1string
  (JNIEnv *, jobject, jstring);

傳遞來的是一個jstring(因?yàn)樵赾里,是沒有string的), jstring其實(shí)是void*(任意類型)

我們需要調(diào)用一個方法,把jstring轉(zhuǎn)為C語言的char*類型,先看下這個工具方法:

#include <stdlib.h>

/**
 * 把一個jstring轉(zhuǎn)換成一個c語言的char* 類型.
 */
char* _JString2CStr(JNIEnv* env, jstring jstr) {
    char* rtn = NULL;
    jclass clsstring = (*env)->FindClass(env, "java/lang/String");
    jstring strencode = (*env)->NewStringUTF(env,"GB2312");
    jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B");
    jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid, strencode); // String .getByte("GB2312");
    jsize alen = (*env)->GetArrayLength(env, barr);
    jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
    if(alen > 0) {
        rtn = (char*)malloc(alen+1); //"\0"
        memcpy(rtn, ba, alen);
        rtn[alen]=0;
    }
    (*env)->ReleaseByteArrayElements(env, barr, ba,0);
    return rtn;
}

實(shí)現(xiàn)native方法

JNIEXPORT jstring JNICALL Java_jni_study_com_jnibsetpractice_Jni_transe_1string
        (JNIEnv *env, jobject instance, jstring jstr) {

    //把一個jstring轉(zhuǎn)換成一個c語言的char* 類型
    char *cStr = _JString2CStr(env, jstr);
    //c語言拼接字符串
    char *cNewStr = strcat(cStr, "簡單加密一下哈哈哈!!!");
    // 把c語言里的char* 字符串轉(zhuǎn)成java認(rèn)識的字符串
    return (*env)->NewStringUTF(env, cNewStr);
}

三、JNI基礎(chǔ)學(xué)習(xí)-在C里輸出log的辦法

  1. 在C 里輸入
image
  1. 在Android.mk里輸入
image
  1. 使用log
image
LOGD("length = %d", length);

四、JNI基礎(chǔ)學(xué)習(xí)- 數(shù)據(jù)類型和簽名機(jī)制

由于Java語言與C/C++語言數(shù)據(jù)類型的不匹配,需要單獨(dú)定義一系列的數(shù)據(jù)類型轉(zhuǎn)換關(guān)系來完成兩者之間的對等(或者說是映射)。下面給出jni與Java數(shù)據(jù)類型對應(yīng)表(jni類型均被定義在jni.h頭文件中),如下表1和表2,在jni函數(shù)中,需要使用以下jni類型來等價與Java語言對應(yīng)的類型。

1.基本類型對照表

<style> td {white-space:pre-wrap;border:1px solid #dee0e3;}</style> <byte-sheet-html-origin data-id="Oe3MXsb6ys-1611024151055" data-version="1" data-is-embed="true"><colgroup><col width="249"><col width="258"><col width="270"></colgroup>
| Java類型 | JNI類型 | 描述 |
| boolean | Jboolean | 無符號8位 |
| byte | Jbyte | 無符號8位 |
| char | Jchar | 無符號16位 |
| short | Jshort | 有符號16位 |
| int | Jint | 有符號32位 |
| long | Jlong | 有符號64位 |
| float | Jfloat | 有符號32位 |
| double | Jdouble | 有符號64位 |</byte-sheet-html-origin>

2.引用類型對照表

<style> td {white-space:pre-wrap;border:1px solid #dee0e3;}</style> <byte-sheet-html-origin data-id="qhTSRvgq9c-1611024151061" data-version="1" data-is-embed="true"><colgroup><col width="341"><col width="440"></colgroup>
| Java引用類型 | JNI類型 |
| boolean[] | jbooleanArray |
| byte[] | jbyteArray |
| char[] | jcharArray |
| short[] | jshortArray |
| int[] | jintArray |
| long[] | jlongArray |
| float[] | jfloatArray |
| double[] | jdoubleArray |
| All objects | jobject |
| java.lang.Class | jclass |
| java.lang.String | jstring |
| Object[] | jobjectArray |
| java.lang.Throwable | jthrowable |</byte-sheet-html-origin>

1 深入理解JNIEnv

上面列出了JNI自定義類型,而為了操作這些類型,尤其是引用類型,就需要JNIEnv來協(xié)助完成。那么,什么是JNIEnv呢?實(shí)際上,JNIEnv的實(shí)體是一個名為JNINativeInterface的結(jié)構(gòu)體,而這個結(jié)構(gòu)體又是什么呢?JNINativeInterface結(jié)構(gòu)體定義在頭文件jni.h中,是一個復(fù)雜的函數(shù)指針集合,每一個函數(shù)指針又會指向一個本地實(shí)現(xiàn)函數(shù),來完成特定的功能。諸如常見的New StringUTF,F(xiàn)indClass都定義在其中,如下列出了部分內(nèi)容:

/*  jni.h */#if defined(__cplusplus)typedef _JNIEnv JNIEnv;     // C++typedef _JavaVM JavaVM;#elsetypedef const struct JNINativeInterface* JNIEnv;     // Ctypedef const struct JNIInvokeInterface* JavaVM;#endifstruct JNINativeInterface {
    …
    jclass      (*FindClass)(JNIEnv*, const char*);
    …
    jstring     (*NewString)(JNIEnv*, const jchar*, jsize);
   …
   void        (*SetCharArrayRegion)(JNIEnv*, jcharArray,
                        jsize, jsize, const jchar*);
  …
    jint    (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*,
                        jint);
  …
    jint        (*GetJavaVM)(JNIEnv*, JavaVM**);
  …./* added in JNI 1.6 */// … 表示省略了部分內(nèi)容
 };

下圖來幫助理解這個復(fù)雜的指向關(guān)系:

image

有了JNIEnv*指針,就可以使用函數(shù)指針調(diào)用特定的實(shí)現(xiàn)函數(shù),來完成特定需求的功能。需要注意的是,env變量是線程線程相關(guān)的,不可從一個線程傳遞env變量到另外一個線程。

那么又是如何使線程獲得這個JNIEnv結(jié)構(gòu)體指針的呢?這里涉及到一個重要的函數(shù)JNI_OnLoad(JavaVM* vm,void* reserved),當(dāng)通過System. loadLibrary()方法來加載我們指定的動態(tài)庫(如.so庫)時,Java虛擬機(jī)會檢測庫中是否實(shí)現(xiàn)了JNI_OnLoad函數(shù),如果實(shí)現(xiàn)了則這個函數(shù)就會被調(diào)用,并且一個代表JVM的對象vm被作為參數(shù)傳遞進(jìn)來,這個對象一個進(jìn)程只有一份,可以通過它的AttachCurrentThread方法來獲得JNIEnv*對象,當(dāng)我們的線程完成特定任務(wù)退出之前,應(yīng)該調(diào)用vm的DetachCurrentThread來釋放資源。

上述方法均被定義在jni.h,如下:

/* jni.h */#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;#endif/*
 * JNI invocation interface.
 */struct JNIInvokeInterface {  // C// ....
    jint        (*DestroyJavaVM)(JavaVM*);
    jint        (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
    jint        (*DetachCurrentThread)(JavaVM*);
    jint        (*GetEnv)(JavaVM*, void**, jint);
    jint        (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};
/*
 * C++ version.
 */struct _JavaVM {    // C++const struct JNIInvokeInterface* functions;
#if defined(__cplusplus)
    jint DestroyJavaVM()
    { return functions->DestroyJavaVM(this); }
    jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
    { return functions->AttachCurrentThread(this, p_env, thr_args); }
    jint DetachCurrentThread()
    { return functions->DetachCurrentThread(this); }
    jint GetEnv(void** env, jint version)
    { return functions->GetEnv(this, env, version); }
    jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
    { return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }#endif /*__cplusplus*/
};
/*
 * Prototypes for functions exported by loadable shared libs.  These are
 * called by JNI, not provided by JNI.
 */
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved);
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved);// ....

在JNI_OnLoad()函數(shù)中,也可以通過vm->GetEnv((void*)&env來獲得JNIEnv指針。JNI_OnLoad()函數(shù)基本功能是確定并返回Java虛擬機(jī)支持的JNI版本,我們還可以用作其他用途(諸如做一些初始化工作),一個重要的用途是實(shí)現(xiàn)JNI函數(shù)的動態(tài)注冊。

與JNI_OnLoad()函數(shù)正好相反,當(dāng)共享庫被卸載時,會調(diào)用JNI_OnUnload()函數(shù),我們可以做一些收尾的工作。

2 JNI函數(shù)的注冊過程

在前面講解了JNI函數(shù),并沒有深入探究Java層函數(shù)與jni函數(shù)的對應(yīng)關(guān)系的建立,那么這種關(guān)聯(lián)是怎樣建立的,或者說當(dāng)發(fā)起java native方法的調(diào)用時,是如何找到與之對應(yīng)的jni函數(shù)的呢?這個過程可以分別用靜態(tài)注冊和動態(tài)注冊的方式來完成。其實(shí)前面已經(jīng)講過了靜態(tài)注冊的原理。沒錯!就是命名規(guī)范,按照前面說的來命名jni函數(shù),就可以實(shí)現(xiàn),這里就不再贅述了。接下來,介紹JNI函數(shù)的動態(tài)注冊過程。

何為動態(tài)注冊呢?說的直白點(diǎn)就是手動的參與它的注冊過程,讓JNI函數(shù)在一加載完.so動態(tài)庫后就完成它的注冊過程(使之與對應(yīng)java native函數(shù)關(guān)聯(lián)起來),而不是等到調(diào)用時再來進(jìn)行注冊,以提高調(diào)用效率,并且我們也不用遵守前面的命名規(guī)范了,可以給jni函數(shù)取自己認(rèn)為合適的名字。

要完成這個動態(tài)注冊過程,就需要使用在上面提到過的JNI_OnLoad函數(shù),它是在.so動態(tài)庫加載后就會被調(diào)用的,而這又早于JNI函數(shù)的調(diào)用時機(jī),因此在這個函數(shù)里實(shí)現(xiàn)注冊過程是很合理的。

要完成動態(tài)注冊,方法一可以選擇使用AndroidRuntime類的registerNativeMethods方法來完成注冊,這個方法原型如下:

/*
 * Register native methods using JNI.
 */
static int AndroidRuntime::registerNativeMethods(JNIEnv* env,
    const char* className, const JNINativeMethod* gMethods, int numMethods)
{
    return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}

使用這個函數(shù)需要提供包含進(jìn)行注冊的jni函數(shù)的類的全路徑(如項(xiàng)目中的OkayOps 類,全路徑為com/yu/ops/OkayOps),要進(jìn)行注冊的方法信息結(jié)構(gòu)體數(shù)組(JNINativeMethod)及方法個數(shù)。JNINativeMethod是一個C結(jié)構(gòu)體,用于存儲Java native方法與JNI函數(shù)的一一對應(yīng)關(guān)系,包含的信息有native方法名、函數(shù)簽名、函數(shù)指針。它的定義如下所示:

typedef struct {
const char* name; // Java層聲明的native函數(shù)的名字,不需要帶路徑  。
const char* signature; // Java層聲明的native函數(shù)簽名信息,用字符串表示
void*  fnPtr;   //JNI 層對應(yīng)函數(shù)的函數(shù)指針,它的類型void*
} JNINativeMethod;

上面涉及一個新概念函數(shù)簽名,現(xiàn)在只需知道它是用來標(biāo)識匹配哪個java的native方法即可,為了分析注冊過程的條理清晰,將在下一節(jié)詳細(xì)介紹。在registerNativeMethods方法的最后又調(diào)用了jniRegisterNativeMethods方法來完成注冊,這個函數(shù)是在JNIHelp.h中聲明(Android提供的幫助類來方便使用jni,路徑android/libnativehelper/include/nativehelper/JNIHelp.h,實(shí)現(xiàn)在JNIHelp.cpp),可以先來看看這個方法:

/* JNIHelp.cpp */
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    JNIEnv* e = reinterpret_cast<JNIEnv*>(env);

    ALOGV("Registering %s's %d native methods...", className, numMethods);
    // 獲取指定類名的Class對象,并存儲在局部引用中
    scoped_local_ref<jclass> c(env, findClass(env, className));
    if (c.get() == NULL) {  // 獲取class對象為NULL
        char* tmp;
        const char* msg;
        if (asprintf(&tmp,
                     "Native registration unable to find class '%s'; aborting...",
                     className) == -1) {
            // Allocation failed, print default warning.
            msg = "Native registration unable to find class; aborting...";
        } else {
            msg = tmp;
        }
        e->FatalError(msg);
    }
     // 調(diào)用JNIEnv的RegisterNatives來完成注冊
    if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
        char* tmp;
        const char* msg;
        if (asprintf(&tmp, "RegisterNatives failed for '%s'; aborting...", className) == -1) {
            // Allocation failed, print default warning.
            msg = "RegisterNatives failed; aborting...";
        } else {
            msg = tmp;
        }
        e->FatalError(msg);
    }

    return 0;
}

可以發(fā)現(xiàn),jniRegisterNativeMethods函數(shù)并不是具體實(shí)現(xiàn),最終它會調(diào)用JNIEnv的RegisterNatives函數(shù)來完成JNI函數(shù)的注冊。到此注冊過程分析完成,終究是回到JNIEnv上。下面看看RegisterNatives函數(shù)原型:

jint (*RegisterNatives) (JNIEnv* env, jclass clazz, const JNINativeMethod* gMethods , jint numMethods);

可以發(fā)現(xiàn),它和AndroidRuntime::registerNativeMethods函數(shù)的參數(shù)較為類似,除了第二個參數(shù)不同以外,其他均相同。而第二個參數(shù)正是要進(jìn)行動態(tài)注冊的類的Class運(yùn)行時類,可以使用JNIEnv的FindClass函數(shù)來獲取。

第二種進(jìn)行動態(tài)注冊的方式就是基于上面的分析,即:第一步,使用JNIEnv的FindClass函數(shù)來拿到需要進(jìn)行動態(tài)注冊的類的運(yùn)行時Class類;第二步,直接使用JNIEnv的RegisterNatives函數(shù)來完成JNI函數(shù)的注冊。

到此,我們分析了兩種方案來完成JNI函數(shù)動態(tài)注冊的目標(biāo)。第一種,分析了使用AndroidRuntime::registerNativeMethods函數(shù)來完成動態(tài)注冊的流程,使用該函數(shù)總體上來說使用方便,但流程較為復(fù)雜,第二種,使用JNIEnv的RegisterNatives函數(shù)完成動態(tài)注冊,這種方法流程簡單,但需要自個獲取運(yùn)行時Class類,稍顯得煩瑣點(diǎn)。

本文實(shí)現(xiàn)注冊的代碼如下:

// 需要注冊的方法信息表
static JNINativeMethod method_table[] = {
        {"NativeReadOkayData", "([B)I", (void*)Java_android_com_read_yu_data},
    {"NativeWriteOkayData", "([BI)I", (void*) Java_android_com_write_yu_data}, 
};

// 包含本地方法的類的全路徑
static const char* classPathName="com/yu/ops/OkayOps";

// 使用AndroidRuntime的registerNativeMethods方法來完成注冊
static int register_com_yu_signature_ops(JNIEnv *env)
{
    LOGI("register_com_yu_ops_OkayOps");

    return AndroidRuntime::registerNativeMethods(env,classPathName,method_table,NELEM(method_table));
}
// 加載動態(tài)庫的時候被回調(diào)
jint JNI_OnLoad(JavaVM* vm,void* reserved)
{
 LOGI("JNI_OnLoad");
 JNIEnv* env = NULL;
 jint result = -1;
 if(vm->GetEnv((void**)&env,JNI_VERSION_1_6) != JNI_OK)  
{
    goto bail;
 }

 LOGI("register method");

 if(register_com_yu_signature_ops(env) < 0)  // 注冊
 {
    goto bail;
 }
 init();  // 做一些初始化工作
 return JNI_VERSION_1_6;
bail:
 return result;
}

3 簽名機(jī)制

在上面動態(tài)注冊小節(jié)提到一個函數(shù)簽名(signature)的概念,這是用來干什么的呢?了解java語言的都知道它有一種方法重載機(jī)制,因此,為了能夠調(diào)用正確的java層native方法,光憑方法名稱是不夠的,還需要知道它的具體參數(shù)與返回值。函數(shù)簽名就是函數(shù)的參數(shù)與返回值的結(jié)合體,用來進(jìn)行精準(zhǔn)匹配。

函數(shù)簽名由字符串組成,第一部分是包含在圓括號()里的,用來說明參數(shù)類型,第二部分則跟的是返回值類型。比如”([Ljava/lang/Object;)Z”就是參數(shù)為Object[],返回值是boolean的函數(shù)的簽名。下表列出類型與簽名標(biāo)識的對應(yīng)關(guān)系:

| Java類型 | 類型標(biāo)識 |
| boolean | Z |
| byte | B |
| char | C |
| short | S |
| int | I |
| long | J |
| float | F |
| double | D |
| String | L/java/lang/String; |
| int[] | [I |
| Object[] | [L/java/lang/Object; |

int[]的標(biāo)識是[I,其他基本數(shù)據(jù)類型的標(biāo)識基本類似,用[+類型標(biāo)識組合。需要注意的是,除了基本數(shù)據(jù)類型的數(shù)組以外,引用類型的標(biāo)識后都需要跟上一個分號。一般,人為的寫簽名字符串難免會出錯,而且類型簽名標(biāo)識又難以記憶,所幸的是java提供了相關(guān)命令來快速生成簽名信息。到要生成簽名的項(xiàng)目的bin目錄下,使用javap命令加 –s選項(xiàng)來快速生成簽名信息,如下:

D:\code\yu_jar\bin>javap -s com.yu.ops.OkayOps
Compiled from "OkayOps.java"
public class com.yu.ops.OkayOps {
  public java.lang.String SERVICE;
    descriptor: Ljava/lang/String;
  static {};
    descriptor: ()V
  public com.yu.ops.OkayOps();
    descriptor: ()V
  public final int yu_read(byte[]);
    descriptor: ([B)I
  public final void yu_write(byte[]);
    descriptor: ([B)V
  public native int NativeReadOkayData(byte[]);
    descriptor: ([B)I
  public native int NativeWriteOkayData(byte[], int);
    descriptor: ([BI)I
}

在方法下面的descriptor的內(nèi)容即是所需要的簽名信息。簽名信息比較有用,在JNI函數(shù)的調(diào)用中,經(jīng)常會需要以簽名作為參數(shù)。

在jni.h頭文件我們可以看到基本類型方法簽名定義,如下:

typedef union jvalue {
    jboolean    z;
    jbyte       b;
    jchar       c;
    jshort      s;
    jint        i;
    jlong       j;
    jfloat      f;
    jdouble     d;
    jobject     l;
} jvalue;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容