音視頻開發(fā)之旅(50)-邊緩存邊播放之緩存分片(1)

目錄

  1. 什么是緩存分片
  2. 為什么要緩存分片
  3. 如何實(shí)現(xiàn)
  4. 資料
  5. 收獲

一彤侍、什么是緩存分片

我們在上一篇介紹AndroidVideoCache時(shí)帝牡,知道它會一直下載數(shù)據(jù)直到完全下載婴洼。這會帶來流量的浪費(fèi)海蔽。比如一個(gè)5MB的視頻钥组,碼率是2Mb/s输硝,共有5Mx8/2=20秒。如果帶寬是5MB/s程梦,一個(gè)5M的視頻1秒鐘就下載完了点把,但是用戶也許只看到了2秒鐘因?yàn)椴桓信d趣劃走了,這樣就造成了兩個(gè)弊端 流量的浪費(fèi)和LRU緩存策略的漏洞屿附。
這個(gè)問題我們可以通過限速以及緩存LRU策略的調(diào)整來進(jìn)行優(yōu)化郎逃。

同時(shí)還存在另外一個(gè)問題, 如果采用斷點(diǎn)續(xù)傳的方案設(shè)置每次請求的range,如果AndroidVideoCache在拖動超過當(dāng)前當(dāng)前緩存的位置加上總長度的20%就不緩存了挺份。
request.rangeOffset <= cacheAvailable + sourceLength * 0.2f

我們畫圖來分析下這個(gè)邏輯褒翰,看下如果緩存會存在什么問題。


為什么要這樣設(shè)計(jì)吶?如果想要在超過該區(qū)域后想要能夠繼續(xù)緩存該怎么辦吶优训?
我們來思考下seek后改如何進(jìn)行數(shù)據(jù)的獲取朵你。
有如下三種方案:

1.繼續(xù)沿著cached_position順序緩存下去
2.只要當(dāng)前拖動的進(jìn)度條超過cached_position,那就不繼續(xù)緩存揣非,后續(xù)的數(shù)據(jù)完全是網(wǎng)絡(luò)請求抡医。
3.拖動的進(jìn)度條即使超過了cached_position, 從新的位置開始發(fā)起Range 請求
三種方式的優(yōu)劣比較一下:
方案一:繼續(xù)沿著cached_position順序緩存下去早敬,我拖動進(jìn)度條的位置和cached_position相隔很> 遠(yuǎn)魂拦,如果采用這種方案拖動進(jìn)度條之后播放會很慢,所以方案一被斃掉了搁嗓。
方案二:方案二的做法也是可以的芯勘,拖動進(jìn)度條之后也不會卡,但是也有問題腺逛,就是無法做法真正的邊下邊播荷愕,只能順序下載。目前網(wǎng)絡(luò)上熱門的開源項(xiàng)目https://github.com/danikula/AndroidVideoCache 就是采用的這種方案
方案三:解決拖動之后卡頓棍矛,也解決了只能順序下載的問題安疗。目前是最優(yōu)的解決方案
引用自: 頭條都在用的邊下邊播方案

能夠想到有如下兩種方式:

  1. 物理文件空洞的方式,進(jìn)行緩存分片够委,無數(shù)據(jù)的部分被填充為0荐类,有數(shù)據(jù)的部分記錄start和end點(diǎn) 填充數(shù)據(jù)∽旅保—》這個(gè)方案會占用更多的空間(不和系統(tǒng)對文件的空洞方案不同)和內(nèi)存玉罐;該方案要維護(hù)一個(gè)緩存分片信息文件,用于記錄緩存的分片的start和end信息潘拨。
  2. 邏輯文件空洞的方式吊输,進(jìn)行緩存分片,把緩存文件分片成N個(gè)文件铁追,如果某些文件沒有數(shù)據(jù)就不創(chuàng)建季蚂,有數(shù)據(jù)的記錄開始和結(jié)束點(diǎn),如果相鄰的兩個(gè)文件start和end能夠?qū)由侠攀M(jìn)行merge合并扭屁。該方案也可以采用緩存分片信息文件的方案,但是也可以直接從文件夾和文件的命名上進(jìn)行區(qū)分涩禀。

