您的位置:首页 > 移动开发 > Android开发

android4.0.3源码之鼠标光标绘制简略版

2013-03-21 14:50 267 查看
搞定了android4.0.3的触摸屏的适配后(其实只要驱动没有什么问题,加个配置文件就很容易搞定了),新的任务就下来了,就是要寻找android4.0.3中的鼠标是如何绘画的,哪里createSurface,哪里分配空间的。因为如果是软鼠标的话,在播放视频或者玩大型游戏的时候是很卡的,而走overlay,硬鼠标的话,就显得很灵敏了。艰巨的任务啊,看了我好久还是没有找到鼠标是在哪里绘制的。因为android2.3是在WindowManagerService里面new了一个surface,接着画了几条线而得到的,而android4.0.3中,找了n久还是没有发现,可恶的android4.0.3,网上资料也没有介绍。

期间也添加了android中的power、volumeup、volumedown三个按键,虽然基本功能实现了,但是还是没有搞明白那个长按电源键出来关机和重启等选项是怎么实现的。无聊之余,把KEY的DOWN和UP顺序对调了下,居然有点效果,在此非常疑惑。就是我本来实现的时候,input子系统上报是

[html] view
plaincopy

input_report_key(&button_dev, KEY_POWER, 1);

[html] view
plaincopy

input_report_key(&button_dev, KEY_POWER, 0);

这样可以实现电源键的锁屏功能。但当我交换了上报的顺寻后也就是

[html] view
plaincopy



[html] view
plaincopy

input_report_key(&button_dev, KEY_POWER, 0);

<pre class="html" name="code">input_report_key(&button_dev, KEY_POWER, 1);</pre><br>

<pre></pre>

<p>居然是那个长按键的功能,不知道哪位高手可以指导一下,这个还是得等找到了鼠标绘制的地方后再深入下去吧。 一直以为surface的创建是在WindowManagerService中实现的,没想到,这个android4.0.3居然把surface创建,鼠标绘制放在了input下了,这个可恶的SpriteController.cpp,目录/frameworks/base/services/input/SpriteController.cpp。找得心灰意冷啊。居然中文解释说是精灵,怪不得这么难找呢。网上找了下,说java中好像是有一个Sprite的东东,说是什么绘图时候用的,这不正好是鼠标吗,小小的精灵,难怪鼠标也是天蓝色透明的了,看来写这个代码的高手还真的别有一番寓意啊,在此膜拜下。

由于本人还是菜鸟,学电子的孩子,C++和Java接触好少,看不大懂,只能简单的了解下。先看看Sprite类的定义。注释的已经很清楚了,六级都没过的我也能看懂,相信难不倒你了。</p>

<pre class="html" name="code">class Sprite : public RefBase {

protected:

Sprite() { }

virtual ~Sprite() { }



public:

enum {

// The base layer for pointer sprites.

BASE_LAYER_POINTER = 0, // reserve space for 1 pointer



// The base layer for spot sprites.

BASE_LAYER_SPOT = 1, // reserve space for MAX_POINTER_ID spots

};



/* Sets the bitmap that is drawn by the sprite.

* The sprite retains a copy of the bitmap for subsequent rendering. */

virtual void setIcon(const SpriteIcon& icon) = 0;



inline void clearIcon() {

setIcon(SpriteIcon());

}



/* Sets whether the sprite is visible. */

virtual void setVisible(bool visible) = 0;



/* Sets the sprite position on screen, relative to the sprite's hot spot. */

virtual void setPosition(float x, float y) = 0;



/* Sets the layer of the sprite, relative to the system sprite overlay layer.

* Layer 0 is the overlay layer, > 0 appear above this layer. */

virtual void setLayer(int32_t layer) = 0;



/* Sets the sprite alpha blend ratio between 0.0 and 1.0. */

virtual void setAlpha(float alpha) = 0;



/* Sets the sprite transformation matrix. */

virtual void setTransformationMatrix(const SpriteTransformationMatrix& matrix) = 0;

};





</pre>

<p><br>

一开始的搜寻路线是找到鼠标那个png图片是哪里调用的,鼠标早就不叫mouse了,cursor或者pointer早就替换了mouse了。鼠标的图标是在EventHub发现了鼠标这个驱动</p>

