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