原文地址:
最近遇到这样一个问题:
第三方的SDK除了Jar包外,还提供了对应的so文件。 APK集成SDK后进行测试,发现一切正常。 但将APK作为系统应用集成到ROM时,发现so获取失败。看了一下SDK的代码,发现由于底层库的需求,SDK没有直接利用System.loadLibrary来加载so,
而是主动获取so的路径,对应的代码如下:protected static String findNativeLibraryPath(Context context, String libraryName) { if (context == null) { return null; } if (TextUtils.isEmpty(libraryName)) { return null; } String dexPath = context.getPackageCodePath(); //注意这个地方 //取的是ApplicationInfo中的nativeLibraryDir String nativeLibraryDir = context.getApplicationInfo().nativeLibraryDir; //创建PathClassLoader PathClassLoader pathClassLoader = new PathClassLoader(dexPath, nativeLibraryDir, ClassLoader.getSystemClassLoader()); //利用PathClassLoader来获取so的路径 return pathClassLoader.findLibrary(libraryName);}
为了解释这个问题及寻找解决方案,我们就必须查找对应的源码了。
接下来,我们就以8.0的代码为例,看看相应的流程。一、PathClassLoader相关内容
我们先来看看PathClassLoader相关的源码:public class PathClassLoader extends BaseDexClassLoader { ........... //前文传入ApplicationInfo的nativeLibraryDir //对应此处的librarySearchPath public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) { super(dexPath, null, librarySearchPath, parent); }}
PathClassLoader继承BaseDexClassLoader,仅实现了两个构造函数,
主要的逻辑还是实现于BaseDexClassLoader中。1.1 BaseDexClassLoader相关内容
我们来看看BaseDexClassLoader中的代码:public class BaseDexClassLoader extends ClassLoader { ............. public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(parent); //注意这里构造了DexPathList, 同样传入了librarySearchPath this.pathList = new DexPathList(this, dexPath, librarySearchPath, null); ................. } ........... @Override public String findLibrary(String name) { return pathList.findLibrary(name); } .......}
从代码不难发现,findLibrary查找的实际上是DexPathList,
后者于BaseDexClassLoader的构造函数中创建。1.2 DexPathList相关内容
继续跟进DexPathList对应的代码:final class DexPathList { ....... public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) { ........... //从下面的代码,容易看出native path包括传入的librarySearchPath //及"java.library.path"对应的路径 this.nativeLibraryDirectories = splitPaths(librarySearchPath, false); this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true); ListallNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories); allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories); //最后将信息保存到nativeLibraryPathElements this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories); ..................... } ........... //容易看出DexPathList就是从nativeLibraryPathElements中获取结果 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; } ...............}
至此,我们应该可以得出结论,so路径无法找到的原因就是:
ApplicationInfo.nativeLibraryDir及”java.library.path”对应的路径中没有so。 那么APK被集成到系统时,so的路径到底在哪里?为了解决这个问题,我们就要看看APK创建过程相关的代码了。
二、APK创建过程相关内容
APK被系统创建时,会调用handleBindApplication函数,我们截取其中一部分:private void handleBindApplication(AppBindData data) { ........... //判断APK是否支持了测试框架 if (ii != null) { ......... //创建LoadedApk final LoadedApk pi = getPackageInfo(instrApp, data.compatInfo, //初次调用ContextImpl的classLoader appContext.getClassLoader(), false, true, false); //以LoadedApk重新创建Context final ContextImpl instrContext = ContextImpl.createAppContext(this, pi); try { //再次调用ContextImpl的classLoader final ClassLoader cl = instrContext.getClassLoader(); ........ } ...........}
当APK支持测试框架时才会进入上述逻辑。
不过在Apk加载service、activity等组件时, 最终都会用到ContextImpl的getClassLoader函数。 因此,可以以这段代码为例进行分析。我们来看看ContextImpl的getClassLoader函数:
public ClassLoader getClassLoader() { //初次调用时,返回的是ClassLoader.getSystemClassLoader() //再次调用时,调用的是LoadedApk.getClassLoader() return mClassLoader != null ? mClassLoader : (mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader());}
当Apk信息解析完毕后,会生成LoadedApk信息。
一旦有了LoadedApk信息后,ContextImpl返回的就是LoadedApk.getClassLoader的结果。我们仅关注APK解析完毕,有了LoadedApk信息后的代码流程。
因此,直接跟进LoadedApk相关的代码:public ClassLoader getClassLoader() { synchronized (this) { if (mClassLoader == null) { //初次调用时,会进入此分支 createOrUpdateClassLoaderLocked(null /*addedPaths*/); } return mClassLoader; }}
我们来看看createOrUpdateClassLoaderLocked函数,重点关注路径相关的信息:
private void createOrUpdateClassLoaderLocked(ListaddedPaths) { .......... //这两个对象保存最终的结果, //其中libPaths保存的是so相关的路径 final List zipPaths = new ArrayList<>(10); final List libPaths = new ArrayList<>(10); //判断是否为BundledApp(主要为系统应用) final boolean isBundledApp = mApplicationInfo.isSystemApp() && !mApplicationInfo.isUpdatedSystemApp(); //构建libPaths等 makePaths(mActivityThread, isBundledApp, mApplicationInfo, zipPaths, libPaths); //之后利用libPaths等信息构造ClassLoader ...........}
按照代码流程,接下来我们分析下makePaths函数,
主要关注libPaths相关的内容:public static void makePaths(ActivityThread activityThread, boolean isBundledApp, ApplicationInfo aInfo, ListoutZipPaths, List outLibPaths) { //取出app对应的sourceDir final String appDir = aInfo.sourceDir; //这里取出了ApplicationInfo中的nativeLibraryDir final String libDir = aInfo.nativeLibraryDir; final String[] sharedLibraries = aInfo.sharedLibraryFiles; //清空结果并增加sourceDir outZipPaths.clear(); outZipPaths.add(appDir); ........... if (outLibPaths != null) { outLibPaths.clear(); } ............ if (outLibPaths != null) { if (outLibPaths.isEmpty()) { //输出结果中增加nativeLibraryDir outLibPaths.add(libDir); } //当ApplicationInfo指定了Abi时 if (aInfo.primaryCpuAbi != null) { if (aInfo.targetSdkVersion < Build.VERSION_CODES.N) { outLibPaths.add("/system/fake-libs" + (VMRuntime.is64BitAbi(aInfo.primaryCpuAbi) ? "64" : "")); } //前文中outZipPaths已经增加了ApplicationInfo.sourceDir //增加abi对应的路径 for (String apk : outZipPaths) { outLibPaths.add(apk + "!/lib/" + aInfo.primaryCpuAbi); } } //系统APK if (isBundledApp) { ........ outLibPaths.add(System.getProperty("java.library.path")); } //如果还有shared library, 还需要增加shared library对应的路径 if (sharedLibraries != null) { for (String lib : sharedLibraries) { if (!outZipPaths.contains(lib)) { outZipPaths.add(0, lib); appendApkLibPathIfNeeded(lib, aInfo, outLibPaths); } } } ........... }}
至此我们应该可以看出,对于一个APK而言:
ApplicationInfo.nativeLibraryDir只是so存储路径的一部分。 整体的so路径可能还包括!/lib/abi部分、system/lib部分及shared library部分,例如:nativeLibraryDirectories=[/data/app/test-app/lib/arm, /data/app/test-app/base.apk!/lib/armeabi-v7a, /vendor/lib, /system/lib]]]
关于so何时被拷贝到上述目录,就需要分析so拷贝的流程,本博客暂不对此进行分析。
三、解决方案
了解问题的原因后,解决方案其实就呼之欲出了。 从前文的代码,我们容易看出: APK对应的so路径就保存Context对应的ClassLoader中。 该ClassLoader调用Context.getClassLoader就能获取。在前文提及的LoadedApk的createOrUpdateClassLoaderLocked中,
创建ClassLoader的代码如下:private void createOrUpdateClassLoaderLocked(ListaddedPaths) { ............. if (mClassLoader == null) { ......... mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, mApplicationInfo.targetSdkVersion, isBundledApp, librarySearchPath, libraryPermittedPath, mBaseClassLoader); ....... }}
我们跟进一下ApplicationLoaders.getClassLoader函数:
private ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled, String librarySearchPath, String libraryPermittedPath, ClassLoader parent, String cacheKey) { ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent(); synchronized (mLoaders) { if (parent == null) { parent = baseParent; } if (parent == baseParent) { ............. PathClassLoader pathClassloader = PathClassLoaderFactory.createClassLoader( zip, librarySearchPath, libraryPermittedPath, parent, targetSdkVersion, isBundled); ......... } ........... PathClassLoader pathClassloader = new PathClassLoader(zip, parent); ........... return pathClassloader; }}
容易看出,上述代码无论进入哪个分支,
getClassLoader最终返回的是一个PathClassLoader函数。于是,文中开头部分提及findNativeLibraryPath改为如下方式即可:
protected static String findNativeLibraryPath(AVLContext context, String libraryName) { if (context == null) { return null; } if (TextUtils.isEmpty(libraryName)) { return null; } PathClassLoader classLoader = (PathClassLoader) context.getClassLoader(); return classLoader.findLibrary(libraryName);}
经过测试,此种方法可以满足我们当前的各种集成场景的需求。