红橙Darren视频笔记 类加载机制(API28) 自己写个热修复

第一部分 类加载机制

一个Activity是如何被Android虚拟机找到的?
在之前的文章
红橙Darren视频笔记 自定义View总集篇(https://blog.csdn.net/u011109881/article/details/113273632)
中 有涉及一点
以ActivityThread.java中的main函数为起点,
其中调用了Looper.loop(); loop方法内部会分发消息dispatchMessage
之后进入handleMessage 走到LAUNCH_ACTIVITY的case
–> handleLaunchActivity(r, null, “LAUNCH_ACTIVITY”);
其中有非常重要的一步
Activity a = performLaunchActivity(r, customIntent);
我们这次重点是看一下这个activity是如何通过ClassLoader得到的

        Activity activity = null;
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            //通过ClassLoader创建activity对象
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
        } catch (Exception e) {
            if (!mInstrumentation.onException(activity, e)) {
                throw new RuntimeException(
                    "Unable to instantiate activity " + component
                    + ": " + e.toString(), e);
            }
        }
    public Activity newActivity(ClassLoader cl, String className,
            Intent intent)
            throws InstantiationException, IllegalAccessException,
            ClassNotFoundException {
        String pkg = intent != null && intent.getComponent() != null
                ? intent.getComponent().getPackageName() : null;
        return getFactory(pkg).instantiateActivity(cl, className, intent);
    }
    public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className,
            @Nullable Intent intent)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        return (Activity) cl.loadClass(className).newInstance();//通过classLoader+反射创建Activity
    }
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

追踪到ClassLoader的loadClass

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);//重点关注
                }
            }
            return c;
    }

那么这个findClass的具体实现是什么呢 我们发现classLoader是个空实现 那么必然是ClassLoader的实现类去实现该方法的下面开始追寻他的实现类
往回走到performLaunchActivity的方法内部
java.lang.ClassLoader cl = appContext.getClassLoader();
appContext是ContextImpl实例 查看对应getClassLoader方法
ContextImpl.java

    public ClassLoader getClassLoader() {
        return mClassLoader != null ? mClassLoader : (mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader());
    }

看mPackageInfo是什么对象
final @NonNull LoadedApk mPackageInfo;
我们看LoadedApk 的getClassLoader方法

    @UnsupportedAppUsage
    public ClassLoader getClassLoader() {
        synchronized (this) {
            if (mClassLoader == null) {
                createOrUpdateClassLoaderLocked(null /*addedPaths*/);
            }
            return mClassLoader;
        }
    }

这个mClassLoader是怎么初始化的呢
通过断点的方式知道它在构造方法中就初始化了

    LoadedApk(ActivityThread activityThread) {
        mActivityThread = activityThread;
        mApplicationInfo = new ApplicationInfo();
        mApplicationInfo.packageName = "android";
        mPackageName = "android";
        mAppDir = null;
        mResDir = null;
        mSplitAppDirs = null;
        mSplitResDirs = null;
        mSplitClassLoaderNames = null;
        mOverlayDirs = null;
        mDataDir = null;
        mDataDirFile = null;
        mDeviceProtectedDataDirFile = null;
        mCredentialProtectedDataDirFile = null;
        mLibDir = null;
        mBaseClassLoader = null;
        mSecurityViolation = false;
        mIncludeCode = true;
        mRegisterPackage = false;
        mClassLoader = ClassLoader.getSystemClassLoader();//这里初始化
        mResources = Resources.getSystem();
        mAppComponentFactory = createAppFactory(mApplicationInfo, mClassLoader);
    }

继续往下跟

    public static ClassLoader getSystemClassLoader() {
        return SystemClassLoader.loader;
    }

    static private class SystemClassLoader {
        public static ClassLoader loader = ClassLoader.createSystemClassLoader();
    }

    private static ClassLoader createSystemClassLoader() {
        String classPath = System.getProperty("java.class.path", ".");
        String librarySearchPath = System.getProperty("java.library.path", "");

        // String[] paths = classPath.split(":");
        // URL[] urls = new URL[paths.length];
        // for (int i = 0; i < paths.length; i++) {
        // try {
        // urls[i] = new URL("file://" + paths[i]);
        // }
        // catch (Exception ex) {
        // ex.printStackTrace();
        // }
        // }
        //
        // return new java.net.URLClassLoader(urls, null);

        // TODO Make this a java.net.URLClassLoader once we have those?
        //最终发现 是一个PathClassLoader
        return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
    }
public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}

我们发现该类和其的父类BaseDexClassLoader了,但是我们都看不到源码,显然ClassLoader中的findClass是定义在父类中了,我们必须要看到完整的安卓源码才知道实现方式

这里有一个在线网站 可以查找完整Android源码,不过貌似只更新到9.0 http://androidxref.com/

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);//here
        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;
    }

33    /**
34     * Hook for customizing how dex files loads are reported.
35     *
36     * This enables the framework to monitor the use of dex files. The
37     * goal is to simplify the mechanism for optimizing foreign dex files and
38     * enable further optimizations of secondary dex files.
39     *
40     * The reporting happens only when new instances of BaseDexClassLoader
41     * are constructed and will be active only after this field is set with
42     * {@link BaseDexClassLoader#setReporter}.
43     */
44    /* @NonNull */ private static volatile Reporter reporter = null;
45
46    private final DexPathList pathList;

这个DexPathList又是个什么呢
其路径位于
/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
我们需要关注他的两参数的findClass方法

472    /**
473     * Finds the named class in one of the dex files pointed at by
474     * this instance. This will find the one in the earliest listed
475     * path element. If the class is found but has not yet been
476     * defined, then this method will define it in the defining
477     * context that this instance was constructed with.
478     *
479     * @param name of class to find
480     * @param suppressed exceptions encountered whilst finding the class
481     * @return the named class or {@code null} if the class is not
482     * found in any of the dex files
483     */
484    public Class<?> findClass(String name, List<Throwable> suppressed) {
485        for (Element element : dexElements) {
486            Class<?> clazz = element.findClass(name, definingContext, suppressed);
487            if (clazz != null) {
488                return clazz;
489            }
490        }
491
492        if (dexElementsSuppressedExceptions != null) {
493            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
494        }
495        return null;
496    }

59    /**
60     * List of dex/resource (class path) elements.
61     * Should be called pathElements, but the Facebook app uses reflection
62     * to modify 'dexElements' (http://b/7726934).
63     */
64    private Element[] dexElements;

现在我们找到最终的findClass调用的地方了
小结一下流程:
PathClassLoader继承自BaseDexClassLoader继承自ClassLoader

ActivityThread.handleLaunchActivity(r, null, “LAUNCH_ACTIVITY”);–>
Activity a = performLaunchActivity(r, customIntent);–>
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);–>
getFactory(pkg).instantiateActivity(cl, className, intent);–>
(Activity) cl.loadClass(className).newInstance–>
ClassLoader.loadClass–>
ClassLoader.findClass–>
BaseDexClassLoader.findClass–>
DexPathList.findClass
红橙Darren视频笔记 类加载机制(API28) 自己写个热修复

第二部分 自己写个热修复

dex热修复的2种方案
1.可以把出错的class 重新打成一个dex包,混淆可能影响该方案。
2.直接下载整个dex包,然后进行插入修复,但是dex包可能比较大
我这里使用第二种方案
原理:通过上面的分析 我们发现Android设备是通过Activity的name查找dex列表来加载不同的Activity的 那么如果我们改写了Android的dex列表,我们就可以实现热修复(使用修复好的Activity的dex包替换存在bug的dex包)
比如DexPathList中存放了dex列表dexElements,它遍历的时候从前向后遍历 那么我们在这个列表初始位置插入正确的dex包,class Loader找到Activity之后就不会继续向后查找存在bug的dex了
下面进入实战阶段

1.获取Android系统中原来的dex list
利用反射获取private Element[] dexElements;这个filed
2.准备dex解压路径 解压路径是拷贝路径的一个子文件夹
拷贝路径:/data/user/0/com.example.learneassyjoke/app_odex/
解压路径:/data/user/0/com.example.learneassyjoke/app_odex/odex/
3.查看是否有dex文件 如果存在 copy到指定目录,只有在app内部的目录才能解压和解析dex文件(这里code有点问题 如果不存在 不应该创建文件)
4.获取copy目录中的所有dex补丁包(*.dex)
5.合并所有补丁dex包以及运行中的dex包
6.将合并的dex包注入classLoader

主体类

public class FixDexManager {
    private final Context mContext;
    private final File mDexDir;//copy路径 /data/user/0/com.example.learneassyjoke/app_odex/
    private final static String TAG = "FixDexManager";

    public FixDexManager(Context context) {
        this.mContext = context;
        // 获取应用可以访问的dex目录
        this.mDexDir = context.getDir("odex", Context.MODE_PRIVATE);
    }

    public void fixBugByDex() throws Exception {
        //1.获取Android系统中原来的dex list
        Object applicationOriginDexElements = loadOriginDex();
        Log.e(TAG, "fixBugByDex: loadOriginDex end ");

        //2.准备dex解压路径 解压路径是拷贝路径的一个子文件夹
        File unzipDirectory = prepareUnzipDir();
        Log.e(TAG, "fixBugByDex: prepareUnzipDir end " + unzipDirectory);

        //3.查看是否有dex文件 如果存在 copy到指定目录,只有在app内部的目录才能解压和解析dex文件
        File srcFile = findDexFiles();
        File destFile = new File(mDexDir,srcFile.getName());
        copyFile(srcFile, destFile);

        //4.获取copy目录中的所有dex补丁包(*.dex)
        List<File> allFixDexFile = loadAllFixDex();
        if (allFixDexFile == null || allFixDexFile.size() == 0) {
            Log.e(TAG, "fixBugByDex: dex size incorrect, return!");
            return;
        } else {
            Log.e(TAG, "fixBugByDex: dex size is " + allFixDexFile.size());
        }
        //5.合并所有补丁dex包以及运行中的dex包
        applicationOriginDexElements = combineDex(allFixDexFile, unzipDirectory, applicationOriginDexElements);
        Log.e(TAG, "fixBugByDex: combineDex end");
        //6.将合并的dex包注入classLoader
        injectDexToClassLoader(applicationOriginDexElements);
        Log.e(TAG, "fixBugByDex: injectDexToClassLoader end");
    }

    /**
     * copy file
     *
     * @param src  source file
     * @param dest target file
     * @throws IOException
     */
    public static void copyFile(File src, File dest) throws IOException {
        FileChannel inFile = null;
        FileChannel outFile = null;
        try {
            if (!dest.exists()) {
                dest.createNewFile();
            }
            inFile = new FileInputStream(src).getChannel();
            outFile = new FileOutputStream(dest).getChannel();
            inFile.transferTo(0, inFile.size(), outFile);
        } finally {
            if (inFile != null) {
                inFile.close();
            }
            if (outFile != null) {
                outFile.close();
            }
        }
    }

    private File findDexFiles() throws Exception {
        File fixFile = new File(mContext.getExternalFilesDir(null), "fix2.dex");
        if (fixFile.exists()) {
            Log.e(TAG, "findDexFiles: " + fixFile.getAbsolutePath());
        } else {
            boolean b = fixFile.mkdir();
            if (b) {
                Log.e(TAG, "create success" + fixFile.getAbsolutePath());
            } else {
                Log.e(TAG, "create failed" + fixFile.getAbsolutePath());
                throw new FileNotFoundException();
            }
        }
        return fixFile;
    }

    private File prepareUnzipDir() {
        File unzipDirectory = new File(mDexDir, "odex");

        if (!unzipDirectory.exists()) {
            boolean res = unzipDirectory.mkdirs();
            if (!res) {
                Log.e(TAG, "prepareUnzipDir: mkdir failed");
            }
        }
        return unzipDirectory;
    }

    private void injectDexToClassLoader(Object applicationOriginDexElements) throws Exception {
        // 1.先获取 pathList
        ClassLoader applicationClassLoader = mContext.getClassLoader();
        Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
        pathListField.setAccessible(true);
        Object pathList = pathListField.get(applicationClassLoader);

        // 2. pathList里面的dexElements
        Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
        dexElementsField.setAccessible(true);
        dexElementsField.set(pathList, applicationOriginDexElements);
    }

    private Object combineDex(List<File> allFixDexFiles, File unzipDirectory, Object applicationOriginDexElements) throws Exception {
        ClassLoader applicationClassLoader = mContext.getClassLoader();
        for (File fixDexFile : allFixDexFiles) {
            // dexPath  dex路径
            // optimizedDirectory  解压路径
            // libraryPath .so文件位置
            // parent 父ClassLoader
            ClassLoader fixDexClassLoader = new BaseDexClassLoader(
                    fixDexFile.getAbsolutePath(),// dex路径  必须要在应用目录下的odex文件中
                    unzipDirectory,// 解压路径
                    null,// .so文件位置
                    applicationClassLoader // 父ClassLoader
            );
            //5.1 解析当前fixDexFile为Elements对象
            Object fixDexElements = getDexElementsByClassLoader(fixDexClassLoader);

            //5.2 把补丁的dexElement 插到 已经运行的 dexElement 的最前面
            //即在applicationClassLoader数组前插入数次解析出的fixDexElements对象
            //核心是数组合并
            applicationOriginDexElements = combineArray(fixDexElements, applicationOriginDexElements);
        }
        return applicationOriginDexElements;
    }

    /**
     * 从classLoader中获取 dexElements
     */
    private Object getDexElementsByClassLoader(ClassLoader classLoader) throws Exception {

        Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
        pathListField.setAccessible(true);
        Object pathList = pathListField.get(classLoader);
        Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
        dexElementsField.setAccessible(true);
        return dexElementsField.get(pathList);
    }

