您现在的位置:首页 > 博客 > Android开发 > UI开发 > 正文
android中的surface
http://www.drovik.com/      2013-6-25 18:09:48      来源:www.drovik.com      点击:
android中,对view及其子类,都是画在surface上的。每个window对应一个surface,各surface对象通过surfaceflinger合成到framebuffer,每个surface都是双缓冲,它有一个back buffer和一个front bufferback buffer就是画画的地方,front buffer是用来合成的。

surface
创建Canvas对象(用来管理surface绘图操作),Canvas对应bitmap(存储surface内容)。当调用unlockCanvas()后,back buffer开始变为可用,就开始显示了。有一套机制实现back bufferfront buffer的互换,当要更新时,back bufferfront buffer互换,back buffer变成front buffer
流程
- create a bitmap
- attach a canvas to it
- do the rendering into that canvas
- lockCanvas
- draw your bitmap into the backbuffer
- unlockAndPost


frameworks/base/core/java/android/view/Surface.java
Surface::Surface ()创建一个surface
   
            public Surface(SurfaceSession s,
            int pid, int display, int w, int h, int format, int flags)
        throws OutOfResourcesException {
        mCanvas = new Canvas();
        init(s,pid,display,w,h,format,flags);
    }
frameworks/base/core/jni/android_view_Surface.cpp
Surface_init ()

在这个函数中SurfaceComposerClient 对象被创建。
frameworks/base/libs/ui/SurfaceComposerClient.cpp
SurfaceComposerClient::SurfaceComposerClient ()这个函数非常重要,在这里建立了clientserver之间的桥梁。通过函数_get_surface_manager()获得了一个指向 serverIBinder 对象(具有ISurfaceComposer接口),之后通过这个IBinder就可以跨进程访问Server的功能。接着调用 ISurfaceComposer::createConnection()创建并返回了一个ISurfaceFlingerClient IBinder

frameworks/base/libs/ui/SurfaceComposerClient.cpp
SurfaceComposerClient::createSurface().这个函数中,利用前面获得的ISurfaceFlingerClientIBinder,调用其createSurface

frameworks/base/libs/surfaceflinger/SurfaceFlinger.cpp
BClient::createSurface ()BclientISurfaceFlingerClient派生而来
frameworks/base/libs/surfaceflinger/SurfaceFlinger.cpp
SurfaceFlinger:: createSurface()。这个函数为Surface创建一个对应的Layer



Android GUI
系统:
涉及JAVA框架层的内容:
    andriod.graphics
类 ,对应Skia底层库,提供绘图接口。
    andriod.view.Surface
构建显示界面。
    andriod.view.View
各种UI元素的基类。
    Javax.microedition.khronos.opengles
标准的OpenGL接口。

Pixelflinger
是一个底层的工具库。负责像素级别的基本处理。在system/core/include/pixelflinger/ /system/core/libpixelflinger/


libui
是一个Andriod在本地层次的一个框架库,是GUI系统的中枢。这个库提供接口,其它的库通过类的继承方式来实现。包含颜色格式,Egl窗口,按键及事件处理,surfaceoverlay camera等多个方面的定义。

framework/base/include/ui/ framework/base/libs/ui/中。
包含以下部分:
        format
(颜色格式)部分:需要用pixelflinger中的一些关于数据格式的定义。头文件为PixelFormat.h
Point.h Region.h Rect.h DispleyInfo.h
        Native Windows
(本地窗口)部分:主要是实现下个egl_native_window_t 的类。程序通过调用这个类来完成基本的显示功能。头文件为:
EGLNativeSurface.h  EGLDisplaySurface.h EGLNativeWindowSurface.h
        Key/Event(
按键和事件处理)部分:系统输入的基础,定义按键映射,通过Event事件设备来实现系统输入,头文件为:
EventHub.h  KeycodeLabels.h KeyCharacterMap.h
        Surface
(显示界面)部分:本部分定义显示界面较高层次的接口,包含显示界面的管理功能,头文件为带有Surface字符串的所有文件,这部分只是定义了Surface部分的框架,具体实现是SurfaceFlinger

        Overlay
(显示部分的叠加层)部分:定义一个叠加的显示输出层接口,覆盖在主显示层之上,通常用于视频输出,主要在SurfaceFlinger中实现。头文件为:IOverlay.h   Overlay.h
        Camera
部分:定义摄像头的框架和接口,主要在CameraService部分实现。头文件为带有Camera定符串的所有文件。



输入输出的接口:(主要和linux驱动打交道的)使用FrameBuffer的标准显示驱动和标准事件Input驱动,在libui中使用标准方式实现:
    1
。显示输出的硬件接口:
       
对于andriod的显示部分,需要实现的接口是egl_native_window_t,它是一个OpenGL结构,也是给libEGL使用的。
        EGLNativeSurface.h
定义了类EGLNativeSurface,这个类继承了egl_native_windows_t.
        EGLDisplaySurface.h
定义了类EGLDisplaySurface,继承了EGLNativeSurface,它是最终的实现类。

       
EGLDisplaySurface.cpp所实现的构造函数中调用mapFrameBuffer()函数对驱动程序进行操作如:
status_t EGLDisplaySurface::mapFrameBuffer()
{
    char const * const device_template[] = {
            "/dev/graphics/fb%u",
            "/dev/fb%u",
            0 };
    while ((fd==-1) && device_template[i]) {
        snprintf(name, 64, device_template[i], 0);
        fd = open(name, O_RDWR, 0);
        i++;
    }
   struct fb_fix_screeninfo finfo;
    if (ioctl(fd, FBIOGET_FSCREENINFO, &finfo) == -1)
        return -errno;
    struct fb_var_screeninfo info;
    if (ioctl(fd, FBIOGET_VSCREENINFO, &info) == -1)
        return -errno;
 void* buffer  = (uint16_t*) mmap(
            0, finfo.smem_len,
            PROT_READ | PROT_WRITE,
            MAP_SHARED,
            fd, 0);
    if (buffer == MAP_FAILED)
        return -errno;
    // at least for now, always clear the fb
    memset(buffer, 0, finfo.smem_len);
//
省略了部分内容


    2.
输入的硬件接口:
   
andriod的事件处理部分,主要是向上层提供统一的按键码(KeyCode),是一个整数,上层的Java程序中主要通过这个值来判断系统的实现。在libui中通过对标准的Input驱动的处理来将input值转换成andriod系统的按键码,按键码参考KeyCharacterMay.h头文件。
    EventHub.cpp
文件是输入部分的硬件抽象定义设备节点所在的路径。
    Static const char *device_path =
/dev/input;   //输入设备目录
   
处理过程中将搜索路径下面所有input驱动设备节点,这在openPlatformInput()中通过调用scan_dir()来实现,scan_dir()将会从目录中查找设备,找到后调用open_device()将其打开。
bool EventHub::openPlatformInput(void)
{
//
省略了部分内容
    res = scan_dir(device_path);
    if(res < 0) {
        LOGE("scan dir failed for %s\n", device_path);
        //open_device("/dev/input/event0");
    }
    return true;
}
   
主要事件处理有getEvent()中完成,处理过程是在一个无限循环内,调用阻塞的函数等待事件到来。

bool EventHub::getEvent(int32_t* outDeviceId, int32_t* outType,
        int32_t* outScancode, int32_t* outKeycode, uint32_t *outFlags,
        int32_t* outValue, nsecs_t* outWhen)
{
     
    while(1) {
//
省略了部分内容
             


        for(i = 1; i < mFDCount; i++) {
            if(mFDs[i].revents) {
                    if(mFDs[i].revents & POLLIN) {
                    res = read(mFDs[i].fd, &iev, sizeof(iev));
                   }    
//
省略了部分内容

            }
}
poll
()函数会阻塞程序的运行,直到Input设备的相应事件发生,事件发生后poll()将返回,然后通过read()读取input设备发生的事件代码。

实现事件处理实际经过两个步骤:
    1
,将input设备的整数类型事件转换成表示按键的字条串。
    2
。将表示按键的字符串转换成android的按键码。
   
键盘布局文件(*.kl)将完成第一步转换,运行时的文本文件在system/usr/keylayout目录中。
   
源文件的development/emulator/keymaps/目录中有多个布局文件。
   
第二步是通过查找KEYCODES数组,将literal字符串转换成value的整数值。
(在keyCharacterMap.h文件中KEYCODES表示的数值和android.view.KeyEvent类中数值是一致的,这个java类的路径为frameworks/base/core/java/android/view/KeyEvent.java
 
开发过程中对不同的硬件,只要定不同的键盘布局文件就OK了。Android中一般不需要增加新的按键码。
(在andriod的输入处理经过了两次映射,第一次将Event驱动中的整数按键码映射成字符串,第二次将字符串映射成JavaUI 程序中使用的整数值。如要增加按键在用户程序 中进行处理除了KeyCharacterMap.hKeyEvent.java两个文件 ,还要改两个文件 tools/puppet_master/PuppetMaster.nav_keys.pyframeworks/base/core/res/res/values/attrs.xml



Surface
系统:

包括本地代码和java代码部分,关系如下:
    libui
提供本地的Surface系统框架
    surfaceflinger
完成本地接口实现
    java
框架层次主要调用SurfaceUI 提供接口
   
本地部分可以使用ISurface接口。

 Surface
系统本地接口
   
libui中定义Surface的本地接口,路径frameworks/base/include/ui,主要有以下几个文件;
        Surface.h
        SurfaceComposerClient.h
        ISurface.h
        ISurfaceFlingerClient.h
        IsurfaceComposer.h
Surface.h
SurfaceComposerClient.h是为上层提供的调用接口通过surface系统的JNI提供给java层使用。ISurface.h IsurfaceFlingerClient.h IsurfaceComposer.h 是需要下层去继承和实现的接口,其中 Isurface.h 中的接口可以给本地程序来调用,进而实现图形数据输出功能。
isurfaceComposeer.h接口中,定义了Surface系统的各种枚举值和接口,
class ISurfaceComposer : public IInterface
{
public:
    DECLARE_META_INTERFACE(SurfaceComposer);

    enum { // (keep in sync with Surface.java)
        eHidden             = 0x00000004,
           //
省略了部分内容
    };
//
省略了部分内容


调用createConnection()接口将构建一个ISurfaceFlingerClient,eFXSurfaceNormal eFXSurfaceBlur eFXSurfaceDim  eFXSurfaceMask 表示不同类型的Surface层次,它们和java代码是对应的(对应surface.java文件)



SurfaceFlinger
本地代码:
SurfaceFlinger
Surface的本地实现。实现Surface的建立。控制,管理等功能。其路径为:
frameworks/base/libs/surfaceflinger
 
surfaceFlinger.hSurfaceFlinger.cpp 文件中,SurfaceFlinger类继承IsurfaceComposer,是一个核心的实现。SurfaceFlinger::BClient类继承了ISurfaceFlingerClient。另一个重要的部分就是提供不同的层(layer),用于构建不同的显示界面。
surfaceflinger内部有一个表示surface层次的类,就是LayerBase,它提供了与上层相关的通用接口,LayerBaseClient继承LayerBase,而LayerBaseClient的内部类Surface又继承Bnsurface
这个LayBaseClient是各种层的一个基类,它被以下其它几个类继承:LayerLayerBufferLayerDim,和LayerBlur。这几个类则表示了几种不同的“层”,
以上几个类中,LayerBuffer中的surfaceBuffer继承了本地的ISurface接口,也就是说,本地使用的ISurface接口在android的图形系统中只有LayerBuffer中的一个实现。

在上层的程序调用过程中,创建一个surface过程如下:
    1
。调用libui接口SurfaceComposerClient::createSurface();
    2
。调用ISurfaceFlingerClient::creatSurface();
    3
。由于继承关系,实际上调用 的是SurfaceFlinger中的接口,即BClient::creatSurface();
    4
。继续调用SurfaceFlinger::createSurface()函数,其处理过程如下:
   
sp SurfaceFlinger::createSurface(ClientID clientId, int pid,
        ISurfaceFlingerClient::surface_data_t* params,
        DisplayID d, uint32_t w, uint32_t h, PixelFormat format,
        uint32_t flags)
{
    LayerBaseClient* layer = 0;
    sp surfaceHandle;
    Mutex::Autolock _l(mStateLock);
    Client* const c = mClientsMap.valueFor(clientId);
    if (UNLIKELY(!c)) {
        LOGE("createSurface() failed, client not found (id=%d)", clientId);
        return surfaceHandle;
    }

    //LOGD("createSurface for pid %d (%d x %d)", pid, w, h);
    int32_t id = c->generateId(pid);
    if (uint32_t(id) >= NUM_LAYERS_MAX) {
        LOGE("createSurface() failed, generateId = %d", id);
        return surfaceHandle;
    }

    switch (flags & eFXSurfaceMask) {
        case eFXSurfaceNormal:
            if (UNLIKELY(flags & ePushBuffers)) {
                layer = createPushBuffersSurfaceLocked(c, d, id, w, h, flags);
            } else {
                layer = createNormalSurfaceLocked(c, d, id, w, h, format, flags);
            }
            break;
        case eFXSurfaceBlur:
            layer = createBlurSurfaceLocked(c, d, id, w, h, flags);
            break;
        case eFXSurfaceDim:
            layer = createDimSurfaceLocked(c, d, id, w, h, flags);
            break;
    }

    if (layer) {
        setTransactionFlags(eTransactionNeeded);
        surfaceHandle = layer->getSurface();
        if (surfaceHandle != 0)
            surfaceHandle->getSurfaceData(params);
    }

    return surfaceHandle;
}

创建surface时,调用 createSurface(),创建各个层后,分别调用不同层中的getSurface()接口来得到一个ISurface类型的实例,然后在SurfaceComposerClient::createSurface()中得到surface并将其返回。
实际是根据参数flags选择使用不同的层:eFXSurfaceNormal对应的层是LayerLayerBuffer; eFXSurfaceBlur对应的层是LayerBlur eFXSurfaceDim对应 的层是LayerDim
如果将参数指定为普通,一般情况下会建立Layer类,当ePushBuffers为真时才会建立 LayerBuffer类。建立 LayerLayerBuffer分别调用的是createNormalSurfaceLocked()函数和createPushBufferSurfacLocked()函数。
createNormalSurfaceLocked
()先要建立一个Layer类,再向其中设置一个Buffer,然后增加层。
createPushBufferSurfacLocked
()情况比较简单,只要建立 一个LayerBuffer的类将其加入层即可。
二者的区别是:LayBuffer是一个push类型的层,通常要使用队列的方式将显示的内容“推”入其中,Layer是一个普通 的层,建立时要将一个内存设置到其中 。
以设置大小 为例 ;
对一个Surface进行设置的过程如下:
    1
,调用libuisurface接口 setSize();
    2
, 实际调用 的是surfacecomposerClient::setSize(),在其中的参数surface的大小,并将其成员what设置为eSizeChanged
    3
, 由于逻辑关系,由IsurfaceFlingerClient::setState()函数进行处理。
    4
,由于继承关系,实际调用 的是类SurfaceFlinger中的BClient::setState()。
    5
, 进一步调用surfaceFlinger::setClientState()函数。
在代码处理中,根据不同的状态变化命令来进行处理,最终调用的是各个“层”的setSize()函数,之后的内容由几个层的不同实现来进行处理。


Surfaceflinger
和显示硬件的接口
显示设备由DisplayHardware目录中的DisplayHardware.cpp文件来实现的,其中创建了一个DisplayHardware来作为主要的显示界面。
  mDisplaySurface = new EGLDisplaySurface();
EGLDisplaySurface
类是在libui中实现的,它直接操作FrameBuffer的硬件驱动。在SurfaceFlinger.cpp中,将创建类DisplayHardware为实例,从而获取实际的显示设备,在上面进行显示输出。
此外,surfaceflinger可以使用可选的硬件模块copybit作为2D图形处理部分的加速器。这部分的接口在frameworks/base/include/core/java/android/hardware目录的copybit.h文件定义 的相关接口。作为硬件模块使用,这个模块在DisplayHardware初始化的过程中被打开。


Surface
javaJNI代码
Surface
部分的JNI代码路径是:
frameworks/base/core/jni/android_view_Surface.cpp
它主要提供了android.view.SurfaceSessionandroid.view.Surface两个java类,分别调用SurfaceComposerClientSurface两个本地类来完成实现。
Surface
部分的Java代码的路径是:
frameworks/base/core/java/android/view/
由此对应的java类在android.view包中,除了上面提到的类surfaceSessionSurface之外,与其相关的还有接口SurfaceHolder和类SurfaceView
Android.view.Surface
表示一个可以绘制图形的界面,它实际上是调用 底层的Surface接口来实现控制的硬件载体。
实际上,在java框架中,所有UI元素的基类都是android.view.View,这些UI元素本身也是基于Surface2D绘图函数来实现的,如果要在java程序中使用一个可以进行自由绘制的界面,那么就需要使用类android.view.SurfaceView,这个类也继承了android.view.View,因此也是android中的一个UI元素,andriod.view.SurfaceHolderandroid.view.SurfaceView 中包含的一个接口,用于处理Surface相关的事件。
surface.java中定义的整数常量和本地的ISurfaceComposer.h是具有对应关系的)





Skia
2D图形系统:
skia
是一个C++本地代码库,路径为external/skia/
包含3个库

    Core Cg
核心图形库: libcorecg.so
    GL (Skia
图形库)
liblibsgl.so
    skia-opengl glue library : libskiagl.so
核心库是libcorecg.so它是skia中最基础的库,其源代码主要在src/core/目录中,提供一系列基本功能,

Skia
图形库:liblibsgl.soskia系统主要的库,它包含移植层,图形绘制,图像编解码,效果等方面内容。其源代码主要在src/effects , src/images/ , src/ports/,  src/core/ , src/utils/  目录 中。liblibsgl.so需要连接libcorecg.so,以及被其调用 的图像的编解码库,字体处理的库等。
libskiagl.so
skiaOpenGL 相关联的库,其源代码在src/gl目录中。

skia
对上层有众多的接口,接口头文件在include目录 中,skia API中最主要的SKCavans类,这个类提供了众多的绘制功能。事实上,整个androidGUI系统的底层绘制,就是这个类来完成的。其头文件 和源代码路径分别是:include/core/SkCavans.h src/core/SkCavans.cpp
SkCanvas
类有两个构造函数,其参数类型分别是SkBitmapSkDevice,分别表示Skia进行绘制的目标。事实上,Skia的基本功能 就是一个绘制工具,这个绘制工具和绘制的目标是无关的。SkBitmap 可以视为一个表示位图区域的内存,除了一般的内存首指针和大小 之外,还包括宽,高和像素格式等信息,在这块内存上,可以进行skia图形系统的绘制工作。

SkCanvas
的主要绘制功能有3种: 基本图形绘制 (如drawARGB , drawLine 函数)图像文件绘制(如drawBitmap函数)和文本绘制(如drawText函数).

Skia
的图像编码部分:

skia
的图像编码部分是一个相对独立 的部分,其接口分别在include/image目录下的SkImageDecoder.h 和skImageEncoder.h中定义:

SkImageDecoder
既可以作为动态类使用,又可以使用静态函数,也支持同步和异步方式解码,它可以把图像文件或者流解码到skia的内部内存SkBitmap中。
SkImageEncoder
和解码器类似,完成 的是编码工作,
skia
的解码器和编码器都是接口,需要具体的类去实现,在src/images中的几个源文件通过继承SkImageDecoderSkImageEncoder来实现解码器和编码器,如 SkImageDecoder_libjpeg.cpp 通过调用libjpeg 库实现了JPEG的解码。
如有其它 的解码器和编码器,也可以通过 SkImageEncoderSkImageDecoder的类来实现。




Android
图形系统的JNI接口
android
图形系统和skia底层库联系比较紧密,android图形系统的JNI提供 了从skia底层到java层的支持,JNI代码路径为:frameworks/base/core/jni/android/grphic/
Canvas.cpp
JNI中的核心接口,为java上层的android.graphics.Canvas类提供了支持,其中 ,initRaster()initGL()两个函数将和Skia本地库联系起来。即通过建立skia本地库的Cavvas来建立给java层使用的Canvas,除此外,图形JNI还通过JNI接口,提供andriod.graphics.Region , android.graphics.Bitmap, android.graphics.Picture , android.graphics.Matrix 等多个java类的支持,这一般调用的是skia中同名文件中的函数。


Android
的图形包:
android
图形类的包是android.graphics它通过调用图形系统的JNI提供了对java框架中的图形系统的支持,代码路径为:frameworks/graphic/java/android/graphics/
Canvas.java
定义了android图形系统中最为重要的一个类:android.graphics.Canvas. Canvas类处理“draw“的调用,当绘制(draw)内容时需要4个基本组件,一个保持像素的Bitmap,一个处理绘制调用 的canvas(写入Bitmap),绘制 的内容(如,RectPathtextBitmap)和一个paint(用开描述颜色和样式)。Bitmap.java文件实现了类android.graphics.Bitmap, 它表示内存中的一个位图。

Canvas
是一个比较基础的图形类,android中的UI 元素也是通过调用Canvas类来构建的。在View.java文件 中,实现的类是android.view.View, 通过建立这个Canvas类来构建绘画的基础。


Android
系统的OpenGL系统与3D图形系统
分本地代码和java框架代码两部分。
本地代码实现OpenGL接口的库,java框架层,javax.microedition.khronos.openglesjava标准的OpenGL包。

本地整体结构:
主要内容在frameworks/base/opengl/中,本地代码头文件 路径为:
        frameworks/base/opengl/include/EGL/
        frameworks/base/opengl/include/GLES/
源代码目录:
        frameworks/base/opengl/libagl/
        frameworks/base/opengl/libs/
编绎后生成3个库:
    libGLESv1_CM.so: OpenGL Es
库的封装,对应libs/GLES_CM目录 中的文件
    libEGL.so:EGL
库,OpenGL Es库的封装,对应libs/EGL目录 中的文件
    libagl.so: OpenGL
的软件实现库,对应libagl目录中的文件。

Android
OpenGL实现方式:
OpenGL
本地库可使用软件库和硬件库这两种不同的 方式来实现。如果是软件实现,则用libagl.so库,如果是硬件实现,则使用libhgl.so库。
libs/EGL/egl.cpp文件中,选择使用这两个库中的一个,主要操作在eglGetDisplay()函数中实现。

load_driver()
函数实际上将通过dlopen()方式打开库,默认先去找OpenGL的软件实现libagl.so 当设置使用OpenGL的硬件实现(在init.rc中设置属性debug.egl.hw)时,则使用OpenGL的硬件实现libagl.so.然后用dlsysm方式打开其中支持API的符号,
gl_hooks_t
结构描述了OpenGL系统所支持的各种API。实际的符号将在gl_entries.inegl_entries.in两个文件中定义。各种函数以GL_ENTRYEGL_ENTRY的方式来进行描述。


Android
openGL的本地测试代码:
路径为:frameworks/base/opengl/tests 包含几个独立 的测试程序,


openGL
JNI代码:
openGL
向上提供的JNI接口,主要由以下两个文件提供:
    frameworks/base/core/jni/com_google_android_gles_jni_GLImpl.cpp
    frameworks/base/core/jni/com_google_android_gles_jni_EGLImpl.cpp
这是包com.google.android.gles_jni中的两个类,分别是EGLImplGLImpl,分别对应eglgl的实现。
其中,EGLImpl中的各个接口负责一些管理功能,而GLImpl中的各个接口对应于OpenGLGLES/gl.h头文件中定义的各个OpenGL功能函数。


OpenGL
java
andriod中使用openGL常要结合使用javax.microedition.khronos.opengles android.opengl包。
实现方法是使用一个类来继承OpenGL java的标准类,通过实现这个类来实现,在java层只要使用标准类。
OpenGL
java类是javax中的一部分,这个包是javax.microedition.khronos.egl javax.microedition.khronos.opengls路径为:
    opengl/java/javax/microedition/khronos/egl/
    opengl/java/javax/microedition/khronos/opengles/
egl类中主要的文件是GL10.java GL11.java ;opengles中主要的文件 是EGL10.java;
android
使用继承的方法实现opengl这个继承的包为com.google.android.gles_jni 其路径为:

    opengl/java/com/google/andriod/gles_jni/
事实上,EGLImplGLImpl的大部分函数是通过JNI调用本地的OpenGL程序来实现的。
java应用层不会调com.google.android.gles_jni包中的类,只会调用标准包avax.microedition.khronos.opengles中的接口。
另个,android.opengl包提供了openGL的标准接口到android系统的媒介,路径为:
    opengl/java/android/opengl/
其中主要的类调用com.google.android.gles_jni包中的类和android的基础GUI系统的类实现GLSurfaceView
GLSurfaceView
继承了SurfaceView surfaceView又继承View,因此GLSurfaceView本身也是个UI元素,在androidjava应用程序层使用OpenGL,具体的实现其实是继承GLSurfaceView类并调用OpenGL标准的接口。
分享到:
发表评论(0)
姓名 *
评论内容 *
验证码 *图片看不清?点击重新得到验证码