mirror of
https://github.com/HighCapable/YukiHookAPI.git
synced 2025-09-07 03:05:36 +08:00
Added member cache function
This commit is contained in:
@@ -54,6 +54,10 @@ class HookEntry : YukiHookXposedInitProxy {
|
|||||||
// 若无和模块频繁交互数据在宿主重新启动之前建议开启
|
// 若无和模块频繁交互数据在宿主重新启动之前建议开启
|
||||||
// 若需要实时交互数据建议关闭或从 [YukiHookModulePrefs] 中进行动态配置
|
// 若需要实时交互数据建议关闭或从 [YukiHookModulePrefs] 中进行动态配置
|
||||||
isEnableModulePrefsCache = true
|
isEnableModulePrefsCache = true
|
||||||
|
// 是否启用 [Member] 缓存功能
|
||||||
|
// 为防止 [Member] 复用过高造成的系统 GC 问题 - 此功能默认启用
|
||||||
|
// 除非缓存的 [Member] 发生了混淆的问题 - 否则建议启用
|
||||||
|
isEnableMemberCache = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -35,14 +35,22 @@ import com.highcapable.yukihookapi.YukiHookAPI.configs
|
|||||||
import com.highcapable.yukihookapi.YukiHookAPI.encase
|
import com.highcapable.yukihookapi.YukiHookAPI.encase
|
||||||
import com.highcapable.yukihookapi.annotation.DoNotUseField
|
import com.highcapable.yukihookapi.annotation.DoNotUseField
|
||||||
import com.highcapable.yukihookapi.annotation.DoNotUseMethod
|
import com.highcapable.yukihookapi.annotation.DoNotUseMethod
|
||||||
|
import com.highcapable.yukihookapi.hook.core.finder.ConstructorFinder
|
||||||
|
import com.highcapable.yukihookapi.hook.core.finder.FieldFinder
|
||||||
|
import com.highcapable.yukihookapi.hook.core.finder.MethodFinder
|
||||||
import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker
|
import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker
|
||||||
import com.highcapable.yukihookapi.hook.factory.processName
|
import com.highcapable.yukihookapi.hook.factory.processName
|
||||||
import com.highcapable.yukihookapi.hook.log.*
|
import com.highcapable.yukihookapi.hook.log.*
|
||||||
import com.highcapable.yukihookapi.hook.param.PackageParam
|
import com.highcapable.yukihookapi.hook.param.PackageParam
|
||||||
import com.highcapable.yukihookapi.hook.param.wrapper.PackageParamWrapper
|
import com.highcapable.yukihookapi.hook.param.wrapper.PackageParamWrapper
|
||||||
|
import com.highcapable.yukihookapi.hook.store.MemberCacheStore
|
||||||
import com.highcapable.yukihookapi.hook.xposed.prefs.YukiHookModulePrefs
|
import com.highcapable.yukihookapi.hook.xposed.prefs.YukiHookModulePrefs
|
||||||
import de.robv.android.xposed.XposedBridge
|
import de.robv.android.xposed.XposedBridge
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
|
import java.lang.reflect.Constructor
|
||||||
|
import java.lang.reflect.Field
|
||||||
|
import java.lang.reflect.Member
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [YukiHookAPI] 的装载调用类
|
* [YukiHookAPI] 的装载调用类
|
||||||
@@ -155,11 +163,11 @@ object YukiHookAPI {
|
|||||||
*
|
*
|
||||||
* - ❗关闭后将会停用 [YukiHookAPI] 对全部日志的输出
|
* - ❗关闭后将会停用 [YukiHookAPI] 对全部日志的输出
|
||||||
*
|
*
|
||||||
* 但是不影响当你手动调用下面这些方法输出日志
|
* 但是不影响当你手动调用下面这些方法输出日志
|
||||||
*
|
*
|
||||||
* [loggerD]、[loggerI]、[loggerW]、[loggerE]
|
* [loggerD]、[loggerI]、[loggerW]、[loggerE]
|
||||||
*
|
*
|
||||||
* 当 [isAllowPrintingLogs] 关闭后 [isDebug] 也将同时关闭
|
* 当 [isAllowPrintingLogs] 关闭后 [isDebug] 也将同时关闭
|
||||||
*/
|
*/
|
||||||
var isAllowPrintingLogs = true
|
var isAllowPrintingLogs = true
|
||||||
|
|
||||||
@@ -168,10 +176,27 @@ object YukiHookAPI {
|
|||||||
*
|
*
|
||||||
* - 为防止内存复用过高问题 - 此功能默认启用
|
* - 为防止内存复用过高问题 - 此功能默认启用
|
||||||
*
|
*
|
||||||
* 你可以手动在 [YukiHookModulePrefs] 中自由开启和关闭缓存功能以及清除缓存
|
* 你可以手动在 [YukiHookModulePrefs] 中自由开启和关闭缓存功能以及清除缓存
|
||||||
*/
|
*/
|
||||||
var isEnableModulePrefsCache = true
|
var isEnableModulePrefsCache = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用 [Member] 缓存功能
|
||||||
|
*
|
||||||
|
* - 为防止 [Member] 复用过高造成的系统 GC 问题 - 此功能默认启用
|
||||||
|
*
|
||||||
|
* 启用后会缓存已经找到的 [Class]、[Method]、[Constructor]、[Field]
|
||||||
|
*
|
||||||
|
* 缓存的 [Member] 都将处于 [MemberCacheStore] 的全局静态实例中
|
||||||
|
*
|
||||||
|
* 推荐使用 [MethodFinder]、[ConstructorFinder]、[FieldFinder] 来获取 [Member]
|
||||||
|
*
|
||||||
|
* 详情请参考 [API 文档](https://github.com/fankes/YukiHookAPI/wiki/API-%E6%96%87%E6%A1%A3)
|
||||||
|
*
|
||||||
|
* 除非缓存的 [Member] 发生了混淆的问题 - 例如使用 R8 混淆后的 APP 的目标 [Member] - 否则建议启用
|
||||||
|
*/
|
||||||
|
var isEnableMemberCache = true
|
||||||
|
|
||||||
/** 结束方法体 */
|
/** 结束方法体 */
|
||||||
internal fun build() {}
|
internal fun build() {}
|
||||||
}
|
}
|
||||||
|
@@ -34,6 +34,7 @@ import com.highcapable.yukihookapi.hook.core.finder.ConstructorFinder
|
|||||||
import com.highcapable.yukihookapi.hook.core.finder.FieldFinder
|
import com.highcapable.yukihookapi.hook.core.finder.FieldFinder
|
||||||
import com.highcapable.yukihookapi.hook.core.finder.MethodFinder
|
import com.highcapable.yukihookapi.hook.core.finder.MethodFinder
|
||||||
import com.highcapable.yukihookapi.hook.core.finder.type.ModifierRules
|
import com.highcapable.yukihookapi.hook.core.finder.type.ModifierRules
|
||||||
|
import com.highcapable.yukihookapi.hook.store.MemberCacheStore
|
||||||
import java.lang.reflect.Member
|
import java.lang.reflect.Member
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,9 +64,13 @@ val String.hasClass get() = hasClass(loader = null)
|
|||||||
* @return [Class]
|
* @return [Class]
|
||||||
* @throws NoClassDefFoundError 如果找不到 [Class] 或设置了错误的 [ClassLoader]
|
* @throws NoClassDefFoundError 如果找不到 [Class] 或设置了错误的 [ClassLoader]
|
||||||
*/
|
*/
|
||||||
fun classOf(name: String, loader: ClassLoader? = null): Class<*> =
|
fun classOf(name: String, loader: ClassLoader? = null): Class<*> {
|
||||||
if (loader == null) Class.forName(name)
|
val hashCode = ("[$name][$loader]").hashCode()
|
||||||
else loader.loadClass(name)
|
return MemberCacheStore.findClass(hashCode) ?: run {
|
||||||
|
(if (loader == null) Class.forName(name)
|
||||||
|
else loader.loadClass(name)).also { MemberCacheStore.putClass(hashCode, it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过字符串查找类是否存在
|
* 通过字符串查找类是否存在
|
||||||
|
@@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* YukiHookAPI - An efficient Kotlin version of the Xposed Hook API.
|
||||||
|
* Copyright (C) 2019-2022 HighCapable
|
||||||
|
* https://github.com/fankes/YukiHookAPI
|
||||||
|
*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
* This file is Created by fankes on 2022/3/29.
|
||||||
|
*/
|
||||||
|
package com.highcapable.yukihookapi.hook.store
|
||||||
|
|
||||||
|
import com.highcapable.yukihookapi.YukiHookAPI
|
||||||
|
import java.lang.reflect.Constructor
|
||||||
|
import java.lang.reflect.Field
|
||||||
|
import java.lang.reflect.Member
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 这是一个全局静态的 [Member] 缓存实例
|
||||||
|
*
|
||||||
|
* 为防止 [Member] 复用过高造成的系统 GC 问题
|
||||||
|
*
|
||||||
|
* 查询后的 [Member] 在 [YukiHookAPI.Configs.isEnableMemberCache] 启用后自动进入缓存
|
||||||
|
*/
|
||||||
|
internal object MemberCacheStore {
|
||||||
|
|
||||||
|
/** 缓存的 [Class] */
|
||||||
|
private val classCacheDatas = HashMap<Int, Class<*>?>()
|
||||||
|
|
||||||
|
/** 缓存的 [Method] */
|
||||||
|
private val methodCacheDatas = HashMap<Int, Method?>()
|
||||||
|
|
||||||
|
/** 缓存的 [Constructor] */
|
||||||
|
private val constructorCacheDatas = HashMap<Int, Constructor<*>?>()
|
||||||
|
|
||||||
|
/** 缓存的 [Field] */
|
||||||
|
private val fieldCacheDatas = HashMap<Int, Field?>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找缓存中的 [Class]
|
||||||
|
* @param hashCode 标识符
|
||||||
|
* @return [Class] or null
|
||||||
|
*/
|
||||||
|
internal fun findClass(hashCode: Int) = classCacheDatas[hashCode]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找缓存中的 [Method]
|
||||||
|
* @param hashCode 标识符
|
||||||
|
* @return [Method] or null
|
||||||
|
*/
|
||||||
|
internal fun findMethod(hashCode: Int) = methodCacheDatas[hashCode]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找缓存中的 [Constructor]
|
||||||
|
* @param hashCode 标识符
|
||||||
|
* @return [Constructor] or null
|
||||||
|
*/
|
||||||
|
internal fun findConstructor(hashCode: Int) = constructorCacheDatas[hashCode]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找缓存中的 [Field]
|
||||||
|
* @param hashCode 标识符
|
||||||
|
* @return [Field] or null
|
||||||
|
*/
|
||||||
|
internal fun findField(hashCode: Int) = fieldCacheDatas[hashCode]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入 [Class] 到缓存
|
||||||
|
* @param hashCode 标识符
|
||||||
|
* @param instance 实例
|
||||||
|
*/
|
||||||
|
internal fun putClass(hashCode: Int, instance: Class<*>?) {
|
||||||
|
if (YukiHookAPI.Configs.isEnableMemberCache.not()) return
|
||||||
|
classCacheDatas[hashCode] = instance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入 [Method] 到缓存
|
||||||
|
* @param hashCode 标识符
|
||||||
|
* @param instance 实例
|
||||||
|
*/
|
||||||
|
internal fun putMethod(hashCode: Int, instance: Method?) {
|
||||||
|
if (YukiHookAPI.Configs.isEnableMemberCache.not()) return
|
||||||
|
methodCacheDatas[hashCode] = instance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入 [Constructor] 到缓存
|
||||||
|
* @param hashCode 标识符
|
||||||
|
* @param instance 实例
|
||||||
|
*/
|
||||||
|
internal fun putConstructor(hashCode: Int, instance: Constructor<*>?) {
|
||||||
|
if (YukiHookAPI.Configs.isEnableMemberCache.not()) return
|
||||||
|
constructorCacheDatas[hashCode] = instance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入 [Field] 到缓存
|
||||||
|
* @param hashCode 标识符
|
||||||
|
* @param instance 实例
|
||||||
|
*/
|
||||||
|
internal fun putField(hashCode: Int, instance: Field?) {
|
||||||
|
if (YukiHookAPI.Configs.isEnableMemberCache.not()) return
|
||||||
|
fieldCacheDatas[hashCode] = instance
|
||||||
|
}
|
||||||
|
}
|
@@ -28,6 +28,7 @@
|
|||||||
package com.highcapable.yukihookapi.hook.utils
|
package com.highcapable.yukihookapi.hook.utils
|
||||||
|
|
||||||
import com.highcapable.yukihookapi.hook.core.finder.type.ModifierRules
|
import com.highcapable.yukihookapi.hook.core.finder.type.ModifierRules
|
||||||
|
import com.highcapable.yukihookapi.hook.store.MemberCacheStore
|
||||||
import java.lang.reflect.Constructor
|
import java.lang.reflect.Constructor
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.lang.reflect.Member
|
import java.lang.reflect.Member
|
||||||
@@ -52,26 +53,30 @@ internal object ReflectionTool {
|
|||||||
* @throws NoSuchFieldError 如果找不到变量
|
* @throws NoSuchFieldError 如果找不到变量
|
||||||
*/
|
*/
|
||||||
internal fun findField(classSet: Class<*>?, name: String, modifiers: ModifierRules?, type: Class<*>?): Field {
|
internal fun findField(classSet: Class<*>?, name: String, modifiers: ModifierRules?, type: Class<*>?): Field {
|
||||||
var field: Field? = null
|
val hashCode = ("[$name][$type][$modifiers][$classSet]").hashCode()
|
||||||
run {
|
return MemberCacheStore.findField(hashCode) ?: let {
|
||||||
classSet?.declaredFields?.forEach {
|
var field: Field? = null
|
||||||
var conditions = name == it.name
|
run {
|
||||||
if (type != null) conditions = conditions && it.type == type
|
classSet?.declaredFields?.forEach {
|
||||||
if (modifiers != null) conditions = conditions && modifiers.contains(it)
|
var conditions = name == it.name
|
||||||
if (conditions) {
|
if (type != null) conditions = conditions && it.type == type
|
||||||
field = it.apply { isAccessible = true }
|
if (modifiers != null) conditions = conditions && modifiers.contains(it)
|
||||||
return@run
|
if (conditions) {
|
||||||
}
|
field = it.apply { isAccessible = true }
|
||||||
} ?: error("Can't find this Field [$name] because classSet is null")
|
return@run
|
||||||
|
}
|
||||||
|
} ?: error("Can't find this Field [$name] because classSet is null")
|
||||||
|
}
|
||||||
|
field?.also { MemberCacheStore.putField(hashCode, field) }
|
||||||
|
?: throw NoSuchFieldError(
|
||||||
|
"Can't find this Field --> " +
|
||||||
|
"name:[$name] " +
|
||||||
|
"type:[$type] " +
|
||||||
|
"modifiers:${modifiers ?: "[]"} " +
|
||||||
|
"in Class [$classSet] " +
|
||||||
|
"by $TAG"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return field ?: throw NoSuchFieldError(
|
|
||||||
"Can't find this Field --> " +
|
|
||||||
"name:[$name] " +
|
|
||||||
"type:[$type] " +
|
|
||||||
"modifiers:${modifiers ?: "[]"} " +
|
|
||||||
"in Class [$classSet] " +
|
|
||||||
"by $TAG"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,30 +99,34 @@ internal object ReflectionTool {
|
|||||||
paramCount: Int,
|
paramCount: Int,
|
||||||
paramTypes: Array<out Class<*>>?
|
paramTypes: Array<out Class<*>>?
|
||||||
): Method {
|
): Method {
|
||||||
var method: Method? = null
|
val hashCode = ("[$name][$paramCount][${paramTypes.typeOfString()}][$returnType][$modifiers][$classSet]").hashCode()
|
||||||
run {
|
return MemberCacheStore.findMethod(hashCode) ?: let {
|
||||||
classSet?.declaredMethods?.forEach {
|
var method: Method? = null
|
||||||
var conditions = name == it.name
|
run {
|
||||||
if (returnType != null) conditions = conditions && it.returnType == returnType
|
classSet?.declaredMethods?.forEach {
|
||||||
if (paramCount >= 0) conditions = conditions && it.parameterTypes.size == paramCount
|
var conditions = name == it.name
|
||||||
if (paramTypes != null) conditions = conditions && arrayContentsEq(paramTypes, it.parameterTypes)
|
if (returnType != null) conditions = conditions && it.returnType == returnType
|
||||||
if (modifiers != null) conditions = conditions && modifiers.contains(it)
|
if (paramCount >= 0) conditions = conditions && it.parameterTypes.size == paramCount
|
||||||
if (conditions) {
|
if (paramTypes != null) conditions = conditions && arrayContentsEq(paramTypes, it.parameterTypes)
|
||||||
method = it.apply { isAccessible = true }
|
if (modifiers != null) conditions = conditions && modifiers.contains(it)
|
||||||
return@run
|
if (conditions) {
|
||||||
}
|
method = it.apply { isAccessible = true }
|
||||||
} ?: error("Can't find this Method [$name] because classSet is null")
|
return@run
|
||||||
|
}
|
||||||
|
} ?: error("Can't find this Method [$name] because classSet is null")
|
||||||
|
}
|
||||||
|
method?.also { MemberCacheStore.putMethod(hashCode, method) }
|
||||||
|
?: throw NoSuchMethodError(
|
||||||
|
"Can't find this Method --> " +
|
||||||
|
"name:[$name] " +
|
||||||
|
"paramCount:[${paramCount.takeIf { it >= 0 } ?: "unspecified"}] " +
|
||||||
|
"paramTypes:[${paramTypes.typeOfString()}] " +
|
||||||
|
"returnType:[$returnType] " +
|
||||||
|
"modifiers:${modifiers ?: "[]"} " +
|
||||||
|
"in Class [$classSet] " +
|
||||||
|
"by $TAG"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return method ?: throw NoSuchMethodError(
|
|
||||||
"Can't find this Method --> " +
|
|
||||||
"name:[$name] " +
|
|
||||||
"paramCount:[${paramCount.takeIf { it >= 0 } ?: "unspecified"}] " +
|
|
||||||
"paramTypes:[${paramTypes.typeOfString()}] " +
|
|
||||||
"returnType:[$returnType] " +
|
|
||||||
"modifiers:${modifiers ?: "[]"} " +
|
|
||||||
"in Class [$classSet] " +
|
|
||||||
"by $TAG"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -136,27 +145,31 @@ internal object ReflectionTool {
|
|||||||
paramCount: Int,
|
paramCount: Int,
|
||||||
paramTypes: Array<out Class<*>>?
|
paramTypes: Array<out Class<*>>?
|
||||||
): Constructor<*> {
|
): Constructor<*> {
|
||||||
var constructor: Constructor<*>? = null
|
val hashCode = ("[$paramCount][${paramTypes.typeOfString()}][$modifiers][$classSet]").hashCode()
|
||||||
run {
|
return MemberCacheStore.findConstructor(hashCode) ?: let {
|
||||||
classSet?.declaredConstructors?.forEach {
|
var constructor: Constructor<*>? = null
|
||||||
var conditions = false
|
run {
|
||||||
if (paramCount >= 0) conditions = it.parameterTypes.size == paramCount
|
classSet?.declaredConstructors?.forEach {
|
||||||
if (paramTypes != null) conditions = arrayContentsEq(paramTypes, it.parameterTypes)
|
var conditions = false
|
||||||
if (modifiers != null) conditions = conditions && modifiers.contains(it)
|
if (paramCount >= 0) conditions = it.parameterTypes.size == paramCount
|
||||||
if (conditions) {
|
if (paramTypes != null) conditions = arrayContentsEq(paramTypes, it.parameterTypes)
|
||||||
constructor = it.apply { isAccessible = true }
|
if (modifiers != null) conditions = conditions && modifiers.contains(it)
|
||||||
return@run
|
if (conditions) {
|
||||||
}
|
constructor = it.apply { isAccessible = true }
|
||||||
} ?: error("Can't find this Constructor because classSet is null")
|
return@run
|
||||||
|
}
|
||||||
|
} ?: error("Can't find this Constructor because classSet is null")
|
||||||
|
}
|
||||||
|
return constructor?.also { MemberCacheStore.putConstructor(hashCode, constructor) }
|
||||||
|
?: throw NoSuchMethodError(
|
||||||
|
"Can't find this Constructor --> " +
|
||||||
|
"paramCount:[${paramCount.takeIf { it >= 0 } ?: "unspecified"}] " +
|
||||||
|
"paramTypes:[${paramTypes.typeOfString()}] " +
|
||||||
|
"modifiers:${modifiers ?: "[]"} " +
|
||||||
|
"in Class [$classSet] " +
|
||||||
|
"by $TAG"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return constructor ?: throw NoSuchMethodError(
|
|
||||||
"Can't find this Constructor --> " +
|
|
||||||
"paramCount:[${paramCount.takeIf { it >= 0 } ?: "unspecified"}] " +
|
|
||||||
"paramTypes:[${paramTypes.typeOfString()}] " +
|
|
||||||
"modifiers:${modifiers ?: "[]"} " +
|
|
||||||
"in Class [$classSet] " +
|
|
||||||
"by $TAG"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Reference in New Issue
Block a user