Revert "Optimize dexmaker for yahfa"
This reverts commit 1dde1467f89f697de736db0eb0d21e9043113884.
This commit is contained in:
parent
17bf424d93
commit
c84c09b2d5
|
|
@ -41,9 +41,9 @@ public class NormalProxy extends BaseProxy {
|
||||||
mRouter.initResourcesHook();
|
mRouter.initResourcesHook();
|
||||||
mRouter.prepare(isSystem);
|
mRouter.prepare(isSystem);
|
||||||
PrebuiltMethodsDeopter.deoptBootMethods(); // do it once for secondary zygote
|
PrebuiltMethodsDeopter.deoptBootMethods(); // do it once for secondary zygote
|
||||||
|
mRouter.installBootstrapHooks(isSystem);
|
||||||
ConfigManager.appDataDir = appDataDir;
|
ConfigManager.appDataDir = appDataDir;
|
||||||
ConfigManager.niceName = niceName;
|
ConfigManager.niceName = niceName;
|
||||||
mRouter.installBootstrapHooks(isSystem);
|
|
||||||
XposedInit.prefsBasePath = ConfigManager.getPrefsPath("");
|
XposedInit.prefsBasePath = ConfigManager.getPrefsPath("");
|
||||||
mRouter.onEnterChildProcess();
|
mRouter.onEnterChildProcess();
|
||||||
Utils.logI("Loading modules for " + niceName);
|
Utils.logI("Loading modules for " + niceName);
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,6 @@ public class FileUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getDataPathPrefix() {
|
public static String getDataPathPrefix() {
|
||||||
return ConfigManager.getDataPathPrefix() + "/";
|
return ConfigManager.getDataPathPrefix();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -231,6 +231,7 @@ fi
|
||||||
set_perm_recursive /data/misc/$MISC_PATH/framework root root 0755 0644 "u:object_r:magisk_file:s0" || abortC "! ${LANG_CUST_ERR_PERM}"
|
set_perm_recursive /data/misc/$MISC_PATH/framework root root 0755 0644 "u:object_r:magisk_file:s0" || abortC "! ${LANG_CUST_ERR_PERM}"
|
||||||
|
|
||||||
mkdir -p /data/misc/$MISC_PATH/cache
|
mkdir -p /data/misc/$MISC_PATH/cache
|
||||||
|
rm /data/misc/$MISC_PATH/cache/*
|
||||||
set_perm /data/misc/$MISC_PATH/cache root root 0777 "u:object_r:magisk_file:s0" || abortC "! ${LANG_CUST_ERR_PERM}"
|
set_perm /data/misc/$MISC_PATH/cache root root 0777 "u:object_r:magisk_file:s0" || abortC "! ${LANG_CUST_ERR_PERM}"
|
||||||
|
|
||||||
mv "${MODPATH}/system/lib/libriru_edxp.so" "${MODPATH}/system/lib/${LIB_RIRU_EDXP}"
|
mv "${MODPATH}/system/lib/libriru_edxp.so" "${MODPATH}/system/lib/${LIB_RIRU_EDXP}"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
package com.elderdrivers.riru.edxp.yahfa.dexmaker;
|
package com.elderdrivers.riru.edxp.yahfa.dexmaker;
|
||||||
|
|
||||||
|
|
||||||
import com.elderdrivers.riru.edxp.config.ConfigManager;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Member;
|
import java.lang.reflect.Member;
|
||||||
|
|
@ -12,15 +9,13 @@ import java.lang.reflect.Modifier;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import de.robv.android.xposed.EdHooker;
|
|
||||||
import de.robv.android.xposed.XposedBridge;
|
import de.robv.android.xposed.XposedBridge;
|
||||||
|
|
||||||
public final class DynamicBridge {
|
public final class DynamicBridge {
|
||||||
|
|
||||||
private static final HashMap<Member, EdHooker> hookedInfo = new HashMap<>();
|
private static final HashMap<Member, Method> hookedInfo = new HashMap<>();
|
||||||
private static final HookerDexMaker dexMaker = new HookerDexMaker();
|
private static final HookerDexMaker dexMaker = new HookerDexMaker();
|
||||||
private static final AtomicBoolean dexPathInited = new AtomicBoolean(false);
|
private static final AtomicBoolean dexPathInited = new AtomicBoolean(false);
|
||||||
private static File dexOptDir;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset dexPathInited flag once we enter child process
|
* Reset dexPathInited flag once we enter child process
|
||||||
|
|
@ -42,16 +37,13 @@ public final class DynamicBridge {
|
||||||
}
|
}
|
||||||
|
|
||||||
DexLog.d("start to generate class for: " + hookMethod);
|
DexLog.d("start to generate class for: " + hookMethod);
|
||||||
long startTime = System.nanoTime();
|
|
||||||
try {
|
try {
|
||||||
dexMaker.start(hookMethod, additionalHookInfo,
|
dexMaker.start(hookMethod, additionalHookInfo,
|
||||||
hookMethod.getDeclaringClass().getClassLoader());
|
hookMethod.getDeclaringClass().getClassLoader());
|
||||||
hookedInfo.put(hookMethod, dexMaker.getHooker());
|
hookedInfo.put(hookMethod, dexMaker.getCallBackupMethod());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
DexLog.e("error occur when generating dex.", e);
|
DexLog.e("error occur when generating dex.", e);
|
||||||
}
|
}
|
||||||
long endTime = System.nanoTime();
|
|
||||||
DexLog.d("generated class for " + hookMethod + " in " + ((endTime-startTime) * 1.e-6) + "ms");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean checkMember(Member member) {
|
private static boolean checkMember(Member member) {
|
||||||
|
|
@ -74,11 +66,26 @@ public final class DynamicBridge {
|
||||||
|
|
||||||
public static Object invokeOriginalMethod(Member method, Object thisObject, Object[] args)
|
public static Object invokeOriginalMethod(Member method, Object thisObject, Object[] args)
|
||||||
throws InvocationTargetException, IllegalAccessException {
|
throws InvocationTargetException, IllegalAccessException {
|
||||||
EdHooker hooker = hookedInfo.get(method);
|
Method callBackup = hookedInfo.get(method);
|
||||||
if (hooker == null) {
|
if (callBackup == null) {
|
||||||
throw new IllegalStateException("method not hooked, cannot call original method.");
|
throw new IllegalStateException("method not hooked, cannot call original method.");
|
||||||
}
|
}
|
||||||
return hooker.callBackup(thisObject, args);
|
if (!Modifier.isStatic(callBackup.getModifiers())) {
|
||||||
|
throw new IllegalStateException("original method is not static, something must be wrong!");
|
||||||
|
}
|
||||||
|
callBackup.setAccessible(true);
|
||||||
|
if (args == null) {
|
||||||
|
args = new Object[0];
|
||||||
|
}
|
||||||
|
final int argsSize = args.length;
|
||||||
|
if (Modifier.isStatic(method.getModifiers())) {
|
||||||
|
return callBackup.invoke(null, args);
|
||||||
|
} else {
|
||||||
|
Object[] newArgs = new Object[argsSize + 1];
|
||||||
|
newArgs[0] = thisObject;
|
||||||
|
System.arraycopy(args, 0, newArgs, 1, argsSize);
|
||||||
|
return callBackup.invoke(null, newArgs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ package com.elderdrivers.riru.edxp.yahfa.dexmaker;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import com.elderdrivers.riru.edxp.config.ConfigManager;
|
import com.elderdrivers.riru.edxp.config.ConfigManager;
|
||||||
|
import com.elderdrivers.riru.edxp.core.Yahfa;
|
||||||
import com.elderdrivers.riru.edxp.core.yahfa.HookMain;
|
import com.elderdrivers.riru.edxp.core.yahfa.HookMain;
|
||||||
import com.elderdrivers.riru.edxp.util.ProxyClassLoader;
|
import com.elderdrivers.riru.edxp.util.ProxyClassLoader;
|
||||||
import com.elderdrivers.riru.edxp.yahfa.BuildConfig;
|
import com.elderdrivers.riru.edxp.yahfa.BuildConfig;
|
||||||
|
|
@ -19,11 +19,14 @@ import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
import dalvik.system.InMemoryDexClassLoader;
|
import dalvik.system.InMemoryDexClassLoader;
|
||||||
import de.robv.android.xposed.EdHooker;
|
import de.robv.android.xposed.XC_MethodHook;
|
||||||
import de.robv.android.xposed.XposedBridge;
|
import de.robv.android.xposed.XposedBridge;
|
||||||
|
import external.com.android.dx.BinaryOp;
|
||||||
import external.com.android.dx.Code;
|
import external.com.android.dx.Code;
|
||||||
|
import external.com.android.dx.Comparison;
|
||||||
import external.com.android.dx.DexMaker;
|
import external.com.android.dx.DexMaker;
|
||||||
import external.com.android.dx.FieldId;
|
import external.com.android.dx.FieldId;
|
||||||
|
import external.com.android.dx.Label;
|
||||||
import external.com.android.dx.Local;
|
import external.com.android.dx.Local;
|
||||||
import external.com.android.dx.MethodId;
|
import external.com.android.dx.MethodId;
|
||||||
import external.com.android.dx.TypeId;
|
import external.com.android.dx.TypeId;
|
||||||
|
|
@ -39,6 +42,7 @@ public class HookerDexMaker {
|
||||||
|
|
||||||
public static final String METHOD_NAME_BACKUP = "backup";
|
public static final String METHOD_NAME_BACKUP = "backup";
|
||||||
public static final String METHOD_NAME_HOOK = "hook";
|
public static final String METHOD_NAME_HOOK = "hook";
|
||||||
|
public static final String METHOD_NAME_CALL_BACKUP = "callBackup";
|
||||||
public static final String METHOD_NAME_SETUP = "setup";
|
public static final String METHOD_NAME_SETUP = "setup";
|
||||||
public static final TypeId<Object[]> objArrayTypeId = TypeId.get(Object[].class);
|
public static final TypeId<Object[]> objArrayTypeId = TypeId.get(Object[].class);
|
||||||
private static final String CLASS_DESC_PREFIX = "L";
|
private static final String CLASS_DESC_PREFIX = "L";
|
||||||
|
|
@ -46,14 +50,49 @@ public class HookerDexMaker {
|
||||||
* Note: this identifier is used in native codes to pass class access verification.
|
* Note: this identifier is used in native codes to pass class access verification.
|
||||||
*/
|
*/
|
||||||
private static final String CLASS_NAME_PREFIX = "EdHooker_";
|
private static final String CLASS_NAME_PREFIX = "EdHooker_";
|
||||||
private static final String FIELD_NAME_HOOKER = "hooker";
|
private static final String FIELD_NAME_HOOK_INFO = "additionalHookInfo";
|
||||||
private static final TypeId<EdHooker> hookerTypeId = TypeId.get(EdHooker.class);
|
private static final String FIELD_NAME_METHOD = "method";
|
||||||
private static final MethodId<EdHooker, Object> handleHookedMethodMethodId =
|
private static final String PARAMS_FIELD_NAME_METHOD = "method";
|
||||||
hookerTypeId.getMethod(TypeId.OBJECT, "handleHookedMethod", objArrayTypeId);
|
private static final String PARAMS_FIELD_NAME_THIS_OBJECT = "thisObject";
|
||||||
|
private static final String PARAMS_FIELD_NAME_ARGS = "args";
|
||||||
|
private static final String CALLBACK_METHOD_NAME_BEFORE = "callBeforeHookedMethod";
|
||||||
|
private static final String CALLBACK_METHOD_NAME_AFTER = "callAfterHookedMethod";
|
||||||
|
private static final String PARAMS_METHOD_NAME_IS_EARLY_RETURN = "isEarlyReturn";
|
||||||
|
private static final TypeId<Throwable> throwableTypeId = TypeId.get(Throwable.class);
|
||||||
|
private static final TypeId<Member> memberTypeId = TypeId.get(Member.class);
|
||||||
|
private static final TypeId<XC_MethodHook> callbackTypeId = TypeId.get(XC_MethodHook.class);
|
||||||
|
private static final TypeId<XposedBridge.AdditionalHookInfo> hookInfoTypeId
|
||||||
|
= TypeId.get(XposedBridge.AdditionalHookInfo.class);
|
||||||
|
private static final TypeId<XposedBridge.CopyOnWriteSortedSet> callbacksTypeId
|
||||||
|
= TypeId.get(XposedBridge.CopyOnWriteSortedSet.class);
|
||||||
|
private static final TypeId<XC_MethodHook.MethodHookParam> paramTypeId
|
||||||
|
= TypeId.get(XC_MethodHook.MethodHookParam.class);
|
||||||
|
private static final MethodId<XC_MethodHook.MethodHookParam, Void> setResultMethodId =
|
||||||
|
paramTypeId.getMethod(TypeId.VOID, "setResult", TypeId.OBJECT);
|
||||||
|
private static final MethodId<XC_MethodHook.MethodHookParam, Void> setThrowableMethodId =
|
||||||
|
paramTypeId.getMethod(TypeId.VOID, "setThrowable", throwableTypeId);
|
||||||
|
private static final MethodId<XC_MethodHook.MethodHookParam, Object> getResultMethodId =
|
||||||
|
paramTypeId.getMethod(TypeId.OBJECT, "getResult");
|
||||||
|
private static final MethodId<XC_MethodHook.MethodHookParam, Throwable> getThrowableMethodId =
|
||||||
|
paramTypeId.getMethod(throwableTypeId, "getThrowable");
|
||||||
|
private static final MethodId<XC_MethodHook.MethodHookParam, Boolean> hasThrowableMethodId =
|
||||||
|
paramTypeId.getMethod(TypeId.BOOLEAN, "hasThrowable");
|
||||||
|
private static final MethodId<XC_MethodHook, Void> callAfterCallbackMethodId =
|
||||||
|
callbackTypeId.getMethod(TypeId.VOID, CALLBACK_METHOD_NAME_AFTER, paramTypeId);
|
||||||
|
private static final MethodId<XC_MethodHook, Void> callBeforeCallbackMethodId =
|
||||||
|
callbackTypeId.getMethod(TypeId.VOID, CALLBACK_METHOD_NAME_BEFORE, paramTypeId);
|
||||||
|
private static final FieldId<XC_MethodHook.MethodHookParam, Boolean> returnEarlyFieldId =
|
||||||
|
paramTypeId.getField(TypeId.BOOLEAN, "returnEarly");
|
||||||
|
private static final TypeId<XposedBridge> xposedBridgeTypeId = TypeId.get(XposedBridge.class);
|
||||||
|
private static final MethodId<XposedBridge, Void> logThrowableMethodId =
|
||||||
|
xposedBridgeTypeId.getMethod(TypeId.VOID, "log", throwableTypeId);
|
||||||
|
private static final MethodId<XposedBridge, Void> logStrMethodId =
|
||||||
|
xposedBridgeTypeId.getMethod(TypeId.VOID, "log", TypeId.STRING);
|
||||||
|
|
||||||
private static AtomicLong sClassNameSuffix = new AtomicLong(1);
|
private static AtomicLong sClassNameSuffix = new AtomicLong(1);
|
||||||
|
|
||||||
private FieldId<?, EdHooker> mHookerFieldId;
|
private FieldId<?, XposedBridge.AdditionalHookInfo> mHookInfoFieldId;
|
||||||
|
private FieldId<?, Member> mMethodFieldId;
|
||||||
private MethodId<?, ?> mBackupMethodId;
|
private MethodId<?, ?> mBackupMethodId;
|
||||||
private MethodId<?, ?> mCallBackupMethodId;
|
private MethodId<?, ?> mCallBackupMethodId;
|
||||||
private MethodId<?, ?> mHookMethodId;
|
private MethodId<?, ?> mHookMethodId;
|
||||||
|
|
@ -64,6 +103,8 @@ public class HookerDexMaker {
|
||||||
private Class<?> mReturnType;
|
private Class<?> mReturnType;
|
||||||
private TypeId<?> mReturnTypeId;
|
private TypeId<?> mReturnTypeId;
|
||||||
private boolean mIsStatic;
|
private boolean mIsStatic;
|
||||||
|
// TODO use this to generate methods
|
||||||
|
private boolean mHasThrowable;
|
||||||
|
|
||||||
private DexMaker mDexMaker;
|
private DexMaker mDexMaker;
|
||||||
private Member mMember;
|
private Member mMember;
|
||||||
|
|
@ -72,7 +113,7 @@ public class HookerDexMaker {
|
||||||
private Class<?> mHookClass;
|
private Class<?> mHookClass;
|
||||||
private Method mHookMethod;
|
private Method mHookMethod;
|
||||||
private Method mBackupMethod;
|
private Method mBackupMethod;
|
||||||
private EdHooker mHooker;
|
private Method mCallBackupMethod;
|
||||||
|
|
||||||
private static TypeId<?>[] getParameterTypeIds(Class<?>[] parameterTypes, boolean isStatic) {
|
private static TypeId<?>[] getParameterTypeIds(Class<?>[] parameterTypes, boolean isStatic) {
|
||||||
int parameterSize = parameterTypes.length;
|
int parameterSize = parameterTypes.length;
|
||||||
|
|
@ -118,6 +159,7 @@ public class HookerDexMaker {
|
||||||
}
|
}
|
||||||
mParameterTypeIds = getParameterTypeIds(method.getParameterTypes(), mIsStatic);
|
mParameterTypeIds = getParameterTypeIds(method.getParameterTypes(), mIsStatic);
|
||||||
mActualParameterTypes = getParameterTypes(method.getParameterTypes(), mIsStatic);
|
mActualParameterTypes = getParameterTypes(method.getParameterTypes(), mIsStatic);
|
||||||
|
mHasThrowable = method.getExceptionTypes().length > 0;
|
||||||
} else if (member instanceof Constructor) {
|
} else if (member instanceof Constructor) {
|
||||||
Constructor constructor = (Constructor) member;
|
Constructor constructor = (Constructor) member;
|
||||||
mIsStatic = false;
|
mIsStatic = false;
|
||||||
|
|
@ -125,6 +167,7 @@ public class HookerDexMaker {
|
||||||
mReturnTypeId = TypeId.VOID;
|
mReturnTypeId = TypeId.VOID;
|
||||||
mParameterTypeIds = getParameterTypeIds(constructor.getParameterTypes(), mIsStatic);
|
mParameterTypeIds = getParameterTypeIds(constructor.getParameterTypes(), mIsStatic);
|
||||||
mActualParameterTypes = getParameterTypes(constructor.getParameterTypes(), mIsStatic);
|
mActualParameterTypes = getParameterTypes(constructor.getParameterTypes(), mIsStatic);
|
||||||
|
mHasThrowable = constructor.getExceptionTypes().length > 0;
|
||||||
} else if (member.getDeclaringClass().isInterface()) {
|
} else if (member.getDeclaringClass().isInterface()) {
|
||||||
throw new IllegalArgumentException("Cannot hook interfaces: " + member.toString());
|
throw new IllegalArgumentException("Cannot hook interfaces: " + member.toString());
|
||||||
} else if (Modifier.isAbstract(member.getModifiers())) {
|
} else if (Modifier.isAbstract(member.getModifiers())) {
|
||||||
|
|
@ -185,10 +228,12 @@ public class HookerDexMaker {
|
||||||
|
|
||||||
mHookClass = Class.forName(className.replace("/", "."), true, loader);
|
mHookClass = Class.forName(className.replace("/", "."), true, loader);
|
||||||
// Execute our newly-generated code in-process.
|
// Execute our newly-generated code in-process.
|
||||||
mBackupMethod = mHookClass.getMethod(METHOD_NAME_BACKUP, mActualParameterTypes);
|
mHookClass.getMethod(METHOD_NAME_SETUP, Member.class, XposedBridge.AdditionalHookInfo.class)
|
||||||
mHooker = new EdHooker(mHookInfo, mMember, mBackupMethod, mIsStatic);
|
.invoke(null, mMember, mHookInfo);
|
||||||
mHookClass.getMethod(METHOD_NAME_SETUP, EdHooker.class).invoke(null, mHooker);
|
|
||||||
mHookMethod = mHookClass.getMethod(METHOD_NAME_HOOK, mActualParameterTypes);
|
mHookMethod = mHookClass.getMethod(METHOD_NAME_HOOK, mActualParameterTypes);
|
||||||
|
mBackupMethod = mHookClass.getMethod(METHOD_NAME_BACKUP, mActualParameterTypes);
|
||||||
|
mCallBackupMethod = mHookClass.getMethod(METHOD_NAME_CALL_BACKUP, mActualParameterTypes);
|
||||||
|
Yahfa.setMethodNonCompilable(mCallBackupMethod);
|
||||||
HookMain.backupAndHook(mMember, mHookMethod, mBackupMethod);
|
HookMain.backupAndHook(mMember, mHookMethod, mBackupMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,6 +245,7 @@ public class HookerDexMaker {
|
||||||
generateSetupMethod();
|
generateSetupMethod();
|
||||||
generateBackupMethod();
|
generateBackupMethod();
|
||||||
generateHookMethod();
|
generateHookMethod();
|
||||||
|
generateCallBackupMethod();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Method getHookMethod() {
|
public Method getHookMethod() {
|
||||||
|
|
@ -210,26 +256,32 @@ public class HookerDexMaker {
|
||||||
return mBackupMethod;
|
return mBackupMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Method getCallBackupMethod() {
|
||||||
|
return mCallBackupMethod;
|
||||||
|
}
|
||||||
|
|
||||||
public Class getHookClass() {
|
public Class getHookClass() {
|
||||||
return mHookClass;
|
return mHookClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EdHooker getHooker() {return mHooker;}
|
|
||||||
|
|
||||||
private void generateFields() {
|
private void generateFields() {
|
||||||
mHookerFieldId = mHookerTypeId.getField(hookerTypeId, FIELD_NAME_HOOKER);
|
mHookInfoFieldId = mHookerTypeId.getField(hookInfoTypeId, FIELD_NAME_HOOK_INFO);
|
||||||
mDexMaker.declare(mHookerFieldId, Modifier.STATIC, null);
|
mMethodFieldId = mHookerTypeId.getField(memberTypeId, FIELD_NAME_METHOD);
|
||||||
|
mDexMaker.declare(mHookInfoFieldId, Modifier.STATIC, null);
|
||||||
|
mDexMaker.declare(mMethodFieldId, Modifier.STATIC, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void generateSetupMethod() {
|
private void generateSetupMethod() {
|
||||||
MethodId<?, Void> setupMethodId = mHookerTypeId.getMethod(
|
MethodId<?, Void> setupMethodId = mHookerTypeId.getMethod(
|
||||||
TypeId.VOID, METHOD_NAME_SETUP, hookerTypeId);
|
TypeId.VOID, METHOD_NAME_SETUP, memberTypeId, hookInfoTypeId);
|
||||||
Code code = mDexMaker.declare(setupMethodId, Modifier.PUBLIC | Modifier.STATIC);
|
Code code = mDexMaker.declare(setupMethodId, Modifier.PUBLIC | Modifier.STATIC);
|
||||||
// init logic
|
// init logic
|
||||||
// get parameters
|
// get parameters
|
||||||
Local<EdHooker> hooker = code.getParameter(0, hookerTypeId);
|
Local<Member> method = code.getParameter(0, memberTypeId);
|
||||||
|
Local<XposedBridge.AdditionalHookInfo> hookInfo = code.getParameter(1, hookInfoTypeId);
|
||||||
// save params to static
|
// save params to static
|
||||||
code.sput(mHookerFieldId, hooker);
|
code.sput(mMethodFieldId, method);
|
||||||
|
code.sput(mHookInfoFieldId, hookInfo);
|
||||||
code.returnVoid();
|
code.returnVoid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -246,48 +298,253 @@ public class HookerDexMaker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void generateCallBackupMethod() {
|
||||||
|
mCallBackupMethodId = mHookerTypeId.getMethod(mReturnTypeId, METHOD_NAME_CALL_BACKUP, mParameterTypeIds);
|
||||||
|
Code code = mDexMaker.declare(mCallBackupMethodId, Modifier.PUBLIC | Modifier.STATIC);
|
||||||
|
// just call backup and return its result
|
||||||
|
Local[] allArgsLocals = createParameterLocals(code);
|
||||||
|
Map<TypeId, Local> resultLocals = createResultLocals(code);
|
||||||
|
if (mReturnTypeId.equals(TypeId.VOID)) {
|
||||||
|
code.invokeStatic(mBackupMethodId, null, allArgsLocals);
|
||||||
|
code.returnVoid();
|
||||||
|
} else {
|
||||||
|
Local result = resultLocals.get(mReturnTypeId);
|
||||||
|
code.invokeStatic(mBackupMethodId, result, allArgsLocals);
|
||||||
|
code.returnValue(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void generateHookMethod() {
|
private void generateHookMethod() {
|
||||||
mHookMethodId = mHookerTypeId.getMethod(mReturnTypeId, METHOD_NAME_HOOK, mParameterTypeIds);
|
mHookMethodId = mHookerTypeId.getMethod(mReturnTypeId, METHOD_NAME_HOOK, mParameterTypeIds);
|
||||||
Code code = mDexMaker.declare(mHookMethodId, Modifier.PUBLIC | Modifier.STATIC);
|
Code code = mDexMaker.declare(mHookMethodId, Modifier.PUBLIC | Modifier.STATIC);
|
||||||
|
|
||||||
// code starts
|
// code starts
|
||||||
|
|
||||||
|
// prepare common labels
|
||||||
|
Label noHookReturn = new Label();
|
||||||
|
Label incrementAndCheckBefore = new Label();
|
||||||
|
Label tryBeforeCatch = new Label();
|
||||||
|
Label noExceptionBefore = new Label();
|
||||||
|
Label checkAndCallBackup = new Label();
|
||||||
|
Label beginCallBefore = new Label();
|
||||||
|
Label beginCallAfter = new Label();
|
||||||
|
Label tryOrigCatch = new Label();
|
||||||
|
Label noExceptionOrig = new Label();
|
||||||
|
Label tryAfterCatch = new Label();
|
||||||
|
Label decrementAndCheckAfter = new Label();
|
||||||
|
Label noBackupThrowable = new Label();
|
||||||
|
Label throwThrowable = new Label();
|
||||||
// prepare locals
|
// prepare locals
|
||||||
Local<EdHooker> hooker = code.newLocal(hookerTypeId);
|
Local<Boolean> disableHooks = code.newLocal(TypeId.BOOLEAN);
|
||||||
|
Local<XposedBridge.AdditionalHookInfo> hookInfo = code.newLocal(hookInfoTypeId);
|
||||||
|
Local<XposedBridge.CopyOnWriteSortedSet> callbacks = code.newLocal(callbacksTypeId);
|
||||||
|
Local<Object[]> snapshot = code.newLocal(objArrayTypeId);
|
||||||
|
Local<Integer> snapshotLen = code.newLocal(TypeId.INT);
|
||||||
|
Local<Object> callbackObj = code.newLocal(TypeId.OBJECT);
|
||||||
|
Local<XC_MethodHook> callback = code.newLocal(callbackTypeId);
|
||||||
|
|
||||||
Local<Object> resultObj = code.newLocal(TypeId.OBJECT); // as a temp Local
|
Local<Object> resultObj = code.newLocal(TypeId.OBJECT); // as a temp Local
|
||||||
|
Local<Integer> one = code.newLocal(TypeId.INT);
|
||||||
|
Local<Object> nullObj = code.newLocal(TypeId.OBJECT);
|
||||||
|
Local<Throwable> throwable = code.newLocal(throwableTypeId);
|
||||||
|
|
||||||
|
Local<XC_MethodHook.MethodHookParam> param = code.newLocal(paramTypeId);
|
||||||
|
Local<Member> method = code.newLocal(memberTypeId);
|
||||||
|
Local<Object> thisObject = code.newLocal(TypeId.OBJECT);
|
||||||
Local<Object[]> args = code.newLocal(objArrayTypeId);
|
Local<Object[]> args = code.newLocal(objArrayTypeId);
|
||||||
|
Local<Boolean> returnEarly = code.newLocal(TypeId.BOOLEAN);
|
||||||
|
|
||||||
Local<Integer> actualParamSize = code.newLocal(TypeId.INT);
|
Local<Integer> actualParamSize = code.newLocal(TypeId.INT);
|
||||||
Local<Integer> argIndex = code.newLocal(TypeId.INT);
|
Local<Integer> argIndex = code.newLocal(TypeId.INT);
|
||||||
|
|
||||||
|
Local<Integer> beforeIdx = code.newLocal(TypeId.INT);
|
||||||
|
Local<Object> lastResult = code.newLocal(TypeId.OBJECT);
|
||||||
|
Local<Throwable> lastThrowable = code.newLocal(throwableTypeId);
|
||||||
|
Local<Boolean> hasThrowable = code.newLocal(TypeId.BOOLEAN);
|
||||||
|
|
||||||
Local[] allArgsLocals = createParameterLocals(code);
|
Local[] allArgsLocals = createParameterLocals(code);
|
||||||
|
|
||||||
Map<TypeId, Local> resultLocals = createResultLocals(code);
|
Map<TypeId, Local> resultLocals = createResultLocals(code);
|
||||||
|
|
||||||
code.loadConstant(args, null);
|
code.loadConstant(args, null);
|
||||||
code.loadConstant(argIndex, 0);
|
code.loadConstant(argIndex, 0);
|
||||||
|
code.loadConstant(one, 1);
|
||||||
|
code.loadConstant(snapshotLen, 0);
|
||||||
|
code.loadConstant(nullObj, null);
|
||||||
|
|
||||||
code.sget(mHookerFieldId, hooker);
|
// check XposedBridge.disableHooks flag
|
||||||
|
|
||||||
|
FieldId<XposedBridge, Boolean> disableHooksField =
|
||||||
|
xposedBridgeTypeId.getField(TypeId.BOOLEAN, "disableHooks");
|
||||||
|
code.sget(disableHooksField, disableHooks);
|
||||||
|
// disableHooks == true => no hooking
|
||||||
|
code.compareZ(Comparison.NE, noHookReturn, disableHooks);
|
||||||
|
|
||||||
|
// check callbacks length
|
||||||
|
code.sget(mHookInfoFieldId, hookInfo);
|
||||||
|
code.iget(hookInfoTypeId.getField(callbacksTypeId, "callbacks"), callbacks, hookInfo);
|
||||||
|
code.invokeVirtual(callbacksTypeId.getMethod(objArrayTypeId, "getSnapshot"), snapshot, callbacks);
|
||||||
|
code.arrayLength(snapshotLen, snapshot);
|
||||||
|
// snapshotLen == 0 => no hooking
|
||||||
|
code.compareZ(Comparison.EQ, noHookReturn, snapshotLen);
|
||||||
|
|
||||||
// start hooking
|
// start hooking
|
||||||
|
|
||||||
// prepare hooking locals
|
// prepare hooking locals
|
||||||
int paramsSize = mParameterTypeIds.length;
|
int paramsSize = mParameterTypeIds.length;
|
||||||
code.loadConstant(actualParamSize, paramsSize);
|
int offset = 0;
|
||||||
|
// thisObject
|
||||||
|
if (mIsStatic) {
|
||||||
|
// thisObject = null
|
||||||
|
code.loadConstant(thisObject, null);
|
||||||
|
} else {
|
||||||
|
// thisObject = args[0]
|
||||||
|
offset = 1;
|
||||||
|
code.move(thisObject, allArgsLocals[0]);
|
||||||
|
}
|
||||||
|
// actual args (exclude thisObject if this is not a static method)
|
||||||
|
code.loadConstant(actualParamSize, paramsSize - offset);
|
||||||
code.newArray(args, actualParamSize);
|
code.newArray(args, actualParamSize);
|
||||||
for (int i = 0; i < paramsSize; i++) {
|
for (int i = offset; i < paramsSize; i++) {
|
||||||
Local parameter = allArgsLocals[i];
|
Local parameter = allArgsLocals[i];
|
||||||
// save parameter to resultObj as Object
|
// save parameter to resultObj as Object
|
||||||
autoBoxIfNecessary(code, resultObj, parameter);
|
autoBoxIfNecessary(code, resultObj, parameter);
|
||||||
code.loadConstant(argIndex, i);
|
code.loadConstant(argIndex, i - offset);
|
||||||
// save Object to args
|
// save Object to args
|
||||||
code.aput(args, argIndex, resultObj);
|
code.aput(args, argIndex, resultObj);
|
||||||
}
|
}
|
||||||
// handleHookedMethod
|
// create param
|
||||||
|
code.newInstance(param, paramTypeId.getConstructor());
|
||||||
|
// set method, thisObject, args
|
||||||
|
code.sget(mMethodFieldId, method);
|
||||||
|
code.iput(paramTypeId.getField(memberTypeId, "method"), param, method);
|
||||||
|
code.iput(paramTypeId.getField(TypeId.OBJECT, "thisObject"), param, thisObject);
|
||||||
|
code.iput(paramTypeId.getField(objArrayTypeId, "args"), param, args);
|
||||||
|
|
||||||
|
// call beforeCallbacks
|
||||||
|
code.loadConstant(beforeIdx, 0);
|
||||||
|
|
||||||
|
code.mark(beginCallBefore);
|
||||||
|
// start of try
|
||||||
|
code.addCatchClause(throwableTypeId, tryBeforeCatch);
|
||||||
|
|
||||||
|
code.aget(callbackObj, snapshot, beforeIdx);
|
||||||
|
code.cast(callback, callbackObj);
|
||||||
|
code.invokeVirtual(callBeforeCallbackMethodId, null, callback, param);
|
||||||
|
code.jump(noExceptionBefore);
|
||||||
|
|
||||||
|
// end of try
|
||||||
|
code.removeCatchClause(throwableTypeId);
|
||||||
|
|
||||||
|
// start of catch
|
||||||
|
code.mark(tryBeforeCatch);
|
||||||
|
code.moveException(throwable);
|
||||||
|
code.invokeStatic(logThrowableMethodId, null, throwable);
|
||||||
|
code.invokeVirtual(setResultMethodId, null, param, nullObj);
|
||||||
|
code.loadConstant(returnEarly, false);
|
||||||
|
code.iput(returnEarlyFieldId, param, returnEarly);
|
||||||
|
code.jump(incrementAndCheckBefore);
|
||||||
|
|
||||||
|
// no exception when calling beforeCallbacks
|
||||||
|
code.mark(noExceptionBefore);
|
||||||
|
code.iget(returnEarlyFieldId, returnEarly, param);
|
||||||
|
// if returnEarly == false, continue
|
||||||
|
code.compareZ(Comparison.EQ, incrementAndCheckBefore, returnEarly);
|
||||||
|
// returnEarly == true, break
|
||||||
|
code.op(BinaryOp.ADD, beforeIdx, beforeIdx, one);
|
||||||
|
code.jump(checkAndCallBackup);
|
||||||
|
|
||||||
|
// increment and check to continue
|
||||||
|
code.mark(incrementAndCheckBefore);
|
||||||
|
code.op(BinaryOp.ADD, beforeIdx, beforeIdx, one);
|
||||||
|
code.compare(Comparison.LT, beginCallBefore, beforeIdx, snapshotLen);
|
||||||
|
|
||||||
|
// check and call backup
|
||||||
|
code.mark(checkAndCallBackup);
|
||||||
|
code.iget(returnEarlyFieldId, returnEarly, param);
|
||||||
|
// if returnEarly == true, go to call afterCallbacks directly
|
||||||
|
code.compareZ(Comparison.NE, noExceptionOrig, returnEarly);
|
||||||
|
// try to call backup
|
||||||
|
// try start
|
||||||
|
code.addCatchClause(throwableTypeId, tryOrigCatch);
|
||||||
|
// we have to load args[] to paramLocals
|
||||||
|
// because args[] may be changed in beforeHookedMethod
|
||||||
|
// should consider first param is thisObj if hooked method is not static
|
||||||
|
offset = mIsStatic ? 0 : 1;
|
||||||
|
for (int i = offset; i < allArgsLocals.length; i++) {
|
||||||
|
code.loadConstant(argIndex, i - offset);
|
||||||
|
code.aget(resultObj, args, argIndex);
|
||||||
|
autoUnboxIfNecessary(code, allArgsLocals[i], resultObj, resultLocals, true);
|
||||||
|
}
|
||||||
|
// get pre-created Local with a matching typeId
|
||||||
|
if (mReturnTypeId.equals(TypeId.VOID)) {
|
||||||
|
code.invokeStatic(mBackupMethodId, null, allArgsLocals);
|
||||||
|
// TODO maybe keep preset result to do some magic?
|
||||||
|
code.invokeVirtual(setResultMethodId, null, param, nullObj);
|
||||||
|
} else {
|
||||||
|
Local returnedResult = resultLocals.get(mReturnTypeId);
|
||||||
|
code.invokeStatic(mBackupMethodId, returnedResult, allArgsLocals);
|
||||||
|
// save returnedResult to resultObj as a Object
|
||||||
|
autoBoxIfNecessary(code, resultObj, returnedResult);
|
||||||
|
// save resultObj to param
|
||||||
|
code.invokeVirtual(setResultMethodId, null, param, resultObj);
|
||||||
|
}
|
||||||
|
// go to call afterCallbacks
|
||||||
|
code.jump(noExceptionOrig);
|
||||||
|
// try end
|
||||||
|
code.removeCatchClause(throwableTypeId);
|
||||||
|
// catch
|
||||||
|
code.mark(tryOrigCatch);
|
||||||
|
code.moveException(throwable);
|
||||||
|
// exception occurred when calling backup, save throwable to param
|
||||||
|
code.invokeVirtual(setThrowableMethodId, null, param, throwable);
|
||||||
|
|
||||||
|
code.mark(noExceptionOrig);
|
||||||
|
code.op(BinaryOp.SUBTRACT, beforeIdx, beforeIdx, one);
|
||||||
|
|
||||||
|
// call afterCallbacks
|
||||||
|
code.mark(beginCallAfter);
|
||||||
|
// save results of backup calling
|
||||||
|
code.invokeVirtual(getResultMethodId, lastResult, param);
|
||||||
|
code.invokeVirtual(getThrowableMethodId, lastThrowable, param);
|
||||||
|
// try start
|
||||||
|
code.addCatchClause(throwableTypeId, tryAfterCatch);
|
||||||
|
code.aget(callbackObj, snapshot, beforeIdx);
|
||||||
|
code.cast(callback, callbackObj);
|
||||||
|
code.invokeVirtual(callAfterCallbackMethodId, null, callback, param);
|
||||||
|
// all good, just continue
|
||||||
|
code.jump(decrementAndCheckAfter);
|
||||||
|
// try end
|
||||||
|
code.removeCatchClause(throwableTypeId);
|
||||||
|
// catch
|
||||||
|
code.mark(tryAfterCatch);
|
||||||
|
code.moveException(throwable);
|
||||||
|
code.invokeStatic(logThrowableMethodId, null, throwable);
|
||||||
|
// if lastThrowable == null, go to recover lastResult
|
||||||
|
code.compareZ(Comparison.EQ, noBackupThrowable, lastThrowable);
|
||||||
|
// lastThrowable != null, recover lastThrowable
|
||||||
|
code.invokeVirtual(setThrowableMethodId, null, param, lastThrowable);
|
||||||
|
// continue
|
||||||
|
code.jump(decrementAndCheckAfter);
|
||||||
|
code.mark(noBackupThrowable);
|
||||||
|
// recover lastResult and continue
|
||||||
|
code.invokeVirtual(setResultMethodId, null, param, lastResult);
|
||||||
|
// decrement and check continue
|
||||||
|
code.mark(decrementAndCheckAfter);
|
||||||
|
code.op(BinaryOp.SUBTRACT, beforeIdx, beforeIdx, one);
|
||||||
|
code.compareZ(Comparison.GE, beginCallAfter, beforeIdx);
|
||||||
|
|
||||||
|
// callbacks end
|
||||||
|
// return
|
||||||
|
code.invokeVirtual(hasThrowableMethodId, hasThrowable, param);
|
||||||
|
// if hasThrowable, throw the throwable and return
|
||||||
|
code.compareZ(Comparison.NE, throwThrowable, hasThrowable);
|
||||||
|
// return getResult
|
||||||
if (mReturnTypeId.equals(TypeId.VOID)) {
|
if (mReturnTypeId.equals(TypeId.VOID)) {
|
||||||
code.invokeVirtual(handleHookedMethodMethodId, null, hooker, args);
|
|
||||||
code.returnVoid();
|
code.returnVoid();
|
||||||
} else {
|
} else {
|
||||||
// hooker always return an Object, so save to resultObj
|
// getResult always return an Object, so save to resultObj
|
||||||
code.invokeVirtual(handleHookedMethodMethodId, resultObj, hooker, args);
|
code.invokeVirtual(getResultMethodId, resultObj, param);
|
||||||
// have to unbox it if returnType is primitive
|
// have to unbox it if returnType is primitive
|
||||||
// casting Object
|
// casting Object
|
||||||
TypeId objTypeId = getObjTypeIdIfPrimitive(mReturnTypeId);
|
TypeId objTypeId = getObjTypeIdIfPrimitive(mReturnTypeId);
|
||||||
|
|
@ -299,6 +556,21 @@ public class HookerDexMaker {
|
||||||
// return
|
// return
|
||||||
code.returnValue(toReturn);
|
code.returnValue(toReturn);
|
||||||
}
|
}
|
||||||
|
// throw throwable
|
||||||
|
code.mark(throwThrowable);
|
||||||
|
code.invokeVirtual(getThrowableMethodId, throwable, param);
|
||||||
|
code.throwValue(throwable);
|
||||||
|
|
||||||
|
// call backup and return
|
||||||
|
code.mark(noHookReturn);
|
||||||
|
if (mReturnTypeId.equals(TypeId.VOID)) {
|
||||||
|
code.invokeStatic(mBackupMethodId, null, allArgsLocals);
|
||||||
|
code.returnVoid();
|
||||||
|
} else {
|
||||||
|
Local result = resultLocals.get(mReturnTypeId);
|
||||||
|
code.invokeStatic(mBackupMethodId, result, allArgsLocals);
|
||||||
|
code.returnValue(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Local[] createParameterLocals(Code code) {
|
private Local[] createParameterLocals(Code code) {
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
package de.robv.android.xposed;
|
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Member;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
import static de.robv.android.xposed.XposedBridge.disableHooks;
|
|
||||||
|
|
||||||
public class EdHooker {
|
|
||||||
private final XposedBridge.AdditionalHookInfo additionalInfo;
|
|
||||||
private final Member method;
|
|
||||||
private final Method backup;
|
|
||||||
private final boolean isStatic;
|
|
||||||
|
|
||||||
public EdHooker(XposedBridge.AdditionalHookInfo info, Member origin, Method backup, boolean isStatic) {
|
|
||||||
this.additionalInfo = info;
|
|
||||||
this.method = origin;
|
|
||||||
this.backup = backup;
|
|
||||||
this.isStatic = isStatic;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Object callBackup(Object thisObject, Object[] args) throws InvocationTargetException, IllegalAccessException {
|
|
||||||
if (args == null) {
|
|
||||||
args = new Object[0];
|
|
||||||
}
|
|
||||||
if (isStatic) {
|
|
||||||
return backup.invoke(null, args);
|
|
||||||
} else {
|
|
||||||
Object[] newArgs = new Object[args.length + 1];
|
|
||||||
newArgs[0] = thisObject;
|
|
||||||
System.arraycopy(args, 0, newArgs, 1, args.length);
|
|
||||||
return backup.invoke(null, newArgs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings({"unused", "RedundantSuppression"})
|
|
||||||
public Object handleHookedMethod(Object[] args) throws Throwable {
|
|
||||||
if (disableHooks) {
|
|
||||||
return backup.invoke(null, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
Object[] callbacksSnapshot = additionalInfo.callbacks.getSnapshot();
|
|
||||||
final int callbacksLength = callbacksSnapshot.length;
|
|
||||||
if (callbacksLength == 0) {
|
|
||||||
return backup.invoke(null, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
XC_MethodHook.MethodHookParam param = new XC_MethodHook.MethodHookParam();
|
|
||||||
|
|
||||||
param.method = method;
|
|
||||||
|
|
||||||
if (isStatic) {
|
|
||||||
param.thisObject = null;
|
|
||||||
param.args = args;
|
|
||||||
} else {
|
|
||||||
param.thisObject = args[0];
|
|
||||||
param.args = new Object[args.length - 1];
|
|
||||||
System.arraycopy(args, 1, param.args, 0, args.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// call "before method" callbacks
|
|
||||||
int beforeIdx = 0;
|
|
||||||
do {
|
|
||||||
try {
|
|
||||||
((XC_MethodHook) callbacksSnapshot[beforeIdx]).beforeHookedMethod(param);
|
|
||||||
} catch (Throwable t) {
|
|
||||||
XposedBridge.log(t);
|
|
||||||
|
|
||||||
// reset result (ignoring what the unexpectedly exiting callback did)
|
|
||||||
param.setResult(null);
|
|
||||||
param.returnEarly = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (param.returnEarly) {
|
|
||||||
// skip remaining "before" callbacks and corresponding "after" callbacks
|
|
||||||
beforeIdx++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} while (++beforeIdx < callbacksLength);
|
|
||||||
|
|
||||||
// call original method if not requested otherwise
|
|
||||||
if (!param.returnEarly) {
|
|
||||||
param.setResult(callBackup(param.thisObject, param.args));
|
|
||||||
}
|
|
||||||
|
|
||||||
// call "after method" callbacks
|
|
||||||
int afterIdx = beforeIdx - 1;
|
|
||||||
do {
|
|
||||||
Object lastResult = param.getResult();
|
|
||||||
Throwable lastThrowable = param.getThrowable();
|
|
||||||
|
|
||||||
try {
|
|
||||||
((XC_MethodHook) callbacksSnapshot[afterIdx]).afterHookedMethod(param);
|
|
||||||
} catch (Throwable t) {
|
|
||||||
XposedBridge.log(t);
|
|
||||||
|
|
||||||
// reset to last result (ignoring what the unexpectedly exiting callback did)
|
|
||||||
if (lastThrowable == null)
|
|
||||||
param.setResult(lastResult);
|
|
||||||
else
|
|
||||||
param.setThrowable(lastThrowable);
|
|
||||||
}
|
|
||||||
} while (--afterIdx >= 0);
|
|
||||||
|
|
||||||
// return
|
|
||||||
if (param.hasThrowable())
|
|
||||||
throw param.getThrowable();
|
|
||||||
else
|
|
||||||
return param.getResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue