android app防被杀策略


Service篇

  1.         <service
                android:name="com.example.service.**Service"
                android:icon="@drawable/w6"
                android:enabled="true" 
                android:exported="false">  不允许外部程序骚扰
            </service>
  2.     <application
            android:name=".****"
            android:allowBackup="false"

            android:theme="@style/Black" 
            android:enabled="true">  要和1中一致才能处于激活状态
  3. alarmreceiver 时间间隔要超过60s
    • 因内存资源不足而杀死Service 
      这种情况比较容易处理,可将onStartCommand() 方法的返回值设为 START_STICKY或START_REDELIVER_INTENT ,该值表示服务在内存资源紧张时被杀死后,在内存资源足够时再恢复。也可将Service设置为前台服务,这样就有比较高的优先级,在内存资源紧张时也不会被杀掉。这两点的实现,我们在前面已分析过和实现过这里就不重复。简单代码如下:
    /**
         * 返回 START_STICKY或START_REDELIVER_INTENT
         * @param intent
         * @param flags
         * @param startId
         * @return
         */
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
    //        return super.onStartCommand(intent, flags, startId);
            return START_STICKY;
        }

  4. 将Service设置为前台服务,这样就有比较高的优先级,在内存资源紧张时也不会被杀掉。可以生成1像素的页面,假装还在前台,免于被后台清理工具杀死。
    	
    
    
    
    
    
    
    
    
    
    
    
  5. 用户通过 settings -> Apps -> Running -> Stop 方式杀死Service  这种情况可以开2个service互相照顾,一个被关了,在destroy里面发求救广播给另外一个
    public class Baidumapservice extends Service{
    
        private BroadcastReceiver mReceiver;
        private IntentFilter mIF;
        
    	@Override
    	public IBinder onBind(Intent intent) {
    		// TODO Auto-generated method stub
    		return null;
    	}
    	@Override
        public void onCreate() {
            super.onCreate();
            mReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    Intent a = new Intent(Baidumapservice.this, BootService.class);
                    startService(a);
                }
            };
            mIF = new IntentFilter();
            //自定义action
            mIF.addAction("com.restart.service");
            //注册广播接者
            registerReceiver(mReceiver, mIF);
        }
    
        @Override
        public void onDestroy() {
            super.onDestroy();
    
            Intent intent = new Intent();
            intent.setAction("com.restart.service");
            //发送广播
            sendBroadcast(intent);
    
            unregisterReceiver(mReceiver);
        }
    }
    

  6. 把自己应用的包名改成微信的包名,或QQ的包名,伪装成白名单,真相就出来了。
  7. 在jni中循环监听服务的状态,如果被停止了,就重新启动它。并且在java代码中实现开机自动启动
  8. 安卓系统中有一段代码会判断,如果是用户强制停止了你的进程,那么该进程所有注册的广播、闹钟都会被移掉,除非用户再次手动启动,这样是为了防止一些非法进程在后台不停骚扰用户,好像还找不到好的解决方案...

                





 Android 通过JNI实现守护进程

转载请注明出处:http://blog.csdn.net/yyh352091626/article/details/50542554

开发一个需要常住后台的App其实是一件非常头疼的事情,不仅要应对国内各大厂商的ROM,还需要应对各类的安全管家...  虽然不断的研究各式各样的方法,但是效果并不好,比如任务管理器把App干掉,服务就起不来了...

网上搜寻一番后,主要的方法有以下几种方法,但其实也都治标不治本:

  1、提高Service的优先级:这个,也只能说在系统内存不足需要回收资源的时候,优先级较高,不容易被回收,然并卵...

  2、提高Service所在进程的优先级:效果不是很明显

  3、在onDestroy方法里重启service:这个倒还算挺有效的一个方法,但是,直接干掉进程的时候,onDestroy()方法都进不来,更别想重启了

  4、broadcast广播:和第3种一样,没进入onDestroy(),就不知道什么时候发广播了,另外,在Android4.4以上,程序完全退出后,就不好接收广播了,需要在发广播的地方特定处理

  5、放到System/app底下作为系统应用:这个也就是平时玩玩,没多大的实际意义。

  6、Service的onStartCommand()方法,返回START_STICKY,这个主要是针对系统资源不足而导致的服务被关闭,还是有一定的道理的。

