Android C/C++开发指南

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 众所周知,Android作为目前主流的移动终端领域的开发平台,其主要的开发语言就是JAVA。Android借助于JAVA高效、灵活的开发模式,迅速占领了移动互联网开发的半壁江山。基于JDK和Android实现的各种JAVA框架,开发人员可以快速实现各种功能的APP,而且APP的安装和升级维护都实现的方便。

Android C/C++开发指南

1.Android C/C++简介


众所周知,Android作为目前主流的移动终端领域的开发平台,其主要的开发语言就是JAVA。Android借助于JAVA高效、灵活的开发模式,迅速占领了移动互联网开发的半壁江山。基于JDK和Android实现的各种JAVA框架,开发人员可以快速实现各种功能的APP,而且APP的安装和升级维护都实现的方便。


对于,AndroidJAVA的开发模式大家已经很熟悉了,今天要说的是基于Android 平台如何开发原生的C/C++应用程序程序。Google对于AndroidC/C++开发提供了NDK(Native Development Kit),基于NDK我们可以编译出能够在Android平台上运行了动态库/静态库/可执行程序。对于动态库/静态库,JAVA程序可以通过JNI(Java Native Interface)机制实现与其通信。


说到这里,可能大家觉得开发Android C/C++程序还需要花时间学习NDK的使用方式,未免有些麻烦。其实,基于NDK可以生成传统意义上的交叉开发工具链,对于经常开发嵌入式Linux应用程序的开发人员来说,对于交叉工具链的使用再熟悉不过了,所以对于不愿意使用NDK开发方式的程序员来说,使用交叉工具链则简单、高效的多,下面分别介绍这两种开发方式。


2.AndroidNDK开发方式


下面是官方对于NDK的简要介绍:NDK是一套可以允许APP调用像C或者C++编写的原生代码的工具集。对于某些类型的APP,NDK可以实现现有代码库的复用,但对于大多数的APP来说不需要NDK提供的这项服务。


官方文档同时说明,NDK对于大多数的APPs并没有太多的好处。并提醒,作为一名开发者,需要平衡使用NDK的利与弊。需要注意的是,在Android平台上使用原生的代码,一般不会带来可观的性能提升,但它常常增加APP的复杂度。所以,除非你的APP必须使用NDK,其他情况下,不要仅仅为了使用C/C++编程而使用NDK。


典型的使用NDK的场景是CPU密集型的应用,例如游戏引擎、信号处理、物理环境模拟等。当你使用NDK之前,请确定Android框架提供的APIs是否满足你的需求。


好了,上面就是官方对于NDK的简单介绍,具体使用场景大家可以参考上面提到的注意事项。下面就对NDK的具体使用方式做一下介绍。


2.1NDK开发环境搭建


首先,介绍介绍一下主机开发环境:


操作系统:Ubuntu14.04 TLS 32bit内存:4G


2.1.1安装NDK


NDK的官方下载地址:wear.techbrood.com/tools/sdk/n…


NDK的安装十分的简单,将下载来的NDK,解压就可使用,例如:


$tar jxvf android-ndk32-r10b-linux-x86.tar.bz2 -C /to/NDK/path


这样NDK就安装好了,你可以按照自己的习惯重新给NDK的开发目录命名。


2.1.2NDK环境变量设置


使用NDK之前需要将NDK的安装目录加入到PATH环境变量中,具体的PATH变量可以配置到~/.bashrc文件中,配置完成后,使用source命令使之生效。


$source ~/.bashrc


2.2Android.mk


NDK依赖于Android.mk完成具体的编译过程,Android.mk类似于GNU Makefile文件,下面的举一个Android.mk简单的例子。


   LOCAL_PATH := (call my−dir)x000D   include (call my-dir)_x000D_   include (callmydir)x000D   include (CLEAR_VARS)x000D   LOCAL_MODULE    := hello-jni_x000D_   LOCAL_SRC_FILES := hello-jni.c_x000D_   include $(BUILD_SHARED_LIBRARY)


下面是对于上面Android.mk的解析:


① LOCAL_PATH:=$(call my-dir):每个Android.mk的开始位置必须定义LOCAL_PATH。其用于定义源文件的根目录。在本例子中,函数’my-dir’为构建系统提供的内建函数,其用于返回Android.mk所在当前目录。


