Gson解析复杂数据

本文主要关注所解析的 JSON 对象与已定义的 java 对象结构不匹配的情况,解决方案就是使用 JsonDeserializer 来自定义从 JSON 对象到 Java 对象的映射。

一个简单的例子

有如下 JSON 对象,表示一本书的基本信息,本书有两个作者。

1
2
3
4
5
6
{
'title': 'Java Puzzlers: Traps, Pitfalls, and Corner Cases',
'isbn-10': '032133678X',
'isbn-13': '978-0321336781',
'authors': ['Joshua Bloch', 'Neal Gafter']
}

这个 JSON 对象包含 4 个字段,其中有一个是数组,这些字段表征了一本书的基本信息。
如果我们直接使用 Gson 来解析:

1
Book book = new Gson().fromJson(jsonString, Book.class);

会发现 ‘isbn-10’ 这种字段表示在 Java 中是不合法的,因为 Java 的变量名中是不允许含有 ‘-‘ 符号的。
对于这个例子,我们可以使用 Gson 的 @SerializedName 注解来处理,不过注解也仅限于此,遇到后文的场景,注解就无能为力了。
这个时候,就是 JsonDeserializer 派上用场的时候。

Read More

Cupboard非官方翻译

Cupboard 是一个适用于 Android 的持久化存储方案,简单而且容易与现有代码集成。

更准确的说, Cupboard 只是一个存取对象方案。为了保简洁,它并不会去维护对象之间的关系,所以也并不是一个真正的ORM。

设计理念

设计 Cupboard 是因为现有的持久化框架并不能满足实际的需求,我们真正想要的是:

  1. 非侵入的:不必要继承某个特殊的Activity,model 也不必要去实现某个特殊的接口,甚至都不必要实现 DAO 模式
  2. 通用的选择:在整个应用中都可以使用所定义的 model 对象,而并不局限于数据库
  3. 完美适应 Android 自有的类,比如 Cursor 以及 ContentValues,这样,可以在任何时候回退到 Android 框架本身的实现

官网

Cupboard 官网(目测被墙了)

官方文档的非官方翻译

参见 官方文档的非官方翻译 https://xesam.github.io/cupboard-cn/

使用

引入 Cupboard 依赖,然后静态导入 cupboard():

build.gradle:

1
2
compile 'nl.qbusict:cupboard:(insert latest version)'
//最新是 2.2.0 所以可以这么写: compile 'nl.qbusict:cupboard:2.2.0'

java 类:

1
import static nl.qbusict.cupboard.CupboardFactory.cupboard;

在代码中可以这么调用:

1
2
3
public long storeBook(SQLiteDatabase database, Book book) {
return cupboard().withDatabase(database).put(book);
}

上面的代码将一个 Book entity 存入数据库中,然后返回记录的 id, 就这么简单!

更多参见 Cupboard 非官方翻译

chrome缓存设置

使用浏览器调试 http 缓存头的时候,有一些需要注意的地方。一个显著的问题是刷新(F5)或者地址栏输入网址方式(Enter)访问页面,所有请求都会自动设置 Cache-Control:max-age=0。
如果是强制刷新(SHIFT + F5)的方式,所有请求都会自动设置 Cache-Control:no-cache。

另外,控制台(Network)也有一个 “disable cache” 的选项,需要注意。

启用 stale-while-revalidate 扩展,这个需要通过 chrome://flags 设置,将 stale-while-revalidate 设置为 enable。

如果需要在浏览器里面去掉这个强制设定的“Cache-Control:max-age=0”,可以使用 ajax 来发起请求,或者使用页面内链接的方式访问。
比如直接设置 a 标签的 href,这个时候浏览器就不会设定 max-age=0 了。

Read More

BIOS与0x7C00

你知道 0x7C00,这个在 x86 汇编中的一个神秘数字吗?
0x7C00 是一个内存地址,BIOS 就是将 MBR (Master Boot Record, hdd/fdd 的第一个 sector) 读入 0x7C00 这个地址,
然后进行后续引导的。

