Android 組件 — Service 剖析

簡介
 Service 是Android的四大組件之一,當應用程序希望在應用程序空閒的時候去運行一個耗時較長的操作,或者爲其他應用提供功能實現的時候可以使用Service來完成。每個service都必須在AndroidManifest.xml中有一個相應的聲明,Services 可以通過Context.bindService() 或者Context.startService()來啓動Service。
 需要注意的是,Service與其他應用程序的對象(UI Activity...)一樣是運行在主進程的main Thread中,這就意味着,如果你的service打算做密集的CPU運算(比如播放媒體文件)或者做阻塞的操作(如聯網),都應該在Service中單獨開啓一個子線程來處理。而IntentService 繼承了Service,當你希望Service在一個單獨的線程中運行時,IntentService是一個標準的實現。

這篇文章涉及的主題:

  • 什麼是Service?
  • Service的生命週期
  • Service 權限的定義與聲明
  • 進程的聲明週期
  • Local Service 範例
  • Remote Messenger Service 範例

1.什麼是Service?

 實際上,大多數的困惑都是在說Service 不是什麼:
 1)Service不是一個單獨的進程,Service的對象實體並不一定運行在自己特有的進程中,它作爲應用程序的一部分與應用程序運行在同一個進程空間。
  如果希望Service運行在自己獨立的進程:
  a.可以在AndroidManifest.xml中通過android:process="" 爲Service 指定一個進程名稱,則Service會重新開啓一個進程。
  b.即便是不在AndroidManifest.xml指定android:process="",如果某個應用程序A期望使用其他應用程序B中的Service,那麼B的中Service與A 肯定不在同一個進程中。
 2)Service 不是一個線程,這就意味着它並不能脫離主線程(主要是爲了避免應用程序沒有反應的錯誤發生)
 
 所以Service實際上非常簡單,提供了兩個主要的特性:
 1)某個應用程序的功能告訴系統需要將一些事物放在後臺運行,只要調用了 Context.startService(),系統就會安排時間讓Service在後臺處理這些請求,除非你明確的去關閉Service,否則Service不會停止。
 2)如果說應用程序A希望使用應用程序B中的Service完成某件事,需要調用Context.bindService()來建立連接,bindService()允許一種長期的連接來保證B中的Service能正常與它交互。
 當Service這個組件已經創建,之後何時調用onCreate()和onBind()完全取決於Service自己,系統會讓Service在合適的時間點去調用這些方法
 需要注意的是,因爲Service本身的設計非常簡單,你可以按照具體的需求來完善Service:讓它成爲一個本地的Java對象然後直接調用它的方法(可以參照下文中的 Local Service 範例),也可以通過AIDL提供一個完全遠程的接口。
 
2.Service 的生命週期
 啓動Service的方式有兩種:
 1)Context.startService()
 context.startService()  -> onCreate()  -> onStartCommand(可能多次調用)  -> Service running  -> context.stopService()  -> onDestroy()  -> Service stop
 如果Service尚未運行,Client執行context.startService()時,則調用吮吸onCreate() -> onStartCommand()。
 如果Service已經運行,當Client多次執行context.startService(Intent service)時則只執行onStartCommand(),Client的數據可放在intent中傳遞到Service。
 無論Client執行多少次context.startService(),Client只需要執行一次context.stopService()就能關閉Service(調用onDestroy())。
 即使多次調用startService(Intent service),但是隻要你Intent中啓動的是同一個Service,則不會重新建立一個Service。

  •  onStartCommand() 方法是在2.0 以後才引入的,代替了已經不建議使用的onStart()。它提供了和onStart()一樣的功能,但是還允許你告訴系統,如果系統在顯示調用stopService或stopSelf之前終止了Service,該如何重啓Service。
	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		startBackgroundTask(intent,startId);
