應用監聽自身卸載,彈出用戶反饋調查(上)

slzby 8年前發布 | 27K 次閱讀 Android開發 移動開發

來自: http://blog.csdn.net//guijiaoba/article/details/50161335


出處:http://blog.csdn.net/allen315410/article/details/42521251


監聽卸載情景和原理分析

1,情景分析

        在上上篇博客中我寫了一下NDK開發實踐項目,使用開源的LAME庫轉碼MP3,作為前面幾篇基礎博客的加深理解使用的,但是這樣的項目用處不大,除了練練NDK功底。這篇博客,我將講述一下一個各大應用中很常見的一個功能,同樣也是基于JNI開發的Android應用小Demo,看完這個之后,不僅可以加深對NDK開發的理解,而且該Demo也可以使用在實際的開發中。不知道大家在使用一個Android應用的時候,當我們卸載這個應用后,設備上會彈出一個“用戶反饋調查”的網頁出來,也許很多人沒有留意過或者直接忽視了,那么從現在開始請留意,大家不妨下載一下“豌豆莢”“360”之類的應用裝上,然后卸載,看看設備上有沒有彈出瀏覽器,瀏覽器上打開的“XXX用戶反饋”?上面寫了一些HTML表單,問我們“你為毛要卸載我們這么好的應用啊?”“我們哪里得罪你了?”“卸載之后,你丫的還裝不?”,呵呵,開個玩笑,實際效果如下圖:


       好了,上面的圖片是感覺似曾顯示啊?那么這樣的一個小功能是怎么實現的呢?我們先從Java層以我們有的Android基礎分析一下:

1,監聽系統的卸載廣播,但是這個只能監聽其他應用的卸載廣播的動作,通過卸載廣播監聽自己是監聽不到的:失敗
2,系統配置文件,做一個標記應用是否卸載,判斷標記來show用戶反饋,顯然這也是不合理的,因為應用卸載之后,配置文件也沒有了。
3,靜默安裝另一個程序,監聽自己的應用被卸載的動作。前提是要root,才能實現。但是市場絕大多數手機都是默認沒有root權限的。
4,服務檢測,只能是自己開啟,當自身被卸載了,服務也一并被干掉了。

以上幾點看起來都無法實現這個功能,確實如此啊,單純的從Java層是做不到這一點的。


2,原理分析

       上面情景分析后表明Java實現不了這樣的一個功能,是否該考慮一下使用JNI了,用C在底層為我們實現這樣一個打開內置瀏覽器加載用戶反饋網頁即可,在知道這個方法之前,我們有必要了解以下幾個知識點。

1.通過c語言,c進程監視。

    既然Java做不到的話,我們試著使用C語言在底層實現好了,讓C語言調用Android adb的命令去打開內置瀏覽器。

判斷自己是否被卸載
andoird程序在被安裝的時候會在/data/data/目錄下生成一個以為包名為文件名的目錄/data/data/包名
監聽該目錄是否還存在,如果不存在,就證明應用被卸載了。


2.c代碼可以復制一個當前的進程作為自己的兒子,父進程銷毀的時候,子進程還存在。

fork()函數:

        fork()函數通過系統調用創建一個與原來進程幾乎完全相同的進程,兩個進程可以做相同的事,相當于自己生了個兒子,如果初始參數或者傳入的參數不一樣,兩個進程做的事情也不一樣。當前進程調用fork函數之后,系統先給當前進程分配資源,然后再將當前進程的所有變量的值復制到新進程中(只有少數值不一樣),相當于克隆了一個自己。

       pid_t fpid = fork()被調用前,就一個進程執行該段代碼,這條語句執行之后,就將有兩個進程執行代碼,兩個進程執行沒有固定先后順序,主要看系統調度策略,fork函數的特別之處在于調用一次,但是卻可以返回兩次,甚至是三種的結果
(1)在父進程中返回子進程的進程id(pid)
(2)在子進程中返回0
(3)出現錯誤,返回小于0的負值
出現錯誤原因:(1)進程數已經達到系統規定 (2)內存不足,此時返回


3.在c代碼的子進程中監視父進程是否被卸載,如果被卸載,通知Android系統打開一個url,卸載調查的網頁。

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


execlp()函數

          execlp函數簡單的來說就是C語言中執行系統命令的函數
          execlp()會從PATH 環境變量所指的目錄中查找符合參數file 的文件名, 找到后便執行該文件, 然后將第二個以后的參數當做該文件的argv[0], argv[1], ..., 最后一個參數必須用空指針(NULL)作結束.
          android開發中,execlp函數對應android的path路徑為system/bin/目錄下

調用格式:

execlp("am","am","start","--user","0","-a","android.intent.action.VIEW","-d","http://shouji.#/web/uninstall/uninstall.html",(char*)NULL);

===================================================================================================================


編寫代碼實現

1,Java層定義native方法

       在Java層定義一個native方法,提供在Java端和C端調用

[java]  view plain copy print ? 在CODE上查看代碼片 派生到我的代碼片
  1. public native void uninstall(String packageDir, int sdkVersion);  

該方法需要傳遞應用的安裝目錄和當前設備的版本號,在Java代碼中獲取,傳遞給C代碼處理。


2,使用javah命令生成方法簽名頭文件

  1. /* DO NOT EDIT THIS FILE - it is machine generated */  
  2. #include <jni.h>  
  3. /* Header for class com_example_appuninstall_MainActivity */  
  4.   
  5. #ifndef _Included_com_example_appuninstall_MainActivity  
  6. #define _Included_com_example_appuninstall_MainActivity  
  7. #ifdef __cplusplus  
  8. extern "C" {  
  9. #endif  
  10. /* 
  11.  * Class:     com_example_appuninstall_MainActivity 
  12.  * Method:    uninstall 
  13.  * Signature: (Ljava/lang/String;)V 
  14.  */  
  15. JNIEXPORT void JNICALL Java_com_example_appuninstall_MainActivity_uninstall  
  16.   (JNIEnv *, jobject, jstring);  
  17.   
  18. #ifdef __cplusplus  
  19. }  
  20. #endif  
  21. #endif  

       方法簽名生成好之后,工程上右鍵 --> Android Tools --> Add Native Support,在彈出的對話框中輸入編輯的C/C++的文件名,確定之后,在工程的自動生成的jni目錄下找到cpp后綴名的文件修改為.c后綴名的文件,因為本案例是基于C語言上實現的,然后同樣修改Android.mk文件中的LOCAL_SRC_FILES為.c的C文件,最后將上面生成好的.h方法簽名文件拷貝到jni目錄下。


3,編寫C語言代碼

        正如上面原理分析的那樣,我們在實現這樣一個功能的時候用Java是無法實現的,只能在C中克隆出一個當前App的子進程,讓這個子進程去監聽應用本身的卸載。那么實現這樣的功能我們需要哪些步驟呢?下面就是編寫代碼的思路:

1,將傳遞過來的java的包名轉為c的字符串
2,創建當前進程的克隆進程
3,根據返回值的不同做不同的操作
4,在子進程中監視/data/data/包名這個目錄
5,目錄被刪除,說明被卸載,執行打開用戶反饋的頁面

  1. #include <stdio.h>  
  2. #include <jni.h>  
  3. #include <malloc.h>  
  4. #include <string.h>  
  5. #include <strings.h>  
  6. #include <stdlib.h>  
  7. #include <unistd.h>  
  8. #include "com_example_appuninstall_MainActivity.h"  
  9. #include <android/log.h>  
  10. #define LOG_TAG "System.out.c"  
  11. #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)  
  12. #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)  
  13.   
  14. /** 
  15.  * 返回值 char* 這個代表char數組的首地址 
  16.  * Jstring2CStr 把java中的jstring的類型轉化成一個c語言中的char 字符串 
  17.  */  
  18. char* Jstring2CStr(JNIEnv* env, jstring jstr) {  
  19.     char* rtn = NULL;  
  20.     jclass clsstring = (*env)->FindClass(env, "java/lang/String"); //String  
  21.     jstring strencode = (*env)->NewStringUTF(env, "GB2312"); // 得到一個java字符串 "GB2312"  
  22.     jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes",  
  23.             "(Ljava/lang/String;)[B"); //[ String.getBytes("gb2312");  
  24.     jbyteArray barr = (jbyteArray) (*env)->CallObjectMethod(env, jstr, mid,  
  25.             strencode); // String .getByte("GB2312");  
  26.     jsize alen = (*env)->GetArrayLength(env, barr); // byte數組的長度  
  27.     jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);  
  28.     if (alen > 0) {  
  29.         rtn = (char*) malloc(alen + 1); //"\0"  
  30.         memcpy(rtn, ba, alen);  
  31.         rtn[alen] = 0;  
  32.     }  
  33.     (*env)->ReleaseByteArrayElements(env, barr, ba, 0); //  
  34.     return rtn;  
  35. }  
  36.   
  37. JNIEXPORT void JNICALL Java_com_example_appuninstall_MainActivity_uninstall(  
  38.         JNIEnv * env, jobject obj, jstring packageDir, jint sdkVersion) {  
  39.     // 1,將傳遞過來的java的包名轉為c的字符串  
  40.     char * pd = Jstring2CStr(env, packageDir);  
  41.   
  42.     // 2,創建當前進程的克隆進程  
  43.     pid_t pid = fork();  
  44.   
  45.     // 3,根據返回值的不同做不同的操作,<0,>0,=0  
  46.     if (pid < 0) {  
  47.         // 說明克隆進程失敗  
  48.         LOGD("current crate process failure");  
  49.     } else if (pid > 0) {  
  50.         // 說明克隆進程成功,而且該代碼運行在父進程中  
  51.         LOGD("crate process success,current parent pid = %d", pid);  
  52.     } else {  
  53.         // 說明克隆進程成功,而且代碼運行在子進程中  
  54.         LOGD("crate process success,current child pid = %d", pid);  
  55.   
  56.         // 4,在子進程中監視/data/data/包名這個目錄  
  57.         while (JNI_TRUE) {  
  58.             FILE* file = fopen(pd, "rt");  
  59.   
  60.             if (file == NULL) {  
  61.                 // 應用被卸載了,通知系統打開用戶反饋的網頁  
  62.                 LOGD("app uninstall,current sdkversion = %d", sdkVersion);  
  63.                 if (sdkVersion >= 17) {  
  64.                     // Android4.2系統之后支持多用戶操作,所以得指定用戶  
  65.                     execlp("am""am""start""--user""0""-a",  
  66.                             "android.intent.action.VIEW""-d",  
  67.                             "http://www.baidu.com", (char*) NULL);  
  68.                 } else {  
  69.                     // Android4.2以前的版本無需指定用戶  
  70.                     execlp("am""am""start""-a",  
  71.                             "android.intent.action.VIEW""-d",  
  72.                             "http://www.baidu.com", (char*) NULL);  
  73.                 }  
  74.             } else {  
  75.                 // 應用沒有被卸載  
  76.                 LOGD("app run normal");  
  77.             }  
  78.             sleep(1);  
  79.         }  
  80.     }  
  81.   
  82. }  
        上述代碼就如上述的步驟一樣,用C代碼實現了,首先注意的一點就是Android的版本問題,眾所周知,Android是基于Linux的非常優秀的操作系統,而且在Android4.2版本以后支持多用戶操作,但是這也給我們這個小小的項目中帶來了不便之處,因為在多用戶情況下執行am命令的時候強制指定一個用戶和一個編號,在Android4.2之前的版本這些參數是沒有必要的,所以我們在編寫C代碼的時候需要區別Android系統版本,分別執行相應的am命令,關于獲取Android系統版本可以在Java層實現,然后將其作為參數傳遞給C代碼中,C代碼根據Android版本為判斷條件執行am命令。

        注意:為了簡便起見,我在C代碼監視應用是否被卸載的時候,使用了一個While(true)的死循環,并且是每隔1毫秒執行一次監視檢測,這樣寫的代碼是“不環保的”,想想這樣的結果是程序被不停的執行,LOG被不停的打印,造成cpu計算資源浪費和耗電是難免的。最好的解決方案是,使用Android給我們提供的FileObserve文件觀察者,FileObserve使用到的是Linux系統下的inotify進程,用來監視文件目錄的變化的,本實例中如果需要優化就需要使用這個API,但是需要的知識就更加多了,我現在為了簡單的演示起見,暫時用了while(true)死循環,關于后期的優化版本,等我寫出來,再一起公布一下!


4,編譯.so動態庫

       正如上篇博客寫的那樣,我們編寫好了C源碼之后,就需要使用ndk-build命令來編譯成.so文件了,具體編譯的過程也是非常簡單的,在Eclipse中切換到C/C++編輯的手下,找到“小錘子”按鈕,點擊一下就開始編譯了,如果代碼沒有出現錯誤的情況,編譯之后的結果是這樣的:



5,編寫Java代碼,傳遞數據 ,加載鏈接庫

        上面的工作做好了,剩下的就是在Java中加載這個鏈接庫,和調用這個本地方法了。首先,要獲取本應用安裝的目錄/data/data/包名,然后獲取當前設備的版本號,一起傳給本地方法中,最后調用這個方法。

[java]  view plain copy print ? 在CODE上查看代碼片 派生到我的代碼片
  1. public class MainActivity extends Activity {  
  2.   
  3.     static {  
  4.         System.loadLibrary("uninstall");  
  5.     }  
  6.   
  7.     public native void uninstall(String packageDir, int sdkVersion);  
  8.   
  9.     @Override  
  10.     protected void onCreate(Bundle savedInstanceState) {  
  11.         super.onCreate(savedInstanceState);  
  12.         setContentView(R.layout.activity_main);  
  13.   
  14.         String packageDir = "/data/data/" + getPackageName();  
  15.         int sdkVersion = android.os.Build.VERSION.SDK_INT;  
  16.         uninstall(packageDir, sdkVersion);  
  17.     }  
  18.   
  19. }  

6,測試

       好了,應用是做完了,我們clean一下工程,然后啟動一個基于ARM的模擬器,運行這個程序,回到桌面,點擊應用圖片——卸載掉這個應用,看看效果:


好了,大家看看效果吧,實際上打開的網頁應該是用戶反饋調查頁面,由于我暫時沒有服務器,所以將網址定向到了百度首頁了,大家在開發的時候,可以將execlp函數里的參數網址改成自己的服務器網址,這樣就大功告成了。檢查一下Log日志的輸出:


看到了,LOG輸入日志跟代碼流程是一致的,好了,源碼在下面的鏈接下,有興趣的朋友可以下載研究,歡迎你給我提出寶貴意見,大家一起學習一起進步!

經過查詢資料,我已經了解不使用while(true)輪詢方式,改用Linux的Inotify機制監聽應用安裝目錄的實現方法了,關于最新優化版本的案例已經做完,請點擊這里查看實現原理和代碼:Android NDK開發(九)——應用監聽自身卸載升級版,使用Inotify監聽安裝目錄


源碼請在這里下載

 本文由用戶 slzby 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!