10 后臺默默地勞動者--探究服務

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

10.1 服務是什么

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

不過需要注意的是,服務并不是運行在一個獨立的進程當中的,而是依賴于創建服務時所在的應用程序進程。當某個應用程序進程被殺掉時,所有依賴于該進程的服務也會停止運行。

另外,也不要被服務的后臺概念所迷惑,實際上服務并不會自動開啟線程,所有的代碼都是默認運行在主線程當中的。也就是說,我們需要在服務的內部手動創建子線程,并在這里執行具體的任務,否則就有可能出現主線程被阻塞住的情況。

10.2.1 線程的基本用法

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

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

只需要newMyThread的實例,然后調用它的start()方法,這樣run()方法中的代碼就會在子線程當中運行了。

new MyThread().start();

當然,使用繼承的方式耦合性有點高,更多的時候我們都會選擇使用實現Runnable接口的方式來定義一個線程。

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

如果使用了這種寫法,啟動線程的方法也需要進行相應的改變。

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

Thread的構造函數接收一個Runnable參數,而我們new出的MyThread正是一個實現了Runnable接口的對象,所以可以直接將它傳入到Thread的構造函數中。接著調用Threadstart()方法,run()方法中的代碼就會在子線程當中運行了。

當然,如果你不想專門再定義一個類去實現Runnable接口,也可以使用匿名類的方式,這種寫法更為常見。

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

10.2.2 在子線程中更新UI

和其他的GUI庫一樣,AndroidUI也是線程不安全的。也就是說,如果想要更新應用程序里的UI元素,則必須在主線程中進行,否則就會出現異常。

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中的錯誤日志,可以看出是由于在子線程中更新UI所導致的

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

由此證實了Android確實是不允許在子線程中進行UI操作的。

有些時候,我們必須在子線程里去執行一些耗時任務,然后根據任務的執行結果來更新相應的UI控件。對于這種情況,Android提供了一套異步消息處理機制,完美地解決了在子線程中進行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:
                        //在這里可以進行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對象發送出去
                    }
                }).start();
                break;
            default:
                break;
        }
    }
}

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

可以看到,這次我們并沒有在子線程里直接進行UI操作,而是創建了一個Message(android.os.Message)對象,并將它的what字段的值指定為UPDATE_TEXT,然后調用HandlersendMessage()方法將這條Message發送出去。很快,Handler就會收到這條Message,并在handleMessage()方法中對它進行處理。注意此時handleMessage()方法中的代碼就是在主線程當中運行的了,所以我們可以放心地在這里進行UI操作。接下來對Message攜帶的what字段的值進行判斷,如果等于UPDATE_TEXT,就將TextView顯示的內容改成Nice to meet you.

10.2.3 解析異步消息處理機制

Android中的異步消息處理主要由四部分組成:MessageHandlerMessageQueueLooper

1.Message

Message是在線程之間傳遞的消息,它可以在內部攜帶少量的信息,用于在不同線程之間交換數據。上面我們使用到了Messagewhat字段,除此之外還可以使用arg1arg2字段來攜帶一些整形數據,使用obj字段攜帶一個Object對象。

2.Handler

Handler顧名思義也就是處理者的意思,它主要是用于發送和處理消息。發送消息一般是使用HandlersendMessage()方法,而發出的消息經過一系列輾轉處理后,最終會傳遞到HandlerhandleMessage()方法中。

3.MessageQueue

MessageQueue是消息隊列的意思,它主要用于存放所有通過Handler發送的消息。這部分消息會一直存在于消息隊列中,等待被處理。每個線程中只會有一個MessageQueue對象。

4.Looper

Looper是每個線程中的MessageQueue的管家,調用Looperloop()方法后,就會進入到一個無限循環當中,然后每當發現MessageQueue中存在一條消息,就會將它取出,并傳遞到HandlerhandleMessage()方法中。每個線程中也只會有一個Looper對象。

異步消息處理機制流程圖

我們來把異步消息處理的整個流程梳理一遍。首先需要在主線程當中創建一個Handler對象,并重寫handleMessage()方法。然后當子線程中需要進行UI操作時,就創建一個Message對象,并通過Handler將這條消息發送出去。之后這條消息會被添加到MessageQueue的隊列中等待被處理,而Looper則會一直嘗試從MessageQueue中取出待處理消息,最后分發回HandlerhandleMessage()方法中。由于Handler是在主線程中創建的,所以此時handleMessage()方法中的代碼也會在主線程中運行,于是我們在這里就可以安心地進行UI操作了。

