後臺默默的勞動者,探究服務

服務做爲Android四大組件之一,是一種可在後臺執行長時間運行操做而不提供界面的應用組件。服務可由其餘應用組件啓動,並且即便用戶切換到其餘應用,服務仍將在後臺繼續運行。須要注意的是服務並不會自動開啓線程,全部的代碼都是默認運行在主線程當中的,因此須要在服務的內部手動建立子線程,並在這裏執行具體的任務,不然就有可能出現主線程被阻塞住的狀況。html

<!-- more -->java

Android多線程編程

異步消息機制

關於多線程編程其實和Java一致,不管是繼承Thread仍是實現Runnable接口均可以實現。在Android中須要掌握的就是在子線程中更新UI,UI是由主線程來控制的,因此主線程又稱爲UI線程。android

Only the original thread that created a view hierarchy can touch its views.

雖然不容許在子線程中更新UI,可是Android提供了一套異步消息處理機制,完美解決了在子線程中操做UI的問題,那就是使用Handler。先來回顧一下使用Handler更新UI的用法:程序員

public class MainActivity extends AppCompatActivity {
    private static final int UPDATE_UI = 1001;
    private TextView textView;

    private Handler handler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(@NonNull Message msg) {
            if(msg.what == UPDATE_UI) textView.setText("Hello Thread!");
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.tv_main);
    }

    public void updateUI(View view) {
        // new Thread(()-> textView.setText("Hello Thread!")).start(); Error!
        new Thread(()->{
            Message message = new Message();
            message.what = UPDATE_UI;
            handler.sendMessage(message);
        }).start();
    }
}

使用這種機制就能夠出色地解決掉在子線程中更新UI的問題,下面就來分析一下Android異步消息處理機制到底的工做原理:Android中的異步消息處理主要由4個部分組成:Message,Handler,MessageQueue和Looper。
一、Message:線程之間傳遞的消息,它能夠在內部攜帶少許的信息,用於在不一樣線程之間交換數據。
二、Handler:處理者,它主要是用於發送和處理消息的。發送消息通常是使用Handler的sendMessage()方法,而發出的消息通過一系列地展轉處理後,最終會傳遞到Handler的handleMessage()方法中。
三、MessageQueue:消息隊列,它主要用於存放全部經過Handler發送的消息。這部分消息會一直存在於消息隊列中,等待被處理。每一個線程中只會有一個MessageQueue對象。編程

四、Looper是每一個線程中的MessageQueue的管家,調用Looper的loop()方法後,就會進入到一個無限循環當中,而後每當發現 MessageQueue 中存在一條消息,就會將它取出,並傳遞到Handler的handleMessage()方法中。每一個線程中也只會有一個Looper對象。多線程

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

AsyncTask

不過爲了更加方便咱們在子線程中對UI進行操做,Android還提供了另一些好用的工具,好比AsyncTask。AsyncTask背後的實現原理也是基於異步消息處理機制,只是Android幫咱們作了很好的封裝而已。首先來看一下AsyncTask的基本用法,因爲AsyncTask是一個抽象類,因此若是咱們想使用它,就必需要建立一個子類去繼承它。在繼承時咱們能夠爲AsyncTask類指定3個泛型參數,這3個參數的用途以下:異步

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

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private final int REQUEST_EXTERNAL_STORAGE = 1;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void startDownload(View view) {
        verifyStoragePermissions(this);
        ProgressBar progressBar = findViewById(R.id.download_pb);
        TextView textView = findViewById(R.id.download_tv);
        new MyDownloadAsyncTask(progressBar, textView).execute("http://xxx.zip");
    }


    class MyDownloadAsyncTask extends AsyncTask<String, Integer, Boolean> {
        private ProgressBar progressBar;
        private TextView textView;

        public MyDownloadAsyncTask(ProgressBar progressBar, TextView textView) {
            this.progressBar = progressBar;
            this.textView = textView;
        }

        @Override
        protected Boolean doInBackground(String... strings) {
            String urlStr = strings[0];
            try {
                URL url = new URL(urlStr);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                InputStream inputStream = conn.getInputStream();
                // 獲取文件總長度
                int length = conn.getContentLength();
                File downloadsDir = new File("...");
                File descFile = new File(downloadsDir, "xxx.zip");
                int downloadSize = 0;
                int offset;
                byte[] buffer = new byte[1024];
                FileOutputStream fileOutputStream = new FileOutputStream(descFile);
                while ((offset = inputStream.read(buffer)) != -1){
                    downloadSize += offset;
                    fileOutputStream.write(buffer, 0, offset);
                    
                    // 拋出任務執行的進度
                    publishProgress((downloadSize * 100 / length));
                }
                fileOutputStream.close();
                inputStream.close();
                Log.i(TAG, "download: descFile = " + descFile.getAbsolutePath());
            } catch (IOException e) {
                e.printStackTrace();
                return false;
            }
            return true;
        }

        // 在主線程中執行結果處理
        @Override
        protected void onPostExecute(Boolean aBoolean) {
            super.onPostExecute(aBoolean);
            if(aBoolean){
                textView.setText("下載完成,文件位於..xx.zip");
            }else{
                textView.setText("下載失敗");
            }
        }

        // 任務進度更新
        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            // 收到新進度,執行處理
            textView.setText("已下載" + values[0] + "%");
            progressBar.setProgress(values[0]);
        }

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            textView.setText("未點擊下載");
        }
    }
}

