从源码角度讲讲我对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|资源包|打包等功能非专业团队勿扰

Unity热更模块基于 HybridCLR + Addressable

Unity 接入 ILRuntime 热更方案

Unity 接入 ILRuntime 热更方案