Android Activity 重难点解析

一、前言


Activity 作为四大组件之首,是使用最为频繁的一种组件,知识点非常的多并且容易混淆,接下来我们学习一下。

 

二、生命周期


2.1 正常情况下生命周期分析

 如下图,详细地描述了 Activity 各种生命周期的切换过程。

 正常情况下,Activity 的常用生命周期就只有上面 7 个,介绍如下:

1、onCreate:这是生命周期的第一个方法,表示 Activity 正在被创建。我们可以在这个方法中做一些初始化工作,比如调用 setContentView 方法区加载界面布局资源、初始化 Activity 所需数据等。

2、onStart:表示 Activity 正在被启动,即将开始,这时 Activity 已经可见了,但是还没有出现在前台,还无法和用户交互。这个时候其实可以理解为 Activity 已经显示出来了,但是我们还看不到。

3、onResume:表示 Activity 已经可见了,并且出现在前台并开始活动。要注意的是,onStart 和 onResume 都表示 Activity 已经可见,但是 onStart 的时候 Activity 还在后台,onResume 的时候 Activity 才显示到前台。

4、onPause:表示 Activity 正在停止,正常情况下,紧接着 onStop 会被调用。在特殊情况下,如果这个时候快速地再回到当前 Activity,那么 onResume 会被调用。但是这种属于极端情况,用户操作很难重现这一场景。此时可以做一些存储数据、停止动画等工作,但是注意不要太耗时,因为这会影响到新 Activity 的显示,onPause 必须先执行完,新 Activity 的 onResume 才会执行。

5、onStop:表示 Activity 即将停止,可以做一些稍微重量级的回收工作,同样不能太耗时。

6、onRestart:表示 Activity 正在重新启动。一般情况下,当当前 Activity 从不可见重新变为可见状态时,onRestart 就会被调用,比如用户按 Home 键切换到桌面或者用户打开一个新的 Activity,这时当前的 Activity 就会暂停,也就是 onPause 和 onStop 被执行了,接着用户又回到了这个 Activity,就会出现这种情况。

7、onDestroy:表示 Activity 即将被销毁,这是 Activity 生命周期中的最后一个回调,在这里,我们可以做一些回收工作和最终的资源释放。

 

2.2 异常情况下生命周期分析

我们知道,Activity 除了受用户操作所导致的正常的生命周期方法调度,还有一些异常情况,比如当资源相关的系统配置发生改变以及系统内存不足时,Activity 就可以被杀死。下面我们具体分析这两种情况:

1、情况1:资源相关的系统配置发生改变导致 Activity 被杀死并重新创建

理解这个问题,我们首先要对系统的资源加载机制有一定的了解。拿最简单的图片来说,当我们把一张图片放在 drawable 目录后,就可以通过 Resources 去获取这张图片,同时为了兼容不同的设备,我们可能还需要在其他一些目录放置不同的图片,比如 drawale-mdpi、drawable-land 等,这样,当应用程序启动时,系统就会根据当前设备的情况去加载合适的 Resources 资源,比如说横屏和竖屏会拿到两张不同的图片。当前 Activity 处于竖屏状态,如果突然旋转屏幕,由于系统配置发生了改变,在默认情况下,Activity 就会被销毁并且重新创建。当然我们也可以阻止系统重新创建我们的 Activity。

在默认情况下,如果我们的 Activity 不做特殊处理,那么当系统配置发生改变后,Activity 就会被销毁并重新创建,其生命周期如下:

 当系统配置发生改变后,Activity 会被销毁,其 onPause、onStop、onDestroy 均会被调用,同时由于 Activity 是在异常情况下终止的,系统会调用 onSaveInstanceState 来保存当前 Activity 的状态。这个方法的调用时机是在 onStop 之前,但它和 onPause 没有既定的时序关系,有可能在 onPause 之前调用,也可能在 onPause 之后调用。需要强调的一点是,这个方法只会出现在 Activity 被异常终止的情况下,正常情况下系统不会回调这个方法。

当 Activity 被重新创建后,系统会调用 onRestoreInstanceState 方法,并且把 Activity 销毁时 onSaveInstanceState 方法所保存的 Bundle 对象作为参数同时传递给 onCreate 和 onRestoreInstanceState 方法。因此,我们可以通过 onRestoreInstanceState 和 onCreate 方法来判断 Activity 是否被重建了,如果被重建了,那么我们就可以取出之前保存的数据并恢复,从时序上来说,onRestoreInstanceState 的调用时机在 onStart 之后。

