美文网首页Android录集(公众号:龙旋)
Android实现进程保活方案解析

Android实现进程保活方案解析

作者: 龙旋之谷 | 来源:发表于2021-02-02 17:54 被阅读0次

    众所周知,日活率是一款App的核心绩效指标,日活量不仅反应了应用的受欢迎程度,同时反应了产品的变现能力,进而直接影响盈利能力和企业估值。为了抢占市场,谁都不会放过任何一个可以提高应用日活的方法,所以App进程保活都是各大厂商,特别是头部应用开发商永恒的追求,毕竟一旦 App 进程死亡,那就再也无法在用户的手机上开展任何业务,所有的商业模型在用户侧都没有立足之地。

    早期的Android系统不完善,从而导致有很多空子可以钻,它们用着各种各样的方式进行保活,长期以来被人诟病耗电、卡顿,也滋生了很多流氓应用,拖垮Android 平台的流畅性,建议不要这么做,本文只作技术性的探讨。

    随着 Android 系统的发展,这一切都在往好的方向演变。

    • Android 5.0 以下,系统杀进程以 uid为标识,通过杀死整个进程组来杀进程。
    • Android 6.0 引入了待机模式(doze),一旦用户拔下设备的电源插头,并在屏幕关闭后的一段时间内使其保持不活动状态,设备会进入低电耗模式,在该模式下设备会尝试让系统保持休眠状态。
    • Android 7.0 加强了之前鸡肋的待机模式(不再要求设备静止状态),同时对开启了 Project Svelte,Project Svelte 是专门用来优化 Android 系统后台的项目,在 Android 7.0 上直接移除了一些隐式广播,App 无法再通过监听这些广播拉起自己。
    • Android 8.0 进一步加强了应用后台执行限制:一旦应用进入已缓存状态时,如果没有活动的组件,系统将解除应用具有的所有唤醒锁。另外,系统会限制未在前台运行的应用的某些行为,比如说应用的后台服务的访问受到限制,也无法使用 Mainifest 注册大部分隐式广播。
    • Android 9.0 进一步改进了省电模式的功能并加入了应用待机分组,长时间不用的 App 会被打入冷宫;另外,系统监测到应用消耗过多资源时,系统会通知并询问用户是否需要限制该应用的后台活动。

    随着Android系统日渐完善,单单通过自己拉活自己逐渐变得不可能了;
    因此后面的所谓「保活」基本上是两条路:

    1. 提升进程的优先级,让系统不要轻易杀死进程;
    2. App间关联唤醒,打开一个App的时候会启动、唤醒其他App。

    进程保活方案:

    1、最好的方案那肯定是跟各大系统厂商建立合作关系,把App加入系统内存清理的白名单;比如微信,降低oom_adj值,尽量保证进程不被系统杀死。

    那问题又来了:什么是oom_adj?

    Android有一个oom的机制,系统会根据进程的优先级,给每个进程一个oom权重值,当系统内存不足时,系统会根据这个优先级去选择将哪些进程杀掉,以腾出空间保证更高优先级的进程能正常运行。要想让进程长期存活,提高优先级是个不二之选。这个可以在adb中,通过以下命令查看:su cat /proc/pid/oom_adj , 这个值越小,说明进程的优先级越高,越不容易被进程kill掉。

    如果是负数,表示该进程为系统进程,肯定不会被杀掉,
    如果是0,表示是前台进程,即当前用户正在操作的进程,除非万不得已,也不会被杀掉;
    如果是1,表示是可见进程,通常表示有一个前台服务,会在通知栏有一个划不掉的通知,比如放歌,下载文件什么的;
    再增大,则优先级逐渐降低,顺序为服务进程,缓存进程,空进程等等。

    2、我们常常将保活方法进行分类:白色保活、灰色保活、黑色保活。

    白色保活

    • 用startForeground()启动前台服务,这是官方提供的后台保活方式,不足的就是通知栏会常驻一条通知,像360的状态栏。

    灰色保活

    • 开启前台Service,开启另一个Service将通知栏移除,其oom_adj值还是没变的,这样用户就察觉不到app在后台保活。
    • 用广播唤醒自启,像开机广播、网络切换广播等,但在国产Rom中几乎都被堵上了。
    • 多个app关联唤醒,就像BAT的全家桶,打开一个App的时候会启动、唤醒其他App,包括一些第三方推送也是,对于大多数单独app,比较难以实现。

    黑色保活

    • 1 像素activity保活方案,监听息屏事件,在息屏时启动个一像素的activity,提升自身优先级;
    • Service中循环播放一段无声音频,伪装音乐app,播放音乐中的app优先级还是蛮高的,也能很大程度保活效果较好,但耗电量高,谨慎使用;
    • 双进程守护,这在国产rom中几乎没用,因为划掉app会把所有相关进程都杀死。

    3、实现过程:

    1)、用startForeground()启动前台服务
    前台Service,使用startForeground这个Service尽量要轻,不要占用过多的系统资源,否则系统在资源紧张时,照样会将其杀死。

    • DaemonService.java
    
    public class DaemonService extends Service {
        private static final String TAG = "DaemonService";
        public static final int NOTICE_ID = 100;
    
        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
    
        @Override
        public void onCreate() {
            super.onCreate();
    
            Log.i(TAG, "DaemonService---->onCreate被调用,启动前台service");
            //如果API大于18,需要弹出一个可见通知
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                Notification.Builder builder = new Notification.Builder(this);
                builder.setSmallIcon(R.mipmap.ic_launcher);
                builder.setContentTitle("KeepAppAlive");
                builder.setContentText("DaemonService is runing...");
                startForeground(NOTICE_ID, builder.build());
            } else {
                startForeground(NOTICE_ID, new Notification());
            }
        }
    
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            // 如果Service被终止,当资源允许情况下,重启service
            return START_STICKY;
        }
    
        @Override
        public void onDestroy() {
            super.onDestroy();
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                NotificationManager mManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
                mManager.cancel(NOTICE_ID);
            }
    
            Log.d(TAG, "DaemonService---->onDestroy,前台service被杀死");
            // 重启自己
            Intent intent = new Intent(getApplicationContext(), DaemonService.class);
            startService(intent);
        }
    }
    
    • AndroidManifest.xml
    <service
        android:name=".service.DaemonService"
        android:enabled="true"
        android:exported="true"
        android:process=":daemon_service" />
    
    • 启动服务的代码
    
    Intent i = new Intent(this, DaemonService.class);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        startForegroundService(i);
    } else {
        startService(i);
    }
    

    这种保活方式,会在通知栏常驻一条通知。

    2)、开启前台Service
    这个其实跟(1)是相同的,区别在于这个方式将常驻通知栏移除了

    • DaemonService.java
    
    @Override
    public void onCreate() {
        super.onCreate();
    
        Log.i(TAG, "DaemonService---->onCreate被调用,启动前台service");
        //如果API大于18,需要弹出一个可见通知
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            Notification.Builder builder = new Notification.Builder(this);
            builder.setSmallIcon(R.mipmap.ic_launcher);
            builder.setContentTitle("KeepAppAlive");
            builder.setContentText("DaemonService is runing...");
            startForeground(NOTICE_ID, builder.build());
            // 如果觉得常驻通知栏体验不好
                  // 可以通过启动CancelNoticeService,将通知移除,oom_adj值不变
                  Intent intent = new Intent(this, CancelNoticeService.class);
            startService(intent);
        } else {
            startForeground(NOTICE_ID, new Notification());
        }
    }
    
    • CancelNoticeService.java
    
    /** 移除前台Service通知栏标志,这个Service选择性使用 */
    public class CancelNoticeService extends Service {
        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
    
        @Override
        public void onCreate() {
            super.onCreate();
        }
    
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            if(Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2){
                Notification.Builder builder = new Notification.Builder(this);
                builder.setSmallIcon(R.mipmap.ic_launcher);
                startForeground(DaemonService.NOTICE_ID,builder.build());
                // 开启一条线程,去移除DaemonService弹出的通知
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        // 延迟1s
                        SystemClock.sleep(1000);
                        // 取消CancelNoticeService的前台
                        stopForeground(true);
                        // 移除DaemonService弹出的通知
                        NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
                        manager.cancel(DaemonService.NOTICE_ID);
                        // 任务完成,终止自己
                        stopSelf();
                    }
                }).start();
            }
            return super.onStartCommand(intent, flags, startId);
        }
    
        @Override
        public void onDestroy() {
            super.onDestroy();
        }
    }
    
    
    • AndroidManifest.xml
    <service
        android:name=".service.DaemonService"
        android:enabled="true"
        android:exported="true"
        android:process=":daemon_service" />
    <service
        android:name=".service.CancelNoticeService"
        android:enabled="true"
        android:exported="true"
        android:process=":service" />
    

    同时启动两个service,共享同一个NotificationID,并且将他们同时置为前台状态,此时会出现两个前台服务,但通知管理器里只有一个关联的通知。这时我们在其中一个服务中调用 stopForeground(true),这个服务前台状态会被取消,同时状态栏通知也被移除。另外一个服务并没有受到影响,还是前台服务状态,但是此时,状态栏通知已经没了!

    3)、1 像素activity保活方案
    屏幕关闭的时候打开一个1px的透明的activity,屏幕开启的时候再去finsh掉这个activty即可

    • OnepxActivity.java
    
    public class OnepxActivity extends Activity {
        private BroadcastReceiver br;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            Window window = getWindow();
            window.setGravity(Gravity.LEFT | Gravity.TOP);
            WindowManager.LayoutParams params = window.getAttributes();
            params.x = 0;
            params.y = 0;
            params.height = 1;
            params.width = 1;
            window.setAttributes(params);
            //结束该页面的广播
            br = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    LogUtils.d("OnepxActivity finish   ================");
                    finish();
                }
            };
            registerReceiver(br, new IntentFilter("finish activity"));
    
            //检查屏幕状态
            checkScreenOn("onCreate");
        }
    
        /**
         * 检查屏幕状态  isScreenOn为true  屏幕“亮”结束该Activity
         * */
        private void checkScreenOn(String methodName) {
            LogUtils.d("from call method: " + methodName);
            PowerManager pm = (PowerManager) OnepxActivity.this.getSystemService(Context.POWER_SERVICE);
            boolean isScreenOn = pm.isScreenOn();
            LogUtils.i("isScreenOn: "+isScreenOn);
            if(isScreenOn){
                finish();
            }
        }
    
        @Override
        protected void onDestroy() {
            LogUtils.i("===onDestroy===");
            try{
                unregisterReceiver(br);
            }catch (IllegalArgumentException e){
                LogUtils.e("receiver is not resisted: "+e);
            }
            super.onDestroy();
        }
    
        @Override
        protected void onResume() {
            super.onResume();
            checkScreenOn("onResume");
        }
    }
    
    • OnepxReceiver.java
    
    public class OnepxReceiver extends BroadcastReceiver {
        private static OnepxReceiver receiver;
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {//屏幕被关闭
                Intent it = new Intent(context, OnepxActivity.class);
                it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                context.startActivity(it);
                LogUtils.i("1px--screen off-");
            } else if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) {//屏幕被打开
                context.sendBroadcast(new Intent("finish activity"));
                LogUtils.i("1px--screen on-");
            }
        }
    
        public static void register1pxReceiver(Context context) {
            if (receiver == null) {
                receiver = new OnepxReceiver();
            }
            context.registerReceiver(receiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
            context.registerReceiver(receiver, new IntentFilter(Intent.ACTION_SCREEN_ON));
        }
    
        public static void unregister1pxReceiver(Context context) {
            context.unregisterReceiver(receiver);
        }
    }
    

    4)、Service中循环播放一段无声音频
    新建一个播放音乐的Service类,将播放模式改为无限循环播放。在其onDestroy方法中对自己重新启动。

    • PlayerMusicService.java
    
    public class PlayerMusicService extends Service {
        private final static String TAG = PlayerMusicService.class.getSimpleName();
        private MediaPlayer mMediaPlayer;
     
        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
     
        @Override
        public void onCreate() {
            super.onCreate();
            Logger.d(TAG, TAG + "---->onCreate,启动服务");
            mMediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.silent);
            mMediaPlayer.setLooping(true);
        }
     
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    startPlayMusic();
                }
            }).start();
            return START_STICKY;
        }
     
        private void startPlayMusic() {
            if (mMediaPlayer != null) {
                Logger.d(TAG, "启动后台播放音乐");
                mMediaPlayer.start();
            }
        }
     
        private void stopPlayMusic() {
            if (mMediaPlayer != null) {
                Logger.d(TAG, "关闭后台播放音乐");
                mMediaPlayer.stop();
            }
        }
     
        @Override
        public void onDestroy() {
            super.onDestroy();
            stopPlayMusic();
            Logger.d(TAG, TAG + "---->onCreate,停止服务");
            // 重启
            Intent intent = new Intent(getApplicationContext(), PlayerMusicService.class);
            startService(intent);
        }
    }
    
    • AndroidManifest.xml
    <service
        android:name=".service.PlayerMusicService"
        android:enabled="true"
        android:exported="true"
        android:process=":music_service" />
    
    • 启动服务的代码
    Intent intent = new Intent(this, PlayerMusicService.class);
    startService(intent);
    

    这里就介绍保活的几种方式啦,到这里就介绍完啦。


    小编整理了一份Android电子书籍,需要的童鞋关注公众号回复:"e_books" 即可获取哦!


    在这里插入图片描述 gzh.png

    相关文章

      网友评论

        本文标题:Android实现进程保活方案解析

        本文链接:https://www.haomeiwen.com/subject/ovgytltx.html