余一

纸上得来终觉浅,绝知此事要躬行。

0%

Activity

使用Intent在Activity之间穿梭

使用显式Intent

1
2
val intent = Intent(this,SecondActivity::class.java)
startActivity(intent)

第一个参数传入this也就是FirstActivity作为上下文,第二个参数传入SecondActivity::class.java作为目标Activity,这样我们的“意图”就非常明显了,即在FirstActivity的基础上打开SecondActivity。

隐式Intent

打开AndroidManifest.xml,添加如下代码:

1
2
3
4
5
6
7
8
<activity
android:name=".SecondActivity"
android:exported="true" >
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

< action>标签中我们指明了当前Activity可以响应com.example.activitytest.ACTION_START这个action,而< category>标签则包含了一些附加信息,更精确地指明了当前Activity能够响应的Intent中还可能带有的category。只有< action>和< category>中的内容同时匹配Intent中指定的action和category时,这个Activity才能响应该Intent。

1
2
3
4
5
6
btn.setOnClickListener {
val intent = Intent("com.example.activitytest.ACTION_START")
// val intent = Intent(Intent.ACTION_VIEW)
// intent.data = Uri.parse("https://www.baidu.com")
startActivity(intent)
}

android.intent.category.DEFAULT是一种默认的category,在调用startActivity()方法的时候会自动将这个category添加到Intent中。

  1. 增加category

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <activity
    android:name=".SecondActivity"
    android:exported="true" >
    <intent-filter>
    <action android:name="com.example.ACTION_START" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="com.example.activitytest.MY_CATEGORY" />
    </intent-filter>
    </activity>
    1
    2
    3
    4
    5
    6
    7
    btn.setOnClickListener {
    val intent = Intent("com.example.activitytest.ACTION_START")
    // val intent = Intent(Intent.ACTION_VIEW)
    // intent.data = Uri.parse("https://www.baidu.com")
    intent.addCategory("com.example.activitytest.MY_CATEGORY")
    startActivity(intent)
    }

    3.更多隐式Intent用法

    使用隐式Intent,不仅可以启动自己程序内的Activity,还可以启动其他程序的Activity,这就使多个应用程序之间的功能共享成为了可能。比如调用系统的浏览器来打开网页

    1
    2
    3
    4
    5
    btn.setOnClickListener {
    val intent = Intent(Intent.ACTION_VIEW)
    intent.data = Uri.parse("https://www.baidu.com")
    startActivity(intent)
    }

    setData():接收一个Uri对象,主要用于指定当前Intent正在操作的数据,而这些数据通常是以字符串形式传入Uri.parse()方法中解析产生的。

与此对应,我们还可以在< intent-filter>标签中再配置一个< data>标签,用于更精确地指定当前Activity能够响应的数据。< data>标签中主要可以配置以下内容。

  • android:scheme。用于指定数据的协议部分,如上例中的https部分。
  • android:host。用于指定数据的主机名部分,如上例中的www.baidu.com部分。
  • android:port。用于指定数据的端口部分,一般紧随在主机名之后。
  • android:path。用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内容。
  • android:mimeType。用于指定可以处理的数据类型,允许使用通配符的方式进行指定。

只有当< data>标签中指定的内容和Intent中携带的Data完全一致时,当前Activity才能够响应该Intent。不过,在< data>标签中一般不会指定过多的内容。例如在上面的浏览器示例中,其实只需要指定android:scheme为https,就可以响应所有https协议的Intent了。

1
2
3
4
5
6
7
8
9
<activity
android:name=".SecondActivity"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="https"/>
</intent-filter>
</activity>

虽然我们声明了ThirdActivity是可以响应打开网页的Intent的,但实际上这个Activity并没有加载并显示网页的功能,所以在真正的项目中尽量不要出现这种有可能误导用户的行为,不然会让用户对我们的应用产生负面的印象。

除了https协议外,我们还可以指定很多其他协议,比如geo表示显示地理位置、tel表示拨打电话。下面的代码展示了如何在我们的程序中调用系统拨号界面。

