13(下) 繼續進階---你還應該掌握高級技巧

動漫美少女.jpg

13.5 創建定時任務

Android中的定時任務一般有兩種實現方式,一種是使用Java API里提供的Timer類,一種是使用AndroidAlarm機制。這兩種方式在多數情況下都能實現類似的效果,但Timer有一個明顯的短板,它并不太適用于那些需要長期在后臺運行的定時任務。我們都知道為了能讓電池更加耐用,每種手機都會有自己的休眠策略,Android手機就會在長時間不操作的情況下自動讓CPU進入到睡眠狀態,這就有可能導致Timer中的定時任務無法正常運行。而Alarm則具有喚醒CPU的功能,它可以保證在大多數情況下需要執行定時任務的時候CPU都能正常工作。需要注意: 這里喚醒CPU和喚醒屏幕完全不是一個概念,千萬不要產生混淆。

13.5.1 Alarm機制

Alarm機制的用法主要就是借助了AlarmManager類來實現的。這個類和NotificationManager有點類似,都是通過調用ContextgetSystemService()方法來獲取實例的,只是這里需要傳入的參數是Context.ALARM_SERVICE

獲取一個AlarmManager的實例:
AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

接下來調用AlarmManagerset()方法就可以設置一個定時任務了,比如說想要設定一個任務在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: 讓定時任務的觸發時間從1970110點開始算起,但不會喚醒CPU

RTC_WAKEUP: 讓定時任務的觸發時間從1970110點開始算起,但會喚醒CPU

使用SystemClock.elapsedRealtime()方法可以獲取到系統開機至今所經歷時間的毫秒數,使用System.currentTimeMillis()方法可以獲取到從1970110點至今所經歷時間的毫秒數。

第二個參數就好理解多了,就是定時任務觸發的時間,以毫秒為單位。如果第一個參數使用的是ELAPSED_REALTIMEELAPSED_REALTIME_WAKEUP,則這里傳入開機至今的時間再加上延遲至今的時間。如果第一個參數使用的是RTCRTC_WAKEUP則這里傳入1970110點至今的時間再加上延遲至今的時間。

第三個參數是一個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模式的工作過程.jpg

左邊是未插電源,設備靜止,屏幕關閉,接著是短暫退出Doze模式

可以看到,隨著設備進入Doze模式的時間越長,間歇性地退出Doze模式的時間間隔也會越長。因為如果設備長時間不使用的話是沒必要頻繁退出Doze模式來執行同步等操作的,Android在這些細節上的把控使的電池壽命進一步得到了延長。

接下來我們具體看一看Doze模式下有哪些功能會受到限制吧。

  • 網絡訪問被禁止
  • 系統忽略喚醒CPU或者屏幕操作
  • 系統不再執行WIFI掃描
  • 系統不再執行同步服務
  • Alarm任務將會在下次退出Doze模式的時候執行

注意其中的最后一條,也就是說,在Doze模式下,我們的Alarm任務將會變得不準時。當然,這在大多數情況下都是合理的,因為只有當用戶長時間不使用手機的時候才會進入Doze模式,通常在這種情況下對Alarm任務的準時性要求并沒有那么高。

不過,如果你真的有非常特殊的需求,要求Alarm任務即使在Doze模式下也必須正常執行,Android還是提供了解決方案。調用AlarmManagersetAndAllowWhileIdle()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將會自動推斷出參數aString類型,參數bint類型,從而使得我們的代碼變得更加精簡了。

看個例子

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

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,662評論 25 708
  • 本篇文章主要介紹以幾下個知識點:獲取全局 Context使用 Intent 傳遞對象定制日志工具創建定時任務多窗口...
    開心wonderful閱讀 1,443評論 5 28
  • 學踩高翹記 新的一周從滴答滴答的雨滴開始,今天我們一改往日的與雨滴妹妹的親密接觸,簇擁著來到了大棚里,...
    安姐的媽咪閱讀 609評論 0 0
  • 我愿意變成一個冷漠的人。這樣才會讓我客觀的去看待這個世界,無法戀愛,無法和任何人走的太近,不會全心全意付出,任何事...
    jes2321閱讀 150評論 0 1