安卓基础巩固(二):四大组件:Activity、Service、Broadcast、Content Provider

Activity

Activity 是一个应用组件,用户可与其提供的屏幕进行交互,以执行拨打电话、拍摄照片、发送电
子邮件或查看地图等操作。 每个 Activity 都会获得一个用于绘制其用户界面的窗口。窗口通常会
充满屏幕,但也可小于屏幕并浮动在其他窗口之上。

如果要新建activity,需要在清单中注册。

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

从这个默认的清单文件中我们可以得知,activity是属于application的。application就是我们的应用。
application标签中也指定了各种元素,例如应用的图标,名字,主题等等。

MainActivity是应用启动的第一个activity。可以观察到它设置了action和category属性。

  • android.intent.action.MAIN 决定应用程序最先启动的Activity。
  • android.intent.category.LAUNCHER 表示可以在手机“桌面”上看到应用图标

生命周期

在这里插入图片描述

onCreate和onStart的区别

  • onCreate 在系统首次创建 Activity 时触发。Activity会在创建后进入已创建状态。onCreate方法处应该是我们创建视图,准备数据的位置。
  • 当 Activity 进入“已开始”状态时,系统会调用此回调。onStart() 调用使 Activity 对用户可见,因为应用会为 Activity 进入前台并支持交互做准备。
  • onCreate方法在activity的生命周期中只会执行一次,而onStart方法在activity停止时,执行完onRestart后,会再次执行。

onPause和onStop的区别

  • onPause() 执行非常简单,而且不一定要有足够的时间来执行保存操作。== 因此,您不应使用onPause() 来保存应用或用户数据、进行网络调用,或执行数据库事务。因为在该方法完成之前,此类工作可能无法完成。==
  • 在 onStop() 方法中,应用应释放或调整应用对用户不可见时的无用资源。例如,应用可以暂停动画效果,或从细粒度位置更新切换到粗粒度位置更新。 使用 onStop() 而非 onPause() 可确保与界面相关的工作继续进行,即使用户在多窗口模式下查看您的 Activity 也能如此。 在 onStop() 关闭CPU 相对密集的操作。

生命周期的变化

  1. 启动后退出:

onCreate
onStart
onResume
onWindowFocusChanged: hasFocus: true
onWindowFocusChanged: hasFocus: false
onPause
onStop
onDestroy

2.启动后按home键

Act1: onCreate
Act1: onStart
Act1: onResume
Act1: onWindowFocusChanged: hasFocus: true
// 按home键
Act1: onWindowFocusChanged: hasFocus: false
Act1: onPause
Act1: onStop
// 再回来
Act1: onRestart
Act1: onStart
Act1: onResume
Act1: onWindowFocusChanged: hasFocus: true
// 按返回键退出act
Act1: onWindowFocusChanged: hasFocus: false
Act1: onPause
Act1: onStop
Act1: onDestroy

  1. 旋转手机(横竖屏切换时的生命周期变化)
    [Life]: onCreate
    [Life]: onStart
    [Life]: onResume
    [Life]: onWindowFocusChanged: hasFocus: true
    // 横屏
    [Life]: onPause
    [Life]: onStop
    [Life]: onDestroy
    [Life]: onCreate
    [Life]: onStart
    [Life]: onResume
    [Life]: onWindowFocusChanged: hasFocus: true
    // 竖屏
    [Life]: onPause
    [Life]: onStop
    [Life]: onDestroy
    [Life]: onCreate
    [Life]: onStart
    [Life]: onResume
    [Life]: onWindowFocusChanged: hasFocus: true
    // 返回
    [Life]: onWindowFocusChanged: hasFocus: false
    [Life]: onPause
    [Life]: onStop
    [Life]: onDestroy
  2. 两个activity切换

Act1: onCreate
Act1: onStart
Act1: onResume
Act1: onWindowFocusChanged: hasFocus: true
Act1: onPause // 切换到Act2
Act1: onWindowFocusChanged: hasFocus: false
Act2: onCreate
Act2: onStart
Act2: onResume
Act2: onWindowFocusChanged: hasFocus: true
Act1: onStop
Act2: onWindowFocusChanged: hasFocus: false // 再切换回Act1
Act2: onPause
Act1: onRestart
Act1: onStart
Act1: onResume
Act1: onWindowFocusChanged: hasFocus: true
Act2: onStop
Act2: onDestroy
Act1: onWindowFocusChanged: hasFocus: false
Act1: onPause
Act1: onStop
Act1: onDestroy

在这里插入图片描述

  1. 弹出一个AlertDialog
    会调用onWindowFocusChanged
    onWindowFocusChanged: hasFocus: false
    onWindowFocusChanged: hasFocus: true

Activity的启动

Intent

Intent,直译为“意图”。我们把信息包裹在intent对象中,然后执行。

这里用到一个很常见的方法startActivity (Intent intent) 。 startActivity 属于Context类,Activity是Context的子类。

从LoginActivity 跳转到 WaitActivity:

Intent intent = new Intent(LoginActivity.this, WaitActivity.class);
startActivity(intent);

在跳转去下一个页面时,我们可能会想携带一些信息到下一个界面去。例如携带一些文本,数字等等,或者是一个对象。 这些信息我们可以交给Intent,传递到下一个activity去。下一个activity中拿到我们传入的Intent。

Intent intent = new Intent(getApplicationContext(), SendParamsDemo.class);
intent.putExtra(SendParamsDemo.K_INT, 100);
intent.putExtra(SendParamsDemo.K_BOOL, true);
intent.putExtra(SendParamsDemo.K_STR, "Input string");
startActivity(intent);

在另外一个activity中直接通过get_获取相应的参数。

int i = intent.getIntExtra(K_INT, -1);
boolean b = intent.getBooleanExtra(K_BOOL, false);
String str = intent.getStringExtra(K_STR);

Bundle

实际上在安卓开发我们使用Bundle用于传递数据;它保存的数据,是以key-value(键值对)的形式存在的。

经常使用Bundle在Activity之间传递数据,传递的数据可以是boolean、byte、int、long、float、double、string等基本类型或它们对应的数组,也可以是对象或对象数组。当Bundle传递的是对象或对象数组时,必须实现Serializable 或Parcelable接口。

intent其实是调用了bundle相应的put函数,也就是说,intent内部还是用bundle来实现数据传递的,只是封装了一层而已。

Activity携带参数返回

在一个主界面(主Activity)通过意图跳转至多个不同子Activity上去,当子模块的代码执行完毕后再次返回主页面,将子Activity中得到的数据显示在主界面/完成的数据交给主Activity处理。这种带数据的意图跳转需要使用下边三个方法:

  • startActivityForResult(Intent intent, int requestCode);
  • setResult(int resultCode, Intent data)
  • onActivityResult(int requestCode, int resultCode, Intent data)

下边的例子中有2个activity作为示范:ForResultFirstAct 和ForResultSecondAct 。

步骤:

  1. 主activity:调用startActivityForResult(Intent intent, int requestCode);

第一个参数:一个Intent对象,用于携带将跳转至下一个界面中使用的数据,使用putExtra(A,B)方法,此处存储的数据类型特别多,基本类型全部支持。
第二个参数:如果> = 0,当Activity结束时requestCode将归还在onActivityResult()中。以便确定返回的数据是从哪个Activity中返回,用来标识目标activity。

private static final int REQ_CODE = 10;
startActivityForResult(new Intent(getApplicationContext(),
ForResultSecondAct.class), REQ_CODE);
  1. ForResultSecondAct 是第二个activity。它可以设置返回时携带的数据,然后调用setResult(RESULT_OK, resultIntent);
Intent resultIntent = new Intent();
resultIntent.putExtra(K_TITLE, mTitleEt.getText().toString());
resultIntent.putExtra(K_SUB_TITLE, mSubTitleEt.getText().toString());
setResult(RESULT_OK, resultIntent);
finish();

其中RESULT_OK 是Activity类的静态常量。可用于代表操作的结果,除此外还有其他状态:

/** Standard activity result: operation canceled. */
public static final int RESULT_CANCELED = 0;
/** Standard activity result: operation succeeded. */
public static final int RESULT_OK = -1;
/** Start of user-defined activity results. */
public static final int RESULT_FIRST_USER = 1;
  1. 在主activity中重写onActivityResult(int requestCode, int resultCode, Intent data),验证requestCode、resultCode后,获取到返回的数据:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
	super.onActivityResult(requestCode, resultCode, data);
	switch (requestCode) {
		case REQ_CODE:
		if (resultCode == RESULT_OK) {
		if (data != null) {
				mTitleTv.setText(data.getStringExtra(ForResultSecondAct.K_TITLE));
				mSubTitleTv.setText(data.getStringExtra(ForResultSecondAct.K_SUB_TITLE));
			}
		} else {
			Toast.makeText(getApplicationContext(), "未保存修改",
			Toast.LENGTH_SHORT).show();
		}
		break;
	}
}

Activity启动模式

任务(task),返回栈(back stack)

任务是指在执行特定作业时与用户交互的一系列 Activity。 这些 Activity 按照各自的打开顺序排列在堆栈(即返回栈)中。

Activity的四种启动模式

在这里插入图片描述

standard(默认模式)

默认模式。如果不指定启动模式,则会使用这个模式。 系统在启动 Activity 的任务中创建 Activity 的新实例并向其传送 Intent。Activity 可以多次实例化,而每个实例均可属于不同的任务,并且一个任务可以拥有多个实例。

singleTop

可以有多个实例,但是不允许多个相同Activity叠加。即,如果Activity在栈顶的时候,启动相同的Activity,不会创建新的实例,而会调用其onNewIntent方法

目标作用是在栈顶添加activity实例或走栈顶activity的onNewIntent() 方法。若目标activity已在栈顶,则不会新建实例。

如果当前任务的顶部已存在 Activity 的一个实例,则系统会通过调用该实例的 onNewIntent() 方法向其传送 Intent,而不是创建 Activity 的新实例。Activity 可以多次实例化,而每个实例均可属于不同的任务,并且一个任务可以拥有多个实例(但前提是位于返回栈顶部的 Activity 并不是 Activity 的现有实例)。

例如,假设任务的返回栈包含根 Activity A 以及 Activity B、C 和位于顶部的 D(堆栈是 A-B-C-D;D 位于顶部)。收到针对 D 类 Activity 的 Intent。如果 D 具有默认的 “standard” 启动模式,则会启动该类的新实例,且堆栈会变成 A-B-C-D-D。但是,如果 D 的启动模式是 “singleTop”,则 D 的现有实例会通过 onNewIntent() 接收 Intent,因为它位于堆栈的顶部;而堆栈仍为 A-B-C-D。但是,如果收到针对 B 类 Activity 的 Intent,则会向堆栈添加 B 的新实例,即便其启动模式为 “singleTop” 也是如此。

singleTask

只有一个实例。在同一个应用程序中启动他的时候,若Activity不存在,则会在当前task创建一个新的实例,若存在,则会把task中在其之上的其它Activity destory掉并调用它的onNewIntent方法。

  • 若目标activity已在栈中,则会销毁在目标activity之上的其他实例,此时目标activity来到栈顶。
  • 若目标activity是 “MAIN” activity,能被Launcher启动。那么按home键将App退到后台,在桌面上点击App图标。目标activity之上的页面都会被销毁掉,并调用目标activity的onNewIntent()方法。
  • 系统创建新任务并实例化位于新任务底部的 Activity。但是,如果该 Activity 的一个实例已存在于一个单独的任务中,则系统会通过调用现有实例的 onNewIntent() 方法向其传送 Intent,而不是创建新实例。一次只能存在 Activity 的一个实例。

注:尽管 Activity 在新任务中启动,但是用户按“返回”按钮仍会返回到前一个 Activity。

singleInstance

只有一个实例,并且这个实例独立运行在一个task中,这个task只有这个实例,不允许有别的Activity存在。

与 “singleTask” 相同,只是系统不会将任何其他 Activity 启动到包含实例的任务中。该 Activity 始终是其任务唯一仅有的成员;由此 Activity 启动的任何 Activity 均在单独的任务中打开。

例如,A,B,C 3个Activity,只有B是以singleInstance模式启动,其他是默认模式。 页面启动顺序是 A -> B -> C,B会自己在一个task中;栈情况如下(方括号表示在前台):

stack    |   | => |   | |   | => |   | |   | 
         |   |    |   | |   |    | C | |   | 
         | A |    | A | | B |    | A | | B | 
task id: [1082]   1082  [1083]   [1082] 1083

此时屏幕上显示是C的界面,按返回键,C被销毁,显示的是A的界面;再返回,A被销毁,原A和C所在的task结束。此时显示B的界面。

如果在只剩下B的时候,去启动C;由于B是singleInstance模式,B所在的栈只能有一个activity,则会新建一个task来存放C

stack     |   | => |   | |   | 
          |   |    |   | |   | 
          | B |    | B | | C | 
task id:  [1083]   1083  [1084]  

这4种启动模式各有特点。在开发中我们根据实际情况选择不同的启动模式。 例如
“根页面”可以考虑用singleTask的模式。

Activity中获取View的宽高

有些时候我们需要获取到View的宽高信息。

在onCreate和onResume中尝试view.getWidth()或是view.getHeiht()时,我们会发现获取到的是0。

Activity视图在创建完成后,各个子view并不一定被加载完成。 获取宽高正确的方法有哪些呢?

  • 方法1 - 在Activity的onWindowFocusChanged获取宽高
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        // 在这里我们可以获取到View的真实宽高
        Log.d(TAG, "onWindowFocusChanged: mBtn1.getWidth == " + mBtn1.getWidth());
    }

  • 方法2-使用ViewTreeObserver的OnGlobalLayoutListener回调
    获取View的ViewTreeObserver,添加回调
    ViewTreeObserver vto = mBtn1.getViewTreeObserver();
    vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            int height = mBtn1.getHeight();
            int width = mBtn1.getWidth();
            Log.d(TAG, "onGlobalLayout: mBtn1 " + width + ", " + height);
            mBtn1.getViewTreeObserver().removeOnGlobalLayoutListener(this);
        }
    });

  • 方法3-使用View.post(Runnable action)方法
    例如我们在onCreate中post一个Runnable
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mBtn1 = findViewById(R.id.btn1);
    Log.d(TAG, "mBtn1 post runnable");
    mBtn1.post(new Runnable() {
        @Override
        public void run() {
            Log.d(TAG, "mBtn1: " + mBtn1.getWidth() + ", " + mBtn1.getHeight());
        }
    });
}

可以获取到view的宽高。从log的时间上可以看出,在view加载完毕后,执行的Runnable。

06-19 11:54:17.865 28009-28009/com.rustfisher.basic4 D/rustApp: mBtn1 post runnable
06-19 11:54:17.867 28009-28009/com.rustfisher.basic4 D/rustApp: [act2] onResume
06-19 11:54:17.899 28009-28009/com.rustfisher.basic4 D/rustApp: mBtn1: 355, 144

应用:动态调整ImageView的宽高

获取到view的宽高后,我们可以动态地调整ImageView的高度。 假设图片宽高为704 * 440。xml中设置scaleType为fitXY。已知ImageView的宽度是固定的,我们可以调整高度。

<ImageView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:scaleType="fitXY"/>

根据图片真实大小来重设ImageView的高度。

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    resetIntroIvParams();
}

private void resetIntroIvParams() {
    int height = mIntroIv.getHeight(); // 704 * 440
    int wid = mIntroIv.getWidth();
    if (height > 0 && wid > 0) {
        ViewGroup.LayoutParams layoutParams = mIntroIv.getLayoutParams();
        layoutParams.height = (int) (wid * 440.0 / 704.0);
        mIntroIv.setLayoutParams(layoutParams);
    }
}

Fragment

Fragment,直译为“碎片”,“片段”。 Fragment 表示 FragmentActivity 中的行为或界面的一部分。您可以在一个 Activity 中组合多个片段,从而构建多窗格界面,并在多个 Activity 中重复使用某个片段。

注意:

  • Fragment必须始终托管在 Activity 中,其生命周期直接受宿主 Activity 生命周期的影响。例如,当 Activity 暂停时,Activity 的所有片段也会暂停;当 Activity 被销毁时,所有Fragment也会被销毁。不过,当 Activity 正在运行(处于已恢复生命周期状态)时,您可以独立操纵每个Fragment,如添加或移除Fragment。当执行此类Fragment事务时,您也可将其添加到由 Activity 管理的返回栈 — Activity 中的每个返回栈条目都是一条已发生片段事务的记录。借助返回栈,用户可以通过按返回按钮撤消片段事务(后退)。
  • 当您将Fragment作为 Activity 布局的一部分添加时,其位于 Activity 视图层次结构的某个 ViewGroup 中,并且Fragment会定义其自己的视图布局。您可以通过在 Activity 的布局文件中声明Fragment,将其作为 元素插入您的 Activity 布局,或者通过将其添加到某个现有的 ViewGroup,利用应用代码将其插入布局。

Fragment的优点

  • Fragment加载灵活,替换方便。定制你的UI,在不同尺寸的屏幕上创建合适的UI,提高用户体验。
  • 可复用,页面布局可以使用多个Fragment,不同的控件和内容可以分布在不同的Fragment上。
  • 使用Fragment,可以少用一些Activity。一个Activity可以管辖多个Fragment。

Fragmet的生命周期

Fragment 类的代码与 Activity 非常相似。它包含与 Activity 类似的回调方法,如 onCreate()、onStart()、onPause() 和 onStop()。实际上,如果您要将现有 Android 应用转换为使用片段,可能只需将代码从 Activity 的回调方法移入片段相应的回调方法中。

在这里插入图片描述

生命周期变化

  1. Fragmet被创建时:

onAttach()
onCreate()
onCreateView()
onActivityCreated()

  1. Fragment对用户可见时:

onStart()
onResume()

  1. Fragmet进入“后台模式”时:

onPause()
onStop()

  1. Fragmet被销毁或者持有它的activity被销毁时:

onPause()
onStop()
onDestroyView()
onDestroy()
onDetach()

Fragment与Activity不同的生命周期

Fragment的大部分状态都和Activity很相似,但fragment有一些新的状态。

Fragment不同于Activity的生命周期:

  • onAttached() —— 当fragment被加入到activity时调用(在这个方法中可以获得所在的activity)。
  • onCreateView() —— 当activity要得到fragment的layout时,调用此方法,fragment在其中创建自己的layout(界面)。
  • onActivityCreated() —— 当activity的onCreated()方法返回后调用此方法
  • onDestroyView() —— 当fragment中的视图被移除的时候,调用这个方法。
  • onDetach() —— 当fragment和activity分离的时候,调用这个方法。

一旦activity进入resumed状态(也就是running状态),你就可以自由地添加和删除fragment了。因此,只有当activity在resumed状态时,fragment的生命周期才能独立的运转,其它时候是依赖于activity的生命周期变化的。

对于 Activity 生命周期与片段生命周期而言,二者最显著的差异是在其各自返回栈中的存储方式。

  • 默认情况下,Activity 停止时会被放入由系统管理的 Activity 返回栈中
  • 而Fragment需要我们在移除Fragment的事务执行期间通过调用 addToBackStack() 显式请求保存实例时,系统才会将片段放入由宿主 Activity 管理的返回栈。

加载和使用Fragmet

向Activity中添加Fragment

静态加载

在 Activity 的布局文件内声明片段。 在本例中,您可以将片段当作视图来为其指定布局属性。例如,以下是拥有两个片段的 Activity 的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <fragment android:name="com.example.news.ArticleListFragment"
            android:id="@+id/list"
            android:layout_weight="1"
            android:layout_width="0dp"
            android:layout_height="match_parent" />
    <fragment android:name="com.example.news.ArticleReaderFragment"
            android:id="@+id/viewer"
            android:layout_weight="2"
            android:layout_width="0dp"
            android:layout_height="match_parent" />
</LinearLayout>

<fragment> 中的android:name属性指定要在布局中进行实例化的 Fragment 类。

创建此 Activity 布局时,系统会将布局中指定的每个片段实例化,并为每个片段调用 onCreateView() 方法,以检索每个片段的布局。系统会直接插入片段返回的 View,从而代替 <fragment> 元素。

注意:每个片段都需要唯一标识符,重启 Activity 时,系统可使用该标识符来恢复片段(您也可以使用该标识符来捕获片段,从而执行某些事务,如将其移除)。

不给fragment指定id会报错!!!

动态加载

通过编程方式将片段添加到某个现有 ViewGroup。 在 Activity 运行期间,您可以随时将片段添加到 Activity 布局中。您只需指定要将片段放入哪个 ViewGroup。
步骤:

  • ①准备好Fragment xml布局文件

  • ②新建一个类,继承自Fragment;在这个类中找到Fragment布局文件

  • ③在Activity中使用FragmentManager来操作Fragment

  • ④别忘了commit

准备fragment.xml

新建一个类FirstFragment.java,继承自Fragment。复写onCreateView方法。在onCreateView方法中,可以操作Fragment上的控件。

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_first, container,false);
//    fragment_first是自定义好的布局
//    如果此Fragment上放了控件,比如Button,Edittext等。可以在这里定义动作
  btn_fragment1_send = (Button) rootView.findViewById(R.id.btn_fragment1_1);
//...
        return rootView;
    }

准备一个位置给Fragment,比如在activity_main.xml中用Framelayout来占位。

在MainActivity.java里,先获得FragmentManager,得到FragmentTransaction。Fragment的添加删除等操作由FragmentTransaction来完成。

f1 = new FirstFragment();    //    获取实例
f2 = new SecondFragment();    //
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.add(R.id.layout_container1,f1);    //    添加
fragmentTransaction.replace(R.id.layout_container1,f1);    //    替换
// 或者也可以写成
fragmentTransaction.replace(R.id.layout_container1,new FirstFragment());
//              fragmentTransaction.addToBackStack(null);   //添加到返回栈,这样按返回键的时候能返回已添加的fragment
fragmentTransaction.commit();    //别忘了commit
//    移除操作 getFragmentManager().beginTransaction().remove(f1).commit();

管理Fragmet

如要管理 Activity 中的片段,您需使用 FragmentManager。如要获取它,请从您的 Activity 调用 getSupportFragmentManager()。

可使用 FragmentManager 执行的操作包括:

  • 通过 findFragmentById()(针对在 Activity 布局中提供界面的片段)或 findFragmentByTag()(针对提供或不提供界面的片段)获取 Activity 中存在的片段。
  • 通过 popBackStack()(模拟用户发出的返回命令)使片段从返回栈中弹出。
  • 通过 addOnBackStackChangedListener() 注册侦听返回栈变化的侦听器。
  • 打开一个 FragmentTransaction,通过它来执行某些事务,如添加和移除片段。

如要在您的 Activity 中执行片段事务(如添加、移除或替换片段),则必须使用 FragmentTransaction 中的 API。如下所示,您可以从 FragmentActivity 获取一个 FragmentTransaction 实例:

FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

然后,您可以使用 add() 方法添加一个片段,指定要添加的片段以及将其插入哪个视图。例如:

ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();

传递到 add() 的第一个参数是 ViewGroup,即应放置片段的位置,由资源 ID 指定,第二个参数是要添加的片段。 一旦您通过 FragmentTransaction 做出了更改,就必须调用 commit() 以使更改生效。

Fragment间通信

在Fragment的java文件中,可以使用getActivity()来获得调用它的activity,然后再找到另一个Fragment,进行通信。

getActivity().getFragmentManager().findFragmentById(R.id.fragment_list);
但这样做耦合度太高,不方便后续的修改操作。

Fragment与其附着的Activity之间的通信,都应该由Activity来完成;不能是多个Fragment之间直接通信。

Fragment与其附着的Activity之间通信方式

1.在发起事件的Fragment中定义一个接口,接口中声明你的方法。

2.在onAttach方法中要求Activity实现该接口。

3.在Activity中实现该方法。

例如一个activity中布置了2个Fragment,它们之间的通信要依靠activity来完成

代码:ListStoreActivity.java NewItemFragment.java ListStoreFragment.java
布局文件为:liststore.xml new_item_fragment.xml

ListStoreFragment.java 使用前面定义的界面

public class ListStoreFragment extends ListFragment{
/// 继承自ListFragment,已经封装好了listview
/// 不需要自己写ListView了
}

NewItemFragment.java

    /**
     * 声明一个接口,定义向activity传递的方法
     * 绑定的activity必须实现这个方法
     */
    public interface OnNewItemAddedListener {
        public void newItemAdded(String content);
    }
    private OnNewItemAddedListener onNewItemAddedListener;
    private Button btnAddItem;
    /*复写onAttach方法*/
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            onNewItemAddedListener = (OnNewItemAddedListener) activity;
        } catch (ClassCastException e){
            throw new ClassCastException(activity.toString() + "must implement OnNewItemAddedListener");
        }
    }

ListStoreActivity.java 加载主视图liststore.xml;
两个Fragment通过ListStoreActivity来通信

在onCreate方法中获取ListStoreFragment的实例;并且复写newItemAdded方法,在里面加上业务逻辑

public class ListStoreActivity extends Activity implements OnNewItemAddedListener{

    private ArrayList<String> data;
    private ArrayAdapter<String> adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.liststore);
        data = new ArrayList<String>();
        // 把data装入adapter中
        adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data);
        // ListFragment并不需要再定义一个listview        
        ListStoreFragment listStoreFragment = (ListStoreFragment) getFragmentManager().findFragmentById(R.id.fragment_listview);
        listStoreFragment.setListAdapter(adapter);
    }

    @Override
    public void newItemAdded(String content) {
        //  复写接口中的方法,业务代码在这里实现
        if(!content.equals("")) {
            data.add(content);
            adapter.notifyDataSetChanged();
        }
    }
}

Fragment跟Activity的其他通信方式:

  • 通过构造器传递信息

在Activity中构造Fragment的时候,可以将需要传递的数据封装在成一个Bundle对象,然后通过setArguments()方法传递参数,例子如下
Activity中设置Bundle:

FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
Fragment fragTop = new FrameTop();
fragmentTransaction.replace(R.id.frame1,fragTop);
Bundle bundle = new Bundle();
fragTop.setArguments(bundle);
bundle.putString("name","fragTop");
fragmentTransaction.commit();

Fragment中获取数据:

 @Override
 public void onAttach(@NonNull Context context) {
     super.onAttach(context);
     Bundle arguments = getArguments();
     String name = arguments.getString("name");
 }

  • 通过广播

  • 通过EventBus
    通过EventBus不仅可以实现Activity与Fragment, 还可以实现Fragment与Fragment之间通信,只要是注册了EventBus的并且可见地方都可以收到它发出的消息。

EventBus是一个开源库,它使用的是发布/订阅模式来实现组件之间的通信,相对于广播机制,handler机制等,其所需要的代码更少,耦合度更低,下面是一张官方的图说明其工作方式。

  • 通过Activity和Fragment共用ViewModel

DialogFragment

弹窗,是常见的一种提示方式。市面上有多种多样、五彩缤纷的弹窗。
在这里插入图片描述
构建步骤:

  1. 准备xml布局:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="12dp">

    <TextView
        android:id="@+id/title_tv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:textColor="#111111"
        android:textSize="16sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/content_tv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:gravity="center"
        android:textColor="#111111"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/title_tv" />

</androidx.constraintlayout.widget.ConstraintLayout>

  1. 新建弹窗类,继承自DialogFragmet

新建一个SimpleDialog类继承DialogFragment。

  • 在onCreate方法中接收传入的数据。传递数据使用了Bundle。
  • 在onCreateView方法中,使用上文建立的layout。
  • 在onViewCreated方法中进行ui操作。
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;

public class SimpleDialog extends DialogFragment {
    public static final String K_TITLE = "k_title"; // 传输数据时用到的key
    public static final String K_CONTENT = "k_content";

    private String title;
    private String content;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Bundle in = getArguments();
        if (in != null) {
            title = in.getString(K_TITLE);
            content = in.getString(K_CONTENT);
        }
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.dialog_simple, container, false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        TextView titleTv = view.findViewById(R.id.title_tv);
        TextView contentTv = view.findViewById(R.id.content_tv);

        titleTv.setText(title);
        contentTv.setText(content);
    }
}

把这个窗口弹出来。我们使用DialogFragment.show(@NonNull FragmentManager manager, @Nullable String tag)方法。

    private void popSimpleDialog1(String title, String content) {
        SimpleDialog dialog = new SimpleDialog();
        Bundle bundle = new Bundle();
        bundle.putString(SimpleDialog.K_TITLE, title);
        bundle.putString(SimpleDialog.K_CONTENT, content);
        dialog.setArguments(bundle);
        dialog.show(getSupportFragmentManager(), "one-tag");
    }

    // 调用
    popSimpleDialog1("欢迎访问", "欢迎访问https://an.rustfisher.com\n入门的好选择~");

在这里插入图片描述

Service

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

服务和子线程的区别

  • 服务是长期在后台运行的。
  • 开启一个用真的死循环的子线程也是在后台长期运行的。
  • 如果一个应用程序有后台服务在运行,即使杀掉进程,进程和服务还会自动复活。
  • 如果一个应用程序只有后台的子线程运行,杀掉进程,进程和子线程都挂了。
  • 如果要保证一个后台的操作长期运行:可以开启一个服务,在服务里边创建线程。

服务的状态

  • Started:Android的应用程序组件,如活动,通过startService()启动了服务,则服务是Started状态。一旦启动,服务可以在后台无限期运行,即使启动它的组件已经被销毁。
  • Bound: 当Android的应用程序组件通过bindService()绑定了服务,则服务是Bound状态。Bound状态的服务提供了一个客户服务器接口来允许组件与服务进行交互,如发送请求,获取结果,甚至通过IPC来进行跨进程通信。

服务的生命周期

服务拥有生命周期方法,可以实现监控服务状态的变化,可以在合适的阶段执行工作。下面的左图展示了当服务通过startService()被创建时的生命周期,右图则显示了当服务通过bindService()被创建时的生命周期:
在这里插入图片描述

要创建服务,你需要创建一个继承自Service基类或者它的已知子类的Java类。Service基类定义了不同的回调方法和多数重要方法。你不需要实现所有的回调方法。虽然如此,理解所有的方法还是非常重要的。实现这些回调能确保你的应用以用户期望的方式实现。

在这里插入图片描述

使用服务

如同对 Activity 及其他组件的操作一样,您必须在应用的清单文件中声明所有服务。

如要声明服务,请添加 <service> 元素作为 <application> 元素的子元素。下面是示例:

<manifest ... >
  ...
  <application ... >
      <service android:name=".ExampleService" />
      ...
  </application>
</manifest>

通过添加 android:exported 属性并将其设置为 false,确保服务仅适用于您的应用。这可以有效阻止其他应用启动您的服务,即便在使用显式 Intent 时也如此。

后台服务

后台服务:后台服务执行用户不会直接注意到的操作。例如,如果应用使用某个服务来压缩其存储空间,则此服务通常是后台服务。(注意:如果您的应用面向 API 级别 26 或更高版本,当应用本身未在前台运行时,系统会对运行后台服务施加限制。在诸如此类的大多数情况下,您的应用应改为使用作业计划。)

startService()

应用组件(如 Activity)可通过调用startService()方法并传递 Intent 对象(指定服务并包含待使用服务的所有数据)来启动服务。服务会在 onStartCommand() 方法接收此 Intent。

步骤:

  1. 创建一个服务类,继承自Service
public class MyService extends Service{
  //Service启动时调用
  @Override
  public void onCreate() {
    super.onCreate();
    Log.v("wang", "OnCreate 服务启动时调用");
    
  }
  @Override
  public IBinder onBind(Intent intent) {
    // TODO Auto-generated method stub
    return null;
  }
  //服务被关闭时调用
  @Override
  public void onDestroy() {
    super.onDestroy();
    Log.v("wang", "onDestroy 服务关闭时");
  }


  1. 在activity中使用startService(Intent)stopService(Intent)
public class MainActivity extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  }
  public void createServiceClick(View v){
    Intent intent = new Intent(this,MyService.class);
    //启动servicce服务
    startService(intent);
  }
  //虽然应用界面已经退出  但是服务还是存在的
  //停止服务  可以通过按钮来关闭   可以通过代码关闭服务
  public void clickStopService(View v){
    Intent name= new Intent(this,MyService.class);
    stopService(name);//name表示停止哪一个服务
  }
}
bindService()
  • 绑定服务允许应用组件通过调用 bindService() 与其绑定,从而创建长期连接。

  • 绑定型服务只能它所绑定的应用程序组件同时存在,当服务与所有组件之间的绑定全部取消时,Android系统会自动销毁该服务,即绑定型服务并不会在后台持久存在。

如需与 Activity 和其他应用组件中的服务进行交互,或需要通过进程间通信 (IPC) 向其他应用公开某些应用功能,则应创建绑定服务。

构建绑定型服务的步骤:

  1. 新建一个Service的子类区别为BindService.java,重写回调方法onCreate(),onBind(),onUnbind(),onDestroy()。
  2. 在activity中创建服务连接ServiceConnection对象mServiceConnection,重写onServiceConnectedonServiceDisconnected方法。在服务连接成功后,可以调用服务中的方法。
  3. 构建一个意图:Intent intent = new Intent(this, BindService.class);
    调用 bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);绑定服务。
  4. 在activity的onDestroy方法处,调用unbindService(),解绑服务。

BindService.java:
注意这里使用了LocalBinder内部类,继承自Binder,在内部类中将BindService对象本身返回。

public class BindService extends Service {
    private IBinder mBinder = new LocalBinder();
    private int mCounter = 0;

    public class LocalBinder extends Binder {
        public BindService getService() {
            return BindService.this;
        }
    }

    public int getCounter() {
        return mCounter;
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    public void incrementCounter() {
        mCounter++;
    }
}

在activity的onServiceConnected,从服务会返回给我们IBinder类型的 service对象,将它强转为 LocalBinder类型,调用getService方法,就可以获取到service对象,并使用service的方法。

public class MainActivity extends AppCompatActivity {
    private BindService mBoundService;
    private boolean mServiceBound = false;

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            BindService.LocalBinder binder = (BindService.LocalBinder) service;
            mBoundService = binder.getService();
            mServiceBound = true;

            // 在服务连接成功后,可以调用服务中的方法
            mBoundService.incrementCounter();
            int counter = mBoundService.getCounter();
            Log.d("MainActivity", "Counter: " + counter);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mServiceBound = false;
        }
    };

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

        // 绑定服务
        Intent intent = new Intent(this, BindService.class);
        bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        
        // 解绑服务
        if (mServiceBound) {
            unbindService(mServiceConnection);
            mServiceBound = false;
        }
    }
}

前台服务

前台服务可以给用户提供界面上的操作。 每个前台服务都必须要在通知栏显示一个通知(notification)。用户可以感知到app的前台服务正在运行。 这个通知(notification)默认是不能移除的。服务停止后,通知会被系统移除。 当用户不需要直接操作app,app需要给用户一个状态显示的时候,可以用前台服务。
例如各类音乐app
在这里插入图片描述
创建前台服务的步骤:

  1. 在manifest里注册活动:ForegroundDemoAct和服务:ForegroundService1。并且申请权限FOREGROUND_SERVICE。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.rustfisher.tutorial2020">

    <!--  前台服务权限  -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

    <application ...  >

        <service android:name=".service.foreground.ForegroundService1" />

        <activity
            android:name=".service.foreground.ForegroundDemoAct"
            android:launchMode="singleTop" />

    </application>
</manifest>

Activity的启动模式我们选择了singleTop。是为了方便演示点击通知时候的跳转效果。

2.启动前台服务
在activity中启动服务,调用startForegroundService(Intent)方法。

startForegroundService(Intent(applicationContext, ForegroundService1::class.java))

然后在service中,需要对应地使用startForeground方法。

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    Log.d(TAG, "onStartCommand flags:$flags, startId:$startId [$this] ${Thread.currentThread()}")

    val pendingIntent: PendingIntent =
            Intent(this, ForegroundDemoAct::class.java).let { notificationIntent ->
                PendingIntent.getActivity(this, 0, notificationIntent, 0)
            }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val chanId = "f-channel"
        val chan = NotificationChannel(chanId, "前台服务channel",
                NotificationManager.IMPORTANCE_NONE)
        chan.lightColor = Color.BLUE
        chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
        val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        service.createNotificationChannel(chan)
        Log.d(TAG, "服务调用startForeground")

        val notification: Notification =
                Notification.Builder(applicationContext, chanId)
                        .setContentTitle("RustFisher前台服务")
                        .setContentText("https://an.rustfisher.com")
                        .setSmallIcon(R.drawable.f_zan_1)
                        .setContentIntent(pendingIntent)
                        .build()
        startForeground(1, notification)
    } else {
        Log.d(TAG, "${Build.VERSION.SDK_INT} < O(API 26) ")
    }
    return super.onStartCommand(intent, flags, startId)
}

我们来看service里的这段代码。创建了一个简单的Notification。

  • PendingIntent会被分配给Notification,作为点击通知后的跳转动作
  • 使用NotificationManager先创建了一个NotificationChannel
  • 用Notification.Builder配置并创建一个Notification,例如配置标题,内容文字,图标等
  • 启动前台服务,调用startForeground(1, notification)方法

在设备上会显示出一个通知:
在这里插入图片描述
点击这个通知,会跳转到ForegroundDemoAct。这是之前用PendingIntent设置的。

  1. 停止服务
    stopService(Intent(applicationContext, ForegroundService1::class.java))

这样Service退出,走onDestroy方法。

  1. 停止前台服务
    在Service中调用stopForeground(boolean)方法,能停止前台,但是不退出整个服务。 这个boolean表示是否取消掉前台服务的通知。false表示保留通知。

例如在Service中调用

stopForeground(false)

服务就变成了后台服务,但是并没有退出。此时对应的通知可以滑动取消掉。

Binder 机制

IBinder 与 Binder

在 Android 开发中,IBinder 和 Binder 是用于实现进程间通信(IPC)的关键类。

  • IBinder(接口):
    IBinder 是一个接口,定义了用于跨进程通信的方法。它是 Android 系统中用于实现进程间通信的基础接口之一。在客户端和服务端之间进行通信时,客户端持有服务端返回的 IBinder 对象,通过该对象调用服务端提供的方法。

  • Binder(类):
    Binder 是 IBinder 接口的默认实现类,也是一个抽象类。它提供了将服务端对象和 IBinder 对象绑定在一起的机制,并实现了 IBinder 接口中的方法。在 Android 中,服务端可以继承 Binder 类创建自己的 Binder 子类,实现自定义的服务接口。

面向对象的IPC - Binder

Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。

与其它IPC不同,Binder使用了面向对象的思想来描述作为访问接入点的Binder及其在Client中的入口。Binder是一个实体位于Server中的对象,该对象提供了一套方法用以实现对服务的请求,就象类的成员函数。遍布于client中的入口可以看成指向这个binder对象的‘指针’,一旦获得了这个指针就可以调用该对象的方法访问server。

Binder通信模型

Binder框架定义了四个角色:Server,Client,ServiceManager(以后简称SMgr)以及Binder驱动。其中Server,Client,SMgr运行于用户空间,驱动运行于内核空间。这四个角色的关系和互联网类似:Server是服务器,Client是客户终端,SMgr是域名服务器(DNS),驱动是路由器。

Binder驱动

和路由器一样,Binder驱动虽然默默无闻,却是通信的核心。尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的:它工作于内核态,提供open(),mmap(),poll(),ioctl()等标准文件操作,以字符驱动设备中的misc设备注册在设备目录/dev下,用户通过/dev/binder访问该它。驱动负责进程之间Binder通信的建立,Binder在进程之间的传递,Binder引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。驱动和应用程序之间定义了一套接口协议,主要功能由ioctl()接口实现,不提供read(),write()接口,因为ioctl()灵活方便,且能够一次调用实现先写后读以满足同步交互,而不必分别调用write()和read()。Binder驱动的代码位于linux目录的drivers/misc/binder.c中。

ServiceManager 与实名Binder

和DNS类似,SMgr的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。注册了名字的Binder叫实名Binder,就象每个网站除了有IP地址外还有自己的网址。Server创建了Binder实体,为其取一个字符形式,可读易记的名字,将这个Binder连同名字以数据包的形式通过Binder驱动发送给SMgr,通知SMgr注册一个名叫张三的Binder,它位于某个Server中。驱动为这个穿过进程边界的Binder创建位于内核中的实体节点以及SMgr对实体的引用,将名字及新建的引用打包传递给SMgr。SMgr收数据包后,从中取出名字和引用填入一张查找表中。

ServiceManager什么时候注册的

细心的读者可能会发现其中的蹊跷:SMgr是一个进程,Server是另一个进程,Server向SMgr注册Binder必然会涉及进程间通信。当前实现的是进程间通信却又要用到进程间通信,这就好象蛋可以孵出鸡前提却是要找只鸡来孵蛋。Binder的实现比较巧妙:预先创造一只鸡来孵蛋:SMgr和其它进程同样采用Binder通信,SMgr是Server端,有自己的Binder对象(实体),其它进程都是Client,需要通过这个Binder的引用来实现Binder的注册,查询和获取。SMgr提供的Binder比较特殊,它没有名字也不需要注册,当一个进程使用BINDER_SET_CONTEXT_MGR命令将自己注册成SMgr时Binder驱动会自动为它创建Binder实体(这就是那只预先造好的鸡)。其次这个Binder的引用在所有Client中都固定为0而无须通过其它手段获得。也就是说,一个Server若要向SMgr注册自己Binder就必需通过0这个引用号和SMgr的Binder通信。类比网络通信,0号引用就好比域名服务器的地址,你必须预先手工或动态配置好。要注意这里说的Client是相对SMgr而言的,一个应用程序可能是个提供服务的Server,但对SMgr来说它仍然是个Client。

Client 获得实名Binder的引用

Server向SMgr注册了Binder实体及其名字后,Client就可以通过名字获得该Binder的引用了。Client也利用保留的0号引用向SMgr请求访问某个Binder:我申请获得名字叫张三的Binder的引用。SMgr收到这个连接请求,从请求数据包里获得Binder的名字,在查找表里找到该名字对应的条目,从条目中取出Binder的引用,将该引用作为回复发送给发起请求的Client。从面向对象的角度,这个Binder对象现在有了两个引用:一个位于SMgr中,一个位于发起请求的Client中。如果接下来有更多的Client请求该Binder,系统中就会有更多的引用指向该Binder,就象java里一个对象存在多个引用一样。而且类似的这些指向Binder的引用是强类型,从而确保只要有引用Binder实体就不会被释放掉。通过以上过程可以看出,SMgr象个火车票代售点,收集了所有火车的车票,可以通过它购买到乘坐各趟火车的票-得到某个Binder的引用。

Broadcast

Android应用可以通过广播从系统或其他App接收或发送消息。类似于订阅-发布设计模式。当某些事件发生时,可以发出广播。 系统在某些状态改变时会发出广播,例如开机、充电。App也可发送自定义广播。广播可用于组件间的通讯(包含应用内/不同应用间),是IPC的一种方式。

Android广播分为两个角色:广播发送者、广播接受者。

广播的实现原理

Android中的广播使用了设计模式中的观察者模式:基于消息的发布/订阅事件模型

因此Android将广播的发送者和接收者解耦,使得系统方便集成,更容易拓展。

观察者模式

  • 模型中有3个角色:
    1. 消息订阅者(广播接收者)
    2. 消息发布者(广播发布者)
    3. 消息中心(AMS,即Activity Manager Service)

示意图如下:

  1. 广播发送者通过AMS发送广播
  2. 广播接收者通过Binder机制在AMS注册
  3. AMS根据广播发送者要求,在已经注册列表中,寻找合适的广播接收者。
  4. AMS将广播发送到合适的广播接受者相应的消息循环队列中。
  5. 广播接收者通过消息队列,拿到此广播,并回调onReceive()。
    在这里插入图片描述

广播的种类

  • 系统广播:系统发出的广播,如通知网络断开、蓝牙断开。
  • 自定义广播:开发者注册的广播。
  • 标准广播:完全异步的广播。广播发出后,所有的广播接收器几乎同时接收到这条广播。 不同的App可以注册并接到标准广播。例如系统广播。
  • 有序广播:同步广播。同一时刻只有一个广播接收器能接收到这条广播。这个接收器处理完后,广播才会继续传递。 有序广播是全局的广播。
  • 本地广播:只在本App发送和接收的广播。注册为本地广播的接收器无法收到标准广播。
  • 带权限的广播:发送广播时可以带上相关权限,申请了权限的App或广播接收器才能收到相应的带权限的广播。 如果在manifest中申请了相应权限,接收器可以不用再申请一次权限即可接到相应广播。

接收广播

自定义广播接收者

收新建一个MyExampleReceiver继承自BroadcastReceiver,并重写onReceiver()方法

public class MyExampleReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context,"Got it",Toast.LENGTH_SHORT).show();
        //abortBroadcast();
    }
}

abortBroadcast()可以截断有序广播。

注意:不要在onReceive()方法中添加过多的逻辑操作或耗时的操作。因为在广播接收器中不允许开启线程,当onReceive()方法运行较长时间而没结束时,程序会报错。因此广播接收器一般用来打开其他组件,比如创建一条状态栏通知或启动一个服务。

两种方式注册广播接收者

  • 静态注册:在AndroidManifest.xml中注册广播接收器;android:name里填接收器的名字。 可以设置广播接收器优先级:
<intent-filter android:priority="100">
<receiver android:name=".MyExampleReceiver">
    <intent-filter>
        <action android:name="com.rust.broadcasttest.MY_BROADCAST"/>
    </intent-filter>
</receiver>

在APP首次启动时,系统会自动实例化MyExampleReceiver类,注册到系统中。

  • 动态注册
    在代码中通过调用Context.registerReceiver()方法
// 选择在Activity生命周期方法中的onResume()中注册
@Override
  protected void onResume(){
      super.onResume();

    // 1. 实例化BroadcastReceiver子类 &  IntentFilter
     mBroadcastReceiver mBroadcastReceiver = new mBroadcastReceiver();
     IntentFilter intentFilter = new IntentFilter();

    // 2. 设置接收广播的类型
    intentFilter.addAction(android.net.conn.CONNECTIVITY_CHANGE);

    // 3. 动态注册:调用Context的registerReceiver()方法
     registerReceiver(mBroadcastReceiver, intentFilter);
 }


// 注册广播后,要在相应位置记得销毁广播
// 即在onPause() 中unregisterReceiver(mBroadcastReceiver)
// 当此Activity实例化时,会动态将MyBroadcastReceiver注册到系统中
// 当此Activity销毁时,动态注册的MyBroadcastReceiver将不再接收到相应的广播。
 @Override
 protected void onPause() {
     super.onPause();
      //销毁在onResume()方法中的广播
     unregisterReceiver(mBroadcastReceiver);
     }
}

特别注意:动态广播最好在ActivityonResume()注册,onPause注销。
原因在于:

  • 对于动态广播,有注册就必须有注销,否则会内存泄露。
  • 重复注册、重复注销也是不允许的。
  • 在onResume()注册、onPause()注销是因为onPause()在App死亡前一定会被执行,从而保证广播在App死亡前一定会被注销,从而防止内存泄露。

两种方式注册的优缺点:
在这里插入图片描述

广播发送

App有3种发送广播的方式。发送广播需要使用Intent类。

  • sendOrderedBroadcast(Intent, String)
    发送有序广播。每次只有1个广播接收器能接到广播。 接收器接到有序广播后,可以完全地截断广播,或者传递一些信息给下一个接收器。 有序广播的顺序可受android:priority标签影响。同等级的接收器收到广播的顺序是随机的。
  • sendBroadcast(Intent)
    以一个未定义的顺序向所有接收器发送广播。也称作普通广播。 这种方式更高效,但是接收器不能给下一个接收器传递消息。这类广播也无法截断。
  • LocalBroadcastManager.sendBroadcast
    广播只能在应用程序内部进行传递,并且广播接收器也只能接收到来自本应用程序发出的广播。 这个方法比全局广播更高效(不需要Interprocess communication,IPC),而且不需要担心其它App会收到你的广播以及其他安全问题。

Content Provider

Content Provider是Android系统的四大组件之一,他们封装好数据,并提供定义数据安全的机制。Content Provider 是一个进程同另一个进程连接数据的标准接口。在Android系统中,应用程序之间是相互独立的,分别运行在自己的进程中,相互之间没有数据交换。若应用程序之间需要程序共享那么用的手段就是Content Provider。

原理

content provider 可以实现数据交换与共享,属于一种跨进程通信方式。

content provider 的底层采用的是Android中的binder机制。
在这里插入图片描述

在这里插入图片描述

content provider的使用方式

统一资源标识符(URI)

Uniform Resource Identifier,URI,即统一资源标识符
唯一标识content provider 其中的数据。

外界进程通过URI找到对应的contentprovider中的数据,再进程数据操作。

URI分为系统预置和自定义两种,分别对应着系统内置的数据(如通讯录、日程表等等)和自定义的数据库。

这里主要讲解自定义URI

URI的格式一般包含 Schema(主题) + Authority(授权信息) + Path(表名) + ID(记录)。

在这里插入图片描述

多用途互联网邮件扩展类(MIME)

Multipurpose Internet Mail Extensions,MIME,
多用途互联网邮件扩展类,是设定某种扩展名文件用一种应用程序打开的方式类型。

  • 作用:指定某个扩展名的文件用某种应用程序打开。如指定.html文件采用text应用程序打开,指定.pdf文件采用flash应用程序打开。

contentprovider 根据URI返回MIME类型

ContentProvider.geType(uri)

每种MIME类型 由2部分组成 = 类型 + 子类型
是一种包含2部分的字符串。

text / html
// 类型 = text、子类型 = html
text/css
text/xml
application/pdf

ContentProvider类

ContentProvider主要以表格的形式组织数据。同时也支持文件类型,只是表格形式用得比较多。

类似数据库,每个表格包含多张表,每张表包含多个行列,对应记录和字段。

进程间共享数据的本质是:添加、删除、获取 & 修改(更新)数据

所以ContentProvider的核心方法主要也是上述4个应用。

<-- 4个核心方法 -->
  public Uri insert(Uri uri, ContentValues values) 
  // 外部进程向 ContentProvider 中添加数据

  public int delete(Uri uri, String selection, String[] selectionArgs) 
  // 外部进程 删除 ContentProvider 中的数据

  public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
  // 外部进程更新 ContentProvider 中的数据

  public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,  String sortOrder)  
  // 外部应用 获取 ContentProvider 中的数据

// 注:
  // 1. 上述4个方法由外部进程回调,并运行在ContentProvider进程的Binder线程池中(不是主线程)
 // 2. 存在多线程并发访问,需要实现线程同步
   // a. 若ContentProvider的数据存储方式是使用SQLite & 一个,则不需要,因为SQLite内部实现好了线程同步,若是多个SQLite则需要,因为SQL对象之间无法进行线程同步
  // b. 若ContentProvider的数据存储方式是内存,则需要自己实现线程同步

<-- 2个其他方法 -->
public boolean onCreate() 
// ContentProvider创建后 或 打开系统后其它进程第一次访问该ContentProvider时 由系统进行调用
// 注:运行在ContentProvider进程的主线程,故不能做耗时操作

public String getType(Uri uri)
// 得到数据类型,即返回当前 Url 所代表数据的MIME类型

ContentResolver类

ContentProvider类并不会直接与外部进程交互,而是通过ContentResolver 类。

ContentResolver 的作用是同一管理不同的ContentProvider间的操作。

  1. 即通过 URI 即可操作 不同的ContentProvider 中的数据
  2. 外部进程通过 ContentResolver类 从而与ContentProvider类进行交互

为什么要使用通过ContentResolver类从而与ContentProvider类进行交互,而不直接访问ContentProvider类?

  • 一般来说,一款应用要使用多个ContentProvider,若需要了解每个ContentProvider的不同实现从而再完成数据交互,操作成本高 & 难度大
  • 所以再ContentProvider类上加多了一个 ContentResolver类对所有的ContentProvider进行统一管理。

ContentResolver 类提供了与ContentProvider类相同名字 & 作用的4个方法:

// 外部进程向 ContentProvider 中添加数据
public Uri insert(Uri uri, ContentValues values)  

// 外部进程 删除 ContentProvider 中的数据
public int delete(Uri uri, String selection, String[] selectionArgs)

// 外部进程更新 ContentProvider 中的数据
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)  

// 外部应用 获取 ContentProvider 中的数据
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)

使用案例:

// 使用ContentResolver前,需要先获取ContentResolver
// 可通过在所有继承Context的类中 通过调用getContentResolver()来获得ContentResolver
ContentResolver resolver =  getContentResolver(); 

// 设置ContentProvider的URI
Uri uri = Uri.parse("content://cn.scu.myprovider/user"); 
 
// 根据URI 操作 ContentProvider中的数据
// 此处是获取ContentProvider中 user表的所有记录 
Cursor cursor = resolver.query(uri, null, null, null, "userid desc"); 

ContentProvider辅助工具类

Android提供了3个用于辅助ContentProvide的工具类:

  • ContentUris
  • UriMatcher
  • ContentObserver
ContentUris类

它的作用是操作URI,核心方法有两个:withAppendedID()parseId().
withAppendedID()给URI追加一个标识id,而parseId()的作用正好相反,用于解析URI的id。

// withAppendedId()作用:向URI追加一个id
Uri uri = Uri.parse("content://cn.scu.myprovider/user") 
Uri resultUri = ContentUris.withAppendedId(uri, 7);  
// 最终生成后的Uri为:content://cn.scu.myprovider/user/7

// parseId()作用:从URL中获取ID
Uri uri = Uri.parse("content://cn.scu.myprovider/user/7") 
long personid = ContentUris.parseId(uri); 
//获取的结果为:7
UriMatcher类

它的作用在于:

  1. 在contentprovider中注册URI
  2. 根据URI匹配contentprovider中对应的数据表

UriMatcher类与ContentUris类提供的方法是相互辅助的。

具体使用:

// 步骤1:初始化UriMatcher对象
    UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); 
    //常量UriMatcher.NO_MATCH  = 不匹配任何路径的返回码
    // 即初始化时不匹配任何东西

// 步骤2:在ContentProvider 中注册URI(addURI())
    int URI_CODE_a = 1int URI_CODE_b = 2;
    matcher.addURI("cn.scu.myprovider", "user1", URI_CODE_a); 
    matcher.addURI("cn.scu.myprovider", "user2", URI_CODE_b); 
    // 若URI资源路径 = content://cn.scu.myprovider/user1 ,则返回注册码URI_CODE_a
    // 若URI资源路径 = content://cn.scu.myprovider/user2 ,则返回注册码URI_CODE_b

// 步骤3:根据URI 匹配 URI_CODE,从而匹配ContentProvider中相应的资源(match())

@Override   
    public String getType(Uri uri) {   
      Uri uri = Uri.parse(" content://cn.scu.myprovider/user1");   

      switch(matcher.match(uri)){   
     // 根据URI匹配的返回码是URI_CODE_a
     // 即matcher.match(uri) == URI_CODE_a
      case URI_CODE_a:   
        return tableNameUser1;   
        // 如果根据URI匹配的返回码是URI_CODE_a,则返回ContentProvider中的名为tableNameUser1的表
      case URI_CODE_b:   
        return tableNameUser2;
        // 如果根据URI匹配的返回码是URI_CODE_b,则返回ContentProvider中的名为tableNameUser2的表
    }   
}

ContentObserver类

内容观察者,它的作用是观察URI引起contentprovider中的数据变化,并通知外界(访问该数据的访问者)

当ContentProvider中的数据发生变化(增删改)时,就会触发该ContentObserver类。

// 步骤1:注册内容观察者ContentObserver
    getContentResolver().registerContentObserver(uri);
    // 通过ContentResolver类进行注册,并指定需要观察的URI

// 步骤2:当该URI的ContentProvider数据发生变化时,通知外界(即访问该ContentProvider数据的访问者)
    public class UserContentProvider extends ContentProvider { 
      public Uri insert(Uri uri, ContentValues values) { 
      db.insert("user", "userid", values); 
      getContext().getContentResolver().notifyChange(uri, null); 
      // 通知访问者
   } 
}

// 步骤3:解除观察者
 getContentResolver().unregisterContentObserver(uri);
    // 同样需要通过ContentResolver类进行解除

使用案例

ContentProvider不仅常用于进程间通信,同时也适用于进程内通信。

进程内通信

步骤如下:

  1. 创建数据库类DBHelper.java继承自SQLiteOpenHelper

在这个案例中,创建了一个名为“finch.db”的数据库,并创建了“user”表和“job”表。

public class DBHelper extends SQLiteOpenHelper {

    // 数据库名
    private static final String DATABASE_NAME = "finch.db";

    // 表名
    public static final String USER_TABLE_NAME = "user";
    public static final String JOB_TABLE_NAME = "job";

    private static final int DATABASE_VERSION = 1;
    //数据库版本号

    public DBHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {

        // 创建两个表格:用户表 和职业表
        db.execSQL("CREATE TABLE IF NOT EXISTS " + USER_TABLE_NAME + "(_id INTEGER PRIMARY KEY AUTOINCREMENT," + " name TEXT)");
        db.execSQL("CREATE TABLE IF NOT EXISTS " + JOB_TABLE_NAME + "(_id INTEGER PRIMARY KEY AUTOINCREMENT," + " job TEXT)");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)   {

    }
}

  1. 自定义ContentProvider类
public class MyProvider extends ContentProvider {

    private Context mContext;
    DBHelper mDbHelper = null;
    SQLiteDatabase db = null;

    public static final String AUTOHORITY = "cn.scu.myprovider";
    // 设置ContentProvider的唯一标识

    public static final int User_Code = 1;
    public static final int Job_Code = 2;

    // UriMatcher类使用:在ContentProvider 中注册URI
    private static final UriMatcher mMatcher;
    static{
        mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        // 初始化
        mMatcher.addURI(AUTOHORITY,"user", User_Code);
        mMatcher.addURI(AUTOHORITY, "job", Job_Code);
        // 若URI资源路径 = content://cn.scu.myprovider/user ,则返回注册码User_Code
        // 若URI资源路径 = content://cn.scu.myprovider/job ,则返回注册码Job_Code
    }

    // 以下是ContentProvider的6个方法

    /**
     * 初始化ContentProvider
     */
    @Override
    public boolean onCreate() {

        mContext = getContext();
        // 在ContentProvider创建时对数据库进行初始化
        // 运行在主线程,故不能做耗时操作,此处仅作展示
        mDbHelper = new DBHelper(getContext());
        db = mDbHelper.getWritableDatabase();

        // 初始化两个表的数据(先清空两个表,再各加入一个记录)
        db.execSQL("delete from user");
        db.execSQL("insert into user values(1,'Carson');");
        db.execSQL("insert into user values(2,'Kobe');");

        db.execSQL("delete from job");
        db.execSQL("insert into job values(1,'Android');");
        db.execSQL("insert into job values(2,'iOS');");

        return true;
    }

    /**
     * 添加数据
     */

    @Override
    public Uri insert(Uri uri, ContentValues values) {

        // 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
        // 该方法在最下面
        String table = getTableName(uri);

        // 向该表添加数据
        db.insert(table, null, values);

        // 当该URI的ContentProvider数据发生变化时,通知外界(即访问该ContentProvider数据的访问者)
        mContext.getContentResolver().notifyChange(uri, null);

//        // 通过ContentUris类从URL中获取ID
//        long personid = ContentUris.parseId(uri);
//        System.out.println(personid);

        return uri;
        }

    /**
     * 查询数据
     */
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        // 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
        // 该方法在最下面
        String table = getTableName(uri);

//        // 通过ContentUris类从URL中获取ID
//        long personid = ContentUris.parseId(uri);
//        System.out.println(personid);

        // 查询数据
        return db.query(table,projection,selection,selectionArgs,null,null,sortOrder,null);
    }

    /**
     * 更新数据
     */
    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {
        // 由于不展示,此处不作展开
        return 0;
    }

    /**
     * 删除数据
     */
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        // 由于不展示,此处不作展开
        return 0;
    }

    @Override
    public String getType(Uri uri) {

        // 由于不展示,此处不作展开
        return null;
    }

    /**
     * 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
     */
    private String getTableName(Uri uri){
        String tableName = null;
        switch (mMatcher.match(uri)) {
            case User_Code:
                tableName = DBHelper.USER_TABLE_NAME;
                break;
            case Job_Code:
                tableName = DBHelper.JOB_TABLE_NAME;
                break;
        }
        return tableName;
        }
    }
  1. 在AndroidManifest.xml中注册创建的ContentProvider类,这样在APP启动时myprovider就会被加载到系统中。
<provider android:name="MyProvider"
 android:authorities="cn.scu.myprovider"/>
  1. 进程内访问ContentProvider中的数据

在mainActivity中访问ContentProvider中的数据。

public class MainActivity extends AppCompatActivity {

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

        /**
         * 对user表进行操作
         */

        // 设置URI
        Uri uri_user = Uri.parse("content://cn.scu.myprovider/user");

        // 插入表中数据
        ContentValues values = new ContentValues();
        values.put("_id", 3);
        values.put("name", "Iverson");


        // 获取ContentResolver
        ContentResolver resolver =  getContentResolver();
        // 通过ContentResolver 根据URI 向ContentProvider中插入数据
        resolver.insert(uri_user,values);

        // 通过ContentResolver 向ContentProvider中查询数据
        Cursor cursor = resolver.query(uri_user, new String[]{"_id","name"}, null, null, null);
        while (cursor.moveToNext()){
            System.out.println("query book:" + cursor.getInt(0) +" "+ cursor.getString(1));
            // 将表中数据全部输出
        }
        cursor.close();
        // 关闭游标

        /**
         * 对job表进行操作
         */
        // 和上述类似,只是URI需要更改,从而匹配不同的URI CODE,从而找到不同的数据资源
        Uri uri_job = Uri.parse("content://cn.scu.myprovider/job");
        
        // 插入表中数据
        ContentValues values2 = new ContentValues();
        values2.put("_id", 3);
        values2.put("job", "NBA Player");

        // 获取ContentResolver
        ContentResolver resolver2 =  getContentResolver();
        // 通过ContentResolver 根据URI 向ContentProvider中插入数据
        resolver2.insert(uri_job,values2);

        // 通过ContentResolver 向ContentProvider中查询数据
        Cursor cursor2 = resolver2.query(uri_job, new String[]{"_id","job"}, null, null, null);
        while (cursor2.moveToNext()){
            System.out.println("query job:" + cursor2.getInt(0) +" "+ cursor2.getString(1));
            // 将表中数据全部输出
        }
        cursor2.close();
        // 关闭游标
}
}

结果:
在这里插入图片描述

ContentProvider的优点总结

  • 1、安全:ContentProvider为应用间的数据交互提供了一个安全的环境:允许把自己的应用数据根据需求开放给 其他应用 进行 增、删、改、查,而不用担心因为直接开放数据库权限而带来的安全问题。
  • 2、访问简单且高效,对比于其他对外共享数据的方式,数据访问方式会因数据存储的方式而不同:
  • 采用文件方式对外共享数据,需要文件操作读写数据;
  • 采用sharedpreferences共享数据,需要使用sharedpreferences 的API读写数据。
  • 采用ContentProvider的方式,其解耦了底层数据的存储方式,使得无论底层数据存储采用何种方式,外界对数据的访问方式都是统一的,这使得访问变动简单高效。

比如一开始数据存储方式采用SQLite数据库,后来把数据库换成MongoDB,也不会对上层数据contentprovider使用代码产生影响。

在这里插入图片描述

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值