android 监听 删除,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);

}

}

}

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值