[ndk,2]ndk开发案例和错误处理

浏览:
字体:
发布时间:2013-12-23 12:22:31
来源:

一、编译、连接、交叉编译:

编译:把一个源文件 源代码 翻译(编译)成一个二进制文件的过程

 

连接:把编译生成的二进制,根据操作系统,根据当前处理器的类型.把这个二进制文件转化成一个真正可以执行的二进制文件.

 

 

交叉编译: 在一个操作系统平台(一种cpu的平台)上 编译出来另外一个操作系统平台上(另外一种cpu上)可以运行的代码.

 

 

.c-> .exe

.c-> elf 可执行的二进制文件

 

 

 

交叉编译,借助于一个工具链,工具链可以模拟目标平台的一些环境,把编译后的二进制文件链接成一个目标平台下可以执行的二进制代码.

 

二、ndk开发步骤:

1.创建一个android工程

2.JAVA代码中写声明native 方法 public native String helloFromJNI();

3.创建jni目录,编写c代码,方法名字要对应

4.编写Android.mk文件

5.Ndk编译生成动态库

6.Java代码load 动态库.调用native代码

 

三、ndk 开发入门 案例:使用java调用底层c代码返回返回字符串。

1、创建一个android工程:

2、在DemoActivity类中定义一个 native方法:

 

public class DemoActivityextends Activity {

 

//声明本地native方法 注意没有方法体

public native String helloFromJNI();

}

3、在应用程序目录下创建一个jni目录:在内部写Hello.c 的c文件。C文件如下:

#include

#include //c与java之间的映射的函数库:

 

//通过 包名_类名_方法名-完成 java-c代码的映射

//方法名固定写法:Java_包名_类名_本地方法名(JNIEnv* env, jobject obj)

 

jstring Java_cn_itcast_ndk_DemoActivity_helloFromC(JNIEnv*env,

jobject obj) {

 

//*env 得到了 JNIEnv

// 因为JNIEnv 是JNINativeInterface的指针类型

// **env 得到JNINativeInterface结构体

char* str = "hellofrom c";

// return (**env).NewStringUTF(env,str);

return (*env)->NewStringUTF(env,str);

}

 

4、在jni目录下 编写Android.mk文件:

该文件中指定要编译的源文件的名称和编译出来的可执行文件的名称。

内容如下:

 

LOCAL_PATH := $(call my-dir)

 

include $(CLEAR_VARS)

 

#告诉编译器编译出来的可执行性文件叫什么名字

LOCAL_MODULE := Hello

#告诉编译器 编译的源文件的名称

LOCAL_SRC_FILES := Hello.c

 

include $(BUILD_SHARED_LIBRARY)

 

5、Ndk编译生成动态库:(交叉编译)

使用Cygwin 工具到达工程的jni目录下/cygdrive/g/androidSpace/myndk/jni

使用ndk-build命令生成一个libHello.so文件,该文件被放在当前工程下的libs/armeabi/目录下。

生成的libHello.so是一个linux系统下可执行的二进制文件。

 

6、使用Java代码load 动态库.调用native代码:

就是将可执行的二进制文件(.so的c代码库文件)加载到java虚拟机中:

在静态代码块使用 System.loadLibrary("");方法

public class DemoActivityextends Activity {

 

static{

//把 的c代码的库文件加载到java虚拟机里面

// 不要写so的前缀lib 也不要写扩展名.so

System.loadLibrary("Hello");

}

 

//声明本地native方法 注意没有方法体

public native StringhelloFromJNI();

 

 

@Override

public voidonCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_demo);

}

 

//点击按钮的事件:

public void click(View v){

String str = helloFromJNI();

 

Toast.makeText(this, str, 1).show();

}

}

四、jdk使用javah工具生成 jni中c代码中的方法名:

在含有native方法的java源文件的目录下,下使用javac 将.java文件编译成.class文件,

