从源码角度讲讲我对Android和Unity的热更so的理解
Posted 小杨在玩iOS
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从源码角度讲讲我对Android和Unity的热更so的理解相关的知识,希望对你有一定的参考价值。
一、前言
因为有一些功能的修改动到了Unity的源码,需要放到现网去调试,但是通过发包的方式进行测试实在是太重度了,因此特意的研究了一下Unity的加载So的过程,实现对libunity.so和libmono.so两个库文件的热更,方便在现网进行调试。
底层的东西只是浅尝辄止,如果有写的不对的地方欢迎留言指正(Atany)
二、android是怎么读取So的
基于Android 5.1,因为之前查bugly的一个崩溃用的这个版本就直接看了,对比了部分Android O的代码,对于So的读取这块有点差别,但是不影响原理。
2.1 系统API
读取任何的So文件都是通过System类的两个系统API实现
System.load(name)
Loads the native library specified by the filename argument. The filename argument must be an absolute path name.
System.loadLibrary(name)
Loads the native library specified by the libname argument. The libname argument must not contain any platform specific prefix, file extension or path.
两个API的差别是:
1)System.load
是读取一个绝对路径的参数,需要传入So的完整路径;
2)System.loadLibrary
是读取的相对路径,只用传入So的名称即可(例如想要读取libmain.so,传入main即可),在读取So的过程中,会有一个mapLibraryName方法去添加前缀和后缀(main => libmain.so)
2.2 System.load 实现
开始分别讲一讲两个API的实现:
- System.java
/**
* See @link Runtime#load.
*/
public static void load(String pathName)
Runtime.getRuntime().load(pathName, VMStack.getCallingClassLoader());
1)直接调用Runtime.java里面的load方法,并添加了一个参数VMStack.getCallingClassLoader()
,这里获取的是当前加载此类的ClassLoader,在Android里面为PathClassLoader。
2)Runtime.java主要是在处理运行时的一些东西,例如Gc。
3)读取So的路径有一套机制叫做双亲委托,可自行Google。
继续看load方法:
- Runtime.java
/*
* Loads the given shared library using the given ClassLoader.
*/
void load(String absolutePath, ClassLoader loader)
if (absolutePath == null)
throw new NullPointerException("absolutePath == null");
String error = doLoad(absolutePath, loader);
if (error != null)
throw new UnsatisfiedLinkError(error);
这里就很明显了,当绝对路径为空的时候抛出异常,否则就调用doLoad的方法进行加载。(doLoad放在后面讲)
2.3 System.loadLibrary 实现
同System.Load一样,调用Runtime类里面的LoadLibrary方法
- System.java
/**
* See @link Runtime#loadLibrary.
*/
public static void loadLibrary(String libName)
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
- Runtime.Java
/*
* Searches for and loads the given shared library using the given ClassLoader.
*/
void loadLibrary(String libraryName, ClassLoader loader)
if (loader != null)
String filename = loader.findLibrary(libraryName);
if (filename == null)
// It's not necessarily true that the ClassLoader used
// System.mapLibraryName, but the default setup does, and it's
// misleading to say we didn't find "libMyLibrary.so" when we
// actually searched for "liblibMyLibrary.so.so".
throw new UnsatisfiedLinkError(loader + " couldn't find \\"" +
System.mapLibraryName(libraryName) + "\\"");
String error = doLoad(filename, loader);
if (error != null)
throw new UnsatisfiedLinkError(error);
return;
String filename = System.mapLibraryName(libraryName);
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);
这里根据ClassLoader是否存在分了两种情况:
1)当ClasssLoader存在的时候,通过loader的findLibrary()查看目标库所在路径。
2)当ClassLoader不存在的时候,通过mLibPaths加载路径。最终都会调用doLoad加载动态库。
2.3.1 ClassLoader 不存在的情况
- Runtime.java
for (String directory : mLibPaths)
String candidate = directory + filename;
candidates.add(candidate);
mLibPaths通过initLibPaths方法进行初始化:
/**
* Holds the library paths, used for native library lookup.
*/
private final String[] mLibPaths = initLibPaths();
private static String[] initLibPaths()
String javaLibraryPath = System.getProperty("java.library.path");
if (javaLibraryPath == null)
return EmptyArray.STRING;
String[] paths = javaLibraryPath.split(":");
// Add a '/' to the end of each directory so we don't have to do it every time.
for (int i = 0; i < paths.length; ++i)
if (!paths[i].endsWith("/"))
paths[i] += "/";
return paths;
可以看到,其实就是读取系统的Properties,这里读取的是java.library.path这个Properties。
所以如果我们想热更自己的So文件的话,可以通过setProperties的方法修改java.library.path这个配置的路径,让系统先读取我们的目录,再读取默认的目录
把java.library.path的路径打印出来看看:
可以看出各个手机运营商的不同,会有不同的读取路径,但是都会有/system/lib和/vendor/lib 这两个目录。
2.3.1.1 system.getproperty
这里列举一下常见的properties
2.3.2 ClassLoader 存在的情况
通过loader.findLibrary来找到So的路径
首先在PathClassLoader的父类BaseDexClassLoader中找到findLibrary,发现是调用pathList的findLibrary,pathList是DexPathList的实例。
- (PathClassLoader) 父类BaseDexClassLoader.java
@Override
public String findLibrary(String name)
return pathList.findLibrary(name);
- DexPathList.java
public String findLibrary(String libraryName)
String fileName = System.mapLibraryName(libraryName);
for (File directory : nativeLibraryDirectories)
String path = new File(directory, fileName).getPath();
if (IoUtils.canOpenReadOnly(path))
return path;
return null;
通过遍历nativeLibraryDirectories继续找路径,那个这个nativeLibraryDirectories是什么呢?
- DexPathList.java
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory)
...//省略无关代码
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
if (suppressedExceptions.size() > 0)
this.dexElementsSuppressedExceptions =
suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
else
dexElementsSuppressedExceptions = null;
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
在DexPathList的构造方法中,对传入参数libraryPath进行了split,得到了nativeLibraryDirectories。
再看看splitLibraryPath方法:
private static File[] splitLibraryPath(String path)
ArrayList<File> result = splitPaths(path, System.getProperty("java.library.path"), true);
return result.toArray(new File[result.size()]);
把传入的path和java.library.path进行合并,那么这个传入的libraryPath是什么呢?
- BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent)
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
libraryPath是在BaseDexClassLoader的构造方法由上层传入的。
往上就没有找到Java层的源码了,猜测是用C调用的,在创建PathClassLoader实例的时候传入的。
打印出来看发现这个是传入的是Apk内部lib文件夹的路径。
所以Android层面System.Load方法读取So最终的路径是:
1)/data/app/$package-name/lib/arm/
2)/vendor/lib /system/lib
64位系统也可能 /vendor/lib64 /system/lib64
2.4 doload的实现
在了解load方法找寻So文件的路径后,我们来看看真正的加载是怎么实现的。
- Runtime.Java
private String doLoad(String name, ClassLoader loader)
String ldLibraryPath = null;
if (loader != null && loader instanceof BaseDexClassLoader)
ldLibraryPath = ((BaseDexClassLoader) loader).getLdLibraryPath();
synchronized (this)
return nativeLoad(name, loader, ldLibraryPath);
getLdLibraryPath方法
/**
* @hide
*/
public String getLdLibraryPath()
StringBuilder result = new StringBuilder();
for (File directory : pathList.getNativeLibraryDirectories())
if (result.length() > 0)
result.append(':');
result.append(directory);
return result.toString();
@Override public String toString()
return getClass().getName() + "[" + pathList + "]";
getLdLibraryPath方法实际上也是获得nativeLibraryDirectories这个目录。
实际的调用从nativeLoad方法往下看,我在源码全局搜索了Runtime_nativeLoad方法,发现调用到java_lang_Runtime.cc里面的Runtime_nativeLoad方法
- java_lang_Runtime.cc
static jstring Runtime_nativeLoad(JNIEnv* env, jclass, jstring javaFilename, jobject javaLoader, jstring javaLdLibraryPath)
ScopedUtfChars filename(env, javaFilename);
if (filename.c_str() == NULL)
return NULL;
if (javaLdLibraryPath != NULL)
ScopedUtfChars ldLibraryPath(env, javaLdLibraryPath);
if (ldLibraryPath.c_str() == NULL)
return NULL;
void* sym = dlsym(RTLD_DEFAULT, "android_update_LD_LIBRARY_PATH");
if (sym != NULL)
typedef void (*Fn)(const char*);
Fn android_update_LD_LIBRARY_PATH = reinterpret_cast<Fn>(sym);
(*android_update_LD_LIBRARY_PATH)(ldLibraryPath.c_str());
else
LOG(ERROR) << "android_update_LD_LIBRARY_PATH not found; .so dependencies will not work!";
std::string detail;
ScopedObjectAccess soa(env);
StackHandleScope<1> hs(soa.Self());
Handle<mirror::ClassLoader> classLoader(
hs.NewHandle(soa.Decode<mirror::ClassLoader*>(javaLoader)));
JavaVMExt* vm = Runtime::Current()->GetJavaVM();
bool success = vm->LoadNativeLibrary(filename.c_str(), classLoader, &detail);
if (success)
return nullptr;
// Don't let a pending exception from JNI_OnLoad cause a CheckJNI issue with NewStringUTF.
env->ExceptionClear();
return env->NewStringUTF(detail.c_str());
经过一些列的字符串转化之后,调用到了JavaVMExt对象的LoadNativeLibrary
- Jni_internal.cc(Android O for java_vm_ext.cc)
bool JavaVMExt::LoadNativeLibrary(const std::string& path,
Handle<mirror::ClassLoader> class_loader,
std::string* detail)
detail->clear();
// See if we've already loaded this library. If we have, and the class loader
// matches, return successfully without doing anything.
// TODO: for better results we should canonicalize the pathname (or even compare
// inodes). This implementation is fine if everybody is using System.loadLibrary.
SharedLibrary* library;
Thread* self = Thread::Current();
// TODO: move the locking (and more of this logic) into Libraries.
MutexLock mu(self, libraries_lock);
library = libraries->Get(path);
if (library != nullptr)
if (library->GetClassLoader() != class_loader.Get())
// The library will be associated with class_loader. The JNI
// spec says we can't load the same library into more than one
// class loader.
StringAppendF(detail, "Shared library \\"%s\\" already opened by "
"ClassLoader %p; can't open in ClassLoader %p",
path.c_str(), library->GetClassLoader(), class_loader.Get());
LOG(WARNING) << detail;
return false;
VLOG(jni) << "[Shared library \\"" << path << "\\" already loaded in "
<< "ClassLoader " << class_loader.Get() << "]";
if (!library->CheckOnLoadResult())
StringAppendF(detail, "JNI_OnLoad failed on a previous attempt "
"to load \\"%s\\"", path.c_str());
return false;
return true;
// Open the shared library. Because we're using a full path, the system
// doesn't have to search through LD_LIBRARY_PATH. (It may do so to
// resolve this library's dependencies though.)
// Failures here are expected when java.library.path has several entries
// and we have to hunt for the lib.
// Below we dlopen but there is no paired dlclose, this would be necessary if we supported
// class unloading. Libraries will only be unloaded when the reference count (incremented by
// dlopen) becomes zero from dlclose.
// This can execute slowly for a large library on a busy system, so we
// want to switch from kRunnable while it executes. This allows the GC to ignore us.
self->TransitionFromRunnableToSuspended(kWaitingForJniOnLoad);
const char* path_str = path.empty() ? nullptr : path.c_str();
void* handle = dlopen(path_str, RTLD_LAZY);
bool needs_native_bridge = false;
if (handle == nullptr)
if (android::NativeBridgeIsSupported(path_str))
handle = android::NativeBridgeLoadLibrary(path_str, RTLD_LAZY);
needs_native_bridge = true;
self->TransitionFromSuspendedToRunnable();
VLOG(jni) << "[Call to dlopen(\\"" << path << "\\", RTLD_LAZY) returned " << handle << "]";
if (handle == nullptr)
*detail = dlerror();
LOG(ERROR) << "dlopen(\\"" << path << "\\", RTLD_LAZY) failed: " << *detail;
return false;
// Create a new entry.
// TODO: move the locking (and more of this logic) into Libraries.
bool created_library = false;
MutexLock mu(self, libraries_lock);
library = libraries->Get(path);
if (library == nullptr) // We won race to get libraries_lock
library = new SharedLibrary(path, handle, class_loader.Get());
libraries->Put(path, library);
created_library = true;
if (!created_library)
LOG(INFO) << "WOW: we lost a race to add shared library: "
<< "\\"" << path << "\\" ClassLoader=" << class_loader.Get();
return library->CheckOnLoadResult();
VLOG(jni) << "[Added shared library \\"" << path << "\\" for ClassLoader " << class_loader.Get()
<< "]";
bool was_successful = false;
void* sym = nullptr;
if (UNLIKELY(needs_native_bridge))
library->SetNeedsNativeBridge();
sym = library->FindSymbolWithNativeBridge("JNI_OnLoad", nullptr);
else
sym = dlsym(handle, "JNI_OnLoad");
if (sym == nullptr)
VLOG(jni) << "[No JNI_OnLoad found in \\"" << path << "\\"]";
was_successful = true;
else
// Call JNI_OnLoad. We have to override the current class
// loader, which will always be "null" since the stuff at the
// top of the stack is around Runtime.loadLibrary(). (See
// the comments in the JNI FindClass function.)
typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
StackHandleScope<1> hs(self);
Handle<mirror::ClassLoader> old_class_loader(hs.NewHandle(self->GetClassLoaderOverride()));
self->SetClassLoaderOverride(class_loader.Get());
int version = 0;
ScopedThreadStateChange tsc(self, kNative);
VLOG(jni) << "[Calling JNI_OnLoad in \\"" << path << "\\"]";
version = (*jni_on_load)(this, nullptr);
if (runtime->GetTargetSdkVersion() != 0 && runtime->GetTargetSdkVersion() <= 21)
fault_manager.EnsureArtActionInFrontOfSignalChain();
self->SetClassLoaderOverride(old_class_loader.Get());
if (version == JNI_ERR)
StringAppendF(detail, "JNI_ERR returned from JNI_OnLoad in \\"%s\\"", path.c_str());
else if (IsBadJniVersion(version))
StringAppendF(detail, "Bad JNI version returned from JNI_OnLoad in \\"%s\\": %d",
path.c_str(), version);
// It's unwise to call dlclose() here, but we can mark it
// as bad and ensure that future load attempts will fail.
// We don't know how far JNI_OnLoad got, so there could
// be some partially-initialized stuff accessible through
// newly-registered native method calls. We could try to
// unregister them, but that doesn't seem worthwhile.
else
was_successful = true;
VLOG(jni) << "[Returned " << (was_successful ? "successfully" : "failure")
<< " from JNI_OnLoad in \\"" << path << "\\"]";
library->SetResult(was_successful);
return was_successful;
1)开始的时候会去缓存查看是否已经加载过动态库,如果已经加载过会判断上次加载的ClassLoader和这次加载的ClassLoader是否一致,如果不一致则加载失败,就是说不允许不同的ClassLoader加载同一个动态库。
2)之后会通过dlopen打开动态共享库。然后会获取动态库中的JNI_OnLoad方法,如果有的话调用之。最后会通过JNI_OnLoad的返回值确定是否加载成功。
3)非关键的代码就没细看了,源码已贴出来,可自行阅读。
2.5 Android 读取So总结
1、System.loadLibrary会优先查找apk中的so目录,再查找系统目录,系统目录包括:/vendor/lib(64),/system/lib(64) (不同的厂商可能有不同)
2、System.loadLibrary加载过程中会调用目标库的JNI_OnLoad方法,对相关的JNI方法进行注册。
3、Android动态库的加载使用dlopen、dlsylm、dlclose系列函数,通过动态库的句柄和函数名称来调用动态库的函数和变量。
三、Unity是怎么读取相关So的?
Unity读取So分两个部分
1)第一个部分是读取第三方的So,这个部分的读取其实跟Android读取So的过程很像,就不赘述了。
2)我想研究的是,Unity是怎么加载自身使用的So,这里主要是三个so。
- libmain.so
- libunity.so
- libmono.so
3.1 libMain.so
libmain.so这个是Unity启动的时候最开始加载的,先加载了libmain.so,才能继续加载其他相关的so。
1)在UnityNativePlayer.activity中创建了UnityPlayer的实例。(activity的名字不记得了,好像是这个- b-)
2)在UnityPlayer.java 中使用静态块加载Libmain.so
- UnityPlayer.java
static
System.loadLibrary ("main"); // main / NativeActivity helper library
3.1.1网上有个总结static块调用时机说的很详细
当一个类被主动使用时,Java虚拟机就会对其初始化,如下六种情况为主动使用:
1、当创建某个类的新实例时(如通过new或者反射,克隆,反序列化等)
2、当调用某个类的静态方法时
3、当使用某个类或接口的静态字段时
4、当调用Java API中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时
5、当初始化某个子类时
6、当虚拟机启动某个被标明为启动类的类(即包含main方法的那个类)
所以在第一个Activity oncreate的时候,libmain.so就加载进来了。
3.2 libunity.so、libmono.so
这两个so的加载,一度认为是使用Android的System.loadLibrary加载进来的,实际上UnityPlayer.java中确实也有这个相关的loadLibrary方法,但是最后发现其实是没有调用的。
继续寻找,发现另有地方单独编写了这两个So的加载。
- UnityPlayer.Java
在UnityPlayer的构造方法中有一个loadNative方法:
public UnityPlayer(ContextWrapper context)
super(context);
if (context instanceof Activity)
currentActivity = (Activity)context;
mSurfaceManager = new SurfaceManager(this);
mContext = context;
mNativeActivitySupport = (context instanceof android.app.Activity) ? new NativeActivitySupport(context) : null;
mSensors = new Sensors(context, this);
parseSettings ();
//loadNative方法
loadNative(mContext.getApplicationInfo());
loadNative方法传入了当前的Context对象的applicationInfo。
private static void loadNative(ApplicationInfo applicationInfo)
if (NativeLoader.load(applicationInfo.nativeLibraryDir))
UnityPlayerState.setLibrariesLoaded();
else
throw new UnsatisfiedLinkError("Unable to load libraries from libmain.so");
接着查看一下NativeLoader.load方法,是在libmain.so 中的ANativeActivity_onCreate.cpp方法中进行注册的。在JNI_OnLoad中注册了load和unload方法。
- ANativeActivity_onCreate.cpp
static JNINativeMethod sNativeMethods[] =
"load", "(Ljava/lang/String;)Z", (void*)Load ,
"unload", "()Z", (void*)Unload ,
;
EXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
JNIEnv* env = 0;
vm->AttachCurrentThread(&env, 0);
const char* className = "com/unity3d/player/NativeLoader";
jclass clazz = env->FindClass(className);
const size_t num_methods = sizeof(sNativeMethods) / sizeof(sNativeMethods[0]);
if (env->RegisterNatives(clazz, sNativeMethods, num_methods) < 0)
env->FatalError(className);
return -1;
return JNI_VERSION_1_6; // minimum JNI version
看看load方法的实现:
static jboolean Load(JNIEnv* env, jclass clazz, jstring nativeLibraryDir)
LOGD("%s", __FUNCTION__);
char* libdir;
jsize str_len = env->GetStringUTFLength(nativeLibraryDir) + 1;
libdir = (char*)malloc(str_len);
const char* dir = env->GetStringUTFChars(nativeLibraryDir, 0);
memcpy(libdir, dir, str_len);
env->ReleaseStringUTFChars(nativeLibraryDir, dir);
LOGD("nativeLibraryDir '%s' (%i)", libdir, str_len);
#if UNITY_PSM
dlload(env, libdir, "libandroid-bridge.so", &libbridge);
dlload(env, libdir, "libmono-2.0.so", &libmono);
#else
dlload(env, libdir, "libmono.so", &libmono);
#endif
dlload(env, libdir, "libunity.so", &libunity);
#if UNITY_PSM && DEBUGMODE
LOGD("Sleeping 10s - attach debugger now...");
sleep(10);
#endif
free(libdir);
return libmono && libunity;
ok,看到这里就知道了,在UnityPlayer的构造方法中调用loadNative,会加载两个so,一个是libmono.so,另一个是libunity.so。如果是PlayStation的平台的话,还会加载另外的一个libandroid-bridge.so,这个就不是手机范围的事情咯。
继续往下看dllload
static bool dlload(JNIEnv* env, const char* libdir, const char* libname, volatile void** handle_out)
LOGD("Loading '%s'", libname);
if (*handle_out)
LOGD("Already loaded '%s'", libname);
return true;
JavaVM* vm;
if (env->GetJavaVM(&vm) < 0)
env->FatalError("Unable to retrieve Java VM");
return false;
char path[2048];
snprintf(path, sizeof(path)-1, "%s/%s", libdir, libname);
LOGD("Path '%s'", path);
void* handle = dlopen(path, RTLD_LAZY);
if (!handle)
char errorStr[1024];
snprintf(errorStr, sizeof(errorStr), "Unable to load library: %s [%s]", path, dlerror());
env->FatalError(errorStr);
return false;
typedef jint (*JNI_OnLoad_Func)(JavaVM* vm, void* reserved);
JNI_OnLoad_Func OnLoad = (JNI_OnLoad_Func)dlsym(handle, "JNI_OnLoad");
if (OnLoad)
if (OnLoad(vm, 0) > JNI_VERSION_1_6)
// we should throw an exception here...
env->FatalError("Unsupported VM version");
return false;
if (handle_out)
*handle_out = handle;
return true;
其实同Android加载So的流程一样,最后也是通过dlopen这一系列的函数进行加载So的。当然,也不可能不一样。
四、讲讲Unity热更So的实现思路吧
4.1 Android的热更可以通过两种方法
1)使用System.load方法去读取自己目录的绝对路径的So
2)使用System.loadLibrary方法读取So的名称,通过修改java.library.path的路径实现热更。
4.2 Unity 热更So
1)存在明显的入口loadNative,可以通过在这里添加自己的目录进行热更,然后重新编译出classes.jar放到自己的项目中去。
具体的代码就不贴了,因为源码已经讲的蛮清楚了,如果能够拿到Unity的源码,是可以对Unity的原生So进行改动和热更的。当然,如果只是想热更第三方的一些So的话,和4.1中讲的Android热更就是一个套路了。
杨光(atany)原创,转载请注明博主与博文链接,未经博主允许,禁止任何商业用途。
博文地址:http://blog.csdn.net/yang8456211/article/details/78572362
博客地址:http://blog.csdn.net/yang8456211
本文遵循“署名-非商业用途-保持一致”创作公用协议
以上是关于从源码角度讲讲我对Android和Unity的热更so的理解的主要内容,如果未能解决你的问题,请参考以下文章
从源码角度讲讲我对Android和Unity的热更so的理解
Unity3D开发之手游热更,基于GameFramework(GF),涵盖Android|IOS|资源包|打包等功能非专业团队勿扰
Unity3D开发之手游热更,基于GameFramework(GF),涵盖Android|IOS|资源包|打包等功能非专业团队勿扰