The blue words

Author: MinuitZhttps://www.jianshu.com/p/9e67e3eb129b authorized by the author and the original start

The new version was released on Monday, and by the end of the day, users were up in the air about an untested bug in the app. Well, it didn’t take long to find the problem — an inadvertent null pointer. Although it is only a small bug but not fixed is very affect the user experience ah, if you want to repair online, spread to too wide, all users have to download again. We can fix this bug by stealth

1. Class loading process

Class loading is done by the implementation class of the ClassLoader. As anyone who has ever decompiled apk knows, we end up needing a file in dex format, which is a package of class files, to do our work. So in Android, to load the class file in the dex file, you need to use DexClassLoader or PathClassLoader.

We can open it directly in AS, but we can’t view it properly because it’s system-level source code. We can choose to download the source code, or go to http://androidxref.com/ to find it.

1.1 Let’s start with class loaders

DexClassLoader can load dex files in Android. DexClassLoader can load dex/zip/apk/ JAR files in any directory, but specify optimizedDirectory. Both classloaders inherit from BaseDexClassLoader, and in the constructor, DexClassLoader passes in an additional optimizedDirectory, which is something to remember for a moment

Take a look at the BaseDexClassLoader constructor:

public class BaseDexClassLoader extends ClassLoader {    private final DexPathList pathList;    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent){        super(parent);        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);    }    ...}Copy the code

The constructor initializes the pathList with three parameters, dexPath: the directory where the target file path (usually a dex file or jar/apk/zip file) is located. During hot repair, it is used to specify the new dexoptimizedDirectory: the output directory of dex files (because the dex files will be decompressed when loading the program files in jar/ APk /zip format, and this directory is specially used to store the decompressed dex files). LibraryPath: libraryPath used when loading program files. Parent: parent loader

1.2 The process of loading classes

In BaseDexClassLoader, the constructor is followed by a method called findClass, which loads the corresponding class file in the dex file.

@Overrideprotected Class<? > findClass(String name) throws ClassNotFoundException { List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); Class c = pathList.findClass(name, suppressedExceptions); / / to empty, Throw an exception 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; }Copy the code

In general, it is not hard to understand that after getting the initialized pathList, you can find the corresponding class bytecode file based on the class name, and return the class if there is no exception. Next we follow up on pathList

1.3 DexPathList

The DexPathList source code is here, don’t panic, we only need to know two things so far:

We instantiate DexPathList in BaseDexClassLoader using findClass method, in the findClass of BaseDexClassLoader, essentially called the fndClass method of DexPathList. Forget about the other methods.1-> constructor

public DexPathList(ClassLoader definingContext, String dexPath,        String libraryPath, File optimizedDirectory) {     this.definingContext = definingContext;     ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();     // save dexPath for BaseDexClassLoader     this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,                                         suppressedExceptions);     this.nativeLibraryDirectories = splitPaths(libraryPath, false);     this.systemNativeLibraryDirectories =             splitPaths(System.getProperty("java.library.path"), true);     List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);     allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);     this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null,                                                       suppressedExceptions);    }Copy the code

First, save the classLoader passed in. Next, use the makePathElements method to initialize the Element array.

The makeDexElements() method must be analyzed. This is a long part of the code.

private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) { // 1. Elements = new ArrayList<Element>(); For (File File: files) {ZipFile zip = null; DexFile dex = null; String name = file.getName(); . // if (name.endswith (DEX_SUFFIX)) {dex = loadDexFile(file, optimizedDirectory); } else {zip = file;} else {zip = file; dex = loadDexFile(file, optimizedDirectory); }... // 3. Wrap the dex file or zip file as an Element object and add it to the Element collection. = null) || (dex ! = null)) { elements.add(new Element(file, false, zip, dex)); Return elements. ToArray (new Element[elements. Size ()]); }Copy the code

In general, the constructor of DexPathList is to encapsulate each target (perhaps dex, APK, JAR, zip, the types defined at the beginning) into each Element object and add it to the Element collection.

In fact, Android class loaders (either PathClassLoader or DexClassLoader) only recognize dex files in the end, and loadDexFile() is the core method of loading dex files. Dex can be extracted from JAR, APK, zip, etc. But I won’t analyze it here, because the first goal is done, so I’ll analyze it later.

2 – > findClass method

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; }Copy the code

DexElements is already initialized in the DexPathList constructor, so this method is easy to understand. It simply iterates through the Element array and returns the class as soon as it finds a class with the same name, or null if it doesn’t.

Why call DexFile loadClassBinaryName() to load the class? This is because an Element object corresponds to a dex file, and a dex file contains multiple classes. The Element array contains dex files, not class files. This can be seen from the source code of the Element class and the internal structure of the dex file.

2. Implementation method of hot repair

The BaseDexClassLoader is used to load the class. When loading, the BaseDexClassLoader iterates through the element in the file and retrieves the dex file scheme from the Element. The class file is in dex, and the way to find dex is to traverse the number group, so the principle of hot repair is to put the dex file that has fixed the bug into the head of the set, so that the traverse will traverse the repaired DEX first and find the repaired class. This way, we can fix existing bugs without releasing a new version. Although we cannot change the existing DEX file, the order of traversal is front to back, and the target class in the old dex has no chance of playing.

3. Hand polish a hot fix Demo

With an overview of the thermal repair process, here are a few things to prepare for:

Apk with bugs, and dex files can be obtained to repair the dex files with bugs. Because the repair work needs to be carried out secretly, after all, it is not a glorious thing to have bugs, so I put the dex insertion operation in Splash interface. If there is a dex file, it will be inserted. Otherwise, it will directly enter MainActivity.1-> Write a bug program.

Public class BugTest {public void getBug(Context Context) {int I = 10; int a = 0; Toast.makeText(context, "Hello,Minuit:" + i / a, Toast.LENGTH_SHORT).show(); }}public class MainActivity extends AppCompatActivity { Button btnFix; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); new BugTest().getBug(MainActivity.this); } private void init() { btnFix = findViewById(R.id.btn_fix); }}Copy the code

Running this code is bound to report an error, but let’s first install this code on the phone so we can fix it later.

Next, write SplashActivity and utility classes. You can modify them according to your logic

