perf(android): keep Core calls and events off the main thread
Changelog: Smoother Android UI while the proxy is busy
This commit is contained in:
@@ -64,7 +64,7 @@ object ServiceController {
|
||||
}
|
||||
}
|
||||
|
||||
fun setEventListener(callback: ((String?) -> Unit)?): Result<Unit> = runCatching {
|
||||
fun setEventListener(callback: ((ByteArray) -> Unit)?): Result<Unit> = runCatching {
|
||||
Core.updateEventListener(callback)
|
||||
}
|
||||
|
||||
|
||||
@@ -123,8 +123,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, rawResult: Result) {
|
||||
val result = MainThreadResult(rawResult)
|
||||
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||
when (call.method) {
|
||||
"moveTaskToBack" -> {
|
||||
activity?.moveTaskToBack(true)
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
/**
|
||||
* Delivers a [MethodChannel.Result] on the main thread, which is where Flutter
|
||||
* requires it, from handlers that finish on a background dispatcher.
|
||||
*
|
||||
* The hop goes through the main looper rather than the plugin's coroutine scope:
|
||||
* that scope is cancelled when the engine detaches, and a reply dropped by a
|
||||
* cancelled scope leaves the Dart future waiting forever. A reply posted after
|
||||
* the engine is gone is the messenger's problem to ignore, and it does.
|
||||
*/
|
||||
internal class MainThreadResult(
|
||||
private val delegate: MethodChannel.Result,
|
||||
) : MethodChannel.Result {
|
||||
override fun success(result: Any?) = post { delegate.success(result) }
|
||||
|
||||
override fun error(code: String, message: String?, details: Any?) =
|
||||
post { delegate.error(code, message, details) }
|
||||
|
||||
override fun notImplemented() = post { delegate.notImplemented() }
|
||||
|
||||
private fun post(block: () -> Unit) {
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
block()
|
||||
} else {
|
||||
mainHandler.post(block)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val mainHandler = Handler(Looper.getMainLooper())
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,10 @@ import com.follow.clash.common.Components
|
||||
import com.follow.clash.models.SharedState
|
||||
import com.google.gson.Gson
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.StandardMethodCodec
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -15,32 +17,39 @@ import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
private lateinit var messenger: BinaryMessenger
|
||||
private lateinit var channel: MethodChannel
|
||||
private lateinit var coreChannel: MethodChannel
|
||||
private lateinit var scope: CoroutineScope
|
||||
private val gson = Gson()
|
||||
private var commandRevision = 0
|
||||
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
channel = MethodChannel(binding.binaryMessenger, "${Components.PACKAGE_NAME}/service")
|
||||
messenger = binding.binaryMessenger
|
||||
channel = MethodChannel(messenger, SERVICE_CHANNEL)
|
||||
channel.setMethodCallHandler(this)
|
||||
// Serial: watchRoute keeps the latest intent by the order calls reach the Core.
|
||||
coreChannel = MethodChannel(
|
||||
messenger,
|
||||
CORE_CHANNEL,
|
||||
StandardMethodCodec.INSTANCE,
|
||||
messenger.makeBackgroundTaskQueue(),
|
||||
)
|
||||
coreChannel.setMethodCallHandler(::onCoreMethodCall)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel.setMethodCallHandler(null)
|
||||
coreChannel.setMethodCallHandler(null)
|
||||
scope.cancel()
|
||||
ServiceController.setEventListener(null)
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, rawResult: MethodChannel.Result) {
|
||||
// Most handlers below reply from a scope worker on Dispatchers.Default,
|
||||
// but a MethodChannel.Result has to be answered on the platform thread.
|
||||
// Wrapping once here covers every branch, including notImplemented.
|
||||
val result = MainThreadResult(rawResult)
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"init" -> initialize(result)
|
||||
"shutdown" -> shutdown(result)
|
||||
"invokeMethod" -> invokeMethod(call, result)
|
||||
"getRunTime" -> getRunTime(result)
|
||||
"syncState" -> syncState(call, result)
|
||||
"start" -> start(call, result)
|
||||
@@ -49,6 +58,13 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCoreMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"invokeMethod" -> invokeMethod(call, result)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initialize(result: MethodChannel.Result) {
|
||||
ServiceController.setEventListener(::sendEvent)
|
||||
.onSuccess { result.success("") }
|
||||
@@ -66,9 +82,6 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
result.error("INVALID_ARGUMENT", "Method call payload must be a string", null)
|
||||
return
|
||||
}
|
||||
// Inline rather than a coroutine per call, which could swap two: watchRoute
|
||||
// keeps the latest intent by the order calls reach the Core
|
||||
// (dispatchMethodCall in core/method.go), and this only hands data over.
|
||||
ServiceController.invokeMethod(data) { response ->
|
||||
result.success(response)
|
||||
}.onFailure { error ->
|
||||
@@ -118,9 +131,15 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendEvent(value: String?) {
|
||||
private fun sendEvent(value: ByteArray) {
|
||||
val message = StandardMethodCodec.INSTANCE.encodeMethodCall(MethodCall("event", value))
|
||||
scope.launch(Dispatchers.Main) {
|
||||
channel.invokeMethod("event", value)
|
||||
messenger.send(SERVICE_CHANNEL, message)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val SERVICE_CHANNEL = "${Components.PACKAGE_NAME}/service"
|
||||
val CORE_CHANNEL = "${Components.PACKAGE_NAME}/core"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,10 +67,10 @@ object Core {
|
||||
}
|
||||
|
||||
fun updateEventListener(
|
||||
callback: ((result: String?) -> Unit)?,
|
||||
callback: ((result: ByteArray) -> Unit)?,
|
||||
) {
|
||||
Mobile.setEventListener(
|
||||
callback?.let { ResultHandler { result -> it(result?.decodeToString()) } },
|
||||
callback?.let { ResultHandler { result -> it(result ?: ByteArray(0)) } },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+10
-5
@@ -10,6 +10,7 @@ import android.net.NetworkCapabilities.TRANSPORT_USB
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.Looper
|
||||
import androidx.core.content.getSystemService
|
||||
import com.follow.clash.core.Core
|
||||
@@ -32,7 +33,7 @@ internal class NetworkObserveModule(private val service: Service) : ServiceModul
|
||||
private val connectivity by lazy {
|
||||
service.getSystemService<ConnectivityManager>()
|
||||
}
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private val handler = Handler(networkObserveLooper)
|
||||
private var currentDnsList = listOf<String>()
|
||||
private var currentNetwork: Network? = null
|
||||
|
||||
@@ -56,7 +57,7 @@ internal class NetworkObserveModule(private val service: Service) : ServiceModul
|
||||
info.losingUntilMillis = System.currentTimeMillis() + maxMsToLive
|
||||
scheduleUpdateDns()
|
||||
if (maxMsToLive > 0) {
|
||||
mainHandler.postDelayed({
|
||||
handler.postDelayed({
|
||||
if (networkInfos.containsKey(network)) {
|
||||
updateDns()
|
||||
}
|
||||
@@ -80,8 +81,8 @@ internal class NetworkObserveModule(private val service: Service) : ServiceModul
|
||||
// A switch arrives as a burst, and a network that just came up has no DNS
|
||||
// until its link properties follow; the Core flushes its cache per list.
|
||||
private fun scheduleUpdateDns() {
|
||||
mainHandler.removeCallbacks(updateDnsTask)
|
||||
mainHandler.postDelayed(updateDnsTask, DNS_UPDATE_DELAY_MILLIS)
|
||||
handler.removeCallbacks(updateDnsTask)
|
||||
handler.postDelayed(updateDnsTask, DNS_UPDATE_DELAY_MILLIS)
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
@@ -131,7 +132,7 @@ internal class NetworkObserveModule(private val service: Service) : ServiceModul
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
mainHandler.removeCallbacksAndMessages(null)
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
try {
|
||||
connectivity?.unregisterNetworkCallback(callback)
|
||||
} finally {
|
||||
@@ -145,6 +146,10 @@ internal class NetworkObserveModule(private val service: Service) : ServiceModul
|
||||
private const val DNS_PORT = 53
|
||||
private const val DNS_UPDATE_DELAY_MILLIS = 200L
|
||||
|
||||
private val networkObserveLooper: Looper by lazy {
|
||||
HandlerThread("NetworkObserve").apply { start() }.looper
|
||||
}
|
||||
|
||||
private fun InetAddress.asSocketAddressText(port: Int): String = when (this) {
|
||||
is Inet6Address -> "[$hostAddress]:$port"
|
||||
is Inet4Address -> "$hostAddress:$port"
|
||||
|
||||
Reference in New Issue
Block a user