一條Message經過這樣一個流程的輾轉調用后,也就從子線程進入到了主線程,從不能更新UI變成了可以更新UI,整個異步消息處理的核心思想也就是如此。

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

10.2.4 使用AsyncTask

為了更加方便我們在子線程中對UI進行操作,Android還提供了另外一些好用的工具,比如AsyncTask。借助AsyncTask,即便你對異步消息處理機制完全不了解,也可以十分簡單地從子線程切換到主線程。當然,AsyncTask背后的實現原理也是基于異步消息處理機制的,只是Android幫我們做了很好地封裝而已。

AsyncTask的基本用法,由于AsyncTask是一個抽象類,所以如果我們想使用它,就必須要創建一個子類去繼承它。在繼承時我們可以為AsyncTask類指定3個泛型參數。

Params: 在執行AsyncTask時需要傳入的參數,可用于在后臺任務中執行。

Progress: 后臺任務執行時,如果需要在界面上顯示當前的進度,則使用這里指定的泛型作為進度單位。

Result: 當任務執行完畢后,如果需要對結果進行返回,則使用這里指定的泛型作為返回值類型。

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

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

我們把AsyncTask的第一個泛型參數指定為Void,表示在執行AsyncTask的時候不需要傳入參數給后臺任務。第二個泛型參數指定為Integer,表示使用整形數據來做為進度顯示單位。第三個泛型參數指定為Boolean,則表示使用布爾型數據來反饋執行結果。

目前我們自定義的DownloadTask還是一個空任務,并不能進行任何的操作,我們還需要去重寫AsyncTask中的幾個方法才能完成對任務的定制。

1.onPreExecute()

這個方法會在后臺任務開始之前調用,用于一些界面上的初始化操作,比如顯示一個進度條對話框等。

2.doInBackground(Params...)
這個方法中的所有代碼都會在子線程中運行,我們應該在這里去處理所有的耗時任務。任務一旦完成就可以通過return語句來將任務的執行結果返回,如果AsyncTask的第三個泛型參數指定的是Void,就可以不返回任務執行結果。注意:在這個方法中是不可以進行UI操作的,如果需要更新UI元素,比如說反饋當前任務的執行進度,可以調用publishProgress(Progress...)方法來完成

3.onProgressUpdate(Progress...)
當在后臺任務中調用了publishProgress(Progress...)方法后,onProgressUpdate(Progress...)方法就會很快被調用,該方法中攜帶的參數就是在后臺任務中傳遞過來的。在這個方法中可以對UI進行操作,利用參數中的數值就可以對界面元素進行相應的更新。

4.onPostExecute(Result)
當后臺任務執行完畢通過return語句進行返回時,這個方法就很快被調用。返回的數據會作為參數傳遞到此方法中,可以利用返回的數據來進行一些UI的操作,比如說提醒任務執行的結果,以及關閉掉進度對話框等。

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();//這是一個虛構的方法
                    publishProgress(downloadPercent);
                    if (downloadPercent >= 100)
                    {
                        break;
                    }
                }
            } catch (Exception e)
            {
                return false;
            }
            return true;
        }

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

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

在這個DownloadTask中,我們在doInBackground()方法中去執行具體的下載任務。這個方法里的代碼都是在子線程中運行的,因而不會影響到主線程的運行。注意這里虛構了一個doDownload()方法,這個方法用于計算當前的下載進度并返回,我們假設這個方法已經存在了。在得到了當前的下載進度后,下面就該考慮如何把它顯示到界面上了,由于doInBackground()方法是在子線程中運行的,在這里肯定不能進行UI操作,所以我們可以調用publishProgress()方法并將當前的下載進度傳進來,這樣onProgressUpdate()方法就會很快被調用,在這里就可以進行UI操作了。

當下載完成后,doInBackground()方法會返回一個布爾型變量,這樣onPostExecuet()方法就會很快被調用,這個方法也是在主線程中運行的。然后在這里我們會根據下載的結果來彈出相應的Toast提示,從而完成整個DownloadTask任務。

使用AsyncTask的訣竅就是,在doInBackground()方法中執行具體的耗時任務,在onProgressUpdate()方法中進行UI操作,在onPostExecute()方法中執行一些任務的收尾工作。

如果想要啟動這個任務,只需編寫以下代碼:

new DownloadTask().execute()

服務的基本用法

定義一個服務

image

我們將服務命名為MyService,Exported屬性表示是否允許除了當前程序以外的其他程序訪問這個服務,Enabled屬性表示是否啟用這個服務。

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

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

MyService是繼承自Service類的,說明這是一個服務。目前MyService中可以算是空空如也,但有一個onBind()方法特別醒目。這個方法是Service中唯一的一個抽象方法,所以必須要在子類里實現。

我們在服務中處理一些事情,處理事情的邏輯寫在哪呢?這時就可以重寫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個方法,他們是每個服務中最常用到的3個方法了。其中onCreate()方法會在服務創建的時候調用,onStartCommand()方法會在每次服務啟動的時候調用,onDestroy()方法會在服務銷毀的時候調用。

通常情況下,如果我們希望服務一旦啟動就立刻去執行某個動作,就可以將邏輯寫在onStartCommand()方法中。而當服務銷毀時,我們又應該在onDestroy()方法中回收那些不再使用的資源。

另外需要注意的是,每一個服務都需要在AndroidManifest.xml文件中進行注冊才能生效。

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

10.3.2 啟動和停止服務

啟動服務和停止服務的方法當然你也不會陌生,主要是借助Intent來實現的。

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

onCreate()方法中分別獲取到了Start Service按鈕和Stop Service按鈕的實例,并給他們注冊了點擊事件。然后在Start Service按鈕的點擊事件里我們構建出了一個Intent對象,并調用startService()方法來啟動MyService服務。

Stop Service按鈕的點擊事件里我們同樣構建出了一個Intent對象,并調用stopService()方法來停止MyService服務。

startService()stopService()方法都是定義在Context類中的,所以我們在活動里可以直接調用這兩個方法。注意,這里完全是由活動來決定服務何時停止的,如果沒有點擊Stop Service按鈕,服務就會一直處于運行狀態。那服務有沒有什么辦法讓自己停止下來呢?當然可以,只需要在MyService的任何一個位置調用stopSelf()方法就能讓這個服務停止下來了。

onCreate()方法是在服務第一次創建的時候調用的,而onStartCommand()方法則在每次啟動服務的時候都會調用,由于剛才我們是第一次點擊Start Service按鈕,服務此時還未創建過所以兩個方法都會執行,之后如果你再連續多點擊幾次Start Service按鈕,你就會發現只有onStartCommand()方法可以得到執行了。

10.3.3 活動和服務進行通信

上一節中雖然服務是在活動里啟動的,但在啟動了服務之后,活動和服務基本就沒有什么關系了。

服務會一直處于運行狀態,但具體是運行的什么邏輯,活動就控制不了了。這就類似于活動通知了服務一下:“你可以啟動了”,然后服務就去忙自己的事情了,但活動并不知道服務到底去做了什么事情,以及完成的如何。

創建一個專門的Binder對象來對下載功能進行管理。

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;
    }
}

我們新建了一個DownloadBinder類,并讓它繼承自Binder,然后在它的內部提供了開始下載以及查看下載進度的方法。當然這只是兩個模擬方法,并沒有實現真正的功能,我們在這兩個方法中分別打印了一行日志。

當一個活動和服務綁定之后,就可以調用該服務里的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);//啟動服務
                break;
            case R.id.stop_service:
                Intent stopIntent = new Intent(this,MyService.class);
                stopService(stopIntent);//停止服務
                break;
            case R.id.bind_service:
                Intent bindIntent = new Intent(this,MyService.class);
                bindService(bindIntent,connection,BIND_AUTO_CREATE);//綁定服務
                break;
            case R.id.unbind_service:
                unbindService(connection);//解綁服務
                break;
            default:
                break;
        }
    }
}

我們首先創建了一個ServiceConnection的匿名類,在里面重寫了onServiceConnected()方法和onServiceDisconnected()方法,這兩個方法分別會在活動與服務成功綁定以及解除綁定的時候調用。在onServiceConnected()方法中,我們又通過向下轉型得到了DownloadBinder的實例,有了這個實例,活動和服務之間的關系就變得非常緊密了。現在我們可以在活動中根據具體的場景來調用DownloadBinder中的任何public方法,即實現了指揮服務干什么服務就去干什么的功能。

