Activity的生命週期和啓動模式

Activity的生命週期

生命週期和啓動模式以及IntentFilter的匹配規則分析。

Activity的生命週期分爲兩個部分:

  1. 典型情況下的生命週期
  2. 異常情況下的生命週期

1.典型情況下的生命週期分析

  • onCreate :表示Activity正在被創建。在這裏可以做一些初始化的工作。
  • onRestart :表示Activity正在重新啓動。噹噹前Activity從不可見重新變成可見狀態。
  • onStart :表示Activity正在被啓動。已經可見,但不在前臺,無法交互。
  • onResume :表示Activity已經可見,並且出現在前臺可以交互。
  • onPause :表示Activity正在停止。在這裏可以做一些儲存數據,停止動畫等工作,但不能太耗時,因爲必須onPause執行完成之後新的Activity才能Resume
  • onStop :表示Activity即將停止。可以進行一些稍微重量級的回收工作,不能太耗時。
  • onDestroy :表示Activity即將被銷燬。可以進行一些回收工作和最終的資源釋放。

Activity的生命週期

注意:

  • onStart和onStop是從Activity是否可見這個角度來回調的
  • onResum和onPause是從Activity是否在前臺這個角度來回調的

2.異常情況下的生命週期分析

情況 1:資源相關的系統配置發生改變導致Activity被殺死並重新創建

比如說橫屏手機和豎屏手機會拿到兩張不同的圖片(設定了landscape或者portrait狀態下的圖片)。本來手機在豎屏狀態,突然旋轉屏幕,由於系統配置發生了變化,在默認情況下,Activity會被銷燬並且重新創建,當然我們也可以阻止系統重新創建我們的Activity。

當系統配置發生改變後,Activity會調用 onPause -> onStop -> onDestroy。

由於是異常情況終止,系統會在onStop之前調用onSaveInstanceState來保存當前Activity的狀態。(與onPause沒有時序關係)

Activity被系統重新創建後,系統會調用onRestoreInstanceState,把之前onSaveInstanceState方法所保存的Bundle對象作爲參數同時傳給onRestoreInstanceStateonCreate方法。(從時序來說,onRestoreInstanceState的調用時機在onStart之後)

異常情況下Activity的重建過程

而在視圖方面,當Activity在異常情況下需要重新創建時,系統會默認爲我們保存當前Activity的視圖結構,並且在Activity重啓後爲我們恢復這些數據。

其實每個View都有onSaveInstanceStateonRestoreInstanceState,關於保存和恢復View層級結構,系統的工作流程如下:

關於保存與恢復View層級結構

onSaveInstanceState方法,系統只會在Activity即將被銷燬並且有機會重新顯示的情況下才會去調用它。

情況 2:資源內存不足導致低優先級的Activity被殺死

其實這種情況的數據存儲與恢復過程與情況 1完全一致。

Activity的優先級情況:

  • 前臺的Activity —— 正在和用戶交互的Activity,優先級最高
  • 可見但非前臺的Activity —— 比如Activity中彈出了一個對話框,導致Activity可見但是位於後臺,無法和用戶進行直接交互
  • 後臺的Activity —— 已經被暫停的Activity,比如執行了onStop,優先級最低

當系統內存不足時,系統就會按照上述優先級去殺死目標Activity所在的進程,並在後續通過onSaveInstanceStateonRestoreInstanceState來存儲和恢復數據。而將後臺工作放入Service中是一個比較好的方法。

當系統配置改變後,Activity如何不被重新創建

由於系統配置中有很多內容,如果當某項內容發生改變後,不想系統重新創建Activity可以給Activity指定configChanges屬性

android:configChanges="orientation|keyboardHidden"
項目 含義
mcc SIM卡唯一標識IMSI(國際移動用戶識別碼)中的國家代碼,由3位數組成,中國爲460.此項 標識mcc代碼發生了改變
mnc SIM卡唯一標識IMSI(國際移動用戶識別碼)中的運營商代碼,由兩位數字組成,中國移動TD系統爲00,中國聯通爲01,中國電信爲03。此項標識mnc發生改變
locale 設備的本地位置發生了改變們一般指切換了系統語言
touchscreen 觸摸屏發生了改變,正常情況下無法發生,可以忽略它
keyboard 鍵盤類型發生了改變,比如用戶使用了外插鍵盤
keyboardHidden 鍵盤的可訪問性發生了改變,比如用戶調出了鍵盤
navigation 系統導航方式發生了改變,比如採用了軌跡球導航,很難發生,可以忽略
screenLayout 屏幕布局發生了改變,很可能是用戶激活了另一個顯示設備
fontScale 系統字體縮放比如發生了改變,比如用戶選擇了一個新字號
uiMode 用戶界面模式發生了改變,比如是否開啓了夜間模式(API8新添加)
orientation 屏幕方向發生了改變,這個是最常用的,比如旋轉了手機屏幕
screenSize 當屏幕的尺寸信息發生了改變,當旋轉設備屏幕時,屏幕尺寸會發生變化,這個選項比較特殊,它和編譯選項有關,當編譯選項中的minSdkVersion和targetSdkVersion 均低於13時,此選項不會導致Activity重啓,否則會導致Activity重啓(API13新添加)
smallestScreenSize 設備的物理屏幕尺寸發生了改變,這個項目和屏幕的方向沒有關係,僅僅表示在實際的物理屏幕的尺寸改變的時候發生,比如用戶切換到了外部的顯示設備,這個選項和screenSize一樣,當編譯選項中的minSdkVersion和targetSdkVersion均低於13時,此選項不會導致Activity重啓,否則會導致Activity重啓(API13新添加)
layoutDirection 當佈局方向發生變化,這個屬性用的比較少,正常情況下無須修改佈局的layoutDirection屬性(API17新添加)

如果我們沒有在ActivityconfigChanges屬性中指定該選項的話,當配置發生改變後就會導致Activity重新創建。

最常用的只有localeorientationkeyboardHidden

需要修改的代碼很簡單,只需要在AndroidMenifest.xml中加入Activity的聲明即可:

<activity
    android:name="com.dimon.MainActivity"
    android:configChanges="orientation|screenSize"
    android:label="@string/app_name">
    <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.d(TAG,"onConfigurationChanged,newOrientation:" + newConfig.orientation);
}

Activity沒有重新創建,並且沒有調用onSaveInstanceStateonRestoreInstanceState來存儲和恢復數據,而是系統調用了ActivityonConfigurationChanged方法,這個時候我們可以加入一些自己的特殊處理了。

Activity的啓動模式

Activity 的 LaunchMode

複習一點:啓動Activity時,系統會創造實例並把他們放入任務棧裏,而任務棧是一種“後進先出”的棧結構。

Activity的四種啓動模式:

  • standard:標準模式、默認模式。每次啓動一個Activity都會重新創建一個新的實例,不管這個實例是否已經存在。在這種模式下,某個Activity啓動了一號Activity,那麼一號Activity就運行在啓動它的那個Activity所在的棧中。

  • singleTop:棧頂複用模式。如果新的Activity已經位於任務棧的棧頂,那麼此Activity就不會被重新創建,同時它的onNewIntent方法會被回調,並且可以根據此方法的參數獲得當前請求的信息。

  • singleTask:棧內複用模式。在這種單實例模式下,只要Activity在一個棧中存在,那麼多次啓動此Activity都不會重新創建實例,系統也會調用其onNewIntent

singleTask 的工作流程

  • singleInstance:單實例模式。這是一種加強的singleTask模式,除了具有singleTask模式的所有特性外,還加強了一點,那就是具體此種模式的Activity只能單獨地位於一個任務棧中。

注:在任何跳轉的時候,首先調用本ActivityonPause,然後跳轉。如果被跳轉的activity由於啓動方式而沒創建新的實例,則會先調用onNewIntent,然後按照正常的生命週期調用。

如:

  1. A→B,A:onPause;B:onCreate,onStart,onResume。
  2. A(singleTop)→A,A:onPause;A:onSaveInstanceState;A:onResume。

一些具體問題與情況

  • 1:首先要說明:任務棧分爲前臺任務棧和後臺任務棧,後臺任務棧中的Activity位於暫停狀態。

當在前臺任務棧AB啓動Activity D,而D正好是後臺任務棧CD的棧頂。現在假設後臺任務棧裏的Activity的啓動模式是singleTask,請求啓動D,那麼整個後臺任務棧都會被切換到前臺,直接佔據前臺任務棧的棧頂,即ABCD。如果之前請求啓動的是C,那麼就會變成ABC。

singleTask模式的Activity切換到棧頂會導致在它之上的棧內的Activity出棧。

  • 2:TaskAffinity:任務相關性。標識一個Activity所需要的任務棧的名字。
    adnroid:taskAffinity="com.dimon.task1"

默認情況下Activity所需要的任務棧的名字爲應用的包名。

TaskAddinity

如何給Activity指定啓動模式呢?

第一種方法:通過AndroidMenifestActivity指定啓動模式。

<activity
    android:name="com.dimon.SecondActivity"
    android:configChanges="screenLayout"
    adnroid:taskAffinity="com.dimon.task1"
    android:launchMode="singleTask"
    android:label="@string/app_name"/>
第二種方法:通過在Intent中設置標誌位來爲Activity指定啓動模式。
Intent intent = new Intent();
intent.setClass(MainActivity.this, SecondActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

區別

  • 優先級:第二種方法比第一種優先級高,兩種都存在時,以第二種爲準。
  • 限定範圍:第一種無法設置FLAG_ACTIVITY_NEW_TASK標識,而第二種無法指定singleTask模式。

Acticity 中的一些比較常用的 Flags

  • FLAG_ACTIVITY_NEW_TASK

這個標記位的作用是爲Activity指定“singleTask”啓動模式,其效果和在XML中指定該啓動模式相同。

  • FLAG_ACTIVITY_SINGLE_TOP

這個標記位的作用是爲Activity指定“singleTop”啓動模式,其效果和在XML中指定該啓動模式相同。

  • FLAG_ACTIVITY_CLEAR_TOP

具有此標記位的Activity,當它啓動時,在同一個任務棧中所有位於他上面的Activity都要出棧。singleTask默認就具有這個標記位的效果。如果被啓動的Activity採用了standard啓動模式,那麼它連通它之上的Activity都要出棧,系統會創建新的Activity實例並放入棧頂。

  • FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS

具有這個標記的Activity不會出現在歷史Activity的列表中,當某種情況下我們不希望用戶通過歷史列表回到我們的Activity的時候這個標記比較有用。它等同於在XML中指定Activity的屬性android:excludeFromRecents="true"

IntentFilter的匹配規則

action的匹配規則

action的匹配要求Intent中的action存在且必須和過濾規則中的一個action相同。區分大小寫。

categoryd的匹配規則

如果Intent中含有category,那麼所以的category都必須和過濾規則中的其中一個category相同。

如果Intent中不含有category,那麼也能匹配,因爲系統在調用startActivity或者startActivityForResult的時候會默認爲Intent加上“android.intent.category.DEFAULT”這個category,所以爲了我們的Activity能夠接收隱式調用,就必須在intent-filter中指定“android.intent.category.DEFAULT”。

data的匹配規則

規則與action相似。

<intent-filter>
  <data android:mimeType="image/*"/>
  ...
</intent-filter>

以上規則說明Intent中的mimeType屬性必須爲“image/*”才能匹配。雖然沒有定義URI,但是還是得有默認值,URI的默認值爲contentfile。也就是說沒有URI也得在Intent中的URI部分的schema必須爲content或者file才能匹配。

intent.setDataAndType(uri.parse("file://abc"),"image/png");

如果要爲Intent指定完整的data,必須調用setDataAndType方法,不能先調用setData再調用setType。因爲這兩個方法會彼此清除對方的值。源碼如下:

public Intent setData(Uri data){
  mData = data;
  mType = null;
  return this;
}

data的結構略微複雜,語法如下:

<data android:scheme="string"
  android:host="string"
  android:port="string"
  android:path="string"
  android:pathPattern="string"
  android:pathPrefix="string"
  android:mimeType="string"/>

data由兩部分組成,mimeTypeURI

mimeType指媒體類型,例如image/jpeg、audio/mpeg4-generic和video/*等。

URI的結構如下:

<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]
  • Scheme:URI的模式,比如http、file、content等。
  • Host:URI的主機名,比如www.baidu.com
  • Port:URI的端口號。
  • Path、pathPattern和pathPrefix:這三個表示路徑信息,path完整路徑信息;pathPattern也表示完整的路徑信息,但是它裏面可以包括通配符“*”,注意正則表達式;pathPrefix表示路徑的前綴信息。

IntentFilter整合例子

<intent-filter>
  <action android:name="com.dimon.1"/>
  <action android:name="com.dimon.2"/>
  <category android:name="com.dimon.1a"/>
  <category android:name="com.dimon.2b"/>
  <category android:name="android.intent.category.DEFAULF"/>\
  <data android:mimeType="text/plain"/>
</intent-filter>
Intent intent = new Intent("com.dimon.1");
intent.addCategory("com.dimon.2b");
intent.setDataAndType(Uri.parse("file://abc"),"text/plain");
startActivity(intent)

Some Tips

使用隱式方式啓動Activity時,最好做一下判斷,看看是否有Activity能夠匹配我們的隱式Intent

方法有兩個:採用PackageManagerresolveActivity方法或者IntentresolveActivity方法,如果他們匹配不了就返回null

另外PackageManager還提供了queryIntentActivities方法:它不是返回最佳匹配的Activity信息而是返回所有成功匹配的Activity信息。

public abstract List<ResolveInfo> queryIntentActivities(Intent intent, int flags);
public abstract ResolveInfo resolveActivity(Intent intent, int flags);

第二個參數我們使用MATCH_DEFAULT_ONLY這個標記位,這個標記位僅僅匹配那些在intent-filter中聲明瞭<category android:name="android.intent.category.DEFAULT"/>這個categoryActivity
只要上述兩個方法不返回null,那麼startActivity一定可以成功。因爲不含有DEFAULT這個categoryActivity是無法接收隱式Intent的。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章