Android之service保活

这段时间做的项目需要service保活,也就是实时监听设备的通知栏消息,并可以捕获到通知的内容,然后进行对应的操作。之前尝试过很多方式,最后感觉前台service对于服务保活相对好使(据说这个微信也用过的方案),知情者可能要问了:前台service不是有个通知栏一直显示么?这样对用户来说不是很好。我们这里可以使用两个service互调来实现不显示通知栏,原理如下:

对于 API level < 18 :调用startForeground(ID, new Notification()),发送空的Notification ,图标则不会显示。
对于 API level >= 18:在需要提优先级的service A启动一个InnerService,两个服务同时startForeground,且绑定同样的 ID。Stop 掉InnerService ,这样通知栏图标即被移除。
前台服务类:

public class ForeService extends Service {

    private final int FORESERVICE_PID = android.os.Process.myPid();
    private AssistServiceConnection mConnection;


    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();

        /**
         * 之前的额前台service(会显示通知栏)
         */
/*        //定义一个notification
        Notification.Builder builder1 = new Notification.Builder(this);
        builder1.setSmallIcon(R.mipmap.ic_launcher); //设置图标
//        builder1.setTicker("新消息");
        builder1.setContentTitle("My title"); //设置标题
        builder1.setContentText("My content"); //消息内容
//        builder1.setContentInfo("");//补充内容
//        builder1.setWhen(System.currentTimeMillis()); //发送时间
//        builder1.setDefaults(Notification.DEFAULT_ALL); //设置默认的提示音,振动方式,灯光
//        builder1.setAutoCancel(true);//打开程序后图标消失
        Intent intent = new Intent(this, MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
        builder1.setContentIntent(pendingIntent);
        Notification notification1 = builder1.build();
        //把该service创建为前台service
        startForeground(1, notification1);*/

        setForeground();
    }


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }

    private void setForeground() {
        //如果sdk<18 , 直接调用startForeground即可,不会在通知栏创建通知
        if (Build.VERSION.SDK_INT < 18) {
            this.startForeground(FORESERVICE_PID, getNotification());
            return;
        }

        if (null == mConnection) {
            mConnection = new AssistServiceConnection();
        }

        this.bindService(new Intent(this, AssistService.class), mConnection,
                Service.BIND_AUTO_CREATE);
    }

    public Notification getNotification() {
        //定义一个notification
        Notification.Builder builder1 = new Notification.Builder(this);
        builder1.setSmallIcon(R.mipmap.ic_launcher); //设置图标
//        builder1.setTicker("新消息");
        builder1.setContentTitle("My title"); //设置标题
        builder1.setContentText("My content"); //消息内容
//        builder1.setContentInfo("");//补充内容
//        builder1.setWhen(System.currentTimeMillis()); //发送时间
//        builder1.setDefaults(Notification.DEFAULT_ALL); //设置默认的提示音,振动方式,灯光
//        builder1.setAutoCancel(true);//打开程序后图标消失
        Intent intent = new Intent(this, MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
        builder1.setContentIntent(pendingIntent);
        Notification notification1 = builder1.build();
        return notification1;
    }


    private class AssistServiceConnection implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder binder) {
            // sdk >=18 的,会在通知栏显示service正在运行,这里不要让用户感知,所以这里的实现方式是利用2个同进程的service,利用相同的notificationID,
            // 2个service分别startForeground,然后只在1个service里stopForeground,这样即可去掉通知栏的显示
            Service assistService = ((AssistService.LocalBinder) binder)
                    .getService();
            ForeService.this.startForeground(FORESERVICE_PID, getNotification());
            assistService.startForeground(FORESERVICE_PID, getNotification());
            assistService.stopForeground(true);

            ForeService.this.unbindService(mConnection);
            mConnection = null;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    }
}
辅佐服务类:

public class AssistService extends Service {

    public class LocalBinder extends Binder {
        public AssistService getService() {
            return AssistService.this;
        }
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new LocalBinder();
    }
}
然后开启前台service即可。


下面给介绍一种新的保活方案:NotificationListenerService。NotificationListenerService就是一个监听通知的服务,只要手机收到了通知,NotificationListenerService都能监听到,即时用户把进程杀死,也能重启。

