Android知识移动开发Android 问题杂记

Android卸载监听实现

2016-03-27  本文已影响6391人  逍遥wqy

Demo地址:https://github.com/wqycsu/uninstalldemo
注:Android 5.0以上主进程被杀后,fork出的进程也会被杀,所以无法实现监听

最近项目要增加卸载统计,用户卸载时android层的代码已经没作用了,所以传统的打点统计方式也无法发挥作用。在网上搜了下目前已有的实现方式,基本都是使用linux的inotify_add_watch,方法详细介绍可以看这里,大概思路就是使用该方法监听某一个文件,如果该文件被删除就可以认为App被卸载了,思路有了,那怎么实现呢,接下来便一一解答。?这里有几点需要注意:1、如何监听?2、监听那个文件?3、如何避免重复监听?

如何监听

App被卸载时主进程肯定是靠不住了,那自然就需要另一个独立的进程实现监听,所以就需要使用fork()了,关于fork(),这篇博客讲解很清楚。fork()调用一次,能够返回两次,它有三种可能的返回值:
1、在父进程中,fork()返回新创建子进程的进程ID;
2、在子进程中,fork()返回0;
3、如果出现错误,fork()返回一个负值;
所以通过返回值我们可以知道那个是子进程,然后在子进程做需要做的事情,在这里要做的就是监听文件了。那监听那个文件呢?

监听文件选择

关于监听那个文件,我也在网上查找了资料,大家的方案基本上是两种(使用的监听方法都是inotify_add_watch(fd, dir, IN_DELETE)):
1、监听/data/data/packageName/目录
2、自己创建一个文件,然后监听自己创建的文件
使用这种监听方式时,第一种方案就暴露出了不足,因为IN_DELETE是监听被监听目录下的文件或目录被删除,所以它无法区分是清数据还是卸载。这样第二种监听方案就出现了,但是在/data/data/packageName/目录下创建文件的权限系统是否给予,这点我没去验证。所以我还是选择了第一种方案,但监听方法的参数我修改为IN_DELETE_SELF,因为该参数标识只有被监听的文件或目录本身被删除时才会触发notify事件。

如何避免重复监听

监听方式已经有了,但还是有问题的,那就是我们不可能每次打开App都fork一个进程出来,这显然是不合理的,那如何判断监听进程已经存在了呢?我想到了两种方案:
1、通过Socket端口绑定判断
进程每一次创建时会先尝试绑定一个端口,如果绑定成功则说明监听进程还没创建或已经dead,此时则fork进程。否则就说明监听进程已经存在,代码如下:

//使用绑定socket形式判断进程是否存在,有风险
int need_fork_new_process() {
    //create socket
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(fd==-1)
    {
        LOGD("create socket success");
        return 1;
    }

    //build connection address
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9876);
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int r = -2;
    r = bind(fd,(struct sockaddr*)&addr,sizeof(addr));
    if(r == -1)
    {
        LOGD("bind socket failed");
        close(fd);
        return 0;
    } else if(r == 0) {
        LOGD("bind socket success");
        return 1;
    }
    return 1;
}

2、使用文件锁判断
同样,进程每次fork前先尝试对某个文件进行加锁,如果加锁成功则说明监听进程存在,否则则fork新的进程。代码如下:

//使用文件锁的形式判断
int need_fork_new_process_with_fl(string packageNameStr) {
    string path = "/data/data/";
    path += packageNameStr;
    path += "/lib";//对app的libs目录加锁
    int fd = open(path.c_str(), O_RDONLY);
    return flock(fd,LOCK_EX|LOCK_NB);
}

在具体实现时我选择了第二种方案,因为Socket端口号绑定时,如果选择的端口已经被绑定,我无法100%保证它是被我创建的进程绑定的。

OK,到此方案雏形已经有了,接下来就看代码如何实现了:

JNIEXPORT void JNICALL Java_com_alibaba_wxlib_util_AppUninstallListener_uninstallListen
  (JNIEnv *env, jobject obj, jstring packageName, jstring versionCode, jstring ttid,
   jstring phoneBrand, jstring osVersion){

    const char * p_package = env->GetStringUTFChars(packageName, NULL);
    string packageNameStr = p_package;
    env->ReleaseStringUTFChars(packageName, p_package);

    const char * p_version = env->GetStringUTFChars(versionCode, NULL);
    string versionCodeStr = p_version;
    env->ReleaseStringUTFChars(versionCode, p_version);

    const char * p_ttid = env->GetStringUTFChars(ttid, NULL);
    string ttidStr = p_ttid;
    env->ReleaseStringUTFChars(ttid, p_ttid);

    const char * p_phoneBrand = env->GetStringUTFChars(phoneBrand, NULL);
    string phoneBrandStr = p_phoneBrand;
    env->ReleaseStringUTFChars(phoneBrand, p_phoneBrand);

    const char * p_osVersion = env->GetStringUTFChars(osVersion, NULL);
    string osVersionStr = p_phoneBrand;
    env->ReleaseStringUTFChars(osVersion, p_osVersion);

    //if(!need_fork_new_process()) {
        //LOGD("has exist a process on listening");
        //return;
    //}

    if(need_fork_new_process_with_fl(packageNameStr) != 0) {
        LOGD("文件已经被一个进程加锁了");
        return;
    }

    pid_t pid = fork();

    if(pid < 0) {
        //进程fork失败
        LOGD("进程fork失败");
    } else if(pid > 0) {
        //父进程中运行
    } else {
        //fork出的进程运行
        LOGD("fork进程成功,pid = %d", getpid());

        //初始化inotify
        int fd = inotify_init();
        if (fd < 0) {
            LOGD("inotify_init失败");
            exit(1);
        }
        string path = "/data/data/";
        path += packageNameStr;
        LOGD("监听目录:%s", path.c_str());
        int wd = inotify_add_watch(fd, path.c_str(), IN_DELETE_SELF);
        if (wd < 0) {
            LOGD("inotify_add_watch失败");
            exit(1);
        }

        //分配缓存读取event
        void *p_buf = malloc(sizeof(struct inotify_event));
        if (p_buf == NULL) {
            LOGD("分配缓存失败!");
            exit(1);
        }

        //开始监听
        //LOGD("开始监听");
        while(1) {
            ssize_t readBytes = read(fd, p_buf,sizeof(struct inotify_event));
            //read是阻塞式的,运行到这里目录被删除了
            FILE *libDir = fopen(path.c_str(), "r");//再次判断该目录是否存在,如果存在则是覆盖安装
            if(libDir == NULL) {
                // 应用被卸载了
                LOGD("应用被卸载,version = %s, ttid = %s, phoneBrand = %s, osVersion = %s", versionCodeStr.c_str(), ttidStr.c_str(), phoneBrandStr.c_str(), osVersionStr.c_str());
                inotify_rm_watch(fd, IN_DELETE);//移除监听
                free(p_buf);
                p_buf = NULL;
                //向服务端发送app卸载事件
                int ret = sendUninstallInfoToServer(packageNameStr, versionCodeStr, ttidStr, phoneBrandStr, osVersionStr);
                if(ret) {
                    LOGD("发送成功!");
                } else {
                    LOGD("发送失败!");
                }
                //工作结束,退出进程
                exit(0);
            } else {
                // 覆盖安装
                LOGD("覆盖安装了");
                fclose(libDir);
                free(p_buf);
                p_buf = NULL;
                //退出进程,因为需要重新监听新安装的版本了
                exit(0);
            }
        }
        if(p_buf != NULL) {
            free(p_buf);
        }
    }
}

本文结束,经过这个功能的实现,收获良多。

上一篇 下一篇

猜你喜欢

热点阅读