Android Activity的启动和跳转

在Android应用中一般都是通过另一个已经启动的Activity对象,调用其startActivity()或startActivityForResult()方法来启动一个Activity,或者通过Service对象,Application对象的startActivity()方法也可以启动Activity。

startActivity()方法的定义

startActivity()方法其实是Context类中的方法。它的定义如下。

public void startActivity(Intent intent)

然而Context只是一个抽象类,并没有实现startActivity()这个方法,它的实现是在继承它的子类中实现的。

常见的Context及子类的继承关系如下。

这里写图片描述

在ContextImpl,ContextWrapper和Activity类中都有startActivity()方法的实现,而Service,ContextThemeWrapper和Application类只是继承了ContextWrapper类中的startActivity()方法。

再来看ContextWrapper中的实现。ContextWrapper类从名字可以看出它是Context的封装类,它的的内部有一个Context成员变量mBase,startActivity()的实现方法调用的是mBase的startActivity()方法。而这个mBase成员变量通常就是ContextImpl对象。

// ContextWrapper类中的startActivity()方法
@Override
public void startActivity(Intent intent) {
    mBase.startActivity(intent);
}

所以这6个类中,只有ContextImpl和Activity中有startActivity()方法的具体实现,其他4个类中的startActivity()则实际调用的是ContextImpl中的startActivity()。ContextImpl和Activity中的startActivity()方法最终调用的都是Instrumentation类中的execStartActivity()方法,Activity中的startActivity()比ContextImpl中的startActivity()多了一些关于parent Activity, Activity options,Activity result的处理流程。

startActivity()方法的使用

startActivity()方法只有一个Intent参数,Intent对象中包含了要启动Activity的各项信息。使用startActivity()方法关键就在构造需要的Intent对象。在介绍如何构造Intent对象之前,这里打算先介绍下另一个启动Activity的方法startActivityForResult()。

startActivityForResult()方法的定义

startActivityForResult()方法是Activity类中的方法,在Application,Service中没有此方法,所以在Application,Service中是不能用startActivityForResult()方法启动Activity的。startActivityForResult()的定义如下。

public void startActivityForResult(Intent intent, int requestCode);

startActivityForResult()方法的使用

可以看到startActivityForResult()有两个参数,intent和requestCode,intent的含义和用法和startActivity()的intent完全相同,会在后文介绍。requestCode则是用来标记此次请求的请求码,可以由调用的Activity自由定义,但是它的取值范围并非整个整数集,而是必须在[0,65535]之间,否则在执行startActivityForResult()时会产生异常。

和startActivity()的区别是,startActivityForResult()启动一个Activity后,允许在新启动的Activity中返回结果,在调用startActivityForResult()的Activity中可以获取到这个结果。

在被启动的Activity中可以通过setResult()方法来返回需要的结果和数据。

setResult()有两个重载的方法,分别定义如下。

public final void setResult(int resultCode);
public final void setResult(int resultCode, Intent data);

resultCode表示返回的结果,data则表示返回的数据。当调用setResult(resultCode)时,表示只需要返回结果,没有数据,等同于setResult(resultCode, null);

在主调Activity中可以通过重写其onActivityResult()方法来得到被启动的Activity中返回的结果。onActivityResult()方法定义如下。

protected void onActivityResult(int requestCode, int resultCode, Intent data);

requestCode是之前调用startActivityForResult()时传入的参数,它的作用就是用来区分不同的startActivityForResult()调用。resultCode和data就是在被启动的Activity中通过setResult()返回的结果和数据。

在Activity中预定义了两个表示结果的常量,RESULT_CANCELED和RESULT_OK。很多时候我们并不需要在被调用的Activity返回时表达很多的返回结果,例如只需要告知主调Activity结果是成功还是失败,需要处理数据还是不需要处理等。这时就可以不需要再额外定义新的常量表示返回结果,直接使用这两个常量即可。值得注意的是RESULT_CANCELED的值是0,而RESULT_OK的值是-1,和通常情况下用0或正值表示成功,负值表示失败的用法不同。

