不管怎么說,后臺功能屬于四大組件之一,其重要程度不言而喻
10.1 服務是什么
服務(Service
)是Android
中實現程序后臺運行的解決方案,它非常適合去執行那些不需要和用戶交互而且還要求長期運行的任務。服務的運行不依賴于任何用戶界面,即使程序被切換到后臺,或者用戶打開了另外一個應用程序,服務仍然能夠保持正常運行。
不過需要注意的是,服務并不是運行在一個獨立的進程當中的,而是依賴于創建服務時所在的應用程序進程。當某個應用程序進程被殺掉時,所有依賴于該進程的服務也會停止運行。
另外,也不要被服務的后臺概念所迷惑,實際上服務并不會自動開啟線程,所有的代碼都是默認運行在主線程當中的。也就是說,我們需要在服務的內部手動創建子線程,并在這里執行具體的任務,否則就有可能出現主線程被阻塞住的情況。
10.2.1 線程的基本用法
定義一個線程只需要新建一個類繼承自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
正是一個實現了Runnable
接口的對象,所以可以直接將它傳入到Thread
的構造函數中。接著調用Thread
的start()
方法,run()
方法中的代碼就會在子線程當中運行了。
當然,如果你不想專門再定義一個類去實現Runnable
接口,也可以使用匿名類的方式,這種寫法更為常見。
new Thread(new Runnable() {
@Override
public void run() {
//處理具體的邏輯
}
}).start();
10.2.2 在子線程中更新UI
和其他的GUI
庫一樣,Android
的UI
也是線程不安全的。也就是說,如果想要更新應用程序里的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
進行處理。如果發現Message
的what
字段的值等于UPDATE_TEXT
,就將TextView
顯示的內容改成Nice to meet you
。
可以看到,這次我們并沒有在子線程里直接進行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
.
10.2.3 解析異步消息處理機制
Android
中的異步消息處理主要由四部分組成:Message
,Handler
,MessageQueue
和Looper
。
1.Message
Message
是在線程之間傳遞的消息,它可以在內部攜帶少量的信息,用于在不同線程之間交換數據。上面我們使用到了Message
的what
字段,除此之外還可以使用arg1
和arg2
字段來攜帶一些整形數據,使用obj
字段攜帶一個Object
對象。
2.Handler
Handler
顧名思義也就是處理者的意思,它主要是用于發送和處理消息。發送消息一般是使用Handler
的sendMessage()
方法,而發出的消息經過一系列輾轉處理后,最終會傳遞到Handler
的handleMessage()
方法中。
3.MessageQueue
MessageQueue
是消息隊列的意思,它主要用于存放所有通過Handler
發送的消息。這部分消息會一直存在于消息隊列中,等待被處理。每個線程中只會有一個MessageQueue
對象。
4.Looper
Looper
是每個線程中的MessageQueue
的管家,調用Looper
的loop()
方法后,就會進入到一個無限循環當中,然后每當發現MessageQueue
中存在一條消息,就會將它取出,并傳遞到Handler
的handleMessage()
方法中。每個線程中也只會有一個Looper
對象。
我們來把異步消息處理的整個流程梳理一遍。首先需要在主線程當中創建一個Handler
對象,并重寫handleMessage()
方法。然后當子線程中需要進行UI
操作時,就創建一個Message
對象,并通過Handler
將這條消息發送出去。之后這條消息會被添加到MessageQueue
的隊列中等待被處理,而Looper
則會一直嘗試從MessageQueue
中取出待處理消息,最后分發回Handler
的handleMessage()
方法中。由于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()
服務的基本用法
定義一個服務
我們將服務命名為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()
方法將MainActivity
和MyService
進行綁定。bindService()
方法接收三個參數,第一個參數就是剛剛構建出的Intent
對象,第二個參數是前面創建出的ServiceConnection
的實例,第三個參數則是一個標志位,這里傳入BIND_AUTO_CREATE
表示在活動和服務進行綁定后自動創建服務。這會使得MyService
中的onCreate()
方法得到執行,但是onStartCommand()
方法不會執行。
解除服務和活動之間的綁定,調用一下unbindService()
方法就可以了。
另外需要注意,任何一個服務在整個應用程序范圍內都是通用的,即MyService
不僅可以和MainActivity
綁定,還可以和任何一個其他的活動進行綁定,而且在綁定完成后他們都可以獲取到相同的DownloadBinder
實例。
10.4 服務的生命周期
服務有自己的生命周期。
一旦在項目的任何位置調用了Context
的startService()
方法,相應的服務就會啟動起來,并回調onStartCommand()
方法。如果這個服務之前還沒有創建過,onStart()
方法會先于onStartCommand()
方法執行。服務啟動了之后會一直保持運行狀態,直到stopService()
或stopSelf()
方法被調用。注意,雖然每調用一次startService()
方法,onStartCommand()
就會執行一次,但實際上每個服務都只會存在一個實例。所以不管你調用了多少次startService()
方法,只需調用一次stopService()
或stopSelf()
方法,服務就會停止下來了。
另外,還可以調用Context
的bindService()
來獲取一個服務的持久連接,這時就會回調服務中的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()方法。