本文简介:本文前篇,可以帮助朋友们快速集成极光推送。本文后篇,是我自己项目实践的一些总结和心得,应该对读者们还是很有参考价值的,相信读完这篇文章,你会对极光推送有更加深入的理解,而不仅仅只是会集成而已。总之呢,集成第三方SDK,都不是很难的事情,仔细阅读文档,一步步来,遇到Bug,慢慢解决就行,实在解决不了,可以问问客服小哥哥或者小姐姐,重要的是,你得有着解决它的决心和耐心。
《一》JPush SDK的集成
简要介绍:
极光推送(JPush)是一个端到端的推送服务,使得服务器端消息能够及时地推送到终端用户手机上,让开发者积极地保持与用户的连接,从而提高用户活跃度、提高应用的留存率。
开发者集成 JPush Android SDK 到其应用里,JPush Android SDK 创建到 JPush Cloud 的长连接,为 App 提供永远在线的能力。 当开发者想要及时地推送消息到达 App 时,只需要调用 JPush API 推送,或者使用其他方便的智能推送工具,即可轻松与用户交流。
JPush Android SDK 是作为 Android Service 长期运行在后台的,从而创建并保持长连接,保持永远在线的能力。
假设你已经注册了极光的账号,登录进来了。点击立即体验,创建自己的应用。
![img_fd763efcd717770649e38dcdf9e6d0a6.png](https://yqfile.alicdn.com/img_fd763efcd717770649e38dcdf9e6d0a6.png?x-oss-process=image/resize,w_1400/format,webp)
![img_b10fd3e208c9c4a9ffca4031269566dd.png](https://yqfile.alicdn.com/img_b10fd3e208c9c4a9ffca4031269566dd.png?x-oss-process=image/resize,w_1400/format,webp)
App端集成需要用到的AppKey。
![img_d9632deec4f6279e4149c666585edf13.png](https://yqfile.alicdn.com/img_d9632deec4f6279e4149c666585edf13.png?x-oss-process=image/resize,w_1400/format,webp)
添加集成代码,此处使用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.** { *; }
七、通过极光官网控制台推送,测试推送结果。
![img_64ac3d1cecc2c49488de7a59edc55268.png](https://yqfile.alicdn.com/img_64ac3d1cecc2c49488de7a59edc55268.png?x-oss-process=image/resize,w_1400/format,webp)
《二》推送方式分析
使用场景分析:推送消息,无疑就是两种情形,一种是全部推送,这种方式简单,群发就行了,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和用户标识的对应关系,上传自己的服务端。
![img_7a420cf9fc1d85c7edc95bc1b72d8858.png](https://yqfile.alicdn.com/img_7a420cf9fc1d85c7edc95bc1b72d8858.png?x-oss-process=image/resize,w_1400/format,webp)
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被杀死后,如何接收极光推送
阅读参考:
官网集成
极光推送Android端API
详解极光推送的 4 种消息形式 —— Android 篇
常见问题 - JPush 合集(持续更新)