android hot update (no framework)

Keywords: Android Java jvm Mobile

I. Preface

Recently, I saw the andix hot fix framework of Alibaba, but it seems that it can't support all devices. So, I need to manually implement Android hot fix once, and find out how to achieve it.

Two, principle

When Java's virtual machine JVM runs the code, it loads the. Class bytecode file, while Android's Dalvik/ART virtual machine loads the Dex file, but their working mechanism is the same. They all pass ClassLoader, a ClassLoader. However, Android redefines two classes, DexClassLoader and PathClassLoader, to parse the class. They inherit the BaseDexClassLoader class, which is closed Introduction to these two classes:

1) . PathClassLoader: official document parsing Android uses this class for its system class loader and for its application class loader(s). Use this class as the loader of system class and application class, that is, only apk files that have been installed to Android system can be loaded

2) , DexClassLoader: jar, apk, and dex files can be loaded from outside the storage.

3, Implementation steps

1. First, write a bug class to test the modification effect, and use it in activity,

public class Bugs {
    public static String getBugs(){
        return "Yes bug!";
    }
}
public class TestHotfixActivity extends Activity {
    TextView tv_result;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv_result=(TextView)findViewById(R.id.tv_result);
    }
    public void onPost(View view){
        tv_result.setText(Bugs.getBugs());
    }
}

2. New application and HotFixEngine classes

public class BaseApplication extends Application {
    public static final String FIX_DEX_PATH = "fix_dex";//Path to fixDex storage

    @Override
    public void onCreate() {
        super.onCreate();
        copyDexFileToAppAndFix(this,"patch.dex");
        
        loadAllDex();
    }

    /**
     * Fix the app with the downloaded dex file
     */
    public void loadAllDex() {
        File dexFilePath = getDir(FIX_DEX_PATH, Context.MODE_PRIVATE);
       for (File dexFile:dexFilePath.listFiles()){
           Log.d("======>",dexFile.getAbsolutePath());
           if (dexFile.getAbsolutePath().endsWith("dex") ) {
               new HotFixEngine().loadDex(this, dexFile);
           }
       }
    }
    /**
     * Copy the patch file from SD card to dex directory, or download the patch file code from the server
     */
    public static void copyDexFileToAppAndFix(Context context, String dexFileName) {
        File path = new File(Environment.getExternalStorageDirectory(), dexFileName);
        if (!path.exists()) {
            Toast.makeText(context, "Patch file not found", Toast.LENGTH_SHORT).show();
            return;
        }
        if (!path.getAbsolutePath().endsWith("dex")){
            Toast.makeText(context, "Incorrect patch file format", Toast.LENGTH_SHORT).show();
            return;
        }
        File dexFilePath = context.getDir(FIX_DEX_PATH, Context.MODE_PRIVATE);
        File dexFile = new File(dexFilePath, dexFileName);
        if (dexFile.exists()) {
            dexFile.delete();
        }
        //copy
        InputStream is = null;
        FileOutputStream os = null;
        try {
            is = new FileInputStream(path);
            os = new FileOutputStream(dexFile);
            int len = 0;
            byte[] buffer = new byte[1024];
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            path.delete();//Delete the patch file in the sdcard, or you can download it directly to the app path
            is.close();
            os.close();
        } catch (Exception e) {
            e.printStackTrace();
            Log.d("======>",e.getMessage());
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                    Log.d("======>",e.getMessage());
                }
            }
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                    Log.d("======>",e.getMessage());
                }
            }
        }
    }
}
package com.lemon.testhotfix;

import android.content.Context;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;

import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

public class HotFixEngine {

    public static final String DEX_OPT_DIR = "optimize_dex";//Optimization path of dex
    public static final String DEX_BASECLASSLOADER_CLASS_NAME = "dalvik.system.BaseDexClassLoader";
    public static final String DEX_ELEMENTS_FIELD = "dexElements";//dexElements field in pathList
    public static final String DEX_PATHLIST_FIELD = "pathList";//pathList field in BaseClassLoader
    public static final String FIX_DEX_PATH = "fix_dex";//Path to fixDex storage


    /**
     * Get dexElements in pathList
     *
     * @param obj
     * @return
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    public Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
        return getField(obj, obj.getClass(), DEX_ELEMENTS_FIELD);
    }

    /**
     * fix
     *
     * @param context
     */
    public void loadDex(Context context, File dexFile) {
        if (context == null) {
            Log.d("======>","context==null");
            return;
        }
        File fixDir = context.getDir(FIX_DEX_PATH, Context.MODE_PRIVATE);
        //mrege and fix
        mergeDex(context, fixDir,dexFile);
    }

    /**
     * Gets the value of the pathList field in the specified classloader (DexPathList)
     *
     * @param classLoader
     * @return
     */
    public Object getDexPathListField(Object classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        return getField(classLoader, Class.forName(DEX_BASECLASSLOADER_CLASS_NAME), DEX_PATHLIST_FIELD);
    }

    /**
     * Get the value of a field
     *
     * @return
     */
    public Object getField(Object obj, Class<?> clz, String fieldName) throws NoSuchFieldException, IllegalAccessException {

        Field field = clz.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(obj);

    }

    /**
     * Reassign fields in the specified object
     *
     * @param obj
     * @param claz
     * @param filed
     * @param value
     */
    public void setFiledValue(Object obj, Class<?> claz, String filed, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = claz.getDeclaredField(filed);
        field.setAccessible(true);
        field.set(obj, value);
    }

    /**
     * Merge dex
     *
     * @param context
     * @param fixDexPath
     */
    public void mergeDex(Context context, File fixDexPath, File dexFile) {
        try {
            //Create the optimize path of dex
            File optimizeDir = new File(fixDexPath.getAbsolutePath(), DEX_OPT_DIR);
            if (!optimizeDir.exists()) {
                optimizeDir.mkdir();
            }
            //Load the dex of its own Apk through the PathClassLoader
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
            //Find dex and load it through DexClassLoader
            //dex file path, optimized output path, null, parent loader
            DexClassLoader dexClassLoader = new DexClassLoader(dexFile.getAbsolutePath(), optimizeDir.getAbsolutePath(), null, pathClassLoader);
            //Get the pathList field in the BaseDexClassLoader of the app itself
            Object appDexPathList = getDexPathListField(pathClassLoader);
            //Get the pathList field in the BaseDexClassLoader of the patch
            Object fixDexPathList = getDexPathListField(dexClassLoader);

            Object appDexElements = getDexElements(appDexPathList);
            Object fixDexElements = getDexElements(fixDexPathList);
            //Merge the data of two elements and insert the fixed dex at the front of the array
            Object finalElements = combineArray(fixDexElements, appDexElements);
            //Reassign the dexElements in the dex pathList in the app
            setFiledValue(appDexPathList, appDexPathList.getClass(), DEX_ELEMENTS_FIELD, finalElements);
            Log.d("======>","Repair success!");

        } catch (Exception e) {
            e.printStackTrace();
            Log.d("======>",e.getMessage());
        }
    }

    /**
     * Merge two arrays
     *
     * @param arrayLhs
     * @param arrayRhs
     * @return
     */
    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class<?> localClass = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);
        int j = i + Array.getLength(arrayRhs);
        Object result = Array.newInstance(localClass, j);
        for (int k = 0; k < j; ++k) {
            if (k < i) {
                Array.set(result, k, Array.get(arrayLhs, k));
            } else {
                Array.set(result, k, Array.get(arrayRhs, k - i));
            }
        }
        return result;
    }

}

3. Package or run directly on the mobile phone or simulator, and then change the method of Bugs: Bugs. Getbugs() {return "has been fixed! ";}, recompile.

4. Find Bugs.class and use SDK / build tools / dx.bat to convert it to a dex file.

dx --dex --output=patch.dex package name / Bugs.class

5. Send patch.dex to the root of the phone. Restart the application.

Posted by baselineace on Sat, 04 Jan 2020 11:58:13 -0800