1. 程式人生 > >Android之Service詳解

Android之Service詳解

1. 簡介

與前一篇Android之Activity的細枝末節是同一系列的文章,是自己在學習和研發過程中,對Service的一些知識點的總結,彙總得到這篇文章。

這篇文章會從Service的一些小知識點,延伸到Android中幾種常用程序間通訊方法。

2. 程序

       Service是一種不提供使用者互動頁面但是可以在後臺長時間執行的元件,可以通過在AndroidManifest.xml設定Service的android:process=":remote"屬性,讓Service執行另一個程序中,也就是說,雖然你是在當前應用啟動的這個Service,但是這個Service和這個應用並不是同一個程序。

四大元件都支援android:process=":remote"這個屬性。

因為Service可以執行在不同的程序,這裡說一下Android中幾種程序的優先順序,當系統記憶體不足時候,系統會從優先順序低的程序開始回收,下面根據優先順序由高到低列出Android中幾種程序。

  • 前臺程序,當前使用者操作所需要的程序

    • 使用者正在互動的Activity(Activity執行了onResume方法)
    • 與正在互動的Activity繫結的Service
    • 設定為前臺許可權的Service(Service呼叫startForeground()方法)
    • 正在執行某些生命週期回撥的Service,onCreate()、onStart()、onDestroy()
    • 正在執行onReceive()的BroadcastReceiver

    這種程序基本不會被回收,只有當記憶體不足以支援前臺程序同時執行時候,系統才回回收它們,主要關注前三個。

  • 可見程序,沒有與使用者互動所必須的元件,但是在螢幕上仍然可見其內容的程序

    • 呼叫了onPause()方法但仍對使用者可見的Activity
    • 與上面這種Activity繫結的Service
  • 服務程序,使用startService()啟動的Service且不屬於上面兩種類別程序的程序,雖然這個程序與使用者互動沒有直接關係,但是一般會在後臺執行一些耗時操作,所以,只有當記憶體不足以維持所有前臺程序和可見程序同時執行,系統才回回收這個類別的程序。

  • 後臺程序,對使用者不可見的Activity程序,已呼叫了onStop()方法的Activity

  • 空程序,不包含任何活動應用元件的程序,保留這種程序唯一目的是作為快取,縮短引用元件下次啟動時間。通常系統會最優先回收這類程序。

此外,一個程序的級別可能會因為其他程序對它的依賴而有所提高,即程序A服務於程序B(B依賴A),那麼A的程序級別至少是和B一樣高的。

3. Service配置

和其他元件(Activity/ContentProvider/BroadcastReceiver)一樣,Service需要在Androidmanifest.xml中宣告。

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

Service是執行在主執行緒中的,如果有什麼耗時的操作,建議新建子執行緒去處理,避免阻塞主執行緒,降低ANR的風險。

       在另外一篇文章中Intent以及IntentFilter詳解提到過,為了確保應用的安全,不要為Service設定intent-filter,因為如果使用隱式Intent去啟動Service時候,手機裡面那麼多應用,並不能確定哪一個Service響應了這個Intent,所以在專案中儘量使用顯式Intent去啟動Service。在Android 5.0(API LEVEL 21)版本後的,如果傳入隱式Intent去呼叫bindService()方法,系統會丟擲異常。

可以通過設定android:exported=false來確保這個Service僅能在本應用中使用。

4. 服務啟動方式

