10 后臺默默地勞動者--探究服務(wù)

不管怎么說奸例,后臺功能屬于四大組件之一查吊,其重要程度不言而喻

10.1 服務(wù)是什么

服務(wù)(Service)是Android中實(shí)現(xiàn)程序后臺運(yùn)行的解決方案,它非常適合去執(zhí)行那些不需要和用戶交互而且還要求長期運(yùn)行的任務(wù)评也。服務(wù)的運(yùn)行不依賴于任何用戶界面,即使程序被切換到后臺罚缕,或者用戶打開了另外一個(gè)應(yīng)用程序,服務(wù)仍然能夠保持正常運(yùn)行腌乡。

不過需要注意的是,服務(wù)并不是運(yùn)行在一個(gè)獨(dú)立的進(jìn)程當(dāng)中的渣锦,而是依賴于創(chuàng)建服務(wù)時(shí)所在的應(yīng)用程序進(jìn)程型檀。當(dāng)某個(gè)應(yīng)用程序進(jìn)程被殺掉時(shí),所有依賴于該進(jìn)程的服務(wù)也會停止運(yùn)行。

另外腰吟,也不要被服務(wù)的后臺概念所迷惑,實(shí)際上服務(wù)并不會自動開啟線程灵疮,所有的代碼都是默認(rèn)運(yùn)行在主線程當(dāng)中的。也就是說蒿赢,我們需要在服務(wù)的內(nèi)部手動創(chuàng)建子線程,并在這里執(zhí)行具體的任務(wù)晾腔,否則就有可能出現(xiàn)主線程被阻塞住的情況。

10.2.1 線程的基本用法

定義一個(gè)線程只需要新建一個(gè)類繼承自Thread剔应,然后重寫父類的run()方法,并在里面編寫耗時(shí)邏輯即可纤控。

class MyThread extends Thread {
    @Override
    public void run() {
       //處理具體的邏輯
    }
}

只需要newMyThread的實(shí)例刻撒,然后調(diào)用它的start()方法,這樣run()方法中的代碼就會在子線程當(dāng)中運(yùn)行了醋火。

new MyThread().start();

當(dāng)然,使用繼承的方式耦合性有點(diǎn)高晚树,更多的時(shí)候我們都會選擇使用實(shí)現(xiàn)Runnable接口的方式來定義一個(gè)線程。

class MyThread implements Runnable {
    @Override
    public void run() {
       //處理具體的邏輯
    }
}

如果使用了這種寫法,啟動線程的方法也需要進(jìn)行相應(yīng)的改變愚铡。

MyThread myThread = new MyThread();
new Thread(myThread).start();

Thread的構(gòu)造函數(shù)接收一個(gè)Runnable參數(shù),而我們new出的MyThread正是一個(gè)實(shí)現(xiàn)了Runnable接口的對象邑雅,所以可以直接將它傳入到Thread的構(gòu)造函數(shù)中。接著調(diào)用Threadstart()方法,run()方法中的代碼就會在子線程當(dāng)中運(yùn)行了洞难。

當(dāng)然,如果你不想專門再定義一個(gè)類去實(shí)現(xiàn)Runnable接口露筒,也可以使用匿名類的方式,這種寫法更為常見。

new Thread(new Runnable() {
    @Override
    public void run() {
      //處理具體的邏輯
    }
}).start();

10.2.2 在子線程中更新UI

和其他的GUI庫一樣掌眠,AndroidUI也是線程不安全的。也就是說渺尘,如果想要更新應(yīng)用程序里的UI元素,則必須在主線程中進(jìn)行,否則就會出現(xiàn)異常腋逆。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private TextView text;

    private Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        text = (TextView) findViewById(R.id.text2);
        button = (Button) findViewById(R.id.change_text);
        button.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.change_text:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        text.setText("Nice to meet you");
                    }
                }).start();
                break;
            default:
                break;
        }
    }
}

觀察logcat中的錯(cuò)誤日志俏蛮,可以看出是由于在子線程中更新UI所導(dǎo)致的

 Process: com.example.androidthreadtest, PID: 10616  
 
 android.view.ViewRootImpl$CalledFromWrongThreadException:
 
 Only the original thread that created a view hierarchy can touch its views.

由此證實(shí)了Android確實(shí)是不允許在子線程中進(jìn)行UI操作的争涌。

有些時(shí)候模软,我們必須在子線程里去執(zhí)行一些耗時(shí)任務(wù)燃异,然后根據(jù)任務(wù)的執(zhí)行結(jié)果來更新相應(yīng)的UI控件。對于這種情況,Android提供了一套異步消息處理機(jī)制忘瓦,完美地解決了在子線程中進(jìn)行UI操作的問題。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private TextView text;

    private Button button;

    private static final int UODATE_TEXT = 1;

    private Handler handler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        text = (TextView) findViewById(R.id.text2);
        button = (Button) findViewById(R.id.change_text);
        button.setOnClickListener(this);

        handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case UODATE_TEXT:
                        //在這里可以進(jìn)行UI操作
                        text.setText("Nice to meet you !");
                        break;
                    default:
                        break;
                }
            }
        };

    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.change_text:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Message message = new Message();
                        message.what = UODATE_TEXT;
                        handler.sendMessage(message);//將Message對象發(fā)送出去
                    }
                }).start();
                break;
            default:
                break;
        }
    }
}

我們先是定義了一個(gè)整形常量UPDATE_TEXT李丰,用于表示更新TextView這個(gè)動作舟舒。然后新增一個(gè)Handler對象,并重寫父類的handleMessage()方法夺鲜,在這里對具體的Message進(jìn)行處理。如果發(fā)現(xiàn)Messagewhat字段的值等于UPDATE_TEXT,就將TextView顯示的內(nèi)容改成Nice to meet you

可以看到每辟,這次我們并沒有在子線程里直接進(jìn)行UI操作渠欺,而是創(chuàng)建了一個(gè)Message(android.os.Message)對象讹开,并將它的what字段的值指定為UPDATE_TEXT,然后調(diào)用HandlersendMessage()方法將這條Message發(fā)送出去。很快赏半,Handler就會收到這條Message,并在handleMessage()方法中對它進(jìn)行處理。注意此時(shí)handleMessage()方法中的代碼就是在主線程當(dāng)中運(yùn)行的了埃撵,所以我們可以放心地在這里進(jìn)行UI操作。接下來對Message攜帶的what字段的值進(jìn)行判斷,如果等于UPDATE_TEXT,就將TextView顯示的內(nèi)容改成Nice to meet you.

10.2.3 解析異步消息處理機(jī)制

Android中的異步消息處理主要由四部分組成:MessageHandler辅鲸,MessageQueueLooper例书。

1.Message

Message是在線程之間傳遞的消息,它可以在內(nèi)部攜帶少量的信息,用于在不同線程之間交換數(shù)據(jù)。上面我們使用到了Messagewhat字段凉敲,除此之外還可以使用arg1arg2字段來攜帶一些整形數(shù)據(jù)阻塑,使用obj字段攜帶一個(gè)Object對象渤昌。