//		return Service.START_STICKY;
//		or
//		return Service.START_NOT_STICKY;
//		or
		return Service.START_REDELIVER_INTENT;
	}

 

 2)Context.bindService()
 context.bindService()  -> onCreate()  -> onBind()(只執行一次)  -> Service running  -> onUnbind()  -> onDestroy()  -> Service stop
 onBind()將返回給客戶端一個IBind接口實例,IBind 實際上可以簡單理解成某個定義在Service中的類A(這個接口至少需要extend IBind),它是通向Service的入口。當Client 拿到IBind之後可以將其向下轉型成類A,然後回調類A中的方法,類A中的方法就可以任意操作Service了。
 bindService()會讓Client與Service綁定在一起,當所有連接到Service的Client退出時,Service也會退出。
 Client接口IBind允許客戶端回調服務的方法,比如得到Service的實例、運行狀態或其他操作。這個時候把調用者(Context,例如Activity)會和Service綁定在一起,Context退出了,Srevice就會調用onUnbind->onDestroy相應退出。

3.Service 權限的定義與聲明
 當Service 在它的AndroidManifest.xml <service> 標籤下聲明瞭android:permission="",則其他app在訪問該Service的時候必須在各自的AndroidManifest.xml <uses-permission>中聲明訪問權限,才能去start, stop, or bind service。

 1)權限的定義

 <manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.me.app.myapp" >
    <permission
        android:name="com.me.app.myapp.permission.DEADLY_ACTIVITY"
        android:description="@string/permdesc_deadlyActivity"
        android:label="@string/permlab_deadlyActivity"
        android:permissionGroup="android.permission-group.COST_MONEY"
        android:protectionLevel="dangerous" />
     ... 
</manifest>

 2)權限的聲明

    <application>
    ...
        <service
            android:name="PrivService"
            android:permission="com.me.app.myapp.permission.DEADLY_SERVICE" />
    ...
    </application>

 另外,Service也可以通過上述方式來保護它的IPC方法調用,在調用service 的某個方法前可以調用contex.checkCallingPermission()來檢查是否擁有執行權限。

4.進程的生命週期
 只要一個進程的Service已經啓動或者有客戶端綁定到該Service,那麼Android 系統會儘可能保證該進程能正常運行。當系統的內存不足需要殺死一些現有的進程來釋放資源的時候,Service所在進程的優先級只有在下面幾種情況下才會更高一些:

  •  如果Service正在執行onCreate(), onStartCommand(), or onDestroy()這些方法,Service所在的進程的優先級會提高成前臺進程,以保證這些代碼能正常執行。
  •  如果Service 已經啓動,Service所在的進程的重要性只比用戶可見的進程低,但是比其他任何不可見的進程的優先級要高。因爲很少有進程對用戶來說是可見的(比如UI進程是可見的),所以只有內存極端缺乏的時候纔會終止Service所在的進程。
  •  一旦有客戶端綁定到Service,只要綁定到service中的其中一個客戶端變成對用戶可見的進程,那麼也可以認爲service也是可見的。
     一個已經啓動的service可以使用startForeground(int, Notification)方法將service切換到前臺線程狀態,這樣當系統內存不足時不會不會終止此service(但是理論上如果在內存極端缺乏的情況下還是會終止service進程)。
  •  需要注意的是,如果你的service長時間運行,當系統出現內存緊張的情況下系統可能終止service的運行。如果發生這樣的情況,系統之後會重新啓動service。如果你重寫了onStartCommand()方法,並在此方法中去新開線程或者異步的執行一些操作,爲了避免service在運行的過程中被終止後重啓丟失intent數據,你可以在onStartCommand()中return Service.START_FLAG_REDELIVERY,這樣在系統重啓Service的時候會重新傳遞intent數據(否則重新啓動時inten數據的值爲null)。

5.Local Service 範例:

1)界面

package sunmedia.chenjian.localservice;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.widget.Toast;

