diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json
index 67a26671..da0fd63e 100644
--- a/.changelog/lang_zh-Hans.json
+++ b/.changelog/lang_zh-Hans.json
@@ -69,6 +69,7 @@
"构建工具启用 isCleanup[Paddle/Rapid]Ocr 配置选项时无法正常完成 Rebuild Project 任务的问题"
],
"improvement": [
+ "runtime.loadJar 方法增强兼容性 (by [LYS86](https://github.com/LYS86)) _[`pr #475`](http://pr.autojs6.com/475)_",
"ui.statusBarHeight 属性 (getter) 增强一定程度的兼容性",
"console.log 等方法打印全局对象 (images, app, ocr 等) 时支持显示详细信息",
"android.graphics.Paint#setColor 支持正常解析 ColorInt/ColorHex/ColorName 等颜色参数",
diff --git a/.readme/common.json b/.readme/common.json
index 2bb041fb..4909240f 100644
--- a/.readme/common.json
+++ b/.readme/common.json
@@ -10,7 +10,7 @@
"android_studio_latest_recommended_file_name_of_tar": "android-studio-2025.2.2.8-linux.tar.gz",
"android_studio_latest_recommended_download_address_of_tar": "https://redirector.gvt1.com/edgedl/android/studio/ide-zips/2025.2.2.8/android-studio-2025.2.2.8-linux.tar.gz",
"android_studio_latest_recommended_file_size_of_tar": "1.38 GiB",
- "var_date_contribution_table_data_updated": "2025/09/06",
+ "var_date_contribution_table_data_updated": "2025/12/26",
"latest_rhino_engine_name_with_github_lineno_address": "[v2.0.0-SNAPSHOT](http://rhino.autojs6.com/blob/master/gradle.properties#L3)",
"var_date_rhino_engine_latest_committed": "2025/12/25",
"var_date_jdk_max_supported": "%CURRENT_DATE%",
diff --git a/.readme/template_readme.md b/.readme/template_readme.md
index d0b7b6d2..9d798268 100644
--- a/.readme/template_readme.md
+++ b/.readme/template_readme.md
@@ -382,6 +382,7 @@ autojs6-v6.6.2-arm64-v8a-0f2a9d74.apk
| {{ table_header_contribution_contributors }} | {{ table_header_contribution_number_of_commits }} | {{ table_header_contribution_recent_submissions }} |
|:------:|:------:|:------:|
+| [LYS86](https://github.com/LYS86) `(Lin)` | [4](https://github.com/SuperMonster003/AutoJs6/pulls?q=is%3Apr+is%3Amerged+author%3ALYS86) | `2025/12/26` |
| [wirsnow](https://github.com/wirsnow) | [1](https://github.com/SuperMonster003/AutoJs6/pulls?q=is%3Apr+is%3Amerged+author%3Awirsnow) | `2025/05/19` |
| [TonyJiangWJ](https://github.com/TonyJiangWJ) | [4](https://github.com/SuperMonster003/AutoJs6/pulls?q=is%3Apr+is%3Amerged+author%3ATonyJiangWJ) | `2025/04/24` |
| [luckyloogn](https://github.com/luckyloogn) | [3](https://github.com/SuperMonster003/AutoJs6/pulls?q=is%3Apr+is%3Amerged+author%3Aluckyloogn) | `2024/12/31` |
@@ -390,8 +391,6 @@ autojs6-v6.6.2-arm64-v8a-0f2a9d74.apk
| [LZX284](https://github.com/LZX284) `(AI)` | [7](https://github.com/SuperMonster003/AutoJs6/pulls?q=is%3Apr+is%3Amerged+author%3ALZX284) | `2023/11/15` |
| [little‑alei](https://github.com/little-alei) `(抠脚本人)` | [12](https://github.com/SuperMonster003/AutoJs6/pulls?q=is%3Apr+is%3Amerged+author%3Alittle-alei) | `2023/07/12` |
| [aiselp](https://github.com/aiselp) | [6](https://github.com/SuperMonster003/AutoJs6/pulls?q=is%3Apr+is%3Amerged+author%3Aaiselp) | `2023/06/14` |
-| [LYS86](https://github.com/LYS86) `(Lin)` | [2](https://github.com/SuperMonster003/AutoJs6/pulls?q=is%3Apr+is%3Amerged+author%3ALYS86) | `2023/06/03` |
-
{{ p_contribution_table_data_updated_on }}.
{{ p_contribution_table_data_entries_sorted_by_recent_submissions }}.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 29bba513..47aa94e8 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -251,7 +251,7 @@ dependencies /* Unclassified */ {
implementation(libs.icu4j)
// R8
- implementation("com.android.tools:r8:8.13.17")
+ implementation(libs.r8)
// Plugin API: Paddle OCR
implementation(project(":plugin-api:paddle-ocr"))
diff --git a/app/src/main/java/org/autojs/autojs/rhino/AndroidClassLoader.kt b/app/src/main/java/org/autojs/autojs/rhino/AndroidClassLoader.kt
index 7926f4d0..0d4cc698 100644
--- a/app/src/main/java/org/autojs/autojs/rhino/AndroidClassLoader.kt
+++ b/app/src/main/java/org/autojs/autojs/rhino/AndroidClassLoader.kt
@@ -20,12 +20,15 @@ import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
+import java.util.concurrent.ConcurrentHashMap
+import kotlin.random.Random
import com.legacy.android.dx.command.dexer.Main as LegacyMain
/**
* Created by Stardust on Apr 5, 2017.
* Modified by SuperMonster003 as of Jul 5, 2023.
* Transformed by SuperMonster003 on Jul 5, 2023.
+ * Modified by LYS86 (https://github.com/LYS86) as of Dec 26, 2025.
*/
/**
* Create a new instance with the given parent classloader and cache directory
@@ -35,7 +38,11 @@ import com.legacy.android.dx.command.dexer.Main as LegacyMain
*/
class AndroidClassLoader(private val parent: ClassLoader, private val cacheDir: File) : ClassLoader(), GeneratedClassLoader {
- private val mDexClassLoaders = HashMap()
+ private val mDexClassLoaders = ConcurrentHashMap()
+
+ // Ensure per-jar conversion is thread-safe.
+ // zh-CN: 确保对同一个 jar 的转换过程线程安全.
+ private val conversionLocks = ConcurrentHashMap()
init {
if (cacheDir.exists()) {
@@ -88,31 +95,40 @@ class AndroidClassLoader(private val parent: ClassLoader, private val cacheDir:
}
}
- fun loadJar(file: File): DexClassLoader {
- try {
- val dexFile = jarToDex(file, cacheDir)
- return loadDex(dexFile)
- } catch (e: Exception) {
- Log.e(TAG, "loadJar: failed to load jar ${file.path}", e)
- return loadJar1(file)
+ @Throws(IOException::class, FileNotFoundException::class)
+ fun loadJar(jar: File): DexClassLoader {
+ Log.d(TAG, "loadJar: jar = ${jar.path}")
+ if (!jar.exists() || !jar.canRead()) {
+ throw FileNotFoundException(str(R.string.file_not_exist_or_readable, jar.path))
+ }
+
+ val key = generateArtifactsCacheName(jar)
+ val lock = conversionLocks.getOrPut(key) { Any() }
+
+ return synchronized(lock) {
+ runCatching {
+ jarToDexDx(jar)
+ }.recoverCatching { eDx ->
+ Log.e(TAG, "Failed: jarToDexDx", eDx)
+ jarToDexR8(jar)
+ }.getOrElse { eR8 ->
+ Log.e(TAG, "Failed: jarToDexR8", eR8)
+ throw eR8
+ }
}
}
- @Throws(IOException::class)
- fun loadJar1(file: File): DexClassLoader {
- val path = file.path
- Log.d(TAG, "loadJar: jar = $path")
- if (!file.exists() || !file.canRead()) {
- throw FileNotFoundException(str(R.string.file_not_exist_or_readable, path))
- }
- val dexFile = File(cacheDir, generateDexFileName(file))
- if (dexFile.exists()) {
- return loadDex(dexFile)
+ @Throws(IOException::class, FileNotFoundException::class)
+ private fun jarToDexDx(jar: File): DexClassLoader {
+ val cacheName = generateArtifactsCacheName(jar)
+ val cacheFile = File(cacheDir, "$cacheName.jar")
+ if (cacheFile.exists()) {
+ return loadDex(cacheFile)
}
try {
- val classFile = generateTempFile(path, false)
+ val classFile = generateTempFile(jar.path, false)
val zipFile = ZipFile(classFile)
- val jarFile = ZipFile(file)
+ val jarFile = ZipFile(jar)
for (header in jarFile.fileHeaders) {
if (!header.isDirectory) {
zipFile.addStream(jarFile.getInputStream(header), ZipParameters().apply {
@@ -120,7 +136,7 @@ class AndroidClassLoader(private val parent: ClassLoader, private val cacheDir:
})
}
}
- val classLoader = dexJar(classFile, dexFile)
+ val classLoader = dexJar(classFile, cacheFile)
if (!classFile.delete()) {
Log.e(TAG, "classFile.delete() failed")
}
@@ -176,9 +192,13 @@ class AndroidClassLoader(private val parent: ClassLoader, private val cacheDir:
} catch (e: ClassNotFoundException) {
throw FatalLoadingException(e)
} finally {
- if (classFile != null) {
- if (!classFile.delete()) {
- Log.e(TAG, "classFile.delete() failed")
+ when {
+ // Treat as a temporary file that has been generated.
+ // zh-CN: 视为临时文件已生成.
+ classFile != null -> {
+ if (!classFile.delete()) {
+ Log.e(TAG, "classFile.delete() failed")
+ }
}
}
}
@@ -202,9 +222,13 @@ class AndroidClassLoader(private val parent: ClassLoader, private val cacheDir:
}
}
val loader = loadDex(niceDexFile)
- if (dexFile == null) /* is temporary file generated */ {
- if (!niceDexFile.delete()) {
- Log.e(TAG, "dexFile.delete() failed")
+ when (dexFile) {
+ // Treat as a temporary file that has been generated.
+ // zh-CN: 视为临时文件已生成.
+ null -> {
+ if (!niceDexFile.delete()) {
+ Log.e(TAG, "dexFile.delete() failed")
+ }
}
}
return loader
@@ -227,39 +251,74 @@ class AndroidClassLoader(private val parent: ClassLoader, private val cacheDir:
return file
}
- private fun generateDexFileName(jar: File) = md5("${jar.path}_${jar.lastModified()}")
+ private fun generateArtifactsCacheName(jar: File) = md5("${jar.path}_${jar.lastModified()}")
- private fun jarToDex(jar: File, cacheDir: File): File {
- val dexName = generateDexFileName(jar)
- val dexFile = File(cacheDir, "$dexName.dex")
+ @Throws(IOException::class, FileNotFoundException::class)
+ private fun jarToDexR8(jar: File): DexClassLoader {
+ val cacheName = generateArtifactsCacheName(jar)
+
+ val dexFile = File(cacheDir, "$cacheName.dex")
+ val dexJarFile = File(cacheDir, "$cacheName.jar")
+
+ if (dexJarFile.exists()) {
+ return loadDex(dexJarFile)
+ }
if (dexFile.exists()) {
- return dexFile
+ return loadDex(dexFile)
}
- val tmpOut = File(cacheDir, "${dexName}_tmp").apply { mkdirs() }
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val cmd = D8Command.builder()
- .addProgramFiles(jar.toPath())
- .setOutput(tmpOut.toPath(), OutputMode.DexIndexed)
- .setMode(CompilationMode.RELEASE)
- .build()
- D8.run(cmd)
- } else {
- val args = arrayOf(
- "--output", tmpOut.absolutePath,
- "--release",
- jar.absolutePath
- )
- D8.main(args)
+
+ val tmpOut = File(cacheDir, "${cacheName}_tmp_${System.currentTimeMillis()}_${Random.nextInt(1000, 9999)}")
+ if (!tmpOut.mkdirs()) {
+ throw IOException("Failed to create temp output directory: ${tmpOut.path}")
}
- val classesDex = File(tmpOut, "classes.dex")
- if (classesDex.exists().not()) {
- throw IOException("R8 did not produce classes.dex")
+
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val cmd = D8Command.builder()
+ .addProgramFiles(jar.toPath())
+ .setOutput(tmpOut.toPath(), OutputMode.DexIndexed)
+ .setMode(CompilationMode.RELEASE)
+ .build()
+ D8.run(cmd)
+ } else {
+ val args = arrayOf(
+ "--output", tmpOut.absolutePath,
+ "--release",
+ jar.absolutePath
+ )
+ D8.main(args)
+ }
+
+ val dexFiles = tmpOut.listFiles { _, name ->
+ Regex("classes\\d*\\.dex").matches(name)
+ }?.sortedBy { it.name } ?: emptyList()
+
+ if (dexFiles.isEmpty()) {
+ throw IOException("D8 did not produce any classes*.dex")
+ }
+
+ return if (dexFiles.size == 1) {
+ val classesDex = dexFiles.first()
+ if (!classesDex.renameTo(dexFile)) {
+ dexFile.outputStream().use { out ->
+ classesDex.inputStream().use { it.copyTo(out) }
+ }
+ }
+ loadDex(dexFile)
+ } else {
+ // Multi-dex output: pack all classes*.dex into a single jar for DexClassLoader.
+ // zh-CN: 多 dex 输出: 将所有 classes*.dex 打包到单个 jar 文件中供 DexClassLoader 使用.
+ val zip = ZipFile(dexJarFile)
+ dexFiles.forEach { f ->
+ zip.addStream(f.inputStream(), ZipParameters().apply {
+ fileNameInZip = f.name
+ })
+ }
+ loadDex(dexJarFile)
+ }
+ } finally {
+ tmpOut.deleteRecursively()
}
- if (classesDex.renameTo(dexFile).not()) {
- dexFile.outputStream().use { out -> classesDex.inputStream().use { it.copyTo(out) } }
- }
- tmpOut.deleteRecursively()
- return dexFile
}
companion object {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c242c42d..0018f762 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -34,7 +34,6 @@ log4j = "1.2.17"
#purpose : Compatibility of java.time.* for Android API Level < 26 (Android 8.0) [O]
desugar = "2.1.5"
-analytics = "14.4.0"
android-device-names = "2.1.1"
android-logging-log4j = "1.0.3"
android-support-v7 = "28.0.0"
@@ -82,6 +81,7 @@ paho-client-mqttv3 = "1.2.5"
pinyin4j = "2.5.1"
preference-ktx = "1.2.1"
prov = "1.58.0.0"
+r8 = "8.13.17"
recyclerview = "1.4.0"
retrofit = "3.0.0"
retrofit2-kotlin-coroutines-adapter = "0.9.2"
@@ -160,6 +160,7 @@ paho-client-mqttv3 = { module = "org.eclipse.paho:org.eclipse.paho.client.mqttv3
pinyin4j = { module = "com.belerweb:pinyin4j", version.ref = "pinyin4j" }
preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preference-ktx" }
prov = { module = "com.madgag.spongycastle:prov", version.ref = "prov" }
+r8 = { module = "com.android.tools:r8", version.ref = "r8" }
recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
recyclerview-v7 = { module = "com.android.support:recyclerview-v7", version.ref = "android-support-v7" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
diff --git a/version.properties b/version.properties
index b87babb3..2c1aca8c 100644
--- a/version.properties
+++ b/version.properties
@@ -1,5 +1,5 @@
-#Thu Dec 25 16:35:57 CST 2025
-BUILD_TIME=1766651757534
+#Fri Dec 26 16:11:42 CST 2025
+BUILD_TIME=1766736702142
COMPILE_SDK_VERSION=36
IMAGE_QUANT_CMAKE_VERSION=3.22.1
IMAGE_QUANT_NDK_VERSION=26.1.10909125
@@ -27,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
TARGET_SDK_VERSION=36
TARGET_SDK_VERSION_INRT=29
-VERSION_BUILD=3548
+VERSION_BUILD=3551
VERSION_NAME=6.7.0 Alpha13
VSCODE_EXT_REQUIRED_VERSION=1.0.8