更好的Android多線程下載框架

/**
 * 作者:Pich
 * 原文鏈接:http://me.woblog.cn/
 * QQ群:129961195
 * Github:https://github.com/lifengsofts
 */

概述

為什么是更好的Android多線程下載框架呢,原因你懂的嵌赠,廣告法嘛词渤!

本篇我們我們就來聊聊多線程下載框架,先聊聊我們框架的特點:

  1. 多線程
  2. 多任務(wù)
  3. 斷點續(xù)傳
  4. 支持大文件
  5. 可以自定義下載數(shù)據(jù)庫
  6. 高度可配置剪芍,像超時時間這類
  7. 業(yè)務(wù)數(shù)據(jù)和下載數(shù)據(jù)分離

下面我們在說下該框架能實現(xiàn)那些的應(yīng)用場景:

  1. 該框架可以很方便的下載單個文件,并且顯示各種狀態(tài),包括開始下載,下載中伦籍,下載失敗蓝晒,刪除等狀態(tài)。
  2. 也可以實現(xiàn)常見的需要下載功能應(yīng)用帖鸦,比如:某某手機助手芝薇,在該應(yīng)用內(nèi)可以說是下載是核心功能,所以對框架的穩(wěn)定性作儿,代碼可靠性洛二,框架擴展性依賴很大,所以該框架真是從這種出發(fā)點而生的。通常這類應(yīng)用的表示形式分三個頁面需要用到下載功能晾嘶,一個列表用來顯示來自業(yè)務(wù)數(shù)據(jù)的列表妓雾,在該列表右邊可以點擊單個條目,或者多選實現(xiàn)下載变擒,點擊每個條目進入詳情君珠,同時還有個一個下載管理寝志,包括大概兩個界面娇斑,正在下載,下載完成的材部,在這幾個界面都需要一個核心的功能就是都可以暫停毫缆,恢復(fù),刪除并且能顯示下載進度乐导。在列表一個最重要的問題就是界面刷新苦丁,如果每次更新都刷新整個列表,那么這將是異常災(zāi)難物臂,而我們這個框架正好解決了該問題旺拉,采用了回調(diào)單個條目并更新該條目的進度和狀態(tài)。

該項目狀態(tài)

該項目的雛形始于14年的公司項目需要用到多線程下載棵磷,但當(dāng)時實現(xiàn)的單線程多任務(wù)斷點續(xù)傳蛾狗,后面不斷完善,在這之間遇到過很多坑仪媒,也對一個下載框架有了更深的認識沉桌,所以在16年又重寫了該框架。

項目的Github地址:https://github.com/lifengsofts/AndroidDownloader

項目的官網(wǎng)地址:http://i.woblog.cn/AndroidDownloader

項目還處于發(fā)展?fàn)顟B(tài)算吩,但已經(jīng)趨于穩(wěn)定留凭,并且有一定的編碼規(guī)范,同時采用了多個開源項目的質(zhì)量控制方案以保證每次代碼提交的可靠性偎巢。

下面上幾張框架Demo的截圖蔼夜,這樣用戶在心中有一個自己的概念,但是推薦各位還是講Demo下載到本地親自压昼,運行一下求冷。

截圖

AndroidDownloader Sample Screenshots
AndroidDownloader Sample Screenshots

AndroidDownloader Sample Screenshots
AndroidDownloader Sample Screenshots
AndroidDownloader Sample Screenshots
AndroidDownloader Sample Screenshots

AndroidDownloader Sample Screenshots
AndroidDownloader Sample Screenshots

第一個界面是單獨下載一個文件。

第二個界面是應(yīng)用中最常用的一個界面巢音,該界面來自業(yè)務(wù)數(shù)據(jù)遵倦。

第三個頁面是離線管理中的下載中的界面。

第四個頁面是離線管理中的下載完成的界面官撼。

可以看到他們在每個界面都能暫停下載梧躺,繼續(xù)下載,以及刪除,并且都能拿到進度掠哥,狀態(tài)等信息巩踏。

下面就來看看這么強大的下載框架那該如何來使用呢?

添加權(quán)限

我相信這一步任何一個項目都已經(jīng)添加了续搀,但是還是不得不提一下塞琼。

