This is the first day of my participation in the More text Challenge. For details, see more text Challenge

First, we need to hold the following questions:

  1. What is a hotfix? What problems can it help me solve?
  2. The background of hotfix?
  3. What is the rationale for hotfix?
  4. How do I select a hotfix framework?
  5. Precautions for hot fixes
  6. Hot fix vs. multi-channel?
  7. Automated builds and hotfixes?

There are altogether 7 questions above, if it is a new student, the latter two may not be very understanding, it is suggested to make up for their own learning. So there are five basic questions that we need to understand that are the basic things that every developer needs to do to learn a new piece of knowledge.

What is a hotfix? What problems can it help me solve?

In simple terms, a hot fix is a dynamic loading technique. For example, if you have a bug in a product online:

Traditional process: debug-> Test -> release -> user install (the review time varies on different platforms, and users need to manually download or update)

In the case of integrated hotfix: Dubug -> Test -> Push patch -> Automatic download patch fix (user unaware of the situation, automatic download patch and fix)

By contrast, it is not difficult to find that the traditional process has the following drawbacks:

  1. Distribution edition is costly
  2. The user download and install cost is too high
  3. Bug fixes are not timely, depending on the review time of each platform, etc

Hot fix generates background?

  • App distribution costs are high
  • Integrating some oft-changing business logic with H5 requires a learning cost and is still unfixable for code that can’t be converted to H5 form;
  • Instant Run

We’ll talk about Instant Run for the following three reasons:

In Android Studio2.0, a new function of Instant Run was added, and the hot fix of various factories, in terms of code, resources and other aspects of the implementation are largely based on the code of Instant Run. So it’s safe to say that Instant Run is the main driver of Android hotfixes.

So how does Instant Run do this internally?

  1. Build a new AssetManager(resource management framework) and add the complete new resource to the AssetManager by calling the addAssetPath through reflection, thus getting an AssetManager containing all the new resources.
  2. Find all previous references to the original AssetManager and replace the references with the new AssetManager by reflection.

From The Android Hot Fix Theory

For more details on InstantRun, see:

How does hotfix work?

We all know that hot fixes are equivalent to dynamic loading, so where is dynamic loading?

There’s no escaping the key point here: ClassLoader, so let’s start with Java.

As we all know, there are four types of class loaders in Java. They are:

  • Bootstarp ClassLoader
  • Extension ClassLoader
  • App ClassLoader Loads the application ClassLoader
  • The Custom ClassLoader loads its own class files

The class loading process is as follows:

Procedure: load – connect (validate – prepare – parse)- initialize

  1. loading

    The class information (bytecode) is retrieved from the file and loaded into the MEMORY of the JVM

  2. The connection

    Validation: Checks that the structure read complies with the JVM specification

    Prepare: Allocate a structure to store information about the class

    Parse: change all references in the class’s constant pool to direct references

  3. Initialize the

    Executes a static initializer to initialize a static variable to a specified value

There are three main mechanisms used:

  1. Parent delegation mechanism
  2. Overall responsibility mechanism
  3. Caching mechanisms

In fact, the latter two mechanisms are mainly continuation of the parent delegation mechanism. For detailed Java class loading, see another of my blogs

Now that we’ve explained the Java ClassLoader, let’s start with the Android ClassLoader. Unlike Java, the Java ClassLoader can load jar files and Class files, while Android loads the Dex file. This requires a redesign of the related ClassLoader classes. So we’ll talk a little bit more about the Android ClassLoader

The source code parsing

Here, by the way, the version of the code posted here is Android 9.0. After 8.0, there is no difference between PathClassLoader and DexClassLoader because the only distinguishing parameter optimizedDirectory is deprecated.

The first is loadClass, which is our core method for class loading:

protectedClass<? > loadClass(String name,boolean resolve)
    throws ClassNotFoundException
        // First, check if the class has already been loaded
        // Check whether the current class has been loadedClass<? > c = findLoadedClass(name);if (c == null) {
            try {
                if(parent ! =null) {
                		// Check whether the parent loader has been loaded
                    c = parent.loadClass(name, false);
                } else {
                		// If not, call the root loader to load the implementation of parent delegate modec = findBootstrapClassOrNull(name); }}catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
						// Find the root loader is still null, you can only load by yourself
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.c = findClass(name); }}return c;
Copy the code

The question here is, can the JVM parent delegation mechanism be broken? Reserve questions for now.

Let’s focus on his findClass method

protectedClass<? > findClass(String name)throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
Copy the code

This method is a null implementation, which means we need to do it ourselves.

In Android, we have a PathClassLoader and a DexClassLoader, And they all inherit from BaseDexClassLoader, which in turn inherits from ClassLoader and gives the findClass method to the subclass to implement. So let’s start with its two subclasses, PathClassLoader and DexClassLoader, and see what they do.

Because Android Studio is not able to view the relevant specific implementation source code, so we query from the source website:


public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null.null, parent);
   // dexPath: list of files to be loaded. The file can be JAR/APK/ZIP containing classes.dex, or you can use the classes.dex file directly
 // librarySearchPath: the directory for storing native libraries to load
 / / parent, the father this
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent); }}Copy the code

PathClassLoader is used to load files or directories on the local file system, because it calls BaseDexClassLoader with null as the second argument, which means the optimized Dex file is not passed in.

Note: After Android 8.0, BaseClassLoader’s second argument (optimizedDirectory) is null, so DexClassLoader is no different from PathClassLoader


public class DexClassLoader extends BaseDexClassLoader {
   // dexPath: list of files to be loaded. The file can be JAR/APK/ZIP containing classes.dex, or you can use the classes.dex file directly
 // optimizedDirectory: stores the optimized dex. The value can be empty
 // librarySearchPath: the directory for storing native libraries to load
 / / parent, the father this
  public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent); }}Copy the code

The DexClassLoader is used to load jar files, apk files, and actually zip files or directly load dex files. It can be used to execute uninstalled code or code that has not been loaded by the application, which is the code we fixed.

Note: After Android 8.0, BaseClassLoader’s second argument (optimizedDirectory) is null, so DexClassLoader is no different from PathClassLoader

As you can see above, they all inherit from BaseDexClassLoader, and their real implementation behavior is the parent method called, so let’s take a look at BaseDexClassLoader.


public class BaseDexClassLoader extends ClassLoader {

  private static volatile Reporter reporter = null;
  // Core concerns
   private finalDexPathList pathList; The BaseDexClassLoader constructor takes four arguments with the following meanings:// dexPath: list of files to be loaded. The file can be JAR/APK/ZIP containing classes.dex, or you can use the classes.dex file directly
 // optimizedDirectory: stores the optimized dex. The value can be empty
 // librarySearchPath: the directory for storing native libraries to load
 / / parent, the father this
   public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
      	// Classloader,dex path, directory list, internal folder
       this(dexPath, optimizedDirectory, librarySearchPath, parent, false);

   public BaseDexClassLoader(String dexPath, File optimizedDirectory,
           String librarySearchPath, ClassLoader parent, boolean isTrusted) {
       this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);

       if(reporter ! =null) { reportClassLoaderChain(); }}...public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
        // TODO We should support giving this a library search path maybe.
        this.pathList = new DexPathList(this, dexFiles);
  // Core method
protectedClass<? > findClass(String name)throws ClassNotFoundException {
  			// Exception handling
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
  			// This is just a transit, focus on the DexPathList
        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) {
            throw cnfe;
        returnc; }... }Copy the code

As we can see from the above, BaseDexClassLoader is not actually the main class to process, so let’s continue to look for DexPathList.


