首页 > 基础资料 博客日记

JNA实践之Java模拟C结构体、结构体指针、结构体数组

2024-09-15 20:00:07基础资料围观141

这篇文章介绍了JNA实践之Java模拟C结构体、结构体指针、结构体数组,分享给大家做个参考,收藏Java资料网收获更多编程知识


第一次写JNA相关的文章是在21年了,中间由于一些事情把后续搁置了,现在补上Java模拟结构体、结构体指针以及结构体中嵌套结构体数组。如果对JNA还不了解,可以先阅读 JNA基础之Java映射char*、int*、float*、double*一文。

1 JNA模拟C结构体

        要使用Java类模拟C的结构体,需要Java类继承Structure类。必须注意,继承Structure类的子类中的公共成员的顺序,必须与C语言结构体中变量的顺序保持一致,否则会报错!因为,Java 调用动态链接库中的C 函数,实际上就是一段内存作为函数的参数传递给C函数。动态链接库以为这个参数就是C 语言传过来的参数。同时,C 语言的结构体是一个严格的规范,它定义了内存的次序。因此,JNA 中模拟的结构体的变量顺序绝对不能错。
        如果一个Struct有2个int变量int a, int b,如果JNA中的顺序和C语言中的顺序相反,那么不会报错,但是数据将会被传递到错误的变量中去。
        Structure 类代表了一个原生结构体。当Structure 对象作为一个函数的参数或者返回值传递时,它代表结构体指针。当它被用在另一个结构体内部作为一个字段时,它代表结构体本身。
        另外,Structure 类有两个内部接口Structure.ByReference 和Structure.ByValue。这两个接口仅仅是标记,如果一个类实现Structure.ByReference 接口,就表示这个类代表结构体指针。如果一个类实现Structure.ByValue接口,就表示这个类代表结构体本身。如果不实现这两个接口,那么就相当于你实现了Structure.ByReference接口。使用这两个接口的实现类,可以明确定义我们的Structure 实例表示的是结构体指针还是结构体本身。

1.1 结构体本身作参数

假设有这样一个C语言结构体:

struct User{
    long id;
    char* name;
    int age;
};

使用上述结构体的函数:

extern "C"{
void sayUser(User user);
}
void sayUser(User user){
    printf("id:%ld\n",user.id);
    printf("name:%s\n",user.name);
    printf("age:%d\n",user.age);
}

JNA中可以这样写:

public class Test {
    public interface CLibrary extends Library {

        CLibrary INSTANCE = (CLibrary) Native.loadLibrary("/libJNADEMO.so", CLibrary.class);
		
		public static class UserStruct extends Structure{
            public NativeLong id;
            public String name;
            public int age;
            public static class ByReference extends UserStruct implements Structure.ByReference{}
            public static class ByValue extends  UserStruct implements Structure.ByValue{}

            @Override
            protected List getFieldOrder(){
                return Arrays.asList(new String[] {"id", "name", "age"});
            }
        }
        void sayUser(UserStruct.ByValue user);
	}
	
	public static void main(String[] args) {
		CLibrary.UserStruct.ByValue user = new CLibrary.UserStruct.ByValue();
		user.id = new NativeLong(100001);
		user.name = "张三";
		user.age = 29;
		CLibrary.INSTANCE.sayUser(user);
	}
}

运行结果如下:

1.2 结构体指针作参数

仍然使用1.1中的结构体,C函数修改为下面的:

extern "C"{
	void sayUser(User* user);
}
void sayUser(User *user){
    printf("use structure pointer\n");
    printf("id:%ld\n",user->id);
    printf("name:%s\n",user->name);
    printf("age:%d\n",user->age);
}

JNA代码中,修改函数声明为:
void sayUser(UserStruct.ByReference user);
JNA main函数中修改为:

CLibrary.UserStruct.ByReference user = new CLibrary.UserStruct.ByReference();
user.id = new NativeLong(100390301);;
user.name = "test";
user.age = 29;
CLibrary.INSTANCE.sayUser(user);

运行结果如下:

1.3 结构体内部嵌套结构体(结构体本身作参数)

C语言最复杂的数据类型就是结构体。结构体的内部可以嵌套结构体,这使它可以模拟任何类型的对象。JNA 也可以模拟这类复杂的结构体,结构体内部可以包含结构体对象指针的数组。
假设有如下结构体:

struct User{
    long id;
    char* name;
    int age;
};

struct CompanyStruct{
    long id;
    const char* name;
    User users[3];
    int count;
};

如下的C函数:

// 头文件函数声明
extern "C"{
	void showNestedStruct(CompanyStruct cst); //结构体本身做参数
}
// 函数定义
void showNestedStruct(CompanyStruct cst){
    printf("This is nested struct\n");
    printf("company id is:%ld\n",cst.id);
    printf("company name:%s\n",cst.name);
    for (int i = 0; i < 3; i++){
        printf("user[%d] info of company\n",i);
        printf("user id:%ld\n",cst.users[i].id);
        printf("user name:%s\n",cst.users[i].name);
        printf("user age:%d\n",cst.users[i].age);
    }
    printf("count %d\n",cst.count);
}

JNA代码:

import com.sun.jna.*;
import java.util.Arrays;
import java.util.List;

public class Test {
    public interface CLibrary extends Library {

        CLibrary INSTANCE = (CLibrary) Native.loadLibrary("/libJNADEMO.so", CLibrary.class);


        public static class UserStruct extends Structure{
            public NativeLong id;
            public String name;
            public int age;
            public static class ByReference extends UserStruct implements Structure.ByReference{}
            public static class ByValue extends  UserStruct implements Structure.ByValue{}

            @Override
            protected List getFieldOrder(){
                return Arrays.asList(new String[] {"id","name","age"});
            }
        }

        public static class CompanyStruct extends Structure{
            public NativeLong id;
            public String name;
            public UserStruct.ByValue[] users = new CLibrary.UserStruct.ByValue[3];
            public int count;
            public static class ByReference extends CompanyStruct implements Structure.ByReference{};
            public static class ByValue extends CompanyStruct implements Structure.ByValue{};

            @Override
            protected List getFieldOrder(){
                return Arrays.asList(new String[] {"id","name","users","count"});
            }
        }



        //函数声明, 对应C函数void showNestedStruct(CompanyStruct cst);
        void showNestedStruct(CompanyStruct.ByValue com); 

    } // interface

    public static void main(String[] args) {
        CLibrary.UserStruct.ByValue user1 = new CLibrary.UserStruct.ByValue();
        user1.id = new NativeLong(100001);
        user1.name = "张三";
        user1.age = 19;

        CLibrary.UserStruct.ByValue user2 = new CLibrary.UserStruct.ByValue();
        user2.id = new NativeLong(100002);
        user2.name = "关羽";
        user2.age = 23;

        CLibrary.UserStruct.ByValue user3 = new CLibrary.UserStruct.ByValue();
        user3.id = new NativeLong(100003);
        user3.name = "test";
        user3.age = 25;

        CLibrary.CompanyStruct.ByValue cst = new CLibrary.CompanyStruct.ByValue();
        cst.id = new NativeLong(30001);
        cst.name = "technology";
        cst.count = 3;
        cst.users[0] = user1;
        cst.users[1] = user2;
        cst.users[2] = user3;
        CLibrary.INSTANCE.showNestedStruct(cst); //调用动态库中的函数
    }
}

运行结果:

1.4 嵌套结构体指针作参数

使用1.3中的结构体,假设有如下C函数:

extern "C"{
	void showNestedStructWithPointer(CompanyStruct* cst);
}

函数定义如下

void showNestedStructWithPointer(CompanyStruct *cst){
	printf("This is nested struct pointer\n");
	printf("company id is:%ld\n",cst->id);
	printf("company name:%s\n",cst->name);
	for (int i = 0; i < 3; i++){
		printf("user[%d] info of company\n",i);
		printf("user id:%ld\n",cst->users[i].id);
		printf("user name:%s\n",cst->users[i].name);
		printf("user age:%d\n",cst->users[i].age);
	}
	printf("count %d\n",cst->count);
}

JNA代码:

import com.sun.jna.*;
import java.util.Arrays;
import java.util.List;

public class Test {
    public interface CLibrary extends Library {

        CLibrary INSTANCE = (CLibrary) Native.loadLibrary("/libJNADEMO.so", CLibrary.class);


        public static class UserStruct extends Structure{
            public NativeLong id;
            public String name;
            public int age;
            public static class ByReference extends UserStruct implements Structure.ByReference{}
            public static class ByValue extends  UserStruct implements Structure.ByValue{}