/***@author Minuit*@time 2018/6/25 0025 15:50*/public class FixDexUtil { private static final String DEX_SUFFIX = ".dex"; private static final String APK_SUFFIX = ".apk"; private static final String JAR_SUFFIX = ".jar"; private static final String ZIP_SUFFIX = ".zip"; public static final String DEX_DIR = "odex"; private static final String OPTIMIZE_DEX_DIR = "optimize_dex"; private static HashSet<File> loadedDex = new HashSet<>(); static { loadedDex.clear(); } /** * load the patch using the default directory: Data /data/ package name /files/odex * * @param context */ public static void loadFixedDex(context context) {loadFixedDex(context, null); } public static void loadFixedDex(context context, loadFixedDex) File patchFilesDir) {// dex Dex doDexInject(context, loadedDex); } /** *@author Minuit *@time 2018/6/25 0025 15:51 *@desc Verify whether a hot fix is required */ public static Boolean isGoingToFix(@nonnull) Context context) { boolean canFix = false; File externalStorageDirectory = Environment.getExternalStorageDirectory(); File fileDir = externalStorageDirectory! File fileDir = externalStorageDirectory! = null ? externalStorageDirectory : new File(context.getFilesDir(), DEX_DIR); // data/data/ package name /files/odex (this can be any position) File[] listFiles = filedir.listfiles (); // data/data/ package name /files/odex (this can be any position) File[] listFiles = filedir.listfiles (); for (File file : listFiles) { if (file.getName().startsWith("classes") && (file.getName().endsWith(DEX_SUFFIX) || file.getName().endsWith(APK_SUFFIX) || file.getName().endsWith(JAR_SUFFIX) || file.getName().endsWith(ZIP_SUFFIX))) { loadedDex.add(file); CanFix = true; canFix = true; } } return canFix; } private static void doDexInject(Context appContext, HashSet<File> loadedDex) { String optimizeDir = appContext.getFilesDir().getAbsolutePath() + File.separator + OPTIMIZE_DEX_DIR; // data/data/ package name /files/optimize_dex (this must be the directory of your own program) File fopt = new File(optimizeDir); if (! fopt.exists()) { fopt.mkdirs(); } try {/ / 1. Dex load application Loader PathClassLoader pathLoader = (PathClassLoader) appContext. GetClassLoader (); for (File dex : loadedDex) { // 2. Loader to load the fixed dex file DexClassLoader dexLoader = new DexClassLoader(dex.getabsolutePath (),// Directory where the fixed dex (patch) is located Fopt.getabsolutepath (),// Decompression directory of dex (used for JAR, ZIP, and APK patches) null,// Library pathLoader required to load dex // parent class loader); /** * There are variables in the BaseDexClassLoader: DexPathList pathList * The variable Element[] dexElements * is reflected in sequence */ //3.1 Prepare a reference to pathList Object DexPathList = getPathList(dexLoader); Object pathPathList = getPathList(pathLoader); Object leftDexElements = getDexElements(dexPathList); Object rightDexElements = getDexElements(pathPathList); Object dexElements = combineArray(leftDexElements, rightDexElements); // Override Element in PathList [] dexElements; Assign Object pathList = getPathList(pathLoader); SetField (pathList, pathlist.getClass (), "dexElements", dexElements); } toast.maketext (appContext, "fix done ", toast.length_short).show(); } catch (Exception e) { e.printStackTrace(); Private static void setField(Object obj, Class<? > cl, String field, Object value) throws NoSuchFieldException, IllegalAccessException { Field declaredField = cl.getDeclaredField(field); declaredField.setAccessible(true); declaredField.set(obj, value); Private static Object getField(Object obj, Class<? > cl, String field) throws NoSuchFieldException, IllegalAccessException { Field localField = cl.getDeclaredField(field);  localField.setAccessible(true); return localField.get(obj); } /** * Reflection gets the pathList Object in the classloader */ private static Object getPathList(Object baseDexClassLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList"); Private static Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException { return getField(pathList, pathList.getClass(), "dexElements"); } private static Object combineArray(Object arrayLhs, Object arrayRhs) {Class<? > clazz = arrayLhs.getClass().getComponentType(); int i = Array.getLength(arrayLhs); Int j = array.getLength (arrayRhs); int j = array.getLength (arrayRhs); Int k = I + j; Object result = array.newinstance (clazz, k); Arraycopy (arrayLhs, 0, result, 0, I); // Create an array of type clazz and length k. System.arraycopy(arrayRhs, 0, result, i, j); return result; }}Copy the code

Next, we performed detection and repair work in Splash

        if (FixDexUtil.isGoingToFix(activity)) {            FixDexUtil.loadFixedDex(activity, Environment.getExternalStorageDirectory());        }        new Thread(new Runnable() {            @Override            public void run() {                try {                    Thread.sleep(2000);                    startActivity(new Intent(activity,MainActivity.class));                    finish();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }).start();Copy the code

Next, be sure to uncheck Instance Run in As, because instance Run works the same way As hot fixes, which means that when you re-run the app, it will not install the entire app, only the code you have modified.

Compile and run:

Well, the next step is to fix the bug and put the fixed package into the SD card so that when Splash starts, the dex will be automatically traversed.

2-> Write the fixed dex to locate the bug that appears in BugTest, so we fix the bug first

public void getBug(Context context) {        int i = 10;        int a = 1;        Toast.makeText(context, "Hello,Minuit:" + i / a, Toast.LENGTH_SHORT).show();    }Copy the code

First click Build->Rebuild Project to Rebuild. Once the Build is complete, Find the class file you just modified in app/build/interintermediate/debug/package name/and copy it

Notice, copy it out with the package name, like this

Because the class file in dex is the package name. Class name, so when we do dex file, we also need to talk about the corresponding package name plus. Here’s a decompiled demo as an example:

Decompile the class file, pick out a class and then you’re going to generate the dex file

To package a class file into a dex file, you need the dx directive, which is similar to a Java directive. The dx command also needs to be provided by an application, which is in the Android SDK build-tools directory of the various Android versions.

Next use the command to compile, shift+ right click to open the command line and type the command:

dx --dex --output c:\Users\Administrator\Desktop\dex\classes.dex c:\Users\Administrator\Desktop\dexCopy the code

You can create a classes.dex file on your desktop that is level with the folder you just copied.

Next, copy the dex file to the SD card, of course, if it is a real project to download, of course, to a specific directory

At this point, the dex file of the target will be seen in the detection of Splash interface, return true, and the operation of hot repair (splicing Element array) will start. Of course, there will be no error when entering the main interface again. So, where is the wrong class?? It’s still in one of the dex’s throughout the Elements collection, but it hasn’t had a chance to be called.

If you want to enter the technical group communication, follow the public account and reply “add group” in the background.

Recommended reading:
Hotfix principles hotfix framework comparison and code repair

The principle of Android plug-in Activity plug-in
7 years as a programmer, what has that taught me?