




版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認領(lǐng)
文檔簡介
【移動應用開發(fā)技術(shù)】BufferQueue的設(shè)計思想和內(nèi)部實現(xiàn)方法是什么
這篇文章主要介紹“BufferQueue的設(shè)計思想和內(nèi)部實現(xiàn)方法是什么”的相關(guān)知識,在下通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“BufferQueue的設(shè)計思想和內(nèi)部實現(xiàn)方法是什么”文章能幫助大家解決問題。1.背景對業(yè)務(wù)開發(fā)來說,無法接觸到BufferQueue,甚至不知道BufferQueue是什么東西。對系統(tǒng)來說,BufferQueue是很重要的傳遞數(shù)據(jù)的組件,Android顯示系統(tǒng)依賴于BufferQueue,只要顯示內(nèi)容到“屏幕”(此處指抽象的屏幕,有時候還可以包含編碼器),就一定需要用到BufferQueue,可以說在顯示/播放器相關(guān)的領(lǐng)域中,BufferQueue無處不在。即使直接調(diào)用Opengl
ES來繪制,底層依然需要BufferQueue才能顯示到屏幕上。弄明白BufferQueue,不僅可以增強對Android系統(tǒng)的了解,還可以弄明白/排查相關(guān)的問題,如為什么Mediacodec調(diào)用dequeueBuffer老是返回-1?為什么普通View的draw方法直接繪制內(nèi)容即可,SurfaceView在draw完畢后還需要unlockCanvasAndPost?注:本文分析的代碼來自于Android6.0.1。2.BufferQueue內(nèi)部運作方式BufferQueue是Android顯示系統(tǒng)的核心,它的設(shè)計哲學是生產(chǎn)者-消費者模型,只要往BufferQueue中填充數(shù)據(jù),則認為是生產(chǎn)者,只要從BufferQueue中獲取數(shù)據(jù),則認為是消費者。有時候同一個類,在不同的場景下既可能是生產(chǎn)者也有可能是消費者。如SurfaceFlinger,在合成并顯示UI內(nèi)容時,UI元素作為生產(chǎn)者生產(chǎn)內(nèi)容,SurfaceFlinger作為消費者消費這些內(nèi)容。而在截屏時,SurfaceFlinger又作為生產(chǎn)者將當前合成顯示的UI內(nèi)容填充到另一個BufferQueue,截屏應用此時作為消費者從BufferQueue中獲取數(shù)據(jù)并生產(chǎn)截圖。以下是常見的BufferQueue使用步驟:初始化一個BufferQueue圖形數(shù)據(jù)的生產(chǎn)者通過BufferQueue申請一塊GraphicBuffer,對應圖中的dequeueBuffer方法申請到GraphicBuffer后,獲取GraphicBuffer,通過函數(shù)requestBuffer獲取獲取到GraphicBuffer后,通過各種形式往GraphicBuffer中填充圖形數(shù)據(jù)后,然后將GraphicBuffer入隊到BufferQueue中,對應上圖中的queueBuffer方法在新的GraphicBuffer入隊BufferQueue時,BufferQueue會通過回調(diào)通知圖形數(shù)據(jù)的消費者,有新的圖形數(shù)據(jù)被生產(chǎn)出來了然后消費者從BufferQueue中出隊一個GraphicBuffer,對應圖中的acquireBuffer方法待消費者消費完圖形數(shù)據(jù)后,將空的GraphicBuffer還給BufferQueue以便重復利用,此時對應上圖中的releaseBuffer方法此時BufferQueue再通過回調(diào)通知圖形數(shù)據(jù)的生產(chǎn)者有空的GraphicBuffer了,圖形數(shù)據(jù)的生產(chǎn)者又可以從BufferQueue中獲取一個空的GraphicBuffer來填充數(shù)據(jù)一直循環(huán)2-8步驟,這樣就有條不紊的完成了圖形數(shù)據(jù)的生產(chǎn)-消費當然圖形數(shù)據(jù)的生產(chǎn)者可以不用等待BufferQueue的回調(diào)再生產(chǎn)數(shù)據(jù),而是一直生產(chǎn)數(shù)據(jù)然后入隊到BufferQueue,直到BufferQueue滿為止。圖形數(shù)據(jù)的消費者也可以不用等BufferQueue的回調(diào)通知,每次都從BufferQueue中嘗試獲取數(shù)據(jù),獲取失敗則嘗試,只是這樣效率比較低,需要不斷的輪訓BufferQueue(因為BufferQueue有同步阻塞和非同步阻塞兩種機種,在非同步阻塞機制下獲取數(shù)據(jù)失敗不會阻塞該線程直到有數(shù)據(jù)才喚醒該線程,而是直接返回-1)。同時使用BufferQueue的生產(chǎn)者和消費者往往處在不同的進程,BufferQueue內(nèi)部使用共享內(nèi)存和Binder在不同的進程傳遞數(shù)據(jù),減少數(shù)據(jù)拷貝提高效率。和BufferQueue有關(guān)的幾個類分別是:BufferBufferCore:BufferQueue的實際實現(xiàn)BufferSlot:用來存儲GraphicBufferBufferState:表示GraphicBuffer的狀態(tài)IGraphicBufferProducer:BufferQueue的生產(chǎn)者接口,實現(xiàn)類是BufferQueueProducerIGraphicBufferConsumer:BufferQueue的消費者接口,實現(xiàn)類是BufferQueueConsumerGraphicBuffer:表示一個Buffer,可以填充圖像數(shù)據(jù)ANativeWindow_Buffer:GraphicBuffer的父類ConsumerBase:實現(xiàn)了ConsumerListener接口,在數(shù)據(jù)入隊列時會被調(diào)用到,用來通知消費者BufferQueue中用BufferSlot來存儲GraphicBuffer,使用數(shù)組來存儲一系列BufferSlot,數(shù)組默認大小為64。GraphicBuffer用BufferState來表示其狀態(tài),有以下狀態(tài):FREE:表示該Buffer沒有被生產(chǎn)者-消費者所使用,該Buffer的所有權(quán)屬于BufferQueueDEQUEUED:表示該Buffer被生產(chǎn)者獲取了,該Buffer的所有權(quán)屬于生產(chǎn)者QUEUED:表示該Buffer被生產(chǎn)者填充了數(shù)據(jù),并且入隊到BufferQueue了,該Buffer的所有權(quán)屬于BufferQueueACQUIRED:表示該Buffer被消費者獲取了,該Buffer的所有權(quán)屬于消費者為什么需要這些狀態(tài)呢?
假設(shè)不需要這些狀態(tài),實現(xiàn)一個簡單的BufferQueue,假設(shè)是如下實現(xiàn):BufferQueue{
vector<GraphicBuffer>slots;
voidpush(GraphicBufferslot){
slots.push(slot);
}
GraphicBufferpull(){
returnslots.pull();
}}生產(chǎn)者生產(chǎn)完數(shù)據(jù)后,通過調(diào)用BufferQueue的push函數(shù)將數(shù)據(jù)插入到vector中。消費者調(diào)用BufferQueue的pull函數(shù)出隊一個Buffer數(shù)據(jù)。上述實現(xiàn)的問題在于,生產(chǎn)者每次都需要自行創(chuàng)建GraphicBuffer,而消費者每次消費完數(shù)據(jù)后的GraphicBuffer就被釋放了,GraphicBuffer沒有得到循環(huán)利用。而在Android中,由于BufferQueue的生產(chǎn)者-消費者往往處于不同的進程,GraphicBuffer內(nèi)部是需要通過共享內(nèi)存來連接生成者-消費者進程的,每次創(chuàng)建GraphicBuffer,即意味著需要創(chuàng)建共享內(nèi)存,效率較低。而BufferQueue中用BufferState來表示GraphicBuffer的狀態(tài)則解決了這個問題。每個GraphicBuffer都有當前的狀態(tài),通過維護GraphicBuffer的狀態(tài),完成GraphicBuffer的復用。由于BufferQueue內(nèi)部實現(xiàn)是BufferQueueCore,下文均用BufferQueueCore代替BufferQueue。先介紹下BufferQueueCore內(nèi)部相應的數(shù)據(jù)結(jié)構(gòu),再介紹BufferQueue的狀態(tài)扭轉(zhuǎn)過程和生產(chǎn)-消費過程。以下是Buffer的入隊/出隊操作和BufferState的狀態(tài)扭轉(zhuǎn)的過程,這里只介紹非同步阻塞模式。2.1BufferQueueCore內(nèi)部數(shù)據(jù)結(jié)構(gòu)核心數(shù)據(jù)結(jié)構(gòu)如下:BufferQueueDefs::SlotsType
mSlots:用數(shù)組存放的Slot,數(shù)組默認大小為BufferQueueDefs::NUM_BUFFER_SLOTS,具體是64,代表所有的Slotstd::set<int>
mFreeSlots:當前所有的狀態(tài)為FREE的Slot,這些Slot沒有關(guān)聯(lián)上具體的GraphicBuffer,后續(xù)用的時候還需要關(guān)聯(lián)上GraphicBufferstd::list<int>
mFreeBuffers:當前所有的狀態(tài)為FREE的Slot,這些Slot已經(jīng)關(guān)聯(lián)上具體的GraphicBuffer,可以直接使用Fifo
mQueue:一個先進先出隊列,保存了生產(chǎn)者生產(chǎn)的數(shù)據(jù)在BufferQueueCore初始化時,由于此時隊列中沒有入隊任何數(shù)據(jù),按照上面的介紹,此時mFreeSlots應該包含所有的Slot,元素大小和mSlots一致,初始化代碼如下:for
(int
slot
=
0;
slot
<
BufferQueueDefs::NUM_BUFFER_SLOTS;
++slot)
{
mFreeSlots.insert(slot);
}當生產(chǎn)者可以生產(chǎn)圖形數(shù)據(jù)時,首先向BufferQueue中申請一塊GraphicBuffer。調(diào)用函數(shù)BufferQueueProducer.dequeueBuffer,如果當前BufferQueue中有可用的GraphicBuffer,則返回其對用的索引;如果不存在,則返回-1,代碼在BufferQueueProducer,流程如下:status_t
BufferQueueProducer::dequeueBuffer(int
*outSlot,
sp<android::Fence>
*outFence,
bool
async,
uint32_t
width,
uint32_t
height,
PixelFormat
format,
uint32_t
usage)
{
//1.
尋找可用的Slot,可用指Buffer狀態(tài)為FREE
status_t
status
=
waitForFreeSlotThenRelock("dequeueBuffer",
async,
&found,
&returnFlags);
if
(status
!=
NO_ERROR)
{
return
status;
}
//2.找到可用的Slot,將Buffer狀態(tài)設(shè)置為DEQUEUED,由于步驟1找到的Slot狀態(tài)為FREE,因此這一步完成了FREE到DEQUEUED的狀態(tài)切換
*outSlot
=
found;
ATRACE_BUFFER_INDEX(found);
attachedByConsumer
=
mSlots[found].mAttachedByConsumer;
mSlots[found].mBufferState
=
BufferSlot::DEQUEUED;
//3.
找到的Slot如果需要申請GraphicBuffer,則申請GraphicBuffer,這里采用了懶加載機制,如果內(nèi)存沒有申請,申請內(nèi)存放在生產(chǎn)者來處理
if
(returnFlags
&
BUFFER_NEEDS_REALLOCATION)
{
status_t
error;
sp<GraphicBuffer>
graphicBuffer(mCore->mAllocator->createGraphicBuffer(width,
height,
format,
usage,
&error));
graphicBuffer->setGenerationNumber(mCore->mGenerationNumber);
mSlots[*outSlot].mGraphicBuffer
=
graphicBuffer;
}}關(guān)鍵在于尋找可用Slot,waitForFreeSlotThenRelock的流程如下:status_t
BufferQueueProducer::waitForFreeSlotThenRelock(const
char*
caller,
bool
async,
int*
found,
status_t*
returnFlags)
const
{
//1.
mQueue
是否太多
bool
tooManyBuffers
=
mCore->mQueue.size()>
static_cast<size_t>(maxBufferCount);
if
(tooManyBuffers)
{
}
else
{
//
2.
先查找mFreeBuffers中是否有可用的,由2.1介紹可知,mFreeBuffers中的元素關(guān)聯(lián)了GraphicBuffer,直接可用
if
(!mCore->mFreeBuffers.empty())
{
auto
slot
=
mCore->mFreeBuffers.begin();
*found
=
*slot;
mCore->mFreeBuffers.erase(slot);
}
else
if
(mCore->mAllowAllocation
&&
!mCore->mFreeSlots.empty())
{
//
3.
再查找mFreeSlots中是否有可用的,由2.1可知,初始化時會填充滿這個列表,因此第一次調(diào)用一定不會為空。同時用這個列表中的元素需要關(guān)聯(lián)上GraphicBuffer才可以直接使用,關(guān)聯(lián)的過程由外層函數(shù)來實現(xiàn)
auto
slot
=
mCore->mFreeSlots.begin();
//
Only
return
free
slots
up
to
the
max
buffer
count
if
(*slot
<
maxBufferCount)
{
*found
=
*slot;
mCore->mFreeSlots.erase(slot);
}
}
}
tryAgain
=
(*found
==
BufferQueueCore::INVALID_BUFFER_SLOT)
||
tooManyBuffers;
//4.
如果找不到可用的Slot或者Buffer太多(同步阻塞模式下),則可能需要等
if
(tryAgain)
{
if
(mCore->mDequeueBufferCannotBlock
&&
(acquiredCount
<=
mCore->mMaxAcquiredBufferCount))
{
return
WOULD_BLOCK;
}
mCore->mDequeueCondition.wait(mCore->mMutex);
}}waitForFreeSlotThenRelock函數(shù)會嘗試尋找一個可用的Slot,可用的Slot狀態(tài)一定是FREE(因為是從兩個FREE狀態(tài)的列表中獲取的),然后dequeueBuffer將狀態(tài)改變?yōu)镈EQUEUED,即完成了狀態(tài)的扭轉(zhuǎn)。waitForFreeSlotThenRelock返回可用的Slot分為兩種:從mFreeBuffers中獲取到的,mFreeBuffers中的元素關(guān)聯(lián)了GraphicBuffer,直接可用從mFreeSlots中獲取到的,沒有關(guān)聯(lián)上GraphicBuffer,因此需要申請GraphicBuffer并和Slot關(guān)聯(lián)上,通過createGraphicBuffer申請一個GraphicBuffer,然后賦值給Slot的mGraphicBuffer完成關(guān)聯(lián)小結(jié)dequeueBuffer:嘗試找到一個Slot,并完成Slot與GraphicBuffer的關(guān)聯(lián)(如果需要),然后將Slot的狀態(tài)由FREE扭轉(zhuǎn)成DEQUEUED,返回Slot在BufferQueueCore中mSlots對應的索引。dequeueBuffer函數(shù)獲取到了可用Slot的索引后,通過requestBuffer獲取到對應的GraphicBuffer。流程如下:status_t
BufferQueueProducer::requestBuffer(int
slot,
sp<GraphicBuffer>*
buf)
{
//
1.
判斷slot參數(shù)是否合法
if
(slot
<
0
||
slot
>=
BufferQueueDefs::NUM_BUFFER_SLOTS)
{
BQ_LOGE("requestBuffer:
slot
index
%d
out
of
range
[0,
%d)",
slot,
BufferQueueDefs::NUM_BUFFER_SLOTS);
return
BAD_VALUE;
}
else
if
(mSlots[slot].mBufferState
!=
BufferSlot::DEQUEUED)
{
BQ_LOGE("requestBuffer:
slot
%d
is
not
owned
by
the
producer
"
"(state
=
%d)",
slot,
mSlots[slot].mBufferState);
return
BAD_VALUE;
}
//2.
將mRequestBufferCalled置為true
mSlots[slot].mRequestBufferCalled
=
true;
*buf
=
mSlots[slot].mGraphicBuffer;
return
NO_ERROR;}這一步不是必須的,業(yè)務(wù)層可以直接通過Slot的索引獲取到對應的GraphicBuffer。上文dequeueBuffer獲取到一個Slot后,就可以在Slot對應的GraphicBuffer上完成圖像數(shù)據(jù)的生產(chǎn)了,可以是View的主線程Draw過程,也可以是SurfaceView的子線程繪制過程,甚至可以是MediaCodec的解碼過程。填充完圖像數(shù)據(jù)后,需要將Slot入隊BufferQueueCore(數(shù)據(jù)寫完了,可以傳給生產(chǎn)者-消費者隊列,讓消費者來消費了),入隊調(diào)用queueBuffer函數(shù)。queueBuffer的流程如下:status_t
BufferQueueProducer::queueBuffer(int
slot,
const
QueueBufferInput
&input,
QueueBufferOutput
*output)
{
//
1.
先判斷傳入的Slot是否合法
if
(slot
<
0
||
slot
>=
maxBufferCount)
{
BQ_LOGE("queueBuffer:
slot
index
%d
out
of
range
[0,
%d)",
slot,
maxBufferCount);
return
BAD_VALUE;
}
//2.
將Buffer狀態(tài)扭轉(zhuǎn)成QUEUED,此步完成了Buffer的狀態(tài)由DEQUEUED到QUEUED的過程
mSlots[slot].mFence
=
fence;
mSlots[slot].mBufferState
=
BufferSlot::QUEUED;
++mCore->mFrameCounter;
mSlots[slot].mFrameNumber
=
mCore->mFrameCounter;
//3.
入隊mQueue
if
(mCore->mQueue.empty())
{
mCore->mQueue.push_back(item);
frameAvailableListener
=
mCore->mConsumerListener;
}
//
4.
回調(diào)frameAvailableListener,告知消費者有數(shù)據(jù)入隊了
if
(frameAvailableListener
!=
NULL)
{
frameAvailableListener->onFrameAvailable(item);
}
else
if
(frameReplacedListener
!=
NULL)
{
frameReplacedListener->onFrameReplaced(item);
}}從上面的注釋可以看到,queueBuffer的主要步驟如下:將Buffer狀態(tài)扭轉(zhuǎn)成QUEUED,此步完成了Buffer的狀態(tài)由DEQUEUED到QUEUED的過程將Buffer入隊到BufferQueueCore的mQueue隊列中回調(diào)frameAvailableListener,告知消費者有數(shù)據(jù)入隊,可以來消費數(shù)據(jù)了,frameAvailableListener是消費者注冊的回調(diào)小結(jié)queueBuffer:將Slot的狀態(tài)扭轉(zhuǎn)成QUEUED,并添加到mQueue中,最后通知消費者有數(shù)據(jù)入隊。在消費者接收到onFrameAvailable回調(diào)時或者消費者主動想要消費數(shù)據(jù),調(diào)用acquireBuffer嘗試向BufferQueueCore獲取一個數(shù)據(jù)以供消費。消費者的代碼在BufferQueueConsumer中,acquireBuffer流程如下:status_t
BufferQueueConsumer::acquireBuffer(BufferItem*
outBuffer,
nsecs_t
expectedPresent,
uint64_t
maxFrameNumber)
{
//1.
如果隊列為空,則直接返回
if
(mCore->mQueue.empty())
{
return
NO_BUFFER_AVAILABLE;
}
//2.
取出mQueue隊列的第一個元素,并從隊列中移除
BufferQueueCore::Fifo::iterator
front(mCore->mQueue.begin());
int
slot
=
front->mSlot;
*outBuffer
=
*front;
mCore->mQueue.erase(front);
//3.
處理expectedPresent的情況,這種情況可能會連續(xù)丟幾個Slot的“顯示”時間小于expectedPresent的情況,這種情況下這些Slot已經(jīng)是“過時”的,直接走下文的releaseBuffer消費流程,代碼比較長,忽略了
//4.
更新Slot的狀態(tài)為ACQUIRED
if
(mCore->stillTracking(front))
{
mSlots[slot].mAcquireCalled
=
true;
mSlots[slot].mNeedsCleanupOnRelease
=
false;
mSlots[slot].mBufferState
=
BufferSlot::ACQUIRED;
mSlots[slot].mFence
=
Fence::NO_FENCE;
}
//5.
如果步驟3有直接releaseBuffer的過程,則回調(diào)生產(chǎn)者,有數(shù)據(jù)被消費了
if
(listener
!=
NULL)
{
for
(int
i
=
0;
i
<
numDroppedBuffers;
++i)
{
listener->onBufferReleased();
}
}}從上面的注釋可以看到,acquireBuffer的主要步驟如下:從mQueue隊列中取出并移除一個元素改變Slot對應的狀態(tài)為ACQUIRED如果有丟幀邏輯,回調(diào)告知生產(chǎn)者有數(shù)據(jù)被消費,生產(chǎn)者可以準備生產(chǎn)數(shù)據(jù)了小結(jié)acquireBuffer:將Slot的狀態(tài)扭轉(zhuǎn)成ACQUIRED,并從mQueue中移除,最后通知生產(chǎn)者有數(shù)據(jù)出隊。消費者獲取到Slot后開始消費數(shù)據(jù)(典型的消費如SurfaceFlinger的UI合成),消費完畢后,需要告知BufferQueueCore這個Slot被消費者消費完畢了,可以給生產(chǎn)者重新生產(chǎn)數(shù)據(jù),releaseBuffer流程如下:status_t
BufferQueueConsumer::releaseBuffer(int
slot,
uint64_t
frameNumber,
const
sp<Fence>&
releaseFence,
EGLDisplay
eglDisplay,EGLSyncKHR
eglFence)
{
//1.
檢查Slot是否合法
if
(slot
<
0
||
slot
>=
BufferQueueDefs::NUM_BUFFER_SLOTS
||
return
BAD_VALUE;
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責。
- 6. 下載文件中如有侵權(quán)或不適當內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 例會管理制度
- 大氣匯報類型模板
- 學校膳食管理委員會議探討幼兒膳食營養(yǎng)管理飲食健康課件模板
- 上海電子信息職業(yè)技術(shù)學院《大學英語B(二)》2023-2024學年第二學期期末試卷
- 長沙環(huán)境保護職業(yè)技術(shù)學院《語言學導論》2023-2024學年第一學期期末試卷
- 溫州大學《首飾材料研究》2023-2024學年第二學期期末試卷
- 浙江省麗水市級名校2025年初三中考適應性測試(一)化學試題含解析
- 2025年江蘇省普通高中第一次聯(lián)考高三物理試題含解析
- 2025年安徽省蕪湖市重點中學高三下學期4月考英語試題理試題含解析
- 2025年甘肅省天水市秦安縣第二中學高三5月高三調(diào)研測試歷史試題含解析
- 2025年河南經(jīng)貿(mào)職業(yè)學院單招職業(yè)技能測試題庫學生專用
- 2024年襄陽汽車職業(yè)技術(shù)學院高職單招職業(yè)技能測驗歷年參考題庫(頻考版)含答案解析
- 國家開放大學《課程與教學論》形考任務(wù)1-4參考答案
- 2024年江蘇省腫瘤醫(yī)院高層次衛(wèi)技人才招聘筆試歷年參考題庫頻考點附帶答案
- 2024年護士資格證考試三基知識考試題庫及答案(共650題)
- 2024年世界職業(yè)院校技能大賽中職組“養(yǎng)老照護組”賽項參考試題庫(含答案)
- 《SLAM介紹以及淺析》課件
- 藥物過量病人的護理
- 第十七屆山東省職業(yè)院校技能大賽機器人系統(tǒng)集成應用技術(shù)樣題1學生賽
- 物理治療電療法
- 2024年上海市中考語文真題卷及答案解析
評論
0/150
提交評論