基于HttpURLConnection+多線程的安卓多任務(wù)下載庫

安卓多任務(wù)庫下載使用了HttpURLConnection進行網(wǎng)絡(luò)傳輸根资,使用了多線程進行多任務(wù)管理⊥剑可以實時更新下載進度玄帕,速度,下載狀態(tài)想邦,剩余時間預(yù)計裤纹。

1.先定義下載數(shù)據(jù)以及狀態(tài)的model(DownloadInfoModel)

public class DownloadInfoModel {
    public static final int DOWNLOADING_STATUS = 0;//下載狀態(tài)
    public static final int WAIT_STATUS = 1;//等待狀態(tài)
    public static final int DOWNLOAD_FAILD_STATUS = 2;//下載失敗狀態(tài)
    public static final int DOWNLOAD_SUCCESS_STATUS = 3;//下載成功狀態(tài)
    public static final int DOWNLOAD_REMOVE_STATUS = 4;//刪除該條下載
    private String downloadId = "";//下載任務(wù)的id
    private String remoteUrl = "";//遠程下載地址
    private String savePath = "";//保存的路徑文件夾
    private String fileName = "";//保存的文件名(不包括擴展名)
    private String fileExtname = "";//保存的文件擴展名
    private DownloadCallback downloadCallback;

    private int percentage = 0;//下載進度0-100
    private long speed = 0;//下載速度byte/s
    private long lestTime = 0;//剩余時間
    private long len = 0;//
    private long fileLength = 0;//文件大小
    private int status = WAIT_STATUS;//下載狀態(tài)

    public int getStatus() {
        return status;
    }

    public DownloadInfoModel setStatus(int status) {
        this.status = status;
        return this;
    }

    public String getDownloadId() {
        return downloadId;
    }

    public DownloadInfoModel setDownloadId(String downloadId) {
        this.downloadId = downloadId;
        return this;
    }

    public String getRemoteUrl() {
        return remoteUrl;
    }

    public DownloadInfoModel setRemoteUrl(String remoteUrl) {
        this.remoteUrl = remoteUrl;
        return this;
    }

    public String getSavePath() {
        return savePath;
    }

    public DownloadInfoModel setSavePath(String savePath) {
        this.savePath = savePath;
        return this;
    }

    public String getFileName() {
        return fileName;
    }

    public DownloadInfoModel setFileName(String fileName) {
        this.fileName = fileName;
        return this;
    }

    public String getFileExtname() {
        return fileExtname;
    }

    public DownloadInfoModel setFileExtname(String fileExtname) {
        this.fileExtname = fileExtname;
        return this;
    }

    public DownloadCallback getDownloadCallback() {
        return downloadCallback;
    }

    public DownloadInfoModel setDownloadCallback(DownloadCallback downloadCallback) {
        this.downloadCallback = downloadCallback;
        return this;
    }

    public int getPercentage() {
        return percentage;
    }

    public DownloadInfoModel setPercentage(int percentage) {
        this.percentage = percentage;
        return this;
    }

    public long getSpeed() {
        return speed;
    }

    public DownloadInfoModel setSpeed(long speed) {
        this.speed = speed;
        return this;
    }

    public long getLestTime() {
        return lestTime;
    }

    public DownloadInfoModel setLestTime(long lestTime) {
        this.lestTime = lestTime;
        return this;
    }

    public long getLen() {
        return len;
    }

    public DownloadInfoModel setLen(long len) {
        this.len = len;
        return this;
    }

    public long getFileLength() {
        return fileLength;
    }

    public DownloadInfoModel setFileLength(long fileLength) {
        this.fileLength = fileLength;
        return this;
    }

    @Override
    public String toString() {
        return "DownloadInfoModel{" +
                "downloadId='" + downloadId + '\'' +
                ", remoteUrl='" + remoteUrl + '\'' +
                ", savePath='" + savePath + '\'' +
                ", fileName='" + fileName + '\'' +
                ", fileExtname='" + fileExtname + '\'' +
                ", downloadCallback=" + downloadCallback +
                ", percentage=" + percentage +
                ", speed=" + speed +
                ", lestTime=" + lestTime +
                ", len=" + len +
                ", fileLength=" + fileLength +
                ", status=" + status +
                '}';
    }
}

2.定義下載進度回調(diào)的接口(DownloadCallback)

