本文简介:本文前篇,可以帮助朋友们快速集成极光推送。本文后篇,是我自己项目实践的一些总结和心得,应该对读者们还是很有参考价值的,相信读完这篇文章,你会对极光推送有更加深入的理解,而不仅仅只是会集成而已。总之呢,集成第三方SDK,都不是很难的事情,仔细阅读文档,一步步来,遇到Bug,慢慢解决就行,实在解决不了,可以问问客服小哥哥或者小姐姐,重要的是,你得有着解决它的决心和耐心。
《一》JPush SDK的集成
简要介绍:
极光推送(JPush)是一个端到端的推送服务,使得服务器端消息能够及时地推送到终端用户手机上,让开发者积极地保持与用户的连接,从而提高用户活跃度、提高应用的留存率。
开发者集成 JPush Android SDK 到其应用里,JPush Android SDK 创建到 JPush Cloud 的长连接,为 App 提供永远在线的能力。 当开发者想要及时地推送消息到达 App 时,只需要调用 JPush API 推送,或者使用其他方便的智能推送工具,即可轻松与用户交流。
JPush Android SDK 是作为 Android Service 长期运行在后台的,从而创建并保持长连接,保持永远在线的能力。
假设你已经注册了极光的账号,登录进来了。点击立即体验,创建自己的应用。
App端集成需要用到的AppKey。
添加集成代码,此处使用jcenter的方式集成。
一、确认android studio的 Project 根目录的主 gradle 中配置了jcenter支持。(新建project默认配置就支持)
buildscript {
repositories {
jcenter()
}
......
}
allprojets {
repositories {
jcenter()
}
}
二、在 module 的 gradle 中添加依赖和AndroidManifest的替换变量。
android {
......
defaultConfig {
applicationId "com.xxx.xxx" //JPush上注册的包名.
......
ndk {
//选择要添加的对应cpu类型的.so库。
abiFilters 'armeabi', 'armeabi-v7a', 'arm64-v8a'
// 还可以添加 'x86', 'x86_64', 'mips', 'mips64'
}
manifestPlaceholders = [
JPUSH_PKGNAME : applicationId,
JPUSH_APPKEY : "你的appkey", //JPush上注册的包名对应的appkey.
JPUSH_CHANNEL : "developer-default", //暂时填写默认值即可.
]
......
}
......
}
dependencies {
......
compile 'cn.jiguang.sdk:jpush:3.1.1' // 此处以JPush 3.1.1 版本为例。
compile 'cn.jiguang.sdk:jcore:1.1.9' // 此处以JCore 1.1.9 版本为例。
......
}
三、AndroidManifest.xml中添加
<!--Jpush配置 所需权限start-->
<!-- Required -->
<permission
android:name="你的应用包名.permission.JPUSH_MESSAGE"
android:protectionLevel="signature" />
<!-- Required 一些系统要求的权限,如访问网络等-->
<uses-permission android:name="你的应用包名.permission.JPUSH_MESSAGE" />
<uses-permission android:name="android.permission.RECEIVE_USER_PRESENT" />
<!--<uses-permission android:name="android.permission.INTERNET" />-->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!--<uses-permission android:name="android.permission.READ_PHONE_STATE" />-->
<!--<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />-->
<!--<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />-->
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<!--<uses-permission android:name="android.permission.VIBRATE" />-->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<!--<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />-->
<!--<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />-->
<!-- Optional for location -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <!-- 用于开启 debug 版本的应用在6.0 系统上 层叠窗口权限 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!--<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />-->
<!--<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />-->
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<!--<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />-->
<!--<uses-permission android:name="android.permission.GET_TASKS" />-->
<!--Jpush配置 所需权限end-->
<!--Jpush配置 start-->
<application
<!-- Rich push 核心功能 since 2.0.6-->
<activity
android:name="cn.jpush.android.ui.PopWinActivity"
android:theme="@style/MyDialogStyle"
android:exported="false">
</activity>
<!-- Required SDK核心功能-->
<activity
android:name="cn.jpush.android.ui.PushActivity"
android:configChanges="orientation|keyboardHidden"
android:theme="@android:style/Theme.NoTitleBar"
android:exported="false">
<intent-filter>
<action android:name="cn.jpush.android.ui.PushActivity" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="你的应用包名" />
</intent-filter>
</activity>
<!-- Required SDK 核心功能-->
<!-- 可配置android:process参数将PushService放在其他进程中 -->
<service
android:name="cn.jpush.android.service.PushService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="cn.jpush.android.intent.REGISTER" />
<action android:name="cn.jpush.android.intent.REPORT" />
<action android:name="cn.jpush.android.intent.PushService" />
<action android:name="cn.jpush.android.intent.PUSH_TIME" />
</intent-filter>
</service>
<!-- since 3.0.9 Required SDK 核心功能-->
<provider
android:authorities="你的应用包名.DataProvider"
android:name="cn.jpush.android.service.DataProvider"
android:exported="false"
/>
<!-- since 1.8.0 option 可选项。用于同一设备中不同应用的JPush服务相互拉起的功能。 -->
<!-- 若不启用该功能可删除该组件,将不拉起其他应用也不能被其他应用拉起 -->
<service
android:name="cn.jpush.android.service.DaemonService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="cn.jpush.android.intent.DaemonService" />
<category android:name="你的应用包名" />
</intent-filter>
</service>
<!-- since 3.1.0 Required SDK 核心功能-->
<provider
android:authorities="你的应用包名.DownloadProvider"
android:name="cn.jpush.android.service.DownloadProvider"
android:exported="true"
/>
<!-- Required SDK核心功能-->
<receiver
android:name="cn.jpush.android.service.PushReceiver"
android:enabled="true"
android:exported="false">
<intent-filter android:priority="1000">
<action android:name="cn.jpush.android.intent.NOTIFICATION_RECEIVED_PROXY" /> <!--Required 显示通知栏 -->
<category android:name="你的应用包名" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.USER_PRESENT" />
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
<!-- Optional -->
<intent-filter>
<action android:name="android.intent.action.PACKAGE_ADDED" />
<action android:name="android.intent.action.PACKAGE_REMOVED" />
<data android:scheme="package" />
</intent-filter>
</receiver>
<!-- Required SDK核心功能-->
<receiver android:name="cn.jpush.android.service.AlarmReceiver" android:exported="false"/>
<!-- User defined. For test only MyReceiver为用户自定义的广播接收器-->
<receiver
android:name=".util.MyReceiver"
android:exported="false"
android:enabled="true">
<intent-filter>
<action android:name="cn.jpush.android.intent.REGISTRATION" /> <!--Required 用户注册SDK的intent-->
<action android:name="cn.jpush.android.intent.MESSAGE_RECEIVED" /> <!--Required 用户接收SDK消息的intent-->
<action android:name="cn.jpush.android.intent.NOTIFICATION_RECEIVED" /> <!--Required 用户接收SDK通知栏信息的intent-->
<action android:name="cn.jpush.android.intent.NOTIFICATION_OPENED" /> <!--Required 用户打开自定义通知栏的intent-->
<action android:name="cn.jpush.android.intent.CONNECTION" /><!-- 接收网络变化 连接/断开 since 1.6.3 -->
<category android:name="你的应用包名" />
</intent-filter>
</receiver>
<!-- Required . Enable it you can get statistics data with channel -->
<meta-data android:name="JPUSH_CHANNEL" android:value="developer-default"/>
<meta-data android:name="JPUSH_APPKEY" android:value="应用的Appkey" /> <!-- </>值来自开发者平台取得的AppKey-->
</application>
<!--Jpush配置 end-->
四、自定义一个广播接收器MyReceiver(此处直接使用官方Demo中的MyReceiver)
/**
* 自定义接收器
*
* 如果不定义这个 Receiver,则:
* 1) 默认用户会打开主界面
* 2) 接收不到自定义消息
*/
public class MyReceiver extends BroadcastReceiver {
private static final String TAG = "JIGUANG-Example";
@Override
public void onReceive(Context context, Intent intent) {
try {
Bundle bundle = intent.getExtras();
Logger.d(TAG, "[MyReceiver] onReceive - " + intent.getAction() + ", extras: " + printBundle(bundle));
if (JPushInterface.ACTION_REGISTRATION_ID.equals(intent.getAction())) {
String regId = bundle.getString(JPushInterface.EXTRA_REGISTRATION_ID);
Logger.d(TAG, "[MyReceiver] 接收Registration Id : " + regId);
//send the Registration Id to your server...
} else if (JPushInterface.ACTION_MESSAGE_RECEIVED.equals(intent.getAction())) {
Logger.d(TAG, "[MyReceiver] 接收到推送下来的自定义消息: " + bundle.getString(JPushInterface.EXTRA_MESSAGE));
processCustomMessage(context, bundle);
} else if (JPushInterface.ACTION_NOTIFICATION_RECEIVED.equals(intent.getAction())) {
Logger.d(TAG, "[MyReceiver] 接收到推送下来的通知");
int notifactionId = bundle.getInt(JPushInterface.EXTRA_NOTIFICATION_ID);
Logger.d(TAG, "[MyReceiver] 接收到推送下来的通知的ID: " + notifactionId);
} else if (JPushInterface.ACTION_NOTIFICATION_OPENED.equals(intent.getAction())) {
Logger.d(TAG, "[MyReceiver] 用户点击打开了通知");
//打开自定义的Activity
Intent i = new Intent(context, TestActivity.class);
i.putExtras(bundle);
//i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP );
context.startActivity(i);
} else if (JPushInterface.ACTION_RICHPUSH_CALLBACK.equals(intent.getAction())) {
Logger.d(TAG, "[MyReceiver] 用户收到到RICH PUSH CALLBACK: " + bundle.getString(JPushInterface.EXTRA_EXTRA));
//在这里根据 JPushInterface.EXTRA_EXTRA 的内容处理代码,比如打开新的Activity, 打开一个网页等..
} else if(JPushInterface.ACTION_CONNECTION_CHANGE.equals(intent.getAction())) {
boolean connected = intent.getBooleanExtra(JPushInterface.EXTRA_CONNECTION_CHANGE, false);
Logger.w(TAG, "[MyReceiver]" + intent.getAction() +" connected state change to "+connected);
} else {
Logger.d(TAG, "[MyReceiver] Unhandled intent - " + intent.getAction());
}
} catch (Exception e){
}
}
// 打印所有的 intent extra 数据
private static String printBundle(Bundle bundle) {
StringBuilder sb = new StringBuilder();
for (String key : bundle.keySet()) {
if (key.equals(JPushInterface.EXTRA_NOTIFICATION_ID)) {
sb.append("\nkey:" + key + ", value:" + bundle.getInt(key));
}else if(key.equals(JPushInterface.EXTRA_CONNECTION_CHANGE)){
sb.append("\nkey:" + key + ", value:" + bundle.getBoolean(key));
} else if (key.equals(JPushInterface.EXTRA_EXTRA)) {
if (TextUtils.isEmpty(bundle.getString(JPushInterface.EXTRA_EXTRA))) {
Logger.i(TAG, "This message has no Extra data");
continue;
}
try {
JSONObject json = new JSONObject(bundle.getString(JPushInterface.EXTRA_EXTRA));
Iterator<String> it = json.keys();
while (it.hasNext()) {
String myKey = it.next();
sb.append("\nkey:" + key + ", value: [" +
myKey + " - " +json.optString(myKey) + "]");
}
} catch (JSONException e) {
Logger.e(TAG, "Get message extra JSON error!");
}
} else {
sb.append("\nkey:" + key + ", value:" + bundle.getString(key));
}
}
return sb.toString();
}
//send msg to MainActivity
private void processCustomMessage(Context context, Bundle bundle) {
if (MainActivity.isForeground) {
String message = bundle.getString(JPushInterface.EXTRA_MESSAGE);
String extras = bundle.getString(JPushInterface.EXTRA_EXTRA);
Intent msgIntent = new Intent(MainActivity.MESSAGE_RECEIVED_ACTION);
msgIntent.putExtra(MainActivity.KEY_MESSAGE, message);
if (!ExampleUtil.isEmpty(extras)) {
try {
JSONObject extraJson = new JSONObject(extras);
if (extraJson.length() > 0) {
msgIntent.putExtra(MainActivity.KEY_EXTRAS, extras);
}
} catch (JSONException e) {
}
}
LocalBroadcastManager.getInstance(context).sendBroadcast(msgIntent);
}
}
}
五、在Application中的onCreate()方法中初始化极光推送
// 初始化 JPush
JPushInterface.init(this);
//发布时关闭日志
JPushInterface.setDebugMode(false);
六、最后不要忘了添加混淆代码哦!
请在工程的混淆文件proguard-rules.pro中添加以下配置:
-dontoptimize
-dontpreverify
-dontwarn cn.jpush.**
-keep class cn.jpush.** { *; }
-keep class * extends cn.jpush.android.helpers.JPushMessageReceiver { *; }
-dontwarn cn.jiguang.**
-keep class cn.jiguang.** { *; }
七、通过极光官网控制台推送,测试推送结果。
《二》推送方式分析
使用场景分析:推送消息,无疑就是两种情形,一种是全部推送,这种方式简单,群发就行了,Portal与API都支持向指定的 appKey 群发消息。但是一般实际的业务需求,都不仅仅是群发,还需要针对某一个人或者某一群人进行推送。例如:给会员推送一些新的内容,此时就需要针对会员这一群特定的人,来进行推送。简单的说,就是需要把JPush的注册用户与开发者App用户绑定起来,以达到精准推送的目的。
1.RegistrationID方式实现点对点的精准推送(把绑定关系保存到开发者应用服务器中)
集成了 JPush SDK 的应用程序在第一次成功注册到 JPush 服务器时,JPush 服务器会以广播的形式发送RegistrationID到应用程序,给客户端返回一个唯一的该设备的标识 - RegistrationID。首次注册成功,自定义的MyReceiver中会收到一条广播。
如下图,会在MyReceiver中收到RegistrationID的值。只要极光推送第一次注册成功了,后期不会再发 RegistrationID 的广播了。RegistrationID 就会被保留在手机内,下次即使你是无网状态进入APP,你也可以获取到这个RegistrationID。有了这个标识,App 编程可以把这个 RegistrationID 保存到自己的应用服务器上,然后就可以根据 RegistrationID 来向设备推送消息或者通知。所以建议可以在你的Application和MyReceiver中都对这个RegistrationID进行赋值。然后根据项目的业务需要,将RegistrationID和用户标识的对应关系,上传自己的服务端。
public class MyApplication extends Application{
public static String registrationID;
@Override
public void onCreate() {
// 初始化 JPush
JPushInterface.init(this);
registrationID = JPushInterface.getRegistrationID(this);
Log.d("TAG", "接收Registration Id : " + registrationID);
}
}
【注意】:
如果 App 不卸载,是直接覆盖安装,Android, iOS 上 RegistrationID 的值都不会变化。
如果 App 是卸载之后再次安装:Android 上 RegistrationID 基本不会变;
iOS 上如果启用了 IDFA 变化可能性不大,如果未启用 IDFA 则每次安装 RegistrationID 都会变;
参考:极光推送的设备唯一性标识 RegistrationID
如果使用此种推送方式,你可能会遇到的问题:假设在一个设备中登录不同的账号,那此时上传给服务器的都是同一个RegistrationID,因为设备没有变化。那么可能出现:本来服务器是根据A用户找到它的RegistrationID进行推送,但是B用户也是在A用户登录的设备登录的,RegistrationID跟A的一样,这样就导致B用户收到了推送给A用户的推送内容。
需要谨记:
使用 RegistrationID 推送的关键于,App 开发者需要在开发 App 时,获取到这个 RegistrationID,保存到 App 业务服务器上去,并且与自己的用户标识对应起来。建议 App 开发者尽可能做这个保存动作。因为这是最精确地定位到设备的。(RegistrationID 的方式,相对而言比较麻烦一点,因为需要自己的App服务端维护RegistrationID和用户的对应关系。暂时我还没有使用这种方式,真正投入到线上的项目,只是测试环境下调试过。)
值得一读:推送人群的选择 – 推送方式-技术篇
2.别名与标签推送(把绑定关系保存到 JPush 服务器端)
别名推送也是一种实现点对点推送的方式,用于给某特定用户推送消息。
功能介绍:
①为安装了应用程序的用户,取个别名来标识。以后给该用户 Push 消息时,就可以用此别名来指定。
②每个用户只能指定一个别名。
③同一个应用程序内,对不同的用户,建议取不同的别名。这样,尽可能根据别名来唯一确定用户。
④系统不限定一个别名只能指定一个用户。如果一个别名被指定到了多个用户,当给指定这个别名发消息时,服务器端API会同时给这多个用户发送消息。
举例:在一个用户要登录的游戏中,可能设置别名为 userid。游戏运营时,发现该用户 3 天没有玩游戏了,则根据 userid 调用服务器端API发通知到客户端提醒用户。
别名设置:
需要和自己的服务端协商好别名的规则,例如下面的代码中,是将用户ID通过MD5加密,作为别名,设置保存到JPush服务器。当然你也可以使用其他的拼接规则,具体根据每个公司的项目需要设置即可,只要满足别名的命名限制就行: 命名长度限制为 40 字节。(判断长度需采用UTF-8编码)
深入理解各种推送方式可以参考:推送人群的选择 – 推送方式-技术篇
下面是一个JPush设置别名和标签的辅助类。
public class JPushHelper {
private String TAG = "JPushHelper";
/**
* 设置别名与标签
*
* @param UUID
*/
private void setAlias(String UUID) {
if (null != UUID) {
//恢复接收推送
JPushInterface.resumePush(MyApplication.getInstance());
JPushInterface.setAliasAndTags(MyApplication.getInstance(), UUID, null, mAliasCallback);
}
}
public void setAlias() {
if (MyApplication.isLogin) { //必须登录
UserInfo userBean = ACT_Login.getLoginUser();
if (null != userBean) {
//根据具体业务需求,可以再拼接上其他相关字段
//String alias = "";
//StringBuilder stringBuffer = new StringBuilder();
// stringBuffer.append(StringUtil.getString(userBean.user_id));
Log.e(TAG, stringBuffer.toString());
alias = MD5Util.string2MD5(userBean.user_id);
//限制:alias 命名长度限制为 40 字节。(判断长度需采用UTF-8编码)
Log.e(TAG, alias);
//setAlias("");
setAlias(alias);
}
}
}
/**
* 停止接收推送
*/
public void removeAlias() {
JPushInterface.clearAllNotifications(MyApplication.getInstance());
//setAlias("");//该句打开的话,如果退出登录后,用户就收不到离线的消息了。
JPushInterface.stopPush(MyApplication.getInstance());
}
private final TagAliasCallback mAliasCallback = new TagAliasCallback() {
@Override
public void gotResult(int code, String alias, Set<String> tags) {
String logs;
switch (code) {
case 0:
// 建议这里往 SharePreference 里写一个成功设置的状态。成功设置一次后,以后不必再次设置了。
logs = "Set tag and alias success";
Log.e(TAG, logs);
break;
case 6002:
logs = "Failed to set alias and tags due to timeout. Try again after 60s.";
// 延迟 60 秒来调用 Handler 设置别名
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SET_ALIAS, alias), 1000 * 6);
Log.e(TAG, logs + AppDateUtil.getTimeStamp(System.currentTimeMillis(), AppDateUtil.MM_DD_HH_MM_SS));
break;
default:
logs = "Failed with errorCode = " + code;
Log.e(TAG, logs);
}
}
};
private static final int MSG_SET_ALIAS = 1001;
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(android.os.Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MSG_SET_ALIAS:
Log.e(TAG, "Set alias in handler." + ((String) msg.obj));
// 调用 JPush 接口来设置别名。
JPushInterface.setAliasAndTags(MyApplication.getInstance(),
(String) msg.obj,
null,
mAliasCallback);
break;
default:
Log.e(TAG, "Unhandled msg - " + msg.what);
}
}
};
// 校验Tag Alias 只能是数字,英文字母和中文
public static boolean isValidTagAndAlias(String s) {
Pattern p = Pattern.compile("^[\u4E00-\u9FA50-9a-zA-Z_!@#$&*+=.|]+$");
Matcher m = p.matcher(s);
return m.matches();
}
}
3.关于后端服务器设置别名还是前端设置别名:
说实在的,以前没有遇到过这个选择题,因为之前做的极光推送都是在我们客户端设置,不过最近调试JPush的时候后台说他设置别名,我突然就有点蒙了?不是一般都是前端设置吗???
于是有个疑问:如果是服务端设置别名,那服务端也没有经过客户端,那极光服务器咋通过别名来匹配用户进行点对点推送呀???我一下子有点想不通了。然后查了一下文档和相关博客,确实极光服务器也给后端服务提供设置别名的API,又问了一下后端开发,他是不是只是单单设置了别名,还是在设备ID上也有做了处理,因为没有映射关系,极光服务器也不可能找到对应的用户进行推送呀。果不其然,他是在RegistrationID上设置的别名,这下我就明白了。
不过大部分的情况,应该还是前端设置别名吧,毕竟那些登入登出的操作在前端控制会比较方便。
小提醒:
实际应用场景,客户端一般都需要在登录到App成功后,设置别名,恢复接收推送。退出登录后,停止接收推送。
恢复接收推送:
JPushInterface.resumePush(MyApplication.getInstance());
停止接收推送:
JPushInterface.stopPush(MyApplication.getInstance());
4.App杀死后还想要收到推送
有的公司由于业务需求,可能会要求你,App杀死后还想要收到推送。如果知道JPush Android SDK 是作为 Android Service 长期运行在后台的,从而创建并保持长连接,保持永远在线的能力...的童鞋们,那么必须要清楚一点的是:如果APP真的被杀死了,是不可能收到推送的,如果杀死了还能收到,那说明可能是以下几种情况:
①应用自启动了
②要么是其他方式将App拉起来了。
③你压根就没有杀死App,Service还在运行着。(有的手机清理App,并没有完全杀死进程的)
关于这个问题可以看看这位小姐姐的总结:Android 关于App被杀死后,如何接收极光推送
5.项目实战中各个设备的推送到达率问题
1、oppo等手机安装软件后通知权限默认关闭的问题、Android:检查通知权限并跳转到通知设置界面
阅读参考:
官网集成
极光推送Android端API
详解极光推送的 4 种消息形式 —— Android 篇
常见问题 - JPush 合集(持续更新)