Android下动态加载libUnreal.so
Android下动态加载so库
具体有两种方式:
(1)调用 load(String filename) 方法
System.load("/data/data/com.example.mygame/files/libUnreal.so"); // 传递进去的是全路径
(2)调用 loadLibrary(String libname) 方法
System.loadLibrary("Unreal"); // 传递进去的是so文件的名称
具体的Android系统代码如下:
// https://android.googlesource.com/platform/libcore/+/15d8280/luni/src/main/java/java/lang/System.java public final class System { // 。。。 。。。 /** * Loads and links the dynamic library that is identified through the * specified path. This method is similar to {@link #loadLibrary(String)}, * but it accepts a full path specification whereas {@code loadLibrary} just * accepts the name of the library to load. * * @param pathName * the path of the file to be loaded. */ public static void load(String pathName) { Runtime.getRuntime().load(pathName, VMStack.getCallingClassLoader()); } /** * Loads and links the library with the specified name. The mapping of the * specified library name to the full path for loading the library is * implementation-dependent. * * @param libName * the name of the library to load. * @throws UnsatisfiedLinkError * if the library could not be loaded. */ public static void loadLibrary(String libName) { Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader()); } // 。。。 。。。 } // https://android.googlesource.com/platform/libcore/+/15d8280/luni/src/main/java/java/lang/Runtime.java public class Runtime { // 。。。 。。。 /** * Loads and links the dynamic library that is identified through the * specified path. This method is similar to {@link #loadLibrary(String)}, * but it accepts a full path specification whereas {@code loadLibrary} just * accepts the name of the library to load. * * @param pathName * the absolute (platform dependent) path to the library to load. * @throws UnsatisfiedLinkError * if the library can not be loaded. */ public void load(String pathName) { load(pathName, VMStack.getCallingClassLoader()); } /* * Loads and links the given library without security checks. */ void load(String pathName, ClassLoader loader) { if (pathName == null) { throw new NullPointerException("pathName == null"); } String error = doLoad(pathName, loader); if (error != null) { throw new UnsatisfiedLinkError(error); } } /** * Loads and links the library with the specified name. The mapping of the * specified library name to the full path for loading the library is * implementation-dependent. * * @param libName * the name of the library to load. * @throws UnsatisfiedLinkError * if the library can not be loaded. */ public void loadLibrary(String libName) { loadLibrary(libName, VMStack.getCallingClassLoader()); } /* * Searches for a library, then loads and links it without security checks. */ void loadLibrary(String libraryName, ClassLoader loader) { if (loader != null) { String filename = loader.findLibrary(libraryName); if (filename == null) { throw new UnsatisfiedLinkError("Couldn't load " + libraryName + " from loader " + loader + ": findLibrary returned null"); } String error = doLoad(filename, loader); if (error != null) { throw new UnsatisfiedLinkError(error); } return; } String filename = System.mapLibraryName(libraryName); // 实现是在System.c里面,返回so的文件名。例如System.mapLibraryName('Unreal') 返回的是libUnreal.so List<String> candidates = new ArrayList<String>(); String lastError = null; for (String directory : mLibPaths) { String candidate = directory + filename; candidates.add(candidate); if (IoUtils.canOpenReadOnly(candidate)) { String error = doLoad(candidate, loader); if (error == null) { return; // We successfully loaded the library. Job done. } lastError = error; } } if (lastError != null) { throw new UnsatisfiedLinkError(lastError); } throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates); } // 。。。 。。。 private String doLoad(String name, ClassLoader loader) { // Android apps are forked from the zygote, so they can't have a custom LD_LIBRARY_PATH, // which means that by default an app's shared library directory isn't on LD_LIBRARY_PATH. // The PathClassLoader set up by frameworks/base knows the appropriate path, so we can load // libraries with no dependencies just fine, but an app that has multiple libraries that // depend on each other needed to load them in most-dependent-first order. // We added API to Android's dynamic linker so we can update the library path used for // the currently-running process. We pull the desired path out of the ClassLoader here // and pass it to nativeLoad so that it can call the private dynamic linker API. // We didn't just change frameworks/base to update the LD_LIBRARY_PATH once at the // beginning because multiple apks can run in the same process and third party code can // use its own BaseDexClassLoader. // We didn't just add a dlopen_with_custom_LD_LIBRARY_PATH call because we wanted any // dlopen(3) calls made from a .so's JNI_OnLoad to work too. // So, find out what the native library search path is for the ClassLoader in question... String ldLibraryPath = null; if (loader != null && loader instanceof BaseDexClassLoader) { ldLibraryPath = ((BaseDexClassLoader) loader).getLdLibraryPath(); } // nativeLoad should be synchronized so there's only one LD_LIBRARY_PATH in use regardless // of how many ClassLoaders are in the system, but dalvik doesn't support synchronized // internal natives. synchronized (this) { return nativeLoad(name, loader, ldLibraryPath); } } // TODO: should be synchronized, but dalvik doesn't support synchronized internal natives. private static native String nativeLoad(String filename, ClassLoader loader, String ldLibraryPath); // 注:该方法为c++实现 // 。。。 。。。 } // https://android.googlesource.com/platform/libcore/+/master/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java // https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java /** * Base class for common functionality between various dex-based * {@link ClassLoader} implementations. */ public class BaseDexClassLoader extends ClassLoader { /** * Hook for customizing how dex files loads are reported. * * This enables the framework to monitor the use of dex files. The * goal is to simplify the mechanism for optimizing foreign dex files and * enable further optimizations of secondary dex files. * * The reporting happens only when new instances of BaseDexClassLoader * are constructed and will be active only after this field is set with * {@link BaseDexClassLoader#setReporter}. */ /* @NonNull */ private static volatile Reporter reporter = null; @UnsupportedAppUsage private final DexPathList pathList; // 。。。 。。。 public BaseDexClassLoader(String dexPath, String librarySearchPath, ClassLoader parent, ClassLoader[] sharedLibraryLoaders, ClassLoader[] sharedLibraryLoadersAfter, boolean isTrusted) { super(parent); // Setup shared libraries before creating the path list. ART relies on the class loader // hierarchy being finalized before loading dex files. this.sharedLibraryLoaders = sharedLibraryLoaders == null ? null : Arrays.copyOf(sharedLibraryLoaders, sharedLibraryLoaders.length); this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted); // 。。。 。。。 } public BaseDexClassLoader(ByteBuffer[] dexFiles, String librarySearchPath, ClassLoader parent) { super(parent); this.sharedLibraryLoaders = null; this.sharedLibraryLoadersAfter = null; this.pathList = new DexPathList(this, librarySearchPath); this.pathList.initByteBufferDexPath(dexFiles); // Run background verification after having set 'pathList'. this.pathList.maybeRunBackgroundVerification(this); } @SystemApi(client = MODULE_LIBRARIES) public void addNativePath(@NonNull Collection<String> libPaths) { pathList.addNativePath(libPaths); } @Override public String findLibrary(String name) { return pathList.findLibrary(name); } /** * Returns colon-separated set of directories where libraries should be * searched for first, before the standard set of directories. * * @return colon-separated set of search directories * * @hide */ @UnsupportedAppUsage @SystemApi(client = MODULE_LIBRARIES) public @NonNull String getLdLibraryPath() { StringBuilder result = new StringBuilder(); for (File directory : pathList.getNativeLibraryDirectories()) { if (result.length() > 0) { result.append(':'); } result.append(directory); } return result.toString(); } // 。。。 。。。 } // https://android.googlesource.com/platform/libcore/+/master/dalvik/src/main/java/dalvik/system/DexPathList.java // https://www.androidos.net.cn/android/9.0.0_r8/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java public final class DexPathList { // 。。。 。。。 /** List of native library path elements. */ // Some applications rely on this field being an array or we'd use a final list here @UnsupportedAppUsage /* package visible for testing */ NativeLibraryElement[] nativeLibraryPathElements; /** List of application native library directories. */ @UnsupportedAppUsage private final List<File> nativeLibraryDirectories; /** List of system native library directories. */ @UnsupportedAppUsage private final List<File> systemNativeLibraryDirectories; // 。。。 。。。 private List<File> getAllNativeLibraryDirectories() { List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories); allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories); return allNativeLibraryDirectories; } public DexPathList(ClassLoader definingContext, String librarySearchPath) { if (definingContext == null) { throw new NullPointerException("definingContext == null"); } this.definingContext = definingContext; this.nativeLibraryDirectories = splitPaths(librarySearchPath, false); this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true); // 系统的 "java.library.path" 路径 /system/lib/; /vendor/lib/; /product/lib/ this.nativeLibraryPathElements = makePathElements(getAllNativeLibraryDirectories()); } DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory, boolean isTrusted) { if (definingContext == null) { throw new NullPointerException("definingContext == null"); } if (dexPath == null) { throw new NullPointerException("dexPath == null"); } if (optimizedDirectory != null) { if (!optimizedDirectory.exists()) { throw new IllegalArgumentException( "optimizedDirectory doesn't exist: " + optimizedDirectory); } if (!(optimizedDirectory.canRead() && optimizedDirectory.canWrite())) { throw new IllegalArgumentException( "optimizedDirectory not readable/writable: " + optimizedDirectory); } } this.definingContext = definingContext; ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); // save dexPath for BaseDexClassLoader this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext, isTrusted); // Native libraries may exist in both the system and // application library paths, and we use this search order: // // 1. This class loader's library path for application libraries (librarySearchPath): // 1.1. Native library directories // 1.2. Path to libraries in apk-files // 2. The VM's library path from the system property for system libraries // also known as java.library.path // // This order was reversed prior to Gingerbread; see http://b/2933456. this.nativeLibraryDirectories = splitPaths(librarySearchPath, false); this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true); // 系统的 "java.library.path" 路径 /system/lib/; /vendor/lib/; /product/lib/ this.nativeLibraryPathElements = makePathElements(getAllNativeLibraryDirectories()); if (suppressedExceptions.size() > 0) { this.dexElementsSuppressedExceptions = suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]); } else { dexElementsSuppressedExceptions = null; } } /** * For BaseDexClassLoader.getLdLibraryPath. */ public List<File> getNativeLibraryDirectories() { return nativeLibraryDirectories; } public String findLibrary(String libraryName) { String fileName = System.mapLibraryName(libraryName); for (NativeLibraryElement element : nativeLibraryPathElements) { String path = element.findNativeLibrary(fileName); if (path != null) { return path; } } return null; } /** * Adds a collection of library paths from which to load native libraries. Paths can be absolute * native library directories (i.e. /data/app/foo/lib/arm64) or apk references (i.e. * /data/app/foo/base.apk!/lib/arm64). * * Note: This method will attempt to dedupe elements. * Note: This method replaces the value of {@link #nativeLibraryPathElements} */ @UnsupportedAppUsage public void addNativePath(Collection<String> libPaths) { // 添加新的查找路径 例如:/data/app/com.example.mygame-S72Tv4IGEow5LtbwxPQHpQ==/lib/arm64 if (libPaths.isEmpty()) { return; } List<File> libFiles = new ArrayList<>(libPaths.size()); for (String path : libPaths) { libFiles.add(new File(path)); } ArrayList<NativeLibraryElement> newPaths = new ArrayList<>(nativeLibraryPathElements.length + libPaths.size()); newPaths.addAll(Arrays.asList(nativeLibraryPathElements)); for (NativeLibraryElement element : makePathElements(libFiles)) { if (!newPaths.contains(element)) { newPaths.add(element); } } nativeLibraryPathElements = newPaths.toArray(new NativeLibraryElement[newPaths.size()]); } // 。。。 。。。 }

