Android的活动介绍
1. 活动是什么
活动(Activity)是最容易吸引用户的地方,它是一种可以包含用户界面的组件,主要用于和用户进行交互。一个应用程序中可以包含零个或多个活动,但不包含任何活动的应用程序很少见,谁也不想让自己的应用永远无法被用户看到吧?
2. Intent 在活动中的使用
Intent
大致可分为两种:显示 Intent 和 隐式 Intent
使用显示 Intent
Intent 有多个构造函数的重载,其中一个是Intent(Context packageContext,Class<?>cls)
- 第一个参数Context要求提供一个启动活动的上下文
- 第二个参数Class则是指定想要启动的目标活动
代码如下所示:
// 显示Intent
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
startActivity(intent);
使用隐式 Intent
隐式 Intent并不明确指出想要启动哪一个活动,而是指定了一系列更为抽象的action和category等信息,然后交由系统去分析这个Intent,并找出合适的活动去启动。
用法:打开AndroidManifest.xml,添加如下代码:
<activity android:name=".inquiry_activity.SecondActivity">
<intent-filter>
<!-- 指定活动能响应的action和category -->
<action android:name="com.yezijie.ACTION_START"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
此时在activity中的代码修改为:
// 隐式Intent
Intent intent = new Intent("com.yezijie.ACTION_START");
startActivity(intent);
只有和中的内容同时能够匹配上Intent中指定的action和category时,活动才能响应该Intent。上例中android.intent.category.DEFAULT
是一种默认的category,若Intent中增加一个category:
// 隐式Intent
Intent intent = new Intent("com.yezijie.ACTION_START");
intent.addCategory("com.yezijie.MY_CATEGORY");//增加一个category
startActivity(intent);
此时在 标签中应再添加一个category声明:
<activity android:name=".inquiry_activity.SecondActivity">
<intent-filter>
<!-- 指定活动能响应的action和category -->
<action android:name="com.yezijie.ACTION_START"/>
<category android:name="android.intent.category.DEFAULT"/>
<!-- 新添加的category -->
<category android:name="android.intent.category.MY_CATEGORY"/>
</intent-filter>
</activity>
更多隐式 Intent 的用法
使用隐式Intent,还可以启动其他程序的活动,比如调用系统的浏览器来打开某个网页:
// 隐式Intent:打开网页
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("http://www.baidu.com"));
startActivity(intent);
比如调用系统的拨号界面:
// 隐式Intent:打开系统拨号界面
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
在标签中再配置一个标签,用于更精确地指定当前活动能够响应什么类
型的数据。标签中主要可以配置以下内容。
android:scheme
。用于指定数据的协议部分,如上例中的http部分、geo表示显示地理位置、tel表示拨打电话。
android:host
。用于指定数据的主机名部分,如上例中的www.baidu.com部分。
android:port
。用于指定数据的端口部分,一般紧随在主机名之后。
android:path
。用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内容。
android:mimeType
。用于指定可以处理的数据类型,允许使用通配符的方式进行指定。
只有标签中指定的内容和Intent
中携带的Data
完全一致时,当前活动才能够响应该Intent。不过一般在标签中都不会指定过多的内容,如上面浏览器示例中,其实只需要指定android:scheme
为http
,就可以响应所有的http
协议的Intent
了。
向下一个活动传递数据
Intent
中提供了一系列 putExtra()
方法的重载,可把要传递的数据暂存在Intent
中,启动了另一个活动后再从Intent
中取出。如FirstActivity
中传递一个字符串到SecondActivity
中,FirstActivity
中的代码为:
String data = "hello SecondActivity";
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
intent.putExtra("extra_data",data);
startActivity(intent);
SecondActivity
中的代码如下:
Intent intent = getIntent();
// String:getStringExtra(); int:getIntExtra(); 布尔型:getBooleanExtra() ...以此类推
String data = intent.getStringExtra("extra_data");
Log.d(TAG, data);
返回数据给上一个活动
Activity
中有个startActivityForResult
() 方法用于启动活动,此方法在活动销毁时能返回一个结果给上个活动。startActivityForResult()
方法接收两个参数:Intent、请求码(用作回调时判断数据来源)。
修改FirstActivity
中的代码如下:
Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
startActivityForResult(intent,1);//请求码只要是唯一值就行了,这里传入1
接着在SecondActivity
中添加返回数据:
Intent intent = new Intent();
intent.putExtra("data_return","hello FirstActivity");
setResult(RESULT_OK,intent);
finish();//销毁当前活动
在SecondActivity
被销毁后回调上一个活动的onActivityResult()
方法,因此还需要在FirstActivity
中重写此方法来得到返回数据:
/**
* 处理得到的返回数据
* @param requestCode 启动活动时传入的请求码
* @param resultCode 返回数据时传入的处理结果
* @param data 携带返回数据的Intent
*/
@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(TAG, returnedData);
}
break;
default:
break;
}
}
若在SecondActivity
是通过按下Back键回到FirstActivity
,可以在SecondActivity
中重写onBackPressed()
方法:
@Override
public void onBackPressed() {
Intent intent = new Intent();
intent.putExtra("data_return","hello FirstActivity");
setResult(RESULT_OK,intent);
finish();
}
3. 活动的生命周期
返回栈
Android是使用任务(Task)来管理活动的,一个任务就是一组存放在栈里的活动的集合,这个栈也被称作返回栈(Back Stack)。栈是一种后进先出的数据结构,在默认情况下,每当我们启动了一个新的活动,它会在返回栈中入栈,并处于栈顶的位置。而每当我们按下Back 键或调用finish()方法去销毁一个活动时,处于栈顶的活动会出栈,这时前一个入栈的活动就会重新处于栈顶的位置。系统总是会显示处于栈顶的活动给用户。
活动状态
每个活动在其生命周期中最多可能会有4种状态。
-
运行状态
当一个活动位于返回栈的栈顶时,这时活动就处于运行状态。系统最不愿意回收的就是处于运行状态的活动,因为这会带来非常差的用户体验。 -
暂停状态
当一个活动不再处于栈顶位置,但仍然可见时,这时活动就进入了暂停状态。你可能会觉得既然活动已经不在栈顶了,还怎么会可见呢?这是因为并不是每一 个活动都会占满整个屏幕的,比如对话框形式的活动只会占用屏幕中间的部分区域,系统也不愿意去回收这种活动(因为它还是可见的,回收可见的东西都会在用户体验方面有不好的影响),只有在内存极低的情况下,系统才会去考虑回收这种活动。 -
停止状态
当一个活动不再处于栈顶位置,并且完全不可见的时候,就进入了停止状态。系统仍然会为这种活动保存相应的状态和成员变量,但是这并不是完全可靠的,当其他地方需要内存时,处于停止状态的活动有可能会被系统回收。 -
销毁状态
当一个活动从返回栈中移除后就变成了销毁状态。系统会最倾向于回收处于这种状态的活动,从而保证手机的内存充足。
活动的生存期
Activity类中定义了7个回调方法,覆盖了活动生命周期的每一个环节。
-
onCreate()
。每个活动中都重写了这个方法,它会在活动第一次被创建的时候调用。在这个方法中完成活动的初始化操作,比如说加载布局、绑定事件等。 -
onStart()
。这个方法在活动由不可见变为可见的时候调用。 -
onResume()
。这个方法在活动准备好和用户进行交互的时候调用。此时的活动一定位于返回栈的栈顶,并且处于运行状态。 -
onPause()
。这个方法在系统准备去启动或者恢复另一个活动的时候调用。通常会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶活动的使用。 -
onStop()
。这个方法在活动完全不可见的时候调用。它和onPause()
方法的主要区别在于,如果启动的新活动是一个对话框式的活动,那么onPause()
方法会得到执行,而onStop()
方法并不会执行。 -
onDestroy()
。这个方法在活动被销毁之前调用,之后活动的状态将变为销毁状态。 -
onRestart()
。这个方法在活动由停止状态变为运行状态之前调用,也就是活动被重新启动了。
以上7个方法中除了
onRestart()
方法,其他都是两两相对的,从而又可以将活动分为3种生存期。
- 完整生存期。活动在
onCreate()
方法和onDestroy()
方法之间所经历的,就是完整生存期。- 可见生存期。活动在
onStart()
方法和onStop()
方法之间所经历的,就是可见生存期。- 前台生存期。活动在
onResume()
方法和onPause()
方法之间所经历的,就是前台生存期。
活动被回收
Activity
中提供了一个 onSaveInstanceState()
回调方法,这 个方法会保证一定在活动被回收之前调用。onSaveInstanceState()
方法会携带一个 Bundle 类型的参数,Bundle 提供了一系列的方法用于保存数据,如用 putString()
方法保存字符串,用 putInt()
方法保存整型数据, 以此类推。
每个保存方法需要传入两个参数,
-
第一个参数是键,用于后面从 Bundle 中取值,
-
第二个参数是真正要保存的内容。
在Activity 中添加如下代码就可以将临时数据进行保存:
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
String tempData = "Something you just typed";
outState.putString("data_key", tempData);
}
Activity 被回收后的恢复操作,修改其onCreate()
方法,如下所示:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState != null) {
// 若不为空,取出相应的数据
String tempData = savedInstanceState.getString("data_key");
Log.d(TAG, tempData);
}
}
注:
Intent
还可以结合Bundle
一起用于传递数据,首先可以把需要传递的数据都保存在Bundle
对象中,然后再将Bundle
对象存放在Intent
里。到了目标活动之后先从Intent
中取出Bundle
,再从Bundle
中一一取出数据。
活动的启动模式
启动模式一共有4种,分别是 standard
、singleTop
、singleTask
和singleInstance
,可以在AndroidManifest.xml
中通过给标签指定android:launchMode
属性来选择启动模式。
standard
standard 是活动默认的启动模式,在不进行显式指定的情况下,所有活动都会自动使用这种启动模式。在 standard 模式(即默认情况)下,每当启动一个新的活动,它就会在返回栈中入栈,并处于栈顶的位置。对于使用 standard 模式的活动,系统不会在乎这个活动是否已经在返回栈中存在,每次启动都会创建该活动的一个新的实例。
singleTop
当活动的启动模式指定为singleTop
,在启动活动时如果发现返回栈的栈顶已经是该活动,则认为可以直接使用它,不会再创建新的活动实例。不过当Activity
并未处于栈顶位置时,这时再启动Activity
,还是会创建新的实例的。
singleTask
当活动的启动模式指定为singleTask
,每次启动该活动时系统首先会在返回栈中检查是否存在该活动的实例,如果发现已经存在则直接使用该实例,并把在这个活动之上的所有活动统统出栈,如果没有发现就会创建一个新的活动实例。
singleInstance
singleInstance
模式应该算是4种启动模式中最特殊也最复杂的一个了,不同于以上3种启动模式,指定为singleInstance
模式的活动会启用一个新的返回栈来管理这个活动(其实如果singleTask
模式指定了不同的 taskAffinity
,也会启动一个新的返回栈)。假设我们的程序中有一个活动是允许其他程序调用的,如果我们想实现其他程序和我们的程序可以共享这个活动的实例,应该如何实现呢?使用前面3种启动模式肯定是做不到的,因为每个应用程序都会有自己的返回栈,同一 个活动在不同的返回栈中入栈时必然是创建了新的实例。而使用singleInstance
模式就可以解决这个问题,在这种模式下会有一个单独的返回栈来管理这个活动,不管是哪个应用程序来访问这个活动,都共用的同一个返回栈,也就解决了共享活动实例的问题。
知晓当前是在哪一个活动
新建一个BaseActivity
类,然后让BaseActivity
继承自AppCompatActivity
,并重写onCreate()
方法,如下所示:
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("BaseActivity", getClass().getSimpleName());
}
}
getClass().getSimpleName()
获取了当前实例的类名。接下来我们需要让BaseActivity
成为Activity
项目中所有活动的父类。
随时随地退出程序
新建一个ActivityCollector
类作为活动管理器。
public class ActivityCollector {
public static List<Activity> activities = new ArrayList<>();
public static void addActivity(Activity activity) {
activities.add(activity);
}
public static void removeActivity(Activity activity) {
activities.remove(activity);
}
public static void finishAll() {
for (Activity activity : activities) {
if (!activity.isFinishing()) {
activity.finish();
}
}
activities.clear();
}
}
通过一个List来暂存活动,然后提供了一个addActivity()
方法用于向List中添加一个活动,提供了一个removeActivity()
方法用于从List中移除活动,最后提供了一个finishAll()
方法用于将List
中存储的活动全部销毁掉。
接下来修改BaseActivity
中的代码
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("BaseActivity", getClass().getSimpleName());
ActivityCollector.addActivity(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
ActivityCollector.removeActivity(this);
}
}
不管你想在什么地方退出程序,只需要调用ActivityCollector.finishAll()
方法就可以了。
当然你还可以在销毁所有活动的代码后面再加上杀掉当前进程的代码,以保证程序完全退出,杀掉进程的代码如下所示:
android.os.Process.killProcess(android.os.Process.myPid());
其中,killProcess()
方法用于杀掉一个进程,它接收一个进程id参数,可以通过myPid()
方法来获得当前程序的进程id。需要注意的是,killProcess()
方法只能用于杀掉当前程序的进程,不能使用这个方法去杀掉其他程序。
启动活动的最佳写法
动活动的方法:通过 Intent
构建出当前的“意图”,然后调用 startActivity()
或 startActivityForResult()
方法将活动启动起来,如果有数据需要从一个活 动传递到另一个活动,也可以借助 Intent
来完成。假设 SecondActivity
中需要用到两个非常重要的字符串参数,在启动 SecondActivity
的时候必须要传递过来,那么我们很容易会写出如下代码:
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
intent.putExtra("param1", "data1");
intent.putExtra("param2", "data2");
startActivity(intent);
但此时 SecondActivity
并不是由你开发的,但现在你负责的部分需要有启动SecondActivity
这个功能,而你却不清楚启动这个活动需要传递哪些数据。这时无非就有两种办法,一个是你自己去阅读 SecondActivity
中的代码,二是询问负责编写 SecondActivity
的同事。你会不会觉得很麻烦呢?其实只需要换一种写法,就可以轻松解决掉上面的窘境。
修改 SecondActivity
中的代码,如下所示:
public class SecondActivity extends BaseActivity {
public static void actionStart(Context context, String data1, String data2) {
Intent intent = new Intent(context, SecondActivity.class);
intent.putExtra("param1", data1);
intent.putExtra("param2", data2);
context.startActivity(intent);
}
……
}
我们在 SecondActivity
中添加了一个 actionStart()
方法,在这个方法中完成了 Intent
的构建,另外所有 SecondActivity
中需要的数据都是通过 actionStart()
方法的参数传递过来的,然后把它们存储到 Intent
中,最后调用 startActivity()
方法启动 SecondActivity
。
这样写的好处在哪里呢?最重要的一点就是一目了然,SecondActivity
所需要的数据全部都在方法参数中体现出来了,这样即使不用阅读 SecondActivity
中的代码,或者询问负责编写 SecondActivity
的同事,你也可以非常清晰地知道启动 SecondActivity
需要传递哪些数据。
另外,这样写还简化了启动活动的代码,现在只需要一行代码就可以启动 SecondActivity
, 如下所示:
SecondActivity.actionStart(FirstActivity.this, "data1", "data2");