MediaCodec進(jìn)行AAC編解碼(文件格式轉(zhuǎn)換)

AAC轧飞,全稱Advanced Audio Coding,是一種專為聲音數(shù)據(jù)設(shè)計(jì)的文件壓縮格式毅厚。與MP3不同塞颁,它采用了全新的算法進(jìn)行編碼,更加高效吸耿,具有更高的“性價(jià)比”祠锣。利用AAC格式,可使人感覺(jué)聲音質(zhì)量沒(méi)有明顯降低的前提下咽安,更加小巧伴网。至于AAC的其他特點(diǎn)網(wǎng)上資料就很多,就不多做介紹了妆棒。
在介紹AAC編解碼之前澡腾,首先要先學(xué)習(xí)幾個(gè)新知識(shí)MediaExtractor和ADTS格式
倉(cāng)庫(kù)源碼FFmpegSample,對(duì)應(yīng)版本代碼v1.6

MediaExtractor

前面在介紹視頻編碼的時(shí)候使用到了MediaCodec糕珊,其功能主要是進(jìn)行音視頻的編解碼动分。下面要介紹另外一個(gè)類MediaExtractor:負(fù)責(zé)將指定類型的媒體文件從文件中找到軌道,可以用來(lái)分離容器中的視頻track和音頻track红选。將得到的原始數(shù)據(jù)解析成解碼器需要的數(shù)據(jù)澜公。

1.png

對(duì)象創(chuàng)建和設(shè)置源

對(duì)象的創(chuàng)建直接new出來(lái)即可。然后最要要的是設(shè)置數(shù)據(jù)源喇肋。調(diào)用setDataSource即可坟乾,

Sets the data source (file-path or http URL) to use.

這個(gè)方法的注釋寫(xiě)的比較清楚,可以設(shè)置本地文件的位置或者一個(gè)http URL蝶防。

分離軌道信息

  • getTrackCount()獲取軌道數(shù)量
  • MediaFormat format = mediaExtractor.getTrackFormat(i);獲取對(duì)應(yīng)軌道的信息甚侣。通過(guò)MediaFormat我們就可以知道每個(gè)track的詳細(xì)信息,如音頻/視頻间学、格式等等殷费。
  • selectTrack選擇軌道

讀取數(shù)據(jù)

制定軌道后就可以開(kāi)始讀取數(shù)據(jù)了印荔。

  • readSampleData 將數(shù)據(jù)讀取到ByteBuffer 中。返回-1時(shí)代表沒(méi)有更多數(shù)據(jù)了
  • advance 跳到下一個(gè)數(shù)據(jù)包宗兼,如果沒(méi)有下一個(gè)就返回false

釋放資源

使用完后調(diào)用release進(jìn)行資源釋放

ADTS

ADTS是AAC音頻文件常見(jiàn)的傳輸格式。當(dāng)你編碼AAC裸流的時(shí)候氮采,會(huì)遇到寫(xiě)出來(lái)的AAC文件并不能在PC和手機(jī)上播放殷绍,很大的可能就是AAC文件的每一幀里缺少了ADTS頭信息文件的包裝拼接。只需要加入頭文件ADTS即可鹊漠。一個(gè)AAC原始數(shù)據(jù)塊長(zhǎng)度是可變的主到,對(duì)原始幀加上ADTS頭進(jìn)行ADTS的封裝,就形成了ADTS幀躯概。

2.png
長(zhǎng)度 說(shuō)明
Syncword 12 總是0xFFF, 代表一個(gè)ADTS幀的開(kāi)始, 用于同步
MPEG version 1 0 for MPEG-4 登钥、 1 for MPEG-2
Layer 2 always 0
Protection Absent 1 et to 1 if there is no CRC and 0 if there is CRC
Profile 2 表示使用哪個(gè)級(jí)別的AAC( Audio Object Type的值減1)
MPEG-4 Sampling Frequency Index 4 采樣率的下標(biāo)
Originality 1 set to 0 when encoding, ignore when decoding
Home 1 set to 0 when encoding, ignore when decoding
Copyrighted Stream 1 set to 0 when encoding, ignore when decoding
Copyrighted Start 1 set to 0 when encoding, ignore when decoding
Frame Length 13 一個(gè)ADTS幀的長(zhǎng)度包括ADTS頭和AAC原始流。aac_frame_length = (protection_absent == 1 ? 7 : 9) + size(AACFrame)
Buffer Fullness 11 0x7FF 說(shuō)明是碼率可變的碼流
Number of AAC Frames 2 表示ADTS幀中有number_of_raw_data_blocks_in_frame number_of_raw_data_blocks_in_frame == 0 表示說(shuō)ADTS幀中有一個(gè)AAC數(shù)據(jù)塊娶靡。 (一個(gè)AAC原始幀包含一段時(shí)間內(nèi)1024個(gè)采樣及相關(guān)數(shù)據(jù))

文件格式轉(zhuǎn)換

先來(lái)張流程圖


5.png

第一步 初始化解碼器

讀取視頻文件初始化解碼器

    /**
     * 初始化解碼器
     */
    private void initMediaDecode() {
        try {
            mediaExtractor = new MediaExtractor();//此類可分離視頻文件的音軌和視頻軌道
            mediaExtractor.setDataSource(srcPath);//媒體文件的位置
            for (int i = 0; i < mediaExtractor.getTrackCount(); i++) {//遍歷媒體軌道 此處我們傳入的是音頻文件牧牢,所以也就只有一條軌道
                MediaFormat format = mediaExtractor.getTrackFormat(i);
                String mime = format.getString(MediaFormat.KEY_MIME);
                if (mime.startsWith("audio")) {//獲取音頻軌道
                    mediaExtractor.selectTrack(i);//選擇此音頻軌道
                    LogUtils.d("mime:" + mime);
                    key_bit_rate = format.getInteger(MediaFormat.KEY_BIT_RATE);
                    key_channel_count = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
                    key_sample_rate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
                    sampleRateType = ADTSUtils.getSampleRateType(key_sample_rate);
                    mediaDecode = MediaCodec.createDecoderByType(mime);//創(chuàng)建Decode解碼器
                    mediaDecode.configure(format, null, null, 0);
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (mediaDecode == null) {
            LogUtils.e("create mediaDecode failed");
            return;
        }
        mediaDecode.start();//啟動(dòng)MediaCodec ,等待傳入數(shù)據(jù)
        decodeInputBuffers = mediaDecode.getInputBuffers();//MediaCodec在此ByteBuffer[]中獲取輸入數(shù)據(jù)
        decodeOutputBuffers = mediaDecode.getOutputBuffers();//MediaCodec將解碼后的數(shù)據(jù)放到此ByteBuffer[]中 我們可以直接在這里面得到PCM數(shù)據(jù)
        decodeBufferInfo = new MediaCodec.BufferInfo();//用于描述解碼得到的byte[]數(shù)據(jù)的相關(guān)信息
        LogUtils.d("buffers:" + decodeInputBuffers.length);
    }

前面已經(jīng)介紹了MediaExtractor的用法姿锭,這里就是解析得到音頻軌道塔鳍,然后創(chuàng)建一個(gè)對(duì)應(yīng)解碼格式MediaCodec用于解碼。MediaCodec的用法在前面視頻編碼文章中有介紹呻此,這里就不累述轮纫。

第二步 初始化編碼器

    /**
     * 初始化AAC編碼器
     */
    private void initAACMediaEncode() {
        try {
            LogUtils.d(key_bit_rate + " " + key_channel_count + " " + key_sample_rate + " " + sampleRateType);
            MediaFormat encodeFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC,
                    key_sample_rate, key_channel_count);//參數(shù)對(duì)應(yīng)-> mime type、采樣率焚鲜、聲道數(shù)
            encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, key_bit_rate);//比特率
            encodeFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
            encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 100 * 1024);
            mediaEncode = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
            mediaEncode.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (mediaEncode == null) {
            LogUtils.e("create mediaEncode failed");
            return;
        }
        mediaEncode.start();
        encodeInputBuffers = mediaEncode.getInputBuffers();
        encodeOutputBuffers = mediaEncode.getOutputBuffers();
        encodeBufferInfo = new MediaCodec.BufferInfo();
    }

這里也是創(chuàng)建一個(gè)MediaCodec用于編碼掌唾,同時(shí)設(shè)置相關(guān)參數(shù),我們保持和源文件的參數(shù)一致忿磅,也就是MediaExtractor解析得到的碼率糯彬、聲道數(shù)、采樣率等等葱她。

第三步 分別開(kāi)啟線程編解碼

    /**
     * 開(kāi)始轉(zhuǎn)碼
     * 音頻數(shù)據(jù){@link #srcPath}先解碼成PCM  PCM數(shù)據(jù)在編碼成MediaFormat.MIMETYPE_AUDIO_AAC音頻格式
     * mp3->PCM->aac
     */
    public void startAsync() {
        LogUtils.w("start");

        new Thread(new DecodeRunnable()).start();
        new Thread(new EncodeRunnable()).start();

    }

先看到解碼邏輯

    /**
     * 解碼{@link #srcPath}音頻文件 得到PCM數(shù)據(jù)塊
     *
     * @return 是否解碼完所有數(shù)據(jù)
     */
    private void srcAudioFormatToPCM() {
        for (int i = 0; i < decodeInputBuffers.length - 1; i++) {
            int inputIndex = mediaDecode.dequeueInputBuffer(-1);//獲取可用的inputBuffer -1代表一直等待情连,0表示不等待 建議-1,避免丟幀
            if (inputIndex < 0) {
                codeOver = true;
                return;
            }

            ByteBuffer inputBuffer = decodeInputBuffers[inputIndex];//拿到inputBuffer
            inputBuffer.clear();//清空之前傳入inputBuffer內(nèi)的數(shù)據(jù)
            int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);//MediaExtractor讀取數(shù)據(jù)到inputBuffer中
            if (sampleSize < 0) {//小于0 代表所有數(shù)據(jù)已讀取完成
                codeOver = true;
            } else {
                mediaDecode.queueInputBuffer(inputIndex, 0, sampleSize, 0, 0);//通知MediaDecode解碼剛剛傳入的數(shù)據(jù)
                mediaExtractor.advance();//MediaExtractor移動(dòng)到下一取樣處
                decodeSize += sampleSize;
                LogUtils.d("read:" + sampleSize);
                if (onProgressListener != null) {
                    onProgressListener.progress(decodeSize, fileTotalSize);
                }
            }
        }

        //獲取解碼得到的byte[]數(shù)據(jù) 參數(shù)BufferInfo上面已介紹 10000同樣為等待時(shí)間 同上-1代表一直等待,0代表不等待览效。此處單位為微秒
        //此處建議不要填-1 有些時(shí)候并沒(méi)有數(shù)據(jù)輸出却舀,那么他就會(huì)一直卡在這 等待
        int outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, 10000);

        ByteBuffer outputBuffer;
        byte[] chunkPCM;
        while (outputIndex >= 0) {//每次解碼完成的數(shù)據(jù)不一定能一次吐出 所以用while循環(huán),保證解碼器吐出所有數(shù)據(jù)
            outputBuffer = decodeOutputBuffers[outputIndex];//拿到用于存放PCM數(shù)據(jù)的Buffer
            chunkPCM = new byte[decodeBufferInfo.size];//BufferInfo內(nèi)定義了此數(shù)據(jù)塊的大小
            outputBuffer.get(chunkPCM);//將Buffer內(nèi)的數(shù)據(jù)取出到字節(jié)數(shù)組中
            outputBuffer.clear();//數(shù)據(jù)取出后一定記得清空此Buffer MediaCodec是循環(huán)使用這些Buffer的锤灿,不清空下次會(huì)得到同樣的數(shù)據(jù)
            putPCMData(chunkPCM);//自己定義的方法挽拔,供編碼器所在的線程獲取數(shù)據(jù),下面會(huì)貼出代碼
            mediaDecode.releaseOutputBuffer(outputIndex, false);//此操作一定要做,不然MediaCodec用完所有的Buffer后 將不能向外輸出數(shù)據(jù)
            outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, 10000);//再次獲取數(shù)據(jù)但校,如果沒(méi)有數(shù)據(jù)輸出則outputIndex=-1 循環(huán)結(jié)束
        }

    }

