學習android開發已經四五個月,由於項目中職責的原因一直沒有接觸過Service的實際項目,今天重新學一遍Service用法。
問題:
作為四大組件,為什么需要Service?
它與Thread又有何區別?
具體怎么用?
如何實現與Activity之間的通信?
一、Service 介紹
從官網中,我們可以看到這么一句:
Most confusion about the Service class actually revolves around what it is not:
- A Service is not a separate process. The Service object itself does not imply it is running in its own process; unless otherwise specified, it runs in the same process as the application it is part of.
- A Service is not a thread. It is not a means itself to do work off of the main thread (to avoid Application Not Responding errors).
Thus a Service itself is actually very simple, providing two main features:
- A facility for the application to tell the system about something it wants to be doing in the background (even when the user is not directly interacting with the application). This corresponds to calls to Context.startService(), which ask the system to schedule work for the service, to be run until the service or someone else explicitly stop it.
- A facility for an application to expose some of its functionality to other applications. This corresponds to calls to Context.bindService(), which allows a long-standing connection to be made to the service in order to interact with it.
簡單來說,Service不是一個獨立的進程,除非它被特殊指定,否則它也是我們應用程序的一部分,它需要依賴於創建服務時所在的應用程序。同時Service也不是一個線程,它其實是在主線程工作的,所以不能在Service中處理耗時的操作,不然就會出現ARN現象,如果要處理耗時的操作,可以新開一個子線程,單獨處理。
更進一步講,Service是 Android中實現程序后台運行的解決方案,它非常適合用於去執行那 些不需要和用戶交互而且還要求長期運行的任務。服務的運行不依賴於任何用戶界面,即使 當程序被切換到后台,或者用戶打開了另外一個應用程序,服務仍然能夠保持正常運行。
Service有兩種啟動方式,一種是通過startService()方式,一種是通過bindService()方式,那這兩種具體有什么區別呢,我們可以看下它們的生命周期來看出端倪。
1.1 Service 生命周期 及兩種狀態
Service 分為兩種工作狀態: 一種是 啟動狀態用於執行后台計算,另一種是 綁定狀態用於和其他組件和Service進行交互。需要注意的是這兩種狀態的Service是可以共存的,即Service既可以處於綁定狀態和啟動狀態。
startService()啟動的生命周期:
當我們第一次使用startService啟動一個服務時,系統實例化一個Service實例,然后依次調用onCreate()和onStartCommand()方法,然后運行,需要注意的是,再次使用startService方法時,不會在創建一個新的服務對象了,但還是會再次執行onStartCommand()方法,如果我們想要停掉一個服務,可以用stopService方法,此時,onDestroy()方法就會被調用,不管前面使用了多少次的startService,stopService方法調用一次,就可停掉服務。
自定義一個 MyService繼承自Service