public interface DownloadCallback {
    /**
     * 正在下載中
     * <p>
     * //     * @param len        當(dāng)前文件大小
     * //     * @param fileLength 文件總大小
     * //     * @param remoteUrl  下載路徑
     * //     * @param fileName   文件名
     * //     * @param percentage 下載的百分比(0-100)
     * //     * @param speed      下載速度
     * //     * @param lestTime   剩余時間
     */
    void onDownloading(DownloadInfoModel downloadInfoModel);

    /**
     * 開始下載
     *
     * @param downloadInfoModel
     */
    void onStartDownload(DownloadInfoModel downloadInfoModel);

    /**
     * 下載結(jié)束調(diào)用接口
     *
     * @param downloadInfoModel
     */
    void onFinishDownload(DownloadInfoModel downloadInfoModel);

    /**
     * 下載出現(xiàn)錯誤
     *
     * @param downloadInfoModel
     * @param e
     */
    void onErrorDownload(DownloadInfoModel downloadInfoModel, Exception e);

    /**
     * 停止下載
     *
     * @param downloadInfoModel
     * @param isDelete
     */
    void onStopDownload(DownloadInfoModel downloadInfoModel, boolean isDelete);

    /**
     * 如果已經(jīng)有文件
     *
     * @param downloadInfoModel
     */
    void hasExists(DownloadInfoModel downloadInfoModel);

    /**
     * 刪除單條信息
     *
     * @param model
     */
    void onRemoveDownload(DownloadInfoModel model);
}

3.添加一個時間工具類(DownloadTimeUtils),處理剩余時間的顯示

public class DownloadTimeUtils {
    public static long SECOND_TIME = 1 * 1000;
    public static long MIN_TIME = 60 * 1000;
    public static long HOUR_TIME = 60 * 60 * 1000;
    public static long DAY_TIME = 24 * 60 * 60 * 1000;

    public static String getStringByLongTime(long time) {
        String timeString = "";
        DecimalFormat df = new DecimalFormat("#00");
        if (time != 0) {
            if (time > 0 && time < SECOND_TIME) {
                timeString = "00:00:01";
            } else if (time >= SECOND_TIME && time < MIN_TIME) {
                //一分鐘內(nèi)丧没,大于一秒鐘
                timeString = "00:00:" + df.format(time / SECOND_TIME);
            } else if (time >= MIN_TIME && time < HOUR_TIME) {
                //一小時內(nèi)鹰椒,大于一分鐘
                int min = 0;
                int second = 0;
                min = (int) (time / MIN_TIME);
                second = (int) (time % MIN_TIME) / 1000;
                timeString = "00:" + df.format(min) + ":" + df.format(second);
            } else if (time >= HOUR_TIME && time < DAY_TIME) {
                //一天內(nèi),大于一小時
                int min = 0;
                int hour = 0;
                int second = 0;
                hour = (int) (time / HOUR_TIME);
                min = (int) ((time % HOUR_TIME) / MIN_TIME);
                second = (int) (time % MIN_TIME) / 1000;
                timeString = df.format(hour) + ":" + df.format(min) + ":" + df.format(second);
            } else {
                timeString = "大于一天";
            }
        }
        return timeString;
    }
}

4.編寫一個文件下載的工具類(DownSingleRunnable)呕童,實現(xiàn)Runnable接口漆际,用于加入線程池。

public class DownSingleRunnable implements Runnable {
    private final static String TAG = "DownSingleRunnable";
    public static final int START_MSG = 0;
    public static final int DOWNLOADING_MSG = 1;
    public static final int FINISH_DOWNLOAD_MSG = 2;
    public static final int ERROR_DOWNLOAD_MSG = 3;
    public static final int EXISTS_DOWNLOAD_MSG = 4;
    public static final int STOP_DOWNLOAD_MSG = 5;
    public static final int REMOVE_DOWNLOAD_MSG = 6;
    private DownloadInfoModel model;
    private long countTime = 1000;//定時計算速度
    private DownloadCallback downloadCallback;
    private boolean isDownloading = false;//是否在下載中
    private boolean isStop = false;//是否是暫停結(jié)束的
    private boolean isDelete = false;

    public DownSingleRunnable(DownloadInfoModel model) {
        this.model = model;
        this.downloadCallback = model.getDownloadCallback();
    }

    public DownloadInfoModel getModel() {
        return model;
    }

    public void setModel(DownloadInfoModel model) {
        this.model = model;
    }

