【Android 音視頻開發(fā)打怪升級(jí):音視頻硬解碼篇】二、音視頻硬解碼流程:封裝基礎(chǔ)解碼框架

【聲 明】

首先,這一系列文章均基于自己的理解和實(shí)踐,可能有不對(duì)的地方,歡迎大家指正。
其次,這是一個(gè)入門系列,涉及的知識(shí)也僅限于夠用,深入的知識(shí)網(wǎng)上也有許許多多的博文供大家學(xué)習(xí)了。
最后,寫文章過程中,會(huì)借鑒參考其他人分享的文章,會(huì)在文章最后列出,感謝這些作者的分享。

碼字不易,轉(zhuǎn)載請(qǐng)注明出處!

教程代碼:【Github傳送門

目錄

一、Android音視頻硬解碼篇:
二、使用OpenGL渲染視頻畫面篇
三、Android FFmpeg音視頻解碼篇
  • 1,F(xiàn)Fmpeg so庫(kù)編譯
  • 2,Android 引入FFmpeg
  • 3,Android FFmpeg視頻解碼播放
  • 4,Android FFmpeg+OpenSL ES音頻解碼播放
  • 5,Android FFmpeg+OpenGL ES播放視頻
  • 6,Android FFmpeg簡(jiǎn)單合成MP4:視屏解封與重新封裝
  • 7,Android FFmpeg視頻編碼

本文你可以了解到

本文主要簡(jiǎn)介Android使用硬解碼API實(shí)現(xiàn)硬解碼的流程,包含MediaCodec輸入輸出緩沖、MediaCodec解碼流程、解碼代碼封裝和講解。

一、簡(jiǎn)介

MediaCodec 是Android 4.1(api 16)版本引入的編解碼接口,同時(shí)支持音視頻的編碼和解碼。

一定要好好理解接下來這兩幅圖,因?yàn)楹罄m(xù)的代碼就是基于這兩幅圖來編寫的。

數(shù)據(jù)流

首先,來看看MediaCodec的數(shù)據(jù)流,也是官方Api文檔中的,很多文章都會(huì)引用。

MediaCodec數(shù)據(jù)流

仔細(xì)看一下,MediaCodec將數(shù)據(jù)分為兩部分,分別為input(左邊)和output(右邊),即輸入和輸出兩個(gè)數(shù)據(jù)緩沖區(qū)。

input:是給客戶端輸入需要解碼的數(shù)據(jù)(解碼時(shí))或者需要編碼的數(shù)據(jù)(編碼時(shí))。

output:是輸出解碼好(解碼時(shí))或者編碼好(編碼時(shí))的數(shù)據(jù)給客戶端。

MediaCodec內(nèi)部使用異步的方式對(duì)input和output數(shù)據(jù)進(jìn)行處理。MediaCodec將處理好input的數(shù)據(jù),填充到output緩沖區(qū),交給客戶端渲染或處理

注:客戶端處理完數(shù)據(jù)后,必須手動(dòng)釋放output緩沖區(qū),否則將會(huì)導(dǎo)致MediaCodec輸出緩沖被占用,無法繼續(xù)解碼。

狀態(tài)

依然是一副來自官方的狀態(tài)圖

MediaCodec狀態(tài)圖

再仔細(xì)看看這幅圖,整體上分為三個(gè)大的狀態(tài):Sotpped、Executing、Released。

  • Stoped:包含了3個(gè)小狀態(tài):Error、Uninitialized、Configured。

首先,新建MediaCodec后,會(huì)進(jìn)入U(xiǎn)ninitialized狀態(tài);
其次,調(diào)用configure方法配置參數(shù)后,會(huì)進(jìn)入Configured;

  • Executing:同樣包含3個(gè)小狀態(tài):Flushed、Running、End of Stream。

再次,調(diào)用start方法后,MediaCodec進(jìn)入Flushed狀態(tài);
接著,調(diào)用dequeueInputBuffer方法后,進(jìn)入Running狀態(tài);
最后,當(dāng)解碼/編碼結(jié)束時(shí),進(jìn)入End of Stream(EOF)狀態(tài)。
這時(shí),一個(gè)視頻就處理完成了。

  • Released:最后,如果想結(jié)束整個(gè)數(shù)據(jù)處理過程,可以調(diào)用release方法,釋放所有的資源。