2.Handler

Handler顧名思義也就是處理者的意思,它主要是用于發(fā)送和處理消息。發(fā)送消息一般是使用HandlersendMessage()方法狂秘,而發(fā)出的消息經(jīng)過一系列輾轉(zhuǎn)處理后清女,最終會傳遞到HandlerhandleMessage()方法中拴袭。

3.MessageQueue

MessageQueue是消息隊(duì)列的意思怜瞒,它主要用于存放所有通過Handler發(fā)送的消息。這部分消息會一直存在于消息隊(duì)列中,等待被處理近刘。每個(gè)線程中只會有一個(gè)MessageQueue對象臀晃。

4.Looper

Looper是每個(gè)線程中的MessageQueue的管家,調(diào)用Looperloop()方法后介劫,就會進(jìn)入到一個(gè)無限循環(huán)當(dāng)中徽惋,然后每當(dāng)發(fā)現(xiàn)MessageQueue中存在一條消息,就會將它取出险绘,并傳遞到HandlerhandleMessage()方法中。每個(gè)線程中也只會有一個(gè)Looper對象誉碴。

異步消息處理機(jī)制流程圖

我們來把異步消息處理的整個(gè)流程梳理一遍宦棺。首先需要在主線程當(dāng)中創(chuàng)建一個(gè)Handler對象,并重寫handleMessage()方法黔帕。然后當(dāng)子線程中需要進(jìn)行UI操作時(shí)代咸,就創(chuàng)建一個(gè)Message對象,并通過Handler將這條消息發(fā)送出去成黄。之后這條消息會被添加到MessageQueue的隊(duì)列中等待被處理呐芥,而Looper則會一直嘗試從MessageQueue中取出待處理消息,最后分發(fā)回HandlerhandleMessage()方法中奋岁。由于Handler是在主線程中創(chuàng)建的思瘟,所以此時(shí)handleMessage()方法中的代碼也會在主線程中運(yùn)行,于是我們在這里就可以安心地進(jìn)行UI操作了闻伶。

一條Message經(jīng)過這樣一個(gè)流程的輾轉(zhuǎn)調(diào)用后滨攻,也就從子線程進(jìn)入到了主線程,從不能更新UI變成了可以更新UI,整個(gè)異步消息處理的核心思想也就是如此光绕。

而我們前面使用到的runOnUiThread()方法其實(shí)就是一個(gè)異步消息處理機(jī)制的接口封裝更鲁,它雖然表面上看起來用法更為簡單,但其實(shí)背后的實(shí)現(xiàn)原理和上圖的描述是一樣的奇钞。

10.2.4 使用AsyncTask

為了更加方便我們在子線程中對UI進(jìn)行操作澡为,Android還提供了另外一些好用的工具,比如AsyncTask景埃。借助AsyncTask媒至,即便你對異步消息處理機(jī)制完全不了解,也可以十分簡單地從子線程切換到主線程谷徙。當(dāng)然拒啰,AsyncTask背后的實(shí)現(xiàn)原理也是基于異步消息處理機(jī)制的,只是Android幫我們做了很好地封裝而已完慧。

AsyncTask的基本用法谋旦,由于AsyncTask是一個(gè)抽象類,所以如果我們想使用它屈尼,就必須要創(chuàng)建一個(gè)子類去繼承它册着。在繼承時(shí)我們可以為AsyncTask類指定3個(gè)泛型參數(shù)。

Params: 在執(zhí)行AsyncTask時(shí)需要傳入的參數(shù)脾歧,可用于在后臺任務(wù)中執(zhí)行甲捏。

Progress: 后臺任務(wù)執(zhí)行時(shí),如果需要在界面上顯示當(dāng)前的進(jìn)度鞭执,則使用這里指定的泛型作為進(jìn)度單位司顿。

Result: 當(dāng)任務(wù)執(zhí)行完畢后,如果需要對結(jié)果進(jìn)行返回兄纺,則使用這里指定的泛型作為返回值類型大溜。

因此,一個(gè)最簡單的自定義AsyncTask就可以寫成如下方式:

class DownloadTask extends AsyncTask<Void,Integer,Boolean> {
    ....
}

我們把AsyncTask的第一個(gè)泛型參數(shù)指定為Void估脆,表示在執(zhí)行AsyncTask的時(shí)候不需要傳入?yún)?shù)給后臺任務(wù)钦奋。第二個(gè)泛型參數(shù)指定為Integer,表示使用整形數(shù)據(jù)來做為進(jìn)度顯示單位。第三個(gè)泛型參數(shù)指定為Boolean,則表示使用布爾型數(shù)據(jù)來反饋執(zhí)行結(jié)果旁蔼。

目前我們自定義的DownloadTask還是一個(gè)空任務(wù)锨苏,并不能進(jìn)行任何的操作聘鳞,我們還需要去重寫AsyncTask中的幾個(gè)方法才能完成對任務(wù)的定制糯耍。

1.onPreExecute()

這個(gè)方法會在后臺任務(wù)開始之前調(diào)用谆膳,用于一些界面上的初始化操作巢株,比如顯示一個(gè)進(jìn)度條對話框等座每。

2.doInBackground(Params...)
這個(gè)方法中的所有代碼都會在子線程中運(yùn)行逛揩,我們應(yīng)該在這里去處理所有的耗時(shí)任務(wù)罐柳。任務(wù)一旦完成就可以通過return語句來將任務(wù)的執(zhí)行結(jié)果返回劲适,如果AsyncTask的第三個(gè)泛型參數(shù)指定的是Void,就可以不返回任務(wù)執(zhí)行結(jié)果。注意:在這個(gè)方法中是不可以進(jìn)行UI操作的作喘,如果需要更新UI元素理疙,比如說反饋當(dāng)前任務(wù)的執(zhí)行進(jìn)度,可以調(diào)用publishProgress(Progress...)方法來完成泞坦。

3.onProgressUpdate(Progress...)
當(dāng)在后臺任務(wù)中調(diào)用了publishProgress(Progress...)方法后窖贤,onProgressUpdate(Progress...)方法就會很快被調(diào)用,該方法中攜帶的參數(shù)就是在后臺任務(wù)中傳遞過來的贰锁。在這個(gè)方法中可以對UI進(jìn)行操作赃梧,利用參數(shù)中的數(shù)值就可以對界面元素進(jìn)行相應(yīng)的更新。

4.onPostExecute(Result)
當(dāng)后臺任務(wù)執(zhí)行完畢通過return語句進(jìn)行返回時(shí)豌熄,這個(gè)方法就很快被調(diào)用授嘀。返回的數(shù)據(jù)會作為參數(shù)傳遞到此方法中,可以利用返回的數(shù)據(jù)來進(jìn)行一些UI的操作锣险,比如說提醒任務(wù)執(zhí)行的結(jié)果蹄皱,以及關(guān)閉掉進(jìn)度對話框等。

class DownloadTask extends AsyncTask<Void,Integer,Boolean> {

        @Override
        protected void onPreExecute() {
            progressDialog.show();
        }

