Skip to content

Commit 245b872

Browse files
committed
Optimize android vpn performance
Update ndk version Optimize more details Add linux on arm build Add win on arm build
1 parent a77b3a3 commit 245b872

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1292
-546
lines changed

.github/workflows/build.yaml

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,15 @@ jobs:
1818
- platform: windows
1919
os: windows-latest
2020
arch: amd64
21+
- platform: windows
22+
os: windows-11-arm
23+
arch: arm64
2124
- platform: linux
2225
os: ubuntu-latest
2326
arch: amd64
27+
- platform: linux
28+
os: ubuntu-24.04-arm
29+
arch: arm64
2430
- platform: macos
2531
os: macos-13
2632
arch: amd64
@@ -34,22 +40,6 @@ jobs:
3440
with:
3541
submodules: recursive
3642

37-
- name: Setup JAVA
38-
if: startsWith(matrix.platform,'android')
39-
uses: actions/setup-java@v4
40-
with:
41-
distribution: 'zulu'
42-
java-version: 17
43-
44-
- name: Setup NDK
45-
if: startsWith(matrix.platform,'android')
46-
uses: nttld/setup-ndk@v1
47-
id: setup-ndk
48-
with:
49-
ndk-version: r26b
50-
add-to-path: true
51-
link-to-sdk: true
52-
5343
- name: Setup Android Signing
5444
if: startsWith(matrix.platform,'android')
5545
run: |
@@ -62,14 +52,14 @@ jobs:
6252
- name: Setup Go
6353
uses: actions/setup-go@v5
6454
with:
65-
go-version: 'stable'
55+
go-version: '1.24.0'
6656
cache-dependency-path: |
6757
core/go.sum
6858
6959
- name: Setup Flutter
7060
uses: subosito/flutter-action@v2
7161
with:
72-
channel: stable
62+
channel: ${{ (startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')) && 'master' || 'stable' }}
7363
cache: true
7464

7565
- name: Get Flutter Dependency

.gitmodules

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,5 @@
66
path = plugins/flutter_distributor
77
url = [email protected]:chen08209/flutter_distributor.git
88
branch = FlClash
9-
[submodule "plugins/tray_manager"]
10-
path = plugins/tray_manager
11-
url = [email protected]:chen08209/tray_manager.git
12-
branch = main
139

1410

Makefile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
android_arm64:
2+
dart ./setup.dart android --arch arm64
3+
macos_arm64:
4+
dart ./setup.dart macos --arch arm64
5+
android_app:
6+
dart ./setup.dart android
7+
android_arm64_core:
8+
dart ./setup.dart android --arch arm64 --out core
9+
macos_arm64_core:
10+
dart ./setup.dart macos --arch arm64 --out core

android/app/build.gradle

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import com.android.build.gradle.tasks.MergeSourceSetFolders
2-
31
plugins {
42
id "com.android.application"
53
id "kotlin-android"
@@ -33,8 +31,8 @@ def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias
3331

3432
android {
3533
namespace "com.follow.clash"
36-
compileSdkVersion 35
37-
ndkVersion "27.1.12297006"
34+
compileSdk 35
35+
ndkVersion = "28.0.13004108"
3836

3937
compileOptions {
4038
sourceCompatibility JavaVersion.VERSION_17
@@ -48,6 +46,7 @@ android {
4846
sourceSets {
4947
main.java.srcDirs += 'src/main/kotlin'
5048
}
49+
5150
signingConfigs {
5251
if (isRelease) {
5352
release {
@@ -84,31 +83,15 @@ android {
8483
}
8584
}
8685

87-
tasks.register('copyNativeLibs', Copy) {
88-
delete('src/main/jniLibs')
89-
from('../../libclash/android')
90-
into('src/main/jniLibs')
91-
}
92-
93-
tasks.withType(MergeSourceSetFolders).configureEach {
94-
dependsOn copyNativeLibs
95-
}
96-
9786
flutter {
9887
source '../..'
9988
}
10089

10190
dependencies {
91+
implementation project(":core")
10292
implementation 'androidx.core:core-splashscreen:1.0.1'
103-
implementation 'com.google.code.gson:gson:2.10'
104-
implementation("com.android.tools.smali:smali-dexlib2:3.0.7") {
93+
implementation 'com.google.code.gson:gson:2.10.1'
94+
implementation("com.android.tools.smali:smali-dexlib2:3.0.9") {
10595
exclude group: "com.google.guava", module: "guava"
10696
}
107-
}
108-
109-
110-
afterEvaluate {
111-
assembleDebug.dependsOn copyNativeLibs
112-
113-
assembleRelease.dependsOn copyNativeLibs
114-
}
97+
}
Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
xmlns:tools="http://schemas.android.com/tools">
23
<!-- The INTERNET permission is required for development. Specifically,
34
the Flutter tool needs it to communicate with the running application
45
to allow setting breakpoints, to provide hot reload, etc.
56
-->
6-
<application android:label="FlClash Debug" tools:replace="android:label">
7+
<application
8+
android:icon="@mipmap/ic_launcher"
9+
android:label="FlClash Debug"
10+
tools:replace="android:label">
711
<service
8-
android:name=".services.FlClashTileService"
9-
android:label="FlClash Debug"
10-
tools:replace="android:label">
11-
</service>
12+
android:name=".services.FlClashTileService"
13+
android:label="FlClash Debug"
14+
tools:replace="android:label"
15+
tools:targetApi="24" />
1216
</application>
1317
</manifest>

android/app/src/main/kotlin/com/follow/clash/MainActivity.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.follow.clash
22

3+
import com.follow.clash.core.Core
34
import com.follow.clash.plugins.AppPlugin
45
import com.follow.clash.plugins.ServicePlugin
56
import com.follow.clash.plugins.TilePlugin

android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ data object ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
5252
}
5353

5454
private fun handleDestroy() {
55-
GlobalState.getCurrentVPNPlugin()?.handleStop()
5655
GlobalState.destroyServiceEngine()
5756
}
5857
}

android/app/src/main/kotlin/com/follow/clash/plugins/VpnPlugin.kt

Lines changed: 50 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@ import androidx.core.content.getSystemService
1414
import com.follow.clash.FlClashApplication
1515
import com.follow.clash.GlobalState
1616
import com.follow.clash.RunState
17+
import com.follow.clash.core.Core
1718
import com.follow.clash.extensions.awaitResult
18-
import com.follow.clash.extensions.getProtocol
1919
import com.follow.clash.extensions.resolveDns
20-
import com.follow.clash.models.Process
2120
import com.follow.clash.models.StartForegroundParams
2221
import com.follow.clash.models.VpnOptions
2322
import com.follow.clash.services.BaseServiceInterface
@@ -40,17 +39,20 @@ import kotlin.concurrent.withLock
4039
data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
4140
private lateinit var flutterMethodChannel: MethodChannel
4241
private var flClashService: BaseServiceInterface? = null
43-
private lateinit var options: VpnOptions
42+
private var options: VpnOptions? = null
43+
private var isBind: Boolean = false
4444
private lateinit var scope: CoroutineScope
4545
private var lastStartForegroundParams: StartForegroundParams? = null
4646
private var timerJob: Job? = null
47+
private val uidPageNameMap = mutableMapOf<Int, String>()
4748

4849
private val connectivity by lazy {
4950
FlClashApplication.getAppContext().getSystemService<ConnectivityManager>()
5051
}
5152

5253
private val connection = object : ServiceConnection {
5354
override fun onServiceConnected(className: ComponentName, service: IBinder) {
55+
isBind = true
5456
flClashService = when (service) {
5557
is FlClashVpnService.LocalBinder -> service.getService()
5658
is FlClashService.LocalBinder -> service.getService()
@@ -60,6 +62,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
6062
}
6163

6264
override fun onServiceDisconnected(arg: ComponentName) {
65+
isBind = false
6366
flClashService = null
6467
}
6568
}
@@ -90,69 +93,16 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
9093
result.success(true)
9194
}
9295

93-
"setProtect" -> {
94-
val fd = call.argument<Int>("fd")
95-
if (fd != null && flClashService is FlClashVpnService) {
96-
try {
97-
(flClashService as FlClashVpnService).protect(fd)
98-
result.success(true)
99-
} catch (e: RuntimeException) {
100-
result.success(false)
101-
}
102-
} else {
103-
result.success(false)
104-
}
105-
}
106-
107-
"resolverProcess" -> {
108-
val data = call.argument<String>("data")
109-
val process = if (data != null) Gson().fromJson(
110-
data, Process::class.java
111-
) else null
112-
val metadata = process?.metadata
113-
if (metadata == null) {
114-
result.success(null)
115-
return
116-
}
117-
val protocol = metadata.getProtocol()
118-
if (protocol == null) {
119-
result.success(null)
120-
return
121-
}
122-
scope.launch {
123-
withContext(Dispatchers.Default) {
124-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
125-
result.success(null)
126-
return@withContext
127-
}
128-
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
129-
val dst = InetSocketAddress(
130-
metadata.destinationIP.ifEmpty { metadata.host },
131-
metadata.destinationPort
132-
)
133-
val uid = try {
134-
connectivity?.getConnectionOwnerUid(protocol, src, dst)
135-
} catch (_: Exception) {
136-
null
137-
}
138-
if (uid == null || uid == -1) {
139-
result.success(null)
140-
return@withContext
141-
}
142-
val packages =
143-
FlClashApplication.getAppContext().packageManager?.getPackagesForUid(uid)
144-
result.success(packages?.first())
145-
}
146-
}
147-
}
148-
14996
else -> {
15097
result.notImplemented()
15198
}
15299
}
153100
}
154101

155102
fun handleStart(options: VpnOptions): Boolean {
103+
if (options.enable != this.options?.enable) {
104+
this.flClashService = null
105+
}
156106
this.options = options
157107
when (options.enable) {
158108
true -> handleStartVpn()
@@ -162,10 +112,9 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
162112
}
163113

164114
private fun handleStartVpn() {
165-
GlobalState.getCurrentAppPlugin()
166-
?.requestVpnPermission {
167-
handleStartService()
168-
}
115+
GlobalState.getCurrentAppPlugin()?.requestVpnPermission {
116+
handleStartService()
117+
}
169118
}
170119

171120
fun requestGc() {
@@ -235,6 +184,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
235184
}
236185

237186
private fun startForegroundJob() {
187+
stopForegroundJob()
238188
timerJob = CoroutineScope(Dispatchers.Main).launch {
239189
while (isActive) {
240190
startForeground()
@@ -256,26 +206,58 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
256206
GlobalState.runLock.withLock {
257207
if (GlobalState.runState.value == RunState.START) return
258208
GlobalState.runState.value = RunState.START
259-
val fd = flClashService?.start(options)
260-
flutterMethodChannel.invokeMethod(
261-
"started", fd
209+
val fd = flClashService?.start(options!!)
210+
Core.startTun(
211+
fd = fd ?: 0,
212+
protect = this::protect,
213+
resolverProcess = this::resolverProcess,
262214
)
263-
startForegroundJob();
215+
startForegroundJob()
264216
}
265217
}
266218

219+
private fun protect(fd: Int): Boolean {
220+
return (flClashService as? FlClashVpnService)?.protect(fd) == true
221+
}
222+
223+
private fun resolverProcess(
224+
protocol: Int,
225+
source: InetSocketAddress,
226+
target: InetSocketAddress,
227+
uid: Int,
228+
): String {
229+
val nextUid = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
230+
connectivity?.getConnectionOwnerUid(protocol, source, target) ?: -1
231+
} else {
232+
uid
233+
}
234+
if (nextUid == -1) {
235+
return ""
236+
}
237+
if (!uidPageNameMap.containsKey(nextUid)) {
238+
uidPageNameMap[nextUid] =
239+
FlClashApplication.getAppContext().packageManager?.getPackagesForUid(nextUid)
240+
?.first() ?: ""
241+
}
242+
return uidPageNameMap[nextUid] ?: ""
243+
}
244+
267245
fun handleStop() {
268246
GlobalState.runLock.withLock {
269247
if (GlobalState.runState.value == RunState.STOP) return
270248
GlobalState.runState.value = RunState.STOP
271249
stopForegroundJob()
250+
Core.stopTun()
272251
flClashService?.stop()
273252
GlobalState.handleTryDestroy()
274253
}
275254
}
276255

277256
private fun bindService() {
278-
val intent = when (options.enable) {
257+
if (isBind) {
258+
FlClashApplication.getAppContext().unbindService(connection)
259+
}
260+
val intent = when (options?.enable == true) {
279261
true -> Intent(FlClashApplication.getAppContext(), FlClashVpnService::class.java)
280262
false -> Intent(FlClashApplication.getAppContext(), FlClashService::class.java)
281263
}

android/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ subprojects {
2424
}
2525
subprojects {
2626
project.evaluationDependsOn(':app')
27+
project.evaluationDependsOn(':core')
2728
}
2829

2930
tasks.register("clean", Delete) {

android/core/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

0 commit comments

Comments
 (0)