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);
}
}
}
本文结束,经过这个功能的实现,收获良多。