天天看點

Android動态加載DEX檔案流程分析

Android提供機制來在應用運作的過程中動态加載dex檔案中的類,ClassLoader是抽象類,一般使用DexClassLoader或者PathClassLoader加載,他們的差別是

  • DexClassLoader可以加載jar/apk/dex,可以從SD卡中加載未安裝的apk
  • PathClassLoader隻能加載系統中已經安裝過的apk

首先看一下動态代碼加載如何加載dex檔案中的類,下面給出一個DexClassLoader動态加載dex檔案的例子:

final File optimizedDexOutputPath = new File(Environment
        .getExternalStorageDirectory().toString()
        + File.separator
        + "test.dex");
DexClassLoader cl = new DexClassLoader(
        optimizedDexOutputPath.getAbsolutePath(), Environment
                .getExternalStorageDirectory().toString(), null,
        getClassLoader());
Class Clazz = null;

try {
    Clazz = cl.loadClass("com.dynamic.DynamicTest");
    Object lib = Clazz.newInstance();
    Method m = Clazz.getDeclaredMethod("hehe");
    m.invoke(lib);
} catch (Exception exception) {
    // Handle exception gracefully here.
    exception.printStackTrace();
}
           

接下來以這個例子往下分析DexClassLoader是如何一步一步加載dex檔案中的類,底層是如何将dex檔案轉換成oat檔案的。

DexClassLoader建立流程(java層):

首先看DexClassLoader構造函數(libcore\dalvik\src\main\java\dalvik\system\DexClassLoader.java):

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}
           

DexClassLoader隻是簡單的對BaseDexClassLoader做了一下封裝,具體的實作還是在父類裡(libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java):

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
        String libraryPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
           

這裡建立了一個DexPathList執行個體,後邊類的查找都會用到(libcore\dalvik\src\main\java\dalvik\system\DexPathList.java):

public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    ......    
    this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
    ......
}

private static Element[] makePathElements(List<File> files, File optimizedDirectory,
                                          List<IOException> suppressedExceptions) {
    List<Element> elements = new ArrayList<>();
    for (File file : files) {
        File zip = null;
        File dir = new File("");
        DexFile dex = null;
        String path = file.getPath();
        String name = file.getName();

        if (path.contains(zipSeparator)) {
            String split[] = path.split(zipSeparator, );
            zip = new File(split[]);
            dir = new File(split[]);
        } else if (file.isDirectory()) {
            elements.add(new Element(file, true, null, null));
        } else if (file.isFile()) {
            if (name.endsWith(DEX_SUFFIX)) {
                // Raw dex file (not inside a zip/jar).
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else {
                zip = file;
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException suppressed) {
                    suppressedExceptions.add(suppressed);
                }
            }
        } else {
            System.logW("ClassLoader referenced unknown path: " + file);
        }

        if ((zip != null) || (dex != null)) {
            elements.add(new Element(dir, false, zip, dex));
        }
    }

    return elements.toArray(new Element[elements.size()]);
}

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, );
    }
}

private static String optimizedPathFor(File path,
        File optimizedDirectory) {
    String fileName = path.getName();
    if (!fileName.endsWith(DEX_SUFFIX)) {
        int lastDot = fileName.lastIndexOf(".");
        if (lastDot < ) {
            fileName += DEX_SUFFIX;
        } else {
            StringBuilder sb = new StringBuilder(lastDot + );
            sb.append(fileName, , lastDot);
            sb.append(DEX_SUFFIX);
            fileName = sb.toString();
        }
    }

    File result = new File(optimizedDirectory, fileName);
    return result.getPath();
}
           

optimizedDirectory是用來緩存我們需要加載的dex檔案的,并建立一個DexFile對象,如果它為null,那麼會直接使用dex檔案原有的路徑來建立DexFile對象。

optimizedDirectory必須是一個内部存儲路徑,無論哪種動态加載,加載的可執行檔案一定要存放在内部存儲。DexClassLoader可以指定自己的optimizedDirectory,是以它可以加載外部的dex,因為這個dex會被複制到内部路徑的optimizedDirectory;而PathClassLoader沒有optimizedDirectory,是以它隻能加載内部的dex,這些大都是存在系統中已經安裝過的apk裡面的。

DexFile建立流程(native層):

從上一步的分析,DexClassLoader最終會調用DexFile類将加載的dex檔案緩存在DexPathList對象中。

DexFile建立流程如下(libcore\dalvik\src\main\java\dalvik\system\DexFile.java):

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

public DexFile(String fileName) throws IOException {
    mCookie = openDexFile(fileName, null, );
    mFileName = fileName;
    guard.open("close");
}

private static Object openDexFile(String sourceName, String outputName, int flags) throws IOException {
    return openDexFileNative(new File(sourceName).getAbsolutePath(),
                             (outputName == null) ? null : new File(outputName).getAbsolutePath(),
                             flags);
}

