流行框架源碼解析(17)-英語(yǔ)流利說(shuō)文件下載器源碼解析

主目錄見(jiàn):Android高級(jí)進(jìn)階知識(shí)(這是總目錄索引)
下載器Github地址:FileDownloader

?文件下載在Android的開發(fā)中應(yīng)該可以說(shuō)是都會(huì)用到,所以一個(gè)完善的好的下載工具應(yīng)該是不可或缺的,第一次看這個(gè)框架還是感覺(jué)類比較多的,應(yīng)該是作者為了類的職責(zé)劃分更加明確吧,但是個(gè)人感覺(jué)適當(dāng)為好。正如介紹所說(shuō),這個(gè)框架是多任務(wù),多線程,支持?jǐn)帱c(diǎn)恢復(fù),高并發(fā),簡(jiǎn)單易用且支持多進(jìn)程的。確實(shí)是個(gè)不錯(cuò)的框架,值得我們來(lái)讀讀代碼,不過(guò)我也只是看過(guò)一下午,如果有理解不到位的大家諒解一下。

一.目標(biāo)

這篇文章應(yīng)該來(lái)說(shuō)也是比較實(shí)用的,而且本庫(kù)的wiki應(yīng)該也有部分的介紹,但是我們今天的主要目標(biāo)如下:
1.學(xué)習(xí)該庫(kù)中優(yōu)秀的思維;
2.懂得自己開發(fā)或者修改和改進(jìn)該庫(kù)。

二.源碼解析

首先當(dāng)你拿到一個(gè)庫(kù)不知道從哪開始的時(shí)候,往往就是從使用上入手,所以我們先來(lái)看看怎么使用該庫(kù),因?yàn)槭褂梅绞接袔追N,我們挑其中一種來(lái)說(shuō)明:
1.添加gradle依賴

dependencies {
    compile 'com.liulishuo.filedownloader:library:1.6.8'
}

2.在Application初始化

  FileDownloader.setupOnApplicationOnCreate(this)
                .connectionCreator(new FileDownloadUrlConnection
                        .Creator(new FileDownloadUrlConnection.Configuration()
                        .connectTimeout(15_000) // set connection timeout.
                        .readTimeout(15_000) // set read timeout.
                        .proxy(Proxy.NO_PROXY) // set proxy
                ))
                .commit();

3.開始下載(這里我挑一個(gè)使用任務(wù)隊(duì)列來(lái)下載的方式)

 final FileDownloadQueueSet queueSet = new FileDownloadQueueSet(downloadListener);

        final List<BaseDownloadTask> tasks = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            tasks.add(FileDownloader.getImpl().create(Constant.URLS[i]).setTag(i + 1));
        }
        queueSet.disableCallbackProgressTimes(); // do not want each task's download progress's callback,
        // we just consider which task will completed.

        // auto retry 1 time if download fail
        queueSet.setAutoRetryTimes(1);

        if (serialRbtn.isChecked()) {
            // start download in serial order
            queueSet.downloadSequentially(tasks);
        } else {
            // start parallel download
            queueSet.downloadTogether(tasks);
        }
        queueSet.start();

那么我們就從初始化入手,我們先來(lái)看看FileDownloader#setupOnApplicationOnCreate()干了什么工作。

1.setupOnApplicationOnCreate

 public static DownloadMgrInitialParams.InitCustomMaker setupOnApplicationOnCreate(Application application) {
        final Context context = application.getApplicationContext();
        //將context緩存在FileDownloadHelper中
        FileDownloadHelper.holdContext(context);

        //實(shí)例化InitCustomMaker
        DownloadMgrInitialParams.InitCustomMaker customMaker = new DownloadMgrInitialParams.InitCustomMaker();
       //賦值給DownloadMgrInitialParams然后保存在CustomComponentHolder中
        CustomComponentHolder.getImpl().setInitCustomMaker(customMaker);

        return customMaker;
    }

我們看到這個(gè)方法沒(méi)有做什么工作,這也是合理的,因?yàn)樵?code>Application#onCreate避免做過(guò)多的工作導(dǎo)致啟動(dòng)速度變慢。這個(gè)方法會(huì)返回一個(gè)InitCustomMaker類的實(shí)例,所以程序會(huì)調(diào)用這個(gè)類connectionCreator()方法:

 public InitCustomMaker connectionCreator(FileDownloadHelper.ConnectionCreator creator) {
            this.mConnectionCreator = creator;
            return this;
        }

這個(gè)方法就是將一個(gè)連接創(chuàng)建器賦值給InitCustomMaker類中的變量,返回this是為了鏈?zhǔn)讲僮餍枰驗(yàn)?code>InitCustomMaker這里還有很多可以自定義的方法。這樣我們的初始化工作就完成了,我們來(lái)看我們的開始下載部分。

2.FileDownloadQueueSet的設(shè)置

這個(gè)類是用來(lái)配置下載任務(wù)和啟動(dòng)下載任務(wù)的,首先我們看到構(gòu)造函數(shù)里面給他設(shè)置了一個(gè)FileDownloadListener實(shí)例用于回調(diào)。然后會(huì)在串行下載或者并行下載的時(shí)候會(huì)給他傳一個(gè)任務(wù)集合,我們首先來(lái)看看任務(wù)集合的創(chuàng)建和添加。任務(wù)的創(chuàng)建是在FileDownloader#create中創(chuàng)建的:

  public BaseDownloadTask create(final String url) {
        return new DownloadTask(url);
    }

通過(guò)這個(gè)我們可以知道,一個(gè)下載任務(wù)對(duì)應(yīng)一個(gè)DownloadTask,我們看下它的構(gòu)造函數(shù):

    DownloadTask(final String url) {
        this.mUrl = url;
        mPauseLock = new Object();
        final DownloadTaskHunter hunter = new DownloadTaskHunter(this, mPauseLock);

        mHunter = hunter;
        mMessageHandler = hunter;
    }

這里除了給DownloadTask的mUrl賦值之外,還實(shí)例化了一個(gè)DownloadTaskHunter實(shí)例,我們看到這里參數(shù)將DownloadTask實(shí)例傳給了它的構(gòu)造函數(shù),然后傳了mPauseLock用來(lái)做線程鎖操作。我們看下DownloadTaskHunter構(gòu)造函數(shù):

 DownloadTaskHunter(ICaptureTask task, Object pauseLock) {
        mPauseLock = pauseLock;
        mTask = task;
        final DownloadSpeedMonitor monitor = new DownloadSpeedMonitor();
        mSpeedMonitor = monitor;
        mSpeedLookup = monitor;
        mMessenger = new FileDownloadMessenger(task.getRunningTask(), this);
    }

可以看到除了傳進(jìn)來(lái)的兩個(gè)參數(shù)賦值之外,還是實(shí)例化了DownloadSpeedMonitor類,這個(gè)類主要是用來(lái)監(jiān)測(cè)下載進(jìn)度的。同時(shí)還實(shí)例化了FileDownloadMessenger類,這個(gè)類是用來(lái)給回調(diào)接口FileDownloadListener發(fā)送消息的。這樣我們大致看了下載任務(wù)DownloadTask的創(chuàng)建過(guò)程。

接下來(lái)就是FileDownloadQueueSet類中一些方法的設(shè)置,例如我們這里使用到的disableCallbackProgressTimes()方法,這個(gè)方法是設(shè)置了的話,那么我們就監(jiān)聽(tīng)不到一個(gè)任務(wù)的下載進(jìn)度了,只有任務(wù)下載完成的回調(diào)。與這個(gè)方法對(duì)應(yīng)的還有setCallbackProgressTimes()設(shè)置更新進(jìn)度回調(diào)次數(shù),setCallbackProgressMinInterval()設(shè)置了更新進(jìn)度的最小間隔時(shí)間。setAutoRetryTimes()方法是設(shè)置下載失敗重試的次數(shù)。然后會(huì)把上面創(chuàng)建的任務(wù)集合賦值給FileDownloadQueueSet類,這里調(diào)用的方法是downloadSequentially()downloadTogether(),這兩個(gè)方法唯一的不同就是會(huì)設(shè)置標(biāo)志位isSerial,來(lái)說(shuō)明是串行下載還是并行下載。最后最重要的當(dāng)然就是start()方法了,這里就真正啟動(dòng)開始下載了。

3.FileDownloadQueueSet start

 public void start() {
        //遍歷任務(wù)集合
        for (BaseDownloadTask task : tasks) {
            //設(shè)置任務(wù)的回調(diào)監(jiān)聽(tīng)為傳進(jìn)來(lái)的FileDownloadListener實(shí)例
            task.setListener(target);

            if (autoRetryTimes != null) {
                //設(shè)置失敗重試次數(shù)
                task.setAutoRetryTimes(autoRetryTimes);
            }

            if (syncCallback != null) {
              //設(shè)置是否進(jìn)行同步回調(diào)
                task.setSyncCallback(syncCallback);
            }

            if (isForceReDownload != null) {
               //是否強(qiáng)制重新下載
                task.setForceReDownload(isForceReDownload);
            }

            if (callbackProgressTimes != null) {
                //設(shè)置進(jìn)度回調(diào)次數(shù)
                task.setCallbackProgressTimes(callbackProgressTimes);
            }

            if (callbackProgressMinIntervalMillis != null) {
              //設(shè)置進(jìn)度回調(diào)最小間隔時(shí)間
                task.setCallbackProgressMinInterval(callbackProgressMinIntervalMillis);
            }

            if (tag != null) {
                task.setTag(tag);
            }

            if (taskFinishListenerList != null) {
                //任務(wù)結(jié)束監(jiān)聽(tīng)器,只有在非UI現(xiàn)場(chǎng)中調(diào)用
                for (BaseDownloadTask.FinishListener finishListener : taskFinishListenerList) {
                    task.addFinishListener(finishListener);
                }
            }

            if (this.directory != null) {
                //設(shè)置下載下來(lái)的文件存放的路徑
                task.setPath(this.directory, true);
            }

            if (this.isWifiRequired != null) {
                //是否需要只在wifi情況下才下載
                task.setWifiRequired(this.isWifiRequired);
            }

           //這個(gè)方法主要是標(biāo)識(shí)下是在隊(duì)列中的任務(wù),而且會(huì)將任務(wù)添加到FileDownloadList的任務(wù)集合中
            task.asInQueueTask().enqueue();
        }

      //啟動(dòng)下載任務(wù)
        FileDownloader.getImpl().start(target, isSerial);
    }

我們看到這個(gè)方法主要是給任務(wù)DownloadTask設(shè)置一些參數(shù)值,然后將任務(wù)添加到FileDownloadList中的集合里。然后調(diào)用FileDownloader#start()開始下載任務(wù):

  public boolean start(final FileDownloadListener listener, final boolean isSerial) {

        if (listener == null) {
            FileDownloadLog.w(this, "Tasks with the listener can't start, because the listener " +
                    "provided is null: [null, %B]", isSerial);
            return false;
        }


        return isSerial ?
                getQueuesHandler().startQueueSerial(listener) :
                getQueuesHandler().startQueueParallel(listener);
    }