② Include $(CLEAR_VARS):CLEAR_VARS为构建系统提供的变量,其指向一个GNU Makefile,用来重置除LOCAL_PATH之外的所有以LOCAL开头的变量(例如,LOCAL_MODULE、LOCAL_SRC_FILES等)。这是必须的,因为在GNU Makefile执行系统中所有的构建控制文件的变量都是全局的,为了不相互干扰,所以在Android.mk中必须调用CLAER_VARS重置以LOCAL开头的变量。


③ LOCAL_MODULE:=hello-jni:LOCAL_MODULE变量用来定义Android.mk所描述的目标模块名称。这个名字必须为唯一的,并且不能带有空格。构建系统会自动的为其添加适当的前缀和后缀。例如,动态库的模块名‘foo’为会被命名为’libfoo.so’;需要注意的是如果模块的名称为libfoo,构建系统不会在为其添加‘lib’前缀,同样最后生成’libfoo.so’。


④ LOCAL_SRC_FILES := hello-jni.c:LOCAL_SRC_FILES变量包含所有需要被编译进模块中的C/C++源文件。需要注意的是,你不需要对应的头文件定义出来,构建系统会自动的为你分析依赖关系。


⑤ Include $(BUILD_SHARED_LIBRARY):BUILD_SHARED

_LIBRARY变量为构建系统提供的内建GNU Makefile脚本,其会收集距其最近的‘include $(CLEAR_VARS)之后的所有LOCAL_X变量的信息,从而决定如何构建最终的目标文件。与其相对应的是BUILD_STATIC_LIBRARY,其用来生成静态库。

上面为一个最为简单的Android.mk的分析,下面具体分析一下Android.mk的具体细节。SOURCES


2.2.1Android.mk变量


以下是在 Android.mk中依赖或定义的变量列表,可以定义其他变量为自己使用,但是NDK编译系统保留下列变量名:


a. 以LOCAL_开头的名字(例如LOCAL_MODULE)


b. 以PRIVATE_, NDK_ 或 APP_开头的名字(内部使用)


c. 小写名字(内部使用,例如‘my-dir’)


 如果为了方便在 Android.mk 中定义自己的变量,建议使用前缀MY_,例如下面的例子:


MY_SOURCES:=foo.c  ifneq((MYCONFIGBAR),) MYSOURCES+=bar.c endif LOCALSRCFILES += (MY_CONFIG_BAR),)  MY_SOURCES+=bar.c  endif  LOCAL_SRC_FILES += (MYCONFIGBAR),)MYSOURCES+=bar.cendifLOCALSRCFILES+= (MY_SOURCES)


注意:‘:=’是赋值的意思;'+='是追加的意思;‘$’表示引用某变量的值。


2.2.2NDK內建变量


这些 GNU Make变量在你的 Android.mk 文件解析之前,就由编译系统定义好了。注意在某些情况下,NDK可能分析 Android.mk 几次,每一次某些变量的定义会有不同。


(1)CLEAR_VARS:  指向一个编译脚本,几乎所有未定义的 LOCAL_XXX 变量都在"Module-description"节中列出。必须在开始一个新模块之前包含这个脚本:include$(CLEAR_VARS),用于重置除LOCAL_PATH变量外的,所有LOCAL_XXX系列变量。


(2)BUILD_SHARED_LIBRARY:  指向编译脚本,根据所有的在 LOCAL_XXX 变量把列出的源代码文件编译成一个共享库。


注意,必须至少在包含这个文件之前定义 LOCAL_MODULE 和 LOCAL_SRC_FILES.


(3)BUILD_STATIC_LIBRARY: 一个 BUILD_SHARED_LIBRARY 变量用于编译一个静态库。静态库不会复制到的APK包中,但是可以用于编译共享库。


示例:include$(BUILD_STATIC_LIBRARY)


注意,这将会生成一个名为 lib$(LOCAL_MODULE).a 的文件


(4)TARGET_ARCH:目标 CPU平台的名字


(5)TARGET_PLATFORM: Android.mk 解析的时候,目标 Android 平台的名字.详情可考/development/ndk/docs/stable- apis.txt.


android-3 -> Official Android 1.5 system images


android-4 -> Official Android 1.6 system images


