【记录问题解决】 VPNService 导致手机VPN无限重启

背景

新版本发布后,忽然线上有用户反馈使用我们的APP开启VPN后,手机上的VPN图标不停的闪烁。并且发现这是个概率性问题。

这个问题发生后,直接影响到了用户VPN相关功能的使用。所以属于很严重的一个问题。

分析流程

  • 系统VPN状态栏的问题 ?

看到这个反馈,在没分析日志之前,首先脑海中第一反应是系统状态栏VPN发生问题了。由于问题是概率性的,我们自己测试没有复现过,所以只好等待用户发送日志给我们。

拿到日志后,先看了下VPN逻辑中最重要的VPNService的流程是否正确,因为VPNService会直接影响到系统VPN和VPN图标的显示与否。

对于不是很了解VPNService的同学,可以继续看下方对于VPNService的使用说明~

科普一下VPNService的使用流程

下图是官方文档中对于VPNService的使用说明。 developer.android.com/reference/k...

  • 总结起来就是:
  1. 定义一个service继承自VPNService
  2. 在manifest中声明好权限
  3. 在Service的onCreate方法中构建VPNConfig
  4. 通过establish方法获取到VPN的ParcelFileDescriptor,简称pfd
  5. 在service的onDestroy方法中回收pfd
  6. 在需要开启VPN的地方,通过startService方法启动Service
  7. 在关闭VPN的地方,通过stopService关闭service

但是我们使用了bindService 来启动VPNService

Android O之后,google建议我们启动前台服务,对于后台服务必须显示一个通知,否则会抛出异常。

但是我们之前的版本发现,即使我们后台启动了VPNService,显示了通知,仍然有概率会抛出异常。原因是因为系统判断是否抛异常,是根据时间判断的,如果5s内没有显示通知,就会抛异常。而服务是极有可能在5s内做不完逻辑的。

由于线上监控的这个异常不断,为了根治这个问题,首先想service的替代方案。google建议使用worker和 JobScheduler 来替代service。但是我们这里比较特殊,我们想使用VPN,必须要继承VPNService。没办法,又想要后台启动服务,那么就考虑到service的另一种启动方式,bindService。

该方式不会存在后台启动Service的异常,并且替换后,实测,确实可以正常使用,对VPN功能无影响。

所以当前版本,我们做了改动,将启动VPNService的方式,由startService改为bindService。

回到VPN图标闪烁问题本身

以上是我们为什么要使用bindService。好,当前版本使用了bindService,结果忽然就出现了问题,那么第一反应自然就是,是不是bindService导致的这个问题呢?

带着疑虑,使用我们的APP 想了各种方法进行了测试,问题不复现。

分析日志

从用户拿到日志后,打开日志,搜索VPN,可以看到VPNService打印了日志,日志显示VPNService一直在无限的重启销毁再重启。

从日志中的红框可以看到,Service一直在无限重复执行onCreate和onDestroy方法。     同时 VPN的方法establish也一直在执行。(对于VPN来讲,执行了系统方法 establish,状态栏就会显示VPN图标了)所以可以看到VPNService一直重复执行生命周期,导致establish一直重复执行,而我们在onDestroy中会回收VPN,VPN图标会在回收时消失。所以一直无限循环这个流程,对用户感知就是图标一直在闪烁。

问题原因知道了,那么第一时间排查方向就是,代码是否存在无限循环调用bindService和unBIndService。排查了代码,确定不存在该循环,且开启service和停止service均有日志打印,在出问题时刻,确实没发现相关日志。

再次回到日志,我们发现第一次的service启动和停止调用时间比较近,于是尝试做个Demo来看下能否复现问题。

尝试复现

在APP上增加了一个按钮,点击按钮,直接调用bindService和unBindService。

ini 复制代码
context.bindService(intent, muCC, bindFlags);
context.unbindService(muCC);

点击按钮,发现问题复现。现象就是VPN不停的闪烁。查看日志,发现VPNService不停的创建和销毁。

而在自定义的VPNService的onCreate方法里,只有如下一行逻辑,构建通过VPN系统方法获取到PFD。于是接下来有三个思考方向。

ini 复制代码
ParcelFileDescriptor pfd = builder.establish();
  1. Service机制里的bindService或unBindService存在问题会导致Service重启
  2. VPNService里的establish方法存在问题导致重启
  3. VPNService本身存在问题导致重启

验证猜想

对于第1个方向,直接定义了一个空的Service,一样的调用方式,发现没问题。 猜想1排除。

对于第2个猜想,注释掉了VPNService里的establish方法,点击Demo按钮,发现问题不会复现。一下子排除了猜想3,验证了猜想2.

因为我们在onDestroy中还会对pfd进行回收。而注释掉establish方法后,pfd会为null,回收也就不会运行到了。为了发现是回收的问题还是establish的问题,又将pfd.close()进行了注释。

运行Demo点击按钮,神奇的发现VPN不闪烁了,但是会发现VPNService永远会重启一次。

说明establish确实存在触发VPNService重启的机制。

然后又将pfd.close()的注释打开,发现VPNService就开始无限重启了。说明close方法里有关闭VPNService的机制。

研究establish方法

为了搞明白establish为啥存在启动service的机制,研究了下其源码。

一路看到VPN.class中的establish方法,发现其源码中,正常流程下,必然会再次bindService一次。这就导致了我们每次调用establish方法,AMS就会再次收到一个启动Service的bind消息。

研究pfd.close方法

因为pfd.close方法会导致service无限重启,怀疑该方法会发送一个unBindService的消息给VPN。研究了下该源码,但是发现源码毫无关联,需要对整个VPN系统了解才知道close触发在哪里。

源码看不到,只好想办法来验证猜想。

注释掉onDestroy中的pfd.clsoe方法,APP增加一个按钮,点击按钮,调用pfd.close. 然后发现每次调用pfd.close,都会触发service的unBind方法,说明pfd.close方法会发送unBIndService消息。

导致原因

通过上述梳理,原因出来了,由于时序问题导致的该问题。 如图:

  1. 主线程通过bindService启动VPNService服务,此时发送了消息到AMS进程中
  2. 接着主线程又直接调用了unBindService方法去解绑服务,此时发送了解绑消息到AMS进程中
  3. 主线程方法执行完了,AMS发送消息给主线程,先触发了bindService
  4. 由于VPNService启动后,在establish方法里会再次bindService一次,也就是会再次发送一次bindService的消息到AMS中
  5. 当unBindService执行结束后,服务销毁,此时VPNService内部发送的那个bindService消息被触发了,所以又导致了Service的创建。
  6. 而在第5步中,service销毁时候,onDestroy会触发,在onDestroy中会进行pfd的回收,pfd的回收又会发送一次unBIndService,
  7. 这样按照上述流程下来,无限循环了。所以service开始无限重启和关闭。

对照用户现象和日志核对了一下,全部可以符合上。

为什么系统VPN进程中内部会再自己bind一次service呢?

造成上述问题的根本原因在于每次启动VPNService,establish时候,vpn进程内部都会再次bindService一次。

那么这个是系统BUG吗?为什么系统要这样设计呢?

  • 有以下几种情况:
  1. 如果经常用VPN相关的APP,可能大家会注意到一个现象,那就是手机只会显示一个VPN图标,永远不会显示两个或多个VPN图标。所以如果APP1开启了VPN,接着APP2也开启了VPN,那么这个时候VPN图标是谁显示的呢?我们不知道。
  2. 如果开启了VPN后,用户手动从设置里把VPN关掉,APP怎么知道呢?

对于上述两个问题,Android 具有一个机制,那就是后开启VPN的APP会把先开启VPN的APP挤掉。也就是APP1先开启VPN,接着APP2 开启VPN后,APP1 的VPN就会断开。所以VPN图标永远是后开启VPN的那个APP所控制的。

那么对于APP1,他怎么知道VPN断开呢?在VPNService里有个周期函数 onRevoke()。当当前APP的VPN断开时候,该方法会触发(当前APP主动关闭VPN不会触发)。

该方法原理就是VPNService每次establish时候,VPN进程里会为当前的APP进程的VPNService绑定一个serviceConnection,当VPN进程发现VPN断开时候,通过该ServiceConnection来通知APP进程里的VPNService,触发onRevoke方法。

由于是AIDL机制,所以在绑定ServiceConnection时候,会通过bindService方式进行。

  • 系统源码

解决方案

问题找到了,且不能更改源码。那么只能想办法更改调用逻辑了。 该问题的直接原因是在bindService还没结束时候就调用了unBIndService解绑VPNService导致。那么想办法限制unBindService在bindService之后调用不就可以了?

制作了两个接口 开启VPN和关闭VPN,分别在开启VPN和关闭VPN接口里调用bindService和unBIndService。接着又维护了一个回调,当开启VPN调用后,等待VPNService的establish方法执行结束后,触发回调,告知调用方,开启VPN结束,在回调触发后,代码逻辑上保证再去调用关闭VPN。

这样某些场景下,正好瞬时间内触发了先开启后关闭的情况,也会保证关闭VPN在VPN完全开启之后进行。

交给测试验证,问题解决!

相关推荐
Mr -老鬼11 分钟前
Android studio 最新Gradle 8.13版本“坑点”解析与避坑指南
android·ide·android studio
xiaolizi5674898 小时前
安卓远程安卓(通过frp与adb远程)完全免费
android·远程工作
阿杰100018 小时前
ADB(Android Debug Bridge)是 Android SDK 核心调试工具,通过电脑与 Android 设备(手机、平板、嵌入式设备等)建立通信,对设备进行控制、文件传输、命令等操作。
android·adb
梨落秋霜9 小时前
Python入门篇【文件处理】
android·java·python
遥不可及zzz11 小时前
Android 接入UMP
android
Coder_Boy_13 小时前
基于SpringAI的在线考试系统设计总案-知识点管理模块详细设计
android·java·javascript
冬奇Lab14 小时前
【Kotlin系列03】控制流与函数:从if表达式到Lambda的进化之路
android·kotlin·编程语言
冬奇Lab14 小时前
稳定性性能系列之十二——Android渲染性能深度优化:SurfaceFlinger与GPU
android·性能优化·debug
冬奇Lab15 小时前
稳定性性能系列之十一——Android内存优化与OOM问题深度解决
android·性能优化