专栏首页Android技术干货Activity 的启动方式和 flag 详解
原创

Activity 的启动方式和 flag 详解

1、Activity 的 4 种状态

活动的:Activity 在栈顶,它是可视、有焦点、可接受用户输入的。Android 试图尽最大可能保持它活动状态,杀死其它 Activity 来确保当前活动 Activity 有足够的资源可使用。 当另外一个 Activity 被激活,这个将会被暂停。

暂停:Activity 可视,但是它没有焦点, 换句话说它被暂停了 。可能的原因是一个透明或者非全屏的 Activity 被激活。 当被暂停,一个 Activity 仍会当成活动状态,只不过是不可以接受用户输入。在极特殊的情况下,Android 将会杀死一个暂停的 Activity 来为活动的 Activity 提供充足的资源。当一个 Activity 变为完全隐藏,它将会变成停止。

停止:当一个 Activity 不是可视的,它“停止”了。这个 Activity 将仍然在内存中保存它所有的状态和会员信息。尽管如此,当其它地方需要内存时,它将是最有可能被释放资源的。 当一个 Activity 停止后,一个很重要的步骤是要保存数据和当前 UI 状态。一旦一个 Activity 退出或关闭了,它将变为待用状态。

待用:在一个 Activity 被杀死后和被装载前,它是待用状态的。待用 Acitivity 会被移除 Activity 栈,并且需要在显示和可用之前重新启动它。

2、Activity 的 4 种加载模式

在 android 的多 activity 开发中,activity 之间的跳转可能需要有多种方式,有时是普通的生成一个新实例,有时希望跳转到原来某个 activity 实例,而不是生成大量的重复的 activity。 加载模式便是决定以哪种方式启动一个跳转到原来某个 Activity 实例。

在 android 里,有 4 种 activity 的启动模式,这些启动模式可以在功能清单文件 AndroidManifest.xml 中进行设置,Activity 中的 launchMode 属性。 分别为:

2.1、standard

标准模式,一调用 startActivity()方法就会产生一个新的实例。

2.2、singleTop

启动 Activity 时每次都创建新的实例,仅一个例外:当栈顶的 activity 恰恰就是该 activity 的实例(即需要创建的实例)时,不再创建新实例。这解决了栈顶复用问题。

2.3、singleTask

谷歌的官方文档上称,如果一个activity的启动模式为singleTask,那么系统总会在一个新任务的最底部(root)启动这个activity,并且被这个activity启动的其他activity会和该activity同时存在于这个新任务中。如果系统中已经存在这样的一个activity则会重用这个实例,并且调用他的onNewIntent()方法。即,这样的一个activity在系统中只会存在一个实例。其实官方文档中的这种说法并不准确,启动模式为singleTask的activity并不会总是开启一个新的任务。

设置singleTask启动模式的activity,它在启动的时候,会先在系统中查找属性值 affinity 等于它的属性值 taskAffinity 的任务存在;如果存在这样的任务,它就会在这个任务中启动,否则就会在新任务中启动。因此,如果我们想要设置了 singleTask 启动模式的 activity 在新的任务中启动,就要为它设置一个独立的 taskAffinity 属性值。如果设置了 singleTask 启动模式的Activity不是在新的任务中启动时,它会在已有的任务中查看是否已经存在相应的 activity 实例,如果存在,就会把位于这个 activity 实例上面的Activity全部结束掉,即最终这个Activity实例会位于任务的堆栈顶端中。解决了在一个 task 中共享一个 activity。这样的一个activity在系统中只会存在一个实例。

2.4、singleInstance

设置了该模式的activity,总是在新的任务中开启,并且这个新的任务中有且只有这一个实例,也就是说被该实例启动的其 activity会自动运行于另一个任务中。当再次启动该activity的实例时,会重用已存在的任务和实例。并且会调用这个实例的onNewIntent()方法,将Intent实例传递到该实例中。和singleTask相同,同一时刻在系统中只会存在一个这样的Activity实例。解决了多个 task 共享一个 activity。

3、实例验证singleTask启动模式

