首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android拾萃- Activity的生命周期和启动模式

Android拾萃- Activity的生命周期和启动模式

作者头像
我就是马云飞
发布2018-02-05 10:51:00
1.4K0
发布2018-02-05 10:51:00
举报
文章被收录于专栏:我就是马云飞我就是马云飞

概述

Activity 作为与用户交互的一个窗口,是使用非常频繁的一个基本组件。Android系统是通过Activity栈来管理Activity的,而Activity则是通过哦生命周期来进行自己的创建、活动与销毁等。所以掌握Activity生命周期很有必要。

金字塔模式

  官方的描述很形象,Activity 生命周期的每个阶段就是金字塔上的一阶。   当系统创建新 Activity 实例时,每个回调方法会将 Activity 状态向顶端移动一阶。 金字塔的顶端是 Activity 在前台运行并且用户可以与其交互的时间点。   当用户开始离开 Activity 时,系统会调用其他方法在金字塔中将 Activity 状态下移,从而销毁 Activity。   在有些情况下,Activity 将只在金字塔中部分下移并等待(比如,当用户切换到其他应用时),Activity 可从该点开始移回顶端(如果用户返回到该 Activity),并在用户停止的位置继续。

这个模型中包含了Activity的六种状态:

Created:创建完成 Started:可见(不可交互) Resumed:可见(活动) Paused:部分可见(后台) Stopped:不可见 Destroyed:销毁

在这六种状态当中,只有ResumedPausedStopped这几种状态在用户没有进一步操作时会保持在该状态,而其余的,都会在执行完相应的回调函数后快速跳过,很容易理解,resumed 状态就是在当前界面,后面两个状态是进入了另一个界面活动,如果打开一个dialog或者透明主题(dialog主题)的Activity,这个时候,只会进入paused状态,不会进入stoped状态。

一般情况下,您不得使用 onPause() 永久性存储用户更改(比如输入表格的个人信息)。 只有在您确定用户希望自动保存这些更改的情况(比如,草拟电子邮件时)下,才能在 onPause() 中永久性存储用户更改。但您应避免在 onPause() 期间执行 CPU 密集型工作,比如向数据库写入信息,因为这会拖慢向下一 Activity 过渡的过程(您应改为在 onStop() 间执行高负载关机操作)。另外一点就是,启动新的Activity,当前的Activity必须onpause进入后台,才会开始启动下一个Activity。

异常情况下的生命周期

在有些情况下,您的 Activity 会因正常应用行为而销毁,比如当用户按 返回按钮或您的 Activity 通过调用 finish()示意自己的销毁。 如果 Activity 当前被停止或长期未使用,或者前台 Activity 需要更多资源以致系统必须关闭后台进程恢复内存,系统也可能会销毁 Activity。

当您的 Activity 因用户按了返回 或 Activity 自行完成而被销毁时,系统的 Activity 实例概念将永久消失,因为行为指示不再需要 Activity。 但是,如果系统因系统局限性(而非正常应用行为)而销毁 Activity,尽管 Activity 实际实例已不在,系统会记住其存在,这样,如果用户导航回实例,系统会使用描述 Activity 被销毁时状态的一组已保存数据创建 Activity 的新实例。 系统用于恢复先前状态的已保存数据被称为“实例状态”,并且是 Bundle 对象中存储的键值对集合。

注意:每次用户旋转屏幕时,您的 Activity 将被销毁并重新创建。 当屏幕方向变化时,系统会销毁并重新创建前台 Activity,因为屏幕配置已更改并且您的 Activity 可能需要加载备用资源(比如布局)。

默认情况下,系统会使用 Bundle 实例状态保存您的 Activity 布局(比如,输入到 EditText 对象中的文本值)中有关每个 View 对象的信息。 这样,如果您的 Activity 实例被销毁并重新创建,布局状态便恢复为其先前的状态,且您无需代码。 但是,您的 Activity 可能具有您要恢复的更多状态信息,比如跟踪用户在 Activity 中进度的成员变量。

:为了 Android 系统恢复 Activity 中视图的状态,每个视图必须具有 android:id 属性提供的唯一 ID。