二料滥、為什么要緩存分片

通過上面一小節(jié)我們了解了AndroidVideoCache在Seek后不緩存的場景和原因,以及緩存分片的概念埋泵。這一小節(jié)我們來分析下為什么要用緩存分片

緩存分片有如下好處:

  1. 把大的文件拆分成小的文件進(jìn)行單獨(dú)緩存幔欧,這樣帶來的好處是存儲空間按需分配


圖片來自:十億級視頻播放技術(shù)優(yōu)化揭秘王輝終稿2.key

  1. 為后面的seek緩存的實(shí)現(xiàn)奠定了基礎(chǔ),
  2. 可以提升緩存的命中率
  3. 降低由于seek過多余部分?jǐn)?shù)據(jù)造成播放延遲
  4. 如果使用P2P策略節(jié)省了流量丽声,每個(gè)小的分片可以作為一個(gè)單獨(dú)的種源礁蔗,提升P2P命中率

三、如何實(shí)現(xiàn)

要實(shí)現(xiàn)緩存分片雁社,主要要解決如下兩個(gè)問題

  1. 緩存分片文件的存儲和合并等管理
  2. 緩存分片文件信息的管理

下面我們來分析下一個(gè)實(shí)現(xiàn)緩存分片的開源項(xiàng)目 JeffVideoCache
這個(gè)開源項(xiàng)目不僅實(shí)現(xiàn)了MP4的緩存分片浴井,還增加了對m3u8的支持,在架構(gòu)設(shè)計(jì)上相比較AndroidVideoCache也有很大的改變霉撵。
其中MP4的緩存采用了物理文件空洞的方式磺浙;而M3U8采用的是邏輯文件空洞的方式。

定一個(gè)VideoRange 數(shù)據(jù)結(jié)構(gòu), 用于記錄分片的位置信息

public class VideoRange {
    private long mStart;   //分片的起始位置
    private long mEnd;     //分片的結(jié)束位置
}

LinkedHashMap<Long, VideoRange> mVideoRangeMap; //已經(jīng)緩存的video range結(jié)構(gòu)徒坡,維護(hù)了一個(gè)VideoRange列表撕氧,key是VideoRange的開始位置,value是VideoRange的對象喇完。
如果兩個(gè)VideoRange之間有部分重合伦泥,通過merge合成一個(gè)新的VideoRange。

這一篇我們來分析該開源項(xiàng)目針對MP4的物理文件空洞緩存分片的方案锦溪,下一篇我們再分析針對M3U8邏輯文件空洞緩存分片的方案不脯。

下面我們從代碼看下主流程

3.1 LocalProxyVideoControl#startRequestVideoInfo
添加緩存listener,有開始緩存、緩存進(jìn)度更新刻诊、緩存失敗防楷、緩存成功的回調(diào),觸發(fā)緩存分片信息, 接下來去獲取緩存分片信息文件则涯,緩存分片信息中記錄了改了文件的每個(gè)分片的start和end信息复局。

//LocalProxyVideoControl#startRequestVideoInfo   
 
public void startRequestVideoInfo(String videoUrl, Map<String, String> headers, Map<String, Object> extraParams) {
        //待請求的url
        mVideoUrl = videoUrl;
        //添加緩存listener,有開始緩存、緩存進(jìn)度更新粟判、緩存失敗肖揣、緩存成功的回調(diào)
        VideoProxyCacheManager.getInstance().addCacheListener(videoUrl, mListener);
        VideoProxyCacheManager.getInstance().setPlayingUrlMd5(ProxyCacheUtils.computeMD5(videoUrl));
        //重點(diǎn)分析startRequestVideoInfo
        VideoProxyCacheManager.getInstance().startRequestVideoInfo(videoUrl, headers, extraParams);
    }


 public void startRequestVideoInfo(String videoUrl, Map<String, String> headers, Map<String, Object> extraParams) {

...
//拿到緩存分片信息后,開始觸發(fā)ranged邏輯
startNonM3U8Task(videoCacheInfo, headers);
...
}

