2011-12-15

在 Android 裡使用 Local Service

Android Service

Android Service 是與 Activity 同位階的元件,不同於 Activity 的地方在於 Service 是在背景執行,沒有 UI 可以操作,所以不像 Activity 會有 onPause(...) 或 onResume(...) 的情況,Service 只有 onCreate(...)、onDestroy(...) 與負責工作的 onStartCommand(...),因此當 Activity 被關掉時,可以將未完成的工作交給 Service 執行。

Android Service 的生命週期獨立於 Activity,就是說,關閉 Activity 不會一併關閉 Service。

Android Service 可以分成 Local 與 Remote 兩種,Local Service 只能由同一 App 存取,而 Remote Service 則可以由同一 Android 裡的所有 App 存取。

非常重要的一點是,Android Service 預設是在 Main thread 裡執行,所以 Service 不可以執行費時的工作,否則 UI 會停住,因此 Service 若要執行費時的工作,得另起 thread 去處理;跑在 Main Thread 的 Service 因此可以透過 Activity 來修改 UI,而另起 Thread 後就不行了。

Local Service 適合用在寄送 Email 這類費時且不可因 Activity 關閉而停止的工作,而 Remote Service 則適合負責多個 App 都需要的功能,也就是 code  reuse 的概念。

Local Service 與 Remote Service 都是透過實做 Service,差別在於 Remote Service 得實做 onBind(...),而 Local Service 不用,因為 Local Service 是透過 Context.startService(...) 手動啟動,而 Remote Service 則是由系統呼叫 onBind(...)  來自動啟動,但是千萬不可以將一個 Service 同時實做 Local Service 與 Remote Service,生或週期不同會錯亂。

Local Service

Local Service 透過手動呼叫 Context.startService(...) 啟動,然後呼叫 Context.stopService(...) 或者 Service 本身呼叫 stopSelf() 來停止。

Local Service 只會有一個 instance,所以第一次啟動時,會產生 instance 並呼叫 onStartCommand(...),第二次以後呼叫,就直接呼叫 onStartCommand(...),不會再產生新的 instance。

main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <EditText
        android:id="@+id/et"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:onClick="startLocalService"
        android:text="開始" />

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:onClick="stopLocalService"
        android:text="停止" />

</LinearLayout>
LocalService
public class LocalService extends Service {

    private static final String TAG = "LocalService";
    public static final String KEY = "key";
    private ThreadGroup threads;
    private NotificationManager nMgr;
    private int threadId = 1;

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate...只會執行一次");
        // 方便在 onDestroy 一次殺掉所有 Thread
        this.threads = new ThreadGroup("LocalService");
        // Service 沒有 UI 可以互動,只能透過 Notification
        this.nMgr = (NotificationManager) this.getSystemService(NOTIFICATION_SERVICE);
        this.notify(0, "Local Service 執行中...");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        super.onStartCommand(intent, flags, startId);
        String key = intent.getExtras().getString(LocalService.KEY);
        this.notify(this.threadId, "Local Service " + key + "開始");
        Log.d(TAG, "onStartCommand...可以多次呼叫 - " + key);
        Log.d(TAG, "啟動 Thread " + key);
        new Thread(this.threads, new CounterThread(this, this.threadId, key),
                "LocalService").start();
        this.threadId++;
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy...");
        Log.d(TAG, "停止所有 Local Service 啟動的 Thread");
        this.threads.interrupt();
        Log.d(TAG, "清空所有通知訊息");
        this.nMgr.cancelAll();
        Log.d(TAG, "onDestroyed");
    }

    public void notify(int id, String msg) {
        Notification n = new Notification(R.drawable.ic_launcher, msg,
                System.currentTimeMillis());
        // 設為不可清除,以防被清掉就回不來了
        n.flags = Notification.FLAG_NO_CLEAR;
        PendingIntent pi = PendingIntent.getActivity(this, 1, new Intent(this,
                LocalServiceActivity.class), 0);
        n.setLatestEventInfo(this, "Local Service 通知", msg, pi);
        // 使用同一 id 來覆蓋(更新)前一訊息,所以永遠只會有一個通知訊息
        this.nMgr.notify(id, n);
    }

    @Override
    public IBinder onBind(Intent intent) {
        Log.d(TAG, "Remote Service 專用,Local Service 不用");
        return null;
    }
}
CounterThread
public class CounterThread implements Runnable {

    private static final String TAG = "CounterThread";
    private LocalService service;
    private int threadId;
    private String key;
    private int cnt;

    public CounterThread(LocalService service, int threadId, String key) {
        super();
        this.service = service;
        this.threadId = threadId;
        this.key = key;
        Log.d(TAG, "建立 Thread - " + this.key);
    }

    @Override
    public void run() {
        while (this.cnt < 5) {
            this.cnt++;
            Log.d(TAG, "Thread " + key + " - " + this.cnt);
            this.service.notify(this.threadId, "Thread " + key + " - "
                    + this.cnt);
            try {
                Thread.sleep(3 * 1000);
            }
            catch (InterruptedException e) {
                Log.e(TAG, "Thread " + key + " 被中斷了!");
                this.service.notify(this.threadId, "Thread " + key + " 被中斷了!");
                return;
            }
        }
        Log.d(TAG, "Thread " + key + " 完成了");
        this.service.notify(this.threadId, "Thread " + key + " 完成了");
        return;
    }
}
LocalServiceActivity
public class LocalServiceActivity extends Activity {

    private static final String TAG = "LocalServiceActivity";
    private EditText et;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        this.et = (EditText) this.findViewById(R.id.et);
    }

    public void startLocalService(View v) {
        String key = this.et.getText().toString();
        Log.d(TAG, "開始 " + key + " ...");
        Intent it = new Intent(this, LocalService.class);
        it.putExtra(LocalService.KEY, key);
        this.startService(it);
    }

    public void stopLocalService(View v) {
        Log.d(TAG, "全部收工");
        boolean ok = this.stopService(new Intent(this, LocalService.class));
        if (ok) {
            Log.d(TAG, "順利收工");
        }
        else {
            Log.e(TAG, "收工不順");
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // 很重要,一定要有,不然 Thread 會跑到暴肝
        this.stopLocalService(null);
    }
}
AndroidManifest.xml
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:label="@string/app_name"
            android:name=".LocalServiceActivity" android:launchMode="singleTop">
            <intent-filter >
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service android:name="LocalService"></service>
    </application>

沒有留言:

張貼留言