要保存有关 Activity 状态的其他数据,您必须替代 onSaveInstanceState() 回调方法。当用户要离开 Activity 并在 Activity 意外销毁时向其传递将保存的 Bundle 对象时,系统会调用此方法。 如果系统必须稍后重新创建 Activity 实例,它会将相同的 Bundle 对象同时传递给 onRestoreInstanceState() 和 onCreate() 方法。

如果Activity A 启动 B 在启动 C,如果A和B被回收了,这个时候C返回,B会重绘(实例被回收了,但是栈还是在的)

由重建引发的窗体泄漏

Android的每一个Activity都有个WindowManager窗体管理器,构建在某个Activity之上的对话框、PopupWindow也有相应的WindowManager窗体管理器。因为Dialog、PopupWindown不能脱离Activity而单独存在着,所以当承载某个Dialog或者某个PopupWindow正在显示的Activity被finish()后,而Dialog(或PopupWindow)没有正常退出的话,就会抛Window Leaked错误了,因为这个Dialog(或PopupWindow)的WindowManager已经没有谁可以附属了,所以它的窗体管理器就泄漏了。

在进入新的Activity时突然转屏(哥们开发的sdk支持横竖屏切换),因为在AndroidManifest.xml中没有配置android:configChanges属性,此时Activity会重新调用onCreate方法,即会重新调用整个生命周期,而此时的Dialog已经显示并没有dismiss,所以造成了窗体泄漏。解决的方法就变得如此简单,在AndroidManifest.xml中配置android:configChanges属性,这样当我们横竖屏切换的时候会调用Activity的onConfigurationChanged方法,不会重新调用整个生命周期了。

android:configChanges的一些属性

1、不设置Activity的android:configChanges时,切屏会重新调用整个生命周期,切横屏时会执行一次,切竖屏时会执行两次

2、设置Activity的android:configChanges="orientation"时,切屏还是会重新调用整个生命周期,切横、竖屏时只会执行一次

3、设置Activity的android:configChanges="orientation|screenSize"一起设置的时候才是moshi有效的(原因看下面的表格)。虽然不会重建Activity,但是会回调Activity里面的一个方法: onConfigurationChanged(Configuration config)