上面将activity的四种启动模式就基本介绍完了。为了加深对启动模式的了解,下面会通过一个简单的例子进行验证。由以上的介绍可知,standard和singleTop这两种启动模式行为比较简单,所以在下面的例子中,会对singleTask着重介绍。

3.1、验证启动singleTask模式的activity时是否会创建新的任务

以下为验证示例AndroidTaskTest。这个实例中有三个Activity,分别为:MainActivity,SecondActivity和ThirdActivity。以下为这个示例的manifest文件。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.jg.zhang.androidtasktest"
    android:versionCode="1"
    android:versionName="1.0" >
 
    <uses-sdk android:minSdkVersion="10" android:targetSdkVersion="17" />
 
    <application android:icon="@drawable/ic_launcher" android:label="@string/app_name">
        <activity  android:label="@string/app_name"
            android:name="com.jg.zhang.androidtasktest.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        	
         <activity android:name="com.jg.zhang.androidtasktest.SecondActivity"
             android:launchMode="singleTask">
            <intent-filter >
                <action android:name="com.jg.zhang.androidtasktest.SecondActivity"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
        </activity>
        
         <activity android:name="com.jg.zhang.androidtasktest.ThirdActivity"
            android:label="@string/app_name" >
        </activity>
    </application>
    
</manifest>

由代码可见,MainActivity和ThirdActivity都是标准的启动模式,而SecondActivity的启动模式为singleTask。

测试逻辑很简单,在MainActivity中点击按钮启动SecondActivity,在SecondActivity中点击按钮启动ThirdActivity。并且在onCreate方法中会以log的形式打印出当前activity所属的任务(Task)的Id。

int taskId = getTaskId();
Log.i(LOG_TAG, ACTIVITY_NAME +"所在的任务的id为: " +  taskId);

其他代码就不展示了。

运行该示例,并且点击MainActivity界面中的按钮,开启SecondActivity。在该示例中SecondActivity的启动模式为singleTask。按照官方文档的说法,SecondActivity会在一个新的任务中开启。但是查看打印出的log,发现MainActivity和SecondActivity所在的任务的Id相同。

所以,和官方文档表述的不同,MainActivity和SecondActivity是启动在同一个任务中的。其实,把启动模式设置为singleTask,framework在启动该activity时只会把它标示为可在一个新任务中启动,至于是否在一个新任务中启动,还要受其他条件的限制。现在在SecondActivity增加一个taskAffinity属性,如下所示:

         <activity android:name="com.jg.zhang.androidtasktest.SecondActivity"
             android:launchMode="singleTask"
             android:taskAffinity="com.jg.zhang.androidtasktest.second">
            <intent-filter >
                <action android:name="com.jg.zhang.androidtasktest.SecondActivity"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
        </activity>

重新运行该示例,执行相同的操作,即:点击MainActivity界面中的按钮,开启SecondActivity,并且点击SecondActivity中的按钮,启动ThirdActivity,log中输出的内容为:

由此可见,MainActivity和SecondActivity运行在不同的任务中了,并且被SecondActivity启动的ThirdActivity和SecondActivity运行在同一个任务中。

在这里便引出了manifest文件中<activity>的一个重要属性,taskAffinity。在官方文档中可以得到关于taskAffinity的以下信息:

  1. taskAffinity表示当前activity具有亲和力的一个任务(翻译不是很准确,原句为The task that the activity has an affinity for.),大致可以这样理解,这个 taskAffinity表示一个任务,这个任务就是当前activity所在的任务。
  2. 在概念上,具有相同的affinity的activity(即设置了相同taskAffinity属性的activity)属于同一个任务。
  3.  一个任务的affinity决定于这个任务的根activity(root activity)的taskAffinity。
  4.  这个属性决定两件事:当activity被re-parent时,它可以被re-paren哪个任务中;当activity以FLAG_ACTIVITY_NEW_TASK标志启动时,它会被启动到哪个任务中。(这个比较难以理解,请结合<activity>中的属性allowTaskReparenting和Intent中的标志 FLAG_ACTIVITY_NEW_TASK加以理解)
  5.  默认情况下,一个应用中的所有activity具有相同的taskAffinity,即应用程序的包名。我们可以通过设置不同的taskAffinity属性给应用中的activity分组,也可以把不同的应用中的activity的taskAffinity设置成相同的值。
  6.  为一个activity的taskAffinity设置一个空字符串,表明这个activity不属于任何task。