同时,在 onSaveInstanceState 和 onRestoreInstanceState 方法中,系统自动为我们做了一定的恢复工作。当 Activity 在异常情况下需要重新创建时,系统会默认保存当前 Activity 的视图结构,并且在 Activity 重启后为我们恢复这些数据,比如文本框中用户输入的数据、ListView 滚动的位置等,这些 View 相关的状态,系统都能够默认为我们恢复。具体针对某一个特定的 View 系统能为我们恢复哪些数据,我们可以查看 VIew 的源码。和 Activity 一样,每个 View 都有 onSaveInstanceState 和 onRestoreInstanceState 这两个方法,看一下它们的具体实现,就能知道系统能够为每个 View 恢复哪些数据。

关于保存和恢复 View 层次结构,系统的工作流程是这样的:首先 Activity 被意外终止时,Activity 会调用 onSaveInstanceState 去保存上数据,然后 Activity 会委托 Window 去保存数据,接着 Window 再委托它上面的顶级容器去保存数据。顶层容器是一个 ViewGroup,一般来说它可能是 DecorView。最后顶层容器再去一一通知它的子元素来保存数据,这样整个数据保存过程就完成了。

接收的位置可以选择 onRestoreInstanceState 或者 onCreate,二者的区别是:onRestoreInstanceState 一旦被调用,其参数 Bundle saveInstanceState 一定是有值的,我们不用额外地判断是否为空,但是 onCreate 不行,onCreate 如果是正常启动的话,其参数 Bundle saveInstanceState 为 null,所以必须要额外判断。这两个方法我们选择任意一个都可以进行数据恢复,但是官方文档的建议是采用 onRestoreInstanceState 去恢复数据。

static final String STATE_SCORE = "playerScore";
static final String STATE_LEVEL = "playerLevel";

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState); // 记得总是调用父类
   
    // 检查是否正在重新创建一个以前销毁的实例
    if (savedInstanceState != null) {
        // 从已保存状态恢复成员的值
        mCurrentScore = savedInstanceState.getInt(STATE_SCORE);
        mCurrentLevel = savedInstanceState.getInt(STATE_LEVEL);
    } else {
        // 可能初始化一个新实例的默认值的成员
    }
    
}

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    // 保存用户自定义的状态
    savedInstanceState.putInt(STATE_SCORE, mCurrentScore);
    savedInstanceState.putInt(STATE_LEVEL, mCurrentLevel);
    
    // 调用父类交给系统处理,这样系统能保存视图层次结构状态
    super.onSaveInstanceState(savedInstanceState);
}

public void onRestoreInstanceState(Bundle savedInstanceState) {
    // 总是调用超类,以便它可以恢复视图层次超级
    super.onRestoreInstanceState(savedInstanceState);
   
    // 从已保存的实例中恢复状态成员
    mCurrentScore = savedInstanceState.getInt(STATE_SCORE);
    mCurrentLevel = savedInstanceState.getInt(STATE_LEVEL);
}

 针对 onSaveInstanceState 方法还有一点需要说明,那就是系统只会在 Activity 即将被销毁并且有机会重新显示的情况下才会去调用它。考虑这么一种情况,当 Activity 正常销毁的时候,系统不会调用 onSaveInstanceState,因为被销毁的 Activity 不可能再次被显示。这句话不好理解,但是我们可以对比一下旋转屏幕所造成的 Activity 异常销毁,这个过程和正常停止 Activity 是不一样的,因为旋转屏幕后,Activity 被销毁的同时会立刻创建新的 Activity 实例,这个时候 Activity 有机会再次立即展示,所以系统要进行数据存储。这里可以简单地这么理解,系统只在 Activity 异常终止的时候才会调用 onSaveInstanceState 和 onRestoreInstanceState 来存储和恢复数据,其他情况不会触发这个过程。

2、情况2:资源内存不足导致低优先级的 Activity 被杀死

这种情况不好模拟,但是其数据存储和恢复过程和情况 1 完成一致。这里我们描述一下 Activity 的优先级情况。Activity 按照优先级从高到低,可以分为如下三种:

(1)前台 Activity:正在和用户交互的 Activity,优先级最高。

(2)可见但非前台 Activity:比如 Activity 中弹出了一个对话框,导致 Activity 可见但是位于后台无法和用户直接交互。

(3)后台 Activity:已经被暂停的 Activity,比如执行了 onStop,优先级最低。

当系统内存不足时,系统就会按照上述优先级去杀死目标 Activity 所在的进程,并在后续通过 onSaveInstanceState 和 onRestoreInstanceState 来存储和恢复数据。如果一个进程中没有四大组件在执行,那么这个进程将很快被系统杀死,因此,一些后台工作不适合脱离四大组件而独立运行在后台中,这样进程很容易被杀死。比较好的方法是将后台工作放入 Service 中从而保证进程有一定的优先级,这样就不会轻易地被系统杀死。

当系统配置发生改变后,Activity 会被重新创建。系统配置有很多内容,如果当某项内容发生改变后,我们不想让系统重新创建 Activity,可以给 Activity 指定 configChanges 属性。如下:

比如当屏幕方向发生改变时,Activity会被销毁重建,如果在 AndroidManifest 文件中处理屏幕方向配置信息如下:

<activity
    android:name=".MainActivity"
    android:label="@string/app_name"
    android:configChanges="orientation|screenSize" />

则 Activity 不会被销毁重建,而是调用 onConfigurationChanged 方法。

注意:横竖屏切换的属性是 orientation。如果 targetSdkVersion 的值大于等于13,则如下配置才会回调onConfigurationChanged 方法。

android:configChanges="orientation|screenSize"

如果targetSdkVersion的值小于13,则只要配置

android:configChanges="orientation"

 

三、启动模式


3.1 Activity 的 LaunchMode

除了 Activity 的生命周期外,Activity 的启动模式也是一个难点,形形色色的启动模式和标志位实在太容易被混淆了。但它又非常重要,有时候为了满足项目的特殊需求,就必须使用 Activity 的启动模式。

在默认启动模式情况下,当我们多次启动同一个 Activity 的时候,系统会创建多个实例并把它们一一放到任务栈中,当我们单击 back 键,会发现这些 Activity 会一一回退。任务栈是一种 “后进先出” 的栈结构,每按一下 back 键就会有一个 Activity 出栈,直到栈空为止,当栈中没有任何 Activity 的时候,系统就会回收这个任务栈。下面我们看看各种启动模式:

1、standard:标准模式,这也是系统的默认模式。每次启动一个 Activity ,不管这个实例是否已经存在,都会重新创建一个新的实例。这是一种典型的多实例实现,一个任务栈中可以有多个实例,每个实例也可以属于不同的任务栈。在这种模式下,谁启动了这个 Activity ,那么这个 Activity 就运行在启动它的那个 Activity 所在的栈中。

 注意,当我们用 ApplicationContext 去启动 standard 模式的 Activity 的时候会报如下错误:

android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?

 这是因为standard 模式的 Activity 默认会进入启动它的 Activity 所属的任务栈中,但是由于非 Activity 类型的 Context(如 ApplicationContext)并没有所谓的任务栈,所以这就有问题了。解决这个问题的办法就是为待启动 Activity 指定 FLAG_ACTIVITY_NEW_TASK 标记位,这样启动得时候就会为它创建一个新的任务栈,这个时候待启动的 Activity 实际上是以 singleTask 模式启动的。

intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 
context.startActivity(intent);

2、singleTop:栈顶复用模式。在这种模式下,如果新 Activity 已经位于任务栈的栈顶,那么此 Activity 不会被重新创建,同时它的 onNewIntent 方法会被回调,通过此方法的参数可以获取当前请求的 Intent 信息。如果新 Activity 的实例已存在栈内,但不是位于栈顶,那么新 Activity 仍然会重新创建。

举个例子,假设目前栈内的情况为 ABCD 四个 Activity,A 位于栈底,D 位于栈顶。这个时候要再次启动 D,如果 D 的启动模式为 singleTop,那么栈内的情况仍然是 ABCD。如果 D 的启动模式是 standard,那么 D 会被重新创建,栈内的情况就变为 ABCDD 了。

3、singleTask:栈内复用模式。这是一种单实例模式,在这种模式下,只要 Activity 在目标任务栈中存在,那么启动此 Activity 不会重新创建实例。和 singleTop 一样,系统也会回调其 onNewIntent 方法。具体一点,当一个具有 singleTask 模式的 A(Activity)请求启动后,系统首先会寻找是否存在 A 想要的任务栈,如果不存在就重新创建一个任务栈,然后创建 A 的实例后把 A 放到栈中。如果存在 A 所需的任务栈,这时要看 A 是否在栈中有实例存在,如果有实例存在,那么系统就会把 A 调到栈顶并调用它的 OnNewIntent 方法,如果实例不存在,就创建 A 的实例并把 A 压入栈中。

举个例子,比如目前任务栈 S1 中的情况为 ABC,这个时候 Activity D 以 singleTask 模式请求启动,其所需要的任务栈为 S2,由于 S2 和 D 的实例均不存在,所以系统会先创建任务栈 S2,然后再创建 D 的实例并将其入栈到 S2。

上述例子中,假设 D 所需的任务栈为 S1,那么由于 S1 已经存在,所以系统会直接创建 D 的实例并将其入栈到 S1。

如果 D 所需的任务栈为 S1,并且当前任务栈 S1 的情况为 ADBC,根据栈内复用的原则,此时 D 不会重新创建,系统会把 D 切换到栈顶并调用其 onNewIntent 方法,同时由于 singleTask 默认具有 clearTop 的效果,会导致栈内所有在 D 上面的 Activity 全部出栈,于是最终 S1 中的情况为 AD。

4、singleInstance:单实例模式。这是一种加强的 singleTask 模式,它除了具有 singleTask 模式的所有特性外,还加强了一点,那就是具有此种模式的 Activity 只能单独地位于一个任务栈中。

比如 Activity A 是 singleInstance 模式,当 A 启动后,系统会为它创建一个新的任务栈,然后 A 独自在这个新的任务栈中,由于栈内复用的特性,后续的请求均不会创建新的 Activity,除非这个独特的任务栈被系统销毁了。

 

任务栈分为前台任务栈和后台任务栈,后台任务栈中的 Activity 处于暂停状态,用户可以通过切换将后台任务栈再次调到前台。

在 singleTask 启动模式中,多次提到某个 Activity 所需的任务栈,这要从一个参数说起:TaskAffinity,这个参数标识了一个 Activity 所需要的任务栈的名字。默认情况下,所有 Activity 所需的任务栈的名字为应用的包名,我们也可以为每个 Activity 都单独指定 TaskAffinity 属性,这个属性值不能和包名相同,否则就相当于没有指定。TaskAffinity 属性主要和 singleTask 启动模式或者 allowTaskReparenting 属性配对使用,在其他情况下没有意义。

当 TaskAffinity 和 singleTask 启动模式配对使用的时候,它是具有该模式的 Activity 的目标任务栈的名字,待启动的 Activity 会运行在名字和 TaskAffinity 相同的任务栈中。

当 TaskAffinity 和 allowTaskReparenting 结合的时候,这种情况比较特殊,会产生特殊的效果。当一个应用 A 启动了应用 B 的某个 Activity 后,如果这个 Activity 的 allowTaskReparenting 属性为 true 的话,那么当应用 B 被启动后,此 Activity 会直接从应用 A 的任务栈转移到应用 B 的任务栈中。这还是很抽象,再具体点,比如现在有 2 个应用 A 和 B,A 启动了 B 的一个 Activity C,然后按 Home 键回到桌面,然后再点击 B 的桌面图标,这个时候并不是启动了 B 的主 Activity,而是重新显示了已经被应用 A 启动的 Activity C,或者说,C 从 A 的任务栈转移到了 B 的任务栈中。可以这么理解,由于 A 启动了 C,这个时候 C 只能运行在 A 的任务栈中,但是 C 属于 B 应用,正常情况下,它的 TaskAffinity 值肯定不可能和 A 的任务栈相同(因为包名不同)。所以,当 B 被启动后,B 会创建自己的任务栈,这个时候系统发现 C 原本所想要的任务栈已经被创建了,所以就把 C 从 A 的任务栈中转移过来了。

如何给 Activity 指定启动模式呢?有两种方法,第一种是通过 AndroidMenifest 为 Activity 指定启动模式,如下:

<activity
    android:name="com.lerendan.SecondActivity"
    android:launchMode="singleTask" />

另一种是通过在 Intent 中设置标志位来为 Activity 指定启动模式,如下:

Intent intent = new Intent();
intent.setClass(MainActivity.this, SecondActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

 这两种方式都可以为 Activity 指定启动模式,但是二者还是有区别的。首先,第二种方式的优先级要高于第一种,当两种同时存在时,以第二种方式为准;其次,上述两种方式在限定范围上有所不同,比如,第一种方式无法直接为 Activity 设定 FLAG_ACTIVITY_CLEAR_TOP标识,而第二种方式无法为 Activity 指定 singleInstance 模式。

3.2 Activity 的 Flags

Activity 的 Flags 有很多,这里主要分析一些比较常用的标志位。下面主要介绍几个比较常用的标志位,剩下的大家可以查看官方文档去了解。大部分情况下,我们不需要为 Activity 指定标志位,因此,对于标志位理解即可。在使用标志位的时候,要注意有些标志位是系统内部使用的,应用程序不需要手动设置这些标志位以防出现问题。

FLAG_ACTIVITY_NEW_TASK

为 Activity 指定 singleTask 启动模式,其效果和在 XML 中指定该启动模式相同。

FLAG_ACTIVITY_SINGLE_TOP

为 Activity 指定 singleTop 启动模式,其效果和在 XML 中指定该启动模式相同。

FLAG_ACTIVITY_CLEAR_TOP

具有此标志位的 Activity,当它启动时,在同一个任务栈中所有位于它上面的 Activity 都要出栈。这个模式一般需要和 FLAG_ACTIVITY_NEW_TASK 配合使用,在这种情况下,被启动 Activity 的实例如果已经存在,那么系统就会调用它的 onNewIntent。如果被启动的 Activity 采用 standard 模式启动,那么它连同它之上的 Activity 都要出栈,系统会创建新的 Activity 实例并放入栈顶。singleTask 启动模式默认就具有此标志位的效果。

FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS

它等同于在 XML 中指定 Activity 的属性 android:excludeFromRecents="true"。具有这种标志的 Activity 不会出现在历史 Activity 的列表中,当某些情况下我们不希望用户通过历史列表(多任务键)回到我们的 Activity 的时候这个标志比较有用。

 

四、IntentFilter


 Intent解析机制主要是通过查找已注册在AndroidManifest.xml中的所有IntentFilter及其中定义的Intent,最终找到匹配的Intent。在这个解析过程中,Android是通过Intent的action、type、category这三个属性来进行匹配判断的。一个过滤列表中的action、type、category可以有多个,所有的action、type、category分别构成不同类别,同一类别信息共同约束当前类别的匹配过程。只有一个Intent同时匹配action、type、category这三个类别才算完全匹配,只有完全匹配才能启动Activity。另外一个组件若声明了多个Intent Filter,只需要匹配任意一个即可启动该组件。 

4.1 action的匹配规则

action 是一个字符串,系统预定义了一些 action,同时我们也可以在应用中定义自己的 action。如果 Intent 指明了 action,则目标组件的 IntentFilter 的 action 列表中就必须包含有这个 action,否则不能匹配。一个 Intent Filter 中可声明多个 action,Intent 中的 action 与其中的任一个 action 在字符串形式上完全相同(注意,区分大小写,大小写不同但字符串内容相同也会造成匹配失败),action 方面就匹配成功。可通过 setAction 方法为 Intent 设置 action,也可在构造 Intent 时传入 action。需要注意的是,隐式 Intent 必须指定 action。比如我们在 Manifest 文件中为 MyActivity 定义了如下 Intent Filter:

<intent-filter>
    <action android:name="android.intent.action.SEND"/>
    <action android:name="android.intent.action.SEND_TO"/>
</intent-filter>

那么只要 Intent 的 action 为 “SEND” 或 “SEND_TO”,那么这个 Intent 在 action 方面就能和上面那个 Activity 匹配成功。比如我们的 Intent 定义如下就可以与 MyActivity 匹配成功了:

Intent intent = new Intent("android.intent.action.SEND") ;
startActivity(intent);

4.2 category的匹配规则

category 也是一个字符串,系统预定义了一些 category,同时我们也可以在应用中定义自己的 category。但是它与 action 的匹配规则不同,它要求 Intent 中如果含有 category,那么所有的 category 都必须和过滤规则中的其中一个 category 相同。也换句话说,Intent 中如果出现了 category,不管有几个 category,对于每个 category 来说,它必须是过滤规则中已经定义了的category。当然,Intent 中也可以没有 category(若 Intent 中未指定 category,系统会自动为它带上 “android.intent.category.DEFAULT”),如果没有,仍然可以匹配成功。category 和 action 的区别在于,action 要求 Intent 中必须有一个 action 且必须和过滤规则中的某几个action相同,而category 要求 Intent 可以没有 category,但是一旦发现存在 category,不论你有几个,每个都要能够和过滤规则中的任何一个 category 相同。我们可以通过 addCategory 方法为 Intent 添加 category。为了我们的 Activity 能够接收隐式调用,就必须在 intent-filter 中指定 "android.intent.category.DEFAULT" 这个 category。

4.3 data的匹配规则

如果Intent没有提供type,系统将从data中得到数据类型。和action一样,同action类似,只要Intent的data只要与Intent Filter中的任一个data声明完全相同,data方面就完全匹配成功。

data由两部分组成:mimeType和URI。

MineType指的是媒体类型:例如imgage/jpeg,auto/mpeg4和viedo/*等,可以表示图片、文本、视频等不同的媒体格式 
uri则由scheme、host、port、path | pathPattern | pathPrefix这4部分组成:

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

例如:content://com.wooyun.org:200/folder/etc 

Intent 的 uri 可通过 setData 方法设置,mimetype 可通过 setType 方法设置。 需要注意的是:若 Intent Filter 的 data 声明部分未指定 uri,则缺省 uri 为 content 或 file,Intent 中的 uri 的 scheme 部分需为 content 或 file 才能匹配;若要为 Intent 指定完整的 data,必须用 setDataAndType 方法,究其原因在,setData 和 setType 方法的源码中我们发现:

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

 这两个方法会彼此互相清除对方的值,即 setData 会把 mimeType 置为 null,setType 会把uri 置为 null。下面我们来举例说明一下 data 的匹配。首先我们先来看一下 Intent Filter 中指定 data 的语法:

<data 
    android:scheme="String.“ 
    android:host="String"
    android:port="String"
    android:path="String"
    android:pathPattern="String"
    android:pathPrefix="String"
    android:mimeType="String"/>
其中scheme、host等各个部分无需全部指定。

使用案例: 

1、如果我们想要匹配 http 以 ".pdf" 结尾的路径,使得别的程序想要打开网络 pdf 时,用户能够可以选择我们的程序进行下载查看。 我们可以将 scheme 设置为 “http”,pathPattern 设置为 “.*//.pdf”,整个 intent-filter 设置为:

<intent-filter>  
    <action android:name="android.intent.action.VIEW"></action>  
    <category android:name="android.intent.category.DEFAULT"></category>  
    <data android:scheme="http" android:pathPattern=".*//.pdf"></data>  
</intent-filter>

 如果你只想处理某个站点的 pdf,那么在 data 标签里增加 android:host=”yoursite.com” 则只会匹配 http://yoursite.com/xxx/xxx.pdf,但这不会匹配 www.yoursite.com,如果你也想匹配这个站点的话,你就需要再添加一个 data 标签,除了 android:host 改为 “www.yoursite.com” 其他都一样。

2、如果我们做的是一个IM应用,或是其他类似于微博之类的应用,如何让别人通过 Intent 进行调用出现在选择框里呢?我们只用注册 android.intent.action.SEND 与 mimeType 为 “text/plain” 或 “/” 就可以了,整个 intent-filter 设置为:

<intent-filter>  
    <action android:name="android.intent.action.SEND" />  
    <category android:name="android.intent.category.DEFAULT" />  
    <data mimeType="*/*" />  
</intent-filter>

这里设置 category 的原因是,创建的 Intent 的实例默认 category 就包含了 Intent.CATEGORY_DEFAULT ,google 这样做的原因是为了让这个 Intent 始终有一个 category。

3、如果我们做的是一个音乐播放软件,当文件浏览器打开某音乐文件的时候,使我们的应用能够出现在选择框里?这类似于文件关联了,其实做起来跟上面一样,也很简单,我们只用注册 android.intent.action.VIEW 与 mimeType 为 “audio/*” 就可以了,整个 intent-filter 设置为:

<intent-filter>  
     <action android:name="android.intent.action.VIEW" />  
     <category android:name="android.intent.category.DEFAULT" />  
     <data android:mimeType="audio/*" />  
</intent-filter> 

 

 

五、setResult() 调用时机


Activity 中有一个 startActivityForResult() 方法用于启动 Activity,并期望在 Activity 销毁的时候能够返回一个结果给上一个 Activity。以下是如何返回结果给上一个 Activity 的代码:

FirstActivity

...
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
        case 1:
            if (resultCode == RESULT_OK) {
                String returnedData = data.getStringExtra("data_return");
                Log.d("FirstActivity", returnedData);
            }
            break;
        default:
    }
}

button1.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
        //startActivityForResult()方法接收两个参数,第一个参数还是 Intent,第二个参数是请求
        //码,用于在之后的回调中判断数据的来源
        startActivityForResult(intent, 1);
    }
});


SecondActivity

...
button2.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent();
        intent.putExtra("data_return", "Hello FirstActivity");
        //setResult()方法接收两个参数 ,第一个参数用于向上一个活动返回处理结果,一般只使用            
        //RESULT_OK 或 RESULT_CANCELED 这两个值,第二个参数则是把带有数据的 Intent 传递回去,    
        //然后调用了 finish() 方法来销毁当前活动。
        setResult(RESULT_OK, intent);
        finish();
    }
});

现在我们分析一下这个问题,在 Activity A 中用 startActivityForResult() 方法启动了 Activity B,并且在 B 中通过 setResult() 方法给 A 返回值,由于某些原因不能在 setResult() 之后立刻调用 finish() 函数,只能通过用户按 Back 键自己退出到A。按理说从 B 退出回到 Aactivity A 过程中,A 中的 onActivityResult() 应该被调用, 可是通过 log 发现,整个操作过程中 onActivityResult() 始终没有被调用。 前后研究了半天才发现 是 setResult() 的调用时机不对造成的,因为在我是在 B 的 onStop()  函数中调用 setResult() 函数的,这个时候的 seResult 是没有任何意义的,因为已经错过了A onActivityResult() 的调用时机。

在 B 退回 A 过程中,执行过程是这样的:

  B---onPause
  A---onActivityResult
  A---onRestart
  A---onStart
  A---onResume
  B---onStop
  B---onDestroy

从上面过程可以看出,首先是 B 处于 Pause 状态,然后等待 A 执行 onRestart、onStart、onResume,然后才是 B 的onStop、onDestroy,而 A 的 onActivityResult() 需要在 B 的 onPause 之后,A 的 onRestart 之前这中间调用,所以 B 中的setResult() 函数应该放在 B 的 onPause 之前调用。

另外,如果把 setResult() 放在 B 的 onPause() 里面调用,结果仍然是无效的。那么 setResult() 应该在什么时候调用呢?从源码可以看出,Activity 返回 result 是在被 finish 的时候,也就是说调用setResult() 方法必须在 finish() 之前。所以在 onPause、onStop、onDestroy 方法中调用 setResult() 也有可能不会返回成功,因为这些方法调用不一定是在finish之前的,当然在onCreate()就调用setResult肯定是在finish之前的,但是又不满足业务需要。

实际使用场景有两个:

1、按 BACK 键从一个 Activity 退出来的,一按 BACK,android 就会自动调用 Activity 的 finish() 方法。方法是重写 onBackPressed() 方法,捕获 BACK 事件,捕获到之后先 setResult。代码如下:

@Override
public void onBackPressed()
{
    setResult(RESULT_OK);
    super.onBackPressed();
}

2、按点击事件中显式的调用 finish()。

setResult(RESULT_OK);
finish();

执行过程如下:

       B---onBackPressed
  B---finish
  B---onPause
  A---onActivityResult
  A---onRestart
  A---onStart
  A---onResume
  B---onStop
  B---onDestroy

 

六、Activity切换时生命周期顺序

两个 Activity,从一个 Activity 通过 Intent 切换到另一个 Activity:

MainActivity------->onPause()
Another------->onCreate()
Another------->onStart()
Another------->onResume()
MainActivity------->onStop()

按Back键返回:

Another------->onPause()
MainActivity------->onRestart()
MainActivity------->onStart()
MainActivity------->onResume()
Another------->onStop()
Another------->onDestroy()

第二个 Activity 使用了 finish() 方法,返回:

Another------->onPause()
MainActivity------->onRestart()
MainActivity------->onStart()
MainActivity------->onResume()
Another------->onStop()
Another------->onDestroy()

但是在当前 Activity 调出透明的窗口或者是对话框的样式, 就不会调用它的 onStop() 方法。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值