13.5 創建定時任務
Android
中的定時任務一般有兩種實現方式,一種是使用Java API
里提供的Timer
類,一種是使用Android
的Alarm
機制。這兩種方式在多數情況下都能實現類似的效果,但Timer
有一個明顯的短板,它并不太適用于那些需要長期在后臺運行的定時任務。我們都知道為了能讓電池更加耐用,每種手機都會有自己的休眠策略,Android
手機就會在長時間不操作的情況下自動讓CPU
進入到睡眠狀態,這就有可能導致Timer
中的定時任務無法正常運行。而Alarm
則具有喚醒CPU
的功能,它可以保證在大多數情況下需要執行定時任務的時候CPU
都能正常工作。需要注意: 這里喚醒CPU
和喚醒屏幕完全不是一個概念,千萬不要產生混淆。
13.5.1 Alarm機制
Alarm
機制的用法主要就是借助了AlarmManager
類來實現的。這個類和NotificationManager
有點類似,都是通過調用Context
的getSystemService()
方法來獲取實例的,只是這里需要傳入的參數是Context.ALARM_SERVICE
。
獲取一個AlarmManager的實例:
AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
接下來調用AlarmManager
的set()
方法就可以設置一個定時任務了,比如說想要設定一個任務在10
秒鐘后執行
long triggerAtTime = SystemClock.elapsedRealtime() + 10 * 1000;
manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,triggerAtTime,pendingIntent);
set()
方法中需要傳入的3
個參數稍微有點復雜。
第一個參數是一個整形參數,用于指定AlarmManager
的工作類型,有四種值可選,
ELAPSED_REALTIME
: 讓定時任務的觸發時間從系統開機開始算起,但不會喚醒CPU
。
ELAPSED_REALTIME_WAKEUP
: 表示讓定時任務的觸發時間從系統開機開始算起,但會喚醒CPU
。
RTC
: 讓定時任務的觸發時間從1970
年1
月1
日0
點開始算起,但不會喚醒CPU
。
RTC_WAKEUP
: 讓定時任務的觸發時間從1970
年1
月1
日0
點開始算起,但會喚醒CPU
。
使用SystemClock.elapsedRealtime()
方法可以獲取到系統開機至今所經歷時間的毫秒數,使用System.currentTimeMillis()
方法可以獲取到從1970
年1
月1
日0
點至今所經歷時間的毫秒數。
第二個參數就好理解多了,就是定時任務觸發的時間,以毫秒為單位。如果第一個參數使用的是ELAPSED_REALTIME
或ELAPSED_REALTIME_WAKEUP
,則這里傳入開機至今的時間再加上延遲至今的時間。如果第一個參數使用的是RTC
或RTC_WAKEUP
則這里傳入1970
年1
月1
日0
點至今的時間再加上延遲至今的時間。
第三個參數是一個PendingIntent
,這里我們一般會調用getService()
方法或者getBroadcast()
方法來獲取一個能夠執行服務或廣播的PendingIntent
。這樣當定時任務被觸發的時候,服務的onStartCommand()
方法或廣播接收器的onReceive()
方法就可以得到執行。
設定一個任務在10秒鐘后執行可以寫成:
long triggerAtTime = System.currentTimeMillis() + 10 * 1000;
manager.set(AlarmManager.RTC_WAKEUP,triggerAtTime,pendingIntent);
如果我們要實現一個長時間在后臺定時運行的服務,首先新建一個普通的服務,起名叫LongRunningService,然后將觸發定時任務的代碼寫到onStartCommand()方法中。
public class LongRunningService extends Service
{
@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()
{
//執行具體的邏輯操作
}
}).start();
AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
int anHour = 60 * 60 * 1000; //這是一小時的毫秒數
long triggerAtTime = SystemClock.elapsedRealtime() + anHour;
Intent i = new Intent(this,LongRunningService.class);
PendingIntent pi = PendingIntent.getService(this,0,i,0);
manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,triggerAtTime,pi);
return super.onStartCommand(intent, flags, startId);
}
}
我們先是在onStartCommand()方法中開啟了一個子線程,這樣就可以在這里執行具體的邏輯操作了。之所以要在子線程里執行邏輯操作,是因為邏輯操作也是需要耗時的,如果放在主線程里執行可能會對定時任務的準確性造成輕微的影響。
創建線程之后的代碼就是我們剛剛講解的Alarm機制的用法了。首先獲取到了AlarmManager的實例,然后定義任務的觸發時間為一小時后,再使用PendingIntent指定處理定時任務的服務為LongRunningService,最后調用set()方法完成設定。
這樣我們就將一個長時間在后臺定時運行的服務成功實現了。因為一旦啟動了LongRunningService,就會在onStartCommand()方法中設定一個定時任務,這樣一小時后將會再次啟動LongRunningService,從而也就形成了一個永久的循環,保證LongRunningService的onStartCommand()方法可以每隔一小時就執行一次。
最后,只需要在你想要啟動定式服務的時候調用如下代碼即可:
Intent intent = new Intent(context,LongRunningService.class);
context.startService(intent);
另外需要注意的是,從Android4.4系統開始,Alarm任務的觸發時間將會變得不正確,有可能會延遲一段時間后任務才能得到執行。這并不是個bug,而是系統在耗電性方面進行的優化。系統會自動檢測目前有多少Alarm任務存在,然后將觸發時間相近的幾個任務放在一起執行,這就可以大幅度地減少CPU被喚醒的次數,從而有效延長電池的有效時間。
如果你要求Alarm任務的執行時間必須準確無誤,Android仍然提供了解決方案。使用AlarmManager的setExact()方法來替代set()方法,就基本可以保證任務能夠準時執行了。
13.5.2 Doze模式
雖然Android
的每個系統版本都在手機電量方面努力進行優化,不過一直沒能解決后臺服務泛濫,手機電量消耗過快的問題。于是在Android6.0
系統中,谷歌加入了一個全新的Doze
模式,從而可以極大幅度地延長電池的使用壽命。
首先看一下到底什么是Doze
模式。當用戶的設備是Android6.0
或以上系統時,如果該設備未插接電源,處于靜止狀態(Android7.0中刪除了這一條)
,且屏幕關閉了一段時間之后,就會進入到Doze
模式。在Doze
模式下,系統會對CPU
,網絡,Alarm
等活動進行限制,從而延長電池的使用壽命。
當然,系統并不會一直處于Doze
模式,而是會間歇性地退出Doze
模式一小段時間,在這段時間中,應用就可以去完成它們的同步操作,Alarm
任務,等等。
左邊是未插電源,設備靜止,屏幕關閉,接著是短暫退出Doze
模式
可以看到,隨著設備進入Doze
模式的時間越長,間歇性地退出Doze
模式的時間間隔也會越長。因為如果設備長時間不使用的話是沒必要頻繁退出Doze
模式來執行同步等操作的,Android
在這些細節上的把控使的電池壽命進一步得到了延長。
接下來我們具體看一看Doze
模式下有哪些功能會受到限制吧。
- 網絡訪問被禁止
- 系統忽略喚醒
CPU
或者屏幕操作 - 系統不再執行
WIFI
掃描 - 系統不再執行同步服務
-
Alarm
任務將會在下次退出Doze
模式的時候執行
注意其中的最后一條,也就是說,在Doze
模式下,我們的Alarm
任務將會變得不準時。當然,這在大多數情況下都是合理的,因為只有當用戶長時間不使用手機的時候才會進入Doze
模式,通常在這種情況下對Alarm
任務的準時性要求并沒有那么高。
不過,如果你真的有非常特殊的需求,要求Alarm
任務即使在Doze
模式下也必須正常執行,Android
還是提供了解決方案。調用AlarmManager
的setAndAllowWhileIdle()
或setExactAndAllowWhileIdle()
方法就能讓定時任務即使在Doze
模式下也能正常執行了,這兩個方法之間的區別和set()
,setExact()
方法之間的區別是一樣的。
13.6
Android7.0系統中引入了一個非常有特色的功能---多窗口模式
13.6.1 進入多窗口模式
我們不用編寫任何額外的代碼來讓應用程序支持多窗口模式。
如何才能進入到多窗口模式?
手機底部有一個正方形的Overview按鈕,它的作用是打開一個最近訪問過的活動或任務的列表界面。
我們可以通過以下兩種方式進入多窗口模式。
在Overview列表界面長按任意一個活動的標題,將該活動拖動到屏幕突出顯示的區域,則可以進入多窗口模式。
打開任意一個程序,長按Overview按鈕,也可以進入多窗口模式。
我們還可以將模擬器旋轉至水平方向,這樣上下分屏的多窗口模式會自動轉換成左右分屏的多窗口模式。
如果想要退出多窗口模式,只需要再次長按Overview按鈕,或者將屏幕中央的分割線向屏幕任意一個方向拖動到底即可。
可以看出,再多窗口模式下,整個應用的界面會縮小很多,那么編寫程序時就應該多考慮使用match_parent屬性,RecyclerView,ListView,ScrollView等控件,來讓應用的界面能夠更好地適配各種不同尺寸的屏幕,盡量不要出現屏幕尺寸變化過大時界面就無法正常顯示的情況。
13.6.2 多窗口模式下的生命周期
其實多窗口模式并不會改變活動原有的生命周期,只是會將用戶最近交互過的那個活動設置為運行狀態,而將多窗口模式下另外一個可見的活動設置為暫停狀態。如果這時用戶又去和暫停的活動進行交互,那么該活動就變成運行狀態,之前處于運行狀態的活動變成暫停狀態。
我們選擇MaterialTest項目和LBSTest項目。
先啟動MaterialTest項目:
MaterialTest: onCreate
MaterialTest: onStart
MaterialTest: onResume
然后長按Overview按鈕,進入多窗口模式:
MaterialTest: onPause
MaterialTest: onStop
MaterialTest: onDestory
MaterialTest: onCreate
MaterialTest: onStart
MaterialTest: onResume
MaterialTest: onPause
可以看到MaterialTest經歷了一個重建的過程。其實這是個正常現象,因為進入到多窗口模式后活動的大小發生了比較大的變化,此時默認是會重新創建活動的。進入多窗口模式后,MaterialTest變成了暫停狀態。
接著在Overview列表界面選中LBSTest程序
LBSTest: onCreate
LBSTest: onStart
LBSTest: onResume
現在LBSTset變成了運行狀態。
我們隨意操作一下MaterialTest程序:
LBSTest: onPause
MaterialTest: onResume
說明LBSTest變成了暫停狀態,MaterialTest變成了運行狀態。
這和我們在本小節開頭所分析的生命周期行為是一致的。
在多窗口模式下,用戶仍然可以看到處于暫停狀態的應用,那么像視頻播放器之類的應用在此時就應該能繼續播放視頻才對。因此,我們最好不要再活動的onPause()方法中去處理視頻播放的暫停邏輯,而是應該在onStop()方法中去處理,并且在onStart()方法恢復視頻的播放。
另外,針對于進入多窗口模式時活動會被重新創建,如果你想改變這一默認行為,可以在AndroidManifest.xml活動中進行如下配置。
<activity
android:name= ".MainActivity"
android:label= "Fruits"
android:configChanges="orientation|keyboardHidden|screenSize|screenLayout">
</activity>
加入了這行配置之后,不管是進入多窗口模式,還是橫豎屏切換,活動都不會被重新創建,而是會將屏幕發生變化的事件通知到Activity的onConfigurationChanged()方法當中。因此,如果你想在屏幕發生變化的時候進行相應的邏輯處理,那么在活動中重寫onConfigurationChanged()方法即可。
13.6.3 禁用多窗口模式
多窗口模式雖然功能非常強大,但是未必就適用于所有的程序。
因此,Android還是給我們提供了禁用多窗口模式的選項,如果你非常不希望自己的應用能夠在多窗口模式下運行,那么就可以將這個功能關閉掉。
只需要在AndroidManifest.xml的<application>或<activity>標簽中加入如下屬性即可:
android:resizeableActivity=["true" | "false"];
其中,true表示應用支持多窗口模式,false表示應用不支持多窗口模式,如果不配置這個屬性,那么默認值為true。
配置好之后,打開程序。
現在是無法進入到多窗口模式的,而是屏幕下方還會彈出一個Toast提示來告知用戶,當前應用不支持多窗口模式。
雖說android:resizeableActivity這個屬性的用法很簡單,但是它還存在著一個問題,就是這個屬性只有當項目的targetSdkVersion指定成24或者更高的時候才會有用,否則這個屬性是無效的。那么比如說我們將項目的targetSdkVersion指定成23,這個是夠嘗試進入多窗口模式。
可以看到,雖說界面上彈出了一個提示,告知我們此應用在多窗口模式下可能無法正常工作,但還是進入了多窗口模式。
針對這個情況,還有一種解決方案,Android規定,如果項目指定的targetSdkVersion低于24,并且活動是不允許橫豎屏切換的,那么該應用也將不支持多窗口模式。
默認情況下,我們的應用都是可以隨著手機的旋轉自由地橫豎屏切換,如果想要應用不允許橫豎屏切換,那么就需要在AndroidManifest.xml的<activity>標簽中加入如下屬性即可:
android:screenOrientation=["portrail" | "landscape"];
portrail表示活動只允許豎屏,landscape表示活動只允許橫屏,當然android:screenOrientation還有很多的可選值,"portrail" | "landscape"是最常用的。
13.7 Lambda表達式
Java8
中著實引入了一些非常有特色的功能,如Lambda
表達式,stream API
,接口默認實現
。
stream API
,接口默認實現
等特性都是只支持Android7.0
及以上的系統。而Lambda
表達式卻最低兼容到Android2.3
系統,幾乎覆蓋了所有Android
手機。
Lambda
表達式本質上是一種匿名方法,它既沒有方法名,也沒有訪問修飾符和返回值類型,使用它來編寫代碼將會更加簡潔,也更加易讀。
如果想要在Android
項目中使用Lambda
表達式或者Java8
的其他新特性,首先我們需要在app/build.gradle
中添加如下配置。
android {
·············
jackOptions.enabled = true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
之后就可以使用Lambda
表達式了。比如說傳統情況下開啟一個子線程的寫法如下:
new Thread(new Runnable(){
@Override
public void run(){
// 處理具體的邏輯
}
}).start();
而使用Lambda
表達式則可以這樣使用:
new Thread(() ->{
//處理具體的邏輯
}).start();
因為Thread
類的構造函數接收的參數是一個Runnable
接口,并且該接口中只有一個待實現方法。我們看一下Runnable
接口的源碼
public interface Runnable {
public abstract void run();
}
凡是這種只有一個待實現方法的接口,都可以使用Lambda
表達式的寫法。比如說,通常創建一個類似于上述接口的匿名類實現需要這樣寫:
Runnable runnable = new Runnable(){
@Override
public void run(){
//添加具體的邏輯
}
};
而有了Lambda
表達式之后,我們可以這樣寫:
Runnable runnable1 = () -> {
//添加具體的實現
};
接下來我們嘗試自定義一個接口,然后再使用Lambda
表達式的方式進行實現:
public interface MyListener {
String doSomething(String a,int b);
}
MyListener
接口中也只有一個待實現方法,這和Runnable
接口的結構是基本一致的。唯一不同的是,MyListener
中的`doSomething()方法是有參數并且有返回值的。
MyListener listener = (String a,int b) -> {
String result = a + b;
return result;
};
doSomething()
方法的參數直接寫在括號里就可以了,而返回值仍然像往常一樣,寫在具體實現的最后一行即可。
另外,Java
還可以根據上下文自動推斷出Lambda
表達式中的參數類型,上面的也可以簡化為:
MyListener listener1 = (a,b) ->{
String result = a + b;
return result;
};
Java
將會自動推斷出參數a
是String
類型,參數b
是int
類型,從而使得我們的代碼變得更加精簡了。
看個例子:
public void hello(MyListener listener) {
String a = "Hello Lambda";
int b = 1024;
String result = listener.doSomething(a,b);
Log.d(TAG, result);
}
在調用hello()
這個方法可以這樣寫;
hello((a,b) -> {
String result = a + b;
return result;
});
那么doSomething()
方法就會將a
和b
兩個參數進行相加,結果是Hello Lambda1024
。
在Android中
的應用:
button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v){
// 處理點擊事件
}
});
使用Lambda
后表達式后為:
button = (Button) findViewById(R.id.button);
button.setOnClickListener((v) -> {
// 處理點擊事件
});
另外,當接口的待實現方法有且只有一個參數的時候,還可以進一步簡化
button = (Button) findViewById(R.id.button);
button.setOnClickListener(v -> {
// 處理點擊事件
});