操作系统或是 bootloader 的开发者必须假设他们的汇编代码被加载并从 0x7C00 处开始执行。
不过,为什么在 x86 中 BIOS 要将 MBR 加载到 0x7C00 呢?在回答这个问题之前,我们还有几个疑惑:

第一个疑惑:

我翻阅了所有的 Intel x86(32bit) 编程手册,都没有找到 0x7C00 这个数字。

没错!0x7C00 不是 x86 CPU 里面的定义,你当然不可能在 cpu 文档里面找得到。那么你就要问了,“那是谁定义了 0x7C00?”

第二个疑惑:

0x7C00 的值等于 32KiB - 1024B,这个数字表示什么意思?

不管是谁定义了 0x7C00,他/她为何要定义这样一个奇怪的数字?

Read More

传值调用与传引用调用

这个问题的关键是明确“传值调用(Call by value)”与“传引用调用(Call by reference)”的定义。

简单来说:

“传值调用”是将值(values)作为函数参数传入;
“传引用调用”是将变量(variables)本身作为函数参数传入;

一个隐喻:

“传值调用”是我在纸片上写下一些内容并把纸片交给你, 内容可以是一个URL,也可能是一份完整的《战争与和平》副本。
不管是什么,现在都在我给你的那张纸上,所以它实际上是你的纸。你可以在那张纸上随便涂鸦,也可以通过那张纸找到别的什么东西。

“传引用调用”是当我在我的笔记本上写了一些内容然后把笔记本递给你。
你可以在我的笔记本上涂鸦(也许我想要你这么做,也许我没料到你会这么做),然后我保留我的笔记本,你的任何涂鸦也一并保留在笔记本里。
此外,如果你或我写的是有关如何找到其他内容的信息,无论你或我可以去那里,更改这些信息。

Read More

Android Splash实践

Splash 就是通常说的启动页,启动页不是必须的,也不一定要用单独的 Activity 实现。使用 Splash 的大致出于以下看考虑:

  1. 为了加入开屏广告,在 App 广告中,这种开屏广告价格是比较高的。
  2. App 从启动到到进入第一个功能界面需要一定的时间,App 会持续一段时间的白屏。特别是冷启动,以及第一次安装启动的时候,这种白屏会特别明显。

系统在启动 App 的第一个 Activity 时,会先显示 Activity 对应 Window 的 Background,
所以可以将第一个 Activity 的 WindowBackground 设置为特定的颜色或者图片,那么在启动阶段,会显示我们设置的 WindowBackground,而不是持续的白屏。

values/styles.xml

1
2
3
4
5
6
<style name="SplashBase" parent="AppTheme">
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowBackground">@drawable/as_splash</item>
</style>

一个典型的 Splash 界面如下:

即底部有一个产品的 Logo,界面中间或者上部是产品的 Slogan 或者图标。

为了实现这种效果,最简单的方式就是把 WindowBackground 设置成一张带有 Logo 和 Slogan 的全屏图片。不过这种处理方式太暴力了,而且会发生不可避免的变形。
比较合适的方式是把界面元素分成多个部分,然后通过 LayerDrawable 组合到一起。如下:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" /><!-- 白色底色 -->
<item
android:drawable="@drawable/as_slogan"
android:gravity="center" /><!-- slogan 居中 -->
<item
android:bottom="16dp"
android:drawable="@drawable/as_logo"
android:gravity="bottom|center_horizontal" /><!-- logo 居底部,并有 16dp 的边距 -->
</layer-list>

在 6.0 以下的系统中,对 item 的解析可能会有问题,所以,为了兼容老系统,建议这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<item>
<bitmap
android:gravity="center"
android:src="@drawable/as_slogan" />
</item>
<item android:bottom="16dp">
<bitmap
android:gravity="bottom"
android:src="@drawable/as_logo" />
</item>
</layer-list>