這個(gè)方法很簡(jiǎn)單,這里isSerial 標(biāo)志我們?cè)谇懊嬉呀?jīng)設(shè)置了,我們這里就走并行下載這條道吧,所以isSerial 為false,所以這里會(huì)調(diào)用getQueuesHandler().startQueueParallel()方法來(lái)啟動(dòng)并行下載任務(wù),我們這里跟蹤到QueuesHandler#startQueueParallel()方法:

  @Override
    public boolean startQueueParallel(FileDownloadListener listener) {
        final int attachKey = listener.hashCode();

        //這里主要防止在添加了下載任務(wù)之后又有新的任務(wù)到達(dá),所以這里會(huì)重新組裝任務(wù)隊(duì)列
        final List<BaseDownloadTask.IRunningTask> list = FileDownloadList.getImpl().
                assembleTasksToStart(attachKey, listener);

        //這里會(huì)驗(yàn)證是否任務(wù)隊(duì)列里面有任務(wù),然后會(huì)調(diào)用全局監(jiān)聽(tīng)器的onRequestStart(),如果有打點(diǎn)/統(tǒng)計(jì)等需求可以考慮設(shè)置這個(gè)下載監(jiān)聽(tīng)器
        if (onAssembledTasksToStart(attachKey, list, listener, false)) {
            return false;
        }

       //遍歷任務(wù)集合,然后分別啟動(dòng)
        for (BaseDownloadTask.IRunningTask task : list) {
            task.startTaskByQueue();
        }

        return true;
    }

我們看到前面兩步就是重新組裝一下任務(wù)隊(duì)列,然后我們會(huì)調(diào)用任務(wù)task中的startTaskByQueue()方法進(jìn)行啟動(dòng)任務(wù):

 @Override
    public void startTaskByQueue() {
        startTaskUnchecked();
    }

  private int startTaskUnchecked() {
        if (isUsing()) {
            if (isRunning()) {
                throw new IllegalStateException(
                        FileDownloadUtils.formatString("This task is running %d, if you" +
                                " want to start the same task, please create a new one by" +
                                " FileDownloader.create", getId()));
            } else {
                throw new IllegalStateException("This task is dirty to restart, If you want to " +
                        "reuse this task, please invoke #reuse method manually and retry to " +
                        "restart again." + mHunter.toString());
            }
        }

        if (!isAttached()) {
            setAttachKeyDefault();
        }

        mHunter.intoLaunchPool();

        return getId();
    }

前面是判斷這個(gè)任務(wù)是否在執(zhí)行或者占用,最后會(huì)調(diào)用DownloadTaskHunter#intoLaunchPool()方法:

  @Override
    public void intoLaunchPool() {
        synchronized (mPauseLock) {
            //如果任務(wù)是在空閑狀態(tài)則現(xiàn)在設(shè)置狀態(tài)為toLaunchPool,否則返回
            if (mStatus != FileDownloadStatus.INVALID_STATUS) {
                FileDownloadLog.w(this, "High concurrent cause, this task %d will not input " +
                                "to launch pool, because of the status isn't idle : %d",
                        getId(), mStatus);
                return;
            }

            mStatus = FileDownloadStatus.toLaunchPool;
        }

       //獲取下載任務(wù)
        final BaseDownloadTask.IRunningTask runningTask = mTask.getRunningTask();
        final BaseDownloadTask origin = runningTask.getOrigin();

        //如果設(shè)置了全局監(jiān)聽(tīng)就調(diào)用他的onRequestStart
        if (FileDownloadMonitor.isValid()) {
            FileDownloadMonitor.getMonitor().onRequestStart(origin);
        }

        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.v(this, "call start " +
                            "Url[%s], Path[%s] Listener[%s], Tag[%s]",
                    origin.getUrl(), origin.getPath(), origin.getListener(), origin.getTag());
        }

      //設(shè)置任務(wù)準(zhǔn)備就緒
        boolean ready = true;

        try {
            //前面給任務(wù)設(shè)置了path,也就是下載文件存放路徑,這里就是創(chuàng)建這個(gè)路徑
            prepare();
        } catch (Throwable e) {
            ready = false;

            FileDownloadList.getImpl().add(runningTask);
            FileDownloadList.getImpl().remove(runningTask, prepareErrorMessage(e));
        }

        if (ready) {
            //調(diào)用FileDownloadTaskLauncher的launch啟動(dòng)任務(wù)
            FileDownloadTaskLauncher.getImpl().launch(this);
        }

        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.v(this, "the task[%d] has been into the launch pool.", getId());
        }
    }

前面的步驟已經(jīng)注釋的很清楚了,最后我們看到會(huì)調(diào)用FileDownloadTaskLauncher#launch()方法來(lái)啟動(dòng)任務(wù),傳的參數(shù)就是本類DownloadTaskHunter實(shí)例,我們看下launch()方法:

 synchronized void launch(final ITaskHunter.IStarter taskStarter) {
        mLaunchTaskPool.asyncExecute(taskStarter);
    }

這里的mLaunchTaskPool是一個(gè)LaunchTaskPool類實(shí)例,所以會(huì)調(diào)用LaunchTaskPool#asyncExecute()方法:

public void asyncExecute(final ITaskHunter.IStarter taskStarter) {
            mPool.execute(new LaunchTaskRunnable(taskStarter));
}

