Android之APP保活

前言

强烈建议不要这么做,不仅仅从用户角度考虑,它只会滋生更多的流氓应用,拖垮Android 平台的流畅性(假如你手机里装了支付宝、淘宝、天猫、UC等阿里系的app,那么你打开任意一个阿里系的app后,有可能就顺便把其他阿里系的app给唤醒了。(只是拿阿里打个比方,其实BAT系都差不多)没错,我们的Android手机就是一步一步的被上面这些场景给拖卡机的。)。作为Android开发者也有责任去维护Android的生态环境。现在很多Android开发工程师,主力机居然是iPhone而不是Android设备,感到相当悲哀。

这种做法其实是捡了芝麻丢了西瓜,最终倒霉的还是安卓开发者!越来越多的人转向苹果阵营,到时候你想写点良心代码都不会有人来买你帐了!看看现在公交车上的情景,10个里有7、8个人是苹果机,此情此景那部分写流氓代码的安卓程序员还能为自己写了一个用户怎么关都关不了的程序而自豪么?!
 

本文只作技术探讨,如果希望找到进程永生的方法,可能要失望了。

 

支付宝、微信如何实现保活常驻系统后台?

1、与手机厂商沟通好,把它们app放进系统白名单,降低omm_adj值,尽量保证进程不被系统杀死。
2、常用方法:

  • 开启前台Service(效果好,推荐)
  • Service中循环播放一段无声音频(效果较好,但耗电量高,谨慎使用
  • 双进程守护(Android 5.0前有效)
  • JobScheduler(Android 5.0后引入,8.0后失效)
  • 1 像素activity保活方案(不推荐)
  • 广播锁屏、自定义锁屏(不推荐)
  • 第三方推送SDK唤醒(效果好,缺点是第三方接入)

PS:不以节能来维持进程保活的手段,都是耍流氓。

 

 

什么是omm_adj?

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

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



常见的保活手段:

1)开启前台Service

原理:通过使用 startForeground()方法将当前Service置于前台来提高Service的优先级。需要注意的是,对API大于18而言 startForeground()方法需要弹出一个可见通知,如果你觉得不爽,可以开启另一个Service将通知栏移除,其oom_adj值还是没变的。实现代码如下:

a) DaemonService.java
/**前台Service,使用startForeground
 * 这个Service尽量要轻,不要占用过多的系统资源,否则
 * 系统在资源紧张时,照样会将其杀死
 */
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();
        if(Contants.DEBUG)
            Log.d(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());
        }
    } 
 
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // 如果Service被终止,当资源允许情况下,重启service
        return START_STICKY;
    }
 
    @Override
    public void onDestroy() {
        super.onDestroy();
        // 如果Service被杀死,干掉通知
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2){
            NotificationManager mManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
            mManager.cancel(NOTICE_ID);
        }
        if(Contants.DEBUG)
            Log.d(TAG,"DaemonService---->onDestroy,前台service被杀死");
        // 重启自己
        Intent intent = new Intent(getApplicationContext(),DaemonService.class);
        startService(intent);
    }
}
讲解一下:
       这里还用到了两个技巧:一是在onStartCommand方法中返回START_STICKY,其作用是当Service进程被kill后,系统会尝试重新创建这个Service,且会保留Service的状态为开始状态,但不保留传递的Intent对象,onStartCommand方法一定会被重新调用。其二在onDestory方法中重新启动自己,也就是说,只要Service在被销毁时走到了onDestory这里我们就重新启动它。

b) 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();
    }
}

c) 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),这个服务前台状态会被取消,同时状态栏通知也被移除。另外一个服务并没有受到影响,还是前台服务状态,但是此时,状态栏通知已经没了! 这就是支付宝的黑科技。

 

 

 

2)循环播放无声音频

a) PlayerMusicService.java
/**循环播放一段无声音频,以提升进程优先级*/
public class PlayerMusicService extends Service {
    private final static String TAG = "PlayerMusicService";
    private MediaPlayer mMediaPlayer;
 
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
 
