Activity 必知必会

4,429 阅读12分钟

1. Activity 的生命周期

1.1 分类

在讲解生命周期的方法之前,先放上这张官方的图:

这张图片讲述了 Activity 的回调的方法,下表分类讲解这些方法的作用。

生命周期方法 作用
onCreate 表示 Activity 正在被创建
onRestart 表示 Activity 正在重新启动
onStart 表示 Activity 正在被启动
onResume 表示 Activity 已经可见
onPause 表示 Activity 正在停止
onStop 表示 Activity 即将停止
onDestroy 表示 Activity 即将被销毁

1.2 各种情况下生命周期的回调

以下总结一下各种情况下,生命周期中的回调情况(表中的 A,B 代表的是两个 Activity):

情况 回调
第一次启动 onCreate() -> onStart() -> onResume()
从 A 跳转到不透明的 B A_onPause() -> B_onCreate() -> B_onStart() -> B_onResume() -> A_onStop()
从 A 跳转到透明的 B A_onPause() -> B_onCreate() -> B_onStart() -> B_onResume()
从不透明的 B 再次回到 A B_onPause() -> A_onRestart() -> A_onStart() -> A_onResume() -> B_onStop()
从透明的 B 再次回到 A B_onPause() -> A_onResume() -> B_onStop() -> B_onDestroy()
用户按 home 键 onPause() -> onStop()
按 home 键回后回到应用 onRestart() -> onStart() -> onResume()
用户按 back 键回退 onPause() -> onStop() -> onDestroy()

1.3 onSaveInstanceState() 与 onRestoreInstanceState()

这两个方法只有在应用遇到意外情况下才会触发。可以用于保存一些临时性的数据。

1.3.1 触发场景

onSaveInstanceState():

  1. 横竖屏切换
  2. 按下电源键
  3. 按下菜单键
  4. 切换到别的 Activity
  5. ....

onRestoreInstanceState():

  1. 横竖屏切换
  2. 切换语言
  3. ....

2. Activity 之间的跳转

2.1 相关API

2.1.1 startActivity()

怎么用:

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

使用这个方法就可以跳转到 SecondActivity

2.1.2 startActivityforResult()

怎么用:

MainActivity.java:

Intent intent = new Intent(MainActivity.this, SecondActivity.class);
startActivityForResult(intent, 1);

这里第二个参数是一个 requestCode,这个参数会在 onActivityResult 回调回来。

SecondActivity.java:

setResult(2);
finish();

当 SecondActivity finish 后会回调 MainActivity 中的 onActivityResult 方法:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    Log.e("chan", "==================onActivityResult main= " + requestCode + " " + resultCode);
}

打印结果:

E/chan: ==================onActivityResult main= 1 2

当然你也可以不调用 setResult() 方法,这时回调过来的 resultCode 就是 0。

2.2 显式启动

显示启动分类:

  1. 直接在 Intent 构造方法启动:
Intent intent = new Intent(this, SecondActivity.class);
  1. setComponent:
ComponentName componentName = new ComponentName(this, SecondActivity.class);
Intent intent = new Intent();
intent.setComponent(componentName);
  1. setClass / setClassName:
Intent intent = new Intent();
intent.setClass(this, SecondActivity.class);
intent.setClassName(this, "com.example.administrator.myapplication.SecondActivity");

2.3 隐式启动

隐式启动就是要在该 Activity 中设置 IntentFilter 属性,只要启用的 Intent 匹配 IntentFilter 的条件就可以启动相应的 Activity。

要理解隐式启动就必须要理解 IntentFilter 是如何使用的

2.3.1 IntentFilter 的使用

IntentFilter 有三个标签分别是:

  1. action
  2. category
  3. data

这三个标签都有对应的匹配规则,下面会说到。这里来说下使用 IntentFilter 要注意的地方

  1. 一个 Activity 中可以有多个 intent-filter
  2. 一个 intent-filter 同时可以有多个 action,category,data
  3. 一个 Intent 只要能匹配任何一组 intent-filter 即可启动对应 Activity
  4. 新建的 Activity 必须加上以下这句:
<category android:name="android.intent.category.DEFAULT"/>

否则就会出现如下错误:

android.content.ActivityNotFoundException: No Activity found to handle Intent { act=com.chan1 }

2.3.2 action 的匹配规则

action 的匹配规则就是只要满足其中一个 action 就可以启动成功。

在 Manifest 定义一个 SecondActivity:

<activity android:name=".SecondActivity">
    <intent-filter>
        <action android:name="com.chan" />
        <action android:name="com.chan2" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
    <intent-filter>
    <action android:name="com.chan3" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

MainActivity:

Intent intent = new Intent();
intent.setAction("com.chan2");
startActivity(intent);

这样就可以启动 SecondActivity,要注意的是 action 是区分大小写的。

2.3.3 category 匹配规则

category 在代码设置如下:

intent.addCategory("com.zede");

这句可以添加也可以不添加,因为代码默认会为我们匹配 “android.intent. category.DEFAULT”。

2.3.4 data 匹配规则

data 主要是由 URI 和 mimeType 组成的。URI 的结构如下:

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

这些值在 Manifest 文件中可以定义,语法如下:

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

以下用一个例子来说明:

Manifest:

<activity android:name=".SecondActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <data
            android:host="zede"
            android:port="1010"
            android:scheme="chan" />
    </intent-filter>
</activity>

MainActivity:

Intent intent = new Intent();
intent.setData(Uri.parse("chan://zede:1010"));
startActivity(intent);

通过这个方法就可以跳转到 SecondActivity。

我们也可以创建一个 html 文件,来实现跳转 SecondActivity。

test.html:

<!DOCTYPE html>
<html>
<head>
	<title></title>
</head>
<body>
<a href="chan://zede:1010">跳转至SecondActivity</a>
</body>
</html>

使用手机浏览器打开这个 html 文件,点击这个超链接也可以跳转到 SecondActivity。

通过这个链接也可以传输数据到 SecondActivity,代码如下:

<!DOCTYPE html>
<html>
<head>
	<title></title>
</head>
<body>
<a href="chan://zede:1010/mypath?user=admin&psd=123456">跳转至SecondActivity</a>
</body>
</html>

在 SecondActivity 接收数据:

Intent intent = getIntent();
Uri uri = intent.getData();

Log.e("chan", "==================getScheme= " + intent.getScheme());
Log.e("chan", "==================getHost= " + uri.getHost());
Log.e("chan", "==================getPort= " + uri.getPort());
Log.e("chan", "==================getPath= " + uri.getPath());

Log.e("chan", "==================getQuery= " + uri.getQuery());

Set < String > names = uri.getQueryParameterNames();

Iterator < String > iterator = names.iterator();
while (iterator.hasNext()) {
    String key = iterator.next();
    uri.getQueryParameter(key);
    Log.e("chan", "==================getQueryParameter= " + uri.getQueryParameter(key));
}

打印结果:

07-19 10:47:54.969 19201-19201/com.example.administrator.myapplication E/chan: ==================getScheme= chan
07-19 10:47:54.970 19201-19201/com.example.administrator.myapplication E/chan: ==================getHost= zede
    ==================getPort= 1010
    ==================getPath= /mypath
    ==================getQuery= user=admin&psd=123456
    ==================getQueryParameter= admin
    ==================getQueryParameter= 123456

另外还需要注意另一个属性:android:mimeType,这个属性就是说要传递什么类型的数据,通常有 text/plain 或 image/jpeg。

可以通过以下代码来启动 Activity:

intent.setType("text/plain");

不过需要注意的是,如果同时设置了 URI 和 mimeType 的话就必须使用如下代码才可以跳转:

intent.setDataAndType(Uri.parse("chan://zede:1010"), "text/plain");

因为如果使用 setData() 或者 setType() 的话,分别会将相应 type 和 data 置为 null。

3. Activity的启动模式

3.1 分类:

启动模式 作用
standard 每次启动都会重新创建一个 Activity
singleTop 如果该栈顶上有所要启动的 Activity,那么就不会重新创建该 Activity,并会回调 onNewIntent()
singleTask 如果栈内已经有所要启动的 Activity 就不会被创建,同时也会调用 onNewIntent()
singleInstance 创建该 Activity 系统会创建一个新的任务栈

这里重点说下 singleTask。

3.2 singleTask 分析

singleTask 叫做栈内复用模式,这个启动模式的启动逻辑如下图:

singleTask 逻辑

相信看了上面这个图,大家也清楚 singleTask 的逻辑了,但是这个模式还有几个需要注意的地方。

3.2.1 taskAffinity

前面提到 A 想要的任务栈,那什么是 A 想要的任务栈呢?这就提到一个属性 taskAffinity,以下详细介绍这个属性。

3.2.1.1 作用

标识一个 Activity 所需要的任务栈的名字。如果不设置这个属性值,默认值是应用的包名。

3.2.1.2 taskAffinity 与 singleTask 配对使用

如果启动了设置了这两个属性的 Activity,这个 Activity 就会在 taskAffinity 设置的任务栈中,下面用代码来验证下:

创建 SecondActvitiy,在 Mnifest 文件设置 SecondActvitiy,代码如下:

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

现在使用 MainActivity 启动 SecondActvitiy,这里的代码就不展示了,我们直接看看结果,在终端输入以下命令:

adb shell dumpsys activity activities | sed -En -e '/Running activities/,/Run #0/p'

这个命令可以查看正在运行的 Activity,结果如下:

Running activities (most recent first):
    TaskRecord{762a040 #63 A=com.chan U=0 StackId=1 sz=1}
    Run #1: ActivityRecord{3881f68 u0 com.example.activitydemo/.SecondActivity t63}
    TaskRecord{351eb79 #62 A=com.example.activitydemo U=0 StackId=1 sz=1}

从打印结果可以看出, MainActivity 和 SecondActivity 运行在不同的任务栈中。

3.2.1.3 taskAffinity 和 allowTaskReparenting 配对使用

allowTaskReparenting 这个属性直接解释的话,可能很多人都会听得懵逼,下面直接使用例子来解释:

现在创建两个应用,一个应用的包名为:com.example.activitydemo,以下成为应用 A。另一个应用的包名为:com.example.hellodemo,以下称为应用 B。

在应用 A 的 MainActivtiy 中添加如下代码:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    findViewById(R.id.start1).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
        Intent intent=new Intent();
        intent.setClassName("com.example.hellodemo", "com.example.hellodemo.HelloActivity");
        startActivity(intent);
        }
    });

}

应用 B 中创建 HelloActivity,并在 Manifest 文件中设置 HelloActivity 的 allowTaskReparenting 为 true,代码如下:

<activity android:name=".HelloActivity"
    android:exported="true"
    android:allowTaskReparenting="true" />

然后根据以下步骤操作:

  1. 现在先运行应用 B,然后退出
  2. 再运行应用 A,点击按钮启动
  3. 按 home 键,回到主界面
  4. 启动应用 B

完成第二步的时候,在终端看下任务栈的情况:

Running activities (most recent first):
    TaskRecord{5d54c1c #85 A=com.example.activitydemo U=0 StackId=1 sz=2}
    Run #1: ActivityRecord{ff0b8e u0 com.example.hellodemo/.HelloActivity t85}
    Run #0: ActivityRecord{95ee35c u0 com.example.activitydemo/.MainActivity t85}

可以看出 HelloActivity 运行在应用 A 的任务栈中。

完成第四步后,再看下任务栈:

Running activities (most recent first):
    TaskRecord{74c894d #86 A=com.example.hellodemo U=0 StackId=1 sz=2}
        Run #1: ActivityRecord{ff0b8e u0 com.example.hellodemo/.HelloActivity t86}
    TaskRecord{5d54c1c #85 A=com.example.activitydemo U=0 StackId=1 sz=1}
        Run #0: ActivityRecord{95ee35c u0 com.example.activitydemo/.MainActivity t85}

从结果可以看到,HelloActivity 从应用 A 的任务栈移动到应用 B 的任务栈。

现在再修改下 HelloActivity 的 taskAffinity 属性,代码如下:

<activity android:name=".HelloActivity"
    android:exported="true"
    android:allowTaskReparenting="true"
    android:taskAffinity="com.chan"/>

重新根据以上步骤操作,操作完毕后看下任务栈信息:

Running activities (most recent first):
    TaskRecord{50264fe #90 A=com.example.hellodemo U=0 StackId=1 sz=1}
    Run #2: ActivityRecord{bc77713 u0 com.example.hellodemo/.MainActivity t90}
    TaskRecord{41abf9e #89 A=com.example.activitydemo U=0 StackId=1 sz=2}
    Run #1: ActivityRecord{2d0b7bb u0 com.example.hellodemo/.HelloActivity t89}
    Run #0: ActivityRecord{8b57551 u0 com.example.activitydemo/.MainActivity t89}

可以看出 HelloActivity 并没有移动到应用 B 的主任务栈中,因为这并不是 HelloActivity 想要的任务栈。

继续修改 HelloActivity 配置属性,增加 singleTask 属性:

<activity android:name=".HelloActivity"
    android:exported="true"
    android:allowTaskReparenting="true"
    android:taskAffinity="com.chan"
    android:launchMode="singleTask"/>

继续操作,任务栈结果如下:

Running activities (most recent first):
    TaskRecord{775e709 #95 A=com.example.hellodemo U=0 StackId=1 sz=1}
        Run #2: ActivityRecord{757bb47 u0 com.example.hellodemo/.MainActivity t95}
    TaskRecord{aa75b2 #94 A=com.chan U=0 StackId=1 sz=1}
        Run #1: ActivityRecord{76e2133 u0 com.example.hellodemo/.HelloActivity t94}
    TaskRecord{21c8903 #93 A=com.example.activitydemo U=0 StackId=1 sz=1}
        Run #0: ActivityRecord{be84df4 u0 com.example.activitydemo/.MainActivity t93}

可以看出与没有增加 singleTask 属性的结果是一样的,其实 allowTaskReparenting 这个属性的最主要作用就是将这个 Activity 转移到它所属的任务栈中,例如一个短信应用收到一条带网络链接的短信,点击链接会跳转到浏览器中,这时候如果 allowTaskReparenting 设置为 true 的话,打开浏览器应用就会直接显示刚才打开的网页页面,而打开短信应用后这个浏览器界面就会消失。

3.3 指定启动模式的方式

指定启动模式的方式有两种,一种是在 AndroidMenifest 文件设置 launchMode 属性,另一种就是在 Intent 当中设置标志位。第二种方式的优先级会比第一种的要高,如果两种都设置了会以第二种方式为准。我们来验证一下:

在 MainActivity 设置如下代码:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.start1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                intent.setClass(MainActivity.this, SecondActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                startActivity(intent);
            }
        });

    }

SecondActivity 在 AndroidMenifest 设置如下:

        <activity android:name=".SecondActivity"
            android:taskAffinity="com.chan" />

这里我并没有设置 SecondActivity 为 sigleTask,来验证下启动 SecondActivity 是否会开启一个新的任务栈。

运行后,任务栈的结果为:

    Running activities (most recent first):
      TaskRecord{148d7c5 #143 A=com.chan U=0 StackId=1 sz=1}
        Run #2: ActivityRecord{de59b2d u0 com.example.activitydemo/.SecondActivity t143}
      TaskRecord{520151a #142 A=com.example.activitydemo U=0 StackId=1 sz=1}
        Run #1: ActivityRecord{d80bfc1 u0 com.example.activitydemo/.MainActivity t142}

从结果可以看出,是开启了一个新的任务栈的,也证明了第二种方式的优先级比较高

3.4 onNewIntent 回调时机

启动 singleTask 的 Activity 的时候会回调 onNewIntent() 方法,但是并不是所有情况都这样,总结如下图:

onNewIntent 回调时机

以下使用代码来验证一下这四种情况:

3.4.1 不存在 A 所需的任务栈

代码如下:

MainActivity.java:

public class MainActivity extends AppCompatActivity {


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.start1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(MainActivity.this, SecondActivity.class));
            }
        });

    }


    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        Log.e("chan", "MainActivity=======================onNewIntent");
    }


}

SecondActivity.java:

public class SecondActivity extends AppCompatActivity {


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        findViewById(R.id.start2).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(SecondActivity.this, ThirdActivity.class));
            }
        });

    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        Log.e("chan", "SecondActivity=======================onNewIntent");
    }
}

