Android Thermal Repair Technology: Analysis of QQ Space Patch Scheme (2)

Keywords: calculator Android Java SDK

In the next few blogs, I'll use a real demo to show you how to implement hot fixes. Specific contents include:

  • How to pack patch packs
  • How to load patch packs through ClassLoader

1. Create Demo

Demo is very simple. Create a demo with only one Activity:

package com.biyan.demo
public class MainActivity extends Activity {

    private Calculator mCal;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mCal = new Calculator();
    }
    public void click(View view) {
        Toast.makeText(this, String.valueOf(mCal.calculate()),Toast.LENGTH_SHORT).show();
    }
}
Public class Caculoator {
    public float calculate() {
        return 1 / 0;
    }
}

The demo code is simple, and it's clear what bug s will occur when it runs, so I won't show you here.

2. Create patch packs

Fix Calculator's bug first.

package com.biyan.demo
Public class Caculoator {
    public float calculate() {
        return 1 / 1;
    }
}

Recompile the project, find the Calculator.class file in the build directory, copy it out, and prepare to pack it. Place it in the same path as the Calculator package name.

Put it in a jar bag:

jar -cvf patch.jar com

Then the corresponding jar package is packed into dex package:

dx --dex --output=patch_dex.jar patch.jar

DX is a tool for jar packages to be packaged into dex packages. It is installed in path-android-sdk/build-tools/version (e.g. 24.0.0)/dx.
patch_dex.jar is the patch package. Next, install it in the sdCard. Next, the application loads the patch package from the sdCard.

3. Loading patches

According to the introduction of the previous blog, the idea of loading patches is as follows:

  • Get the BaseDexClassLoader of the application itself in the onCreate() method of Application, and then get the corresponding dexElements by reflection.
  • Create a new DexClassLoader instance, load the patch package on the sdCard, and get the corresponding dexElements in the same way
  • Merge the two dexElements and then assign the merged dexElements to the BaseDexClassLoader of the application itself using reflection

Next, look at the specific code:

package com.hotpatch.demo;

import android.app.Application;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.DexClassLoader;

/**
 * Created by hp on 2016/4/6.
 */
public class HotPatchApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        // Get the patch and execute the injection if it exists
        String dexPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/patch_dex.jar");
        File file = new File(dexPath);
        if (file.exists()) {
            inject(dexPath);
        } else {
            Log.e("BugFixApplication", dexPath + "Non-existent");
        }
    }

    /**
     * The path of dex to be injected
     *
     * @param path
     */
    private void inject(String path) {
        try {
            // Get dexElements for classes
            Class<?> cl = Class.forName("dalvik.system.BaseDexClassLoader");
            Object pathList = getField(cl, "pathList", getClassLoader());
            Object baseElements = getField(pathList.getClass(), "dexElements", pathList);

            // Get the dexElements of patch_dex (need to load DEX first)
            String dexopt = getDir("dexopt", 0).getAbsolutePath();
            DexClassLoader dexClassLoader = new DexClassLoader(path, dexopt, dexopt, getClassLoader());
            Object obj = getField(cl, "pathList", dexClassLoader);
            Object dexElements = getField(obj.getClass(), "dexElements", obj);

            // Merge two Elements
            Object combineElements = combineArray(dexElements, baseElements);

            // Reassigning the merged Element array to app's classLoader
            setField(pathList.getClass(), "dexElements", pathList, combineElements);

            //======== Here's how to test for successful injection=================
            Object object = getField(pathList.getClass(), "dexElements", pathList);
            int length = Array.getLength(object);
            Log.e("BugFixApplication", "length = " + length);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    /**
     * Obtaining attribute values of objects by reflection
     */
    private Object getField(Class<?> cl, String fieldName, Object object) throws NoSuchFieldException, IllegalAccessException {
        Field field = cl.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(object);
    }

    /**
     * Setting the property value of an object by reflection
     */
    private void setField(Class<?> cl, String fieldName, Object object, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = cl.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object, value);
    }

    /**
     * Merge two arrays by reflection
     */
    private Object combineArray(Object firstArr, Object secondArr) {
        int firstLength = Array.getLength(firstArr);
        int secondLength = Array.getLength(secondArr);
        int length = firstLength + secondLength;

        Class<?> componentType = firstArr.getClass().getComponentType();
        Object newArr = Array.newInstance(componentType, length);
        for (int i = 0; i < length; i++) {
            if (i < firstLength) {
                Array.set(newArr, i, Array.get(firstArr, i));
            } else {
                Array.set(newArr, i, Array.get(secondArr, i - firstLength));
            }
        }
        return newArr;
    }

}

That's all the core code. Next, run the program. The program is still Crash...

The reason is that the class pre-verification problem causes:
- When the apk is installed, the system optimizes the dex file into odex file, which involves a pre-verification process.
- If the static method, private method, override method and constructor of a class refer to other classes, and these classes belong to the same dex file, the class will be called CLASS_ISPREVERIFIED.
- If the CLASS_ISPREVERIFIED class is referenced at runtime by other dex classes, an error will be reported.
- So the problem with referencing another dex class in MainActivity's onCreate() method is that
- Normal subcontracting schemes ensure that related classes are entered into the same dex file
- To make the patch loadable properly, it is necessary to ensure that the class is not labeled CLASS_ISPREVERIFIED. To achieve this goal, it is necessary to insert references to other classes in dex files into the classes after subcontracting.
- To insert references to other classes in the compiled classes, bytecodes need to be manipulated, and the usual solution is stuffing. Common tools are Java asist, asm, etc.

In fact, the key of QQ space patch scheme lies in bytecode injection rather than dex injection. The next blog will cover the details of bytecode injection.

Posted by GetReady on Sat, 06 Jul 2019 19:33:44 -0700