一本android開發程序員必讀的一本書,感謝任玉剛大佬的分享
一、Activity的生命周期全面分析
-
典型情況下的生命周期
所謂典型情況下的生命周期,是指在有用戶參與的情況下,Activity所經過的生命周期的改變 -
異常情況下的生命周期
異常情況下的生命周期是指Activity被系統回收或者由于當前設備的Configutation發生改變從而導致Activity被銷毀重建
1.1.1 典型情況下的生命周期分析
在正常情況下,Activity會經歷如下生命周期。
- onCreate: 表示Activity正在被創建,這是生命周期的第一個方法。在這個方法中,我們可以做一些初始化工作,比如調用setContentView去加載界面布局資源、初始化Activity所需數據等
- onRestart: 表示Activity正在重新啟動。一般情況下,當當前Activity從不可見重新變為可見狀態時,onRestart就會被調用。這種情形一般都是用戶行為所導致的,比如用戶按Home鍵切換到桌面或者用戶打開了一個新的Activity,這時當前的Activity就會暫停,也就是onPause和onStop被執行了,接著用戶又回到了這個Activity,就會出現這種情況。
- onStart: 表示Activity正在被啟動。即將開始,這時Activity已經可見了,但是還沒有出現在前臺,還無法和用戶交互。這個時候可以理解為Activity已經顯示出來了,但是我們還看不到。
- onResume: 表示Activity已經可見了,并且出現在前臺并開始活動。要注意這個和onstart的對比,onStart和onResume都表示Activity已經可見,但是onStart的時候Activity還在后臺,onResume的時候Activity才顯示到前臺。
- onPause:表示Activity正在停止,此時可以做一些存儲數據、停止動畫等工作,但是注意不能太耗時,因為這回影響到新Activity的顯示,onPause必須先執行玩,新Activity的onResume才會執行。
- onStop: 表示Activity即將停止,可以做一些稍微重量級的回收工作,同樣不能太耗時。
- onDestroy: 表示Activity即將被銷毀,這是Activity生命周期中的最后一個回調,在這里,我們可以做一些回收工作和最終的資源釋放。
(1)針對一個特定的Activity,第一次啟動,回調onCreate > onStart > onResume。
(2)當用戶打開新Activity或者切換桌面的時候,回調如下: onPause> onStop。
(3)當用戶再次回到原Activity時,回調如下: onRestart > onStart > onResume 。
(4)當用戶按back鍵回退時,回調如下:onPuse > onStop > onDestroy。
(5)當Activity被系統回收后再次打開,生命周期方法回調過程和(1)一樣,注意只是生命周期方法一樣,不代表所有過程都一樣。
(6)對整個生命周期來說,onCreate和onDestroy是配對的,分別標識著Activity的創建和銷毀,并且只可能有一次調用。從Activity是否可見來說,onStart和onStop是配對的,onResume和onPause是配對的。這兩個方法可被多次調用。
- 當打開一個新的Activity時,舊的Activity會先調用onPause后,新的Activity才能調用onCreate,Android官方文檔對onPause是這樣說的:
Called when the system is about to start resuming a previous activity. This is typically used to commit unsaved changes to persistent data, stop animations and other things that may be consuming CPU, etc. Implementations of this method must be very quick because the next activity will not be resumed until this method returns.
Followed by either onResume() if the activity returns back to the front, or onStop() if it becomes invisible to the user.
.....此方法的實現必須非常快,因為在該方法返回之前,將不會恢復下一個活動。....
1.1.2 異常情況下的生命周期分析
- 情況1:資源相關的系統配置發生改變導致Activity被殺死并重新創建(例:手機橫豎屏切換)
當系統配置發生改變時,Activity 會被銷毀,其onPause、onStop、onDestory均會被調用,同時由于Activity 是異常情況下種植的,系統會調用onSaveInstanceStace來保存當前Activity狀態,這個方法調用在onStop之前,它和onPause調用沒有既定的先后順序。當Activity被重新創建,系統會調用onRestoreInstanceState,并且把Activity銷毀時onSaveInstanceStace保存的Bundle對象作為參數傳遞給onRestoreInstanceState和onCreate方法。
-
情況 2:資源內存不足導致低優先級的Activity被殺死
Activity的優先級從高到低可以分為三種:
(1)前臺Activity——正在和用戶交互的Activity,優先級最高。
(2)可見但非前臺Activity——比如Activity中彈出一個對話框,導致Activity可見但是位于后臺無法和用戶直接交互。
(3)后臺Activity已經被暫停的Activity,比如執行了onStop,優先級最低。
當系統內存不足時,系統會按照上述優先級去殺死目標Activity所在的進程。并通過onSaveInstanceStace和onRestoreInstanceState儲存和回復數據。如果一個進程中沒有四大組件在執行,那么這個進程將很快被系統殺死,因此一些后臺工作放入Service中從而保證進程有一定的優先級比較好,這樣就不會被輕易的殺死。
1.1.3 configChanges
針對橫豎屏發生Activity會發生重建的情況,我們可以給configChanges設置orientation這個值來避免。
android:configChanges="orientation"
configChanges的項目和含義:
項目 | 含義 |
---|---|
mcc | SIM卡唯一標識IMSI(國際移動用戶識別碼)中的國家代碼,由三位數字組成,中國為460.此項標識mcc代碼發生了改變。 |
mnc | SIM卡唯一標識IMSI(國際移動用戶識別碼)中運營上代碼,由兩位數字組成,中國移動TD系統為00,中國聯通為01,中國電信為03.此項標識mnc發生改變。 |
locale | 設備的本地位置發生了改變,一般指切換了語言。 |
touchscreen | 觸摸屏發生了改變,這個很費解,正常情況下無法發生,可以忽略他。 |
keyboard | 鍵盤類型發生了改變,比如用戶使用了外插鍵盤。 |
keyboardHidden | 鍵盤的可訪問性發生了改變,比如用戶調出了鍵盤。 |
navigaion | 系統導航方式發生了改變,比如采用了軌跡球導航,這個有點費解,正常情況下無法發生,可以忽略他。 |
screenLayout | 屏幕布局發生了改變,很可能是用戶激活了另外一個顯示設備。 |
fontScale | 系統字體縮放比例發生了改變,比如用戶選擇了一個新字號。 |
uiMode | 用戶界面模式發生了改變,比如是否開啟了夜間模式(API 8 新添加) |
orientation | 當屏幕的尺寸信息發生了改變,當旋轉設備屏幕時,屏幕尺寸會發生變化,這個選項比較特殊,它和編譯選項有關,當編譯選項中的minSdkVersion和targetSdkVersion均低于13時,此選項不會導致Activity重啟,否則會導致Activity重啟(API 13 新添加) |
smallestScreenSize | 設備的屋里屏幕尺寸發生改變,這個項目和屏幕方向沒有關系,僅僅表示在實際的物理屏幕的尺寸改變的時候發生,比如用戶切換到了外部的顯示設備,這個選項和screenSize一樣,當編譯選項中的minSdkVersion和targetSdkVersion均低于13時,此選項不會導致Activity重啟,否則會導致Activity重啟(API 13 新添加) |
layoutDirection | 當布局方向發生變化,這個屬性用的比較少,正常情況下無須修改布局的layoutDirection屬性(API 17 新添加) |
在代碼中:
...
<activity android:name=".MainActivity"
android:configChanges="orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
...
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
log("onConfigurationChanged: "+newConfig.orientation);
}
旋轉幾次得出結果為:
E/main: onCreate
E/main: onStart
E/main: onResume
E/main: onConfigurationChanged:2
E/main: onConfigurationChanged:1
E/main: onConfigurationChanged:2
由上面的日志可見,配置過后Activity旋轉屏幕將不會重新創建而是調用onConfigurationChanged方法。
1.2 Activity的啟動模式
1.2.1 Activity 的 LaunchMode
- standard:標準模式,系統默認模式,每次啟動一個 Activity 都會重新創建一個新的實例,不管是否已經存在,調用正常的Activity生命周期。
- singleTop:棧頂復用模式。顧名思義只有在棧頂的 Activity 會被復用,在這種情況下,如果新 Activity 已經位于任務棧的棧頂,那么此 Activity 不會被重新創建,同時它的 onNewIntent 方法會被調用,
1.當任務棧中存在 ABCD(Activity) ,此時啟動C,依舊會開啟一個新的CActivity,然后任務棧中 ABCDC 。
2.此時我們在啟動C,C就不會被創建,這時候會調用一次 onPause > onNewIntent > onResume。任務棧中任然會是 ABCDC。
- singleTask:棧內復用模式。這是一種單實例模式,在這種模式下,只要 Activity 在一個棧中存在,那么多次啟動此 Activity 都不會重新創建實例,重復創建同一個 Activity 會調用 onNewIntent 。
1.當任務棧中存在 ABCD (Activity),此時打開 D,會調用一次 onPause > onNewIntent > onResume。
任務棧中會是 ABCD,復用 D,不會創建新的 Activity 。
2.當任務棧中存在 ABCD (Activity),此時打開 A,A依舊是復用流程,但是B、C、D都會調用 onPause > onStop > onDestory 被銷毀掉。因為singleTask默認具有clearTop的效果,會導致 A 上面所有的 Activity 全部出棧。
-
singleInstance:單實例模式,這是一種加強的 singleTask 模式,它除了具有 singleTask模式的所有特性外,那就是具有此種模式的 Activity 智能單獨的位于一個任務棧中,
四個 singleInstance 模式的 Activity 切換
啟動7下 Activity ,退出的時候只要 4 下,說明所有 Activity 都被復用了,但是這是棧復用,總共四個棧,按先進后出順序。
GIF畫面切換的可能有點快,為了縮短時間。
如圖啟動模式,共11個步驟,singleInstance 模式下的 Activity 會單獨創建一個任務棧并且如果重復調用已經創建過的 singleInstance 模式的 Activity ,系統將會復用這個棧調用 onNewIntent,而不會重新創建,而其他模式則不會。
1.2.2 Activity 的 Flags
- FLAG_ACTIVITY_NEW_VIEW
這個標記位的作用是為 Activity 指定 “singleTask” 啟動模式,其效果和在 XML 中指定該啟動模式相同。
- FLAG_ACTIVITY_SINGLE_TOP
這個標記位的作用是為 Activity 指定 “singleTop” 啟動模式,其效果和在 XML 中指定該啟動模式相同。
- FLAG_ACTIVITY_CLEAR_TOP
具有此標記位的Activity,當他啟動時,在同一個任務棧中所有位于他上面的Activity都要出棧,這個模式一般需要和FLAG_ ACTIVITY_ NEW _ TASK配合使用,在這種情況下,被啟動的Activity的實例如果已經存在,那么系統就會調用它的onNewIntent,如果被啟動的Activity采用標準模式,那么他連同他之上的Activity都要出棧,系統會創建新的Activity實例并放入棧頂
- FLAG_ ACTIVITY_ EXCLUDE_ FROM _ RECENTS
具有此標記位的Activity,不會出現在歷史Activity的列表當中,當某種情況下我們不希望用戶通過歷史列表回到我們的Activity的時候就使用這個標記位了,他等同于在XML中指定Activity的屬性: android:excludeFromRecents="true"
1.3IntentFilter 的匹配規則
Activity 的啟動分顯示和隱式調用,顯示就是明確的指定被啟動對象的組件信息,包括包名和類名,隱式調用需要 Intent 能夠匹配目標組件設置的 IntentFilter 中的過濾信息,其中有 action 、category 、data。一個過濾列表中 action 、category、data 可以有多個,只有一個 Intent 能同時匹配一組 IntentFilter 中的 action 類別 、category 類別 、data 類別,才能啟動對應的 Activity 。另外一點,一個 Activity 可以有多個 intent-filter ,一個 Intent 只要能夠匹配任何一組 intent-filter 即可成功啟動對應的 Activity 。如下所示:
AndroidManifest:
<activity android:name=".ShareActivity">
<!-- This activity handlers "SEND" actions with text data-->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<!--This activity also handlers "SEND" and "SEND_MULTIPLE" with media data-->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/vnd.google.panorama360+jpg"/>
<data android:mimeType="image/*" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
執行代碼:
Intent intent = new Intent();
intent.setAction("android.intent.action.SEND");
intent.addCategory("android.intent.category.DEFAULT");
intent.setType("text/plain");
startActivity(intent);
- action 的匹配規則
action是一個字符串,系統預定了一些action,同時我們也可以在應用中定義自己的action,action的匹配規則是intent中的action必須能夠和過濾規則中的action匹配,這里說的匹配是指action的字符串值完全一樣,一個過濾規則中的可以有多個action,那么只要intent中的action能夠和過濾規則匹配成功,針對上面的過濾規則,需要注意的是,intent如果沒有指定action,那么匹配失敗,總結一下,action的匹配需求就是intent中的action存在且必和過濾規則一樣的action,這里需要注意的是他和category匹配規則的不同,另外,action區分大小寫,大小寫不同的字符串匹配也會失敗
- category
category是一個字符串,系統預定義了一些category,同時我們也可以在應用中定義自己的category。category的匹配規則和action不同,它要求Intent中如果含有category,那么所有的category都必須和過濾規則中的其中一個category相同。換句話說,Intent如果出現了category,不管有幾個category,對于每個category來說,它必須是過濾規則中已經定義的category。當然,Intent中可以沒有category,如果沒有category的話,按照上面的描述,這個Intent仍然可以匹配成功。這里要注意下它和action匹配過程的不同,action
是要求Intent中必須有一個action且必須能夠和過濾規則中的某個action相同,而category要求Intent可以沒有category,但是如果你一旦有category,不管有幾個,每個都要能和過濾規則中的任何一個category相同。為了匹配前面的過濾規則中的category,我們可出下面的Intent,intent.addcategory (“com.ryg.category.c”)或者Intent.addcategory (“com rcategory.d)亦或者不設category。為什么不設置category也可以匹配呢?原因是系統在調用startActivity或者startActivityForResult的時候會默認為Intent加上“android.intent.category.DEFAULT”這個category,所以這個category就可以匹配前面的過濾規則中的第三個category。同時,為了我們的activity能夠接收隱式調用,就必須在intent-filter中指定“android intent categor.DEFAULT”這個category,原因剛才已經說明了。
- data匹配規則
data的匹配規則和action有點類似,如果過濾規則中定義了data,那么intent中必須也要定義可匹配的data,在介紹data的匹配規則之前,我們需要來了解一下data的結構,因為data稍微有點復雜
data的語法如下所示:
<data
android:host="string"
android:mimeType="string"
android:path="string"
android:pathPattern="string"
android:pathPrefix="string"
android:port="string"
android:scheme="sstring" />
> data 由兩部分組成,mimeType 和 URI。mimeType 指定媒體類型,比如 image/jpeg 、audio/mpeg4-generic 和 video/* 等,可以表示圖片、文本、視頻等不同的媒體格式,而 URI 中包含的數據就比較多了,下面是 URI 的結構:
` <scheme>://<host>"<port>/[<path>|<pathPrefix>|<pathPattern>]`
這里再給幾個實際的例子就好理解了,如下所示。
content://com.example.project:200/folder/subfolder/etc
http://www.baidu.com:80/search/info
看了上面的兩個示例應該就瞬間明白了,沒錯,就是這么簡單。不過下面還是要介紹一下每個數據的含義。
- **Scheme**:URI 的模式,比如 http、file、content 等,如果 URI 中沒有指定 scheme,那么整個 URI 的其他參數無效,這頁意味著 URI 是無效的。
- **Host**:URI 的主機名,比如 www.baidu.com,如果 host 未指定,那么整個 URI 中的其他參數無效,這頁意味著 URI 是無效的。
- **Port**: URI 中的端口號,比如 80,僅當 URI 中指定了 scheme 和 host 參數的時候 port 參數才是有意義的。
- **Path、pathPattern 和 pathPrefix**:這三個參數表述路徑信息,其中 path 表示完整的路徑信息;pathPattern 也表示完整的路徑信息,但是它里面可以包含通配符 “ \* ” , “ \* ” 表示 0 個或多個任意字符,需要注意的是,由于正則表達式的規范,如果想表達真實的字符串,那么 “ * ” 要寫成 “ \\\\\* ”,“ \ ”要寫成“ \\\\\\\ ”,pathPrefix表示路徑的前綴信息。
> 介紹完data的數據格式后,我們要說一下data的匹配規則了。前面說到,data的匹配規則和action類似,它也要求Intent中必須含有data數據,并且data數據能夠完全匹配過濾規則中的某一個datn.這里的完全匹配是指過濾規則中出現的data部分也出現在了 Intent
中的data中。下面分情況說明。
(1)如下過濾規則:
1. ```
<intent-filter>
<data android:mimeType="image/*"/>
...
</intent-filter>
這種規則指定了媒體類型微所有類型的圖片,嘛呢 Intent 中的 mimeType 屬性必須為 “ image/* ” 才能匹配,這種情況下雖然過濾規則沒有指定 URI,但是 Intent 中的 URI 部分的 scheme 必須為 content 或者 file 才能匹配,這點是需要尤其注意的。為了匹配(1)中的規則,我們可以寫出如下示例。
intent.setDataAndType(Uri.parse("file://abc"), "image/png");
另外,如果要為 Intent 指定完整的 data,必須要用調用 setDataAndType 方法,不能調用 setDate 在調用 setType,因為這兩個方法會彼此清空對方的值,源碼:
3. ```
public Intent setData(Uri data) {
mData = data;
mType = null;
return this;
}
...
public Intent setType(String type) {
mData = null;
mType = type;
return this;
}
(2)如下過濾規則:
<intent-filter>
<data android:mimeType="video/mpeg" android:scheme="http" .../>
<data android:mimeType="audio/mpeg" android:scheme="http" .../>
</intent-filter>
??這種規則指定了兩組data規則,且每個data都指出了完整的屬性值,既有URI又有類型,為了匹配類型(2),我們可以寫出如下示例:
intent.setDataAndType(Uri.parse("http://abc"),"video/png");
//或者
intent.setDataAndType(Uri.parse("http://abc"),"audio/png");
??通過上面的實例,我們應該知道了data的匹配規則,關于data還有一些特殊的情況需要說明一下,這也是他和action不同的地方,如下兩種特殊的寫法,他們的作用是一樣的:
<intent-filter >
<data android:scheme="file" android:host="www.baidu.com"/>
...
</intent-filter>
<intent-filter >
<data android:scheme="file" />
<data android:host="www.baidu.com"/>
...
</intent-filter>```
??到這里我們已經把IntentFilter的過濾規則都講了一遍了,還記得本書前面給出的一個實例嗎?現在我們給出完整匹配它的intent:
Intent intent = new Intent();
intent.setAction("android.intent.action.SEND");
intent.addCategory("android.intent.category.DEFAULT");
intent.setDataAndType(Uri.parse("file//abc"),"text/plain");
startActivity(intent);
還記得URI中的scheme中的默認值嗎?如果把上面的intent.setDataAndType(Uri.parse(“file//abc”),”text/plain”);這句改成intent.setDataAndType(Uri.parse(“http//abc”),”text/plain”);打開的actiivty就會報錯,提示無法找到Activity,另外一點,intent-filter的匹配規則對于服務和廣播也是同樣的道理,不過系統對于 Service 的建議是盡量使用顯式意圖來啟動服務。
??最后,當我們通過隱式方式啟動一個Activity的時候,可以做一下判斷,看是否Activity能夠匹配我們的隱式 Intent,如果不做判斷就有可能出現上述的錯誤了。判斷方法有兩種:采用 PackageManager 的 resolveActivity 方法或者 Intent 的 resolveActivity 方法, 如果它們找不到匹配的Activity就會返回null,我們通過判斷返回值就可以規避上述錯誤了,另外,PackageManager 還提供了 queryIntentActivities 方法,這個方法和resolveActivity方法法不同的是:它不是返回最佳匹配的Activity信息而是返回所有成功匹配的Activity信息,我們看一下 queryIntentActivities 和 resolveActivity 的用法:
public abstract List<ResolveInfo>queryIntentActivities(Intent intent,int fladgs);
public abstract ResolveInfo resolveActivity(Intent intent,int flags);
PackageManager packageManager = getPackageManager();
List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(intent, 0);
int size = resolveInfos.size();
if (size > 0) {
startActivity(intent);
}
//或者
if (intent.resolveActivity(getPackageManager()) != null) {
startActivity(intent);
}
上述兩個方法的第一個參數比較好理解,第二個參數需要注意,我們要使用MATCH_ DEFAULT _ ONLY這個標記位,這個標記位的含義是僅僅匹配那些在intentfilter中聲明了 < category android-name=”android.intent.category DEFAULT”>這個category的 Activity。使用這個標記位的意義在于,只要上述兩個方法不返回null,那么startActivity一定可以成功,如果不用這個標記位,就可以把intent-filter 中 category不含DEFAULT的那些Activity給匹配出來,從而導致startActivity可能失敗。因為不含有DEFAULT這個category的Activity是無法接收隱式Intent的。在action和 category中,有一類action和category比較重要,他們是:
<action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" />
??這二者共同作用是用來標明這是一個入口Activity并且會出現在系統的應用列表中,少了任何一個都沒有實際意義,也無法出現在系統的應用列表中,也就是二者缺一不可,另外,針對 Service和BroadcastReceiver,PackageManager同樣提供了類似的方法去獲取成功匹配的組件信息。