<p>(android4.0.3中应该是cursor这个驱动)后再由frameworks/base/core/java/android/view/PointerIcon.java中的getSystemIcon()导入,然后再由SpriteController::SpriteImpl::setIcon()(c++的类的继承,虚函数,各种纠结,只能稍微理解)设置为Sprite这个精灵的资源。每次鼠标移动后,InputReader线程总会获取这个坐标值,然后InputDispatch会分发给WindowManagerService,而ViewRootImpl会读取等等,这个网上讲得已经很详细了。<br>

对于鼠标,应该也是pointer,input目录下面还有一个PointerController,看着这个名字,就知道了,这个肯定是控制鼠标啊, 触摸屏啊什么的。具体看注释吧,写得够直</p>

<p>白的。</p>

<pre class="html" name="code">/**

* Interface for tracking a mouse / touch pad pointer and touch pad spots.

*

* The spots are sprites on screen that visually represent the positions of

* fingers

*

* The pointer controller is responsible for providing synchronization and for tracking

* display orientation changes if needed.

*/

class PointerControllerInterface : public virtual RefBase {

protected:

PointerControllerInterface() { }

virtual ~PointerControllerInterface() { }



public:

/* Gets the bounds of the region that the pointer can traverse.

* Returns true if the bounds are available. */

virtual bool getBounds(float* outMinX, float* outMinY,

float* outMaxX, float* outMaxY) const = 0;



/* Move the pointer. */

virtual void move(float deltaX, float deltaY) = 0;



/* Sets a mask that indicates which buttons are pressed. */

virtual void setButtonState(int32_t buttonState) = 0;



/* Gets a mask that indicates which buttons are pressed. */

virtual int32_t getButtonState() const = 0;



/* Sets the absolute location of the pointer. */

virtual void setPosition(float x, float y) = 0;



/* Gets the absolute location of the pointer. */

virtual void getPosition(float* outX, float* outY) const = 0;



enum Transition {

// Fade/unfade immediately.

TRANSITION_IMMEDIATE,

// Fade/unfade gradually.

TRANSITION_GRADUAL,

};



/* Fades the pointer out now. */

virtual void fade(Transition transition) = 0;



/* Makes the pointer visible if it has faded out.

* The pointer never unfades itself automatically. This method must be called

* by the client whenever the pointer is moved or a button is pressed and it

* wants to ensure that the pointer becomes visible again. */

virtual void unfade(Transition transition) = 0;



enum Presentation {

// Show the mouse pointer.

PRESENTATION_POINTER,

// Show spots and a spot anchor in place of the mouse pointer.

PRESENTATION_SPOT,

};



/* Sets the mode of the pointer controller. */

virtual void setPresentation(Presentation presentation) = 0;



/* Sets the spots for the current gesture.

* The spots are not subject to the inactivity timeout like the pointer

* itself it since they are expected to remain visible for so long as

* the fingers are on the touch pad.

*

* The values of the AMOTION_EVENT_AXIS_PRESSURE axis is significant.

* For spotCoords, pressure != 0 indicates that the spot's location is being

* pressed (not hovering).

*/

virtual void setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex,

BitSet32 spotIdBits) = 0;



/* Removes all spots. */

virtual void clearSpots() = 0;

};





/*

* Pointer resources.

*/

struct PointerResources {

SpriteIcon spotHover;

SpriteIcon spotTouch;

SpriteIcon spotAnchor;

};



</pre>

<p><br>

而PointerController是继承PointerControllerInterface的。突然看到这下面那两行英文,这么熟悉,这么刺眼,顿时觉得,晴空霹雳啊,明明写得这么清楚了,画pointer精灵到surface,这,,让我情何以堪,找了这么久,苍天呢,都怪C++不够好有木有,英语不够牛有不有。其实android中很多函数的功能都在他的类的定义中有说明的,何不仔细研究.h呢?而把大把大把的时间浪费在.cpp中了,经验啊,经验不足啊。</p>

<pre class="html" name="code">/*

* Tracks pointer movements and draws the pointer sprite to a surface.

*

* Handles pointer acceleration and animation.

*/

class PointerController : public PointerControllerInterface, public MessageHandler

好了,发泄好了,而在Sprite中,最重要的就是SpriteController::doUpdateSprites()这个函数了,这里就是鼠标绘制和移动后更新的全过程了。



