本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了,
每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以后閱讀和查閱,最后,非常感激郭霖先生提供這么好的書籍,
第13章 繼續進階——你還應該掌握的高級技巧
13.1 全域獲取Context的技巧
回想這么久以來我們所學的內容,你會發現有很多地方都需要用到Context,彈出Toast的時候需要,啟動活動的時候需要,發送廣播的時候需要,操作資料庫的時候需要,使用通知的時候需要,等等等等,
或許目前你還沒有為得不到Context而發愁過,因為我們很多的操作都是在活動中進行的,而活動本身就是一個Context物件,但是,當應用程式的架構逐漸開始復雜起來的時候,很多的邏輯代碼都將脫離Activity類,但此時你又恰恰需要使用Context,也許這個時候你就會感到有些傷腦筋了,
舉個例子來說吧,在第9章的最佳實踐環節,我們撰寫了一個HttpUtil類,在這里將一些通用的網路操作封裝了起來,代碼如下所示:
package com.zhouzhou.networktest;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class HttpUtil {
public static void sendHttpRequest(final String address,final HttpCallbackListener listener) {
new Thread(new Runnable() {
@Override
public void run() {
HttpURLConnection connection = null;
try {
URL url = new URL(address);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
connection.setDoInput(true);
connection.setDoOutput(true);
InputStream inputStream = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
if (listener != null) {
// 回呼onFinish()方法
listener.onFinish(response.toString());
}
} catch (Exception e) {
e.printStackTrace();
// 回呼onError()方法
listener.onError(e);
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
}).start();
}
}
這里使用sendHttpRequest()方法來發送HTTP請求顯然是沒有問題的,并且我們還可以在回呼方法中處理服務器回傳的資料,但現在我們想對sendHttpRequest()方法進行一些優化,當檢測到網路不存在的時候就給用戶一個Toast提示,并且不再執行后面的代碼,看似一個挺簡單的功能,可是卻存在一個讓人頭疼的問題,彈出Toast提示需要一個Context引數,而我們在HttpUtil類中顯然是獲取不到Context物件的,這該怎么辦呢?
其實要想快速解決這個問題也很簡單,大不了在sendHttpRequest()方法中添加一個Context引數就行了嘛,于是可以將HttpUtil中的代碼進行如下修改:
public class HttpUtil {
public static void sendHttpRequest(final Context context,final String address,final HttpCallbackListener listener) {
if (! isNetworkAvailable()) {
Toast.makeText(context,"network is unavailable",Toast.LENGT SHORT).show();
return;
}
new Thread(new Runnable() {
@Override
public void run() {
...
}
}).start();
}
private static boolean isNetworkAvailable() {
...
}
}
可以看到,這里在方法中添加了一個Context引數,并且假設有一個isNetworkAvailable()方法用于判斷當前網路是否可用,如果網路不可用的話就彈出Toast提示,并將方法return掉,
雖說這也確實是一種解決方案,但是卻有點推卸責任的嫌疑,因為我們將獲取Context的任務轉移給了sendHttpRequest()方法的呼叫方,至于呼叫方能不能得到Context物件,那就不是我們需要考慮的問題了,
由此可以看出,在某些情況下,獲取Context并非是那么容易的一件事,有時候還是挺傷腦筋的,不過別擔心,下面我們就來學習一種技巧,讓你在專案的任何地方都能夠輕松獲取到Context,
Android提供了一個Application類,每當應用程式啟動的時候,系統就會自動將這個類進行初始化,而我們可以定制一個自己的Application類,以便于管理程式內一些全域的狀態資訊,比如說全域Context,
定制一個自己的Application其實并不復雜,首先我們需要創建一個MyApplication類繼承自Application,代碼如下所示:
public class MyApplication extends Application {
private static Context context;
@Override
public void onCreate() {
context = getApplicationContext();
}
public static Context getContext() {
return context;
}
}
這里我們重寫了父類的onCreate()方法,并通過呼叫getApplicationContext()方法得到了一個應用程式級別的Context,然后又提供了一個靜態的getContext()方法,在這里將剛才獲取到的Context進行回傳,
接下來我們需要告知系統,當程式啟動的時候應該初始化MyApplication類,而不是默認的Application類,這一步也很簡單,在AndroidManifest.xml檔案的<application>標簽下進行指定就可以了,代碼如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhouzhou.networktest"
android:versionCode="1"
android:versionName="1.0">
...
<application
android:name="com.zhouhzou.networktest.MyApplication"
...>
...
</application>
</manifest>
注意,這里在指定MyApplication的時候一定要加上完整的包名,不然系統將無法找到這個類,
這樣我們就已經實作了一種全域獲取Context的機制,之后不管你想在專案的任何地方使用Context,只需要呼叫一下MyApplication.getContext()就可以了,那么接下來我們再對sendHttpRequest()方法進行優化,代碼如下所示:
public static void sendHttpRequest(final String address,final HttpCallbackListener listener) {
if (! isNetworkAvailable()) {
Toast.makeText(MyApplication.getContext(),"network is unavailable",Toast.LENGTH_SHORT).show();
return;
}
...
}
可以看到,sendHttpRequest()方法不需要再通過傳參的方式來得到Context物件,而是呼叫一下MyApplication.getContext()方法就可以了,
有了這個技巧,你再也不用為得不到Context物件而發愁了,然后我們再回顧一下6.5.2小節學過的內容,當時為了讓LitePal可以正常作業,要求必須在AndroidManifest.xml中配置如下內容:
<application
android:name="org.litepal.LitePalApplication"
...>
...
</application>
其實道理也是一樣的,因為經過這樣的配置之后,LitePal就能在內部自動獲取到Context了,
不過這里你可能又會產生疑問,如果我們已經配置過了自己的Application怎么辦?這樣豈不是和LitePalApplication沖突了?沒錯,任何一個專案都只能配置一個Application,對于這種情況,LitePal提供了很簡單的解決方案,那就是在我們自己的Application中去呼叫LitePal的初始化方法就可以了,如下所示:
public class MyApplication extends Application {
private static Context context;
@Override
public void onCreate() {
context = getApplicationContext();
LitePal.initialize(context);
}
public static Context getContext() {
return context;
}
}
使用這種寫法,就相當于我們把全域的Context物件通過引數傳遞給了LitePal,效果和在AndroidManifest.xml中配置LitePalApplication是一模一樣的,
13.2 使用Intent傳遞物件
Intent的用法:可以借助它來啟動活動、發送廣播、啟動服務等,在進行上述操作的時候,我們還可以在Intent中添加一些附加資料,以達到傳值的效果,比如在FirstActivity中添加如下代碼:
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
intent.putExtra("string_data","hello");
intent.putExtra("int_data",100);
startActivity(intent);
這里呼叫了Intent的putExtra()方法來添加要傳遞的資料,之后在SecondActivity中就可以得到這些值了,代碼如下所示:
getIntent().getStringExtra("string_data");
getIntent().getIntExtra("int_data",0);
不知道你有沒有發現,putExtra()方法中所支持的資料型別是有限的,雖然常用的一些資料型別它都會支持,但是當你想去傳遞一些自定義物件的時候,就會發現無從下手,不用擔心,下面我們就學習一下使用Intent來傳遞物件的技巧,
13.2.1 Serializable方式
使用Intent來傳遞物件通常有兩種實作方式:Serializable和Parcelable,本小節中學習第一種實作方式:
Serializable是序列化的意思,表示將一個物件轉換成可存盤或可傳輸的狀態,序列化后的物件可以在網路上進行傳輸,也可以存盤到本地,至于序列化的方法也很簡單,只需要讓一個類去實作Serializable這個介面就可以了,比如說有一個Person類,其中包含了name和age這兩個欄位,想要將它序列化就可以這樣寫:
public class Person implements Serializable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
其中,get、set方法都是用于賦值和讀取欄位資料的,最重要的部分是在第一行,這里讓Person類去實作了Serializable介面,這樣所有的Person物件就都是可序列化的了,接下來在FirstActivity中的寫法非常簡單:
Person person = new Person();
person.setName("Tom");
person.setAge(20);
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
intent.putExtra("person_data",person);
startActivity(intent);
可以看到,這里我們創建了一個Person的實體,然后就直接將它傳入到putExtra()方法中了,由于Person類實作了Serializable介面,所以才可以這樣寫,接下來在SecondActivity中獲取這個物件也很簡單,寫法如下:
Person person = (Persion) getIntent.getSerializableExtra("person_data");
這里呼叫了getSerializableExtra()方法來獲取通過引數傳遞過來的序列化物件,接著再將它向下轉型成Person物件,這樣我們就成功實作了使用Intent來傳遞物件的功能了,
13.2.2 Parcelable方式
除了Serializable之外,使用Parcelable也可以實作相同的效果,不過不同于將物件進行序列化,Parcelable方式的實作原理是將一個完整的物件進行分解,而分解后的每一部分都是Intent所支持的資料型別,這樣也就實作傳遞物件的功能了,下面我們來看一下Parcelable的實作方式,修改Person中的代碼,如下所示:
public class Person implements Parcelable {
private String name;
private int age;
...
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest,int flags) {
dest.writeString(name);//寫出name
dest.writeInt(age);//寫出age
}
public static final Parcelable.Creator<Person> CREATOR = new Parcelable.Creator<Person>() {
@Override
public Person createFromParcel(Parcel source) {
Person person = new Person();
person.name = source.readString();//讀取name
person.age = source.readInt();//讀取age
return person;
}
@Override
public Person[] newArray(int size) {
return new Person[size]
}
}
}
Parcelable的實作方式要稍微復雜一些,可以看到,首先我們讓Person類去實作了Parcelable介面,這樣就必須重寫describeContents()和writeToParcel()這兩個方法,其中describeContents()方法直接回傳0就可以了,而writeToParcel()方法中我們需要呼叫Parcel的writeXxx()方法,將Person類中的欄位一一寫出,注意,字串型資料就呼叫writeString()方法,整型資料就呼叫writeInt()方法,以此類推,
除此之外,我們還必須在Person類中提供一個名為CREATOR的常量,這里創建了Parcelable.Creator介面的一個實作,并將泛型指定為Person,接著需要重寫createFromParcel()和newArray()這兩個方法,在createFromParcel()方法中我們要去讀取剛才寫出的name和age欄位,并創建一個Person物件進行回傳,其中name和age都是呼叫Parcel的readXxx()方法讀取到的,注意這里讀取的順序一定要和剛才寫出的順序完全相同,而newArray()方法中的實作就簡單多了,只需要new出一個Person陣列,并使用方法中傳入的size作為陣列大小就可以了,
接下來,在FirstActivity中我們仍然可以使用相同的代碼來傳遞Person物件,只不過在SecondActivity中獲取物件的時候需要稍加改動,如下所示:
Person person = (Person) getIntent().getParcelableExtra("person_data");
注意,這里不再是呼叫getSerializableExtra()方法,而是呼叫getParcelableExtra()方法來獲取傳遞過來的物件了,其他的地方都完全相同,
對比一下,Serializable的方式較為簡單,但由于會把整個物件進行序列化,因此效率會比Parcelable方式低一些,所以在通常情況下還是更加推薦使用Parcelable的方式來實作Intent傳遞物件的功能,
13.3 定制自己的日志工具
雖然Android中自帶的日志工具功能非常強大,但也不能說是完全沒有缺點,例如在列印日志的控制方面就做得不夠好,
打個比方,你正在撰寫一個比較龐大的專案,期間為了方便除錯,在代碼的很多地方都列印了大量的日志,最近專案已經基本完成了,但是卻有一個非常讓人頭疼的問題,之前用于除錯的那些日志,在專案正式上線之后仍然會照常列印,這樣不僅會降低程式的運行效率,還有可能將一些機密性的資料泄露出去,
那該怎么辦呢?難道要一行一行地把所有列印日志的代碼都刪掉?顯然這不是什么好點子,不僅費時費力,而且以后你繼續維護這個專案的時候可能還會需要這些日志,因此,最理想的情況是能夠自由地控制日志的列印,當程式處于開發階段時就讓日志列印出來,當程式上線了之后就把日志屏蔽掉,看起來好像是挺高級的一個功能,其實并不復雜,我們只需要定制一個自己的日志工具就可以輕松完成了,比如新建一個LogUtil類,代碼如下所示:
public class LogUtil {
public static final int VERBOSE = 1;
public static final int DEBUG =2;
public static final int INFO = 3;
public static final int WARN = 4;
public static final int ERROR = 5;
public static final int NOTHING = 6;
public static int level = VERBOSE;
public static void v (String tag,String msg) {
if (level <= VERBOSE) {
Log.v (tag,msg);
}
}
public static void d (String tag,String msg) {
if (level <= DEBUG) {
Log.d (tag,msg);
}
}
public static void i (String tag,String msg) {
if (level <= INFO) {
Log.i (tag,msg);
}
}
public static void w (String tag,String msg) {
if (level <=WARN) {
Log.w (tag,msg);
}
}
public static void e(String tag,String msg) {
if (level <= ERROR) {
Log.e (tag,msg);
}
}
}
可以看到,我們在LogUtil中先是定義了VERBOSE、DEBUG、INFO、WARN、ERROR、NOTHING這6個整型常量,并且它們對應的值都是遞增的,然后又定義了一個靜態變數level,可以將它的值指定為上面6個常量中的任意一個,
接下來我們提供了v()、d()、i()、w()、e()這5個自定義的日志方法,在其內部分別呼叫了Log.v()、Log.d()、Log.i()、Log.w()、Log.e()這5個方法來列印日志,只不過在這些自定義的方法中我們都加入了一個if判斷,只有當level的值小于或等于對應日志級別值的時候,才會將日志列印出來,
這樣就把一個自定義的日志工具創建好了,之后在專案里我們可以像使用普通的日志工具一樣使用LogUtil,比如列印一行DEBUG級別的日志就可以這樣寫:
LogUtil.d("TAG","debug log");
列印一行WARN級別的日志就可以這樣寫:
LogUtil.w("TAG","warn log");
然后,只需要修改level變數的值,就可以自由地控制日志的列印行為了,比如讓level等于VERBOSE就可以把所有的日志都列印出來,讓level等于WARN就可以只列印警告以上級別的日志,讓level等于NOTHING就可以把所有日志都屏蔽掉,
使用了這種方法之后,剛才所說的那個問題就不復存在了,你只需要在開發階段將level指定成VERBOSE,當專案正式上線的時候將level指定成NOTHING就可以了,
13.4 除錯Android程式
當開發程序中遇到一些奇怪的bug,但又遲遲定位不出來原因是什么的時候,最好的解決辦法就是除錯了,除錯允許我們逐行地執行代碼,并可以實時觀察記憶體中的資料,從而能夠比較輕易地查出問題的原因,
那么本節中我們就來學習一下使用Android Studio來除錯Android程式的技巧,(以第5章的最佳實踐環節中,程式中有一個登錄功能,比如說現在登錄出現了問題,我們就可以通過除錯來定位問題的原因,)
除錯作業的第一步肯定是添加斷點,添加斷點的方法,只需要在相應代碼行的左邊點擊一下就可以了,如圖:

如果想要取消這個斷點,對著它再次點擊就可以了,添加好了斷點,接下來就可以對程式進行除錯了,點擊Android Studio頂部工具列中的Debug按鈕,就會使用除錯模式來啟動程式:

等到程式運行起來的時候,首先會看到一個提示框,如圖:

這個框很快就會自動消失,然后在輸入框里輸入賬號和密碼,并點擊Login按鈕,這時Android Studio就會自動打開Debug視窗,如圖:

接下來每按一次F8健,代碼就會向下執行一行,并且通過Variables視圖還可以看到記憶體中的資料,如圖:

可以看到,我們從輸入框里獲取到的賬號密碼分別是abc和123,而程式里要求正確的賬號密碼是admin和123456,所以登錄才會出現問題,這樣我們就通過除錯的方式輕松地把問題定位出來了,除錯完成之后點擊Debug視窗中的Stop按鈕來結束除錯即可:

這種除錯方式雖然完全可以正常作業,但在除錯模式下,程式的運行效率將會大大地降低,如果你的斷點加在一個比較靠后的位置,需要執行很多的操作才能運行到這個斷點,那么前面這些操作就都會有一些卡頓的感覺,
沒關系,Android還提供了另外一種除錯的方式,可以讓程式隨時進入到除錯模式,下面我們就來嘗試一下,
這次不需要選擇除錯模式來啟動程式了,就使用正常的方式來啟動程式,由于現在不是在除錯模式下,程式的運行速度比較快,可以先把賬號和密碼輸入好,然后點擊Android Studio頂部工具列的Attach debugger to Androidprocess按鈕:

此時會彈出一個行程選擇提示框,如圖:

這里目前只列出了一個行程,也就是我們當前程式的行程,選中這個行程,然后點擊OK按鈕,就會讓這個行程進入到除錯模式了,
接下來在程式中點擊Login按鈕,Android Studio同樣也會自動打開Debug視窗,之后的流程就都是相同的了,相比起來,第二種除錯方式會比第一種更加靈活,也更加常用,
13.5 創建定時任務
Android中的定時任務一般有兩種實作方式:
-
一種是使用Java API里提供的Timer類;
Timer有一個明顯的短板,它并不太適用于那些需要長期在后臺運行的定時任務,我們都知道,為了能讓電池更加耐用,每種手機都會有自己的休眠策略,Android手機就會在長時間不操作的情況下自動讓CPU進入到睡眠狀態,這就有可能導致Timer中的定時任務無法正常運行,
-
一種是使用Android的Alarm機制;
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的作業型別,有4種值可選,分別是ELAPSED_REALTIME、ELAPSED_REALTIME_WAKEUP、RTC和RTC_WAKEUP,
- 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()方法來獲取一個能夠執行服務或廣播的Pending-Intent,這樣當定時任務被觸發的時候,服務的onStartCommand()方法或廣播接收器的onReceive()方法就可以得到執行,了解了set()方法的每個引數之后,你應該能想到,設定一個任務在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(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);
另外需要注意的是,從Android 4.4系統開始,Alarm任務的觸發時間將會變得不準確,有可能會延遲一段時間后任務才能得到執行,這并不是個bug,而是系統在耗電性方面進行的優化,系統會自動檢測目前有多少Alarm任務存在,然后將觸發時間相近的幾個任務放在一起執行,這就可以大幅度地減少CPU被喚醒的次數,從而有效延長電池的使用時間,
當然,如果你要求Alarm任務的執行時間必須準確無誤,Android仍然提供了解決方案,使用AlarmManager的setExact()方法來替代set()方法,就基本上可以保證任務能夠準時執行了,
13.5.2 Doze模式
雖然Android的每個系統版本都在手機電量方面努力進行優化,不過一直沒能解決后臺服務泛濫、手機電量消耗過快的問題,于是在Android 6.0系統中,谷歌加入了一個全新的Doze模式,從而可以極大幅度地延長電池的使用壽命,
到底什么是Doze模式,當用戶的設備是Android 6.0或以上系統時,如果該設備未插接電源,處于靜止狀態(Android 7.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 多視窗模式編程
(以下為記錄書上的內容,我手上沒有Android 7.0及以上的手機,前面學習的內容,我測驗用的都是6.0的)
由于手機螢屏大小的限制,傳統情況下一個手機只能同時打開一個應用程式,無論是Android、iOS還是Windows Phone都是如此,而Android 7.0系統中卻引入了一個非常有特色的功能——多視窗模式,它允許我們在同一個螢屏中同時打開兩個應用程式,
13.6.1 進入多視窗模式
首先你需要知道,我們不用撰寫任何額外的代碼來讓應用程式支持多視窗模式,事實上,本書中所撰寫的所有專案都是支持多視窗模式的,但是這并不意味著我們就不需要對多視窗模式進行學習,因為系統化地了解這些知識點才能撰寫出在多視窗模式下兼容性更好的程式,
那么先來看一下如何才能進入到多視窗模式,手機的導航欄上面一共有3個按鈕,如圖:

其中左邊的Back按鈕和中間的Home按鈕,右邊的Overview按鈕,這個按鈕的作用是打開一個最近訪問過的活動或任務的串列界面,從而能夠方便地在多個應用程式之間進行切換,如圖:

可以通過以下兩種方式進入多視窗模式,
? 在Overview串列界面長按任意一個活動的標題,將該活動拖動到螢屏突出顯示的區域,則可以進入多視窗模式,
? 打開任意一個程式,長按Overview按鈕,也可以進入多視窗模式,比如說我們首先打開了MaterialTest程式,然后長按Overview按鈕,效果如圖:

可以看到,現在整個螢屏被分成了上下兩個部分,MaterialTest程式占據了上半屏,下半屏仍然還是一個Overview串列界面,另外Overview按鈕的樣式也有了變化,現在我們可以從Overview串列中選擇任意一個其他程式,比如說這里點擊LBSTest,效果如圖:

還可以將模擬器旋轉至水平方向,這樣上下分屏的多視窗模式會自動切換成左右分屏的多視窗模式,如圖:

多視窗模式的用法大概就是這個樣子了,我們可以將任意兩個應用同時打開,這樣就能組合出許多更為豐富的使用場景,
可以看出,在多視窗模式下,整個應用的界面會縮小很多,那么撰寫程式時就應該多考慮使用match_parent屬性、RecyclerView、ListView、ScrollView等控制元件,來讓應用的界面能夠更好地適配各種不同尺寸的螢屏,盡量不要出現螢屏尺寸變化過大時界面就無法正常顯示的情況,
13.6.2 多視窗模式下的生命周期
多視窗模式下的生命周期,其實多視窗模式并不會改變活動原有的生命周期,只是會將用戶最近互動過的那個活動設定為運行狀態,而將多視窗模式下另外一個可見的活動設定為暫停狀態,如果這時用戶又去和暫停的活動進行互動,那么該活動就變成運行狀態,之前處于運行狀態的活動變成暫停狀態,
下面我們還是通過一個例子來更加直觀地理解多視窗模式下活動的生命周期,首先打開MaterialTest專案,修改MainActivity中的代碼,如下所示:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG,"onCreate");
...
}
@Override
protected void onStart(Bundle savedInstanceState) {
super.onStart(savedInstanceState);
Log.d(TAG,"onStart");
}
@Override
protected void onResume(Bundle savedInstanceState) {
super.onResume(savedInstanceState);
Log.d(TAG,"onResume");
}
@Override
protected void onPause(Bundle savedInstanceState) {
super.onPause(savedInstanceState);
Log.d(TAG,"onPause");
}
@Override
protected void onStop(Bundle savedInstanceState) {
super.onStop(savedInstanceState);
Log.d(TAG,"onSop");
}
@Override
protected void onDestroy(Bundle savedInstanceState) {
super.onDestroy(savedInstanceState);
Log.d(TAG,"onDestroy");
}
@Override
protected void onRestart(Bundle savedInstanceState) {
super.onRestart(savedInstanceState);
Log.d(TAG,"onRestart");
}
...
}
這里我們在Activity的7個生命周期回呼方法中分別列印了一句日志,然后點擊Android Studio導航欄上的File→Open Recent→LBSTest,重新打開LBSTest專案,修改MainActivity的代碼,如下所示:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "LBSTest";
...
@Override
protected void onCreate() {
super.onCreate();
Log.d(TAG,"onCreate");
...
}
...
@Override
protected void onStart() {
super.onStart();
Log.d(TAG,"onStart");
}
@Override
protected void onResume() {
super.onResume();
Log.d(TAG,"onResume");
mapView.onResume();
}
@Override
protected void onPause() {
super.onPause();
Log.d(TAG,"onPause");
mapView.onPause();
}
@Override
protected void onStop() {
super.onStop();
Log.d(TAG,"onSop");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG,"onDestroy");
mLocationClient.stop();
baiduMap.setMyLocationEnabled(false);
}
@Override
protected void onRestart() {
super.onRestart();
Log.d(TAG,"onRestart");
}
...
}
這里同樣也是在Activity的7個生命周期回呼方法中分別列印了一句日志,注意這兩處日志的TAG是不一樣的,方便我們進行區分,
現在,先將MaterialTest和LBSTest這兩個專案的最新代碼都運行到模擬器上,然后啟動MaterialTest程式,這時觀察logcat中的列印日志(注意要將logcat的過濾器選擇為No Filters),如圖:

可以看到,onCreate()、onStart()和onResume()方法會依次得到執行,這個也是在我們意料之中的,然后長按Overview按鈕,進入多視窗模式,此時的列印資訊如圖:

會發現,MaterialTest中的MainActivity經歷了一個重新創建的程序,其實這個是正常現象,因為進入多視窗模式后活動的大小發生了比較大的變化,此時默認是會重新創建活動的,
除此之外,像橫豎屏切換也是會重新創建活動的,進入多視窗模式后,MaterialTest變成了暫停狀態,接著在Overview串列界面選中LBSTest程式,列印資訊如圖:

可以看到,現在LBSTest的onCreate()、onStart()和onResume()方法依次得到了執行,說明現在LBSTest變成了運行狀態,接下來我們可以隨意操作一下MaterialTest程式,然后觀察logcat中的列印日志,如圖:

現在LBSTest的onPause()方法得到了執行,而MaterialTest的onResume()方法得到了執行,說明LBSTest變成了暫停狀態,MaterialTest則變成了運行狀態,這和我們在本小節開頭所分析的生命周期行為是一致的,
了解了多視窗模式下活動的生命周期規則,那么我們在撰寫程式的時候,就可以將一些關鍵性的點考慮進去了,
比如說,在多視窗模式下,用戶仍然可以看到處于暫停狀態的應用,那么像視頻播放器之類的應用在此時就應該能繼續播放視頻才對,因此,我們最好不要在活動的onPause()方法中去處理視頻播放器的暫停邏輯,而是應該在onStop()方法中去處理,并且在onStart()方法恢復視頻的播放,
另外,針對于進入多視窗模式時活動會被重新創建,如果你想改變這一默認行為,可以在AndroidManifest.xml中對活動進行如下配置:
<activity
android:name=".MainActivity"
android:lable="Fruits"
android:configChanges="orientation|keyboardHidden|screenSize|screenLayout">
...
</activity>
加入了這行配置之后,不管是進入多視窗模式,還是橫豎屏切換,活動都不會被重新創建,而是會將螢屏發生變化的事件通知到Activity的onConfigurationChanged()方法當中,因此,如果你想在螢屏發生變化的時候進行相應的邏輯處理,那么在活動中重寫onConfiguration-Changed()方法即可,
13.6.3 禁用多視窗模式
多視窗模式雖然功能非常強大,但是未必就適用于所有的程式,比如說,手機游戲就非常不適合在多視窗模式下運行,很難想象我們如何一邊玩著游戲,一邊又操作著其他應用,因此,Android還是給我們提供了禁用多視窗模式的選項,如果你非常不希望自己的應用能夠在多視窗模式下運行,那么就可以將這個功能關閉掉,
禁用多視窗模式的方法非常簡單,只需要在AndroidManifest.xml的<application>或<activity>標簽中加入如下屬性即可:
android:resizeableActivity=["true" | "false"]
其中,true表示應用支持多視窗模式,false表示應用不支持多視窗模式,如果不配置這個屬性,那么默認值為true,現在我們將MaterialTest程式設定為不支持多視窗模式,如下所示:
<application
...
android:resizeableActivity="false">
...
</application>
重新運行程式,然后長按Overview按鈕,結果如圖:

可以看到,現在是無法進入到多視窗模式的,而且螢屏下方還會彈出一個Toast提示來告知用戶,當前應用不支持多視窗模式,
雖說android:resizeableActivity這個屬性的用法很簡單,但是它還存在著一個問題,就是這個屬性只有當專案的targetSdkVersion指定成24或者更高的時候才會有用,否則這個屬性是無效的,那么比如說我們將專案的targetSdkVersion指定成23,這個時候嘗試進入多視窗模式,結果如圖:

可以看到,雖說界面上彈出了一個提示,告知我們此應用在多視窗模式下可能無法正常作業,但還是進入了多視窗模式,
針對這種情況,還有一種解決方案,Android規定,如果專案指定的targetSdkVersion低于24,并且活動是不允許橫豎屏切換的,那么該應用也將不支持多視窗模式,
默認情況下,我們的應用都是可以隨著手機的旋轉自由地橫豎屏切換的,如果想要讓應用不允許橫豎屏切換,那么就需要在AndroidManifest.xml的<activity>標簽中加入如下配置:
android:screenOrientation=["portrait"|"landscape"]
其中,portrait表示活動只支持豎屏,landscape表示活動只支持橫屏,當然android:screenOrientation屬性中還有很多其他可選值,不過最常用的就是portrait和landscape了,現在我們將MaterialTest的MainActivity設定為只支持豎屏,如下所示:
<activity
android:name=".MainActivity"
android:lable="Fruits"
android:screenOrientation="portrait">
...
</activity>
重新運行程式之后你會發現MaterialTest現在不支持橫豎屏切換了,此時長按Overview按鈕會彈出禁用多視窗模式的提示,
13.7 Lambda運算式
Java 8中著實引入了一些非常有特色的功能,如Lambda運算式、streamAPI、介面默認實作,等等,
雖然剛才已經提到了幾個Java 8中的新特性,不過現在能夠立即應用到專案當中的也就只有Lambda運算式而已,因為stream API和介面默認實作等特性都只支持Android 7.0及以上的系統,我們顯然不可能為了使用這些新特性而放棄兼容眾多低版本的Android手機,而Lambda運算式卻最低兼容到Android2.3系統,基本上可以算是覆寫所有的Android手機了,
Lambda運算式本質上是一種匿名方法,它既沒有方法名,也即沒有訪問修飾符和回傳值型別,使用它來撰寫代碼將會更加簡潔,也更加易讀,
如果想要在Android專案中使用Lambda運算式或者Java 8的其他新特性,首先我們需要在app/build.gradle中添加如下配置:
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
注意,不要加入:
jackOptions {
enabled true
}
https://developer.android.com/studio/write/java8-support?utm_source=android-studio 這里提到Jack已不被支持,
之后就可以開始使用Lambda運算式來撰寫代碼了,比如說傳統情況下開啟一個子執行緒的寫法如下:
new Thread(new Runnable() {
@Override
public void run() {
//處理具體邏輯
}
}).start();
而使用Lambda運算式則可以這樣寫:
new Thread(() -> {
//處理具體邏輯
}).start();
不管是從代碼行數上還是縮進結構上來看,Lambda運算式的寫法明顯要更加精簡,
那么為什么我們可以使用這么神奇的寫法呢?這是因為Thread類的建構式接收的引數是一個Runnable介面,并且該介面中只有一個待實作方法,我們查看一下Runnable介面的原始碼,如下所示:
public interface Runnable {
/**
* Start executing the active part of the class' code. This method is
* called when a thread is started that has been created with a class which
* implements {@code Runnable}
*/
public void run();
}
凡是這種只有一個待實作方法的介面,都可以使用Lambda運算式的寫法,比如說,通常創建一個類似于上述介面的匿名類實作需要這樣寫:
Runnable runnable = new Runnable() {
@Override
public void run() {
//添加具體邏輯
}
};
//而有了Lambda運算式之后,我們就可以這樣寫了:
Runnable runnable = () -> {
//添加具體邏輯
};
了解了Lambda運算式的基本寫法,接下來我們嘗試自定義一個介面,然后再使用Lambda運算式的方式進行實作,新建一個MyListener介面,代碼如下所示:
public interface MyListener {
String doSomething(String a,int b);
}
MyListener介面中也只有一個待實作方法,這和Runnable介面的結構是基本一致的,唯一不同的是,MyListener中的doSomething()方法是有引數并且有回傳值的,那么我們就來看一看這種情況下該如何使用Lambda運算式進行實作,其實寫法也是比較相似的,使用Lambda運算式創建MyListener介面的匿名實作寫法如下:
MyListener listener = (String a,int b) -> {
String result = a + b;
return result;
};
可以看到,doSomething()方法的引數直接寫在括號里面就可以了,而回傳值則仍然像往常一樣,寫在具體實作的最后一行即可,另外,Java還可以根據背景關系自動推斷出Lambda運算式中的引數型別,因此上面的代碼也可以簡化成如下寫法:
MyListener listener = (a ,b) -> {
String result = a + b;
return result;
}
Java將會自動推斷出引數a是String型別,引數b是int型別,從而使得我們的代碼變得更加精簡了,接下來舉個具體的例子,比如說現在有一個方法是接收MyListener引數的,如下所示:
public void hello (MyListener listener) {
String a = "Hello Lambda";
int b = 1024;
Stirng 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當中有哪些常用的功能是可以使用Lambda運算式進行替換的,
其實只要是符合介面中只有一個待實作方法這個規則的功能,都是可以使用Lambda運算式來撰寫的,除了剛才舉例說明的開啟子執行緒之外,還有像設定點擊事件之類的功能也是非常適合使用Lambda運算式的,
傳統情況下,我們給一個按鈕設定點擊事件需要這樣寫:
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 處理點擊事件
}
});
而使用Lambda運算式之后,就可以將代碼簡化成這個樣子了:
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener((v) -> {
// 處理點擊事件
});
另外,當介面的待實作方法有且只有一個引數的時候,我們還可以進一步簡化,將引數外面的括號去掉,如下所示:
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(v -> {
// 處理點擊事件
});
當然,有些人可能并不喜歡Lambda運算式這種極簡主義的寫法,不管你喜歡與否,Java 8對于哪一種寫法都是完全支持的,至于到底要不要使用Lambda運算式其實全憑個人,
13.8 總結
這13章的內容不算很多,但卻已經把Android中絕大部分比較重要的知識點都覆寫到了,從搭建開發環境開始學起,后面逐步學習了四大組件、UI、碎片、資料存盤、多媒體、網路、定位服務、Material Design等內容,本章中又學習了如全域獲取Context、定制日志工具、除錯程式、多視窗模式編程、Lambda運算式等高級技巧,
個人學習筆記,針對本人在自學中遇到的問題,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/472413.html
標籤:Android