該框架需要網(wǎng)絡(luò)訪問權(quán)限,如果你是講文件下載到存儲卡禁舷,那相應(yīng)的需要添加存儲卡訪問權(quán)限彪杉。

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

配置Service

因為該框架采用才service中下載一個文件的。這樣做的目的是下載任務(wù)一般都需要在后頭下載牵咙,如果在Activity中來做這類任務(wù)派近,我想任何一個新手都知道這樣不行。

<service android:name="cn.woblog.android.downloader.DownloadService">
  <intent-filter>
    <action android:name="cn.woblog.android.downloader.DOWNLOAD_SERVICE" />
  </intent-filter>
</service>

添加依賴

我們提供了多種集成方式洁桌,比如:gradle,maven,jar渴丸。選擇適合你自己的就行了。

Gradle

在module目錄下面的build.gradle文件中添加如下內(nèi)容:

compile 'cn.woblog.android:downloader:1.0.1'

Maven

或者你使用的Maven依賴管理工具另凌。那道理其實是一樣的谱轨,在pom文件中添加:

<dependency>
  <groupId>cn.woblog.android</groupId>
  <artifactId>downloader</artifactId>
  <version>1.0.0</version>
</dependency>

或者你也可以參考該鏈接使用Snapshots版本

混淆配置

如果你的項目使用了混淆規(guī)則吠谢,那么一定要加上土童。

-keep public class * implements cn.woblog.android.downloader.db.DownloadDBController
-keep class cn.woblog.android.downloader.domain.** { *; }

創(chuàng)建下載管理器

現(xiàn)在萬事俱備只欠東風(fēng)了,接下來只需要創(chuàng)建一個下載管理器囊卜,該框架所有的操作都是通過該來實現(xiàn)的:

downloadManager = DownloadService.getDownloadManager(context.getApplicationContext());

或者你可以使用更詳細的來配置該框架:

Config config = new Config();
//set database path.
//    config.setDatabaseName("/sdcard/a/d.db");
//      config.setDownloadDBController(dbController);

//set download quantity at the same time.
config.setDownloadThread(3);

//set each download info thread number
config.setEachDownloadThread(2);

// set connect timeout,unit millisecond
config.setConnectTimeout(10000);

// set read data timeout,unit millisecond
config.setReadTimeout(10000);
downloadManager = DownloadService.getDownloadManager(this.getApplicationContext(), config);

下載一個文件

//create download info set download uri and save path.
final DownloadInfo downloadInfo = new DownloadInfo.Builder().setUrl("http://example.com/a.apk")
    .setPath("/sdcard/a.apk")
    .build();

//set download callback.
downloadInfo.setDownloadListener(new DownloadListener() {

  @Override
  public void onStart() {
    tv_download_info.setText("Prepare downloading");
  }

  @Override
  public void onWaited() {
    tv_download_info.setText("Waiting");
    bt_download_button.setText("Pause");
  }

  @Override
  public void onPaused() {
    bt_download_button.setText("Continue");
    tv_download_info.setText("Paused");
  }

  @Override
  public void onDownloading(long progress, long size) {
    tv_download_info
        .setText(FileUtil.formatFileSize(progress) + "/" + FileUtil
            .formatFileSize(size));
    bt_download_button.setText("Pause");
  }

  @Override
  public void onRemoved() {
    bt_download_button.setText("Download");
    tv_download_info.setText("");
    downloadInfo = null;
  }

  @Override
  public void onDownloadSuccess() {
    bt_download_button.setText("Delete");
    tv_download_info.setText("Download success");
  }

  @Override
  public void onDownloadFailed(DownloadException e) {
    e.printStackTrace();
    tv_download_info.setText("Download fail:" + e.getMessage());
  }
});

//submit download info to download manager.
downloadManager.download(downloadInfo);

下載一個文件時直接創(chuàng)建一個DownloadInfo娜扇,然后設(shè)置下載鏈接和下載路徑。再添加一個監(jiān)聽栅组。就可以提交到下載框架了雀瓢。

通過下載監(jiān)聽器我們可以獲取到很多狀態(tài)。開始下載玉掸,等待中刃麸,暫停完成,下載中司浪,刪除成功泊业,下載成功,下載失敗等狀態(tài)啊易。

在列表控件使用

我們這里演示如何在RecyclerView這類列表控件使用吁伺。當(dāng)然如果你用的是ListView那道理是一樣的。

class ViewHolder extends RecyclerView.ViewHolder {

  private final ImageView iv_icon;
  private final TextView tv_size;
  private final TextView tv_status;
  private final ProgressBar pb;
  private final TextView tv_name;
  private final Button bt_action;
  private DownloadInfo downloadInfo;

  public ViewHolder(View view) {
    super(view);

    iv_icon = (ImageView) view.findViewById(R.id.iv_icon);
    tv_size = (TextView) view.findViewById(R.id.tv_size);
    tv_status = (TextView) view.findViewById(R.id.tv_status);
    pb = (ProgressBar) view.findViewById(R.id.pb);
    tv_name = (TextView) view.findViewById(R.id.tv_name);
    bt_action = (Button) view.findViewById(R.id.bt_action);
  }

  @SuppressWarnings("unchecked")
  public void bindData(final MyDownloadInfo data, int position, final Context context) {
    Glide.with(context).load(data.getIcon()).into(iv_icon);
    tv_name.setText(data.getName());

    // Get download task status
    downloadInfo = downloadManager.getDownloadById(data.getUrl().hashCode());

    // Set a download listener
    if (downloadInfo != null) {
      downloadInfo
          .setDownloadListener(new MyDownloadListener(new SoftReference(ViewHolder.this)) {
            //  Call interval about one second
            @Override
            public void onRefresh() {
              if (getUserTag() != null && getUserTag().get() != null) {
                ViewHolder viewHolder = (ViewHolder) getUserTag().get();
                viewHolder.refresh();
              }
            }
          });

    }

    refresh();

//      Download button
    bt_action.setOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View v) {
        if (downloadInfo != null) {

          switch (downloadInfo.getStatus()) {
            case DownloadInfo.STATUS_NONE:
            case DownloadInfo.STATUS_PAUSED:
            case DownloadInfo.STATUS_ERROR:

              //resume downloadInfo
              downloadManager.resume(downloadInfo);
              break;

            case DownloadInfo.STATUS_DOWNLOADING:
            case DownloadInfo.STATUS_PREPARE_DOWNLOAD:
            case STATUS_WAIT:
              //pause downloadInfo
              downloadManager.pause(downloadInfo);
              break;
            case DownloadInfo.STATUS_COMPLETED:
              downloadManager.remove(downloadInfo);
              break;
          }
        } else {
//            Create new download task
          File d = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "d");
          if (!d.exists()) {
            d.mkdirs();
          }
          String path = d.getAbsolutePath().concat("/").concat(data.getName());
          downloadInfo = new Builder().setUrl(data.getUrl())
              .setPath(path)
              .build();
          downloadInfo
              .setDownloadListener(new MyDownloadListener(new SoftReference(ViewHolder.this)) {

                @Override
                public void onRefresh() {
                  if (getUserTag() != null && getUserTag().get() != null) {
                    ViewHolder viewHolder = (ViewHolder) getUserTag().get();
                    viewHolder.refresh();
                  }
                }
              });
          downloadManager.download(downloadInfo);
        }
      }
    });

  }

  private void refresh() {
    if (downloadInfo == null) {
      tv_size.setText("");
      pb.setProgress(0);
      bt_action.setText("Download");
      tv_status.setText("not downloadInfo");
    } else {
      switch (downloadInfo.getStatus()) {
        case DownloadInfo.STATUS_NONE:
          bt_action.setText("Download");
          tv_status.setText("not downloadInfo");
          break;
        case DownloadInfo.STATUS_PAUSED:
        case DownloadInfo.STATUS_ERROR:
          bt_action.setText("Continue");
          tv_status.setText("paused");
          try {
            pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
          } catch (Exception e) {
            e.printStackTrace();
          }
          tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
              .formatFileSize(downloadInfo.getSize()));
          break;

        case DownloadInfo.STATUS_DOWNLOADING:
        case DownloadInfo.STATUS_PREPARE_DOWNLOAD:
          bt_action.setText("Pause");
          try {
            pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
          } catch (Exception e) {
            e.printStackTrace();
          }
          tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
              .formatFileSize(downloadInfo.getSize()));
          tv_status.setText("downloading");
          break;
        case STATUS_COMPLETED:
          bt_action.setText("Delete");
          try {
            pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
          } catch (Exception e) {
            e.printStackTrace();
          }
          tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
              .formatFileSize(downloadInfo.getSize()));
          tv_status.setText("success");
          break;
        case STATUS_REMOVED:
          tv_size.setText("");
          pb.setProgress(0);
          bt_action.setText("Download");
          tv_status.setText("not downloadInfo");
        case STATUS_WAIT:
          tv_size.setText("");
          pb.setProgress(0);
          bt_action.setText("Pause");
          tv_status.setText("Waiting");
          break;
      }

    }
  }
}

