第十六章 Service與執行緒

初學Android都會有個疑問, Service和Thread到底有什麼差異? 用Thread就好了,為什麼要用Service. 其實Service和Thread沒有任何關係!
會把他們聯想起來, 主要是因為Service的後台觀念, 它可以在背景執行一些任務. 而眾所皆知的, Thread是開啟一個執行緒, 然後在這執行緒去執行一些耗時的操作, 這樣就不會阻塞UI主執行緒的運行.

請切記一件事, Service其實是運行在UI主執行緒, 所以Service若需操作耗時的工作, 一樣會產生ANR. 所以若有耗時的工作, 還是要開啟新的執行緒來處理.
Android有個規定, 當UI主執行緒超過5秒沒有反應的話, 就會彈出ANR的錯誤訊息, 要求使用者等待或離開. 而有的不只5秒, 這要看手機製造商是否有調校Framework而定

一般會說明Service可以提供在背景運作的機制, 比如由網路下載所需的資料.  好, 問題來了
請參照如下的程式碼, 開啟一個執行緒, 然後關閉應用程式. 然而這個執行緒還是照樣在背景中執行啊, 一直在吐Log. 所以為什麼一定要用Service?

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread(new Runnable(){
            @Override
            public void run() {
                int count=0;
                while(true){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    Log.d("Thomas", ""+count++);
                }
            }
        }).start();
        finish();
    }
}

使用Service的理由

1. 將元件的生命週期跟Thread的生命週期分開(避免前述Thread參考到元件, 在Thread結束前無法釋放物件導致Memory leak)
2. 當一個Process內只剩下Thread在執行, 避免Process被系統意外回收, 導致Thread被提前結束
3. 跨行程演出

Thread的運行是獨立於Activity, 上述的程式碼, 當Activity 結束, 若沒有主動停止Thread, Thread還是會一直執行. 但因為此時的Activity己停止, 所以無法對此Thread進行控制, 也無使用不同的Activity對同一Thread進行控制.

Android系統在資源吃緊的情況下, 會刪除Service及Thread, 待資源較寬裕時會自動重啟Service, 但不會重啟Thread

啟動Service有二種方式, 分別為startService及binService, 這二種有不同的生命周期

startService

底下範例, 使用二個按鈕來啟動及停止Service. 而Service的任務就是每隔一秒就吐出一個Log.

Service元件需繼承Service類別, 並一定要覆寫onBind(), 因為它在父類別中是抽像方法. startService因為不綁定Binder, 所以直接傳回null即可.

startService的生命周期如下

android_service_liftcycle

startService的主要工作是寫在onStartCommand()方法裏. 這個方法在Android 2.0前原為onStart(). 在onStartCommand()裏的任務若耗時較多, 都會在此啟動一個新的執行緒來執行. 此方法的返回值為int, 共有三個, 宣告在Service類別變數中
Service.START_STICKY : 一般用途的Service都是傳回這個. 如果Service被系統停止後, 會儘快重新啟動. 系統重啟此Service後, 其Intent為null
Service.START_NOT_STICKY : 系統中止此Service, 不會自動重啟
Service.START_REDELIVER_INTENT : 會自動重啟, 並傳送Intent

onStartCommand()方法的第二個參數為系統重啟此Service的狀況
START_FLAG_REDELIVER : 系統重啟, 並重送Intent
START_FLAG_RETRY : 系統重啟, Intent為null

以下範例, 請在專案下按右鍵/New/Service/Service, 再輸入CountService即可

CountService.java