3.2 startNonM3U8Task: 開始緩存MP4分片任務(wù)

//VideoProxyCacheManager#startNonM3U8Task   

    private void startNonM3U8Task(VideoCacheInfo cacheInfo, Map<String, String> headers) {
        VideoCacheTask cacheTask = mCacheTaskMap.get(cacheInfo.getVideoUrl());
        if (cacheTask == null) {
            //創(chuàng)建mp4緩存任務(wù)
            cacheTask = new Mp4CacheTask(cacheInfo, headers);
            //加入到map中浮入,
            mCacheTaskMap.put(cacheInfo.getVideoUrl(), cacheTask);
        }
        startVideoCacheTask(cacheTask, cacheInfo);
    }

private void startVideoCacheTask(VideoCacheTask cacheTask, VideoCacheInfo cacheInfo) {
...
     //開始緩存任務(wù)
        cacheTask.startCacheTask();
...
}

3.3 開啟線程進(jìn)行緩存

//Mp4CacheTask#startCacheTask      
public void startCacheTask() {
        //如果文件緩存完(整個(gè)文件龙优,而不是單個(gè)緩存分片文件),直接通知完成
        if (mCacheInfo.isCompleted()) {
            notifyOnTaskCompleted();
            return;
        }
        notifyOnTaskStart();
        LogUtils.i(TAG, "startCacheTask");
        //獲取緩存分片的對象(start 、end)
        VideoRange requestRange = getRequestRange(0L);
        //啟動線程(線程池方式)進(jìn)行緩存(下載)
        startVideoCacheThread(requestRange);
    }

    private void startVideoCacheThread(VideoRange requestRange) {
        mRequestRange = requestRange;
        //saveDir 是videocacheinfo存儲的目錄
        mVideoCacheThread = new Mp4VideoCacheThread(mVideoUrl, mHeaders, requestRange, mTotalSize, mSaveDir.getAbsolutePath(), mCacheThreadListener);
        //通過線程池來執(zhí)行
   VideoProxyThreadUtils.submitRunnableTask(mVideoCacheThread);
    }

3.4 下面我們看下Mp4VideoCacheThread的實(shí)現(xiàn)

public class Mp4VideoCacheThread implements Runnable {

   ...

    private VideoRange mRequestRange;//當(dāng)前請求的video range
                     
    private boolean mIsRunning = true; //是否增長運(yùn)行事秀,該任務(wù)可以pause

    private String mMd5; //緩存文件的md5
    ...


    public void run() {
        //該緩存任務(wù)可以pause彤断,如果沒有在running直接返回
        if (!mIsRunning) {
            return;
        }
        //支持OKHttp和HttpUrlConnection兩種方式進(jìn)行網(wǎng)絡(luò)請求
        if (ProxyCacheUtils.getConfig().useOkHttp()) {
            downloadVideoByOkHttp();
        } else {
            //使用HttpUrlConnection
            downloadVideo();
        }
    }
}