这就可以解释上面示例中的现象了,由第5条可知,MainActivity和SecondActivity具有不同的taskAffinity,MainActivity的taskAffinity为com.jg.zhang.androidtasktest,SecondActivity的taskAffinity为com.jg.zhang.androidtasktest.second,根据上面第4条,taskAffinity可以影响当 activity 以 FLAG_ACTIVITY_NEW_TASK 标志启动时,它会被启动到哪个任务中。这句话的意思是,当新启动的activity(SecondActivity)是以 FLAG_ACTIVITY_NEW_TASK 标志启动时(可以认为FLAG_ACTIVITY_NEW_TASK 和singleTask 作用相同,当启动模式为singleTask时,framework会将它的启动标志设为FLAG_ACTIVITY_NEW_TASK),framework会检索是否已经存在了一个affinity为com.jg.zhang.androidtasktest.second的任务(即一个TaskRecord对象):

  • 如果存在这样的一个任务,则检查在这个任务中是否已经有了一个SecondActivity的实例:
    • 如果已经存在一个SecondActivity的实例,则会重用这个任务和任务中的SecondActivity实例,将这个任务调到前台,清除位于SecondActivity上面的所有Activity,显示SecondActivity,并调用SecondActivity的onNewIntent()。
    • 如果不存在一个SecondActivity的实例,会在这个任务中创建SecondActivity的实例,并调用onCreate()方法
  • 如果不存在这样的一个任务,会创建一个新的,affinity 为com.jg.zhang.androidtasktest.second的任务,并且将SecondActivity启动到这个新的任务中。

上面讨论的是设置taskAffinity属性的情况,如果SecondActivity只设置启动模式为singleTask,而不设置taskAffinity,即三个Activity的taskAffinity相同,都为应用的包名,那么SecondActivity是不会开启一个新任务的,framework中的判定过程如下:

  1. 在MainActivity启动SecondActivity时,发现启动模式为singleTask,那么设定他的启动标志为FLAG_ACTIVITY_NEW_TASK
  2.  然后获得SecondActivity的taskAffinity,即为包名com.jg.zhang.androidtasktest。
  3. 检查是否已经存在一个affinity为com.jg.zhang.androidtasktest的任务,这个任务是存在的,就是MainActivity所在的任务,这个任务是在启动MainActivity时开启的。
  4.  既然已经存在这个任务,就检索在这个任务中是否存在一个SecondActivity的实例,发现不存在。
  5.  在这个已有的任务中启动一个SecondActivity的实例

为了作一个清楚的比较,列出SecondActivity启动模式设为singleTask,并且taskAffinity设为com.jg.zhang.androidtasktest.second 时的启动过程:

  1. 在MainActivity启动SecondActivity时,发现启动模式为singleTask,那么设定他的启动标志为FLAG_ACTIVITY_NEW_TASK
  2. 然后获得SecondActivity的taskAffinity,即com.jg.zhang.androidtasktest.second
  3. 检查是否已经存在一个affinity为com.jg.zhang.androidtasktest.second的任务,这个任务是不存在的
  4. 创建一个新的affinity为com.jg.zhang.androidtasktest.second的任务,并且将SecondActivity启动到这个新的任务中

其实framework中对任务和activity 的调度是很复杂的,尤其是把启动模式设为singleTask或者以 FLAG_ACTIVITY_NEW_TASK标志启动时。所以,在使用singleTask和FLAG_ACTIVITY_NEW_TASK时,要仔细测试应用程序。这也是官方文档上的建议。

3.2、实例验证将两个不同app中的不同的singleTask模式的Activity的taskAffinity设成相同

