Android开发——关于32位与64位so的加载问题
Android加载so文件的机制
有时候会为了减少apk的大小,只设置支持 “armeabi-v7a
” so库架构,然而在apk在安装的过程中,系统就会对apk进行解析根据里面so文件类型,确定这个apk安装是在32 还是 64位的虚拟机上,如果是32位虚拟机那么就不能使用64位so,如果是64位虚拟机也不能使用32位so。而64位设备可以提供32和64位两种虚拟机,根据apk选择开启哪一种,因此说64位设备兼容32的so库;加载so机制分下面四种情况:
1. 假设apk的lib目录放置了32和64位两种so,那么安装时根据当前设备的cpu架构从上到下筛选(X86 > arm64 > arm32),一旦发现lib里面有和设备匹配的so文件,那么直接选定这种架构为标准。比如当前设备是64位并且发现lib有一个64位的so,那么apk会拷贝lib下所有64位的so文件到data/data/packageName/lib64/目录下;
2. apk的lib目录只有32位的so,由于64位设备时兼容32位的库,所以安装时根据当前设备的cpu架构从上到下筛选(X86 > arm64 > arm32),能够正常的运行在32位设备和大部分64位设备上(目前没有遇到不正常运行的状态)。
3. apk的lib目录只有64位的so时,那这个apk只能运行在64位的设备上。
4. apk的lib不放任何的so文件,全部动态加载时,安装在32位设备就只能加载32位so,安装在64位的设备系统会默认将apk运行在64位虚拟机,不过可以通过指定当前加载。
TODO:
1. 如果apk的lib不放任何的so文件,并在build.grade中通过abiFilters设置过滤,是否能起到加载特定的so的目的
ndk {
abiFilters 'armeabi-v7a'
}
Android so的动态加载
有时会为了减少apk的大小,会将so放到服务器上,比如我们的小游戏大厅中,在引擎框架下,会将每个小游戏编译成so,再启动相应的游戏时去下载这个游戏的so,然后再去加载so。
对于动态加载so方式的apk需要通过上传的方式进行处理,然后下载使用合适的so,下面来看下so的动态加载过程(8.0.0_r4):
/** 1625 * Loads the native library specified by the <code>libname</code> 1626 * argument. The <code>libname</code> argument must not contain any platform 1627 * specific prefix, file extension or path. If a native library 1628 * called <code>libname</code> is statically linked with the VM, then the 1629 * JNI_OnLoad_<code>libname</code> function exported by the library is invoked. 1630 * See the JNI Specification for more details. 1631 * 1632 * Otherwise, the libname argument is loaded from a system library 1633 * location and mapped to a native library image in an implementation- 1634 * dependent manner. 1635 * <p> 1636 * The call <code>System.loadLibrary(name)</code> is effectively 1637 * equivalent to the call 1638 * <blockquote><pre> 1639 * Runtime.getRuntime().loadLibrary(name) 1640 * </pre></blockquote> 1641 * 1642 * @param libname the name of the library. 1643 * @exception SecurityException if a security manager exists and its 1644 * <code>checkLink</code> method doesn't allow 1645 * loading of the specified dynamic library 1646 * @exception UnsatisfiedLinkError if either the libname argument 1647 * contains a file path, the native library is not statically 1648 * linked with the VM, or the library cannot be mapped to a 1649 * native library image by the host system. 1650 * @exception NullPointerException if <code>libname</code> is 1651 * <code>null</code> 1652 * @see java.lang.Runtime#loadLibrary(java.lang.String) 1653 * @see java.lang.SecurityManager#checkLink(java.lang.String) 1654 */ 1655 @CallerSensitive 1656 public static void loadLibrary(String libname) { 1657 Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname); 1658 }
上面的源码很简单,直接看Runtime.loadLibrary0具体实现:
998 synchronized void loadLibrary0(ClassLoader loader, String libname) { 999 if (libname.indexOf((int)File.separatorChar) != -1) { 1000 throw new UnsatisfiedLinkError( 1001 "Directory separator should not appear in library name: " + libname); 1002 } 1003 String libraryName = libname; 1004 if (loader != null) {//ClassLoader非空时,利用ClassLoader的findLibrary()方法来获取library的path 1005 String filename = loader.findLibrary(libraryName); 1006 if (filename == null) { 1007 // It's not necessarily true that the ClassLoader used 1008 // System.mapLibraryName, but the default setup does, and it's 1009 // misleading to say we didn't find "libMyLibrary.so" when we 1010 // actually searched for "liblibMyLibrary.so.so". 1011 throw new UnsatisfiedLinkError(loader + " couldn't find \"" + 1012 System.mapLibraryName(libraryName) + "\""); 1013 } 1014 String error = doLoad(filename, loader); 1015 if (error != null) { 1016 throw new UnsatisfiedLinkError(error); 1017 } 1018 return; 1019 } 1020 //当loader为空时, 则从默认目录mLibPaths下来查找是否存在该动态库; 1021 String filename = System.mapLibraryName(libraryName); 1022 List<String> candidates = new ArrayList<String>(); 1023 String lastError = null; 1024 for (String directory : getLibPaths()) { 1025 String candidate = directory + filename; 1026 candidates.add(candidate); 1027 1028 if (IoUtils.canOpenReadOnly(candidate)) { 1029 String error = doLoad(candidate, loader); 1030 if (error == null) { 1031 return; // We successfully loaded the library. Job done. 1032 } 1033 lastError = error; 1034 } 1035 } 1036 1037 if (lastError != null) { 1038 throw new UnsatisfiedLinkError(lastError); 1039 } 1040 throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates); 1041 }
1. loader为null 时在做什么?
a. mapLibraryName会调用System的native方法,System_mapLibraryName是其具体的实现,改方法是将xxx动态库的名字转换为libxxx.so(目前还不知道为啥要这么麻烦通过native来实现)。
147JNIEXPORT jstring JNICALL 148System_mapLibraryName(JNIEnv *env, jclass ign, jstring libname) 149{ 150 int len; 151 int prefix_len = (int) strlen(JNI_LIB_PREFIX); 152 int suffix_len = (int) strlen(JNI_LIB_SUFFIX); 153 154 jchar chars[256]; 155 if (libname == NULL) { 156 JNU_ThrowNullPointerException(env, 0); 157 return NULL; 158 } 159 len = (*env)->GetStringLength(env, libname); 160 if (len > 240) { 161 JNU_ThrowIllegalArgumentException(env, "name too long"); 162 return NULL; 163 } 164 cpchars(chars, JNI_LIB_PREFIX, prefix_len); 165 (*env)->GetStringRegion(env, libname, 0, len, chars + prefix_len); 166 len += prefix_len; 167 cpchars(chars + len, JNI_LIB_SUFFIX, suffix_len); 168 len += suffix_len; 169 170 return (*env)->NewString(env, chars, len); 171}
b. getLibPaths的实现如下,只是为了获取加载库时搜索的路径列表:
1043 private volatile String[] mLibPaths = null; 1044 1045 private String[] getLibPaths() { 1046 if (mLibPaths == null) { 1047 synchronized(this) { 1048 if (mLibPaths == null) { 1049 mLibPaths = initLibPaths(); 1050 } 1051 } 1052 } 1053 return mLibPaths; 1054 } 1055 1056 private static String[] initLibPaths() { 1057 String javaLibraryPath = System.getProperty("java.library.path"); 1058 if (javaLibraryPath == null) { 1059 return EmptyArray.STRING; 1060 } 1061 String[] paths = javaLibraryPath.split(":"); 1062 // Add a '/' to the end of each directory so we don't have to do it every time. 1063 for (int i = 0; i < paths.length; ++i) { 1064 if (!paths[i].endsWith("/")) { 1065 paths[i] += "/"; 1066 } 1067 } 1068 return paths; 1069 }
序号 | 属性 | 说明 |
1 | java.version | Java 运行时环境版本 |
2 | java.vendor | Java 运行时环境供应商 |
3 | java.vendor.url | Java 供应商的 URL |
4 | java.home | Java 安装目录 |
5 | java.vm.specification.version | Java 虚拟机规范版本 |
6 | java.vm.specification.vendor | Java 虚拟机规范供应商 |
7 | java.vm.specification.name | Java 虚拟机规范名称 |
8 | java.vm.version | Java 虚拟机实现版本 |
9 | java.vm.vendor | Java 虚拟机实现供应商 |
10 | java.vm.name | Java 虚拟机实现名称 |
11 | java.specification.version | Java 运行时环境规范版本 |
12 | java.specification.vendor | Java 运行时环境规范供应商 |
13 | java.specification.name | Java 运行时环境规范名称 |
14 | java.class.version | Java 类格式版本号 |
15 | java.class.path | Java 类路径 |
16 | java.library.path | 加载库时搜索的路径列表 |
17 | java.io.tmpdir | 默认的临时文件路径 |
18 | java.compiler | 要使用的 JIT 编译器的名称 |
19 | java.ext.dirs | 一个或多个扩展目录的路径 |
20 | os.name | 操作系统的名称 |
21 | os.arch | 操作系统的架构 |
22 | os.version | 操作系统的版本 |
23 | file.separator | 文件分隔符(在 UNIX 系统中是“/”) |
24 | path.separator | 路径分隔符(在 UNIX 系统中是“:”) |
25 | line.separator | 行分隔符(在 UNIX 系统中是“/n”) |
26 | user.name | 用户的账户名称 |
27 | user.home | 用户的主目录 |
28 | user.dir | 用户的当前工作目录 |
c. canOpenReadOnly方法来判定目标动态库是否存在
151 /** 152 * Do not use. This is for System.loadLibrary use only. 153 * 154 * Checks whether {@code path} can be opened read-only. Similar to File.exists, but doesn't 155 * require read permission on the parent, so it'll work in more cases, and allow you to 156 * remove read permission from more directories. Everyone else should just open(2) and then 157 * use the fd, but the loadLibrary API is broken by its need to ask ClassLoaders where to 158 * find a .so rather than just calling dlopen(3). 159 */ 160 public static boolean canOpenReadOnly(String path) { 161 try { 162 // Use open(2) rather than stat(2) so we require fewer permissions. http://b/6485312. 163 FileDescriptor fd = Libcore.os.open(path, O_RDONLY, 0); 164 Libcore.os.close(fd); 165 return true; 166 } catch (ErrnoException errnoException) { 167 return false; 168 } 169 }
d. 如找到指定的目标动态库,调用doLoad来加载,在loader为null 时直接调用了nativeLoad:
1070 private String doLoad(String name, ClassLoader loader) { 1071 // Android apps are forked from the zygote, so they can't have a custom LD_LIBRARY_PATH, 1072 // which means that by default an app's shared library directory isn't on LD_LIBRARY_PATH. 1073 1074 // The PathClassLoader set up by frameworks/base knows the appropriate path, so we can load 1075 // libraries with no dependencies just fine, but an app that has multiple libraries that 1076 // depend on each other needed to load them in most-dependent-first order. 1077 1078 // We added API to Android's dynamic linker so we can update the library path used for 1079 // the currently-running process. We pull the desired path out of the ClassLoader here 1080 // and pass it to nativeLoad so that it can call the private dynamic linker API. 1081 1082 // We didn't just change frameworks/base to update the LD_LIBRARY_PATH once at the 1083 // beginning because multiple apks can run in the same process and third party code can 1084 // use its own BaseDexClassLoader. 1085 1086 // We didn't just add a dlopen_with_custom_LD_LIBRARY_PATH call because we wanted any 1087 // dlopen(3) calls made from a .so's JNI_OnLoad to work too. 1088 1089 // So, find out what the native library search path is for the ClassLoader in question... 1090 String librarySearchPath = null; 1091 if (loader != null && loader instanceof BaseDexClassLoader) { 1092 BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader; 1093 librarySearchPath = dexClassLoader.getLdLibraryPath(); 1094 } 1095 // nativeLoad should be synchronized so there's only one LD_LIBRARY_PATH in use regardless 1096 // of how many ClassLoaders are in the system, but dalvik doesn't support synchronized 1097 // internal natives. 1098 synchronized (this) { 1099 return nativeLoad(name, loader, librarySearchPath); 1100 } 1101 }
e. nativeLoad是通过native来实现的,JVM_NativeLoad来完成具体的实现细节:
322JNIEXPORT jstring JVM_NativeLoad(JNIEnv* env, 323 jstring javaFilename, 324 jobject javaLoader, 325 jstring javaLibrarySearchPath) { 326 ScopedUtfChars filename(env, javaFilename); 327 if (filename.c_str() == NULL) { 328 return NULL; 329 } 330 331 std::string error_msg; 332 { 333 art::JavaVMExt* vm = art::Runtime::Current()->GetJavaVM(); 334 bool success = vm->LoadNativeLibrary(env, 335 filename.c_str(), 336 javaLoader, 337 javaLibrarySearchPath, 338 &error_msg); 339 if (success) { 340 return nullptr; 341 } 342 } 343 344 // Don't let a pending exception from JNI_OnLoad cause a CheckJNI issue with NewStringUTF. 345 env->ExceptionClear(); 346 return env->NewStringUTF(error_msg.c_str()); 347}
f. 真正加载so的实现来了,OpenNativeLibrary 来处理加载,加载完成之后创建动态库,并放到libraries_中,找到JNI_OnLoad符号所对应的方法, 并调用该方法:
766bool JavaVMExt::LoadNativeLibrary(JNIEnv* env, 767 const std::string& path, 768 jobject class_loader, 769 jstring library_path, 770 std::string* error_msg) { 771 error_msg->clear(); 772 773 // See if we've already loaded this library. If we have, and the class loader 774 // matches, return successfully without doing anything. 775 // TODO: for better results we should canonicalize the pathname (or even compare 776 // inodes). This implementation is fine if everybody is using System.loadLibrary. 777 SharedLibrary* library; 778 Thread* self = Thread::Current(); 779 { 780 // TODO: move the locking (and more of this logic) into Libraries. 781 MutexLock mu(self, *Locks::jni_libraries_lock_); 782 library = libraries_->Get(path); 783 } 784 void* class_loader_allocator = nullptr; 785 { 786 ScopedObjectAccess soa(env); 787 // As the incoming class loader is reachable/alive during the call of this function, 788 // it's okay to decode it without worrying about unexpectedly marking it alive. 789 ObjPtr<mirror::ClassLoader> loader = soa.Decode<mirror::ClassLoader>(class_loader); 790 791 ClassLinker* class_linker = Runtime::Current()->GetClassLinker(); 792 if (class_linker->IsBootClassLoader(soa, loader.Ptr())) { 793 loader = nullptr; 794 class_loader = nullptr; 795 } 796 797 class_loader_allocator = class_linker->GetAllocatorForClassLoader(loader.Ptr()); 798 CHECK(class_loader_allocator != nullptr); 799 } 800 if (library != nullptr) { 801 // Use the allocator pointers for class loader equality to avoid unnecessary weak root decode. 802 if (library->GetClassLoaderAllocator() != class_loader_allocator) { 803 // The library will be associated with class_loader. The JNI 804 // spec says we can't load the same library into more than one 805 // class loader. 806 StringAppendF(error_msg, "Shared library \"%s\" already opened by " 807 "ClassLoader %p; can't open in ClassLoader %p", 808 path.c_str(), library->GetClassLoader(), class_loader); 809 LOG(WARNING) << error_msg; 810 return false; 811 } 812 VLOG(jni) << "[Shared library \"" << path << "\" already loaded in " 813 << " ClassLoader " << class_loader << "]"; 814 if (!library->CheckOnLoadResult()) { 815 StringAppendF(error_msg, "JNI_OnLoad failed on a previous attempt " 816 "to load \"%s\"", path.c_str()); 817 return false; 818 } 819 return true; 820 } 821 822 // Open the shared library. Because we're using a full path, the system 823 // doesn't have to search through LD_LIBRARY_PATH. (It may do so to 824 // resolve this library's dependencies though.) 825 826 // Failures here are expected when java.library.path has several entries 827 // and we have to hunt for the lib. 828 829 // Below we dlopen but there is no paired dlclose, this would be necessary if we supported 830 // class unloading. Libraries will only be unloaded when the reference count (incremented by 831 // dlopen) becomes zero from dlclose. 832 833 Locks::mutator_lock_->AssertNotHeld(self); 834 const char* path_str = path.empty() ? nullptr : path.c_str(); 835 bool needs_native_bridge = false; 836 void* handle = android::OpenNativeLibrary(env, 837 runtime_->GetTargetSdkVersion(), 838 path_str, 839 class_loader, 840 library_path, 841 &needs_native_bridge, 842 error_msg); 843 844 VLOG(jni) << "[Call to dlopen(\"" << path << "\", RTLD_NOW) returned " << handle << "]"; 845 846 if (handle == nullptr) { 847 VLOG(jni) << "dlopen(\"" << path << "\", RTLD_NOW) failed: " << *error_msg; 848 return false; 849 } 850 851 if (env->ExceptionCheck() == JNI_TRUE) { 852 LOG(ERROR) << "Unexpected exception:"; 853 env->ExceptionDescribe(); 854 env->ExceptionClear(); 855 } 856 // Create a new entry. 857 // TODO: move the locking (and more of this logic) into Libraries. 858 bool created_library = false; 859 { 860 // Create SharedLibrary ahead of taking the libraries lock to maintain lock ordering. 861 std::unique_ptr<SharedLibrary> new_library( 862 new SharedLibrary(env, 863 self, 864 path, 865 handle, 866 needs_native_bridge, 867 class_loader, 868 class_loader_allocator)); 869 870 MutexLock mu(self, *Locks::jni_libraries_lock_); 871 library = libraries_->Get(path); 872 if (library == nullptr) { // We won race to get libraries_lock. 873 library = new_library.release(); 874 libraries_->Put(path, library); 875 created_library = true; 876 } 877 } 878 if (!created_library) { 879 LOG(INFO) << "WOW: we lost a race to add shared library: " 880 << "\"" << path << "\" ClassLoader=" << class_loader; 881 return library->CheckOnLoadResult(); 882 } 883 VLOG(jni) << "[Added shared library \"" << path << "\" for ClassLoader " << class_loader << "]"; 884 885 bool was_successful = false; 886 void* sym = library->FindSymbol("JNI_OnLoad", nullptr); 887 if (sym == nullptr) { 888 VLOG(jni) << "[No JNI_OnLoad found in \"" << path << "\"]"; 889 was_successful = true; 890 } else { 891 // Call JNI_OnLoad. We have to override the current class 892 // loader, which will always be "null" since the stuff at the 893 // top of the stack is around Runtime.loadLibrary(). (See 894 // the comments in the JNI FindClass function.) 895 ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride())); 896 self->SetClassLoaderOverride(class_loader); 897 898 VLOG(jni) << "[Calling JNI_OnLoad in \"" << path << "\"]"; 899 typedef int (*JNI_OnLoadFn)(JavaVM*, void*); 900 JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym); 901 int version = (*jni_on_load)(this, nullptr); 902 903 if (runtime_->GetTargetSdkVersion() != 0 && runtime_->GetTargetSdkVersion() <= 21) { 904 // Make sure that sigchain owns SIGSEGV. 905 EnsureFrontOfChain(SIGSEGV); 906 } 907 908 self->SetClassLoaderOverride(old_class_loader.get()); 909 910 if (version == JNI_ERR) { 911 StringAppendF(error_msg, "JNI_ERR returned from JNI_OnLoad in \"%s\"", path.c_str()); 912 } else if (JavaVMExt::IsBadJniVersion(version)) { 913 StringAppendF(error_msg, "Bad JNI version returned from JNI_OnLoad in \"%s\": %d", 914 path.c_str(), version); 915 // It's unwise to call dlclose() here, but we can mark it 916 // as bad and ensure that future load attempts will fail. 917 // We don't know how far JNI_OnLoad got, so there could 918 // be some partially-initialized stuff accessible through 919 // newly-registered native method calls. We could try to 920 // unregister them, but that doesn't seem worthwhile. 921 } else { 922 was_successful = true; 923 } 924 VLOG(jni) << "[Returned " << (was_successful ? "successfully" : "failure") 925 << " from JNI_OnLoad in \"" << path << "\"]"; 926 } 927 928 library->SetResult(was_successful); 929 return was_successful; 930}
458void* OpenNativeLibrary(JNIEnv* env, 459 int32_t target_sdk_version, 460 const char* path, 461 jobject class_loader, 462 jstring library_path, 463 bool* needs_native_bridge, 464 std::string* error_msg) { 465#if defined(__ANDROID__) 466 UNUSED(target_sdk_version); 467 if (class_loader == nullptr) { 468 *needs_native_bridge = false; 469 return dlopen(path, RTLD_NOW); 470 } 471 472 std::lock_guard<std::mutex> guard(g_namespaces_mutex); 473 NativeLoaderNamespace ns; 474 475 if (!g_namespaces->FindNamespaceByClassLoader(env, class_loader, &ns)) { 476 // This is the case where the classloader was not created by ApplicationLoaders 477 // In this case we create an isolated not-shared namespace for it. 478 if (!g_namespaces->Create(env, 479 target_sdk_version, 480 class_loader, 481 false, 482 library_path, 483 nullptr, 484 &ns, 485 error_msg)) { 486 return nullptr; 487 } 488 } 489 490 if (ns.is_android_namespace()) { 491 android_dlextinfo extinfo; 492 extinfo.flags = ANDROID_DLEXT_USE_NAMESPACE; 493 extinfo.library_namespace = ns.get_android_ns(); 494 495 void* handle = android_dlopen_ext(path, RTLD_NOW, &extinfo); 496 if (handle == nullptr) { 497 *error_msg = dlerror(); 498 } 499 *needs_native_bridge = false; 500 return handle; 501 } else { 502 void* handle = NativeBridgeLoadLibraryExt(path, RTLD_NOW, ns.get_native_bridge_ns()); 503 if (handle == nullptr) { 504 *error_msg = NativeBridgeGetError(); 505 } 506 *needs_native_bridge = true; 507 return handle; 508 } 509#else 510 UNUSED(env, target_sdk_version, class_loader, library_path); 511 *needs_native_bridge = false; 512 void* handle = dlopen(path, RTLD_NOW); 513 if (handle == nullptr) { 514 if (NativeBridgeIsSupported(path)) { 515 *needs_native_bridge = true; 516 handle = NativeBridgeLoadLibrary(path, RTLD_NOW); 517 if (handle == nullptr) { 518 *error_msg = NativeBridgeGetError(); 519 } 520 } else { 521 *needs_native_bridge = false; 522 *error_msg = dlerror(); 523 } 524 } 525 return handle; 526#endif 527}
2. loader不为null时做了什么?
a. findLibrary从所有的native目录中查找库文件,如果找到返回查找结果:
515 /** 516 * Finds the named native code library on any of the library 517 * directories pointed at by this instance. This will find the 518 * one in the earliest listed directory, ignoring any that are not 519 * readable regular files. 520 * 521 * @return the complete path to the library or {@code null} if no 522 * library was found 523 */ 524 public String findLibrary(String libraryName) { 525 String fileName = System.mapLibraryName(libraryName); 526 527 for (NativeLibraryElement element : nativeLibraryPathElements) { 528 String path = element.findNativeLibrary(fileName); 529 530 if (path != null) { 531 return path; 532 } 533 } 534 535 return null; 536 }
b. findNativeLibrary查询当前nativeLibrary下面是否存在库文件,存在就返回:
781 public String findNativeLibrary(String name) { 782 maybeInit(); 783 784 if (zipDir == null) { 785 String entryPath = new File(path, name).getPath(); 786 if (IoUtils.canOpenReadOnly(entryPath)) { 787 return entryPath; 788 } 789 } else if (urlHandler != null) { 790 // Having a urlHandler means the element has a zip file. 791 // In this case Android supports loading the library iff 792 // it is stored in the zip uncompressed. 793 String entryName = zipDir + '/' + name; 794 if (urlHandler.isEntryStored(entryName)) { 795 return path.getPath() + zipSeparator + entryName; 796 } 797 } 798 799 return null; 800 }
c. 如果上述可以找到文件则执行doLoad加载:
1003 String libraryName = libname; 1004 if (loader != null) { 1005 String filename = loader.findLibrary(libraryName); 1006 if (filename == null) { 1007 // It's not necessarily true that the ClassLoader used 1008 // System.mapLibraryName, but the default setup does, and it's 1009 // misleading to say we didn't find "libMyLibrary.so" when we 1010 // actually searched for "liblibMyLibrary.so.so". 1011 throw new UnsatisfiedLinkError(loader + " couldn't find \"" + 1012 System.mapLibraryName(libraryName) + "\""); 1013 } 1014 String error = doLoad(filename, loader); 1015 if (error != null) { 1016 throw new UnsatisfiedLinkError(error); 1017 } 1018 return; 1019 }
以上参考:
本文地址:https://blog.csdn.net/qq_25065595/article/details/107941428