服務可以由其他元件啟動,而且如果使用者切換到其他應用,這個服務可能會繼續在後臺執行。到目前為止,Android中Service總共有三種啟動方式。

  • Scheduled,可定時執行的Service,是Android 5.0(API LEVEL 21)版本中新新增的一個Service,名為JobService,繼承Service類,使用JobScheduler類排程它並且設定JobService執行的一些配置。具體文件可以參考JobScheduler,如果你的應用最低支援版本是21,官方建議使用JobService。
  • Started,通過startService()啟動的Service。通過這種方式啟動的Service會獨立的執行在後臺,即使啟動它的元件已經銷燬了。例如Activity A使用startService()啟動了Service B,過了會兒,Activity A執行onDestroy()被銷燬了,如果Service B任務沒有執行完畢,它仍然會在後臺執行。這種啟動方式啟動的Service需要主動呼叫StopService()停止服務。
  • Bound,通過bindService()啟動的Service。通過這種方式啟動Service時候,會返回一個客戶端互動介面,使用者可以通過這個介面與服務進行互動,如果這個服務是在另一個程序中,那麼就實現了程序間通訊,也就是Messenger和AIDL,這個會是下篇文章的重點。多個元件可以同時繫結同一個Service,如果所有的元件都呼叫unbindService()解綁後,Service會被銷燬。

startService和bindService可以同時使用

5. 主要方法

Service是一個抽象類,使用需要我們去實現它的抽象方法onBind(),Service有且僅有這一個抽象方法,還有一些其他的生命週期回撥方法需要複寫幫助我們實現具體的功能。

  • onCreate(),在建立服務時候,可以在這個方法中執行一些的初始化操作,它在onStartCommand()onBind()之前被呼叫。如果服務已經存在,呼叫startService()啟動服務時候這個方法不會呼叫,只會呼叫onStartCommand()方法。
  • onStartCommand(),其他元件通過startService()啟動服務時候會回撥這個方法,這個方法執行後,服務會啟動被在後臺執行,需要呼叫stopSelf()或者stopService()停止服務。
  • onBind(),其他元件通過bindService()繫結服務時候會回撥的方法,這是Service的一個抽象方法,如果客戶端需要與服務互動,需要在這個方法中返回一個IBinder實現類例項化物件,如果不想其他客戶端與服務繫結,直接返回null。
  • onDestroy(),當服務不在還是用且即將被銷燬時,會回撥這個方法,可以在這個方法中做一些釋放資源操作,這是服務生命週期的最後一個回撥。

如果元件僅通過startService()啟動服務,不論服務是否已經啟動,都會回撥onStartCommand()方法,而且服務會一直執行,需要呼叫stopSelfstopService方法關閉服務。

如果元件僅通過bindService()繫結服務,則服務只有在與元件繫結時候執行,一旦所有的客戶端全部取消繫結unbindService,系統才會銷燬該服務。

多次啟動同一個服務,只有在服務初次啟動時候會回撥onCreate方法,但是每次都會回撥onStartCommand,可以利用這個向服務傳遞一些資訊。

onStartCommand()的回撥是在UI主執行緒,如果有什麼耗時的操作,建議新啟執行緒去處理。

6. 啟動和關閉服務

啟動服務:

  • JobScheduler.schedule()
  • startService(Intent)
  • bindService(Intent service, ServiceConnection conn, int flags)

關閉服務:

  • JobScheduler.cancel()或者JobScheduler.cancelAll(),對應JobScheduler.schedule()
  • Service自身的stopSelf()方法,元件的stopService(Intent)方法,對應startService啟動方法
  • unbindService(ServiceConnection conn),對應bindService

示例:

// 啟動服務
Intent intent = new Intent(this, DemoService.class);
startService(intent);

// 停止服務
stopService(intent)

// 繫結服務
ServiceConnection mConnection = ServiceConnection() { ... };
Intent intent = new Intent(this, DemoService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);

// 解除繫結
unbindService(mConnection);

繫結服務bindService()第三個引數數值:

  • 0,如果不想設定任何值,就設定成0
  • Context.BIND_AUTO_CREATE,繫結服務時候,如果服務尚未建立,服務會自動建立,在API LEVEL 14以前的版本不支援這個標誌,使用Context.BIND_WAIVE_PRIORITY可以達到同樣效果
  • Context.BIND_DEBUG_UNBIND,通常用於Debug,在unbindService時候,會將服務資訊儲存並打印出來,這個標記很容易造成記憶體洩漏。
  • Context.BIND_NOT_FOREGROUND,不會將被繫結的服務提升到前臺優先順序,但是這個服務也至少會和客戶端在記憶體中優先順序是相同的。
  • Context.BIND_ABOVE_CLIENT,設定服務的程序優先順序高於客戶端的優先順序,只有當需要服務晚於客戶端被銷燬這種情況才這樣設定。
  • Context.BIND_ALLOW_OOM_MANAGEMENT,保持服務受預設的服務管理器管理,當記憶體不足時候,會銷燬服務
  • Context.BIND_WAIVE_PRIORITY,不會影響服務的程序優先順序,像通用的應用程序一樣將服務放在一個LRU表中
  • Context.BIND_IMPORTANT,標識服務對客戶端是非常重要的,會將服務提升至前臺程序優先順序,通常情況下,即時客戶端是前臺優先順序,服務最多也只能被提升至可見程序優先順序,
  • BIND_ADJUST_WITH_ACTIVITY,如果客戶端是Activity,服務優先順序的提高取決於Activity的程序優先順序,使用這個標識後,會無視其他標識。

7. onStartCommand()返回值

onStartCommand()方法有一個int的返回值,這個返回值標識服務關閉後系統的後續操作。

返回值有以下幾種:

  • Service.START_STICKY,啟動後的服務被殺死,系統會自動重建服務並呼叫on onStartCommand(),但是不會傳入最後一個Intent(Service可能多次執行onStartCommand),會傳入一個空的Intent,使用這個標記要注意對Intent的判空處理。這個標記適用於太依靠外界資料Intent,在特定的時間,有明確的啟動和關閉的服務,例如後臺執行的音樂播放。
  • Service.START_NOT_STICKY,啟動後的服務被殺死,系統不會自動重新建立服務。這個標記是最安全的,適用於依賴外界資料Intent的服務,需要完全執行的服務。
  • Service.START_REDELIVER_INTENT,啟動後的服務被殺死,系統會重新建立服務並呼叫onStartCommand(),同時會傳入最後一個Intent。這個標記適用於可恢復繼續執行的任務,比如說下載檔案。
  • Service.START_STICKY_COMPATIBILITY,啟動後的服務被殺死,不能保證系統一定會重新建立Service。

8. Service生命週期

Service生命週期(從建立到銷燬)跟它被啟動的方式有關係,這裡只介紹startServicebindService兩種啟動方法時候Service的生命週期。

  • startService啟動方式,其他元件用這種方式啟動服務,服務會在後臺一直執行,只有服務呼叫本身的stopSelf()方法或者其他元件呼叫stopService()才能停止服務。
  • bindService啟動方式,其他元件用這種方法繫結服務,服務通過IBinder與客戶端通訊,客戶端通過unbindService接觸對服務的繫結,當沒有客戶端繫結到服務,服務會被系統銷燬。

這兩種生命週期不是獨立的,元件可以同時用startService啟動服務同時用bindService繫結服務,例如跨頁面的音樂播放器,就可以在多個頁面同時繫結同一個服務,這種情況下需要呼叫stopService()或者服務本身的stopSelf()並且沒有客戶端繫結到服務,服務才會被銷燬。


圖-1Service生命週期圖

左圖是使用startService()所建立的服務的生命週期,右圖是使用bindService()所建立的服務的生命週期。

9. 在前臺執行服務

服務可以通過startForeground來使服務變成前臺優先順序。

public final void startForeground(int id, Notification notification) {
    try {
        mActivityManager.setServiceForeground(
                new ComponentName(this, mClassName), mToken, id,
                notification, true);
    } catch (RemoteException ex) {
    }
}

第一個引數用於標識你應用中唯一的通知標識id,不能設為0,最終會傳入NotificationManager.notify(int id, Notification notification)取消通知需要用到,第二個引數是通知具體內容。