行时分配到同一任务中,下面对此进行验证,在这里,会使用上面的示例AndroidTaskTest,并创建一个新的示例AndroidTaskTest1。AndroidTaskTest1由两个activity组成,分别为MianActivity和OtherActivity,在MianActivity中点击按钮会启动OtherActivity,该程序的界面和上一个类似,代码也类似,再此仅列出清单文件。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.jg.zhang.androidtasktest1"
    android:versionCode="1"  android:versionName="1.0" >
 
    <uses-sdk android:minSdkVersion="9"  android:targetSdkVersion="17" />
 
    <application
        android:allowBackup="true" android:icon="@drawable/ic_launcher" 
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.jg.zhang.androidtasktest1.MainActivity"
            android:label="com.jg.zhang.androidtasktest1.MainActivity" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <activity
            android:name="com.jg.zhang.androidtasktest1.OtherActivity"
            android:label="com.jg.zhang.androidtasktest1.OtherActivity"
            android:taskAffinity="com.jg.zhang.androidtasktest.second"
            android:launchMode="singleTask">
        </activity>
    </application>
 
</manifest>

可以看到OtherActivity的启动模式被设置为singleTask,并且taskAffinity属性被设置为com.jg.zhang.androidtasktest.second,这和AndroidTaskTest应用中的SecondActivity相同。现在将这两个应用安装在设备上。执行以下操作:

启动AndroidTaskTest应用,在它的MianActivity中点击按钮开启SecondActivity,由上面的介绍可知secondActivity是运行在一个新任务中的,这个任务就是com.jg.zhang.androidtasktest.second。

然后按Home键回到Launcher,启动AndroidTaskTest1,在启动AndroidTaskTest1的入口Activity(MianActivity)时,会自动启动新的任务,那么现在一共有三个任务,AndroidTaskTest的MianActivity和SecondActivity分别占用一个任务,AndroidTaskTest1的MianActivity也占用一个任务。

在AndroidTaskTest1的MianActivity中点击按钮启动OtherActivity,那么这个OtherActivity是在哪个任务中呢?

所以由此可见,AndroidTaskTest的SecondActivity和AndroidTaskTest1的OtherActivity是在同一任务中的。AndroidTaskTest和AndroidTaskTest1这两个应用程序会开启两个进程,他们的所有组件分别运行在独立的进程中。com.jg.zhang.androidtasktest.second任务中的两个activity属于不同的应用,并且运行在不同的进程中,这也说明了一个问题:任务(Task)不仅可以跨应用(Application),还可以跨进程(Process)。

4、影响加载模式的一些特性

核心的 Intent Flag 有:

FLAG_ACTIVITY_NEW_TASK、FLAG_ACTIVITY_CLEAR_TOP、FLAG_ACTIVITY_RESET_TASK_IF_NEEDED、FLAG_ACTIVITY_SINGLE_TOP

核心的特性有:

taskAffinity、launchMode、allowTaskReparenting、clearTaskOnLaunch、alwaysRetainTaskState、finishOnTaskLaunch

5、Intent 常用标识

Activity 在 Activity 栈(Task)中的加载顺序是可以控制的,这就需要用到 Intent Flag。

FLAG_ACTIVITY_NEW_TASK

如果设置,这个 Activity 会成为历史 stack 中一个新 Task 的开始。一个 Task(从启动它的Activity 到下一个 Task 中的 Activity)定义了用户可以迁移的 Activity 原子组。Task 可以移动 到前台和后台;在某个特定 Task 中的所有 Activity 总是保持相同的次序。

这个标志一般用于呈现“启动”类型的行为:它们提供用户一系列可以单独完成的事情, 与启动它们的 Activity 完全无关。

使用这个标志,如果正在启动的 Activity 的 Task 已经在运行的话,那么,新的 Activity 将 不会启动;代替的,当前 Task 会简单的移入前台。参考 FLAG_ACTIVITY_MULTIPLE_TASK 标志, 可以禁用这一行为。

这个标志不能用于调用方对已经启动的 Activity 请求结果。

FLAG_ACTIVITY_SINGLE_TOP

如果设置,当这个 Activity 位于历史 stack 的顶端运行时,不再启动一个新的。

FLAG_ACTIVITY_CLEAR_TOP

当启动一个activity设置了该flag,如果要启动的这个 Activity 已经在当前的 Task 中运行:

  • 当该activity在当前任务栈的最上面时:
    • 如果它的启动模式声明为 “multiple”(默认值), 并且没有在这个 Intent 中设置 FLAG_ACTIVITY_SINGLE_TOP 标志,那么它将关闭然后重新创建。
    • 对于其它的启动模式,或者在这个 Intent 中设置 FLAG_ACTIVITY_SINGLE_TOP 标志,都将把这个 Intent 投递到当前这个实例的 onNewIntent()中。
  • 当该activity不在当前任务栈的最上面时:
    • 则不再是重新启动一个这个 Activity 的实例,而是将这个 Activity 上方的所有 Activity 都将关闭,然后这个 Intent 会作为 一个新的 Intent 投递到老的 Activity(现在位于顶端)中。

这个启动模式还可以与 FLAG_ACTIVITY_NEW_TASK 结合起来使用:用于启动一个 Task 中的根 Activity,它会把那个 Task 中任何运行的实例带入前台,然后清除它直到根 Activity。这非常有用,例如,当从 Notification Manager 处启动一个 Activity。

FLAG_ACTIVITY_CLEAR_TASK

如果设置,在activity启动前,与该activity关联的任务被清空(就是该activity将要加入的任务栈会被清空)。也就是说,新activity成为新任务的根,旧的活动都被结束了。本flag只能与FLAG_ACTIVITY_NEW_TASK联合使用。

FLAG_ACTIVITY_REORDER_TO_FRONT

如果在 Intent 中设置,并传递给 Context.startActivity(),这个标志将引发已经运行的 Activity 移动到历史 stack 的顶端。 例如,假设一个 Task 由四个 Activity 组成:A,B,C,D。如果 D 调用 startActivity()来启动 Activity B ,那么, B 会移动到历史 stack 的顶端,现在的次序变成 A,C,D,B 。如果 FLAG_ACTIVITY_CLEAR_TOP 标志也设置的话,那么这个标志将被忽略。

FLAG_ACTIVITY_BROUGHT_TO_FRONT

这个标志一般不是由程序代码设置的,如在 launchMode 中设置 singleTask 模式时系统帮你设定。

FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET

如果设置,这将在 Task 的 Activity stack 中设置一个还原点,当 Task 恢复时,需要清理 Activity。也就是说,下一次 Task 带着 FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 标记进入前台时(典型的操作是用户在主画面重启它),这个 Activity 和它之上的都将关闭,以至于用户不能再返回到它们,但是可以回到之前的 Activity。

这在你的程序有分割点的时候很有用。例如,一个 e-mail 应用程序可能有一个操作是查看一个附件,需要启动图片浏览 Activity 来显示。这个 Activity 应该作为 e-mail 应用程序 Task 的一部分,因为这是用户在这个 Task 中触发的操作。然而,当用户离开这个 Task,然后从 主画面选择 e-mail app,我们可能希望回到查看的会话中,但不是查看图片附件,因为这让人困惑。通过在启动图片浏览时设定这个标志,浏览及其它启动的 Activity 在下次用户返回 到 mail 程序时都将全部清除。

FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS

如果设置,新的 Activity 不会在最近启动的 Activity 的列表中保存。

FLAG_ACTIVITY_FORWARD_RESULT

如果设置,并且这个 Intent 用于从一个存在的 Activity 启动一个新的 Activity,那么,这个作为答复目标的 Activity 将会传到这个新的 Activity 中。这种方式下,新的 Activity 可以调用 setResult(int),并且这个结果值将发送给那个作为答复目标的 Activity。

FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY

这个标志一般不由应用程序代码设置,如果这个 Activity 是从历史记录里启动的(常按 HOME 键),那么,系统会帮你设定。

FLAG_ACTIVITY_MULTIPLE_TASK

不要使用这个标志,除非你自己实现了应用程序启动器。与 FLAG_ACTIVITY_NEW_TASK 结合起来使用,可以禁用把已存的 Task 送入前台的行为。当设置时,新的 Task 总是会启动来处理 Intent,而不管这是是否已经有一个 Task 可以处理相同的事情。 由于默认的系统不包含图形 Task 管理功能,因此,你不应该使用这个标志,除非你提供给用户一种方式可以返回到已经启动的 Task。 如果 FLAG_ACTIVITY_NEW_TASK 标志没有设置,这个标志被忽略。

FLAG_ACTIVITY_NO_ANIMATION

如果在 Intent 中设置,并传递给 Context.startActivity()的话,这个标志将阻止系统进入下 一个 Activity 时应用 Acitivity 迁移动画。这并不意味着动画将永不运行——如果另一个 Activity 在启动显示之前,没有指定这个标志,那么,动画将被应用。这个标志可以很好的 用于执行一连串的操作,而动画被看作是更高一级的事件的驱动。

FLAG_ACTIVITY_NO_HISTORY

如果设置,新的 Activity 将不再历史 stack 中保留。用户一离开它,这个 Activity 就关闭了。 这也可以通过设置 noHistory 特性。

FLAG_ACTIVITY_NO_USER_ACTION

如果设置,作为新启动的 Activity 进入前台时,这个标志将在 Activity 暂停之前阻止从最 前方的 Activity 回调的onUserLeaveHint()。典型的,一个 Activity 可以依赖这个回调指明显式的用户动作引起的 Activity 移出后台。 这个回调在 Activity 的生命周期中标记一个合适的点,并关闭一些 Notification。如果一个 Activity 通过非用户驱动的事件,如来电或闹钟,启动的,这个标志也应该传递 给 Context.startActivity,保证暂停的 Activity 不认为用户已经知晓其 Notification。

FLAG_ACTIVITY_PREVIOUS_IS_TOP

待补充

FLAG_ACTIVITY_RESET_TASK_IF_NEEDED

待补充

6、注意

如果是从 BroadcastReceiver 启动一个新的 Activity,或者是从 Service 往一个 Activity 跳转时,不要忘记添加 Intent 的 Flag 为 FLAG_ACTIVITY_NEW_TASK。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

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

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Android避坑指南,Gson与Kotlin碰撞出一个不安全的操作

    是的,确实很偏,跳过这个问题,我们往下看,看看是怎么在Android开发过程中遇到的,而且看完后,这个问题就迎刃而解了。

    李林LiLin
  • AsyncTask 相关问题

    AsyncTask 对应的线程池 ThreadPoolExecutor 都是进程范围内共享的,且都是static 的,所以是 Asynctask 控制着进程范围...

    李林LiLin
  • java数据结构与算法-快速排序

    李林LiLin
  • 超详细的生命周期图-你能回答全吗

    超详细的Activity与Fragment的生命周期图,可能大家会说你这篇文章也太水了吧。就这么一个破图。可是我觉得它写的很详细,有些方法是哪些情况下会运行,哪...

    青蛙要fly
  • Android项目实战(十一):moveTaskToBack(boolean ) 方法的使用

    听着music睡
  • Python 多线程 - 共享变量

    从上面两个线程执行的结果来看,线程t1将 g_num 加到 103,在线程t2也是打印g_num=103。所以对于两个线程,g_num这个全局变量是共享的。

    Devops海洋的渔夫
  • hdu 3367(Pseudoforest ) (最大生成树)

    Pseudoforest Time Limit: 10000/5000 MS (Java/Others)    Memory Limit: 65536/6553...

    Gxjun
  • 敏捷开发实践总结

    敏捷开发它是一种指导思想或开发方式,但是它没有明确告诉我们到底采用什么样的流程进行开发,而Scrum和XP就是敏捷开发的具体方式了,你可以采用Scrum方式也可...

    PhoenixZheng
  • iOS·UITableView分割线颜色,隐藏,边距(宽度,起点)等设置

    陈满iOS
  • 资深程序员的敏捷开发实践总结

    敏捷开发它是一种指导思想或开发方式,但是它没有明确告诉我们到底采用什么样的流程进行开发,而Scrum和XP就是敏捷开发的具体方式了,你可以采用Scrum方式也可...

    用户5224393

扫码关注云+社区

领取腾讯云代金券