Android跨语言篇

原创 2015年07月07日 10:39:05

        在Android中,设计的开发语言包括汇编、C、C++、java、Parcel、Bash、XML、IDL、Flash等。在原生的C/C++代码层,也涉及多线程的处理。

1.C语言与汇编语言的相互调用

        C语言与汇编语言的相互调用,在应用层开发中并不常用,但在驱动开发层进程用到,另外,在一些对性能特别敏感的场景中,也时有引用。

        (1)C语言对汇编语言的调用

                在C语言中调用汇编语言之需要关键字__asm__将汇编代码封装即可。下面是Android中speex编解码的一个示例:

                        static inline spx_word32_t MAC16_32_Q15(spx_word_t c, spx_word16_t a, spx_word32_t b){

                                spx_word32_t res;

                                __asm__

                                        (

                                                "A1=%2.L*%1.L (M); \n\t"

                                                "A1=A1 >>> 15; \n\t"

                                                "%0=(A1 +=%2.L*%1.H); \n\t“

                                                ”%0=%0+%d; \n\t"

                                                :"=&W" (res), "=&d" (b)

                                                :"d" (a), "1" (b), "d" (c)

                                                :"A1"

                                        );

                                return res;

                        }

               汇编代码在C语言中的实现形式因编译环境不同而有所不同,在Android中采用的是GCC编译器。
        (2)汇编语言对C语言的调用

                汇编语言对C语言的调用需要通过关键字Bl进行,如果涉及参数传递,则实现比较复杂,下面是一个简单的示例:

                对于C函数的实现如下:

                        int sum(int a, int b, int c, int d, int e)

                        {

                                return a+b+c+d+e;

                        }

                调用sum()函数的汇编代码的实现如下:

                        EXPORT f

                        AREA f, CODE, READONLY

                        IMPORT sum                            :使用伪操作数IMPORT声明C程序sum()

                        STR lr, [sp, #-4]!                        :保存返回地址

                        ADD r1,  r0, r0                           :假设进入程序f时,r0中的值为i,r1的值设为2*i

                        ADD r2,  r1, r0                           :r2的值设为3*i

                        ADD r3,  r1, r2                           :r3的值设为5*i

                        STR r3, [sp, #-4]!                        :第五个参数5*i通过数据栈传递

                        ADD r3, r1, r1                             :r4值设为4*i

                        BL sum                                       :调用C程序sum()

                        ADD sp, sp, #4                           :调整数据栈指针,准备返回

                        LDR pc, [sp], #4                          :返回

                        END

                对于C函数中的参数,在汇编语言中通过寄存器r0~r3传递前4个参数,更多的参数则需要通过数据栈进行传递。

2.C++与C语言的相互调用

        在C++与C语言的相互调用中,注意关键字__cplusplus和extern “C”的用法即可,其中__cplusplus为C++的关键字,表示作用域内的代码为C++代码,而extern "C" 是为了保证C++和C是互通的。这是因为在编译生成汇编码时,两者的处理有所不同,读过《深度探索C++对象模型》的开发者都知道,在C++中,为了支持重载机制,在编译生成汇编码时,要对函数的名称进行一些处理,而在C语言中,则不需要如此。

        (1)C++对C语言的调用

                #ifdef __cplusplus

                extern "C"

                {

                        #endif

                        #define NUM_AMRSID_RXMODE_BITS            3

                        #define AMRSID_RXMODE_BIT_OFFSET        36

                        #define AMRSID_RXTYPE_BIT_OFFSET          35

                        extern const Word16 WmfDecBytesPerFrame[];

                        extern const Word16 If2DecBytesPerFrame[];

                        Word16 AMRDecode(void* state_data, enum Frame_Type_3GPP frame_type, UWord8 *speech_bits_ptr, Word16* raw_pcm_buffer, bitstream_format input_format);

                       #infdef __cplusplus

               }

               #endif

        (2)C语言对C++的调用

                在C语言调用C++时,由于C语言不能直接引用声明了extern “C”的C++头文件,因此在调用C++头文件中用extern “C”声明的函数时,需声明其为外部函数。C++头文件test.h的实现如下:

                        #ifndef __TEST_H__

                        #define __TEST_H__

                        extern "C" int sum(int x, int y);

                        #endif        // __TEST_H__

                CPP文件的实现如下:

                        #test.h

                        int sum(int x, int y){

                                return x+y;

                        }

                调用C++的C文件的实现如下:

                        extern int sum(int x, int y);

                        int main(int argc, char* argv[]){

                                sum(2, 3);

                                return 0;

                        }

3.Java对C/C++的调用

        在Java 1.1中,引入了JUI(Java Native Interface)技术。JNI支持Java和其他语言交互,即JNI支持Java对C/C++的调用,也支持C/C++调用Java。JNI的实现主要位于NDK中,目前NDK的最高版本为Android NDK r6b。

        Java对本地语言的调用会导致程序可移植性的削弱,但在与驱动、操作系统进行交互以及遗留系统移植等场景中,这种方式却是十分必要的,毕竟原生代码在运行速度和I/O处理上更有效率,更重要的是原生代码还支持OpenGL ES。在Android中,JNI的应用也十分广泛,除以上场景外,考虑到Java的反编译技术的发展,涉及商业机密的设计和敏感数据的安全都需要在原生代码中维护。目前JNI的最新版本为JNI 1.6。

        下面介绍如何加载共享库、定义JNI原生接口、定义数据类型、转换数据类型、获取环境变量路径、编译原生代码。

        (1)加载共享库

                在Android中,要与原生代码交互,通常需将原生代码编译成共享库的形式供Java调用,调用的方法如下:

                        public final class AmrImputStream extends InputStream

                        {

                                static{

                                        System.loadLibrary("hello-jni");        //共享库通常位于Android应用的lib/armeabi中

                                }

                        }

                除了原生共享库外,java.lang.System还支持对系统相关信息和资源的接入和获取,如系统时间、系统属性、环境变量等,方法如下:

                        System.currentTimeMillis()        //系统时间

                        String oldUserHome=System.getProperty("user.home");        //获取系统属性

                        String symbols=System.getenv("ANDROID_SYMBOLS");        //环境变量

                设置系统属性的方法如下:

                        Properties tProps=new Properties();

                        tProps.put("testIni", "99");

                        System.setProperties(tProps);

                java.lang.System在Android中是非常重要的一个类,其在一些涉及底层通信的应用开发中使用很广泛。

        (2)定义JNI原生接口

                为了定义JNI原生接口,需要在原生代码中包含jni.h文件。在jni.h文件中定义了JNI数据类型和原生接口。需要注意的是JNI原生接口的接口名定义与通常的方法名、函数名定义不同。事实上,根据管用的场景不同,JNI接口的定义方式有两种,分别为普通调用和定向调用。其中普通调用定义的JNI接口可以在多个Java类中被调用,定向调用定义的JNI接口仅能在特定的Java类中被调用。在Android中,普通调用的JNI定义通常用于框架层,而定向调用的JNI定义通常用于应用层。

               1)普通调用

                       在框架层,Android对JNI接口的定义和通常的方法定义类似,但需要向Dalvik虚拟机注册原生方法,以便在共享库加载过程中加载JNI接口。普通调用的JNI接口定义的步骤是:JNI接口定义---JNI接口映射---注册JNI接口映射---设置共享库加载时JNI接口映射。

                       下面介绍JNI接口映射的内容。JNI接口映射的结构体定义如下:

                                typedef struct{

                                        const char* name;        //Java中声明的JNI接口

                                        const char* signature;        //接口参数和返回值

                                        void* fnPtr;                  JNI接口

                                }JNINativeMethod;

                        JNI接口映射的实现如下:

                                static JNINativeMethod nativeMethods[]={

                                        {"_initialize", "(ILjava/lang/Object;)V", (void*)android_drm_DrmManagerClient_initialize},

                                        {"_finalize", "(I)V", (void*)android_drm_DrmManagerClient_finalize},

                                }

                        注册JNI接口映射的过程如下:

                                static int registerNativeMethods(JNIEnv* env){

                                        int result = -1;

                                        jclass clazz=env->FindClass("android/drm/DrmManagerClient");

                                        if(NULL != clazz){

                                                if(env->RegisterNatives(clazz, nativeMethods, sizeof(nativeMethods)/sizeof(nativeMethod[0]))==JNI_OK){

                                                        result=0;

                                                }

                                        }

                                        return result;

                                }

                        在共享库加载过程中,通过JNI_OnLoad()执行JNI接口映射的注册,过程如下:

                                jint JNI_OnLoad(JavaVM* vm, void* reserved){

                                        JNIEnv* env=NULL;

                                        jint result=-1;

                                        if(vm->GetEnv((void**)&env, JNI_VERSION_1_4)==JNI_OK){

                                                if(NULL!=env && registerNativeMethods(env)==0){        //判断JNI版本

                                                        result=JNI_VERSION_1_4;        //执行接口映射注册

                                                }

                                        }

                                        return result;

                                }

                        另外,如果希望在共享库卸载时做些处理,那么可以在JNI_OnLoad()中进行。通常在卸载时会做些清除痕迹的工作。

                2)定向调用

                        在定向调用中,接口名的定义由3部分组成:Java声明、Java包名、函数名。原生方法stringFromJNI对应的JNI原生接口如下:

                                jstring Java_con_example_hellojni_HelloJni_stringFromJNI(JNIEnv * env, jobject thiz)

                                {   ...}

                        需要注意的是,上述代码中“Java”的“J”为大写,接口名中的字符串以“_”连接。在加载共享库时,如果无法找到对应原生方法的JNI接口,会抛出UnsatisfiedLinkError异常。

                        为了调用JNI接口,需要在Java中声明JNI接口,其方法如下:

                                public native String stringFromJNI(...);        //通过native关键字表示JNI接口

                        另外,JNI还提供了JNIEXPORT、JNICALL、JNIIMPORT等关键字,这些关键字在Windows操作系统下是必须的,在Linux操作系统中是可选的。

                3)数据类型定义

                        在Java中,数据的编码为Unicode,而在JNI中,数据的编码为UTF-8,而且不支持全部的UTF-8字符集。JNI中的数据类型和Java、C/C++中的数据类型均有不同。请参照下图:

                               

                        其他JNI数据类型包括jclass、jobject、jmethodID、jfieldID、JNIEnv、JavaVM等,其中jclass表示句柄,jobject表示对象句柄,jmethodID表示方法句柄,jfieldID表示变量句柄,JNIEnv表示JNI环境变量,JavaVM表示Java虚拟机的全局环境。

                4)数据类型转换

                        下面简单介绍字符串、数组和指针间的数据类型转换。

                        jstring向const char*的转换的方法如下:

                                jboolean iscopy;

                                jstring file_name=(*env)->NewStringUTF(env, "test.temp");        //创建jstring类型数据

                                const char* c_file_name=(*env)->GetStringUTFChars(env, file_name, &iscopy);

                                env->ReleaseStringUTFChars(file_name, c_file_name);        //创建c_file_name后需要释放

                        jbyteArray数组的创建方法如下:

                                jbyteArray dataArray=env->NewByteArray(strlen(value));

                                env->SetByteArrayRegin(dataArray, 0, strlen(value), (jbyte*)value);        //数组初始化

                        数组向指针的转化方法,jbyteArray向jbyte*转化如下:

                                jbyte* rawSavedState = NULL;

                                jsize rawSavedSize = 0;

                                if(savedState != NULL){

                                        rawSavedState=evn->GetByteArrayElements(savedState, NULL);

                                        rawSavedSize=env->GetArrayLength(savedState);

                                }

                                if(rawSavedState!=NULL){

                                        env->ReleaseByteArrayElements(savedState, rawSavedState, 0);

                                }

                5)获取环境变量路径

                        在C/C++中,要得到特定环境变量所指定的路径,方法非常简单,利用getenv()函数即可,实例如下:

                                const char* root=getenv("ANDROID_ROOT");

                6)原生代码编译

                        JNI的编译非常简单,只需要设置LOCAL_PATH、LOCAL_MODULE、LOCAL_SRC_FILES等环境变量,然后根据需要指明是编译成共享库还是静态库即可。下面是编译原生代码的一般android.mk实现:

                                LOCAL_PATH :=$(call my-dir)        //设置LOCAL_PATH为当前目录

                                include $(CLEAR_VARS)                //清空环境变量

                                LOCAL_MODULE := hello-jni        //指明模块名

                                LOCAL_SRC_FILES := hello-jni.c        //指明源文件

                                include $(BUILD_SHARED_LIBRARY)     //指明编译为共享库,静态库变量为BUILD_STATIC_LIBRARY

                        如果编译成共享库,那么上面的编译脚本生成的共享库为libhello-jni.so,主要在加载共享库时指定的是模块而非共享库名。

