应用监听自身卸载,弹出用户反馈调查(下)——使用Inotify监听安装目录

silejiuok 8年前

来自: http://blog.csdn.net//guijiaoba/article/details/50161449


        转载出处:http://blog.csdn.net/allen315410/article/details/42555415

        在上一篇博客中,我们讲了一个小小的案例,用NDK监听应用程序自身卸载,并且打开内置浏览器加载用户调用页面。关于监听应用程序自身卸载的原理和实现方案可以在上篇博客中找到,地址是:http://blog.csdn.net/allen315410/article/details/42521251,这里就不再复述了。

        值得注意的是,在上篇博客中我也已经引述了一个案例中存在的问题,就是在监听应用程序安装目录是否被删除时,使用了while(true)这种死循环,让C代码每隔1秒钟去自动执行一次检查应用程序安装目录是否还存在,这样做效果是完全可以实现的,但是弊端也是显而易见的,由于使用了死循环,这样代码是不环保的,不可避免的重复执行,重复打印LOG,占用cpu计算资源,这是一种不太好的解决方案。

        在这里,我来介绍一个比较好的解决方案,就是使用Linux系统的一个内核特性——Inotify,来监听应用程序安装目录的变化,inotify 是一种文件系统的变化通知机制,如文件增加、删除等事件可以立刻让用户态得知,这种机制是为了弥补Linux系统在桌面领域的不足而产生,在Linux2,.6内核中被添加,值得庆幸的是我们伟大的Android系统就是构建在Linux2.6内核的基础上的,所以Android里也就包含了Inotify机制。


关于Inotify:

        在用户态,inotify 通过三个系统调用和在返回的文件描述符上的文件 I/O 操作来使用,使用 inotify 的第一步是创建 inotify 实例:

  1. int fd = inotify_init ();  
每一个 inotify 实例对应一个独立的排序的队列。

        文件系统的变化事件被称做 watches 的一个对象管理,每一个 watch 是一个二元组(目标,事件掩码),目标可以是文件或目录,事件掩码表示应用希望关注的 inotify 事件,每一个位对应一个 inotify 事件。Watch 对象通过 watch描述符引用,watches 通过文件或目录的路径名来添加。目录 watches 将返回在该目录下的所有文件上面发生的事件。
下面函数用于添加一个 watch:

  1. int wd = inotify_add_watch (fd, path, mask);  
         fd 是 inotify_init() 返回的文件描述符,path 是被监视的目标的路径名(即文件名或目录名),mask 是事件掩码, 在头文件 linux/inotify.h 中定义了每一位代表的事件。可以使用同样的方式来修改事件掩码,即改变希望被通知的inotify 事件。Wd 是 watch 描述符。

        下面是mask事件掩码的可选值:

IN_ACCESS,即文件被访问
IN_MODIFY,文件被 write
IN_ATTRIB,文件属性被修改,如 chmod、chown、touch 等
IN_CLOSE_WRITE,可写文件被 close
IN_CLOSE_NOWRITE,不可写文件被 close
IN_OPEN,文件被 open
IN_MOVED_FROM,文件被移走,如 mv
IN_MOVED_TO,文件被移来,如 mv、cp
IN_CREATE,创建新文件
IN_DELETE,文件被删除,如 rm
IN_DELETE_SELF,自删除,即一个可执行文件在执行时删除自己
IN_MOVE_SELF,自移动,即一个可执行文件在执行时移动自己
IN_UNMOUNT,宿主文件系统被 umount
IN_CLOSE,文件被关闭,等同于(IN_CLOSE_WRITE | IN_CLOSE_NOWRITE)
IN_MOVE,文件被移动,等同于(IN_MOVED_FROM | IN_MOVED_TO)
注:上面所说的文件也包括目录。


下面的函数用于删除一个 watch:

  1. int ret = inotify_rm_watch (fd, wd);  
 fd 是 inotify_init() 返回的文件描述符,wd 是 inotify_add_watch() 返回的 watch 描述符。Ret 是函数的返回值。
文件事件用一个 inotify_event 结构表示,它通过由 inotify_init() 返回的文件描述符使用通常文件读取函数 read 来获得:

  1. struct inotify_event {  
  2.         __s32           wd;             /* watch descriptor */  
  3.         __u32           mask;           /* watch mask */  
  4.         __u32           cookie;         /* cookie to synchronize two events */  
  5.         __u32           len;            /* length (including nulls) of name */  
  6.         char            name[0];        /* stub for possible name */  
  7. };  
         结构中的 wd 为被监视目标的 watch 描述符,mask 为事件掩码,len 为 name字符串的长度,name 为被监视目标的路径名,该结构的 name 字段为一个桩,它只是为了用户方面引用文件名,文件名是变长的,它实际紧跟在该结构的后面,文件名将被 0 填充以使下一个事件结构能够 4 字节对齐。注意,len 也把填充字节数统计在内。
通过 read 调用可以一次获得多个事件,只要提供的 buf 足够大。

  1. size_t len = read (fd, buf, BUF_LEN);  
         buf 是一个 inotify_event 结构的数组指针,BUF_LEN 指定要读取的总长度,buf 大小至少要不小于 BUF_LEN,该调用返回的事件数取决于 BUF_LEN 以及事件中文件名的长度。Len 为实际读去的字节数,即获得的事件的总长度。
可以在函数 inotify_init() 返回的文件描述符 fd 上使用 select() 或poll(), 也可以在 fd 上使用 ioctl 命令 FIONREAD 来得到当前队列的长度。close(fd)将删除所有添加到 fd 中的 watch 并做必要的清理。

  1. int inotify_init (void);  
  2. int inotify_add_watch (int fd, const char *path, __u32 mask);  
  3. int inotify_rm_watch (int fd, __u32 mask);  
注意:上述资料参考了 IBM Developerworks ,IBM Developerworks是个非常非常优秀的技术学习,我们可以在上面找到很多牛逼的资料来学习,如果你想更深入的了解Inotify机制,请点击 http://www.ibm.com/developerworks/cn/linux/l-inotifynew/


代码实现:

        关于代码的编写,大部分可以参考上篇博客的案例代码,因为大部分代码和配置文件以及编译步骤在上篇博客中以及写的比较详尽了,这里就不重复编写了,唯一需要改的部分就是,将C代码中的while(true)死循环部分删掉,改成用Inotify机制去监听应用目录的变化:

编译的头文件:

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

  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 <sys/inotify.h>  
  9. #include <fcntl.h>  
  10. #include <stdint.h>  
  11. #include "com_example_appuninstalldemo_MainActivity.h"  
  12. #include <android/log.h>  
  13. #define LOG_TAG "System.out.c"  
  14. #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)  
  15. #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)  
  16.   
  17. /** 
  18.  * 返回值 char* 这个代表char数组的首地址 
  19.  * Jstring2CStr 把java中的jstring的类型转化成一个c语言中的char 字符串 
  20.  */  
  21. char* Jstring2CStr(JNIEnv* env, jstring jstr) {  
  22.     char* rtn = NULL;  
  23.     jclass clsstring = (*env)->FindClass(env, "java/lang/String"); //String  
  24.     jstring strencode = (*env)->NewStringUTF(env, "GB2312"); // 得到一个java字符串 "GB2312"  
  25.     jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes",  
  26.             "(Ljava/lang/String;)[B"); //[ String.getBytes("gb2312");  
  27.     jbyteArray barr = (jbyteArray) (*env)->CallObjectMethod(env, jstr, mid,  
  28.             strencode); // String .getByte("GB2312");  
  29.     jsize alen = (*env)->GetArrayLength(env, barr); // byte数组的长度  
  30.     jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);  
  31.     if (alen > 0) {  
  32.         rtn = (char*) malloc(alen + 1); //"\0"  
  33.         memcpy(rtn, ba, alen);  
  34.         rtn[alen] = 0;  
  35.     }  
  36.     (*env)->ReleaseByteArrayElements(env, barr, ba, 0); //  
  37.     return rtn;  
  38. }  
  39.   
  40. JNIEXPORT void JNICALL Java_com_example_appuninstalldemo_MainActivity_uninstall(  
  41.         JNIEnv * env, jobject obj, jstring packageDir, jint sdkVersion) {  
  42.     // 1,将传递过来的java的包名转为c的字符串  
  43.     char * pd = Jstring2CStr(env, packageDir);  
  44.   
  45.     // 2,创建当前进程的克隆进程  
  46.     pid_t pid = fork();  
  47.   
  48.     // 3,根据返回值的不同做不同的操作,<0,>0,=0  
  49.     if (pid < 0) {  
  50.         // 说明克隆进程失败  
  51.         LOGD("current crate process failure");  
  52.     } else if (pid > 0) {  
  53.         // 说明克隆进程成功,而且该代码运行在父进程中  
  54.         LOGD("crate process success,current parent pid = %d", pid);  
  55.     } else {  
  56.         // 说明克隆进程成功,而且代码运行在子进程中  
  57.         LOGD("crate process success,current child pid = %d", pid);  
  58.   
  59.         // 4,在子进程中监视/data/data/包名这个目录  
  60.         //初始化inotify进程  
  61.         int fd = inotify_init();  
  62.         if (fd < 0) {  
  63.             LOGD("inotify_init failed !!!");  
  64.             exit(1);  
  65.         }  
  66.   
  67.         //添加inotify监听器  
  68.         int wd = inotify_add_watch(fd, pd, IN_DELETE);  
  69.         if (wd < 0) {  
  70.             LOGD("inotify_add_watch failed !!!");  
  71.             exit(1);  
  72.         }  
  73.   
  74.         //分配缓存,以便读取event,缓存大小=一个struct inotify_event的大小,这样一次处理一个event  
  75.         void *p_buf = malloc(sizeof(struct inotify_event));  
  76.         if (p_buf == NULL) {  
  77.             LOGD("malloc failed !!!");  
  78.             exit(1);  
  79.         }  
  80.   
  81.         //开始监听  
  82.         LOGD("start observer");  
  83.         ssize_t readBytes = read(fd, p_buf,sizeof(struct inotify_event));  
  84.   
  85.         //read会阻塞进程,走到这里说明收到目录被删除的事件,注销监听器  
  86.         free(p_buf);  
  87.         inotify_rm_watch(fd, IN_DELETE);  
  88.   
  89.         // 应用被卸载了,通知系统打开用户反馈的网页  
  90.         LOGD("app uninstall,current sdkversion = %d", sdkVersion);  
  91.         if (sdkVersion >= 17) {  
  92.             // Android4.2系统之后支持多用户操作,所以得指定用户  
  93.             execlp("am""am""start""--user""0""-a",  
  94.                     "android.intent.action.VIEW""-d""http://www.baidu.com",  
  95.                     (char*) NULL);  
  96.         } else {  
  97.             // Android4.2以前的版本无需指定用户  
  98.             execlp("am""am""start""-a""android.intent.action.VIEW",  
  99.                     "-d""http://www.baidu.com", (char*) NULL);  
  100.         }  
  101.   
  102.     }  
  103. }  
      代码如上所示,大家可以根据上面的Inotify介绍和代码中注释来看,实现的代码基本上不难,但是了解实现原理还得好好理解一下Inotify的实现机制,这样才能事半功倍啊。

Java层代码很简单,直接看:

[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. }  

运行之后卸载应用程序,效果如下:



源码请在这里下载