【记录问题解决】 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完全开启之后进行。

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

相关推荐
一枚小小程序员哈3 小时前
基于Android的随身小管家APP的设计与实现/基于SSM框架的财务管理系统/android Studio/java/原生开发
android·ide·android studio
stevenzqzq3 小时前
android data 文件夹作用
android
2501_915918413 小时前
iOS 应用上架全流程实践,从开发内测到正式发布的多工具组合方案
android·ios·小程序·https·uni-app·iphone·webview
auxor8 小时前
Android 开机动画音频播放优化方案
android
whysqwhw8 小时前
安卓实现屏幕共享
android
深盾科技8 小时前
Kotlin Data Classes 快速上手
android·开发语言·kotlin
一条上岸小咸鱼9 小时前
Kotlin 基本数据类型(五):Array
android·前端·kotlin
whysqwhw9 小时前
Room&Paging
android
whysqwhw9 小时前
RecyclerView超长列表优化
android
Tiger_Hu9 小时前
Android系统日历探索
android