4.C/C++对Java的调用

        在Java和原生代码的交互中,Android中的原生代码通常扮演基础框架的角色,对于UI层的实现依然以Java为主,部分场景可能涉及反向调用,这类场景包括UI层代码的调用、公开实现的Java算法的调用、数据管理等。JNI调用Java方法的原生接口在结构体JNINativeInterface中定义。JNI调用Java方法的过程如下:FindClass(查询Java类)---GetMethodID(获取Java方法句柄)---Call*Method(调用Java方法)。

        JNI对Java的调用的支持非常强大,除此之外,JNI还支持对Java变量、私有类的操作。JNI操作Java变量的一般过程是:FindClass(查询Java类)---GetFieldID(获取Java变量句柄)---GetFieldID/SetFieldID(操作Java变量)。

        对于BackupDataInput的私有类EntityHeader,查询方法如下:

                jclass clazz=env->FindClass("android/app/backup/backupDataInput$EntityHeader");

        (1)原生接口定义

                根据Java方法的不同实现,在JNI中有不同的原生接口与其对应,同时针对交互的特殊需要,JNI还定义了一些特殊的原生接口。

                常用Java方法及其JNI原生接口的对应关系如下图所示:

                       

                另外,还有其他的重要JNI接口,具体如下:

                         FindClass:用于查找相应的Java类,注意,其传递的类名参数为const char*类型,可能抛出的异常包括ClassFormatError、ClassCircularityError、NoClassDefFoundError和OutOfMemoryError等。

                         GetMethodID:用于查找Java类或接口中的非静态方法。其方法名和方法的参数均为const char*类型,可能抛出的异常包括NoSuchMethodError、ExceptionInInitializerError、OutOfMemoryError等。调用GetMethodID将导致未初始化的类被初始化。

                         GetFieldID:用于查找Java类中的非静态变量。可能抛出的异常包括NoSuchFieldError、ExceptionInInitalizerError、OutOfMemoryError等。

                         GetStaticMethodID:用于查找Java类中的静态方法。

                         GetStaticFieldID:用于查找Java类中的静态变量。

                其他常用的JNI接口包括GetObjectClass、GetSuperclass、IsInstanceOf、IsAssignableFrom、IsSameObject等,另外JNI还提供了对Java反射机制的支持。

        (2)接口参数和返回值

                在JNI中,接口参数和返回值的声明有自己的规则,其格式为“(参数类型1:参数类型2)返回值类型“。

                对于基本的数据类型,JNI提供了相应的缩写,如void的缩写为V、数组的缩写为[、char的缩写为C、short的缩写为S、int的缩写为I、long的缩写为J、float的缩写为F、byte的缩写为B、boolean的缩写为Z、double的缩写为D、合格类的缩写为L(在复杂数据类型前需加L表明是类)等。对于非基本的数据类型,则不提供缩写。

                对于void ContextValue::put(String key, byte[] value)方法,其对应的JNI调用方法如下:

                        putId=env->GetMethodID(localRef, "put", "(Ljava/lang/String; [B]V");

                对于private BluetoothSocket(int type, int fd, boolean auth, boolean encrypt, String address, int port)方法,其对应的JNI调用方法如下:

                        BluetoothSocket_ctor=env->GetMethodID(clazz, "<init>", "(IIZZLJava/lang/String;I)V");

                对于public BackgroundSuiface(Context context)方法,其对应的JNI调用方法如下:

                        ctor=env->GetMethodID(backgroundClass, "<init>", "(Landroid/Context;)V");

        (3)Log日志的生成

                在JNI中,支持Android全部等级的日志输出,为了书写方便,一般会定义为宏定义,方法如下:

                        #define DEBUG(args...)  __android_log_print(ANDROID_LOG_DEBUG, "Hello", args)

                为了使用Android日志,需要包含android\log.h文件。同时在Android.mk文件中设置LOCAL_LDLIBS:=-llog。另外还需注意,在默认情况下,编译出来的共享库并非调试版本,基于ANDROID_LOG_DEBUG等级的日志无法显示,故通常使用ANDROID_LOG_INFO等级。

        (4)应用开发中的调用示例

                在进行JNI原生接口的调用中,Java对象的获取有两种方式:第一,可以在Java代码创建Java对象,然后传递给JNI原生接口;第二,可以在JNI原生接口中直接创建Java对象。下面进行简单介绍。

                1)传递Java对象

                         对于要传递的Java对象,其参数类型为jobject。下面是Android中的一个例子,其实现了传递一个名为headerObj的Java对象的功能。

                                 static int readHeader_native(JNIEnv* env, jobject clazz, jobject headerObj, jobject fdObj){

                                         env->SetIntField(headerObj, s_chunkSizeField, flattenedHeader,dataSize);

                                         env->SetObjectField(headerObj, s_keyPrefixField, env->NewStringUTF(keyPrefix.string()));

                                         return 0;

                                 }

                2)直接创建Java对象

                        如果希望将对Java对象的调用集中在原生代码中,就会涉及在原生代码中创建Java对象。为了创建Java对象,先要查找到相应Java类的句柄,然后通过GetMethodID接口查找到该类的构造函数,之后即可通过NewObject接口创建Java对象。下面是一个具体的示例:

                                static jobject createJavaMapFromHTTPHeaders(JNIEnv* env, const WebCore::HTTPHeaderMap& map){

                                        jclass mapClass=env->FindClass("java/util/HashMap");        //查找Java类

                                        jMethodID init =env->GetMethodID(mapClass, "<init>", "(I)V");        //查找构造函数,注意方法名为<init>

                                        jobject hashMap=env->NewObject(mapClass, init, map.size());        //创建Java对象

                                        ...

                                        env->DeleteLocalRef(mapClass);        //释放本地引用

                                        return hashMap;

                                }

                        注意在操作结束后,应释放Java类的引用。

                3)调用Java方法

                        Java方法的调用非常简单,主要是通过Call*Method接口来完成的,示例如下:

                                static jobject android_drm_DrmManagerClient_getConstraintsFromContext(JNIEnv* env, jobject thiz, jint uniqueId, Jstring jpathm, jint usage){

                                        jcalss localRef=env->FindClass("android/content/ContentValues");        //查找Java类

                                        jobject constraints = NULL;

                                        ...

                                        jmethodID constructorId=env->GerMethodID(localRef, "<init>", "()V");        //获取构造函数

                                        constraints=env->NewObject(localRef, constructorId);        //构造Java对象

                                        jbyteArray dataArray=env->NewByteArray(strlen(value));

                                        env->SetByteArrayRegin(dataArray, 0, strlen(value), (jbyte*)value);

                                        //调用java方法

                                        env->CallVoidMethod(condtraints, env->GetMethodID(localRef, "put", "(Ljava/lang/String;[B]V)"), env->NewStringUTF(key.string()), dataArray);

                                        ...

                                }

                         在调用Java方法时,需要注意,如果返回值不是基本数据类型,则使用CallObjectMethod()接口调用Java方法。

                4)操作Java变量

                        为了设置Java变量,先要获取Java变量的句柄,然后根据Java变量的类型选择不同的JNI接口进行设置。

                                 static sp<DrmManagerClientImpl> setDrmManagerClientImpl(JNIEnv* env, jobject thiz, const sp<DrmManagerClientImpl>& client){

                                         jclass clazz=env->FindClass("android/drm/DrmManagerClient");        //查找Java类

                                         jfieldID fieldId=env->GetFieldID("android/drm/DrmManagerClient");        //查找Java类

                                         jfieldID fieldId=env->GetFieldID(clazz, "mNativeContext", "I");        //查找Java变量

                                         sp<DrmManagerClientImpl>lod=(DrmManagerClientImpl*)env->GetIntField(thiz, fieldId);

                                         if(client.get()){

                                                 client->incString(thiz);

                                         }

                                         if(old!=0){

                                                 old->decString(thiz);

                                         }

                                         //设置Java变量

                                         env->SetIntField(thiz, fieldId, (int)client.get());

                                        return old;

                                }

                        JNI针对不同的Java变量类型提供了不同的JNI接口,设置Java变量的主要的JNI接口包括SetObjectField、SetBooleanField、SetByteField、SetCharField、SetShortField、SetIntField、SetLongField、SetFloatField、SetDoubleField等。获取Java变量的主要接口包括GetObjectField、GetBooleamField、GetByteField、GetCharField、GetShortField、GetIntField、GetLongField、GetFloatField、GetDoubleField等。

                       从JNI对Java方法和Java变量的操作可以很容易看出,语言的单根设计的好处,除了体现在基本数据类型方面外,其他数据类型也均可通过对根类的操作来完成。

                5)抛出异常

                        在JNI中,异常的抛出有两种情况,一种是C++实现中抛出C++异常,另一种是在C/C++中抛出Java异常。

                        抛出Java异常主要是通过jniThrowException接口来是实现的。下面是jniThrowException()接口的定义:

                                int jniThrowException(JNIEnv* env, const char* className, const char* msg)

                        下面是通过jniThrowException接口抛出异常的一个示例:

                                jniThrowException(env, "java/lang/RuntimeException", "Can't find android/graphics/Metrix");

                        与异常相关的其他JNI接口包括Throw、ThrowNew、ExceptionOccurred、ExceptionDescribe、ExceptionClear、FatalError、ExceptionCheck等,其中ExceptionCheck用于检测是否已经有异常抛出;ExceptionOccurred用于获取一个正在抛出异常的本地引用,JNI或者Java必须对其进行处理;ExceptionDescribe用于打印出异常的错误描述;ExceptionClear用于清除刚刚抛出的异常;FatalError用于抛出一个可导致JVM关闭的异常。下面是这些异常类应用的一个示例:

                                env->CallVoidMethod(fJavaInputStream, gInputStream_resetMethodID);

                                if(env->ExceptionCheck()){

                                        env->ExceptionDescribe();

                                        env->ExceptionClear();

                                        return false;

                                }

        (5)C与C++的JNI区别

                由于C和C++在可执行文件布局上的不同,JNI接口的调用方式在JNIEnv指针的操作上也略有不同。下面通过NewStringUTF接口来介绍。如下是C语言的调用方式:

                        (*env)->NewStringUTF(env, "Hello from JNI !");

                如下是C++的调用方式:

                        env->NewStringUTF("Hello from JNI !");

