學習Android的同學注意了!!!
學習過程中遇到什麼問題或者想獲取學習資源的話,歡迎加入Android學習交流羣,羣號碼:364595326 我們一起學Android!
Services
服務是一個應用程序組件,可以在後臺執行長時間運行的操作,不提供用戶界面。一個應用程序組件可以啓動一個服務,它將繼續在後臺運行,即使用戶切換到另一個應用程序。此外,一個組件可以綁定到一個服務與它交互,甚至執行進程間通信(IPC)。例如,一個服務可能處理網絡通信,播放音樂,執行文件I/O,或與一個內容提供者交互,都在後臺執行。
一個服務本質上講有兩種形式:
Started 啓動的方式
started形式的服務是指當一個應用組件(比如activity)通過startService()方法開啓的服務。一旦開啓,該服務就可以無限期地在後臺運行,哪怕開啓它的組件被銷燬掉。
通常,開啓的服務執行一個單獨的操作且並不向調用者返回一個結果。
比如,可能從網絡進行下載或者上傳一個文件。當任務完成,服務就該自我停止。
Bound 綁定的方式
bound形式的服務是指一個應用組件通過調用 bindService() 方法與服務綁定。一個綁定的服務提供一個客戶-服務端接口,以允許組件與服務交互,發送請求,獲得結果,甚至執行進程間通信。一個綁定的服務只和與其綁定的組件同時運行。多個組件可以同時綁定到一個服務,但當全部接觸綁定後,服務就被銷燬。
雖然分這兩類,但是一個服務可以同時使用這兩種方式——可以用started無限期的運行,同時允許綁定。只需要在服務中實現兩個回調方法:onStartCommand()允許組件開啓服務,onBind()允許綁定。
不論應用程序是怎麼起服務的,任何應用程序都可以用這個服務。同樣的,任何組件可以使用一個Activity通過傳遞Intent開啓服務。你也可以在配置文件設置服務爲私有來防止其他應用訪問該服務。
注意:一個服務在進程中的主線程運行——一個服務不會創建自己的線程,也不會在另外的進程運行(除非另外指定)。這意味着,如果服務需要做一些頻繁佔用CPU的工作或者會發生阻塞的操作,你需要在服務中另開線程執行任務。這可以降低產生ANR的風險,提高用戶體驗。
Service的基礎生命週期
創建一個服務需要建立一個Service相關的子類,然後需要實現一些回調方法,好在不同的生命週期內做對應處理和綁定服務,比較重要的方法如下:
onStartCommand()
當其他組件,如 activity 請求服務啓動時,系統會調用這個方法。一旦這個方法執行,服務就開始並且無限期的執行。如果實現這個方法,當這個服務完成任務後,需要你來調用 stopSelf() 或者 stopService() 停掉服務。如果只想提供綁定,不需要自己實現這個方法。
onBind()
當有其他組件想通過 bindService() 方法綁定這個服務時系統就會調用此方法。在實現的方法裏面,必須添加一個供客戶端使用的接口通過返回一個IBinder來與服務通信,這個方法必須實現。當然不想允許綁定的話,返回null即可。
onCreate()
服務第一次建立的時候會調用這個方法,執行一次性設置程序,在上面2個方法執行前調用。如果服務已存在,則不執行該方法。
onDestroy()
服務不再使用則使用該方法。服務應該實現這個方法來清理諸如線程,註冊的監聽器等資源。這是最後調用的方法。
安卓系統只會在內存佔用很高,必須恢復系統資源供當前運行程序的情況下強制停掉一個運行中的服務。如果服務綁定在當前運行的程序中,就幾乎不會被殺掉,如果服務聲明瞭在前臺運行(其實在後臺,只是給系統一個錯的信息來提高優先級),就幾乎不會被殺掉。另外,如果一個服務正在運行,且運行了很久,系統就會根據運行時間把其排在後臺任務列表的後面,則這個服務很容易被殺掉。根據onStartCommand() 的返回值設置,服務被殺掉後仍可以在資源充足的條件下立即重啓。
是用一個服務好還是開一個線程好?
一個服務就是一個可以忽略交互,在後臺獨立運行的組件,如果你需要這樣就用服務如果你需要在用戶與程序交互時在主線程外執行任務,那就開個線程吧。比如想播放音樂,但只在程序運行時播放,你可能在 onCreate() 開一個線程,在 onStart() 中開啓它,在 onStop() 停止它。也可以考慮使用AsyncTask或者HandlerThread取代一般的線程。記住,如果使用一個服務,它還是默認在主線程中運行,如果會發生阻塞,還是要在服務中另開線程的。
服務就必須在 manifest 文件聲明。
<application>
<service android:name=".ExampleService"
android:enabled=["true" | "false"]
android:exported=["true" | "false"]
android:isolatedProcess=["true" | "false"]
android:label="string resource"
android:icon="drawable resource"
android:permission="string"
android:process="string">
</service>
</application>
service標籤屬性
1.android:name
你所編寫的服務類的類名,可填寫完整名稱,包名+類名,如com.example.test.ServiceA,也可以忽略包名,用.開頭,如.ServiceA,因爲在manifest文件開頭會定義包名,它會自己引用。一旦你發佈應用,你就不能改這個名字(除非設置android:exported="false"),另外name沒有默認值,必須定義。
2.android:enabled
是否可以被系統實例化,默認爲true。因爲父標籤也有enable屬性,所以必須兩個都爲默認值true的情況下服務纔會被激活,否則不會激活。
3.android:exported
其他應用能否訪問該服務,如果不能,則只有本應用或有相同用戶ID的應用能訪問。當然除了該屬性也可以在下面permission中限制其他應用訪問本服務。這個默認值與服務是否包含意圖過濾器intent filters有關。如果一個也沒有則爲false。
4.android:isolatedProcess
設置true意味着,服務會在一個特殊的進程下運行,這個進程與系統其他進程分開且沒有自己的權限。與其通信的唯一途徑是通過服務的API(binding and starting)。
5.android:label
可以顯示給用戶的服務名稱。如果沒設置,就用的lable。不管怎樣,這個值是所有服務的意圖過濾器的默認lable。定義儘量用對字符串資源的引用。
6.android:icon
類似label,是圖標,儘量用drawable資源的引用定義。
7.android:permission
是一個實體必須要運行或綁定一個服務的權限。如果沒有權限,startService(),bindService()或stopService()方法將不執行,Intent也不會傳遞到服務。如果屬性未設置,會由權限設置情況應用到服務。如果兩者都未設置,服務就不受權限保護。
8.android:process
服務運行所在的進程名。通常爲默認爲應用程序所在的進程,與包名同名。元素的屬性process可以設置不同的進程名,當然組件也可設置自己的進程覆蓋應用的設置。
如果名稱設置爲冒號:開頭,一個對應用程序私有的新進程會在需要時和運行到這個進程時建立。如果名稱爲小寫字母開頭,服務會在一個相同名字的全局進程運行,如果有權限這樣的話。這允許不同應用程序的組件可以分享一個進程,減少了資源的使用。
創建“啓動的”服務
啓動的(started)服務由startService(Intent)方法啓動,在服務中的onStartCommand()方法裏獲得Intent信息。關閉則由服務自己的方法stopSelf()或者由啓動服務的地方調用stopService(Intent)方法來關閉。並不會因爲啓動服務的應用程序銷燬而關閉。
示例,一個應用需要保存數據到遠程數據庫,這時啓動一個服務,通過創建啓動的服務給服務傳遞數據,由服務執行保存行爲,行爲結束再自我銷燬。因爲服務跟啓動它的應用在一個進程的主線程中,所以對於耗時的操作要起一個新的線程去做。
/**Activity*/
Intent intent = new Intent(MainActivity.this, ServiceA.class);
intent.putExtra("name", strName);
startService(intent);
/**Service*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 獲取數據
String strName = intent.getStringExtra("name");
// ... 數據庫操作
new Thread(new Runnable() {
@Override
public void run() {
//耗時的操作
}
}).run();
return Service.START_STICKY;
}
寫服務有2種,繼承service或者IntentService。後者是前者的子類。前者包含上面介紹的各種方法,用於普通的服務。後者可以自己開一個工作線程一個接一個處理多個請求。
繼承IntentService
大多數服務不需要同時處理多個請求,繼承IntentService是最好的選擇。
IntentService處理流程:
創建默認的一個worker線程處理傳遞給onStartCommand()的所有intent,不佔據應用的主線程
創建一個工作隊列一次傳遞一個intent到你實現的onHandleIntent()方法,避免了多線程
在所以啓動請求被處理後自動關閉服務,不需要調用stopSelf()
默認提供onBind()的實現,並返回null
默認提供onStartCommand()的實現,實現發送intent到工作隊列再到你的onHandleIntent()方法實現。
這些都加入到IntentService中了,你需要做的就是實現構造方法和onHandleIntent(),如下:
public class HelloIntentService extends IntentService {
//構造器指明服務中線程的名字,方便調試使用
public HelloIntentService() {
super("HelloIntentService");
}
//子線程執行的代碼
@Override
protected void onHandleIntent(Intent intent) {
long endTime = System.currentTimeMillis() + 5*1000;
while (System.currentTimeMillis() < endTime) {
synchronized (this) {
try {
wait(endTime - System.currentTimeMillis());
} catch (Exception e) {
}
}
}
}
}
如果需要重寫其他回調方法,如onCreate(),onStartCommand()等,一定要調用super()方法,保證IntentService正確處理worker線程,只有onHandleIntent()和onBind()不需要這樣。如:
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Toast.makeText(this, "service starting",Toast.LENGTH_SHORT).show();
return super.onStartCommand(intent,flags,startId);
}
繼承Service
繼承Service就可以實現對請求多線程的處理,前面介紹了service的生命週期,可以按照生命週期實現方法。就不放示例了。
onStartCommand()的返回值:
1.START_NOT_STICKY
系統不重新創建服務,除非有將要傳遞來的intent。這是最安全的選項,可以避免在不必要的時候運行服務。
2.START_STICKY
系統重新創建服務並且調用onStartCommand()方法,但並不會傳遞最後一次傳遞的intent,只是傳遞一個空的intent。除非存在將要傳遞來的intent,那麼就會傳遞這些intent。這個適合播放器一類的服務,不需要執行命令,只需要獨自運行,等待任務。
3.START_REDELIVER_INTENT
系統重新創建服務並且調用onStartCommand()方法,傳遞最後一次傳遞的intent。其餘存在的需要傳遞的intent會按順序傳遞進來。這適合像下載一樣的服務,立即恢復,積極執行。
注意:如果想從服務獲得結果,可以用廣播來處理。
創建“綁定的”服務
用bindService()方法將應用組件綁定到服務,建立一個長時間保持的聯繫。
如果需要在activity或其他組件和服務交互或者通過進程間通信給其他應用程序提供本應用的功能,就需要綁定的服務。
建立一個綁定的服務需要實現onBind()方法返回一個定義了與服務通信接口的IBinder對象。其他應用程序組件可以調用bindService()方法獲取接口並且調用服務上的方法。
創建一個綁定的服務,第一件事就是定義一個說明客戶端與服務通信方式的接口。這個接口必須是IBinder的實現,並且必須要從onBind()方法返回。一旦客戶端接收到了IBinder,就可以通過這個接口進行交互。
多個客戶端可以綁定到一個服務,可以用unbindService()方法解除綁定,當沒有組件綁定在服務上,這個服務就會被銷燬。
/**Activity*/
private ServiceConnection connB = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
Log.v(tag, "Service B disconnected");
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.v(tag, "Service B connected");
MyBinderB binder = (MyBinderB) service;
ServiceB SB = binder.getService();
SB.showLog();
}
};
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.button1).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent a = new Intent(MainActivity.this, ServiceB.class);
bindService(a, connB, BIND_AUTO_CREATE);
}
}
}
/**Service*/
public class ServiceB extends Service {
public void showLog() {
Log.i(tag, "serviceB-->showLog()");
}
public class MyBinderB extends Binder {
public ServiceB getService() {
return ServiceB.this;
}
}
private MyBinderB myBinderB = new MyBinderB();
@Override
public IBinder onBind(Intent intent) {
return myBinderB;
}
}
啓動前臺服務
前臺服務是被認爲是用戶已知的正在運行的服務,當系統需要釋放內存時不會優先殺掉該進程。前臺進程必須發一個notification在狀態欄中顯示,直到進程被殺死。因爲前臺服務會一直消耗一部分資源,但不像一般服務那樣會在需要的時候被殺掉,所以爲了能節約資源,保護電池壽命,一定要在建前臺服務的時候發notification,提示用戶。當然,系統提供的方法就是必須有notification參數的,所以不要想着怎麼把notification隱藏掉。
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
Notification noti = new Notification.Builder(this)
.setContentTitle("Title")
.setContentText("Message")
.setSmallIcon(R.drawable.ic_launcher)
.setContentIntent(pendingIntent)
.build();
startForeground(12346, noti);
return Service.START_STICKY;
}
startForeground()方法就是將服務設爲前臺服務。參數12346就是這個通知唯一的id,只要不爲0即可。
Service的生命週期圖
啓動的服務的生命週期:
startService()->onCreate()->onStartCommand()->running->stopService()/stopSelf()->onDestroy()->stopped
其中,服務未運行時會調用一次onCreate(),運行時不調用。
綁定的服務的生命週期:
bindService()->onCreate()->onBind()->running->onUnbind()->onDestroy()->stopped
服務起始於onCreate(),終止於onDestory()
服務的開關過程,只有onStartCommand()可多次調用,其他在一個生命週期只調用一次。
這兩個過程並不完全獨立,也可以綁定一個由startService()啓動過的服務。
學習Android的同學注意了!!!
學習過程中遇到什麼問題或者想獲取學習資源的話,歡迎加入Android學習交流羣,羣號碼:364595326 我們一起學Android!