refactor: use PatchConfig and optimize module loading

[feat: Local mode runs without background manager](https://github.com/7723mod/NPatch/pulls/2)
This commit is contained in:
NkBe 2025-09-16 01:15:14 +08:00
parent 5bbef84a43
commit ed202bbb10
No known key found for this signature in database
GPG Key ID: 75EF144ED8F4D7B8
3 changed files with 89 additions and 117 deletions

View File

@ -6,40 +6,46 @@ import static org.lsposed.lspatch.share.Constants.ORIGINAL_APK_ASSET_PATH;
import android.app.ActivityThread;
import android.app.LoadedApk;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.res.CompatibilityInfo;
import android.os.Build;
import android.os.RemoteException;
import android.system.Os;
import android.util.Log;
import com.google.gson.Gson;
import org.json.JSONArray;
import org.json.JSONObject;
import org.lsposed.lspatch.loader.util.FileUtils;
import org.lsposed.lspatch.loader.util.XLog;
import org.lsposed.lspatch.service.LocalApplicationService;
import org.lsposed.lspatch.service.RemoteApplicationService;
import org.lsposed.lspatch.share.PatchConfig;
import org.lsposed.lspd.core.Startup;
import org.lsposed.lspd.models.Module;
import org.lsposed.lspd.service.ILSPApplicationService;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import java.util.zip.ZipFile;
import dalvik.system.DexFile;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import hidden.HiddenApiBridge;
@ -57,7 +63,7 @@ public class LSPApplication {
private static LoadedApk stubLoadedApk;
private static LoadedApk appLoadedApk;
private static JSONObject config;
private static PatchConfig config;
public static boolean isIsolated() {
return (android.os.Process.myUid() % PER_USER_RANGE) >= FIRST_APP_ZYGOTE_ISOLATED_UID;
@ -77,29 +83,47 @@ public class LSPApplication {
Log.d(TAG, "Initialize service client");
ILSPApplicationService service;
if (config.optBoolean("useManager")) {
if (config.useManager) {
try {
service = new RemoteApplicationService(context);
List<Module> m = service.getLegacyModulesList();
JSONArray moduleArr = new JSONArray();
for (Module module : m) {
JSONObject moduleObj = new JSONObject();
moduleObj.put("path",module.apkPath);
moduleObj.put("packageName",module.packageName);
moduleArr.put(moduleObj);
}
SharedPreferences shared = context.getSharedPreferences("npatch", Context.MODE_PRIVATE);
shared.edit().putString("modules",moduleArr.toString()).commit();
Log.e(TAG, "Success update module scope");
}catch (Exception e){
Log.e(TAG, "Failed to connect to manager, fallback to fixed local service");
service = new LocalApplicationService(context);
}
} else {
service = new LocalApplicationService(context);
}
disableProfile(context);
Startup.initXposed(false, ActivityThread.currentProcessName(), context.getApplicationInfo().dataDir, service);
Startup.bootstrapXposed();
// WARN: Since it uses `XResource`, the following class should not be initialized
// before forkPostCommon is invoke. Otherwise, you will get failure of XResources
Log.i(TAG, "Load modules");
LSPLoader.initModules(appLoadedApk);
Log.i(TAG, "Modules initialized");
switchAllClassLoader();
SigBypass.doSigBypass(context, config.optInt("sigBypassLevel"));
SigBypass.doSigBypass(context, config.sigBypassLevel);
Log.i(TAG, "LSPatch bootstrap completed");
}
private static Context createLoadedApkWithContext() {
try {
var timeStart = System.currentTimeMillis();
var mBoundApplication = XposedHelpers.getObjectField(activityThread, "mBoundApplication");
stubLoadedApk = (LoadedApk) XposedHelpers.getObjectField(mBoundApplication, "info");
@ -109,13 +133,13 @@ public class LSPApplication {
try (var is = baseClassLoader.getResourceAsStream(CONFIG_ASSET_PATH)) {
BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
config = new JSONObject(streamReader.lines().collect(Collectors.joining()));
} catch (Throwable e) {
Log.e(TAG, "Failed to parse config file", e);
config = new Gson().fromJson(streamReader, PatchConfig.class);
} catch (IOException e) {
Log.e(TAG, "Failed to load config file");
return null;
}
Log.i(TAG, "Use manager: " + config.optBoolean("useManager"));
Log.i(TAG, "Signature bypass level: " + config.optInt("sigBypassLevel"));
Log.i(TAG, "Use manager: " + config.useManager);
Log.i(TAG, "Signature bypass level: " + config.sigBypassLevel);
Path originPath = Paths.get(appInfo.dataDir, "cache/lspatch/origin/");
Path cacheApkPath;
@ -125,9 +149,7 @@ public class LSPApplication {
appInfo.sourceDir = cacheApkPath.toString();
appInfo.publicSourceDir = cacheApkPath.toString();
if (config.has("appComponentFactory")) {
appInfo.appComponentFactory = config.optString("appComponentFactory");
}
appInfo.appComponentFactory = config.appComponentFactory;
if (!Files.exists(cacheApkPath)) {
Log.i(TAG, "Extract original apk");
@ -137,10 +159,13 @@ public class LSPApplication {
Files.copy(is, cacheApkPath);
}
}
cacheApkPath.toFile().setWritable(false);
var mPackages = (Map<?, ?>) XposedHelpers.getObjectField(activityThread, "mPackages");
mPackages.remove(appInfo.packageName);
appLoadedApk = activityThread.getPackageInfoNoCheck(appInfo, compatInfo);
XposedHelpers.setObjectField(mBoundApplication, "info", appLoadedApk);
var activityClientRecordClass = XposedHelpers.findClass("android.app.ActivityThread$ActivityClientRecord", ActivityThread.class.getClassLoader());
@ -165,14 +190,16 @@ public class LSPApplication {
Log.i(TAG, "hooked app initialized: " + appLoadedApk);
var context = (Context) XposedHelpers.callStaticMethod(Class.forName("android.app.ContextImpl"), "createAppContext", activityThread, stubLoadedApk);
if (config.has("appComponentFactory")) {
if (config.appComponentFactory != null) {
try {
context.getClassLoader().loadClass(appInfo.appComponentFactory);
} catch (Throwable e) { // 捕捉更廣泛的類載入錯誤, 可能可以兼容部分加固如 360
Log.w(TAG, "Original AppComponentFactory not found: " + appInfo.appComponentFactory, e);
context.getClassLoader().loadClass(config.appComponentFactory);
} catch (Throwable e) {
Log.w(TAG, "Original AppComponentFactory not found: " + config.appComponentFactory, e);
appInfo.appComponentFactory = null;
}
}
Log.i(TAG,"createLoadedApkWithContext cost: " + (System.currentTimeMillis() - timeStart) + "ms");
return context;
} catch (Throwable e) {
Log.e(TAG, "createLoadedApk", e);
@ -180,55 +207,6 @@ public class LSPApplication {
}
}
public static void disableProfile(Context context) {
final ArrayList<String> codePaths = new ArrayList<>();
var appInfo = context.getApplicationInfo();
var pkgName = context.getPackageName();
if (appInfo == null) return;
if ((appInfo.flags & ApplicationInfo.FLAG_HAS_CODE) != 0) {
codePaths.add(appInfo.sourceDir);
}
if (appInfo.splitSourceDirs != null) {
Collections.addAll(codePaths, appInfo.splitSourceDirs);
}
if (codePaths.isEmpty()) {
// If there are no code paths there's no need to setup a profile file and register with
// the runtime,
return;
}
var profileDir = HiddenApiBridge.Environment_getDataProfilesDePackageDirectory(appInfo.uid / PER_USER_RANGE, pkgName);
var attrs = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("r--------"));
for (int i = codePaths.size() - 1; i >= 0; i--) {
String splitName = i == 0 ? null : appInfo.splitNames[i - 1];
File curProfileFile = new File(profileDir, splitName == null ? "primary.prof" : splitName + ".split.prof").getAbsoluteFile();
Log.d(TAG, "Processing " + curProfileFile.getAbsolutePath());
try {
if (!curProfileFile.exists()) {
Files.createFile(curProfileFile.toPath(), attrs);
continue;
}
if (!curProfileFile.canWrite() && Files.size(curProfileFile.toPath()) == 0) {
Log.d(TAG, "Skip profile " + curProfileFile.getAbsolutePath());
continue;
}
if (curProfileFile.exists() && !curProfileFile.delete()) {
try (var writer = new FileOutputStream(curProfileFile)) {
Log.d(TAG, "Failed to delete, try to clear content " + curProfileFile.getAbsolutePath());
} catch (Throwable e) {
Log.e(TAG, "Failed to delete and clear profile file " + curProfileFile.getAbsolutePath(), e);
}
Os.chmod(curProfileFile.getAbsolutePath(), 00400);
}
} catch (Throwable e) {
Log.e(TAG, "Failed to disable profile file " + curProfileFile.getAbsolutePath(), e);
}
}
}
private static void switchAllClassLoader() {
var fields = LoadedApk.class.getDeclaredFields();
for (Field field : fields) {

View File

@ -1,84 +1,75 @@
package org.lsposed.lspatch.service;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.os.Environment;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Log;
import org.lsposed.lspatch.loader.util.FileUtils;
import org.lsposed.lspatch.share.Constants;
import org.json.JSONArray;
import org.json.JSONObject;
import org.lsposed.lspatch.util.ModuleLoader;
import org.lsposed.lspd.models.Module;
import org.lsposed.lspd.service.ILSPApplicationService;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipFile;
public class LocalApplicationService extends ILSPApplicationService.Stub {
private static final String TAG = "NPatch";
private final List<Module> modules = new ArrayList<>();
private final List<Module> cachedModule;
public LocalApplicationService(Context context){
SharedPreferences shared = context.getSharedPreferences("npatch", Context.MODE_PRIVATE);
cachedModule = new ArrayList<>();
try {
for (var name : context.getAssets().list("lspatch/modules")) {
String packageName = name.substring(0, name.length() - 4);
String modulePath = context.getCacheDir() + "/lspatch/" + packageName + "/";
String cacheApkPath;
try (ZipFile sourceFile = new ZipFile(context.getPackageResourcePath())) {
cacheApkPath = modulePath + sourceFile.getEntry(Constants.EMBEDDED_MODULES_ASSET_PATH + name).getCrc() + ".apk";
}
if (!Files.exists(Paths.get(cacheApkPath))) {
Log.i(TAG, "Extract module apk: " + packageName);
FileUtils.deleteFolderIfExists(Paths.get(modulePath));
Files.createDirectories(Paths.get(modulePath));
try (var is = context.getAssets().open("lspatch/modules/" + name)) {
Files.copy(is, Paths.get(cacheApkPath));
JSONArray mArr = new JSONArray(shared.getString("modules", "{}"));
Log.i(TAG,"use fixed local application service:"+shared.getString("modules", "{}"));
for (int i = 0; i < mArr.length(); i++) {
JSONObject mObj = mArr.getJSONObject(i);
Module m = new Module();
String path = mObj.getString("path");
String packageName = mObj.getString("packageName");
m.apkPath = path;
m.packageName = packageName;
if (!new File(m.apkPath).exists()){
Log.i("NPatch","Module:" + m.packageName + " path not available, reset.");
try {
ApplicationInfo info = context.getPackageManager().getApplicationInfo(m.packageName, 0);
m.apkPath = info.sourceDir;
Log.i("NPatch","Module:" + m.packageName + " path reset to " + m.apkPath);
}catch (Exception e){
Log.e("NPatch",Log.getStackTraceString(e));
}
}
var module = new Module();
module.apkPath = cacheApkPath;
module.packageName = packageName;
module.file = ModuleLoader.loadModule(cacheApkPath);
modules.add(module);
m.file = ModuleLoader.loadModule(m.apkPath);
cachedModule.add(m);
}
} catch (IOException e) {
Log.e(TAG, "Error when initializing LocalApplicationServiceClient", e);
}catch (Exception e){
Log.e(TAG,Log.getStackTraceString(e));
}
}
@Override
public boolean isLogMuted() throws RemoteException {
return false;
public List<Module> getLegacyModulesList() throws RemoteException {
return cachedModule;
}
@Override
public List<Module> getLegacyModulesList() {
return modules;
}
@Override
public List<Module> getModulesList() {
public List<Module> getModulesList() throws RemoteException {
return new ArrayList<>();
}
@Override
public String getPrefsPath(String packageName) {
public String getPrefsPath(String packageName) throws RemoteException {
return new File(Environment.getDataDirectory(), "data/" + packageName + "/shared_prefs/").getAbsolutePath();
}
@Override
public ParcelFileDescriptor requestInjectedManagerBinder(List<IBinder> binder) {
public ParcelFileDescriptor requestInjectedManagerBinder(List<IBinder> binder) throws RemoteException {
return null;
}
@ -86,4 +77,9 @@ public class LocalApplicationService extends ILSPApplicationService.Stub {
public IBinder asBinder() {
return this;
}
@Override
public boolean isLogMuted() throws RemoteException {
return false;
}
}

View File

@ -14,7 +14,6 @@ import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import android.widget.Toast;
import org.lsposed.lspatch.share.Constants;
import org.lsposed.lspd.models.Module;
@ -48,7 +47,7 @@ public class RemoteApplicationService implements ILSPApplicationService {
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
Log.i(TAG, "Manager binder received");
service = Stub.asInterface(binder);
service = ILSPApplicationService.Stub.asInterface(binder);
latch.countDown();
}
@ -76,7 +75,6 @@ public class RemoteApplicationService implements ILSPApplicationService {
if (!success) throw new TimeoutException("Bind service timeout");
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException |
InterruptedException | TimeoutException e) {
Toast.makeText(context, "Unable to connect to Manager", Toast.LENGTH_SHORT).show();
var r = new RemoteException("Failed to get manager binder");
r.initCause(e);
throw r;