    @Override
    public void onCreate() {
        super.onCreate();
        if(Contants.DEBUG)
            Log.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){
            if(Contants.DEBUG)
                Log.d(TAG,"启动后台播放音乐");
            mMediaPlayer.start();
        }
    }
 
    private void stopPlayMusic(){
        if(mMediaPlayer != null){
            if(Contants.DEBUG)
                Log.d(TAG,"关闭后台播放音乐");
            mMediaPlayer.stop();
        }
    }
 
    @Override
    public void onDestroy() {
        super.onDestroy();
        stopPlayMusic();
        if(Contants.DEBUG)
            Log.d(TAG,TAG+"---->onCreate,停止服务");
        // 重启
        Intent intent = new Intent(getApplicationContext(),PlayerMusicService.class);
        startService(intent);
    }
}
b) AndroidManifest.xml
<service android:name=".service.PlayerMusicService"
          android:enabled="true"
          android:exported="true"
          android:process=":music_service"/>

 


3)双进程守护

  •  开启2个服务分别在不同的进程里面,根据AIDL进行进程之间通信
  • 本地服务跟远程服务互相绑定,当本地服务开启成功,开启远程服务,然后跟远程服务绑定。
  • 反之,当其中一个进程出现异常,另一个进程会马上把这个出现异常的进程重新启动。

首先是一个AIDL接口,两边的Service都要通过继承Service_1.Stub来实现AIDL接口中的方法,这里做一个空实现,目的是为了实现进程通信。接口声明如下:
package com.ph.myservice;
interface Service_1 {
    String getName();
}

然后是两个Service,为了保持连接,内部写一个内部类实现ServiceConnection的接口,当外部杀了其中一个进程的时候,会进入onDisConnection中,那么此时要做的就是start和bind另一个进程,因为Service的启动是可以多次的,所以这样是没问题的,代码如下:

package com.ph.myservice;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.widget.Toast;

import java.util.List;

public class LocalService extends Service {
    private ServiceConnection conn;
    private MyService myService;

    @Override
    public IBinder onBind(Intent intent) {
        return myService;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        init();
    }

    private void init() {
        if (conn == null) {
            conn = new MyServiceConnection();
        }
        myService = new MyService();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Toast.makeText(getApplicationContext(), "本地进程启动", Toast.LENGTH_LONG).show();
        Intent intents = new Intent();
        intents.setClass(this, RemoteService.class);
        bindService(intents, conn, Context.BIND_IMPORTANT);
        return START_STICKY;
    }

    class MyService extends Service_1.Stub {
        @Override
        public String getName() throws RemoteException {
            return null;
        }
    }

    class MyServiceConnection implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            System.out.println("获取连接");
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Toast.makeText(LocalService.this, "远程连接被干掉了", Toast.LENGTH_SHORT).show();
            LocalService.this.startService(new Intent(LocalService.this, RemoteService.class));
            LocalService.this.bindService(new Intent(LocalService.this, RemoteService.class), conn, Context.BIND_IMPORTANT);
         }
       }
    }

远程服务类如下:

package com.ph.myservice;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.widget.Toast;

public class RemoteService extends Service {
    private MyBinder binder;
    private ServiceConnection conn;

    @Override
    public void onCreate() {
        super.onCreate();
        // System.out.println("远程进程开启");
        init();
  }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Toast.makeText(getApplicationContext(), "远程进程启动", Toast.LENGTH_LONG).show();
        Intent intents = new Intent();
        intents.setClass(this, LocalService.class);
        bindService(intents, conn, Context.BIND_IMPORTANT);
        return START_STICKY;
    }

    private void init() {
        if (conn == null) {
            conn = new MyConnection();
        }
        binder = new MyBinder();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    static class MyBinder extends Service_1.Stub {
        @Override
        public String getName() throws RemoteException {
            return "远程连接";
        }
    }

    class MyConnection implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            System.out.println("获取远程连接");
        }

        @Override
        public void onServiceDisconnected(ComponentName nme) {
            Toast.makeText(RemoteService.this, "本地连接被干掉了", Toast.LENGTH_SHORT).show();
            RemoteService.this.startService(new Intent(RemoteService.this,
                    LocalService.class));
            RemoteService.this.bindService(new Intent(RemoteService.this,
                    LocalService.class), conn, Context.BIND_IMPORTANT);
        }
    }

}

布局文件里要加上声明

<service android:name=".LocalService" />
<service android:name=".RemoteService" android:process=":remote" />

实际情况我个人测试,在5.0以下的模拟器上是没问题的,不管多次从系统的进程里kill掉,也还是会重新启动tos,但是5.0以上这种方法是无效的,5.0以上Android应该是意识到了这种双进程守护的方式,因此修改了一下源码,让这种双进程保活应用的方式无效。因此,针对5.0以上,我们采用另一种方案。

 

4)JobScheduler执行任务调度保活

JobScheduler这个类是21版本google新提供的api,参考链接

 

5)一个像素activity保活方案

一个像素的方案网上也非常多不做过多的解释,就是在屏幕关闭的时候打开一个1px的透明的activity据说是QQ的保活方案,屏幕开启的时候再去finsh掉这个activty即可,大致代码如下:
 /** 一个像素的保活界面 */
public class LiveActivity extends BaseActivity {

    public static final String TAG="LiveActivity";
    private BroadcastReceiver endReceiver=null;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
        super.onCreate(savedInstanceState, persistentState);
        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);
        //结束该页面的广播
        endReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                finish();
            }
        };
        registerReceiver(endReceiver, new IntentFilter("finish"));
        //检查屏幕状态
        checkScreen();
    }

    @Override
    protected void onResume() {
        super.onResume();
        checkScreen();
    }

    /**  检查屏幕状态  isScreenOn为true  屏幕“亮”结束该Activity */
    private void checkScreen() {
        PowerManager pm = (PowerManager) LiveActivity.this.getSystemService(Context.POWER_SERVICE);
        boolean isScreenOn = pm.isScreenOn();
        if (isScreenOn) {
            finish();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (endReceiver!=null){
            unregisterReceiver(endReceiver);
        }
    }
}

当然还需要在设置1像素Activity的样式
    <style name="LiveActivity" parent="android:Theme.Holo.Light.NoActionBar">
        <item name="android:windowFrame">@null</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowIsFloating">true</item>
        <item name="android:windowContentOverlay">@null</item>
        <item name="android:backgroundDimEnabled">false</item>
        <item name="android:windowBackground">@null</item>
        <item name="android:windowIsTranslucent">true</item>
    </style>


广播的代码:

public class LiveBroadcastReceiver extends BroadcastReceiver {
    public static final String TAG="LiveActivity";
    private KeepLiveManager mKeepLiveManager;

    private HomeActivity mHomeActivity;
    public LiveBroadcastReceiver(HomeActivity homeActivity){
              this.mHomeActivity=homeActivity;
        mKeepLiveManager=mHomeActivity.getKeepLiveManger();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public void onReceive(Context context, Intent intent) {
        switch (intent.getAction()){
            case Intent.ACTION_SCREEN_OFF://屏幕被关闭
                Intent it=new Intent(context, LiveActivity.class);
                it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                context.startActivity(it);
                break;
            case Intent.ACTION_SCREEN_ON://屏幕被打开
                context.sendBroadcast(new Intent("finish"));
                /**
                 * 以下的代码会导致屏幕解锁后会出现返回主界面的情况
                 */
//                Intent main = new Intent(Intent.ACTION_MAIN);
               main.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
//                //注释掉这段避免在再次打开APP界面时出现的返回主界面的问题
//                //main.addCategory(Intent.CATEGORY_HOME);
//                context.startActivity(main);
                break;
        }
    }
}

 

 

 

 

 

本文综合以下资料所得:
https://blog.csdn.net/andrexpert/article/details/75045678
http://zhoujianghua.com/2015/07/28/black_technology_in_alipay/
https://blog.csdn.net/zhoukongxiao/article/details/80611059 
https://blog.csdn.net/pan861190079/article/details/72773549 
https://www.zhihu.com/question/29826231/answer/71956560

 

 

  • 6
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值