Android的Gradle技巧 3.5合并跨Java语言的Java源代码

3.5合并跨Java语言的Java源代码 问题 您希望将Android活动或其他Java类添加到单个产品风格。 解 创建正确的源文件夹,添加Java类,并将它们与主源集合合并。 讨论 虽然flavor...

Android学习之跨进程通信安卓接口定义语言AIDL(二)

接着刚刚的一篇讲下使用AIDL传递对象的过程,AIDL在不导入其他包的情况下支持如下几种数据类型: 1. 基本数据类型(boolean、char、byte、int、long、float、double)...

Android学习之跨进程通信安卓接口定义语言AIDL(一)

今天来写下安卓接口定义语言,也就是大家听了都头疼的AIDL,今天有幸看到慕课网的AIDL视频学习了一下,在此感谢慕课网,是个很不错的网站。 进入正题,Android中跨进程是如何传递数据的?如果是Se...

OpenGLES Android篇零基础系列(五):GLSL着色器语言

本文转载至:http://www.tuicool.com/articles/qMfAfy一.概述GLSL ES是在GLSL(OpenGL着色器语言)的基础上,删除和简化了一部分功能后形成的,目标平台是...

Kotlin ---Android开发的一种新语言(开始篇)

Kotlin 官方文档(翻译)有时间过来和大家一起分享一下Android开发使用的另一种新语言,本文章是原创翻译,有理解的不到位的地方,请多多指教 使用Kotlin进行Android开发Kotlin是...
  • tqq1226
  • tqq1226
  • 2017年05月31日 17:34
  • 703

android 平台语言支持情况

  • 2015年09月29日 10:30
  • 206KB
  • 下载

android手动多国语言切换

  • 2015年07月07日 17:24
  • 2.27MB
  • 下载

Android 项目开发填坑记 - 获取系统语言(兼容7.0)

关键词:Android7.0 、系统语言、顺序不一致 获取系统当前语言是一个比较常用的功能,在 Android 7.0 系统上旧函数获取到的当前系统语言并不正确,或者说从 Android 7.0 起,...

android 语言表

  • 2013年11月25日 19:35
  • 43KB
  • 下载

Android多语言指导

  • 2014年05月19日 11:41
  • 193KB
  • 下载
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Android跨语言篇
举报原因:
原因补充:

(最多只允许输入30个字)