NotificationListenerService 的使用范围也挺广的,比如我们熟知的抢红包,智能手表同步通知,通知栏去广告工具等,都是利用它来完成的。所以,我也想赶时髦地好好利用这把“利器”。最后方案也就出来了:在 Android 4.3 以下(API < 18)使用 AccessibilityService 来读取新通知,在 Android 4.3 及以上(API >= 18)使用 NotificationListenerService 来满足需求。

NotificationListenerService

在这里,我们就做一个小需求:实时检测微信的新通知,如果该通知是微信红包的话,就进入微信聊天页面。

首先创建一个 WeChatNotificationListenerService 继承 NotificationListenerService 。然后在 AndroidManifest.xml 中进行声明相关权限和 <intent-filter> :

1
2
3
4
5
6
7
<service android:name="com.yuqirong.listenwechatnotification.WeChatNotificationListenerService"
android:label="@string/app_name"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">

<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>

然后一般会重写下面这三个方法:

  • onNotificationPosted(StatusBarNotification sbn) :当有新通知到来时会回调;
  • onNotificationRemoved(StatusBarNotification sbn) :当有通知移除时会回调;
  • onListenerConnected() :当 NotificationListenerService 是可用的并且和通知管理器连接成功时回调。

onNotificationPosted(StatusBarNotification sbn)

下面我们来看看 NotificationListenerService 中的重点: onNotificationPosted(StatusBarNotification sbn) 方法。

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
43
44
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
// 如果该通知的包名不是微信,那么 pass 掉
if (!"com.tencent.mm".equals(sbn.getPackageName())) {
return;
}
Notification notification = sbn.getNotification();
if (notification == null) {
return;
}
PendingIntent pendingIntent = null;
// 当 API > 18 时,使用 extras 获取通知的详细信息
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Bundle extras = notification.extras;
if (extras != null) {
// 获取通知标题
String title = extras.getString(Notification.EXTRA_TITLE, "");
// 获取通知内容
String content = extras.getString(Notification.EXTRA_TEXT, "");
if (!TextUtils.isEmpty(content) && content.contains("[微信红包]")) {
pendingIntent = notification.contentIntent;
}
}
} else {
// 当 API = 18 时,利用反射获取内容字段
List<String> textList = getText(notification);
if (textList != null && textList.size() > 0) {
for (String text : textList) {
if (!TextUtils.isEmpty(text) && text.contains("[微信红包]")) {
pendingIntent = notification.contentIntent;
break;
}
}
}
}
// 发送 pendingIntent 以此打开微信
try {
if (pendingIntent != null) {
pendingIntent.send();
}
} catch (PendingIntent.CanceledException e) {
e.printStackTrace();
}
}

从上面的代码可知,对于分析 Notification 的内容分为了两种:

  • 当 API > 18 时,利用 Notification.extras 来获取通知内容。extras 是在 API 19 时被加入的;
  • 当 API = 18 时,利用反射获取 Notification 中的内容。具体的代码在下方。
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
43
44
45
46
public List<String> getText(Notification notification) {
if (null == notification) {
return null;
}
RemoteViews views = notification.bigContentView;
if (views == null) {
views = notification.contentView;
}
if (views == null) {
return null;
}
// Use reflection to examine the m_actions member of the given RemoteViews object.
// It's not pretty, but it works.
List<String> text = new ArrayList<>();
try {
Field field = views.getClass().getDeclaredField("mActions");
field.setAccessible(true);
@SuppressWarnings("unchecked")
ArrayList<Parcelable> actions = (ArrayList<Parcelable>) field.get(views);
// Find the setText() and setTime() reflection actions
for (Parcelable p : actions) {
Parcel parcel = Parcel.obtain();
p.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
// The tag tells which type of action it is (2 is ReflectionAction, from the source)
int tag = parcel.readInt();
if (tag != 2) continue;
// View ID
parcel.readInt();
String methodName = parcel.readString();
if (null == methodName) {
continue;
} else if (methodName.equals("setText")) {
// Parameter type (10 = Character Sequence)
parcel.readInt();
// Store the actual string
String t = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel).toString().trim();
text.add(t);
}
parcel.recycle();
}
} catch (Exception e) {
e.printStackTrace();
}
return text;
}

凭着 onNotificationPosted(StatusBarNotification sbn) 方法就已经可以完成监听微信通知并打开的动作了。下面我们来看一下其他关于 NotificationListenerService 的二三事。

取消通知