public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		doBindService();// 與service建立連接
	}

	private LocalService mBoundService;
	private ServiceConnection mConnection = new ServiceConnection() {
		public void onServiceConnected(ComponentName className, IBinder service) {

			mBoundService = ((LocalService.LocalBinder) service).getService();
			Toast.makeText(MainActivity.this, R.string.local_service_connected,
					Toast.LENGTH_SHORT).show();
		}

		public void onServiceDisconnected(ComponentName className) {
			mBoundService = null;
			Toast.makeText(MainActivity.this,
					R.string.local_service_disconnected, Toast.LENGTH_SHORT)
					.show();
		}
	};

	private boolean mIsBound;

	void doBindService() {
		// 由於事先知道service與client運行在同一進程,所以可以直接指定類名來啓動service
		bindService(new Intent(MainActivity.this, LocalService.class),
				mConnection, Context.BIND_AUTO_CREATE);
		mIsBound = true;
	}

	void doUnbindService() {
		if (mIsBound) {
			// 斷開連接
			unbindService(mConnection);
			mIsBound = false;
		}
	}

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

2)Service

package sunmedia.chenjian.localservice;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;

public class LocalService extends Service {

	public static final String NAME = "sunmedia.chen.jian.localservice";
	private NotificationManager mNM;
	// Unique Identification Number for the Notification.
	// We use it on Notification start, and to cancel it.
	private int NOTIFICATION = R.string.local_service_started;

	/**
	 * 爲client提供訪問,因爲事先知道service與client運行在同一進程,所以不需要 考慮IPC的影響
	 */
	public class LocalBinder extends Binder {
		LocalService getService() {
			return LocalService.this;
		}
	}

	@Override
	public void onCreate() {
		mNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
		// 顯示service的啓動狀態,啓動時會在狀態現顯示一個圖標
		showNotification();
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		Log.i("LocalService", "Received start id " + startId + ": " + intent);
		// 由於我們希望service一直在運行,除非調用stop終止進程,所以返回值可以設置成
		// START_STICKY
		return START_STICKY;
	}

	@Override
	public void onDestroy() {
		// 取消在狀態欄的提示
		mNM.cancel(NOTIFICATION);
		Toast.makeText(this, R.string.local_service_stopped, Toast.LENGTH_SHORT)
				.show();
	}

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

	// 用於與client交互,可以理解爲service的入口
	private final IBinder mBinder = new LocalBinder();

	/** * Show a notification while this service is running. */
	private void showNotification() {
		// 獲取用於狀態欄提示的字串
		CharSequence text = getText(R.string.local_service_started);
		// 設置狀態欄提示用的參數資源(圖片/文字/時間戳)
		Notification notification = new Notification(R.drawable.stat_sample,
				text, System.currentTimeMillis());
		// PendingIntent 預先設置狀態被點擊時的動作
		PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
				new Intent(this, Operation.class), 0);
		// 設置狀態欄中持續顯示時的參數
		notification.setLatestEventInfo(this,
				getText(R.string.local_service_label), text, contentIntent);
		// 發送Notification
		mNM.notify(NOTIFICATION, notification);
	}
}

3)operation

package sunmedia.chenjian.localservice;

import android.app.Activity;
import android.os.Bundle;
import android.widget.Toast;

//用於測試狀態欄被點擊時的動作
public class Operation extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.operation);
		Toast.makeText(this, "Operation Create", Toast.LENGTH_LONG).show();
	}

	@Override
	protected void onDestroy() {
		// TODO Auto-generated method stub
		super.onDestroy();
	}

}

4)AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="sunmedia.chenjian.localservice"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="17"
        android:targetSdkVersion="17" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="sunmedia.chenjian.localservice.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name="sunmedia.chenjian.localservice.Operation"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name="sunmedia.chenjian.localservice.LocalService"
            android:enabled="true"
            android:exported="false" >
            <intent-filter android:name="LocalService" >
            </intent-filter>
        </service>
    </application>

</manifest>

6.Remote Mesager Service範例

1)界面

