分享

Android 通过JNI实现守护进程

 万皇之皇 2018-01-03



开发一个需要常住后台的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,这个也主要是针对系统资源不足而导致的服务被关闭,还是有一定的道理的。

应对的方法是有,实现起来都比较繁琐。如果你自己可以定制ROM,那就有很多种办法了,比如把你的应用加入白名单,或是多安装一个没有图标的app作为守护进程…但是,哪能什么都是定制的,对于安卓开发者来说,这个难题必须攻破。

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

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

Process.killProcessQuiet(pid);

应用退出后,ActivityManagerService就把主进程给杀死了,但是,在Android5.0中,ActivityManagerService却是这样处理的:

Process.killProcessQuiet(app.pid);  
Process.killProcessGroup(app.info.uid, app.pid);

就差了一句话,却差别很大。Android5.0在应用退出后,ActivityManagerService不仅把主进程给杀死,另外把主进程所属的进程组一并杀死,这样一来,由于子进程和主进程在同一进程组,子进程在做的事情,也就停止了…要不怎么说Android5.0在安全方面做了很多更新呢…

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

/**  
* srvname  进程名  
* sd 之前创建子进程的pid写入的文件路径  
*/
 int start(int argc, char* srvname, char* sd) {  
   pthread_t id;      
   int ret;      
   struct rlimit r;      
   
   int pid = fork();      
   LOGI('fork pid: %d', pid);      
   if (pid <>0) {    
       LOGI('first fork() error pid %d,so exit', pid);          
       exit(0);      
   } else if (pid != 0) {  
       LOGI('first fork(): I'am father pid=%d', getpid());          
       //exit(0);      
   } else { //  第一个子进程    
       LOGI('first fork(): I'am child pid=%d', getpid());          
       setsid();          
       LOGI('first fork(): setsid=%d', setsid());          
       umask(0); //为文件赋予更多的权限,因为继承来的文件可能某些权限被屏蔽          
       
       int pid = fork();          
       if (pid == 0) { // 第二个子进程      
           // 这里实际上为了防止重复开启线程,应该要有相应处理              
           
           LOGI('I'am child-child pid=%d', getpid());              
           chdir('/'); //修改进程工作目录为根目录,chdir(“/”)              
           //关闭不需要的从父进程继承过来的文件描述符。              
           if (r.rlim_max == RLIM_INFINITY) {                  
               r.rlim_max = 1024;            
           }              
           int i;              
           for (i = 0; i < r.rlim_max;="" i++)="" {=""  =""  =""  =""  =""  =""  =""  =""  ="">
                close(i);              
           }              
           
           umask(0);              
           ret = pthread_create(&id, NULL, (void *) thread, srvname); // 开启线程,轮询去监听启动服务              
           if (ret != 0) {          
               printf('Create pthread error!);            
               exit(1);        
           }              
           int stdfd = open ('/dev/null', O_RDWR);              
           dup2(stdfd, STDOUT_FILENO);              
           dup2(stdfd, STDERR_FILENO);      
       } else {          
           exit(0);          
        }      
   }      
   return 0;  
}
 /**  
* 启动Service  
*/
 void Java_com_yyh_fork_NativeRuntime_startService(JNIEnv* env, jobject thiz,  
       jstring cchrptr_ProcessName, jstring sdpath) {  
    char * rtn = jstringTostring(env, cchrptr_ProcessName); // 得到进程名称      
    char * sd = jstringTostring(env, sdpath);      
    LOGI('Java_com_yyh_fork_NativeRuntime_startService run....ProcessName:%s', rtn);      
    a = rtn;      
    start(1, rtn, sd);  
}

这里有几个重点需要理解一下:

1、为什么要fork两次?第一次fork的作用是为后面setsid服务。setsid的调用者不能是进程组组长(group leader),而第一次调用的时候父进程是进程组组长。第二次调用后,把前面一次fork出来的子进程退出,这样第二次fork出来的子进程,就和他们脱离了关系。
2、setsid()作用是什么?setsid() 使得第二个子进程是会话组长(sid==pid),也是进程组组长(pgid == pid),并且脱离了原来控制终端。故不管控制终端怎么操作,新的进程正常情况下不会收到他发出来的这些信号。
3、umask(0)的作用:由于子进程从父进程继承下来的一些东西,可能并未把权限继承下来,所以要赋予他更高的权限,便于子进程操作。
4、chdir (“/“);作用:进程活动时,其工作目录所在的文件系统不能卸下,一般需要将工作目录改变到根目录。
5、进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。所以在最后,记得关闭掉从父进程继承过来的文件描述符。

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

void thread(char* srvname) {      
   while(1){          
       check_and_restart_service(srvname); // 应该要去判断service状态,这里一直restart 是不足之处          
       sleep(4);      
   }  
}
 /**  
* 检测服务,如果不存在服务则启动.  
* 通过am命令启动一个laucher服务,由laucher服务负责进行主服务的检测,laucher服务在检测后自动退出  
*/
 void check_and_restart_service(char* service) {      
   LOGI('当前所在的进程pid=',getpid());      
   char cmdline[200];      
   sprintf(cmdline, 'am startservice --user 0 -n %s', service);      
   char tmp[200];      
   sprintf(tmp, 'cmd=%s', cmdline);      
   ExecuteCommandWithPopen(cmdline, tmp, 200);      
   LOGI( tmp, LOG);  
}      
/**  
* 执行命令  
*/
 void ExecuteCommandWithPopen(char* command, char* out_result,      
       int resultBufferSize) {      
    FILE * fp;      
    out_result[resultBufferSize - 1] = '';      
    fp = popen(command, 'r');      
    if (fp) {    
        fgets(out_result, resultBufferSize - 1, fp);          
        out_result[resultBufferSize - 1] = '';          
        pclose(fp);      
    } else {          
        LOGI('popen null,so exit');          
        exit(0);      
    }  
}

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

C/C++端关键的部分主要是以上这些,自然而然,Java端还得配合执行。

首先来看一下C/C++代码编译完的so库的加载类,以及native的调用:

package com.yyh.fork;  

import java.io.DataInputStream;  
import java.io.DataOutputStream;  
import java.io.File;  

public class NativeRuntime {      

   private static NativeRuntime theInstance = null;      
   private NativeRuntime() {      
   
   }      
   
   public static NativeRuntime getInstance() {          
       if (theInstance == null)              
           theInstance = new NativeRuntime();          
       return theInstance;      
   }      
   
   /**     
    * RunExecutable 启动一个可自行的lib*.so文件          
    * @date 2016-1-18 下午8:22:28     
    * @param pacaageName     
    * @param filename     
    * @param alias 别名     
    * @param args 参数     
    * @return     
    */
     
   public String RunExecutable(String pacaageName, String filename, String alias, String args) {          
       String path = '/data/data/' + pacaageName;          
       String cmd1 = path + '/lib/' + filename;          
       String cmd2 = path + '/' + alias;          
       String cmd2_a1 = path + '/' + alias + ' ' + args;          
       String cmd3 = 'chmod 777 ' + cmd2;          
       String cmd4 = 'dd if=' + cmd1 + ' of=' + cmd2;          
       StringBuffer sb_result = new StringBuffer();          
       
       if (!new File('/data/data/' + alias).exists()) {            
           RunLocalUserCommand(pacaageName, cmd4, sb_result); // 拷贝lib/libtest.so到上一层目录,同时命名为test.              
           sb_result.append(';');      
       }          
       RunLocalUserCommand(pacaageName, cmd3, sb_result); // 改变test的属性,让其变为可执行          
       sb_result.append(';');          
       RunLocalUserCommand(pacaageName, cmd2_a1, sb_result); // 执行test程序.          
       sb_result.append(';');          
       return sb_result.toString();  
   }      
   /**     
    * 执行本地用户命令     
    * @date 2016-1-18 下午8:23:01     
    * @param pacaageName     
    * @param command     
    * @param sb_out_Result     
    * @return     
    */
     
   public boolean RunLocalUserCommand(String pacaageName, String command, StringBuffer sb_out_Result) {  
        Process process = null;          
        try {      
            process = Runtime.getRuntime().exec('sh'); // 获得shell进程              
            DataInputStream inputStream = new DataInputStream(process.getInputStream());              
            DataOutputStream outputStream = new DataOutputStream(process.getOutputStream());              
            outputStream.writeBytes('cd /data/data/' + pacaageName + ''); // 保证在command在自己的数据目录里执行,才有权限写文件到当前目录              
            outputStream.writeBytes(command + ' &'); // 让程序在后台运行,前台马上返回              
            outputStream.writeBytes('exit');              
            outputStream.flush();              
            process.waitFor();              
            byte[] buffer = new byte[inputStream.available()];              
            inputStream.read(buffer);              
            String s = new String(buffer);              
            if (sb_out_Result != null)                  
                sb_out_Result.append('CMD Result:' + s);    
         } catch (Exception e) {            
             if (sb_out_Result != null)                  
                 sb_out_Result.append('Exception:' + e.getMessage());              
                 return false;          
         }          
         return true;    
     }      
    
     public native void startActivity(String compname);      
     
     public native String stringFromJNI();      
     
     public native void startService(String srvname, String sdpath);      
     
     public native int findProcess(String packname);      
     
     public native int stopService();      
     
     static {          
         try {              
             System.loadLibrary('helper'); // 加载so库          
         } catch (Exception e) {              
             e.printStackTrace();          
         }      
    }  
}

然后,我们在收到开机广播后,启动该服务。

package com.yyh.activity;  

import android.content.BroadcastReceiver;  
import android.content.Context;  
import android.content.Intent;  
import android.util.Log;  

import com.yyh.fork.NativeRuntime;  
import com.yyh.utils.FileUtils;  
public class PhoneStatReceiver extends BroadcastReceiver {      
   
   private String TAG = 'tag';      
   
   @Override      
   public void onReceive(Context context, Intent intent) {  
       if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {              
           Log.i(TAG, '手机开机了~~');              
           NativeRuntime.getInstance().startService(context.getPackageName() + '/com.yyh.service.HostMonitor', FileUtils.createRootPath());          
       } else if (Intent.ACTION_USER_PRESENT.equals(intent.getAction())) {          
       }      
   }  
   
   
}

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

package com.yyh.service;  

import android.app.Service;  
import android.content.Intent;  
import android.os.IBinder;  
import android.util.Log;  

public class HostMonitor extends Service {      
   
   @Override      
   public void onCreate() {          
       super.onCreate();          
       Log.i('daemon_java', 'HostMonitor: onCreate! I can not be Killed!');      
   }      
   
   @Override      
   public int onStartCommand(Intent intent, int flags, int startId) {          
       Log.i('daemon_java', 'HostMonitor: onStartCommand! I can not be Killed!');          
       return super.onStartCommand(intent, flags, startId);    
   }      
   
   @Override      
   public IBinder onBind(Intent arg0) {    
       return null;      
   }  
}

当然,也不要忘记在Manifest.xml文件配置receiver和service:


           android:name='com.yyh.activity.PhoneStatReceiver'              
           android:enabled='true'              
           android:permission='android.permission.RECEIVE_BOOT_COMPLETED' >              
           intent-filter>          
              action android:name='android.intent.action.BOOT_COMPLETED' />                  
              action android:name='android.intent.action.USER_PRESENT' />      
           intent-filter>  
       receiver>          
       
       service  android:name='com.yyh.service.HostMonitor'                    
                 android:enabled='true'                    
                 android:exported='true'>
   
       service>

run起来,在程序应用里面,结束掉这个进程,不一会了,又自动起来,没错,就是这么贱,就是这么霸道!!

这边是运行在谷歌的原生系统上,Android版本为5.0…总结一下就是:服务常驻要应对的不是各种难的技术,而是各大ROM。QQ为什么不会被杀死,是因为国内各大ROM不想让他死…

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


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多