Android 通过JNI实现守护进程(上)

来源:LeBron_Six  

链接: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)是如何关闭在应用退出后清理内存的:


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("/"); //<span style="font-family: Arial, Helvetica, sans-serif;">修改进程工作目录为根目录,chdir(“/”)</span>  

            //关闭不需要的从父进程继承过来的文件描述符。  

            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!\n");  

                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] = '\0';  

    fp = popen(command, "r");  

    if (fp) {  

        fgets(out_result, resultBufferSize - 1, fp);  

        out_result[resultBufferSize - 1] = '\0';  

        pclose(fp);  

    } else {  

        LOGI("popen null,so exit");  

        exit(0);  

    }  

}


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


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值