那么,F(xiàn)lushed是什么狀態(tài)呢?

從圖中我們可以看到,在Running或者End of Stream狀態(tài)時(shí),都可以調(diào)用flush方法,重新進(jìn)入Flushed狀態(tài)。

當(dāng)我們?cè)诮獯a過程中,進(jìn)入了End of Stream后,解碼器就不再接收輸入了,這時(shí)候,需要調(diào)用flush方法,重新進(jìn)入接收數(shù)據(jù)狀態(tài)。

或者,我們?cè)诓シ乓曨l過程中,想進(jìn)行跳播,這時(shí)候,我們需要Seek到指定的時(shí)間點(diǎn),這時(shí)候,也需要調(diào)用flush方法,清除緩沖,否則解碼時(shí)間戳?xí)靵y。

再次強(qiáng)調(diào)一下,一定要好好理解這兩幅圖,因?yàn)楹罄m(xù)的代碼就是基于這兩幅圖來編寫的。

二、解碼流程

MediaCodec有兩種工作模式,分別為異步模式和同步模式,這里我們使用同步模式,異步模式可以參考官網(wǎng)例子。

根據(jù)官方的數(shù)據(jù)流圖和狀態(tài)圖,畫出一個(gè)最基礎(chǔ)的解碼流程如下:

解碼流程圖

經(jīng)過初始化和配置以后,進(jìn)入循環(huán)解碼流程,不斷的輸入數(shù)據(jù),然后獲取解碼完數(shù)據(jù),最后渲染出來,直到所有數(shù)據(jù)解碼完成(End of Stream)。

三、開始解碼

根據(jù)上面的流程圖,可以發(fā)現(xiàn),無論音頻還是視頻,解碼流程基本是一致的,不同的地方只在于【配置】、【渲染】?jī)蓚€(gè)部分。

定義解碼器

因此,我們將整個(gè)解碼流程抽象為一個(gè)解碼基類:BaseDecoder,為了規(guī)范代碼和更好的拓展性,我們先定義一個(gè)解碼器:IDecoder,繼承Runnable。

interface IDecoder: Runnable {

    /**
     * 暫停解碼
     */
    fun pause()

    /**
     * 繼續(xù)解碼
     */
    fun goOn()

    /**
     * 停止解碼
     */
    fun stop()

    /**
     * 是否正在解碼
     */
    fun isDecoding(): Boolean

    /**
     * 是否正在快進(jìn)
     */
    fun isSeeking(): Boolean

    /**
     * 是否停止解碼
     */
    fun isStop(): Boolean

    /**
     * 設(shè)置狀態(tài)監(jiān)聽器
     */
    fun setStateListener(l: IDecoderStateListener?)

    /**
     * 獲取視頻寬
     */
    fun getWidth(): Int

    /**
     * 獲取視頻高
     */
    fun getHeight(): Int

    /**
     * 獲取視頻長(zhǎng)度
     */
    fun getDuration(): Long

    /**
     * 獲取視頻旋轉(zhuǎn)角度
     */
    fun getRotationAngle(): Int

    /**
     * 獲取音視頻對(duì)應(yīng)的格式參數(shù)
     */
    fun getMediaFormat(): MediaFormat?

    /**
     * 獲取音視頻對(duì)應(yīng)的媒體軌道
     */
    fun getTrack(): Int

    /**
     * 獲取解碼的文件路徑
     */
    fun getFilePath(): String
}

定義了解碼器的一些基礎(chǔ)操作,如暫停/繼續(xù)/停止解碼,獲取視頻的時(shí)長(zhǎng),視頻的寬高,解碼狀態(tài)等等

為什么繼承Runnable?

這里使用的是同步模式解碼,需要不斷循環(huán)壓入和拉取數(shù)據(jù),是一個(gè)耗時(shí)操作,因此,我們將解碼器定義為一個(gè)Runnable,最后放到線程池中執(zhí)行。

