Android通過JNI實現守護進程
開發(fā)一個需要常住后臺的App其實是一件非常頭疼的事情,不僅要應對國內各大廠商的ROM,還需要應對各類的安全管家...雖然不斷的研究各式各樣的方法,但是效果并不好,比如任務管理器把App干掉,服務就起不來了...
網上搜尋一番后,主要的方法有以下幾種方法,但都是治標不治本:
1、提高Service的優(yōu)先級:這個,也只能說在系統(tǒng)內存不足需要回收資源的時候,優(yōu)先級較高,不容易被回收,然并卵...
2、提高Service所在進程的優(yōu)先級:效果不是很明顯
3、在onDestroy方法里重啟service:這個倒還算挺有效的一個方法,但是,直接干掉進程的時候,onDestroy方法都進不來,更別想重啟了
4、broadcast廣播:和第3種一樣,沒進入onDestroy,就不知道什么時候發(fā)廣播了,另外,在Android4.4以上,程序完全退出后,就不好接收廣播了,需要在發(fā)廣播的地方特定處理
5、放到System/app底下作為系統(tǒng)應用:這個也就是平時玩玩,沒多大的實際意義。
6、Service的onStartCommand方法,返回START_STICKY,這個也主要是針對系統(tǒng)資源不足而導致的服務被關閉,還是有一定的道理的。
應對的方法是有,實現起來都比較繁瑣。如果你自己可以定制ROM,那就有很多種辦法了,比如把你的應用加入白名單,或是多安裝一個沒有圖標的app作為守護進程...但是,哪能什么都是定制的,對于安卓開發(fā)者來說,這個難題必須攻破~
那么,有沒有辦法在一個APP里面,開啟一個子線程,在主線程被干掉了之后,子線程通過監(jiān)聽、輪詢等方式去判斷服務是否存在,不存在的話則開啟服務。答案自然是肯定的,通過JNI的方式(NDK編程),fork()出一個子線程作為守護進程,輪詢監(jiān)聽服務狀態(tài)。守護進程(Daemon)是運行在后臺的一種特殊進程。它獨立于控制終端并且周期性地執(zhí)行某種任務或等待處理某些發(fā)生的事件。而守護進程的會話組和當前目錄,文件描述符都是獨立的。后臺運行只是終端進行了一次fork,讓程序在后臺執(zhí)行,這些都沒有改變。
那么我們先來看看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 之前創(chuàng)建子進程的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); // 開啟線程,輪詢去監(jiān)聽啟動服務
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),并且脫離了原來控制終端。故不管控制終端怎么操作,新的進程正常情況下不會收到他發(fā)出來的這些信號。
3、umask(0)的作用:由于子進程從父進程繼承下來的一些東西,可能并未把權限繼承下來,所以要賦予他更高的權限,便于子進程操作。
4、chdir ("/");作用:進程活動時,其工作目錄所在的文件系統(tǒng)不能卸下,一般需要將工作目錄改變到根目錄。
5、進程從創(chuàng)建它的父進程那里繼承了打開的文件描述符。如不關閉,將會浪費系統(tǒng)資源,造成進程所在的文件系統(tǒng)無法卸下以及引起無法預料的錯誤。所以在最后,記得關閉掉從父進程繼承過來的文件描述符。
然后,在上面的代碼中開啟線程后做的事,就是循環(huán)去startService(),代碼如下:
void thread(char* srvname) {
while(1){
check_and_restart_service(srvname); // 應該要去判斷service狀態(tài),這里一直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);
}
/**
* 執(zhí)行命令
*/
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端還得配合執(zhí)行。
首先來看一下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的屬性,讓其變?yōu)榭蓤?zhí)行
sb_result.append(";");
RunLocalUserCommand(pacaageName, cmd2_a1, sb_result); // 執(zhí)行test程序.
sb_result.append(";");
return sb_result.toString();
}
/**
* 執(zhí)行本地用戶命令
* @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在自己的數據目錄里執(zhí)行,才有權限寫文件到當前目錄
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起來,在程序應用里面,結束掉這個進程,不一會了,又自動起來了~~~~完美~~~~跟流氓軟件一個樣,沒錯,就是這么賤,就是這么霸道!!

這邊是運行在谷歌的原生系統(tǒng)上,Android版本為5.0...總結一下就是:服務常駐要應對的不是各種難的技術,而是各大ROM。QQ為什么不會被殺死,是因為國內各大ROM不想讓他死...
本文主要提供的是一個思路,實現還有諸多不足之處,菜鳥之作,不喜勿噴。
最后附上本例的源代碼:Android 通過JNI實現雙守護進程
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
Android 使用FragmentTabhost代替Tabhost
這篇文章主要介紹了Android 使用FragmentTabhost代替Tabhost的相關資料,需要的朋友可以參考下2017-05-05
Android開發(fā)實現按鈕點擊切換背景并修改文字顏色的方法
這篇文章主要介紹了Android開發(fā)實現按鈕點擊切換背景并修改文字顏色的方法,涉及Android界面布局與相關屬性設置技巧,需要的朋友可以參考下2018-01-01
實例講解Android中的AutoCompleteTextView自動補全組件
AutoCompleteTextView組件被用在輸入框中能實現輸入內容自動補全的功能,類似于大家平時用Google時的輸入聯想,這里我們來用實例講解Android中的AutoCompleteTextView自動補全組件,特別是實現郵箱地址補全的例子,非常實用2016-05-05