我們這里仍然是構建出了一個Intent對象,然后調用bindService()方法將MainActivityMyService進行綁定。bindService()方法接收三個參數,第一個參數就是剛剛構建出的Intent對象,第二個參數是前面創建出的ServiceConnection的實例,第三個參數則是一個標志位,這里傳入BIND_AUTO_CREATE表示在活動和服務進行綁定后自動創建服務。這會使得MyService中的onCreate()方法得到執行,但是onStartCommand()方法不會執行。

解除服務和活動之間的綁定,調用一下unbindService()方法就可以了。

另外需要注意,任何一個服務在整個應用程序范圍內都是通用的,即MyService不僅可以和MainActivity綁定,還可以和任何一個其他的活動進行綁定,而且在綁定完成后他們都可以獲取到相同的DownloadBinder實例。

10.4 服務的生命周期

服務有自己的生命周期。

一旦在項目的任何位置調用了ContextstartService()方法,相應的服務就會啟動起來,并回調onStartCommand()方法。如果這個服務之前還沒有創建過,onStart()方法會先于onStartCommand()方法執行。服務啟動了之后會一直保持運行狀態,直到stopService()stopSelf()方法被調用。注意,雖然每調用一次startService()方法,onStartCommand()就會執行一次,但實際上每個服務都只會存在一個實例。所以不管你調用了多少次startService()方法,只需調用一次stopService()stopSelf()方法,服務就會停止下來了。

另外,還可以調用ContextbindService()來獲取一個服務的持久連接,這時就會回調服務中的onBind()方法。類似里,如果這個服務之前還沒有創建過,onCreate()方法會先于onBind()方法執行。之后,調用方可以獲取到onBind()方法里返回的IBinder對象的實例,這樣就能自由地和服務進行通信了。只要調用方和服務之間的連接沒有斷開,服務就會一直保持運行狀態。

當調用了startService()方法后,又去調用stopService()方法,這時服務中的onDestroy()方法就會執行,表示服務已經銷毀了。類似地,當調用了bindService()方法后,又去調用unbindService()方法,onDestroy()方法也會執行。這兩種情況都很好理解。但是需要注意,我們是完全有可能對一個服務即調用了startService()方法,又調用了bindService()方法,這種下情況該如何讓服務銷毀呢?根據Android系統的機制,一個服務只要被啟動或者被綁定了之后,就會一直處于運行狀態,必須要讓以上兩種條件同時不滿足,服務才能被銷毀。所以,這種情況下要同時調用stopService()unbindService()方法,onDestroy()方法才能執行。

這樣你就已經把服務的生命周期完整里走了一遍。

10.5 服務的更多技巧

10.5.1 使用前臺服務

服務幾乎都是在后臺運行的,一直以來它都是默默地做著辛苦的工作。但是服務的系統優先級還是比較低的,當系統出現內存不足的情況時,就有可能回收掉正在后臺運行的服務。如果你希望服務可以一直保持運行狀態,而不會由于系統內存不足的原因導致被回收,就可以考慮使用前臺服務。

前臺服務和普通服務最大的區別就在于,它會有一個正在運行的圖標在系統的狀態欄顯示,下拉狀態欄后可以看到更加詳細的信息,非常類似于通知的效果。當然有時候你也可能不僅僅是為了防止服務被回收掉才使用前臺服務的,有些項目由于特殊的需求會要求必須使用前臺服務,比如說彩云天氣這款天氣預報應用,它的服務在后臺更新天氣數據的同時,還會在系統狀態欄一直顯示當前的天氣信息。

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);
    }

    ``````
}

這次在構建出Notification對象后并沒有使用NotificationManager來將通知顯示出來,而是調用了startForeground()方法。這個方法接收兩個參數,第一個參數是通知的id,類似于notify()方法的第一個參數,第二個參數則是構建出的Notification對象。調用startForeground()方法后就會讓MyService變成一個前臺服務,并在系統狀態顯示出來。

10.5.2 使用IntentService

服務中的代碼都是默認運行在主線程當中的,如果直接在服務里去處理一些耗時的邏輯,就很容易出現ANR(Application Not Responding)的情況。

所以這個時候就需要用到Android多線程編程的技術了,我們應該在服務的每個具體的方法里開啟一個子線程,然后在這里去處理那些耗時的邏輯。因此,一個比較標準的服務就可以寫成如下形式:

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);
    }

}