接著,繼承IDecoder,定義基礎(chǔ)解碼器BaseDecoder。

首先來看下基礎(chǔ)參數(shù):

abstract class BaseDecoder: IDecoder {
    //-------------線程相關(guān)------------------------
    /**
     * 解碼器是否在運(yùn)行
     */
    private var mIsRunning = true

    /**
     * 線程等待鎖
     */
    private val mLock = Object()

    /**
     * 是否可以進(jìn)入解碼
     */
    private var mReadyForDecode = false

    //---------------解碼相關(guān)-----------------------
    /**
     * 音視頻解碼器
     */
    protected var mCodec: MediaCodec? = null
    
    /**
     * 音視頻數(shù)據(jù)讀取器
     */
    protected var mExtractor: IExtractor? = null

    /**
     * 解碼輸入緩存區(qū)
     */
    protected var mInputBuffers: Array<ByteBuffer>? = null

    /**
     * 解碼輸出緩存區(qū)
     */
    protected var mOutputBuffers: Array<ByteBuffer>? = null

    /**
     * 解碼數(shù)據(jù)信息
     */
    private var mBufferInfo = MediaCodec.BufferInfo()
    
    private var mState = DecodeState.STOP

    private var mStateListener: IDecoderStateListener? = null

    /**
     * 流數(shù)據(jù)是否結(jié)束
     */
    private var mIsEOS = false

    protected var mVideoWidth = 0

    protected var mVideoHeight = 0
    
    //省略后面的方法
    ....
}
  • 首先,我們定義了線程相關(guān)的資源,用于判斷是否持續(xù)解碼的mIsRunning,掛起線程的mLock等。

  • 然后,就是解碼相關(guān)的資源了,比如MdeiaCodec本身,輸入輸出緩沖,解碼狀態(tài)等等。

  • 其中,有一個(gè)解碼狀態(tài)DecodeState和音視頻數(shù)據(jù)讀取器IExtractor。

定義解碼狀態(tài)

為了方便記錄解碼狀態(tài),這里使用一個(gè)枚舉類表示

enum class DecodeState {
    /**開始狀態(tài)*/
    START,
    /**解碼中*/
    DECODING,
    /**解碼暫停*/
    PAUSE,
    /**正在快進(jìn)*/
    SEEKING,
    /**解碼完成*/
    FINISH,
    /**解碼器釋放*/
    STOP
}
定義音視頻數(shù)據(jù)分離器

前面說過,MediaCodec需要我們不斷地喂數(shù)據(jù)給輸入緩沖,那么數(shù)據(jù)從哪里來呢?肯定是音視頻文件了,這里的IExtractor就是用來提取音視頻文件中數(shù)據(jù)流。

Android自帶有一個(gè)音視頻數(shù)據(jù)讀取器MediaExtractor,同樣為了方便維護(hù)和拓展性,我們依然先定一個(gè)讀取器IExtractor。

interface IExtractor {
    /**
     * 獲取音視頻格式參數(shù)
     */
    fun getFormat(): MediaFormat?

    /**
     * 讀取音視頻數(shù)據(jù)
     */
    fun readBuffer(byteBuffer: ByteBuffer): Int

    /**
     * 獲取當(dāng)前幀時(shí)間
     */
    fun getCurrentTimestamp(): Long

    /**
     * Seek到指定位置,并返回實(shí)際幀的時(shí)間戳
     */
    fun seek(pos: Long): Long

    fun setStartPos(pos: Long)

    /**
     * 停止讀取數(shù)據(jù)
     */
    fun stop()
}

最重要的一個(gè)方法就是readBuffer,用于讀取音視頻數(shù)據(jù)流

定義解碼流程

前面我們只貼出了解碼器的參數(shù)部分,接下來,貼出最重要的部分,也就是解碼流程部分。

abstract class BaseDecoder: IDecoder {
    //省略參數(shù)定義部分,見上
    .......
    
