Android Service中Companion Object的Flag实例不一致问题排查
问题
在跨进程的客户端-服务端架构中,通过AIDL接口连接服务端的AIDLApiService,调用start(启动VPN服务)和getInfo(获取服务状态)方法时出现异常:调用start后,VPNService的onStartCommand中修改了Companion Object的isServiceStarted为true,但客户端调用getInfo始终返回false。日志显示start、getInfo调用的是同一个Companion Object实例,而onStartCommand中却是另一个完全不同的实例。
相关代码片段
AIDLApiService.kt
override fun onBind(intent: Intent): IBinder { return object : IAIDLApi.Stub() { override fun getInfo(): VPNInfo { Timber.d("[AIDL:getInfo()] CO: $VpnService") return VPNInfo( isVpnStarted = VpnService.isStarted(), // 其他字段... ) } override fun start(): Error? = try { Timber.d("[AIDL:start()] CO: $VpnService") if (!VpnService.isVpnPrepared(applicationContext)) { Error(ERROR_VPN_PERMISSION_NOT_GRANTED, null) } else { VpnService.start(applicationContext) null } } catch (e: Exception) { Timber.e(e.message) Error(e) } } }
VPNService.kt
class VPNService : VpnService() { // 其他代码... override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { Timber.d("onStartCommand") when (intent.action) { ACTION_START_VPN -> try { // 初始化VPN相关逻辑... // !!! 此处的Companion Object与调用start()和getInfo()的实例不同 isServiceStarted = true Timber.d("[onStartCommand] CO: $VpnService, VPN: $this") } catch (e: Exception) { Timber.e(e.message) exception = e // 异常处理逻辑... if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { stopForeground(STOP_FOREGROUND_REMOVE) } else { stopForeground(true) } stopSelf() } ACTION_STOP_VPN -> try { // 停止VPN逻辑... } } return START_STICKY } companion object { @Volatile private var isServiceStarted = false @Volatile private var exception: Exception? = null fun isStarted(): Boolean = isServiceStarted fun start(context: Context, isWait: Boolean = false) { Timber.e("[START] CO: $this") startSync(context = context, wait = isWait) } @Synchronized private fun startSync(context: Context, wait: Boolean) { startNotSync(context = context, wait = wait) } private fun startNotSync(context: Context, wait: Boolean) { Timber.d("starting") exception = null if (isServiceStarted) return val intent = Intent(context, VPNService::class.java) intent.action = ACTION_START_VPN startService(context = context, intent = intent) if (wait) { while (true) { Thread.sleep(SERVICE_STATE_CHECKING_INTERVAL_MILLIS) if (isServiceStarted) return val e = exception if (e != null) { throw e } } } } private fun startService(context: Context, intent: Intent) { Timber.d("start service: intent.action: ${intent.action}") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(intent) } else { context.startService(intent) } } // 其他伴生对象方法... } }
原因分析
核心问题是跨进程类加载器隔离:
AIDLApiService作为远程服务运行在独立进程中,若VPNService被配置为单独进程,两个进程会各自加载VPNService类,导致伴生对象(Companion Object)在两个进程中是完全独立的实例。- 日志中显示的不同实例ID,本质是两个进程拥有各自的
VPNService.Companion实例:AIDLApiService所在进程的伴生对象记录的状态,和VPNService所在进程的伴生对象状态完全不共享。
修复方案
要解决跨进程状态共享问题,需放弃依赖进程内的伴生对象存储状态,改用跨进程同步方案,推荐以下两种:
方案1:使用SharedPreferences存储状态
将isServiceStarted等状态存储在SharedPreferences中,所有进程通过读取SharedPreferences获取最新状态:
修改VPNService的伴生对象及生命周期方法:
companion object { private const val PREF_NAME = "vpn_service_prefs" private const val KEY_IS_STARTED = "is_service_started" fun isStarted(context: Context): Boolean { val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) return prefs.getBoolean(KEY_IS_STARTED, false) } } // 在VPNService的onStartCommand中修改状态 override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { when (intent.action) { ACTION_START_VPN -> try { // 初始化VPN逻辑... val prefs = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) prefs.edit().putBoolean(KEY_IS_STARTED, true).apply() Timber.d("[onStartCommand] VPN状态已更新为启动") } catch (e: Exception) { // 异常处理... } } return START_STICKY } // 在VPNService停止时同步更新状态 override fun onDestroy() { super.onDestroy() val prefs = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) prefs.edit().putBoolean(KEY_IS_STARTED, false).apply() }
注意:API 23及以上
MODE_MULTI_PROCESS已废弃,若需保证跨进程读写原子性,可改用ContentProvider封装状态操作。
方案2:让AIDLApiService和VPNService运行在同一进程
如果业务允许,直接将两个服务配置为同一进程,消除类加载器隔离:
在AndroidManifest.xml中给两个service指定相同的android:process属性:
<service android:name=".AIDLApiService" android:process=":vpn_process" android:exported="true"> <intent-filter> <action android:name="com.your.package.IAIDLApi" /> </intent-filter> </service> <service android:name=".VPNService" android:process=":vpn_process" android:permission="android.permission.BIND_VPN_SERVICE"> </service>
该方案仅适用于不需要VPNService单独进程的场景,若VPNService必须独立运行,则只能采用方案1或其他跨进程状态同步方式(如Room数据库、Messenger)。
内容的提问来源于stack exchange,提问作者Sunbey13

