[This is ZY’s 13th original technical article]

Preliminary knowledge

  1. Basic Knowledge of Android ClassLoader

How far can I go after reading this article

  1. Understand the differences between PathClassLoader and DexClassLoader

The article gives an overview of

A, cause

Speaking of PathClassLoader and DexClassLoader in Android, first put forward a question, what is the difference between PathClassLoader and DexClassLoader? PathClassLoader is used to load apK already installed, and DexClassLoader is used to load dex/APK files for storage space. Why do I say that? Because I’ve always understood it that way, and most articles on the Internet explain it that way. So why all of a sudden talk about PathClassLoader and DexClassLoader? The reason is that I wrote some plug-in demo some time ago, at that time forgot the PathClassLoader and DexClassLoader, directly used PathClassLoader to load the plug-in, unexpectedly also can load successfully?? A little confusion appeared on my handsome and handsome face, a small question mark in my clever little brain. So to turn over the source code, there is this article.

Second, put the conclusion first

Both PathClassLoader and DexClassLoader can load external dex/APK. The only difference is that DexClassLoader can specify optimizedDirectory. Odex is the product of Dex2OAT, and PathClassLoader can only use the system default location. However, the optimizedDirectory has been deprecated since Android 8.0, and only the default directory is used.

We’ll start with the Android 5.0 code and then look at some of the differences between the other versions. (I chose 5.0 because the art source is relatively simple at this point.)

Constructor of ClassLoader

3.1 BaseDexClassLoader constructor

Both PathClassLoader and DexClassLoader inherit BaseDexClassLoader. The BaseDexClassLoader constructor.

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    /**
     * Constructs an instance.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param optimizedDirectory directory where optimized dex files
     * should be written; may be {@code null}
     * @param libraryPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); }}Copy the code

The BaseDexClassLoader constructor takes four parameters, with the following meanings:

  • DexPath: a list of files to be loaded. The files can be JAR/APK/ZIP containing classes.dex, or directly use classes.dex. Multiple files are separated by colons
  • OptimizedDirectory: stores the optimized dex. The value can be empty
  • LibraryPath: Directory for storing native libraries that need to be loaded
  • This parent: father

By using the constructor, we can learn about the operation of BaseDexClassLoader, pass in the dex file, and then optimize, save the optimized dex file to the optimizedDirectory.

3.2 PathClassLoader constructor

Next we look at the constructor of the PathClassLoader.

/**
 * Provides a simple {@link ClassLoader} implementation that operates on a list
 * of files and directories in the local file system, but does not attempt to
 * load classes from the network. Android uses this class for its system class
 * loader and for its application class loader(s).
 */
public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null.null, parent);
    }

    /**
     * Creates a {@code PathClassLoader} that operates on two given
     * lists of files and directories. The entries of the first list
     * should be one of the following:
     *
     * <ul>
     * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
     * well as arbitrary resources.
     * <li>Raw ".dex" files (not inside a zip file).
     * </ulyanzheng>
     *
     * The entries of the second list should be directories containing
     * native library files.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param libraryPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     */
    public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
        super(dexPath, null, libraryPath, parent); }}Copy the code

One thing to note about PathClassLoader is that it is described in the code comments as a ClassLoader implementation that operates on a series of files and directories on the file system. There is no mention that only installed APK files can be loaded. PathClassLoader has two constructors, the difference being whether the libraryPath passed to BaseDexClassLoader is null. When the BaseDexClassLoader constructor is finally called, the optimizedDirectory passed in is empty.

3.3 DexClassLoader constructor

Look again at the DexClassLoader constructor. These are the same parameters as the BaseDexClassLoader constructor.