    /**
     * 停止下載
     *
     * @param isDelete
     */
    public void stopDownload(boolean isDelete) {

        Log.e(TAG, "stopDownload " + model.getDownloadId());
        isDownloading = false;
        isStop = true;
        this.isDelete = isDelete;
        handler.sendEmptyMessage(STOP_DOWNLOAD_MSG);
    }

    /**
     * 從頭文件得到下載內(nèi)容的大小
     *
     * @param urlConnection
     * @return
     */
    private long getContentLengthFromHeader(URLConnection urlConnection) {
        List values = urlConnection.getHeaderFields().get("content-Length");
        if (values != null && !values.isEmpty()) {

            String sLength = (String) values.get(0);

            if (sLength != null) {
                return Long.parseLong(sLength, 10);
            }
        }
        return -1;
    }

    @Override
    public void run() {
        if (model.getStatus() == DOWNLOAD_REMOVE_STATUS) {
            Log.e(TAG, "已經(jīng)移除了任務(wù) " + model.getDownloadId());
            return;
        }
        Log.e(TAG, "" + model.getDownloadId());
        File file = null;
        BufferedInputStream bin = null;
        OutputStream out = null;
        try {
            //建立連接
            HttpURLConnection httpURLConnection = null;
            URL url = new URL(model.getRemoteUrl());
            httpURLConnection = (HttpURLConnection) url.openConnection();
            httpURLConnection.setRequestProperty("Charset", "UTF-8");// 設(shè)置字符編碼
            httpURLConnection.setRequestProperty("Accept-Encoding", "identity");
            httpURLConnection.setReadTimeout(1000);//斷網(wǎng)操作
            httpURLConnection.connect();

            long fileLength = httpURLConnection.getContentLength();
            if (fileLength == -1) {
                //大文件下載機制
                fileLength = getContentLengthFromHeader(httpURLConnection);
            }
            String filePathUrl = httpURLConnection.getURL().getFile();
            String fileFullName = model.getFileName() + "." + model.getFileExtname();

            bin = new BufferedInputStream(httpURLConnection.getInputStream());
            String path = model.getSavePath() + File.separatorChar + fileFullName;
            file = new File(path);
            //沒文件夾就創(chuàng)建文件夾
            if (!file.getParentFile().exists()) {
                file.getParentFile().mkdirs();
            }
            //如果文件已經(jīng)存在
            if (file.exists()) {
                handler.sendEmptyMessage(EXISTS_DOWNLOAD_MSG);
                model.setFileName(model.getFileName() + "(1)");
                fileFullName = model.getFileName() + "." + model.getFileExtname();
                path = model.getSavePath() + File.separatorChar + fileFullName;
                file = new File(path);
            }
            Log.d(TAG, "filePathUrl = " + filePathUrl + "   fileFullName  = " + fileFullName
                    + "   path = " + path + "   fileLength = " + fileLength);
            out = new FileOutputStream(file);
            long size = 0;
            long len = 0;
            byte[] buf = new byte[1024];
            long lastTime = System.currentTimeMillis();//上一次計算速度的時間
            long lastLen = 0;//上一次的長度夺饲,用來計算速度
            long speed = 0;//速度
            long lestTime = 0;//剩余時間ms
            handler.sendEmptyMessage(START_MSG);
            isDownloading = true;
            while ((size = bin.read(buf)) != -1) {

                if (!isDownloading) {
                    break;
                }
                len += size;
                long nowTime = System.currentTimeMillis();//獲取當(dāng)前時間
                if (lastLen == 0) {
                    lastLen = len;
                }
                //每隔countTime時間以上才進行速度計算
                if ((nowTime - lastTime) >= countTime) {
                    speed = (len - lastLen) * 1000 / (nowTime - lastTime);
                    lestTime = (fileLength - len) * 1000 / speed;
                    lastTime = nowTime;
                    lastLen = len;
                    model.setLen(len).setFileLength(fileLength).setPercentage((int) (len * 100 / fileLength))
                            .setSpeed(speed).setLestTime(lestTime);
                    handler.sendEmptyMessage(DOWNLOADING_MSG);
                }
                out.write(buf, 0, (int) size);
            }
            Log.e(TAG, "finish " + model.getDownloadId());
            //下載完成
            if (!isStop) {
                handler.sendEmptyMessage(FINISH_DOWNLOAD_MSG);
            }
        } catch (Exception e) {
            //斷網(wǎng)會進入當(dāng)前錯誤
            Message message = new Message();
            message.what = ERROR_DOWNLOAD_MSG;
            message.obj = e;
            handler.sendMessage(message);
        } finally {
            Log.e(TAG, "finally ");
            try {
                if (bin != null) {
                    bin.close();
                }
                if (out != null) {
                    out.close();
                }
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }

    private Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case START_MSG:
                    if (null != downloadCallback) {
                        model.setStatus(DownloadInfoModel.DOWNLOADING_STATUS);
                        downloadCallback.onStartDownload(model);
                    }
                    break;
                case DOWNLOADING_MSG:
                    if (null != downloadCallback) {
                        downloadCallback.onDownloading(model);
                    }
                    break;
                case FINISH_DOWNLOAD_MSG:
                    if (null != downloadCallback) {
                        model.setStatus(DownloadInfoModel.DOWNLOAD_SUCCESS_STATUS);
                        downloadCallback.onFinishDownload(model);
                    }
                    break;
                case ERROR_DOWNLOAD_MSG:
                    if (null != downloadCallback) {
                        model.setStatus(DownloadInfoModel.DOWNLOAD_FAILD_STATUS);
                        downloadCallback.onErrorDownload(model, (Exception) msg.obj);
                    }
                    break;
                case EXISTS_DOWNLOAD_MSG:
                    if (null != downloadCallback) {
                        downloadCallback.hasExists(model);
                    }
                    break;
                case STOP_DOWNLOAD_MSG:
                    if (null != downloadCallback) {
                        downloadCallback.onStopDownload(model, isDelete);
                    }
                    break;
                case REMOVE_DOWNLOAD_MSG:
                    if (null != downloadCallback) {
                        model.setStatus(DOWNLOAD_REMOVE_STATUS);
                        downloadCallback.onRemoveDownload(model);

                    }
                    break;
                default:
                    break;
            }
        }
    };

    /**
     * 清除單個列表
     */
    public void removeList() {
        handler.sendEmptyMessage(REMOVE_DOWNLOAD_MSG);
    }
}

