Android进程保活-自“裁”或者耍流氓(中篇)

猪小花1号2018-08-30 09:24

作者:李尚

通过“流氓”手段提高oom_adj,降低被杀风险,化身流氓进程

进程优先级的计算Android是有自己的一条准则的,某些特殊场景的需要额外处理进程的oom_adj Android也是给了参考方案的。但是,那对于流氓来说,并没有任何约束效力。 "流氓"仍然能够参照oom_adj(优先级)的计算规则,利用其漏洞,提高进程的oom_adj,以降低被杀的风险。如果单单降低被杀风险还好,就怕那种即不想死,又想占用资源的APP,累积下去就会导致系统内存不足,导致整个系统卡顿。

优先级的计算逻辑比较复杂,这里只简述非缓存进程,因为一旦沦为缓存进程,其优先级就只能依靠LRU来计算,不可控。而流氓是不会让自己沦为缓存进程的,非缓存进程是以下进程中的一种,并且,优先级越高(数值越小),越不易被杀死:

ADJ优先级 优先级 进程类型
SERVICE_ADJ 5 服务进程(Service process)
HEAVY_WEIGHT_APP_ADJ 4 后台的重量级进程,system/rootdir/init.rc文件中设置
BACKUP_APP_ADJ 3 备份进程(这个不太了解)
PERCEPTIBLE_APP_ADJ 2 可感知进程,比如后台音乐播放 ,通过startForeground设置的进程
VISIBLE_APP_ADJ 1 可见进程(可见,但是没能获取焦点,比如新进程仅有一个悬浮Activity,其后面的进程就是Visible process)
FOREGROUND_APP_ADJ 0 前台进程(正在展示是APP,存在交互界面,Foreground process)

  • 第一种提高到FOREGROUND_APP_ADJ

我们从低到高看:如何让进程编程FOREGROUND_APP_ADJ进程,也就是前台进程,这个没有别的办法,只有TOP activity进程才能是算前台进程。正常的交互逻辑下,这个是无法实现的,锁屏的时候倒是可以启动一个Activity,但是需要屏幕点亮的时候再隐藏,容易被用户感知,得不偿失,所以基本是无解,所以之前传说的QQ通过一个像素来保活的应该不是这种方案,而通过WindowManager往主屏幕添加View的方式也并未阻止进程被杀,到底是否通过一像素实现进程包活,个人还未得到解答,希望能有人解惑。

  • 第二种,提高到VISIBLE_APP_ADJ或者PERCEPTIBLE_APP_ADJ(不同版本等级可能不同 “4.3 = PERCEPTIBLE_APP_ADJ” 而 “> 5.0 = VISIBLE_APP_ADJ”),就表现形式上看,微博、微等信都可能用到了,而且这种手段的APP一般很难杀死,就算从最近的任务列表删除,其实进程还是没有被杀死,只是杀死了Activity等组件。

先看一下源码中对两种优先级的定义,VISIBLE_APP_ADJ是含有可见但是非交互Activity的进程,PERCEPTIBLE_APP_ADJ是用户可感知的进程,如后台音乐播放等

    // This is a process only hosting components that are perceptible to the
    // user, and we really want to avoid killing them, but they are not
    // immediately visible. An example is background music playback.
    static final int PERCEPTIBLE_APP_ADJ = 2;

    // This is a process only hosting activities that are visible to the
    // user, so we'd prefer they don't disappear.
    static final int VISIBLE_APP_ADJ = 1;

这种做法是相对温和点的,Android官方曾给过类似的方案,比如音乐播放时后,通过设置前台服务的方式来保活,这里就为流氓进程提供了入口,不过显示一个常住服务会在通知栏上有个运行状态的图标,会被用户感知到。但是Android恰恰还有个漏洞可以把该图标移除,真不知道是不是Google故意的。这里可以参考微信的保活方案:双Service强制前台进程保活

