so 库的动态加载
在客户端开发过程中,有些时候在考虑到效率的时候不可避免的会用到java调用c以此来解决一些效率的问题,但是不可避免的,需要编译很多个平台的so文件,可能就会造成app文件过大,所以有时候需要采用其他方式来加载so文件。
通常我们在开发中遇到要加载本地方法时会这么写
static {
System.loadLibrary("native-lib");
}
这是通过库名去找本地已加载的列表中去找对应的文件地址,在通过地址去加载so文件。
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
其实他是调用runtime的loadLibrary0 这个方法去加载。
synchronized void loadLibrary0(ClassLoader loader, String libname) {
if (libname.indexOf((int)File.separatorChar) != -1) {
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
}
String libraryName = libname;
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 : getLibPaths()) {
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);
}
这个是loadLibrary0的具体实现,无论这个逻辑怎么样,都是通过调用doLoad方法。
具体分析一下这个方法,如果classLoader 不为null的话 会去调用classLoader的findLibrary方法,这个方法的是在BaseDexClassLoader实现的,
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
pathList其实是DexPathList实现的看一下他的实现
public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);
for (Element element : nativeLibraryPathElements) {
String path = element.findNativeLibrary(fileName);
if (path != null) {
return path;
}
}
return null;
}
public String findNativeLibrary(String name) {
maybeInit();
if (isDirectory) {
String path = new File(dir, name).getPath();
if (IoUtils.canOpenReadOnly(path)) {
return path;
}
} else if (urlHandler != null) {
// Having a urlHandler means the element has a zip file.
// In this case Android supports loading the library iff
// it is stored in the zip uncompressed.
String entryName = new File(dir, name).getPath();
if (urlHandler.isEntryStored(entryName)) {
return zip.getPath() + zipSeparator + entryName;
}
}
return null;
}
这个方法System.mapLibraryName(libraryName) 其实是把方法库映射一个特定的cpu架构平台库的名称,然后 findNativeLibrary这个方法是去找本地的这个库的具体路径。回头去看RunTime类的加载方法如果类加载器不存在时,也是先将库名映射到相应架构的库名称在去本地库去查这个文件
private String[] getLibPaths() {
if (mLibPaths == null) {
synchronized(this) {
if (mLibPaths == null) {
mLibPaths = initLibPaths();
}
}
}
return mLibPaths;
}
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;
}
上面就是去本地的库去查这个文件,最终都是调用doLoad方法。
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 librarySearchPath = null;
if (loader != null && loader instanceof BaseDexClassLoader) {
BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
librarySearchPath = dexClassLoader.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, librarySearchPath);
}
}
java的这个方法没什么好说的,具体就是将可搜索的文件历经传入c层,由c层完成加载。
System其实还有一个直接加载绝对路径的方法
public static void load(String filename) {
Runtime.getRuntime().load0(VMStack.getStackClass1(), filename);
}
RunTime实现
synchronized void load0(Class fromClass, String filename) {
if (!(new File(filename).isAbsolute())) {
throw new UnsatisfiedLinkError(
"Expecting an absolute path of the library: " + filename);
}
if (filename == null) {
throw new NullPointerException("filename == null");
}
String error = doLoad(filename, fromClass.getClassLoader());
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
}
看到这个方法也是通过doLoad方法进行加载,而且还有判断是否是绝对路径,否则就抛出异常。
也就是说在android中其实也是可以去加载外部的so文件,以此来完成so的动态修复。
正常的so文件加载就不多说了,下面是加载外部so的演示代码
将so文件放在/storage/emulated/0/jniLibs路径下面,是编译之后的一个完整的路径文件
是一个完整的结构。
以下代码路径基本是固定的
private void copySoAndLoadLib() {
try {
File dir = this.getDir("jniLibs", Activity.MODE_PRIVATE);
File distFile = new File(dir.getAbsolutePath() + File.separator + "libnative-lib.so");
File file = new File("/storage/emulated/0/jniLibs");
if (copySoLib(file, "libnative-lib.so")) {
//使用load方法加载内部储存的SO库
Log.i(TAG,"path:" + distFile.getAbsolutePath());
System.load(distFile.getAbsolutePath());
loadCTest test = new loadCTest();
String testStr = test.getTestStr();
if (TextUtils.isEmpty(testStr)) {
testStr = "Not found string";
}
tv.setText(testStr);
} else {
tv.setText("失败");
}
} catch (Exception e) {
e.printStackTrace();
}
}
判断,然后加载so库,并设置显示
/**
* 将一个SO库复制到指定路径,会先检查改SO库是否与当前CPU兼容
*
* @param sourceDir SO库所在目录
* @param so SO库名字
* @return
*/
public boolean copySoLib(File sourceDir, String so) throws IOException {
boolean isSuccess = false;
try {
Log.d(TAG, "[copySo] 开始处理so文件");
if (Build.VERSION.SDK_INT >= 21) {
String[] abis = Build.SUPPORTED_ABIS;
if (abis != null) {
for (String abi : abis) {
Log.d(TAG, "[copySo] try supported abi:" + abi);
String name = File.separator + abi + File.separator + so;
File sourceFile = new File(sourceDir, name);
if (sourceFile.exists()) {
Log.i(TAG, "[copySo] copy so: " + sourceFile.getAbsolutePath());
isSuccess = copyFile(sourceFile.getAbsolutePath());
//api21 64位系统的目录可能有些不同
//copyFile(sourceFile.getAbsolutePath(), destDir + File.separator + name);
break;
}
}
} else {
Log.e(TAG, "[copySo] get abis == null");
}
} else {
Log.d(TAG, "[copySo] supported api:" + Build.CPU_ABI + " " + Build.CPU_ABI2);
String name = "lib" + File.separator + Build.CPU_ABI + File.separator + so;
File sourceFile = new File(sourceDir, name);
if (!sourceFile.exists() && Build.CPU_ABI2 != null) {
name = "lib" + File.separator + Build.CPU_ABI2 + File.separator + so;
sourceFile = new File(sourceDir, name);
if (!sourceFile.exists()) {
name = "lib" + File.separator + "armeabi" + File.separator + so;
sourceFile = new File(sourceDir, name);
}
}
if (sourceFile.exists()) {
Log.i(TAG, "[copySo] copy so: " + sourceFile.getAbsolutePath());
isSuccess = copyFile(sourceFile.getAbsolutePath());
}
}
if (!isSuccess) {
Log.e(TAG, "[copySo] 安装 " + so + " 失败 : NO_MATCHING_ABIS");
throw new IOException("install " + so + " fail : NO_MATCHING_ABIS");
}
} catch (IOException e) {
e.printStackTrace();
throw e;
}
return true;
}
去找相应的cpu架构的so文件然后复制
public boolean copyFile(String filePath) {
boolean copyIsFinish = false;
File dir = this.getDir("jniLibs", Activity.MODE_PRIVATE);
if(!dir.exists()){
dir.mkdirs();
}
File distFile = new File(dir.getAbsolutePath() + File.separator + "libnative-lib.so");
try {
File jniFile = new File(filePath);
FileInputStream is = new FileInputStream(jniFile);
File file = new File(distFile.getAbsolutePath());
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
byte[] temp = new byte[1024];
int i = 0;
while ((i = is.read(temp)) > 0) {
fos.write(temp, 0, i);
}
fos.close();
is.close();
copyIsFinish = true;
} catch (IOException e) {
e.printStackTrace();
Log.e("MainActivity", "[copyFileFromAssets] IOException " + e.toString());
}
Log.i(TAG,"filePath:" + distFile.getAbsolutePath());
return copyIsFinish;
}
具体copy文件的代码
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring JNICALL
Java_com_nat_ncreate_loadCTest_getNativeStr(JNIEnv *env, jobject /* this */) {
std::string hello = "asdfkjkjshafkjs";
return env->NewStringUTF(hello.c_str());
}
这个是so文件,随便写了个返回值。
验证结果
验证是生效的,动态替换so方法,采用这种方式是可行的。