public class DexClassLoader extends BaseDexClassLoader {
    /**
     * Creates a {@code DexClassLoader} that finds interpreted and native
     * code.  Interpreted classes are found in a set of DEX files contained
     * in Jar or APK files.
     *
     * <p>The path lists are separated using the character specified by the
     * {@code path.separator} system property, which defaults to {@code :}.
     *
     * @param dexPath the list of jar/apk files containing classes and
     *     resources, delimited by {@code File.pathSeparator}, which
     *     defaults to {@code ":"} on Android
     * @param optimizedDirectory directory where optimized dex files
     *     should be written; must not be {@code null}
     * @param librarySearchPath the list of directories containing native
     *     libraries, delimited by {@code File.pathSeparator}; may be
     *     {@code null}
     * @param parent the parent class loader
     */
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(dexPath, newFile(optimizedDirectory), librarySearchPath, parent); }}Copy the code

In the optimizedDirectory argument, PathClassLoader passes null. DexClassLoader passes in the directory specified by the user.

4, optimizedDirectory parameter processing

Now that you know the difference between optimizedDirectory, let’s see how BaseDexClassLoader handles optimizedDirectory.

4.1 DexPathList processing

In BaseDexClassLoader, optimizedDirectory is passed directly to DexPathList transparently. Here’s a quick introduction to DexPathList. There are two member variables in the DexPathList, dexElements for dex and resource directories, and nativeLibraryDirectories for native library directories.

class DexPathList {
    private final Element[] dexElements;
    private final File[] nativeLibraryDirectories;
}
Copy the code

In DexPathList, the path using optimizedDirectory is:

DexPathList -> makeDexElements -> loadDexFile
Copy the code

Here’s a look at the loadDexFile method.

class DexPathList {
    private static DexFile loadDexFile(File file, File optimizedDirectory)
            throws IOException {
        if (optimizedDirectory == null) {
            return new DexFile(file);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0); }}}Copy the code

In DexPathList, a DexFile object is created for each DEX file in two ways: if the optimizedDirectory is empty, call DexFile(file) to create the object; otherwise, call dexfile.loaddex (). The processing of optimizedDirectory is then transferred to DexFile.

4.2 DexFile processing

In DexFile. LoadDex, the DexFile constructor is called directly

class DexFile {
       public DexFile(File file) throws IOException {
        this(file.getPath());
    }

    public DexFile(String fileName) throws IOException {
        // Call openDexFile to process the dex
        mCookie = openDexFile(fileName, null.0);
        mFileName = fileName;
        guard.open("close");
    }

    private DexFile(String sourceName, String outputName, int flags) throws IOException {
        // ...
        // Call openDexFile to process the dex
        mCookie = openDexFile(sourceName, outputName, flags);
        mFileName = sourceName;
        guard.open("close");
    }

    static public DexFile loadDex(String sourcePathName, String outputPathName,
        int flags) throws IOException {
        return new DexFile(sourcePathName, outputPathName, flags);
    }

    private static long openDexFile(String sourceName, String outputName, int flags) throws IOException {
        // Finally call native methods
        return openDexFileNative(new File(sourceName).getAbsolutePath(),
                                 (outputName == null)?null : new File(outputName).getAbsolutePath(),
                                 flags);
    }

    private static native long openDexFileNative(String sourceName, String outputName, int flags);
}
Copy the code

There’s not much DexFile code, so that’s basically the main code. As you can see, whatever DexFile constructor is called will be processed through openDexFileNative. The difference is whether the outputName parameter is empty, whereas the outputName parameter, That is the optimizeDirectory argument passed along above. Let’s review the invoked link again:

PathClassLoader.constructor / DexClassLoader.constructor -> BaseDexClassLoader.constructor -> DexPathList.constructor ->  DexPathList.makeDexElements -> DexPathList.loadDexFile -> DexFile.constructor / DexFile.loadDex -> DexFile.openDexFile -> DexFile.openDexFileNativeCopy the code

Further down, you get to native logic. Native logic can download art source code to check.

4.3 native processing

OpenDexFileNative corresponds to the DexFile_openDexFileNative method in dalvik_system_dexfile. cc. The main thing to do in DexFile_openDexFileNative is to process DEX files and generate.odex files into the optimizedDirectory. OptimizedDirectory:

DexFile_openDexFileNative -> ClassLinker::OpenDexFilesFromOat
Copy the code

In OpenDexFilesFromOat, there is the following processing logic:

ClassLinker::OpenDexFilesFromOat() { // ... If (oat_location == NULlptr) {// Dalvikcache const STD ::string dalvik_cache(GetDalvikCacheOrDie(GetInstructionSetString(kRuntimeISA))); cache_location = GetDalvikCacheFilenameOrDie(dex_location, dalvik_cache.c_str()); oat_location = cache_location.c_str(); } / /... if (Runtime::Current()->IsDex2OatEnabled() && has_flock && scoped_flock.HasFile()) { // Create the oat file. open_oat_file.reset(CreateOatFileForDexLocation(dex_location, scoped_flock.GetFile()->Fd(), oat_location, error_msgs)); }}Copy the code

Oat_location in the above method is the oat_location avatar of optimizeDirectory introduced to native. There is a judgment logic here, if OAT_location is empty, the default dalvikcache path is used. Called after CreateOatFileForDexLocation to optimize the DEX file. Dalvikcache is obtained by GetDalvikCacheOrDie.

// art/runtime/utils.cc
std::string GetDalvikCacheOrDie(const char* subdir, const bool create_if_absent) { CHECK(subdir ! = nullptr);// AndroidData is the /data directory
  const char* android_data = GetAndroidData();
  const std::string dalvik_cache_root(StringPrintf("%s/dalvik-cache/", android_data));
  const std::string dalvik_cache = dalvik_cache_root + subdir;
  if(create_if_absent && ! OS::DirectoryExists(dalvik_cache.c_str())) {// Don't create the system's /data/dalvik-cache/... because it needs special permissions.
    if (strcmp(android_data, "/data") != 0) {
      int result = mkdir(dalvik_cache_root.c_str(), 0700);
      if(result ! =0&& errno ! = EEXIST) { PLOG(FATAL) <<"Failed to create dalvik-cache directory " << dalvik_cache_root;
        return "";
      }
      result = mkdir(dalvik_cache.c_str(), 0700);
      if(result ! =0) {
        PLOG(FATAL) << "Failed to create dalvik-cache directory " << dalvik_cache;
        return ""; }}else {
      LOG(FATAL) << "Failed to find dalvik-cache directory " << dalvik_cache;
      return ""; }}return dalvik_cache;
}
Copy the code

GetDalvikCacheOrDie gets the /data/dalvik-cache/ directory. Here’s a review of some of the questions I’ve asked so you don’t get lost in the code. If optmizedDirectory is null and optmizedDirectory is not null, the PathClassLoader will pass optmizedDirectory as null. The optimizedDirectory passed by DexClassLoader is a user – defined directory. Look back at the call link.

PathClassLoader.constructor / DexClassLoader.constructor -> BaseDexClassLoader.constructor -> DexPathList.constructor ->  DexPathList.makeDexElements -> DexPathList.loadDexFile -> DexFile.constructor / DexFile.loadDex -> DexFile.openDexFile -> DexFile.openDexFileNative -> DexFile_openDexFileNative -> ClassLinker::OpenDexFilesFromOatCopy the code

When optmizedDirectory is not empty, the user defined directory is used as the storage directory of the optimized product of DEX file. Odex; when optmizedDirectory is empty, the default /data/dalvik-cache/ directory is used. Therefore, PathClassLoader can not only load APK after installation, but also load other DEX/JAR/APK files, but the generated.odex file can only be stored in the default path of the system. A puzzle that had been misdirected for years was finally solved. Ears can not help but sound conan solve the case of BGM.

Verification on other system versions

However, the above analysis is under 5.0 source code, we select 4.4 and 8.0 to have a look. Why choose these two versions? First of all, 4.4 and 5.0 are the dividing line between ART and Dalvik, and there are some changes to PathClassLoader after 8.0.

5.1 the Android 4.4

With the above analysis foundation, we can analyze 4.4 code much more smoothly. All the way from Java analytics to Native. Java layer code is unchanged, native entry is still DexFile_openDexFileNative. Then the code is a little different.

DexFile_openDexFileNative() {
  // ...
  if (outputName.c_str() == NULL) {
    dex_file = linker->FindDexFileInOatFileFromDexLocation(dex_location, dex_location_checksum);
  } else {
    std::string oat_location(outputName.c_str());
    dex_file = linker->FindOrCreateOatFileForDexLocation(dex_location, dex_location_checksum, oat_location);
  }
  // ...
}
Copy the code

The difference between this and 5.0 is that two different functions are called depending on whether the outputName (optimizedDirectory) is empty. And the logic in the FindDexFileInOatFileFromDexLocation some familiar.

ClassLinker::FindDexFileInOatFileFromDexLocation() {
  // ...
  std::string oat_cache_filename(GetDalvikCacheFilenameOrDie(dex_location));
  return FindOrCreateOatFileForDexLocationLocked(dex_location, dex_location_checksum, oat_cache_filename);
}
Copy the code

By default, the dalvikCache directory is also obtained as the.odex file storage path.

5.2 the Android 8.0

On the 8.0 system, where things have slightly changed, let’s look at the BaseDexClassLoader constructor.

class BaseDexClassLoader {
    /**
     * Constructs an instance.
     * Note that all the *.jar and *.apk files from {@code dexPath} might be
     * first extracted in-memory before the code is loaded. This can be avoided
     * by passing raw dex files (*.dex) in the {@code dexPath}.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android.
     * @param optimizedDirectory this parameter is deprecated and has no effect
     * @param librarySearchPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);

        if(reporter ! =null) {
            reporter.report(this.pathList.getDexPaths()); }}}Copy the code

One obvious change is that optimizedDirectory is deprecated and the optimizedDirectory passed to DexPathList is empty regardless of what value is passed in. In other words, there is no difference between PathClassLoader and DexClassLoader on 8.0. DexClassLoader also cannot specify optimizedDirectory.

In DexFile_openDexFileNative, you can see that the javaOutputName parameter has also been deprecated.

static jobject DexFile_openDexFileNative(JNIEnv* env, jclass, jstring javaSourceName, jstring javaOutputName ATTRIBUTE_UNUSED, jint flags ATTRIBUTE_UNUSED, jobject class_loader, jobjectArray dex_elements) {}Copy the code

The processing link of DEX file is as follows:

DexFile_openDexFileNative -> DexLocationToOdexNames -> OatFileManager::OpenDexFilesFromOat -> OatFileAssistant::OatFileAssistant -> OatFileAssistant::DexLocationToOdexFilename -> DexLocationToOdexNames
Copy the code

In the DexLocationToOdexNames method, the path to the.odex file is handled.

static bool DexLocationToOdexNames(const std::string& location,
                                   InstructionSet isa,
                                   std::string* odex_filename,
                                   std::string* oat_dir,
                                   std::string* isa_dir,
                                   std::string* error_msg) { CHECK(odex_filename ! = nullptr); CHECK(error_msg ! = nullptr);// The odex file name is formed by replacing the dex_location extension with
  // .odex and inserting an oat/<isa> directory. For example:
  // location = /foo/bar/baz.jar
  // odex_location = /foo/bar/oat/
      
       /baz.odex
      

  // Find the directory portion of the dex location and add the oat/<isa>
  // directory.
  size_t pos = location.rfind('/');
  if (pos == std::string::npos) {
    *error_msg = "Dex location " + location + " has no directory.";
    return false;
  }
  std::string dir = location.substr(0, pos+1);
  // Add the oat directory.
  dir += "oat";
  if(oat_dir ! = nullptr) { *oat_dir = dir; }// Add the isa directory
  dir += "/" + std::string(GetInstructionSetString(isa));
  if(isa_dir ! = nullptr) { *isa_dir = dir; }// Get the base part of the file without the extension.
  std::string file = location.substr(pos+1);
  pos = file.rfind('. ');
  if (pos == std::string::npos) {
    *error_msg = "Dex location " + location + " has no extension.";
    return false;
  }
  std::string base = file.substr(0, pos);

  *odex_filename = dir + "/" + base + ".odex";
  return true;
}
Copy the code

As you can see above, add an oat/ file in the same directory as the DEX file as the.odex storage directory.

conclusion

About me