Android四大組件之Service

1. Android中進程和線程概述

1.1. Android中的進程

當一個程序第一次啓動的時候,Android會啓動一個Linux進程和一個主線程。默認的狀況下,全部該程序的組件都將在該進程和線程中運行。Android會盡可能保留一個正在運行進程,只在內存資源出現不足時,Android會嘗試中止一些進程從而釋放足夠的資源給其餘新的進程使用,也能保證用戶正在訪問的當前進程有足夠的資源去及時地響應用戶的事件。 
咱們能夠將一些組件運行在其餘進程中,而且能夠爲任意的進程添加線程。組件運行在哪一個進程中是在manifest文件裏設置的,其中<activity><service><receiver><provider>都有一個process屬性來指定該組件運行在哪一個進程之中。咱們能夠設置這個屬性,使得每一個組件運行在它們本身的進程中,或是幾個組件共同享用一個進程,或是不共同享用。<application>元素也有一個process屬性,用來指定全部的組件的默認屬性。 
Android會根據進程中運行的組件類別以及組件的狀態來判斷該進程的重要性,Android會首先中止那些不重要的進程。按照重要性從高到低一共有五個級別:java

1.前臺進程:android

前臺進程是用戶當前正在使用的進程。只有一些前臺進程能夠在任什麼時候候都存在。他們是最後一個被結束的,當內存低到根本連他們都不能運行的時候。通常來講,在這種狀況下,設備會進行內存調度,停止一些前臺進程來保持對用戶交互的響應。 
若是有如下的情形的那麼就是前臺進程: 
(a)這個進程運行着一個正在和用戶交互的Activity(這個Activity的onResume()方法被調用)。 
(b)這個進程裏有綁定到當前正在和用戶交互的Activity的一個service。 
(c)這個進程裏有一個service對象,這個service對象正在執行一個它的生命週期的回調函數(onCreate(), onStart(), onDestroy()) 
(d)這個進程裏有一個正在運行onReceive()方法的BroadCastReiver對象。數據庫

2.可見進程api

  可見進程是不包含前臺的組件可是仍會影響用戶在屏幕上所見內容的進程,除非前臺進程須要獲取它的資源,否則不會被停止。 
  若是有以下的一種情形就是可見進程: 
(a)這個進程中含有一個不位於前臺的Activity,可是仍然對用戶是可見的(這個Activity的onPause()方法被調用),這是極可能發生的,例如,若是前臺Activity是一個對話框的話,就會容許在它後面看到前一個Activity。 
(b)這個進程裏有一個綁定到一個可見的Activity的Service。緩存

3.服務進程安全

  運行着一個經過startService() 方法啓動的service,它不會升級爲上面兩種級別。service所在的進程雖然對用戶不是直接可見的,可是他們執行了用戶很是關注的任務(好比播放mp3,從網絡下載數據)。只要前臺進程和可見進程有足夠的內存,系統不會回收他們。服務器

4.後臺進程網絡

運行着一個對用戶不可見的activity(調用過 onStop() 方法)。這些進程對用戶體驗沒有直接的影響,能夠在服務進程、可見進程、前臺進程須要內存的時候回收。一般,系統中會有不少不可見進程在運行,他們被保存在LRU (least recently used) 列表中,以便內存不足的時候被第一時間回收。若是一個activity正確的執行了它的生命週期,關閉這個進程對於用戶體驗沒有太大的影響。app

5.空進程ide

  未運行任何程序組件。運行這些進程的惟一緣由是做爲一個緩存,縮短下次程序須要從新使用的啓動時間。系統常常停止這些進程,這樣能夠調節程序緩存和系統緩存的平衡。 
Android 對進程的重要性評級的時候,選取它最高的級別。例如,若是一個進程含有一個service和一個可視activity,進程將被納入一個可視進程而不是service進程。

1.2. Android中的線程

應用程序啓動時,系統會爲它建立一個名爲「main」的主線程。主線程很是重要,由於它負責把事件分發給相應的用戶界面widget——包括屏幕繪圖事件。它也是應用程序與Android UI組件包(來自android.widget和android.view包)進行交互的線程。所以,主線程有時也被叫作UI線程。

系統並不會爲每一個組件的實例都建立單獨的線程。運行於同一個進程中的全部組件都是在UI線程中實例化的,對每一個組件的系統調用也都是由UI線程分發的。所以,對系統回調進行響應的方法(好比報告用戶操做的onKeyDown()或生命週期回調方法)老是運行在UI線程中。

若是UI線程須要處理每一件事情,那些耗時很長的操做——諸如訪問網絡或查詢數據庫等將會阻塞整個UI(線程)。一旦線程被阻塞,全部事件都不能被分發,包括屏幕繪圖事件。從用戶的角度看來,應用程序看上去像是掛起了。更糟糕的是,若是UI線程被阻塞超過必定時間(目前大約是5秒鐘),用戶就會被提示那個可惡的「應用程序沒有響應」(ANR)對話框。

