Android Service詳解(一)
本文主要介紹Service相關的使用,以及使用Service實現IPC通訊
What is a Service
根據官方的介紹:
- Service既不是一個執行緒,Service通常執行在當成宿主程序的主執行緒中,所以在Service中進行一些耗時操作就需要在Service內部開啟執行緒去操作,否則會引發ANR異常。
- 也不是一個單獨的程序。除非在清單檔案中宣告時指定程序名,否則Service所在程序就是application所在程序。
程序存在的目的有2個:
- 告訴系統,當前程式需要在後臺做一些處理。這意味著,Service可以不需要UI就在後臺執行,不用管開啟它的頁面是否被銷燬,只要程序還在就可以在後臺執行。可以通過startService()方式呼叫,這裡需要注意,除非Service手動呼叫stopService()或者Service內部主動呼叫了stopSelf(),否則Service一直執行。
- 程式通過Service對外開放某些操作。通過bindService()方式與Service呼叫,長期連線和互動,Service生命週期和其繫結的元件相關。
Service Lifecycle
public class MyService extends Service { @Override public void onCreate() { super.onCreate(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { return startCommandReturnId; } @Override public void onDestroy() { super.onDestroy(); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } } 複製程式碼
要解釋這個首先要知道Service的實現,需要實現抽象方法onBind,以及重寫onStartCommand,這2個方法會在下文介紹到。
通過上面的介紹可以知道,Service有3種啟動方式:
- startService()
- bindService()
- 同時呼叫
這幾種方式啟動的Service生命週期略微不同。
startService方式
startService()只要一個Intent引數,指定要開啟的Service即可
Intent intent = new Intent(MainActivity.this, MyService.class); 複製程式碼
-
當呼叫Service的startService()後,
- Service首次啟動,則先呼叫onCreate(),在呼叫onStartCommand()
- Service已經啟動,則直接呼叫onStartCommand()
-
當呼叫stopSelf()或者stopService()後,會執行onDestroy(),代表Service生命週期結束。
-
startService方式啟動Service不會呼叫到onBind()。 startService可以多次呼叫,每次呼叫都會執行onStartCommand()。 不管呼叫多少次startService,只需要呼叫一次stopService就結束。 如果startService後沒有呼叫stopSelf或者stopService,則Service一直存活並執行在後臺。
-
onStartCommand的返回值一共有3種
- START_STICKY = 1:service所在程序被kill之後,系統會保留service狀態為開始狀態。系統嘗試重啟service,當服務被再次啟動,傳遞過來的intent可能為null,需要注意。
- START_NOT_STICKY = 2:service所在程序被kill之後,系統不再重啟服務
- START_REDELIVER_INTENT = 3:系統自動重啟service,並傳遞之前的intent
預設返回START_STICKY;
bindService方式
通過bindService繫結Service相對startService方式要複雜一點。 由於bindService是非同步執行的,所以需要額外構建一個ServiceConnection物件用與接收bindService的狀態,同時還要指定bindService的型別。
//1. 定義用於通訊的物件,在Service的onBind()中返回的物件。 public class MyBind extends Binder { public int mProcessId; } //2. 定義用於接收狀體的ServiceConnection mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { //和服務繫結成功後,服務會回撥該方法 //服務異常中斷後重啟,也會重新呼叫改方法 MyService.MyBind myBinder = (MyService.MyBind) service; } @Override public void onNullBinding(ComponentName name) { //Service的onBind()返回null時將會呼叫這個方法,並不會呼叫onServiceConnected() } @Override public void onServiceDisconnected(ComponentName name) { // 當服務異常終止時會呼叫。 // 注意,unbindService時不會呼叫 } }; //3. 在需要的地方繫結到Service bindService(intent, mServiceConnection, BIND_AUTO_CREATE); 複製程式碼
bindService()也可以呼叫多次,與startService()不同,當發起物件與Service已經成功繫結後,不會多次返回ServiceConnection中的回撥方法。
通過bindService方式與Service進行繫結後,當沒有物件與Service繫結後,Service生命週期結束,這個過程包括繫結物件被銷燬,或者主動掉呼叫unbindService()
startService和bindService同時開啟
當同時呼叫startService和bindService後,需要分別呼叫stopService和unbindService,Service才會走onDestroy()
一個Service必須要在既沒有和任何Activity關聯又處理停止狀態的時候才會被銷燬。
程式碼實操---與遠端程序的Service繫結
上面的程式碼都是在當前程序內跟Service通訊,現在我們來實現一下,不同程序內Service如何繫結。
主要步驟是這樣的
- 編寫aidl檔案,AS自動生成的java類實現IPC通訊的代理
- 繼承自己的aidl類,實現裡面的方法
- 在onBind()中返回我們的實現類,暴露給外界
- 需要跟Service通訊的物件通過bindService與Service繫結,並在ServiceConnection接收資料。
我們通過程式碼來實現一下:
-
首先我們需要新建一個Service
public class MyRemoteService extends Service { @Nullable @Override public IBinder onBind(Intent intent) { Log.e("MyRemoteService", "MyRemoteService thread id = " + Thread.currentThread().getId()); return null; } } 複製程式碼
-
在manifest檔案中宣告我們的Service同時指定執行的程序名, 這裡並是不只能寫remote程序名,你想要程序名都可以
<service android:name=".service.MyRemoteService" android:process=":remote" /> 複製程式碼
-
新建一個aidl檔案使用者程序間傳遞資料。
AIDL支援的型別:八大基本資料型別、String型別、CharSequence、List、Map、自定義型別。List、Map、自定義型別放到下文講解。
裡面會有一個預設的實現方法,刪除即可,這裡我們新建的檔案如下:
package xxxx;//aidl所在的包名 //interface之前不能有修飾符 interface IProcessInfo { //你想要的通訊用的方法都可以在這裡新增 int getProcessId(); } 複製程式碼
-
實現我們的aidl類
public class IProcessInfoImpl extends IProcessInfo.Stub { @Override public int getProcessId() throws RemoteException { return android.os.Process.myPid(); } } 複製程式碼
-
在Service的onBind()中返回
public class MyRemoteService extends Service { IProcessInfoImpl mProcessInfo = new IProcessInfoImpl(); @Nullable @Override public IBinder onBind(Intent intent) { Log.e("MyRemoteService", "MyRemoteService thread id = " + Thread.currentThread().getId()); return mProcessInfo; } } 複製程式碼
-
繫結Service
mTvRemoteBind.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(MainActivity.this, MyRemoteService.class); bindService(intent, mRemoteServiceConnection, BIND_AUTO_CREATE); } }); mRemoteServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { Log.e("MainActivity", "MyRemoteService onServiceConnected"); // 通過aidl取出資料 IProcessInfo processInfo = IProcessInfo.Stub.asInterface(service); try { Log.e("MainActivity", "MyRemoteService process id = " + processInfo.getProcessId()); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { Log.e("MainActivity", "MyRemoteService onServiceDisconnected"); } }; 複製程式碼
只要繫結成功就能在有log列印成MyRemoteService所在程序的程序id。這樣我們就完成了跟不同程序的Service通訊的過程。
程式碼實操---呼叫其他app的Service
跟調同app下不同程序下的Service相比,呼叫其他的app定義的Service有一些細微的差別
-
由於需要其他app訪問,所以之前的bindService()使用的隱式呼叫不在合適,需要在Service定義時定義action
我們在定義的執行緒的App A 中定義如下Service:
<service android:name=".service.ServerService"> <intent-filter> //這裡的action自定義 <action android:name="com.jxx.server.service.bind" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </service> 複製程式碼
-
我們在需要bindService的App B 中需要做這些處理
-
首先要將A中定義的aidl檔案複製到B中,比如我們在上面定義的IProcessInfo.aidl這個檔案,包括路徑在內需要原封不動的複製過來。
-
在B中呼叫Service通過顯式呼叫
mTvServerBind.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(); intent.setAction("com.jxx.server.service.bind");//Service的action intent.setPackage("com.jxx.server");//App A的包名 bindService(intent, mServerServiceConnection, BIND_AUTO_CREATE); } }); 複製程式碼
-
aidl中自定義物件的傳遞
主要步驟如下:
- 定義自定物件,需要實現Parcelable介面
- 新建自定義物件的aidl檔案
- 在傳遞資料的aidl檔案中引用自定義物件
- 將自定義物件以及aidl檔案拷貝到需要bindService的app中,主要路徑也要原封不動
我們來看一下具體的程式碼:
-
定義自定義物件,並實現Parcelable介面
public class ServerInfo implements Parcelable { public ServerInfo() { } String mPackageName; public String getPackageName() { return mPackageName; } public void setPackageName(String packageName) { mPackageName = packageName; } protected ServerInfo(Parcel in) { mPackageName = in.readString(); } public static final Creator<ServerInfo> CREATOR = new Creator<ServerInfo>() { @Override public ServerInfo createFromParcel(Parcel in) { return new ServerInfo(in); } @Override public ServerInfo[] newArray(int size) { return new ServerInfo[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(mPackageName); } //使用out或者inout修飾時需要自己新增這個方法 public void readFromParcel(Parcel dest) { mPackageName = dest.readString(); } } 複製程式碼
-
新建自定義物件的aidl檔案
package com.jxx.server.aidl; //注意parcelable 是小寫的 parcelable ServerInfo; 複製程式碼
-
引用自定義物件
package com.jxx.server.aidl; //就算在同一包下,這裡也要導包 import com.jxx.server.aidl.ServerInfo; interface IServerServiceInfo { ServerInfo getServerInfo(); void setServerInfo(inout ServerInfo serverinfo); } 複製程式碼
注意這裡的set方法,這裡用了inout,一共有3種修飾符
- in:客戶端寫入,服務端的修改不會通知到客戶端 - out:服務端修改同步到客戶端,但是服務端獲取到的物件可能為空 - inout:修改都收同步的 複製程式碼
當使用out和inout時,除了要實現Parcelable外還要手動新增readFromParcel(Parcel dest)
-
拷貝自定義物件以及aidl檔案到在要引用的App中即可。
-
引用
mServerServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { IServerServiceInfo serverServiceInfo = IServerServiceInfo.Stub.asInterface(service); try { ServerInfo serviceInfo = serverServiceInfo.getServerInfo(); Log.e("MainActivity", "ServerService packageName = " + serviceInfo.getPackageName()); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { Log.e("MainActivity", "ServerService onServiceDisconnected"); } }; 複製程式碼
List、Map中引用的物件也應該是符合上面要求的自定義物件,或者其他的幾種資料型別。