科技头条

看我用Android开发者听得懂的语言解释快应用页面的生命周期和接口router-12.4

51CTO 作者:东风玖哥 2018-12-06 10:50:28 快应用 Android

就像世界上第一批Android工程师大多都是iOS工程师转行一样,世界上第一批QuickApp工程师也大多都是Android工程师转行。将快应用知识与Android知识对比学习可以起到温故知新的效果。

查阅快应用官方文档可知快应用的“页面”和Android原生的Activity都是提供一个可以给用户来交互的屏幕,在底层也都是用Stack保存浏览记录。理解页面的生命周期就像理解Activity’的生命周期一样,有助于更好的组织页面的业务逻辑,方便页面之间的交互与资源释放等的处理。但为何“页面”仅有三种状态而Activity却有四种呢?又为何“页面”没有类似Activity的启动模式呢?本文将为你揭晓答案:

页面的生命周期和状态

众所众知Activity的生命周期由七个主要被动方法以及onBackPressed()、onNewIntent()、onActivityResult()、onSaveInstanceState()和onRestoreInstanceState()等其他被动方法组成,并且有、、和共四种状态;而查阅官方文档可知页面的被动方法仅有七个,而状态只有、和三种。我来给大家对比分析一下两组方法的对应关系。

onInit()和onReady()

根据快应用官方文档的说法:onInit()方法表示ViewModel的数据已经准备好,可以开始使用页面中的数据,能且仅能调用一次。onReady()方法表示ViewModel的模板已经编译完成,可以开始获取DOM节点,能且仅能调用一次。

每个Android开发者的类库里都有一个BaseActivity,这个BaseActivity里一般都有初始化配置、绑定View的同步方法onInitViews()和请求数据的异步方法onInitData();我们可以把onInit()理解为onInitData(),把onReady()理解为onInitViews()。

如果把眼光放远一点,拿页面与Fragment比较,onInit()更像Fragment的onCreate(),而onReady()更像onCreateView()。

onShow()和onHide()

每个快应用的App中可以同时运行多个页面,但是每次只能显示其中一个页面;这点不同于Android开发,可以同时显示多个Activity;也不同与纯前端开发,浏览器页面中每次只能有一个页面,当前页签打开另一个页面,上个页面就销毁了。

根据快应用官方文档的说法:页面被切换隐藏时调用onHide(),页面被切换重新显示时调用onShow()。很明显这与Activity有onStart()和onStop()、onResume()和onPause()两对方法不同,这是因为页面不像Activity有透明背景和Theme.Dialog主题,所以Activity的可见状态和前台状态在页面里仅对应显示状态。

onDestroy()

根据快应用官方文档的说法:onDestroy()方法在页面被销毁时调用,能且仅能调用一次。被销毁的可能原因有:用户从当前页面返回到上一页,或者用户打开了太多的页面,框架自动销毁掉部分页面,避免占用资源。而官方建议页面进入销毁状态时应该做一些释放资源的操作,这和Activity的onDestroy()方法的推荐使用方式不谋而合,所以页面的onDestroy()方法就是Activity的onDestroy()方法。

onBackPress()

根据快应用官方文档的说法:当用户点击实体BACK按键或左上角返回菜单时触发onBackPress()事件。我想没有人不会把页面的onBackPress()方法和Actvity的onBackPressed()方法联系到一起。

如果事件响应方法最后返回true表示不返回,自己处理业务逻辑,完毕后开发者自行调用router.back()方法返回。代码如下:


onBackPress (params) {

//做自己喜欢的事

return true

}

对比一下Activity的onBackPressed()的override方式:


@Override 

public void onBackPressed() { 

// super.onBackPressed();

// 做自己喜欢的事 

}

onMenuPress()

对比一下onBackPress()可知:当用户点击右上角菜单时触发onMenuPress()事件。如果我们有使用菜单的需求,可以通过manifest.json中的menu属性配置是否显示右上角的菜单。

所有支持快应用的国产Android设备的MENU键都用来清理内存,因此实体MENU键不会触发onMenuPress(),这点与onBackPress()有所区别。

