Android Context基础!

英文原文:Context, What Context?

译文Github地址:Context, What Context?

Context

Context 估计是 Android 开发中最常用的元素了,它的获取和使用如此普遍,加载资源,启动新的 Activity,获取系统服务,获取内部文件路径以及创建 View 都离不开 Context。同时,Context 也是最容易误操作的元素,以致于很容易把你带到坑里面去。下面就让我们全面对比了解一下 Context,让你的开发更得心应手。

Context 类型

不同类型的Context各异:根据 Android 组件的不同,获得到的 Context 也是不同的。

Application

Application 是存在于 app 进程的一个单例 Context 对象。在 Activity 或者 Service 中,可以使用 getApplication() 方法获取这个 Application 对象。除此之外,从他继承了 Context 的组件,都可以通过 getApplicationContext() 来获取到这个 Application 对象。不过,不论是通过什么方法获得,最后得到的 Application 对象都是同一个。

【译者注:这个单例 Context 对象在后文用 application context 指代】

Activity/Service

Activity/Service 继承自同一个基类 Context —— ContextWrapper,因此两者拥有相同的 Context API,但是具体任务还是通过将调用委托代理给实际的内部对象来完成。每当你创建一个新的 Activity 或者 Service 的时候,同时就会创建一个新的 ContextImpl,ContextImpl 就是最终处理所有 Context API 方法的内部对象。不同的Activity 或者 Service 的 Context 都是不一样的。

BroadcastReceiver

BroadcastReceiver 本身并不是一个 Context,但是 Android framework 会在每一个广播事件发生的时候,给相应 BroadcastReceiver 的 onReceive() 传递一个 Context,这个 ReceiverRestrictedContext 有个两个方法不可用: registerReceiver() 和 bindService()。BroadcastReceiver 每次处理 broadcast 的时候,传递给它的 Context 都是一个新的实例。

ContentProvider

ContentProvider 本身也不是 Context,但是调用 getContext() 可以获取一个 Context。如果调用者与 ContentProvider 运行在同一个进程内,那么这个返回的 Context 就是上文的 application context。如果调用者与 ContentProvider 不是运行在同一个进程之内,那么这个方法会返回一个指代 provider 所在包的新 Context 实例。

保存 Context 引用

第一个问题:当我们将一个 Context 的引用保存到一个存活时间比 Context 本身生命周期还长的对象时,问题就来了。比如,我们有一个单例对象,要求使用一个 Context 来执行资源加载或访问 ContentProvider,并且传入的是一个 Activity 或 Service 对象:

错误的 Singleton 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

public class CustomManager {
private static CustomManager sInstance;

public static CustomManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new CustomManager(context);
}

return sInstance;
}

private Context mContext;

private CustomManager(Context context) {
mContext = context;
}
}

我们知道,单例对象是一个静态变量,受其所在类的生命周期控制。这就意味着,在这个单例对象的存活时间内,这个单例持有的对象(引用)都不会被垃圾回收。
所以这种实现的问题就是你无法知道 Context 到底来自何处,如果这个 Context 是一个 Activity 或者 Service, 就会变得不安全:这个被持有的 Activity,以及其内部所有的 View 或者其他的耗内存对象都无法被回收,从而引发内存泄露。

为了防止这种问题,我们改为让单例持有 application context:

改进的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CustomManager {
private static CustomManager sInstance;

public static CustomManager getInstance(Context context) {
if (sInstance == null) {
//Always pass in the Application Context
sInstance = new CustomManager(context.getApplicationContext());
}

return sInstance;
}

private Context mContext;

private CustomManager(Context context) {
mContext = context;
}
}

如此以来,我们就不用再关心 Context 来自哪里,也不用关心 Context 是什么类型,因为最终持有的都会是 application context,因此就避免了内存泄露的问题。这个处理技巧在后台线程或者 Handler 处理中同样有效。

既然如此,是不是意味着我们可以在任何情况下都用 application context 来处理呢?这样就永远不用担心内存泄露了。答案显然是否定的,就如上文说的一样,不同情况下的 Context 各不相同。这就像葫芦娃一样,虽然都是葫芦娃,但是每个娃的技能都不一样。

Context 特点

Context 能实现哪些功能,主要还是取决于 Context 从何而来,下表列出了不同 Context 的一些不同点:

Application Activity Service ContentProvider BroadcastReceiver
Show a Dialog NO YES NO NO NO
Start an Activity NO1 YES NO1 NO1 NO1
Layout Inflation NO2 YES NO2 NO2 NO2
Start a Service YES YES YES YES YES
Bind to a Service YES YES YES YES NO
Send a Broadcast YES YES YES YES YES
Register BroadcastReceiver YES YES YES YES NO3
Load Resource Values YES YES YES YES YES
  1. application context 可以启动 Activity, 但是前提是需要创建一个新任务。在某些情况下,我们可以使用这种方式实现某种特殊目的,这种方式会创建一个非标准的回退栈,一般不推荐使用,至少不是一个好的实践。
  2. 这个是合法的调用,但是 inflation 获得的 View 只会应用系统的主题,而不是当前 app 的自定义主题。
  3. 在 4.2 及以上系统版本中, 允许注册 receiver 为 null 的广播监听,主要目的是为了获取 sticky broadcast 的当前值。

User Interface

从上面的列表可以看到,application context 不能胜任很多场景,而且都是与UI相关的情况。实际上,只有 Activity 拥有处理 UI 的能力,其他类型的 Context 在这方面都大同小异。

上面的三种行为,除了 Activity, 其他的 Context 也都无法处理,从而避免误用。试图显示一个使用 application context 创建的 Dialog,或者从 application context 启动一个Activity,都会导致 app 崩溃,系统通过这种方式告诉你:你用错了。

另一个问题就是 inflating layout。如果你读过 layout inflation一文, 你就会知道 inflating layout 有一些容易让人迷惑的地方,如何正确使用 Context 就是其中一个。如果你使用 application context 来进行 inflating layout,并不会发生任何错误,但是当前 app 所定义的 themes 以及 styles 都被忽略了。究其原因,正如你在 manifest 中定义的一样, 只有 Activity 才是唯一能够响应 themes 定义的组件。任何其他组件所含有的 Context 都会应用 Android 系统自身的主题,所以,最终的 UI 表现可能出乎你的意料。

规则冲突

可能有人会提出这样一种场景:在 app 的当前设计下,由于涉及到处理 UI 的操作,所以需要长期持有一个 Activity 的引用。如果是这样的话,我只能说:请重新考虑你的设计。

经验法则

简而言之,在组件的生命周期之内可以直接使用自身的 Context, 一旦需要在超出组件生命周期之外的对象中使用 Context ,就应该只用 application context,哪怕只是临时的引用,也是如此。

Support Annotations

查看原文

在Support Library 19.1以及以后的版本中,Android工具小组引入了几个很酷的注解类型,方便开发者在工程中使用,同时Support Library自身也使用了这些注解。

本文的代码都使用android studio完成。首先,添加注解支持:

compile 'com.android.support:support-annotations:22.1.1'

有三种类型的注解:

  1. NonNull & Nullable
  2. 资源Id
  3. IntDef & StringDef

Nullness注解

@NonNull 用来修饰不能为null的参数。在下面的代码例子中,我们有一个取值为 null 的 name 变量,它被作为参数传递给 sayHello 方法,而该方法要求这个参数是非null的String类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MainActivity extends ActionBarActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String name = null;
sayHello(name);
}

void sayHello(@NonNull String name) {
Toast.makeText(this, "Hello " + s, Toast.LENGTH_LONG).show();
}

}

由于代码中参数 name 被 @NonNull 注解修饰,因此 android studio 将会以警告的形式提醒我们这个地方有问题:

name_is_null_warning

如果我们给 name 赋值,例如

1
String name = “Our Lord Duarte”

那么警告将消失。

【注:这个我试了一下,在android studio里面,就算不用注解,也会有提示的,android studio 就是这么智能。】

@Nullable 用来修饰方法的参数或者返回值可能为 null。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MainActivity extends ActionBarActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toast.makeText(this, getName().toLowerCase(), Toast.LENGTH_LONG).show();
}

@Nullable
String getName() {
return "";
}

}

因为 getName 方法的返回值使用 @Nullable 修饰,所以 android studio 会提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    Method invocation "getName().toLowerCase()" may produce "java.lang.NollPointerException"

## 资源注解

资源类型注解可以帮助我们准确的使用资源id,例如,避免我们在要求colorId的地方错误的使用了dimenId。
在下面的代码中,我们的sayHello方法预期接受一个字符串类型的资源Id,并使用@StringRes注解修饰:

```java
public class MainActivity extends ActionBarActivity {

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

sayHello(R.style.AppTheme);
}


void sayHello(@StringRes int id) {
Toast.makeText(this, "Hello " + getString(id), Toast.LENGTH_LONG).show();
}

}

而我们传递给它的是一个样式资源Id,与预期的字符串资源Id不符合,这时IDE将提示警告如下:

wrong_resource_type_error

类似的,我们把警告的地方使用一个字符串资源Id代替警告就消失了:

1
sayHello(R.string.name);

基本上,每一种资源类型都有相应的资源注解

AnimatorRes
AnimRes
AnyRes
ArrayRes
AttrRes
BoolRes
ColorRes
DimenRes
DrawableRes
FractionRes
IdRes
IntegerRes
InterpolatorRes
LayoutRes
MenuRes
PluralsRes
RawRes
StringRes
StyleableRes
StyleRes
XmlRes

IntDef和StringDef注解

最后一种类型的注解是基于Intellij的“魔数”检查机制功能

【注:“魔数”就是那些不能看出有什么含义的数字常量,这里也包括字符串常量】

很多时候,出于性能的考虑,我们会使用整型常量代替枚举类型。例如我们有一个IceCreamFlavourManager类,它定义三种操作:

VANILLA
CHOCOLATE
STRAWBERRY

我们可以定义一个名为@Flavour的新注解,并使用@IntDef指定它可以接受的取值范围,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class IceCreamFlavourManager {

private int flavour;

public static final int VANILLA = 0;
public static final int CHOCOLATE = 1;
public static final int STRAWBERRY = 2;

@IntDef({VANILLA, CHOCOLATE, STRAWBERRY})
public @interface Flavour {
}

@Flavour
public int getFlavour() {
return flavour;
}

public void setFlavour(@Flavour int flavour) {
this.flavour = flavour;
}
}

这时如果我们使用直接字面量来调用IceCreamFlavourManager.setFlavour,IDE将提示错误如下:

wrong_flavour_error

IDE甚至会提示我们可以使用的有效取值:

ide_suggests_flavours

我们也可以指定整型取值可以用作标志,也就是说这些整型值可以使用’|’或者’&’进行与或等操作。如果我们定义@Flavour如下:

1
2
3
@IntDef(flag = true, value = {VANILLA, CHOCOLATE, STRAWBERRY})
public @interface Flavour {
}

那么可以进行如下调用:

1
iceCreamFlavourManager.setFlavour(IceCreamFlavourManager.VANILLA & IceCreamFlavourManager.CHOCOLATE);

@StringDef 用法与 @IntDef 基本差不多,只不过是针对String类型值而已。

更多信息可以参考tools site

如何实现一个简单的事件总线

MicroBus

使用

注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Receiver implements MicroBus.BusEventReceiver {

public onCreate() {
bus.register(this, String.class);
}

public onDestroy() {
bus.unregister(this, String.class);
}

@Override
public void onBusEvent(Object event) {
if (event instanceof String)
System.out.println("Hello, " + event);
}
}

运行

bus.post("World|");

运行结果

Hello, World!

Read More

Android 组件通信模式

本文对原文进行了精简

原文链接:Communication patterns for application components

紧耦合

组件之间相互持有引用,以及直接调用方法.在下面的代码中,MenuFragment持有MagazineActivity的直接引用,因此, MenuFragment 就与 MagazineActivity紧耦合了.
一旦没有了MagazineActivity,就无法工作了.

1
2
3
4
5
6
7
8
// 紧耦合示例

class MenuFragment extends Fragment {
private void onArticleClick(int articleId) {
MagazineActivity magAct = (MagazineActivity) getActivity();
magAct.showArticle(articleId);
}
}

在这样的设计中,一个类的修改可能会影响到一大波相关的类,如果开发中还涉及到多个开发人员,那么这种情况带来的只会是痛苦和悲哀..

松耦合的核心就是一种减少组件之间的依赖关系,一个松耦合系统可以很方便的分解为良构的元素.这样就使得系统更具有弹性与扩展性.
每个开发人员可以维护一个单独的模块,并使用标准的协议与其他部分通信.

常规解耦方式:接口

(个人补充:接口并不是单纯指代的java里面的interface关键字,而是指的一种消息规范/协议)
接口是一个强大的解耦工具:类可以通过接口进行通信,而不必要直接引用另一个具体类.一个类可以向外提供一个接口供其他类与之通信.在这个抽象层上,其他类不用关心具体的实现是什么.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//接口示例

class MenuFragment extends Fragment {
public static MenuFragment instantiate(ArticleSwitcher articleSwitcher) {
MenuFragment menuFragment = new MenuFragment();
menuFragment.articleSwitcher = articleSwitcher;
return menuFragment;
}

ArticleSwitcher articleSwitcher;

public void onArticleClick(int articleId) {
articleSwitcher.showArticle(articleId);
}
}

使用接口的缺点:

  1. 组件之间还是需要相互了解以传递接口,部分依赖依旧存在
  2. 接口不能通过intent传递
  3. 在大项目中,接口数量会急剧增加,导致大量的模板代码
  4. 当接口在组件之间传递时会形成接口链,导致复杂度上升

优雅解决方案:消息总线

基于发布/订阅者模式的通信方案.发布者发布一个通知,订阅者得到通知并做出反应,因此,发布者和订阅者之间就实现了解耦.

实现消息总线

隐式intent + BroadcastReceiver

通过隐式Intent的通信可以看作是消息队列的一种形式:
sendBroadcast(Intent) 或者 startActivity(Intent)是发布者方法,含有相应IntentFilter的BroadcastReceiver 或者 Activity则扮演订阅者的角色.

优点与缺点

优点:可以跨应用传递,也是Android里面的标准方法

缺点:无法传递复杂数据,必须通过bundle来传递

FluffyEvents 就是一个通过 BroadcastReceiver 实现的消息总线.

EventBus: 基于事件的消息总线

(“事件”本身也是一种消息,原理还是订阅/发布者模式)

事件本身可以是任何java类,如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 一个示例事件类

class DownloadProgressEvent {
private float progress;
public DownloadProgressEvent(float progress) {
this.progress = progress;
}

public float getProgress() {
return progress;
}
}

EventBus: 大致原理

EventBus使用合适的数据结构来维持事件以及订阅者的对应关系,比如Otto使用的方式:

1
2
/** Cache event bus subscriber methods for each class. */
private static final Map<Class<?>, Map<EventClass, Set>> SUBSCRIBERS_CACHE = new HashMap<Class<?>, Map<EventClass, Set>>();

每当有订阅者注册或者注销的时候,都会同时更新相应的对应关系,通常,Activity 或者 Fragment会在onResume阶段进行注册,在onPause阶段进行注销.(译者注:可以减少内存泄露的问题)

每当有事件发布的时候,EventBus遍历整个对应关系,找到所有符合条件的订阅者方法并执行.

Sticky Event

类似Android原生的StickyBroadcast,使得一个订阅者在任何时候注册都可以获得这个事件.

总结

由于EventBus并不是Android的内在机制,因此无法跨应用传递事件.

比较常用的两个EventBus开源类库:

Square’s Otto:基于注解,在Guava EventBus基础上针对Android平台进行了优化

GreenRobot’s EventBus:更好的线程分发机制.

(译者注: EventBus比较适合组件之间的解耦, 而接口是一个更抽象的概念,还可以用在view或者其他方面)

Android UI组件变量组织

这不是一篇技术文章,而是因为最近项目新来了几个android新人,所以我琢磨在代码风格上进行一下统一,所以有了以下以及后面的几篇文章。

java文件内部UI组件变量组织

UI组件使用小写字母“v”作为前缀。

v主要表示“View”,android源码中,变量前缀主要是“m”,但是个人觉得对于UI变量,还是用“v”比较清晰

意图导向,最小化抽象

UI变量的“声明类型”应该是能够实现意图的最小化抽象类型,变量名需要能表现出一个组件的功能。

比如,交互需求有一个ImageButton,点击之后的动作是触发一个搜索请求。应该怎么组织变量呢?

第一步:声明类型

声明这个变量大概有几种方式:

1
2
3
1. private ImageButton varName;
2. private ImageView varName;
3. private View varName;

这些声明都没有什么问题,问题是如果这个按钮只是单纯的触发一个点击事件,
那么也就表明我们的意图只需要使用View级别就行,因为View.setOnClickListener(OnClickListener)是我们唯一关心的功能,所以只需要将类型限定在View级别就行。

另外,UI是一个变化频度比较高的部分,越抽象就越容易修改。
换句话说,就是“面向接口编程”,View在这里就充当了接口的角色,这样,不管以后我们把ImageButton换成ImageView,Button或者TextView都不用修改定义的。

第二步:变量名

变量命名可能有下面几种:

    ImageButton vImageButton1

这种命名只应该出现在教程代码中,项目代码中是绝对不应该出现的

    ImageButton vSearchImageButton(或者vQueryBtn)

这个命名其实可以接受,但是一个问题就是,变量名涉及到具体的组件类型,参照上面第一步的说明,如果并不会使用到“ImageButton”特定的方法,就没有必要过于具体化一个变量。

    ImageButton vSearch

这个命名其实也可以接受,而且也比较适用。

    ImageButton vSearchAction

相比上面的vSearch,个人还是比较偏向于这种命名。
虽然都是纯粹的意图相关,但一个问题就是完成某个意图可能需要几个组件配合,比如对于搜索来说,一般还会有一个输入框,这时候就可以将输入框命名为vSearchContent,这样可以使用后缀“Action”和“Content”将两个部分区分开来

XML内部组件id命名

xml文件中的id命名也是一样,需要提供给java文件使用的尽量意图导线。
比如RelativeLayout命名中没有必要含有完整的“RelativeLayout”字段,因为实际项目中,需要调用RelativeLayout特定方法的情况基本没有。所以使用container等更泛的名字也可以。

至于只在布局文件中用来进行定位或者锚点的组件,可以将id命名得更明显一下,比如使用local,anchor之类名字,以便告诉java代码不应该随意获取这些组件,因为这些id并非稳定字段。

Android 触屏事件传递简述

ActivityA包含ViewGroupB,ViewGroupB包含ViewC,这里选取ViewGroupB(中间节点)作为当前节点视角

在此输入图片描述

过程一:当前节点与父节点的事件关系

ActivityA只关心ViewGroupB.onDispatchTouchEvent返回值,只要在ACTION_DOWN分发过程中B.onDispatchTouchEvent()返回为true,那么后续的事件都会向B分发。至于ViewGroupB在onDispatchTouchEvent方法中是怎么处理的,父节点ActivityA并不会过问。

换句话说,父节点向直接子节点ViewGroupB兜售ACTION_DOWN事件,子节点使用onDispatchTouchEvent方法的返回值进行应答,只要子节点表示能够处理ACTION_DOWN,那么父节点就会将后续事件持续交给子节点。

过程二:当前节点与子节点的事件关系

ViewGroupB从上一级收到ACTION_DOWN事件之后,会调用onInterceptTouchEvent判断是否进行拦截。如果不拦截,那么流程与上面的“过程一”一致,如果拦截,那么ViewGroupB就吃掉事件,直接忽略ViewC。

不过onInterceptTouchEvent属于内部方法,这个方法本身并不会影响当前节点的onDispatchTouchEvent返回值。

onDispatchTouchEvent返回值的判断

通常情况下,一个节点的onDispatchTouchEvent返回值只与当前节点的onTouchEvent返回值和子节点的onDispatchTouchEvent返回值有关:

1
this.onDispatchTouchEventValue = child.onDispatchTouchEventValue || this.onTouchEvent

当然,也可以强制给onDispatchTouchEvent赋一个返回值,尽管如此,也不应该强制抛弃原有的事件机制,因为原有的事件机制会进行一些状态清理等操作。

ACTION_DOWN示例图:
在此输入图片描述

Android 修改菜单

常用的与菜单相关的主要有三个回调:

1
2
3
4
5
6
7
8
//创建菜单的时候调用
public boolean onCreateOptionsMenu(Menu menu)

//显示菜单的时候调用
public boolean onPrepareOptionsMenu(Menu menu)

//选择菜单项的时候调用
public boolean onOptionsItemSelected(MenuItem item)

修改view

因此,如果我们想自定义菜单的显示效果,应该在创建菜单项的时候去干预创建过程,但是这个过程并没有向开发者开放。
这种情况下,我们要么重新自定义整个菜单视图,要么在系统创建号之后,再去修改每一个菜单项。

对于后面一中情况,我们可以在onCreateOptionsMenu中对每一项进行修改,主要方法就是通过ActionView来模拟:

1
2
3
4
5
MenuItem.setActionView(actionView);

或者

MenuItemCompat.setActionView(menuItem, actionView);//兼容包

比如,替换成Textview:

1
2
3
4
5
6
7
8
9
10
TextView action = new TextView(this);
action.setText(menuItem.getTitle());
action.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
onOptionsItemSelected(menuItem);
}
});
MenuItemCompat.setActionView(menuItem, action);

动态修改

有时候需要动态修改菜单,这个时候就需要分情况了,对于那些被收起来的菜单,可以在onPrepareOptionsMenu中进行动态增减,
因为onPrepareOptionsMenu会在每次菜单展示给用户的时候调用(比如点击ActionBar上的“更多”按钮)。

但是如果需要修改的菜单是一直展示在ActionBar或者Toolbar上面的话,可能触发修改菜单事件的时候,并不会涉及到onPrepareOptionsMenu,这个时候可以重新创建菜单,使用

1
2
3
4
5
Activity.invalidateOptionsMenu()

或者

ActionBarActivity.supportInvalidateOptionsMenu()//兼容包

来重新进入菜单流程。

演示图

在此输入图片描述

demo源码

android_menu