編輯:關於Android編程
本篇文章主要講述android servivce相關知識,其中會穿插一些其他的知識點,作為初學者的教程。老鳥繞路
本文會講述如下內容:
- 為什麼要用Service
- Service及其繼承者IntentService
- 一個後台計數器的例子來講述Service
- Service如何與UI組件通信
我們接觸android的時候,大部分時候是在和activity打交道,但是有些比如網絡下載、大文件讀取、解析等耗時卻又不需要界面對象的操作。一旦退出界面,那麼可能就會變得不可控(比如界面退出後,線程通知UI顯示進度,但是由於View已經被銷毀導致報錯,或者界面退出後下載中斷,就算你寫得非常完美,什麼異常狀態都考慮到了,還是保證不了系統由於內存緊張把你這個後台的activity給干掉,依附於於它的下載線程也中斷。)
這時候Service就有它的用武之地了,不依賴界面,消耗資源少,優先級比後台activity高,不會輕易被系統干掉(就算被干掉,也有標志位設置可以讓它自動重啟,這也是一些流氓軟件牛皮鮮的招數)、
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()方法
答案自己想下,結尾公布
一些容易被忽略的基礎知識: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需要自己去停止。實戰環節,本文通過一個計數器的例子模擬下載文件的耗時操作。
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
可以看到重載onHandleIntent處理事件,handleActionCounter表示具體服務。根據傳入的參數決定循環時間和sleep間隔。
當然別忘了在manifest文件中聲明該Service
以上就是最基本的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中,看代碼:
public static class BackgroundServiceReceiver extends BroadcastReceiver {
private static List 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函數裡吧。或者把所有的函數寫在同一個文件裡吧。
這裡需要注意的是,由於之前提過IntentService內部其實是一個Worker Thread,所以多按幾次start,其實是多發了幾次消息,導致會計數完成後,重新計數。這個自己感受下就知道了。
那麼我們加一個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
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層。
Service
public interface UIHandler {
void onUpdateUI(int current,int total);
void onFinish();
}
新增一個結束時的回調
@Override
public void onDestroy() {
....
Intent intent = new Intent(BROADCAST_FINISH);
sendBroadcast(intent);
}
在被銷毀時發送廣播
@Override
public void onReceive(Context context, Intent intent) {
if(intent.getAction().equals(BROADCAST_UPDATE_UI)){
....
}else if(intent.getAction().equals(BROADCAST_FINISH)){
for (UIHandler handler : mHandlers) {
handler.onFinish();
}
}
}
在onReceive中發送onFinish的回調
最重要的是別忘了在manifest中聲明這個廣播,因為Service中的是靜態廣播接收器
而在Activity和Notification中就簡單多了,只要實現相應的onFinish回調就可以了
@Override
public void onFinish() {
Log.d(TAG,"receive: service finish");
mHint.setText("Finished");
}
@Override
public void onFinish() {
Log.d(TAG,"Notification onFinish");
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Notification.Builder builder = new Notification.Builder(getApplicationContext());
Notification notification = builder.setContentTitle("Background Service")
.setContentText("Finished")
.setOngoing(false)
.setContentIntent(null)//這裡PendingIntent設置為null,只是為了演示代碼,這樣這個通知點上去就不會有反應
.setDefaults(Notification.DEFAULT_ALL)
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setSmallIcon(R.mipmap.ic_launcher)
.build();
//設置兩個不同的notification ID,為了演示兩個不同通知,並且演示如何取消一個通知
nm.notify(1232,notification);
nm.cancel(1234);
}
教程到此結束。謝謝
最後公布,文中一道問題的答案,A和D。很簡單吧
源碼點這裡下載
Android加載Gif動畫如何實現?相信大家都很好奇,本文就為大家揭曉,內容如下<?xml version=1.0 encoding=utf-8?
現在越來越多的應用開始重視流暢度方面的測試,了解Android應用程序是如何在屏幕上顯示的則是基礎中的基礎,就讓我們一起看看小小屏幕中大大的學問。這也是我下篇文章&mda
先來看看效果圖:一、布局 <?xml version=1.0 encoding=utf-8?><LinearLayout xm
模糊搜索框。APP需要一個該控件,安卓端。先上個圖,看起來不錯的效果。圖一為未點擊狀態,圖二為點擊之後的狀態。圖三為輸入之後的狀態。主要的功能點有 :1、點擊直接懸浮層,