1. 程式人生 > 實用技巧 >Android Service完全解析,關於服務你所需知道的一切(下)

Android Service完全解析,關於服務你所需知道的一切(下)

在上篇文章中我們知道了,Service其實是執行在主執行緒裡的,如果直接在Service中處理一些耗時的邏輯,就會導致程式ANR。

讓我們來做個實驗驗證一下吧,修改上一篇文章中建立的ServiceTest專案,在MyService的onCreate()方法中讓執行緒睡眠60秒,如下所示:

public class MyService extends Service {
 
    ......
 
    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate() executed
"); try { Thread.sleep(60000); } catch
(InterruptedException e) { e.printStackTrace(); } } ...... }

重新執行後,點選一下Start Service按鈕或Bind Service按鈕,程式就會阻塞住並無法進行任何其它操作,過一段時間後就會彈出ANR的提示框,如下圖所示。

之前我們提到過,應該在Service中開啟執行緒去執行耗時任務,這樣就可以有效地避免ANR的出現。

那麼本篇文章的主題是介紹遠端Service的用法,如果將MyService轉換成一個遠端Service,還會不會有ANR的情況呢?讓我們來動手嘗試一下吧。

將一個普通的Service轉換成遠端Service其實非常簡單,只需要在註冊Service的時候將它的android:process屬性指定成:remote就可以了,程式碼如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.servicetest
" android:versionCode="1" android:versionName="1.0" > ...... <service android:name="com.example.servicetest.MyService" android:process=":remote" > </service> </manifest>

現在重新執行程式,並點選一下Start Service按鈕,你會看到控制檯立刻列印了onCreate() executed的資訊,而且主介面並沒有阻塞住,也不會出現ANR。大概過了一分鐘後,又會看到onStartCommand() executed列印了出來。

為什麼將MyService轉換成遠端Service後就不會導致程式ANR了呢?這是由於,使用了遠端Service後,MyService已經在另外一個程序當中運行了,所以只會阻塞該程序中的主執行緒,並不會影響到當前的應用程式。

為了證實一下MyService現在確實已經執行在另外一個程序當中了,我們分別在MainActivity的onCreate()方法和MyService的onCreate()方法里加入一行日誌,打印出各自所在的程序id,如下所示:

Log.d("TAG", "process id is " + Process.myPid());

再次重新執行程式,然後點選一下Start Service按鈕,列印結果如下圖所示:

可以看到,不僅僅是程序id不同了,就連應用程式包名也不一樣了,MyService中列印的那條日誌,包名後面還跟上了:remote標識。

那既然遠端Service這麼好用,乾脆以後我們把所有的Service都轉換成遠端Service吧,還省得再開啟執行緒了。其實不然,遠端Service非但不好用,甚至可以稱得上是較為難用。一般情況下如果可以不使用遠端Service,就儘量不要使用它。

下面就來看一下它的弊端吧,首先將MyService的onCreate()方法中讓執行緒睡眠的程式碼去除掉,然後重新執行程式,並點選一下Bind Service按鈕,你會發現程式崩潰了!為什麼點選Start Service按鈕程式就不會崩潰,而點選Bind Service按鈕就會崩潰呢?這是由於在Bind Service按鈕的點選事件裡面我們會讓MainActivity和MyService建立關聯,但是目前MyService已經是一個遠端Service了,Activity和Service執行在兩個不同的程序當中,這時就不能再使用傳統的建立關聯的方式,程式也就崩潰了。

那麼如何才能讓Activity與一個遠端Service建立關聯呢?這就要使用AIDL來進行跨程序通訊了(IPC)。

AIDL(Android Interface Definition Language)是Android介面定義語言的意思,它可以用於讓某個Service與多個應用程式元件之間進行跨程序通訊,從而可以實現多個應用程式共享同一個Service的功能。

下面我們就來一步步地看一下AIDL的用法到底是怎樣的。首先需要新建一個AIDL檔案,在這個檔案中定義好Activity需要與Service進行通訊的方法。新建MyAIDLService.aidl檔案,程式碼如下所示:

package com.example.servicetest;
interface MyAIDLService {
    int plus(int a, int b);
    String toUpperCase(String str);
}

點選儲存之後,gen目錄下就會生成一個對應的Java檔案,如下圖所示:

然後修改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的實現。

接下來修改MainActivity中的程式碼,如下所示:

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();
            }
        }
    };
 
    ......
 
}

