安卓多線程斷點(diǎn)續(xù)傳下載功能(靠譜第三方組件扰魂,原理demo)

一麦乞,原生的DownloadManager

從Android 2.3(API level 9)開(kāi)始,Android以Service的方式提供了全局的DownloadManager來(lái)系統(tǒng)級(jí)地優(yōu)化處理長(zhǎng)時(shí)間的下載操作劝评。

DownloadManager支持失敗重試姐直、Notification通知等基本特性。但是DownloadManager提供的接口很有限蒋畜,雖然有暫停狀態(tài)卻沒(méi)有提供主動(dòng)暫停的接口声畏。所以需手動(dòng)實(shí)現(xiàn)斷點(diǎn)續(xù)傳,以及需手動(dòng)實(shí)現(xiàn)單文件的多線程(分段)下載姻成,還是比較麻煩插龄。

總結(jié):DownloadManager比較適用于簡(jiǎn)單的單文件下載。

二科展,第三方組件

1均牢,F(xiàn)ileDownloader
git地址:https://github.com/lingochamp/FileDownloader
中文文檔:https://github.com/lingochamp/FileDownloader/blob/master/README-zh.md
描述:在第三方下載組件中比較突出,成熟才睹,穩(wěn)定徘跪,健壯。支持多任務(wù)下載琅攘,支持多線程下載真椿,支持?jǐn)帱c(diǎn)續(xù)傳,支持替換網(wǎng)絡(luò)請(qǐng)求框架等乎澄。
實(shí)際應(yīng)用:在快手的后臺(tái)進(jìn)程里有l(wèi)iulishuo/FileDownloader,因?yàn)榭焓謨?nèi)有下載服務(wù)测摔,有可能用于應(yīng)用內(nèi)的更新模塊置济,liulishuo的FileDownloader就是這個(gè)組件解恰。

image

2,OkDownload
git地址:https://github.com/lingochamp/okdownload
中文文檔:https://github.com/lingochamp/okdownload/blob/master/README-zh.md
描述:繼承了所有FileDownloader的優(yōu)點(diǎn)浙于,甚至做了更多的優(yōu)化以及更多的特性护盈。

總結(jié):建議在項(xiàng)目中實(shí)現(xiàn)多任務(wù)多線程下載功能優(yōu)先考慮上面的第三方組件,而不是重復(fù)造輪子羞酗。畢竟能在受眾群大的商業(yè)應(yīng)用中成熟使用的第三方框架腐宋,是經(jīng)過(guò)多次迭代的思想結(jié)晶,考慮了多種情況檀轨,踩過(guò)很多坑胸竞。一般自己從頭寫(xiě)的話,很難比其的做得更好参萄,考慮得更完善卫枝,尤其是在有限的項(xiàng)目開(kāi)發(fā)時(shí)間內(nèi)。

三讹挎,原理demo

這里列一個(gè)兩年前寫(xiě)的demo校赤,用的都是非常基礎(chǔ)的東西筒溃÷砝海考慮的點(diǎn)也比較少,就只是從頭實(shí)現(xiàn)這個(gè)功能怜奖。

(一)思路:

1浑测,如何實(shí)現(xiàn)多線程下載功能?

(后續(xù)都假設(shè)1個(gè)文件啟動(dòng)3個(gè)線程去下載)先起一個(gè)初始線程(initThread)去查詢目標(biāo)下載文件的基本信息比如文件長(zhǎng)度等烦周,生成對(duì)應(yīng)本地文件尽爆。計(jì)算每個(gè)下載線程(DownloadThread)的下載長(zhǎng)度和下載起始結(jié)束點(diǎn),通過(guò)線程池啟動(dòng)3個(gè)下載線程進(jìn)行下載并寫(xiě)入到本地文件的對(duì)應(yīng)起始點(diǎn)中读慎,當(dāng)3個(gè)下載線程都下載完成后漱贱,這個(gè)文件就下載完整了。

2夭委,如何實(shí)現(xiàn)暫停功能幅狮?

設(shè)計(jì)一個(gè)下載任務(wù)類(DownloadTask),開(kāi)始下載一個(gè)文件就新建一個(gè)下載任務(wù)類DownloadTask株灸。下載任務(wù)類負(fù)責(zé)管理啟動(dòng)多個(gè)下載線程開(kāi)始下載崇摄。下載任務(wù)類里設(shè)置一個(gè)公開(kāi)的暫停標(biāo)志,在外部可以設(shè)置暫停標(biāo)志的true/false慌烧,當(dāng)下載線程下載文件時(shí)逐抑,判斷到暫停標(biāo)志為true,則結(jié)束本線程即中斷下載屹蚊。

3厕氨,如何實(shí)現(xiàn)即時(shí)展示當(dāng)前下載進(jìn)度进每?

下載任務(wù)類里設(shè)置一個(gè)表示當(dāng)前已完成下載長(zhǎng)度的變量,在下載線程的下載過(guò)程中命斧,及時(shí)更新累加田晚。設(shè)置每500ms計(jì)算一次下載進(jìn)度=當(dāng)前已完成下載長(zhǎng)度/下載文件總長(zhǎng)度,并發(fā)出廣播国葬。外部接收到廣播后更新ui贤徒。

4,如何實(shí)現(xiàn)斷點(diǎn)續(xù)傳功能(暫停/網(wǎng)絡(luò)故障后再恢復(fù)下載從之前下載進(jìn)度基礎(chǔ)上繼續(xù)下載汇四,而不是重新下載)接奈?

這就必須把下載進(jìn)程的相關(guān)信息本地存儲(chǔ),然后在恢復(fù)下載時(shí)查詢本地存儲(chǔ)的數(shù)據(jù)船殉,獲得目前的下載進(jìn)度鲫趁,再?gòu)哪情_(kāi)始下載。

要給每一個(gè)下載線程保存一條這個(gè)線程相關(guān)信息的數(shù)據(jù)利虫,考慮到當(dāng)新建n個(gè)下載任務(wù)時(shí)挨厚,同時(shí)開(kāi)啟3n個(gè)下載線程,就需要保存3n條線程信息數(shù)據(jù)糠惫。而在暫停/網(wǎng)絡(luò)故障/下載失敗時(shí)疫剃,都要更新對(duì)應(yīng)數(shù)據(jù),選用本地?cái)?shù)據(jù)庫(kù)sqlite來(lái)存儲(chǔ)數(shù)據(jù)硼讽。

安卓的數(shù)據(jù)表的操作巢价,基本就是“數(shù)據(jù)模型+helper幫助類+dao實(shí)現(xiàn)類”來(lái)一套。

但是注意這里是多線程操作數(shù)據(jù)表固阁,“增刪改查”操作里壤躲,“增刪改”方法都要加上synchronized修飾詞。synchronized是保證線程安全备燃,就是在當(dāng)多個(gè)線程操作同一資源時(shí)保證某一時(shí)刻只能有一個(gè)線程在執(zhí)行這個(gè)任務(wù)碉克。

舉個(gè)例子,想象一個(gè)線程是一個(gè)人并齐,一張數(shù)據(jù)表裝在一棟房子里漏麦,房子有兩個(gè)門(mén),門(mén)a只給不帶synchronized方法的人進(jìn)况褪,門(mén)b只給帶synchronized方法的人進(jìn)撕贞。門(mén)a沒(méi)什么條件,只要是使用不帶synchronized方法的人都可以直接進(jìn)去测垛。門(mén)b卻有條件捏膨,一次只能進(jìn)去一個(gè)人,并且下一個(gè)進(jìn)去的人要在上一個(gè)進(jìn)去的人出來(lái)之后才能進(jìn)去食侮。

synchronized更多詳細(xì)資料參考:http://www.reibang.com/p/d53bf830fa09

https://www.cnblogs.com/yulinfeng/p/11020576.html

(二)當(dāng)年湊zhuanli畫(huà)的流程圖:

image
image

(三)代碼:
啟動(dòng)下載

Intent intent = new Intent( mContext, DownloadService.class );
fileInfo.setIsDownload("true");                 
intent.setAction(IntentAction.ACTION_START);
intent.putExtra( KeyName.FILEINFO_TAG, fileInfo );
mContext.startService(intent); // 通過(guò)intent傳遞信息fileInfo給servers

暫停下載

Intent intent = new Intent( mContext, DownloadService.class );
fileInfo.setIsDownload("false");        
intent.setAction(IntentAction.ACTION_PAUSE);
intent.putExtra( KeyName.FILEINFO_TAG, fileInfo );
mContext.startService(intent);

DownloadService

/**
* 用于文件下載的service
*/
public class DownloadService extends Service {
/**
* 定義消息處理handler的標(biāo)志
*/
  private static final int MSG_INIT =0 ;
  /**
* 文件下載線程任務(wù)的集合
*/
  private MapmTask =new LinkedHashMap();
  /**
* onStartCommand()接收Activity中StartService發(fā)送的信息
*/
  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
// 獲取Activity傳進(jìn)來(lái)的參數(shù)
      FileInfo fileInfo =(FileInfo) intent.getSerializableExtra( KeyName.FILEINFO_TAG );// 獲取下載文件信息
      Log.i("intent", "DownloadService-47行- "+fileInfo.getFileName()+" intent=null: " + (intent==null) );
      Log.i("intent", "DownloadService-48行- "+fileInfo.getFileName()+" intent.getSerializableExtra(Config.FILEINFO_TAG):"
            +(intent.getSerializableExtra(KeyName.FILEINFO_TAG)==null) );
      // 開(kāi)啟多線程下載
      if ( IntentAction.ACTION_START.equals(intent.getAction()) ) {
initThread mInitThread =new initThread(fileInfo);
      DownloadTask.sExecutorService.execute(mInitThread);// 通過(guò)線程池開(kāi)啟初始化線程
      // 暫停多線程下載
      }else if ( IntentAction.ACTION_PAUSE.equals(intent.getAction()) ) {
DownloadTask tast =mTask.get(fileInfo.getId() ); // 從集合中取出下載任務(wù)
        if (tast !=null) {
          tast.isPause =true ;
        }
      }else if(IntentAction.ACTION_DELETE.equals(intent.getAction())){
        DownloadTask tast =mTask.get(fileInfo.getId() ); // 從集合中取出下載任務(wù)
          if (tast !=null) {
            tast.isDel=true ;
          }
      }
      return super.onStartCommand(intent, flags, startId);
}

  @Override
  public IBinderonBind(Intent intent) {
      return null;
  }
/**
* 初始化文件下載線程:創(chuàng)建本地下載位置号涯,并開(kāi)啟下載任務(wù)
**/
  class initThread extends Thread{
/**
* 下載的文件的所有屬性信息
*/
      private FileInfomFileInfo =null ;
      /**
* 初始化文件下載線程:確保創(chuàng)建本地下載位置熬北,并開(kāi)啟文件下載任務(wù)
      * @param mFileInfo 下載的文件的所有屬性信息
*/
      public initThread(FileInfo mFileInfo) {
          this.mFileInfo = mFileInfo;
      }

//開(kāi)啟開(kāi)啟下載任務(wù)
      Handlerhandler =new Handler(){
        @Override
        public void handleMessage(android.os.Message msg) {
          switch (msg.what) {
              case MSG_INIT:
                  FileInfo fileInfo = (FileInfo)  msg.obj ;
                  // 啟動(dòng)下載任務(wù)
                  DownloadTask task =new DownloadTask( DownloadService.this, fileInfo, DownloadConfig.DONWNLOAD_THREAD_NUM);
                  task.download();
                  //把下載任務(wù)添加到下載集合中
                  mTask.put(fileInfo.getId(), task);  //將開(kāi)啟下載線程的id和實(shí)例添加到map中,在暫停時(shí)通過(guò)id獲取實(shí)例诚隙,并令它暫停
                  break;
              default:
                  break;
          }
      }
};
      @Override
      public void run(){
          HttpURLConnection conn =null ;
          RandomAccessFile raf =null ;// 隨機(jī)訪問(wèn)文件,可以在文件的隨機(jī)寫(xiě)入起胰,對(duì)應(yīng)斷點(diǎn)續(xù)傳功能
          try {
            // 連接網(wǎng)絡(luò)文件
            URL url =new URL(mFileInfo.getUrl() );
            conn = (HttpURLConnection) url.openConnection();
            conn.setConnectTimeout(3000);// 設(shè)置連接超時(shí)時(shí)間
            conn.setRequestMethod("GET");
            // 獲取文件長(zhǎng)度
            int length = -1;
            if (conn.getResponseCode()== HttpStatus.SC_OK ) {
              length = conn.getContentLength();
              Log.i("http", "DownloadService-138行 獲取網(wǎng)絡(luò)數(shù)據(jù)長(zhǎng)度="+ length );
            }else{
              Log.i("http", "DownloadService-140行 http連接失斁糜帧!");
            }
            if ( length <=0 ){
               return ;
            }
            // 判斷下載路徑是否存在
            File dir =new File( DownloadConfig.DOWNLOAD_PATH );
            if ( !dir.exists() ){
              dir.mkdir();
            }
            // 在本地創(chuàng)建文件
            File file =new File( dir, mFileInfo.getFileName() );
            raf =new RandomAccessFile( file, "rwd" );
            // 設(shè)置文件長(zhǎng)度
            raf.setLength( length );
            mFileInfo.setLength( length );
            handler.obtainMessage(MSG_INIT, mFileInfo).sendToTarget();  //將數(shù)據(jù)發(fā)回給handler
            //關(guān)閉連接
            if (raf!=null) {
              raf.close();
            }if (conn !=null) {
              conn.disconnect();
            }
         }catch (Exception e) {
            e.printStackTrace();
         }
      }
  }
}

DownloadTask

/**
 * 下載任務(wù)類
 **/
public class DownloadTask {
    /**
     * 上下文
     */
    private Context  mContext = null ;
    /**
     * 下載的文件的所有屬性信息
     */
    private FileInfo mFileInfo = null ;
    /**
     * 數(shù)據(jù)(庫(kù))訪問(wèn)接口
     */
    private ThreadDAO mDAO = null;
    /**
     * 已下載字節(jié)長(zhǎng)度
     */
    private int mFinishedLen  = 0;
    /**
     * 下載暫停標(biāo)志
     */
    public  Boolean isPause = false;
    /**
     * 下載刪除標(biāo)志
     */
    public  Boolean isDel = false;
    /**
     * 默認(rèn)的下載線程數(shù)
     */
    private int mThreadNum  = 0 ;
    /**
     * 下載線程集合
     */
    private List<DownloadThread> mThreadList = null ;
    /**
     * 帶緩存線程池效五,s開(kāi)頭表示用到static關(guān)鍵字 
     */
    public static ExecutorService sExecutorService = Executors.newCachedThreadPool();
    /**
     * 文件下載的線程任務(wù)類
     * @param mContext  上下文
     * @param mFileInfo 下載的文件的所有屬性信息
     * @param threadNum 文件分段下載線程數(shù)
     */
    public DownloadTask(Context mContext, FileInfo mFileInfo, int threadNum) {
        this.mContext = mContext;
        this.mFileInfo = mFileInfo;
        this.mThreadNum = threadNum ;
        mDAO = new ThreadDAOIImpl(mContext);
    }
    /**
     * 開(kāi)始下載文件
     */
    public  void download() {
        List<ThreadInfo> threadInfoList = mDAO.getThreads(mFileInfo.getUrl());  // 讀取數(shù)據(jù)庫(kù)中文件下載的信息
        // 不存下載線程則創(chuàng)建
        if( threadInfoList.size()== 0 ){
            int length = mFileInfo.getLength()/mThreadNum ; // 獲取單個(gè)線程下載長(zhǎng)度
            for (int i = 0; i < mThreadNum; i++) {          // 創(chuàng)建線程信息
                ThreadInfo threadInfo = new ThreadInfo(i, mFileInfo.getUrl(),length*i, (i+1)*length-1, 0, mFileInfo.getMd5(),"false", "none");
                if (i==mThreadNum-1) {
                    threadInfo.setEnd(mFileInfo.getLength());// 設(shè)置最后一個(gè)線程的下載長(zhǎng)度
                }
                threadInfoList.add(threadInfo); // 添加線程信息到集合
                mDAO.insertThread(threadInfo);  // 向數(shù)據(jù)庫(kù)插入文件下載的信息
            }                                   // 放到run外面地消,比較不容易產(chǎn)生數(shù)據(jù)庫(kù)的死鎖   
        }
        // 啟動(dòng)多個(gè)線程進(jìn)行下載
        mThreadList = new ArrayList<DownloadThread>();
        for (ThreadInfo info : threadInfoList) {
            DownloadThread downloadThread = new DownloadThread(info);
            if ( info.getFinished()<(info.getEnd()-info.getStart())&&  info.getId()==0  ||
                    info.getFinished()<(info.getEnd()-info.getStart()+1)&&  info.getId()!=0 ) {  //若未完成下載則下載
                DownloadTask.sExecutorService.execute(downloadThread);
                mThreadList.add(downloadThread);                       //添加線程到集合中
            }
        }
    }
    /**
     * 判斷所有線程是否執(zhí)行完成
     * @param threadInfo  下載文件的線程信息
     */
    private synchronized  void checkAllThreadsFinished(ThreadInfo threadInfo){  // synchronized 同步方法,保證同一時(shí)間只有一個(gè)線程訪問(wèn)該方法
        boolean allFinished = true ;   //所有線程下載結(jié)束標(biāo)識(shí)
        // 遍歷線程集合畏妖,判斷是否都下載完畢
        for (DownloadThread thread : mThreadList) {
            if (!thread.isFinished) {
                allFinished = false ;
                break ;
            }
        }
        // 所有線程下載結(jié)束:驗(yàn)證md5脉执,相同則存儲(chǔ)下載長(zhǎng)度,否則清空戒劫;發(fā)送廣播通知UI下載任務(wù)結(jié)束
        if (allFinished) {
            MD5Util md5Util = new MD5Util();
            DateTools dateTools = new DateTools();
            List<ThreadInfo> threadInfosList =mDAO.getThreads(threadInfo.getUrl());
            for (ThreadInfo info :threadInfosList) {
                if ( !md5Util.isMD5Equal(info) ) {  // md5驗(yàn)證失敗半夷,清空下載長(zhǎng)度
                    mDAO.updateThread(info.getUrl(), info.getId(), 0, info.getMd5(), "false" ,dateTools.getCurrentTime() );
                }else if(info.getId()==0) {
                    mDAO.updateThread(info.getUrl(), info.getId(),(info.getEnd()-info.getStart()), info.getMd5(), "true", dateTools.getCurrentTime() );
                }else if(info.getId()!=0){
                    mDAO.updateThread(info.getUrl(), info.getId(),(info.getEnd()-info.getStart()+1), info.getMd5(), "true",dateTools.getCurrentTime() );
                }
            }
            Intent intent = new Intent(IntentAction.ACTION_FINISH);
            mFileInfo.setOvertime(dateTools.getCurrentTime());
            intent.putExtra( KeyName.FILEINFO_TAG, mFileInfo);
            mContext.sendBroadcast(intent);
        }
    }
    /**
     * 進(jìn)行文件下載的線程
     **/
    class DownloadThread extends Thread{
        /**
         * 文件下載線程的信息
         */
        private ThreadInfo mThreadInfo = null ;
        /**
         * 標(biāo)識(shí)線程是否執(zhí)行完成
         */
        public Boolean isFinished = false ;
        /**
         * 廣播下載進(jìn)度的間隔時(shí)間
         */
        private final static int BROADCAST_TIME = 100 ;
        /**
         * httpUrl連接
         */
        HttpURLConnection conn = null ;
        /**
         * 任意寫(xiě)入文件:RandomAccessFile
         */
        RandomAccessFile raf= null;
        /**
         * 輸入流
         */
        InputStream input = null ;
        /**
         * 進(jìn)行文件下載的線程
         * @param mThreadInfo 文件下載線程的信息
         **/
        public DownloadThread(ThreadInfo mThreadInfo) {
            this.mThreadInfo = mThreadInfo;
        }
        @Override
        public void run(){
            try {
                URL url = new URL(mThreadInfo.getUrl());
                conn = (HttpURLConnection) url.openConnection();
                conn.setConnectTimeout(3000);
                conn.setRequestMethod("GET");
                //設(shè)置url上資源下載位置/范圍
                int start = mThreadInfo.getStart() +mThreadInfo.getFinished();
                conn.setRequestProperty("Range", "bytes=" +start +"-" + mThreadInfo.getEnd() );
                //設(shè)置文件本地寫(xiě)入位置
                File file = new File( DownloadConfig.DOWNLOAD_PATH, mFileInfo.getFileName() );
                raf = new RandomAccessFile(file, "rwd");
                raf.seek(start);
                mFinishedLen +=mThreadInfo.getFinished();
                //開(kāi)始下載啦
                if ( conn.getResponseCode() == HttpStatus.SC_PARTIAL_CONTENT ){
                    //讀取數(shù)據(jù)
                    input = conn.getInputStream();
                    byte[] buffer = new byte[ 1024 * 4 ];
                    int len = -1;
                    long time = System.currentTimeMillis();
                    while((len = input.read(buffer))!=-1){
                        //寫(xiě)入文件
                        raf.write(buffer,0,len);
                        mFinishedLen += len ;
                        mThreadInfo.setFinished(mThreadInfo.getFinished() + len);
                        if (System.currentTimeMillis()-time > BROADCAST_TIME ) {  //每500ms刷新一次
                            time = System.currentTimeMillis();
                            double progress_result = (double)mFinishedLen / (double)mFileInfo.getLength();  // 計(jì)算下載進(jìn)度
                            double download_rate = (double)len / (double)(1024*BROADCAST_TIME/1000);        // 計(jì)算下載速率
                            Intent  intent  = new Intent( IntentAction.ACTION_UPDATE);
                            intent.putExtra( KeyName.FINISHED_TAG,progress_result );
                            intent.putExtra( KeyName.DOWNLOAD_RATE_TAG,download_rate );
                            intent.putExtra("id", mFileInfo.getId() );
                            mContext.sendBroadcast(intent);
                        }
                        //下載暫停時(shí),保存下載進(jìn)度搭配數(shù)據(jù)庫(kù)
                        if (isPause) {
                            mDAO.updateThread(mThreadInfo.getUrl(), mThreadInfo.getId(), mThreadInfo.getFinished(), mThreadInfo.getMd5(), "false", "none" );
                            return;
                        }
                        //刪除下載時(shí)迅细,刪除數(shù)據(jù)庫(kù)內(nèi)的相關(guān)信息
                        if (isDel) {
                            if(!mDAO.getThreads(mThreadInfo.getUrl()).isEmpty()) {
                                mDAO.deleteThread(mThreadInfo.getUrl());
                            }
//                          Intent  intent  = new Intent( IntentAction.ACTION_DELETE);
//                          mContext.sendBroadcast(intent);
                            return;
                        }
                        // 下載連接獲取輸入流或文件寫(xiě)入失敗巫橄,清空文件下載進(jìn)度,并重新下載
                        if (raf ==null || input ==null ){
                            mDAO.updateThread(mThreadInfo.getUrl(), 0, 0, mThreadInfo.getMd5(), "false" , "none");
                            mDAO.updateThread(mThreadInfo.getUrl(), 1, 0, mThreadInfo.getMd5(), "false" , "none");
                            mDAO.updateThread(mThreadInfo.getUrl(), 2, 0, mThreadInfo.getMd5(), "false" , "none");
                            Intent  intent  = new Intent( IntentAction.ACTION_UPDATE);
                            intent.putExtra( KeyName.FINISHED_TAG, 0 );
                            intent.putExtra( KeyName.DOWNLOAD_RATE_TAG, 0 );
                            intent.putExtra("id", mFileInfo.getId() );
                            mContext.sendBroadcast(intent);
                            download();
                        }
                    }
                    isFinished = true ;// 標(biāo)識(shí)線程執(zhí)行完畢
                    // 檢查下載任務(wù)是否執(zhí)行完畢
                    checkAllThreadsFinished(mThreadInfo );
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally{//關(guān)閉連接
                try {
                    if (raf !=null) {
                        raf.close();
                    }else {
                        System.out.println("DOwnloadTask-280行 RadomAccessFile發(fā)生錯(cuò)誤");
                    }if (input !=null) {
                        input.close();
                    }else {
                        System.out.println("DOwnloadTask-280行 inputStream發(fā)生錯(cuò)誤");
                    }if (conn !=null) {
                        conn.disconnect();
                    }else {
                        System.out.println("DOwnloadTask-280行 HttpURLConnection發(fā)生錯(cuò)誤");
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

FileInfo

/**
 * 下載的文件的所有屬性信息
 */
public class FileInfo  implements Serializable{
    private static final long serialVersionUID = 1L;
    /**
     * 下載文件的id
     */
    private int id ;
    /**
     * 下載文件總長(zhǎng)度
     */
    private int length ;
    /**
     * 下載文件的web路徑
     */
    private String url ;
    /**
     * 下載文件名
     */
    private String  fileName ;
    /**
     * 下載文件的 MD5值
     */
    private String  md5 ;
    /**
     * 文件下載進(jìn)度(%)
     */
    private double finished ;
    /**
     * 文件下載速率
     */
    private double rate ;
    /**
     * 下載文件的 MD5值
     */
    private String  over ;
    /**
     * 下載文件完成下載的時(shí)間
     */
    private String  overtime ;
    public String getOvertime() {
        return overtime;
    }
    public void setOvertime(String overtime) {
        this.overtime = overtime;
    }
    /**
     * 下載文件是否正在下載
     */
    private String  isDownload ;
    public FileInfo() {
    }
    /**
     * 存儲(chǔ)下載文件的屬性信息
     * @param id        下載文件的id標(biāo)識(shí)
     * @param length    文件總長(zhǎng)度
     * @param url       文件web路徑
     * @param fileName  下載文件名
     * @param finished  文件下載進(jìn)度
     * @param rate      文件下載速率
     * @param md5              文件的md5值
     * @param over      文件是否完成下載
     * @param isDownload 文件是否正在下載
     */
    public FileInfo(int id, int length, String url, String fileName,
                    int finished,double rate, String md5, String over, String  isDownload, String overtime) {
        this.id = id;
        this.url = url;
        this.length = length;
        this.fileName = fileName;
        this.finished = finished;
        this.rate = rate;
        this.md5 = md5;
        this.over = over ;
        this.overtime = overtime ;
        this.isDownload = isDownload ;
    }
    /**
     * 獲取文件下載狀態(tài)
     */
    public String getIsDownload() {
        return isDownload;
    }
    /**
     * 設(shè)置文件下載狀態(tài)
     * @param isDownload 文件是否正在下載
     */
    public void setIsDownload(String isDownload) {
        this.isDownload = isDownload;
    }
    /**
     * 獲取文件下載進(jìn)度(%)
     */
    public double getRate() {
        return rate;
    }
    /**
     * 存儲(chǔ)文件下載進(jìn)度(%)
     */
    public void setRate(double rate) {
        this.rate = rate;
    }
    /**
     * 獲取文件MD5
     */
    public String getMd5() {
        return md5;
    }
    /**
     * 存儲(chǔ)文件MD5
     */
    public void setMd5(String md5) {
        this.md5 = md5;
    }
    /**
     * 獲取下載文件的id標(biāo)識(shí)
     */
    public int getId() {
        return id;
    }
    /**
     * 設(shè)置下載文件的
     */
    public void setId(int id) {
        this.id = id;
    }
    /**
     * 獲取下載文件的長(zhǎng)度
     */
    public int getLength() {
        return length;
    }
    /**
     * 存入下載文件的
     */
    public void setLength(int length) {
        this.length = length;
    }
    /**
     * 獲取下載文件的URL路徑
     */
    public String getUrl() {
        return url;
    }
    /**
     * 存儲(chǔ)下載文件的URL路徑
     */
    public void setUrl(String url) {
        this.url = url;
    }
    /**
     * 獲取下載文件的文件名
     */
    public String getFileName() {
        return fileName;
    }
    /**
     * 存儲(chǔ)下載文件的文件名
     */
    public void setFileName(String fileName) {
        this.fileName = fileName;
    }
    /**
     * 獲取下載文件的下載進(jìn)度
     */
    public double getFinished() {
        return finished;
    }
    /**
     * 存儲(chǔ)下載文件的存儲(chǔ)進(jìn)度
     */
    public void setFinished(double finished) {
        this.finished = finished;
    }
    /**
     * 獲取文件完成下載標(biāo)識(shí)
     */
    public String getOver() {
        return over;
    }
    /**
     * 存儲(chǔ)文件完成下載標(biāo)識(shí)
     */
    public void setOver(String over) {
        this.over = over;
    }
    /**
     * 獲取下載文件的所有屬性信息
     */
    @Override
    public String toString() {
        return "FileInfo [id=" + id + ", length=" + length + ", url=" + url
                + ", fileName=" + fileName + ", finished="
                + finished +", rate=" + rate +", md5=" + md5 + "]";
    }
}

ThreadInfo

/**
 *文件下載線程的信息 
 **/
public class ThreadInfo {
    /**
     * 文件下載線程的id標(biāo)識(shí)
     */
    private int id ;
    /**
     * 下載文件的URL路徑
     */
    private String url ;
    /**
     * 文件下載的起始字節(jié)位置
     */
    private int start ;
    /**
     * 文件下載的結(jié)束字節(jié)位置
     */
    private int end ;
    /**
     * 文件已下載的字節(jié)長(zhǎng)度
     */
    private int finished ;
    /**
     * 下載文件的 MD5值
     */
    private String  md5 ;
    /**
     * 下載文件是否完成下載
     */
    private String  over ;
    /**
     * 下載文件完成下載的時(shí)間
     */
    private String  overtime ;
    public ThreadInfo() {
    }
    /**
     * 存儲(chǔ)文件下載線程的屬性信息
     * @param id    文件下載線程id標(biāo)識(shí)
     * @param url   下載文件路徑
     * @param start 文件下載起始字節(jié)位置
     * @param end   文件下載終止字節(jié)位置
     * @param finished 文件已下載的字節(jié)長(zhǎng)度
     * @param md5            文件的md5值
     * @param over     文件是否完成下載
     * @param overtime 文件完成下載的時(shí)間
     */
    public ThreadInfo(int id, String url, int start, int end, int finished, String md5, String over,String  overtime) {
        this.id = id;
        this.url = url;
        this.start = start;
        this.end = end;
        this.finished = finished;
        this.md5 = md5;
        this.over = over;
        this.overtime = overtime;
    }
    /**
     * 獲取文件完成下載的時(shí)間
     */
    public String getOvertime() {
        return overtime;
    }
    /**
     * 設(shè)置文件完成下載的時(shí)間
     */
    public void setOver_time(String overtime) {
        this.overtime = overtime;
    }
    /**
     * 獲取文件下載線程的id標(biāo)識(shí)
     */
    public int getId() {
        return id;
    }
    /**
     * 設(shè)置文件下載線程的id標(biāo)識(shí)
     */
    public void setId(int id) {
        this.id = id;
    }
    /**
     * 獲取文件下載線程的url
     */
    public String getUrl() {
        return url;
    }
    /**
     * 設(shè)置下載文件的url
     */
    public void setUrl(String url) {
        this.url = url;
    }
    /**
     * 獲取文件下載線程的起始字節(jié)位置
     */
    public int getStart() {
        return start;
    }
    /**
     * 設(shè)置下載文件的起始字節(jié)位置
     */
    public void setStart(int start) {
        this.start = start;
    }
    /**
     * 獲取文件下載線程的結(jié)束字節(jié)位置
     */
    public int getEnd() {
        return end;
    }
    /**
     * 設(shè)置下載文件的結(jié)束字節(jié)位置
     */
    public void setEnd(int end) {
        this.end = end;
    }
    /**
     * 獲取文件下載線程已下載的字節(jié)長(zhǎng)度
     */
    public int getFinished() {
        return finished;
    }
    /**
     * 設(shè)置文件下載線程已下載的字節(jié)長(zhǎng)度
     */
    public void setFinished(int finished) {
        this.finished = finished;
    }
    /**
     * 獲取文件MD5
     */
    public String getMd5() {
        return md5;
    }
    /**
     * 存儲(chǔ)文件MD5
     */
    public void setMd5(String md5) {
        this.md5 = md5;
    }
    /**
     * 獲取文件完成下載標(biāo)識(shí)
     */
    public String getOver() {
        return over;
    }
    /**
     * 存儲(chǔ)文件完成下載標(biāo)識(shí)
     */
    public void setOver(String over) {
        this.over = over;
    }
    /**
     * 獲取文件下載線程的所有屬性信息
     */
    @Override
    public String toString() {
        return "ThreadInfo [id=" + id + ", url=" + url + ", start=" + start
                + ", end =" + end + ", finished=" + finished +", md5=" + md5
                + ", over=" + over +"]";
    }
}

DBHelper

/**
 * 數(shù)據(jù)庫(kù)幫助類
 * 多線程操作數(shù)據(jù)庫(kù)時(shí)注意使用【單例模式】
 * 1茵典、構(gòu)造方法定為private
 * 2湘换、定義該類的一個(gè)靜態(tài)對(duì)象用以應(yīng)用
 * 3、通過(guò)getInstance()方法返回該類對(duì)象统阿,
 *    使該方法無(wú)論調(diào)用多少次彩倚,該類都是唯一。
 */
public class DBHelper extends SQLiteOpenHelper{
    private AtomicInteger mOpenCounter = new AtomicInteger();
    private SQLiteDatabase mDatabase;
    /**
     * 數(shù)據(jù)庫(kù)名稱
     */
    private static final String DB_NAME = "download.db" ;
    /**
     * 數(shù)據(jù)庫(kù)名稱
     */
    public static final String TABLE_NAME = "thread_info" ;
    /**
     * 文件下載線程id
     */
    public static final String THREAD_ID = "thread_id" ;
    /**
     * 下載文件的url文件
     */
    public static final String URL = "url" ;
    /**
     * 文件下載的起始位置(字節(jié)長(zhǎng)度)
     */
    public static final String START = "start" ;
    /**
     * 文件下載的結(jié)束位置(字節(jié)長(zhǎng)度)
     */
    public static final String END = "end" ;
    /**
     * 文件已下載的字節(jié)長(zhǎng)度
     */
    public static final String FINISHED = "finished" ;
    /**
     * 文件的md5
     */
    public static final String MD5 = "md5" ;
    /**
     * 文件是否完成下載標(biāo)識(shí)
     */
    public static final String OVER = "over" ;
    /**
     * 文件是成下載時(shí)間
     */
    public static final String OVER_TIME = "over_time" ;
    /**
     * 數(shù)據(jù)庫(kù)幫助類的靜態(tài)對(duì)象引用
     */
    private static DBHelper sHelper = null ;
    /**
     * 數(shù)據(jù)庫(kù)版本
     */
    private static final int VERSION = 1;
    /**
     * sql創(chuàng)建保存線程信息表命令句
     */
    private static final String SQL_CREATE =
            "create table " +TABLE_NAME +" (_id integer primary key autoincrement,"
                    + THREAD_ID +" integer, "
                    + URL +" text, "
                    + START +" integer, "
                    + END +" integer,"
                    + FINISHED +" integer,"
                    + MD5 +" text, "
                    + OVER +" text, "
                    +OVER_TIME+ " text )";
    /**
     * 刪除表命令句
     */
    private static final String SQL_DROP = "drop table if exists "+TABLE_NAME;
    private DBHelper(Context context) {         // 將public改為private扶平,
        super(context, DB_NAME, null, VERSION); // 防止在其它地方被new出來(lái)帆离,保證db的單例,防止數(shù)據(jù)庫(kù)被鎖定
    }
    /**
     * 獲得類對(duì)象sHelper
     */
    public static DBHelper getInstance(Context context){   // 單例模式蜻直,DBHelper只會(huì)被實(shí)例化一次
        if (sHelper == null ) {                            // 靜態(tài)方法訪問(wèn)數(shù)據(jù)庫(kù)盯质,無(wú)論創(chuàng)建多少個(gè)數(shù)據(jù)庫(kù)訪問(wèn)對(duì)象,
            sHelper = new DBHelper(context);               // 里面的Helper只有一個(gè)概而,保證程序中只有一個(gè)DBHelper對(duì)數(shù)據(jù)庫(kù)進(jìn)行訪問(wèn)
        }
        return sHelper ;
    }
    public synchronized SQLiteDatabase openDatabase() {
        if(mOpenCounter.incrementAndGet() == 1) {
            // Opening new database
            mDatabase = sHelper.getWritableDatabase();
        }
        return mDatabase;
    }
    public synchronized void closeDatabase() {
        if(mOpenCounter.decrementAndGet() == 0) {
            // Closing database
            mDatabase.close();
        }
    }
    /**
     * 創(chuàng)建表
     */
    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(SQL_CREATE);  //創(chuàng)建表
    }
    /**
     * 更新數(shù)據(jù)庫(kù)表
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL(SQL_DROP);
        db.execSQL(SQL_CREATE);
    }
}

ThreadDAO

/**
 * 【數(shù)據(jù)訪問(wèn)接口】
 * DAO:data access object 數(shù)據(jù)訪問(wèn)對(duì)象呼巷,DAO一定是和數(shù)據(jù)庫(kù)的每張表一一對(duì)應(yīng);
 * 是一種應(yīng)用程序表承接口(api),可以訪問(wèn)其它的結(jié)構(gòu)化查詢語(yǔ)言(SQL)數(shù)據(jù)庫(kù);
 * J2EE用DAO設(shè)計(jì)模塊赎瑰,把底層數(shù)據(jù)訪問(wèn)和高層的商務(wù)邏輯分開(kāi)王悍;典型的DAO有以下幾個(gè)組件:
 * 1、一個(gè)DAO工廠類
 * 2餐曼、一個(gè)DAO接口
 * 3压储、一個(gè)實(shí)現(xiàn)DAO接口的具體類
 * 4鲜漩、數(shù)據(jù)傳遞對(duì)象
 */
public interface ThreadDAO {
    /**
     * 插入文件下載信息
     * @param threadInfo  下載線程的信息
     */
    public void insertThread(ThreadInfo threadInfo);
    /**
     * 刪除文件下載信息
     */
    public void deleteThread(String url );
    /**
     * 更新下載文件下載進(jìn)度
     * @param url  下載線程的URL
     * @param thread_id  下載線程的id
     * @param finished   下載線程的文件已下載字節(jié)數(shù)
     * @param md5   下載文件的md5
     * @param over   文件下載完成標(biāo)識(shí)
     * @param over_time   文件下載完成時(shí)間
     */
    public void  updateThread(String url, int thread_id, int finished, String md5, String over, String over_time) ;
    /**
     * 查詢文件的文件下載線程信息
     * @param url  下載線程的URL
     * @return  List<ThreadInfo> 線程信息集
     */
    public List<ThreadInfo> getThreads(String url) ;
    /**
     * 獲取所有文件的文件下載信息
     * @return List<ThreadInfo> 包含下載線程信息的list集
     */
    public List<FileInfo> getDBFileInfoList();
    /**
     * 文件下載信息是否已結(jié)束下載
     * @return 當(dāng)存在下載信息返回true;否則返回false
     */
    boolean isDownloadOver(String url);
}

ThreadDAOIImpl

/**
 * 【數(shù)據(jù)庫(kù)訪問(wèn)接口的實(shí)現(xiàn)】
 */
public class ThreadDAOIImpl implements ThreadDAO{
    /**
     * 數(shù)據(jù)庫(kù)幫助類
     */
    private DBHelper mDBHelper = null ; // 將DBHelper用private修飾
    public ThreadDAOIImpl(Context context) {
        mDBHelper = DBHelper.getInstance(context);
    }
    /**
     * 插入文件下載信息】
     * 多線程數(shù)據(jù)庫(kù)的增集惋、刪孕似、改(更新)方法用synchronized修飾,以保證線程安全:
     * 保證同一時(shí)間段不會(huì)有多個(gè)(只有一個(gè))線程對(duì)數(shù)據(jù)庫(kù)進(jìn)行增刪改刮刑,需等待線程執(zhí)
     * 行完后再開(kāi)啟線程執(zhí)行下一個(gè)功能喉祭;而查詢因?yàn)椴挥貌僮鲾?shù)據(jù)庫(kù),不會(huì)導(dǎo)致 數(shù)據(jù)庫(kù)
     * 死鎖  雷绢,所以不用
     * @see com.download.db.ThreadDAO#insertThread(com.download.entities.ThreadInfo)
     */
    @Override
    public synchronized void insertThread(ThreadInfo threadInfo ){
        SQLiteDatabase db = mDBHelper.openDatabase();  // 實(shí)例化數(shù)據(jù)庫(kù)泛烙,設(shè)置為【讀寫(xiě)】模式
        db.execSQL( "insert into " +DBHelper.TABLE_NAME
                        + " ( "+DBHelper.THREAD_ID +","
                        +DBHelper.URL       +","
                        +DBHelper.START     +","
                        +DBHelper.END       +","
                        +DBHelper.FINISHED  +","
                        +DBHelper.MD5       +","
                        +DBHelper.OVER      +","
                        +DBHelper.OVER_TIME +")"
                        + " values(?,?,?,?,?,?,?,?)",
                new Object[] {threadInfo.getId(),      threadInfo.getUrl(),
                        threadInfo.getStart(),   threadInfo.getEnd(),
                        threadInfo.getFinished(),threadInfo.getMd5(),
                        threadInfo.getOver()    ,threadInfo.getOvertime()   } // 插入數(shù)據(jù)
        );
        mDBHelper.closeDatabase();  // 關(guān)閉數(shù)據(jù)庫(kù)
    }
    /**
     * 【刪除文件下載信息】
     */
    @Override
    public synchronized void deleteThread(String url) {
        SQLiteDatabase db = mDBHelper.openDatabase();
        db.execSQL( "delete from thread_info where url = ?",
                new Object[] {url});
        mDBHelper.closeDatabase();
    }
    /**
     * 【更新下載文件下載進(jìn)度】
     */
    @Override
    public synchronized void updateThread(String url, int thread_id, int finished, String md5, String over, String over_time) {
        SQLiteDatabase db = mDBHelper.openDatabase();
        db.execSQL( "update " +DBHelper.TABLE_NAME + " set "
                        +DBHelper.FINISHED+" = ?where " +DBHelper.URL       +" =? and "
                        +DBHelper.THREAD_ID +" =?",
                new Object[] {finished, url, thread_id});
        db.execSQL( "update " +DBHelper.TABLE_NAME + " set "
                        +DBHelper.OVER+" = ?where " +DBHelper.URL       +" =? and "
                        +DBHelper.THREAD_ID +" =?",
                new Object[] {over, url, thread_id});
        db.execSQL("update " + DBHelper.TABLE_NAME + " set "
                        + DBHelper.OVER_TIME + " = ?where " + DBHelper.URL + " =? and "
                        + DBHelper.THREAD_ID + " =?",
                new Object[]{over_time, url, thread_id});
        mDBHelper.closeDatabase();
    }
    /**
     * 獲取文件的文件下載信息
     * @return List<ThreadInfo> 包含下載線程信息的list集
     * @see com.download.db.ThreadDAO#getThreads(java.lang.String)
     */
    @Override
    public List<ThreadInfo> getThreads(String url) {
        SQLiteDatabase db = mDBHelper.getReadableDatabase();  //注意,此處用【只讀】模式
        List<ThreadInfo> list = new ArrayList<ThreadInfo>();
        Cursor cursor = db.rawQuery("select * from " +DBHelper.TABLE_NAME
                + " where " +DBHelper.URL +" =?", new String[]{url} );
        while (cursor.moveToNext()) {
            ThreadInfo threadInfo = new ThreadInfo();
            threadInfo.setId(cursor.getInt(cursor.getColumnIndex(DBHelper.THREAD_ID)));
            threadInfo.setUrl(cursor.getString(cursor.getColumnIndex(DBHelper.URL)));
            threadInfo.setStart(cursor.getInt(cursor.getColumnIndex(DBHelper.START)));
            threadInfo.setEnd(cursor.getInt(cursor.getColumnIndex(DBHelper.END)));
            threadInfo.setFinished(cursor.getInt(cursor.getColumnIndex(DBHelper.FINISHED)));
            threadInfo.setMd5(cursor.getString(cursor.getColumnIndex(DBHelper.MD5)));
            threadInfo.setOver(cursor.getString(cursor.getColumnIndex(DBHelper.OVER)));
            list.add(threadInfo);
        }
        cursor.close();
        db.close();
        return list;
    }
    /**
     * 獲取數(shù)據(jù)庫(kù)中所有下載文件的信息
     * @return List<ThreadInfo> 包含下載線程信息的list集
     */
    @Override
    public List<FileInfo> getDBFileInfoList() {
        URLTools urlTools = new URLTools();
        List<FileInfo> list = new ArrayList<FileInfo>();
        SQLiteDatabase db = mDBHelper.getReadableDatabase();  //注意翘紊,此處用【只讀】模式
        //查詢數(shù)據(jù)庫(kù)文件下載信息
        Cursor cursor = db.rawQuery("select * from " +DBHelper.TABLE_NAME, null );
        while( cursor.moveToNext() ){
            FileInfo fileInfo = new FileInfo();
            if( fileInfo.getId()==0 ){
                long finish = 0;  //文件已下載的字節(jié)數(shù)
                fileInfo.setOver( cursor.getString(cursor.getColumnIndex(DBHelper.OVER)) );
                fileInfo.setOvertime( cursor.getString(cursor.getColumnIndex(DBHelper.OVER_TIME)) );
                fileInfo.setMd5( cursor.getString(cursor.getColumnIndex(DBHelper.MD5)) );
                fileInfo.setUrl( cursor.getString(cursor.getColumnIndex(DBHelper.URL)) );
                String fileName = urlTools.getURLFileName( fileInfo.getUrl(), "/" );
                fileInfo.setFileName( fileName );
                fileInfo.setIsDownload("false");
                //獲取百分比下載進(jìn)度
                for (int i = 0; i < DownloadConfig.DONWNLOAD_THREAD_NUM; i++) {
                    finish += cursor.getInt( cursor.getColumnIndex(DBHelper.FINISHED) );
                    if ( i < DownloadConfig.DONWNLOAD_THREAD_NUM -1 ){
                        cursor.moveToPosition( cursor.getPosition() +1 );
                    }
                }
                long finished = finish*100/( cursor.getInt(cursor.getColumnIndex(DBHelper.END)) );
                fileInfo.setFinished( (int)finished);
                list.add(fileInfo);
            }
        }
        cursor.close();
        db.close();
        return list;
    }
    /**
     * 文件下載是否已完成
     * @return 當(dāng)存在下載信息返回true蔽氨;否則返回false
     */
    @Override
    public boolean isDownloadOver(String url) {
        SQLiteDatabase db = mDBHelper.getWritableDatabase();
        Cursor cursor = db.rawQuery("select * from " +DBHelper.TABLE_NAME
                        +" where " +DBHelper.URL  +" =? and " +DBHelper.THREAD_ID  +" = ?",
                new String[]{url, 1+"" } );
        boolean over = false;
        if (cursor.moveToNext()) {
            String overStr =   cursor.getString(cursor.getColumnIndex(DBHelper.OVER)) ;
            try {
                String name =  URLDecoder.decode(cursor.getString(cursor.getColumnIndex(DBHelper.URL)), "UTF-8")  ;
                if (overStr.equals("true")) {
                    over = true ;
                    Log.i("db", name+"文件是否下載完成:"+ over  );
                }else {
                    over = false ;
                    Log.i("db",name+ "文件是否下載完成:"+ over  );
                }
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
        cursor.close();
        db.close();
        return over;
    }
結(jié)束啦~~
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市帆疟,隨后出現(xiàn)的幾起案子鹉究,更是在濱河造成了極大的恐慌,老刑警劉巖鸯匹,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坊饶,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡殴蓬,警方通過(guò)查閱死者的電腦和手機(jī)匿级,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)染厅,“玉大人痘绎,你說(shuō)我怎么就攤上這事⌒ち福” “怎么了孤页?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)涩馆。 經(jīng)常有香客問(wèn)我行施,道長(zhǎng),這世上最難降的妖魔是什么魂那? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任蛾号,我火速辦了婚禮,結(jié)果婚禮上涯雅,老公的妹妹穿的比我還像新娘鲜结。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布精刷。 她就那樣靜靜地躺著拗胜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪怒允。 梳的紋絲不亂的頭發(fā)上埂软,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音纫事,去河邊找鬼仰美。 笑死,一個(gè)胖子當(dāng)著我的面吹牛儿礼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播庆寺,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蚊夫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼懦尝!你這毒婦竟也來(lái)了知纷?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤陵霉,失蹤者是張志新(化名)和其女友劉穎琅轧,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體踊挠,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡乍桂,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了效床。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片睹酌。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖剩檀,靈堂內(nèi)的尸體忽然破棺而出憋沿,到底是詐尸還是另有隱情,我是刑警寧澤沪猴,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布辐啄,位于F島的核電站,受9級(jí)特大地震影響运嗜,放射性物質(zhì)發(fā)生泄漏壶辜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一洗出、第九天 我趴在偏房一處隱蔽的房頂上張望士复。 院中可真熱鬧,春花似錦、人聲如沸阱洪。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)冗荸。三九已至承璃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蚌本,已是汗流浹背盔粹。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留程癌,地道東北人舷嗡。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像嵌莉,于是被迫代替她去往敵國(guó)和親进萄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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