美文网首页
Android-类加载和热修复

Android-类加载和热修复

作者: 超人TIGA | 来源:发表于2021-03-19 16:24 被阅读0次

    当我们开发完一个APP,打包成了apk装进了手机,然后启动和使用APP,这一个过程中,必定会使用各种的类和方法,有系统的有自己的,那这些类都是如何加载成功,供我们使用的呢?
    例如有一个A类,我们使用的时候,一般是new A()创建个对象,然后使用,到底是怎么new的?其实就是Android的类加载帮我们做的。

    apk的组成

    懂apk的打包流程或者反编译过apk的都知道,apk里面会有一个或者多个的dex文件,这些都是我们的代码,但是是经过处理的代码,把我们的代码转化成电脑能懂的代码,其实就是字节码。

    安装apk

    这里需要分一下版本,不同的版本安装机制有点区别。
    Android N(7.0)以上的:
    安装apk的时候,不进行任何的预编译(提高安装速度);
    运行的过程中解析执行,并且对经常使用的方法进行优化,就是即时编译(JIT just in time),经过JIT处理的代码,都会记录在一个profile配置文件里;
    最后在手机闲的时候,有一个编译守护进程,会对profile里面的方法进行预先编译(AOT),把这些代码转化为本地机器码。
    Android L(5.0)- Android N:
    安装时直接使用预先编译(AOT),就是把所有代码一次性转化为本地机器码,当需要使用时就可以直接使用了。但是缺点就是第一次安装的时候十分的慢,因为一次性转化全部代码很耗时。
    Android 2.2-4.4:这部分都是使用JIT,就是说都是在运行的时候,需要用什么,就加载什么,好处就是安装贼快,但缺点也明显,每次运行都需要重新编译,浪费资源,例如电量。

    关键类:ClassLoader
    image.png

    我们先了解下类加载的所有类关系。
    ①BootClassLoader:用来加载系统framework层的class文件。
    ②BaseDexClassLoader:衍生出PathClassLoader和DexClassLoader。
    ③ PathClassLoader:Android应用的类加载器,也就是我们写的类,都由这个来加载。
    ④DexClassLoader:这个是加载一些额外的动态类。

    类加载

    安装成功了,打开APP,每当我们使用类,创建对象的时候,类加载器都会帮我们从代码里面找出我们要的那个类,然后加载给我们用。

        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 {
                            //这里是如果父类加载器是null(也就是bootstrap),那这个方法会去查找name指向的这个类是不是由bootstrap加载了,是的话就返回class对象,不是的话返回null
                            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;
        }
    

    可以看到,加载一个类,会分为2步
    1、Class<?> c = findLoadedClass(name);找缓存,如果已经被加载过了,就直接return返回。
    2、如果没有缓存,就根据变量parent是否为null来判断逻辑,如果不为null,就直接调用parent的loadClass方法;如果为null,就调用findBootStrapClassOrNull方法。

    这里值得注意的是这个parent,其实这里使用了双亲委托机制(先把任务交给父类去处理,直到没有父类或者父类处理不了,才自己去尝试处理),我们的类需要加载的时候,是由pathClassLoader处理的,但是!这里双亲委托说的父类,指的不是BaseDexClassLoader,而是BootClassLoader。

    所以这里的逻辑理解应该是:
    先判断当前加载器是否有父类,没有就从Bootstrap里面找,如果没有加载过就自己去执行findClass方法去加载。
    如果有父类,就根据双亲委托机制,递归加载,如果都没有加载过,最后也是交给自己去执行findClass。

    那findClass又做了什么?

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

    ClassLoader中的findClass其实是空实现,也就是说实现交给了子类去实现。那再找BaseDexClassLoader中的。

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
            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;
        }
    

    重点就这一句:Class c = pathList.findClass(name, suppressedExceptions);
    从变量pathList中,根据name来findClass,并且把结果return。那pathList是什么?先看定义

    private final DexPathList pathList;
    

    再看赋值

        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();
            }
        }
    

    可以看到,是一个DexPathList的对象,而且是在BaseDexClassLoader的构造函数里赋值的,而构造函数中的形参dexPath,其实就是dex文件的路径。dex文件是什么?就是开头讲的apk里面的我们写的代码。继续看DexPathList的实现:

        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) {
           ···省略部分代码···
    
            this.definingContext = definingContext;
    
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            // save dexPath for BaseDexClassLoader
            this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                               suppressedExceptions, definingContext, isTrusted);
    
            ···省略部分代码···
        }
    

    通过makeDexElements,把dexPath路径上的文件拆分,变成一个Element数组。

    private Element[] dexElements;
    

    也就是说,DexPathList类型的pathList对象里面,有一个dexElements数组,存放的就是我们dex文件里面的所有代码的类。那再看回pathList的findClass方法:

    //DexPathList类:
    
        public Class<?> findClass(String name, List<Throwable> suppressed) {
            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;
        }
    
    //Element类
            public Class<?> findClass(String name, ClassLoader definingContext,
                    List<Throwable> suppressed) {
                return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                        : null;
            }
    

    遍历Element数组,再根据Element对象内的dexFile和loadClassBinaryName得到的Class,就可以用了,也就是说,我们new的对象,已经成功了。

    双亲委托的作用

    第一、避免了重复加载类,交由父类先处理,可以知道是否被加载过。
    第二、为了安全,因为各种各样的对象都有类加载器来加载,有系统的,有我们自己的,而系统的必须优先度高并且不可改,不然就会有安全性问题。所以父类加载完系统对象,就算我们在代码里自己写一个去修改实现,也是没用。

    APP热修复

    通过上面的知识点,了解到了一个apk是怎么安装并启动加载到ART虚拟机里面的了。既然类的加载,是一个Element数组的遍历,而Element存放的又是dexFile。
    那也就是说:如果APP某个类有bug,我们只需要修复这个类的bug,然后生成一个dex文件,用户下载后,根据逻辑把这个dex文件放在Element数组的第一位,那么根据类加载的逻辑,修复后的类会先加载,而后面有bug的类,由于类名一样,所以就不会再加载了,达到了问题被修复,而不需要重新发包的目的。

    简单例子实现

    增加入口,确保第一时间把这个新的类被加载器加载,不然由于同名的原因,如果另一个同名的类先加载了,那这个就无法修复了。

    public class MyApplication extends Application {
    
        @Override
        protected void attachBaseContext(Context base) {
            super.attachBaseContext(base);
            //执行热修复。 插入补丁dex
            Hotfix.installPatch(this,new File("/sdcard/bugFix.dex"));
        }
    }
    

    根据版本,分别处理

        public static void installPatch(Application application, File patch) {
            //1、获得classloader,PathClassLoader
            ClassLoader classLoader = application.getClassLoader();
    
            List<File> files = new ArrayList<>();
            if (patch.exists()) {
                files.add(patch);
            }
            File dexOptDir = application.getCacheDir();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                try {
                    NewClassLoaderInjector.inject(application, classLoader, files);
                } catch (Throwable throwable) {
                    throwable.printStackTrace();
                }
            } else {
                try {
                    //23 6.0及以上
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        V23.install(classLoader, files, dexOptDir);
                    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                        V19.install(classLoader, files, dexOptDir); //4.4以上
                    } else {  // >= 14
                        V14.install(classLoader, files, dexOptDir);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    

    处理无非就是利用反射,找到BaseDexClassLoader中的pathList,然后找到pathList中的makePathElements方法,得到补丁创建的 Element[],最后合并2个Element数组并修改 classLoader中 pathList的 dexelements。

        private static final class V23 {
    
            private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                                        File optimizedDirectory)
                    throws IllegalArgumentException, IllegalAccessException,
                    NoSuchFieldException, InvocationTargetException, NoSuchMethodException,
                    IOException {
                //找到 pathList
                Field pathListField = ShareReflectUtil.findField(loader, "pathList");
                Object dexPathList = pathListField.get(loader);
    
                ArrayList<IOException> suppressedExceptions = new ArrayList<>();
                // 从 pathList找到 makePathElements 方法并执行
                // 得到补丁创建的 Element[]
                Object[] patchElements = makePathElements(dexPathList,
                        new ArrayList<>(additionalClassPathEntries), optimizedDirectory,
                        suppressedExceptions);
    
                //将原本的 dexElements 与 makePathElements生成的数组合并
                ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", patchElements);
                if (suppressedExceptions.size() > 0) {
                    for (IOException e : suppressedExceptions) {
                        Log.w(TAG, "Exception in makePathElement", e);
                        throw e;
                    }
    
                }
            }
    
            /**
             * 把dex转化为Element数组
             */
            private static Object[] makePathElements(
                    Object dexPathList, ArrayList<File> files, File optimizedDirectory,
                    ArrayList<IOException> suppressedExceptions)
                    throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
                //通过阅读android6、7、8、9源码,都存在makePathElements方法
                Method makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements",
                        List.class, File.class,
                        List.class);
                return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory,
                        suppressedExceptions);
            }
        }
    

    代码太多,不全放了。
    然后MainActivity写点bug,为了方便,我新建了一个类来抛出bug。

    public class MainActivity extends AppCompatActivity {
        private static final String TAG = "MainActivity";
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            ExceptionBug.test();
        }
    }
    
    public class ExceptionBug {
        public static void test() {
            throw new UnsupportedOperationException("this is a exception");
        }
    }
    

    这样就能保证出bug了。

    修复
    public class ExceptionBug {
        public static void test() {
            //throw new UnsupportedOperationException("this is a exception");
        }
    }
    
    注释掉异常的抛出。重新编译项目,注意只是编译项目,不是重新运行到手机。找到这个类的class文件。编译成dex文件。位置在app-build-intermediates-javac image.png
    编译

    找到工具:/Users/chenjy/Library/Android/sdk/build-tools/29.0.3


    image.png

    把这个添加到配置环境里面去。然后回到ExceptionBug.class文件所在的目录。
    然后输入命令:dx --dex --output=bugFix.dex com/cjy/hotfixdemo/ExceptionBug.class
    执行命令后就会生成这个dex文件了,然后把文件放到MyApplication指定的位置那里,也就是/sdcard/bugFix.dex,放到sdcard里。
    再重新打开,就会发现,没抛出异常了。

    值得注意的问题

    1、AndroidQ(10.0)以上,热修复的dex文件,不要放到sdcard中,因为外部存储的访问权限改了,只能看到自己的,所以应该放到私有目录下,或者application中加入android:requestLegacyExternalStorage="true"。
    2、修复的代码一定要先于bug代码被加载,所以这里我直接在application中就调用了,如果先启动的是MAinActivity,已经加载过了ExceptionBug这个类,之后跳转到activity2,再去热修复ExceptionBug类就不行了。
    3、这个只是个简单的例子,实际的热修复没这么简单,这里只不过是通过热修复的例子,来解释实现类加载的流程。

    相关文章

      网友评论

          本文标题:Android-类加载和热修复

          本文链接:https://www.haomeiwen.com/subject/zuhucltx.html