在 Java 中,JVM 加载的是 .class 文件,而在 Android 中,Dalvik或ART虚拟机加载的是 dex 文件。
这里的 dex 文件不仅仅指 .dex 后缀的文件,而是指携带 classed.dex 项的任何文件(例如:jar / zip / apk)。
在 Android 中,Java 代码的编译产物是 dex 格式字节码,所以 Android 系统提供了 BaseDexClassLoader 类加载器,用于从 dex 文件中加载类。
PathClassLoader & DexClassLoader 是 BaseDexClassLoader 的子类,但它们都没有重写方法,所以主要的逻辑还是在 BaseDexClassLoader。
UE5动态加载libUnreal.so
GameApplication.java
import android.app.Application; import android.content.Context; import android.content.res.Configuration; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.OnLifecycleEvent; import androidx.lifecycle.ProcessLifecycleOwner; public class GameApplication extends Application implements LifecycleObserver { // 。。。 。。。 private static Context context; // 。。。 。。。 @Override public void onCreate() { super.onCreate(); GameApplication.context = getApplicationContext(); // 。。。 。。。。 } public static Context getAppContext() { return GameApplication.context; } // 。。。 。。。 }
GameActivity.java
import dalvik.system.BaseDexClassLoader; import java.util.Collection; import java.util.Set; public class GameActivity extends NativeActivity implements SurfaceHolder.Callback2, SensorEventListener, Logger.ILoggerCallback, ComponentCallbacks2 { // 。。。 。。。 @Override public ClassLoader getClassLoader() { ClassLoader baseClassLoader = super.getClassLoader(); try { BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) baseClassLoader; if (dexClassLoader.findLibrary("Unreal") == null) { Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList"); pathListField.setAccessible(true); Object pathListObj = pathListField.get(dexClassLoader); Method addNativePathMethod = pathListObj.getClass().getMethod("addNativePath", Collection.class); String filesDir = getApplicationContext().getFilesDir().getAbsolutePath(); Log.verbose("Adding '" + filesDir + "' to GameActivity BaseDexClassLoader"); Collection<String> paths = Arrays.asList(filesDir); addNativePathMethod.invoke(pathListObj, paths); } } catch (Exception e) { Log.warn("Failed to add native library path due to " + e); } return baseClassLoader; } // 。。。 。。。 static { // We need to decide on enabling memory tracing before loading native code. // It's not trivial to get a full command line before GameActivity is constructed, so fallback to a sentinel file. try { Context context = GameApplication.getAppContext(); Bundle bundle = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA).metaData; boolean bShipping = bundle.containsKey("com.epicgames.unreal.GameActivity.BuildConfiguration") && bundle.getString("com.epicgames.unreal.GameActivity.BuildConfiguration").equals("Shipping"); if (!bShipping) { boolean bEnableFullMemoryTracing = new File(GameApplication.getAppContext().getFilesDir() + "/UEEnableMemoryTracing.txt").exists(); boolean bEnableLightlMemoryTracing = new File(GameApplication.getAppContext().getFilesDir() + "/UEEnableMemoryTracingLight.txt").exists(); if (bEnableFullMemoryTracing) { Os.setenv("UEEnableMemoryTracing", "full", true); } else if (bEnableLightlMemoryTracing) { Os.setenv("UEEnableMemoryTracing", "light", true); } } } catch (ErrnoException | NameNotFoundException e) { Log.debug(e.toString()); Log.debug("Cannot set \"UEEnableMemoryTracing\" environment variable"); } if (!isUnderTest()) { System.load(GameApplication.getAppContext().getFilesDir().getAbsolutePath() + "/libUnreal.so"); } } }
注1:上面从/data/data/com.example.mygame/files/libUnreal.so外部路径加载so文件,需要保证包中不要打入libUnreal.so,否则在JNI层还是会优先加载包中的libUnreal.so导致崩溃
#00 0x000000000fcb099c /data/app/~~SQe_fczr1HZkR1FQUrh3yg==/com.tencent.mf.uam-o1lQw3oYq0yWYpV2OuG_qw==/lib/arm64/libUnreal.so [BuildId:7ae7636d6f836b996106f4bfa0c814ecd601fb76] GetEnv C:/Users/Lenovo/AppData/Local/Android/Sdk/ndk/25.1.8937393/toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/include/jni.h:1062:14 AndroidJavaEnv::GetJavaEnv(bool) D://UnrealEngine/Engine/Source/Runtime/Core/Private/Android/AndroidJavaEnv.cpp:165:37 #01 0x0000000006f7e658 /data/app/~~SQe_fczr1HZkR1FQUrh3yg==/com.tencent.mf.uam-o1lQw3oYq0yWYpV2OuG_qw==/lib/arm64/libUnreal.so [BuildId:7ae7636d6f836b996106f4bfa0c814ecd601fb76] GetJavaEnv D://UnrealEngine/Engine/Source/Runtime/ApplicationCore/Public/Android/AndroidApplication.h:66:10 AndroidMain(android_app*) D://UnrealEngine/Engine/Source/Runtime/Launch/Private/Android/LaunchAndroid.cpp:442:2 #02 0x0000000006f84b80 /data/app/~~SQe_fczr1HZkR1FQUrh3yg==/com.tencent.mf.uam-o1lQw3oYq0yWYpV2OuG_qw==/lib/arm64/libUnreal.so [BuildId:7ae7636d6f836b996106f4bfa0c814ecd601fb76] android_main D://UnrealEngine/Engine/Source/Runtime/Launch/Private/Android/LaunchAndroid.cpp:857:2 #03 0x0000000006fb98a0 /data/app/~~SQe_fczr1HZkR1FQUrh3yg==/com.example.mygame-o1lQw3oYq0yWYpV2OuG_qw==/lib/arm64/libUnreal.so [BuildId:7ae7636d6f836b996106f4bfa0c814ecd601fb76] android_app_entry C:/Users/Lenovo/AppData/Local/Android/Sdk/ndk/21.4.7075529/sources/android/native_app_glue/android_native_app_glue.c:233:5 #04 0x00000000000ebe70 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+264) [BuildId:a85ef5fb74e0282813cd1edb10854577] #05 0x000000000008b7f0 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) [BuildId:a85ef5fb74e0282813cd1edb10854577]
注2:libUnreal.so要放在App的沙盒目录且设置为可执行,否则可能由于权限问题加载失败。可使用代码为libUnreal.so设置可执行权限
// 方式一: File file = new File("/data/data/com.example.mygame/files/libUnreal.so"); boolean success = file.setExecutable(true); // 为所有用户添加可执行权限 // 如果只为“所有者”添加权限,使用 file.setExecutable(true, true); // 第1个参数:是否可执行; 第2个参数(可选):是否只对所有者设置(true 为只对 owner,false 为所有用户) // 方式二: import java.nio.file.*; import java.nio.file.attribute.*; import java.util.*; Path path = Paths.get("/data/data/com.example.mygame/files/libUnreal.so"); Set<PosixFilePermission> perms = Files.getPosixFilePermissions(path); // 添加可执行权限 perms.add(PosixFilePermission.OWNER_EXECUTE); perms.add(PosixFilePermission.GROUP_EXECUTE); perms.add(PosixFilePermission.OTHERS_EXECUTE); // 可按需添加其他权限 Files.setPosixFilePermissions(path, perms); // 方式三: Path path = Paths.get("/data/data/com.example.mygame/files/libUnreal.so"); Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rwxr-xr-x"); Files.setPosixFilePermissions(path, perms);
注3:打包脚本要去掉-archive参数 // 会在Binaries\Android目录中生成剔除libUnreal.so的apk包
"cmd.exe" /c ""D:/UnrealEngine/Engine/Build/BatchFiles/RunUAT.bat" -ScriptsForProject="D:/MyGame/MyGame.uproject" Turnkey -command=VerifySdk -platform=Android -UpdateIfNeeded -EditorIO -EditorIOPort=52576 -project="D:/MyGame/MyGame.uproject" BuildCookRun -nop4 -utf8output -nocompileeditor -skipbuildeditor -cook -project="D:/MyGame/MyGame.uproject" -unrealexe="D:\UnrealEngine\Engine\Binaries\Win64\UnrealEditor-Win64-DebugGame-Cmd.exe" -platform=Android -cookflavor=ASTC -stage -archive -package -build -pak -iostore -compressed -prereqs -archivedirectory="D:/MyGame/Achived" -clientconfig=Development" -nocompile -nocompileuat
浙公网安备 33010602011771号