再使用javah 就会在当前目录下 生成 .h文件 该文件中有生成对应的c的方法名。

 

G:/androidSpace/myndk/bin/classes>javah com.li.myndk.DemoActivity

五、在实际开发中的NDK的开发步骤:

1.创建一个android工程

2.JAVA代码中写声明native 方法 public native String helloFromJNI();

3.用javah工具生成头文件

来到 工程classes目录下执行javah 命令:javahcom.li.myndk.DemoActivity

命令为: javah 全类名

在classes目录下就生成com_li_myndk_DemoActivity.h文件

 

 

 

4. 在工程下创建jni目录,引入头文件,根据头文件实现c代码

 

将com_li_myndk_DemoActivity.h文件拷贝到jni目录下

 

编写Hello.c文件: 在.C文件中引入 .h文件。

#include

#include"cn_itcast_ndk2_DemoActivity.h"

char* str = "abc ";

// return (**env).NewStringUTF(env, str);

return (*env)->NewStringUTF(env, str);

 

}

 

5.编写Android.mk文件

6.Ndk编译生成动态库

7.Java代码load 动态库.调用native代码

 

六、ndk开发的常见错误:

1. 忘记写android.mk文件,或者路径不正确

Android NDK: Your APP_BUILD_SCRIPT pointsto an unknown file: ./jni/Android.mk

 

2. android.mk文件语法错误

jni/Android.mk:4: *** 遗漏分隔符 。 停止。

 

3. c代码的语法出现问题

make: ***[obj/local/armeabi/objs/Hello/Hello.o] Error 1

 

一般出现了error 1 错误 是c代码的语法出现了问题.

需要先去查看第一个error对应的内容

error: 'intt' undeclared (first use inthis function)

 

 

4.加载库文件出错,库文件不存在 Library Hel1o not found

06-03 03:42:03.817:ERROR/AndroidRuntime(13632): Caused by: java.lang.UnsatisfiedLinkError: LibraryHel1o not found

06-03 03:42:03.817:ERROR/AndroidRuntime(13632): atjava.lang.Runtime.loadLibrary(Runtime.java:461)

06-03 03:42:03.817:ERROR/AndroidRuntime(13632): atjava.lang.System.loadLibrary(System.java:557)

06-03 03:42:03.817:ERROR/AndroidRuntime(13632): atcn.itcast.ndk2.DemoActivity.(DemoActivity.java:9)

06-03 03:42:03.817:ERROR/AndroidRuntime(13632): ... 15more

 

 

5.c的代码库没有被加载到java虚拟机里面(1.忘记加载了c代码库,2.c代码库里面没有与java里面native的方法对应的代码)

06-03 03:43:35.768:ERROR/AndroidRuntime(13972): FATAL EXCEPTION: main

06-03 03:43:35.768:ERROR/AndroidRuntime(13972): java.lang.UnsatisfiedLinkError: hello_from_c

06-03 03:43:35.768: ERROR/AndroidRuntime(13972): atcn.itcast.ndk2.DemoActivity.hello_from_c(Native Method)

 

6.控制台打印Buildfingerprint: 程序界面显示一下就消失了.

原因就是c代码里面的逻辑有问题,参数内存泄露,运行时的异常. 比如指针的类型不匹配

修改通过打印log的方式 定位出来错误究竟出现在哪一行的代码.

 

06-03 03:48:55.132: INFO/DEBUG(31): ****** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

06-03 03:48:55.132: INFO/DEBUG(31): Buildfingerprint: 'generic/sdk/generic/:2.2/FRF91/43546:eng/test-keys'

06-03 03:48:55.132: INFO/DEBUG(31): pid:15204, tid: 15204 >>>cn.itcast.ndk2 <<<

06-03 03:48:55.132: INFO/DEBUG(31):signal 11 (SIGSEGV), fault addr 8090214d