前臺服務需要在狀態列中新增通知,例如,將音樂播放器的服務設定為前臺服務,狀態列通知顯示正在播放的歌曲,並允許其他元件與其互動。

// 設定Notification屬性
Notification notification = new Notification(R.drawable.icon, getText(R.string.ticker_text),System.currentTimeMillis());
Intent notificationIntent = new Intent(this, ExampleActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
notification.setLatestEventInfo(this, getText(R.string.notification_title), getText(R.string.notification_message), pendingIntent);
startForeground(ONGOING_NOTIFICATION_ID, notification);

要將服務從前臺移除,需要呼叫stopForeground(boolean removeNotification),引數是一個布林值,用來標識服務從前臺服務移除時候,是否需要移除狀態列的通知。如果服務在前臺執行時候被停止,狀態列的通知也會被移除。

10. 與服務通訊

10.1 廣播

不多說,萬能的通訊。

10.2 本地資料共享

不多說,萬能的通訊,例如ContentProvider/SharePreference等等。

10.3 startService()

       使用這個方法啟動的服務,再次呼叫startService()傳入Intent即可與服務通訊,因為這種方式啟動的服務在完整的生命週期內onCreate()只會執行一次,而onStartCommand()會執行多次,我們再次呼叫startService()時候,可以在oonStartCommand()去處理。

10.4 bindService()

使用這種方法啟動的服務,元件有三種與服務通訊的方式。

  • Service中實現IBinder
  • Messenger(AIDL的簡化版)
  • AIDL

下一篇文章具體介紹Messenger、AIDL,因為它們是屬於Android程序間通訊。

如果一個服務Service只需要在本應用的程序中使用,不提供給其他程序,推薦使用第一種方法。

使用示例:

Service:

/**
 * 本地服務
 * <br/>
 * 和啟動應用屬於同一程序
 */
public class LocalService extends Service {
    /**
     * 自定的IBinder
     */
    private final IBinder mBinder = new LocalBinder();

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

    /**
     * 提供給客戶端的方法
     *
     * @return
     */
    public String getServiceInfo() {
        return this.getPackageName() + " " + this.getClass().getSimpleName();
    }

    /**
     * 自定義的IBinder
     */
    public class LocalBinder extends Binder {
        public LocalService getService() {
            return LocalService.this;
        }
    }
}

Activity:

/**
 * 繫結本地服務的元件
 *
 * Created by KyoWang.
 */
public class BindLocalServiceActivity extends AppCompatActivity implements View.OnClickListener {

    private Button mShowServiceNameBtn;

    private LocalService mService;

    private boolean mBound = false;

    public ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            LocalService.LocalBinder binder = (LocalService.LocalBinder) service;
            mService = binder.getService();
            mBound = true;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mBound = false;
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.a_bind_local_service);
        mShowServiceNameBtn = (Button) findViewById(R.id.bind_local_service_btn);
        mShowServiceNameBtn.setOnClickListener(this);
    }

    @Override
    protected void onStart() {
        super.onStart();
        Intent intent = new Intent(this, LocalService.class);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onStop() {
        super.onStop();
        if(mBound) {
            unbindService(mConnection);
            mBound = false;
        }
    }

    @Override
    public void onClick(View v) {
        int id = v.getId();
        if(id == R.id.bind_local_service_btn) {
            if(mBound) {
                String info = mService.getServiceInfo();
                Toast.makeText(BindLocalServiceActivity.this, info, Toast.LENGTH_SHORT).show();
            }
        }
    }
}

11. 服務長存後臺