void SpriteController::doUpdateSprites() {

// Collect information about sprite updates.

// Each sprite update record includes a reference to its associated sprite so we can

// be certain the sprites will not be deleted while this function runs. Sprites

// may invalidate themselves again during this time but we will handle those changes

// in the next iteration.

Vector<SpriteUpdate> updates;

size_t numSprites;

{ // acquire lock

AutoMutex _l(mLock);



numSprites = mLocked.invalidatedSprites.size();

for (size_t i = 0; i < numSprites; i++) {

const sp<SpriteImpl>& sprite = mLocked.invalidatedSprites.itemAt(i);



updates.push(SpriteUpdate(sprite, sprite->getStateLocked()));

sprite->resetDirtyLocked();

}

mLocked.invalidatedSprites.clear();

} // release lock



// Create missing surfaces.

bool surfaceChanged = false;

for (size_t i = 0; i < numSprites; i++) {

SpriteUpdate& update = updates.editItemAt(i);



if (update.state.surfaceControl == NULL && update.state.wantSurfaceVisible()) {

update.state.surfaceWidth = update.state.icon.bitmap.width();

update.state.surfaceHeight = update.state.icon.bitmap.height();

update.state.surfaceDrawn = false;

update.state.surfaceVisible = false;

update.state.surfaceControl = obtainSurface(

update.state.surfaceWidth, update.state.surfaceHeight);

if (update.state.surfaceControl != NULL) {

update.surfaceChanged = surfaceChanged = true;

}

}

}



// Resize sprites if needed, inside a global transaction.

bool haveGlobalTransaction = false;

for (size_t i = 0; i < numSprites; i++) {

SpriteUpdate& update = updates.editItemAt(i);



if (update.state.surfaceControl != NULL && update.state.wantSurfaceVisible()) {

int32_t desiredWidth = update.state.icon.bitmap.width();

int32_t desiredHeight = update.state.icon.bitmap.height();

if (update.state.surfaceWidth < desiredWidth

|| update.state.surfaceHeight < desiredHeight) {

if (!haveGlobalTransaction) {

SurfaceComposerClient::openGlobalTransaction();

haveGlobalTransaction = true;

}



status_t status = update.state.surfaceControl->setSize(desiredWidth, desiredHeight);

if (status) {

LOGE("Error %d resizing sprite surface from %dx%d to %dx%d",

status, update.state.surfaceWidth, update.state.surfaceHeight,

desiredWidth, desiredHeight);

} else {

update.state.surfaceWidth = desiredWidth;

update.state.surfaceHeight = desiredHeight;

update.state.surfaceDrawn = false;

update.surfaceChanged = surfaceChanged = true;



if (update.state.surfaceVisible) {

status = update.state.surfaceControl->hide();

if (status) {

LOGE("Error %d hiding sprite surface after resize.", status);

} else {

update.state.surfaceVisible = false;

}

}

}

}

}

}

if (haveGlobalTransaction) {

SurfaceComposerClient::closeGlobalTransaction();

}



// Redraw sprites if needed.

for (size_t i = 0; i < numSprites; i++) {

SpriteUpdate& update = updates.editItemAt(i);



if ((update.state.dirty & DIRTY_BITMAP) && update.state.surfaceDrawn) {

update.state.surfaceDrawn = false;

update.surfaceChanged = surfaceChanged = true;

}



if (update.state.surfaceControl != NULL && !update.state.surfaceDrawn

&& update.state.wantSurfaceVisible()) {

sp<Surface> surface = update.state.surfaceControl->getSurface();

Surface::SurfaceInfo surfaceInfo;

status_t status = surface->lock(&surfaceInfo);

if (status) {

LOGE("Error %d locking sprite surface before drawing.", status);

} else {

SkBitmap surfaceBitmap;

ssize_t bpr = surfaceInfo.s * bytesPerPixel(surfaceInfo.format);

surfaceBitmap.setConfig(SkBitmap::kARGB_8888_Config,

surfaceInfo.w, surfaceInfo.h, bpr);

surfaceBitmap.setPixels(surfaceInfo.bits);



SkCanvas surfaceCanvas;

surfaceCanvas.setBitmapDevice(surfaceBitmap);



SkPaint paint;

paint.setXfermodeMode(SkXfermode::kSrc_Mode);

surfaceCanvas.drawBitmap(update.state.icon.bitmap, 0, 0, &paint);



if (surfaceInfo.w > uint32_t(update.state.icon.bitmap.width())) {

paint.setColor(0); // transparent fill color

surfaceCanvas.drawRectCoords(update.state.icon.bitmap.width(), 0,

surfaceInfo.w, update.state.icon.bitmap.height(), paint);

}

if (surfaceInfo.h > uint32_t(update.state.icon.bitmap.height())) {

paint.setColor(0); // transparent fill color

surfaceCanvas.drawRectCoords(0, update.state.icon.bitmap.height(),

surfaceInfo.w, surfaceInfo.h, paint);

}



status = surface->unlockAndPost();

if (status) {

LOGE("Error %d unlocking and posting sprite surface after drawing.", status);

} else {

update.state.surfaceDrawn = true;

update.surfaceChanged = surfaceChanged = true;

}

}

}

}



// Set sprite surface properties and make them visible.

bool haveTransaction = false;

for (size_t i = 0; i < numSprites; i++) {

SpriteUpdate& update = updates.editItemAt(i);



bool wantSurfaceVisibleAndDrawn = update.state.wantSurfaceVisible()

&& update.state.surfaceDrawn;

bool becomingVisible = wantSurfaceVisibleAndDrawn && !update.state.surfaceVisible;

bool becomingHidden = !wantSurfaceVisibleAndDrawn && update.state.surfaceVisible;

if (update.state.surfaceControl != NULL && (becomingVisible || becomingHidden

|| (wantSurfaceVisibleAndDrawn && (update.state.dirty & (DIRTY_ALPHA

| DIRTY_POSITION | DIRTY_TRANSFORMATION_MATRIX | DIRTY_LAYER

| DIRTY_VISIBILITY | DIRTY_HOTSPOT))))) {

status_t status;

if (!haveTransaction) {

SurfaceComposerClient::openGlobalTransaction();

haveTransaction = true;

}



if (wantSurfaceVisibleAndDrawn

&& (becomingVisible || (update.state.dirty & DIRTY_ALPHA))) {

status = update.state.surfaceControl->setAlpha(update.state.alpha);

if (status) {

LOGE("Error %d setting sprite surface alpha.", status);

}

}



if (wantSurfaceVisibleAndDrawn

&& (becomingVisible || (update.state.dirty & (DIRTY_POSITION

| DIRTY_HOTSPOT)))) {

status = update.state.surfaceControl->setPosition(

update.state.positionX - update.state.icon.hotSpotX,

update.state.positionY - update.state.icon.hotSpotY);

if (status) {

LOGE("Error %d setting sprite surface position.", status);

}

}



if (wantSurfaceVisibleAndDrawn

&& (becomingVisible

|| (update.state.dirty & DIRTY_TRANSFORMATION_MATRIX))) {

status = update.state.surfaceControl->setMatrix(

update.state.transformationMatrix.dsdx,

update.state.transformationMatrix.dtdx,

update.state.transformationMatrix.dsdy,

update.state.transformationMatrix.dtdy);

if (status) {

LOGE("Error %d setting sprite surface transformation matrix.", status);

}

}



int32_t surfaceLayer = mOverlayLayer + update.state.layer;

if (wantSurfaceVisibleAndDrawn

&& (becomingVisible || (update.state.dirty & DIRTY_LAYER))) {

status = update.state.surfaceControl->setLayer(surfaceLayer);

if (status) {

LOGE("Error %d setting sprite surface layer.", status);

}

}



if (becomingVisible) {

status = update.state.surfaceControl->show(surfaceLayer);

if (status) {

LOGE("Error %d showing sprite surface.", status);

} else {

update.state.surfaceVisible = true;

update.surfaceChanged = surfaceChanged = true;

}

} else if (becomingHidden) {

status = update.state.surfaceControl->hide();

if (status) {

LOGE("Error %d hiding sprite surface.", status);

} else {

update.state.surfaceVisible = false;

update.surfaceChanged = surfaceChanged = true;

}

}

}

}



if (haveTransaction) {

SurfaceComposerClient::closeGlobalTransaction();

}



// If any surfaces were changed, write back the new surface properties to the sprites.

if (surfaceChanged) { // acquire lock

AutoMutex _l(mLock);



for (size_t i = 0; i < numSprites; i++) {

const SpriteUpdate& update = updates.itemAt(i);



if (update.surfaceChanged) {

update.sprite->setSurfaceLocked(update.state.surfaceControl,

update.state.surfaceWidth, update.state.surfaceHeight,

update.state.surfaceDrawn, update.state.surfaceVisible);

}

}

} // release lock



// Clear the sprite update vector outside the lock. It is very important that

// we do not clear sprite references inside the lock since we could be releasing

// the last remaining reference to the sprite here which would result in the

// sprite being deleted and the lock being reacquired by the sprite destructor

// while already held.

updates.clear();

}



</pre>

<p><br>

看到 update.state.surfaceControl = obtainSurface(update.state.surfaceWidth, update.state.surfaceHeight); 其实就是创建了一个surface,在一开始进入的主界面</p>

<p>的时候,鼠标是没有的,当移动了下鼠标或者点击了下鼠标后就执行这个创建了这个surface,有logcat后的信息为证(只可惜公司nta内的东西拿不出来)。</p>

<pre class="html" name="code">sp<SurfaceControl> SpriteController::obtainSurface(int32_t width, int32_t height) {

ensureSurfaceComposerClient();



sp<SurfaceControl> surfaceControl = mSurfaceComposerClient->createSurface(

String8("Sprite"), 0, width, height, PIXEL_FORMAT_RGBA_8888);

if (surfaceControl == NULL || !surfaceControl->isValid()

|| !surfaceControl->getSurface()->isValid()) {

LOGE("Error creating sprite surface.");

return NULL;

}

return surfaceControl;

}



</pre>

<p><br>

而创建完后,必须得分配空间啊,surface的信息也得录入啊。具体就在下面了</p>

<p> </p>

<pre class="html" name="code">sp<Surface> surface = update.state.surfaceControl->getSurface();

Surface::SurfaceInfo surfaceInfo;

status_t status = surface->lock(&surfaceInfo);

