Fix memory issues when hooking too many methods in one process
This commit is contained in:
parent
045104d1bf
commit
d57d602420
|
|
@ -9,13 +9,22 @@ import com.elderdrivers.riru.xposed.entry.Router;
|
|||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static com.elderdrivers.riru.xposed.util.FileUtils.getDataPathPrefix;
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public class Main implements KeepAll {
|
||||
|
||||
/**
|
||||
* When set to true, install bootstrap hooks and loadModules
|
||||
* for each process when it starts.
|
||||
* This means you can deactivate or activate every module
|
||||
* for the process you restart without rebooting.
|
||||
*/
|
||||
private static final boolean DYNAMIC_LOAD_MODULES = false;
|
||||
// private static String sForkAndSpecializePramsStr = "";
|
||||
// private static String sForkSystemServerPramsStr = "";
|
||||
public static String sAppDataDir = "";
|
||||
private static final boolean DYNAMIC_LOAD_MODULES = false;
|
||||
public static String sAppProcessName = "";
|
||||
|
||||
static {
|
||||
init(Build.VERSION.SDK_INT);
|
||||
|
|
@ -66,7 +75,7 @@ public class Main implements KeepAll {
|
|||
// Utils.logD(sForkSystemServerPramsStr + " = " + pid);
|
||||
if (pid == 0) {
|
||||
// in system_server process
|
||||
sAppDataDir = "/data/data/android/";
|
||||
sAppDataDir = getDataPathPrefix() + "android/";
|
||||
Router.onProcessForked(true);
|
||||
} else {
|
||||
// in zygote process, res is child zygote pid
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package com.elderdrivers.riru.xposed.dexmaker;
|
||||
|
||||
import android.app.AndroidAppHelper;
|
||||
import android.os.Build;
|
||||
|
||||
import com.elderdrivers.riru.xposed.Main;
|
||||
import com.elderdrivers.riru.xposed.util.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Constructor;
|
||||
|
|
@ -12,12 +12,29 @@ import java.lang.reflect.Member;
|
|||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
|
||||
import static com.elderdrivers.riru.xposed.dexmaker.HookerDexMaker.SHOULD_USE_IN_MEMORY_DEX;
|
||||
import static com.elderdrivers.riru.xposed.util.FileUtils.getDataPathPrefix;
|
||||
|
||||
public final class DynamicBridge {
|
||||
|
||||
private static HashMap<Member, HookerDexMaker> hookedInfo = new HashMap<>();
|
||||
private static final HashMap<Member, Method> hookedInfo = new HashMap<>();
|
||||
private static final HookerDexMaker dexMaker = new HookerDexMaker();
|
||||
private static final AtomicBoolean dexPathInited = new AtomicBoolean(false);
|
||||
private static final File dexDir;
|
||||
private static final File dexOptDir;
|
||||
|
||||
static {
|
||||
// we always choose to use device encrypted storage data on android n and later
|
||||
// in case some app is installing hooks before phone is unlocked
|
||||
String fixedAppDataDir = getDataPathPrefix() + AndroidAppHelper.currentPackageName() + "/";
|
||||
dexDir = new File(fixedAppDataDir, "/cache/edhookers/"
|
||||
+ Main.sAppProcessName.replace(":", "_") + "/");
|
||||
dexOptDir = new File(dexDir, "oat");
|
||||
}
|
||||
|
||||
public static synchronized void hookMethod(Member hookMethod, XposedBridge.AdditionalHookInfo additionalHookInfo) {
|
||||
|
||||
|
|
@ -33,21 +50,24 @@ public final class DynamicBridge {
|
|||
DexLog.d("start to generate class for: " + hookMethod);
|
||||
try {
|
||||
// for Android Oreo and later use InMemoryClassLoader
|
||||
String dexDirPath = "";
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
if (!SHOULD_USE_IN_MEMORY_DEX) {
|
||||
// under Android Oreo, using DexClassLoader
|
||||
String dataDir = Main.sAppDataDir;
|
||||
String processName = AndroidAppHelper.currentProcessName();
|
||||
File dexDir = new File(dataDir, "cache/edhookers/" + processName + "/");
|
||||
dexDir.mkdirs();
|
||||
dexDirPath = dexDir.getAbsolutePath();
|
||||
if (dexPathInited.compareAndSet(false, true)) {
|
||||
// delete previous compiled dex to prevent potential crashing
|
||||
// TODO find a way to reuse them in consideration of performance
|
||||
try {
|
||||
dexDir.mkdirs();
|
||||
DexLog.d(Main.sAppProcessName + " deleting dir: " + dexOptDir.getAbsolutePath());
|
||||
FileUtils.delete(dexOptDir);
|
||||
} catch (Throwable throwable) {
|
||||
}
|
||||
}
|
||||
}
|
||||
HookerDexMaker dexMaker = new HookerDexMaker();
|
||||
dexMaker.start(hookMethod, additionalHookInfo,
|
||||
hookMethod.getDeclaringClass().getClassLoader(), dexDirPath);
|
||||
hookedInfo.put(hookMethod, dexMaker);
|
||||
hookMethod.getDeclaringClass().getClassLoader(), dexDir.getAbsolutePath());
|
||||
hookedInfo.put(hookMethod, dexMaker.getCallBackupMethod());
|
||||
} catch (Exception e) {
|
||||
DexLog.e("error occur when generating dex", e);
|
||||
DexLog.e("error occur when generating dex. dexDir=" + dexDir, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -71,13 +91,9 @@ public final class DynamicBridge {
|
|||
|
||||
public static Object invokeOriginalMethod(Member method, Object thisObject, Object[] args)
|
||||
throws InvocationTargetException, IllegalAccessException {
|
||||
HookerDexMaker dexMaker = hookedInfo.get(method);
|
||||
if (dexMaker == null) {
|
||||
throw new IllegalStateException("method not hooked, cannot call original method.");
|
||||
}
|
||||
Method callBackup = dexMaker.getCallBackupMethod();
|
||||
Method callBackup = hookedInfo.get(method);
|
||||
if (callBackup == null) {
|
||||
throw new IllegalStateException("original method is null, something must be wrong!");
|
||||
throw new IllegalStateException("method not hooked, cannot call original method.");
|
||||
}
|
||||
if (!Modifier.isStatic(callBackup.getModifiers())) {
|
||||
throw new IllegalStateException("original method is not static, something must be wrong!");
|
||||
|
|
|
|||
|
|
@ -34,10 +34,17 @@ import static com.elderdrivers.riru.xposed.dexmaker.DexMakerUtils.getObjTypeIdIf
|
|||
|
||||
public class HookerDexMaker {
|
||||
|
||||
public static final boolean IN_MEMORY_DEX_ELIGIBLE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
|
||||
// using InMemoryDexClassLoader when too many methods (about >175 ?)
|
||||
// are to hook might lead to large memory allocation and gc problems, forbid it for now
|
||||
public static final boolean IN_MEMORY_DEX_FORBIDDEN = true;
|
||||
public static final boolean SHOULD_USE_IN_MEMORY_DEX = IN_MEMORY_DEX_ELIGIBLE && !IN_MEMORY_DEX_FORBIDDEN;
|
||||
|
||||
public static final String METHOD_NAME_BACKUP = "backup";
|
||||
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 TypeId<Object[]> objArrayTypeId = TypeId.get(Object[].class);
|
||||
private static final String CLASS_DESC_PREFIX = "L";
|
||||
private static final String CLASS_NAME_PREFIX = "EdHooker";
|
||||
private static final String FIELD_NAME_HOOK_INFO = "additionalHookInfo";
|
||||
|
|
@ -48,8 +55,6 @@ public class HookerDexMaker {
|
|||
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";
|
||||
|
||||
public static final TypeId<Object[]> objArrayTypeId = TypeId.get(Object[].class);
|
||||
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);
|
||||
|
|
@ -194,7 +199,7 @@ public class HookerDexMaker {
|
|||
generateCallBackupMethod();
|
||||
|
||||
ClassLoader loader;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (SHOULD_USE_IN_MEMORY_DEX) {
|
||||
// in memory dex classloader
|
||||
byte[] dexBytes = mDexMaker.generate();
|
||||
loader = new InMemoryDexClassLoader(ByteBuffer.wrap(dexBytes), mAppClassLoader);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import android.content.pm.ApplicationInfo;
|
|||
import android.content.res.CompatibilityInfo;
|
||||
|
||||
import com.elderdrivers.riru.common.KeepMembers;
|
||||
import com.elderdrivers.riru.xposed.Main;
|
||||
import com.elderdrivers.riru.xposed.util.Utils;
|
||||
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
||||
|
|
@ -36,7 +38,11 @@ public class HandleBindAppHooker implements KeepMembers {
|
|||
logD("ActivityThread#handleBindApplication() starts");
|
||||
ActivityThread activityThread = (ActivityThread) thiz;
|
||||
ApplicationInfo appInfo = (ApplicationInfo) getObjectField(bindData, "appInfo");
|
||||
// save app process name here for later use
|
||||
Main.sAppProcessName = (String) getObjectField(bindData, "processName");
|
||||
String reportedPackageName = appInfo.packageName.equals("android") ? "system" : appInfo.packageName;
|
||||
Utils.logD("processName=" + Main.sAppProcessName +
|
||||
", packageName=" + reportedPackageName + ", appDataDir=" + Main.sAppDataDir);
|
||||
|
||||
if (XposedBlackListHooker.shouldDisableHooks(reportedPackageName)) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import android.app.AndroidAppHelper;
|
|||
import android.app.LoadedApk;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.res.CompatibilityInfo;
|
||||
import android.util.Log;
|
||||
|
||||
import com.elderdrivers.riru.common.KeepMembers;
|
||||
|
||||
|
|
@ -58,13 +59,21 @@ public class LoadedApkConstructorHooker implements KeepMembers {
|
|||
return;
|
||||
}
|
||||
|
||||
// mIncludeCode checking should go ahead of loadedPackagesInProcess added checking
|
||||
if (!getBooleanField(loadedApk, "mIncludeCode")) {
|
||||
logD("LoadedApk#<init> mIncludeCode == false: " + mAppDir);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!loadedPackagesInProcess.add(packageName)) {
|
||||
logD("LoadedApk#<init> has been loaded before, skip: " + mAppDir);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!getBooleanField(loadedApk, "mIncludeCode")) {
|
||||
logD("LoadedApk#<init> mIncludeCode == false: " + mAppDir);
|
||||
// OnePlus magic...
|
||||
if (Log.getStackTraceString(new Throwable()).
|
||||
contains("android.app.ActivityThread$ApplicationThread.schedulePreload")) {
|
||||
logD("LoadedApk#<init> maybe oneplus's custom opt, skip");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ import de.robv.android.xposed.XC_MethodHook;
|
|||
import de.robv.android.xposed.XSharedPreferences;
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
|
||||
import static com.elderdrivers.riru.xposed.util.FileUtils.IS_USING_PROTECTED_STORAGE;
|
||||
import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
|
||||
import static de.robv.android.xposed.XposedInit.INSTALLER_PACKAGE_NAME;
|
||||
|
||||
public class XposedBlackListHooker {
|
||||
|
||||
public static final String BLACK_LIST_PACKAGE_NAME = "com.flarejune.xposedblacklist";
|
||||
private static final boolean IS_USING_PROTECTED_STORAGE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
|
||||
private static final String BLACK_LIST_PREF_NAME = "list";
|
||||
private static final String PREF_KEY_BLACK_LIST = "blackList";
|
||||
public static final String PREF_FILE_PATH = (IS_USING_PROTECTED_STORAGE ? "/data/user_de/0/" : "/data/data")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
package com.elderdrivers.riru.xposed.util;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
|
||||
public class FileUtils {
|
||||
|
||||
public static final boolean IS_USING_PROTECTED_STORAGE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
|
||||
|
||||
/**
|
||||
* Delete a file or a directory and its children.
|
||||
*
|
||||
* @param file The directory to delete.
|
||||
* @throws IOException Exception when problem occurs during deleting the directory.
|
||||
*/
|
||||
public static void delete(File file) throws IOException {
|
||||
|
||||
for (File childFile : file.listFiles()) {
|
||||
|
||||
if (childFile.isDirectory()) {
|
||||
delete(childFile);
|
||||
} else {
|
||||
if (!childFile.delete()) {
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!file.delete()) {
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
|
||||
public static String readLine(File file) {
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
|
||||
return reader.readLine();
|
||||
} catch (Throwable throwable) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeLine(File file, String line) {
|
||||
try {
|
||||
file.createNewFile();
|
||||
} catch (IOException ex) {
|
||||
}
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
|
||||
writer.write(line);
|
||||
writer.flush();
|
||||
} catch (Throwable throwable) {
|
||||
Utils.logE("error writing line to file " + file + ": " + throwable.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static String getDataPathPrefix() {
|
||||
return IS_USING_PROTECTED_STORAGE ? "/data/user_de/0/" : "/data/data/";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.elderdrivers.riru.xposed.util;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class ProcessUtils {
|
||||
|
||||
/**
|
||||
* a common solution from https://stackoverflow.com/a/21389402
|
||||
* <p>
|
||||
* use {@link com.elderdrivers.riru.xposed.Main#sAppProcessName} to get current process name
|
||||
*/
|
||||
public static String getProcessName(int pid) {
|
||||
BufferedReader cmdlineReader = null;
|
||||
try {
|
||||
cmdlineReader = new BufferedReader(new InputStreamReader(
|
||||
new FileInputStream(
|
||||
"/proc/" + pid + "/cmdline"),
|
||||
"iso-8859-1"));
|
||||
int c;
|
||||
StringBuilder processName = new StringBuilder();
|
||||
while ((c = cmdlineReader.read()) > 0) {
|
||||
processName.append((char) c);
|
||||
}
|
||||
return processName.toString();
|
||||
} catch (Throwable throwable) {
|
||||
Utils.logW("getProcessName: " + throwable.getMessage());
|
||||
} finally {
|
||||
try {
|
||||
if (cmdlineReader != null) {
|
||||
cmdlineReader.close();
|
||||
}
|
||||
} catch (Throwable throwable) {
|
||||
Utils.logE("getProcessName: " + throwable.getMessage());
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static boolean isLastPidAlive(File lastPidFile) {
|
||||
String lastPidInfo = FileUtils.readLine(lastPidFile);
|
||||
try {
|
||||
String[] split = lastPidInfo.split(":", 2);
|
||||
return checkProcessAlive(Integer.parseInt(split[0]), split[1]);
|
||||
} catch (Throwable throwable) {
|
||||
Utils.logW("error when check last pid " + lastPidFile + ": " + throwable.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveLastPidInfo(File lastPidFile, int pid, String processName) {
|
||||
try {
|
||||
if (!lastPidFile.exists()) {
|
||||
lastPidFile.getParentFile().mkdirs();
|
||||
lastPidFile.createNewFile();
|
||||
}
|
||||
} catch (Throwable throwable) {
|
||||
}
|
||||
FileUtils.writeLine(lastPidFile, pid + ":" + processName);
|
||||
}
|
||||
|
||||
public static boolean checkProcessAlive(int pid, String processName) {
|
||||
String existsPrcName = getProcessName(pid);
|
||||
Utils.logW("checking pid alive: " + pid + ", " + processName + ", processName=" + existsPrcName);
|
||||
return existsPrcName.equals(processName);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,6 @@ int doInitHookCap(unsigned int cap);
|
|||
void setupTrampoline();
|
||||
void *genTrampoline(void *hookMethod);
|
||||
|
||||
#define DEFAULT_CAP 100 //size of each trampoline area would be no more than 4k Bytes(one page)
|
||||
#define DEFAULT_CAP 1 //size of each trampoline area would be no more than 4k Bytes(one page)
|
||||
|
||||
#endif //YAHFA_TAMPOLINE_H
|
||||
|
|
|
|||
Loading…
Reference in New Issue