此外,Andoid的UI組件包並非線程安全的。所以不容許從工做線程中操做UI——只能從UI線程中操做用戶界面。因而,Andoid的單線程模式必須遵照兩個規則: 
(a)不要阻塞UI線程。 
(b)不要在UI線程以外訪問Andoid的UI組件包。

2. Service

2.1. Service概述

Service是Android中四大組件之一,在Android開發中有很是重要的做用。 
Service(服務)是一個沒有用戶界面的在後臺運行執行耗時操做的應用組件。其餘應用組件可以啓動Service,而且當用戶切換到另外的應用場景,Service將持續在後臺運行。另外,一個組件可以綁定到一個service與之交互(IPC機制),例如,一個service可能會處理網絡操做,播放音樂,操做文件 I/O或者與內容提供者(content provider)交互,全部這些活動都是在後臺進行。

2.2. Service的兩種狀態

Service有兩種狀態,「啓動的」和「綁定」:

Started

經過startService() 啓動的服務處於「啓動的」狀態,一旦啓動,Service就在後臺運行,即便啓動它的應用組件已經被銷燬了。一般started狀態的Service執行單任務而且不返回任何結果給啓動者。好比當下載或上傳一個文件,當這項操做完成時,Service應該中止它自己。

Bound

還有一種「綁定」狀態的Service,經過調用bindService()來啓動,一個綁定的Service提供一個容許組件與Service交互的接口,能夠發送請求、獲取返回結果,還能夠經過跨進程通訊來交互(IPC)。綁定的Service只有當應用組件綁定後才能運行,多個組件能夠綁定一個Service,當調用unbind()方法時,這個Service就會被銷燬了。

另外,在官方的說明文檔中還有一個警告: 
Service 與Activity同樣都存在與當前進程的主線程中,因此,一些阻塞UI的操做,好比耗時操做不能放在Service裏進行,好比另外開啓一個線程來處理 諸如網絡請求的耗時操做。若是在Service裏進行一些耗CPU和耗時操做,可能會引起ANR警告,這時應用會彈出強制關閉仍是等待的對話框。因此,對Service的理解就是和Activity平級的,只不過是看不見的,在後臺運行的一個組件,這也是爲何和Activity同被說爲Android的基本組件。

2.3. Service的簡單使用

1) 建立一個Service,須要繼承Service類。

2) 覆蓋一些回調函數來處理服務生命週期的一些關鍵要素,而且若是合適的話,須要提供一個機制讓組件綁定到服務。這些最重要的須要覆蓋的函數以下:

onStartCommand():

onStartCommand()方法當另外一個組件(如Activity)經過調用startService()請求Service啓動時調用。一旦這個方法執行,這個服務就被開啓而且在後臺無限期地運行。若是你實現了這個方法,你就有責任在服務工做完畢後經過調用stopSelf()或者stopService()關閉它(若是僅僅用來提供綁定,就不須要實現這個方法)。

onBind():

onBind()方法當另外一個組件(如執行RPC)經過bindService()和這個服務綁定的時候調用。在這個方法的實現中,須要經過返回一個IBinder對象提供客戶端用來和Service通信的接口,你必須一致實現該方法,除非不想綁定服務,這時候須要返回null。

onCreate()

Service第一次建立的時候調用該方法來執行一次性安裝程序(以前調用要麼onStartCommand()要麼onBind())。若是Service已經運行了,這個方法不會被調用。

onDestory()

當Service再也不使用而且被銷燬的時候調用。服務須要實現這個方法來清理資源如線程,註冊的監聽器,接受者等。這個方法是Service最後一個調用的。

3) 在清單文件中註冊服務

<manifest ... >
  ...
  <application ... >
    <service android:name=".ExampleService" />
    ...
  </application>
</manifest>

3. 服務的第一種啓動方式

服務的第一種啓動方式是調用startService()方法。 
建立一個服務,必須實現onBind()方法,實現onCreate(),onStartCommand()和onDestory()方法:

public class DemoService extends Service {
    @Override
    public IBinder onBind(Intent intent) {      
        return null;
    }
    @Override
    public void onCreate() {
        System.out.println("onCreate");
        super.onCreate();
    }   
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        System.out.println("onStartCommand");
        return super.onStartCommand(intent, flags, startId);
    }
    @Override
    public void onDestroy() {

        System.out.println("onDestroy");
        super.onDestroy();
    }   
}

在清單文件中註冊

<service android:name="com.itheima.servicedemo.DemoService"></service>

MainActivity中實現開啓服務 
這裏寫圖片描述 
點擊按鈕開啓服務

public void click(View v){
    Intent intent = new Intent(MainActivity.this,DemoService.class);
    startService(intent);   
}

查看Logcat日誌發現開啓服務調用onCreate()和onStartCommand()方法: 
這裏寫圖片描述

在此點擊按鈕,查看日誌,發現onStartCommand()又執行了一次: 
這裏寫圖片描述

那麼如何查看服務真的被啓動了呢?咱們能夠在模擬器設置界面中的應用界面查看,以下圖: 
這裏寫圖片描述

那麼如何中止服務呢?點開上圖標出的條目,跳轉到以下圖的另外一個界面,點擊中止按鈕。以下圖: 
這裏寫圖片描述

這時候,服務就中止了。咱們能夠查看Logcat日誌,發現調用了onDestory()方法: 
這裏寫圖片描述

經過上面的操做能夠得出調用startService()方法時Service的生命週期以下圖: 
這裏寫圖片描述

4. 電話竊聽器案例

本案例實如今後臺監聽電話狀態,當手機來電而且接通後,開始錄音,而且保存到sdcard中。 
爲何須要在服務中監聽電話狀態呢?由於服務開啓後能夠在後臺一直運行,若是放在Activity中監聽電話狀態,當Activity銷燬後就不能監聽到電話狀態了。 
使用Thread(){}.start()方法也能夠在後臺實現監聽,那麼爲何不使用子線程而使用Service呢?由於以前咱們已經瞭解了進程的等級,當應用程序退出時,當前應用的進程就變成一個空進程,最容易被系統回收。開啓服務是在服務進程中運行,服務進程的優先級比後臺進程高。

4.1. 監聽電話狀態

監聽電話狀態須要使用TelephonyManager類。TelephonyManager類主要提供了一系列用於訪問與手機通信相關的狀態和信息的get方法。其中包括手機SIM的狀態和信息、電信網絡的狀態及手機用戶的信息。

Context.getSystemService(Context.TELEPHONY_SERVICE)方法能夠用來獲取到TelephonyManager類的對象。須要注意的是有些通信信息的獲取,對應用程序的權限有必定的限制,在開發的時候須要爲其添加相應的權限。

查看api文檔,找到以下圖方法,能夠用來監聽電話狀態: 
這裏寫圖片描述

listen()方法,註冊一個監聽對象用來接收指定電話狀態變化的通知。 
在服務中利用TelephonyManager監聽電話狀態:

public class PhoneService extends Service {
    private TelephonyManager tManager;
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
    @Override
    public void onCreate() {
        tManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
        //調用TelephonyManager的listen()方法監聽電話狀態。參數1表示電話狀態監聽器,參數2表示須要監聽的電話事件
        tManager.listen(new MyPhoneStateListener(),PhoneStateListener.LISTEN_CALL_STATE);
        super.onCreate();
    }
    @Override
    public void onDestroy() {
        super.onDestroy();
    }
}

建立自定義電話狀態監聽器:

private class MyPhoneStateListener extends PhoneStateListener {
    @Override
    public void onCallStateChanged(int state, String incomingNumber) {
        super.onCallStateChanged(state, incomingNumber);
        //判斷state狀態
        switch (state) {
        case TelephonyManager.CALL_STATE_IDLE: // CALL_STATE_IDLE表示空閒狀態
            System.out.println("判斷用戶是否已經開啓錄音機,若是開啓,上傳");
            break;
        case TelephonyManager.CALL_STATE_OFFHOOK: // CALL_STATE_OFFHOOK表示接通狀態
            System.out.println("開始錄");
            break;
        case TelephonyManager.CALL_STATE_RINGING: // CALL_STATE_RINGING表示電話響鈴狀態
            System.out.println("電話響鈴的時候  我就準備一個錄音機 ");
            break;
        default:
            break;
        }
    }
}

清單文件加入讀取手機狀態權限:

<uses-permission android:name="android.permission.READ_PHONE_STATE" />

開啓服務

public void click(View view){
    Intent intent = new Intent(this,PhoneService.class);
    startService(intent);
}

運行結果 
當開啓服務後,沒有來電,Logcat打印以下日誌: 
這裏寫圖片描述

當有來電時Logcat打印以下日誌: 
這裏寫圖片描述 
這裏寫圖片描述

當掛斷電話後,打印以下日誌: 
這裏寫圖片描述 
這裏寫圖片描述

4.2. 錄音

上面已經實現了電話各類狀態的監聽,如今能夠在不一樣的狀態下操做錄音,將通話錄音記錄到sdcard,這裏須要用到MediaRecorder對象。具體的介紹咱們能夠參考api。 
這裏寫圖片描述

在監聽方法中實現錄音功能:

//建立MediaRecorder
private MediaRecorder recorder;
......
switch (state) {
    case TelephonyManager.CALL_STATE_IDLE: 
        //關閉Recorder,釋放資源
        if (recorder!=null) {
            recorder.stop();    
            recorder.reset();  
            recorder.release(); 
        }           
        break;
    case TelephonyManager.CALL_STATE_OFFHOOK: 
        System.out.println("開始錄");
        //開啓錄音
        recorder.start(); 
        break;      
    case TelephonyManager.CALL_STATE_RINGING:   
        //建立MediaRecorder對象
        recorder = new MediaRecorder(); 
        //設置音頻來源
        recorder.setAudioSource(MediaRecorder.AudioSource.MIC);  
        //設置影音的輸出格式
        recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
        //設置音頻文件的編碼格式
        recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
        //設置音頻文件保存的路徑
        recorder.setOutputFile("/mnt/sdcard/luyin.3gp");
        try {
            //準備錄音
            recorder.prepare();
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        break;
    default:
        break;
}

加入權限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

運行結果,能夠在sdcard中看到保存的錄音luyin.3gp。 
這裏寫圖片描述

4.3. 自動開啓服務

如何自動開啓服務呢?能夠在系統重啓時,利用廣播接受者接收系統重啓廣播,在廣播中開啓監聽電話狀態的服務。 
定義廣播接受者

public class BootReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Intent intent2 = new Intent(context,PhoneService.class);
        context.startService(intent2);
    }
}

須要加入權限

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

5. 服務的第二種啓動方式

開啓服務的第二種方式是bindService()。第一種方式是startService()方法開啓,與之對應的中止服務的方法是stopService(),bindService()對應的方法是unbindService()。 
首先編寫Service類:

public class TestService extends Service {
    //當服務被成功綁定的時候調用
    @Override
    public IBinder onBind(Intent intent) {
        System.out.println("onBind");
        return null;
    }
    @Override
    public void onCreate() {
        System.out.println("onCreate");
        super.onCreate();
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        System.out.println("onStartCommand");
        return super.onStartCommand(intent, flags, startId);
    }
    @Override
    public boolean onUnbind(Intent intent) {
        System.out.println("onUnbind");
        return super.onUnbind(intent);
    }
    @Override
    public void onDestroy() {
        System.out.println("onDestroy");
        super.onDestroy();
    }
}

而後實現Activity中按鈕點擊事件開啓服務: 
這裏寫圖片描述 
MainActivity中實現兩種方式開啓服務:

public class MainActivity extends Activity {
    private MyConn conn;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    // 採用start-service方式開啓服務
    public void click1(View v) {
        Intent intent = new Intent(this, TestService.class);
        startService(intent);
    }
    // 採用stop-service方式開啓服務
    public void click2(View v) {
        Intent intent = new Intent(this, TestService.class);
        stopService(intent);
    }
    // 採用bind-service方式開啓服務
    public void click3(View v) {
        Intent intent = new Intent(this, TestService.class);
        // conn 是ServiceConnection接口用來監視服務的狀態 flags 爲了綁定服務
        conn = new MyConn();
        //bindService()方法,用來綁定服務,當這個方法一調用,Servie中的onBind()方法就會執行。參數1表示須要傳入的服務的intent,參數2即是一個ServiceConnection的實現類,用來監視服務的狀態,參數3是綁定的操做選項
        bindService(intent, conn, BIND_AUTO_CREATE);
    }
    // 採用unbind-service方式開啓服務
    public void click4(View v) {
        unbindService(conn);
    }

    //MyConn類實現ServiceConnection接口,用來監視服務的狀態。其中有兩個回調方法,onServiceConnected()方法表示當服務器鏈接的時候調用,onServiceDisconnected()方法表示當服務失去鏈接時調用
    private class MyConn implements ServiceConnection {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service)
         {
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    }
    @Override
    protected void onDestroy() {
        //unbindService()方法,用來解綁服務
        //unbindService(conn);
        super.onDestroy();
    }
}

首先點擊bindService(),Service會調用onCreate()和onBind()方法,查看Logcat日誌: 
這裏寫圖片描述

咱們點擊返回鍵,這樣Activity就會銷燬,這時候查看日誌,發現Service調用了onDestory()方法,同時Logcat日誌會報錯,以下圖: 
這裏寫圖片描述 
這裏寫圖片描述

出現這個錯誤的緣由是因爲當Activity銷燬的時候會調用onDestory()方法,在Activity須要調用unbindService()方法將服務解綁。因此咱們還須要在Activity的onDestory()方法中加入如下代碼:

@Override
protected void onDestroy() {
    unbindService(conn);
    super.onDestroy();
}

接下來,咱們從新部署項目,而後點擊bind-service按鈕,Service會調用onCreate()和onBind()方法,Logcat日誌會打印以下結果: 
這裏寫圖片描述

這時候咱們查看模擬器設置裏的應用中查找咱們的服務,發現找不到咱們開啓的服務,以下圖: 
這裏寫圖片描述

而後點擊unbind-service按鈕,Service會調用onUnbind()和onDestory()方法,Logcat日誌會打印如下結果: 
這裏寫圖片描述

接着,咱們從新部署項目,連續點擊兩次bind-service按鈕,而後咱們點擊unbind-service按鈕,再次點擊unbind-service按鈕,這時候應用會崩潰,而且Logcat會輸出如下錯誤日誌: 
這裏寫圖片描述