        @Override
        protected Boolean doInBackground(Void... params)
        {
            try
            {
                while (true)
                {
                    int downloadPercent = doDownload();//這是一個(gè)虛構(gòu)的方法
                    publishProgress(downloadPercent);
                    if (downloadPercent >= 100)
                    {
                        break;
                    }
                }
            } catch (Exception e)
            {
                return false;
            }
            return true;
        }

        @Override
        protected void onProgressUpdate(Integer... values)
        {
            //在這里進(jìn)行更新下載進(jìn)度
            progressDialog.setMessage("Downloaded " + values[0] + "%");
        }

        @Override
        protected void onPostExecute(Boolean aBoolean)
        {
            progressDialog.dismiss();//關(guān)閉進(jìn)度對話框
            //在這里提示下載結(jié)果
            if (aBoolean)
            {
                Toast.makeText(context, "Download succeeded", Toast.LENGTH_SHORT).show();
            }
            else 
            {
                Toast.makeText(context, "Download failed", Toast.LENGTH_SHORT).show();
            }
        }
        
    }

在這個(gè)DownloadTask中芯肤,我們在doInBackground()方法中去執(zhí)行具體的下載任務(wù)巷折。這個(gè)方法里的代碼都是在子線程中運(yùn)行的,因而不會影響到主線程的運(yùn)行纷妆。注意這里虛構(gòu)了一個(gè)doDownload()方法盔几,這個(gè)方法用于計(jì)算當(dāng)前的下載進(jìn)度并返回晴弃,我們假設(shè)這個(gè)方法已經(jīng)存在了掩幢。在得到了當(dāng)前的下載進(jìn)度后,下面就該考慮如何把它顯示到界面上了上鞠,由于doInBackground()方法是在子線程中運(yùn)行的际邻,在這里肯定不能進(jìn)行UI操作,所以我們可以調(diào)用publishProgress()方法并將當(dāng)前的下載進(jìn)度傳進(jìn)來芍阎,這樣onProgressUpdate()方法就會很快被調(diào)用世曾,在這里就可以進(jìn)行UI操作了。

當(dāng)下載完成后谴咸,doInBackground()方法會返回一個(gè)布爾型變量轮听,這樣onPostExecuet()方法就會很快被調(diào)用,這個(gè)方法也是在主線程中運(yùn)行的岭佳。然后在這里我們會根據(jù)下載的結(jié)果來彈出相應(yīng)的Toast提示血巍,從而完成整個(gè)DownloadTask任務(wù)。

使用AsyncTask的訣竅就是珊随,在doInBackground()方法中執(zhí)行具體的耗時(shí)任務(wù)述寡,在onProgressUpdate()方法中進(jìn)行UI操作柿隙,在onPostExecute()方法中執(zhí)行一些任務(wù)的收尾工作。

如果想要啟動這個(gè)任務(wù)鲫凶,只需編寫以下代碼:

new DownloadTask().execute()

服務(wù)的基本用法

定義一個(gè)服務(wù)

image

我們將服務(wù)命名為MyService,Exported屬性表示是否允許除了當(dāng)前程序以外的其他程序訪問這個(gè)服務(wù)禀崖,Enabled屬性表示是否啟用這個(gè)服務(wù)。

public class MyService extends Service {
    public MyService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }
}

MyService是繼承自Service類的螟炫,說明這是一個(gè)服務(wù)波附。目前MyService中可以算是空空如也,但有一個(gè)onBind()方法特別醒目昼钻。這個(gè)方法是Service中唯一的一個(gè)抽象方法叶雹,所以必須要在子類里實(shí)現(xiàn)。

我們在服務(wù)中處理一些事情换吧,處理事情的邏輯寫在哪呢折晦?這時(shí)就可以重寫Service中的另外一些方法了。

public class MyService extends Service {
    