在这里推荐一篇文章:【腾讯Bugly】Android 进程保活招式大全

应对的方法是有,实现起来都比较繁琐,而且稳定性也不好。如果你自己可以定制ROM,那就有很多种办法了,比如把你的应用加入白名单,或是多安装一个没有图标的app作为守护进程... 不过这个思想大部分程序都不适用。

那么,有没有办法在一个APP里面,开启一个子线程,在主线程被干掉了之后,子线程通过监听、轮询等方式去判断服务是否存在,不存在的话则开启服务。答案自然是肯定的,通过JNI的方式,fork()出一个子线程作为守护进程,轮询监听服务状态。守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。而守护进程的会话组和当前目录,文件描述符都是独立的。后台运行只是终端进行了一次fork,让程序在后台执行,这些都没有改变。

那么我们先来看看Android4.4的源码,ActivityManagerService(源码/frameworks/base/services/core/Java/com/Android/server/am/ActivityManagerService.java)是如何关闭在应用退出后清理内存的:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. Process.killProcessQuiet(pid);  
应用退出后,ActivityManagerService就把主进程给杀死了,但是,在Android5.0中,ActivityManagerService却是这样处理的:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. Process.killProcessQuiet(app.pid);  
  2. Process.killProcessGroup(app.info.uid, app.pid);  
虽只差了一句代码,差别却很大。Android5.0在应用退出后,ActivityManagerService不仅把主进程给杀死,另外把主进程所属的进程组一并杀死,这样一来,由于子进程和主进程在同一进程组,子进程在做的事情,也就停止了...要不怎么说Android5.0在安全方面做了很多更新呢...

那么,有没有办法让子进程脱离出来,不要受到主进程的影响,当然也是可以的。那么,在C/C++层是如何实现的呢?先上关键代码:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. /** 
  2.  * srvname  进程名 
  3.  * sd 之前创建子进程的pid写入的文件路径 
  4.  */  
  5. int start(int argc, char* srvname, char* sd) {  
  6.     pthread_t id;  
  7.     int ret;  
  8.     struct rlimit r;  
  9.   
  10.     int pid = fork();  
  11.     LOGI("fork pid: %d", pid);  
  12.     if (pid < 0) {  
  13.         LOGI("first fork() error pid %d,so exit", pid);  
  14.         exit(0);  
  15.     } else if (pid != 0) {  
  16.         LOGI("first fork(): I'am father pid=%d", getpid());  
  17.         //exit(0);  
  18.     } else { //  第一个子进程  
  19.         LOGI("first fork(): I'am child pid=%d", getpid());  
  20.         setsid();  
  21.         LOGI("first fork(): setsid=%d", setsid());  
  22.         umask(0); //为文件赋予更多的权限,因为继承来的文件可能某些权限被屏蔽  
  23.   
  24.         int pid = fork();  
  25.         if (pid == 0) { // 第二个子进程  
  26.             // 这里实际上为了防止重复开启线程,应该要有相应处理  
  27.   
  28.             LOGI("I'am child-child pid=%d", getpid());  
  29.             chdir("/"); //<span style="font-family: Arial, Helvetica, sans-serif;">修改进程工作目录为根目录,chdir(“/”)</span>  
  30.             //关闭不需要的从父进程继承过来的文件描述符。  
  31.             if (r.rlim_max == RLIM_INFINITY) {  
  32.                 r.rlim_max = 1024;  
  33.             }  
  34.             int i;  
  35.             for (i = 0; i < r.rlim_max; i++) {  
  36.                 close(i);  
  37.             }  
  38.   
  39.             umask(0);  
  40.             ret = pthread_create(&id, NULL, (void *) thread, srvname); // 开启线程,轮询去监听启动服务  
  41.             if (ret != 0) {  
  42.                 printf("Create pthread error!\n");  
  43.                 exit(1);  
  44.             }  
  45.             int stdfd = open ("/dev/null", O_RDWR);  
  46.             dup2(stdfd, STDOUT_FILENO);  
  47.             dup2(stdfd, STDERR_FILENO);  
  48.         } else {  
  49.             exit(0);  
  50.         }  
  51.     }  
  52.     return 0;  
  53. }  
  54.   
  55. /** 
  56.  * 启动Service 
  57.  */  
  58. void Java_com_yyh_fork_NativeRuntime_startService(JNIEnv* env, jobject thiz,  
  59.         jstring cchrptr_ProcessName, jstring sdpath) {  
  60.     char * rtn = jstringTostring(env, cchrptr_ProcessName); // 得到进程名称  
  61.     char * sd = jstringTostring(env, sdpath);  
  62.     LOGI("Java_com_yyh_fork_NativeRuntime_startService run....ProcessName:%s", rtn);  
  63.     a = rtn;  
  64.     start(1, rtn, sd);  
  65. }  