这样可以避免变形,也方便计算各种边距。要显示广告的时候,则可以将广告区域填满 Logo 上部区域(前景区域),从而遮住 Slogan 区域(背景区域)。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="dev.xesam.android.splash.demo.SplashActivity">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="100dp"
android:background="#e4e4e4"><!-- 增加一个背景色,避免广告图片穿透 -->

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ad Area(Left TOp)" />

<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:src="@drawable/ad_1" />

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="Skip" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:text="Ad Area(Right Bottom)" />
</RelativeLayout>
</RelativeLayout>

如果 Splash 还有广告的话,大致会类似下面:
即底部的产品 Logo 保持不变,界面上部换成对应的广告。

问题

如果要求在 Splash 的时候 Fullscreen,也就是隐藏 StatusBar,可以使用 Fullscreen 主题。 不过 Fullscreen 主题并不会隐藏底部的 Navigation Bar。
在 5.0+ 带有 Navigation Bar 的手机上,windowBackground 会显示在 Navigation Bar 下面,也就是背景图会被遮挡了一部分。
因此,在 5.0+ 系统中,可以设置:

values-v21/styles.xml

1
2
3
<style name="Splash" parent="SplashBase">
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
</style>

当然,也可以使用

1
getWindow().getDecorView().setSystemUiVisibility()

来隐藏 Navigation Bar,不过这种操作会导致其他的问题:当 Navigation Bar 这种系统 UI 被隐藏之后,用户的第一次触屏操作会导致系统 UI 的显示,
结果就导致其他 View 的点击事件失效,需要两次点击才能触发 App 的事件操作。所以,如果 Splash 不是单纯的品牌展示作用,还是尽量避免这种处理方式。

示例 Demo 参见 GitHub AndroidSplash

Android 多语言切换

这里的多语言切换专指应用内的多语言切换,不涉及直接通过应用修改系统语言设置的功能。比如微信里面的

我 -> 设置 -> 通用 -> 多语言

举个例子,假如 App 支持简体和繁体两种设置,默认界面为“中文简体”。

如果用户选择“中文简体”,那么展示简体界面;
如果用户选择“中文繁体”,那么展示繁体界面;
如果用户选择“跟随系统”,那么如果系统语言设置是“中文简体”,则展示简体界面,如果系统语言设置是“中文繁体”,则展示繁体界面,
如果系统语言设置是“English(US)”,则展示默认界面,即简体界面。

上文描述功能的流程图大致如下:

android_push

另外,在 App 运行过程中,用户如果在系统的设置里面切换语言,同样会提示应用“语言设置”发生变化,因此,多一个流程图:

android_push

此图与前一个图的区别就是,如果用户没有选择“跟随系统”,那么,来自系统的设置就被忽略掉。

现在需要处理的问题如下:

  1. 如何获取应用内语言设置
  2. 如何获取系统语言设置
  3. 如何更改应用的语言设置

1. 如何获取应用内语言设置

这个问题比较好处理,在 SharedPreference 内保存用户的语言设置即可。

1
2
3
4
5
6
7
8
9
10
class UserLocale{
public static final int FOLLOW_SYSTEM = 0;//跟随系统
public static final int SIMPLIFIED_CHINESE = 1; //简体中文
public static final int TRADITIONAL_CHINESE = 2; //繁体中文

//获取用户设置
public int getUserLocale(){
//...
}
}

2. 如何获取系统语言设置

第一种:

1
Locale.getDefault()

这是从 Java 来的方法,可以通过 Locale.setDefault(Locale) 来修改。
不过这个方法有时候会出现莫名其妙的问题,比如一会儿返回系统语言 “zn_CN”,一会儿就返回 “en_US”,并不稳定。

第二种:

1
context.getResources().getConfiguration().locale;

对于 Android N 来说,应该这样

1
context.getResources().getConfiguration().getLocales().get(0);

这个返回的是当前 APP 的 Resource 对应的 Locale 设置。这个也可以通过代码来修改。后面“更新应用的语言设置”的时候就是通过更新 Configuration 来实现的。
同样,问题是我们肯定要修改 Configuration,一旦修改之后,这个操作就再也获取不到系统语言设置了。
如果一定要用这个,一个变通的方法就是在一开始就将 Configuration 保存下来,同时在以后每次系统语言设置有改动的时候,就同步更新一次即可。

第三种:

1
Resources.getSystem().getConfiguration().locale;

这个方法与上面的很像,不过这个返回的是系统全局 Resource 的 Locale,可以当做系统 Locale 来用。

3. 如何更改应用的语言设置

如上文所述,用户选定语言之后,要修改 APP 对应的 Configuration:

1
2
3
4
5
6
7
8
Locale targetLocale = getTargetLocale();
Locale.setDefault(targetLocale);
Resources resources = context.getResources();
Configuration config = resources.getConfiguration();
DisplayMetrics dm = resources.getDisplayMetrics();
config.locale = targetLocale;
resources.updateConfiguration(config, dm);

至于 getTargetLocale() 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int getTargetLocale(){
int userType = UserLocale.getUserLocale();
if(userType == LanguageType.FOLLOW_SYSTEM){
int sysType = getSysLocale();
if(sysType == LanguageType.TRADITIONAL_CHINESE){
return Locale.TRADITIONAL_CHINESE;
}else{
return Locale.SIMPLIFIED_CHINESE;
}
}else if(userType == LanguageType.TRADITIONAL_CHINESE){
return Locale.TRADITIONAL_CHINESE;
}else{
return Locale.SIMPLIFIED_CHINESE;
}
}

getSysLocale() 的实现参考 “2. 如何获取系统语言设置”。

触发语言切换的来源有两种:

  1. 应用内切换语言设置
  2. 系统切换语言设置

对于“应用内切换语言设置”来说,用户选择语言之后,需要手动重启所有的界面,为了方便,可以直接重启 Root Activity 并 Clear Top,这样方便快捷。

对于“系统切换语言设置”,默认情况下,如果我们的 APP 在后台运行,系统会主动重建所有的 Activity。我们可以在 Root Activity 监听

Intent.ACTION_LOCALE_CHANGED

广播,在重启 Activity 之前修改 APP 全局的 Configuration。

如果对应的 Activity 设置了

1
android:configChanges="locale"

则可以直接在

1
Activity.onConfigurationChanged(Configuration newConfig)

中响应 Configuration 的变化。如果无效,请参《android:configChanges locale 改语言后,该配置不起作用的原因》
更详细的可以参考《Android-语言设置流程分析》

几个问题

  1. RadioButton 或者 CheckBox 在重建之后没有更改语言。

这个问题应该存在于 5.0 之前的系统,问题原因可以参考Not all items in the layout update properly when switching locales

主要是 setFreezesText(true) 导致的,不过 5.0 之后好像就修复了这个问题,因为 Framework 中 CompoundButton 的源码有改动:

Android 4.1.1_r3:

1
2
3
4
5
6
7
8
9
10
@Override
public Parcelable onSaveInstanceState() {
setFreezesText(true);
Parcelable superState = super.onSaveInstanceState();

SavedState ss = new SavedState(superState);

ss.checked = isChecked();
return ss;
}

Android 6.0:

1
2
3
4
5
6
7
8
9
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();

SavedState ss = new SavedState(superState);

ss.checked = isChecked();
return ss;
}

比不过其他版本源码还没翻,我不能太确认。

Android 编写可维护的Android代码

todo 未完 未整理

让一个程序可维护是多方面的,本文不谈各种模式,只谈原则。兴起随笔,勿喷。。

模块之间的通信,基于接口的通信,用方法来表明意图,而不是用参数来表明意图

比如发送消息