package sunmedia.chenjian.remoteservice;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		mCallbackText = (TextView)findViewById(R.id.TextView);
		doBindService();
	}
	
	/** 用於與Service交互*/
	Messenger mService = null;
	/** 用於表示是否執行過bindService() */
	boolean mIsBound;
	/** 用於顯示當前連接的狀態 */
	TextView mCallbackText;

	/** 用於處理從Service 接收到的消息*/
	class IncomingHandler extends Handler {
		@Override
		public void handleMessage(Message msg) {
			switch (msg.what) {
			case MessengerService.MSG_SET_VALUE:
				mCallbackText.setText("Received from service: " + msg.arg1);
				break;
			default:
				super.handleMessage(msg);
			}
		}
	}

	/** 用於發佈消息到客戶端. */
	final Messenger mMessenger = new Messenger(new IncomingHandler());
	/** 用於指示 Service 連接狀態 */
	private ServiceConnection mConnection = new ServiceConnection() {
		/** 當Service建立完後後會調用此方法,並將Service作爲參數傳遞進來,需要注意的是
		 * ServiceConnection中的方法都是在Activity所在的主線程調用的,也就是說其實是客
		 * 戶端的主線程不斷的輪詢Service的狀態再調用ServiceConnection中的方法。
		 * 導致不能連接的情況有幾種:
		 * 1)Service 沒有在AndroidManifest.xml中聲明或者啓動Service時名稱不正確
		 * 2)如果當前Activity所在的主線程被卡主,則無法正常調用此方法
		 * */
		public void onServiceConnected(ComponentName className, IBinder service) {
			mService = new Messenger(service);
			mCallbackText.setText("Attached.");
			//將mMessenger傳遞到Service方便Service發消息到客戶端
			try {
				Message msg = Message.obtain(null,
						MessengerService.MSG_REGISTER_CLIENT);
				msg.replyTo = mMessenger;
				mService.send(msg);
				//傳遞一個標識到Service,方便其測試連接
				msg = Message.obtain(null, MessengerService.MSG_SET_VALUE,
						this.hashCode(), 0);
				mService.send(msg);
			} catch (RemoteException e) {
				//Service異常終止,可以選擇重新連接策略
			}

			Toast.makeText(MainActivity.this,
					R.string.remote_service_connected, Toast.LENGTH_SHORT)
					.show();
		}
		/**
		 * 當Service被異常終止的時,客戶端所在的主線程會調用此方法,之後系統會重新啓動
		 * Service並再次調用onServiceConnected()。
		 */
		public void onServiceDisconnected(ComponentName className) {
			mService = null;
			mCallbackText.setText("Disconnected.");
			
			Toast.makeText(MainActivity.this,
					R.string.remote_service_disconnected, Toast.LENGTH_SHORT)
					.show();
		}
	};

	void doBindService() {
		Intent intent = new Intent(MessengerService.NAME);
		bindService(intent,mConnection, Context.BIND_AUTO_CREATE);
		mIsBound = true;
		mCallbackText.setText("Binding.");
	}

	void doUnbindService() {
		if (mIsBound) {
			//如果客戶端正常退出,則需要註銷與Service的連接
			if (mService != null) {
				try {
					Message msg = Message.obtain(null,
							MessengerService.MSG_UNREGISTER_CLIENT);
					msg.replyTo = mMessenger;
					mService.send(msg);
				} catch (RemoteException e) {
					//Service已經異常終止
				}
			}
			unbindService(mConnection);
			mIsBound = false;
			mCallbackText.setText("Unbinding.");
		}
	}
}
2)Service

package sunmedia.chenjian.remoteservice;

import java.util.ArrayList;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.widget.Toast;

public class MessengerService extends Service {
	
