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.360.cn/web/uninstall/uninstall.html",(char*)NULL);

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

编写代码实现

1,Java层定义native方法

在Java层定义一个native方法,提供在Java端和C端调用

[java] view plaincopyprint?
  1. public native void uninstall(String packageDir, int sdkVersion);

该方法需要传递应用的安装目录和当前设备的版本号,在Java代码中获取,传递给C代码处理。

2,使用javah命令生成方法签名头文件

[cpp] view plaincopyprint?
  1. /* DO NOT EDIT THIS FILE - it is machine generated */
  2. #include <jni.h>
  3. /* Header for class com_example_appuninstall_MainActivity */
  4. #ifndef _Included_com_example_appuninstall_MainActivity
  5. #define _Included_com_example_appuninstall_MainActivity
  6. #ifdef __cplusplus
  7. extern "C" {
  8. #endif
  9. /*
  10. * Class:     com_example_appuninstall_MainActivity
  11. * Method:    uninstall
  12. * Signature: (Ljava/lang/String;)V
  13. */
  14. JNIEXPORT void JNICALL Java_com_example_appuninstall_MainActivity_uninstall
  15. (JNIEnv *, jobject, jstring);
  16. #ifdef __cplusplus
  17. }
  18. #endif
  19. #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,目录被删除,说明被卸载,执行打开用户反馈的页面

[cpp] view plaincopyprint?
  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. * 返回值 char* 这个代表char数组的首地址
  15. * Jstring2CStr 把java中的jstring的类型转化成一个c语言中的char 字符串
  16. */
  17. char* Jstring2CStr(JNIEnv* env, jstring jstr) {
  18. char* rtn = NULL;
  19. jclass clsstring = (*env)->FindClass(env, "java/lang/String"); //String
  20. jstring strencode = (*env)->NewStringUTF(env, "GB2312"); // 得到一个java字符串 "GB2312"
  21. jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes",
  22. "(Ljava/lang/String;)[B"); //[ String.getBytes("gb2312");
  23. jbyteArray barr = (jbyteArray) (*env)->CallObjectMethod(env, jstr, mid,
  24. strencode); // String .getByte("GB2312");
  25. jsize alen = (*env)->GetArrayLength(env, barr); // byte数组的长度
  26. jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
  27. if (alen > 0) {
  28. rtn = (char*) malloc(alen + 1); //"\0"
  29. memcpy(rtn, ba, alen);
  30. rtn[alen] = 0;
  31. }
  32. (*env)->ReleaseByteArrayElements(env, barr, ba, 0); //
  33. return rtn;
  34. }
  35. JNIEXPORT void JNICALL Java_com_example_appuninstall_MainActivity_uninstall(
  36. JNIEnv * env, jobject obj, jstring packageDir, jint sdkVersion) {
  37. // 1,将传递过来的java的包名转为c的字符串
  38. char * pd = Jstring2CStr(env, packageDir);
  39. // 2,创建当前进程的克隆进程
  40. pid_t pid = fork();
  41. // 3,根据返回值的不同做不同的操作,<0,>0,=0
  42. if (pid < 0) {
  43. // 说明克隆进程失败
  44. LOGD("current crate process failure");
  45. } else if (pid > 0) {
  46. // 说明克隆进程成功,而且该代码运行在父进程中
  47. LOGD("crate process success,current parent pid = %d", pid);
  48. } else {
  49. // 说明克隆进程成功,而且代码运行在子进程中
  50. LOGD("crate process success,current child pid = %d", pid);
  51. // 4,在子进程中监视/data/data/包名这个目录
  52. while (JNI_TRUE) {
  53. FILE* file = fopen(pd, "rt");
  54. if (file == NULL) {
  55. // 应用被卸载了,通知系统打开用户反馈的网页
  56. LOGD("app uninstall,current sdkversion = %d", sdkVersion);
  57. if (sdkVersion >= 17) {
  58. // Android4.2系统之后支持多用户操作,所以得指定用户
  59. execlp("am", "am", "start", "--user", "0", "-a",
  60. "android.intent.action.VIEW", "-d",
  61. "http://www.baidu.com", (char*) NULL);
  62. } else {
  63. // Android4.2以前的版本无需指定用户
  64. execlp("am", "am", "start", "-a",
  65. "android.intent.action.VIEW", "-d",
  66. "http://www.baidu.com", (char*) NULL);
  67. }
  68. } else {
  69. // 应用没有被卸载
  70. LOGD("app run normal");
  71. }
  72. sleep(1);
  73. }
  74. }
  75. }

上述代码就如上述的步骤一样,用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 plaincopyprint?
  1. public class MainActivity extends Activity {
  2. static {
  3. System.loadLibrary("uninstall");
  4. }
  5. public native void uninstall(String packageDir, int sdkVersion);
  6. @Override
  7. protected void onCreate(Bundle savedInstanceState) {
  8. super.onCreate(savedInstanceState);
  9. setContentView(R.layout.activity_main);
  10. String packageDir = "/data/data/" + getPackageName();
  11. int sdkVersion = android.os.Build.VERSION.SDK_INT;
  12. uninstall(packageDir, sdkVersion);
  13. }
  14. }

6,测试

好了,应用是做完了,我们clean一下工程,然后启动一个基于ARM的模拟器,运行这个程序,回到桌面,点击应用图片——卸载掉这个应用,看看效果:

好了,大家看看效果吧,实际上打开的网页应该是用户反馈调查页面,由于我暂时没有服务器,所以将网址定向到了百度首页了,大家在开发的时候,可以将execlp函数里的参数网址改成自己的服务器网址,这样就大功告成了。检查一下Log日志的输出:

看到了,LOG输入日志跟代码流程是一致的,好了,源码在下面的链接下,有兴趣的朋友可以下载研究,欢迎你给我提出宝贵意见,大家一起学习一起进步!

经过查询资料,我已经了解不使用while(true)轮询方式,改用Linux的Inotify机制监听应用安装目录的实现方法了,关于最新优化版本的案例已经做完,请点击这里查看实现原理和代码:Android NDK开发(九)——应用监听自身卸载升级版,使用Inotify监听安装目录

源码请在这里下载

监听自身卸载,弹出用户反馈调查相关推荐

  1. android 键盘隐藏监听,安卓监听软键盘弹出与隐藏的两种方法

    需求: 现在有一个需求是点击一行文本框,弹出一个之前隐藏的输入框,输入完成后按返回键或者其他的东西隐藏键盘和输入框,将输入框的内容填充到文本框中. 实现: 拿到这个需求的第一反应就是写一个监听来监听键 ...

  2. Android页面监听虚拟键盘弹出、收起

    js 移动端关于页面布局,如果底部有 position: fixed 的盒子, 又有input,当软键盘弹出收起都会影响页面布局. 如图: 页面这时候Android可以监听resize事件,代码如下, ...

  3. 安卓键盘事件监听,键盘弹出收起

    一.键盘事件监听 1.在mainifest.xml 中设置activity模式 ```<activityandroid:name=".ui.activity.MainActivity& ...

  4. Android 监听网络变化弹出提示窗口

    项目有这个需求,监听如果网络断开后3秒内如果没有恢复则弹出网络异常的页面.于是在度娘找了些资料自己写了一个.现在分享一下出现的问题以及解决方法. 1.查到要监听网络需要使用广播接收者.于是摘了一段网上 ...

  5. java-swing-事件监听-MouseEvent-右键弹出菜单

    这篇文章对 MouseEvent 想说的主要是关于鼠标右键弹出菜单的一些体会 关于MouseEvent的一些信息 事件名称:MouseEvent 事件监听接口:MouseListener 需要注意的是 ...

  6. js 监听手机输入法弹出

    <script>var winHeight = $(window).height(); //获取当前页面高度$(window).resize(function () {var thisHe ...

  7. android webview监听软键盘弹出和隐藏来修改虚拟导航栏颜色

    最近项目中用到了webview,然后里面有输入框,当我们点击输入框的时候,软键盘挡住了布局,这就尴尬了,并且产品说,只有在软键盘弹出的时候底部的虚拟导航栏为黑色,软键盘隐藏的时候虚拟导航栏应该也隐藏. ...

  8. Swift--监听iPhone键盘弹出及隐藏事件

    开发需求:对键盘弹出及隐藏事件进行监听 需要通过NotificationCenter对键盘事件进行监听 //键盘即将弹出NotificationCenter.default.addObserver(s ...

  9. win7如何设置某个软件不弹出用户账户控制

    手动修改注册表: 在 HKEY_CURRENT_USERS\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers 键下面 ...

  10. 禁用计算机账户控制,win8系统禁止弹出用户账户控制窗口的方法

    有不少win8系统用户在运行一些软件程序的时候,发现Windows总是会自动弹出用户账户控制窗口,虽然可以有效防止有害程序更改计算机设置,但是对于一些可信任的程序来说就多余了,那么要win8系统如何禁 ...

最新文章

  1. swift 和 oc中检测textfield是否输入数字
  2. 中的count函数_关于计数的5个函数都不掌握,那就真的Out了!
  3. jQuery插件ASP.NET应用之AjaxUpload
  4. 传智168期JavaEE struts2杜宏 day32~day33(2017年2月15日23:27:09)
  5. 迅雷7核心技术Bolt界面引擎正式开放
  6. 智能驾驶场景库设计方法-V2X
  7. 计算机打印字与印刷字的大小,字号与尺寸对表.doc
  8. 小米怎么和计算机连接网络连接网络,电脑怎么连接小米路由器上网
  9. 奥克兰大学商学院计算机专业,奥克兰大学的商科专业 推荐三大专业
  10. 12月运营/营销/市场/广告人热点营销指南!
  11. 做短视频怎么赚钱,盈利模式包括哪些模式,如何做短视频自媒体赚钱
  12. MySQL_day2笔记
  13. FoxyProxy使用教程
  14. 自定义校验手机号码和电话号码注解
  15. 高跟鞋踩猫、踩狗视频下载
  16. Windows下解决依赖动态库问题:bat脚本实现自动复制dll文件
  17. Eclipse安装以及J2EE的安装
  18. 以太坊智能合约编程简单教程(全)
  19. 玩转控件:Fucking ERP之流程图
  20. 一天让你成为PPT达人

热门文章

  1. 红黑树简介与C++应用
  2. 基于词典的社交媒体内容的情感分析(Python实现)
  3. leetcode刷题日记-转换成小写字母
  4. 遥感软件envi5.31
  5. dotween路径移动_使用DOTween Pro插件设置物体移动的位置、移动的方式、以及动画结束时执行方法...
  6. 实习踩坑之路:LocalDateTime计算间隔天数,compareTo/Period的beetween方法导致的bug
  7. 实习成长:logback收集项目日志,实现日志告警机器人
  8. 实习踩坑之路:快速失败:使用stream流便利集合的时候删除了对象,导致抛错Null
  9. 实习踩坑之路:Date、LocalDate和LocalDateTime的区别
  10. Flutter实战之AS快键键