轉自:http://klob.diandi.life/?p=21#symple-tab-%e8%b0%83%e6%9f%a5%e5%af%b9%e8%b1%a1
情景還原:
我的應用調用了Notification,但是如果被流氓清理軟件殺死,在有些機型出現Notification沒有被抹除的情況,因為喪失了對Notification的引用,用戶也無法抹除這個Notification,這將大大降低用戶體驗。於是,我想出了如果我的應用可以不死,主動清除Notification。
既然開始做了,干脆做了個小調查。
調查內容:
經獲取Root權限TaskManager清除之后能重生的應用使用的方式(測試機型:魅藍Note )
調查對象:
UC瀏覽器,網易郵箱,課程格子,戀愛筆記,今日頭條,練練
調查結果:
UC瀏覽器(未知),網易郵箱(自寫),課程格子,戀愛筆記,今日頭條,練練四個應用全部重生,且都重啟了一個名為NotificationCenter的進程
結果分析:
課程格子,戀愛筆記,今日頭條,練練四個應用全部重生,且都采用了個推的推送服務
那么個推可能是那樣的,然后我從網上找到了一個有關Daemon進程,即守護進程。原作者分析地址http://coolerfall.com/android/android-app-daemon/,原作者github地址https://github.com/Coolerfall/Android-AppDaemon
使用方法
1 public class YourDaemonService extend Service 2 { 3 public void onCreate() 4 { 5 Daemon.run(this,YourDaemonService.class,60); 6 } 7 }
原理分析:
一、首先調用這個函數 開啟守護進程
Daemon.run(this, DaemonService.class, Daemon.INTERVAL_ONE_MINUTE * 2);
1 public class Daemon { 2 /** 3 * Run daemon process. 4 * 5 * @param context context 6 * @param daemonServiceClazz the name of daemon service class 7 * @param interval the interval to check 8 */ 9 public static void run(final Context context, final Class<?> daemonServiceClazz, 10 final int interval) { 11 new Thread(new Runnable() { 12 @Override 13 public void run() { 14 Command.install(context, BIN_DIR_NAME, DAEMON_BIN_NAME); 15 start(context, daemonServiceClazz, interval); 16 } 17 }).start(); 18 } 19 }
二、install 安裝庫
1 public class Daemon { 2 /** 3 * Install specified binary into destination directory. 4 * 5 * @param context context 6 * @param destDir destionation directory 7 * @param filename filename of binary 8 * @return true if install successfully, otherwise return false 9 */ 10 @SuppressWarnings("deprecation") 11 public static boolean install(Context context, String destDir, String filename) 12 }
這個函數類似
1 public final class System { 2 /** 3 * See {@link Runtime#load}. 4 */ 5 6 public static void load(String pathName) { 7 Runtime.getRuntime().load(pathName, VMStack.getCallingClassLoader()); 8 } 9 }
三、調用核心函數
1 public class Daemon { 2 /** start daemon */ 3 private static void start(Context context, Class<?> daemonClazzName, int interval) { 4 String cmd = context.getDir(BIN_DIR_NAME, Context.MODE_PRIVATE) 5 .getAbsolutePath() + File.separator + DAEMON_BIN_NAME; 6 7 /* create the command string */ 8 StringBuilder cmdBuilder = new StringBuilder(); 9 cmdBuilder.append(cmd); 10 cmdBuilder.append(" -p "); 11 cmdBuilder.append(context.getPackageName()); 12 cmdBuilder.append(" -s "); 13 cmdBuilder.append(daemonClazzName.getName()); 14 cmdBuilder.append(" -t "); 15 cmdBuilder.append(interval); 16 17 try { 18 Runtime.getRuntime().exec(cmdBuilder.toString()).waitFor(); 19 } catch (IOException | InterruptedException e) { 20 Log.e(TAG, "start daemon error: " + e.getMessage()); 21 } 22 } 23 }
有必要解釋一下Runtime.exec(String prog)函數,指令加上幾個參數,
1 public class Runtime { 2 /** 3 * Executes the specified program in a separate native process. The new 4 * process inherits the environment of the caller. Calling this method is 5 * equivalent to calling {@code exec(prog, null, null)}. 6 * 7 * @param prog 8 * the name of the program to execute. 9 * @return the new {@code Process} object that represents the native 10 * process. 11 * @throws IOException 12 * if the requested program can not be executed. 13 */ 14 public Process exec(String prog) throws java.io.IOException { 15 return exec(prog, null, null); 16 } 17 }
四、調用了Daemon的main函數
1 Daemon.c 2 int main(int argc, char *argv[]) 3 { 4 int i; 5 pid_t pid; 6 //包名 7 char *package_name = NULL; 8 //Service名 9 char *service_name = NULL; 10 //daemon文件目錄 11 char *daemon_file_dir = NULL; 12 ////daemon休眠時間 13 int interval = SLEEP_INTERVAL; 14 15 if (argc < 7) 16 { 17 LOGE(LOG_TAG, "usage: %s -p package-name -s " 18 "daemon-service-name -t interval-time", argv[0]); 19 return; 20 } 21 //得到參數 22 for (i = 0; i < argc; i ++) 23 { 24 if (!strcmp("-p", argv[i])) 25 { 26 package_name = argv[i + 1]; 27 LOGD(LOG_TAG, "package name: %s", package_name); 28 } 29 30 if (!strcmp("-s", argv[i])) 31 { 32 service_name = argv[i + 1]; 33 LOGD(LOG_TAG, "service name: %s", service_name); 34 } 35 36 if (!strcmp("-t", argv[i])) 37 { 38 interval = atoi(argv[i + 1]); 39 LOGD(LOG_TAG, "interval: %d", interval); 40 } 41 } 42 43 /* package name and service name should not be null */ 44 if (package_name == NULL || service_name == NULL) 45 { 46 LOGE(LOG_TAG, "package name or service name is null"); 47 return; 48 } 49 //調用fork函數 50 if ((pid = fork()) < 0) 51 { 52 exit(EXIT_SUCCESS); 53 } 54 //子 55 else if (pid == 0) 56 { 57 /* add signal */ 58 /* SIGTERM 59 程序結束(terminate)信號 60 */ 61 signal(SIGTERM, sigterm_handler); 62 /* become session leader */ 63 setsid(); 64 /* change work directory */ 65 chdir("/"); 66 67 for (i = 0; i < MAXFILE; i ++) 68 { 69 close(i); 70 } 71 72 /* find pid by name and kill them */ 73 int pid_list[100]; 74 int total_num = find_pid_by_name(argv[0], pid_list); 75 LOGD(LOG_TAG, "total num %d", total_num); 76 for (i = 0; i < total_num; i ++) 77 { 78 int retval = 0; 79 int daemon_pid = pid_list[i]; 80 if (daemon_pid > 1 && daemon_pid != getpid()) 81 { 82 retval = kill(daemon_pid, SIGTERM); 83 if (!retval) 84 { 85 LOGD(LOG_TAG, "kill daemon process success: %d", daemon_pid); 86 } 87 else 88 { 89 LOGD(LOG_TAG, "kill daemon process %d fail: %s", daemon_pid, strerror(errno)); 90 exit(EXIT_SUCCESS); 91 } 92 } 93 } 94 95 LOGD(LOG_TAG, "child process fork ok, daemon start: %d", getpid()); 96 97 while(sig_running) 98 { 99 select_sleep(interval < SLEEP_INTERVAL ? SLEEP_INTERVAL : interval, 0); 100 101 LOGD(LOG_TAG, "check the service once"); 102 103 /* start service */ 104 start_service(package_name, service_name); 105 } 106 107 exit(EXIT_SUCCESS); 108 } 109 else 110 { 111 /* parent process */ 112 exit(EXIT_SUCCESS); 113 }
這里有必要其中的函數說明一下
1.signal()函數
void (*signal(int signum, void (*handler))(int)))(int);
該函數有兩個參數, signum指定要安裝的信號, handler指定信號的處理函數.
SIGTERM 是程序結束(terminate)信號
當程序終止會調用
2.fork()函數:
fork()函數通過系統調用創建一個與原來進程幾乎完全相同的進程,兩個進程可以做相同的事,相當於自己生了個兒子,如果初始參數或者傳入的參數不一樣,兩個進程做的事情也不一樣。當前進程調用fork函數之后,系統先給當前進程分配資源,然后再將當前進程的所有變量的值復制到新進程中(只有少數值不一樣),相當於克隆了一個自己。
pid_t fpid = fork()被調用前,就一個進程執行該段代碼,這條語句執行之后,就將有兩個進程執行代碼,兩個進程執行沒有固定先后順序,主要看系統調度策略,fork函數的特別之處在於調用一次,但是卻可以返回兩次,甚至是三種的結果
(1)在父進程中返回子進程的進程id(pid)
(2)在子進程中返回0
(3)出現錯誤,返回小於0的負值
出現錯誤原因:(1)進程數已經達到系統規定 (2)內存不足,此時返回
3.AM命令
Android系統提供的adb工具,在adb的基礎上執行adb shell就可以直接對android系統執行shell命令
am命令:在Android系統中通過adb shell 啟動某個Activity、Service、撥打電話、啟動瀏覽器等操作Android的命令。
am命令的源碼在Am.java中,在shell環境下執行am命令實際是啟動一個線程執行Am.java中的主函數(main方法),am命令后跟的參數都會當做運行時參數傳遞到主函數中,主要實現在Am.java的run方法中。
am命令可以用start子命令,和帶指定的參數,start是子命令,不是參數
常見參數:-a:表示動作,-d:表示攜帶的數據,-t:表示傳入的類型,-n:指定的組件名
例如,我們現在在命令行模式下進入adb shell下,使用這個命令去打開一個網頁
類似的命令還有這些:
撥打電話
命令:am start -a android.intent.action.CALL -d tel:電話號碼
示例:am start -a android.intent.action.CALL -d tel:10086
打開一個網頁
命令:am start -a android.intent.action.VIEW -d 網址
示例:am start -a android.intent.action.VIEW -d http://www.baidu.com
啟動一個服務
命令:am startservice <服務名稱>
示例:am startservice -n com.android.music/com.android.music.MediaPlaybackService
1 /* start daemon service */ 2 static void start_service(char *package_name, char *service_name) 3 { 4 /* get the sdk version */ 5 int version = get_version(); 6 7 pid_t pid; 8 9 if ((pid = fork()) < 0) 10 { 11 exit(EXIT_SUCCESS); 12 } 13 else if (pid == 0) 14 { 15 if (package_name == NULL || service_name == NULL) 16 { 17 LOGE(LOG_TAG, "package name or service name is null"); 18 return; 19 } 20 21 char *p_name = str_stitching(package_name, "/"); 22 char *s_name = str_stitching(p_name, service_name); 23 LOGD(LOG_TAG, "service: %s", s_name); 24 25 if (version >= 17 || version == 0) 26 { 27 int ret = execlp("am", "am", "startservice", 28 "--user", "0", "-n", s_name, (char *) NULL); 29 LOGD(LOG_TAG, "result %d", ret); 30 } 31 else 32 { 33 execlp("am", "am", "startservice", "-n", s_name, (char *) NULL); 34 } 35 36 LOGD(LOG_TAG , "exit start-service child process"); 37 exit(EXIT_SUCCESS); 38 } 39 }
五、讓程序徹底終止
如此一來你可能會發現你的程序根本就死不了
但是你又想要退出,那該怎么辦呢?
我這里有個解決辦法
1 @Override 2 public void onCreate() { 3 super.onCreate(); 4 Log.e(TAG, "onCreate"); 5 ACache cache = CustomApplication.getInstance().getCache();; 6 int m=2; 7 if (cache.getAsString(Constant.IS_CLEAN) != null) { 8 try { 9 m = Integer.parseInt(cache.getAsString(Constant.IS_CLEAN)); 10 // L.e(TAG," " + m ); 11 } catch (NumberFormatException e) { 12 cache.remove(Constant.IS_CLEAN); 13 //e.printStackTrace(); 14 m=1; 15 } 16 } 17 if (m>1) { 18 Daemon.run(this, NotificationCenter.class, 0); 19 cache.put(Constant.IS_CLEAN, m - 1 + ""); 20 } 21 if(m==1) 22 { 23 // L.e(TAG, "cancelAll"); 24 NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); 25 notificationManager.cancelAll(); 26 cache.put(Constant.IS_CLEAN, 0); 27 stopSelf(); 28 } 29 if (m == 0) { 30 stopSelf(); 31 } 32 33 }
IS_CLEAN 是個標志位,我將它緩存為文件
每啟動一次Service,會對進行一次V操作,即IS_CLEAN--
當IS_CLEAN <=1 時,那么不會再啟動守護進程
ps
並不是所有手機都能用此方法實現進程守護,主要是因為現目前的進程清理軟件不會清理c層fork出的進程,但有的手機(如小米),自帶清理進程會清理掉應用相關的所有進程,故而不能實現進程守護。
如果自發探索守護進程,可以下載android 終端模擬器
輸入 ps 命令查看進程
輸入 su 命令獲取root權限
輸入 kill pid 可以殺死進程
看到其中的進程詳情,可以對其中含有 Daemon 字段的進程對探索一二
最后一點,希望開發者利用守護進程完及時關閉,不要耍流氓,本人十分討厭一些以耍流氓為驕傲的行為