2017-10-09
概述
所謂Android系統服務其本質就是一個通過AIDL跨進程通信的小Demo的延伸而已。按照 AIDL 跨進程通信的標准創建一套程序,將服務端通過系統進程來運行實現永駐內存,在其它程序中就可以通過約定好的方式來建立通信了。而所謂回調,本質上也是一個 AIDL 跨進程通信,只不過是將回調的服務端放在系統服務通信的客戶端中而已。
本實例我們模擬一個燈光管理功能。設備中有一盞燈,我們定義一個系統服務用於統一控制燈的亮滅操作,客戶端可以發送控制燈光的請求給服務端,也可以接收來自服務端的反饋信息。系統環境為 Android 4.4 。
本文所述的知識,是本人參考了很多網上的前輩們的文章才學習到的,在這里將本人對於系統服務及添加回調接口的理解書以成文,以期能將知識分享出去。
創建一個系統服務
一般不會直接將新建的代碼文件混入Android原生代碼目錄中,這里我們的習慣是創建一個自己的組織或者公司的名稱的目錄專門用於存放這些自定義代碼,作為演示,本例程中的代碼都放置於自建的 manufactor 目錄內。
撰寫基礎代碼
首先定義一個 AIDL 文件,此處為簡化實例,僅定義一個接口。
1 ./frameworks/base/core/java/com/manufactor/ILightManager.aidl 2 3 package com.manufactor; 4 5 interface ILightManager { 6 boolean setLight(boolean isOpen); 7 }
其次是定義暴露給其它程序使用的Manager接口類 LightManager.java 這個類的定義方式,尤其是構造函數可以說是有固定格式的,按照下列代碼中的格式來書寫即可。
1 ./frameworks/base/core/java/com/manufactor/LightManager.java 2 3 package com.manufactor; 4 5 import android.util.Log; 6 import android.content.Context; 7 import android.os.RemoteException; 8 9 import com.manufactor.ILightManager; 10 11 public class LightManager { 12 13 private ILightManager manager; 14 15 public LightManager(Context ctx, ILightManager manager){ 16 this.manager = manager; 17 } 18 19 public boolean setLight(boolean isOpen){ 20 try{ 21 //將點燈請求發送到服務端處理。 22 return manager.setLight(isOpen); 23 }catch(RemoteException e){ 24 e.printStackTrace(); 25 } 26 return false; 27 } 28 }
再然后是創建服務端的程序 LightService.java 。一般而言,服務端里寫的代碼就是真正的干活的代碼了。但是在這里,我們僅是添加一個打印作為演示就夠了。
1 ./frameworks/base/services/java/com/manufactor/server/LightService.java 2 3 4 package com.manufactor.server; 5 6 import android.content.Context; 7 import android.util.Log; 8 9 import com.manufactor.ILightManager; 10 11 public class LightService extends ILightManager.Stub { 12 13 private static final String TAG = "LightService"; 14 15 private Context mContext; 16 17 public LightService(Context ctx){ 18 /* 19 可根據自己的實際情況來決定是否需要傳入 Context 參數。 20 */ 21 mContext = ctx; 22 } 23 24 @Override 25 public boolean setLight(boolean iso){ 26 Log.d(TAG, "setLight:"+iso); 27 return true; 28 } 29 }
至此,創建一個系統服務的第一步就已經完成了。萬事俱備,只欠東風。接下來就要將服務端在系統進程中注冊,並且要讓 LightService 與 LightManager 產生聯系。
在系統中注冊
將定義接口的 AIDL 添加到編譯隊列中。
1 ./frameworks/base/Android.mk
在 framework-base 模塊中將 aidl 文件的路徑添加進去,參照 mk 文件中已有的添加 aidl 的寫法即可。
在 Context 中添加用於識別這個服務的標識符。這條其實不重要,但是為了規范起見,還是應該不辭麻煩地寫上的。
1 ./frameworks/base/core/java/android/content/Context.java
然后是注冊成為系統服務,開機自動運行。
1 ./frameworks/base/services/java/com/android/server/SystemServer.java
這個類里有一個內部類 ServerThread 。注冊的動作一般寫在這個內部類的 public void initAndLoop() 方法中。其實就是參照文件中已有的注冊服務的寫法,在其后面添加上自己的服務注冊代碼即可。系統中幾乎所有的服務都是在這啟動的,如果你寫的服務對其它服務有依賴關系,那么就應該考慮代碼放置的先后順序問題。在本實例中,我們沒有依賴其它服務,因此不需要考慮放置位置。
上圖中黃色底紋處就用到了我們在上一步中定義的標識符。
如此一來,我們的燈光管理服務就會隨着系統的啟動而運行了。下面還有最后一步:將 LightService 與 LightManager “建立聯系”。
在 ContextImpl 類的靜態初始化塊中實現。
./frameworks/base/core/java/android/app/ContextImpl.java
如此一來,在其它程序中,通過 mContext.getSystemServer() 時,傳入燈光管理的標識符,就可以得到 LightManager 類的對象了,從而也就可以與燈光管理的服務端進行通信了。
編譯,將編譯產物 framework.jar framework2.jar services.jar 推到設備的 /system/framework/ 目錄下,重啟系統即可。
對於某些源代碼,可能還需要先執行一下 make update-api 命令后才可完成編譯工作。
至此,整個的系統級服務就已經完成了,下面我們再寫一個測試程序來測試一下是否可以工作。
測試APK
創建一個Activity,有一個按鈕,點擊它,即與燈光管理服務發消息。下面列出主要代碼
1 public class MainActivity extends Activity { 2 3 private static final String TAG = "LightTestAPK"; 4 5 private LightManager lm; 6 7 @Override 8 protected void onCreate(Bundle savedInstanceState) { 9 super.onCreate(savedInstanceState); 10 setContentView(R.layout.activity_main); 11 12 //取得實例。 13 lm = (LightManager)getSystemService(Context.LIGHTSERVICE); 14 if(lm == null) { 15 throw new RuntimeException("LightService doesn't working!"); 16 } 17 } 18 19 public void click(View v) { 20 boolean ret = lm.setLight(false); 21 Log.d(TAG, "setLight result:"+ret); 22 } 23 }
Android.mk中要引用 framework JAVA庫才能編譯通過。
LOCAL_JAVA_LIBRARIES := framework
將APK安裝進設備中,運行它,並監聽日志即可證明燈光管理服務的運行狀態。
實現系統服務的回調
根據上述方式添加的系統服務,它並不是一個“全雙工”通信方式的程序,服務端一般不能主動聯系客戶端。那么要如何實現這一功能呢?我們可以嘗試給它添加一個回調功能。
前面概述中也講到過,所謂回調其實就是另外創建多一個 AIDL 通信程序,將服務端放到燈光管理程序中的客戶端里去,將回調的客戶端放到燈光管理程序的服務端中去而已。
首先先定義用於回調的接口。這個接口用於外部程序注冊回調時作為參數來使用。
1 ./frameworks/base/core/java/com/manufactor/LightListener.java 2 3 package com.manufactor; 4 5 public interface LightListener { 6 void onStatusChange(boolean isOpen); 7 }
其次,定義用於描述上述接口的 AIDL 類。這個 AIDL 的作用就是用於燈光管理程序中的服務端與客戶端之間進行回調的跨進程通信。
1 ./frameworks/base/core/java/com/manufactor/ILightListener.java 2 3 package com.manufactor; 4 5 interface ILightListener { 6 void onStatusChange(boolean isOpen); 7 }
再然后便是創建一個類用於實現上面定義的 AIDL 接口。這個類在燈光管理程序中的客戶端中創建實例,由於它是 AIDL 的子類,因此可以作為參數傳遞給燈光管理程序的服務端,在服務端中保留一個“句柄”,供服務端回調用。
1 ./frameworks/base/core/java/com/manufactor/LightListenerCallback.java 2 3 4 package com.manufactor; 5 6 import com.manufactor.ILightListener; 7 import com.manufactor.LightListener; 8 9 10 11 public class LightListenerCallback extends ILightListener.Stub { 12 13 private LightListener listener; 14 15 public LightListenerCallback(LightListener lstn){ 16 listener = lstn; 17 } 18 19 @Override 20 public void onStatusChange(boolean isOpen){ 21 if(listener != null){ 22 listener.onStatusChange(isOpen); 23 } 24 } 25 }
至此,回調接口已經全部定義好了。接下來就是將定義好的回調接口應用到現有的系統服務程序中去了。
先來改造一下 ILightManager.aidl 添加一個注冊回調的接口。在前面描述的 ILightManager.aidl 中添加多一個接口即可:
void setOnListener(ILightListener lstn);
然后便是在 LightManager.java 中添加暴露給外部應用調用的注冊回調的方法:
1 ./frameworks/base/core/java/com/manufactor/LightManager.java 2 3 public void setOnListener(LightListener lstn){ 4 try{ 5 LightListenerCallback callback = new LightListenerCallback(lstn); 6 manager.setOnListener(callback); 7 }catch(RemoteException e){ 8 e.printStackTrace(); 9 } 10 }
外部應用在調用這個方法進行回調方法注冊時,就可以像為Button注冊點擊事件監聽一樣,將回調方法對象作為參數傳入即可。
最后一步便是在 LightService.java 中處理傳遞過來的回調類“句柄”。這里的改造主要是實現了在 ILightManager.aidl 中新增的注冊回調的方法,以及模擬了一個燈泡狀態改變的事件,用於調用回調接口,以將消息通知到客戶端中去。在這里,為了能實現一個服務端支持多個客戶端同時注冊事件監聽,要把接收到的回調接口“句柄”用一個集合類來管理。並且,還應該實現反注冊回調監聽的功能,但是這里為了簡化實例,就不做反注冊的功能了。
1 ./frameworks/base/service/java/com/manufactor/server/LightService.java 2 3 public class LightService extends ILightManager.Stub { 4 5 private static final String TAG = "LightService"; 6 7 private Context mContext; 8 private ArrayList<ILightListener> lList = new ArrayList<ILightListener>(); 9 10 public LightService(Context ctx){ 11 /* 12 可根據自己的實際情況來決定是否需要傳入 Context 參數。 13 */ 14 mContext = ctx; 15 //模擬狀態發生改變的事件。 16 new Thread(){ 17 public void run(){ 18 while(true){ 19 try{ 20 Thread.sleep(2000); 21 }catch(Exception e){ 22 23 } 24 //通知客戶端,燈泡狀態發生改變。 25 for(ILightListener l:lList){ 26 try{ 27 l.onStatusChange(true); 28 }catch(RemoteException e){ 29 e.printStackTrace(); 30 } 31 } 32 } 33 } 34 }.start(); 35 } 36 37 @Override 38 public boolean setLight(boolean iso){ 39 Log.d(TAG, "setLight:"+iso); 40 return true; 41 } 42 43 @Override 44 public void setOnListener(ILightListener lstn){ 45 /* 46 將注冊的回調通過集合來管理,可以實現多客戶端 47 同時監聽的功能。 48 */ 49 if(!lList.contains(lstn)){ 50 lList.add(lstn); 51 } 52 } 53 }
如此一來,在系統服務中回調的功能就全部做好了。我們再在測試APK中添加測試方法。還是在上面添加系統服務時的測試APK的基礎之上添加測試回調的代碼。
編譯,推入設備中,監聽日志,運行后可以看到如下圖所示的日志:
由打印可以看到,正序傳遞消息沒有問題,服務端回調功能也運行正常。
當然,其實服務端與客戶端通信的功能完全可以通過發送廣播或者其它進程間通信的方式來實現 。喜歡哪種用哪種,我只是覺得這種回調的方式挺有趣的~
代碼: https://pan.baidu.com/s/1kUPnJBD