總結: 
(a)使用bindService()綁定服務,不會使進程變成服務進程; 
(b)使用bindService()綁定服務,只能夠綁定一次,不能屢次綁定。 
使用bindService()方法開啓服務,Service的生命週期以下圖: 
這裏寫圖片描述

6. 調用服務中的方法

6.1. 普通方式調用服務中方法

建立一個Service,在Service中定義一個內部方法:

public class TestDemoService extends Service {
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
    @Override
    public void onCreate() {
        super.onCreate();
    }
    @Override
    public void onDestroy() {
        super.onDestroy();
    }
    public void serviceMethod(){
        Toast.makeText(this, "我是服務裏面的方法", 1).show();
    }
}

在MainActivity中點擊按鈕調用服務中的方法:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
     public void click(View v){
         //建立TestDemoService對象
        TestDemoService testDemoService = new TestDemoService();
        //調用serviceMethod()方法
        testDemoService.serviceMethod();
     }
}

運行結果: 
這裏寫圖片描述

應用程序會崩潰,而且Logcat會有以下日誌輸出: 
這裏寫圖片描述 
定位到代碼中,是Service中自定義的方法中彈出吐司出現了錯誤。因爲這邊調用Service服務中的方法是經過Service的一個實例來調用的,那麼這個實例對象就是一個普通的類,普通的類中沒有上下文,因此彈出吐司須要傳遞的上下文就爲null,因此程序會報空指針異常。

6.2. 經過IBinder調用服務中的方法

當使用bindService()綁定服務的時候,須要傳入一個參數,這個參數是ServiceConnection的一個實現類,這個實現類代碼以下:

private class MyConn implements ServiceConnection{
    @Override
    //onServiceConnected()方法當綁定成功後調用,其中有一個IBinder對象
    public void onServiceConnected(ComponentName name, IBinder service) {
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
    }
}

而後分析Service中的onBind()方法代碼:

@Override
public IBinder onBind(Intent intent) {
    return null;
}

經過onBind()方法發現這個方法返回值是IBinder類型,和咱們上面ServiceConnection中綁定成功回調中的一個參數類型一致。其實,這邊的兩個對象是同一個對象,當服務綁定成功後,會返回IBinder對象給ServiceConnection中的回調函數。

查看IBinder類,發現這個類是一個接口,因此咱們要麼實現IBinder接口,要麼繼承它已知的子類,這裏咱們繼承IBinder的一個子類Binder類。

public class TestDemoService extends Service {
    @Override
    public IBinder onBind(Intent intent) {
        //返回自定義MyBinder對象
        return new MyBinder();
    }
    @Override
    public void onCreate() {
        super.onCreate();
    }
    @Override
    public void onDestroy() {
        super.onDestroy();
    }
    public void serviceMethod(){
        Toast.makeText(this, "我是服務裏面的方法", 1).show();
    }   

    //建立MyBinder類繼承Binder類
    public class MyBinder extends Binder{
        //建立callServiceMethod()方法
        void callServiceMethod(){
            //調用Service中的serviceMethod()方法
            serviceMethod();
        }
    }
}

這時候當服務成功綁定後,ServiceConnection中的回調函數就能接收到這個對象,經過這個對象來調用服務中的方法。在MainActivity中綁定服務,而且經過IBinder調用服務中的方法:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    public void click(View v){
        bindService(new Intent(this,TestDemoService.class), new MyConn(), BIND_AUTO_CREATE);
    }   
    class MyConn implements ServiceConnection{
        @Override
        public void onServiceConnected(ComponentName name, IBinder service)
         {
             //接收Service中onBind()返回的IBinder對象,這個IBinder對象就是咱們自定義的MyBinder對象
            MyBinder binder = (MyBinder) service;
            //經過IBinder對象調用服務中的方法
            binder.callServiceMethod();
        }
        @Override
        public void onServiceDisconnected(ComponentName name) { 
        }
    }   
}

下圖是簡易的Activity中調用Service中的方法圖: 
這裏寫圖片描述

這時候,問題又來了,若是Service中有不少方法,那麼中間人IBinder就能夠調用Service中的方法,那麼如何控制IBinder中的方法呢?咱們能夠定義一個接口,將Service中的方法定義到接口中,讓咱們的自定義的Binder實現這個接口。

定義的接口:

public interface Iservice {
    public void callBanZheng(int money);
    public void callPlayMaJiang();
    public void callXiSangNa();
}

自定義的MyBinder實現該接口:

private class Mybinder extends Binder implements IService {
    @Override
    public void callBanZheng(int money) {
        banZheng(money);
    }
    @Override
    public void callPlayMaJiang() { 
         playMaJiang(); 
    }
    @Override
    public void callXiSangNa() {
         xiSangna();        
    }
}

這樣,自定義的MyBinder類就能夠調用Service中的方法。若是Service不但願MyBinder具備其中某個方法,那麼能夠在IService接口中不提供該方法。

