《第一行代码 第二版》第二章
《第一行代码》
本博客是对第一行代码的精简总结,仅供个人学习使用。如需系统学习请购买正版或者电子书籍。
链接附上 🔗图灵社区:https://www.ituring.com.cn/book/2744/
第 2 章 先从看得到的入手——探究活动
2.1 使用显式Intent
Intent是Android程序中各组件之间进行交互的一种重要方式,它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent一般可被用于启动活动、启动服务以及发送广播等场景,由于服务、广播等概念你暂时还未涉及,那么本章我们的目光无疑就锁定在了启动活动上面。
Intent大致可以分为两种:显式Intent和隐式Intent
Intent有多个构造函数的重载,其中一个是Intent(Context packageContext, Class<?> cls)
。这个构造函数接收两个参数,第一个参数Context
要求提供一个启动活动的上下文,第二个参数Class
则是指定想要启动的目标活动,通过这个构造函数就可以构建出Intent
的“意图”。然后我们应该怎么使用这个Intent呢?Activity类中提供了一个startActivity()
方法,这个方法是专门用于启动活动的,它接收一个Intent
参数,这里我们将构建好的Intent传入startActivity()
方法就可以启动目标活动了。
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
startActivity(intent);
}
});
我们首先构建出了一个Intent,传入FirstActivity.this
作为上下文,传入SecondActivity.class
作为目标活动,这样我们的“意图”就非常明显了,即在FirstActivity这个活动的基础上打开SecondActivity这个活动。然后通过startActivity()
方法来执行这个Intent。
按下Back键就可以销毁当前活动,从而回到上一个活动了
2.2 使用隐式Intent
相比于显式Intent,隐式Intent则含蓄了许多,它并不明确指出我们想要启动哪一个活动,而是指定了一系列更为抽象的action
和category
等信息,然后交由系统去分析这个Intent,并帮我们找出合适的活动去启动。
什么叫作合适的活动呢?简单来说就是可以响应我们这个隐式Intent的活动,那么目前SecondActivity可以响应什么样的隐式Intent呢?额,现在好像还什么都响应不了,不过很快就会有了。
通过在<activity>
标签下配置<intent-filter>
的内容,可以指定当前活动能够响应的action
和category
,打开AndroidManifest.xml,添加如下代码:
<activity android:name=".SecondActivity" >
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
在<action>
标签中我们指明了当前活动可以响应com.example.activitytest.ACTION_START
这个action
,而<category>
标签则包含了一些附加信息,更精确地指明了当前的活动能够响应的Intent中还可能带有的category
。只有<action>
和<category>
中的内容同时能够匹配上Intent中指定的action
和category
时,这个活动才能响应该Intent。
修改FirstActivity中按钮的点击事件,代码如下所示:
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.activitytest.ACTION_START");
startActivity(intent);
}
});
可以看到,我们使用了Intent的另一个构造函数,直接将action
的字符串传了进去,表明我们想要启动能够响应com.example.activitytest.ACTION_START
这个action
的活动。那前面不是说要<action>
和<category>
同时匹配上才能响应的吗?怎么没看到哪里有指定category
呢?这是因为android.intent.category.DEFAULT
是一种默认的category
,在调用startActivity()
方法的时候会自动将这个category
添加到Intent中。
每个Intent中只能指定一个action
,但却能指定多个category
。目前我们的Intent中只有一个默认的category
,那么现在再来增加一个吧。
2.3 更多隐式Intent的用法
使用隐式Intent,我们不仅可以启动自己程序内的活动,还可以启动其他程序的活动,这使得Android多个应用程序之间的功能共享成为了可能。比如说你的应用程序中需要展示一个网页,这时你没有必要自己去实现一个浏览器(事实上也不太可能),而是只需要调用系统的浏览器来打开这个网页就行了
修改FirstActivity中按钮点击事件的代码,如下所示:
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("http://www.baidu.com"));
startActivity(intent);
}
});
这里我们首先指定了Intent的action
是Intent.ACTION_VIEW
,这是一个Android系统内置的动作,其常量值为android.intent.action.VIEW
。然后通过Uri.parse()
方法,将一个网址字符串解析成一个Uri
对象,再调用Intent的setData()
方法将这个Uri
对象传递进去。
在上述代码中,可能你会对setData()
部分感觉到陌生,这是我们前面没有讲到的。这个方法其实并不复杂,它接收一个Uri
对象,主要用于指定当前Intent正在操作的数据,而这些数据通常都是以字符串的形式传入到Uri.parse()
方法中解析产生的。
与此对应,我们还可以在<intent-filter>
标签中再配置一个<data>
标签,用于更精确地指定当前活动能够响应什么类型的数据。<data>
标签中主要可以配置以下内容。
android:scheme
。用于指定数据的协议部分,如上例中的http部分。android:host
。用于指定数据的主机名部分,如上例中的www.baidu.com部分。android:port
。用于指定数据的端口部分,一般紧随在主机名之后。android:path
。用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内容。android:mimeType
。用于指定可以处理的数据类型,允许使用通配符的方式进行指定。
只有<data>
标签中指定的内容和Intent中携带的Data完全一致时,当前活动才能够响应该Intent。不过一般在<data>
标签中都不会指定过多的内容,如上面浏览器示例中,其实只需要指定android:scheme
为http,就可以响应所有的http协议的Intent了。
图 2.18 选择响应Intent的程序
除了http协议外,我们还可以指定很多其他协议,比如geo表示显示地理位置、tel表示拨打电话。下面的代码展示了如何在我们的程序中调用系统拨号界面。
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}
});
首先指定了Intent的action
是Intent.ACTION_DIAL
,这又是一个Android系统的内置动作。然后在data部分指定了协议是tel,号码是10086。重新运行一下程序,在FirstActivity的界面点击一下按钮,结果如图2.19所示。
图 2.19 系统拨号界面
2.4 向下一个活动传递数据
其实Intent还可以在启动活动的时候传递数据
在启动活动时传递数据的思路很简单,Intent中提供了一系列putExtra()
方法的重载,可以把我们想要传递的数据暂存在Intent中,启动了另一个活动后,只需要把这些数据再从Intent中取出就可以了。比如说FirstActivity中有一个字符串,现在想把这个字符串传递到SecondActivity中,你就可以这样编写:
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String data = "Hello SecondActivity";
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
intent.putExtra("extra_data", data);
startActivity(intent);
}
});
这里我们还是使用显式Intent的方式来启动SecondActivity,并通过putExtra()
方法传递了一个字符串。注意这里putExtra()
方法接收两个参数,第一个参数是键,用于后面从Intent中取值,第二个参数才是真正要传递的数据。
然后我们在SecondActivity中将传递的数据取出,并打印出来,代码如下所示:
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.second_layout);
Intent intent = getIntent();
String data = intent.getStringExtra("extra_data");
Log.d("SecondActivity", data);
}
}
首先可以通过getIntent()
方法获取到用于启动SecondActivity的Intent,然后调用getStringExtra()
方法,传入相应的键值,就可以得到传递的数据了。这里由于我们传递的是字符串,所以使用getStringExtra()
方法来获取传递的数据。如果传递的是整型数据,则使用getIntExtra()
方法;如果传递的是布尔型数据,则使用getBooleanExtra()
方法,以此类推。
2.5 返回数据给上一个活动
既然可以传递数据给下一个活动,那么能不能够返回数据给上一个活动呢?答案是肯定的。不过不同的是,返回上一个活动只需要按一下Back键就可以了,并没有一个用于启动活动的Intent来传递数据。通过查阅文档你会发现,Activity中还有一个startActivityForResult()
方法也是用于启动活动的,但这个方法期望在活动销毁的时候能够返回一个结果给上一个活动。毫无疑问,这就是我们所需要的。
startActivityForResult()
方法接收两个参数,第一个参数还是Intent,第二个参数是请求码,用于在之后的回调中判断数据的来源。我们还是来实战一下,修改FirstActivity中按钮的点击事件,代码如下所示:
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
startActivityForResult(intent, 1);
}
});
这里我们使用了startActivityForResult()
方法来启动SecondActivity,请求码只要是一个唯一值就可以了,这里传入了1。接下来我们在SecondActivity中给按钮注册点击事件,并在点击事件中添加返回数据的逻辑,代码如下所示:
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.second_layout);
Button button2 = (Button) findViewById(R.id.button_2);
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent();
intent.putExtra("data_return", "Hello FirstActivity");
setResult(RESULT_OK, intent);
finish();
}
});
}
}
可以看到,我们还是构建了一个Intent,只不过这个Intent仅仅是用于传递数据而已,它没有指定任何的“意图”。紧接着把要传递的数据存放在Intent中,然后调用了setResult()
方法。这个方法非常重要,是专门用于向上一个活动返回数据的。setResult()
方法接收两个参数,第一个参数用于向上一个活动返回处理结果,一般只使用RESULT_OK
或RESULT_CANCELED
这两个值,第二个参数则把带有数据的Intent传递回去,然后调用了finish()
方法来销毁当前活动。
由于我们是使用startActivityForResult()
方法来启动SecondActivity的,在SecondActivity被销毁之后会回调上一个活动的onActivityResult()
方法,因此我们需要在FirstActivity中重写这个方法来得到返回的数据,如下所示:
@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("FirstActivity", returnedData);
}
break;
default:
}
}
onActivityResult()
方法带有三个参数,第一个参数requestCode
,即我们在启动活动时传入的请求码。第二个参数resultCode
,即我们在返回数据时传入的处理结果。第三个参数data
,即携带着返回数据的Intent。由于在一个活动中有可能调用startActivityForResult()
方法去启动很多不同的活动,每一个活动返回的数据都会回调到onActivityResult()
这个方法中,因此我们首先要做的就是通过检查requestCode
的值来判断数据来源。确定数据是从SecondActivity返回的之后,我们再通过resultCode
的值来判断处理结果是否成功。最后从data
中取值并打印出来,这样就完成了向上一个活动返回数据的工作。
2.6 活动的生命周期
2.6.1 返回栈
经过前面几节的学习,我相信你已经发现了这一点,Android中的活动是可以层叠的。我们每启动一个新的活动,就会覆盖在原活动之上,然后点击Back键会销毁最上面的活动,下面的一个活动就会重新显示出来。
2.6.2 活动状态
-
运行状态
当一个活动位于返回栈的栈顶时,这时活动就处于运行状态。系统最不愿意回收的就是处于运行状态的活动,因为这会带来非常差的用户体验。
-
暂停状态
当一个活动不再处于栈顶位置,但仍然可见时,这时活动就进入了暂停状态。你可能会觉得既然活动已经不在栈顶了,还怎么会可见呢?这是因为并不是每一个活动都会占满整个屏幕的,比如对话框形式的活动只会占用屏幕中间的部分区域,你很快就会在后面看到这种活动。处于暂停状态的活动仍然是完全存活着的,系统也不愿意去回收这种活动(因为它还是可见的,回收可见的东西都会在用户体验方面有不好的影响),只有在内存极低的情况下,系统才会去考虑回收这种活动。
-
停止状态
当一个活动不再处于栈顶位置,并且完全不可见的时候,就进入了停止状态。系统仍然会为这种活动保存相应的状态和成员变量,但是这并不是完全可靠的,当其他地方需要内存时,处于停止状态的活动有可能会被系统回收。
-
销毁状态
当一个活动从返回栈中移除后就变成了销毁状态。系统会最倾向于回收处于这种状态的活动,从而保证手机的内存充足。
2.6.3 活动的生存期
ctivity类中定义了7个回调方法,覆盖了活动生命周期的每一个环节,下面就来一一介绍这7个方法。
onCreate()
。这个方法你已经看到过很多次了,每个活动中我们都重写了这个方法,它会在活动第一次被创建的时候调用。你应该在这个方法中完成活动的初始化操作,比如说加载布局、绑定事件等。onStart()
。这个方法在活动由不可见变为可见的时候调用。onResume()
。这个方法在活动准备好和用户进行交互的时候调用。此时的活动一定位于返回栈的栈顶,并且处于运行状态。onPause()
。这个方法在系统准备去启动或者恢复另一个活动的时候调用。我们通常会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶活动的使用。onStop()
。这个方法在活动完全不可见的时候调用。它和onPause()
方法的主要区别在于,如果启动的新活动是一个对话框式的活动,那么onPause()
方法会得到执行,而onStop()
方法并不会执行。onDestroy()
。这个方法在活动被销毁之前调用,之后活动的状态将变为销毁状态。onRestart()
。这个方法在活动由停止状态变为运行状态之前调用,也就是活动被重新启动了。
以上7个方法中除了onRestart()
方法,其他都是两两相对的,从而又可以将活动分为3种生存期。
- 完整生存期。活动在
onCreate()
方法和onDestroy()
方法之间所经历的,就是完整生存期。一般情况下,一个活动会在onCreate()
方法中完成各种初始化操作,而在onDestroy()
方法中完成释放内存的操作。 - 可见生存期。活动在
onStart()
方法和onStop()
方法之间所经历的,就是可见生存期。在可见生存期内,活动对于用户总是可见的,即便有可能无法和用户进行交互。我们可以通过这两个方法,合理地管理那些对用户可见的资源。比如在onStart()
方法中对资源进行加载,而在onStop()
方法中对资源进行释放,从而保证处于停止状态的活动不会占用过多内存。 - 前台生存期。活动在
onResume()
方法和onPause()
方法之间所经历的就是前台生存期。在前台生存期内,活动总是处于运行状态的,此时的活动是可以和用户进行交互的,我们平时看到和接触最多的也就是这个状态下的活动。
2.6.4 活动被回收了怎么办
前面我们已经说过,当一个活动进入到了停止状态,是有可能被系统回收的。那么想象以下场景:应用中有一个活动A,用户在活动A的基础上启动了活动B,活动A就进入了停止状态,这个时候由于系统内存不足,将活动A回收掉了,然后用户按下Back键返回活动A,会出现什么情况呢?其实还是会正常显示活动A的,只不过这时并不会执行onRestart()
方法,而是会执行活动A的onCreate()
方法,因为活动A在这种情况下会被重新创建一次。
这样看上去好像一切正常,可是别忽略了一个重要问题,活动A中是可能存在临时数据和状态的。打个比方,MainActivity中有一个文本输入框,现在你输入了一段文字,然后启动NormalActivity,这时MainActivity由于系统内存不足被回收掉,过了一会你又点击了Back键回到MainActivity,你会发现刚刚输入的文字全部都没了,因为MainActivity被重新创建了。
如果我们的应用出现了这种情况,是会严重影响用户体验的,所以必须要想想办法解决这个问题。查阅文档可以看出,Activity中还提供了一个onSaveInstanceState()
回调方法,这个方法可以保证在活动被回收之前一定会被调用,因此我们可以通过这个方法来解决活动被回收时临时数据得不到保存的问题。
onSaveInstanceState()
方法会携带一个Bundle
类型的参数,Bundle
提供了一系列的方法用于保存数据,比如可以使用putString()
方法保存字符串,使用putInt()
方法保存整型数据,以此类推。每个保存方法需要传入两个参数,第一个参数是键,用于后面从Bundle
中取值,第二个参数是真正要保存的内容。
在MainActivity中添加如下代码就可以将临时数据进行保存:
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
String tempData = "Something you just typed";
outState.putString("data_key", tempData);
}
数据是已经保存下来了,那么我们应该在哪里进行恢复呢?细心的你也许早就发现,我们一直使用的onCreate()
方法其实也有一个Bundle
类型的参数。这个参数在一般情况下都是null
,但是如果在活动被系统回收之前有通过onSaveInstanceState()
方法来保存数据的话,这个参数就会带有之前所保存的全部数据,我们只需要再通过相应的取值方法将数据取出即可。
们在使用Intent传递数据时也是用的类似的方法。这里跟你提醒一点,Intent还可以结合Bundle
一起用于传递数据,首先可以把需要传递的数据都保存在Bundle
对象中,然后再将Bundle
对象存放在Intent里。到了目标活动之后先从Intent中取出Bundle
,再从Bundle
中一一取出数据。
2.7 活动的启动模式
启动模式一共有4种,分别是standard、singleTop、singleTask和singleInstance,可以在AndroidManifest.xml中通过给<activity>
标签指定android:launchMode
属性来选择启动模式。下面我们来逐个进行学习。
2.8 知晓当前是在哪一个活动
注意这里BaseActivity
和普通活动的创建方式并不一样,因为我们不需要让BaseActivity
在AndroidManifest.xml中注册,所以选择创建一个普通的Java类就可以了。然后让BaseActivity
继承自AppCompatActivity
,并重写onCreate()
方法,如下所示:
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("BaseActivity", getClass().getSimpleName());
}
}
我们在onCreate()
方法中获取了当前实例的类名,并通过Log打印了出来。
接下来我们需要让BaseActivity
成为ActivityTest项目中所有活动的父类。修改FirstActivity、SecondActivity和ThirdActivity的继承结构,让它们不再继承自AppCompatActivity
,而是继承自BaseActivity
。而由于BaseActivity
又是继承自AppCompatActivity
的,所以项目中所有活动的现有功能并不受影响,它们仍然完全继承了Activity中的所有特性。
现在重新运行程序,然后通过点击按钮分别进入到FirstActivity、SecondActivity和ThirdActivity的界面,这时观察logcat中的打印信息,如图2.43所示。
2.9 随时随地退出程序
如果目前你手机的界面还停留在ThirdActivity,你会发现当前想退出程序是非常不方便的,需要连按3次Back键才行。按Home键只是把程序挂起,并没有退出程序。其实这个问题就足以引起你的思考,如果我们的程序需要一个注销或者退出的功能该怎么办呢?必须要有一个随时随地都能退出程序的方案才行。
其实解决思路也很简单,只需要用一个专门的集合类对所有的活动进行管理就可以了,下面我们就来实现一下。
新建一个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中存储的活动全部销毁掉。
2.10 启动活动的最佳写法
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,
button1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
SecondActivity.actionStart(FirstActivity.this, "data1", "data2");
}
});