但是,這個服務一旦啟動之后,就會一直處于運行狀態,必須調用stopService()或者stopSelf()方法才能讓服務停止下來。所以,如果想要實現讓一個服務在執行完畢后自動停止的功能,就可以這樣寫:

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);
    }

}

為了可以簡單里創建一個異步的,會自動停止的服務,Android專門提供了一個IntentService類。

public class MyIntentService extends IntentService {

    private static final String TAG = "MyIntentService";
    
    public MyIntentService() {
        super("MyIntentService");//調用父類的有參構造函數
    }

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

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

這里首先要提供一個無參的構造函數,并且必須在其內部調用父類的有參構造函數。然后要在子類中去實現onHandleIntent()這個抽象方法,在這個方法中可以去處理一些具體的邏輯,而且不用擔心 ANR的問題,因為這個方法已經是在子線程中運行的了。這里為了證實一下,我們在onHandleIntent()方法中打印了當前線程的id。另外根據IntentService的特性,這個服務在運行結束后應該是會自動停止的,所以我們又重寫了onDestroy()方法,在這里也打印了一行日志,以證實服務是不是停止掉了。

 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;

在按鈕的點擊事件里面去啟動MyIntentService這個服務,并在這里打印了一下主線程的id,稍后用于和IntentService進行對比。你會發現,其實IntentService的用法和普通的服務沒什么兩樣。

最后不要忘記,服務都是需要在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);//啟動服務
        bindService(intent,connection,BIND_AUTO_CREATE);//綁定服務

        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, "拒絕權限將無法使用程序!",
                            Toast.LENGTH_SHORT).show();
                    finish();
                }
                break;
            default:
                break;
        }
    }

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

這里我們首先創建一個ServiceConnection的匿名類,然后在onServiceConnected()方法中獲取到DownloadBinder的實例,有了這個實例,我們就可以在活動中調用服務提供的各種方法了。

在onCreate()方法中,對按鈕進行了初始化操作并設置了點擊事件,然后分別調用了startService()和bindService()方法來啟動和綁定服務。這一點至關重要,因為啟動服務可以保證DownloadService一直在后臺運行,綁定服務則可以讓MainActivity和DownloadService進行通信,因此兩個方法調用都必不可少。在onCreate()方法的最后,我們還進行了WRITE_EXTERNAL_STORAGE的運行時權限申請,因為下載文件是要下載到SD卡的Download目錄下的,如果沒有這個權限的話,我們整個程序就都無法正常工作。

在onClick()方法中我們對點擊事件進行判斷,如果點擊了開始按鈕就調用DownloadBinder的startDownload()方法,如果點擊了暫停按鈕就調用pauseDownload()方法,如果點擊了取消按鈕就調用cancelDownload()方法。startDownload()方法中你可以傳入任意的下載地址。

另外還有一點需要注意,如果活動被銷毀了,那么一定要記得對服務進行解綁,不然就有可能會造成內存泄露。

第二部分

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;
            //下載成功時將前臺服務通知關閉,并創建一個下載成功的通知
            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;
            //下載失敗時將前臺服務通知關閉,并創建一個下載失敗的通知
            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)
        {
            //當progress大于或等于0時才需顯示下載進度
            builder.setContentText(progress + "%");
            builder.setProgress(100,progress,false);
        }
        return builder.build();
    }
}

首先這里創建了一個DownloadListener的匿名類實例,并在匿名類中實現了onProgress(),onSuccess(),onFailed(),onPaused()和onCanceled()這5個方法。在onProgress()方法中,我們調用getNotification()構建了一個用于顯示下載進度的通知,然后調用NotificationManager的notify()方法去觸發這個通知,這樣就可以在下拉狀態欄中實時看到當前的下載進度了。在onSuccess()方法中,我們首先是將正在下載的前臺通知關掉,然后創建了一個新的通知用于告訴用戶下載成功了。

接下來為了讓DownloadService可以和活動進行通信,我們又創建了一個DownloadBinder。DownloadBinder中提供了startDownload(),pauseDownload()和cancelDownload()這三個方法,在startDownload()方法中,我們創建了一個DownloadTask的實例,把剛才的DownloadListener作為參數傳入,然后調用execute()方法開啟下載,并將下載文件的URL地址傳入到execute()方法中。同時為了讓這個下載服務成為一個前臺服務,我們還調用了startForeground()方法,這樣就會在系統狀態欄中創建一個持續運行的通知了。

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