</pre>

<p><br>

看到lock()了吗?就是这,继续跟进代码<br>

</p>

<pre class="html" name="code">status_t Surface::lock(SurfaceInfo* other, Region* inOutDirtyRegion) {

ANativeWindow_Buffer outBuffer;



ARect temp;

ARect* inOutDirtyBounds = NULL;

if (inOutDirtyRegion) {

temp = inOutDirtyRegion->getBounds();

inOutDirtyBounds = &temp;

}



status_t err = SurfaceTextureClient::lock(&outBuffer, inOutDirtyBounds);



if (err == NO_ERROR) {

other->w = uint32_t(outBuffer.width);

other->h = uint32_t(outBuffer.height);

other->s = uint32_t(outBuffer.stride);

other->usage = GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_SW_WRITE_OFTEN;

other->format = uint32_t(outBuffer.format);

other->bits = outBuffer.bits;

}



if (inOutDirtyRegion) {

inOutDirtyRegion->set( static_cast<Rect const&>(temp) );

}



return err;

}



</pre>

<p><br>

也行你会说,怎么这个lock有两个参数的,明显不对吗,好吧,看来我的C++还是有点基础的,后悔以前搞ACM的时候只是一个main函数到底,没有好好用C++来实现,要不然现在也肯定是个大牛了。看看这个 status_t lock(SurfaceInfo* info, Region* dirty = NULL);明白了吧?使用了默认的参数,刚开始还以为这个lock是假的。哈哈接着,看到了SurfaceTextureClient::lock(&outBuffer, inOutDirtyBounds);了吧,好明显的说,接着看看这个函数到底干了什么了</p>

<pre class="html" name="code">status_t SurfaceTextureClient::lock(

ANativeWindow_Buffer* outBuffer, ARect* inOutDirtyBounds)

