0%

Broadcast

基本原理

广播的使用包括注册发送两步。注册广播又分为两种方式

  • 在代码中进行动态注册
  • 在XML文件中进行静态注册。

不管哪种方式都涉及两个类 BroadcastReceiver 与 IntentFilter。前者负责处理广播,后者负责匹配广播

因为广播易造成内存泄漏,一般在活动的onResumeonPause方法中成对的进行注册和销毁。

2.发送广播本质上是一个后台操作,发送广播的类型包括

  • sendBroadcast 发送无序广播,异步执行,效率高,但存在隐患
  • sendOrderedBroadcast 发送有序广播,在某个接收器执行的同时会阻塞其他的接收器

虽然执行广播的进程是一个优先级较高的前台进程,但BroadcastReceiver 对象的生命周期只在 onReceive方法的执行过程中,一旦执行完,对象将销毁。这一特性决定了如果在广播接收器内部执行异步操作,将无法返回。

局部广播

LocalBroadcastManager局部广播不需要跨进程,并非使用Binder机制。

粘性广播

粘性广播在21中被deprecated 了,使用粘性广播首先需要权限

1
android.permission.BROADCAST_STICKYXML

使用方法sendStickyBroadcast方法来发送粘性广播

1
2
3
Intent intent = new Intent("receiver");
intent.putExtra("DATA","receiver");
sendStickyBroadcast(intent);

它的行为和正常广播基本一致,在广播接收器取消注册后不会受理广播。唯一有区别的一点是广播接收器取消注册后,如果发送粘性广播,则Intent将被会缓存到系统中。这样再次注册广播接收器时,能从该方法返回值中获取先前的Intent即其中的数据。

1
2
3
Intent mIntentSticky = registerReceiver(receiver,filter);
-------------------------
//s = "receiver"String s = mIntentSticky .getStringExtra("DATA");

因此发送粘性广播即便不能被成功接受,也可以保存数据,可见粘性广播的好处是使得广播能够在注册周期之外感知数据变化,但损失的是安全性,这些数据可以被任意获取与修改。

最佳实践

使用广播必须要手动注册接收机,可以优化的地方有两点

1.注册和解除配对出现,一般放在 onStart 和 onStop 方法中,其目的是防止内存泄漏。

2.视广播如何处理决定是否在注册时禁用(enable),以避免无谓的耗电。

首先禁止广播

1
2
3
4
5
<receiver
android:name=".MyReceiver"
android:enabled="false"
android:exported="true">
</receiver>

在必要时开启,并及时释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PackageManager packageManager = getPackageManager();
ComponentName componentName = new ComponentName(this, MyReceiver.class);
@Override
protected void onStart() {
super.onStart();
registerReceiver(receiver, filter);
packageManager.setComponentEnabledSetting(componentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP);
}
@Override
protected void onStop() {
super.onStop();
unregisterReceiver(receiver);
packageManager.setComponentEnabledSetting(componentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);

广播注册原理

在 Android 的广播机制中, ActivityManagerService 扮演着广播中心的角色,负责系统中所有广播的注册和发布操作,因此,Android应用程序注册广播接收器的过程就是把广播接收器注册到ActivityManagerService的过程

广播注册实际由ContextImpl的如下方法执行(已作精简)

1
2
3
4
private Intent registerReceiverInternal(BroadcastReceiver receiver, ..., Context context) {
IIntentReceiver rd = new LoadedApk.ReceiverDispatcher(receiver, context, scheduler, null, true).getIIntentReceiver();
return ActivityManagerNative.getDefault().registerReceiver(...);
}

该方法首先构建 IIntentReceiver 对象接口 ,这是一个单向的 Binder 对象,专门负责执行接收广播,定义如下

1
2
3
oneway interface IIntentReceiver {
void performReceive(in Intent intent, int resultCode, String data,in Bundle extras, boolean ordered, boolean sticky, int sendingUser);
}

而后使用ActivityManagerService类在服务端进行真实的广播注册


在研究AMS如何注册广播之前,先做些预备工作

1.ActivityManagerService维持着一个广播过滤器集合

1
HashMap<IBinder, ReceiverList> mRegisteredReceivers

其中key为 IIntentReceivers 对象,而Value为 ReceiverList ,代表一个注册了若干个广播的广播接收机,定义如下

1
2
3
4
class ReceiverList extends ArrayList<BroadcastFilter>{	
IIntentReceiver receiver;
BroadcastRecord curBroadcast = null;
}

可见 ReceiverList 是一个集合类,元素为广播过滤器 BroadcastFilter 。该类是 IntentFilter 的子类。

2.IntentResolver类负责操作广播过滤器,其中有一个重要的方法是判断两个 IntentFilter 对象是否相等。其实现原理是是依次比较ActionCategory以及Data是否相等,其中Data的比较又分为多个部分。


AMS 中注册广播的方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Intent registerReceiver(IIntentReceiver receiver, IntentFilter filter, String permission, int userId) { 
//1,收集粘性广播列表
//2,获取匹配过滤器的所有粘性广播
//3. 是否使用粘性广播
Intent sticky = allSticky != null ? allSticky.get(0) : null;
//4,广播注册
synchronized (this) {
ReceiverList rl = mRegisteredReceivers.get(receiver.asBinder());
if (rl == null) {
rl = new ReceiverList(this, callerApp, callingPid, callingUid,userId, receiver);
mRegisteredReceivers.put(receiver.asBinder(), rl);
}
BroadcastFilter bf = new BroadcastFilter(filter, rl, callerPackage,permission, callingUid, userId);
rl.add(bf);
mReceiverResolver.addFilter(bf);
}
return sticky;
}

注册广播的大部分代码是在处理粘性广播,对此只大略叙述

1.首先收集所有的粘性广播
2.而后收集能够经过过滤器的粘性广播集合
3.如果确实发送的是粘性广播,则返回代表最近的一条粘性广播的Intent

广播注册的过程是同步的,实际步骤是

1.先从广播过滤器缓存集合中查询是否存在传入的广播接收器,如果没有,则创建 ReceiverList ,并将其存入缓存
2.根据参数 IntentFilter 创建过滤器 BroadcastFilter ,并添加到系统解析器 mReceiverResolver中。

总结:广播有两个要素BroadcastReceiverIntentFilter。注册广播时前者生成Binder对象,定义了如何处理广播;后者生成一个新的过滤器。系统内存维持着一个字典集合,不考虑粘性广播,则广播的注册过程是将二者写入这个字典集合中去。

WakefulBroadcastReceiver

唤醒锁

在认识WakefulBroadcastReceiver广播之前先要了解唤醒锁。

安卓使用PowerManager服务来控制设备电源状态,设备的接口定义在IPowerManager接口中,可执行的方法如下

1
2
3
4
5
void goToSleep(long time)
void wakeUp(long time)
boolean isScreenOn()
void reboot(String reason)
void shutdown(boolean confirm, boolean wait)

其中最重要的方法是创建唤醒锁

1
WakeLock newWakeLock(int levelAndFlags, String tag)

WakeLock类即代表唤醒锁,是PowerManager的内部类,保持该锁会使得设备保持开启状态,无法进入休眠,必须等待锁的释放。

在上述方法中参数levelAndFlags表示锁的级别与类型,实际使用中应尽量不使用以及使用最低级别的锁

  • FULL_WAKE_LOCK
  • PARTIAL_WAKE_LOCK 该类型的所会使CPU保持运行,无视屏幕是否熄灭,即使按下电源键设备也不能进入休眠。
  • SCREEN_DIM_WAKE_LOCK 屏幕将一直保持较暗的亮度,但不会熄灭。按下电源键锁将释放
  • SCREEN_BRIGHT_WAKE_LOCK 同上

在创建唤醒锁之后使用如下方法启用

1
acquire()

释放唤醒锁

1
release()

使用唤醒锁可以保持屏幕长亮,但更轻量级的做法是对窗口对象使用属性android.view.WindowManager.LayoutParams#FLAG_KEEP_SCREEN_ON。

WifiLock

WakeLock类似的还有WifiLock,该锁保持Wi-Fi射频模块开启。正常模式下Wi-Fi射频模块会自动关闭以节省电量,在下载大文件时可以使用该锁。

1
WifiLock createWifiLock(int lockType, String tag)

lockType 的可能取值为

  • WIFI_MODE_FULL_HIGH_PERF 表示高性能连接,低丢包率,适合传输语音
  • WIFI_MODE_FULL
  • WIFI_MODE_SCAN_ONLY

WakefulBroadcastReceiver

WakefulBroadcastReceiver是一种利用唤醒锁的特殊广播,其目的是确保广播发射到启动服务的过程中,设备始终处于唤醒状态,不会因为进入休眠状态而中止启动服务。类内部保持了唤醒锁集合

1
SparseArray<PowerManager.WakeLock> mActiveWakeLocks;

该广播提供了一个工具方法startWakefulService来启动服务,其实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static ComponentName startWakefulService(Context context, Intent intent) {
synchronized (mActiveWakeLocks) {
int id = mNextId;
mNextId++;
if (mNextId <= 0) {
mNextId = 1;
}
intent.putExtra(EXTRA_WAKE_LOCK_ID, id);
ComponentName comp = context.startService(intent);
PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
"wake:" + comp.flattenToShortString());
wl.setReferenceCounted(false);
wl.acquire(60*1000);
mActiveWakeLocks.put(id, wl);
return comp;
}
}

可见在启动服务的时候,将创建一个唤醒锁,并获得60s的唤醒时间,在此期间设备保持唤醒状态。

该广播还提供了completeWakefulIntent方法以便在服务中释放唤醒锁,其实现如下

1
2
3
4
5
6
7
public static boolean completeWakefulIntent(Intent intent) {
final int id = intent.getIntExtra(EXTRA_WAKE_LOCK_ID, 0);
PowerManager.WakeLock wl = mActiveWakeLocks.get(id);
wl.release();
mActiveWakeLocks.remove(id);
return true;
}

小部件(AppWidgetProvider)

小部件是 APP 的简易入口,宿主APP与小部件处于不同的进程中,宿主通过广播(AppWidgetProvider)来更新小部件,小部件通过 PedentIndent 与宿主交互。

继承 AppWidgetProvider 类创建一个广播,并注册到清单文件

1
2
3
4
5
6
7
<receiver android:name="ExampleAppWidgetProvider" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/example_appwidget_info" />
</receiver>

有两点要注意

  • 必须制定特殊的 action,系统由此判定是小部件
  • 必须提供小部件的配置信息
1
2
3
4
5
6
7
8
9
10
11
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="40dp"
android:minHeight="40dp"
android:updatePeriodMillis="86400000"
android:previewImage="@drawable/preview" //
android:initialLayout="@layout/example_appwidget"
android:configure="com.example.android.ExampleAppWidgetConfigure"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen|keyguard"
android:initialKeyguardLayout="@layout/example_keyguard">
</appwidget-provider>

小部件信息的必要配置有4个,但小部件布局能够支持的布局和控件内是有限的,且要注意小部件的边距问题。

到此为止 ,小部件就已经建立起来了,但 没有任何功能。

AppWidgetProvider 类实际就是普通广播,仅仅对5个广播相关的事件进行了转发处理

  • ACTION_APPWIDGET_UPDATE: 小部件更新
  • ACTION_APPWIDGET_DELETED:删除每一个小部件
  • ACTION_APPWIDGET_ENABLED :发生在添加第一个小部件时
  • ACTION_APPWIDGET_DISABLED:发生在移除最后一个小部件时
  • ACTION_APPWIDGET_OPTIONS_CHANGED 小部件配置改变

更新小部件需要利用 AppWidgetManager 类,更具体的内容是操作 RemoteViews

1
2
3
4
5
6
7
8
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.update_main);
remoteViews.setImageViewResource(R.id.image, srcs[index]);
remoteViews.setOnClickPendingIntent(R.id.image, PendingIntent.getBroadcast(context, 1, new Intent("com.mowang.click"), PendingIntent.FLAG_UPDATE_CURRENT));
ComponentName componentName = new ComponentName(context, this.getClass());
appWidgetManager.updateAppWidget(componentName , remoteViews);
}