我們只是修改了ServiceConnection中的程式碼。可以看到,這裡首先使用了MyAIDLService.Stub.asInterface()方法將傳入的IBinder物件傳換成了MyAIDLService物件,接下來就可以呼叫在MyAIDLService.aidl檔案中定義的所有介面了。這裡我們先是呼叫了plus()方法,並傳入了3和5作為引數,然後又呼叫了toUpperCase()方法,並傳入hello world字串作為引數,最後將呼叫方法的返回結果打印出來。

現在重新執行程式,並點選一下Bind Service按鈕,可以看到列印日誌如下所示:

由此可見,我們確實已經成功實現跨程序通訊了,在一個程序中訪問到了另外一個程序中的方法。

不過你也可以看出,目前的跨程序通訊其實並沒有什麼實質上的作用,因為這只是在一個Activity裡呼叫了同一個應用程式的Service裡的方法。而跨程序通訊的真正意義是為了讓一個應用程式去訪問另一個應用程式中的Service,以實現共享Service的功能。那麼下面我們自然要學習一下,如何才能在其它的應用程式中呼叫到MyService裡的方法。

在上一篇文章中我們已經知道,如果想要讓Activity與Service之間建立關聯,需要呼叫bindService()方法,並將Intent作為引數傳遞進去,在Intent裡指定好要繫結的Service,示例程式碼如下:

Intent bindIntent = new Intent(this, MyService.class);
bindService(bindIntent, connection, BIND_AUTO_CREATE);

這裡在構建Intent的時候是使用MyService.class來指定要繫結哪一個Service的,但是在另一個應用程式中去繫結Service的時候並沒有MyService這個類,這時就必須使用到隱式Intent了。現在修改AndroidManifest.xml中的程式碼,給MyService加上一個action,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.servicetest"
    android:versionCode="1"
    android:versionName="1.0" >
 
    ......
 
    <service
        android:name="com.example.servicetest.MyService"
        android:process=":remote" >
        <intent-filter>
            <action android:name="com.example.servicetest.MyAIDLService"/>
        </intent-filter>
    </service>
 
</manifest>

這就說明,MyService可以響應帶有com.example.servicetest.MyAIDLService這個action的Intent。

現在重新執行一下程式,這樣就把遠端Service端的工作全部完成了。

然後建立一個新的Android專案,起名為ClientTest,我們就嘗試在這個程式中遠端呼叫MyService中的方法。

ClientTest中的Activity如果想要和MyService建立關聯其實也不難,首先需要將MyAIDLService.aidl檔案從ServiceTest專案中拷貝過來,注意要將原有的包路徑一起拷貝過來,完成後專案的結構如下圖所示:

然後開啟或新建activity_main.xml,在佈局檔案中也加入一個Bind Service按鈕:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
     >
 
   <Button 
       android:id="@+id/bind_service"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:text="Bind Service"
       />
 
</LinearLayout>

接下來開啟或新建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。

在當前Activity和MyService建立關聯之後,我們仍然是呼叫了plus()和toUpperCase()這兩個方法,遠端的MyService會對傳入的引數進行處理並返回結果,然後將結果打印出來。

這樣的話,ClientTest中的程式碼也就全部完成了,現在執行一下這個專案,然後點選Bind Service按鈕,此時就會去和遠端的MyService建立關聯,觀察LogCat中的列印資訊如下所示:

不用我說,大家都已經看出,我們的跨程序通訊功能已經完美實現了。

不過還有一點需要說明的是,由於這是在不同的程序之間傳遞資料,Android對這類資料的格式支援是非常有限的,基本上只能傳遞Java的基本資料型別、字串、List或Map等。那麼如果我想傳遞一個自定義的類該怎麼辦呢?這就必須要讓這個類去實現Parcelable介面,並且要給這個類也定義一個同名的AIDL檔案。這部分內容並不複雜,而且和Service關係不大,所以就不再詳細進行講解了,感興趣的朋友可以自己去查閱一下相關的資料。

好了,結合上下兩篇,這就是關於Service你所需知道的一切。

希望本文對你有所幫助~~如果對軟體測試、介面測試、自動化測試、面試經驗交流感興趣可以加入我們。642830685,免費領取最新軟體測試大廠面試資料和Python自動化、介面、框架搭建學習資料!技術大牛解惑答疑,同行一起交流。