    final override fun run() {
        mState = DecodeState.START
        mStateListener?.decoderPrepare(this)

        //【解碼步驟:1. 初始化,并啟動(dòng)解碼器】
        if (!init()) return

        while (mIsRunning) {
            if (mState != DecodeState.START &&
                mState != DecodeState.DECODING &&
                mState != DecodeState.SEEKING) {
                waitDecode()
            }

            if (!mIsRunning ||
                mState == DecodeState.STOP) {
                mIsRunning = false
                break
            }

            //如果數(shù)據(jù)沒有解碼完畢,將數(shù)據(jù)推入解碼器解碼
            if (!mIsEOS) {
                //【解碼步驟:2. 將數(shù)據(jù)壓入解碼器輸入緩沖】
                mIsEOS = pushBufferToDecoder()
            }

            //【解碼步驟:3. 將解碼好的數(shù)據(jù)從緩沖區(qū)拉取出來】
            val index = pullBufferFromDecoder()
            if (index >= 0) {
                //【解碼步驟:4. 渲染】
                render(mOutputBuffers!![index], mBufferInfo)
                //【解碼步驟:5. 釋放輸出緩沖】
                mCodec!!.releaseOutputBuffer(index, true)
                if (mState == DecodeState.START) {
                    mState = DecodeState.PAUSE
                }
            }
            //【解碼步驟:6. 判斷解碼是否完成】
            if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
                mState = DecodeState.FINISH
                mStateListener?.decoderFinish(this)
            }
        }
        doneDecode()
        //【解碼步驟:7. 釋放解碼器】
        release()
    }


    /**
     * 解碼線程進(jìn)入等待
     */
    private fun waitDecode() {
        try {
            if (mState == DecodeState.PAUSE) {
                mStateListener?.decoderPause(this)
            }
            synchronized(mLock) {
                mLock.wait()
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
    
    /**
     * 通知解碼線程繼續(xù)運(yùn)行
     */
    protected fun notifyDecode() {
        synchronized(mLock) {
            mLock.notifyAll()
        }
        if (mState == DecodeState.DECODING) {
            mStateListener?.decoderRunning(this)
        }
    }
    
    /**
     * 渲染
     */
    abstract fun render(outputBuffers: ByteBuffer,
                        bufferInfo: MediaCodec.BufferInfo)

    /**
     * 結(jié)束解碼
     */
    abstract fun doneDecode()
}

在Runnable的run回調(diào)方法中,集成了整個(gè)解碼流程:

  • 【解碼步驟:1. 初始化,并啟動(dòng)解碼器】
abstract class BaseDecoder: IDecoder {
    //省略上面已有代碼
    ......
    
    private fun init(): Boolean {
        //1.檢查參數(shù)是否完整
        if (mFilePath.isEmpty() || File(mFilePath).exists()) {
            Log.w(TAG, "文件路徑為空")
            mStateListener?.decoderError(this, "文件路徑為空")
            return false
        }
        //調(diào)用虛函數(shù),檢查子類參數(shù)是否完整
        if (!check()) return false

        //2.初始化數(shù)據(jù)提取器
        mExtractor = initExtractor(mFilePath)
        if (mExtractor == null ||
            mExtractor!!.getFormat() == null) return false

        //3.初始化參數(shù)
        if (!initParams()) return false

        //4.初始化渲染器
        if (!initRender()) return false

        //5.初始化解碼器
        if (!initCodec()) return false
        return true
    }
    
    private fun initParams(): Boolean {
        try {
            val format = mExtractor!!.getFormat()!!
            mDuration = format.getLong(MediaFormat.KEY_DURATION) / 1000
            if (mEndPos == 0L) mEndPos = mDuration

            initSpecParams(mExtractor!!.getFormat()!!)
        } catch (e: Exception) {
            return false
        }
        return true
    }

    private fun initCodec(): Boolean {
        try {
            //1.根據(jù)音視頻編碼格式初始化解碼器
            val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
            mCodec = MediaCodec.createDecoderByType(type)
            //2.配置解碼器
            if (!configCodec(mCodec!!, mExtractor!!.getFormat()!!)) {
                waitDecode()
            }
            //3.啟動(dòng)解碼器
            mCodec!!.start()
            
            //4.獲取解碼器緩沖區(qū)
            mInputBuffers = mCodec?.inputBuffers
            mOutputBuffers = mCodec?.outputBuffers
        } catch (e: Exception) {
            return false
        }
        return true
    }
    
    /**
     * 檢查子類參數(shù)
     */
    abstract fun check(): Boolean

    /**
     * 初始化數(shù)據(jù)提取器
     */
    abstract fun initExtractor(path: String): IExtractor

    /**
     * 初始化子類自己特有的參數(shù)
     */
    abstract fun initSpecParams(format: MediaFormat)

    /**
     * 初始化渲染器
     */
    abstract fun initRender(): Boolean

    /**
     * 配置解碼器
     */
    abstract fun configCodec(codec: MediaCodec, format: MediaFormat): Boolean
}

初始化方法中,分為5個(gè)步驟,看起很復(fù)雜,實(shí)際很簡(jiǎn)單。

  1. 檢查參數(shù)是否完整:路徑是否有效等

  2. 初始化數(shù)據(jù)提取器:初始化Extractor

  3. 初始化參數(shù):提取一些必須的參數(shù),duration,width,height等

  4. 初始化渲染器:視頻不需要,音頻為AudioTracker

  5. 初始化解碼器:初始化MediaCodec

    在initCodec()中,

    val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
    mCodec = MediaCodec.createDecoderByType(type)
    

初始化MediaCodec的時(shí)候:

  1. 首先,通過Extractor獲取到音視頻數(shù)據(jù)的編碼信息MediaFormat;
  2. 然后,查詢MediaFormat中的編碼類型(如video/avc,即H264;audio/mp4a-latm,即AAC);
  3. 最后,調(diào)用createDecoderByType創(chuàng)建解碼器。

需要說明的是:由于音頻和視頻的初始化稍有不同,所以定義了幾個(gè)虛函數(shù),將不同的東西交給子類去實(shí)現(xiàn)。具體將在下一篇文章[音視頻播放:音視頻同步]說明。

  • 【解碼步驟:2. 將數(shù)據(jù)壓入解碼器輸入緩沖】

直接進(jìn)入pushBufferToDecoder方法中


abstract class BaseDecoder: IDecoder {
    //省略上面已有代碼
    ......
    
    private fun pushBufferToDecoder(): Boolean {
        var inputBufferIndex = mCodec!!.dequeueInputBuffer(2000)
        var isEndOfStream = false
    
        if (inputBufferIndex >= 0) {
            val inputBuffer = mInputBuffers!![inputBufferIndex]
            val sampleSize = mExtractor!!.readBuffer(inputBuffer)
            if (sampleSize < 0) {
                //如果數(shù)據(jù)已經(jīng)取完,壓入數(shù)據(jù)結(jié)束標(biāo)志:BUFFER_FLAG_END_OF_STREAM
                mCodec!!.queueInputBuffer(inputBufferIndex, 0, 0,
                    0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
                isEndOfStream = true
            } else {
                mCodec!!.queueInputBuffer(inputBufferIndex, 0,
                    sampleSize, mExtractor!!.getCurrentTimestamp(), 0)
            }
        }
        return isEndOfStream
    }
}

調(diào)用了以下方法:

  1. 查詢是否有可用的輸入緩沖,返回緩沖索引。其中參數(shù)2000為等待2000ms,如果填入-1則無限等待。
var inputBufferIndex = mCodec!!.dequeueInputBuffer(2000)
  1. 通過緩沖索引 inputBufferIndex 獲取可用的緩沖區(qū),并使用Extractor提取待解碼數(shù)據(jù),填充到緩沖區(qū)中。
val inputBuffer = mInputBuffers!![inputBufferIndex]
val sampleSize = mExtractor!!.readBuffer(inputBuffer)
  1. 調(diào)用queueInputBuffer將數(shù)據(jù)壓入解碼器。
mCodec!!.queueInputBuffer(inputBufferIndex, 0,
    sampleSize, mExtractor!!.getCurrentTimestamp(), 0)

注意:如果SampleSize返回-1,說明沒有更多的數(shù)據(jù)了。
這個(gè)時(shí)候,queueInputBuffer的最后一個(gè)參數(shù)要傳入結(jié)束標(biāo)記MediaCodec.BUFFER_FLAG_END_OF_STREAM。

  • 【解碼步驟:3. 將解碼好的數(shù)據(jù)從緩沖區(qū)拉取出來】

直接進(jìn)入pullBufferFromDecoder()

abstract class BaseDecoder: IDecoder {
    //省略上面已有代碼
    ......
    
    private fun pullBufferFromDecoder(): Int {
        // 查詢是否有解碼完成的數(shù)據(jù),index >=0 時(shí),表示數(shù)據(jù)有效,并且index為緩沖區(qū)索引
        var index = mCodec!!.dequeueOutputBuffer(mBufferInfo, 1000)
        when (index) {
            MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {}
            MediaCodec.INFO_TRY_AGAIN_LATER -> {}
            MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
                mOutputBuffers = mCodec!!.outputBuffers
            }
            else -> {
                return index
            }
        }
        return -1
    }
}

第一、調(diào)用dequeueOutputBuffer方法查詢是否有解碼完成的可用數(shù)據(jù),其中mBufferInfo用于獲取數(shù)據(jù)幀信息,第二參數(shù)是等待時(shí)間,這里等待1000ms,填入-1是無限等待。

var index = mCodec!!.dequeueOutputBuffer(mBufferInfo, 1000)

第二、判斷index類型:

MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:輸出格式改變了

MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:輸入緩沖改變了

MediaCodec.INFO_TRY_AGAIN_LATER:沒有可用數(shù)據(jù),等會(huì)再來

大于等于0:有可用數(shù)據(jù),index就是輸出緩沖索引

  • 【解碼步驟:4. 渲染】

這里調(diào)用了一個(gè)虛函數(shù)render,也就是將渲染交給子類

  • 【解碼步驟:5. 釋放輸出緩沖】

調(diào)用releaseOutputBuffer方法, 釋放輸出緩沖區(qū)。

注:第二個(gè)參數(shù),是個(gè)boolean,命名為render,這個(gè)參數(shù)在視頻解碼時(shí),用于決定是否要將這一幀數(shù)據(jù)顯示出來。

mCodec!!.releaseOutputBuffer(index, true)
  • 【解碼步驟:6. 判斷解碼是否完成】

還記得我們?cè)诎褦?shù)據(jù)壓入解碼器時(shí),當(dāng)sampleSize < 0 時(shí),壓入了一個(gè)結(jié)束標(biāo)記嗎?