使用setResult()和onActivityResult()还有如下几点需要注意:

  1. 如果主调Activity是通过startActivity()来启动一个Activity,则被启动的Activity中无论是否有执行setResult(),主调Activity的onActivityResult()都不会被调用。

  2. 如果主调Activity是通过startActivityForResult()来启动一个Activity,则被启动的Activity中无论是否有执行setResult(),主调Activity的onActivityResult()都会被调用。如果被启动的Activity没有执行setResult(),则主调Activity的onActivityResult()中的resultCode等于RESULT_CANCELED(0),data为null。

  3. 被调用的Activity要想返回结果和数据,则必须在Activity finish之前执行setResult(),如果Activity已经finish了,再执行setResult()不会有任何效果。等同于没有调用setResult(),主调Activity的onActivityResult()被回调时,resultCode等于RESULT_CANCELED,data为null。

  4. 被调用的Activity在finish之前调用了setResult(),则只有在其finish之后,主调Activity的onActivityResult()才会被回调,并不是一执行setResult(),主调Activity的onActivityResult()就会被执行。

  5. 被调用的Activity在finish之前多次调用了setResult(),则只有最后一次调用的setResult()有效。也就是说,当被调用的Activity finish之后,主调Activity的onActivityResult()接收到的结果是被调用的Activity在finish之前最近一次执行setResult()设置的结果。

    例如,在被调用的Activity中执行如下代码,则主调Activity的onActivityResult()接收到的resultCode值会是2。

    setResult(1);
    setResult(2);
    finish();
    setResult(3);
    setResult(4);
  6. 在被调用的Activity中可以通过getCallingActivity()来获取主调Activity的信息,如果getCallingActivity()返回null,则表示被调用的Activity是通过startActivity()来启动的。如果不是null,则表示被调用的Activity是通过startActivityForResult()来启动的。getCallingActivity()会返回一个ComponentName对象,从中可以得到主调Activity的名字等信息。在被调用的Activity中可以根据不同的主调Activity来执行不同的setResult()。如果没有主调Activity,也就是getCallingActivity()返回null,就不需要执行setResult()了,当然如果执行了,也不会有影响。

  7. 在被调用的Activity中没有直接的方法可以获取到主调Activity中执行startActivityForResult()时指定的requestCode,从而也就不能根据不同的requestCode来执行不同的操作。如果确实需要得到requestCode,则只能通过startActivityForResult()时,将requestCode添加到intent中,作为一个参数传递给被调用的Activity。

  8. 如果在被调用的Activiy finish之前,主调Activity已经finish了,则从被调用的Activiy返回后,主调Activity不会收到任何结果,即主调Activity的onActivityResult()不会被调用。

  9. 如果在被调用的Activiy finish之前,主调Activity已经被系统自动回收了,则从被调用的Activiy返回后,
    主调Activity仍然可以收到结果。主调Activity生命周期执行流程是:onCreate –> onRestoreInstanceState –> onActivityResult –> onResume。也就是说onActivityResult会在onCreate和onRestoreInstanceState之后,在onResume之前被调用。

下面介绍调用startActivity()和startActivityForResult()时intent参数的构造过程。

启动Activity时intent对象的构造

根据不同的应用场景,Intent对象的创建有如下几种方式。

启动当前app中的Activity

从MainActivity跳转到SecondActivity的示例代码如下。

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

或者

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

或者

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

查看Intent类的源码可以发现这三种方式实际上是完全等价的,都是创建了一个ComponentName对象,然后赋值给Intent类的mComponent成员变量。此外,参数中的Context对象(也就是例子中的MainActivity.this)只是用来获取应用的包名(packagename),并不需要它来启动Activity,所以可以传入任意一个Activity对象,或者是Application对象,并不会影响执行结果。

在MainActivity中执行如下代码,假设BaseActivity.getTopActivity()返回的是SecondActivity对象。那么当ThirdActivity返回时,仍然是MainActivity的onActivityResult()方法被调用,而不是调用SecondActivity的onActivityResult()方法。

Intent intent = new Intent();
intent.setClass(BaseActivity.getTopActivity(), ThirdActivity.class);
startActivityForResult(intent, 5);

启动其他app中的Activity

通常我们都是跳转到当前app自身的其他Activity中,但事实上startActivity()还可以启动手机上已经安装的其他app中的Activity。

假设要启动的Activity所在app的package name为com.ccpat.otherapp,类名为com.ccpat.otherapp.MainActivity。启动该Activity可以使用如下方法。

ComponentName componetName = new ComponentName("com.ccpat.otherapp", "com.ccpat.otherapp.MainActivity");
Intent intent= new Intent();
intent.setComponent(componetName);
startActivity(intent);

或者

Intent intent = new Intent();
String packageName = "com.ccpat.otherapp";
String className = "com.ccpat.otherapp.MainActivity";
intent.setClassName(packageName, className);
startActivity(intent);

查看Intent类的源码可以发现setClassName和setComponent方式是完全等价的,同样都是创建了一个ComponentName对象,然后赋值给Intent类的mComponent成员变量。附上Activity类中的setClassName()方法的实现及注释。

这里写图片描述

由于setComponent和setClassName方法本质上是相同的,所以后面会将它们作为一种方法来看待,称为setComponent/setClassName方法。

通过setComponent/setClassName方法启动的其他app中的Activity,要求app和该Activity必须存在。如果要启动的Activity所在的app不存在,或者Activity不存在,则会抛出ActivityNotFoundException异常,如图所示。

这里写图片描述

此外,通过setComponent/setClassName方法启动的其他app中的Activity,在AndroidManifest中声明时必须是exported。

在AndroidManifest.xml中声明一个Activity时可以为其添加android:exported属性,它的值可以是true或false。如果一个Activity为exported,它才可以被其他应用启动,否则它就只能在当前app中启动。
如果没有显式的指定这个值,则它的默认值取决于该Activity是否有设置intent-filter,如果有设置intent-filter,则默认为true,否则为false。

如下Activity的exported属性为true

<!-- 显式的声明exported为true --> 
<activity
    android:name=".MainActivity"
    android:exported="true" />
<!-- 指定了intent-filter,exported默认为true --> 
<activity
    android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.net.VpnService" />
    </intent-filter>
</activity>

如下Activity的exported属性为false

<!-- 没有intent-filter,exported默认为false --> 
<activity
    android:name=".MainActivity" />
<!-- 显式的声明exported为false,则无论有无intent-filter,exported属性都是false --> 
<activity
    android:name=".MainActivity"
    android:exported="false">
    <intent-filter>
        <action android:name="android.net.VpnService" />
    </intent-filter>
</activity>

如果通过上述方式启动了其他app中一个非exported的Activity,则会抛出SecurityException异常,如图所示。
这里写图片描述

由于无法控制用户手机上是否会安装某个app,也无法决定该app中Activity的类名及exported属性是否会随着版本升级发生改变,所以使用上述方式启动Activity时,通常都需要用try catch来捕获异常。

例如:

try {
    ComponentName componetName = new ComponentName("com.ccpat.otherapp", "com.ccpat.otherapp.MainActivity");
    Intent intent= new Intent();
    intent.setComponent(componetName);
    startActivity(intent);
} catch (ActivityNotFoundException e) {

} catch (SecurityException e) {

}

此外,部分机型的ROM将这种启动其他app中Activity的行为纳入到权限管理中,如果一个app要其启动其他app中的Activity,会弹出一个权限请求的界面,只有用户选择同意后才可以启动。

setComponent/setClassName方法既可以用来启动其他app中的Activity,也可以用来启动当前app中的Activity,只需要将对应的包名和类名替换为当前app的包名和要启动的Activity类名即可。和启动其他app中Activity唯一的不同是,setComponent/setClassName方法启动当前app中的Activity不需要该Activity的exported属性为true。

查看Intent类的源码可以发现,无论是“启动当前app中的Activity”中介绍的三种方式,还是“启动其他app中的Activity”中介绍的setComponent/setClassName方法,本质上都是相同的。它们都是创建了一个ComponentName对象,然后将其赋值给Intent类的mComponent成员变量。这也是为什么setComponent/setClassName方法也可以用来启动当前app中的Activity。

最后再举个例子,如果我们想要在自己的app中开启微信,可以使用如下代码(这里省略了异常处理的代码)。

ComponentName componetName = new ComponentName("com.tencent.mm", "com.tencent.mm.ui.LauncherUI");
Intent intent= new Intent();
// 在LauncherUI的onCreate中会对当前Activity堆栈的baseactivity的包名做校验,
// 如果baseactivity的包名不是"com.tencent.mm",则直接finish。
// 所以这里启动的时候需要将其放到一个新的Activity堆栈中 
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setComponent(componetName);
startActivity(intent);

显式(explicit)intent启动Activity

上述这些启动Activity的方式都称为通过显式(explicit)Intent来启动Activity。这种启动方式的特点是它们都需要明确的知道要启动的Activity所在的app的包名及Activity自身的类名。如果同时使用了多种方式,或者同一种方式设置多次,则显然只有最后设置的才会生效。

在下面的例子中通过不同的方式设置了4个要启动的Activity,但最后实际启动的只有”com.ccpat.app2”中的”com.ccpat.app2.MainActivity”。

Intent intent= new Intent(MainActivity.this, SecondActivity.class); // 方式1
intent.setClass(MainActivity.this, ThirdActivity.class);            // 方式2
ComponentName componetName = new ComponentName("com.ccpat.app1", "com.ccpat.app1.MainActivity");
intent.setComponent(componetName);                                  // 方式3
String packageName = "com.ccpat.app2";
String className = "com.ccpat.app2.MainActivity";
intent.setClassName(packageName, className);                        // 方式4
startActivity(intent);

通过显式Intent来启动其他app中Activity的方式需要已知目标app的包名和Activity类名。这种方式有一定的局限性,一方面我们想要启动某项功能,但是可能并不知道该功能对应的app包名和Activity的类名,另一方面,应用的包名可能会随着Android系统版本或应用市场的不同而不同,Activity的类名也可能随着版本的更新而改变。在Android中提供了另外一种启动其他应用中Activity的方式,即隐式intent的方式。通过隐式intent方式启动Activity不需要知道Activity的类名及对应的包名。

隐式intent启动Activity

通过隐式intent启动其他app中Activity的示例如下:

Intent intent = new Intent("com.ccpat.ACTION_TEST");
startActivity(intent);

或者

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

这两种方式同样是完全等价的。

隐式intent方法启动Activity的要求

和显式intent方法启动Activity一样,如果目标Activity不在当前app中,而是在其他app中,则必须是exported,否则会抛出SecurityException。此外,要启动的Activity必须要在intent-filter中声明指定的action。

例如,上述方式启动的action为com.ccpat.ACTION_TEST,则需要将com.ccpat.ACTION_TEST声明在要启动的Activity所在app的AndroidManifest.xml中。

<activity
    android:name=".MainActivity" >
    <intent-filter>
        <action android:name="com.ccpat.ACTION_TEST" />
        <category android:name="android.intent.category.DEFAULT" 
    </intent-filter>
</activity>

由于隐式intent方法启动的Activity必须指定intent-filter,其exported属性默认为true,所以可以不用在AndroidManifest.xml中手动添加这项属性。但是如果只希望在当前app中通过隐式intent启动该Activity,则可以加上android:exported=”false”。

和显式Intent一样,通过隐式intent方法启动的Activity时,如果设备中没有找到任何Activity处理该action,则会抛出ActivityNotFoundException的异常。因此,一般来说,通过隐式Intent来启动Activity也应当加上try catch来捕获异常。

通过隐式intent方式启动Activity只需要知道该Activity所声明的action即可,而action对应的是某项功能,因此只需要知道某项功能对应的action字符串,将其填入intent中。

在Android系统中已经有很多已经定义好action的Activity,它们都可以通过隐式Intent来启动。这里列举了一些常见的ACTION。

// 拨打电话
Intent intent = new Intent(Intent.ACTION_CALL);
// 打开系统时间设置(在Settings类中还有很多其他定义好的ACTION,对应于其他的设置界面)
Intent intent = new Intent(Settings.ACTION_DATE_SETTINGS);
// 相机
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// 文件选择
Intent intent = new Intent(Intent.ACTION_PICK);
// 系统分享
Intent intent = new Intent(Intent.ACTION_SEND);

如果有多个Activity声明了同一个Action,系统会按照一定的规则进行匹配。在隐式intent中可以增加一些额外信息用于匹配过程,额外的信息包括PackageName,Category,data,type,这里不再详述。

intent中显式和隐式一起使用

如果在一个intent中同时使用了显式和隐式两种方式来指定要启动的Activity,则实际启动的Activity以显式Intent指定的为准,和他们使用的先后顺序无关。

例如:

Intent intent = new Intent();
String packageName = "com.ccpat.otherapp";
String className = "com.ccpat.otherapp.MainActivity";
intent.setClassName(packageName, className);
intent.setAction(Intent.ACTION_VIEW);
intent.addCategory("android.intent.category.BROWSABLE");
intent.setData(Uri.parse("http://www.csdn.net"));
startActivity(intent);

这里先是通过显式intent指定要启动的activity为com.ccpat.otherapp中的com.ccpat.otherapp.MainActivity,然后又通过隐式intent指定要启动的activity为action为Intent.ACTION_VIEW,category为”android.intent.category.BROWSABLE”。 这时会显式intent指定的activity,也就是启动com.ccpat.otherapp中的com.ccpat.otherapp.MainActivity,而不是打开浏览器。

Activity的构造方法

在Android APP中启动一个Activity总是通过startActivity()或startActivityForResult()来实现,并不会直接在APP代码中new一个Activity对象来使用,直接new出来的Activity对象是无法使用的。一般来说我们在实现一个Activity类时会将初始化的操作放到onCreate()中执行,并不会实现其构造方法。但是这并不是说Activity类没有构造方法,或者构造方法不会被调用。

事实上,调用startActivity()或startActivityForResult()之后,Android框架中的代码会完成Activity对象的创建。在创建Activity对象时仍然会执行Activity类的构造方法。

在android.app.Instrumentation类中有一个newActivity方法。Android框架正是通过此方法来创建一个Activity对象的。其代码如下。

public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    return (Activity)cl.loadClass(className).newInstance();
}

可以看到,这里通过ClassLoader的loadClass()方法来加载指定的Activity类,得到对应的Class对象,然后再调用Class对象的newInstance()方法创建Activity对象。

由于Class对象的newInstance()方法在创建对象时会调用该类的无参构造方法,因此,如果确实需要在Activity构造的时候执行一些操作,可以在这个Activity类中增加一个无参的构造方法,这个构造方法会在newInstance()时被自动调用。

这里有两点需要注意:

  1. 一个Activity类中只有无参的构造方法会被执行,定义有参数的构造方法是没有意义的。
  2. 一个Activity类中如果没有无参构造方法,或者无参构造方法不是public的,则在执行newInstance()的时候会产生异常。

启动Activity的线程

对Activity来说,我们一般都是在主线程中调用startActivity()或startActivityForResult()来启动Activity,但其实startActivity()或startActivityForResult()是可以在非主线程中执行的。不过即使是在非主线程中调用startActivity,新启动的Activity生命周期的相关方法(包括构造方法)仍然会在主线程中执行。

如下代码可以正常执行,不会产生异常。

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    new Thread(new Runnable() {
        @Override
        public void run() {
            Intent intent = new Intent(MainActivity.this, SecondActivity.class);
            startActivity(intent);
        }
    }).start();
}
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 像素格子 设计师:CSDN官方博客 返回首页