android-5 -> Official Android 2.0 system images


            ... ...


(6)TARGET_ARCH_ABI:  暂时只支持两个 value,armeabi 和 armeabi-v7a。。


(7)TARGET_ABI: 目标平台和 ABI 的组合


(8)BUILD_EXECUTABLE:编译成为一个可执行文件。


2.2.3NDK內建函数宏


GNU Make函数宏,必须通过使用'$(call  )'来调用,返回值是文本化的信息。


(1)my-dir:返回当前 Android.mk 所在的目录的路径,相当于于 NDK 编译系统的顶层。这是有用的,在 Android.mk 文件的开头如此定义:


LOCAL_PATH:=$(callmy-dir)


(2)all-subdir-makefiles: 返回一个位于当前'my-dir'路径的子目录中的所有Android.mk的列表。 例如,某一子项目的目录层次如下:


src/foo/Android.msrc/foo/lib1/Android.mk       src/foo/lib2/Android.mk 

 

如果src/foo/Android.mk包含:include $(call all-subdir-makefiles) 那么它就会自动包含 src/foo/lib1/Android.mk 和 src/foo/lib2/Android.mk。    


这这项功能用于向编译系统提供深层次嵌套的代码目录层次项功能用于向编译系统提供深层次嵌套的代码目录层次。