mPool是一個(gè)自定義的線程池ThreadPoolExecutor實(shí)例,所以調(diào)用execute會(huì)執(zhí)行LaunchTaskRunnable#run()方法:

       LaunchTaskRunnable(final ITaskHunter.IStarter taskStarter) {
            this.mTaskStarter = taskStarter;
            this.mExpired = false;
        }

        @Override
        public void run() {
            if (mExpired) {
                return;
            }

            mTaskStarter.start();
        }

我們看到run方法中會(huì)調(diào)用mTaskStarterstart()方法,從上面我們可以知道mTaskStarterDownloadTaskHunter實(shí)例,所以這里會(huì)調(diào)用DownloadTaskHunterstart()方法,這個(gè)方法現(xiàn)在是在子線程中執(zhí)行的:

  @Override
    public void start() {
        //在前面我們已經(jīng)設(shè)置過(guò)標(biāo)識(shí)為toLaunchPool
        if (mStatus != FileDownloadStatus.toLaunchPool) {
            FileDownloadLog.w(this, "High concurrent cause, this task %d will not start," +
                            " because the of status isn't toLaunchPool: %d",
                    getId(), mStatus);
            return;
        }

        //獲取下載任務(wù)
        final BaseDownloadTask.IRunningTask runningTask = mTask.getRunningTask();
        final BaseDownloadTask origin = runningTask.getOrigin();

        //這個(gè)類是用來(lái)檢查服務(wù)是否連接的,因?yàn)槟J(rèn)是跨進(jìn)程的,所以這里會(huì)有跨進(jìn)程的調(diào)用檢查是否
        //連接,如果沒(méi)有連接的話那么會(huì)bindService連接
        final ILostServiceConnectedHandler lostConnectedHandler = FileDownloader.getImpl().
                getLostConnectedHandler();
        try {

            //檢查服務(wù)有沒(méi)有連接,沒(méi)有連接就連接,如果是跨進(jìn)程就啟動(dòng)SeparateProcessService
            //服務(wù),不是跨進(jìn)程就啟動(dòng)SharedMainProcessService服務(wù)
            if (lostConnectedHandler.dispatchTaskStart(runningTask)) {
                return;
            }

            synchronized (mPauseLock) {
                if (mStatus != FileDownloadStatus.toLaunchPool) {
                    FileDownloadLog.w(this, "High concurrent cause, this task %d will not start," +
                                    " the status can't assign to toFileDownloadService, because the status" +
                                    " isn't toLaunchPool: %d",
                            getId(), mStatus);
                    return;
                }
                //將任務(wù)狀態(tài)設(shè)置為toFileDownloadService
                mStatus = FileDownloadStatus.toFileDownloadService;
            }

            //這里會(huì)通知FileDownloadMonitor的onTaskBegin說(shuō)任務(wù)要開始下載了,同時(shí)會(huì)重新
            //組裝一下任務(wù)列表
            FileDownloadList.getImpl().add(runningTask);
            if (FileDownloadHelper.inspectAndInflowDownloaded(
                    origin.getId(), origin.getTargetFilePath(), origin.isForceReDownload(), true)
                    ) {
                // Will be removed when the complete message is received in #update
                return;
            }

           //啟動(dòng)任務(wù)
            final boolean succeed = FileDownloadServiceProxy.getImpl().
                    start(
                            origin.getUrl(),
                            origin.getPath(),
                            origin.isPathAsDirectory(),
                            origin.getCallbackProgressTimes(), origin.getCallbackProgressMinInterval(),
                            origin.getAutoRetryTimes(),
                            origin.isForceReDownload(),
                            mTask.getHeader(),
                            origin.isWifiRequired());

          //如果任務(wù)是暫停的話那么這里就會(huì)執(zhí)行暫停操作
            if (mStatus == FileDownloadStatus.paused) {
                FileDownloadLog.w(this, "High concurrent cause, this task %d will be paused," +
                        "because of the status is paused, so the pause action must be applied", getId());
                if (succeed) {
                    FileDownloadServiceProxy.getImpl().pause(getId());
                }
                return;
            }

            if (!succeed) {//如果任務(wù)啟動(dòng)失敗
                //noinspection StatementWithEmptyBody
                //如果啟動(dòng)服務(wù)失敗則進(jìn)入如下代碼
                if (!lostConnectedHandler.dispatchTaskStart(runningTask)) {
                    final MessageSnapshot snapshot = prepareErrorMessage(
                            new RuntimeException("Occur Unknown Error, when request to start" +
                                    " maybe some problem in binder, maybe the process was killed in " +
                                    "unexpected."));
                    //任務(wù)列表沒(méi)有當(dāng)前任務(wù)則添加
                    if (FileDownloadList.getImpl().isNotContains(runningTask)) {
                        lostConnectedHandler.taskWorkFine(runningTask);
                        FileDownloadList.getImpl().add(runningTask);
                    }
                    //有的話就刪除,說(shuō)明執(zhí)行任務(wù)出錯(cuò)了
                    FileDownloadList.getImpl().remove(runningTask, snapshot);

                } else {
                    // the FileDownload Service host process was killed when request stating and it
                    // will be restarted by LostServiceConnectedHandler.
                }
            } else {
                //任務(wù)執(zhí)行成功
                lostConnectedHandler.taskWorkFine(runningTask);
            }

        } catch (Throwable e) {
            e.printStackTrace();

            FileDownloadList.getImpl().remove(runningTask, prepareErrorMessage(e));
        }
    }