一、onPreExecute():方法會在後臺任務開始執行以前調用,用於進行一些界面上的初始化操做,好比顯示一個進度條對話框等。工具

二、doInBackground():方法中的全部代碼都會在子線程中運行,咱們應該在這裏去處理全部的耗時任務。任務一旦完成就能夠經過return語句來將任務的執行結果返回,若是 AsyncTask的第三個泛型參數指定的是Void,就能夠不返回任務執行結果。注意,在這個方法中是不能夠進行UI操做的,若是須要更新UI元素,好比說反饋當前任務的執行進度,能夠調用publishProgress()方法來完成。

三、onProgressUpdate():當在後臺任務中調用了publishProgress()方法後,onProgressUpdate()方法就會很快被調用,該方法中攜帶的參數就是在後臺任務中傳遞過來的。在這個方法中能夠對UI進行操做,利用參數中的數值就能夠對界面元素進行相應的更新。

四、onPostExecute():當後臺任務執行完畢並經過return語句進行返回時,這個方法就很快會被調用。返回的數據會做爲參數傳遞到此方法中,能夠利用返回的數據來進行一些UI操做,好比說提醒任務執行的結果,以及關閉掉進度條對話框等。

服務的基本用法

服務首先做爲Android之一,天然也要在Manifest文件中聲明,這是Android四大組件共有的特色。新建一個MyService類繼承自Service,而後再清單文件中聲明便可。

服務的建立與啓動

MyService.java:

public class MyService extends Service {
    private static final String TAG = "MyService";

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

AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="cn.tim.basic_service">
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        
        <service
            android:name=".MyService"
            android:enabled="true"
            android:exported="true" />

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

</manifest>

能夠看到,MyService的服務標籤中有兩個屬性,exported屬性表示是否容許除了當前程序以外的其餘程序訪問這個服務,enabled屬性表示是否啓用這個服務。而後在MainActivity.java中啓動這個服務:

// 啓動服務
startService(new Intent(this, MyService.class));

服務的中止(銷燬)

如何中止服務呢?在MainActivity.java中中止這個服務:

Intent intent = new Intent(this, MyService.class);
// 啓動服務
startService(intent);
// 中止服務
stopService(intent);

其實Service還能夠重寫其餘方法:

public class MyService extends Service {
    private static final String TAG = "MyService";

    public MyService() {
    }

    // 建立
    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG, "onCreate: ");
    }

    // 啓動
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i(TAG, "onStartCommand: ");
        return super.onStartCommand(intent, flags, startId);
    }

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

    // 解綁
    @Override
    public void unbindService(ServiceConnection conn) {
        super.unbindService(conn);
        Log.i(TAG, "unbindService: ");
    }

    // 銷燬
    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i(TAG, "onDestroy: ");
    }
}

其實onCreate()方法是在服務第一次建立的時候調用的,而 onStartCommand()方法則在每次啓動服務的時候都會調用,因爲剛纔咱們是第一次點擊Start Service按鈕,服務此時還未建立過,因此兩個方法都會執行,以後若是再連續多點擊幾回 Start Service按鈕,就只有onStartCommand()方法能夠獲得執行了:

服務綁定與解綁

在上面的例子中,雖然服務是在活動裏啓動的,但在啓動了服務以後,活動與服務基本就沒有什麼關係了。這就相似於活動通知了服務一下:你能夠啓動了!而後服務就去忙本身的事情了,但活動並不知道服務到底去作了什麼事情,以及完成得如何。因此這就要藉助服務綁定了。

好比在MyService裏提供一個下載功能,而後在活動中能夠決定什麼時候開始下載,以及隨時查看下載進度。實現這個功能的思路是建立一個專門的Binder對象來對下載功能進行管理,修改MyService.java:

public class MyService extends Service {
    private static final String TAG = "MyService";

    private DownloadBinder mBinder = new DownloadBinder();
    
    static class DownloadBinder extends Binder {
        public void startDownload() {
            // 模擬開始下載
            Log.i(TAG, "startDownload executed");
        }

        public int getProgress() {
            // 模擬返回下載進度
            Log.i(TAG, "getProgress executed");
            return 0;
        }
    }

    public MyService() {}