public class MyService extends Service { public static final String TAG = "MyService"; @Override public void onCreate() { super.onCreate(); Log.d(TAG, "onCreate() executed"); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.d(TAG, "onStartCommand() executed"); return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { super.onDestroy(); Log.d(TAG, "onDestroy() executed"); } @Override public IBinder onBind(Intent intent) { return null; } }
當通國startService()啟動一個Service時
Intent startIntent = new Intent(this, MyService.class); startService(startIntent);
log信息如下:
再次啟動Service時:
調用stopService()時,Service就會被停止。
Intent stopIntent = new Intent(this, MyService.class); stopService(stopIntent);
這種方法Service和Activity的關系並不大,只是Activity通知了Service一下:“你可以啟動了。”然后Service就去忙自己的事情了
bindService()啟動的生命周期:
當調用者首次使用bindService綁定一個服務時,系統會實例化一個Service實例,並一次調用其onCreate()方法和onBind()方法,然后調用者就可以和服務進行交互了,此后,如果再次使用bindService綁定服務,系統不會創建新的Service實例,也不會再調用onBind方法;如果我們需要解除與這個服務的綁定,可使用unbindService方法,此時onUnbind方法和onDestroy方法會被調用。
自定義Service;
public class MyService extends Service { public static final String TAG = "MyService"; private MyBinder mBinder = new MyBinder(); @Override public void onCreate() { super.onCreate(); Log.d(TAG, "onCreate() executed"); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.d(TAG, "onStartCommand() executed"); return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { super.onDestroy(); Log.d(TAG, "onDestroy() executed"); } @Override public IBinder onBind(Intent intent) { return mBinder; } class MyBinder extends Binder { public void startDownload() { Log.d("TAG", "startDownload() executed"); // 執行具體的下載任務 } } }
這里我們新增了一個MyBinder類繼承自Binder類,然后在MyBinder中添加了一個startDownload()方法用於在后台執行下載任務,當然這里並不是真正地去下載某個東西,只是做個測試,所以startDownload()方法只是打印了一行日志。
在MainActivity中開啟和關閉Service:
public class MainActivity extends Activity implements OnClickListener { private Button startService; private Button stopService; private Button bindService; private Button unbindService; private MyService.MyBinder myBinder; private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName name) { } @Override public void onServiceConnected(ComponentName name, IBinder service) { myBinder = (MyService.MyBinder) service; myBinder.startDownload(); } }; @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); bindService = (Button) findViewById(R.id.bind_service); unbindService = (Button) findViewById(R.id.unbind_service); startService.setOnClickListener(this); stopService.setOnClickListener(this); 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()方法,這兩個方法分別會在Activity與Service建立關聯和解除關聯的時候調用。在onServiceConnected()方法中,我們又通過向下轉型得到了MyBinder的實例,有了這個實例,Activity和Service之間的關系就變得非常緊密了。現在我們可以在Activity中根據具體的場景來調用MyBinder中的任何public方法,即實現了Activity指揮Service干什么Service就去干什么的功能。
1.2 同時startService和binderService
如果我們既點擊了StartService按鈕,又點擊了Bind Service按鈕會怎么樣呢?這個時候你會發現,不管你是單獨點擊Stop Service按鈕還是Unbind Service按鈕,Service都不會被銷毀,必要將兩個按鈕都點擊一下,Service才會被銷毀。也就是說,點擊Stop Service按鈕只會讓Service停止,點擊Unbind Service按鈕只會讓Service和Activity解除關聯,一個Service必須要在既沒有和任何Activity關聯又處理停止狀態的時候才會被銷毀。
先點擊一下Start Service按鈕,再點擊一下Bind Service按鈕,這樣就將Service啟動起來,並和Activity建立了關聯。然后點擊Stop Service按鈕后Service並不會銷毀,再點擊一下Unbind Service按鈕,Service就會銷毀了,打印日志如下所示:
總結:
這兩個啟動方式的不同,導致生命周期也不同。startService與調用者沒有必然的聯系,即調用者結束了自己的生命周期,只要沒有使用stopService方法停止這個服務,服務仍會運行。而bindService需要有個寄宿的對象,就相當於bind到某個宿主中去,誰綁定了,誰就要負責,負責它的生命周期,從開始到結束,如果宿主自己的生命周期結束了,bindService模式就要先把服務給銷毀掉。
值得注意的一點是,如果調用者首先是先用startService方式啟動服務,然后再用bindService方式綁定某個服務的話,一定要先用unbindService方式解綁,然后才用stopService方式銷毀服務對象,不然的話,僅僅只是stopService是不夠的,沒解綁的對象還是在的,就容易造成內存泄露了。
我們應該始終記得在Service的onDestroy()方法里去清理掉那些不再使用的資源,防止在Service被銷毀后還會有一些不再使用的對象仍占用着內存。
二、Service 與 Thread 之間的關系
Service與Thread這兩個是沒有什么關系的。因為我們知道Service是運行在UI線程中,那么當需要耗時操作的時候,就需要Thread幫助,不是說Service因為是在后台運行,就跟Thread等同了。Thread是用於開啟一個子線程,在這里去執行一些耗時操作就不會阻塞主線程的運行。而Service我們最初理解的時候,總會覺得它是用來處理一些后台任務的,一些比較耗時的操作也可以放在這里運行,這就會讓人產生混淆了。
Android的后台就是指,它的運行是完全不依賴UI的。即使Activity被銷毀,或者程序被關閉,只要進程還在,Service就可以繼續運行。比如說一些應用程序,始終需要與服務器之間始終保持着心跳連接,就可以使用Service來實現。
我們可以在Service中再創建一個子線程,然后在這里去處理耗時邏輯就沒問題。既然在Service里要創建一個子線程,那為什么不直接在Activity里創建呢?這是因為Activity很難對Thread進行控制,當Activity被銷毀之后,就沒有任何其它的辦法可以再重新獲取到之前創建的子線程的實例。而且在一個Activity中創建的子線程,另一個Activity無法對其進行操作。但是Service就不同了,所有的Activity都可以與Service進行關聯,然后可以很方便地操作其中的方法,即使Activity被銷毀了,之后只要重新與Service建立關聯,就又能夠獲取到原有的Service中Binder的實例。因此,使用Service來處理后台任務,Activity就可以放心地finish,完全不需要擔心無法對后台任務進行控制的情況。
當需要在一個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); } class MyBinder extends Binder { public void startDownload() { new Thread(new Runnable() { @Override public void run() { // 執行具體的下載任務 } }).start(); } }
三、 創建前台Service
Service幾乎都是在后台運行的,一直以來它都是默默地做着辛苦的工作。但是Service的系統優先級還是比較低的,當系統出現內存不足情況時,就有可能會回收掉正在后台運行的Service。如果你希望Service可以一直保持運行狀態,而不會由於系統內存不足的原因導致被回收,就可以考慮使用前台Service。前台Service和普通Service最大的區別就在於,它會一直有一個正在運行的圖標在系統的狀態欄顯示,下拉狀態欄后可以看到更加詳細的信息,非常類似於通知的效果。當然有時候你也可能不僅僅是為了防止Service被回收才使用前台Service,有些項目由於特殊的需求會要求必須使用前台Service,比如說墨跡天氣,它的Service在后台更新天氣數據的同時,還會在系統狀態欄一直顯示當前天氣的信息。
那么我們就來看一下如何才能創建一個前台Service吧,其實並不復雜,修改MyService中的代碼,如下所示:
public class MyService extends Service { public static final String TAG = "MyService"; private MyBinder mBinder = new MyBinder(); @Override public void onCreate() { super.onCreate(); Notification notification = new Notification(R.drawable.ic_launcher, "有通知到來", System.currentTimeMillis()); Intent notificationIntent = new Intent(this, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); notification.setLatestEventInfo(this, "這是通知的標題", "這是通知的內容", pendingIntent); startForeground(1, notification); Log.d(TAG, "onCreate() executed"); } ......... }
這里只是修改了MyService中onCreate()方法的代碼。可以看到,我們首先創建了一個Notification對象,然后調用了它的setLatestEventInfo()方法來為通知初始化布局和數據,並在這里設置了點擊通知后就打開MainActivity。然后調用startForeground()方法就可以讓MyService變成一個前台Service,並會將通知的圖片顯示出來。
四、遠程Service(開辟一個新進程)
將一個Service放到單獨的進程中去執行:
<service android:name="com.example.servicetest.MyService" android:process=":remote" > </service>
此時,可以通過startService去開啟這個Service(可以在這個Service中處理耗時的操作,而不影響主線程的執行,因為它在另一個單獨的進程中)。但是當我們在MainAcitivity中去bindService時,程序會崩潰(因為MainActivity在主進程中而Service運行在遠程的進程中,讓MainActivity和MyService建立關聯會讓程序崩潰)。
那么如何關聯一個Activity與Service,這時候需要用AIDL來進程間通信(IPC)
AIDL(Android Interface Definition Language)是Android接口定義語言的意思,它可以用於讓某個Service與多個應用程序組件之間進行跨進程通信,從而可以實現多個應用程序共享同一個Service的功能。
4.1 開啟service的Server端程序
下面我們就來一步步地看一下AIDL的用法到底是怎樣的。首先需要新建一個AIDL文件,在這個文件中定義好Activity需要與Service進行通信的方法。新建MyAIDLService.aidl文件,代碼如下所示:(點擊sync后,會生成MyAIDLService.java文件)
package com.example.servicetest; interface MyAIDLService { int plus(int a, int b); String toUpperCase(String str); }
然后修改MyService中的代碼,在里面實現我們剛剛定義好的MyAIDLService接口,如下所示:
public class MyService extends Service { ...... @Override public IBinder onBind(Intent intent) { return mBinder; } MyAIDLService.Stub mBinder = new Stub() { @Override public String toUpperCase(String str) throws RemoteException { if (str != null) { return str.toUpperCase(); } return null; } @Override public int plus(int a, int b) throws RemoteException { return a + b; } }; }
這里先是對MyAIDLService.Stub進行了實現,重寫里了toUpperCase()和plus()這兩個方法。這兩個方法的作用分別是將一個字符串全部轉換成大寫格式,以及將兩個傳入的整數進行相加。然后在onBind()方法中將MyAIDLService.Stub的實現返回。這里為什么可以這樣寫呢?因為Stub其實就是Binder的子類,所以在onBind()方法中可以直接返回Stub的實現。
public class MainActivity extends Activity implements OnClickListener { private Button startService; private Button stopService; private Button bindService; private Button unbindService; private MyAIDLService myAIDLService; private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName name) { } @Override public void onServiceConnected(ComponentName name, IBinder service) { myAIDLService = MyAIDLService.Stub.asInterface(service); try { int result = myAIDLService.plus(3, 5); String upperStr = myAIDLService.toUpperCase("hello world"); Log.d("TAG", "result is " + result); Log.d("TAG", "upperStr is " + upperStr); } catch (RemoteException e) { e.printStackTrace(); } } }; ...... }
可以看到,這里首先使用了MyAIDLService.Stub.asInterface()方法將傳入的IBinder對象傳換成了MyAIDLService對象,接下來就可以調用在MyAIDLService.aidl文件中定義的所有接口了。這里我們先是調用了plus()方法,並傳入了3和5作為參數,然后又調用了toUpperCase()方法,並傳入hello world字符串作為參數,最后將調用方法的返回結果打印出來。
由此可見,我們確實已經成功實現跨進程通信了,在一個進程中訪問到了另外一個進程中的方法。
不過你也可以看出,目前的跨進程通信其實並沒有什么實質上的作用,因為這只是在一個Activity里調用了同一個應用程序的Service里的方法。而跨進程通信的真正意義是為了讓一個應用程序去訪問另一個應用程序中的Service,以實現共享Service的功能。那么下面我們自然要學習一下,如何才能在其它的應用程序中調用到MyService里的方法。
如果想要讓Activity與Service之間建立關聯,需要調用bindService()方法,並將Intent作為參數傳遞進去,在Intent里指定好要綁定的Service,示例代碼如下:
Intent bindIntent = new Intent(this, MyService.class); bindService(bindIntent, connection, BIND_AUTO_CREATE);
注冊Service(讓其它程序可以通過隱式方法訪問該Service)
<service android:name="com.example.servicetest.MyService" android:process=":remote" > <intent-filter> <action android:name="com.example.servicetest.MyAIDLService"/> </intent-filter> </service>
注:android 5.0 之后隱式方法訪問Service需要:
Intent intent = new Intent("com.example.servicetest.MyAIDLService").setPackage(com.example.servicetest);
到此,Server端的 service就定義好了,他可以在該server端的程序中運行,其它程序也可以訪問該Service
4.2 訪問Service的Client端程序
然后創建一個新的Android項目,起名為ClientTest,我們就嘗試在這個程序中遠程調用MyService中的方法。
ClientTest中的Activity如果想要和MyService建立關聯其實也不難,首先需要將MyAIDLService.aidl文件從ServiceTest項目中拷貝過來,注意要將原有的包路徑一起拷貝過來,完成后項目的結構如下圖所示:
接下來打開或新建MainActivity,在其中加入和MyService建立關聯的代碼,如下所示:
public class MainActivity extends Activity { private MyAIDLService myAIDLService; private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName name) { } @Override public void onServiceConnected(ComponentName name, IBinder service) { myAIDLService = MyAIDLService.Stub.asInterface(service); try { int result = myAIDLService.plus(50, 50); String upperStr = myAIDLService.toUpperCase("comes from ClientTest"); Log.d("TAG", "result is " + result); Log.d("TAG", "upperStr is " + upperStr); } catch (RemoteException e) { e.printStackTrace(); } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button bindService = (Button) findViewById(R.id.bind_service); bindService.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent("com.example.servicetest.MyAIDLService"); bindService(intent, connection, BIND_AUTO_CREATE); } }); } }
這部分代碼大家一定會非常眼熟吧?沒錯,這和在ServiceTest的MainActivity中的代碼幾乎是完全相同的,只是在讓Activity和Service建立關聯的時候我們使用了隱式Intent,將Intent的action指定成了com.example.servicetest.MyAIDLService。
這樣我們跨進程跨app的通信就完成了。
參考博客:
http://www.cnblogs.com/cr330326/p/5741464.html
http://blog.csdn.net/guolin_blog/article/details/11952435/
http://blog.csdn.net/guolin_blog/article/details/9797169