入職小白隨筆之Android四大組件——服務(Service)

Service

Android多線程編程

當咱們在程序中執行一些耗時操做時,好比發起一條網絡請求,考慮到網速等緣由,服務器未必會馬上響應咱們的請求,此時咱們就須要將這些操做放在子線程中去運行,以防止主線程被阻塞。android

線程的基本用法

新建一個類繼承自Thread,而後重寫父類的run()方法編程

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

如何啓動這個線程呢,只須要new出一個MyThread的實例,而後調用它的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 正是一個實現了Runable接口的對象,因此能夠將它直接傳入Thread的構造函數裏。接着調用Thread的start()方法,run()方法中的代碼就會在子線程中運行了。
固然若是不想專門定義一個類去實現Runnable接口,也可使用匿名類的方式實現多線程

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

在子線程中更新UI

和許多其餘的GUI庫同樣,Android的UI也是線程不安全的,也就是說想要更新程序裏的UI元素,必須在主線程中進行,不然就會出現異常。app

下面經過一個具體的例子來驗證一下。新建一個AndroidThreadTest項目,修改activity_main.xml中的代碼:異步

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/change_text"
        android:text="Change Text"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:id="@+id/text"
        android:layout_centerInParent="true"
        android:textSize="20sp"/>

</RelativeLayout>

佈局文件中定義了兩個控件:TextView用於在屏幕的正中央顯示一個Hello World字符串,Button用於改變TextView中顯示的內容。咱們但願在點擊Button後能夠把TextView中顯示的字符串改爲Nice to meet you。修改MainActivity中的代碼:async

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private TextView text;

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

        text = (TextView)findViewById(R.id.text);
        Button changeText = (Button) findViewById(R.id.change_text);
        changeText.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;
        }
    }
}

咱們在Change Text按鈕點擊事件裏開啓了一個子線程,而後在子線程中調用TextView的setText()方法將顯示的字符串改爲Nice to meet you。代碼邏輯很簡單,不過咱們是在子線程中更新UI的。運行程序發現程序崩潰了:

從日誌中咱們能夠看出是因爲在子線程中更新UI致使的。對於這種狀況,Android提供了一套異步消息處理機制,完美的解決了在子線程中進行UI操做的問題。ide

解析異步消息處理機制

修改MainActivity中的代碼:

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private TextView text;

    public static final int UPDATE_TEXT = 1;

    private Handler handler = new Handler(){
        public void handleMessage(Message msg){
            switch (msg.what){
                case UPDATE_TEXT:
                    //在這裏進行UI操做
                    text.setText("Nice to meet you");
                    break;
                default:
                    break;
            }
        }
    };

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

        text = (TextView)findViewById(R.id.text);
        Button changeText = (Button) findViewById(R.id.change_text);
        changeText.setOnClickListener(this);
    }

    @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 = UPDATE_TEXT;
                        handler.sendMessage(message);//將Message對象發送出去
                    }
                }).start();
                break;
            default:
                break;
        }
    }
}

這裏咱們先是定義了一個整型常量UPDATE_TEXT,用於表示更新TextView這個動做。而後新增一個Handler對象,並重寫父類的handleMessage()方法,在這裏對具體的Message進行處理。若是發現Message的what字段的值等於UPDATE_TEXT,就將 TextView顯示的內容改爲Nice to meet you。下面再來看一下 Change Text按鈕的點擊事件中的代碼。能夠看到,此次咱們並無在子線程裏直接進行UI操做。而是建立了一個Message(android.os.Message)對象,並將它的what字段的值指定爲UPDATE_TEXT
而後調用Handler的sendMessage()方法將這條Message發送出去。很快,Handler就會收到這條Message,並在handleMessage()方法中對它進行處理。注意此時handleMessage()方法中的代碼就是在主線程當中運行的了,因此咱們能夠放心地在這裏進行UI操做。接下來對Message攜帶的what字段的值進行判斷,若是等於UPDATE_TEXT,就將 TextView顯示的內容改爲 Nice to meet you。如今從新運行程序,發現UI確實更新了。
下面來分析一下Android異步消息處理機制是如何工做的
Android異步消息處理主要由4個部分組成:Message、Handle、MessageQueue和Looper。

  • Message:Message是在線程之間傳遞的消息,它能夠在內部攜帶少許的信息,用於在不一樣線程之間交換數據。前面咱們使用到了Message的what字段,除此以外還可使用arg1和arg2字段來攜帶一些整型數據,使用obj字段攜帶一個Object對象。
  • Handler:Handler顧名思義也就是處理者的意思,它主要用於發送和處理消息的。發送消息通常是使用Handler的sendMessage()方法,而發出的消息通過一系列的展轉處理後,最終會傳遞到Handler的handleMessage()方法中。
  • MessageQueue:MessageQueue是消息隊列的意思,它主要用於存放全部經過Handler發送的消息。這部分消息會一直存在於消息隊列中,等待被處理。每一個線程中只會有一個MessageQueue對象。
  • Looper:Looper是每一個線程中的MessageQueue的管家,調用Looper的loop()方法後,就會進入到一個無限循環當中,而後每當發現MessageQueue中存在一條消息,就會將它取出,並傳遞到Handler的handleMessage()方法中。每一個線程中也只會有一個Looper對象。

