陈小云Android学习之旅:第一章 四大组件 之 Broadcast Receiver(四)

第一章 四大组件

第四组件 Broadcast Receiver

(1)定义

广播,是一个全局的监听器,属于Android四大组件之一,Android 广播分为两个角色:广播发送者、广播接收者

(2)作用

监听 / 接收 应用 App 发出的广播消息,并 做出响应

(3)应用场景

3.1)Android不同组件间的通信(含 :应用内 / 不同应用之间)
3.2)多线程通信
3.3)与 Android 系统在特定情况下的通信。如:电话呼入时、网络可用时

(4)实现原理

Android中的广播使用了设计模式中的观察者模式:基于消息的发布 / 订阅事件模型
Android将广播的发送者 和 接收者 解耦,使得系统方便集成,更易扩展
模型中有3个角色:
1、消息订阅者(广播接收者)
2、消息发布者(广播发布者)
3、消息中心(AMS,即Activity Manager Service)
陈小云Android学习之旅:第一章 四大组件 之 Broadcast Receiver(四)

(5)使用流程:

陈小云Android学习之旅:第一章 四大组件 之 Broadcast Receiver(四)

5.1 自定义广播接受者BroadcastReceiver

继承BroadcastReceivre基类,必须复写抽象方法onReceive()方法
1.广播接收器接收到相应广播后,会自动回调 onReceive() 方法
2.一般情况下,onReceive方法会涉及 与 其他组件之间的交互,如发送Notification、启动Service等
3.默认情况下,广播接收器运行在 UI 线程,因此,onReceive()方法不能执行耗时操作,否则将导致ANR

// 继承BroadcastReceivre基类
public class mBroadcastReceiver extends BroadcastReceiver {
    // 复写onReceive()方法
    // 接收到广播后,则自动调用该方法
    @Override
    public void onReceive(Context context, Intent intent) {
        //写入接收广播后的操作
    }
}

5.2 广播接收器注册

注册的方式分为两种:静态注册、动态注册

(1)静态注册

使用:在AndroidManifest.xml里通过标签声明
特点:常驻、不受任何组件的生命周期影响(应用程序关闭后,如果有信息广播,程序依旧会被系统调用),耗电,占内存
应用场景:需要时刻监听广播

<receiver
    android:enabled=["true" | "false"]
//此broadcastReceiver能否接收其他App的发出的广播
    //默认值是由receiver中有无intent-filter决定的:如果有intent-filter,默认值为true,否则为false
    android:exported=["true" | "false"]
    android:icon="drawable resource"
    android:label="string resource"
    //继承BroadcastReceiver子类的类名
    android:name=".mBroadcastReceiver"
    //具有相应权限的广播发送者发送的广播才能被此BroadcastReceiver所接收;
    android:permission="string"
    //BroadcastReceiver运行所处的进程
    //默认为app的进程,可以指定独立的进程
    //注:Android四大基本组件都可以通过此属性指定自己的独立进程
    android:process="string" >

    //用于指定此广播接收器将接收的广播类型
// IntentFilter翻译成中文就是“意图过滤器”,主要用来过滤隐式意图。当用户进行一项操作的时候,Android系统会根据配置的 “意图过滤器” 来寻找可以响应该操作的组件,服务。
    //本示例中给出的是用于接收网络状态改变时发出的广播
    <intent-filter>
        <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
    </intent-filter>
</receiver>

当此 App首次启动时,系统会自动实例化mBroadcastReceiver类,并注册到系统中。

(2)动态注册

使用:在代码中调用Context.registerReceiver()方法
特点:非常驻、灵活、跟随组件的生命周期变化(组件结束,广播结束。故在组件结束前,必须移除广播接收器)
应用场景
2.1)源码

    // 选择在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);
    }
}

2.2)动态广播最好在Activity 的 onResume()注册、onPause()注销。

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

5.3 广播发送者向AMS发送广播

(1)广播的发送

广播 是 用”意图(Intent)“标识,定义广播的本质 = 定义广播所具备的“意图(Intent)”
广播发送 = 广播发送者 将此广播的“意图(Intent)”通过sendBroadcast()方法发送出去

(2)广播类型
a)普通广播(Normal Broadcast)

开发者自身定义 intent的广播(最常用)。
1、发送者发送自定义广播

Intent intent = new Intent();
//对应BroadcastReceiver中intentFilter的action
intent.setAction(BROADCAST_ACTION);
//发送广播
sendBroadcast(intent);

2、接受者注册广播

    <receiver
    //此广播接收者类是mBroadcastReceiver
    android:name=".mBroadcastReceiver" >
    //用于接收网络状态改变时发出的广播
    <intent-filter>
        <action android:name="BROADCAST_ACTION" />
    </intent-filter>
</receiver>

若被注册了的广播接收者中注册时intentFilter的action与上述匹配,则会接收此广播(即进行回调onReceive())。

b)系统广播(System Broadcast)

Android中内置了多个系统广播:只要涉及到手机的基本操作(如开机、网络状态变化、拍照等等),都会发出相应的广播
每个广播都有特定的Intent - Filter(包括具体的action),Android系统广播action如下:(部分)

系统操作 Action
监听网络变化 android.net.conn.CONNECTIVITY_CHANGE
电池电量低 Intent.ACTION_BATTERY_LOW
屏幕锁屏 Intent.ACTION_CLOSE_SYSTEM_DIALOGS
屏幕被关闭 Intent.ACTION_SCREEN_OFF
屏幕被打开 Intent.ACTION_SCREEN_ON

注:当使用系统广播时,只需要在注册广播接收者时定义相关的action即可,并不需要手动发送广播,当系统有相关操作时会自动进行系统广播

c)有序广播(Ordered Broadcast)

发送出去的广播被广播接收者按照先后顺序接收(按照Priority属性值从大-小排序)

sendOrderedBroadcast(intent);
d)粘性广播(Sticky Broadcast)失效
e)App应用内广播(Local Broadcast)

Android中的广播可以跨App直接通信(exported对于有intent-filter情况下默认值为true),App应用内广播可理解为一种局部广播,广播的发送者和接收者都同属于一个App。
(1) 将全局广播设置为局部广播
(a)注册广播时将exported属性设置为false,使得非本App内部发出的此广播不被接收;
(b)在广播发送和接收时,增设相应权限permission,用于权限验证;
(c)发送广播时指定该广播接收器所在的包名,此广播将只会发送到此包中的App内与之相匹配的有效广播接收器中。(通过intent.setPackage(packageName)指定报名)
(2) 使用封装好的LocalBroadcastManager类
使用方式上与全局广播几乎相同,只是注册/取消注册广播接收器和发送广播时将参数的context变成了LocalBroadcastManager的单一实例

//注册应用内广播接收器
//步骤1:实例化BroadcastReceiver子类 & IntentFilter mBroadcastReceiver 
mBroadcastReceiver = new mBroadcastReceiver();
        IntentFilter intentFilter = new IntentFilter();
//步骤2:实例化LocalBroadcastManager的实例
        localBroadcastManager = LocalBroadcastManager.getInstance(this);
//步骤3:设置接收广播的类型 
        intentFilter.addAction(android.net.conn.CONNECTIVITY_CHANGE);
//步骤4:调用LocalBroadcastManager单一实例的registerReceiver()方法进行动态注册 
        localBroadcastManager.registerReceiver(mBroadcastReceiver, intentFilter);
//取消注册应用内广播接收器
        localBroadcastManager.unregisterReceiver(mBroadcastReceiver);
//发送应用内广播
        Intent intent = new Intent();
        intent.setAction(BROADCAST_ACTION);
        localBroadcastManager.sendBroadcast(intent);

注:对于不同注册方式的广播接收器回调OnReceive(Context context,Intent intent)中的context返回值是不一样的
(3) 本地广播与全局广播的差别
(3.1)定义
全局广播BroadcastReceiver是针对应用间、应用与系统间、应用内部进行通信的一种方式
本地广播LocalBroadcastReceiver仅在自己的应用内发送接收广播,也就是只有自己的应用能收到,数据更加安全广播只在这个程序里,而且效率更高。
(3.2)使用
全局广播:
1)制作intent(可以携带参数)
2)使用sendBroadcast()传入intent;
3)制作广播接收器类继承BroadcastReceiver重写onReceive方法(或者可以匿名内部类啥的)
4)在java中(动态注册)或者直接在Manifest中注册广播接收器(静态注册)使用registerReceiver()传入接收器和intentFilter
5)取消注册可以在OnDestroy()函数中,unregisterReceiver()传入接收器
本地广播:
1)LocalBroadcastReceiver不能静态注册,只能采用动态注册的方式。
2)在发送和注册的时候采用,LocalBroadcastManager的sendBroadcast方法和registerReceiver方法

补充:Context

(一)Context是什么?

1.1)广义理解

Context:语境,上下文,可看做用户与操作系统操作的一个场景;Context在加载资源、启动Activity、获取系统服务、创建View等操作都要参与。

1.2)官方注释

Interface to global information about an application environment. This is an abstract class whose implementation is provided by the Android system. It allows access to application-specific resources and classes, as well as up-calls for application-level operations such as launching activities,broadcasting and receiving intents, etc.
Context提供了关于应用环境全局信息的接口。它是一个抽象类,它的执行被Android系统所提供。它允许获取以应用为特征的资源和类型,是一个统领一些资源(应用程序环境变量等)的上下文。就是说,它描述一个应用程序环境的信息(即上下文);是一个抽象类,Android提供了该抽象类的具体实现类;通过它我们可以获取应用程序的资源和类(包括应用级别操作,如启动Activity,发广播,接受Intent等)。

1.3)类关系

陈小云Android学习之旅:第一章 四大组件 之 Broadcast Receiver(四)
Context类本身是一个纯abstract类,它有两个具体的实现子类:ContextImpl和ContextWrapper。

(1)ContextWrapper类

如其名所言,这只是一个包装而已,ContextWrapper构造函数中必须包含一个真正的Context引用,同时ContextWrapper中提供了attachBaseContext()用于给ContextWrapper对象中指定真正的Context对象,调用ContextWrapper的方法都会被转向其所包含的真正的Context对象。

(2)ContextThemeWrapper类

如其名所言,其内部包含了与主题(Theme)相关的接口,这里所说的主题就是指在AndroidManifest.xml中通过android:theme为Application元素或者Activity元素指定的主题。当然,只有Activity才需要主题,Service是不需要主题的,因为Service是没有界面的后台场景,所以Service直接继承于ContextWrapper,Application同理。

(3)ContextImpl类

则真正实现了Context中的所以函数,应用程序中所调用的各种Context类的方法,其实现均来自于该类。
一句话总结:Context的两个子类分工明确,其中ContextImpl是Context的具体实现类,ContextWrapper是Context的包装类。Activity,Application,Service虽都继承自ContextWrapper(Activity继承自ContextWrapper的子类ContextThemeWrapper),但它们初始化的过程中都会创建ContextImpl对象,由ContextImpl实现Context中的方法。
面试题:一个应用程序有几个Context?
从上面的关系图我们已经可以得出答案了,在应用程序中Context的具体实现子类就是:Activity,Service,Application。那么
Context数量=Activity数量+Service数量+1。
我们常说四大组件,这里怎么只有Activity,Service持有Context,那Broadcast Receiver,Content Provider呢?Broadcast Receiver,Content Provider并不是Context的子类,他们所持有的Context都是其他地方传过去的,所以并不计入Context总数。

(二)Context能做什么?

弹出Toast、启动Activity、启动Service、发送广播、操作数据库等等都需要用到Context。

TextView tv = new TextView(getContext());
ListAdapter adapter = new SimpleCursorAdapter(getApplicationContext(), ...);
AudioManager am = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);getApplicationContext().getSharedPreferences(name, mode);
getApplicationContext().getContentResolver().query(uri, ...);
getContext().getResources().getDisplayMetrics().widthPixels * 5 / 8;
getContext().startActivity(intent);
getContext().startService(intent);
getContext().sendBroadcast(intent);

(三)Context作用域

由于Context的具体实例是由ContextImpl类去实现的,因此在绝大多数场景下,Activity、Service和Application这三种类型的Context都是可以通用的。
不过有几种场景比较特殊,比如启动Activity,还有弹出Dialog。出于安全原因的考虑,Android是不允许Activity或Dialog凭空出现的,一个Activity的启动必须要建立在另一个Activity的基础之上,也就是以此形成的返回栈。而Dialog则必须在一个Activity上面弹出(除非是System Alert类型的Dialog),因此在这种场景下,我们只能使用Activity类型的Context,否则将会出错。
陈小云Android学习之旅:第一章 四大组件 之 Broadcast Receiver(四)
(1)从上图我们可以发现Activity所持有的Context的作用域最广,无所不能。因为Activity继承自ContextThemeWrapper,而Application和Service继承自ContextWrapper,很显然ContextThemeWrapper在ContextWrapper的基础上又做了一些操作使得Activity变得更强大
(2)如果我们用ApplicationContext去启动一个LaunchMode为standard的Activity的时候会报错android.util.AndroidRuntimeException: Calling startActivity from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?这是因为非Activity类型的Context并没有所谓的任务栈,所以待启动的Activity就找不到栈了。解决这个问题的方法就是为待启动的Activity指定FLAG_ACTIVITY_NEW_TASK标记位,这样启动的时候就为它创建一个新的任务栈,而此时Activity是以singleTask模式启动的。所有这种用Application启动Activity的方式不推荐使用,Service同Application。
(3)在Application和Service中去layout inflate也是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。所以这种方式也不推荐使用。
凡是跟UI相关的,都应该使用Activity做为Context来处理;其他的一些操作,Service,Activity,Application等实例都可以,当然了,注意Context引用的持有,防止内存泄漏。

(四)Context的获取方式

4.1)View.getContext,返回当前View对象的Context对象,通常是当前正在展示的Activity对象。
4.2)Activity.getApplicationContext,获取当前Activity所在的(应用)进程的Context对象,通常我们使用Context对象时,要优先考虑这个全局的进程Context。
4.3)ContextWrapper.getBaseContext():用来获取一个ContextWrapper进行装饰之前的Context,可以使用这个方法,这个方法在实际开发中使用并不多,也不建议使用。
4.4)Activity.this 返回当前的Activity实例,如果是UI控件需要使用Activity作为Context对象,但是默认的Toast实际上使用ApplicationContext也可以。

(五)Context内存泄露

5.1)内存泄露情况

1、错误的单例模式

这是一个非线程安全的单例模式,instance作为静态对象,其生命周期要长于普通的对象,其中也包含Activity,假如Activity A去getInstance获得instance对象,传入this,常驻内存的Singleton保存了你传入的Activity A对象,并一直持有,即使Activity被销毁掉,但因为它的引用还存在于一个Singleton中,就不可能被GC掉,这样就导致了内存泄漏。

public class Singleton {
    private static Singleton instance;
    private Context mContext;

    private Singleton(Context context) {
        this.mContext = context;
    }

    public static Singleton getInstance(Context context) {
        if (instance == null) {
            instance = new Singleton(context);
        }
        return instance;
    }
}
2、View持有Activity引用

有一个静态的Drawable对象当ImageView设置这个Drawable时,ImageView保存了mDrawable的引用,而ImageView传入的this是MainActivity的mContext,因为被static修饰的mDrawable是常驻内存的,MainActivity是它的间接引用,MainActivity被销毁时,也不能被GC掉,所以造成内存泄漏。

public class MainActivity extends Activity {
    private static Drawable mDrawable;

    @Override
    protected void onCreate(Bundle saveInstanceState) {
        super.onCreate(saveInstanceState);
        setContentView(R.layout.activity_main);
        ImageView iv = new ImageView(this);
        mDrawable = getResources().getDrawable(R.drawable.ic_launcher);
        iv.setImageDrawable(mDrawable);
    }
}

5.2)正确使用Context

一般Context造成的内存泄漏,几乎都是当Context销毁的时候,却因为被引用导致销毁失败,而Application的Context对象可以理解为随着进程存在的,所以我们总结出使用Context的正确姿势:
1:当Application的Context能搞定的情况下,并且生命周期长的对象,优先使用Application的Context。
2:不要让生命周期长于Activity的对象持有到Activity的引用。
3:尽量不要在Activity中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用,如果使用静态内部类,将外部实例引用作为弱引用持有。