第一行代码第三版——第三章:探究 Activity

先从看得到的入手,探究 Activity

Activity 是什么

Activity 是最容易吸引用户的地方,它是一种可以包含用户界面的组件,主要用于和用户进行交
互。

Activity 的基本用法

  1. 在 Android Studio 中手动创建过 Activity;

  2. 创建和加载布局;

  3. 在 AndroidManifest 文件中注册;

    <!--主 Activity 的配置-->
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    
  4. 在 Activity 中使用 Toast;

    class FirstActivity : AppCompatActivity() {
       override fun onCreate(savedInstanceState: Bundle?) {
           super.onCreate(savedInstanceState)
           setContentView(R.layout.first_layout)
    
            // findViewById() 方法返回的是一个继承自 View 的泛型对象,
            // 因此 Kotlin 无法自动推导出它是一个Button还是其他控件,所以我们需要将 button1 变量显式地声明成 Button 类型。
            var button1: Button = findViewById(R.id.button1)
            button1.setOnClickListener {
                Toast.makeText(this, "You clicked Button 1", Toast.LENGTH_SHORT).show()
            }
        }
    }
    

    关于 findViewById() 方法的使用,我还得再多讲一些。我们已经知道 findViewById() 方法的作用就是获取布局文件中控件的实例,但是前面的例子比较简单,只有一个按钮,如果某个布局文件中有10个控件呢?没错,我们就需要调用 10 次 findViewById() 方法才行。这种写法虽然很正确,但是很笨拙,于是就滋生出了诸如 ButterKnife 之类的第三方开源库,来简化 findViewById() 方法的调用。

    不过,这个问题在 Kotlin 中就不复存在了,因为使用 Kotlin 编写的 Android 项目在 app/build.gradle 文件的头部默认引入了一个 kotlin-android-extensions 插件,这个插件会根据布局文件中定义的控件 id 自动生成一个具有相同名称的变量,我们可以在 Activity 里直接使用这个变量,而不用再调用findViewById() 方法了。

    plugins {
       id 'com.android.application'
       id 'kotlin-android'
       id 'kotlin-android-extensions'
    }
    
    // 也可以用下面这种写法
    // apply plugin: 'com.android.application'
    // apply plugin: 'kotlin-android'
    // apply plugin: 'kotlin-android-extensions'
    

    所以上面的代码就可以简化成:

    class FirstActivity : AppCompatActivity() {
       override fun onCreate(savedInstanceState: Bundle?) {
           super.onCreate(savedInstanceState)
               setContentView(R.layout.first_layout)
    
               button1.setOnClickListener {
               Toast.makeText(this, "You clicked Button 1", Toast.LENGTH_SHORT).show()
           }
       }
    }
    
  5. 在 Activity 中使用 Menu;
    在 res 目录下新建一个 menu 文件夹,右击 res 目录 →New→Directory,输入文件名 “menu”,点击 “OK”。接着在这个文件夹下新建一个名叫 “main” 的菜单文件,右击 menu 文件夹→New→Menu resource file,文件名输入“main”,点击“OK”完成创建,然后在main.xml 中添加如下代码:

    <?xml version="1.0" encoding="utf-8"?>
    <menu xmlns:android="http://schemas.android.com/apk/res/android">
    
        <item
        	android:id="@+id/add_item"
        	android:title="Add" />
    
        <item
        	android:id="@+id/remove_item"
        	android:title="Remove" />
    </menu>
    

    这里我们创建了两个菜单项,其中 标签用来创建具体的某一个菜单项,然后通过 android:id 给这个菜单项指定一个唯一的标识符,通过 android:title 给这个菜单项指定一个名称。

    接着回到 FirstActivity 中来重写 onCreateOptionsMenu() 方法和 onOptionsItemSelected() 方法,如下所示:

    // 这里 menuInflater 实际上是调用了父类的 getMenuInflater() 方法,这是 Kotlin 的一种语法糖
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.main, menu)
     	return true
    }
    
    // 这里 item.itemId 也是用了跟上面一样的语法糖,实际上是调用了 getItemId() 方法
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            R.id.add_item -> Toast.makeText(this, "You clicked Add", Toast.LENGTH_SHORT).show()
            R.id.remove_item -> Toast.makeText(this, "You clicked Remove", Toast.LENGTH_SHORT).show()
        }
        return true
    }
    
  6. 销毁一个 Activity
    通过上面的学习,你已经掌握了手动创建 Activity 的方法,并学会了如何在 Activity 中创建 Toast 和菜单。或许你现在心中会有个疑惑:如何销毁一个 Activity 呢?

    Activity 类提供了一个 finish() 方法,我们只需要调用一下这个方法就可以销毁当前的Activity 了。