當(dāng)接收到這個(gè)標(biāo)志后,解碼器就知道所有數(shù)據(jù)已經(jīng)接收完畢,在所有數(shù)據(jù)解碼完成以后,會(huì)在最后一幀數(shù)據(jù)加上結(jié)束標(biāo)記信息,即

if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
    mState = DecodeState.FINISH
    mStateListener?.decoderFinish(this)
}
  • 【解碼步驟:7. 釋放解碼器】

在while循環(huán)結(jié)束后,釋放掉所有的資源。至此,一次解碼結(jié)束。

abstract class BaseDecoder: IDecoder {
    //省略上面已有代碼
    ......
    
    private fun release() {
        try {
            mState = DecodeState.STOP
            mIsEOS = false
            mExtractor?.stop()
            mCodec?.stop()
            mCodec?.release()
            mStateListener?.decoderDestroy(this)
        } catch (e: Exception) {
        }
    }
}

最后,解碼器定義的其他方法(如pause、goOn、stop等)不再細(xì)說,可查看工程源碼。

結(jié)尾

本來打算把音頻和視頻播放部分也放到本篇來講,最后發(fā)現(xiàn)篇幅太長(zhǎng),不利于閱讀,看了會(huì)累。所以把真正實(shí)現(xiàn)播放部分和下一篇【音視頻播放:音視頻同步】做一個(gè)整合,內(nèi)容和長(zhǎng)度都會(huì)更合理。

so,下一篇見!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過簡(jiǎn)信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容