應用監聽自身卸載,彈出用戶反饋調查(上)
來自: 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,原理分析
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端調用
- public native void uninstall(String packageDir, int sdkVersion);
該方法需要傳遞應用的安裝目錄和當前設備的版本號,在Java代碼中獲取,傳遞給C代碼處理。
2,使用javah命令生成方法簽名頭文件
- /* DO NOT EDIT THIS FILE - it is machine generated */
- #include <jni.h>
- /* Header for class com_example_appuninstall_MainActivity */
- #ifndef _Included_com_example_appuninstall_MainActivity
- #define _Included_com_example_appuninstall_MainActivity
- #ifdef __cplusplus
- extern "C" {
- #endif
- /*
- * Class: com_example_appuninstall_MainActivity
- * Method: uninstall
- * Signature: (Ljava/lang/String;)V
- */
- JNIEXPORT void JNICALL Java_com_example_appuninstall_MainActivity_uninstall
- (JNIEnv *, jobject, jstring);
- #ifdef __cplusplus
- }
- #endif
- #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,目錄被刪除,說明被卸載,執行打開用戶反饋的頁面
- #include <stdio.h>
- #include <jni.h>
- #include <malloc.h>
- #include <string.h>
- #include <strings.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include "com_example_appuninstall_MainActivity.h"
- #include <android/log.h>
- #define LOG_TAG "System.out.c"
- #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
- #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
- /**
- * 返回值 char* 這個代表char數組的首地址
- * Jstring2CStr 把java中的jstring的類型轉化成一個c語言中的char 字符串
- */
- char* Jstring2CStr(JNIEnv* env, jstring jstr) {
- char* rtn = NULL;
- jclass clsstring = (*env)->FindClass(env, "java/lang/String"); //String
- jstring strencode = (*env)->NewStringUTF(env, "GB2312"); // 得到一個java字符串 "GB2312"
- jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes",
- "(Ljava/lang/String;)[B"); //[ String.getBytes("gb2312");
- jbyteArray barr = (jbyteArray) (*env)->CallObjectMethod(env, jstr, mid,
- strencode); // String .getByte("GB2312");
- jsize alen = (*env)->GetArrayLength(env, barr); // byte數組的長度
- jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
- if (alen > 0) {
- rtn = (char*) malloc(alen + 1); //"\0"
- memcpy(rtn, ba, alen);
- rtn[alen] = 0;
- }
- (*env)->ReleaseByteArrayElements(env, barr, ba, 0); //
- return rtn;
- }
- JNIEXPORT void JNICALL Java_com_example_appuninstall_MainActivity_uninstall(
- JNIEnv * env, jobject obj, jstring packageDir, jint sdkVersion) {
- // 1,將傳遞過來的java的包名轉為c的字符串
- char * pd = Jstring2CStr(env, packageDir);
- // 2,創建當前進程的克隆進程
- pid_t pid = fork();
- // 3,根據返回值的不同做不同的操作,<0,>0,=0
- if (pid < 0) {
- // 說明克隆進程失敗
- LOGD("current crate process failure");
- } else if (pid > 0) {
- // 說明克隆進程成功,而且該代碼運行在父進程中
- LOGD("crate process success,current parent pid = %d", pid);
- } else {
- // 說明克隆進程成功,而且代碼運行在子進程中
- LOGD("crate process success,current child pid = %d", pid);
- // 4,在子進程中監視/data/data/包名這個目錄
- while (JNI_TRUE) {
- FILE* file = fopen(pd, "rt");
- if (file == NULL) {
- // 應用被卸載了,通知系統打開用戶反饋的網頁
- LOGD("app uninstall,current sdkversion = %d", sdkVersion);
- if (sdkVersion >= 17) {
- // Android4.2系統之后支持多用戶操作,所以得指定用戶
- execlp("am", "am", "start", "--user", "0", "-a",
- "android.intent.action.VIEW", "-d",
- "http://www.baidu.com", (char*) NULL);
- } else {
- // Android4.2以前的版本無需指定用戶
- execlp("am", "am", "start", "-a",
- "android.intent.action.VIEW", "-d",
- "http://www.baidu.com", (char*) NULL);
- }
- } else {
- // 應用沒有被卸載
- LOGD("app run normal");
- }
- sleep(1);
- }
- }
- }
注意:為了簡便起見,我在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/包名,然后獲取當前設備的版本號,一起傳給本地方法中,最后調用這個方法。
- public class MainActivity extends Activity {
- static {
- System.loadLibrary("uninstall");
- }
- public native void uninstall(String packageDir, int sdkVersion);
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- String packageDir = "/data/data/" + getPackageName();
- int sdkVersion = android.os.Build.VERSION.SDK_INT;
- uninstall(packageDir, sdkVersion);
- }
- }
6,測試
好了,應用是做完了,我們clean一下工程,然后啟動一個基于ARM的模擬器,運行這個程序,回到桌面,點擊應用圖片——卸載掉這個應用,看看效果:
好了,大家看看效果吧,實際上打開的網頁應該是用戶反饋調查頁面,由于我暫時沒有服務器,所以將網址定向到了百度首頁了,大家在開發的時候,可以將execlp函數里的參數網址改成自己的服務器網址,這樣就大功告成了。檢查一下Log日志的輸出:
看到了,LOG輸入日志跟代碼流程是一致的,好了,源碼在下面的鏈接下,有興趣的朋友可以下載研究,歡迎你給我提出寶貴意見,大家一起學習一起進步!
經過查詢資料,我已經了解不使用while(true)輪詢方式,改用Linux的Inotify機制監聽應用安裝目錄的實現方法了,關于最新優化版本的案例已經做完,請點擊這里查看實現原理和代碼:Android NDK開發(九)——應用監聽自身卸載升級版,使用Inotify監聽安裝目錄