这里有几个重点需要理解一下:

1、为什么要fork两次?第一次fork的作用是为后面setsid服务。setsid的调用者不能是进程组组长(group leader),而第一次调用的时候父进程是进程组组长。第二次调用后,把前面一次fork出来的子进程退出,这样第二次fork出来的子进程,就和他们脱离了关系。

2、setsid()作用是什么?setsid() 使得第二个子进程是会话组长(sid==pid),也是进程组组长(pgid == pid),并且脱离了原来控制终端。故不管控制终端怎么操作,新的进程正常情况下不会收到他发出来的这些信号。

3、umask(0)的作用:由于子进程从父进程继承下来的一些东西,可能并未把权限继承下来,所以要赋予他更高的权限,便于子进程操作。

4、chdir ("/");作用:进程活动时,其工作目录所在的文件系统不能卸下,一般需要将工作目录改变到根目录。 

5、进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。所以在最后,记得关闭掉从父进程继承过来的文件描述符。

然后,在上面的代码中开启线程后做的事,就是循环去startService(),代码如下:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. void thread(char* srvname) {  
  2.     while(1){  
  3.         check_and_restart_service(srvname); // 应该要去判断service状态,这里一直restart 是不足之处  
  4.         sleep(4);  
  5.     }  
  6. }  
  7.   
  8. /** 
  9.  * 检测服务,如果不存在服务则启动. 
  10.  * 通过am命令启动一个laucher服务,由laucher服务负责进行主服务的检测,laucher服务在检测后自动退出 
  11.  */  
  12. void check_and_restart_service(char* service) {  
  13.     LOGI("当前所在的进程pid=",getpid());  
  14.     char cmdline[200];  
  15.     sprintf(cmdline, "am startservice --user 0 -n %s", service);  
  16.     char tmp[200];  
  17.     sprintf(tmp, "cmd=%s", cmdline);  
  18.     ExecuteCommandWithPopen(cmdline, tmp, 200);  
  19.     LOGI( tmp, LOG);  
  20. }       
  21.   
  22. /** 
  23.  * 执行命令 
  24.  */  
  25. void ExecuteCommandWithPopen(char* command, char* out_result,  
  26.         int resultBufferSize) {  
  27.     FILE * fp;  
  28.     out_result[resultBufferSize - 1] = '\0';  
  29.     fp = popen(command, "r");  
  30.     if (fp) {  
  31.         fgets(out_result, resultBufferSize - 1, fp);  
  32.         out_result[resultBufferSize - 1] = '\0';  
  33.         pclose(fp);  
  34.     } else {  
  35.         LOGI("popen null,so exit");  
  36.         exit(0);  
  37.     }  
  38. }  

这两个启动服务的函数,里面就涉及到一些Android和Linux的命令了,这里我就不细说了。特别是am,挺强大的功能的,不仅可以开启服务,也可以开启广播等等...然后调用ndk-build命令进行编译,生成so库。


C/C++端关键的部分主要是以上这些,接下来就是Java端调用。

首先来看一下so库的加载类,以及C++函数的调用:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. package com.yyh.fork;  
  2.   
  3. import java.io.DataInputStream;  
  4. import java.io.DataOutputStream;  
  5. import java.io.File;  
  6.   
  7. public class NativeRuntime {  
  8.   
  9.     private static NativeRuntime theInstance = null;  
  10.   
  11.     private NativeRuntime() {  
  12.   
  13.     }  
  14.       
  15.     public static NativeRuntime getInstance() {  
  16.         if (theInstance == null)  
  17.             theInstance = new NativeRuntime();  
  18.         return theInstance;  
  19.     }  
  20.   
  21.     /** 
  22.      * RunExecutable 启动一个可自行的lib*.so文件 
  23.      * @date 2016-1-18 下午8:22:28 
  24.      * @param pacaageName 
  25.      * @param filename 
  26.      * @param alias 别名 
  27.      * @param args 参数 
  28.      * @return 
  29.      */  
  30.     public String RunExecutable(String pacaageName, String filename, String alias, String args) {  
  31.         String path = "/data/data/" + pacaageName;  
  32.         String cmd1 = path + "/lib/" + filename;  
  33.         String cmd2 = path + "/" + alias;  
  34.         String cmd2_a1 = path + "/" + alias + " " + args;  
  35.         String cmd3 = "chmod 777 " + cmd2;  
  36.         String cmd4 = "dd if=" + cmd1 + " of=" + cmd2;  
  37.         StringBuffer sb_result = new StringBuffer();  
  38.   
  39.         if (!new File("/data/data/" + alias).exists()) {  
  40.             RunLocalUserCommand(pacaageName, cmd4, sb_result); // 拷贝lib/libtest.so到上一层目录,同时命名为test.  
  41.             sb_result.append(";");  
  42.         }  
  43.         RunLocalUserCommand(pacaageName, cmd3, sb_result); // 改变test的属性,让其变为可执行  
  44.         sb_result.append(";");  
  45.         RunLocalUserCommand(pacaageName, cmd2_a1, sb_result); // 执行test程序.  
  46.         sb_result.append(";");  
  47.         return sb_result.toString();  
  48.     }  
  49.   
  50.     /** 
  51.      * 执行本地用户命令 
  52.      * @date 2016-1-18 下午8:23:01 
  53.      * @param pacaageName 
  54.      * @param command 
  55.      * @param sb_out_Result 
  56.      * @return 
  57.      */  
  58.     public boolean RunLocalUserCommand(String pacaageName, String command, StringBuffer sb_out_Result) {  
  59.         Process process = null;  
  60.         try {  
  61.             process = Runtime.getRuntime().exec("sh"); // 获得shell进程  
  62.             DataInputStream inputStream = new DataInputStream(process.getInputStream());  
  63.             DataOutputStream outputStream = new DataOutputStream(process.getOutputStream());  
  64.             outputStream.writeBytes("cd /data/data/" + pacaageName + "\n"); // 保证在command在自己的数据目录里执行,才有权限写文件到当前目录  
  65.             outputStream.writeBytes(command + " &\n"); // 让程序在后台运行,前台马上返回  
  66.             outputStream.writeBytes("exit\n");  
  67.             outputStream.flush();  
  68.             process.waitFor();  
  69.             byte[] buffer = new byte[inputStream.available()];  
  70.             inputStream.read(buffer);  
  71.             String s = new String(buffer);  
  72.             if (sb_out_Result != null)  
  73.                 sb_out_Result.append("CMD Result:\n" + s);  
  74.         } catch (Exception e) {  
  75.             if (sb_out_Result != null)  
  76.                 sb_out_Result.append("Exception:" + e.getMessage());  
  77.             return false;  
  78.         }  
  79.         return true;  
  80.     }  
  81.   
  82.     public native void startActivity(String compname);  
  83.   
  84.     public native String stringFromJNI();  
  85.   
  86.     public native void startService(String srvname, String sdpath);  
  87.   
  88.     public native int findProcess(String packname);  
  89.   
  90.     public native int stopService();  
  91.   
  92.     static {  
  93.         try {  
  94.             System.loadLibrary("helper"); // 加载so库  
  95.         } catch (Exception e) {  
  96.             e.printStackTrace();  
  97.         }  
  98.     }  
  99.   
  100. }  
然后,我们在收到开机广播后,启动该服务。

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. package com.yyh.activity;  
  2.   
  3. import android.content.BroadcastReceiver;  
  4. import android.content.Context;  
  5. import android.content.Intent;  
  6. import android.util.Log;  
  7.   
  8. import com.yyh.fork.NativeRuntime;  
  9. import com.yyh.utils.FileUtils;  
  10. public class PhoneStatReceiver extends BroadcastReceiver {  
  11.   
  12.     private String TAG = "tag";  
  13.   
  14.     @Override  
  15.     public void onReceive(Context context, Intent intent) {  
  16.         if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {  
  17.             Log.i(TAG, "手机开机了~~");  
  18.             NativeRuntime.getInstance().startService(context.getPackageName() + "/com.yyh.service.HostMonitor", FileUtils.createRootPath());  
  19.         } else if (Intent.ACTION_USER_PRESENT.equals(intent.getAction())) {  
  20.         }  
  21.     }  
  22.   
  23.       
  24. }  

Service服务里面,就可以做该做的事情。

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. package com.yyh.service;  
  2.   
  3. import android.app.Service;  
  4. import android.content.Intent;  
  5. import android.os.IBinder;  
  6. import android.util.Log;  
  7.   
  8. public class HostMonitor extends Service {  
  9.   
  10.     @Override  
  11.     public void onCreate() {  
  12.         super.onCreate();  
  13.         Log.i("daemon_java""HostMonitor: onCreate! I can not be Killed!");  
  14.     }  
  15.   
  16.     @Override  
  17.     public int onStartCommand(Intent intent, int flags, int startId) {  
  18.         Log.i("daemon_java""HostMonitor: onStartCommand! I can not be Killed!");  
  19.         return super.onStartCommand(intent, flags, startId);  
  20.     }  
  21.   
  22.     @Override  
  23.     public IBinder onBind(Intent arg0) {  
  24.         return null;  
  25.     }  
  26. }  
当然,也不要忘记在Manifest.xml文件配置receiver和service:

[html]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. <receiver  
  2.             android:name="com.yyh.activity.PhoneStatReceiver"  
  3.             android:enabled="true"  
  4.             android:permission="android.permission.RECEIVE_BOOT_COMPLETED" >  
  5.             <intent-filter>  
  6.                 <action android:name="android.intent.action.BOOT_COMPLETED" />  
  7.                 <action android:name="android.intent.action.USER_PRESENT" />  
  8.             </intent-filter>  
  9.         </receiver>  
  10.           
  11.         <service  android:name="com.yyh.service.HostMonitor"  
  12.                   android:enabled="true"  
  13.                   android:exported="true">  
  14.          </service>  

run起来,在程序应用里面,结束掉这个进程,不一会了,又自动起来了~~~~跟流氓软件一个样,没错,就是这么贱... 

这边是运行在谷歌的原生系统上,Android版本为5.0... 在其他系统下稳定性还远远不足,但是要真正做到杀不死服务几乎是不可能的。 总结一下就是:服务常驻要应对的不是各种难的技术,而是各大ROM。QQ为什么不会被杀死,是因为国内各大ROM不想让他死...

本文主要提供的是一个思路,实现还有诸多不足之处,菜鸟之作,不喜勿喷。

最后附上本例的源代码:Android 通过JNI实现双守护进程

Android 进程保活招式大全


http://chuansong.me/n/504099451432