            @Override
            protected List getFieldOrder(){
                return Arrays.asList(new String[] {"id","name","age"});
            }
        }

        public static class CompanyStruct extends Structure{
            public NativeLong id;
            public String name;
            public UserStruct.ByValue[] users = new CLibrary.UserStruct.ByValue[3];
            public int count;
            public static class ByReference extends CompanyStruct implements Structure.ByReference{};
            public static class ByValue extends CompanyStruct implements Structure.ByValue{};

            @Override
            protected List getFieldOrder(){
                return Arrays.asList(new String[] {"id","name","users","count"});
            }
        }



        //函数声明
        void showNestedStructWithPointer(CompanyStruct.ByReference cbr);

    } // interface

    public static void main(String[] args) {
	    System.out.println("showNestedStructWithPointer");
        CLibrary.UserStruct.ByValue user1 = new CLibrary.UserStruct.ByValue();
        user1.id = new NativeLong(100001);
        user1.name = "张三";
        user1.age = 19;

        CLibrary.UserStruct.ByValue user2 = new CLibrary.UserStruct.ByValue();
        user2.id = new NativeLong(100002);
        user2.name = "李四";
        user2.age = 23;

        CLibrary.UserStruct.ByValue user3 = new CLibrary.UserStruct.ByValue();
        user3.id = new NativeLong(100003);
        user3.name = "test";
        user3.age = 25;

        CLibrary.CompanyStruct.ByReference cbr = new CLibrary.CompanyStruct.ByReference();
        cbr.id = new NativeLong(30001);
        cbr.name = "TEST";
        cbr.users[0] = user1;
        cbr.users[1] = user2;
        cbr.users[2] = user3;
        cbr.count = 3;
        CLibrary.INSTANCE.showNestedStructWithPointer(cbr);
    }
}

运行结果

2 结构体中嵌套结构体数组

2.1 用作输入

假设有下面的结构体:

typedef struct
{
    int enable;
    int x;
    int y;
    int width;
    int height;
} area_pos;

typedef struct
{
    int enable;
    int x;
    int y;
} spot_pos;

typedef struct
{
    int enable;
    int sta_x;
    int sta_y;
    int end_x;
    int end_y;
} line_pos;

typedef struct
{
    area_pos area[2];
    spot_pos spot[2];
    line_pos line;
} image_pos;

如下C函数:

extern "C"{
	void get_struct_array_value(image_pos*  img_data);
}

//函数定义
void get_struct_array_value(image_pos *img_data){
    printf("line_pos enable:%d\n",img_data->line.enable);
    printf("line_pos sta_x:%d\n",img_data->line.sta_x);
    printf("line_pos sta_y:%d\n",img_data->line.sta_y);
    printf("line_pos end_x:%d\n",img_data->line.end_x);
    printf("line_pos end_y:%d\n",img_data->line.end_y);
    for (int i = 0; i < 2; i++){
        printf("area_pos[%d] enable:%d\n",i,img_data->area[i].enable);
        printf("area_pos[%d] x:%d\n",i,img_data->area[i].x);
        printf("area_pos[%d] y:%d\n",i,img_data->area[i].y);
        printf("area_pos[%d] width:%d\n",i,img_data->area[i].width);
        printf("area_pos[%d] height:%d\n",i,img_data->area[i].height);
    }
    for (int j = 0; j < 2; j++){
        printf("spot_pos[%d] enable:%d\n",j,img_data->spot[j].enable);
        printf("spot_pos[%d] x:%d\n",j,img_data->spot[j].x);
        printf("spot_pos[%d] y:%d\n",j,img_data->spot[j].y);
    }
}

JNA代码:

import com.sun.jna.*;
import java.util.Arrays;
import java.util.List;

public class Test {
    public interface CLibrary extends Library {

        CLibrary INSTANCE = (CLibrary) Native.loadLibrary("/libJNADEMO.so", CLibrary.class);


        public static class AREAPOS extends Structure{
            public int enable;
            public int x;
            public int y;
            public int width;
            public int height;

            public static class ByReference extends AREAPOS implements Structure.ByReference{};
            public static class ByValue extends AREAPOS implements Structure.ByValue{};

            @Override
            protected List getFieldOrder(){
                return Arrays.asList(new String[] {"enable","x","y","width","height"});
            }
        }

        public static class SPOTPOS extends Structure{
            public int enable;
            public int x;
            public int y;

            public static class ByReference extends SPOTPOS implements Structure.ByReference{};
            public static class ByValue extends SPOTPOS implements  Structure.ByValue{};
            @Override
            protected List getFieldOrder(){
                return Arrays.asList(new String[] {"enable","x","y"});
            }
        }

        public static class LINEPOS extends Structure{
            public int enable;
            public int sta_x;
            public int sta_y;
            public int end_x;
            public int end_y;

            public static class ByReference extends LINEPOS implements Structure.ByReference{};
            public static class ByValue extends LINEPOS implements  Structure.ByValue{};
            @Override
            protected List getFieldOrder(){
                return Arrays.asList(new String[] {"enable","sta_x","sta_y","end_x","end_y"});
            }
        }

        //模拟结构体数组
        public static class IMAGEPOS extends Structure{
            public AREAPOS.ByValue[] area = new AREAPOS.ByValue[2];
            public SPOTPOS.ByValue[] spot = new SPOTPOS.ByValue[2];
            public LINEPOS.ByValue line;

            public static class ByReference extends IMAGEPOS implements Structure.ByReference{};
            public static class ByValue extends IMAGEPOS implements Structure.ByValue{};
            @Override
            protected List getFieldOrder(){
                return Arrays.asList(new String[] {"area","spot","line"});
            }
        }


        //C函数声明
        void get_struct_array_value(IMAGEPOS.ByReference img);

    } // interface

    public static void main(String[] args) {
        CLibrary.AREAPOS.ByValue abv1 = new CLibrary.AREAPOS.ByValue();
        abv1.enable = 1;
        abv1.x = 10;
        abv1.y = 20;
        abv1.height = 1080;
        abv1.width = 1920;

        CLibrary.AREAPOS.ByValue abv2 = new CLibrary.AREAPOS.ByValue();
        abv2.enable = 0;
        abv2.x = 20;
        abv2.y = 10;
        abv2.height = 1920;
        abv2.width = 1080;

        CLibrary.SPOTPOS.ByValue sp1 = new CLibrary.SPOTPOS.ByValue();
        sp1.enable = 0;
        sp1.x = 1;
        sp1.y = 1;

        CLibrary.SPOTPOS.ByValue sp2 = new CLibrary.SPOTPOS.ByValue();
        sp2.enable = 1;
        sp2.x = 2;
        sp2.y = 2;

        CLibrary.LINEPOS.ByValue line = new CLibrary.LINEPOS.ByValue();
        line.enable = 0;
        line.end_x = 10;
        line.end_y = 20;
        line.sta_x = 30;
        line.sta_y = 40;

        CLibrary.IMAGEPOS.ByReference img = new CLibrary.IMAGEPOS.ByReference();
        img.area[0] = abv1;
        img.area[1] = abv2;
        img.spot[0] = sp1;
        img.spot[1] = sp2;
        img.line = line;
        CLibrary.INSTANCE.get_struct_array_value(img);
    }
}

运行结果:

2.2 用作输出

结构体中嵌套结构体数组用作输出参数时,需要对结构体数组的第一个元素赋初值。
结构体定义:

typedef struct
{
	int enable;
	int max_temp;
	int max_temp_x;
	int max_temp_y;
	int min_temp;	
	int min_temp_x;
	int min_temp_y;	
	int ave_temp;
} area_temp;

typedef struct
{
	int enable;
	int temp;
} spot_temp;

typedef struct
{
	int enable;
	int max_temp;
	int max_temp_x;
	int max_temp_y;
	int min_temp;
	int min_temp_x;
	int min_temp_y;
	int ave_temp;
} line_temp;

typedef struct
{
	int max_temp;
	int max_temp_x;
	int max_temp_y;
	int min_temp;
	int min_temp_x;
	int min_temp_y;
} globa_temp;

typedef struct
{
	area_temp area[6];
	spot_temp spot[6];
	line_temp line;
	globa_temp globa;
} image_temp;

C函数声明:

extern "C" {
	int sdk_get_all_temp_data(const char* ip, image_temp* all_data);
}

PS:此处用到的是第三方sdk的so,函数定义就没有了,只做参考用。
area_temp、spot_temp、line_temp、globa_temp结构体参考章节1中的写法,此处只写了复杂的结构体在JNA中的写法,嵌套的结构体数组JNA代码如下:

import com.sun.jna.Structure;
import java.util.Arrays;
import java.util.List;

public class ImageTemp extends Structure {
    public AreaTemp.ByValue[] area = new AreaTemp.ByValue[6];
    public SpotTemp.ByValue[] spot = new SpotTemp.ByValue[6];
    public LineTemp.ByValue line = new LineTemp.ByValue();
    public GlobaTemp.ByValue globa = new GlobaTemp.ByValue();

    public static class ByReference extends ImageTemp implements Structure.ByReference{}
    public static class ByValue extends ImageTemp implements Structure.ByValue{}

    @Override
    protected List getFieldOrder(){
        return Arrays.asList("area","spot","line","globa");
    }
}

测试代码:

@Test
public void sdkGetAllTempDataTest(){
	A8SDK sdk = new A8SDK();
	ImageTemp.ByReference itbr = new ImageTemp.ByReference();
	int size = itbr.size();
	System.out.println("size="+size);


	AreaTemp.ByValue atbv = new AreaTemp.ByValue();
	SpotTemp.ByValue stbv = new SpotTemp.ByValue();
	itbr.area[0] = atbv;
	itbr.spot[0] = stbv;
	int res = sdk.sdkGetAllTempData("",itbr);
	System.out.println("sdkGetAllTempData res="+res);
	for(int i = 0; i < 6; i++){
		System.out.println("GetAllTempData AreaTemp[" + i + "] enable:" + itbr.area[i].enable);
		System.out.println("GetAllTempData AreaTemp[" + i + "] max_temp:" + itbr.area[i].max_temp);
		System.out.println("GetAllTempData AreaTemp[" + i + "] max_temp_x:" + itbr.area[i].max_temp_x);
		System.out.println("GetAllTempData AreaTemp[" + i + "] max_temp_y:" + itbr.area[i].max_temp_y);
		System.out.println("GetAllTempData AreaTemp[" + i + "] min_temp:" + itbr.area[i].min_temp);
		System.out.println("GetAllTempData AreaTemp[" + i + "] min_temp_x:" + itbr.area[i].min_temp_x);
		System.out.println("GetAllTempData AreaTemp[" + i + "] min_temp_y:" + itbr.area[i].min_temp_y);
		System.out.println("GetAllTempData AreaTemp[" + i + "] ave_temp:" + itbr.area[i].ave_temp);
		System.out.println("GetAllTempData SpotTemp[" + i + "] enable:" + itbr.spot[i].enable);
		System.out.println("GetAllTempData SpotTemp[" + i + "] temp:" + itbr.spot[i].temp);
	}
	System.out.println("GetAllTempData LineTemp enable:" + itbr.line.enable);
	System.out.println("GetAllTempData LineTemp max_temp:" + itbr.line.max_temp);
	System.out.println("GetAllTempData LineTemp max_temp_x:" + itbr.line.max_temp_x);
	System.out.println("GetAllTempData LineTemp max_temp_y:" + itbr.line.max_temp_y);
	System.out.println("GetAllTempData LineTemp min_temp:" + itbr.line.min_temp);
	System.out.println("GetAllTempData LineTemp min_temp_x:" + itbr.line.min_temp_x);
	System.out.println("GetAllTempData LineTemp min_temp_y:" + itbr.line.min_temp_y);
	System.out.println("GetAllTempData LineTemp ave_temp:" + itbr.line.ave_temp);
	System.out.println("GetAllTempData GlobaTemp max_temp:" + itbr.globa.max_temp);
	System.out.println("GetAllTempData GlobaTemp max_temp_x:" + itbr.globa.max_temp_x);
	System.out.println("GetAllTempData GlobaTemp max_temp_y:" + itbr.globa.max_temp_y);
	System.out.println("GetAllTempData GlobaTemp min_temp:" + itbr.globa.min_temp);
	System.out.println("GetAllTempData GlobaTemp min_temp_x:" + itbr.globa.min_temp_x);
	System.out.println("GetAllTempData GlobaTemp min_temp_y:" + itbr.globa.min_temp_y);
}

运行结果如下

注意:如果没有对结构体数组中第一个元素赋值,会报下面的错

如果对每一项元素都赋相同的初值,会导致获取到的数组元素值都相同。

3 结构体数组作参数

