1. 程式人生 > >Android 通過JNI實現守護程序,使Service服務不被殺死

Android 通過JNI實現守護程序,使Service服務不被殺死

開發一個需要常住後臺的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的方式,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庫。


C/C++端關鍵的部分主要是以上這些,接下來就是Java端呼叫。

首先來看一下so庫的載入類,以及C++函式的呼叫:

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不想讓他死...

本文主要提供的是一個思路,實現還有諸多不足之處,菜鳥之作,不喜勿噴。