Android應用界面開發——Service與IntentService(實現定時更換壁紙)

Service是Android四大組件中與Activity最相似的組件,它們都代表可執行的程序,Service與Activity的區別是:Service一直在后臺運行,它沒有用戶界面,所以絕不會到前臺運行。如果某個程序組件需要在運行時向用戶呈現某種界面,或者該程序需要與用戶交互,就需要使用Activity;否則就應該考慮使用Service了,最后通過使用Service實現定時更換壁紙。

定時更換壁紙效果圖:

Service簡介


Service是一個可長期在后臺運行的應用組件,并且不提供用戶界面。

  • Service不是一個單獨的進程。
  • Service不是一個線程。

創建、配置Service


開發Service分為兩步:

  • 定義一個繼承Service的子類。
  • 在AndroidManifest.xml文件中配置該Service。

補充:需要在AndroidManifest.xml中聲明的有:

  • activity:活動
  • activity-alias:活動別名
  • service:服務
  • provider:內容提供者
  • receiver:廣播接收者
  • meta-data:數據格式
  • uses-library:第三方類庫

Service中定義了一系列生命周期方法:

  • IBinder onBind(Intent intent):該方法是Service子類必須實現的方法。該方法返回一個IBinder對象,應用程序可通過該對象與Service組件通信。
  • void onCreate():在Service第一次被創建后立即回調該方法。
  • void onDestroy():在Service被關閉之前回調該方法。
  • void onStartCommand(Intent intent, int flags, int startId):該方法的早期版本是onStart(Intent intent, int startId),每次客戶端調用startService(Intent)方法啟動Service時都會回調該方法。
  • boolean onUnbind(Intent intent):當該Service上綁定的所有客戶端都斷開連接時將會回調該方法。

定義一個Service組件如下:

public class FirstService extends Service {
    
    private static final String TAG = FirstService.class.getSimpleName();

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

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "onStartCommand");
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
    }
}

上面的Service只是重寫了Service組件的onCreate()、onStartCommand()、onDestroy()、onBind()等方法,重寫這些方法時只是打印了一個字符串。

在Android系統中運行Service有兩種方式:

  • 通過Context的startService()方法:通過該方法啟動Service,訪問者與Service之間沒有關聯,即使訪問者退出了,Service也仍然運行。
  • 通過Context的bindService()方法:通過該方法啟動Service,訪問者與Service綁定在一起,訪問者一旦退出,Service也就終止了。

啟動和停止Service——startService()方式啟動


使用Activity作為Service的訪問者,該Activity中包含兩個按鈕,一個用于啟動Service,一個用于關閉Service。代碼如下:

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private Button mStartService;
    private Button mStopService;
    private Intent intent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mStartService = (Button) findViewById(R.id.start_service);
        mStopService = (Button) findViewById(R.id.stop_service);
        intent = new Intent(MainActivity.this, FirstService.class);

        mStartService.setOnClickListener(this);
        mStopService.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.start_service:
                startService(intent);
                break;
            case R.id.stop_service:
                stopService(intent);
                break;
        }
    }
}

啟動、關閉Service十分簡單,調用Context的startService()、stopService()方法即可啟動、關閉Service。

注意:Android5.0開始,Google要求必須使用顯示的Intent啟動Service組件。

運行該程序,點擊啟動按鈕啟動Service,再點擊停止按鈕關閉Service,在Logcat面板可以看到如下輸出:

如果在不關閉Service的情況下,連續點擊三次啟動Service按鈕,程序會連續啟動三次Service,在Logcat面板可以看到如下輸出:

從上圖可以看出,每當Service被創建時會回調onCreate()方法,每次Service被啟動時都會回調onStartCommand()方法;多次啟動一個已有的Service不會再回調onCreate()方法,但每次啟動時都會回調onStartCommand()方法。

綁定本地Service并與之通信——bindService()方式啟動

如果Service和訪問者之間需要進行方法調用或者交換數據,則應該使用bindService()和unbindService()方法啟動、關閉Service。

Context的bindService()方法包含三個參數,分別如下:

  • service:該參數通過Intent指定要啟動的Service。
  • conn:該參數是一個ServiceConnection對象,該對象用于監聽訪問者與Service之間的連接情況。當訪問者與Service之間連接成功時回調該ServiceConnection對象的onServiceConnected(ComponentName name, IBinder service)方法;當Service所在的宿主進程由于異常中止或者其他原因終止,導致該Service與訪問者之間斷開連接時回調該ServiceConnection對象的onServiceDisconnected(ComponentName name)方法。
    • onServiceConnected(ComponentName name, IBinder service)方法中的IBinder即可實現與被綁定Service之間的通信。
  • flags:指定綁定時是否自動創建Service(如果Service還未創建)。該參數可指定為0(不自動創建)或者BIND_AUTO_CREATE(自動創建)。

實際開發時通常會采用繼承Binder(IBinder的實現類)的方式實現自己的IBinder對象。

下面程序示范了如何在Activity中綁定Service,并獲取Service的運行狀態。該程序的Service類需要真正實現onBind()方法,并讓該方法返回一個有效的IBinder對象。Service類代碼如下:

public class BindService extends Service {

    private static final String TAG = BindService.class.getSimpleName();

    private int count;
    private boolean quit;

    //定義onBinder方法所返回的對象
    private MyBinder binder = new MyBinder();
    //通過繼承Binder實現IBinder類
    public class MyBinder extends Binder{
        public int getCount(){
            return count;
        }
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Log.d(TAG, "onBind");
        return binder;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate");
        //啟動一條線程,動態修改count狀態值
        new Thread(){
            @Override
            public void run() {
                while (!quit){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count++;
                }
            }
        }.start();
    }

    @Override
    public boolean onUnbind(Intent intent) {
        Log.d(TAG, "onUnbind");
        return true;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        this.quit = true;
        Log.d(TAG, "onDestroy");
    }
}

Service類實現了onBind()方法,該方法返回一個可訪問該Service狀態數據(count)的IBinder對象,該對象將被傳給該Service的訪問者。

接下來定義一個Activity來綁定該Service,并在Activity中通過MyBinder對象訪問Service的內部狀態。該Activity包含三個按鈕,分別為綁定Service、解綁Service、獲取Service的運行狀態。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private static final String TAG = MainActivity.class.getSimpleName();
    private Button mBindService;
    private Button mUnbindService;
    private Button mGetServiceState;
    private Intent intent;
    //保持所啟動的Service的IBinder對象
    BindService.MyBinder binder;

    private ServiceConnection conn = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.d(TAG, "onServiceConnected");
            binder = (BindService.MyBinder) service;//①
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mBindService = (Button) findViewById(R.id.bind_service);
        mUnbindService = (Button) findViewById(R.id.unbind_service);
        mGetServiceState = (Button) findViewById(R.id.get_service_state);
        intent = new Intent(MainActivity.this, BindService.class);

        mBindService.setOnClickListener(this);
        mUnbindService.setOnClickListener(this);
        mGetServiceState.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.bind_service:
                bindService(intent, conn, BIND_AUTO_CREATE);
                break;
            case R.id.unbind_service:
                unbindService(conn);
                break;
            case R.id.get_service_state:
                Toast.makeText(MainActivity.this, "Service的count值為:" + binder.getCount(), Toast.LENGTH_SHORT).show();//②
                break;
        }
    }
}

上面①號代碼用于在Activity與Service連接成功時獲取Service的onBind()方法返回的MyBinder對象,②號代碼可以通過MyBinder對象訪問Service的運行狀態。

點擊綁定Service按鈕,在Logcat面板可以看到如下輸出:

點擊獲取Service狀態按鈕,可以看到如下圖所示的輸出:

點擊解綁Service按鈕,在Logcat面板可以看到如下輸出:

如上圖所示,當程序調用unbindService()方法解除對某個Service的綁定時,系統會先調用該Service的onUnbind()方法,然后再回調onDestroy()方法。

與多次調用startService()方法啟動不同的是,多次調用bindService()方法并不會執行重復綁定。

Service的生命周期


隨著應用程序啟動Service方式不同,Service的生命周期也略有差異,如下圖:

如果應用程序通過startService()方法來啟動Service,Service的生命周期如上圖左半部分所示。如果應用程序通過bindService()方法來啟動Service,Service的生命周期如上圖右半部分所示。

當Activity調用bindService()綁定一個已通過startService()啟動的Service時,系統只是把Service內部IBinder對象傳給Activity,并不會把該Service生命周期完全綁定到該Activity,因而當Activity調用UNBindService()方法取消與該Service的綁定時,也只是切斷該Activity與Service之間的關聯,并不能停止該Service組件。要停止該Service組件,還需調用stopService()方法。