06-03 03:48:55.132: INFO/DEBUG(31): r0 8090214d r1 80902154 r2 00000006 r3 6e61687a

七、在c代码中使用logcat

1、在Android.mk文件增加LOCAL_LDLIBS += -llog

 

#引入liblog.so 文件: 向logcat控制台输出log对应的库文件

LOCAL_LDLIBS += -llog

2、C代码中增加

#include

#defineLOG_TAG "System.out"

#defineLOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)

#defineLOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)

LOGI("info/n");

LOGD("debug/n");

例:

#include

#include"cn_itcast_ndk2_DemoActivity.h"

#include

#define LOG_TAG "System.out"

#define LOGD(...)__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)

#define LOGI(...)__android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)

 

 

JNIEXPORT jstring JNICALLJava_cn_itcast_ndk2_DemoActivity_hello_1from_1c

(JNIEnv * env, jobject obj){

 

 

LOGD("begin");向控制台打印log 打印出begin

//定义一个字符串的常量 (不能被修改的变量)

constchar* str1 = "hello";

LOGD("INITSTR1");

constchar* str2 = "zhang san";

LOGD("initstr2");

LOGD("str1=%s",str1);

LOGD("str2=%s",str2);

 

 

//c代码不允许修改一个字符串的常量

strcat(str1,str2);//合并两个字符串方法

LOGD("strcat");

char*str = "你好 ,中国";

return(*env)->NewStringUTF(env, str);

 

}

八、Ndk开发中的中文乱码问题:

1、ndk-r6以上的版本解决中文乱码问题

①、把c语言的源文件改为 utf-8的格式

②、在调用c代码的时候,模拟器就可以显示出来中文了...

 

 

ndk-r6以上的版本 在编译链接 c语言的源文件的时候 采用的编码格式是utf-8

保证所有的c代码的源文件也要是utf-8

android上在显示 字符串的时候 默认的语言集是utf-8

 

 

2、ndk-r4老的ndk版本, 在编译源代码的采用的编码集是西欧的编码集iso8859-1的编码: 所以在获取中文是iso8859-1的编码格式。

String str = hello_from_c();

new String(str.getBytes("iso8859-1"),"utf-8");

 

九、android.mk 文件详解:

# $ 代表调用一个makefile的函数

# call my-dir这个函数的作用就是获取当前文件所在的目录赋给 LOCAL_PATH变量

LOCAL_PATH := $(call my-dir)

 

# 重新初始化 gnu make的脚本环境

include $(CLEAR_VARS)

#上面的CLEAR_VARS方法 会把所有的编译环境的变量重新初始化,不会初始化 LOCAL_PATH。

 

#告诉编译器编译出来的可执行性文件叫什么名字,编译完以后会变成libHello.so

LOCAL_MODULE := Hello

#告诉编译器 编译的源文件的名称

LOCAL_SRC_FILES := Hello.c

#liblog.so 向logcat控制台输出log对应的库文件

LOCAL_LDLIBS += -llog

#代表将该c代码最终会变成一个动态库 扩展名为.so

include $(BUILD_SHARED_LIBRARY)

 

十、c中的静态库 与动态库:

1、如果在android.mk文件中放置下面这一句:

include$(BUILD_SHARED_LIBRARY)

代表c代码最终会变成一个动态库 扩展名为.so

2、如果在android.mk文件中放置下面这一句:

include $(BUILD_STATIC_LIBRARY)

代表最终c代码会编译成一个静态库 扩展名为.a

3、动态库与静态库比较:

一般来说 ,动态库的体积要比静态库的体积小很多.

 

动态库: 动态的,代码的执行的时候 依赖的函数 依赖的c代码都是动态的加载执行的.

静态库: 静态的,代码在执行之前,所有依赖的库函数 ,所有依赖的代码,都必须预先加载编译到文件里面.

 

4、什么时候使用静态库:

当你的公司,有一个需求:把某一个函数,某一个功能做成一个模块 供别的开发人员使用的时候  

 

十一:ndk编程应用,java传递数据给c, c 返回数据给java

1、创建一个android工程:在内部定义一个类:在类中定义一下几个本地方法:

public class DataProvider{

/**

* 把java中的两个int类型的数据 传递给c语言,

* c语言在得到这两个数据之后,对这两个数据进行求和

* 返回回来的结果 相加后的值

* @param x

* @param y

* @return

*/

public native int add(int x ,int y);

 

/**

* 把java中的一个字符串传递个c语言

* c语言得到这个字符串之后对这个字符串进行一些操作

* 在字符串后面添加一个 你好.

* @param s

* @return

*/

public native StringsayHelloInC(String s);

/**

* 把java中的一个int数组 传递给c语言 ,

* c语言获取到这个int数组之后,把数组的每一个元素的值都+10

* 把相加操作之后 新的数组返回给java代码

* @param iNum

* @return

*/

public native int[] intMethod(int[] iNum); // 图片 或者 音频 视频处理的时候 需要把一个数组传递给c代码

}

2、编写c代码:Hello.c 如下:分别完成以上的操作:

#include

#include "cn_itcast_ndk3_DataProvider.h"

#include

#define LOG_TAG "System.out"

#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG,__VA_ARGS__)

#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG,__VA_ARGS__)

 

/**

* 工具方法

* 作用: 把java中的string 转化成一个c语言中的char数组

* 接受的参数 envjni环境的指针

* jstr 代表的是要被转化的java的string 字符串

* 返回值 : 一个c语言中的char数组的首地址 (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");

// String .getByte("GB2312");

jbyteArray barr=

(jbyteArray)(*env)->CallObjectMethod(env,jstr,mid,strencode);

 

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;

}

//计算两个数和的方法

JNIEXPORT jint JNICALL Java_cn_itcast_ndk3_DataProvider_add

(JNIEnv * env, jobject obj, jint x, jint y){

 

LOGD("x=%d",x);

LOGD("y=%d",y);

int result = x + y;

LOGD("result=%d",result);

return result;

}

 

//拼接字符串的方法

JNIEXPORT jstring JNICALL Java_cn_itcast_ndk3_DataProvider_sayHelloInC

(JNIEnv * env , jobjectobj , jstring jstr){

//1.把java中的string 转化成 c语言里面的char数组

char* cstr = Jstring2CStr(env,jstr);

LOGI("cstr=%s",cstr);

char* hellostr ="hello";

strcat(cstr,hellostr); //拼接两个字符串

LOGI("new cstr=%s",cstr);

return (*env)->NewStringUTF(env,cstr);

}

 

//操作数组的方法

JNIEXPORT jintArray JNICALL Java_cn_itcast_ndk3_DataProvider_intMethod

(JNIEnv * env , jobjectobj , jintArray jarr){

// 获取jarr的长度

// jsize (*GetArrayLength)(JNIEnv*, jarray);

//调用jni方法获取数组的长度

int len =(*env)->GetArrayLength(env,jarr);

LOGI("len =%d",len);

// jint* (*GetIntArrayElements)(JNIEnv*,jintArray, jboolean*);

//获取数组的首地址

int* carr = (*env)->GetIntArrayElements(env,jarr,0);

int i;

for(i=0;i

LOGI("arr[%d]=%d",i,*(carr+i));

*(carr+i) = *(carr+i)+10;

}

return jarr;

 

}

3、注意:

①、在c中接收java中int类型的数据时:由于int类型的数据java和c中的长度相同,c可以直接使用java传递过来int型数据。

②、c接收了java中的字符串数据 ,由于c中没有字符串类型,首先c要先将java的字符串类型转成char数组类型;

在c语言中 字符串的结果都是以/0这样一个标示 作为字符串的结尾

下面是具体的转换方法:

/**

* 工具方法

* 作用: 把java中的string 转化成一个c语言中的char数组

* 接受的参数 envjni环境的指针

* jstr 代表的是要被转化的java的string 字符串

* 返回值 : 一个c语言中的char数组的首地址 (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");

// String.getByte("GB2312");

jbyteArray barr=

(jbyteArray)(*env)->CallObjectMethod(env,jstr,mid,strencode);

 

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;

}

③、在将java 中int类型的数组传递c代码中 ,获取数组的长度,再获取数组的首地址后获取每一个元素。

④、java不会将字符串数组传给c代码, 而是将字符串数组元素用:隔开转成字符串传给c代码.

 

十二、c代码调用java代码:案例:c代码调用java 代码 弹出一个土司.

主要步骤:首先java调用c代码的方法 在c代码中再调用java代码:

1、在java类中定义方法:

public class DemoActivity extends Activity {

static{

System.loadLibrary("Hello");

}

 

public native void callMethod1();

 

//按钮的点击事件

public void showToast(View view){

//1.调用底层的c代码

 

//2.c代码里面 让c语言的代码调用java代码 显示出来一个土司

callMethod1();

}

 

 

//被c调用的方法

public void javaShowToast(String message){

Toast.makeText(this, message, 0).show();

}

}

2、c代码:类似java中的反射,先获取java类的字节码文件,再获取method对象,再执行该方法。

#include

#include "cn_itcast_ndkcallback_DemoActivity.h"

#include

#define LOG_TAG "System.out"

#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG,__VA_ARGS__)

#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG,__VA_ARGS__)

 

//参数env 是JNIEnv的指针

// obj 代表的是调用这个c代码的java对象

JNIEXPORT void JNICALLJava_cn_itcast_ndkcallback_DemoActivity_callMethod1

(JNIEnv * env , jobject obj) {

// 找到 java中的代码javaShowToast 调用他 让java代码显示出来一个土司

//

// Method method = DemoActivity.class.getMethod("javaShowToast",new Class[]{String.class});

// method.invoke(obj, "string");

 

// 1. 寻找要调用的java代码的字节码 class

//jclass (*FindClass)(JNIEnv*, const char*);

//获取java的类的字节码。FindClass方法参数(env ,java类类路径)

jclass jclazz = (*env)->FindClass(env,"cn/itcast/ndkcallback/DemoActivity");

 

// 2.寻找当前jclass里面的方法的id

// jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*,const char*);

//获取字节码中指定方法的id。GetMethodID方法参数(env ,字节码文件,方法名, 该方法签名)

//方法签名通过javap 工具获得;

jmethodID jshowtoastmethod =

(*env)->GetMethodID(env,jclazz,"javaShowToast","(Ljava/lang/String;)V");

 

//3. 调用该方法

// void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);

//执行方法:CallVoidMethod方法参数(env,obj ,方法id,传递到被调用java方法中的参数)

(*env)->CallVoidMethod(env,obj,jshowtoastmethod,(*env)->NewStringUTF(env,"hi from c"));

}

3、注意: 获取方法签名的方法是:在命令行中使用javap 工具:

命令为: javap -s 完整类名 。 就可以获取指定类中所有方法的方法签名。

 

十二、c代码调用java代码方法的常见案例:

1、定义被c代码调用的java的方法

public class DataProvider{

//C调用java空方法

public voidhelloFromJava(){

System.out.println("我是java的空方法");

}

//C调用java中的带两个int参数的方法

public int Add(int x,int y){

int result =x+y;

System.out.println("相加的结果为"+result);

return result;

}

//C调用java中参数为string的方法

public voidprintString(String s){

 

System.out.println("我是java方法"+s);

}

}

2、c代码的方法:

①、调用java中的helloFromJava()方法

//obj 代表的是调用这个c代码的java对象

JNIEXPORT void JNICALLJava_cn_itcast_ndkcallback_DemoActivity_callMethod2

(JNIEnv * env , jobject obj) {

//获取字节码

jclass jclazz =

(*env)->FindClass(env,"cn/itcast/ndkcallback/DataProvider");

// jmethodID (*GetStaticMethodID)(JNIEnv*, jclass, const char*, const char*);

//如果一个java方法是 静态的 就必须通过 GetStaticMethodID

//获取方法的id

jmethodIDjvoidmethod =

(*env)->GetMethodID(env,jclazz,"helloFromJava","()V");

//必须在这个地方 创建出来 dataprovider的对象

// jobject (*AllocObject)(JNIEnv*, jclass);

//创建java中dataprovider类的对象

jobject dpobj =(*env)->AllocObject(env,jclazz);

//调用方法:

(*env)->CallVoidMethod(env,dpobj,jvoidmethod);

 

}

②、调用java中Add(int x,int y)方法:

JNIEXPORT void JNICALLJava_cn_itcast_ndkcallback_DemoActivity_callMethod3

(JNIEnv * env , jobject obj) {

jclass jclazz =

(*env)->FindClass(env,"cn/itcast/ndkcallback/DataProvider");

 

jmethodID jintmethod =(*env)->GetMethodID(env,jclazz,"Add","(II)I");

jobject dpobj =(*env)->AllocObject(env,jclazz);

// jint (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);

//调用带有int类型返回值的方法:

intresult =(*env)->CallIntMethod(env,dpobj,jintmethod,3,5);

LOGI("c resutl=%d",result);

}

 

③、调用java中printString(Strings)方法:

JNIEXPORT void JNICALLJava_cn_itcast_ndkcallback_DemoActivity_callMethod4

(JNIEnv * env , jobject obj) {

 

jclass jclazz =

(*env)->FindClass(env,"cn/itcast/ndkcallback/DataProvider");

 

jmethodID jvoidmethod =

(*env)->GetMethodID(env,jclazz,"printString","(Ljava/lang/String;)V");

jobject dpobj =(*env)->AllocObject(env,jclazz);

 

(*env)->CallVoidMethod(env,dpobj,jvoidmethod,(*env)->NewStringUTF(env,"haha from c"));

}

 

十三 c代码调用java中的静态方法:

1、在类中定义一个静态方法:

public class DataProvider {

//静态方法

public static void hellofromstatic(){

System.out.println("我是静态方法");

}

}

2、c代码:

//obj 代表的是调用这个c代码的java对象

JNIEXPORT void JNICALLJava_cn_itcast_ndkcallback_DemoActivity_callMethod2

(JNIEnv * env , jobject obj) {

//获取类字节码

jclass jclazz =

(*env)->FindClass(env,"cn/itcast/ndkcallback/DataProvider");

// jmethodID (*GetStaticMethodID)(JNIEnv*, jclass, const char*, const char*);

//如果一个java方法是 静态的 就必须通过 GetStaticMethodID

//获取静态方法id 使用GetStaticMethodID方法

jmethodID jvoidmethod =

(*env)->GetStaticMethodID(env,jclazz,"hellofromstatic","()V");

 

/// void (*CallStaticVoidMethod)(JNIEnv*,jclass, jmethodID, ...);

//执行静态方法

(*env)->CallStaticVoidMethod(env,jclazz,jvoidmethod);

}

>更多相关文章
24小时热门资讯
24小时回复排行
资讯 | QQ | 安全 | 编程 | 数据库 | 系统 | 网络 | 考试 | 站长 | 关于东联 | 安全雇佣 | 搞笑视频大全 | 微信学院 | 视频课程 |
关于我们 | 联系我们 | 广告服务 | 免责申明 | 作品发布 | 网站地图 | 官方微博 | 技术培训
Copyright © 2007 - 2024 Vm888.Com. All Rights Reserved
粤公网安备 44060402001498号 粤ICP备19097316号 请遵循相关法律法规
');})();