關(guān)鍵代碼就是bindData方法中先通過業(yè)務(wù)的id租谈,我們這里使用的url來獲取該業(yè)務(wù)數(shù)據(jù)是否有對應(yīng)的下載任務(wù)篮奄。如果有,則從新綁定監(jiān)聽器,也就是這段代碼

downloadInfo
  .setDownloadListener(new MyDownloadListener(new SoftReference(ViewHolder.this)) {
    //  Call interval about one second
    @Override
    public void onRefresh() {
      if (getUserTag() != null && getUserTag().get() != null) {
        ViewHolder viewHolder = (ViewHolder) getUserTag().get();
        viewHolder.refresh();
      }
    }
  });

其中要注意到的是緩存每個條目我們使用了SoftReference窟却,這樣做的目的內(nèi)容在吃緊的情況下而已及時的是否這些條目昼丑。

接下來又一個重要的點是,設(shè)置按鈕的點擊事件夸赫,通常在這樣的列表中有一個或多個按鈕控制下載狀態(tài)菩帝。

if (downloadInfo != null) {

  switch (downloadInfo.getStatus()) {
    case DownloadInfo.STATUS_NONE:
    case DownloadInfo.STATUS_PAUSED:
    case DownloadInfo.STATUS_ERROR:

      //resume downloadInfo
      downloadManager.resume(downloadInfo);
      break;

    case DownloadInfo.STATUS_DOWNLOADING:
    case DownloadInfo.STATUS_PREPARE_DOWNLOAD:
    case STATUS_WAIT:
      //pause downloadInfo
      downloadManager.pause(downloadInfo);
      break;
    case DownloadInfo.STATUS_COMPLETED:
      downloadManager.remove(downloadInfo);
      break;
  }
} else {
//            Create new download task
  File d = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "d");
  if (!d.exists()) {
    d.mkdirs();
  }
  String path = d.getAbsolutePath().concat("/").concat(data.getName());
  downloadInfo = new Builder().setUrl(data.getUrl())
      .setPath(path)
      .build();
  downloadInfo
      .setDownloadListener(new MyDownloadListener(new SoftReference(ViewHolder.this)) {

        @Override
        public void onRefresh() {
          if (getUserTag() != null && getUserTag().get() != null) {
            ViewHolder viewHolder = (ViewHolder) getUserTag().get();
            viewHolder.refresh();
          }
        }
      });
  downloadManager.download(downloadInfo);
}

關(guān)鍵點就是如果沒有下載任務(wù)就創(chuàng)建一個下載任務(wù),如果已有下載任務(wù)就根據(jù)任務(wù)現(xiàn)在的狀態(tài)執(zhí)行相應(yīng)的操作茬腿,比如當(dāng)前是沒有下載呼奢,點擊就是創(chuàng)建一個下載任務(wù)。

接下還有一個重點就是滓彰,我們在回調(diào)監(jiān)聽中調(diào)用了refresh方法控妻,在該方法中根據(jù)狀態(tài)顯示進度和相應(yīng)的操作按鈕州袒。這樣做的好處上面已經(jīng)提到了揭绑。

