Android startForeground 却无notification的黑科技原理分析 以及Android7.1的修复

契机:今天升级了Android7.1 beta版。然而升上去之后,国产的部分App简直丑态百出啊,给各位看看我的手机截图
这里写图片描述

啧啧,原来Android7.0以及以下干干净净的通知栏瞬间被这几个家伙占满。有句话说:潮水退去,才知道谁在裸泳啊。同样的,系统升级修复漏洞后,才赤果果地暴露出吃相呢。

开始进入正题:
startForeground啥效果我就不啰嗦了。
而国内大部分主流应用,其实都使用了Android的一个通知栏的bug,使得在调用startForegound之后在通知栏没有任何通知,而且进程同样处于低oom_adj状态。直到Android7.1才修复了这个漏洞。

首先是怎么做:这里代码参考https://github.com/D-clock/AndroidDaemonService
首先是一个主service,在这个service里的onstartCommand里头启动一个临时的GrayInnerService

Intent innerIntent = new Intent(this, GrayInnerService.class);
startService(innerIntent);
startForeground(GRAY_SERVICE_ID, new Notification());

随后在GrayInnerService的onstartCommand里头

startForeground(GRAY_SERVICE_ID, new Notification());
//stopForeground(true);
stopSelf();
return super.onStartCommand(intent, flags, startId);

看起来十分容易,总结关键点就是 一个进程里头的两个service同时用同一个ID来startForeground,然后其中一个自杀,就OK了。

原理的话也相当简单:
因为Android没有针对startForeground的ID的唯一性做判定,然后两个service对应了一个notification,然后其中一个自杀,会把这个Notification带走,所以我们看不见notification了,但是另一个处于foreground的service依然存活着!,只要存在一个foreground的service,那么这个进程的oomadj的值就比较底,就不容易被杀死

代码分析如下:

    public final void startForeground(int id, Notification notification) {
        try {
            mActivityManager.setServiceForeground(
                    new ComponentName(this, mClassName), mToken, id,
                    notification, true);
        } catch (RemoteException ex) {
        }
    }

跟踪setServiceForeground

585    public void setServiceForegroundLocked(ComponentName className, IBinder token,
586            int id, Notification notification, boolean removeNotification) {
587        final int userId = serHandle.getCallingUserId();
588        final long origId =Binder.clearCallingIdentity();
589        try {
590            ServiceRecord r = findServiceLocked(className, token, userId);//找到service对应的serviceRecord
591            if (r != null) {
592                if (id != 0) {
593                    if (notification == null) {
594                        throw new IllegalArgumentException("null notification");
595                    }
596                    if (r.foregroundId != id) {//这里并没有对id的进程内的唯一性做检查。只是单存地更新一下ID和notification而已
597                        r.cancelNotification();
598                        r.foregroundId = id;
599                    }
600                    notification.flags |= Notification.FLAG_FOREGROUND_SERVICE;
601                    r.foregroundNoti = notification;
602                    r.isForeground = true;
603                    r.postNotification();
604                    if (r.app != null) {
605                        updateServiceForegroundLocked(r.app, true);//走到这里!
606                    }
607                    getServiceMap(r.userId).ensureNotStartingBackground(r);
.....
}

注意本段代码已经出现了最最重要的关键点,Android只是简单地把startForeground传入的id记录在r.foregroundId ,而没有检查是否id之前是否被其他的foreground service使用过了·

然后调用updatServiceForeground:

630    private void updateServiceForegroundLocked(ProcessRecord proc, boolean oomAdj) {
631        boolean anyForeground = false;
632        for (int i=proc.services.size()-1; i>=0; i--) {
633            ServiceRecord sr = proc.services.valueAt(i);
634            if (sr.isForeground) {
635                anyForeground = true;
636                break;
637            }
638        }
639        mAm.updateProcessForegroundLocked(proc, anyForeground, oomAdj);
640    }

即检查本进程内有任意service为foreground状态,然后依据这个结果进入updateProcessForegroundLocked对进程进行后续调整:

18964    final void updateProcessForegroundLocked(ProcessRecord proc, boolean isForeground,
18965            boolean oomAdj) {
18966        if (isForeground != proc.foregroundServices) {
18967            proc.foregroundServices = isForeground;//更新ProcessRecord的foregroundServices
18968            ArrayList<ProcessRecord> curProcs = mForegroundPackages.get(proc.info.packageName,
18969                    proc.info.uid);
18970            if (isForeground) {
18971                if (curProcs == null) {
18972                    curProcs = new ArrayList<ProcessRecord>();
18973                    mForegroundPackages.put(proc.info.packageName, proc.info.uid, curProcs);
18974                }
18975                if (!curProcs.contains(proc)) {
18976                    curProcs.add(proc);
...电量统计相关...
18992            if (oomAdj) {
18993                updateOomAdjLocked(); //更新并应用进程oomadj的值
18994            }
18995        }
18996    }

分析就到updateOomAdjLocked为止了。后面就是一长串的oomadj计算并且将新的oomadj应用到本进程上了,这样进程就不被杀的优先级就提升了,以后如果有机会再详细说,这不是本章的重点。

还记得我们启动了两个相同id的service了吗,然后其中一个Service开始自杀。开始看自杀啦:

   public final void stopSelf(int startId) {
        if (mActivityManager == null) {
            return;
        }
        try {
            mActivityManager.stopServiceToken(
                    new ComponentName(this, mClassName), mToken, startId);
        } catch (RemoteException ex) {
        }
    }
    private void stopServiceLocked(ServiceRecord service) {
460        if (service.delayed) {
461            // If service isn't actually running, but is is being held in the
462            // delayed list, then we need to keep it started but note that it
463            // should be stopped once no longer delayed.
464            if (DEBUG_DELAYED_STARTS) Slog.v(TAG_SERVICE, "Delaying stop of pending: " + service);
465            service.delayedStop = true;
466            return;
467        }
468        synchronized (service.stats.getBatteryStats()) {
469            service.stats.stopRunningLocked();
470        }
471        service.startRequested = false;
472        if (service.tracker != null) {
473            service.tracker.setStarted(false, mAm.mProcessStats.getMemFactorLocked(),
474                    SystemClock.uptimeMillis());
475        }
476        service.callStart = false;
477        bringDownServiceIfNeededLocked(service, false, false);//重点
478    }

跟进bringDownServiceIfNeededLocked

   private final void bringDownServiceLocked(ServiceRecord r) {
1691        //Slog.i(TAG, "Bring down service:");
1692        //r.dump("  ");
1693
1694        // Report to all of the connections that the service is no longer
1695        // available.
1696        for (int conni=r.connections.size()-1; conni>=0; conni--) {
1697            ArrayList<ConnectionRecord> c = r.connections.valueAt(conni);
1698            for (int i=0; i<c.size(); i++) {
1699                ConnectionRecord cr = c.get(i);
1700                // There is still a connection to the service that is
1701                // being brought down.  Mark it as dead.
1702                cr.serviceDead = true;
1703                try {
1704                    cr.conn.connected(r.name, null);
1705                } catch (Exception e) {
1706                    Slog.w(TAG, "Failure disconnecting service " + r.name +
1707                          " to connection " + c.get(i).conn.asBinder() +
1708                          " (in " + c.get(i).binding.client.processName + ")", e);
1709                }
1710            }
1711        }
.............
1755
1756        r.cancelNotification();//取消serviceRecord对应的前台的广播!!
1757        r.isForeground = false;//取消serviceRecord的前台资格。
1758        r.foregroundId = 0;
1759        r.foregroundNoti = null;
1760
1761        // Clear start entries.
1762        r.clearDeliveredStartsLocked();
1763        r.pendingStarts.clear();
1764
1765        if (r.app != null) {
1766            synchronized (r.stats.getBatteryStats()) {
1767                r.stats.stopLaunchedLocked();
1768            }
1769            r.app.services.remove(r);
1770            if (r.app.thread != null) {
1771                (r.app, false);//重点。更新前台的状态,回到前面的那个函数
1772                try {
1773                    bumpServiceExecutingLocked(r, false, "destroy");
1774                    mDestroyingServices.add(r);
1775                    r.destroying = true;
1776                    mAm.updateOomAdjLocked(r.app);
1777                    r.app.thread.scheduleStopService(r);
1778                } catch (Exception e) {
1779                    Slog.w(TAG, "Exception when destroying service "
1780                            + r.shortName, e);
1781                    serviceProcessGoneLocked(r);
1782                }
1783            } else {
1784                if (DEBUG_SERVICE) Slog.v(
1785                    TAG_SERVICE, "Removed service that has no process: " + r);
1786            }
1787        } else {
1788            if (DEBUG_SERVICE) Slog.v(
1789                TAG_SERVICE, "Removed service that is not running: " + r);
1790        }
1791
。。。。。。。。
1812    }

可以看到一个service自杀的时候,会先取消和对应serviceRecord相关的所有的前台广播的notification(1756行),还有把自身设置为非foreground状态(1757行)。
然后调用updateServiceForegroundLocked,这个方法是之前startForeground那块就分析过的。它是根据serviceRecord是否是前台service的信息更新一下进程的oomadj。而当时我们开了两个前台Service,现在死了一个,还剩下一个呢!所以进程依旧保持高优先级状态。
发现问题了吗?那个代表着foreground的notification没了,但是进程却仍然保持低oomadj值!

修复其实也相当容易,
方案1:严格要求一个startForeground的id对应一个notification。不过这要修改API文档描述。不可取。
方案2:在移除通知的时候做判定,如果通知对应的Service没有死光,那么通知不能够移除!因为service和notification是多对一的状态。

手头没有7.1的代码,不过可以推断应该就是走了方案二,然后导致各个国产App没能成功移除Notification,导致在通知栏上群魔乱舞,不忍直视

花絮:翻阅代码的时候看到这么一段:

if (localForegroundNoti.getSmallIcon() == null) {
     // It is not correct for the caller to not supply a notification
     // icon, but this used to be able to slip through, so for
     // those dirty apps we will create a notification clearly
     // blaming the app.
     Slog.v(TAG, "Attempted to start a foreground service ("
             + name
             + ") with a broken notification (no icon: "
             + localForegroundNoti
             + ")");
     CharSequence appName = appInfo.loadLabel(
             ams.mContext.getPackageManager());
     if (appName == null) {
         appName = appInfo.packageName;
     }
     Context ctx = null;
     try {
         ctx = ams.mContext.createPackageContextAsUser(
                 appInfo.packageName, 0, new UserHandle(userId));
         Notification.Builder notiBuilder = new Notification.Builder(ctx);
         // it's ugly, but it clearly identifies the app
         notiBuilder.setSmallIcon(appInfo.icon);

这个就是Android对一个以前的一个保活方法的修复,利用方法是:startForeground的notification没有setSmallIcon的话就不会在通知栏出现。然后后面的版本很暴力地直接取出App的图标给他填了上去。

参考:www.jianshu.com/p/63aafe3c12af

### 回答1: android.app.remoteserviceexception: bad notification for startforeground是一个异常,通常是由于在启动前台服务时,通知的参数不正确导致的。这个异常通常会在Android应用程序中出现,需要检查代码中的通知参数是否正确,以确保启动前台服务时不会出现异常。如果您需要更多的帮助,请提供更多的上下文信息,以便我们更好地理解您的问题。 ### 回答2: Android.app.remoteserviceexception是一种异常,通常在通过启动前台服务时发生。在Android系统中,前台服务比后台服务更重要,因为它们能够向用户显示通知,使用户了解到正在运行的服务,同时确保系统不会在资源紧张时关闭它们。启动前台服务时,应该向系统提供可让用户了解服务正在运行的通知。 当出现“bad notification for startforeground”异常时,通常表示通知无效或不可用。这可能是因为通知没有正确地设置,或者可能是因为通知复制失败,例如,如果通知超过了允许的大小限制。如果通知无效,它将无法在前台服务启动后正确地显示给用户。 要解决此异常,您需要检查通知的正确性。首先,您需要确保通知具有必要的元素,例如标题和内容。其次,您需要确保正确设置通知的id,以确保通知可以正常显示和更新。还可以检查通知是否越过了允许的大小限制,导致通知无效。 此外,您可以使用日志来检查异常的详细信息,并确定导致该问题的根本原因。通过日志,您可以查看哪些操作导致了异常,以及异常的位置。这可以帮助您快速解决问题,并确保您能够提供正常运行的前台服务。 总之,在启动前台服务时,您应该充分了解通知的重要性,并确保它们得到正确的设置。如果遇到bad notification for startforeground异常,您应该检查通知是否有效和正确设置,以确保正常运行您的前台服务。 ### 回答3: 在 Android 应用程序开发中,有时候会遇到 android.app.remoteserviceexception: bad notification for startforeground 错误。这个错误通常是因为通知栏的设置不正确,而导致的崩溃异常。 在 Android 系统中,使用 Foreground Service 进行长时间运行的任务,需要在通知栏中创建一个持久的通知,来告知用户当前正在后台运行的任务。在创建通知时,需要注意以下几点: 1. 通知的 Channel ID 要正确设置,与 Notification Channel 一致。 2. 通知的 Icon 要正确设置,不能为 0。 3. 通知的 Title 和 Content 要正确设置,以便用户能够理解当前后台运行的任务。 如果通知栏的设置不正确,就可能会引起 android.app.remoteserviceexception: bad notification for startforeground 错误。通常这个错误会在 startForeground 方法被调用时出现,导致应用程序崩溃。 为了解决这个问题,我们可以检查通知栏的设置是否正确,尤其是 Channel ID、Icon、Title 和 Content 等参数。同时,也可以查看日志文件,找出造成错误的具体原因,再进行相应的修复。另外,在 Android 8.0 及以上的版本中,还需要注意是否已经创建了 Notification Channel。如果没有创建 Notification Channel,也会导致类似的错误。 总的来说,android.app.remoteserviceexception: bad notification for startforeground 错误是由于通知栏的设置不正确,而导致的崩溃异常。对于开发者来说,需要仔细检查通知栏的设置,确保各个参数都正确无误,避免出现类似的错误。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值