private static native Object openDexFileNative(String sourceName, String outputName, int flags);
           

DexFile構造函數調用openDexFile,然後調用本地方法openDexFileNative,進入到native層的dex檔案建立。

openDexFileNative函數在art\runtime\native\dalvik_system_DexFile.cc檔案中下面的代碼導出了openDexFileNative符号。

static JNINativeMethod gMethods[] = {
  NATIVE_METHOD(DexFile, closeDexFile, "(Ljava/lang/Object;)V"),
  NATIVE_METHOD(DexFile, defineClassNative,
                "(Ljava/lang/String;Ljava/lang/ClassLoader;Ljava/lang/Object;)Ljava/lang/Class;"),
  NATIVE_METHOD(DexFile, getClassNameList, "(Ljava/lang/Object;)[Ljava/lang/String;"),
  NATIVE_METHOD(DexFile, isDexOptNeeded, "(Ljava/lang/String;)Z"),
  NATIVE_METHOD(DexFile, getDexOptNeeded,
                "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)I"),
  NATIVE_METHOD(DexFile, openDexFileNative,
                "(Ljava/lang/String;Ljava/lang/String;I)Ljava/lang/Object;"),
};
           

通過觀察上面的全局變量可以發現,openDexFileNative所代表的函數是DexFile_openDexFileNative。

static jobject DexFile_openDexFileNative(
    JNIEnv* env, jclass, jstring javaSourceName, jstring javaOutputName, jint) {
  ScopedUtfChars sourceName(env, javaSourceName);
  if (sourceName.c_str() == nullptr) {
    return ;
  }
  NullableScopedUtfChars outputName(env, javaOutputName);
  if (env->ExceptionCheck()) {
    return ;
  }

  ClassLinker* linker = Runtime::Current()->GetClassLinker();
  std::vector<std::unique_ptr<const DexFile>> dex_files;
  std::vector<std::string> error_msgs;

  dex_files = linker->OpenDexFilesFromOat(sourceName.c_str(), outputName.c_str(), &error_msgs);

  if (!dex_files.empty()) {
    jlongArray array = ConvertNativeToJavaArray(env, dex_files);
    if (array == nullptr) {
      ScopedObjectAccess soa(env);
      for (auto& dex_file : dex_files) {
        if (Runtime::Current()->GetClassLinker()->IsDexFileRegistered(*dex_file)) {
          dex_file.release();
        }
      }
    }
    return array;
  } else {
    ScopedObjectAccess soa(env);
    CHECK(!error_msgs.empty());
    // The most important message is at the end. So set up nesting by going forward, which will
    // wrap the existing exception as a cause for the following one.
    auto it = error_msgs.begin();
    auto itEnd = error_msgs.end();
    for ( ; it != itEnd; ++it) {
      ThrowWrappedIOException("%s", it->c_str());
    }

    return nullptr;
  }
}
           

這個函數非常大,其主要的幾個動作如下:

  • 構造OatFileAssistant對象:這個對象的主要作用是協助dex檔案解析成oat檔案
  • 周遊ClassLinker的成員變量:oat_files_,目的是檢視目前的dex檔案是否已經被解析成oat了
  • 如果目前dex已經被解析成了oat檔案,那麼就跳過dex的解析,直接進入下一步),反之,如果目前dex檔案并沒有被解析過,那麼就會走:oat_file_assistant.MakeUpToDate
  • 填充dex_files對象并傳回,這裡也有兩種情況(區分oat or dex):

    1.oat_file_assistant.LoadDexFiles

    2.DexFile::Open

其中我們需要關注的點其實隻有兩點共兩個函數:

  • oat_file_assistant.MakeUpToDate
  • oat_file_assistant.LoadDexFiles

oat_file_assistant.MakeUpToDate的實作如下(art\runtime\Oat_file_assistant.cc):

bool OatFileAssistant::MakeUpToDate(std::string* error_msg) {
  switch (GetDexOptNeeded()) {
    case kNoDexOptNeeded: return true;
    case kDex2OatNeeded: return GenerateOatFile(error_msg);
    case kPatchOatNeeded: return RelocateOatFile(OdexFileName(), error_msg);
    case kSelfPatchOatNeeded: return RelocateOatFile(OatFileName(), error_msg);
  }
  UNREACHABLE();
}
           

由于我們這裡是第一次load,是以就會進入到GenerateOatFile:

bool OatFileAssistant::GenerateOatFile(std::string* error_msg) {
  ......
  std::vector<std::string> args;
  args.push_back("--dex-file=" + std::string(dex_location_));
  args.push_back("--oat-file=" + oat_file_name);
  if (!OS::FileExists(dex_location_)) {
    *error_msg = "Dex location " + std::string(dex_location_) + " does not exists.";
    return false;
  }
  if (!Dex2Oat(args, error_msg)) {
    TEMP_FAILURE_RETRY(unlink(oat_file_name.c_str()));
    return false;
  }
  ClearOatFileCache();
  return true;
}
           

可以看到這支函數的主要最用是調用Dex2Oat,在最終調用之前會去檢查一下檔案是否存在。Dex2Oat函數實作如下:

bool OatFileAssistant::Dex2Oat(const std::vector<std::string>& args,
                               std::string* error_msg) {
  ......
  std::vector<std::string> argv;
  argv.push_back(runtime->GetCompilerExecutable());
  argv.push_back("--runtime-arg");
  ......
  return Exec(argv, error_msg);
}
           

argv是此處的關鍵,不光會傳入dst,src等基本資訊,還會有諸如debugger,classpath等資訊,接下來跳轉到(art/runtime/utils.cc)執行Exec函數:

bool Exec(std::vector<std::string>& arg_vector, std::string* error_msg) {
  ......
  const char* program = arg_vector[].c_str();
  ......
  pid_t pid = fork();
  if (pid == ) {
    ......
    execv(program, &args[]);
    ......
  } else {
    ......
    pid_t got_pid = TEMP_FAILURE_RETRY(waitpid(pid, &status, ));
    ......
  return true;
}
           

這個函數的重點是在fork,而fork之後會在子程序執行arg帶入的第一個參數指定的那個可執行檔案。

而在fork的父程序,則會繼續監聽子程序的運作狀态并一直wait,是以對于上層來說..這邊就是一個同步調用了。

再回頭看一下argv的第一個參數:argv.push_back(runtime->GetCompilerExecutable());位于(art\runtime\Runtime.cc)

std::string Runtime::GetCompilerExecutable() const {
  if (!compiler_executable_.empty()) {
    return compiler_executable_;
  }
  std::string compiler_executable(GetAndroidRoot());
  compiler_executable += (kIsDebugBuild ? "/bin/dex2oatd" : "/bin/dex2oat");
  return compiler_executable;
}
           

是以..如果非debug狀态下,我們用到可執行檔案就是:/bin/dex2oat,是以到這邊,我們大緻就了解了dex檔案是如何轉換到oat檔案的。

DexClassLoader查找類過程

當DexClassLoader.loadClass被調用時,同樣繼承自父類BaseDexClassLoader,BaseDexClassLoader又繼承自父類ClassLoader(libcore\libart\src\main\java\java\lang\ClassLoader.java):

public Class<?> loadClass(String className) throws ClassNotFoundException {
    return loadClass(className, false);
}

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    Class<?> clazz = findLoadedClass(className);
    if (clazz == null) {
        ClassNotFoundException suppressed = null;
        try {
            clazz = parent.loadClass(className, false);
        } catch (ClassNotFoundException e) {
            suppressed = e;
        }
        if (clazz == null) {
            try {
                clazz = findClass(className);
            } catch (ClassNotFoundException e) {
                e.addSuppressed(suppressed);
                throw e;
            }
        }
    }
    return clazz;
}
           

loadClass方法調用了findClass方法,而BaseDexClassLoader重載了這個方法(libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java):

protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    Class c = pathList.findClass(name, suppressedExceptions);
    if (c == null) {
        ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
        for (Throwable t : suppressedExceptions) {
            cnfe.addSuppressed(t);
        }
        throw cnfe;
    }
    return c;
}
           

這裡就調用了之前建立DexClassLoader時建立的pathList對象的findClass方法(libcore\dalvik\src\main\java\dalvik\system\DexPathList.java):

public Class findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}
           

這裡周遊了之前pathList所有的DexFile執行個體,其實也就是周遊了所有加載過的dex檔案,再調用loadClassBinaryName方法一個個嘗試能不能加載想要的類(libcore\dalvik\src\main\java\dalvik\system\DexFile.java):

public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
    return defineClass(name, loader, mCookie, suppressed);
}

private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                 List<Throwable> suppressed) {
    Class result = null;
    try {
        result = defineClassNative(name, loader, cookie);
    } catch (NoClassDefFoundError e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    } catch (ClassNotFoundException e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    }
    return result;
}

private static native Class defineClassNative(String name, ClassLoader loader, Object cookie)
        throws ClassNotFoundException, NoClassDefFoundError;
           

loadClassBinaryName實際就是封裝調用了defineClass(從代碼中可以看出,傳入的參數中有一個是mCookie,這個變量儲存的正是之前加載的dex檔案清單),而defineClass實際調用的是native方法defineClassNative,隻是額外做了異常處理.defineClassNative的實作在art/runtime/native/dalvik_system_DexFile.cc

參考資料:

Android動态加載——DexClassloader分析

Art下DexClassLoader将dex轉化為oat檔案格式的過程

Android ClassLoader