final class DexPathList {
  // File suffix
	private static final String DEX_SUFFIX = ".dex";
	private static final String zipSeparator = ! "" /"; 阿鲁纳恰尔邦class definition context* /private final ClassLoader definingContext;

// The inner class Element
private Element[] dexElements;

public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) {
    this(definingContext, dexPath, librarySearchPath, optimizedDirectory, false);

DexPathList(ClassLoader definingContext, String dexPath,
        String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
    if (definingContext == null) {
        throw new NullPointerException("definingContext == null");

    if (dexPath == null) {
        throw new NullPointerException("dexPath == null");

    if(optimizedDirectory ! =null) {
        if(! optimizedDirectory.exists()) {throw new IllegalArgumentException(
                    "optimizedDirectory doesn't exist: "
                    + optimizedDirectory);

        if(! (optimizedDirectory.canRead() && optimizedDirectory.canWrite())) {throw new IllegalArgumentException(
                    "optimizedDirectory not readable/writable: "+ optimizedDirectory); }}this.definingContext = definingContext;

    ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    // save dexPath for BaseDexClassLoader
  	// Let's focus on the makeDexElements method
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                       suppressedExceptions, definingContext, isTrusted);
    this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
    this.systemNativeLibraryDirectories =
            splitPaths(System.getProperty("java.library.path"), true);
    List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);

    this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);

    if (suppressedExceptions.size() > 0) {
        this.dexElementsSuppressedExceptions =
            suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
    } else {
        dexElementsSuppressedExceptions = null; }}static class Element {
	// If the dex file is null, it indicates the jar file /dex
 private final File path;
 // A concrete implementation of the Android virtual machine file in Android
 private final DexFile dexFile;

 private ClassPathURLStreamHandler urlHandler;
 private boolean initialized;

 /** * Element encapsulates a dex file. This may be a plain dex file (in which case dexZipPath * should be null), or a jar (in which case dexZipPath should denote the zip file). */
 public Element(DexFile dexFile, File dexZipPath) {
     this.dexFile = dexFile;
     this.path = dexZipPath;

 public Element(DexFile dexFile) {
     this.dexFile = dexFile;
     this.path = null;

 public Element(File path) {
   this.path = path;
   this.dexFile = null;
 publicClass<? > findClass(String name, ClassLoader definingContext, List<Throwable> suppressed) {// Core point, DexFile
           returndexFile ! =null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                   : null;
  * Constructor for a bit of backwards compatibility. Some apps use reflection into
  * internal APIs. Warn, and emulate old behavior if we can. See b/33399341.
  * @deprecated The Element class has been split. Use new Element constructors for
  *             classes and resources, and NativeLibraryElement for the library
  *             search path.
 public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) {
     System.err.println("Warning: Using deprecated Element constructor. Do not use internal"
             + " APIs, this constructor will be removed in the future.");
     if(dir ! =null&& (zip ! =null|| dexFile ! =null)) {
         throw new IllegalArgumentException("Using dir and zip|dexFile no longer"
                 + " supported.");
     if(isDirectory && (zip ! =null|| dexFile ! =null)) {
         throw new IllegalArgumentException("Unsupported argument combination.");
     if(dir ! =null) {
         this.path = dir;
         this.dexFile = null;
     } else {
         this.path = zip;
         this.dexFile = dexFile; }}... }...// The main function is to convert all files in the specified path to a DexFile and save them in the Eelement array
// Why do you do this? The goal is for findClass to do that
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
  List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
  Element[] elements = new Element[files.size()];
  int elementsPos = 0;
  // Iterate through all files
  for (File file : files) {
      if (file.isDirectory()) {
         	// If there is a folder, look inside the folder
          elements[elementsPos++] = new Element(file);
        // If it is a file
      } else if (file.isFile()) {
          String name = file.getName();
          DexFile dex = null;
        // Check whether it is a dex file
          if (name.endsWith(DEX_SUFFIX)) {
              // Raw dex file (not inside a zip/jar).
              try {
                	// Create a DexFile
                  dex = loadDexFile(file, optimizedDirectory, loader, elements);
                  if(dex ! =null) {
                      elements[elementsPos++] = new Element(dex, null); }}catch (IOException suppressed) {
                  System.logE("Unable to load dex file: "+ file, suppressed); suppressedExceptions.add(suppressed); }}else {
              try {
                  dex = loadDexFile(file, optimizedDirectory, loader, elements);
              } catch (IOException suppressed) {
                  /* * IOException might get thrown "legitimately" by the DexFile constructor if * the zip file turns out to be resource-only (that is, no classes.dex file * in it). * Let dex == null and hang on to the exception to add to the tea-leaves for * when findClass returns null. */

              if (dex == null) {
                  elements[elementsPos++] = new Element(file);
              } else {
                  elements[elementsPos++] = newElement(dex, file); }}if(dex ! =null&& isTrusted) { dex.setTrusted(); }}else {
          System.logW("ClassLoader referenced unknown path: "+ file); }}if(elementsPos ! = elements.length) { elements = Arrays.copyOf(elements, elementsPos); }returnelements; } -- -- --private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
Element[] elements)throws IOException {
     // Check whether the replicable folder is null
     if (optimizedDirectory == null) {
         return new DexFile(file, loader, elements);
     } else {
       	// If it is not null, unzip it and create it again
         String optimizedPath = optimizedPathFor(file, optimizedDirectory);
         return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements); }} -- -- -- -- --publicClass<? > findClass(String name, List<Throwable> suppressed) {// Iterate over the DexFile array initialized and generated by Element calling the findClass method
    for (Element element : dexElements) {
      	//Class<? > clazz = element.findClass(name, definingContext, suppressed);if(clazz ! =null) {
            returnclazz; }}if(dexElementsSuppressedExceptions ! =null) {
    return null;
Copy the code

The above code is a bit complicated, but I have selected some of the points we need to pay attention to for our analysis:

In the BaseDexClassLoader, we see that the final class loading is done by DexPathList, so we go into the DexPathList class, and we can see that when we initialize, There is a key method that we need to pay attention to makeDexElements. And the main thing that this method does is convert all the files in the path that we’ve specified into a DexFile and store them in an Eelement array.

Instead, the initial call to findClass() in DexPathList is the findClass method called by Element, The findClass method of Emement is actually a loadClassBinaryName method called by DexFile, so with this question in mind, we go to DexFile to check it out.


public final class DexFile {
 If close is called, mCookie becomes null but the internal cookie is preserved if the close
 failed so that we can free resources in the finalizer.
private Object mCookie;

private Object mInternalCookie;
private finalString mFileName; . DxFile(String fileName, ClassLoader loader, DexPathList.Element[] elements)throws IOException {
     mCookie = openDexFile(fileName, null.0, loader, elements);
     mInternalCookie = mCookie;
     mFileName = fileName;
     //System.out.println("DEX FILE cookie is " + mCookie + " fileName=" + fileName);
 // The focus is here
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
     return defineClass(name, loader, mCookie, this, suppressed);

 private static Class defineClass(String name, ClassLoader loader, Object cookie, DexFile dexFile, List
     Class result = null;
     try {
       // A JNI layer method is called
         result = defineClassNative(name, loader, cookie, dexFile);
     } 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, DexFile dexFile)
         throws ClassNotFoundException, NoClassDefFoundError;
Copy the code

From the loadClassBinaryName method, we find that the defineClass method is called, and finally defineClassNative method is called, and defineClassNative method is a JNI layer method, So we don’t know exactly how. But if you think about it, from the BaseDexClassLoader that we started with all the way to the DexFile, we’ve gone all the way to the bottom of the entry, and it’s not hard to guess, The defineClassNative method is internally C/C++ to help us generate the dex file we need in bytecode or whatever, and this is the hardest part.

Finally, let’s summarize the class loading process in Android with a diagram.

After understanding the above knowledge, let’s summarize the principle of Android hotfix?

Since Android already has a DexClassLoader and PathClassLoader, then I can directly replace my own Dex file during the loading process, that is, to load my own Dex file, so that the hot fix is not implemented.

Is it really that simple? What is the difficulty of hotfix?

  • Resources to repair
  • Code to repair
  • So library repair

With this in mind, we Android developers must consider how to choose the most appropriate framework, and we will analyze the differences of each solution.

How do I select a hotfix framework?

At present, there are many hotfix frameworks on the market. I found a graph from ali Hotfix website to compare:

platform Sophix AndFix Tinker Qzone Robust
Effective immediately yes yes no no yes
Performance loss smaller smaller larger larger smaller
Intrusive packing Non-invasive packing Non-invasive packing Rely on intrusive packaging Rely on intrusive packaging Rely on intrusive packaging
Rom volume smaller smaller larger smaller smaller
Access complexity Fool access Relatively simple complex Relatively simple complex
Patch Pack Size smaller smaller smaller larger general
Full platform support yes yes yes yes yes
Class to replace yes yes yes yes no
So to replace yes no yes no no
Resources to replace yes no yes yes no

Simple division is the three giants, Ali, Tencent, Meituan. It is not who supports more functions to use, in terms of access we need to take a comprehensive consideration.

Detailed technical comparison, please refer to Android hot fix technology selection – three schools of analysis

Take it from my own experience: Tinker and Sophix so far


Tinker’s integration is a bit cumbersome, but I think it’s quite simple. Moreover, the patch management system TinkerPatch is charged (there is a free quota), and the patch delivery is slow, with a waiting time of about 5 minutes.

Tinker has a free version of the back end,Bugly, patch management is free, Tinker hotfix, integration is so… Em, it is recommended to read the official website tutorial and watch the video more, because there is patch upload monitoring, it takes 5-10 minutes to issue a patch to take effect, and it takes about 10 minutes to withdraw the patch to take effect, and it may not take effect once. It takes several times to observe logs in the background to realize the patch retraction. (Test device: Xiaomi 5S Plus, Android 8.0)

Final conclusion:

Pros: Free, simple

Disadvantages: integration trouble, problems can not be the first time to get a solution, after all, free to understand

Performance method: This method takes effect only after a cold start


The official website tutorial is detailed, completely stupid, quick response, problems, high efficiency, after all, it costs money.

Performance: cold start + immediate response (conditional),

A little: the most functions, support the most versions, solve the problem quickly

Cons: Paid

Other frameworks do not experience, also do not make self-evaluation. About the implementation principle of the above scheme, you can click on Android hot fix technology selection – three schools of analysis, or Baidu search. Simple understanding is not difficult.

Precautions for hot fixes

With hotfix, we can do whatever we want?

Began to talk:

No, hotfixes are limited to every device and can fail, so we, as developers, should be in awe of fix packs as well.

The same is true for hotfixes, but our daily development should at least ensure the following:

Debug -> Patch package -> Development device test -> Gray level (conditional)-> Full

The following is a solution to the problems ENCOUNTERED in my development.

Hot fix with multiple channels

Multi-channel packaging uses meituan’s one-button packaging scheme. In the case of fix packs, it doesn’t really matter, because fix packs usually change the same code, but we need to make sure that each of our channel base packs is ok. If the code changes are different, it needs to be fixed separately for this channel.

Automated build and hotfix

Android development usually integrates Jenkins or other automated packaging tools. Our base packages are usually in the app/build/bakApk directory, so we can write shell commands to move the generated base packages to a specific folder while packaging in Jenkins. Tinker, Sophix are support backend server, so we can also build tools to upload through automation patches, if the corresponding hot fix framework does not support the server management, then the patches can be uploaded the specified folder, and then our app opens, visit our server interface drop-down the latest patches, It is then synthesized in the service. However, **Tinker(Bugly) ** and Sophix both support back office management, so we choose which solution to use.

I’ve written a lot about hotfixes here, but the hard part is not hotfixes, but the class loading process in Android and some basic knowledge. After understanding this, we can really understand how good frameworks work to fix.

If this article has helped you in any way, I am honored. If there are any mistakes or questions, you are welcome to raise them.