使用 Intent 在 Activity 之间穿梭

只有一个 Activity 的应用也太简单了吧?所以我们再创建一个 Activity 就叫 SecondActivity 那么怎样才能由主 Activity 跳转到其他 Activity 呢?我们现在就一起来看一看。

  1. 使用显示 Intent
    Intent 是 Android 程序中各组件之间进行交互的一种重要方式,它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent 一般可用于启动 Activity、启动 Service 以及发送广播等场景。

    Intent大致可以分为两种:显式 Intent 和隐式 Intent。我们先来看一下显式 Intent 如何使用,我们在 FirstActivity 中新增一个按钮用来显示启动 SecondActivity,具体代码如下所示:

    btn_intent.setOnClickListener {
        val intent = Intent(this, SecondActivity::class.java)
        startActivity(intent)
    }
    

    我们首先构建了一个 Intent 对象,第一个参数传入 this 也就是 FirstActivity 作为上下文,第二个参数传入 SecondActivity::class.java 作为目标 Activity,这样我们的“意图”就非常明显了,即在 FirstActivity 的基础上打开 SecondActivity。注意, Kotlin 中 SecondActivity::class.java 的写法就相当于 Java 中 SecondActivity.class 的写法。接下来再通过 startActivity() 方法执行这个 Intent 就可以了。

    使用这种方式来启动 Activity,Intent 的“意图”非常明显,因此我们称之为显式 Intent。

  2. 使用隐式 Intent
    相比于显式 Intent,隐式 Intent 则含蓄了许多,它并不明确指出想要启动哪一个 Activity,而是指定了一系列更为抽象的 action 和 category 等信息,然后交由系统去分析这个 Intent,并帮我们找出合适的 Activity 去启动。
    通过在 标签下配置 的内容,可以指定当前 Activity 能够响应的 action 和 category,打开 AndroidManifest.xml,在 SecondActivity 上添加如下代码:

    <activity android:name=".SecondActivity">
    	<intent-filter>
     		<action android:name="com.example.activitytest.ACTION_START"/>
     		<category android:name="android.intent.category.DEFAULT"/>
    	</intent-filter>
     </activity>
    

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

    我们在 FirstActivity 中新增一个按钮用来隐式启动 SecondActivity,具体代码如下所示:

    btn_intent_implicit.setOnClickListener {
       startActivity(Intent("com.example.activitytest.ACTION_START"))
    }
    

    可以看到,我们使用了 Intent 的另一个构造函数,直接将 action 的字符串传了进去,表明我们想要启动能够响应 com.example.activitytest.ACTION_START 这个 action 的 Activity。前面不是说要 和 同时匹配才能响应吗?怎么没看到哪里有指定 category 呢?这是因为 android.intent.category.DEFAULT 是一种默认的category,在调用 startActivity() 方法的时候会自动将这个 category 添加到 Intent 中。

    每个 Intent 中只能指定一个 action,但能指定多个 category。目前我们的 Intent 中只有一个默认的 category,那么现在再来增加一个吧。

    btn_intent_implicit.setOnClickListener {
    	val intent = Intent("com.example.activitytest.ACTION_START")
    	intent.addCategory("com.example.activitytest.MY_CATEGORY")
    	startActivity(intent)
    }
    

    这样写的话,我们还需要在 AndroidManifest 中给 SecondActivity 添加一个 Category,否则程序会崩溃,如下:

    <activity android:name=".SecondActivity">
    <intent-filter>
       	<action android:name="com.example.activitytest.ACTION_START"/>
       	<category android:name="android.intent.category.DEFAULT"/>
       	<category android:name="com.example.activitytest.MY_CATEGORY"/>
    </intent-filter>
    
```
  1. 更多隐式 Intent 的用法
    通过上面的讲解,我们掌握了通过隐式 Intent 来启动 Activity 的方法,但实际上隐式 Intent 还有更多的内容需要我们去了解。

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

    我们在 FirstActivity 中新增一个按钮用来测试上述情况,具体代码如下所示:

    btn_intent_baidu.setOnClickListener {
       val intent = Intent(Intent.ACTION_VIEW)
       intent.data = Uri.parse("https://www.baidu.com")
       startActivity(intent)
    }
    

    这里我们首先指定了 Intent 的 action 是 Intent.ACTION_VIEW,这是一个 Android 系统内置的动作,其常量值为 android.intent.action.VIEW。然后通过 Uri.parse() 方法将一个网址字符串解析成一个 Uri 对象,再调用 Intent 的 setData() 方法将这个 Uri 对象传递进去。当然,这里再次使用了前面学习的语法糖,看上去像是给 Intent 的 data 属性赋值一样。

  2. 向下一个 Activity 传递数据
    经过上面的学习,我们已经对 Intent 有了一定的了解。不过到目前为止,我们只是简单地使用 Intent 来启动一个 Activity,其实 Intent 在启动 Activity 的时候还可以传递数据,下面我们来一起看一下。

    在启动 Activity 时传递数据的思路很简单,Intent 中提供了一系列 putExtra() 方法的重载,可以把我们想要传递的数据暂存在 Intent 中,在启动另一个 Activity 后,只需要把这些数据从 Intent 中取出就可以了。比如说 FirstActivity 中有一个字符串,现在想把这个字符串传递到 SecondActivity 中,就可以这么写:

    btn_transfer_data.setOnClickListener {
       val data = "Hello SecondActivity"
       val intent = Intent(this, SecondActivity::class.java)
       intent.putExtra("extra_data", data)
       startActivity(intent)
    }
    

    传递的代码写完之后,我们就要写接收的代码了,如下所示:

    class SecondActivity : AppCompatActivity() {
       override fun onCreate(savedInstanceState: Bundle?) {
           super.onCreate(savedInstanceState)
           setContentView(R.layout.second_layout)
    
           // 接收数据
           val extraData = intent.getStringExtra("extra_data")
           tv_text.text = extraData
       }
    }
    

    上述代码中的 intent 实际上调用的是父类的 getIntent() 方法,该方法会获取用于启动 SecondActivity 的 Intent,然后调用 getStringExtra() 方法并传入相应的键值,就可以得到传递的数据了。这里由于我们传递的是字符串,所以使用 getStringExtra() 方法来获取传递的数据。如果传递的是整型数据,则使用 getIntExtra() 方法;如果传递的是布尔型数据,则使用 getBooleanExtra() 方法,以此类推。

  3. 返回数据给上一个 Activity
    既然可以传递数据给下一个 Activity,那么能不能够返回数据给上一个 Activity 呢?答案是肯定的。不过不同的是,返回上一个 Activity 只需要按一下 Back 键或者调用 finish() 方法就可以了,并没有一个用于启动 Activity 的 Intent 来传递数据,这该怎么办呢?其实 Activity 类中还有一个用于启动 Activity 的 startActivityForResult() 方法,但它期望在 Activity 销毁的时候能够返回一个结果给上一个 Activity。毫无疑问,这就是我们所需要的。

    startActivityForResult() 方法接收两个参数:第一个参数还是 Intent;第二个参数是请求码,用于在之后的回调中判断数据的来源。我们还是来实战一下,在 FirstActivity 中添加一个测试按钮,具体代码如下所示:

    btn_start_activity_for_result.setOnClickListener {
        startActivityForResult(Intent(this, SecondActivity::class.java), 1000)
    }
    

    这里我们使用了 startActivityForResult() 方法来启动 SecondActivity,请求码只要是一个唯一值即可,这里传入了 1000。接下来我们在 SecondActivity 中给按钮注册点击事件,并在点击事件中添加返回数据的逻辑,代码如下所示:

    btn_back.setOnClickListener {
    	val intent = Intent()
    	intent.putExtra("data_return", "Hello FirstActivity")
    	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 中重写这个方法来得到返回的数据,如下所示:

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
    
        when(requestCode) {
            1000 -> if (resultCode == RESULT_OK) {
                val returnedData = data?.getStringExtra("data_return")
                Log.d("FIRST_LINE_OF_CODE", "returned data is $returnedData")
            }
        }
    }
    

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

Activity 的生命周期

掌握 Activity 的生命周期对任何 Android 开发者来说都非常重要,当你深入理解 Activity的生命周期之后,就可以写出更加连贯流畅的程序,你的应用程序也将会拥有更好的用户体验。

  1. 返回栈
    Android 是使用任务来管理 Activity 的,栈是一种先进后出的数据结构,在默认情况下,每当我们启动了一个新的 Activity,它就会入栈,并处于栈顶的位置。而每当我们按下 Back 键或调用 finish() 方法去销毁一个 Activity 时,处于栈顶的 Activity 就会出栈,前一个入栈的 Activity 就会重新处于栈顶的位置。系统总是会显示处于栈顶的 Activity 给用户。

  2. Activity 状态

    • 运行状态
      当一个 Activity 位于返回栈的栈顶时,Activity 就处于运行状态。系统最不愿意回收的就是处于运行状态的 Activity,因为这会带来非常差的用户体验。
    • 暂停状态
      当一个 Activity 不再处于栈顶位置,但仍然可见时,Activity 就进入了暂停状态。你可能会觉得,既然 Activity 已经不在栈顶了,怎么会可见呢?这是因为并不是每一个 Activity 都会占满整个屏幕,比如对话框形式的 Activity 只会占用屏幕中间的部分区域。处于暂停状态的 Activity 仍然是完全存活着的,系统也不愿意回收这种 Activity(因为它还是可见的,回收可见的东西都会在用户体验方面有不好的影响),只有在内存极低的情况下,系统才会去考虑回收这种 Activity。
    • 停止状态
      当一个 Activity 不再处于栈顶位置,并且完全不可见的时候,就进入了停止状态。系统仍然会为这种 Activity 保存相应的状态和成员变量,但是这并不是完全可靠的,当其他地方需要内存时,处于停止状态的 Activity 有可能会被系统回收。
    • 销毁状态
      一个 Activity 从返回栈中移除后就变成了销毁状态。系统最倾向于回收处于这种状态的 Activity,以保证手机的内存充足。
  3. Activity 的什么周期
    Activity 类中定义了 7 个回调方法,覆盖了 Activity 生命周期的每一个环节,下面就来一一介绍这 7 个方法。

    • onCreate()
      会在 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 种生存期。

    • 完整生存期
      Activity 在 onCreate() 方法和 onDestroy() 方法之间所经历的就是完整生存期。一般情况下,一个 Activity 会在 onCreate() 方法中完成各种初始化操作,而在
      onDestroy() 方法中完成释放内存的操作。
    • 可见生存期
      Activity 在 onStart() 方法和 onStop() 方法之间所经历的就是可见生存期。在可见生存期内,Activity 对于用户总是可见的,即便有可能无法和用户进行交互。我们可
      以通过这两个方法合理地管理那些对用户可见的资源。比如在 onStart() 方法中对资源进行加载,而在 onStop() 方法中对资源进行释放,从而保证处于停止状态的 Activity 不会占用过多内存。
    • 前台生存期
      Activity 在 onResume() 方法和 onPause() 方法之间所经历的就是前台生存期。在前台生存期内,Activity 总是处于运行状态,此时的 Activity 是可以和用户进行交互的,我们平时看到和接触最多的就是这个状态下的 Activity。
  4. 体验 Activity 的什么周期
    讲了这么多理论知识,是时候进行实战了。下面我们将通过一个实例,让你可以更加直观地体验 Activity 的生命周期。

    我们首先新建一个工程 ActivityLifeCycleTest,然后再再这个工程下创建三个 Activity,分别是 MainActivity、NormalActivity 和 DialogActivity,并在 AndroidManifest 中设置 DialogActivity 的 theme 为 @style/Theme.AppCompat.Dialog,使得 DialogActivity 的主题是对话框式,方便我们后面的测试。

    在 MainActivity 里面定义两个按钮分别启动 NormalActivity 和 DialogActivity,来看什么周期的变化,具体代码如下所示:

    class MainActivity : AppCompatActivity() {
    
    	private val tag = "测试生命周期"
    
    	override fun onCreate(savedInstanceState: Bundle?) {
    		super.onCreate(savedInstanceState)
    		setContentView(R.layout.activity_main)
    
    		Log.d(tag, "onCreate")
    
    		startNormalActivity.setOnClickListener {
    			startActivity(Intent(this, NormalActivity::class.java))
    		}
    
    		startDialogActivity.setOnClickListener {
    			startActivity(Intent(this, DialogActivity::class.java))
    		}
    	}
    
    override fun onStart() {
    		super.onStart()
    
    		Log.d(tag, "onStart")
    	}
    
    	override fun onResume() {
    		super.onResume()
    
    		Log.d(tag, "onResume")
    	}
    
        override fun onPause() {
        	super.onPause()
    
        	Log.d(tag, "onPause")
        }
    
        override fun onStop() {
        	super.onStop()
    
        	Log.d(tag, "onStop")
        }
    
        override fun onDestroy() {
        	super.onDestroy()
    
        	Log.d(tag, "onDestroy")
        }
    
        override fun onRestart() {
        	super.onRestart()
    
        	Log.d(tag, "onRestart")
        }
    }
    
    

    运行程序,当 MainActivity 第一次被创建时会依次执行 onCreate()、onStart() 和 onResume() 方法:

    image-20210415092816629.png

    然后点击第一个按钮,启动 NormalActivity,MainActivity 生命周期变化如下所示,由于 NormalActivity 已经把 MainActivity 完全遮挡住,因此 onPause() 和 onStop() 方法都会得到执行。

    image-20210415093237693.png

    然后按下 Back 按键回到 MainActivity,MainActivity 生命周期变化如下所示,由于之前 MainActivity 已经进入了停止状态,所以 onRestart() 方法会得到执行,之后会依次执行 onStart() 和 onResume() 方法。注意,此时 onCreate() 方法不会执行,因为MainActivity 并没有重新创建。

    image-20210415094002268.png

    以上就是启动 NormalActivity,MainActivity 的生命周期变化情况。

    现在我们来看启动 DialogActivity 的情况,同样当 MainActivity 第一次被创建时会依次执行 onCreate()、onStart() 和 onResume() 方法,点击启动 DialogActivity,这时候只有 onPause() 会被调用,onStop()方法并没有执行,这是因为 DialogActivity 并没有完全遮挡住 MainActivity,此时 MainActivity 只是进入了暂停状态,并没有进入停止状态。相应地,按下 Back 键返回 MainActivity 也应该只有 onResume() 方法会得到执行。

    image-20210415101123292.png

    image-20210415101417857.png

    最后在 MainActivity 按下 Back 键退出程序,MainActivity 的生命周期如下所示:

    image-20210415101623612.png

    以上只是单 Activity 的生命周期,如果要看多 Activity 生命周期执行的先后顺序,同样只要把日志打印出来看就可以了。

  5. Activity 被回收了怎么办
    前面我们说过,当一个 Activity 进入了停止状态,是有可能被系统回收的。那么想象以下场景:应用中有一个 Activity A,用户在 Activity A 的基础上启动了 Activity B,Activity A 就进入了停止状态,这个时候由于系统内存不足,将 Activity A 回收掉了,然后用户按下 Back 键返回 Activity A,会出现什么情况呢?其实还是会正常显示 Activity A 的,只不过这时并不会执行 onRestart() 方法,而是会执行 Activity A 的 onCreate() 方法,因为 Activity A 在这种情况下会被重新创建一次。

    这样看上去好像一切正常,可是别忽略了一个重要问题:Activity A 中是可能存在临时数据和状态的。打个比方,MainActivity 中如果有一个文本输入框,现在你输入了一段文字,然后启动 NormalActivity,这时 MainActivity 由于系统内存不足被回收掉,过了一会你又点击了 Back 键回到 MainActivity,你会发现刚刚输入的文字都没了,因为 MainActivity 被重新创建了。

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

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

    override fun onSaveInstanceState(outState: Bundle) {
       super.onSaveInstanceState(outState)
    	val tempData = "Something you just typed"
    	outState.putString("data_key", tempData)
    }
    

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

    override fun onCreate(savedInstanceState: Bundle?) {
    	super.onCreate(savedInstanceState)
    	Log.d(tag, "onCreate")
    	setContentView(R.layout.activity_main)
    	if (savedInstanceState != null) {
    		val tempData = savedInstanceState.getString("data_key")
    		Log.d(tag, "tempData is $tempData")
    	}
    ...	
    }	
    

Activity 的启动模式

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

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

    我们现在通过实践来体会一下 standard 模式,这次还是在 ActivityTest 项目的基础上修改。首先关闭 ActivityLifeCycleTest 项目,打开 ActivityTest 项目。修改 FirstActivity 中 onCreate() 方法的代码,如下所示:

    btn_test_standard.setOnClickListener {
       startActivity(Intent(this, FirstActivity::class.java))
    }
    

    这个代码写的比较奇怪,在 FirstActivity 里面启动 FirstActivity,但是我们主要是为了测试 standard 这个模式,这种模式下,每点击一次按钮,就会生产一个 FirstActivity 实例,并且会处于返回栈的栈顶,假设我们点击了 3 次按钮,那么如果我们要退出程序就要按三次 Back 按键。

    image-20210415111304204.png

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

    我们还是通过实践来体会一下,修改 AndroidManifest.xml 中 FirstActivity 的启动模式,如下所示:

    <activity
    android:name=".FirstActivity"
    android:label="This is FirstActivity"
    android:launchMode="singleTop">
    	<intent-filter>
     		<action android:name="android.intent.action.MAIN" />
       		<category android:name="android.intent.category.LAUNCHER" />
    		</intent-filter>
    </activity>
    

    还是运行上面启动 FirstActivity 的程序,这时候因为启动模式变成了 singleTop,而且 FirstActivity 已经在栈顶了,每当想要再启动一个 FirstActivity 时,都会直接使用栈顶的 Activity 所以就不会再创建 FirstActivity 的实例了。

    不过当 FirstActivity 并未处于栈顶位置时,再启动 FirstActivity 还是会创建新的实例的。

    比如说从 FirstActivity 启动 SecondActivity,再从 SecondActivity 启动 FirstActivity,这时候虽然 FirstActivity 的启动模式是 singleTop,但是不在栈顶,所以还是会再创建 FirstActivity 的实例的。

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

    我们还是通过代码来更加直观地理解一下。修改 AndroidManifest.xml 中FirstActivity 的启动模式为 singleTask,然后在 FirstActivity 中添加 onRestart() 方法,在 SecondActivity 中添加 onDestroy() 方法,来进行测试,从 FirstActivity 启动 SecondActivity,再从 SecondActivity 启动 FirstActivity,查看 Logcat 中的打印信息,如下所示:

    image-20210415114921709.png

    从打印信息中就可以明显看出,在 SecondActivity 中启动 FirstActivity 时,会发现返回栈中已经存在一个 FirstActivity 的实例,并且是在 SecondActivity 的下面,于是 SecondActivity 会从返回栈中出栈,而 FirstActivity 重新成为了栈顶 Activity,因此FirstActivity 的 onRestart() 方法和 SecondActivity 的 onDestroy() 方法会得到执行。现在返回栈中只剩下一个 FirstActivity 的实例了,按一下 Back 键就可以退出程序。

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

    应用场景:呼叫来电界面。这种模式的使用情况比较罕见,除非你确定你需要使 Activity 只有一个实例。建议谨慎使用。

Activity 的最佳实践

关于 Activity,我们已经掌握了非常多的知识,不过恐怕离能够完全灵活运用还有一段距离。虽然知识点只有这么多,但运用的技巧却是多种多样的。所以,在这里我准备教你几种关于 Activity 的最佳实践技巧,这些技巧在你以后的开发工作当中将会非常有用。

  1. 知晓当前是在哪一个 Activity
    这个技巧将教会你如何根据程序当前的界面就能判断出这是哪一个 Activity。可能你会觉得挺纳闷的,我自己写的代码怎么会不知道这是哪一个 Activity 呢?然而现实情况是,在你进入一家公司之后,更有可能的是接手一份别人写的代码,因为你刚进公司就正好有一个新项目启动的概率并不高。阅读别人的代码时有一个很头疼的问题,就是当你需要在某个界面上修改一些非常简单的东西时,却半天找不到这个界面对应的 Activity 是哪一个。学会了本节的技巧之后,这对你来说就再也不是难题了。

    我们还是在 ActivityTest 项目的基础上修改,首先需要新建一个 BaseActivity 类,注意,这里的 BaseActivity 和普通 Activity 的创建方式并不一样,因为我们不需要让 BaseActivity 在 AndroidManifest.xml 中注册,所以选择创建一个普通的 Kotlin 类就可以了。然后让 BaseActivity 继承自 AppCompatActivity,并重写 onCreate() 方法,如下所示:

    open class BaseActivity : AppCompatActivity() {
       override fun onCreate(savedInstanceState: Bundle?) {
        	super.onCreate(savedInstanceState)
    	Log.d("BaseActivity", javaClass.simpleName)
       }
    }
    

    我们在 onCreate() 方法中加了一行日志,用于打印当前实例的类名。这里我要额外说明一下,Kotlin 中的 javaClass 表示获取当前实例的 Class 对象,相当于在 Java 中调用getClass() 方法;而 Kotlin 中的 BaseActivity::class.java 表示获取BaseActivity 类的 Class 对象,相当于在 Java 中调用 BaseActivity.class。在上述代码中,我们先是获取了当前实例的 Class 对象,然后再调用 simpleName 获取当前实例的类名。

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

    现在重新运行程序,然后通过点击按钮分别进入 FirstActivity 和 SecondActivity 的界面,这时观察 Logcat 中的打印信息,如图所示:

    image-20210415141828612.png

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

  2. 随时随地退出程序
    如果目前你手机的界面还停留在 ThirdActivity,你会发现当前想退出程序是非常不方便的,需要连按 3 次 Back 键才行。按 Home 键只是把程序挂起,并没有退出程序。如果我们的程序需要注销或者退出的功能该怎么办呢?看来要有一个随时随地都能退出程序的方案才行。

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

    新建一个单例类 ActivityCollector 作为 Activity 的集合,代码如下所示:

    object ActivityCollector {
    
       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) {
               if (!activity.isFinishing) {
                   activity.finish()
               }
           }
           activities.clear()
       }
    }
    

    这里使用了单例类,是因为全局只需要一个 Activity 集合。在集合中,我们通过一个 ArrayList 来暂存 Activity,然后提供了一个 addActivity() 方法,用于向 ArrayList中添加 Activity;提供了一个 removeActivity() 方法,用于从 ArrayList 中移除 Activity;最后提供了一个 finishAll() 方法,用于将 ArrayList 中存储的 Activity 全部销毁。注意在销毁 Activity 之前,我们需要先调用 activity.isFinishing 来判断 Activity 是否正在销毁中,因为 Activity 还可能通过按下 Back 键等方式被销毁,如果该 Activity 没有正在销毁中,我们再去调用它的 finish() 方法来销毁它。

    接下来修改 BaseActivity 中的代码,如下所示:

    open class BaseActivity : AppCompatActivity() {
     
     override fun onCreate(savedInstanceState: Bundle?) {
     	super.onCreate(savedInstanceState)
     	Log.d("BaseActivity", javaClass.simpleName)
             
     	ActivityCollector.addActivity(this)
     }
     
     override fun onDestroy() {
     	super.onDestroy()
             
    		ActivityCollector.removeActivity(this)
    	}
    }
    

    在 BaseActivity 的 onCreate() 方法中调用了 ActivityCollector 的 addActivity() 方法,表明将当前正在创建的 Activity 添加到集合里。然后在 BaseActivity 中重写 onDestroy() 方法,并调用了 ActivityCollector 的 removeActivity() 方法,表明从集合里移除一个马上要销毁的 Activity。

    从此以后,不管你想在什么地方退出程序,只需要调用 ActivityCollector.finishAll() 方法就可以了。例如在 ThirdActivity 界面想通过点击按钮直接退出程序,只需将代码改成如下形式:

    class ThirdActivity : BaseActivity() {
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
             setContentView(R.layout.third_layout)
    
             Log.d("ThirdActivity", "Task id is $taskId")
    
             button3.setOnClickListener {
                 ActivityCollector.finishAll()
             }
     	}
    }
    

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

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

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

  3. 启动 Activity 的最佳写法
    启动 Activity 的方法相信你已经非常熟悉了,首先通过 Intent 构建出当前的“意图”,然后调用 startActivity() 或 startActivityForResult() 方法将 Activity 启动起来,如果有数据需要在 Activity 之间传递,也可以借助 Intent 来完成。

    假设 SecondActivity 中需要用到两个非常重要的字符串参数,在启动 SecondActivity 的时候必须传递过来,那么我们很容易会写出如下代码:

    val intent = Intent(this, SecondActivity::class.java)
    intent.putExtra("param1", "data1")
    intent.putExtra("param2", "data2")
    startActivity(intent)
    

    虽然这样写是完全正确的,但是在真正的项目开发中经常会出现对接的问题。比如 SecondActivity 并不是由你开发的,但现在你负责开发的部分需要启动 SecondActivity,而你却不清楚启动 SecondActivity 需要传递哪些数据。这时无非就有两个办法:一个是你自己去阅读 SecondActivity 中的代码,另一个是询问负责编写 SecondActivity 的同事。你会不会觉得很麻烦呢?其实只需要换一种写法,就可以轻松解决上面的窘境。

    修改 SecondActivity 中的代码,如下所示:

    class SecondActivity : BaseActivity() {
        ...
        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)
            }
        }
    }
    

    在这里我们使用了一个新的语法结构 companion object,并在 companion object 中定义了一个 actionStart() 方法。之所以要这样写,是因为 Kotlin 规定,所有定义在 companion object 中的方法都可以使用类似于 Java 静态方法的形式调用。

    接下来我们重点看 actionStart() 方法,在这个方法中完成了 Intent 的构建,另外所有 SecondActivity 中需要的数据都是通过 actionStart() 方法的参数传递过来的,然后把它们存储到 Intent 中,最后调用 startActivity() 方法启动 SecondActivity。

    这样写的好处在哪里呢?最重要的一点就是一目了然,SecondActivity 所需要的数据在方法参数中全部体现出来了,这样即使不用阅读 SecondActivity 中的代码,不去询问负责编写SecondActivity 的同事,你也可以非常清晰地知道启动 SecondActivity 需要传递哪些数据。另外,这样写还简化了启动 Activity 的代码,现在只需要一行代码就可以启动SecondActivity,如下所示:

    button1.setOnClickListener {
    	SecondActivity.actionStart(this, "data1", "data2")
    }
    

    养成一个良好的习惯,给你编写的每个 Activity 都添加类似的启动方法,这样不仅可以让启动 Activity 变得非常简单,还可以节省不少你同事过来询问你的时间。

小结

本章我们不管是理论型还是实践型的东西都涉及到了,从 Activity 的基本用法,到启动 Activity 和传递数据的方式,再到 Activity 的生命周期以及 Activity 的启动模式,你几乎已经学会了关于 Activity 所有重要的知识点。在本章的最后,还学习了几种可以应用在 Activity 中的最佳实践技巧。毫不夸张地说,你在 Android Activity 方面已经算是一个小高手了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值