public class CountService extends Service {
    static boolean runFlag;
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        this.flag=intent.getBooleanExtra("flag", true);
        new Thread(new Runnable() {
            @Override
            public void run() {
                int count=0;
                while(runFlag){
                    try {
                        Thread.sleep(1000);
                        Log.d("Thomas", (count++)+"秒");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
        return super.onStartCommand(intent, flags, startId);
    }
    @Override
    public void onDestroy() {
        runFlag=false;
        super.onDestroy();
    }
    public static void close(){
        runFlag=false;
    }
}

請注意上面紅色及藍色的地方. 正常來說, 在MainActivity下達stopService()時, Service會先執行onDestroy方法再結束. 但實際上卻不會, 所以最保險的方法, 就是在Service下新增close方法. 然後在MainActivity下達stopService前, 先手動執行Service的close方法.

MainActivity.java

跟啟動Activity一樣, 先產生Intent物件, 再由startService(intent)啟動Service元件,  stopService(intent)結束Service元件.

public class MainActivity extends AppCompatActivity {
    Intent intent;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        intent=new Intent(this, CountService.class);
        intent.putExtra("flag", true);
    }
    public void btnStart_click(View view){
        startService(intent);
    }
    public void btnClose_click(View view){
        stopService(intent);
    }
}

AndroidManifest.xml設定檔

與Activity一樣, 需要在設定檔中設定, 如下

<service
    android:name=".CountService" >
    <intent-filter>
        <action android:name="com.asuscomm.mahaljsp.action.CountService" />
    </intent-filter>
</service>

bindService

bindService, 就是把自己傳回給呼叫它的Activity. 要傳回Service自己,  就要把自己包在IBinder類別裏. 操作步驟如下

1. 在Service類中宣告一個繼承 Binder的內部類別. 且在該內部類別中提供public getService()方法來返回service的實例.
2. 在Service類別中需要new出這個內部類別, 並由onBind()方法中傳回
3. 記得要在AndroidManifest.xml設定檔設定<service>
4. 在client端的onServiceConnected()方法中得到從onBind()方法傳回的IBinder物件, 然後透過該物件的getService()取得Service實例.
5. 在Service中提供其他public方法, 這樣就可以在其他元件如Activity中調用這些方法
6. 最後在Activity的onDestroy()方法中, 要unbindService(conn), 不然在退出程式後會發生Runtime Exception

bindService跟startService不一樣, 它不會自動執行onStartCommand(). 所以要把任務寫在自訂的public方法, 再由Activity來啟動執行

bindService的生命周期如下

android_service_liftcycle2

CountService.java

onBind()方法中要傳回的是IBinder物件, 此物件為內部類別, 繼承了Binder類別, 只提供一個傳回自身Service類別的方法而以.

而其他public方法, 如startCount(), 是給外部元件使用的公用方法

public class CountService extends Service {
    boolean flag=true;
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new CountBinder();
    }
    public class CountBinder extends Binder{
        public CountService getService(){
            return CountService.this;
        }
    }
    public void startCount(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                int count=0;
                while(flag){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    Log.d("Thomas", (count++)+"秒");
                }
            }
        }).start();
    }
    public void stopCount(){
        flag=false;
    }
}

MainActivity.java

在MainActivity中, 使用bindService()啟動Service, 此方法有三個參數
intent : 這就不用說了
ServiceConnection : 此物件設定跟Service連線及斷線所需處理的事項
flag: 連線狀況

另需特別注意, 在Activity的onDestroy中, 需unbindService(conn), 不然退出程式後會發生錯誤

public class MainActivity extends AppCompatActivity {
    Button btnStart, btnStop;
    CountService service;
    Intent intent;
    ServiceConnection conn;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        processView();
        processControllers();
    }
    public void processView(){
        btnStart=(Button)findViewById(R.id.btnStart);
        btnStop=(Button)findViewById(R.id.btnStop);
        conn=new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
                service = ((CountService.CountBinder)iBinder).getService();
            }
            @Override
            public void onServiceDisconnected(ComponentName componentName) {
                service=null;
            }
        };
        intent=new Intent(this, CountService.class);
        bindService(intent, conn, Context.BIND_AUTO_CREATE);
    }
    public void processControllers(){
        btnStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                service.startCount();
            }
        });
        btnStop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                service.stopCount();
            }
        });
    }
    @Override
    protected void onDestroy() {
        unbindService(conn);
        super.onDestroy();
    }
}

完整程式碼下載

IntentService

一般繼承Service的元件, 都是在UI主執行緒中執行, 所以Android加了一個IntentService的類別, 會自己創建新的執行緒來處理後續的工作. 建立IntentService, 需手動加入一個預設建構子, 並調用super(String). 然後覆寫 protected void onHandleIntent(Intent), 將要執行的務任寫在這裏面, 這裏面的任務就會自動由新的執行緒來處理的, 所以不可以在這裏手動產生新執行緒

不要忘記了, AndroidManifest.xml設定檔還是要設定<service>標簽

CountService.java

public class CountService extends IntentService {
    boolean flag=true;
    public CountService() {
        super("CountService");
    }
    @Override
    protected void onHandleIntent(Intent intent) {
        int count=0;
        while(flag){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Log.d("Thomas", (count++)+"秒");
        }
    }
    @Override
    public void onDestroy() {
        flag=false;
        super.onDestroy();
    }
}

致於MainActivity, 則跟一般Service一模一樣, 由startService來啟動, stopService來停止

MainActivity.java

public void processControllers(){
    btnStart.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            startService(intent);
        }
    });
    btnStop.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            stopService(intent);
        }
    });

Handler

由MainActivity啟動Service後, 絕大部份都會在Service中新增執行緒處理後續的工作. 但這個新的執行緒是無法更新UI畫面的, 只能由UI主執行緒來更新(也就是MainActivity的執行緒). 所以如何在Service中改變UI畫面呢, 這時就要使用Handler.