上面的代碼可以看到我已經(jīng)注釋的非常清楚了,像服務(wù)的啟動(dòng),我這里就不會(huì)展開講了,因?yàn)橥?jiǎn)單的。FileDownloadHelper#inspectAndInflowDownloaded()方法我們看到?jīng)]有注釋,有機(jī)會(huì)會(huì)拿出來(lái)講下,我們先來(lái)看看主線邏輯,我們來(lái)看看啟動(dòng)任務(wù)做了什么工作FileDownloadServiceProxy#start()

 @Override
    public boolean start(String url, String path, boolean pathAsDirectory, int callbackProgressTimes,
                         int callbackProgressMinIntervalMillis,
                         int autoRetryTimes, boolean forceReDownload, FileDownloadHeader header,
                         boolean isWifiRequired) {
        return handler.start(url, path, pathAsDirectory, callbackProgressTimes,
                callbackProgressMinIntervalMillis, autoRetryTimes, forceReDownload, header,
                isWifiRequired);
    }

這里的handler是在FileDownloadServiceProxy的構(gòu)造函數(shù)里面初始化的,這里我們可以來(lái)看下:

  private FileDownloadServiceProxy() {
        handler = FileDownloadProperties.getImpl().PROCESS_NON_SEPARATE ?
                new FileDownloadServiceSharedTransmit() :
                new FileDownloadServiceUIGuard();
    }

我們看到這里會(huì)在FileDownloadProperties類中去獲取PROCESS_NON_SEPARATE 標(biāo)志。如果你去看FileDownloadProperties構(gòu)造函數(shù)就會(huì)知道,這些值是從filedownloader.properties這個(gè)文件配置的,這里還有很多其他的標(biāo)志,這里的PROCESS_NON_SEPARATE是為了標(biāo)識(shí)是否下載服務(wù)是在單獨(dú)的進(jìn)程中的,我們這里就默認(rèn)設(shè)置為在單獨(dú)進(jìn)程中下載,在單獨(dú)的進(jìn)程中執(zhí)行下載服務(wù)的好處是可以減少應(yīng)用進(jìn)程占用的內(nèi)存,且使應(yīng)用更加穩(wěn)定。所以當(dāng)PROCESS_NON_SEPARATE為false的情況下,我們的handler會(huì)是FileDownloadServiceUIGuard類實(shí)例,所以這里的start()方法就會(huì)是FileDownloadServiceUIGuard#start()

    public boolean start(final String url, final String path, final boolean pathAsDirectory,
                         final int callbackProgressTimes,
                         final int callbackProgressMinIntervalMillis,
                         final int autoRetryTimes, final boolean forceReDownload,
                         final FileDownloadHeader header, final boolean isWifiRequired) {
        if (!isConnected()) {
            return DownloadServiceNotConnectedHelper.start(url, path, pathAsDirectory);
        }

        try {
            getService().start(url, path, pathAsDirectory, callbackProgressTimes,
                    callbackProgressMinIntervalMillis, autoRetryTimes, forceReDownload, header,
                    isWifiRequired);
        } catch (RemoteException e) {
            e.printStackTrace();

            return false;
        }

        return true;
    }

這里的getService()方法返回的是遠(yuǎn)程的服務(wù)代理,因?yàn)檫@里是跨進(jìn)程的,所以遠(yuǎn)程的服務(wù)是SeparateProcessService,為什么說(shuō)這個(gè)方法獲取到的是遠(yuǎn)程的服務(wù)代理呢?因?yàn)槭?code>FileDownloadServiceUIGuard實(shí)現(xiàn)了ServiceConnection,所以在綁定遠(yuǎn)程服務(wù)的時(shí)候會(huì)回調(diào)onServiceConnected()方法,我們?cè)谶@里可以看到:

 @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        this.service = asInterface(service);

        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this, "onServiceConnected %s %s", name, this.service);
        }

        try {
            registerCallback(this.service, this.callback);
        } catch (RemoteException e) {
            e.printStackTrace();
        }

        @SuppressWarnings("unchecked") final List<Runnable> runnableList =
                (List<Runnable>) connectedRunnableList.clone();
        connectedRunnableList.clear();
        for (Runnable runnable : runnableList) {
            runnable.run();
        }

        FileDownloadEventPool.getImpl().
                asyncPublishInNewThread(new DownloadServiceConnectChangedEvent(
                        DownloadServiceConnectChangedEvent.ConnectStatus.connected, serviceClass));

    }

可以看到第一句就是賦值service,所以我們直接看遠(yuǎn)程服務(wù)SeparateProcessService#start()方法,這個(gè)SeparateProcessService是繼承FileDownloadService類的,它本身是一個(gè)空實(shí)現(xiàn),我們來(lái)看看FileDownloadService中的start()方法,熟悉AIDL的人都知道,這些實(shí)現(xiàn)都在Binder實(shí)體中實(shí)現(xiàn),我們來(lái)看下onBind方法:

 @Override
    public IBinder onBind(Intent intent) {
        return handler.onBind(intent);
    }

這里又調(diào)用了handleronBind方法,我們看下這里的handler是個(gè)什么東西?

 if (FileDownloadProperties.getImpl().PROCESS_NON_SEPARATE) {
            handler = new FDServiceSharedHandler(new WeakReference<>(this), manager);
        } else {
            handler = new FDServiceSeparateHandler(new WeakReference<>(this), manager);
        }

可以看到,我們這里PROCESS_NON_SEPARATE是為false的,所以我們的handlerFDServiceSeparateHandler實(shí)例。所以我們看FDServiceSeparateHandler#onBind()方法:

  @Override
    public IBinder onBind(Intent intent) {
        return this;
    }

public class FDServiceSeparateHandler extends IFileDownloadIPCService.Stub
        implements MessageSnapshotFlow.MessageReceiver, IFileDownloadServiceHandler {
}

我們看到這里onBind方法返回this,FDServiceSeparateHandler 實(shí)現(xiàn)了IFileDownloadIPCService.Stub,所以FDServiceSeparateHandler就是Binder實(shí)體,我們看這個(gè)類里面的start()方法:

 @Override
    public void start(String url, String path, boolean pathAsDirectory, int callbackProgressTimes,
                      int callbackProgressMinIntervalMillis, int autoRetryTimes, boolean forceReDownload,
                      FileDownloadHeader header, boolean isWifiRequired) throws RemoteException {
        downloadManager.start(url, path, pathAsDirectory, callbackProgressTimes,
                callbackProgressMinIntervalMillis, autoRetryTimes, forceReDownload, header,
                isWifiRequired);
    }

這里調(diào)用了downloadManager的start方法,這里的downloadManagerFileDownloadManager實(shí)例,所以程序會(huì)調(diào)用FileDownloadManager#start()方法。

4.FileDownloadManager start

這里就是真正開始啟動(dòng)下載任務(wù)了,我們完整地看下代碼:

    // synchronize for safe: check downloading, check resume, update data, execute runnable
    public synchronized void start(final String url, final String path, final boolean pathAsDirectory,
                                   final int callbackProgressTimes,
                                   final int callbackProgressMinIntervalMillis,
                                   final int autoRetryTimes, final boolean forceReDownload,
                                   final FileDownloadHeader header, final boolean isWifiRequired) {
        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this, "request start the task with url(%s) path(%s) isDirectory(%B)",
                    url, path, pathAsDirectory);
        }

        //根據(jù)請(qǐng)求鏈接,文件存儲(chǔ)路徑,已經(jīng)一個(gè)boolean值來(lái)生成一次請(qǐng)求的唯一id
        final int id = FileDownloadUtils.generateId(url, path, pathAsDirectory);
        //從數(shù)據(jù)庫(kù)中獲取這個(gè)id對(duì)應(yīng)的請(qǐng)求,封裝在FileDownloadModel 中,因?yàn)橛锌赡芮懊嬉呀?jīng)有下載過(guò)了,只是由于不可抗性終止了
        FileDownloadModel model = mDatabase.find(id);

        List<ConnectionModel> dirConnectionModelList = null;

      //如果pathAsDirectory 為false也就是說(shuō)是文件,而且獲取到的model為null,那么進(jìn)入如下邏輯
        if (!pathAsDirectory && model == null) {
            // try dir data.
            //根據(jù)這個(gè)文件的上級(jí)目錄來(lái)生成一個(gè)唯一id
            final int dirCaseId = FileDownloadUtils.generateId(url, FileDownloadUtils.getParent(path),
                    true);
            //尋找這個(gè)id對(duì)應(yīng)的model
            model = mDatabase.find(dirCaseId);
            if (model != null && path.equals(model.getTargetFilePath())) {
                if (FileDownloadLog.NEED_LOG) {
                    FileDownloadLog.d(this, "task[%d] find model by dirCaseId[%d]", id, dirCaseId);
                }
                //獲取該id下的所有連接,即對(duì)應(yīng)的FileDownloadModel集合
                dirConnectionModelList = mDatabase.findConnectionModel(dirCaseId);
            }
        }

        if (FileDownloadHelper.inspectAndInflowDownloading(id, model, this, true)) {
            if (FileDownloadLog.NEED_LOG) {
                FileDownloadLog.d(this, "has already started download %d", id);
            }
            return;
        }

        //獲取文件路徑,沒(méi)有則生成一個(gè)
        final String targetFilePath = model != null ? model.getTargetFilePath() :
                FileDownloadUtils.getTargetFilePath(path, pathAsDirectory, null);
        if (FileDownloadHelper.inspectAndInflowDownloaded(id, targetFilePath, forceReDownload,
                true)) {
            if (FileDownloadLog.NEED_LOG) {
                FileDownloadLog.d(this, "has already completed downloading %d", id);
            }
            return;
        }

        //獲取文件下載了多少字節(jié),因?yàn)橛锌赡苤跋铝艘话霐嗟袅耍缓螳@取暫存文件路徑
        final long sofar = model != null ? model.getSoFar() : 0;
        final String tempFilePath = model != null ? model.getTempFilePath() :
                FileDownloadUtils.getTempPath(targetFilePath);
        if (FileDownloadHelper.inspectAndInflowConflictPath(id, sofar, tempFilePath, targetFilePath,
                this)) {
            if (FileDownloadLog.NEED_LOG) {
                FileDownloadLog.d(this, "there is an another task with the same target-file-path %d %s",
                        id, targetFilePath);
                // because of the file is dirty for this task.
                if (model != null) {
                    mDatabase.remove(id);
                    mDatabase.removeConnections(id);
                }
            }
            return;
        }

        // real start
        // - create model
        //創(chuàng)建連接model,如果數(shù)據(jù)庫(kù)已經(jīng)存在這個(gè)model,而且狀態(tài)是下面狀態(tài)中的一種,
       //那么說(shuō)明真的是意外退出了,則根據(jù)下面情況看要不要更新數(shù)據(jù)庫(kù),否則就創(chuàng)建一個(gè)新的model請(qǐng)求
        boolean needUpdate2DB;
        if (model != null &&
                (model.getStatus() == FileDownloadStatus.paused ||
                        model.getStatus() == FileDownloadStatus.error ||
                        model.getStatus() == FileDownloadStatus.pending ||
                        model.getStatus() == FileDownloadStatus.started ||
                        model.getStatus() == FileDownloadStatus.connected) // FileDownloadRunnable invoke
            // #isBreakpointAvailable to determine whether it is really invalid.
                ) {
            if (model.getId() != id) {
                // in try dir case.
                mDatabase.remove(model.getId());
                mDatabase.removeConnections(model.getId());

                model.setId(id);
                model.setPath(path, pathAsDirectory);
                if (dirConnectionModelList != null) {
                    for (ConnectionModel connectionModel : dirConnectionModelList) {
                        connectionModel.setId(id);
                        mDatabase.insertConnectionModel(connectionModel);
                    }
                }

                needUpdate2DB = true;
            } else {
                if (!TextUtils.equals(url, model.getUrl())) {
                    // for cover the case of reusing the downloaded processing with the different url( using with idGenerator ).
                    model.setUrl(url);
                    needUpdate2DB = true;
                } else {
                    needUpdate2DB = false;
                }
            }
        } else {
            if (model == null) {
                model = new FileDownloadModel();
            }
            model.setUrl(url);
            model.setPath(path, pathAsDirectory);

            model.setId(id);
            model.setSoFar(0);
            model.setTotal(0);
            model.setStatus(FileDownloadStatus.pending);
            model.setConnectionCount(1);
            needUpdate2DB = true;
        }

        // - update model to db
        if (needUpdate2DB) {
            mDatabase.update(model);
        }

        final DownloadLaunchRunnable.Builder builder = new DownloadLaunchRunnable.Builder();
        //利用建造者模式創(chuàng)建DownloadLaunchRunnable 對(duì)象
        final DownloadLaunchRunnable runnable =
                builder.setModel(model)
                        .setHeader(header)
                        .setThreadPoolMonitor(this)
                        .setMinIntervalMillis(callbackProgressMinIntervalMillis)
                        .setCallbackProgressMaxCount(callbackProgressTimes)
                        .setForceReDownload(forceReDownload)
                        .setWifiRequired(isWifiRequired)
                        .setMaxRetryTimes(autoRetryTimes)
                        .build();

        // - execute
        //執(zhí)行這個(gè)Runnable
        mThreadPool.execute(runnable);
    }

我們看到上面的方法主要作用就是根據(jù)url,path與是否是目錄來(lái)標(biāo)識(shí)一個(gè)請(qǐng)求,如果符合意外退出的情況,那么會(huì)對(duì)SoFar(文件已經(jīng)下載了多少)這個(gè)值進(jìn)行恢復(fù)。最后重新賦值給DownloadLaunchRunnable對(duì)象,進(jìn)行執(zhí)行,DownloadLaunchRunnable是一個(gè)Runnable對(duì)象,所以會(huì)執(zhí)行他的run()方法:

 @Override
    public void run() {
        try {
          //設(shè)置線程優(yōu)先級(jí)為后臺(tái),這樣當(dāng)多個(gè)線程并發(fā)后很多無(wú)關(guān)緊要的線程分配的CPU時(shí)間將會(huì)減少,有利于主線程的處理
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

            // status checkout
          //檢查狀態(tài)
            if (model.getStatus() != FileDownloadStatus.pending) {
                if (model.getStatus() == FileDownloadStatus.paused) {
                    if (FileDownloadLog.NEED_LOG) {
                        FileDownloadLog.d(this, "High concurrent cause, start runnable but " +
                                "already paused %d", model.getId());
                    }

                } else {
                    onError(new RuntimeException(
                            FileDownloadUtils.formatString("Task[%d] can't start the download" +
                                            " runnable, because its status is %d not %d",
                                    model.getId(), model.getStatus(), FileDownloadStatus.pending)));
                }
                return;
            }

            //更新model的狀態(tài)為啟動(dòng)狀態(tài)
            if (!paused) {
                statusCallback.onStartThread();
            }

            do {
              //如果是暫停狀態(tài)則返回不執(zhí)行,如日志所說(shuō),高并發(fā)可能導(dǎo)致這個(gè)結(jié)果
                if (paused) {
                    if (FileDownloadLog.NEED_LOG) {
                        FileDownloadLog.d(this, "High concurrent cause, start runnable but " +
                                "already paused %d", model.getId());
                    }
                    return;
                }

                FileDownloadConnection connection = null;
                try {


                    // 1. connect
                    //檢查權(quán)限和是否需要wifi才能下載條件
                    checkupBeforeConnect();

                    // the first connection is for: 1. etag verify; 2. first connect.
                    final List<ConnectionModel> connectionOnDBList = database.findConnectionModel(model.getId());
                    //檢查是否恢復(fù)之前的連接,如果是多線程連接且不支持seek的情況是不支持
                    //斷點(diǎn)恢復(fù)的,如果支持?jǐn)帱c(diǎn)恢復(fù),在多線程連接的情況下會(huì)把每個(gè)連接下載了
                    //多少計(jì)算賦值給model的SoFar,
                    final ConnectionProfile connectionProfile = buildFirstConnectProfile(connectionOnDBList);
                    final ConnectTask.Builder build = new ConnectTask.Builder();
                    //創(chuàng)建新的連接
                    final ConnectTask firstConnectionTask = build.setDownloadId(model.getId())
                            .setUrl(model.getUrl())
                            .setEtag(model.getETag())
                            .setHeader(userRequestHeader)
                            .setConnectionProfile(connectionProfile)
                            .build();
                    //調(diào)用FileDownloadUrlConnection進(jìn)行連接請(qǐng)求,獲取文件長(zhǎng)度
                    connection = firstConnectionTask.connect();
                    //會(huì)根據(jù)ETag來(lái)判斷遠(yuǎn)端的文件是否有改變,改變的話就是從頭開始下載,不然就獲取文件總長(zhǎng)度
                    handleFirstConnected(firstConnectionTask.getRequestHeader(),
                            firstConnectionTask, connection);

                    if (paused) {
                        model.setStatus(FileDownloadStatus.paused);
                        return;
                    }

                    // 2. fetch
                    //檢查是否有另外一個(gè)url請(qǐng)求任務(wù)跟這個(gè)任務(wù)目錄存儲(chǔ)路徑一樣,
                    //一樣的話判斷這個(gè)url下載的文件是否存在,如果其他進(jìn)程正在下載這個(gè)文件,
                   //那么這里請(qǐng)求就停止,如果其他進(jìn)程下載文件已經(jīng)停止了,那么這里就從斷點(diǎn)
                    //進(jìn)行恢復(fù)下載
                    checkupBeforeFetch();
                    final long totalLength = model.getTotal();
                    // pre-allocate if need.
                    //預(yù)先分配文件的大小
                    handlePreAllocate(totalLength, model.getTempFilePath());

                    final int connectionCount;
                    // start fetching
                    //是否支持多線程即多個(gè)連接下載
                    if (isMultiConnectionAvailable()) {
                        if (isResumeAvailableOnDB) {
                            connectionCount = model.getConnectionCount();
                        } else {
                            connectionCount = CustomComponentHolder.getImpl()
                                    .determineConnectionCount(model.getId(), model.getUrl(), model.getPath(), totalLength);
                        }
                    } else {
                        connectionCount = 1;
                    }

                    if (connectionCount <= 0) {
                        throw new IllegalAccessException(FileDownloadUtils
                                .formatString("invalid connection count %d, the connection count" +
                                        " must be larger than 0", connection));
                    }

                    if (paused) {
                        model.setStatus(FileDownloadStatus.paused);
                        return;
                    }

                    isSingleConnection = connectionCount == 1;
                    if (isSingleConnection) {
                        // single connection
                        //單個(gè)子線程下載
                        fetchWithSingleConnection(firstConnectionTask.getProfile(), connection);
                    } else {
                        if (connection != null) {
                            connection.ending();
                            connection = null;
                        }
                        // multiple connection
                        statusCallback.onMultiConnection();
                        if (isResumeAvailableOnDB) {
                            //恢復(fù)多線程下載
                            fetchWithMultipleConnectionFromResume(connectionCount, connectionOnDBList);
                        } else {
                             //從頭開始多線程下載
                            fetchWithMultipleConnectionFromBeginning(totalLength, connectionCount);
                        }
                    }

                } catch (IOException | IllegalAccessException | InterruptedException | IllegalArgumentException | FileDownloadGiveUpRetryException e) {
                    if (isRetry(e)) {
                        onRetry(e, 0);
                        continue;
                    } else {
                        onError(e);
                    }
                } catch (DiscardSafely discardSafely) {
                    return;
                } catch (RetryDirectly retryDirectly) {
                    model.setStatus(FileDownloadStatus.retry);
                    continue;
                } finally {
                    if (connection != null) connection.ending();
                }

                break;
            } while (true);
        } finally {
            statusCallback.discardAllMessage();

            if (paused) {
                statusCallback.onPausedDirectly();
            } else if (error) {
                statusCallback.onErrorDirectly(errorException);
            } else {
                try {
                    statusCallback.onCompletedDirectly();
                } catch (IOException e) {
                    statusCallback.onErrorDirectly(e);
                }
            }

            alive.set(false);
        }
    }

我們看到這個(gè)方法就是下載的主要方法了,流程注釋已經(jīng)寫得很清楚了,就是里面沒(méi)有展開寫,不然本篇文章的篇幅要很長(zhǎng)了,到這里我們已經(jīng)講完請(qǐng)求的大體流程了,這里明確下一個(gè)請(qǐng)求對(duì)應(yīng)一個(gè)FileDownloadModel實(shí)體,每個(gè)請(qǐng)求下面又可以有多個(gè)ConnectionModel實(shí)體,每個(gè)ConnectionModel實(shí)體對(duì)應(yīng)一條請(qǐng)求線程。當(dāng)然這里面的FileDownloadHelper#inspectAndInflowDownloaded()方法沒(méi)有講,如果大家看不懂可以給我留言,我會(huì)解答。

總結(jié):這個(gè)下載框架整體來(lái)說(shuō)思維還是不錯(cuò)的,希望里面的編程思想會(huì)有幫助到大家,在以后自己編寫框架的過(guò)程中能有所借鑒,最后祝大家源碼之路愉快哈。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評(píng)論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,441評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,475評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,834評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,009評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,559評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,516評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,728評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,249評(píng)論 3 399
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,484評(píng)論 2 379

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

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,826評(píng)論 18 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法,類相關(guān)的語(yǔ)法,內(nèi)部類的語(yǔ)法,繼承相關(guān)的語(yǔ)法,異常的語(yǔ)法,線程的語(yǔ)...
    子非魚_t_閱讀 31,733評(píng)論 18 399
  • AFHTTPRequestOperationManager 網(wǎng)絡(luò)傳輸協(xié)議UDP、TCP、Http、Socket、X...
    Carden閱讀 4,371評(píng)論 0 12
  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程,因...
    小菜c閱讀 6,494評(píng)論 0 17
  • 1.螢火蟲之墓 珍惜眼前,珍惜當(dāng)下,誰(shuí)知道明天和意外,哪一個(gè)先來(lái) ——《螢火蟲之墓》 推薦理由:催淚神器,戰(zhàn)爭(zhēng)的殘...
    林強(qiáng)自媒體閱讀 1,635評(píng)論 7 73