瞭解了 Message、 Handler、 Messagequeue以及 Looper的基本概念後,咱們再來把異步消息處理的整個流程梳理一遍。
首先須要在主線程當中建立一個Handler對象,並重寫handMessage()方法。而後當子線程中須要進行UI操做時,就建立一個 Message對象,並經過Handler將這條消息發送出去。
以後這條消息會被添加到 MessageQueue的隊列中等待被處理,而Looper則會一直嘗試從MessageQueue中取出待處理消息,最後分發回Handler的handleMessage()方法中。
因爲Handler是在主線程中建立的,因此此時handleMessage()方法中的代碼也會在主線程中運行,因而咱們在這裏就能夠安心地進行UI操做了。
整個異步消息處理機制的流程示意圖如圖所示:

使用AsyncTask

爲了更加方便在子線程中對UI進行更新,Android提供了一些好用的工具,好比AsyncTask。藉助AsyncTask,即便對異步消息處理機制徹底不瞭解,也能夠很簡單的從子線程切換到主線程。因爲AsyncTask是一個抽象類,因此若是咱們想使用它,就必須建立一個子類去繼承它。在繼承時咱們能夠爲AsyncTask類指定3個泛型參數:

  • Params:在執行AsyncTask時須要傳入的參數,可用於在後臺任務中使用
  • Progress:後臺任務執行時,若是須要在界面上顯示當前進度,則使用這裏指定的泛型做爲進度單位
  • Result:當任務執行完畢後,若是須要對結果進行返回,則使用這裏指定的泛型做爲返回值類型

所以一個最簡單的自定義AsyncTask就能夠寫成以下方式:

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

這裏咱們把AsyncTask的
第一個泛型參數指定爲Void,表示在執行AsyncTask的時候不須要傳人蔘數給後臺任務。
第二個泛型參數指定爲Integer,表示使用整型數據來做爲進度顯示單位。
第三個泛型參數指定爲Boolean,則表示使用布爾型數據來反饋執行結果。
固然,目前咱們自定義的DownloadTask仍是一個空任務,並不能進行任何實際的操做,咱們還須要去重寫 AsyncTask中的幾個方法才能完成對任務的定製。
常常須要去重寫的方法有如下4個。

  • onPreExecute()
    這個方法會在後臺任務開始執行以前調用,用於進行一些界面上的初始化操做,好比顯示個進度條對話框等。
  • doInBackground(Params ..)
    這個方法中的全部代碼都會在子線程中運行,咱們應該在這裏去處理全部的耗時任務。任務一旦完成就能夠經過return語句來將任務的執行結果返回,若是AsyncTask的第三個泛型參數指定的是Void,就能夠不返回任務執行結果。注意,在這個方法中是不能夠進行UI操做的,若是須要更新UI元素,好比說反饋當前任務的執行進度,能夠調用publishProgress(Progress..)方法來完成
  • onProgressUpdate(Progress ...)
    當在後臺任務中調用了publishProgress(Progress...)方法後,onProgressUpdate(Progress...)方法就會很快被調用,該方法中攜帶的參數就是在後臺任務中傳遞過來的。在這個方法中能夠對UI進行操做,利用參數中的數值就能夠對界面元素進行相應的更新。
  • onPostExecute(Result)
    當後臺任務執行完畢並經過return語句進行返回時,這個方法就很快會被調用。返回的數據會做爲參數傳遞到此方法中,能夠利用返回的數據來進行一些UI操做,好比說提醒任務執行的結果,以及關閉掉進度條對話框等。
    所以,一個比較完整的自定義AsyncTask就能夠寫成以下方式:
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 result){
        progressDialog.dismiss();
        if(result){
            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()方法會返回一個布爾型變量,這樣onPostExecute()方法就會很快被調用,這個方法也是在主線程中運行的。而後在這裏咱們會根據下載的結果來彈出相應的Toast提示,從而完成整個DownloadTask任務。簡單來講,使用AsyncTask的訣竅就是,在doInBackground()方法中執行具體的耗時任務,在onProgressUpdate()方法中進行UI操做,在onPostExecute()方法中執行一些任務的收尾工做。
若是想要啓動這個任務,只需編寫如下代碼便可:

new DownloadTask().execute();

以上就是AsyncTask的基本用法,怎麼樣,是否是感受簡單方便了許多?咱們並不須要去考慮什麼異步消息處理機制,也不須要專門使用一個Handler來發送和接收消息,只須要調用一下publishProgress()方法,就能夠輕鬆地從子線程切換到UI線程了。

服務的基本用法

定義一個服務

咱們首先新建一個ServiceTest項目,而後右擊com.example.servicetest新建一個服務。服務命名爲MyService,Exported屬性表示是否容許除了當前以外的其餘程序訪問這個服務Enabled屬性表示是否啓用這個服務,這裏將兩個屬性都選上。下面是自動生成的代碼:

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

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }
}

