From 1010edc5614f05237b797be9293a8c07cdd97e00 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 17 Dec 2017 04:43:19 -0800 Subject: [PATCH] Speed up Acl.fromReader It should run 9x faster than before now. Past mistakes: * InetAddress.parseNumericAddress throws TWO exceptions underthehood if the string passed is invalid. This caused a huge overhead; * CharSequence.split in Kotlin is EXTREMELY SLOW. --- .../com/github/shadowsocks/JniHelper.java | 12 ++- core/src/main/jni/Android.mk | 2 + core/src/main/jni/Application.mk | 4 + core/src/main/jni/jni-helper.cpp | 97 +++++++++++++------ .../java/com/github/shadowsocks/acl/Acl.kt | 10 +- .../shadowsocks/acl/CustomRulesFragment.kt | 4 +- .../com/github/shadowsocks/bg/VpnService.kt | 4 +- .../com/github/shadowsocks/utils/Subnet.kt | 18 ++-- .../com/github/shadowsocks/utils/Utils.kt | 19 +--- .../github/shadowsocks/utils/SubnetTest.kt | 19 ++-- 10 files changed, 111 insertions(+), 78 deletions(-) create mode 100644 core/src/main/jni/Application.mk diff --git a/core/src/main/java/com/github/shadowsocks/JniHelper.java b/core/src/main/java/com/github/shadowsocks/JniHelper.java index 2cc41a34..5aa353d7 100755 --- a/core/src/main/java/com/github/shadowsocks/JniHelper.java +++ b/core/src/main/java/com/github/shadowsocks/JniHelper.java @@ -39,15 +39,19 @@ package com.github.shadowsocks; import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.system.ErrnoException; +import java.net.InetAddress; + public class JniHelper { static { System.loadLibrary("jni-helper"); } @Deprecated // Use Process.destroy() since API 24 - public static void sigtermCompat(Process process) throws Exception { + public static void sigtermCompat(@NonNull Process process) throws Exception { if (Build.VERSION.SDK_INT >= 24) throw new UnsupportedOperationException("Never call this method in OpenJDK!"); int errno = sigterm(process); if (errno != 0) throw Build.VERSION.SDK_INT >= 21 @@ -55,7 +59,7 @@ public class JniHelper { } @Deprecated // only implemented for before API 24 - public static boolean waitForCompat(Process process, long millis) throws Exception { + public static boolean waitForCompat(@NonNull Process process, long millis) throws Exception { if (Build.VERSION.SDK_INT >= 24) throw new UnsupportedOperationException("Never call this method in OpenJDK!"); final Object mutex = getExitValueMutex(process); synchronized (mutex) { @@ -68,6 +72,8 @@ public class JniHelper { private static native int sigterm(Process process); private static native Integer getExitValue(Process process); private static native Object getExitValueMutex(Process process); - public static native int sendFd(int fd, String path); + public static native int sendFd(int fd, @NonNull String path); public static native void close(int fd); + @Nullable + public static native InetAddress parseNumericAddress(@NonNull String str); } diff --git a/core/src/main/jni/Android.mk b/core/src/main/jni/Android.mk index f84f593f..2ed0d4aa 100755 --- a/core/src/main/jni/Android.mk +++ b/core/src/main/jni/Android.mk @@ -318,6 +318,8 @@ include $(CLEAR_VARS) LOCAL_MODULE:= jni-helper +LOCAL_CFLAGS := -std=c++11 + LOCAL_C_INCLUDES:= $(LOCAL_PATH)/libancillary LOCAL_SRC_FILES:= jni-helper.cpp diff --git a/core/src/main/jni/Application.mk b/core/src/main/jni/Application.mk new file mode 100644 index 00000000..1935e646 --- /dev/null +++ b/core/src/main/jni/Application.mk @@ -0,0 +1,4 @@ +APP_ABI := armeabi-v7a arm64-v8a x86 +APP_PLATFORM := android-19 +APP_STL := c++_static +NDK_TOOLCHAIN_VERSION := clang diff --git a/core/src/main/jni/jni-helper.cpp b/core/src/main/jni/jni-helper.cpp index 6c56cb52..8e0f6ce4 100644 --- a/core/src/main/jni/jni-helper.cpp +++ b/core/src/main/jni/jni-helper.cpp @@ -4,32 +4,32 @@ #include #include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include #include #include #include +using namespace std; + #define LOGI(...) do { __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__); } while(0) #define LOGW(...) do { __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__); } while(0) #define LOGE(...) do { __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__); } while(0) #define THROW(env, clazz, msg) do { env->ThrowNew(env->FindClass(clazz), msg); } while (0) -static jclass ProcessImpl; +static int sdk_version; +static jclass ProcessImpl, Inet4Address, Inet6Address; static jfieldID ProcessImpl_pid, ProcessImpl_exitValue, ProcessImpl_exitValueMutex; - -static int sdk_version() { - char version[PROP_VALUE_MAX + 1]; - __system_property_get("ro.build.version.sdk", version); - return atoi(version); -} +static jmethodID Inet4Address__init_, Inet6Address__init_; extern "C" { JNIEXPORT jint JNICALL Java_com_github_shadowsocks_JniHelper_sigkill(JNIEnv *env, jobject thiz, jint pid) { @@ -103,6 +103,26 @@ JNIEXPORT jint JNICALL env->ReleaseStringUTFChars(path, sock_str); return 0; } + +JNIEXPORT jobject JNICALL +Java_com_github_shadowsocks_JniHelper_parseNumericAddress(JNIEnv *env, jobject thiz, jstring str) { + const char *src = env->GetStringUTFChars(str, 0); + jbyte dst[max(sizeof(in_addr), sizeof(in6_addr))]; + jobject result = nullptr; + if (inet_pton(AF_INET, src, dst) == 1) { + jbyteArray arr = env->NewByteArray(sizeof(in_addr)); + env->SetByteArrayRegion(arr, 0, sizeof(in_addr), dst); + result = sdk_version < 24 ? env->NewObject(Inet4Address, Inet4Address__init_, arr, str) + : env->NewObject(Inet4Address, Inet4Address__init_, str, arr); + } else if (inet_pton(AF_INET6, src, dst) == 1) { + jbyteArray arr = env->NewByteArray(sizeof(in6_addr)); + env->SetByteArrayRegion(arr, 0, sizeof(in6_addr), dst); + result = sdk_version < 24 ? env->NewObject(Inet6Address, Inet6Address__init_, arr, str, 0) + : env->NewObject(Inet6Address, Inet6Address__init_, str, arr, 0); + } + env->ReleaseStringUTFChars(str, src); + return result; +} } /* @@ -126,24 +146,39 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { } env = uenv.env; - if (sdk_version() < 24) { - if (!(ProcessImpl = env->FindClass("java/lang/ProcessManager$ProcessImpl"))) { - THROW(env, "java/lang/RuntimeException", "ProcessManager$ProcessImpl not found"); - goto bail; - } - ProcessImpl = (jclass) env->NewGlobalRef((jobject) ProcessImpl); - if (!(ProcessImpl_pid = env->GetFieldID(ProcessImpl, "pid", "I"))) { - THROW(env, "java/lang/RuntimeException", "ProcessManager$ProcessImpl.pid not found"); - goto bail; - } - if (!(ProcessImpl_exitValue = env->GetFieldID(ProcessImpl, "exitValue", "Ljava/lang/Integer;"))) { - THROW(env, "java/lang/RuntimeException", "ProcessManager$ProcessImpl.exitValue not found"); - goto bail; - } - if (!(ProcessImpl_exitValueMutex = env->GetFieldID(ProcessImpl, "exitValueMutex", "Ljava/lang/Object;"))) { - THROW(env, "java/lang/RuntimeException", "ProcessManager$ProcessImpl.exitValueMutex not found"); - goto bail; - } + char version[PROP_VALUE_MAX + 1]; + __system_property_get("ro.build.version.sdk", version); + sdk_version = atoi(version); + +#define FIND_CLASS(out, name) \ + if (!(out = env->FindClass(name))) { \ + THROW(env, "java/lang/RuntimeException", name " not found"); \ + goto bail; \ + } \ + out = reinterpret_cast(env->NewGlobalRef(reinterpret_cast(out))) +#define GET_FIELD(out, clazz, name, sig) \ + if (!(out = env->GetFieldID(clazz, name, sig))) { \ + THROW(env, "java/lang/RuntimeException", "Field " #clazz "." name " with type " sig " not found"); \ + goto bail; \ + } +#define GET_METHOD(out, clazz, name, sig) \ + if (!(out = env->GetMethodID(clazz, name, sig))) { \ + THROW(env, "java/lang/RuntimeException", "Method " #clazz "." name sig " not found"); \ + goto bail; \ + } + + FIND_CLASS(Inet4Address, "java/net/Inet4Address"); + FIND_CLASS(Inet6Address, "java/net/Inet6Address"); + if (sdk_version < 24) { + FIND_CLASS(ProcessImpl, "java/lang/ProcessManager$ProcessImpl"); + GET_FIELD(ProcessImpl_pid, ProcessImpl, "pid", "I") + GET_FIELD(ProcessImpl_exitValue, ProcessImpl, "exitValue", "Ljava/lang/Integer;") + GET_FIELD(ProcessImpl_exitValueMutex, ProcessImpl, "exitValueMutex", "Ljava/lang/Object;") + GET_METHOD(Inet4Address__init_, Inet4Address, "", "([BLjava/lang/String;)V") + GET_METHOD(Inet6Address__init_, Inet6Address, "", "([BLjava/lang/String;I)V") + } else { + GET_METHOD(Inet4Address__init_, Inet4Address, "", "(Ljava/lang/String;[B)V") + GET_METHOD(Inet6Address__init_, Inet6Address, "", "(Ljava/lang/String;[BI)V") } result = JNI_VERSION_1_6; diff --git a/mobile/src/main/java/com/github/shadowsocks/acl/Acl.kt b/mobile/src/main/java/com/github/shadowsocks/acl/Acl.kt index d8069143..156723a7 100644 --- a/mobile/src/main/java/com/github/shadowsocks/acl/Acl.kt +++ b/mobile/src/main/java/com/github/shadowsocks/acl/Acl.kt @@ -116,7 +116,8 @@ class Acl { var subnets: SortedList? = if (defaultBypass) proxySubnets else bypassSubnets reader.useLines { for (line in it) { - val blocks = line.split('#', limit = 2) + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + val blocks = (line as java.lang.String).split("#", 2) val url = networkAclParser.matchEntire(blocks.getOrElse(1, {""}))?.groupValues?.getOrNull(1) if (url != null) urls.add(URL(url)) val input = blocks[0].trim() @@ -135,10 +136,9 @@ class Acl { } "[reject_all]", "[bypass_all]" -> bypass = true "[accept_all]", "[proxy_all]" -> bypass = false - else -> if (subnets != null && input.isNotEmpty()) try { - subnets!!.add(Subnet.fromString(input)) - } catch (_: IllegalArgumentException) { - hostnames!!.add(input) + else -> if (subnets != null && input.isNotEmpty()) { + val subnet = Subnet.fromString(input) + if (subnet == null) hostnames!!.add(input) else subnets!!.add(subnet) } } } diff --git a/mobile/src/main/java/com/github/shadowsocks/acl/CustomRulesFragment.kt b/mobile/src/main/java/com/github/shadowsocks/acl/CustomRulesFragment.kt index 9bf86bc1..a57af005 100644 --- a/mobile/src/main/java/com/github/shadowsocks/acl/CustomRulesFragment.kt +++ b/mobile/src/main/java/com/github/shadowsocks/acl/CustomRulesFragment.kt @@ -333,8 +333,8 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) if (savedInstanceState != null) { - selectedItems.addAll(savedInstanceState.getStringArray(SELECTED_SUBNETS)?.map(Subnet.Companion::fromString) - ?: listOf()) + selectedItems.addAll(savedInstanceState.getStringArray(SELECTED_SUBNETS) + ?.mapNotNull(Subnet.Companion::fromString) ?: listOf()) selectedItems.addAll(savedInstanceState.getStringArray(SELECTED_HOSTNAMES) ?: arrayOf()) selectedItems.addAll(savedInstanceState.getStringArray(SELECTED_URLS)?.map { URL(it) } diff --git a/mobile/src/main/java/com/github/shadowsocks/bg/VpnService.kt b/mobile/src/main/java/com/github/shadowsocks/bg/VpnService.kt index 8f2b610c..3e1ef078 100644 --- a/mobile/src/main/java/com/github/shadowsocks/bg/VpnService.kt +++ b/mobile/src/main/java/com/github/shadowsocks/bg/VpnService.kt @@ -159,10 +159,10 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { Acl.ALL, Acl.BYPASS_CHN, Acl.CUSTOM_RULES -> builder.addRoute("0.0.0.0", 0) else -> { resources.getStringArray(R.array.bypass_private_route).forEach { - val subnet = Subnet.fromString(it) + val subnet = Subnet.fromString(it)!! builder.addRoute(subnet.address.hostAddress, subnet.prefixSize) } - profile.remoteDns.split(",").map { it.trim().parseNumericAddress() } + profile.remoteDns.split(",").mapNotNull { it.trim().parseNumericAddress() } .forEach { builder.addRoute(it, it.address.size shl 3) } } } diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/Subnet.kt b/mobile/src/main/java/com/github/shadowsocks/utils/Subnet.kt index e2948430..56468075 100644 --- a/mobile/src/main/java/com/github/shadowsocks/utils/Subnet.kt +++ b/mobile/src/main/java/com/github/shadowsocks/utils/Subnet.kt @@ -25,15 +25,15 @@ import java.util.* class Subnet(val address: InetAddress, val prefixSize: Int) : Comparable { companion object { - @Throws(IllegalArgumentException::class) - fun fromString(value: String): Subnet { - val parts = value.split('/') - val addr = parts[0].parseNumericAddress() - return when (parts.size) { - 1 -> Subnet(addr, addr.address.size shl 3) - 2 -> Subnet(addr, parts[1].toInt()) - else -> throw IllegalArgumentException() - } + fun fromString(value: String): Subnet? { + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + val parts = (value as java.lang.String).split("/", 2) + val addr = parts[0].parseNumericAddress() ?: return null + return if (parts.size == 2) try { + Subnet(addr, parts[1].toInt()) + } catch (_: NumberFormatException) { + null + } else Subnet(addr, addr.address.size shl 3) } } diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/Utils.kt b/mobile/src/main/java/com/github/shadowsocks/utils/Utils.kt index cf0fabee..5aa41fcd 100644 --- a/mobile/src/main/java/com/github/shadowsocks/utils/Utils.kt +++ b/mobile/src/main/java/com/github/shadowsocks/utils/Utils.kt @@ -11,31 +11,20 @@ import android.support.v4.app.FragmentManager import android.support.v7.util.SortedList import android.util.TypedValue import com.github.shadowsocks.App.Companion.app +import com.github.shadowsocks.JniHelper import java.lang.reflect.InvocationTargetException +import java.net.Inet4Address import java.net.InetAddress import java.net.URLConnection -private val isNumericMethod by lazy { InetAddress::class.java.getMethod("isNumeric", String::class.java) } -private val parseNumericAddressMethod by lazy { - try { - InetAddress::class.java.getMethod("parseNumericAddress", String::class.java) - } catch (_: NoSuchMethodException) { - null - } -} private val fieldChildFragmentManager by lazy { val field = Fragment::class.java.getDeclaredField("mChildFragmentManager") field.isAccessible = true field } -fun String.isNumericAddress(): Boolean = isNumericMethod.invoke(null, this) as Boolean -fun String.parseNumericAddress(): InetAddress = - if (parseNumericAddressMethod == null) InetAddress.getByName(this) else try { - parseNumericAddressMethod!!.invoke(null, this) as InetAddress - } catch (exc: InvocationTargetException) { - throw exc.cause ?: exc - } +fun String.isNumericAddress() = parseNumericAddress() != null +fun String.parseNumericAddress() = JniHelper.parseNumericAddress(this) fun parsePort(str: String?, default: Int, min: Int = 1025): Int { val x = str?.toIntOrNull() ?: default diff --git a/mobile/src/test/java/com/github/shadowsocks/utils/SubnetTest.kt b/mobile/src/test/java/com/github/shadowsocks/utils/SubnetTest.kt index 4b2f23be..4c33fd64 100644 --- a/mobile/src/test/java/com/github/shadowsocks/utils/SubnetTest.kt +++ b/mobile/src/test/java/com/github/shadowsocks/utils/SubnetTest.kt @@ -16,16 +16,13 @@ class SubnetTest { Assert.assertNotEquals(Subnet.fromString("1.2.3.4/12"), Subnet.fromString("1.2.3.5/12")) } - @Test(expected = IllegalArgumentException::class) - fun invalidParsing1() { - Subnet.fromString("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78/129") - } - @Test(expected = IllegalArgumentException::class) - fun invalidParsing2() { - Subnet.fromString("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78/-99") - } - @Test(expected = IllegalArgumentException::class) - fun invalidParsing3() { - Subnet.fromString("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78/1/0") + @Test + fun invalidParsings() { + Assert.assertEquals(null, Subnet.fromString("1.2.3.456")) + Assert.assertEquals(null, Subnet.fromString("ffff::f0000")) + Assert.assertEquals(null, Subnet.fromString("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78/129")) + Assert.assertEquals(null, Subnet.fromString("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78/129")) + Assert.assertEquals(null, Subnet.fromString("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78/-99")) + Assert.assertEquals(null, Subnet.fromString("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78/1/0")) } }