startForeground(ID, new Notification()),可以将Service变成前台服务,所在进程就算退到后台,优先级只会降到PERCEPTIBLE_APP_ADJ或者VISIBLE_APP_ADJ,一般不会被杀掉,Android的有个漏洞,如果两个Service通过同样的ID设置为前台进程,而其一通过stopForeground取消了前台显示,结果是保留一个前台服务,但不在状态栏显示通知,这样就不会被用户感知到耍流氓,这种手段是比较常用的流氓手段。优先级提高后,AMS的killBackgroundProcesses已经不能把进程杀死了,它只会杀死oom_adj大于ProcessList.SERVICE_ADJ的进程,而最近的任务列表也只会清空Activity,无法杀掉进程。 因为后台APP的优先级已经提高到了PERCEPTIBLE_APP_ADJ或ProcessList.VISIBLE_APP_ADJ,可谓流氓至极,如果再占据着内存不释放,那就是泼皮无赖了。具体做法如下:

public class RogueBackGroundService extends Service {

    private static int ROGUE_ID = 1;
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

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

    @Override
    public void onCreate() {
        super.onCreate();
        Intent intent = new Intent(this, RogueIntentService.class);
        startService(intent);
        startForeground(ROGUE_ID, new Notification());
    }
    public static class RogueIntentService extends IntentService {

        //流氓相互唤醒Service
        public RogueIntentService(String name) {
            super(name);
        }

        public RogueIntentService() {
            super("RogueIntentService");
        }

        @Override
        protected void onHandleIntent(Intent intent) {

        }
        @Override
        public void onCreate() {
            super.onCreate();
            startForeground(ROGUE_ID, new Notification());
        }    
       @Override
        public void onDestroy() {
            stopForeground(true); 
            super.onDestroy();
        }
    }
}

不过这个漏洞在Android7.1之后失效了,因为Google加了一个校验:如果还有Service通过setForeground绑定相同id的Notification,就不能cancelNotification,也就是说还是会显示通知(在通知列表)。

 private void cancelForegroudNotificationLocked(ServiceRecord r) {
        if (r.foregroundId != 0) {
            // First check to see if this app has any other active foreground services
            // with the same notification ID.  If so, we shouldn't actually cancel it,
            // because that would wipe away the notification that still needs to be shown
            // due the other service.
            ServiceMap sm = getServiceMap(r.userId);
            if (sm != null) {
            <!--查看是不是与该ID 通知绑定的Service取消了了前台显示-->
                for (int i = sm.mServicesByName.size()-1; i >= 0; i--) {
                    ServiceRecord other = sm.mServicesByName.valueAt(i);
                    if (other != r && other.foregroundId == r.foregroundId
                            && other.packageName.equals(r.packageName)) {
                        // Found one!  Abort the cancel.
                        <!--如果找到还有显示的Service,直接返回-->
                        return;
                    }
                }
            }
            r.cancelNotification();
        }
    }

在7.1上,Google PlayStore渠道的微信似乎也放弃了这种保活手段,因为7.1的微信从最近的任务列表删除是可以杀死进程的,如果采用上述手段是杀不了的。

双Service守护进程保活(这个也很流氓,不过如果不提高优先级(允许被杀),也算稍微良心)

前文我们分析过Android Binder的讣告机制:如果Service Binder实体的进程挂掉,系统会向Client发送讣告,而这个讣告系统就给进程保活一个可钻的空子。可以通过两个进程中启动两个binder服务,并且互为C/S,一旦一个进程挂掉,另一个进程就会收到讣告,在收到讣告的时候,唤起被杀进程。逻辑如下下:


首先编写两个binder实体服务PairServiceA ,PairServiceB,并且在onCreate的时候相互绑定,并在onServiceDisconnected收到讣告的时候再次绑定。

public class PairServiceA extends Service {

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

    @Override
    public void onCreate() {
        super.onCreate();
        bindService(new Intent(PairServiceA.this, PairServiceB.class), mServiceConnection, BIND_AUTO_CREATE);
    }

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            bindService(new Intent(PairServiceA.this, PairServiceB.class), mServiceConnection, BIND_AUTO_CREATE);
            ToastUtil.show("bind A");
        }
    };

与之配对的B

public class PairServiceB extends Service {

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

    @Override
    public void onCreate() {
        super.onCreate();
        bindService(new Intent(PairServiceB.this, PairServiceA.class), mServiceConnection, BIND_AUTO_CREATE);
    }

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            bindService(new Intent(PairServiceB.this, PairServiceA.class), mServiceConnection, BIND_AUTO_CREATE);
        }
    };
}

之后再Manifest中注册,注意要进程分离

    <service android:name=".service.alive.PairServiceA"/>
    <service
        android:name=".service.alive.PairServiceB"
        android:process=":alive"/>