IntentService


首先看一下Service本身存在的兩個問題。

  • Service不會專門啟動一個單獨的進程,Service與它所在應用位于同一個進程中。
  • Service不是一個新的線程,因此不應該在Service中直接處理耗時的任務。

IntentService正好彌補了Service的不足:IntentService會使用隊列來管理請求Intent,每當客戶端代碼通過Intent請求啟動IntentService時,IntentService會將該Intent加入隊列,然后開啟一條新的worker線程來處理該Intent。對于異步的startService()請求,IntentService會按次序依次處理隊列中的Intent,該線程保證同一時刻只處理一個Intent。由于IntentService使用新的worker線程處理Intent請求,因此IntentService不會阻塞主線程,所以IntentService自己就可以處理耗時任務。

IntentService的特征:

  • IntentService會創建單獨的worker線程來處理所有的Intent請求。
  • IntentService會創建單獨的worker線程來處理onHandleIntent()方法實現的代碼,因此開發者無須處理多線程問題。
  • 當所有請求處理完成后,IntentService會自動停止,因此開發者無須調用stopSelf()方法來停止該Service。
  • 為Service的onBind()方法提供了默認實現,默認實現的onBind()方法返回null。
  • 為Service的onStartCommand()方法提供了默認實現,該實現會將請求Intent添加到隊列中。

擴展IntentService實現Service無須重寫onBind()、onStartCommand()方法,只要重寫onHandleIntent()方法即可。

定時更換壁紙


通過AlarmManager周期性調用某個Service,從而讓系統實現定時更換壁紙的功能。

該程序界面有兩個按鈕,一個用于啟動定時更換壁紙,一個用于關閉定時更換壁紙,代碼如下:

public class MainActivity extends AppCompatActivity {

    private Button mStart;
    private Button mStop;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mStart = (Button) findViewById(R.id.start);
        mStop = (Button) findViewById(R.id.stop);
        //指定啟動ChangeService組件
        Intent intent = new Intent(MainActivity.this, ChangeService.class);
        //創建PendingIntent對象
        final PendingIntent pi = PendingIntent.getService(MainActivity.this, 0, intent, 0);

        mStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //獲取AlarmManager對象
                AlarmManager alarmManager = (AlarmManager) getSystemService(Service.ALARM_SERVICE);
                //設置每隔2秒執行pi所代表的組件一次
                alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, 0, 2000, pi);
                mStart.setEnabled(false);
                mStop.setEnabled(true);
                Toast.makeText(MainActivity.this, "壁紙定時更換啟動成功啦", Toast.LENGTH_SHORT).show();
            }
        });

        mStop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mStart.setEnabled(true);
                mStop.setEnabled(false);
                //獲取AlarmManager對象
                AlarmManager alarmManager = (AlarmManager) getSystemService(Service.ALARM_SERVICE);
                //取消對pi的調度
                alarmManager.cancel(pi);
            }
        });
    }
}

上面程序代碼指定程序每2秒執行一次pi所代表的組件。程序中pi代表了ChangeService組件,ChangeService代碼如下:

public class ChangeService extends Service {

    //定義定時更換的壁紙資源
    int[] wallpapers = new int[]{
            R.drawable.author,
            R.drawable.girl,
            R.drawable.life
    };

    //定義系統的壁紙管理服務
    WallpaperManager wallpaperManager;
    //定義當前所顯示的壁紙
    int current = 0;

    @Override
    public void onCreate() {
        super.onCreate();
        //初始化WallPaperManager
        wallpaperManager = WallpaperManager.getInstance(this);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //如果到了最后一張,系統重新開始
        if (current >= 3) {
            current = 0;
        }
        try {
            //改變壁紙
            wallpaperManager.setResource(wallpapers[current++]);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return START_STICKY;
    }

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

重寫Service的onStartCommand()方法,也就是每次啟動該Service時都會執行onStartCommand()方法中的代碼,更換壁紙的代碼就放在該方法中。

為了允許該程序改變壁紙,還需在AndroidManifest.xml中添加權限:

<uses-permission android:name="android.permission.SET_WALLPAPER" />

運行該程序,點擊開始,返回桌面即可看到系統壁紙每2秒更換一次,效果圖如下:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容