本篇文章主要講述android servivce相關知識,其中會穿插一些其他的知識點,作爲初學者的教程。老鳥繞路
目錄
爲什麼要用Service
我們接觸android的時候,大部分時候是在和activity打交道,但是有些比如網絡下載、大文件讀取、解析等耗時卻又不需要界面對象的操作。一旦退出界面,那麼可能就會變得不可控(比如界面退出後,線程通知UI顯示進度,但是由於View已經被銷燬導致報錯,或者界面退出後下載中斷,就算你寫得非常完美,什麼異常狀態都考慮到了,還是保證不了系統由於內存緊張把你這個後臺的activity給幹掉,依附於於它的下載線程也中斷。)
這時候Service就有它的用武之地了,不依賴界面,消耗資源少,優先級比後臺activity高,不會輕易被系統幹掉(就算被幹掉,也有標誌位設置可以讓它自動重啓,這也是一些流氓軟件牛皮鮮的招數)、
Service及其繼承者IntentService
service的生命週期
service的生命週期相對activity要簡單不少。
可以看出service有兩條生命線,一條是調用startService,一條是調用bindService
,兩條生命線相互獨立。本文只講startService。
一道選擇題,解釋service生命週期的所有問題:
android通過startService的方式開啓服務,關於service生命週期的onCreate()和onStart() 說法正確的是哪兩項
A.當第一次啓動的時候先後調用 onCreate()和 onStart()方法
B.當第一次啓動的時候只會調用 onCreate()方法
C.如果 service 已經啓動,將先後調用 onCreate()和 onStart()方法
D.如果 service 已經啓動,只會執行 onStart()方法,不在執行 onCreate()方法
答案自己想下,結尾公佈
IntentService
一些容易被忽略的基礎知識:Service運行的代碼是在主線程上的,也就是說,直接在上面運行會卡住UI,這時就Service的繼承者(繼承於Service的子類)IntentService就應運而生。android studio的新建裏面直接就有IntentService的模板,足見其應用之廣。
那麼Service與IntentService的區別在哪呢?
詳見這裏 Android之Service與IntentService的比較
簡單來說就是
- IntentService內部有個工作線程(Worker Thread),會將startService傳入的intent通過Handler-Message機制傳入工作線程,開發者通過重載onHandleIntent進行服務的具體實現。
- IntentService在跑完onHandleIntent後,如何Handler隊列裏沒有其他消息,就會自動結束服務,有點像Thread中run函數一樣,跑完run函數之後,線程就結束了。而service需要自己去停止。
一個後臺計數器的例子來講述Service
實戰環節,本文通過一個計數器的例子模擬下載文件的耗時操作。
public void startService(View view){
Intent intent = new Intent(this,BackgroundService.class); intent.setAction("com.example.administrator.servicestudy.action.counter");
intent.putExtra("duration",10);
intent.putExtra("interval",1.0f);
startService(intent);
}
上述代碼就是一個啓動service的例子,action相當於做什麼操作(適用於一個service處理多種請求的情況。),extra就是參數。參數中duration代表總時間10秒,interval代碼每隔一秒。
private static final String ACTION_COUNTER = "com.example.administrator.servicestudy.action.counter";
@Override
protected void onHandleIntent(Intent intent) {
if (intent != null) {
final String action = intent.getAction();
if (ACTION_COUNTER.equals(action)) {
final int duration = intent.getIntExtra(EXTRA_DURATION,0);
final float interval = intent.getFloatExtra(EXTRA_INTERVAL,0);
handleActionCounter(duration, interval);
}
}
}
private void handleActionCounter(int duration, float interval) {
for(int i=0; i<duration; i++){
updateUI(i,duration);
try {
Thread.sleep((long) (interval*1000));
} catch (InterruptedException ignored) {
}
}
updateUI(duration,duration);
}
可以看到重載onHandleIntent處理事件,handleActionCounter表示具體服務。根據傳入的參數決定循環時間和sleep間隔。
當然別忘了在manifest文件中聲明該Service
<service
android:name=".BackgroundService"
android:exported="false" />
以上就是最基本的IntentService的用法了,不過爲了代碼獨立性更好,可以將代碼寫成這樣。
Activity
public void startService(View view){
BackgroundService.startCounterService(this,1,10);
}
Service
public static void startCounterService(@NonNull Context context, int interval, int duration) {
Intent intent = new Intent(context, BackgroundService.class);
intent.setAction(ACTION_COUNTER);
intent.putExtra(EXTRA_DURATION, duration);
intent.putExtra(EXTRA_INTERVAL, interval);
context.startService(intent);
}
在Service裏寫個靜態方法,只將參數傳入,剩餘的全都在Service內實現。雖然代碼寫的位置變了,但是代碼運行的位置沒變(靜態方法依然還是運行在activity端),這樣做將EXTRA_DURATION、EXTRA_INTERVAL等參數也不暴露給外部。做到更好的封裝性和模塊化,推薦這種做法。
Service如何與UI組件通信
那麼Service在後臺努力幹活的時候,如何將當前進度通知給用戶呢,因爲Service不依賴任何界面,所以自身沒辦法操作界面(除非用Toast)。所以Service就要與其他組件進行通信(主要就是activity和通知欄了,但不限於上述兩者)。
android組件間的通信(還記得android四大組件是哪四個不?)。 大部分通過android四大組件之一的Broadcast來通信。
那麼簡要說下Broadcast
Broadcast
生命週期:
就這麼簡單,一旦處理完廣播就被銷燬,沒有onCreate,也沒有onDestory
最重要的一點就是receiver裏不能處理耗時操作,超過5秒(好像是)系統就會報錯
Service
private void updateUI(int current,int total){
Intent intent = new Intent(BROADCAST_UPDATE_UI);
intent.putExtra(EXTRA_CURRENT,current);
intent.putExtra(EXTRA_TOTAL,total);
sendBroadcast(intent);
}
可以看到,發個廣播就這麼簡單,把參數填入intent,自定義一個action,send!好了。
Activity
@Override
protected void onResume() {
super.onResume();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BackgroundService.BROADCAST_UPDATE_UI);
registerReceiver(mBackgroundServiceReceiver,intentFilter);
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(mBackgroundServiceReceiver);
}
private BroadcastReceiver mBackgroundServiceReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG,"receive:"+intent.getAction());
if(intent.getAction() == BackgroundService.BROADCAST_UPDATE_UI){
int current = intent.getIntExtra(BackgroundService.EXTRA_CURRENT,0);
int total = intent.getIntExtra(BackgroundService.EXTRA_TOTAL,0);
mHint.setText(current+"/"+total);
}
}
};
Activity在resume的時候註冊一個廣播接收器,pasue的時候註銷掉。在receiver裏處理更新UI的操作。就這麼簡單
同樣的,爲了代碼更具有封裝性。在Activity中將recevier去掉。放在Service中,看代碼:
<receiver android:name=".BackgroundService$BackgroundServiceReceiver">
<intent-filter>
<action android:name="com.example.administrator.servicestudy.action.update_ui" />
</intent-filter>
</receiver>
public static class BackgroundServiceReceiver extends BroadcastReceiver {
private static List<UIHandler> mHandlers = new ArrayList<>();
@Override
public void onReceive(Context context, Intent intent) {
if(intent.getAction().equals(BROADCAST_UPDATE_UI)){
int current = intent.getIntExtra(BackgroundService.EXTRA_CURRENT,0);
int total = intent.getIntExtra(BackgroundService.EXTRA_TOTAL,0);
for (UIHandler handler : mHandlers) {
handler.onUpdateUI(current,total);
}
}
}
}
public interface UIHandler {
void onUpdateUI(int current,int total);
}
public static void registerUIHandler(UIHandler handler){
if(handler != null){
BackgroundServiceReceiver.mHandlers.add(handler);
}
}
public static void unregisterUIHandler(UIHandler handler){
BackgroundServiceReceiver.mHandlers.remove(handler);
}
這裏代碼有點多,一點一點說,
- 首先在manifest裏註冊一個靜態廣播接收器,靜態就是表示一直都會接收的,不需要手動register和unregister。一般的receiver都是單獨一個文件,這裏爲了更好地封裝性,寫在Service裏作爲靜態內部類。所以在manifest裏的註冊名字也寫成了.BackgroundService$BackgroundServiceReceiver,注意中間一個美元符號,那就是表示公共靜態內部類的標誌。
- 在Service內部實現一個Receiver,具體和Activity裏面的一樣。
- 然後寫一個interface,代表具體的UI處理
- 寫一個註冊函數和反註冊函數,用以界面組件註冊UI更新事件。
- 由於該Service可能不止只更新一個界面組件,所以註冊的Handler是一個列表。在收到廣播後,將所有註冊過的組件都通知更新一遍。
然後在Activity中註冊一下。替換掉註冊廣播的地方。
@Override
protected void onResume() {
super.onResume();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BackgroundService.BROADCAST_UPDATE_UI);
// registerReceiver(mBackgroundServiceReceiver,intentFilter);
BackgroundService.registerUIHandler(mServiceUIHandler);
}
@Override
protected void onPause() {
super.onPause();
BackgroundService.unregisterUIHandler(mServiceUIHandler);
// unregisterReceiver(mBackgroundServiceReceiver);
}
private BackgroundService.UIHandler mServiceUIHandler = new BackgroundService.UIHandler() {
@Override
public void onUpdateUI(int current, int total) {
Log.d(TAG,"receive: service broadcast");
mHint.setText(current+"/"+total);
}
};
這樣就完成了一個Service的封裝,簡化Activity的代碼,我的思想一直都是Activity中,應該只處理和界面有關的代碼。就像C語言的main函數一樣,你不可能把所有代碼都寫在main函數裏吧。或者面函數的同一個文件裏吧。
那麼我們加一個stop Service的函數吧。
Service
public static void stopCounterService(@NonNull Context context){
Intent intent = new Intent(context, BackgroundService.class);
intent.setAction(ACTION_COUNTER);
context.stopService(intent);
}
Activity
public void stopService(View view){
// Intent intent = new Intent(this,BackgroundService.class);
// intent.setAction("com.example.administrator.servicestudy.action.counter");
// stopService(intent);
BackgroundService.stopCounterService(this);
}
IntentService是以Message爲單位來停止的,也就是說,一定要等到當前消息處理完才能完全stop掉,爲此我們可以加一個標誌位,一旦Service停止,強制循環退出。
Service
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG,"onCreate");
mServiceFinished = false;
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG,"onDestroy");
mServiceFinished = true;
}
private void handleActionCounter(int duration, float interval) {
for(int i=0; i<duration; i++){
if(mServiceFinished){
break;
}
updateUI(i,duration);
try {
Thread.sleep((long) (interval*1000));
} catch (InterruptedException ignored) {
}
}
updateUI(duration,duration);
}
Service與通知欄的通信
至此我們已經完成了Service與Activity的通信,Service與Activity之間通過廣播進行通信。Service負責邏輯處理,Activity負責更新界面顯示。但是到這邊還沒發現Service的獨特之處,就是這個這些代碼完全也可以寫在Activity裏面的,寫在Service裏面無非就是結構更好看點,如果你那麼認爲就錯了。你可以在Activity中退出再進入,可以發現計數器並沒有因爲Activity的退出而終止或者暫停。依然跟着時間走。這點是寫在Activity中完全做不到的。當然你也可以通過一些小技巧來達到同樣的效果,不過我們這個例子是爲了模擬後臺下載用的。所以不扯這些了。
下面進入真正的後臺下載。Service與通知欄的通信。
我們這樣設計一個程序,當Activity退出後,通知欄繼續顯示計數器進度,點擊通知或者再次進入Activity,通知欄取消顯示進度(爲了不重複顯示,也爲了演示代碼)。
爲此我們新建一個新的Service,並在Activity添加如下代碼
NotificationService
public class NotificationService extends Service {
private static final String TAG = NotificationService.class.getSimpleName();
public NotificationService() {
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
BackgroundService.registerUIHandler(mUIHandler);
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG,"onDestroy");
BackgroundService.unregisterUIHandler(mUIHandler);
}
......
}
這裏我們新建的是一個普通的service,而不是IntentService,因爲這邊我們不需要耗時操作,我們甚至連onStartCommand都沒有重載,因爲我們只需要在啓動服務的時候註冊一個UI更新的回調就可以了,然後在銷燬服務的時候註銷掉。
Activity
@Override
protected void onResume() {
super.onResume();
...
stopService(new Intent(this,NotificationService.class));
}
@Override
protected void onPause() {
super.onPause();
...
startService(new Intent(this,NotificationService.class));
}
我們在Activity Resume的時候關閉通知欄通知服務,在Pause的時候開啓該服務,這樣就能做到我們的設計初衷。
接下來就是通知欄的UI更新操作了,都是通知欄的接口,聽說2.3和4.0以上的接口很不一樣,我們這邊用的是4.0以上的接口。
private BackgroundService.UIHandler mUIHandler = new BackgroundService.UIHandler() {
@Override
public void onUpdateUI(int current, int total) {
Log.d(TAG,"Notification onUpdateUI");
//點擊通知後,啓動Activity,最後的FLAG_ONE_SHOT,表示只執行一次,具體自行百度。
PendingIntent pendingIntent = PendingIntent.getActivity(NotificationService.this,
0,
new Intent(NotificationService.this,MainActivity.class),
PendingIntent.FLAG_ONE_SHOT);
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Notification.Builder builder = new Notification.Builder(getApplicationContext());
Notification notification = builder.setContentTitle("Background Service")
.setTicker("Counting...")//狀態欄上滾動的字符串
.setContentText("Ongoing")//設置通知的正文
.setProgress(total, current, false)//設置通知欄的進度條,android真貼心,終於可以不用自定義進度條了。
.setOngoing(true)//設置可不可以取消該通知
.setContentIntent(pendingIntent)//點擊該通知後的操作。
.setDefaults(Notification.DEFAULT_ALL)//通知的音效、震動、呼吸燈全都隨系統設置,當然你也可以自定義
.setAutoCancel(true)//是不是點擊之後自動取消,否則的話,可能你需要手動調用接口來取消
.setOnlyAlertOnce(true)//音效震動呼吸燈是否只提醒一下,專門給進度條之類,頻繁更新的通知用的,不設置這個,你可以試試,那鬼畜的音效
.setSmallIcon(R.mipmap.ic_launcher)//這個不解釋了
.build();
//第一個參數爲ID,APP內全局唯一,相同的ID表示相同的通知,不會在通知欄新增一條通知,不同的話,則在通知欄插入一條新的通知。第二個參數就是剛纔配置的通知。
nm.notify(1234,notification);
}
};
最後提醒一句,通知不配置PendingIntent是不會顯示的哦
爲了完美模擬後臺下載,我們在下載完成後(服務被銷燬後),發送一個結束廣播,通知UI層。