Activity使用場景解讀

在之前的一篇文章《基于場景解讀Android四大組件》中談到Activity是Android提供給開發(fā)者的一個組件仆百,主要用于前臺界面的展示和交互金砍,為了讓開發(fā)者很方便的達到這個目的,Activity這個組件提供了兩大接口:生命周期和啟動模式,官方手冊上對于這兩塊描述的很詳細,但是我們在實際使用的過程中依然會遇到很多問題,所以今天我們結(jié)合實際使用場景來解讀Activity的這兩大接口瑞妇。

Activity的生命周期

activity生命周期流程圖

這張圖大家應(yīng)該很熟悉了,這里我按使用場景來把它們分成三組:

  • onCreate和onDestroy
  • onStart梭冠、onRestart和onStop
  • onResume和onPause

第一組使用頻率最高辕狰,為什么?因為它的使用場景是最多的控漠,在一個App里面我們會經(jīng)常需要打開(startActivity)和關(guān)閉(finish)一個頁面蔓倍。onCreate是Activity生命周期里面的第一步,當(dāng)我們進入到這一步時就表示一個Activity實例對象(從Java的角度看)已經(jīng)產(chǎn)生了盐捷,當(dāng)我們New了一個Java對象之后偶翅,首先要做的肯定是對其進行初始化了,那么onCreate就是Android提供給開發(fā)者用來對Activity實例對象中的成員做初始化的碉渡。Tips:Android為了方便對Activity組件的管理以及開發(fā)者使用聚谁,對Activity做了封裝,開發(fā)者不能直接new一個Activity對象(你也可以直接new滞诺,但是new出來的對象不會被Android管理垦巴,也就失去了界面的展示和交互的功能媳搪,跟普通Java對象無異).

在onCreate里面一般我們會做View的初始化操作,比如添加View(setContentView骤宣,addView等)和View中數(shù)據(jù)的填充(setText秦爆,setImage等),那么問題來了:為什么對View的初始化要放在這里憔披,可不可以放到其他地方(onStart或者onResume)等限?答案是肯定的,但是我們不建議這么做芬膝,因為放在onCreate里面可以保證初始化操作只做一次望门,而放到其他地方可能會調(diào)用多次,當(dāng)然你可以添加一個flag來標記是否已經(jīng)做過初始化锰霜,這樣就可以保證放在onStart或者onResume里面也只做一次初始化筹误,但是這樣不覺得多此一舉么?既然onCreate已經(jīng)幫我們實現(xiàn)了這么一個功能癣缅,為啥不用呢厨剪,當(dāng)然如果你有特殊需求,另當(dāng)別論友存。

還有就是在onCreate里面有個savedInstanceState參數(shù)祷膳,這個主要用于你的Activity在非正常情況下被銷毀前幫你自動保存的一些數(shù)據(jù),這些數(shù)據(jù)會在這個Activity被重新創(chuàng)建時用到屡立,因此Android將這個參數(shù)放在了onCreate里面直晨。注意,我這里說的是非正常情況銷毀Activity膨俐,這種場景比較多勇皇,比如系統(tǒng)內(nèi)存不夠用,系統(tǒng)語言改變焚刺,屏幕方向改變等儒士,如果你不清楚哪些是非正常情況,沒關(guān)系檩坚,只要清楚正常情況就行了着撩,其他的自然就都可以認為是非正常場景下的Activity銷毀行為。那么正常情況是什么匾委?用戶主動意愿想要銷毀Activity就是正常情況拖叙,這種場景很少,就兩種:調(diào)用finish和帶特殊啟動模式的startActivity方法赂乐。那么對于非正常情況下的onCreate我們在里面又該如何使用這個savedInstanceState薯鳍?那么就要搞清楚savedInstanceState會保存到哪些數(shù)據(jù),有兩種:系統(tǒng)幫你自動保存的和你自己保存的。系統(tǒng)只會保存它認為有必要保存的(比方說EditText里面的內(nèi)容挖滤,CheckBox的Check狀態(tài)崩溪,F(xiàn)ragment實例等),但是很多童鞋不知道Activity會自動保存其中的Fragment實例斩松,onCreate寫成這樣子:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        getSupportFragmentManager().beginTransaction()
                .add(R.id.container, FragmentBase.newInstance(this, "One", FragmentBase.class.getName()))
                .add(R.id.container, FragmentFour.newInstance(this, "Two", FragmentFour.class.getName()))
                .add(R.id.container, FragmentFive.newInstance(this, "Three", FragmentFive.class.getName()))
                .commit();
    }

這樣會有個問題伶唯,當(dāng)Activity在非正常情況下重啟時,由于系統(tǒng)已經(jīng)保存了FragmentBase惧盹,F(xiàn)ragmentFour和FragmentFive三個Fragment實例乳幸,而你又重新添加了三個Fragment實例,結(jié)果導(dǎo)致Activity中存在了6個Fragment實例钧椰。截圖如下:

activitymanager查看到的結(jié)果

這樣不僅浪費內(nèi)存資源還有可能會引發(fā)App行為異常粹断。所以在了解了savedInstanceState的作用后正確的寫法應(yīng)該是這樣的:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        if (savedInstanceState != null) {
            // 這里根據(jù)自己需要去從savedInstanceState中去數(shù)據(jù)
            Fragment fragment = getSupportFragmentManager().findFragmentByTag(FragmentBase.class.getName());
            if (fragment instanceof FragmentBase) {
                FragmentBase base = (FragmentBase) fragment;
                
            }
        } else {
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.container, FragmentBase.newInstance(this, "One", FragmentBase.class.getName()))
                    .add(R.id.container, FragmentFour.newInstance(this, "Two", FragmentFour.class.getName()))
                    .add(R.id.container, FragmentFive.newInstance(this, "Three", FragmentFive.class.getName()))
                    .commit();
        } 
    }

再說說onDestroy,執(zhí)行到這一步嫡霞,一般代表activity即將要被銷毀掉瓶埋,不管是正常情況還是非正常情況關(guān)閉activity。一般在這里面我們會做一些資源的釋放操作诊沪,以防止出現(xiàn)資源泄露或者依賴activity所引發(fā)的一些異常情況的發(fā)生养筒。這里我舉兩個例子來說下上面說的兩種情況:

  • 異步任務(wù)引發(fā)的資源泄露,比如handler或者thread娄徊。這種情況發(fā)生的原因主要是異步任務(wù)的生命周期與activity生命周期不同步造成的,以handler中的message為例:
Handler handler =  new Handler();
handler.postDelayed(new Runnable() {
    @Override
    public void run() {
        tvContent.setText("newContent");
    }
}, 2000);
handler.obtainMessage(1).sendToTarget();

不管是使用哪種形式來發(fā)送message盾戴,message都會直接或者間接引用到當(dāng)前所在的activity實例對象寄锐,如果在activity finish后,還有其相關(guān)的message在主線程的消息隊列中尖啡,就會導(dǎo)致該activity實例對象無法被GC回收橄仆,引起內(nèi)存泄露。所以一般我們需要在onDestroy階段將handler所持有的message對象從主線程的消息隊列中清除衅斩。示例如下:

@Override
protected void onDestroy() {
    super.onDestroy();
    if (handler != null) {
        handler.removeCallbacksAndMessages(null);
    }
}
  • 異步任務(wù)引發(fā)的App運行異常盆顾,這里以一個顯示Dialog的場景為例:
Handler handler =  new Handler();
handler.postDelayed(new Runnable() {
    @Override
    public void run() {
        new AlertDialog.Builder(MainActivity.this).setMessage("Show Dialog").show();
    }
}, 5000);

由于我們設(shè)置的是5秒后顯示一個dialog,當(dāng)activity在5秒內(nèi)被finish后可能會導(dǎo)致顯示dialog時App發(fā)生崩潰畏梆。

FATAL EXCEPTION: main
Process: xiaofei.com.fragmenttest, PID: 4645
android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@aebf1e6 is not valid; is your activity running?
    at android.view.ViewRootImpl.setView(ViewRootImpl.java:567)
    at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:310)
    at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:85)
    at android.app.Dialog.show(Dialog.java:319)
    at android.support.v7.app.AlertDialog$Builder.show(AlertDialog.java:955)
    at xiaofei.com.fragmenttest.Main2Activity$1.run(MainActivity.java:49)
    at android.os.Handler.handleCallback(Handler.java:739)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:148)
    at android.app.ActivityThread.main(ActivityThread.java:5417)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

由于我們在activity的onDestroy中會銷毀activity對應(yīng)的窗體資源您宪,所以在顯示Dialog的時候由于dialog找不到父窗體就發(fā)生異常了。關(guān)于onDestroy中系統(tǒng)到底做了哪些資源清理的工作看下面的代碼就清楚了:

final void performDestroy() {
    mDestroyed = true;
    mWindow.destroy();
    mFragments.dispatchDestroy();
    onDestroy();
    mFragments.doLoaderDestroy();
    if (mVoiceInteractor != null) {
        mVoiceInteractor.detachActivity();
    }
}

所以在這種場景下正確的做法應(yīng)該是這樣的:

Handler handler =  new Handler();
handler.postDelayed(new Runnable() {
    @Override
    public void run() {
        if (!MainActivity.this.isDestroyed()) {
            new AlertDialog.Builder(MainActivity.this).setMessage("Show Dialog").show();
        }
    }
}, 5000);

isDestroyed方法要求最低api level是17奠涌,一般我們目前app支持的最低api level是14宪巨,所以你可以在activity中添加一個flag來標記當(dāng)前activity的狀態(tài)是否被destroyed,其實activity源碼里面也是這么干的溜畅,依葫蘆畫瓢就行捏卓。

/**
 * Returns true if the final {@link #onDestroy()} call has been made
 * on the Activity, so this instance is now dead.
 */
public boolean isDestroyed() {
    return mDestroyed;
}

onStart、onRestart和onStop

說實話這三個回調(diào)接口在實際使用場景中并不多慈格,對onStart怠晴、onRestart和onStop的使用可以從是否可見這點來找到它的正確使用姿勢遥金。這里舉幾個常用的場景:

  • 對數(shù)據(jù)的時效性要求較高。以新聞類App為例:Activity A代表新聞列表蒜田,點擊列表中的一個Item進入到Activity B新聞詳情稿械,在從B返回到A的時候為了保證用戶能看到最新的新聞,就需要從服務(wù)器拉取最新的新聞列表數(shù)據(jù)并填充到Activity A物邑,那么這個工作就可以放在onStart里面溜哮,當(dāng)然你也可以放在onRestart里面,但是activity首次加載啟動的時候不會調(diào)用onRestart色解,所以也就不會去拉取新聞列表數(shù)據(jù)茂嗓。
  • 需要顯示動畫效果。有些activity需要顯示一些動畫來幫助提升用戶體驗科阎,但是當(dāng)我們從該頁面進入到一個新頁面時述吸,由于該頁面已經(jīng)不可見了,所以就可以把當(dāng)前頁面中的動畫給關(guān)掉以節(jié)省系統(tǒng)資源锣笨,而這個工作就可以放在onStop中進行蝌矛。

onResume和onPause

這兩個接口使用的頻率比上一組要高,對onResume和onPause的使用可以從可以從是否獲得焦點(焦點即代表是是否可交互)這點來找到它的正確使用姿勢错英,這里也舉幾個常見場景:

  • 結(jié)束占用CPU的動畫或者其他正在運行任務(wù)入撒,這種在使用地圖SDK的時候比較常見:
@Override  
protected void onPause(){  
        mMapView.onPause();
        super.onPause();  
}  
@Override  
protected void onResume(){  
        mMapView.onResume();
       super.onResume();  
}  
  • 視頻播放,當(dāng)視圖組件獲得焦點時椭岩,即onResume中播放視頻茅逮,當(dāng)視圖組件失去焦點時,即onPause中暫停播放視頻判哥。
  • 保存重要數(shù)據(jù)献雅,為了防止App被意外強殺,一般會在onPause中將一些重要數(shù)據(jù)保存到本地塌计。

其實一般情況下他們和onStart挺身、onRestart和onStop這一組里面做的事情可以是一樣的,也就是說放在onResume中執(zhí)行的任務(wù)也可以放在onStop中去做锌仅,放在onPause中執(zhí)行的任務(wù)也可以放到onStop中去做章钾。不過他們之間還是有區(qū)別的:可見性和可交互性。我們需要根據(jù)具體的需求來分析哪些任務(wù)可以在視圖可見或者不可見的時候做热芹,哪些任務(wù)可以在焦點獲得或者失去的時候去做伍玖。

Activity四種啟動模式

個人認為Activity四種啟動模式的設(shè)定出于兩種目的:

  • 復(fù)用機制,節(jié)省系統(tǒng)資源剿吻,這種情況主要發(fā)生在除Standard模式外的三種模式上窍箍,通過他們的名字前綴Single也可以看出,跟Java中的單例模式有類似的思想,避免太多的實例對象創(chuàng)建開銷椰棘。
  • 根據(jù)用戶的交互行為定義纺棺,因為Activity最終的目的還是完成用戶在各種交互場景下的需求。

下面結(jié)合具體使用場景來細說每種啟動模式:

Standard

這種啟動模式最常見邪狞,也是Activity的默認啟動模式祷蝌,每當(dāng)我們需要開啟一個新的Activity頁面時系統(tǒng)都會新建一個Activity實例對象,然后開啟上面說的Activity的生命周期流程之旅帆卓,onCreate->onStart->onResume巨朦。

SingleTop

設(shè)置該模式后,當(dāng)通過startActivity啟動的Activity與當(dāng)前Task 棧中最頂部的Activity一樣時剑令,系統(tǒng)不會重新創(chuàng)建一個Activity實例糊啡,而是進入一個特殊的方法onNewIntent,具體流程為:onNewIntent->onResume吁津。這種場景還是比較多的棚蓄,比方說商品詳情頁面一般都會有相關(guān)的商品推薦,點擊推薦的某個商品后進入的還是一個商品詳情頁面碍脏,這個時候就不需要重新再創(chuàng)建一個新的商品詳情Activity頁面梭依,直接復(fù)用已有的頁面,刷新下View中的數(shù)據(jù)就好了典尾。

SingleTask

設(shè)置該模式可以保證當(dāng)前Task棧中每種Activity只會有一個實例存在役拴,當(dāng)通過startActivity啟動Activity A時,如果當(dāng)前Task棧中已經(jīng)存在一個Activity A的實例钾埂,那么不再重新創(chuàng)建一個新的Activity實例河闰,而是直接復(fù)用該實例,進入該Activity的onNewIntent方法勃教,同時將位于Activity A實例之上的所有Activity彈出Task棧并銷毀淤击。這種場景在IM應(yīng)用中使用的比較多匠抗,比如QQ或者微信的聊天頁面故源,當(dāng)從聊天頁面進入其他頁面,然后在重新進入聊天頁面時就會直接進入原來的聊天頁面汞贸,同時銷毀中間新創(chuàng)建的Activity頁面绳军,并刷新聊天頁面的數(shù)據(jù)。

SingleInstance

設(shè)置該模式可以保證該Activity所在的Task中有且僅有一個activity實例矢腻,當(dāng)通過startActivity啟動Activity A時门驾,如果該Activity的實例已經(jīng)存在,那么不再重新創(chuàng)建一個新的Activity實例多柑,而是直接復(fù)用該實例奶是,進入該Activity的onNewIntent方法。這種場景出現(xiàn)的比較少,該Activity在整個系統(tǒng)只有一個實例聂沙,一般用于系統(tǒng)應(yīng)用秆麸,并且可以被其他應(yīng)用共享使用(有點類似于操作系統(tǒng)概念中的臨界資源),比方說來電呼叫頁面及汉,在整個系統(tǒng)中就只能有一個沮趣,因為同一時刻只能存在一個電話呼叫。

可以發(fā)現(xiàn)后面三種模式的原理其實跟一些App中頁面的交互流程比較類似坷随,可能Android也是考慮到為了更方便實現(xiàn)這種交互方式而定義了這三種特殊的啟動模式房铭。其實不管是哪種啟動方式最終都會影響到Activity的生命周期流程,因此我們在啟動一個Activity頁面的時候需要留意其對該activity生命周期回調(diào)的影響温眉,并做相應(yīng)的處理邏輯缸匪。此外這四種啟動方式一般都會在AndroidManifest中寫死,同時也可以根據(jù)需要在代碼中動態(tài)配置芍殖,代碼中可使用的啟動標志一般有以下三個:

intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);

一般將Intent.FLAG_ACTIVITY_NEW_TASK和Intent.FLAG_ACTIVITY_CLEAR_TOP搭配使用實現(xiàn)類似SingleTask的效果豪嗽,將Intent.FLAG_ACTIVITY_NEW_TASK和Intent.FLAG_ACTIVITY_SINGLE_TOP搭配使用實現(xiàn)類似SingleTop的效果。


到此豌骏,關(guān)于Activity的兩大主題:生命周期和啟動模式就基本講完了龟梦。當(dāng)然Activity作為與用戶交互的入口,所包含的內(nèi)容還遠不止這些窃躲,比如與Activity關(guān)聯(lián)的Window计贰,View,還有Dialog等等蒂窒。如何正確理解他們與Activity的關(guān)系躁倒,正確的使用他們并與Activity配合來完成用戶的交互,后面再單獨分析洒琢。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末秧秉,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子衰抑,更是在濱河造成了極大的恐慌象迎,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件呛踊,死亡現(xiàn)場離奇詭異砾淌,居然都是意外死亡,警方通過查閱死者的電腦和手機谭网,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進店門汪厨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人愉择,你說我怎么就攤上這事劫乱≈校” “怎么了?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵衷戈,是天一觀的道長抠璃。 經(jīng)常有香客問我,道長脱惰,這世上最難降的妖魔是什么搏嗡? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮拉一,結(jié)果婚禮上采盒,老公的妹妹穿的比我還像新娘。我一直安慰自己蔚润,他們只是感情好磅氨,可當(dāng)我...
    茶點故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著嫡纠,像睡著了一般烦租。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上除盏,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天叉橱,我揣著相機與錄音,去河邊找鬼者蠕。 笑死窃祝,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的踱侣。 我是一名探鬼主播粪小,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼抡句!你這毒婦竟也來了探膊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤待榔,失蹤者是張志新(化名)和其女友劉穎逞壁,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體究抓,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡猾担,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年袭灯,在試婚紗的時候發(fā)現(xiàn)自己被綠了刺下。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡稽荧,死狀恐怖橘茉,靈堂內(nèi)的尸體忽然破棺而出工腋,到底是詐尸還是另有隱情,我是刑警寧澤畅卓,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布擅腰,位于F島的核電站,受9級特大地震影響翁潘,放射性物質(zhì)發(fā)生泄漏趁冈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一拜马、第九天 我趴在偏房一處隱蔽的房頂上張望渗勘。 院中可真熱鬧,春花似錦俩莽、人聲如沸旺坠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽取刃。三九已至,卻和暖如春出刷,著一層夾襖步出監(jiān)牢的瞬間璧疗,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工馁龟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留病毡,地道東北人。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓屁柏,卻偏偏與公主長得像啦膜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子淌喻,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內(nèi)容