知识点摘要:四大组件的使用、Activity 的启动模式、Service 的 start 和 bind

四大组件之 Activity

学习 Android 就不得不讲到 Activity 了,毕竟用户打交道最多的就是 Activity,所以我们一定要学好它。

创建 Activity

创建方法很简单右击你的包名(如 com.example.activitytest) --> New --> Activity --> Empty Activity,会弹出来一个创建活动的窗口。

在图中「Activity Name」就是所要创建 Activity 的名字;「Generate Layout File」选项勾选之后,IDE 会自动为我们创建布局文件,并且「Layout Name」是布局文件的名字;「Launcher Activity」选项勾选后,IDE 会在 ActivityManifest.xml 中将 activity 注册为启动界面,也就是我们打开 app 后显示的第一个界面;「Package name」是表示 Activity 文件所存放的位置;「Source Language」则可以选择 Activity 的编程语言,可以选择 Java 或者 Kotlin。

Activity 的生命周期

创建一个 Activity 就是这么简单,学会了怎么创建 Activity,我们就可以来学习 Activity 的生命周期。Google 官方给出的生命周期图如下:

想要学习 Activity 的生命周期其实很简单,只需要创建一个 BaseActivity 类,在并且重写(override)所有的方法,在方法体中打印出 Log。之后创建新的 Activity 只需要继承 BaseActivity。

public class BaseActivity extends AppCompatActivity {
    public final String TAG = "Life Cycle - " + getClass().getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "**************onCreate**************");
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.d(TAG, "onStart");
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d(TAG, "onResume");
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.d(TAG, "onPause");
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
    }

    @Override
    protected void onRestart() {
        super.onRestart();
        Log.d(TAG, "onRestart");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
    }
}
复制代码

在整个生命周期中,图中的那些方法是什么?都有什么作用呢?

onCreate(): 这个回调是必须实现,该回调会在系统创建 Activity 时触发。可以在该回调的实现中初始化 Activity 的基本组件,例如:IDE 为我们实现了 setContentView(R.layout.activity_main) 方法。

onStart(): 当 onCreate() 执行完成之后,下一个回调始终是 onStart(),这个时候活动进入 Started 状态。此时活动对用户是可见的,但是还没出现在前台,无法与用户进行交互。

onResume(): 系统在活动开始与用户交互之前调用此回调。此时,活动位于活动堆栈的顶部,并捕获所有用户输入。应用程序的大多数核心功能都是在 onResume() 方法中实现的。

onPause(): 当活动失去焦点并进入暂停状态时,系统调用 onPause() 。例如,当用户点击“后退”或“最近”按钮时,会出现此状态。当系统为您的活动调用 onPause() 时,它在技术上意味着您的活动仍然部分可见,但大多数情况下表明用户正在离开活动,并且活动很快将进入“已停止”或“已恢复”状态。

此时可以做一些存储数据,停止动画等工作,注意不能太耗时,因为这会影响到新 Activity 的显示,onPause 必须先执行完,新的 Activity 的 onResume() 才会执行。

如果用户期望UI更新,则处于暂停状态的活动可以继续更新UI。这种活动的示例包括示出导航地图屏幕或媒体播放器播放的活动。即使这些活动失去焦点,用户也希望他们的UI继续更新。

一旦 onPause() 完成执行,下一个回调就是 onStop() 或 onResume(),具体取决于活动进入 Paused 状态后会发生什么。

onStop(): 当活动不再对用户可见时,系统调用 onStop()。这可能是因为活动被破坏,新活动正在开始,或者现有活动正在进入恢复状态并且正在覆盖已停止的活动。在所有这些情况下,停止的活动根本不再可见。

如果活动返回与用户交互,系统调用的下一个回调是 onRestart(),或者如果此活动完全终止,则由 onDestroy() 调用。

onRestart(): 当处于“已停止”状态的活动即将重新启动时,系统将调用此回调。在这个回调之后执行始终是 onStart()。

onDestroy(): 系统在销毁活动之前调用此回调。此回调是活动收到的最后一个回调。通常实现 onDestroy() 以确保在活动或包含它的进程被销毁时释放所有活动的资源。

知道了 Activity 的整个生命周期回调的方法,我们现在对于各种千奇百怪的操作,只需要查看日志就能掌握了。比如,按后退、Home、菜单键,或者再打开一个新的活动等等,会发生什么呢?活动会回调哪些方法?读者可以自行尝试,我这里就不赘述。

还要提醒读者有一个特殊情况,就是当 activity 中弹出 dialog 对话框的时候,activity 不会回调 onPause。 然而当 activity 启动 dialog 风格的 activity 的时候,此 activity 会回调 onPause 方法。

异常情况下的生命周期:

情况一: 比如当系统资源配置发生改变(比如,从竖屏状态变成横屏状态)以及系统内存不足时,activity 就会被杀死。

当系统配置发生改变之后,Activity 会销毁,会依此执行 onPause,onStop,onDestory,由于 activity 是在异常情况下终止的,系统会调用 onSaveInstance 来保存当前 activity 的状态,当 activity 重新创建后,系统会调用 onRestoreInstance,并且把 onSaveInstance 方法保存的 Bundle 对象作为参数同时传递给 onRestoreInstance 和 onCreate 方法。

同时,在 onSaveInstanceState 和 nRestoreInstanceState 方法中,系统自动为我们做了一些恢复工作,如:EditText 中用户输入的数据,ListView 滚动的位置等,这些 view 相关的状态,系统都能默认为我们恢复。在 view 的源码中,可以看到每个 view 都有 onSaveInstanceState 方法和 onRestoreInstanceState 方法。

情况二: 当系统内存不足时,会导致低优先级的 Activity 被杀死,这时候数据存储和恢复方法和前面是一致的。一般 Activity 的优先级是根据是否可见,能否交互来分级的。

优先级最高的就是,前台的正在和用户交互的 Activity。其次是可见但不是前台也无法与用户进行交互(比如,在 Activity 中弹出来一个对话框),优先级最低的就是后台 Activity,也就是已经被执行了 onStop 的 Activity。

防止重新创建 Activity:可以指定 Activity 的 configChange 属性(android : configChanges = "orientation"),让系统不会在配置发生变化后,重新创建 Activity。

Activity 的启动模式

我们已经学会了 Activity 的生命周期,充分了解 Activity 一生的经过,那么肯定还要知道 Activity 怎么来的。Activity 给我们提供了有四种启动模式:standard,singleTop,singleTask,singleInstance。

我们要新建一个 LaunchModeActivity 活动,其中再放入两个按钮,一个是 「Restart Self」用来重启 LaunchModeActivity,还有一个是「Open Other Activity」按钮用来打开 OtherLaunchModeActivity,还要创建一个 OtherLaunchModeActivity 活动,它包含一个「Open Father Activity」按钮用来打开 LaunchModeActivity。

我们在 BaseActivity 中添加 dumpTaskAffinity 方法,用来打印 taskAffinity 属性,taskAffinity 又是什么呢?它 表示此活动对系统中另一个任务的亲和力。 此处的字符串是任务的名称,通常是整个包的包名称。 如果为null,则活动没有亲和力。我们还可以在 AndroidManifest.xml 中为 activity 修改 taskAffinity 属性。

在 AndroidManifest.xml 中
<activity
        android:name=".activities.LaunchModeActivity"
        android:launchMode="standard"
        android:taskAffinity="com.wendraw.demo.standard" />
复制代码
public class BaseActivity extends AppCompatActivity {
    public final String TAG = "Life Cycle - " + getClass().getSimpleName();
    public final String LAUNCHMODE = "Life Cycle(Launch Mode) - " + getClass().getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "**************onCreate**************");
        Log.d(LAUNCHMODE, "onCreate: " + getClass().getSimpleName() + "'s TaskId: "
                + getTaskId() + " HashCode: " + this.hashCode());
        dumpTaskAffinity();
    }
    
    ....

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        Log.d(LAUNCHMODE, "onNewIntent: " + getClass().getSimpleName() + " TaskId: "
                + getTaskId() + " HashCode: " + this.hashCode());
    }

    /**
     * 打印 taskAffinity 属性
     */
    protected void dumpTaskAffinity() {
        try {
            ActivityInfo info = this.getPackageManager()
                    .getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
            Log.i(LAUNCHMODE, "taskAffinity:" + info.taskAffinity);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
    }
}
复制代码

为了更直观的对比,我们在每种模式中操作的顺序保持一致,都是从 MainActivity -> 点击按钮打开 LaunchModeActivity -> 点击两次「Restart Self」按钮重启 LaunchModeActivity -> 点击「Open Other Activity」按钮打开 OtherLaunchModeActivity -> 点击「Open Father Activity」按钮打开 LaunchModeActivity。

  • standard 模式:
    由 LaunchModeActivity 的 log 可以看到,每次点击按钮重新启动自己时,hashcode 的值都不一样,也就是说每次启动一个 Activity 都会重新创建一个实例。再由每个实例的 TaskId 都是一样的,与其启动 Activity(MainActivity)的 TaskId 一致,也就是说 standard 模式下, Activity 默认会进入启动它的 activity 所属的任务栈中。并且这个 Activity 它的 onCreate(),onStart(),onResume() 方法都会被调用。

    注意:在非 activity 类型的 context(如 ApplicationContext)并没有所谓的任务栈,所以不能通过 ApplicationContext 启动 standard 模式的 activity。
  • singleTop 模式:
    SingleTop 模式下,如果要打开的 Activity 位于栈顶,那么这个 Activity 不会被重新创建,会直接从栈顶弹出(由点击两次重启按钮后,LaunchModeActivity 的 HashCode 没有改变可以得到这个结论),同时 OnNewIntent 方法会被调用,通过此方法的参数,我们可以去除当前请求的信息,且不执行 onCreate、onStart 方法,因为它并没有发生改变。如果该 Activity 不在栈顶的时候,则情况与 standard 模式相同(通过在 OtherSingleTopActivity 点击按钮启动 LaunchModeActivity 可以看出来,HashCode 改变了,且执行了 onCreate、onStart 方法)。
    总结来说,singleTop 模式分3种情况:
  1. 当前栈中已有该 Activity 的实例,并且位于栈顶,这时不会创建新实例,而是复用栈顶对象,并且会将 Intent 对象传入,回调 OnNewIntent 方法。
  2. 当前栈中已有该 Activity 的实例,但不位于栈顶,这时会创建新实例,其行为和 Standard 模式一样。
  3. 当前栈中不存在该 Activity 实例,其行为和 Standard 模式一样。

注意:这个 activity 的 onCreate、onStart、onResume 不会被调用,因为它们没有发生改变。

  • singleTask 模式:
    由 log 日志可以知道,点击两次按钮重启 LaunchModeActivity 时,HashCode 没有改变,也就是说重启没有创建新实例,并且会调用 OnNewIntent 方法。从 OtherLaunchModeActivity 打开 LaunchModeActivity 时,此时 LaunchModeActivity 的 OnNewIntent、OnRestart 被调用,HashCode 没有改变,也就是复用了栈内实例,当 LaunchModeActivity 调用 OnResume 也就是 Activity 前台可见后,OtherLaunchModeActivity 执行了 OnStop、OnDestroy 销毁了。
    总结来说,singleTask 启动模式下,要启动的 Activity 如果位于当前栈内,就会直接复用栈内实例,如果实例位于栈顶,当然可以直接出栈使用;但是如果不在栈顶,则会将栈顶的元素直接弹出栈,知道找到当前 Activity 的实例。

    补充: 由 taskAffinity 的值没变,并且是默认的包名,也就是说目前采用的是栈内复用模式。 如果在 AndroidManifest.xml 中,将 LaunchModeActivity 的 taskAffinity 属性设置成 "com.wendraw.demo.singletask",此时 log 会有所改变,在启动 LaunchModeActivity 的时候TaskId 与 MainActivity 的不同了,而OtherLaunchModeActivity 没有改变 taskAffinity ,其 TaskId 与 MainActivity 相同。这就说明了,在 singleTask 启动模式下,会根据 taskAffinity 的值来为 Activity 分配任务栈。
    singleInstance 模式:
    由 log 日志可以知道,启动 LaunchModeActivity 时,其 TaskId 与 MainActivity 的 TaskId 不同。并且在点击按钮重启的时候,调用了 OnNewIntent,而 TaskId 与 HashCode 没有改变,这就说明了 singleInstance 模式的 Activity 都拥有自己的任务栈。

    ingleInstance 模式又叫全局唯一模式,如果我们将 LaunchModeActivity 设置为 singleInstance,并且设置其 intent-filter 属性的 action android:name="com.wendraw.demo.singleinstance"、
    category android:name="android.intent.category.DEFAULT",然后在 MainActivity 中启动 LaunchModeActivity(不管是显式 Intent 还是隐式 Intent) -> 点击 Home 键回到桌面。打开另一个 SingleInstanceDemo 应用,它很简单只有一个 MainActivity,还有一个按钮用来隐式 Intent 启动 LaunchModeActivity,点击按钮后直接启动了 learnfourmaincomponents 的 LaunchModeActivity。我们可以看到调用了 OnNewIntent、OnRestart,而且 TaskId、HashCode 与之前的一致,也就证明了singleInstance 在整个系统中是全局唯一的。
    注意:默认情况下,所有 activity 所需的任务栈的名字为应用的包名,可以通过 activity 指定 TaskAffinity 属性来指定任务栈,当然这个属性不能和包名相同,否则就没有任何意义了不是吗?

Activity 与 Fragment

Fragment(碎片)是一种可以嵌入在活动当中的 UI 片段,它能让程序更加合理和充分的利用大屏幕空间。关于 Fragment 的基本用法,我就不赘述了,不知道的读者可以自行 Google。既然 Fragment 是嵌入在活动中的,那我们主要来学习 Activity 和 Fragment 的生命周期。

我们在 Android 官网 可以找到「Fragment 的生命周期图」和「Activity 与 Fragment 的生命周期的对比图」如下:

先创建一个 BaseFragment 继承 Fragment 并实现生命周期上的所有方法

public class BaseFragment extends Fragment {

    private final String TAG = "Life Cycle -" + this.getClass().getSimpleName();

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        Log.d(TAG, "onAttach");
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
        @Nullable Bundle savedInstanceState) {
        Log.d(TAG, "onCreateView");
        return super.onCreateView(inflater, container, savedInstanceState);
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Log.d(TAG, "onActivityCreated");
    }

    @Override
    public void onStart() {
        super.onStart();
        Log.d(TAG, "onStart");
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.d(TAG, "onResume");
    }

    /************************* Fragment is active ***************************/

    @Override
    public void onPause() {
        super.onPause();
        Log.d(TAG, "onPause");
    }

    @Override
    public void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        Log.d(TAG, "onDestroyView");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
    }

    @Override
    public void onDetach() {
        super.onDetach();
        Log.d(TAG, "onDetach");
    }
}

复制代码

然后创建 FirstFragment 并继承 BaseFragment,这是四大组件中唯一一个不需要在 Manifest.xml 中进行注册的。

public class FirstFragment extends BaseFragment {

    public FirstFragment() {
        // Required empty public constructor
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_first, container, false);
    }

}
复制代码

Fragment 的使用方法也很简单,有两种方式,第一种是静态加载,只需要在 Activity 的 layout.xml 中添加 fragment 控件,并指定其 name 为 FirstFragment 即可。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".activities.SecondActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:text="I'm Activity Content." />

    <fragment
        android:id="@+id/first_fragment"
        android:name="com.wendraw.learnfourmaincomponents.fragments.FirstFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>
复制代码

第二种方式是动态加载,先在 layout 中添加一个 FrameLayout 控件,然后再在 SecondActivity 中,使用代码进行动态替换。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".activities.SecondActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:text="I'm Activity Content." />

    <FrameLayout
        android:id="@+id/fragment_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>
复制代码
public class SecondActivity extends BaseActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);

        //动态替换 Fragment
        FragmentManager fragmentManager = getSupportFragmentManager();
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.replace(R.id.fragment_layout, new FirstFragment());
        transaction.commit();
    }
}
复制代码

虽然实现的方式不一样,但运行程序再打开 SecondActivity 之后,其效果都是如下图所示,灰色的部分是 Fragment,白色的部分还是属于 Activity。

Activity And Fragment

通过 MainActivity 点击 Button 跳转到 SecondActivity,并且 SecondActivity 包含一个 FirstFragment,可以看出 Activity 和 Fragment 的生命周期的关系与上图描述的一致。

Activity 与 menu 创建先后顺序

官方介绍,如果开发的应用适用于 Android 2.3.x(API 级别 10)或者更低版本,则当用户首次点击菜单选项(也就是那三个点的图标)时,系统会调用 onCreateOptionMenu() 来创建菜单;如果开发的应用适用于 Android 3.0 及更高版本,则将在系统启动 Activity 时调用 onCreateOptionMenu() 创建菜单,因为 Android 3.0 之后,可以在 ActionBar 显示菜单的 item。

笔者只测试了第二种情况,可以看出来是当 Activity 调用 onResume 之后才开始创建菜单的。如果读者感兴趣可以自行测试 Android 2.3.x 及其一下的版本的情况。

四大组件之 Service

Service 是一个可以在后台执行长时间运行操作而不提供用户界面的应用组件。服务可由其他应用组件启动,而且即使用户切换到其他应用,服务仍将在后台继续运行。 此外,组件可以绑定到服务,以与之进行交互,甚至是执行进程间通信 (IPC)。 例如,服务可以处理网络事务、播放音乐,执行文件 I/O 或与内容提供程序交互,而所有这一切均可在后台进行

创建 Service 其实很简单,右键包名 com.wendraw.learnfourmaincomponents -> New -> Service,发现有 Service 和 Service(IntentService) 可以选择,那我们应该选择哪一个呢?Service 一般分为两种形式:

启动: 当应用组件(如 Activity)通过调用 startService() 启动服务时,服务即处于“启动”状态。一旦启动,服务即可在后台无限期运行,即使启动服务的组件已被销毁也不受影响。 已启动的服务通常是执行单一操作,而且不会将结果返回给调用方。例如,它可能通过网络下载或上传文件。 操作完成后,服务会自行停止运行。

绑定: 当应用组件通过调用 bindService() 绑定到服务时,服务即处于“绑定”状态。绑定服务提供了一个客户端-服务器接口,允许组件与服务进行交互、发送请求、获取结果,甚至是利用进程间通信 (IPC) 跨进程执行这些操作。 仅当与另一个应用组件绑定时,绑定服务才会运行。 多个组件可以同时绑定到该服务,但全部取消绑定后,该服务即会被销毁。

虽然我们分开介绍这两种形式的服务,但是我们创建的服务可以同时包含这两种形式,也就是说,它既可以是启动服务(以无限期运行),也允许绑定。问题只是在于您是否实现了一组回调方法:onStartCommand()(允许组件启动服务)和 onBind()(允许绑定服务)。

注意:服务在其托管进程的主线程中运行,它既不创建自己的线程,也不在单独的进程中运行(除非另行指定)。 这意味着,如果服务将执行任何 CPU 密集型工作或阻止性操作(例如 MP3 播放或联网),则应在服务内创建新线程来完成这项工作。通过使用单独的线程,可以降低发生“应用无响应”(ANR) 错误的风险,而应用的主线程仍可继续专注于运行用户与 Activity 之间的交互。

跟前面已经学习过的四大组件一样,我们主要来关注 Service 的生命周期。同样在 Android 官网 的 Service 找到如下生命周期图,左边表示使用启动方式的 Service 的生命周期,右边表示使用绑定方式的 Service 的生命周期。

与 Activity 一样新建一个 Service,并重写生命周期中的方法,然后在 AndroidManifest.xml 中注册。

class MyService : Service() {
    private val TAG = "Life Cycle - " + javaClass.simpleName

    override fun onCreate() {
        Log.d(TAG, "**************onCreate**************")
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d(TAG, "onStartCommand")
        return super.onStartCommand(intent, flags, startId)
    }

    override fun onBind(intent: Intent?): IBinder? {
        Log.d(TAG, "onBind")
        return MyBinder()
    }

    override fun onUnbind(intent: Intent?): Boolean {
        Log.d(TAG, "onUnbind")
        return super.onUnbind(intent)
    }

    override fun onDestroy() {
        Log.d(TAG, "onDestroy")
    }

    interface MyIBinder {
        fun invokeMethodInMyService()
    }

    inner class MyBinder : Binder(), MyIBinder {

        fun stopService(serviceConnection: ServiceConnection) {
            unbindService(serviceConnection)
        }

        override fun invokeMethodInMyService() {
            for (i in 0..19) {
                println("service is opening")
            }
        }

    }
}
复制代码
AndroidManifest.xml
<service
       android:name=".services.MyService"
       android:enabled="true"
       android:exported="true" />
复制代码

然后在 Activity 中选择需要启动 Service 的方式

class ServiceActivity : AppCompatActivity() {

    private lateinit var mBinder: MyService.MyBinder
    private lateinit var mServiceConnection: ServiceConnection

    private var isBind = false  //标记 Service 是否已经绑定

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

        //使用 startService 方式启动 Service
        start_service_btn.setOnClickListener {
            val intent = Intent(this@ServiceActivity, MyService::class.java)
            startService(intent)
        }

        //停止 Service
        stop_service_btn.setOnClickListener {
            val intent = Intent(this@ServiceActivity, MyService::class.java)
            stopService(intent)
        }

        //使用 bindService  方式启动 Service
        bind_service_btn.setOnClickListener {
            isBind = true
            mServiceConnection = MyServiceConnection()
            val intent = Intent(this@ServiceActivity, MyService::class.java)
            bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE)
        }

        //解绑 Service
        unbind_service_btn.setOnClickListener {
            unbindService(mServiceConnection)
        }

        //使用 startService 方式启动 IntentService
        start_intent_service_btn.setOnClickListener {
            //打印主线程的 id
            Log.d("ServiceActivity", "Thread id is " + Thread.currentThread().id)
            val intent = Intent(this@ServiceActivity, MyIntentService::class.java)
            startService(intent)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        if (isBind) {
            //当活动被销毁时,需要解绑 Service
            unbindService(mServiceConnection)
        }
    }

    inner class MyServiceConnection : ServiceConnection {

        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            Log.d("MyService", "onServiceConnected")
            mBinder = service as MyService.MyBinder
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            Log.d("MyService", "onServiceDisconnected")
        }
    }
}
复制代码

在 ServiceActivity 中有五个按钮,分别是启动服务、停止服务、绑定服务、解绑服务和启动 IntentService。

ServiceActivity 界面

我们先点击启动服务,再点击停止服务按钮,也就是用 startService() 启动服务,使用我们可以看到如下日志:

如果我们依此点击绑定服务、解绑服务,使用绑定形式启动服务,可以得到如下日志:

可以看到与官方给出的生命周期图是一致的。

我们还可以尝试使用 startService、bindService 方式进行混合启动服务,先点击启动服务按钮 -> 点击绑定服务按钮,此时 MyService 是一个无限期运行的、绑定的服务,如果此时像退出服务怎么半呢?需要点击「STOP SERVICE」、「UNBIND SERVICE」两个按钮,才会执行 onDestroy 方法,表示服务已经被销毁。

四大组件之 Content Provider

下面是摘自「第一行代码」中的介绍:

内容提供器(Content Provider) 主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证数据被访问的安全性。目前,使用内容提供器是 Android 实现跨程序共享数据的标准方式。

内容提供器作为四大组件之一,我们肯定要学习一波,但是在日常开发中我们用到就是读取电话簿之类的,所以也没有必要学习的太过深入,学会增删查改即可,那么接下来就一起学习一下对本地电话簿的增删查改吧。

我们先在 layout.xml 中添加 ListView 用来展示获取到的联系人,query_btn 用来查询电话簿,insert_btn 用来插入数据。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".activities.ContentProviderActivity">

    <Button
        android:id="@+id/query_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:text="Query"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/insert_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:text="Insert"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/query_btn"
        app:layout_constraintTop_toTopOf="parent" />

    <ListView
        android:id="@+id/content_provider_list_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/insert_btn" />

</android.support.constraint.ConstraintLayout>
复制代码

然后在 Manifest.xml 中申请读写联系人信息的权限

<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
复制代码

但是读写联系人信息是非常敏感的,所以 Android 要求我们动态的申请权限,不仅仅在 Manifest 中申明。

class ContentProviderActivity : AppCompatActivity() {

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

        //检查权限
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this,
                    arrayOf(Manifest.permission.READ_CONTACTS), 1)
        } else {
            //读取联系人信息
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>,  grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            1 -> {
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    readContacts()
                } else {
                    Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }
}
复制代码

我们已经获取了读写联系人的权限,接下来就可以愉快的获取和修改联系人信息了。

class ContentProviderActivity : AppCompatActivity() {

    private lateinit var mAdapter: ArrayAdapter<String>
    private val mContactsList: ArrayList<String> = ArrayList()

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

        mAdapter = ArrayAdapter(this, R.layout.simple_list_item, mContactsList)
        content_provider_list_view.adapter = mAdapter
        
        ...
        
        query_btn.setOnClickListener {
            readContacts()
        }

        insert_btn.setOnClickListener {
            insertContact("wendraw", "86-13355550000")
        }
    }

    //获取联系人信息
    private fun readContacts() {
        var cursor: Cursor? = null
        try {
            //查询联系人
            cursor = contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                    null, null, null, null)
            if (cursor != null) {
                while (cursor.moveToNext()) {
                    //获取联系人姓名
                    val displayName = cursor.getString(cursor.getColumnIndex(
                            ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME
                    ))
                    //获取联系人电话
                    val displayPhoneNumber = cursor.getString(cursor.getColumnIndex(
                            ContactsContract.CommonDataKinds.Phone.NUMBER
                    ))
                    mContactsList.add(displayName + "\n" + displayPhoneNumber)
                }
                mAdapter.notifyDataSetChanged()
            }
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            cursor?.close()
        }
    }
    
    //新增联系人信息
    private fun insertContact(name: String, phoneNumber: String) {
        // 创建一个空的ContentValues
        val values = ContentValues()
        // 向RawContacts.CONTENT_URI执行一个空值插入,
        // 目的是获取系统返回的rawContactId
        val rawContactUri = contentResolver.insert(ContactsContract.RawContacts.CONTENT_URI, values)
        val rawContactId = ContentUris.parseId(rawContactUri)

        //清空数据
        values.clear()
        values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId)
        //设置内容类型
        values.put(ContactsContract.Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
        //设置联系人姓名
        values.put(StructuredName.GIVEN_NAME, name)
        // 向联系人URI添加联系人名字
        contentResolver.insert(ContactsContract.Data.CONTENT_URI, values)

        //清空数据
        values.clear()
        values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId)
        //设置电话类型
        values.put(ContactsContract.Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE)
        //设置联系人电话
        values.put(Phone.NUMBER, phoneNumber)
        // 向联系人URI添加联系人电话
        contentResolver.insert(ContactsContract.Data.CONTENT_URI, values)

        Toast.makeText(this, "联系人数据添加成功", Toast.LENGTH_SHORT)
                .show()
    }

    ...
}
复制代码

点击 query_btn 按钮查询手机内的所有联系人并展示到 ListView

查询联系人

再点击 insert_btn 按钮,将名字 wendraw ,电话  插入,点击 query_btn 按钮查询手机内的所有联系人,我们可以看到联系人顺利插入到电话簿当中。你还可以去手机中的通讯录查看,也能找到我们刚刚插入的联系人。

插入联系人

结束

至此,我们注重学习了四大组件的生命周期,当然通过这一篇文章就能完全掌握四大组件是不现实的,但是我相信通过对组件生命周期的学习,会有助于接下来的学习。