C函数:

// test.h
struct Person{
    int age;
    char name[20];
};
extern "C"{
	int changeObjs(Person per[], int size);
}


//test.cpp
int changeObjs(Person per[], int size){
    if( size <= 0){
        return -1;
    }
    for (int i = 0; i<size;i++){
        per[i].age *= 10;
        strcpy(per[i].name,"wokettas");
    }

    for(int k=0;k<size;k++){
        printf("person[%d] age:%d\n",k,per[k].age);
        printf("person[%d] name:%s\n",k,per[k].name);
    }
    return 0;
}

JNA代码:

//Person.java
import com.sun.jna.Structure;

import java.util.Arrays;
import java.util.List;

public class Person extends Structure {
    public int age;
    public byte[] name = new byte[20];

    @Override
    protected List getFieldOrder(){
        return Arrays.asList("age","name");
    }
}

Java接口声明:

int changeObjs(Structure per[], int size);

注意:结构体数组做参数时,Java接口中传该结构体本身即可。加上.ByReference或者.ByValue反而不正确

典型错误1–内存不连续

如果在JNA中调用的C函数参数中有结构体数组时,直接new对象会报错

结构体数组必须使用连续的内存区域。p1,p2都是new出来的对象,不可能连续,用传统方式初始化数组不能解决。查看JNA api发现提供了toArray

public Structure[] toArray(int size)

Returns a view of this structure’s memory as an array of structures. Note that this Structure must have a public, no-arg constructor. If the structure is currently using a Memory backing, the memory will be resized to fit the entire array.(以结构数组的形式返回此结构的内存视图。请注意,此结构必须具有公共的、无参数的构造函数。如果结构当前使用内存备份,则内存将调整大小以适应整个数组。)
使用JNA的toArray产生内存连续的结构体数组:

Person pb = new Person();
Person[] pers = (Person[]) pb.toArray(2);
pers[0].age = 1;
pers[0].name = Arrays.copyOf("k1".getBytes(), 20);
pers[1].age = 2;
pers[1].name = Arrays.copyOf("k2".getBytes(), 20);
int res = CLibrary.INSTANCE.changeObjs(pers, 2);
System.out.println("res="+res);

典型错误2–误用ByValue

如果在Java接口声明中错误把参数类型写成Person.ByValue,会报java.lang.IndexOutOfBoundsException

// C函数声明
int changeObjs(Person.ByValue per[], int size);
Person.ByValue pb = new Person.ByValue();
Person.ByValue[] pers = (Person.ByValue[]) pb.toArray(2);
pers[0].age = 1;
pers[0].name = Arrays.copyOf("k1".getBytes(), 20);
pers[1].age = 2;
pers[1].name = Arrays.copyOf("k2".getBytes(), 20);
int res = CLibrary.INSTANCE.changeObjs(pers, 2);

运行报错:

解决方法:将参数类型改为结构体本身即可,即不带ByReference或ByValue。结构体数组做参数时,要区别于非数组的ByReference和ByValue。

4 Java映射C中char[]类型

C中有时候会用char[]传字符串,java中对应的类型为byte[]。可以借助java.util.Arrays类的copyOf ()方法进行转换。
假设有如下C结构体:

typedef struct {
    int enable;
    char ip[20];
} Info;
// C函数声明:
extern "C"{
	void get_info(Info info);
}
// 函数定义
void get_info(Info info){
    printf("enable:%d\n",info.enable);
    printf("ip:%s\n",info.ip);
}

JNA代码:

public static class InfoStruct extends Structure{
public int enable;
public byte[] ip;   // 映射C类型char[]
//public String ip; // 不能直接使用String去映射C类型char[],传参失败


public static class ByReference extends InfoStruct implements Structure.ByReference{};
public static class ByValue extends InfoStruct implements Structure.ByValue{};

@Override
protected List getFieldOrder(){
        return Arrays.asList(new String[] {"enable","ip"});
    }
}

Java调用C函数:

5 常见问题及其解决方法

参考Java JNA调用C函数常见问题及解决方法一文。
以上就是JNA调用C函数最常用,也是比较复杂的用法了。本篇文章内容较长,希望对你有所帮助,不足之处欢迎指出!


文章来源:https://blog.csdn.net/xb_2015/article/details/141498088
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!

标签:

相关文章

本站推荐

标签云