Handler, 就如一個手柄, 放在MainActivity中, 並且宣告為static. 這樣這個Handler就可以被Service取用, 並使用sendMessage(Message)發送訊息, 然後由Handler接收. MainActivity的Handler一接收後, 就分別處理該作的事情.

底下使用一個時鐘的範例進行說明

android_handlerservice

activity_main.xml畫面配置檔如下

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    android:gravity="center"
    tools:context="com.asuscomm.mahaljsp.ch15_05_handlerservice.MainActivity">
    <TextView
        android:text="目前時間 : "
        android:textSize="20sp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/textView2"/>
    <TextView
        android:textSize="20sp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/txtTime"/>
</LinearLayout>

MainActivity.java

將handler宣告為類別變數, 並使用匿名類別實作, 覆寫handleMessage()方法. 這個方法會接收各種的Message, 所以就在此處設定不同Message所要處理的事情

public class MainActivity extends AppCompatActivity {
    TextView txtTime;
    Intent intent;
    static Handler handler;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        processViews();
        processControllers();
    }
    public void processViews(){
        txtTime=(TextView)findViewById(R.id.txtTime);
        intent=new Intent(this, CountService.class);
    }
    public void processControllers(){
        handler=new Handler(){
            @Override
            public void handleMessage(Message msg) {
                switch(msg.what){
                    case 0:
                        SimpleDateFormat sdf=new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
                        sdf.setTimeZone(TimeZone.getTimeZone("GMT+08:00"));
                        txtTime.setText(sdf.format(new Date()));
                        break;
                }
                super.handleMessage(msg);
            }
        };
        startService(intent);
    }
    @Override
    protected void onDestroy() {
        stopService(intent);
        super.onDestroy();
    }
}

CountService.java

在CountService中每隔一秒, 就利用MainActivity.handler.sendMessage()來傳送訊息

public class CountService extends Service {
    boolean flag=true;
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(flag) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    Message msg=new Message();
                    msg.what=0;
                    MainActivity.handler.sendMessage(msg);
                    //也可以使用下面較簡單的寫法
                    // MainActivity.handler.sendEmptyMessage(0);
                }
            }
        }).start();
        return super.onStartCommand(intent, flags, startId);
    }
    @Override
    public void onDestroy() {
        flag=false;
        super.onDestroy();
    }
}

最後記得要在AndroidManifest.xml註冊Service

完整程式碼下載

Service & MainActivity

Service啟動一定要透過Activity, 不然就成了駭客專用了, 不過可以使用如下方法, 立即關掉Activity

AndroidManifest設定Activity theme 為 Theme.Translucent.NoTitleBar, onCreate()不調用setContentView, 直接啟動Service, 然後使用finish()關掉Activity

Service啟動Activity

Service執行到某階段, 想要調用前端控制程式, 如Activity時, 使用一般intent即可

Intent intent=new Intent(MyService.this, MainActivity.class);
startActivity(intent);

Service架構

網路傳言, 在UI中使用stopService(intent),  就會執行Service裏的 onDestroy方法. 但其實不然, 文件中亦說明了並不保証會執行 onDestroy.

建議如下 :
如下程式碼中, 在UI中產生Intent並使用startScrvice啟動Service. 但結束時, 在UI中使用自訂的close()方法停止, 不要再使用stopService(intent).
然後於Service中的close方法, 使用stopSelf(); 停止Service

完整的架構如下

主程式

public class MainActivity extends AppCompatActivity {
    Intent intent;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        intent=new Intent(this, MyService.class);
        startService(intent);
    }
    @Override
    protected void onDestroy() {
        MyService.close();
        super.onDestroy();
    }
}

Service

public class MyService extends Service {
    public static boolean runFlag;
    public static Thread serviceThread;
    int index;
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
    @Override

    public int onStartCommand(Intent intent, int flags, int startId) {
        runFlag=true;
        serviceThread=new Thread(new Runnable() {
            @Override
            public void run() {
                while (runFlag) {
                    try {task();}
                    catch (Exception ex) {Log.d("Thomas", ex.getMessage());}
                    try {Thread.sleep(1000);} catch (InterruptedException e) {}
                }
                stopSelf();
            }
        });
        serviceThread.start();
        return super.onStartCommand(intent, flags, startId);
    }
    private void task() throws Exception{
        Log.d("Thomas", ""+index++);
    }
    public static void close(){
        runFlag=false;
        serviceThread.interrupt();
    }
}

頑強的Service

上面的架構是非常頑強的, 就算按了Power鍵進入休眠狀態, Service還是持續在跑, 這是新執行緒的特性. 但若資源不足, Service被砍了,  還是會盡可能的回復Service重新執行.

另外, 就算UI主執行緒停止, 也就是App中止執行了, Service 還是持續在背景偷偷運作, 所以要小心耗電的問題. 不過這也是一些抓姦程式或入侵程式最愛的方法

 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *