如何让你的app一直在运行状态呢?

默认情况下,不做任何跨进程部署配置的话,每个android app运行在单独一个虚拟机上,每个虚拟机对应一个进程。当app被系统回收或者是被用户主动杀掉(通过app管理软件),进程就彻底退出了。

在有些场景,app所在的进程退出了,我们希望还能做一些操作。比如,app被卸载后(卸载会先退出运行),我们希望跳转浏览器做一些卸载原因的调查问卷;或者为了优化体验,提高app热启动速度,再比如,有些监控的app,我们希望一直在运行,否则可能数据不准确,这就需要app所在进程退出后能自我启动。

为了做到app停止运行状态的监控和执行回调,典型的解决方案就是多进程相互守护。

去你的手机上瞧瞧,设置-应用管理-运行中

multiprocess

双进程相互守护

双进程A和B相互守护,当A进程检测到B进程退出时,A进程重新启动B进程,同理当A进程退出时,B重新启动A进程。进程的运行状态的检测是实现进程守护的关键点,比较容易想到的方案有:

  1. 通过轮训的方式查询远端进程的状态
  2. 通过发送心跳包来看是否能接受应答,无应答,远端进程可能已经退出
  3. 在Application生命周期终止时,也就是onTerminate方法里发送广播通知对方自己即将灭亡

通过轮训的方式,只要通过ps命令查询状态,无需A和B做进程间通信;心跳包应答检测的方式需要通过socket或者别的IPC机制来做通信。

守护进程可以是一个运行android app的进程(Dalvik进程),也可以是一个linux的原生进程,如果是Dalvik进程,当app被卸载时,进程会被退出,字节码被移除,无法再运行任何逻辑比如跳转浏览器页面。

如果守护进程是linux系统里fork一个新的进程,与app不在同一个进程空间,当app被关闭或者杀掉或者卸载的时候,不会影响守护的运行状态,也就是说守护还是处于运行状态,可以执行相应操作。因此守护可以监控app的运行状态,发现app停止运行时,可以发送命令启动app进程,保证app的生存,这对某些系统监控的app来说至关重要。而且linux上的进程,通过android上的PackageManger获取不到,也不会在app管理软件的运行软件之列,基本上不会被杀掉,守护本身可以相对可靠的生存。

本文介绍一下,如何在linux 上fork一个原生进程来守护Dalvik app的进程。

linux原生进程守护

介绍几个linux下的几个api


1. int daemon (int nochdir, int noclose);



2. 将程序将运行在后台,成为一个daemon程序,而linux下大多的服务都是以此方式运行的


1. int getopt_long(int argc, char const argv[],       const char *optstring,  



2. const struct option *longopts, int *longindex);



3. 参数解析的帮助函数,当命令参数多个而且有些课选参数时,如果只按顺序接受参数容易混乱,如果按参数名来对应则便利很多,可以参考一下


1. FILE  popen(const char command , const char *type );



2. popen()创建一个管道,fork一个新的进程执行shell命令,popen()的返回值是个标准I/O流,必须由pclose来终止。前面提到这个流是单向的(只能用于读或写)。向这个流写内容相当于写入该命令的标准输入,命令的标准输出和调用popen()的进程相同;与之相反的,从流中读数据相当于读取命令的标准输出,命令的标准输入和调用popen()的进程相同.


1. int system(const char * string);



2. 函数说明



3. system()会调用fork()产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令,此命>令执行完后随即返回原调用的进程。在调用system()期间SIGCHLD 信号会被暂时搁置,SIGINT和SIGQUIT 信号则会被忽略。



4. 返回值


守护流程图

守护流程图

守护的程序代码


01. 
   int main( int argc, char* argv[]  )  
 
  
 
   02. 
   {  
 
  
 
   03. 
   signal(SIGTERM, SIG_IGN);
 
  
 
   04. 
    
 
  
 
   05. 
   const char *process_name = NULL;
 
  
 
   06. 
   const char *package_name = NULL;
 
  
 
   07. 
   const char *activity_name = NULL;
 
  
 
   08. 
   int interval_sec = 30;
 
  
 
   09. 
    
 
  
 
   10. 
   struct option options[] =
 
  
 
   11. 
   {
 
  
 
   12. 
   { "process_name", required_argument, 0, 'p' },
 
  
 
   13. 
   { "package_name", required_argument, 0, 'a' },
 
  
 
   14. 
   { "activity_name", required_argument, 0, 'c' },
 
  
 
   15. 
   { "interval_sec", required_argument, 0, 'i' },
 
  
 
   16. 
   { 0, 0, 0, 0 }
 
  
 
   17. 
   };
 
  
 
   18. 
    
 
  
 
   19. 
   int c;
 
  
 
   20. 
    
 
  
 
   21. 
   for (;;)
 
  
 
   22. 
   {
 
  
 
   23. 
   c = getopt_long(argc, argv, "p:a:c:i:", options, NULL);
 
  
 
   24. 
   if (c == -1)
 
  
 
   25. 
   {
 
  
 
   26. 
   break;
 
  
 
   27. 
   }
 
  
 
   28. 
   switch (c)
 
  
 
   29. 
   {
 
  
 
   30. 
   case 'p':
 
  
 
   31. 
   process_name = optarg;
 
  
 
   32. 
   break;
 
  
 
   33. 
   case 'a':
 
  
 
   34. 
   package_name = optarg;
 
  
 
   35. 
   break;
 
  
 
   36. 
   case 'c':
 
  
 
   37. 
   activity_name = optarg;
 
  
 
   38. 
   break;
 
  
 
   39. 
   case 'i':
 
  
 
   40. 
   interval_sec = atoi(optarg);
 
  
 
   41. 
   break;
 
  
 
   42. 
   default:
 
  
 
   43. 
   exit(EXIT_FAILURE);
 
  
 
   44. 
   }
 
  
 
   45. 
   }
 
  
 
   46. 
    
 
  
 
   47. 
   if (process_name == NULL || package_name == NULL || activity_name == NULL)
 
  
 
   48. 
   exit(EXIT_FAILURE);
 
  
 
   49. 
    
 
  
 
   50. 
   daemon(1, 1);
 
  
 
   51. 
    
 
  
 
   52. 
   run_service(process_name, package_name, activity_name, 10);
 
  
 
   53. 
    
 
  
 
   54. 
   return 0;
 
  
 
   55. 
   }

run_service的实现


01. 
   int chk_process(const char *process_name)
 
  
 
   02. 
   {
 
  
 
   03. 
   FILE   *stream;
 
  
 
   04. 
   char   *line = NULL;
 
  
 
   05. 
   size_t len = 0;
 
  
 
   06. 
   ssize_t read_len;
 
  
 
   07. 
    
 
  
 
   08. 
   stream = popen( "ps", "r" );
 
  
 
   09. 
   if (stream == NULL)
 
  
 
   10. 
   return -1;
 
  
 
   11. 
    
 
  
 
   12. 
   int exists = 0;
 
  
 
   13. 
   while ( (read_len = getline(&line, &len,  stream)) != -1)
 
  
 
   14. 
   {
 
  
 
   15. 
   int len = strlen(line);
 
  
 
   16. 
   char *cmd = line + len;
 
  
 
   17. 
   while ( len >0 ) 
 
  
 
   18. 
   {    
 
  
 
   19. 
   len--;
 
  
 
   20. 
   if ( *cmd == ' ')
 
  
 
   21. 
   {
 
  
 
   22. 
   cmd++;
 
  
 
   23. 
   break;
 
  
 
   24. 
   }
 
  
 
   25. 
    
 
  
 
   26. 
   cmd--;
 
  
 
   27. 
   }
 
  
 
   28. 
    
 
  
 
   29. 
   if( strncmp(cmd, process_name, strlen(process_name)) == 0 )
 
  
 
   30. 
   {
 
  
 
   31. 
   exists = 1;
 
  
 
   32. 
   break;
 
  
 
   33. 
   }
 
  
 
   34. 
   }
 
  
 
   35. 
    
 
  
 
   36. 
   pclose( stream );
 
  
 
   37. 
   if ( line != NULL )
 
  
 
   38. 
   free(line);
 
  
 
   39. 
    
 
  
 
   40. 
   return exists;
 
  
 
   41. 
   }
 
  
 
   42. 
    
 
  
 
   43. 
   void run_service(const char *process_name, const char *package_name, const char *activity_name, int interval_sec)
 
  
 
   44. 
   {
 
  
 
   45. 
   while (1)
 
  
 
   46. 
   {
 
  
 
   47. 
   if ( chk_process(process_name) == 0)
 
  
 
   48. 
   {
 
  
 
   49. 
   char *pkg_activity_name = NULL;
 
  
 
   50. 
   // 格式化命令
 
  
 
   51. 
   asprintf(&pkg_activity_name, "/system/bin/am start --user 0 -n %s/%s", package_name, activity_name);
 
  
 
   52. 
   system(pkg_activity_name);// 执行命令启动app
 
  
 
   53. 
   free(pkg_activity_name);
 
  
 
   54. 
   }
 
  
 
   55. 
   // sleep 指定时间间隔
 
  
 
   56. 
   sleep(interval_sec);
 
  
 
   57. 
   }
 
  
 
   58. 
    
 
  
 
   59. 
   return;
 
  
 
   60. 
   }

编译成功后生成xxx,重命名为xxx.so,把文件拷贝到libs下,这样安装后该文件会被同动态库一起拷贝到data/data/app_package目录下,编写拷贝和chmod相关逻辑的代码,大概流程如下

  1. path = "/data/data/" +packageName; // 安装后的app路径
  2. 执行shell命令:dd if= path+lib/xxx.so of=path/xxx ;//拷贝到app路径下,重命名为xxx
  3. 赋可执行权限 chmod 777 path/xxx;
  4. 运行可执行文件 path/xxx -p process_name -a pkgname ..(别的参数)

需要注意的一点:
这里的操作都是通过执行shell来完成的,需要先cd到app 路径下,才会有读写权限。


01. 
   public static boolean execCommand(String command, String packageName) {
 
  
 
   02. 
   Process process = null;
 
  
 
   03. 
   try {
 
  
 
   04. 
   process = Runtime.getRuntime().exec("sh");  //获得shell.
 
  
 
   05. 
   DataInputStream inputStream = new DataInputStream(process.getInputStream());
 
  
 
   06. 
   DataOutputStream outputStream = new DataOutputStream(process.getOutputStream());
 
  
 
   07. 
    
 
  
 
   08. 
   //保证在command在自己的数据目录里执行,才有权限写文件到当前目录
 
  
 
   09. 
   outputStream.writeBytes("cd /data/data/" + packageName + "\n");   
 
  
 
   10. 
    
 
  
 
   11. 
   outputStream.writeBytes(command + " \n");
 
  
 
   12. 
   outputStream.writeBytes("exit\n");
 
  
 
   13. 
   outputStream.flush();
 
  
 
   14. 
   process.waitFor();
 
  
 
   15. 
    
 
  
 
   16. 
   byte[] buffer = new byte[inputStream.available()];
 
  
 
   17. 
   inputStream.read(buffer);
 
  
 
   18. 
   String s = new String(buffer);
 
  
 
   19. 
   } catch (Exception e) {
 
  
 
   20. 
   return false;
 
  
 
   21. 
   }
 
  
 
   22. 
   return true;
 
  
 
   23. 
   }

编好代码打包测试时,通过app管理界面停止app的运行,看看app是否会被重新启动。