今天在V2EX上看到有人提到Notification有漏洞,好奇也就查了一下,结果发现有人专门针对这个问题进行了分析。本身的技术分析并不多,写在这里只是为了作为今后的一个参考。
1. 问题的由来
Android对后台应用是有一个权重区分的,最直观的就是查看最近使用的应用,这里每一个应用可能有一个或者多个Process,而系统在资源紧张时会干掉一些Process,而决定后台应用生死的是一个Lru List,也就是least recently used 会被干掉。显然大家都不希望自己被干掉,DAU对于很多应用来说是优先于系统资源和用户体验的。
根据官方文档,Android Process有五种,根据优先级从高到低为:
- 前台进程
- 可见进程
- 服务进程
- 后台进程
- 空进程
越靠前的进程就越不容易被系统干掉,所以大家都希望能够成为前台进程。成为前台进程的条件:
用户当前操作所必需的进程。如果一个进程满足以下任一条件,即视为前台进程:
托管用户正在交互的 Activity(已调用 Activity 的 onResume() 方法)
托管某个 Service,后者绑定到用户正在交互的 Activity
托管正在“前台”运行的 Service(服务已调用 startForeground())
托管正执行一个生命周期回调的 Service(onCreate()、onStart() 或 onDestroy())
托管正执行其 onReceive() 方法的 BroadcastReceiver
通常,在任意给定时间前台进程都为数不多。只有在内在不足以支持它们同时继续运行这一万不得已的情况下,系统才会终止它们。 此时,设备往往已达到内存分页状态,因此需要终止一些前台进程来确保用户界面正常响应。
以上条件只有startForeground满足条件了,但大家都知道startForeground会在通知栏常驻一个Notification,且用户取消不了。对于我这种强迫症来说实在是太丑。
2. startForeground一定会在系统状态栏显示一个通知,真的吗?
void startForeground (int id,
Notification notification)
我找到了G+上的Chris Banes的一篇post,这其中明确指出
Unfortunately there are a number of applications on Google Play which are using the startForeground() API without passing a valid notification. While this worked in previous versions of Android, it is a loophole which has been fixed in Android 4.3. The system now displays a notifications for you automatically if you do not provide a valid one.
也就是说,API 18以前,只需要提供一个无效的Notification就可以让Notification不显示了。所以,判断下API<18的时候,直接new Notification()就可以得到一个不完整的Notification.
文章也指出了这是一个Loophole(已经是个贬义词了)。
Api 18之后的修复措施,看ServiceRecord的源码:
public void postNotification() {
final int appUid = appInfo.uid;
final int appPid = app.pid;
if (foregroundId != 0 && foregroundNoti != null) {
// Do asynchronous communication with notification manager to
// avoid deadlocks.
final String localPackageName = packageName;
final int localForegroundId = foregroundId;
final Notification localForegroundNoti = foregroundNoti;
ams.mHandler.post(new Runnable() {
public void run() {
NotificationManagerService nm =
(NotificationManagerService) NotificationManager.getService();
if (nm == null) {
return;
}
try {
if (localForegroundNoti.icon == 0) {
// It is not correct for the caller to supply a notification
// icon, but this used to be able to slip through, so for
// those dirty apps give it the app's icon.
localForegroundNoti.icon = appInfo.icon;
// Do not allow apps to present a sneaky invisible content view either.
localForegroundNoti.contentView = null;
localForegroundNoti.bigContentView = null;
CharSequence appName = appInfo.loadLabel(
ams.mContext.getPackageManager());
if (appName == null) {
appName = appInfo.packageName;
}
Context ctx = null;
try {
ctx = ams.mContext.createPackageContext(
appInfo.packageName, 0);
Intent runningIntent = new Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
runningIntent.setData(Uri.fromParts("package",
appInfo.packageName, null));
PendingIntent pi = PendingIntent.getActivity(ams.mContext, 0,
runningIntent, PendingIntent.FLAG_UPDATE_CURRENT);
localForegroundNoti.setLatestEventInfo(ctx,
ams.mContext.getString(
com.android.internal.R.string
.app_running_notification_title,
appName),
ams.mContext.getString(
com.android.internal.R.string
.app_running_notification_text,
appName),
pi);
} catch (PackageManager.NameNotFoundException e) {
localForegroundNoti.icon = 0;
}
}
if (localForegroundNoti.icon == 0) {
// Notifications whose icon is 0 are defined to not show
// a notification, silently ignoring it. We don't want to
// just ignore it, we want to prevent the service from
// being foreground.
throw new RuntimeException("icon must be non-zero");
}
int[] outId = new int[1];
nm.enqueueNotificationInternal(localPackageName, localPackageName,
appUid, appPid, null, localForegroundId, localForegroundNoti,
outId, userId);
} catch (RuntimeException e) {
Slog.w(ActivityManagerService.TAG,
"Error showing notification for service", e);
// If it gave us a garbage notification, it doesn't
// get to be foreground.
ams.setServiceForeground(name, ServiceRecord.this,
0, null, true);
ams.crashApplication(appUid, appPid, localPackageName,
"Bad notification for startForeground: " + e);
}
}
});
}
}
单单是看注释大概能看出来Android团队对于这种做法的不满。所以如果不提供有效Notification,则显示你的App的Icon。所以Api 18以上一定会显示一个Notification。
然而套路还是太深。。。。又有人给出了API 18以上的解决办法:
我在这里找到了新的方法,简单来说就是起两个Service,两个Service都在一个进程里。
先Start A Service ,onCreate里面 bind B Service,
在onServiceConnected的时候A service startForeground(processId,notification)
B service startForeground(processId,notification)
随后立即调用B service stopForeGround(true)
由于两个Notification具有相同的id,所以A service最终成为Foreground Service,Notification也被清除掉了。
3.最后
整个过程看下来,API 18以下,给一个不完整的Notification(比如new Notification()),就不会出现在通知栏;API 18以上,起两个Service,B Service负责取消Notification就可以了。
目前看来,国内很多App为了保活,都采取了类似的方式。
而整体技术层面的实现并不难,只是利用了一个又一个小漏洞罢了。
所谓脏代码不过是技术上做的一些欺骗系统的手段,作为开发者,理应明白谷歌设计这一套系统是为了更好的提升用户体验(占据市场)。然而在当前国内应用开发环境下,我们真的能够为用户考虑考虑吗,或者说,我们提交的代码能吗?
updates
CommonsWare表示 Services are natural singletons ,Service都是单例