You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 13:55:02