Android通过JNI实现守护进程
作者:LeBron_Six 发布时间:2021-11-24 20:38:02
开发一个需要常住后台的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("/"); //<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库,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 + "\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();
}
}
}
然后,我们在收到开机广播后,启动该服务。
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:
<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" />
</intent-filter>
</receiver>
<service android:name="com.yyh.service.HostMonitor"
android:enabled="true"
android:exported="true">
</service>
run起来,在程序应用里面,结束掉这个进程,不一会了,又自动起来了~~~~完美~~~~跟流氓软件一个样,没错,就是这么贱,就是这么霸道!!
这边是运行在谷歌的原生系统上,Android版本为5.0...总结一下就是:服务常驻要应对的不是各种难的技术,而是各大ROM。QQ为什么不会被杀死,是因为国内各大ROM不想让他死...
本文主要提供的是一个思路,实现还有诸多不足之处,菜鸟之作,不喜勿喷。
最后附上本例的源代码:Android 通过JNI实现双守护进程


猜你喜欢
- 前言:经常会看到有一些app的banner界面可以实现循环播放多个广告图片和手动滑动循环。本以为单纯的ViewPager就可以实
- 访问权限符:(1)public:对于成员来说:任何其他类都可以访问它们,不管在同一个包中还是在另外的包中。对于类来说:  
- 1.JavaBean转Map1.1.简介这篇博客是通过反射来进行实现转换的在学习redis中,发现了一个知识点,就是Java对象转map,视
- 升级到grails 2.3.2之后,运行时报如下的异常:Exception in thread "main"Error
- Android中处理图像是一件很常见的事情,这里记录备忘一些亲身使用过的处理图片数据的方法。转为BitmapRGB值转Bitmappriva
- 本文讲述了Android版Intent.ACTION_SEND分享图片和文字内容。分享给大家供大家参考,具体如下:编辑推荐:稀土掘金,这是一
- 目录1、以下关于java封装的描述中,正确的是:2、请问所有的异常类皆直接继承于哪一个类?()3、Which statement is tr
- springboot配置templates直接访问springboot下的templates目录的资源默认是受保护的,类似于javaweb项
- 一、概述一个Process组件提供了在计算机运行进程的访问权限。 进程,在最简单的术语中,是正在运行的应用。提供对本地和远程进程的访问权限并
- 概念代理:为控制A对象,而创建出新B对象,由B对象代替执行A对象所有操作,称之为代理。一个代理体系建立涉及到3个参与角色:真实对象(A),代
- 一、堆排序1、什么是堆排序(1)堆排序:堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构
- 并发编程中的三个概念:1.原子性在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执
- 前言: 最近一直在看Launcher模块,经过差不多两个月学习,终于摸透了Launcher的一些主要功能实现,目前继续还处于摸索状态。未看L
- 最大数给定一组非负整数 nums,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。注意:输出结果可能非常大,所以你需要返回一个
- 1.创建简单的XML文件为了便于测试,我们首先创建控制台应用程序,项目命名为CreateXml,Program.cs代码如下:using S
- 前言在Windows下JAVA用到的环境变量主要有3个,JAVA_HOME、CLASSPATH、PATH,下面来详细的介绍。JAVA_HOM
- 生命周期速览优先级servlet 的声明周期由 tomcat 服务器自行管辖,程序员无法插手;只要没有通过 url 访问 servlet,那
- 1.MyBatis中map的应用1.1.应用场景假设,实体类,或者数据库中的表,字段或者参数过多,应当考虑使用Map!!!1.2.具体实现/
- 问题:Kotlin升级引起的类找不到情况[其实跟Kotlin版本无关]java.lang.NoClassDefFoundError: Fai
- 一、maven引入依赖,数据库驱动根据项目需求自行引入<!-- https://mvnrepository.com/artifact/