在这里你可以监听了,Activity的什么改变了,比如方向,比如弹出了键盘还是隐藏了moshi键盘(清单文件的Activity 添加android:configChanges="keyboard|keyboardHidden“),如果有需要监控其他属性的需求,请参考底下的表格进行属性添加

附上开发艺术探索的一张图(侵删)android:configChanges属性解释:

Activity启动模式

任务栈

我们知道系统使用栈来管理Activity,而栈根据是否在前台,可以划分为前台栈和后台栈(实际没有区别,根据当前的Activity划分,即前台只有一个,后台可能有多个)。

  1. Android任务栈又称为Task,它是一个栈结构,具有后进先出的特性,用于存放我们的Activity组件。
  2. 我们每次打开一个新的Activity或者退出当前Activity都会在一个称为任务栈的结构中添加或者减少一个Activity组件,因此一个任务栈包含了一个activity的集合, android系统可以通过Task有序地管理每个activity,并决定哪个Activity与用户进行交互:只有在任务栈栈顶的activity才可以跟用户进行交互。
  3. 在我们退出应用程序时,必须把所有的任务栈中所有的activity清除出栈时,任务栈才会被销毁。当然任务栈也可以移动到后台, 并且保留了每一个activity的状态. 可以有序的给用户列出它们的任务, 同时也不会丢失Activity的状态信息。
  4. 需要注意的是,一个App中可能不止一个任务栈,某些特殊情况下,单独一个Actvity可以独享一个任务栈。还有一点就是一个Task中的Actvity可以来自不同的App,同一个App的Activity也可能不在一个Task中。

那么系统是怎么划分Activity是在同一个栈里呢?这个时候就要说下TaskAffinity这个属性了。

TaskAffinity属性

TaskAffinity(任务相关性),这个参数标识了一个Activity所需要的任务栈的名字,默认情况下,所有的Activity所需的任务栈的名字为应用的包名. 可以为每个Activity都单独指定TaskAffinity属性,不同的名字代表不同的任务栈android:taskAffinity="属性值为字符串"。 TaskAffinity如何生效

  • TaskAffinity + singleTask (其实就是把singletask放到和包名不一样的栈,singletask单独使用,不代表不能在包名这个栈,他只表示一旦创建之后,只允许一个栈存在一个实例)
  • TaskAffinity + allowTaskReparenting(允许了其他应用的某个Activity无缝迁移进入我们应用的Activity栈,一旦再次打开其他应用的时候,又会迁移回去,具体见下面的图)
  • 其他情况是无效的

可以通过 adb shell dumpsys activity activities 命令查看栈的情况 command + K是terminal的清屏快捷键 l 在adb命令中显示的launchMode代表的数值 standard : launchMode = 0 singleTop : launchMode=1 singleTask: launchMode= 2 singleInstance: launchMode=3

allowTaskReparenting = true 的迁移行为,如下图(来源于网络,侵删)

不过有点需要说明的是allowTaskReparenting仅限于singleTopstandard模式,这是因为一个activity的affinity属性由它的taskAffinity属性定义(代表栈名),而一个task的affinity由它的root activity定义。所以,一个task的root activity总是拥有和它所在task相同的affinity。由于以singleTask和singleInstance启动的activity只能是一个task的root activity,因此allowTaskReparenting仅限于以standard 和singleTop启动的activity

四种启动模式

我们应用中有多个Activity组件,之间经常会进行跳转,也有可能需要在本应用中打开其它应用的的Activity。当我们返回上一个组件时,我们更希望复用这个Activity。 但Android系统的stander模式每次都会为我们创建一个新的Activity并添加到Task中。另外,我们开启一次页面,它的数据和信息状态都会被保留,这样会造成数据冗余, 重复数据太多, 最终还可能导致内存溢出的问题(OOM)。

为了解决这些问题,android系统提供了一套Activity的启动模式来修改系统Activity的默认启动行为。目前启动模式有四种,分别是standardsingleTopsingTasksingleInstance,接下来我们将分别介绍这四种模式。

  • Standard 模式   又称为标准模式,也是系统的默认模式(可以不指定),在这样模式下,每启动一个Activity都会重新创建一个Activity的新实例,并且将其加入任务栈中,而且完全不会去考虑这个实例是否已存在。 singleTop 模式   又称栈顶复用模式,顾名思义,在这种模式下,如果有新的Activity已经存在任务栈的栈顶,那么此Activity就不会被重新创建新实例,而是复用已存在任务栈栈顶的Activity。这里重点是位于栈顶,才会被复用,如果新的Activity的实例已存在但没有位于栈顶,那么新的Activity仍然会被重建。需要注意的是,Activity的onNewIntent方法会被调用,方法原型如下:
  • @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); }

通过此方法的参数,我们可以获取当前请求的相关信息,此时Activity的onCreate、onStart方法不会被调用,因为Activity并没有被重建。   这种模式通常比较适用于接收到消息后显示的界面,如qq接收到消息后弹出Activity界面,如果一次来10条消息,总不能一次弹10个Activity,是吧?再比如新闻客户端收到了100个推送,你每次点一下推送他都会进入某个activiy界面(显singleTask 模式    又称为栈内复用模式。这是一种单例模式,与singTop点类似,只不过singTop是检测栈顶元素是否有需要启动的Activity,而singTask则是检测整个栈中是否存在当前需要启动的Activity,如果存在就直接将该Activity置于栈顶,并将该Activity以上的Activity都从任务栈中移出销毁,同时也会回调onNewIntent方法。示新闻只用一个activity,只是内容不同而已),这时也比较适合使用singleTop模式。

  • singleTask 模式    又称为栈内复用模式。这是一种单例模式,与singTop点类似,只不过singTop是检测栈顶元素是否有需要启动的Activity,而singTask则是检测整个栈中是singleTask 模式否存在当前需要启动的Activity,如果存在就直接将该Activity置于栈顶,并将该Activity以上的Activity都从任务栈中移出销毁,同时也会回调onNewIntent方法。

singleTask 模式比较适合应用的主界面activity(频繁使用的主架构),可以用于主架构的activity,(如新闻,侧滑,应用主界面等)里面有好多fragment,一般不会被销毁,它可以跳转其它的activity 界面再回主架构界面,此时其他Activity就销毁了。

  • singleInstance 模式   在singleInstance模式下,该Activity在整个android系统内存中有且只有一个实例,而且该实例单独尊享一个Task。换句话说,A应用需要启动的MainActivity 是singleInstance模式,当A启动后,系统会为它创建一个新的任务栈,然后A单独在这个新的任务栈中,如果此时B应用也要激活MainActivity,由于栈内复用的特性,则不会重新创建,而是两个应用共享一个Activity的实例。

Activity启动模式的使用方法

如何给Activity指定启动模式呢?事实上共有如下两种方式: 1. 通过AndroidMenifest.xml文件为Activity指定启动模式,代码如下:

<activity android:name=".ActivityC"   
         android:launchMode="singleTask" />

2. 通过在Intent中设置标志位(addFlags方法)来为Activity指定启动模式,示例代码如下:

Intent intent = new Intent(); 
intent.setClass(ActivityB.this,ActivityA.class); 
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

那么什么是标志位呢?常用的标志位有哪一些?

启动标记 Intent Flag
  • Intent.FLAG_ACTIVITY_NEW_TASK 该标志位表示使用一个新的Task来启动一个Activity,相当于在清单文件中给Activity指定“singleTask”启动模式。
  • Intent.FLAG_ACTIVITY_SINGLE_TOP   该标志位表示使用singleTop模式来启动一个Activity,与在清单文件指定android:launchMode="singleTop"效果相同。
  • Intent.FLAG_ACTIVITY_CLEAR_TOP 该标志位表示使用singleTask模式来启动一个Activity,与在清单文件指定android:launchMode="singleTask"效果相同。
  • Intent.FLAG_ACTIVITY_NO_HISTORY   使用该模式来启动Activity,当该Activity启动其他Activity后,该Activity就被销毁了,不会保留在任务栈中。如A-B,B中以这种模式启动C,C再启动D,则任务栈只有ABD。
  • Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS   使用该标识位启动的Activity不添加到最近应用列表,也即我们从最近应用里面查看不到我们启动的这个activity。与属性android:excludeFromRecents="true"效果相同。

启动模式中SingleTask的特殊情景

特殊情景一

前面我们在分析singleTask模式时,提到过singleTask模式有些比较特殊的场景,现在我们就来了解了解它们。 特殊情景一:现在我们假设有如下两个Task栈,分别为前台任务栈和后台任务栈

从图中我们看出前台任务栈分别为AB两个Activity,后台任务栈分别为CD两个任务栈,而且其启动模式均为singleTask,此时我们先启动CD,然后再启动AB,再有B启动D,此时后台任务栈便会被切换到前台,而且这个时候整个后退列表就变成了ABCD,请注意我们这里强调的是后退列表,而非栈合并。因此当用户点击back键时,列表中的Activity会依次按DCBA顺序出栈,如下图所示:

特殊情景二:

如果上面B不是请求启动D而是请求启动C,那么又会是什么情况呢?其实这个时候任务栈退出列表变成C->B->A,其实原因很简单,singleTask模式的ActivityC切换到栈顶时会导致在他之上的栈内的Activity出栈。其他情况都一样。

特殊场景三:

  • StartActivityForResult的时候,requestCode必须>0,否则收不到result 看下以下关系图

这是为什么呢?

这是因为ActivityStackSupervisor类中的startActivityUncheckedLocked方法在5.0中进行了修改。在5.0之前,当启动一个Activity时,系统将首先检查Activity的launchMode,如果为A页面设置为SingleInstance或者B页面设置为singleTask或者singleInstance,则会在LaunchFlags中加入FLAG_ACTIVITY_NEW_TASK标志,而如果含有FLAG_ACTIVITY_NEW_TASK标志的话,onActivityResult将会立即接收到一个cancle的信息,而5.0之后这个方法做了修改,修改之后即便启动的页面设置launchMode为singleTask或singleInstance,onActivityResult依旧可以正常工作,也就是说无论设置哪种启动方式,StartActivityForResult和onActivityResult()这一组合都是有效的。所以如果你目前正好基于5.0做相关开发,不要忘了向下兼容,这里有个坑请注意避让。

结合时的应用场景

TaskAffinity与singleTask应用场景

假如现在有这么一个需求,我们的客户端app正处于后台运行,此时我们因为某些需要,让微信调用自己客户端app的某个页面,用户完成相关操作后,我们不做任何处理,按下回退或者当前Activity.finish(),页面都会停留在自己的客户端(此时我们的app回退栈不为空),这显然不符合逻辑的,用户体验也是相当出问题的。我们要求是,回退必须回到微信客户端,而且要保证不杀死自己的app.这时候我们的处理方案就是,设置当前被调起Activity的属性为:

LaunchMode=""SingleTask" taskAffinity="com.tencent.mm"

其中com.tencent.mm是借助于工具找到的微信包名,就是把自己的Activity放到微信默认的Task栈里面,这样回退时就会遵循“Task只要有Activity一定从本Task剩余Activity回退”的原则,不会回到自己的客户端;而且也不会影响自己客户端本来的Activity和Task逻辑。

TaskAffinity与allowTaskReparenting应用场景

一个e-mail应用消息包含一个网页链接,点击这个链接将触发一个activity来显示这个页面,虽然这个activity是浏览器应用定义的,但是activity由于e-mail应用程序加载的,所以在这个时候该activity也属于e-mail这个task。如果e-mail应用切换到后台,浏览器在下次打开时由于allowTaskReparenting值为true,此时浏览器就会显示该activity而不显示浏览器主界面,同时actvity也将从e-mail的任务栈迁移到浏览器的任务栈,下次打开e-mail时并不会再显示该activity

清空任务栈

Android系统除了给我提供了TaskAffinity来指定任务栈名称外,还给我提供了清空任务栈的方法,在一般情况下我们只需要在<activity>标签中指明相应的属性值即可。   如果用户离开一个task很久,系统就会清理这个task中的所有activities,除了根activity。当用户返回到这个task,只有根activity会被恢复。   有一些activity的属性,你可以用来改变这一行为:

android:clearTaskOnLaunch

这个属性用来标记是否从task清除除根Activity之外的所有的Activity,“true”表示清除,“false”表示不清除,默认为“false”。这里有点我们必须要注意的,这个属性只对任务栈内的root Activity起作用,任务栈内其他的Activity都会被忽略。如果android:clearTaskOnLaunch属性为“true”,每次我们重新android:clearTaskOnLaunch进入这个应用时,我们只会看到根Activity,任务栈中的其他Activity都会被清除出栈。   比如一个应用的Activity A,B,C,其中A 的clearTaskOnLaunch设置为true,C为默认值,我们依次启动A,B,C,点击HOME,再在桌面点击图标。启动的是A,而B,C将都被移除当前任务栈。也就是说,当Activity的属性clearTaskOnLaunch为true时将被优先启动,其余的Activity(B、C)都被移除任务栈并销毁,除非前面A已经finish销毁,后面的已注册clearTaskOnLaunch为true的activity(B)才会生效。   特别地,如果我们的应用中引用到了其他应用的Activity,这些Activity设置了android:allowTaskReparenting属性为“true”,则它们会被重新宿主到有共同affinity的task中。

android:finishOnTaskLaunch

finishOnTaskLaunch属性与clearTaskOnLaunch 有些类似,它们的区别是finishOnTaskLaunch是作用在自己身上(把自己移除任务栈,不影响别的Activity),而clearTaskOnLaunch则是作用在别人身上(把别的Activity移除任务栈),如果我们把Activity的android:finishOnTaskLaunch属性值设置为true时,离开这个Activity所依赖的任务栈后,当我们重新返回时,该Activity将会被finish掉,而且其他Activity不会受到影响。

android:alwaysRetainTaskState

alwaysRetainTaskState实际上是给了当前Activity所在的任务栈一个“免死金牌”,如果当前Activity的android:alwaysRetainTaskState设置为true时,那么该Activity所在的任务栈将不会受到任何清理命令的影响,一直保持当前任务栈的状态。

应用场景

  1. singleTop适合接收通知启动的内容显示页面。例如,某个新闻客户端的新闻内容页面,如果收到10个新闻推送,每次都打开一个新闻内容页面是很烦人的。聊天的对话窗口,
  2. singleTask适合作为程序入口点。例如浏览器的主界面。不管从多少个应用启动浏览器,只会启动主界面一次,其余情况都会走onNewIntent,并且会清空主界面上面的其他页面。之前打开过的页面,打开之前的页面就ok,不再新建。 singleTask:a界面购物,b界面确认订单,c界面付款,如果付款成功会跳到a,如果不付款则返回b,这时候重启a就会用到singleTask.
  3. singleInstance适合需要与程序分离开的页面。例如闹铃提醒,将闹铃提醒与闹铃设置分离。singleInstance不要用于中间页面,如果用于中间页面,跳转会有问题,比如:A -> B (singleInstance) -> C,完全退出后,在此启动,首先打开的是B。
  4. standard 标准的启动模式,也是默认的启动模式。

隐式启动Activity匹配规则

这一部分参考的 http://www.jianshu.com/p/151640add690 启动activity分为两种,显式启动和隐式启动。显式:明确指出被调用activity的包名类名,隐式调用不需要明确信息。显式和隐式原则上是不共存的,如果共存以显示为主。隐式启动匹配信息在AndroidManifest的activity中的<intent-filter>,三种过滤信息:action,category,data。三个信息可同时存在多个。intent-filter也可同时存在多个,匹配其中一组intent-filter的三种信息各一种即可。

匹配规则
action

区分大小写,action系统有自定义一些,action匹配字符串必须一样。若intentFilter定义了action属性,隐式启动至少匹配其中一个。

category

Intent未指定category时,系统会默认给Intent增加category属性:<category android:name="android.intent.category.DEFAULT" ,所以如果你隐式启动activity且不想指定category在AndroidManifest总定义隐式启动时,需加上<category android:name="android.intent.category.DEFAULT"。 Intent指定category,指定一个必须正确匹配一个,多个必须正确匹配多个。

data

intentFilter配置data,Intent隐式启动必须匹配至少一个,和action类似 先介绍一种结构 URL:

<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]

例如 content://com.example.test:100/folder/subfolder/test http://www.baidu.com:80/search/info data的所有匹配属性如下:

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

主要分为两种,一种是mimeType,一种是URI中的其中任何之一属性。

属性简介:

mimeType:媒体类型,image/jpeg,image/png,image/* 、video/等等 Scheme:URI模式,http、file、content等,URI无此参数URI无效 Host:URI主机名,www.baidu.com等,URI无此参数URI无效 Port:URI中端口号 Path/PathPrefix/PathPattern:路径信息,path和pathPattern表示完整的路径信息,pahPatten可包含通配符"",PathPrefix路径的前缀信息。 设置方法三种:

mIntent.setType(mType)
mIntent.setData(mUri) 
mIntent.setDataAndType(mUri,mType)

若先setType再setData,mimeType会被清空 若先setData再setType,data会被清空 原因看源码,setType和seData类似

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

如下两种属性同时使用,标明这是一个入口activity,并且会出现在系统应用列表中

<action android:name="android.intent.action.MAIN" />   
<category android:name="android.intent.category.LAUNCHER"/>
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-09-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 我就是马云飞 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • android:configChanges的一些属性
  • 任务栈
  • TaskAffinity属性
  • 四种启动模式
  • 启动标记 Intent Flag
    • 特殊情景一
      • 特殊情景二:
        • 特殊场景三:
          • TaskAffinity与singleTask应用场景
            • TaskAffinity与allowTaskReparenting应用场景
              • 清空任务栈
                • android:clearTaskOnLaunch
                  • android:finishOnTaskLaunch
                    • android:alwaysRetainTaskState
                      • 匹配规则
                        • action
                        • category
                        • data
                      • 属性简介:
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档