DownloadService類中所有使用到的通知都是調用getNotification()方法進行構建的,這個方法中的代碼我們我們之前基本都是學過的,只有一個setProgress()方法沒有見過。
setProgress()方法接收3個參數,第一個參數傳入通知的最大進度,第二個參數傳入通知的當前進度,第三個參數表示是否使用模糊進度條,這里傳入false。設置完setProgress()方法,通知上就會有進度條顯示出來了。

第三部分

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()
                    //斷點下載,指定從哪個字節開始下載
                    .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);//跳過已下載的字節
                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個泛型參數:第一個泛型參數指定為String,表示在執行AsyncTask的時候需要傳入一個字符串參數給后臺任務;第二個泛型參數指定為Integer,表示使用整形數據來作為進度顯示單位;第三個泛型參數指定為Integer,則表示使用整形數據來反饋執行結果。

接下來我們定義了4個整形常量用于表示下載的狀態,TYPE_SUCCESS表示下載成功,TYEP_FAILED表示下載失敗,TYEP_PAUSED表示暫停下載,TYPE_CANCELED表示取消下載。然后在DownloadTask的構造函數中要求傳入一個剛剛定義的DownloadListener參數,我們待會就會將下載的狀態通過這個參數進行回調。

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

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

onProgressUpdate():用于在界面上更新當前的下載進度

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

先來看一下doInBackground()方法,首先我們從參數中獲取到了下載的URL地址,并根據URL地址解析出了下載的文件名,然后指定將文件下載到Environment.DIRECTORY_DOWNLOADS目錄下,也就是SD卡的Download目錄。我們還要判斷一下Download 目錄中是不是已經存在要下載的文件了,如果已經存在的話則讀取已下載的字節數,這樣就可以在后面啟用斷點續傳的功能了。接下來先是調用getContentLength()方法來獲取待下載文件的總長度,如果文件長度等于0則說明文件有問題,直接返回TYPE_FAILED,如果文件長度等于已下載文件長度,那么就說明已經下載完了,直接返回TYPE_SUCCESS即可。緊接著使用OKhttp來發送一條網絡請求,需要注意的是,這里在請求中加入了一個header,用于告訴服務器我們想要從哪個字節下載,因為已下載過的部分就不要重新下載了。接下來讀取服務器響應的數據,并使用Java的文件流方式,不斷從網絡上讀取數據,不斷寫入到本地,一直到文件下載完成為止。在這個過程中,我們還要判斷用戶有沒有觸發暫停或者取消的操作,如果有的話則返回TYPE_PAUSED或TYPE_CANCELED來中斷下載,如果沒有的話則實時計算當前的下載進度,然后調用publishProgress()方法進行通知。暫停和取消操作都是使用一個布爾型的變量來進行控制的,調用pauseDownload()或cancelDownload()方法即可更改變量的值。

接下來看一下onProgressUpdate()方法,這個方法就簡單的多了,它首先從參數中獲取到當前的下載進度,然后和上一次的下載進度對比,如果有變化的話則調用DownloadListener的onProgress()方法來通知下載進度更新。

最后是onPostExecute()方法,也非常簡單,就是根據參數中傳入的下載狀態來進行回調,下載成功就調用DownloadListener的onSuccess()方法,下載失敗就調用onFailed()方法,暫停下載就調用onPaused()方法,取消下載就調用onCeaceled()方法。

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

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,715評論 25 708
  • 9.1服務是什么 服務( Service)是 Android 中實現程序后臺運行的解決方案,它適合用于去執行不需要...
    wyxjoker閱讀 342評論 0 1
  • 《課程的邏輯》第二章內容是課程改革的文化使命。在新課程實施中首當其沖的,是直接參與課程教材設計的編審隊伍的建...
    徐徐聰閱讀 280評論 0 0
  • 1邱衛豪 隨筆 失敗是一種營養,讓我們學會堅強,跌倒是一種營養,讓我們在風雨中茁壯成長,挫折是一種...
    跬步堂閱讀 884評論 0 0
  • 昨天是爺爺去世二十周年的日子,我寫了一篇《沒有告別的永別》,謹以懷念。爺爺的“得意門生”侯保方看后,情不自禁,回憶...
    雪琴吟閱讀 611評論 0 3