	static final String NAME = "MyMessengerService";
	/**用於在狀態欄顯示通知. */
	NotificationManager mNM;
	/** 用於記錄客戶端的連接情況*/
	ArrayList<Messenger> mClients = new ArrayList<Messenger>();
	/** 用於存儲最後一次連接的客戶端標識符 */
	int mValue = 0;
	/**
	 * 此命令用於註冊客戶端,Message的 replyTo欄位需要放置客戶端的Messenger,用於
	 * Service發消息到客戶端
	 * */
	static final int MSG_REGISTER_CLIENT = 1;
	/**
	 * 此命令用於註銷客戶端,Message的 replyTo欄位需要放置客戶端的Messenger,用於
	 * 將Messenger從記錄中移除。
	 */
	static final int MSG_UNREGISTER_CLIENT = 2;
	/**
	 * 此命令主要用於測試Service與客戶端的連接
	 */
	static final int MSG_SET_VALUE = 3;

	/** * Handler of incoming messages from clients. */
	class IncomingHandler extends Handler {
		@Override
		public void handleMessage(Message msg) {
			switch (msg.what) {
			case MSG_REGISTER_CLIENT:
				mClients.add(msg.replyTo);
				break;
			case MSG_UNREGISTER_CLIENT:
				mClients.remove(msg.replyTo);
				break;
			case MSG_SET_VALUE:
				mValue = msg.arg1;
				for (int i = mClients.size() - 1; i >= 0; i--) {
					try {
						mClients.get(i).send(
								Message.obtain(null, MSG_SET_VALUE, mValue, 0));
					} catch (RemoteException e) {
						//如果發送消息給客戶端失敗,則認爲客戶端異常終止,將客戶端從
						//記錄中移除
						mClients.remove(i);
					}
				}
				break;
			default:
				super.handleMessage(msg);
			}
		}
	}

	/** 用於與客戶端之間的信息交互 */
	final Messenger mMessenger = new Messenger(new IncomingHandler());

	@Override
	public void onCreate() {
		mNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
		// Display a notification about us starting.
		showNotification();
	}

	@Override
	public void onDestroy() {
		// Cancel the persistent notification.
		mNM.cancel(R.string.remote_service_started);
		// Tell the user we stopped.
		Toast.makeText(this, R.string.remote_service_stopped,
				Toast.LENGTH_SHORT).show();
	}

	/**
	 * 將服務端的Messenger發送給客戶端,方便客戶端傳遞消息給Service
	 */
	@Override
	public IBinder onBind(Intent intent) {
		return mMessenger.getBinder();
	}

	/** * Show a notification while this service is running. */
	private void showNotification() {
		// In this sample, we'll use the same text for the ticker and the
		// expanded notification
		CharSequence text = getText(R.string.remote_service_started);

		// Set the icon, scrolling text and timestamp
		Notification notification = new Notification(R.drawable.stat_sample,
				text, System.currentTimeMillis());
		// The PendingIntent to launch our activity if the user selects this
		// notification
		PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
				new Intent(this, Operation.class), 0);
		// Set the info for the views that show in the notification panel.
		notification.setLatestEventInfo(this,
				getText(R.string.remote_service_label), text, contentIntent);
		// Send the notification.
		// We use a string id because it is a unique number. We use it later
		// to cancel.
		mNM.notify(R.string.remote_service_started, notification);
	}
}
3)AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="sunmedia.chenjian.remoteservice"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="17" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="sunmedia.chenjian.remoteservice.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name="sunmedia.chenjian.remoteservice.Operation"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name="sunmedia.chenjian.remoteservice.MessengerService"
            android:enabled="true"
            android:exported="true"
            android:process=":remote_service" >
            <intent-filter>
                <action android:name="MyMessengerService" />
            </intent-filter>
        </service>
    </application>

</manifest>


個人原創:轉載請註明出處:http://blog.csdn.net/chenjianjk/article/details/9499271

Local Service、Remote Service代碼下載地址(中文的編碼格式爲UTF-8):

http://download.csdn.net/detail/chenjianjk/5826495

http://download.csdn.net/detail/chenjianjk/5826501
發佈了28 篇原創文章 · 獲贊 0 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章