项目业务需求,需要将后台服务进行保活。通过开启远程服务与APP的进程进行进程间通信(IPC),寻求保活的方式花了一段时间,最后对MIUI的系统机制还是无果,Debug的时候发现MIUI拥有一个PowerKeeper,一旦触发就会对任何后台进程的APP(据说有白名单)进行KillApplication操作,在我的压力测试下,无一应用幸免(包括优化得极其稳定的Bilibili,GooglePlay录屏APP排行第一的AZ ScreenRecorder)。
在Android实现录屏直播(二)需求才是硬道理之产品功能调研一文中我们实现了通过启动一个Service来进行悬浮窗的管理,效果似乎也还不错,但其中我们只是开启了一个普通服务,并没有提到该服务的优先级和后台存活率,所以可以想象在系统进行内存释放的时候的结局,悬浮窗突然没了。于是我们需要将服务存活率提高(除非类似多媒体播放器等这种需求,不然如果是要强行保活,各种后台资源占用的话,这种缺心眼的事还是少做吧,Android环境需要我们开发者一起慢慢建立完善,Android一天天更流畅用户也会感激我们的)。
关于服务的更多概念和绑定服务的使用还请参阅官方文档(早已经有中文版了,没有阅读障碍):
- 服务
- 绑定服务
将服务改为前台服务
前台服务被认为是用户主动意识到的一种服务,因此在内存不足时,系统也不会考虑将其终止。 前台服务必须为状态栏提供通知,放在“正在进行”标题下方,这意味着除非服务停止或从前台移除,否则不能清除通知。
例如,应该将通过服务播放音乐的音乐播放器设置为在前台运行,这是因为用户明确意识到其操作。 状态栏中的通知可能表示正在播放的歌曲,并允许用户启动 Activity 来与音乐播放器进行交互。
要请求让服务运行于前台,请调用 startForeground()。此方法采用两个参数:唯一标识通知的整型数和状态栏的 Notification。
要从前台移除服务,请调用 stopForeground()。此方法采用一个布尔值,指示是否也移除状态栏通知。 此方法不会停止服务。 但是,如果您在服务正在前台运行时将其停止,则通知也会被移除。
如需了解有关通知的详细信息,请参阅创建状态栏通知。
前面Demo中的Service并没有设置setForeground属性,需要我们手动设置该方法。前台服务会在通知栏有一个图标,这样用户能知道我们的APP在后台运行着,心里也有个底,以免不法分子悄悄录屏盗取隐私都不知道。
private void setupRecordingNotification(String notificationContent) {
startForeground(FOREGROUND_ID, createNotification(notificationContent));
}
private Notification createNotification(String notificationContent) {
builder = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.icon)
.setContentTitle(getResources().getString(R.string.app_name))
.setContentText(notificationContent)
.setOngoing(true)
.setDefaults(Notification.DEFAULT_VIBRATE);
// 点击回到对应的Activity
Intent backIntent = new Intent(this, RecordActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0x01, backIntent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(pendingIntent);
return builder.build();
}
上面代码中我们startForeground()时需要传入一个FOREGROUND_ID,也就是通知栏的标识ID,不能为0。第二个参数是Notification对象,createNotification()方法中我们创建了一个Notification,Notification是通过建造器模式(Builder)创建的。这样我们就将服务设置为前台服务了。
启动一个Remote Service
由于之前的Service会与APP的其他资源共同生存与同一个进程中,于是可能APP的进程被杀死后Service也被干掉了,于是我们可以将Service独立出来生成到独立的进程中,并且这样的好处还可以让其他的第三方应用启动我们的服务或者进行进程间通信(IPC)互相传递数据。
AndroidManifest中如下所示:
<service
android:name=".ui.service.MyService"
android:enabled="true"
android:exported="false"
android:process=":remote" /> <!-- 加入该行,并改为:remote,注意冒号 -->
既然都是IPC了,那么我们需要使用AIDL。
使用 AIDL
AIDL(Android 接口定义语言)执行所有将对象分解成原语的工作,操作系统可以识别这些原语并将它们编组到各进程中,以执行 IPC。 之前采用 Messenger 的方法实际上是以 AIDL 作为其底层结构。 如上所述,Messenger 会在单一线程中创建包含所有客户端请求的队列,以便服务一次接收一个请求。 不过,如果您想让服务同时处理多个请求,则可直接使用 AIDL。 在此情况下,您的服务必须具备多线程处理能力,并采用线程安全式设计。如需直接使用 AIDL,您必须创建一个定义编程接口的 .aidl 文件。Android SDK 工具利用该文件生成一个实现接口并处理 IPC 的抽象类,您随后可在服务内对其进行扩展。
注:大多数应用“都不会”**使用 AIDL 来创建绑定服务,因为它可能要求具备多线程处理能力,并可能导致实现的复杂性增加。因此,AIDL 并不适合大多数应用,本文也不会阐述如何将其用于您的服务。如果您确定自己需要直接使用 AIDL,请参阅 AIDL 文档。
官方的AIDL文档
您必须使用 Java 编程语言构建 .aidl 文件。每个 .aidl 文件都必须定义单个接口,并且只需包含接口声明和方法签名。
默认情况下,AIDL 支持下列数据类型:
- Java 编程语言中的所有原语类型(如 int、long、char、boolean 等等)
- String
- CharSequence
- List``List 中的所有元素都必须是以上列表中支持的数据类型、其他 AIDL 生成的接口或您声明的可打包类型。 可选择将 List 用作“通用”类(例如,List)。另一端实际接收的具体类始终是 ArrayList,但生成的方法使用的是 List 接口。
- Map``Map 中的所有元素都必须是以上列表中支持的数据类型、其他 AIDL 生成的接口或您声明的可打包类型。 不支持通用 Map(如 Map 形式的 Map)。 另一端实际接收的具体类始终是 HashMap,但生成的方法使用的是 Map 接口。
您必须为以上未列出的每个附加类型加入一个 import 语句,即使这些类型是在与您的接口相同的软件包中定义。
定义服务接口时,请注意:
- 方法可带零个或多个参数,返回值或空值。
- 所有非原语参数都需要指示数据走向的方向标记。可以是 in、out 或 inout(见以下示例)。原语默认为 in,不能是其他方向。注意:您应该将方向限定为真正需要的方向,因为编组参数的开销极大。
- .aidl 文件中包括的所有代码注释都包含在生成的 IBinder 接口中(import 和 package 语句之前的注释除外)
- 只支持方法;您不能公开 AIDL 中的静态字段。
可以通过Android Studio的自动创建功能创建相应的.aidl文件。此时IDE会在main/aidl/中创建一个包名一致的.aidl文件,其中有一个默认的方法basicTypes()。
// IScreenRecorderAidlInterface.aidl
package tech.shutu.screenrecord;
// Declare any non-default types here with import statements
import tech.shutu.screenrecord.model.bean.DanmakuBean;
import android.content.Intent;
interface IScreenRecorderAidlInterface {
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
void sendDanmaku(in DanmakuBean danmakuBean);
void startScreenRecord(in Intent bundleData); //
}
此时我们还需要Build一次项目,过程中IDE会生成一个与.aidl文件对应的Java文件(AIDL文件中是以接口的形式存在的,Java中相当于实现了该接口)。
在我们的Service类中实现接口并与在onBind方法中与Binder进行关联:
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
private IScreenRecorderAidlInterface.Stub mBinder = new IScreenRecorderAidlInterface.Stub() {
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
}
@Override
public void setDanmakuData(DanmakuBean danmakuBean) {
List<DanmakuBean> list = new ArrayList<>();
list.add(danmakuBean);
MyWindowManager.getSmallWindow().setDataToList(list);
}
@Override
public void startScreenRecord(Intent it) throws RemoteException {
startRecord(it);
}
};
这样就可以进行APP进程和远程服务进程的通信了,其实官方现有的文档已经说明的很详细了。最后还是记录一下让自己能够有所巩固。