目前市面上的应用,貌似除了微信和手Q都会比较担心被用户或者系统(厂商)杀死问题。本文对 Android 进程拉活进行一个总结。

Android 进程拉活包括两个层面:

A. 提供进程优先级,降低进程被杀死的概率

B. 在进程被杀死后,进行拉活

本文下面就从这两个方面做一下总结。

1. 进程的优先级

Android 系统将尽量长时间地保持应用进程,但为了新建进程或运行更重要的进程,最终需要清除旧进程来回收内存。 为了确定保留或终止哪些进程,系统会根据进程中正在运行的组件以及这些组件的状态,将每个进程放入“重要性层次结构”中。 必要时,系统会首先消除重要性最低的进程,然后是清除重要性稍低一级的进程,依此类推,以回收系统资源。

进程的重要性,划分5级:

  1. 前台进程(Foreground process)

  2. 可见进程(Visible process)

  3. 服务进程(Service process)

  4. 后台进程(Background process)

  5. 空进程(Empty process)


前台进程的重要性最高,依次递减,空进程的重要性最低,下面分别来阐述每种级别的进程

1.1. 前台进程 —— Foreground process

用户当前操作所必需的进程。通常在任意给定时间前台进程都为数不多。只有在内存不足以支持它们同时继续运行这一万不得已的情况下,系统才会终止它们。

A. 拥有用户正在交互的 Activity(已调用 onResume()

B. 拥有某个 Service,后者绑定到用户正在交互的 Activity

C. 拥有正在“前台”运行的 Service(服务已调用 startForeground()

D. 拥有正执行一个生命周期回调的 Service(onCreate()onStart() onDestroy()

E. 拥有正执行其 onReceive() 方法的 BroadcastReceiver

1.2. 可见进程 —— Visible process

没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。

A. 拥有不在前台、但仍对用户可见的 Activity(已调用 onPause())。

B. 拥有绑定到可见(或前台)Activity 的 Service

1.3. 服务进程 —— Service process

尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。

A. 正在运行 startService() 方法启动的服务,且不属于上述两个更高类别进程的进程。

1.4. 后台进程 —— Background process

后台进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU 列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。

A. 对用户不可见的 Activity 的进程(已调用 Activity的onStop() 方法)

1.5. 空进程 —— Empty process

保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。

A. 不含任何活动应用组件的进程

详情参见:

http://developer.android.com/intl/zh-cn/guide/components/processes-and-threads.html

2. Android 进程回收策略

Android 中对于内存的回收,主要依靠 Lowmemorykiller 来完成,是一种根据 OOM_ADJ 阈值级别触发相应力度的内存回收的机制。

关于 OOM_ADJ 的说明如下:


其中红色部分代表比较容易被杀死的 Android 进程(OOM_ADJ>=4),绿色部分表示不容易被杀死的 Android 进程,其他表示非 Android 进程(纯 Linux 进程)。在 Lowmemorykiller 回收内存时会根据进程的级别优先杀死 OOM_ADJ 比较大的进程,对于优先级相同的进程则进一步受到进程所占内存和进程存活时间的影响。

Android 手机中进程被杀死可能有如下情况:


综上,可以得出减少进程被杀死概率无非就是想办法提高进程优先级,减少进程在内存不足等情况下被杀死的概率。

3. 提升进程优先级的方案

3.1. 利用 Activity 提升权限
3.1.1. 方案设计思想

监控手机锁屏解锁事件,在屏幕锁屏时启动1个像素的 Activity,在用户解锁时将 Activity 销毁掉。注意该 Activity 需设计成用户无感知。

通过该方案,可以使进程的优先级在屏幕锁屏时间由4提升为最高优先级1。

3.1.2. 方案适用范围

适用场景: 本方案主要解决第三方应用及系统管理工具在检测到锁屏事件后一段时间(一般为5分钟以内)内会杀死后台进程,已达到省电的目的问题。

适用版本: 适用于所有的 Android 版本。

3.1.3. 方案具体实现

首先定义 Activity,并设置 Activity 的大小为1像素:


其次,从 AndroidManifest 中通过如下属性,排除 Activity 在 RecentTask 中的显示:


最后,控制 Activity 为透明:


Activity 启动与销毁时机的控制:


3.2. 利用 Notification 提升权限
3.2.1. 方案设计思想

Android 中 Service 的优先级为4,通过 setForeground 接口可以将后台 Service 设置为前台 Service,使进程的优先级由4提升为2,从而使进程的优先级仅仅低于用户当前正在交互的进程,与可见进程优先级一致,使进程被杀死的概率大大降低。

3.2.2. 方案实现挑战

从 Android2.3 开始调用 setForeground 将后台 Service 设置为前台 Service 时,必须在系统的通知栏发送一条通知,也就是前台 Service 与一条可见的通知时绑定在一起的。

对于不需要常驻通知栏的应用来说,该方案虽好,但却是用户感知的,无法直接使用。

3.2.3. 方案挑战应对措施

通过实现一个内部 Service,在 LiveService 和其内部 Service 中同时发送具有相同 ID 的 Notification,然后将内部 Service 结束掉。随着内部 Service 的结束,Notification 将会消失,但系统优先级依然保持为2。

3.2.4. 方案适用范围

适用于目前已知所有版本。

3.2.5. 方案具体实现



4. 进程死后拉活的方案

4.1. 利用系统广播拉活
4.1.1. 方案设计思想

在发生特定系统事件时,系统会发出响应的广播,通过在 AndroidManifest 中“静态”注册对应的广播监听器,即可在发生响应事件时拉活。

常用的用于拉活的广播事件包括:


4.1.2. 方案适用范围

适用于全部 Android 平台。但存在如下几个缺点:

1) 广播接收器被管理软件、系统软件通过“自启管理”等功能禁用的场景无法接收到广播,从而无法自启。

2) 系统广播事件不可控,只能保证发生事件时拉活进程,但无法保证进程挂掉后立即拉活。

因此,该方案主要作为备用手段。

4.2. 利用第三方应用广播拉活
4.2.1. 方案设计思想

该方案总的设计思想与接收系统广播类似,不同的是该方案为接收第三方 Top 应用广播。

通过反编译第三方 Top 应用,如:手机QQ、微信、支付宝、UC浏览器等,以及友盟、信鸽、个推等 SDK,找出它们外发的广播,在应用中进行监听,这样当这些应用发出广播时,就会将我们的应用拉活。

4.2.2. 方案适用范围

该方案的有效程度除与系统广播一样的因素外,主要受如下因素限制:

1) 反编译分析过的第三方应用的多少

2) 第三方应用的广播属于应用私有,当前版本中有效的广播,在后续版本随时就可能被移除或被改为不外发。

这些因素都影响了拉活的效果。

4.3. 利用系统Service机制拉活
4.3.1. 方案设计思想

将 Service 设置为 START_STICKY,利用系统机制在 Service 挂掉后自动拉活:


4.3.2. 方案适用范围

如下两种情况无法拉活:

  1. Service 第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内 Service 被杀死达到5次,则系统不再拉起。

  2. 进程被取得 Root 权限的管理工具或系统工具通过 forestop 停止掉,无法重启。

4.4. 利用Native进程拉活
4.4.1. 方案设计思想

主要思想:利用 Linux 中的 fork 机制创建 Native 进程,在 Native 进程中监控主进程的存活,当主进程挂掉后,在 Native 进程中立即对主进程进行拉活。

主要原理:在 Android 中所有进程和系统组件的生命周期受 ActivityManagerService 的统一管理。而且,通过 Linux 的 fork 机制创建的进程为纯 Linux 进程,其生命周期不受 Android 的管理。

4.4.2. 方案实现挑战

挑战一:在 Native 进程中如何感知主进程死亡。

要在 Native 进程中感知主进程是否存活有两种实现方式:

  1. 在 Native 进程中通过死循环或定时器,轮训判断主进程是否存活,档主进程不存活时进行拉活。该方案的很大缺点是不停的轮询执行判断逻辑,非常耗电。

  2. 在主进程中创建一个监控文件,并且在主进程中持有文件锁。在拉活进程启动后申请文件锁将会被堵塞,一旦可以成功获取到锁,说明主进程挂掉,即可进行拉活。由于 Android 中的应用都运行于虚拟机之上,Java 层的文件锁与 Linux 层的文件锁是不同的,要实现该功能需要封装 Linux 层的文件锁供上层调用。

封装 Linux 文件锁的代码如下:


Native 层中堵塞申请文件锁的部分代码:


挑战二:在 Native 进程中如何拉活主进程。

通过 Native 进程拉活主进程的部分代码如下,即通过 am 命令进行拉活。通过指定“—include-stopped-packages”参数来拉活主进程处于 forestop 状态的情况。


挑战三:如何保证 Native 进程的唯一。

从可扩展性和进程唯一等多方面考虑,将 Native 进程设计层 C/S 结构模式,主进程与 Native 进程通过 Localsocket 进行通信。在Native进程中利用 Localsocket 保证 Native 进程的唯一性,不至于出现创建多个 Native 进程以及 Native 进程变成僵尸进程等问题。


4.4.3. 方案适用范围

该方案主要适用于 Android5.0 以下版本手机。

该方案不受 forcestop 影响,被强制停止的应用依然可以被拉活,在 Android5.0 以下版本拉活效果非常好。

对于 Android5.0 以上手机,系统虽然会将native进程内的所有进程都杀死,这里其实就是系统“依次”杀死进程时间与拉活逻辑执行时间赛跑的问题,如果可以跑的比系统逻辑快,依然可以有效拉起。记得网上有人做过实验,该结论是成立的,在某些 Android 5.0 以上机型有效。

4.5. 利用 JobScheduler 机制拉活
4.5.1. 方案设计思想

Android5.0 以后系统对 Native 进程等加强了管理,Native 拉活方式失效。系统在 Android5.0 以上版本提供了 JobScheduler 接口,系统会定时调用该进程以使应用进行一些逻辑操作。

在本项目中,我对 JobScheduler 进行了进一步封装,兼容 Android5.0 以下版本。封装后 JobScheduler 接口的使用如下:



4.5.2. 方案适用范围

该方案主要适用于 Android5.0 以上版本手机。

该方案在 Android5.0 以上版本中不受 forcestop 影响,被强制停止的应用依然可以被拉活,在 Android5.0 以上版本拉活效果非常好。

仅在小米手机可能会出现有时无法拉活的问题。

4.6. 利用账号同步机制拉活
4.6.1. 方案设计思想

Android 系统的账号同步机制会定期同步账号进行,该方案目的在于利用同步机制进行进程的拉活。添加账号和设置同步周期的代码如下:


该方案需要在 AndroidManifest 中定义账号授权与同步服务。


4.6.2. 方案适用范围

该方案适用于所有的 Android 版本,包括被 forestop 掉的进程也可以进行拉活。

最新 Android 版本(Android N)中系统好像对账户同步这里做了变动,该方法不再有效。

5. 其他有效拉活方案

经研究发现还有其他一些系统拉活措施可以使用,但在使用时需要用户授权,用户感知比较强烈。

这些方案包括:

  1. 利用系统通知管理权限进行拉活

  2. 利用辅助功能拉活,将应用加入厂商或管理软件白名单。

这些方案需要结合具体产品特性来搞。

上面所有解释这些方案都是考虑的无 Root 的情况。

其他还有一些技术之外的措施,比如说应用内 Push 通道的选择:

  1. 国外版应用:接入 Google 的 GCM。

  2. 国内版应用:根据终端不同,在小米手机(包括 MIUI)接入小米推送、华为手机接入华为推送;其他手机可以考虑接入腾讯信鸽或极光推送与小米推送做 A/B Test。




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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值