Android卸载监听实现

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);
        }
    }
}

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,050评论 25 707
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,678评论 6 342
  • 维年月日,潮州刺史韩愈使军事衙推秦济,以羊一、猪一,投恶溪之潭水,以与鳄鱼食,而告之曰: 昔先王既有天下,列山...
    静静安静的炉阅读 389评论 0 1
  • 一转身,也许就是一个十年 今天偶然间看到了关于我的母校----禹州三高的一部毕业班的纪录片,三高记忆,就这样突兀...
    秦风小北阅读 198评论 0 0