能夠看到,MyService是繼承自Service類的,說明這是一個服務,其中有個onBind()方法,這是Service中惟一的一個抽象方法,因此必須在子類中實現,下面咱們重寫Service中的另一些方法:

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

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }
    
    @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文件中進行註冊才能生效,不知道你有沒有發現,這是Android四大組件共有的特色。不過相信你已經猜到了,智能的Android Studio早已自動幫咱們將這一步完成了。打開AndroidManifest.rml文件瞧一瞧,代碼以下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.servicetest">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

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

    </application>
</manifest>

這樣的話就已經將一個服務定義好了。

啓動和中止服務

接下來咱們考慮如何去啓動以及中止這個服務。修改activity_main.xml代碼:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content" 
        android:id="@+id/start_service"
        android:text="Start Service"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/stop_service"
        android:text="Stop Service"/>
    
</LinearLayout>

修改MainActivity:

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

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

        Button startService = (Button) findViewById(R.id.start_service);
        Button stopService = (Button) findViewById(R.id.stop_service);
        startService.setOnClickListener(this);
        stopService.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;
            default:
                break;
        }
    }
}

能夠看到:這裏在onCreate()方法中分別獲取到了Start Service按鈕和Stop Service按鈕的實例,並給它們註冊了點擊事件。而後在Start Service按鈕的點擊事件裏,咱們構建出了一個Intent對象,並調用startService()方法來啓動 MyService這個服務。在Stop Serivce按鈕的點擊事件裏,咱們一樣構建出了一個Intent對象,並調用stopService()方法來中止 MyService這個服務。startService()和stopService()方法都是定義在 Context類中的,因此咱們在活動裏能夠直接調用這兩個方法。注意,這裏徹底是由活動來決定服務什麼時候中止的,若是沒有點擊Stop Service按鈕,服務就會一直處於運行狀態。那服務有沒有什辦法讓自已中止下來呢?固然能夠,只須要在MyService的任何一個位置調用stopSelf()方法就能讓這個服務中止下來了。

活動和服務進行通訊

雖然服務是在活動裏啓動的,但在啓動了服務以後,活動與服務基本就沒有什麼關係了。咱們在活動裏調用startService()方法來啓動MyService這個服務,而後MyService的onCreate()和onStartCommand()方法就會獲得執行,以後服務會一直處於運行狀態,但具體運行的是什麼邏輯,活動就控制不了了。這就相似於活動通知了服務一下:「你能夠啓動了!」而後服務就去忙本身的事情了,但活動並不知道服務到底去作了什麼事情,以及完成得如何。那麼有沒有什麼辦法能讓活動和服務的關係更緊密一些呢?例如在活動中指揮服務去幹什麼,服務就去幹什麼。固然能夠,這就須要藉助咱們剛剛忽略的onBind()方法了。好比說,目前咱們但願在MyService裏提供一個下載功能,而後在活動中能夠決定什麼時候開始下載,以及隨時查看下載進度。實現這個功能的思路是建立一個專門的Binder對象來對下載功能進行管理。修改MyService中的代碼:

能夠看到,這裏咱們新建了一個DownloadBinder類,並讓它繼承自Binder,而後在它的內部提供了開始下載以及查看下載進度的方法。固然這只是兩個模擬方法,並無實現真正的功能,咱們在這兩個方法中分別打印了一行日誌。接着,在MyService中建立了DownloadBinder的實例,而後在onBind()方法裏返回了這個實例,這樣MyService中的工做就所有完成了。下面就要看一看,在活動中如何去調用服務裏的這些方法了。先須要在佈局文件裏新增兩個按鈕,修改 activity_main.xml中的代碼:

這兩個按鈕分別是用於綁定服務和取消綁定服務的,那究竟是誰須要去和服務綁定呢?固然就是活動了,當一個活動和服務綁定了以後,就能夠調用該服務裏的Binder提供的方法了。修改MainActivity:

能夠看到,這裏咱們首先建立了一個ServiceConnection的匿名類,在裏面重寫了onServiceConnected()方法和onServiceDisconnected()方法,這兩個方法分別會在活動與服務成功綁定以及解除綁定的時候調用。
在onServiceConnected()方法中,咱們又經過向下轉型獲得了Down loadBinder的實例,有了這個實例,活動和服務之間的關係就變得很是緊密了。如今咱們能夠在活動中根據具體的場景來調用DownloadBinder中的任何public()方法,即實現了指揮服務幹什麼服務就去幹什麼的功能。這裏仍然只是作了個簡單的測試,在onServiceConnected()方法中調用了DownloadBinder的startDownload()和getProgress()方法。

固然,如今活動和服務其實還沒進行綁定呢,這個功能是在Bind Service按鈕的點擊事件裏完成的。能夠看到,這裏咱們仍然是構建出了一個Intent對象,而後調用bindService()方法將MainActivity和MyService進行綁定。
bindService()方法接收3個參數:
第一個參數就是剛剛構建出的Intent對象
第二個參數是前面建立出的ServiceConnection的實例
第三個參數則是一個標誌位,這裏傳入BIND_AUTO_CREATE表示在活動和服務進行綁定後自動建立服務
這會使得MyService中的onCreate()方法獲得執行,但onStartCommand()方法不會執行。
而後若是咱們想解除活動和服務之間的綁定該怎麼辦呢?調用一下unbindService()方法就能夠了,這也是 Unbind Service按鈕的點擊事件裏實現的功能。

服務的生命週期

以前咱們學習過了活動以及碎片的生命週期。相似地,服務也有本身的生命週期,前面咱們使用到的 onCreate()、 onStartCommand()、onBind()和 onDestroy()等方法都是在服務的生命週期內可能回調的方法。

一旦在項目的任何位置調用了Context的startService()方法,相應的服務就會啓動起來,並回調onStart Command()方法。若是這個服務以前尚未建立過, onCreate()方法會先於onStartCommand()方法執行。服務啓動了以後會一直保持運行狀態,直到stopService()或stopSelf()方法被調用。注意,雖然每調用一次startService()方法,onStartCommand()就會執行一次,但實際上每一個服務都只會存在一個實例。因此無論你調用了多少次 startservice()方法,只需調用一次stopService()或stopSelf()方法,服務就會中止下來了。

另外,還能夠調用Context的bindService()來獲取一個服務的持久鏈接,這時就會回調服務中的onBind()方法。相似地,若是這個服務以前尚未建立過,onCreate()方法會先於unBind()方法執行。以後,調用方能夠獲取到onBind()方法裏返回的IBinder對象的實例,這樣就能自由地和服務進行通訊了。只要調用方和服務之間的鏈接沒有斷開,服務就會一直保持運行狀態。

當調用了startService()方法後,又去調用stopService()方法,這時服務中的onDestroy()方法就會執行,表示服務已經銷燬了。相似地。當調用了bindService()方法後,又去調用 unbindservice()方法, ondestroy()方法也會執行,這兩種狀況都很好理解。可是須要注意,咱們是徹底有可能對一個服務既調用了startService()方法,又調用了bindService()方法的,這種狀況下該如何才能讓服務銷燬掉呢?根據 Android系統的機制,一個服務只要被啓動或者綁定了以後,就會一直處於運行狀態,必需要讓以上兩種條件同時不知足,服務才能被銷燬。因此,這種狀況下要同時調用stopService()和unbindService()方法,onDestroy()方法纔會執行

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

相關文章
相關標籤/搜索