Android開發藝術探索學習筆記(1)--生命周期與啟動模式


在網上搜安卓教程的時候聽到好幾個地方推薦這本書,說是比較適合入門之后的菜鳥們學習。那我就二話不說下了個PDF看了一下,發現確實有些地方介紹的挺不錯的,尤其是干貨比較多。因此可能會寫的比較長或者啰嗦,希望大家諒解。雖然這是一本android進階書,但我希望我寫的筆記能給更多的人看懂。特地也開了這本書的學習筆記連載,希望能對大家有所幫助。
(有些內容書上沒有講的很仔細,針對一些我自己想到的情況我會自己盡可能做一下實驗然后把結果分享給大家,所以不可避免有時候會有錯誤。歡迎大家指正~)。

第一章 Activity的生命周期和啟動模式

正如書中所說的Activity作為安卓的四大組件之首,其重要程度不言而喻。那么這一章的側重點介紹的是Activity在使用過程中的一些不容易搞清楚的概念,主要包括生命周期和啟動模式以及IntentFilter的匹配規則。

Activity的生命周期

典型情況下的生命周期

相信大家也都知道。一個Activity從開始到創建經歷的一般流程是:onCreate,onStart,onResume,onPause,onStop,onDestroy。下面我們就來細說一下其中究竟在每個環節都做什么什么樣的事情。

  • onCreate:相信寫過app的童鞋們都會發現里面總是做一些初始化工作。比如setContentView來加載布局資源,還有一些控件的綁定。在onCreate做這些能大概率的避免因沒有綁定組件而產生的空指針異常。
  • onStart:書中原話為:表示Activity已經可見了,但沒有出現在前台,還無法和用戶交互。可以理解為Activity已經顯示出來了,但我們還看不到。(讀到這里表示有點不懂,但是我們至少可以知道可見前台這是兩個不同的概念)。
  • onResume:Activity已可見了。(onStart的時候也已經可見了,但那時還在后台,onResume的時候才顯示到前台)。
  • onPause:表示Activity正在停止一般情況下會立馬調用onStop方法。如果這個時候快速回到當前Activity則會調用onResume方法。(???就是說我從A切換到B然后突然反悔啦?搞不懂)。作者也說上述情況比較極端,用戶很難模擬這種情況。在onPause里面可以放一些數據存儲,停止動畫等操作,但是不能很耗時。為什么呢?因為這個會影響下個Activity的產生。因為順序是當前的Activity的onPause執行完之后才能執行下一個Activity的onResume。(這個大家感興趣的可以自己去試一下,畢竟親身體驗過才更有印象)
  • onStop:表示Activity**即將停止**(有點文字游戲的趕腳。。)書中說是可以做一些稍微重量級的操作,但也不能很耗時。
  • onDestroy:表示Activity**即將被銷毀**,這里通常放一些資源回收。
  • onRestart:這個書中沒有寫,不過相信大家都知道。就是A跳轉到B,而且A沒有被銷毀掉的情況下,B跳轉到A,那么會執行A的onRestart而不是onCreate。

我自己實驗的時候發現兩個我自己之前還不知道的情況:
(1)假設從A跳轉到B,那么執行的順序一定是AonPause->BonCreate/BonRestart->BonStart->BonResume->AonStop。大家可以自己試試。無論在這五個環節的任意一個寫一個耗時操作(我用的是直接線程睡眠1s),順序都是不變的。
(2)在(1)的5個執行序列里面,除了在AonStop里面放耗時操作不會影響Activity跳轉的流暢性,其他四個都會。那么這個是不是可以說明:A到B的跳轉實質是從AonPause到BonResume執行完。可能大家會說onDestroy呢?因為onDestroy肯定是在onStop之后的,所以就不需要考慮了。

這里說明書中提到的幾點,可能大家都知道了,但我這里還是啰嗦一下:
1. 一個Activity的啟動是onCreate->onStart->onResume
2. 用戶打開新的Activity或者切換到桌面時原來的Activity會執行:onPause->onStop。如果用戶采用透明主題那不會回調onStop。
3. 用戶回到原Activity時,onRestart->onStart->onResume。
4. 用戶按back回退時,onPause->onStop->onDestroy
5. Activity被系統回收以后,再次打開和1一樣。只是生命周期一樣,並不是所有過程都一樣
6. 從生命周期來看,onCreate和onDestroy是一對,表示Activity的創建和銷毀並且只能被調用一次;onStart和onStop是一對,表示Activity是否可見,可以被調用多次;onResume和onPause是一對,表示Activity是否在前台,可以被調用多次。

作者提出的兩個問題:

  • onStart和onResume,onPause和onStop的不同點。
  • A->B,A的onPause和B的onResume哪個先執行。
    第一個問題我相信我在前面也寫的比較詳細了,第二問題的話根據我自己試驗的結果也能得出答案(A先執行onPause)。從源碼角度分析原因的話太復雜啦,我就不寫啦~(偷個懶,其實書上也就貼了一段代碼。大家只要記住這個結論就行了額)

異常情況下的生命周期

書中分析了兩種情況:
1. 資源相關的系統配置發生改變導致Activity被殺死並重新創建
這說的是什么意思呢?比如一個Activity允許旋轉屏幕的話,這個Activity就會被殺死並重新創建。在這種情況下,Activity會調用onSaveInstanceState來保存當前Activity的狀態(只有在異常終止的時候會自動回調)。這個方法調用在onStop之前,但是跟onPause沒有關系。在Activity重建的時候會調用onRestoreInstanceState來恢復Activity的狀態。(可以通過監聽onRestoreInstanceState來判斷Activity是否被重建了)onRestoreInstanceState在onStart之后被調用。
上述的狀態恢復技術,能夠為我們恢復View層次的信息,如輸入框里面的內容。具體流程為:Activity被終止時會調用onSaveInstanceState保存數據,然后Acitvity會委托Window去保存數據,接着Window再委托它的頂層容器來保存。這個頂層容器是個ViewGroup,一般情況是個DecorView,最后頂層容器再去一一通知它的子元素來保存相應的數據。(不知大家看明白沒,斜眼笑~。在我的理解上就是這樣子:一個官員得到消息說我們這里要地震,然后就上報消息給他的上級直到最后到皇上,然后皇上就下令讓手下的官員准備好一切工作,官員再告知他們手下的官員直到最后做實事的人。嘿嘿嘿不知道這樣子描述大家能不能聽懂~)
2. 資源內存不足導致低優先級的Activity被殺死
Activity按優先級從高到低可以分為:(1)前台Activity:正在和用戶交互的Activity,(2)可見但非前台Activity:比如彈出對話框以后,被對話框擋住的Activity,(3)后台Activity:A->B,那么A就是后台Activity。其實這個也挺好理解的,作為用戶的角度,用戶永遠只關心當前操作的界面,后台的(不執行下載任務的那種)誰管它死沒死,死了更好還省內存呢!!說到這里,有必要提一下那種在后台執行四大組件的Activity優先級並不是很低,如我剛剛說的在執行某個下載任務。所以當系統內存不足時會優先殺死優先級低的Activity。這個也是屬於異常情況殺死Activity的所以自然也可以通過onSaveInstanceState和onRestoreInstanceState來恢復數據。

如果要指定系統配置改變以后不重新創建Activity則需要在Manifiest里面配置一個叫做configChanges的屬性。如我們這個旋轉屏幕的例子,就只需要加一句android:configChanges=”orientation|screenSize”就可以阻止Activity重建。(沒有重建自然不會有恢復機制了)。

Activity的啟動模式

作為新手寫app的時候肯定沒有在意過Activity的啟動模式,因為標准的啟動模式完全足夠了,而且一般來說自己寫一些app玩玩的話,只用標准的啟動模式也不怎么會出問題。但有時候如果要開發一些項目,我們卻又不得不使用其他的啟動模式。

Activity的LaunchMode

首先介紹一下任務棧這個概念:所謂任務棧其實也就是Activity棧。既然是棧就會遵循先入后出的規則。而且系統不只有一個任務棧,當一個任務棧里面沒有東西的時候系統就會回收這個任務棧。

  1. standard:標准模式。也就是我們平時什么都不加的系統默認啟動模式。每次啟動一個Activity都會產生一個新的實例並壓入任務棧中。比如A->A->A->A->A那么這個任務棧里面就會有5個A實例,如果要用back鍵退出程序則需要按5下。再如:A->B(B是標准模式),則B會進入A的任務棧。
    這里我有一個小問題:作者說使用ApplicationContext去啟動standard模式的Activity會報錯,原因是standard模式啟動的Activity會默認進入啟動它的任務棧里面,如果用ApplicationContext則丟失所謂的任務棧。startActivity(new Intent(getApplicationContext(),FirstActivity.class));我使用這句話實驗的時候,但好像沒有出現錯誤。此處確實有點不解,如果大家知道為什么的話懇請指點一下小弟的迷津~
  2. singleTop:棧頂復用模式。如果要啟動的Activity已經位於棧頂了,則不會創建新的實例。
    如:A->A->A->A->A還是只有一個A實例。**這里要注意一點:**A->A以后並不會執行onCreate和onStart,而是執行onNewIntent(Intent),大家看到這個方法參數是Intent型,說明可以接受消息並處理。還有一點是如果要啟動的Activity不在棧頂,則仍然會新建一個Activity。singleTop也跟standard一樣會進入啟動它的Activity所在的棧里面。
    如果A->A:則生命周期為onPause->onNewIntent->onResume。
  3. singleTask:棧內復用模式。這個就是singleTop的進化版,只要棧里面有實例的則就直接把那個實例放到棧頂,所有本來在它上面的Activity直接從棧里移除。(所謂的clearTop性質)。和singleTop一樣,如果本來有實例,調用的也是onNewIntent。不同的是它在剛發出啟動的請求的時候,系統會先找是否存在我這個Activity期望呆的任務棧,沒有就新建一個任務棧然后新建一個Activity實例放進去。
    如果A->A:則生命周期為onPause->onNewIntent->onResume。
    如果A->B(不為singleInstance)->A:則B->A的生命周期為:BonPause->AonNewIntent->AonRestart->AonStart->AonResume->BonStop->BonDestroy

  4. singleInstance:單實例模式。這個是終極single。就是說他具有singleTask的一切特性,還包括一個:這個Activity只能位於一個獨立的任務棧。除非這個任務棧被銷毀了,否則再也不會創建這個Activity的新的實例。這個有什么用呢?我在另外一本《第一行代碼》中找到了實用例子:多個程序要共享一個Activity實例的時候就要使用singleInstance(我覺得原因是因為singleInstance確保自己的實例是放在一個單獨的返回棧里面的)。
    如果A->B->A:則B->A的生命周期為:BonPause->AonNewIntent->AonRestart->AonStart->AonResume->BonStop

總結:通過上面的描述以及我做的幾個實驗大致可以得出以下一些結論:如果一個Activity位於棧頂,並且是single家族的,那么啟動自己就不會調用onRestart,如果不位於棧頂,並且是singleTask則會執行onRestart;singleTask默認沒有獨立的任務棧,所以上面會執行BonDestroy(clearTop性質還記得嗎?);反之singleInstance則是位於獨立的任務棧。

這里作者還指出一種情況:如果AB在前台任務棧,CD在后台任務棧,並且CD為singleTask,此時啟動ActivityD的話會把后台的所有Activity移至前台任務棧。即前台任務棧為ABCD;但如果啟動ActivityC的話前台任務棧為ABC。(書中說原因在后面分析,但這個難道不是clearTop嗎???)

關於任務棧:這里我就把作者說的話寫一遍吧,感覺如果用自己說的話會有偏差。先說一個參數:TaskAffinity(任務相關性)。這個參數標識了一個Activity所需要的任務棧的名字,默認情況下所有任務棧的名字是包名,如果TaskAffinity指定為包名就相當於沒有指定。TaskAffinity屬性主要和singleTask啟動模式或者allowTaskParenting屬性配對使用,在其他情況下沒有意義。另外,任務棧分為前台任務棧和后台任務棧,后台任務棧中的Activity位於暫停狀態,用戶可以通過切換將后台任務棧調到前台。
當TaskAffinity和singleTask配對使用的時候:TaskAffinity 是 具有該模式的Activity的目前任務棧的名字,待啟動的Activity會運行在名字和TaskAffinity相同的任務棧。
當TaskAffinity和allowTaskParenting結合的時候:*allowTaskParenting為true時,假設有兩個應用 A B 。A啟動的屬於B的Activity C,C會運行在A的任務棧里面。此時按下home鍵回到桌面(C還在運行),按下B的啟動按鈕,會把C重新拉回到B的任務棧。因為是新啟動B,B的任務棧里面還是空的,而C已經被創建了,所以無論B的MainActivity是不是C都會顯示C。大家可以這么理解*:B原來養了一只狗C,但是B有事出門了。A作為B的鄰居想要把C叫到自己家里來看門。因為C也認識A,所以就過去了,但是呆的肯定沒有在B家里舒服。有一天B回家了,那么C看到以后直接屁顛屁顛跑回來了。
TaskAffinity在Manifest里面可以指定,如:android:taskAffinity=”com.example.test”
allowTaskParenting在Manifest里面可以指定,如:android:allowTaskParenting=”true”

那么如何指定啟動模式呢?

  1. 通過Menifest。如:android:launchMode=”singleTask”
  2. 通過Java代碼:如Intent intent = new Intent();
    intent.setClass(this,FirstActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    startActivity(intent);
    這兩種方式都可以指定但仍有區別

  3. 優先級第二種高於第一種;

  4. 兩種方式各有限定范圍:1不能指定clearTop標識,2不能指定singleInstance。

這里作者出了一道挺有意思的題目,我寫出來跟大家分享下,看看自己的答案是否與結果相同:A是standard,BC是singleTask而且BC的taskAffinity相同但與A不同,。打開應用進入A,執行如下界面跳轉A->B->C->A->B,此時按下back鍵會是什么界面,再按下back鍵呢?
答案:按下第一下的時候回到A界面,再次按下back鍵回到桌面。
原因:A為standard,所以A的任務棧是包名,A啟動B,B的任務棧與A不同,所以會新建一個任務棧壓入B,B啟動C,由於BC的任務棧相同,則把C壓入B的棧頂。C啟動A,由於A是standard所以會新建一個A的實例並把這個實例壓入C的上面。即目前的棧情況為:后台棧:A,前台棧:BCA。此時A啟動B,由於B是singleTask,所以由clearTop的性質,那么后台棧就只有B。按下返回鍵,B出棧,前台棧銷毀,此時調用后台棧放到前台,並顯示棧頂Activity也就是A,再按back,A出棧。所有任務棧銷毀,退出程序。

Activity的Flags

這里就直接把一些書中提及的標志位羅列出來:

  • FLAGS_ACTIVITY_NEW_TASK:指定“singleTask”啟動模式
  • FLAGS_ACTIVITY_SINGLE_TOP:指定“singleTop”啟動模式
  • FLAGS_ACTIVITY_CLEAR_TOP:指定clearTop屬性,一般與FLAGS_ACTIVITY_NEW_TASK配合使用。如果被啟動的Activity屬性是standard,會把之前的Activity和它之前的所有Activity都出棧,然后創建一個新的實例壓入棧中。比如ABC都是standard,A啟動B具有clearTop屬性則經過A->B>C>A>B之后,棧里面還剩下的是A->B只需要按兩下back鍵就會退出程序。再舉個復雜的例子A->A->A->A,前兩個A->A如果沒有指定clearTop,最后一個指定了clearTop,則執行完序列后棧中為AAA。這說明了clearTop屬性只會找出和自己一樣的最靠近棧頂的出棧。(可能我這描述比較拗口,大家看看我之前的分析自己理解一下。
  • FLAGS_ACTIVITY_EXCLUDE_FROM_RECENTS:具有這個標記的Activity不會出現在歷史Activity列表(recent列表)中。

IntentFilter的匹配規則

大家都知道啟動Activity分為顯示調用和隱式調用。其實就我而言基本不用隱式調用的,感覺太麻煩。(不知道多少人跟我一樣呢?)作者在這里主要介紹的就是隱式調用,剛好可以彌補一下自己對這方面知識的空缺。

隱式調用主要就是在Java代碼里面寫一個intent,然后里面的一長串參數和在Manifest里面定義的IntentFilter的信息匹配上就可以啟動Activity,我感覺比較類似於廣播。IntentFilter的過濾信息主要分為:actioncategorydata

  1. action:在 <intent-filter>里面通過 <action android:name="" />標簽調用。
    匹配規則:Intent里面必須要有(好像也只能指定一個,在構造intent的時候傳入一個,在后面調用intent.addAction()好像會把之前的action覆蓋掉),但是在Intent-filter里面可以定義多個action過濾器,Intent里面定義的那個只要和其中之一個過濾器對上就算匹配成功。action過濾區分大小寫。
  2. category:在 <intent-filter>里面通過 <category android:name="" />標簽調用。
    匹配規則:Intent里面可以沒有,但是如果有的話必須要與Intent-filter里面其中一個對應上。(我感覺好像和action匹配規則差不多,只是catagory可以不寫,但是action必須寫)此外還有一點比較重要:系統默認會給每一個Intent加上一個android.intent.category.DEFAULT的category(所以category可以不設置),所以我們在每個intent-filter里面必須加上<category android:name="android.intent.category.DEFAULT" />,原因已經說明了。

其實到了現在 使用action和category就已經可以使用隱式調用啟動Activity了。因為這兩個必須在intent-filter里面指明,action用來指明大方向,category必須要加一個DEFAULT+其他自己定義的過濾規則。我們在Intent里面只要適配intent-filter里面的過濾信息就可以隱式啟動Activity。

3.data: 如果你在intent-filter定義了data,那么你也必須要在intent里面定義data,而且要適配上。以上是我實驗得出的結論。data比較復雜,其結構分為:mimeType和URI。
(1)mimeType指的是媒體類型,如:image/jpeg、audio/mpeg4-generic、video/*等。
(2)URI學名叫做統一資源標識符(Uniform Resource Identifier):表示的意思類似於路徑。
URI的結構又可以這樣子表示:<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>],
content://com.example.project:200/folder/subfolder/etc;
http://www.baidu.com:80/search/info

  • scheme:URI模式,如:file,http,content等。如果沒有scheme,則整個URI失效。
  • host:主機名,如:www.baidu.com。如果沒有host,則整個URI失效。
  • port:端口號。如:80。只有指定scheme和host的時候port才是有意義的。
  • path,pathPattern,pathPrefix:表示路徑信息。path是完整路徑,pathPattern也是完整路徑但是里面可以有通配符*,*表示0個或者多個任意字符;pathPrefix表示路徑的前綴信息(我猜想應該是完整路徑減文件名吧)。
    data在intent-filter里面的字段表示:
 <data android:scheme="string"/>
<data android:host="string"/>
<data android:port="string "/>
<data android:path="string"/>
<data android:pathPattern="string"/>
<data android:pathPrefix="string"/>
<data android:mimeType="string"/>

匹配規則:和action差不多,即data可以有很多很多的mimeType,scheme等等屬性。只要Intent里面寫的有一個能匹配上就可以了。
這里注意一點:如果你在intent-filter中沒有指定URI,那么系統默認URI的scheme部分為content或者file,也就是說你在Intent里面指定scheme是http是不能匹配的。
還有幾點書上沒有提及的:如果intent是image/*,intent-filter是image/png可以成功,反之也可以。但如果intent是image/png,intent-filter是image/jpeg,則匹配失敗;在intent-filter里面指定scheme就只需scheme=“content”,而在intent里面就需要setData(Uri.parse(“content://”)),或者setData(Uri.parse(“content:/”)),或者setData(Uri.parse(“content:”))。
這里介紹3個方法:setData(Uri data),我們看參數就知道這個是來設置URI的(不要被它的名字迷惑),這個方法會把mimeType置為null;setType(String mimeType),設置mimeType,同樣也會把URI置null;所以在寫完整的data的時候要用setDataAndType()來設置。

最后作者介紹了幾種避免因為匹配不成功出異常的方法:
(我都親自試驗過,親測可用~)
1. intent.resolveActivity(packManager),如果匹配不到則會返回null,匹配到了則返回下一個Activity的ComponentName。
2. packManager.queryIntentActivities(intent, int flags);這個返回的是一個List<ResolveInfo>的數組。里面放的是所有匹配成功的Activity的信息。intent參數不用多說了,flags書上說我們都要用PackageManager.MATCH_DEFAULT_ONLY這個參數。簡單一想大家也能明白為什么:若要使一個intent-filter有用,則肯定有DEFAULT這個category。我們就找這個參數,找出來的肯定是滿足基本要求的。如果不用這個,雖然滿足其他category的要求,但是找出來的Intent-filter不能保證有DEFAULT這個參數(即無效的)。
3. packManager.resolveActivity(intent, int flags);據說返回的是最適配的Activity,但我自己試的時候如果有兩個Activity都能適配的話,他返回的值都不是這兩個Activity的信息,而是另外一個奇奇怪怪的東西。這個就很怪,求大神指導咯~~~

特殊的intent-filter

 <intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

這個大家肯定是不會陌生,這二者的共同作用是標明這是個入口Activity。其實還有許許多多其他的針對系統組件的intent-filter。我在最后TIPS那里貼幾個鏈接,大家感興趣的可以看看。

終於碼完了,我本來以為只要一點點就可以把第一章給寫完。但發現有很多東西如果直接把書抄進來你們會看不懂,而且書上講的也很模糊。我不放心就自己一項一項去試,畢竟我不確定的東西也不能很肯定的寫出來~~

TIPS

  1. decorView:http://blog.csdn.net/guxiao1201/article/details/41744107
    http://www.cnblogs.com/yogin/p/4061050.html
  2. configChanges:http://blog.csdn.net/zhaokaiqiang1992/article/details/19921703
  3. recent列表:就是手機上顯示最近使用過哪些應用的列表。一般都是在桌面下滑都會出來。或者說按手機的下面的正方形的按鈕,不會出現這個應用。
  4. intent-filter:http://blog.csdn.net/playboyanta123/article/details/7913679
    http://blog.csdn.net/aminfo/article/details/7623231#comments

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com