Fix memory issues when hooking too many methods in one process

This commit is contained in:
solohsu 2019-01-25 22:08:42 +08:00
parent 045104d1bf
commit d57d602420
9 changed files with 206 additions and 28 deletions

View File

@ -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

View File

@ -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!");

View File

@ -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);

View File

@ -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;

View File

@ -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;
}

View File

@ -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")

View File

@ -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/";
}
}

View File

@ -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);
}
}

View File

@ -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