On the cause and effect and implementation principle of Android hot repair.

Posted by srividya on Wed, 09 Feb 2022 17:10:06 +0100

I should have spent the most time in the last month, but I didn't write a few blogs. I'm really ashamed. Recently, in the integrated hot repair, technology sharing happens to take place on Monday, so let's take a good look at the causes and consequences of hot repair.

First, we need to hold the following questions:

  1. What is hot repair? What problems can it help me solve?
  2. Background of thermal repair?
  3. What is the basic principle of thermal repair?
  4. How to select a hot fix frame?
  5. Precautions for thermal repair
  6. Thermal repair and multi-channel?
  7. Automated build and hot fix?

There are seven questions above. If you are a new student, you may not know the last two very well. It is suggested to make up lessons by yourself. So the five most basic questions, we must understand that this is the basic need for each developer to learn a new knowledge.

Test environment: Android 9.0 -P

What is hot repair? What problems can it help me solve?

In fact, in simple terms, hot repair is a dynamic loading technology. For example, a bug occurs in a product on your line:

Traditional process: debug - > test - > release new version - > user installation (the audit time of each platform is different, and users need to download or update manually) In the case of integrated hot repair: dubug - > test - > push patch - > automatically download patch repair (if the user doesn't know the situation, automatically download the patch and repair it)

In contrast, it is not difficult for us to find these disadvantages in the traditional process:

  1. The price of publishing is high
  2. The cost of user download and installation is too high
  3. bug repair is not timely, depending on the audit time of each platform, etc

Background of thermal repair?

  • High cost of app distribution
  • Integrate some frequently changing business logic with H5, but this scheme requires learning cost, and it is still impossible to repair the code that cannot be converted to H5 form;
  • Instant Run

Among the above three reasons, we mainly talk about Instant Run:

Android Studio2. At 0, an Instant Run function was added, and the hot repair schemes of major manufacturers refer to the code of Instant Run to a great extent in terms of code and resources. Therefore, it can be said that Instant Run is the main reason to promote Android hot repair. How does Instant Run achieve this internally?

  1. Build a new AssetManager (resource management framework), and call the addAssetPath through reflection to add the complete new resource to AssetManager, so as to get an AssetManager containing all new resources
  2. Find all places previously referenced to the original AssetManager, and replace the reference with a new AssetManager through reflection

Refer to self < explore the principle of Android hot repair technology in depth >

For more explanation of InstantRun, please refer to:

What is the principle of thermal repair?

We all know that hot repair is equivalent to dynamic loading, so where is the dynamic loading.

Speaking of this, we can't avoid a key point: classloader, so let's start with Java.

We all know that there are four types of Java class loaders, namely:

  • Bootstarp ClassLoader
  • Extension ClassLoader
  • App ClassLoader
  • Custom ClassLoader loads its own class file

Class loading process is as follows:

Process: load connect (verify prepare parse) - initialize

  1. load Get the class information (bytecode) from the file and load it into the memory of the JVM
  2. connect Verify: check whether the read structure conforms to the JVM specification Preparation: assign a structure to store class information Resolution: change all references in the constant pool of the class to direct references
  3. initialization Execute the static initialization program to initialize the static variable to the specified value

There are three main mechanisms used

  1. Parental entrustment mechanism
  2. Overall responsibility mechanism
  3. Caching mechanism

In fact, the latter two mechanisms are mainly inherited from the parental entrustment mechanism. detailed For Java class loading, please refer to my other blog After explaining the Java ClassLoader, let's start with Android ClassLoader. Unlike Java, Java ClassLoader can load jar files and Class files, while Android loads Dex files, which requires redesign of relevant ClassLoader classes. So we will be more specific about Android ClassLoader

Source code analysis

By the way, the code version posted here is Android 9.0. After 8.0, there is no difference between PathClassLoader and DexClassLoader, because the only difference parameter, optimized directory, has been abandoned.

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

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
        // First, check if the class has already been loaded
        //Find out whether the current class has been loaded
        Class<?> 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 it has not been loaded, call the root loader to load and implement the parental delegation mode
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
						
						//The root loader found is still null and can only be loaded by itself
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                c = findClass(name);
            }
        }
        return c;
}

Here's a question: can the JVM parent delegation mechanism be broken? Keep questions first.

We mainly look at his findClass method

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

This method is a null implementation, that is, we developers need to do it ourselves.

From the above foundation, we know that in Android, there are PathClassLoader and DexClassLoader, and both of them inherit from BaseDexClassLoader, and this BaseDexClassLoader inherits from ClassLoader, and gives the findClass method to the subclass to implement. Therefore, we start with its two subclasses PathClassLoader and DexClassLoader to see how they are handled.

Here, because Android Studio cannot view the relevant specific implementation source code, we can query from the source code website (the title can be directly clicked to enter the corresponding source code):

PathClassLoader

public class PathClassLoader extends BaseDexClassLoader {
    /**
     * Creates a {@code PathClassLoader} that operates on a given list of files
     * and directories. This method is equivalent to calling
     * {@link #PathClassLoader(String, String, ClassLoader)} with a
     * {@code null} value for the second argument (see description there).
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param parent the parent class loader
     */
    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).
     * </ul>
     *
     * 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 librarySearchPath 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 librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

From the comments, it can be found that PathClassLoader is used to load files or directories on the local file system, because the second parameter of BaseDexClassLoader it calls is null, that is, the optimized Dex file is not passed in. Note: after Android 8.0, the second parameter of BaseClassLoader is null (optimized directory), so there is no difference between DexClassLoader and PathClassLoader

DexClassLoader

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 this parameter is deprecated and has no effect since API level 26.
     * @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, null, librarySearchPath, parent);
    }
}

DexClassLoader is used to load jar s and apk s. In fact, it also includes zip files or directly load dex files. It can be used to execute uninstalled code or code not loaded by applications, that is, the code we repaired.

Note: classloader (classpathloader. 0) is null, so there is no difference between classloader and Android

We can see from the above that they all inherit from BaseDexClassLoader, and their real implementation behavior is the calling parent method, so let's take a look at BaseDexClassLoader

BaseDexClassLoader

public class BaseDexClassLoader extends ClassLoader {

  private static volatile Reporter reporter = null;
	
  //Core concerns
   private final DexPathList pathList;


   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) {
       super(parent);
       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.
        super(parent);
        this.pathList = new DexPathList(this, dexFiles);
    }
	
  //Core method
    @Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
  			//exception handling
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
  			//This is just a transit. The focus is on 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) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

 
  ...
}

From the above, we can find that BaseDexClassLoader is not the main processing class, so we continue to find DexPathList

DexPathList

final class DexPathList {
  //file extension
	private static final String DEX_SUFFIX = ".dex";
	private static final String zipSeparator = "!/";

** class definition context */
private final ClassLoader definingContext;

//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
  	//We 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);
    allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);

    this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);

    if (suppressedExceptions.size() > 0) {
        this.dexElementsSuppressedExceptions =
            suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
    } else {
        dexElementsSuppressedExceptions = null;
    }
}
  
  
  
static class Element {
	//When the dex file is null, it means jar / dex Jar file
 private final File path;
	
 //A concrete implementation of 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;
 }
	
 public Class<?> findClass(String name, ClassLoader definingContext,
               List<Throwable> suppressed) {
   					//Core point, DexFile
           return dexFile != 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.
  */
 @Deprecated
 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 into DexFile and save them into Eelement array at the same time
//Why? The purpose is to let findClass realize
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;
  //Traverse all files
  for (File file : files) {
      if (file.isDirectory()) {
         	//If there is a folder, find the internal query of the folder
          elements[elementsPos++] = new Element(file);
        //If it's a file
      } else if (file.isFile()) {
          String name = file.getName();
          DexFile dex = null;
        //Judge 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.
                   */
                  suppressedExceptions.add(suppressed);
              }

              if (dex == null) {
                  elements[elementsPos++] = new Element(file);
              } else {
                  elements[elementsPos++] = new Element(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);
  }
  return elements;
}
  
  ---
 private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
Element[] elements)throws IOException {
     //Determine whether the replicable folder is null
     if (optimizedDirectory == null) {
         return new DexFile(file, loader, elements);
     } else {
       	//If it is not null, decompress it before creating it
         String optimizedPath = optimizedPathFor(file, optimizedDirectory);
         return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
     }
 }
  
  -----
public Class<?> findClass(String name, List<Throwable> suppressed) {
    //Traverse the initialized DexFile array, and the Element calls the findClass method to generate it
    for (Element element : dexElements) {
      	//
        Class<?> clazz = element.findClass(name, definingContext, suppressed);
        if (clazz != null) {
            return clazz;
        }
    }

    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

The above code is a little complex. I have extracted some points that we need to pay attention to for our analysis:

In BaseDexClassLoader, we found that the final loading of classes is carried out by DexPathList, so we entered the DexPathList class. We can find that there is a key method to pay attention to makeDexElements during initialization. The main function of this method is to convert all files in the specified path into DexFile and save them into Eelement array at the same time.

The findClass() in the DexPathList called at the beginning is the findClass method called by Element, and the findClass method of ement is actually the loadClassBinaryName method called by DexFile. Therefore, with this question, let's enter the class of DexFile.

DexFile

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.
/
@ReachabilitySensitive
private Object mCookie;

private Object mInternalCookie;
private final String 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<Throwable> suppressed) {
     Class result = null;
     try {
       //A JNI layer method is called here
         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;

From the loadClassBinaryName method, we find that the defineClass method is called, and finally the defineClassNative method is called. The defineClassNative method is a JNI layer method, so we can't know how to do it. But let's think about it. From the beginning of BaseDexClassLoader to the current DexFile, we have found the bottom from the entry. It's not difficult to guess. The internal part of this defineClassNative method is C/C + + to help us generate the dex file we need with bytecode or other methods, which is also the most difficult part.

Finally, we use another figure to summarize the class loading process in Android.

After understanding the above knowledge, let's summarize the principle of hot repair in Android?

Since there are already DexClassLoader and PathClassLoader in Android, I can directly replace my own Dex file in the loading process, that is, I can load my own Dex file first, so as to realize hot repair.

Is it really that simple? What are the difficulties of thermal repair?

  • Resource repair
  • Code repair
  • so library repair

With this problem in mind, we Android developers must consider how to choose the most appropriate framework. Let's analyze the differences between various schemes.

How to select a hot fix frame?

At present, there are many hot repair frameworks in the market. I found a figure from Alibaba hot repair website to compare:

platform

Sophix

AndFix

Tinker

Qzone

Robust

Immediate effect

yes

yes

no

no

yes

Performance loss

less

less

more

more

less

Intrusive packaging

Non intrusive packaging

Non intrusive packaging

Rely on intrusive packaging

Rely on intrusive packaging

Rely on intrusive packaging

Rom volume

less

less

more

less

less

Access complexity

Fool access

Relatively simple

complex

Relatively simple

complex

Patch package size

less

less

less

more

commonly

Full platform support

yes

yes

yes

yes

yes

Class substitution

yes

yes

yes

yes

no

so substitution

yes

no

yes

no

no

Resource replacement

yes

no

yes

yes

no

The simple division is three giants, Ali, Tencent and meituan. We don't use whoever supports many functions. We need to consider comprehensively in terms of access.

For detailed technical comparison, please refer to Selection of Android hot repair technology -- Analysis of three schools

In my personal experience, I have experienced Tinker and Sophix

Tinker

Tinker integration is a little troublesome. Personally, I think it's very simple. Moreover, TinkerPatch, a patch management system, charges a fee (there is a free quota). Patch distribution is slow, and it takes about 5 minutes to wait. Tinker has a free background version, Bugly. Patch management is free. Tinker for hot fix is very integrated... em, it is recommended to read more tutorials on the official website and watch videos. Because there is patch upload monitoring, it takes 5-10 minutes to issue a patch and about 10 minutes to withdraw the patch. Moreover, it may not take effect at one time. It takes many times to observe the log in the background to withdraw the patch. (test equipment: Xiaomi 5s Plus, Android 8.0) Final summary: Advantages: free, simple Disadvantages: the integration is troublesome. If there is a problem, you can't get a solution at the first time. After all, it's free to understand it Performance method: it will take effect after cold start

Sophix

The official website tutorial is detailed, completely stupid, fast response, problems and high efficiency. After all, it costs money. Performance: cold start + immediate response (conditional), A little: most functions, most supported versions, and fast problem solving Disadvantages: payment

If there is no experience in other frameworks, there will be no self-evaluation. For the implementation principle of the above scheme, you can click Selection of Android hot repair technology -- Analysis of three schools , or Baidu search. Simple understanding is not difficult.

Precautions for thermal repair

With thermal repair, can we do whatever we want?

Start talking:

It's not that hot fixes are limited to various types of devices, and there is also the possibility of failure. Therefore, we developers should also be in awe of the patch package. For thermal repair, it is also due to the strict process, but our daily development should at least ensure the following points: Debug - > patch package - > development equipment test - > gray distribution (conditional distribution) - > full distribution

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

Thermal repair and multi-channel

Multi channel packaging uses meituan's one click packaging scheme. In fact, the patch package will not be affected, because the code of the patch package is generally the same, but the premise is to ensure that there is no problem with each channel benchmark package. If there is a difference in the changed code, it needs to be supplemented separately for this channel.

Automated build and hot fix

Android development generally integrates jenkins or other automatic packaging tools. Our benchmark packages are generally in the app/build/bakApk directory, so we can write shell commands to move the generated benchmark package to a specific folder when packaging in jenkins. Tinker and sophix support the server background, so we can also upload the patch package through the automatic construction tool. If the corresponding hot repair framework does not support server management, we can upload the patch package to the specified folder, and then when we open the app, access our server interface, pull down the latest patch package, and then synthesize it in the service. However, Tinker (bugly) and sophix support background management, so we choose which scheme to use.

The hot fix has been basically written here. There are so many scattered ones. In fact, the difficulty is not the hot fix, but the class loading process and some basic related knowledge in Android. After understanding these, we can really understand how those excellent frameworks are repaired.

It would be a great honor if this article can help you. If there are any mistakes or questions, you are also welcome to put forward them.