有了监听,NotificationListenerService 自然提供了可以取消通知的方法。取消通知的方法有:

  • cancelNotification(String key) :是 API >= 21 才可以使用的。利用 StatusBarNotification 的 getKey() 方法来获取 key 并取消通知。
  • cancelNotification(String pkg, String tag, int id) :在 API < 21 时可以使用,在 API >= 21 时使用此方法来取消通知将无效,被废弃。

最后,取消通知的方法:

1
2
3
4
5
6
7
public void cancelNotification(StatusBarNotification sbn) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cancelNotification(sbn.getKey());
} else {
cancelNotification(sbn.getPackageName(), sbn.getTag(), sbn.getId());
}
}

检测通知监听服务是否被授权

1
2
3
4
5
6
7
public boolean isNotificationListenerEnabled(Context context) {
Set<String> packageNames = NotificationManagerCompat.getEnabledListenerPackages(this);
if (packageNames.contains(context.getPackageName())) {
return true;
}
return false;
}

打开通知监听设置页面

1
2
3
4
5
6
7
8
9
10
11
12
13
public void openNotificationListenSettings() {
try {
Intent intent;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
} else {
intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
}
startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
}
}

被杀后再次启动时,监听不生效的问题

这个问题来源于知乎问题: NotificationListenerService不能监听到通知,研究了一天不知道是什么原因?

从问题的回答中可以了解到,是因为 NotificationListenerService 被杀后再次启动时,并没有去 bindService ,所以导致监听效果无效。

最后,在回答中还给出了解决方案:利用 NotificationListenerService 先 disable 再 enable ,重新触发系统的 rebind 操作。代码如下:

1
2
3
4
5
6
7
private void toggleNotificationListenerService() {
PackageManager pm = getPackageManager();
pm.setComponentEnabledSetting(new ComponentName(this, com.fanwei.alipaynotification.ui.AlipayNotificationListenerService.class),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
pm.setComponentEnabledSetting(new ComponentName(this, com.fanwei.alipaynotification.ui.AlipayNotificationListenerService.class),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
}

该方法使用前提是 NotificationListenerService 已经被用户授予了权限,否则无效。另外,在自己的小米手机上实测,重新完成 rebind 操作需要等待 10 多秒(我的手机测试过大概在 13 秒左右)。幸运的是,官方也已经发现了这个问题,在 API 24 中提供了 requestRebind(ComponentName componentName) 方法来支持重新绑定。

AccessibilityService

讲完了 NotificationListenerService 之后,按照前面说的那样,在 API < 18 的时候使用 AccessibilityService 。

同样,创建一个 WeChatAccessibilityService ,并且在 AndroidManifest.xml 中进行声明:

1
2
3
4
5
6
7
8
9
10
11
<service
android:name="com.yuqirong.listenwechatnotification.WeChatAccessibilityService"
android:label="@string/app_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessible_service_config" />
</service>

声明之后,还要对 WeChatAccessibilityService 进行配置。需要在 res 目录下新建一个 xml 文件夹,在里面新建一个 accessible_service_config.xml 文件:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeNotificationStateChanged"
android:accessibilityFeedbackType="feedbackAllMask"
android:accessibilityFlags="flagIncludeNotImportantViews"
android:canRetrieveWindowContent="true"
android:description="@string/app_name"
android:notificationTimeout="100"
android:packageNames="com.tencent.mm" />

最后就是代码了:

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
public class WeChatAccessibilityService extends AccessibilityService {

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
if (Build.VERSION.SDK_INT < 18) {
Notification notification = (Notification) event.getParcelableData();
List<String> textList = getText(notification);
if (textList != null && textList.size() > 0) {
for (String text : textList) {
if (!TextUtils.isEmpty(text) &&
text.contains("[微信红包]")) {
final PendingIntent pendingIntent = notification.contentIntent;
try {
if (pendingIntent != null) {
pendingIntent.send();
}
} catch (PendingIntent.CanceledException e) {
e.printStackTrace();
}
}
break;
}
}
}
}

@Override
public void onInterrupt() {

}

}

看了一圈 WeChatAccessibilityService 的代码,发现和 WeChatNotificationListenerService 在 API < 18 时处理的逻辑是一样的,getText(notification) 方法就是上面那个,在这里就不复制粘贴了,基本没什么好讲的了。

有了 WeChatAccessibilityService 之后,在 API < 18 的情况下也能监听通知啦。\(^ο^)/

我们终于实现了当初许下的那个需求了。 cry …

总结

除了监听通知之外,AccessibilityService 还可以进行模拟点击、检测界面变化等功能。具体的可以在 GitHub 上搜索抢红包有关的 Repo 进行深入学习。

而 NotificationListenerService 的监听通知功能更加强大,也更加专业。在一些设备上,如果 NotificationListenerService 被授予了权限,那么可以做到该监听进程不死的效果,也算是另类的进程保活。

源码下载:ListenWeChatNotification.rar

### 关于Android Service的最佳实践 #### 前置条件与背景 在现代Android开发中,应用的后台存能力是一个重要话题。尽管Google通过引入前台服务和其他机制试图限制过度占用资源的应用程序行为,开发者仍然可以通过一些技术手段提高应用程序的存率。需要注意的是,这些方法可能因不同厂商定制系统的差异而表现各异。 --- #### 权限声明与基础配置 为了实现Service功能,首先需要在`AndroidManifest.xml`文件中正确声明必要的权限和服务组件: ```xml <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <service android:name=".MyJobDaemonService" android:enabled="true" android:exported="true" android:permission="android.permission.BIND_JOB_SERVICE" /> ``` 以上代码片段展示了如何定义一个可绑定的服务,并赋予其必要权限[^2]。 --- #### 使用前台服务延长生命周期 前台服务是一种有效的持应用跃的方法。它允许应用显示通知栏消息,从而告知用户当前正在进行的任务。虽然这种方法并不能完全阻止被杀死的可能性,但在大多数情况下能够显著提升服务的优先级。 启动前台服务的关键在于调用`startForeground()`方法,并提供相应的Notification对象: ```java public class MyForegroundService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { Notification notification = new NotificationCompat.Builder(this, "CHANNEL_ID") .setContentTitle("Running in Background") .setContentText("This is a foreground service.") .setSmallIcon(R.drawable.ic_notification) .build(); startForeground(1, notification); // 启动前台服务 return START_STICKY; } @Nullable @Override public IBinder onBind(Intent intent) { return null; } } ``` 此代码实现了基本的前台服务逻辑,确即使系统内存不足时也能维持较高的存概率[^1]。 --- #### 利用JobScheduler增强稳定性 除了传统的Service外,还可以借助`JobScheduler` API来安排周期性的任务执行。这种方式特别适合那些不需要实时响应但希望长期运行的操作。 创建自定义的`JobService`类如下所示: ```java public class MyJobService extends JobService { @Override public boolean onStartJob(JobParameters params) { // 开始处理工作 performWork(); jobFinished(params, false); return true; } private void performWork() { Log.d("JOB_SCHEDULER", "Executing background task..."); } @Override public boolean onStopJob(JobParameters params) { return false; // 不重新调度该作业 } } ``` 随后可以在适当位置注册这个任务计划器实例: ```java ComponentName componentName = new ComponentName(context, MyJobService.class); JobInfo jobInfo = new JobInfo.Builder(JOB_ID, componentName) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) .setPersisted(true) .setPeriodic(TimeUnit.HOURS.toMillis(1)) .build(); JobScheduler scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); scheduler.schedule(jobInfo); ``` 利用上述方式可以有效减少因系统清理而导致的服务中断风险[^2]。 --- #### 处理极端情况下的进程恢复 考虑到部分手机制造商实施了额外的安全措施(如强制关闭长时间未使用的后台进程),单纯依赖单一解决方案往往难以奏效。此时可通过以下几种补充策略进一步巩固效果: 1. **多进程架构设计** 将核心业务拆分至独立进程中运行,即便主界面所在的UI线程遭到销毁也不会影响其他模块正常运作[^3]。 2. **广播接收器监听事件** 注册特定类型的BroadcastReceiver以便及时感知环境变化并作出反应。例如开机完成后自动重启指定组件等功能均能借此达成。 3. **本地守护进程配合远程唤醒机制** 结合JNI层面上的技术构建跨平台兼容性强的新实体监控目标状态;一旦发现异常即刻触发重连流程完成自我修复动作[^4]。 --- ### 技术局限性说明 值得注意的一点是,任何企图绕过官方框架设定的行为都存在违反政策的风险。随着版本迭代推进以及监管力度加大,未来此类技巧可能会面临更多阻碍甚至失效局面。因此建议仅当确实有必要才考虑采用类似做法,同时密切关注最新动态调整部署方案。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值