7. 混合方式開啓Service

7.1. 音樂播放器案例

音樂播放器須要使用到服務,由於當音樂播放器須要在後臺也能運行,當手機按Home鍵後,音樂播放器也須要可以播放,本案例實模擬音樂播放器的實現。在後臺服務中,既要保證服務可以在後臺一直運行,又要保證Activity中可以調用服務中的方法,這就須要利用兩種啓動方法混合使用。

主界面以下圖: 
這裏寫圖片描述

定義一個服務MusicService用來在服務中播放音樂,暫停播放,繼續播放等:

public class MusicService extends Service {
    @Override
    public IBinder onBind(Intent intent) {
        return new MyBinder();
    }
    @Override
    public void onCreate() {
        super.onCreate();
    }
    @Override
    public void onDestroy() {
        super.onDestroy();
    }
    //定義開始播放方法
    public void play(){
        System.out.println("音樂開始播放了 ");
    }
    //定義音樂暫停方法
    public void pause(){
        System.out.println("音樂暫停了 ");
    }   
    //定義音樂繼續播放方法
    public void rePlay(){
        System.out.println("音樂繼續播放 ");
    }
    private class MyBinder extends Binder implements IService{
        @Override
        public void callPlay() {
            play();
        }
        @Override
        public void callPause() {
            pause();
        }
        @Override
        public void callReplay() {
            rePlay();
        }
    }
}

IService接口中定義方法:

public interface Iservice {
    public void callPlay();
    public void callPause();
    public void callReplay();
}

在Activity中點擊按鈕,實現音樂播放器的功能:

public class MainActivity extends Activity {
    private Myconn myconn;
    private Iservice iservice; 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent intent = new Intent(this,MusicService.class);
        //調用startService()方法開啓服務,保證服務在後臺長期運行
        startService(intent);
        myconn = new Myconn();
        //調用bindService()方法,目的是爲了調用服務中的方法
        bindService(intent, myconn, BIND_AUTO_CREATE);
    }
    public void click1(View v) {
        iservice.callPlay();
    }
    public void click2(View v) {
        iservice.callPause();
    }
    public void click3(View v) {
        iservice.callReplay();
    }
    private class Myconn implements ServiceConnection{
        @Override
        public void onServiceConnected(ComponentName name, IBinder service)
         {
            iservice = (Iservice) service;
        }
        @Override
        public void onServiceDisconnected(ComponentName name) { 
        }
    }
    @Override
    protected void onDestroy() {
        unbindService(myconn);
        super.onDestroy();
    }
}

運行結果: 
當應用程序一塊兒動,就開啓服務,而且一直運行。以下圖: 
這裏寫圖片描述

首先點擊播放,這時候音樂開始播放,Logcat輸出以下日誌: 
這裏寫圖片描述

而後點擊暫停,這時候音樂暫停播放,Logcat輸出以下日誌: 
這裏寫圖片描述

接下來點擊繼續播放,這時候音樂繼續播放,Logcat輸出以下日誌: 
這裏寫圖片描述

7.2. 混合方式開啓服務流程

(a)採用startServie()方法開啓服務,保證服務可以在後臺長期運行; 
(b)調用bindServie()方法,目的是可以調用服務中的方法; 
(c)Activity銷燬時調用unbindService()方法解綁; 
(d)最後調用stopService()方法中止服務。

8. 使用服務註冊特殊廣播接收者

安卓中,有些廣播須要廣播接受者動態註冊(採用代碼的方式註冊),好比電池電量低、解鎖屏等特殊的廣播。

8.1. 接收解鎖屏廣播

定義廣播接受者類ScreenReceiver:

public class ScreenReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if ("android.intent.action.SCREEN_OFF".equals(action)) {
            System.out.println("屏幕off了");
        }else if("android.intent.action.SCREEN_ON".equals(action)){
            System.out.println("屏幕on了");
        }
    }
}

在清單文件中註冊廣播:

<receiver android:name="com.itheima.register.ScreenReceiver" >
    <intent-filter>
        <action android:name="android.intent.action.SCREEN_ON" />
        <action android:name="android.intent.action.SCREEN_OFF" />
    </intent-filter>
</receiver>

咱們在模擬器上,解屏或者鎖屏,發現Logcat沒有日誌輸出,說明靜態註冊廣播接受者不能接收到解鎖屏的廣播。

爲何不能接收到屏幕解鎖屏的廣播呢?由於手機一天會解鎖屏不少次,因此廣播接受者會不斷的註冊,這樣就比較耗電,因此谷歌就對發送這種特殊的廣播作了特殊處理,只有動態註冊,才能監聽到這類廣播。

咱們在Activity中註冊廣播:

public class MainActivity extends Activity {
    private ScreenReceiver screenReceiver;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        screenReceiver = new ScreenReceiver();
        //建立IntentFilter對象,給該對象設置Action
        IntentFilter filter = new IntentFilter();
        filter.addAction("android.intent.action.SCREEN_OFF");
        filter.addAction("android.intent.action.SCREEN_ON");
        //調用Context對象的registerReceiver()註冊廣播。參數1表示廣播接受者,參數2表示意圖過濾器
        registerReceiver(screenReceiver, filter);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
}

運行結果: 
當點擊鎖屏按鈕,屏幕黑屏,Logcat會輸出以下日誌: 
這裏寫圖片描述 
這裏寫圖片描述

點擊解屏按鈕,屏幕亮起,Logcat會輸出以下日誌: 
這裏寫圖片描述

這時候,若是點擊返回按鈕,應用程序退出,Logcat會報以下錯誤,提示咱們須要調用unregisterReceiver()方法: 
這裏寫圖片描述

在Activity的onDestory()中調用unregisterReceiver()方法:

protected void onDestroy() {
    unregisterReceiver(screenReceiver);
    super.onDestroy();
}

8.2. 保持廣播監聽

由於在Activity中註冊廣播,當Activity被銷燬後,就不能監聽到解鎖屏的廣播,因此,能夠在Service中後臺一直監聽廣播。

public class ScreenService extends Service {
    private ScreenReceiver screenReceiver;
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
    @Override
    public void onCreate() {
        screenReceiver = new ScreenReceiver();
        IntentFilter filter = new IntentFilter();
        filter.addAction("android.intent.action.SCREEN_OFF");
        filter.addAction("android.intent.action.SCREEN_ON");
        registerReceiver(screenReceiver, filter);
        super.onCreate();
    }
    @Override
    public void onDestroy() {
        unregisterReceiver(screenReceiver);
        super.onDestroy();
    }
}

9. 進程間通訊

IPC(Inter-Process Communication):進程間通訊。 
Android系統中,每一個應用程序都會有一個進程,因爲應用程序之間不能共享內存,那麼就須要進程間通訊。

9.1. 本地服務和遠程服務

本地服務:服務和啓動它的組件在同一個進程。 
遠程服務:服務和啓動它的組件不在同一個進程。 
遠程服務只能隱式啓動,相似隱式啓動Activity,在清單文件中配置Service標籤時,必須配置intent-filter子節點,並指定action子節點。

9.2. AIDL

AIDL(Android Interface Definition Language):安卓接口定義語言。AIDL是安卓中用來跨進程通訊的一種接口定義語言。 
應用場景:遠程服務中的中間人對象,其餘應用是拿不到的,那麼在經過綁定服務獲取中間人對象時,就沒法強制轉換,使用aidl技術,就能夠在其餘應用中拿到中間人類所實現的接口。

9.3. AIDL的使用

建立一個遠程應用項目,在項目中建立一個遠程服務類RemoteService:

public class RemoteService extends Service {
    @Override
    public IBinder onBind(Intent intent) {
        return new MyBinder();
    }
    @Override
    public void onCreate() {
        super.onCreate();
    }
    public void remoteMethod(){
        System.out.println("我是遠程服務裏面的方法");
    }
    private class MyBinder extends Binder implements Iservice{
        @Override
        public void callRemoteMethod() {
            remoteMethod();
        }
    }
}

IService代碼:

public interface Iservice {
     public void callRemoteMethod();
}

配置遠程服務:

<service android:name="com.itheima.remoteservice.RemoteService"></service>

建立本地應用項目,在MainActivity中實現點擊按鈕事件:

public class MainActivity extends Activity {
    private Iservice iservice;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent intent = new Intent();
        bindService(intent, new MyConn(), BIND_AUTO_CREATE);
    }
    public void click(View v){  
    }
     private class MyConn implements ServiceConnection{
        //當服務被連接成功的時候調用 
        @Override
        public void onServiceConnected(ComponentName name, IBinder service)
        {   
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {     
        }
    }
}

以上是調用本地服務的操做步驟,下面利用aidl實現本地應用訪問遠程服務。 
首先,將定義方法的接口類Iservice文件的後綴名改爲.aidl,這時候刷新下工程,Iservice文件會報錯,以下圖: 
這裏寫圖片描述 
這裏寫圖片描述

從上圖能夠看出,須要將public關鍵字去掉,這樣咱們的Iservice就不會報錯。爲何不能使用public呢?既然Iservice這個aidl是用來解決進程間通訊的,那麼它確定是public的,因此不須要加上public。 
這時候到工程目錄的gen目錄下能夠看到Iservice.java這個文件,打開看一下,以下圖: 
這裏寫圖片描述

這裏寫圖片描述

從上圖能夠看到生成的文件中,有一個Stub類,繼承Binder類而且實現Iservice接口。這時候咱們發現,自定義的中間人能夠繼承這個Stub類。 
接下來咱們要在本地應用中經過中間人來訪問遠程服務,那麼如何保證本地應用的中間人和遠程服務的中間人是同一個呢?谷歌規定,只要在本地應用中,包名和aidl文件名和遠程服務一致就能夠了。 
遠程服務的包名: 
這裏寫圖片描述