    ```

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

}

我們又重寫了onCreate(),onStartCommand()onDestroy()這3個(gè)方法沾瓦,他們是每個(gè)服務(wù)中最常用到的3個(gè)方法了满着。其中onCreate()方法會在服務(wù)創(chuàng)建的時(shí)候調(diào)用,onStartCommand()方法會在每次服務(wù)啟動的時(shí)候調(diào)用贯莺,onDestroy()方法會在服務(wù)銷毀的時(shí)候調(diào)用风喇。

通常情況下,如果我們希望服務(wù)一旦啟動就立刻去執(zhí)行某個(gè)動作缕探,就可以將邏輯寫在onStartCommand()方法中魂莫。而當(dāng)服務(wù)銷毀時(shí),我們又應(yīng)該在onDestroy()方法中回收那些不再使用的資源爹耗。

另外需要注意的是耙考,每一個(gè)服務(wù)都需要在AndroidManifest.xml文件中進(jìn)行注冊才能生效。

<service
      android:name=".MyService"
      android:enabled="true"
      android:exported="true">
</service>

10.3.2 啟動和停止服務(wù)

啟動服務(wù)和停止服務(wù)的方法當(dāng)然你也不會陌生潭兽,主要是借助Intent來實(shí)現(xiàn)的倦始。

 @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.start_service:
                Intent startIntent = new Intent(this,MyService.class);
                startService(startIntent);//啟動服務(wù)
                break;
            case R.id.stop_service:
                Intent stopIntent = new Intent(this,MyService.class);
                stopService(stopIntent);//停止服務(wù)
                break;
            default:
                break;
        }
    }

onCreate()方法中分別獲取到了Start Service按鈕和Stop Service按鈕的實(shí)例,并給他們注冊了點(diǎn)擊事件山卦。然后在Start Service按鈕的點(diǎn)擊事件里我們構(gòu)建出了一個(gè)Intent對象鞋邑,并調(diào)用startService()方法來啟動MyService服務(wù)。

Stop Service按鈕的點(diǎn)擊事件里我們同樣構(gòu)建出了一個(gè)Intent對象账蓉,并調(diào)用stopService()方法來停止MyService服務(wù)枚碗。

startService()stopService()方法都是定義在Context類中的,所以我們在活動里可以直接調(diào)用這兩個(gè)方法铸本。注意肮雨,這里完全是由活動來決定服務(wù)何時(shí)停止的,如果沒有點(diǎn)擊Stop Service按鈕归敬,服務(wù)就會一直處于運(yùn)行狀態(tài)酷含。那服務(wù)有沒有什么辦法讓自己停止下來呢鄙早?當(dāng)然可以,只需要在MyService的任何一個(gè)位置調(diào)用stopSelf()方法就能讓這個(gè)服務(wù)停止下來了椅亚。

onCreate()方法是在服務(wù)第一次創(chuàng)建的時(shí)候調(diào)用的限番,而onStartCommand()方法則在每次啟動服務(wù)的時(shí)候都會調(diào)用,由于剛才我們是第一次點(diǎn)擊Start Service按鈕呀舔,服務(wù)此時(shí)還未創(chuàng)建過所以兩個(gè)方法都會執(zhí)行弥虐,之后如果你再連續(xù)多點(diǎn)擊幾次Start Service按鈕,你就會發(fā)現(xiàn)只有onStartCommand()方法可以得到執(zhí)行了媚赖。

10.3.3 活動和服務(wù)進(jìn)行通信

上一節(jié)中雖然服務(wù)是在活動里啟動的霜瘪,但在啟動了服務(wù)之后,活動和服務(wù)基本就沒有什么關(guān)系了惧磺。

服務(wù)會一直處于運(yùn)行狀態(tài)颖对,但具體是運(yùn)行的什么邏輯,活動就控制不了了磨隘。這就類似于活動通知了服務(wù)一下:“你可以啟動了”缤底,然后服務(wù)就去忙自己的事情了,但活動并不知道服務(wù)到底去做了什么事情番捂,以及完成的如何个唧。

創(chuàng)建一個(gè)專門的Binder對象來對下載功能進(jìn)行管理。

public class MyService extends Service {

    private static final String TAG = "MyService";
    
    ······

    private DownloadBinder mBinder = new DownloadBinder();

    class DownloadBinder extends Binder {
        public void startDownload() {
            Log.d(TAG, "startDownload executed");
        }

        public int getProgress() {
            Log.d(TAG, "getProgress executed");
            return 0;
        }
    }

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

我們新建了一個(gè)DownloadBinder類设预,并讓它繼承自Binder徙歼,然后在它的內(nèi)部提供了開始下載以及查看下載進(jìn)度的方法。當(dāng)然這只是兩個(gè)模擬方法鳖枕,并沒有實(shí)現(xiàn)真正的功能魄梯,我們在這兩個(gè)方法中分別打印了一行日志。

當(dāng)一個(gè)活動和服務(wù)綁定之后耕魄,就可以調(diào)用該服務(wù)里的Binder提供的方法了画恰。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private Button startService,stopService;

    private Button bindService,unbindService;

    private MyService.DownloadBinder downloadBinder;

    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            downloadBinder = (MyService.DownloadBinder) service;
            downloadBinder.startDownload();
            downloadBinder.getProgress();
        }

        @Override
        public void onServiceDisconnected(ComponentName name)
        {

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        startService = (Button) findViewById(R.id.start_service);
        stopService = (Button) findViewById(R.id.stop_service);
        startService.setOnClickListener(this);
        stopService.setOnClickListener(this);

        bindService = (Button) findViewById(R.id.bind_service);
        unbindService = (Button) findViewById(R.id.unbind_service);
        bindService.setOnClickListener(this);
        unbindService.setOnClickListener(this);
    }

    @Override
    public void onClick(View v)
    {
        switch (v.getId())
        {
            case R.id.start_service:
                Intent startIntent = new Intent(this,MyService.class);
                startService(startIntent);//啟動服務(wù)
                break;
            case R.id.stop_service:
                Intent stopIntent = new Intent(this,MyService.class);
                stopService(stopIntent);//停止服務(wù)
                break;
            case R.id.bind_service:
                Intent bindIntent = new Intent(this,MyService.class);
                bindService(bindIntent,connection,BIND_AUTO_CREATE);//綁定服務(wù)
                break;
            case R.id.unbind_service:
                unbindService(connection);//解綁服務(wù)
                break;
            default:
                break;
        }
    }
}

我們首先創(chuàng)建了一個(gè)ServiceConnection的匿名類,在里面重寫了onServiceConnected()方法和onServiceDisconnected()方法吸奴,這兩個(gè)方法分別會在活動與服務(wù)成功綁定以及解除綁定的時(shí)候調(diào)用。在onServiceConnected()方法中缠局,我們又通過向下轉(zhuǎn)型得到了DownloadBinder的實(shí)例则奥,有了這個(gè)實(shí)例,活動和服務(wù)之間的關(guān)系就變得非常緊密了∠猎埃現(xiàn)在我們可以在活動中根據(jù)具體的場景來調(diào)用DownloadBinder中的任何public方法读处,即實(shí)現(xiàn)了指揮服務(wù)干什么服務(wù)就去干什么的功能。

我們這里仍然是構(gòu)建出了一個(gè)Intent對象唱矛,然后調(diào)用bindService()方法將MainActivityMyService進(jìn)行綁定罚舱。bindService()方法接收三個(gè)參數(shù)井辜,第一個(gè)參數(shù)就是剛剛構(gòu)建出的Intent對象,第二個(gè)參數(shù)是前面創(chuàng)建出的ServiceConnection的實(shí)例管闷,第三個(gè)參數(shù)則是一個(gè)標(biāo)志位粥脚,這里傳入BIND_AUTO_CREATE表示在活動和服務(wù)進(jìn)行綁定后自動創(chuàng)建服務(wù)。這會使得MyService中的onCreate()方法得到執(zhí)行包个,但是onStartCommand()方法不會執(zhí)行刷允。

解除服務(wù)和活動之間的綁定,調(diào)用一下unbindService()方法就可以了碧囊。

另外需要注意树灶,任何一個(gè)服務(wù)在整個(gè)應(yīng)用程序范圍內(nèi)都是通用的,即MyService不僅可以和MainActivity綁定糯而,還可以和任何一個(gè)其他的活動進(jìn)行綁定天通,而且在綁定完成后他們都可以獲取到相同的DownloadBinder實(shí)例。

10.4 服務(wù)的生命周期

服務(wù)有自己的生命周期熄驼。

一旦在項(xiàng)目的任何位置調(diào)用了ContextstartService()方法土砂,相應(yīng)的服務(wù)就會啟動起來,并回調(diào)onStartCommand()方法谜洽。如果這個(gè)服務(wù)之前還沒有創(chuàng)建過萝映,onStart()方法會先于onStartCommand()方法執(zhí)行。服務(wù)啟動了之后會一直保持運(yùn)行狀態(tài)阐虚,直到stopService()stopSelf()方法被調(diào)用序臂。注意,雖然每調(diào)用一次startService()方法实束,onStartCommand()就會執(zhí)行一次奥秆,但實(shí)際上每個(gè)服務(wù)都只會存在一個(gè)實(shí)例。所以不管你調(diào)用了多少次startService()方法咸灿,只需調(diào)用一次stopService()stopSelf()方法构订,服務(wù)就會停止下來了。

另外避矢,還可以調(diào)用ContextbindService()來獲取一個(gè)服務(wù)的持久連接悼瘾,這時(shí)就會回調(diào)服務(wù)中的onBind()方法。類似里审胸,如果這個(gè)服務(wù)之前還沒有創(chuàng)建過亥宿,onCreate()方法會先于onBind()方法執(zhí)行。之后砂沛,調(diào)用方可以獲取到onBind()方法里返回的IBinder對象的實(shí)例烫扼,這樣就能自由地和服務(wù)進(jìn)行通信了。只要調(diào)用方和服務(wù)之間的連接沒有斷開碍庵,服務(wù)就會一直保持運(yùn)行狀態(tài)映企。

當(dāng)調(diào)用了startService()方法后悟狱,又去調(diào)用stopService()方法,這時(shí)服務(wù)中的onDestroy()方法就會執(zhí)行堰氓,表示服務(wù)已經(jīng)銷毀了挤渐。類似地,當(dāng)調(diào)用了bindService()方法后豆赏,又去調(diào)用unbindService()方法挣菲,onDestroy()方法也會執(zhí)行。這兩種情況都很好理解掷邦。但是需要注意白胀,我們是完全有可能對一個(gè)服務(wù)即調(diào)用了startService()方法,又調(diào)用了bindService()方法抚岗,這種下情況該如何讓服務(wù)銷毀呢或杠?根據(jù)Android系統(tǒng)的機(jī)制,一個(gè)服務(wù)只要被啟動或者被綁定了之后宣蔚,就會一直處于運(yùn)行狀態(tài)向抢,必須要讓以上兩種條件同時(shí)不滿足,服務(wù)才能被銷毀胚委。所以挟鸠,這種情況下要同時(shí)調(diào)用stopService()unbindService()方法,onDestroy()方法才能執(zhí)行亩冬。

這樣你就已經(jīng)把服務(wù)的生命周期完整里走了一遍艘希。

10.5 服務(wù)的更多技巧

10.5.1 使用前臺服務(wù)

服務(wù)幾乎都是在后臺運(yùn)行的,一直以來它都是默默地做著辛苦的工作硅急。但是服務(wù)的系統(tǒng)優(yōu)先級還是比較低的覆享,當(dāng)系統(tǒng)出現(xiàn)內(nèi)存不足的情況時(shí),就有可能回收掉正在后臺運(yùn)行的服務(wù)营袜。如果你希望服務(wù)可以一直保持運(yùn)行狀態(tài)撒顿,而不會由于系統(tǒng)內(nèi)存不足的原因?qū)е卤换厥眨涂梢钥紤]使用前臺服務(wù)荚板。

前臺服務(wù)和普通服務(wù)最大的區(qū)別就在于凤壁,它會有一個(gè)正在運(yùn)行的圖標(biāo)在系統(tǒng)的狀態(tài)欄顯示,下拉狀態(tài)欄后可以看到更加詳細(xì)的信息啸驯,非常類似于通知的效果客扎。當(dāng)然有時(shí)候你也可能不僅僅是為了防止服務(wù)被回收掉才使用前臺服務(wù)的,有些項(xiàng)目由于特殊的需求會要求必須使用前臺服務(wù)罚斗,比如說彩云天氣這款天氣預(yù)報(bào)應(yīng)用,它的服務(wù)在后臺更新天氣數(shù)據(jù)的同時(shí)宅楞,還會在系統(tǒng)狀態(tài)欄一直顯示當(dāng)前的天氣信息针姿。

public class MyService extends Service {
     ······
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate executed");
        Intent intent = new Intent(this,MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this,0,intent,0);
        Notification notification = new Notification.Builder(this)
                .setContentTitle("This is content title")
                .setContentText("This is content text")
                .setWhen(System.currentTimeMillis())
                .setSmallIcon(R.mipmap.ic_launcher)
                .setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher))
                .setContentIntent(pendingIntent)
                .build();
        startForeground(1,notification);
    }

    ``````
}

這次在構(gòu)建出Notification對象后并沒有使用NotificationManager來將通知顯示出來袱吆,而是調(diào)用了startForeground()方法。這個(gè)方法接收兩個(gè)參數(shù)距淫,第一個(gè)參數(shù)是通知的id,類似于notify()方法的第一個(gè)參數(shù)绞绒,第二個(gè)參數(shù)則是構(gòu)建出的Notification對象。調(diào)用startForeground()方法后就會讓MyService變成一個(gè)前臺服務(wù)榕暇,并在系統(tǒng)狀態(tài)顯示出來蓬衡。

10.5.2 使用IntentService

服務(wù)中的代碼都是默認(rèn)運(yùn)行在主線程當(dāng)中的,如果直接在服務(wù)里去處理一些耗時(shí)的邏輯彤枢,就很容易出現(xiàn)ANR(Application Not Responding)的情況狰晚。

所以這個(gè)時(shí)候就需要用到Android多線程編程的技術(shù)了,我們應(yīng)該在服務(wù)的每個(gè)具體的方法里開啟一個(gè)子線程缴啡,然后在這里去處理那些耗時(shí)的邏輯壁晒。因此,一個(gè)比較標(biāo)準(zhǔn)的服務(wù)就可以寫成如下形式:

public class MyService extends Service {
    ······
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(new Runnable() {
            @Override
            public void run() {
               //處理具體的邏輯
            }
        }).start();
        return super.onStartCommand(intent,flags,startId);
    }

}

但是业栅,這個(gè)服務(wù)一旦啟動之后秒咐,就會一直處于運(yùn)行狀態(tài),必須調(diào)用stopService()或者stopSelf()方法才能讓服務(wù)停止下來碘裕。所以携取,如果想要實(shí)現(xiàn)讓一個(gè)服務(wù)在執(zhí)行完畢后自動停止的功能,就可以這樣寫:

public class MyService extends Service {
    ······
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(new Runnable() {
            @Override
            public void run() {
               //處理具體的邏輯
               stopSelf();
            }
        }).start();
        return super.onStartCommand(intent,flags,startId);
    }

}

為了可以簡單里創(chuàng)建一個(gè)異步的帮孔,會自動停止的服務(wù)雷滋,Android專門提供了一個(gè)IntentService類。

public class MyIntentService extends IntentService {

    private static final String TAG = "MyIntentService";
    
    public MyIntentService() {
        super("MyIntentService");//調(diào)用父類的有參構(gòu)造函數(shù)
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        //打印當(dāng)前線程的id
        Log.d(TAG, "Thread id is " +Thread.currentThread().getId());
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy executed");
    }
    
}

這里首先要提供一個(gè)無參的構(gòu)造函數(shù)你弦,并且必須在其內(nèi)部調(diào)用父類的有參構(gòu)造函數(shù)惊豺。然后要在子類中去實(shí)現(xiàn)onHandleIntent()這個(gè)抽象方法,在這個(gè)方法中可以去處理一些具體的邏輯禽作,而且不用擔(dān)心 ANR的問題尸昧,因?yàn)檫@個(gè)方法已經(jīng)是在子線程中運(yùn)行的了。這里為了證實(shí)一下旷偿,我們在onHandleIntent()方法中打印了當(dāng)前線程的id烹俗。另外根據(jù)IntentService的特性,這個(gè)服務(wù)在運(yùn)行結(jié)束后應(yīng)該是會自動停止的萍程,所以我們又重寫了onDestroy()方法幢妄,在這里也打印了一行日志,以證實(shí)服務(wù)是不是停止掉了茫负。

 case R.id.start_intent_service:
                //打印主線程id
                Log.d(TAG, "Thread id is " +Thread.currentThread().getId());
                Intent intentService = new Intent(this,MyIntentService.class);
                startService(intentService);
                break;

在按鈕的點(diǎn)擊事件里面去啟動MyIntentService這個(gè)服務(wù)蕉鸳,并在這里打印了一下主線程的id,稍后用于和IntentService進(jìn)行對比。你會發(fā)現(xiàn)潮尝,其實(shí)IntentService的用法和普通的服務(wù)沒什么兩樣榕吼。

最后不要忘記,服務(wù)都是需要在AndroidManifest.xml里注冊的勉失。

<service android:name=".MyIntentService"/>

第一部分

public class MainActivity extends AppCompatActivity implements View.OnClickListener
{

    private Button startService,pauseService,cancelService;

    private DownloadService.DownloadBinder downloadBinder;

    private ServiceConnection connection = new ServiceConnection()
    {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service)
        {
            downloadBinder = (DownloadService.DownloadBinder) service;
        }

        @Override
        public void onServiceDisconnected(ComponentName name)
        {

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        startService = (Button) findViewById(R.id.start_download);
        pauseService = (Button) findViewById(R.id.pause_download);
        cancelService = (Button) findViewById(R.id.cancel_download);

        startService.setOnClickListener(this);
        pauseService.setOnClickListener(this);
        cancelService.setOnClickListener(this);

        Intent intent = new Intent(this,DownloadService.class);
        startService(intent);//啟動服務(wù)
        bindService(intent,connection,BIND_AUTO_CREATE);//綁定服務(wù)

        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission
                .WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
        {
            ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission
                    .WRITE_EXTERNAL_STORAGE},1);
        }
    }

    @Override
    public void onClick(View v)
    {
        if (downloadBinder == null)
        {
            return;
        }

        switch (v.getId())
        {
            case R.id.start_download:
                String url = "https://raw.githubusercontent.com/guolindev/eclipse/" +
                        "master/eclipse-inst-win64.exe";
                downloadBinder.startDownload(url);
                break;
            case R.id.pause_download:
                downloadBinder.pauseDownload();
                break;
            case R.id.cancel_download:
                downloadBinder.cancelDownload();
                break;
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[]
            permissions, @NonNull int[] grantResults)
    {
        switch (requestCode)
        {
            case 1:
                if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED)
                {
                    Toast.makeText(MainActivity.this, "拒絕權(quán)限將無法使用程序!",
                            Toast.LENGTH_SHORT).show();
                    finish();
                }
                break;
            default:
                break;
        }
    }

    @Override
    protected void onDestroy()
    {
        super.onDestroy();
        unbindService(connection);
    }
}

這里我們首先創(chuàng)建一個(gè)ServiceConnection的匿名類羹蚣,然后在onServiceConnected()方法中獲取到DownloadBinder的實(shí)例,有了這個(gè)實(shí)例乱凿,我們就可以在活動中調(diào)用服務(wù)提供的各種方法了顽素。

在onCreate()方法中,對按鈕進(jìn)行了初始化操作并設(shè)置了點(diǎn)擊事件徒蟆,然后分別調(diào)用了startService()和bindService()方法來啟動和綁定服務(wù)胁出。這一點(diǎn)至關(guān)重要,因?yàn)閱臃?wù)可以保證DownloadService一直在后臺運(yùn)行后专,綁定服務(wù)則可以讓MainActivity和DownloadService進(jìn)行通信划鸽,因此兩個(gè)方法調(diào)用都必不可少。在onCreate()方法的最后戚哎,我們還進(jìn)行了WRITE_EXTERNAL_STORAGE的運(yùn)行時(shí)權(quán)限申請裸诽,因?yàn)橄螺d文件是要下載到SD卡的Download目錄下的,如果沒有這個(gè)權(quán)限的話型凳,我們整個(gè)程序就都無法正常工作丈冬。

在onClick()方法中我們對點(diǎn)擊事件進(jìn)行判斷,如果點(diǎn)擊了開始按鈕就調(diào)用DownloadBinder的startDownload()方法甘畅,如果點(diǎn)擊了暫停按鈕就調(diào)用pauseDownload()方法埂蕊,如果點(diǎn)擊了取消按鈕就調(diào)用cancelDownload()方法。startDownload()方法中你可以傳入任意的下載地址疏唾。

另外還有一點(diǎn)需要注意蓄氧,如果活動被銷毀了,那么一定要記得對服務(wù)進(jìn)行解綁槐脏,不然就有可能會造成內(nèi)存泄露喉童。

第二部分

public class DownloadService extends Service
{

    private DownloadTask downloadTask;

    private String downloadUrl;

    private DownloadListener listener = new DownloadListener()
    {
        @Override
        public void onProgress(int progress)
        {
            getNotificationManager().notify(1,getNotification("Downloading...",progress));
        }

        @Override
        public void onSuccess()
        {
            downloadTask = null;
            //下載成功時(shí)將前臺服務(wù)通知關(guān)閉,并創(chuàng)建一個(gè)下載成功的通知
            stopForeground(true);
            getNotificationManager().notify(1,getNotification("Downloading Success",-1));
            Toast.makeText(DownloadService.this, "Download Success", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onFailed()
        {
            downloadTask = null;
            //下載失敗時(shí)將前臺服務(wù)通知關(guān)閉顿天,并創(chuàng)建一個(gè)下載失敗的通知
            stopForeground(true);
            getNotificationManager().notify(1,getNotification("Download Failed",-1));
            Toast.makeText(DownloadService.this, "Download Failed", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onPaused()
        {
            downloadTask = null;
            Toast.makeText(DownloadService.this, "Paused", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onCanceled()
        {
            downloadTask = null;
            stopForeground(true);
            Toast.makeText(DownloadService.this, "Canceled", Toast.LENGTH_SHORT).show();
        }
    };

    private DownloadBinder mBinder = new DownloadBinder();

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

    class DownloadBinder extends Binder
    {
        public void startDownload(String url)
        {
            if (downloadTask == null)
            {
                downloadUrl = url;
                downloadTask = new DownloadTask(listener);
                downloadTask.execute(downloadUrl);
                startForeground(1,getNotification("Downloading...",0));
                Toast.makeText(DownloadService.this, "Downloading...",
                        Toast.LENGTH_SHORT).show();
            }
        }

        public void pauseDownload()
        {
            if (downloadTask != null)
            {
                downloadTask.pauseDownload();
            }
        }

        public void cancelDownload()
        {
            if (downloadTask != null)
            {
                downloadTask.cancelDownload();
            }
            else
            {
                if (downloadUrl != null)
                {
                    String filename = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
                    String directory = Environment.getExternalStoragePublicDirectory(Environment
                    .DIRECTORY_DOWNLOADS).getPath();
                    File file = new File(directory + filename);
                    if (file.exists())
                    {
                        file.delete();
                    }
                    getNotificationManager().cancel(1);
                    stopForeground(true);
                    Toast.makeText(DownloadService.this, "canceled", Toast.LENGTH_SHORT).show();
                }
            }
        }
    }

    private NotificationManager getNotificationManager()
    {
        NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        return manager;
    }

    private Notification getNotification(String title,int progress)
    {
        Intent intent = new Intent(this,MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this,0,intent,0);
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
        builder.setSmallIcon(R.mipmap.ic_launcher);
        builder.setLargeIcon(BitmapFactory.decodeResource(getResources(),
                R.mipmap.ic_launcher));
        builder.setContentIntent(pendingIntent);
        builder.setContentTitle(title);
        if (progress > 0)
        {
            //當(dāng)progress大于或等于0時(shí)才需顯示下載進(jìn)度
            builder.setContentText(progress + "%");
            builder.setProgress(100,progress,false);
        }
        return builder.build();
    }
}

首先這里創(chuàng)建了一個(gè)DownloadListener的匿名類實(shí)例堂氯,并在匿名類中實(shí)現(xiàn)了onProgress(),onSuccess(),onFailed(),onPaused()和onCanceled()這5個(gè)方法。在onProgress()方法中牌废,我們調(diào)用getNotification()構(gòu)建了一個(gè)用于顯示下載進(jìn)度的通知咽白,然后調(diào)用NotificationManager的notify()方法去觸發(fā)這個(gè)通知,這樣就可以在下拉狀態(tài)欄中實(shí)時(shí)看到當(dāng)前的下載進(jìn)度了鸟缕。在onSuccess()方法中晶框,我們首先是將正在下載的前臺通知關(guān)掉,然后創(chuàng)建了一個(gè)新的通知用于告訴用戶下載成功了。

接下來為了讓DownloadService可以和活動進(jìn)行通信三妈,我們又創(chuàng)建了一個(gè)DownloadBinder畜埋。DownloadBinder中提供了startDownload(),pauseDownload()和cancelDownload()這三個(gè)方法莫绣,在startDownload()方法中畴蒲,我們創(chuàng)建了一個(gè)DownloadTask的實(shí)例,把剛才的DownloadListener作為參數(shù)傳入对室,然后調(diào)用execute()方法開啟下載模燥,并將下載文件的URL地址傳入到execute()方法中。同時(shí)為了讓這個(gè)下載服務(wù)成為一個(gè)前臺服務(wù)掩宜,我們還調(diào)用了startForeground()方法蔫骂,這樣就會在系統(tǒng)狀態(tài)欄中創(chuàng)建一個(gè)持續(xù)運(yùn)行的通知了。

pausedDownload()方法中的代碼就非常簡單了牺汤,就是簡單地調(diào)用了一下DownloadTask中的pauseDownload()方法辽旋。cancelDownload()方法中的邏輯也基本類似,但是要注意檐迟,取消下載的時(shí)候我們需要將正在下載的文件刪除掉补胚,這一點(diǎn)和暫停下載時(shí)不同的。

DownloadService類中所有使用到的通知都是調(diào)用getNotification()方法進(jìn)行構(gòu)建的追迟,這個(gè)方法中的代碼我們我們之前基本都是學(xué)過的溶其,只有一個(gè)setProgress()方法沒有見過。
setProgress()方法接收3個(gè)參數(shù)敦间,第一個(gè)參數(shù)傳入通知的最大進(jìn)度瓶逃,第二個(gè)參數(shù)傳入通知的當(dāng)前進(jìn)度,第三個(gè)參數(shù)表示是否使用模糊進(jìn)度條廓块,這里傳入false厢绝。設(shè)置完setProgress()方法,通知上就會有進(jìn)度條顯示出來了带猴。

第三部分

public class DownloadTask extends AsyncTask<String,Integer,Integer>
{
    public static final int TYPE_SUCCESS = 0;
    public static final int TYPE_FAILED = 1;
    public static final int TYPE_PAUSED = 2;
    public static final int TYPE_CANCELED = 3;

    private DownloadListener listener;

    private boolean isCanceled = false;

    private boolean isPaused = false;

    private int lastProgress;

    public DownloadTask(DownloadListener listener)
    {
        this.listener = listener;
    }

    @Override
    protected Integer doInBackground(String... params)
    {
        InputStream is = null;
        RandomAccessFile savedFile = null;
        File file = null;

        try
        {
            long downloadLength = 0;
            String downloadUrl = params[0];
            String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
            String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
                    .getPath();
            file = new File(directory + fileName);
            if (file.exists())
            {
                downloadLength = file.length();
            }
            long contentLength = getContentLength(downloadUrl);

            if (contentLength == 0)
            {
                return TYPE_FAILED;
            }
            else if (contentLength == downloadLength)
            {
                return TYPE_SUCCESS;
            }

            OkHttpClient client = new OkHttpClient();
            Request request = new Request.Builder()
                    //斷點(diǎn)下載昔汉,指定從哪個(gè)字節(jié)開始下載
                    .addHeader("RANGE","bytes = " + downloadLength + "-")
                    .url(downloadUrl)
                    .build();
            Response response = client.newCall(request).execute();
            if (response != null)
            {
                is = response.body().byteStream();
                savedFile = new RandomAccessFile(file,"rw");
                savedFile.seek(downloadLength);//跳過已下載的字節(jié)
                byte[] b = new byte[1024];
                int total = 0;
                int len;
                while ((len = is.read(b)) != -1)
                {
                    if (isCanceled)
                    {
                        return TYPE_CANCELED;
                    }
                    else if (isPaused)
                    {
                        return TYPE_PAUSED;
                    }
                    else
                    {
                        total += len;
                        savedFile.write(b,0,len);
                        int progress = (int) ((total + downloadLength) * 100 / contentLength);
                        publishProgress(progress);
                    }
                }
                response.body().close();
                return TYPE_SUCCESS;
            }
        } catch (Exception e)
        {
            e.printStackTrace();
        } finally
        {
            try
            {
                if (is != null)
                {
                    is.close();
                }
                if (savedFile != null)
                {
                    savedFile.close();
                }
                if (isCanceled && file != null)
                {
                    file.delete();
                }
            } catch (Exception e)
            {
                e.printStackTrace();
            }
        }
        return TYPE_FAILED;
    }

    @Override
    protected void onProgressUpdate(Integer... values)
    {
        int progress = values[0];
        if (progress > lastProgress)
        {
            listener.onProgress(progress);
            lastProgress = progress;
        }
    }

    @Override
    protected void onPostExecute(Integer integer)
    {
        switch (integer)
        {
            case TYPE_SUCCESS:
                listener.onSuccess();
                break;
            case TYPE_FAILED:
                listener.onFailed();
                break;
            case TYPE_PAUSED:
                listener.onPaused();
                break;
            case TYPE_CANCELED:
                listener.onCanceled();
                break;
        }
    }

    public void pauseDownload()
    {
        isPaused = true;
    }

    public void cancelDownload()
    {
        isCanceled = true;
    }

    private long getContentLength(String downloadUrl) throws IOException
    {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url(downloadUrl)
                .build();
        Response response = client.newCall(request).execute();
        if (response != null && response.isSuccessful())
        {
            long contentLength = response.body().contentLength();
            response.close();
            return contentLength;
        }
        return 0;
    }
}

首先看一下AsyncTask的3個(gè)泛型參數(shù):第一個(gè)泛型參數(shù)指定為String,表示在執(zhí)行AsyncTask的時(shí)候需要傳入一個(gè)字符串參數(shù)給后臺任務(wù)浓利;第二個(gè)泛型參數(shù)指定為Integer挤庇,表示使用整形數(shù)據(jù)來作為進(jìn)度顯示單位;第三個(gè)泛型參數(shù)指定為Integer,則表示使用整形數(shù)據(jù)來反饋執(zhí)行結(jié)果贷掖。

接下來我們定義了4個(gè)整形常量用于表示下載的狀態(tài)嫡秕,TYPE_SUCCESS表示下載成功,TYEP_FAILED表示下載失敗苹威,TYEP_PAUSED表示暫停下載昆咽,TYPE_CANCELED表示取消下載。然后在DownloadTask的構(gòu)造函數(shù)中要求傳入一個(gè)剛剛定義的DownloadListener參數(shù),我們待會就會將下載的狀態(tài)通過這個(gè)參數(shù)進(jìn)行回調(diào)掷酗。

接著就是重寫doInBackground(),onProgressUpdate()和onPostExecute()這三個(gè)方法了调违,

doInBackground():用于在后臺執(zhí)行具體的下載邏輯

onProgressUpdate():用于在界面上更新當(dāng)前的下載進(jìn)度

onPostExecute():通知最終的下載結(jié)果。

先來看一下doInBackground()方法泻轰,首先我們從參數(shù)中獲取到了下載的URL地址技肩,并根據(jù)URL地址解析出了下載的文件名,然后指定將文件下載到Environment.DIRECTORY_DOWNLOADS目錄下浮声,也就是SD卡的Download目錄虚婿。我們還要判斷一下Download 目錄中是不是已經(jīng)存在要下載的文件了,如果已經(jīng)存在的話則讀取已下載的字節(jié)數(shù)泳挥,這樣就可以在后面啟用斷點(diǎn)續(xù)傳的功能了然痊。接下來先是調(diào)用getContentLength()方法來獲取待下載文件的總長度,如果文件長度等于0則說明文件有問題屉符,直接返回TYPE_FAILED,如果文件長度等于已下載文件長度剧浸,那么就說明已經(jīng)下載完了,直接返回TYPE_SUCCESS即可矗钟。緊接著使用OKhttp來發(fā)送一條網(wǎng)絡(luò)請求唆香,需要注意的是,這里在請求中加入了一個(gè)header真仲,用于告訴服務(wù)器我們想要從哪個(gè)字節(jié)下載袋马,因?yàn)橐严螺d過的部分就不要重新下載了。接下來讀取服務(wù)器響應(yīng)的數(shù)據(jù)秸应,并使用Java的文件流方式虑凛,不斷從網(wǎng)絡(luò)上讀取數(shù)據(jù),不斷寫入到本地软啼,一直到文件下載完成為止桑谍。在這個(gè)過程中,我們還要判斷用戶有沒有觸發(fā)暫突雠玻或者取消的操作锣披,如果有的話則返回TYPE_PAUSED或TYPE_CANCELED來中斷下載,如果沒有的話則實(shí)時(shí)計(jì)算當(dāng)前的下載進(jìn)度贿条,然后調(diào)用publishProgress()方法進(jìn)行通知雹仿。暫停和取消操作都是使用一個(gè)布爾型的變量來進(jìn)行控制的,調(diào)用pauseDownload()或cancelDownload()方法即可更改變量的值整以。

接下來看一下onProgressUpdate()方法胧辽,這個(gè)方法就簡單的多了,它首先從參數(shù)中獲取到當(dāng)前的下載進(jìn)度公黑,然后和上一次的下載進(jìn)度對比邑商,如果有變化的話則調(diào)用DownloadListener的onProgress()方法來通知下載進(jìn)度更新摄咆。

最后是onPostExecute()方法,也非常簡單人断,就是根據(jù)參數(shù)中傳入的下載狀態(tài)來進(jìn)行回調(diào)吭从,下載成功就調(diào)用DownloadListener的onSuccess()方法,下載失敗就調(diào)用onFailed()方法恶迈,暫停下載就調(diào)用onPaused()方法涩金,取消下載就調(diào)用onCeaceled()方法。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蝉绷,一起剝皮案震驚了整個(gè)濱河市鸭廷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌熔吗,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件佳晶,死亡現(xiàn)場離奇詭異桅狠,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)轿秧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進(jìn)店門中跌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人菇篡,你說我怎么就攤上這事漩符。” “怎么了驱还?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵嗜暴,是天一觀的道長。 經(jīng)常有香客問我议蟆,道長闷沥,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任咐容,我火速辦了婚禮舆逃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘戳粒。我一直安慰自己路狮,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布蔚约。 她就那樣靜靜地躺著奄妨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪炊琉。 梳的紋絲不亂的頭發(fā)上展蒂,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天粱檀,我揣著相機(jī)與錄音,去河邊找鬼乏屯。 笑死熊泵,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的箕般。 我是一名探鬼主播耐薯,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼丝里!你這毒婦竟也來了曲初?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤杯聚,失蹤者是張志新(化名)和其女友劉穎臼婆,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體幌绍,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡颁褂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了傀广。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颁独。...
    茶點(diǎn)故事閱讀 38,650評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖伪冰,靈堂內(nèi)的尸體忽然破棺而出誓酒,到底是詐尸還是另有隱情,我是刑警寧澤贮聂,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布靠柑,位于F島的核電站,受9級特大地震影響寂汇,放射性物質(zhì)發(fā)生泄漏病往。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一骄瓣、第九天 我趴在偏房一處隱蔽的房頂上張望停巷。 院中可真熱鬧,春花似錦榕栏、人聲如沸畔勤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽庆揪。三九已至,卻和暖如春妨托,著一層夾襖步出監(jiān)牢的瞬間缸榛,已是汗流浹背吝羞。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留内颗,地道東北人钧排。 一個(gè)月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像均澳,于是被迫代替她去往敵國和親恨溜。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評論 2 349

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,777評論 25 707
  • 9.1服務(wù)是什么 服務(wù)( Service)是 Android 中實(shí)現(xiàn)程序后臺運(yùn)行的解決方案,它適合用于去執(zhí)行不需要...
    wyxjoker閱讀 328評論 0 1
  • 《課程的邏輯》第二章內(nèi)容是課程改革的文化使命躺盛。在新課程實(shí)施中首當(dāng)其沖的项戴,是直接參與課程教材設(shè)計(jì)的編審隊(duì)伍的建...
    徐徐聰閱讀 273評論 0 0
  • 1邱衛(wèi)豪 隨筆 失敗是一種營養(yǎng),讓我們學(xué)會堅(jiān)強(qiáng)颗品,跌倒是一種營養(yǎng)肯尺,讓我們在風(fēng)雨中茁壯成長,挫折是一種...
    跬步堂閱讀 851評論 0 0
  • 昨天是爺爺去世二十周年的日子躯枢,我寫了一篇《沒有告別的永別》,謹(jǐn)以懷念槐臀。爺爺?shù)摹暗靡忾T生”侯保方看后锄蹂,情不自禁,回憶...
    雪琴吟閱讀 600評論 0 3