3.5 我們來分析HttpUrlConnection的方式進(jìn)行網(wǎng)絡(luò)請求
可以看到這里采用了物理文件空洞的方案,有數(shù)據(jù)的進(jìn)行填充易迹。至于緩存緩存信息文件(記錄所有的start和end信息)在notifyOnCacheRangeCompleted等中進(jìn)行更新

  /**
     * 通過HttpUrlConnection下載緩存片段
     */
    private void downloadVideo() {
        File videoFile;
        try {
            //mSaveDir是存儲緩存片段的文件夾宰衙,該文件夾下有videocacheinfo和各個(gè)緩存片段;
            videoFile = new File(mSaveDir, mMd5 + StorageUtils.NON_M3U8_SUFFIX);
            if (!videoFile.exists()) {
                videoFile.createNewFile();
            }
        } catch (Exception e) {
            notifyOnCacheFailed(new VideoCacheException("Cannot create video file, exception="+e));
            return;
        }

        long requestStart = mRequestRange.getStart();
        long requestEnd = mRequestRange.getEnd();
        mHeaders.put("Range", "bytes=" + requestStart + "-" + requestEnd);
        HttpURLConnection connection = null;
        InputStream inputStream = null;
        RandomAccessFile randomAccessFile = null;

        try {
            //這里采用了物理文件空洞的方案睹欲。有數(shù)據(jù)的進(jìn)行填充供炼,并通過緩存信息文件記錄所有的start和end信息
            randomAccessFile = new RandomAccessFile(videoFile.getAbsolutePath(), "rw");
            randomAccessFile.seek(requestStart);
            //這里為什么要把requestStart賦值給cachedSize一屋??這里的命名不好改為cachedOffset更合適
            long cachedOffset = requestStart;
            LogUtils.i(TAG, "Start request : " + mRequestRange + ", CurrentCachedSize="+cachedOffset);
            connection = HttpUtils.getConnection(mVideoUrl, mHeaders);
            inputStream = connection.getInputStream();
            LogUtils.i(TAG, "Receive response");

            byte[] buffer = new byte[StorageUtils.DEFAULT_BUFFER_SIZE];
            int readLength;
            while(mIsRunning && (readLength = inputStream.read(buffer)) != -1) {
                if (cachedOffset >= requestEnd) {
                    cachedOffset = requestEnd;
                }
                if (cachedOffset + readLength > requestEnd) {
                    long read = requestEnd - cachedOffset;
                    randomAccessFile.write(buffer, 0, (int)read);
                    cachedOffset = requestEnd;
                } else {
                    randomAccessFile.write(buffer, 0, readLength);
                    cachedOffset += readLength;
                }

                //更新緩存進(jìn)度
                notifyOnCacheProgress(cachedOffset);

                if (cachedOffset >= requestEnd) {
                    //緩存好了一段,通知回調(diào)
                    notifyOnCacheRangeCompleted();
                }
            }
            mIsRunning = false;
        } catch (Exception e) {
            notifyOnCacheFailed(e);
        } finally {
            mIsRunning = false;
            ProxyCacheUtils.close(inputStream);
            ProxyCacheUtils.close(randomAccessFile);
            HttpUtils.closeConnection(connection);
        }
    }

3.6 通知更新緩存分片信息

//Mp4CacheTask#notifyOnCacheRangeCompleted  
   /**
     * @param startPosition :上一個(gè)緩存分片的 end
     */  
 private void notifyOnCacheRangeCompleted(long startPosition) {
        //這時(shí)候已經(jīng)緩存好了一段分片,可以更新一下video range數(shù)據(jù)結(jié)構(gòu)了
        updateVideoRangeInfo();
        if (mCacheInfo.isCompleted()) {
            notifyOnTaskCompleted();
        } else {
            if (startPosition == mTotalSize) {
                //說明已經(jīng)緩存好,但是整視頻中間還有一些洞,但是不影響,可以忽略
            } else {
                //開啟下一段視頻分片的緩存
                VideoRange requestRange = getRequestRange(startPosition);
                //是否開啟下一緩存分片的下載袋哼。
                // 這里可以再精準(zhǔn)的控制下冀墨,按需下載
                startVideoCacheThread(requestRange);
            }
        }
    }

3.7 更新緩存分片信息
這個(gè)方法比較關(guān)鍵,針對緩存分片信息進(jìn)行整合涛贯,重疊的部分進(jìn)行合并诽嘉,重新生成videoRange列表。更新后把其更新到文件中

//Mp4CacheTask#updateVideoRangeInfo