注意,在默认情况下,NDK 将会只搜索在 src/*/Android.mk 中的文件。


(3)this-makefile:  返回当前Makefile 的路径(即这个函数调用的地方)。


(4)parent-makefile:  返回调用树中父 Makefile 路径。即包含当前Makefile的Makefile 路径。


(5)grand-parent-makefile:返回调用树中父Makefile的父Makefile的路径


2.2.4模块相关变量


下面的变量用于向编译系统描述你的模块。应该定义在'include  (CLEARVARS)′和′include (CLEAR_VARS)'和'include (CLEARVARS)include (BUILD_XXXXX)'之间。

$(CLEAR_VARS)是一个脚本,清除所有这些变量。


(1)LOCAL_PATH:  这个变量用于给出当前文件的路径。


必须在 Android.mk 的开头定义,可以这样使用:LOCAL_PATH:=(call my−dir) ,如当前目录下有个文件夹名称 src,则可以这样写 (call my-dir) ,如当前目录下有个文件夹名称 src,则可以这样写 (callmydir)src (call src),那么就会得到 src 目录的完整路径 。这个变量不会被(CLEARVARS)清除,Android.mk(使)  


(2LOCALMODULE:(BUILD_XXXX)脚本之前定义它。模块的名字决定了生成文件的名字。


(3)LOCAL_SRC_FILES:  这是要编译的源代码文件列表。


只要列出要传递给编译器的文件,因为编译系统自动计算依赖。注意源代码文件名称都是相对于 LOCAL_PATH的,你可以使用路径部分,例如:


LOCAL_SRC_FILES:=foo.c toto/bar.c\


Hello.c 文件之间可以用空格或Tab键进行分割,换行请用""


如果是追加源代码文件的话,请用LOCAL_SRC_FILES += 。


注意:可以LOCAL_SRC_FILES := $(call all-subdir-java-files)这种形式来包含local_path目录下的所有java文件。


(4)LOCAL_C_INCLUDES:  可选变量,表示头文件的搜索路径。


默认的头文件的搜索路径是LOCAL_PATH目录。


(5)LOCAL_STATIC_LIBRARIES: 表示该模块需要使用哪些静态库,以便在编译时进行链接。


(6)LOCAL_SHARED_LIBRARIES:  表示模块在运行时要依赖的共享库(动态库),在链接时就需要,以便在生成文件时嵌入其相应的信息。


(7)LOCAL_LDLIBS:  编译模块时要使用的附加的链接器选项。这对于使用‘-l’前缀传递指定库的名字是有用的。


例如,LOCAL_LDLIBS := -lz表示告诉链接器生成的模块要在加载时刻链接

到/system/lib/libz.so 。可查看 docs/STABLE-APIS.TXT 获取使用 NDK发行版能链接到的开放的系统库列表。


(8)LOCAL_CFLAGS:用于指定C/C++编译器所使用的编译选项。


注意:不要试图通过LOCAL_CFLAGS修改Android.mk的优化/调试等级,Application.mk中定义的信息会自动控制,并让NDK产生有意义的调试信息。


可以通过如下设置完成额外的头文件的指定:


LOCAL_CFLAGS += -I


但使用最好使用LOCAL_C_INCLUDES,因为ndk-gdb同样会使用这些路径。

更多的Android.mk信息请参看NDKdocs中的Android.mk


2.3 Application.mk


Application.mk目的是描述在你的应用程序中所需要的模块(即静态库或动态库)。

Application.mk文件通常被放置在 PROJECT/jni/Application.mk下,PROJECT/jni/Application.mk下,PROJECT/jni/Application.mkPROJECT指的是您的项目。


要将C\C++代码编译为SO文件,光有Android.mk文件还不行,还需要一个Application.mk文件。


下面具体描述Application.mk的语法使用。


1.  APP_PROJECT_PATH :这个变量是强制性的,并且会给出应用程序工程的根目录的一个绝对路径。这是用来复制或者安装一个没有任何版本限制的JNI库,从而给APK生成工具一个详细的路径。


2. APP_MODULES :这个变量是可选的,如果没有定义,这个模块名字被定义在Android.mk文件中的 LOCAL_MODULE 中。 NDK将由在Android.mk中声明的默认的模块编译,并且包含所有的子文件(makefile文), NDK会自动计算模块的依赖。如果APP_MODULES定义了,它必须是一个空格分隔的模块列表( 注意:NDK在R4开始改变了这个变量的行为,在此之前: 在Application.mk中,该变量是强制的必须明确列出所有需要的模块)


3. APP_OPTIM:这个变量是可选的,用来定义“release”或"debug"。在编译您的应用程序模块的时候,可以用来改变优先级。  “release”模式是默认的,并且会生成高度优化的二进制代码。"debug"模式生成的是未优化的二进制代码,但可以检测出很多的BUG,可以用于调试。注意:如果你的应用程序是可调试的(即,如果你的清单文件在它的标签中把android:debuggable属性设为true),

默认将是debug而非release。把APP_OPTIM设置为release可以覆写它。注意:可以调试release和debug版二进制,但release版构建倾向于在调试会话中提供较少信息:一些变量被优化并且不能被检测,码重新排序可能致使代码步进变得困难,堆栈跟踪可能不可靠,等等。


4. APP_CFLAGS:一个C编译器开关集合,在编译任意模块的任意C或C++源代码时传递。它可以用于改变一个给定的应用程序需要依赖的模块的构建,而不是修改它自身的Android.mk文件


5. APP_BUILD_SCRIPT:默认,NDK构建系统将在 (APPPROJECTPATH)/jni 下寻找一个名为 Android.mk 的文件。即,对于这个文件(APP_PROJECT_PATH)/jni 下寻找一个名为 Android.mk 的文件。即,对于这个文件(APPPROJECTPATH)/jniAndroid.mk(APP_PROJECT_PATH)/jni/Android.mk如果你想重载这个行为,你可以定义APP_BUILD_SCRIPT指向一个不同的构建脚本。一个非绝对路径将总是被解析为相对于NDK顶级目录的路径。


6. APP_ABI : 默认情况下,NDK的编译系统根据 "armeabi" ABI生成机器代码。可以使用APP_ABI 来选择一个不同的ABI。比如:为了在ARMv7的设备上支持硬件FPU指令。可以使用  APP_ABI := armeabi-v7a或者为了支持IA-32指令集,可以使用 APP_ABI := x86或者为了同时支持这三种,可以使用 APP_ABI := armeabi armeabi-v7a x86


7. APP_STL :默认,NDK构建系统提供由Android系统给出的最小C++运行时库(/system/lib/libstdc++.so)的C++头文件。 然而,NDK带有另一个C++实现,你可以在你自己的应用程序中使用或链接它。定义APP_STL以选择它们其中的一个:  


APP_STL := stlport_static       -->     static STLport library


APP_STL := stlport_shared    -->     shared STLport library


APP_STL := system               -->      default C++ runtime library


更多的Application.mk信息请参看NDKdocs中的Application.mk


2.4C/C++库编写


上面将NDK的安装、配置以及Android.mk和Application.mk的编写格式都进行了一些介绍,下面举几个简单的基于NDK的程序编写例子。本节介绍C/C++动态库和静态库的编写方式。下面是程序的源代码,很简单只打印“Hello, NDK!”。


首先,建立NDK的工作目录,例如:


$mkdir -p  ~/android_ndk  ~/android_ndk/jni


打开~/.bashrc文件,添加NDK_PROJECT_PATH环境变量,如下:


export NDK_PROJECT_PATH=~/android_ndk


并执行sourcr ~/.bashrc,是刚才配置的环境变量生效。


进入android_ndk目录,创建hello_ndk.c文件,代码如下:


#include <stdio.h>int hello_ndk(void){    printf("Hello, NDK\n");    return 0;}


 进入~/andorid_ndk目录,编写Android.mk,配置如下:


 LOCAL_PATH := (call my−dir)   include (call my-dir)   include (callmydir)   include (CLEAR_VARS)   LOCAL_MODULE = hello   LOCAL_SRC_FILES = hello.c   include (BUILD_SHARED_LIBRARY)#编译libhello.so动态库#include (BUILD_STATIC_LIBRARY)#编译静态库libhello.a


进入~/android_ndk/jni目录,编写Application.mk,配置如下:


APP_BUILD_SCRIPT=android_c_cpp/Android.mkAPP_PROJECT_PATH=/android_c_cpp/APP_OPTIM=releaseAPP_ABI=armeabi


 进入~/android_ndk目录,执行ndk-build,创建libhello.so动态库,如下:


[armeabi] Compile thumb  : hello <= hello.c[armeabi] SharedLibrary  : libhello.so[armeabi] Install        : libhello.so => libs/armeabi/libhello.so


2.5C/C++可执行程序编写


NDK同样可以编译能够在Android系统运行的可执行程序,将该程序通过adb置于android系统中,就可以直接运行它。生成可执行的程序的Androd.mk,如下:


 LOCAL_PATH := (call my−dir)   include (call my-dir)   include (callmydir)   include (CLEAR_VARS)   LOCAL_MODULE = hell_ndk   LOCAL_CFLAGS=-pie -fPIE#生成位置无关的可执行程序 LOCAL_SRC_FILES = main.c include $(BUILD_EXECUTABLE)#生成可执行程序


[armeabi] Compile thumb  : hello <= hello.c[armeabi] Compile thumb  : hello <= main.c[armeabi] Executable     : hello[armeabi] Install        : hello => libs/armeabi/hello


有时,我们在编译可执行程序时,需要链接外部的动态库,那Android.mk该如何编写呢?根据Android.mk一节,我们需要用到两个NDK变量:LOCAL_SHARED_LIBRARIES和LOCAL_LDLIBS。


例如,我们已经生成了libhello.so,如果另一个文件main.c需要调用hello_ndk函数,我们就需要调用libhello.so库,main.c如下:


#include "hello.h"int main(void){    hello_ndk();    return 0;}


Android.mk如下:


 LOCAL_PATH := (call my−dir)   include (call my-dir)   include (callmydir)   include (CLEAR_VARS)   LOCAL_MODULE = hello_ndk  LOCAL_SHARED_LIBRARIES=libhello LOCAL_LDLIBS=-L./ -lhello #libhello.so动态库,注意必须添加-L制定动态库的位置 LOCAL_CFLAGS=-pie -fPIE LOCAL_SRC_FILES = main.c include $(BUILD_EXECUTABLE)


3.Android Standalone Toolchain


如果开发者已经习惯了使用交叉工具完成应用程序的编译工作,NDK提供了了Standalone Toolchian方式,我们可以直接通过NDK生成交叉工具链,然后直接修改Makfile中的CC变量就可以直接生成可以在Android系统上运行的应用程序。


3.1制作StandaloneToolchain


首先,进入NDK的安装目录,进入build/tools目录,然后可以看make-standalone-toolchain.sh即为StandaloneToolchain的生成工具。


可以运行如下命令,查看其使用方法:


$./make-standalone-toolchain.sh --help --help                   Print this help.  --verbose                Enable verbose mode.  --toolchain=       定义交叉工具链名称  --llvm-version=     Specify LLVM version  --stl=             Specify C++ STL [gnustl]  --arch=name>            指定目标平台例如:arm、x86、mips  --abis=             指定所使用的ABI  --ndk-dir=         制定NDK的安装目录  --system=          Specify host system [linux-x86]  --package-dir=     交叉工具链压缩包的存放位置  --install-dir=     Don't create package, install files to  instead.  --platform=     指定Android平台的APIS,具体参考NDK/docs中的NDK Programmer's Guide中的Stable APIs的对应表。


下面为一个具体配置方式:


$./make-standalone-toolchain.sh --toolchain=arm-linux-androideabi-4.8 --ndk-dir=/home/lhl/android_ndk/android-ndk-r10b/ --package-dir=/tmp/ndk-4.8 --platform=android-19


完成后会在/tmp/ndk-4.8目录下生成arm-linux-androideabi-4.8交叉工具链的压缩包,arm-linux-androideabi-4.8.tar.bz2。将其到任意目录配置PATH环境变量后就可以想普通的交叉工具链一样使用了。


附录


NDK提供的开发工具


libc (C library) headerslibm (math library) headersJNinterface headerslibz (Zlib compression) headersliblog (Android logging) headerOpenGL ES 1.1 and OpenGL ES 2.0 (3D graphics libraries) headerslibjnigraphics (Pixel buffer access) header (for Android 2.2 and above).A Minimal set of headers for C++ supportOpenSL ES native audio librariesAndroid native application APIS


NOTE:对于posixpthread的使用我们没有必要在LOCAL_LIBS中指定-lpthread,libc中已经包含了pthread的实现。同样对于rt(real-time extentions (-rt),同样已经包含进了libc库。


相关文章
|
数据采集 编解码 Ubuntu
Android流媒体开发之路二:NDK C++开发Android端RTMP直播推流程序
Android流媒体开发之路二:NDK C++开发Android端RTMP直播推流程序
278 0
|
2月前
|
Java Android开发 C++
🚀Android NDK开发实战!Java与C++混合编程,打造极致性能体验!📊
在Android应用开发中,追求卓越性能是不变的主题。本文介绍如何利用Android NDK(Native Development Kit)结合Java与C++进行混合编程,提升应用性能。从环境搭建到JNI接口设计,再到实战示例,全面展示NDK的优势与应用技巧,助你打造高性能应用。通过具体案例,如计算斐波那契数列,详细讲解Java与C++的协作流程,帮助开发者掌握NDK开发精髓,实现高效计算与硬件交互。
137 1
|
4月前
|
Java Android开发 C++
🚀Android NDK开发实战!Java与C++混合编程,打造极致性能体验!📊
【7月更文挑战第28天】在 Android 开发中, NDK 让 Java 与 C++ 混合编程成为可能, 从而提升应用性能。**为何选 NDK?** C++ 在执行效率与内存管理上优于 Java, 特别适合高性能需求场景。**环境搭建** 需 Android Studio 和 NDK, 工具如 CMake。**JNI** 构建 Java-C++ 交互, 通过声明 `native` 方法并在 C++ 中实现。**实战** 示例: 使用 C++ 计算斐波那契数列以提高效率。**总结** 混合编程增强性能, 但增加复杂性, 使用前需谨慎评估。
141 4
|
3月前
|
JSON Android开发 C++
Android c++ core guideline checker 应用
Android c++ core guideline checker 应用
|
3月前
|
JSON Android开发 数据格式
Android c++ core guideline checker 应用问题之JSON compilation database的定义如何解决
Android c++ core guideline checker 应用问题之JSON compilation database的定义如何解决
|
3月前
|
IDE 开发工具 Android开发
Android c++ core guideline checker 应用问题之clang-tidy 检查后发现的问题如何解决
Android c++ core guideline checker 应用问题之clang-tidy 检查后发现的问题如何解决
|
5月前
|
JSON Android开发 C++
Android c++ core guideline checker 应用
Android c++ core guideline checker 应用
|
6月前
|
缓存 网络协议 编译器
针对Android系统工程师的C/C++学习目录
针对Android系统工程师的C/C++学习目录
58 0
|
6月前
|
算法 Java 开发工具
Android 编译C++
Android 编译C++
113 0
|
存储 Linux Android开发
会C/C++就可以开发Linux/Android应用程序?替代传统串口屏的Yoxios了解一下!
会C/C++就可以开发Linux/Android应用程序?替代传统串口屏的Yoxios了解一下!
203 0