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

服務作為Android四大組件之一,是一種可在后臺執(zhí)行長時間運行操作而不提供界面的應用組件。服務可由其他應用組件啟動,而且即使用戶切換到其他應用,服務仍將在后臺繼續(xù)運行。需要注意的是服務并不會自動開啟線程,所有的代碼都是默認運行在主線程當中的,所以需要在服務的內(nèi)部手動創(chuàng)建子線程,并在這里執(zhí)行具體的任務,否則就有可能出現(xiàn)主線程被阻塞住的情況。

Android多線程編程

異步消息機制

關(guān)于多線程編程其實和Java一致,無論是繼承Thread還是實現(xiàn)Runnable接口都可以實現(xiàn)。在Android中需要掌握的就是在子線程中更新UI,UI是由主線程來控制的,所以主線程又稱為UI線程。

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

使用這種機制就可以出色地解決掉在子線程中更新UI的問題,下面就來分析一下Android異步消息處理機制到底的工作原理:Android中的異步消息處理主要由4個部分組成:Message,Handler,MessageQueue和Looper。
1、Message:線程之間傳遞的消息,它可以在內(nèi)部攜帶少量的信息,用于在不同線程之間交換數(shù)據(jù)。
2、Handler:處理者,它主要是用于發(fā)送和處理消息的。發(fā)送消息一般是使用Handler的sendMessage()方法,而發(fā)出的消息經(jīng)過一系列地輾轉(zhuǎn)處理后,最終會傳遞到Handler的handleMessage()方法中。
3、MessageQueue:消息隊列,它主要用于存放所有通過Handler發(fā)送的消息。這部分消息會一直存在于消息隊列中,等待被處理。每個線程中只會有一個MessageQueue對象。

4、Looper是每個線程中的MessageQueue的管家,調(diào)用Looper的loop()方法后,就會進入到一個無限循環(huán)當中,然后每當發(fā)現(xiàn) MessageQueue 中存在一條消息,就會將它取出,并傳遞到Handler的handleMessage()方法中。每個線程中也只會有一個Looper對象。

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

image

AsyncTask

不過為了更加方便我們在子線程中對UI進行操作,Android還提供了另外一些好用的工具,比如AsyncTask。AsyncTask背后的實現(xiàn)原理也是基于異步消息處理機制,只是Android幫我們做了很好的封裝而已。首先來看一下AsyncTask的基本用法,由于AsyncTask是一個抽象類,所以如果我們想使用它,就必須要創(chuàng)建一個子類去繼承它。在繼承時我們可以為AsyncTask類指定3個泛型參數(shù),這3個參數(shù)的用途如下:

Params:在執(zhí)行AsyncTask時需要傳入的參數(shù),可用于在后臺任務中使用。
Progress:后臺任務執(zhí)行時,如果需要在界面上顯示當前的進度,則使用這里指定的泛型作為進度單位。
Result:當任務執(zhí)行完畢后,如果需要對結(jié)果進行返回,則使用這里指定的泛型作為返回值類型。

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);
                    
                    // 拋出任務執(zhí)行的進度
                    publishProgress((downloadSize * 100 / length));
                }
                fileOutputStream.close();
                inputStream.close();
                Log.i(TAG, "download: descFile = " + descFile.getAbsolutePath());
            } catch (IOException e) {
                e.printStackTrace();
                return false;
            }
            return true;
        }

        // 在主線程中執(zhí)行結(jié)果處理
        @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);
            // 收到新進度,執(zhí)行處理
            textView.setText("已下載" + values[0] + "%");
            progressBar.setProgress(values[0]);
        }

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

1、onPreExecute():方法會在后臺任務開始執(zhí)行之前調(diào)用,用于進行一些界面上的初始化操作,比如顯示一個進度條對話框等。

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

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

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

服務的基本用法

服務首先作為Android之一,自然也要在Manifest文件中聲明,這是Android四大組件共有的特點。新建一個MyService類繼承自Service,然后再清單文件中聲明即可。

服務的創(chuàng)建與啟動

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

服務的停止(銷毀)

如何停止服務呢?在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() {
    }

    // 創(chuàng)建
    @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: ");
    }
}
image

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

image

服務綁定與解綁

在上面的例子中,雖然服務是在活動里啟動的,但在啟動了服務之后,活動與服務基本就沒有什么關(guān)系了。這就類似于活動通知了服務一下:你可以啟動了!然后服務就去忙自己的事情了,但活動并不知道服務到底去做了什么事情,以及完成得如何。所以這就要借助服務綁定了。

比如在MyService里提供一個下載功能,然后在活動中可以決定何時開始下載,以及隨時查看下載進度。實現(xiàn)這個功能的思路是創(chuàng)建一個專門的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() {}

    // 創(chuàng)建
    @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表示在活動和服務進行綁定后自動創(chuàng)建服務
                bindService(intent, connection, BIND_AUTO_CREATE);
                break;
            case R.id.unbind_btn:
                unbindService(connection);
                break;
        }
    }
}
image

這個ServiceConnection的匿名類里面重寫了onServiceConnected()方法和 onServiceDisconnected()方法,這兩個方法分別會在活動與服務成功綁定以及解除綁定的時候調(diào)用。在 onServiceConnected()方法中,通過向下轉(zhuǎn)型得到DownloadBinder的實例,有了這個實例,活動和服務之間的關(guān)系就變得非常緊密了?,F(xiàn)在我們可以在活動中根據(jù)具體的場景來調(diào)用DownloadBinder中的任何public()方法,即實現(xiàn)了指揮服務干什么服務就去干什么的功能(雖然實現(xiàn)startDownload與getProgress實現(xiàn)很簡單)。

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

服務的生命周期

image

一旦調(diào)用了startServices()方法,對應的服務就會被啟動且回調(diào)onStartCommand(),如果服務未被創(chuàng)建,則會調(diào)用onCreate()創(chuàng)建Service對象。服務被啟動后會一直保持運行狀態(tài),直到stopService()或者stopSelf()方法被調(diào)用。不管startService()被調(diào)用了多少次,但是只要Service對象存在,onCreate()方法就不會被執(zhí)行,所以只需要調(diào)用一次stopService()或者stopSelf()方法就會停止對應的服務。

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

那么即調(diào)用了startService()又調(diào)用了bindService()方法的,這種情況下該如何才能讓服務銷毀掉呢?根據(jù)Android系統(tǒng)的機制,一個服務只要被啟動或者被綁定了之后,就會一直處于運行狀態(tài),必須要讓以上兩種條件同時不滿足,服務才能被銷毀。所以,這種情況下要同時調(diào)用stopService()和 unbindService()方法,onDestroy()方法才會執(zhí)行。

服務的更多技巧

上面講述了服務最基本的用法,下面來看看關(guān)于服務的更高級的技巧。

使用前臺服務

服務幾乎都是在后臺運行的,服務的系統(tǒng)優(yōu)先級還是比較低的,當系統(tǒng)出現(xiàn)內(nèi)存不足的情況時,就有可能會回收掉正在后臺運行的服務。如果你希望服務可以一直保持運行狀態(tài),而不會由于系統(tǒng)內(nèi)存不足的原因?qū)е卤换厥?,就可以使用前臺服務。比如QQ電話的懸浮窗口,或者是某些天氣應用需要在狀態(tài)欄顯示天氣。

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

使用IntentService

服務中的代碼都是默認運行在主線程當中的,如果直接在服務里去處理一些耗時的邏輯,就很容易出現(xiàn)ANR的情況。所以需要用到多線程編程,遇到耗時操作可以在服務的每個具體的方法里開啟一個子線程,然后在這里去處理那些耗時的邏輯。就可以寫成如下形式:

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

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

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

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

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

雖然這種寫法并不復雜,但是總會有一些程序員忘記開啟線程,或者忘記調(diào)用stopSelf()方法。為了可以簡單地創(chuàng)建一個異步的、會自動停止的服務,Android 專門提供了一個IntentService類,這個類就很好地解決了前面所提到的兩種尷尬,下面我們就來看一下它的用法:

image

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

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

原文地址:《后臺默默的勞動者,探究服務》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容