之后再Application或者Activity中启动一个Service即可。

startService(new Intent(MainActivity.this, PairServiceA.class));

这个方案一般都没问题,因为Binder讣告是系统中Binder框架自带的,一次性全部杀死所有父子进程可能会不行。这个手段虽然不能提高优先级,但是从最近的任务列表已经不能杀死进程了。原因如下: 此时APP内至少两个进程A\B ,并且AB相互通过bindService绑定,此时就是互为客户端,在oom_adj中有这么一种计算逻辑,如果进程A的Service被B通过bind绑定,那么A的优先级可能会受到B的影响,因为在计算A的时候需要先计算B,但是B同样是A的Service,反过来有需要计算A,如果不加额外的判断,就会出现死循环,AMS是通过一个计数来标识的:mAdjSeq == app.adjSeq。于是流程就是这样 

  • 计算A:发现依赖B,去计算B
  • 计算B:发现依赖A,回头计算A
  • 计算A:发现A正在计算,直接返回已经计算到一半的A优先级

上面的流程能保证不出现死循环,并且由于A只计算了一半,所以A的很多东西没有更新,所以B拿到的A就是之前的数值,比如 curProcState、curSchedGroup:

private final int computeOomAdjLocked(ProcessRecord app, int cachedAdj, ProcessRecord TOP_APP,
        boolean doingAll, long now) {
    if (mAdjSeq == app.adjSeq) {
        // This adjustment has already been computed.
        return app.curRawAdj;
    }
    ....
      for (int is = app.services.size()-1;
            is >= 0 && (adj > ProcessList.FOREGROUND_APP_ADJ
                    || schedGroup == Process.THREAD_GROUP_BG_NONINTERACTIVE
                    || procState > ActivityManager.PROCESS_STATE_TOP);
            is--) {
        ServiceRecord s = app.services.valueAt(is);
       ...
        for (int conni = s.connections.size()-1;
                conni >= 0 && (adj > ProcessList.FOREGROUND_APP_ADJ
                        || schedGroup == Process.THREAD_GROUP_BG_NONINTERACTIVE
                        || procState > ActivityManager.PROCESS_STATE_TOP);
                conni--) {
            ArrayList<ConnectionRecord> clist = s.connections.valueAt(conni);
            for (int i = 0;
                    i < clist.size() && (adj > ProcessList.FOREGROUND_APP_ADJ
                            || schedGroup == Process.THREAD_GROUP_BG_NONINTERACTIVE
                            || procState > ActivityManager.PROCESS_STATE_TOP);
                    i++) {
                ConnectionRecord cr = clist.get(i);

                if (cr.binding.client == app) {
                    // Binding to ourself is not interesting.
                    continue;
                }
                if ((cr.flags&Context.BIND_WAIVE_PRIORITY) == 0) {
                    ProcessRecord client = cr.binding.client;
                    // 这里会不会出现死循环的问题呢? A需要B的计算、B需要A的计算,这个圆环也许就是为什么
                    // 无法左滑删除的原因 循环的,
                    <!--关键点1 -->
                    int clientAdj = computeOomAdjLocked(client, cachedAdj,
                            TOP_APP, doingAll, now);

                    int clientProcState = client.curProcState;
                    if (clientProcState >= ActivityManager.PROCESS_STATE_CACHED_ACTIVITY) {
                        clientProcState = ActivityManager.PROCESS_STATE_CACHED_EMPTY;
                    }
                   <!--关键点2-->
                            ...
                    if ((cr.flags&Context.BIND_NOT_FOREGROUND) == 0) {
                        if (client.curSchedGroup == Process.THREAD_GROUP_DEFAULT) {
                            schedGroup = Process.THREAD_GROUP_DEFAULT;
                        }
                ...        
        }

上面的代码中:关键点1是循环计算的入口,关键点2是无法删除的原因所在,由于A没及时更新,导致schedGroup = Process.THREAD_GROUP_DEFAULT,反过来也让A保持schedGroup = Process.THREAD_GROUP_DEFAULT。A B 都无法左滑删除。



相关阅读:Android进程保活-自“裁”或者耍流氓(上篇)

Android进程保活-自“裁”或者耍流氓(下篇)

https://www.163yun.com/gift

本文来自网易实践者社区,经作者李尚授权发布。