1
2
3
4
5
6
7
public static final int MSG_START = 0;
public static final int MSG_STOP = 1;

// start
Message.obtain(handler, MSG_START).sendToTarget();
// stop
Message.obtain(handler, MSG_STOP).sendToTarget();

更容易维护的代码:

1
2
3
4
5
6
7
8
9
10
11
12
private static final int MSG_START = 0;
private static final int MSG_STOP = 1;

// start
public void start(){
Message.obtain(handler, MSG_START).sendToTarget();
}
// stop
public void stop(){
Message.obtain(handler, MSG_STOP).sendToTarget();
}

隐藏细节

BroadcastReceiver 的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public abstract class BroadcastReceiverA extends BroadcastReceiver {

static final String ACTION_A = "action.a";
private IntentFilter mIntentFilter;

public CityBroadcastReceiver() {
mIntentFilter = new IntentFilter();
mIntentFilter.addAction(ACTION_A);
}

@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (TextUtils.isEmpty(action)) {
return;
}

switch (action) {
case ACTION_A:
onActionXXX(context);
break;
}
}

public final void register(Context context) {
FireFlyBroadcastManager.getInstance(context).registerReceiver(this, mIntentFilter);
}

public final void unRegister(Context context) {
FireFlyBroadcastManager.getInstance(context).unregisterReceiver(this);
}

protected void onActionXXX(Context context, Intent intent) {

}
}

出发点就是将注册,接收,解析的各个部分都封装在一起,即,相关的责任交由相关的组件处理。
当然,这个例子里面每个广播都会使用一个 IntentFilter,加重了系统负担,但是你可以通过合适的设计,使用一个广播持有集合来管理所有的接口。
总而言之,这里主要想说明的是,在责任划分完毕之后,隐藏细节。

用层次来构建人与机器的桥梁

通常一个 app 的数据流向是

Server <--> App <--> User

这个时候,程序员至少应该构建两层抽象,分别对应:

代码与接口之间的抽象:

Server <--> App 

代码与需求之间的抽象:

App <--> User

而后者的优先级更高,所以,需要先制定代码与需求之间的抽象。比如有个用户登陆的需求,要求功能:

  1. 注册
  2. 登录
  3. 退出登录

这个时候,需要构建的抽象就是:

1
2
3
4
UserMgr:
register()
login()
logout()

用户的每一步操作,我们都只需要调用对应的方法而已。至于方法内部怎么实现,那是另一层抽象的问题。

待补充,待整理。。。

Android 第三方推送SDK集成简述

由于我们 Android 应用的推送(Push)效果一直不是很理想,所以前段时间调研了现在国内市场上几种推送集成方案,大致包括:

  1. 个推推送
  2. 极光推送
  3. 阿里云推送
  4. 友盟推送。

不过鉴于混乱的 Android 局面来说, 各个推送效果都不算很好,也都是难兄难弟。

前言

就实现来说,iOS 的推送机制就很好,系统统一管理推送,然后分发给对应的应用,各个应用自己处理来自系统的推送消息。
这样既不用考虑应用进程的保活问题,也不用考虑无休止的兼容问题。

虽然小米或者华为之列有自己的系统推送,通知栏推送效果还好,至于透传推送,效果就呵呵了。
我们暂且不论效果,他们各自的不兼容不仅没有给开发者减压,还给 App 增加了压力。

举个例子,如果我针对 MIUI 包含 MIUI 的系统推送,并在小米应用市场上架,这样小米手机的用户推送就棒棒哒。

不过华为手机怎么办,那就集成华为的系统推送呗,然后在华为应用市场上架。

三星手机怎么办,一样啊,集成三星系统 SDK,在三星应用市场上架。

至于其他的应用市场,都用第三方 Push SDK 吧,毕竟要优先照顾主流手机厂商用户。