1
2
3
4
5
btn.setOnClickListener {
val intent = Intent(Intent.ACTION_DIAL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
}

Intent.ACTION_DIAL,这又是一个Android系统的内置动作。然后在data部分指定了协议是tel,号码是10086。

向下一个Activity传递数据

Intent中提供了一系列putExtra()方法的重载,可以把我们想要传递的数据暂存在Intent中,在启动另一个Activity后,只需要把这些数据从Intent中取出就可以了。

1
2
3
4
5
6
btn.setOnClickListener {
val name = "Irving"
val intent = Intent(this,com.xixun.firstapp.SecondActivity::class.java)
intent.putExtra("name",name)
startActivity(intent)
}

传递的是字符串,所以SecondActivity使用getStringExtra()方法来获取传递的数据。如果传递的是整型数据,则使用getIntExtra()方法;如果传递的是布尔型数据,则使用getBooleanExtra()方法,以此类推。

返回数据给上一个Activity

Activity类中还有一个用于启动Activity的startActivityForResult()方法,它期望在Activity销毁的时候能够返回一个结果给上一个Activity。

startActivityForResult()方法接收两个参数:第一个参数还是Intent;第二个参数是请求码,用于在之后的回调中判断数据的来源。

修改FirstActivity中按钮的点击事件,代码如下所示:

1
2
3
4
btn.setOnClickListener {
val intent = Intent(this,com.xixun.firstapp.SecondActivity::class.java)
startActivityForResult(intent,1)
}

在SecondActivity中给按钮注册点击事件,并在点击事件中添加返回数据的逻辑,代码如下所示:

1
2
3
4
5
6
7
btn2.setOnClickListener {
val intent = Intent()
intent.putExtra("data_return","hello,MainActivity.")
//销毁前发个数据给上一个Activity
setResult(RESULT_OK,intent)
finish()
}

可以看到,我们还是构建了一个Intent,只不过这个Intent仅仅用于传递数据而已,它没有指定任何的“意图”。紧接着把要传递的数据存放在Intent中,然后调用了setResult()方法。这个方法非常重要,专门用于向上一个Activity返回数据。setResult()方法接收两个参数:第一个参数用于向上一个Activity返回处理结果,一般只使用RESULT_OK或RESULT_CANCELED这两个值;第二个参数则把带有数据的Intent传递回去。最后调用了finish()方法来销毁当前Activity。

由于我们是使用startActivityForResult()方法来启动SecondActivity的,在SecondActivity被销毁之后会回调上一个Activity的onActivityResult()方法,因此我们需要在FirstActivity中重写这个方法来得到返回的数据,如下所示:

1
2
3
4
5
6
7
8
9
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when(requestCode){
1 -> if (resultCode == RESULT_OK){
val data = data?.getStringExtra("data_return")
Log.i("Main","data is $data")
}
}
}

onActivityResult()方法带有3个参数:第一个参数requestCode,即我们在启动Activity时传入的请求码;第二个参数resultCode,即我们在返回数据时传入的处理结果;第三个参数data,即携带着返回数据的Intent。由于在一个Activity中有可能调用startActivityForResult()方法去启动很多不同的Activity,每一个Activity返回的数据都会回调到onActivityResult()这个方法中,因此我们首先要做的就是通过检查requestCode的值来判断数据来源。确定数据是从SecondActivity返回的之后,我们再通过resultCode的值来判断处理结果是否成功。最后从data中取值并打印出来,这样就完成了向上一个Activity返回数据的工作。

如果用户在SecondActivity中并不是通过点击按钮,而是通过按下Back键回到FirstActivity,这样数据不就没法返回了吗?没错,不过这种情况还是很好处理的,我们可以通过在SecondActivity中重写onBackPressed()方法来解决这个问题,代码如下所示:

1
2
3
4
5
6
7
override fun onBackPressed() {
val intent = Intent()
intent.putExtra("data_return","hello,MainActivity.")
//销毁前发个数据给上一个Activity
setResult(RESULT_OK,intent)
finish()
}

Activity的生命周期

返回栈

Android中的Activity是可以层叠的。我们每启动一个新的Activity,就会覆盖在原Activity之上,然后点击Back键会销毁最上面的Activity,下面的一个Activity就会重新显示出来。

其实Android是使用任务(task)来管理Activity的,一个任务就是一组存放在栈里的Activity的集合,这个栈也被称作返回栈(back stack)。栈是一种后进先出的数据结构,在默认情况下,每当我们启动了一个新的Activity,它就会在返回栈中入栈,并处于栈顶的位置。而每当我们按下Back键或调用finish()方法去销毁一个Activity时,处于栈顶的Activity就会出栈,前一个入栈的Activity就会重新处于栈顶的位置。系统总是会显示处于栈顶的Activity给用户。

Activity状态

每个Activity在其生命周期中最多可能会有4种状态。

运行状态

当一个Activity位于返回栈的栈顶时,Activity就处于运行状态。系统最不愿意回收的就是处于运行状态的Activity,因为这会带来非常差的用户体验。

暂停状态

当一个Activity不再处于栈顶位置,但仍然可见时,Activity就进入了暂停状态。你可能会觉得,既然Activity已经不在栈顶了,怎么会可见呢?这是因为并不是每一个Activity都会占满整个屏幕,比如对话框形式的Activity只会占用屏幕中间的部分区域。处于暂停状态的Activity仍然是完全存活着的,系统也不愿意回收这种Activity(因为它还是可见的,回收可见的东西都会在用户体验方面有不好的影响),只有在内存极低的情况下,系统才会去考虑回收这种Activity。

停止状态

当一个Activity不再处于栈顶位置,并且完全不可见的时候,就进入了停止状态。系统仍然会为这种Activity保存相应的状态和成员变量,但是这并不是完全可靠的,当其他地方需要内存时,处于停止状态的Activity有可能会被系统回收。

销毁状态

一个Activity从返回栈中移除后就变成了销毁状态。系统最倾向于回收处于这种状态的Activity,以保证手机的内存充足。

Activity的生存期

Activity类中定义了7个回调方法,覆盖了Activity生命周期的每一个环节,下面就来一一介绍这7个方法。

  • onCreate()。我们在每个Activity中都重写了这个方法,它会在Activity第一次被创建的时候调用。你应该在这个方法中完成Activity的初始化操作,比如加载布局、绑定事件等。
  • onStart()。这个方法在Activity由不可见变为可见的时候调用。
  • onResume()。这个方法在Activity准备好和用户进行交互的时候调用。此时的Activity一定位于返回栈的栈顶,并且处于运行状态。
  • onPause()。这个方法在系统准备去启动或者恢复另一个Activity的时候调用。我们通常会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶Activity的使用。
  • onStop()。这个方法在Activity完全不可见的时候调用。它和onPause()方法的主要区别在于,如果启动的新Activity是一个对话框式的Activity,那么onPause()方法会得到执行,而onStop()方法并不会执行。
  • onDestroy()。这个方法在Activity被销毁之前调用,之后Activity的状态将变为销毁状态。
  • onRestart()。这个方法在Activity由停止状态变为运行状态之前调用,也就是Activity被重新启动了。

以上7个方法中除了onRestart()方法,其他都是两两相对的,从而又可以将Activity分为以下3种生存期。

  1. 完整生存期。Activity在onCreate()方法和onDestroy()方法之间所经历的就是完整生存期。一般情况下,一个Activity会在onCreate()方法中完成各种初始化操作,而在onDestroy()方法中完成释放内存的操作。
  2. 可见生存期。Activity在onStart()方法和onStop()方法之间所经历的就是可见生存期。在可见生存期内,Activity对于用户总是可见的,即便有可能无法和用户进行交互。我们可以通过这两个方法合理地管理那些对用户可见的资源。比如在onStart()方法中对资源进行加载,而在onStop()方法中对资源进行释放,从而保证处于停止状态的Activity不会占用过多内存。
  3. 前台生存期。Activity在onResume()方法和onPause()方法之间所经历的就是前台生存期。在前台生存期内,Activity总是处于运行状态,此时的Activity是可以和用户进行交互的,我们平时看到和接触最多的就是这个状态下的Activity。

Activity被回收了怎么办

​ 打个比方,MainActivity中如果有一个文本输入框,现在你输入了一段文字,然后启动NormalActivity,这时MainActivity由于系统内存不足被回收掉,过了一会你又点击了Back键回到MainActivity,你会发现刚刚输入的文字都没了,因为MainActivity被重新创建了。

​ 如果我们的应用出现了这种情况,是会比较影响用户体验的,所以得想想办法解决这个问题。其实,Activity中还提供了一个onSaveInstanceState()回调方法,这个方法可以保证在Activity被回收之前一定会被调用,因此我们可以通过这个方法来解决问题。

​ onSaveInstanceState()方法会携带一个Bundle类型的参数,Bundle提供了一系列的方法用于保存数据,比如可以使用putString()方法保存字符串,使用putInt()方法保存整型数据,以此类推。每个保存方法需要传入两个参数,第一个参数是键,用于后面从Bundle中取值,第二个参数是真正要保存的内容。

1
2
3
4
5
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val data = "When I can see you again?"
outState.putString("key",data)
}

​ 数据是已经保存下来了,那么我们应该在哪里进行恢复呢?细心的你也许早就发现,我们一直使用的onCreate()方法其实也有一个Bundle类型的参数。这个参数在一般情况下都是null,但是如果在Activity被系统回收之前,你通过onSaveInstanceState()方法保存数据,这个参数就会带有之前保存的全部数据,我们只需要再通过相应的取值方法将数据取出即可。修改MainActivity的onCreate()方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val btn:Button = findViewById<Button>(R.id.btn)
btn.setOnClickListener {
val intent = Intent(this,com.xixun.firstapp.SecondActivity::class.java)
startActivityForResult(intent,1)
}

if (savedInstanceState!=null){
val data = savedInstanceState.getString("key")
}
}

取出值之后再做相应的恢复操作就可以了,比如将文本内容重新赋值到文本输入框上

​ Intent还可以结合Bundle一起用于传递数据。首先我们可以把需要传递的数据都保存在Bundle对象中,然后再将Bundle对象存放在Intent里。到了目标Activity之后,先从Intent中取出Bundle,再从Bundle中一一取出数据。

Activity的启动模式

Activity的启动模式对你来说应该是个全新的概念,在实际项目中我们应该根据特定的需求为每个Activity指定恰当的启动模式。启动模式一共有4种,分别是standard、singleTop、singleTask和singleInstance,可以在AndroidManifest.xml中通过给< activity>标签指定android:launchMode属性来选择启动模式。

standard

​ standard是Activity默认的启动模式,在不进行显式指定的情况下,所有Activity都会自动使用这种启动模式。到目前为止,我们写过的所有Activity都是使用的standard模式。经过上一节的学习,你已经知道了Android是使用返回栈来管理Activity的,在standard模式下,每当启动一个新的Activity,它就会在返回栈中入栈,并处于栈顶的位置。对于使用standard模式的Activity,系统不会在乎这个Activity是否已经在返回栈中存在,每次启动都会创建一个该Activity的新实例。

singleTop

​ Activity明明已经在栈顶了,为什么再次启动的时候还要创建一个新的Activity实例呢?别着急,这只是系统默认的一种启动模式而已,你完全可以根据自己的需要进行修改,比如使用singleTop模式。当Activity的启动模式指定为singleTop,在启动Activity时如果发现返回栈的栈顶已经是该Activity,则认为可以直接使用它,不会再创建新的Activity实例。

1
2
3
4
5
6
7
8
9
10
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

singleTask

​ 使用singleTop模式可以很好地解决重复创建栈顶Activity的问题,但是正如你在上一节所看到的,如果该Activity并没有处于栈顶的位置,还是可能会创建多个Activity实例的。那么有没有什么办法可以让某个Activity在整个应用程序的上下文中只存在一个实例呢?这就要借助singleTask模式来实现了。当Activity的启动模式指定为singleTask,每次启动该Activity时,系统首先会在返回栈中检查是否存在该Activity的实例,如果发现已经存在则直接使用该实例,并把在这个Activity之上的所有其他Activity统统出栈,如果没有发现就会创建一个新的Activity实例。

singleInstance

​ singleInstance模式应该算是4种启动模式中最特殊也最复杂的一个了,你也需要多花点工夫来理解这个模式。不同于以上3种启动模式,指定为singleInstance模式的Activity会启用一个新的返回栈来管理这个Activity(其实如果singleTask模式指定了不同的taskAffinity,也会启动一个新的返回栈)。那么这样做有什么意义呢?想象以下场景,假设我们的程序中有一个Activity是允许其他程序调用的,如果想实现其他程序和我们的程序可以共享这个Activity的实例,应该如何实现呢?使用前面3种启动模式肯定是做不到的,因为每个应用程序都会有自己的返回栈,同一个Activity在不同的返回栈中入栈时必然创建了新的实例。而使用singleInstance模式就可以解决这个问题,在这种模式下,会有一个单独的返回栈来管理这个Activity,不管是哪个应用程序来访问这个Activity,都共用同一个返回栈,也就解决了共享Activity实例的问题。

Activity的最佳实践

知晓当前是在哪一个Activity

  1. 这里的BaseActivity和普通Activity的创建方式并不一样,因为我们不需要让BaseActivity在AndroidManifest.xml中注册,所以选择创建一个普通的Kotlin类就可以了。然后让BaseActivity继承自AppCompatActivity,并重写onCreate()方法,如下所示:

    1
    2
    3
    4
    5
    6
    open class BaseActivity : androidx.appcompat.app.AppCompatActivity {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Log.d("Base",javaClass.simpleName)
    }
    }

    ​ Kotlin中的javaClass表示获取当前实例的Class对象,相当于在Java中调用getClass()方法;而Kotlin中的BaseActivity::class.java表示获取BaseActivity类的Class对象,相当于在Java中调用BaseActivity.class。

  2. 接下来我们需要让BaseActivity成为ActivityTest项目中所有Activity的父类,为了使BaseActivity可以被继承,我已经提前在类名的前面加上了open关键字。然后修改FirstActivity、SecondActivity和ThirdActivity的继承结构,让它们不再继承自AppCompatActivity,而是继承自BaseActivity。而由于BaseActivity又是继承自AppCompatActivity的,所以项目中所有Activity的现有功能并不受影响,它们仍然继承了Activity中的所有特性。

  3. 现在每当我们进入一个Activity的界面,该Activity的类名就会被打印出来,这样我们就可以时刻知晓当前界面对应的是哪一个Activity了。

随时随地退出程序

解决思路也很简单,只需要用一个专门的集合对所有的Activity进行管理就可以了。下面我们就来实现一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
object class ActivityManager {
private val activities = ArrayList<Activity>()

fun addActivity(activity: Activity){
activities.add(activity)
}

fun removeActivity(activity: Activity){
activities.remove(activity)
}

fun finishAll(){
for (activity in activities) {
//判断Activity是否正在销毁中
if (!activity.isFinishing){
activity.finish()
}
}
activities.clear()
}
}

修改BaseActivity代码:

1
2
3
4
5
6
7
8
9
10
11
open class BaseActivity : androidx.appcompat.app.AppCompatActivity {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("Base",javaClass.simpleName)
}

override fun onDestroy() {
super.onDestroy()
ActivityManager.removeActivity(this)
}
}

当然你还可以在销毁所有Activity的代码后面再加上杀掉当前进程的代码,以保证程序完全退出,杀掉进程的代码如下所示:

1
android.os.Process.killProcess(android.os.Process.myPid())

killProcess()方法用于杀掉一个进程,它接收一个进程id参数,我们可以通过myPid()方法来获得当前程序的进程id。需要注意的是,killProcess()方法只能用于杀掉当前程序的进程,不能用于杀掉其他程序。

启动Activity的最佳写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ThirdActivity : Activity() {
companion object{
fun actionStart(context: Context,data1:String,data2:String){
val intent = Intent(context,SecondActivity::class.java)
intent.putExtra("param1",data1)
intent.putExtra("param2",data2)
context.startActivity(intent)
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_third)
}
}

一目了然,SecondActivity所需要的数据在方法参数中全部体现出来了,这样写还简化了启动Activity的代码,现在只需要一行代码就可以启动SecondActivity。

1
2
3
btn2.setOnClickListener {
ThirdActivity.actionStart(this,"data1","data2")
}