其實(shí)就是基本的MediaCodec操作螃诅。使用MediaExtractor.readSampleData讀取文件音頻數(shù)據(jù),然后交給MediaCodec進(jìn)行解碼,最后將得到的PCM數(shù)據(jù)加入隊(duì)列中

這里隊(duì)列我們使用ArrayBlockingQueue术裸,在多線程操作時(shí)候倘是,這個(gè)容器還是比較好用的

接下來(lái)看到編碼流程

    /**
     * 編碼線程
     */
    private class EncodeRunnable implements Runnable {

        @Override
        public void run() {
            long t = System.currentTimeMillis();
            while (!codeOver || !queue.isEmpty()) {
                dstAudioFormatFromPCM();
            }
            if (onCompleteListener != null) {
                onCompleteListener.completed();
            }
            LogUtils.w("size:" + fileTotalSize + " decodeSize:" + decodeSize + "time:" + (System.currentTimeMillis() - t));
        }
    }

這里判斷如果解碼未結(jié)束或者隊(duì)列不為空就進(jìn)入編碼流程

    /**
     * 編碼PCM數(shù)據(jù) 得到MediaFormat.MIMETYPE_AUDIO_AAC格式的音頻文件,并保存到{@link #dstPath}
     */
    private void dstAudioFormatFromPCM() {

        int inputIndex;
        ByteBuffer inputBuffer;
        int outputIndex;
        ByteBuffer outputBuffer;
        byte[] chunkAudio;
        int outBitSize;
        int outPacketSize;
        byte[] chunkPCM;

        for (int i = 0; i < encodeInputBuffers.length - 1; i++) {
            chunkPCM = getPCMData();//獲取解碼器所在線程輸出的數(shù)據(jù) 代碼后邊會(huì)貼上
            if (chunkPCM == null) {
                break;
            }
            inputIndex = mediaEncode.dequeueInputBuffer(-1);//同解碼器
            inputBuffer = encodeInputBuffers[inputIndex];//同解碼器
            inputBuffer.clear();//同解碼器
            inputBuffer.limit(chunkPCM.length);
            inputBuffer.put(chunkPCM);//PCM數(shù)據(jù)填充給inputBuffer
            mediaEncode.queueInputBuffer(inputIndex, 0, chunkPCM.length, 0, 0);//通知編碼器 編碼
        }

        outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 10000);//同解碼器
        while (outputIndex >= 0) {//同解碼器

            outBitSize = encodeBufferInfo.size;
            outPacketSize = outBitSize + 7;//7為ADTS頭部的大小
            outputBuffer = encodeOutputBuffers[outputIndex];//拿到輸出Buffer
            outputBuffer.position(encodeBufferInfo.offset);
            outputBuffer.limit(encodeBufferInfo.offset + outBitSize);
            chunkAudio = new byte[outPacketSize];
            addADTStoPacket(chunkAudio, outPacketSize);//添加ADTS 代碼后面會(huì)貼上
            outputBuffer.get(chunkAudio, 7, outBitSize);//將編碼得到的AAC數(shù)據(jù) 取出到byte[]中 偏移量offset=7 你懂得
            outputBuffer.position(encodeBufferInfo.offset);
            try {
                bos.write(chunkAudio, 0, chunkAudio.length);//BufferOutputStream 將文件保存到內(nèi)存卡中 *.aac
                LogUtils.d("write " + chunkAudio.length);
            } catch (IOException e) {
                e.printStackTrace();
            }

            mediaEncode.releaseOutputBuffer(outputIndex, false);
            outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 10000);

        }
    }

這里也是常規(guī)的MediaCodec操作袭艺,只是多了一個(gè)ADTS封裝操作搀崭。ADTS前面有介紹,就是多了7個(gè)字節(jié)猾编。這里直接上代碼

    /**
     * 添加ADTS頭
     *
     * @param packet
     * @param packetLen
     */
    private void addADTStoPacket(byte[] packet, int packetLen) {
        int profile = 2; // AAC LC
        int freqIdx = sampleRateType; // 44.1KHz
        int chanCfg = 2; // CPE


// fill in ADTS data
        packet[0] = (byte) 0xFF;
        packet[1] = (byte) 0xF9;
        packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
        packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
        packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
        packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
        packet[6] = (byte) 0xFC;
    }

第四步 釋放資源

    /**
     * 釋放資源
     */
    public void release() {
        try {
            if (bos != null) {
                bos.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    bos = null;
                }
            }
        }

        try {
            if (fos != null) {
                fos.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            fos = null;
        }

        if (mediaEncode != null) {
            mediaEncode.stop();
            mediaEncode.release();
            mediaEncode = null;
        }

        if (mediaDecode != null) {
            mediaDecode.stop();
            mediaDecode.release();
            mediaDecode = null;
        }

        if (mediaExtractor != null) {
            mediaExtractor.release();
            mediaExtractor = null;
        }

        if (onCompleteListener != null) {
            onCompleteListener = null;
        }

        if (onProgressListener != null) {
            onProgressListener = null;
        }
        LogUtils.w("release");
    }

主要就是I/O流瘤睹、MediaCodec、MediaExtractor的釋放答倡。

到這里整個(gè)流程完成


提示:在使用項(xiàng)目代碼時(shí)注意對(duì)應(yīng)版本v1.6:

6.png

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末轰传,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子瘪撇,更是在濱河造成了極大的恐慌获茬,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件倔既,死亡現(xiàn)場(chǎng)離奇詭異锦茁,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)叉存,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門码俩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人歼捏,你說(shuō)我怎么就攤上這事稿存。” “怎么了瞳秽?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵瓣履,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我练俐,道長(zhǎng)袖迎,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任腺晾,我火速辦了婚禮燕锥,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘悯蝉。我一直安慰自己归形,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布鼻由。 她就那樣靜靜地躺著暇榴,像睡著了一般厚棵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蔼紧,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天婆硬,我揣著相機(jī)與錄音,去河邊找鬼奸例。 笑死彬犯,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的哩至。 我是一名探鬼主播躏嚎,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蜜自,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼菩貌!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起重荠,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤箭阶,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后戈鲁,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體仇参,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年婆殿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了诈乒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡婆芦,死狀恐怖怕磨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情消约,我是刑警寧澤肠鲫,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站或粮,受9級(jí)特大地震影響导饲,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜氯材,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一渣锦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧氢哮,春花似錦泡挺、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)贱除。三九已至,卻和暖如春媳溺,著一層夾襖步出監(jiān)牢的瞬間月幌,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工悬蔽, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留扯躺,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓蝎困,卻偏偏與公主長(zhǎng)得像录语,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子禾乘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容