页面路由接口router

根据快应用官方文档的说法:我们可以通过配置a组件的href属性跳转到应用内的页面,有点类似于Android开发中已不被推荐使用的的隐式Intent跳转Activity;此外我们也可以使用router接口,这就有点类似于Android开发的显式Intent组件或者ARouter框架。本文的一切页面跳转都使用router接口。

常见方法

接口router常见方法在官方文档里写得很清楚,我只讲几点注意事项:

(1)接口router的push()方法能跳转应用外的Activity包括电话、短信、邮件和其他快应用

(2)接口router的push()方法不能实现Android的Intent的“android.intent.category.HOME”标签的功能,也就是说,除非用户点HOME能回到桌面,否则开发者不能靠重写onBackPress()保留首页

(3)打开照相机、QQ聊天、微信分享、支付宝付款用的不是router

(4)back()方法的路径参数是path,并非push()和replace()的uri

回传参数的方式

传递参数的方式不在本文的讨论范围之内,但回传参数的方式却涉及生命周期,我们先看快应用回传参数的官方代码:


onHide () {

// 页面被切换隐藏时,将要传递的数据对象写入全局变量

this.$app.$data.dataPageB = {

gotoPage: 'pageA',

params: {

msg: this.msg

}

}

},

对比一下Activity的setResult()方法的调用:


Intent intent = new Intent(); 

intent.putExtra("dataPageB",dataPageB); 

setResult(RESULT_OK,intent);

快应用接收回传参数的官方代码:

onShow () {

// 页面被切换显示时,从数据中检查是否有页面B传递来的数据

if (this.$app.$data.dataPageB && this.$app.$data.dataPageB.gotoPage === 'pageA') {

// 从数据中获取回传给本页面的数据

const data = this.$app.$data.dataPageB.params

this.msg = data.msg

}

},

对比一下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(); 

} 

}

由此可见,在onRscume()方法里检验全局变量的变化这一行为,作为Android原生开发中饱受诟病的新手行为,在快应用开发中是官方推荐的,所以快应用不需要类似onActivityResult()方法的方法。

研究接口router和页面生命周期关系的实践

“纸上得来终觉浅”,我们写一个LifecycleDemo来研究接口router和页面生命周期关系:

首先打开这个LifecycleDemo,我们可以看到logcat打印出如下信息:

### 页面A onInit ###

### 页面A onReady ###

### 页面A onShow ###

当前页面在页面栈中的位置 : 1/1

点击BACK键,返回桌面,logcat打印出如下信息:

### 页面A onBackPress ###

### 页面A onHide ###

### 页面A onDestroy ###

与官方文档描述相同,符合预期

打开其他Activity

接下来我们打开其他Activity,包括系统桌面、打电话界面和其他应用

点击HOME键,然后热启动LifecycleDemo,Logcat打印如下:


### 页面A onHide ###

### 页面A onShow ###

当前页面在页面栈中的位置 : 1/1

应用内打开其他系统Activity,然后热启动LifecycleDemo,Logcat打印如下:


### 页面A onHide ###

### 页面A onShow ###

当前页面在页面栈中的位置 : 1/1

应用内打开别的快应用,然后热启动LifecycleDemo,Logcat打印如下:

### 页面A onHide ###

### 页面A onShow ###

当前页面在页面栈中的位置 : 1/1

结论:符合预期,支持上文onShow()相当于onStart()和onResume(),onHide()相当于onPause()和onStop()的猜想。

用push()方法进行应用内页面跳转

用push()方法跳转到页面A,logcat打印如下:


### 页面A onHide ###

### 页面A onInit ###

### 页面A onReady ###

### 页面A onShow ###

当前页面在页面栈中的位置 : 2/2

显然页面栈里的顺序为AA,支持上文页面的启动模式相当于Activity的Standard模式的猜想。现在猜想第一个A是前面的,第2、3、4个A是后面的。我们接着用push()方法跳转到页面B,logcat打印如下:


### 页面A onHide ###

### 页面B onInit ###

### 页面B onReady ###

### 页面B onShow ###

当前页面在页面栈中的位置 : 3/3

显然页面栈里的顺序为AAB,也符合预期,支持上文猜想。

我们发现快应用官方文档存在歧义,就是首页究竟是指稳定运行时页面栈底的页面(类似Android原生开发的MainActivity),还是指manifest.json文件中“router.entry”对应的页面(类似AndroidManifest.xml文件中带“android.intent.action.MAIN"标签的Activity,通常被命名为SplashActivity),我们验证一下:

当“router.entry”对应页面A,而页面栈里页面顺序为BBAACC的时候,我们用push()方法跳转到首页。首页是这样的:

而logcat打印如下:

### 页面C onHide ###

### 页面A onInit ###

### 页面A onReady ###

### 页面A onShow ###

当前页面在页面栈中的位置 : 7/7

原来接口router可以跳转的首页指的是“router.entry”对应的页面。

用replace()方法进行应用内页面跳转

当页面栈里仅有A的情况下,用replace()方法跳转到页面A,logcat打印如下:

### 页面A onHide ###

### 页面A onDestroy ###

### 页面A onInit ###

### 页面A onReady ###

### 页面A onShow ###

当前页面在页面栈中的位置 : 1/1

显然页面栈里仅有一个A,猜想replace()方法类似Activity里的这段代码:


startActivity(intent);

finish();

又猜想第1、2个A是前面的,第3、4、5个A是后面的。我们接着用replace()方法跳转到页面B,logcat打印如下:

### 页面A onHide ###

### 页面A onDestroy ###

### 页面B onInit ###

### 页面B onReady ###

### 页面B onShow ###

当前页面在页面栈中的位置 : 1/1

符合预期,支持上文猜想。

用back()方法进行应用内页面跳转

在页面栈里的顺序为AABBCC的情况下,根据文档仅能得出用back()方法返回上一页后页面栈里的顺序为AABBC,返回页面B后页面栈里的顺序为AABB,返回页面A或首页后页面栈里的顺序为AA,有点类似Intent的FLAG_ACTIVITY_CLEAR_TASK标签。我们只讨论官方文档忽略的内容:

我们用back()方法跳转到页面C,页面无变化;在页面栈里的顺序为ABCABC的情况下,我们用back()方法跳转到页面C,页面也无变化。得出back()方法不能用来跳转到栈顶页面的结论。

总结

本文中获得的有关快应用页面生命周期的知识和经验的总结如下:

(1)页面可以理解为Activity,并且启动模式能且仅能为standard

(2)页面的onInit()和onReady()可以分别理解为你的BaseActivity的onInitData()和 onInitViews()。

(3)Activity的可见状态和前台状态在页面里都是显示状态,所以onShow()可以理解为onStart()和onResume(),同理onHide()可以理解为onPause()和onStop()

(4)页面的onDestroy()里可以理解为Activity的onDestroy()

(5)快应用没有singleTop这种启动模式,自然没有onNewIntent()方法,但用replace()方法启动栈顶页面可以起到同样效果。

(6)onActivityResult()、onSaveInstanceState()和onRestoreInstanceState()和也都没有对应方法

(7)onBackPress()是BACK键触发的方法,可以被拦截,但无法改成HOME键的效果

(8)onMenuPress()方法不是MENU键触发的方法

(9)快应用没有singleTask这种启动模式,但back()方法起到类似Intent的FLAG_ACTIVITY_CLEAR_TASK的作用。

(10)back()方法不能用来跳转到栈顶页面。

(11)官方文档中所有的“首页”都指manifest.json文件中“router.entry”对应的页面(类似AndroidManifest.xml文件中带“android.intent.action.MAIN"标签的Activity,通常被命名为SplashActivity),而不是指指稳定运行时页面栈底的页面(类似Android原生开发的MainActivity)

附录:本文完整代码

页面A(文件路径:…/src/PageA/index.ux)的完整代码(B、C的代码仅title不同):

<template>

<div class="doc-page">

<text class="title">欢迎打开{{title}}</text>

<text class='text' if="{msg}">{{msg}}</text>

<input type="button" class="btn" onclick="this.$app.$def.routePush('/PageA')" value="用push()方法跳转到页面A" />

<input type="button" class="btn" onclick="this.$app.$def.routePush('/PageB')" value="用push()方法跳转到页面B" />

<input type="button" class="btn" onclick="this.$app.$def.routePush('/PageC')" value="用push()方法跳转到页面C" />

<input type="button" class="btn" onclick="this.$app.$def.routePush('/')" value="用push()方法跳转到首页" />

<input type="button" class="btn" onclick="this.$app.$def.routeReplace('/PageA')" value="用replace()方法跳转到页面A" />

<input type="button" class="btn" onclick="this.$app.$def.routeReplace('/PageB')" value="用replace()方法跳转到页面B" />

<input type="button" class="btn" onclick="this.$app.$def.routeReplace('/PageC')" value="用replace()方法跳转到页面C" />

<input type="button" class="btn" onclick="this.$app.$def.routeReplace('/')" value="用replace()方法跳转到首页" />

<input type="button" class="btn" onclick="this.$app.$def.routeBack('/PageA')" value="用back()方法跳转到页面A" />

<input type="button" class="btn" onclick="this.$app.$def.routeBack('/PageB')" value="用back()方法跳转到页面B" />

<input type="button" class="btn" onclick="this.$app.$def.routeBack('/PageC')" value="用back()方法跳转到页面C" />

<input type="button" class="btn" onclick="this.$app.$def.routeBack('/')" value="用back()方法跳转到首页" />

<input type="button" class="btn" onclick="this.$app.$def.routeBack()" value="用back()方法返回上一页" />

<input type="button" class="btn" onclick="routeClear()" value="只保留当前页面" />

<input type="button" class="btn" onclick="this.$app.$def.routePush('tel:10086')" value="跳转到打电话页面" />

<input type="button" class="btn" onclick="this.$app.$def.routePush('hap://app/me.ele.xyy/')" value="跳转到指定快应用(饿了么)" />

</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: '页面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) { 
 
// 跳转到应用内的某个页面,或其他Activity 
 
// 匹配到与路径与uri相同的页面,则跳转到该页面,否则跳转到首页 
 
// 参数为"/",跳转到首页 
 
// uri若为包含schema的完整uri,则跳转到应用外的Activity(目前仅支持电话、短信、邮件和其他快应用) 
 
// params为传递的参数,不在本文讨论范围内 
 
router.push ({ 
 
uri: uri, 
 
params: params 
 
}) 
 
} 
 
function routeReplace(uri,params) { 
 
// 跳转到应用内的某个页面,同时关闭当前页面 
 
// 除了不能跳转到应用外的页面,一切同push()方法 
 
router.replace ({ 
 
uri: uri, 
 
params: params 
 
}) 
 
} 
 
function routeBack(path) { 
 
// 跳转到应用内的某个已经打开过的页面,同时关闭当前页面 
 
// 不传参数,或没有匹配到对应页面,则返回上一个页面 
 
// 参数为"/",返回首页 
 
// 若匹配到多个页面,返回至最后打开的页面 
 
// 注意back()方法的参数是path而不是uri 
 
router.back ({ 
 
path: path 
 
}) 
 
} 
 
function routeInfo (){ 
 
// 用getState()方法获取当前页面状态,index表示当前页面在页面栈中的位置(计数从0开始) 
 
// 用getLength()方法获取当前页面栈的页面数量 
 
return `当前页面在页面栈中的位置 : `+ (router.getState().index + 1) + `/` + router.getLength() 
 
} 
 
function routeClear (){ 
 
// 清空所有历史页面记录,仅保留当前页面 
 
router.clear() 
 
} 
 
export default { 
 
routeReplace, 
 
routePush, 
 
routeBack, 
 
routeInfo, 
 
routeClear 
 
}