这本书的知识其实比较基础,以后就不再通篇记录了,只记录一下自己的收获。
第二十五章 HTTP 与后台任务
一、AsyncTask 的替代方案
虽然调用 Fragment 的 setRetainInstance(true) 方法来保存数据可以解决问题,但它不是万能的。很多时候,你还得介入,编写特殊场景应对代码,让应用无懈可击。这些特殊场景有:用户在 AsyncTask 运行时按后退键,以及启动AsyncTask 的 fragment 因内存紧张而被销毁。
使用 Loader 是另一种可行的解决方案。它可以代劳很多(并非全部)棘手的事情。Loader 用来从某些数据源加载数据(对象)。数据源可以是磁盘、数据库、ContentProvider、网络,甚至是另一进程。
AsyncTaskLoader 是个抽象 Loader。它可以使用 AsyncTask 把数据加载工作转移到其他线程上,能在不阻塞主线程的前提下获取数据,并把结果发送给目标对象。
最重要的原因是,遇到类似设备旋转这样的场景时,LoaderManager 会帮我们妥善管理 loader 及其加载的数据。而且,LoaderManager 还负责启动和停止 loader,以及管理 loader 的生命周期。设备配置改变后,如果初始化一个已经加载完数据的loader,它能立即提交数据,而不是再次尝试获取数据。无论fragment是否得到保留,它都会这样做。
感兴趣的可以看看这个:,不是我写的,我就记录个链接。
第二十六章 Looper、Handler 和 HandlerThread
一、ImageView 的 scaleType 属性:centerCrop 作用是先居中放置图片,然后放大较小图片,裁剪较大图片(裁两头)以匹配视图。
二、与主线程通信
AsyncTask 不适合用于那些重复且长时间运行的任务。我们需要创建一个专用的后台线程。但是,后台线程需要与主线程通信才能更新 UI。
(message queue)。使用消息队列的线程叫作消息循环(message loop)。消息循环会循环检查队列上是否有新消息。
消息循环由线程和 looper 组成。Looper 对象管理着线程的消息队列。主线程就是个消息循环,因此也拥有 looper。主线程的所有工作都是由其 looper 完成的。looper 不断从消息队列中抓取消息,然后完成消息指定的任务。
1、剖析 Handler
要处理消息以及消息指定的任务,首先需要一个 Handler 实例。Handler 不仅仅是处理 Message 的目标(target),也是创建和发布 Message 的接口,如图:
Looper 拥有 Message 对象的收件箱,所以 Message 必须在 Looper 上发布或处理。既然有这层关系,为协同工作,Handler总是引用着 Looper。
一个 Handler 仅与一个 Looper 相关联,一个 Message 也仅与一个目标 Handler(也称作 Message 目标)相关联,Looper 拥有整个 Message 队列。多个 Message 可以引用同一目标 Handler。多个 Handler 也可与一个 Looper 相关联。这意味着一个 Handler 的 Message 可能与另一个 Handler 的 Message 存放在同一消息队列中。
2、使用 Handler
一般来讲,不应手动设置消息的目标Handler。创建信息时,最好调用 Handler.obtainMessage (...)方法。传入其他必要消息字段后,该方法会自动设置目标 Handler。
为避免反复创建新的 Message 对象,Handler.obtainMessage(...)方法会从公共回收池里获取消息。相比创建新实例,这样更加高效。
一旦取得 Message,就可以调用 sendToTarget() 方法将其发送给它的 Handler。然后,Handler 会将这个 Message 放置在Looper 消息队列的尾部。消息的 what 属性是一个定义为MESSAGE_DOWNLOAD的常量,用来标识下载请求消息。消息的 obj 属性是一个T类型对象。
Looper 取得消息队列中的特定消息后,会将它发送给消息的目标 Handler 去处理。消息一般是在目标 Handler 的Handler.handleMessage(...)实现方法中进行处理的。
图26-6展示了其中的对象关系:
ConcurrentHashMap 是一种线程安全的 HashMap。
HandlerThread.onLooperPrepared() 是在 Looper 首次检查消息队列之前调用,所以该方法是创建 Handler 实现的好地方。在 Handler.handleMessage(...) 方法中,首先检查消息类型,再获取 obj 值(T类型下载请求),然后进行数据处理。队列中的下载消息取出并可以处理时,就会触发调用 Handler.handleMessage(...) 方法。
3、传递 Handler
如此,我们已可以从主线程安排后台线程任务,反过来,也可以从后台线程使用与主线程关联的 Handler,安排主线程任务。
主线程是一个拥有 handler 和 Looper 的消息循环。主线程上创建的 Handler 会自动与它的 Looper 相关联。主线程上创建的这个 Handler 也可以传递给另一线程。传递出去的 Handler 与创建它的线程 Looper 始终保持着联系。因此,已传出 Handler 负责处理的所有消息都将在主线程的消息队列中处理。
最后,还有一个风险点,如果用户旋转屏幕,可能会发生异常。别忘了在 Activity 或者 Fragment 销毁的时候,清除队列中的所有请求:
public void clearQueue() {
//MESSAGE_DOWNLOAD:msg的what
mRequestHandler.removeMessages(MESSAGE_DOWNLOAD);
mRequestMap.clear();
}
三、AsyncTask 与线程
理解了 Handler 和 Looper 之后,AsyncTask 也就没有当初看上去那么神奇了。为什么要用HandlerThread,而不用它呢?
原因有好几个。最基本的一个是 AsyncTask 的工作方式,它主要应用于那些短暂且较少重复的任务。如果创建了大量的AsyncTask,或者长时间在运行 AsyncTask,那么很可能就是错用了它。
有一个技术层面的理由更让人信服:在Android 3.2系统版本中,AsyncTask 的内部实现有了重大变化。自Android 3.2版本起,AsyncTask 不再为每一个 AsyncTask 实例单独创建线程。相反,它使用一个 Executor 在单一的后台线程上运行所有AsyncTask 后台任务。这意味着每个 AsyncTask 都需要排队顺序执行。显然,长时间运行的 AsyncTask 会阻塞其他AsyncTask。
使用一个线程池 executor 虽然可安全地并发运行多个 AsyncTask,但不推荐这么做。如果真的考虑这么做,最好自己处理线程相关的工作,必要时就使用 Handler 与主线程通信。
四、StrictMode
开发应用时,有些东西最好要避免,比如,让应用崩溃的代码漏洞、安全漏洞等。举例来讲,网络条件不好的情况下,在主线程上发送网络请求很可能就会导致设备出现 ANR 错误。
表现在后台的话,你应该会看到 NetworkOnMainThread 异常以及其他大量日志信息。这实际是 StrictMode 就错误在警告你。Android 引入的 StrictMode 可以帮助开发者探测代码问题。像在主线程上发起网络请求、编码漏洞以及安全漏洞这样的问题都是它探测的对象。
无需配置,StrictMode 就会阻止在主线程上发起网络请求这样的代码问题。它还能探测影响系统性能的代码问题。想启用StrictMode 默认防御策略的话,调用 StrictMode.enableDefaults() 方法就行了。
一旦调用了 StrictMode.enableDefaults() 方法,如果代码有相关问题,就能在Logcat看到以下提醒:
- 在主线程上发起网络请求
- 在主线程上做了磁盘读写
- Activity 未及时销毁(又称为 activity 泄露)
- SQLite 数据库游标未关闭
- 网络通信使用了明文(未使用 SSL/TLS 加密)
假如应用违反了防御策略,你想定制应对行为,可使用 ThreadPolicy.Builder 和 VmPolicy.Builder 类定制。你可以定制的应对行为有:控制是否抛出异常,弹出对话框或是日志记录违反策略警示信息。
第二十七章 搜索
学习如何使用 SearchView 在应用中整合搜索功能。SearchView 是个可以嵌入工具栏的操作视图类(action view)。用户提交过的查询关键字会保存下来。
一、使用 SearchView
SearchView 是个操作视图。所谓操作视图,就是可以内置在工具栏中的视图。SearchView 可以让整个搜索界面完全内置在应用的工具栏中。
首先,确认应用顶部有工具栏(包含应用名称)。如果没有,需要添加。
接下来,为项目创建一个新的菜单XML文件在 res/menu/fragment_photo_gallery.xml,可以通过这个文件指定工具栏上要显示什么。
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- app:actionViewClass 属性指定的值,告诉工具栏要显示 SearchView -->
<item android:id="@+id/menu_item_search"
android:title="@string/search"
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="ifRoom" />
<!-- 添加一个Clear Search选项 -->
<item android:id="@+id/menu_item_clear"
android:title="@string/clear_search"
app:showAsAction="never" />
</menu>
在 onCreate(...) 方法中调用 setHasOptionsMenu(true) 方法让 activity 或者 fragment 接收菜单回调方法,然后,覆盖 onCreateOptionsMenu(...) 方法并实例化菜单XML文件。这样,工具栏就能显示定义在菜单XML中的选项了。
//onCreate()方法中
setHasOptionsMenu(true);
//覆盖onCreateOptionsMenu(...)方法
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
super.onCreateOptionsMenu(menu, menuInflater);
menuInflater.inflate(R.menu.fragment_photo_gallery, menu);
}
1、响应用户搜索
SearchView.OnQueryTextListener 接口已提供了接收回调的方式,可以响应查询指令。
更新上面的 onCreateOptionsMenu(...) 方法,添加一个 SearchView.OnQueryTextListener 监听方法:
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
super.onCreateOptionsMenu(menu, menuInflater);
menuInflater.inflate(R.menu.fragment_photo_gallery, menu);
//先从菜单中取出MenuItem并把它保存在searchItem变量中
MenuItem searchItem = menu.findItem(R.id.menu_item_search);
//使用getActionView()方法从这个变量中取出SearchView对象
final SearchView searchView = (SearchView) searchItem.getActionView();
//设置SearchView. OnQueryTextListener
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
/**
* 提交搜索查询时,此方法就会执行
* 用户提交的搜索字符串会传给它。搜索请求受理后,该方法会返回true
*/
@Override
public boolean onQueryTextSubmit(String s) {
Log.d(TAG, "QueryTextSubmit: " + s);
return true;
}
//只要SearchView文本框里的文字有变化,此方法就会执行
@Override
public boolean onQueryTextChange(String s) {
Log.d(TAG, "QueryTextChange: " + s);
return false;
}
});
}
注意:如果在模拟器上使用物理键盘(比如笔记本的键盘)提交查询,搜索会连续执行两次。从用户角度看,就是先看到下载的搜索结果,然后这些图片又全部重新加载一次。这是SearchView的一个bug。这个问题只会出现在模拟器上。
若是需要搜索框能显示已保存或已查询过的字符串,可以利用这个回调方法设置搜索文本框的值:
//用户点击搜索按钮时,SearchView的View.OnClickListener.onClick()方法会被调用
searchView.setOnSearchClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String query = QueryPreferences.getStoredQuery(getActivity());
searchView.setQuery(query, false);
}
});
二、使用 shared preferences 实现轻量级数据存储
shared preferences 文件保存在应用沙盒中,所以,不应用它保存类似密码这样的敏感信息。
要获得 SharedPreferences 定制实例,可使用 Context.getSharedPreferences (String, int)方法。然而,在实际开发中,我们并不关心 SharedPreferences 实例具体是什么样,只要它能共享于整个应用就可以了。这种情况下,最好使用PreferenceManager.getDefaultSharedPreferences(Context) 方法,该方法会返回具有私有权限和默认名称的实例(仅在当前应用内可用)。
第二十八章 后台服务
一、IntentService的使用
在 Android 应用程序中,普通的 Service 代码是运行在主线程中的,如果想要在 Service 中进行一些耗时的操作,可能会出现ANR的现象,大概是 20s 会出现。所以,我们需要在 onStartCommand() 方法中开启一个子线程去执行耗时操作,执行完毕后,如果需要停止服务还得调用 stopSelf() 方法。
有一种简单的方法来处理这个过程,就是使用 IntentService。
IntentService 是继承于 Service 并处理异步请求的一个类,在 IntentService 内有一个工作线程来处理耗时操作,启动IntentService 的方式和启动传统 Service 一样,同时,当任务执行完后,IntentService 会自动停止,而不需要我们去手动控制。另外,可以启动 IntentService 多次,而每一个耗时操作会以工作队列的方式在 IntentService 的 onHandleIntent 回调方法中执行,并且,每次只会执行一个工作线程,执行完第一个再执行第二个,以此类推。所有请求都在一个单线程中,不会阻塞应用程序的主线程(UI Thread),同一时间只处理一个请求。
收到第一条命令时,IntentService 启动,触发一个后台线程,然后将命令放入一个队列。
随后,IntentService 按顺序执行每一条命令,并针对每一条命令在后台线程上调用 onHandleIntent(Intent) 方法。新进命令总是放置在队尾。最后,执行完队列中的全部命令后,服务也随即停止并被销毁。
二、使用 AlarmManager 延迟运行服务
AlarmManager ,是Android中常用的一种系统级别的提醒服务。在特定的时刻,为我们广播一个指定的 Intent。就是一个闹钟,定时执行一些功能。
具体的解释和使用请看以下帖子(不是我写的):
https://www.jianshu.com/p/3c6a71b55c72
另外,启动定时器需要考虑性能问题,可能会比较耗电,要根据自己的需求来,该关的时候关掉。
三、服务之细节
对于大多数服务任务,推荐使用 IntentService。但是 IntentService 并不一定适合所有架构。
1、服务的生命周期
如果是 startService(Intent) 方法启动的服务,其生命周期很简单,并有三种生命周期回调方法:
- onCreate(...) 方法:服务创建时调用
- onStartCommand(Intent,int,int) 方法:每次组件通过 startService(Intent) 方法启动服务时调用一次。它有两个整数参数,一个是标识符集,一个是启动ID。标识符集用来表示当前 intent 发送究竟是一次重新发送,还是一次从没成功过的发送。每次调用 onStartCommand(Intent,int,int) 方法,启动 ID 都会不同。因此,启动 ID 也可用于区分不同的命令。
- onDestroy()方法:服务不再需要时调用
服务停止时会调用 onDestroy() 方法。服务停止的方式取决于服务的类型。服务的类型由 onStartCommand(...) 方法的返回值确定,可能的服务类型有 Service.START_NOT_STICKY、START_REDELIVER_INTENT 和 START_STICKY。
(1)non-sticky 服务
IntentService 是一种 non-sticky 服务。non-sticky 服务在服务自己认为已完成任务时停止。为获得 non-sticky 服务,应返回START_NOT_STICKY 或 START_REDELIVER_INTENT。
通过调用 stopSelf() 或 stopSelf(int) 方法,我们告诉Android任务已完成。stopSelf() 是个无条件方法。不管onStartCommand(...) 方法调用多少次,该方法总是会成功停止服务。stopSelf(int) 是个有条件的方法。该方法需要来自于onStartCommand(...) 方法的启动 ID 。只有在接收到最新启动 ID 后,该方法才会停止服务(这也是 IntentService 的后台工作原理)。
返回 START_NOT_STICKY 和 START_REDELIVER_INTENT 有什么不同呢?区别就在于,如果系统需要在服务完成任务之前关闭它,则服务的具体表现会有所不同。START_NOT_STICKY 型服务说没就没了;而 START_REDELIVER_INTENT 型服务会在资源不再吃紧时,尝试再次启动服务。
选择 START_NOT_STICKY 还是 START_REDELIVER_INTENT,这要看服务对应用有多重要了。如果不重要,就选择START_NOT_STICKY,同时,它也是 IntentService 的默认行为。如有需要,我们也可调用 IntentService.setIntentRedelivery(true) 方法,改用START_REDELIVER_INTENT。
(2)sticky 服务
sticky 服务会持续运行,直到外部组件调用 Context.stopService(Intent) 方法让它停止。为获得 sticky 服务,应返回START_STICKY。如因某种原因需终止服务,可传入一个 null intent 给 onStartCommand(...) 方法重启服务。
sticky 服务适用于长时间运行的服务,如音乐播放器这种启动后一直运行,直到用户主动停止的服务。即使是这样,也应考虑一种使用 non-sticky 服务的替代架构方案。sticky 服务的管理很不方便,因为很难判断服务是否已启动。
2、绑定服务
除以上各类服务外,也可使用 bindService(Intent,ServiceConnection, int) 方法绑定一个服务,以此获得直接调用绑定服务方法的能力。
对服务来说,绑定引入了另外两个生命周期回调方法:
- onBind(Intent)方法:每次绑定服务时调用,返回来自 ServiceConnection.onServiceConnected(ComponentName,IBinder)方法的 IBinder 对象。
- onUnbind(Intent)方法:服务绑定终止时调用。
(1)本地服务绑定
我们并不推荐此种模式。服务是种高效的单例,与仅使用一个单例相比,使用此种模式显现不出优势。
(2)远程服务绑定
绑定更适用于远程服务,因为它们赋予了其他进程中应用调用服务方法的能力。
四、JobScheduler 和 JobService
在 Lollipop(API 21)系统中,为更好地实现后台服务,Android 引入了一个叫 JobScheduler 的全新API。除了能用来处理各类任务外,JobScheduler 还能做到更多:发现没有网络时,它能禁止服务启动;如果请求失败或网络连接受限,它能提供稍后重试机制;它能控制只在设备充电的时候,才允许联网检查是否有新图片。虽然上述某些场景使用 AlarmManager 和 IntentService也能实现,但是很麻烦。
五、Sync adapter
还可以使用 sync adapter 创建常规的 polling 网络服务。和前面看到过的 adapter 不一样,sync adapter 主要用于从某个数据源同步数据(上传、下载或既上传又下载)。不像 JobScheduler,sync adapter 早就有了,所以不用担心新旧系统版本问题。
和 JobScheduler 一样,sync adapter 可代替 PhotoGallery 应用中的 AlarmManger。不同应用中的同步功能都是默认一起执行的。而且,即使设备重启,也不用重置同步定时器,因为 sync adapter 会自动处理。
sync adapter 还能和操作系统完美整合。你可以设置一个可同步账户,对外暴露应用。然后,用户通过 Settings → Accounts菜单来管理应用的同步。当然,这个菜单还可以用来管理其他使用 sync adapter 的应用账户,如Google自己的一些应用套件。
虽然使用 sync adapter 既能让周期性的网络任务变得容易可靠,又能让开发者免于编写定时器管理和 pending intent 的相关代码,但仍然免不了写另外一些代码。首先,需要与网络请求相关的代码。其次,要有一个 content provider 实现来封装数据、账户和授权类,用以代表远程服务器的某个账户(即使远程服务器不需要授权),以及一个 sync adapter 和 sync service 的实现。另外,还要懂得运用绑定服务。
所以,如果应用已经使用 content provider 作为数据层,并且需要账号授权,那使用 sync adapter 是最理想的。此外,相较于 JobScheduler,sync adapter 还有同操作系统提供的用户界面自然整合的优势。考虑到这些因素,即使要写一大堆代码,某些场景下,仍然值得使用 sync adapter。
第二十九章 broadcast intent
broadcast intent 的工作原理类似之前学过的 intent,唯一不同的是 broadcast intent 可同时被多个叫作 broadcast receiver 的组件接收。
一、接收系统 broadcast:重启后唤醒
如果用户重启了设备,定时器就会失效。设备重启后,那些持续运行的应用通常也需要重启。
监听带有 BOOT_COMPLETED 操作的 broadcast intent,可知道设备是否已完成启动。无论何时,只要打开设备,系统就会发送一个 BOOT_COMPLETED broadcast intent。要想监听它,可以创建并登记一个 standalone broadcast receiver。
1、创建并登记 standalone receiver
standalone receiver 是一个在 manifest 配置文件中声明的 broadcast receiver。即便应用进程已消亡,standalone receiver 也可以被激活。此外,还有可以同 fragment 或 activity 的生命周期绑定的 dynamic receiver。
与服务和 activity 一样,broadcast receiver 必须在系统中登记后才能用。如果不登记,系统就不知道该向哪里发送 intent。登记响应隐式 intent 的 standalone receiver 和登记服务或 activity 差不多。我们使用 receiver 标签并在其中包含相应的 intent-filter。StartupReceiver 会监听 BOOT_COMPLETED 操作,而该操作也需要配置使用权限。因此,还需要添加一个相应的 uses-permission 标签:
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
...>
<receiver android:name=".StartupReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
</application>
在配置文件中完成声明后,即使应用还没运行,只要有匹配的 broadcast intent 发来,broadcast receiver 就会醒来接收。一收到 intent,broadcast receiver 的 onReceive(Context, Intent) 方法即开始运行,随后会被销毁。
2、使用 receiver
broadcast receiver 的生命非常短暂,因而难以有所作为。例如,我们无法使用任何异步 API 或登记任何监听器,因为一旦onReceive(Context, Intent) 方法运行完,receiver 就不存在了。onReceive(Context, Intent) 方法同样运行在主线程上,因此不能在该方法内做一些费时费力的事情,如网络连接或数据的永久存储等。
然而,这并不代表 receiver 一无用处。一些便利型任务就非常适合它,比如启动 activity 或服务(不需要等返回结果),以及系统重启后重置定时运行的定时器。
二、过滤前台通知信息
1、发送 broadcast intent
要发送 broadcast intent,只需创建一个intent,并传入 sendBroadcast(Intent) 方法即可。
2、动态注册 receiver 时,要记得事后清理。通常,如果在生命周期启动方法中登记了一个 receiver ,就应在相应的停止方法中调用 Context.unregisterReceiver(BroadcastReceiver) 方法。这里,我们在 onStart() 方法里登记,在 onStop() 方法里撤销登记。同样,如果在 onCreate(...) 方法里登记,就应在 onDestroy() 里撤销登记。
顺便要说的是,我们应注意在保留 fragment 中的 onCreate(...) 和 onDestroy() 方法的使用。设备旋转时,onCreate(...) 和onDestroy() 方法中的 getActivity() 方法会返回不同的值。因此,如果想在 Fragment.onCreate(Bundle) 和 Fragment.onDestroy()方法中实现登记或撤销登记,应改用 getActivity().getApplicationContext() 方法。
3、使用私有权限限制 broadcast
使用动态 broadcast receiver 存在一个问题,即系统中的任何应用均可监听并触发你的 receiver。不要担心,有多种方式可以阻止未授权应用闯入你的私人领域。
一种办法是在 manifest 配置文件里给 receiver 标签添加一个 android:exported="false" 属性,声明它仅限应用内部使用。这样,系统中的其他应用就再也无法接触到该 receiver 了。
另外,也可创建自己的使用权限。这需要在 AndroidManifest.xml 中添加一个 permission 标签:
<permission android:name="com.bignerdranch.android.photogallery.PRIVATE"
android:protectionLevel="signature" />
<uses-permission android:name="com.bignerdranch.android.photogallery.PRIVATE" />
以上代码中,你使用 protection level 签名定义了自己的定制权限。如同前面用过的 intent 操作、类别和系统权限,权限本身只是一行简单的字符串。即使是自定义的权限,也必须在使用这个权限前获取它,这是规则。
要使用权限,需将其作为参数传入 sendBroadcast(...) 方法。有了这个权限,所有应用都必须使用同样的权限才能接收你发送的 intent:
sendBroadcast(new Intent(ACTION_SHOW_NOTIFICATION),
"com.bignerdranch.android.photogallery.PRIVATE");
该如何保护你的 broadcast receiver 呢?其他应用可通过创建自己的 broadcast intent 来触发它。同样,在registerReceiver(...) 方法中传入自定义权限就能解决该问题:
getActivity().registerReceiver(mOnShowNotification, filter,
"com.bignerdranch.android.photogallery.PRIVATE",null);
(1)深入学习 protection level
自定义权限必须指定 android:protectionLevel 属性值。Android 根据 protectionLevel 属性值确定自定义权限的使用方式。
4、使用有序 broadcast 收发数据
因而,不可能指望它们按照某种顺序依次运行,也不知道它们什么时候全部结束运行。结果就是,无论是broadcast receiver 之间要通信,还是 intent 发送者要从 receiver 接收反馈信息,处理起来都很困难。
不过,我们可以使用有序 broadcast intent 来实现双向通信。有序 broadcast 允许多个 broadcast receiver 依序处理 broadcast intent。另外,通过传入一个名为 response receiver 的特别 broadcast receiver,有序 broadcast 还支持让broadcast 发送者接收 broadcast 接收者的返回结果。
从接收方来看,这看上去与一般 broadcast 没什么不同。然而,我们却因此获得了特别的工具:一套改变 receiver 返回值的方法。若是我们需要做一些简单的操作,比如取消通知消息,这很简单,使用一个简单的整形结果码,将此要求告诉信息发送者就可以了。使用 setResultCode( int ) 方法:
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
//Activity.RESULT_CANCELED 就是个int值
setResultCode(Activity.RESULT_CANCELED);
}
};
如需设置所有三个参数值,那就调用 setResult( int, String, Bundle ) 方法。设定返回值后,每个后续接收者均可看到或修改它们。
为了让以上方法发挥作用,broadcast 必须有序。编写一个可发送有序 broadcast 的新方法:
private void showBackgroundNotification(int requestCode, Notification notification) {
Intent i = new Intent(ACTION_SHOW_NOTIFICATION);
i.putExtra(REQUEST_CODE, requestCode);
i.putExtra(NOTIFICATION, notification);
//发送有序的broadcast
sendOrderedBroadcast(i, PERM_PRIVATE, null, null, Activity.RESULT_OK, null, null);
}
除了在 sendBroadcast(Intent,String) 方法中使用的两个参数外,Context.sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle) 方法还有另外五个参数,依次为:一个 result receiver,一个支持 result receiver 运行的 Handler,结果代码初始值,结果数据以及有序 broadcast 的结果附加内容。
result receiver 比较特殊,只有在所有有序 broadcast intent 接收者结束运行后,它才开始运行。
三、处理WebView的设备旋转问题
旋转屏幕的时候,WebView会重新加载网页。这是因为WebView包含太多的数据,无法在onSaveInstanceState(......)方法内全部保存。所以每次设备旋转,它都必须从头开始加载网页数据。
对于一些类似的类(如VideoView),Android文档推荐让activity自己处理设备配置变更。也就是说,无需销毁重建activity,就能直接调整自己的视图以适应新的屏幕尺寸。这样,WebView也就不必重新加载全部数据了。为了让Activity自己处理配置调整,可在AndroidManifest.xml配置文件中做如下调整:
<manifest ... >
...
<activity
android:name=".PhotoPageActivity"
android:configChanges="keyboardHidden|orientation|screenSize" />
...
</manifest>
android:configChanges属性表明,如果因键盘开或关、屏幕方向改变、屏幕大小改变(也包括Android 3.2之后的屏幕方向变化)而发生设备配置更改,那么activity应自己处理配置更改。
自己处理配置更改的风险:首先,资源修饰符无法自动工作了。开发人员必须手工重载视图。这实际是非常棘手的。其次,也是更重要的一点,既然activity自己处理配置更改了,你很可能不会去覆盖Activity. onSavedInstanceState(...)方法存储UI状态。然而,这依然是必需的,即使自己处理设备配置更改也是一样。因为低内存情况还是要考虑的。
四、使用后退键浏览历史网页
还可以在WebView中点击跳转到其他链接。然而,不管如何跳转,访问了多少个网页,只要按后退键,就会立即回到Activity。如果想使用后退键在WebView里层层退回到已浏览的历史网页呢?
首先覆盖后退键方法Activity.onBackPressed()。在该方法内,再搭配使用WebView的历史记录浏览方法(WebView.canGoBack()和WebView.goBack())实现想要的浏览逻辑。如果WebView里有历史浏览记录,就回到前一个历史网页,否则调用super.onBackPressed()方法回到Activity。