开发一个需要常住后台的App其实是一件非常头疼的事情,不仅要应对国内各大厂商的ROM,还需要应对各类的安全管家… 虽然不断的研究各式各样的方法,但是效果并不好,比如任务管理器把App干掉,服务就起不来了…网上搜寻一番后,主要的方法有以下几种方法,但其实也都治标不治本:
1 2 3 4 5 6 7 8 9 10 11 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)是如何关闭在应用退出后清理内存的:
1 Process.killProcessQuiet(pid);
应用退出后,ActivityManagerService就把主进程给杀死了,但是,在Android5.0中,ActivityManagerService却是这样处理的:
1 2 Process.killProcessQuiet(app.pid); Process.killProcessGroup(app.info.uid, app.pid);
虽只差了一句代码,差别却很大。Android5.0在应用退出后,ActivityManagerService不仅把主进程给杀死,另外把主进程所属的进程组一并杀死,这样一来,由于子进程和主进程在同一进程组,子进程在做的事情,也就停止了 …要不怎么说Android5.0在安全方面做了很多更新呢…
那么,有没有办法让子进程脱离出来,不要受到主进程的影响,当然也是可以的。那么,在C/C++层是如何实现的呢?先上关键代码:
1 2 3 4 5 6 7 8 9 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 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()); } 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("/" ); 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 ; } 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(),代码如下:
1 2 3 4 5 6 7 8 9 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 ); } } 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库。
C/C++端关键的部分主要是以上这些,接下来就是Java端调用。 首先来看一下so库的加载类,以及C++函数的调用:
1 2 3 4 5 6 7 8 9 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 96 97 98 99 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; } 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); sb_result.append(";" ); } RunLocalUserCommand(pacaageName, cmd3, sb_result); sb_result.append(";" ); RunLocalUserCommand(pacaageName, cmd2_a1, sb_result); sb_result.append(";" ); return sb_result.toString(); } public boolean RunLocalUserCommand (String pacaageName, String command, StringBuffer sb_out_Result) { Process process = null ; try { process = Runtime.getRuntime().exec("sh" ); DataInputStream inputStream = new DataInputStream(process.getInputStream()); DataOutputStream outputStream = new DataOutputStream(process.getOutputStream()); outputStream.writeBytes("cd /data/data/" + pacaageName + "\n" ); 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" ); } catch (Exception e) { e.printStackTrace(); } } }
然后,我们在收到开机广播后,启动该服务。
1 2 3 4 5 6 7 8 9 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服务里面,就可以做该做的事情。1 2 3 4 5 6 7 8 9 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <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实现双守护进程可以用这个思路去实现。
来源:http://blog.csdn.net/yyh352091626/article/details/50542554
其他还有一些技术之外的措施,比如说应用内 Push 通道 的选择:
国外版应用:
接入 Google 的 GCM。
国内版应用:
根据终端不同,在小米手机(包括 MIUI)接入小米推送、华为手机接入华为推送;其他手机可以考虑接入腾讯信鸽或极光推送与小米推送做 A/B Test。
github开源进程保活项目