關於網上通用的提升服務優先順序以保證服務長存後臺,即保證服務不輕易被系統殺死的方法有以下幾種。

  • 設定android:persistent="true",這是application的一個屬性,官方都不建議使用。

    Whether or not the application should remain running at all times"true" if it should, and "false" if not. 
    The default value is "false". 
    Applications should not normally set this flag; 
    persistence mode is intended only for certain system applications.
  • 設定android:priority優先順序,這個並不是Service的屬性。這個屬性是在intent-filter中設定的。官方解釋,這個屬性只對活動和廣播有用,而且這個是接受Intent的優先順序,並不是在記憶體中的優先順序,呵呵。

    android:priority
    The priority that should be given to the parent component with regard to 
    handling intents of the type described by the filter. 
    This attribute has meaning for both activities and broadcast receivers。
  • 在Service的onDestroy中傳送廣播,然後重啟服務,就目前我知道的,會出現Service的onDestroy不呼叫的情況。

  • startForeground,這個上面提到過,是通過Notification提升優先順序。

  • 設定onStartCommand()返回值,讓服務被殺死後,系統重新建立服務,上面提到過。

五個裡面就兩個能稍微有點用,所以啊,網路謠傳害死人。

12. IntentService

敲黑板時間,重點來了,官方強力推薦。

前面提到兩點。

  • 因為Service中幾個方法的回撥都是在主執行緒中,如果使用Service執行特別耗時的操作,建議單獨新建執行緒去操作,避免阻塞主執行緒(UI執行緒)
  • 啟動服務和停止服務是成對出現的,需要手動停止服務

       IntentService完美的幫我們解決了這個問題,在內部幫我們新建的執行緒,不需要我們手動新建,執行完畢任務後會自動關閉。IntentService也是一個抽象類,裡面有一個onHandleIntent(Intent intent)抽象方法,這個方法是在非UI執行緒呼叫的,在這裡執行耗時的操作。

       IntentService使用非UI執行緒逐一處理所有的啟動需求,它在內部使用Handler,將所有的請求放入佇列中,依次處理,關於Handler可以看這篇文章,也就是說IntentService不能同時處理多個請求,如果不要求服務同時處理多個請求,可以考慮使用IntentService。

IntentService在內部使用HandlerThread配合Handler來處理耗時操作。

private final class ServiceHandler extends Handler {
    public ServiceHandler(Looper looper) {
        super(looper);
    }

    @Override
    public void handleMessage(Message msg) {
        onHandleIntent((Intent)msg.obj);
        stopSelf(msg.arg1);
    }
}

public int onStartCommand(Intent intent, int flags, int startId) {
    onStart(intent, startId);
    return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

public void onStart(Intent intent, int startId) {
    Message msg = mServiceHandler.obtainMessage();
    msg.arg1 = startId;
    msg.obj = intent;
    mServiceHandler.sendMessage(msg);
}

       注意msg.arg1它是請求的唯一標識,每傳送一個請求,會生成一個唯一標識,然後將請求放入Handler處理佇列中,從原始碼裡面可以看見,在執行完畢onHandleIntent方法後,會執行stopSelf來關閉本身,同時IntentService中onBind()方法預設返回null,這說明啟動IntetService的方式最好是用startService方法,這樣在服務執行完畢後才會自動停止;如果使用bindService來啟動服務,還是需要呼叫unbindService來解綁服務的,也需要複寫onBind()方法。

小盆宇:在ServiceHandler類的handleMessage方法中,執行onHandleIntent後緊接著執行stopSelf(int startId),把服務就給停止了,那第一個請求執行完畢服務就停止了,後續的請求怎麼會執行?

       注意stopSelf(int startID)方法作用是在其引數startId跟最後啟動該service時生成的id相等時才會執行停止服務,當有多個請求時候,如果發現當前請求的startId不是最後一個請求的id,那麼不會停止服務,所以只有當最後一個請求執行完畢後,才會停止服務。

13. 總結

在年前寫了出來,比Activity中的坑少了太多,希望對大家有幫助。下一篇是關於Android中程序通訊Messenger/AIDL的,是本篇的補充,但是也屬於單獨的知識點。