清单文件中将 SecondActivity 设置为 singleTask,taskAffinity 属性设置一个非该程序包名的值,代码如下:

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

以上代码的结果并没有打印任何东西,证明这样并不会回调 onNewIntent()。

3.4.2 存在 A 所需的任务栈

这种情况还要分两种子情况,一种就是 A 不在栈中,另一种 A 不在栈中。

3.4.2.1 A 不在栈中

还是用回上面的例子的代码,添加一个 ThridActivity,ThridActivity 在清单文件描述如下:

        <activity android:name=".ThirdActivity"
            android:taskAffinity="com.onnewintent"
            android:launchMode="singleTask" />

点击 SecondActivity 跳转按钮后同样也不会有任何打印,证明并不会回调 onNewIntent()。

3.4.2.2 A 在栈中

这种情况也会分两种子情况,一种就是 A 在栈顶,另一种就是 A 不在栈顶。

3.4.2.2.1 A 在栈顶

同样也是使用上面的例子,修改 ThirdActivity,代码如下:

public class ThirdActivity extends AppCompatActivity {



    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
        findViewById(R.id.start3).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(ThirdActivity.this, ThirdActivity.class));
            }
        });

    }


    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        Log.e("chan", "ThirdActivity=======================onNewIntent");
    }


}

到 ThirdActivity 时,ThirdActivity 已经是在栈顶了,这时候点击按钮再次启动 ThirdActivity,打印结果如下:

chan    : ThirdActivity=======================onNewIntent

可以发现这种情况会回调 ThirdActivity 的 onNewIntent。

3.4.2.2.2 A 不在在栈顶

同样改动 ThirdActivity,这次启动的是 SecondActivity。具体代码就不写了,打印结果如下:

chan    : SecondActivity=======================onNewIntent

可以发现这种情况会回调 SecondActivity 的 onNewIntent。

3.5 Activity 中的 Flags

3.5.1 FLAG_ACTIVITY_NEW_TASK

与启动模式 singleTask 的作用一样,必须设置 taskAffinity

3.5.2 FLAG_ACTIVITY_SINGLE_TOP

与启动模式 singleTop 的作用一样

3.5.3 FLAG_ACTIVITY_CLEAR_TOP

从名字就可以看出这个标志的作用就是如果这个 Activity 顶部有别的 Activity 的话,那么就会它顶部的 Activity 全部出栈,如果这个 Activity 的启动模式为 standard 的话,就会将之前的 Activity 出栈,并且创建一个新的 Activity,如果不是的话就会调用 onNewIntent 方法。

3.5.4 FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS

不能在查看历史 Activity 中查看到此 Activity