看我用Android開(kāi)發(fā)者聽(tīng)得懂的語(yǔ)言解釋快應(yīng)用頁(yè)面的生命周期和接口router-12.4
就像世界上***批Android工程師大多都是iOS工程師轉(zhuǎn)行一樣,世界上***批QuickApp工程師也大多都是Android工程師轉(zhuǎn)行。將快應(yīng)用知識(shí)與Android知識(shí)對(duì)比學(xué)習(xí)可以起到溫故知新的效果。
查閱快應(yīng)用官方文檔可知快應(yīng)用的“頁(yè)面”和Android原生的Activity都是提供一個(gè)可以給用戶來(lái)交互的屏幕,在底層也都是用Stack保存瀏覽記錄。理解頁(yè)面的生命周期就像理解Activity’的生命周期一樣,有助于更好的組織頁(yè)面的業(yè)務(wù)邏輯,方便頁(yè)面之間的交互與資源釋放等的處理。但為何“頁(yè)面”僅有三種狀態(tài)而Activity卻有四種呢?又為何“頁(yè)面”沒(méi)有類似Activity的啟動(dòng)模式呢?本文將為你揭曉答案:
頁(yè)面的生命周期和狀態(tài)
眾所眾知Activity的生命周期由七個(gè)主要被動(dòng)方法以及onBackPressed()、onNewIntent()、onActivityResult()、onSaveInstanceState()和onRestoreInstanceState()等其他被動(dòng)方法組成,并且有、、和共四種狀態(tài);而查閱官方文檔可知頁(yè)面的被動(dòng)方法僅有七個(gè),而狀態(tài)只有、和三種。我來(lái)給大家對(duì)比分析一下兩組方法的對(duì)應(yīng)關(guān)系。
onInit()和onReady()
根據(jù)快應(yīng)用官方文檔的說(shuō)法:onInit()方法表示ViewModel的數(shù)據(jù)已經(jīng)準(zhǔn)備好,可以開(kāi)始使用頁(yè)面中的數(shù)據(jù),能且僅能調(diào)用一次。onReady()方法表示ViewModel的模板已經(jīng)編譯完成,可以開(kāi)始獲取DOM節(jié)點(diǎn),能且僅能調(diào)用一次。
每個(gè)Android開(kāi)發(fā)者的類庫(kù)里都有一個(gè)BaseActivity,這個(gè)BaseActivity里一般都有初始化配置、綁定View的同步方法onInitViews()和請(qǐng)求數(shù)據(jù)的異步方法onInitData();我們可以把onInit()理解為onInitData(),把onReady()理解為onInitViews()。
如果把眼光放遠(yuǎn)一點(diǎn),拿頁(yè)面與Fragment比較,onInit()更像Fragment的onCreate(),而onReady()更像onCreateView()。
onShow()和onHide()
每個(gè)快應(yīng)用的App中可以同時(shí)運(yùn)行多個(gè)頁(yè)面,但是每次只能顯示其中一個(gè)頁(yè)面;這點(diǎn)不同于Android開(kāi)發(fā),可以同時(shí)顯示多個(gè)Activity;也不同與純前端開(kāi)發(fā),瀏覽器頁(yè)面中每次只能有一個(gè)頁(yè)面,當(dāng)前頁(yè)簽打開(kāi)另一個(gè)頁(yè)面,上個(gè)頁(yè)面就銷毀了。
根據(jù)快應(yīng)用官方文檔的說(shuō)法:頁(yè)面被切換隱藏時(shí)調(diào)用onHide(),頁(yè)面被切換重新顯示時(shí)調(diào)用onShow()。很明顯這與Activity有onStart()和onStop()、onResume()和onPause()兩對(duì)方法不同,這是因?yàn)轫?yè)面不像Activity有透明背景和Theme.Dialog主題,所以Activity的可見(jiàn)狀態(tài)和前臺(tái)狀態(tài)在頁(yè)面里僅對(duì)應(yīng)顯示狀態(tài)。
onDestroy()
根據(jù)快應(yīng)用官方文檔的說(shuō)法:onDestroy()方法在頁(yè)面被銷毀時(shí)調(diào)用,能且僅能調(diào)用一次。被銷毀的可能原因有:用戶從當(dāng)前頁(yè)面返回到上一頁(yè),或者用戶打開(kāi)了太多的頁(yè)面,框架自動(dòng)銷毀掉部分頁(yè)面,避免占用資源。而官方建議頁(yè)面進(jìn)入銷毀狀態(tài)時(shí)應(yīng)該做一些釋放資源的操作,這和Activity的onDestroy()方法的推薦使用方式不謀而合,所以頁(yè)面的onDestroy()方法就是Activity的onDestroy()方法。
onBackPress()
根據(jù)快應(yīng)用官方文檔的說(shuō)法:當(dāng)用戶點(diǎn)擊實(shí)體BACK按鍵或左上角返回菜單時(shí)觸發(fā)onBackPress()事件。我想沒(méi)有人不會(huì)把頁(yè)面的onBackPress()方法和Actvity的onBackPressed()方法聯(lián)系到一起。
如果事件響應(yīng)方法***返回true表示不返回,自己處理業(yè)務(wù)邏輯,完畢后開(kāi)發(fā)者自行調(diào)用router.back()方法返回。代碼如下:
onBackPress (params) { //做自己喜歡的事 return true }
對(duì)比一下Activity的onBackPressed()的override方式:
@Override public void onBackPressed() { // super.onBackPressed(); // 做自己喜歡的事 }
onMenuPress()
對(duì)比一下onBackPress()可知:當(dāng)用戶點(diǎn)擊右上角菜單時(shí)觸發(fā)onMenuPress()事件。如果我們有使用菜單的需求,可以通過(guò)manifest.json中的menu屬性配置是否顯示右上角的菜單。
所有支持快應(yīng)用的國(guó)產(chǎn)Android設(shè)備的MENU鍵都用來(lái)清理內(nèi)存,因此實(shí)體MENU鍵不會(huì)觸發(fā)onMenuPress(),這點(diǎn)與onBackPress()有所區(qū)別。
頁(yè)面路由接口router
根據(jù)快應(yīng)用官方文檔的說(shuō)法:我們可以通過(guò)配置a組件的href屬性跳轉(zhuǎn)到應(yīng)用內(nèi)的頁(yè)面,有點(diǎn)類似于Android開(kāi)發(fā)中已不被推薦使用的的隱式Intent跳轉(zhuǎn)Activity;此外我們也可以使用router接口,這就有點(diǎn)類似于Android開(kāi)發(fā)的顯式Intent組件或者ARouter框架。本文的一切頁(yè)面跳轉(zhuǎn)都使用router接口。
常見(jiàn)方法
接口router常見(jiàn)方法在官方文檔里寫(xiě)得很清楚,我只講幾點(diǎn)注意事項(xiàng):
(1)接口router的push()方法能跳轉(zhuǎn)應(yīng)用外的Activity包括電話、短信、郵件和其他快應(yīng)用
(2)接口router的push()方法不能實(shí)現(xiàn)Android的Intent的“android.intent.category.HOME”標(biāo)簽的功能,也就是說(shuō),除非用戶點(diǎn)HOME能回到桌面,否則開(kāi)發(fā)者不能靠重寫(xiě)onBackPress()保留首頁(yè)
(3)打開(kāi)照相機(jī)、QQ聊天、微信分享、支付寶付款用的不是router
(4)back()方法的路徑參數(shù)是path,并非push()和replace()的uri
回傳參數(shù)的方式
傳遞參數(shù)的方式不在本文的討論范圍之內(nèi),但回傳參數(shù)的方式卻涉及生命周期,我們先看快應(yīng)用回傳參數(shù)的官方代碼:
onHide () { // 頁(yè)面被切換隱藏時(shí),將要傳遞的數(shù)據(jù)對(duì)象寫(xiě)入全局變量 this.$app.$data.dataPageB = { gotoPage: 'pageA', params: { msg: this.msg } } },
對(duì)比一下Activity的setResult()方法的調(diào)用:
Intent intent = new Intent(); intent.putExtra("dataPageB",dataPageB); setResult(RESULT_OK,intent);
快應(yīng)用接收回傳參數(shù)的官方代碼:
onShow () { // 頁(yè)面被切換顯示時(shí),從數(shù)據(jù)中檢查是否有頁(yè)面B傳遞來(lái)的數(shù)據(jù) if (this.$app.$data.dataPageB && this.$app.$data.dataPageB.gotoPage === 'pageA') { // 從數(shù)據(jù)中獲取回傳給本頁(yè)面的數(shù)據(jù) const data = this.$app.$data.dataPageB.params this.msg = data.msg } },
對(duì)比一下Activity的onActivityResult()方法:
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == pageA && resultCode == RESULT_OK){ this.msg = ((BaseBean)data.getSerializableExtra("dataPageB")).getMessage(); } }
由此可見(jiàn),在onRscume()方法里檢驗(yàn)全局變量的變化這一行為,作為Android原生開(kāi)發(fā)中飽受詬病的新手行為,在快應(yīng)用開(kāi)發(fā)中是官方推薦的,所以快應(yīng)用不需要類似onActivityResult()方法的方法。
研究接口router和頁(yè)面生命周期關(guān)系的實(shí)踐
“紙上得來(lái)終覺(jué)淺”,我們寫(xiě)一個(gè)LifecycleDemo來(lái)研究接口router和頁(yè)面生命周期關(guān)系:
首先打開(kāi)這個(gè)LifecycleDemo,我們可以看到logcat打印出如下信息:
### 頁(yè)面A onInit ### ### 頁(yè)面A onReady ### ### 頁(yè)面A onShow ### 當(dāng)前頁(yè)面在頁(yè)面棧中的位置 : 1/1
點(diǎn)擊BACK鍵,返回桌面,logcat打印出如下信息:
### 頁(yè)面A onBackPress ### ### 頁(yè)面A onHide ### ### 頁(yè)面A onDestroy ###
與官方文檔描述相同,符合預(yù)期
打開(kāi)其他Activity
接下來(lái)我們打開(kāi)其他Activity,包括系統(tǒng)桌面、打電話界面和其他應(yīng)用
點(diǎn)擊HOME鍵,然后熱啟動(dòng)LifecycleDemo,Logcat打印如下:
### 頁(yè)面A onHide ### ### 頁(yè)面A onShow ### 當(dāng)前頁(yè)面在頁(yè)面棧中的位置 : 1/1
應(yīng)用內(nèi)打開(kāi)其他系統(tǒng)Activity,然后熱啟動(dòng)LifecycleDemo,Logcat打印如下:
### 頁(yè)面A onHide ### ### 頁(yè)面A onShow ### 當(dāng)前頁(yè)面在頁(yè)面棧中的位置 : 1/1
應(yīng)用內(nèi)打開(kāi)別的快應(yīng)用,然后熱啟動(dòng)LifecycleDemo,Logcat打印如下:
### 頁(yè)面A onHide ### ### 頁(yè)面A onShow ### 當(dāng)前頁(yè)面在頁(yè)面棧中的位置 : 1/1
結(jié)論:符合預(yù)期,支持上文onShow()相當(dāng)于onStart()和onResume(),onHide()相當(dāng)于onPause()和onStop()的猜想。
用push()方法進(jìn)行應(yīng)用內(nèi)頁(yè)面跳轉(zhuǎn)
用push()方法跳轉(zhuǎn)到頁(yè)面A,logcat打印如下:
### 頁(yè)面A onHide ### ### 頁(yè)面A onInit ### ### 頁(yè)面A onReady ### ### 頁(yè)面A onShow ### 當(dāng)前頁(yè)面在頁(yè)面棧中的位置 : 2/2
顯然頁(yè)面棧里的順序?yàn)锳A,支持上文頁(yè)面的啟動(dòng)模式相當(dāng)于Activity的Standard模式的猜想。現(xiàn)在猜想***個(gè)A是前面的,第2、3、4個(gè)A是后面的。我們接著用push()方法跳轉(zhuǎn)到頁(yè)面B,logcat打印如下:
### 頁(yè)面A onHide ### ### 頁(yè)面B onInit ### ### 頁(yè)面B onReady ### ### 頁(yè)面B onShow ### 當(dāng)前頁(yè)面在頁(yè)面棧中的位置 : 3/3
顯然頁(yè)面棧里的順序?yàn)锳AB,也符合預(yù)期,支持上文猜想。
我們發(fā)現(xiàn)快應(yīng)用官方文檔存在歧義,就是首頁(yè)究竟是指穩(wěn)定運(yùn)行時(shí)頁(yè)面棧底的頁(yè)面(類似Android原生開(kāi)發(fā)的MainActivity),還是指manifest.json文件中“router.entry”對(duì)應(yīng)的頁(yè)面(類似AndroidManifest.xml文件中帶“android.intent.action.MAIN"標(biāo)簽的Activity,通常被命名為SplashActivity),我們驗(yàn)證一下:
當(dāng)“router.entry”對(duì)應(yīng)頁(yè)面A,而頁(yè)面棧里頁(yè)面順序?yàn)锽BAACC的時(shí)候,我們用push()方法跳轉(zhuǎn)到首頁(yè)。首頁(yè)是這樣的:
而logcat打印如下:
### 頁(yè)面C onHide ### ### 頁(yè)面A onInit ### ### 頁(yè)面A onReady ### ### 頁(yè)面A onShow ### 當(dāng)前頁(yè)面在頁(yè)面棧中的位置 : 7/7
原來(lái)接口router可以跳轉(zhuǎn)的首頁(yè)指的是“router.entry”對(duì)應(yīng)的頁(yè)面。
用replace()方法進(jìn)行應(yīng)用內(nèi)頁(yè)面跳轉(zhuǎn)
當(dāng)頁(yè)面棧里僅有A的情況下,用replace()方法跳轉(zhuǎn)到頁(yè)面A,logcat打印如下:
### 頁(yè)面A onHide ### ### 頁(yè)面A onDestroy ### ### 頁(yè)面A onInit ### ### 頁(yè)面A onReady ### ### 頁(yè)面A onShow ### 當(dāng)前頁(yè)面在頁(yè)面棧中的位置 : 1/1
顯然頁(yè)面棧里僅有一個(gè)A,猜想replace()方法類似Activity里的這段代碼:
startActivity(intent); finish();
又猜想第1、2個(gè)A是前面的,第3、4、5個(gè)A是后面的。我們接著用replace()方法跳轉(zhuǎn)到頁(yè)面B,logcat打印如下:
### 頁(yè)面A onHide ### ### 頁(yè)面A onDestroy ### ### 頁(yè)面B onInit ### ### 頁(yè)面B onReady ### ### 頁(yè)面B onShow ### 當(dāng)前頁(yè)面在頁(yè)面棧中的位置 : 1/1
符合預(yù)期,支持上文猜想。
用back()方法進(jìn)行應(yīng)用內(nèi)頁(yè)面跳轉(zhuǎn)
在頁(yè)面棧里的順序?yàn)锳ABBCC的情況下,根據(jù)文檔僅能得出用back()方法返回上一頁(yè)后頁(yè)面棧里的順序?yàn)锳ABBC,返回頁(yè)面B后頁(yè)面棧里的順序?yàn)锳ABB,返回頁(yè)面A或首頁(yè)后頁(yè)面棧里的順序?yàn)锳A,有點(diǎn)類似Intent的FLAG_ACTIVITY_CLEAR_TASK標(biāo)簽。我們只討論官方文檔忽略的內(nèi)容:
我們用back()方法跳轉(zhuǎn)到頁(yè)面C,頁(yè)面無(wú)變化;在頁(yè)面棧里的順序?yàn)锳BCABC的情況下,我們用back()方法跳轉(zhuǎn)到頁(yè)面C,頁(yè)面也無(wú)變化。得出back()方法不能用來(lái)跳轉(zhuǎn)到棧頂頁(yè)面的結(jié)論。
總結(jié)
本文中獲得的有關(guān)快應(yīng)用頁(yè)面生命周期的知識(shí)和經(jīng)驗(yàn)的總結(jié)如下:
(1)頁(yè)面可以理解為Activity,并且啟動(dòng)模式能且僅能為standard
(2)頁(yè)面的onInit()和onReady()可以分別理解為你的BaseActivity的onInitData()和 onInitViews()。
(3)Activity的可見(jiàn)狀態(tài)和前臺(tái)狀態(tài)在頁(yè)面里都是顯示狀態(tài),所以onShow()可以理解為onStart()和onResume(),同理onHide()可以理解為onPause()和onStop()
(4)頁(yè)面的onDestroy()里可以理解為Activity的onDestroy()
(5)快應(yīng)用沒(méi)有singleTop這種啟動(dòng)模式,自然沒(méi)有onNewIntent()方法,但用replace()方法啟動(dòng)棧頂頁(yè)面可以起到同樣效果。
(6)onActivityResult()、onSaveInstanceState()和onRestoreInstanceState()和也都沒(méi)有對(duì)應(yīng)方法
(7)onBackPress()是BACK鍵觸發(fā)的方法,可以被攔截,但無(wú)法改成HOME鍵的效果
(8)onMenuPress()方法不是MENU鍵觸發(fā)的方法
(9)快應(yīng)用沒(méi)有singleTask這種啟動(dòng)模式,但back()方法起到類似Intent的FLAG_ACTIVITY_CLEAR_TASK的作用。
(10)back()方法不能用來(lái)跳轉(zhuǎn)到棧頂頁(yè)面。
(11)官方文檔中所有的“首頁(yè)”都指manifest.json文件中“router.entry”對(duì)應(yīng)的頁(yè)面(類似AndroidManifest.xml文件中帶“android.intent.action.MAIN"標(biāo)簽的Activity,通常被命名為SplashActivity),而不是指指穩(wěn)定運(yùn)行時(shí)頁(yè)面棧底的頁(yè)面(類似Android原生開(kāi)發(fā)的MainActivity)
附錄:本文完整代碼
頁(yè)面A(文件路徑:…/src/PageA/index.ux)的完整代碼(B、C的代碼僅title不同):
<template>
<div class="doc-page">
<text class="title">歡迎打開(kāi){{title}}</text>
<text class='text' if="{msg}">{{msg}}</text>
<input type="button" class="btn" onclick="this.$app.$def.routePush('/PageA')" value="用push()方法跳轉(zhuǎn)到頁(yè)面A" />
<input type="button" class="btn" onclick="this.$app.$def.routePush('/PageB')" value="用push()方法跳轉(zhuǎn)到頁(yè)面B" />
<input type="button" class="btn" onclick="this.$app.$def.routePush('/PageC')" value="用push()方法跳轉(zhuǎn)到頁(yè)面C" />
<input type="button" class="btn" onclick="this.$app.$def.routePush('/')" value="用push()方法跳轉(zhuǎn)到首頁(yè)" />
<input type="button" class="btn" onclick="this.$app.$def.routeReplace('/PageA')" value="用replace()方法跳轉(zhuǎn)到頁(yè)面A" />
<input type="button" class="btn" onclick="this.$app.$def.routeReplace('/PageB')" value="用replace()方法跳轉(zhuǎn)到頁(yè)面B" />
<input type="button" class="btn" onclick="this.$app.$def.routeReplace('/PageC')" value="用replace()方法跳轉(zhuǎn)到頁(yè)面C" />
<input type="button" class="btn" onclick="this.$app.$def.routeReplace('/')" value="用replace()方法跳轉(zhuǎn)到首頁(yè)" />
<input type="button" class="btn" onclick="this.$app.$def.routeBack('/PageA')" value="用back()方法跳轉(zhuǎn)到頁(yè)面A" />
<input type="button" class="btn" onclick="this.$app.$def.routeBack('/PageB')" value="用back()方法跳轉(zhuǎn)到頁(yè)面B" />
<input type="button" class="btn" onclick="this.$app.$def.routeBack('/PageC')" value="用back()方法跳轉(zhuǎn)到頁(yè)面C" />
<input type="button" class="btn" onclick="this.$app.$def.routeBack('/')" value="用back()方法跳轉(zhuǎn)到首頁(yè)" />
<input type="button" class="btn" onclick="this.$app.$def.routeBack()" value="用back()方法返回上一頁(yè)" />
<input type="button" class="btn" onclick="routeClear()" value="只保留當(dāng)前頁(yè)面" />
<input type="button" class="btn" onclick="this.$app.$def.routePush('tel:10086')" value="跳轉(zhuǎn)到打電話頁(yè)面" />
<input type="button" class="btn" onclick="this.$app.$def.routePush('hap://app/me.ele.xyy/')" value="跳轉(zhuǎn)到指定快應(yīng)用(餓了么)" />
</div>
</template>
<style>
@import '../Common/css/common.css';
.title {
font-size: 40px;
text-align: center;
}
.text {
font-size: 30px;
text-align: center;
}
</style>
<script>
import router from '@system.router'
export default {
private: {
msg:'',
title: '頁(yè)面A',
},onInit () {
this.$page.setTitleBar({text: this.title})
console.error(`### `+this.title+` onInit ###`)
this.msg = ""
},
onReady () {
console.error(`### `+this.title+` onReady ###`)
},
onShow () {
console.error(`### `+this.title+` onShow ###`)
this.msg = this.$app.$def.routeInfo()
console.error(`${this.msg}`)
},
onHide () {
console.error(`### `+this.title+` onHide ###`)
},
onDestroy () {
console.error(`### `+this.title+` onDestroy ###`)
},
onBackPress (params) {
console.error(`### `+this.title+` onBackPress ###`)
},
onMenuPress () {
console.error(`### `+this.title+` onMenuPress ###`)
},
routeClear() {
this.$app.$def.routeClear()
this.msg = this.$app.$def.routeInfo()
console.error(`${this.msg}`)
}
}
</script>
工具類util.js的完整代碼:
import router from '@system.router' function routePush(uri,params) { // 跳轉(zhuǎn)到應(yīng)用內(nèi)的某個(gè)頁(yè)面,或其他Activity // 匹配到與路徑與uri相同的頁(yè)面,則跳轉(zhuǎn)到該頁(yè)面,否則跳轉(zhuǎn)到首頁(yè) // 參數(shù)為"/",跳轉(zhuǎn)到首頁(yè) // uri若為包含schema的完整uri,則跳轉(zhuǎn)到應(yīng)用外的Activity(目前僅支持電話、短信、郵件和其他快應(yīng)用) // params為傳遞的參數(shù),不在本文討論范圍內(nèi) router.push ({ uri: uri, params: params }) } function routeReplace(uri,params) { // 跳轉(zhuǎn)到應(yīng)用內(nèi)的某個(gè)頁(yè)面,同時(shí)關(guān)閉當(dāng)前頁(yè)面 // 除了不能跳轉(zhuǎn)到應(yīng)用外的頁(yè)面,一切同push()方法 router.replace ({ uri: uri, params: params }) } function routeBack(path) { // 跳轉(zhuǎn)到應(yīng)用內(nèi)的某個(gè)已經(jīng)打開(kāi)過(guò)的頁(yè)面,同時(shí)關(guān)閉當(dāng)前頁(yè)面 // 不傳參數(shù),或沒(méi)有匹配到對(duì)應(yīng)頁(yè)面,則返回上一個(gè)頁(yè)面 // 參數(shù)為"/",返回首頁(yè) // 若匹配到多個(gè)頁(yè)面,返回至***打開(kāi)的頁(yè)面 // 注意back()方法的參數(shù)是path而不是uri router.back ({ path: path }) } function routeInfo (){ // 用getState()方法獲取當(dāng)前頁(yè)面狀態(tài),index表示當(dāng)前頁(yè)面在頁(yè)面棧中的位置(計(jì)數(shù)從0開(kāi)始) // 用getLength()方法獲取當(dāng)前頁(yè)面棧的頁(yè)面數(shù)量 return `當(dāng)前頁(yè)面在頁(yè)面棧中的位置 : `+ (router.getState().index + 1) + `/` + router.getLength() } function routeClear (){ // 清空所有歷史頁(yè)面記錄,僅保留當(dāng)前頁(yè)面 router.clear() } export default { routeReplace, routePush, routeBack, routeInfo, routeClear }