5.編寫一個操作工具類(DownloadThreadHelper)奸汇,用來處理多任務(wù)下載施符,內(nèi)部使用的是線程池(newFixedThreadPool),可在外部設(shè)置同時下載數(shù)量擂找,通過改變線程池大小戳吝。

public class DownloadThreadHelper {
    private int maxDownloadNum = 1;//最大同時下載數(shù)
    private ArrayList<DownloadInfoModel> downloadInfoModels;
    private ArrayList<DownSingleRunnable> downSingleRunnables;
    ExecutorService executorService;

    public int getMaxDownloadNum() {
        return maxDownloadNum;
    }

    public DownloadThreadHelper setMaxDownloadNum(int maxDownloadNum) {
        this.maxDownloadNum = maxDownloadNum;
        return this;
    }

    public ArrayList<DownloadInfoModel> getDownloadInfoModels() {
        return downloadInfoModels;
    }

    public DownloadThreadHelper setDownloadInfoModels(ArrayList<DownloadInfoModel> downloadInfoModels) {
        this.downloadInfoModels = downloadInfoModels;
        return this;
    }

    /**
     * 開啟線程池下載文件
     */
    public void startDownloadFiles() {
        executorService = Executors.newFixedThreadPool(maxDownloadNum);

        downSingleRunnables = new ArrayList<>();
        for (DownloadInfoModel model : downloadInfoModels) {
            DownSingleRunnable downSingleRunnable = new DownSingleRunnable(model);
            downSingleRunnables.add(downSingleRunnable);
        }
        for (DownSingleRunnable runnable : downSingleRunnables) {
            executorService.execute(runnable);
        }
        executorService.shutdown();//不讓其他線程再加進來
    }

    /**
     * 停止單個下載
     *
     * @param downloadId
     */
    public void stopDownloadSingleFile(String downloadId) {
        if (isEmpty()) {
            return;
        }
        for (DownSingleRunnable runnable : downSingleRunnables) {
            if (runnable.getModel().getDownloadId().equals(downloadId)) {
                runnable.stopDownload(true);
                break;
            }
        }
    }

    /**
     * 停止全部下載
     */
    public void stopAllFile() {
        if (isEmpty()) {
            return;
        }
        for (DownSingleRunnable runnable : downSingleRunnables) {
            runnable.stopDownload(false);
        }
    }