    /**
     * 合并两个数组
     */
    private static Object combineArray(Object arrayHead, Object arrayTail) {
        Class<?> localClass = arrayHead.getClass().getComponentType();
        int i = Array.getLength(arrayHead);
        int j = i + Array.getLength(arrayTail);
        Log.d(TAG, "combineArray: first array length "+i+" second array length "+j);
        Object result = Array.newInstance(localClass, j);
        for (int k = 0; k < j; ++k) {
            if (k < i) {
                Array.set(result, k, Array.get(arrayHead, k));
            } else {
                Array.set(result, k, Array.get(arrayTail, k - i));
            }
        }
        return result;
    }

    private List<File> loadAllFixDex() {
        File[] dexFiles = mDexDir.listFiles();
        if (dexFiles == null || dexFiles.length == 0) {
            Log.e(TAG, "file list is null or file size is 0");
            return null;
        }
        List<File> fixDexFiles = new ArrayList<>();
        for (File dexFile : dexFiles) {
            if (dexFile.getName().endsWith(".dex")) {
                fixDexFiles.add(dexFile);
            }
        }
        return fixDexFiles;
    }

    //TODO 可以和getDexElementsByClassLoader合并
    private Object loadOriginDex() throws Exception {
        ClassLoader applicationClassLoader = mContext.getClassLoader();
        // 1.1 先获取 pathList
        Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
        pathListField.setAccessible(true);
        Object pathList = pathListField.get(applicationClassLoader);

        if (pathList == null) {
            return null;
        }
        // 1.2 pathList里面的dexElements
        Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
        dexElementsField.setAccessible(true);
        return dexElementsField.get(pathList);
    }
}

在application的onCreate中调用fixbug的方法

    //自己写的dex fix热修复
    private void fixDexBug() {
        //准备工作 将修复好的dex 拷贝到 /storage/emulated/0/Android/data/app包名/files/
        FixDexManager fixDexManager = new FixDexManager(this);
        try {
            fixDexManager.fixBugByDex();
            Log.e(TAG, "fixDexBug: 修复成功");
        } catch (Exception e) {
            e.printStackTrace();
            Log.e(TAG, "fixDexBug: 修复失败");
        }
    }

开始测试

准备工作,在Activity中写一个存在问题的界面或者bug
比如我是这么写的

mTextView.setText("This is Bug text ");

运行该程序
之后将该bug修改

mTextView.setText("This is OK text ");

生成apk 解压,这里我发现Android28和29会自动开启multiDexEnabled因此它生成了4个dex文件
红橙Darren视频笔记 类加载机制(API28) 自己写个热修复

那么mainActivity在哪个dex文件呢 这里涉及一点反编译了
下载最新的dex2jar工具包先将dex解析成jar
https://github.com/pxb1988/dex2jar/releases
红橙Darren视频笔记 类加载机制(API28) 自己写个热修复
然后用工具jd-gui查看jar文件 这里正确的情况应该是OK text,我用存在bug的apk做了个测试
工具应该可以从这里下载 我是很早以前下的 不记得从哪里下得了
http://java-decompiler.github.io/
红橙Darren视频笔记 类加载机制(API28) 自己写个热修复
找到存在正确Activity的类的dex文件 将之重命名为fix2.dex (我的程序中是这么写的),copy到/storage/emulated/0/Android/data/app包名/files/
之后重新启动程序,问题即可热修复成功
红橙Darren视频笔记 类加载机制(API28) 自己写个热修复
当然如果不想进行反编译,应该也可以禁止分包 ,即在gradle中设置multiDexEnabled false
然而生成的dex文件虽然只有一个了 但是我反编译后没有找到MainActivity,热修复也没有成功,不清楚问题出在哪里,哪位大神知道原因还望指教。

后记

实际上热修复的代码我早就写的差不多了,但是始终没有测试成功,没有报错没有crash,后面反编译+log+断点调试都没有找到原因,最后我无意间将fixDexBug的调用从MainActivity移动到了Application中就好了。本来我都快放弃了,没成想瞎蒙解决了问题,这里在修复的Activity调用fix方法确实晚了,但是我当时没有考虑过这个问题,一直在FixDexManager调试,发现即使到了注入DexElement的地方都没有发现问题,搞了三个晚上心态都快崩了。这大概就是有心栽花花不开 无心插柳柳成荫,有的时候运气也很重要呀(狗头)

上一篇:Java中读取文件的几种路径配置


下一篇:2. 类加载