美文网首页
组件化,插件化,热更新

组件化,插件化,热更新

作者: 帝王鲨kingcp | 来源:发表于2019-02-20 20:08 被阅读0次

    组件化

    拆成多个 module 开发就是组件化

    插件化

    App 的部分功能模块在打包时并不以传统方式打包进 apk 文件中,而是以另⼀种形式⼆次封装进 apk内部,或者放在网络上适时下载,在需要的时候动态对这些功能模块进行加载,称之为插件化。这些单独⼆次封装的功能模块 apk ,就称作「插件」,初始安装的 apk 称作「宿主」。插件化是组件化的更进⼀步推进。

    插件化步骤:

    主Apk分析:

    1. 主App打包完成后,会形成dex,image,xml资源
    2. Dex靠PathClassLoader加载
    3. 图片以及xml资源靠Resource加载

    代码实现:

    1. 创建DexClassLoader加载插件代码
    2. 创建Resource加载资源文件
    3. 管理插件Activity生命周期
    插件化基础:反射
    • 使用反射的目的:
      Java 既然提供了可见性关键字 public private 等等,用来限制代码之间的可见性。可见性特性的支持不是为了代码不被坏⼈使用,而是是为了程序开发的简洁性。可见性的支持提供的是 Safety 的安全,而不是 Security 的安全。即,可见性的支持让程序更不容易写出 bug,而不是更不容易被人入侵。反射的支持可以让开发者在可见性的例外场景中,可以突破可见性限制来调⽤自己需要的 API。这是基于对开发者「在使⽤反射时已经足够了解和谨慎」的假设的。所以,可见性的支持不是为了防御外来者⼊侵,因此反射功能的⽀持并没有什么不合理。

    总结一句话:反射可以让程序员抛开代码的限制,任意使用各种方法。

            try {
                Class clz = Class.forName("com.king.reflect.Apple");
                Constructor constructor = clz.getDeclaredConstructors()[0];
                constructor.setAccessible(true);
                Object object = constructor.newInstance();
                Method method = clz.getDeclaredMethod("setPrice", int.class);
                method.setAccessible(true);
                method.invoke(object, 4);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
    
    插件化原理:动态加载

    通过自定义 ClassLoader 来加载新的 dex 文件,从而让程序员原本没有的类可以被使用,这就是插件化的原理。

    • 关于 DEX:
      class: java编译后的文件,每个类对应⼀个class文件
      dex: Dalvik EXecutable 把class打包在⼀起,⼀个dex可以包含多个class文件
      odex: Optimized DEX针对系统的优化,例如某个方法的调用指令,会把虚拟的调用转换为使用具体的 index,这样在执行的时候就不用再查找了
      oat: Optimized Android file Type。使用AOT策略对dex预先编译(解释)成本地指令,这样再运行阶段就不需再经历⼀次解释过程,程序的运行可以更快
      AOT: Ahead-Of-Time compilation预先编译
            File dexfile = new File(getCacheDir() + "/plugin.dex");
            if (!dexfile.exists()) {
                try {
                    InputStream is = getAssets().open("apk/dex/plugin.dex");
                    int size = is.available();
                    byte[] buffer = new byte[size];
                    is.read(buffer);
                    is.close();
    
                    FileOutputStream fos = new FileOutputStream(dexfile);
                    fos.write(buffer);
                    fos.close();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
    
            try {
                DexClassLoader classLoader = new DexClassLoader(dexfile.getPath(), getCacheDir().getPath(), null, null);
                Class pluginUtilsClass = classLoader.loadClass("com.king.reflect.Apple");
                Constructor utilsConstructor = pluginUtilsClass.getDeclaredConstructors()[0];
                Object utils = utilsConstructor.newInstance();
                Method shoutMethod = pluginUtilsClass.getDeclaredMethod("sell");
                shoutMethod.invoke(utils);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    

    DexClassLoader构造方法参数说明:
    dexPath:apk/dex/jar文件路径
    optimizedDirectory:文件解压路径(这个路径下保存的是.dex文件不是.class)
    libraryPath:加载时用到的so库(可能理解有问题)
    parent:父加载器(这个比较重要与Android加载class的机制有关)

        public DexClassLoader(String dexPath, String optimizedDirectory,
                String libraryPath, ClassLoader parent) {
            super(dexPath, new File(optimizedDirectory), libraryPath, parent);
        }
    
    • 需要注册的组件(例如 Activity)如何打开
      解决方式⼀:代理 Activity
      解决方式⼆:欺骗系统
      解决方式三:重写 gradle 打包过程,合并 AndroiManifest.xml

    我也只是稍微了解方式一的方法。首先在宿主apk中留一个注册的ProxyActivity,生命周期的方法由RealActivity来实现,RealActivity通过插件化的反射来实现。

    public class ProxyActivity extends Activity {
        Object realActivity;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            realActivity.onCreate(savedInstanceState);
    
        }
       
    .....
    }
    
    • 资源文件如何加载
      解决方式:自定义 AssetManager 和 Resources 对象,重写getResources() 和 getAssets() 方法。
    public class ProxyActivity extends Activity {
    
        @Override
        public AssetManager getAssets() {
            try {
                Class assetManagerClass = AssetManager.class;
                AssetManager assetManager = (AssetManager) assetManagerClass.newInstance();
                Method addAssetPath = assetManagerClass.getDeclaredMethod("addAssetPath", String.class);
                addAssetPath.invoke(assetManager, "apkPath");
                return assetManager;
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
            return super.getAssets();
        }
    
        @Override
        public Resources getResources() {
            return new Resources(getAssets(), getResources().getDisplayMetrics(), getResources().getConfiguration());
        }
    }
    
    • 插件化的作用
      解决 dex 65535 问题。现在由 multidex 工具来专门解决。
      减小安装包大小。
      动态部署。

    热更新

    不安装新版本的软件,直接从网络下载新功能模块来对软件进行局部更新

    • 热更新和插件化的区别
    1. 插件化的内容在原 App 中没有,而热更新是原 App 中的内容做了改动
    2. 插件化在代码中有固定的入口,而热更新则可能改变任何⼀个位置的代码
    • 热更新的原理
      ClassLoader 的 dex 文件替换
      直接修改字节码
    • loadClass() 的类加载过程(双亲委托):是⼀个带缓存的、从上到下的加载过程(如下图所示):
      灰色部分为缓存:
      双亲委托.png

    对于具体的⼀个 ClassLoader:
    先从自己的缓存中取
    自己没有缓存,就找父ClassLoader 要(parent.loadClass())
    父ClassLoader也没有,就自己加载(findClass())BaseDexClassLoader 或者它的⼦类(DexClassLoader、 PathClassLoader 等)的findClass():通过 pathList.findClass(),它的 pathList.loadClass() 通过 DexPathList 的 dexElements 的 findClass()。

        public Class<?> loadClass(String name) throws ClassNotFoundException {
            return loadClass(name, false);
        }
        //            
        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;
        }
    
    

    所以热更新的关键在于,把补丁 dex 文件加载放进⼀个 Element,并且插入到 dexElements 这个数组的前面(插入到后面的话会被忽略掉)

    • 热更新代码
      因为无法在更新之前就指定要更新谁;所以不能定义新的 ClassLoader,而只能选择对ClassLoader 进行修改,让它能够加载补丁里面的类。因为补丁的类在原先的 App 中已经存在,所以应该把补丁的 Element 对象插入到 dexElements 的前面才行,插入到后面会被忽略掉。

    具体的代码:

    1. 自己用补丁创建⼀个 PathClassLoader
    2. 把补丁 PathClassLoader 里面的 elements 替换到就的里面去, 尽早加载热更新(通用手段是把加载过程放在 Application.attachBaseContext())
                try {
                    ClassLoader classLoader = getClassLoader();
                    Class loaderClass = BaseDexClassLoader.class;
                    Field pathListField = loaderClass.getDeclaredField("pathList");
                    pathListField.setAccessible(true);
                    Object pathListObject = pathListField.get(classLoader);
                    Class pathListClass = pathListObject.getClass();
                    Field dexElementsField = pathListClass.getDeclaredField("dexElements");
                    dexElementsField.setAccessible(true);
                    Object dexElementsObject = dexElementsField.get(pathListObject);
    
                    // classLoader.pathList.dexElements = 热更新内容elements ;
                    PathClassLoader newClassLoader = new PathClassLoader(apk.getPath(), null);
                    Object newPathListObject = pathListField.get(newClassLoader);
                    Object newDexElementsObject = dexElementsField.get(newPathListObject);
    
                    //将newElement添加到Element[]的前面
                    int oldLength = Array.getLength(dexElementsObject);
                    int newLength = Array.getLength(newDexElementsObject);
                    Object concatDexElementsObject = Array.newInstance(dexElementsObject.getClass().getComponentType(), oldLength + newLength);
                    for (int i = 0; i < newLength; i++) {
                        Array.set(concatDexElementsObject, i, Array.get(newDexElementsObject, i));
                    }
                    for (int i = 0; i < oldLength; i++) {
                        Array.set(concatDexElementsObject, newLength + i, Array.get(dexElementsObject, i));
                    }
    
                    dexElementsField.set(pathListObject, concatDexElementsObject);
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
    

    相关文章

      网友评论

          本文标题:组件化,插件化,热更新

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