    /**
     * 刪除在列表,改狀態(tài)
     *
     * @param downloadId
     */
    public void removeList(String downloadId) {
        if (isEmpty()) {
            return;
        }
        for (DownSingleRunnable runnable : downSingleRunnables) {
            if (runnable.getModel().getDownloadId().equals(downloadId)) {
                runnable.removeList();
                break;
            }
        }
    }

    /**
     * 判斷線程池是否為空
     *
     * @return
     */
    private boolean isEmpty() {
        if (null == downSingleRunnables) {
            return true;
        }
        if (downSingleRunnables.size() <= 1) {
            return true;
        }
        return false;
    }
}

上面就是全部的代碼庫封裝贯涎,使用的方式如下:

DownloadThreadHelper downloadThreadHelper = new DownloadThreadHelper();
ArrayList<DownloadInfoModel> downloadInfoModels = new ArrayList<>();
DownloadInfoModel model = new DownloadInfoModel();
//設(shè)置下載的id
model.setDownloadId(downloadId);
//設(shè)置服務(wù)器文件的路徑
model.setRemoteUrl(remoteUrl);
//設(shè)置存儲路徑
model.setSavePath(savePath);
//設(shè)置文件名听哭,不帶擴展名
model.setFileName(fileName);
//設(shè)置文件擴展名
model.setFileExtname(fileExtname);
//設(shè)置監(jiān)聽器
model.setDownloadCallback(new DownloadCallback() {
    @Override
    public void onDownloading(DownloadInfoModel downloadInfoModel) {

    }

    @Override
    public void onStartDownload(DownloadInfoModel downloadInfoModel) {

    }

    @Override
    public void onFinishDownload(DownloadInfoModel downloadInfoModel) {

    }

    @Override
    public void onErrorDownload(DownloadInfoModel downloadInfoModel, Exception e) {

    }

    @Override
    public void onStopDownload(DownloadInfoModel downloadInfoModel, boolean isDelete) {

    }

    @Override
    public void hasExists(DownloadInfoModel downloadInfoModel) {

    }

    @Override
    public void onRemoveDownload(DownloadInfoModel model) {

    }
});
//添加一個下載任務(wù),如果有多個就add多個任務(wù)進去
downloadInfoModels.add(model);
//將全部任務(wù)添加進去
downloadThreadHelper.setDownloadInfoModels(downloadInfoModels);
//開始下載
downloadThreadHelper.startDownloadFiles();
//停止所有下載任務(wù)
downloadThreadHelper.stopAllFile();
//停止指定任務(wù)
downloadThreadHelper.stopDownloadSingleFile(downloadId);

該庫可以實時更新下載進度塘雳,速度欢唾,下載狀態(tài),剩余時間預(yù)計粉捻。暫時沒去弄斷點續(xù)傳礁遣,后續(xù)有時間的話再加上去。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末肩刃,一起剝皮案震驚了整個濱河市祟霍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌盈包,老刑警劉巖沸呐,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異呢燥,居然都是意外死亡崭添,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門叛氨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來呼渣,“玉大人,你說我怎么就攤上這事寞埠∑ㄖ茫” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵仁连,是天一觀的道長蓝角。 經(jīng)常有香客問我,道長饭冬,這世上最難降的妖魔是什么使鹅? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮昌抠,結(jié)果婚禮上患朱,老公的妹妹穿的比我還像新娘。我一直安慰自己扰魂,他們只是感情好麦乞,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布蕴茴。 她就那樣靜靜地躺著,像睡著了一般姐直。 火紅的嫁衣襯著肌膚如雪倦淀。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天声畏,我揣著相機與錄音撞叽,去河邊找鬼。 笑死插龄,一個胖子當(dāng)著我的面吹牛愿棋,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播均牢,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼糠雨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了徘跪?” 一聲冷哼從身側(cè)響起甘邀,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎垮庐,沒想到半個月后松邪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡哨查,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年逗抑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片寒亥。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡邮府,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出护盈,到底是詐尸還是另有隱情挟纱,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布腐宋,位于F島的核電站,受9級特大地震影響檀轨,放射性物質(zhì)發(fā)生泄漏胸竞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一参萄、第九天 我趴在偏房一處隱蔽的房頂上張望卫枝。 院中可真熱鬧,春花似錦讹挎、人聲如沸校赤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽马篮。三九已至沾乘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間浑测,已是汗流浹背翅阵。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留迁央,地道東北人掷匠。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像岖圈,于是被迫代替她去往敵國和親讹语。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

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