private synchronized void updateVideoRangeInfo() {
        if (mVideoRangeMap.size() > 0) {
            long finalStart = -1;
            long finalEnd = -1;

            long requestStart = mRequestRange.getStart();
            long requestEnd = mRequestRange.getEnd();

            for(Map.Entry<Long, VideoRange> entry : mVideoRangeMap.entrySet()) {
                VideoRange videoRange = entry.getValue();
                long startResult = VideoRangeUtils.determineVideoRangeByPosition(videoRange, requestStart);
                long endResult = VideoRangeUtils.determineVideoRangeByPosition(videoRange, requestEnd);

                if (finalStart == -1) {
                    if (startResult == 1) {
                        //如果requestStart小于遍歷的一個(gè)片段的start位置弟翘,取requestStart
                        finalStart = requestStart;
                    } else if (startResult == 2) {
                        //如果requestStart在遍歷的一個(gè)片段的start和end中虫腋,取該片段的start
                        finalStart = videoRange.getStart();
                    } else {
                        //如果超出繼續(xù)遍歷其他片段,進(jìn)行對比
                        //先別急著賦值,還要看下一個(gè)videoRange
                    }
                }
                if (finalEnd == -1) {
                    if (endResult == 1) {
                        finalEnd = requestEnd;
                    } else if (endResult == 2) {
                        finalEnd = videoRange.getEnd();
                    } else {
                        //先別急著賦值,還要看下一個(gè)videoRange
                    }
                }
                //該循環(huán)的目的是確定finalStart和finalEnd稀余,用于確定VideoRange
                if (finalStart != -1 && finalEnd != -1) {
                    break;
                }
            }
            if (finalStart == -1) {
                finalStart = requestStart;
            }
            if (finalEnd == -1) {
                finalEnd = requestEnd;
            }

            VideoRange finalVideoRange = new VideoRange(finalStart, finalEnd);
            LogUtils.i(TAG, "updateVideoRangeInfo--->finalVideoRange: " + finalVideoRange);

            LinkedHashMap<Long, VideoRange> tempVideoRangeMap = new LinkedHashMap<>();
            for(Map.Entry<Long, VideoRange> entry : mVideoRangeMap.entrySet()) {
                VideoRange videoRange = entry.getValue();
                if (VideoRangeUtils.containsVideoRange(finalVideoRange, videoRange)) {
                    //如果finalVideoRange包含videoRange
                    tempVideoRangeMap.put(finalVideoRange.getStart(), finalVideoRange);
                } else if (VideoRangeUtils.compareVideoRange(finalVideoRange, videoRange) == 1) {
                    //如果兩個(gè)沒有交集,且finalVideoRange的end 小于videoRange的start悦冀,則map先加入finalVideoRange再加入videoRange
                    tempVideoRangeMap.put(finalVideoRange.getStart(), finalVideoRange);
                    tempVideoRangeMap.put(videoRange.getStart(), videoRange);
                } else if (VideoRangeUtils.compareVideoRange(finalVideoRange, videoRange) == 2) {
                    //如果兩個(gè)沒有交集,且finalVideoRange的start 大于videoRange的end,則map先加入videoRange再加入finalVideoRange
                    tempVideoRangeMap.put(videoRange.getStart(), videoRange);
                    tempVideoRangeMap.put(finalVideoRange.getStart(), finalVideoRange);
                }
            }
            mVideoRangeMap.clear();
            mVideoRangeMap.putAll(tempVideoRangeMap);
        } else {
            LogUtils.i(TAG, "updateVideoRangeInfo--->mRequestRange : " + mRequestRange);
            mVideoRangeMap.put(mRequestRange.getStart(), mRequestRange);
        }

        LinkedHashMap<Long, Long> tempSegMap = new LinkedHashMap<>();
        //進(jìn)行了merge睛琳?
        for(Map.Entry<Long, VideoRange> entry : mVideoRangeMap.entrySet()) {
            VideoRange videoRange = entry.getValue();
            LogUtils.i(TAG, "updateVideoRangeInfo--->Result videoRange : " + videoRange);
            tempSegMap.put(videoRange.getStart(), videoRange.getEnd());
        }
        //最小化鎖的作用范圍
        synchronized (mSegMapLock) {
            mVideoSegMap.clear();
            mVideoSegMap.putAll(tempSegMap);
        }
        mCacheInfo.setVideoSegMap(mVideoSegMap);

        // 當(dāng)mVideoRangeMap只有一個(gè)片段雏门,并且該ranged是完整的這個(gè)那個(gè)緩存文件(不是某個(gè)子片段),則標(biāo)記為completed
        if (mVideoRangeMap.size() == 1) {
            VideoRange videoRange = mVideoRangeMap.get(0L);
            LogUtils.i(TAG, "updateVideoRangeInfo---> videoRange : " + videoRange);
            if (videoRange != null && videoRange.equals(new VideoRange(0, mTotalSize))) {
                LogUtils.i(TAG, "updateVideoRangeInfo--->Set completed");
                mCacheInfo.setIsCompleted(true);
            }
        }

        //子線程中掸掏,更新緩存信息文件
        saveVideoInfo();
    }

 public static void saveVideoCacheInfo(VideoCacheInfo info, File dir) {
        File file = new File(dir, INFO_FILE);
        ObjectOutputStream fos = null;
        try {
            synchronized (sInfoFileLock) {
                fos = new ObjectOutputStream(new FileOutputStream(file));
                fos.writeObject(info);
            }
        } catch (Exception e) {

        } finally {
            ProxyCacheUtils.close(fos);
        }
    }

緩存分片物理文件空洞的方案分析分析到這里基本上就結(jié)束了茁影,感謝JeffVideoCache作者的開源,
下一篇我們再來分析緩存分片采用的邏輯文件空洞的方案丧凤,歡迎交流

四募闲、資料

  1. JeffVideoCache
  2. 頭條都在用的邊下邊播方案
  3. 重點(diǎn)推薦-QQ空間十億級視頻播放技術(shù)優(yōu)化揭秘王輝終稿2.key

五、收獲

從本篇的學(xué)習(xí)分析

  1. 了解緩存分片的是什么愿待,為什么浩螺,以及如何實(shí)現(xiàn)
  2. 分析了緩存分片物理文件空洞方案的實(shí)現(xiàn)。

感謝你的閱讀
下一篇我們我們來分析緩存分片邏輯文件空洞方案的實(shí)現(xiàn)仍侥,歡迎關(guān)注公眾號“音視頻開發(fā)之旅”要出,一起學(xué)習(xí)成長。
歡迎交流

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末农渊,一起剝皮案震驚了整個(gè)濱河市患蹂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌砸紊,老刑警劉巖传于,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異醉顽,居然都是意外死亡沼溜,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進(jìn)店門游添,熙熙樓的掌柜王于貴愁眉苦臉地迎上來系草,“玉大人通熄,你說我怎么就攤上這事≌叶迹” “怎么了唇辨?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長檐嚣。 經(jīng)常有香客問我,道長啰扛,這世上最難降的妖魔是什么嚎京? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮隐解,結(jié)果婚禮上鞍帝,老公的妹妹穿的比我還像新娘。我一直安慰自己煞茫,他們只是感情好帕涌,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著续徽,像睡著了一般蚓曼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上钦扭,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天纫版,我揣著相機(jī)與錄音,去河邊找鬼客情。 笑死其弊,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的膀斋。 我是一名探鬼主播梭伐,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼仰担!你這毒婦竟也來了糊识?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤摔蓝,失蹤者是張志新(化名)和其女友劉穎技掏,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體项鬼,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡哑梳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绘盟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸠真。...
    茶點(diǎn)故事閱讀 40,498評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡悯仙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出吠卷,到底是詐尸還是另有隱情锡垄,我是刑警寧澤,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布祭隔,位于F島的核電站货岭,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏疾渴。R本人自食惡果不足惜千贯,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望搞坝。 院中可真熱鬧搔谴,春花似錦、人聲如沸桩撮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽店量。三九已至梗掰,卻和暖如春备埃,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工购裙, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留阻桅,地道東北人鸯匹。 一個(gè)月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓伦腐,卻偏偏與公主長得像,于是被迫代替她去往敵國和親疼鸟。 傳聞我的和親對象是個(gè)殘疾皇子后控,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評論 2 359

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