private void refresh() {
  if (downloadInfo == null) {
    tv_size.setText("");
    pb.setProgress(0);
    bt_action.setText("Download");
    tv_status.setText("not downloadInfo");
  } else {
    switch (downloadInfo.getStatus()) {
      case DownloadInfo.STATUS_NONE:
        bt_action.setText("Download");
        tv_status.setText("not downloadInfo");
        break;
      case DownloadInfo.STATUS_PAUSED:
      case DownloadInfo.STATUS_ERROR:
        bt_action.setText("Continue");
        tv_status.setText("paused");
        try {
          pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
        } catch (Exception e) {
          e.printStackTrace();
        }
        tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
            .formatFileSize(downloadInfo.getSize()));
        break;

      case DownloadInfo.STATUS_DOWNLOADING:
      case DownloadInfo.STATUS_PREPARE_DOWNLOAD:
        bt_action.setText("Pause");
        try {
          pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
        } catch (Exception e) {
          e.printStackTrace();
        }
        tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
            .formatFileSize(downloadInfo.getSize()));
        tv_status.setText("downloading");
        break;
      case STATUS_COMPLETED:
        bt_action.setText("Delete");
        try {
          pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
        } catch (Exception e) {
          e.printStackTrace();
        }
        tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
            .formatFileSize(downloadInfo.getSize()));
        tv_status.setText("success");
        break;
      case STATUS_REMOVED:
        tv_size.setText("");
        pb.setProgress(0);
        bt_action.setText("Download");
        tv_status.setText("not downloadInfo");
      case STATUS_WAIT:
        tv_size.setText("");
        pb.setProgress(0);
        bt_action.setText("Pause");
        tv_status.setText("Waiting");
        break;
    }

  }
}

到這里改下框架的核心使用方法就介紹完了。

支持

如有任何問題可以在加我們的QQ群或者在Github上提Issue郎哭,另外請?zhí)酙ssue或者的PR的一定要看下項目的貢獻代碼的方法以及一要求他匪,因為如果要保證一個開源項目的質(zhì)量就必須在各方面都規(guī)范化。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末夸研,一起剝皮案震驚了整個濱河市邦蜜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌亥至,老刑警劉巖悼沈,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異姐扮,居然都是意外死亡絮供,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門茶敏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來壤靶,“玉大人,你說我怎么就攤上這事惊搏≈椋” “怎么了?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵恬惯,是天一觀的道長向拆。 經(jīng)常有香客問我,道長酪耳,這世上最難降的妖魔是什么浓恳? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上奖蔓,老公的妹妹穿的比我還像新娘赞草。我一直安慰自己,他們只是感情好吆鹤,可當(dāng)我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布厨疙。 她就那樣靜靜地躺著,像睡著了一般疑务。 火紅的嫁衣襯著肌膚如雪沾凄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天知允,我揣著相機與錄音撒蟀,去河邊找鬼。 笑死温鸽,一個胖子當(dāng)著我的面吹牛保屯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播涤垫,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼姑尺,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蝠猬?” 一聲冷哼從身側(cè)響起切蟋,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎榆芦,沒想到半個月后柄粹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡匆绣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年驻右,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片犬绒。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡旺入,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出凯力,到底是詐尸還是另有隱情茵瘾,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布咐鹤,位于F島的核電站拗秘,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏祈惶。R本人自食惡果不足惜雕旨,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一扮匠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧凡涩,春花似錦棒搜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至育韩,卻和暖如春克蚂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背筋讨。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工埃叭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人悉罕。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓赤屋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蛮粮。 傳聞我的和親對象是個殘疾皇子益缎,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,452評論 2 348

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,737評論 25 707
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)然想,斷路器,智...
    卡卡羅2017閱讀 134,628評論 18 139
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫欣范、插件变泄、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,059評論 4 62
  • 猶豫了很久,還是選擇了這個題目恼琼,可能是年齡大了妨蛹,近來越發(fā)的喜歡懷舊,父親去世距今已經(jīng)十二年了晴竞,可每當(dāng)想起往事蛙卤,...
    素顏hb閱讀 212評論 0 0
  • “披星戴月地奔波,只為一扇窗噩死,當(dāng)你迷失在路上颤难,能夠看見那燈光,不知不覺把他鄉(xiāng)已维,當(dāng)做了故鄉(xiāng)行嗤,只是偶爾難過時,不經(jīng)意遙...
    小賴E華姐姐閱讀 146評論 0 1