那么问题来了,如果各个应用市场相互抓包怎么办,比如一个应用市场抓取了小米应用市场的 APK, MIUI 用户下载了怎么办,或者 MIUI 用户从百度应用市场下载怎么办,这就尴尬了。
除非我在一个 APK 里面集成所有的第三方厂商的系统推送,然后还得外送一个第三方 Push SDK,不然就没法预防各种情况。所以最终还是只用第三方 Push SDK 简单。

国内太监 Android 应用的一部分保活需求,就来自推送,这样不但让系统背了锅,也恶心了自己,更恶心了用户。

集成指南

为了在各个 Push SDK 之间无缝切换,所以肯定会对 Push 层进行封装,另一个常见的需求是:

当 Push 消息到达的时候,如果用户在接收消息界面,那就直接更新界面,如果用户不在接收消息界面,那就弹出通知栏提示。

所以,针对这样的现状与需求,大致的结构如下:

android_push

过程描述:

  1. App 注册对应的 BroadcastReceiver,并定义不同的优先级,用以接收推送消息。
  2. 第三方 Push SDk 接收到推送消息,解析之后,发送一个优先级系统广播。
  3. Android 系统将广播将投递给对应的 App。
  4. App 组件的 BroadcastReceiver 按照预定义的优先级来接收消息广播,如图,广播会一层层穿透各个组件的 BroadcastReceiver。
    如果广播在传递过程中,被某个组件拦截并消费了,就停止传播,如果被最后一个 BroadcastReceiver,就进行收尾操作。
    比如,如果当前的 Activity 接收了广播,弹出一个对话框,然后告知系统广播已经被消费即可;
    如果没有任何 Activity 消费广播,最后一个 BroadcastReceiver 就可以接收广播,然后弹出一个通知栏消息,这样就满足需求了。

集成过程中的几个问题:

第一个问题,由于第三方推送过来的消息数据通常都格式不一,所以就需要将接收到的推送消息进行一道抽象,将第三方的数据包装成我们的语义对象。
这样屏蔽细节,也不扯什么设计模式了,反正在计算机工程应用方面,就是分层,分层,再分层。

第二个问题,第三方推送面向开发者的接收方式也不一样,比如个推是要求开发者继承一个 BroadcastReceiver 来接收消息,但是友盟要求开发者实现一个 PushHandler 来接收消息。
为了统一处理,我个人建议使用 BroadcastReceiver 的接收方式,因为在最终的处理流程中,依旧逃不过 BroadcastReceiver 的使用。

因此,整个设计中涉及到两种 BroadcastReceiver,第一种是用来统一接收第三方 Push 消息的,第二种是用来子在应用中处理接收到的消息的。

示例代码如下:

定义抽象数据消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//每个消息都会有类型
class Msg{
public int type;
}

final class MsgType{
public static final int TYPE_USER_REPLY = 1;
}

//示例 用户回复
public class UserReply extends Msg implements Parcelable{
public String title;
public long time;
}

//实现 Parcelable
...

定义接收第三方 Push 消息的 BroadcastReceiver,我称之为“标准” Push 接收器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class PushMsgBroadcastReceiver extends BroadcastReceiver{

public static final String ACTION_RECEIVE_MSG = "dev.xesam.push.action.msg";
public static final String EXTRA_RECEIVE_MSG = "dev.xesam.push.extra.msg";

@Override
public final void onReceive(Context context, Intent intent) {
String message = intent.getStringExtra(EXTRA_RECEIVE_MSG);
}

public Msg parsePushMsg(String message){
return ...
}

public void dispatchPushMsg(String message ){
Msg msg = parsePushMsg(message);
if(msg.type == MsgType.TYPE_USER_REPLY){
//投递消息到相应的应用内 PushHandleReceiver
Intent intent = new Intent();
intent.setAction(PushHandleReceiver.ACTION_RECEIVE_MSG);
intent.putExtra(PushHandleReceiver.EXTRA_RECEIVE_MSG, message);
//注意,这里发送的是优先级广播
context.sendOrderedBroadcast(intent, null);
}else ...
}
}

个推本身就是用的 BroadcastReceiver,直接使用即可。至于友盟的推送,可以将 Push 消息转发到我们定义的 PushMsgBroadcastReceiver 上,其他的 sdk 可以类似处理:

1
2
3
4
5
6
7
8
9
10
11
12

public class UmengHandler extends UmengMessageHandler {
@Override
public void dealWithCustomMessage(Context context, UMessage msg) {
//将友盟的推送转化为广播的形式
Intent intent = new Intent();
intent.setAction(PushMsgBroadcastReceiver.ACTION_RECEIVE_MSG);
intent.putExtra(PushMsgBroadcastReceiver.EXTRA_RECEIVE_MSG, message);
context.sendBroadcast(intent);
}
}

定义应用内各个组件用来处理消息的 BroadcastReceiver :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

public class PushHandleReceiver extends BroadcastReceiver {

public static final String ACTION_RECEIVE_MSG = "dev.xesam.app.action.msg";
public static final String EXTRA_RECEIVE_MSG = "dev.xesam.app.extra.msg";

@Override
public final void onReceive(Context context, Intent intent) {
Msg msg = intent.getParcelableExtra(EXTRA_RECEIVE_MSG);
boolean consumed = false;
if(msg.type == MsgType.TYPE_USER_REPLY){
consumed = onReceiveUserReply((UserReply) msg);
}

//如果广播被消耗,停止广播
if(consumed){
abortBroadcast();
}
}

public boolean onReceiveUserReply(UserReply userReply){
...
}

...
}

在上图的设计中,会有一个优先级最低,用来“兜底”的 BroadcastReceiver,用来在没人处理广播的情况下,将广播内容发送到通知栏消息。
我们可以将这个 BroadcastReceiver 注册到 AndroidManifest, 这样即使在用户没有启动的时候也可以接收并处理广播。

1
2
3
4
5
6
7
8
9
10
11

public class LastHandleReceiver extends PushHandleReceiver {

public boolean onReceiveUserReply(UserReply userReply){
//这里将 UserReply 发送到通知栏
return true;
}

...
}

如果我们想在某个 Activity 处理广播,拦截即可,这样就可以在对应界面弹出一个对话框,而不会弹出通知栏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

public class userActivity extends Activity {

public PushHandleReceiver mUserHandleReceiver new PushHandleReceiver() {

public boolean onReceiveUserReply(UserReply userReply){
showDialog(userReply);
return true;
}

...
}

public void onResume(){
IntentFilter intentFilter=new IntentFilter();
intentFilter.addAction(PushConstant.INTENT_ACTION_PUSH_DEMISE_REMINDER);
intentFilter.addAction(PushConstant.INTENT_ACTION_PUSH_FEED);
//优先级较高
intentFilter.setPriority(99);
context.registerReceiver(mUserHandleReceiver, intentFilter);
}

public void onPause(){
context.unregisterReceiver(mUserHandleReceiver);
}

public void showDialog(UserReply){
...
}
}

注意点

  1. 第三方通常的接收消息里面通常不仅仅是你的应用消息,还有 SDK 自身的 push id 什么的,这个需要区分处理。
  2. SDK 自身的 push id 获取时机各不相同,需要区分对待,如果你自己的后台想知道 push id,处理方式与上图类似。
  3. 由于通知栏随时可能出现,所以面临的一个问题是点击通知栏之后怎么办。
    这一个“回退栈”的交互问题,我个人偏好的是,点击通知栏之后直接打开 MainActivity, 在 OnNewIntent 回调中根据点击通知消息携带的 Intent 再来跳转到对应的界面,
    就像微信一样,这样既可以避免回退之后直接退出应用的尴尬,也可以避免繁琐的应用栈判断,各自爱好,唯与产品经理 PK 而已。

以上只是一种设计方式,其他设计方式欢迎讨论。

Q群:315658668