手写hook式插件化框架

2021-01-03  本文已影响0人  yuLiangC

之前介绍了占位式插件化原理:即将插件的context环境替换成宿主的context环境就能加载对应的class和资源。但在使用是有诸多限制,比如插件的页面跳转和资源文件使用时都要特别注意使用宿主的context环境,否则就会出错。而hook式插件话就不需要如此麻烦,要分析其原理,我们需先了解Android进程运行的一些知识。

一.基本原理

1.类加载

外部apk中类的加载

在Android开发中,不管是插件化还是组件化,都是基于Android系统的类加载器ClassLoader来设计的。只不过Android平台上虚拟机运行的是Dex字节码,一种对class文件优化的产物,传统Class文件是一个Java源码文件会生成一个.class文件,而Android是把所有Class文件进行合并、优化,然后再生成一个最终的class.dex,目的是把不同class文件重复的东西只需保留一份,在早期的Android应用开发中,如果不对Android应用进行分dex处理,那么最后一个应用的apk只会有一个dex文件。
常用的类加载器有两种,DexClassLoader和PathClassLoader,它们都继承于BaseDexClassLoader。

// DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {   
 public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {        
            super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}
// PathClassLoader
public class PathClassLoader extends BaseDexClassLoader {    
public PathClassLoader(String dexPath, ClassLoader parent) {     
   super(dexPath, null, null, parent);
    } 
 public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {    
    super(dexPath, null, libraryPath, parent);
    }
}

区别在于调用父类构造器时,DexClassLoader多传了一个optimizedDirectory参数,这个目录必须是内部存储路径,用来缓存系统创建的Dex文件。而PathClassLoader该参数为null,只能加载内部存储目录的Dex文件。
所以我们可以用DexClassLoader去加载外部的apk,用法如下

//第一个参数为apk的文件目录//第二个参数为内部存储目录//第三个为库文件的存储目录//第四个参数为父加载器new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent)

双亲委托机制
ClassLoader调用loadClass方法加载类

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException { 
       //首先从已经加载的类中查找
        Class<?> clazz = findLoadedClass(className);    
    if (clazz == null) {
            ClassNotFoundException suppressed = null;     
           try {   
                //如果没有加载过,先调用父加载器的loadClass
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }      
        if (clazz == null) {        
                try {           
       
                  //父加载器都没有加载,则尝试加载
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);       
                     throw e;
                }
            }
        }    
            return clazz;
    }

可以看出ClassLoader加载类时,先查看自身是否已经加载过该类,如果没有加载过会首先让父加载器去加载,如果父加载器无法加载该类时才会调用自身的findClass方法加载,该机制很大程度上避免了类的重复加载。

DexClassLoader的DexPathList

DexClassLoader重载了findClass方法,在加载类时会调用其内部的DexPathList去加载。DexPathList是在构造DexClassLoader时生成的,其内部包含了DexFile。
DexPathList的loadClass会去遍历DexFile直到找到需要加载的类
有一种热修复技术正是利用了DexClassLoader的加载机制,将需要替换的类添加到dexElements的前面,这样系统会使用先找到的修复过的类。

2.2 单DexClassLoader与多DexClassLoader

通过给插件apk生成相应的DexClassLoader便可以访问其中的类,这边又有两种处理方式,有单DexClassLoader和多DexClassLoader两种结构。
多DexClassLoader
对于每个插件都会生成一个DexClassLoader,当加载该插件中的类时需要通过对应DexClassLoader加载。这样不同插件的类是隔离的,当不同插件引用了同一个类库的不同版本时,不会出问题。
单DexClassLoader
将插件的DexClassLoader中的pathList合并到主工程的DexClassLoader中。这样做的好处时,可以在不同的插件以及主工程间直接互相调用类和方法,并且可以将不同插件的公共模块抽出来放在一个common插件中直接供其他插件使用。Small采用的是这种方式。

互相调用
插件和主工程的互相调用涉及到以下两个问题
插件调用主工程

二.代码演示(单ClassLoader机制)

public class HookApplication extends Application {

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

    private void plugToApplication() {
        try {
            //第一步,找到宿主的dexElements
            PathClassLoader classLoader = (PathClassLoader) getClassLoader();
            Class baseClass = Class.forName("dalvik.system.PathClassLoader");
            Field pathListField = baseClass.getDeclaredField("pathList");
            pathListField.setAccessible(true);
            Object mDexPathList = pathListField.get(classLoader);
            Field dexElementsField = mDexPathList.getClass().getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);
            Object dexElements = dexElementsField.get(mDexPathList);

            //第二步,找到插件的dexElements。需注意插件的classLoader需要新构建,因为路径已经变化了
            File file = new File(Environment.getExternalStorageDirectory() +File.separator+"p.apk");
            String plugPath = file.getAbsolutePath();
            File dir = this.getDir("plugDir", Context.MODE_PRIVATE);
            DexClassLoader dexClassLoader = new DexClassLoader(plugPath,dir.getAbsolutePath(),null,getClassLoader());
            Class baseClass2 = Class.forName("dalvik.system.PathClassLoader");
            Field pathListField2 = baseClass2.getDeclaredField("pathList");
            pathListField2.setAccessible(true);
            Object mDexPathList2 = pathListField2.get(dexClassLoader);
            Field dexElementsField2 = mDexPathList2.getClass().getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);
            Object dexElements2 = dexElementsField2.get(mDexPathList);

            //第三步,创建新的dexElements:将宿主和插件的dexElements合并
            int mainLength = Array.getLength(dexElements);
            int plugLength = Array.getLength(dexElements2);
            int sumLength = mainLength + plugLength;
            Object newDexElements = Array.newInstance(dexElements.getClass().getComponentType(),sumLength);
            for (int i=0;i<sumLength;i++){
                if (i<mainLength){
                    Array.set(newDexElements,i,Array.get(dexElements,i));
                }else {
                    Array.set(newDexElements,i,Array.get(dexElements2,i - mainLength));
                }
            }

            //第四步,将宿主的dexElements替换成新的dexElements
            dexElementsField.set(mDexPathList,newDexElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

如上,将宿主加载的DexElements替换成宿主和插件合起来的DexElements即可。

上一篇下一篇

猜你喜欢

热点阅读