{

if (mLockedBuffer != 0) {

LOGE("Surface::lock failed, already locked");

return INVALID_OPERATION;

}



if (!mConnectedToCpu) {

int err = SurfaceTextureClient::connect(NATIVE_WINDOW_API_CPU);

if (err) {

return err;

}

// we're intending to do software rendering from this point

setUsage(GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_SW_WRITE_OFTEN);

}



ANativeWindowBuffer* out;

status_t err = dequeueBuffer(&out);

LOGE_IF(err, "dequeueBuffer failed (%s)", strerror(-err));

if (err == NO_ERROR) {

sp<GraphicBuffer> backBuffer(GraphicBuffer::getSelf(out));

err = lockBuffer(backBuffer.get());

LOGE_IF(err, "lockBuffer (handle=%p) failed (%s)",

backBuffer->handle, strerror(-err));

if (err == NO_ERROR) {

const Rect bounds(backBuffer->width, backBuffer->height);



Region newDirtyRegion;

if (inOutDirtyBounds) {

newDirtyRegion.set(static_cast<Rect const&>(*inOutDirtyBounds));

newDirtyRegion.andSelf(bounds);

} else {

newDirtyRegion.set(bounds);

}



// figure out if we can copy the frontbuffer back

const sp<GraphicBuffer>& frontBuffer(mPostedBuffer);

const bool canCopyBack = (frontBuffer != 0 &&

backBuffer->width == frontBuffer->width &&

backBuffer->height == frontBuffer->height &&

backBuffer->format == frontBuffer->format);



if (canCopyBack) {

// copy the area that is invalid and not repainted this round

const Region copyback(mOldDirtyRegion.subtract(newDirtyRegion));

if (!copyback.isEmpty())

copyBlt(backBuffer, frontBuffer, copyback);

} else {

// if we can't copy-back anything, modify the user's dirty

// region to make sure they redraw the whole buffer

newDirtyRegion.set(bounds);

}



// keep track of the are of the buffer that is "clean"

// (ie: that will be redrawn)

mOldDirtyRegion = newDirtyRegion;



if (inOutDirtyBounds) {

*inOutDirtyBounds = newDirtyRegion.getBounds();

}



void* vaddr;

status_t res = backBuffer->lock(

GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_SW_WRITE_OFTEN,

newDirtyRegion.bounds(), &vaddr);



LOGW_IF(res, "failed locking buffer (handle = %p)",

backBuffer->handle);



mLockedBuffer = backBuffer;

outBuffer->width = backBuffer->width;

outBuffer->height = backBuffer->height;

outBuffer->stride = backBuffer->stride;

outBuffer->format = backBuffer->format;

outBuffer->bits = vaddr;

}

}

return err;

}



</pre>

<p><br>

好了,找到了 ANativeWindowBuffer* out; status_t err = dequeueBuffer(&out);差不多告一段落了,代码看着头晕,还是分析不大来额。菜鸟有待提高,打好基础,慢慢</p>

<p>地分析。</p>

<pre class="html" name="code">int SurfaceTextureClient::dequeueBuffer(android_native_buffer_t** buffer) {

LOGV("SurfaceTextureClient::dequeueBuffer");

Mutex::Autolock lock(mMutex);

int buf = -1;

status_t result = mSurfaceTexture->dequeueBuffer(&buf, mReqWidth, mReqHeight,

mReqFormat, mReqUsage);

if (result < 0) {

LOGV("dequeueBuffer: ISurfaceTexture::dequeueBuffer(%d, %d, %d, %d)"

"failed: %d", mReqWidth, mReqHeight, mReqFormat, mReqUsage,

result);

return result;

}

sp<GraphicBuffer>& gbuf(mSlots[buf]);

if (result & ISurfaceTexture::RELEASE_ALL_BUFFERS) {

freeAllBuffers();

}



if ((result & ISurfaceTexture::BUFFER_NEEDS_REALLOCATION) || gbuf == 0) {

result = mSurfaceTexture->requestBuffer(buf, &gbuf);

if (result != NO_ERROR) {

LOGE("dequeueBuffer: ISurfaceTexture::requestBuffer failed: %d",

result);

return result;

}

}

*buffer = gbuf.get();

return OK;

}



</pre>

<p><br>

由于对surface很多的概念还不是很清楚,分析代码也有点难度。只能拿出来,慢慢啃了。快要下班了,就这么着吧。android的图形显示系统好复杂好复杂啊,看得我头都大</p>

<p>了。不过结合板子和代码慢慢调试倒是可以理解得很好。不错。还有就是代码中的英文得好好啃,看来单词还得好好记啊。<br>

</p>

<pre></pre>

<pre></pre>
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: