文章目录
- Activity
- 生命周期
- onCreate和onStart的区别
- onPause和onStop的区别
- 生命周期的变化
- Activity的启动
- Intent
- Bundle
- Activity携带参数返回
- Activity启动模式
- 任务(task),返回栈(back stack)
- Activity的四种启动模式
- standard(默认模式)
- singleTop
- singleTask
- singleInstance
- Activity中获取View的宽高
- 应用:动态调整ImageView的宽高
- Fragment
- Fragment的优点
- Fragmet的生命周期
- 生命周期变化
- Fragment与Activity不同的生命周期
- 加载和使用Fragmet
- 向Activity中添加Fragment
- 静态加载
- 动态加载
- 管理Fragmet
- Fragment间通信
- Fragment与其附着的Activity之间通信方式
- DialogFragment
- Service
- 服务和子线程的区别
- 服务的状态
- 服务的生命周期
- 使用服务
- 后台服务
- startService()
- bindService()
- 前台服务
- Binder 机制
- IBinder 与 Binder
- 面向对象的IPC - Binder
- Binder通信模型
- Binder驱动
- ServiceManager 与实名Binder
- ServiceManager什么时候注册的
- Client 获得实名Binder的引用
- Broadcast
- 广播的实现原理
- 观察者模式
- 广播的种类
- 接收广播
- 自定义广播接收者
- 两种方式注册广播接收者
- 广播发送
- Content Provider
- 原理
- content provider的使用方式
- 统一资源标识符(URI)
- 多用途互联网邮件扩展类(MIME)
- ContentProvider类
- ContentResolver类
- ContentProvider辅助工具类
- ContentUris类
- UriMatcher类
- ContentObserver类
- 使用案例
- 进程内通信
- ContentProvider的优点总结
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 相对密集的操作。
生命周期的变化
- 启动后退出:
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
- 旋转手机(横竖屏切换时的生命周期变化)
[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 - 两个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
- 弹出一个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 。
步骤:
- 主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);
- 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;
- 在主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 的回调方法移入片段相应的回调方法中。
生命周期变化
- Fragmet被创建时:
onAttach()
onCreate()
onCreateView()
onActivityCreated()
- Fragment对用户可见时:
onStart()
onResume()
- Fragmet进入“后台模式”时:
onPause()
onStop()
- 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
弹窗,是常见的一种提示方式。市面上有多种多样、五彩缤纷的弹窗。
构建步骤:
- 准备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>
- 新建弹窗类,继承自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。
步骤:
- 创建一个服务类,继承自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 服务关闭时");
}
- 在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) 向其他应用公开某些应用功能,则应创建绑定服务。
构建绑定型服务的步骤:
- 新建一个Service的子类区别为BindService.java,重写回调方法onCreate(),onBind(),onUnbind(),onDestroy()。
- 在activity中创建服务连接
ServiceConnection
对象mServiceConnection,重写onServiceConnected
和onServiceDisconnected
方法。在服务连接成功后,可以调用服务中的方法。 - 构建一个意图:
Intent intent = new Intent(this, BindService.class);
调用bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
绑定服务。 - 在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
创建前台服务的步骤:
- 在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设置的。
- 停止服务
stopService(Intent(applicationContext, ForegroundService1::class.java))
这样Service退出,走onDestroy方法。
- 停止前台服务
在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个角色:
- 消息订阅者(广播接收者)
- 消息发布者(广播发布者)
- 消息中心(AMS,即Activity Manager Service)
示意图如下:
- 广播发送者通过AMS发送广播
- 广播接收者通过Binder机制在AMS注册
- AMS根据广播发送者要求,在已经注册列表中,寻找合适的广播接收者。
- AMS将广播发送到合适的广播接受者相应的消息循环队列中。
- 广播接收者通过消息队列,拿到此广播,并回调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);
}
}
特别注意:动态广播最好在Activity
的onResume()
注册,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间的操作。
- 即通过 URI 即可操作 不同的ContentProvider 中的数据
- 外部进程通过 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类
它的作用在于:
- 在contentprovider中注册URI
- 根据URI匹配contentprovider中对应的数据表
UriMatcher类与ContentUris类提供的方法是相互辅助的。
具体使用:
// 步骤1:初始化UriMatcher对象
UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
//常量UriMatcher.NO_MATCH = 不匹配任何路径的返回码
// 即初始化时不匹配任何东西
// 步骤2:在ContentProvider 中注册URI(addURI())
int URI_CODE_a = 1;
int 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不仅常用于进程间通信,同时也适用于进程内通信。
进程内通信
步骤如下:
- 创建数据库类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) {
}
}
- 自定义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;
}
}
- 在AndroidManifest.xml中注册创建的ContentProvider类,这样在APP启动时myprovider就会被加载到系统中。
<provider android:name="MyProvider"
android:authorities="cn.scu.myprovider"/>
- 进程内访问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使用代码产生影响。