AndroidNDK開發(八)——應用監聽自身卸載,彈出(chū)用戶反 - 新聞資訊 - 雲南小程序開發|雲南軟件開發|雲南網站建設-昆明融晨信息技術有限公司

159-8711-8523

雲南網建設/小程序開發/軟件開發

知識

不(bù)管是(shì)網站,軟件還是(shì)小程序,都要(yào / yāo)直接或間接能爲(wéi / wèi)您産生價值,我們在(zài)追求其視覺表現的(de)同時(shí),更側重于(yú)功能的(de)便捷,營銷的(de)便利,運營的(de)高效,讓網站成爲(wéi / wèi)營銷工具,讓軟件能切實提升企業内部管理水平和(hé / huò)效率。優秀的(de)程序爲(wéi / wèi)後期升級提供便捷的(de)支持!

您當前位置>首頁 » 新聞資訊 » 技術分享 >

AndroidNDK開發(八)——應用監聽自身卸載,彈出(chū)用戶反

發表時(shí)間:2021-1-10

發布人(rén):融晨科技

浏覽次數:23


轉載請注明出(chū)處:http://blog.csdn.net/allen315410/article/details/42521251

監聽卸載情景和(hé / huò)原理分析

1,情景分析


        在(zài)上(shàng)上(shàng)篇博客中我寫了(le/liǎo)一下NDK開發實踐項目,使用開源的(de)LAME庫轉碼MP3,作爲(wéi / wèi)前面幾篇基礎博客的(de)加深理解使用的(de),但是(shì)這(zhè)樣的(de)項目用處不(bù)大(dà),除了(le/liǎo)練練NDK功底。這(zhè)篇博客,我将講述一下一個(gè)各大(dà)應用中很常見的(de)一個(gè)功能,同樣也(yě)是(shì)基于(yú)JNI開發的(de)Android應用小Demo,看完這(zhè)個(gè)之(zhī)後,不(bù)僅可以(yǐ)加深對NDK開發的(de)理解,而(ér)且該Demo也(yě)可以(yǐ)使用在(zài)實際的(de)開發中。不(bù)知道(dào)大(dà)家在(zài)使用一個(gè)Android應用的(de)時(shí)候,當我們卸載這(zhè)個(gè)應用後,設備上(shàng)會彈出(chū)一個(gè)“用戶反饋調查”的(de)網頁出(chū)來(lái),也(yě)許很多人(rén)沒有留意過或者直接忽視了(le/liǎo),那麽從現在(zài)開始請留意,大(dà)家不(bù)妨下載一下“豌豆莢”“360”之(zhī)類的(de)應用裝上(shàng),然後卸載,看看設備上(shàng)有沒有彈出(chū)浏覽器,浏覽器上(shàng)打開的(de)“XXX用戶反饋”?上(shàng)面寫了(le/liǎo)一些HTML表單,問我們“你爲(wéi / wèi)毛要(yào / yāo)卸載我們這(zhè)麽好的(de)應用啊?”“我們哪裏得罪你了(le/liǎo)?”“卸載之(zhī)後,你丫的(de)還裝不(bù)?”,呵呵,開個(gè)玩笑,實際效果如下圖:
[img]http://img.blog.csdn.net/20150108104422098
       好了(le/liǎo),上(shàng)面的(de)圖片是(shì)感覺似曾顯示啊?那麽這(zhè)樣的(de)一個(gè)小功能是(shì)怎麽實現的(de)呢?我們先從Java層以(yǐ)我們有的(de)Android基礎分析一下:
1,監聽系統的(de)卸載廣播,但是(shì)這(zhè)個(gè)隻能監聽其他(tā)應用的(de)卸載廣播的(de)動作,通過卸載廣播監聽自己是(shì)監聽不(bù)到(dào)的(de):失敗
2,系統配置文件,做一個(gè)标記應用是(shì)否卸載,判斷标記來(lái)show用戶反饋,顯然這(zhè)也(yě)是(shì)不(bù)合理的(de),因爲(wéi / wèi)應用卸載之(zhī)後,配置文件也(yě)沒有了(le/liǎo)。
3,靜默安裝另一個(gè)程序,監聽自己的(de)應用被卸載的(de)動作。前提是(shì)要(yào / yāo)root,才能實現。但是(shì)市場絕大(dà)多數手機都是(shì)默認沒有root權限的(de)。
4,服務檢測,隻能是(shì)自己開啓,當自身被卸載了(le/liǎo),服務也(yě)一并被幹掉了(le/liǎo)。
以(yǐ)上(shàng)幾點看起來(lái)都無法實現這(zhè)個(gè)功能,确實如此啊,單純的(de)從Java層是(shì)做不(bù)到(dào)這(zhè)一點的(de)。

2,原理分析


       上(shàng)面情景分析後表明Java實現不(bù)了(le/liǎo)這(zhè)樣的(de)一個(gè)功能,是(shì)否該考慮一下使用JNI了(le/liǎo),用C在(zài)底層爲(wéi / wèi)我們實現這(zhè)樣一個(gè)打開内置浏覽器加載用戶反饋網頁即可,在(zài)知道(dào)這(zhè)個(gè)方法之(zhī)前,我們有必要(yào / yāo)了(le/liǎo)解以(yǐ)下幾個(gè)知識點。
1.通過c語言,c進程監視。
    既然Java做不(bù)到(dào)的(de)話,我們試着使用C語言在(zài)底層實現好了(le/liǎo),讓C語言調用Android adb的(de)命令去打開内置浏覽器。
判斷自己是(shì)否被卸載
andoird程序在(zài)被安裝的(de)時(shí)候會在(zài)/data/data/目錄下生成一個(gè)以(yǐ)爲(wéi / wèi)包名爲(wéi / wèi)文件名的(de)目錄/data/data/包名
監聽該目錄是(shì)否還存在(zài),如果不(bù)存在(zài),就(jiù)證明應用被卸載了(le/liǎo)。

2.c代碼可以(yǐ)複制一個(gè)當前的(de)進程作爲(wéi / wèi)自己的(de)兒子(zǐ),父進程銷毀的(de)時(shí)候,子(zǐ)進程還存在(zài)。
fork()函數:
        fork()函數通過系統調用創建一個(gè)與原來(lái)進程幾乎完全相同的(de)進程,兩個(gè)進程可以(yǐ)做相同的(de)事,相當于(yú)自己生了(le/liǎo)個(gè)兒子(zǐ),如果初始參數或者傳入的(de)參數不(bù)一樣,兩個(gè)進程做的(de)事情也(yě)不(bù)一樣。當前進程調用fork函數之(zhī)後,系統先給當前進程分配資源,然後再将當前進程的(de)所有變量的(de)值複制到(dào)新進程中(隻有少數值不(bù)一樣),相當于(yú)克隆了(le/liǎo)一個(gè)自己。
       pid_t fpid = fork()被調用前,就(jiù)一個(gè)進程執行該段代碼,這(zhè)條語句執行之(zhī)後,就(jiù)将有兩個(gè)進程執行代碼,兩個(gè)進程執行沒有固定先後順序,主要(yào / yāo)看系統調度策略,fork函數的(de)特别之(zhī)處在(zài)于(yú)調用一次,但是(shì)卻可以(yǐ)返回兩次,甚至是(shì)三種的(de)結果
(1)在(zài)父進程中返回子(zǐ)進程的(de)進程id(pid)
(2)在(zài)子(zǐ)進程中返回0
(3)出(chū)現錯誤,返回小于(yú)0的(de)負值
出(chū)現錯誤原因:(1)進程數已經達到(dào)系統規定 (2)内存不(bù)足,此時(shí)返回



3.在(zài)c代碼的(de)子(zǐ)進程中監視父進程是(shì)否被卸載,如果被卸載,通知Android系統打開一個(gè)url,卸載調查的(de)網頁。
AM命令

        Android系統提供的(de)adb工具,在(zài)adb的(de)基礎上(shàng)執行adb shell就(jiù)可以(yǐ)直接對android系統執行shell命令
        am命令:在(zài)Android系統中通過adb shell 啓動某個(gè)Activity、Service、撥打電話、啓動浏覽器等操作Android的(de)命令。
        am命令的(de)源碼在(zài)Am.java中,在(zài)shell環境下執行am命令實際是(shì)啓動一個(gè)線程執行Am.java中的(de)主函數(main方法),am命令後跟的(de)參數都會當做運行時(shí)參數傳遞到(dào)主函數中,主要(yào / yāo)實現在(zài)Am.java的(de)run方法中。
        am命令可以(yǐ)用start子(zǐ)命令,和(hé / huò)帶指定的(de)參數,start是(shì)子(zǐ)命令,不(bù)是(shì)參數
常見參數:-a:表示動作,-d:表示攜帶的(de)數據,-t:表示傳入的(de)類型,-n:指定的(de)組件名

例如,我們現在(zài)在(zài)命令行模式下進入adb shell下,使用這(zhè)個(gè)命令去打開一個(gè)網頁
[img]http://img.blog.csdn.net/20150108115851865
類似的(de)命令還有這(zhè)些:
撥打電話
命令:am start -a android.intent.action.CALL -d tel:電話号碼
示例:am start -a android.intent.action.CALL -d tel:10086
打開一個(gè)網頁
命令:am start -a android.intent.action.VIEW -d  網址
示例:am start -a android.intent.action.VIEW -d  http://www.baidu.com 
啓動一個(gè)服務
命令:am startservice <服務名稱>
示例:am startservice -n com.android.music/com.android.music.MediaPlaybackService
execlp()函數

          execlp函數簡單的(de)來(lái)說(shuō)就(jiù)是(shì)C語言中執行系統命令的(de)函數
          execlp()會從PATH 環境變量所指的(de)目錄中查找符合參數file 的(de)文件名, 找到(dào)後便執行該文件, 然後将第二個(gè)以(yǐ)後的(de)參數當做該文件的(de)argv[0], argv[1], ..., 最後一個(gè)參數必須用空指針(NULL)作結束.
          android開發中,execlp函數對應android的(de)path路徑爲(wéi / wèi)system/bin/目錄下

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

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

編寫代碼實現

1,Java層定義native方法


       在(zài)Java層定義一個(gè)native方法,提供在(zài)Java端和(hé / huò)C端調用
public native void uninstall(String packageDir, int sdkVersion);

該方法需要(yào / yāo)傳遞應用的(de)安裝目錄和(hé / huò)當前設備的(de)版本号,在(zài)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

       方法簽名生成好之(zhī)後,工程上(shàng)右鍵 --> Android Tools --> Add Native Support,在(zài)彈出(chū)的(de)對話框中輸入編輯的(de)C/C++的(de)文件名,确定之(zhī)後,在(zài)工程的(de)自動生成的(de)jni目錄下找到(dào)cpp後綴名的(de)文件修改爲(wéi / wèi).c後綴名的(de)文件,因爲(wéi / wèi)本案例是(shì)基于(yú)C語言上(shàng)實現的(de),然後同樣修改Android.mk文件中的(de)LOCAL_SRC_FILES爲(wéi / wèi).c的(de)C文件,最後将上(shàng)面生成好的(de).h方法簽名文件拷貝到(dào)jni目錄下。

3,編寫C語言代碼


        正如上(shàng)面原理分析的(de)那樣,我們在(zài)實現這(zhè)樣一個(gè)功能的(de)時(shí)候用Java是(shì)無法實現的(de),隻能在(zài)C中克隆出(chū)一個(gè)當前App的(de)子(zǐ)進程,讓這(zhè)個(gè)子(zǐ)進程去監聽應用本身的(de)卸載。那麽實現這(zhè)樣的(de)功能我們需要(yào / yāo)哪些步驟呢?下面就(jiù)是(shì)編寫代碼的(de)思路:
1,将傳遞過來(lái)的(de)java的(de)包名轉爲(wéi / wèi)c的(de)字符串
2,創建當前進程的(de)克隆進程
3,根據返回值的(de)不(bù)同做不(bù)同的(de)操作
4,在(zài)子(zǐ)進程中監視/data/data/包名這(zhè)個(gè)目錄
5,目錄被删除,說(shuō)明被卸載,執行打開用戶反饋的(de)頁面
#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* 這(zhè)個(gè)代表char數組的(de)首地(dì / de)址
 * Jstring2CStr 把java中的(de)jstring的(de)類型轉化成一個(gè)c語言中的(de)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"); // 得到(dào)一個(gè)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數組的(de)長度
	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,将傳遞過來(lái)的(de)java的(de)包名轉爲(wéi / wèi)c的(de)字符串
	char * pd = Jstring2CStr(env, packageDir);

	// 2,創建當前進程的(de)克隆進程
	pid_t pid = fork();

	// 3,根據返回值的(de)不(bù)同做不(bù)同的(de)操作,<0,>0,=0
	if (pid < 0) {
		// 說(shuō)明克隆進程失敗
		LOGD("current crate process failure");
	} else if (pid > 0) {
		// 說(shuō)明克隆進程成功,而(ér)且該代碼運行在(zài)父進程中
		LOGD("crate process success,current parent pid = %d", pid);
	} else {
		// 說(shuō)明克隆進程成功,而(ér)且代碼運行在(zài)子(zǐ)進程中
		LOGD("crate process success,current child pid = %d", pid);

		// 4,在(zài)子(zǐ)進程中監視/data/data/包名這(zhè)個(gè)目錄
		while (JNI_TRUE) {
			FILE* file = fopen(pd, "rt");

			if (file == NULL) {
				// 應用被卸載了(le/liǎo),通知系統打開用戶反饋的(de)網頁
				LOGD("app uninstall,current sdkversion = %d", sdkVersion);
				if (sdkVersion >= 17) {
					// Android4.2系統之(zhī)後支持多用戶操作,所以(yǐ)得指定用戶
					execlp("am", "am", "start", "--user", "0", "-a",
							"android.intent.action.VIEW", "-d",
							"http://www.baidu.com", (char*) NULL);
				} else {
					// Android4.2以(yǐ)前的(de)版本無需指定用戶
					execlp("am", "am", "start", "-a",
							"android.intent.action.VIEW", "-d",
							"http://www.baidu.com", (char*) NULL);
				}
			} else {
				// 應用沒有被卸載
				LOGD("app run normal");
			}
			sleep(1);
		}
	}

}
        上(shàng)述代碼就(jiù)如上(shàng)述的(de)步驟一樣,用C代碼實現了(le/liǎo),首先注意的(de)一點就(jiù)是(shì)Android的(de)版本問題,衆所周知,Android是(shì)基于(yú)Linux的(de)非常優秀的(de)操作系統,而(ér)且在(zài)Android4.2版本以(yǐ)後支持多用戶操作,但是(shì)這(zhè)也(yě)給我們這(zhè)個(gè)小小的(de)項目中帶來(lái)了(le/liǎo)不(bù)便之(zhī)處,因爲(wéi / wèi)在(zài)多用戶情況下執行am命令的(de)時(shí)候強制指定一個(gè)用戶和(hé / huò)一個(gè)編号,在(zài)Android4.2之(zhī)前的(de)版本這(zhè)些參數是(shì)沒有必要(yào / yāo)的(de),所以(yǐ)我們在(zài)編寫C代碼的(de)時(shí)候需要(yào / yāo)區别Android系統版本,分别執行相應的(de)am命令,關于(yú)獲取Android系統版本可以(yǐ)在(zài)Java層實現,然後将其作爲(wéi / wèi)參數傳遞給C代碼中,C代碼根據Android版本爲(wéi / wèi)判斷條件執行am命令。
        注意:爲(wéi / wèi)了(le/liǎo)簡便起見,我在(zài)C代碼監視應用是(shì)否被卸載的(de)時(shí)候,使用了(le/liǎo)一個(gè)While(true)的(de)死循環,并且是(shì)每隔1毫秒執行一次監視檢測,這(zhè)樣寫的(de)代碼是(shì)“不(bù)環保的(de)”,想想這(zhè)樣的(de)結果是(shì)程序被不(bù)停的(de)執行,LOG被不(bù)停的(de)打印,造成cpu計算資源浪費和(hé / huò)耗電是(shì)難免的(de)。最好的(de)解決方案是(shì),使用Android給我們提供的(de)FileObserve文件觀察者,FileObserve使用到(dào)的(de)是(shì)Linux系統下的(de)inotify進程,用來(lái)監視文件目錄的(de)變化的(de),本實例中如果需要(yào / yāo)優化就(jiù)需要(yào / yāo)使用這(zhè)個(gè)API,但是(shì)需要(yào / yāo)的(de)知識就(jiù)更加多了(le/liǎo),我現在(zài)爲(wéi / wèi)了(le/liǎo)簡單的(de)演示起見,暫時(shí)用了(le/liǎo)while(true)死循環,關于(yú)後期的(de)優化版本,等我寫出(chū)來(lái),再一起公布一下!

4,編譯.so動态庫


       正如上(shàng)篇博客寫的(de)那樣,我們編寫好了(le/liǎo)C源碼之(zhī)後,就(jiù)需要(yào / yāo)使用ndk-build命令來(lái)編譯成.so文件了(le/liǎo),具體編譯的(de)過程也(yě)是(shì)非常簡單的(de),在(zài)Eclipse中切換到(dào)C/C++編輯的(de)手下,找到(dào)“小錘子(zǐ)”按鈕,點擊一下就(jiù)開始編譯了(le/liǎo),如果代碼沒有出(chū)現錯誤的(de)情況,編譯之(zhī)後的(de)結果是(shì)這(zhè)樣的(de):
[img]http://img.blog.csdn.net/20150108165919244

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


        上(shàng)面的(de)工作做好了(le/liǎo),剩下的(de)就(jiù)是(shì)在(zài)Java中加載這(zhè)個(gè)鏈接庫,和(hé / huò)調用這(zhè)個(gè)本地(dì / de)方法了(le/liǎo)。首先,要(yào / yāo)獲取本應用安裝的(de)目錄/data/data/包名,然後獲取當前設備的(de)版本号,一起傳給本地(dì / de)方法中,最後調用這(zhè)個(gè)方法。
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,測試


       好了(le/liǎo),應用是(shì)做完了(le/liǎo),我們clean一下工程,然後啓動一個(gè)基于(yú)ARM的(de)模拟器,運行這(zhè)個(gè)程序,回到(dào)桌面,點擊應用圖片——卸載掉這(zhè)個(gè)應用,看看效果:
[img]http://img.blog.csdn.net/20150108170609306
好了(le/liǎo),大(dà)家看看效果吧,實際上(shàng)打開的(de)網頁應該是(shì)用戶反饋調查頁面,由于(yú)我暫時(shí)沒有服務器,所以(yǐ)将網址定向到(dào)了(le/liǎo)百度首頁了(le/liǎo),大(dà)家在(zài)開發的(de)時(shí)候,可以(yǐ)将execlp函數裏的(de)參數網址改成自己的(de)服務器網址,這(zhè)樣就(jiù)大(dà)功告成了(le/liǎo)。檢查一下Log日志的(de)輸出(chū):
[img]http://img.blog.csdn.net/20150108171022140
看到(dào)了(le/liǎo),LOG輸入日志跟代碼流程是(shì)一緻的(de),好了(le/liǎo),源碼在(zài)下面的(de)鏈接下,有興趣的(de)朋友可以(yǐ)下載研究,歡迎你給我提出(chū)寶貴意見,大(dà)家一起學習一起進步!
源碼請在(zài)這(zhè)裏下載

相關案例查看更多