本地應用中須要建立和遠程服務同樣的包名,而後將Iservice.aidl文件拷貝到本地應用中建立的包中: 
這裏寫圖片描述

作完以上操做後,在本地應用的gen目錄下就能夠看到自動生成的Iservice.java文件,有了這個文件,就能夠調用遠程服務中的方法,以下圖: 
這裏寫圖片描述

在本地應用的MainActivity中完善代碼:

public class MainActivity extends Activity {
    private Iservice iservice;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent intent = new Intent();
        //經過設置intent的action來指定打開的服務,這邊須要在遠程服務的項目的清單文件中配置這個action
        intent.setAction("com.itheima.remoteservice");
        bindService(intent, new MyConn(), BIND_AUTO_CREATE);
    }
    public void click(View v){
        try {
            //調用中間人對象調用遠程服務中的方法
            iservice.callRemoteMethod();
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
    private class MyConn implements ServiceConnection{
        @Override
        public void onServiceConnected(ComponentName name, IBinder service)
         {
             //經過Iservice.Stub.asInterface(service)方法獲取IBinder對象
             iservice = Iservice.Stub.asInterface(service);
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    }
}

在遠程服務的項目的清單文件中配置這個action

<service android:name="com.itheima.remoteservice.RemoteService">
     <intent-filter >
          <action android:name="com.itheima.remoteservice"/>
     </intent-filter>
</service>

運行效果: 
首先部署遠程服務到手機中,而後部署本地服務,點擊按鈕後,查看日誌,發現調用了遠程服務中的方法。以下圖: 
這裏寫圖片描述 
這裏寫圖片描述

10. 遠程服務的應用場景

10.1. 支付寶服務

遠程服務最多見的應用場景就是支付寶,本案例模擬支付寶支付過程。 
建立Iservice接口,在接口中定義中間人對象須要實現的方法:

public interface Iservice {
     public boolean callPay(String name,String pwd,int money);
}

將Iservice文件後綴名改成.aidl,取消訪問修飾符public,以下圖: 
這裏寫圖片描述

定義支付寶的服務,在服務中定義pay方法:

public class ALiPayService extends Service {
    @Override
    public IBinder onBind(Intent intent) {
        return new MyBinder();
    }
    @Override
    public void onCreate() {
        super.onCreate();
    }
    @Override
    public void onDestroy() {
        super.onDestroy();
    }
    //定義pay方法,返回值爲boolean類型,用來判斷支付結果(成功或失敗)
    public boolean pay(String name,String pwd,int money){
        System.out.println("檢查用戶名和密碼是否正確....");
        System.out.println("檢查手機是否有病毒....");
        System.out.println("檢查餘額是否夠用....");     
        if ("abc".equals(name)&&"123".equals(pwd)&&money>5000) {
            return true;
        }else {
            return false;
        }
    }
}

在服務中定義中間人對象MyBinder類,直接繼承Stub類,實現方法:

private class MyBinder extends Stub{
    @Override
    public boolean callPay(String name, String pwd, int money) {
         //調用Service中的pay方法
         return pay(name, pwd, money);
    }
}

註冊支付寶服務,須要在AndroidManifest.xml文件中配置,而且加上意圖過濾器:

<service android:name="com.itheima.alipay.ALiPayService">
    <intent-filter >
         <action android:name="com.itheima.alipay"/>
    </intent-filter>
</service>

10.2. 其餘應用調用支付寶服務

接下來模擬「歡樂鬥地主」應用調用支付寶支付,在歡樂鬥地主項目工程目錄下建立與遠程服務同名的包,將遠程服務的aidl文件拷貝到新建的包下:

這裏寫圖片描述

綁定到遠程服務,點擊按鈕調用遠程服務的支付方法支付:

public class MainActivity extends Activity {
    private MyConn myConn;
    private Iservice iservice
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent intent = new Intent();
        intent.setAction("com.itheima.alipay");
        myConn = new MyConn();
        bindService(intent, myConn, BIND_AUTO_CREATE);
    }
    public void click(View v){  
        try {
            boolean result = iservice.callPay("abc", "123", 501);       
            if (result) {
                Toast.makeText(getApplicationContext(), "夠買歡樂豆成功", 0).show();
            }else {
                Toast.makeText(getApplicationContext(), "夠買失敗", 0).show();
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }   
    private class MyConn implements ServiceConnection{
        @Override
        public void onServiceConnected(ComponentName name, IBinder service)
        {       
             iservice = Iservice.Stub.asInterface(service); 
        @Override
        public void onServiceDisconnected(ComponentName name) {     
        }   
    }
}

咱們調用支付功能,支付501,運行結果: 
這裏寫圖片描述

咱們將支付金額改爲5001,從新部署項目,再次調用支付功能,運行結果: 
這裏寫圖片描述

相關文章
相關標籤/搜索