为了账号安全,请及时绑定邮箱和手机立即绑定

Android 通过JNI实现守护进程,使得Service服务不被杀死

标签:
Android

开发一个需要常住后台的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)是如何关闭在应用退出后清理内存的:

 

1.      Process.killProcessQuiet(pid);  

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

 

1.      Process.killProcessQuiet(app.pid);  

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

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

 

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

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

/**

 * 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) { // 第二个子进程 

            FILE    *fp; 

            sprintf(sd,"%s/pid",sd);   

            if((fp=fopen(sd,"a"))==NULL)   {//打开文件 没有就创建 

                LOGI("%s文件还未创建!",sd); 

                ftruncate(fp,   0); 

                lseek(fp,   0, SEEK_SET); 

            }   

            fclose(fp);   

            fp=fopen(sd,"rw");   

            if(fp>0){   

                char   buff1[6]; 

                int   p = 0; 

                memset(buff1,0,sizeof(buff1));   

                fseek(fp,0,SEEK_SET);   

                fgets(buff1,6,fp);    //读取一行(pid)   

                LOGI("读取的进程号:%s",buff1); 

                if(strlen(buff1)>1){   // 有值 

                    kill(atoi(buff1),   SIGTERM); // 把上一次的进程干掉,防止重复执行 

                    LOGI("杀死进程,pid=%d",atoi(buff1)); 

                }   

            }   

            fclose(fp);   

            fp=fopen(sd,"w");   

            char   buff[100]; 

            int   k = 3; 

            if(fp>0){   

                sprintf(buff,"%lu",getpid());   

                fprintf(fp,"%s\n",buff);   // 把进程号写入文件 

            }   

            fclose(fp);   

            fflush(fp);   

   

            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(),代码如下:

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

void thread(char* srvname) { 

    while(1){ 

        check_and_restart_service(srvname);   

        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库,ndk不会的,自行百度咯~


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

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

[代码]java代码:

?

001

002

003

004

005

006

007

008

009

010

011

012

013

014

015

016

017

018

019

020

021

022

023

024

025

026

027

028

029

030

031

032

033

034

035

036

037

038

039

040

041

042

043

044

045

046

047

048

049

050

051

052

053

054

055

056

057

058

059

060

061

062

063

064

065

066

067

068

069

070

071

072

073

074

075

076

077

078

079

080

081

082

083

084

085

086

087

088

089

090

091

092

093

094

095

096

097

098

099

100

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 + "\n"); // 保证在command在自己的数据目录里执行,才有权限写文件到当前目录 

            outputStream.writeBytes(command   + " &\n"); // 让程序在后台运行,前台马上返回 

            outputStream.writeBytes("exit\n");   

            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:\n" + 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();   

        }   

    } 

   

}

 

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

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

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服务里面,就可以做该做的事情。

[代码]java代码:

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

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:

[代码]xml代码:

?

1

2

3

4

5

6

7

8

9

<receiver 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"> 

            </action></action></intent-filter>   

        </receiver>   

           

        <service   android:name="com.yyh.service.HostMonitor" android:enabled="true"   android:exported="true"> 

         </service>

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

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

原文链接:http://www.apkbus.com/blog-705730-60652.html

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消