    // 建立
    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG, "onCreate: ");
    }

    // 啓動
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i(TAG, "onStartCommand: ");
        return super.onStartCommand(intent, flags, startId);
    }

    // 綁定
    @Override
    public IBinder onBind(Intent intent) {
        Log.i(TAG, "onBind: ");
        return mBinder;
    }

    // 解綁
    @Override
    public void unbindService(ServiceConnection conn) {
        super.unbindService(conn);
        Log.i(TAG, "unbindService: ");
    }

    // 銷燬
    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i(TAG, "onDestroy: ");
    }
}

MainActivity.java以下:

public class MainActivity extends AppCompatActivity {

    private MyService.DownloadBinder downloadBinder;

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

    public void aboutService(View view) {
        int id = view.getId();
        Intent intent = new Intent(this, MyService.class);
        switch (id){
            case R.id.start_btn:
                startService(intent);
                break;
            case R.id.stop_btn:
                stopService(intent);
                break;
            case R.id.bind_btn:
                // 這裏傳入BIND_AUTO_CREATE表示在活動和服務進行綁定後自動建立服務
                bindService(intent, connection, BIND_AUTO_CREATE);
                break;
            case R.id.unbind_btn:
                unbindService(connection);
                break;
        }
    }
}

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

須要注意的是,任何一個服務在整個應用程序範圍內都是通用的,即 MyService不只能夠和MainActivity綁定,還能夠和任何一個其餘的活動進行綁定,並且在綁定完成後它們均可以獲取到相同的DownloadBinder實例。

服務的生命週期

一旦調用了startServices()方法,對應的服務就會被啓動且回調onStartCommand(),若是服務未被建立,則會調用onCreate()建立Service對象。服務被啓動後會一直保持運行狀態,直到stopService()或者stopSelf()方法被調用。無論startService()被調用了多少次,可是隻要Service對象存在,onCreate()方法就不會被執行,因此只須要調用一次stopService()或者stopSelf()方法就會中止對應的服務。

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

那麼即調用了startService()又調用了bindService()方法的,這種狀況下該如何才能讓服務銷燬掉呢?根據Android系統的機制,一個服務只要被啓動或者被綁定了以後,就會一直處於運行狀態,必需要讓以上兩種條件同時不知足,服務才能被銷燬。因此,這種狀況下要同時調用stopService()和 unbindService()方法,onDestroy()方法纔會執行。

服務的更多技巧

上面講述了服務最基本的用法,下面來看看關於服務的更高級的技巧。

使用前臺服務

服務幾乎都是在後臺運行的,服務的系統優先級仍是比較低的,當系統出現內存不足的狀況時,就有可能會回收掉正在後臺運行的服務。若是你但願服務能夠一直保持運行狀態,而不會因爲系統內存不足的緣由致使被回收,就可使用前臺服務。好比QQ電話的懸浮窗口,或者是某些天氣應用須要在狀態欄顯示天氣。

public class FrontService extends Service {
    String mChannelId = "1001";

    public FrontService() {
    }

    @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();
        Intent intent = new Intent(this, MainActivity.class);
        PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
        Notification notification = new NotificationCompat.Builder(this, mChannelId)
                .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(pi)
                .build();
        startForeground(1, notification);
    }
}

使用IntentService

服務中的代碼都是默認運行在主線程當中的,若是直接在服務裏去處理一些耗時的邏輯,就很容易出現ANR的狀況。因此須要用到多線程編程,遇到耗時操做能夠在服務的每一個具體的方法裏開啓一個子線程,而後在這裏去處理那些耗時的邏輯。就能夠寫成以下形式:

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

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(()->{
            // TODO 執行耗時操做
        }).start();
        return super.onStartCommand(intent, flags, startId);
    }
    ...
}

可是,這種服務一旦啓動以後,就會一直處於運行狀態,必須調用stopService()或者stopSelf()方法才能讓服務中止下來。因此,若是想要實現讓一個服務在執行完畢後自動中止的功能,就能夠這樣寫:

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

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(()->{
            // TODO 執行耗時操做
            stopSelf();
        }).start();
        return super.onStartCommand(intent, flags, startId);
    }
    ...
}

雖然這種寫法並不複雜,可是總會有一些程序員忘記開啓線程,或者忘記調用stopSelf()方法。爲了能夠簡單地建立一個異步的、會自動中止的服務,Android 專門提供了一個IntentService類,這個類就很好地解決了前面所提到的兩種尷尬,下面咱們就來看一下它的用法:

MyIntentService.java

public class MyIntentService extends IntentService {
    private static final String TAG = "MyIntentService";
    private int count = 0;
    public MyIntentService() {
        super("MyIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        count++;
        Log.i(TAG, "onHandleIntent: count = " + count);
    }
}

MainActivity.java:

for (int i = 0; i < 10; i++) {
    Intent intent = new Intent(MainActivity.this, MyIntentService.class);
    startService(intent);
}

參考資料:《第一行代碼》

原文地址: 《後臺默默的勞動者,探究服務》
相關文章
相關標籤/搜索